@elytracms/studio 0.0.90 → 0.0.91

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/convex/asset-revalidate.test.ts +14 -1
  2. package/convex/cms.ts +3 -4
  3. package/convex/document-revalidate.test.ts +14 -1
  4. package/convex/redirect-revalidate.test.ts +14 -1
  5. package/dist/components/assets/asset-selection.d.ts +8 -9
  6. package/dist/components/assets/asset-selection.js +26 -16
  7. package/dist/components/assets/asset-selection.js.map +1 -1
  8. package/dist/components/builder/blocks-model.d.ts +7 -5
  9. package/dist/components/builder/blocks-model.js +11 -9
  10. package/dist/components/builder/blocks-model.js.map +1 -1
  11. package/dist/components/builder/blocks-panel.js +1 -1
  12. package/dist/components/builder/blocks-panel.js.map +1 -1
  13. package/dist/components/builder/builder-store.d.ts +11 -3
  14. package/dist/components/builder/builder-store.js +12 -5
  15. package/dist/components/builder/builder-store.js.map +1 -1
  16. package/dist/components/builder/canvas-dnd.d.ts +7 -1
  17. package/dist/components/builder/canvas-dnd.js +6 -3
  18. package/dist/components/builder/canvas-dnd.js.map +1 -1
  19. package/dist/components/builder/canvas-render-context.d.ts +13 -1
  20. package/dist/components/builder/canvas-render-context.js +28 -18
  21. package/dist/components/builder/canvas-render-context.js.map +1 -1
  22. package/dist/components/builder/canvas.js +1 -1
  23. package/dist/components/builder/canvas.js.map +1 -1
  24. package/dist/components/builder/clipboard-actions.d.ts +0 -6
  25. package/dist/components/builder/clipboard-actions.js +9 -4
  26. package/dist/components/builder/clipboard-actions.js.map +1 -1
  27. package/dist/components/builder/composition-field-editor.d.ts +22 -0
  28. package/dist/components/builder/composition-field-editor.js +47 -8
  29. package/dist/components/builder/composition-field-editor.js.map +1 -1
  30. package/dist/components/builder/inline-edit.d.ts +7 -4
  31. package/dist/components/builder/inline-edit.js +11 -8
  32. package/dist/components/builder/inline-edit.js.map +1 -1
  33. package/dist/components/builder/insert-context.js +9 -5
  34. package/dist/components/builder/insert-context.js.map +1 -1
  35. package/dist/components/builder/layout-inspector.d.ts +2 -2
  36. package/dist/components/builder/layout-inspector.js +8 -6
  37. package/dist/components/builder/layout-inspector.js.map +1 -1
  38. package/dist/components/builder/node-factory.d.ts +31 -14
  39. package/dist/components/builder/node-factory.js +73 -93
  40. package/dist/components/builder/node-factory.js.map +1 -1
  41. package/dist/components/builder/page-tree.d.ts +3 -1
  42. package/dist/components/builder/page-tree.js +6 -4
  43. package/dist/components/builder/page-tree.js.map +1 -1
  44. package/dist/components/builder/property-inspector.js +2 -1
  45. package/dist/components/builder/property-inspector.js.map +1 -1
  46. package/dist/components/builder/reuse.d.ts +1 -1
  47. package/dist/components/builder/reuse.js +8 -5
  48. package/dist/components/builder/reuse.js.map +1 -1
  49. package/dist/components/builder/selection-overlay.d.ts +2 -2
  50. package/dist/components/builder/selection-overlay.js +3 -3
  51. package/dist/components/builder/selection-overlay.js.map +1 -1
  52. package/dist/components/builder/spacing-handles.d.ts +2 -2
  53. package/dist/components/builder/spacing-handles.js +3 -3
  54. package/dist/components/builder/spacing-handles.js.map +1 -1
  55. package/dist/components/cms/asset-field.d.ts +1 -1
  56. package/dist/components/cms/asset-field.js +15 -13
  57. package/dist/components/cms/asset-field.js.map +1 -1
  58. package/dist/components/cms/asset-picker-core.js +3 -4
  59. package/dist/components/cms/asset-picker-core.js.map +1 -1
  60. package/dist/components/cms/detail-editor.js +10 -5
  61. package/dist/components/cms/detail-editor.js.map +1 -1
  62. package/dist/components/cms/discard-draft.d.ts +1 -2
  63. package/dist/components/cms/discard-draft.js +1 -1
  64. package/dist/components/cms/discard-draft.js.map +1 -1
  65. package/dist/components/cms/editable-cell.js +11 -7
  66. package/dist/components/cms/editable-cell.js.map +1 -1
  67. package/dist/components/cms/field-control-lib.d.ts +10 -0
  68. package/dist/components/cms/field-control-lib.js +20 -0
  69. package/dist/components/cms/field-control-lib.js.map +1 -1
  70. package/dist/components/cms/field-control.js +3 -3
  71. package/dist/components/cms/field-control.js.map +1 -1
  72. package/dist/components/cms/object-array-field.js +3 -5
  73. package/dist/components/cms/object-array-field.js.map +1 -1
  74. package/dist/components/cms/prop-fields.d.ts +16 -3
  75. package/dist/components/cms/prop-fields.js +57 -12
  76. package/dist/components/cms/prop-fields.js.map +1 -1
  77. package/dist/components/cms/rich-text/embed-props.d.ts +6 -1
  78. package/dist/components/cms/rich-text/embed-props.js +7 -12
  79. package/dist/components/cms/rich-text/embed-props.js.map +1 -1
  80. package/dist/components/cms/rich-text/insert.d.ts +13 -6
  81. package/dist/components/cms/rich-text/insert.js +16 -17
  82. package/dist/components/cms/rich-text/insert.js.map +1 -1
  83. package/dist/components/cms/use-cell-edit.js +7 -12
  84. package/dist/components/cms/use-cell-edit.js.map +1 -1
  85. package/dist/components/cms/use-discard-controller.d.ts +1 -2
  86. package/dist/components/cms/use-discard-controller.js +1 -2
  87. package/dist/components/cms/use-discard-controller.js.map +1 -1
  88. package/dist/components/cms/versions-pane.js +2 -2
  89. package/dist/components/cms/versions-pane.js.map +1 -1
  90. package/dist/components/components/component-detail.d.ts +1 -1
  91. package/dist/components/components/component-detail.js +2 -4
  92. package/dist/components/components/component-detail.js.map +1 -1
  93. package/dist/components/components/component-preview.js +9 -4
  94. package/dist/components/components/component-preview.js.map +1 -1
  95. package/dist/components/components/components-index.d.ts +2 -2
  96. package/dist/components/components/components-index.js +3 -6
  97. package/dist/components/components/components-index.js.map +1 -1
  98. package/dist/components/components/field-inspector.js +5 -2
  99. package/dist/components/components/field-inspector.js.map +1 -1
  100. package/dist/components/components/registry-browser.d.ts +6 -5
  101. package/dist/components/components/registry-browser.js +11 -37
  102. package/dist/components/components/registry-browser.js.map +1 -1
  103. package/dist/components/components/taxonomy.d.ts +14 -21
  104. package/dist/components/components/taxonomy.js +5 -29
  105. package/dist/components/components/taxonomy.js.map +1 -1
  106. package/dist/components/components/transfer.d.ts +10 -8
  107. package/dist/components/components/transfer.js +17 -13
  108. package/dist/components/components/transfer.js.map +1 -1
  109. package/dist/lib/ai/constraints.d.ts +16 -14
  110. package/dist/lib/ai/constraints.js +64 -48
  111. package/dist/lib/ai/constraints.js.map +1 -1
  112. package/dist/lib/ai/update-fields.js +5 -3
  113. package/dist/lib/ai/update-fields.js.map +1 -1
  114. package/dist/lib/assets/asset-placements.d.ts +3 -10
  115. package/dist/lib/assets/asset-placements.js +17 -8
  116. package/dist/lib/assets/asset-placements.js.map +1 -1
  117. package/dist/lib/clipboard/paste-target.js +3 -3
  118. package/dist/lib/clipboard/paste-target.js.map +1 -1
  119. package/dist/lib/cms-views/view-model.js +11 -8
  120. package/dist/lib/cms-views/view-model.js.map +1 -1
  121. package/dist/lib/data/collection-content.d.ts +10 -7
  122. package/dist/lib/data/collection-content.js +34 -15
  123. package/dist/lib/data/collection-content.js.map +1 -1
  124. package/dist/lib/dnd/drop-rules.d.ts +22 -7
  125. package/dist/lib/dnd/drop-rules.js +21 -14
  126. package/dist/lib/dnd/drop-rules.js.map +1 -1
  127. package/dist/lib/engine-version.d.ts +1 -1
  128. package/dist/lib/engine-version.js +1 -1
  129. package/dist/lib/engine-version.js.map +1 -1
  130. package/dist/lib/i18n/messages.de.js +25 -9
  131. package/dist/lib/i18n/messages.de.js.map +1 -1
  132. package/dist/lib/i18n/messages.en.d.ts +21 -7
  133. package/dist/lib/i18n/messages.en.js +28 -9
  134. package/dist/lib/i18n/messages.en.js.map +1 -1
  135. package/dist/lib/operations/cms-persistence.js +7 -4
  136. package/dist/lib/operations/cms-persistence.js.map +1 -1
  137. package/dist/lib/persona/labels.d.ts +4 -9
  138. package/dist/lib/persona/labels.js +5 -12
  139. package/dist/lib/persona/labels.js.map +1 -1
  140. package/dist/lib/references/reference-model.d.ts +0 -2
  141. package/dist/lib/references/reference-model.js +3 -5
  142. package/dist/lib/references/reference-model.js.map +1 -1
  143. package/dist/lib/registry/__fixtures__/demo-components.d.ts +32 -0
  144. package/dist/lib/registry/{project-components.js → __fixtures__/demo-components.js} +18 -5
  145. package/dist/lib/registry/__fixtures__/demo-components.js.map +1 -0
  146. package/dist/lib/registry/index.d.ts +9 -13
  147. package/dist/lib/registry/index.js +34 -61
  148. package/dist/lib/registry/index.js.map +1 -1
  149. package/dist/lib/registry/preview.d.ts +22 -7
  150. package/dist/lib/registry/preview.js +26 -89
  151. package/dist/lib/registry/preview.js.map +1 -1
  152. package/dist/previews/enumerate.d.ts +23 -27
  153. package/dist/previews/enumerate.js +27 -48
  154. package/dist/previews/enumerate.js.map +1 -1
  155. package/dist/previews/harness.js +1 -1
  156. package/dist/previews/harness.js.map +1 -1
  157. package/package.json +4 -3
  158. package/dist/lib/registry/project-components.d.ts +0 -16
  159. package/dist/lib/registry/project-components.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"registry-browser.js","sourceRoot":"","sources":["../../../src/components/components/registry-browser.tsx"],"names":[],"mappings":"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","sourcesContent":["import { Link, STUDIO_PROJECT_ROUTE, studioPath } from '@elytracms/studio/router'\nimport { Search } from 'lucide-react'\nimport { useMemo, useState } from 'react'\nimport type { ComponentManifest } from '@elytracms/core/component-registry'\nimport { componentStatus, getStudioRegistry } from '@elytracms/studio/lib/registry'\nimport { localizedLabel, translate } from '@elytracms/studio/lib/i18n'\nimport { Code, Input, Row, SegmentedControl, Spacer, Stack, Text } from '@elytracms/studio/ui'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { ComponentStatusDot } from './component-status'\nimport { deriveTaxonomy } from './taxonomy'\nimport type { ComponentNamespace } from './taxonomy'\n\ntype NamespaceFilter = 'all' | ComponentNamespace\n\n/**\n * The registry browser sidebar (EC-050, taxonomy overhaul EC-320). A searchable,\n * two-level tree: namespace (base / project) → category (the manifest's declared\n * `category`, uncategorized last). The segmented control narrows to a namespace;\n * search filters across id/title/category/description. Grouping + search are pure\n * ({@link deriveTaxonomy}), so what renders here is exactly what the tests pin.\n */\nexport function RegistryBrowser({\n projectId,\n activeComponentId,\n}: {\n projectId: string\n activeComponentId?: string\n}) {\n const studio = getStudioRegistry()\n const [query, setQuery] = useState('')\n const [namespace, setNamespace] = useState<NamespaceFilter>('all')\n\n const manifests = useMemo(() => studio.registry.list(), [studio])\n const primitiveCount = studio.registry.primitives().length\n const projectCount = studio.registry.projectComponents().length\n\n // The full taxonomy for the query; the segmented control then narrows namespaces.\n const taxonomy = useMemo(() => deriveTaxonomy(manifests, query), [manifests, query])\n const shown = taxonomy.filter((group) => namespace === 'all' || group.namespace === namespace)\n const empty = shown.length === 0\n\n return (\n <div className=\"flex h-full min-h-0 flex-col\">\n <div className=\"shrink-0 space-y-2 border-b border-border p-3\">\n <div className=\"relative\">\n <Search className=\"pointer-events-none absolute left-2.5 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground\" />\n <Input\n value={query}\n onChange={(e) => setQuery(e.target.value)}\n placeholder={translate('components.browser.searchPlaceholder')}\n className=\"pl-8\"\n aria-label={translate('components.browser.searchAria')}\n />\n </div>\n <SegmentedControl\n equalWidth\n ariaLabel={translate('components.browser.namespaceAria')}\n items={[\n {\n key: 'all',\n label: translate('components.browser.all'),\n count: primitiveCount + projectCount,\n active: namespace === 'all',\n onClick: () => setNamespace('all'),\n },\n {\n key: 'base',\n label: translate('components.browser.primitives'),\n count: primitiveCount,\n active: namespace === 'base',\n onClick: () => setNamespace('base'),\n },\n {\n key: 'project',\n label: translate('components.browser.project'),\n count: projectCount,\n active: namespace === 'project',\n onClick: () => setNamespace('project'),\n },\n ]}\n />\n </div>\n\n <div className=\"min-h-0 flex-1 overflow-auto p-2\">\n {empty ? (\n <Text size=\"sm\" tone=\"muted\" className=\"p-3\">\n {query.trim()\n ? translate('components.browser.noMatchQuery', { query })\n : translate('components.browser.noMatchFilters')}\n </Text>\n ) : (\n <Stack gap=\"4\">\n {shown.map((group) => (\n <Stack key={group.namespace} gap=\"2\">\n <Row gap=\"2\" className=\"px-2\">\n <Code tone=\"muted\">{`${group.namespace}.*`}</Code>\n <Spacer />\n <Text size=\"xs\" tone=\"subtle\" mono>\n {group.count}\n </Text>\n </Row>\n <Stack gap=\"3\">\n {group.categories.map((category) => (\n <Stack key={category.key || '__uncategorized__'} gap=\"1\">\n <Text\n size=\"xs\"\n weight=\"medium\"\n tone=\"subtle\"\n className={cn('px-2 uppercase tracking-wide', category.key && 'capitalize')}\n >\n {category.key || translate('components.taxonomy.uncategorized')}\n </Text>\n <Stack gap=\"px\">\n {category.manifests.map((m) => (\n <ComponentRow\n key={m.id}\n manifest={m}\n projectId={projectId}\n active={m.id === activeComponentId}\n />\n ))}\n </Stack>\n </Stack>\n ))}\n </Stack>\n </Stack>\n ))}\n </Stack>\n )}\n </div>\n </div>\n )\n}\n\nfunction ComponentRow({\n manifest,\n projectId,\n active,\n}: {\n manifest: ComponentManifest\n projectId: string\n active: boolean\n}) {\n const studio = getStudioRegistry()\n const status = componentStatus(studio, manifest)\n return (\n <Link\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId, _splat: studioPath.component(manifest.id) }}\n aria-current={active ? 'page' : undefined}\n className={cn(\n 'flex items-center gap-2 rounded-md px-2 py-1.5 text-sm transition-colors',\n active\n ? 'bg-accent text-accent-foreground'\n : 'text-muted-foreground hover:bg-muted hover:text-foreground',\n )}\n >\n <ComponentStatusDot status={status} />\n <span className=\"min-w-0 flex-1 truncate font-medium text-foreground\">\n {localizedLabel(manifest.title, manifest.id)}\n </span>\n </Link>\n )\n}\n"]}
