@elytracms/studio 0.0.24 → 0.0.26

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 (62) hide show
  1. package/convex/cms.ts +49 -7
  2. package/convex/delivery.ts +14 -16
  3. package/convex/references.ts +38 -2
  4. package/convex/schema.ts +6 -1
  5. package/dist/components/assets/asset-library.js +5 -2
  6. package/dist/components/assets/asset-library.js.map +1 -1
  7. package/dist/components/assets/assets-store.js +45 -30
  8. package/dist/components/assets/assets-store.js.map +1 -1
  9. package/dist/components/auth-context.d.ts +4 -2
  10. package/dist/components/auth-context.js +19 -19
  11. package/dist/components/auth-context.js.map +1 -1
  12. package/dist/components/cms/cms-store.js +5 -4
  13. package/dist/components/cms/cms-store.js.map +1 -1
  14. package/dist/components/cms/collection-table.js +10 -3
  15. package/dist/components/cms/collection-table.js.map +1 -1
  16. package/dist/components/cms/detail-editor.js +36 -9
  17. package/dist/components/cms/detail-editor.js.map +1 -1
  18. package/dist/components/cms/field-control.d.ts +2 -1
  19. package/dist/components/cms/field-control.js +166 -74
  20. package/dist/components/cms/field-control.js.map +1 -1
  21. package/dist/components/cms/versions-pane.js +28 -16
  22. package/dist/components/cms/versions-pane.js.map +1 -1
  23. package/dist/components/project-session.d.ts +11 -0
  24. package/dist/components/project-session.js.map +1 -1
  25. package/dist/components/routes/routes-shell.js +22 -22
  26. package/dist/components/routes/routes-shell.js.map +1 -1
  27. package/dist/components/studio-context.js +5 -1
  28. package/dist/components/studio-context.js.map +1 -1
  29. package/dist/lib/assets/delete-asset.js +7 -3
  30. package/dist/lib/assets/delete-asset.js.map +1 -1
  31. package/dist/lib/cms/published-documents.d.ts +6 -6
  32. package/dist/lib/cms/published-documents.js +21 -58
  33. package/dist/lib/cms/published-documents.js.map +1 -1
  34. package/dist/lib/cms-views/view-model.d.ts +7 -3
  35. package/dist/lib/cms-views/view-model.js +11 -5
  36. package/dist/lib/cms-views/view-model.js.map +1 -1
  37. package/dist/lib/data/collection-content.js +7 -5
  38. package/dist/lib/data/collection-content.js.map +1 -1
  39. package/dist/lib/data/data-store.js +34 -24
  40. package/dist/lib/data/data-store.js.map +1 -1
  41. package/dist/lib/persistence/convex-adapter.js +22 -0
  42. package/dist/lib/persistence/convex-adapter.js.map +1 -1
  43. package/dist/lib/persistence/index.js +6 -2
  44. package/dist/lib/persistence/index.js.map +1 -1
  45. package/dist/lib/query/index.d.ts +6 -0
  46. package/dist/lib/query/index.js +6 -0
  47. package/dist/lib/query/index.js.map +1 -0
  48. package/dist/lib/query/operations-query.d.ts +85 -0
  49. package/dist/lib/query/operations-query.js +124 -0
  50. package/dist/lib/query/operations-query.js.map +1 -0
  51. package/dist/lib/references/reference-model.js +7 -1
  52. package/dist/lib/references/reference-model.js.map +1 -1
  53. package/dist/lib/references/references-store.d.ts +5 -10
  54. package/dist/lib/references/references-store.js +61 -77
  55. package/dist/lib/references/references-store.js.map +1 -1
  56. package/dist/ui/collapsible-section.d.ts +20 -0
  57. package/dist/ui/collapsible-section.js +18 -0
  58. package/dist/ui/collapsible-section.js.map +1 -0
  59. package/dist/ui/index.d.ts +1 -0
  60. package/dist/ui/index.js +1 -0
  61. package/dist/ui/index.js.map +1 -1
  62. package/package.json +3 -2
