@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.
@@ -25,7 +25,7 @@
25
25
  /* END USAGE */
26
26
 
27
27
  const DC = {
28
- bg: '#FFFFFF',
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 go through the
157
- // host's window.miaoda bridge — editing requires the miaoda runtime.
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
- window.miaoda?.writeFile(DC_STATE_FILE, JSON.stringify({ sections: state.sections })).catch(() => {});
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 → zoom (ctrlKey wheel; Safari gesture* events)
368
- // • trackpad scroll → pan (two-finger)
369
- // • mouse wheel → zoom (notched; distinguished from trackpad scroll)
370
- // • middle-drag / primary-drag-on-bg → pan
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
- // Default to 50% so a fresh canvas opens zoomed-out enough to see the
380
- // overall layout; localStorage restore (below) overrides this on revisits.
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), so the canvas never flashes 100% before snapping to 50%.
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
- // Mouse-wheel vs trackpad-scroll heuristic. A physical wheel sends
494
- // line-mode deltas (Firefox) or large integer pixel deltas with no X
495
- // component (Chrome/Safari, typically multiples of 100/120). Trackpad
496
- // two-finger scroll sends small/fractional pixel deltas, often with
497
- // non-zero deltaX. ctrlKey is set by the browser for trackpad pinch.
498
- const isMouseWheel = (e) =>
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 ((e.ctrlKey || e.metaKey) && !isMouseWheel(e)) {
506
- // trackpad pinch, or ctrl/cmd + smooth-scroll mouse. Notched
507
- // wheels fall through to the fixed-step branch below.
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
- // trackpad two-finger scroll — pan
514
- tf.current.x -= e.deltaX;
515
- tf.current.y -= e.deltaY;
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
- const name = String(label || id || 'artboard').replace(/[^\w\s.-]+/g, '_');
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: 400, color: DC.label, lineHeight: '22px' }} />
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, boxShadow: '0 1px 3px rgba(0,0,0,.08),0 4px 16px rgba(0,0,0,.06)', overflow: 'hidden', background: '#fff' }), ...style }}>
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
- const scale = Math.max(0.1, Math.min((vp.w - 200) / width, (vp.h - 260) / height, 2));
1015
-
1016
- const [ddOpen, setDd] = React.useState(false);
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]: 28, transform: 'translateY(-50%)',
1020
- border: 'none', background: 'rgba(255,255,255,.08)', color: 'rgba(255,255,255,.9)',
1021
- width: 44, height: 44, borderRadius: 22, fontSize: 18, cursor: 'pointer',
1022
- display: 'flex', alignItems: 'center', justifyContent: 'center', transition: 'background .15s' }}
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="18" height="18" viewBox="0 0 18 18" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
1026
- <path d={dir === 'left' ? 'M11 3L5 9l6 6' : 'M7 3l6 6-6 6'} /></svg>
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
- onWheel={(e) => e.preventDefault()}
1035
- style={{ position: 'fixed', inset: 0, zIndex: 100, background: 'rgba(24,20,16,.6)', backdropFilter: 'blur(14px)',
1036
- fontFamily: DC.font, color: '#fff' }}>
1037
-
1038
- {/* top bar: section dropdown (left) · close (right) */}
1039
- <div onClick={(e) => e.stopPropagation()}
1040
- style={{ position: 'absolute', top: 0, left: 0, right: 0, height: 72, display: 'flex', alignItems: 'flex-start', padding: '16px 20px 0', gap: 16 }}>
1041
- <div style={{ position: 'relative' }}>
1042
- <button onClick={() => setDd((o) => !o)}
1043
- style={{ border: 'none', background: 'transparent', color: '#fff', cursor: 'pointer', padding: '6px 8px',
1044
- borderRadius: 6, textAlign: 'left', fontFamily: 'inherit' }}>
1045
- <span style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
1046
- <span style={{ fontSize: 18, fontWeight: 600, letterSpacing: -0.3 }}>{meta.title}</span>
1047
- <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>
1048
- </span>
1049
- {meta.subtitle && <span style={{ display: 'block', fontSize: 13, opacity: .6, fontWeight: 400, marginTop: 2 }}>{meta.subtitle}</span>}
1050
- </button>
1051
- {ddOpen && (
1052
- <div style={{ position: 'absolute', top: '100%', left: 0, marginTop: 4, background: '#2a251f', borderRadius: 8,
1053
- boxShadow: '0 8px 32px rgba(0,0,0,.4)', padding: 4, minWidth: 200, zIndex: 10 }}>
1054
- {sectionOrder.filter((sid) => sectionMeta[sid].slotIds.length).map((sid) => (
1055
- <button key={sid} onClick={() => { setDd(false); const f = sectionMeta[sid].slotIds[0]; if (f) ctx.setFocus(`${sid}/${f}`); }}
1056
- style={{ display: 'block', width: '100%', textAlign: 'left', border: 'none', cursor: 'pointer',
1057
- background: sid === sectionId ? 'rgba(255,255,255,.1)' : 'transparent', color: '#fff',
1058
- padding: '8px 12px', borderRadius: 5, fontSize: 14, fontWeight: sid === sectionId ? 600 : 400, fontFamily: 'inherit' }}>
1059
- {sectionMeta[sid].title}
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
- {/* card centered, label + index below — only the card itself stops
1074
- propagation so any backdrop click (including the margins around
1075
- the card) exits focus */}
1076
- <div
1077
- style={{ position: 'absolute', top: 64, bottom: 56, left: 100, right: 100, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 16 }}>
1078
- <div onClick={(e) => e.stopPropagation()} style={{ width: width * scale, height: height * scale, position: 'relative' }}>
1079
- <div style={{ width, height, transform: `scale(${scale})`, transformOrigin: 'top left', background: '#fff', borderRadius: 2, overflow: 'hidden',
1080
- boxShadow: '0 20px 80px rgba(0,0,0,.4)' }}>
1081
- {children || <div style={{ height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#bbb' }}>{aid}</div>}
1082
- </div>
1083
- </div>
1084
- <div onClick={(e) => e.stopPropagation()} style={{ fontSize: 14, fontWeight: 500, opacity: .85, textAlign: 'center' }}>
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: 20, left: '50%', transform: 'translateX(-50%)', display: 'flex', gap: 8 }}>
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,
@@ -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-area CSS variables available inside <IOSDevice>:
16
- // --ios-safe-top top inset (Dynamic Island + status bar)
17
- // --ios-safe-bottom bottom inset (home indicator)
18
- // Content that doesn't account for these will overlap the device chrome.
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
- boxShadow: '0 40px 80px rgba(0,0,0,0.18), 0 0 0 1px rgba(0,0,0,0.12)',
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
- boxShadow: '0 0 0 1px rgba(0,0,0,0.23), 0 16px 48px rgba(0,0,0,0.35)',
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-alpha.0",
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": [