jaml-ui 0.14.2 → 0.14.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/DESIGN.md +197 -0
  2. package/dist/components/AnalyzerExplorer.js +0 -11
  3. package/dist/components/JamlAestheticSelector.js +1 -3
  4. package/dist/components/JamlAnalyzerFullscreen.js +3 -3
  5. package/dist/components/JamlIde.js +0 -2
  6. package/dist/components/JamlSeedInput.js +1 -2
  7. package/dist/components/JamlSpeedometer.js +1 -1
  8. package/dist/ui/codeBlock.js +1 -1
  9. package/dist/ui/jimboCopyRow.js +1 -2
  10. package/dist/ui/jimboFilterBar.js +2 -4
  11. package/dist/ui/showcase.js +4 -4
  12. package/package.json +8 -5
  13. package/assets/Balatro Seed Curator (DesignsV2)/.design-canvas.state.json +0 -1
  14. package/assets/Balatro Seed Curator (DesignsV2)/Assets/BlindChips.png +0 -0
  15. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Boosters/Boosters.json +0 -303
  16. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Boosters/boosters.png +0 -0
  17. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Boosters.png +0 -0
  18. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Bosses/BlindChips.png +0 -0
  19. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Bosses/blinds_metadata.json +0 -51
  20. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Decks/8BitDeck.png +0 -0
  21. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Decks/Enhancers.png +0 -0
  22. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Decks/balatro-stake-chips.png +0 -0
  23. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Decks/enhancers_metadata.json +0 -52
  24. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Decks/playing_cards_metadata.json +0 -249
  25. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Decks/stakes.json +0 -19
  26. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Editions.png +0 -0
  27. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Enhancers.png +0 -0
  28. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Jokers/Editions.png +0 -0
  29. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Jokers/Jokers.png +0 -0
  30. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Jokers/jokers.json +0 -1087
  31. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Jokers/stickers.png +0 -0
  32. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Jokers/stickers_metadata.json +0 -25
  33. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Jokers.png +0 -0
  34. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Tags/tags.json +0 -191
  35. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Tags/tags.png +0 -0
  36. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Tarots/Tarots.png +0 -0
  37. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Tarots/planets.json +0 -15
  38. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Tarots/spectrals.json +0 -21
  39. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Tarots/tarots.json +0 -163
  40. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Tarots.png +0 -0
  41. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Vouchers/Vouchers.png +0 -0
  42. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Vouchers/vouchers.json +0 -130
  43. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Vouchers.png +0 -0
  44. package/assets/Balatro Seed Curator (DesignsV2)/Assets/blinds.json +0 -51
  45. package/assets/Balatro Seed Curator (DesignsV2)/Assets/boosters.json +0 -303
  46. package/assets/Balatro Seed Curator (DesignsV2)/Assets/fonts/m6x11plusplus.otf +0 -0
  47. package/assets/Balatro Seed Curator (DesignsV2)/Assets/jokers.json +0 -1087
  48. package/assets/Balatro Seed Curator (DesignsV2)/Assets/planets.json +0 -15
  49. package/assets/Balatro Seed Curator (DesignsV2)/Assets/spectrals.json +0 -21
  50. package/assets/Balatro Seed Curator (DesignsV2)/Assets/stakes.png +0 -0
  51. package/assets/Balatro Seed Curator (DesignsV2)/Assets/stickers.png +0 -0
  52. package/assets/Balatro Seed Curator (DesignsV2)/Assets/tags.json +0 -191
  53. package/assets/Balatro Seed Curator (DesignsV2)/Assets/tags.png +0 -0
  54. package/assets/Balatro Seed Curator (DesignsV2)/Assets/tarots.json +0 -163
  55. package/assets/Balatro Seed Curator (DesignsV2)/Assets/vouchers.json +0 -130
  56. package/assets/Balatro Seed Curator (DesignsV2)/Seed Detail v2.html +0 -40
  57. package/assets/Balatro Seed Curator (DesignsV2)/Seed Detail.html +0 -34
  58. package/assets/Balatro Seed Curator (DesignsV2)/public/fonts/m6x11plusplus.otf +0 -0
  59. package/assets/Balatro Seed Curator (DesignsV2)/src/AntePage.jsx +0 -228
  60. package/assets/Balatro Seed Curator (DesignsV2)/src/SeedDetail.jsx +0 -222
  61. package/assets/Balatro Seed Curator (DesignsV2)/src/app.jsx +0 -35
  62. package/assets/Balatro Seed Curator (DesignsV2)/src/mockData.js +0 -185
  63. package/assets/Balatro Seed Curator (DesignsV2)/src/sprites.jsx +0 -259
  64. package/assets/Balatro Seed Curator (DesignsV2)/src/tokens.js +0 -49
  65. package/assets/Balatro Seed Curator (DesignsV2)/src/v2/AntePageV2.jsx +0 -290
  66. package/assets/Balatro Seed Curator (DesignsV2)/src/v2/BalButton.jsx +0 -107
  67. package/assets/Balatro Seed Curator (DesignsV2)/src/v2/JamlBuilderV2.jsx +0 -594
  68. package/assets/Balatro Seed Curator (DesignsV2)/src/v2/JamlIde.jsx +0 -302
  69. package/assets/Balatro Seed Curator (DesignsV2)/src/v2/SearchResultsV2.jsx +0 -286
  70. package/assets/Balatro Seed Curator (DesignsV2)/src/v2/SeedDetailV2.jsx +0 -336
  71. package/assets/Balatro Seed Curator (DesignsV2)/src/v2/SeedOGCard.jsx +0 -251
  72. package/assets/Balatro Seed Curator (DesignsV2)/src/v2/Showcase.jsx +0 -131
  73. package/assets/Balatro Seed Curator (DesignsV2)/src/v2/app.jsx +0 -55
  74. package/assets/Balatro Seed Curator (DesignsV2)/src/v2/data.js +0 -296
  75. package/assets/Balatro Seed Curator (DesignsV2)/starters/design-canvas.jsx +0 -622
  76. package/assets/Balatro Seed Curator (DesignsV2)/uploads/8BitDeck.png +0 -0
  77. package/assets/Balatro Seed Curator (DesignsV2)/uploads/BlindChips.png +0 -0
  78. package/assets/Balatro Seed Curator (DesignsV2)/uploads/Boosters.png +0 -0
  79. package/assets/Balatro Seed Curator (DesignsV2)/uploads/Editions.png +0 -0
  80. package/assets/Balatro Seed Curator (DesignsV2)/uploads/Enhancers.png +0 -0
  81. package/assets/Balatro Seed Curator (DesignsV2)/uploads/Jokers.png +0 -0
  82. package/assets/Balatro Seed Curator (DesignsV2)/uploads/Tarots.png +0 -0
  83. package/assets/Balatro Seed Curator (DesignsV2)/uploads/pasted-1776749540653-0.png +0 -0
  84. package/assets/Balatro Seed Curator (DesignsV2)/uploads/pasted-1776749644934-0.png +0 -0
  85. package/assets/Balatro Seed Curator (DesignsV2)/uploads/pasted-1776749661871-0.png +0 -0
  86. package/assets/Balatro Seed Curator (DesignsV2)/uploads/pasted-1776749674748-0.png +0 -0
  87. package/assets/Balatro Seed Curator (DesignsV2)/uploads/pasted-1776749703076-0.png +0 -0
  88. package/assets/Balatro Seed Curator (DesignsV2)/uploads/pasted-1776749882759-0.png +0 -0
  89. package/assets/Balatro Seed Curator (DesignsV2)/uploads/pasted-1776750354200-0.png +0 -0
  90. package/assets/Balatro Seed Curator (DesignsV2)/uploads/pasted-1776750733265-0.png +0 -0
  91. package/assets/Balatro Seed Curator (DesignsV2)/uploads/pasted-1776751928925-0.png +0 -0
  92. package/assets/Balatro Seed Curator (DesignsV2)/uploads/pasted-1776800975060-0.png +0 -0
  93. package/assets/Balatro Seed Curator (DesignsV2)/uploads/stickers.png +0 -0
  94. package/assets/Balatro Seed Curator (DesignsV2)/uploads/tags.png +0 -0
