aeon-ui-engine 1.1.0 → 1.2.0

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 (33) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/CONSUMER.md +45 -0
  3. package/package.json +1 -1
  4. package/packages/core/dist/index.d.ts +88 -8
  5. package/packages/core/dist/index.js +113 -0
  6. package/packages/core/src/anatomy.ts +38 -0
  7. package/packages/core/src/brand-theme.ts +172 -0
  8. package/packages/core/src/index.ts +1 -0
  9. package/packages/panda/aeon.css +1 -0
  10. package/packages/panda/src/theme/consumer-shell.css +129 -0
  11. package/packages/panda/src/theme/electron.css +30 -0
  12. package/packages/primitives/dist/chunk-4NOKRUSG.js +81 -0
  13. package/packages/primitives/dist/index.d.ts +1 -0
  14. package/packages/primitives/dist/index.js +16 -6
  15. package/packages/primitives/dist/machines/app-nav.d.ts +96 -0
  16. package/packages/primitives/dist/machines/app-nav.js +12 -0
  17. package/packages/primitives/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  18. package/packages/primitives/src/__tests__/app-nav.test.ts +30 -0
  19. package/packages/primitives/src/index.ts +1 -0
  20. package/packages/primitives/src/machines/app-nav.ts +107 -0
  21. package/packages/react/dist/index.d.ts +135 -3
  22. package/packages/react/dist/index.js +1126 -720
  23. package/packages/react/src/app-nav/index.tsx +249 -0
  24. package/packages/react/src/avatar/index.tsx +4 -1
  25. package/packages/react/src/hooks/use-fit-text.ts +59 -0
  26. package/packages/react/src/index.ts +4 -0
  27. package/packages/react/src/prompt/index.tsx +161 -0
  28. package/packages/react/src/status-banner/index.tsx +84 -0
  29. package/packages/ui/src/app-nav.tsx +2 -0
  30. package/packages/ui/src/index.ts +4 -0
  31. package/packages/ui/src/prompt.tsx +29 -0
  32. package/packages/ui/src/status-banner.tsx +1 -0
  33. package/scripts/vite-aliases.mjs +8 -0
package/CHANGELOG.md CHANGED
@@ -5,6 +5,24 @@ All notable changes to **aeon-ui-engine** are documented here.
5
5
  Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
  Versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.2.0] - 2026-07-30
9
+
10
+ ### Added
11
+
12
+ - **Brand palette bridge** — `applyBrandPalette`, `normalizeBrandPalette`, `brandPaletteToCssText` map product tokens onto Aeon CSS vars without a parallel token sheet.
13
+ - **AppNav** — `appNavMachine` + `AppNav` / `useAppNavMachine` for section + detail stack navigation with breadcrumb/back parts.
14
+ - **Prompt** — confirm/permission-style compound on top of Dialog (`eyebrow`, `meta`, `amount`, `actions`).
15
+ - **StatusBanner** — non-blocking top-of-app notice compound.
16
+ - **`useFitText`** — shrink-to-fit metric/title helper.
17
+ - **Consumer shell CSS** — baseline Prompt / StatusBanner / AppNav styles in `aeon.css`.
18
+ - **Electron chrome CSS** — optional `@aeon-ui/panda/electron.css` titlebar drag helpers.
19
+ - Docs: [PRODUCT_SHELL.md](./docs/PRODUCT_SHELL.md).
20
+
21
+ ### Changed
22
+
23
+ - Avatar root also projects `ready` alongside `loaded` for friendlier product CSS.
24
+ - `aeonUiVitePlugin` aliases include consumer-shell and electron CSS paths.
25
+
8
26
  ## [1.1.0] - 2026-07-30
9
27
 
10
28
  ### Added
@@ -59,6 +77,7 @@ Versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
59
77
 
60
78
  - Initial npm release of **aeon-ui-engine** — monorepo bundle with headless statechart primitives, React bindings, Panda styled layer, and themes.
61
79
 
80
+ [1.2.0]: https://github.com/GenericCPU/aeon-ui/compare/v1.1.0...v1.2.0
62
81
  [1.1.0]: https://github.com/GenericCPU/aeon-ui/compare/v1.0.3...v1.1.0
63
82
  [1.0.3]: https://github.com/GenericCPU/aeon-ui/compare/v1.0.2...v1.0.3
64
83
  [1.0.2]: https://github.com/GenericCPU/aeon-ui/compare/v1.0.1...v1.0.2
package/CONSUMER.md CHANGED
@@ -60,6 +60,51 @@ Or extend directly:
60
60
  }
