@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.
Files changed (88) hide show
  1. package/dist/components/account/self-account-view-source.js +1 -0
  2. package/dist/components/account/self-account-view-source.js.map +1 -1
  3. package/dist/components/app-views/studio-config.d.ts +22 -0
  4. package/dist/components/app-views/studio-config.js.map +1 -1
  5. package/dist/components/assets/assets-collection-view-source.js +1 -0
  6. package/dist/components/assets/assets-collection-view-source.js.map +1 -1
  7. package/dist/components/builder/blocks-model.d.ts +91 -29
  8. package/dist/components/builder/blocks-model.js +83 -54
  9. package/dist/components/builder/blocks-model.js.map +1 -1
  10. package/dist/components/builder/blocks-panel.d.ts +40 -12
  11. package/dist/components/builder/blocks-panel.js +88 -34
  12. package/dist/components/builder/blocks-panel.js.map +1 -1
  13. package/dist/components/builder/fixture-thumbnail.d.ts +13 -23
  14. package/dist/components/builder/fixture-thumbnail.js +27 -99
  15. package/dist/components/builder/fixture-thumbnail.js.map +1 -1
  16. package/dist/components/builder/spacing-handles.d.ts +14 -11
  17. package/dist/components/builder/spacing-handles.js +33 -57
  18. package/dist/components/builder/spacing-handles.js.map +1 -1
  19. package/dist/components/cms/collection-table.js +19 -2
  20. package/dist/components/cms/collection-table.js.map +1 -1
  21. package/dist/components/cms/detail-editor.js +73 -51
  22. package/dist/components/cms/detail-editor.js.map +1 -1
  23. package/dist/components/cms/duplicate-document.d.ts +45 -0
  24. package/dist/components/cms/duplicate-document.js +141 -0
  25. package/dist/components/cms/duplicate-document.js.map +1 -0
  26. package/dist/components/cms/field-anchors.d.ts +67 -0
  27. package/dist/components/cms/field-anchors.js +91 -0
  28. package/dist/components/cms/field-anchors.js.map +1 -0
  29. package/dist/components/cms/issue-copy.d.ts +45 -0
  30. package/dist/components/cms/issue-copy.js +89 -0
  31. package/dist/components/cms/issue-copy.js.map +1 -0
  32. package/dist/components/cms/issues-pane.d.ts +11 -2
  33. package/dist/components/cms/issues-pane.js +22 -4
  34. package/dist/components/cms/issues-pane.js.map +1 -1
  35. package/dist/components/cms/object-array-field.d.ts +2 -0
  36. package/dist/components/cms/object-array-field.js +75 -21
  37. package/dist/components/cms/object-array-field.js.map +1 -1
  38. package/dist/components/cms/row-actions.d.ts +10 -3
  39. package/dist/components/cms/row-actions.js +8 -0
  40. package/dist/components/cms/row-actions.js.map +1 -1
  41. package/dist/components/cms/use-field-focus.d.ts +25 -9
  42. package/dist/components/cms/use-field-focus.js +80 -15
  43. package/dist/components/cms/use-field-focus.js.map +1 -1
  44. package/dist/components/collection-view/cms-collection-view-source.js +1 -0
  45. package/dist/components/collection-view/cms-collection-view-source.js.map +1 -1
  46. package/dist/components/redirects/redirects-collection-view-source.js +1 -0
  47. package/dist/components/redirects/redirects-collection-view-source.js.map +1 -1
  48. package/dist/components/session-expired-overlay.d.ts +16 -0
  49. package/dist/components/session-expired-overlay.js +22 -6
  50. package/dist/components/session-expired-overlay.js.map +1 -1
  51. package/dist/components/sign-in-view.d.ts +10 -1
  52. package/dist/components/sign-in-view.js +14 -3
  53. package/dist/components/sign-in-view.js.map +1 -1
  54. package/dist/components/users/users-collection-view-source.js +1 -0
  55. package/dist/components/users/users-collection-view-source.js.map +1 -1
  56. package/dist/customization.d.ts +18 -1
  57. package/dist/customization.js +8 -0
  58. package/dist/customization.js.map +1 -1
  59. package/dist/lib/cms-views/view-model.d.ts +6 -6
  60. package/dist/lib/collection-view/collection-view-source.d.ts +8 -0
  61. package/dist/lib/collection-view/collection-view-source.js.map +1 -1
  62. package/dist/lib/i18n/messages.de.js +44 -1
  63. package/dist/lib/i18n/messages.de.js.map +1 -1
  64. package/dist/lib/i18n/messages.en.d.ts +39 -0
  65. package/dist/lib/i18n/messages.en.js +47 -1
  66. package/dist/lib/i18n/messages.en.js.map +1 -1
  67. package/dist/lib/persona/labels.d.ts +5 -1
  68. package/dist/lib/persona/labels.js +43 -30
  69. package/dist/lib/persona/labels.js.map +1 -1
  70. package/dist/lib/workspace/index.d.ts +10 -0
  71. package/dist/lib/workspace/index.js +11 -0
  72. package/dist/lib/workspace/index.js.map +1 -1
  73. package/dist/previews/enumerate.d.ts +68 -0
  74. package/dist/previews/enumerate.js +78 -0
  75. package/dist/previews/enumerate.js.map +1 -0
  76. package/dist/previews/harness.d.ts +50 -0
  77. package/dist/previews/harness.js +123 -0
  78. package/dist/previews/harness.js.map +1 -0
  79. package/dist/previews/manifest-format.d.ts +57 -0
  80. package/dist/previews/manifest-format.js +59 -0
  81. package/dist/previews/manifest-format.js.map +1 -0
  82. package/dist/ui/dialog.d.ts +9 -1
  83. package/dist/ui/dialog.js +8 -2
  84. package/dist/ui/dialog.js.map +1 -1
  85. package/dist/ui/field.d.ts +24 -1
  86. package/dist/ui/field.js +23 -2
  87. package/dist/ui/field.js.map +1 -1
  88. package/package.json +10 -6