1
+ {"version":3,"file":"registry-browser.js","sourceRoot":"","sources":["../../../src/components/components/registry-browser.tsx"],"names":[],"mappings":"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","sourcesContent":["import { Link, STUDIO_PROJECT_ROUTE, studioPath } from '@elytracms/studio/router'\nimport { Search } from 'lucide-react'\nimport { useMemo, useState } from 'react'\nimport type { ComponentManifest } from '@elytracms/core/component-registry'\nimport { componentStatus, getStudioRegistry } from '@elytracms/studio/lib/registry'\nimport { localizedLabel, translate } from '@elytracms/studio/lib/i18n'\nimport { Input, Stack, Text } from '@elytracms/studio/ui'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { ComponentStatusDot } from './component-status'\nimport { deriveTaxonomy } from './taxonomy'\n\n/**\n * The registry browser sidebar (EC-050, taxonomy overhaul EC-320). A searchable\n * list grouped by category (the manifest's declared `category`, uncategorized\n * last); search filters across id/title/category/description. Grouping + search\n * are pure ({@link deriveTaxonomy}), so what renders here is exactly what the\n * tests pin. EC-591: one level — the `base.*`/`project.*` namespace filter went\n * with the id classification; the registry is the config's one list.\n */\nexport function RegistryBrowser({\n projectId,\n activeComponentId,\n}: {\n projectId: string\n activeComponentId?: string\n}) {\n const studio = getStudioRegistry()\n const [query, setQuery] = useState('')\n\n const manifests = useMemo(() => studio.registry.list(), [studio])\n const categories = useMemo(() => deriveTaxonomy(manifests, query), [manifests, query])\n const empty = categories.length === 0\n\n return (\n <div className=\"flex h-full min-h-0 flex-col\">\n <div className=\"shrink-0 space-y-2 border-b border-border p-3\">\n <div className=\"relative\">\n <Search className=\"pointer-events-none absolute left-2.5 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground\" />\n <Input\n value={query}\n onChange={(e) => setQuery(e.target.value)}\n placeholder={translate('components.browser.searchPlaceholder')}\n className=\"pl-8\"\n aria-label={translate('components.browser.searchAria')}\n />\n </div>\n </div>\n\n <div className=\"min-h-0 flex-1 overflow-auto p-2\">\n {empty ? (\n <Text size=\"sm\" tone=\"muted\" className=\"p-3\">\n {query.trim()\n ? translate('components.browser.noMatchQuery', { query })\n : translate('components.browser.noMatchFilters')}\n </Text>\n ) : (\n <Stack gap=\"3\">\n {categories.map((category) => (\n <Stack key={category.key || '__uncategorized__'} gap=\"1\">\n <Text\n size=\"xs\"\n weight=\"medium\"\n tone=\"subtle\"\n className={cn('px-2 uppercase tracking-wide', category.key && 'capitalize')}\n >\n {category.key || translate('components.taxonomy.uncategorized')}\n </Text>\n <Stack gap=\"px\">\n {category.manifests.map((m) => (\n <ComponentRow\n key={m.id}\n manifest={m}\n projectId={projectId}\n active={m.id === activeComponentId}\n />\n ))}\n </Stack>\n </Stack>\n ))}\n </Stack>\n )}\n </div>\n </div>\n )\n}\n\nfunction ComponentRow({\n manifest,\n projectId,\n active,\n}: {\n manifest: ComponentManifest\n projectId: string\n active: boolean\n}) {\n const studio = getStudioRegistry()\n const status = componentStatus(studio, manifest)\n return (\n <Link\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId, _splat: studioPath.component(manifest.id) }}\n aria-current={active ? 'page' : undefined}\n className={cn(\n 'flex items-center gap-2 rounded-md px-2 py-1.5 text-sm transition-colors',\n active\n ? 'bg-accent text-accent-foreground'\n : 'text-muted-foreground hover:bg-muted hover:text-foreground',\n )}\n >\n <ComponentStatusDot status={status} />\n <span className=\"min-w-0 flex-1 truncate font-medium text-foreground\">\n {localizedLabel(manifest.title, manifest.id)}\n </span>\n </Link>\n )\n}\n"]}
@@ -1,51 +1,44 @@
1
1
  import type { ComponentManifest } from '@elytracms/core/component-registry';
