@elytracms/studio 0.0.43 → 0.0.45
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/cms.ts +59 -21
- package/convex/content.ts +6 -3
- package/convex/delivery.ts +8 -5
- package/convex/triggers.ts +8 -1
- package/dist/components/app-views/studio-config.d.ts +12 -0
- package/dist/components/app-views/studio-config.js.map +1 -1
- package/dist/components/assets/assets-collection-view-source.d.ts +13 -1
- package/dist/components/assets/assets-collection-view-source.js +42 -51
- package/dist/components/assets/assets-collection-view-source.js.map +1 -1
- package/dist/components/cms/cms-store.js +9 -11
- package/dist/components/cms/cms-store.js.map +1 -1
- package/dist/components/cms/detail-editor.js +110 -338
- package/dist/components/cms/detail-editor.js.map +1 -1
- package/dist/components/cms/document-staging.d.ts +8 -3
- package/dist/components/cms/document-staging.js +81 -21
- package/dist/components/cms/document-staging.js.map +1 -1
- package/dist/components/cms/use-compare-selection.d.ts +27 -0
- package/dist/components/cms/use-compare-selection.js +29 -0
- package/dist/components/cms/use-compare-selection.js.map +1 -0
- package/dist/components/cms/use-discard-controller.d.ts +44 -0
- package/dist/components/cms/use-discard-controller.js +51 -0
- package/dist/components/cms/use-discard-controller.js.map +1 -0
- package/dist/components/cms/use-embed-inspector-host.d.ts +12 -0
- package/dist/components/cms/use-embed-inspector-host.js +17 -0
- package/dist/components/cms/use-embed-inspector-host.js.map +1 -0
- package/dist/components/cms/use-field-focus.d.ts +38 -0
- package/dist/components/cms/use-field-focus.js +70 -0
- package/dist/components/cms/use-field-focus.js.map +1 -0
- package/dist/components/cms/use-live-url.d.ts +41 -0
- package/dist/components/cms/use-live-url.js +55 -0
- package/dist/components/cms/use-live-url.js.map +1 -0
- package/dist/components/cms/use-publish-controller.d.ts +49 -0
- package/dist/components/cms/use-publish-controller.js +102 -0
- package/dist/components/cms/use-publish-controller.js.map +1 -0
- package/dist/components/cms/use-published-perspective.d.ts +67 -0
- package/dist/components/cms/use-published-perspective.js +67 -0
- package/dist/components/cms/use-published-perspective.js.map +1 -0
- package/dist/components/cms/use-restore-version.d.ts +57 -0
- package/dist/components/cms/use-restore-version.js +66 -0
- package/dist/components/cms/use-restore-version.js.map +1 -0
- package/dist/components/cms/use-save-mode.d.ts +35 -0
- package/dist/components/cms/use-save-mode.js +54 -0
- package/dist/components/cms/use-save-mode.js.map +1 -0
- package/dist/components/cms/version-compare.js +13 -16
- package/dist/components/cms/version-compare.js.map +1 -1
- package/dist/components/cms/versions-pane.js +21 -19
- package/dist/components/cms/versions-pane.js.map +1 -1
- package/dist/components/components/transfer-dialog.d.ts +11 -6
- package/dist/components/components/transfer-dialog.js +98 -25
- package/dist/components/components/transfer-dialog.js.map +1 -1
- package/dist/components/components/transfer.d.ts +74 -8
- package/dist/components/components/transfer.js +87 -2
- package/dist/components/components/transfer.js.map +1 -1
- package/dist/components/rail-store-status.d.ts +7 -5
- package/dist/components/rail-store-status.js +12 -9
- package/dist/components/rail-store-status.js.map +1 -1
- package/dist/components/rail-view-source.d.ts +111 -0
- package/dist/components/rail-view-source.js +139 -0
- package/dist/components/rail-view-source.js.map +1 -0
- package/dist/components/redirects/redirects-collection-view-source.d.ts +13 -1
- package/dist/components/redirects/redirects-collection-view-source.js +42 -70
- package/dist/components/redirects/redirects-collection-view-source.js.map +1 -1
- package/dist/components/section-state.d.ts +12 -3
- package/dist/components/section-state.js +8 -4
- package/dist/components/section-state.js.map +1 -1
- package/dist/components/users/users-collection-view-source.d.ts +13 -1
- package/dist/components/users/users-collection-view-source.js +60 -79
- package/dist/components/users/users-collection-view-source.js.map +1 -1
- package/dist/lib/ai/anthropic-provider.d.ts +29 -0
- package/dist/lib/ai/anthropic-provider.js +59 -0
- package/dist/lib/ai/anthropic-provider.js.map +1 -0
- package/dist/lib/ai/apply-content.d.ts +9 -0
- package/dist/lib/ai/apply-content.js +170 -0
- package/dist/lib/ai/apply-content.js.map +1 -0
- package/dist/lib/ai/config.d.ts +58 -0
- package/dist/lib/ai/config.js +134 -0
- package/dist/lib/ai/config.js.map +1 -0
- package/dist/lib/ai/fake-provider.d.ts +34 -0
- package/dist/lib/ai/fake-provider.js +31 -0
- package/dist/lib/ai/fake-provider.js.map +1 -0
- package/dist/lib/ai/index.d.ts +22 -0
- package/dist/lib/ai/index.js +23 -0
- package/dist/lib/ai/index.js.map +1 -0
- package/dist/lib/ai/knowledge.d.ts +39 -0
- package/dist/lib/ai/knowledge.js +60 -0
- package/dist/lib/ai/knowledge.js.map +1 -0
- package/dist/lib/ai/provenance.d.ts +20 -0
- package/dist/lib/ai/provenance.js +15 -0
- package/dist/lib/ai/provenance.js.map +1 -0
- package/dist/lib/ai/route.d.ts +24 -0
- package/dist/lib/ai/route.js +90 -0
- package/dist/lib/ai/route.js.map +1 -0
- package/dist/lib/ai/shape.d.ts +35 -0
- package/dist/lib/ai/shape.js +113 -0
- package/dist/lib/ai/shape.js.map +1 -0
- package/dist/lib/ai/types.d.ts +227 -0
- package/dist/lib/ai/types.js +2 -0
- package/dist/lib/ai/types.js.map +1 -0
- package/dist/lib/i18n/messages.en.js +4 -2
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/workspace/index.d.ts +9 -0
- package/dist/lib/workspace/index.js +11 -0
- package/dist/lib/workspace/index.js.map +1 -1
- package/package.json +8 -2
|
@@ -1,41 +1,101 @@
|
|
|
1
1
|
import { isPlaygroundMode } from '@elytracms/studio/lib/persistence';
|
|
2
2
|
/**
|
|
3
|
-
* EC-320 — a tiny cross-mount handoff for a freshly created
|
|
3
|
+
* EC-320 (0.0.44 follow-up) — a tiny cross-mount handoff for a freshly created
|
|
4
|
+
* (or freshly appended-to) document.
|
|
4
5
|
*
|
|
5
|
-
* The "Use in document…" transfer creates a draft in the Components
|
|
6
|
-
* navigates to that document's CMS editor — a FRESH `CmsProvider`
|
|
7
|
-
*
|
|
8
|
-
* `
|
|
9
|
-
*
|
|
10
|
-
* (fixtures) and never reads back the seeded adapter, so a doc created in one
|
|
11
|
-
* mount is invisible to the next.
|
|
6
|
+
* The "Use in document…" transfer creates/updates a draft in the Components
|
|
7
|
+
* view, then navigates to that document's CMS editor — a FRESH `CmsProvider`
|
|
8
|
+
* mount. That fresh mount seeds its documents from whatever hydration/kit
|
|
9
|
+
* snapshot its `CmsSessionGate` resolves, and in BOTH modes that snapshot can
|
|
10
|
+
* miss a document the transfer just wrote — for two DIFFERENT reasons:
|
|
12
11
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
12
|
+
* - **Playground**: `CmsSessionGate` renders synchronously with `session:
|
|
13
|
+
* null` (the EC-138 determinism contract) — it never queries anything, so
|
|
14
|
+
* the seeded set is always the STATIC kit/fixture baseline, forever. It
|
|
15
|
+
* never reflects a write, on the first mount or the hundredth.
|
|
16
|
+
* - **Convex**: the seeded set comes from a REAL fresh-per-mount query
|
|
17
|
+
* (`loadCms()`), so it usually catches up — but there is no synchronous
|
|
18
|
+
* guarantee it observes a write from a moment earlier (belt and braces: the
|
|
19
|
+
* transfer also AWAITS its own write landing — see `transfer-dialog.tsx` —
|
|
20
|
+
* but staging covers the mount regardless of that timing).
|
|
21
|
+
*
|
|
22
|
+
* A previous version of this module gated `stageDocument` itself on
|
|
23
|
+
* `isPlaygroundMode()`, reasoning that Convex mode "re-hydrates from
|
|
24
|
+
* persistence" per mount and so never needs staging. That reasoning was
|
|
25
|
+
* wrong — a fresh query has no synchronous ordering guarantee against a write
|
|
26
|
+
* from moments earlier — and the gate was exactly why the Convex-mode
|
|
27
|
+
* transfer 404'd (EC-320 follow-up finding). Staging now runs in every mode.
|
|
28
|
+
*
|
|
29
|
+
* The two modes DO still genuinely differ on the read side, though — not by a
|
|
30
|
+
* wrong assumption this time, but structurally:
|
|
31
|
+
*
|
|
32
|
+
* - Playground's seeded set can NEVER be "fresher" than a staged write (it
|
|
33
|
+
* never changes at all), so a staged entry always wins over it — exactly
|
|
34
|
+
* the original, working behavior, now also covering "insert into an
|
|
35
|
+
* EXISTING document" (whose key was already in the static baseline before
|
|
36
|
+
* the edit — the append would otherwise be invisible).
|
|
37
|
+
* - Convex's seeded set is a genuine live signal, so a staged entry only
|
|
38
|
+
* fills a GAP it doesn't already carry; once it does, the seeded set wins
|
|
39
|
+
* outright (the newer-or-equal persisted truth) and the stale staged entry
|
|
40
|
+
* is dropped — so it can never resurrect on a later mount and shadow a
|
|
41
|
+
* revision made after the transfer (e.g. a subsequent edit, once persisted
|
|
42
|
+
* and re-hydrated).
|
|
43
|
+
*
|
|
44
|
+
* Reads that don't drop anything are non-destructive (StrictMode-safe: a
|
|
45
|
+
* double mount computes the identical merge both times); the buffer itself is
|
|
46
|
+
* in-memory, session-scoped runtime state — never persisted, never a
|
|
47
|
+
* substitute for the real write.
|
|
19
48
|
*/
|
|
20
49
|
const staged = new Map();
|
|
21
50
|
function docKey(doc) {
|
|
22
51
|
return `${doc.collection}/${doc.id}`;
|
|
23
52
|
}
|
|
24
|
-
/** Stage a document for the next `CmsProvider` mount of `projectId` (
|
|
53
|
+
/** Stage a document for the next `CmsProvider` mount of `projectId` (any mode). */
|
|
25
54
|
export function stageDocument(projectId, doc) {
|
|
26
|
-
if (!isPlaygroundMode())
|
|
27
|
-
return;
|
|
28
55
|
const list = staged.get(projectId) ?? [];
|
|
29
56
|
// Replace any prior staging of the same doc key so a re-transfer wins.
|
|
30
57
|
const next = list.filter((d) => docKey(d) !== docKey(doc));
|
|
31
58
|
next.push(doc);
|
|
32
59
|
staged.set(projectId, next);
|
|
33
60
|
}
|
|
34
|
-
/**
|
|
35
|
-
|
|
61
|
+
/**
|
|
62
|
+
* Merge any documents staged for `projectId` into a fresh mount's
|
|
63
|
+
* seeded/hydrated document list. See the module doc for why the merge rule
|
|
64
|
+
* itself is mode-aware (playground: staged always wins; Convex: staged fills
|
|
65
|
+
* gaps only, dropped once superseded).
|
|
66
|
+
*/
|
|
67
|
+
export function consumeStagedDocuments(projectId, seeded) {
|
|
36
68
|
if (projectId === undefined)
|
|
37
|
-
return [];
|
|
38
|
-
|
|
69
|
+
return [...seeded];
|
|
70
|
+
const list = staged.get(projectId);
|
|
71
|
+
if (!list || list.length === 0)
|
|
72
|
+
return [...seeded];
|
|
73
|
+
if (isPlaygroundMode()) {
|
|
74
|
+
// The seeded set is a static kit/fixture baseline that never reflects a
|
|
75
|
+
// write on its own — a staged doc always wins for its key, matching the
|
|
76
|
+
// ONLY mechanism that ever represents a live edit across mounts here.
|
|
77
|
+
const byKey = new Map(seeded.map((d) => [docKey(d), d]));
|
|
78
|
+
for (const d of list)
|
|
79
|
+
byKey.set(docKey(d), structuredClone(d));
|
|
80
|
+
return [...byKey.values()];
|
|
81
|
+
}
|
|
82
|
+
// Convex: `seeded` is a fresh per-mount hydration query — a real signal of
|
|
83
|
+
// the current persisted truth. A staged doc only fills a gap that snapshot
|
|
84
|
+
// doesn't already carry; a key it DOES carry is superseded and dropped from
|
|
85
|
+
// the buffer so it can never resurrect on a later mount.
|
|
86
|
+
const seededKeys = new Set(seeded.map(docKey));
|
|
87
|
+
const superseded = list.filter((d) => seededKeys.has(docKey(d)));
|
|
88
|
+
if (superseded.length > 0) {
|
|
89
|
+
const remaining = list.filter((d) => !superseded.some((s) => docKey(s) === docKey(d)));
|
|
90
|
+
if (remaining.length > 0)
|
|
91
|
+
staged.set(projectId, remaining);
|
|
92
|
+
else
|
|
93
|
+
staged.delete(projectId);
|
|
94
|
+
}
|
|
95
|
+
const gap = list.filter((d) => !seededKeys.has(docKey(d)));
|
|
96
|
+
if (gap.length === 0)
|
|
97
|
+
return [...seeded];
|
|
98
|
+
return [...seeded, ...gap.map((d) => structuredClone(d))];
|
|
39
99
|
}
|
|
40
100
|
/** Test seam: drop all staged documents. */
|
|
41
101
|
export function clearStagedDocuments() {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"document-staging.js","sourceRoot":"","sources":["../../../src/components/cms/document-staging.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,MAAM,mCAAmC,CAAA;AAEpE
|
|
1
|
+
{"version":3,"file":"document-staging.js","sourceRoot":"","sources":["../../../src/components/cms/document-staging.ts"],"names":[],"mappings":"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","sourcesContent":["import type { CmsDocument } from '@elytracms/core/cms-core'\nimport { isPlaygroundMode } from '@elytracms/studio/lib/persistence'\n\n/**\n * EC-320 (0.0.44 follow-up) — a tiny cross-mount handoff for a freshly created\n * (or freshly appended-to) document.\n *\n * The \"Use in document…\" transfer creates/updates a draft in the Components\n * view, then navigates to that document's CMS editor — a FRESH `CmsProvider`\n * mount. That fresh mount seeds its documents from whatever hydration/kit\n * snapshot its `CmsSessionGate` resolves, and in BOTH modes that snapshot can\n * miss a document the transfer just wrote — for two DIFFERENT reasons:\n *\n * - **Playground**: `CmsSessionGate` renders synchronously with `session:\n * null` (the EC-138 determinism contract) — it never queries anything, so\n * the seeded set is always the STATIC kit/fixture baseline, forever. It\n * never reflects a write, on the first mount or the hundredth.\n * - **Convex**: the seeded set comes from a REAL fresh-per-mount query\n * (`loadCms()`), so it usually catches up — but there is no synchronous\n * guarantee it observes a write from a moment earlier (belt and braces: the\n * transfer also AWAITS its own write landing — see `transfer-dialog.tsx` —\n * but staging covers the mount regardless of that timing).\n *\n * A previous version of this module gated `stageDocument` itself on\n * `isPlaygroundMode()`, reasoning that Convex mode \"re-hydrates from\n * persistence\" per mount and so never needs staging. That reasoning was\n * wrong — a fresh query has no synchronous ordering guarantee against a write\n * from moments earlier — and the gate was exactly why the Convex-mode\n * transfer 404'd (EC-320 follow-up finding). Staging now runs in every mode.\n *\n * The two modes DO still genuinely differ on the read side, though — not by a\n * wrong assumption this time, but structurally:\n *\n * - Playground's seeded set can NEVER be \"fresher\" than a staged write (it\n * never changes at all), so a staged entry always wins over it — exactly\n * the original, working behavior, now also covering \"insert into an\n * EXISTING document\" (whose key was already in the static baseline before\n * the edit — the append would otherwise be invisible).\n * - Convex's seeded set is a genuine live signal, so a staged entry only\n * fills a GAP it doesn't already carry; once it does, the seeded set wins\n * outright (the newer-or-equal persisted truth) and the stale staged entry\n * is dropped — so it can never resurrect on a later mount and shadow a\n * revision made after the transfer (e.g. a subsequent edit, once persisted\n * and re-hydrated).\n *\n * Reads that don't drop anything are non-destructive (StrictMode-safe: a\n * double mount computes the identical merge both times); the buffer itself is\n * in-memory, session-scoped runtime state — never persisted, never a\n * substitute for the real write.\n */\n\nconst staged = new Map<string, CmsDocument[]>()\n\nfunction docKey(doc: CmsDocument): string {\n return `${doc.collection}/${doc.id}`\n}\n\n/** Stage a document for the next `CmsProvider` mount of `projectId` (any mode). */\nexport function stageDocument(projectId: string, doc: CmsDocument): void {\n const list = staged.get(projectId) ?? []\n // Replace any prior staging of the same doc key so a re-transfer wins.\n const next = list.filter((d) => docKey(d) !== docKey(doc))\n next.push(doc)\n staged.set(projectId, next)\n}\n\n/**\n * Merge any documents staged for `projectId` into a fresh mount's\n * seeded/hydrated document list. See the module doc for why the merge rule\n * itself is mode-aware (playground: staged always wins; Convex: staged fills\n * gaps only, dropped once superseded).\n */\nexport function consumeStagedDocuments(\n projectId: string | undefined,\n seeded: readonly CmsDocument[],\n): CmsDocument[] {\n if (projectId === undefined) return [...seeded]\n const list = staged.get(projectId)\n if (!list || list.length === 0) return [...seeded]\n\n if (isPlaygroundMode()) {\n // The seeded set is a static kit/fixture baseline that never reflects a\n // write on its own — a staged doc always wins for its key, matching the\n // ONLY mechanism that ever represents a live edit across mounts here.\n const byKey = new Map(seeded.map((d) => [docKey(d), d]))\n for (const d of list) byKey.set(docKey(d), structuredClone(d))\n return [...byKey.values()]\n }\n\n // Convex: `seeded` is a fresh per-mount hydration query — a real signal of\n // the current persisted truth. A staged doc only fills a gap that snapshot\n // doesn't already carry; a key it DOES carry is superseded and dropped from\n // the buffer so it can never resurrect on a later mount.\n const seededKeys = new Set(seeded.map(docKey))\n const superseded = list.filter((d) => seededKeys.has(docKey(d)))\n if (superseded.length > 0) {\n const remaining = list.filter((d) => !superseded.some((s) => docKey(s) === docKey(d)))\n if (remaining.length > 0) staged.set(projectId, remaining)\n else staged.delete(projectId)\n }\n\n const gap = list.filter((d) => !seededKeys.has(docKey(d)))\n if (gap.length === 0) return [...seeded]\n return [...seeded, ...gap.map((d) => structuredClone(d))]\n}\n\n/** Test seam: drop all staged documents. */\nexport function clearStagedDocuments(): void {\n staged.clear()\n}\n"]}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Dispatch, SetStateAction } from 'react';
|
|
2
|
+
import type { CompareState } from './document-editor-context';
|
|
3
|
+
export interface CompareSelection {
|
|
4
|
+
/** The open version comparison, or null when none (the sticky Compare tab). */
|
|
5
|
+
compare: CompareState | null;
|
|
6
|
+
/** Open (or refocus) the Compare tab with `base` vs the current draft. */
|
|
7
|
+
startCompare: (base: number) => void;
|
|
8
|
+
/** Change which recorded version is the (older) base side. */
|
|
9
|
+
setCompareBase: (base: number) => void;
|
|
10
|
+
/** Change what the base is compared against (a version, or the live draft). */
|
|
11
|
+
setCompareTarget: (target: number | 'draft') => void;
|
|
12
|
+
/** Leave compare mode, back to the first content tab. */
|
|
13
|
+
exitCompare: () => void;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* EC-298 — the version-compare selection. The Compare tab is STICKY: opening it
|
|
17
|
+
* sets the selection, focuses the tab and the Versions pane; tabbing to the real
|
|
18
|
+
* fields keeps the comparison open (the selection persists) so an editor can flip
|
|
19
|
+
* between diff and fields; only its ✕ closes it. Owns the selection; the tab focus
|
|
20
|
+
* and the inspector pane it drives are threaded in from the editor shell.
|
|
21
|
+
*/
|
|
22
|
+
export declare function useCompareSelection(input: {
|
|
23
|
+
tabs: string[];
|
|
24
|
+
compareTab: string;
|
|
25
|
+
setActiveTab: Dispatch<SetStateAction<string>>;
|
|
26
|
+
setInspectorPaneId: (id: string | undefined) => void;
|
|
27
|
+
}): CompareSelection;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { useCallback, useState } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* EC-298 — the version-compare selection. The Compare tab is STICKY: opening it
|
|
4
|
+
* sets the selection, focuses the tab and the Versions pane; tabbing to the real
|
|
5
|
+
* fields keeps the comparison open (the selection persists) so an editor can flip
|
|
6
|
+
* between diff and fields; only its ✕ closes it. Owns the selection; the tab focus
|
|
7
|
+
* and the inspector pane it drives are threaded in from the editor shell.
|
|
8
|
+
*/
|
|
9
|
+
export function useCompareSelection(input) {
|
|
10
|
+
const { tabs, compareTab, setActiveTab, setInspectorPaneId } = input;
|
|
11
|
+
const [compare, setCompare] = useState(null);
|
|
12
|
+
const startCompare = useCallback((base) => {
|
|
13
|
+
setCompare({ base, target: 'draft' });
|
|
14
|
+
setActiveTab(compareTab);
|
|
15
|
+
setInspectorPaneId('versions');
|
|
16
|
+
}, [compareTab, setActiveTab, setInspectorPaneId]);
|
|
17
|
+
const setCompareBase = useCallback((base) => {
|
|
18
|
+
setCompare((c) => (c ? { ...c, base } : { base, target: 'draft' }));
|
|
19
|
+
}, []);
|
|
20
|
+
const setCompareTarget = useCallback((target) => {
|
|
21
|
+
setCompare((c) => (c ? { ...c, target } : c));
|
|
22
|
+
}, []);
|
|
23
|
+
const exitCompare = useCallback(() => {
|
|
24
|
+
setCompare(null);
|
|
25
|
+
setActiveTab((prev) => (prev === compareTab ? tabs[0] : prev));
|
|
26
|
+
}, [tabs, compareTab, setActiveTab]);
|
|
27
|
+
return { compare, startCompare, setCompareBase, setCompareTarget, exitCompare };
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=use-compare-selection.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-compare-selection.js","sourceRoot":"","sources":["../../../src/components/cms/use-compare-selection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAiB7C;;;;;;GAMG;AACH,MAAM,UAAU,mBAAmB,CAAC,KAKnC;IACC,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,YAAY,EAAE,kBAAkB,EAAE,GAAG,KAAK,CAAA;IACpE,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,GAAG,QAAQ,CAAsB,IAAI,CAAC,CAAA;IACjE,MAAM,YAAY,GAAG,WAAW,CAC9B,CAAC,IAAY,EAAE,EAAE;QACf,UAAU,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,CAAA;QACrC,YAAY,CAAC,UAAU,CAAC,CAAA;QACxB,kBAAkB,CAAC,UAAU,CAAC,CAAA;IAChC,CAAC,EACD,CAAC,UAAU,EAAE,YAAY,EAAE,kBAAkB,CAAC,CAC/C,CAAA;IACD,MAAM,cAAc,GAAG,WAAW,CAAC,CAAC,IAAY,EAAE,EAAE;QAClD,UAAU,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC,CAAA;IACrE,CAAC,EAAE,EAAE,CAAC,CAAA;IACN,MAAM,gBAAgB,GAAG,WAAW,CAAC,CAAC,MAAwB,EAAE,EAAE;QAChE,UAAU,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IAC/C,CAAC,EAAE,EAAE,CAAC,CAAA;IACN,MAAM,WAAW,GAAG,WAAW,CAAC,GAAG,EAAE;QACnC,UAAU,CAAC,IAAI,CAAC,CAAA;QAChB,YAAY,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,KAAK,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAA;IACjE,CAAC,EAAE,CAAC,IAAI,EAAE,UAAU,EAAE,YAAY,CAAC,CAAC,CAAA;IAEpC,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,gBAAgB,EAAE,WAAW,EAAE,CAAA;AACjF,CAAC","sourcesContent":["import { useCallback, useState } from 'react'\nimport type { Dispatch, SetStateAction } from 'react'\nimport type { CompareState } from './document-editor-context'\n\nexport interface CompareSelection {\n /** The open version comparison, or null when none (the sticky Compare tab). */\n compare: CompareState | null\n /** Open (or refocus) the Compare tab with `base` vs the current draft. */\n startCompare: (base: number) => void\n /** Change which recorded version is the (older) base side. */\n setCompareBase: (base: number) => void\n /** Change what the base is compared against (a version, or the live draft). */\n setCompareTarget: (target: number | 'draft') => void\n /** Leave compare mode, back to the first content tab. */\n exitCompare: () => void\n}\n\n/**\n * EC-298 — the version-compare selection. The Compare tab is STICKY: opening it\n * sets the selection, focuses the tab and the Versions pane; tabbing to the real\n * fields keeps the comparison open (the selection persists) so an editor can flip\n * between diff and fields; only its ✕ closes it. Owns the selection; the tab focus\n * and the inspector pane it drives are threaded in from the editor shell.\n */\nexport function useCompareSelection(input: {\n tabs: string[]\n compareTab: string\n setActiveTab: Dispatch<SetStateAction<string>>\n setInspectorPaneId: (id: string | undefined) => void\n}): CompareSelection {\n const { tabs, compareTab, setActiveTab, setInspectorPaneId } = input\n const [compare, setCompare] = useState<CompareState | null>(null)\n const startCompare = useCallback(\n (base: number) => {\n setCompare({ base, target: 'draft' })\n setActiveTab(compareTab)\n setInspectorPaneId('versions')\n },\n [compareTab, setActiveTab, setInspectorPaneId],\n )\n const setCompareBase = useCallback((base: number) => {\n setCompare((c) => (c ? { ...c, base } : { base, target: 'draft' }))\n }, [])\n const setCompareTarget = useCallback((target: number | 'draft') => {\n setCompare((c) => (c ? { ...c, target } : c))\n }, [])\n const exitCompare = useCallback(() => {\n setCompare(null)\n setActiveTab((prev) => (prev === compareTab ? tabs[0]! : prev))\n }, [tabs, compareTab, setActiveTab])\n\n return { compare, startCompare, setCompareBase, setCompareTarget, exitCompare }\n}\n"]}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { QueryKey } from '@tanstack/react-query';
|
|
2
|
+
import type { CmsDocument, CollectionDef, DocumentRef } from '@elytracms/core/cms-core';
|
|
3
|
+
import type { CollectionViewSource } from '@elytracms/studio/lib/collection-view/collection-view-source';
|
|
4
|
+
import type { DiscardDraftStatus } from './discard-draft';
|
|
5
|
+
export interface DiscardController {
|
|
6
|
+
status: DiscardDraftStatus;
|
|
7
|
+
/** The hint shown when the item is disabled (view-read-only / up-to-date / loading). */
|
|
8
|
+
disabledReason: string | undefined;
|
|
9
|
+
/** Is the confirmation dialog open? */
|
|
10
|
+
confirmOpen: boolean;
|
|
11
|
+
setConfirmOpen: (open: boolean) => void;
|
|
12
|
+
/** Is a discard restore round-tripping (gates the dialog buttons)? */
|
|
13
|
+
busy: boolean;
|
|
14
|
+
/** Confirm the discard: flush → restore the pinned published version whole. */
|
|
15
|
+
discard: () => Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* EC-316 / EC-325 — the "Discard changes" controller. Reverting the draft to the
|
|
19
|
+
* published content IS a whole-document restore of the pinned published version,
|
|
20
|
+
* so it routes through the SAME shared restore sequence
|
|
21
|
+
* ({@link useRestoreVersion}) the Compare view's and Versions pane's restores use
|
|
22
|
+
* — never a parallel write path. That sequence flushes a dirty in-memory buffer
|
|
23
|
+
* FIRST (`documents.restore` snapshots whatever is currently PERSISTED), closing
|
|
24
|
+
* the undo-guarantee gap the header's discard used to have.
|
|
25
|
+
*
|
|
26
|
+
* The enablement is the pure `discardDraftStatus` decision (tested in
|
|
27
|
+
* `discard-draft.test.ts`); its semantic-equality edge is the SAME comparator the
|
|
28
|
+
* Versions pane's publish gate uses, so the two never contradict (EC-298).
|
|
29
|
+
*/
|
|
30
|
+
export declare function useDiscardController(input: {
|
|
31
|
+
canDiscard: boolean;
|
|
32
|
+
published: boolean;
|
|
33
|
+
viewReadOnly: boolean;
|
|
34
|
+
publishedSnapshotDoc: CmsDocument | undefined;
|
|
35
|
+
publishedVersionNumber: number | undefined;
|
|
36
|
+
versionKey: QueryKey;
|
|
37
|
+
collection: CollectionDef;
|
|
38
|
+
draft: CmsDocument;
|
|
39
|
+
source: CollectionViewSource;
|
|
40
|
+
docRef: DocumentRef;
|
|
41
|
+
replaceDraft: (doc: CmsDocument) => void;
|
|
42
|
+
dirty: boolean;
|
|
43
|
+
save: () => void;
|
|
44
|
+
}): DiscardController;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { useCallback, useState } from 'react';
|
|
2
|
+
import { translate } from '@elytracms/studio/lib/i18n';
|
|
3
|
+
import { discardDraftStatus } from './discard-draft';
|
|
4
|
+
import { useRestoreVersion } from './use-restore-version';
|
|
5
|
+
/**
|
|
6
|
+
* EC-316 / EC-325 — the "Discard changes" controller. Reverting the draft to the
|
|
7
|
+
* published content IS a whole-document restore of the pinned published version,
|
|
8
|
+
* so it routes through the SAME shared restore sequence
|
|
9
|
+
* ({@link useRestoreVersion}) the Compare view's and Versions pane's restores use
|
|
10
|
+
* — never a parallel write path. That sequence flushes a dirty in-memory buffer
|
|
11
|
+
* FIRST (`documents.restore` snapshots whatever is currently PERSISTED), closing
|
|
12
|
+
* the undo-guarantee gap the header's discard used to have.
|
|
13
|
+
*
|
|
14
|
+
* The enablement is the pure `discardDraftStatus` decision (tested in
|
|
15
|
+
* `discard-draft.test.ts`); its semantic-equality edge is the SAME comparator the
|
|
16
|
+
* Versions pane's publish gate uses, so the two never contradict (EC-298).
|
|
17
|
+
*/
|
|
18
|
+
export function useDiscardController(input) {
|
|
19
|
+
const { canDiscard, published, viewReadOnly, publishedSnapshotDoc, publishedVersionNumber, versionKey, collection, draft, source, docRef, replaceDraft, dirty, save, } = input;
|
|
20
|
+
const [confirmOpen, setConfirmOpen] = useState(false);
|
|
21
|
+
const { restore, busy } = useRestoreVersion({ source, docRef, versionKey, dirty, save, replaceDraft });
|
|
22
|
+
const status = discardDraftStatus({
|
|
23
|
+
canDiscard,
|
|
24
|
+
published,
|
|
25
|
+
viewReadOnly,
|
|
26
|
+
publishedSnapshot: publishedSnapshotDoc,
|
|
27
|
+
collection,
|
|
28
|
+
draft,
|
|
29
|
+
});
|
|
30
|
+
const disabledReason = status === 'view-read-only'
|
|
31
|
+
? translate('cms.editor.switchToDraftToDiscard')
|
|
32
|
+
: status === 'up-to-date'
|
|
33
|
+
? translate('cms.editor.discardDraftUpToDateHint')
|
|
34
|
+
: status === 'loading'
|
|
35
|
+
? translate('common.loading')
|
|
36
|
+
: undefined;
|
|
37
|
+
const discard = useCallback(async () => {
|
|
38
|
+
if (publishedVersionNumber === undefined)
|
|
39
|
+
return;
|
|
40
|
+
try {
|
|
41
|
+
// Whole-document restore of the pinned published version (the shared helper
|
|
42
|
+
// flushes any dirty buffer first, mirroring the Versions pane / Compare view).
|
|
43
|
+
await restore(publishedVersionNumber, { kind: 'all' });
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
setConfirmOpen(false);
|
|
47
|
+
}
|
|
48
|
+
}, [publishedVersionNumber, restore]);
|
|
49
|
+
return { status, disabledReason, confirmOpen, setConfirmOpen, busy, discard };
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=use-discard-controller.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-discard-controller.js","sourceRoot":"","sources":["../../../src/components/cms/use-discard-controller.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAI7C,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AACtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAA;AAEpD,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAA;AAezD;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,oBAAoB,CAAC,KAcpC;IACC,MAAM,EACJ,UAAU,EACV,SAAS,EACT,YAAY,EACZ,oBAAoB,EACpB,sBAAsB,EACtB,UAAU,EACV,UAAU,EACV,KAAK,EACL,MAAM,EACN,MAAM,EACN,YAAY,EACZ,KAAK,EACL,IAAI,GACL,GAAG,KAAK,CAAA;IAET,MAAM,CAAC,WAAW,EAAE,cAAc,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAA;IACrD,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,iBAAiB,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,IAAI,EAAE,YAAY,EAAE,CAAC,CAAA;IAEtG,MAAM,MAAM,GAAG,kBAAkB,CAAC;QAChC,UAAU;QACV,SAAS;QACT,YAAY;QACZ,iBAAiB,EAAE,oBAAoB;QACvC,UAAU;QACV,KAAK;KACN,CAAC,CAAA;IACF,MAAM,cAAc,GAClB,MAAM,KAAK,gBAAgB;QACzB,CAAC,CAAC,SAAS,CAAC,mCAAmC,CAAC;QAChD,CAAC,CAAC,MAAM,KAAK,YAAY;YACvB,CAAC,CAAC,SAAS,CAAC,qCAAqC,CAAC;YAClD,CAAC,CAAC,MAAM,KAAK,SAAS;gBACpB,CAAC,CAAC,SAAS,CAAC,gBAAgB,CAAC;gBAC7B,CAAC,CAAC,SAAS,CAAA;IAEnB,MAAM,OAAO,GAAG,WAAW,CAAC,KAAK,IAAI,EAAE;QACrC,IAAI,sBAAsB,KAAK,SAAS;YAAE,OAAM;QAChD,IAAI,CAAC;YACH,4EAA4E;YAC5E,+EAA+E;YAC/E,MAAM,OAAO,CAAC,sBAAsB,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAA;QACxD,CAAC;gBAAS,CAAC;YACT,cAAc,CAAC,KAAK,CAAC,CAAA;QACvB,CAAC;IACH,CAAC,EAAE,CAAC,sBAAsB,EAAE,OAAO,CAAC,CAAC,CAAA;IAErC,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,WAAW,EAAE,cAAc,EAAE,IAAI,EAAE,OAAO,EAAE,CAAA;AAC/E,CAAC","sourcesContent":["import { useCallback, useState } from 'react'\nimport type { QueryKey } from '@tanstack/react-query'\nimport type { CmsDocument, CollectionDef, DocumentRef } from '@elytracms/core/cms-core'\nimport type { CollectionViewSource } from '@elytracms/studio/lib/collection-view/collection-view-source'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { discardDraftStatus } from './discard-draft'\nimport type { DiscardDraftStatus } from './discard-draft'\nimport { useRestoreVersion } from './use-restore-version'\n\nexport interface DiscardController {\n status: DiscardDraftStatus\n /** The hint shown when the item is disabled (view-read-only / up-to-date / loading). */\n disabledReason: string | undefined\n /** Is the confirmation dialog open? */\n confirmOpen: boolean\n setConfirmOpen: (open: boolean) => void\n /** Is a discard restore round-tripping (gates the dialog buttons)? */\n busy: boolean\n /** Confirm the discard: flush → restore the pinned published version whole. */\n discard: () => Promise<void>\n}\n\n/**\n * EC-316 / EC-325 — the \"Discard changes\" controller. Reverting the draft to the\n * published content IS a whole-document restore of the pinned published version,\n * so it routes through the SAME shared restore sequence\n * ({@link useRestoreVersion}) the Compare view's and Versions pane's restores use\n * — never a parallel write path. That sequence flushes a dirty in-memory buffer\n * FIRST (`documents.restore` snapshots whatever is currently PERSISTED), closing\n * the undo-guarantee gap the header's discard used to have.\n *\n * The enablement is the pure `discardDraftStatus` decision (tested in\n * `discard-draft.test.ts`); its semantic-equality edge is the SAME comparator the\n * Versions pane's publish gate uses, so the two never contradict (EC-298).\n */\nexport function useDiscardController(input: {\n canDiscard: boolean\n published: boolean\n viewReadOnly: boolean\n publishedSnapshotDoc: CmsDocument | undefined\n publishedVersionNumber: number | undefined\n versionKey: QueryKey\n collection: CollectionDef\n draft: CmsDocument\n source: CollectionViewSource\n docRef: DocumentRef\n replaceDraft: (doc: CmsDocument) => void\n dirty: boolean\n save: () => void\n}): DiscardController {\n const {\n canDiscard,\n published,\n viewReadOnly,\n publishedSnapshotDoc,\n publishedVersionNumber,\n versionKey,\n collection,\n draft,\n source,\n docRef,\n replaceDraft,\n dirty,\n save,\n } = input\n\n const [confirmOpen, setConfirmOpen] = useState(false)\n const { restore, busy } = useRestoreVersion({ source, docRef, versionKey, dirty, save, replaceDraft })\n\n const status = discardDraftStatus({\n canDiscard,\n published,\n viewReadOnly,\n publishedSnapshot: publishedSnapshotDoc,\n collection,\n draft,\n })\n const disabledReason =\n status === 'view-read-only'\n ? translate('cms.editor.switchToDraftToDiscard')\n : status === 'up-to-date'\n ? translate('cms.editor.discardDraftUpToDateHint')\n : status === 'loading'\n ? translate('common.loading')\n : undefined\n\n const discard = useCallback(async () => {\n if (publishedVersionNumber === undefined) return\n try {\n // Whole-document restore of the pinned published version (the shared helper\n // flushes any dirty buffer first, mirroring the Versions pane / Compare view).\n await restore(publishedVersionNumber, { kind: 'all' })\n } finally {\n setConfirmOpen(false)\n }\n }, [publishedVersionNumber, restore])\n\n return { status, disabledReason, confirmOpen, setConfirmOpen, busy, discard }\n}\n"]}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { EmbedInspectorApi, EmbedInspectorPayload } from './rich-text/embed-inspector-context';
|
|
2
|
+
/**
|
|
3
|
+
* 0.0.41 — the selection-driven embed-inspector host. A `componentEmbed` node
|
|
4
|
+
* selection in a rich-text field swaps the inspector sidebar for the embed's prop
|
|
5
|
+
* form; deselect returns the normal inspector. `clearActiveEmbed` keys on the node
|
|
6
|
+
* id so a fast A→B reselect can't let A's deselect wipe B (the selection race). Fed
|
|
7
|
+
* by the rich-text field through the EmbedInspector context (the returned api).
|
|
8
|
+
*/
|
|
9
|
+
export declare function useEmbedInspectorHost(): {
|
|
10
|
+
activeEmbed: EmbedInspectorPayload | null;
|
|
11
|
+
embedInspectorApi: EmbedInspectorApi;
|
|
12
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { useMemo, useState } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* 0.0.41 — the selection-driven embed-inspector host. A `componentEmbed` node
|
|
4
|
+
* selection in a rich-text field swaps the inspector sidebar for the embed's prop
|
|
5
|
+
* form; deselect returns the normal inspector. `clearActiveEmbed` keys on the node
|
|
6
|
+
* id so a fast A→B reselect can't let A's deselect wipe B (the selection race). Fed
|
|
7
|
+
* by the rich-text field through the EmbedInspector context (the returned api).
|
|
8
|
+
*/
|
|
9
|
+
export function useEmbedInspectorHost() {
|
|
10
|
+
const [activeEmbed, setActiveEmbed] = useState(null);
|
|
11
|
+
const embedInspectorApi = useMemo(() => ({
|
|
12
|
+
setActiveEmbed: (payload) => setActiveEmbed(payload),
|
|
13
|
+
clearActiveEmbed: (nodeId) => setActiveEmbed((prev) => (prev?.nodeId === nodeId ? null : prev)),
|
|
14
|
+
}), []);
|
|
15
|
+
return { activeEmbed, embedInspectorApi };
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=use-embed-inspector-host.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-embed-inspector-host.js","sourceRoot":"","sources":["../../../src/components/cms/use-embed-inspector-host.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAGzC;;;;;;GAMG;AACH,MAAM,UAAU,qBAAqB;IAInC,MAAM,CAAC,WAAW,EAAE,cAAc,CAAC,GAAG,QAAQ,CAA+B,IAAI,CAAC,CAAA;IAClF,MAAM,iBAAiB,GAAG,OAAO,CAC/B,GAAG,EAAE,CAAC,CAAC;QACL,cAAc,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,cAAc,CAAC,OAAO,CAAC;QACpD,gBAAgB,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,cAAc,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;KAChG,CAAC,EACF,EAAE,CACH,CAAA;IACD,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAAE,CAAA;AAC3C,CAAC","sourcesContent":["import { useMemo, useState } from 'react'\nimport type { EmbedInspectorApi, EmbedInspectorPayload } from './rich-text/embed-inspector-context'\n\n/**\n * 0.0.41 — the selection-driven embed-inspector host. A `componentEmbed` node\n * selection in a rich-text field swaps the inspector sidebar for the embed's prop\n * form; deselect returns the normal inspector. `clearActiveEmbed` keys on the node\n * id so a fast A→B reselect can't let A's deselect wipe B (the selection race). Fed\n * by the rich-text field through the EmbedInspector context (the returned api).\n */\nexport function useEmbedInspectorHost(): {\n activeEmbed: EmbedInspectorPayload | null\n embedInspectorApi: EmbedInspectorApi\n} {\n const [activeEmbed, setActiveEmbed] = useState<EmbedInspectorPayload | null>(null)\n const embedInspectorApi = useMemo<EmbedInspectorApi>(\n () => ({\n setActiveEmbed: (payload) => setActiveEmbed(payload),\n clearActiveEmbed: (nodeId) => setActiveEmbed((prev) => (prev?.nodeId === nodeId ? null : prev)),\n }),\n [],\n )\n return { activeEmbed, embedInspectorApi }\n}\n"]}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { CollectionDef, FieldDef } from '@elytracms/core/cms-core';
|
|
2
|
+
import type { CollectionViewSource } from '@elytracms/studio/lib/collection-view/collection-view-source';
|
|
3
|
+
/**
|
|
4
|
+
* EC-283 / EC-285 — where a jump-to-field lands, as a pure decision. A field in
|
|
5
|
+
* the reserved `sidebar` group renders in the Details inspector pane (open that
|
|
6
|
+
* pane, EC-285); any other field lives on a content tab (switch to it). Returns
|
|
7
|
+
* `null` for an unknown field name (no-op). Directly testable, no DOM.
|
|
8
|
+
*/
|
|
9
|
+
export declare function focusTarget(collection: CollectionDef, fieldName: string): {
|
|
10
|
+
field: FieldDef;
|
|
11
|
+
paneId: 'details';
|
|
12
|
+
} | {
|
|
13
|
+
field: FieldDef;
|
|
14
|
+
tab: string;
|
|
15
|
+
} | null;
|
|
16
|
+
export interface FieldFocus {
|
|
17
|
+
/** The field ring-highlighted right now (or null). */
|
|
18
|
+
highlightedField: string | null;
|
|
19
|
+
/** The inspector sidebar's active pane, controlled so a jump can open Details. */
|
|
20
|
+
inspectorPaneId: string | undefined;
|
|
21
|
+
setInspectorPaneId: (id: string | undefined) => void;
|
|
22
|
+
/** Focus a field by name: switch to its tab/pane (+ locale), scroll, highlight. */
|
|
23
|
+
focusField: (fieldName: string, localeHint?: string) => void;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* EC-172 / EC-283 — the field-focus/highlight controller. Focusing a field
|
|
27
|
+
* switches to its tab (or opens the Details pane for a sidebar field) and the
|
|
28
|
+
* given locale, scrolls its control into view, and ring-highlights it briefly.
|
|
29
|
+
* Shared by the reference deep-link (`focusPath`) and the Issues pane's
|
|
30
|
+
* jump-to-field. Owns the highlight + inspector-pane state and the scroll/clear
|
|
31
|
+
* timers (cleaned up on unmount, and cleared between back-to-back jumps).
|
|
32
|
+
*/
|
|
33
|
+
export declare function useFieldFocus(input: {
|
|
34
|
+
collection: CollectionDef;
|
|
35
|
+
source: CollectionViewSource;
|
|
36
|
+
focusPath: string | undefined;
|
|
37
|
+
setActiveTab: (tab: string) => void;
|
|
38
|
+
}): FieldFocus;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { SIDEBAR_FIELD_GROUP, tabOrder } from '@elytracms/core/cms-core';
|
|
3
|
+
import { setStudioLocale } from '@elytracms/studio/lib/locale';
|
|
4
|
+
import { parseDocumentFieldPath } from '@elytracms/studio/lib/references';
|
|
5
|
+
/**
|
|
6
|
+
* EC-283 / EC-285 — where a jump-to-field lands, as a pure decision. A field in
|
|
7
|
+
* the reserved `sidebar` group renders in the Details inspector pane (open that
|
|
8
|
+
* pane, EC-285); any other field lives on a content tab (switch to it). Returns
|
|
9
|
+
* `null` for an unknown field name (no-op). Directly testable, no DOM.
|
|
10
|
+
*/
|
|
11
|
+
export function focusTarget(collection, fieldName) {
|
|
12
|
+
const field = collection.fields.find((f) => f.name === fieldName);
|
|
13
|
+
if (!field)
|
|
14
|
+
return null;
|
|
15
|
+
if (field.form?.group === SIDEBAR_FIELD_GROUP)
|
|
16
|
+
return { field, paneId: 'details' };
|
|
17
|
+
const fieldTabs = tabOrder(collection.form);
|
|
18
|
+
return { field, tab: field.form?.tab ?? fieldTabs[0] ?? 'main' };
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* EC-172 / EC-283 — the field-focus/highlight controller. Focusing a field
|
|
22
|
+
* switches to its tab (or opens the Details pane for a sidebar field) and the
|
|
23
|
+
* given locale, scrolls its control into view, and ring-highlights it briefly.
|
|
24
|
+
* Shared by the reference deep-link (`focusPath`) and the Issues pane's
|
|
25
|
+
* jump-to-field. Owns the highlight + inspector-pane state and the scroll/clear
|
|
26
|
+
* timers (cleaned up on unmount, and cleared between back-to-back jumps).
|
|
27
|
+
*/
|
|
28
|
+
export function useFieldFocus(input) {
|
|
29
|
+
const { collection, source, focusPath, setActiveTab } = input;
|
|
30
|
+
const [highlightedField, setHighlightedField] = useState(null);
|
|
31
|
+
const [inspectorPaneId, setInspectorPaneId] = useState();
|
|
32
|
+
const focusTimersRef = useRef([]);
|
|
33
|
+
const focusField = useCallback((fieldName, localeHint) => {
|
|
34
|
+
const target = focusTarget(collection, fieldName);
|
|
35
|
+
if (!target)
|
|
36
|
+
return;
|
|
37
|
+
// A sidebar-group field renders in the Details pane; any other on its tab.
|
|
38
|
+
if ('paneId' in target)
|
|
39
|
+
setInspectorPaneId(target.paneId);
|
|
40
|
+
else
|
|
41
|
+
setActiveTab(target.tab);
|
|
42
|
+
if (localeHint !== undefined && source.localeConfig.locales.includes(localeHint)) {
|
|
43
|
+
setStudioLocale(localeHint);
|
|
44
|
+
}
|
|
45
|
+
setHighlightedField(target.field.name);
|
|
46
|
+
// Clear any timers from a prior jump so back-to-back jumps don't fight.
|
|
47
|
+
focusTimersRef.current.forEach((t) => window.clearTimeout(t));
|
|
48
|
+
const scrollTimer = window.setTimeout(() => {
|
|
49
|
+
// Some controls (relation/asset pickers) don't render the input id —
|
|
50
|
+
// the field label (`htmlFor`) is always present.
|
|
51
|
+
const control = document.getElementById(`f-${target.field.name}`) ??
|
|
52
|
+
document.querySelector(`label[for="${CSS.escape(`f-${target.field.name}`)}"]`);
|
|
53
|
+
control?.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
|
54
|
+
}, 60);
|
|
55
|
+
const clearTimer = window.setTimeout(() => setHighlightedField(null), 3200);
|
|
56
|
+
focusTimersRef.current = [scrollTimer, clearTimer];
|
|
57
|
+
}, [collection, source.localeConfig, setActiveTab]);
|
|
58
|
+
useEffect(() => () => focusTimersRef.current.forEach((t) => window.clearTimeout(t)), []);
|
|
59
|
+
// Reference deep links (EC-172): `?field=<index path>` pinpoints the exact field.
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
if (!focusPath)
|
|
62
|
+
return;
|
|
63
|
+
const parsed = parseDocumentFieldPath(focusPath);
|
|
64
|
+
if (!parsed)
|
|
65
|
+
return;
|
|
66
|
+
focusField(parsed.field, parsed.locale);
|
|
67
|
+
}, [focusPath, focusField]);
|
|
68
|
+
return { highlightedField, inspectorPaneId, setInspectorPaneId, focusField };
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=use-field-focus.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-field-focus.js","sourceRoot":"","sources":["../../../src/components/cms/use-field-focus.ts"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react'\nimport { SIDEBAR_FIELD_GROUP, tabOrder } from '@elytracms/core/cms-core'\nimport type { CollectionDef, FieldDef } from '@elytracms/core/cms-core'\nimport type { CollectionViewSource } from '@elytracms/studio/lib/collection-view/collection-view-source'\nimport { setStudioLocale } from '@elytracms/studio/lib/locale'\nimport { parseDocumentFieldPath } from '@elytracms/studio/lib/references'\n\n/**\n * EC-283 / EC-285 — where a jump-to-field lands, as a pure decision. A field in\n * the reserved `sidebar` group renders in the Details inspector pane (open that\n * pane, EC-285); any other field lives on a content tab (switch to it). Returns\n * `null` for an unknown field name (no-op). Directly testable, no DOM.\n */\nexport function focusTarget(\n collection: CollectionDef,\n fieldName: string,\n): { field: FieldDef; paneId: 'details' } | { field: FieldDef; tab: string } | null {\n const field = collection.fields.find((f) => f.name === fieldName)\n if (!field) return null\n if (field.form?.group === SIDEBAR_FIELD_GROUP) return { field, paneId: 'details' }\n const fieldTabs = tabOrder(collection.form)\n return { field, tab: field.form?.tab ?? fieldTabs[0] ?? 'main' }\n}\n\nexport interface FieldFocus {\n /** The field ring-highlighted right now (or null). */\n highlightedField: string | null\n /** The inspector sidebar's active pane, controlled so a jump can open Details. */\n inspectorPaneId: string | undefined\n setInspectorPaneId: (id: string | undefined) => void\n /** Focus a field by name: switch to its tab/pane (+ locale), scroll, highlight. */\n focusField: (fieldName: string, localeHint?: string) => void\n}\n\n/**\n * EC-172 / EC-283 — the field-focus/highlight controller. Focusing a field\n * switches to its tab (or opens the Details pane for a sidebar field) and the\n * given locale, scrolls its control into view, and ring-highlights it briefly.\n * Shared by the reference deep-link (`focusPath`) and the Issues pane's\n * jump-to-field. Owns the highlight + inspector-pane state and the scroll/clear\n * timers (cleaned up on unmount, and cleared between back-to-back jumps).\n */\nexport function useFieldFocus(input: {\n collection: CollectionDef\n source: CollectionViewSource\n focusPath: string | undefined\n setActiveTab: (tab: string) => void\n}): FieldFocus {\n const { collection, source, focusPath, setActiveTab } = input\n const [highlightedField, setHighlightedField] = useState<string | null>(null)\n const [inspectorPaneId, setInspectorPaneId] = useState<string>()\n const focusTimersRef = useRef<number[]>([])\n\n const focusField = useCallback(\n (fieldName: string, localeHint?: string) => {\n const target = focusTarget(collection, fieldName)\n if (!target) return\n // A sidebar-group field renders in the Details pane; any other on its tab.\n if ('paneId' in target) setInspectorPaneId(target.paneId)\n else setActiveTab(target.tab)\n if (localeHint !== undefined && source.localeConfig.locales.includes(localeHint)) {\n setStudioLocale(localeHint)\n }\n setHighlightedField(target.field.name)\n // Clear any timers from a prior jump so back-to-back jumps don't fight.\n focusTimersRef.current.forEach((t) => window.clearTimeout(t))\n const scrollTimer = window.setTimeout(() => {\n // Some controls (relation/asset pickers) don't render the input id —\n // the field label (`htmlFor`) is always present.\n const control =\n document.getElementById(`f-${target.field.name}`) ??\n document.querySelector(`label[for=\"${CSS.escape(`f-${target.field.name}`)}\"]`)\n control?.scrollIntoView({ block: 'center', behavior: 'smooth' })\n }, 60)\n const clearTimer = window.setTimeout(() => setHighlightedField(null), 3200)\n focusTimersRef.current = [scrollTimer, clearTimer]\n },\n [collection, source.localeConfig, setActiveTab],\n )\n useEffect(() => () => focusTimersRef.current.forEach((t) => window.clearTimeout(t)), [])\n\n // Reference deep links (EC-172): `?field=<index path>` pinpoints the exact field.\n useEffect(() => {\n if (!focusPath) return\n const parsed = parseDocumentFieldPath(focusPath)\n if (!parsed) return\n focusField(parsed.field, parsed.locale)\n }, [focusPath, focusField])\n\n return { highlightedField, inspectorPaneId, setInspectorPaneId, focusField }\n}\n"]}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { CmsDocument, CollectionDef, Locale } from '@elytracms/core/cms-core';
|
|
2
|
+
import type { CollectionViewSource } from '@elytracms/studio/lib/collection-view/collection-view-source';
|
|
3
|
+
/** The header's live-link descriptor: an `href` (open live) + a title, or a muted
|
|
4
|
+
* reason-only title while it isn't openable (loading / publish-first / no frontendUrl). */
|
|
5
|
+
export interface HeaderLive {
|
|
6
|
+
href?: string;
|
|
7
|
+
title: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* 0.0.42 (round 3) — the header composed-URL row's open-live affordance, as a pure
|
|
11
|
+
* decision. Only a routable collection shows the row; its leaf opens the live page
|
|
12
|
+
* when the doc is published AND a frontendUrl is set (an absolute `href`),
|
|
13
|
+
* otherwise it is a muted icon whose tooltip explains WHY (still loading, publish
|
|
14
|
+
* first, or set the frontendUrl). The draft-derived path itself updates as you
|
|
15
|
+
* type — only the OPEN-live action is published-gated, preserving draft/live.
|
|
16
|
+
*/
|
|
17
|
+
export declare function resolveHeaderLive(input: {
|
|
18
|
+
routableCollection: boolean;
|
|
19
|
+
liveUrl: {
|
|
20
|
+
href?: string;
|
|
21
|
+
path: string;
|
|
22
|
+
} | undefined;
|
|
23
|
+
loading: boolean;
|
|
24
|
+
published: boolean;
|
|
25
|
+
}): HeaderLive | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* EC-205 / EC-216 — the LIVE URL derives strictly from PUBLISHED data (the pinned
|
|
28
|
+
* published snapshot + its published ancestors), never the draft, so an unpublished
|
|
29
|
+
* slug/parent edit can't leak into the live URL. Resolves the header's live-link
|
|
30
|
+
* descriptor for the composed-URL row; `undefined` for a non-routable collection.
|
|
31
|
+
*/
|
|
32
|
+
export declare function useLiveUrl(input: {
|
|
33
|
+
source: CollectionViewSource;
|
|
34
|
+
collection: CollectionDef;
|
|
35
|
+
doc: CmsDocument;
|
|
36
|
+
locale: Locale;
|
|
37
|
+
published: boolean;
|
|
38
|
+
routableCollection: boolean;
|
|
39
|
+
}): {
|
|
40
|
+
headerLive: HeaderLive | undefined;
|
|
41
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { urlForDocument } from '@elytracms/core/cms-core';
|
|
3
|
+
import { translate } from '@elytracms/studio/lib/i18n';
|
|
4
|
+
import { resolveDeploymentFrontendUrl, resolveDeploymentRoutes } from '@elytracms/studio/lib/workspace';
|
|
5
|
+
import { usePublishedDocuments } from '@elytracms/studio/lib/cms/published-documents';
|
|
6
|
+
/**
|
|
7
|
+
* 0.0.42 (round 3) — the header composed-URL row's open-live affordance, as a pure
|
|
8
|
+
* decision. Only a routable collection shows the row; its leaf opens the live page
|
|
9
|
+
* when the doc is published AND a frontendUrl is set (an absolute `href`),
|
|
10
|
+
* otherwise it is a muted icon whose tooltip explains WHY (still loading, publish
|
|
11
|
+
* first, or set the frontendUrl). The draft-derived path itself updates as you
|
|
12
|
+
* type — only the OPEN-live action is published-gated, preserving draft/live.
|
|
13
|
+
*/
|
|
14
|
+
export function resolveHeaderLive(input) {
|
|
15
|
+
if (!input.routableCollection)
|
|
16
|
+
return undefined;
|
|
17
|
+
if (input.liveUrl?.href) {
|
|
18
|
+
return { href: input.liveUrl.href, title: translate('cms.editor.openLiveUrl', { path: input.liveUrl.path }) };
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
title: input.loading
|
|
22
|
+
? translate('cms.editor.loadingLiveUrl')
|
|
23
|
+
: !input.published
|
|
24
|
+
? translate('cms.editor.publishToViewLive')
|
|
25
|
+
: translate('cms.editor.setFrontendUrlHint'),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* EC-205 / EC-216 — the LIVE URL derives strictly from PUBLISHED data (the pinned
|
|
30
|
+
* published snapshot + its published ancestors), never the draft, so an unpublished
|
|
31
|
+
* slug/parent edit can't leak into the live URL. Resolves the header's live-link
|
|
32
|
+
* descriptor for the composed-URL row; `undefined` for a non-routable collection.
|
|
33
|
+
*/
|
|
34
|
+
export function useLiveUrl(input) {
|
|
35
|
+
const { source, collection, doc, locale, published, routableCollection } = input;
|
|
36
|
+
const publishedDocs = usePublishedDocuments(source.projectId, collection.id);
|
|
37
|
+
const liveUrl = useMemo(() => {
|
|
38
|
+
const publishedDoc = publishedDocs.byId.get(doc.id);
|
|
39
|
+
if (!publishedDoc)
|
|
40
|
+
return undefined;
|
|
41
|
+
return urlForDocument({
|
|
42
|
+
routes: resolveDeploymentRoutes(),
|
|
43
|
+
frontendUrl: resolveDeploymentFrontendUrl(),
|
|
44
|
+
localeConfig: source.localeConfig,
|
|
45
|
+
}, collection, publishedDoc, { siblings: [...publishedDocs.byId.values()], locale });
|
|
46
|
+
}, [publishedDocs, collection, doc.id, locale, source.localeConfig]);
|
|
47
|
+
const headerLive = resolveHeaderLive({
|
|
48
|
+
routableCollection,
|
|
49
|
+
liveUrl,
|
|
50
|
+
loading: publishedDocs.status === 'loading',
|
|
51
|
+
published,
|
|
52
|
+
});
|
|
53
|
+
return { headerLive };
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=use-live-url.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-live-url.js","sourceRoot":"","sources":["../../../src/components/cms/use-live-url.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,OAAO,CAAA;AAC/B,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAA;AAGzD,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AACtD,OAAO,EAAE,4BAA4B,EAAE,uBAAuB,EAAE,MAAM,iCAAiC,CAAA;AACvG,OAAO,EAAE,qBAAqB,EAAE,MAAM,+CAA+C,CAAA;AASrF;;;;;;;GAOG;AACH,MAAM,UAAU,iBAAiB,CAAC,KAKjC;IACC,IAAI,CAAC,KAAK,CAAC,kBAAkB;QAAE,OAAO,SAAS,CAAA;IAC/C,IAAI,KAAK,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC;QACxB,OAAO,EAAE,IAAI,EAAE,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,KAAK,EAAE,SAAS,CAAC,wBAAwB,EAAE,EAAE,IAAI,EAAE,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,EAAE,CAAA;IAC/G,CAAC;IACD,OAAO;QACL,KAAK,EAAE,KAAK,CAAC,OAAO;YAClB,CAAC,CAAC,SAAS,CAAC,2BAA2B,CAAC;YACxC,CAAC,CAAC,CAAC,KAAK,CAAC,SAAS;gBAChB,CAAC,CAAC,SAAS,CAAC,8BAA8B,CAAC;gBAC3C,CAAC,CAAC,SAAS,CAAC,+BAA+B,CAAC;KACjD,CAAA;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,UAAU,CAAC,KAO1B;IACC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,kBAAkB,EAAE,GAAG,KAAK,CAAA;IAChF,MAAM,aAAa,GAAG,qBAAqB,CAAC,MAAM,CAAC,SAAS,EAAE,UAAU,CAAC,EAAE,CAAC,CAAA;IAC5E,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,EAAE;QAC3B,MAAM,YAAY,GAAG,aAAa,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;QACnD,IAAI,CAAC,YAAY;YAAE,OAAO,SAAS,CAAA;QACnC,OAAO,cAAc,CACnB;YACE,MAAM,EAAE,uBAAuB,EAAE;YACjC,WAAW,EAAE,4BAA4B,EAAE;YAC3C,YAAY,EAAE,MAAM,CAAC,YAAY;SAClC,EACD,UAAU,EACV,YAAY,EACZ,EAAE,QAAQ,EAAE,CAAC,GAAG,aAAa,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CACvD,CAAA;IACH,CAAC,EAAE,CAAC,aAAa,EAAE,UAAU,EAAE,GAAG,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,YAAY,CAAC,CAAC,CAAA;IAEpE,MAAM,UAAU,GAAG,iBAAiB,CAAC;QACnC,kBAAkB;QAClB,OAAO;QACP,OAAO,EAAE,aAAa,CAAC,MAAM,KAAK,SAAS;QAC3C,SAAS;KACV,CAAC,CAAA;IACF,OAAO,EAAE,UAAU,EAAE,CAAA;AACvB,CAAC","sourcesContent":["import { useMemo } from 'react'\nimport { urlForDocument } from '@elytracms/core/cms-core'\nimport type { CmsDocument, CollectionDef, Locale } from '@elytracms/core/cms-core'\nimport type { CollectionViewSource } from '@elytracms/studio/lib/collection-view/collection-view-source'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { resolveDeploymentFrontendUrl, resolveDeploymentRoutes } from '@elytracms/studio/lib/workspace'\nimport { usePublishedDocuments } from '@elytracms/studio/lib/cms/published-documents'\n\n/** The header's live-link descriptor: an `href` (open live) + a title, or a muted\n * reason-only title while it isn't openable (loading / publish-first / no frontendUrl). */\nexport interface HeaderLive {\n href?: string\n title: string\n}\n\n/**\n * 0.0.42 (round 3) — the header composed-URL row's open-live affordance, as a pure\n * decision. Only a routable collection shows the row; its leaf opens the live page\n * when the doc is published AND a frontendUrl is set (an absolute `href`),\n * otherwise it is a muted icon whose tooltip explains WHY (still loading, publish\n * first, or set the frontendUrl). The draft-derived path itself updates as you\n * type — only the OPEN-live action is published-gated, preserving draft/live.\n */\nexport function resolveHeaderLive(input: {\n routableCollection: boolean\n liveUrl: { href?: string; path: string } | undefined\n loading: boolean\n published: boolean\n}): HeaderLive | undefined {\n if (!input.routableCollection) return undefined\n if (input.liveUrl?.href) {\n return { href: input.liveUrl.href, title: translate('cms.editor.openLiveUrl', { path: input.liveUrl.path }) }\n }\n return {\n title: input.loading\n ? translate('cms.editor.loadingLiveUrl')\n : !input.published\n ? translate('cms.editor.publishToViewLive')\n : translate('cms.editor.setFrontendUrlHint'),\n }\n}\n\n/**\n * EC-205 / EC-216 — the LIVE URL derives strictly from PUBLISHED data (the pinned\n * published snapshot + its published ancestors), never the draft, so an unpublished\n * slug/parent edit can't leak into the live URL. Resolves the header's live-link\n * descriptor for the composed-URL row; `undefined` for a non-routable collection.\n */\nexport function useLiveUrl(input: {\n source: CollectionViewSource\n collection: CollectionDef\n doc: CmsDocument\n locale: Locale\n published: boolean\n routableCollection: boolean\n}): { headerLive: HeaderLive | undefined } {\n const { source, collection, doc, locale, published, routableCollection } = input\n const publishedDocs = usePublishedDocuments(source.projectId, collection.id)\n const liveUrl = useMemo(() => {\n const publishedDoc = publishedDocs.byId.get(doc.id)\n if (!publishedDoc) return undefined\n return urlForDocument(\n {\n routes: resolveDeploymentRoutes(),\n frontendUrl: resolveDeploymentFrontendUrl(),\n localeConfig: source.localeConfig,\n },\n collection,\n publishedDoc,\n { siblings: [...publishedDocs.byId.values()], locale },\n )\n }, [publishedDocs, collection, doc.id, locale, source.localeConfig])\n\n const headerLive = resolveHeaderLive({\n routableCollection,\n liveUrl,\n loading: publishedDocs.status === 'loading',\n published,\n })\n return { headerLive }\n}\n"]}
|