@lark-apaas/coding-registry-buildless 0.1.26 → 0.1.27-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.
@@ -315,6 +315,17 @@ if (typeof document !== 'undefined' && !document.getElementById('dc-styles')) {
315
315
  }
316
316
 
317
317
  const DCCtx = React.createContext(null);
318
+ const DC_EMPTY_SECTION = {};
319
+
320
+ function dcSectionsInDOMOrder(sections) {
321
+ return [...sections].sort(([a], [b]) => {
322
+ const position = a.compareDocumentPosition(b);
323
+ if (position & Node.DOCUMENT_POSITION_DISCONNECTED) return 0;
324
+ if (position & Node.DOCUMENT_POSITION_FOLLOWING) return -1;
325
+ if (position & Node.DOCUMENT_POSITION_PRECEDING) return 1;
326
+ return 0;
327
+ });
328
+ }
318
329
 
319
330
  // Recursively unwrap React.Fragment so <>…</> grouping doesn't hide
320
331
  // DCSection/DCArtboard children from the type-based walks below.
@@ -365,6 +376,23 @@ function miaodaWriteFile(path, content, writeContext) {
365
376
 
366
377
  function DesignCanvas({ children, minScale, maxScale, style }) {
367
378
  const [state, setState] = React.useState({ sections: {}, focus: null });
379
+ const [registeredSections, setRegisteredSections] = React.useState(() => new Map());
380
+ const registeredSectionsRef = React.useRef(registeredSections);
381
+ registeredSectionsRef.current = registeredSections;
382
+ const getSectionNavigation = React.useCallback(() =>
383
+ dcSectionsInDOMOrder(registeredSectionsRef.current).map(([, sec]) => ({
384
+ id: sec.id, slotIds: sec.slotIds,
385
+ })), []);
386
+ const registerSection = React.useCallback((node, snapshot) => {
387
+ setRegisteredSections((current) => new Map(current).set(node, snapshot));
388
+ return () => setRegisteredSections((current) => {
389
+ if (current.get(node) !== snapshot) return current;
390
+ const next = new Map(current);
391
+ next.delete(node);
392
+ return next;
393
+ });
394
+ }, []);
395
+
368
396
  // Hold rendering until the sidecar read settles so the saved order/titles
369
397
  // appear on first paint (no source-order flash). didRead gates writes until
370
398
  // the read settles so the empty initial state can't clobber a slow read;
@@ -480,51 +508,44 @@ function DesignCanvas({ children, minScale, maxScale, style }) {
480
508
  return () => offs.forEach((off) => off());
481
509
  }, []);
482
510
 
483
- // Build registries synchronously from children so FocusOverlay can read
484
- // them in the same render. Fragments are flattened; wrapping in other
485
- // elements still opts out of focus/reorder.
511
+ // Sections register the same visible artboards they render, even when a
512
+ // custom component separates them from DesignCanvas in the JSX tree.
486
513
  const registry = {}; // slotId -> { sectionId, artboard }
487
- const sectionMeta = {}; // sectionId -> { title, subtitle, slotIds[] }
488
- const sectionOrder = [];
489
- dcFlatten(children).forEach((sec) => {
490
- if (!sec || sec.type !== DCSection) return;
491
- const sid = sec.props.id ?? sec.props.title;
492
- if (!sid) return;
493
- sectionOrder.push(sid);
494
- const persisted = state.sections[sid] || {};
495
- const abs = [];
496
- dcFlatten(sec.props.children).forEach((ab) => {
497
- if (!ab || ab.type !== DCArtboard) return;
498
- const aid = ab.props.id ?? ab.props.label;
499
- if (aid) abs.push([aid, ab]);
500
- });
501
- // hidden is scoped to one source revision — when the agent regenerates
502
- // (artboard-ID set changes), prior deletes don't apply to new content.
503
- const srcKey = abs.map(([k]) => k).join('\x1f');
504
- const hidden = persisted.srcKey === srcKey ? (persisted.hidden || []) : [];
505
- const srcIds = [];
506
- abs.forEach(([aid, ab]) => {
507
- if (hidden.includes(aid)) return;
508
- registry[`${sid}/${aid}`] = { sectionId: sid, artboard: ab };
509
- srcIds.push(aid);
514
+ registeredSections.forEach((sec, node) => {
515
+ const sid = sec.id;
516
+ sec.slotIds.forEach((aid) => {
517
+ registry[`${sid}/${aid}`] = { sectionId: sid, artboard: sec.artboards[aid], node };
510
518
  });
511
- const kept = (persisted.order || []).filter((k) => srcIds.includes(k));
512
- sectionMeta[sid] = {
513
- title: persisted.title ?? sec.props.title,
514
- subtitle: sec.props.subtitle,
515
- slotIds: [...kept, ...srcIds.filter((k) => !kept.includes(k))],
516
- };
517
519
  });
518
520
 
521
+ const focusedEntry = registry[state.focus];
522
+ const focusedSection = focusedEntry && focusedEntry.node;
523
+ const displayedFocus = React.useRef(null);
524
+ React.useLayoutEffect(() => {
525
+ if (focusedEntry) displayedFocus.current = state.focus;
526
+ else if (state.focus && displayedFocus.current === state.focus) {
527
+ // Match the WC correction path: a removed target must not broadcast a
528
+ // focus:null that would close the presenter's overlay on another client.
529
+ skipNextFocusEmit.current = true;
530
+ focusRef.current = null;
531
+ setState((s) => s.focus === state.focus ? { ...s, focus: null } : s);
532
+ } else if (!state.focus) displayedFocus.current = null;
533
+ // A mirrored focus can precede section registration; only clear a target
534
+ // that was displayed here and has subsequently been removed.
535
+ }, [state.focus, focusedEntry]);
536
+
519
537
  const api = React.useMemo(() => ({
520
538
  state,
521
- section: (id) => state.sections[id] || {},
539
+ registerSection,
540
+ getSectionNavigation,
541
+ focusedSection,
542
+ section: (id) => state.sections[id] || DC_EMPTY_SECTION,
522
543
  patchSection: (id, p) => setState((s) => ({
523
544
  ...s,
524
545
  sections: { ...s.sections, [id]: { ...s.sections[id], ...(typeof p === 'function' ? p(s.sections[id] || {}) : p) } },
525
546
  })),
526
547
  setFocus: (slotId) => setState((s) => ({ ...s, focus: slotId })),
527
- }), [state]);
548
+ }), [state, registerSection, getSectionNavigation, focusedSection]);
528
549
 
529
550
  // Esc exits focus; any outside pointerdown commits an in-progress rename.
530
551
  React.useEffect(() => {
@@ -551,9 +572,6 @@ function DesignCanvas({ children, minScale, maxScale, style }) {
551
572
  <DCCtx.Provider value={api}>
552
573
  <DCViewport minScale={minScale} maxScale={maxScale} style={style}>{ready && worldChildren}</DCViewport>
553
574
  {ready && overlayChildren}
554
- {state.focus && registry[state.focus] && (
555
- <DCFocusOverlay entry={registry[state.focus]} sectionMeta={sectionMeta} sectionOrder={sectionOrder} />
556
- )}
557
575
  </DCCtx.Provider>
558
576
  );
559
577
  }
@@ -1249,13 +1267,19 @@ function DCViewport({ children, minScale = 0.1, maxScale = 8, style = {} }) {
1249
1267
  // ─────────────────────────────────────────────────────────────
1250
1268
  function DCSection({ id, title, subtitle, children, gap = 48 }) {
1251
1269
  const ctx = React.useContext(DCCtx);
1270
+ const sectionRef = React.useRef(null);
1252
1271
  const sid = id ?? title;
1253
- const all = React.Children.toArray(dcFlatten(children));
1254
- const artboards = all.filter((c) => c && c.type === DCArtboard);
1255
- const rest = all.filter((c) => !(c && c.type === DCArtboard));
1256
- const sec = (ctx && sid && ctx.section(sid)) || {};
1257
- // Must match DesignCanvas's srcKey computation exactly (it filters falsy
1258
- // IDs), or onDelete persists a srcKey that DesignCanvas never recognizes.
1272
+ const { artboards, rest, byId } = React.useMemo(() => {
1273
+ const all = React.Children.toArray(dcFlatten(children));
1274
+ const artboards = all.filter((c) => c && c.type === DCArtboard);
1275
+ return {
1276
+ artboards,
1277
+ rest: all.filter((c) => !(c && c.type === DCArtboard)),
1278
+ byId: Object.fromEntries(artboards.map((a) => [a.props.id ?? a.props.label, a])),
1279
+ };
1280
+ }, [children]);
1281
+ const sec = (ctx && sid && ctx.section(sid)) || DC_EMPTY_SECTION;
1282
+ // Scope saved deletes to the current source revision.
1259
1283
  const allIds = artboards.map((a) => a.props.id ?? a.props.label).filter(Boolean);
1260
1284
  const srcKey = allIds.join('\x1f');
1261
1285
  const hidden = sec.srcKey === srcKey ? (sec.hidden || []) : [];
@@ -1266,7 +1290,17 @@ function DCSection({ id, title, subtitle, children, gap = 48 }) {
1266
1290
  return [...kept, ...srcOrder.filter((k) => !kept.includes(k))];
1267
1291
  }, [sec.order, srcOrder.join('|')]);
1268
1292
 
1269
- const byId = Object.fromEntries(artboards.map((a) => [a.props.id ?? a.props.label, a]));
1293
+ const snapshot = React.useMemo(() => ({
1294
+ id: sid, title: sec.title ?? title, subtitle, slotIds: order, artboards: byId,
1295
+ }), [sid, sec.title, title, subtitle, order, byId]);
1296
+ const registerSection = ctx && ctx.registerSection;
1297
+ React.useLayoutEffect(() => {
1298
+ if (!sid || !registerSection) return;
1299
+ return registerSection(sectionRef.current, snapshot);
1300
+ }, [sid, registerSection, snapshot]);
1301
+
1302
+ const focusedId = order.find((k) => ctx && ctx.state.focus === `${sid}/${k}`);
1303
+ const focusedArtboard = focusedId === undefined ? null : byId[focusedId];
1270
1304
 
1271
1305
  // marginBottom counter-scales so the on-screen gap between sections stays
1272
1306
  // constant — otherwise at low zoom the (world-space) gap collapses while
@@ -1275,7 +1309,8 @@ function DCSection({ id, title, subtitle, children, gap = 48 }) {
1275
1309
  // the 24px artboard-header (abs-positioned above each card) plus ~8px, so
1276
1310
  // the title sits tight against its own row at every zoom.
1277
1311
  return (
1278
- <div data-dc-section={sid}
1312
+ <>
1313
+ <div ref={sectionRef} data-dc-section={sid}
1279
1314
  style={{ marginBottom: 'calc(80px * var(--dc-inv-zoom, 1))', position: 'relative' }}>
1280
1315
  <div style={{ padding: '0 60px' }}>
1281
1316
  <div className="dc-sectionhead" style={{ paddingBottom: 'calc(36px * var(--dc-inv-zoom, 1))' }}>
@@ -1305,6 +1340,13 @@ function DCSection({ id, title, subtitle, children, gap = 48 }) {
1305
1340
  </div>
1306
1341
  {rest}
1307
1342
  </div>
1343
+ {/* Keep the portal in the section's React ancestry so content retains
1344
+ its providers even though the overlay DOM lives under document.body. */}
1345
+ {focusedArtboard && ctx.focusedSection === sectionRef.current && (
1346
+ <DCFocusOverlay entry={{ sectionId: sid, artboard: focusedArtboard }}
1347
+ peers={order} getSectionNavigation={ctx.getSectionNavigation} />
1348
+ )}
1349
+ </>
1308
1350
  );
1309
1351
  }
1310
1352
 
@@ -1605,24 +1647,24 @@ function DCEditable({ value, onChange, style, tag = 'span', onClick }) {
1605
1647
  // Focus mode — overlay one artboard; ←/→ within section, ↑/↓ across
1606
1648
  // sections, Esc or backdrop click to exit.
1607
1649
  // ─────────────────────────────────────────────────────────────
1608
- function DCFocusOverlay({ entry, sectionMeta, sectionOrder }) {
1650
+ function DCFocusOverlay({ entry, peers, getSectionNavigation }) {
1609
1651
  const ctx = React.useContext(DCCtx);
1610
1652
  const { sectionId, artboard } = entry;
1611
- const meta = sectionMeta[sectionId];
1612
- const peers = meta.slotIds;
1613
1653
  const aid = artboard.props.id ?? artboard.props.label;
1614
1654
  const idx = peers.indexOf(aid);
1615
- const secIdx = sectionOrder.indexOf(sectionId);
1616
1655
 
1617
1656
  const go = (d) => { const n = peers[(idx + d + peers.length) % peers.length]; if (n) ctx.setFocus(`${sectionId}/${n}`); };
1618
1657
  const goSection = (d) => {
1658
+ // Other sections can update independently while this portal stays mounted.
1659
+ const sections = getSectionNavigation();
1660
+ const secIdx = sections.findIndex((sec) => sec.id === sectionId);
1619
1661
  // Sections whose artboards are all deleted have slotIds:[] — step past
1620
1662
  // them to the next non-empty section so ↑/↓ doesn't dead-end.
1621
- const n = sectionOrder.length;
1663
+ const n = sections.length;
1622
1664
  for (let i = 1; i < n; i++) {
1623
- const ns = sectionOrder[(((secIdx + d * i) % n) + n) % n];
1624
- const first = sectionMeta[ns] && sectionMeta[ns].slotIds[0];
1625
- if (first) { ctx.setFocus(`${ns}/${first}`); return; }
1665
+ const next = sections[(((secIdx + d * i) % n) + n) % n];
1666
+ const first = next.slotIds[0];
1667
+ if (first) { ctx.setFocus(`${next.id}/${first}`); return; }
1626
1668
  }
1627
1669
  };
1628
1670
 
@@ -1694,7 +1736,7 @@ function DCFocusOverlay({ entry, sectionMeta, sectionOrder }) {
1694
1736
  // Portal to body so position:fixed is the real viewport regardless of any
1695
1737
  // transform on DesignCanvas's ancestors (including the canvas zoom itself).
1696
1738
  return ReactDOM.createPortal(
1697
- <div onClick={() => ctx.setFocus(null)}
1739
+ <div onClick={(e) => { e.stopPropagation(); ctx.setFocus(null); }}
1698
1740
  onMouseMove={(e) => setChromeVisible(!(contentRef.current && contentRef.current.contains(e.target)))}
1699
1741
  onMouseLeave={() => setChromeVisible(false)}
1700
1742
  onWheel={(e) => { if (!(contentRef.current && contentRef.current.contains(e.target))) e.preventDefault(); }}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lark-apaas/coding-registry-buildless",
3
- "version": "0.1.26",
3
+ "version": "0.1.27-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": [
@@ -2196,7 +2196,8 @@
2196
2196
  this.registry = {};
2197
2197
  this.sectionMeta = {};
2198
2198
  this.sectionOrder = [];
2199
- for (const sec of this.querySelectorAll(':scope > dc-section')) {
2199
+ for (const sec of this.querySelectorAll('dc-section')) {
2200
+ if (sec.closest('design-canvas') !== this) continue;
2200
2201
  const sid = sec.sectionId;
2201
2202
  if (!sid) continue;
2202
2203
  this.sectionOrder.push(sid);
@@ -2223,8 +2224,7 @@
2223
2224
  // This is a LOCAL correction, not a user action, so it must not broadcast: in a
2224
2225
  // Magic Share session one follower whose content lacks the artboard would
2225
2226
  // otherwise push focus:null back and kick the presenter out of the overlay.
2226
- // The React version has no equivalent broadcast — it simply declines to render
2227
- // the overlay for an unknown id and leaves its own state alone.
2227
+ // Keep this correction local in both the React and custom-element versions.
2228
2228
  if (this.state.focus && !this.registry[this.state.focus]) {
2229
2229
  this._skipNextFocusEmit = true;
2230
2230
  this.setFocus(null);