@elytracms/studio 0.0.72 → 0.0.74

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.js +1 -1
  2. package/dist/components/app-views/default-modules.js.map +1 -1
  3. package/dist/components/cms/cms-labels.d.ts +6 -0
  4. package/dist/components/cms/cms-labels.js +21 -1
  5. package/dist/components/cms/cms-labels.js.map +1 -1
  6. package/dist/components/cms/cms-store.d.ts +16 -1
  7. package/dist/components/cms/cms-store.js +114 -31
  8. package/dist/components/cms/cms-store.js.map +1 -1
  9. package/dist/components/cms/collection-table.js +71 -9
  10. package/dist/components/cms/collection-table.js.map +1 -1
  11. package/dist/components/cms/column-width.d.ts +8 -0
  12. package/dist/components/cms/column-width.js +18 -0
  13. package/dist/components/cms/column-width.js.map +1 -1
  14. package/dist/components/cms/versions-model.d.ts +13 -0
  15. package/dist/components/cms/versions-model.js +44 -0
  16. package/dist/components/cms/versions-model.js.map +1 -1
  17. package/dist/components/cms/versions-pane.js +27 -6
  18. package/dist/components/cms/versions-pane.js.map +1 -1
  19. package/dist/components/cms/view-bar.js +16 -6
  20. package/dist/components/cms/view-bar.js.map +1 -1
  21. package/dist/components/collection-view/cms-collection-view-source.js +7 -0
  22. package/dist/components/collection-view/cms-collection-view-source.js.map +1 -1
  23. package/dist/lib/cms-views/view-model.d.ts +114 -28
  24. package/dist/lib/cms-views/view-model.js +262 -71
  25. package/dist/lib/cms-views/view-model.js.map +1 -1
  26. package/dist/lib/collection-view/collection-view-source.d.ts +28 -0
  27. package/dist/lib/collection-view/collection-view-source.js +15 -0
  28. package/dist/lib/collection-view/collection-view-source.js.map +1 -1
  29. package/dist/lib/i18n/messages.de.js +5 -1
  30. package/dist/lib/i18n/messages.de.js.map +1 -1
  31. package/dist/lib/i18n/messages.en.d.ts +3 -0
  32. package/dist/lib/i18n/messages.en.js +5 -1
  33. package/dist/lib/i18n/messages.en.js.map +1 -1
  34. package/dist/lib/operations/session-hydration.d.ts +8 -0
  35. package/dist/lib/operations/session-hydration.js +3 -0
  36. package/dist/lib/operations/session-hydration.js.map +1 -1
  37. package/dist/ui/index.d.ts +1 -0
  38. package/dist/ui/index.js +1 -0
  39. package/dist/ui/index.js.map +1 -1
  40. package/dist/ui/status-dot.d.ts +39 -0
  41. package/dist/ui/status-dot.js +36 -0
  42. package/dist/ui/status-dot.js.map +1 -0
  43. package/package.json +2 -2
