@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.
- package/components/design-canvas.jsx +96 -54
- package/package.json +1 -1
- package/scripts/design-canvas.js +3 -3
|
@@ -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
|
-
//
|
|
484
|
-
//
|
|
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
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
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
|
-
|
|
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
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
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 =
|
|
1663
|
+
const n = sections.length;
|
|
1622
1664
|
for (let i = 1; i < n; i++) {
|
|
1623
|
-
const
|
|
1624
|
-
const first =
|
|
1625
|
-
if (first) { ctx.setFocus(`${
|
|
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.
|
|
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": [
|
package/scripts/design-canvas.js
CHANGED
|
@@ -2196,7 +2196,8 @@
|
|
|
2196
2196
|
this.registry = {};
|
|
2197
2197
|
this.sectionMeta = {};
|
|
2198
2198
|
this.sectionOrder = [];
|
|
2199
|
-
for (const sec of this.querySelectorAll('
|
|
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
|
-
//
|
|
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);
|