@@ -0,0 +1,123 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Component } from 'react';
3
+ import { createRoot } from 'react-dom/client';
4
+ import { flushSync } from 'react-dom';
5
+ import { renderNode } from '@elytracms/core/runtime-renderer';
6
+ import { DEFAULT_VARIANT, fixtureSubtree } from '../components/builder/blocks-model.js';
7
+ import { seededNodeFromManifest } from '../components/builder/node-factory.js';
8
+ import { previewContext } from '../lib/registry/index.js';
9
+ import { previewEntriesForConfig, previewRegistryFromConfig } from './enumerate.js';
10
+ /**
11
+ * EC-440 — the browser half of the `elytra previews` render harness. The CLI
12
+ * boots the consumer studio app's OWN Vite (programmatically, with a minimal
13
+ * inline config) and serves a page whose entry module does exactly three
14
+ * things: import the consumer's `elytra.config.ts` (the component registry —
15
+ * code), import the consumer THEME (the same standalone Tailwind entry the
16
+ * canvas iframe and delivery load, EC-414 — so preview == canvas == delivery
17
+ * by construction), and call {@link mountPreviewHarness}.
18
+ *
19
+ * DATA-FREE, deliberately (no new trust boundary): no persistence adapter, no
20
+ * auth, no content data, no env secrets — `createStudio` is never called. The
21
+ * registry is built directly from the config's `components` entries plus the
22
+ * base primitives ({@link previewRegistryFromConfig}), and bindings resolve to
23
+ * `undefined` (`previewContext`), so data-dependent components render their
24
+ * honest empty states.
25
+ *
26
+ * The driving CLI talks to the page through `window.__ELYTRA_PREVIEWS__`:
27
+ * `entries` (the same enumeration the CLI derives node-side — a handshake
28
+ * guard) and `render(componentId, fixture)`, which mounts ONE fixture subtree
29
+ * — the exact subtree the blocks panel inserts (`fixtureSubtree`, else the
30
+ * seeded manifest default, EC-439) — into the stage and resolves once the
31
+ * document has settled (fonts loaded, images complete, two animation frames).
32
+ * The CLI then screenshots the viewport.
33
+ */
34
+ /** The id of the stage element fixtures render into (created when absent). */
35
+ export const PREVIEW_STAGE_ID = 'elytra-preview-stage';
36
+ /** Catches a fixture whose render throws — the entry fails visibly, the page survives. */
37
+ class HarnessErrorBoundary extends Component {
38
+ state = { failed: false };
39
+ static getDerivedStateFromError() {
40
+ return { failed: true };
41
+ }
42
+ componentDidCatch(error) {
43
+ this.props.onError(error);
44
+ }
45
+ render() {
46
+ return this.state.failed ? null : this.props.children;
47
+ }
48
+ }
49
+ /** Resolve once the document is visually settled: fonts, images, two frames. */
50
+ async function settled(doc) {
51
+ try {
52
+ await doc.fonts?.ready;
53
+ }
54
+ catch {
55
+ // Font-loading failures must not wedge the run — the render is still shot.
56
+ }
57
+ await Promise.all(Array.from(doc.images).map((img) => img.complete
58
+ ? Promise.resolve()
59
+ : new Promise((resolve) => {
60
+ img.addEventListener('load', () => resolve(), { once: true });
61
+ img.addEventListener('error', () => resolve(), { once: true });
62
+ })));
63
+ await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve())));
64
+ }
65
+ /** Build the node a fixture renders — the exact subtree the blocks panel inserts. */
66
+ function nodeForEntry(studio, componentId, fixture) {
67
+ const manifest = studio.registry.getManifest(componentId);
68
+ if (!manifest)
69
+ return { error: `Unknown component "${componentId}".` };
70
+ const declared = manifest.fixtures?.find((f) => f.name === fixture);
71
+ if (declared)
72
+ return { node: fixtureSubtree(studio, manifest, declared, `preview-${componentId}`).node };
73
+ if (fixture === DEFAULT_VARIANT && (manifest.fixtures ?? []).length === 0) {
74
+ return { node: seededNodeFromManifest(manifest) };
75
+ }
76
+ return { error: `Unknown fixture "${fixture}" for component "${componentId}".` };
77
+ }
78
+ /**
79
+ * Mount the harness: build the registry from the consumer config, expose the
80
+ * driver API on `window.__ELYTRA_PREVIEWS__`. Idempotent per document.
81
+ */
82
+ export function mountPreviewHarness(config) {
83
+ const studio = previewRegistryFromConfig(config);
84
+ const ctx = previewContext(studio);
85
+ const entries = previewEntriesForConfig(config).map(({ componentId, fixture }) => ({
86
+ componentId,
87
+ fixture,
88
+ }));
89
+ let stage = document.getElementById(PREVIEW_STAGE_ID);
90
+ if (!stage) {
91
+ stage = document.createElement('div');
92
+ stage.id = PREVIEW_STAGE_ID;
93
+ document.body.appendChild(stage);
94
+ }
95
+ let root = null;
96
+ const render = async (componentId, fixture) => {
97
+ const built = nodeForEntry(studio, componentId, fixture);
98
+ if ('error' in built)
99
+ return { ok: false, error: built.error };
100
+ // A fresh root per fixture: no state carryover between shots.
101
+ root?.unmount();
102
+ root = createRoot(stage);
103
+ let renderError = null;
104
+ try {
105
+ flushSync(() => {
106
+ root.render(_jsx(HarnessErrorBoundary, { onError: (error) => {
107
+ renderError = error;
108
+ }, children: renderNode(built.node, ctx) }));
109
+ });
110
+ }
111
+ catch (error) {
112
+ renderError = error instanceof Error ? error : new Error(String(error));
113
+ }
114
+ if (renderError !== null)
115
+ return { ok: false, error: renderError.message };
116
+ await settled(document);
117
+ return { ok: true };
118
+ };
119
+ const api = { ready: true, entries, render };
120
+ window.__ELYTRA_PREVIEWS__ = api;
121
+ return api;
122
+ }
123
+ //# sourceMappingURL=harness.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"harness.js","sourceRoot":"","sources":["../../src/previews/harness.tsx"],"names":[],"mappings":"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","sourcesContent":["import { Component } from 'react'\nimport type { ReactNode } from 'react'\nimport { createRoot, type Root } from 'react-dom/client'\nimport { flushSync } from 'react-dom'\nimport { renderNode } from '@elytracms/core/runtime-renderer'\nimport type { RenderContext } from '@elytracms/core/runtime-renderer'\nimport type { ComponentNode } from '@elytracms/core/project-graph'\nimport { DEFAULT_VARIANT, fixtureSubtree } from '../components/builder/blocks-model'\nimport { seededNodeFromManifest } from '../components/builder/node-factory'\nimport { previewContext } from '../lib/registry'\nimport type { StudioRegistry } from '../lib/registry'\nimport { previewEntriesForConfig, previewRegistryFromConfig } from './enumerate'\n\n/**\n * EC-440 — the browser half of the `elytra previews` render harness. The CLI\n * boots the consumer studio app's OWN Vite (programmatically, with a minimal\n * inline config) and serves a page whose entry module does exactly three\n * things: import the consumer's `elytra.config.ts` (the component registry —\n * code), import the consumer THEME (the same standalone Tailwind entry the\n * canvas iframe and delivery load, EC-414 — so preview == canvas == delivery\n * by construction), and call {@link mountPreviewHarness}.\n *\n * DATA-FREE, deliberately (no new trust boundary): no persistence adapter, no\n * auth, no content data, no env secrets — `createStudio` is never called. The\n * registry is built directly from the config's `components` entries plus the\n * base primitives ({@link previewRegistryFromConfig}), and bindings resolve to\n * `undefined` (`previewContext`), so data-dependent components render their\n * honest empty states.\n *\n * The driving CLI talks to the page through `window.__ELYTRA_PREVIEWS__`:\n * `entries` (the same enumeration the CLI derives node-side — a handshake\n * guard) and `render(componentId, fixture)`, which mounts ONE fixture subtree\n * — the exact subtree the blocks panel inserts (`fixtureSubtree`, else the\n * seeded manifest default, EC-439) — into the stage and resolves once the\n * document has settled (fonts loaded, images complete, two animation frames).\n * The CLI then screenshots the viewport.\n */\n\n/** The id of the stage element fixtures render into (created when absent). */\nexport const PREVIEW_STAGE_ID = 'elytra-preview-stage'\n\nexport type PreviewRenderResult = { ok: true } | { ok: false; error: string }\n\nexport interface PreviewHarnessApi {\n ready: true\n entries: Array<{ componentId: string; fixture: string }>\n render: (componentId: string, fixture: string) => Promise<PreviewRenderResult>\n}\n\ndeclare global {\n interface Window {\n __ELYTRA_PREVIEWS__?: PreviewHarnessApi\n }\n}\n\n/** Catches a fixture whose render throws — the entry fails visibly, the page survives. */\nclass HarnessErrorBoundary extends Component<\n { children: ReactNode; onError: (error: Error) => void },\n { failed: boolean }\n> {\n override state = { failed: false }\n\n static getDerivedStateFromError(): { failed: boolean } {\n return { failed: true }\n }\n\n override componentDidCatch(error: Error): void {\n this.props.onError(error)\n }\n\n override render(): ReactNode {\n return this.state.failed ? null : this.props.children\n }\n}\n\n/** Resolve once the document is visually settled: fonts, images, two frames. */\nasync function settled(doc: Document): Promise<void> {\n try {\n await doc.fonts?.ready\n } catch {\n // Font-loading failures must not wedge the run — the render is still shot.\n }\n await Promise.all(\n Array.from(doc.images).map((img) =>\n img.complete\n ? Promise.resolve()\n : new Promise<void>((resolve) => {\n img.addEventListener('load', () => resolve(), { once: true })\n img.addEventListener('error', () => resolve(), { once: true })\n }),\n ),\n )\n await new Promise<void>((resolve) =>\n requestAnimationFrame(() => requestAnimationFrame(() => resolve())),\n )\n}\n\n/** Build the node a fixture renders — the exact subtree the blocks panel inserts. */\nfunction nodeForEntry(\n studio: StudioRegistry,\n componentId: string,\n fixture: string,\n): { node: ComponentNode } | { error: string } {\n const manifest = studio.registry.getManifest(componentId)\n if (!manifest) return { error: `Unknown component \"${componentId}\".` }\n const declared = manifest.fixtures?.find((f) => f.name === fixture)\n if (declared) return { node: fixtureSubtree(studio, manifest, declared, `preview-${componentId}`).node }\n if (fixture === DEFAULT_VARIANT && (manifest.fixtures ?? []).length === 0) {\n return { node: seededNodeFromManifest(manifest) }\n }\n return { error: `Unknown fixture \"${fixture}\" for component \"${componentId}\".` }\n}\n\n/**\n * Mount the harness: build the registry from the consumer config, expose the\n * driver API on `window.__ELYTRA_PREVIEWS__`. Idempotent per document.\n */\nexport function mountPreviewHarness(config: unknown): PreviewHarnessApi {\n const studio = previewRegistryFromConfig(config)\n const ctx: RenderContext = previewContext(studio)\n const entries = previewEntriesForConfig(config).map(({ componentId, fixture }) => ({\n componentId,\n fixture,\n }))\n\n let stage = document.getElementById(PREVIEW_STAGE_ID)\n if (!stage) {\n stage = document.createElement('div')\n stage.id = PREVIEW_STAGE_ID\n document.body.appendChild(stage)\n }\n\n let root: Root | null = null\n\n const render = async (componentId: string, fixture: string): Promise<PreviewRenderResult> => {\n const built = nodeForEntry(studio, componentId, fixture)\n if ('error' in built) return { ok: false, error: built.error }\n\n // A fresh root per fixture: no state carryover between shots.\n root?.unmount()\n root = createRoot(stage!)\n let renderError: Error | null = null\n try {\n flushSync(() => {\n root!.render(\n <HarnessErrorBoundary\n onError={(error) => {\n renderError = error\n }}\n >\n {renderNode(built.node, ctx)}\n </HarnessErrorBoundary>,\n )\n })\n } catch (error) {\n renderError = error instanceof Error ? error : new Error(String(error))\n }\n if (renderError !== null) return { ok: false, error: (renderError as Error).message }\n await settled(document)\n return { ok: true }\n }\n\n const api: PreviewHarnessApi = { ready: true, entries, render }\n window.__ELYTRA_PREVIEWS__ = api\n return api\n}\n"]}
@@ -0,0 +1,57 @@
1
+ import type { GeneratedPreviewLookup } from '../components/builder/blocks-model.js';
2
+ /**
3
+ * The previews-manifest format (EC-440) — the JSON contract between the
4
+ * `elytra previews` CLI (the writer) and a consumer's `studio.config.tsx`
5
+ * (the reader, via {@link generatedPreviewsFromManifest}).
6
+ *
7
+ * The CLI renders every canvas-eligible component's fixtures headlessly
8
+ * against the consumer theme, writes the images to the studio app's
9
+ * `public/elytra-previews/` (served at `/elytra-previews/...`), and writes
10
+ * this manifest next to `studio.config.tsx` (`studio/elytra-previews.json`)
11
+ * so it is importable as a plain JSON module. Everything in it is
12
+ * deterministic — no timestamps — so an unchanged re-run rewrites nothing.
13
+ */
14
+ /** Format version the CLI writes; bump on incompatible schema/hash changes. */
15
+ export declare const PREVIEWS_MANIFEST_VERSION = 1;
16
+ /** One generated preview artifact: a component fixture and its rendered image. */
17
+ export interface GeneratedPreviewsManifestEntry {
18
+ componentId: string;
19
+ /** The fixture name, or `'Default'` for a fixture-less component's implicit variant. */
20
+ fixture: string;
21
+ /** Root-relative image URL (the file lives in the studio app's `public/`). */
22
+ url: string;
23
+ /**
24
+ * Input hash (sha-256 hex over the stable-serialized inputs: manifest prop +
25
+ * slot shape, fixture props/slotChildren, engine version). `elytra previews`
26
+ * skips entries whose hash is unchanged; `elytra previews --check` flags
27
+ * entries whose hash drifted as stale. Deliberately NOT part of the lookup —
28
+ * the studio only reads `componentId`/`fixture`/`url`.
29
+ */
30
+ hash: string;
31
+ }
32
+ /** The manifest document `elytra previews` writes (`studio/elytra-previews.json`). */
33
+ export interface GeneratedPreviewsManifest {
34
+ version: number;
35
+ /** The `@elytracms/studio` version the images were rendered with. */
36
+ engine: string;
37
+ entries: GeneratedPreviewsManifestEntry[];
38
+ }
39
+ /**
40
+ * Adapt a previews manifest (a plain JSON import) into the EC-439
41
+ * {@link GeneratedPreviewLookup} seam. The whole consumer wiring is ~2 lines:
42
+ *
43
+ * ```tsx
44
+ * // studio.config.tsx
45
+ * import previewsManifest from './elytra-previews.json'
46
+ * export default defineStudio({
47
+ * previews: { generated: generatedPreviewsFromManifest(previewsManifest) },
48
+ * })
49
+ * ```
50
+ *
51
+ * Tolerant by design (the input is a generated file a human may hand-edit or
52
+ * delete): a malformed document or entry yields lookup misses — the blocks
53
+ * panel then falls back to the icon+name placeholder — never a crash. A
54
+ * fixture-less component's single implicit variant is looked up (and written
55
+ * by the CLI) under the fixture name `'Default'`.
56
+ */
57
+ export declare function generatedPreviewsFromManifest(manifest: unknown): GeneratedPreviewLookup;
@@ -0,0 +1,59 @@
1
+ /**
2
+ * The previews-manifest format (EC-440) — the JSON contract between the
3
+ * `elytra previews` CLI (the writer) and a consumer's `studio.config.tsx`
4
+ * (the reader, via {@link generatedPreviewsFromManifest}).
5
+ *
6
+ * The CLI renders every canvas-eligible component's fixtures headlessly
7
+ * against the consumer theme, writes the images to the studio app's
8
+ * `public/elytra-previews/` (served at `/elytra-previews/...`), and writes
9
+ * this manifest next to `studio.config.tsx` (`studio/elytra-previews.json`)
10
+ * so it is importable as a plain JSON module. Everything in it is
11
+ * deterministic — no timestamps — so an unchanged re-run rewrites nothing.
12
+ */
13
+ /** Format version the CLI writes; bump on incompatible schema/hash changes. */
14
+ export const PREVIEWS_MANIFEST_VERSION = 1;
15
+ /** Tolerant entry guard — a malformed entry is skipped, never a crash. */
16
+ function isManifestEntry(value) {
17
+ if (typeof value !== 'object' || value === null)
18
+ return false;
19
+ const entry = value;
20
+ return (typeof entry.componentId === 'string' &&
21
+ typeof entry.fixture === 'string' &&
22
+ typeof entry.url === 'string');
23
+ }
24
+ /**
25
+ * Adapt a previews manifest (a plain JSON import) into the EC-439
26
+ * {@link GeneratedPreviewLookup} seam. The whole consumer wiring is ~2 lines:
27
+ *
28
+ * ```tsx
29
+ * // studio.config.tsx
30
+ * import previewsManifest from './elytra-previews.json'
31
+ * export default defineStudio({
32
+ * previews: { generated: generatedPreviewsFromManifest(previewsManifest) },
33
+ * })
34
+ * ```
35
+ *
36
+ * Tolerant by design (the input is a generated file a human may hand-edit or
37
+ * delete): a malformed document or entry yields lookup misses — the blocks
38
+ * panel then falls back to the icon+name placeholder — never a crash. A
39
+ * fixture-less component's single implicit variant is looked up (and written
40
+ * by the CLI) under the fixture name `'Default'`.
41
+ */
42
+ export function generatedPreviewsFromManifest(manifest) {
43
+ const byKey = new Map();
44
+ if (typeof manifest === 'object' && manifest !== null) {
45
+ const entries = manifest.entries;
46
+ if (Array.isArray(entries)) {
47
+ for (const entry of entries) {
48
+ if (isManifestEntry(entry))
49
+ byKey.set(lookupKey(entry.componentId, entry.fixture), entry.url);
50
+ }
51
+ }
52
+ }
53
+ return (componentId, fixtureName) => byKey.get(lookupKey(componentId, fixtureName));
54
+ }
55
+ function lookupKey(componentId, fixture) {
56
+ // NUL (\u0000) can appear in neither a component id nor a fixture name.
57
+ return `${componentId}\u0000${fixture}`;
58
+ }
59
+ //# sourceMappingURL=manifest-format.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest-format.js","sourceRoot":"","sources":["../../src/previews/manifest-format.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;GAWG;AAEH,+EAA+E;AAC/E,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAAC,CAAA;AA2B1C,0EAA0E;AAC1E,SAAS,eAAe,CAAC,KAAc;IACrC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,KAAK,CAAA;IAC7D,MAAM,KAAK,GAAG,KAAgC,CAAA;IAC9C,OAAO,CACL,OAAO,KAAK,CAAC,WAAW,KAAK,QAAQ;QACrC,OAAO,KAAK,CAAC,OAAO,KAAK,QAAQ;QACjC,OAAO,KAAK,CAAC,GAAG,KAAK,QAAQ,CAC9B,CAAA;AACH,CAAC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,6BAA6B,CAAC,QAAiB;IAC7D,MAAM,KAAK,GAAG,IAAI,GAAG,EAAkB,CAAA;IACvC,IAAI,OAAO,QAAQ,KAAK,QAAQ,IAAI,QAAQ,KAAK,IAAI,EAAE,CAAC;QACtD,MAAM,OAAO,GAAI,QAAkC,CAAC,OAAO,CAAA;QAC3D,IAAI,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;YAC3B,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;gBAC5B,IAAI,eAAe,CAAC,KAAK,CAAC;oBAAE,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC,KAAK,CAAC,WAAW,EAAE,KAAK,CAAC,OAAO,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,CAAA;YAC/F,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,CAAC,WAAW,EAAE,WAAW,EAAE,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC,CAAA;AACrF,CAAC;AAED,SAAS,SAAS,CAAC,WAAmB,EAAE,OAAe;IACrD,wEAAwE;IACxE,OAAO,GAAG,WAAW,SAAS,OAAO,EAAE,CAAA;AACzC,CAAC","sourcesContent":["import type { GeneratedPreviewLookup } from '../components/builder/blocks-model'\n\n/**\n * The previews-manifest format (EC-440) — the JSON contract between the\n * `elytra previews` CLI (the writer) and a consumer's `studio.config.tsx`\n * (the reader, via {@link generatedPreviewsFromManifest}).\n *\n * The CLI renders every canvas-eligible component's fixtures headlessly\n * against the consumer theme, writes the images to the studio app's\n * `public/elytra-previews/` (served at `/elytra-previews/...`), and writes\n * this manifest next to `studio.config.tsx` (`studio/elytra-previews.json`)\n * so it is importable as a plain JSON module. Everything in it is\n * deterministic — no timestamps — so an unchanged re-run rewrites nothing.\n */\n\n/** Format version the CLI writes; bump on incompatible schema/hash changes. */\nexport const PREVIEWS_MANIFEST_VERSION = 1\n\n/** One generated preview artifact: a component fixture and its rendered image. */\nexport interface GeneratedPreviewsManifestEntry {\n componentId: string\n /** The fixture name, or `'Default'` for a fixture-less component's implicit variant. */\n fixture: string\n /** Root-relative image URL (the file lives in the studio app's `public/`). */\n url: string\n /**\n * Input hash (sha-256 hex over the stable-serialized inputs: manifest prop +\n * slot shape, fixture props/slotChildren, engine version). `elytra previews`\n * skips entries whose hash is unchanged; `elytra previews --check` flags\n * entries whose hash drifted as stale. Deliberately NOT part of the lookup —\n * the studio only reads `componentId`/`fixture`/`url`.\n */\n hash: string\n}\n\n/** The manifest document `elytra previews` writes (`studio/elytra-previews.json`). */\nexport interface GeneratedPreviewsManifest {\n version: number\n /** The `@elytracms/studio` version the images were rendered with. */\n engine: string\n entries: GeneratedPreviewsManifestEntry[]\n}\n\n/** Tolerant entry guard — a malformed entry is skipped, never a crash. */\nfunction isManifestEntry(value: unknown): value is GeneratedPreviewsManifestEntry {\n if (typeof value !== 'object' || value === null) return false\n const entry = value as Record<string, unknown>\n return (\n typeof entry.componentId === 'string' &&\n typeof entry.fixture === 'string' &&\n typeof entry.url === 'string'\n )\n}\n\n/**\n * Adapt a previews manifest (a plain JSON import) into the EC-439\n * {@link GeneratedPreviewLookup} seam. The whole consumer wiring is ~2 lines:\n *\n * ```tsx\n * // studio.config.tsx\n * import previewsManifest from './elytra-previews.json'\n * export default defineStudio({\n * previews: { generated: generatedPreviewsFromManifest(previewsManifest) },\n * })\n * ```\n *\n * Tolerant by design (the input is a generated file a human may hand-edit or\n * delete): a malformed document or entry yields lookup misses — the blocks\n * panel then falls back to the icon+name placeholder — never a crash. A\n * fixture-less component's single implicit variant is looked up (and written\n * by the CLI) under the fixture name `'Default'`.\n */\nexport function generatedPreviewsFromManifest(manifest: unknown): GeneratedPreviewLookup {\n const byKey = new Map<string, string>()\n if (typeof manifest === 'object' && manifest !== null) {\n const entries = (manifest as { entries?: unknown }).entries\n if (Array.isArray(entries)) {\n for (const entry of entries) {\n if (isManifestEntry(entry)) byKey.set(lookupKey(entry.componentId, entry.fixture), entry.url)\n }\n }\n }\n return (componentId, fixtureName) => byKey.get(lookupKey(componentId, fixtureName))\n}\n\nfunction lookupKey(componentId: string, fixture: string): string {\n // NUL (\\u0000) can appear in neither a component id nor a fixture name.\n return `${componentId}\\u0000${fixture}`\n}\n"]}
@@ -19,8 +19,16 @@ export declare function DialogContent({ className, children, ...props }: Compone
19
19
  * layer — its Escape handler `preventDefault`s, which the `ExpandableSurface`
20
20
  * window-bubble handler reads as "handled above me", so sheets open beneath
21
21
  * the blocker stay open instead of swallowing keys.
22
+ *
23
+ * `overlayClassName` is presentation-only (EC-437): it restyles the backdrop
24
+ * element (the session-expired re-auth overlay swaps in the stage family's
25
+ * translucent blurred treatment so the studio beneath stays visible). Every
26
+ * blocking mechanic — the overlay element itself, the consumed dismissal
27
+ * events, focus trap, scroll lock — is untouched by it.
22
28
  */
23
- export declare function DialogBlockingContent({ className, children, ...props }: ComponentProps<typeof DialogPrimitive.Content>): import("react").JSX.Element;
29
+ export declare function DialogBlockingContent({ className, children, overlayClassName, ...props }: ComponentProps<typeof DialogPrimitive.Content> & {
30
+ overlayClassName?: string;
31
+ }): import("react").JSX.Element;
24
32
  export declare function DialogHeader({ className, ...props }: ComponentProps<'div'>): import("react").JSX.Element;
25
33
  export declare function DialogFooter({ className, ...props }: ComponentProps<'div'>): import("react").JSX.Element;
26
34
  export declare function DialogTitle({ className, ...props }: ComponentProps<typeof DialogPrimitive.Title>): import("react").JSX.Element;
package/dist/ui/dialog.js CHANGED
@@ -25,9 +25,15 @@ export function DialogContent({ className, children, ...props }) {
25
25
  * layer — its Escape handler `preventDefault`s, which the `ExpandableSurface`
26
26
  * window-bubble handler reads as "handled above me", so sheets open beneath
27
27
  * the blocker stay open instead of swallowing keys.
28
+ *
29
+ * `overlayClassName` is presentation-only (EC-437): it restyles the backdrop
30
+ * element (the session-expired re-auth overlay swaps in the stage family's
31
+ * translucent blurred treatment so the studio beneath stays visible). Every
32
+ * blocking mechanic — the overlay element itself, the consumed dismissal
33
+ * events, focus trap, scroll lock — is untouched by it.
28
34
  */
29
- export function DialogBlockingContent({ className, children, ...props }) {
30
- return (_jsxs(DialogPortal, { children: [_jsx(DialogOverlay, {}), _jsx(DialogPrimitive.Content, { onEscapeKeyDown: (event) => event.preventDefault(), onPointerDownOutside: (event) => event.preventDefault(), onInteractOutside: (event) => event.preventDefault(), className: cn('fixed inset-0 z-50 overflow-y-auto outline-none', className), ...props, children: children })] }));
35
+ export function DialogBlockingContent({ className, children, overlayClassName, ...props }) {
36
+ return (_jsxs(DialogPortal, { children: [_jsx(DialogOverlay, { className: overlayClassName }), _jsx(DialogPrimitive.Content, { onEscapeKeyDown: (event) => event.preventDefault(), onPointerDownOutside: (event) => event.preventDefault(), onInteractOutside: (event) => event.preventDefault(), className: cn('fixed inset-0 z-50 overflow-y-auto outline-none', className), ...props, children: children })] }));
31
37
  }
32
38
  export function DialogHeader({ className, ...props }) {
33
39
  return _jsx("div", { className: cn('flex flex-col gap-1.5', className), ...props });
@@ -1 +1 @@
1
- {"version":3,"file":"dialog.js","sourceRoot":"","sources":["../../src/ui/dialog.tsx"],"names":[],"mappings":";AAAA,OAAO,KAAK,eAAe,MAAM,wBAAwB,CAAA;AACzD,OAAO,EAAE,CAAC,EAAE,MAAM,cAAc,CAAA;AAEhC,OAAO,EAAE,EAAE,EAAE,MAAM,cAAc,CAAA;AAEjC,MAAM,CAAC,MAAM,MAAM,GAAG,eAAe,CAAC,IAAI,CAAA;AAC1C,MAAM,CAAC,MAAM,aAAa,GAAG,eAAe,CAAC,OAAO,CAAA;AACpD,MAAM,CAAC,MAAM,WAAW,GAAG,eAAe,CAAC,KAAK,CAAA;AAChD,MAAM,CAAC,MAAM,YAAY,GAAG,eAAe,CAAC,MAAM,CAAA;AAElD,MAAM,UAAU,aAAa,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAAkD;IACnG,OAAO,CACL,KAAC,eAAe,CAAC,OAAO,IACtB,SAAS,EAAE,EAAE,CACX,4KAA4K,EAC5K,SAAS,CACV,KACG,KAAK,GACT,CACH,CAAA;AACH,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,EAC5B,SAAS,EACT,QAAQ,EACR,GAAG,KAAK,EACuC;IAC/C,OAAO,CACL,MAAC,YAAY,eACX,KAAC,aAAa,KAAG,EACjB,MAAC,eAAe,CAAC,OAAO,IACtB,SAAS,EAAE,EAAE,CACX,4KAA4K,EAC5K,uFAAuF,EACvF,gGAAgG,EAChG,SAAS,CACV,KACG,KAAK,aAER,QAAQ,EACT,MAAC,eAAe,CAAC,KAAK,IAAC,SAAS,EAAC,0KAA0K,aACzM,KAAC,CAAC,IAAC,SAAS,EAAC,QAAQ,GAAG,EACxB,eAAM,SAAS,EAAC,SAAS,sBAAa,IAChB,IACA,IACb,CAChB,CAAA;AACH,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,qBAAqB,CAAC,EACpC,SAAS,EACT,QAAQ,EACR,GAAG,KAAK,EACuC;IAC/C,OAAO,CACL,MAAC,YAAY,eACX,KAAC,aAAa,KAAG,EACjB,KAAC,eAAe,CAAC,OAAO,IACtB,eAAe,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,cAAc,EAAE,EAClD,oBAAoB,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,cAAc,EAAE,EACvD,iBAAiB,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,cAAc,EAAE,EACpD,SAAS,EAAE,EAAE,CAAC,iDAAiD,EAAE,SAAS,CAAC,KACvE,KAAK,YAER,QAAQ,GACe,IACb,CAChB,CAAA;AACH,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAAyB;IACzE,OAAO,cAAK,SAAS,EAAE,EAAE,CAAC,uBAAuB,EAAE,SAAS,CAAC,KAAM,KAAK,GAAI,CAAA;AAC9E,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAAyB;IACzE,OAAO,cAAK,SAAS,EAAE,EAAE,CAAC,0CAA0C,EAAE,SAAS,CAAC,KAAM,KAAK,GAAI,CAAA;AACjG,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAAgD;IAC/F,OAAO,KAAC,eAAe,CAAC,KAAK,IAAC,SAAS,EAAE,EAAE,CAAC,wCAAwC,EAAE,SAAS,CAAC,KAAM,KAAK,GAAI,CAAA;AACjH,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,EAChC,SAAS,EACT,GAAG,KAAK,EAC2C;IACnD,OAAO,KAAC,eAAe,CAAC,WAAW,IAAC,SAAS,EAAE,EAAE,CAAC,+BAA+B,EAAE,SAAS,CAAC,KAAM,KAAK,GAAI,CAAA;AAC9G,CAAC","sourcesContent":["import * as DialogPrimitive from '@radix-ui/react-dialog'\nimport { X } from 'lucide-react'\nimport type { ComponentProps } from 'react'\nimport { cn } from '../lib/utils'\n\nexport const Dialog = DialogPrimitive.Root\nexport const DialogTrigger = DialogPrimitive.Trigger\nexport const DialogClose = DialogPrimitive.Close\nexport const DialogPortal = DialogPrimitive.Portal\n\nexport function DialogOverlay({ className, ...props }: ComponentProps<typeof DialogPrimitive.Overlay>) {\n return (\n <DialogPrimitive.Overlay\n className={cn(\n 'fixed inset-0 z-50 bg-black/55 backdrop-blur-[2px] data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=closed]:animate-out data-[state=closed]:fade-out-0',\n className,\n )}\n {...props}\n />\n )\n}\n\nexport function DialogContent({\n className,\n children,\n ...props\n}: ComponentProps<typeof DialogPrimitive.Content>) {\n return (\n <DialogPortal>\n <DialogOverlay />\n <DialogPrimitive.Content\n className={cn(\n 'fixed left-1/2 top-1/2 z-50 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl border border-border bg-popover p-5 text-popover-foreground shadow-2xl',\n 'data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',\n 'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95',\n className,\n )}\n {...props}\n >\n {children}\n <DialogPrimitive.Close className=\"absolute right-3.5 top-3.5 rounded-sm text-muted-foreground opacity-70 transition-opacity hover:opacity-100 focus-visible:ring-2 focus-visible:ring-ring/60 outline-none\">\n <X className=\"size-4\" />\n <span className=\"sr-only\">Close</span>\n </DialogPrimitive.Close>\n </DialogPrimitive.Content>\n </DialogPortal>\n )\n}\n\n/**\n * Full-viewport, NON-dismissible modal content (EC-403: the blocking\n * session-expired re-auth overlay). Escape, backdrop clicks, and outside\n * interaction are all consumed — the only way out is the flow rendered inside\n * (or the owner clearing the controlled `open`). Radix still provides the\n * modal guarantees the blocking treatment relies on: focus trap, scroll lock,\n * `aria-modal`, and outside pointer-event lockout, so the studio beneath stays\n * fully inert while the React tree (and all unsaved editor state) lives on.\n *\n * Stacking (EC-392 discipline): mounted last and modal, it is the topmost\n * layer — its Escape handler `preventDefault`s, which the `ExpandableSurface`\n * window-bubble handler reads as \"handled above me\", so sheets open beneath\n * the blocker stay open instead of swallowing keys.\n */\nexport function DialogBlockingContent({\n className,\n children,\n ...props\n}: ComponentProps<typeof DialogPrimitive.Content>) {\n return (\n <DialogPortal>\n <DialogOverlay />\n <DialogPrimitive.Content\n onEscapeKeyDown={(event) => event.preventDefault()}\n onPointerDownOutside={(event) => event.preventDefault()}\n onInteractOutside={(event) => event.preventDefault()}\n className={cn('fixed inset-0 z-50 overflow-y-auto outline-none', className)}\n {...props}\n >\n {children}\n </DialogPrimitive.Content>\n </DialogPortal>\n )\n}\n\nexport function DialogHeader({ className, ...props }: ComponentProps<'div'>) {\n return <div className={cn('flex flex-col gap-1.5', className)} {...props} />\n}\n\nexport function DialogFooter({ className, ...props }: ComponentProps<'div'>) {\n return <div className={cn('flex items-center justify-end gap-2 pt-2', className)} {...props} />\n}\n\nexport function DialogTitle({ className, ...props }: ComponentProps<typeof DialogPrimitive.Title>) {\n return <DialogPrimitive.Title className={cn('text-base font-semibold tracking-tight', className)} {...props} />\n}\n\nexport function DialogDescription({\n className,\n ...props\n}: ComponentProps<typeof DialogPrimitive.Description>) {\n return <DialogPrimitive.Description className={cn('text-sm text-muted-foreground', className)} {...props} />\n}\n"]}
1
+ {"version":3,"file":"dialog.js","sourceRoot":"","sources":["../../src/ui/dialog.tsx"],"names":[],"mappings":"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","sourcesContent":["import * as DialogPrimitive from '@radix-ui/react-dialog'\nimport { X } from 'lucide-react'\nimport type { ComponentProps } from 'react'\nimport { cn } from '../lib/utils'\n\nexport const Dialog = DialogPrimitive.Root\nexport const DialogTrigger = DialogPrimitive.Trigger\nexport const DialogClose = DialogPrimitive.Close\nexport const DialogPortal = DialogPrimitive.Portal\n\nexport function DialogOverlay({ className, ...props }: ComponentProps<typeof DialogPrimitive.Overlay>) {\n return (\n <DialogPrimitive.Overlay\n className={cn(\n 'fixed inset-0 z-50 bg-black/55 backdrop-blur-[2px] data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=closed]:animate-out data-[state=closed]:fade-out-0',\n className,\n )}\n {...props}\n />\n )\n}\n\nexport function DialogContent({\n className,\n children,\n ...props\n}: ComponentProps<typeof DialogPrimitive.Content>) {\n return (\n <DialogPortal>\n <DialogOverlay />\n <DialogPrimitive.Content\n className={cn(\n 'fixed left-1/2 top-1/2 z-50 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl border border-border bg-popover p-5 text-popover-foreground shadow-2xl',\n 'data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',\n 'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95',\n className,\n )}\n {...props}\n >\n {children}\n <DialogPrimitive.Close className=\"absolute right-3.5 top-3.5 rounded-sm text-muted-foreground opacity-70 transition-opacity hover:opacity-100 focus-visible:ring-2 focus-visible:ring-ring/60 outline-none\">\n <X className=\"size-4\" />\n <span className=\"sr-only\">Close</span>\n </DialogPrimitive.Close>\n </DialogPrimitive.Content>\n </DialogPortal>\n )\n}\n\n/**\n * Full-viewport, NON-dismissible modal content (EC-403: the blocking\n * session-expired re-auth overlay). Escape, backdrop clicks, and outside\n * interaction are all consumed — the only way out is the flow rendered inside\n * (or the owner clearing the controlled `open`). Radix still provides the\n * modal guarantees the blocking treatment relies on: focus trap, scroll lock,\n * `aria-modal`, and outside pointer-event lockout, so the studio beneath stays\n * fully inert while the React tree (and all unsaved editor state) lives on.\n *\n * Stacking (EC-392 discipline): mounted last and modal, it is the topmost\n * layer — its Escape handler `preventDefault`s, which the `ExpandableSurface`\n * window-bubble handler reads as \"handled above me\", so sheets open beneath\n * the blocker stay open instead of swallowing keys.\n *\n * `overlayClassName` is presentation-only (EC-437): it restyles the backdrop\n * element (the session-expired re-auth overlay swaps in the stage family's\n * translucent blurred treatment so the studio beneath stays visible). Every\n * blocking mechanic — the overlay element itself, the consumed dismissal\n * events, focus trap, scroll lock — is untouched by it.\n */\nexport function DialogBlockingContent({\n className,\n children,\n overlayClassName,\n ...props\n}: ComponentProps<typeof DialogPrimitive.Content> & { overlayClassName?: string }) {\n return (\n <DialogPortal>\n <DialogOverlay className={overlayClassName} />\n <DialogPrimitive.Content\n onEscapeKeyDown={(event) => event.preventDefault()}\n onPointerDownOutside={(event) => event.preventDefault()}\n onInteractOutside={(event) => event.preventDefault()}\n className={cn('fixed inset-0 z-50 overflow-y-auto outline-none', className)}\n {...props}\n >\n {children}\n </DialogPrimitive.Content>\n </DialogPortal>\n )\n}\n\nexport function DialogHeader({ className, ...props }: ComponentProps<'div'>) {\n return <div className={cn('flex flex-col gap-1.5', className)} {...props} />\n}\n\nexport function DialogFooter({ className, ...props }: ComponentProps<'div'>) {\n return <div className={cn('flex items-center justify-end gap-2 pt-2', className)} {...props} />\n}\n\nexport function DialogTitle({ className, ...props }: ComponentProps<typeof DialogPrimitive.Title>) {\n return <DialogPrimitive.Title className={cn('text-base font-semibold tracking-tight', className)} {...props} />\n}\n\nexport function DialogDescription({\n className,\n ...props\n}: ComponentProps<typeof DialogPrimitive.Description>) {\n return <DialogPrimitive.Description className={cn('text-sm text-muted-foreground', className)} {...props} />\n}\n"]}
@@ -1,4 +1,25 @@
1
1
  import type { LabelHTMLAttributes, ReactNode, SelectHTMLAttributes } from 'react';
2
+ /**
3
+ * EC-443 — THE nested-field anchor convention (one convention, used by both the
4
+ * stamping and the lookup — do not invent a second):
5
+ *
6
+ * data-ec-field-path="<field>(/<token>)*"
7
+ *
8
+ * Slash-joined tokens from the TOP-LEVEL field name down into the value —
9
+ * array indices are numeric tokens, object subfields their names — e.g.
10
+ * `links`, `links/3`, `links/3/badge`, `links/3/badge/text`. This mirrors the
11
+ * `values/…` reference-index path with the `values`/`localized/<locale>` prefix
12
+ * stripped: the locale axis is the studio's ambient locale switch, never part
13
+ * of the DOM anchor.
14
+ *
15
+ * Stamped by: the document form's top-level field rows (`detail-editor`'s
16
+ * `renderFieldRow` via the `fieldPath` prop below) and the object/repeater
17
+ * controls (`object-array-field`: item cards, table rows/cells, and nested
18
+ * subfield rows). Read by: `use-field-focus`'s deepest-available-anchor
19
+ * resolution (jump-to-field / `?field=` deep links) via the pure helpers in
20
+ * `components/cms/field-anchors.ts`.
21
+ */
22
+ export declare const FIELD_PATH_ATTR = "data-ec-field-path";
2
23
  /** A labelled form field group: label, control, and optional hint/error. */
3
24
  export interface FieldProps {
4
25
  label: ReactNode;
@@ -8,8 +29,10 @@ export interface FieldProps {
8
29
  required?: boolean;
9
30
  children: ReactNode;
10
31
  className?: string;
32
+ /** Stamps the {@link FIELD_PATH_ATTR} jump-to-field anchor (EC-443, see above). */
33
+ fieldPath?: string;
11
34
  }
12
- export declare function Field({ label, htmlFor, hint, error, required, children, className }: FieldProps): import("react").JSX.Element;
35
+ export declare function Field({ label, htmlFor, hint, error, required, children, className, fieldPath }: FieldProps): import("react").JSX.Element;
13
36
  export declare function FieldLabel({ className, ...props }: LabelHTMLAttributes<HTMLLabelElement>): import("react").JSX.Element;
14
37
  /** Native select styled to match Input — accessible and SSR-friendly. */
15
38
  export declare function Select({ className, children, ...props }: SelectHTMLAttributes<HTMLSelectElement>): import("react").JSX.Element;
package/dist/ui/field.js CHANGED
@@ -1,8 +1,29 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { ChevronDown } from 'lucide-react';
3
3
  import { cn } from '../lib/utils.js';
4
- export function Field({ label, htmlFor, hint, error, required, children, className }) {
5
- return (_jsxs("div", { className: cn('flex flex-col gap-1.5', className), children: [_jsxs(FieldLabel, { htmlFor: htmlFor, children: [label, required ? _jsx("span", { className: "text-destructive", children: " *" }) : null] }), children, error ? (_jsx("p", { className: "text-xs font-medium text-destructive", role: "alert", children: error })) : hint ? (_jsx("p", { className: "text-xs text-muted-foreground", children: hint })) : null] }));
4
+ /**
5
+ * EC-443 — THE nested-field anchor convention (one convention, used by both the
6
+ * stamping and the lookup — do not invent a second):
7
+ *
8
+ * data-ec-field-path="<field>(/<token>)*"
9
+ *
10
+ * Slash-joined tokens from the TOP-LEVEL field name down into the value —
11
+ * array indices are numeric tokens, object subfields their names — e.g.
12
+ * `links`, `links/3`, `links/3/badge`, `links/3/badge/text`. This mirrors the
13
+ * `values/…` reference-index path with the `values`/`localized/<locale>` prefix
14
+ * stripped: the locale axis is the studio's ambient locale switch, never part
15
+ * of the DOM anchor.
16
+ *
17
+ * Stamped by: the document form's top-level field rows (`detail-editor`'s
18
+ * `renderFieldRow` via the `fieldPath` prop below) and the object/repeater
19
+ * controls (`object-array-field`: item cards, table rows/cells, and nested
20
+ * subfield rows). Read by: `use-field-focus`'s deepest-available-anchor
21
+ * resolution (jump-to-field / `?field=` deep links) via the pure helpers in
22
+ * `components/cms/field-anchors.ts`.
23
+ */
24
+ export const FIELD_PATH_ATTR = 'data-ec-field-path';
25
+ export function Field({ label, htmlFor, hint, error, required, children, className, fieldPath }) {
26
+ return (_jsxs("div", { className: cn('flex flex-col gap-1.5', className), ...(fieldPath !== undefined ? { [FIELD_PATH_ATTR]: fieldPath } : {}), children: [_jsxs(FieldLabel, { htmlFor: htmlFor, children: [label, required ? _jsx("span", { className: "text-destructive", children: " *" }) : null] }), children, error ? (_jsx("p", { className: "text-xs font-medium text-destructive", role: "alert", children: error })) : hint ? (_jsx("p", { className: "text-xs text-muted-foreground", children: hint })) : null] }));
6
27
  }
7
28
  export function FieldLabel({ className, ...props }) {
8
29
  return (_jsx("label", { className: cn('text-xs font-medium text-foreground/90', className), ...props }));
@@ -1 +1 @@
1
- {"version":3,"file":"field.js","sourceRoot":"","sources":["../../src/ui/field.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAE1C,OAAO,EAAE,EAAE,EAAE,MAAM,cAAc,CAAA;AAajC,MAAM,UAAU,KAAK,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAc;IAC9F,OAAO,CACL,eAAK,SAAS,EAAE,EAAE,CAAC,uBAAuB,EAAE,SAAS,CAAC,aACpD,MAAC,UAAU,IAAC,OAAO,EAAE,OAAO,aACzB,KAAK,EACL,QAAQ,CAAC,CAAC,CAAC,eAAM,SAAS,EAAC,kBAAkB,mBAAU,CAAC,CAAC,CAAC,IAAI,IACpD,EACZ,QAAQ,EACR,KAAK,CAAC,CAAC,CAAC,CACP,YAAG,SAAS,EAAC,sCAAsC,EAAC,IAAI,EAAC,OAAO,YAC7D,KAAK,GACJ,CACL,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CACT,YAAG,SAAS,EAAC,+BAA+B,YAAE,IAAI,GAAK,CACxD,CAAC,CAAC,CAAC,IAAI,IACJ,CACP,CAAA;AACH,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAAyC;IACvF,OAAO,CACL,gBACE,SAAS,EAAE,EAAE,CAAC,wCAAwC,EAAE,SAAS,CAAC,KAC9D,KAAK,GACT,CACH,CAAA;AACH,CAAC;AAED,yEAAyE;AACzE,MAAM,UAAU,MAAM,CAAC,EAAE,SAAS,EAAE,QAAQ,EAAE,GAAG,KAAK,EAA2C;IAC/F,OAAO,CACL,eAAK,SAAS,EAAC,UAAU,aACvB,iBACE,SAAS,EAAE,EAAE,CACX,8IAA8I,EAC9I,wFAAwF,EACxF,iDAAiD,EACjD,SAAS,CACV,KACG,KAAK,YAER,QAAQ,GACF,EACT,KAAC,WAAW,IAAC,SAAS,EAAC,gGAAgG,GAAG,IACtH,CACP,CAAA;AACH,CAAC","sourcesContent":["import { ChevronDown } from 'lucide-react'\nimport type { LabelHTMLAttributes, ReactNode, SelectHTMLAttributes } from 'react'\nimport { cn } from '../lib/utils'\n\n/** A labelled form field group: label, control, and optional hint/error. */\nexport interface FieldProps {\n label: ReactNode\n htmlFor?: string\n hint?: ReactNode\n error?: ReactNode\n required?: boolean\n children: ReactNode\n className?: string\n}\n\nexport function Field({ label, htmlFor, hint, error, required, children, className }: FieldProps) {\n return (\n <div className={cn('flex flex-col gap-1.5', className)}>\n <FieldLabel htmlFor={htmlFor}>\n {label}\n {required ? <span className=\"text-destructive\"> *</span> : null}\n </FieldLabel>\n {children}\n {error ? (\n <p className=\"text-xs font-medium text-destructive\" role=\"alert\">\n {error}\n </p>\n ) : hint ? (\n <p className=\"text-xs text-muted-foreground\">{hint}</p>\n ) : null}\n </div>\n )\n}\n\nexport function FieldLabel({ className, ...props }: LabelHTMLAttributes<HTMLLabelElement>) {\n return (\n <label\n className={cn('text-xs font-medium text-foreground/90', className)}\n {...props}\n />\n )\n}\n\n/** Native select styled to match Input — accessible and SSR-friendly. */\nexport function Select({ className, children, ...props }: SelectHTMLAttributes<HTMLSelectElement>) {\n return (\n <div className=\"relative\">\n <select\n className={cn(\n 'flex h-8 w-full appearance-none rounded-md border border-input bg-background pl-2.5 pr-8 text-sm text-foreground shadow-sm transition-colors',\n 'focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/40 outline-none',\n 'disabled:cursor-not-allowed disabled:opacity-50',\n className,\n )}\n {...props}\n >\n {children}\n </select>\n <ChevronDown className=\"pointer-events-none absolute right-2.5 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground\" />\n </div>\n )\n}\n"]}
1
+ {"version":3,"file":"field.js","sourceRoot":"","sources":["../../src/ui/field.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAE1C,OAAO,EAAE,EAAE,EAAE,MAAM,cAAc,CAAA;AAEjC;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,oBAAoB,CAAA;AAenD,MAAM,UAAU,KAAK,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS,EAAc;IACzG,OAAO,CACL,eACE,SAAS,EAAE,EAAE,CAAC,uBAAuB,EAAE,SAAS,CAAC,KAC7C,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,eAAe,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,aAErE,MAAC,UAAU,IAAC,OAAO,EAAE,OAAO,aACzB,KAAK,EACL,QAAQ,CAAC,CAAC,CAAC,eAAM,SAAS,EAAC,kBAAkB,mBAAU,CAAC,CAAC,CAAC,IAAI,IACpD,EACZ,QAAQ,EACR,KAAK,CAAC,CAAC,CAAC,CACP,YAAG,SAAS,EAAC,sCAAsC,EAAC,IAAI,EAAC,OAAO,YAC7D,KAAK,GACJ,CACL,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CACT,YAAG,SAAS,EAAC,+BAA+B,YAAE,IAAI,GAAK,CACxD,CAAC,CAAC,CAAC,IAAI,IACJ,CACP,CAAA;AACH,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAAyC;IACvF,OAAO,CACL,gBACE,SAAS,EAAE,EAAE,CAAC,wCAAwC,EAAE,SAAS,CAAC,KAC9D,KAAK,GACT,CACH,CAAA;AACH,CAAC;AAED,yEAAyE;AACzE,MAAM,UAAU,MAAM,CAAC,EAAE,SAAS,EAAE,QAAQ,EAAE,GAAG,KAAK,EAA2C;IAC/F,OAAO,CACL,eAAK,SAAS,EAAC,UAAU,aACvB,iBACE,SAAS,EAAE,EAAE,CACX,8IAA8I,EAC9I,wFAAwF,EACxF,iDAAiD,EACjD,SAAS,CACV,KACG,KAAK,YAER,QAAQ,GACF,EACT,KAAC,WAAW,IAAC,SAAS,EAAC,gGAAgG,GAAG,IACtH,CACP,CAAA;AACH,CAAC","sourcesContent":["import { ChevronDown } from 'lucide-react'\nimport type { LabelHTMLAttributes, ReactNode, SelectHTMLAttributes } from 'react'\nimport { cn } from '../lib/utils'\n\n/**\n * EC-443 — THE nested-field anchor convention (one convention, used by both the\n * stamping and the lookup — do not invent a second):\n *\n * data-ec-field-path=\"<field>(/<token>)*\"\n *\n * Slash-joined tokens from the TOP-LEVEL field name down into the value —\n * array indices are numeric tokens, object subfields their names — e.g.\n * `links`, `links/3`, `links/3/badge`, `links/3/badge/text`. This mirrors the\n * `values/…` reference-index path with the `values`/`localized/<locale>` prefix\n * stripped: the locale axis is the studio's ambient locale switch, never part\n * of the DOM anchor.\n *\n * Stamped by: the document form's top-level field rows (`detail-editor`'s\n * `renderFieldRow` via the `fieldPath` prop below) and the object/repeater\n * controls (`object-array-field`: item cards, table rows/cells, and nested\n * subfield rows). Read by: `use-field-focus`'s deepest-available-anchor\n * resolution (jump-to-field / `?field=` deep links) via the pure helpers in\n * `components/cms/field-anchors.ts`.\n */\nexport const FIELD_PATH_ATTR = 'data-ec-field-path'\n\n/** A labelled form field group: label, control, and optional hint/error. */\nexport interface FieldProps {\n label: ReactNode\n htmlFor?: string\n hint?: ReactNode\n error?: ReactNode\n required?: boolean\n children: ReactNode\n className?: string\n /** Stamps the {@link FIELD_PATH_ATTR} jump-to-field anchor (EC-443, see above). */\n fieldPath?: string\n}\n\nexport function Field({ label, htmlFor, hint, error, required, children, className, fieldPath }: FieldProps) {\n return (\n <div\n className={cn('flex flex-col gap-1.5', className)}\n {...(fieldPath !== undefined ? { [FIELD_PATH_ATTR]: fieldPath } : {})}\n >\n <FieldLabel htmlFor={htmlFor}>\n {label}\n {required ? <span className=\"text-destructive\"> *</span> : null}\n </FieldLabel>\n {children}\n {error ? (\n <p className=\"text-xs font-medium text-destructive\" role=\"alert\">\n {error}\n </p>\n ) : hint ? (\n <p className=\"text-xs text-muted-foreground\">{hint}</p>\n ) : null}\n </div>\n )\n}\n\nexport function FieldLabel({ className, ...props }: LabelHTMLAttributes<HTMLLabelElement>) {\n return (\n <label\n className={cn('text-xs font-medium text-foreground/90', className)}\n {...props}\n />\n )\n}\n\n/** Native select styled to match Input — accessible and SSR-friendly. */\nexport function Select({ className, children, ...props }: SelectHTMLAttributes<HTMLSelectElement>) {\n return (\n <div className=\"relative\">\n <select\n className={cn(\n 'flex h-8 w-full appearance-none rounded-md border border-input bg-background pl-2.5 pr-8 text-sm text-foreground shadow-sm transition-colors',\n 'focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/40 outline-none',\n 'disabled:cursor-not-allowed disabled:opacity-50',\n className,\n )}\n {...props}\n >\n {children}\n </select>\n <ChevronDown className=\"pointer-events-none absolute right-2.5 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground\" />\n </div>\n )\n}\n"]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@elytracms/studio",
3
3
  "author": "finaldream <hi@finaldream.de>",
4
- "version": "0.0.69",
4
+ "version": "0.0.71",
5
5
  "private": false,
6
6
  "description": "The Elytra studio engine (UI + lib + routes + data-only Convex schema) as an importable package. Extraction staged in EXTRACTION.md (EC-223); Stages 1-2 = UI primitives + router-free lib leaves.",
7
7
  "license": "MIT",
@@ -32,10 +32,6 @@
32
32
  "types": "./dist/components/builder/canvas-stylesheet.d.ts",
33
33
  "default": "./dist/components/builder/canvas-stylesheet.js"
34
34
  },
35
- "./components/sections": {
36
- "types": "./dist/components/sections.d.ts",
37
- "default": "./dist/components/sections.js"
38
- },
39
35
  "./components/*": {
40
36
  "types": "./dist/components/*.d.ts",
41
37
  "default": "./dist/components/*.js"
@@ -108,6 +104,14 @@
108
104
  "types": "./dist/lib/*.d.ts",
109
105
  "default": "./dist/lib/*.js"
110
106
  },
107
+ "./previews/enumerate": {
108
+ "types": "./dist/previews/enumerate.d.ts",
109
+ "default": "./dist/previews/enumerate.js"
110
+ },
111
+ "./previews/harness": {
112
+ "types": "./dist/previews/harness.d.ts",
113
+ "default": "./dist/previews/harness.js"
114
+ },
111
115
  "./convex/*": "./convex/*.ts"
112
116
  },
113
117
  "files": [
@@ -146,7 +150,7 @@
146
150
  "lucide-react": "^1.23.0",
147
151
  "tailwind-merge": "^3.6.0",
148
152
  "zod": "^4.4.3",
149
- "@elytracms/core": "^0.0.69"
153
+ "@elytracms/core": "^0.0.71"
150
154
  },
151
155
  "peerDependencies": {
152
156
  "@tanstack/react-router": "^1.170.0",