Skip to content

Commit e6b8520

Browse files
committed
Introduce light/dark theme
1 parent 49b71ba commit e6b8520

3 files changed

Lines changed: 512 additions & 195 deletions

File tree

README.md

Lines changed: 112 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,31 +2,139 @@
22

33
Next.js app for building Eurorack and VCV Rack patch diagrams with PATCH & TWEAK patch symbols.
44

5-
## What is included
5+
## Current scope
66

77
- `91` SVG symbol assets loaded directly from [`public/symbols`](/Users/walmik/Github/pnt/public/symbols)
8-
- A drag-and-drop patch canvas with pan, zoom, fit-to-content, and grid snapping
8+
- A drag-and-drop patch canvas with pan, zoom, reset view, fit-to-content, and grid snapping
9+
- Tool modes for selection, cable patching, and inline text annotations
910
- Cable drawing with sound, modulation, gate/trigger, clock, and pitch cable colors
1011
- IndexedDB-backed local patch library plus JSON import/export
11-
- A sample patch seeded from the visual example you shared
12+
- SVG and PNG export
13+
- A bundled sample patch
1214

1315
## Commands
1416

1517
```bash
1618
npm install
1719
npm run dev
20+
npm run build
1821
```
1922

23+
## Complexity
24+
25+
This is still a relatively small application from an architecture perspective, but most of the behavior is concentrated in [`components/PatchEditor.jsx`](/Users/walmik/Github/pnt/components/PatchEditor.jsx). That file is the main complexity hotspot and the place contributors should read first before making editor changes.
26+
27+
## Architecture
28+
29+
The app is intentionally simple and mostly client-side:
30+
31+
- [`app/page.jsx`](/Users/walmik/Github/pnt/app/page.jsx) is the route entry and renders the editor
32+
- [`app/layout.jsx`](/Users/walmik/Github/pnt/app/layout.jsx) defines metadata, global CSS, and analytics
33+
- [`components/PatchEditor.jsx`](/Users/walmik/Github/pnt/components/PatchEditor.jsx) contains the editor state machine, canvas interactions, persistence hooks, export logic, and most UI
34+
- [`components/SymbolIcon.jsx`](/Users/walmik/Github/pnt/components/SymbolIcon.jsx) renders symbol assets from `public/symbols`
35+
- [`lib/symbols.js`](/Users/walmik/Github/pnt/lib/symbols.js) is the symbol catalog and filename contract for SVG assets
36+
- [`lib/patchLibrary.js`](/Users/walmik/Github/pnt/lib/patchLibrary.js) wraps IndexedDB for named local patch storage
37+
- [`public/symbols`](/Users/walmik/Github/pnt/public/symbols) contains the official PATCH & TWEAK SVG assets used by the UI and export pipeline
38+
39+
## How The Editor Works
40+
41+
The editor is built around a few core state objects:
42+
43+
- `nodes`: placed symbols on the stage
44+
- `connections`: cables between node ids, each with a signal color/type
45+
- `view`: camera state with `x`, `y`, and `scale`
46+
47+
Each node also stores annotation data:
48+
49+
- `note`: a freeform note attached to the symbol itself
50+
- `portNotes`: optional notes attached to `top`, `right`, `bottom`, and `left` patch points
51+
52+
The stage is a large fixed virtual canvas:
53+
54+
- node positions are stored in world coordinates
55+
- panning and zooming only change the camera transform
56+
- drag/drop, cable creation, marquee selection, export, and note placement all resolve through the same world-coordinate model
57+
58+
Connections are constrained by signal type:
59+
60+
- `sound` runs horizontally from `right -> left`
61+
- `modulation`, `gate`, `clock`, and `pitch` run vertically from `top -> bottom`
62+
63+
Tool modes are explicit:
64+
65+
- `Select` is the normal mode for selecting, marquee, and moving nodes
66+
- cable color tools activate connection creation for that signal type
67+
- `Text` lets contributors add notes directly to symbols or patch points
68+
69+
## Data model
70+
71+
Patch files and stored patches share the same basic structure:
72+
73+
```json
74+
{
75+
"version": 1,
76+
"name": "Patch name",
77+
"nodes": [
78+
{
79+
"id": "node-id",
80+
"symbolId": "low-pass-filter",
81+
"x": 288,
82+
"y": 64,
83+
"note": "optional symbol note",
84+
"portNotes": {
85+
"top": "",
86+
"right": "",
87+
"bottom": "",
88+
"left": ""
89+
}
90+
}
91+
],
92+
"connections": [
93+
{
94+
"id": "connection-id",
95+
"from": "node-a",
96+
"to": "node-b",
97+
"color": "sound"
98+
}
99+
],
100+
"view": {
101+
"x": 0,
102+
"y": 0,
103+
"scale": 1
104+
}
105+
}
106+
```
107+
108+
## Persistence and export
109+
110+
- browser autosave uses `localStorage`
111+
- named patches are stored in IndexedDB through [`lib/patchLibrary.js`](/Users/walmik/Github/pnt/lib/patchLibrary.js)
112+
- JSON import/export is kept for portability and backup
113+
- SVG export builds a standalone document from the current patch state
114+
- PNG export rasterizes that SVG in-browser
115+
20116
## Important files
21117

