@elytracms/studio 0.0.41 → 0.0.43

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 (151) hide show
  1. package/dist/components/assets/asset-detail.js +1 -1
  2. package/dist/components/assets/asset-detail.js.map +1 -1
  3. package/dist/components/assets/assets-module.js +19 -6
  4. package/dist/components/assets/assets-module.js.map +1 -1
  5. package/dist/components/assets/assets-store.d.ts +2 -1
  6. package/dist/components/assets/assets-store.js +4 -8
  7. package/dist/components/assets/assets-store.js.map +1 -1
  8. package/dist/components/assets/assets-view-preference.d.ts +0 -8
  9. package/dist/components/assets/assets-view-preference.js +12 -13
  10. package/dist/components/assets/assets-view-preference.js.map +1 -1
  11. package/dist/components/auth-context.d.ts +2 -1
  12. package/dist/components/auth-context.js +6 -8
  13. package/dist/components/auth-context.js.map +1 -1
  14. package/dist/components/builder/builder-store.d.ts +2 -2
  15. package/dist/components/builder/builder-store.js +4 -8
  16. package/dist/components/builder/builder-store.js.map +1 -1
  17. package/dist/components/builder/canvas.js +16 -2
  18. package/dist/components/builder/canvas.js.map +1 -1
  19. package/dist/components/cms/bulk-actions.js +2 -1
  20. package/dist/components/cms/bulk-actions.js.map +1 -1
  21. package/dist/components/cms/cell-edit.js +2 -1
  22. package/dist/components/cms/cell-edit.js.map +1 -1
  23. package/dist/components/cms/cms-composition-validators.d.ts +25 -0
  24. package/dist/components/cms/cms-composition-validators.js +75 -0
  25. package/dist/components/cms/cms-composition-validators.js.map +1 -0
  26. package/dist/components/cms/cms-labels.d.ts +15 -0
  27. package/dist/components/cms/cms-labels.js +27 -0
  28. package/dist/components/cms/cms-labels.js.map +1 -0
  29. package/dist/components/cms/cms-store.d.ts +23 -10
  30. package/dist/components/cms/cms-store.js +78 -154
  31. package/dist/components/cms/cms-store.js.map +1 -1
  32. package/dist/components/cms/collection-table.js +1 -1
  33. package/dist/components/cms/collection-table.js.map +1 -1
  34. package/dist/components/cms/detail-editor.js +67 -67
  35. package/dist/components/cms/detail-editor.js.map +1 -1
  36. package/dist/components/cms/details-pane.d.ts +11 -21
  37. package/dist/components/cms/details-pane.js +14 -31
  38. package/dist/components/cms/details-pane.js.map +1 -1
  39. package/dist/components/cms/discard-draft.d.ts +18 -0
  40. package/dist/components/cms/discard-draft.js +18 -0
  41. package/dist/components/cms/discard-draft.js.map +1 -1
  42. package/dist/components/cms/document-editor-context.d.ts +2 -1
  43. package/dist/components/cms/document-editor-context.js +4 -9
  44. package/dist/components/cms/document-editor-context.js.map +1 -1
  45. package/dist/components/cms/document-ref-utils.d.ts +33 -0
  46. package/dist/components/cms/document-ref-utils.js +47 -0
  47. package/dist/components/cms/document-ref-utils.js.map +1 -0
  48. package/dist/components/cms/document-url-path.d.ts +39 -0
  49. package/dist/components/cms/document-url-path.js +60 -0
  50. package/dist/components/cms/document-url-path.js.map +1 -0
  51. package/dist/components/cms/field-control.js +19 -13
  52. package/dist/components/cms/field-control.js.map +1 -1
  53. package/dist/components/cms/issues-pane.js +1 -1
  54. package/dist/components/cms/issues-pane.js.map +1 -1
  55. package/dist/components/cms/rich-text/block-commands.d.ts +36 -0
  56. package/dist/components/cms/rich-text/block-commands.js +47 -0
  57. package/dist/components/cms/rich-text/block-commands.js.map +1 -0
  58. package/dist/components/cms/rich-text/slash-command.js +25 -8
  59. package/dist/components/cms/rich-text/slash-command.js.map +1 -1
  60. package/dist/components/cms/rich-text/toolbar.js +3 -2
  61. package/dist/components/cms/rich-text/toolbar.js.map +1 -1
  62. package/dist/components/cms/version-compare.js +1 -1
  63. package/dist/components/cms/version-compare.js.map +1 -1
  64. package/dist/components/cms/versions-model.d.ts +31 -0
  65. package/dist/components/cms/versions-model.js +30 -0
  66. package/dist/components/cms/versions-model.js.map +1 -1
  67. package/dist/components/cms/view-bar.js +9 -6
  68. package/dist/components/cms/view-bar.js.map +1 -1
  69. package/dist/components/collab/collab-context.d.ts +2 -1
  70. package/dist/components/collab/collab-context.js +4 -8
  71. package/dist/components/collab/collab-context.js.map +1 -1
  72. package/dist/components/collection-view/collection-view-context.d.ts +9 -1
  73. package/dist/components/collection-view/collection-view-context.js +4 -9
  74. package/dist/components/collection-view/collection-view-context.js.map +1 -1
  75. package/dist/components/components/component-detail.js +2 -5
  76. package/dist/components/components/component-detail.js.map +1 -1
  77. package/dist/components/components/component-preview.js +8 -4
  78. package/dist/components/components/component-preview.js.map +1 -1
  79. package/dist/components/components/registry-browser.js +24 -5
  80. package/dist/components/components/registry-browser.js.map +1 -1
  81. package/dist/components/components/transfer-dialog.d.ts +10 -0
  82. package/dist/components/components/transfer-dialog.js +24 -8
  83. package/dist/components/components/transfer-dialog.js.map +1 -1
  84. package/dist/components/delivery-health-banner.d.ts +12 -6
  85. package/dist/components/delivery-health-banner.js +27 -29
  86. package/dist/components/delivery-health-banner.js.map +1 -1
  87. package/dist/components/dev-login-badge.d.ts +0 -2
  88. package/dist/components/dev-login-badge.js +1 -3
  89. package/dist/components/dev-login-badge.js.map +1 -1
  90. package/dist/components/inspector/inspector-sidebar.js +20 -3
  91. package/dist/components/inspector/inspector-sidebar.js.map +1 -1
  92. package/dist/components/playground-badge.d.ts +0 -2
  93. package/dist/components/playground-badge.js +0 -2
  94. package/dist/components/playground-badge.js.map +1 -1
  95. package/dist/components/project-list.js +2 -6
  96. package/dist/components/project-list.js.map +1 -1
  97. package/dist/components/project-session.d.ts +6 -2
  98. package/dist/components/project-session.js +32 -83
  99. package/dist/components/project-session.js.map +1 -1
  100. package/dist/components/project-switcher.js +2 -6
  101. package/dist/components/project-switcher.js.map +1 -1
  102. package/dist/components/redirects/redirects-store.d.ts +2 -1
  103. package/dist/components/redirects/redirects-store.js +4 -8
  104. package/dist/components/redirects/redirects-store.js.map +1 -1
  105. package/dist/components/studio-context.d.ts +2 -1
  106. package/dist/components/studio-context.js +4 -8
  107. package/dist/components/studio-context.js.map +1 -1
  108. package/dist/components/studio-shell.js +1 -1
  109. package/dist/components/studio-shell.js.map +1 -1
  110. package/dist/components/theme-context.d.ts +2 -1
  111. package/dist/components/theme-context.js +4 -8
  112. package/dist/components/theme-context.js.map +1 -1
  113. package/dist/components/use-hydration-phase.d.ts +65 -0
  114. package/dist/components/use-hydration-phase.js +64 -0
  115. package/dist/components/use-hydration-phase.js.map +1 -0
  116. package/dist/components/users/users-store.d.ts +2 -1
  117. package/dist/components/users/users-store.js +4 -8
  118. package/dist/components/users/users-store.js.map +1 -1
  119. package/dist/components/workspace-projects.d.ts +14 -0
  120. package/dist/components/workspace-projects.js +21 -0
  121. package/dist/components/workspace-projects.js.map +1 -1
  122. package/dist/lib/data/data-store.d.ts +2 -1
  123. package/dist/lib/data/data-store.js +4 -8
  124. package/dist/lib/data/data-store.js.map +1 -1
  125. package/dist/lib/delivery/instant-publish.d.ts +9 -13
  126. package/dist/lib/delivery/instant-publish.js +0 -7
  127. package/dist/lib/delivery/instant-publish.js.map +1 -1
  128. package/dist/lib/diff-view-preference.js +4 -4
  129. package/dist/lib/diff-view-preference.js.map +1 -1
  130. package/dist/lib/i18n/messages.en.js +7 -5
  131. package/dist/lib/i18n/messages.en.js.map +1 -1
  132. package/dist/lib/local-preference.d.ts +37 -0
  133. package/dist/lib/local-preference.js +36 -0
  134. package/dist/lib/local-preference.js.map +1 -1
  135. package/dist/lib/persistence/convex-adapter.d.ts +3 -1
  136. package/dist/lib/persistence/convex-adapter.js +20 -0
  137. package/dist/lib/persistence/convex-adapter.js.map +1 -1
  138. package/dist/lib/rail.js +7 -6
  139. package/dist/lib/rail.js.map +1 -1
  140. package/dist/lib/required-context.d.ts +24 -0
  141. package/dist/lib/required-context.js +28 -0
  142. package/dist/lib/required-context.js.map +1 -0
  143. package/dist/lib/theme.js +11 -4
  144. package/dist/lib/theme.js.map +1 -1
  145. package/dist/ui/index.d.ts +1 -0
  146. package/dist/ui/index.js +1 -0
  147. package/dist/ui/index.js.map +1 -1
  148. package/dist/ui/segmented-control.d.ts +46 -0
  149. package/dist/ui/segmented-control.js +13 -0
  150. package/dist/ui/segmented-control.js.map +1 -0
  151. package/package.json +2 -2
@@ -4,12 +4,11 @@ import { FilePlus2 } from 'lucide-react';
4
4
  import { STUDIO_PROJECT_ROUTE, studioPath, useNavigate } from '@elytracms/studio/router';
5
5
  import { Button, Code, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Row, Stack, Text, } from '@elytracms/studio/ui';
6
6
  import { localizedLabel, translate } from '@elytracms/studio/lib/i18n';
