@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.
- package/convex/asset-revalidate.test.ts +14 -1
- package/convex/cms.ts +3 -4
- package/convex/document-revalidate.test.ts +14 -1
- package/convex/redirect-revalidate.test.ts +14 -1
- package/dist/components/assets/asset-selection.d.ts +8 -9
- package/dist/components/assets/asset-selection.js +26 -16
- package/dist/components/assets/asset-selection.js.map +1 -1
- package/dist/components/builder/blocks-model.d.ts +7 -5
- package/dist/components/builder/blocks-model.js +11 -9
- package/dist/components/builder/blocks-model.js.map +1 -1
- package/dist/components/builder/blocks-panel.js +1 -1
- package/dist/components/builder/blocks-panel.js.map +1 -1
- package/dist/components/builder/builder-store.d.ts +11 -3
- package/dist/components/builder/builder-store.js +12 -5
- package/dist/components/builder/builder-store.js.map +1 -1
- package/dist/components/builder/canvas-dnd.d.ts +7 -1
- package/dist/components/builder/canvas-dnd.js +6 -3
- package/dist/components/builder/canvas-dnd.js.map +1 -1
- package/dist/components/builder/canvas-render-context.d.ts +13 -1
- package/dist/components/builder/canvas-render-context.js +28 -18
- package/dist/components/builder/canvas-render-context.js.map +1 -1
- package/dist/components/builder/canvas.js +1 -1
- package/dist/components/builder/canvas.js.map +1 -1
- package/dist/components/builder/clipboard-actions.d.ts +0 -6
- package/dist/components/builder/clipboard-actions.js +9 -4
- package/dist/components/builder/clipboard-actions.js.map +1 -1
- package/dist/components/builder/composition-field-editor.d.ts +22 -0
- package/dist/components/builder/composition-field-editor.js +47 -8
- package/dist/components/builder/composition-field-editor.js.map +1 -1
- package/dist/components/builder/inline-edit.d.ts +7 -4
- package/dist/components/builder/inline-edit.js +11 -8
- package/dist/components/builder/inline-edit.js.map +1 -1
- package/dist/components/builder/insert-context.js +9 -5
- package/dist/components/builder/insert-context.js.map +1 -1
- package/dist/components/builder/layout-inspector.d.ts +2 -2
- package/dist/components/builder/layout-inspector.js +8 -6
- package/dist/components/builder/layout-inspector.js.map +1 -1
- package/dist/components/builder/node-factory.d.ts +31 -14
- package/dist/components/builder/node-factory.js +73 -93
- package/dist/components/builder/node-factory.js.map +1 -1
- package/dist/components/builder/page-tree.d.ts +3 -1
- package/dist/components/builder/page-tree.js +6 -4
- package/dist/components/builder/page-tree.js.map +1 -1
- package/dist/components/builder/property-inspector.js +2 -1
- package/dist/components/builder/property-inspector.js.map +1 -1
- package/dist/components/builder/reuse.d.ts +1 -1
- package/dist/components/builder/reuse.js +8 -5
- package/dist/components/builder/reuse.js.map +1 -1
- package/dist/components/builder/selection-overlay.d.ts +2 -2
- package/dist/components/builder/selection-overlay.js +3 -3
- package/dist/components/builder/selection-overlay.js.map +1 -1
- package/dist/components/builder/spacing-handles.d.ts +2 -2
- package/dist/components/builder/spacing-handles.js +3 -3
- package/dist/components/builder/spacing-handles.js.map +1 -1
- package/dist/components/cms/asset-field.d.ts +1 -1
- package/dist/components/cms/asset-field.js +15 -13
- package/dist/components/cms/asset-field.js.map +1 -1
- package/dist/components/cms/asset-picker-core.js +3 -4
- package/dist/components/cms/asset-picker-core.js.map +1 -1
- package/dist/components/cms/detail-editor.js +10 -5
- package/dist/components/cms/detail-editor.js.map +1 -1
- package/dist/components/cms/discard-draft.d.ts +1 -2
- package/dist/components/cms/discard-draft.js +1 -1
- package/dist/components/cms/discard-draft.js.map +1 -1
- package/dist/components/cms/editable-cell.js +11 -7
- package/dist/components/cms/editable-cell.js.map +1 -1
- package/dist/components/cms/field-control-lib.d.ts +10 -0
- package/dist/components/cms/field-control-lib.js +20 -0
- package/dist/components/cms/field-control-lib.js.map +1 -1
- package/dist/components/cms/field-control.js +3 -3
- package/dist/components/cms/field-control.js.map +1 -1
- package/dist/components/cms/object-array-field.js +3 -5
- package/dist/components/cms/object-array-field.js.map +1 -1
- package/dist/components/cms/prop-fields.d.ts +16 -3
- package/dist/components/cms/prop-fields.js +57 -12
- package/dist/components/cms/prop-fields.js.map +1 -1
- package/dist/components/cms/rich-text/embed-props.d.ts +6 -1
- package/dist/components/cms/rich-text/embed-props.js +7 -12
- package/dist/components/cms/rich-text/embed-props.js.map +1 -1
- package/dist/components/cms/rich-text/insert.d.ts +13 -6
- package/dist/components/cms/rich-text/insert.js +16 -17
- package/dist/components/cms/rich-text/insert.js.map +1 -1
- package/dist/components/cms/use-cell-edit.js +7 -12
- package/dist/components/cms/use-cell-edit.js.map +1 -1
- package/dist/components/cms/use-discard-controller.d.ts +1 -2
- package/dist/components/cms/use-discard-controller.js +1 -2
- package/dist/components/cms/use-discard-controller.js.map +1 -1
- package/dist/components/cms/versions-pane.js +2 -2
- package/dist/components/cms/versions-pane.js.map +1 -1
- package/dist/components/components/component-detail.d.ts +1 -1
- package/dist/components/components/component-detail.js +2 -4
- package/dist/components/components/component-detail.js.map +1 -1
- package/dist/components/components/component-preview.js +9 -4
- package/dist/components/components/component-preview.js.map +1 -1
- package/dist/components/components/components-index.d.ts +2 -2
- package/dist/components/components/components-index.js +3 -6
- package/dist/components/components/components-index.js.map +1 -1
- package/dist/components/components/field-inspector.js +5 -2
- package/dist/components/components/field-inspector.js.map +1 -1
- package/dist/components/components/registry-browser.d.ts +6 -5
- package/dist/components/components/registry-browser.js +11 -37
- package/dist/components/components/registry-browser.js.map +1 -1
- package/dist/components/components/taxonomy.d.ts +14 -21
- package/dist/components/components/taxonomy.js +5 -29
- package/dist/components/components/taxonomy.js.map +1 -1
- package/dist/components/components/transfer.d.ts +10 -8
- package/dist/components/components/transfer.js +17 -13
- package/dist/components/components/transfer.js.map +1 -1
- package/dist/lib/ai/constraints.d.ts +16 -14
- package/dist/lib/ai/constraints.js +64 -48
- package/dist/lib/ai/constraints.js.map +1 -1
- package/dist/lib/ai/update-fields.js +5 -3
- package/dist/lib/ai/update-fields.js.map +1 -1
- package/dist/lib/assets/asset-placements.d.ts +3 -10
- package/dist/lib/assets/asset-placements.js +17 -8
- package/dist/lib/assets/asset-placements.js.map +1 -1
- package/dist/lib/clipboard/paste-target.js +3 -3
- package/dist/lib/clipboard/paste-target.js.map +1 -1
- package/dist/lib/cms-views/view-model.js +11 -8
- package/dist/lib/cms-views/view-model.js.map +1 -1
- package/dist/lib/data/collection-content.d.ts +10 -7
- package/dist/lib/data/collection-content.js +34 -15
- package/dist/lib/data/collection-content.js.map +1 -1
- package/dist/lib/dnd/drop-rules.d.ts +22 -7
- package/dist/lib/dnd/drop-rules.js +21 -14
- package/dist/lib/dnd/drop-rules.js.map +1 -1
- package/dist/lib/engine-version.d.ts +1 -1
- package/dist/lib/engine-version.js +1 -1
- package/dist/lib/engine-version.js.map +1 -1
- package/dist/lib/i18n/messages.de.js +25 -9
- package/dist/lib/i18n/messages.de.js.map +1 -1
- package/dist/lib/i18n/messages.en.d.ts +21 -7
- package/dist/lib/i18n/messages.en.js +28 -9
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/operations/cms-persistence.js +7 -4
- package/dist/lib/operations/cms-persistence.js.map +1 -1
- package/dist/lib/persona/labels.d.ts +4 -9
- package/dist/lib/persona/labels.js +5 -12
- package/dist/lib/persona/labels.js.map +1 -1
- package/dist/lib/references/reference-model.d.ts +0 -2
- package/dist/lib/references/reference-model.js +3 -5
- package/dist/lib/references/reference-model.js.map +1 -1
- package/dist/lib/registry/__fixtures__/demo-components.d.ts +32 -0
- package/dist/lib/registry/{project-components.js → __fixtures__/demo-components.js} +18 -5
- package/dist/lib/registry/__fixtures__/demo-components.js.map +1 -0
- package/dist/lib/registry/index.d.ts +9 -13
- package/dist/lib/registry/index.js +34 -61
- package/dist/lib/registry/index.js.map +1 -1
- package/dist/lib/registry/preview.d.ts +22 -7
- package/dist/lib/registry/preview.js +26 -89
- package/dist/lib/registry/preview.js.map +1 -1
- package/dist/previews/enumerate.d.ts +23 -27
- package/dist/previews/enumerate.js +27 -48
- package/dist/previews/enumerate.js.map +1 -1
- package/dist/previews/harness.js +1 -1
- package/dist/previews/harness.js.map +1 -1
- package/package.json +4 -3
- package/dist/lib/registry/project-components.d.ts +0 -16
- 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":";AAAA,OAAO,EAAE,IAAI,EAAE,oBAAoB,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAA;AACjF,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAA;AACrC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAEzC,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAA;AACnF,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AACtE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAA;AACzD,OAAO,EAAE,EAAE,EAAE,MAAM,6BAA6B,CAAA;AAChD,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AACvD,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAE3C;;;;;;;GAOG;AACH,MAAM,UAAU,eAAe,CAAC,EAC9B,SAAS,EACT,iBAAiB,GAIlB;IACC,MAAM,MAAM,GAAG,iBAAiB,EAAE,CAAA;IAClC,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAC,EAAE,CAAC,CAAA;IAEtC,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAA;IACjE,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,cAAc,CAAC,SAAS,EAAE,KAAK,CAAC,EAAE,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC,CAAA;IACtF,MAAM,KAAK,GAAG,UAAU,CAAC,MAAM,KAAK,CAAC,CAAA;IAErC,OAAO,CACL,eAAK,SAAS,EAAC,8BAA8B,aAC3C,cAAK,SAAS,EAAC,+CAA+C,YAC5D,eAAK,SAAS,EAAC,UAAU,aACvB,KAAC,MAAM,IAAC,SAAS,EAAC,+FAA+F,GAAG,EACpH,KAAC,KAAK,IACJ,KAAK,EAAE,KAAK,EACZ,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EACzC,WAAW,EAAE,SAAS,CAAC,sCAAsC,CAAC,EAC9D,SAAS,EAAC,MAAM,gBACJ,SAAS,CAAC,+BAA+B,CAAC,GACtD,IACE,GACF,EAEN,cAAK,SAAS,EAAC,kCAAkC,YAC9C,KAAK,CAAC,CAAC,CAAC,CACP,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,EAAC,SAAS,EAAC,KAAK,YACzC,KAAK,CAAC,IAAI,EAAE;wBACX,CAAC,CAAC,SAAS,CAAC,iCAAiC,EAAE,EAAE,KAAK,EAAE,CAAC;wBACzD,CAAC,CAAC,SAAS,CAAC,mCAAmC,CAAC,GAC7C,CACR,CAAC,CAAC,CAAC,CACF,KAAC,KAAK,IAAC,GAAG,EAAC,GAAG,YACX,UAAU,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,CAC5B,MAAC,KAAK,IAA2C,GAAG,EAAC,GAAG,aACtD,KAAC,IAAI,IACH,IAAI,EAAC,IAAI,EACT,MAAM,EAAC,QAAQ,EACf,IAAI,EAAC,QAAQ,EACb,SAAS,EAAE,EAAE,CAAC,8BAA8B,EAAE,QAAQ,CAAC,GAAG,IAAI,YAAY,CAAC,YAE1E,QAAQ,CAAC,GAAG,IAAI,SAAS,CAAC,mCAAmC,CAAC,GAC1D,EACP,KAAC,KAAK,IAAC,GAAG,EAAC,IAAI,YACZ,QAAQ,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAC7B,KAAC,YAAY,IAEX,QAAQ,EAAE,CAAC,EACX,SAAS,EAAE,SAAS,EACpB,MAAM,EAAE,CAAC,CAAC,EAAE,KAAK,iBAAiB,IAH7B,CAAC,CAAC,EAAE,CAIT,CACH,CAAC,GACI,KAlBE,QAAQ,CAAC,GAAG,IAAI,mBAAmB,CAmBvC,CACT,CAAC,GACI,CACT,GACG,IACF,CACP,CAAA;AACH,CAAC;AAED,SAAS,YAAY,CAAC,EACpB,QAAQ,EACR,SAAS,EACT,MAAM,GAKP;IACC,MAAM,MAAM,GAAG,iBAAiB,EAAE,CAAA;IAClC,MAAM,MAAM,GAAG,eAAe,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAA;IAChD,OAAO,CACL,MAAC,IAAI,IACH,EAAE,EAAE,oBAAoB,EACxB,MAAM,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,CAAC,SAAS,CAAC,QAAQ,CAAC,EAAE,CAAC,EAAE,kBAClD,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,EACzC,SAAS,EAAE,EAAE,CACX,0EAA0E,EAC1E,MAAM;YACJ,CAAC,CAAC,kCAAkC;YACpC,CAAC,CAAC,4DAA4D,CACjE,aAED,KAAC,kBAAkB,IAAC,MAAM,EAAE,MAAM,GAAI,EACtC,eAAM,SAAS,EAAC,qDAAqD,YAClE,cAAc,CAAC,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC,EAAE,CAAC,GACvC,IACF,CACR,CAAA;AACH,CAAC","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)
|
|
4
|
+
* manifest actually declares (no invented axes): the manifest's optional
|
|
5
|
+
* `category` string, with an "uncategorized" bucket (`key: ''`) collected last.
|
|
5
6
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
|
30
|
-
*
|
|
31
|
-
*
|
|
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):
|
|
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
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
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
|
|
44
|
-
*
|
|
45
|
-
*
|
|
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
|
-
|
|
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":"
|
|
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) —
|
|
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
|
|
79
|
-
* room for one more); a
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
* document from its offered targets rather than
|
|
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`
|
|
115
|
-
*
|
|
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) —
|
|
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
|
|
78
|
-
* room for one more); a
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
* document from its offered targets rather than
|
|
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
|
-
|
|
86
|
+
const limit = itemLimit(blocksField);
|
|
87
|
+
if (limit === undefined)
|
|
85
88
|
return true;
|
|
86
|
-
return blockRootsOf(readBlocksValue(collection, document, blocksField, locale)).length
|
|
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`
|
|
108
|
-
*
|
|
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
|
-
|
|
115
|
-
|
|
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
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
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
|
|
67
|
-
*
|
|
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
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
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
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
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
|
-
|
|
64
|
-
|
|
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.
|
|
98
|
-
case '
|
|
119
|
+
switch (fieldTypeOf(field).writer) {
|
|
120
|
+
case 'markdown':
|
|
99
121
|
return richTextWriterSchema(field);
|
|
100
|
-
case '
|
|
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 '
|
|
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
|
|
110
|
-
|
|
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
|
|
210
|
-
*
|
|
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,
|
|
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
|