@elytracms/studio 0.0.59 → 0.0.61
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/dist/components/ai/proposal-card.js +14 -23
- package/dist/components/ai/proposal-card.js.map +1 -1
- package/dist/components/app-views/studio-config.d.ts +34 -0
- package/dist/components/app-views/studio-config.js.map +1 -1
- package/dist/components/builder/canvas-chrome.d.ts +65 -0
- package/dist/components/builder/canvas-chrome.js +125 -0
- package/dist/components/builder/canvas-chrome.js.map +1 -0
- package/dist/components/builder/canvas.js +13 -91
- package/dist/components/builder/canvas.js.map +1 -1
- package/dist/components/cms/prop-fields.d.ts +15 -0
- package/dist/components/cms/prop-fields.js +38 -1
- package/dist/components/cms/prop-fields.js.map +1 -1
- package/dist/components/cms/rich-text/editor.d.ts +8 -0
- package/dist/components/cms/rich-text/editor.js +38 -3
- package/dist/components/cms/rich-text/editor.js.map +1 -1
- package/dist/components/cms/rich-text/toolbar.d.ts +16 -1
- package/dist/components/cms/rich-text/toolbar.js +15 -4
- package/dist/components/cms/rich-text/toolbar.js.map +1 -1
- package/dist/components/project-session.d.ts +31 -1
- package/dist/components/project-session.js +56 -22
- package/dist/components/project-session.js.map +1 -1
- package/dist/customization.d.ts +1 -1
- package/dist/customization.js.map +1 -1
- package/dist/lib/i18n/messages.en.js +5 -0
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/workspace/index.d.ts +8 -0
- package/dist/lib/workspace/index.js +10 -0
- package/dist/lib/workspace/index.js.map +1 -1
- package/dist/ui/expandable-surface.d.ts +48 -0
- package/dist/ui/expandable-surface.js +72 -0
- package/dist/ui/expandable-surface.js.map +1 -0
- package/dist/ui/index.d.ts +2 -0
- package/dist/ui/index.js +2 -0
- package/dist/ui/index.js.map +1 -1
- package/dist/ui/layer-stack.d.ts +36 -0
- package/dist/ui/layer-stack.js +42 -0
- package/dist/ui/layer-stack.js.map +1 -0
- package/package.json +2 -2
|
@@ -127,6 +127,14 @@ export declare function resolveDeploymentAi(): StudioConfig['ai'];
|
|
|
127
127
|
* resolution, never silent.
|
|
128
128
|
*/
|
|
129
129
|
export declare function resolveDeploymentSidebar(): SidebarNode[] | undefined;
|
|
130
|
+
/**
|
|
131
|
+
* The canvas page-surface theme from the studio config (EC-389): body classes +
|
|
132
|
+
* raw CSS for the preview iframe, so the canvas wears the SITE's chrome. Studio-only
|
|
133
|
+
* (from `defineStudio`, never the delivery bundle); `undefined` when none — the
|
|
134
|
+
* canvas keeps its built-in defaults (`bg-background text-foreground font-sans`
|
|
135
|
+
* body, kit `tokensCss` only). Studio-wide, like the rail and field renderers.
|
|
136
|
+
*/
|
|
137
|
+
export declare function resolveDeploymentCanvasTheme(): StudioConfig['canvas'];
|
|
130
138
|
/**
|
|
131
139
|
* The resolved URL routes of the active config project (EC-207, "routes as
|
|
132
140
|
* code"). Routes are config-sourced: the studio reads the materialized
|
|
@@ -196,6 +196,16 @@ export function resolveDeploymentAi() {
|
|
|
196
196
|
export function resolveDeploymentSidebar() {
|
|
197
197
|
return studioConfig?.sidebar;
|
|
198
198
|
}
|
|
199
|
+
/**
|
|
200
|
+
* The canvas page-surface theme from the studio config (EC-389): body classes +
|
|
201
|
+
* raw CSS for the preview iframe, so the canvas wears the SITE's chrome. Studio-only
|
|
202
|
+
* (from `defineStudio`, never the delivery bundle); `undefined` when none — the
|
|
203
|
+
* canvas keeps its built-in defaults (`bg-background text-foreground font-sans`
|
|
204
|
+
* body, kit `tokensCss` only). Studio-wide, like the rail and field renderers.
|
|
205
|
+
*/
|
|
206
|
+
export function resolveDeploymentCanvasTheme() {
|
|
207
|
+
return studioConfig?.canvas;
|
|
208
|
+
}
|
|
199
209
|
/**
|
|
200
210
|
* The resolved URL routes of the active config project (EC-207, "routes as
|
|
201
211
|
* code"). Routes are config-sourced: the studio reads the materialized
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/lib/workspace/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,yBAAyB,EAAE,MAAM,6BAA6B,CAAA;AASvE,OAAO,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAA;AAC9D,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAA;AAG1D,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAA;AAE1D,OAAO,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAA;AAGpG,IAAI,MAAM,GAA0B,IAAI,CAAA;AACxC,IAAI,YAAY,GAAwB,IAAI,CAAA;AA8B5C,MAAM,UAAU,YAAY,CAC1B,MAAyB,EACzB,MAAqB,EACrB,OAA4B;IAE5B,gBAAgB,CAAC,OAAO,EAAE,GAAG,CAAC,CAAA;IAC9B,MAAM,GAAG,qBAAqB,CAAC;QAC7B,UAAU,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE;QAC/B,QAAQ,EAAE,kBAAkB;KAC7B,CAAC,CAAA;IACF,YAAY,GAAG,MAAM,IAAI,IAAI,CAAA;AAC/B,CAAC;AAED,oFAAoF;AACpF,MAAM,UAAU,uBAAuB,CAAC,IAAyB;IAC/D,YAAY,GAAG,IAAI,CAAA;AACrB,CAAC;AAED;;;;;;;;;GASG;AACH,IAAI,iBAAiB,GAAkB,IAAI,CAAA;AAE3C,oFAAoF;AACpF,MAAM,UAAU,oBAAoB,CAAC,IAAmB;IACtD,MAAM,MAAM,GAAG,oBAAoB,CAAC,EAAE,CAAC,CAAA;IACvC,iBAAiB,GAAG,IAAI,IAAI,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAA;IAC5D,OAAO,oBAAoB,CAAC,EAAE,CAAC,KAAK,MAAM,CAAA;AAC5C,CAAC;AAED,MAAM,UAAU,oBAAoB;IAClC,OAAO,iBAAiB,CAAA;AAC1B,CAAC;AAED,wEAAwE;AACxE,MAAM,UAAU,4BAA4B,CAAC,IAAmB;IAC9D,iBAAiB,GAAG,IAAI,CAAA;AAC1B,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,sBAAsB,GAAwB,IAAI,GAAG,CAAC,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC,CAAA;AAEtF,8EAA8E;AAC9E,MAAM,UAAU,mBAAmB,CAAC,QAAgB;IAClD,MAAM,KAAK,GAAG,cAAc,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;IAC3C,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAA;IACvB,MAAM,OAAO,GAAG,kBAAkB,CAAC,KAAK,CAAC,CAAC,CAAE,CAAC,CAAA;IAC7C,OAAO,sBAAsB,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAA;AAC7D,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,qBAAqB,CAAC,QAAgB,EAAE,QAAiB;IACvE,MAAM,MAAM,GAAG,mBAAmB,CAAC,QAAQ,CAAC,CAAA;IAC5C,IAAI,MAAM;QAAE,OAAO,MAAM,CAAA;IACzB,IAAI,QAAQ,KAAK,UAAU,IAAI,QAAQ;QAAE,OAAO,mBAAmB,CAAC,QAAQ,CAAC,CAAA;IAC7E,OAAO,IAAI,CAAA;AACb,CAAC;AAED,4EAA4E;AAC5E,SAAS,mBAAmB;IAC1B,MAAM,KAAK,GAAG,iBAAiB,EAAE,CAAA;IACjC,IAAI,KAAK,CAAC,IAAI,KAAK,QAAQ;QAAE,OAAO,SAAS,CAAA;IAC7C,IAAI,iBAAiB,EAAE,CAAC;QACtB,MAAM,KAAK,GAAG,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,iBAAiB,CAAC,CAAA;QACtE,IAAI,KAAK;YAAE,OAAO,KAAK,CAAA;IACzB,CAAC;IACD,OAAO,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAA;AAC1B,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB;IAC/B,gFAAgF;IAChF,mFAAmF;IACnF,yDAAyD;IACzD,IAAI,CAAC,MAAM;QAAE,MAAM,GAAG,qBAAqB,CAAC,EAAE,CAAC,CAAA;IAC/C,OAAO,MAAM,CAAA;AACf,CAAC;AAED,yEAAyE;AACzE,MAAM,UAAU,yBAAyB,CAAC,IAA2B;IACnE,MAAM,GAAG,IAAI,CAAA;AACf,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,oBAAoB,CAAC,WAAmB;IACtD,OAAO,oBAAoB,CAAC,mBAAmB,EAAE,EAAE,WAAW,CAAC,CAAA;AACjE,CAAC;AAED,gFAAgF;AAChF,MAAM,UAAU,oBAAoB,CAAC,IAAY,EAAE,WAAmB;IACpE,MAAM,KAAK,GAAG,iBAAiB,EAAE,CAAA;IACjC,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;IACjG,OAAO,oBAAoB,CAAC,OAAO,EAAE,WAAW,CAAC,CAAA;AACnD,CAAC;AAED,sFAAsF;AACtF,SAAS,oBAAoB,CAC3B,OAA6C,EAC7C,WAAmB;IAEnB,IAAI,CAAC,OAAO;QAAE,OAAO,WAAW,CAAA;IAChC,MAAM,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,EAAE,UAAU,CAAA;IAChG,OAAO,aAAa,IAAI,OAAO,CAAC,MAAM,EAAE,UAAU,IAAI,WAAW,CAAA;AACnE,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,0BAA0B,CAAC,UAAkB;IAC3D,OAAO,mBAAmB,EAAE,EAAE,IAAI,IAAI,UAAU,CAAA;AAClD,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,4BAA4B;IAC1C,OAAO,mBAAmB,EAAE,EAAE,WAAW,IAAI,EAAE,CAAA;AACjD,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,4BAA4B;IAC1C,OAAO,mBAAmB,EAAE,EAAE,WAAW,CAAA;AAC3C,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,+BAA+B;IAC7C,OAAO,YAAY,EAAE,cAAc,CAAA;AACrC,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,qBAAqB;IACnC,OAAO,YAAY,EAAE,IAAI,CAAA;AAC3B,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,mBAAmB;IACjC,OAAO,YAAY,EAAE,EAAE,CAAA;AACzB,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,wBAAwB;IACtC,OAAO,YAAY,EAAE,OAAO,CAAA;AAC9B,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,uBAAuB;IACrC,OAAO,mBAAmB,EAAE,EAAE,MAAM,IAAI,EAAE,CAAA;AAC5C,CAAC;AAED,kFAAkF;AAClF,wFAAwF;AACxF,sFAAsF;AAEtF;;;;;GAKG;AACH,MAAM,UAAU,wBAAwB;IACtC,OAAO,mBAAmB,EAAE,EAAE,OAAO,IAAI,EAAE,CAAA;AAC7C,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,6BAA6B;IAC3C,MAAM,OAAO,GAAG,mBAAmB,EAAE,CAAA;IACrC,IAAI,CAAC,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,SAAS,CAAA;IAC9D,MAAM,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC,CAAE,CAAA;IACpC,MAAM,QAAQ,GAAG,OAAO,CAAC,aAAa,CAAA;IACtC,OAAO;QACL,OAAO,EAAE,QAAQ,IAAI,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ;QAC7E,OAAO,EAAE,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC;KAC9B,CAAA;AACH,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,2BAA2B;IACzC,OAAO,mBAAmB,EAAE,EAAE,UAAU,CAAA;AAC1C,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,4BAA4B;IAC1C,MAAM,OAAO,GAAG,mBAAmB,EAAE,CAAA;IACrC,IAAI,CAAC,OAAO;QAAE,OAAO,SAAS,CAAA;IAC9B,MAAM,QAAQ,GAAG,OAAO,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,EAAE,UAAU,CAAC,CAAA;IACvE,OAAO,yBAAyB,CAAC,OAAO,EAAE,QAAQ,EAAE,GAAG,CAAC,CAAA;AAC1D,CAAC","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, LocaleConfig, 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 { provideStudioEnv } from '../persistence/backend-mode'\nimport { resolveWorkspaceState } from './workspace-source'\nimport type { WorkspaceState } from './workspace-source'\n\nexport { resolveWorkspaceState } 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 *\n * EC-357 — `options.env`: the consumer's `import.meta.env`, captured HERE because\n * the entry module is code Vite always transforms live. The engine's own\n * `import.meta.env` reads are unreliable inside dep-optimized package code (Vite ≥8\n * omits custom `VITE_*` values there; Vite 7 froze them at optimize time), which\n * silently broke backend URL / mode / dev-login resolution in real consumers.\n * Always pass it: `createStudio(config, studioConfig, { env: import.meta.env })`.\n */\nexport interface StudioMountOptions {\n /** The consumer entry's `import.meta.env` (see `provideStudioEnv` for the contract). */\n env?: Record<string, unknown>\n}\n\nexport function createStudio(\n config: ElytraConfigInput,\n studio?: StudioConfig,\n options?: StudioMountOptions,\n): void {\n provideStudioEnv(options?.env)\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 studio-UI localization config from the studio config (EC-241): the default\n * locale + imported translation catalogs. Studio-only live code (from `defineStudio`,\n * never the project schema or the delivery bundle); `undefined` when none — the\n * studio then renders its built-in English chrome. Studio-wide, like the rail and\n * field renderers (localizes the studio, not one backend).\n */\nexport function resolveDeploymentI18n(): StudioConfig['i18n'] {\n return studioConfig?.i18n\n}\n\n/**\n * The AI platform config from the studio config (EC-331): provider, knowledge, and\n * limits. Studio-only live code (from `defineStudio`, never the delivery bundle);\n * `undefined` when no `ai` block is configured — the feature is then fully absent\n * (the AI subsystem never boots, so `@elytracms/studio/lib/ai`'s runtime — and the\n * `ai`/`@ai-sdk/*` SDK behind it — is never imported). Studio-wide, like the rail\n * and field renderers.\n */\nexport function resolveDeploymentAi(): StudioConfig['ai'] {\n return studioConfig?.ai\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// Redirects are editorial DATA now (EC-303), managed in the studio Redirects rail\n// and served dynamically — no longer resolved from config. `resolveDeploymentRedirects`\n// is retired; static/infra redirects are the dev's job via native next.config/vercel.\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 config project's locales as a `LocaleConfig` (EC-357 finding #3).\n * Locales are structure and structure is code: when `elytra.config.ts` declares\n * them, they drive the studio's ambient locale switcher in EVERY mode — the CMS\n * surface must not fall back to the sample `en/de` fixture (playground, or a\n * connected first run with nothing persisted) when the config says otherwise.\n * `undefined` when no config project is active or it declares no locales (env-only\n * wiring keeps the existing kit/sample fallbacks). A missing/invalid explicit\n * default falls back to the first declared locale.\n */\nexport function resolveDeploymentLocaleConfig(): LocaleConfig | undefined {\n const project = activeConfigProject()\n if (!project || project.locales.length === 0) return undefined\n const fallback = project.locales[0]!\n const explicit = project.defaultLocale\n return {\n default: explicit && project.locales.includes(explicit) ? explicit : fallback,\n locales: [...project.locales],\n }\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, LocaleConfig, 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 { provideStudioEnv } from '../persistence/backend-mode'\nimport { resolveWorkspaceState } from './workspace-source'\nimport type { WorkspaceState } from './workspace-source'\n\nexport { resolveWorkspaceState } 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 *\n * EC-357 — `options.env`: the consumer's `import.meta.env`, captured HERE because\n * the entry module is code Vite always transforms live. The engine's own\n * `import.meta.env` reads are unreliable inside dep-optimized package code (Vite ≥8\n * omits custom `VITE_*` values there; Vite 7 froze them at optimize time), which\n * silently broke backend URL / mode / dev-login resolution in real consumers.\n * Always pass it: `createStudio(config, studioConfig, { env: import.meta.env })`.\n */\nexport interface StudioMountOptions {\n /** The consumer entry's `import.meta.env` (see `provideStudioEnv` for the contract). */\n env?: Record<string, unknown>\n}\n\nexport function createStudio(\n config: ElytraConfigInput,\n studio?: StudioConfig,\n options?: StudioMountOptions,\n): void {\n provideStudioEnv(options?.env)\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 studio-UI localization config from the studio config (EC-241): the default\n * locale + imported translation catalogs. Studio-only live code (from `defineStudio`,\n * never the project schema or the delivery bundle); `undefined` when none — the\n * studio then renders its built-in English chrome. Studio-wide, like the rail and\n * field renderers (localizes the studio, not one backend).\n */\nexport function resolveDeploymentI18n(): StudioConfig['i18n'] {\n return studioConfig?.i18n\n}\n\n/**\n * The AI platform config from the studio config (EC-331): provider, knowledge, and\n * limits. Studio-only live code (from `defineStudio`, never the delivery bundle);\n * `undefined` when no `ai` block is configured — the feature is then fully absent\n * (the AI subsystem never boots, so `@elytracms/studio/lib/ai`'s runtime — and the\n * `ai`/`@ai-sdk/*` SDK behind it — is never imported). Studio-wide, like the rail\n * and field renderers.\n */\nexport function resolveDeploymentAi(): StudioConfig['ai'] {\n return studioConfig?.ai\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 canvas page-surface theme from the studio config (EC-389): body classes +\n * raw CSS for the preview iframe, so the canvas wears the SITE's chrome. Studio-only\n * (from `defineStudio`, never the delivery bundle); `undefined` when none — the\n * canvas keeps its built-in defaults (`bg-background text-foreground font-sans`\n * body, kit `tokensCss` only). Studio-wide, like the rail and field renderers.\n */\nexport function resolveDeploymentCanvasTheme(): StudioConfig['canvas'] {\n return studioConfig?.canvas\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// Redirects are editorial DATA now (EC-303), managed in the studio Redirects rail\n// and served dynamically — no longer resolved from config. `resolveDeploymentRedirects`\n// is retired; static/infra redirects are the dev's job via native next.config/vercel.\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 config project's locales as a `LocaleConfig` (EC-357 finding #3).\n * Locales are structure and structure is code: when `elytra.config.ts` declares\n * them, they drive the studio's ambient locale switcher in EVERY mode — the CMS\n * surface must not fall back to the sample `en/de` fixture (playground, or a\n * connected first run with nothing persisted) when the config says otherwise.\n * `undefined` when no config project is active or it declares no locales (env-only\n * wiring keeps the existing kit/sample fallbacks). A missing/invalid explicit\n * default falls back to the first declared locale.\n */\nexport function resolveDeploymentLocaleConfig(): LocaleConfig | undefined {\n const project = activeConfigProject()\n if (!project || project.locales.length === 0) return undefined\n const fallback = project.locales[0]!\n const explicit = project.defaultLocale\n return {\n default: explicit && project.locales.includes(explicit) ? explicit : fallback,\n locales: [...project.locales],\n }\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"]}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
export interface ExpandableSurfaceProps {
|
|
3
|
+
/** Heading text; doubles as the sheet's `aria-label`. */
|
|
4
|
+
title: string;
|
|
5
|
+
/** Accessible label + tooltip for the header close button (caller-translated). */
|
|
6
|
+
closeLabel: string;
|
|
7
|
+
/**
|
|
8
|
+
* CSS width of the sheet panel — callers keep their shipped widths
|
|
9
|
+
* (e.g. `min(820px, 92vw)` rich text, `min(720px, 92vw)` proposal review).
|
|
10
|
+
*/
|
|
11
|
+
width: string;
|
|
12
|
+
/**
|
|
13
|
+
* Dismiss request: Escape (only while this surface is the topmost layer),
|
|
14
|
+
* backdrop click, or the header close button. The caller owns the open state.
|
|
15
|
+
*/
|
|
16
|
+
onDismiss: () => void;
|
|
17
|
+
/** Optional leading header adornment (an icon) rendered before the title. */
|
|
18
|
+
headerLeading?: ReactNode;
|
|
19
|
+
/** Everything below the header. Callers own their scroll areas and footers. */
|
|
20
|
+
children: ReactNode;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* ExpandableSurface (EC-392) — the tier-2 answer to "an inline surface outgrows
|
|
24
|
+
* its slot": the SAME surface grown into a roomy, full-height right-anchored
|
|
25
|
+
* sheet (rich-text expand editing EC-388, AI proposal review EC-382; future:
|
|
26
|
+
* object arrays, JSON/code fields). It is never a second form — no save/cancel
|
|
27
|
+
* semantics of its own (forms-never-modal) — and stays lightly dismissible:
|
|
28
|
+
* Escape, backdrop click, header close.
|
|
29
|
+
*
|
|
30
|
+
* Rendered through a `createPortal` to `document.body` so it escapes narrow
|
|
31
|
+
* inspectors/sidebars and any transformed ancestor; without a DOM (the
|
|
32
|
+
* `renderToStaticMarkup` smoke path) it renders `null`, so callers keep their
|
|
33
|
+
* sheets closed by default on SSR paths.
|
|
34
|
+
*
|
|
35
|
+
* Stacking (ratified semantics): all instances register on the shared layer
|
|
36
|
+
* stack; only the TOPMOST layer acts on Escape (backdrop ownership falls out of
|
|
37
|
+
* paint order — the topmost backdrop covers everything below it). Foreign layers
|
|
38
|
+
* above (the immediate case: the asset picker's Radix dialog opened from the
|
|
39
|
+
* expanded rich-text toolbar) are bridged via the event itself: Radix consumes
|
|
40
|
+
* Escape on the document capture phase and marks it `defaultPrevented`, which
|
|
41
|
+
* this surface treats as "handled above me" — so Escape closes the picker first
|
|
42
|
+
* and the sheet only on the next press.
|
|
43
|
+
*
|
|
44
|
+
* Motion: the sheet deliberately has NO open/close transition today, so there is
|
|
45
|
+
* nothing to gate on `prefers-reduced-motion` — if a transition is ever added it
|
|
46
|
+
* must be wrapped in `motion-safe:`.
|
|
47
|
+
*/
|
|
48
|
+
export declare function ExpandableSurface({ title, closeLabel, width, onDismiss, headerLeading, children, }: ExpandableSurfaceProps): import("react").ReactPortal | null;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef } from 'react';
|
|
3
|
+
import { createPortal } from 'react-dom';
|
|
4
|
+
import { X } from 'lucide-react';
|
|
5
|
+
import { Button } from './button.js';
|
|
6
|
+
import { Heading } from './typography.js';
|
|
7
|
+
import { Row, Spacer } from './layout.js';
|
|
8
|
+
import { createLayerStack } from './layer-stack.js';
|
|
9
|
+
/**
|
|
10
|
+
* The ONE shared open-layer stack all {@link ExpandableSurface} instances
|
|
11
|
+
* coordinate through. The surfaces themselves are instance-per-caller (ratified:
|
|
12
|
+
* no singleton/global sheet slot — concurrent sheets from different features are
|
|
13
|
+
* legal); only the *ordering* is shared, so that the topmost open sheet owns
|
|
14
|
+
* Escape while the ones below stay open.
|
|
15
|
+
*/
|
|
16
|
+
const surfaceLayers = createLayerStack();
|
|
17
|
+
/**
|
|
18
|
+
* ExpandableSurface (EC-392) — the tier-2 answer to "an inline surface outgrows
|
|
19
|
+
* its slot": the SAME surface grown into a roomy, full-height right-anchored
|
|
20
|
+
* sheet (rich-text expand editing EC-388, AI proposal review EC-382; future:
|
|
21
|
+
* object arrays, JSON/code fields). It is never a second form — no save/cancel
|
|
22
|
+
* semantics of its own (forms-never-modal) — and stays lightly dismissible:
|
|
23
|
+
* Escape, backdrop click, header close.
|
|
24
|
+
*
|
|
25
|
+
* Rendered through a `createPortal` to `document.body` so it escapes narrow
|
|
26
|
+
* inspectors/sidebars and any transformed ancestor; without a DOM (the
|
|
27
|
+
* `renderToStaticMarkup` smoke path) it renders `null`, so callers keep their
|
|
28
|
+
* sheets closed by default on SSR paths.
|
|
29
|
+
*
|
|
30
|
+
* Stacking (ratified semantics): all instances register on the shared layer
|
|
31
|
+
* stack; only the TOPMOST layer acts on Escape (backdrop ownership falls out of
|
|
32
|
+
* paint order — the topmost backdrop covers everything below it). Foreign layers
|
|
33
|
+
* above (the immediate case: the asset picker's Radix dialog opened from the
|
|
34
|
+
* expanded rich-text toolbar) are bridged via the event itself: Radix consumes
|
|
35
|
+
* Escape on the document capture phase and marks it `defaultPrevented`, which
|
|
36
|
+
* this surface treats as "handled above me" — so Escape closes the picker first
|
|
37
|
+
* and the sheet only on the next press.
|
|
38
|
+
*
|
|
39
|
+
* Motion: the sheet deliberately has NO open/close transition today, so there is
|
|
40
|
+
* nothing to gate on `prefers-reduced-motion` — if a transition is ever added it
|
|
41
|
+
* must be wrapped in `motion-safe:`.
|
|
42
|
+
*/
|
|
43
|
+
export function ExpandableSurface({ title, closeLabel, width, onDismiss, headerLeading, children, }) {
|
|
44
|
+
// Latest dismiss handler, read inside the once-per-mount listener.
|
|
45
|
+
const onDismissRef = useRef(onDismiss);
|
|
46
|
+
onDismissRef.current = onDismiss;
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
const layer = surfaceLayers.push();
|
|
49
|
+
const onKey = (event) => {
|
|
50
|
+
if (event.key !== 'Escape')
|
|
51
|
+
return;
|
|
52
|
+
// A layer above already consumed this Escape — a Radix dialog (asset
|
|
53
|
+
// picker) preventDefaults on dismiss, and a sibling surface that handled
|
|
54
|
+
// it marks it the same way. Layers below stay open.
|
|
55
|
+
if (event.defaultPrevented)
|
|
56
|
+
return;
|
|
57
|
+
if (!surfaceLayers.isTop(layer))
|
|
58
|
+
return;
|
|
59
|
+
event.preventDefault();
|
|
60
|
+
onDismissRef.current();
|
|
61
|
+
};
|
|
62
|
+
window.addEventListener('keydown', onKey);
|
|
63
|
+
return () => {
|
|
64
|
+
window.removeEventListener('keydown', onKey);
|
|
65
|
+
surfaceLayers.remove(layer);
|
|
66
|
+
};
|
|
67
|
+
}, []);
|
|
68
|
+
if (typeof document === 'undefined')
|
|
69
|
+
return null;
|
|
70
|
+
return createPortal(_jsxs("div", { className: "fixed inset-0 z-50 flex", children: [_jsx("div", { className: "flex-1 bg-background/70 backdrop-blur-sm", "aria-hidden": true, onClick: onDismiss }), _jsxs("aside", { role: "dialog", "aria-label": title, style: { width }, className: "flex h-full flex-col border-l border-border bg-card shadow-2xl", children: [_jsxs(Row, { gap: "2", align: "center", className: "h-12 shrink-0 border-b border-border px-4", children: [headerLeading, _jsx(Heading, { level: 2, size: "h4", className: "min-w-0 truncate", children: title }), _jsx(Spacer, {}), _jsx(Button, { size: "icon-sm", variant: "ghost", onClick: onDismiss, title: closeLabel, "aria-label": closeLabel, children: _jsx(X, {}) })] }), children] })] }), document.body);
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=expandable-surface.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"expandable-surface.js","sourceRoot":"","sources":["../../src/ui/expandable-surface.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,OAAO,CAAA;AAEzC,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA;AACxC,OAAO,EAAE,CAAC,EAAE,MAAM,cAAc,CAAA;AAChC,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AACjC,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AACtC,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AACtC,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAA;AAEhD;;;;;;GAMG;AACH,MAAM,aAAa,GAAG,gBAAgB,EAAE,CAAA;AAuBxC;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,UAAU,iBAAiB,CAAC,EAChC,KAAK,EACL,UAAU,EACV,KAAK,EACL,SAAS,EACT,aAAa,EACb,QAAQ,GACe;IACvB,mEAAmE;IACnE,MAAM,YAAY,GAAG,MAAM,CAAC,SAAS,CAAC,CAAA;IACtC,YAAY,CAAC,OAAO,GAAG,SAAS,CAAA;IAEhC,SAAS,CAAC,GAAG,EAAE;QACb,MAAM,KAAK,GAAG,aAAa,CAAC,IAAI,EAAE,CAAA;QAClC,MAAM,KAAK,GAAG,CAAC,KAAoB,EAAE,EAAE;YACrC,IAAI,KAAK,CAAC,GAAG,KAAK,QAAQ;gBAAE,OAAM;YAClC,qEAAqE;YACrE,yEAAyE;YACzE,oDAAoD;YACpD,IAAI,KAAK,CAAC,gBAAgB;gBAAE,OAAM;YAClC,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,KAAK,CAAC;gBAAE,OAAM;YACvC,KAAK,CAAC,cAAc,EAAE,CAAA;YACtB,YAAY,CAAC,OAAO,EAAE,CAAA;QACxB,CAAC,CAAA;QACD,MAAM,CAAC,gBAAgB,CAAC,SAAS,EAAE,KAAK,CAAC,CAAA;QACzC,OAAO,GAAG,EAAE;YACV,MAAM,CAAC,mBAAmB,CAAC,SAAS,EAAE,KAAK,CAAC,CAAA;YAC5C,aAAa,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;QAC7B,CAAC,CAAA;IACH,CAAC,EAAE,EAAE,CAAC,CAAA;IAEN,IAAI,OAAO,QAAQ,KAAK,WAAW;QAAE,OAAO,IAAI,CAAA;IAEhD,OAAO,YAAY,CACjB,eAAK,SAAS,EAAC,yBAAyB,aACtC,cAAK,SAAS,EAAC,0CAA0C,uBAAa,OAAO,EAAE,SAAS,GAAI,EAC5F,iBACE,IAAI,EAAC,QAAQ,gBACD,KAAK,EACjB,KAAK,EAAE,EAAE,KAAK,EAAE,EAChB,SAAS,EAAC,gEAAgE,aAE1E,MAAC,GAAG,IAAC,GAAG,EAAC,GAAG,EAAC,KAAK,EAAC,QAAQ,EAAC,SAAS,EAAC,2CAA2C,aAC9E,aAAa,EACd,KAAC,OAAO,IAAC,KAAK,EAAE,CAAC,EAAE,IAAI,EAAC,IAAI,EAAC,SAAS,EAAC,kBAAkB,YACtD,KAAK,GACE,EACV,KAAC,MAAM,KAAG,EACV,KAAC,MAAM,IACL,IAAI,EAAC,SAAS,EACd,OAAO,EAAC,OAAO,EACf,OAAO,EAAE,SAAS,EAClB,KAAK,EAAE,UAAU,gBACL,UAAU,YAEtB,KAAC,CAAC,KAAG,GACE,IACL,EACL,QAAQ,IACH,IACJ,EACN,QAAQ,CAAC,IAAI,CACd,CAAA;AACH,CAAC","sourcesContent":["import { useEffect, useRef } from 'react'\nimport type { ReactNode } from 'react'\nimport { createPortal } from 'react-dom'\nimport { X } from 'lucide-react'\nimport { Button } from './button'\nimport { Heading } from './typography'\nimport { Row, Spacer } from './layout'\nimport { createLayerStack } from './layer-stack'\n\n/**\n * The ONE shared open-layer stack all {@link ExpandableSurface} instances\n * coordinate through. The surfaces themselves are instance-per-caller (ratified:\n * no singleton/global sheet slot — concurrent sheets from different features are\n * legal); only the *ordering* is shared, so that the topmost open sheet owns\n * Escape while the ones below stay open.\n */\nconst surfaceLayers = createLayerStack()\n\nexport interface ExpandableSurfaceProps {\n /** Heading text; doubles as the sheet's `aria-label`. */\n title: string\n /** Accessible label + tooltip for the header close button (caller-translated). */\n closeLabel: string\n /**\n * CSS width of the sheet panel — callers keep their shipped widths\n * (e.g. `min(820px, 92vw)` rich text, `min(720px, 92vw)` proposal review).\n */\n width: string\n /**\n * Dismiss request: Escape (only while this surface is the topmost layer),\n * backdrop click, or the header close button. The caller owns the open state.\n */\n onDismiss: () => void\n /** Optional leading header adornment (an icon) rendered before the title. */\n headerLeading?: ReactNode\n /** Everything below the header. Callers own their scroll areas and footers. */\n children: ReactNode\n}\n\n/**\n * ExpandableSurface (EC-392) — the tier-2 answer to \"an inline surface outgrows\n * its slot\": the SAME surface grown into a roomy, full-height right-anchored\n * sheet (rich-text expand editing EC-388, AI proposal review EC-382; future:\n * object arrays, JSON/code fields). It is never a second form — no save/cancel\n * semantics of its own (forms-never-modal) — and stays lightly dismissible:\n * Escape, backdrop click, header close.\n *\n * Rendered through a `createPortal` to `document.body` so it escapes narrow\n * inspectors/sidebars and any transformed ancestor; without a DOM (the\n * `renderToStaticMarkup` smoke path) it renders `null`, so callers keep their\n * sheets closed by default on SSR paths.\n *\n * Stacking (ratified semantics): all instances register on the shared layer\n * stack; only the TOPMOST layer acts on Escape (backdrop ownership falls out of\n * paint order — the topmost backdrop covers everything below it). Foreign layers\n * above (the immediate case: the asset picker's Radix dialog opened from the\n * expanded rich-text toolbar) are bridged via the event itself: Radix consumes\n * Escape on the document capture phase and marks it `defaultPrevented`, which\n * this surface treats as \"handled above me\" — so Escape closes the picker first\n * and the sheet only on the next press.\n *\n * Motion: the sheet deliberately has NO open/close transition today, so there is\n * nothing to gate on `prefers-reduced-motion` — if a transition is ever added it\n * must be wrapped in `motion-safe:`.\n */\nexport function ExpandableSurface({\n title,\n closeLabel,\n width,\n onDismiss,\n headerLeading,\n children,\n}: ExpandableSurfaceProps) {\n // Latest dismiss handler, read inside the once-per-mount listener.\n const onDismissRef = useRef(onDismiss)\n onDismissRef.current = onDismiss\n\n useEffect(() => {\n const layer = surfaceLayers.push()\n const onKey = (event: KeyboardEvent) => {\n if (event.key !== 'Escape') return\n // A layer above already consumed this Escape — a Radix dialog (asset\n // picker) preventDefaults on dismiss, and a sibling surface that handled\n // it marks it the same way. Layers below stay open.\n if (event.defaultPrevented) return\n if (!surfaceLayers.isTop(layer)) return\n event.preventDefault()\n onDismissRef.current()\n }\n window.addEventListener('keydown', onKey)\n return () => {\n window.removeEventListener('keydown', onKey)\n surfaceLayers.remove(layer)\n }\n }, [])\n\n if (typeof document === 'undefined') return null\n\n return createPortal(\n <div className=\"fixed inset-0 z-50 flex\">\n <div className=\"flex-1 bg-background/70 backdrop-blur-sm\" aria-hidden onClick={onDismiss} />\n <aside\n role=\"dialog\"\n aria-label={title}\n style={{ width }}\n className=\"flex h-full flex-col border-l border-border bg-card shadow-2xl\"\n >\n <Row gap=\"2\" align=\"center\" className=\"h-12 shrink-0 border-b border-border px-4\">\n {headerLeading}\n <Heading level={2} size=\"h4\" className=\"min-w-0 truncate\">\n {title}\n </Heading>\n <Spacer />\n <Button\n size=\"icon-sm\"\n variant=\"ghost\"\n onClick={onDismiss}\n title={closeLabel}\n aria-label={closeLabel}\n >\n <X />\n </Button>\n </Row>\n {children}\n </aside>\n </div>,\n document.body,\n )\n}\n"]}
|
package/dist/ui/index.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ export * from './collapsible-section.js';
|
|
|
11
11
|
export * from './separator.js';
|
|
12
12
|
export * from './toolbar.js';
|
|
13
13
|
export * from './inspector.js';
|
|
14
|
+
export * from './expandable-surface.js';
|
|
15
|
+
export * from './layer-stack.js';
|
|
14
16
|
export * from './dialog.js';
|
|
15
17
|
export * from './dropdown-menu.js';
|
|
16
18
|
export * from './popover.js';
|
package/dist/ui/index.js
CHANGED
|
@@ -11,6 +11,8 @@ export * from './collapsible-section.js';
|
|
|
11
11
|
export * from './separator.js';
|
|
12
12
|
export * from './toolbar.js';
|
|
13
13
|
export * from './inspector.js';
|
|
14
|
+
export * from './expandable-surface.js';
|
|
15
|
+
export * from './layer-stack.js';
|
|
14
16
|
export * from './dialog.js';
|
|
15
17
|
export * from './dropdown-menu.js';
|
|
16
18
|
export * from './popover.js';
|
package/dist/ui/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/ui/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAA;AAC5B,cAAc,UAAU,CAAA;AACxB,cAAc,UAAU,CAAA;AACxB,cAAc,UAAU,CAAA;AACxB,cAAc,SAAS,CAAA;AACvB,cAAc,SAAS,CAAA;AACvB,cAAc,QAAQ,CAAA;AACtB,cAAc,SAAS,CAAA;AACvB,cAAc,YAAY,CAAA;AAC1B,cAAc,uBAAuB,CAAA;AACrC,cAAc,aAAa,CAAA;AAC3B,cAAc,WAAW,CAAA;AACzB,cAAc,aAAa,CAAA;AAC3B,cAAc,UAAU,CAAA;AACxB,cAAc,iBAAiB,CAAA;AAC/B,cAAc,WAAW,CAAA;AACzB,cAAc,SAAS,CAAA;AACvB,cAAc,SAAS,CAAA;AACvB,cAAc,qBAAqB,CAAA","sourcesContent":["export * from './typography'\nexport * from './layout'\nexport * from './avatar'\nexport * from './button'\nexport * from './input'\nexport * from './badge'\nexport * from './card'\nexport * from './panel'\nexport * from './row-item'\nexport * from './collapsible-section'\nexport * from './separator'\nexport * from './toolbar'\nexport * from './inspector'\nexport * from './dialog'\nexport * from './dropdown-menu'\nexport * from './popover'\nexport * from './field'\nexport * from './toast'\nexport * from './segmented-control'\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/ui/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAA;AAC5B,cAAc,UAAU,CAAA;AACxB,cAAc,UAAU,CAAA;AACxB,cAAc,UAAU,CAAA;AACxB,cAAc,SAAS,CAAA;AACvB,cAAc,SAAS,CAAA;AACvB,cAAc,QAAQ,CAAA;AACtB,cAAc,SAAS,CAAA;AACvB,cAAc,YAAY,CAAA;AAC1B,cAAc,uBAAuB,CAAA;AACrC,cAAc,aAAa,CAAA;AAC3B,cAAc,WAAW,CAAA;AACzB,cAAc,aAAa,CAAA;AAC3B,cAAc,sBAAsB,CAAA;AACpC,cAAc,eAAe,CAAA;AAC7B,cAAc,UAAU,CAAA;AACxB,cAAc,iBAAiB,CAAA;AAC/B,cAAc,WAAW,CAAA;AACzB,cAAc,SAAS,CAAA;AACvB,cAAc,SAAS,CAAA;AACvB,cAAc,qBAAqB,CAAA","sourcesContent":["export * from './typography'\nexport * from './layout'\nexport * from './avatar'\nexport * from './button'\nexport * from './input'\nexport * from './badge'\nexport * from './card'\nexport * from './panel'\nexport * from './row-item'\nexport * from './collapsible-section'\nexport * from './separator'\nexport * from './toolbar'\nexport * from './inspector'\nexport * from './expandable-surface'\nexport * from './layer-stack'\nexport * from './dialog'\nexport * from './dropdown-menu'\nexport * from './popover'\nexport * from './field'\nexport * from './toast'\nexport * from './segmented-control'\n"]}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EC-392 — the open-layer stack behind stacked dismissible surfaces.
|
|
3
|
+
*
|
|
4
|
+
* A tiny, framework-free core managing which transient surfaces (expand-in-place
|
|
5
|
+
* sheets, {@link ExpandableSurface}) are currently open and — crucially — which of
|
|
6
|
+
* them is the TOPMOST. The ratified stacking contract (EC-392 design gate):
|
|
7
|
+
* multiple layers may be open at once; the topmost layer owns Escape and
|
|
8
|
+
* backdrop-dismiss; layers below stay open and take ownership only when everything
|
|
9
|
+
* above them is gone.
|
|
10
|
+
*
|
|
11
|
+
* The stack knows nothing about React or the DOM: layers register on mount
|
|
12
|
+
* ({@link LayerStack.push}), release on unmount ({@link LayerStack.remove}, which
|
|
13
|
+
* tolerates mid-stack removal — a lower layer may unmount while a higher one stays
|
|
14
|
+
* open), and consult {@link LayerStack.isTop} before acting on a dismiss gesture.
|
|
15
|
+
* Foreign layer systems (e.g. Radix dialogs, which run their own dismissable-layer
|
|
16
|
+
* stack) are bridged at the event level, not here: a Radix layer consumes Escape
|
|
17
|
+
* on the document capture phase and marks the event `defaultPrevented`, which the
|
|
18
|
+
* surface's handler treats as "a layer above me already handled this".
|
|
19
|
+
*/
|
|
20
|
+
/** An opaque handle identifying one open layer. Hold it; never inspect it. */
|
|
21
|
+
export type LayerToken = object;
|
|
22
|
+
export interface LayerStack {
|
|
23
|
+
/** Register a new topmost layer; returns its token. */
|
|
24
|
+
push(): LayerToken;
|
|
25
|
+
/**
|
|
26
|
+
* Remove a layer wherever it sits (topmost or mid-stack). Idempotent —
|
|
27
|
+
* removing an already-removed token is a no-op.
|
|
28
|
+
*/
|
|
29
|
+
remove(token: LayerToken): void;
|
|
30
|
+
/** Whether this token is the current topmost open layer. */
|
|
31
|
+
isTop(token: LayerToken): boolean;
|
|
32
|
+
/** How many layers are open. */
|
|
33
|
+
readonly size: number;
|
|
34
|
+
}
|
|
35
|
+
/** Create an isolated layer stack (the shared instance lives with the surface). */
|
|
36
|
+
export declare function createLayerStack(): LayerStack;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EC-392 — the open-layer stack behind stacked dismissible surfaces.
|
|
3
|
+
*
|
|
4
|
+
* A tiny, framework-free core managing which transient surfaces (expand-in-place
|
|
5
|
+
* sheets, {@link ExpandableSurface}) are currently open and — crucially — which of
|
|
6
|
+
* them is the TOPMOST. The ratified stacking contract (EC-392 design gate):
|
|
7
|
+
* multiple layers may be open at once; the topmost layer owns Escape and
|
|
8
|
+
* backdrop-dismiss; layers below stay open and take ownership only when everything
|
|
9
|
+
* above them is gone.
|
|
10
|
+
*
|
|
11
|
+
* The stack knows nothing about React or the DOM: layers register on mount
|
|
12
|
+
* ({@link LayerStack.push}), release on unmount ({@link LayerStack.remove}, which
|
|
13
|
+
* tolerates mid-stack removal — a lower layer may unmount while a higher one stays
|
|
14
|
+
* open), and consult {@link LayerStack.isTop} before acting on a dismiss gesture.
|
|
15
|
+
* Foreign layer systems (e.g. Radix dialogs, which run their own dismissable-layer
|
|
16
|
+
* stack) are bridged at the event level, not here: a Radix layer consumes Escape
|
|
17
|
+
* on the document capture phase and marks the event `defaultPrevented`, which the
|
|
18
|
+
* surface's handler treats as "a layer above me already handled this".
|
|
19
|
+
*/
|
|
20
|
+
/** Create an isolated layer stack (the shared instance lives with the surface). */
|
|
21
|
+
export function createLayerStack() {
|
|
22
|
+
const layers = [];
|
|
23
|
+
return {
|
|
24
|
+
push() {
|
|
25
|
+
const token = {};
|
|
26
|
+
layers.push(token);
|
|
27
|
+
return token;
|
|
28
|
+
},
|
|
29
|
+
remove(token) {
|
|
30
|
+
const index = layers.indexOf(token);
|
|
31
|
+
if (index !== -1)
|
|
32
|
+
layers.splice(index, 1);
|
|
33
|
+
},
|
|
34
|
+
isTop(token) {
|
|
35
|
+
return layers.length > 0 && layers[layers.length - 1] === token;
|
|
36
|
+
},
|
|
37
|
+
get size() {
|
|
38
|
+
return layers.length;
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=layer-stack.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"layer-stack.js","sourceRoot":"","sources":["../../src/ui/layer-stack.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAmBH,mFAAmF;AACnF,MAAM,UAAU,gBAAgB;IAC9B,MAAM,MAAM,GAAiB,EAAE,CAAA;IAC/B,OAAO;QACL,IAAI;YACF,MAAM,KAAK,GAAe,EAAE,CAAA;YAC5B,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;YAClB,OAAO,KAAK,CAAA;QACd,CAAC;QACD,MAAM,CAAC,KAAK;YACV,MAAM,KAAK,GAAG,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAA;YACnC,IAAI,KAAK,KAAK,CAAC,CAAC;gBAAE,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAA;QAC3C,CAAC;QACD,KAAK,CAAC,KAAK;YACT,OAAO,MAAM,CAAC,MAAM,GAAG,CAAC,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,KAAK,KAAK,CAAA;QACjE,CAAC;QACD,IAAI,IAAI;YACN,OAAO,MAAM,CAAC,MAAM,CAAA;QACtB,CAAC;KACF,CAAA;AACH,CAAC","sourcesContent":["/**\n * EC-392 — the open-layer stack behind stacked dismissible surfaces.\n *\n * A tiny, framework-free core managing which transient surfaces (expand-in-place\n * sheets, {@link ExpandableSurface}) are currently open and — crucially — which of\n * them is the TOPMOST. The ratified stacking contract (EC-392 design gate):\n * multiple layers may be open at once; the topmost layer owns Escape and\n * backdrop-dismiss; layers below stay open and take ownership only when everything\n * above them is gone.\n *\n * The stack knows nothing about React or the DOM: layers register on mount\n * ({@link LayerStack.push}), release on unmount ({@link LayerStack.remove}, which\n * tolerates mid-stack removal — a lower layer may unmount while a higher one stays\n * open), and consult {@link LayerStack.isTop} before acting on a dismiss gesture.\n * Foreign layer systems (e.g. Radix dialogs, which run their own dismissable-layer\n * stack) are bridged at the event level, not here: a Radix layer consumes Escape\n * on the document capture phase and marks the event `defaultPrevented`, which the\n * surface's handler treats as \"a layer above me already handled this\".\n */\n\n/** An opaque handle identifying one open layer. Hold it; never inspect it. */\nexport type LayerToken = object\n\nexport interface LayerStack {\n /** Register a new topmost layer; returns its token. */\n push(): LayerToken\n /**\n * Remove a layer wherever it sits (topmost or mid-stack). Idempotent —\n * removing an already-removed token is a no-op.\n */\n remove(token: LayerToken): void\n /** Whether this token is the current topmost open layer. */\n isTop(token: LayerToken): boolean\n /** How many layers are open. */\n readonly size: number\n}\n\n/** Create an isolated layer stack (the shared instance lives with the surface). */\nexport function createLayerStack(): LayerStack {\n const layers: LayerToken[] = []\n return {\n push() {\n const token: LayerToken = {}\n layers.push(token)\n return token\n },\n remove(token) {\n const index = layers.indexOf(token)\n if (index !== -1) layers.splice(index, 1)\n },\n isTop(token) {\n return layers.length > 0 && layers[layers.length - 1] === token\n },\n get size() {\n return layers.length\n },\n }\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elytracms/studio",
|
|
3
3
|
"author": "finaldream <hi@finaldream.de>",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.61",
|
|
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",
|
|
@@ -142,7 +142,7 @@
|
|
|
142
142
|
"lucide-react": "^1.23.0",
|
|
143
143
|
"tailwind-merge": "^3.6.0",
|
|
144
144
|
"zod": "^4.4.3",
|
|
145
|
-
"@elytracms/core": "^0.0.
|
|
145
|
+
"@elytracms/core": "^0.0.61"
|
|
146
146
|
},
|
|
147
147
|
"peerDependencies": {
|
|
148
148
|
"@tanstack/react-router": "^1.170.0",
|