@elytracms/studio 0.0.63 → 0.0.65

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 (146) hide show
  1. package/convex/assets.ts +11 -0
  2. package/convex/delivery.ts +5 -0
  3. package/convex/schema.ts +23 -0
  4. package/dist/components/assets/asset-thumb.d.ts +4 -2
  5. package/dist/components/assets/asset-thumb.js +26 -4
  6. package/dist/components/assets/asset-thumb.js.map +1 -1
  7. package/dist/components/assets/assets-module.js +48 -2
  8. package/dist/components/assets/assets-module.js.map +1 -1
  9. package/dist/components/assets/assets-store.d.ts +11 -1
  10. package/dist/components/assets/assets-store.js +18 -1
  11. package/dist/components/assets/assets-store.js.map +1 -1
  12. package/dist/components/builder/blocks-model.d.ts +11 -0
  13. package/dist/components/builder/blocks-model.js +26 -8
  14. package/dist/components/builder/blocks-model.js.map +1 -1
  15. package/dist/components/builder/canvas-chrome-css.d.ts +54 -0
  16. package/dist/components/builder/canvas-chrome-css.js +246 -0
  17. package/dist/components/builder/canvas-chrome-css.js.map +1 -0
  18. package/dist/components/builder/canvas-chrome.d.ts +122 -18
  19. package/dist/components/builder/canvas-chrome.js +148 -82
  20. package/dist/components/builder/canvas-chrome.js.map +1 -1
  21. package/dist/components/builder/canvas-inline-editing.d.ts +7 -0
  22. package/dist/components/builder/canvas-inline-editing.js +3 -1
  23. package/dist/components/builder/canvas-inline-editing.js.map +1 -1
  24. package/dist/components/builder/canvas-overlay-geometry.d.ts +72 -0
  25. package/dist/components/builder/canvas-overlay-geometry.js +80 -0
  26. package/dist/components/builder/canvas-overlay-geometry.js.map +1 -0
  27. package/dist/components/builder/canvas-rect-tracking.d.ts +124 -0
  28. package/dist/components/builder/canvas-rect-tracking.js +198 -0
  29. package/dist/components/builder/canvas-rect-tracking.js.map +1 -0
  30. package/dist/components/builder/canvas-render-context.d.ts +13 -0
  31. package/dist/components/builder/canvas-render-context.js +7 -5
  32. package/dist/components/builder/canvas-render-context.js.map +1 -1
  33. package/dist/components/builder/canvas-stylesheet.d.ts +1 -1
  34. package/dist/components/builder/canvas-stylesheet.js +16 -9
  35. package/dist/components/builder/canvas-stylesheet.js.map +1 -1
  36. package/dist/components/builder/canvas-theme-notice.d.ts +6 -0
  37. package/dist/components/builder/canvas-theme-notice.js +46 -0
  38. package/dist/components/builder/canvas-theme-notice.js.map +1 -0
  39. package/dist/components/builder/canvas.js +78 -22
  40. package/dist/components/builder/canvas.js.map +1 -1
  41. package/dist/components/builder/composition-field-editor.js +22 -3
  42. package/dist/components/builder/composition-field-editor.js.map +1 -1
  43. package/dist/components/builder/delete-key.d.ts +38 -0
  44. package/dist/components/builder/delete-key.js +46 -0
  45. package/dist/components/builder/delete-key.js.map +1 -0
  46. package/dist/components/builder/drag-session.d.ts +8 -0
  47. package/dist/components/builder/drag-session.js +15 -0
  48. package/dist/components/builder/drag-session.js.map +1 -1
  49. package/dist/components/builder/insert-affordance-context.d.ts +19 -0
  50. package/dist/components/builder/insert-affordance-context.js +7 -0
  51. package/dist/components/builder/insert-affordance-context.js.map +1 -0
  52. package/dist/components/builder/insert-context.d.ts +40 -0
  53. package/dist/components/builder/insert-context.js +86 -3
  54. package/dist/components/builder/insert-context.js.map +1 -1
  55. package/dist/components/builder/node-title.d.ts +60 -0
  56. package/dist/components/builder/node-title.js +135 -0
  57. package/dist/components/builder/node-title.js.map +1 -0
  58. package/dist/components/builder/page-tree.js +21 -3
  59. package/dist/components/builder/page-tree.js.map +1 -1
  60. package/dist/components/builder/prop-selection.d.ts +52 -0
  61. package/dist/components/builder/prop-selection.js +38 -0
  62. package/dist/components/builder/prop-selection.js.map +1 -0
  63. package/dist/components/builder/prop-sync-context.d.ts +49 -0
  64. package/dist/components/builder/prop-sync-context.js +23 -0
  65. package/dist/components/builder/prop-sync-context.js.map +1 -0
  66. package/dist/components/builder/property-inspector.js +23 -1
  67. package/dist/components/builder/property-inspector.js.map +1 -1
  68. package/dist/components/builder/reuse-context.js +1 -1
  69. package/dist/components/builder/reuse-context.js.map +1 -1
  70. package/dist/components/builder/rich-text-toolbar.d.ts +23 -0
  71. package/dist/components/builder/rich-text-toolbar.js +24 -17
  72. package/dist/components/builder/rich-text-toolbar.js.map +1 -1
  73. package/dist/components/builder/selection-overlay.d.ts +79 -0
  74. package/dist/components/builder/selection-overlay.js +244 -0
  75. package/dist/components/builder/selection-overlay.js.map +1 -0
  76. package/dist/components/builder/spacing-handles.d.ts +88 -1
  77. package/dist/components/builder/spacing-handles.js +191 -63
  78. package/dist/components/builder/spacing-handles.js.map +1 -1
  79. package/dist/components/cms/asset-picker.js +11 -5
  80. package/dist/components/cms/asset-picker.js.map +1 -1
  81. package/dist/components/cms/rich-text/block-style.d.ts +48 -0
  82. package/dist/components/cms/rich-text/block-style.js +30 -0
  83. package/dist/components/cms/rich-text/block-style.js.map +1 -0
  84. package/dist/components/cms/rich-text/embed-picker.js +4 -3
  85. package/dist/components/cms/rich-text/embed-picker.js.map +1 -1
  86. package/dist/components/cms/rich-text/extensions.d.ts +2 -0
  87. package/dist/components/cms/rich-text/extensions.js +4 -0
  88. package/dist/components/cms/rich-text/extensions.js.map +1 -1
  89. package/dist/components/cms/rich-text/font-size-control.d.ts +31 -0
  90. package/dist/components/cms/rich-text/font-size-control.js +40 -0
  91. package/dist/components/cms/rich-text/font-size-control.js.map +1 -0
  92. package/dist/components/cms/rich-text/font-size.d.ts +16 -0
  93. package/dist/components/cms/rich-text/font-size.js +40 -0
  94. package/dist/components/cms/rich-text/font-size.js.map +1 -0
  95. package/dist/components/cms/rich-text/toolbar-state.d.ts +3 -0
  96. package/dist/components/cms/rich-text/toolbar-state.js +3 -0
  97. package/dist/components/cms/rich-text/toolbar-state.js.map +1 -1
  98. package/dist/components/cms/rich-text/toolbar.d.ts +13 -0
  99. package/dist/components/cms/rich-text/toolbar.js +17 -5
  100. package/dist/components/cms/rich-text/toolbar.js.map +1 -1
  101. package/dist/components/components/component-detail.js +3 -3
  102. package/dist/components/components/component-detail.js.map +1 -1
  103. package/dist/components/components/component-preview.d.ts +2 -1
  104. package/dist/components/components/component-preview.js +25 -26
  105. package/dist/components/components/component-preview.js.map +1 -1
  106. package/dist/components/components/registry-browser.js +2 -2
  107. package/dist/components/components/registry-browser.js.map +1 -1
  108. package/dist/components/components/taxonomy.js +3 -2
  109. package/dist/components/components/taxonomy.js.map +1 -1
  110. package/dist/components/components/transfer-dialog.js +1 -1
  111. package/dist/components/components/transfer-dialog.js.map +1 -1
  112. package/dist/lib/assets/backfill-thumbs.d.ts +67 -0
  113. package/dist/lib/assets/backfill-thumbs.js +120 -0
  114. package/dist/lib/assets/backfill-thumbs.js.map +1 -0
  115. package/dist/lib/assets/delete-asset.d.ts +2 -1
  116. package/dist/lib/assets/delete-asset.js +12 -0
  117. package/dist/lib/assets/delete-asset.js.map +1 -1
  118. package/dist/lib/assets/index.d.ts +2 -0
  119. package/dist/lib/assets/index.js +2 -0
  120. package/dist/lib/assets/index.js.map +1 -1
  121. package/dist/lib/assets/thumbnail.d.ts +145 -0
  122. package/dist/lib/assets/thumbnail.js +220 -0
  123. package/dist/lib/assets/thumbnail.js.map +1 -0
  124. package/dist/lib/assets/upload.d.ts +9 -0
  125. package/dist/lib/assets/upload.js +31 -0
  126. package/dist/lib/assets/upload.js.map +1 -1
  127. package/dist/lib/i18n/messages.de.js +20 -0
  128. package/dist/lib/i18n/messages.de.js.map +1 -1
  129. package/dist/lib/i18n/messages.en.d.ts +18 -0
  130. package/dist/lib/i18n/messages.en.js +21 -0
  131. package/dist/lib/i18n/messages.en.js.map +1 -1
  132. package/dist/lib/persistence/backend-mode.d.ts +7 -0
  133. package/dist/lib/persistence/backend-mode.js +9 -0
  134. package/dist/lib/persistence/backend-mode.js.map +1 -1
  135. package/dist/lib/persistence/convex-adapter.js +4 -0
  136. package/dist/lib/persistence/convex-adapter.js.map +1 -1
  137. package/dist/lib/persistence/index.d.ts +1 -1
  138. package/dist/lib/persistence/index.js +1 -1
  139. package/dist/lib/persistence/index.js.map +1 -1
  140. package/dist/lib/persona/labels.d.ts +9 -5
  141. package/dist/lib/persona/labels.js +13 -8
  142. package/dist/lib/persona/labels.js.map +1 -1
  143. package/dist/lib/references/references-store.js +7 -1
  144. package/dist/lib/references/references-store.js.map +1 -1
  145. package/package.json +2 -2
  146. package/src/styles/studio.css +13 -0