2
2
  /**
3
3
  * EC-320 — the component registry TAXONOMY, derived purely from the metadata a
4
- * manifest actually declares (no invented axes). Two levels of grouping:
4
+ * manifest actually declares (no invented axes): the manifest's optional
5
+ * `category` string, with an "uncategorized" bucket (`key: ''`) collected last.
5
6
  *
6
- * 1. **namespace** — `base` platform primitives vs `project` components
7
- * (`isPrimitive(id)`, i.e. `namespaceOf(id) === 'base'`).
8
- * 2. **category** — the manifest's optional `category` string, with an
9
- * "uncategorized" bucket (`key: ''`) collected last.
7
+ * EC-591: the namespace level (`base` primitives vs `project` components) is gone
8
+ * with the id classification it read — the registry is the config's one list, and
9
+ * an id prefix means nothing to the engine.
10
10
  *
11
11
  * Manifests carry no free-form `tags`, so category is the finest declared axis —
12
12
  * this makes the best of what's there rather than fabricating structure. Pure and
13
13
  * store-agnostic so the sidebar's grouping/search is unit-testable without React.
14
14
  */
15
- export type ComponentNamespace = 'base' | 'project';
16
15
  export interface TaxonomyCategory {
17
16
  /** The manifest category, or `''` for the uncategorized bucket. */
18
17
  key: string;
19
18
  manifests: ComponentManifest[];
20
19
  }
21
- export interface TaxonomyNamespace {
22
- namespace: ComponentNamespace;
23
- count: number;
24
- categories: TaxonomyCategory[];
25
- }
26
20
  /** Case-insensitive match against a manifest's id, title, category, description. */
27
21
  export declare function manifestMatchesQuery(manifest: ComponentManifest, query: string): boolean;
28
22
  /**
29
- * Derive the two-level taxonomy for the sidebar. `base` precedes `project` (rail
30
- * order). An optional `query` filters manifests first; a namespace/category with
31
- * no surviving manifest is omitted, so every rendered group yields results.
23
+ * Derive the taxonomy for the sidebar: category groups. An optional `query`
24
+ * filters manifests first; a category with no surviving manifest is omitted, so
25
+ * every rendered group yields results.
32
26
  */
33
- export declare function deriveTaxonomy(manifests: readonly ComponentManifest[], query?: string): TaxonomyNamespace[];
27
+ export declare function deriveTaxonomy(manifests: readonly ComponentManifest[], query?: string): TaxonomyCategory[];
34
28
  export interface CategoryCount {
35
29
  /** The manifest category, or `''` for the uncategorized bucket. */
36
30
  key: string;
37
31
  count: number;
38
32
  }
39
33
  /**
40
- * Cheap registry stats for the intro page (EC-320): the total, the base/project
41
- * split, and a per-category breakdown (uncategorized last). Derived purely from
42
- * the manifest set — no health/implementation lookups here (those come from the
43
- * separate {@link import('@elytracms/studio/lib/registry').registryHealth}).
34
+ * Cheap registry stats for the intro page (EC-320): the total and a per-category
35
+ * breakdown (uncategorized last). Derived purely from the manifest set — no
36
+ * health/implementation lookups here (those come from the separate
37
+ * {@link import('@elytracms/studio/lib/registry').registryHealth}). EC-591: no
38
+ * base/project split — there is one list.
44
39
  */
45
40
  export interface ComponentStats {
46
41
  total: number;
47
- base: number;
48
- project: number;
49
42
  categories: CategoryCount[];
50
43
  }
51
44
  export declare function deriveComponentStats(manifests: readonly ComponentManifest[]): ComponentStats;
@@ -1,8 +1,4 @@
1
- import { isPrimitive } from '@elytracms/core/component-registry';
2
1
  import { localizedStringVariants, resolveLabel } from '@elytracms/core/cms-core';
3
- function namespaceOfManifest(manifest) {
4
- return isPrimitive(manifest.id) ? 'base' : 'project';
5
- }
6
2
  /** Case-insensitive match against a manifest's id, title, category, description. */
7
3
  export function manifestMatchesQuery(manifest, query) {
8
4
  const q = query.trim().toLowerCase();
@@ -40,35 +36,15 @@ function groupCategories(manifests) {
40
36
  }));
41
37
  }
42
38
  /**
43
- * Derive the two-level taxonomy for the sidebar. `base` precedes `project` (rail
44
- * order). An optional `query` filters manifests first; a namespace/category with
45
- * no surviving manifest is omitted, so every rendered group yields results.
39
+ * Derive the taxonomy for the sidebar: category groups. An optional `query`
40
+ * filters manifests first; a category with no surviving manifest is omitted, so
41
+ * every rendered group yields results.
46
42
  */
47
43
  export function deriveTaxonomy(manifests, query = '') {
48
- const matched = manifests.filter((m) => manifestMatchesQuery(m, query));
49
- const order = ['base', 'project'];
50
- return order
51
- .map((namespace) => {
52
- const inNamespace = matched.filter((m) => namespaceOfManifest(m) === namespace);
53
- return {
54
- namespace,
55
- count: inNamespace.length,
56
- categories: groupCategories(inNamespace),
57
- };
58
- })
59
- .filter((group) => group.count > 0);
44
+ return groupCategories(manifests.filter((m) => manifestMatchesQuery(m, query)));
60
45
  }
61
46
  export function deriveComponentStats(manifests) {
62
- let base = 0;
63
- for (const m of manifests)
64
- if (namespaceOfManifest(m) === 'base')
65
- base += 1;
66
47
  const categories = groupCategories([...manifests]).map((c) => ({ key: c.key, count: c.manifests.length }));
67
- return {
68
- total: manifests.length,
69
- base,
70
- project: manifests.length - base,
71
- categories,
72
- };
48
+ return { total: manifests.length, categories };
73
49
  }
74
50
  //# sourceMappingURL=taxonomy.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"taxonomy.js","sourceRoot":"","sources":["../../../src/components/components/taxonomy.ts"],"names":[],"mappings":"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","sourcesContent":["import { isPrimitive } from '@elytracms/core/component-registry'\nimport type { ComponentManifest } from '@elytracms/core/component-registry'\nimport { localizedStringVariants, resolveLabel } from '@elytracms/core/cms-core'\n\n/**\n * EC-320 — the component registry TAXONOMY, derived purely from the metadata a\n * manifest actually declares (no invented axes). Two levels of grouping:\n *\n * 1. **namespace** — `base` platform primitives vs `project` components\n * (`isPrimitive(id)`, i.e. `namespaceOf(id) === 'base'`).\n * 2. **category** — the manifest's optional `category` string, with an\n * \"uncategorized\" bucket (`key: ''`) collected last.\n *\n * Manifests carry no free-form `tags`, so category is the finest declared axis —\n * this makes the best of what's there rather than fabricating structure. Pure and\n * store-agnostic so the sidebar's grouping/search is unit-testable without React.\n */\n\nexport type ComponentNamespace = 'base' | 'project'\n\nexport interface TaxonomyCategory {\n /** The manifest category, or `''` for the uncategorized bucket. */\n key: string\n manifests: ComponentManifest[]\n}\n\nexport interface TaxonomyNamespace {\n namespace: ComponentNamespace\n count: number\n categories: TaxonomyCategory[]\n}\n\nfunction namespaceOfManifest(manifest: ComponentManifest): ComponentNamespace {\n return isPrimitive(manifest.id) ? 'base' : 'project'\n}\n\n/** Case-insensitive match against a manifest's id, title, category, description. */\nexport function manifestMatchesQuery(manifest: ComponentManifest, query: string): boolean {\n const q = query.trim().toLowerCase()\n if (q === '') return true\n const haystack = [manifest.id, ...localizedStringVariants(manifest.title), manifest.category, manifest.description]\n .filter((v): v is string => typeof v === 'string')\n .join(' ')\n .toLowerCase()\n return haystack.includes(q)\n}\n\n/** Group category buckets: sorted alphabetically, the uncategorized bucket last. */\nfunction groupCategories(manifests: ComponentManifest[]): TaxonomyCategory[] {\n const byCategory = new Map<string, ComponentManifest[]>()\n for (const manifest of manifests) {\n const key = manifest.category ?? ''\n const bucket = byCategory.get(key)\n if (bucket) bucket.push(manifest)\n else byCategory.set(key, [manifest])\n }\n return [...byCategory.entries()]\n .sort(([a], [b]) => {\n // Uncategorized ('') always sorts last; otherwise alphabetical.\n if (a === '') return 1\n if (b === '') return -1\n return a.localeCompare(b)\n })\n .map(([key, ms]) => ({\n key,\n manifests: [...ms].sort((x, y) =>\n (resolveLabel(x.title) ?? x.id).localeCompare(resolveLabel(y.title) ?? y.id),\n ),\n }))\n}\n\n/**\n * Derive the two-level taxonomy for the sidebar. `base` precedes `project` (rail\n * order). An optional `query` filters manifests first; a namespace/category with\n * no surviving manifest is omitted, so every rendered group yields results.\n */\nexport function deriveTaxonomy(\n manifests: readonly ComponentManifest[],\n query = '',\n): TaxonomyNamespace[] {\n const matched = manifests.filter((m) => manifestMatchesQuery(m, query))\n const order: ComponentNamespace[] = ['base', 'project']\n return order\n .map((namespace) => {\n const inNamespace = matched.filter((m) => namespaceOfManifest(m) === namespace)\n return {\n namespace,\n count: inNamespace.length,\n categories: groupCategories(inNamespace),\n }\n })\n .filter((group) => group.count > 0)\n}\n\n// --- Stats (intro page) ---\n\nexport interface CategoryCount {\n /** The manifest category, or `''` for the uncategorized bucket. */\n key: string\n count: number\n}\n\n/**\n * Cheap registry stats for the intro page (EC-320): the total, the base/project\n * split, and a per-category breakdown (uncategorized last). Derived purely from\n * the manifest set — no health/implementation lookups here (those come from the\n * separate {@link import('@elytracms/studio/lib/registry').registryHealth}).\n */\nexport interface ComponentStats {\n total: number\n base: number\n project: number\n categories: CategoryCount[]\n}\n\nexport function deriveComponentStats(manifests: readonly ComponentManifest[]): ComponentStats {\n let base = 0\n for (const m of manifests) if (namespaceOfManifest(m) === 'base') base += 1\n const categories = groupCategories([...manifests]).map(\n (c): CategoryCount => ({ key: c.key, count: c.manifests.length }),\n )\n return {\n total: manifests.length,\n base,\n project: manifests.length - base,\n categories,\n }\n}\n"]}