22118
- [`components/PatchEditor.jsx`](/Users/walmik/Github/pnt/components/PatchEditor.jsx)
23119
- [`components/SymbolIcon.jsx`](/Users/walmik/Github/pnt/components/SymbolIcon.jsx)
24120
- [`lib/symbols.js`](/Users/walmik/Github/pnt/lib/symbols.js)
121+
- [`lib/patchLibrary.js`](/Users/walmik/Github/pnt/lib/patchLibrary.js)
122+
- [`app/layout.jsx`](/Users/walmik/Github/pnt/app/layout.jsx)
123+
- [`app/globals.css`](/Users/walmik/Github/pnt/app/globals.css)
25124
- [`public/symbols`](/Users/walmik/Github/pnt/public/symbols)
125+
- [`.github/workflows/deploy-pages.yml`](/Users/walmik/Github/pnt/.github/workflows/deploy-pages.yml)
126+
127+
## Contribution notes
128+
129+
- if you add or rename a symbol, update [`lib/symbols.js`](/Users/walmik/Github/pnt/lib/symbols.js) and keep the filename in [`public/symbols`](/Users/walmik/Github/pnt/public/symbols) aligned with the symbol id
130+
- most editor behavior currently lives in one file, [`components/PatchEditor.jsx`](/Users/walmik/Github/pnt/components/PatchEditor.jsx), so changes there should be tested across drag, selection, patching, text mode, save/load, and export
131+
- the project currently favors straightforward state and local helper functions over deeper abstraction; if the editor keeps growing, a sensible next refactor would be to split canvas math, export logic, and persistence concerns into separate modules
132+
- because this is a static-exported Next.js app, anything that relies on browser APIs must stay in client components or client-only helpers
26133

27134
## Reference
28135

29-
The app expects PATCH & TWEAK SVG files to live in [`public/symbols`](/Users/walmik/Github/pnt/public/symbols) using the symbol ids from [`lib/symbols.js`](/Users/walmik/Github/pnt/lib/symbols.js) as filenames.
136+
- the app expects PATCH & TWEAK SVG files to live in [`public/symbols`](/Users/walmik/Github/pnt/public/symbols) using the symbol ids from [`lib/symbols.js`](/Users/walmik/Github/pnt/lib/symbols.js) as filenames
137+
- deployment is handled through GitHub Pages via [`.github/workflows/deploy-pages.yml`](/Users/walmik/Github/pnt/.github/workflows/deploy-pages.yml)
30138

31139
## Credits
32140

app/globals.css

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,17 @@
1414
--radius: 22px;
1515
}
1616