61
61
  ```
62
62
 
63
+ ## Optional: Brand palette (skip catalog themes)
64
+
65
+ If your product already has tokens, map them onto Aeon CSS vars once at boot:
66
+
67
+ ```ts
68
+ import { applyBrandPalette } from '@aeon-ui/core'
69
+
70
+ applyBrandPalette(
71
+ {
72
+ bg: '#000000',
73
+ surface: '#0a0a0a',
74
+ border: '#262626',
75
+ text: '#fafafa',
76
+ muted: '#a1a1aa',
77
+ accent: '#57ff97',
78
+ font: "'Archivo', system-ui, sans-serif",
79
+ },
80
+ { mode: 'dark' },
81
+ )
82
+ ```
83
+
84
+ Then use `@aeon-ui/ui` without maintaining a parallel `--product-*` token sheet
85
+ for Aeon components. See [docs/PRODUCT_SHELL.md](./docs/PRODUCT_SHELL.md).
86
+
87
+ ## Optional: Product shell compounds
88
+
89
+ ```tsx
90
+ import { AppNav, Prompt, StatusBanner, AppShell } from '@aeon-ui/ui'
91
+
92
+ // Section + detail stack (replaces ad-hoc nav stores)
93
+ <AppNav.Root defaultSection="home">...</AppNav.Root>
94
+
95
+ // Confirm / permission style dialog (portal + focus trap via Dialog)
96
+ <Prompt.Root open={open} status="pending">...</Prompt.Root>
97
+
98
+ // Non-blocking top banner (updates, sync, offline)
99
+ <StatusBanner.Root status="ready">...</StatusBanner.Root>
100
+ ```
101
+
102
+ Electron titlebar helpers (drag regions, traffic-light padding):
103
+
104
+ ```ts
105
+ import '@aeon-ui/panda/electron.css'
106
+ ```
107
+
63
108
  ## Optional: Headless only
64
109
 
65
110
  Skip the styled layer and import headless components directly:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aeon-ui-engine",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
@@ -26,6 +26,25 @@ declare const checkboxAnatomy: Record<"root" | "label" | "control" | "hiddenInpu
26
26
  declare const dialogAnatomy: Record<"root" | "trigger" | "portal" | "backdrop" | "positioner" | "content" | "title" | "description" | "closeTrigger", "root" | "trigger" | "portal" | "backdrop" | "positioner" | "content" | "title" | "description" | "closeTrigger"> & {
27
27
  scope: string;
28
28
  };
29
+ /**
30
+ * Prompt — confirm / permission / update-style dialog on top of Dialog.
31
+ * Universal parts: eyebrow, meta, amount, actions (primary + secondary).
32
+ */
33
+ declare const promptAnatomy: Record<"root" | "eyebrow" | "meta" | "amount" | "actions" | "primary" | "secondary", "root" | "eyebrow" | "meta" | "amount" | "actions" | "primary" | "secondary"> & {
34
+ scope: string;
35
+ };
36
+ /**
37
+ * AppNav — section + detail stack (list→detail, settings drill-in).
38
+ */
39
+ declare const appNavAnatomy: Record<"root" | "rail" | "section" | "stage" | "breadcrumb" | "crumb" | "back", "root" | "rail" | "section" | "stage" | "breadcrumb" | "crumb" | "back"> & {
40
+ scope: string;
41
+ };
42
+ /**
43
+ * StatusBanner — non-blocking top-of-app notice (update ready, syncing, offline).
44
+ */
45
+ declare const statusBannerAnatomy: Record<"root" | "title" | "actions" | "copy" | "body", "root" | "title" | "actions" | "copy" | "body"> & {
46
+ scope: string;
47
+ };
29
48
  declare const tabsAnatomy: Record<"root" | "indicator" | "trigger" | "content" | "list", "root" | "indicator" | "trigger" | "content" | "list"> & {
30
49
  scope: string;
31
50
  };
@@ -33,7 +52,7 @@ declare const accordionAnatomy: Record<"root" | "item" | "itemTrigger" | "itemCo
33
52
  scope: string;
34
53
  };
35
54
  /** Async data region — track shows every lifecycle state; readout + actions mirror snapshot. */
36
- declare const asyncAnatomy: Record<"root" | "track" | "segment" | "readout" | "readoutRail" | "readoutBody" | "actions", "root" | "track" | "segment" | "readout" | "readoutRail" | "readoutBody" | "actions"> & {
55
+ declare const asyncAnatomy: Record<"root" | "actions" | "track" | "segment" | "readout" | "readoutRail" | "readoutBody", "root" | "actions" | "track" | "segment" | "readout" | "readoutRail" | "readoutBody"> & {
37
56
  scope: string;
38
57
  };
39
58
  /** Form field — orthogonal interaction × validation × submission (see STATES.md). */
@@ -78,14 +97,14 @@ declare const avatarAnatomy: Record<"root" | "image" | "fallback" | "badge", "ro
78
97
  * Identity strip — avatar + primary/secondary text (items-market AvatarWithName).
79
98
  * Presentational; compose into ProfileHeader / ListRow / AccountCluster.
80
99
  */
81
- declare const identityAnatomy: Record<"root" | "title" | "avatar" | "subtitle" | "meta" | "trailing", "root" | "title" | "avatar" | "subtitle" | "meta" | "trailing"> & {
100
+ declare const identityAnatomy: Record<"root" | "title" | "meta" | "avatar" | "subtitle" | "trailing", "root" | "title" | "meta" | "avatar" | "subtitle" | "trailing"> & {
82
101
  scope: string;
83
102
  };
84
103
  /**
85
104
  * Profile header — dense account hero (not dating). Media optional; metrics + actions zones.
86
105
  * Spatial: identity leading, metrics inline, actions trailing / bottom leading pair.
87
106
  */
88
- declare const profileHeaderAnatomy: Record<"root" | "actions" | "identity" | "media" | "metrics" | "body", "root" | "actions" | "identity" | "media" | "metrics" | "body"> & {
107
+ declare const profileHeaderAnatomy: Record<"root" | "actions" | "body" | "identity" | "media" | "metrics", "root" | "actions" | "body" | "identity" | "media" | "metrics"> & {
89
108
  scope: string;
90
109
  };
91
110
  /**
@@ -94,7 +113,7 @@ declare const profileHeaderAnatomy: Record<"root" | "actions" | "identity" | "me
94
113
  * List stacks cards; Root is one card (idle | selected | muted).
95
114
  * Zones: header (leading · heading · meta) · media · body · values · actions · footer.
96
115
  */
97
- declare const entryAnatomy: Record<"values" | "root" | "title" | "list" | "actions" | "value" | "subtitle" | "meta" | "media" | "body" | "header" | "leading" | "heading" | "footer", "values" | "root" | "title" | "list" | "actions" | "value" | "subtitle" | "meta" | "media" | "body" | "header" | "leading" | "heading" | "footer"> & {
116
+ declare const entryAnatomy: Record<"values" | "root" | "title" | "meta" | "actions" | "body" | "list" | "value" | "subtitle" | "media" | "header" | "leading" | "heading" | "footer", "values" | "root" | "title" | "meta" | "actions" | "body" | "list" | "value" | "subtitle" | "media" | "header" | "leading" | "heading" | "footer"> & {
98
117
  scope: string;
99
118
  };
100
119
  /**
@@ -115,13 +134,13 @@ declare const listRowAnatomy: Record<"root" | "label" | "description" | "trailin
115
134
  * Leading avatar · title · preview · meta (time) · badge (unread).
116
135
  * Item state: idle | unread | selected.
117
136
  */
118
- declare const conversationAnatomy: Record<"root" | "title" | "item" | "badge" | "meta" | "body" | "leading" | "preview", "root" | "title" | "item" | "badge" | "meta" | "body" | "leading" | "preview"> & {
137
+ declare const conversationAnatomy: Record<"root" | "title" | "meta" | "body" | "item" | "badge" | "leading" | "preview", "root" | "title" | "meta" | "body" | "item" | "badge" | "leading" | "preview"> & {
119
138
  scope: string;
120
139
  };
121
140
  /**
122
141
  * Thread — message stream. Items project mine | theirs | pending | failed.
123
142
  */
124
- declare const threadAnatomy: Record<"root" | "list" | "item" | "meta" | "bubble" | "day", "root" | "list" | "item" | "meta" | "bubble" | "day"> & {
143
+ declare const threadAnatomy: Record<"root" | "meta" | "list" | "item" | "bubble" | "day", "root" | "meta" | "list" | "item" | "bubble" | "day"> & {
125
144
  scope: string;
126
145
  };
127
146
  /**
@@ -174,7 +193,7 @@ declare const appShellAnatomy: Record<"root" | "content" | "header" | "footer" |
174
193
  * Content region — status-slotted body. Every contentRegionMachine state has a part.
175
194
  * Pair with Async for fetch; use slots for idle/pending/empty/error/ready/loadingMore/success.
176
195
  */
177
- declare const contentAnatomy: Record<"root" | "empty" | "body" | "toolbar" | "pending" | "error" | "success" | "sentinel", "root" | "empty" | "body" | "toolbar" | "pending" | "error" | "success" | "sentinel"> & {
196
+ declare const contentAnatomy: Record<"root" | "body" | "empty" | "toolbar" | "pending" | "error" | "success" | "sentinel", "root" | "body" | "empty" | "toolbar" | "pending" | "error" | "success" | "sentinel"> & {
178
197
  scope: string;
179
198
  };
180
199
  /**
@@ -422,4 +441,65 @@ declare function initAeonTheme(themeMap?: Record<string, AeonThemeDefinition>):
422
441
  mode: AeonColorMode;
423
442
  };
424
443
 
425
- export { AEON_PRINCIPLE, AEON_THEME_TRANSITION_CLASS, type AeonActor, type AeonColorMode, type AeonContext, type AeonDataAttrs, type AeonEvent, type AeonPalette, type AeonSnapshot, type AeonStateValue, type AeonThemeDefinition, DEFAULT_COMPACT_MQ, type FieldFace, type LayoutComposition, type LayoutCompositionSpec, type LayoutMode, NORM_PLANE, type NormOrigin, type NormPoint, type NormRect, type RegionLayoutSlot, type ResponsiveLayoutSpec, type ScrollOverflow, type ScrollSnapshot, UI_IS_FUNCTION_OF_STATE, accordionAnatomy, anatomy, appShellAnatomy, applyAeonTheme, asyncAnatomy, avatarAnatomy, barAnatomy, buttonAnatomy, checkboxAnatomy, clampNorm, clientFromNorm, comboboxAnatomy, composerAnatomy, contentAnatomy, conversationAnatomy, dialogAnatomy, entryAnatomy, fieldAnatomy, fieldFace, fieldRegions, getCompactMq, getRegionValue, getScrollSnapshot, identityAnatomy, initAeonTheme, insetNormCluster, isAeonColorMode, isAeonMode, isAeonThemeId, layoutCompositionForMode, layoutModeAttrs, listRowAnatomy, matchLayoutMode, measureElement, menuAnatomy, metricStripAnatomy, navAnatomy, normFromClient, panelAnatomy, partAttrs, partOnlyAttrs, pinInputAnatomy, playSurfaceAnatomy, popoverAnatomy, profileHeaderAnatomy, progressAnatomy, radioGroupAnatomy, regionBoundsForMode, regionLeaf, regionMatches, regionPath, registerAeonThemes, resolveAeonColorMode, resolveRegionRect, runAeonThemeTransition, scopeAttrs, scrollAnatomy, selectAnatomy, separatorAnatomy, sliderAnatomy, stateToAttr, switchAnatomy, syncAeonColorMode, tabsAnatomy, themeSwitcherAnatomy, threadAnatomy, toastAnatomy, tooltipAnatomy };
444
+ /**
445
+ * Brand → Aeon CSS variable bridge.
446
+ *
447
+ * Product apps often already have design tokens. Instead of maintaining a parallel
448
+ * `--product-*` layer forever, map them onto Aeon’s runtime vars so `@aeon-ui/ui`
449
+ * recipes and `aeon.css` pick up the brand without forking Panda.
450
+ */
451
+
452
+ /** Minimal brand fields — anything omitted gets a safe default. */
453
+ type BrandPaletteInput = {
454
+ bg: string;
455
+ surface: string;
456
+ surfaceRaised?: string;
457
+ border: string;
458
+ text: string;
459
+ muted: string;
460
+ accent: string;
461
+ accentDim?: string;
462
+ danger?: string;
463
+ dangerDim?: string;
464
+ gridLine?: string;
465
+ /** Optional product fonts (applied as --aeon-font / --aeon-font-display). */
466
+ font?: string;
467
+ fontDisplay?: string;
468
+ radius?: string;
469
+ };
470
+ type BrandPalette = Required<Pick<BrandPaletteInput, 'bg' | 'surface' | 'surfaceRaised' | 'border' | 'text' | 'muted' | 'accent' | 'accentDim' | 'danger' | 'dangerDim' | 'gridLine'>> & {
471
+ font?: string;
472
+ fontDisplay?: string;
473
+ radius?: string;
474
+ focusRing: string;
475
+ };
476
+ /** Normalize a partial brand palette into a full Aeon-compatible palette. */
477
+ declare function normalizeBrandPalette(input: BrandPaletteInput, mode?: AeonColorMode): BrandPalette;
478
+ /** CSS custom properties Aeon recipes already read. */
479
+ declare function brandPaletteToCssVars(palette: BrandPalette): Record<string, string>;
480
+ type ApplyBrandPaletteOptions = {
481
+ mode?: AeonColorMode;
482
+ /** Theme id stamped on <html> (default: `brand`). */
483
+ themeId?: string;
484
+ /** Persist mode/theme to localStorage (default: false — brand apps usually own prefs). */
485
+ persist?: boolean;
486
+ root?: HTMLElement;
487
+ };
488
+ /**
489
+ * Apply a product brand palette to the document so Aeon CSS vars match the app.
490
+ * Does not require registering a catalog theme — one call at boot is enough.
491
+ *
492
+ * @example
493
+ * applyBrandPalette({
494
+ * bg: '#000', surface: '#0a0a0a', border: '#262626',
495
+ * text: '#fafafa', muted: '#a1a1aa', accent: '#57ff97',
496
+ * }, { mode: 'dark' })
497
+ */
498
+ declare function applyBrandPalette(input: BrandPaletteInput, options?: ApplyBrandPaletteOptions): BrandPalette;
499
+ /** Serialize palette vars for injecting a `<style>` tag (SSR / non-DOM). */
500
+ declare function brandPaletteToCssText(input: BrandPaletteInput, options?: {
501
+ mode?: AeonColorMode;
502
+ selector?: string;
503
+ }): string;
504
+
505
+ export { AEON_PRINCIPLE, AEON_THEME_TRANSITION_CLASS, type AeonActor, type AeonColorMode, type AeonContext, type AeonDataAttrs, type AeonEvent, type AeonPalette, type AeonSnapshot, type AeonStateValue, type AeonThemeDefinition, type ApplyBrandPaletteOptions, type BrandPalette, type BrandPaletteInput, DEFAULT_COMPACT_MQ, type FieldFace, type LayoutComposition, type LayoutCompositionSpec, type LayoutMode, NORM_PLANE, type NormOrigin, type NormPoint, type NormRect, type RegionLayoutSlot, type ResponsiveLayoutSpec, type ScrollOverflow, type ScrollSnapshot, UI_IS_FUNCTION_OF_STATE, accordionAnatomy, anatomy, appNavAnatomy, appShellAnatomy, applyAeonTheme, applyBrandPalette, asyncAnatomy, avatarAnatomy, barAnatomy, brandPaletteToCssText, brandPaletteToCssVars, buttonAnatomy, checkboxAnatomy, clampNorm, clientFromNorm, comboboxAnatomy, composerAnatomy, contentAnatomy, conversationAnatomy, dialogAnatomy, entryAnatomy, fieldAnatomy, fieldFace, fieldRegions, getCompactMq, getRegionValue, getScrollSnapshot, identityAnatomy, initAeonTheme, insetNormCluster, isAeonColorMode, isAeonMode, isAeonThemeId, layoutCompositionForMode, layoutModeAttrs, listRowAnatomy, matchLayoutMode, measureElement, menuAnatomy, metricStripAnatomy, navAnatomy, normFromClient, normalizeBrandPalette, panelAnatomy, partAttrs, partOnlyAttrs, pinInputAnatomy, playSurfaceAnatomy, popoverAnatomy, profileHeaderAnatomy, progressAnatomy, promptAnatomy, radioGroupAnatomy, regionBoundsForMode, regionLeaf, regionMatches, regionPath, registerAeonThemes, resolveAeonColorMode, resolveRegionRect, runAeonThemeTransition, scopeAttrs, scrollAnatomy, selectAnatomy, separatorAnatomy, sliderAnatomy, stateToAttr, statusBannerAnatomy, switchAnatomy, syncAeonColorMode, tabsAnatomy, themeSwitcherAnatomy, threadAnatomy, toastAnatomy, tooltipAnatomy };
@@ -37,6 +37,31 @@ var dialogAnatomy = anatomy("dialog", [
37
37
  "description",
38
38
  "closeTrigger"
39
39
  ]);
40
+ var promptAnatomy = anatomy("prompt", [
41
+ "root",
42
+ "eyebrow",
43
+ "meta",
44
+ "amount",
45
+ "actions",
46
+ "primary",
47
+ "secondary"
48
+ ]);
49
+ var appNavAnatomy = anatomy("appNav", [
50
+ "root",
51
+ "rail",
52
+ "section",
53
+ "stage",
54
+ "breadcrumb",
55
+ "crumb",
56
+ "back"
57
+ ]);
58
+ var statusBannerAnatomy = anatomy("statusBanner", [
59
+ "root",
60
+ "copy",
61
+ "title",
62
+ "body",
63
+ "actions"
64
+ ]);
40
65
  var tabsAnatomy = anatomy("tabs", [
41
66
  "root",
42
67
  "list",
@@ -557,6 +582,87 @@ function initAeonTheme(themeMap) {
557
582
  applyAeonTheme(themeId, mode, themeMap);
558
583
  return { themeId, mode };
559
584
  }
585
+
586
+ // src/brand-theme.ts
587
+ var DEFAULT_DANGER = { light: "#dc2626", dark: "#f87171" };
588
+ function focusRing(bg, accent) {
589
+ return `0 0 0 2px ${bg}, 0 0 0 4px ${accent}`;
590
+ }
591
+ function normalizeBrandPalette(input, mode = "dark") {
592
+ const danger = input.danger ?? DEFAULT_DANGER[mode];
593
+ return {
594
+ bg: input.bg,
595
+ surface: input.surface,
596
+ surfaceRaised: input.surfaceRaised ?? input.surface,
597
+ border: input.border,
598
+ text: input.text,
599
+ muted: input.muted,
600
+ accent: input.accent,
601
+ accentDim: input.accentDim ?? `${input.accent}2e`,
602
+ danger,
603
+ dangerDim: input.dangerDim ?? `${danger}18`,
604
+ gridLine: input.gridLine ?? (mode === "light" ? "rgba(15, 23, 42, 0.07)" : "rgba(255, 255, 255, 0.05)"),
605
+ font: input.font,
606
+ fontDisplay: input.fontDisplay,
607
+ radius: input.radius,
608
+ focusRing: focusRing(input.bg, input.accent)
609
+ };
610
+ }
611
+ function brandPaletteToCssVars(palette) {
612
+ const vars = {
613
+ "--colors-aeon-bg": palette.bg,
614
+ "--colors-aeon-surface": palette.surface,
615
+ "--colors-aeon-surface-raised": palette.surfaceRaised,
616
+ "--colors-aeon-border": palette.border,
617
+ "--colors-aeon-text": palette.text,
618
+ "--colors-aeon-muted": palette.muted,
619
+ "--colors-aeon-accent": palette.accent,
620
+ "--colors-aeon-accent-dim": palette.accentDim,
621
+ "--colors-aeon-danger": palette.danger,
622
+ "--colors-aeon-danger-dim": palette.dangerDim,
623
+ "--shadows-focus-ring": palette.focusRing,
624
+ "--aeon-grid-line": palette.gridLine
625
+ };
626
+ if (palette.font) vars["--aeon-font"] = palette.font;
627
+ if (palette.fontDisplay) vars["--aeon-font-display"] = palette.fontDisplay;
628
+ if (palette.radius) vars["--aeon-radius"] = palette.radius;
629
+ return vars;
630
+ }
631
+ function applyBrandPalette(input, options = {}) {
632
+ const mode = options.mode ?? "dark";
633
+ const palette = normalizeBrandPalette(input, mode);
634
+ if (typeof document === "undefined") return palette;
635
+ const root = options.root ?? document.documentElement;
636
+ const themeId = options.themeId ?? "brand";
637
+ root.dataset.aeonTheme = themeId;
638
+ root.dataset.aeonMode = mode;
639
+ root.style.colorScheme = mode;
640
+ const vars = brandPaletteToCssVars(palette);
641
+ for (const [key, value] of Object.entries(vars)) {
642
+ root.style.setProperty(key, value);
643
+ }
644
+ const meta = document.querySelector('meta[name="theme-color"]');
645
+ if (meta) meta.setAttribute("content", palette.bg);
646
+ if (options.persist) {
647
+ try {
648
+ localStorage.setItem("aeon-theme", themeId);
649
+ localStorage.setItem("aeon-mode", mode);
650
+ } catch {
651
+ }
652
+ }
653
+ return palette;
654
+ }
655
+ function brandPaletteToCssText(input, options = {}) {
656
+ const mode = options.mode ?? "dark";
657
+ const palette = normalizeBrandPalette(input, mode);
658
+ const vars = brandPaletteToCssVars(palette);
659
+ const body = Object.entries(vars).map(([k, v]) => ` ${k}: ${v};`).join("\n");
660
+ const selector = options.selector ?? "html";
661
+ return `${selector} {
662
+ color-scheme: ${mode};
663
+ ${body}
664
+ }`;
665
+ }
560
666
  export {
561
667
  AEON_PRINCIPLE,
562
668
  AEON_THEME_TRANSITION_CLASS,
@@ -565,11 +671,15 @@ export {
565
671
  UI_IS_FUNCTION_OF_STATE,
566
672
  accordionAnatomy,
567
673
  anatomy,
674
+ appNavAnatomy,
568
675
  appShellAnatomy,
569
676
  applyAeonTheme,
677
+ applyBrandPalette,
570
678
  asyncAnatomy,
571
679
  avatarAnatomy,
572
680
  barAnatomy,
681
+ brandPaletteToCssText,
682
+ brandPaletteToCssVars,
573
683
  buttonAnatomy,
574
684
  checkboxAnatomy,
575
685
  clampNorm,
@@ -602,6 +712,7 @@ export {
602
712
  metricStripAnatomy,
603
713
  navAnatomy,
604
714
  normFromClient,
715
+ normalizeBrandPalette,
605
716
  panelAnatomy,
606
717
  partAttrs,
607
718
  partOnlyAttrs,
@@ -610,6 +721,7 @@ export {
610
721
  popoverAnatomy,
611
722
  profileHeaderAnatomy,
612
723
  progressAnatomy,
724
+ promptAnatomy,
613
725
  radioGroupAnatomy,
614
726
  regionBoundsForMode,
615
727
  regionLeaf,
@@ -625,6 +737,7 @@ export {
625
737
  separatorAnatomy,
626
738
  sliderAnatomy,
627
739
  stateToAttr,
740
+ statusBannerAnatomy,
628
741
  switchAnatomy,
629
742
  syncAeonColorMode,
630
743
  tabsAnatomy,
@@ -40,6 +40,44 @@ export const dialogAnatomy = anatomy('dialog', [
40
40
  'closeTrigger',
41
41
  ] as const)
42
42
 
43
+ /**
44
+ * Prompt — confirm / permission / update-style dialog on top of Dialog.
45
+ * Universal parts: eyebrow, meta, amount, actions (primary + secondary).
46
+ */
47
+ export const promptAnatomy = anatomy('prompt', [
48
+ 'root',
49
+ 'eyebrow',
50
+ 'meta',
51
+ 'amount',
52
+ 'actions',
53
+ 'primary',
54
+ 'secondary',
55
+ ] as const)
56
+
57
+ /**
58
+ * AppNav — section + detail stack (list→detail, settings drill-in).
59
+ */
60
+ export const appNavAnatomy = anatomy('appNav', [
61
+ 'root',
62
+ 'rail',
63
+ 'section',
64
+ 'stage',
65
+ 'breadcrumb',
66
+ 'crumb',
67
+ 'back',
68
+ ] as const)
69
+
70
+ /**
71
+ * StatusBanner — non-blocking top-of-app notice (update ready, syncing, offline).
72
+ */
73
+ export const statusBannerAnatomy = anatomy('statusBanner', [
74
+ 'root',
75
+ 'copy',
76
+ 'title',
77
+ 'body',
78
+ 'actions',
79
+ ] as const)
80
+
43
81
  export const tabsAnatomy = anatomy('tabs', [
44
82
  'root',
45
83
  'list',
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Brand → Aeon CSS variable bridge.
3
+ *
4
+ * Product apps often already have design tokens. Instead of maintaining a parallel
5
+ * `--product-*` layer forever, map them onto Aeon’s runtime vars so `@aeon-ui/ui`
6
+ * recipes and `aeon.css` pick up the brand without forking Panda.
7
+ */
8
+ import type { AeonColorMode } from './theme.js'
9
+
10
+ /** Minimal brand fields — anything omitted gets a safe default. */
11
+ export type BrandPaletteInput = {
12
+ bg: string
13
+ surface: string
14
+ surfaceRaised?: string
15
+ border: string
16
+ text: string
17
+ muted: string
18
+ accent: string
19
+ accentDim?: string
20
+ danger?: string
21
+ dangerDim?: string
22
+ gridLine?: string
23
+ /** Optional product fonts (applied as --aeon-font / --aeon-font-display). */
24
+ font?: string
25
+ fontDisplay?: string
26
+ radius?: string
27
+ }
28
+
29
+ export type BrandPalette = Required<
30
+ Pick<
31
+ BrandPaletteInput,
32
+ | 'bg'
33
+ | 'surface'
34
+ | 'surfaceRaised'
35
+ | 'border'
36
+ | 'text'
37
+ | 'muted'
38
+ | 'accent'
39
+ | 'accentDim'
40
+ | 'danger'
41
+ | 'dangerDim'
42
+ | 'gridLine'
43
+ >
44
+ > & {
45
+ font?: string
46
+ fontDisplay?: string
47
+ radius?: string
48
+ focusRing: string
49
+ }
50
+
51
+ const DEFAULT_DANGER = { light: '#dc2626', dark: '#f87171' } as const
52
+
53
+ function focusRing(bg: string, accent: string) {
54
+ return `0 0 0 2px ${bg}, 0 0 0 4px ${accent}`
55
+ }
56
+
57
+ /** Normalize a partial brand palette into a full Aeon-compatible palette. */
58
+ export function normalizeBrandPalette(
59
+ input: BrandPaletteInput,
60
+ mode: AeonColorMode = 'dark',
61
+ ): BrandPalette {
62
+ const danger = input.danger ?? DEFAULT_DANGER[mode]
63
+ return {
64
+ bg: input.bg,
65
+ surface: input.surface,
66
+ surfaceRaised: input.surfaceRaised ?? input.surface,
67
+ border: input.border,
68
+ text: input.text,
69
+ muted: input.muted,
70
+ accent: input.accent,
71
+ accentDim: input.accentDim ?? `${input.accent}2e`,
72
+ danger,
73
+ dangerDim: input.dangerDim ?? `${danger}18`,
74
+ gridLine:
75
+ input.gridLine ??
76
+ (mode === 'light' ? 'rgba(15, 23, 42, 0.07)' : 'rgba(255, 255, 255, 0.05)'),
77
+ font: input.font,
78
+ fontDisplay: input.fontDisplay,
79
+ radius: input.radius,
80
+ focusRing: focusRing(input.bg, input.accent),
81
+ }
82
+ }
83
+
84
+ /** CSS custom properties Aeon recipes already read. */
85
+ export function brandPaletteToCssVars(palette: BrandPalette): Record<string, string> {
86
+ const vars: Record<string, string> = {
87
+ '--colors-aeon-bg': palette.bg,
88
+ '--colors-aeon-surface': palette.surface,
89
+ '--colors-aeon-surface-raised': palette.surfaceRaised,
90
+ '--colors-aeon-border': palette.border,
91
+ '--colors-aeon-text': palette.text,
92
+ '--colors-aeon-muted': palette.muted,
93
+ '--colors-aeon-accent': palette.accent,
94
+ '--colors-aeon-accent-dim': palette.accentDim,
95
+ '--colors-aeon-danger': palette.danger,
96
+ '--colors-aeon-danger-dim': palette.dangerDim,
97
+ '--shadows-focus-ring': palette.focusRing,
98
+ '--aeon-grid-line': palette.gridLine,
99
+ }
100
+ if (palette.font) vars['--aeon-font'] = palette.font
101
+ if (palette.fontDisplay) vars['--aeon-font-display'] = palette.fontDisplay
102
+ if (palette.radius) vars['--aeon-radius'] = palette.radius
103
+ return vars
104
+ }
105
+
106
+ export type ApplyBrandPaletteOptions = {
107
+ mode?: AeonColorMode
108
+ /** Theme id stamped on <html> (default: `brand`). */
109
+ themeId?: string
110
+ /** Persist mode/theme to localStorage (default: false — brand apps usually own prefs). */
111
+ persist?: boolean
112
+ root?: HTMLElement
113
+ }
114
+
115
+ /**
116
+ * Apply a product brand palette to the document so Aeon CSS vars match the app.
117
+ * Does not require registering a catalog theme — one call at boot is enough.
118
+ *
119
+ * @example
120
+ * applyBrandPalette({
121
+ * bg: '#000', surface: '#0a0a0a', border: '#262626',
122
+ * text: '#fafafa', muted: '#a1a1aa', accent: '#57ff97',
123
+ * }, { mode: 'dark' })
124
+ */
125
+ export function applyBrandPalette(
126
+ input: BrandPaletteInput,
127
+ options: ApplyBrandPaletteOptions = {},
128
+ ): BrandPalette {
129
+ const mode = options.mode ?? 'dark'
130
+ const palette = normalizeBrandPalette(input, mode)
131
+ if (typeof document === 'undefined') return palette
132
+
133
+ const root = options.root ?? document.documentElement
134
+ const themeId = options.themeId ?? 'brand'
135
+ root.dataset.aeonTheme = themeId
136
+ root.dataset.aeonMode = mode
137
+ root.style.colorScheme = mode
138
+
139
+ const vars = brandPaletteToCssVars(palette)
140
+ for (const [key, value] of Object.entries(vars)) {
141
+ root.style.setProperty(key, value)
142
+ }
143
+
144
+ const meta = document.querySelector('meta[name="theme-color"]')
145
+ if (meta) meta.setAttribute('content', palette.bg)
146
+
147
+ if (options.persist) {
148
+ try {
149
+ localStorage.setItem('aeon-theme', themeId)
150
+ localStorage.setItem('aeon-mode', mode)
151
+ } catch {
152
+ /* private mode */
153
+ }
154
+ }
155
+
156
+ return palette
157
+ }
158
+
159
+ /** Serialize palette vars for injecting a `<style>` tag (SSR / non-DOM). */
160
+ export function brandPaletteToCssText(
161
+ input: BrandPaletteInput,
162
+ options: { mode?: AeonColorMode; selector?: string } = {},
163
+ ): string {
164
+ const mode = options.mode ?? 'dark'
165
+ const palette = normalizeBrandPalette(input, mode)
166
+ const vars = brandPaletteToCssVars(palette)
167
+ const body = Object.entries(vars)
168
+ .map(([k, v]) => ` ${k}: ${v};`)
169
+ .join('\n')
170
+ const selector = options.selector ?? 'html'
171
+ return `${selector} {\n color-scheme: ${mode};\n${body}\n}`
172
+ }
@@ -6,3 +6,4 @@ export * from './types.js'
6
6
  export * from './scroll.js'
7
7
  export * from './layout.js'
8
8
  export * from './theme.js'
9
+ export * from './brand-theme.js'
@@ -5,3 +5,4 @@
5
5
  @import './theme-runtime.css';
6
6
  @import './src/theme/scrollbars.css';
7
7
  @import './src/theme/chrome.css';
8
+ @import './src/theme/consumer-shell.css';