@@ -51,6 +51,14 @@ export interface HydratedCms {
51
51
  * `publishedKeys` from this — published-ness is the pointer, nothing else.
52
52
  */
53
53
  publishedKeys: string[];
54
+ /**
55
+ * Per-document last-write timestamps (EC-445), keyed by `documentKey`
56
+ * (`collection:id`) → the persisted `cmsDocuments.updatedAt` ISO string. Backs the
57
+ * `$updatedAt` list column; the session seeds its `documentUpdatedAt` map from this.
58
+ * Optional so pre-EC-445 fixtures/callers stay valid (the session falls back to a
59
+ * session-start stamp for the seeded documents); `loadHydratedCms` always sets it.
60
+ */
61
+ timestamps?: Record<string, string>;
54
62
  /**
55
63
  * The project's asset records (the `assets` table — NOT cms `asset`-collection
56
64
  * documents). The preview content engine wires these into its lookup so listing/
@@ -83,6 +83,9 @@ export async function loadHydratedCms(operations, projectId) {
83
83
  publishedKeys: contentRecords
84
84
  .filter(isRecordPublished)
85
85
  .map((record) => documentKey(record.document)),
86
+ // EC-445: carry each record's `updatedAt` onto the session for the $updatedAt
87
+ // column — a per-document metadata accessor, exactly like `publishedKeys`.
88
+ timestamps: Object.fromEntries(contentRecords.map((record) => [documentKey(record.document), record.updatedAt])),
86
89
  assets: assets.ok ? assets.value : [],
87
90
  },
88
91
  };
@@ -1 +1 @@
1
- {"version":3,"file":"session-hydration.js","sourceRoot":"","sources":["../../../src/lib/operations/session-hydration.ts"],"names":[],"mappings":"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","sourcesContent":["import { documentKey } from '@elytracms/core/cms-core'\nimport type { CmsDocument, CollectionDef } from '@elytracms/core/cms-core'\nimport type { AssetRecord } from '@elytracms/core/persistence'\nimport { DATA_SOURCES_COLLECTION } from '../data/sources-store'\nimport type { OperationResult, OperationsClient } from './operations-package'\n\n/**\n * Legacy marker (EC-246b): editor-owned saved views are retired — list\n * presentation is config-as-code now (named views live in `cms/`). The id is kept\n * only so hydration keeps excluding any `cms-saved-views` documents a pre-EC-246b\n * deployment persisted, so they never surface as content.\n */\nexport const VIEWS_COLLECTION = 'cms-saved-views'\n\n/**\n * Persisted-session hydration (Milestone B sign-off fix, EC-177 gap 1).\n *\n * Against a persistent backend (Convex mode) a studio session must BOOT from\n * what is persisted — the saved CMS schema + documents (+ asset records) — and\n * fall back to the deterministic fixtures only when the project has nothing\n * persisted yet (first run). Before this, every session seeded from fixtures,\n * so reloads hid the editor's work.\n *\n * EC-287: the DB-resident project graph and its publish path are retired —\n * layouts/config/schema/components are CODE, the DB holds content only. A\n * session no longer hydrates a graph revision or graph-publishing state; CMS\n * content (with per-document `publishedVersion`, EC-224) is the only persisted\n * surface a session boots from.\n *\n * This module is the pure load half: read-only `@elytracms/core/operations` queries\n * composed into one hydration result. `not-found` is a *normal* answer (the\n * first-run fallback); every other failure propagates as the structured\n * operation error so the gate renders it — never a silent fixture fallback.\n * The React gate that applies it lives in\n * `components/studio/project-session.tsx`. EC-361: the playground calls the\n * SAME loader (over the seeded in-memory adapter, through operations — AD-17)\n * to hydrate the canvas/preview content session; only the render-gating is\n * Convex-specific.\n */\n\n/**\n * Collections owned by studio subsystems (data sources, saved views). They live\n * in the same persisted schema but are NOT content: the CMS session, the\n * sidebar, and the content preview engine must never offer them as authorable\n * collections. Routes/redirects are NO LONGER system collections (EC-207): they\n * are config-sourced and never persisted to the backend.\n */\nexport const STUDIO_SYSTEM_COLLECTION_IDS: ReadonlySet<string> = new Set([\n DATA_SOURCES_COLLECTION,\n VIEWS_COLLECTION,\n])\n\n/** The persisted CMS content of a project: schema + documents, system collections excluded. */\nexport interface HydratedCms {\n collections: CollectionDef[]\n documents: CmsDocument[]\n /**\n * Published document keys (EC-224), via the version pointer: a record is\n * published iff `publishedVersion` is set. The session boots its\n * `publishedKeys` from this — published-ness is the pointer, nothing else.\n */\n publishedKeys: string[]\n /**\n * The project's asset records (the `assets` table — NOT cms `asset`-collection\n * documents). The preview content engine wires these into its lookup so listing/\n * relation results resolve their `asset`-typed FIELDS (e.g. a ProductGrid product's\n * `gallery`) to delivery shape — matching what the renderer's `resolveAsset` already\n * does for static `asset` props (EC-262). Without them, the studio canvas renders\n * such nested images blank even though the documents reference real assets.\n */\n assets: AssetRecord[]\n}\n\n/** Everything a Convex-mode session boots from. `null` fields mean \"nothing persisted\" (first run). */\nexport interface HydratedProjectSession {\n /** Persisted CMS schema + documents, or `null` when no content schema was ever saved. */\n cms: HydratedCms | null\n}\n\n/**\n * Load the persisted CMS content state. `null` when the project has no\n * persisted content schema (first run — the session seeds fixtures and the\n * persister's baseline will persist them on the first edit). A schema that\n * contains only studio system collections counts as \"no content persisted\":\n * system stores (saved views, routes) may create the schema record before any\n * content baseline ran.\n */\nexport async function loadHydratedCms(\n operations: OperationsClient,\n projectId: string,\n): Promise<OperationResult<HydratedCms | null>> {\n const schema = await operations.schema.get({ projectId })\n if (!schema.ok) {\n return schema.error.code === 'not-found' ? { ok: true, value: null } : schema\n }\n const collections = schema.value.collections.filter(\n (collection) => !STUDIO_SYSTEM_COLLECTION_IDS.has(collection.id),\n )\n if (collections.length === 0) return { ok: true, value: null }\n const documents = await operations.documents.list({ projectId })\n if (!documents.ok) return documents\n // The project's asset records (assets table), wired into the preview lookup so\n // listing/relation results resolve their nested `asset` fields (EC-262). Non-fatal:\n // a failed asset list degrades previews to no images, never blocks the session.\n const assets = await operations.assets.list({ projectId })\n const contentIds = new Set(collections.map((collection) => collection.id))\n const contentRecords = documents.value.filter((record) =>\n contentIds.has(record.document.collection),\n )\n // EC-224: a record is published when its version pointer is set — the same\n // gate delivery reads. There is no flat-state fallback (the field is gone).\n const isRecordPublished = (record: (typeof contentRecords)[number]) =>\n record.publishedVersion !== undefined\n return {\n ok: true,\n value: {\n collections,\n // The live row hydrates AS PERSISTED — a continuous draft (EC-224). We no\n // longer stamp a `state:'published'` mirror: published-ness is the version\n // pointer (`publishedKeys`), which every studio reader now consults (the\n // table badge/$state via `isPublished`, the preview engine via the pin).\n documents: contentRecords.map((record) => record.document),\n publishedKeys: contentRecords\n .filter(isRecordPublished)\n .map((record) => documentKey(record.document)),\n assets: assets.ok ? assets.value : [],\n },\n }\n}\n\n/**\n * Load everything a project session hydrates from. Post-EC-287 that is just the\n * persisted CMS content (schema + documents + assets): the project graph and its\n * publish path are gone, so there is no graph revision or graph-publishing state\n * to load. Per-document `publishedVersion` (EC-224) is the only published surface.\n */\nexport async function loadProjectSession(\n operations: OperationsClient,\n projectId: string,\n): Promise<OperationResult<HydratedProjectSession>> {\n const cms = await loadHydratedCms(operations, projectId)\n if (!cms.ok) return cms\n return { ok: true, value: { cms: cms.value } }\n}\n"]}
1
+ {"version":3,"file":"session-hydration.js","sourceRoot":"","sources":["../../../src/lib/operations/session-hydration.ts"],"names":[],"mappings":"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","sourcesContent":["import { documentKey } from '@elytracms/core/cms-core'\nimport type { CmsDocument, CollectionDef } from '@elytracms/core/cms-core'\nimport type { AssetRecord } from '@elytracms/core/persistence'\nimport { DATA_SOURCES_COLLECTION } from '../data/sources-store'\nimport type { OperationResult, OperationsClient } from './operations-package'\n\n/**\n * Legacy marker (EC-246b): editor-owned saved views are retired — list\n * presentation is config-as-code now (named views live in `cms/`). The id is kept\n * only so hydration keeps excluding any `cms-saved-views` documents a pre-EC-246b\n * deployment persisted, so they never surface as content.\n */\nexport const VIEWS_COLLECTION = 'cms-saved-views'\n\n/**\n * Persisted-session hydration (Milestone B sign-off fix, EC-177 gap 1).\n *\n * Against a persistent backend (Convex mode) a studio session must BOOT from\n * what is persisted — the saved CMS schema + documents (+ asset records) — and\n * fall back to the deterministic fixtures only when the project has nothing\n * persisted yet (first run). Before this, every session seeded from fixtures,\n * so reloads hid the editor's work.\n *\n * EC-287: the DB-resident project graph and its publish path are retired —\n * layouts/config/schema/components are CODE, the DB holds content only. A\n * session no longer hydrates a graph revision or graph-publishing state; CMS\n * content (with per-document `publishedVersion`, EC-224) is the only persisted\n * surface a session boots from.\n *\n * This module is the pure load half: read-only `@elytracms/core/operations` queries\n * composed into one hydration result. `not-found` is a *normal* answer (the\n * first-run fallback); every other failure propagates as the structured\n * operation error so the gate renders it — never a silent fixture fallback.\n * The React gate that applies it lives in\n * `components/studio/project-session.tsx`. EC-361: the playground calls the\n * SAME loader (over the seeded in-memory adapter, through operations — AD-17)\n * to hydrate the canvas/preview content session; only the render-gating is\n * Convex-specific.\n */\n\n/**\n * Collections owned by studio subsystems (data sources, saved views). They live\n * in the same persisted schema but are NOT content: the CMS session, the\n * sidebar, and the content preview engine must never offer them as authorable\n * collections. Routes/redirects are NO LONGER system collections (EC-207): they\n * are config-sourced and never persisted to the backend.\n */\nexport const STUDIO_SYSTEM_COLLECTION_IDS: ReadonlySet<string> = new Set([\n DATA_SOURCES_COLLECTION,\n VIEWS_COLLECTION,\n])\n\n/** The persisted CMS content of a project: schema + documents, system collections excluded. */\nexport interface HydratedCms {\n collections: CollectionDef[]\n documents: CmsDocument[]\n /**\n * Published document keys (EC-224), via the version pointer: a record is\n * published iff `publishedVersion` is set. The session boots its\n * `publishedKeys` from this — published-ness is the pointer, nothing else.\n */\n publishedKeys: string[]\n /**\n * Per-document last-write timestamps (EC-445), keyed by `documentKey`\n * (`collection:id`) → the persisted `cmsDocuments.updatedAt` ISO string. Backs the\n * `$updatedAt` list column; the session seeds its `documentUpdatedAt` map from this.\n * Optional so pre-EC-445 fixtures/callers stay valid (the session falls back to a\n * session-start stamp for the seeded documents); `loadHydratedCms` always sets it.\n */\n timestamps?: Record<string, string>\n /**\n * The project's asset records (the `assets` table — NOT cms `asset`-collection\n * documents). The preview content engine wires these into its lookup so listing/\n * relation results resolve their `asset`-typed FIELDS (e.g. a ProductGrid product's\n * `gallery`) to delivery shape — matching what the renderer's `resolveAsset` already\n * does for static `asset` props (EC-262). Without them, the studio canvas renders\n * such nested images blank even though the documents reference real assets.\n */\n assets: AssetRecord[]\n}\n\n/** Everything a Convex-mode session boots from. `null` fields mean \"nothing persisted\" (first run). */\nexport interface HydratedProjectSession {\n /** Persisted CMS schema + documents, or `null` when no content schema was ever saved. */\n cms: HydratedCms | null\n}\n\n/**\n * Load the persisted CMS content state. `null` when the project has no\n * persisted content schema (first run — the session seeds fixtures and the\n * persister's baseline will persist them on the first edit). A schema that\n * contains only studio system collections counts as \"no content persisted\":\n * system stores (saved views, routes) may create the schema record before any\n * content baseline ran.\n */\nexport async function loadHydratedCms(\n operations: OperationsClient,\n projectId: string,\n): Promise<OperationResult<HydratedCms | null>> {\n const schema = await operations.schema.get({ projectId })\n if (!schema.ok) {\n return schema.error.code === 'not-found' ? { ok: true, value: null } : schema\n }\n const collections = schema.value.collections.filter(\n (collection) => !STUDIO_SYSTEM_COLLECTION_IDS.has(collection.id),\n )\n if (collections.length === 0) return { ok: true, value: null }\n const documents = await operations.documents.list({ projectId })\n if (!documents.ok) return documents\n // The project's asset records (assets table), wired into the preview lookup so\n // listing/relation results resolve their nested `asset` fields (EC-262). Non-fatal:\n // a failed asset list degrades previews to no images, never blocks the session.\n const assets = await operations.assets.list({ projectId })\n const contentIds = new Set(collections.map((collection) => collection.id))\n const contentRecords = documents.value.filter((record) =>\n contentIds.has(record.document.collection),\n )\n // EC-224: a record is published when its version pointer is set — the same\n // gate delivery reads. There is no flat-state fallback (the field is gone).\n const isRecordPublished = (record: (typeof contentRecords)[number]) =>\n record.publishedVersion !== undefined\n return {\n ok: true,\n value: {\n collections,\n // The live row hydrates AS PERSISTED — a continuous draft (EC-224). We no\n // longer stamp a `state:'published'` mirror: published-ness is the version\n // pointer (`publishedKeys`), which every studio reader now consults (the\n // table badge/$state via `isPublished`, the preview engine via the pin).\n documents: contentRecords.map((record) => record.document),\n publishedKeys: contentRecords\n .filter(isRecordPublished)\n .map((record) => documentKey(record.document)),\n // EC-445: carry each record's `updatedAt` onto the session for the $updatedAt\n // column — a per-document metadata accessor, exactly like `publishedKeys`.\n timestamps: Object.fromEntries(\n contentRecords.map((record) => [documentKey(record.document), record.updatedAt]),\n ),\n assets: assets.ok ? assets.value : [],\n },\n }\n}\n\n/**\n * Load everything a project session hydrates from. Post-EC-287 that is just the\n * persisted CMS content (schema + documents + assets): the project graph and its\n * publish path are gone, so there is no graph revision or graph-publishing state\n * to load. Per-document `publishedVersion` (EC-224) is the only published surface.\n */\nexport async function loadProjectSession(\n operations: OperationsClient,\n projectId: string,\n): Promise<OperationResult<HydratedProjectSession>> {\n const cms = await loadHydratedCms(operations, projectId)\n if (!cms.ok) return cms\n return { ok: true, value: { cms: cms.value } }\n}\n"]}
@@ -4,6 +4,7 @@ export * from './avatar.js';
4
4
  export * from './button.js';
5
5
  export * from './input.js';
6
6
  export * from './badge.js';
7
+ export * from './status-dot.js';
7
8
  export * from './card.js';
8
9
  export * from './panel.js';
9
10
  export * from './row-item.js';
package/dist/ui/index.js CHANGED
@@ -4,6 +4,7 @@ export * from './avatar.js';
4
4
  export * from './button.js';
5
5
  export * from './input.js';
6
6
  export * from './badge.js';
7
+ export * from './status-dot.js';
7
8
  export * from './card.js';
8
9
  export * from './panel.js';
9
10
  export * from './row-item.js';
@@ -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,sBAAsB,CAAA;AACpC,cAAc,oBAAoB,CAAA;AAClC,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 './surface-geometry'\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"]}
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,cAAc,CAAA;AAC5B,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,oBAAoB,CAAA;AAClC,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 './status-dot'\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 './surface-geometry'\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,39 @@
1
+ import type { HTMLAttributes } from 'react';
2
+ /**
3
+ * The status vocabulary a {@link StatusDot} can express. Kept deliberately
4
+ * generic (not domain-specific) so the same primitive marks a published version
5
+ * (`success`), a draft/pending state (`warning`), or a quiet/absent state
6
+ * (`muted`/`neutral`) — the versions pane (EC-446) is its first consumer, the
7
+ * list `$state` column (EC-445) its next.
8
+ */
9
+ export type StatusTone = 'success' | 'warning' | 'muted' | 'neutral';
10
+ /**
11
+ * The dot's fill class per tone — a pure mapping onto the shared token palette
12
+ * (the same `--success`/`--warning`/`--muted-foreground` tokens {@link Badge}
13
+ * uses), extracted so it is unit-testable without a DOM.
14
+ */
15
+ export declare function statusDotToneClass(tone: StatusTone): string;
16
+ export interface StatusDotProps extends Omit<HTMLAttributes<HTMLSpanElement>, 'children' | 'aria-label'> {
17
+ /** Which status the dot expresses — maps onto the shared token palette. */
18
+ tone: StatusTone;
19
+ /**
20
+ * The accessible + hover/focus-revealed text (e.g. "Published"). Exposed as the
21
+ * `aria-label` and native `title`, and shown as a small pill on hover/focus so a
22
+ * keyboard user (no mouse) can still read it.
23
+ */
24
+ label: string;
25
+ }
26
+ /**
27
+ * EC-446 — a compact, reusable status indicator: a small colored dot whose
28
+ * meaning is revealed on hover/focus rather than spent as a full-width pill. The
29
+ * primary space win over {@link Badge} for dense rows (the versions history, the
30
+ * list `$state` column). Pure-presentational and domain-agnostic: it knows a
31
+ * `tone` and a `label`, nothing about versions or documents.
32
+ *
33
+ * Accessibility: the wrapper is focusable (`tabIndex=0`, `role="img"`) and
34
+ * carries the label as both `aria-label` (screen readers) and native `title`
35
+ * (mouse); the CSS pill additionally reveals the label on `:focus-visible`, so
36
+ * the meaning is reachable without a mouse. The dot glyph and the pill are
37
+ * `aria-hidden` — the wrapper's `aria-label` is the single announced name.
38
+ */
39
+ export declare function StatusDot({ tone, label, className, ...props }: StatusDotProps): import("react").JSX.Element;
@@ -0,0 +1,36 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cn } from '../lib/utils.js';
3
+ /**
4
+ * The dot's fill class per tone — a pure mapping onto the shared token palette
5
+ * (the same `--success`/`--warning`/`--muted-foreground` tokens {@link Badge}
6
+ * uses), extracted so it is unit-testable without a DOM.
7
+ */
8
+ export function statusDotToneClass(tone) {
9
+ switch (tone) {
10
+ case 'success':
11
+ return 'bg-success';
12
+ case 'warning':
13
+ return 'bg-warning';
14
+ case 'muted':
15
+ return 'bg-muted-foreground/40';
16
+ case 'neutral':
17
+ return 'bg-muted-foreground';
18
+ }
19
+ }
20
+ /**
21
+ * EC-446 — a compact, reusable status indicator: a small colored dot whose
22
+ * meaning is revealed on hover/focus rather than spent as a full-width pill. The
23
+ * primary space win over {@link Badge} for dense rows (the versions history, the
24
+ * list `$state` column). Pure-presentational and domain-agnostic: it knows a
25
+ * `tone` and a `label`, nothing about versions or documents.
26
+ *
27
+ * Accessibility: the wrapper is focusable (`tabIndex=0`, `role="img"`) and
28
+ * carries the label as both `aria-label` (screen readers) and native `title`
29
+ * (mouse); the CSS pill additionally reveals the label on `:focus-visible`, so
30
+ * the meaning is reachable without a mouse. The dot glyph and the pill are
31
+ * `aria-hidden` — the wrapper's `aria-label` is the single announced name.
32
+ */
33
+ export function StatusDot({ tone, label, className, ...props }) {
34
+ return (_jsxs("span", { role: "img", tabIndex: 0, "aria-label": label, title: label, className: cn('group/status-dot relative inline-flex items-center justify-center rounded-full outline-none', 'focus-visible:ring-2 focus-visible:ring-ring/60', className), ...props, children: [_jsx("span", { "aria-hidden": true, className: cn('size-2 shrink-0 rounded-full', statusDotToneClass(tone)) }), _jsx("span", { "aria-hidden": true, className: cn('pointer-events-none absolute left-1/2 top-full z-50 mt-1.5 -translate-x-1/2 whitespace-nowrap', 'rounded-md border border-border bg-popover px-2 py-1 text-xs font-medium text-popover-foreground shadow-md', 'opacity-0 transition-opacity duration-100', 'group-hover/status-dot:opacity-100 group-focus-visible/status-dot:opacity-100'), children: label })] }));
35
+ }
36
+ //# sourceMappingURL=status-dot.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"status-dot.js","sourceRoot":"","sources":["../../src/ui/status-dot.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,EAAE,EAAE,MAAM,cAAc,CAAA;AAWjC;;;;GAIG;AACH,MAAM,UAAU,kBAAkB,CAAC,IAAgB;IACjD,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,SAAS;YACZ,OAAO,YAAY,CAAA;QACrB,KAAK,SAAS;YACZ,OAAO,YAAY,CAAA;QACrB,KAAK,OAAO;YACV,OAAO,wBAAwB,CAAA;QACjC,KAAK,SAAS;YACZ,OAAO,qBAAqB,CAAA;IAChC,CAAC;AACH,CAAC;AAaD;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,SAAS,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,KAAK,EAAkB;IAC5E,OAAO,CACL,gBACE,IAAI,EAAC,KAAK,EACV,QAAQ,EAAE,CAAC,gBACC,KAAK,EACjB,KAAK,EAAE,KAAK,EACZ,SAAS,EAAE,EAAE,CACX,6FAA6F,EAC7F,iDAAiD,EACjD,SAAS,CACV,KACG,KAAK,aAET,oCAAkB,SAAS,EAAE,EAAE,CAAC,8BAA8B,EAAE,kBAAkB,CAAC,IAAI,CAAC,CAAC,GAAI,EAC7F,oCAEE,SAAS,EAAE,EAAE,CACX,+FAA+F,EAC/F,4GAA4G,EAC5G,2CAA2C,EAC3C,+EAA+E,CAChF,YAEA,KAAK,GACD,IACF,CACR,CAAA;AACH,CAAC","sourcesContent":["import type { HTMLAttributes } from 'react'\nimport { cn } from '../lib/utils'\n\n/**\n * The status vocabulary a {@link StatusDot} can express. Kept deliberately\n * generic (not domain-specific) so the same primitive marks a published version\n * (`success`), a draft/pending state (`warning`), or a quiet/absent state\n * (`muted`/`neutral`) — the versions pane (EC-446) is its first consumer, the\n * list `$state` column (EC-445) its next.\n */\nexport type StatusTone = 'success' | 'warning' | 'muted' | 'neutral'\n\n/**\n * The dot's fill class per tone — a pure mapping onto the shared token palette\n * (the same `--success`/`--warning`/`--muted-foreground` tokens {@link Badge}\n * uses), extracted so it is unit-testable without a DOM.\n */\nexport function statusDotToneClass(tone: StatusTone): string {\n switch (tone) {\n case 'success':\n return 'bg-success'\n case 'warning':\n return 'bg-warning'\n case 'muted':\n return 'bg-muted-foreground/40'\n case 'neutral':\n return 'bg-muted-foreground'\n }\n}\n\nexport interface StatusDotProps extends Omit<HTMLAttributes<HTMLSpanElement>, 'children' | 'aria-label'> {\n /** Which status the dot expresses — maps onto the shared token palette. */\n tone: StatusTone\n /**\n * The accessible + hover/focus-revealed text (e.g. \"Published\"). Exposed as the\n * `aria-label` and native `title`, and shown as a small pill on hover/focus so a\n * keyboard user (no mouse) can still read it.\n */\n label: string\n}\n\n/**\n * EC-446 — a compact, reusable status indicator: a small colored dot whose\n * meaning is revealed on hover/focus rather than spent as a full-width pill. The\n * primary space win over {@link Badge} for dense rows (the versions history, the\n * list `$state` column). Pure-presentational and domain-agnostic: it knows a\n * `tone` and a `label`, nothing about versions or documents.\n *\n * Accessibility: the wrapper is focusable (`tabIndex=0`, `role=\"img\"`) and\n * carries the label as both `aria-label` (screen readers) and native `title`\n * (mouse); the CSS pill additionally reveals the label on `:focus-visible`, so\n * the meaning is reachable without a mouse. The dot glyph and the pill are\n * `aria-hidden` — the wrapper's `aria-label` is the single announced name.\n */\nexport function StatusDot({ tone, label, className, ...props }: StatusDotProps) {\n return (\n <span\n role=\"img\"\n tabIndex={0}\n aria-label={label}\n title={label}\n className={cn(\n 'group/status-dot relative inline-flex items-center justify-center rounded-full outline-none',\n 'focus-visible:ring-2 focus-visible:ring-ring/60',\n className,\n )}\n {...props}\n >\n <span aria-hidden className={cn('size-2 shrink-0 rounded-full', statusDotToneClass(tone))} />\n <span\n aria-hidden\n className={cn(\n 'pointer-events-none absolute left-1/2 top-full z-50 mt-1.5 -translate-x-1/2 whitespace-nowrap',\n 'rounded-md border border-border bg-popover px-2 py-1 text-xs font-medium text-popover-foreground shadow-md',\n 'opacity-0 transition-opacity duration-100',\n 'group-hover/status-dot:opacity-100 group-focus-visible/status-dot:opacity-100',\n )}\n >\n {label}\n </span>\n </span>\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.72",
4
+ "version": "0.0.74",
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",
@@ -150,7 +150,7 @@
150
150
  "lucide-react": "^1.23.0",
151
151
  "tailwind-merge": "^3.6.0",
152
152
  "zod": "^4.4.3",
153
- "@elytracms/core": "^0.0.72"
153
+ "@elytracms/core": "^0.0.74"
154
154
  },
155
155
  "peerDependencies": {
156
156
  "@tanstack/react-router": "^1.170.0",