@@ -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 { 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 {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 { 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,4 +1,5 @@
1
1
  import { isPrimitive } from '@elytracms/core/component-registry';
2
+ import { localizedStringVariants, resolveLabel } from '@elytracms/core/cms-core';
2
3
  function namespaceOfManifest(manifest) {
3
4
  return isPrimitive(manifest.id) ? 'base' : 'project';
4
5
  }
@@ -7,7 +8,7 @@ export function manifestMatchesQuery(manifest, query) {
7
8
  const q = query.trim().toLowerCase();
8
9
  if (q === '')
9
10
  return true;
10
- const haystack = [manifest.id, manifest.title, manifest.category, manifest.description]
11
+ const haystack = [manifest.id, ...localizedStringVariants(manifest.title), manifest.category, manifest.description]
11
12
  .filter((v) => typeof v === 'string')
12
13
  .join(' ')
13
14
  .toLowerCase();
@@ -35,7 +36,7 @@ function groupCategories(manifests) {
35
36
  })
36
37
  .map(([key, ms]) => ({
37
38
  key,
38
- manifests: [...ms].sort((x, y) => (x.title ?? x.id).localeCompare(y.title ?? y.id)),
39
+ manifests: [...ms].sort((x, y) => (resolveLabel(x.title) ?? x.id).localeCompare(resolveLabel(y.title) ?? y.id)),
39
40
  }));
40
41
  }
41
42
  /**
@@ -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'\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, 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) => (x.title ?? x.id).localeCompare(y.title ?? y.id)),\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":"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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"]}
@@ -30,7 +30,7 @@ const MAX_EXISTING_TARGETS = 8;
30
30
  * opens on the canvas.
31
31
  */
32
32
  export function TransferToDocumentDialog({ projectId, manifest, node, open, onOpenChange, }) {
33
- return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "max-w-md", children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: translate('components.transfer.title', { name: manifest.title ?? manifest.id }) }), _jsx(DialogDescription, { children: translate('components.transfer.description') })] }), open ? (_jsx(CmsSessionGate, { children: (cms) => {
33
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "max-w-md", children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: translate('components.transfer.title', { name: localizedLabel(manifest.title, manifest.id) }) }), _jsx(DialogDescription, { children: translate('components.transfer.description') })] }), open ? (_jsx(CmsSessionGate, { children: (cms) => {
34
34
  const kit = getInstantiatedKit(projectId);
35
35
  // Bug fix: this used to reimplement `resolveCmsInitialCollections`'s
36
36
  // fallback chain inline with a final `[]` instead of `undefined` — `[]`
@@ -1 +1 @@
1
- {"version":3,"file":"transfer-dialog.js","sourceRoot":"","sources":["../../../src/components/components/transfer-dialog.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useMemo, useState } from 'react'\nimport { FilePlus2, FileText, Loader2, Search } from 'lucide-react'\nimport type { ReactNode } from 'react'\nimport type { ComponentManifest } from '@elytracms/core/component-registry'\nimport type { CmsDocument } from '@elytracms/core/cms-core'\nimport type { ComponentNode } from '@elytracms/core/project-graph'\nimport { STUDIO_PROJECT_ROUTE, studioPath, useNavigate } from '@elytracms/studio/router'\nimport {\n Button,\n Code,\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n Input,\n Row,\n Stack,\n Text,\n} from '@elytracms/studio/ui'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { localizedLabel, translate } from '@elytracms/studio/lib/i18n'\nimport { getInstantiatedKit } from '@elytracms/studio/lib/starters/kit-session'\nimport { CmsSessionGate } from '../project-session'\nimport { CmsProvider, useCms } from '../cms/cms-store'\nimport { stageDocument } from '../cms/document-staging'\nimport { resolveCmsInitialCollections } from '../app-views/default-modules'\nimport {\n appendToBlockDocument,\n buildBlockDocument,\n canAppendTo,\n collectionsWithBlocks,\n isComponentAllowed,\n} from './transfer'\nimport type { CollectionBlocksTarget } from './transfer'\n\n/** Existing documents offered per target, beyond which the rest are hidden (filter to find them). */\nconst MAX_EXISTING_TARGETS = 8\n\n/**\n * EC-320 (+ 0.0.44 follow-up) — \"Use in document…\". Transfers the current\n * component combination (the live preview {@link ComponentNode}, edited props\n * and all) into a document's blocks field, then opens that document. Each\n * eligible collection+field target offers a NEW draft AND its existing\n * documents to append into.\n *\n * The dialog mounts a CMS session (so it reuses the store's create/save machinery,\n * collision-free ids and persistence) only while open. On confirm it builds the\n * draft or the append (pure {@link buildBlockDocument} / {@link appendToBlockDocument}),\n * saves it and AWAITS the persisted write landing (belt and braces alongside the\n * STAGE for the next CMS mount, see `document-staging` — both matter: staging\n * covers the very next mount regardless of mode, the await guards any later\n * reload), then navigates to the document — focused on its blocks field so it\n * opens on the canvas.\n */\nexport function TransferToDocumentDialog({\n projectId,\n manifest,\n node,\n open,\n onOpenChange,\n}: {\n projectId: string\n manifest: ComponentManifest\n node: ComponentNode\n open: boolean\n onOpenChange: (open: boolean) => void\n}) {\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent className=\"max-w-md\">\n <DialogHeader>\n <DialogTitle>\n {translate('components.transfer.title', { name: manifest.title ?? manifest.id })}\n </DialogTitle>\n <DialogDescription>{translate('components.transfer.description')}</DialogDescription>\n </DialogHeader>\n {open ? (\n <CmsSessionGate>\n {(cms) => {\n const kit = getInstantiatedKit(projectId)\n // Bug fix: this used to reimplement `resolveCmsInitialCollections`'s\n // fallback chain inline with a final `[]` instead of `undefined` — `[]`\n // is \"present\" to `CmsProvider`'s `initialCollections ??\n // sampleFilterableCollections`, so it suppressed the fixture fallback a\n // fresh Convex-mode project (`cms === null`, no kit) needs. Delegating to\n // the shared, tested resolver (also used by the CMS/Assets tabs) keeps\n // this dialog's targets in sync with what those tabs show.\n const collections = resolveCmsInitialCollections(cms, kit)\n return (\n <CmsProvider\n projectId={projectId}\n initialCollections={collections}\n initialDocuments={cms?.documents ?? kit?.documents}\n initialPublishedKeys={cms?.publishedKeys}\n >\n <TransferPicker projectId={projectId} node={node} onClose={() => onOpenChange(false)} />\n </CmsProvider>\n )\n }}\n </CmsSessionGate>\n ) : null}\n </DialogContent>\n </Dialog>\n )\n}\n\n/** A pending transfer's in-flight target key: `<collectionId>/new` or `<collectionId>/<docId>`. */\nfunction newTargetKey(target: CollectionBlocksTarget): string {\n return `${target.collection.id}/new`\n}\nfunction existingTargetKey(target: CollectionBlocksTarget, doc: CmsDocument): string {\n return `${target.collection.id}/${doc.id}`\n}\n\n/**\n * Exported for the fresh-Convex-project regression test (bug fix,\n * transfer-dialog.test.tsx) — `TransferToDocumentDialog` is otherwise its only\n * caller.\n */\nexport function TransferPicker({\n projectId,\n node,\n onClose,\n}: {\n projectId: string\n node: ComponentNode\n onClose: () => void\n}) {\n const cms = useCms()\n const navigate = useNavigate()\n const [query, setQuery] = useState('')\n // The in-flight target's key while its save is awaited (disables every row\n // + shows a spinner on the clicked one) — `null` when idle.\n const [pendingKey, setPendingKey] = useState<string | null>(null)\n\n // Read the LIVE store schema (`cms.collections`) rather than re-deriving it\n // from the same `resolveCmsInitialCollections` result passed to `CmsProvider`\n // above — `cms.collections` is what the provider actually resolved to\n // (its OWN `initialCollections ?? sampleFilterableCollections` fallback,\n // EC-320), so the targets list can never drift from — or reintroduce the\n // `[]`-suppresses-the-fixture-fallback bug against — what the CMS/Assets\n // tabs show for this same session. Allow-gated (0.0.44 follow-up): a target\n // whose field's `allow` vocabulary rejects this component is never offered,\n // new-document or existing-document alike — the validator would flag it.\n const targets = useMemo(\n () => collectionsWithBlocks(cms.collections).filter((t) => isComponentAllowed(t.field, node.componentId)),\n [cms.collections, node.componentId],\n )\n\n /** Persist (awaited) + stage + navigate — shared by both transfer paths. */\n async function persistAndOpen(doc: CmsDocument, target: CollectionBlocksTarget) {\n // Optimistic session apply + queues the persisted upsert (EC-141's normal\n // fire-and-forget autosave path — unchanged for every OTHER caller).\n cms.saveDocument(doc)\n // Cross-mount handoff for the very next `CmsProvider` mount, in EVERY mode\n // (see document-staging.ts) — covers the instant after this write, before\n // any later hydration is guaranteed to observe it.\n stageDocument(projectId, doc)\n // Belt and braces: AWAIT the queued persistence command landing (the same\n // `flushPersistence` publish/unpublish already await) so a reload of the\n // destination editor — not just its very next mount — also finds the\n // document. `saveDocument` itself stays fire-and-forget for every other\n // caller; only this explicit await makes the transfer's save synchronous.\n await cms.flushPersistence()\n onClose()\n void navigate({\n to: STUDIO_PROJECT_ROUTE,\n params: { projectId, _splat: studioPath.cmsDocument(target.collection.id, doc.id) },\n search: { field: `values/${target.field.name}` },\n })\n }\n\n async function transferNew(target: CollectionBlocksTarget) {\n const key = newTargetKey(target)\n setPendingKey(key)\n try {\n const existingIds = new Set(cms.documentsOf(target.collection.id).map((d) => d.id))\n const doc = buildBlockDocument({\n collection: target.collection,\n blocksField: target.field,\n node,\n existingIds,\n seq: { current: 0 },\n })\n await persistAndOpen(doc, target)\n } finally {\n setPendingKey(null)\n }\n }\n\n async function transferInto(target: CollectionBlocksTarget, existing: CmsDocument) {\n const key = existingTargetKey(target, existing)\n setPendingKey(key)\n try {\n const doc = appendToBlockDocument({\n collection: target.collection,\n document: existing,\n blocksField: target.field,\n node,\n locale: cms.activeLocale,\n })\n await persistAndOpen(doc, target)\n } finally {\n setPendingKey(null)\n }\n }\n\n if (targets.length === 0) {\n return (\n <Text size=\"sm\" tone=\"muted\" data-testid=\"transfer-no-targets\">\n {translate('components.transfer.noTargets')}\n </Text>\n )\n }\n\n const trimmedQuery = query.trim().toLowerCase()\n const hasAnyExistingDocs = targets.some((t) => cms.documentsOf(t.collection.id).length > 0)\n\n return (\n <>\n {hasAnyExistingDocs ? (\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('common.searchPlaceholder')}\n className=\"pl-8\"\n />\n </div>\n ) : null}\n <Stack gap=\"4\" className=\"max-h-[50vh] overflow-y-auto\">\n {targets.map((target) => {\n const existing = cms\n .documentsOf(target.collection.id)\n .filter((doc) => canAppendTo({ collection: target.collection, document: doc, blocksField: target.field, locale: cms.activeLocale }))\n .filter(\n (doc) =>\n trimmedQuery === '' ||\n cms.titleOf({ collection: target.collection.id, id: doc.id }).toLowerCase().includes(trimmedQuery),\n )\n .slice(0, MAX_EXISTING_TARGETS)\n\n return (\n <Stack key={target.collection.id} gap=\"2\">\n <Stack gap=\"px\">\n <Text size=\"sm\" weight=\"medium\" className=\"truncate\">\n {localizedLabel(target.collection.form?.label, target.collection.id)}\n </Text>\n <Row gap=\"1\" align=\"center\">\n <Text size=\"xs\" tone=\"muted\">\n {translate('components.transfer.intoField')}\n </Text>\n <Code tone=\"muted\">{target.field.name}</Code>\n </Row>\n </Stack>\n <Stack gap=\"1\">\n <TransferTargetRow\n testId=\"transfer-target\"\n icon={<FilePlus2 className=\"size-4\" />}\n label={translate('components.transfer.newDocument', {\n collection: localizedLabel(target.collection.form?.labelSingular, target.collection.id),\n })}\n pending={pendingKey === newTargetKey(target)}\n disabled={pendingKey !== null}\n onClick={() => void transferNew(target)}\n />\n {existing.length > 0 ? (\n <Text size=\"xs\" tone=\"muted\" className=\"px-1 pt-1\">\n {translate('components.transfer.orExisting')}\n </Text>\n ) : null}\n {existing.map((doc) => (\n <TransferTargetRow\n key={doc.id}\n testId=\"transfer-target-existing\"\n icon={<FileText className=\"size-4\" />}\n label={cms.titleOf({ collection: target.collection.id, id: doc.id })}\n pending={pendingKey === existingTargetKey(target, doc)}\n disabled={pendingKey !== null}\n onClick={() => void transferInto(target, doc)}\n />\n ))}\n </Stack>\n </Stack>\n )\n })}\n </Stack>\n <DialogFooter>\n <Button type=\"button\" variant=\"ghost\" onClick={onClose} disabled={pendingKey !== null}>\n {translate('common.cancel')}\n </Button>\n </DialogFooter>\n </>\n )\n}\n\n/** One selectable transfer target row — a new draft, or an existing document to append into. */\nfunction TransferTargetRow({\n testId,\n icon,\n label,\n pending,\n disabled,\n onClick,\n}: {\n testId: string\n icon: ReactNode\n label: string\n pending: boolean\n disabled: boolean\n onClick: () => void\n}) {\n return (\n <button\n type=\"button\"\n data-testid={testId}\n disabled={disabled}\n onClick={onClick}\n className={cn(\n 'flex items-center gap-3 rounded-md border border-border p-3 text-left transition-colors hover:border-primary/40 hover:bg-muted',\n disabled && 'cursor-not-allowed opacity-60',\n )}\n >\n <span className=\"grid size-8 shrink-0 place-items-center rounded-md bg-muted text-muted-foreground\">\n {pending ? <Loader2 className=\"size-4 animate-spin\" /> : icon}\n </span>\n <Text size=\"sm\" weight=\"medium\" className=\"min-w-0 flex-1 truncate\">\n {label}\n </Text>\n </button>\n )\n}\n"]}
1
+ {"version":3,"file":"transfer-dialog.js","sourceRoot":"","sources":["../../../src/components/components/transfer-dialog.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useMemo, useState } from 'react'\nimport { FilePlus2, FileText, Loader2, Search } from 'lucide-react'\nimport type { ReactNode } from 'react'\nimport type { ComponentManifest } from '@elytracms/core/component-registry'\nimport type { CmsDocument } from '@elytracms/core/cms-core'\nimport type { ComponentNode } from '@elytracms/core/project-graph'\nimport { STUDIO_PROJECT_ROUTE, studioPath, useNavigate } from '@elytracms/studio/router'\nimport {\n Button,\n Code,\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n Input,\n Row,\n Stack,\n Text,\n} from '@elytracms/studio/ui'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { localizedLabel, translate } from '@elytracms/studio/lib/i18n'\nimport { getInstantiatedKit } from '@elytracms/studio/lib/starters/kit-session'\nimport { CmsSessionGate } from '../project-session'\nimport { CmsProvider, useCms } from '../cms/cms-store'\nimport { stageDocument } from '../cms/document-staging'\nimport { resolveCmsInitialCollections } from '../app-views/default-modules'\nimport {\n appendToBlockDocument,\n buildBlockDocument,\n canAppendTo,\n collectionsWithBlocks,\n isComponentAllowed,\n} from './transfer'\nimport type { CollectionBlocksTarget } from './transfer'\n\n/** Existing documents offered per target, beyond which the rest are hidden (filter to find them). */\nconst MAX_EXISTING_TARGETS = 8\n\n/**\n * EC-320 (+ 0.0.44 follow-up) — \"Use in document…\". Transfers the current\n * component combination (the live preview {@link ComponentNode}, edited props\n * and all) into a document's blocks field, then opens that document. Each\n * eligible collection+field target offers a NEW draft AND its existing\n * documents to append into.\n *\n * The dialog mounts a CMS session (so it reuses the store's create/save machinery,\n * collision-free ids and persistence) only while open. On confirm it builds the\n * draft or the append (pure {@link buildBlockDocument} / {@link appendToBlockDocument}),\n * saves it and AWAITS the persisted write landing (belt and braces alongside the\n * STAGE for the next CMS mount, see `document-staging` — both matter: staging\n * covers the very next mount regardless of mode, the await guards any later\n * reload), then navigates to the document — focused on its blocks field so it\n * opens on the canvas.\n */\nexport function TransferToDocumentDialog({\n projectId,\n manifest,\n node,\n open,\n onOpenChange,\n}: {\n projectId: string\n manifest: ComponentManifest\n node: ComponentNode\n open: boolean\n onOpenChange: (open: boolean) => void\n}) {\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent className=\"max-w-md\">\n <DialogHeader>\n <DialogTitle>\n {translate('components.transfer.title', { name: localizedLabel(manifest.title, manifest.id) })}\n </DialogTitle>\n <DialogDescription>{translate('components.transfer.description')}</DialogDescription>\n </DialogHeader>\n {open ? (\n <CmsSessionGate>\n {(cms) => {\n const kit = getInstantiatedKit(projectId)\n // Bug fix: this used to reimplement `resolveCmsInitialCollections`'s\n // fallback chain inline with a final `[]` instead of `undefined` — `[]`\n // is \"present\" to `CmsProvider`'s `initialCollections ??\n // sampleFilterableCollections`, so it suppressed the fixture fallback a\n // fresh Convex-mode project (`cms === null`, no kit) needs. Delegating to\n // the shared, tested resolver (also used by the CMS/Assets tabs) keeps\n // this dialog's targets in sync with what those tabs show.\n const collections = resolveCmsInitialCollections(cms, kit)\n return (\n <CmsProvider\n projectId={projectId}\n initialCollections={collections}\n initialDocuments={cms?.documents ?? kit?.documents}\n initialPublishedKeys={cms?.publishedKeys}\n >\n <TransferPicker projectId={projectId} node={node} onClose={() => onOpenChange(false)} />\n </CmsProvider>\n )\n }}\n </CmsSessionGate>\n ) : null}\n </DialogContent>\n </Dialog>\n )\n}\n\n/** A pending transfer's in-flight target key: `<collectionId>/new` or `<collectionId>/<docId>`. */\nfunction newTargetKey(target: CollectionBlocksTarget): string {\n return `${target.collection.id}/new`\n}\nfunction existingTargetKey(target: CollectionBlocksTarget, doc: CmsDocument): string {\n return `${target.collection.id}/${doc.id}`\n}\n\n/**\n * Exported for the fresh-Convex-project regression test (bug fix,\n * transfer-dialog.test.tsx) — `TransferToDocumentDialog` is otherwise its only\n * caller.\n */\nexport function TransferPicker({\n projectId,\n node,\n onClose,\n}: {\n projectId: string\n node: ComponentNode\n onClose: () => void\n}) {\n const cms = useCms()\n const navigate = useNavigate()\n const [query, setQuery] = useState('')\n // The in-flight target's key while its save is awaited (disables every row\n // + shows a spinner on the clicked one) — `null` when idle.\n const [pendingKey, setPendingKey] = useState<string | null>(null)\n\n // Read the LIVE store schema (`cms.collections`) rather than re-deriving it\n // from the same `resolveCmsInitialCollections` result passed to `CmsProvider`\n // above — `cms.collections` is what the provider actually resolved to\n // (its OWN `initialCollections ?? sampleFilterableCollections` fallback,\n // EC-320), so the targets list can never drift from — or reintroduce the\n // `[]`-suppresses-the-fixture-fallback bug against — what the CMS/Assets\n // tabs show for this same session. Allow-gated (0.0.44 follow-up): a target\n // whose field's `allow` vocabulary rejects this component is never offered,\n // new-document or existing-document alike — the validator would flag it.\n const targets = useMemo(\n () => collectionsWithBlocks(cms.collections).filter((t) => isComponentAllowed(t.field, node.componentId)),\n [cms.collections, node.componentId],\n )\n\n /** Persist (awaited) + stage + navigate — shared by both transfer paths. */\n async function persistAndOpen(doc: CmsDocument, target: CollectionBlocksTarget) {\n // Optimistic session apply + queues the persisted upsert (EC-141's normal\n // fire-and-forget autosave path — unchanged for every OTHER caller).\n cms.saveDocument(doc)\n // Cross-mount handoff for the very next `CmsProvider` mount, in EVERY mode\n // (see document-staging.ts) — covers the instant after this write, before\n // any later hydration is guaranteed to observe it.\n stageDocument(projectId, doc)\n // Belt and braces: AWAIT the queued persistence command landing (the same\n // `flushPersistence` publish/unpublish already await) so a reload of the\n // destination editor — not just its very next mount — also finds the\n // document. `saveDocument` itself stays fire-and-forget for every other\n // caller; only this explicit await makes the transfer's save synchronous.\n await cms.flushPersistence()\n onClose()\n void navigate({\n to: STUDIO_PROJECT_ROUTE,\n params: { projectId, _splat: studioPath.cmsDocument(target.collection.id, doc.id) },\n search: { field: `values/${target.field.name}` },\n })\n }\n\n async function transferNew(target: CollectionBlocksTarget) {\n const key = newTargetKey(target)\n setPendingKey(key)\n try {\n const existingIds = new Set(cms.documentsOf(target.collection.id).map((d) => d.id))\n const doc = buildBlockDocument({\n collection: target.collection,\n blocksField: target.field,\n node,\n existingIds,\n seq: { current: 0 },\n })\n await persistAndOpen(doc, target)\n } finally {\n setPendingKey(null)\n }\n }\n\n async function transferInto(target: CollectionBlocksTarget, existing: CmsDocument) {\n const key = existingTargetKey(target, existing)\n setPendingKey(key)\n try {\n const doc = appendToBlockDocument({\n collection: target.collection,\n document: existing,\n blocksField: target.field,\n node,\n locale: cms.activeLocale,\n })\n await persistAndOpen(doc, target)\n } finally {\n setPendingKey(null)\n }\n }\n\n if (targets.length === 0) {\n return (\n <Text size=\"sm\" tone=\"muted\" data-testid=\"transfer-no-targets\">\n {translate('components.transfer.noTargets')}\n </Text>\n )\n }\n\n const trimmedQuery = query.trim().toLowerCase()\n const hasAnyExistingDocs = targets.some((t) => cms.documentsOf(t.collection.id).length > 0)\n\n return (\n <>\n {hasAnyExistingDocs ? (\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('common.searchPlaceholder')}\n className=\"pl-8\"\n />\n </div>\n ) : null}\n <Stack gap=\"4\" className=\"max-h-[50vh] overflow-y-auto\">\n {targets.map((target) => {\n const existing = cms\n .documentsOf(target.collection.id)\n .filter((doc) => canAppendTo({ collection: target.collection, document: doc, blocksField: target.field, locale: cms.activeLocale }))\n .filter(\n (doc) =>\n trimmedQuery === '' ||\n cms.titleOf({ collection: target.collection.id, id: doc.id }).toLowerCase().includes(trimmedQuery),\n )\n .slice(0, MAX_EXISTING_TARGETS)\n\n return (\n <Stack key={target.collection.id} gap=\"2\">\n <Stack gap=\"px\">\n <Text size=\"sm\" weight=\"medium\" className=\"truncate\">\n {localizedLabel(target.collection.form?.label, target.collection.id)}\n </Text>\n <Row gap=\"1\" align=\"center\">\n <Text size=\"xs\" tone=\"muted\">\n {translate('components.transfer.intoField')}\n </Text>\n <Code tone=\"muted\">{target.field.name}</Code>\n </Row>\n </Stack>\n <Stack gap=\"1\">\n <TransferTargetRow\n testId=\"transfer-target\"\n icon={<FilePlus2 className=\"size-4\" />}\n label={translate('components.transfer.newDocument', {\n collection: localizedLabel(target.collection.form?.labelSingular, target.collection.id),\n })}\n pending={pendingKey === newTargetKey(target)}\n disabled={pendingKey !== null}\n onClick={() => void transferNew(target)}\n />\n {existing.length > 0 ? (\n <Text size=\"xs\" tone=\"muted\" className=\"px-1 pt-1\">\n {translate('components.transfer.orExisting')}\n </Text>\n ) : null}\n {existing.map((doc) => (\n <TransferTargetRow\n key={doc.id}\n testId=\"transfer-target-existing\"\n icon={<FileText className=\"size-4\" />}\n label={cms.titleOf({ collection: target.collection.id, id: doc.id })}\n pending={pendingKey === existingTargetKey(target, doc)}\n disabled={pendingKey !== null}\n onClick={() => void transferInto(target, doc)}\n />\n ))}\n </Stack>\n </Stack>\n )\n })}\n </Stack>\n <DialogFooter>\n <Button type=\"button\" variant=\"ghost\" onClick={onClose} disabled={pendingKey !== null}>\n {translate('common.cancel')}\n </Button>\n </DialogFooter>\n </>\n )\n}\n\n/** One selectable transfer target row — a new draft, or an existing document to append into. */\nfunction TransferTargetRow({\n testId,\n icon,\n label,\n pending,\n disabled,\n onClick,\n}: {\n testId: string\n icon: ReactNode\n label: string\n pending: boolean\n disabled: boolean\n onClick: () => void\n}) {\n return (\n <button\n type=\"button\"\n data-testid={testId}\n disabled={disabled}\n onClick={onClick}\n className={cn(\n 'flex items-center gap-3 rounded-md border border-border p-3 text-left transition-colors hover:border-primary/40 hover:bg-muted',\n disabled && 'cursor-not-allowed opacity-60',\n )}\n >\n <span className=\"grid size-8 shrink-0 place-items-center rounded-md bg-muted text-muted-foreground\">\n {pending ? <Loader2 className=\"size-4 animate-spin\" /> : icon}\n </span>\n <Text size=\"sm\" weight=\"medium\" className=\"min-w-0 flex-1 truncate\">\n {label}\n </Text>\n </button>\n )\n}\n"]}
@@ -0,0 +1,67 @@
1
+ import type { AssetRecord, AssetRepository, BlobStorageAdapter, NewAssetInput } from '@elytracms/core/persistence';
2
+ import type { GeneratedVariants, ThumbSource, VariantWant } from './thumbnail.js';
3
+ /**
4
+ * The assets that still need at least one variant (pure, node-tested idempotence
5
+ * core, EC-420 AD-4): an image record missing a needed thumb OR a needed lqip.
6
+ * Records already carrying both, non-images, SVGs, and (for the thumb) already-
7
+ * small images are silently excluded — re-running on a done library is a no-op.
8
+ */
9
+ export declare function assetsNeedingVariants(assets: readonly AssetRecord[]): AssetRecord[];
10
+ /** The variants an asset is missing — what the per-variant backfill asks the encoder for. */
11
+ export declare function variantWantFor(asset: AssetRecord): VariantWant;
12
+ /**
13
+ * The full upsert input that re-states an existing record with freshly generated
14
+ * variants attached — the targeted patch path for the backfill, within the
15
+ * existing repository/`assets.upsert` contract (EC-400 AD-4 / EC-420). Mirrors
16
+ * `assetMetadataUpsertInput`: every current column is restated so the full-replace
17
+ * upsert preserves it. A newly generated variant overlays; a variant the record
18
+ * ALREADY had (the other half of a per-variant fill) is preserved from `current`.
19
+ */
20
+ export declare function assetVariantUpsertInput(current: AssetRecord, variants: GeneratedVariants): NewAssetInput;
21
+ /** Live tally the "Generate previews" UI renders (EC-400 AD-1). */
22
+ export interface BackfillProgress {
23
+ /** How many assets need at least one variant (the candidate set size). */
24
+ total: number;
25
+ /** How many candidates have been processed (generated + skipped + failed). */
26
+ done: number;
27
+ /** How many candidates had at least one variant generated and patched in. */
28
+ generated: number;
29
+ /** Candidates the re-plan/encode declined (e.g. undecodable) — silent, valid. */
30
+ skipped: number;
31
+ /** Candidates whose fetch/encode/patch errored — surfaced, not fatal. */
32
+ failed: number;
33
+ /** Filename of the asset currently in flight (for the progress label). */
34
+ current?: string;
35
+ /** Set once the run stops early because the caller cancelled. */
36
+ cancelled?: boolean;
37
+ }
38
+ /** A mutable cancellation flag the UI flips to stop the sequential run (AD-1). */
39
+ export interface BackfillSignal {
40
+ cancelled: boolean;
41
+ }
42
+ export interface BackfillDeps {
43
+ /** Metadata reads + the variant patch write (the operations facade in the studio). */
44
+ assets: AssetRepository;
45
+ /** Blob boundary: fetch each original's bytes, store the thumb variant. */
46
+ blobs: BlobStorageAdapter;
47
+ /**
48
+ * Injectable variant producer; defaults to {@link generateVariants} over `blobs`.
49
+ * Receives the per-asset `want` so it produces ONLY the missing variants. Tests
50
+ * pass a deterministic fake so the iterator runs in node.
51
+ */
52
+ generate?: (source: Blob, meta: ThumbSource, want: VariantWant) => Promise<GeneratedVariants>;
53
+ /** Fired with each patched record so the caller can update its cache in place. */
54
+ onGenerated?: (asset: AssetRecord) => void;
55
+ /** Fired after each candidate (and once at the end) to drive the progress UI. */
56
+ onProgress?: (progress: BackfillProgress) => void;
57
+ /** Cooperative cancellation — checked before each candidate (AD-1). */
58
+ signal?: BackfillSignal;
59
+ }
60
+ /**
61
+ * Run the backfill sequentially over a project's images that still need a variant.
62
+ * Resolves with the final tally; never throws (per-asset failures are counted, not
63
+ * fatal). One fetch per candidate serves both variants; only the missing ones are
64
+ * (re)generated (EC-420 AD-4), so an asset with a thumb but no lqip is patched with
65
+ * just the lqip and its existing thumb blob is left untouched.
66
+ */
67
+ export declare function runVariantBackfill(deps: BackfillDeps, projectId: string): Promise<BackfillProgress>;
@@ -0,0 +1,120 @@
1
+ import { generateVariants, planLqip, planThumb } from './thumbnail.js';
2
+ /**
3
+ * The assets-rail "Generate previews" backfill (EC-400 AD-1 + EC-420 AD-4).
4
+ * Existing libraries predate the upload-time previews, so an explicit,
5
+ * deterministic maintenance action fetches each original ONCE, runs the SAME
6
+ * decode path as upload, and patches the record. It is idempotent PER VARIANT
7
+ * (EC-420 AD-4): an asset needing only the lqip gets one fetch of the original
8
+ * and fills just what's missing; an asset that already has both is skipped;
9
+ * non-images / SVGs / (for the thumb) already-small images never qualify. The run
10
+ * is sequential with visible progress and cancellable. Opportunistic per-open
11
+ * generation was REJECTED (it hides egress in random editor sessions and is
12
+ * untestable).
13
+ *
14
+ * The write goes through the injected {@link AssetRepository} — in the studio the
15
+ * EC-141 operations facade — so the patch flows through the one operations layer /
16
+ * TanStack Query cache (AD-5 / AD-17), never a direct backend client. The pure
17
+ * cores ({@link assetsNeedingVariants}, {@link assetVariantUpsertInput}) are
18
+ * node-tested; the byte fetch + canvas encode stay in the injectable shell.
19
+ */
20
+ /** An image record still missing a thumb whose {@link planThumb} decision is `encode`. */
21
+ function needsThumb(asset) {
22
+ return asset.thumb === undefined && planThumb(asset).kind === 'encode';
23
+ }
24
+ /** An image record still missing an lqip whose {@link planLqip} decision is `encode`. */
25
+ function needsLqip(asset) {
26
+ return asset.lqip === undefined && planLqip(asset).kind === 'encode';
27
+ }
28
+ /**
29
+ * The assets that still need at least one variant (pure, node-tested idempotence
30
+ * core, EC-420 AD-4): an image record missing a needed thumb OR a needed lqip.
31
+ * Records already carrying both, non-images, SVGs, and (for the thumb) already-
32
+ * small images are silently excluded — re-running on a done library is a no-op.
33
+ */
34
+ export function assetsNeedingVariants(assets) {
35
+ return assets.filter((asset) => needsThumb(asset) || needsLqip(asset));
36
+ }
37
+ /** The variants an asset is missing — what the per-variant backfill asks the encoder for. */
38
+ export function variantWantFor(asset) {
39
+ return { thumb: needsThumb(asset), lqip: needsLqip(asset) };
40
+ }
41
+ /**
42
+ * The full upsert input that re-states an existing record with freshly generated
43
+ * variants attached — the targeted patch path for the backfill, within the
44
+ * existing repository/`assets.upsert` contract (EC-400 AD-4 / EC-420). Mirrors
45
+ * `assetMetadataUpsertInput`: every current column is restated so the full-replace
46
+ * upsert preserves it. A newly generated variant overlays; a variant the record
47
+ * ALREADY had (the other half of a per-variant fill) is preserved from `current`.
48
+ */
49
+ export function assetVariantUpsertInput(current, variants) {
50
+ const thumb = variants.thumb ?? current.thumb;
51
+ const lqip = variants.lqip ?? current.lqip;
52
+ return {
53
+ id: current.id,
54
+ filename: current.filename,
55
+ contentType: current.contentType,
56
+ byteSize: current.byteSize,
57
+ storageKey: current.storageKey,
58
+ ...(current.width !== undefined ? { width: current.width } : {}),
59
+ ...(current.height !== undefined ? { height: current.height } : {}),
60
+ ...(current.url !== undefined ? { url: current.url } : {}),
61
+ ...(current.alt !== undefined ? { alt: current.alt } : {}),
62
+ ...(current.title !== undefined ? { title: current.title } : {}),
63
+ ...(current.focalPoint !== undefined ? { focalPoint: current.focalPoint } : {}),
64
+ ...(current.meta !== undefined ? { meta: current.meta } : {}),
65
+ ...(thumb !== undefined ? { thumb } : {}),
66
+ ...(lqip !== undefined ? { lqip } : {}),
67
+ };
68
+ }
69
+ /**
70
+ * Run the backfill sequentially over a project's images that still need a variant.
71
+ * Resolves with the final tally; never throws (per-asset failures are counted, not
72
+ * fatal). One fetch per candidate serves both variants; only the missing ones are
73
+ * (re)generated (EC-420 AD-4), so an asset with a thumb but no lqip is patched with
74
+ * just the lqip and its existing thumb blob is left untouched.
75
+ */
76
+ export async function runVariantBackfill(deps, projectId) {
77
+ const all = await deps.assets.listAssets(projectId);
78
+ const candidates = assetsNeedingVariants(all);
79
+ const generate = deps.generate ??
80
+ ((source, meta, want) => generateVariants({ blobs: deps.blobs }, source, meta, want));
81
+ const progress = { total: candidates.length, done: 0, generated: 0, skipped: 0, failed: 0 };
82
+ deps.onProgress?.({ ...progress });
83
+ for (const asset of candidates) {
84
+ if (deps.signal?.cancelled) {
85
+ progress.cancelled = true;
86
+ break;
87
+ }
88
+ deps.onProgress?.({ ...progress, current: asset.filename });
89
+ try {
90
+ const bytes = await deps.blobs.get(asset.storageKey);
91
+ if (!bytes) {
92
+ progress.failed += 1;
93
+ }
94
+ else {
95
+ const source = new Blob([bytes], { type: asset.contentType });
96
+ const variants = await generate(source, {
97
+ contentType: asset.contentType,
98
+ ...(asset.width !== undefined ? { width: asset.width } : {}),
99
+ ...(asset.height !== undefined ? { height: asset.height } : {}),
100
+ }, variantWantFor(asset));
101
+ if (variants.thumb === undefined && variants.lqip === undefined) {
102
+ progress.skipped += 1;
103
+ }
104
+ else {
105
+ const patched = await deps.assets.upsertAsset(projectId, assetVariantUpsertInput(asset, variants));
106
+ deps.onGenerated?.(patched);
107
+ progress.generated += 1;
108
+ }
109
+ }
110
+ }
111
+ catch {
112
+ progress.failed += 1;
113
+ }
114
+ progress.done += 1;
115
+ deps.onProgress?.({ ...progress });
116
+ }
117
+ deps.onProgress?.({ ...progress });
118
+ return progress;
119
+ }
120
+ //# sourceMappingURL=backfill-thumbs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backfill-thumbs.js","sourceRoot":"","sources":["../../../src/lib/assets/backfill-thumbs.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n AssetRecord,\n AssetRepository,\n BlobStorageAdapter,\n NewAssetInput,\n} from '@elytracms/core/persistence'\nimport { generateVariants, planLqip, planThumb } from './thumbnail'\nimport type { GeneratedVariants, ThumbSource, VariantWant } from './thumbnail'\n\n/**\n * The assets-rail \"Generate previews\" backfill (EC-400 AD-1 + EC-420 AD-4).\n * Existing libraries predate the upload-time previews, so an explicit,\n * deterministic maintenance action fetches each original ONCE, runs the SAME\n * decode path as upload, and patches the record. It is idempotent PER VARIANT\n * (EC-420 AD-4): an asset needing only the lqip gets one fetch of the original\n * and fills just what's missing; an asset that already has both is skipped;\n * non-images / SVGs / (for the thumb) already-small images never qualify. The run\n * is sequential with visible progress and cancellable. Opportunistic per-open\n * generation was REJECTED (it hides egress in random editor sessions and is\n * untestable).\n *\n * The write goes through the injected {@link AssetRepository} — in the studio the\n * EC-141 operations facade — so the patch flows through the one operations layer /\n * TanStack Query cache (AD-5 / AD-17), never a direct backend client. The pure\n * cores ({@link assetsNeedingVariants}, {@link assetVariantUpsertInput}) are\n * node-tested; the byte fetch + canvas encode stay in the injectable shell.\n */\n\n/** An image record still missing a thumb whose {@link planThumb} decision is `encode`. */\nfunction needsThumb(asset: AssetRecord): boolean {\n return asset.thumb === undefined && planThumb(asset).kind === 'encode'\n}\n\n/** An image record still missing an lqip whose {@link planLqip} decision is `encode`. */\nfunction needsLqip(asset: AssetRecord): boolean {\n return asset.lqip === undefined && planLqip(asset).kind === 'encode'\n}\n\n/**\n * The assets that still need at least one variant (pure, node-tested idempotence\n * core, EC-420 AD-4): an image record missing a needed thumb OR a needed lqip.\n * Records already carrying both, non-images, SVGs, and (for the thumb) already-\n * small images are silently excluded — re-running on a done library is a no-op.\n */\nexport function assetsNeedingVariants(assets: readonly AssetRecord[]): AssetRecord[] {\n return assets.filter((asset) => needsThumb(asset) || needsLqip(asset))\n}\n\n/** The variants an asset is missing — what the per-variant backfill asks the encoder for. */\nexport function variantWantFor(asset: AssetRecord): VariantWant {\n return { thumb: needsThumb(asset), lqip: needsLqip(asset) }\n}\n\n/**\n * The full upsert input that re-states an existing record with freshly generated\n * variants attached — the targeted patch path for the backfill, within the\n * existing repository/`assets.upsert` contract (EC-400 AD-4 / EC-420). Mirrors\n * `assetMetadataUpsertInput`: every current column is restated so the full-replace\n * upsert preserves it. A newly generated variant overlays; a variant the record\n * ALREADY had (the other half of a per-variant fill) is preserved from `current`.\n */\nexport function assetVariantUpsertInput(\n current: AssetRecord,\n variants: GeneratedVariants,\n): NewAssetInput {\n const thumb = variants.thumb ?? current.thumb\n const lqip = variants.lqip ?? current.lqip\n return {\n id: current.id,\n filename: current.filename,\n contentType: current.contentType,\n byteSize: current.byteSize,\n storageKey: current.storageKey,\n ...(current.width !== undefined ? { width: current.width } : {}),\n ...(current.height !== undefined ? { height: current.height } : {}),\n ...(current.url !== undefined ? { url: current.url } : {}),\n ...(current.alt !== undefined ? { alt: current.alt } : {}),\n ...(current.title !== undefined ? { title: current.title } : {}),\n ...(current.focalPoint !== undefined ? { focalPoint: current.focalPoint } : {}),\n ...(current.meta !== undefined ? { meta: current.meta } : {}),\n ...(thumb !== undefined ? { thumb } : {}),\n ...(lqip !== undefined ? { lqip } : {}),\n }\n}\n\n/** Live tally the \"Generate previews\" UI renders (EC-400 AD-1). */\nexport interface BackfillProgress {\n /** How many assets need at least one variant (the candidate set size). */\n total: number\n /** How many candidates have been processed (generated + skipped + failed). */\n done: number\n /** How many candidates had at least one variant generated and patched in. */\n generated: number\n /** Candidates the re-plan/encode declined (e.g. undecodable) — silent, valid. */\n skipped: number\n /** Candidates whose fetch/encode/patch errored — surfaced, not fatal. */\n failed: number\n /** Filename of the asset currently in flight (for the progress label). */\n current?: string\n /** Set once the run stops early because the caller cancelled. */\n cancelled?: boolean\n}\n\n/** A mutable cancellation flag the UI flips to stop the sequential run (AD-1). */\nexport interface BackfillSignal {\n cancelled: boolean\n}\n\nexport interface BackfillDeps {\n /** Metadata reads + the variant patch write (the operations facade in the studio). */\n assets: AssetRepository\n /** Blob boundary: fetch each original's bytes, store the thumb variant. */\n blobs: BlobStorageAdapter\n /**\n * Injectable variant producer; defaults to {@link generateVariants} over `blobs`.\n * Receives the per-asset `want` so it produces ONLY the missing variants. Tests\n * pass a deterministic fake so the iterator runs in node.\n */\n generate?: (source: Blob, meta: ThumbSource, want: VariantWant) => Promise<GeneratedVariants>\n /** Fired with each patched record so the caller can update its cache in place. */\n onGenerated?: (asset: AssetRecord) => void\n /** Fired after each candidate (and once at the end) to drive the progress UI. */\n onProgress?: (progress: BackfillProgress) => void\n /** Cooperative cancellation — checked before each candidate (AD-1). */\n signal?: BackfillSignal\n}\n\n/**\n * Run the backfill sequentially over a project's images that still need a variant.\n * Resolves with the final tally; never throws (per-asset failures are counted, not\n * fatal). One fetch per candidate serves both variants; only the missing ones are\n * (re)generated (EC-420 AD-4), so an asset with a thumb but no lqip is patched with\n * just the lqip and its existing thumb blob is left untouched.\n */\nexport async function runVariantBackfill(\n deps: BackfillDeps,\n projectId: string,\n): Promise<BackfillProgress> {\n const all = await deps.assets.listAssets(projectId)\n const candidates = assetsNeedingVariants(all)\n const generate =\n deps.generate ??\n ((source: Blob, meta: ThumbSource, want: VariantWant) =>\n generateVariants({ blobs: deps.blobs }, source, meta, want))\n\n const progress: BackfillProgress = { total: candidates.length, done: 0, generated: 0, skipped: 0, failed: 0 }\n deps.onProgress?.({ ...progress })\n\n for (const asset of candidates) {\n if (deps.signal?.cancelled) {\n progress.cancelled = true\n break\n }\n deps.onProgress?.({ ...progress, current: asset.filename })\n try {\n const bytes = await deps.blobs.get(asset.storageKey)\n if (!bytes) {\n progress.failed += 1\n } else {\n const source = new Blob([bytes as BlobPart], { type: asset.contentType })\n const variants = await generate(\n source,\n {\n contentType: asset.contentType,\n ...(asset.width !== undefined ? { width: asset.width } : {}),\n ...(asset.height !== undefined ? { height: asset.height } : {}),\n },\n variantWantFor(asset),\n )\n if (variants.thumb === undefined && variants.lqip === undefined) {\n progress.skipped += 1\n } else {\n const patched = await deps.assets.upsertAsset(projectId, assetVariantUpsertInput(asset, variants))\n deps.onGenerated?.(patched)\n progress.generated += 1\n }\n }\n } catch {\n progress.failed += 1\n }\n progress.done += 1\n deps.onProgress?.({ ...progress })\n }\n\n deps.onProgress?.({ ...progress })\n return progress\n}\n"]}
@@ -6,7 +6,8 @@ import type { AssetRepository, BlobStorageAdapter, ReferenceIndexRepository } fr
6
6
  * `{ type: 'asset' }` target) — index-accurate, replacing the interim string
7
7
  * scan (`find-asset-usages`), which could miss rich-text asset hrefs and
8
8
  * false-positive on unrelated strings. Deleting an unused asset removes bytes
9
- * (blob) AND the metadata record. Results are structured data — never throws.
9
+ * (blob) — original AND the EC-400 thumb variant blob — plus the metadata
10
+ * record. Results are structured data — never throws.
10
11
  */
11
12
  export interface DeleteAssetDeps {
12
13
  assets: AssetRepository;
@@ -15,6 +15,18 @@ export async function deleteAsset(deps, projectId, assetId, options = {}) {
15
15
  // Bytes first, metadata last: a failure midway leaves a visible metadata
16
16
  // record (re-deletable) rather than invisible orphaned bytes.
17
17
  await deps.blobs.delete(asset.storageKey);
18
+ // EC-400 (AD-4): the downscaled preview is a SECOND stored blob — delete it
19
+ // too. Best-effort: a leftover thumb blob is harmless wasted storage (its
20
+ // metadata is going), so a thumb-delete failure must not block removing the
21
+ // asset the way an orphaned original would.
22
+ if (asset.thumb) {
23
+ try {
24
+ await deps.blobs.delete(asset.thumb.storageKey);
25
+ }
26
+ catch {
27
+ // Swallow — the asset delete proceeds; the orphaned thumb blob is inert.
28
+ }
29
+ }
18
30
  await deps.assets.removeAsset(projectId, assetId);
19
31
  return { outcome: 'deleted' };
20
32
  }
@@ -1 +1 @@
1
- {"version":3,"file":"delete-asset.js","sourceRoot":"","sources":["../../../src/lib/assets/delete-asset.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAA;AAoCjE,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,IAAqB,EACrB,SAAiB,EACjB,OAAe,EACf,UAA8B,EAAE;IAEhC,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,SAAS,EAAE,OAAO,CAAC,CAAA;QAC7D,IAAI,CAAC,KAAK;YAAE,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,CAAA;QAE3C,8EAA8E;QAC9E,iFAAiF;QACjF,mFAAmF;QACnF,IAAI,OAAO,CAAC,YAAY,KAAK,IAAI,EAAE,CAAC;YAClC,MAAM,UAAU,GAAG,MAAM,eAAe,CAAC,IAAI,EAAE,SAAS,EAAE,OAAO,CAAC,CAAA;YAClE,IAAI,UAAU,GAAG,CAAC;gBAAE,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,CAAA;QAC/D,CAAC;QAED,yEAAyE;QACzE,8DAA8D;QAC9D,MAAM,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,CAAA;QACzC,MAAM,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,SAAS,EAAE,OAAO,CAAC,CAAA;QACjD,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,CAAA;IAC/B,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,KAAK,YAAY,mBAAmB;YAAE,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,CAAA;QACzE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAA;IAC9F,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,KAAK,UAAU,eAAe,CAC5B,IAAqB,EACrB,SAAiB,EACjB,OAAe;IAEf,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,OAAO,EAAE,CAAC,CAAA;IACxF,MAAM,QAAQ,GAAG,IAAI,GAAG,CACtB,OAAO,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC,IAAI,IAAI,KAAK,CAAC,MAAM,CAAC,EAAE,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC,CAChF,CAAA;IACD,OAAO,QAAQ,CAAC,IAAI,CAAA;AACtB,CAAC","sourcesContent":["import { RecordNotFoundError } from '@elytracms/core/persistence'\nimport type {\n AssetRepository,\n BlobStorageAdapter,\n ReferenceIndexRepository,\n} from '@elytracms/core/persistence'\n\n/**\n * Reference-respecting asset deletion (EC-151, reworked EC-202). Deleting an\n * asset that is referenced is blocked until explicitly confirmed; the usage\n * count comes from the **EC-155 reference index** (`references.inbound` for the\n * `{ type: 'asset' }` target) — index-accurate, replacing the interim string\n * scan (`find-asset-usages`), which could miss rich-text asset hrefs and\n * false-positive on unrelated strings. Deleting an unused asset removes bytes\n * (blob) AND the metadata record. Results are structured data — never throws.\n */\n\nexport interface DeleteAssetDeps {\n assets: AssetRepository\n blobs: BlobStorageAdapter\n /** The EC-155 reference index — the accurate inbound-usage source (EC-202). */\n references: Pick<ReferenceIndexRepository, 'inbound'>\n}\n\nexport type DeleteAssetResult =\n | { outcome: 'deleted' }\n /** The asset is referenced and `confirmInUse` was not set: nothing was removed. */\n | { outcome: 'blocked'; usageCount: number }\n | { outcome: 'not-found' }\n | { outcome: 'error'; message: string }\n\nexport interface DeleteAssetOptions {\n /** Caller has seen the usage count and explicitly confirmed deletion. */\n confirmInUse?: boolean\n}\n\nexport async function deleteAsset(\n deps: DeleteAssetDeps,\n projectId: string,\n assetId: string,\n options: DeleteAssetOptions = {},\n): Promise<DeleteAssetResult> {\n try {\n const asset = await deps.assets.findAsset(projectId, assetId)\n if (!asset) return { outcome: 'not-found' }\n\n // Only consult the reference index when the caller has NOT already confirmed.\n // The delete dialog enumerates usages up-front and confirms with `confirmInUse`,\n // so a confirmed delete skips this redundant inbound query entirely (EC-301 perf).\n if (options.confirmInUse !== true) {\n const usageCount = await assetUsageCount(deps, projectId, assetId)\n if (usageCount > 0) return { outcome: 'blocked', usageCount }\n }\n\n // Bytes first, metadata last: a failure midway leaves a visible metadata\n // record (re-deletable) rather than invisible orphaned bytes.\n await deps.blobs.delete(asset.storageKey)\n await deps.assets.removeAsset(projectId, assetId)\n return { outcome: 'deleted' }\n } catch (error) {\n if (error instanceof RecordNotFoundError) return { outcome: 'not-found' }\n return { outcome: 'error', message: error instanceof Error ? error.message : String(error) }\n }\n}\n\n/**\n * Distinct inbound usages of an asset from the reference index (EC-202). One\n * `source + path` (e.g. `post:hello · values/cover`) can carry both a draft and\n * a published index entry — that is a single usage, so variants collapse before\n * counting (the gate is \"is this referenced\", not \"how many index rows\").\n */\nasync function assetUsageCount(\n deps: DeleteAssetDeps,\n projectId: string,\n assetId: string,\n): Promise<number> {\n const inbound = await deps.references.inbound(projectId, { type: 'asset', id: assetId })\n const distinct = new Set(\n inbound.map((entry) => `${entry.source.type}:${entry.source.id}:${entry.path}`),\n )\n return distinct.size\n}\n"]}
1
+ {"version":3,"file":"delete-asset.js","sourceRoot":"","sources":["../../../src/lib/assets/delete-asset.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAA;AAqCjE,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,IAAqB,EACrB,SAAiB,EACjB,OAAe,EACf,UAA8B,EAAE;IAEhC,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,SAAS,EAAE,OAAO,CAAC,CAAA;QAC7D,IAAI,CAAC,KAAK;YAAE,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,CAAA;QAE3C,8EAA8E;QAC9E,iFAAiF;QACjF,mFAAmF;QACnF,IAAI,OAAO,CAAC,YAAY,KAAK,IAAI,EAAE,CAAC;YAClC,MAAM,UAAU,GAAG,MAAM,eAAe,CAAC,IAAI,EAAE,SAAS,EAAE,OAAO,CAAC,CAAA;YAClE,IAAI,UAAU,GAAG,CAAC;gBAAE,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,CAAA;QAC/D,CAAC;QAED,yEAAyE;QACzE,8DAA8D;QAC9D,MAAM,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,CAAA;QACzC,4EAA4E;QAC5E,0EAA0E;QAC1E,4EAA4E;QAC5E,4CAA4C;QAC5C,IAAI,KAAK,CAAC,KAAK,EAAE,CAAC;YAChB,IAAI,CAAC;gBACH,MAAM,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,UAAU,CAAC,CAAA;YACjD,CAAC;YAAC,MAAM,CAAC;gBACP,yEAAyE;YAC3E,CAAC;QACH,CAAC;QACD,MAAM,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,SAAS,EAAE,OAAO,CAAC,CAAA;QACjD,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,CAAA;IAC/B,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,KAAK,YAAY,mBAAmB;YAAE,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,CAAA;QACzE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAA;IAC9F,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,KAAK,UAAU,eAAe,CAC5B,IAAqB,EACrB,SAAiB,EACjB,OAAe;IAEf,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,OAAO,EAAE,CAAC,CAAA;IACxF,MAAM,QAAQ,GAAG,IAAI,GAAG,CACtB,OAAO,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC,IAAI,IAAI,KAAK,CAAC,MAAM,CAAC,EAAE,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC,CAChF,CAAA;IACD,OAAO,QAAQ,CAAC,IAAI,CAAA;AACtB,CAAC","sourcesContent":["import { RecordNotFoundError } from '@elytracms/core/persistence'\nimport type {\n AssetRepository,\n BlobStorageAdapter,\n ReferenceIndexRepository,\n} from '@elytracms/core/persistence'\n\n/**\n * Reference-respecting asset deletion (EC-151, reworked EC-202). Deleting an\n * asset that is referenced is blocked until explicitly confirmed; the usage\n * count comes from the **EC-155 reference index** (`references.inbound` for the\n * `{ type: 'asset' }` target) — index-accurate, replacing the interim string\n * scan (`find-asset-usages`), which could miss rich-text asset hrefs and\n * false-positive on unrelated strings. Deleting an unused asset removes bytes\n * (blob) — original AND the EC-400 thumb variant blob — plus the metadata\n * record. Results are structured data — never throws.\n */\n\nexport interface DeleteAssetDeps {\n assets: AssetRepository\n blobs: BlobStorageAdapter\n /** The EC-155 reference index — the accurate inbound-usage source (EC-202). */\n references: Pick<ReferenceIndexRepository, 'inbound'>\n}\n\nexport type DeleteAssetResult =\n | { outcome: 'deleted' }\n /** The asset is referenced and `confirmInUse` was not set: nothing was removed. */\n | { outcome: 'blocked'; usageCount: number }\n | { outcome: 'not-found' }\n | { outcome: 'error'; message: string }\n\nexport interface DeleteAssetOptions {\n /** Caller has seen the usage count and explicitly confirmed deletion. */\n confirmInUse?: boolean\n}\n\nexport async function deleteAsset(\n deps: DeleteAssetDeps,\n projectId: string,\n assetId: string,\n options: DeleteAssetOptions = {},\n): Promise<DeleteAssetResult> {\n try {\n const asset = await deps.assets.findAsset(projectId, assetId)\n if (!asset) return { outcome: 'not-found' }\n\n // Only consult the reference index when the caller has NOT already confirmed.\n // The delete dialog enumerates usages up-front and confirms with `confirmInUse`,\n // so a confirmed delete skips this redundant inbound query entirely (EC-301 perf).\n if (options.confirmInUse !== true) {\n const usageCount = await assetUsageCount(deps, projectId, assetId)\n if (usageCount > 0) return { outcome: 'blocked', usageCount }\n }\n\n // Bytes first, metadata last: a failure midway leaves a visible metadata\n // record (re-deletable) rather than invisible orphaned bytes.\n await deps.blobs.delete(asset.storageKey)\n // EC-400 (AD-4): the downscaled preview is a SECOND stored blob — delete it\n // too. Best-effort: a leftover thumb blob is harmless wasted storage (its\n // metadata is going), so a thumb-delete failure must not block removing the\n // asset the way an orphaned original would.\n if (asset.thumb) {\n try {\n await deps.blobs.delete(asset.thumb.storageKey)\n } catch {\n // Swallow — the asset delete proceeds; the orphaned thumb blob is inert.\n }\n }\n await deps.assets.removeAsset(projectId, assetId)\n return { outcome: 'deleted' }\n } catch (error) {\n if (error instanceof RecordNotFoundError) return { outcome: 'not-found' }\n return { outcome: 'error', message: error instanceof Error ? error.message : String(error) }\n }\n}\n\n/**\n * Distinct inbound usages of an asset from the reference index (EC-202). One\n * `source + path` (e.g. `post:hello · values/cover`) can carry both a draft and\n * a published index entry — that is a single usage, so variants collapse before\n * counting (the gate is \"is this referenced\", not \"how many index rows\").\n */\nasync function assetUsageCount(\n deps: DeleteAssetDeps,\n projectId: string,\n assetId: string,\n): Promise<number> {\n const inbound = await deps.references.inbound(projectId, { type: 'asset', id: assetId })\n const distinct = new Set(\n inbound.map((entry) => `${entry.source.type}:${entry.source.id}:${entry.path}`),\n )\n return distinct.size\n}\n"]}
@@ -5,5 +5,7 @@
5
5
  */
6
6
  export * from './upload-state.js';
7
7
  export * from './image-dimensions.js';
8
+ export * from './thumbnail.js';
9
+ export * from './backfill-thumbs.js';
8
10
  export * from './upload.js';
9
11
  export * from './delete-asset.js';
@@ -5,6 +5,8 @@
5
5
  */
6
6
  export * from './upload-state.js';
7
7
  export * from './image-dimensions.js';
8
+ export * from './thumbnail.js';
9
+ export * from './backfill-thumbs.js';
8
10
  export * from './upload.js';
9
11
  export * from './delete-asset.js';
10
12
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/lib/assets/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,cAAc,gBAAgB,CAAA;AAC9B,cAAc,oBAAoB,CAAA;AAClC,cAAc,UAAU,CAAA;AACxB,cAAc,gBAAgB,CAAA","sourcesContent":["/**\n * Asset upload, deletion, and usage logic (EC-151). Pure/injectable modules:\n * the studio UI consumes these; storage is reached only through the\n * `BlobStorageAdapter` boundary.\n */\nexport * from './upload-state'\nexport * from './image-dimensions'\nexport * from './upload'\nexport * from './delete-asset'\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/lib/assets/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,cAAc,gBAAgB,CAAA;AAC9B,cAAc,oBAAoB,CAAA;AAClC,cAAc,aAAa,CAAA;AAC3B,cAAc,mBAAmB,CAAA;AACjC,cAAc,UAAU,CAAA;AACxB,cAAc,gBAAgB,CAAA","sourcesContent":["/**\n * Asset upload, deletion, and usage logic (EC-151). Pure/injectable modules:\n * the studio UI consumes these; storage is reached only through the\n * `BlobStorageAdapter` boundary.\n */\nexport * from './upload-state'\nexport * from './image-dimensions'\nexport * from './thumbnail'\nexport * from './backfill-thumbs'\nexport * from './upload'\nexport * from './delete-asset'\n"]}