@elytracms/studio 0.0.70 → 0.0.72
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/convex/cms.ts +8 -0
- package/convex/delivery.ts +12 -2
- package/convex/write-limits.ts +62 -0
- 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/assets/assets-collection-view-source.js +1 -0
- package/dist/components/assets/assets-collection-view-source.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/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 +9 -0
- package/dist/customization.js +8 -0
- package/dist/customization.js.map +1 -1
- 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 +38 -1
- package/dist/lib/i18n/messages.de.js.map +1 -1
- package/dist/lib/i18n/messages.en.d.ts +34 -0
- package/dist/lib/i18n/messages.en.js +41 -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/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/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
|
@@ -4,7 +4,11 @@ import type { OperationErrorCode } from '@elytracms/core/operations';
|
|
|
4
4
|
import type { ComponentManifest } from '@elytracms/core/component-registry';
|
|
5
5
|
/** Plain-language title for a graph validation issue code. */
|
|
6
6
|
export declare function graphIssueLabel(code: IssueCode): string;
|
|
7
|
-
/**
|
|
7
|
+
/**
|
|
8
|
+
* Plain-language title for a CMS validation issue code — localized against the
|
|
9
|
+
* active studio-UI locale (EC-443). A runtime-unknown code (impossible typewise;
|
|
10
|
+
* a cast could smuggle one) degrades to its humanized form, never the raw enum.
|
|
11
|
+
*/
|
|
8
12
|
export declare function cmsIssueLabel(code: CmsIssueCode): string;
|
|
9
13
|
/** Plain-language title for an operation failure code. */
|
|
10
14
|
export declare function operationErrorLabel(code: OperationErrorCode): string;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { resolveLabel } from '@elytracms/core/cms-core';
|
|
2
|
-
import { localizedLabel } from '@elytracms/studio/lib/i18n';
|
|
2
|
+
import { localizedLabel, translate } from '@elytracms/studio/lib/i18n';
|
|
3
3
|
import { humanizeName } from '../utils.js';
|
|
4
4
|
/**
|
|
5
5
|
* Persona-aware labels (EC-192 item 2, Milestone B promise).
|
|
@@ -35,33 +35,41 @@ const GRAPH_ISSUE_LABELS = {
|
|
|
35
35
|
'invalid-container-columns': 'Invalid columns',
|
|
36
36
|
'invalid-container-responsive': 'Invalid responsive setting',
|
|
37
37
|
};
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
'
|
|
48
|
-
'
|
|
49
|
-
'
|
|
50
|
-
'
|
|
51
|
-
'
|
|
52
|
-
'
|
|
53
|
-
'missing-
|
|
54
|
-
'
|
|
55
|
-
'
|
|
56
|
-
'
|
|
57
|
-
'
|
|
58
|
-
'
|
|
59
|
-
'
|
|
60
|
-
'
|
|
61
|
-
'
|
|
62
|
-
'
|
|
63
|
-
'
|
|
64
|
-
'unknown-
|
|
38
|
+
/**
|
|
39
|
+
* EC-443: the CMS issue titles live in the EC-241 message catalog
|
|
40
|
+
* (`cms.issue.<code>`, EN source + shipped de), so the Issues pane / for-review
|
|
41
|
+
* lane localize with the rest of the chrome. This map stays the exhaustive
|
|
42
|
+
* `Record` over the Zod enum — a new code still fails typecheck here until it
|
|
43
|
+
* gets an editor-facing catalog key (the persona boundary cannot silently
|
|
44
|
+
* regress); {@link cmsIssueLabel} resolves it through `translate`.
|
|
45
|
+
*/
|
|
46
|
+
const CMS_ISSUE_LABEL_KEYS = {
|
|
47
|
+
'duplicate-collection': 'cms.issue.duplicate-collection',
|
|
48
|
+
'duplicate-field': 'cms.issue.duplicate-field',
|
|
49
|
+
'unknown-collection': 'cms.issue.unknown-collection',
|
|
50
|
+
'invalid-collection-config': 'cms.issue.invalid-collection-config',
|
|
51
|
+
'invalid-field-config': 'cms.issue.invalid-field-config',
|
|
52
|
+
'reserved-field-name': 'cms.issue.reserved-field-name',
|
|
53
|
+
'missing-required-field': 'cms.issue.missing-required-field',
|
|
54
|
+
'invalid-field-value': 'cms.issue.invalid-field-value',
|
|
55
|
+
'unknown-relation-target': 'cms.issue.unknown-relation-target',
|
|
56
|
+
'unpublished-relation-target': 'cms.issue.unpublished-relation-target',
|
|
57
|
+
'cardinality-violation': 'cms.issue.cardinality-violation',
|
|
58
|
+
'unknown-asset': 'cms.issue.unknown-asset',
|
|
59
|
+
'duplicate-document': 'cms.issue.duplicate-document',
|
|
60
|
+
'unknown-locale': 'cms.issue.unknown-locale',
|
|
61
|
+
'missing-localized-value': 'cms.issue.missing-localized-value',
|
|
62
|
+
'route-conflict': 'cms.issue.route-conflict',
|
|
63
|
+
'redirect-loop': 'cms.issue.redirect-loop',
|
|
64
|
+
'unknown-route-target': 'cms.issue.unknown-route-target',
|
|
65
|
+
'hierarchy-cycle': 'cms.issue.hierarchy-cycle',
|
|
66
|
+
'document-nesting-depth': 'cms.issue.document-nesting-depth',
|
|
67
|
+
'singleton-document-drift': 'cms.issue.singleton-document-drift',
|
|
68
|
+
'duplicate-alias': 'cms.issue.duplicate-alias',
|
|
69
|
+
'unknown-alias': 'cms.issue.unknown-alias',
|
|
70
|
+
'unfilled-slot': 'cms.issue.unfilled-slot',
|
|
71
|
+
'custom-validation': 'cms.issue.custom-validation',
|
|
72
|
+
'unknown-version': 'cms.issue.unknown-version',
|
|
65
73
|
};
|
|
66
74
|
const OPERATION_ERROR_LABELS = {
|
|
67
75
|
validation: "Couldn't save",
|
|
@@ -94,9 +102,14 @@ const CONDITION_OPERATOR_LABELS = {
|
|
|
94
102
|
export function graphIssueLabel(code) {
|
|
95
103
|
return GRAPH_ISSUE_LABELS[code];
|
|
96
104
|
}
|
|
97
|
-
/**
|
|
105
|
+
/**
|
|
106
|
+
* Plain-language title for a CMS validation issue code — localized against the
|
|
107
|
+
* active studio-UI locale (EC-443). A runtime-unknown code (impossible typewise;
|
|
108
|
+
* a cast could smuggle one) degrades to its humanized form, never the raw enum.
|
|
109
|
+
*/
|
|
98
110
|
export function cmsIssueLabel(code) {
|
|
99
|
-
|
|
111
|
+
const key = CMS_ISSUE_LABEL_KEYS[code];
|
|
112
|
+
return key !== undefined ? translate(key) : humanizeName(code);
|
|
100
113
|
}
|
|
101
114
|
/** Plain-language title for an operation failure code. */
|
|
102
115
|
export function operationErrorLabel(code) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"labels.js","sourceRoot":"","sources":["../../../src/lib/persona/labels.ts"],"names":[],"mappings":"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","sourcesContent":["import type { IssueCode, ConditionOperator } from '@elytracms/core/project-graph'\nimport type { CmsIssueCode } from '@elytracms/core/cms-core'\nimport { resolveLabel } from '@elytracms/core/cms-core'\nimport type { OperationErrorCode } from '@elytracms/core/operations'\nimport type { ComponentManifest } from '@elytracms/core/component-registry'\nimport { localizedLabel } from '@elytracms/studio/lib/i18n'\nimport { humanizeName } from '../utils'\n\n/**\n * Persona-aware labels (EC-192 item 2, Milestone B promise).\n *\n * A non-technical editor must never see a manifest/binding/path-token/validation\n * identifier on — or one click off — the editor path. The engine speaks in stable\n * machine codes (`issueCodeSchema`, `cmsIssueCodeSchema`, `operationErrorCodeSchema`)\n * and machine ids (`base.Heading`, `r-blog-slug-en`); this module is the\n * single translation layer the editor-facing surfaces route through.\n *\n * The maps are exhaustive `Record<…Code, string>` over the Zod enums, so adding a\n * code in `@elytracms/core/project-graph`, `@elytracms/core/cms-core`, or `@elytracms/core/operations`\n * fails typecheck here until it is given an editor-facing label — the persona\n * boundary cannot silently regress. Engineers keep the raw code: every surface that\n * shows a label also carries the code in a `title=`/`data-*` attribute (inspectable\n * via hover/devtools, never rendered as visible copy).\n */\n\nconst GRAPH_ISSUE_LABELS: Record<IssueCode, string> = {\n 'invalid-component-name': 'Invalid component',\n 'unknown-component': 'Unknown component',\n 'duplicate-node-id': 'Duplicate item',\n 'unknown-prop': 'Unknown setting',\n 'invalid-prop': 'Invalid setting',\n 'binding-not-allowed': 'Data not allowed here',\n 'unresolved-binding': 'Missing data',\n 'missing-required-slot': 'Missing content',\n 'unknown-slot': 'Unknown content area',\n 'forbidden-component': 'Not allowed here',\n 'server-only-component': 'Not available in the editor',\n 'invalid-container-direction': 'Invalid layout',\n 'invalid-container-alignment': 'Invalid alignment',\n 'invalid-container-gap': 'Invalid spacing',\n 'invalid-container-columns': 'Invalid columns',\n 'invalid-container-responsive': 'Invalid responsive setting',\n}\n\nconst CMS_ISSUE_LABELS: Record<CmsIssueCode, string> = {\n 'duplicate-collection': 'Duplicate collection',\n 'duplicate-field': 'Duplicate field',\n 'unknown-collection': 'Unknown collection',\n 'invalid-collection-config': 'Invalid collection setup',\n 'invalid-field-config': 'Invalid field setup',\n 'reserved-field-name': 'Reserved field name',\n 'missing-required-field': 'Missing required field',\n 'invalid-field-value': 'Invalid value',\n 'unknown-relation-target': 'Missing reference',\n 'unpublished-relation-target': 'Unpublished reference',\n 'cardinality-violation': 'Wrong number of values',\n 'unknown-asset': 'Missing asset',\n 'duplicate-document': 'Duplicate entry',\n 'unknown-locale': 'Unknown language',\n 'missing-localized-value': 'Missing translation',\n 'route-conflict': 'Conflicting URL',\n 'redirect-loop': 'Redirect loop',\n 'unknown-route-target': 'Missing page',\n 'hierarchy-cycle': 'Page nesting loop',\n 'document-nesting-depth': 'Content nested too deeply',\n 'singleton-document-drift': 'More than one document',\n 'duplicate-alias': 'Slot used twice',\n 'unknown-alias': 'Unknown slot',\n 'unfilled-slot': 'Empty slot',\n 'custom-validation': 'Validation rule',\n 'unknown-version': 'Unknown version',\n}\n\nconst OPERATION_ERROR_LABELS: Record<OperationErrorCode, string> = {\n validation: \"Couldn't save\",\n 'not-found': 'Not found',\n conflict: 'Already exists',\n 'backend-unavailable': 'Connection problem',\n unauthorized: 'Not allowed',\n unknown: 'Something went wrong',\n}\n\nconst EXPORT_TARGET_LABELS: Record<string, string> = {\n nextjs: 'Next.js',\n}\n\n/**\n * Plain-language labels for condition operators. Exhaustive over\n * `conditionOperatorSchema` — a new operator fails typecheck here until it has an\n * editor-facing label. The raw operator (`eq`, `gte`, …) is a machine token an\n * editor must never read in the inspector.\n */\nconst CONDITION_OPERATOR_LABELS: Record<ConditionOperator, string> = {\n truthy: 'Is set',\n exists: 'Exists',\n eq: 'Equals',\n neq: 'Does not equal',\n gt: 'Greater than',\n gte: 'At least',\n lt: 'Less than',\n lte: 'At most',\n}\n\n/** Plain-language title for a graph validation issue code. */\nexport function graphIssueLabel(code: IssueCode): string {\n return GRAPH_ISSUE_LABELS[code]\n}\n\n/** Plain-language title for a CMS validation issue code. */\nexport function cmsIssueLabel(code: CmsIssueCode): string {\n return CMS_ISSUE_LABELS[code]\n}\n\n/** Plain-language title for an operation failure code. */\nexport function operationErrorLabel(code: OperationErrorCode): string {\n return OPERATION_ERROR_LABELS[code] ?? OPERATION_ERROR_LABELS.unknown\n}\n\n/** Friendly name for a project export target (`nextjs` → `Next.js`). */\nexport function exportTargetLabel(target: string): string {\n return EXPORT_TARGET_LABELS[target] ?? humanizeName(target)\n}\n\n/** Plain-language label for a condition operator (`gte` → `At least`). */\nexport function conditionOperatorLabel(operator: ConditionOperator): string {\n return CONDITION_OPERATOR_LABELS[operator]\n}\n\n/** The leaf of a component id, namespace stripped (`base.Heading` → `Heading`). */\nexport function componentLeafId(componentId: string): string {\n const dot = componentId.lastIndexOf('.')\n return dot === -1 ? componentId : componentId.slice(dot + 1)\n}\n\n/**\n * Strip the `base.`/`project.` namespace prefix from a component id for compact,\n * editor-facing labels (`base.Heading` → `Heading`). Unlike {@link componentLeafId}\n * it removes only those two known prefixes, so a dotted custom id keeps its shape.\n */\nexport function shortId(componentId: string): string {\n return componentId.replace('base.', '').replace('project.', '')\n}\n\n/**\n * The human display name of a component TYPE for editor-facing surfaces: the\n * manifest's authored `title` (EC-407: resolved through {@link resolveLabel}, so a\n * `{ en, de }` localized title renders in the active studio-UI locale) when present,\n * else the humanized leaf of its id (`base.Heading` → `Heading`,\n * `project.MarketingHero` → `Marketing hero`). Never the raw namespaced id.\n *\n * `locale` is optional: omit it (the render-site default) to resolve against the\n * active studio-UI locale non-reactively; pass it explicitly for pure/unit callers.\n */\nexport function componentDisplayName(\n componentId: string,\n manifest?: ComponentManifest | undefined,\n locale?: string,\n): string {\n const resolved =\n locale === undefined ? localizedLabel(manifest?.title) : (resolveLabel(manifest?.title, locale) ?? '')\n return resolved || humanizeName(componentLeafId(componentId))\n}\n"]}
|
|
1
|
+
{"version":3,"file":"labels.js","sourceRoot":"","sources":["../../../src/lib/persona/labels.ts"],"names":[],"mappings":"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","sourcesContent":["import type { IssueCode, ConditionOperator } from '@elytracms/core/project-graph'\nimport type { CmsIssueCode } from '@elytracms/core/cms-core'\nimport { resolveLabel } from '@elytracms/core/cms-core'\nimport type { OperationErrorCode } from '@elytracms/core/operations'\nimport type { ComponentManifest } from '@elytracms/core/component-registry'\nimport { localizedLabel, translate } from '@elytracms/studio/lib/i18n'\nimport { humanizeName } from '../utils'\n\n/**\n * Persona-aware labels (EC-192 item 2, Milestone B promise).\n *\n * A non-technical editor must never see a manifest/binding/path-token/validation\n * identifier on — or one click off — the editor path. The engine speaks in stable\n * machine codes (`issueCodeSchema`, `cmsIssueCodeSchema`, `operationErrorCodeSchema`)\n * and machine ids (`base.Heading`, `r-blog-slug-en`); this module is the\n * single translation layer the editor-facing surfaces route through.\n *\n * The maps are exhaustive `Record<…Code, string>` over the Zod enums, so adding a\n * code in `@elytracms/core/project-graph`, `@elytracms/core/cms-core`, or `@elytracms/core/operations`\n * fails typecheck here until it is given an editor-facing label — the persona\n * boundary cannot silently regress. Engineers keep the raw code: every surface that\n * shows a label also carries the code in a `title=`/`data-*` attribute (inspectable\n * via hover/devtools, never rendered as visible copy).\n */\n\nconst GRAPH_ISSUE_LABELS: Record<IssueCode, string> = {\n 'invalid-component-name': 'Invalid component',\n 'unknown-component': 'Unknown component',\n 'duplicate-node-id': 'Duplicate item',\n 'unknown-prop': 'Unknown setting',\n 'invalid-prop': 'Invalid setting',\n 'binding-not-allowed': 'Data not allowed here',\n 'unresolved-binding': 'Missing data',\n 'missing-required-slot': 'Missing content',\n 'unknown-slot': 'Unknown content area',\n 'forbidden-component': 'Not allowed here',\n 'server-only-component': 'Not available in the editor',\n 'invalid-container-direction': 'Invalid layout',\n 'invalid-container-alignment': 'Invalid alignment',\n 'invalid-container-gap': 'Invalid spacing',\n 'invalid-container-columns': 'Invalid columns',\n 'invalid-container-responsive': 'Invalid responsive setting',\n}\n\n/**\n * EC-443: the CMS issue titles live in the EC-241 message catalog\n * (`cms.issue.<code>`, EN source + shipped de), so the Issues pane / for-review\n * lane localize with the rest of the chrome. This map stays the exhaustive\n * `Record` over the Zod enum — a new code still fails typecheck here until it\n * gets an editor-facing catalog key (the persona boundary cannot silently\n * regress); {@link cmsIssueLabel} resolves it through `translate`.\n */\nconst CMS_ISSUE_LABEL_KEYS: Record<CmsIssueCode, string> = {\n 'duplicate-collection': 'cms.issue.duplicate-collection',\n 'duplicate-field': 'cms.issue.duplicate-field',\n 'unknown-collection': 'cms.issue.unknown-collection',\n 'invalid-collection-config': 'cms.issue.invalid-collection-config',\n 'invalid-field-config': 'cms.issue.invalid-field-config',\n 'reserved-field-name': 'cms.issue.reserved-field-name',\n 'missing-required-field': 'cms.issue.missing-required-field',\n 'invalid-field-value': 'cms.issue.invalid-field-value',\n 'unknown-relation-target': 'cms.issue.unknown-relation-target',\n 'unpublished-relation-target': 'cms.issue.unpublished-relation-target',\n 'cardinality-violation': 'cms.issue.cardinality-violation',\n 'unknown-asset': 'cms.issue.unknown-asset',\n 'duplicate-document': 'cms.issue.duplicate-document',\n 'unknown-locale': 'cms.issue.unknown-locale',\n 'missing-localized-value': 'cms.issue.missing-localized-value',\n 'route-conflict': 'cms.issue.route-conflict',\n 'redirect-loop': 'cms.issue.redirect-loop',\n 'unknown-route-target': 'cms.issue.unknown-route-target',\n 'hierarchy-cycle': 'cms.issue.hierarchy-cycle',\n 'document-nesting-depth': 'cms.issue.document-nesting-depth',\n 'singleton-document-drift': 'cms.issue.singleton-document-drift',\n 'duplicate-alias': 'cms.issue.duplicate-alias',\n 'unknown-alias': 'cms.issue.unknown-alias',\n 'unfilled-slot': 'cms.issue.unfilled-slot',\n 'custom-validation': 'cms.issue.custom-validation',\n 'unknown-version': 'cms.issue.unknown-version',\n}\n\nconst OPERATION_ERROR_LABELS: Record<OperationErrorCode, string> = {\n validation: \"Couldn't save\",\n 'not-found': 'Not found',\n conflict: 'Already exists',\n 'backend-unavailable': 'Connection problem',\n unauthorized: 'Not allowed',\n unknown: 'Something went wrong',\n}\n\nconst EXPORT_TARGET_LABELS: Record<string, string> = {\n nextjs: 'Next.js',\n}\n\n/**\n * Plain-language labels for condition operators. Exhaustive over\n * `conditionOperatorSchema` — a new operator fails typecheck here until it has an\n * editor-facing label. The raw operator (`eq`, `gte`, …) is a machine token an\n * editor must never read in the inspector.\n */\nconst CONDITION_OPERATOR_LABELS: Record<ConditionOperator, string> = {\n truthy: 'Is set',\n exists: 'Exists',\n eq: 'Equals',\n neq: 'Does not equal',\n gt: 'Greater than',\n gte: 'At least',\n lt: 'Less than',\n lte: 'At most',\n}\n\n/** Plain-language title for a graph validation issue code. */\nexport function graphIssueLabel(code: IssueCode): string {\n return GRAPH_ISSUE_LABELS[code]\n}\n\n/**\n * Plain-language title for a CMS validation issue code — localized against the\n * active studio-UI locale (EC-443). A runtime-unknown code (impossible typewise;\n * a cast could smuggle one) degrades to its humanized form, never the raw enum.\n */\nexport function cmsIssueLabel(code: CmsIssueCode): string {\n const key = CMS_ISSUE_LABEL_KEYS[code] as string | undefined\n return key !== undefined ? translate(key) : humanizeName(code)\n}\n\n/** Plain-language title for an operation failure code. */\nexport function operationErrorLabel(code: OperationErrorCode): string {\n return OPERATION_ERROR_LABELS[code] ?? OPERATION_ERROR_LABELS.unknown\n}\n\n/** Friendly name for a project export target (`nextjs` → `Next.js`). */\nexport function exportTargetLabel(target: string): string {\n return EXPORT_TARGET_LABELS[target] ?? humanizeName(target)\n}\n\n/** Plain-language label for a condition operator (`gte` → `At least`). */\nexport function conditionOperatorLabel(operator: ConditionOperator): string {\n return CONDITION_OPERATOR_LABELS[operator]\n}\n\n/** The leaf of a component id, namespace stripped (`base.Heading` → `Heading`). */\nexport function componentLeafId(componentId: string): string {\n const dot = componentId.lastIndexOf('.')\n return dot === -1 ? componentId : componentId.slice(dot + 1)\n}\n\n/**\n * Strip the `base.`/`project.` namespace prefix from a component id for compact,\n * editor-facing labels (`base.Heading` → `Heading`). Unlike {@link componentLeafId}\n * it removes only those two known prefixes, so a dotted custom id keeps its shape.\n */\nexport function shortId(componentId: string): string {\n return componentId.replace('base.', '').replace('project.', '')\n}\n\n/**\n * The human display name of a component TYPE for editor-facing surfaces: the\n * manifest's authored `title` (EC-407: resolved through {@link resolveLabel}, so a\n * `{ en, de }` localized title renders in the active studio-UI locale) when present,\n * else the humanized leaf of its id (`base.Heading` → `Heading`,\n * `project.MarketingHero` → `Marketing hero`). Never the raw namespaced id.\n *\n * `locale` is optional: omit it (the render-site default) to resolve against the\n * active studio-UI locale non-reactively; pass it explicitly for pure/unit callers.\n */\nexport function componentDisplayName(\n componentId: string,\n manifest?: ComponentManifest | undefined,\n locale?: string,\n): string {\n const resolved =\n locale === undefined ? localizedLabel(manifest?.title) : (resolveLabel(manifest?.title, locale) ?? '')\n return resolved || humanizeName(componentLeafId(componentId))\n}\n"]}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { ComponentFixture, ComponentManifest } from '@elytracms/core/component-registry';
|
|
2
|
+
import type { ComponentImplementations } from '@elytracms/core/runtime-renderer';
|
|
3
|
+
import type { StudioRegistry } from '../lib/registry/index.js';
|
|
4
|
+
/**
|
|
5
|
+
* EC-440 — the fixture-preview enumeration behind `elytra previews`. ONE
|
|
6
|
+
* source of truth for "which previews exist" shared by the CLI's node-side
|
|
7
|
+
* driver (hashing, staleness `--check`) and the browser render harness
|
|
8
|
+
* ({@link import('./harness').mountPreviewHarness}) — both derive the entry
|
|
9
|
+
* set from the consumer's `elytra.config.ts` default export through here, so
|
|
10
|
+
* generation, staleness checking, and rendering can never disagree.
|
|
11
|
+
*
|
|
12
|
+
* DATA-FREE by construction: this module reads the consumer CONFIG (registry
|
|
13
|
+
* manifests + implementations — code), never a persistence adapter, auth, or
|
|
14
|
+
* content data. The entry set mirrors the blocks panel exactly (EC-439): every
|
|
15
|
+
* canvas-eligible manifest, one entry per declared fixture, and a single
|
|
16
|
+
* implicit {@link DEFAULT_VARIANT} entry for fixture-less manifests. Base
|
|
17
|
+
* primitives are included — they are insertable tiles too. Layout-helper
|
|
18
|
+
* presets are NOT manifests and keep their placeholder treatment.
|
|
19
|
+
*/
|
|
20
|
+
/** One renderable preview: a component fixture plus its staleness-hash inputs. */
|
|
21
|
+
export interface PreviewEntrySpec {
|
|
22
|
+
componentId: string;
|
|
23
|
+
/** The fixture name, or {@link DEFAULT_VARIANT} for a fixture-less manifest. */
|
|
24
|
+
fixture: string;
|
|
25
|
+
/**
|
|
26
|
+
* The render-relevant inputs the CLI hashes (with the engine version) for
|
|
27
|
+
* staleness: the manifest's prop + slot shape (defaults live in the prop
|
|
28
|
+
* field-defs, so seeded renders are covered) and the declared fixture's own
|
|
29
|
+
* props/slotChildren (`fixture: null` for the implicit Default variant).
|
|
30
|
+
* Deliberately NOT hashed: the component implementation and the theme —
|
|
31
|
+
* drift there is caught by regeneration runs, not by the hash (the ratified
|
|
32
|
+
* EC-440 staleness split).
|
|
33
|
+
*/
|
|
34
|
+
hashInput: {
|
|
35
|
+
manifest: {
|
|
36
|
+
props: ComponentManifest['props'];
|
|
37
|
+
slots: ComponentManifest['slots'];
|
|
38
|
+
};
|
|
39
|
+
fixture: {
|
|
40
|
+
props: NonNullable<ComponentFixture['props']> | null;
|
|
41
|
+
slotChildren: NonNullable<ComponentFixture['slotChildren']> | null;
|
|
42
|
+
} | null;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The component set a config carries, as the harness registry input: the base
|
|
47
|
+
* primitives plus every resolved project's `components` entries, deduped by
|
|
48
|
+
* component id (first declaration wins — the generated-preview seam is keyed
|
|
49
|
+
* by component id alone, so one image serves every project that registers the
|
|
50
|
+
* id). A config that fails shape-resolution contributes no project components
|
|
51
|
+
* — the primitives still enumerate, and the CLI surfaces config problems
|
|
52
|
+
* through the studio/doctor, not here.
|
|
53
|
+
*/
|
|
54
|
+
export declare function previewComponentsFromConfig(config: unknown): {
|
|
55
|
+
manifests: ComponentManifest[];
|
|
56
|
+
implementations: ComponentImplementations;
|
|
57
|
+
};
|
|
58
|
+
/** The {@link StudioRegistry}-shaped registry the harness renders against. */
|
|
59
|
+
export declare function previewRegistryFromConfig(config: unknown): StudioRegistry;
|
|
60
|
+
/**
|
|
61
|
+
* Enumerate every generated-preview entry for a config: canvas-eligible
|
|
62
|
+
* manifests only (server-only components never render on the client canvas),
|
|
63
|
+
* one entry per declared fixture in declaration order, else one implicit
|
|
64
|
+
* {@link DEFAULT_VARIANT} entry. Components are sorted by id so the output —
|
|
65
|
+
* and everything the CLI derives from it (manifest JSON, file writes) — is
|
|
66
|
+
* deterministic regardless of registration order.
|
|
67
|
+
*/
|
|
68
|
+
export declare function previewEntriesForConfig(config: unknown): PreviewEntrySpec[];
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { ComponentRegistry, isCanvasEligible } from '@elytracms/core/component-registry';
|
|
2
|
+
import { basePrimitives, getImplementation } from '@elytracms/core/runtime-renderer';
|
|
3
|
+
import { resolveWorkspaceConfig } from '@elytracms/core/studio-core';
|
|
4
|
+
import { DEFAULT_VARIANT } from '../components/builder/blocks-model.js';
|
|
5
|
+
/**
|
|
6
|
+
* The component set a config carries, as the harness registry input: the base
|
|
7
|
+
* primitives plus every resolved project's `components` entries, deduped by
|
|
8
|
+
* component id (first declaration wins — the generated-preview seam is keyed
|
|
9
|
+
* by component id alone, so one image serves every project that registers the
|
|
10
|
+
* id). A config that fails shape-resolution contributes no project components
|
|
11
|
+
* — the primitives still enumerate, and the CLI surfaces config problems
|
|
12
|
+
* through the studio/doctor, not here.
|
|
13
|
+
*/
|
|
14
|
+
export function previewComponentsFromConfig(config) {
|
|
15
|
+
const base = basePrimitives();
|
|
16
|
+
const manifests = [...base.manifests];
|
|
17
|
+
const implementations = {};
|
|
18
|
+
for (const manifest of base.manifests) {
|
|
19
|
+
const implementation = getImplementation(base.implementations, manifest.id);
|
|
20
|
+
if (implementation)
|
|
21
|
+
implementations[manifest.id] = implementation;
|
|
22
|
+
}
|
|
23
|
+
const seen = new Set(manifests.map((m) => m.id));
|
|
24
|
+
const resolved = resolveWorkspaceConfig(config);
|
|
25
|
+
for (const project of resolved.projects) {
|
|
26
|
+
for (const entry of project.components ?? []) {
|
|
27
|
+
if (seen.has(entry.manifest.id))
|
|
28
|
+
continue;
|
|
29
|
+
seen.add(entry.manifest.id);
|
|
30
|
+
manifests.push(entry.manifest);
|
|
31
|
+
implementations[entry.manifest.id] = entry.implementation;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return { manifests, implementations };
|
|
35
|
+
}
|
|
36
|
+
/** The {@link StudioRegistry}-shaped registry the harness renders against. */
|
|
37
|
+
export function previewRegistryFromConfig(config) {
|
|
38
|
+
const { manifests, implementations } = previewComponentsFromConfig(config);
|
|
39
|
+
return { registry: new ComponentRegistry(manifests), implementations };
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Enumerate every generated-preview entry for a config: canvas-eligible
|
|
43
|
+
* manifests only (server-only components never render on the client canvas),
|
|
44
|
+
* one entry per declared fixture in declaration order, else one implicit
|
|
45
|
+
* {@link DEFAULT_VARIANT} entry. Components are sorted by id so the output —
|
|
46
|
+
* and everything the CLI derives from it (manifest JSON, file writes) — is
|
|
47
|
+
* deterministic regardless of registration order.
|
|
48
|
+
*/
|
|
49
|
+
export function previewEntriesForConfig(config) {
|
|
50
|
+
const { manifests } = previewComponentsFromConfig(config);
|
|
51
|
+
const entries = [];
|
|
52
|
+
for (const manifest of [...manifests].sort((a, b) => a.id.localeCompare(b.id))) {
|
|
53
|
+
if (!isCanvasEligible(manifest))
|
|
54
|
+
continue;
|
|
55
|
+
const manifestShape = { props: manifest.props, slots: manifest.slots };
|
|
56
|
+
if (manifest.fixtures && manifest.fixtures.length > 0) {
|
|
57
|
+
for (const fixture of manifest.fixtures) {
|
|
58
|
+
entries.push({
|
|
59
|
+
componentId: manifest.id,
|
|
60
|
+
fixture: fixture.name,
|
|
61
|
+
hashInput: {
|
|
62
|
+
manifest: manifestShape,
|
|
63
|
+
fixture: { props: fixture.props ?? null, slotChildren: fixture.slotChildren ?? null },
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
entries.push({
|
|
70
|
+
componentId: manifest.id,
|
|
71
|
+
fixture: DEFAULT_VARIANT,
|
|
72
|
+
hashInput: { manifest: manifestShape, fixture: null },
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return entries;
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=enumerate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"enumerate.js","sourceRoot":"","sources":["../../src/previews/enumerate.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ComponentType } from 'react'\nimport { ComponentRegistry, isCanvasEligible } from '@elytracms/core/component-registry'\nimport type { ComponentFixture, ComponentManifest } from '@elytracms/core/component-registry'\nimport { basePrimitives, getImplementation } from '@elytracms/core/runtime-renderer'\nimport type { ComponentImplementations } from '@elytracms/core/runtime-renderer'\nimport { resolveWorkspaceConfig } from '@elytracms/core/studio-core'\nimport { DEFAULT_VARIANT } from '../components/builder/blocks-model'\nimport type { StudioRegistry } from '../lib/registry'\n\n/**\n * EC-440 — the fixture-preview enumeration behind `elytra previews`. ONE\n * source of truth for \"which previews exist\" shared by the CLI's node-side\n * driver (hashing, staleness `--check`) and the browser render harness\n * ({@link import('./harness').mountPreviewHarness}) — both derive the entry\n * set from the consumer's `elytra.config.ts` default export through here, so\n * generation, staleness checking, and rendering can never disagree.\n *\n * DATA-FREE by construction: this module reads the consumer CONFIG (registry\n * manifests + implementations — code), never a persistence adapter, auth, or\n * content data. The entry set mirrors the blocks panel exactly (EC-439): every\n * canvas-eligible manifest, one entry per declared fixture, and a single\n * implicit {@link DEFAULT_VARIANT} entry for fixture-less manifests. Base\n * primitives are included — they are insertable tiles too. Layout-helper\n * presets are NOT manifests and keep their placeholder treatment.\n */\n\n/** One renderable preview: a component fixture plus its staleness-hash inputs. */\nexport interface PreviewEntrySpec {\n componentId: string\n /** The fixture name, or {@link DEFAULT_VARIANT} for a fixture-less manifest. */\n fixture: string\n /**\n * The render-relevant inputs the CLI hashes (with the engine version) for\n * staleness: the manifest's prop + slot shape (defaults live in the prop\n * field-defs, so seeded renders are covered) and the declared fixture's own\n * props/slotChildren (`fixture: null` for the implicit Default variant).\n * Deliberately NOT hashed: the component implementation and the theme —\n * drift there is caught by regeneration runs, not by the hash (the ratified\n * EC-440 staleness split).\n */\n hashInput: {\n manifest: { props: ComponentManifest['props']; slots: ComponentManifest['slots'] }\n fixture: {\n props: NonNullable<ComponentFixture['props']> | null\n slotChildren: NonNullable<ComponentFixture['slotChildren']> | null\n } | null\n }\n}\n\n/**\n * The component set a config carries, as the harness registry input: the base\n * primitives plus every resolved project's `components` entries, deduped by\n * component id (first declaration wins — the generated-preview seam is keyed\n * by component id alone, so one image serves every project that registers the\n * id). A config that fails shape-resolution contributes no project components\n * — the primitives still enumerate, and the CLI surfaces config problems\n * through the studio/doctor, not here.\n */\nexport function previewComponentsFromConfig(config: unknown): {\n manifests: ComponentManifest[]\n implementations: ComponentImplementations\n} {\n const base = basePrimitives()\n const manifests: ComponentManifest[] = [...base.manifests]\n const implementations: Record<string, ComponentType<Record<string, unknown>>> = {}\n for (const manifest of base.manifests) {\n const implementation = getImplementation(base.implementations, manifest.id)\n if (implementation) implementations[manifest.id] = implementation\n }\n const seen = new Set(manifests.map((m) => m.id))\n\n const resolved = resolveWorkspaceConfig(config)\n for (const project of resolved.projects) {\n for (const entry of project.components ?? []) {\n if (seen.has(entry.manifest.id)) continue\n seen.add(entry.manifest.id)\n manifests.push(entry.manifest)\n implementations[entry.manifest.id] = entry.implementation as ComponentType<Record<string, unknown>>\n }\n }\n return { manifests, implementations }\n}\n\n/** The {@link StudioRegistry}-shaped registry the harness renders against. */\nexport function previewRegistryFromConfig(config: unknown): StudioRegistry {\n const { manifests, implementations } = previewComponentsFromConfig(config)\n return { registry: new ComponentRegistry(manifests), implementations }\n}\n\n/**\n * Enumerate every generated-preview entry for a config: canvas-eligible\n * manifests only (server-only components never render on the client canvas),\n * one entry per declared fixture in declaration order, else one implicit\n * {@link DEFAULT_VARIANT} entry. Components are sorted by id so the output —\n * and everything the CLI derives from it (manifest JSON, file writes) — is\n * deterministic regardless of registration order.\n */\nexport function previewEntriesForConfig(config: unknown): PreviewEntrySpec[] {\n const { manifests } = previewComponentsFromConfig(config)\n const entries: PreviewEntrySpec[] = []\n for (const manifest of [...manifests].sort((a, b) => a.id.localeCompare(b.id))) {\n if (!isCanvasEligible(manifest)) continue\n const manifestShape = { props: manifest.props, slots: manifest.slots }\n if (manifest.fixtures && manifest.fixtures.length > 0) {\n for (const fixture of manifest.fixtures) {\n entries.push({\n componentId: manifest.id,\n fixture: fixture.name,\n hashInput: {\n manifest: manifestShape,\n fixture: { props: fixture.props ?? null, slotChildren: fixture.slotChildren ?? null },\n },\n })\n }\n } else {\n entries.push({\n componentId: manifest.id,\n fixture: DEFAULT_VARIANT,\n hashInput: { manifest: manifestShape, fixture: null },\n })\n }\n }\n return entries\n}\n"]}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EC-440 — the browser half of the `elytra previews` render harness. The CLI
|
|
3
|
+
* boots the consumer studio app's OWN Vite (programmatically, with a minimal
|
|
4
|
+
* inline config) and serves a page whose entry module does exactly three
|
|
5
|
+
* things: import the consumer's `elytra.config.ts` (the component registry —
|
|
6
|
+
* code), import the consumer THEME (the same standalone Tailwind entry the
|
|
7
|
+
* canvas iframe and delivery load, EC-414 — so preview == canvas == delivery
|
|
8
|
+
* by construction), and call {@link mountPreviewHarness}.
|
|
9
|
+
*
|
|
10
|
+
* DATA-FREE, deliberately (no new trust boundary): no persistence adapter, no
|
|
11
|
+
* auth, no content data, no env secrets — `createStudio` is never called. The
|
|
12
|
+
* registry is built directly from the config's `components` entries plus the
|
|
13
|
+
* base primitives ({@link previewRegistryFromConfig}), and bindings resolve to
|
|
14
|
+
* `undefined` (`previewContext`), so data-dependent components render their
|
|
15
|
+
* honest empty states.
|
|
16
|
+
*
|
|
17
|
+
* The driving CLI talks to the page through `window.__ELYTRA_PREVIEWS__`:
|
|
18
|
+
* `entries` (the same enumeration the CLI derives node-side — a handshake
|
|
19
|
+
* guard) and `render(componentId, fixture)`, which mounts ONE fixture subtree
|
|
20
|
+
* — the exact subtree the blocks panel inserts (`fixtureSubtree`, else the
|
|
21
|
+
* seeded manifest default, EC-439) — into the stage and resolves once the
|
|
22
|
+
* document has settled (fonts loaded, images complete, two animation frames).
|
|
23
|
+
* The CLI then screenshots the viewport.
|
|
24
|
+
*/
|
|
25
|
+
/** The id of the stage element fixtures render into (created when absent). */
|
|
26
|
+
export declare const PREVIEW_STAGE_ID = "elytra-preview-stage";
|
|
27
|
+
export type PreviewRenderResult = {
|
|
28
|
+
ok: true;
|
|
29
|
+
} | {
|
|
30
|
+
ok: false;
|
|
31
|
+
error: string;
|
|
32
|
+
};
|
|
33
|
+
export interface PreviewHarnessApi {
|
|
34
|
+
ready: true;
|
|
35
|
+
entries: Array<{
|
|
36
|
+
componentId: string;
|
|
37
|
+
fixture: string;
|
|
38
|
+
}>;
|
|
39
|
+
render: (componentId: string, fixture: string) => Promise<PreviewRenderResult>;
|
|
40
|
+
}
|
|
41
|
+
declare global {
|
|
42
|
+
interface Window {
|
|
43
|
+
__ELYTRA_PREVIEWS__?: PreviewHarnessApi;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Mount the harness: build the registry from the consumer config, expose the
|
|
48
|
+
* driver API on `window.__ELYTRA_PREVIEWS__`. Idempotent per document.
|
|
49
|
+
*/
|
|
50
|
+
export declare function mountPreviewHarness(config: unknown): PreviewHarnessApi;
|
|
@@ -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;
|