@elytracms/studio 0.0.69 → 0.0.71
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/dist/components/account/self-account-view-source.js +1 -0
- package/dist/components/account/self-account-view-source.js.map +1 -1
- package/dist/components/app-views/studio-config.d.ts +22 -0
- package/dist/components/app-views/studio-config.js.map +1 -1
- package/dist/components/assets/assets-collection-view-source.js +1 -0
- package/dist/components/assets/assets-collection-view-source.js.map +1 -1
- package/dist/components/builder/blocks-model.d.ts +91 -29
- package/dist/components/builder/blocks-model.js +83 -54
- package/dist/components/builder/blocks-model.js.map +1 -1
- package/dist/components/builder/blocks-panel.d.ts +40 -12
- package/dist/components/builder/blocks-panel.js +88 -34
- package/dist/components/builder/blocks-panel.js.map +1 -1
- package/dist/components/builder/fixture-thumbnail.d.ts +13 -23
- package/dist/components/builder/fixture-thumbnail.js +27 -99
- package/dist/components/builder/fixture-thumbnail.js.map +1 -1
- package/dist/components/builder/spacing-handles.d.ts +14 -11
- package/dist/components/builder/spacing-handles.js +33 -57
- package/dist/components/builder/spacing-handles.js.map +1 -1
- package/dist/components/cms/collection-table.js +19 -2
- package/dist/components/cms/collection-table.js.map +1 -1
- package/dist/components/cms/detail-editor.js +73 -51
- package/dist/components/cms/detail-editor.js.map +1 -1
- package/dist/components/cms/duplicate-document.d.ts +45 -0
- package/dist/components/cms/duplicate-document.js +141 -0
- package/dist/components/cms/duplicate-document.js.map +1 -0
- package/dist/components/cms/field-anchors.d.ts +67 -0
- package/dist/components/cms/field-anchors.js +91 -0
- package/dist/components/cms/field-anchors.js.map +1 -0
- package/dist/components/cms/issue-copy.d.ts +45 -0
- package/dist/components/cms/issue-copy.js +89 -0
- package/dist/components/cms/issue-copy.js.map +1 -0
- package/dist/components/cms/issues-pane.d.ts +11 -2
- package/dist/components/cms/issues-pane.js +22 -4
- package/dist/components/cms/issues-pane.js.map +1 -1
- package/dist/components/cms/object-array-field.d.ts +2 -0
- package/dist/components/cms/object-array-field.js +75 -21
- package/dist/components/cms/object-array-field.js.map +1 -1
- package/dist/components/cms/row-actions.d.ts +10 -3
- package/dist/components/cms/row-actions.js +8 -0
- package/dist/components/cms/row-actions.js.map +1 -1
- package/dist/components/cms/use-field-focus.d.ts +25 -9
- package/dist/components/cms/use-field-focus.js +80 -15
- package/dist/components/cms/use-field-focus.js.map +1 -1
- package/dist/components/collection-view/cms-collection-view-source.js +1 -0
- package/dist/components/collection-view/cms-collection-view-source.js.map +1 -1
- package/dist/components/redirects/redirects-collection-view-source.js +1 -0
- package/dist/components/redirects/redirects-collection-view-source.js.map +1 -1
- package/dist/components/session-expired-overlay.d.ts +16 -0
- package/dist/components/session-expired-overlay.js +22 -6
- package/dist/components/session-expired-overlay.js.map +1 -1
- package/dist/components/sign-in-view.d.ts +10 -1
- package/dist/components/sign-in-view.js +14 -3
- package/dist/components/sign-in-view.js.map +1 -1
- package/dist/components/users/users-collection-view-source.js +1 -0
- package/dist/components/users/users-collection-view-source.js.map +1 -1
- package/dist/customization.d.ts +18 -1
- package/dist/customization.js +8 -0
- package/dist/customization.js.map +1 -1
- package/dist/lib/cms-views/view-model.d.ts +6 -6
- package/dist/lib/collection-view/collection-view-source.d.ts +8 -0
- package/dist/lib/collection-view/collection-view-source.js.map +1 -1
- package/dist/lib/i18n/messages.de.js +44 -1
- package/dist/lib/i18n/messages.de.js.map +1 -1
- package/dist/lib/i18n/messages.en.d.ts +39 -0
- package/dist/lib/i18n/messages.en.js +47 -1
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/persona/labels.d.ts +5 -1
- package/dist/lib/persona/labels.js +43 -30
- package/dist/lib/persona/labels.js.map +1 -1
- package/dist/lib/workspace/index.d.ts +10 -0
- package/dist/lib/workspace/index.js +11 -0
- package/dist/lib/workspace/index.js.map +1 -1
- package/dist/previews/enumerate.d.ts +68 -0
- package/dist/previews/enumerate.js +78 -0
- package/dist/previews/enumerate.js.map +1 -0
- package/dist/previews/harness.d.ts +50 -0
- package/dist/previews/harness.js +123 -0
- package/dist/previews/harness.js.map +1 -0
- package/dist/previews/manifest-format.d.ts +57 -0
- package/dist/previews/manifest-format.js +59 -0
- package/dist/previews/manifest-format.js.map +1 -0
- package/dist/ui/dialog.d.ts +9 -1
- package/dist/ui/dialog.js +8 -2
- package/dist/ui/dialog.js.map +1 -1
- package/dist/ui/field.d.ts +24 -1
- package/dist/ui/field.js +23 -2
- package/dist/ui/field.js.map +1 -1
- package/package.json +10 -6
|
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useRef, useState } from 'react';
|
|
|
2
2
|
import { SIDEBAR_FIELD_GROUP, tabOrder } from '@elytracms/core/cms-core';
|
|
3
3
|
import { setStudioLocale } from '@elytracms/studio/lib/locale';
|
|
4
4
|
import { parseDocumentFieldPath } from '@elytracms/studio/lib/references';
|
|
5
|
+
import { anchorCandidates, deepestExistingAnchor, fieldAnchorSelector, } from './field-anchors.js';
|
|
5
6
|
/**
|
|
6
7
|
* EC-283 / EC-285 — where a jump-to-field lands, as a pure decision. A field in
|
|
7
8
|
* the reserved `sidebar` group renders in the Details inspector pane (open that
|
|
@@ -18,19 +19,37 @@ export function focusTarget(collection, fieldName) {
|
|
|
18
19
|
return { field, tab: field.form?.tab ?? fieldTabs[0] ?? 'main' };
|
|
19
20
|
}
|
|
20
21
|
/**
|
|
21
|
-
* EC-
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
* EC-443 — nested-anchor resolution retries. Expanding a collapsed array item is
|
|
23
|
+
* React state (context-driven), so each nested array level needs a render cycle
|
|
24
|
+
* before its deeper anchors exist; a short retry ladder lets a multi-level
|
|
25
|
+
* expansion cascade settle before the plan degrades to a shallower anchor.
|
|
26
|
+
*/
|
|
27
|
+
const RESOLVE_ATTEMPT_DELAYS_MS = [60, 200, 450];
|
|
28
|
+
/** How long the ring highlight (top-level or nested) stays on. */
|
|
29
|
+
const HIGHLIGHT_CLEAR_MS = 3200;
|
|
30
|
+
/**
|
|
31
|
+
* EC-172 / EC-283 / EC-443 — the field-focus/highlight controller. Focusing a
|
|
32
|
+
* field switches to its tab (or opens the Details pane for a sidebar field) and
|
|
33
|
+
* the given locale, then lands on the DEEPEST available anchor along the path
|
|
34
|
+
* (EC-443): nested paths publish a {@link FieldFocusRequest} (auto-expanding
|
|
35
|
+
* collapsed array items via context), retry-resolve the deepest stamped anchor,
|
|
36
|
+
* scroll it into view, and ring-highlight it — degrading prefix by prefix to the
|
|
37
|
+
* top-level field control, never a silent no-op. Shared by the reference
|
|
38
|
+
* deep-link (`focusPath`, `?field=`) and the Issues pane's jump-to-field. Owns
|
|
39
|
+
* the highlight + inspector-pane state and every scroll/clear timer (cleaned up
|
|
40
|
+
* on unmount, and cleared between back-to-back jumps).
|
|
27
41
|
*/
|
|
28
42
|
export function useFieldFocus(input) {
|
|
29
43
|
const { collection, source, focusPath, setActiveTab } = input;
|
|
30
44
|
const [highlightedField, setHighlightedField] = useState(null);
|
|
45
|
+
const [focusRequest, setFocusRequest] = useState(null);
|
|
31
46
|
const [inspectorPaneId, setInspectorPaneId] = useState();
|
|
32
47
|
const focusTimersRef = useRef([]);
|
|
33
|
-
const
|
|
48
|
+
const nonceRef = useRef(0);
|
|
49
|
+
const focusField = useCallback((fieldPath, localeHint) => {
|
|
50
|
+
const tokens = fieldPath.split('/');
|
|
51
|
+
const fieldName = tokens[0] ?? fieldPath;
|
|
52
|
+
const rest = tokens.slice(1);
|
|
34
53
|
const target = focusTarget(collection, fieldName);
|
|
35
54
|
if (!target)
|
|
36
55
|
return;
|
|
@@ -42,29 +61,75 @@ export function useFieldFocus(input) {
|
|
|
42
61
|
if (localeHint !== undefined && source.localeConfig.locales.includes(localeHint)) {
|
|
43
62
|
setStudioLocale(localeHint);
|
|
44
63
|
}
|
|
45
|
-
setHighlightedField(target.field.name);
|
|
46
64
|
// Clear any timers from a prior jump so back-to-back jumps don't fight.
|
|
47
65
|
focusTimersRef.current.forEach((t) => window.clearTimeout(t));
|
|
48
|
-
const
|
|
66
|
+
const scrollTopLevelControl = () => {
|
|
49
67
|
// Some controls (relation/asset pickers) don't render the input id —
|
|
50
68
|
// the field label (`htmlFor`) is always present.
|
|
51
69
|
const control = document.getElementById(`f-${target.field.name}`) ??
|
|
52
70
|
document.querySelector(`label[for="${CSS.escape(`f-${target.field.name}`)}"]`);
|
|
53
71
|
control?.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
|
|
72
|
+
};
|
|
73
|
+
if (rest.length === 0) {
|
|
74
|
+
// Top-level jump: exactly the EC-283 behavior — ring the field row, scroll.
|
|
75
|
+
setFocusRequest(null);
|
|
76
|
+
setHighlightedField(target.field.name);
|
|
77
|
+
const scrollTimer = window.setTimeout(scrollTopLevelControl, 60);
|
|
78
|
+
const clearTimer = window.setTimeout(() => setHighlightedField(null), HIGHLIGHT_CLEAR_MS);
|
|
79
|
+
focusTimersRef.current = [scrollTimer, clearTimer];
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
// EC-443 nested jump. Publishing the request expands collapsed array items
|
|
83
|
+
// along the path (context-driven, `object-array-field`); each retry then
|
|
84
|
+
// probes for the deepest stamped anchor. Settling: full path found → ring +
|
|
85
|
+
// scroll it (early); last attempt → deepest existing prefix (ring follows
|
|
86
|
+
// the REAL element, not the wished-for one); nothing stamped → the
|
|
87
|
+
// top-level field row is the floor.
|
|
88
|
+
const nonce = ++nonceRef.current;
|
|
89
|
+
setHighlightedField(null);
|
|
90
|
+
setFocusRequest({ path: fieldPath, nonce });
|
|
91
|
+
const candidates = anchorCandidates(fieldName, rest);
|
|
92
|
+
const settle = (resolved) => {
|
|
93
|
+
if (resolved !== null && resolved !== fieldName) {
|
|
94
|
+
setFocusRequest({ path: resolved, nonce });
|
|
95
|
+
document
|
|
96
|
+
.querySelector(fieldAnchorSelector(resolved))
|
|
97
|
+
?.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
setFocusRequest(null);
|
|
101
|
+
setHighlightedField(target.field.name);
|
|
102
|
+
scrollTopLevelControl();
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
const settled = { done: false };
|
|
106
|
+
const attemptTimers = RESOLVE_ATTEMPT_DELAYS_MS.map((delay, i) => window.setTimeout(() => {
|
|
107
|
+
if (settled.done)
|
|
108
|
+
return;
|
|
109
|
+
const resolved = deepestExistingAnchor(candidates, (path) => document.querySelector(fieldAnchorSelector(path)) !== null);
|
|
110
|
+
const isLastAttempt = i === RESOLVE_ATTEMPT_DELAYS_MS.length - 1;
|
|
111
|
+
if (resolved === fieldPath || isLastAttempt) {
|
|
112
|
+
settled.done = true;
|
|
113
|
+
settle(resolved);
|
|
114
|
+
}
|
|
115
|
+
}, delay));
|
|
116
|
+
const clearTimer = window.setTimeout(() => {
|
|
117
|
+
setHighlightedField(null);
|
|
118
|
+
setFocusRequest(null);
|
|
119
|
+
}, HIGHLIGHT_CLEAR_MS);
|
|
120
|
+
focusTimersRef.current = [...attemptTimers, clearTimer];
|
|
57
121
|
}, [collection, source.localeConfig, setActiveTab]);
|
|
58
122
|
useEffect(() => () => focusTimersRef.current.forEach((t) => window.clearTimeout(t)), []);
|
|
59
|
-
// Reference deep links (EC-172): `?field=<index path>` pinpoints the exact
|
|
123
|
+
// Reference deep links (EC-172): `?field=<index path>` pinpoints the exact
|
|
124
|
+
// field — nested tokens ride along (EC-443), so the SAME path-aware focus runs.
|
|
60
125
|
useEffect(() => {
|
|
61
126
|
if (!focusPath)
|
|
62
127
|
return;
|
|
63
128
|
const parsed = parseDocumentFieldPath(focusPath);
|
|
64
129
|
if (!parsed)
|
|
65
130
|
return;
|
|
66
|
-
focusField(parsed.field, parsed.locale);
|
|
131
|
+
focusField([parsed.field, ...parsed.rest].join('/'), parsed.locale);
|
|
67
132
|
}, [focusPath, focusField]);
|
|
68
|
-
return { highlightedField, inspectorPaneId, setInspectorPaneId, focusField };
|
|
133
|
+
return { highlightedField, focusRequest, inspectorPaneId, setInspectorPaneId, focusField };
|
|
69
134
|
}
|
|
70
135
|
//# sourceMappingURL=use-field-focus.js.map
|
|
@@ -1 +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"]}
|
|
1
|
+
{"version":3,"file":"use-field-focus.js","sourceRoot":"","sources":["../../../src/components/cms/use-field-focus.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAChE,OAAO,EAAE,mBAAmB,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAA;AAGxE,OAAO,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAA;AAC9D,OAAO,EAAE,sBAAsB,EAAE,MAAM,kCAAkC,CAAA;AACzE,OAAO,EACL,gBAAgB,EAChB,qBAAqB,EACrB,mBAAmB,GACpB,MAAM,iBAAiB,CAAA;AAGxB;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CACzB,UAAyB,EACzB,SAAiB;IAEjB,MAAM,KAAK,GAAG,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,SAAS,CAAC,CAAA;IACjE,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAA;IACvB,IAAI,KAAK,CAAC,IAAI,EAAE,KAAK,KAAK,mBAAmB;QAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,CAAA;IAClF,MAAM,SAAS,GAAG,QAAQ,CAAC,UAAU,CAAC,IAAI,CAAC,CAAA;IAC3C,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,KAAK,CAAC,IAAI,EAAE,GAAG,IAAI,SAAS,CAAC,CAAC,CAAC,IAAI,MAAM,EAAE,CAAA;AAClE,CAAC;AAED;;;;;GAKG;AACH,MAAM,yBAAyB,GAAG,CAAC,EAAE,EAAE,GAAG,EAAE,GAAG,CAAU,CAAA;AACzD,kEAAkE;AAClE,MAAM,kBAAkB,GAAG,IAAI,CAAA;AAuB/B;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,aAAa,CAAC,KAK7B;IACC,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,YAAY,EAAE,GAAG,KAAK,CAAA;IAC7D,MAAM,CAAC,gBAAgB,EAAE,mBAAmB,CAAC,GAAG,QAAQ,CAAgB,IAAI,CAAC,CAAA;IAC7E,MAAM,CAAC,YAAY,EAAE,eAAe,CAAC,GAAG,QAAQ,CAA2B,IAAI,CAAC,CAAA;IAChF,MAAM,CAAC,eAAe,EAAE,kBAAkB,CAAC,GAAG,QAAQ,EAAU,CAAA;IAChE,MAAM,cAAc,GAAG,MAAM,CAAW,EAAE,CAAC,CAAA;IAC3C,MAAM,QAAQ,GAAG,MAAM,CAAC,CAAC,CAAC,CAAA;IAE1B,MAAM,UAAU,GAAG,WAAW,CAC5B,CAAC,SAAiB,EAAE,UAAmB,EAAE,EAAE;QACzC,MAAM,MAAM,GAAG,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;QACnC,MAAM,SAAS,GAAG,MAAM,CAAC,CAAC,CAAC,IAAI,SAAS,CAAA;QACxC,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;QAC5B,MAAM,MAAM,GAAG,WAAW,CAAC,UAAU,EAAE,SAAS,CAAC,CAAA;QACjD,IAAI,CAAC,MAAM;YAAE,OAAM;QACnB,2EAA2E;QAC3E,IAAI,QAAQ,IAAI,MAAM;YAAE,kBAAkB,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;;YACpD,YAAY,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;QAC7B,IAAI,UAAU,KAAK,SAAS,IAAI,MAAM,CAAC,YAAY,CAAC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC;YACjF,eAAe,CAAC,UAAU,CAAC,CAAA;QAC7B,CAAC;QACD,wEAAwE;QACxE,cAAc,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAA;QAE7D,MAAM,qBAAqB,GAAG,GAAG,EAAE;YACjC,qEAAqE;YACrE,iDAAiD;YACjD,MAAM,OAAO,GACX,QAAQ,CAAC,cAAc,CAAC,KAAK,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;gBACjD,QAAQ,CAAC,aAAa,CAAC,cAAc,GAAG,CAAC,MAAM,CAAC,KAAK,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,CAAA;YAChF,OAAO,EAAE,cAAc,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,CAAA;QAClE,CAAC,CAAA;QAED,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACtB,4EAA4E;YAC5E,eAAe,CAAC,IAAI,CAAC,CAAA;YACrB,mBAAmB,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;YACtC,MAAM,WAAW,GAAG,MAAM,CAAC,UAAU,CAAC,qBAAqB,EAAE,EAAE,CAAC,CAAA;YAChE,MAAM,UAAU,GAAG,MAAM,CAAC,UAAU,CAAC,GAAG,EAAE,CAAC,mBAAmB,CAAC,IAAI,CAAC,EAAE,kBAAkB,CAAC,CAAA;YACzF,cAAc,CAAC,OAAO,GAAG,CAAC,WAAW,EAAE,UAAU,CAAC,CAAA;YAClD,OAAM;QACR,CAAC;QAED,2EAA2E;QAC3E,yEAAyE;QACzE,4EAA4E;QAC5E,0EAA0E;QAC1E,mEAAmE;QACnE,oCAAoC;QACpC,MAAM,KAAK,GAAG,EAAE,QAAQ,CAAC,OAAO,CAAA;QAChC,mBAAmB,CAAC,IAAI,CAAC,CAAA;QACzB,eAAe,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC,CAAA;QAC3C,MAAM,UAAU,GAAG,gBAAgB,CAAC,SAAS,EAAE,IAAI,CAAC,CAAA;QACpD,MAAM,MAAM,GAAG,CAAC,QAAuB,EAAE,EAAE;YACzC,IAAI,QAAQ,KAAK,IAAI,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;gBAChD,eAAe,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAA;gBAC1C,QAAQ;qBACL,aAAa,CAAC,mBAAmB,CAAC,QAAQ,CAAC,CAAC;oBAC7C,EAAE,cAAc,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,CAAA;YAC7D,CAAC;iBAAM,CAAC;gBACN,eAAe,CAAC,IAAI,CAAC,CAAA;gBACrB,mBAAmB,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;gBACtC,qBAAqB,EAAE,CAAA;YACzB,CAAC;QACH,CAAC,CAAA;QACD,MAAM,OAAO,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,CAAA;QAC/B,MAAM,aAAa,GAAG,yBAAyB,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE,CAC/D,MAAM,CAAC,UAAU,CAAC,GAAG,EAAE;YACrB,IAAI,OAAO,CAAC,IAAI;gBAAE,OAAM;YACxB,MAAM,QAAQ,GAAG,qBAAqB,CACpC,UAAU,EACV,CAAC,IAAI,EAAE,EAAE,CAAC,QAAQ,CAAC,aAAa,CAAC,mBAAmB,CAAC,IAAI,CAAC,CAAC,KAAK,IAAI,CACrE,CAAA;YACD,MAAM,aAAa,GAAG,CAAC,KAAK,yBAAyB,CAAC,MAAM,GAAG,CAAC,CAAA;YAChE,IAAI,QAAQ,KAAK,SAAS,IAAI,aAAa,EAAE,CAAC;gBAC5C,OAAO,CAAC,IAAI,GAAG,IAAI,CAAA;gBACnB,MAAM,CAAC,QAAQ,CAAC,CAAA;YAClB,CAAC;QACH,CAAC,EAAE,KAAK,CAAC,CACV,CAAA;QACD,MAAM,UAAU,GAAG,MAAM,CAAC,UAAU,CAAC,GAAG,EAAE;YACxC,mBAAmB,CAAC,IAAI,CAAC,CAAA;YACzB,eAAe,CAAC,IAAI,CAAC,CAAA;QACvB,CAAC,EAAE,kBAAkB,CAAC,CAAA;QACtB,cAAc,CAAC,OAAO,GAAG,CAAC,GAAG,aAAa,EAAE,UAAU,CAAC,CAAA;IACzD,CAAC,EACD,CAAC,UAAU,EAAE,MAAM,CAAC,YAAY,EAAE,YAAY,CAAC,CAChD,CAAA;IACD,SAAS,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,CAAC,cAAc,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAA;IAExF,2EAA2E;IAC3E,gFAAgF;IAChF,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,CAAC,SAAS;YAAE,OAAM;QACtB,MAAM,MAAM,GAAG,sBAAsB,CAAC,SAAS,CAAC,CAAA;QAChD,IAAI,CAAC,MAAM;YAAE,OAAM;QACnB,UAAU,CAAC,CAAC,MAAM,CAAC,KAAK,EAAE,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,CAAA;IACrE,CAAC,EAAE,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC,CAAA;IAE3B,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAE,eAAe,EAAE,kBAAkB,EAAE,UAAU,EAAE,CAAA;AAC5F,CAAC","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'\nimport {\n anchorCandidates,\n deepestExistingAnchor,\n fieldAnchorSelector,\n} from './field-anchors'\nimport type { FieldFocusRequest } from './field-anchors'\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\n/**\n * EC-443 — nested-anchor resolution retries. Expanding a collapsed array item is\n * React state (context-driven), so each nested array level needs a render cycle\n * before its deeper anchors exist; a short retry ladder lets a multi-level\n * expansion cascade settle before the plan degrades to a shallower anchor.\n */\nconst RESOLVE_ATTEMPT_DELAYS_MS = [60, 200, 450] as const\n/** How long the ring highlight (top-level or nested) stays on. */\nconst HIGHLIGHT_CLEAR_MS = 3200\n\nexport interface FieldFocus {\n /** The top-level field ring-highlighted right now (or null). */\n highlightedField: string | null\n /**\n * The active nested focus request (EC-443): drives collapsed-array expansion\n * and the ring on the exact nested control, via `FieldFocusRequestContext`.\n */\n focusRequest: FieldFocusRequest | 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 /**\n * Focus a field by anchor path (`links` or `links/3/badge/text` — the EC-443\n * convention on `FIELD_PATH_ATTR`): switch to its tab/pane (+ locale), expand\n * collapsed array items en route, scroll to the deepest available anchor, and\n * ring-highlight it. Deepest existing anchor wins; the top-level field is the\n * degradation floor.\n */\n focusField: (fieldPath: string, localeHint?: string) => void\n}\n\n/**\n * EC-172 / EC-283 / EC-443 — the field-focus/highlight controller. Focusing a\n * field switches to its tab (or opens the Details pane for a sidebar field) and\n * the given locale, then lands on the DEEPEST available anchor along the path\n * (EC-443): nested paths publish a {@link FieldFocusRequest} (auto-expanding\n * collapsed array items via context), retry-resolve the deepest stamped anchor,\n * scroll it into view, and ring-highlight it — degrading prefix by prefix to the\n * top-level field control, never a silent no-op. Shared by the reference\n * deep-link (`focusPath`, `?field=`) and the Issues pane's jump-to-field. Owns\n * the highlight + inspector-pane state and every scroll/clear timer (cleaned up\n * 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 [focusRequest, setFocusRequest] = useState<FieldFocusRequest | null>(null)\n const [inspectorPaneId, setInspectorPaneId] = useState<string>()\n const focusTimersRef = useRef<number[]>([])\n const nonceRef = useRef(0)\n\n const focusField = useCallback(\n (fieldPath: string, localeHint?: string) => {\n const tokens = fieldPath.split('/')\n const fieldName = tokens[0] ?? fieldPath\n const rest = tokens.slice(1)\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 // Clear any timers from a prior jump so back-to-back jumps don't fight.\n focusTimersRef.current.forEach((t) => window.clearTimeout(t))\n\n const scrollTopLevelControl = () => {\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 }\n\n if (rest.length === 0) {\n // Top-level jump: exactly the EC-283 behavior — ring the field row, scroll.\n setFocusRequest(null)\n setHighlightedField(target.field.name)\n const scrollTimer = window.setTimeout(scrollTopLevelControl, 60)\n const clearTimer = window.setTimeout(() => setHighlightedField(null), HIGHLIGHT_CLEAR_MS)\n focusTimersRef.current = [scrollTimer, clearTimer]\n return\n }\n\n // EC-443 nested jump. Publishing the request expands collapsed array items\n // along the path (context-driven, `object-array-field`); each retry then\n // probes for the deepest stamped anchor. Settling: full path found → ring +\n // scroll it (early); last attempt → deepest existing prefix (ring follows\n // the REAL element, not the wished-for one); nothing stamped → the\n // top-level field row is the floor.\n const nonce = ++nonceRef.current\n setHighlightedField(null)\n setFocusRequest({ path: fieldPath, nonce })\n const candidates = anchorCandidates(fieldName, rest)\n const settle = (resolved: string | null) => {\n if (resolved !== null && resolved !== fieldName) {\n setFocusRequest({ path: resolved, nonce })\n document\n .querySelector(fieldAnchorSelector(resolved))\n ?.scrollIntoView({ block: 'center', behavior: 'smooth' })\n } else {\n setFocusRequest(null)\n setHighlightedField(target.field.name)\n scrollTopLevelControl()\n }\n }\n const settled = { done: false }\n const attemptTimers = RESOLVE_ATTEMPT_DELAYS_MS.map((delay, i) =>\n window.setTimeout(() => {\n if (settled.done) return\n const resolved = deepestExistingAnchor(\n candidates,\n (path) => document.querySelector(fieldAnchorSelector(path)) !== null,\n )\n const isLastAttempt = i === RESOLVE_ATTEMPT_DELAYS_MS.length - 1\n if (resolved === fieldPath || isLastAttempt) {\n settled.done = true\n settle(resolved)\n }\n }, delay),\n )\n const clearTimer = window.setTimeout(() => {\n setHighlightedField(null)\n setFocusRequest(null)\n }, HIGHLIGHT_CLEAR_MS)\n focusTimersRef.current = [...attemptTimers, 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\n // field — nested tokens ride along (EC-443), so the SAME path-aware focus runs.\n useEffect(() => {\n if (!focusPath) return\n const parsed = parseDocumentFieldPath(focusPath)\n if (!parsed) return\n focusField([parsed.field, ...parsed.rest].join('/'), parsed.locale)\n }, [focusPath, focusField])\n\n return { highlightedField, focusRequest, inspectorPaneId, setInspectorPaneId, focusField }\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cms-collection-view-source.js","sourceRoot":"","sources":["../../../src/components/collection-view/cms-collection-view-source.ts"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useMemo } from 'react'\nimport { isHierarchical } from '@elytracms/core/cms-core'\nimport type { DocumentRef, DocumentUrl } from '@elytracms/core/cms-core'\nimport { studioPath } from '@elytracms/studio/router'\nimport { resolveDeploymentFrontendUrl, resolveDeploymentRoutes } from '@elytracms/studio/lib/workspace'\nimport type {\n CollectionViewCapabilities,\n CollectionViewSource,\n ResolvedAsset,\n} from '@elytracms/studio/lib/collection-view/collection-view-source'\nimport { useCms } from '../cms/cms-store'\nimport { useOptionalAssets } from '../assets/assets-store'\nimport { usePublishedDocuments } from '@elytracms/studio/lib/cms/published-documents'\nimport { resolveDocumentLiveUrl } from '../cms/use-live-url'\n\n/**\n * Assemble a {@link CollectionViewSource} for a `cmsDocuments` collection from the\n * ambient `CmsStore` + its side contexts (assets, published pool) + the\n * deployment config reads (EC-240 / AD-19). This is the CMS adapter: a\n * behaviour-preserving pass-through so the portable list + edit surfaces render\n * over the source instead of `useCms()` directly, byte-for-byte as before.\n *\n * Returns `null` for an unknown collection id (the caller renders the fallback).\n *\n * Referential stability: collection-parameterised methods are `useCallback`-bound\n * and the source object is `useMemo`-ed, so the surfaces' `useMemo` dependency\n * arrays stay stable across renders (the inline-edit order snapshot depends on it).\n */\nexport function useCmsCollectionViewSource(collectionId: string): CollectionViewSource | null {\n const cms = useCms()\n const assetStore = useOptionalAssets()\n const collection = cms.getCollection(collectionId)\n const publishedDocs = usePublishedDocuments(cms.projectId, collectionId)\n\n // The asset collection id is resolved once from the schema (as the cell does).\n const assetCollectionId = useMemo(\n () => cms.collections.find((c) => c.kind === 'asset')?.id ?? 'asset',\n [cms.collections],\n )\n\n // Live-URL column: the published pool × deployment routes, memoised exactly as\n // the table did — derived from PUBLISHED snapshots, never draft rows (EC-205).\n const isRoutable = useMemo(\n () => resolveDeploymentRoutes().some((route) => route.document?.collection === collectionId),\n [collectionId],\n )\n // EC-384: `resolveDocumentLiveUrl` merges the stored routes with the kit's\n // fallback-home rule (same as the document editor's header affordance,\n // `use-live-url.ts`) — otherwise a fallback-addressed home page showed no URL\n // in this column even though the live site serves it.\n const liveUrlMap = useMemo(() => {\n if (!collection || !isRoutable) return null\n const siblings = [...publishedDocs.byId.values()]\n const map = new Map<string, DocumentUrl>()\n for (const published of siblings) {\n const url = resolveDocumentLiveUrl({\n routes: resolveDeploymentRoutes(),\n frontendUrl: resolveDeploymentFrontendUrl(),\n localeConfig: cms.localeConfig,\n collection,\n doc: published,\n siblings,\n locale: cms.activeLocale,\n })\n if (url) map.set(published.id, url)\n }\n return map\n }, [collection, isRoutable, publishedDocs, cms.localeConfig, cms.activeLocale])\n\n const documents = useCallback(() => cms.documentsOf(collectionId), [cms, collectionId])\n const viewState = useCallback(() => cms.tableStateOf(collectionId), [cms, collectionId])\n const setViewState = useCallback(\n (update: Parameters<CollectionViewSource['setViewState']>[0]) =>\n cms.setTableState(collectionId, update),\n [cms, collectionId],\n )\n // Named views (EC-246b): config-as-code lenses the header selector switches between.\n const views = useMemo(\n () => ({\n list: () => cms.viewsOf(collectionId),\n activeId: () => cms.activeViewId(collectionId),\n setActive: (id: string) => cms.setActiveView(collectionId, id),\n }),\n [cms, collectionId],\n )\n const create = useCallback(() => cms.createDocument(collectionId), [cms, collectionId])\n const imports = useMemo(\n () => ({ importDocuments: (items: unknown[]) => cms.importDocuments(collectionId, items) }),\n [cms, collectionId],\n )\n const resolveAsset = useCallback(\n (id: string): ResolvedAsset | undefined => {\n const record = assetStore?.assets.find((asset) => asset.id === id)\n if (!record) return undefined\n return {\n ...(record.url !== undefined ? { url: record.url } : {}),\n contentType: record.contentType,\n ...(record.title !== undefined ? { title: record.title } : {}),\n filename: record.filename,\n ...(record.alt !== undefined ? { alt: record.alt } : {}),\n }\n },\n [assetStore],\n )\n const liveUrls = useCallback(() => liveUrlMap, [liveUrlMap])\n\n return useMemo(() => {\n if (!collection) return null\n const capabilities: CollectionViewCapabilities = {\n publish: true,\n versions: true,\n locales: true,\n inlineEdit: true,\n create: true,\n remove: true,\n bulk: true,\n liveUrl: isRoutable,\n hierarchy: isHierarchical(collection),\n importExport: true,\n }\n const source: CollectionViewSource = {\n collectionId,\n schema: collection,\n capabilities,\n ...(cms.projectId !== undefined ? { projectId: cms.projectId } : {}),\n assetCollectionId,\n localeConfig: cms.localeConfig,\n activeLocale: cms.activeLocale,\n documents,\n get: cms.getDocument,\n titleOf: cms.titleOf,\n // References are collection-based (EC-072): resolve candidates of ANY target\n // collection the schema declares — the workspace doc store already serves them.\n referenceCandidates: cms.documentsOf,\n collections: cms.collections,\n isFieldReadOnly: cms.isFieldReadOnly,\n validate: cms.validate,\n resolveAsset,\n lastError: cms.persistError,\n clearLastError: cms.clearPersistError,\n // Baseline + queue flush, so a pre-flight reference-index read is current.\n flush: async () => {\n await cms.ensurePersistedBaseline()\n await cms.flushPersistence()\n },\n detailPath: (id: string) => studioPath.cmsDocument(collectionId, id),\n listPath: studioPath.cmsCollection(collectionId),\n viewState,\n setViewState,\n views,\n imports,\n create,\n save: cms.saveDocument,\n remove: cms.deleteDocument,\n bulk: (ids, action, setField) => cms.runBulk(collectionId, ids, action, setField),\n publishing: {\n isPublished: cms.isPublished,\n publish: cms.publishDocument,\n unpublish: cms.unpublishDocument,\n liveUrls,\n },\n versioning: {\n restore: (ref: DocumentRef, version: number, scope) =>\n cms.restoreDocumentVersion(ref, version, scope),\n // Flush-before-read ordering folds into the loader (EC-224) so any surface\n // that mutated the history still lists an authoritative version set.\n load: async (ref: DocumentRef) => {\n await cms.flushPersistence()\n return cms.loadDocumentVersions(ref)\n },\n },\n inlineEditing: {\n commitField: (input) => cms.commitFieldEdit(input),\n },\n }\n return source\n }, [\n collection,\n collectionId,\n cms,\n isRoutable,\n documents,\n viewState,\n setViewState,\n views,\n imports,\n create,\n resolveAsset,\n liveUrls,\n assetCollectionId,\n ])\n}\n"]}
|
|
1
|
+
{"version":3,"file":"cms-collection-view-source.js","sourceRoot":"","sources":["../../../src/components/collection-view/cms-collection-view-source.ts"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useMemo } from 'react'\nimport { isHierarchical } from '@elytracms/core/cms-core'\nimport type { DocumentRef, DocumentUrl } from '@elytracms/core/cms-core'\nimport { studioPath } from '@elytracms/studio/router'\nimport { resolveDeploymentFrontendUrl, resolveDeploymentRoutes } from '@elytracms/studio/lib/workspace'\nimport type {\n CollectionViewCapabilities,\n CollectionViewSource,\n ResolvedAsset,\n} from '@elytracms/studio/lib/collection-view/collection-view-source'\nimport { useCms } from '../cms/cms-store'\nimport { useOptionalAssets } from '../assets/assets-store'\nimport { usePublishedDocuments } from '@elytracms/studio/lib/cms/published-documents'\nimport { resolveDocumentLiveUrl } from '../cms/use-live-url'\n\n/**\n * Assemble a {@link CollectionViewSource} for a `cmsDocuments` collection from the\n * ambient `CmsStore` + its side contexts (assets, published pool) + the\n * deployment config reads (EC-240 / AD-19). This is the CMS adapter: a\n * behaviour-preserving pass-through so the portable list + edit surfaces render\n * over the source instead of `useCms()` directly, byte-for-byte as before.\n *\n * Returns `null` for an unknown collection id (the caller renders the fallback).\n *\n * Referential stability: collection-parameterised methods are `useCallback`-bound\n * and the source object is `useMemo`-ed, so the surfaces' `useMemo` dependency\n * arrays stay stable across renders (the inline-edit order snapshot depends on it).\n */\nexport function useCmsCollectionViewSource(collectionId: string): CollectionViewSource | null {\n const cms = useCms()\n const assetStore = useOptionalAssets()\n const collection = cms.getCollection(collectionId)\n const publishedDocs = usePublishedDocuments(cms.projectId, collectionId)\n\n // The asset collection id is resolved once from the schema (as the cell does).\n const assetCollectionId = useMemo(\n () => cms.collections.find((c) => c.kind === 'asset')?.id ?? 'asset',\n [cms.collections],\n )\n\n // Live-URL column: the published pool × deployment routes, memoised exactly as\n // the table did — derived from PUBLISHED snapshots, never draft rows (EC-205).\n const isRoutable = useMemo(\n () => resolveDeploymentRoutes().some((route) => route.document?.collection === collectionId),\n [collectionId],\n )\n // EC-384: `resolveDocumentLiveUrl` merges the stored routes with the kit's\n // fallback-home rule (same as the document editor's header affordance,\n // `use-live-url.ts`) — otherwise a fallback-addressed home page showed no URL\n // in this column even though the live site serves it.\n const liveUrlMap = useMemo(() => {\n if (!collection || !isRoutable) return null\n const siblings = [...publishedDocs.byId.values()]\n const map = new Map<string, DocumentUrl>()\n for (const published of siblings) {\n const url = resolveDocumentLiveUrl({\n routes: resolveDeploymentRoutes(),\n frontendUrl: resolveDeploymentFrontendUrl(),\n localeConfig: cms.localeConfig,\n collection,\n doc: published,\n siblings,\n locale: cms.activeLocale,\n })\n if (url) map.set(published.id, url)\n }\n return map\n }, [collection, isRoutable, publishedDocs, cms.localeConfig, cms.activeLocale])\n\n const documents = useCallback(() => cms.documentsOf(collectionId), [cms, collectionId])\n const viewState = useCallback(() => cms.tableStateOf(collectionId), [cms, collectionId])\n const setViewState = useCallback(\n (update: Parameters<CollectionViewSource['setViewState']>[0]) =>\n cms.setTableState(collectionId, update),\n [cms, collectionId],\n )\n // Named views (EC-246b): config-as-code lenses the header selector switches between.\n const views = useMemo(\n () => ({\n list: () => cms.viewsOf(collectionId),\n activeId: () => cms.activeViewId(collectionId),\n setActive: (id: string) => cms.setActiveView(collectionId, id),\n }),\n [cms, collectionId],\n )\n const create = useCallback(() => cms.createDocument(collectionId), [cms, collectionId])\n const imports = useMemo(\n () => ({ importDocuments: (items: unknown[]) => cms.importDocuments(collectionId, items) }),\n [cms, collectionId],\n )\n const resolveAsset = useCallback(\n (id: string): ResolvedAsset | undefined => {\n const record = assetStore?.assets.find((asset) => asset.id === id)\n if (!record) return undefined\n return {\n ...(record.url !== undefined ? { url: record.url } : {}),\n contentType: record.contentType,\n ...(record.title !== undefined ? { title: record.title } : {}),\n filename: record.filename,\n ...(record.alt !== undefined ? { alt: record.alt } : {}),\n }\n },\n [assetStore],\n )\n const liveUrls = useCallback(() => liveUrlMap, [liveUrlMap])\n\n return useMemo(() => {\n if (!collection) return null\n const capabilities: CollectionViewCapabilities = {\n publish: true,\n versions: true,\n locales: true,\n inlineEdit: true,\n create: true,\n duplicate: true, // EC-442: CMS collections only (owner call — rails skip)\n remove: true,\n bulk: true,\n liveUrl: isRoutable,\n hierarchy: isHierarchical(collection),\n importExport: true,\n }\n const source: CollectionViewSource = {\n collectionId,\n schema: collection,\n capabilities,\n ...(cms.projectId !== undefined ? { projectId: cms.projectId } : {}),\n assetCollectionId,\n localeConfig: cms.localeConfig,\n activeLocale: cms.activeLocale,\n documents,\n get: cms.getDocument,\n titleOf: cms.titleOf,\n // References are collection-based (EC-072): resolve candidates of ANY target\n // collection the schema declares — the workspace doc store already serves them.\n referenceCandidates: cms.documentsOf,\n collections: cms.collections,\n isFieldReadOnly: cms.isFieldReadOnly,\n validate: cms.validate,\n resolveAsset,\n lastError: cms.persistError,\n clearLastError: cms.clearPersistError,\n // Baseline + queue flush, so a pre-flight reference-index read is current.\n flush: async () => {\n await cms.ensurePersistedBaseline()\n await cms.flushPersistence()\n },\n detailPath: (id: string) => studioPath.cmsDocument(collectionId, id),\n listPath: studioPath.cmsCollection(collectionId),\n viewState,\n setViewState,\n views,\n imports,\n create,\n save: cms.saveDocument,\n remove: cms.deleteDocument,\n bulk: (ids, action, setField) => cms.runBulk(collectionId, ids, action, setField),\n publishing: {\n isPublished: cms.isPublished,\n publish: cms.publishDocument,\n unpublish: cms.unpublishDocument,\n liveUrls,\n },\n versioning: {\n restore: (ref: DocumentRef, version: number, scope) =>\n cms.restoreDocumentVersion(ref, version, scope),\n // Flush-before-read ordering folds into the loader (EC-224) so any surface\n // that mutated the history still lists an authoritative version set.\n load: async (ref: DocumentRef) => {\n await cms.flushPersistence()\n return cms.loadDocumentVersions(ref)\n },\n },\n inlineEditing: {\n commitField: (input) => cms.commitFieldEdit(input),\n },\n }\n return source\n }, [\n collection,\n collectionId,\n cms,\n isRoutable,\n documents,\n viewState,\n setViewState,\n views,\n imports,\n create,\n resolveAsset,\n liveUrls,\n assetCollectionId,\n ])\n}\n"]}
|
|
@@ -23,6 +23,7 @@ export const REDIRECT_COLLECTION_CAPABILITIES = {
|
|
|
23
23
|
locales: false,
|
|
24
24
|
inlineEdit: false,
|
|
25
25
|
create: true, // editors create redirects
|
|
26
|
+
duplicate: false, // EC-442 owner call: no rail Duplicate for now (redirects deferred)
|
|
26
27
|
remove: true,
|
|
27
28
|
bulk: true, // EC-433: bulk delete = the same per-id remove, honestly reported
|
|
28
29
|
liveUrl: false,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"redirects-collection-view-source.js","sourceRoot":"","sources":["../../../src/components/redirects/redirects-collection-view-source.ts"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useMemo } from 'react'\nimport type { CmsDocument, CollectionDef, DocumentRef, Locale, LocaleConfig } from '@elytracms/core/cms-core'\nimport { createCollectionRegistry, validateDocument } from '@elytracms/core/cms-core'\nimport type { RedirectEntry, SaveRedirectInput } from '@elytracms/core/persistence'\nimport { studioPath } from '@elytracms/studio/router'\nimport type {\n CollectionViewCapabilities,\n CollectionViewSource,\n} from '@elytracms/studio/lib/collection-view/collection-view-source'\nimport { useRedirects } from './redirects-store'\nimport { redirectsCollectionDef } from './redirects-schema'\nimport { metaBagProjection, normalizeOkResult, useRailViewSource } from '../rail-view-source'\n\n/** The redirects rail is not localized — one implicit locale keeps the lens honest. */\nconst REDIRECTS_LOCALE: Locale = 'en'\nconst REDIRECTS_LOCALE_CONFIG: LocaleConfig = {\n default: REDIRECTS_LOCALE,\n locales: [REDIRECTS_LOCALE],\n}\n\n/**\n * The capability profile of the redirects rail (EC-303): content-gated, create\n * + remove, no publish/versions/locales — redirects are CONTENT (an editor\n * owns them), so unlike the users rail this source allows `create`, is NOT\n * admin-only, and needs no principal field-access policy. Hoisted to a\n * module-level constant (EC-327), mirroring `ASSET_COLLECTION_CAPABILITIES`.\n */\nexport const REDIRECT_COLLECTION_CAPABILITIES: CollectionViewCapabilities = {\n publish: false,\n versions: false,\n locales: false,\n inlineEdit: false,\n create: true, // editors create redirects\n remove: true,\n bulk: true, // EC-433: bulk delete = the same per-id remove, honestly reported\n liveUrl: false,\n hierarchy: false,\n importExport: false,\n adminOnly: false, // content, not administration\n}\n\n/**\n * The canonical redirect fields — the storage/delivery contract of the primitive\n * rail. Anything a mount adds via *additive* `nav.redirects({ fields })` (EC-311\n * Tier 2) is by definition NOT one of these, so its value round-trips through the\n * record's open `meta` bag. Derived from the built-in def so the two never drift.\n */\nconst CANONICAL_REDIRECT_FIELDS = new Set(redirectsCollectionDef.fields.map((f) => f.name))\n\n/** Project a stored redirect into the lens's `values` shape. */\nexport function redirectToDocument(entry: RedirectEntry): CmsDocument {\n return {\n collection: redirectsCollectionDef.id,\n id: entry.id,\n values: {\n from: entry.from,\n to: entry.to,\n permanent: entry.permanent,\n enabled: entry.enabled,\n // EC-311 Tier 2: additive schema fields round-trip through the open meta bag.\n ...(entry.meta ?? {}),\n },\n }\n}\n\n/** Map an edited lens document back to the redirect write contract. */\nexport function documentToSaveRedirect(doc: CmsDocument): SaveRedirectInput {\n const values = doc.values ?? {}\n // EC-311 Tier 2: any value keyed outside the canonical set comes from a mount's\n // additive `nav.redirects({ fields })` override — persist it in the meta bag. The\n // editor holds the complete additive set, so this is the whole bag (replace, not\n // merge). Omit `meta` entirely for the default rail so its saves are unchanged.\n const meta = metaBagProjection(values, CANONICAL_REDIRECT_FIELDS)\n return {\n id: doc.id,\n from: typeof values.from === 'string' ? values.from : '',\n to: typeof values.to === 'string' ? values.to : '',\n permanent: typeof values.permanent === 'boolean' ? values.permanent : true,\n enabled: typeof values.enabled === 'boolean' ? values.enabled : true,\n ...(Object.keys(meta).length > 0 ? { meta } : {}),\n }\n}\n\n/**\n * Assemble a {@link CollectionViewSource} for the **redirects rail** (EC-303) —\n * the portable list/edit surfaces over the editorial redirects store instead of\n * the CMS store. Redirects are CONTENT (an editor owns them), so unlike the users\n * rail this source allows `create`, is NOT admin-only, and needs no principal\n * field-access policy. No publish/versions/locales/inline/import — a redirect\n * is a small record saved explicitly (a saved+enabled one is live); bulk\n * delete rides the shared rail runner (EC-433).\n *\n * Must be mounted under a `RedirectsProvider` (for {@link useRedirects}).\n *\n * `schema` defaults to the engine-owned {@link redirectsCollectionDef}; a mount may\n * pass a customized def (EC-311 — built-in fields overridden by equal name via\n * `nav.redirects({ fields })`). The canonical `from`/`to`/`permanent`/`enabled`\n * names+types are contract-locked, so the projection helpers still read the same\n * canonical values; a mount's *additive* fields (Tier 2) round-trip through the\n * record's open `meta` bag ({@link documentToSaveRedirect}/{@link redirectToDocument}).\n *\n * The shared scaffold (`useRailViewSource`, EC-327) owns `viewState`/`lastError`\n * plumbing and the assembled source; this hook supplies only the redirects-store\n * reads/writes, the capability profile, and the registry-driven `validate`.\n */\nexport function useRedirectsCollectionViewSource(\n projectId: string,\n schema: CollectionDef = redirectsCollectionDef,\n): CollectionViewSource {\n const store = useRedirects()\n // EC-311: run the (possibly customized) schema's validation — required/pattern and\n // custom `validate` fns on an overridden field must actually fire in the editor.\n const registry = useMemo(() => createCollectionRegistry([schema]), [schema])\n\n const documents = useCallback(() => store.redirects.map(redirectToDocument), [store.redirects])\n const get = useCallback(\n (ref: DocumentRef): CmsDocument | undefined => {\n const record = store.get(ref.id)\n return record ? redirectToDocument(record) : undefined\n },\n [store],\n )\n const titleOf = useCallback(\n (ref: DocumentRef): string => store.get(ref.id)?.from || '(new redirect)',\n [store],\n )\n const validate = useCallback(\n (doc: CmsDocument) => validateDocument(registry, doc, { localeConfig: REDIRECTS_LOCALE_CONFIG }),\n [registry],\n )\n const create = useCallback(\n (): DocumentRef => ({ collection: redirectsCollectionDef.id, id: store.createDraft() }),\n [store],\n )\n const save = useCallback((doc: CmsDocument) => store.save(documentToSaveRedirect(doc)), [store])\n const remove = useCallback((ref: DocumentRef) => store.remove(ref.id), [store])\n\n return useRailViewSource({\n projectId,\n schema,\n capabilities: REDIRECT_COLLECTION_CAPABILITIES,\n localeConfig: REDIRECTS_LOCALE_CONFIG,\n documents,\n get,\n titleOf,\n // No `referenceCandidates`/`collections` override: redirects carry no\n // relation/asset fields, so nothing cross-collection to resolve (the\n // factory's `() => []` / `[schema]` defaults apply).\n detailPath: (id: string) => studioPath.redirect(id),\n listPath: studioPath.redirects(),\n validate,\n create,\n save,\n remove,\n normalizeSaveResult: normalizeOkResult,\n normalizeRemoveResult: normalizeOkResult,\n })\n}\n"]}
|
|
1
|
+
{"version":3,"file":"redirects-collection-view-source.js","sourceRoot":"","sources":["../../../src/components/redirects/redirects-collection-view-source.ts"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useMemo } from 'react'\nimport type { CmsDocument, CollectionDef, DocumentRef, Locale, LocaleConfig } from '@elytracms/core/cms-core'\nimport { createCollectionRegistry, validateDocument } from '@elytracms/core/cms-core'\nimport type { RedirectEntry, SaveRedirectInput } from '@elytracms/core/persistence'\nimport { studioPath } from '@elytracms/studio/router'\nimport type {\n CollectionViewCapabilities,\n CollectionViewSource,\n} from '@elytracms/studio/lib/collection-view/collection-view-source'\nimport { useRedirects } from './redirects-store'\nimport { redirectsCollectionDef } from './redirects-schema'\nimport { metaBagProjection, normalizeOkResult, useRailViewSource } from '../rail-view-source'\n\n/** The redirects rail is not localized — one implicit locale keeps the lens honest. */\nconst REDIRECTS_LOCALE: Locale = 'en'\nconst REDIRECTS_LOCALE_CONFIG: LocaleConfig = {\n default: REDIRECTS_LOCALE,\n locales: [REDIRECTS_LOCALE],\n}\n\n/**\n * The capability profile of the redirects rail (EC-303): content-gated, create\n * + remove, no publish/versions/locales — redirects are CONTENT (an editor\n * owns them), so unlike the users rail this source allows `create`, is NOT\n * admin-only, and needs no principal field-access policy. Hoisted to a\n * module-level constant (EC-327), mirroring `ASSET_COLLECTION_CAPABILITIES`.\n */\nexport const REDIRECT_COLLECTION_CAPABILITIES: CollectionViewCapabilities = {\n publish: false,\n versions: false,\n locales: false,\n inlineEdit: false,\n create: true, // editors create redirects\n duplicate: false, // EC-442 owner call: no rail Duplicate for now (redirects deferred)\n remove: true,\n bulk: true, // EC-433: bulk delete = the same per-id remove, honestly reported\n liveUrl: false,\n hierarchy: false,\n importExport: false,\n adminOnly: false, // content, not administration\n}\n\n/**\n * The canonical redirect fields — the storage/delivery contract of the primitive\n * rail. Anything a mount adds via *additive* `nav.redirects({ fields })` (EC-311\n * Tier 2) is by definition NOT one of these, so its value round-trips through the\n * record's open `meta` bag. Derived from the built-in def so the two never drift.\n */\nconst CANONICAL_REDIRECT_FIELDS = new Set(redirectsCollectionDef.fields.map((f) => f.name))\n\n/** Project a stored redirect into the lens's `values` shape. */\nexport function redirectToDocument(entry: RedirectEntry): CmsDocument {\n return {\n collection: redirectsCollectionDef.id,\n id: entry.id,\n values: {\n from: entry.from,\n to: entry.to,\n permanent: entry.permanent,\n enabled: entry.enabled,\n // EC-311 Tier 2: additive schema fields round-trip through the open meta bag.\n ...(entry.meta ?? {}),\n },\n }\n}\n\n/** Map an edited lens document back to the redirect write contract. */\nexport function documentToSaveRedirect(doc: CmsDocument): SaveRedirectInput {\n const values = doc.values ?? {}\n // EC-311 Tier 2: any value keyed outside the canonical set comes from a mount's\n // additive `nav.redirects({ fields })` override — persist it in the meta bag. The\n // editor holds the complete additive set, so this is the whole bag (replace, not\n // merge). Omit `meta` entirely for the default rail so its saves are unchanged.\n const meta = metaBagProjection(values, CANONICAL_REDIRECT_FIELDS)\n return {\n id: doc.id,\n from: typeof values.from === 'string' ? values.from : '',\n to: typeof values.to === 'string' ? values.to : '',\n permanent: typeof values.permanent === 'boolean' ? values.permanent : true,\n enabled: typeof values.enabled === 'boolean' ? values.enabled : true,\n ...(Object.keys(meta).length > 0 ? { meta } : {}),\n }\n}\n\n/**\n * Assemble a {@link CollectionViewSource} for the **redirects rail** (EC-303) —\n * the portable list/edit surfaces over the editorial redirects store instead of\n * the CMS store. Redirects are CONTENT (an editor owns them), so unlike the users\n * rail this source allows `create`, is NOT admin-only, and needs no principal\n * field-access policy. No publish/versions/locales/inline/import — a redirect\n * is a small record saved explicitly (a saved+enabled one is live); bulk\n * delete rides the shared rail runner (EC-433).\n *\n * Must be mounted under a `RedirectsProvider` (for {@link useRedirects}).\n *\n * `schema` defaults to the engine-owned {@link redirectsCollectionDef}; a mount may\n * pass a customized def (EC-311 — built-in fields overridden by equal name via\n * `nav.redirects({ fields })`). The canonical `from`/`to`/`permanent`/`enabled`\n * names+types are contract-locked, so the projection helpers still read the same\n * canonical values; a mount's *additive* fields (Tier 2) round-trip through the\n * record's open `meta` bag ({@link documentToSaveRedirect}/{@link redirectToDocument}).\n *\n * The shared scaffold (`useRailViewSource`, EC-327) owns `viewState`/`lastError`\n * plumbing and the assembled source; this hook supplies only the redirects-store\n * reads/writes, the capability profile, and the registry-driven `validate`.\n */\nexport function useRedirectsCollectionViewSource(\n projectId: string,\n schema: CollectionDef = redirectsCollectionDef,\n): CollectionViewSource {\n const store = useRedirects()\n // EC-311: run the (possibly customized) schema's validation — required/pattern and\n // custom `validate` fns on an overridden field must actually fire in the editor.\n const registry = useMemo(() => createCollectionRegistry([schema]), [schema])\n\n const documents = useCallback(() => store.redirects.map(redirectToDocument), [store.redirects])\n const get = useCallback(\n (ref: DocumentRef): CmsDocument | undefined => {\n const record = store.get(ref.id)\n return record ? redirectToDocument(record) : undefined\n },\n [store],\n )\n const titleOf = useCallback(\n (ref: DocumentRef): string => store.get(ref.id)?.from || '(new redirect)',\n [store],\n )\n const validate = useCallback(\n (doc: CmsDocument) => validateDocument(registry, doc, { localeConfig: REDIRECTS_LOCALE_CONFIG }),\n [registry],\n )\n const create = useCallback(\n (): DocumentRef => ({ collection: redirectsCollectionDef.id, id: store.createDraft() }),\n [store],\n )\n const save = useCallback((doc: CmsDocument) => store.save(documentToSaveRedirect(doc)), [store])\n const remove = useCallback((ref: DocumentRef) => store.remove(ref.id), [store])\n\n return useRailViewSource({\n projectId,\n schema,\n capabilities: REDIRECT_COLLECTION_CAPABILITIES,\n localeConfig: REDIRECTS_LOCALE_CONFIG,\n documents,\n get,\n titleOf,\n // No `referenceCandidates`/`collections` override: redirects carry no\n // relation/asset fields, so nothing cross-collection to resolve (the\n // factory's `() => []` / `[schema]` defaults apply).\n detailPath: (id: string) => studioPath.redirect(id),\n listPath: studioPath.redirects(),\n validate,\n create,\n save,\n remove,\n normalizeSaveResult: normalizeOkResult,\n normalizeRemoveResult: normalizeOkResult,\n })\n}\n"]}
|
|
@@ -17,7 +17,23 @@
|
|
|
17
17
|
*
|
|
18
18
|
* Stacking: `DialogBlockingContent` (EC-392 discipline) — topmost modal layer,
|
|
19
19
|
* focus-trapped, Escape/outside interaction consumed, studio beneath inert.
|
|
20
|
+
*
|
|
21
|
+
* Presentation (EC-437): the sign-in card mounts BARE (no full-page opaque
|
|
22
|
+
* panel) over the stage family's translucent blurred backdrop, so the editor
|
|
23
|
+
* SEES their intact document sitting behind the prompt — showing the "your
|
|
24
|
+
* changes stay right here" promise instead of only telling it.
|
|
25
|
+
*/
|
|
26
|
+
/**
|
|
27
|
+
* The re-auth backdrop (EC-437) — the stage family's visual language
|
|
28
|
+
* (EC-431 AD-A, `surfaceGeometry`), borrowed as CLASSES only, never the
|
|
29
|
+
* `ExpandableSurface` component: the stage is lightly dismissible, re-auth is
|
|
30
|
+
* contractually non-dismissible (`DialogBlockingContent` stays the shell).
|
|
31
|
+
* One blur step above the stage's `backdrop-blur-sm`: the whole dense studio
|
|
32
|
+
* sits behind a small centered card here (not behind a near-full panel), and
|
|
33
|
+
* the stronger step keeps the card unmistakably focal while the studio stays
|
|
34
|
+
* clearly recognizable — judged live against `-sm` with screenshots.
|
|
20
35
|
*/
|
|
36
|
+
export declare const SESSION_EXPIRED_BACKDROP_CLASS = "bg-background/70 backdrop-blur-md";
|
|
21
37
|
export declare function SessionExpiredOverlay(): import("react").JSX.Element | null;
|
|
22
38
|
/**
|
|
23
39
|
* Why the sign-in screen appeared in place (EC-403): the session-expired copy
|
|
@@ -25,19 +25,35 @@ import { SignInScreen, activeProjectName } from './sign-in-view.js';
|
|
|
25
25
|
*
|
|
26
26
|
* Stacking: `DialogBlockingContent` (EC-392 discipline) — topmost modal layer,
|
|
27
27
|
* focus-trapped, Escape/outside interaction consumed, studio beneath inert.
|
|
28
|
+
*
|
|
29
|
+
* Presentation (EC-437): the sign-in card mounts BARE (no full-page opaque
|
|
30
|
+
* panel) over the stage family's translucent blurred backdrop, so the editor
|
|
31
|
+
* SEES their intact document sitting behind the prompt — showing the "your
|
|
32
|
+
* changes stay right here" promise instead of only telling it.
|
|
33
|
+
*/
|
|
34
|
+
/**
|
|
35
|
+
* The re-auth backdrop (EC-437) — the stage family's visual language
|
|
36
|
+
* (EC-431 AD-A, `surfaceGeometry`), borrowed as CLASSES only, never the
|
|
37
|
+
* `ExpandableSurface` component: the stage is lightly dismissible, re-auth is
|
|
38
|
+
* contractually non-dismissible (`DialogBlockingContent` stays the shell).
|
|
39
|
+
* One blur step above the stage's `backdrop-blur-sm`: the whole dense studio
|
|
40
|
+
* sits behind a small centered card here (not behind a near-full panel), and
|
|
41
|
+
* the stronger step keeps the card unmistakably focal while the studio stays
|
|
42
|
+
* clearly recognizable — judged live against `-sm` with screenshots.
|
|
28
43
|
*/
|
|
44
|
+
export const SESSION_EXPIRED_BACKDROP_CLASS = 'bg-background/70 backdrop-blur-md';
|
|
29
45
|
export function SessionExpiredOverlay() {
|
|
30
46
|
const auth = useAuth();
|
|
31
47
|
const t = useT();
|
|
32
48
|
const cache = useOperationCache();
|
|
33
49
|
if (!auth.sessionExpired)
|
|
34
50
|
return null;
|
|
35
|
-
return (_jsx(Dialog, { open: true, children: _jsxs(DialogBlockingContent, { "aria-describedby": undefined, children: [_jsx(DialogTitle, { className: "sr-only", children: t('error.sessionExpiredTitle') }), _jsx(SignInScreen, { notice: _jsx(SessionExpiredNotice, {}), projectName: activeProjectName(), showModeToggle: false, devCredentials: authMode() === 'playground' ? LOCAL_DEV_CREDENTIALS : null, authenticate: (_mode, email, password) => auth.signIn(email, password), onAuthenticated: () =>
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
51
|
+
return (_jsx(Dialog, { open: true, children: _jsxs(DialogBlockingContent, { "aria-describedby": undefined, overlayClassName: SESSION_EXPIRED_BACKDROP_CLASS, children: [_jsx(DialogTitle, { className: "sr-only", children: t('error.sessionExpiredTitle') }), _jsx("div", { className: "grid min-h-full place-items-center p-6", children: _jsx(SignInScreen, { layout: "bare", notice: _jsx(SessionExpiredNotice, {}), projectName: activeProjectName(), showModeToggle: false, devCredentials: authMode() === 'playground' ? LOCAL_DEV_CREDENTIALS : null, authenticate: (_mode, email, password) => auth.signIn(email, password), onAuthenticated: () =>
|
|
52
|
+
// The session was dead for an unknown span: reads may have failed
|
|
53
|
+
// (retry: false) or gone stale. One wholesale invalidation lets
|
|
54
|
+
// every active query refetch under the fresh session — resume,
|
|
55
|
+
// not reload. (The empty key prefix matches all queries.)
|
|
56
|
+
cache.invalidate([]) }) })] }) }));
|
|
41
57
|
}
|
|
42
58
|
/**
|
|
43
59
|
* Why the sign-in screen appeared in place (EC-403): the session-expired copy
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-expired-overlay.js","sourceRoot":"","sources":["../../src/components/session-expired-overlay.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAC1C,OAAO,EAAE,qBAAqB,EAAE,QAAQ,EAAE,MAAM,4BAA4B,CAAA;AAC5E,OAAO,EAAE,IAAI,EAAE,MAAM,4BAA4B,CAAA;AACjD,OAAO,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAA;AAC/D,OAAO,EAAE,MAAM,EAAE,qBAAqB,EAAE,WAAW,EAAE,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAA;AACnG,OAAO,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAA;AACxC,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAA;AAEhE
|
|
1
|
+
{"version":3,"file":"session-expired-overlay.js","sourceRoot":"","sources":["../../src/components/session-expired-overlay.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAC1C,OAAO,EAAE,qBAAqB,EAAE,QAAQ,EAAE,MAAM,4BAA4B,CAAA;AAC5E,OAAO,EAAE,IAAI,EAAE,MAAM,4BAA4B,CAAA;AACjD,OAAO,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAA;AAC/D,OAAO,EAAE,MAAM,EAAE,qBAAqB,EAAE,WAAW,EAAE,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAA;AACnG,OAAO,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAA;AACxC,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAA;AAEhE;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,8BAA8B,GAAG,mCAAmC,CAAA;AAEjF,MAAM,UAAU,qBAAqB;IACnC,MAAM,IAAI,GAAG,OAAO,EAAE,CAAA;IACtB,MAAM,CAAC,GAAG,IAAI,EAAE,CAAA;IAChB,MAAM,KAAK,GAAG,iBAAiB,EAAE,CAAA;IAEjC,IAAI,CAAC,IAAI,CAAC,cAAc;QAAE,OAAO,IAAI,CAAA;IAErC,OAAO,CACL,KAAC,MAAM,IAAC,IAAI,kBACV,MAAC,qBAAqB,wBACF,SAAS,EAC3B,gBAAgB,EAAE,8BAA8B,aAEhD,KAAC,WAAW,IAAC,SAAS,EAAC,SAAS,YAAE,CAAC,CAAC,2BAA2B,CAAC,GAAe,EAI/E,cAAK,SAAS,EAAC,wCAAwC,YACrD,KAAC,YAAY,IACX,MAAM,EAAC,MAAM,EACb,MAAM,EAAE,KAAC,oBAAoB,KAAG,EAChC,WAAW,EAAE,iBAAiB,EAAE,EAChC,cAAc,EAAE,KAAK,EACrB,cAAc,EAAE,QAAQ,EAAE,KAAK,YAAY,CAAC,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,IAAI,EAC1E,YAAY,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,EACtE,eAAe,EAAE,GAAG,EAAE;wBACpB,kEAAkE;wBAClE,gEAAgE;wBAChE,+DAA+D;wBAC/D,0DAA0D;wBAC1D,KAAK,CAAC,UAAU,CAAC,EAAE,CAAC,GAEtB,GACE,IACgB,GACjB,CACV,CAAA;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,oBAAoB;IAClC,MAAM,CAAC,GAAG,IAAI,EAAE,CAAA;IAChB,OAAO,CACL,cACE,IAAI,EAAC,OAAO,EACZ,SAAS,EAAC,qEAAqE,YAE/E,MAAC,GAAG,IAAC,GAAG,EAAC,GAAG,EAAC,KAAK,EAAC,OAAO,aACxB,KAAC,WAAW,IAAC,SAAS,EAAC,yCAAyC,GAAG,EACnE,MAAC,KAAK,IAAC,GAAG,EAAC,GAAG,EAAC,SAAS,EAAC,gBAAgB,aACvC,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,SAAS,EAAC,8BAA8B,YACrD,CAAC,CAAC,2BAA2B,CAAC,GAC1B,EACP,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,YACzB,CAAC,CAAC,sBAAsB,CAAC,GACrB,IACD,IACJ,GACF,CACP,CAAA;AACH,CAAC","sourcesContent":["import { CircleAlert } from 'lucide-react'\nimport { LOCAL_DEV_CREDENTIALS, authMode } from '@elytracms/studio/lib/auth'\nimport { useT } from '@elytracms/studio/lib/i18n'\nimport { useOperationCache } from '@elytracms/studio/lib/query'\nimport { Dialog, DialogBlockingContent, DialogTitle, Row, Stack, Text } from '@elytracms/studio/ui'\nimport { useAuth } from './auth-context'\nimport { SignInScreen, activeProjectName } from './sign-in-view'\n\n/**\n * The blocking in-place re-auth overlay (EC-403). Mounted once in the studio\n * root (inside `StudioProvider`, over every route) and shown while the auth\n * store reports `sessionExpired`: the backend session died mid-flight, so all\n * editing is blocked behind a non-dismissible modal that mounts the SAME\n * `SignInScreen` composition the `/sign-in` route uses — **without any\n * navigation**. The React tree beneath stays alive, so every unsaved editor\n * state survives; a successful sign-in applies the fresh session (clearing the\n * flag in the auth store) and editing resumes exactly where it stopped.\n * Pending failed writes keep their normal retry paths (a `persistError`\n * clears on the next successful save).\n *\n * Re-auth is strictly sign-in (no create-account toggle — a brand-new account\n * cannot resume an existing editing session). On success the studio query\n * cache is invalidated wholesale so reads that failed or staled while the\n * session was dead recover without a reload.\n *\n * Stacking: `DialogBlockingContent` (EC-392 discipline) — topmost modal layer,\n * focus-trapped, Escape/outside interaction consumed, studio beneath inert.\n *\n * Presentation (EC-437): the sign-in card mounts BARE (no full-page opaque\n * panel) over the stage family's translucent blurred backdrop, so the editor\n * SEES their intact document sitting behind the prompt — showing the \"your\n * changes stay right here\" promise instead of only telling it.\n */\n\n/**\n * The re-auth backdrop (EC-437) — the stage family's visual language\n * (EC-431 AD-A, `surfaceGeometry`), borrowed as CLASSES only, never the\n * `ExpandableSurface` component: the stage is lightly dismissible, re-auth is\n * contractually non-dismissible (`DialogBlockingContent` stays the shell).\n * One blur step above the stage's `backdrop-blur-sm`: the whole dense studio\n * sits behind a small centered card here (not behind a near-full panel), and\n * the stronger step keeps the card unmistakably focal while the studio stays\n * clearly recognizable — judged live against `-sm` with screenshots.\n */\nexport const SESSION_EXPIRED_BACKDROP_CLASS = 'bg-background/70 backdrop-blur-md'\n\nexport function SessionExpiredOverlay() {\n const auth = useAuth()\n const t = useT()\n const cache = useOperationCache()\n\n if (!auth.sessionExpired) return null\n\n return (\n <Dialog open>\n <DialogBlockingContent\n aria-describedby={undefined}\n overlayClassName={SESSION_EXPIRED_BACKDROP_CLASS}\n >\n <DialogTitle className=\"sr-only\">{t('error.sessionExpiredTitle')}</DialogTitle>\n {/* min-h-full (not h-full): when the card outgrows a short viewport,\n the wrapper grows with it and the blocking content scrolls —\n centered when it fits, never clipped when it doesn't. */}\n <div className=\"grid min-h-full place-items-center p-6\">\n <SignInScreen\n layout=\"bare\"\n notice={<SessionExpiredNotice />}\n projectName={activeProjectName()}\n showModeToggle={false}\n devCredentials={authMode() === 'playground' ? LOCAL_DEV_CREDENTIALS : null}\n authenticate={(_mode, email, password) => auth.signIn(email, password)}\n onAuthenticated={() =>\n // The session was dead for an unknown span: reads may have failed\n // (retry: false) or gone stale. One wholesale invalidation lets\n // every active query refetch under the fresh session — resume,\n // not reload. (The empty key prefix matches all queries.)\n cache.invalidate([])\n }\n />\n </div>\n </DialogBlockingContent>\n </Dialog>\n )\n}\n\n/**\n * Why the sign-in screen appeared in place (EC-403): the session-expired copy\n * rendered above the form via the `SignInScreen` notice slot. Pure — exported\n * for the provider-free static-render smoke test.\n */\nexport function SessionExpiredNotice() {\n const t = useT()\n return (\n <div\n role=\"alert\"\n className=\"rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2\"\n >\n <Row gap=\"2\" align=\"start\">\n <CircleAlert className=\"mt-0.5 size-4 shrink-0 text-destructive\" />\n <Stack gap=\"1\" className=\"min-w-0 flex-1\">\n <Text size=\"sm\" className=\"font-medium text-destructive\">\n {t('error.sessionExpiredTitle')}\n </Text>\n <Text size=\"sm\" tone=\"muted\">\n {t('error.sessionExpired')}\n </Text>\n </Stack>\n </Row>\n </div>\n )\n}\n"]}
|
|
@@ -38,6 +38,15 @@ interface SignInScreenProps {
|
|
|
38
38
|
* normal `/sign-in` route passes none.
|
|
39
39
|
*/
|
|
40
40
|
notice?: ReactNode;
|
|
41
|
+
/**
|
|
42
|
+
* Presentation wrapper (EC-437). `'page'` (default) renders the
|
|
43
|
+
* full-viewport opaque panel — the `/sign-in` route's shipped layout,
|
|
44
|
+
* byte-identical. `'bare'` renders only the centered card column so a host
|
|
45
|
+
* surface composes its own backdrop: the session-expired re-auth overlay
|
|
46
|
+
* mounts it over a translucent blurred backdrop, keeping the intact studio
|
|
47
|
+
* visible beneath the prompt.
|
|
48
|
+
*/
|
|
49
|
+
layout?: 'page' | 'bare';
|
|
41
50
|
brandMark?: ReactNode;
|
|
42
51
|
productName?: string;
|
|
43
52
|
/** Attempt auth for the chosen mode. The container maps this to signIn/signUp. */
|
|
@@ -49,5 +58,5 @@ interface SignInScreenProps {
|
|
|
49
58
|
* Provider-free presentational sign-in screen. Owns only local form state and
|
|
50
59
|
* calls injected callbacks — no context hooks — so it static-renders in tests.
|
|
51
60
|
*/
|
|
52
|
-
export declare function SignInScreen({ projectName, showModeToggle, devCredentials, notice, brandMark, productName, authenticate, onAuthenticated, }: SignInScreenProps): import("react").JSX.Element;
|
|
61
|
+
export declare function SignInScreen({ projectName, showModeToggle, devCredentials, notice, layout, brandMark, productName, authenticate, onAuthenticated, }: SignInScreenProps): import("react").JSX.Element;
|
|
53
62
|
export {};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useState } from 'react';
|
|
3
3
|
import { useNavigate, useSearch } from '@tanstack/react-router';
|
|
4
4
|
import { Button, Card, CardContent, Field, Heading, Input, Stack, Text } from '@elytracms/studio/ui';
|
|
@@ -38,7 +38,7 @@ export function SignInView({ brandMark, productName }) {
|
|
|
38
38
|
* Provider-free presentational sign-in screen. Owns only local form state and
|
|
39
39
|
* calls injected callbacks — no context hooks — so it static-renders in tests.
|
|
40
40
|
*/
|
|
41
|
-
export function SignInScreen({ projectName, showModeToggle, devCredentials, notice, brandMark, productName = 'Elytra', authenticate, onAuthenticated, }) {
|
|
41
|
+
export function SignInScreen({ projectName, showModeToggle, devCredentials, notice, layout = 'page', brandMark, productName = 'Elytra', authenticate, onAuthenticated, }) {
|
|
42
42
|
const t = useT();
|
|
43
43
|
const [email, setEmail] = useState('');
|
|
44
44
|
const [password, setPassword] = useState('');
|
|
@@ -66,7 +66,7 @@ export function SignInScreen({ projectName, showModeToggle, devCredentials, noti
|
|
|
66
66
|
}
|
|
67
67
|
const submitting = phase === 'submitting';
|
|
68
68
|
const creating = mode === 'signup';
|
|
69
|
-
return (_jsx(
|
|
69
|
+
return (_jsx(ScreenShell, { layout: layout, children: _jsxs(Stack, { gap: "4", className: "w-full max-w-sm", children: [notice, _jsxs(Stack, { gap: "1", align: "center", children: [brandMark ?? _jsx("span", { className: "size-8 rounded-lg bg-primary shadow-sm", "aria-hidden": true }), _jsx(Heading, { size: "h2", children: creating ? t('auth.signIn.headingCreate') : t('auth.signIn.heading', { productName }) }), _jsx(Text, { size: "sm", tone: "muted", children: projectName
|
|
70
70
|
? t(creating ? 'auth.signIn.subtext.register' : 'auth.signIn.subtext.continue', {
|
|
71
71
|
project: projectName,
|
|
72
72
|
})
|
|
@@ -81,6 +81,17 @@ export function SignInScreen({ projectName, showModeToggle, devCredentials, noti
|
|
|
81
81
|
setError(null);
|
|
82
82
|
}, children: creating ? t('auth.signIn.toggle.signIn') : t('auth.signIn.toggle.createAccount') })] })) : null, devCredentials ? _jsx(DevCredentialsHint, { entries: devCredentials }) : null] }) }));
|
|
83
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* The screen's presentation shell (EC-437). `'page'` is the `/sign-in`
|
|
86
|
+
* route's shipped full-viewport opaque panel, byte-identical; `'bare'`
|
|
87
|
+
* renders the card column alone — the session-expired re-auth overlay owns
|
|
88
|
+
* backdrop + centering there, so the studio stays visible beneath the prompt.
|
|
89
|
+
*/
|
|
90
|
+
function ScreenShell({ layout, children }) {
|
|
91
|
+
if (layout === 'bare')
|
|
92
|
+
return _jsx(_Fragment, { children: children });
|
|
93
|
+
return _jsx("div", { className: "grid min-h-dvh place-items-center bg-background p-6", children: children });
|
|
94
|
+
}
|
|
84
95
|
/** Seeded dev accounts for the explicit playground mode — fixtures, not secrets. */
|
|
85
96
|
function DevCredentialsHint({ entries }) {
|
|
86
97
|
const t = useT();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sign-in-view.js","sourceRoot":"","sources":["../../src/components/sign-in-view.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useState } from 'react'\nimport type { FormEvent, ReactNode } from 'react'\nimport { useNavigate, useSearch } from '@tanstack/react-router'\nimport { Button, Card, CardContent, Field, Heading, Input, Stack, Text } from '@elytracms/studio/ui'\nimport { useAuth } from '@elytracms/studio/components/auth-context'\nimport { LOCAL_DEV_CREDENTIALS, authMode, postSignInTarget } from '@elytracms/studio/lib/auth'\nimport type { SignInResult } from '@elytracms/studio/lib/auth'\nimport { signInErrorMessageKey } from '@elytracms/studio/lib/auth-errors'\nimport { useT } from '@elytracms/studio/lib/i18n'\nimport { getActiveProjectSlug, getWorkspaceState } from '@elytracms/studio/lib/workspace'\n\n/**\n * Engine-shipped `/sign-in` screen (EC-150; consolidated from the copy-pasted\n * consumer route by EC-375, EC-369 finding #8). The consumer route file keeps\n * only the route object — `createFileRoute('/sign-in')({ validateSearch,\n * component: SignInView })` — because TanStack Router requires the route object\n * to live in the consumer's route tree; the ~140-line body lives here so a\n * copy never drifts.\n *\n * Deterministic states: idle → submitting → (signed-in redirect | inline\n * structured failure). Invalid credentials are a rendered state, never a silent\n * failure or crash. In playground (in-memory) mode the seeded dev credentials\n * are listed so the whole flow works offline.\n *\n * Split into a `SignInView` container (owns the router + auth seams) and a\n * provider-free `SignInScreen` (pure local form state + injected callbacks) so\n * the screen static-renders in a node smoke test without the full provider tree.\n *\n * All copy resolves through the studio message catalog (`auth.signIn.*`,\n * EC-397), including the engine-classified auth failures — see\n * `signInErrorMessageKey`; an unrecognized failure renders `result.message`\n * raw (honest fallback).\n */\n\ntype FormPhase = 'idle' | 'submitting'\ntype AuthMode = 'signin' | 'signup'\n\n/** A seeded dev-credential entry surfaced in playground mode (fixtures, not secrets). */\ntype DevCredential = (typeof LOCAL_DEV_CREDENTIALS)[number]\n\nexport interface SignInViewProps {\n /**\n * Brand mark rendered above the heading. Defaults to the neutral studio\n * placeholder square — a future logo just passes a node here (e.g.\n * `component: () => <SignInView brandMark={<Logo />} />`), no engine change.\n */\n brandMark?: ReactNode\n /** Product name in the \"Sign in to …\" heading. Default: `'Elytra'`. */\n productName?: string\n}\n\n/**\n * The project whose backend this sign-in authenticates against (EC-179).\n * Exported for the session-expired re-auth overlay (EC-403), which mounts\n * {@link SignInScreen} with the same subtext.\n */\nexport function activeProjectName(): string | null {\n const state = getWorkspaceState()\n if (state.kind !== 'config') return null\n const slug = getActiveProjectSlug()\n const project = (slug && state.projects.find((p) => p.slug === slug)) || state.projects[0]\n return project?.name ?? null\n}\n\n/**\n * The `/sign-in` route component. Container: reads the router (post-sign-in\n * redirect target from the route search) + auth seams and hands the pure\n * presentational screen its data plus an `authenticate` callback.\n */\nexport function SignInView({ brandMark, productName }: SignInViewProps) {\n const auth = useAuth()\n const navigate = useNavigate()\n // Self-source the redirect (loose search — the consumer route's `validateSearch`\n // guarantees `{ redirect?: string }`), so the route file needs no wrapper.\n const search = useSearch({ strict: false }) as { redirect?: string }\n const redirect = search.redirect\n\n return (\n <SignInScreen\n projectName={activeProjectName()}\n showModeToggle={authMode() === 'convex'}\n devCredentials={authMode() === 'playground' ? LOCAL_DEV_CREDENTIALS : null}\n brandMark={brandMark}\n productName={productName}\n authenticate={(mode, email, password) =>\n mode === 'signup' ? auth.signUp(email, password) : auth.signIn(email, password)\n }\n onAuthenticated={() => navigate({ to: postSignInTarget(redirect) })}\n />\n )\n}\n\ninterface SignInScreenProps {\n projectName: string | null\n /** Whether to offer the sign-in ↔ create-account toggle (convex mode only). */\n showModeToggle: boolean\n /** Seeded dev accounts to list (playground mode), or null to hide the hint. */\n devCredentials: readonly DevCredential[] | null\n /**\n * Contextual notice rendered above the heading — the session-expired\n * re-auth overlay (EC-403) explains WHY the screen appeared here. The\n * normal `/sign-in` route passes none.\n */\n notice?: ReactNode\n brandMark?: ReactNode\n productName?: string\n /** Attempt auth for the chosen mode. The container maps this to signIn/signUp. */\n authenticate: (mode: AuthMode, email: string, password: string) => Promise<SignInResult>\n /** Called once, after a successful authentication (the container navigates). */\n onAuthenticated: () => void | Promise<void>\n}\n\n/**\n * Provider-free presentational sign-in screen. Owns only local form state and\n * calls injected callbacks — no context hooks — so it static-renders in tests.\n */\nexport function SignInScreen({\n projectName,\n showModeToggle,\n devCredentials,\n notice,\n brandMark,\n productName = 'Elytra',\n authenticate,\n onAuthenticated,\n}: SignInScreenProps) {\n const t = useT()\n const [email, setEmail] = useState('')\n const [password, setPassword] = useState('')\n const [phase, setPhase] = useState<FormPhase>('idle')\n const [mode, setMode] = useState<AuthMode>('signin')\n const [error, setError] = useState<string | null>(null)\n\n async function handleSubmit(event: FormEvent<HTMLFormElement>) {\n event.preventDefault()\n setPhase('submitting')\n setError(null)\n const result = await authenticate(mode, email, password)\n if (result.ok) {\n await onAuthenticated()\n return\n }\n // A fresh backend has no account yet: nudge sign-in → create, and vice versa.\n if (result.reason === 'account-exists') setMode('signin')\n // EC-397: engine-classified failures (EC-362/EC-363 reasons) localize\n // through the message catalog; an unrecognized shape renders the adapter's\n // raw English message — honest, never a stale mapping.\n const errorKey = signInErrorMessageKey(result)\n setError(errorKey ? t(errorKey) : result.message)\n setPhase('idle')\n }\n\n const submitting = phase === 'submitting'\n const creating = mode === 'signup'\n\n return (\n <div className=\"grid min-h-dvh place-items-center bg-background p-6\">\n <Stack gap=\"4\" className=\"w-full max-w-sm\">\n {notice}\n <Stack gap=\"1\" align=\"center\">\n {brandMark ?? <span className=\"size-8 rounded-lg bg-primary shadow-sm\" aria-hidden />}\n <Heading size=\"h2\">\n {creating ? t('auth.signIn.headingCreate') : t('auth.signIn.heading', { productName })}\n </Heading>\n <Text size=\"sm\" tone=\"muted\">\n {projectName\n ? t(creating ? 'auth.signIn.subtext.register' : 'auth.signIn.subtext.continue', {\n project: projectName,\n })\n : t('auth.signIn.subtext.generic')}\n </Text>\n </Stack>\n\n <Card>\n <CardContent className=\"pt-5\">\n <form onSubmit={handleSubmit} noValidate>\n <Stack gap=\"4\">\n <Field label={t('auth.signIn.emailLabel')} htmlFor=\"sign-in-email\" required>\n <Input\n id=\"sign-in-email\"\n type=\"email\"\n autoComplete=\"email\"\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n placeholder={t('auth.signIn.emailPlaceholder')}\n disabled={submitting}\n />\n </Field>\n <Field\n label={t('auth.signIn.passwordLabel')}\n htmlFor=\"sign-in-password\"\n required\n error={error}\n >\n <Input\n id=\"sign-in-password\"\n type=\"password\"\n autoComplete={creating ? 'new-password' : 'current-password'}\n value={password}\n onChange={(e) => setPassword(e.target.value)}\n disabled={submitting}\n />\n </Field>\n <Button type=\"submit\" disabled={submitting} className=\"w-full\">\n {submitting\n ? creating\n ? t('auth.signIn.submittingCreate')\n : t('auth.signIn.submitting')\n : creating\n ? t('auth.signIn.submitCreate')\n : t('auth.signIn.submit')}\n </Button>\n </Stack>\n </form>\n </CardContent>\n </Card>\n\n {showModeToggle ? (\n <Text size=\"sm\" tone=\"muted\" className=\"text-center\">\n {creating ? t('auth.signIn.toggle.alreadyHave') : t('auth.signIn.toggle.firstTime')}{' '}\n <button\n type=\"button\"\n className=\"font-medium text-primary underline-offset-2 hover:underline\"\n onClick={() => {\n setMode(creating ? 'signin' : 'signup')\n setError(null)\n }}\n >\n {creating ? t('auth.signIn.toggle.signIn') : t('auth.signIn.toggle.createAccount')}\n </button>\n </Text>\n ) : null}\n\n {devCredentials ? <DevCredentialsHint entries={devCredentials} /> : null}\n </Stack>\n </div>\n )\n}\n\n/** Seeded dev accounts for the explicit playground mode — fixtures, not secrets. */\nfunction DevCredentialsHint({ entries }: { entries: readonly DevCredential[] }) {\n const t = useT()\n return (\n <Card className=\"border-dashed\">\n <CardContent className=\"pt-4\">\n <Stack gap=\"1\">\n <Text size=\"xs\" tone=\"muted\" className=\"font-medium\">\n {t('auth.signIn.devAccounts.title')}\n </Text>\n {entries.map((entry) => (\n <Text key={entry.user.id} size=\"xs\" tone=\"muted\" className=\"font-mono\">\n {entry.user.email} / {entry.password}\n </Text>\n ))}\n </Stack>\n </CardContent>\n </Card>\n )\n}\n"]}
|
|
1
|
+
{"version":3,"file":"sign-in-view.js","sourceRoot":"","sources":["../../src/components/sign-in-view.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useState } from 'react'\nimport type { FormEvent, ReactNode } from 'react'\nimport { useNavigate, useSearch } from '@tanstack/react-router'\nimport { Button, Card, CardContent, Field, Heading, Input, Stack, Text } from '@elytracms/studio/ui'\nimport { useAuth } from '@elytracms/studio/components/auth-context'\nimport { LOCAL_DEV_CREDENTIALS, authMode, postSignInTarget } from '@elytracms/studio/lib/auth'\nimport type { SignInResult } from '@elytracms/studio/lib/auth'\nimport { signInErrorMessageKey } from '@elytracms/studio/lib/auth-errors'\nimport { useT } from '@elytracms/studio/lib/i18n'\nimport { getActiveProjectSlug, getWorkspaceState } from '@elytracms/studio/lib/workspace'\n\n/**\n * Engine-shipped `/sign-in` screen (EC-150; consolidated from the copy-pasted\n * consumer route by EC-375, EC-369 finding #8). The consumer route file keeps\n * only the route object — `createFileRoute('/sign-in')({ validateSearch,\n * component: SignInView })` — because TanStack Router requires the route object\n * to live in the consumer's route tree; the ~140-line body lives here so a\n * copy never drifts.\n *\n * Deterministic states: idle → submitting → (signed-in redirect | inline\n * structured failure). Invalid credentials are a rendered state, never a silent\n * failure or crash. In playground (in-memory) mode the seeded dev credentials\n * are listed so the whole flow works offline.\n *\n * Split into a `SignInView` container (owns the router + auth seams) and a\n * provider-free `SignInScreen` (pure local form state + injected callbacks) so\n * the screen static-renders in a node smoke test without the full provider tree.\n *\n * All copy resolves through the studio message catalog (`auth.signIn.*`,\n * EC-397), including the engine-classified auth failures — see\n * `signInErrorMessageKey`; an unrecognized failure renders `result.message`\n * raw (honest fallback).\n */\n\ntype FormPhase = 'idle' | 'submitting'\ntype AuthMode = 'signin' | 'signup'\n\n/** A seeded dev-credential entry surfaced in playground mode (fixtures, not secrets). */\ntype DevCredential = (typeof LOCAL_DEV_CREDENTIALS)[number]\n\nexport interface SignInViewProps {\n /**\n * Brand mark rendered above the heading. Defaults to the neutral studio\n * placeholder square — a future logo just passes a node here (e.g.\n * `component: () => <SignInView brandMark={<Logo />} />`), no engine change.\n */\n brandMark?: ReactNode\n /** Product name in the \"Sign in to …\" heading. Default: `'Elytra'`. */\n productName?: string\n}\n\n/**\n * The project whose backend this sign-in authenticates against (EC-179).\n * Exported for the session-expired re-auth overlay (EC-403), which mounts\n * {@link SignInScreen} with the same subtext.\n */\nexport function activeProjectName(): string | null {\n const state = getWorkspaceState()\n if (state.kind !== 'config') return null\n const slug = getActiveProjectSlug()\n const project = (slug && state.projects.find((p) => p.slug === slug)) || state.projects[0]\n return project?.name ?? null\n}\n\n/**\n * The `/sign-in` route component. Container: reads the router (post-sign-in\n * redirect target from the route search) + auth seams and hands the pure\n * presentational screen its data plus an `authenticate` callback.\n */\nexport function SignInView({ brandMark, productName }: SignInViewProps) {\n const auth = useAuth()\n const navigate = useNavigate()\n // Self-source the redirect (loose search — the consumer route's `validateSearch`\n // guarantees `{ redirect?: string }`), so the route file needs no wrapper.\n const search = useSearch({ strict: false }) as { redirect?: string }\n const redirect = search.redirect\n\n return (\n <SignInScreen\n projectName={activeProjectName()}\n showModeToggle={authMode() === 'convex'}\n devCredentials={authMode() === 'playground' ? LOCAL_DEV_CREDENTIALS : null}\n brandMark={brandMark}\n productName={productName}\n authenticate={(mode, email, password) =>\n mode === 'signup' ? auth.signUp(email, password) : auth.signIn(email, password)\n }\n onAuthenticated={() => navigate({ to: postSignInTarget(redirect) })}\n />\n )\n}\n\ninterface SignInScreenProps {\n projectName: string | null\n /** Whether to offer the sign-in ↔ create-account toggle (convex mode only). */\n showModeToggle: boolean\n /** Seeded dev accounts to list (playground mode), or null to hide the hint. */\n devCredentials: readonly DevCredential[] | null\n /**\n * Contextual notice rendered above the heading — the session-expired\n * re-auth overlay (EC-403) explains WHY the screen appeared here. The\n * normal `/sign-in` route passes none.\n */\n notice?: ReactNode\n /**\n * Presentation wrapper (EC-437). `'page'` (default) renders the\n * full-viewport opaque panel — the `/sign-in` route's shipped layout,\n * byte-identical. `'bare'` renders only the centered card column so a host\n * surface composes its own backdrop: the session-expired re-auth overlay\n * mounts it over a translucent blurred backdrop, keeping the intact studio\n * visible beneath the prompt.\n */\n layout?: 'page' | 'bare'\n brandMark?: ReactNode\n productName?: string\n /** Attempt auth for the chosen mode. The container maps this to signIn/signUp. */\n authenticate: (mode: AuthMode, email: string, password: string) => Promise<SignInResult>\n /** Called once, after a successful authentication (the container navigates). */\n onAuthenticated: () => void | Promise<void>\n}\n\n/**\n * Provider-free presentational sign-in screen. Owns only local form state and\n * calls injected callbacks — no context hooks — so it static-renders in tests.\n */\nexport function SignInScreen({\n projectName,\n showModeToggle,\n devCredentials,\n notice,\n layout = 'page',\n brandMark,\n productName = 'Elytra',\n authenticate,\n onAuthenticated,\n}: SignInScreenProps) {\n const t = useT()\n const [email, setEmail] = useState('')\n const [password, setPassword] = useState('')\n const [phase, setPhase] = useState<FormPhase>('idle')\n const [mode, setMode] = useState<AuthMode>('signin')\n const [error, setError] = useState<string | null>(null)\n\n async function handleSubmit(event: FormEvent<HTMLFormElement>) {\n event.preventDefault()\n setPhase('submitting')\n setError(null)\n const result = await authenticate(mode, email, password)\n if (result.ok) {\n await onAuthenticated()\n return\n }\n // A fresh backend has no account yet: nudge sign-in → create, and vice versa.\n if (result.reason === 'account-exists') setMode('signin')\n // EC-397: engine-classified failures (EC-362/EC-363 reasons) localize\n // through the message catalog; an unrecognized shape renders the adapter's\n // raw English message — honest, never a stale mapping.\n const errorKey = signInErrorMessageKey(result)\n setError(errorKey ? t(errorKey) : result.message)\n setPhase('idle')\n }\n\n const submitting = phase === 'submitting'\n const creating = mode === 'signup'\n\n return (\n <ScreenShell layout={layout}>\n <Stack gap=\"4\" className=\"w-full max-w-sm\">\n {notice}\n <Stack gap=\"1\" align=\"center\">\n {brandMark ?? <span className=\"size-8 rounded-lg bg-primary shadow-sm\" aria-hidden />}\n <Heading size=\"h2\">\n {creating ? t('auth.signIn.headingCreate') : t('auth.signIn.heading', { productName })}\n </Heading>\n <Text size=\"sm\" tone=\"muted\">\n {projectName\n ? t(creating ? 'auth.signIn.subtext.register' : 'auth.signIn.subtext.continue', {\n project: projectName,\n })\n : t('auth.signIn.subtext.generic')}\n </Text>\n </Stack>\n\n <Card>\n <CardContent className=\"pt-5\">\n <form onSubmit={handleSubmit} noValidate>\n <Stack gap=\"4\">\n <Field label={t('auth.signIn.emailLabel')} htmlFor=\"sign-in-email\" required>\n <Input\n id=\"sign-in-email\"\n type=\"email\"\n autoComplete=\"email\"\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n placeholder={t('auth.signIn.emailPlaceholder')}\n disabled={submitting}\n />\n </Field>\n <Field\n label={t('auth.signIn.passwordLabel')}\n htmlFor=\"sign-in-password\"\n required\n error={error}\n >\n <Input\n id=\"sign-in-password\"\n type=\"password\"\n autoComplete={creating ? 'new-password' : 'current-password'}\n value={password}\n onChange={(e) => setPassword(e.target.value)}\n disabled={submitting}\n />\n </Field>\n <Button type=\"submit\" disabled={submitting} className=\"w-full\">\n {submitting\n ? creating\n ? t('auth.signIn.submittingCreate')\n : t('auth.signIn.submitting')\n : creating\n ? t('auth.signIn.submitCreate')\n : t('auth.signIn.submit')}\n </Button>\n </Stack>\n </form>\n </CardContent>\n </Card>\n\n {showModeToggle ? (\n <Text size=\"sm\" tone=\"muted\" className=\"text-center\">\n {creating ? t('auth.signIn.toggle.alreadyHave') : t('auth.signIn.toggle.firstTime')}{' '}\n <button\n type=\"button\"\n className=\"font-medium text-primary underline-offset-2 hover:underline\"\n onClick={() => {\n setMode(creating ? 'signin' : 'signup')\n setError(null)\n }}\n >\n {creating ? t('auth.signIn.toggle.signIn') : t('auth.signIn.toggle.createAccount')}\n </button>\n </Text>\n ) : null}\n\n {devCredentials ? <DevCredentialsHint entries={devCredentials} /> : null}\n </Stack>\n </ScreenShell>\n )\n}\n\n/**\n * The screen's presentation shell (EC-437). `'page'` is the `/sign-in`\n * route's shipped full-viewport opaque panel, byte-identical; `'bare'`\n * renders the card column alone — the session-expired re-auth overlay owns\n * backdrop + centering there, so the studio stays visible beneath the prompt.\n */\nfunction ScreenShell({ layout, children }: { layout: 'page' | 'bare'; children: ReactNode }) {\n if (layout === 'bare') return <>{children}</>\n return <div className=\"grid min-h-dvh place-items-center bg-background p-6\">{children}</div>\n}\n\n/** Seeded dev accounts for the explicit playground mode — fixtures, not secrets. */\nfunction DevCredentialsHint({ entries }: { entries: readonly DevCredential[] }) {\n const t = useT()\n return (\n <Card className=\"border-dashed\">\n <CardContent className=\"pt-4\">\n <Stack gap=\"1\">\n <Text size=\"xs\" tone=\"muted\" className=\"font-medium\">\n {t('auth.signIn.devAccounts.title')}\n </Text>\n {entries.map((entry) => (\n <Text key={entry.user.id} size=\"xs\" tone=\"muted\" className=\"font-mono\">\n {entry.user.email} / {entry.password}\n </Text>\n ))}\n </Stack>\n </CardContent>\n </Card>\n )\n}\n"]}
|
|
@@ -20,6 +20,7 @@ export const USER_COLLECTION_CAPABILITIES = {
|
|
|
20
20
|
locales: false,
|
|
21
21
|
inlineEdit: false,
|
|
22
22
|
create: false, // identities are minted by sign-up, never here
|
|
23
|
+
duplicate: false, // EC-442: identities aren't copyable content
|
|
23
24
|
remove: true, // revoke membership (does not delete the auth identity)
|
|
24
25
|
bulk: true, // EC-433: bulk delete = the same per-id revoke, honestly reported
|
|
25
26
|
liveUrl: false,
|