1
+ {"version":3,"file":"taxonomy.js","sourceRoot":"","sources":["../../../src/components/components/taxonomy.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,uBAAuB,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAsBhF,oFAAoF;AACpF,MAAM,UAAU,oBAAoB,CAAC,QAA2B,EAAE,KAAa;IAC7E,MAAM,CAAC,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAA;IACpC,IAAI,CAAC,KAAK,EAAE;QAAE,OAAO,IAAI,CAAA;IACzB,MAAM,QAAQ,GAAG,CAAC,QAAQ,CAAC,EAAE,EAAE,GAAG,uBAAuB,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC,WAAW,CAAC;SAChH,MAAM,CAAC,CAAC,CAAC,EAAe,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC;SACjD,IAAI,CAAC,GAAG,CAAC;SACT,WAAW,EAAE,CAAA;IAChB,OAAO,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAA;AAC7B,CAAC;AAED,oFAAoF;AACpF,SAAS,eAAe,CAAC,SAA8B;IACrD,MAAM,UAAU,GAAG,IAAI,GAAG,EAA+B,CAAA;IACzD,KAAK,MAAM,QAAQ,IAAI,SAAS,EAAE,CAAC;QACjC,MAAM,GAAG,GAAG,QAAQ,CAAC,QAAQ,IAAI,EAAE,CAAA;QACnC,MAAM,MAAM,GAAG,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QAClC,IAAI,MAAM;YAAE,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;;YAC5B,UAAU,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAA;IACtC,CAAC;IACD,OAAO,CAAC,GAAG,UAAU,CAAC,OAAO,EAAE,CAAC;SAC7B,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE;QACjB,gEAAgE;QAChE,IAAI,CAAC,KAAK,EAAE;YAAE,OAAO,CAAC,CAAA;QACtB,IAAI,CAAC,KAAK,EAAE;YAAE,OAAO,CAAC,CAAC,CAAA;QACvB,OAAO,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAA;IAC3B,CAAC,CAAC;SACD,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC;QACnB,GAAG;QACH,SAAS,EAAE,CAAC,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAC/B,CAAC,YAAY,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,aAAa,CAAC,YAAY,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,CAC7E;KACF,CAAC,CAAC,CAAA;AACP,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAC5B,SAAuC,EACvC,KAAK,GAAG,EAAE;IAEV,OAAO,eAAe,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,oBAAoB,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,CAAA;AACjF,CAAC;AAsBD,MAAM,UAAU,oBAAoB,CAAC,SAAuC;IAC1E,MAAM,UAAU,GAAG,eAAe,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC,CAAC,GAAG,CACpD,CAAC,CAAC,EAAiB,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE,KAAK,EAAE,CAAC,CAAC,SAAS,CAAC,MAAM,EAAE,CAAC,CAClE,CAAA;IACD,OAAO,EAAE,KAAK,EAAE,SAAS,CAAC,MAAM,EAAE,UAAU,EAAE,CAAA;AAChD,CAAC","sourcesContent":["import type { ComponentManifest } from '@elytracms/core/component-registry'\nimport { localizedStringVariants, resolveLabel } from '@elytracms/core/cms-core'\n\n/**\n * EC-320 — the component registry TAXONOMY, derived purely from the metadata a\n * manifest actually declares (no invented axes): the manifest's optional\n * `category` string, with an \"uncategorized\" bucket (`key: ''`) collected last.\n *\n * EC-591: the namespace level (`base` primitives vs `project` components) is gone\n * with the id classification it read — the registry is the config's one list, and\n * an id prefix means nothing to the engine.\n *\n * Manifests carry no free-form `tags`, so category is the finest declared axis —\n * this makes the best of what's there rather than fabricating structure. Pure and\n * store-agnostic so the sidebar's grouping/search is unit-testable without React.\n */\n\nexport interface TaxonomyCategory {\n /** The manifest category, or `''` for the uncategorized bucket. */\n key: string\n manifests: ComponentManifest[]\n}\n\n/** Case-insensitive match against a manifest's id, title, category, description. */\nexport function manifestMatchesQuery(manifest: ComponentManifest, query: string): boolean {\n const q = query.trim().toLowerCase()\n if (q === '') return true\n const haystack = [manifest.id, ...localizedStringVariants(manifest.title), manifest.category, manifest.description]\n .filter((v): v is string => typeof v === 'string')\n .join(' ')\n .toLowerCase()\n return haystack.includes(q)\n}\n\n/** Group category buckets: sorted alphabetically, the uncategorized bucket last. */\nfunction groupCategories(manifests: ComponentManifest[]): TaxonomyCategory[] {\n const byCategory = new Map<string, ComponentManifest[]>()\n for (const manifest of manifests) {\n const key = manifest.category ?? ''\n const bucket = byCategory.get(key)\n if (bucket) bucket.push(manifest)\n else byCategory.set(key, [manifest])\n }\n return [...byCategory.entries()]\n .sort(([a], [b]) => {\n // Uncategorized ('') always sorts last; otherwise alphabetical.\n if (a === '') return 1\n if (b === '') return -1\n return a.localeCompare(b)\n })\n .map(([key, ms]) => ({\n key,\n manifests: [...ms].sort((x, y) =>\n (resolveLabel(x.title) ?? x.id).localeCompare(resolveLabel(y.title) ?? y.id),\n ),\n }))\n}\n\n/**\n * Derive the taxonomy for the sidebar: category groups. An optional `query`\n * filters manifests first; a category with no surviving manifest is omitted, so\n * every rendered group yields results.\n */\nexport function deriveTaxonomy(\n manifests: readonly ComponentManifest[],\n query = '',\n): TaxonomyCategory[] {\n return groupCategories(manifests.filter((m) => manifestMatchesQuery(m, query)))\n}\n\n// --- Stats (intro page) ---\n\nexport interface CategoryCount {\n /** The manifest category, or `''` for the uncategorized bucket. */\n key: string\n count: number\n}\n\n/**\n * Cheap registry stats for the intro page (EC-320): the total and a per-category\n * breakdown (uncategorized last). Derived purely from the manifest set — no\n * health/implementation lookups here (those come from the separate\n * {@link import('@elytracms/studio/lib/registry').registryHealth}). EC-591: no\n * base/project split — there is one list.\n */\nexport interface ComponentStats {\n total: number\n categories: CategoryCount[]\n}\n\nexport function deriveComponentStats(manifests: readonly ComponentManifest[]): ComponentStats {\n const categories = groupCategories([...manifests]).map(\n (c): CategoryCount => ({ key: c.key, count: c.manifests.length }),\n )\n return { total: manifests.length, categories }\n}\n"]}
@@ -57,7 +57,8 @@ export interface BuildBlockDocumentInput {
57
57
  * (EC-316) and {@link seedDefaults} so declared field defaults are present (as a
58
58
  * normal `+ New` would be). The field stores the converged v2 sequence holding
59
59
  * the one tagged root, written through the core write path (`editBlockRoots`,
60
- * EC-576) — `multiple: false` is a constraint, not a shape (EC-500 D2). The
60
+ * EC-576) — cardinality is a constraint (`validation.max`), not a shape (EC-500 D2,
61
+ * EC-581). The
61
62
  * transferred node REPLACES whatever the field's declared default seeded, as it
62
63
  * always has.
63
64
  *
@@ -75,11 +76,12 @@ export interface CanAppendToInput {
75
76
  }
76
77
  /**
77
78
  * Can `node` be appended to `document`'s blocks field without destroying
78
- * existing content? Always true for a `multiple` field (an ordered list has
79
- * room for one more); a single-root (`multiple: false`) field only while it
80
- * is empty — occupied, there is no second root to hold it without
81
- * overwriting the existing block, so the transfer dialog excludes that
82
- * document from its offered targets rather than risk data loss.
79
+ * existing content? Always true without a declared item limit (an ordered list
80
+ * has room for one more); a field limited by `validation.max` (EC-581: `max: 1` is
81
+ * the single-block field `multiple: false` used to spell) only while it is below
82
+ * the limit — at it, there is no room without overwriting an existing block, so
83
+ * the transfer dialog excludes that document from its offered targets rather than
84
+ * risk data loss.
83
85
  */
84
86
  export declare function canAppendTo({ collection, document, blocksField, locale }: CanAppendToInput): boolean;
85
87
  export interface AppendToBlockDocumentInput {
@@ -111,8 +113,8 @@ export interface AppendToBlockDocumentInput {
111
113
  * every existing root's ids ({@link embedFreshNode}), never a fixed `node-1`
112
114
  * — a document whose blocks already hold that id would otherwise collide.
113
115
  *
114
- * Throws if `blocksField` is a single-root (`multiple: false`) field that
115
- * already holds a root — a defensive invariant guard, not a user-facing
116
+ * Throws if `blocksField` already holds its declared item limit (`validation.max`,
117
+ * EC-581) — a defensive invariant guard, not a user-facing
116
118
  * validation path: the caller must pre-filter candidates with
117
119
  * {@link canAppendTo} so this is never reached in that state.
118
120
  */
@@ -1,4 +1,4 @@
1
- import { isBlocksField, isFieldLocalized, seedDefaults, writeField } from '@elytracms/core/cms-core';
1
+ import { isBlocksField, isFieldLocalized, itemLimit, seedDefaults, writeField } from '@elytracms/core/cms-core';
2
2
  import { blockRootsOf, editBlockRoots, maxNodeIdSeq } from '@elytracms/core/project-graph';
3
3
  import { makeNodeIdCounter } from '../builder/builder-store.js';
4
4
  import { mintDocumentId } from '../cms/cms-store.js';
@@ -53,7 +53,8 @@ function embedFreshNode(node, existingRoots) {
53
53
  * (EC-316) and {@link seedDefaults} so declared field defaults are present (as a
54
54
  * normal `+ New` would be). The field stores the converged v2 sequence holding
55
55
  * the one tagged root, written through the core write path (`editBlockRoots`,
56
- * EC-576) — `multiple: false` is a constraint, not a shape (EC-500 D2). The
56
+ * EC-576) — cardinality is a constraint (`validation.max`), not a shape (EC-500 D2,
57
+ * EC-581). The
57
58
  * transferred node REPLACES whatever the field's declared default seeded, as it
58
59
  * always has.
59
60
  *
@@ -74,16 +75,18 @@ function readBlocksValue(collection, document, blocksField, locale) {
74
75
  }
75
76
  /**
76
77
  * Can `node` be appended to `document`'s blocks field without destroying
77
- * existing content? Always true for a `multiple` field (an ordered list has
78
- * room for one more); a single-root (`multiple: false`) field only while it
79
- * is empty — occupied, there is no second root to hold it without
80
- * overwriting the existing block, so the transfer dialog excludes that
81
- * document from its offered targets rather than risk data loss.
78
+ * existing content? Always true without a declared item limit (an ordered list
79
+ * has room for one more); a field limited by `validation.max` (EC-581: `max: 1` is
80
+ * the single-block field `multiple: false` used to spell) only while it is below
81
+ * the limit — at it, there is no room without overwriting an existing block, so
82
+ * the transfer dialog excludes that document from its offered targets rather than
83
+ * risk data loss.
82
84
  */
83
85
  export function canAppendTo({ collection, document, blocksField, locale }) {
84
- if (blocksField.multiple)
86
+ const limit = itemLimit(blocksField);
87
+ if (limit === undefined)
85
88
  return true;
86
- return blockRootsOf(readBlocksValue(collection, document, blocksField, locale)).length === 0;
89
+ return blockRootsOf(readBlocksValue(collection, document, blocksField, locale)).length < limit;
87
90
  }
88
91
  /**
89
92
  * Append `node` into an EXISTING document's blocks field (EC-320 follow-up,
@@ -104,15 +107,16 @@ export function canAppendTo({ collection, document, blocksField, locale }) {
104
107
  * every existing root's ids ({@link embedFreshNode}), never a fixed `node-1`
105
108
  * — a document whose blocks already hold that id would otherwise collide.
106
109
  *
107
- * Throws if `blocksField` is a single-root (`multiple: false`) field that
108
- * already holds a root — a defensive invariant guard, not a user-facing
110
+ * Throws if `blocksField` already holds its declared item limit (`validation.max`,
111
+ * EC-581) — a defensive invariant guard, not a user-facing
109
112
  * validation path: the caller must pre-filter candidates with
110
113
  * {@link canAppendTo} so this is never reached in that state.
111
114
  */
112
115
  export function appendToBlockDocument({ collection, document, blocksField, node, locale, }) {
113
116
  const nextValue = editBlockRoots(readBlocksValue(collection, document, blocksField, locale), (existingRoots) => {
114
- if (!blocksField.multiple && existingRoots.length > 0) {
115
- throw new Error(`Cannot append to single-root blocks field "${blocksField.name}": already holds a component (caller must gate with canAppendTo).`);
117
+ const limit = itemLimit(blocksField);
118
+ if (limit !== undefined && existingRoots.length >= limit) {
119
+ throw new Error(`Cannot append to blocks field "${blocksField.name}": it already holds its limit of ${limit} (caller must gate with canAppendTo).`);
116
120
  }
117
121
  return [...existingRoots, embedFreshNode(node, existingRoots)];
118
122
  });
@@ -1 +1 @@
1
- {"version":3,"file":"transfer.js","sourceRoot":"","sources":["../../../src/components/components/transfer.ts"],"names":[],"mappings":"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","sourcesContent":["import { isBlocksField, isFieldLocalized, seedDefaults, writeField } from '@elytracms/core/cms-core'\nimport type { BlocksFieldDef, CmsDocument, CollectionDef, Locale, LocaleConfig } from '@elytracms/core/cms-core'\nimport { blockRootsOf, editBlockRoots, maxNodeIdSeq } from '@elytracms/core/project-graph'\nimport type { ComponentNode } from '@elytracms/core/project-graph'\nimport { makeNodeIdCounter } from '../builder/builder-store'\nimport { mintDocumentId } from '../cms/cms-store'\nimport { embedNodeFrom } from './component-preview'\n\n/**\n * EC-320 (+ 0.0.44 follow-up) — the \"design guide → real content\" bridge. A\n * component combination the user has arranged in the Components view (a\n * manifest + edited prop values, captured as a {@link ComponentNode}) is\n * transferred into a document's blocks/composition field — either a NEW DRAFT\n * document created for it ({@link buildBlockDocument}) or an EXISTING\n * document's field, appended alongside whatever it already holds\n * ({@link appendToBlockDocument}). Pure so id-minting/embedding/appending are\n * unit-testable; the React dialog owns the picker, persistence, and\n * navigation.\n */\n\nexport interface CollectionBlocksTarget {\n collection: CollectionDef\n /** The (first) blocks field the node lands in. */\n field: BlocksFieldDef\n}\n\n/**\n * Collections that can receive a transferred combination — those declaring a\n * `blocks` (composition) field. The first blocks field wins (a collection with a\n * single canvas is the norm; a second is an unusual edge we don't disambiguate).\n * Callers additionally gate each target through {@link isComponentAllowed} —\n * this only reports which collections HAVE a blocks field, not which ones\n * accept a given component.\n */\nexport function collectionsWithBlocks(\n collections: readonly CollectionDef[],\n): CollectionBlocksTarget[] {\n const targets: CollectionBlocksTarget[] = []\n for (const collection of collections) {\n const field = collection.fields.find(isBlocksField)\n if (field) targets.push({ collection, field })\n }\n return targets\n}\n\n/**\n * Is `componentId` permitted as a top-level block of `field`? Mirrors\n * `validateCompositionValue`'s (`@elytracms/core/project-graph`) top-level\n * `allow` rule EXACTLY (EC-544): an absent list permits any registered\n * component; a list is the closed vocabulary (an empty one permits nothing).\n * A target this predicate accepts can never fail that validator's\n * `forbidden-component` check on save — the transfer dialog uses this to hide\n * targets the component isn't allowed into (new AND existing document targets\n * alike).\n */\nexport function isComponentAllowed(field: BlocksFieldDef, componentId: string): boolean {\n return !field.allow || field.allow.includes(componentId)\n}\n\n/**\n * Mint a fresh `node-<n>` id for `node`, guaranteed not to collide with any id\n * already present anywhere in `existingRoots`. Reuses the SAME mint the\n * canvas reducer and clipboard-paste path share (`maxNodeIdSeq` +\n * `makeNodeIdCounter`, `builder-store.tsx`) rather than a fixed literal: a\n * brand-new document (`existingRoots` empty) mints `node-1` — matching the\n * transfer's prior behavior — but appending into a document whose blocks\n * already hold `node-1..N` mints past that, never re-using an id already in\n * the tree (the EC-320 follow-up collision hazard).\n */\nfunction embedFreshNode(node: ComponentNode, existingRoots: readonly ComponentNode[]): ComponentNode {\n const maxSeq = existingRoots.reduce((max, root) => Math.max(max, maxNodeIdSeq(root)), 0)\n return embedNodeFrom(node, makeNodeIdCounter(maxSeq).genId())\n}\n\nexport interface BuildBlockDocumentInput {\n collection: CollectionDef\n /** The blocks field the node lands in (from {@link collectionsWithBlocks}). */\n blocksField: BlocksFieldDef\n /** The component node — a manifest id + the edited static props (+ slots). */\n node: ComponentNode\n /** Every doc id already in the target collection (collision guard, EC-316). */\n existingIds: ReadonlySet<string>\n /** The session id counter (mutated by {@link mintDocumentId}). */\n seq: { current: number }\n /** The project's locale config — a new document is written in its default locale (EC-580). */\n locales: LocaleConfig\n}\n\n/**\n * Build the draft document that embeds `node` in the collection's blocks field.\n * Reuses the CMS create machinery: {@link mintDocumentId} for a collision-free id\n * (EC-316) and {@link seedDefaults} so declared field defaults are present (as a\n * normal `+ New` would be). The field stores the converged v2 sequence holding\n * the one tagged root, written through the core write path (`editBlockRoots`,\n * EC-576) — `multiple: false` is a constraint, not a shape (EC-500 D2). The\n * transferred node REPLACES whatever the field's declared default seeded, as it\n * always has.\n *\n * EC-580: both writes go through core's `writeField` in the project's DEFAULT\n * locale — a new document is created in its default bucket, so a localized blocks\n * field holds the node under `localized[default]`, never on the shared bag.\n */\nexport function buildBlockDocument({\n collection,\n blocksField,\n node,\n existingIds,\n seq,\n locales,\n}: BuildBlockDocumentInput): CmsDocument {\n const id = mintDocumentId(collection.id, seq, existingIds)\n const seeded: CmsDocument = { collection: collection.id, id, ...seedDefaults(collection, locales) }\n return writeField(\n collection,\n seeded,\n blocksField.name,\n locales.default,\n editBlockRoots(undefined, () => [embedFreshNode(node, [])]),\n )\n}\n\n/** The blocks field's current value on `document`, respecting locale (EC-018). */\nfunction readBlocksValue(\n collection: CollectionDef,\n document: CmsDocument,\n blocksField: BlocksFieldDef,\n locale: Locale,\n): unknown {\n return isFieldLocalized(collection, blocksField.name)\n ? document.localized?.[locale]?.[blocksField.name]\n : document.values[blocksField.name]\n}\n\nexport interface CanAppendToInput {\n collection: CollectionDef\n document: CmsDocument\n blocksField: BlocksFieldDef\n /** The locale being edited (EC-176 ambient active locale) for a localized field. */\n locale: Locale\n}\n\n/**\n * Can `node` be appended to `document`'s blocks field without destroying\n * existing content? Always true for a `multiple` field (an ordered list has\n * room for one more); a single-root (`multiple: false`) field only while it\n * is empty — occupied, there is no second root to hold it without\n * overwriting the existing block, so the transfer dialog excludes that\n * document from its offered targets rather than risk data loss.\n */\nexport function canAppendTo({ collection, document, blocksField, locale }: CanAppendToInput): boolean {\n if (blocksField.multiple) return true\n return blockRootsOf(readBlocksValue(collection, document, blocksField, locale)).length === 0\n}\n\nexport interface AppendToBlockDocumentInput {\n collection: CollectionDef\n /** The document to append into (the transfer dialog's \"insert into existing document\"). */\n document: CmsDocument\n blocksField: BlocksFieldDef\n /** The component node to append — not yet re-ided (fresh id minted here). */\n node: ComponentNode\n /** The locale being edited (EC-176 ambient active locale) for a localized field. */\n locale: Locale\n}\n\n/**\n * Append `node` into an EXISTING document's blocks field (EC-320 follow-up,\n * 0.0.44 — \"insert into existing document\"), leaving everything else about\n * the document untouched: other fields, other locale bundles, and the\n * field's own existing content, which the embed always lands AFTER.\n *\n * Writes through the core write path (`editBlockRoots`,\n * `@elytracms/core/project-graph`, EC-576): the new root is appended after\n * the existing roots, every typography item keeps its place and order, and\n * the value written is the v2 sequence — undefined-safe for a never-set\n * field. Locale-aware writes go through core's `writeField` (the same\n * locale-write every editor uses, EC-580) so a localized\n * blocks field's OTHER locale bundles are never touched, only the edited\n * locale's.\n *\n * Node-id collision guard: `node` is re-embedded under a fresh id minted past\n * every existing root's ids ({@link embedFreshNode}), never a fixed `node-1`\n * — a document whose blocks already hold that id would otherwise collide.\n *\n * Throws if `blocksField` is a single-root (`multiple: false`) field that\n * already holds a root — a defensive invariant guard, not a user-facing\n * validation path: the caller must pre-filter candidates with\n * {@link canAppendTo} so this is never reached in that state.\n */\nexport function appendToBlockDocument({\n collection,\n document,\n blocksField,\n node,\n locale,\n}: AppendToBlockDocumentInput): CmsDocument {\n const nextValue = editBlockRoots(readBlocksValue(collection, document, blocksField, locale), (existingRoots) => {\n if (!blocksField.multiple && existingRoots.length > 0) {\n throw new Error(\n `Cannot append to single-root blocks field \"${blocksField.name}\": already holds a component (caller must gate with canAppendTo).`,\n )\n }\n return [...existingRoots, embedFreshNode(node, existingRoots)]\n })\n return writeField(collection, document, blocksField.name, locale, nextValue)\n}\n"]}
1
+ {"version":3,"file":"transfer.js","sourceRoot":"","sources":["../../../src/components/components/transfer.ts"],"names":[],"mappings":"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","sourcesContent":["import { isBlocksField, isFieldLocalized, itemLimit, seedDefaults, writeField } from '@elytracms/core/cms-core'\nimport type { BlocksFieldDef, CmsDocument, CollectionDef, Locale, LocaleConfig } from '@elytracms/core/cms-core'\nimport { blockRootsOf, editBlockRoots, maxNodeIdSeq } from '@elytracms/core/project-graph'\nimport type { ComponentNode } from '@elytracms/core/project-graph'\nimport { makeNodeIdCounter } from '../builder/builder-store'\nimport { mintDocumentId } from '../cms/cms-store'\nimport { embedNodeFrom } from './component-preview'\n\n/**\n * EC-320 (+ 0.0.44 follow-up) — the \"design guide → real content\" bridge. A\n * component combination the user has arranged in the Components view (a\n * manifest + edited prop values, captured as a {@link ComponentNode}) is\n * transferred into a document's blocks/composition field — either a NEW DRAFT\n * document created for it ({@link buildBlockDocument}) or an EXISTING\n * document's field, appended alongside whatever it already holds\n * ({@link appendToBlockDocument}). Pure so id-minting/embedding/appending are\n * unit-testable; the React dialog owns the picker, persistence, and\n * navigation.\n */\n\nexport interface CollectionBlocksTarget {\n collection: CollectionDef\n /** The (first) blocks field the node lands in. */\n field: BlocksFieldDef\n}\n\n/**\n * Collections that can receive a transferred combination — those declaring a\n * `blocks` (composition) field. The first blocks field wins (a collection with a\n * single canvas is the norm; a second is an unusual edge we don't disambiguate).\n * Callers additionally gate each target through {@link isComponentAllowed} —\n * this only reports which collections HAVE a blocks field, not which ones\n * accept a given component.\n */\nexport function collectionsWithBlocks(\n collections: readonly CollectionDef[],\n): CollectionBlocksTarget[] {\n const targets: CollectionBlocksTarget[] = []\n for (const collection of collections) {\n const field = collection.fields.find(isBlocksField)\n if (field) targets.push({ collection, field })\n }\n return targets\n}\n\n/**\n * Is `componentId` permitted as a top-level block of `field`? Mirrors\n * `validateCompositionValue`'s (`@elytracms/core/project-graph`) top-level\n * `allow` rule EXACTLY (EC-544): an absent list permits any registered\n * component; a list is the closed vocabulary (an empty one permits nothing).\n * A target this predicate accepts can never fail that validator's\n * `forbidden-component` check on save — the transfer dialog uses this to hide\n * targets the component isn't allowed into (new AND existing document targets\n * alike).\n */\nexport function isComponentAllowed(field: BlocksFieldDef, componentId: string): boolean {\n return !field.allow || field.allow.includes(componentId)\n}\n\n/**\n * Mint a fresh `node-<n>` id for `node`, guaranteed not to collide with any id\n * already present anywhere in `existingRoots`. Reuses the SAME mint the\n * canvas reducer and clipboard-paste path share (`maxNodeIdSeq` +\n * `makeNodeIdCounter`, `builder-store.tsx`) rather than a fixed literal: a\n * brand-new document (`existingRoots` empty) mints `node-1` — matching the\n * transfer's prior behavior — but appending into a document whose blocks\n * already hold `node-1..N` mints past that, never re-using an id already in\n * the tree (the EC-320 follow-up collision hazard).\n */\nfunction embedFreshNode(node: ComponentNode, existingRoots: readonly ComponentNode[]): ComponentNode {\n const maxSeq = existingRoots.reduce((max, root) => Math.max(max, maxNodeIdSeq(root)), 0)\n return embedNodeFrom(node, makeNodeIdCounter(maxSeq).genId())\n}\n\nexport interface BuildBlockDocumentInput {\n collection: CollectionDef\n /** The blocks field the node lands in (from {@link collectionsWithBlocks}). */\n blocksField: BlocksFieldDef\n /** The component node — a manifest id + the edited static props (+ slots). */\n node: ComponentNode\n /** Every doc id already in the target collection (collision guard, EC-316). */\n existingIds: ReadonlySet<string>\n /** The session id counter (mutated by {@link mintDocumentId}). */\n seq: { current: number }\n /** The project's locale config — a new document is written in its default locale (EC-580). */\n locales: LocaleConfig\n}\n\n/**\n * Build the draft document that embeds `node` in the collection's blocks field.\n * Reuses the CMS create machinery: {@link mintDocumentId} for a collision-free id\n * (EC-316) and {@link seedDefaults} so declared field defaults are present (as a\n * normal `+ New` would be). The field stores the converged v2 sequence holding\n * the one tagged root, written through the core write path (`editBlockRoots`,\n * EC-576) — cardinality is a constraint (`validation.max`), not a shape (EC-500 D2,\n * EC-581). The\n * transferred node REPLACES whatever the field's declared default seeded, as it\n * always has.\n *\n * EC-580: both writes go through core's `writeField` in the project's DEFAULT\n * locale — a new document is created in its default bucket, so a localized blocks\n * field holds the node under `localized[default]`, never on the shared bag.\n */\nexport function buildBlockDocument({\n collection,\n blocksField,\n node,\n existingIds,\n seq,\n locales,\n}: BuildBlockDocumentInput): CmsDocument {\n const id = mintDocumentId(collection.id, seq, existingIds)\n const seeded: CmsDocument = { collection: collection.id, id, ...seedDefaults(collection, locales) }\n return writeField(\n collection,\n seeded,\n blocksField.name,\n locales.default,\n editBlockRoots(undefined, () => [embedFreshNode(node, [])]),\n )\n}\n\n/** The blocks field's current value on `document`, respecting locale (EC-018). */\nfunction readBlocksValue(\n collection: CollectionDef,\n document: CmsDocument,\n blocksField: BlocksFieldDef,\n locale: Locale,\n): unknown {\n return isFieldLocalized(collection, blocksField.name)\n ? document.localized?.[locale]?.[blocksField.name]\n : document.values[blocksField.name]\n}\n\nexport interface CanAppendToInput {\n collection: CollectionDef\n document: CmsDocument\n blocksField: BlocksFieldDef\n /** The locale being edited (EC-176 ambient active locale) for a localized field. */\n locale: Locale\n}\n\n/**\n * Can `node` be appended to `document`'s blocks field without destroying\n * existing content? Always true without a declared item limit (an ordered list\n * has room for one more); a field limited by `validation.max` (EC-581: `max: 1` is\n * the single-block field `multiple: false` used to spell) only while it is below\n * the limit — at it, there is no room without overwriting an existing block, so\n * the transfer dialog excludes that document from its offered targets rather than\n * risk data loss.\n */\nexport function canAppendTo({ collection, document, blocksField, locale }: CanAppendToInput): boolean {\n const limit = itemLimit(blocksField)\n if (limit === undefined) return true\n return blockRootsOf(readBlocksValue(collection, document, blocksField, locale)).length < limit\n}\n\nexport interface AppendToBlockDocumentInput {\n collection: CollectionDef\n /** The document to append into (the transfer dialog's \"insert into existing document\"). */\n document: CmsDocument\n blocksField: BlocksFieldDef\n /** The component node to append — not yet re-ided (fresh id minted here). */\n node: ComponentNode\n /** The locale being edited (EC-176 ambient active locale) for a localized field. */\n locale: Locale\n}\n\n/**\n * Append `node` into an EXISTING document's blocks field (EC-320 follow-up,\n * 0.0.44 — \"insert into existing document\"), leaving everything else about\n * the document untouched: other fields, other locale bundles, and the\n * field's own existing content, which the embed always lands AFTER.\n *\n * Writes through the core write path (`editBlockRoots`,\n * `@elytracms/core/project-graph`, EC-576): the new root is appended after\n * the existing roots, every typography item keeps its place and order, and\n * the value written is the v2 sequence — undefined-safe for a never-set\n * field. Locale-aware writes go through core's `writeField` (the same\n * locale-write every editor uses, EC-580) so a localized\n * blocks field's OTHER locale bundles are never touched, only the edited\n * locale's.\n *\n * Node-id collision guard: `node` is re-embedded under a fresh id minted past\n * every existing root's ids ({@link embedFreshNode}), never a fixed `node-1`\n * — a document whose blocks already hold that id would otherwise collide.\n *\n * Throws if `blocksField` already holds its declared item limit (`validation.max`,\n * EC-581) — a defensive invariant guard, not a user-facing\n * validation path: the caller must pre-filter candidates with\n * {@link canAppendTo} so this is never reached in that state.\n */\nexport function appendToBlockDocument({\n collection,\n document,\n blocksField,\n node,\n locale,\n}: AppendToBlockDocumentInput): CmsDocument {\n const nextValue = editBlockRoots(readBlocksValue(collection, document, blocksField, locale), (existingRoots) => {\n const limit = itemLimit(blocksField)\n if (limit !== undefined && existingRoots.length >= limit) {\n throw new Error(\n `Cannot append to blocks field \"${blocksField.name}\": it already holds its limit of ${limit} (caller must gate with canAppendTo).`,\n )\n }\n return [...existingRoots, embedFreshNode(node, existingRoots)]\n })\n return writeField(collection, document, blocksField.name, locale, nextValue)\n}\n"]}
@@ -12,17 +12,25 @@ export declare const INCOMPLETE_CONSTRAINTS_NOTE = "further validation rules app
12
12
  * note with a worked example — and it rides every date field of every turn.
13
13
  */
14
14
  export declare const ISO_DATE_NOTE = "ISO 8601 date";
15
+ /**
16
+ * The phrase for a field's declared `min`/`max` in the UNIT its type declares
17
+ * (`FIELD_TYPES[type].bounds`, EC-581) — or `undefined` when the field declares none
18
+ * or the unit has a JSON-Schema keyword the projection already carries. Today that is
19
+ * rich text: its bounds count PLAIN-TEXT characters of the stored value (ratified
20
+ * 2a(iii)), which no keyword on the markdown string the model writes can express.
21
+ */
22
+ export declare function plainTextBoundPhrase(field: FieldDef): string | undefined;
15
23
  /**
16
24
  * A rich-text field's WRITER form (EC-379, D2 of EC-556): a MARKDOWN string. The
17
25
  * description carries the allowed subset (from the engine converter — one source
18
26
  * of truth); the engine converts the markdown into the v2 storage value and
19
27
  * rejects out-of-subset constructs with explicit issues.
20
28
  *
21
- * The declared `min` / `max` / `pattern` are read straight off `field.validation`
22
- * — deliberately NOT spread from the projection. Since EC-556 the projection is
23
- * validator-facing: a rich-text value at rest is the `{ version: 2, content }`
24
- * object, where a string bound has no place. The model is still told the bounds
25
- * it was told before (whether anything enforces them on v2 is EC-563).
29
+ * EC-563/EC-581: the declared bounds are said in their unit — plain-text characters,
30
+ * markup not counted — in the description, read off the per-FieldType definition.
31
+ * They used to ride as `minLength`/`maxLength` on the markdown string: a different
32
+ * unit (markdown characters) than anything enforced, and a provider validating the
33
+ * structured output would have enforced the wrong one.
26
34
  */
27
35
  export declare function richTextWriterSchema(field: FieldDef): JsonSchema;
28
36
  /**
@@ -52,19 +60,13 @@ export declare function writerFieldSchema(field: FieldDef): JsonSchema;
52
60
  * same object when there is nothing to strip, so callers can rely on identity.
53
61
  */
54
62
  export declare function stripRedundantPattern(schema: JsonSchema): JsonSchema;
55
- /**
56
- * Narrow a date field's `['string','number']` union to the ISO string the model
57
- * should author (EC-505 — see the module note). Keyed on the exact combination the
58
- * date branch of the projection emits (`format: 'date-time'` + an array `type`
59
- * containing `string`), so nothing else is touched. Identity-preserving.
60
- */
61
- export declare function narrowDateUnion(schema: JsonSchema): JsonSchema;
62
63
  /** Recursively strip redundant patterns through `items` / `properties`. */
63
64
  export declare function stripRedundantPatternsDeep(schema: JsonSchema): JsonSchema;
64
65
  /**
65
66
  * The whole model-facing rewrite of a projected schema (EC-505): redundant patterns
66
- * stripped and date unions narrowed, everywhere in the tree — an `object` field's
67
- * date SUBfield gets the same treatment as a top-level one. Identity-preserving.
67
+ * stripped everywhere in the tree — an `object` field's SUBfield gets the same
68
+ * treatment as a top-level one. Identity-preserving. (The date-union narrowing that
69
+ * also ran here is gone: a date is one instant string at rest since EC-586.)
68
70
  */
69
71
  export declare function toModelFacingSchema(schema: JsonSchema): JsonSchema;
70
72
  /**
@@ -13,22 +13,18 @@
13
13
  * EC-474 §3); `format` + bounds says the same thing in four tokens. So on the AI
14
14
  * path a `pattern` is dropped wherever a `format` is present. The JSON Schema
15
15
  * OUTPUT artifact keeps it — different consumer, different needs.
16
- * - **a union `type` is a poor instruction too** (EC-505). A `date` field accepts
17
- * an ISO string OR an epoch number *at rest*, so the general projection honestly
18
- * says `type: ['string','number']`. The model does not need that latitude: it is
19
- * asked to AUTHOR a date, EC-472 ratified that a date is an ISO instant, and the
20
- * declared bounds this same projection carries (`formatMinimum`/`formatMaximum`)
21
- * are ISO strings that mean nothing against an epoch. Some structured-output
22
- * providers also reject a union `type` outright. So the model-facing shape says
23
- * `type: 'string'`; accepting a number stays an input-parsing detail of the
24
- * validator, not a shape the model is taught.
16
+ * - **a date is one instant string** (EC-505 → EC-586). The projection used to say
17
+ * `type: ['string','number']` (an epoch was stored as-is) and this module narrowed
18
+ * it for the model; since EC-586 every write canonicalises a date to ONE ISO-8601
19
+ * UTC instant, so the projection itself says `type: 'string'` and there is nothing
20
+ * left to narrow.
25
21
  * - **incompleteness must be stated.** When `validation.schemaComplete` is `false`
26
22
  * the projection is a partial view of the rule, and the model is told so rather
27
23
  * than being handed a half-truth as the whole rule. (An `ai.hint` is exactly the
28
24
  * right place to say what the dropped refinement wants — the note nudges toward
29
25
  * writing one.)
30
26
  */
31
- import { fieldJsonSchema } from '@elytracms/core/cms-core';
27
+ import { BOUND_UNIT_NOUN, fieldJsonSchema, fieldTypeOf } from '@elytracms/core/cms-core';
32
28
  import { MARKDOWN_PROSE_DESCRIPTION } from '@elytracms/core/rich-text';
33
29
  /** The note appended when a field's constraint projection is knowingly partial. */
34
30
  export const INCOMPLETE_CONSTRAINTS_NOTE = 'further validation rules apply that are not expressible here — keep the value conventional and plausible';
@@ -46,27 +42,52 @@ export const ISO_DATE_NOTE = 'ISO 8601 date';
46
42
  function isSchemaNode(value) {
47
43
  return typeof value === 'object' && value !== null && !Array.isArray(value);
48
44
  }
45
+ /**
46
+ * The phrase for a field's declared `min`/`max` in the UNIT its type declares
47
+ * (`FIELD_TYPES[type].bounds`, EC-581) — or `undefined` when the field declares none
48
+ * or the unit has a JSON-Schema keyword the projection already carries. Today that is
49
+ * rich text: its bounds count PLAIN-TEXT characters of the stored value (ratified
50
+ * 2a(iii)), which no keyword on the markdown string the model writes can express.
51
+ */
52
+ export function plainTextBoundPhrase(field) {
53
+ if (fieldTypeOf(field).bounds(field) !== 'plain-text')
54
+ return undefined;
55
+ const min = typeof field.validation?.min === 'number' ? field.validation.min : undefined;
56
+ const max = typeof field.validation?.max === 'number' ? field.validation.max : undefined;
57
+ const noun = BOUND_UNIT_NOUN['plain-text'];
58
+ if (min !== undefined && max !== undefined)
59
+ return `${min}–${max} ${noun}`;
60
+ if (min !== undefined)
61
+ return `at least ${min} ${noun}`;
62
+ if (max !== undefined)
63
+ return `at most ${max} ${noun}`;
64
+ return undefined;
65
+ }
66
+ /** {@link plainTextBoundPhrase} for a field and every sub-field below it, name-prefixed. */
67
+ function plainTextBoundClauses(field, prefix = '') {
68
+ const own = plainTextBoundPhrase(field);
69
+ const out = own !== undefined ? [`${prefix}${own}`] : [];
70
+ const subs = field.type === 'object' ? field.fields : field.type === 'asset' ? (field.fields ?? []) : [];
71
+ for (const sub of subs)
72
+ out.push(...plainTextBoundClauses(sub, `${prefix}${sub.name}: `));
73
+ return out;
74
+ }
49
75
  /**
50
76
  * A rich-text field's WRITER form (EC-379, D2 of EC-556): a MARKDOWN string. The
51
77
  * description carries the allowed subset (from the engine converter — one source
52
78
  * of truth); the engine converts the markdown into the v2 storage value and
53
79
  * rejects out-of-subset constructs with explicit issues.
54
80
  *
55
- * The declared `min` / `max` / `pattern` are read straight off `field.validation`
56
- * — deliberately NOT spread from the projection. Since EC-556 the projection is
57
- * validator-facing: a rich-text value at rest is the `{ version: 2, content }`
58
- * object, where a string bound has no place. The model is still told the bounds
59
- * it was told before (whether anything enforces them on v2 is EC-563).
81
+ * EC-563/EC-581: the declared bounds are said in their unit — plain-text characters,
82
+ * markup not counted — in the description, read off the per-FieldType definition.
83
+ * They used to ride as `minLength`/`maxLength` on the markdown string: a different
84
+ * unit (markdown characters) than anything enforced, and a provider validating the
85
+ * structured output would have enforced the wrong one.
60
86
  */
61
87
  export function richTextWriterSchema(field) {
62
88
  const schema = { type: 'string' };
63
- if (typeof field.validation?.min === 'number')
64
- schema.minLength = field.validation.min;
65
- if (typeof field.validation?.max === 'number')
66
- schema.maxLength = field.validation.max;
67
- if (field.validation?.pattern !== undefined)
68
- schema.pattern = field.validation.pattern;
69
- schema.description = `Rich text, written as ${MARKDOWN_PROSE_DESCRIPTION} Write genuine, publishable prose.`;
89
+ const bound = plainTextBoundPhrase(field);
90
+ schema.description = `Rich text, written as ${MARKDOWN_PROSE_DESCRIPTION} Write genuine, publishable prose.${bound ? ` Length: ${bound}.` : ''}`;
70
91
  return schema;
71
92
  }
72
93
  /**
@@ -93,21 +114,26 @@ export function richTextWriterSchema(field) {
93
114
  export function writerFieldSchema(field) {
94
115
  return writerForm(field, fieldJsonSchema(field));
95
116
  }
117
+ /** EC-581: the writer form is DECLARED per type (`FIELD_TYPES[type].writer`); built here. */
96
118
  function writerForm(field, base) {
97
- switch (field.type) {
98
- case 'richText':
119
+ switch (fieldTypeOf(field).writer) {
120
+ case 'markdown':
99
121
  return richTextWriterSchema(field);
100
- case 'asset': {
122
+ case 'placement': {
123
+ if (field.type !== 'asset')
124
+ return base;
101
125
  // `type`/`id` belong to the envelope; the projection never lets a sub-field
102
126
  // of that name shadow them, and neither does the writer form.
103
127
  const subfields = (field.fields ?? []).filter((sub) => sub.name !== 'type' && sub.name !== 'id');
104
128
  return perItem(field.multiple, base, (item) => subfields.length === 0 ? bareAssetId(item) : writerObject(subfields, item));
105
129
  }
106
- case 'relation':
107
- return perItem(field.multiple, base, bareReference);
130
+ case 'reference':
131
+ return field.type === 'relation' ? perItem(field.multiple, base, bareReference) : base;
108
132
  case 'object':
109
- return perItem(field.multiple, base, (item) => writerObject(field.fields, item));
110
- default:
133
+ return field.type === 'object'
134
+ ? perItem(field.multiple, base, (item) => writerObject(field.fields, item))
135
+ : base;
136
+ case 'value':
111
137
  return base;
112
138
  }
113
139
  }
@@ -153,20 +179,6 @@ export function stripRedundantPattern(schema) {
153
179
  const { pattern: _pattern, ...rest } = schema;
154
180
  return rest;
155
181
  }
156
- /**
157
- * Narrow a date field's `['string','number']` union to the ISO string the model
158
- * should author (EC-505 — see the module note). Keyed on the exact combination the
159
- * date branch of the projection emits (`format: 'date-time'` + an array `type`
160
- * containing `string`), so nothing else is touched. Identity-preserving.
161
- */
162
- export function narrowDateUnion(schema) {
163
- if (schema.format !== 'date-time')
164
- return schema;
165
- const type = schema.type;
166
- if (!Array.isArray(type) || !type.includes('string'))
167
- return schema;
168
- return { ...schema, type: 'string' };
169
- }
170
182
  /**
171
183
  * Apply a node transform through a schema tree (`items` + `properties`), preserving
172
184
  * object identity wherever nothing changed — `buildTargetShape` relies on that to
@@ -206,11 +218,12 @@ export function stripRedundantPatternsDeep(schema) {
206
218
  }
207
219
  /**
208
220
  * The whole model-facing rewrite of a projected schema (EC-505): redundant patterns
209
- * stripped and date unions narrowed, everywhere in the tree — an `object` field's
210
- * date SUBfield gets the same treatment as a top-level one. Identity-preserving.
221
+ * stripped everywhere in the tree — an `object` field's SUBfield gets the same
222
+ * treatment as a top-level one. Identity-preserving. (The date-union narrowing that
223
+ * also ran here is gone: a date is one instant string at rest since EC-586.)
211
224
  */
212
225
  export function toModelFacingSchema(schema) {
213
- return mapSchemaDeep(schema, (node) => narrowDateUnion(stripRedundantPattern(node)));
226
+ return mapSchemaDeep(schema, stripRedundantPattern);
214
227
  }
215
228
  /**
216
229
  * Human-readable phrases for the constraint keywords of ONE schema node, in a stable
@@ -322,9 +335,12 @@ export function describeFieldConstraints(field) {
322
335
  const out = [];
323
336
  if (field.validation?.required === true)
324
337
  out.push('required');
325
- // EC-556: the prose tells a WRITER what to write, so it reads the writer form —
326
- // a rich-text field's character bounds live there, not on the v2 value.
338
+ // EC-556: the prose tells a WRITER what to write, so it reads the writer form.
327
339
  out.push(...describeSchemaDeep(writerFieldSchema(field)));
340
+ // EC-581: a bound whose unit has no schema keyword — rich text's plain-text
341
+ // characters — is said in its unit, read off the per-FieldType definition, at every
342
+ // depth the writer form reaches (`note: at most 400 plain-text characters …`).
343
+ out.push(...plainTextBoundClauses(field));
328
344
  if (field.validation?.unique === true)
329
345
  out.push('unique across the collection');
330
346
  // The one thing the projection cannot say about itself: that it is partial (an