@lark-apaas/coding-registry-buildless 0.1.2-alpha.0 → 0.1.2-beta.0
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.
- package/components/android-frame.jsx +0 -1
- package/components/animations.jsx +179 -86
- package/components/browser-window.jsx +1 -1
- package/components/design-canvas.jsx +221 -97
- package/components/ios-frame.jsx +5 -5
- package/components/macos-window.jsx +1 -1
- package/components/tweaks-panel.jsx +7 -0
- package/package.json +1 -1
- package/scripts/deck-stage.js +563 -371
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
/* END USAGE */
|
|
26
26
|
|
|
27
27
|
const DC = {
|
|
28
|
-
bg: '#
|
|
28
|
+
bg: '#FCFCFC',
|
|
29
29
|
label: '#8F959E',
|
|
30
30
|
title: '#1F2329',
|
|
31
31
|
subtitle: '#646A73',
|
|
@@ -51,6 +51,11 @@ if (typeof document !== 'undefined' && !document.getElementById('dc-styles')) {
|
|
|
51
51
|
'.dc-card{isolation:isolate;transition:box-shadow .15s,transform .15s}',
|
|
52
52
|
'.dc-card *{scrollbar-width:none}',
|
|
53
53
|
'.dc-card *::-webkit-scrollbar{display:none}',
|
|
54
|
+
// Focus-overlay content isn't inside .dc-card, so the scrollbar-hiding
|
|
55
|
+
// rules above don't reach it — re-hide scrollbars for the scaled,
|
|
56
|
+
// scrollable content shown in DCFocusOverlay.
|
|
57
|
+
'.dc-focus-content *{scrollbar-width:none}',
|
|
58
|
+
'.dc-focus-content *::-webkit-scrollbar{display:none}',
|
|
54
59
|
// Per-artboard header: grip + label on the left, delete/expand on the
|
|
55
60
|
// right. Single flex row; when the artboard's on-screen width is too
|
|
56
61
|
// narrow for both the label yields (ellipsis, then hidden entirely below
|
|
@@ -77,6 +82,11 @@ if (typeof document !== 'undefined' && !document.getElementById('dc-styles')) {
|
|
|
77
82
|
'.dc-btns{flex:0 0 auto;margin-left:auto;display:flex;gap:2px;opacity:0;transition:opacity .12s}',
|
|
78
83
|
'[data-dc-slot]:hover .dc-header,[data-dc-slot]:hover .dc-header .dc-editable{color:#646A73!important}',
|
|
79
84
|
'[data-dc-slot]:hover .dc-btns,.dc-btns:has(.dc-menu){opacity:1}',
|
|
85
|
+
// On touch devices (no hover) the per-artboard action bar (kebab + expand)
|
|
86
|
+
// can't be reveal-on-hover and tends to stick visible after a tap — hide it
|
|
87
|
+
// entirely on mobile. The grip + label stay for per-screen context.
|
|
88
|
+
'@media (hover: none){.dc-btns{display:none}}',
|
|
89
|
+
'[data-dc-slot]:hover .dc-card.chromeless>*, [data-dc-slot]:hover .dc-card:not(.chromeless){box-shadow:0 4px 8px -8px rgba(0, 0, 0, 0.06), 0 6px 12px 0 rgba(0, 0, 0, 0.04), 0 8px 24px 8px rgba(0, 0, 0, 0.04)}',
|
|
80
90
|
'.dc-expand,.dc-kebab{width:22px;height:22px;border-radius:6px;border:none;cursor:pointer;padding:0;',
|
|
81
91
|
' background:transparent;color:#646A73;display:flex;align-items:center;justify-content:center;',
|
|
82
92
|
' font:inherit;transition:background .12s,color .12s}',
|
|
@@ -110,7 +120,7 @@ if (typeof document !== 'undefined' && !document.getElementById('dc-styles')) {
|
|
|
110
120
|
// constant-screen-size title would overflow into the (shrinking) world-
|
|
111
121
|
// space gap and overlap the artboard headers at low zoom.
|
|
112
122
|
'.dc-header{width:calc((100% + 4px) / var(--dc-inv-zoom,1));',
|
|
113
|
-
' transform:scale(var(--dc-inv-zoom,1));transform-origin:bottom left}',
|
|
123
|
+
' transform:scale(var(--dc-inv-zoom,1));transform-origin:bottom left;will-change:transform}',
|
|
114
124
|
'.dc-sectionhead{zoom:var(--dc-inv-zoom,1)}',
|
|
115
125
|
// On-canvas zoom control (bottom-right pill). Stays screen-fixed because
|
|
116
126
|
// it renders outside worldRef. z-index 50 keeps it above world content but
|
|
@@ -153,12 +163,22 @@ function dcFlatten(children) {
|
|
|
153
163
|
// .design-canvas.state.json
|
|
154
164
|
// sidecar next to the HTML. Reads go via plain fetch() so the saved
|
|
155
165
|
// arrangement is visible anywhere the HTML + sidecar are served together
|
|
156
|
-
// (miaoda preview, direct link, downloaded zip). Writes
|
|
157
|
-
//
|
|
166
|
+
// (miaoda preview, direct link, downloaded zip). Writes post the sidecar
|
|
167
|
+
// content up to the miaoda host via postMessage — editing requires the miaoda runtime.
|
|
158
168
|
// Focus is ephemeral.
|
|
159
169
|
// ─────────────────────────────────────────────────────────────
|
|
160
170
|
const DC_STATE_FILE = '.design-canvas.state.json';
|
|
161
171
|
|
|
172
|
+
// Persist a sidecar file back to the host.
|
|
173
|
+
function miaodaWriteFile(path, content) {
|
|
174
|
+
try {
|
|
175
|
+
window.parent.postMessage({ type: 'miaoda:bridge:write-file', path: path, content: content }, '*');
|
|
176
|
+
} catch (e) {
|
|
177
|
+
/* no parent — ignore */
|
|
178
|
+
}
|
|
179
|
+
return Promise.resolve();
|
|
180
|
+
}
|
|
181
|
+
|
|
162
182
|
function DesignCanvas({ children, minScale, maxScale, style }) {
|
|
163
183
|
const [state, setState] = React.useState({ sections: {}, focus: null });
|
|
164
184
|
// Hold rendering until the sidecar read settles so the saved order/titles
|
|
@@ -189,7 +209,7 @@ function DesignCanvas({ children, minScale, maxScale, style }) {
|
|
|
189
209
|
if (!didRead.current) return;
|
|
190
210
|
if (skipNextWrite.current) { skipNextWrite.current = false; return; }
|
|
191
211
|
const t = setTimeout(() => {
|
|
192
|
-
|
|
212
|
+
miaodaWriteFile(DC_STATE_FILE, JSON.stringify({ sections: state.sections })).catch(() => {});
|
|
193
213
|
}, 250);
|
|
194
214
|
return () => clearTimeout(t);
|
|
195
215
|
}, [state.sections]);
|
|
@@ -364,10 +384,10 @@ function DCZoomControl({ scalePct, minScale, maxScale, onZoomTo }) {
|
|
|
364
384
|
// DCViewport — transform-based pan/zoom (internal)
|
|
365
385
|
//
|
|
366
386
|
// Input mapping (Figma-style):
|
|
367
|
-
// • trackpad pinch
|
|
368
|
-
// •
|
|
369
|
-
// • mouse wheel
|
|
370
|
-
// • middle-drag / primary-drag-on-bg
|
|
387
|
+
// • trackpad pinch → zoom (ctrlKey wheel; Safari gesture* events)
|
|
388
|
+
// • Ctrl/⌘ + wheel → zoom
|
|
389
|
+
// • trackpad two-finger / mouse wheel → pan
|
|
390
|
+
// • middle-drag / primary-drag-on-bg → pan
|
|
371
391
|
//
|
|
372
392
|
// Transform state lives in a ref and is written straight to the DOM
|
|
373
393
|
// (translate3d + will-change) so wheel ticks don't go through React —
|
|
@@ -376,8 +396,9 @@ function DCZoomControl({ scalePct, minScale, maxScale, onZoomTo }) {
|
|
|
376
396
|
function DCViewport({ children, minScale = 0.1, maxScale = 8, style = {} }) {
|
|
377
397
|
const vpRef = React.useRef(null);
|
|
378
398
|
const worldRef = React.useRef(null);
|
|
379
|
-
//
|
|
380
|
-
//
|
|
399
|
+
// Pre-fit fallback scale. A fresh canvas is reframed by the first-visit fit
|
|
400
|
+
// (below) once content mounts; localStorage restore overrides this on
|
|
401
|
+
// revisits. This 50% only shows in the gap before either runs.
|
|
381
402
|
const tf = React.useRef({ x: 0, y: 0, scale: 0.5 });
|
|
382
403
|
// Persist viewport across reloads so the user lands back where they were
|
|
383
404
|
// after an agent edit or browser refresh. The sandbox origin is already
|
|
@@ -425,6 +446,11 @@ function DCViewport({ children, minScale = 0.1, maxScale = 8, style = {} }) {
|
|
|
425
446
|
}, 200);
|
|
426
447
|
}, [tfKey]);
|
|
427
448
|
|
|
449
|
+
// Whether the mount-time restore found a saved viewport — when it did, the
|
|
450
|
+
// first-visit fit below is skipped so revisits keep the user's own view.
|
|
451
|
+
const restoredView = React.useRef(false);
|
|
452
|
+
const didFit = React.useRef(false);
|
|
453
|
+
|
|
428
454
|
React.useLayoutEffect(() => {
|
|
429
455
|
const flush = () => {
|
|
430
456
|
clearTimeout(saveT.current);
|
|
@@ -434,10 +460,12 @@ function DCViewport({ children, minScale = 0.1, maxScale = 8, style = {} }) {
|
|
|
434
460
|
const s = JSON.parse(localStorage.getItem(tfKey) || 'null');
|
|
435
461
|
if (s && Number.isFinite(s.x) && Number.isFinite(s.y) && Number.isFinite(s.scale)) {
|
|
436
462
|
tf.current = { x: s.x, y: s.y, scale: Math.min(maxScale, Math.max(minScale, s.scale)) };
|
|
463
|
+
restoredView.current = true;
|
|
437
464
|
}
|
|
438
465
|
} catch {}
|
|
439
466
|
// Apply before first paint whether we restored or fell back to the default
|
|
440
|
-
// (scale 0.5)
|
|
467
|
+
// (scale 0.5) so the canvas never flashes at the wrong zoom. On a fresh
|
|
468
|
+
// canvas the first-visit fit (below) reframes once content mounts.
|
|
441
469
|
apply();
|
|
442
470
|
// Flush on pagehide and unmount so a reload within the 200ms debounce
|
|
443
471
|
// window doesn't drop the last pan/zoom.
|
|
@@ -445,6 +473,73 @@ function DCViewport({ children, minScale = 0.1, maxScale = 8, style = {} }) {
|
|
|
445
473
|
return () => { window.removeEventListener('pagehide', flush); flush(); };
|
|
446
474
|
}, []);
|
|
447
475
|
|
|
476
|
+
// First-visit framing: once the (ready-gated) content mounts, scale + centre
|
|
477
|
+
// the whole canvas so every artboard fits with a ≥64px safe margin on all
|
|
478
|
+
// sides. Skipped when a saved viewport was restored. Runs as a layout effect
|
|
479
|
+
// on the commit that first paints content, so there's no un-framed flash.
|
|
480
|
+
React.useLayoutEffect(() => {
|
|
481
|
+
if (didFit.current || restoredView.current) return;
|
|
482
|
+
const vp = vpRef.current, world = worldRef.current;
|
|
483
|
+
if (!vp || !world) return;
|
|
484
|
+
const slots = world.querySelectorAll('[data-dc-slot]');
|
|
485
|
+
if (!slots.length) return; // content not mounted yet — wait for the ready commit
|
|
486
|
+
const heads = world.querySelectorAll('.dc-sectionhead');
|
|
487
|
+
const PAD = 64;
|
|
488
|
+
|
|
489
|
+
// Content bounding box in WORLD space at the currently applied transform.
|
|
490
|
+
// X extent from cards only (section-head blocks are full width and would
|
|
491
|
+
// over-report); Y from cards + titles so the top heading is kept in frame.
|
|
492
|
+
const measure = () => {
|
|
493
|
+
const vpr = vp.getBoundingClientRect();
|
|
494
|
+
const t = tf.current;
|
|
495
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
496
|
+
slots.forEach((el) => {
|
|
497
|
+
const r = el.getBoundingClientRect();
|
|
498
|
+
minX = Math.min(minX, (r.left - vpr.left - t.x) / t.scale);
|
|
499
|
+
maxX = Math.max(maxX, (r.right - vpr.left - t.x) / t.scale);
|
|
500
|
+
minY = Math.min(minY, (r.top - vpr.top - t.y) / t.scale);
|
|
501
|
+
maxY = Math.max(maxY, (r.bottom - vpr.top - t.y) / t.scale);
|
|
502
|
+
});
|
|
503
|
+
heads.forEach((el) => {
|
|
504
|
+
const r = el.getBoundingClientRect();
|
|
505
|
+
minY = Math.min(minY, (r.top - vpr.top - t.y) / t.scale);
|
|
506
|
+
maxY = Math.max(maxY, (r.bottom - vpr.top - t.y) / t.scale);
|
|
507
|
+
});
|
|
508
|
+
return { vpr, minX, minY, w: maxX - minX, h: maxY - minY };
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
// Content SCREEN width is linear in scale (screenW = s·Wworld) and SCREEN
|
|
512
|
+
// height is affine (screenH = s·P + Q): titles and inter-section gaps
|
|
513
|
+
// counter-scale with zoom, so they hold a constant screen size while cards
|
|
514
|
+
// scale. Sample two scales to solve the coefficients, then pick the largest
|
|
515
|
+
// scale that keeps both axes within the viewport minus a PAD margin — exact,
|
|
516
|
+
// no iterate-to-converge.
|
|
517
|
+
const s1 = tf.current.scale;
|
|
518
|
+
const m1 = measure();
|
|
519
|
+
if (!(m1.w > 0) || !(m1.h > 0)) { didFit.current = true; return; }
|
|
520
|
+
const s2 = s1 * 0.7;
|
|
521
|
+
tf.current.scale = s2; apply();
|
|
522
|
+
const m2 = measure();
|
|
523
|
+
|
|
524
|
+
const { vpr } = m1;
|
|
525
|
+
const avW = vpr.width - PAD * 2, avH = vpr.height - PAD * 2;
|
|
526
|
+
const Wworld = m1.w; // screenW / s, scale-invariant
|
|
527
|
+
const P = (s1 * m1.h - s2 * m2.h) / (s1 - s2); // d(screenH)/d(scale)
|
|
528
|
+
const Q = s1 * m1.h - s1 * P; // constant screen-space part
|
|
529
|
+
const fitW = avW / Wworld;
|
|
530
|
+
const fitH = P > 0 ? (avH - Q) / P : Infinity;
|
|
531
|
+
const next = Math.max(minScale, Math.min(maxScale, Math.min(fitW, fitH)));
|
|
532
|
+
|
|
533
|
+
// Apply the solved scale, then centre using the extent measured AT that
|
|
534
|
+
// scale so margins come out symmetric (exactly PAD on the binding axis).
|
|
535
|
+
tf.current.scale = next; apply();
|
|
536
|
+
const mf = measure();
|
|
537
|
+
tf.current.x = (vpr.width - mf.w * next) / 2 - mf.minX * next;
|
|
538
|
+
tf.current.y = (vpr.height - mf.h * next) / 2 - mf.minY * next;
|
|
539
|
+
apply();
|
|
540
|
+
didFit.current = true;
|
|
541
|
+
});
|
|
542
|
+
|
|
448
543
|
React.useEffect(() => {
|
|
449
544
|
const vp = vpRef.current;
|
|
450
545
|
if (!vp) return;
|
|
@@ -490,29 +585,36 @@ function DCViewport({ children, minScale = 0.1, maxScale = 8, style = {} }) {
|
|
|
490
585
|
zoomAt(r.left + r.width / 2, r.top + r.height / 2, clamped / tf.current.scale);
|
|
491
586
|
};
|
|
492
587
|
|
|
493
|
-
//
|
|
494
|
-
//
|
|
495
|
-
//
|
|
496
|
-
//
|
|
497
|
-
//
|
|
498
|
-
|
|
588
|
+
// Zoom is gated behind a modifier only: the browser sets ctrlKey for a
|
|
589
|
+
// trackpad pinch, and Ctrl/⌘ + wheel is the explicit mouse-zoom gesture.
|
|
590
|
+
// Every unmodified scroll — trackpad two-finger OR mouse wheel — pans.
|
|
591
|
+
// This is Figma's mapping, and it sidesteps a classification that can't be
|
|
592
|
+
// done reliably: a fast vertical trackpad flick's peak frame is identical
|
|
593
|
+
// to a mouse-wheel notch (deltaMode 0, deltaX 0, large integer deltaY), so
|
|
594
|
+
// routing pan-vs-zoom from a single wheel event misfires — a quick two-
|
|
595
|
+
// finger swipe used to jump into zoom. Keying off the modifier removes the
|
|
596
|
+
// ambiguity at the source.
|
|
597
|
+
//
|
|
598
|
+
// isNotchedWheel now only picks the zoom *step size* inside the zoom
|
|
599
|
+
// branch (fixed ratio for discrete wheels / Firefox line-mode, continuous
|
|
600
|
+
// exp for a smooth pinch) — misjudging it is a harmless granularity
|
|
601
|
+
// difference, never a pan↔zoom flip.
|
|
602
|
+
const isNotchedWheel = (e) =>
|
|
499
603
|
e.deltaMode !== 0 ||
|
|
500
604
|
(e.deltaX === 0 && Number.isInteger(e.deltaY) && Math.abs(e.deltaY) >= 40);
|
|
501
605
|
|
|
502
606
|
const onWheel = (e) => {
|
|
503
607
|
e.preventDefault();
|
|
504
608
|
if (isGesturing) return; // Safari: gesture* owns the pinch — discard concurrent wheels
|
|
505
|
-
if (
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
zoomAt(e.clientX, e.clientY, Math.exp(-e.deltaY * 0.01));
|
|
509
|
-
} else if (isMouseWheel(e)) {
|
|
510
|
-
// notched mouse wheel — fixed-ratio step per click
|
|
511
|
-
zoomAt(e.clientX, e.clientY, Math.exp(-Math.sign(e.deltaY) * 0.18));
|
|
609
|
+
if (e.ctrlKey || e.metaKey) {
|
|
610
|
+
zoomAt(e.clientX, e.clientY,
|
|
611
|
+
isNotchedWheel(e) ? Math.exp(-Math.sign(e.deltaY) * 0.18) : Math.exp(-e.deltaY * 0.01));
|
|
512
612
|
} else {
|
|
513
|
-
//
|
|
514
|
-
|
|
515
|
-
|
|
613
|
+
// Unmodified scroll pans. Normalize line/page deltas so a notched
|
|
614
|
+
// wheel moves a sensible distance instead of a few pixels per notch.
|
|
615
|
+
const unit = e.deltaMode === 1 ? 16 : e.deltaMode === 2 ? vp.clientHeight : 1;
|
|
616
|
+
tf.current.x -= e.deltaX * unit;
|
|
617
|
+
tf.current.y -= e.deltaY * unit;
|
|
516
618
|
apply();
|
|
517
619
|
}
|
|
518
620
|
};
|
|
@@ -848,7 +950,10 @@ function DCArtboardFrame({ sectionId, artboard, label, order, onRename, onReorde
|
|
|
848
950
|
const doExport = (kind) => {
|
|
849
951
|
setMenuOpen(false);
|
|
850
952
|
if (!cardRef.current) return;
|
|
851
|
-
|
|
953
|
+
// Unicode-aware sanitize: keep letters (incl. CJK/accented), digits,
|
|
954
|
+
// whitespace, dot/underscore/hyphen; collapse everything else to '_'.
|
|
955
|
+
// ASCII-only \w would strip Chinese labels down to a bare '_'.
|
|
956
|
+
const name = String(label || id || 'artboard').replace(/[^\p{L}\p{N}\s._-]+/gu, '_');
|
|
852
957
|
const ew = cardRef.current.offsetWidth || width;
|
|
853
958
|
const eh = cardRef.current.offsetHeight || height;
|
|
854
959
|
dcExport(cardRef.current, ew, eh, name, kind)
|
|
@@ -926,7 +1031,7 @@ function DCArtboardFrame({ sectionId, artboard, label, order, onRename, onReorde
|
|
|
926
1031
|
</div>
|
|
927
1032
|
<div className="dc-labeltext" onClick={onFocus} title="Click to focus">
|
|
928
1033
|
<DCEditable value={label} onChange={onRename} onClick={(e) => e.stopPropagation()}
|
|
929
|
-
style={{ fontSize: 14, fontWeight:
|
|
1034
|
+
style={{ fontSize: 14, fontWeight: 300, color: DC.label, lineHeight: '22px' }} />
|
|
930
1035
|
</div>
|
|
931
1036
|
</div>
|
|
932
1037
|
<div className="dc-btns">
|
|
@@ -950,8 +1055,8 @@ function DCArtboardFrame({ sectionId, artboard, label, order, onRename, onReorde
|
|
|
950
1055
|
</button>
|
|
951
1056
|
</div>
|
|
952
1057
|
</div>
|
|
953
|
-
<div ref={cardRef} className="dc-card"
|
|
954
|
-
style={{ ...(chromeless ? {} : { width, height, borderRadius: 4,
|
|
1058
|
+
<div ref={cardRef} className={"dc-card" + (chromeless ? ' chromeless' : '')}
|
|
1059
|
+
style={{ ...(chromeless ? {} : { width, height, borderRadius: 4, border: '1px solid #DEE0E3', overflow: 'hidden', background: '#fff' }), ...style }}>
|
|
955
1060
|
{children || <div style={{ height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#bbb', fontSize: 13, fontFamily: DC.font }}>{id}</div>}
|
|
956
1061
|
</div>
|
|
957
1062
|
</div>
|
|
@@ -978,7 +1083,6 @@ function DCEditable({ value, onChange, style, tag = 'span', onClick }) {
|
|
|
978
1083
|
function DCFocusOverlay({ entry, sectionMeta, sectionOrder }) {
|
|
979
1084
|
const ctx = React.useContext(DCCtx);
|
|
980
1085
|
const { sectionId, artboard } = entry;
|
|
981
|
-
const sec = ctx.section(sectionId);
|
|
982
1086
|
const meta = sectionMeta[sectionId];
|
|
983
1087
|
const peers = meta.slotIds;
|
|
984
1088
|
const aid = artboard.props.id ?? artboard.props.label;
|
|
@@ -1008,22 +1112,55 @@ function DCFocusOverlay({ entry, sectionMeta, sectionOrder }) {
|
|
|
1008
1112
|
return () => document.removeEventListener('keydown', k);
|
|
1009
1113
|
});
|
|
1010
1114
|
|
|
1011
|
-
const { width = 260, height = 480, children } = artboard.props;
|
|
1115
|
+
const { width = 260, height = 480, chromeless = false, children } = artboard.props;
|
|
1116
|
+
// width/height describe a fixed-frame (non-chromeless) card and are used
|
|
1117
|
+
// directly for it. A chromeless artboard has NO fixed frame — its canvas
|
|
1118
|
+
// card sizes to the content (a device frame, an auto-sized layout), so
|
|
1119
|
+
// width/height fall back to 260×480 here and DON'T describe the content.
|
|
1120
|
+
// Fitting to those defaults would wrap the real content (e.g. a 402×874
|
|
1121
|
+
// device) in a 260×480 clip box and magnify the clipped slice. So for a
|
|
1122
|
+
// chromeless artboard, measure the content's real rendered size first
|
|
1123
|
+
// (unscaled, no clip) and fit-scale to that instead — mirroring how
|
|
1124
|
+
// DCArtboardFrame lets a chromeless card size to its content.
|
|
1125
|
+
const measureRef = React.useRef(null);
|
|
1126
|
+
const [nat, setNat] = React.useState(null); // { aid, w, h } measured natural size
|
|
1127
|
+
React.useLayoutEffect(() => {
|
|
1128
|
+
if (!chromeless) return;
|
|
1129
|
+
const el = measureRef.current;
|
|
1130
|
+
if (el) setNat({ aid, w: el.offsetWidth, h: el.offsetHeight });
|
|
1131
|
+
}, [aid, chromeless]);
|
|
1132
|
+
const useMeasured = chromeless && nat && nat.aid === aid && nat.w > 0;
|
|
1133
|
+
const needsMeasure = chromeless && !useMeasured;
|
|
1134
|
+
const cw = useMeasured ? nat.w : width;
|
|
1135
|
+
const ch = useMeasured ? nat.h : height;
|
|
1136
|
+
|
|
1012
1137
|
const [vp, setVp] = React.useState({ w: window.innerWidth, h: window.innerHeight });
|
|
1013
1138
|
React.useEffect(() => { const r = () => setVp({ w: window.innerWidth, h: window.innerHeight }); window.addEventListener('resize', r); return () => window.removeEventListener('resize', r); }, []);
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1139
|
+
// Fill the viewport leaving a safe margin — 70px left/right, 32px top/bottom.
|
|
1140
|
+
// Take the smaller of the width-fit / height-fit ratios so the whole artboard
|
|
1141
|
+
// stays fully visible; no upper cap, so small frames still fill the screen.
|
|
1142
|
+
const MARGIN_X = 70;
|
|
1143
|
+
const MARGIN_Y = 32;
|
|
1144
|
+
const scale = Math.max(0.05, Math.min((vp.w - MARGIN_X * 2) / cw, (vp.h - MARGIN_Y * 2) / ch));
|
|
1145
|
+
|
|
1146
|
+
// Chrome (arrows / close / dots) is hidden until the cursor sits on the
|
|
1147
|
+
// dark backdrop (anywhere that isn't the interactive content). Hiding also
|
|
1148
|
+
// sets pointer-events:none so hidden controls are click-through and never
|
|
1149
|
+
// block content interaction or a backdrop-click exit.
|
|
1150
|
+
const [chromeVisible, setChromeVisible] = React.useState(false);
|
|
1151
|
+
const contentRef = React.useRef(null);
|
|
1152
|
+
const chromeStyle = { opacity: chromeVisible ? 1 : 0, pointerEvents: chromeVisible ? 'auto' : 'none' };
|
|
1017
1153
|
const Arrow = ({ dir, onClick }) => (
|
|
1018
1154
|
<button onClick={(e) => { e.stopPropagation(); onClick(); }}
|
|
1019
|
-
style={{ position: 'absolute', top: '50%', [dir]:
|
|
1020
|
-
border: 'none', background: 'rgba(255,255,255,.08)', color: '
|
|
1021
|
-
width:
|
|
1022
|
-
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
1155
|
+
style={{ position: 'absolute', top: '50%', [dir]: 20, transform: 'translateY(-50%)',
|
|
1156
|
+
border: 'none', background: 'rgba(255,255,255,.08)', color: 'rgb(255,255,255)',
|
|
1157
|
+
width: 36, height: 36, borderRadius: 22, fontSize: 16, cursor: 'pointer',
|
|
1158
|
+
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
1159
|
+
transition: 'background .15s, opacity .18s ease', ...chromeStyle }}
|
|
1023
1160
|
onMouseEnter={(e) => (e.currentTarget.style.background = 'rgba(255,255,255,.18)')}
|
|
1024
1161
|
onMouseLeave={(e) => (e.currentTarget.style.background = 'rgba(255,255,255,.08)')}>
|
|
1025
|
-
<svg width="
|
|
1026
|
-
<path d={dir === 'left' ? '
|
|
1162
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
1163
|
+
<path d={dir === 'left' ? 'M10.8619 1.52925C11.1223 1.7896 11.1223 2.21171 10.8619 2.47206L5.33333 8.00065L10.8619 13.5292C11.1223 13.7896 11.1223 14.2117 10.8619 14.4721C10.6016 14.7324 10.1795 14.7324 9.91912 14.4721L4.39052 8.94346C3.86983 8.42276 3.86982 7.57854 4.39052 7.05784L9.91912 1.52925C10.1795 1.2689 10.6016 1.2689 10.8619 1.52925Z' : 'M5.13765 14.4721C4.8773 14.2117 4.8773 13.7896 5.13765 13.5292L10.6662 8.00065L5.13765 2.47206C4.8773 2.21171 4.8773 1.7896 5.13764 1.52925C5.39799 1.2689 5.8201 1.2689 6.08045 1.52925L11.609 7.05784C12.1297 7.57854 12.1298 8.42276 11.6091 8.94346L6.08045 14.4721C5.82011 14.7324 5.398 14.7324 5.13765 14.4721Z'} fill="currentColor" /></svg>
|
|
1027
1164
|
</button>
|
|
1028
1165
|
);
|
|
1029
1166
|
|
|
@@ -1031,68 +1168,55 @@ function DCFocusOverlay({ entry, sectionMeta, sectionOrder }) {
|
|
|
1031
1168
|
// transform on DesignCanvas's ancestors (including the canvas zoom itself).
|
|
1032
1169
|
return ReactDOM.createPortal(
|
|
1033
1170
|
<div onClick={() => ctx.setFocus(null)}
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
</button>
|
|
1061
|
-
))}
|
|
1062
|
-
</div>
|
|
1063
|
-
)}
|
|
1171
|
+
onMouseMove={(e) => setChromeVisible(!(contentRef.current && contentRef.current.contains(e.target)))}
|
|
1172
|
+
onMouseLeave={() => setChromeVisible(false)}
|
|
1173
|
+
onWheel={(e) => { if (!(contentRef.current && contentRef.current.contains(e.target))) e.preventDefault(); }}
|
|
1174
|
+
style={{ position: 'fixed', inset: 0, zIndex: 100, background: 'rgba(0,0,0,.8)', backdropFilter: 'blur(4px)',
|
|
1175
|
+
fontFamily: DC.font, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
|
1176
|
+
|
|
1177
|
+
{/* content fills the viewport to its safe margin (70px left/right, 32px
|
|
1178
|
+
top/bottom), centered — interactive & scrollable. Only the content
|
|
1179
|
+
stops propagation, so any backdrop click (the dark margin around it)
|
|
1180
|
+
exits focus. */}
|
|
1181
|
+
<div ref={contentRef} className="dc-focus-content" onClick={(e) => e.stopPropagation()}
|
|
1182
|
+
style={{ width: cw * scale, height: ch * scale, position: 'relative',
|
|
1183
|
+
// hidden until the (chromeless) measure pass below reports the real size
|
|
1184
|
+
visibility: needsMeasure ? 'hidden' : 'visible' }}>
|
|
1185
|
+
{/* Chromeless renders a hidden measure pass first (inline-block →
|
|
1186
|
+
natural size for offsetWidth/Height), then re-renders scaled. Card
|
|
1187
|
+
chrome (bg / radius / shadow / overflow:auto) is added only for a
|
|
1188
|
+
fixed-frame box — matching the canvas card — and gives that box a
|
|
1189
|
+
scroll viewport so content it clips (taller/wider than the frame) is
|
|
1190
|
+
reachable; scrollbars are hidden by the .dc-focus-content CSS. A
|
|
1191
|
+
chromeless box carries none of it, so its content owns its look. */}
|
|
1192
|
+
<div ref={measureRef} style={needsMeasure
|
|
1193
|
+
? { display: 'inline-block' }
|
|
1194
|
+
: { width: cw, height: ch, transform: `scale(${scale})`, transformOrigin: 'top left',
|
|
1195
|
+
...(chromeless ? {} : { background: '#fff', borderRadius: 2, overflow: 'auto', boxShadow: '0 20px 80px rgba(0,0,0,.4)' }) }}>
|
|
1196
|
+
{children || <div style={{ height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#bbb' }}>{aid}</div>}
|
|
1064
1197
|
</div>
|
|
1065
|
-
<div style={{ flex: 1 }} />
|
|
1066
|
-
<button onClick={() => ctx.setFocus(null)}
|
|
1067
|
-
onMouseEnter={(e) => (e.currentTarget.style.background = 'rgba(255,255,255,.12)')}
|
|
1068
|
-
onMouseLeave={(e) => (e.currentTarget.style.background = 'transparent')}
|
|
1069
|
-
style={{ border: 'none', background: 'transparent', color: 'rgba(255,255,255,.7)', width: 32, height: 32,
|
|
1070
|
-
borderRadius: 16, fontSize: 20, cursor: 'pointer', lineHeight: 1, transition: 'background .12s' }}>×</button>
|
|
1071
1198
|
</div>
|
|
1072
1199
|
|
|
1073
|
-
{/*
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
style={{ position: 'absolute', top:
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
{(sec.labels || {})[aid] ?? artboard.props.label}
|
|
1086
|
-
<span style={{ opacity: .5, marginLeft: 10, fontVariantNumeric: 'tabular-nums' }}>{idx + 1} / {peers.length}</span>
|
|
1087
|
-
</div>
|
|
1088
|
-
</div>
|
|
1200
|
+
{/* close — top-right, revealed on backdrop hover */}
|
|
1201
|
+
<button onClick={() => ctx.setFocus(null)}
|
|
1202
|
+
onMouseEnter={(e) => (e.currentTarget.style.background = 'rgba(255,255,255,.18)')}
|
|
1203
|
+
onMouseLeave={(e) => (e.currentTarget.style.background = 'rgba(255,255,255,.08)')}
|
|
1204
|
+
style={{ position: 'absolute', top: 20, right: 24, border: 'none', background: 'rgba(255,255,255,.08)', color: 'rgb(255,255,255)',
|
|
1205
|
+
width: 36, height: 36, borderRadius: 22, fontSize: 16, cursor: 'pointer', lineHeight: 1,
|
|
1206
|
+
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
1207
|
+
transition: 'background .12s, opacity .18s ease', ...chromeStyle }}>
|
|
1208
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
1209
|
+
<path d="M13.4713 13.4722C13.7301 13.2134 13.732 12.7957 13.4732 12.5369L8.93693 8.00064L13.4731 3.46444C13.7319 3.20562 13.7301 2.78788 13.4713 2.52907C13.2125 2.27026 12.7947 2.26838 12.5359 2.5272C12.2771 2.78601 7.99969 7.0634 7.99969 7.0634L3.46345 2.52716C3.20464 2.26835 2.7869 2.27022 2.52809 2.52903C2.26928 2.78784 2.2674 3.20558 2.52621 3.4644L7.06246 8.00064L2.52618 12.5369C2.26737 12.7957 2.26924 13.2135 2.52806 13.4723C2.78687 13.7311 3.20461 13.733 3.46342 13.4742L7.99969 8.93788L12.5359 13.4741C12.7947 13.7329 13.2125 13.731 13.4713 13.4722Z" fill="currentColor"/>
|
|
1210
|
+
</svg>
|
|
1211
|
+
</button>
|
|
1089
1212
|
|
|
1090
1213
|
<Arrow dir="left" onClick={() => go(-1)} />
|
|
1091
1214
|
<Arrow dir="right" onClick={() => go(1)} />
|
|
1092
1215
|
|
|
1093
|
-
{/* dots */}
|
|
1216
|
+
{/* dots — revealed on backdrop hover */}
|
|
1094
1217
|
<div onClick={(e) => e.stopPropagation()}
|
|
1095
|
-
style={{ position: 'absolute', bottom:
|
|
1218
|
+
style={{ position: 'absolute', bottom: 13, left: '50%', transform: 'translateX(-50%)', display: 'flex', gap: 8,
|
|
1219
|
+
transition: 'opacity .18s ease', ...chromeStyle }}>
|
|
1096
1220
|
{peers.map((p, i) => (
|
|
1097
1221
|
<button key={p} onClick={() => ctx.setFocus(`${sectionId}/${p}`)}
|
|
1098
1222
|
style={{ border: 'none', padding: 0, cursor: 'pointer', width: 6, height: 6, borderRadius: 3,
|
package/components/ios-frame.jsx
CHANGED
|
@@ -12,10 +12,10 @@
|
|
|
12
12
|
// <IOSDevice dark keyboard>…</IOSDevice>
|
|
13
13
|
// <IOSDevice width={390} height={844}>…</IOSDevice> // smaller device size
|
|
14
14
|
//
|
|
15
|
-
// Safe
|
|
16
|
-
//
|
|
17
|
-
// --ios-safe-
|
|
18
|
-
//
|
|
15
|
+
// Safe areas — REQUIRED on every screen. The status bar (top) and home
|
|
16
|
+
// indicator (bottom) float OVER your content; inset it or it overlaps them.
|
|
17
|
+
// --ios-safe-top top inset (Dynamic Island + status bar)
|
|
18
|
+
// --ios-safe-bottom bottom inset (home indicator)
|
|
19
19
|
/* END USAGE */
|
|
20
20
|
|
|
21
21
|
// ─────────────────────────────────────────────────────────────
|
|
@@ -126,7 +126,7 @@ function IOSDevice({
|
|
|
126
126
|
<div style={{
|
|
127
127
|
width, height, borderRadius: 48, overflow: 'hidden',
|
|
128
128
|
position: 'relative', background: dark ? '#000' : '#F2F2F7',
|
|
129
|
-
|
|
129
|
+
border: '1px solid #DEE0E3',
|
|
130
130
|
fontFamily: '-apple-system, system-ui, sans-serif',
|
|
131
131
|
WebkitFontSmoothing: 'antialiased',
|
|
132
132
|
'--ios-safe-top': '62px',
|
|
@@ -176,7 +176,7 @@ function MacWindow({
|
|
|
176
176
|
<div style={{
|
|
177
177
|
width, height, borderRadius: 26, overflow: 'hidden',
|
|
178
178
|
background: '#fff',
|
|
179
|
-
|
|
179
|
+
border: '1px solid #DEE0E3',
|
|
180
180
|
display: 'flex', position: 'relative',
|
|
181
181
|
fontFamily: MAC_FONT,
|
|
182
182
|
}}>
|
|
@@ -19,6 +19,13 @@
|
|
|
19
19
|
// "dark": false
|
|
20
20
|
// }/*EDITMODE-END*/;
|
|
21
21
|
//
|
|
22
|
+
// TWEAK_DEFAULTS must live inline in the HTML file — in a <script type="text/babel"> block,
|
|
23
|
+
// not in a separate .jsx/.js loaded via <script src>. That in-HTML block is the region the
|
|
24
|
+
// host rewrites when the user adjusts a tweak, so keep it wrapped in the /*EDITMODE-BEGIN*/ …
|
|
25
|
+
// /*EDITMODE-END*/ markers and the object between them valid JSON — double-quoted keys, no
|
|
26
|
+
// trailing commas, no comments or expressions — even after you rename the keys. Move it out
|
|
27
|
+
// of the HTML, strip the markers, or use a non-JSON body and tweak edits silently stop persisting.
|
|
28
|
+
//
|
|
22
29
|
// function App() {
|
|
23
30
|
// const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
|
|
24
31
|
// return (
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lark-apaas/coding-registry-buildless",
|
|
3
|
-
"version": "0.1.2-
|
|
3
|
+
"version": "0.1.2-beta.0",
|
|
4
4
|
"description": "Miaoda registry (buildless runtime) — shadcn-style copy-in SDKs (globals + CDN-pinned, browser Babel) for buildless static HTML stacks",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|