17+
html.theme-dark {
18+
--bg: #11171c;
19+
--panel: rgba(25, 31, 38, 0.88);
20+
--panel-strong: #1b2229;
21+
--ink: #eef3f7;
22+
--muted: #a9b6c3;
23+
--line: rgba(238, 243, 247, 0.14);
24+
--shadow: 0 24px 70px rgba(0, 0, 0, 0.35);
25+
--card-shadow: 0 14px 34px rgba(0, 0, 0, 0.32);
26+
}
27+
1728
* {
1829
box-sizing: border-box;
1930
}
@@ -22,6 +33,7 @@ html,
2233
body {
2334
margin: 0;
2435
height: 100%;
36+
color-scheme: light;
2537
background:
2638
radial-gradient(circle at top left, rgba(255, 255, 255, 0.55), transparent 32%),
2739
radial-gradient(circle at bottom right, rgba(11, 136, 216, 0.15), transparent 28%),
@@ -186,6 +198,7 @@ button {
186198
font-size: 0.96rem;
187199
font-weight: 700;
188200
line-height: 1.2;
201+
color: var(--ink);
189202
}
190203

191204
.symbol-category {
@@ -205,6 +218,11 @@ button {
205218
}
206219

207220
.toolbar {
221+
display: grid;
222+
gap: 8px;
223+
}
224+
225+
.toolbar-row {
208226
display: flex;
209227
flex-wrap: wrap;
210228
gap: 10px;
@@ -278,6 +296,10 @@ button {
278296
font-size: 0.92rem;
279297
}
280298

299+
.toolbar-spacer {
300+
flex: 1 1 auto;
301+
}
302+
281303
.canvas {
282304
position: relative;
283305
height: 100%;
@@ -702,6 +724,108 @@ button {
702724
color: #9b1e23;
703725
}
704726

727+
html.theme-dark,
728+
html.theme-dark body {
729+
background:
730+
radial-gradient(circle at top left, rgba(255, 255, 255, 0.06), transparent 28%),
731+
radial-gradient(circle at bottom right, rgba(11, 136, 216, 0.16), transparent 26%),
732+
linear-gradient(180deg, #121920 0%, #0b1015 100%);
733+
}
734+
735+
html.theme-dark .panel {
736+
border-color: rgba(255, 255, 255, 0.08);
737+
}
738+
739+
html.theme-dark .search-input,
740+
html.theme-dark .select-input,
741+
html.theme-dark .note-textarea,
742+
html.theme-dark .port-note-input,
743+
html.theme-dark .toolbar button,
744+
html.theme-dark .toolbar select,
745+
html.theme-dark .button-row button,
746+
html.theme-dark .patch-library-open,
747+
html.theme-dark .patch-library-delete,
748+
html.theme-dark .zoom-controls,
749+
html.theme-dark .legend-swatch {
750+
background: rgba(255, 255, 255, 0.06);
751+
color: var(--ink);
752+
border-color: var(--line);
753+
}
754+
755+
html.theme-dark .symbol-button {
756+
background: rgba(255, 255, 255, 0.05);
757+
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
758+
}
759+
760+
html.theme-dark .symbol-button:hover,
761+
html.theme-dark .legend-swatch:hover {
762+
border-color: rgba(255, 255, 255, 0.14);
763+
}
764+
765+
html.theme-dark .canvas {
766+
border-color: rgba(255, 255, 255, 0.08);
767+
background: linear-gradient(180deg, rgba(15, 20, 26, 0.92), rgba(11, 16, 21, 0.96));
768+
}
769+
770+
html.theme-dark .canvas-stage {
771+
background:
772+
linear-gradient(rgba(255, 255, 255, 0.08) 1px, transparent 1px),
773+
linear-gradient(90deg, rgba(255, 255, 255, 0.08) 1px, transparent 1px),
774+
linear-gradient(180deg, rgba(255, 255, 255, 0.03), rgba(255, 255, 255, 0.01));
775+
box-shadow:
776+
inset 0 0 0 2px rgba(255, 255, 255, 0.12),
777+
0 0 0 1px rgba(255, 255, 255, 0.05);
778+
}
779+
780+
html.theme-dark .canvas-empty {
781+
color: rgba(238, 243, 247, 0.42);
782+
}
783+
784+
html.theme-dark .node-card,
785+
html.theme-dark .palette-drag-preview {
786+
border-color: rgba(255, 255, 255, 0.1);
787+
background: rgba(30, 37, 44, 0.92);
788+
}
789+
790+
html.theme-dark .node-note-badge,
791+
html.theme-dark .port-note {
792+
color: var(--ink);
793+
background: rgba(30, 37, 44, 0.96);
794+
border-color: rgba(255, 255, 255, 0.12);
795+
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.28);
796+
}
797+
798+
html.theme-dark .anchor {
799+
border-color: #dbe5ec;
800+
box-shadow: 0 0 0 2px rgba(11, 16, 21, 0.85), 0 0 0 1px rgba(255, 255, 255, 0.12);
801+
background: #f3f6f8;
802+
}
803+
804+
html.theme-dark .anchor.output {
805+
background: #11161b;
806+
border-color: #f3f6f8;
807+
box-shadow: 0 0 0 2px rgba(243, 246, 248, 0.3), 0 0 0 1px rgba(255, 255, 255, 0.1);
808+
}
809+
810+
html.theme-dark .selection-box {
811+
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.18);
812+
}
813+
814+
html.theme-dark .legend-swatch.selected {
815+
background: rgba(255, 255, 255, 0.1);
816+
box-shadow: inset 0 0 0 2px rgba(255, 255, 255, 0.12);
817+
}
818+
819+
html.theme-dark .button-row .primary-button {
820+
background: #f0f5f8;
821+
color: #12171c;
822+
border-color: #f0f5f8;
823+
}
824+
825+
html.theme-dark .button-row .primary-button:hover {
826+
background: #ffffff;
827+
}
828+
705829
@media (max-width: 1100px) {
706830
body {
707831
overflow: auto;

0 commit comments

Comments
 (0)