@@ -1,622 +0,0 @@
1
-
2
- // DesignCanvas.jsx — Figma-ish design canvas wrapper
3
- // Warm gray grid bg + Sections + Artboards + PostIt notes.
4
- // Artboards are reorderable (grip-drag), labels/titles are inline-editable,
5
- // and any artboard can be opened in a fullscreen focus overlay (←/→/Esc).
6
- // State persists to a .design-canvas.state.json sidecar via the host
7
- // bridge. No assets, no deps.
8
- //
9
- // Usage:
10
- // <DesignCanvas>
11
- // <DCSection id="onboarding" title="Onboarding" subtitle="First-run variants">
12
- // <DCArtboard id="a" label="A · Dusk" width={260} height={480}>…</DCArtboard>
13
- // <DCArtboard id="b" label="B · Minimal" width={260} height={480}>…</DCArtboard>
14
- // </DCSection>
15
- // </DesignCanvas>
16
-
17
- const DC = {
18
- bg: '#f0eee9',
19
- grid: 'rgba(0,0,0,0.06)',
20
- label: 'rgba(60,50,40,0.7)',
21
- title: 'rgba(40,30,20,0.85)',
22
- subtitle: 'rgba(60,50,40,0.6)',
23
- postitBg: '#fef4a8',
24
- postitText: '#5a4a2a',
25
- font: '-apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif',
26
- };
27
-
28
- // One-time CSS injection (classes are dc-prefixed so they don't collide with
29
- // the hosted design's own styles).
30
- if (typeof document !== 'undefined' && !document.getElementById('dc-styles')) {
31
- const s = document.createElement('style');
32
- s.id = 'dc-styles';
33
- s.textContent = [
34
- '.dc-editable{cursor:text;outline:none;white-space:nowrap;border-radius:3px;padding:0 2px;margin:0 -2px}',
35
- '.dc-editable:focus{background:#fff;box-shadow:0 0 0 1.5px #c96442}',
36
- '[data-dc-slot]{transition:transform .18s cubic-bezier(.2,.7,.3,1)}',
37
- '[data-dc-slot].dc-dragging{transition:none;z-index:10;pointer-events:none}',
38
- '[data-dc-slot].dc-dragging .dc-card{box-shadow:0 12px 40px rgba(0,0,0,.25),0 0 0 2px #c96442;transform:scale(1.02)}',
39
- '.dc-card{transition:box-shadow .15s,transform .15s}',
40
- '.dc-card *{scrollbar-width:none}',
41
- '.dc-card *::-webkit-scrollbar{display:none}',
42
- '.dc-labelrow{display:flex;align-items:center;gap:4px;height:24px}',
43
- '.dc-grip{cursor:grab;display:flex;align-items:center;padding:5px 4px;border-radius:4px;transition:background .12s}',
44
- '.dc-grip:hover{background:rgba(0,0,0,.08)}',
45
- '.dc-grip:active{cursor:grabbing}',
46
- '.dc-labeltext{cursor:pointer;border-radius:4px;padding:3px 6px;display:flex;align-items:center;transition:background .12s}',
47
- '.dc-labeltext:hover{background:rgba(0,0,0,.05)}',
48
- '.dc-expand{position:absolute;bottom:100%;right:0;margin-bottom:5px;z-index:2;opacity:0;transition:opacity .12s,background .12s;',
49
- ' width:22px;height:22px;border-radius:5px;border:none;cursor:pointer;padding:0;',
50
- ' background:transparent;color:rgba(60,50,40,.7);display:flex;align-items:center;justify-content:center}',
51
- '.dc-expand:hover{background:rgba(0,0,0,.06);color:#2a251f}',
52
- '[data-dc-slot]:hover .dc-expand{opacity:1}',
53
- ].join('\n');
54
- document.head.appendChild(s);
55
- }
56
-
57
- const DCCtx = React.createContext(null);
58
-
59
- // ─────────────────────────────────────────────────────────────
60
- // DesignCanvas — stateful wrapper around the pan/zoom viewport.
61
- // Owns runtime state (per-section order, renamed titles/labels, focused
62
- // artboard). Order/titles/labels persist to a .design-canvas.state.json
63
- // sidecar next to the HTML. Reads go via plain fetch() so the saved
64
- // arrangement is visible anywhere the HTML + sidecar are served together
65
- // (omelette preview, direct link, downloaded zip). Writes go through the
66
- // host's window.omelette bridge — editing requires the omelette runtime.
67
- // Focus is ephemeral.
68
- // ─────────────────────────────────────────────────────────────
69
- const DC_STATE_FILE = '.design-canvas.state.json';
70
-
71
- function DesignCanvas({ children, minScale, maxScale, style }) {
72
- const [state, setState] = React.useState({ sections: {}, focus: null });
73
- // Hold rendering until the sidecar read settles so the saved order/titles
74
- // appear on first paint (no source-order flash). didRead gates writes until
75
- // the read settles so the empty initial state can't clobber a slow read;
76
- // skipNextWrite suppresses the one echo-write that would otherwise follow
77
- // hydration.
78
- const [ready, setReady] = React.useState(false);
79
- const didRead = React.useRef(false);
80
- const skipNextWrite = React.useRef(false);
81
-
82
- React.useEffect(() => {
83
- let off = false;
84
- fetch('./' + DC_STATE_FILE)
85
- .then((r) => (r.ok ? r.json() : null))
86
- .then((saved) => {
87
- if (off || !saved || !saved.sections) return;
88
- skipNextWrite.current = true;
89
- setState((s) => ({ ...s, sections: saved.sections }));
90
- })
91
- .catch(() => {})
92
- .finally(() => { didRead.current = true; if (!off) setReady(true); });
93
- const t = setTimeout(() => { if (!off) setReady(true); }, 150);
94
- return () => { off = true; clearTimeout(t); };
95
- }, []);
96
-
97
- React.useEffect(() => {
98
- if (!didRead.current) return;
99
- if (skipNextWrite.current) { skipNextWrite.current = false; return; }
100
- const t = setTimeout(() => {
101
- window.omelette?.writeFile(DC_STATE_FILE, JSON.stringify({ sections: state.sections })).catch(() => {});
102
- }, 250);
103
- return () => clearTimeout(t);
104
- }, [state.sections]);
105
-
106
- // Build registries synchronously from children so FocusOverlay can read
107
- // them in the same render. Only direct DCSection > DCArtboard children are
108
- // walked — wrapping them in other elements opts out of focus/reorder.
109
- const registry = {}; // slotId -> { sectionId, artboard }
110
- const sectionMeta = {}; // sectionId -> { title, subtitle, slotIds[] }
111
- const sectionOrder = [];
112
- React.Children.forEach(children, (sec) => {
113
- if (!sec || sec.type !== DCSection) return;
114
- const sid = sec.props.id ?? sec.props.title;
115
- if (!sid) return;
116
- sectionOrder.push(sid);
117
- const persisted = state.sections[sid] || {};
118
- const srcIds = [];
119
- React.Children.forEach(sec.props.children, (ab) => {
120
- if (!ab || ab.type !== DCArtboard) return;
121
- const aid = ab.props.id ?? ab.props.label;
122
- if (!aid) return;
123
- registry[`${sid}/${aid}`] = { sectionId: sid, artboard: ab };
124
- srcIds.push(aid);
125
- });
126
- const kept = (persisted.order || []).filter((k) => srcIds.includes(k));
127
- sectionMeta[sid] = {
128
- title: persisted.title ?? sec.props.title,
129
- subtitle: sec.props.subtitle,
130
- slotIds: [...kept, ...srcIds.filter((k) => !kept.includes(k))],
131
- };
132
- });
133
-
134
- const api = React.useMemo(() => ({
135
- state,
136
- section: (id) => state.sections[id] || {},
137
- patchSection: (id, p) => setState((s) => ({
138
- ...s,
139
- sections: { ...s.sections, [id]: { ...s.sections[id], ...(typeof p === 'function' ? p(s.sections[id] || {}) : p) } },
140
- })),
141
- setFocus: (slotId) => setState((s) => ({ ...s, focus: slotId })),
142
- }), [state]);
143
-
144
- // Esc exits focus; any outside pointerdown commits an in-progress rename.
145
- React.useEffect(() => {
146
- const onKey = (e) => { if (e.key === 'Escape') api.setFocus(null); };
147
- const onPd = (e) => {
148
- const ae = document.activeElement;
149
- if (ae && ae.isContentEditable && !ae.contains(e.target)) ae.blur();
150
- };
151
- document.addEventListener('keydown', onKey);
152
- document.addEventListener('pointerdown', onPd, true);
153
- return () => {
154
- document.removeEventListener('keydown', onKey);
155
- document.removeEventListener('pointerdown', onPd, true);
156
- };
157
- }, [api]);
158
-
159
- return (
160
- <DCCtx.Provider value={api}>
161
- <DCViewport minScale={minScale} maxScale={maxScale} style={style}>{ready && children}</DCViewport>
162
- {state.focus && registry[state.focus] && (
163
- <DCFocusOverlay entry={registry[state.focus]} sectionMeta={sectionMeta} sectionOrder={sectionOrder} />
164
- )}
165
- </DCCtx.Provider>
166
- );
167
- }
168
-
169
- // ─────────────────────────────────────────────────────────────
170
- // DCViewport — transform-based pan/zoom (internal)
171
- //
172
- // Input mapping (Figma-style):
173
- // • trackpad pinch → zoom (ctrlKey wheel; Safari gesture* events)
174
- // • trackpad scroll → pan (two-finger)
175
- // • mouse wheel → zoom (notched; distinguished from trackpad scroll)
176
- // • middle-drag / primary-drag-on-bg → pan
177
- //
178
- // Transform state lives in a ref and is written straight to the DOM
179
- // (translate3d + will-change) so wheel ticks don't go through React —
180
- // keeps pans at 60fps on dense canvases.
181
- // ─────────────────────────────────────────────────────────────
182
- function DCViewport({ children, minScale = 0.1, maxScale = 8, style = {} }) {
183
- const vpRef = React.useRef(null);
184
- const worldRef = React.useRef(null);
185
- const tf = React.useRef({ x: 0, y: 0, scale: 1 });
186
-
187
- const apply = React.useCallback(() => {
188
- const { x, y, scale } = tf.current;
189
- const el = worldRef.current;
190
- if (el) el.style.transform = `translate3d(${x}px, ${y}px, 0) scale(${scale})`;
191
- }, []);
192
-
193
- React.useEffect(() => {
194
- const vp = vpRef.current;
195
- if (!vp) return;
196
-
197
- const zoomAt = (cx, cy, factor) => {
198
- const r = vp.getBoundingClientRect();
199
- const px = cx - r.left, py = cy - r.top;
200
- const t = tf.current;
201
- const next = Math.min(maxScale, Math.max(minScale, t.scale * factor));
202
- const k = next / t.scale;
203
- // keep the world point under the cursor fixed
204
- t.x = px - (px - t.x) * k;
205
- t.y = py - (py - t.y) * k;
206
- t.scale = next;
207
- apply();
208
- };
209
-
210
- // Mouse-wheel vs trackpad-scroll heuristic. A physical wheel sends
211
- // line-mode deltas (Firefox) or large integer pixel deltas with no X
212
- // component (Chrome/Safari, typically multiples of 100/120). Trackpad
213
- // two-finger scroll sends small/fractional pixel deltas, often with
214
- // non-zero deltaX. ctrlKey is set by the browser for trackpad pinch.
215
- const isMouseWheel = (e) =>
216
- e.deltaMode !== 0 ||
217
- (e.deltaX === 0 && Number.isInteger(e.deltaY) && Math.abs(e.deltaY) >= 40);
218
-
219
- const onWheel = (e) => {
220
- e.preventDefault();
221
- if (isGesturing) return; // Safari: gesture* owns the pinch — discard concurrent wheels
222
- if (e.ctrlKey) {
223
- // trackpad pinch (or explicit ctrl+wheel)
224
- zoomAt(e.clientX, e.clientY, Math.exp(-e.deltaY * 0.01));
225
- } else if (isMouseWheel(e)) {
226
- // notched mouse wheel — fixed-ratio step per click
227
- zoomAt(e.clientX, e.clientY, Math.exp(-Math.sign(e.deltaY) * 0.18));
228
- } else {
229
- // trackpad two-finger scroll — pan
230
- tf.current.x -= e.deltaX;
231
- tf.current.y -= e.deltaY;
232
- apply();
233
- }
234
- };
235
-
236
- // Safari sends native gesture* events for trackpad pinch with a smooth
237
- // e.scale; preferring these over the ctrl+wheel fallback gives a much
238
- // better feel there. No-ops on other browsers. Safari also fires
239
- // ctrlKey wheel events during the same pinch — isGesturing makes
240
- // onWheel drop those entirely so they neither zoom nor pan.
241
- let gsBase = 1;
242
- let isGesturing = false;
243
- const onGestureStart = (e) => { e.preventDefault(); isGesturing = true; gsBase = tf.current.scale; };
244
- const onGestureChange = (e) => {
245
- e.preventDefault();
246
- zoomAt(e.clientX, e.clientY, (gsBase * e.scale) / tf.current.scale);
247
- };
248
- const onGestureEnd = (e) => { e.preventDefault(); isGesturing = false; };
249
-
250
- // Drag-pan: middle button anywhere, or primary button on canvas
251
- // background (anything that isn't an artboard or an inline editor).
252
- let drag = null;
253
- const onPointerDown = (e) => {
254
- const onBg = !e.target.closest('[data-dc-slot], .dc-editable');
255
- if (!(e.button === 1 || (e.button === 0 && onBg))) return;
256
- e.preventDefault();
257
- vp.setPointerCapture(e.pointerId);
258
- drag = { id: e.pointerId, lx: e.clientX, ly: e.clientY };
259
- vp.style.cursor = 'grabbing';
260
- };
261
- const onPointerMove = (e) => {
262
- if (!drag || e.pointerId !== drag.id) return;
263
- tf.current.x += e.clientX - drag.lx;
264
- tf.current.y += e.clientY - drag.ly;
265
- drag.lx = e.clientX; drag.ly = e.clientY;
266
- apply();
267
- };
268
- const onPointerUp = (e) => {
269
- if (!drag || e.pointerId !== drag.id) return;
270
- vp.releasePointerCapture(e.pointerId);
271
- drag = null;
272
- vp.style.cursor = '';
273
- };
274
-
275
- vp.addEventListener('wheel', onWheel, { passive: false });
276
- vp.addEventListener('gesturestart', onGestureStart, { passive: false });
277
- vp.addEventListener('gesturechange', onGestureChange, { passive: false });
278
- vp.addEventListener('gestureend', onGestureEnd, { passive: false });
279
- vp.addEventListener('pointerdown', onPointerDown);
280
- vp.addEventListener('pointermove', onPointerMove);
281
- vp.addEventListener('pointerup', onPointerUp);
282
- vp.addEventListener('pointercancel', onPointerUp);
283
- return () => {
284
- vp.removeEventListener('wheel', onWheel);
285
- vp.removeEventListener('gesturestart', onGestureStart);
286
- vp.removeEventListener('gesturechange', onGestureChange);
287
- vp.removeEventListener('gestureend', onGestureEnd);
288
- vp.removeEventListener('pointerdown', onPointerDown);
289
- vp.removeEventListener('pointermove', onPointerMove);
290
- vp.removeEventListener('pointerup', onPointerUp);
291
- vp.removeEventListener('pointercancel', onPointerUp);
292
- };
293
- }, [apply, minScale, maxScale]);
294
-
295
- const gridSvg = `url("data:image/svg+xml,%3Csvg width='120' height='120' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M120 0H0v120' fill='none' stroke='${encodeURIComponent(DC.grid)}' stroke-width='1'/%3E%3C/svg%3E")`;
296
- return (
297
- <div
298
- ref={vpRef}
299
- className="design-canvas"
300
- style={{
301
- height: '100vh', width: '100vw',
302
- background: DC.bg,
303
- overflow: 'hidden',
304
- overscrollBehavior: 'none',
305
- touchAction: 'none',
306
- position: 'relative',
307
- fontFamily: DC.font,
308
- boxSizing: 'border-box',
309
- ...style,
310
- }}
311
- >
312
- <div
313
- ref={worldRef}
314
- style={{
315
- position: 'absolute', top: 0, left: 0,
316
- transformOrigin: '0 0',
317
- willChange: 'transform',
318
- width: 'max-content', minWidth: '100%',
319
- minHeight: '100%',
320
- padding: '60px 0 80px',
321
- }}
322
- >
323
- <div style={{ position: 'absolute', inset: -6000, backgroundImage: gridSvg, backgroundSize: '120px 120px', pointerEvents: 'none', zIndex: -1 }} />
324
- {children}
325
- </div>
326
- </div>
327
- );
328
- }
329
-
330
- // ─────────────────────────────────────────────────────────────
331
- // DCSection — editable title + h-row of artboards in persisted order
332
- // ─────────────────────────────────────────────────────────────
333
- function DCSection({ id, title, subtitle, children, gap = 48 }) {
334
- const ctx = React.useContext(DCCtx);
335
- const sid = id ?? title;
336
- const all = React.Children.toArray(children);
337
- const artboards = all.filter((c) => c && c.type === DCArtboard);
338
- const rest = all.filter((c) => !(c && c.type === DCArtboard));
339
- const srcOrder = artboards.map((a) => a.props.id ?? a.props.label);
340
- const sec = (ctx && sid && ctx.section(sid)) || {};
341
-
342
- const order = React.useMemo(() => {
343
- const kept = (sec.order || []).filter((k) => srcOrder.includes(k));
344
- return [...kept, ...srcOrder.filter((k) => !kept.includes(k))];
345
- }, [sec.order, srcOrder.join('|')]);
346
-
347
- const byId = Object.fromEntries(artboards.map((a) => [a.props.id ?? a.props.label, a]));
348
-
349
- return (
350
- <div data-dc-section={sid} style={{ marginBottom: 80, position: 'relative' }}>
351
- <div style={{ padding: '0 60px 56px' }}>
352
- <DCEditable tag="div" value={sec.title ?? title}
353
- onChange={(v) => ctx && sid && ctx.patchSection(sid, { title: v })}
354
- style={{ fontSize: 28, fontWeight: 600, color: DC.title, letterSpacing: -0.4, marginBottom: 6, display: 'inline-block' }} />
355
- {subtitle && <div style={{ fontSize: 16, color: DC.subtitle }}>{subtitle}</div>}
356
- </div>
357
- <div style={{ display: 'flex', gap, padding: '0 60px', alignItems: 'flex-start', width: 'max-content' }}>
358
- {order.map((k) => (
359
- <DCArtboardFrame key={k} sectionId={sid} artboard={byId[k]} order={order}
360
- label={(sec.labels || {})[k] ?? byId[k].props.label}
361
- onRename={(v) => ctx && ctx.patchSection(sid, (x) => ({ labels: { ...x.labels, [k]: v } }))}
362
- onReorder={(next) => ctx && ctx.patchSection(sid, { order: next })}
363
- onFocus={() => ctx && ctx.setFocus(`${sid}/${k}`)} />
364
- ))}
365
- </div>
366
- {rest}
367
- </div>
368
- );
369
- }
370
-
371
- // DCArtboard — marker; rendered by DCArtboardFrame via DCSection.
372
- function DCArtboard() { return null; }
373
-
374
- function DCArtboardFrame({ sectionId, artboard, label, order, onRename, onReorder, onFocus }) {
375
- const { id: rawId, label: rawLabel, width = 260, height = 480, children, style = {} } = artboard.props;
376
- const id = rawId ?? rawLabel;
377
- const ref = React.useRef(null);
378
-
379
- // Live drag-reorder: dragged card sticks to cursor; siblings slide into
380
- // their would-be slots in real time via transforms. DOM order only
381
- // changes on drop.
382
- const onGripDown = (e) => {
383
- e.preventDefault(); e.stopPropagation();
384
- const me = ref.current;
385
- // translateX is applied in local (pre-scale) space but pointer deltas and
386
- // getBoundingClientRect().left are screen-space — divide by the viewport's
387
- // current scale so the dragged card tracks the cursor at any zoom level.
388
- const scale = me.getBoundingClientRect().width / me.offsetWidth || 1;
389
- const peers = Array.from(document.querySelectorAll(`[data-dc-section="${sectionId}"] [data-dc-slot]`));
390
- const homes = peers.map((el) => ({ el, id: el.dataset.dcSlot, x: el.getBoundingClientRect().left }));
391
- const slotXs = homes.map((h) => h.x);
392
- const startIdx = order.indexOf(id);
393
- const startX = e.clientX;
394
- let liveOrder = order.slice();
395
- me.classList.add('dc-dragging');
396
-
397
- const layout = () => {
398
- for (const h of homes) {
399
- if (h.id === id) continue;
400
- const slot = liveOrder.indexOf(h.id);
401
- h.el.style.transform = `translateX(${(slotXs[slot] - h.x) / scale}px)`;
402
- }
403
- };
404
-
405
- const move = (ev) => {
406
- const dx = ev.clientX - startX;
407
- me.style.transform = `translateX(${dx / scale}px)`;
408
- const cur = homes[startIdx].x + dx;
409
- let nearest = 0, best = Infinity;
410
- for (let i = 0; i < slotXs.length; i++) {
411
- const d = Math.abs(slotXs[i] - cur);
412
- if (d < best) { best = d; nearest = i; }
413
- }
414
- if (liveOrder.indexOf(id) !== nearest) {
415
- liveOrder = order.filter((k) => k !== id);
416
- liveOrder.splice(nearest, 0, id);
417
- layout();
418
- }
419
- };
420
-
421
- const up = () => {
422
- document.removeEventListener('pointermove', move);
423
- document.removeEventListener('pointerup', up);
424
- const finalSlot = liveOrder.indexOf(id);
425
- me.classList.remove('dc-dragging');
426
- me.style.transform = `translateX(${(slotXs[finalSlot] - homes[startIdx].x) / scale}px)`;
427
- // After the settle transition, kill transitions + clear transforms +
428
- // commit the reorder in the same frame so there's no visual snap-back.
429
- setTimeout(() => {
430
- for (const h of homes) { h.el.style.transition = 'none'; h.el.style.transform = ''; }
431
- if (liveOrder.join('|') !== order.join('|')) onReorder(liveOrder);
432
- requestAnimationFrame(() => requestAnimationFrame(() => {
433
- for (const h of homes) h.el.style.transition = '';
434
- }));
435
- }, 180);
436
- };
437
- document.addEventListener('pointermove', move);
438
- document.addEventListener('pointerup', up);
439
- };
440
-
441
- return (
442
- <div ref={ref} data-dc-slot={id} style={{ position: 'relative', flexShrink: 0 }}>
443
- <div className="dc-labelrow" style={{ position: 'absolute', bottom: '100%', left: -4, marginBottom: 4, color: DC.label }}>
444
- <div className="dc-grip" onPointerDown={onGripDown} title="Drag to reorder">
445
- <svg width="9" height="13" viewBox="0 0 9 13" fill="currentColor"><circle cx="2" cy="2" r="1.1"/><circle cx="7" cy="2" r="1.1"/><circle cx="2" cy="6.5" r="1.1"/><circle cx="7" cy="6.5" r="1.1"/><circle cx="2" cy="11" r="1.1"/><circle cx="7" cy="11" r="1.1"/></svg>
446
- </div>
447
- <div className="dc-labeltext" onClick={onFocus} title="Click to focus">
448
- <DCEditable value={label} onChange={onRename} onClick={(e) => e.stopPropagation()}
449
- style={{ fontSize: 15, fontWeight: 500, color: DC.label, lineHeight: 1 }} />
450
- </div>
451
- </div>
452
- <button className="dc-expand" onClick={onFocus} onPointerDown={(e) => e.stopPropagation()} title="Focus">
453
- <svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round"><path d="M7 1h4v4M5 11H1V7M11 1L7.5 4.5M1 11l3.5-3.5"/></svg>
454
- </button>
455
- <div className="dc-card"
456
- style={{ borderRadius: 2, boxShadow: '0 1px 3px rgba(0,0,0,.08),0 4px 16px rgba(0,0,0,.06)', overflow: 'hidden', width, height, background: '#fff', ...style }}>
457
- {children || <div style={{ height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#bbb', fontSize: 13, fontFamily: DC.font }}>{id}</div>}
458
- </div>
459
- </div>
460
- );
461
- }
462
-
463
- // Inline rename — commits on blur or Enter.
464
- function DCEditable({ value, onChange, style, tag = 'span', onClick }) {
465
- const T = tag;
466
- return (
467
- <T className="dc-editable" contentEditable suppressContentEditableWarning
468
- onClick={onClick}
469
- onPointerDown={(e) => e.stopPropagation()}
470
- onBlur={(e) => onChange && onChange(e.currentTarget.textContent)}
471
- onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); e.currentTarget.blur(); } }}
472
- style={style}>{value}</T>
473
- );
474
- }
475
-
476
- // ─────────────────────────────────────────────────────────────
477
- // Focus mode — overlay one artboard; ←/→ within section, ↑/↓ across
478
- // sections, Esc or backdrop click to exit.
479
- // ─────────────────────────────────────────────────────────────
480
- function DCFocusOverlay({ entry, sectionMeta, sectionOrder }) {
481
- const ctx = React.useContext(DCCtx);
482
- const { sectionId, artboard } = entry;
483
- const sec = ctx.section(sectionId);
484
- const meta = sectionMeta[sectionId];
485
- const peers = meta.slotIds;
486
- const aid = artboard.props.id ?? artboard.props.label;
487
- const idx = peers.indexOf(aid);
488
- const secIdx = sectionOrder.indexOf(sectionId);
489
-
490
- const go = (d) => { const n = peers[(idx + d + peers.length) % peers.length]; if (n) ctx.setFocus(`${sectionId}/${n}`); };
491
- const goSection = (d) => {
492
- const ns = sectionOrder[(secIdx + d + sectionOrder.length) % sectionOrder.length];
493
- const first = sectionMeta[ns] && sectionMeta[ns].slotIds[0];
494
- if (first) ctx.setFocus(`${ns}/${first}`);
495
- };
496
-
497
- React.useEffect(() => {
498
- const k = (e) => {
499
- if (e.key === 'ArrowLeft') { e.preventDefault(); go(-1); }
500
- if (e.key === 'ArrowRight') { e.preventDefault(); go(1); }
501
- if (e.key === 'ArrowUp') { e.preventDefault(); goSection(-1); }
502
- if (e.key === 'ArrowDown') { e.preventDefault(); goSection(1); }
503
- };
504
- document.addEventListener('keydown', k);
505
- return () => document.removeEventListener('keydown', k);
506
- });
507
-
508
- const { width = 260, height = 480, children } = artboard.props;
509
- const [vp, setVp] = React.useState({ w: window.innerWidth, h: window.innerHeight });
510
- React.useEffect(() => { const r = () => setVp({ w: window.innerWidth, h: window.innerHeight }); window.addEventListener('resize', r); return () => window.removeEventListener('resize', r); }, []);
511
- const scale = Math.max(0.1, Math.min((vp.w - 200) / width, (vp.h - 260) / height, 2));
512
-
513
- const [ddOpen, setDd] = React.useState(false);
514
- const Arrow = ({ dir, onClick }) => (
515
- <button onClick={(e) => { e.stopPropagation(); onClick(); }}
516
- style={{ position: 'absolute', top: '50%', [dir]: 28, transform: 'translateY(-50%)',
517
- border: 'none', background: 'rgba(255,255,255,.08)', color: 'rgba(255,255,255,.9)',
518
- width: 44, height: 44, borderRadius: 22, fontSize: 18, cursor: 'pointer',
519
- display: 'flex', alignItems: 'center', justifyContent: 'center', transition: 'background .15s' }}
520
- onMouseEnter={(e) => (e.currentTarget.style.background = 'rgba(255,255,255,.18)')}
521
- onMouseLeave={(e) => (e.currentTarget.style.background = 'rgba(255,255,255,.08)')}>
522
- <svg width="18" height="18" viewBox="0 0 18 18" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
523
- <path d={dir === 'left' ? 'M11 3L5 9l6 6' : 'M7 3l6 6-6 6'} /></svg>
524
- </button>
525
- );
526
-
527
- // Portal to body so position:fixed is the real viewport regardless of any
528
- // transform on DesignCanvas's ancestors (including the canvas zoom itself).
529
- return ReactDOM.createPortal(
530
- <div onClick={() => ctx.setFocus(null)}
531
- onWheel={(e) => e.preventDefault()}
532
- style={{ position: 'fixed', inset: 0, zIndex: 100, background: 'rgba(24,20,16,.6)', backdropFilter: 'blur(14px)',
533
- fontFamily: DC.font, color: '#fff' }}>
534
-
535
- {/* top bar: section dropdown (left) · close (right) */}
536
- <div onClick={(e) => e.stopPropagation()}
537
- style={{ position: 'absolute', top: 0, left: 0, right: 0, height: 72, display: 'flex', alignItems: 'flex-start', padding: '16px 20px 0', gap: 16 }}>
538
- <div style={{ position: 'relative' }}>
539
- <button onClick={() => setDd((o) => !o)}
540
- style={{ border: 'none', background: 'transparent', color: '#fff', cursor: 'pointer', padding: '6px 8px',
541
- borderRadius: 6, textAlign: 'left', fontFamily: 'inherit' }}>
542
- <span style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
543
- <span style={{ fontSize: 18, fontWeight: 600, letterSpacing: -0.3 }}>{meta.title}</span>
544
- <svg width="11" height="11" viewBox="0 0 11 11" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" style={{ opacity: .7 }}><path d="M2 4l3.5 3.5L9 4"/></svg>
545
- </span>
546
- {meta.subtitle && <span style={{ display: 'block', fontSize: 13, opacity: .6, fontWeight: 400, marginTop: 2 }}>{meta.subtitle}</span>}
547
- </button>
548
- {ddOpen && (
549
- <div style={{ position: 'absolute', top: '100%', left: 0, marginTop: 4, background: '#2a251f', borderRadius: 8,
550
- boxShadow: '0 8px 32px rgba(0,0,0,.4)', padding: 4, minWidth: 200, zIndex: 10 }}>
551
- {sectionOrder.map((sid) => (
552
- <button key={sid} onClick={() => { setDd(false); const f = sectionMeta[sid].slotIds[0]; if (f) ctx.setFocus(`${sid}/${f}`); }}
553
- style={{ display: 'block', width: '100%', textAlign: 'left', border: 'none', cursor: 'pointer',
554
- background: sid === sectionId ? 'rgba(255,255,255,.1)' : 'transparent', color: '#fff',
555
- padding: '8px 12px', borderRadius: 5, fontSize: 14, fontWeight: sid === sectionId ? 600 : 400, fontFamily: 'inherit' }}>
556
- {sectionMeta[sid].title}
557
- </button>
558
- ))}
559
- </div>
560
- )}
561
- </div>
562
- <div style={{ flex: 1 }} />
563
- <button onClick={() => ctx.setFocus(null)}
564
- onMouseEnter={(e) => (e.currentTarget.style.background = 'rgba(255,255,255,.12)')}
565
- onMouseLeave={(e) => (e.currentTarget.style.background = 'transparent')}
566
- style={{ border: 'none', background: 'transparent', color: 'rgba(255,255,255,.7)', width: 32, height: 32,
567
- borderRadius: 16, fontSize: 20, cursor: 'pointer', lineHeight: 1, transition: 'background .12s' }}>×</button>
568
- </div>
569
-
570
- {/* card centered, label + index below — only the card itself stops
571
- propagation so any backdrop click (including the margins around
572
- the card) exits focus */}
573
- <div
574
- style={{ position: 'absolute', top: 64, bottom: 56, left: 100, right: 100, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 16 }}>
575
- <div onClick={(e) => e.stopPropagation()} style={{ width: width * scale, height: height * scale, position: 'relative' }}>
576
- <div style={{ width, height, transform: `scale(${scale})`, transformOrigin: 'top left', background: '#fff', borderRadius: 2, overflow: 'hidden',
577
- boxShadow: '0 20px 80px rgba(0,0,0,.4)' }}>
578
- {children || <div style={{ height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#bbb' }}>{aid}</div>}
579
- </div>
580
- </div>
581
- <div onClick={(e) => e.stopPropagation()} style={{ fontSize: 14, fontWeight: 500, opacity: .85, textAlign: 'center' }}>
582
- {(sec.labels || {})[aid] ?? artboard.props.label}
583
- <span style={{ opacity: .5, marginLeft: 10, fontVariantNumeric: 'tabular-nums' }}>{idx + 1} / {peers.length}</span>
584
- </div>
585
- </div>
586
-
587
- <Arrow dir="left" onClick={() => go(-1)} />
588
- <Arrow dir="right" onClick={() => go(1)} />
589
-
590
- {/* dots */}
591
- <div onClick={(e) => e.stopPropagation()}
592
- style={{ position: 'absolute', bottom: 20, left: '50%', transform: 'translateX(-50%)', display: 'flex', gap: 8 }}>
593
- {peers.map((p, i) => (
594
- <button key={p} onClick={() => ctx.setFocus(`${sectionId}/${p}`)}
595
- style={{ border: 'none', padding: 0, cursor: 'pointer', width: 6, height: 6, borderRadius: 3,
596
- background: i === idx ? '#fff' : 'rgba(255,255,255,.3)' }} />
597
- ))}
598
- </div>
599
- </div>,
600
- document.body,
601
- );
602
- }
603
-
604
- // ─────────────────────────────────────────────────────────────
605
- // Post-it — absolute-positioned sticky note
606
- // ─────────────────────────────────────────────────────────────
607
- function DCPostIt({ children, top, left, right, bottom, rotate = -2, width = 180 }) {
608
- return (
609
- <div style={{
610
- position: 'absolute', top, left, right, bottom, width,
611
- background: DC.postitBg, padding: '14px 16px',
612
- fontFamily: '"Comic Sans MS", "Marker Felt", "Segoe Print", cursive',
613
- fontSize: 14, lineHeight: 1.4, color: DC.postitText,
614
- boxShadow: '0 2px 8px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.08)',
615
- transform: `rotate(${rotate}deg)`,
616
- zIndex: 5,
617
- }}>{children}</div>
618
- );
619
- }
620
-
621
- Object.assign(window, { DesignCanvas, DCSection, DCArtboard, DCPostIt });
622
-