7
- import { isPlaygroundMode } from '@elytracms/studio/lib/persistence';
8
- import { resolveDeploymentCollections } from '@elytracms/studio/lib/workspace';
9
7
  import { getInstantiatedKit } from '@elytracms/studio/lib/starters/kit-session';
10
8
  import { CmsSessionGate } from '../project-session';
11
9
  import { CmsProvider, useCms } from '../cms/cms-store';
12
10
  import { stageDocument } from '../cms/document-staging';
11
+ import { resolveCmsInitialCollections } from '../app-views/default-modules';
13
12
  import { buildBlockDocument, collectionsWithBlocks } from './transfer';
14
13
  import { embedNodeFrom } from './component-preview';
15
14
  /**
@@ -26,16 +25,33 @@ import { embedNodeFrom } from './component-preview';
26
25
  export function TransferToDocumentDialog({ projectId, manifest, node, open, onOpenChange, }) {
27
26
  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) => {
28
27
  const kit = getInstantiatedKit(projectId);
29
- const collections = cms?.collections ??
30
- kit?.collections ??
31
- (isPlaygroundMode() ? resolveDeploymentCollections() : []);
32
- return (_jsx(CmsProvider, { projectId: projectId, initialCollections: collections, initialDocuments: cms?.documents ?? kit?.documents, initialPublishedKeys: cms?.publishedKeys, children: _jsx(TransferPicker, { projectId: projectId, node: node, collections: collections, onClose: () => onOpenChange(false) }) }));
28
+ // Bug fix: this used to reimplement `resolveCmsInitialCollections`'s
29
+ // fallback chain inline with a final `[]` instead of `undefined` — `[]`
30
+ // is "present" to `CmsProvider`'s `initialCollections ??
31
+ // sampleFilterableCollections`, so it suppressed the fixture fallback a
32
+ // fresh Convex-mode project (`cms === null`, no kit) needs. Delegating to
33
+ // the shared, tested resolver (also used by the CMS/Assets tabs) keeps
34
+ // this dialog's targets in sync with what those tabs show.
35
+ const collections = resolveCmsInitialCollections(cms, kit);
36
+ return (_jsx(CmsProvider, { projectId: projectId, initialCollections: collections, initialDocuments: cms?.documents ?? kit?.documents, initialPublishedKeys: cms?.publishedKeys, children: _jsx(TransferPicker, { projectId: projectId, node: node, onClose: () => onOpenChange(false) }) }));
33
37
  } })) : null] }) }));
34
38
  }
35
- function TransferPicker({ projectId, node, collections, onClose, }) {
39
+ /**
40
+ * Exported for the fresh-Convex-project regression test (bug fix,
41
+ * transfer-dialog.test.tsx) — `TransferToDocumentDialog` is otherwise its only
42
+ * caller.
43
+ */
44
+ export function TransferPicker({ projectId, node, onClose, }) {
36
45
  const cms = useCms();
37
46
  const navigate = useNavigate();
38
- const targets = useMemo(() => collectionsWithBlocks(collections), [collections]);
47
+ // Read the LIVE store schema (`cms.collections`) rather than re-deriving it
48
+ // from the same `resolveCmsInitialCollections` result passed to `CmsProvider`
49
+ // above — `cms.collections` is what the provider actually resolved to
50
+ // (its OWN `initialCollections ?? sampleFilterableCollections` fallback,
51
+ // EC-320), so the targets list can never drift from — or reintroduce the
52
+ // `[]`-suppresses-the-fixture-fallback bug against — what the CMS/Assets
53
+ // tabs show for this same session.
54
+ const targets = useMemo(() => collectionsWithBlocks(cms.collections), [cms.collections]);
39
55
  function transfer(target) {
40
56
  const existingIds = new Set(cms.documentsOf(target.collection.id).map((d) => d.id));
41
57
  const doc = buildBlockDocument({
@@ -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 } from 'react'\nimport { FilePlus2 } from 'lucide-react'\nimport type { ComponentManifest } from '@elytracms/core/component-registry'\nimport type { CollectionDef } 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 Row,\n Stack,\n Text,\n} from '@elytracms/studio/ui'\nimport { localizedLabel, translate } from '@elytracms/studio/lib/i18n'\nimport { isPlaygroundMode } from '@elytracms/studio/lib/persistence'\nimport { resolveDeploymentCollections } from '@elytracms/studio/lib/workspace'\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 { buildBlockDocument, collectionsWithBlocks } from './transfer'\nimport type { CollectionBlocksTarget } from './transfer'\nimport { embedNodeFrom } from './component-preview'\n\n/**\n * EC-320 — \"Use in document…\". Transfers the current component combination (the\n * live preview {@link ComponentNode}, edited props and all) into a NEW DRAFT\n * document whose blocks field holds it, then opens that document.\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 (pure {@link buildBlockDocument}), persists it via `saveDocument`, STAGES\n * it for the next CMS mount (the playground handoff, see document-staging), and\n * navigates to the document — focused on its blocks field so it 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 const collections =\n cms?.collections ??\n kit?.collections ??\n (isPlaygroundMode() ? resolveDeploymentCollections() : [])\n return (\n <CmsProvider\n projectId={projectId}\n initialCollections={collections}\n initialDocuments={cms?.documents ?? kit?.documents}\n initialPublishedKeys={cms?.publishedKeys}\n >\n <TransferPicker\n projectId={projectId}\n node={node}\n collections={collections}\n onClose={() => onOpenChange(false)}\n />\n </CmsProvider>\n )\n }}\n </CmsSessionGate>\n ) : null}\n </DialogContent>\n </Dialog>\n )\n}\n\nfunction TransferPicker({\n projectId,\n node,\n collections,\n onClose,\n}: {\n projectId: string\n node: ComponentNode\n collections: readonly CollectionDef[]\n onClose: () => void\n}) {\n const cms = useCms()\n const navigate = useNavigate()\n const targets = useMemo(() => collectionsWithBlocks(collections), [collections])\n\n function transfer(target: CollectionBlocksTarget) {\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: embedNodeFrom(node, 'node-1'),\n existingIds,\n seq: { current: 0 },\n })\n // Persist through the store (upsert + reference index) and stage for the next\n // CMS mount (playground handoff), then open the document on its canvas.\n cms.saveDocument(doc)\n stageDocument(projectId, doc)\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 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 return (\n <>\n <Stack gap=\"2\" className=\"max-h-[50vh] overflow-y-auto\">\n {targets.map((target) => (\n <button\n key={target.collection.id}\n type=\"button\"\n data-testid=\"transfer-target\"\n onClick={() => transfer(target)}\n className=\"flex items-center gap-3 rounded-md border border-border p-3 text-left transition-colors hover:border-primary/40 hover:bg-muted\"\n >\n <span className=\"grid size-8 shrink-0 place-items-center rounded-md bg-muted text-muted-foreground\">\n <FilePlus2 className=\"size-4\" />\n </span>\n <Stack gap=\"px\" className=\"min-w-0 flex-1\">\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 </button>\n ))}\n </Stack>\n <DialogFooter>\n <Button type=\"button\" variant=\"ghost\" onClick={onClose}>\n {translate('common.cancel')}\n </Button>\n </DialogFooter>\n </>\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 } from 'react'\nimport { FilePlus2 } from 'lucide-react'\nimport type { ComponentManifest } from '@elytracms/core/component-registry'\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 Row,\n Stack,\n Text,\n} from '@elytracms/studio/ui'\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 { buildBlockDocument, collectionsWithBlocks } from './transfer'\nimport type { CollectionBlocksTarget } from './transfer'\nimport { embedNodeFrom } from './component-preview'\n\n/**\n * EC-320 — \"Use in document…\". Transfers the current component combination (the\n * live preview {@link ComponentNode}, edited props and all) into a NEW DRAFT\n * document whose blocks field holds it, then opens that document.\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 (pure {@link buildBlockDocument}), persists it via `saveDocument`, STAGES\n * it for the next CMS mount (the playground handoff, see document-staging), and\n * navigates to the document — focused on its blocks field so it 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/**\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 // 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.\n const targets = useMemo(() => collectionsWithBlocks(cms.collections), [cms.collections])\n\n function transfer(target: CollectionBlocksTarget) {\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: embedNodeFrom(node, 'node-1'),\n existingIds,\n seq: { current: 0 },\n })\n // Persist through the store (upsert + reference index) and stage for the next\n // CMS mount (playground handoff), then open the document on its canvas.\n cms.saveDocument(doc)\n stageDocument(projectId, doc)\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 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 return (\n <>\n <Stack gap=\"2\" className=\"max-h-[50vh] overflow-y-auto\">\n {targets.map((target) => (\n <button\n key={target.collection.id}\n type=\"button\"\n data-testid=\"transfer-target\"\n onClick={() => transfer(target)}\n className=\"flex items-center gap-3 rounded-md border border-border p-3 text-left transition-colors hover:border-primary/40 hover:bg-muted\"\n >\n <span className=\"grid size-8 shrink-0 place-items-center rounded-md bg-muted text-muted-foreground\">\n <FilePlus2 className=\"size-4\" />\n </span>\n <Stack gap=\"px\" className=\"min-w-0 flex-1\">\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 </button>\n ))}\n </Stack>\n <DialogFooter>\n <Button type=\"button\" variant=\"ghost\" onClick={onClose}>\n {translate('common.cancel')}\n </Button>\n </DialogFooter>\n </>\n )\n}\n"]}
@@ -1,11 +1,17 @@
1
1
  /**
2
2
  * EC-268: a boot-up health check for instant publish (EC-146). When the studio is
3
- * connected to a Convex deployment, it reads `delivery:deliveryConfig` once and, if
4
- * any of the delivery env vars are unset, shows a dismissible warning. Without this
5
- * the pipeline fails SILENTLY — a publish reports success but `scheduleRevalidate`
3
+ * connected to a Convex deployment, it reads the delivery config through the
4
+ * operations seam (EC-329 — `operations.delivery.config`, cached by
5
+ * `useOperationQuery` like every other studio read, AD-17) and, if any of the
6
+ * delivery env vars are unset, shows a dismissible warning. Without this the
7
+ * pipeline fails SILENTLY — a publish reports success but `scheduleRevalidate`
6
8
  * returns before scheduling (no `CANVAS_PROJECT_SCOPE`) and the live site never
7
9
  * updates. In playground mode (no backend) instant publish does not apply and the
8
- * banner never renders. Failure to read config is swallowed — it must never block
9
- * the studio.
10
+ * banner never renders (the query is disabled, so it never has data). A failed
11
+ * read degrades silently — it must never block the studio — by treating the
12
+ * query's error branch the same as "nothing missing", matching the former raw
13
+ * client call's swallowed `.catch()`.
10
14
  */
11
- export declare function DeliveryHealthBanner(): import("react").JSX.Element | null;
15
+ export declare function DeliveryHealthBanner({ projectId }: {
16
+ projectId: string;
17
+ }): import("react").JSX.Element | null;
@@ -1,44 +1,42 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useEffect, useState } from 'react';
2
+ import { useState } from 'react';
3
3
  import { Rocket, X } from 'lucide-react';
4
4
  import { Button, Row, Spacer, Text } from '@elytracms/studio/ui';
5
5
  import { translate } from '@elytracms/studio/lib/i18n';
6
6
  import { resolveBackendConfig } from '../lib/persistence/backend-mode';
7
- import { getConvexClient, queryRef } from '../lib/persistence/convex-client';
8
- import { resolveDeploymentUrl } from '../lib/workspace';
9
7
  import { missingInstantPublishVars } from '../lib/delivery/instant-publish';
8
+ import { useOperationQuery } from '@elytracms/studio/lib/query';
10
9
  /**
11
10
  * EC-268: a boot-up health check for instant publish (EC-146). When the studio is
12
- * connected to a Convex deployment, it reads `delivery:deliveryConfig` once and, if
13
- * any of the delivery env vars are unset, shows a dismissible warning. Without this
14
- * the pipeline fails SILENTLY — a publish reports success but `scheduleRevalidate`
11
+ * connected to a Convex deployment, it reads the delivery config through the
12
+ * operations seam (EC-329 — `operations.delivery.config`, cached by
13
+ * `useOperationQuery` like every other studio read, AD-17) and, if any of the
14
+ * delivery env vars are unset, shows a dismissible warning. Without this the
15
+ * pipeline fails SILENTLY — a publish reports success but `scheduleRevalidate`
15
16
  * returns before scheduling (no `CANVAS_PROJECT_SCOPE`) and the live site never
16
17
  * updates. In playground mode (no backend) instant publish does not apply and the
17
- * banner never renders. Failure to read config is swallowed — it must never block
18
- * the studio.
18
+ * banner never renders (the query is disabled, so it never has data). A failed
19
+ * read degrades silently — it must never block the studio — by treating the
20
+ * query's error branch the same as "nothing missing", matching the former raw
21
+ * client call's swallowed `.catch()`.
19
22
  */
20
- export function DeliveryHealthBanner() {
21
- const [missing, setMissing] = useState(null);
23
+ export function DeliveryHealthBanner({ projectId }) {
22
24
  const [dismissed, setDismissed] = useState(false);
23
- useEffect(() => {
24
- const config = resolveBackendConfig();
25
- if (config.kind !== 'convex')
26
- return;
27
- let cancelled = false;
28
- getConvexClient(resolveDeploymentUrl(config.url))
29
- .query(queryRef('delivery:deliveryConfig'), {})
30
- .then((status) => {
31
- if (!cancelled)
32
- setMissing(missingInstantPublishVars(status));
33
- })
34
- .catch(() => {
35
- /* config unknown (older deployment, transport) — stay silent. */
36
- });
37
- return () => {
38
- cancelled = true;
39
- };
40
- }, []);
41
- if (dismissed || !missing || missing.length === 0)
25
+ const enabled = resolveBackendConfig().kind === 'convex';
26
+ const query = useOperationQuery({
27
+ queryKey: ['delivery', 'config', projectId],
28
+ loader: async (operations) => {
29
+ const result = await operations.delivery.config({ projectId });
30
+ if (!result.ok)
31
+ throw new Error(result.error.message);
32
+ return result.value;
33
+ },
34
+ enabled,
35
+ });
36
+ // Loading/error/playground (disabled) all read as "nothing missing" — the
37
+ // banner only ever surfaces a CONFIRMED gap, never a transient/unreachable one.
38
+ const missing = query.data ? missingInstantPublishVars(query.data) : [];
39
+ if (dismissed || missing.length === 0)
42
40
  return null;
43
41
  return (_jsxs(Row, { gap: "2", className: "shrink-0 border-b border-warning/30 bg-warning/10 px-4 py-1.5 text-warning sm:px-6", "data-testid": "delivery-health-banner", children: [_jsx(Rocket, { className: "size-3.5 shrink-0", "aria-hidden": true }), _jsxs(Text, { size: "xs", className: "text-warning", children: ["Instant publish isn\u2019t fully configured \u2014 ", missing.join(', '), " not set on this Convex deployment. Publishes succeed, but the live site won\u2019t update until you set them (see CONNECT.md) and re-push functions."] }), _jsx(Spacer, {}), _jsx(Button, { variant: "ghost", size: "icon-sm", onClick: () => setDismissed(true), "aria-label": translate('shell.deliveryHealth.dismiss'), title: translate('shell.deliveryHealth.dismiss'), children: _jsx(X, { "aria-hidden": true }) })] }));
44
42
  }
@@ -1 +1 @@
1
- {"version":3,"file":"delivery-health-banner.js","sourceRoot":"","sources":["../../src/components/delivery-health-banner.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAC3C,OAAO,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,cAAc,CAAA;AACxC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAA;AAChE,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AACtD,OAAO,EAAE,oBAAoB,EAAE,MAAM,iCAAiC,CAAA;AACtE,OAAO,EAAE,eAAe,EAAE,QAAQ,EAAE,MAAM,kCAAkC,CAAA;AAC5E,OAAO,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAA;AACvD,OAAO,EAAE,yBAAyB,EAAE,MAAM,iCAAiC,CAAA;AAG3E;;;;;;;;;GASG;AACH,MAAM,UAAU,oBAAoB;IAClC,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,GAAG,QAAQ,CAAkB,IAAI,CAAC,CAAA;IAC7D,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAA;IAEjD,SAAS,CAAC,GAAG,EAAE;QACb,MAAM,MAAM,GAAG,oBAAoB,EAAE,CAAA;QACrC,IAAI,MAAM,CAAC,IAAI,KAAK,QAAQ;YAAE,OAAM;QACpC,IAAI,SAAS,GAAG,KAAK,CAAA;QACrB,eAAe,CAAC,oBAAoB,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;aAC9C,KAAK,CAAC,QAAQ,CAAC,yBAAyB,CAAC,EAAE,EAAE,CAAC;aAC9C,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE;YACf,IAAI,CAAC,SAAS;gBAAE,UAAU,CAAC,yBAAyB,CAAC,MAA8B,CAAC,CAAC,CAAA;QACvF,CAAC,CAAC;aACD,KAAK,CAAC,GAAG,EAAE;YACV,iEAAiE;QACnE,CAAC,CAAC,CAAA;QACJ,OAAO,GAAG,EAAE;YACV,SAAS,GAAG,IAAI,CAAA;QAClB,CAAC,CAAA;IACH,CAAC,EAAE,EAAE,CAAC,CAAA;IAEN,IAAI,SAAS,IAAI,CAAC,OAAO,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA;IAE9D,OAAO,CACL,MAAC,GAAG,IACF,GAAG,EAAC,GAAG,EACP,SAAS,EAAC,oFAAoF,iBAClF,wBAAwB,aAEpC,KAAC,MAAM,IAAC,SAAS,EAAC,mBAAmB,wBAAe,EACpD,MAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,SAAS,EAAC,cAAc,oEACI,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,6JAGvD,EACP,KAAC,MAAM,KAAG,EACV,KAAC,MAAM,IACL,OAAO,EAAC,OAAO,EACf,IAAI,EAAC,SAAS,EACd,OAAO,EAAE,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,gBACrB,SAAS,CAAC,8BAA8B,CAAC,EACrD,KAAK,EAAE,SAAS,CAAC,8BAA8B,CAAC,YAEhD,KAAC,CAAC,0BAAe,GACV,IACL,CACP,CAAA;AACH,CAAC","sourcesContent":["import { useEffect, useState } from 'react'\nimport { Rocket, X } from 'lucide-react'\nimport { Button, Row, Spacer, Text } from '@elytracms/studio/ui'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { resolveBackendConfig } from '../lib/persistence/backend-mode'\nimport { getConvexClient, queryRef } from '../lib/persistence/convex-client'\nimport { resolveDeploymentUrl } from '../lib/workspace'\nimport { missingInstantPublishVars } from '../lib/delivery/instant-publish'\nimport type { DeliveryConfigStatus } from '../lib/delivery/instant-publish'\n\n/**\n * EC-268: a boot-up health check for instant publish (EC-146). When the studio is\n * connected to a Convex deployment, it reads `delivery:deliveryConfig` once and, if\n * any of the delivery env vars are unset, shows a dismissible warning. Without this\n * the pipeline fails SILENTLY — a publish reports success but `scheduleRevalidate`\n * returns before scheduling (no `CANVAS_PROJECT_SCOPE`) and the live site never\n * updates. In playground mode (no backend) instant publish does not apply and the\n * banner never renders. Failure to read config is swallowed — it must never block\n * the studio.\n */\nexport function DeliveryHealthBanner() {\n const [missing, setMissing] = useState<string[] | null>(null)\n const [dismissed, setDismissed] = useState(false)\n\n useEffect(() => {\n const config = resolveBackendConfig()\n if (config.kind !== 'convex') return\n let cancelled = false\n getConvexClient(resolveDeploymentUrl(config.url))\n .query(queryRef('delivery:deliveryConfig'), {})\n .then((status) => {\n if (!cancelled) setMissing(missingInstantPublishVars(status as DeliveryConfigStatus))\n })\n .catch(() => {\n /* config unknown (older deployment, transport) — stay silent. */\n })\n return () => {\n cancelled = true\n }\n }, [])\n\n if (dismissed || !missing || missing.length === 0) return null\n\n return (\n <Row\n gap=\"2\"\n className=\"shrink-0 border-b border-warning/30 bg-warning/10 px-4 py-1.5 text-warning sm:px-6\"\n data-testid=\"delivery-health-banner\"\n >\n <Rocket className=\"size-3.5 shrink-0\" aria-hidden />\n <Text size=\"xs\" className=\"text-warning\">\n Instant publish isn’t fully configured — {missing.join(', ')} not set on this Convex\n deployment. Publishes succeed, but the live site won’t update until you set them (see\n CONNECT.md) and re-push functions.\n </Text>\n <Spacer />\n <Button\n variant=\"ghost\"\n size=\"icon-sm\"\n onClick={() => setDismissed(true)}\n aria-label={translate('shell.deliveryHealth.dismiss')}\n title={translate('shell.deliveryHealth.dismiss')}\n >\n <X aria-hidden />\n </Button>\n </Row>\n )\n}\n"]}
1
+ {"version":3,"file":"delivery-health-banner.js","sourceRoot":"","sources":["../../src/components/delivery-health-banner.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAChC,OAAO,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,cAAc,CAAA;AACxC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAA;AAChE,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AACtD,OAAO,EAAE,oBAAoB,EAAE,MAAM,iCAAiC,CAAA;AACtE,OAAO,EAAE,yBAAyB,EAAE,MAAM,iCAAiC,CAAA;AAC3E,OAAO,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAA;AAG/D;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,oBAAoB,CAAC,EAAE,SAAS,EAAyB;IACvE,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAA;IACjD,MAAM,OAAO,GAAG,oBAAoB,EAAE,CAAC,IAAI,KAAK,QAAQ,CAAA;IAExD,MAAM,KAAK,GAAG,iBAAiB,CAAuB;QACpD,QAAQ,EAAE,CAAC,UAAU,EAAE,QAAQ,EAAE,SAAS,CAAC;QAC3C,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,EAAE;YAC3B,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,EAAE,CAAC,CAAA;YAC9D,IAAI,CAAC,MAAM,CAAC,EAAE;gBAAE,MAAM,IAAI,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;YACrD,OAAO,MAAM,CAAC,KAAK,CAAA;QACrB,CAAC;QACD,OAAO;KACR,CAAC,CAAA;IACF,0EAA0E;IAC1E,gFAAgF;IAChF,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,yBAAyB,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;IAEvE,IAAI,SAAS,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA;IAElD,OAAO,CACL,MAAC,GAAG,IACF,GAAG,EAAC,GAAG,EACP,SAAS,EAAC,oFAAoF,iBAClF,wBAAwB,aAEpC,KAAC,MAAM,IAAC,SAAS,EAAC,mBAAmB,wBAAe,EACpD,MAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,SAAS,EAAC,cAAc,oEACI,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,6JAGvD,EACP,KAAC,MAAM,KAAG,EACV,KAAC,MAAM,IACL,OAAO,EAAC,OAAO,EACf,IAAI,EAAC,SAAS,EACd,OAAO,EAAE,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,gBACrB,SAAS,CAAC,8BAA8B,CAAC,EACrD,KAAK,EAAE,SAAS,CAAC,8BAA8B,CAAC,YAEhD,KAAC,CAAC,0BAAe,GACV,IACL,CACP,CAAA;AACH,CAAC","sourcesContent":["import { useState } from 'react'\nimport { Rocket, X } from 'lucide-react'\nimport { Button, Row, Spacer, Text } from '@elytracms/studio/ui'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { resolveBackendConfig } from '../lib/persistence/backend-mode'\nimport { missingInstantPublishVars } from '../lib/delivery/instant-publish'\nimport { useOperationQuery } from '@elytracms/studio/lib/query'\nimport type { DeliveryConfigStatus } from '@elytracms/core/persistence'\n\n/**\n * EC-268: a boot-up health check for instant publish (EC-146). When the studio is\n * connected to a Convex deployment, it reads the delivery config through the\n * operations seam (EC-329 — `operations.delivery.config`, cached by\n * `useOperationQuery` like every other studio read, AD-17) and, if any of the\n * delivery env vars are unset, shows a dismissible warning. Without this the\n * pipeline fails SILENTLY — a publish reports success but `scheduleRevalidate`\n * returns before scheduling (no `CANVAS_PROJECT_SCOPE`) and the live site never\n * updates. In playground mode (no backend) instant publish does not apply and the\n * banner never renders (the query is disabled, so it never has data). A failed\n * read degrades silently — it must never block the studio — by treating the\n * query's error branch the same as \"nothing missing\", matching the former raw\n * client call's swallowed `.catch()`.\n */\nexport function DeliveryHealthBanner({ projectId }: { projectId: string }) {\n const [dismissed, setDismissed] = useState(false)\n const enabled = resolveBackendConfig().kind === 'convex'\n\n const query = useOperationQuery<DeliveryConfigStatus>({\n queryKey: ['delivery', 'config', projectId],\n loader: async (operations) => {\n const result = await operations.delivery.config({ projectId })\n if (!result.ok) throw new Error(result.error.message)\n return result.value\n },\n enabled,\n })\n // Loading/error/playground (disabled) all read as \"nothing missing\" — the\n // banner only ever surfaces a CONFIRMED gap, never a transient/unreachable one.\n const missing = query.data ? missingInstantPublishVars(query.data) : []\n\n if (dismissed || missing.length === 0) return null\n\n return (\n <Row\n gap=\"2\"\n className=\"shrink-0 border-b border-warning/30 bg-warning/10 px-4 py-1.5 text-warning sm:px-6\"\n data-testid=\"delivery-health-banner\"\n >\n <Rocket className=\"size-3.5 shrink-0\" aria-hidden />\n <Text size=\"xs\" className=\"text-warning\">\n Instant publish isn’t fully configured — {missing.join(', ')} not set on this Convex\n deployment. Publishes succeed, but the live site won’t update until you set them (see\n CONNECT.md) and re-push functions.\n </Text>\n <Spacer />\n <Button\n variant=\"ghost\"\n size=\"icon-sm\"\n onClick={() => setDismissed(true)}\n aria-label={translate('shell.deliveryHealth.dismiss')}\n title={translate('shell.deliveryHealth.dismiss')}\n >\n <X aria-hidden />\n </Button>\n </Row>\n )\n}\n"]}
@@ -1,5 +1,3 @@
1
- /** Tooltip/accessible description — the studio signed in automatically. */
2
- export declare const DEV_LOGIN_BADGE_HINT = "Dev auto-login: the studio signed in automatically from VITE_ELYTRA_DEV_LOGIN (dev builds only). Unset it to test the real sign-in flow.";
3
1
  /**
4
2
  * Marker for the dev-only unattended auth (see `resolveDevAutoLogin`). When the
5
3
  * studio auto-signs-in from `VITE_ELYTRA_DEV_LOGIN` in a dev build, this badge
@@ -3,8 +3,6 @@ import { KeyRound } from 'lucide-react';
3
3
  import { Badge } from '@elytracms/studio/ui';
4
4
  import { isDevAutoLoginActive } from '@elytracms/studio/lib/persistence';
5
5
  import { translate } from '@elytracms/studio/lib/i18n';
6
- /** Tooltip/accessible description — the studio signed in automatically. */
7
- export const DEV_LOGIN_BADGE_HINT = 'Dev auto-login: the studio signed in automatically from VITE_ELYTRA_DEV_LOGIN (dev builds only). Unset it to test the real sign-in flow.';
8
6
  /**
9
7
  * Marker for the dev-only unattended auth (see `resolveDevAutoLogin`). When the
10
8
  * studio auto-signs-in from `VITE_ELYTRA_DEV_LOGIN` in a dev build, this badge
@@ -14,6 +12,6 @@ export const DEV_LOGIN_BADGE_HINT = 'Dev auto-login: the studio signed in automa
14
12
  export function DevLoginBadge() {
15
13
  if (!isDevAutoLoginActive())
16
14
  return null;
17
- return (_jsxs(Badge, { variant: "warning", title: DEV_LOGIN_BADGE_HINT, "data-testid": "dev-login-badge", children: [_jsx(KeyRound, { "aria-hidden": true }), translate('shell.devLogin.badge')] }));
15
+ return (_jsxs(Badge, { variant: "warning", title: translate('shell.devLogin.hint'), "data-testid": "dev-login-badge", children: [_jsx(KeyRound, { "aria-hidden": true }), translate('shell.devLogin.badge')] }));
18
16
  }
19
17
  //# sourceMappingURL=dev-login-badge.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"dev-login-badge.js","sourceRoot":"","sources":["../../src/components/dev-login-badge.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAA;AACvC,OAAO,EAAE,KAAK,EAAE,MAAM,sBAAsB,CAAA;AAC5C,OAAO,EAAE,oBAAoB,EAAE,MAAM,mCAAmC,CAAA;AACxE,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AAEtD,2EAA2E;AAC3E,MAAM,CAAC,MAAM,oBAAoB,GAC/B,0IAA0I,CAAA;AAE5I;;;;;GAKG;AACH,MAAM,UAAU,aAAa;IAC3B,IAAI,CAAC,oBAAoB,EAAE;QAAE,OAAO,IAAI,CAAA;IACxC,OAAO,CACL,MAAC,KAAK,IAAC,OAAO,EAAC,SAAS,EAAC,KAAK,EAAE,oBAAoB,iBAAc,iBAAiB,aACjF,KAAC,QAAQ,0BAAe,EACvB,SAAS,CAAC,sBAAsB,CAAC,IAC5B,CACT,CAAA;AACH,CAAC","sourcesContent":["import { KeyRound } from 'lucide-react'\nimport { Badge } from '@elytracms/studio/ui'\nimport { isDevAutoLoginActive } from '@elytracms/studio/lib/persistence'\nimport { translate } from '@elytracms/studio/lib/i18n'\n\n/** Tooltip/accessible description — the studio signed in automatically. */\nexport const DEV_LOGIN_BADGE_HINT =\n 'Dev auto-login: the studio signed in automatically from VITE_ELYTRA_DEV_LOGIN (dev builds only). Unset it to test the real sign-in flow.'\n\n/**\n * Marker for the dev-only unattended auth (see `resolveDevAutoLogin`). When the\n * studio auto-signs-in from `VITE_ELYTRA_DEV_LOGIN` in a dev build, this badge\n * is visible in the shell header so an auto-authenticated session is never\n * silent; in production builds and without the flag it renders nothing.\n */\nexport function DevLoginBadge() {\n if (!isDevAutoLoginActive()) return null\n return (\n <Badge variant=\"warning\" title={DEV_LOGIN_BADGE_HINT} data-testid=\"dev-login-badge\">\n <KeyRound aria-hidden />\n {translate('shell.devLogin.badge')}\n </Badge>\n )\n}\n"]}
1
+ {"version":3,"file":"dev-login-badge.js","sourceRoot":"","sources":["../../src/components/dev-login-badge.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAA;AACvC,OAAO,EAAE,KAAK,EAAE,MAAM,sBAAsB,CAAA;AAC5C,OAAO,EAAE,oBAAoB,EAAE,MAAM,mCAAmC,CAAA;AACxE,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AAEtD;;;;;GAKG;AACH,MAAM,UAAU,aAAa;IAC3B,IAAI,CAAC,oBAAoB,EAAE;QAAE,OAAO,IAAI,CAAA;IACxC,OAAO,CACL,MAAC,KAAK,IAAC,OAAO,EAAC,SAAS,EAAC,KAAK,EAAE,SAAS,CAAC,qBAAqB,CAAC,iBAAc,iBAAiB,aAC7F,KAAC,QAAQ,0BAAe,EACvB,SAAS,CAAC,sBAAsB,CAAC,IAC5B,CACT,CAAA;AACH,CAAC","sourcesContent":["import { KeyRound } from 'lucide-react'\nimport { Badge } from '@elytracms/studio/ui'\nimport { isDevAutoLoginActive } from '@elytracms/studio/lib/persistence'\nimport { translate } from '@elytracms/studio/lib/i18n'\n\n/**\n * Marker for the dev-only unattended auth (see `resolveDevAutoLogin`). When the\n * studio auto-signs-in from `VITE_ELYTRA_DEV_LOGIN` in a dev build, this badge\n * is visible in the shell header so an auto-authenticated session is never\n * silent; in production builds and without the flag it renders nothing.\n */\nexport function DevLoginBadge() {\n if (!isDevAutoLoginActive()) return null\n return (\n <Badge variant=\"warning\" title={translate('shell.devLogin.hint')} data-testid=\"dev-login-badge\">\n <KeyRound aria-hidden />\n {translate('shell.devLogin.badge')}\n </Badge>\n )\n}\n"]}
@@ -1,7 +1,18 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { PanelRight } from 'lucide-react';
2
3
  import { useState } from 'react';
3
4
  import { Row, Text } from '@elytracms/studio/ui';
4
5
  import { cn } from '@elytracms/studio/lib/utils';
6
+ /**
7
+ * When the sidebar carries this many panes or more, inactive tabs collapse to their
8
+ * icon alone (label in a tooltip + `aria-label`) so the tab strip stops overflowing
9
+ * (0.0.42, round 3). 1–2 panes keep their full labels.
10
+ */
11
+ const ICON_COLLAPSE_THRESHOLD = 3;
12
+ /** Fallback icon for a pane that declares none — so an icon-collapsed tab is never blank. */
13
+ function paneIcon(pane) {
14
+ return pane.icon ?? _jsx(PanelRight, { className: "size-3.5" });
15
+ }
5
16
  export function InspectorSidebar({ panes, className, activeId: controlledId, onActiveChange, }) {
6
17
  const [internalId, setInternalId] = useState(() => panes[0]?.id);
7
18
  const activeId = controlledId ?? internalId;
@@ -15,9 +26,15 @@ export function InspectorSidebar({ panes, className, activeId: controlledId, onA
15
26
  const active = panes.find((pane) => pane.id === activeId) ?? panes[0];
16
27
  if (!active)
17
28
  return null;
18
- return (_jsxs("div", { className: cn('flex h-full min-h-0 flex-col bg-card', className), children: [_jsx(Row, { gap: "1", className: "shrink-0 border-b border-border p-1.5", children: panes.map((pane) => (_jsx(PaneTab, { pane: pane, active: pane.id === active.id, onClick: () => select(pane.id) }, pane.id))) }), _jsx("div", { className: "min-h-0 flex-1 overflow-auto", children: active.render() })] }));
29
+ // 0.0.42: with a crowded tab strip (3+ panes) the inactive tabs collapse to icons so
30
+ // the labels stop overflowing; the active tab always keeps its icon + label.
31
+ const collapseInactive = panes.length >= ICON_COLLAPSE_THRESHOLD;
32
+ return (_jsxs("div", { className: cn('flex h-full min-h-0 flex-col bg-card', className), children: [_jsx(Row, { gap: "1", className: "shrink-0 border-b border-border p-1.5", children: panes.map((pane) => {
33
+ const isActive = pane.id === active.id;
34
+ return (_jsx(PaneTab, { pane: pane, active: isActive, showLabel: isActive || !collapseInactive, onClick: () => select(pane.id) }, pane.id));
35
+ }) }), _jsx("div", { className: "min-h-0 flex-1 overflow-auto", children: active.render() })] }));
19
36
  }
20
- function PaneTab({ pane, active, onClick, }) {
21
- return (_jsxs("button", { type: "button", "aria-pressed": active, onClick: onClick, className: cn('inline-flex items-center gap-1.5 rounded-md px-2.5 py-1 transition-colors', active ? 'bg-accent text-accent-foreground' : 'text-muted-foreground hover:bg-muted'), children: [pane.icon, _jsx(Text, { as: "span", size: "xs", weight: "medium", className: "leading-none", children: pane.label }), pane.badge] }));
37
+ function PaneTab({ pane, active, showLabel, onClick, }) {
38
+ return (_jsxs("button", { type: "button", "aria-pressed": active, "aria-label": showLabel ? undefined : pane.label, title: showLabel ? undefined : pane.label, onClick: onClick, className: cn('inline-flex items-center gap-1.5 rounded-md px-2.5 py-1 transition-colors', active ? 'bg-accent text-accent-foreground' : 'text-muted-foreground hover:bg-muted'), children: [paneIcon(pane), showLabel ? (_jsx(Text, { as: "span", size: "xs", weight: "medium", className: "leading-none", children: pane.label })) : null, pane.badge] }));
22
39
  }
23
40
  //# sourceMappingURL=inspector-sidebar.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"inspector-sidebar.js","sourceRoot":"","sources":["../../../src/components/inspector/inspector-sidebar.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAEhC,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAA;AAChD,OAAO,EAAE,EAAE,EAAE,MAAM,6BAA6B,CAAA;AA0BhD,MAAM,UAAU,gBAAgB,CAAC,EAC/B,KAAK,EACL,SAAS,EACT,QAAQ,EAAE,YAAY,EACtB,cAAc,GAWf;IACC,MAAM,CAAC,UAAU,EAAE,aAAa,CAAC,GAAG,QAAQ,CAAqB,GAAG,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAA;IACpF,MAAM,QAAQ,GAAG,YAAY,IAAI,UAAU,CAAA;IAC3C,MAAM,MAAM,GAAG,CAAC,EAAU,EAAE,EAAE;QAC5B,IAAI,YAAY,KAAK,SAAS;YAAE,aAAa,CAAC,EAAE,CAAC,CAAA;QACjD,cAAc,EAAE,CAAC,EAAE,CAAC,CAAA;IACtB,CAAC,CAAA;IACD,8EAA8E;IAC9E,qEAAqE;IACrE,MAAM,MAAM,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,KAAK,QAAQ,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,CAAA;IACrE,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAA;IAExB,OAAO,CACL,eAAK,SAAS,EAAE,EAAE,CAAC,sCAAsC,EAAE,SAAS,CAAC,aACnE,KAAC,GAAG,IAAC,GAAG,EAAC,GAAG,EAAC,SAAS,EAAC,uCAAuC,YAC3D,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CACnB,KAAC,OAAO,IAEN,IAAI,EAAE,IAAI,EACV,MAAM,EAAE,IAAI,CAAC,EAAE,KAAK,MAAM,CAAC,EAAE,EAC7B,OAAO,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,IAHzB,IAAI,CAAC,EAAE,CAIZ,CACH,CAAC,GACE,EACN,cAAK,SAAS,EAAC,8BAA8B,YAAE,MAAM,CAAC,MAAM,EAAE,GAAO,IACjE,CACP,CAAA;AACH,CAAC;AAED,SAAS,OAAO,CAAC,EACf,IAAI,EACJ,MAAM,EACN,OAAO,GAKR;IACC,OAAO,CACL,kBACE,IAAI,EAAC,QAAQ,kBACC,MAAM,EACpB,OAAO,EAAE,OAAO,EAChB,SAAS,EAAE,EAAE,CACX,2EAA2E,EAC3E,MAAM,CAAC,CAAC,CAAC,kCAAkC,CAAC,CAAC,CAAC,sCAAsC,CACrF,aAEA,IAAI,CAAC,IAAI,EACV,KAAC,IAAI,IAAC,EAAE,EAAC,MAAM,EAAC,IAAI,EAAC,IAAI,EAAC,MAAM,EAAC,QAAQ,EAAC,SAAS,EAAC,cAAc,YAC/D,IAAI,CAAC,KAAK,GACN,EACN,IAAI,CAAC,KAAK,IACJ,CACV,CAAA;AACH,CAAC","sourcesContent":["import { useState } from 'react'\nimport type { ReactNode } from 'react'\nimport { Row, Text } from '@elytracms/studio/ui'\nimport { cn } from '@elytracms/studio/lib/utils'\n\n/**\n * EC-224 — the shared, view-supplied inspector sidebar.\n *\n * A generic tabbed frame: it renders whatever {@link InspectorPane}s it is given\n * and owns nothing domain-specific. The OWNER composes the pane list — by\n * convention `panes = [...view.suppliedPanes, versionsPane]`, so a view (the\n * canvas) contributes its context pane (`Properties`) and the frame-owner (the\n * document editor) appends the document-level `Versions` pane. A future rich\n * field editor contributes its own pane the same way, without touching this\n * frame (AD-12 — the canvas is itself just a field editor).\n *\n * Each pane's `render` runs where the frame is mounted, so a pane that needs a\n * scoped context (e.g. the canvas `BuilderProvider` behind `PropertyInspector`)\n * must be supplied by a view that mounts the frame inside that provider.\n */\nexport interface InspectorPane {\n id: string\n label: string\n icon?: ReactNode\n /** Optional trailing adornment on the tab (e.g. a count badge for `Issues`). */\n badge?: ReactNode\n render: () => ReactNode\n}\n\nexport function InspectorSidebar({\n panes,\n className,\n activeId: controlledId,\n onActiveChange,\n}: {\n panes: readonly InspectorPane[]\n className?: string\n /**\n * Optionally control which pane is shown (EC-285): the document editor drives\n * this to open the `Details` pane when a jump-to-field targets a sidebar field.\n * Uncontrolled (internal state) when omitted.\n */\n activeId?: string\n onActiveChange?: (id: string) => void\n}) {\n const [internalId, setInternalId] = useState<string | undefined>(() => panes[0]?.id)\n const activeId = controlledId ?? internalId\n const select = (id: string) => {\n if (controlledId === undefined) setInternalId(id)\n onActiveChange?.(id)\n }\n // Derive the active pane defensively so a changed pane list (switching views)\n // can never render a stale/absent tab — fall back to the first pane.\n const active = panes.find((pane) => pane.id === activeId) ?? panes[0]\n if (!active) return null\n\n return (\n <div className={cn('flex h-full min-h-0 flex-col bg-card', className)}>\n <Row gap=\"1\" className=\"shrink-0 border-b border-border p-1.5\">\n {panes.map((pane) => (\n <PaneTab\n key={pane.id}\n pane={pane}\n active={pane.id === active.id}\n onClick={() => select(pane.id)}\n />\n ))}\n </Row>\n <div className=\"min-h-0 flex-1 overflow-auto\">{active.render()}</div>\n </div>\n )\n}\n\nfunction PaneTab({\n pane,\n active,\n onClick,\n}: {\n pane: InspectorPane\n active: boolean\n onClick: () => void\n}) {\n return (\n <button\n type=\"button\"\n aria-pressed={active}\n onClick={onClick}\n className={cn(\n 'inline-flex items-center gap-1.5 rounded-md px-2.5 py-1 transition-colors',\n active ? 'bg-accent text-accent-foreground' : 'text-muted-foreground hover:bg-muted',\n )}\n >\n {pane.icon}\n <Text as=\"span\" size=\"xs\" weight=\"medium\" className=\"leading-none\">\n {pane.label}\n </Text>\n {pane.badge}\n </button>\n )\n}\n"]}
1
+ {"version":3,"file":"inspector-sidebar.js","sourceRoot":"","sources":["../../../src/components/inspector/inspector-sidebar.tsx"],"names":[],"mappings":"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","sourcesContent":["import { PanelRight } from 'lucide-react'\nimport { useState } from 'react'\nimport type { ReactNode } from 'react'\nimport { Row, Text } from '@elytracms/studio/ui'\nimport { cn } from '@elytracms/studio/lib/utils'\n\n/**\n * When the sidebar carries this many panes or more, inactive tabs collapse to their\n * icon alone (label in a tooltip + `aria-label`) so the tab strip stops overflowing\n * (0.0.42, round 3). 1–2 panes keep their full labels.\n */\nconst ICON_COLLAPSE_THRESHOLD = 3\n\n/** Fallback icon for a pane that declares none — so an icon-collapsed tab is never blank. */\nfunction paneIcon(pane: InspectorPane): ReactNode {\n return pane.icon ?? <PanelRight className=\"size-3.5\" />\n}\n\n/**\n * EC-224 — the shared, view-supplied inspector sidebar.\n *\n * A generic tabbed frame: it renders whatever {@link InspectorPane}s it is given\n * and owns nothing domain-specific. The OWNER composes the pane list — by\n * convention `panes = [...view.suppliedPanes, versionsPane]`, so a view (the\n * canvas) contributes its context pane (`Properties`) and the frame-owner (the\n * document editor) appends the document-level `Versions` pane. A future rich\n * field editor contributes its own pane the same way, without touching this\n * frame (AD-12 — the canvas is itself just a field editor).\n *\n * Each pane's `render` runs where the frame is mounted, so a pane that needs a\n * scoped context (e.g. the canvas `BuilderProvider` behind `PropertyInspector`)\n * must be supplied by a view that mounts the frame inside that provider.\n */\nexport interface InspectorPane {\n id: string\n label: string\n icon?: ReactNode\n /** Optional trailing adornment on the tab (e.g. a count badge for `Issues`). */\n badge?: ReactNode\n render: () => ReactNode\n}\n\nexport function InspectorSidebar({\n panes,\n className,\n activeId: controlledId,\n onActiveChange,\n}: {\n panes: readonly InspectorPane[]\n className?: string\n /**\n * Optionally control which pane is shown (EC-285): the document editor drives\n * this to open the `Details` pane when a jump-to-field targets a sidebar field.\n * Uncontrolled (internal state) when omitted.\n */\n activeId?: string\n onActiveChange?: (id: string) => void\n}) {\n const [internalId, setInternalId] = useState<string | undefined>(() => panes[0]?.id)\n const activeId = controlledId ?? internalId\n const select = (id: string) => {\n if (controlledId === undefined) setInternalId(id)\n onActiveChange?.(id)\n }\n // Derive the active pane defensively so a changed pane list (switching views)\n // can never render a stale/absent tab — fall back to the first pane.\n const active = panes.find((pane) => pane.id === activeId) ?? panes[0]\n if (!active) return null\n\n // 0.0.42: with a crowded tab strip (3+ panes) the inactive tabs collapse to icons so\n // the labels stop overflowing; the active tab always keeps its icon + label.\n const collapseInactive = panes.length >= ICON_COLLAPSE_THRESHOLD\n\n return (\n <div className={cn('flex h-full min-h-0 flex-col bg-card', className)}>\n <Row gap=\"1\" className=\"shrink-0 border-b border-border p-1.5\">\n {panes.map((pane) => {\n const isActive = pane.id === active.id\n return (\n <PaneTab\n key={pane.id}\n pane={pane}\n active={isActive}\n showLabel={isActive || !collapseInactive}\n onClick={() => select(pane.id)}\n />\n )\n })}\n </Row>\n <div className=\"min-h-0 flex-1 overflow-auto\">{active.render()}</div>\n </div>\n )\n}\n\nfunction PaneTab({\n pane,\n active,\n showLabel,\n onClick,\n}: {\n pane: InspectorPane\n active: boolean\n /** When false, the tab renders icon-only — the label moves to the tooltip + aria-label. */\n showLabel: boolean\n onClick: () => void\n}) {\n return (\n <button\n type=\"button\"\n aria-pressed={active}\n aria-label={showLabel ? undefined : pane.label}\n title={showLabel ? undefined : pane.label}\n onClick={onClick}\n className={cn(\n 'inline-flex items-center gap-1.5 rounded-md px-2.5 py-1 transition-colors',\n active ? 'bg-accent text-accent-foreground' : 'text-muted-foreground hover:bg-muted',\n )}\n >\n {paneIcon(pane)}\n {showLabel ? (\n <Text as=\"span\" size=\"xs\" weight=\"medium\" className=\"leading-none\">\n {pane.label}\n </Text>\n ) : null}\n {pane.badge}\n </button>\n )\n}\n"]}
@@ -1,5 +1,3 @@
1
- /** Tooltip/accessible description — playground data is local and ephemeral. */
2
- export declare const PLAYGROUND_BADGE_HINT = "Playground mode: data lives in memory on this device and resets on reload. Nothing is saved to a backend.";
3
1
  /**
4
2
  * EC-153: the explicit playground-mode marker. When the studio runs on the
5
3
  * seeded in-memory adapter (`VITE_ELYTRA_MODE=playground`) this badge is
@@ -3,8 +3,6 @@ import { FlaskConical } from 'lucide-react';
3
3
  import { Badge } from '@elytracms/studio/ui';
4
4
  import { isPlaygroundMode } from '@elytracms/studio/lib/persistence';
5
5
  import { translate } from '@elytracms/studio/lib/i18n';
6
- /** Tooltip/accessible description — playground data is local and ephemeral. */
7
- export const PLAYGROUND_BADGE_HINT = 'Playground mode: data lives in memory on this device and resets on reload. Nothing is saved to a backend.';
8
6
  /**
9
7
  * EC-153: the explicit playground-mode marker. When the studio runs on the
10
8
  * seeded in-memory adapter (`VITE_ELYTRA_MODE=playground`) this badge is
@@ -1 +1 @@
1
- {"version":3,"file":"playground-badge.js","sourceRoot":"","sources":["../../src/components/playground-badge.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAC3C,OAAO,EAAE,KAAK,EAAE,MAAM,sBAAsB,CAAA;AAC5C,OAAO,EAAE,gBAAgB,EAAE,MAAM,mCAAmC,CAAA;AACpE,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AAEtD,+EAA+E;AAC/E,MAAM,CAAC,MAAM,qBAAqB,GAChC,2GAA2G,CAAA;AAE7G;;;;;;GAMG;AACH,MAAM,UAAU,eAAe;IAC7B,IAAI,CAAC,gBAAgB,EAAE;QAAE,OAAO,IAAI,CAAA;IACpC,OAAO,CACL,MAAC,KAAK,IAAC,OAAO,EAAC,SAAS,EAAC,KAAK,EAAE,SAAS,CAAC,uBAAuB,CAAC,iBAAc,kBAAkB,aAChG,KAAC,YAAY,0BAAe,EAC3B,SAAS,CAAC,wBAAwB,CAAC,IAC9B,CACT,CAAA;AACH,CAAC","sourcesContent":["import { FlaskConical } from 'lucide-react'\nimport { Badge } from '@elytracms/studio/ui'\nimport { isPlaygroundMode } from '@elytracms/studio/lib/persistence'\nimport { translate } from '@elytracms/studio/lib/i18n'\n\n/** Tooltip/accessible description — playground data is local and ephemeral. */\nexport const PLAYGROUND_BADGE_HINT =\n 'Playground mode: data lives in memory on this device and resets on reload. Nothing is saved to a backend.'\n\n/**\n * EC-153: the explicit playground-mode marker. When the studio runs on the\n * seeded in-memory adapter (`VITE_ELYTRA_MODE=playground`) this badge is\n * always visible in the shell header; in Convex mode it renders nothing.\n * Deterministic: mode is resolved from build-time env, identical on SSR and\n * client.\n */\nexport function PlaygroundBadge() {\n if (!isPlaygroundMode()) return null\n return (\n <Badge variant=\"warning\" title={translate('shell.playground.hint')} data-testid=\"playground-badge\">\n <FlaskConical aria-hidden />\n {translate('shell.playground.badge')}\n </Badge>\n )\n}\n"]}
1
+ {"version":3,"file":"playground-badge.js","sourceRoot":"","sources":["../../src/components/playground-badge.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAC3C,OAAO,EAAE,KAAK,EAAE,MAAM,sBAAsB,CAAA;AAC5C,OAAO,EAAE,gBAAgB,EAAE,MAAM,mCAAmC,CAAA;AACpE,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AAEtD;;;;;;GAMG;AACH,MAAM,UAAU,eAAe;IAC7B,IAAI,CAAC,gBAAgB,EAAE;QAAE,OAAO,IAAI,CAAA;IACpC,OAAO,CACL,MAAC,KAAK,IAAC,OAAO,EAAC,SAAS,EAAC,KAAK,EAAE,SAAS,CAAC,uBAAuB,CAAC,iBAAc,kBAAkB,aAChG,KAAC,YAAY,0BAAe,EAC3B,SAAS,CAAC,wBAAwB,CAAC,IAC9B,CACT,CAAA;AACH,CAAC","sourcesContent":["import { FlaskConical } from 'lucide-react'\nimport { Badge } from '@elytracms/studio/ui'\nimport { isPlaygroundMode } from '@elytracms/studio/lib/persistence'\nimport { translate } from '@elytracms/studio/lib/i18n'\n\n/**\n * EC-153: the explicit playground-mode marker. When the studio runs on the\n * seeded in-memory adapter (`VITE_ELYTRA_MODE=playground`) this badge is\n * always visible in the shell header; in Convex mode it renders nothing.\n * Deterministic: mode is resolved from build-time env, identical on SSR and\n * client.\n */\nexport function PlaygroundBadge() {\n if (!isPlaygroundMode()) return null\n return (\n <Badge variant=\"warning\" title={translate('shell.playground.hint')} data-testid=\"playground-badge\">\n <FlaskConical aria-hidden />\n {translate('shell.playground.badge')}\n </Badge>\n )\n}\n"]}
@@ -8,7 +8,7 @@ import { PlaygroundBadge } from './playground-badge';
8
8
  import { SignInNeededBadge } from './sign-in-needed-badge';
9
9
  import { SectionStateView } from './section-state';
10
10
  import { useStudio } from './studio-context';
11
- import { useSwitcherEntries } from './workspace-projects';
11
+ import { useRecentProjects, useSwitcherEntries } from './workspace-projects';
12
12
  /**
13
13
  * Studio launcher / project list (brief §4.3, §8.1; reframed by EC-154). The
14
14
  * Studio's front door: lists the workspace's projects and routes into a
@@ -23,11 +23,7 @@ export function ProjectLauncher({ forcedState }) {
23
23
  const { entries, workspace } = useSwitcherEntries();
24
24
  const hasConfig = workspace.kind === 'config';
25
25
  const visible = entries;
26
- const recents = studio.context.recentProjectIds
27
- .map((id) => studio.getProject(id))
28
- .filter((p) => Boolean(p))
29
- .filter((p) => entries.some((e) => e.kind === 'project' && e.project.id === p.id))
30
- .slice(0, 4);
26
+ const recents = useRecentProjects(entries);
31
27
  const forced = forcedState && forcedState !== 'working' ? forcedState : null;
32
28
  return (_jsxs("div", { className: "mx-auto w-full max-w-6xl px-6 py-8", children: [_jsxs(Row, { gap: "3", className: "mb-6", children: [_jsxs(Stack, { gap: "1", children: [_jsxs(Row, { gap: "2", children: [_jsx(Heading, { size: "display", children: translate('shell.projects') }), _jsx(PlaygroundBadge, {}), hasConfig ? (_jsxs(Badge, { variant: "outline", children: [_jsx(TerminalSquare, { className: "size-3" }), " ", workspace.source] })) : null] }), _jsx(Text, { tone: "muted", children: hasConfig
33
29
  ? 'Projects are defined in code. Add entries to elytra.config.ts and provision them with the CLI.'
@@ -1 +1 @@
1
- {"version":3,"file":"project-list.js","sourceRoot":"","sources":["../../src/components/project-list.tsx"],"names":[],"mappings":"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","sourcesContent":["import { Link, STUDIO_PROJECT_ROUTE, studioPath } from '@elytracms/studio/router'\nimport { Globe, LogIn, TerminalSquare } from 'lucide-react'\nimport type { Project, ResolvedWorkspaceProject, SectionState } from '@elytracms/core/studio-core'\nimport { Badge, Button, Card, Code, Heading, Row, Spacer, Stack, Text } from '@elytracms/studio/ui'\nimport { hasProjectSession } from '@elytracms/studio/lib/auth'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { PlaygroundBadge } from './playground-badge'\nimport { SignInNeededBadge } from './sign-in-needed-badge'\nimport { SectionStateView } from './section-state'\nimport { useStudio } from './studio-context'\nimport { useSwitcherEntries } from './workspace-projects'\n\nexport interface ProjectLauncherProps {\n /** Force a deterministic list state for browser verification (?state=…). */\n forcedState?: SectionState | null\n}\n\n/**\n * Studio launcher / project list (brief §4.3, §8.1; reframed by EC-154). The\n * Studio's front door: lists the workspace's projects and routes into a\n * project's shell. With a workspace config the list is config-driven\n * (configured projects ∩ grants, plus explicit \"not provisioned\" states);\n * project *creation* is code-first — `elytra.config.ts` + `builder init\n * --project <slug>` — so there is no in-UI create flow. Without a config the\n * adapter-backed list (and the starter-kit gallery) behaves as before.\n */\nexport function ProjectLauncher({ forcedState }: ProjectLauncherProps) {\n const studio = useStudio()\n const { entries, workspace } = useSwitcherEntries()\n\n const hasConfig = workspace.kind === 'config'\n const visible = entries\n const recents = studio.context.recentProjectIds\n .map((id) => studio.getProject(id))\n .filter((p): p is Project => Boolean(p))\n .filter((p) => entries.some((e) => e.kind === 'project' && e.project.id === p.id))\n .slice(0, 4)\n\n const forced = forcedState && forcedState !== 'working' ? forcedState : null\n\n return (\n <div className=\"mx-auto w-full max-w-6xl px-6 py-8\">\n <Row gap=\"3\" className=\"mb-6\">\n <Stack gap=\"1\">\n <Row gap=\"2\">\n <Heading size=\"display\">{translate('shell.projects')}</Heading>\n {/* EC-153: explicit playground-mode marker (renders nothing on Convex). */}\n <PlaygroundBadge />\n {hasConfig ? (\n // EC-154: this workspace is defined in code.\n <Badge variant=\"outline\">\n <TerminalSquare className=\"size-3\" /> {workspace.source}\n </Badge>\n ) : null}\n </Row>\n <Text tone=\"muted\">\n {hasConfig\n ? 'Projects are defined in code. Add entries to elytra.config.ts and provision them with the CLI.'\n : 'Open and manage your Elytra projects.'}\n </Text>\n </Stack>\n <Spacer />\n </Row>\n\n {recents.length > 0 && !forced ? (\n <Stack gap=\"2\" className=\"mb-6\">\n <Text size=\"xs\" tone=\"muted\" mono className=\"uppercase\">\n {translate('shell.projectList.recent')}\n </Text>\n <Row gap=\"2\" wrap>\n {recents.map((p) => (\n <Link\n key={p.id}\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId: p.id, _splat: studioPath.index() }}\n onClick={() => studio.activate(p.id)}\n className=\"inline-flex items-center gap-2 rounded-md border border-border bg-card px-3 py-1.5 text-sm transition-colors hover:border-primary/40 hover:bg-muted\"\n >\n <span className=\"grid size-4 place-items-center rounded-[4px] bg-primary text-[0.6rem] font-bold text-primary-foreground\">\n {p.name.charAt(0).toUpperCase()}\n </span>\n {p.name}\n </Link>\n ))}\n </Row>\n </Stack>\n ) : null}\n\n {forced ? (\n <SectionStateView\n state={forced}\n title={forced === 'empty' ? translate('shell.projectList.empty.title') : undefined}\n />\n ) : visible.length === 0 ? (\n <SectionStateView\n state=\"empty\"\n title={translate('shell.projectList.empty.title')}\n description={\n hasConfig\n ? 'The workspace config declares no visible projects. Add an entry to elytra.config.ts, or ask an admin to grant you access.'\n : 'No active projects. Create one from the CLI: run `builder init` in your repo to scaffold elytra.config.ts, then `builder init --project <slug>`.'\n }\n />\n ) : (\n <div className=\"grid gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {visible.map((entry) =>\n entry.kind === 'project' ? (\n <ProjectCard key={entry.project.id} project={entry.project} />\n ) : (\n <UnprovisionedCard key={`config:${entry.config.slug}`} config={entry.config} />\n ),\n )}\n </div>\n )}\n </div>\n )\n}\n\nfunction ProjectCard({ project }: { project: Project }) {\n const studio = useStudio()\n // Per-backend sign-in state (EC-179): one project per backend, so a project on\n // a deployment the user hasn't authenticated into shows \"Sign in\", not \"Open\".\n const authed = hasProjectSession(project.slug)\n return (\n <Card className=\"flex flex-col gap-3 p-4 transition-colors hover:border-primary/30\">\n <Row gap=\"2\">\n <span className=\"grid size-8 shrink-0 place-items-center rounded-lg bg-primary text-sm font-bold text-primary-foreground\">\n {project.name.charAt(0).toUpperCase()}\n </span>\n <Stack gap=\"1\" className=\"min-w-0\">\n <Text weight=\"semibold\" className=\"truncate\">\n {project.name}\n </Text>\n <Code tone=\"muted\" className=\"w-fit max-w-full truncate\">\n {project.slug}\n </Code>\n </Stack>\n <Spacer />\n {authed ? null : <SignInNeededBadge />}\n </Row>\n\n <Row gap=\"2\" className=\"text-muted-foreground\">\n <Badge variant=\"outline\">\n <Globe className=\"size-3\" /> {project.settings.locales.length}\n </Badge>\n <Spacer />\n </Row>\n\n <Button variant={authed ? 'secondary' : 'default'} size=\"sm\" asChild className=\"mt-1\">\n <Link\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId: project.id, _splat: studioPath.index() }}\n onClick={() => studio.activate(project.id)}\n >\n {authed ? translate('shell.projectCard.open') : (\n <>\n <LogIn /> {translate('auth.signIn')}\n </>\n )}\n </Link>\n </Button>\n </Card>\n )\n}\n\n/**\n * A configured project that has no backend project yet (EC-154): an explicit,\n * deterministic state pointing at the CLI provisioning flow — never an error.\n */\nfunction UnprovisionedCard({ config }: { config: ResolvedWorkspaceProject }) {\n return (\n <Card className=\"flex flex-col gap-3 border-dashed p-4\">\n <Row gap=\"2\">\n <span className=\"grid size-8 shrink-0 place-items-center rounded-lg border border-border bg-muted text-sm font-bold text-muted-foreground\">\n {config.name.charAt(0).toUpperCase()}\n </span>\n <Stack gap=\"1\" className=\"min-w-0\">\n <Text weight=\"semibold\" className=\"truncate\">\n {config.name}\n </Text>\n <Code tone=\"muted\" className=\"w-fit max-w-full truncate\">\n {config.slug}\n </Code>\n </Stack>\n <Spacer />\n <Badge variant=\"outline\">{translate('shell.project.notProvisioned')}</Badge>\n </Row>\n\n <Text size=\"sm\" tone=\"muted\">\n Declared in the workspace config, but no backend project exists yet. Provision it from your\n repo:\n </Text>\n <Code className=\"block w-fit max-w-full truncate text-xs\">\n builder init --project {config.slug}\n </Code>\n </Card>\n )\n}\n"]}
1
+ {"version":3,"file":"project-list.js","sourceRoot":"","sources":["../../src/components/project-list.tsx"],"names":[],"mappings":"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","sourcesContent":["import { Link, STUDIO_PROJECT_ROUTE, studioPath } from '@elytracms/studio/router'\nimport { Globe, LogIn, TerminalSquare } from 'lucide-react'\nimport type { Project, ResolvedWorkspaceProject, SectionState } from '@elytracms/core/studio-core'\nimport { Badge, Button, Card, Code, Heading, Row, Spacer, Stack, Text } from '@elytracms/studio/ui'\nimport { hasProjectSession } from '@elytracms/studio/lib/auth'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { PlaygroundBadge } from './playground-badge'\nimport { SignInNeededBadge } from './sign-in-needed-badge'\nimport { SectionStateView } from './section-state'\nimport { useStudio } from './studio-context'\nimport { useRecentProjects, useSwitcherEntries } from './workspace-projects'\n\nexport interface ProjectLauncherProps {\n /** Force a deterministic list state for browser verification (?state=…). */\n forcedState?: SectionState | null\n}\n\n/**\n * Studio launcher / project list (brief §4.3, §8.1; reframed by EC-154). The\n * Studio's front door: lists the workspace's projects and routes into a\n * project's shell. With a workspace config the list is config-driven\n * (configured projects ∩ grants, plus explicit \"not provisioned\" states);\n * project *creation* is code-first — `elytra.config.ts` + `builder init\n * --project <slug>` — so there is no in-UI create flow. Without a config the\n * adapter-backed list (and the starter-kit gallery) behaves as before.\n */\nexport function ProjectLauncher({ forcedState }: ProjectLauncherProps) {\n const studio = useStudio()\n const { entries, workspace } = useSwitcherEntries()\n\n const hasConfig = workspace.kind === 'config'\n const visible = entries\n const recents = useRecentProjects(entries)\n\n const forced = forcedState && forcedState !== 'working' ? forcedState : null\n\n return (\n <div className=\"mx-auto w-full max-w-6xl px-6 py-8\">\n <Row gap=\"3\" className=\"mb-6\">\n <Stack gap=\"1\">\n <Row gap=\"2\">\n <Heading size=\"display\">{translate('shell.projects')}</Heading>\n {/* EC-153: explicit playground-mode marker (renders nothing on Convex). */}\n <PlaygroundBadge />\n {hasConfig ? (\n // EC-154: this workspace is defined in code.\n <Badge variant=\"outline\">\n <TerminalSquare className=\"size-3\" /> {workspace.source}\n </Badge>\n ) : null}\n </Row>\n <Text tone=\"muted\">\n {hasConfig\n ? 'Projects are defined in code. Add entries to elytra.config.ts and provision them with the CLI.'\n : 'Open and manage your Elytra projects.'}\n </Text>\n </Stack>\n <Spacer />\n </Row>\n\n {recents.length > 0 && !forced ? (\n <Stack gap=\"2\" className=\"mb-6\">\n <Text size=\"xs\" tone=\"muted\" mono className=\"uppercase\">\n {translate('shell.projectList.recent')}\n </Text>\n <Row gap=\"2\" wrap>\n {recents.map((p) => (\n <Link\n key={p.id}\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId: p.id, _splat: studioPath.index() }}\n onClick={() => studio.activate(p.id)}\n className=\"inline-flex items-center gap-2 rounded-md border border-border bg-card px-3 py-1.5 text-sm transition-colors hover:border-primary/40 hover:bg-muted\"\n >\n <span className=\"grid size-4 place-items-center rounded-[4px] bg-primary text-[0.6rem] font-bold text-primary-foreground\">\n {p.name.charAt(0).toUpperCase()}\n </span>\n {p.name}\n </Link>\n ))}\n </Row>\n </Stack>\n ) : null}\n\n {forced ? (\n <SectionStateView\n state={forced}\n title={forced === 'empty' ? translate('shell.projectList.empty.title') : undefined}\n />\n ) : visible.length === 0 ? (\n <SectionStateView\n state=\"empty\"\n title={translate('shell.projectList.empty.title')}\n description={\n hasConfig\n ? 'The workspace config declares no visible projects. Add an entry to elytra.config.ts, or ask an admin to grant you access.'\n : 'No active projects. Create one from the CLI: run `builder init` in your repo to scaffold elytra.config.ts, then `builder init --project <slug>`.'\n }\n />\n ) : (\n <div className=\"grid gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {visible.map((entry) =>\n entry.kind === 'project' ? (\n <ProjectCard key={entry.project.id} project={entry.project} />\n ) : (\n <UnprovisionedCard key={`config:${entry.config.slug}`} config={entry.config} />\n ),\n )}\n </div>\n )}\n </div>\n )\n}\n\nfunction ProjectCard({ project }: { project: Project }) {\n const studio = useStudio()\n // Per-backend sign-in state (EC-179): one project per backend, so a project on\n // a deployment the user hasn't authenticated into shows \"Sign in\", not \"Open\".\n const authed = hasProjectSession(project.slug)\n return (\n <Card className=\"flex flex-col gap-3 p-4 transition-colors hover:border-primary/30\">\n <Row gap=\"2\">\n <span className=\"grid size-8 shrink-0 place-items-center rounded-lg bg-primary text-sm font-bold text-primary-foreground\">\n {project.name.charAt(0).toUpperCase()}\n </span>\n <Stack gap=\"1\" className=\"min-w-0\">\n <Text weight=\"semibold\" className=\"truncate\">\n {project.name}\n </Text>\n <Code tone=\"muted\" className=\"w-fit max-w-full truncate\">\n {project.slug}\n </Code>\n </Stack>\n <Spacer />\n {authed ? null : <SignInNeededBadge />}\n </Row>\n\n <Row gap=\"2\" className=\"text-muted-foreground\">\n <Badge variant=\"outline\">\n <Globe className=\"size-3\" /> {project.settings.locales.length}\n </Badge>\n <Spacer />\n </Row>\n\n <Button variant={authed ? 'secondary' : 'default'} size=\"sm\" asChild className=\"mt-1\">\n <Link\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId: project.id, _splat: studioPath.index() }}\n onClick={() => studio.activate(project.id)}\n >\n {authed ? translate('shell.projectCard.open') : (\n <>\n <LogIn /> {translate('auth.signIn')}\n </>\n )}\n </Link>\n </Button>\n </Card>\n )\n}\n\n/**\n * A configured project that has no backend project yet (EC-154): an explicit,\n * deterministic state pointing at the CLI provisioning flow — never an error.\n */\nfunction UnprovisionedCard({ config }: { config: ResolvedWorkspaceProject }) {\n return (\n <Card className=\"flex flex-col gap-3 border-dashed p-4\">\n <Row gap=\"2\">\n <span className=\"grid size-8 shrink-0 place-items-center rounded-lg border border-border bg-muted text-sm font-bold text-muted-foreground\">\n {config.name.charAt(0).toUpperCase()}\n </span>\n <Stack gap=\"1\" className=\"min-w-0\">\n <Text weight=\"semibold\" className=\"truncate\">\n {config.name}\n </Text>\n <Code tone=\"muted\" className=\"w-fit max-w-full truncate\">\n {config.slug}\n </Code>\n </Stack>\n <Spacer />\n <Badge variant=\"outline\">{translate('shell.project.notProvisioned')}</Badge>\n </Row>\n\n <Text size=\"sm\" tone=\"muted\">\n Declared in the workspace config, but no backend project exists yet. Provision it from your\n repo:\n </Text>\n <Code className=\"block w-fit max-w-full truncate text-xs\">\n builder init --project {config.slug}\n </Code>\n </Card>\n )\n}\n"]}
@@ -41,16 +41,20 @@ export interface ProjectSessionStore {
41
41
  */
42
42
  loadCms(): Promise<OperationResult<HydratedCms | null>>;
43
43
  }
44
+ declare const useProjectSession: () => ProjectSessionStore;
44
45
  export declare function ProjectSessionProvider({ projectId, children, }: {
45
46
  projectId: string;
46
47
  children: ReactNode;
47
48
  }): import("react").JSX.Element;
48
- export declare function useProjectSession(): ProjectSessionStore;
49
+ export { useProjectSession };
49
50
  /**
50
51
  * Gate one CMS surface mount on a FRESH persisted CMS load (Convex mode).
51
52
  * Render-prop: hands the hydrated content (or `null` → fixture fallback,
52
53
  * first-run) to the caller once it resolved. Playground renders synchronously
53
- * with `null`.
54
+ * with `null`. Unlike `ProjectSessionProvider`, this gate owns no
55
+ * `configureContentSession` side effect of its own — `loadCms` (from
56
+ * `useProjectSession`) already calls it internally when it resolves — so no
57
+ * `onResolved`/`enabled` is passed here.
54
58
  */
55
59
  export declare function CmsSessionGate({ children, }: {
56
60
  children: (cms: HydratedCms | null) => ReactNode;