@@ -1 +1 @@
1
- {"version":3,"file":"reference-model.js","sourceRoot":"","sources":["../../../src/lib/references/reference-model.ts"],"names":[],"mappings":"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","sourcesContent":["import type { CmsDocument, CollectionDef } from '@elytracms/core/cms-core'\nimport type {\n ReferenceEntryRecord,\n ReferenceKind,\n ReferenceSource,\n ReferenceTarget,\n} from '@elytracms/core/persistence'\n\n/**\n * Pure presentation model for the EC-172 references panels.\n *\n * The EC-155 index stores entries as `(source, path, target, kind)` per\n * variant; this module turns those records into renderable rows: variants\n * merged onto one row (draft/published badges), human-readable source/\n * location/target labels, and deep-link descriptors that pinpoint the exact\n * field (CMS) or graph node (builder) a path identifies. No data fetching, no\n * React — everything here is a deterministic function of its inputs, so it is\n * fully testable in the node environment.\n */\n\n// --- Path parsing -------------------------------------------------------------\n\n/** Split a `collection:id` document key (the index's document source/target id). */\nexport function parseDocumentKey(key: string): { collection: string; id: string } | null {\n const separator = key.indexOf(':')\n if (separator <= 0 || separator === key.length - 1) return null\n return { collection: key.slice(0, separator), id: key.slice(separator + 1) }\n}\n\nexport interface DocumentFieldPath {\n /** Present for `localized/<locale>/<field>` paths. */\n locale?: string\n field: string\n /** Tokens below the field value (array indices, rich-text node paths). */\n rest: readonly string[]\n}\n\n/**\n * Parse a document index path (`values/<field>/…` or\n * `localized/<locale>/<field>/…`) onto the owning field.\n */\nexport function parseDocumentFieldPath(path: string): DocumentFieldPath | null {\n const tokens = path.split('/')\n if (tokens[0] === 'values' && tokens[1]) {\n return { field: tokens[1], rest: tokens.slice(2) }\n }\n if (tokens[0] === 'localized' && tokens[1] && tokens[2]) {\n return { locale: tokens[1], field: tokens[2], rest: tokens.slice(3) }\n }\n return null\n}\n\n// --- Variant merging ----------------------------------------------------------\n\n/** One panel row: the same logical entry across both variants, with flags. */\nexport interface MergedReferenceEntry {\n /** Stable row key (source + path + target + kind). */\n key: string\n source: ReferenceSource\n path: string\n target: ReferenceTarget\n kind: ReferenceKind\n /** Present in the draft variant of the index. */\n draft: boolean\n /** Present in the published variant (a live reference). */\n published: boolean\n}\n\n/**\n * Merge per-variant index records onto one row per logical entry. A\n * draft-only row (`draft && !published`) is the Sanity hidden-draft-reference\n * case the panel must make visible. Order is deterministic:\n * source type → source id → path → target type → target id → kind.\n */\nexport function mergeReferenceVariants(\n records: readonly ReferenceEntryRecord[],\n): MergedReferenceEntry[] {\n const byKey = new Map<string, MergedReferenceEntry>()\n for (const record of records) {\n const key = `${record.source.type} ${record.source.id} ${record.path} ${record.target.type} ${record.target.id} ${record.kind}`\n const existing = byKey.get(key)\n if (existing) {\n if (record.variant === 'draft') existing.draft = true\n else existing.published = true\n continue\n }\n byKey.set(key, {\n key,\n source: record.source,\n path: record.path,\n target: record.target,\n kind: record.kind,\n draft: record.variant === 'draft',\n published: record.variant === 'published',\n })\n }\n return [...byKey.values()].sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0))\n}\n\n// --- Labels & deep links --------------------------------------------------------\n\n/** Human label per reference kind (entry-row badge). */\nexport const REFERENCE_KIND_LABELS: Record<ReferenceKind, string> = {\n 'relation-field': 'relation',\n 'asset-field': 'asset field',\n 'rich-text-link': 'rich-text link',\n 'rich-text-node': 'rich-text node',\n 'component-usage': 'component usage',\n}\n\n/**\n * A deep-link descriptor the panel renders as a router link. `cms-field`\n * carries the full index path so the document editor can highlight the exact\n * field control; `builder-node` carries the resolved node id so the builder\n * can select + scroll to the node.\n */\nexport type ReferenceDeepLink =\n | { kind: 'cms-field'; collectionId: string; documentId: string; fieldPath?: string }\n | { kind: 'builder-node'; pageId?: string; nodeId?: string }\n | { kind: 'component'; componentId: string }\n | { kind: 'asset'; assetId: string }\n\n/** Everything the model needs to label entries (loaded once per panel). */\nexport interface ReferenceLabelContext {\n collections: readonly CollectionDef[]\n /** Live documents keyed by `collection:id`. */\n documents: ReadonlyMap<string, CmsDocument>\n /** Asset metadata keyed by asset id. */\n assets: ReadonlyMap<string, { filename: string; title?: string | undefined }>\n /** Resolves a component id to its manifest title (live registry lookup). */\n componentTitle: (componentId: string) => string | undefined\n}\n\n/** One fully-labelled panel row. */\nexport interface ReferenceEntryView extends MergedReferenceEntry {\n kindLabel: string\n /** The holding entity, e.g. `Post · Hello World` or `Page · Home`. */\n sourceLabel: string\n /** The exact location inside the source, e.g. `Author` or `Text (n-post-title)`. */\n locationLabel: string\n /** The referenced entity, e.g. `Ada Lovelace` or `cover.jpg`. */\n targetLabel: string\n /** Deep link to the exact source location (field control / graph node). */\n sourceLink: ReferenceDeepLink | null\n /** Link to the referenced entity's surface. */\n targetLink: ReferenceDeepLink | null\n}\n\nfunction fieldLabelOf(collection: CollectionDef | undefined, fieldName: string): string {\n const field = collection?.fields.find((f) => f.name === fieldName)\n return field?.form?.label ?? fieldName\n}\n\nfunction collectionLabelOf(collection: CollectionDef | undefined, collectionId: string): string {\n return collection?.form?.labelSingular ?? collection?.form?.label ?? collectionId\n}\n\n/** Resolve a document's display title (titleField value, default locale first). */\nexport function documentTitleOf(\n collection: CollectionDef | undefined,\n document: CmsDocument | undefined,\n fallback: string,\n): string {\n if (!collection || !document) return fallback\n const field = collection.titleField ?? 'title'\n const direct = document.values[field]\n if (typeof direct === 'string' && direct.length > 0) return direct\n const locales = Object.keys(document.localized ?? {}).sort()\n const preferred = document.defaultLocale\n for (const locale of [preferred, ...locales]) {\n if (!locale) continue\n const value = document.localized?.[locale]?.[field]\n if (typeof value === 'string' && value.length > 0) return value\n }\n return fallback\n}\n\n/** Strip the namespace off a component id for compact labels. */\nexport function shortComponentId(componentId: string): string {\n return componentId.replace('base.primitives.', '').replace('project.', '')\n}\n\ninterface DocumentParts {\n collection: CollectionDef | undefined\n collectionId: string\n documentId: string\n title: string\n}\n\nfunction documentParts(context: ReferenceLabelContext, key: string): DocumentParts | null {\n const ref = parseDocumentKey(key)\n if (!ref) return null\n const collection = context.collections.find((c) => c.id === ref.collection)\n return {\n collection,\n collectionId: ref.collection,\n documentId: ref.id,\n title: documentTitleOf(collection, context.documents.get(key), ref.id),\n }\n}\n\nfunction describeDocumentSource(\n context: ReferenceLabelContext,\n entry: MergedReferenceEntry,\n): { sourceLabel: string; locationLabel: string; sourceLink: ReferenceDeepLink | null } {\n const parts = documentParts(context, entry.source.id)\n if (!parts) {\n return { sourceLabel: entry.source.id, locationLabel: entry.path, sourceLink: null }\n }\n const parsed = parseDocumentFieldPath(entry.path)\n const fieldLabel = parsed ? fieldLabelOf(parts.collection, parsed.field) : entry.path\n const qualifiers: string[] = []\n if (parsed?.locale) qualifiers.push(parsed.locale)\n if (entry.kind === 'rich-text-link') qualifiers.push('link')\n if (entry.kind === 'rich-text-node') qualifiers.push('embedded node')\n if (\n entry.kind === 'relation-field' &&\n parsed &&\n parsed.rest.length === 1 &&\n /^\\d+$/.test(parsed.rest[0]!)\n ) {\n qualifiers.push(`item ${parsed.rest[0]}`)\n }\n return {\n sourceLabel: `${collectionLabelOf(parts.collection, parts.collectionId)} · ${parts.title}`,\n locationLabel: qualifiers.length > 0 ? `${fieldLabel} (${qualifiers.join(', ')})` : fieldLabel,\n sourceLink: {\n kind: 'cms-field',\n collectionId: parts.collectionId,\n documentId: parts.documentId,\n fieldPath: entry.path,\n },\n }\n}\n\n\nfunction describeTarget(\n context: ReferenceLabelContext,\n target: ReferenceTarget,\n): { targetLabel: string; targetLink: ReferenceDeepLink | null } {\n switch (target.type) {\n case 'document': {\n const parts = documentParts(context, target.id)\n if (!parts) return { targetLabel: target.id, targetLink: null }\n return {\n targetLabel: `${collectionLabelOf(parts.collection, parts.collectionId)} · ${parts.title}`,\n targetLink: {\n kind: 'cms-field',\n collectionId: parts.collectionId,\n documentId: parts.documentId,\n },\n }\n }\n case 'component':\n return {\n targetLabel: context.componentTitle(target.id) ?? shortComponentId(target.id),\n targetLink: { kind: 'component', componentId: target.id },\n }\n case 'asset': {\n const asset = context.assets.get(target.id)\n return {\n targetLabel: asset ? (asset.title ?? asset.filename) : target.id,\n targetLink: { kind: 'asset', assetId: target.id },\n }\n }\n }\n}\n\n/** Label + link one merged entry (see {@link buildReferenceEntryViews}). */\nexport function buildReferenceEntryView(\n entry: MergedReferenceEntry,\n context: ReferenceLabelContext,\n): ReferenceEntryView {\n // EC-287: a reference source is only ever a CMS document (structure is code).\n const sourceParts = describeDocumentSource(context, entry)\n return {\n ...entry,\n kindLabel: REFERENCE_KIND_LABELS[entry.kind],\n ...sourceParts,\n ...describeTarget(context, entry.target),\n }\n}\n\n/** Merge variants and label every record — the panel's full render model. */\nexport function buildReferenceEntryViews(\n records: readonly ReferenceEntryRecord[],\n context: ReferenceLabelContext,\n): ReferenceEntryView[] {\n return mergeReferenceVariants(records).map((entry) => buildReferenceEntryView(entry, context))\n}\n"]}
1
+ {"version":3,"file":"reference-model.js","sourceRoot":"","sources":["../../../src/lib/references/reference-model.ts"],"names":[],"mappings":"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","sourcesContent":["import { resolveDocumentTitle } from '@elytracms/core/cms-core'\nimport type { CmsDocument, CollectionDef } from '@elytracms/core/cms-core'\nimport type {\n ReferenceEntryRecord,\n ReferenceKind,\n ReferenceSource,\n ReferenceTarget,\n} from '@elytracms/core/persistence'\n\n/**\n * Pure presentation model for the EC-172 references panels.\n *\n * The EC-155 index stores entries as `(source, path, target, kind)` per\n * variant; this module turns those records into renderable rows: variants\n * merged onto one row (draft/published badges), human-readable source/\n * location/target labels, and deep-link descriptors that pinpoint the exact\n * field (CMS) or graph node (builder) a path identifies. No data fetching, no\n * React — everything here is a deterministic function of its inputs, so it is\n * fully testable in the node environment.\n */\n\n// --- Path parsing -------------------------------------------------------------\n\n/** Split a `collection:id` document key (the index's document source/target id). */\nexport function parseDocumentKey(key: string): { collection: string; id: string } | null {\n const separator = key.indexOf(':')\n if (separator <= 0 || separator === key.length - 1) return null\n return { collection: key.slice(0, separator), id: key.slice(separator + 1) }\n}\n\nexport interface DocumentFieldPath {\n /** Present for `localized/<locale>/<field>` paths. */\n locale?: string\n field: string\n /** Tokens below the field value (array indices, rich-text node paths). */\n rest: readonly string[]\n}\n\n/**\n * Parse a document index path (`values/<field>/…` or\n * `localized/<locale>/<field>/…`) onto the owning field.\n */\nexport function parseDocumentFieldPath(path: string): DocumentFieldPath | null {\n const tokens = path.split('/')\n if (tokens[0] === 'values' && tokens[1]) {\n return { field: tokens[1], rest: tokens.slice(2) }\n }\n if (tokens[0] === 'localized' && tokens[1] && tokens[2]) {\n return { locale: tokens[1], field: tokens[2], rest: tokens.slice(3) }\n }\n return null\n}\n\n// --- Variant merging ----------------------------------------------------------\n\n/** One panel row: the same logical entry across both variants, with flags. */\nexport interface MergedReferenceEntry {\n /** Stable row key (source + path + target + kind). */\n key: string\n source: ReferenceSource\n path: string\n target: ReferenceTarget\n kind: ReferenceKind\n /** Present in the draft variant of the index. */\n draft: boolean\n /** Present in the published variant (a live reference). */\n published: boolean\n}\n\n/**\n * Merge per-variant index records onto one row per logical entry. A\n * draft-only row (`draft && !published`) is the Sanity hidden-draft-reference\n * case the panel must make visible. Order is deterministic:\n * source type → source id → path → target type → target id → kind.\n */\nexport function mergeReferenceVariants(\n records: readonly ReferenceEntryRecord[],\n): MergedReferenceEntry[] {\n const byKey = new Map<string, MergedReferenceEntry>()\n for (const record of records) {\n const key = `${record.source.type} ${record.source.id} ${record.path} ${record.target.type} ${record.target.id} ${record.kind}`\n const existing = byKey.get(key)\n if (existing) {\n if (record.variant === 'draft') existing.draft = true\n else existing.published = true\n continue\n }\n byKey.set(key, {\n key,\n source: record.source,\n path: record.path,\n target: record.target,\n kind: record.kind,\n draft: record.variant === 'draft',\n published: record.variant === 'published',\n })\n }\n return [...byKey.values()].sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0))\n}\n\n// --- Labels & deep links --------------------------------------------------------\n\n/** Human label per reference kind (entry-row badge). */\nexport const REFERENCE_KIND_LABELS: Record<ReferenceKind, string> = {\n 'relation-field': 'relation',\n 'asset-field': 'asset field',\n 'rich-text-link': 'rich-text link',\n 'rich-text-node': 'rich-text node',\n 'component-usage': 'component usage',\n}\n\n/**\n * A deep-link descriptor the panel renders as a router link. `cms-field`\n * carries the full index path so the document editor can highlight the exact\n * field control; `builder-node` carries the resolved node id so the builder\n * can select + scroll to the node.\n */\nexport type ReferenceDeepLink =\n | { kind: 'cms-field'; collectionId: string; documentId: string; fieldPath?: string }\n | { kind: 'builder-node'; pageId?: string; nodeId?: string }\n | { kind: 'component'; componentId: string }\n | { kind: 'asset'; assetId: string }\n\n/** Everything the model needs to label entries (loaded once per panel). */\nexport interface ReferenceLabelContext {\n collections: readonly CollectionDef[]\n /** Live documents keyed by `collection:id`. */\n documents: ReadonlyMap<string, CmsDocument>\n /** Asset metadata keyed by asset id. */\n assets: ReadonlyMap<string, { filename: string; title?: string | undefined }>\n /** Resolves a component id to its manifest title (live registry lookup). */\n componentTitle: (componentId: string) => string | undefined\n}\n\n/** One fully-labelled panel row. */\nexport interface ReferenceEntryView extends MergedReferenceEntry {\n kindLabel: string\n /** The holding entity, e.g. `Post · Hello World` or `Page · Home`. */\n sourceLabel: string\n /** The exact location inside the source, e.g. `Author` or `Text (n-post-title)`. */\n locationLabel: string\n /** The referenced entity, e.g. `Ada Lovelace` or `cover.jpg`. */\n targetLabel: string\n /** Deep link to the exact source location (field control / graph node). */\n sourceLink: ReferenceDeepLink | null\n /** Link to the referenced entity's surface. */\n targetLink: ReferenceDeepLink | null\n}\n\nfunction fieldLabelOf(collection: CollectionDef | undefined, fieldName: string): string {\n const field = collection?.fields.find((f) => f.name === fieldName)\n return field?.form?.label ?? fieldName\n}\n\nfunction collectionLabelOf(collection: CollectionDef | undefined, collectionId: string): string {\n return collection?.form?.labelSingular ?? collection?.form?.label ?? collectionId\n}\n\n/** Resolve a document's display title (titleField value, default locale first). */\nexport function documentTitleOf(\n collection: CollectionDef | undefined,\n document: CmsDocument | undefined,\n fallback: string,\n): string {\n if (!collection || !document) return fallback\n // EC-242: a function `titleField` (title pattern) has no single source field —\n // resolve it from the base values via the shared identity resolver.\n if (typeof collection.titleField === 'function') {\n return resolveDocumentTitle(collection, document.values, fallback)\n }\n const field = typeof collection.titleField === 'string' ? collection.titleField : 'title'\n const direct = document.values[field]\n if (typeof direct === 'string' && direct.length > 0) return direct\n const locales = Object.keys(document.localized ?? {}).sort()\n const preferred = document.defaultLocale\n for (const locale of [preferred, ...locales]) {\n if (!locale) continue\n const value = document.localized?.[locale]?.[field]\n if (typeof value === 'string' && value.length > 0) return value\n }\n return fallback\n}\n\n/** Strip the namespace off a component id for compact labels. */\nexport function shortComponentId(componentId: string): string {\n return componentId.replace('base.primitives.', '').replace('project.', '')\n}\n\ninterface DocumentParts {\n collection: CollectionDef | undefined\n collectionId: string\n documentId: string\n title: string\n}\n\nfunction documentParts(context: ReferenceLabelContext, key: string): DocumentParts | null {\n const ref = parseDocumentKey(key)\n if (!ref) return null\n const collection = context.collections.find((c) => c.id === ref.collection)\n return {\n collection,\n collectionId: ref.collection,\n documentId: ref.id,\n title: documentTitleOf(collection, context.documents.get(key), ref.id),\n }\n}\n\nfunction describeDocumentSource(\n context: ReferenceLabelContext,\n entry: MergedReferenceEntry,\n): { sourceLabel: string; locationLabel: string; sourceLink: ReferenceDeepLink | null } {\n const parts = documentParts(context, entry.source.id)\n if (!parts) {\n return { sourceLabel: entry.source.id, locationLabel: entry.path, sourceLink: null }\n }\n const parsed = parseDocumentFieldPath(entry.path)\n const fieldLabel = parsed ? fieldLabelOf(parts.collection, parsed.field) : entry.path\n const qualifiers: string[] = []\n if (parsed?.locale) qualifiers.push(parsed.locale)\n if (entry.kind === 'rich-text-link') qualifiers.push('link')\n if (entry.kind === 'rich-text-node') qualifiers.push('embedded node')\n if (\n entry.kind === 'relation-field' &&\n parsed &&\n parsed.rest.length === 1 &&\n /^\\d+$/.test(parsed.rest[0]!)\n ) {\n qualifiers.push(`item ${parsed.rest[0]}`)\n }\n return {\n sourceLabel: `${collectionLabelOf(parts.collection, parts.collectionId)} · ${parts.title}`,\n locationLabel: qualifiers.length > 0 ? `${fieldLabel} (${qualifiers.join(', ')})` : fieldLabel,\n sourceLink: {\n kind: 'cms-field',\n collectionId: parts.collectionId,\n documentId: parts.documentId,\n fieldPath: entry.path,\n },\n }\n}\n\n\nfunction describeTarget(\n context: ReferenceLabelContext,\n target: ReferenceTarget,\n): { targetLabel: string; targetLink: ReferenceDeepLink | null } {\n switch (target.type) {\n case 'document': {\n const parts = documentParts(context, target.id)\n if (!parts) return { targetLabel: target.id, targetLink: null }\n return {\n targetLabel: `${collectionLabelOf(parts.collection, parts.collectionId)} · ${parts.title}`,\n targetLink: {\n kind: 'cms-field',\n collectionId: parts.collectionId,\n documentId: parts.documentId,\n },\n }\n }\n case 'component':\n return {\n targetLabel: context.componentTitle(target.id) ?? shortComponentId(target.id),\n targetLink: { kind: 'component', componentId: target.id },\n }\n case 'asset': {\n const asset = context.assets.get(target.id)\n return {\n targetLabel: asset ? (asset.title ?? asset.filename) : target.id,\n targetLink: { kind: 'asset', assetId: target.id },\n }\n }\n }\n}\n\n/** Label + link one merged entry (see {@link buildReferenceEntryViews}). */\nexport function buildReferenceEntryView(\n entry: MergedReferenceEntry,\n context: ReferenceLabelContext,\n): ReferenceEntryView {\n // EC-287: a reference source is only ever a CMS document (structure is code).\n const sourceParts = describeDocumentSource(context, entry)\n return {\n ...entry,\n kindLabel: REFERENCE_KIND_LABELS[entry.kind],\n ...sourceParts,\n ...describeTarget(context, entry.target),\n }\n}\n\n/** Merge variants and label every record — the panel's full render model. */\nexport function buildReferenceEntryViews(\n records: readonly ReferenceEntryRecord[],\n context: ReferenceLabelContext,\n): ReferenceEntryView[] {\n return mergeReferenceVariants(records).map((entry) => buildReferenceEntryView(entry, context))\n}\n"]}
@@ -56,13 +56,6 @@ export declare function loadInboundAssetReferenceViews(projectId: string, assetI
56
56
  * non-succeeded / non-command event) never trigger a reload.
