@elytracms/studio 0.0.17 → 0.0.19

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 (43) hide show
  1. package/dist/components/app-views/default-modules.d.ts +102 -26
  2. package/dist/components/app-views/default-modules.js +166 -53
  3. package/dist/components/app-views/default-modules.js.map +1 -1
  4. package/dist/components/app-views/module.d.ts +81 -50
  5. package/dist/components/app-views/module.js +25 -43
  6. package/dist/components/app-views/module.js.map +1 -1
  7. package/dist/components/app-views/sidebar.d.ts +64 -73
  8. package/dist/components/app-views/sidebar.js +120 -125
  9. package/dist/components/app-views/sidebar.js.map +1 -1
  10. package/dist/components/app-views/studio-config.d.ts +77 -0
  11. package/dist/components/app-views/studio-config.js +31 -0
  12. package/dist/components/app-views/studio-config.js.map +1 -0
  13. package/dist/components/cms/cms-index.js +8 -8
  14. package/dist/components/cms/cms-index.js.map +1 -1
  15. package/dist/components/cms/collection-focus.d.ts +10 -0
  16. package/dist/components/cms/collection-focus.js +16 -0
  17. package/dist/components/cms/collection-focus.js.map +1 -0
  18. package/dist/components/cms/field-control.d.ts +8 -5
  19. package/dist/components/cms/field-control.js +21 -10
  20. package/dist/components/cms/field-control.js.map +1 -1
  21. package/dist/components/cms/field-renderer.d.ts +28 -30
  22. package/dist/components/cms/field-renderer.js +15 -20
  23. package/dist/components/cms/field-renderer.js.map +1 -1
  24. package/dist/components/section-nav.d.ts +8 -7
  25. package/dist/components/section-nav.js +36 -26
  26. package/dist/components/section-nav.js.map +1 -1
  27. package/dist/components/studio-project-screen.js +16 -16
  28. package/dist/components/studio-project-screen.js.map +1 -1
  29. package/dist/components/studio-shell.js +13 -13
  30. package/dist/components/studio-shell.js.map +1 -1
  31. package/dist/customization.d.ts +48 -0
  32. package/dist/customization.js +46 -0
  33. package/dist/customization.js.map +1 -0
  34. package/dist/lib/cms-views/view-model.d.ts +2 -2
  35. package/dist/lib/cms-views/views-store.d.ts +1 -1
  36. package/dist/lib/workspace/index.d.ts +32 -24
  37. package/dist/lib/workspace/index.js +35 -27
  38. package/dist/lib/workspace/index.js.map +1 -1
  39. package/dist/router/match.d.ts +16 -16
  40. package/dist/router/match.js +17 -17
  41. package/dist/router/match.js.map +1 -1
  42. package/dist/ui/badge.d.ts +1 -1
  43. package/package.json +6 -2
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/lib/workspace/index.ts"],"names":[],"mappings":"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","sourcesContent":["import { frontendUrlForEnvironment } from '@elytracms/core/studio-core'\nimport type {\n AppViewEntry,\n BreakpointConfig,\n ElytraConfigInput,\n FieldRendererEntry,\n ResolvedWorkspaceProject,\n SidebarEntry,\n} from '@elytracms/core/studio-core'\nimport type { CollectionDef, RedirectRecord, RouteRecord } from '@elytracms/core/cms-core'\nimport { resolveWorkspaceState } from './workspace-source'\nimport type { WorkspaceState } from './workspace-source'\n\nexport { resolveWorkspaceState, WORKSPACE_ENV_VAR } from './workspace-source'\nexport type { WorkspaceSources, WorkspaceState } from './workspace-source'\nexport { persistedProjects, selectSwitcherProjects, unprovisionedProjects } from './switcher-source'\nexport type { SwitcherEntry, SwitcherSourceInput } from './switcher-source'\n\nlet cached: WorkspaceState | null = null\n\n/**\n * Mount the studio against a project config (EC-223 mount contract). The\n * consumer's entry point does `import config from './elytra.config'` and calls\n * this — an explicit init, NOT an `import.meta.glob` autoload that reaches into\n * the consumer's filesystem (Oli, 2026-06-19: \"there's always an entry point that\n * can trigger a function call\"). This is the config-injection responsibility of\n * the eventual `createStudio(config)` package export; it seeds the workspace state\n * every later query (`getActiveProjectSlug`, `resolveDeployment*`) reads.\n */\nexport function createStudio(config: ElytraConfigInput): void {\n cached = resolveWorkspaceState({\n fileModule: { default: config },\n fileName: 'elytra.config.ts',\n })\n}\n\n/**\n * The active project slug (EC-179, vision AD-8). One project per backend, so the\n * active project selects which Convex deployment the studio talks to. The root\n * layout sets this from the URL (`/<slug>`) — or, on the sign-in screen, from\n * the `redirect` target's slug — before the auth/persistence adapters are\n * selected, so the *initial* sign-in authenticates against the project the user\n * is heading for (Sanity-style: authenticate to one workspace, then the full\n * config-based switcher is available). `null` at the launcher → the default\n * (first) configured project's deployment.\n */\nlet activeProjectSlug: string | null = null\n\n/** Set the active project slug; returns whether the resolved deployment changed. */\nexport function setActiveProjectSlug(slug: string | null): boolean {\n const before = resolveDeploymentUrl('')\n activeProjectSlug = slug && slug.trim() !== '' ? slug : null\n return resolveDeploymentUrl('') !== before\n}\n\nexport function getActiveProjectSlug(): string | null {\n return activeProjectSlug\n}\n\n/** Test seam: reset the active project slug. Never used by app code. */\nexport function setActiveProjectSlugForTests(slug: string | null): void {\n activeProjectSlug = slug\n}\n\n/**\n * Reserved first-path segments that are NOT project slugs: the studio's own\n * root routes (EC-220). Project surfaces now live at `/<slug>/...` (no `/p/`\n * prefix), so the studio's root namespace is shared between project slugs and\n * these fixed routes — they must be excluded from slug extraction. Keep in\n * sync with the non-`$projectId` files in `src/routes/`; the launcher (`/`) is\n * the empty segment, excluded by the regex requiring one path char.\n */\nconst RESERVED_ROOT_SEGMENTS: ReadonlySet<string> = new Set(['sign-in', 'styleguide'])\n\n/** Extract a project slug from a studio path (`/<slug>/...`), else `null`. */\nexport function projectSlugFromPath(pathname: string): string | null {\n const match = /^\\/([^/?#]+)/.exec(pathname)\n if (!match) return null\n const segment = decodeURIComponent(match[1]!)\n return RESERVED_ROOT_SEGMENTS.has(segment) ? null : segment\n}\n\n/**\n * The project whose backend the studio should bind for a given location\n * (EC-179). A project path (`/<slug>`) binds that project; the sign-in screen\n * binds the project from its `redirect` target so the *initial* sign-in\n * authenticates against the backend the user is heading for (Sanity-style).\n * Anywhere else (the launcher) → `null`, i.e. the default/first project.\n */\nexport function activeSlugForLocation(pathname: string, redirect?: string): string | null {\n const direct = projectSlugFromPath(pathname)\n if (direct) return direct\n if (pathname === '/sign-in' && redirect) return projectSlugFromPath(redirect)\n return null\n}\n\n/** The active config project (by slug), or the first configured project. */\nfunction activeConfigProject() {\n const state = getWorkspaceState()\n if (state.kind !== 'config') return undefined\n if (activeProjectSlug) {\n const match = state.projects.find((p) => p.slug === activeProjectSlug)\n if (match) return match\n }\n return state.projects[0]\n}\n\n/**\n * The studio's resolved workspace state. Deterministic per boot: the config is\n * static input (bundled file or deployment env), so it is resolved once and\n * cached — every consumer (gate, switcher, launcher) sees the same state.\n */\nexport function getWorkspaceState(): WorkspaceState {\n // Seeded by `createStudio(config)` at the entry point. Until then — or when the\n // consumer wires no config — resolve to the empty `none` state (the single-project\n // golden path), rather than reaching for the filesystem.\n if (!cached) cached = resolveWorkspaceState({})\n return cached\n}\n\n/** Test seam: clear/replace the cached state. Never used by app code. */\nexport function setWorkspaceStateForTests(next: WorkspaceState | null): void {\n cached = next\n}\n\n/**\n * Resolve the Convex deployment URL the studio should connect to (vision AD-8:\n * the backend is wired in config). One project per backend, so the **active**\n * project (see {@link setActiveProjectSlug}) selects the deployment via its\n * `convex.deployment` (or its production environment's). At the launcher (no\n * active project) the first configured project is the default backend. Absent\n * config — or a project without a wired deployment — falls back to\n * `VITE_CONVEX_URL` (`fallbackUrl`) for local single-deployment development.\n */\nexport function resolveDeploymentUrl(fallbackUrl: string): string {\n return projectDeploymentUrl(activeConfigProject(), fallbackUrl)\n}\n\n/** The deployment URL for a specific project slug (per-backend auth checks). */\nexport function deploymentUrlForSlug(slug: string, fallbackUrl: string): string {\n const state = getWorkspaceState()\n const project = state.kind === 'config' ? state.projects.find((p) => p.slug === slug) : undefined\n return projectDeploymentUrl(project, fallbackUrl)\n}\n\n/** A project's wired deployment: per-environment ref → project ref → env fallback. */\nfunction projectDeploymentUrl(\n project: ResolvedWorkspaceProject | undefined,\n fallbackUrl: string,\n): string {\n if (!project) return fallbackUrl\n const envDeployment = project.environments.find((e) => e.convex?.deployment)?.convex?.deployment\n return envDeployment ?? project.convex?.deployment ?? fallbackUrl\n}\n\n/**\n * The slug of the project bound to the active deployment (EC-179). One project\n * per backend, so the active config project's slug identifies the deployment's\n * one project; the Convex adapter stamps it onto deployment-scoped records.\n * Absent config falls back to `fallbackId` (used only when wired by env alone).\n */\nexport function resolveDeploymentProjectId(fallbackId: string): string {\n return activeConfigProject()?.slug ?? fallbackId\n}\n\n/**\n * The CMS collections of the active config project (AD-11, EC-183). CMS\n * collection schemas are config-owned: the studio reads the model from\n * `elytra.config.ts` (served through the persistence port — the backend has no\n * `cmsSchemas` table). `[]` when the active project declares none, or when the\n * studio is wired by env alone with no config.\n */\nexport function resolveDeploymentCollections(): CollectionDef[] {\n return activeConfigProject()?.collections ?? []\n}\n\n/**\n * The canvas preview breakpoints of the active config project (EC-250).\n * Config-sourced (config-as-code, derived from the site's Tailwind `screens`);\n * `undefined` when the project declares none — the canvas then falls back to\n * Tailwind's default screens (see `resolveCanvasViewports`), never a baked-in triple.\n */\nexport function resolveDeploymentBreakpoints(): BreakpointConfig[] | undefined {\n return activeConfigProject()?.breakpoints\n}\n\n/**\n * The custom field renderers of the active config project (EC-238). Config-sourced\n * live React code (omitted from the serializable projection); `[]` when none — the\n * document editor then uses its built-in control for every field type.\n */\nexport function resolveDeploymentFieldRenderers(): FieldRendererEntry[] {\n return activeConfigProject()?.fieldRenderers ?? []\n}\n\n/**\n * The custom app-view modules of the active config project (EC-239). Config-sourced\n * live React modules (omitted from the serializable projection); `[]` when none —\n * the studio then mounts only its default module registry (CMS/Routes/Assets/…).\n */\nexport function resolveDeploymentAppViews(): AppViewEntry[] {\n return activeConfigProject()?.appViews ?? []\n}\n\n/**\n * The configurable sidebar of the active config project (EC-237). Config-sourced\n * live entries (carry icons; omitted from the projection); `undefined` when the\n * project declares none — the studio then derives the default rail from its module\n * registry (declared-but-invalid entries surface as visible issues, never silent).\n */\nexport function resolveDeploymentSidebar(): SidebarEntry[] | undefined {\n return activeConfigProject()?.sidebar\n}\n\n/**\n * The resolved URL routes of the active config project (EC-207, \"routes as\n * code\"). Routes are config-sourced: the studio reads the materialized\n * `RouteRecord[]` from the active `elytra.config.ts` project — never from the\n * backend (the `routes` system collection is retired). The Routes surface and\n * the per-page URL preview both read through here. `[]` when the active project\n * declares none, or when the studio is wired by env alone with no config.\n */\nexport function resolveDeploymentRoutes(): RouteRecord[] {\n return activeConfigProject()?.routes ?? []\n}\n\n/**\n * The resolved redirects of the active config project (EC-207). Config-sourced\n * exactly like {@link resolveDeploymentRoutes}: the `redirects` system\n * collection is retired and nothing is read from the backend. `[]` when none\n * are declared.\n */\nexport function resolveDeploymentRedirects(): RedirectRecord[] {\n return activeConfigProject()?.redirects ?? []\n}\n\n/**\n * The configured locales of the active config project (AD-8). The same locale\n * set the host stamps onto its per-locale cache tags — used by the revalidation\n * webhook layer (EC-192) so a publish always invalidates EVERY configured locale\n * (e.g. `/de`, `/fr`), never just the default. `[]` when wired by env alone.\n */\nexport function resolveDeploymentLocales(): string[] {\n return activeConfigProject()?.locales ?? []\n}\n\n/**\n * The active project's components reference (EC-194, AD-11): where the consuming\n * repo's OWN real components live (`{ package }` or `{ dir }`). When set, the\n * studio registry composes with the repo's components instead of the built-in\n * demo fixtures — so the page builder edits the repo's actual component library.\n * `undefined` → the project ships no code components; the studio falls back to the\n * golden-path fixtures.\n */\nexport function resolveDeploymentComponents() {\n return activeConfigProject()?.components\n}\n\n/**\n * The active project's public site URL (EC-215): the live site's base URL the\n * studio links to (a document's live URL + open-live button, EC-216). Resolved\n * in lockstep with {@link resolveDeploymentUrl} — the environment bound to the\n * active deployment (the first with a wired deployment) provides the URL, falling\n * back to the project-level `frontendUrl`. No active-environment selector exists\n * yet, so the bound environment is the active one. `undefined` when the active\n * project (or env-only wiring) configures no frontend URL.\n */\nexport function resolveDeploymentFrontendUrl(): string | undefined {\n const project = activeConfigProject()\n if (!project) return undefined\n const boundEnv = project.environments.find((e) => e.convex?.deployment)\n return frontendUrlForEnvironment(project, boundEnv?.key)\n}\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/lib/workspace/index.ts"],"names":[],"mappings":"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","sourcesContent":["import { frontendUrlForEnvironment } from '@elytracms/core/studio-core'\nimport type {\n BreakpointConfig,\n ElytraConfigInput,\n ResolvedWorkspaceProject,\n} from '@elytracms/core/studio-core'\nimport type { CollectionDef, RedirectRecord, RouteRecord } from '@elytracms/core/cms-core'\nimport type { SidebarNode } from '../../components/app-views/module'\nimport type { CustomFieldRenderer, StudioConfig } from '../../components/app-views/studio-config'\nimport { resolveWorkspaceState } from './workspace-source'\nimport type { WorkspaceState } from './workspace-source'\n\nexport { resolveWorkspaceState, WORKSPACE_ENV_VAR } from './workspace-source'\nexport type { WorkspaceSources, WorkspaceState } from './workspace-source'\nexport { persistedProjects, selectSwitcherProjects, unprovisionedProjects } from './switcher-source'\nexport type { SwitcherEntry, SwitcherSourceInput } from './switcher-source'\n\nlet cached: WorkspaceState | null = null\nlet studioConfig: StudioConfig | null = null\n\n/**\n * Mount the studio against a project config (EC-223 mount contract) and an\n * optional studio-only customization config (EC-236). The consumer's entry point\n * does `import config from './elytra.config'` + `import studio from\n * './studio.config'` and calls this — an explicit init, NOT an `import.meta.glob`\n * autoload that reaches into the consumer's filesystem (Oli, 2026-06-19: \"there's\n * always an entry point that can trigger a function call\").\n *\n * Two configs, deliberately: `config` (`elytra.config.ts`) is the project\n * definition, shared with the `frontend/` delivery bundle and projected to data;\n * `studio` (`studio.config.tsx` via `defineStudio`) is live builder UI code — the\n * section rail, custom field renderers, branding — that must NEVER reach delivery.\n * Keeping them separate is the customization boundary principle in the mount\n * signature. Seeds the workspace state + the studio-config holder every later\n * query (`getActiveProjectSlug`, `resolveDeployment*`) reads.\n */\nexport function createStudio(config: ElytraConfigInput, studio?: StudioConfig): void {\n cached = resolveWorkspaceState({\n fileModule: { default: config },\n fileName: 'elytra.config.ts',\n })\n studioConfig = studio ?? null\n}\n\n/** Test seam: set/clear the studio customization config. Never used by app code. */\nexport function setStudioConfigForTests(next: StudioConfig | null): void {\n studioConfig = next\n}\n\n/**\n * The active project slug (EC-179, vision AD-8). One project per backend, so the\n * active project selects which Convex deployment the studio talks to. The root\n * layout sets this from the URL (`/<slug>`) — or, on the sign-in screen, from\n * the `redirect` target's slug — before the auth/persistence adapters are\n * selected, so the *initial* sign-in authenticates against the project the user\n * is heading for (Sanity-style: authenticate to one workspace, then the full\n * config-based switcher is available). `null` at the launcher → the default\n * (first) configured project's deployment.\n */\nlet activeProjectSlug: string | null = null\n\n/** Set the active project slug; returns whether the resolved deployment changed. */\nexport function setActiveProjectSlug(slug: string | null): boolean {\n const before = resolveDeploymentUrl('')\n activeProjectSlug = slug && slug.trim() !== '' ? slug : null\n return resolveDeploymentUrl('') !== before\n}\n\nexport function getActiveProjectSlug(): string | null {\n return activeProjectSlug\n}\n\n/** Test seam: reset the active project slug. Never used by app code. */\nexport function setActiveProjectSlugForTests(slug: string | null): void {\n activeProjectSlug = slug\n}\n\n/**\n * Reserved first-path segments that are NOT project slugs: the studio's own\n * root routes (EC-220). Project surfaces now live at `/<slug>/...` (no `/p/`\n * prefix), so the studio's root namespace is shared between project slugs and\n * these fixed routes — they must be excluded from slug extraction. Keep in\n * sync with the non-`$projectId` files in `src/routes/`; the launcher (`/`) is\n * the empty segment, excluded by the regex requiring one path char.\n */\nconst RESERVED_ROOT_SEGMENTS: ReadonlySet<string> = new Set(['sign-in', 'styleguide'])\n\n/** Extract a project slug from a studio path (`/<slug>/...`), else `null`. */\nexport function projectSlugFromPath(pathname: string): string | null {\n const match = /^\\/([^/?#]+)/.exec(pathname)\n if (!match) return null\n const segment = decodeURIComponent(match[1]!)\n return RESERVED_ROOT_SEGMENTS.has(segment) ? null : segment\n}\n\n/**\n * The project whose backend the studio should bind for a given location\n * (EC-179). A project path (`/<slug>`) binds that project; the sign-in screen\n * binds the project from its `redirect` target so the *initial* sign-in\n * authenticates against the backend the user is heading for (Sanity-style).\n * Anywhere else (the launcher) → `null`, i.e. the default/first project.\n */\nexport function activeSlugForLocation(pathname: string, redirect?: string): string | null {\n const direct = projectSlugFromPath(pathname)\n if (direct) return direct\n if (pathname === '/sign-in' && redirect) return projectSlugFromPath(redirect)\n return null\n}\n\n/** The active config project (by slug), or the first configured project. */\nfunction activeConfigProject() {\n const state = getWorkspaceState()\n if (state.kind !== 'config') return undefined\n if (activeProjectSlug) {\n const match = state.projects.find((p) => p.slug === activeProjectSlug)\n if (match) return match\n }\n return state.projects[0]\n}\n\n/**\n * The studio's resolved workspace state. Deterministic per boot: the config is\n * static input (bundled file or deployment env), so it is resolved once and\n * cached — every consumer (gate, switcher, launcher) sees the same state.\n */\nexport function getWorkspaceState(): WorkspaceState {\n // Seeded by `createStudio(config)` at the entry point. Until then — or when the\n // consumer wires no config — resolve to the empty `none` state (the single-project\n // golden path), rather than reaching for the filesystem.\n if (!cached) cached = resolveWorkspaceState({})\n return cached\n}\n\n/** Test seam: clear/replace the cached state. Never used by app code. */\nexport function setWorkspaceStateForTests(next: WorkspaceState | null): void {\n cached = next\n}\n\n/**\n * Resolve the Convex deployment URL the studio should connect to (vision AD-8:\n * the backend is wired in config). One project per backend, so the **active**\n * project (see {@link setActiveProjectSlug}) selects the deployment via its\n * `convex.deployment` (or its production environment's). At the launcher (no\n * active project) the first configured project is the default backend. Absent\n * config — or a project without a wired deployment — falls back to\n * `VITE_CONVEX_URL` (`fallbackUrl`) for local single-deployment development.\n */\nexport function resolveDeploymentUrl(fallbackUrl: string): string {\n return projectDeploymentUrl(activeConfigProject(), fallbackUrl)\n}\n\n/** The deployment URL for a specific project slug (per-backend auth checks). */\nexport function deploymentUrlForSlug(slug: string, fallbackUrl: string): string {\n const state = getWorkspaceState()\n const project = state.kind === 'config' ? state.projects.find((p) => p.slug === slug) : undefined\n return projectDeploymentUrl(project, fallbackUrl)\n}\n\n/** A project's wired deployment: per-environment ref → project ref → env fallback. */\nfunction projectDeploymentUrl(\n project: ResolvedWorkspaceProject | undefined,\n fallbackUrl: string,\n): string {\n if (!project) return fallbackUrl\n const envDeployment = project.environments.find((e) => e.convex?.deployment)?.convex?.deployment\n return envDeployment ?? project.convex?.deployment ?? fallbackUrl\n}\n\n/**\n * The slug of the project bound to the active deployment (EC-179). One project\n * per backend, so the active config project's slug identifies the deployment's\n * one project; the Convex adapter stamps it onto deployment-scoped records.\n * Absent config falls back to `fallbackId` (used only when wired by env alone).\n */\nexport function resolveDeploymentProjectId(fallbackId: string): string {\n return activeConfigProject()?.slug ?? fallbackId\n}\n\n/**\n * The CMS collections of the active config project (AD-11, EC-183). CMS\n * collection schemas are config-owned: the studio reads the model from\n * `elytra.config.ts` (served through the persistence port — the backend has no\n * `cmsSchemas` table). `[]` when the active project declares none, or when the\n * studio is wired by env alone with no config.\n */\nexport function resolveDeploymentCollections(): CollectionDef[] {\n return activeConfigProject()?.collections ?? []\n}\n\n/**\n * The canvas preview breakpoints of the active config project (EC-250).\n * Config-sourced (config-as-code, derived from the site's Tailwind `screens`);\n * `undefined` when the project declares none — the canvas then falls back to\n * Tailwind's default screens (see `resolveCanvasViewports`), never a baked-in triple.\n */\nexport function resolveDeploymentBreakpoints(): BreakpointConfig[] | undefined {\n return activeConfigProject()?.breakpoints\n}\n\n/**\n * The custom field renderers from the studio config (EC-238): control name →\n * component. Studio-only live code (from `defineStudio`, never the project schema\n * or the delivery bundle); `undefined` when none — the document editor then uses\n * its built-in control for every field. Studio-wide (not per-project): the rail and\n * renderers customize the studio, not one backend.\n */\nexport function resolveDeploymentFieldRenderers(): Record<string, CustomFieldRenderer> | undefined {\n return studioConfig?.fieldRenderers\n}\n\n/**\n * The configurable section rail from the studio config (EC-237): an ordered list of\n * `nav.*` nodes (views / groups / separators). Studio-only live code (carries view\n * objects + icons); `undefined` when none — the studio then derives the default\n * rail from its built-in views. Invalid entries surface as visible issues at\n * resolution, never silent.\n */\nexport function resolveDeploymentSidebar(): SidebarNode[] | undefined {\n return studioConfig?.sidebar\n}\n\n/**\n * The resolved URL routes of the active config project (EC-207, \"routes as\n * code\"). Routes are config-sourced: the studio reads the materialized\n * `RouteRecord[]` from the active `elytra.config.ts` project — never from the\n * backend (the `routes` system collection is retired). The Routes surface and\n * the per-page URL preview both read through here. `[]` when the active project\n * declares none, or when the studio is wired by env alone with no config.\n */\nexport function resolveDeploymentRoutes(): RouteRecord[] {\n return activeConfigProject()?.routes ?? []\n}\n\n/**\n * The resolved redirects of the active config project (EC-207). Config-sourced\n * exactly like {@link resolveDeploymentRoutes}: the `redirects` system\n * collection is retired and nothing is read from the backend. `[]` when none\n * are declared.\n */\nexport function resolveDeploymentRedirects(): RedirectRecord[] {\n return activeConfigProject()?.redirects ?? []\n}\n\n/**\n * The configured locales of the active config project (AD-8). The same locale\n * set the host stamps onto its per-locale cache tags — used by the revalidation\n * webhook layer (EC-192) so a publish always invalidates EVERY configured locale\n * (e.g. `/de`, `/fr`), never just the default. `[]` when wired by env alone.\n */\nexport function resolveDeploymentLocales(): string[] {\n return activeConfigProject()?.locales ?? []\n}\n\n/**\n * The active project's components reference (EC-194, AD-11): where the consuming\n * repo's OWN real components live (`{ package }` or `{ dir }`). When set, the\n * studio registry composes with the repo's components instead of the built-in\n * demo fixtures — so the page builder edits the repo's actual component library.\n * `undefined` → the project ships no code components; the studio falls back to the\n * golden-path fixtures.\n */\nexport function resolveDeploymentComponents() {\n return activeConfigProject()?.components\n}\n\n/**\n * The active project's public site URL (EC-215): the live site's base URL the\n * studio links to (a document's live URL + open-live button, EC-216). Resolved\n * in lockstep with {@link resolveDeploymentUrl} — the environment bound to the\n * active deployment (the first with a wired deployment) provides the URL, falling\n * back to the project-level `frontendUrl`. No active-environment selector exists\n * yet, so the bound environment is the active one. `undefined` when the active\n * project (or env-only wiring) configures no frontend URL.\n */\nexport function resolveDeploymentFrontendUrl(): string | undefined {\n const project = activeConfigProject()\n if (!project) return undefined\n const boundEnv = project.environments.find((e) => e.convex?.deployment)\n return frontendUrlForEnvironment(project, boundEnv?.key)\n}\n"]}
@@ -11,33 +11,33 @@
11
11
  * convention without needing a router instance.
12
12
  *
13
13
  * EC-239 (app-view modularization): the route table is no longer a hardcoded
14
- * per-section switch — the studio's surfaces are a config-extendable module
15
- * registry, so the matcher is now registry-AGNOSTIC. It splits the splat into a
16
- * module id (the first segment) + that module's trailing segments; the screen
17
- * resolver looks the id up in the registry and the module's own `parsePath`
14
+ * per-section switch — the studio's surfaces are a config-extendable VIEW registry
15
+ * derived from the section rail, so the matcher is registry-AGNOSTIC. It splits the
16
+ * splat into a view id (the first segment) + that view's trailing segments; the
17
+ * screen resolver looks the id up in the registry and the view's own `parsePath`
18
18
  * interprets the segments (CMS: collection/document; components: id; flat: none).
19
- * Keeping the matcher free of the surface list is what lets a consuming repo add
20
- * a routable custom module purely from config.
19
+ * Keeping the matcher free of the surface list is what lets a consuming repo add a
20
+ * routable custom view purely from config.
21
21
  *
22
- * /$projectId → { section: 'index' } (redirect to first module)
23
- * /$projectId/<id> → { section: 'module', moduleId: '<id>', segments: [] }
24
- * /$projectId/<id>/a/b → { section: 'module', moduleId: '<id>', segments: ['a','b'] }
22
+ * /$projectId → { section: 'index' } (redirect to first view)
23
+ * /$projectId/<id> → { section: 'view', viewId: '<id>', segments: [] }
24
+ * /$projectId/<id>/a/b → { section: 'view', viewId: '<id>', segments: ['a','b'] }
25
25
  *
26
- * The module decides whether its segments resolve; an unknown id or an
27
- * over-deep path becomes a not-found at the resolver, not here.
26
+ * The view decides whether its segments resolve; an unknown id or an over-deep
27
+ * path becomes a not-found at the resolver, not here.
28
28
  */
29
29
  export type StudioRouteMatch = {
30
30
  section: 'index';
31
31
  } | {
32
- section: 'module';
33
- moduleId: string;
32
+ section: 'view';
33
+ viewId: string;
34
34
  segments: string[];
35
35
  };
36
36
  /**
37
- * Match the trailing pathname of the project splat (`_splat`) into a module id +
37
+ * Match the trailing pathname of the project splat (`_splat`) into a view id +
38
38
  * its segments. `splat` is everything after `/$projectId/` — e.g. `'cms/page/abc'`
39
- * for `/p1/cms/page/abc` → `{ moduleId: 'cms', segments: ['page', 'abc'] }`. An
40
- * empty splat is the bare project index (which redirects to the first module).
39
+ * for `/p1/cms/page/abc` → `{ viewId: 'cms', segments: ['page', 'abc'] }`. An
40
+ * empty splat is the bare project index (which redirects to the first view).
41
41
  */
42
42
  export declare function matchStudioRoute(splat: string): StudioRouteMatch;
43
43
  /**
@@ -11,20 +11,20 @@
11
11
  * convention without needing a router instance.
12
12
  *
13
13
  * EC-239 (app-view modularization): the route table is no longer a hardcoded
14
- * per-section switch — the studio's surfaces are a config-extendable module
15
- * registry, so the matcher is now registry-AGNOSTIC. It splits the splat into a
16
- * module id (the first segment) + that module's trailing segments; the screen
17
- * resolver looks the id up in the registry and the module's own `parsePath`
14
+ * per-section switch — the studio's surfaces are a config-extendable VIEW registry
15
+ * derived from the section rail, so the matcher is registry-AGNOSTIC. It splits the
16
+ * splat into a view id (the first segment) + that view's trailing segments; the
17
+ * screen resolver looks the id up in the registry and the view's own `parsePath`
18
18
  * interprets the segments (CMS: collection/document; components: id; flat: none).
19
- * Keeping the matcher free of the surface list is what lets a consuming repo add
20
- * a routable custom module purely from config.
19
+ * Keeping the matcher free of the surface list is what lets a consuming repo add a
20
+ * routable custom view purely from config.
21
21
  *
22
- * /$projectId → { section: 'index' } (redirect to first module)
23
- * /$projectId/<id> → { section: 'module', moduleId: '<id>', segments: [] }
24
- * /$projectId/<id>/a/b → { section: 'module', moduleId: '<id>', segments: ['a','b'] }
22
+ * /$projectId → { section: 'index' } (redirect to first view)
23
+ * /$projectId/<id> → { section: 'view', viewId: '<id>', segments: [] }
24
+ * /$projectId/<id>/a/b → { section: 'view', viewId: '<id>', segments: ['a','b'] }
25
25
  *
26
- * The module decides whether its segments resolve; an unknown id or an
27
- * over-deep path becomes a not-found at the resolver, not here.
26
+ * The view decides whether its segments resolve; an unknown id or an over-deep
27
+ * path becomes a not-found at the resolver, not here.
28
28
  */
29
29
  /** Split a splat pathname into clean segments (drops empty/leading/trailing slashes). */
30
30
  function segments(splat) {
@@ -34,16 +34,16 @@ function segments(splat) {
34
34
  .filter(Boolean);
35
35
  }
36
36
  /**
37
- * Match the trailing pathname of the project splat (`_splat`) into a module id +
37
+ * Match the trailing pathname of the project splat (`_splat`) into a view id +
38
38
  * its segments. `splat` is everything after `/$projectId/` — e.g. `'cms/page/abc'`
39
- * for `/p1/cms/page/abc` → `{ moduleId: 'cms', segments: ['page', 'abc'] }`. An
40
- * empty splat is the bare project index (which redirects to the first module).
39
+ * for `/p1/cms/page/abc` → `{ viewId: 'cms', segments: ['page', 'abc'] }`. An
40
+ * empty splat is the bare project index (which redirects to the first view).
41
41
  */
42
42
  export function matchStudioRoute(splat) {
43
- const [moduleId, ...rest] = segments(splat);
44
- if (moduleId === undefined)
43
+ const [viewId, ...rest] = segments(splat);
44
+ if (viewId === undefined)
45
45
  return { section: 'index' };
46
- return { section: 'module', moduleId, segments: rest };
46
+ return { section: 'view', viewId, segments: rest };
47
47
  }
48
48
  /**
49
49
  * The inverse of {@link matchStudioRoute}: build the `_splat` subpath for a
@@ -1 +1 @@
1
- {"version":3,"file":"match.js","sourceRoot":"","sources":["../../src/router/match.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAMH,yFAAyF;AACzF,SAAS,QAAQ,CAAC,KAAa;IAC7B,OAAO,KAAK;SACT,KAAK,CAAC,GAAG,CAAC;SACV,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC;SAChC,MAAM,CAAC,OAAO,CAAC,CAAA;AACpB,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAAa;IAC5C,MAAM,CAAC,QAAQ,EAAE,GAAG,IAAI,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAA;IAC3C,IAAI,QAAQ,KAAK,SAAS;QAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,CAAA;IACvD,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAA;AACxD,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG;IACxB,KAAK,EAAE,GAAG,EAAE,CAAC,EAAE;IACf,QAAQ,EAAE,GAAG,EAAE,CAAC,UAAU;IAC1B,MAAM,EAAE,GAAG,EAAE,CAAC,QAAQ;IACtB,MAAM,EAAE,GAAG,EAAE,CAAC,QAAQ;IACtB,UAAU,EAAE,GAAG,EAAE,CAAC,YAAY;IAC9B,OAAO,EAAE,GAAG,EAAE,CAAC,SAAS;IACxB,UAAU,EAAE,GAAG,EAAE,CAAC,YAAY;IAC9B,SAAS,EAAE,CAAC,WAAmB,EAAE,EAAE,CAAC,cAAc,WAAW,EAAE;IAC/D,GAAG,EAAE,GAAG,EAAE,CAAC,KAAK;IAChB,aAAa,EAAE,CAAC,YAAoB,EAAE,EAAE,CAAC,OAAO,YAAY,EAAE;IAC9D,WAAW,EAAE,CAAC,YAAoB,EAAE,UAAkB,EAAE,EAAE,CACxD,OAAO,YAAY,IAAI,UAAU,EAAE;CAC7B,CAAA","sourcesContent":["/**\n * Studio route table (EC-223 Stage 6 — catch-all routing). The studio mounts as\n * a SINGLE splat route per project (`/$projectId/$`); this pure matcher turns the\n * splat's trailing pathname into a typed match that the screen resolver renders.\n * Keeping the URL→screen map inside the engine (instead of in per-consumer route\n * files) is what lets the package own its navigation with a near-empty consumer\n * route tree — the goal of Stage 6.\n *\n * Deterministic and dependency-free: every screen state is reproducible from a\n * pathname string (see `match.test.ts`), satisfying the fixtures-reproducibility\n * convention without needing a router instance.\n *\n * EC-239 (app-view modularization): the route table is no longer a hardcoded\n * per-section switch — the studio's surfaces are a config-extendable module\n * registry, so the matcher is now registry-AGNOSTIC. It splits the splat into a\n * module id (the first segment) + that module's trailing segments; the screen\n * resolver looks the id up in the registry and the module's own `parsePath`\n * interprets the segments (CMS: collection/document; components: id; flat: none).\n * Keeping the matcher free of the surface list is what lets a consuming repo add\n * a routable custom module purely from config.\n *\n * /$projectId → { section: 'index' } (redirect to first module)\n * /$projectId/<id> → { section: 'module', moduleId: '<id>', segments: [] }\n * /$projectId/<id>/a/b → { section: 'module', moduleId: '<id>', segments: ['a','b'] }\n *\n * The module decides whether its segments resolve; an unknown id or an\n * over-deep path becomes a not-found at the resolver, not here.\n */\n\nexport type StudioRouteMatch =\n | { section: 'index' }\n | { section: 'module'; moduleId: string; segments: string[] }\n\n/** Split a splat pathname into clean segments (drops empty/leading/trailing slashes). */\nfunction segments(splat: string): string[] {\n return splat\n .split('/')\n .map((segment) => segment.trim())\n .filter(Boolean)\n}\n\n/**\n * Match the trailing pathname of the project splat (`_splat`) into a module id +\n * its segments. `splat` is everything after `/$projectId/` — e.g. `'cms/page/abc'`\n * for `/p1/cms/page/abc` → `{ moduleId: 'cms', segments: ['page', 'abc'] }`. An\n * empty splat is the bare project index (which redirects to the first module).\n */\nexport function matchStudioRoute(splat: string): StudioRouteMatch {\n const [moduleId, ...rest] = segments(splat)\n if (moduleId === undefined) return { section: 'index' }\n return { section: 'module', moduleId, segments: rest }\n}\n\n/**\n * The inverse of {@link matchStudioRoute}: build the `_splat` subpath for a\n * screen. Navigation addresses the single project splat as\n * `to={STUDIO_PROJECT_ROUTE} params={{ projectId, _splat: studioPath.X(...) }}`,\n * so the URL-construction lives in exactly one place — the same module that\n * parses it back. `matchStudioRoute(studioPath.cmsDocument(c, d))` round-trips\n * (pinned in `match.test.ts`), which is what keeps the two directions in sync.\n */\nexport const studioPath = {\n index: () => '',\n settings: () => 'settings',\n assets: () => 'assets',\n routes: () => 'routes',\n extensions: () => 'extensions',\n builder: () => 'builder',\n components: () => 'components',\n component: (componentId: string) => `components/${componentId}`,\n cms: () => 'cms',\n cmsCollection: (collectionId: string) => `cms/${collectionId}`,\n cmsDocument: (collectionId: string, documentId: string) =>\n `cms/${collectionId}/${documentId}`,\n} as const\n"]}
1
+ {"version":3,"file":"match.js","sourceRoot":"","sources":["../../src/router/match.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAMH,yFAAyF;AACzF,SAAS,QAAQ,CAAC,KAAa;IAC7B,OAAO,KAAK;SACT,KAAK,CAAC,GAAG,CAAC;SACV,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC;SAChC,MAAM,CAAC,OAAO,CAAC,CAAA;AACpB,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAAa;IAC5C,MAAM,CAAC,MAAM,EAAE,GAAG,IAAI,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAA;IACzC,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,CAAA;IACrD,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAA;AACpD,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG;IACxB,KAAK,EAAE,GAAG,EAAE,CAAC,EAAE;IACf,QAAQ,EAAE,GAAG,EAAE,CAAC,UAAU;IAC1B,MAAM,EAAE,GAAG,EAAE,CAAC,QAAQ;IACtB,MAAM,EAAE,GAAG,EAAE,CAAC,QAAQ;IACtB,UAAU,EAAE,GAAG,EAAE,CAAC,YAAY;IAC9B,OAAO,EAAE,GAAG,EAAE,CAAC,SAAS;IACxB,UAAU,EAAE,GAAG,EAAE,CAAC,YAAY;IAC9B,SAAS,EAAE,CAAC,WAAmB,EAAE,EAAE,CAAC,cAAc,WAAW,EAAE;IAC/D,GAAG,EAAE,GAAG,EAAE,CAAC,KAAK;IAChB,aAAa,EAAE,CAAC,YAAoB,EAAE,EAAE,CAAC,OAAO,YAAY,EAAE;IAC9D,WAAW,EAAE,CAAC,YAAoB,EAAE,UAAkB,EAAE,EAAE,CACxD,OAAO,YAAY,IAAI,UAAU,EAAE;CAC7B,CAAA","sourcesContent":["/**\n * Studio route table (EC-223 Stage 6 — catch-all routing). The studio mounts as\n * a SINGLE splat route per project (`/$projectId/$`); this pure matcher turns the\n * splat's trailing pathname into a typed match that the screen resolver renders.\n * Keeping the URL→screen map inside the engine (instead of in per-consumer route\n * files) is what lets the package own its navigation with a near-empty consumer\n * route tree — the goal of Stage 6.\n *\n * Deterministic and dependency-free: every screen state is reproducible from a\n * pathname string (see `match.test.ts`), satisfying the fixtures-reproducibility\n * convention without needing a router instance.\n *\n * EC-239 (app-view modularization): the route table is no longer a hardcoded\n * per-section switch — the studio's surfaces are a config-extendable VIEW registry\n * derived from the section rail, so the matcher is registry-AGNOSTIC. It splits the\n * splat into a view id (the first segment) + that view's trailing segments; the\n * screen resolver looks the id up in the registry and the view's own `parsePath`\n * interprets the segments (CMS: collection/document; components: id; flat: none).\n * Keeping the matcher free of the surface list is what lets a consuming repo add a\n * routable custom view purely from config.\n *\n * /$projectId → { section: 'index' } (redirect to first view)\n * /$projectId/<id> → { section: 'view', viewId: '<id>', segments: [] }\n * /$projectId/<id>/a/b → { section: 'view', viewId: '<id>', segments: ['a','b'] }\n *\n * The view decides whether its segments resolve; an unknown id or an over-deep\n * path becomes a not-found at the resolver, not here.\n */\n\nexport type StudioRouteMatch =\n | { section: 'index' }\n | { section: 'view'; viewId: string; segments: string[] }\n\n/** Split a splat pathname into clean segments (drops empty/leading/trailing slashes). */\nfunction segments(splat: string): string[] {\n return splat\n .split('/')\n .map((segment) => segment.trim())\n .filter(Boolean)\n}\n\n/**\n * Match the trailing pathname of the project splat (`_splat`) into a view id +\n * its segments. `splat` is everything after `/$projectId/` — e.g. `'cms/page/abc'`\n * for `/p1/cms/page/abc` → `{ viewId: 'cms', segments: ['page', 'abc'] }`. An\n * empty splat is the bare project index (which redirects to the first view).\n */\nexport function matchStudioRoute(splat: string): StudioRouteMatch {\n const [viewId, ...rest] = segments(splat)\n if (viewId === undefined) return { section: 'index' }\n return { section: 'view', viewId, segments: rest }\n}\n\n/**\n * The inverse of {@link matchStudioRoute}: build the `_splat` subpath for a\n * screen. Navigation addresses the single project splat as\n * `to={STUDIO_PROJECT_ROUTE} params={{ projectId, _splat: studioPath.X(...) }}`,\n * so the URL-construction lives in exactly one place — the same module that\n * parses it back. `matchStudioRoute(studioPath.cmsDocument(c, d))` round-trips\n * (pinned in `match.test.ts`), which is what keeps the two directions in sync.\n */\nexport const studioPath = {\n index: () => '',\n settings: () => 'settings',\n assets: () => 'assets',\n routes: () => 'routes',\n extensions: () => 'extensions',\n builder: () => 'builder',\n components: () => 'components',\n component: (componentId: string) => `components/${componentId}`,\n cms: () => 'cms',\n cmsCollection: (collectionId: string) => `cms/${collectionId}`,\n cmsDocument: (collectionId: string, documentId: string) =>\n `cms/${collectionId}/${documentId}`,\n} as const\n"]}
@@ -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" | "warning" | "default" | "success" | "primary" | "outline" | "info" | null | undefined;
4
+ variant?: "error" | "default" | "primary" | "outline" | "success" | "warning" | "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,13 +1,17 @@
1
1
  {
2
2
  "name": "@elytracms/studio",
3
3
  "author": "finaldream <hi@finaldream.de>",
4
- "version": "0.0.17",
4
+ "version": "0.0.19",
5
5
  "private": false,
6
6
  "description": "The Elytra studio engine (UI + lib + routes + data-only Convex schema) as an importable package. Extraction staged in EXTRACTION.md (EC-223); Stages 1-2 = UI primitives + router-free lib leaves.",
7
7
  "license": "MIT",
8
8
  "type": "module",
9
9
  "exports": {
10
10
  "./styles.css": "./src/styles/studio.css",
11
+ "./customization": {
12
+ "types": "./dist/customization.d.ts",
13
+ "default": "./dist/customization.js"
14
+ },
11
15
  "./ui": {
12
16
  "types": "./dist/ui/index.d.ts",
13
17
  "default": "./dist/ui/index.js"
@@ -113,7 +117,7 @@
113
117
  "lucide-react": "^1.17.0",
114
118
  "tailwind-merge": "^3.6.0",
115
119
  "zod": "^4.0.0",
116
- "@elytracms/core": "^0.0.17"
120
+ "@elytracms/core": "^0.0.19"
117
121
  },
118
122
  "peerDependencies": {
119
123
  "@tanstack/react-router": "^1.170.0",