@elytracms/studio 0.0.64 → 0.0.66
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/convex/assets.ts +11 -0
- package/convex/delivery.ts +5 -0
- package/convex/schema.ts +23 -0
- package/dist/components/account/self-account-view-source.js +9 -2
- package/dist/components/account/self-account-view-source.js.map +1 -1
- package/dist/components/app-views/default-modules.d.ts +28 -1
- package/dist/components/app-views/default-modules.js +47 -2
- package/dist/components/app-views/default-modules.js.map +1 -1
- package/dist/components/assets/asset-thumb.d.ts +4 -2
- package/dist/components/assets/asset-thumb.js +26 -4
- package/dist/components/assets/asset-thumb.js.map +1 -1
- package/dist/components/assets/assets-module.js +48 -2
- package/dist/components/assets/assets-module.js.map +1 -1
- package/dist/components/assets/assets-store.d.ts +11 -1
- package/dist/components/assets/assets-store.js +18 -1
- package/dist/components/assets/assets-store.js.map +1 -1
- package/dist/components/auth-context.js +32 -1
- package/dist/components/auth-context.js.map +1 -1
- package/dist/components/builder/blocks-model.d.ts +11 -0
- package/dist/components/builder/blocks-model.js +30 -10
- package/dist/components/builder/blocks-model.js.map +1 -1
- package/dist/components/builder/canvas-chrome-css.js +90 -8
- package/dist/components/builder/canvas-chrome-css.js.map +1 -1
- package/dist/components/builder/canvas-dnd.js +4 -2
- package/dist/components/builder/canvas-dnd.js.map +1 -1
- package/dist/components/builder/canvas-inline-editing.d.ts +7 -0
- package/dist/components/builder/canvas-inline-editing.js +3 -1
- package/dist/components/builder/canvas-inline-editing.js.map +1 -1
- package/dist/components/builder/canvas-overlay-geometry.d.ts +72 -0
- package/dist/components/builder/canvas-overlay-geometry.js +80 -0
- package/dist/components/builder/canvas-overlay-geometry.js.map +1 -0
- package/dist/components/builder/canvas-rect-tracking.d.ts +124 -0
- package/dist/components/builder/canvas-rect-tracking.js +198 -0
- package/dist/components/builder/canvas-rect-tracking.js.map +1 -0
- package/dist/components/builder/canvas.js +61 -12
- package/dist/components/builder/canvas.js.map +1 -1
- package/dist/components/builder/composition-field-editor.js +22 -3
- package/dist/components/builder/composition-field-editor.js.map +1 -1
- package/dist/components/builder/delete-key.d.ts +38 -0
- package/dist/components/builder/delete-key.js +46 -0
- package/dist/components/builder/delete-key.js.map +1 -0
- package/dist/components/builder/drag-session.d.ts +8 -0
- package/dist/components/builder/drag-session.js +15 -0
- package/dist/components/builder/drag-session.js.map +1 -1
- package/dist/components/builder/insert-affordance-context.d.ts +19 -0
- package/dist/components/builder/insert-affordance-context.js +7 -0
- package/dist/components/builder/insert-affordance-context.js.map +1 -0
- package/dist/components/builder/insert-context.d.ts +40 -0
- package/dist/components/builder/insert-context.js +97 -5
- package/dist/components/builder/insert-context.js.map +1 -1
- package/dist/components/builder/node-title.d.ts +60 -0
- package/dist/components/builder/node-title.js +135 -0
- package/dist/components/builder/node-title.js.map +1 -0
- package/dist/components/builder/page-tree.js +25 -5
- package/dist/components/builder/page-tree.js.map +1 -1
- package/dist/components/builder/prop-selection.d.ts +52 -0
- package/dist/components/builder/prop-selection.js +38 -0
- package/dist/components/builder/prop-selection.js.map +1 -0
- package/dist/components/builder/prop-sync-context.d.ts +49 -0
- package/dist/components/builder/prop-sync-context.js +23 -0
- package/dist/components/builder/prop-sync-context.js.map +1 -0
- package/dist/components/builder/property-inspector.js +24 -1
- package/dist/components/builder/property-inspector.js.map +1 -1
- package/dist/components/builder/reuse-context.js +1 -1
- package/dist/components/builder/reuse-context.js.map +1 -1
- package/dist/components/builder/selection-overlay.d.ts +79 -0
- package/dist/components/builder/selection-overlay.js +244 -0
- package/dist/components/builder/selection-overlay.js.map +1 -0
- package/dist/components/builder/spacing-handles.d.ts +57 -1
- package/dist/components/builder/spacing-handles.js +142 -26
- package/dist/components/builder/spacing-handles.js.map +1 -1
- package/dist/components/cms/asset-field.js +21 -5
- package/dist/components/cms/asset-field.js.map +1 -1
- package/dist/components/cms/asset-picker.js +11 -5
- package/dist/components/cms/asset-picker.js.map +1 -1
- package/dist/components/cms/bulk-actions.d.ts +22 -3
- package/dist/components/cms/bulk-actions.js +27 -3
- package/dist/components/cms/bulk-actions.js.map +1 -1
- package/dist/components/cms/cms-store.js +19 -0
- package/dist/components/cms/cms-store.js.map +1 -1
- package/dist/components/cms/detail-editor.js +18 -19
- package/dist/components/cms/detail-editor.js.map +1 -1
- package/dist/components/cms/prop-fields.js +23 -16
- package/dist/components/cms/prop-fields.js.map +1 -1
- package/dist/components/cms/publish-clean.d.ts +21 -0
- package/dist/components/cms/publish-clean.js +30 -0
- package/dist/components/cms/publish-clean.js.map +1 -0
- package/dist/components/cms/rich-text/block-style.d.ts +48 -0
- package/dist/components/cms/rich-text/block-style.js +30 -0
- package/dist/components/cms/rich-text/block-style.js.map +1 -0
- package/dist/components/cms/rich-text/embed-picker.js +4 -3
- package/dist/components/cms/rich-text/embed-picker.js.map +1 -1
- package/dist/components/cms/rich-text/extensions.d.ts +2 -0
- package/dist/components/cms/rich-text/extensions.js +4 -0
- package/dist/components/cms/rich-text/extensions.js.map +1 -1
- package/dist/components/cms/rich-text/font-size-control.d.ts +31 -0
- package/dist/components/cms/rich-text/font-size-control.js +40 -0
- package/dist/components/cms/rich-text/font-size-control.js.map +1 -0
- package/dist/components/cms/rich-text/font-size.d.ts +16 -0
- package/dist/components/cms/rich-text/font-size.js +40 -0
- package/dist/components/cms/rich-text/font-size.js.map +1 -0
- package/dist/components/cms/rich-text/toolbar-state.d.ts +3 -0
- package/dist/components/cms/rich-text/toolbar-state.js +3 -0
- package/dist/components/cms/rich-text/toolbar-state.js.map +1 -1
- package/dist/components/cms/rich-text/toolbar.d.ts +13 -0
- package/dist/components/cms/rich-text/toolbar.js +17 -5
- package/dist/components/cms/rich-text/toolbar.js.map +1 -1
- package/dist/components/cms/use-live-url.d.ts +24 -1
- package/dist/components/cms/use-live-url.js +26 -4
- package/dist/components/cms/use-live-url.js.map +1 -1
- package/dist/components/cms/use-publish-controller.js +8 -6
- package/dist/components/cms/use-publish-controller.js.map +1 -1
- package/dist/components/collection-view/cms-collection-view-source.js +15 -7
- package/dist/components/collection-view/cms-collection-view-source.js.map +1 -1
- package/dist/components/components/component-detail.js +3 -3
- package/dist/components/components/component-detail.js.map +1 -1
- package/dist/components/components/component-preview.js +1 -1
- package/dist/components/components/component-preview.js.map +1 -1
- package/dist/components/components/registry-browser.js +2 -2
- package/dist/components/components/registry-browser.js.map +1 -1
- package/dist/components/components/taxonomy.js +3 -2
- package/dist/components/components/taxonomy.js.map +1 -1
- package/dist/components/components/transfer-dialog.js +1 -1
- package/dist/components/components/transfer-dialog.js.map +1 -1
- package/dist/lib/assets/backfill-thumbs.d.ts +67 -0
- package/dist/lib/assets/backfill-thumbs.js +120 -0
- package/dist/lib/assets/backfill-thumbs.js.map +1 -0
- package/dist/lib/assets/delete-asset.d.ts +2 -1
- package/dist/lib/assets/delete-asset.js +12 -0
- package/dist/lib/assets/delete-asset.js.map +1 -1
- package/dist/lib/assets/index.d.ts +2 -0
- package/dist/lib/assets/index.js +2 -0
- package/dist/lib/assets/index.js.map +1 -1
- package/dist/lib/assets/thumbnail.d.ts +145 -0
- package/dist/lib/assets/thumbnail.js +220 -0
- package/dist/lib/assets/thumbnail.js.map +1 -0
- package/dist/lib/assets/upload.d.ts +9 -0
- package/dist/lib/assets/upload.js +31 -0
- package/dist/lib/assets/upload.js.map +1 -1
- package/dist/lib/auth/auth-adapter.d.ts +22 -0
- package/dist/lib/auth/auth-adapter.js.map +1 -1
- package/dist/lib/auth/convex-auth-adapter.d.ts +42 -1
- package/dist/lib/auth/convex-auth-adapter.js +69 -0
- package/dist/lib/auth/convex-auth-adapter.js.map +1 -1
- package/dist/lib/auth/dev-auto-login-adapter.d.ts +2 -1
- package/dist/lib/auth/dev-auto-login-adapter.js +6 -0
- package/dist/lib/auth/dev-auto-login-adapter.js.map +1 -1
- package/dist/lib/auth/index.d.ts +1 -1
- package/dist/lib/auth/index.js.map +1 -1
- package/dist/lib/auth/local-auth-adapter.d.ts +2 -1
- package/dist/lib/auth/local-auth-adapter.js +7 -0
- package/dist/lib/auth/local-auth-adapter.js.map +1 -1
- package/dist/lib/auth-errors.d.ts +45 -3
- package/dist/lib/auth-errors.js +46 -5
- package/dist/lib/auth-errors.js.map +1 -1
- package/dist/lib/clipboard/paste-target.js +5 -2
- package/dist/lib/clipboard/paste-target.js.map +1 -1
- package/dist/lib/dnd/drop-rules.d.ts +13 -0
- package/dist/lib/dnd/drop-rules.js +15 -0
- package/dist/lib/dnd/drop-rules.js.map +1 -1
- package/dist/lib/i18n/messages.de.js +19 -0
- package/dist/lib/i18n/messages.de.js.map +1 -1
- package/dist/lib/i18n/messages.en.d.ts +16 -0
- package/dist/lib/i18n/messages.en.js +24 -0
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/persistence/convex-adapter.js +4 -0
- package/dist/lib/persistence/convex-adapter.js.map +1 -1
- package/dist/lib/persistence/index.js +6 -3
- package/dist/lib/persistence/index.js.map +1 -1
- package/dist/lib/persona/labels.d.ts +9 -5
- package/dist/lib/persona/labels.js +13 -8
- package/dist/lib/persona/labels.js.map +1 -1
- package/dist/lib/references/references-store.js +7 -1
- package/dist/lib/references/references-store.js.map +1 -1
- package/dist/lib/session-expiry.d.ts +19 -6
- package/dist/lib/session-expiry.js +13 -1
- package/dist/lib/session-expiry.js.map +1 -1
- package/dist/lib/session-revalidation.d.ts +81 -0
- package/dist/lib/session-revalidation.js +94 -0
- package/dist/lib/session-revalidation.js.map +1 -0
- package/package.json +2 -2
- package/src/styles/studio.css +13 -0
|
@@ -23,3 +23,43 @@ import type { InsertContext } from './insert-menu.js';
|
|
|
23
23
|
export declare function resolveInsertContext(builder: {
|
|
24
24
|
present: ComponentNode;
|
|
25
25
|
}, nodeId: string, fieldAllow?: readonly string[], studio?: StudioRegistry): InsertContext | null;
|
|
26
|
+
/**
|
|
27
|
+
* An in-canvas insertion position relative to a reference node (EC-405) — the
|
|
28
|
+
* DnD position vocabulary (`lib/dnd/drop-rules`), narrowed to the three the
|
|
29
|
+
* insertion affordances offer: `before`/`after` a sibling, or `into` an (empty)
|
|
30
|
+
* container. `left`/`right` (the wrap-into-columns gesture) are DnD-only.
|
|
31
|
+
*/
|
|
32
|
+
export type InsertPosition = 'before' | 'after' | 'into';
|
|
33
|
+
/**
|
|
34
|
+
* Resolve an `InsertContext` for an EXPLICIT position relative to `nodeId`
|
|
35
|
+
* (EC-405) — the position-carrying twin of {@link resolveInsertContext}, so an
|
|
36
|
+
* affordance click at a chosen edge lands the insert at exactly that spot rather
|
|
37
|
+
* than the panel re-deriving it from the selection.
|
|
38
|
+
*
|
|
39
|
+
* The slot resolution reuses the shared DnD rules — `into` uses
|
|
40
|
+
* {@link primaryInsertSlot} (the same slot a drop `into` picks; `null` when the
|
|
41
|
+
* node accepts no children), `before`/`after` use the reference's parent slot at
|
|
42
|
+
* `index`/`index + 1` — so this surface and drag-insert can never disagree about
|
|
43
|
+
* where a position lands or what it allows. `fieldAllow` (EC-188) still constrains
|
|
44
|
+
* ONLY a direct child of the composition root, mirroring `resolveInsertContext`.
|
|
45
|
+
*/
|
|
46
|
+
export declare function resolveInsertContextAt(builder: {
|
|
47
|
+
present: ComponentNode;
|
|
48
|
+
}, nodeId: string, position: InsertPosition, fieldAllow?: readonly string[], studio?: StudioRegistry): InsertContext | null;
|
|
49
|
+
/**
|
|
50
|
+
* Which affordance positions are actually insertable at `nodeId` (EC-405 scope 3):
|
|
51
|
+
* a position is offered only when it structurally exists AND at least one registry
|
|
52
|
+
* component may land there (the SAME slot-rule filter the tree/DnD and the blocks
|
|
53
|
+
* panel apply — {@link hasInsertableComponent} over the resolved `slotAllow`), so a
|
|
54
|
+
* blocked spot shows no dead button.
|
|
55
|
+
*
|
|
56
|
+
* An EMPTY, insertable container surfaces ONLY `into` — the meaningful action there
|
|
57
|
+
* is "add the first child", and an empty container often renders zero-height, so
|
|
58
|
+
* its own before/after edges are degenerate and would pile the three "+" buttons
|
|
59
|
+
* onto one point. A filled container and every leaf get before/after instead (each
|
|
60
|
+
* child already carries its own edge affordances, so a filled container needs no
|
|
61
|
+
* center "+").
|
|
62
|
+
*/
|
|
63
|
+
export declare function insertablePositions(builder: {
|
|
64
|
+
present: ComponentNode;
|
|
65
|
+
}, nodeId: string, fieldAllow?: readonly string[], studio?: StudioRegistry): InsertPosition[];
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { getNode, getParentInfo } from '@elytracms/core/project-graph';
|
|
2
2
|
import { getStudioRegistry } from '@elytracms/studio/lib/registry';
|
|
3
3
|
import { shortId } from '@elytracms/studio/lib/persona/labels';
|
|
4
|
-
import {
|
|
4
|
+
import { manifestRules, primaryInsertSlot } from '@elytracms/studio/lib/dnd/drop-rules';
|
|
5
|
+
import { hasInsertableComponent, intersectAllow } from './blocks-model.js';
|
|
5
6
|
/**
|
|
6
7
|
* Where an insert lands: the canvas insert-context resolution, shared by every
|
|
7
8
|
* surface that mounts the canvas engine.
|
|
@@ -26,13 +27,16 @@ export function resolveInsertContext(builder, nodeId, fieldAllow, studio = getSt
|
|
|
26
27
|
const node = getNode(root, nodeId);
|
|
27
28
|
if (!node)
|
|
28
29
|
return null;
|
|
30
|
+
// EC-386: resolve against the merged slot vocabulary (`manifestRules` folds
|
|
31
|
+
// blocks-prop regions in), through the SAME `primaryInsertSlot` rule a DnD
|
|
32
|
+
// `into` uses — so a `project.Section.body`-style region receives the insert
|
|
33
|
+
// instead of silently degrading to a sibling insert.
|
|
29
34
|
const manifest = studio.registry.getManifest(node.componentId);
|
|
30
|
-
const childSlot =
|
|
31
|
-
manifest?.slots.find((s) => s.name !== 'outlet');
|
|
35
|
+
const childSlot = primaryInsertSlot(manifestRules(manifest));
|
|
32
36
|
const ctx = childSlot
|
|
33
37
|
? {
|
|
34
38
|
target: { parentId: nodeId, slot: childSlot.name },
|
|
35
|
-
slotAllow:
|
|
39
|
+
slotAllow: childSlot.allow,
|
|
36
40
|
destinationLabel: `${shortId(node.componentId)} · ${childSlot.name}`,
|
|
37
41
|
}
|
|
38
42
|
: resolveSiblingContext(studio, root, nodeId);
|
|
@@ -48,7 +52,9 @@ export function resolveInsertContext(builder, nodeId, fieldAllow, studio = getSt
|
|
|
48
52
|
function resolveSiblingContext(studio, root, nodeId) {
|
|
49
53
|
const info = getParentInfo(root, nodeId);
|
|
50
54
|
if (info) {
|
|
51
|
-
|
|
55
|
+
// EC-386: the parent slot may be a blocks-prop region — look it up on the
|
|
56
|
+
// merged vocabulary so its `allow` leash still filters sibling inserts.
|
|
57
|
+
const pManifest = manifestRules(studio.registry.getManifest(info.parent.componentId));
|
|
52
58
|
const allow = pManifest?.slots.find((s) => s.name === info.slot)?.allow;
|
|
53
59
|
return {
|
|
54
60
|
target: { parentId: info.parent.id, slot: info.slot, index: info.index + 1 },
|
|
@@ -58,4 +64,90 @@ function resolveSiblingContext(studio, root, nodeId) {
|
|
|
58
64
|
}
|
|
59
65
|
return { target: { parentId: nodeId, slot: 'children' }, destinationLabel: 'root' };
|
|
60
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* Resolve an `InsertContext` for an EXPLICIT position relative to `nodeId`
|
|
69
|
+
* (EC-405) — the position-carrying twin of {@link resolveInsertContext}, so an
|
|
70
|
+
* affordance click at a chosen edge lands the insert at exactly that spot rather
|
|
71
|
+
* than the panel re-deriving it from the selection.
|
|
72
|
+
*
|
|
73
|
+
* The slot resolution reuses the shared DnD rules — `into` uses
|
|
74
|
+
* {@link primaryInsertSlot} (the same slot a drop `into` picks; `null` when the
|
|
75
|
+
* node accepts no children), `before`/`after` use the reference's parent slot at
|
|
76
|
+
* `index`/`index + 1` — so this surface and drag-insert can never disagree about
|
|
77
|
+
* where a position lands or what it allows. `fieldAllow` (EC-188) still constrains
|
|
78
|
+
* ONLY a direct child of the composition root, mirroring `resolveInsertContext`.
|
|
79
|
+
*/
|
|
80
|
+
export function resolveInsertContextAt(builder, nodeId, position, fieldAllow, studio = getStudioRegistry()) {
|
|
81
|
+
const root = builder.present;
|
|
82
|
+
const node = getNode(root, nodeId);
|
|
83
|
+
if (!node)
|
|
84
|
+
return null;
|
|
85
|
+
let ctx;
|
|
86
|
+
if (position === 'into') {
|
|
87
|
+
// EC-386: `manifestRules` folds blocks-prop regions into the slot set, so
|
|
88
|
+
// an affordance/drop `into` a Section-style body lands in that region.
|
|
89
|
+
const manifest = studio.registry.getManifest(node.componentId);
|
|
90
|
+
const slot = primaryInsertSlot(manifestRules(manifest));
|
|
91
|
+
if (!slot)
|
|
92
|
+
return null; // the node accepts no children — no `into` position
|
|
93
|
+
ctx = {
|
|
94
|
+
target: {
|
|
95
|
+
parentId: nodeId,
|
|
96
|
+
slot: slot.name,
|
|
97
|
+
index: (node.slots?.[slot.name] ?? []).length,
|
|
98
|
+
},
|
|
99
|
+
slotAllow: slot.allow,
|
|
100
|
+
destinationLabel: `${shortId(node.componentId)} · ${slot.name}`,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
const info = getParentInfo(root, nodeId);
|
|
105
|
+
if (!info)
|
|
106
|
+
return null; // the root has no siblings — no before/after position
|
|
107
|
+
// EC-386: merged vocabulary — a blocks-prop parent region keeps its leash.
|
|
108
|
+
const pManifest = manifestRules(studio.registry.getManifest(info.parent.componentId));
|
|
109
|
+
ctx = {
|
|
110
|
+
target: {
|
|
111
|
+
parentId: info.parent.id,
|
|
112
|
+
slot: info.slot,
|
|
113
|
+
index: info.index + (position === 'after' ? 1 : 0),
|
|
114
|
+
},
|
|
115
|
+
slotAllow: pManifest?.slots.find((s) => s.name === info.slot)?.allow,
|
|
116
|
+
destinationLabel: `${shortId(info.parent.componentId)} · ${info.slot}`,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
if (fieldAllow && fieldAllow.length > 0 && ctx.target.parentId === root.id) {
|
|
120
|
+
return { ...ctx, slotAllow: intersectAllow(ctx.slotAllow, fieldAllow) };
|
|
121
|
+
}
|
|
122
|
+
return ctx;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Which affordance positions are actually insertable at `nodeId` (EC-405 scope 3):
|
|
126
|
+
* a position is offered only when it structurally exists AND at least one registry
|
|
127
|
+
* component may land there (the SAME slot-rule filter the tree/DnD and the blocks
|
|
128
|
+
* panel apply — {@link hasInsertableComponent} over the resolved `slotAllow`), so a
|
|
129
|
+
* blocked spot shows no dead button.
|
|
130
|
+
*
|
|
131
|
+
* An EMPTY, insertable container surfaces ONLY `into` — the meaningful action there
|
|
132
|
+
* is "add the first child", and an empty container often renders zero-height, so
|
|
133
|
+
* its own before/after edges are degenerate and would pile the three "+" buttons
|
|
134
|
+
* onto one point. A filled container and every leaf get before/after instead (each
|
|
135
|
+
* child already carries its own edge affordances, so a filled container needs no
|
|
136
|
+
* center "+").
|
|
137
|
+
*/
|
|
138
|
+
export function insertablePositions(builder, nodeId, fieldAllow, studio = getStudioRegistry()) {
|
|
139
|
+
// Empty insertable container → into only.
|
|
140
|
+
const into = resolveInsertContextAt(builder, nodeId, 'into', fieldAllow, studio);
|
|
141
|
+
if (into && (into.target.index ?? 0) === 0 && hasInsertableComponent(studio, into.slotAllow)) {
|
|
142
|
+
return ['into'];
|
|
143
|
+
}
|
|
144
|
+
// Otherwise the sibling edges that accept something.
|
|
145
|
+
const out = [];
|
|
146
|
+
for (const position of ['before', 'after']) {
|
|
147
|
+
const ctx = resolveInsertContextAt(builder, nodeId, position, fieldAllow, studio);
|
|
148
|
+
if (ctx && hasInsertableComponent(studio, ctx.slotAllow))
|
|
149
|
+
out.push(position);
|
|
150
|
+
}
|
|
151
|
+
return out;
|
|
152
|
+
}
|
|
61
153
|
//# sourceMappingURL=insert-context.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"insert-context.js","sourceRoot":"","sources":["../../../src/components/builder/insert-context.ts"],"names":[],"mappings":"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","sourcesContent":["import { getNode, getParentInfo } from '@elytracms/core/project-graph'\nimport type { ComponentNode } from '@elytracms/core/project-graph'\nimport { getStudioRegistry } from '@elytracms/studio/lib/registry'\nimport type { StudioRegistry } from '@elytracms/studio/lib/registry'\nimport { shortId } from '@elytracms/studio/lib/persona/labels'\nimport { intersectAllow } from './blocks-model'\nimport type { InsertContext } from './insert-menu'\n\n/**\n * Where an insert lands: the canvas insert-context resolution, shared by every\n * surface that mounts the canvas engine.\n *\n * Extracted from the retired standalone `BuilderShell` (EC-210) so the in-CMS\n * Canvas editor (`composition-field-editor.tsx`, EC-188/AD-12) can own it without\n * importing the dead shell. No UI here — pure graph/registry resolution.\n */\n\n/**\n * Resolve where an insert lands: into the node's child slot, else as a sibling.\n *\n * `fieldAllow` (EC-188) is the composition field's TOP-LEVEL vocabulary\n * (`field.allow`). It constrains ONLY inserts that land as a direct child of the\n * active page root — the top level of a composition field value — where it is\n * intersected into `slotAllow` so forbidden components are never even offered,\n * mirroring the top-level leash in `validateCompositionValue`. Deeper inserts keep\n * following each component's own slot `allow`. Callers that omit `fieldAllow` get\n * an unconstrained top level.\n */\nexport function resolveInsertContext(\n builder: { present: ComponentNode },\n nodeId: string,\n fieldAllow?: readonly string[],\n studio: StudioRegistry = getStudioRegistry(),\n): InsertContext | null {\n const root = builder.present\n const node = getNode(root, nodeId)\n if (!node) return null\n const manifest = studio.registry.getManifest(node.componentId)\n const childSlot =\n manifest?.slots.find((s) => s.name === 'children') ??\n manifest?.slots.find((s) => s.name !== 'outlet')\n const ctx: InsertContext = childSlot\n ? {\n target: { parentId: nodeId, slot: childSlot.name },\n slotAllow: manifest!.slots.find((s) => s.name === childSlot.name)?.allow,\n destinationLabel: `${shortId(node.componentId)} · ${childSlot.name}`,\n }\n : resolveSiblingContext(studio, root, nodeId)\n\n // The field vocabulary governs only the top level (a direct child of the page\n // root). `length > 0` matches the validator, which treats an empty allow as\n // \"allow any\" (validate.ts).\n if (fieldAllow && fieldAllow.length > 0 && ctx.target.parentId === root.id) {\n return { ...ctx, slotAllow: intersectAllow(ctx.slotAllow, fieldAllow) }\n }\n return ctx\n}\n\n/** Sibling-or-root insert context for a node with no insertable child slot. */\nfunction resolveSiblingContext(\n studio: StudioRegistry,\n root: ComponentNode,\n nodeId: string,\n): InsertContext {\n const info = getParentInfo(root, nodeId)\n if (info) {\n const pManifest = studio.registry.getManifest(info.parent.componentId)\n const allow = pManifest?.slots.find((s) => s.name === info.slot)?.allow\n return {\n target: { parentId: info.parent.id, slot: info.slot, index: info.index + 1 },\n slotAllow: allow,\n destinationLabel: `${shortId(info.parent.componentId)} · ${info.slot}`,\n }\n }\n return { target: { parentId: nodeId, slot: 'children' }, destinationLabel: 'root' }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"insert-context.js","sourceRoot":"","sources":["../../../src/components/builder/insert-context.ts"],"names":[],"mappings":"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","sourcesContent":["import { getNode, getParentInfo } from '@elytracms/core/project-graph'\nimport type { ComponentNode } from '@elytracms/core/project-graph'\nimport { getStudioRegistry } from '@elytracms/studio/lib/registry'\nimport type { StudioRegistry } from '@elytracms/studio/lib/registry'\nimport { shortId } from '@elytracms/studio/lib/persona/labels'\nimport { manifestRules, primaryInsertSlot } from '@elytracms/studio/lib/dnd/drop-rules'\nimport { hasInsertableComponent, intersectAllow } from './blocks-model'\nimport type { InsertContext } from './insert-menu'\n\n/**\n * Where an insert lands: the canvas insert-context resolution, shared by every\n * surface that mounts the canvas engine.\n *\n * Extracted from the retired standalone `BuilderShell` (EC-210) so the in-CMS\n * Canvas editor (`composition-field-editor.tsx`, EC-188/AD-12) can own it without\n * importing the dead shell. No UI here — pure graph/registry resolution.\n */\n\n/**\n * Resolve where an insert lands: into the node's child slot, else as a sibling.\n *\n * `fieldAllow` (EC-188) is the composition field's TOP-LEVEL vocabulary\n * (`field.allow`). It constrains ONLY inserts that land as a direct child of the\n * active page root — the top level of a composition field value — where it is\n * intersected into `slotAllow` so forbidden components are never even offered,\n * mirroring the top-level leash in `validateCompositionValue`. Deeper inserts keep\n * following each component's own slot `allow`. Callers that omit `fieldAllow` get\n * an unconstrained top level.\n */\nexport function resolveInsertContext(\n builder: { present: ComponentNode },\n nodeId: string,\n fieldAllow?: readonly string[],\n studio: StudioRegistry = getStudioRegistry(),\n): InsertContext | null {\n const root = builder.present\n const node = getNode(root, nodeId)\n if (!node) return null\n // EC-386: resolve against the merged slot vocabulary (`manifestRules` folds\n // blocks-prop regions in), through the SAME `primaryInsertSlot` rule a DnD\n // `into` uses — so a `project.Section.body`-style region receives the insert\n // instead of silently degrading to a sibling insert.\n const manifest = studio.registry.getManifest(node.componentId)\n const childSlot = primaryInsertSlot(manifestRules(manifest))\n const ctx: InsertContext = childSlot\n ? {\n target: { parentId: nodeId, slot: childSlot.name },\n slotAllow: childSlot.allow,\n destinationLabel: `${shortId(node.componentId)} · ${childSlot.name}`,\n }\n : resolveSiblingContext(studio, root, nodeId)\n\n // The field vocabulary governs only the top level (a direct child of the page\n // root). `length > 0` matches the validator, which treats an empty allow as\n // \"allow any\" (validate.ts).\n if (fieldAllow && fieldAllow.length > 0 && ctx.target.parentId === root.id) {\n return { ...ctx, slotAllow: intersectAllow(ctx.slotAllow, fieldAllow) }\n }\n return ctx\n}\n\n/** Sibling-or-root insert context for a node with no insertable child slot. */\nfunction resolveSiblingContext(studio: StudioRegistry, root: ComponentNode, nodeId: string): InsertContext {\n const info = getParentInfo(root, nodeId)\n if (info) {\n // EC-386: the parent slot may be a blocks-prop region — look it up on the\n // merged vocabulary so its `allow` leash still filters sibling inserts.\n const pManifest = manifestRules(studio.registry.getManifest(info.parent.componentId))\n const allow = pManifest?.slots.find((s) => s.name === info.slot)?.allow\n return {\n target: { parentId: info.parent.id, slot: info.slot, index: info.index + 1 },\n slotAllow: allow,\n destinationLabel: `${shortId(info.parent.componentId)} · ${info.slot}`,\n }\n }\n return { target: { parentId: nodeId, slot: 'children' }, destinationLabel: 'root' }\n}\n\n/**\n * An in-canvas insertion position relative to a reference node (EC-405) — the\n * DnD position vocabulary (`lib/dnd/drop-rules`), narrowed to the three the\n * insertion affordances offer: `before`/`after` a sibling, or `into` an (empty)\n * container. `left`/`right` (the wrap-into-columns gesture) are DnD-only.\n */\nexport type InsertPosition = 'before' | 'after' | 'into'\n\n/**\n * Resolve an `InsertContext` for an EXPLICIT position relative to `nodeId`\n * (EC-405) — the position-carrying twin of {@link resolveInsertContext}, so an\n * affordance click at a chosen edge lands the insert at exactly that spot rather\n * than the panel re-deriving it from the selection.\n *\n * The slot resolution reuses the shared DnD rules — `into` uses\n * {@link primaryInsertSlot} (the same slot a drop `into` picks; `null` when the\n * node accepts no children), `before`/`after` use the reference's parent slot at\n * `index`/`index + 1` — so this surface and drag-insert can never disagree about\n * where a position lands or what it allows. `fieldAllow` (EC-188) still constrains\n * ONLY a direct child of the composition root, mirroring `resolveInsertContext`.\n */\nexport function resolveInsertContextAt(\n builder: { present: ComponentNode },\n nodeId: string,\n position: InsertPosition,\n fieldAllow?: readonly string[],\n studio: StudioRegistry = getStudioRegistry(),\n): InsertContext | null {\n const root = builder.present\n const node = getNode(root, nodeId)\n if (!node) return null\n\n let ctx: InsertContext\n if (position === 'into') {\n // EC-386: `manifestRules` folds blocks-prop regions into the slot set, so\n // an affordance/drop `into` a Section-style body lands in that region.\n const manifest = studio.registry.getManifest(node.componentId)\n const slot = primaryInsertSlot(manifestRules(manifest))\n if (!slot) return null // the node accepts no children — no `into` position\n ctx = {\n target: {\n parentId: nodeId,\n slot: slot.name,\n index: (node.slots?.[slot.name] ?? []).length,\n },\n slotAllow: slot.allow,\n destinationLabel: `${shortId(node.componentId)} · ${slot.name}`,\n }\n } else {\n const info = getParentInfo(root, nodeId)\n if (!info) return null // the root has no siblings — no before/after position\n // EC-386: merged vocabulary — a blocks-prop parent region keeps its leash.\n const pManifest = manifestRules(studio.registry.getManifest(info.parent.componentId))\n ctx = {\n target: {\n parentId: info.parent.id,\n slot: info.slot,\n index: info.index + (position === 'after' ? 1 : 0),\n },\n slotAllow: pManifest?.slots.find((s) => s.name === info.slot)?.allow,\n destinationLabel: `${shortId(info.parent.componentId)} · ${info.slot}`,\n }\n }\n\n if (fieldAllow && fieldAllow.length > 0 && ctx.target.parentId === root.id) {\n return { ...ctx, slotAllow: intersectAllow(ctx.slotAllow, fieldAllow) }\n }\n return ctx\n}\n\n/**\n * Which affordance positions are actually insertable at `nodeId` (EC-405 scope 3):\n * a position is offered only when it structurally exists AND at least one registry\n * component may land there (the SAME slot-rule filter the tree/DnD and the blocks\n * panel apply — {@link hasInsertableComponent} over the resolved `slotAllow`), so a\n * blocked spot shows no dead button.\n *\n * An EMPTY, insertable container surfaces ONLY `into` — the meaningful action there\n * is \"add the first child\", and an empty container often renders zero-height, so\n * its own before/after edges are degenerate and would pile the three \"+\" buttons\n * onto one point. A filled container and every leaf get before/after instead (each\n * child already carries its own edge affordances, so a filled container needs no\n * center \"+\").\n */\nexport function insertablePositions(\n builder: { present: ComponentNode },\n nodeId: string,\n fieldAllow?: readonly string[],\n studio: StudioRegistry = getStudioRegistry(),\n): InsertPosition[] {\n // Empty insertable container → into only.\n const into = resolveInsertContextAt(builder, nodeId, 'into', fieldAllow, studio)\n if (into && (into.target.index ?? 0) === 0 && hasInsertableComponent(studio, into.slotAllow)) {\n return ['into']\n }\n // Otherwise the sibling edges that accept something.\n const out: InsertPosition[] = []\n for (const position of ['before', 'after'] as const) {\n const ctx = resolveInsertContextAt(builder, nodeId, position, fieldAllow, studio)\n if (ctx && hasInsertableComponent(studio, ctx.slotAllow)) out.push(position)\n }\n return out\n}\n"]}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { ComponentNode } from '@elytracms/core/project-graph';
|
|
2
|
+
import type { ComponentManifest } from '@elytracms/core/component-registry';
|
|
3
|
+
/**
|
|
4
|
+
* Node display titles (EC-407) — the ONE helper behind both the page-tree row and
|
|
5
|
+
* the EC-404 selection-overlay frame label.
|
|
6
|
+
*
|
|
7
|
+
* A placed instance shows a derived **instance title** (its own text content, e.g. a
|
|
8
|
+
* heading's words) as the primary label, with the localized **component name** as
|
|
9
|
+
* muted secondary context; a node with no derivable instance title shows the
|
|
10
|
+
* component name alone. This replaces the old tree row's redundant `shortId` (mono
|
|
11
|
+
* id) + `title` pair — the raw id stays one hover away in the inspector header +
|
|
12
|
+
* the tree row `title=` attribute, so agent-inspectability is not lost.
|
|
13
|
+
*
|
|
14
|
+
* Placement: this lives in the studio (not `component-registry`) because the
|
|
15
|
+
* cheap rich-text head reuses `@elytracms/core/rich-text` `richTextPlainText`,
|
|
16
|
+
* which the DAG bars `component-registry` from importing. The pure derivation is
|
|
17
|
+
* mirrored from the CMS `resolveDocumentTitle` pattern (declared field/function +
|
|
18
|
+
* fallback), not shared with it.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* Prop names, in priority order, whose value best titles an instance (EC-407). The
|
|
22
|
+
* name-affinity phase of {@link deriveInstanceTitle}: identity-ish props first
|
|
23
|
+
* (`title`/`heading`/`label`), then generic text (`text`/`name`/`value`), then media
|
|
24
|
+
* captions (`caption`/`alt`, so an Image titles by its alt text, not its URL `src`).
|
|
25
|
+
*/
|
|
26
|
+
export declare const INSTANCE_TITLE_AFFINITY: readonly ["title", "heading", "label", "text", "name", "value", "caption", "alt"];
|
|
27
|
+
/** Max characters of a derived instance title before it is truncated for a row/frame. */
|
|
28
|
+
export declare const INSTANCE_TITLE_MAX = 64;
|
|
29
|
+
export interface NodeDisplayTitle {
|
|
30
|
+
/** The main label a surface shows: the instance title when derivable, else the component name. */
|
|
31
|
+
primary: string;
|
|
32
|
+
/** The localized component name — shown as muted secondary context ONLY when an instance title distinguishes `primary`. */
|
|
33
|
+
secondary?: string;
|
|
34
|
+
/** The localized component TYPE name (always present). */
|
|
35
|
+
componentName: string;
|
|
36
|
+
/** The derived per-instance title (already truncated), when one could be derived. */
|
|
37
|
+
instanceTitle?: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Derive a placed instance's raw (untruncated) title from its effective prop values
|
|
41
|
+
* (EC-407), or `undefined` when nothing text-ish is set. Pure; mirrors the CMS
|
|
42
|
+
* `resolveDocumentTitle` precedence:
|
|
43
|
+
*
|
|
44
|
+
* 1. A declared `nodeTitle` **function** — composed from the values (throws are
|
|
45
|
+
* swallowed so a bad pattern never breaks the tree).
|
|
46
|
+
* 2. A declared `nodeTitle` **prop name** — that prop's display text.
|
|
47
|
+
* 3. **Name-affinity heuristic** — the first {@link INSTANCE_TITLE_AFFINITY} prop
|
|
48
|
+
* that carries display text.
|
|
49
|
+
* 4. **Fallback** — the first declared text-ish prop (declaration order) with a
|
|
50
|
+
* non-URL value.
|
|
51
|
+
*/
|
|
52
|
+
export declare function deriveInstanceTitle(manifest: ComponentManifest | undefined, node: ComponentNode): string | undefined;
|
|
53
|
+
/**
|
|
54
|
+
* The unified node label (EC-407) feeding the page-tree row AND the EC-404 overlay
|
|
55
|
+
* frame. `primary` is the derived instance title when one exists (truncated), else
|
|
56
|
+
* the localized component name; `secondary` is the localized component name, present
|
|
57
|
+
* only when an instance title distinguishes the row. `locale` is optional — omit it
|
|
58
|
+
* to resolve the component name against the active studio-UI locale.
|
|
59
|
+
*/
|
|
60
|
+
export declare function nodeDisplayTitle(manifest: ComponentManifest | undefined, node: ComponentNode, locale?: string): NodeDisplayTitle;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { effectivePropValues } from '@elytracms/core/component-registry';
|
|
2
|
+
import { richTextPlainText } from '@elytracms/core/rich-text';
|
|
3
|
+
import { componentDisplayName } from '@elytracms/studio/lib/persona/labels';
|
|
4
|
+
/**
|
|
5
|
+
* Node display titles (EC-407) — the ONE helper behind both the page-tree row and
|
|
6
|
+
* the EC-404 selection-overlay frame label.
|
|
7
|
+
*
|
|
8
|
+
* A placed instance shows a derived **instance title** (its own text content, e.g. a
|
|
9
|
+
* heading's words) as the primary label, with the localized **component name** as
|
|
10
|
+
* muted secondary context; a node with no derivable instance title shows the
|
|
11
|
+
* component name alone. This replaces the old tree row's redundant `shortId` (mono
|
|
12
|
+
* id) + `title` pair — the raw id stays one hover away in the inspector header +
|
|
13
|
+
* the tree row `title=` attribute, so agent-inspectability is not lost.
|
|
14
|
+
*
|
|
15
|
+
* Placement: this lives in the studio (not `component-registry`) because the
|
|
16
|
+
* cheap rich-text head reuses `@elytracms/core/rich-text` `richTextPlainText`,
|
|
17
|
+
* which the DAG bars `component-registry` from importing. The pure derivation is
|
|
18
|
+
* mirrored from the CMS `resolveDocumentTitle` pattern (declared field/function +
|
|
19
|
+
* fallback), not shared with it.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* Prop names, in priority order, whose value best titles an instance (EC-407). The
|
|
23
|
+
* name-affinity phase of {@link deriveInstanceTitle}: identity-ish props first
|
|
24
|
+
* (`title`/`heading`/`label`), then generic text (`text`/`name`/`value`), then media
|
|
25
|
+
* captions (`caption`/`alt`, so an Image titles by its alt text, not its URL `src`).
|
|
26
|
+
*/
|
|
27
|
+
export const INSTANCE_TITLE_AFFINITY = [
|
|
28
|
+
'title',
|
|
29
|
+
'heading',
|
|
30
|
+
'label',
|
|
31
|
+
'text',
|
|
32
|
+
'name',
|
|
33
|
+
'value',
|
|
34
|
+
'caption',
|
|
35
|
+
'alt',
|
|
36
|
+
];
|
|
37
|
+
/** Max characters of a derived instance title before it is truncated for a row/frame. */
|
|
38
|
+
export const INSTANCE_TITLE_MAX = 64;
|
|
39
|
+
/** A value that looks like a URL / path / protocol — never a useful title. */
|
|
40
|
+
const URL_LIKE = /^(https?:\/\/|\/|#|mailto:|tel:|data:)/i;
|
|
41
|
+
/**
|
|
42
|
+
* Non-empty display text a text-ish prop value carries, else `undefined`.
|
|
43
|
+
* - `text` props: the trimmed string (rejected when `rejectUrl` and it looks URL-ish).
|
|
44
|
+
* - `richText` props: the cheap first-run plain-text head (`richTextPlainText`).
|
|
45
|
+
* - everything else (number/boolean/date/select/relation/asset/blocks/object): not text-ish.
|
|
46
|
+
*/
|
|
47
|
+
function propDisplayText(field, value, rejectUrl) {
|
|
48
|
+
if (field.type === 'text') {
|
|
49
|
+
if (typeof value !== 'string')
|
|
50
|
+
return undefined;
|
|
51
|
+
const trimmed = value.trim();
|
|
52
|
+
if (trimmed.length === 0)
|
|
53
|
+
return undefined;
|
|
54
|
+
if (rejectUrl && URL_LIKE.test(trimmed))
|
|
55
|
+
return undefined;
|
|
56
|
+
return trimmed;
|
|
57
|
+
}
|
|
58
|
+
if (field.type === 'richText') {
|
|
59
|
+
const head = richTextPlainText(value)?.trim();
|
|
60
|
+
return head && head.length > 0 ? head : undefined;
|
|
61
|
+
}
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Derive a placed instance's raw (untruncated) title from its effective prop values
|
|
66
|
+
* (EC-407), or `undefined` when nothing text-ish is set. Pure; mirrors the CMS
|
|
67
|
+
* `resolveDocumentTitle` precedence:
|
|
68
|
+
*
|
|
69
|
+
* 1. A declared `nodeTitle` **function** — composed from the values (throws are
|
|
70
|
+
* swallowed so a bad pattern never breaks the tree).
|
|
71
|
+
* 2. A declared `nodeTitle` **prop name** — that prop's display text.
|
|
72
|
+
* 3. **Name-affinity heuristic** — the first {@link INSTANCE_TITLE_AFFINITY} prop
|
|
73
|
+
* that carries display text.
|
|
74
|
+
* 4. **Fallback** — the first declared text-ish prop (declaration order) with a
|
|
75
|
+
* non-URL value.
|
|
76
|
+
*/
|
|
77
|
+
export function deriveInstanceTitle(manifest, node) {
|
|
78
|
+
if (!manifest)
|
|
79
|
+
return undefined;
|
|
80
|
+
const values = effectivePropValues(manifest, node);
|
|
81
|
+
const declared = manifest.nodeTitle;
|
|
82
|
+
if (typeof declared === 'function') {
|
|
83
|
+
try {
|
|
84
|
+
const out = declared(values);
|
|
85
|
+
if (typeof out === 'string' && out.trim())
|
|
86
|
+
return out.trim();
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// A throwing title pattern must never break the editor — fall through.
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
else if (typeof declared === 'string') {
|
|
93
|
+
const field = manifest.props[declared];
|
|
94
|
+
const text = field ? propDisplayText(field, values[declared], false) : undefined;
|
|
95
|
+
if (text)
|
|
96
|
+
return text;
|
|
97
|
+
}
|
|
98
|
+
for (const name of INSTANCE_TITLE_AFFINITY) {
|
|
99
|
+
const field = manifest.props[name];
|
|
100
|
+
if (!field)
|
|
101
|
+
continue;
|
|
102
|
+
const text = propDisplayText(field, values[name], false);
|
|
103
|
+
if (text)
|
|
104
|
+
return text;
|
|
105
|
+
}
|
|
106
|
+
for (const [name, field] of Object.entries(manifest.props)) {
|
|
107
|
+
const text = propDisplayText(field, values[name], true);
|
|
108
|
+
if (text)
|
|
109
|
+
return text;
|
|
110
|
+
}
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
/** Truncate a title for a compact row / frame bar, appending an ellipsis. */
|
|
114
|
+
function truncate(value, max) {
|
|
115
|
+
return value.length <= max ? value : `${value.slice(0, max - 1).trimEnd()}…`;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* The unified node label (EC-407) feeding the page-tree row AND the EC-404 overlay
|
|
119
|
+
* frame. `primary` is the derived instance title when one exists (truncated), else
|
|
120
|
+
* the localized component name; `secondary` is the localized component name, present
|
|
121
|
+
* only when an instance title distinguishes the row. `locale` is optional — omit it
|
|
122
|
+
* to resolve the component name against the active studio-UI locale.
|
|
123
|
+
*/
|
|
124
|
+
export function nodeDisplayTitle(manifest, node, locale) {
|
|
125
|
+
const componentName = componentDisplayName(node.componentId, manifest, locale);
|
|
126
|
+
const raw = deriveInstanceTitle(manifest, node);
|
|
127
|
+
const instanceTitle = raw ? truncate(raw, INSTANCE_TITLE_MAX) : undefined;
|
|
128
|
+
return {
|
|
129
|
+
componentName,
|
|
130
|
+
instanceTitle,
|
|
131
|
+
primary: instanceTitle ?? componentName,
|
|
132
|
+
secondary: instanceTitle ? componentName : undefined,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
//# sourceMappingURL=node-title.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"node-title.js","sourceRoot":"","sources":["../../../src/components/builder/node-title.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ComponentNode } from '@elytracms/core/project-graph'\nimport type { ComponentManifest, PropField } from '@elytracms/core/component-registry'\nimport { effectivePropValues } from '@elytracms/core/component-registry'\nimport { richTextPlainText } from '@elytracms/core/rich-text'\nimport { componentDisplayName } from '@elytracms/studio/lib/persona/labels'\n\n/**\n * Node display titles (EC-407) — the ONE helper behind both the page-tree row and\n * the EC-404 selection-overlay frame label.\n *\n * A placed instance shows a derived **instance title** (its own text content, e.g. a\n * heading's words) as the primary label, with the localized **component name** as\n * muted secondary context; a node with no derivable instance title shows the\n * component name alone. This replaces the old tree row's redundant `shortId` (mono\n * id) + `title` pair — the raw id stays one hover away in the inspector header +\n * the tree row `title=` attribute, so agent-inspectability is not lost.\n *\n * Placement: this lives in the studio (not `component-registry`) because the\n * cheap rich-text head reuses `@elytracms/core/rich-text` `richTextPlainText`,\n * which the DAG bars `component-registry` from importing. The pure derivation is\n * mirrored from the CMS `resolveDocumentTitle` pattern (declared field/function +\n * fallback), not shared with it.\n */\n\n/**\n * Prop names, in priority order, whose value best titles an instance (EC-407). The\n * name-affinity phase of {@link deriveInstanceTitle}: identity-ish props first\n * (`title`/`heading`/`label`), then generic text (`text`/`name`/`value`), then media\n * captions (`caption`/`alt`, so an Image titles by its alt text, not its URL `src`).\n */\nexport const INSTANCE_TITLE_AFFINITY = [\n 'title',\n 'heading',\n 'label',\n 'text',\n 'name',\n 'value',\n 'caption',\n 'alt',\n] as const\n\n/** Max characters of a derived instance title before it is truncated for a row/frame. */\nexport const INSTANCE_TITLE_MAX = 64\n\n/** A value that looks like a URL / path / protocol — never a useful title. */\nconst URL_LIKE = /^(https?:\\/\\/|\\/|#|mailto:|tel:|data:)/i\n\nexport interface NodeDisplayTitle {\n /** The main label a surface shows: the instance title when derivable, else the component name. */\n primary: string\n /** The localized component name — shown as muted secondary context ONLY when an instance title distinguishes `primary`. */\n secondary?: string\n /** The localized component TYPE name (always present). */\n componentName: string\n /** The derived per-instance title (already truncated), when one could be derived. */\n instanceTitle?: string\n}\n\n/**\n * Non-empty display text a text-ish prop value carries, else `undefined`.\n * - `text` props: the trimmed string (rejected when `rejectUrl` and it looks URL-ish).\n * - `richText` props: the cheap first-run plain-text head (`richTextPlainText`).\n * - everything else (number/boolean/date/select/relation/asset/blocks/object): not text-ish.\n */\nfunction propDisplayText(field: PropField, value: unknown, rejectUrl: boolean): string | undefined {\n if (field.type === 'text') {\n if (typeof value !== 'string') return undefined\n const trimmed = value.trim()\n if (trimmed.length === 0) return undefined\n if (rejectUrl && URL_LIKE.test(trimmed)) return undefined\n return trimmed\n }\n if (field.type === 'richText') {\n const head = richTextPlainText(value)?.trim()\n return head && head.length > 0 ? head : undefined\n }\n return undefined\n}\n\n/**\n * Derive a placed instance's raw (untruncated) title from its effective prop values\n * (EC-407), or `undefined` when nothing text-ish is set. Pure; mirrors the CMS\n * `resolveDocumentTitle` precedence:\n *\n * 1. A declared `nodeTitle` **function** — composed from the values (throws are\n * swallowed so a bad pattern never breaks the tree).\n * 2. A declared `nodeTitle` **prop name** — that prop's display text.\n * 3. **Name-affinity heuristic** — the first {@link INSTANCE_TITLE_AFFINITY} prop\n * that carries display text.\n * 4. **Fallback** — the first declared text-ish prop (declaration order) with a\n * non-URL value.\n */\nexport function deriveInstanceTitle(\n manifest: ComponentManifest | undefined,\n node: ComponentNode,\n): string | undefined {\n if (!manifest) return undefined\n const values = effectivePropValues(manifest, node)\n\n const declared = manifest.nodeTitle\n if (typeof declared === 'function') {\n try {\n const out = declared(values)\n if (typeof out === 'string' && out.trim()) return out.trim()\n } catch {\n // A throwing title pattern must never break the editor — fall through.\n }\n } else if (typeof declared === 'string') {\n const field = manifest.props[declared]\n const text = field ? propDisplayText(field, values[declared], false) : undefined\n if (text) return text\n }\n\n for (const name of INSTANCE_TITLE_AFFINITY) {\n const field = manifest.props[name]\n if (!field) continue\n const text = propDisplayText(field, values[name], false)\n if (text) return text\n }\n\n for (const [name, field] of Object.entries(manifest.props)) {\n const text = propDisplayText(field, values[name], true)\n if (text) return text\n }\n\n return undefined\n}\n\n/** Truncate a title for a compact row / frame bar, appending an ellipsis. */\nfunction truncate(value: string, max: number): string {\n return value.length <= max ? value : `${value.slice(0, max - 1).trimEnd()}…`\n}\n\n/**\n * The unified node label (EC-407) feeding the page-tree row AND the EC-404 overlay\n * frame. `primary` is the derived instance title when one exists (truncated), else\n * the localized component name; `secondary` is the localized component name, present\n * only when an instance title distinguishes the row. `locale` is optional — omit it\n * to resolve the component name against the active studio-UI locale.\n */\nexport function nodeDisplayTitle(\n manifest: ComponentManifest | undefined,\n node: ComponentNode,\n locale?: string,\n): NodeDisplayTitle {\n const componentName = componentDisplayName(node.componentId, manifest, locale)\n const raw = deriveInstanceTitle(manifest, node)\n const instanceTitle = raw ? truncate(raw, INSTANCE_TITLE_MAX) : undefined\n return {\n componentName,\n instanceTitle,\n primary: instanceTitle ?? componentName,\n secondary: instanceTitle ? componentName : undefined,\n }\n}\n"]}
|
|
@@ -2,20 +2,22 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
2
2
|
import { Boxes, ChevronUp, ChevronDown, ClipboardCopy, ClipboardPaste, ClipboardX, Copy, EyeOff, Filter, MoreHorizontal, Plus, Trash2, Unlink, } from 'lucide-react';
|
|
3
3
|
import { useEffect, useRef, useState } from 'react';
|
|
4
4
|
import { getParentInfo } from '@elytracms/core/project-graph';
|
|
5
|
-
import { canNestInto, containerFlowAxis, dropIndicatorFor, dropPositionForPointer, resolveDrop, } from '@elytracms/studio/lib/dnd/drop-rules';
|
|
5
|
+
import { canNestInto, containerFlowAxis, dropIndicatorFor, dropPositionForPointer, manifestRules, resolveDrop, } from '@elytracms/studio/lib/dnd/drop-rules';
|
|
6
6
|
import { getStudioRegistry } from '@elytracms/studio/lib/registry';
|
|
7
7
|
import { shortId } from '@elytracms/studio/lib/persona/labels';
|
|
8
|
-
import { translate } from '@elytracms/studio/lib/i18n';
|
|
8
|
+
import { translate, useUiLocale } from '@elytracms/studio/lib/i18n';
|
|
9
9
|
import { useData } from '@elytracms/studio/lib/data';
|
|
10
10
|
import { Button, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, Row, Spacer, Text, } from '@elytracms/studio/ui';
|
|
11
11
|
import { cn } from '@elytracms/studio/lib/utils';
|
|
12
12
|
import { useCollab } from '../collab/collab-context.js';
|
|
13
13
|
import { useBuilder } from './builder-store.js';
|
|
14
14
|
import { attachClipboardListeners, useStudioClipboard } from './clipboard-actions.js';
|
|
15
|
+
import { attachDeleteKeyListener } from './delete-key.js';
|
|
16
|
+
import { nodeDisplayTitle } from './node-title.js';
|
|
15
17
|
import { useReuse } from './reuse-context.js';
|
|
16
18
|
import { activeDrag, beginDrag, endDrag, onDragSessionEnd } from './drag-session.js';
|
|
17
19
|
function flatten(node, depth, rows, meta) {
|
|
18
|
-
rows.push({ id: node.id, componentId: node.componentId, depth, ...meta, condition: node.condition });
|
|
20
|
+
rows.push({ id: node.id, componentId: node.componentId, node, depth, ...meta, condition: node.condition });
|
|
19
21
|
for (const children of Object.values(node.slots ?? {})) {
|
|
20
22
|
children.forEach((child, i) => flatten(child, depth + 1, rows, {
|
|
21
23
|
isFirst: i === 0,
|
|
@@ -40,6 +42,7 @@ export function PageTree({ onAddChild }) {
|
|
|
40
42
|
const studio = getStudioRegistry();
|
|
41
43
|
const data = useData();
|
|
42
44
|
const collab = useCollab();
|
|
45
|
+
const uiLocale = useUiLocale();
|
|
43
46
|
const rows = [];
|
|
44
47
|
flatten(builder.present, 0, rows, { isFirst: true, isLast: true, isRoot: true });
|
|
45
48
|
// Copy/cut/paste (EC-175): keyboard shortcuts for the whole studio window
|
|
@@ -59,6 +62,20 @@ export function PageTree({ onAddChild }) {
|
|
|
59
62
|
getSelection: () => window.getSelection(),
|
|
60
63
|
});
|
|
61
64
|
}, [clipboard, readOnly]);
|
|
65
|
+
// Delete/Backspace removes the selected node from the tree side too (EC-404),
|
|
66
|
+
// beside the clipboard shortcuts. Inert in the read-only published preview and
|
|
67
|
+
// while focus is in a form field / inline editor (isEditingContext).
|
|
68
|
+
const { dispatch } = builder;
|
|
69
|
+
const rootId = builder.present.id;
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
if (readOnly)
|
|
72
|
+
return;
|
|
73
|
+
return attachDeleteKeyListener(window, {
|
|
74
|
+
getSelectedId: () => selectedIdRef.current,
|
|
75
|
+
remove: (id) => dispatch({ type: 'remove', id }),
|
|
76
|
+
getRootId: () => rootId,
|
|
77
|
+
});
|
|
78
|
+
}, [readOnly, rootId, dispatch]);
|
|
62
79
|
// Drag & drop (EC-160): eligibility comes from the same pure rule set the
|
|
63
80
|
// canvas uses (`lib/dnd/drop-rules`); the tree only renders the resolution.
|
|
64
81
|
// One eligible drop dispatches exactly one move/insert action.
|
|
@@ -71,7 +88,9 @@ export function PageTree({ onAddChild }) {
|
|
|
71
88
|
setDrop(null);
|
|
72
89
|
};
|
|
73
90
|
useEffect(() => onDragSessionEnd(() => clearDrop()), []);
|
|
74
|
-
|
|
91
|
+
// EC-386: adapt through `manifestRules` so blocks-prop regions (e.g. a
|
|
92
|
+
// Section's `body`) are drop targets with their `allow` leash, like raw slots.
|
|
93
|
+
const getManifest = (id) => manifestRules(studio.registry.getManifest(id));
|
|
75
94
|
function handleRowDragStart(row, e) {
|
|
76
95
|
if (row.isRoot) {
|
|
77
96
|
e.preventDefault();
|
|
@@ -165,6 +184,7 @@ export function PageTree({ onAddChild }) {
|
|
|
165
184
|
clearDrop();
|
|
166
185
|
}, children: rows.map((row) => {
|
|
167
186
|
const manifest = studio.registry.getManifest(row.componentId);
|
|
187
|
+
const title = nodeDisplayTitle(manifest, row.node, uiLocale);
|
|
168
188
|
const selected = row.id === builder.selectedId;
|
|
169
189
|
const hovered = row.id === builder.hoveredId;
|
|
170
190
|
// EC-088: indicate condition-gated nodes; flag the currently-hidden ones.
|
|
@@ -179,7 +199,7 @@ export function PageTree({ onAddChild }) {
|
|
|
179
199
|
? 'bg-muted text-foreground'
|
|
180
200
|
: 'text-muted-foreground', drop?.rowId === row.id && drop.indicator
|
|
181
201
|
? ROW_DROP_CLASSES[drop.indicator]
|
|
182
|
-
: undefined), children: [_jsxs("button", { type: "button", onClick: () => builder.dispatch({ type: 'select', id: row.id }), className: "flex min-w-0 flex-1 items-center gap-1.5 py-1", style: { paddingLeft: `${6 + row.depth * 14}px` }, children: [_jsx("span", { className: cn('truncate
|
|
202
|
+
: undefined), children: [_jsxs("button", { type: "button", onClick: () => builder.dispatch({ type: 'select', id: row.id }), title: row.componentId, className: "flex min-w-0 flex-1 items-center gap-1.5 py-1", style: { paddingLeft: `${6 + row.depth * 14}px` }, children: [_jsx("span", { className: cn('truncate', condState === 'hidden' && 'line-through opacity-70'), children: title.primary }), title.secondary ? (_jsx("span", { className: "truncate text-muted-foreground/70", children: title.secondary })) : null, condState === 'hidden' ? (_jsx(EyeOff, { className: "size-3 shrink-0 text-warning", "aria-label": translate('builder.pageTree.hiddenByCondition') })) : condState === 'visible' ? (_jsx(Filter, { className: "size-3 shrink-0 text-muted-foreground/60", "aria-label": translate('builder.pageTree.conditional') })) : null] }), _jsxs("div", { className: cn('flex shrink-0 items-center gap-0.5', selected
|
|
183
203
|
? 'opacity-100'
|
|
184
204
|
: 'opacity-0 group-hover:opacity-100 group-focus-within:opacity-100'), children: [_jsx(IconAction, { label: translate('builder.pageTree.addChild'), onClick: () => onAddChild(row.id), children: _jsx(Plus, {}) }), _jsx(RowActionsMenu, { row: row, builder: builder, clipboard: clipboard })] })] }, row.id));
|
|
185
205
|
}) })] }));
|