57
57
  */
58
58
  export declare function isReferenceRefreshEvent(event: OperationEvent): boolean;
59
- /**
60
- * Trailing-debounce window (ms) for event-driven panel reloads (EC-269): a burst
61
- * of content commands — e.g. one `documents.upsert` per keystroke from autosave —
62
- * coalesces into a SINGLE reload once the writes settle, instead of one full
63
- * (multi-query) reload per event. The initial mount load stays immediate.
64
- */
65
- export declare const REFERENCE_RELOAD_DEBOUNCE_MS = 300;
66
59
  export type ReferencePanelStatus = 'loading' | 'ready' | 'error';
67
60
  export interface ReferencePanelState {
68
61
  status: ReferencePanelStatus;
@@ -71,8 +64,10 @@ export interface ReferencePanelState {
71
64
  refresh: () => void;
72
65
  }
73
66
  /**
74
- * Live panel state for one entity: loads on mount, exposes explicit
75
- * loading/error states, and re-queries whenever a content-bearing command
76
- * succeeds anywhere in the studio (the index is maintained on write).
67
+ * Live panel state for one entity (EC-308/AD-17): loads + caches through the one
68
+ * `useOperationQuery` seam, and re-queries whenever a content-bearing command
69
+ * succeeds anywhere in the studio (the index is maintained on write). Per-direction
70
+ * read errors degrade INSIDE `loadReferencePanelData` (carried on the data); a hard
71
+ * failure (broken backend) surfaces as the query's error → `status: 'error'`.
77
72
  */
78
73
  export declare function useReferencePanel(query: ReferencePanelQuery): ReferencePanelState;
@@ -1,5 +1,6 @@
1
- import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
1
+ import { useMemo } from 'react';
2
2
  import { documentKey } from '@elytracms/core/cms-core';
3
+ import { useOperationQuery } from '@elytracms/studio/lib/query';
3
4
  import { getStudioOperations } from '../operations';
4
5
  import { getStudioRegistry } from '../registry';
5
6
  import { buildReferenceEntryViews } from './reference-model';
@@ -105,6 +106,13 @@ export async function loadReferencePanelData(query) {
105
106
  : { records: [], error: result.error.message };
106
107
  const out = settle(outbound);
107
108
  const inb = settle(inbound);
109
+ // EC-302: nothing referenced in either direction → no sources/targets to label,
110
+ // so skip the label-context load entirely (schema + ALL documents + ALL assets).
111
+ // Common for leaf content; mirrors the asset short-circuit in
112
+ // `loadInboundAssetReferenceViews`.
113
+ if (out.records.length === 0 && inb.records.length === 0) {
114
+ return { outbound: [], inbound: [], outboundError: out.error, inboundError: inb.error };
115
+ }
108
116
  const context = await loadLabelContext(operations, query.projectId);
109
117
  return {
110
118
  outbound: buildReferenceEntryViews(out.records, context),
@@ -126,13 +134,26 @@ export async function loadInboundReferenceViews(projectId, targets) {
126
134
  // Observe a settled index: maintained-on-write updates run async after events.
127
135
  await studio.references.flush();
128
136
  const operations = studio.operations;
129
- const recordsPerTarget = await Promise.all(targets.map((ref) => operations.references
130
- .inbound({ projectId, target: { type: 'document', id: documentKey(ref) } })
137
+ // EC-308: one batched query for ALL targets (the operation loops the by_target
138
+ // index server-side) instead of a per-target `inbound` fan-out. Returns one
139
+ // record list per target, in `targets` order.
140
+ const recordsPerTarget = await operations.references
141
+ .inboundMany({
142
+ projectId,
143
+ targets: targets.map((ref) => ({ type: 'document', id: documentKey(ref) })),
144
+ })
131
145
  .then((result) => {
132
146
  if (!result.ok)
133
147
  throw new Error(result.error.message);
134
148
  return result.value;
135
- })));
149
+ });
150
+ // EC-302: no inbound refs for ANY target → no sources to label, so skip the
151
+ // label-context load (schema + ALL documents + ALL assets). The common case when
152
+ // deleting/unpublishing unreferenced documents (incl. bulk); mirrors the EC-301
153
+ // short-circuit already in `loadInboundAssetReferenceViews`.
154
+ if (recordsPerTarget.every((records) => records.length === 0)) {
155
+ return targets.map((ref) => ({ ref, inbound: [] }));
156
+ }
136
157
  const context = await loadLabelContext(operations, projectId);
137
158
  return targets.map((ref, index) => ({
138
159
  ref,
@@ -158,6 +179,12 @@ export async function loadInboundAssetReferenceViews(projectId, assetId) {
158
179
  });
159
180
  if (!result.ok)
160
181
  throw new Error(result.error.message);
182
+ // No inbound references → no sources to label, so skip the label-context load
183
+ // entirely (schema + ALL documents + ALL assets). This is the common case for a
184
+ // deletable asset, making the delete dialog's pre-flight a single indexed query
185
+ // instead of three full-table lists (EC-301 perf).
186
+ if (result.value.length === 0)
187
+ return [];
161
188
  const context = await loadLabelContext(operations, projectId);
162
189
  return buildReferenceEntryViews(result.value, context);
163
190
  }
@@ -171,13 +198,14 @@ export async function loadInboundAssetReferenceViews(projectId, assetId) {
171
198
  * reloads per document. The index is maintained on write; the panel reacts to the
172
199
  * CONTENT domains that feed it, never to its own index writes.
173
200
  */
174
- const REFRESH_DOMAINS = new Set([
201
+ const REFERENCE_REFRESH_TRIGGERS = [
175
202
  'documents',
176
203
  'graph',
177
204
  'publishing',
178
205
  'assets',
179
206
  'schema',
180
- ]);
207
+ ];
208
+ const REFRESH_DOMAINS = new Set(REFERENCE_REFRESH_TRIGGERS);
181
209
  /**
182
210
  * Whether one operation event should refresh an open reference panel. Exported
183
211
  * for tests — pins the EC-269 contract that reference-index commands (and any
@@ -187,16 +215,11 @@ export function isReferenceRefreshEvent(event) {
187
215
  return (event.kind === 'command' && event.status === 'succeeded' && REFRESH_DOMAINS.has(event.domain));
188
216
  }
189
217
  /**
190
- * Trailing-debounce window (ms) for event-driven panel reloads (EC-269): a burst
191
- * of content commands — e.g. one `documents.upsert` per keystroke from autosave —
192
- * coalesces into a SINGLE reload once the writes settle, instead of one full
193
- * (multi-query) reload per event. The initial mount load stays immediate.
194
- */
195
- export const REFERENCE_RELOAD_DEBOUNCE_MS = 300;
196
- /**
197
- * Live panel state for one entity: loads on mount, exposes explicit
198
- * loading/error states, and re-queries whenever a content-bearing command
199
- * succeeds anywhere in the studio (the index is maintained on write).
218
+ * Live panel state for one entity (EC-308/AD-17): loads + caches through the one
219
+ * `useOperationQuery` seam, and re-queries whenever a content-bearing command
220
+ * succeeds anywhere in the studio (the index is maintained on write). Per-direction
221
+ * read errors degrade INSIDE `loadReferencePanelData` (carried on the data); a hard
222
+ * failure (broken backend) surfaces as the query's error → `status: 'error'`.
200
223
  */
201
224
  export function useReferencePanel(query) {
202
225
  const { projectId } = query;
@@ -209,67 +232,28 @@ export function useReferencePanel(query) {
209
232
  ...(sourceType && sourceId ? { source: { type: sourceType, id: sourceId } } : {}),
210
233
  ...(targetType && targetId ? { target: { type: targetType, id: targetId } } : {}),
211
234
  }), [projectId, sourceType, sourceId, targetType, targetId]);
212
- const [state, setState] = useState({
213
- status: 'loading',
214
- data: null,
215
- errorMessage: null,
235
+ const result = useOperationQuery({
236
+ queryKey: [
237
+ 'referencePanel',
238
+ projectId,
239
+ sourceType ?? '',
240
+ sourceId ?? '',
241
+ targetType ?? '',
242
+ targetId ?? '',
243
+ ],
244
+ loader: () => loadReferencePanelData(stableQuery),
245
+ invalidateOn: REFERENCE_REFRESH_TRIGGERS,
216
246
  });
217
- const epochRef = useRef(0);
218
- const load = useCallback(async () => {
219
- const epoch = ++epochRef.current;
220
- try {
221
- const data = await loadReferencePanelData(stableQuery);
222
- if (epochRef.current === epoch) {
223
- setState({ status: 'ready', data, errorMessage: null });
224
- }
225
- }
226
- catch (cause) {
227
- if (epochRef.current === epoch) {
228
- setState({
229
- status: 'error',
230
- data: null,
231
- errorMessage: cause instanceof Error ? cause.message : String(cause),
232
- });
233
- }
234
- }
235
- }, [stableQuery]);
236
- useEffect(() => {
237
- // Initial load is immediate; event-driven reloads are trailing-debounced so a
238
- // burst of content commands (per-keystroke autosave) coalesces into one reload
239
- // (EC-269).
240
- setState({ status: 'loading', data: null, errorMessage: null });
241
- void load();
242
- let disposed = false;
243
- let unsubscribe;
244
- let reloadTimer;
245
- const scheduleReload = () => {
246
- if (reloadTimer)
247
- clearTimeout(reloadTimer);
248
- reloadTimer = setTimeout(() => {
249
- reloadTimer = undefined;
250
- void load();
251
- }, REFERENCE_RELOAD_DEBOUNCE_MS);
252
- };
253
- void getStudioOperations().then((studio) => {
254
- if (disposed)
255
- return;
256
- unsubscribe = studio.operations.events.subscribe((event) => {
257
- if (isReferenceRefreshEvent(event))
258
- scheduleReload();
259
- });
260
- });
261
- return () => {
262
- disposed = true;
263
- epochRef.current += 1;
264
- if (reloadTimer)
265
- clearTimeout(reloadTimer);
266
- unsubscribe?.();
267
- };
268
- }, [load]);
269
- const refresh = useCallback(() => {
270
- setState((previous) => ({ ...previous, status: 'loading' }));
271
- void load();
272
- }, [load]);
273
- return { ...state, refresh };
247
+ const status = result.isPending
248
+ ? 'loading'
249
+ : result.isError
250
+ ? 'error'
251
+ : 'ready';
252
+ return {
253
+ status,
254
+ data: result.data ?? null,
255
+ errorMessage: result.error instanceof Error ? result.error.message : null,
256
+ refresh: () => void result.refetch(),
257
+ };
274
258
  }
275
259
  //# sourceMappingURL=references-store.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"references-store.js","sourceRoot":"","sources":["../../../src/lib/references/references-store.ts"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport { documentKey } from '@elytracms/core/cms-core'\nimport type { CmsDocument, DocumentRef } from '@elytracms/core/cms-core'\nimport type {\n ReferenceEntryRecord,\n ReferenceSource,\n ReferenceTarget,\n} from '@elytracms/core/persistence'\nimport { getStudioOperations } from '../operations'\nimport type { OperationEvent, OperationsClient, StudioOperations } from '../operations'\nimport { getStudioRegistry } from '../registry'\nimport { buildReferenceEntryViews } from './reference-model'\nimport type { ReferenceEntryView, ReferenceLabelContext } from './reference-model'\n\n/**\n * Data layer for the EC-172 references panels. All reads go through\n * `@elytracms/core/operations` queries (`references.outbound` / `references.inbound`\n * plus the labeling queries) — UI code never touches persistence repositories\n * or index tables. The hook keeps panels live: any succeeded command in a\n * content-bearing domain re-queries after the EC-155 maintained-on-write\n * attachment has flushed.\n */\n\nexport interface ReferencePanelQuery {\n projectId: string\n /** Enables the outbound (\"references\") section. */\n source?: ReferenceSource | undefined\n /** Enables the inbound (\"used by\") section. */\n target?: ReferenceTarget | undefined\n}\n\nexport interface ReferencePanelData {\n outbound: ReferenceEntryView[]\n inbound: ReferenceEntryView[]\n /**\n * Per-direction read error (EC-205): a direction whose query returns a\n * structured non-ok degrades to an empty list plus this message, surfaced as a\n * visible per-section error — instead of collapsing the whole panel into its\n * hard-error state. `null` when the direction loaded (or was not requested).\n */\n outboundError: string | null\n inboundError: string | null\n}\n\n// --- Index bootstrap -----------------------------------------------------------\n\n/**\n * The EC-155 index is maintained on write, but seeded/imported projects start\n * with an empty index. {@link ensureReferenceIndex} seeds it once per project per\n * operations client; failures (e.g. a viewer's command denial) degrade to\n * whatever the maintained-on-write index already holds.\n */\nconst backfillsByClient = new WeakMap<StudioOperations, Map<string, Promise<void>>>()\n\n/**\n * Seed the index once if it has never been backfilled, then never again — the\n * EC-269 mutation-free read path. The index is maintained on write, so the\n * (clear + rebuild) `backfill` is only needed to seed a never-built index. A\n * persisted marker (`references.buildMarker`, a read) records completion; when\n * present this returns after a single query with ZERO mutations — so opening the\n * References panel no longer rewrites the whole index every session (the normal\n * Convex case: the EC-233 import already backfilled). Only a genuinely empty/\n * never-built project still pays the one-time backfill (and a viewer who cannot\n * run it degrades to the maintained-on-write index, as before).\n */\nasync function seedReferenceIndex(studio: StudioOperations, projectId: string): Promise<void> {\n try {\n const marker = await studio.operations.references.buildMarker({ projectId })\n if (marker.ok && marker.value !== null) return\n } catch {\n // A marker-read failure falls through to the backfill (best-effort seed).\n }\n await studio.operations.references.backfill({ projectId }).then(\n () => undefined,\n () => undefined,\n )\n}\n\nexport function ensureReferenceIndex(studio: StudioOperations, projectId: string): Promise<void> {\n let byProject = backfillsByClient.get(studio)\n if (!byProject) {\n byProject = new Map()\n backfillsByClient.set(studio, byProject)\n }\n let pending = byProject.get(projectId)\n if (!pending) {\n pending = seedReferenceIndex(studio, projectId)\n byProject.set(projectId, pending)\n }\n return pending\n}\n\n// --- Label context loading -------------------------------------------------------\n\nasync function loadLabelContext(\n operations: OperationsClient,\n projectId: string,\n): Promise<ReferenceLabelContext> {\n const [schemaResult, documentsResult, assetsResult] = await Promise.all([\n operations.schema.get({ projectId }),\n operations.documents.list({ projectId }),\n operations.assets.list({ projectId }),\n ])\n\n const collections = schemaResult.ok ? schemaResult.value.collections : []\n const documents = new Map<string, CmsDocument>()\n if (documentsResult.ok) {\n for (const record of documentsResult.value) {\n documents.set(\n documentKey({ collection: record.document.collection, id: record.document.id }),\n record.document,\n )\n }\n }\n // EC-287: the project graph is retired; reference source labels resolve from\n // CMS documents only. `page`/`layout` graph sources no longer exist.\n const assets = new Map<string, { filename: string; title?: string | undefined }>()\n if (assetsResult.ok) {\n for (const asset of assetsResult.value) {\n assets.set(asset.id, { filename: asset.filename, title: asset.title })\n }\n }\n const registry = getStudioRegistry().registry\n return {\n collections,\n documents,\n assets,\n componentTitle: (componentId) => registry.getManifest(componentId)?.title,\n }\n}\n\n// --- Panel data loading -----------------------------------------------------------\n\n/** Load both directions + labels for one panel (operations queries only). */\nexport async function loadReferencePanelData(\n query: ReferencePanelQuery,\n): Promise<ReferencePanelData> {\n const studio = await getStudioOperations()\n await ensureReferenceIndex(studio, query.projectId)\n // Observe a settled index: maintained-on-write updates run async after events.\n await studio.references.flush()\n const operations = studio.operations\n\n // EC-205: load each direction INDEPENDENTLY. A structured non-ok from one\n // direction is a degradable operation result (it carries an OperationError —\n // the documented contract), not an exception: degrade that direction to empty\n // and surface its error per-section (references-panel) while the other still\n // renders, instead of throwing the whole panel into its hard-error state. A\n // genuinely broken backend still throws from getStudioOperations/flush above.\n const [outbound, inbound] = await Promise.all([\n query.source\n ? operations.references.outbound({ projectId: query.projectId, source: query.source })\n : null,\n query.target\n ? operations.references.inbound({ projectId: query.projectId, target: query.target })\n : null,\n ])\n\n type DirectionResult = Awaited<ReturnType<typeof operations.references.outbound>>\n const settle = (\n result: DirectionResult | null,\n ): { records: ReferenceEntryRecord[]; error: string | null } =>\n result === null\n ? { records: [], error: null }\n : result.ok\n ? { records: result.value, error: null }\n : { records: [], error: result.error.message }\n const out = settle(outbound)\n const inb = settle(inbound)\n\n const context = await loadLabelContext(operations, query.projectId)\n return {\n outbound: buildReferenceEntryViews(out.records, context),\n inbound: buildReferenceEntryViews(inb.records, context),\n outboundError: out.error,\n inboundError: inb.error,\n }\n}\n\n// --- Impact loading (EC-173 safe unpublish/delete) -------------------------------\n\n/**\n * Load the labelled inbound entry views for a set of target documents in ONE\n * pass (one index flush, one label-context load) — the pre-flight data of the\n * EC-173 confirmation dialogs, including the bulk flows. Returns entries in\n * `targets` order; pair with `buildReferenceImpact` for break/block semantics.\n */\nexport async function loadInboundReferenceViews(\n projectId: string,\n targets: readonly DocumentRef[],\n): Promise<{ ref: DocumentRef; inbound: ReferenceEntryView[] }[]> {\n const studio = await getStudioOperations()\n await ensureReferenceIndex(studio, projectId)\n // Observe a settled index: maintained-on-write updates run async after events.\n await studio.references.flush()\n const operations = studio.operations\n\n const recordsPerTarget = await Promise.all(\n targets.map((ref) =>\n operations.references\n .inbound({ projectId, target: { type: 'document', id: documentKey(ref) } })\n .then((result) => {\n if (!result.ok) throw new Error(result.error.message)\n return result.value\n }),\n ),\n )\n const context = await loadLabelContext(operations, projectId)\n return targets.map((ref, index) => ({\n ref,\n inbound: buildReferenceEntryViews(recordsPerTarget[index] ?? [], context),\n }))\n}\n\n/**\n * Inbound reference views for ONE asset (EC-202): the \"what uses this asset\"\n * pre-flight of the asset-delete dialog, index-accurate (the EC-155 reference\n * index, not the retired `find-asset-usages` string scan). Mirrors\n * {@link loadInboundReferenceViews} for a single `{ type: 'asset' }` target —\n * the source labels resolve the referencing documents/graph-nodes identically.\n */\nexport async function loadInboundAssetReferenceViews(\n projectId: string,\n assetId: string,\n): Promise<ReferenceEntryView[]> {\n const studio = await getStudioOperations()\n await ensureReferenceIndex(studio, projectId)\n // Observe a settled index: maintained-on-write updates run async after events.\n await studio.references.flush()\n const operations = studio.operations\n const result = await operations.references.inbound({\n projectId,\n target: { type: 'asset', id: assetId },\n })\n if (!result.ok) throw new Error(result.error.message)\n const context = await loadLabelContext(operations, projectId)\n return buildReferenceEntryViews(result.value, context)\n}\n\n// --- React hook ----------------------------------------------------------------\n\n/**\n * Domains whose successful commands can change the reference index or labels and\n * should refresh an open panel. `references` is deliberately ABSENT (EC-269):\n * reacting to reference-index command events (the `backfill`) would make the\n * panel re-trigger on its OWN index maintenance — a self-cascade that, compounded\n * with the per-keystroke `documents` events, produced hundreds of redundant\n * reloads per document. The index is maintained on write; the panel reacts to the\n * CONTENT domains that feed it, never to its own index writes.\n */\nconst REFRESH_DOMAINS: ReadonlySet<string> = new Set([\n 'documents',\n 'graph',\n 'publishing',\n 'assets',\n 'schema',\n])\n\n/**\n * Whether one operation event should refresh an open reference panel. Exported\n * for tests — pins the EC-269 contract that reference-index commands (and any\n * non-succeeded / non-command event) never trigger a reload.\n */\nexport function isReferenceRefreshEvent(event: OperationEvent): boolean {\n return (\n event.kind === 'command' && event.status === 'succeeded' && REFRESH_DOMAINS.has(event.domain)\n )\n}\n\n/**\n * Trailing-debounce window (ms) for event-driven panel reloads (EC-269): a burst\n * of content commands — e.g. one `documents.upsert` per keystroke from autosave —\n * coalesces into a SINGLE reload once the writes settle, instead of one full\n * (multi-query) reload per event. The initial mount load stays immediate.\n */\nexport const REFERENCE_RELOAD_DEBOUNCE_MS = 300\n\nexport type ReferencePanelStatus = 'loading' | 'ready' | 'error'\n\nexport interface ReferencePanelState {\n status: ReferencePanelStatus\n data: ReferencePanelData | null\n errorMessage: string | null\n refresh: () => void\n}\n\n/**\n * Live panel state for one entity: loads on mount, exposes explicit\n * loading/error states, and re-queries whenever a content-bearing command\n * succeeds anywhere in the studio (the index is maintained on write).\n */\nexport function useReferencePanel(query: ReferencePanelQuery): ReferencePanelState {\n const { projectId } = query\n const sourceType = query.source?.type\n const sourceId = query.source?.id\n const targetType = query.target?.type\n const targetId = query.target?.id\n\n const stableQuery = useMemo<ReferencePanelQuery>(\n () => ({\n projectId,\n ...(sourceType && sourceId ? { source: { type: sourceType, id: sourceId } } : {}),\n ...(targetType && targetId ? { target: { type: targetType, id: targetId } } : {}),\n }),\n [projectId, sourceType, sourceId, targetType, targetId],\n )\n\n const [state, setState] = useState<Omit<ReferencePanelState, 'refresh'>>({\n status: 'loading',\n data: null,\n errorMessage: null,\n })\n const epochRef = useRef(0)\n\n const load = useCallback(async () => {\n const epoch = ++epochRef.current\n try {\n const data = await loadReferencePanelData(stableQuery)\n if (epochRef.current === epoch) {\n setState({ status: 'ready', data, errorMessage: null })\n }\n } catch (cause) {\n if (epochRef.current === epoch) {\n setState({\n status: 'error',\n data: null,\n errorMessage: cause instanceof Error ? cause.message : String(cause),\n })\n }\n }\n }, [stableQuery])\n\n useEffect(() => {\n // Initial load is immediate; event-driven reloads are trailing-debounced so a\n // burst of content commands (per-keystroke autosave) coalesces into one reload\n // (EC-269).\n setState({ status: 'loading', data: null, errorMessage: null })\n void load()\n let disposed = false\n let unsubscribe: (() => void) | undefined\n let reloadTimer: ReturnType<typeof setTimeout> | undefined\n const scheduleReload = () => {\n if (reloadTimer) clearTimeout(reloadTimer)\n reloadTimer = setTimeout(() => {\n reloadTimer = undefined\n void load()\n }, REFERENCE_RELOAD_DEBOUNCE_MS)\n }\n void getStudioOperations().then((studio) => {\n if (disposed) return\n unsubscribe = studio.operations.events.subscribe((event) => {\n if (isReferenceRefreshEvent(event)) scheduleReload()\n })\n })\n return () => {\n disposed = true\n epochRef.current += 1\n if (reloadTimer) clearTimeout(reloadTimer)\n unsubscribe?.()\n }\n }, [load])\n\n const refresh = useCallback(() => {\n setState((previous) => ({ ...previous, status: 'loading' }))\n void load()\n }, [load])\n\n return { ...state, refresh }\n}\n"]}
1
+ {"version":3,"file":"references-store.js","sourceRoot":"","sources":["../../../src/lib/references/references-store.ts"],"names":[],"mappings":"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","sourcesContent":["import { useMemo } from 'react'\nimport { documentKey } from '@elytracms/core/cms-core'\nimport { useOperationQuery } from '@elytracms/studio/lib/query'\nimport type { CmsDocument, DocumentRef } from '@elytracms/core/cms-core'\nimport type {\n ReferenceEntryRecord,\n ReferenceSource,\n ReferenceTarget,\n} from '@elytracms/core/persistence'\nimport { getStudioOperations } from '../operations'\nimport type { OperationEvent, OperationsClient, StudioOperations } from '../operations'\nimport { getStudioRegistry } from '../registry'\nimport { buildReferenceEntryViews } from './reference-model'\nimport type { ReferenceEntryView, ReferenceLabelContext } from './reference-model'\n\n/**\n * Data layer for the EC-172 references panels. All reads go through\n * `@elytracms/core/operations` queries (`references.outbound` / `references.inbound`\n * plus the labeling queries) — UI code never touches persistence repositories\n * or index tables. The hook keeps panels live: any succeeded command in a\n * content-bearing domain re-queries after the EC-155 maintained-on-write\n * attachment has flushed.\n */\n\nexport interface ReferencePanelQuery {\n projectId: string\n /** Enables the outbound (\"references\") section. */\n source?: ReferenceSource | undefined\n /** Enables the inbound (\"used by\") section. */\n target?: ReferenceTarget | undefined\n}\n\nexport interface ReferencePanelData {\n outbound: ReferenceEntryView[]\n inbound: ReferenceEntryView[]\n /**\n * Per-direction read error (EC-205): a direction whose query returns a\n * structured non-ok degrades to an empty list plus this message, surfaced as a\n * visible per-section error — instead of collapsing the whole panel into its\n * hard-error state. `null` when the direction loaded (or was not requested).\n */\n outboundError: string | null\n inboundError: string | null\n}\n\n// --- Index bootstrap -----------------------------------------------------------\n\n/**\n * The EC-155 index is maintained on write, but seeded/imported projects start\n * with an empty index. {@link ensureReferenceIndex} seeds it once per project per\n * operations client; failures (e.g. a viewer's command denial) degrade to\n * whatever the maintained-on-write index already holds.\n */\nconst backfillsByClient = new WeakMap<StudioOperations, Map<string, Promise<void>>>()\n\n/**\n * Seed the index once if it has never been backfilled, then never again — the\n * EC-269 mutation-free read path. The index is maintained on write, so the\n * (clear + rebuild) `backfill` is only needed to seed a never-built index. A\n * persisted marker (`references.buildMarker`, a read) records completion; when\n * present this returns after a single query with ZERO mutations — so opening the\n * References panel no longer rewrites the whole index every session (the normal\n * Convex case: the EC-233 import already backfilled). Only a genuinely empty/\n * never-built project still pays the one-time backfill (and a viewer who cannot\n * run it degrades to the maintained-on-write index, as before).\n */\nasync function seedReferenceIndex(studio: StudioOperations, projectId: string): Promise<void> {\n try {\n const marker = await studio.operations.references.buildMarker({ projectId })\n if (marker.ok && marker.value !== null) return\n } catch {\n // A marker-read failure falls through to the backfill (best-effort seed).\n }\n await studio.operations.references.backfill({ projectId }).then(\n () => undefined,\n () => undefined,\n )\n}\n\nexport function ensureReferenceIndex(studio: StudioOperations, projectId: string): Promise<void> {\n let byProject = backfillsByClient.get(studio)\n if (!byProject) {\n byProject = new Map()\n backfillsByClient.set(studio, byProject)\n }\n let pending = byProject.get(projectId)\n if (!pending) {\n pending = seedReferenceIndex(studio, projectId)\n byProject.set(projectId, pending)\n }\n return pending\n}\n\n// --- Label context loading -------------------------------------------------------\n\nasync function loadLabelContext(\n operations: OperationsClient,\n projectId: string,\n): Promise<ReferenceLabelContext> {\n const [schemaResult, documentsResult, assetsResult] = await Promise.all([\n operations.schema.get({ projectId }),\n operations.documents.list({ projectId }),\n operations.assets.list({ projectId }),\n ])\n\n const collections = schemaResult.ok ? schemaResult.value.collections : []\n const documents = new Map<string, CmsDocument>()\n if (documentsResult.ok) {\n for (const record of documentsResult.value) {\n documents.set(\n documentKey({ collection: record.document.collection, id: record.document.id }),\n record.document,\n )\n }\n }\n // EC-287: the project graph is retired; reference source labels resolve from\n // CMS documents only. `page`/`layout` graph sources no longer exist.\n const assets = new Map<string, { filename: string; title?: string | undefined }>()\n if (assetsResult.ok) {\n for (const asset of assetsResult.value) {\n assets.set(asset.id, { filename: asset.filename, title: asset.title })\n }\n }\n const registry = getStudioRegistry().registry\n return {\n collections,\n documents,\n assets,\n componentTitle: (componentId) => registry.getManifest(componentId)?.title,\n }\n}\n\n// --- Panel data loading -----------------------------------------------------------\n\n/** Load both directions + labels for one panel (operations queries only). */\nexport async function loadReferencePanelData(\n query: ReferencePanelQuery,\n): Promise<ReferencePanelData> {\n const studio = await getStudioOperations()\n await ensureReferenceIndex(studio, query.projectId)\n // Observe a settled index: maintained-on-write updates run async after events.\n await studio.references.flush()\n const operations = studio.operations\n\n // EC-205: load each direction INDEPENDENTLY. A structured non-ok from one\n // direction is a degradable operation result (it carries an OperationError —\n // the documented contract), not an exception: degrade that direction to empty\n // and surface its error per-section (references-panel) while the other still\n // renders, instead of throwing the whole panel into its hard-error state. A\n // genuinely broken backend still throws from getStudioOperations/flush above.\n const [outbound, inbound] = await Promise.all([\n query.source\n ? operations.references.outbound({ projectId: query.projectId, source: query.source })\n : null,\n query.target\n ? operations.references.inbound({ projectId: query.projectId, target: query.target })\n : null,\n ])\n\n type DirectionResult = Awaited<ReturnType<typeof operations.references.outbound>>\n const settle = (\n result: DirectionResult | null,\n ): { records: ReferenceEntryRecord[]; error: string | null } =>\n result === null\n ? { records: [], error: null }\n : result.ok\n ? { records: result.value, error: null }\n : { records: [], error: result.error.message }\n const out = settle(outbound)\n const inb = settle(inbound)\n\n // EC-302: nothing referenced in either direction → no sources/targets to label,\n // so skip the label-context load entirely (schema + ALL documents + ALL assets).\n // Common for leaf content; mirrors the asset short-circuit in\n // `loadInboundAssetReferenceViews`.\n if (out.records.length === 0 && inb.records.length === 0) {\n return { outbound: [], inbound: [], outboundError: out.error, inboundError: inb.error }\n }\n\n const context = await loadLabelContext(operations, query.projectId)\n return {\n outbound: buildReferenceEntryViews(out.records, context),\n inbound: buildReferenceEntryViews(inb.records, context),\n outboundError: out.error,\n inboundError: inb.error,\n }\n}\n\n// --- Impact loading (EC-173 safe unpublish/delete) -------------------------------\n\n/**\n * Load the labelled inbound entry views for a set of target documents in ONE\n * pass (one index flush, one label-context load) — the pre-flight data of the\n * EC-173 confirmation dialogs, including the bulk flows. Returns entries in\n * `targets` order; pair with `buildReferenceImpact` for break/block semantics.\n */\nexport async function loadInboundReferenceViews(\n projectId: string,\n targets: readonly DocumentRef[],\n): Promise<{ ref: DocumentRef; inbound: ReferenceEntryView[] }[]> {\n const studio = await getStudioOperations()\n await ensureReferenceIndex(studio, projectId)\n // Observe a settled index: maintained-on-write updates run async after events.\n await studio.references.flush()\n const operations = studio.operations\n\n // EC-308: one batched query for ALL targets (the operation loops the by_target\n // index server-side) instead of a per-target `inbound` fan-out. Returns one\n // record list per target, in `targets` order.\n const recordsPerTarget = await operations.references\n .inboundMany({\n projectId,\n targets: targets.map((ref) => ({ type: 'document', id: documentKey(ref) }) as const),\n })\n .then((result) => {\n if (!result.ok) throw new Error(result.error.message)\n return result.value\n })\n // EC-302: no inbound refs for ANY target → no sources to label, so skip the\n // label-context load (schema + ALL documents + ALL assets). The common case when\n // deleting/unpublishing unreferenced documents (incl. bulk); mirrors the EC-301\n // short-circuit already in `loadInboundAssetReferenceViews`.\n if (recordsPerTarget.every((records) => records.length === 0)) {\n return targets.map((ref) => ({ ref, inbound: [] }))\n }\n const context = await loadLabelContext(operations, projectId)\n return targets.map((ref, index) => ({\n ref,\n inbound: buildReferenceEntryViews(recordsPerTarget[index] ?? [], context),\n }))\n}\n\n/**\n * Inbound reference views for ONE asset (EC-202): the \"what uses this asset\"\n * pre-flight of the asset-delete dialog, index-accurate (the EC-155 reference\n * index, not the retired `find-asset-usages` string scan). Mirrors\n * {@link loadInboundReferenceViews} for a single `{ type: 'asset' }` target —\n * the source labels resolve the referencing documents/graph-nodes identically.\n */\nexport async function loadInboundAssetReferenceViews(\n projectId: string,\n assetId: string,\n): Promise<ReferenceEntryView[]> {\n const studio = await getStudioOperations()\n await ensureReferenceIndex(studio, projectId)\n // Observe a settled index: maintained-on-write updates run async after events.\n await studio.references.flush()\n const operations = studio.operations\n const result = await operations.references.inbound({\n projectId,\n target: { type: 'asset', id: assetId },\n })\n if (!result.ok) throw new Error(result.error.message)\n // No inbound references → no sources to label, so skip the label-context load\n // entirely (schema + ALL documents + ALL assets). This is the common case for a\n // deletable asset, making the delete dialog's pre-flight a single indexed query\n // instead of three full-table lists (EC-301 perf).\n if (result.value.length === 0) return []\n const context = await loadLabelContext(operations, projectId)\n return buildReferenceEntryViews(result.value, context)\n}\n\n// --- React hook ----------------------------------------------------------------\n\n/**\n * Domains whose successful commands can change the reference index or labels and\n * should refresh an open panel. `references` is deliberately ABSENT (EC-269):\n * reacting to reference-index command events (the `backfill`) would make the\n * panel re-trigger on its OWN index maintenance — a self-cascade that, compounded\n * with the per-keystroke `documents` events, produced hundreds of redundant\n * reloads per document. The index is maintained on write; the panel reacts to the\n * CONTENT domains that feed it, never to its own index writes.\n */\nconst REFERENCE_REFRESH_TRIGGERS = [\n 'documents',\n 'graph',\n 'publishing',\n 'assets',\n 'schema',\n] as const\n\nconst REFRESH_DOMAINS: ReadonlySet<string> = new Set(REFERENCE_REFRESH_TRIGGERS)\n\n/**\n * Whether one operation event should refresh an open reference panel. Exported\n * for tests — pins the EC-269 contract that reference-index commands (and any\n * non-succeeded / non-command event) never trigger a reload.\n */\nexport function isReferenceRefreshEvent(event: OperationEvent): boolean {\n return (\n event.kind === 'command' && event.status === 'succeeded' && REFRESH_DOMAINS.has(event.domain)\n )\n}\n\nexport type ReferencePanelStatus = 'loading' | 'ready' | 'error'\n\nexport interface ReferencePanelState {\n status: ReferencePanelStatus\n data: ReferencePanelData | null\n errorMessage: string | null\n refresh: () => void\n}\n\n/**\n * Live panel state for one entity (EC-308/AD-17): loads + caches through the one\n * `useOperationQuery` seam, and re-queries whenever a content-bearing command\n * succeeds anywhere in the studio (the index is maintained on write). Per-direction\n * read errors degrade INSIDE `loadReferencePanelData` (carried on the data); a hard\n * failure (broken backend) surfaces as the query's error → `status: 'error'`.\n */\nexport function useReferencePanel(query: ReferencePanelQuery): ReferencePanelState {\n const { projectId } = query\n const sourceType = query.source?.type\n const sourceId = query.source?.id\n const targetType = query.target?.type\n const targetId = query.target?.id\n\n const stableQuery = useMemo<ReferencePanelQuery>(\n () => ({\n projectId,\n ...(sourceType && sourceId ? { source: { type: sourceType, id: sourceId } } : {}),\n ...(targetType && targetId ? { target: { type: targetType, id: targetId } } : {}),\n }),\n [projectId, sourceType, sourceId, targetType, targetId],\n )\n\n const result = useOperationQuery<ReferencePanelData>({\n queryKey: [\n 'referencePanel',\n projectId,\n sourceType ?? '',\n sourceId ?? '',\n targetType ?? '',\n targetId ?? '',\n ],\n loader: () => loadReferencePanelData(stableQuery),\n invalidateOn: REFERENCE_REFRESH_TRIGGERS,\n })\n\n const status: ReferencePanelStatus = result.isPending\n ? 'loading'\n : result.isError\n ? 'error'\n : 'ready'\n return {\n status,\n data: result.data ?? null,\n errorMessage: result.error instanceof Error ? result.error.message : null,\n refresh: () => void result.refetch(),\n }\n}\n"]}
@@ -0,0 +1,20 @@
1
+ import { type ReactNode } from 'react';
2
+ export interface CollapsibleSectionProps {
3
+ /** Section heading (already styled by the caller — kept presentation-neutral here). */
4
+ title: ReactNode;
5
+ /** When true the header toggles the body open/closed; otherwise the body is always shown. */
6
+ collapsible?: boolean;
7
+ /** Initial open state for a collapsible section (default open). */
8
+ defaultOpen?: boolean;
9
+ children: ReactNode;
10
+ className?: string;
11
+ 'data-testid'?: string;
12
+ }
13
+ /**
14
+ * A `Panel`-backed section whose header can collapse its body (EC-306). No
15
+ * existing collapsible/accordion primitive existed; this is the reusable one
16
+ * (the array-legibility and CMS-overhaul work, EC-305/EC-246, can reuse it).
17
+ * Accessible: the header is a real `<button>` with `aria-expanded`. When
18
+ * `collapsible` is false it renders as a plain titled panel (today's behaviour).
19
+ */
20
+ export declare function CollapsibleSection({ title, collapsible, defaultOpen, children, className, 'data-testid': testId, }: CollapsibleSectionProps): import("react").JSX.Element;
@@ -0,0 +1,18 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { ChevronDown } from 'lucide-react';
3
+ import { useState } from 'react';
4
+ import { cn } from '../lib/utils';
5
+ import { Panel, PanelHeader } from './panel';
6
+ /**
7
+ * A `Panel`-backed section whose header can collapse its body (EC-306). No
8
+ * existing collapsible/accordion primitive existed; this is the reusable one
9
+ * (the array-legibility and CMS-overhaul work, EC-305/EC-246, can reuse it).
10
+ * Accessible: the header is a real `<button>` with `aria-expanded`. When
11
+ * `collapsible` is false it renders as a plain titled panel (today's behaviour).
12
+ */
13
+ export function CollapsibleSection({ title, collapsible = false, defaultOpen = true, children, className, 'data-testid': testId, }) {
14
+ const [open, setOpen] = useState(defaultOpen);
15
+ const expanded = collapsible ? open : true;
16
+ return (_jsxs(Panel, { className: className, "data-testid": testId, children: [_jsx(PanelHeader, { children: collapsible ? (_jsxs("button", { type: "button", "aria-expanded": expanded, onClick: () => setOpen((o) => !o), className: "-mx-3 flex h-full flex-1 items-center gap-2 px-3 text-left hover:bg-muted/40", children: [_jsx(ChevronDown, { className: cn('size-4 shrink-0 text-muted-foreground transition-transform', !expanded && '-rotate-90'), "aria-hidden": true }), title] })) : (title) }), expanded ? children : null] }));
17
+ }
18
+ //# sourceMappingURL=collapsible-section.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collapsible-section.js","sourceRoot":"","sources":["../../src/ui/collapsible-section.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAC1C,OAAO,EAAE,QAAQ,EAAkB,MAAM,OAAO,CAAA;AAChD,OAAO,EAAE,EAAE,EAAE,MAAM,cAAc,CAAA;AACjC,OAAO,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AAc5C;;;;;;GAMG;AACH,MAAM,UAAU,kBAAkB,CAAC,EACjC,KAAK,EACL,WAAW,GAAG,KAAK,EACnB,WAAW,GAAG,IAAI,EAClB,QAAQ,EACR,SAAS,EACT,aAAa,EAAE,MAAM,GACG;IACxB,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,QAAQ,CAAC,WAAW,CAAC,CAAA;IAC7C,MAAM,QAAQ,GAAG,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAA;IAC1C,OAAO,CACL,MAAC,KAAK,IAAC,SAAS,EAAE,SAAS,iBAAe,MAAM,aAC9C,KAAC,WAAW,cACT,WAAW,CAAC,CAAC,CAAC,CACb,kBACE,IAAI,EAAC,QAAQ,mBACE,QAAQ,EACvB,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EACjC,SAAS,EAAC,8EAA8E,aAExF,KAAC,WAAW,IACV,SAAS,EAAE,EAAE,CACX,4DAA4D,EAC5D,CAAC,QAAQ,IAAI,YAAY,CAC1B,wBAED,EACD,KAAK,IACC,CACV,CAAC,CAAC,CAAC,CACF,KAAK,CACN,GACW,EACb,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,IACrB,CACT,CAAA;AACH,CAAC","sourcesContent":["import { ChevronDown } from 'lucide-react'\nimport { useState, type ReactNode } from 'react'\nimport { cn } from '../lib/utils'\nimport { Panel, PanelHeader } from './panel'\n\nexport interface CollapsibleSectionProps {\n /** Section heading (already styled by the caller — kept presentation-neutral here). */\n title: ReactNode\n /** When true the header toggles the body open/closed; otherwise the body is always shown. */\n collapsible?: boolean\n /** Initial open state for a collapsible section (default open). */\n defaultOpen?: boolean\n children: ReactNode\n className?: string\n 'data-testid'?: string\n}\n\n/**\n * A `Panel`-backed section whose header can collapse its body (EC-306). No\n * existing collapsible/accordion primitive existed; this is the reusable one\n * (the array-legibility and CMS-overhaul work, EC-305/EC-246, can reuse it).\n * Accessible: the header is a real `<button>` with `aria-expanded`. When\n * `collapsible` is false it renders as a plain titled panel (today's behaviour).\n */\nexport function CollapsibleSection({\n title,\n collapsible = false,\n defaultOpen = true,\n children,\n className,\n 'data-testid': testId,\n}: CollapsibleSectionProps) {\n const [open, setOpen] = useState(defaultOpen)\n const expanded = collapsible ? open : true\n return (\n <Panel className={className} data-testid={testId}>\n <PanelHeader>\n {collapsible ? (\n <button\n type=\"button\"\n aria-expanded={expanded}\n onClick={() => setOpen((o) => !o)}\n className=\"-mx-3 flex h-full flex-1 items-center gap-2 px-3 text-left hover:bg-muted/40\"\n >\n <ChevronDown\n className={cn(\n 'size-4 shrink-0 text-muted-foreground transition-transform',\n !expanded && '-rotate-90',\n )}\n aria-hidden\n />\n {title}\n </button>\n ) : (\n title\n )}\n </PanelHeader>\n {expanded ? children : null}\n </Panel>\n )\n}\n"]}
@@ -6,6 +6,7 @@ export * from './input';
6
6
  export * from './badge';
7
7
  export * from './card';
8
8
  export * from './panel';
9
+ export * from './collapsible-section';
9
10
  export * from './separator';
10
11
  export * from './toolbar';
11
12
  export * from './inspector';
package/dist/ui/index.js CHANGED
@@ -6,6 +6,7 @@ export * from './input';
6
6
  export * from './badge';
7
7
  export * from './card';
8
8
  export * from './panel';
9
+ export * from './collapsible-section';
9
10
  export * from './separator';
10
11
  export * from './toolbar';
11
12
  export * from './inspector';
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/ui/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAA;AAC5B,cAAc,UAAU,CAAA;AACxB,cAAc,UAAU,CAAA;AACxB,cAAc,UAAU,CAAA;AACxB,cAAc,SAAS,CAAA;AACvB,cAAc,SAAS,CAAA;AACvB,cAAc,QAAQ,CAAA;AACtB,cAAc,SAAS,CAAA;AACvB,cAAc,aAAa,CAAA;AAC3B,cAAc,WAAW,CAAA;AACzB,cAAc,aAAa,CAAA;AAC3B,cAAc,UAAU,CAAA;AACxB,cAAc,iBAAiB,CAAA;AAC/B,cAAc,SAAS,CAAA;AACvB,cAAc,SAAS,CAAA","sourcesContent":["export * from './typography'\nexport * from './layout'\nexport * from './avatar'\nexport * from './button'\nexport * from './input'\nexport * from './badge'\nexport * from './card'\nexport * from './panel'\nexport * from './separator'\nexport * from './toolbar'\nexport * from './inspector'\nexport * from './dialog'\nexport * from './dropdown-menu'\nexport * from './field'\nexport * from './toast'\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/ui/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAA;AAC5B,cAAc,UAAU,CAAA;AACxB,cAAc,UAAU,CAAA;AACxB,cAAc,UAAU,CAAA;AACxB,cAAc,SAAS,CAAA;AACvB,cAAc,SAAS,CAAA;AACvB,cAAc,QAAQ,CAAA;AACtB,cAAc,SAAS,CAAA;AACvB,cAAc,uBAAuB,CAAA;AACrC,cAAc,aAAa,CAAA;AAC3B,cAAc,WAAW,CAAA;AACzB,cAAc,aAAa,CAAA;AAC3B,cAAc,UAAU,CAAA;AACxB,cAAc,iBAAiB,CAAA;AAC/B,cAAc,SAAS,CAAA;AACvB,cAAc,SAAS,CAAA","sourcesContent":["export * from './typography'\nexport * from './layout'\nexport * from './avatar'\nexport * from './button'\nexport * from './input'\nexport * from './badge'\nexport * from './card'\nexport * from './panel'\nexport * from './collapsible-section'\nexport * from './separator'\nexport * from './toolbar'\nexport * from './inspector'\nexport * from './dialog'\nexport * from './dropdown-menu'\nexport * from './field'\nexport * from './toast'\n"]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@elytracms/studio",
3
3
  "author": "finaldream <hi@finaldream.de>",
4
- "version": "0.0.24",
4
+ "version": "0.0.26",
5
5
  "private": false,
6
6
  "description": "The Elytra studio engine (UI + lib + routes + data-only Convex schema) as an importable package. Extraction staged in EXTRACTION.md (EC-223); Stages 1-2 = UI primitives + router-free lib leaves.",
7
7
  "license": "MIT",
@@ -110,6 +110,7 @@
110
110
  "@radix-ui/react-dialog": "^1.1.16",
111
111
  "@radix-ui/react-dropdown-menu": "^2.1.16",
112
112
  "@radix-ui/react-slot": "^1.2.5",
113
+ "@tanstack/react-query": "^5.101.2",
113
114
  "class-variance-authority": "^0.7.1",
114
115
  "clsx": "^2.1.1",
115
116
  "convex": "^1.40.0",
@@ -117,7 +118,7 @@
117
118
  "lucide-react": "^1.17.0",
118
119
  "tailwind-merge": "^3.6.0",
119
120
  "zod": "^4.0.0",
120
- "@elytracms/core": "^0.0.24"
121
+ "@elytracms/core": "^0.0.26"
121
122
  },
122
123
  "peerDependencies": {
123
124
  "@tanstack/react-router": "^1.170.0",