@elytracms/studio 0.0.69 → 0.0.71
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/account/self-account-view-source.js +1 -0
- package/dist/components/account/self-account-view-source.js.map +1 -1
- package/dist/components/app-views/studio-config.d.ts +22 -0
- package/dist/components/app-views/studio-config.js.map +1 -1
- package/dist/components/assets/assets-collection-view-source.js +1 -0
- package/dist/components/assets/assets-collection-view-source.js.map +1 -1
- package/dist/components/builder/blocks-model.d.ts +91 -29
- package/dist/components/builder/blocks-model.js +83 -54
- package/dist/components/builder/blocks-model.js.map +1 -1
- package/dist/components/builder/blocks-panel.d.ts +40 -12
- package/dist/components/builder/blocks-panel.js +88 -34
- package/dist/components/builder/blocks-panel.js.map +1 -1
- package/dist/components/builder/fixture-thumbnail.d.ts +13 -23
- package/dist/components/builder/fixture-thumbnail.js +27 -99
- package/dist/components/builder/fixture-thumbnail.js.map +1 -1
- package/dist/components/builder/spacing-handles.d.ts +14 -11
- package/dist/components/builder/spacing-handles.js +33 -57
- package/dist/components/builder/spacing-handles.js.map +1 -1
- package/dist/components/cms/collection-table.js +19 -2
- package/dist/components/cms/collection-table.js.map +1 -1
- package/dist/components/cms/detail-editor.js +73 -51
- package/dist/components/cms/detail-editor.js.map +1 -1
- package/dist/components/cms/duplicate-document.d.ts +45 -0
- package/dist/components/cms/duplicate-document.js +141 -0
- package/dist/components/cms/duplicate-document.js.map +1 -0
- package/dist/components/cms/field-anchors.d.ts +67 -0
- package/dist/components/cms/field-anchors.js +91 -0
- package/dist/components/cms/field-anchors.js.map +1 -0
- package/dist/components/cms/issue-copy.d.ts +45 -0
- package/dist/components/cms/issue-copy.js +89 -0
- package/dist/components/cms/issue-copy.js.map +1 -0
- package/dist/components/cms/issues-pane.d.ts +11 -2
- package/dist/components/cms/issues-pane.js +22 -4
- package/dist/components/cms/issues-pane.js.map +1 -1
- package/dist/components/cms/object-array-field.d.ts +2 -0
- package/dist/components/cms/object-array-field.js +75 -21
- package/dist/components/cms/object-array-field.js.map +1 -1
- package/dist/components/cms/row-actions.d.ts +10 -3
- package/dist/components/cms/row-actions.js +8 -0
- package/dist/components/cms/row-actions.js.map +1 -1
- package/dist/components/cms/use-field-focus.d.ts +25 -9
- package/dist/components/cms/use-field-focus.js +80 -15
- package/dist/components/cms/use-field-focus.js.map +1 -1
- package/dist/components/collection-view/cms-collection-view-source.js +1 -0
- package/dist/components/collection-view/cms-collection-view-source.js.map +1 -1
- package/dist/components/redirects/redirects-collection-view-source.js +1 -0
- package/dist/components/redirects/redirects-collection-view-source.js.map +1 -1
- package/dist/components/session-expired-overlay.d.ts +16 -0
- package/dist/components/session-expired-overlay.js +22 -6
- package/dist/components/session-expired-overlay.js.map +1 -1
- package/dist/components/sign-in-view.d.ts +10 -1
- package/dist/components/sign-in-view.js +14 -3
- package/dist/components/sign-in-view.js.map +1 -1
- package/dist/components/users/users-collection-view-source.js +1 -0
- package/dist/components/users/users-collection-view-source.js.map +1 -1
- package/dist/customization.d.ts +18 -1
- package/dist/customization.js +8 -0
- package/dist/customization.js.map +1 -1
- package/dist/lib/cms-views/view-model.d.ts +6 -6
- package/dist/lib/collection-view/collection-view-source.d.ts +8 -0
- package/dist/lib/collection-view/collection-view-source.js.map +1 -1
- package/dist/lib/i18n/messages.de.js +44 -1
- package/dist/lib/i18n/messages.de.js.map +1 -1
- package/dist/lib/i18n/messages.en.d.ts +39 -0
- package/dist/lib/i18n/messages.en.js +47 -1
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/persona/labels.d.ts +5 -1
- package/dist/lib/persona/labels.js +43 -30
- package/dist/lib/persona/labels.js.map +1 -1
- package/dist/lib/workspace/index.d.ts +10 -0
- package/dist/lib/workspace/index.js +11 -0
- package/dist/lib/workspace/index.js.map +1 -1
- package/dist/previews/enumerate.d.ts +68 -0
- package/dist/previews/enumerate.js +78 -0
- package/dist/previews/enumerate.js.map +1 -0
- package/dist/previews/harness.d.ts +50 -0
- package/dist/previews/harness.js +123 -0
- package/dist/previews/harness.js.map +1 -0
- package/dist/previews/manifest-format.d.ts +57 -0
- package/dist/previews/manifest-format.js +59 -0
- package/dist/previews/manifest-format.js.map +1 -0
- package/dist/ui/dialog.d.ts +9 -1
- package/dist/ui/dialog.js +8 -2
- package/dist/ui/dialog.js.map +1 -1
- package/dist/ui/field.d.ts +24 -1
- package/dist/ui/field.js +23 -2
- package/dist/ui/field.js.map +1 -1
- package/package.json +10 -6
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"self-account-view-source.js","sourceRoot":"","sources":["../../../src/components/account/self-account-view-source.ts"],"names":[],"mappings":"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","sourcesContent":["import { useCallback } from 'react'\nimport type { CmsDocument, CmsValidationIssue, DocumentRef, Locale, LocaleConfig } from '@elytracms/core/cms-core'\nimport type { UserRecord } from '@elytracms/core/persistence'\nimport { studioPath } from '@elytracms/studio/router'\nimport type {\n CollectionViewCapabilities,\n CollectionViewSource,\n} from '@elytracms/studio/lib/collection-view/collection-view-source'\nimport type { OperationError } from '@elytracms/core/operations'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { updateProfileErrorMessageKey } from '@elytracms/studio/lib/auth-errors'\nimport { useAuth } from '../auth-context'\nimport { selfAccountCollectionDef } from './self-account-schema'\nimport { normalizeOkResult, useRailViewSource } from '../rail-view-source'\n\n/** The account surface is not localized — one implicit locale keeps the lens honest. */\nconst ACCOUNT_LOCALE: Locale = 'en'\nconst ACCOUNT_LOCALE_CONFIG: LocaleConfig = { default: ACCOUNT_LOCALE, locales: [ACCOUNT_LOCALE] }\n\n/**\n * The capability profile of the account surface (EC-240 H-tail): a single self\n * record edited in place — no publish/versions/create/remove/import. The record\n * is the caller's own; there is nothing to delete or list.\n */\nexport const ACCOUNT_CAPABILITIES: CollectionViewCapabilities = {\n publish: false,\n versions: false,\n locales: false,\n inlineEdit: false,\n create: false,\n remove: false,\n bulk: false,\n liveUrl: false,\n hierarchy: false,\n importExport: false,\n}\n\n/** Everything in `values` that is NOT an editable identity field is display-only. */\nconst EDITABLE_IDENTITY = new Set(['name', 'avatar'])\n\n/** Project the self record into the account lens's `values`. */\nexport function selfToDocument(record: UserRecord): CmsDocument {\n return {\n collection: selfAccountCollectionDef.id,\n id: record.id,\n values: {\n name: record.name ?? '',\n avatar: record.avatar ?? '',\n email: record.email,\n role: record.roles[0] ?? 'viewer',\n },\n }\n}\n\n/**\n * Compute the identity diff (name / avatar) an edited account document carries —\n * the exact seam handed to `AuthAdapter.updateProfile`. Pure so the split-write is\n * unit-testable: only the fields the person changed are sent (an empty diff is a\n * no-op the caller can short-circuit). Never carries `email`/`role` — those are\n * read-only on this surface and are not auth-adapter-writable here.\n */\nexport function accountIdentityDiff(\n doc: CmsDocument,\n previous: UserRecord,\n): { name?: string; avatar?: string } {\n const values = doc.values ?? {}\n const diff: { name?: string; avatar?: string } = {}\n const nextName = typeof values.name === 'string' ? values.name : ''\n const nextAvatar = typeof values.avatar === 'string' ? values.avatar : ''\n if (nextName !== (previous.name ?? '')) diff.name = nextName\n if (nextAvatar !== (previous.avatar ?? '')) diff.avatar = nextAvatar\n return diff\n}\n\n/**\n * Assemble a {@link CollectionViewSource} for the **account-settings** surface\n * (EC-240 H-tail). Reuses the portable edit shell over the caller's OWN record —\n * name/avatar editable, email/role read-only (per the schema). The `save` routes\n * the identity diff to `AuthAdapter.updateProfile` (auth-owned) — NOT the\n * members-rail `users.save` — then refreshes the loaded record. `EDITABLE_IDENTITY`\n * marks which fields the save may write; everything else is display-only.\n */\nexport function useSelfAccountViewSource(input: {\n projectId: string\n self: UserRecord\n refresh: () => Promise<void>\n}): CollectionViewSource {\n const { projectId, self, refresh } = input\n const auth = useAuth()\n\n const documents = useCallback(() => [selfToDocument(self)], [self])\n const get = useCallback(\n (ref: DocumentRef): CmsDocument | undefined =>\n ref.id === self.id ? selfToDocument(self) : undefined,\n [self],\n )\n const titleOf = useCallback(() => self.name || self.email || self.id, [self])\n\n const save = useCallback(\n async (doc: CmsDocument): Promise<{ ok: true } | { ok: false; message: string }> => {\n const diff = accountIdentityDiff(doc, self)\n // No identity change (e.g. a read-only-only edit) → nothing to write.\n if (diff.name === undefined && diff.avatar === undefined) return { ok: true }\n const result = await auth.updateProfile(diff)\n if (!result.ok) {\n // EC-416: engine-classified failures localize through the message\n // catalog; an unrecognized shape renders the adapter's raw English\n // message — honest, never a stale mapping (the EC-397/EC-412 pattern).\n const errorKey = updateProfileErrorMessageKey(result)\n return { ok: false, message: errorKey ? translate(errorKey) : result.message }\n }\n // Reflect the new identity in the loaded record (session already refreshed).\n await refresh()\n return { ok: true }\n },\n [auth, self, refresh],\n )\n\n const noRemove = useCallback(\n async (): Promise<{ ok: true }> => ({ ok: true }),\n [],\n )\n const noValidation = useCallback((): CmsValidationIssue[] => [], [])\n const createUnsupported = useCallback((): DocumentRef => {\n throw new Error('the account surface has no create')\n }, [])\n\n return useRailViewSource({\n projectId,\n schema: selfAccountCollectionDef,\n capabilities: ACCOUNT_CAPABILITIES,\n localeConfig: ACCOUNT_LOCALE_CONFIG,\n documents,\n get,\n titleOf,\n // Editable-identity fields drive writability; the rest are read-only per schema.\n isFieldReadOnly: (field) => !EDITABLE_IDENTITY.has(field.name) || (field.form?.readOnly ?? false),\n detailPath: (id: string) => studioPath.user(id),\n listPath: studioPath.users(),\n validate: noValidation,\n create: createUnsupported,\n save,\n remove: noRemove,\n normalizeSaveResult: normalizeOkResult,\n normalizeRemoveResult: (): OperationError | null => null,\n })\n}\n"]}
|
|
1
|
+
{"version":3,"file":"self-account-view-source.js","sourceRoot":"","sources":["../../../src/components/account/self-account-view-source.ts"],"names":[],"mappings":"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","sourcesContent":["import { useCallback } from 'react'\nimport type { CmsDocument, CmsValidationIssue, DocumentRef, Locale, LocaleConfig } from '@elytracms/core/cms-core'\nimport type { UserRecord } from '@elytracms/core/persistence'\nimport { studioPath } from '@elytracms/studio/router'\nimport type {\n CollectionViewCapabilities,\n CollectionViewSource,\n} from '@elytracms/studio/lib/collection-view/collection-view-source'\nimport type { OperationError } from '@elytracms/core/operations'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { updateProfileErrorMessageKey } from '@elytracms/studio/lib/auth-errors'\nimport { useAuth } from '../auth-context'\nimport { selfAccountCollectionDef } from './self-account-schema'\nimport { normalizeOkResult, useRailViewSource } from '../rail-view-source'\n\n/** The account surface is not localized — one implicit locale keeps the lens honest. */\nconst ACCOUNT_LOCALE: Locale = 'en'\nconst ACCOUNT_LOCALE_CONFIG: LocaleConfig = { default: ACCOUNT_LOCALE, locales: [ACCOUNT_LOCALE] }\n\n/**\n * The capability profile of the account surface (EC-240 H-tail): a single self\n * record edited in place — no publish/versions/create/remove/import. The record\n * is the caller's own; there is nothing to delete or list.\n */\nexport const ACCOUNT_CAPABILITIES: CollectionViewCapabilities = {\n publish: false,\n versions: false,\n locales: false,\n inlineEdit: false,\n create: false,\n duplicate: false,\n remove: false,\n bulk: false,\n liveUrl: false,\n hierarchy: false,\n importExport: false,\n}\n\n/** Everything in `values` that is NOT an editable identity field is display-only. */\nconst EDITABLE_IDENTITY = new Set(['name', 'avatar'])\n\n/** Project the self record into the account lens's `values`. */\nexport function selfToDocument(record: UserRecord): CmsDocument {\n return {\n collection: selfAccountCollectionDef.id,\n id: record.id,\n values: {\n name: record.name ?? '',\n avatar: record.avatar ?? '',\n email: record.email,\n role: record.roles[0] ?? 'viewer',\n },\n }\n}\n\n/**\n * Compute the identity diff (name / avatar) an edited account document carries —\n * the exact seam handed to `AuthAdapter.updateProfile`. Pure so the split-write is\n * unit-testable: only the fields the person changed are sent (an empty diff is a\n * no-op the caller can short-circuit). Never carries `email`/`role` — those are\n * read-only on this surface and are not auth-adapter-writable here.\n */\nexport function accountIdentityDiff(\n doc: CmsDocument,\n previous: UserRecord,\n): { name?: string; avatar?: string } {\n const values = doc.values ?? {}\n const diff: { name?: string; avatar?: string } = {}\n const nextName = typeof values.name === 'string' ? values.name : ''\n const nextAvatar = typeof values.avatar === 'string' ? values.avatar : ''\n if (nextName !== (previous.name ?? '')) diff.name = nextName\n if (nextAvatar !== (previous.avatar ?? '')) diff.avatar = nextAvatar\n return diff\n}\n\n/**\n * Assemble a {@link CollectionViewSource} for the **account-settings** surface\n * (EC-240 H-tail). Reuses the portable edit shell over the caller's OWN record —\n * name/avatar editable, email/role read-only (per the schema). The `save` routes\n * the identity diff to `AuthAdapter.updateProfile` (auth-owned) — NOT the\n * members-rail `users.save` — then refreshes the loaded record. `EDITABLE_IDENTITY`\n * marks which fields the save may write; everything else is display-only.\n */\nexport function useSelfAccountViewSource(input: {\n projectId: string\n self: UserRecord\n refresh: () => Promise<void>\n}): CollectionViewSource {\n const { projectId, self, refresh } = input\n const auth = useAuth()\n\n const documents = useCallback(() => [selfToDocument(self)], [self])\n const get = useCallback(\n (ref: DocumentRef): CmsDocument | undefined =>\n ref.id === self.id ? selfToDocument(self) : undefined,\n [self],\n )\n const titleOf = useCallback(() => self.name || self.email || self.id, [self])\n\n const save = useCallback(\n async (doc: CmsDocument): Promise<{ ok: true } | { ok: false; message: string }> => {\n const diff = accountIdentityDiff(doc, self)\n // No identity change (e.g. a read-only-only edit) → nothing to write.\n if (diff.name === undefined && diff.avatar === undefined) return { ok: true }\n const result = await auth.updateProfile(diff)\n if (!result.ok) {\n // EC-416: engine-classified failures localize through the message\n // catalog; an unrecognized shape renders the adapter's raw English\n // message — honest, never a stale mapping (the EC-397/EC-412 pattern).\n const errorKey = updateProfileErrorMessageKey(result)\n return { ok: false, message: errorKey ? translate(errorKey) : result.message }\n }\n // Reflect the new identity in the loaded record (session already refreshed).\n await refresh()\n return { ok: true }\n },\n [auth, self, refresh],\n )\n\n const noRemove = useCallback(\n async (): Promise<{ ok: true }> => ({ ok: true }),\n [],\n )\n const noValidation = useCallback((): CmsValidationIssue[] => [], [])\n const createUnsupported = useCallback((): DocumentRef => {\n throw new Error('the account surface has no create')\n }, [])\n\n return useRailViewSource({\n projectId,\n schema: selfAccountCollectionDef,\n capabilities: ACCOUNT_CAPABILITIES,\n localeConfig: ACCOUNT_LOCALE_CONFIG,\n documents,\n get,\n titleOf,\n // Editable-identity fields drive writability; the rest are read-only per schema.\n isFieldReadOnly: (field) => !EDITABLE_IDENTITY.has(field.name) || (field.form?.readOnly ?? false),\n detailPath: (id: string) => studioPath.user(id),\n listPath: studioPath.users(),\n validate: noValidation,\n create: createUnsupported,\n save,\n remove: noRemove,\n normalizeSaveResult: normalizeOkResult,\n normalizeRemoveResult: (): OperationError | null => null,\n })\n}\n"]}
|
|
@@ -2,6 +2,7 @@ import type { ReactNode } from 'react';
|
|
|
2
2
|
import type { FieldDef } from '@elytracms/core/cms-core';
|
|
3
3
|
import type { StudioI18nConfig } from '@elytracms/studio/lib/i18n';
|
|
4
4
|
import type { StudioAiConfig } from '@elytracms/studio/lib/ai';
|
|
5
|
+
import type { GeneratedPreviewLookup } from '../builder/blocks-model.js';
|
|
5
6
|
import type { SidebarNode } from './module.js';
|
|
6
7
|
/**
|
|
7
8
|
* EC-236 — the studio customization config: the single, studio-only home for
|
|
@@ -68,6 +69,27 @@ export interface StudioConfig {
|
|
|
68
69
|
* separate key).
|
|
69
70
|
*/
|
|
70
71
|
canvas?: StudioCanvasTheme;
|
|
72
|
+
/**
|
|
73
|
+
* Insert-menu preview artifacts (EC-439, the seam EC-440 fills): where the
|
|
74
|
+
* studio's blocks panel finds GENERATED preview images for component
|
|
75
|
+
* fixtures. Studio-only (from `defineStudio`, never the delivery bundle) and
|
|
76
|
+
* fully optional — absent, tiles fall back from a curated
|
|
77
|
+
* `manifest.previewImage` straight to the icon+name placeholder. The EC-440
|
|
78
|
+
* CLI (`elytra previews`) renders fixtures headlessly against the consumer
|
|
79
|
+
* theme and writes a previews manifest; the consumer adapts that manifest
|
|
80
|
+
* into `previews.generated` here.
|
|
81
|
+
*/
|
|
82
|
+
previews?: StudioPreviewsConfig;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* The generated-preview seam (EC-439 AD-2). `generated` is a PURE lookup —
|
|
86
|
+
* component id + fixture name → image URL (`undefined` = no artifact). A
|
|
87
|
+
* fixture-less component's single implicit variant is looked up under the
|
|
88
|
+
* fixture name `'Default'`. Curated `manifest.previewImage` always wins over
|
|
89
|
+
* this lookup; the lookup wins over the placeholder.
|
|
90
|
+
*/
|
|
91
|
+
export interface StudioPreviewsConfig {
|
|
92
|
+
generated?: GeneratedPreviewLookup;
|
|
71
93
|
}
|
|
72
94
|
/**
|
|
73
95
|
* The canvas page-surface theme (EC-389). Both knobs target the preview iframe
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"studio-config.js","sourceRoot":"","sources":["../../../src/components/app-views/studio-config.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"studio-config.js","sourceRoot":"","sources":["../../../src/components/app-views/studio-config.ts"],"names":[],"mappings":"AAoIA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,UAAU,YAAY,CAAC,MAAoB;IAC/C,OAAO,MAAM,CAAA;AACf,CAAC","sourcesContent":["import type { ReactNode } from 'react'\nimport type { FieldDef } from '@elytracms/core/cms-core'\nimport type { StudioI18nConfig } from '@elytracms/studio/lib/i18n'\nimport type { StudioAiConfig } from '@elytracms/studio/lib/ai'\nimport type { GeneratedPreviewLookup } from '../builder/blocks-model'\nimport type { SidebarNode } from './module'\n\n/**\n * EC-236 — the studio customization config: the single, studio-only home for\n * everything a consuming repo customizes about the builder UI.\n *\n * This is the \"separate studio-only config solution\": it lives in the consumer's\n * `studio.config.tsx`, is `defineStudio({...})`-typed, and is passed to\n * `createStudio(config, studioConfig)` at the mount — NEVER mixed into the shared\n * `elytra.config.ts`, so the live React it carries (views, icons, field renderers,\n * future branding) never reaches the `frontend/` delivery bundle.\n *\n * The customization PRINCIPLE it embodies: customization is **code passed\n * directly**, not data referenced by a string key. Every field here holds live\n * values/components by reference — `sidebar` is built from `nav.*` view objects,\n * `fieldRenderers` maps a control name to a component. The only string indirection\n * in the whole surface is `fieldRenderers`' key, and ONLY because its other end\n * (`form.control` on a field) lives in the delivery-shared serializable schema and\n * cannot import studio code. This config grows by adding direct-value keys\n * (branding/logo/panels/theme), never by adding registries.\n */\nexport interface StudioConfig {\n /**\n * The section rail (EC-237) as an ordered list of `nav.*` nodes — views, groups,\n * and separators. Omitted → the studio derives the default rail from its built-in\n * views. A rail node carries its view object directly; there are no string ids to\n * keep in sync.\n */\n sidebar?: SidebarNode[]\n /**\n * Custom field renderers (EC-238) keyed by CONTROL name (`form.control`), each a\n * React component replacing the built-in document-editor control. A field opts in\n * per-field via `form: { control: '<name>' }` — so a renderer targets exactly the\n * fields that ask for it, not every field of a datatype. This is the one\n * boundary-crossing seam: the field names its control by string in the shared\n * schema, this map binds that name to a studio-only component.\n */\n fieldRenderers?: Record<string, CustomFieldRenderer>\n /**\n * Studio-UI localization (EC-241): the default locale + imported translation\n * catalogs. English is the built-in source; a consumer adds e.g.\n * `i18n: { locales: { de: deMessages } }` and the studio chrome renders in the\n * browser's preferred locale (no persisted selector in v1). Studio-only — never\n * reaches the delivery bundle. Schema labels localize separately, via a\n * `LocalizedString` on the field/collection def.\n */\n i18n?: StudioI18nConfig\n /**\n * AI platform (EC-331): provider, project knowledge, and soft limits. The block's\n * PRESENCE enables the AI feature; its ABSENCE means the feature is fully absent —\n * no AI UI and no AI dependencies on the critical path (the runtime lives behind a\n * lazy boundary in `@elytracms/studio/lib/ai`, only reached when configured).\n * `enabled: false` is a temporary kill-switch. Studio-only live code (holds the\n * provider adapter + knowledge functions) — never reaches the delivery bundle.\n * Validated at load by `resolveAiConfig` (capability gate: tool calling + structured\n * output required, fail-loud).\n */\n ai?: StudioAiConfig\n /**\n * Canvas page-surface theming (EC-389): make the preview iframe wear the SITE's\n * chrome. Direct values (no registries): body classes + raw CSS, injected LAST\n * into the canvas iframe — after the studio defaults and an instantiated starter\n * kit's `tokensCss` — so consumer values win the cascade. This themes the canvas\n * PAGE surface only, never the studio chrome (branding/logo theming is a future,\n * separate key).\n */\n canvas?: StudioCanvasTheme\n /**\n * Insert-menu preview artifacts (EC-439, the seam EC-440 fills): where the\n * studio's blocks panel finds GENERATED preview images for component\n * fixtures. Studio-only (from `defineStudio`, never the delivery bundle) and\n * fully optional — absent, tiles fall back from a curated\n * `manifest.previewImage` straight to the icon+name placeholder. The EC-440\n * CLI (`elytra previews`) renders fixtures headlessly against the consumer\n * theme and writes a previews manifest; the consumer adapts that manifest\n * into `previews.generated` here.\n */\n previews?: StudioPreviewsConfig\n}\n\n/**\n * The generated-preview seam (EC-439 AD-2). `generated` is a PURE lookup —\n * component id + fixture name → image URL (`undefined` = no artifact). A\n * fixture-less component's single implicit variant is looked up under the\n * fixture name `'Default'`. Curated `manifest.previewImage` always wins over\n * this lookup; the lookup wins over the placeholder.\n */\nexport interface StudioPreviewsConfig {\n generated?: GeneratedPreviewLookup\n}\n\n/**\n * The canvas page-surface theme (EC-389). Both knobs target the preview iframe\n * document — the isolated page the builder renders into — so a site whose theme\n * styles `body` (background, color, font) previews exactly as it delivers.\n */\nexport interface StudioCanvasTheme {\n /**\n * REPLACES the studio's default canvas body classes\n * (`bg-background text-foreground font-sans`). The default's utility classes\n * out-cascade a site theme's `@layer base body` rules (Tailwind layers lose to\n * later layers/un-layered styles), which is exactly how a site's page background\n * used to paint white in the canvas — pass `''` to drop the default entirely and\n * let the site's own `body` rules (from the canvas stylesheet) take over, or\n * pass your own classes. Omitted → the studio default stays.\n */\n bodyClassName?: string\n /**\n * Raw CSS injected into the canvas iframe as the LAST style element — after the\n * canvas stylesheet, the studio editing chrome, and a kit's `tokensCss` — so it\n * wins the cascade over all of them (an un-layered `body { background: … }` or a\n * `:root { --token: … }` override both work). The kit seam keeps working: a kit\n * provides, this overrides.\n */\n tokensCss?: string\n}\n\n/** The props a custom field renderer receives — identical to the built-in control. */\nexport interface CustomFieldRendererProps {\n field: FieldDef\n value: unknown\n onChange: (value: unknown) => void\n readOnly: boolean\n id: string\n}\nexport type CustomFieldRenderer = (props: CustomFieldRendererProps) => ReactNode\n\n/**\n * Typed identity helper for `studio.config.tsx` — keeps the config declarative\n * (like {@link import('@elytracms/core/studio-core').defineElytraConfig}). No\n * validation here; the studio resolves + validates the sidebar and field renderers\n * through the EC-236 seam at mount, surfacing every issue visibly (never throws).\n *\n * @example\n * ```tsx\n * // studio.config.tsx\n * import { defineStudio, nav } from '@elytracms/studio/customization'\n * import { ColorField } from './fields/color'\n *\n * export default defineStudio({\n * sidebar: [\n * nav.cms({ label: 'Content' }),\n * nav.group('Catalog', [\n * nav.cms({ label: 'Catalog', collections: ['product', 'productCategory'] }),\n * nav.cms({ label: 'Categories', collection: 'productCategory' }),\n * ]),\n * nav.separator(),\n * nav.assets(),\n * nav.settings(),\n * ],\n * fieldRenderers: { color: ColorField },\n * })\n * ```\n */\nexport function defineStudio(config: StudioConfig): StudioConfig {\n return config\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"assets-collection-view-source.js","sourceRoot":"","sources":["../../../src/components/assets/assets-collection-view-source.ts"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useMemo } from 'react'\nimport type { CmsDocument, CollectionDef, DocumentRef, Locale, LocaleConfig } from '@elytracms/core/cms-core'\nimport { createCollectionRegistry, validateDocument } from '@elytracms/core/cms-core'\nimport type { AssetRecord, ReferenceTarget } from '@elytracms/core/persistence'\nimport type { OperationError } from '@elytracms/core/operations'\nimport { studioPath } from '@elytracms/studio/router'\nimport type { DeleteAssetResult } from '@elytracms/studio/lib/assets'\nimport type { CollectionViewSource, CollectionViewCapabilities } from '@elytracms/studio/lib/collection-view/collection-view-source'\nimport { useAssets } from './assets-store'\nimport { SYSTEM_ASSET_FIELDS } from './asset-metadata'\nimport { normalizeOkResult, useRailViewSource } from '../rail-view-source'\n\n/** The assets rail is not localized in v1 (EC-301: asset metadata is non-localized). */\nconst ASSETS_LOCALE: Locale = 'en'\nconst ASSETS_LOCALE_CONFIG: LocaleConfig = {\n default: ASSETS_LOCALE,\n locales: [ASSETS_LOCALE],\n}\n\n/**\n * The capability profile of the assets rail (EC-319, EC-433). Hoisted to a\n * module-level constant (no prop/state dependency) so it's both the value the\n * hook returns AND independently unit-testable.\n *\n * `create` is off — a new asset comes from an upload, never a blank draft.\n * `remove` + `bulk` are ON since EC-433: the list's row/bulk delete used to be\n * off deliberately because the generic `ReferenceConfirmDialog` enumerated\n * inbound references as `{ type: 'document' }` targets — the wrong index shape\n * for an asset — which would have shown \"not referenced\" for an asset that IS\n * referenced. That pre-flight now queries the shape THIS source declares via\n * {@link assetReferenceTarget} (`{ type: 'asset' }`, the same lookup the\n * detail surface runs), and the confirmed delete dispatches the SAME checked\n * store delete the detail surface uses (`deleteAsset`: original + EC-400 thumb\n * blobs, then the record — EC-420 lqips ride on the record). Nothing else\n * applies to assets: no publish/versions/locales/inline-edit/live-URL/\n * hierarchy/import-export.\n */\nexport const ASSET_COLLECTION_CAPABILITIES: CollectionViewCapabilities = {\n publish: false,\n versions: false,\n locales: false,\n inlineEdit: false,\n create: false,\n remove: true,\n bulk: true,\n liveUrl: false,\n hierarchy: false,\n importExport: false,\n adminOnly: false,\n}\n\n/** Read one of the record's fixed, upload-managed columns by field name. */\nfunction systemFieldValue(record: AssetRecord, name: string): unknown {\n switch (name) {\n case 'filename':\n return record.filename\n case 'url':\n return record.url\n case 'contentType':\n return record.contentType\n case 'byteSize':\n return record.byteSize\n case 'width':\n return record.width\n case 'height':\n return record.height\n case 'storageKey':\n return record.storageKey\n case 'title':\n return record.title\n default:\n return undefined\n }\n}\n\n/**\n * Project a stored asset record into the lens's `values` shape (EC-319), reading\n * only the schema's DECLARED fields — the same \"schema is a lens\" projection the\n * redirects/users rails use. A system column (`filename`, `url`, dimensions, …)\n * reads straight off the record; everything else — the built-in `alt`, a\n * project's own `copyright`/`credit`, or an EC-311 additive field — round-trips\n * through the record's open `meta` bag (EC-301). `alt` is schema-driven but ALSO\n * has a legacy record column: the meta value wins, falling back to the column\n * (matches delivery's `meta.alt ?? record.alt` read and the image field's\n * options dialog).\n */\nexport function assetToDocument(collection: CollectionDef, record: AssetRecord): CmsDocument {\n const values: Record<string, unknown> = {}\n for (const field of collection.fields) {\n if (field.name === 'alt') {\n const metaAlt = typeof record.meta?.alt === 'string' ? record.meta.alt : undefined\n values.alt = metaAlt ?? record.alt\n continue\n }\n values[field.name] = SYSTEM_ASSET_FIELDS.has(field.name)\n ? systemFieldValue(record, field.name)\n : record.meta?.[field.name]\n }\n return { collection: collection.id, id: record.id, values }\n}\n\n/**\n * The inverse of {@link assetToDocument}: the schema-driven meta bag a save\n * writes back — every declared field OUTSIDE the system set (EC-301/EC-311),\n * keyed exactly as the record stores it. System columns are upload-managed and\n * never written here.\n */\nexport function documentToAssetMetaPatch(\n collection: CollectionDef,\n doc: CmsDocument,\n): Record<string, unknown> {\n const values = doc.values ?? {}\n const meta: Record<string, unknown> = {}\n for (const field of collection.fields) {\n if (SYSTEM_ASSET_FIELDS.has(field.name)) continue\n if (field.name in values) meta[field.name] = values[field.name]\n }\n return meta\n}\n\n/** `create` when `capabilities.create === false` — never reached from the UI\n * (list/detail never call it); an explicit throw rather than a silent no-op. */\nfunction createUnsupported(): DocumentRef {\n throw new Error('assets are created by upload, not the assets rail')\n}\n\n/** Assets' `remove()` returns an `outcome`-discriminated {@link DeleteAssetResult}\n * (not the `{ ok }` shape users/redirects use, EC-327). `'error'` surfaces as-is;\n * `'blocked'` (the unconfirmed usage gate fired — nothing was removed) surfaces\n * as an explicit failure too (EC-433): a delete that left the asset in place\n * must NEVER look like success. It is unreachable from this rail's UI — every\n * delete path confirms through the reference dialog and dispatches with\n * `confirmInUse` — but stays honest for any direct caller. `'not-found'` maps\n * to success (idempotent delete: the desired end state — gone — holds). */\nexport function normalizeAssetRemoveResult(result: DeleteAssetResult): OperationError | null {\n if (result.outcome === 'error') return { code: 'unknown', message: result.message }\n if (result.outcome === 'blocked') {\n return {\n code: 'unknown',\n message: `Asset is referenced in ${result.usageCount} ${result.usageCount === 1 ? 'place' : 'places'} and was not deleted.`,\n }\n }\n return null\n}\n\n/**\n * How an asset row is addressed in the EC-155 reference index (EC-433):\n * `{ type: 'asset' }`, never the document shape — so the delete confirmation's\n * inbound pre-flight counts the SAME references `loadInboundAssetReferenceViews`\n * (the detail surface's check) does.\n */\nexport function assetReferenceTarget(ref: DocumentRef): ReferenceTarget {\n return { type: 'asset', id: ref.id }\n}\n\n/**\n * Assemble a {@link CollectionViewSource} for the **assets rail** (EC-319) — the\n * portable list surface over the existing asset library store instead of a\n * bespoke listing. `schema` is the DEPLOYMENT-resolved `kind:'asset'` collection\n * (EC-311: a project's own declared fields, or a mount's `nav.assets({ fields })`\n * override merged over it — see `resolveAssetsSchema` in `app-views/default-modules.tsx`),\n * never a hardcoded engine default — so a project's `copyright`/`credit`/… fields\n * (and any EC-311 override) drive the list's columns/filters exactly as they\n * drive the image field's options dialog.\n *\n * Must be mounted under an `<AssetsProvider>` (for {@link useAssets}) — already\n * ambient at the project level (EC-195), so callers rarely mount their own.\n *\n * The shared scaffold (`useRailViewSource`, EC-327) owns `viewState`/`lastError`\n * plumbing and the assembled source; this hook supplies only the asset-store\n * reads/writes, the capability profile, and the projections above.\n */\nexport function useAssetsCollectionViewSource(\n projectId: string,\n schema: CollectionDef,\n): CollectionViewSource {\n const store = useAssets()\n const registry = useMemo(() => createCollectionRegistry([schema]), [schema])\n\n const documents = useCallback(\n () => store.assets.map((record) => assetToDocument(schema, record)),\n [store.assets, schema],\n )\n const get = useCallback(\n (ref: DocumentRef): CmsDocument | undefined => {\n const record = store.assets.find((a) => a.id === ref.id)\n return record ? assetToDocument(schema, record) : undefined\n },\n [store.assets, schema],\n )\n const titleOf = useCallback(\n (ref: DocumentRef): string => {\n const record = store.assets.find((a) => a.id === ref.id)\n return record?.title ?? record?.filename ?? ref.id\n },\n [store.assets],\n )\n const validate = useCallback(\n (doc: CmsDocument) => validateDocument(registry, doc, { localeConfig: ASSETS_LOCALE_CONFIG }),\n [registry],\n )\n const save = useCallback(\n (doc: CmsDocument) => store.updateMetadata(doc.id, { meta: documentToAssetMetaPatch(schema, doc) }),\n [schema, store],\n )\n // The unconfirmed delete (the store's usage gate still applies — a referenced\n // asset comes back `blocked`, surfaced via `lastError`). The UI's delete paths\n // never land here: row/bulk deletes confirm through the reference dialog and\n // dispatch `bulkRemove` below.\n const remove = useCallback((ref: DocumentRef) => store.remove(ref.id), [store])\n // The CONFIRMED delete the bulk/row path dispatches (EC-433): the reference\n // dialog has already enumerated inbound usages (asset-shaped, see\n // `assetReferenceTarget`), so this passes `confirmInUse` exactly like the\n // detail surface's confirmed delete — same blob + thumb + cache handling.\n const bulkRemove = useCallback(\n (ref: DocumentRef) => store.remove(ref.id, { confirmInUse: true }),\n [store],\n )\n\n return useRailViewSource({\n projectId,\n schema,\n capabilities: ASSET_COLLECTION_CAPABILITIES,\n localeConfig: ASSETS_LOCALE_CONFIG,\n documents,\n get,\n titleOf,\n // No `referenceCandidates`/`collections`/`isFieldReadOnly` override: assets carry\n // no relation/asset fields of their own (v1) — nothing cross-collection to\n // resolve — and read-only follows the factory's default `form.readOnly` check.\n detailPath: (id: string) => studioPath.asset(id),\n listPath: studioPath.assets(),\n // EC-433: the delete confirmation must enumerate ASSET-shaped inbound\n // references — the document-shaped default would report \"not referenced\"\n // for a referenced asset.\n referenceTarget: assetReferenceTarget,\n validate,\n create: createUnsupported,\n save,\n remove,\n bulkRemove,\n normalizeSaveResult: normalizeOkResult,\n normalizeRemoveResult: normalizeAssetRemoveResult,\n })\n}\n"]}
|
|
1
|
+
{"version":3,"file":"assets-collection-view-source.js","sourceRoot":"","sources":["../../../src/components/assets/assets-collection-view-source.ts"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useMemo } from 'react'\nimport type { CmsDocument, CollectionDef, DocumentRef, Locale, LocaleConfig } from '@elytracms/core/cms-core'\nimport { createCollectionRegistry, validateDocument } from '@elytracms/core/cms-core'\nimport type { AssetRecord, ReferenceTarget } from '@elytracms/core/persistence'\nimport type { OperationError } from '@elytracms/core/operations'\nimport { studioPath } from '@elytracms/studio/router'\nimport type { DeleteAssetResult } from '@elytracms/studio/lib/assets'\nimport type { CollectionViewSource, CollectionViewCapabilities } from '@elytracms/studio/lib/collection-view/collection-view-source'\nimport { useAssets } from './assets-store'\nimport { SYSTEM_ASSET_FIELDS } from './asset-metadata'\nimport { normalizeOkResult, useRailViewSource } from '../rail-view-source'\n\n/** The assets rail is not localized in v1 (EC-301: asset metadata is non-localized). */\nconst ASSETS_LOCALE: Locale = 'en'\nconst ASSETS_LOCALE_CONFIG: LocaleConfig = {\n default: ASSETS_LOCALE,\n locales: [ASSETS_LOCALE],\n}\n\n/**\n * The capability profile of the assets rail (EC-319, EC-433). Hoisted to a\n * module-level constant (no prop/state dependency) so it's both the value the\n * hook returns AND independently unit-testable.\n *\n * `create` is off — a new asset comes from an upload, never a blank draft.\n * `remove` + `bulk` are ON since EC-433: the list's row/bulk delete used to be\n * off deliberately because the generic `ReferenceConfirmDialog` enumerated\n * inbound references as `{ type: 'document' }` targets — the wrong index shape\n * for an asset — which would have shown \"not referenced\" for an asset that IS\n * referenced. That pre-flight now queries the shape THIS source declares via\n * {@link assetReferenceTarget} (`{ type: 'asset' }`, the same lookup the\n * detail surface runs), and the confirmed delete dispatches the SAME checked\n * store delete the detail surface uses (`deleteAsset`: original + EC-400 thumb\n * blobs, then the record — EC-420 lqips ride on the record). Nothing else\n * applies to assets: no publish/versions/locales/inline-edit/live-URL/\n * hierarchy/import-export.\n */\nexport const ASSET_COLLECTION_CAPABILITIES: CollectionViewCapabilities = {\n publish: false,\n versions: false,\n locales: false,\n inlineEdit: false,\n create: false,\n duplicate: false, // EC-442: blob duplication is a different feature, out of scope\n remove: true,\n bulk: true,\n liveUrl: false,\n hierarchy: false,\n importExport: false,\n adminOnly: false,\n}\n\n/** Read one of the record's fixed, upload-managed columns by field name. */\nfunction systemFieldValue(record: AssetRecord, name: string): unknown {\n switch (name) {\n case 'filename':\n return record.filename\n case 'url':\n return record.url\n case 'contentType':\n return record.contentType\n case 'byteSize':\n return record.byteSize\n case 'width':\n return record.width\n case 'height':\n return record.height\n case 'storageKey':\n return record.storageKey\n case 'title':\n return record.title\n default:\n return undefined\n }\n}\n\n/**\n * Project a stored asset record into the lens's `values` shape (EC-319), reading\n * only the schema's DECLARED fields — the same \"schema is a lens\" projection the\n * redirects/users rails use. A system column (`filename`, `url`, dimensions, …)\n * reads straight off the record; everything else — the built-in `alt`, a\n * project's own `copyright`/`credit`, or an EC-311 additive field — round-trips\n * through the record's open `meta` bag (EC-301). `alt` is schema-driven but ALSO\n * has a legacy record column: the meta value wins, falling back to the column\n * (matches delivery's `meta.alt ?? record.alt` read and the image field's\n * options dialog).\n */\nexport function assetToDocument(collection: CollectionDef, record: AssetRecord): CmsDocument {\n const values: Record<string, unknown> = {}\n for (const field of collection.fields) {\n if (field.name === 'alt') {\n const metaAlt = typeof record.meta?.alt === 'string' ? record.meta.alt : undefined\n values.alt = metaAlt ?? record.alt\n continue\n }\n values[field.name] = SYSTEM_ASSET_FIELDS.has(field.name)\n ? systemFieldValue(record, field.name)\n : record.meta?.[field.name]\n }\n return { collection: collection.id, id: record.id, values }\n}\n\n/**\n * The inverse of {@link assetToDocument}: the schema-driven meta bag a save\n * writes back — every declared field OUTSIDE the system set (EC-301/EC-311),\n * keyed exactly as the record stores it. System columns are upload-managed and\n * never written here.\n */\nexport function documentToAssetMetaPatch(\n collection: CollectionDef,\n doc: CmsDocument,\n): Record<string, unknown> {\n const values = doc.values ?? {}\n const meta: Record<string, unknown> = {}\n for (const field of collection.fields) {\n if (SYSTEM_ASSET_FIELDS.has(field.name)) continue\n if (field.name in values) meta[field.name] = values[field.name]\n }\n return meta\n}\n\n/** `create` when `capabilities.create === false` — never reached from the UI\n * (list/detail never call it); an explicit throw rather than a silent no-op. */\nfunction createUnsupported(): DocumentRef {\n throw new Error('assets are created by upload, not the assets rail')\n}\n\n/** Assets' `remove()` returns an `outcome`-discriminated {@link DeleteAssetResult}\n * (not the `{ ok }` shape users/redirects use, EC-327). `'error'` surfaces as-is;\n * `'blocked'` (the unconfirmed usage gate fired — nothing was removed) surfaces\n * as an explicit failure too (EC-433): a delete that left the asset in place\n * must NEVER look like success. It is unreachable from this rail's UI — every\n * delete path confirms through the reference dialog and dispatches with\n * `confirmInUse` — but stays honest for any direct caller. `'not-found'` maps\n * to success (idempotent delete: the desired end state — gone — holds). */\nexport function normalizeAssetRemoveResult(result: DeleteAssetResult): OperationError | null {\n if (result.outcome === 'error') return { code: 'unknown', message: result.message }\n if (result.outcome === 'blocked') {\n return {\n code: 'unknown',\n message: `Asset is referenced in ${result.usageCount} ${result.usageCount === 1 ? 'place' : 'places'} and was not deleted.`,\n }\n }\n return null\n}\n\n/**\n * How an asset row is addressed in the EC-155 reference index (EC-433):\n * `{ type: 'asset' }`, never the document shape — so the delete confirmation's\n * inbound pre-flight counts the SAME references `loadInboundAssetReferenceViews`\n * (the detail surface's check) does.\n */\nexport function assetReferenceTarget(ref: DocumentRef): ReferenceTarget {\n return { type: 'asset', id: ref.id }\n}\n\n/**\n * Assemble a {@link CollectionViewSource} for the **assets rail** (EC-319) — the\n * portable list surface over the existing asset library store instead of a\n * bespoke listing. `schema` is the DEPLOYMENT-resolved `kind:'asset'` collection\n * (EC-311: a project's own declared fields, or a mount's `nav.assets({ fields })`\n * override merged over it — see `resolveAssetsSchema` in `app-views/default-modules.tsx`),\n * never a hardcoded engine default — so a project's `copyright`/`credit`/… fields\n * (and any EC-311 override) drive the list's columns/filters exactly as they\n * drive the image field's options dialog.\n *\n * Must be mounted under an `<AssetsProvider>` (for {@link useAssets}) — already\n * ambient at the project level (EC-195), so callers rarely mount their own.\n *\n * The shared scaffold (`useRailViewSource`, EC-327) owns `viewState`/`lastError`\n * plumbing and the assembled source; this hook supplies only the asset-store\n * reads/writes, the capability profile, and the projections above.\n */\nexport function useAssetsCollectionViewSource(\n projectId: string,\n schema: CollectionDef,\n): CollectionViewSource {\n const store = useAssets()\n const registry = useMemo(() => createCollectionRegistry([schema]), [schema])\n\n const documents = useCallback(\n () => store.assets.map((record) => assetToDocument(schema, record)),\n [store.assets, schema],\n )\n const get = useCallback(\n (ref: DocumentRef): CmsDocument | undefined => {\n const record = store.assets.find((a) => a.id === ref.id)\n return record ? assetToDocument(schema, record) : undefined\n },\n [store.assets, schema],\n )\n const titleOf = useCallback(\n (ref: DocumentRef): string => {\n const record = store.assets.find((a) => a.id === ref.id)\n return record?.title ?? record?.filename ?? ref.id\n },\n [store.assets],\n )\n const validate = useCallback(\n (doc: CmsDocument) => validateDocument(registry, doc, { localeConfig: ASSETS_LOCALE_CONFIG }),\n [registry],\n )\n const save = useCallback(\n (doc: CmsDocument) => store.updateMetadata(doc.id, { meta: documentToAssetMetaPatch(schema, doc) }),\n [schema, store],\n )\n // The unconfirmed delete (the store's usage gate still applies — a referenced\n // asset comes back `blocked`, surfaced via `lastError`). The UI's delete paths\n // never land here: row/bulk deletes confirm through the reference dialog and\n // dispatch `bulkRemove` below.\n const remove = useCallback((ref: DocumentRef) => store.remove(ref.id), [store])\n // The CONFIRMED delete the bulk/row path dispatches (EC-433): the reference\n // dialog has already enumerated inbound usages (asset-shaped, see\n // `assetReferenceTarget`), so this passes `confirmInUse` exactly like the\n // detail surface's confirmed delete — same blob + thumb + cache handling.\n const bulkRemove = useCallback(\n (ref: DocumentRef) => store.remove(ref.id, { confirmInUse: true }),\n [store],\n )\n\n return useRailViewSource({\n projectId,\n schema,\n capabilities: ASSET_COLLECTION_CAPABILITIES,\n localeConfig: ASSETS_LOCALE_CONFIG,\n documents,\n get,\n titleOf,\n // No `referenceCandidates`/`collections`/`isFieldReadOnly` override: assets carry\n // no relation/asset fields of their own (v1) — nothing cross-collection to\n // resolve — and read-only follows the factory's default `form.readOnly` check.\n detailPath: (id: string) => studioPath.asset(id),\n listPath: studioPath.assets(),\n // EC-433: the delete confirmation must enumerate ASSET-shaped inbound\n // references — the document-shaped default would report \"not referenced\"\n // for a referenced asset.\n referenceTarget: assetReferenceTarget,\n validate,\n create: createUnsupported,\n save,\n remove,\n bulkRemove,\n normalizeSaveResult: normalizeOkResult,\n normalizeRemoveResult: normalizeAssetRemoveResult,\n })\n}\n"]}
|
|
@@ -2,40 +2,84 @@ import type { ComponentNode } from '@elytracms/core/project-graph';
|
|
|
2
2
|
import type { ComponentFixture, ComponentManifest } from '@elytracms/core/component-registry';
|
|
3
3
|
import type { StudioRegistry } from '@elytracms/studio/lib/registry';
|
|
4
4
|
/**
|
|
5
|
-
* Blocks panel model (EC-164) — the pure, React-free
|
|
6
|
-
* visual insert surface. Every registry
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
5
|
+
* Blocks panel model (EC-164, reshaped by EC-439) — the pure, React-free
|
|
6
|
+
* derivation behind the visual insert surface. Every canvas-eligible registry
|
|
7
|
+
* manifest yields **one tile** carrying the component identity plus a
|
|
8
|
+
* `variants` list (one per declared fixture, or a single implicit `Default`
|
|
9
|
+
* variant built from the seeded manifest defaults). Tiles group by manifest
|
|
10
|
+
* `category` and filter by a free-text query over name / description /
|
|
11
|
+
* category / **fixture names** — no hand-maintained panel config. Broken
|
|
12
|
+
* fixtures degrade to variants carrying explicit `issues`, never to a missing
|
|
13
|
+
* entry or a crash.
|
|
14
|
+
*
|
|
15
|
+
* Previews are ARTIFACTS, never runtime renders (EC-439 AD-2). Per tile and
|
|
16
|
+
* per variant, `previewUrl` resolves: curated `manifest.previewImage` (EC-409,
|
|
17
|
+
* always wins) → the {@link GeneratedPreviewLookup} seam (EC-440's build-time
|
|
18
|
+
* fixture renders) → `undefined` (the panel shows an icon+name placeholder).
|
|
11
19
|
*/
|
|
12
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* The generated-preview seam (EC-439 AD-2, filled by EC-440): a pure lookup
|
|
22
|
+
* from component id + fixture name to an image URL. The EC-440 CLI renders
|
|
23
|
+
* every canvas-eligible component's fixtures headlessly against the consumer
|
|
24
|
+
* theme and writes a previews manifest; the consumer's `studio.config.tsx`
|
|
25
|
+
* adapts that manifest into this function via `defineStudio({ previews:
|
|
26
|
+
* { generated } })`. A fixture-less manifest's single implicit variant is
|
|
27
|
+
* looked up under the name {@link DEFAULT_VARIANT}. Empty/undefined everywhere
|
|
28
|
+
* until EC-440 lands — the panel then falls back to placeholders.
|
|
29
|
+
*/
|
|
30
|
+
export type GeneratedPreviewLookup = (componentId: string, fixtureName: string) => string | undefined;
|
|
31
|
+
/**
|
|
32
|
+
* The implicit variant name of a fixture-less manifest (and the fixture name
|
|
33
|
+
* a {@link GeneratedPreviewLookup} is queried with for it).
|
|
34
|
+
*/
|
|
35
|
+
export declare const DEFAULT_VARIANT = "Default";
|
|
36
|
+
/**
|
|
37
|
+
* One insertable variant of a component tile: a declared manifest fixture, or
|
|
38
|
+
* the single implicit `Default` for a fixture-less manifest. `variants[0]` is
|
|
39
|
+
* the tile's default — what tile click / Enter / drag inserts.
|
|
40
|
+
*/
|
|
41
|
+
export interface BlockVariant {
|
|
42
|
+
/** The fixture name, or {@link DEFAULT_VARIANT} for the implicit variant. */
|
|
43
|
+
name: string;
|
|
44
|
+
/**
|
|
45
|
+
* Validation problems for this variant (manifest-level statuses plus the
|
|
46
|
+
* fixture's own issues). A non-empty list marks the variant as degraded —
|
|
47
|
+
* it still lists and still inserts.
|
|
48
|
+
*/
|
|
49
|
+
issues: string[];
|
|
50
|
+
/**
|
|
51
|
+
* Resolved preview image URL: curated `manifest.previewImage` (wins) else
|
|
52
|
+
* the generated-preview lookup's URL for this fixture. `undefined` → the
|
|
53
|
+
* panel renders the icon+name placeholder.
|
|
54
|
+
*/
|
|
55
|
+
previewUrl?: string;
|
|
56
|
+
/** Build the subtree this variant inserts (ids are reassigned on insert). */
|
|
57
|
+
build: () => ComponentNode;
|
|
58
|
+
}
|
|
59
|
+
/** One component (or layout preset) in the blocks panel — ONE tile per component (EC-439 AD-1). */
|
|
13
60
|
export interface BlockTile {
|
|
14
|
-
/** Stable identity
|
|
61
|
+
/** Stable identity: the component id, or `preset::<key>` for layout helpers. */
|
|
15
62
|
key: string;
|
|
16
63
|
/** Component id of the node the tile inserts. */
|
|
17
64
|
componentId: string;
|
|
18
65
|
title: string;
|
|
19
|
-
/** Variation label: the fixture name, `Default`, or a preset description. */
|
|
20
|
-
variant: string;
|
|
21
66
|
description?: string;
|
|
22
67
|
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* implementation). A non-empty list marks the tile as degraded — it still
|
|
26
|
-
* renders and still inserts.
|
|
68
|
+
* The DEFAULT variant's validation problems — what clicking the tile
|
|
69
|
+
* inserts. Per-variant issues live on {@link BlockTile.variants}.
|
|
27
70
|
*/
|
|
28
71
|
issues: string[];
|
|
29
|
-
/**
|
|
30
|
-
|
|
72
|
+
/** The default variant's resolved preview URL (see {@link BlockVariant.previewUrl}). */
|
|
73
|
+
previewUrl?: string;
|
|
74
|
+
/** All insertable variants; `variants[0]` is the default the tile inserts. */
|
|
75
|
+
variants: BlockVariant[];
|
|
31
76
|
/**
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* listings) and previews as "just text". Insertion is unaffected.
|
|
77
|
+
* Fixture names the active query matched (stamped by {@link blockGroups}
|
|
78
|
+
* when a query is present) — the panel highlights the variant affordance and
|
|
79
|
+
* the matching flyout rows so "found by fixture name" is visible.
|
|
36
80
|
*/
|
|
37
|
-
|
|
38
|
-
/** Build the subtree to insert
|
|
81
|
+
matchedVariants?: string[];
|
|
82
|
+
/** Build the DEFAULT subtree to insert: the first declared fixture's, else the seeded manifest default. */
|
|
39
83
|
build: () => ComponentNode;
|
|
40
84
|
}
|
|
41
85
|
/** Tiles of one manifest `category`. */
|
|
@@ -44,7 +88,7 @@ export interface BlockGroup {
|
|
|
44
88
|
tiles: BlockTile[];
|
|
45
89
|
}
|
|
46
90
|
export interface BlockFilter {
|
|
47
|
-
/** Case-insensitive match over id, title, description, and
|
|
91
|
+
/** Case-insensitive match over id, title, description, category, and fixture names. */
|
|
48
92
|
query?: string;
|
|
49
93
|
/** When set, only these component ids may be inserted (slot.allow). */
|
|
50
94
|
slotAllow?: string[];
|
|
@@ -72,14 +116,31 @@ export declare function fixtureSubtree(studio: StudioRegistry, manifest: Compone
|
|
|
72
116
|
node: ComponentNode;
|
|
73
117
|
issues: string[];
|
|
74
118
|
};
|
|
75
|
-
/**
|
|
76
|
-
|
|
119
|
+
/**
|
|
120
|
+
* The ONE tile for a manifest (EC-439 AD-1): component identity plus one
|
|
121
|
+
* variant per declared fixture — or a single implicit `Default` variant using
|
|
122
|
+
* the seeded manifest default (a bare layout container is seeded so it
|
|
123
|
+
* survives EC-161 normalization instead of vanishing on commit, EC-383). The
|
|
124
|
+
* tile's own `issues`/`previewUrl`/`build` mirror the default variant
|
|
125
|
+
* (`variants[0]`), so tile click/Enter/drag inserts exactly what the first
|
|
126
|
+
* fixture tile inserted before the reshape.
|
|
127
|
+
*/
|
|
128
|
+
export declare function tileForManifest(studio: StudioRegistry, manifest: ComponentManifest, generatedPreview?: GeneratedPreviewLookup): BlockTile;
|
|
129
|
+
/**
|
|
130
|
+
* A two-letter monogram from a tile title — the deliberate icon+name
|
|
131
|
+
* placeholder glyph (EC-439 AD-2 tier 3) for tiles with neither a curated nor
|
|
132
|
+
* a generated preview image: first letters of the first two words, else the
|
|
133
|
+
* first two letters of a single word. Deterministic and pure.
|
|
134
|
+
*/
|
|
135
|
+
export declare function tileMonogram(title: string): string;
|
|
77
136
|
/**
|
|
78
137
|
* Derive the panel's category groups from the registry: filter by slot allow
|
|
79
|
-
* list and query,
|
|
138
|
+
* list and query, derive ONE tile per manifest, group by `category`, and sort
|
|
80
139
|
* categories alphabetically (registry order within a group). Deterministic.
|
|
140
|
+
* When a query is active, each tile is stamped with the fixture names it
|
|
141
|
+
* matched (see {@link BlockTile.matchedVariants}).
|
|
81
142
|
*/
|
|
82
|
-
export declare function blockGroups(studio: StudioRegistry, filter?: BlockFilter): BlockGroup[];
|
|
143
|
+
export declare function blockGroups(studio: StudioRegistry, filter?: BlockFilter, generatedPreview?: GeneratedPreviewLookup): BlockGroup[];
|
|
83
144
|
/**
|
|
84
145
|
* Whether ANY component could be inserted under a slot allow-list (EC-405) — the
|
|
85
146
|
* cheap existence check behind the canvas insertion affordances: a "+" only shows
|
|
@@ -92,7 +153,8 @@ export declare function blockGroups(studio: StudioRegistry, filter?: BlockFilter
|
|
|
92
153
|
*/
|
|
93
154
|
export declare function hasInsertableComponent(studio: StudioRegistry, slotAllow?: string[]): boolean;
|
|
94
155
|
/**
|
|
95
|
-
* Layout helper presets (EC-067) as panel tiles
|
|
96
|
-
*
|
|
156
|
+
* Layout helper presets (EC-067) as panel tiles — already one tile per preset.
|
|
157
|
+
* A preset has a single implicit variant and never a preview image, so its
|
|
158
|
+
* tile renders the placeholder treatment.
|
|
97
159
|
*/
|
|
98
160
|
export declare function presetTiles(slotAllow?: string[]): BlockTile[];
|
|
@@ -1,10 +1,14 @@
|
|
|
1
|
-
import { cloneWithNewIds, isLayoutContainerId } from '@elytracms/core/project-graph';
|
|
2
1
|
import { isCanvasEligible, manifestSlots, propValueValidator } from '@elytracms/core/component-registry';
|
|
3
2
|
import { localizedStringVariants } from '@elytracms/core/cms-core';
|
|
4
3
|
import { localizedLabel } from '@elytracms/studio/lib/i18n';
|
|
5
4
|
import { componentStatus } from '@elytracms/studio/lib/registry';
|
|
6
|
-
import { nodeFromProps
|
|
5
|
+
import { nodeFromProps } from '@elytracms/studio/lib/registry/preview';
|
|
7
6
|
import { LAYOUT_PRESETS, nodeFromManifest, seededNodeFromManifest } from './node-factory.js';
|
|
7
|
+
/**
|
|
8
|
+
* The implicit variant name of a fixture-less manifest (and the fixture name
|
|
9
|
+
* a {@link GeneratedPreviewLookup} is queried with for it).
|
|
10
|
+
*/
|
|
11
|
+
export const DEFAULT_VARIANT = 'Default';
|
|
8
12
|
/**
|
|
9
13
|
* Intersect two component-id allow-lists. `undefined` means "no constraint from
|
|
10
14
|
* this source", so the result is the other list (copied); two present lists yield
|
|
@@ -25,13 +29,6 @@ export function intersectAllow(slotAllow, fieldAllow) {
|
|
|
25
29
|
return slotAllow.filter((id) => inField.has(id));
|
|
26
30
|
}
|
|
27
31
|
const UNCATEGORIZED = 'other';
|
|
28
|
-
function prefixedCounter(prefix) {
|
|
29
|
-
let n = 0;
|
|
30
|
-
return () => {
|
|
31
|
-
n += 1;
|
|
32
|
-
return `${prefix}-${n}`;
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
32
|
/** A node built from a manifest's defaults with an explicit id. */
|
|
36
33
|
function defaultNode(manifest, id) {
|
|
37
34
|
return { ...nodeFromManifest(manifest), id };
|
|
@@ -94,64 +91,90 @@ export function fixtureSubtree(studio, manifest, fixture, baseId) {
|
|
|
94
91
|
}
|
|
95
92
|
return { node, issues };
|
|
96
93
|
}
|
|
97
|
-
/**
|
|
98
|
-
|
|
94
|
+
/**
|
|
95
|
+
* The ONE tile for a manifest (EC-439 AD-1): component identity plus one
|
|
96
|
+
* variant per declared fixture — or a single implicit `Default` variant using
|
|
97
|
+
* the seeded manifest default (a bare layout container is seeded so it
|
|
98
|
+
* survives EC-161 normalization instead of vanishing on commit, EC-383). The
|
|
99
|
+
* tile's own `issues`/`previewUrl`/`build` mirror the default variant
|
|
100
|
+
* (`variants[0]`), so tile click/Enter/drag inserts exactly what the first
|
|
101
|
+
* fixture tile inserted before the reshape.
|
|
102
|
+
*/
|
|
103
|
+
export function tileForManifest(studio, manifest, generatedPreview) {
|
|
99
104
|
const title = localizedLabel(manifest.title, manifest.id);
|
|
100
105
|
const baseIssues = manifestIssues(studio, manifest);
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
106
|
+
// EC-409: a curated preview image previews the COMPONENT, not one fixture —
|
|
107
|
+
// when declared it wins on the tile and on every variant row (AD-2 tier 1).
|
|
108
|
+
const curated = manifest.previewImage;
|
|
109
|
+
const variants = manifest.fixtures && manifest.fixtures.length > 0
|
|
110
|
+
? manifest.fixtures.map((fixture, index) => {
|
|
111
|
+
const { issues } = fixtureSubtree(studio, manifest, fixture, `probe-${manifest.id}-${index}`);
|
|
112
|
+
return {
|
|
113
|
+
name: fixture.name,
|
|
114
|
+
issues: [...baseIssues, ...issues],
|
|
115
|
+
...previewUrlEntry(curated ?? generatedPreview?.(manifest.id, fixture.name)),
|
|
116
|
+
build: () => fixtureSubtree(studio, manifest, fixture, 'tmp').node,
|
|
117
|
+
};
|
|
118
|
+
})
|
|
119
|
+
: [
|
|
111
120
|
{
|
|
112
|
-
|
|
113
|
-
componentId: manifest.id,
|
|
114
|
-
title,
|
|
115
|
-
variant: 'Default',
|
|
116
|
-
description: manifest.description,
|
|
121
|
+
name: DEFAULT_VARIANT,
|
|
117
122
|
issues: baseIssues,
|
|
118
|
-
|
|
119
|
-
// EC-409: the curated preview image (when declared) replaces the live
|
|
120
|
-
// render on EVERY tile of the manifest — it previews the component,
|
|
121
|
-
// not one fixture variation.
|
|
122
|
-
...(manifest.previewImage ? { previewImage: manifest.previewImage } : {}),
|
|
123
|
+
...previewUrlEntry(curated ?? generatedPreview?.(manifest.id, DEFAULT_VARIANT)),
|
|
123
124
|
build: () => seededNodeFromManifest(manifest),
|
|
124
125
|
},
|
|
125
126
|
];
|
|
126
|
-
|
|
127
|
-
return
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
}
|
|
127
|
+
const defaultVariant = variants[0];
|
|
128
|
+
return {
|
|
129
|
+
key: manifest.id,
|
|
130
|
+
componentId: manifest.id,
|
|
131
|
+
title,
|
|
132
|
+
description: manifest.description,
|
|
133
|
+
issues: defaultVariant.issues,
|
|
134
|
+
...previewUrlEntry(defaultVariant.previewUrl),
|
|
135
|
+
variants,
|
|
136
|
+
build: defaultVariant.build,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
/** Spreadable `{ previewUrl }` entry — omitted (not `undefined`-valued) when absent. */
|
|
140
|
+
function previewUrlEntry(url) {
|
|
141
|
+
return url === undefined ? {} : { previewUrl: url };
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* A two-letter monogram from a tile title — the deliberate icon+name
|
|
145
|
+
* placeholder glyph (EC-439 AD-2 tier 3) for tiles with neither a curated nor
|
|
146
|
+
* a generated preview image: first letters of the first two words, else the
|
|
147
|
+
* first two letters of a single word. Deterministic and pure.
|
|
148
|
+
*/
|
|
149
|
+
export function tileMonogram(title) {
|
|
150
|
+
const words = title.trim().split(/\s+/).filter(Boolean);
|
|
151
|
+
if (words.length === 0)
|
|
152
|
+
return '?';
|
|
153
|
+
const glyph = words.length === 1 ? words[0].slice(0, 2) : `${words[0].charAt(0)}${words[1].charAt(0)}`;
|
|
154
|
+
return glyph.toUpperCase();
|
|
155
|
+
}
|
|
156
|
+
function matchedFixtureNames(manifest, query) {
|
|
157
|
+
return (manifest.fixtures ?? [])
|
|
158
|
+
.map((fixture) => fixture.name)
|
|
159
|
+
.filter((name) => name.toLowerCase().includes(query));
|
|
141
160
|
}
|
|
142
161
|
function matches(manifest, query) {
|
|
143
162
|
const hay = [manifest.id, ...localizedStringVariants(manifest.title), manifest.description, manifest.category]
|
|
144
163
|
.filter(Boolean)
|
|
145
164
|
.join(' ')
|
|
146
165
|
.toLowerCase();
|
|
147
|
-
|
|
166
|
+
// EC-439: fixture names are searchable too — a content-seeded fixture name
|
|
167
|
+
// ("Startseite") must find its component even when no manifest field matches.
|
|
168
|
+
return hay.includes(query) || matchedFixtureNames(manifest, query).length > 0;
|
|
148
169
|
}
|
|
149
170
|
/**
|
|
150
171
|
* Derive the panel's category groups from the registry: filter by slot allow
|
|
151
|
-
* list and query,
|
|
172
|
+
* list and query, derive ONE tile per manifest, group by `category`, and sort
|
|
152
173
|
* categories alphabetically (registry order within a group). Deterministic.
|
|
174
|
+
* When a query is active, each tile is stamped with the fixture names it
|
|
175
|
+
* matched (see {@link BlockTile.matchedVariants}).
|
|
153
176
|
*/
|
|
154
|
-
export function blockGroups(studio, filter = {}) {
|
|
177
|
+
export function blockGroups(studio, filter = {}, generatedPreview) {
|
|
155
178
|
const query = filter.query?.trim().toLowerCase();
|
|
156
179
|
const allow = filter.slotAllow;
|
|
157
180
|
const groups = new Map();
|
|
@@ -165,9 +188,15 @@ export function blockGroups(studio, filter = {}) {
|
|
|
165
188
|
continue;
|
|
166
189
|
if (query && !matches(manifest, query))
|
|
167
190
|
continue;
|
|
191
|
+
const tile = tileForManifest(studio, manifest, generatedPreview);
|
|
192
|
+
if (query) {
|
|
193
|
+
const matched = matchedFixtureNames(manifest, query);
|
|
194
|
+
if (matched.length > 0)
|
|
195
|
+
tile.matchedVariants = matched;
|
|
196
|
+
}
|
|
168
197
|
const category = manifest.category ?? UNCATEGORIZED;
|
|
169
198
|
const tiles = groups.get(category) ?? [];
|
|
170
|
-
tiles.push(
|
|
199
|
+
tiles.push(tile);
|
|
171
200
|
groups.set(category, tiles);
|
|
172
201
|
}
|
|
173
202
|
return [...groups.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([category, tiles]) => ({ category, tiles }));
|
|
@@ -193,18 +222,18 @@ export function hasInsertableComponent(studio, slotAllow) {
|
|
|
193
222
|
return presetTiles(slotAllow).length > 0;
|
|
194
223
|
}
|
|
195
224
|
/**
|
|
196
|
-
* Layout helper presets (EC-067) as panel tiles
|
|
197
|
-
*
|
|
225
|
+
* Layout helper presets (EC-067) as panel tiles — already one tile per preset.
|
|
226
|
+
* A preset has a single implicit variant and never a preview image, so its
|
|
227
|
+
* tile renders the placeholder treatment.
|
|
198
228
|
*/
|
|
199
229
|
export function presetTiles(slotAllow) {
|
|
200
230
|
return LAYOUT_PRESETS.filter((preset) => !slotAllow || slotAllow.includes(preset.build().componentId)).map((preset) => ({
|
|
201
231
|
key: `preset::${preset.key}`,
|
|
202
232
|
componentId: preset.build().componentId,
|
|
203
233
|
title: preset.label,
|
|
204
|
-
variant: preset.description,
|
|
205
234
|
description: preset.description,
|
|
206
235
|
issues: [],
|
|
207
|
-
|
|
236
|
+
variants: [{ name: DEFAULT_VARIANT, issues: [], build: () => preset.build() }],
|
|
208
237
|
build: () => preset.build(),
|
|
209
238
|
}));
|
|
210
239
|
}
|