@elytracms/studio 0.0.86 → 0.0.88
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/convex/asset-revalidate.test.ts +186 -0
- package/convex/assets.ts +53 -19
- package/convex/cms.ts +22 -0
- package/convex/delivery.ts +33 -116
- package/convex/functions.ts +3 -3
- package/convex/rail-write-parity.test.ts +180 -0
- package/convex/redirects.ts +33 -11
- package/convex/revalidate.ts +7 -5
- package/convex/triggers.ts +53 -3
- package/dist/components/ai/ai-chat-panel.js +8 -1
- package/dist/components/ai/ai-chat-panel.js.map +1 -1
- package/dist/components/app-views/default-modules.d.ts +20 -20
- package/dist/components/app-views/default-modules.js +27 -48
- package/dist/components/app-views/default-modules.js.map +1 -1
- package/dist/components/app-views/studio-config.d.ts +5 -6
- package/dist/components/app-views/studio-config.js.map +1 -1
- package/dist/components/assets/asset-metadata.d.ts +28 -19
- package/dist/components/assets/asset-metadata.js +24 -31
- package/dist/components/assets/asset-metadata.js.map +1 -1
- package/dist/components/assets/asset-selection.d.ts +24 -5
- package/dist/components/assets/asset-selection.js +39 -9
- package/dist/components/assets/asset-selection.js.map +1 -1
- package/dist/components/assets/assets-collection-view-source.d.ts +17 -14
- package/dist/components/assets/assets-collection-view-source.js +25 -31
- package/dist/components/assets/assets-collection-view-source.js.map +1 -1
- package/dist/components/assets/assets-store.d.ts +11 -1
- package/dist/components/assets/assets-store.js +10 -0
- package/dist/components/assets/assets-store.js.map +1 -1
- package/dist/components/assets/focal-point.d.ts +0 -7
- package/dist/components/assets/focal-point.js +0 -10
- package/dist/components/assets/focal-point.js.map +1 -1
- package/dist/components/builder/blocks-panel.js +3 -3
- package/dist/components/builder/blocks-panel.js.map +1 -1
- package/dist/components/builder/builder-store.d.ts +25 -1
- package/dist/components/builder/builder-store.js +3 -2
- package/dist/components/builder/builder-store.js.map +1 -1
- package/dist/components/builder/canvas-chrome-css.js +0 -17
- package/dist/components/builder/canvas-chrome-css.js.map +1 -1
- package/dist/components/builder/canvas-chrome.d.ts +10 -12
- package/dist/components/builder/canvas-chrome.js +12 -13
- package/dist/components/builder/canvas-chrome.js.map +1 -1
- package/dist/components/builder/canvas-dnd.d.ts +8 -0
- package/dist/components/builder/canvas-dnd.js +5 -3
- package/dist/components/builder/canvas-dnd.js.map +1 -1
- package/dist/components/builder/canvas-render-context.d.ts +23 -24
- package/dist/components/builder/canvas-render-context.js +35 -60
- package/dist/components/builder/canvas-render-context.js.map +1 -1
- package/dist/components/builder/canvas.js +5 -11
- package/dist/components/builder/canvas.js.map +1 -1
- package/dist/components/builder/clipboard-actions.d.ts +14 -6
- package/dist/components/builder/clipboard-actions.js +29 -20
- package/dist/components/builder/clipboard-actions.js.map +1 -1
- package/dist/components/builder/composition-field-editor.d.ts +19 -26
- package/dist/components/builder/composition-field-editor.js +30 -52
- package/dist/components/builder/composition-field-editor.js.map +1 -1
- package/dist/components/builder/insert-context.d.ts +3 -1
- package/dist/components/builder/insert-context.js +15 -10
- package/dist/components/builder/insert-context.js.map +1 -1
- package/dist/components/builder/page-tree.d.ts +29 -0
- package/dist/components/builder/page-tree.js +56 -58
- package/dist/components/builder/page-tree.js.map +1 -1
- package/dist/components/builder/reuse.js +5 -2
- package/dist/components/builder/reuse.js.map +1 -1
- package/dist/components/cms/asset-field.d.ts +2 -1
- package/dist/components/cms/asset-field.js +39 -83
- package/dist/components/cms/asset-field.js.map +1 -1
- package/dist/components/cms/asset-picker-core.d.ts +77 -40
- package/dist/components/cms/asset-picker-core.js +73 -46
- package/dist/components/cms/asset-picker-core.js.map +1 -1
- package/dist/components/cms/asset-picker.d.ts +48 -19
- package/dist/components/cms/asset-picker.js +69 -30
- package/dist/components/cms/asset-picker.js.map +1 -1
- package/dist/components/cms/cell-edit.d.ts +3 -1
- package/dist/components/cms/cell-edit.js +2 -2
- package/dist/components/cms/cell-edit.js.map +1 -1
- package/dist/components/cms/cms-composition-validators.js +11 -2
- package/dist/components/cms/cms-composition-validators.js.map +1 -1
- package/dist/components/cms/cms-store.js +15 -23
- package/dist/components/cms/cms-store.js.map +1 -1
- package/dist/components/cms/editable-cell.js +3 -3
- package/dist/components/cms/editable-cell.js.map +1 -1
- package/dist/components/cms/gate-validation.d.ts +27 -2
- package/dist/components/cms/gate-validation.js +19 -1
- package/dist/components/cms/gate-validation.js.map +1 -1
- package/dist/components/cms/rich-text/editor.js +2 -2
- package/dist/components/cms/rich-text/editor.js.map +1 -1
- package/dist/components/cms/rich-text/insert.d.ts +7 -0
- package/dist/components/cms/rich-text/insert.js +7 -0
- package/dist/components/cms/rich-text/insert.js.map +1 -1
- package/dist/components/cms/use-document-versions.js +1 -0
- package/dist/components/cms/use-document-versions.js.map +1 -1
- package/dist/components/cms/versions-model.d.ts +0 -12
- package/dist/components/cms/versions-model.js +3 -15
- package/dist/components/cms/versions-model.js.map +1 -1
- package/dist/components/components/component-detail.js +1 -1
- package/dist/components/components/component-detail.js.map +1 -1
- package/dist/components/components/transfer-dialog.js +2 -4
- package/dist/components/components/transfer-dialog.js.map +1 -1
- package/dist/components/components/transfer.d.ts +13 -10
- package/dist/components/components/transfer.js +21 -19
- package/dist/components/components/transfer.js.map +1 -1
- package/dist/components/nav-issues.d.ts +23 -3
- package/dist/components/nav-issues.js +38 -2
- package/dist/components/nav-issues.js.map +1 -1
- package/dist/components/project-session.d.ts +9 -0
- package/dist/components/project-session.js +1 -1
- package/dist/components/project-session.js.map +1 -1
- package/dist/components/redirects/redirects-collection-view-source.d.ts +17 -2
- package/dist/components/redirects/redirects-collection-view-source.js +20 -6
- package/dist/components/redirects/redirects-collection-view-source.js.map +1 -1
- package/dist/components/section-nav.js +26 -2
- package/dist/components/section-nav.js.map +1 -1
- package/dist/components/section-surface.js +3 -2
- package/dist/components/section-surface.js.map +1 -1
- package/dist/components/settings-form.js +1 -2
- package/dist/components/settings-form.js.map +1 -1
- package/dist/components/studio-context.js +1 -18
- package/dist/components/studio-context.js.map +1 -1
- package/dist/components/studio-project-screen.js +3 -5
- package/dist/components/studio-project-screen.js.map +1 -1
- package/dist/lib/ai/apply-content.js +73 -29
- package/dist/lib/ai/apply-content.js.map +1 -1
- package/dist/lib/ai/constraints.d.ts +35 -0
- package/dist/lib/ai/constraints.js +104 -4
- package/dist/lib/ai/constraints.js.map +1 -1
- package/dist/lib/ai/route.js +4 -1
- package/dist/lib/ai/route.js.map +1 -1
- package/dist/lib/ai/shape.d.ts +3 -2
- package/dist/lib/ai/shape.js +16 -24
- package/dist/lib/ai/shape.js.map +1 -1
- package/dist/lib/ai/tools.d.ts +10 -4
- package/dist/lib/ai/tools.js +12 -4
- package/dist/lib/ai/tools.js.map +1 -1
- package/dist/lib/ai/types.d.ts +8 -0
- package/dist/lib/ai/types.js.map +1 -1
- package/dist/lib/ai/update-fields.js +33 -12
- package/dist/lib/ai/update-fields.js.map +1 -1
- package/dist/lib/assets/asset-placements.d.ts +35 -0
- package/dist/lib/assets/asset-placements.js +74 -0
- package/dist/lib/assets/asset-placements.js.map +1 -0
- package/dist/lib/assets/backfill-thumbs.d.ts +9 -8
- package/dist/lib/assets/backfill-thumbs.js +13 -25
- package/dist/lib/assets/backfill-thumbs.js.map +1 -1
- package/dist/lib/clipboard/paste-issues.d.ts +17 -15
- package/dist/lib/clipboard/paste-issues.js +8 -45
- package/dist/lib/clipboard/paste-issues.js.map +1 -1
- package/dist/lib/clipboard/paste-target.d.ts +25 -1
- package/dist/lib/clipboard/paste-target.js +13 -6
- package/dist/lib/clipboard/paste-target.js.map +1 -1
- package/dist/lib/cms/published-documents.js +1 -0
- package/dist/lib/cms/published-documents.js.map +1 -1
- package/dist/lib/cms-views/view-model.d.ts +3 -3
- package/dist/lib/data/collection-content.d.ts +22 -1
- package/dist/lib/data/collection-content.js +15 -0
- package/dist/lib/data/collection-content.js.map +1 -1
- package/dist/lib/data/index.d.ts +0 -3
- package/dist/lib/data/index.js +4 -6
- package/dist/lib/data/index.js.map +1 -1
- package/dist/lib/dnd/drop-rules.d.ts +45 -2
- package/dist/lib/dnd/drop-rules.js +64 -9
- package/dist/lib/dnd/drop-rules.js.map +1 -1
- package/dist/lib/engine-version.d.ts +3 -3
- package/dist/lib/engine-version.js +3 -3
- package/dist/lib/engine-version.js.map +1 -1
- package/dist/lib/i18n/messages.de.js +9 -56
- package/dist/lib/i18n/messages.de.js.map +1 -1
- package/dist/lib/i18n/messages.en.d.ts +8 -53
- package/dist/lib/i18n/messages.en.js +14 -57
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/locale/fallback.js +8 -2
- package/dist/lib/locale/fallback.js.map +1 -1
- package/dist/lib/locale/index.d.ts +0 -1
- package/dist/lib/locale/index.js +0 -1
- package/dist/lib/locale/index.js.map +1 -1
- package/dist/lib/locale/locale-store.d.ts +10 -3
- package/dist/lib/locale/locale-store.js +12 -5
- package/dist/lib/locale/locale-store.js.map +1 -1
- package/dist/lib/operations/session-hydration.d.ts +24 -22
- package/dist/lib/operations/session-hydration.js +15 -53
- package/dist/lib/operations/session-hydration.js.map +1 -1
- package/dist/lib/persistence/convex-adapter.js +63 -75
- package/dist/lib/persistence/convex-adapter.js.map +1 -1
- package/dist/lib/persona/labels.d.ts +0 -2
- package/dist/lib/persona/labels.js +1 -7
- package/dist/lib/persona/labels.js.map +1 -1
- package/dist/lib/registry/index.d.ts +4 -2
- package/dist/lib/registry/index.js +26 -21
- package/dist/lib/registry/index.js.map +1 -1
- package/dist/lib/registry/project-components.js +0 -3
- package/dist/lib/registry/project-components.js.map +1 -1
- package/dist/lib/routes/route-issues.js +4 -5
- package/dist/lib/routes/route-issues.js.map +1 -1
- package/dist/lib/theme.js +3 -2
- package/dist/lib/theme.js.map +1 -1
- package/dist/lib/webhooks/tags.js +11 -14
- package/dist/lib/webhooks/tags.js.map +1 -1
- package/dist/lib/workspace/config-issues.d.ts +6 -0
- package/dist/lib/workspace/config-issues.js +8 -4
- package/dist/lib/workspace/config-issues.js.map +1 -1
- package/dist/lib/workspace/index.d.ts +2 -2
- package/dist/lib/workspace/index.js +2 -2
- package/dist/lib/workspace/index.js.map +1 -1
- package/dist/previews/enumerate.d.ts +27 -9
- package/dist/previews/enumerate.js +27 -6
- package/dist/previews/enumerate.js.map +1 -1
- package/dist/previews/manifest-format.d.ts +9 -0
- package/dist/previews/manifest-format.js.map +1 -1
- package/dist/router/match.d.ts +0 -2
- package/dist/router/match.js +0 -2
- package/dist/router/match.js.map +1 -1
- package/dist/router/navigate-once.d.ts +19 -4
- package/dist/router/navigate-once.js +22 -5
- package/dist/router/navigate-once.js.map +1 -1
- package/dist/ui/badge.d.ts +1 -1
- package/package.json +3 -3
- package/src/styles/studio.css +5 -4
- package/src/styles/tokens.css +72 -55
- package/convex/content.ts +0 -291
- package/dist/components/collab/collab-context.d.ts +0 -27
- package/dist/components/collab/collab-context.js +0 -39
- package/dist/components/collab/collab-context.js.map +0 -1
- package/dist/components/collab/collab-seed.d.ts +0 -15
- package/dist/components/collab/collab-seed.js +0 -20
- package/dist/components/collab/collab-seed.js.map +0 -1
- package/dist/components/extensions/extensions-view.d.ts +0 -7
- package/dist/components/extensions/extensions-view.js +0 -127
- package/dist/components/extensions/extensions-view.js.map +0 -1
- package/dist/lib/bindings/section-bindings.d.ts +0 -24
- package/dist/lib/bindings/section-bindings.js +0 -9
- package/dist/lib/bindings/section-bindings.js.map +0 -1
- package/dist/lib/data/data-store.d.ts +0 -98
- package/dist/lib/data/data-store.js +0 -211
- package/dist/lib/data/data-store.js.map +0 -1
- package/dist/lib/data/providers.d.ts +0 -143
- package/dist/lib/data/providers.js +0 -255
- package/dist/lib/data/providers.js.map +0 -1
- package/dist/lib/data/sample-capture.d.ts +0 -30
- package/dist/lib/data/sample-capture.js +0 -70
- package/dist/lib/data/sample-capture.js.map +0 -1
- package/dist/lib/data/sources-store.d.ts +0 -42
- package/dist/lib/data/sources-store.js +0 -120
- package/dist/lib/data/sources-store.js.map +0 -1
- package/dist/lib/locale/binding-fallback.d.ts +0 -35
- package/dist/lib/locale/binding-fallback.js +0 -73
- package/dist/lib/locale/binding-fallback.js.map +0 -1
- package/dist/lib/starters/kit-session.d.ts +0 -5
- package/dist/lib/starters/kit-session.js +0 -17
- package/dist/lib/starters/kit-session.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"navigate-once.js","sourceRoot":"","sources":["../../src/router/navigate-once.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useEffect, useLayoutEffect, useRef } from 'react'\nimport { useLocation, useNavigate, useRouter } from '@tanstack/react-router'\n\n/**\n * Render-time redirect that fires its navigation ONCE per redirect intent per\n * committed location (EC-441). The studio's declarative redirects (launcher\n * auto-forward, project index → landing view, auth-gate bounces, deep-link\n * normalizations) must use THIS instead of TanStack Router's `<Navigate>`.\n *\n * Why: `<Navigate>` re-fires `navigate(props)` whenever its props OBJECT\n * IDENTITY changes — and JSX builds a fresh props object on every parent\n * render. While the redirect's target transition is still pending (slow route\n * modules on a cold dev boot, slow chunks in production), the redirecting\n * component stays mounted and re-renders on every router-store tick; each\n * re-render re-navigates, each `navigate()` re-enters `commitLocation`/`load`\n * and schedules another transition state update, and the cycle compounds until\n * React throws \"Maximum update depth exceeded\" inside `commitLocation` — an\n * unhandled rejection of the router's load promise (the EC-439 finding: 104\n * launcher re-navigations on one cold playground boot).\n *\n * The decision core (`nextRedirectAttempt`) is pure and value-keyed:\n *\n * - already at the target → never fire (idempotence — covers the EC-179 keyed\n * `AuthProvider` remount, which recreates the redirecting component with\n * fresh refs while the router has already committed the target);\n * - one fire per intent per committed location — re-renders and store ticks\n * while the target transition is pending are inert;\n * - a NEW committed location re-arms the attempt, so navigating back to a URL\n * that redirects (e.g. the bare project index) redirects again.\n */\nexport interface NavigateOnceIntent {\n /** Target route path, addressed as a plain string (the router-seam contract). */\n to: string\n /** Route params (`{ projectId, _splat }` for the studio splat). */\n params?: Record<string, string>\n /** Search params for the target location. */\n search?: Record<string, unknown>\n /** Replace the current history entry (a redirect must not create one). */\n replace?: boolean\n}\n\n/**\n * Pure value identity of a redirect intent. Fresh-but-equal `params`/`search`\n * objects (the upstream failure mode: new object literal every render) map to\n * ONE key; top-level entries are order-normalized so key insertion order never\n * splits an intent. Values are JSON-serialized — the studio's redirect targets\n * are strings and flat search records, both deterministic under JSON.\n */\nexport function navigateIntentKey(intent: NavigateOnceIntent): string {\n const sorted = (record: Record<string, unknown> | undefined) =>\n record\n ? Object.entries(record).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))\n : null\n return JSON.stringify([\n intent.to,\n sorted(intent.params),\n sorted(intent.search),\n intent.replace === true,\n ])\n}\n\n/**\n * Href equality for \"is this redirect already committed?\", trailing-slash\n * trimmed on the path segment (matching the router's own same-URL comparison)\n * while search and hash stay significant.\n */\nexport function isSameRedirectTarget(targetHref: string, committedHref: string): boolean {\n const trim = (href: string) => {\n const [pathAndSearch = '', hash = ''] = href.split('#', 2)\n const [path = '', search = ''] = pathAndSearch.split('?', 2)\n const trimmedPath = path.length > 1 ? path.replace(/\\/+$/, '') : path\n return `${trimmedPath}${search ? `?${search}` : ''}${hash ? `#${hash}` : ''}`\n }\n return trim(targetHref) === trim(committedHref)\n}\n\n/**\n * Attempt bookkeeping for one mounted redirect: which intent keys have already\n * fired at the CURRENT committed location. A new committed href resets the set\n * (re-arm); the state is immutable so the caller can hold it in a ref.\n */\nexport interface RedirectAttemptState {\n committedHref: string\n firedKeys: ReadonlySet<string>\n}\n\n/**\n * The pure fire decision (EC-441's loop breaker, node-tested):\n *\n * 1. target already committed → no fire;\n * 2. this intent already fired at this committed location → no fire (the\n * per-render re-fire cycle, and any intent ping-pong at one location);\n * 3. otherwise → fire, and record the intent for this location.\n */\nexport function nextRedirectAttempt(\n state: RedirectAttemptState | null,\n input: { intentKey: string; targetHref: string; committedHref: string },\n): { fire: boolean; state: RedirectAttemptState } {\n const base: RedirectAttemptState =\n state && state.committedHref === input.committedHref\n ? state\n : { committedHref: input.committedHref, firedKeys: new Set<string>() }\n if (isSameRedirectTarget(input.targetHref, input.committedHref)) return { fire: false, state: base }\n if (base.firedKeys.has(input.intentKey)) return { fire: false, state: base }\n return {\n fire: true,\n state: {\n committedHref: base.committedHref,\n firedKeys: new Set(base.firedKeys).add(input.intentKey),\n },\n }\n}\n\n// Layout-effect timing on the client (start the redirect before paint, like\n// upstream `<Navigate>`); plain effect on the server, where layout effects warn\n// and redirects never run (the studio's redirect surfaces render null on SSR).\nconst useIsomorphicLayoutEffect = typeof document !== 'undefined' ? useLayoutEffect : useEffect\n\nexport function NavigateOnce({ to, params, search, replace }: NavigateOnceIntent) {\n const router = useRouter()\n const navigate = useNavigate()\n // The COMMITTED location (not transient transition state): effect deps stay\n // stable across the pending-transition re-renders that used to re-fire\n // `<Navigate>`, and a commit re-runs the decision exactly once.\n const committedHref = useLocation({ select: (location) => location.href })\n const key = navigateIntentKey({ to, params, search, replace })\n // Latest-value ref: the effect is keyed on the intent's VALUE (`key`), so it\n // reads the current props through a ref instead of listing unstable object\n // identities as dependencies.\n const intentRef = useRef<NavigateOnceIntent>({ to, params, search, replace })\n intentRef.current = { to, params, search, replace }\n const attemptRef = useRef<RedirectAttemptState | null>(null)\n\n useIsomorphicLayoutEffect(() => {\n const intent = intentRef.current\n const target = router.buildLocation({\n to: intent.to,\n params: intent.params,\n search: intent.search,\n })\n const decision = nextRedirectAttempt(attemptRef.current, {\n intentKey: key,\n targetHref: target.href,\n committedHref,\n })\n attemptRef.current = decision.state\n if (!decision.fire) return\n // Fire-and-forget, exactly like upstream `<Navigate>`: with the re-fire\n // cycle gone the transition settles normally, so nothing here swallows a\n // real navigation failure.\n void navigate({\n to: intent.to,\n params: intent.params,\n search: intent.search,\n replace: intent.replace,\n })\n }, [key, committedHref, navigate, router])\n\n return null\n}\n"]}
|
|
1
|
+
{"version":3,"file":"navigate-once.js","sourceRoot":"","sources":["../../src/router/navigate-once.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useEffect, useLayoutEffect, useRef } from 'react'\nimport { useLocation, useMatch, useNavigate, useRouter } from '@tanstack/react-router'\n\n/**\n * Render-time redirect that fires its navigation ONCE per redirect intent per\n * committed location (EC-441). The studio's declarative redirects (launcher\n * auto-forward, project index → landing view, auth-gate bounces, deep-link\n * normalizations) must use THIS instead of TanStack Router's `<Navigate>`.\n *\n * Why: `<Navigate>` re-fires `navigate(props)` whenever its props OBJECT\n * IDENTITY changes — and JSX builds a fresh props object on every parent\n * render. While the redirect's target transition is still pending (slow route\n * modules on a cold dev boot, slow chunks in production), the redirecting\n * component stays mounted and re-renders on every router-store tick; each\n * re-render re-navigates, each `navigate()` re-enters `commitLocation`/`load`\n * and schedules another transition state update, and the cycle compounds until\n * React throws \"Maximum update depth exceeded\" inside `commitLocation` — an\n * unhandled rejection of the router's load promise (the EC-439 finding: 104\n * launcher re-navigations on one cold playground boot).\n *\n * The decision core (`nextRedirectAttempt`) is pure and value-keyed:\n *\n * - already at the target → never fire (idempotence — covers the EC-179 keyed\n * `AuthProvider` remount, which recreates the redirecting component with\n * fresh refs while the router has already committed the target);\n * - one fire per intent per committed location — re-renders and store ticks\n * while the target transition is pending are inert;\n * - a NEW committed location re-arms the attempt, so navigating back to a URL\n * that redirects (e.g. the bare project index) redirects again;\n * - a redirect rendered by a route the committed location no longer matches is\n * inert (EC-611). While a transition is pending the router keeps rendering\n * the PREVIOUS match set, so a route component outlives its own URL: the\n * launcher (`/`) is still mounted at `/sign-in?redirect=/aurora` until the\n * sign-in module has loaded. Left to fire, its auto-forward and the auth\n * gate's sign-in bounce alternate, each commit re-arming the other — the\n * signed-out boot hang on `@tanstack/react-router` ≥ 1.170.41 (NK-050 F3).\n */\nexport interface NavigateOnceIntent {\n /** Target route path, addressed as a plain string (the router-seam contract). */\n to: string\n /** Route params (`{ projectId, _splat }` for the studio splat). */\n params?: Record<string, string>\n /** Search params for the target location. */\n search?: Record<string, unknown>\n /** Replace the current history entry (a redirect must not create one). */\n replace?: boolean\n}\n\n/**\n * Pure value identity of a redirect intent. Fresh-but-equal `params`/`search`\n * objects (the upstream failure mode: new object literal every render) map to\n * ONE key; top-level entries are order-normalized so key insertion order never\n * splits an intent. Values are JSON-serialized — the studio's redirect targets\n * are strings and flat search records, both deterministic under JSON.\n */\nexport function navigateIntentKey(intent: NavigateOnceIntent): string {\n const sorted = (record: Record<string, unknown> | undefined) =>\n record\n ? Object.entries(record).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))\n : null\n return JSON.stringify([\n intent.to,\n sorted(intent.params),\n sorted(intent.search),\n intent.replace === true,\n ])\n}\n\n/**\n * Href equality for \"is this redirect already committed?\", trailing-slash\n * trimmed on the path segment (matching the router's own same-URL comparison)\n * while search and hash stay significant.\n */\nexport function isSameRedirectTarget(targetHref: string, committedHref: string): boolean {\n const trim = (href: string) => {\n const [pathAndSearch = '', hash = ''] = href.split('#', 2)\n const [path = '', search = ''] = pathAndSearch.split('?', 2)\n const trimmedPath = path.length > 1 ? path.replace(/\\/+$/, '') : path\n return `${trimmedPath}${search ? `?${search}` : ''}${hash ? `#${hash}` : ''}`\n }\n return trim(targetHref) === trim(committedHref)\n}\n\n/**\n * Attempt bookkeeping for one mounted redirect: which intent keys have already\n * fired at the CURRENT committed location. A new committed href resets the set\n * (re-arm); the state is immutable so the caller can hold it in a ref.\n */\nexport interface RedirectAttemptState {\n committedHref: string\n firedKeys: ReadonlySet<string>\n}\n\n/**\n * The pure fire decision (EC-441's loop breaker, node-tested):\n *\n * 1. the rendering route is not matched by the committed location → no fire\n * (a stale render during a pending transition, EC-611);\n * 2. target already committed → no fire;\n * 3. this intent already fired at this committed location → no fire (the\n * per-render re-fire cycle, and any intent ping-pong at one location);\n * 4. otherwise → fire, and record the intent for this location.\n */\nexport function nextRedirectAttempt(\n state: RedirectAttemptState | null,\n input: {\n intentKey: string\n targetHref: string\n committedHref: string\n /**\n * Whether the route this redirect renders in is part of the committed\n * location's match set. Absent means \"not a route-scoped render\" (treated\n * as matched) so the decision stays usable from the root layout.\n */\n routeMatched?: boolean\n },\n): { fire: boolean; state: RedirectAttemptState } {\n const base: RedirectAttemptState =\n state && state.committedHref === input.committedHref\n ? state\n : { committedHref: input.committedHref, firedKeys: new Set<string>() }\n if (input.routeMatched === false) return { fire: false, state: base }\n if (isSameRedirectTarget(input.targetHref, input.committedHref)) return { fire: false, state: base }\n if (base.firedKeys.has(input.intentKey)) return { fire: false, state: base }\n return {\n fire: true,\n state: {\n committedHref: base.committedHref,\n firedKeys: new Set(base.firedKeys).add(input.intentKey),\n },\n }\n}\n\n// Layout-effect timing on the client (start the redirect before paint, like\n// upstream `<Navigate>`); plain effect on the server, where layout effects warn\n// and redirects never run (the studio's redirect surfaces render null on SSR).\nconst useIsomorphicLayoutEffect = typeof document !== 'undefined' ? useLayoutEffect : useEffect\n\nexport function NavigateOnce({ to, params, search, replace }: NavigateOnceIntent) {\n const router = useRouter()\n const navigate = useNavigate()\n // The COMMITTED location (not transient transition state): effect deps stay\n // stable across the pending-transition re-renders that used to re-fire\n // `<Navigate>`, and a commit re-runs the decision exactly once.\n const committedHref = useLocation({ select: (location) => location.href })\n // The route this redirect renders in (EC-611). `strict: false` resolves the\n // NEAREST match, so a redirect in the root layout (the auth gate) reads the\n // root route, which every location matches, and a redirect in a route\n // component reads that route — which the committed location stops matching\n // the moment a transition away from it starts.\n const routeId = useMatch({ strict: false, select: (match) => match.routeId }) as\n | string\n | undefined\n const key = navigateIntentKey({ to, params, search, replace })\n // Latest-value ref: the effect is keyed on the intent's VALUE (`key`), so it\n // reads the current props through a ref instead of listing unstable object\n // identities as dependencies.\n const intentRef = useRef<NavigateOnceIntent>({ to, params, search, replace })\n intentRef.current = { to, params, search, replace }\n const attemptRef = useRef<RedirectAttemptState | null>(null)\n\n useIsomorphicLayoutEffect(() => {\n const intent = intentRef.current\n const target = router.buildLocation({\n to: intent.to,\n params: intent.params,\n search: intent.search,\n })\n // Match the COMMITTED location against the route tree (no load, no state\n // change): is the route rendering this redirect still on the URL? During\n // a pending transition the previous match set keeps rendering, so this is\n // the one fact that tells a live redirect from a stale one.\n const routeMatched =\n routeId === undefined ||\n router.matchRoutes(router.state.location).some((match) => match.routeId === routeId)\n const decision = nextRedirectAttempt(attemptRef.current, {\n intentKey: key,\n targetHref: target.href,\n committedHref,\n routeMatched,\n })\n attemptRef.current = decision.state\n if (!decision.fire) return\n // Fire-and-forget, exactly like upstream `<Navigate>`: with the re-fire\n // cycle gone the transition settles normally, so nothing here swallows a\n // real navigation failure.\n void navigate({\n to: intent.to,\n params: intent.params,\n search: intent.search,\n replace: intent.replace,\n })\n }, [key, committedHref, navigate, router, routeId])\n\n return null\n}\n"]}
|
package/dist/ui/badge.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type VariantProps } from 'class-variance-authority';
|
|
2
2
|
import type { HTMLAttributes } from 'react';
|
|
3
3
|
export declare const badgeVariants: (props?: ({
|
|
4
|
-
variant?: "error" | "default" | "warning" | "
|
|
4
|
+
variant?: "error" | "default" | "warning" | "success" | "primary" | "outline" | "info" | null | undefined;
|
|
5
5
|
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
6
6
|
export interface BadgeProps extends HTMLAttributes<HTMLSpanElement>, VariantProps<typeof badgeVariants> {
|
|
7
7
|
}
|
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.
|
|
4
|
+
"version": "0.0.88",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=22.18.0"
|
|
7
7
|
},
|
|
@@ -153,7 +153,7 @@
|
|
|
153
153
|
"lucide-react": "^1.23.0",
|
|
154
154
|
"tailwind-merge": "^3.6.0",
|
|
155
155
|
"zod": "^4.4.3",
|
|
156
|
-
"@elytracms/core": "^0.0.
|
|
156
|
+
"@elytracms/core": "^0.0.88"
|
|
157
157
|
},
|
|
158
158
|
"peerDependencies": {
|
|
159
159
|
"@tanstack/react-router": "^1.170.0",
|
|
@@ -168,7 +168,7 @@
|
|
|
168
168
|
}
|
|
169
169
|
},
|
|
170
170
|
"devDependencies": {
|
|
171
|
-
"@tanstack/react-router": "^1.170.
|
|
171
|
+
"@tanstack/react-router": "^1.170.41",
|
|
172
172
|
"@types/node": "^26.1.0",
|
|
173
173
|
"@types/react": "^19.2.17",
|
|
174
174
|
"@types/react-dom": "^19.2.3",
|
package/src/styles/studio.css
CHANGED
|
@@ -17,10 +17,11 @@
|
|
|
17
17
|
@import '@fontsource-variable/hanken-grotesk';
|
|
18
18
|
@import '@fontsource-variable/jetbrains-mono';
|
|
19
19
|
/*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* Cool-slate neutrals + a confident
|
|
23
|
-
* token (component ids, path tokens,
|
|
20
|
+
* The colour roles are declared once, hand-written, in tokens.css (EC-607); the
|
|
21
|
+
* `@theme inline` block below maps each to its Tailwind colour and is the one
|
|
22
|
+
* declaration of the fonts and the radius scale. Cool-slate neutrals + a confident
|
|
23
|
+
* cobalt accent; monospace carries every technical token (component ids, path tokens,
|
|
24
|
+
* issue codes) to reinforce the code-first identity.
|
|
24
25
|
*/
|
|
25
26
|
@import './tokens.css';
|
|
26
27
|
|
package/src/styles/tokens.css
CHANGED
|
@@ -1,63 +1,80 @@
|
|
|
1
|
-
/*
|
|
1
|
+
/*
|
|
2
|
+
* Studio colour tokens: the one declaration (EC-607). Hand-written, so edit here.
|
|
3
|
+
*
|
|
4
|
+
* Each role is declared ONCE as `light-dark(<light>, <dark>)`, so a role cannot
|
|
5
|
+
* exist without both values. The `dark` class on <html> (lib/theme.ts) only flips
|
|
6
|
+
* `color-scheme`, which picks the value; it also gives native controls and
|
|
7
|
+
* scrollbars the matching scheme. A role that tracks another is a plain `var()`.
|
|
8
|
+
*
|
|
9
|
+
* Components never use raw colours: studio.css maps each role to a Tailwind colour
|
|
10
|
+
* (`bg-primary`, `text-muted-foreground`, …) and the in-canvas chrome uses no var()
|
|
11
|
+
* at all (canvas-chrome-css.ts). A new role is one line here plus its `@theme inline`
|
|
12
|
+
* line in studio.css; lib/theme-tokens-contract.test.ts fails if a mapped role is missing.
|
|
13
|
+
*
|
|
14
|
+
* Needs light-dark(): Chrome 123, Safari 17.5, Firefox 120.
|
|
15
|
+
*/
|
|
2
16
|
:root {
|
|
3
|
-
|
|
4
|
-
--
|
|
5
|
-
|
|
17
|
+
color-scheme: light;
|
|
18
|
+
--radius: 0.5rem;
|
|
19
|
+
|
|
20
|
+
/* App canvas background */
|
|
21
|
+
--background: light-dark(oklch(0.985 0.002 250), oklch(0.176 0.012 264));
|
|
22
|
+
/* Primary text */
|
|
23
|
+
--foreground: light-dark(oklch(0.21 0.02 264), oklch(0.95 0.005 250));
|
|
24
|
+
/* Card surface */
|
|
25
|
+
--card: light-dark(oklch(1 0 0), oklch(0.205 0.013 264));
|
|
26
|
+
/* Text on cards */
|
|
6
27
|
--card-foreground: var(--foreground);
|
|
7
|
-
|
|
28
|
+
/* Inspector / rail surface */
|
|
29
|
+
--panel: light-dark(oklch(0.974 0.003 255), oklch(0.196 0.013 264));
|
|
30
|
+
/* Text on panels */
|
|
8
31
|
--panel-foreground: var(--foreground);
|
|
9
|
-
|
|
32
|
+
/* Floating surface */
|
|
33
|
+
--popover: light-dark(oklch(1 0 0), oklch(0.214 0.013 264));
|
|
34
|
+
/* Text on popovers */
|
|
10
35
|
--popover-foreground: var(--foreground);
|
|
11
|
-
|
|
12
|
-
--muted
|
|
13
|
-
|
|
14
|
-
--
|
|
15
|
-
|
|
16
|
-
--
|
|
17
|
-
|
|
18
|
-
--
|
|
19
|
-
|
|
20
|
-
--
|
|
21
|
-
|
|
22
|
-
--
|
|
23
|
-
|
|
24
|
-
--
|
|
25
|
-
|
|
26
|
-
--
|
|
27
|
-
|
|
28
|
-
--
|
|
29
|
-
|
|
30
|
-
--
|
|
31
|
-
|
|
36
|
+
/* Subtle fill */
|
|
37
|
+
--muted: light-dark(oklch(0.955 0.004 255), oklch(0.256 0.013 264));
|
|
38
|
+
/* Secondary text */
|
|
39
|
+
--muted-foreground: light-dark(oklch(0.5 0.018 260), oklch(0.66 0.02 258));
|
|
40
|
+
/* Hairline borders */
|
|
41
|
+
--border: light-dark(oklch(0.908 0.005 260), oklch(0.278 0.013 264));
|
|
42
|
+
/* Form control borders */
|
|
43
|
+
--input: light-dark(oklch(0.908 0.005 260), oklch(0.3 0.013 264));
|
|
44
|
+
/* Focus ring */
|
|
45
|
+
--ring: light-dark(oklch(0.55 0.17 256), oklch(0.66 0.16 256));
|
|
46
|
+
/* Primary action (cobalt) */
|
|
47
|
+
--primary: light-dark(oklch(0.55 0.17 256), oklch(0.66 0.16 256));
|
|
48
|
+
/* Text on primary */
|
|
49
|
+
--primary-foreground: light-dark(oklch(0.99 0.005 250), oklch(0.16 0.02 264));
|
|
50
|
+
/* Secondary action */
|
|
51
|
+
--secondary: light-dark(oklch(0.955 0.004 255), oklch(0.256 0.013 264));
|
|
52
|
+
/* Text on secondary */
|
|
53
|
+
--secondary-foreground: light-dark(oklch(0.3 0.02 264), oklch(0.92 0.005 250));
|
|
54
|
+
/* Selected / emphasis fill */
|
|
55
|
+
--accent: light-dark(oklch(0.95 0.022 256), oklch(0.288 0.03 256));
|
|
56
|
+
/* Text on accent */
|
|
57
|
+
--accent-foreground: light-dark(oklch(0.36 0.07 256), oklch(0.88 0.03 256));
|
|
58
|
+
/* Success / valid */
|
|
59
|
+
--success: light-dark(oklch(0.58 0.13 150), oklch(0.7 0.14 150));
|
|
60
|
+
/* Text on success */
|
|
61
|
+
--success-foreground: light-dark(oklch(0.99 0.01 150), oklch(0.16 0.03 150));
|
|
62
|
+
/* Warning */
|
|
63
|
+
--warning: light-dark(oklch(0.68 0.14 70), oklch(0.79 0.14 75));
|
|
64
|
+
/* Text on warning */
|
|
65
|
+
--warning-foreground: light-dark(oklch(0.24 0.04 70), oklch(0.2 0.04 75));
|
|
66
|
+
/* Error / destructive */
|
|
67
|
+
--destructive: light-dark(oklch(0.57 0.2 27), oklch(0.67 0.18 25));
|
|
68
|
+
/* Text on destructive */
|
|
69
|
+
--destructive-foreground: light-dark(oklch(0.99 0.01 27), oklch(0.16 0.03 25));
|
|
70
|
+
/* Informational / bound */
|
|
71
|
+
--info: light-dark(oklch(0.56 0.12 248), oklch(0.7 0.13 245));
|
|
72
|
+
/* Text on info */
|
|
73
|
+
--info-foreground: light-dark(oklch(0.99 0.01 248), oklch(0.16 0.03 245));
|
|
74
|
+
/* Canvas grid lines */
|
|
75
|
+
--grid-line: light-dark(oklch(0.8 0.01 260 / 0.5), oklch(0.5 0.012 264 / 0.32));
|
|
32
76
|
}
|
|
33
77
|
|
|
34
78
|
.dark {
|
|
35
|
-
|
|
36
|
-
--foreground: oklch(0.95 0.005 250);
|
|
37
|
-
--card: oklch(0.205 0.013 264);
|
|
38
|
-
--card-foreground: var(--foreground);
|
|
39
|
-
--panel: oklch(0.196 0.013 264);
|
|
40
|
-
--panel-foreground: var(--foreground);
|
|
41
|
-
--popover: oklch(0.214 0.013 264);
|
|
42
|
-
--popover-foreground: var(--foreground);
|
|
43
|
-
--muted: oklch(0.256 0.013 264);
|
|
44
|
-
--muted-foreground: oklch(0.66 0.02 258);
|
|
45
|
-
--border: oklch(0.278 0.013 264);
|
|
46
|
-
--input: oklch(0.3 0.013 264);
|
|
47
|
-
--ring: oklch(0.66 0.16 256);
|
|
48
|
-
--primary: oklch(0.66 0.16 256);
|
|
49
|
-
--primary-foreground: oklch(0.16 0.02 264);
|
|
50
|
-
--secondary: oklch(0.256 0.013 264);
|
|
51
|
-
--secondary-foreground: oklch(0.92 0.005 250);
|
|
52
|
-
--accent: oklch(0.288 0.03 256);
|
|
53
|
-
--accent-foreground: oklch(0.88 0.03 256);
|
|
54
|
-
--success: oklch(0.7 0.14 150);
|
|
55
|
-
--success-foreground: oklch(0.16 0.03 150);
|
|
56
|
-
--warning: oklch(0.79 0.14 75);
|
|
57
|
-
--warning-foreground: oklch(0.2 0.04 75);
|
|
58
|
-
--destructive: oklch(0.67 0.18 25);
|
|
59
|
-
--destructive-foreground: oklch(0.16 0.03 25);
|
|
60
|
-
--info: oklch(0.7 0.13 245);
|
|
61
|
-
--info-foreground: oklch(0.16 0.03 245);
|
|
62
|
-
--grid-line: oklch(0.5 0.012 264 / 0.32);
|
|
79
|
+
color-scheme: dark;
|
|
63
80
|
}
|
package/convex/content.ts
DELETED
|
@@ -1,291 +0,0 @@
|
|
|
1
|
-
import { documentKey, documentSchema, unfoldValue } from '@elytracms/core/cms-core'
|
|
2
|
-
import type {
|
|
3
|
-
CmsDocument,
|
|
4
|
-
CmsValidationIssue,
|
|
5
|
-
CollectionDef,
|
|
6
|
-
Locale,
|
|
7
|
-
LocaleConfig,
|
|
8
|
-
RedirectRecord,
|
|
9
|
-
RouteRecord,
|
|
10
|
-
} from '@elytracms/core/cms-core'
|
|
11
|
-
import { createContentClient, createContentContext, isDocumentHistory, rawDocument } from '@elytracms/core/content'
|
|
12
|
-
import type {
|
|
13
|
-
ContentClient,
|
|
14
|
-
ContentContext,
|
|
15
|
-
ContentDocumentSource,
|
|
16
|
-
ContentLookup,
|
|
17
|
-
Perspective,
|
|
18
|
-
} from '@elytracms/core/content'
|
|
19
|
-
import type { AssetRecord } from '@elytracms/core/persistence'
|
|
20
|
-
import type { QueryCtx } from './functions'
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* EC-145 — the Convex-side content kit (vision AD-3/AD-4).
|
|
24
|
-
*
|
|
25
|
-
* Custom Convex functions are the query escape hatch ("our query language is
|
|
26
|
-
* TypeScript") — but only if developers never touch raw tables. `createContent`
|
|
27
|
-
* gives a Convex query function the same delivery surface as the Next-side
|
|
28
|
-
* accessors: `get`, `list`, `resolvePage`, plus the `raw` escape hatch, all
|
|
29
|
-
* with ambient `{ locale, perspective }` context.
|
|
30
|
-
*
|
|
31
|
-
* Byte-equivalence with the Next side is guaranteed by construction: the kit
|
|
32
|
-
* loads rows through `ctx.db`, builds the same synchronous `ContentLookup`
|
|
33
|
-
* that `@elytracms/next`'s snapshot builds from a `PersistenceAdapter`, and then
|
|
34
|
-
* runs the EXACT pure functions from `@elytracms/core/content`
|
|
35
|
-
* (`createContentClient` → `resolveDocument`, `query.ts` validation, cache
|
|
36
|
-
* tags). Nothing about resolution semantics is reimplemented here.
|
|
37
|
-
*
|
|
38
|
-
* Loading strategy (honest about v1 scale): the project's schema, documents,
|
|
39
|
-
* and asset metadata are prefetched ONCE per `createContent` call via the
|
|
40
|
-
* indexed `by_project` / `by_project_asset` scans, then every kit call is
|
|
41
|
-
* synchronous over memory — exactly like `createContentSnapshot` on the Next
|
|
42
|
-
* side. Filters and sorts therefore execute in-function over an indexed
|
|
43
|
-
* by-project fetch; see `contentIndexes.ts` for the index requirements this
|
|
44
|
-
* implies and the documented field-level-index upgrade path.
|
|
45
|
-
*
|
|
46
|
-
* Perspective isolation: the lookup holds the raw stored rows (draft state and
|
|
47
|
-
* all), and EVERY read path through `@elytracms/core/content` — `resolveDocument`,
|
|
48
|
-
* relation population, `listDocuments` filtering, slug fallback, and the `raw`
|
|
49
|
-
* escape hatch — applies the EC-142 perspective gate (`selectPerspective`)
|
|
50
|
-
* before any value is exposed. The kit never reads field values around that
|
|
51
|
-
* gate, so a `published` kit cannot observe draft values. (Pre-filtering draft
|
|
52
|
-
* rows out of the lookup would be redundant *and* would change structured
|
|
53
|
-
* issue messages — e.g. `unknown-relation-target` reasons — breaking
|
|
54
|
-
* byte-equivalence with the Next-side accessors.)
|
|
55
|
-
*/
|
|
56
|
-
|
|
57
|
-
const DEFAULT_LOCALES: LocaleConfig = { default: 'en', locales: ['en'] }
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Structured load-time problems (stored blobs that fail their canonical Zod
|
|
61
|
-
* schema, unparsable graph revisions). Never thrown — the kit degrades to the
|
|
62
|
-
* valid subset and reports what it skipped, mirroring the package-wide
|
|
63
|
-
* "structured issues, never crashes" stance.
|
|
64
|
-
*
|
|
65
|
-
* `invalid-stored-schema` is retained for wire/back-compat but is no longer
|
|
66
|
-
* produced: CMS collections are config-owned (EC-183, AD-11) and supplied by the
|
|
67
|
-
* caller (already validated), so there is no stored schema blob to reject.
|
|
68
|
-
*/
|
|
69
|
-
export type ContentKitIssueCode = 'invalid-stored-schema' | 'invalid-stored-document'
|
|
70
|
-
|
|
71
|
-
export interface ContentKitIssue {
|
|
72
|
-
code: ContentKitIssueCode
|
|
73
|
-
message: string
|
|
74
|
-
meta?: Record<string, unknown>
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export interface CreateContentOptions {
|
|
78
|
-
projectId: string
|
|
79
|
-
/** Draft or published perspective (EC-021); ambient for every kit call. */
|
|
80
|
-
perspective: Perspective
|
|
81
|
-
/** Requested locale; defaults to the project's default locale. */
|
|
82
|
-
locale?: Locale
|
|
83
|
-
/** Override the locale config derived from the project settings. */
|
|
84
|
-
locales?: LocaleConfig
|
|
85
|
-
/**
|
|
86
|
-
* CMS collections (EC-183, AD-11), sourced from `elytra.config.ts` by the
|
|
87
|
-
* caller — the backend no longer stores a schema. Defaults to none (the kit
|
|
88
|
-
* then resolves documents with no collection structure).
|
|
89
|
-
*/
|
|
90
|
-
collections?: readonly CollectionDef[]
|
|
91
|
-
/** Route records served by `resolvePage` (EC-019). */
|
|
92
|
-
routes?: readonly RouteRecord[]
|
|
93
|
-
// Redirects are no longer an option (EC-303): they are editorial DATA, read from
|
|
94
|
-
// the `redirects` table inside `createContent`, not passed by the caller.
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
export interface ContentKit {
|
|
98
|
-
/** The ambient `{ locale, perspective }` context every call resolves under. */
|
|
99
|
-
readonly context: ContentContext
|
|
100
|
-
/** Structured load-time issues (invalid stored blobs); never thrown. */
|
|
101
|
-
readonly loadIssues: readonly ContentKitIssue[]
|
|
102
|
-
/** Structural route issues from router construction (conflicts, loops). */
|
|
103
|
-
readonly routerIssues: readonly CmsValidationIssue[]
|
|
104
|
-
/** Delivery-shaped single-document accessor — same semantics as Next-side `getDocument`. */
|
|
105
|
-
readonly get: ContentClient['getDocument']
|
|
106
|
-
/** Delivery-shaped list accessor — same filter vocabulary as EC-143. */
|
|
107
|
-
readonly list: ContentClient['listDocuments']
|
|
108
|
-
/** Route → page/document resolution (EC-019), when routes were provided. */
|
|
109
|
-
readonly resolvePage: ContentClient['resolvePage']
|
|
110
|
-
/**
|
|
111
|
-
* Escape hatch (EC-142 `rawDocument`): the stored row, untouched — no locale
|
|
112
|
-
* fallback, no population, no asset resolution. The perspective gate still
|
|
113
|
-
* applies: a `published` kit never sees draft rows, even raw.
|
|
114
|
-
*/
|
|
115
|
-
raw(collection: string, id: string): CmsDocument | null
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Create the server-side content kit for one Convex query invocation.
|
|
120
|
-
*
|
|
121
|
-
* Usage inside a Convex query function:
|
|
122
|
-
* ```ts
|
|
123
|
-
* const content = await createContent(ctx, { projectId, perspective: 'published' })
|
|
124
|
-
* const { document: post } = content.get<Post>('post', slug)
|
|
125
|
-
* ```
|
|
126
|
-
*/
|
|
127
|
-
export async function createContent(
|
|
128
|
-
ctx: QueryCtx,
|
|
129
|
-
options: CreateContentOptions,
|
|
130
|
-
): Promise<ContentKit> {
|
|
131
|
-
const { projectId, perspective } = options
|
|
132
|
-
const routes = options.routes ?? []
|
|
133
|
-
const loadIssues: ContentKitIssue[] = []
|
|
134
|
-
|
|
135
|
-
// Redirects are editorial DATA now (EC-303), read from the `redirects` table —
|
|
136
|
-
// only ENABLED entries resolve. (Config redirects are retired; static infra
|
|
137
|
-
// redirects stay native.) Projected to the routing `RedirectRecord` shape.
|
|
138
|
-
const redirectRows = await ctx.db.query('redirects').withIndex('by_redirect').collect()
|
|
139
|
-
const redirects: RedirectRecord[] = redirectRows
|
|
140
|
-
.filter((row) => row.enabled)
|
|
141
|
-
.map((row) => ({ id: row.redirectId, from: row.from, to: row.to, permanent: row.permanent }))
|
|
142
|
-
|
|
143
|
-
// --- Prefetch (the only async part; everything below is synchronous) -----
|
|
144
|
-
|
|
145
|
-
// Config-first identity (EC-182/EC-183, AD-11): locales AND collections are
|
|
146
|
-
// supplied by the caller (sourced from `elytra.config.ts`); the backend has
|
|
147
|
-
// no `projects` row and no `cmsSchemas` table — it stores documents only.
|
|
148
|
-
const locales = options.locales ?? DEFAULT_LOCALES
|
|
149
|
-
const collectionDefs: CollectionDef[] = options.collections ? [...options.collections] : []
|
|
150
|
-
|
|
151
|
-
// One indexed scan over the project's documents (v1 scale; see module doc).
|
|
152
|
-
// A row's `document` is the continuous working **draft**; `publishedVersion`
|
|
153
|
-
// (when set) pins which recorded version is live — the Sanity model (EC-224).
|
|
154
|
-
const documentRows = await ctx.db.query('cmsDocuments').collect()
|
|
155
|
-
|
|
156
|
-
// The recorded version snapshots, indexed by (collection, docId, version), so
|
|
157
|
-
// a published read can serve the *pinned* version rather than the live draft.
|
|
158
|
-
const versionsByKey = new Map<string, Map<number, CmsDocument>>()
|
|
159
|
-
for (const row of await ctx.db.query('documentVersions').collect()) {
|
|
160
|
-
// EC-260: unfold the folded storage form before parsing — the kit resolves over
|
|
161
|
-
// STRUCTURED values, exactly like the Next-side snapshot (byte-equivalence).
|
|
162
|
-
const parsed = documentSchema.safeParse(unfoldValue(row.snapshot))
|
|
163
|
-
if (!parsed.success) continue
|
|
164
|
-
const key = `${row.collection}:${row.docId}`
|
|
165
|
-
let byVersion = versionsByKey.get(key)
|
|
166
|
-
if (!byVersion) {
|
|
167
|
-
byVersion = new Map()
|
|
168
|
-
versionsByKey.set(key, byVersion)
|
|
169
|
-
}
|
|
170
|
-
byVersion.set(row.version, parsed.data)
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// Hybrid source per row (EC-224 A1a): a `publishedVersion` pointer yields a
|
|
174
|
-
// `DocumentHistory` (draft = the live row, published = the pinned snapshot),
|
|
175
|
-
// so the published perspective serves the recorded version, decoupled from
|
|
176
|
-
// the continuously-edited draft. An unpinned row stays a bare `CmsDocument`
|
|
177
|
-
// (the EC-142 gate reads its `state`) — backward-compatible: pre-pointer data
|
|
178
|
-
// resolves exactly as before.
|
|
179
|
-
const parsedSources: { key: string; source: ContentDocumentSource }[] = []
|
|
180
|
-
for (const row of documentRows) {
|
|
181
|
-
// EC-260: unfold the folded storage form before parsing (see the versions loop).
|
|
182
|
-
const parsed = documentSchema.safeParse(unfoldValue(row.document))
|
|
183
|
-
if (!parsed.success) {
|
|
184
|
-
loadIssues.push({
|
|
185
|
-
code: 'invalid-stored-document',
|
|
186
|
-
message: `Stored document "${row.collection}:${row.docId}" does not match the canonical document schema and was skipped.`,
|
|
187
|
-
meta: { projectId, collection: row.collection, docId: row.docId },
|
|
188
|
-
})
|
|
189
|
-
continue
|
|
190
|
-
}
|
|
191
|
-
const draft = parsed.data
|
|
192
|
-
const key = documentKey(draft)
|
|
193
|
-
if (row.publishedVersion === undefined) {
|
|
194
|
-
parsedSources.push({ key, source: draft })
|
|
195
|
-
continue
|
|
196
|
-
}
|
|
197
|
-
// Pinned: serve the recorded snapshot for `published`. A dangling pin
|
|
198
|
-
// (version missing/invalid) under-serves to `null` — never the live draft.
|
|
199
|
-
const published = versionsByKey.get(`${row.collection}:${row.docId}`)?.get(row.publishedVersion)
|
|
200
|
-
parsedSources.push({ key, source: { draft, published, versions: [] } })
|
|
201
|
-
}
|
|
202
|
-
// Deterministic iteration order regardless of row creation order.
|
|
203
|
-
parsedSources.sort((a, b) => (a.key < b.key ? -1 : 1))
|
|
204
|
-
|
|
205
|
-
const assetRows = await ctx.db.query('assets').withIndex('by_asset').collect()
|
|
206
|
-
const assetRecords: AssetRecord[] = []
|
|
207
|
-
for (const row of assetRows) {
|
|
208
|
-
// EC-151 stores a stable serve URL on upload; older rows fall back to a
|
|
209
|
-
// live storage lookup, then to the blob adapter's placeholder convention.
|
|
210
|
-
const url = row.url ?? (await ctx.storage.getUrl(row.storageKey)) ?? undefined
|
|
211
|
-
assetRecords.push({
|
|
212
|
-
id: row.assetId,
|
|
213
|
-
// Single-tenant: rows no longer store projectId; the kit's `projectId`
|
|
214
|
-
// option (the constant slug) satisfies the AssetRecord shape (EC-179).
|
|
215
|
-
projectId,
|
|
216
|
-
filename: row.filename,
|
|
217
|
-
contentType: row.contentType,
|
|
218
|
-
byteSize: row.byteSize,
|
|
219
|
-
...(row.width !== undefined ? { width: row.width } : {}),
|
|
220
|
-
...(row.height !== undefined ? { height: row.height } : {}),
|
|
221
|
-
...(row.alt !== undefined ? { alt: row.alt } : {}),
|
|
222
|
-
// EC-301: focalPoint (EC-230) and the schema-driven metadata bag must reach
|
|
223
|
-
// delivery — both were dropped here, so a studio-set hotspot/metadata never
|
|
224
|
-
// made it into the LIVE snapshot (only the studio canvas, which reads records
|
|
225
|
-
// directly, showed them).
|
|
226
|
-
...(row.focalPoint !== undefined ? { focalPoint: row.focalPoint } : {}),
|
|
227
|
-
...(row.meta !== undefined ? { meta: row.meta } : {}),
|
|
228
|
-
storageKey: row.storageKey,
|
|
229
|
-
...(url !== undefined ? { url } : {}),
|
|
230
|
-
createdAt: row.createdAt,
|
|
231
|
-
updatedAt: row.updatedAt,
|
|
232
|
-
})
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// EC-287: there is no project graph. Pages are `page`-collection documents,
|
|
236
|
-
// resolved by `resolvePage` from the document lookup + routes — no graph load.
|
|
237
|
-
|
|
238
|
-
// --- Index into the synchronous lookup the pure accessors read through ---
|
|
239
|
-
|
|
240
|
-
const collections = new Map<string, CollectionDef>()
|
|
241
|
-
for (const collection of collectionDefs) collections.set(collection.id, collection)
|
|
242
|
-
|
|
243
|
-
const documents = new Map<string, ContentDocumentSource>()
|
|
244
|
-
const byCollection = new Map<string, ContentDocumentSource[]>()
|
|
245
|
-
for (const { key, source } of parsedSources) {
|
|
246
|
-
documents.set(key, source)
|
|
247
|
-
const collection = isDocumentHistory(source) ? source.draft.collection : source.collection
|
|
248
|
-
const list = byCollection.get(collection) ?? []
|
|
249
|
-
list.push(source)
|
|
250
|
-
byCollection.set(collection, list)
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
const assets = new Map<string, AssetRecord>()
|
|
254
|
-
for (const record of assetRecords) assets.set(record.id, record)
|
|
255
|
-
|
|
256
|
-
const lookup: ContentLookup = {
|
|
257
|
-
collection: (id) => collections.get(id),
|
|
258
|
-
document: (ref) => documents.get(documentKey(ref)),
|
|
259
|
-
documents: (collectionId) => byCollection.get(collectionId) ?? [],
|
|
260
|
-
asset: (id) => assets.get(id),
|
|
261
|
-
// Same convention as `ConvexBlobStorage.url`: stored serve URL when
|
|
262
|
-
// available, otherwise the explicit unresolved-placeholder scheme.
|
|
263
|
-
assetUrl: (asset) => asset.url ?? `convex://storage/${asset.storageKey}`,
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
const context = createContentContext({
|
|
267
|
-
locale: options.locale ?? locales.default,
|
|
268
|
-
perspective,
|
|
269
|
-
locales,
|
|
270
|
-
})
|
|
271
|
-
|
|
272
|
-
// The EXACT pure client the Next-side accessors run — this is what makes
|
|
273
|
-
// results byte-equivalent across runtimes for the same inputs.
|
|
274
|
-
const client = createContentClient({ lookup, context, routes, redirects })
|
|
275
|
-
|
|
276
|
-
const raw = (collection: string, id: string): CmsDocument | null => {
|
|
277
|
-
const source = documents.get(documentKey({ collection, id }))
|
|
278
|
-
if (!source) return null
|
|
279
|
-
return rawDocument(context, source)
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
return {
|
|
283
|
-
context,
|
|
284
|
-
loadIssues,
|
|
285
|
-
routerIssues: client.routerIssues,
|
|
286
|
-
get: client.getDocument,
|
|
287
|
-
list: client.listDocuments,
|
|
288
|
-
resolvePage: client.resolvePage,
|
|
289
|
-
raw,
|
|
290
|
-
}
|
|
291
|
-
}
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import type { ReactNode } from 'react';
|
|
2
|
-
import { Collaboration } from '@elytracms/core/collaboration';
|
|
3
|
-
import type { Actor, PreviewMode } from '@elytracms/core/collaboration';
|
|
4
|
-
/**
|
|
5
|
-
* Per-project collaboration session (Phase 10). Holds one {@link Collaboration} store seeded with
|
|
6
|
-
* an initial version + presence for the current user, and exposes a `mutate` helper that re-renders
|
|
7
|
-
* consumers after an imperative store change. Non-persistent (session): it is the builder canvas's
|
|
8
|
-
* session-scoped presence/perspective store. EC-287 retired the project graph and its publish path,
|
|
9
|
-
* so the seed no longer reflects a pinned graph revision; content publishing is per document
|
|
10
|
-
* (EC-224), surfaced by the CMS document Versions pane.
|
|
11
|
-
*/
|
|
12
|
-
export interface CollabStore {
|
|
13
|
-
collab: Collaboration;
|
|
14
|
-
projectId: string;
|
|
15
|
-
me: Actor;
|
|
16
|
-
/** Draft vs published perspective the canvas/preview renders (EC-123). */
|
|
17
|
-
previewMode: PreviewMode;
|
|
18
|
-
setPreviewMode: (mode: PreviewMode) => void;
|
|
19
|
-
/** Run an imperative store mutation, then re-render consumers. */
|
|
20
|
-
mutate: <T>(fn: () => T) => T;
|
|
21
|
-
}
|
|
22
|
-
declare const useCollab: () => CollabStore;
|
|
23
|
-
export declare function CollabProvider({ projectId, children, }: {
|
|
24
|
-
projectId: string;
|
|
25
|
-
children: ReactNode;
|
|
26
|
-
}): import("react").JSX.Element;
|
|
27
|
-
export { useCollab };
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { useMemo, useReducer, useRef } from 'react';
|
|
3
|
-
import { Collaboration, createSequentialClock, createSequentialIds } from '@elytracms/core/collaboration';
|
|
4
|
-
import { createRequiredContext } from '@elytracms/studio/lib/required-context';
|
|
5
|
-
import { seedCollabSession } from './collab-seed.js';
|
|
6
|
-
const { Context: CollabContext, useRequired: useCollab } = createRequiredContext('Collab');
|
|
7
|
-
const ME = { id: 'u-you', name: 'You' };
|
|
8
|
-
export function CollabProvider({ projectId, children, }) {
|
|
9
|
-
const [tick, bump] = useReducer((n) => n + 1, 0);
|
|
10
|
-
const [previewMode, setPreviewModeState] = useReducer((_, next) => next, 'draft');
|
|
11
|
-
const ref = useRef(null);
|
|
12
|
-
if (ref.current === null) {
|
|
13
|
-
const collab = new Collaboration({ clock: createSequentialClock(), ids: createSequentialIds() });
|
|
14
|
-
// Seed one empty Initial version + presence for the current user (EC-122/126).
|
|
15
|
-
seedCollabSession(collab, projectId, { author: ME.name });
|
|
16
|
-
collab.presence.join(projectId, ME);
|
|
17
|
-
ref.current = collab;
|
|
18
|
-
}
|
|
19
|
-
const store = useMemo(() => {
|
|
20
|
-
const collab = ref.current;
|
|
21
|
-
return {
|
|
22
|
-
collab,
|
|
23
|
-
projectId,
|
|
24
|
-
me: ME,
|
|
25
|
-
previewMode,
|
|
26
|
-
setPreviewMode: setPreviewModeState,
|
|
27
|
-
mutate: (fn) => {
|
|
28
|
-
const result = fn();
|
|
29
|
-
bump();
|
|
30
|
-
return result;
|
|
31
|
-
},
|
|
32
|
-
};
|
|
33
|
-
// `tick` (bumped by `mutate`) and `previewMode` are in deps so consumers
|
|
34
|
-
// re-render after an imperative store mutation or a preview change.
|
|
35
|
-
}, [projectId, previewMode, tick]);
|
|
36
|
-
return _jsx(CollabContext.Provider, { value: store, children: children });
|
|
37
|
-
}
|
|
38
|
-
export { useCollab };
|
|
39
|
-
//# sourceMappingURL=collab-context.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"collab-context.js","sourceRoot":"","sources":["../../../src/components/collab/collab-context.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,OAAO,CAAA;AAEnD,OAAO,EAAE,aAAa,EAAE,qBAAqB,EAAE,mBAAmB,EAAE,MAAM,+BAA+B,CAAA;AAEzG,OAAO,EAAE,qBAAqB,EAAE,MAAM,wCAAwC,CAAA;AAC9E,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAA;AAqBjD,MAAM,EAAE,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,SAAS,EAAE,GAAG,qBAAqB,CAAc,QAAQ,CAAC,CAAA;AAEvG,MAAM,EAAE,GAAU,EAAE,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,CAAA;AAE9C,MAAM,UAAU,cAAc,CAAC,EAC7B,SAAS,EACT,QAAQ,GAIT;IACC,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,UAAU,CAAC,CAAC,CAAS,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAA;IACxD,MAAM,CAAC,WAAW,EAAE,mBAAmB,CAAC,GAAG,UAAU,CACnD,CAAC,CAAc,EAAE,IAAiB,EAAE,EAAE,CAAC,IAAI,EAC3C,OAAsB,CACvB,CAAA;IAED,MAAM,GAAG,GAAG,MAAM,CAAuB,IAAI,CAAC,CAAA;IAC9C,IAAI,GAAG,CAAC,OAAO,KAAK,IAAI,EAAE,CAAC;QACzB,MAAM,MAAM,GAAG,IAAI,aAAa,CAAC,EAAE,KAAK,EAAE,qBAAqB,EAAE,EAAE,GAAG,EAAE,mBAAmB,EAAE,EAAE,CAAC,CAAA;QAChG,+EAA+E;QAC/E,iBAAiB,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,MAAM,EAAE,EAAE,CAAC,IAAI,EAAE,CAAC,CAAA;QACzD,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,SAAS,EAAE,EAAE,CAAC,CAAA;QACnC,GAAG,CAAC,OAAO,GAAG,MAAM,CAAA;IACtB,CAAC;IAED,MAAM,KAAK,GAAG,OAAO,CAAc,GAAG,EAAE;QACtC,MAAM,MAAM,GAAG,GAAG,CAAC,OAAQ,CAAA;QAC3B,OAAO;YACL,MAAM;YACN,SAAS;YACT,EAAE,EAAE,EAAE;YACN,WAAW;YACX,cAAc,EAAE,mBAAmB;YACnC,MAAM,EAAE,CAAC,EAAE,EAAE,EAAE;gBACb,MAAM,MAAM,GAAG,EAAE,EAAE,CAAA;gBACnB,IAAI,EAAE,CAAA;gBACN,OAAO,MAAM,CAAA;YACf,CAAC;SACF,CAAA;QACD,yEAAyE;QACzE,oEAAoE;IACtE,CAAC,EAAE,CAAC,SAAS,EAAE,WAAW,EAAE,IAAI,CAAC,CAAC,CAAA;IAElC,OAAO,KAAC,aAAa,CAAC,QAAQ,IAAC,KAAK,EAAE,KAAK,YAAG,QAAQ,GAA0B,CAAA;AAClF,CAAC;AAED,OAAO,EAAE,SAAS,EAAE,CAAA","sourcesContent":["import { useMemo, useReducer, useRef } from 'react'\nimport type { ReactNode } from 'react'\nimport { Collaboration, createSequentialClock, createSequentialIds } from '@elytracms/core/collaboration'\nimport type { Actor, PreviewMode } from '@elytracms/core/collaboration'\nimport { createRequiredContext } from '@elytracms/studio/lib/required-context'\nimport { seedCollabSession } from './collab-seed'\n\n/**\n * Per-project collaboration session (Phase 10). Holds one {@link Collaboration} store seeded with\n * an initial version + presence for the current user, and exposes a `mutate` helper that re-renders\n * consumers after an imperative store change. Non-persistent (session): it is the builder canvas's\n * session-scoped presence/perspective store. EC-287 retired the project graph and its publish path,\n * so the seed no longer reflects a pinned graph revision; content publishing is per document\n * (EC-224), surfaced by the CMS document Versions pane.\n */\nexport interface CollabStore {\n collab: Collaboration\n projectId: string\n me: Actor\n /** Draft vs published perspective the canvas/preview renders (EC-123). */\n previewMode: PreviewMode\n setPreviewMode: (mode: PreviewMode) => void\n /** Run an imperative store mutation, then re-render consumers. */\n mutate: <T>(fn: () => T) => T\n}\n\nconst { Context: CollabContext, useRequired: useCollab } = createRequiredContext<CollabStore>('Collab')\n\nconst ME: Actor = { id: 'u-you', name: 'You' }\n\nexport function CollabProvider({\n projectId,\n children,\n}: {\n projectId: string\n children: ReactNode\n}) {\n const [tick, bump] = useReducer((n: number) => n + 1, 0)\n const [previewMode, setPreviewModeState] = useReducer(\n (_: PreviewMode, next: PreviewMode) => next,\n 'draft' as PreviewMode,\n )\n\n const ref = useRef<Collaboration | null>(null)\n if (ref.current === null) {\n const collab = new Collaboration({ clock: createSequentialClock(), ids: createSequentialIds() })\n // Seed one empty Initial version + presence for the current user (EC-122/126).\n seedCollabSession(collab, projectId, { author: ME.name })\n collab.presence.join(projectId, ME)\n ref.current = collab\n }\n\n const store = useMemo<CollabStore>(() => {\n const collab = ref.current!\n return {\n collab,\n projectId,\n me: ME,\n previewMode,\n setPreviewMode: setPreviewModeState,\n mutate: (fn) => {\n const result = fn()\n bump()\n return result\n },\n }\n // `tick` (bumped by `mutate`) and `previewMode` are in deps so consumers\n // re-render after an imperative store mutation or a preview change.\n }, [projectId, previewMode, tick])\n\n return <CollabContext.Provider value={store}>{children}</CollabContext.Provider>\n}\n\nexport { useCollab }\n"]}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import type { Collaboration } from '@elytracms/core/collaboration';
|
|
2
|
-
/**
|
|
3
|
-
* Seed a fresh per-project {@link Collaboration} session.
|
|
4
|
-
*
|
|
5
|
-
* EC-287: the project graph and its publish path are retired (the DB holds
|
|
6
|
-
* content only), so a session no longer reflects a pinned graph revision. The
|
|
7
|
-
* collaboration store stays the builder's session-scoped presence/perspective
|
|
8
|
-
* store; it seeds one empty "Initial" version so `versions.latest()` is defined,
|
|
9
|
-
* with no graph snapshot and nothing published. Content publishing is per
|
|
10
|
-
* document (EC-224), surfaced by the CMS document Versions pane — not here.
|
|
11
|
-
*/
|
|
12
|
-
export declare function seedCollabSession(collab: Collaboration, projectId: string, options: {
|
|
13
|
-
/** Author recorded on the Initial version. */
|
|
14
|
-
author: string;
|
|
15
|
-
}): void;
|