aeon-ui-engine 1.1.0 → 1.3.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.
- package/CHANGELOG.md +35 -0
- package/CONSUMER.md +45 -0
- package/package.json +1 -1
- package/packages/core/dist/index.d.ts +88 -8
- package/packages/core/dist/index.js +123 -0
- package/packages/core/src/anatomy.ts +38 -0
- package/packages/core/src/brand-theme.ts +183 -0
- package/packages/core/src/index.ts +1 -0
- package/packages/panda/aeon.css +1 -0
- package/packages/panda/src/theme/consumer-shell.css +250 -0
- package/packages/panda/src/theme/electron.css +114 -0
- package/packages/primitives/dist/chunk-4NOKRUSG.js +81 -0
- package/packages/primitives/dist/index.d.ts +1 -0
- package/packages/primitives/dist/index.js +16 -6
- package/packages/primitives/dist/machines/app-nav.d.ts +96 -0
- package/packages/primitives/dist/machines/app-nav.js +12 -0
- package/packages/primitives/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/packages/primitives/src/__tests__/app-nav.test.ts +30 -0
- package/packages/primitives/src/index.ts +1 -0
- package/packages/primitives/src/machines/app-nav.ts +107 -0
- package/packages/react/dist/index.d.ts +135 -3
- package/packages/react/dist/index.js +1126 -720
- package/packages/react/src/app-nav/index.tsx +249 -0
- package/packages/react/src/avatar/index.tsx +4 -1
- package/packages/react/src/hooks/use-fit-text.ts +59 -0
- package/packages/react/src/index.ts +4 -0
- package/packages/react/src/prompt/index.tsx +161 -0
- package/packages/react/src/status-banner/index.tsx +84 -0
- package/packages/ui/src/app-nav.tsx +2 -0
- package/packages/ui/src/index.ts +4 -0
- package/packages/ui/src/prompt.tsx +29 -0
- package/packages/ui/src/status-banner.tsx +1 -0
- package/scripts/vite-aliases.mjs +8 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,40 @@ 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.3.0] - 2026-07-30
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- **Consumer shell defaults** aligned with dense Electron wallet chrome:
|
|
13
|
+
- Headless Dialog backdrop / positioner / content (so Prompt works without `@aeon-ui/ui` recipe classes)
|
|
14
|
+
- Prompt title + description typography (`--aeon-font-display`)
|
|
15
|
+
- StatusBanner sticky top placement
|
|
16
|
+
- AppNav dense 28px breadcrumb / back rail
|
|
17
|
+
- **`applyBrandPalette` fonts** also set `--fonts-ui`, `--fonts-body`, `--fonts-display` so recipes pick up brand typefaces
|
|
18
|
+
- **Electron CSS** — fill-height `.app-shell` / `.stage`, `.titlebar` alias, compact 3px scrollbars, hover-reveal scroll helper
|
|
19
|
+
|
|
20
|
+
### Docs
|
|
21
|
+
|
|
22
|
+
- [PRODUCT_SHELL.md](./docs/PRODUCT_SHELL.md) — headless Prompt requires consumer-shell; do not re-copy shell CSS into products
|
|
23
|
+
|
|
24
|
+
## [1.2.0] - 2026-07-30
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- **Brand palette bridge** — `applyBrandPalette`, `normalizeBrandPalette`, `brandPaletteToCssText` map product tokens onto Aeon CSS vars without a parallel token sheet.
|
|
29
|
+
- **AppNav** — `appNavMachine` + `AppNav` / `useAppNavMachine` for section + detail stack navigation with breadcrumb/back parts.
|
|
30
|
+
- **Prompt** — confirm/permission-style compound on top of Dialog (`eyebrow`, `meta`, `amount`, `actions`).
|
|
31
|
+
- **StatusBanner** — non-blocking top-of-app notice compound.
|
|
32
|
+
- **`useFitText`** — shrink-to-fit metric/title helper.
|
|
33
|
+
- **Consumer shell CSS** — baseline Prompt / StatusBanner / AppNav styles in `aeon.css`.
|
|
34
|
+
- **Electron chrome CSS** — optional `@aeon-ui/panda/electron.css` titlebar drag helpers.
|
|
35
|
+
- Docs: [PRODUCT_SHELL.md](./docs/PRODUCT_SHELL.md).
|
|
36
|
+
|
|
37
|
+
### Changed
|
|
38
|
+
|
|
39
|
+
- Avatar root also projects `ready` alongside `loaded` for friendlier product CSS.
|
|
40
|
+
- `aeonUiVitePlugin` aliases include consumer-shell and electron CSS paths.
|
|
41
|
+
|
|
8
42
|
## [1.1.0] - 2026-07-30
|
|
9
43
|
|
|
10
44
|
### Added
|
|
@@ -59,6 +93,7 @@ Versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
59
93
|
|
|
60
94
|
- Initial npm release of **aeon-ui-engine** — monorepo bundle with headless statechart primitives, React bindings, Panda styled layer, and themes.
|
|
61
95
|
|
|
96
|
+
[1.2.0]: https://github.com/GenericCPU/aeon-ui/compare/v1.1.0...v1.2.0
|
|
62
97
|
[1.1.0]: https://github.com/GenericCPU/aeon-ui/compare/v1.0.3...v1.1.0
|
|
63
98
|
[1.0.3]: https://github.com/GenericCPU/aeon-ui/compare/v1.0.2...v1.0.3
|
|
64
99
|
[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
|
@@ -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"
|
|
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" | "
|
|
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" | "
|
|
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" | "
|
|
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" | "
|
|
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" | "
|
|
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" | "
|
|
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
|
-
|
|
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,97 @@ 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) {
|
|
627
|
+
vars["--aeon-font"] = palette.font;
|
|
628
|
+
vars["--fonts-ui"] = palette.font;
|
|
629
|
+
vars["--fonts-body"] = palette.font;
|
|
630
|
+
}
|
|
631
|
+
if (palette.fontDisplay) {
|
|
632
|
+
vars["--aeon-font-display"] = palette.fontDisplay;
|
|
633
|
+
vars["--fonts-display"] = palette.fontDisplay;
|
|
634
|
+
}
|
|
635
|
+
if (palette.radius) {
|
|
636
|
+
vars["--aeon-radius"] = palette.radius;
|
|
637
|
+
vars["--radii-aeon"] = palette.radius;
|
|
638
|
+
}
|
|
639
|
+
return vars;
|
|
640
|
+
}
|
|
641
|
+
function applyBrandPalette(input, options = {}) {
|
|
642
|
+
const mode = options.mode ?? "dark";
|
|
643
|
+
const palette = normalizeBrandPalette(input, mode);
|
|
644
|
+
if (typeof document === "undefined") return palette;
|
|
645
|
+
const root = options.root ?? document.documentElement;
|
|
646
|
+
const themeId = options.themeId ?? "brand";
|
|
647
|
+
root.dataset.aeonTheme = themeId;
|
|
648
|
+
root.dataset.aeonMode = mode;
|
|
649
|
+
root.style.colorScheme = mode;
|
|
650
|
+
const vars = brandPaletteToCssVars(palette);
|
|
651
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
652
|
+
root.style.setProperty(key, value);
|
|
653
|
+
}
|
|
654
|
+
const meta = document.querySelector('meta[name="theme-color"]');
|
|
655
|
+
if (meta) meta.setAttribute("content", palette.bg);
|
|
656
|
+
if (options.persist) {
|
|
657
|
+
try {
|
|
658
|
+
localStorage.setItem("aeon-theme", themeId);
|
|
659
|
+
localStorage.setItem("aeon-mode", mode);
|
|
660
|
+
} catch {
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
return palette;
|
|
664
|
+
}
|
|
665
|
+
function brandPaletteToCssText(input, options = {}) {
|
|
666
|
+
const mode = options.mode ?? "dark";
|
|
667
|
+
const palette = normalizeBrandPalette(input, mode);
|
|
668
|
+
const vars = brandPaletteToCssVars(palette);
|
|
669
|
+
const body = Object.entries(vars).map(([k, v]) => ` ${k}: ${v};`).join("\n");
|
|
670
|
+
const selector = options.selector ?? "html";
|
|
671
|
+
return `${selector} {
|
|
672
|
+
color-scheme: ${mode};
|
|
673
|
+
${body}
|
|
674
|
+
}`;
|
|
675
|
+
}
|
|
560
676
|
export {
|
|
561
677
|
AEON_PRINCIPLE,
|
|
562
678
|
AEON_THEME_TRANSITION_CLASS,
|
|
@@ -565,11 +681,15 @@ export {
|
|
|
565
681
|
UI_IS_FUNCTION_OF_STATE,
|
|
566
682
|
accordionAnatomy,
|
|
567
683
|
anatomy,
|
|
684
|
+
appNavAnatomy,
|
|
568
685
|
appShellAnatomy,
|
|
569
686
|
applyAeonTheme,
|
|
687
|
+
applyBrandPalette,
|
|
570
688
|
asyncAnatomy,
|
|
571
689
|
avatarAnatomy,
|
|
572
690
|
barAnatomy,
|
|
691
|
+
brandPaletteToCssText,
|
|
692
|
+
brandPaletteToCssVars,
|
|
573
693
|
buttonAnatomy,
|
|
574
694
|
checkboxAnatomy,
|
|
575
695
|
clampNorm,
|
|
@@ -602,6 +722,7 @@ export {
|
|
|
602
722
|
metricStripAnatomy,
|
|
603
723
|
navAnatomy,
|
|
604
724
|
normFromClient,
|
|
725
|
+
normalizeBrandPalette,
|
|
605
726
|
panelAnatomy,
|
|
606
727
|
partAttrs,
|
|
607
728
|
partOnlyAttrs,
|
|
@@ -610,6 +731,7 @@ export {
|
|
|
610
731
|
popoverAnatomy,
|
|
611
732
|
profileHeaderAnatomy,
|
|
612
733
|
progressAnatomy,
|
|
734
|
+
promptAnatomy,
|
|
613
735
|
radioGroupAnatomy,
|
|
614
736
|
regionBoundsForMode,
|
|
615
737
|
regionLeaf,
|
|
@@ -625,6 +747,7 @@ export {
|
|
|
625
747
|
separatorAnatomy,
|
|
626
748
|
sliderAnatomy,
|
|
627
749
|
stateToAttr,
|
|
750
|
+
statusBannerAnatomy,
|
|
628
751
|
switchAnatomy,
|
|
629
752
|
syncAeonColorMode,
|
|
630
753
|
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,183 @@
|
|
|
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) {
|
|
101
|
+
// Recipes read --fonts-ui / --fonts-body; also expose --aeon-font for shells.
|
|
102
|
+
vars['--aeon-font'] = palette.font
|
|
103
|
+
vars['--fonts-ui'] = palette.font
|
|
104
|
+
vars['--fonts-body'] = palette.font
|
|
105
|
+
}
|
|
106
|
+
if (palette.fontDisplay) {
|
|
107
|
+
vars['--aeon-font-display'] = palette.fontDisplay
|
|
108
|
+
vars['--fonts-display'] = palette.fontDisplay
|
|
109
|
+
}
|
|
110
|
+
if (palette.radius) {
|
|
111
|
+
vars['--aeon-radius'] = palette.radius
|
|
112
|
+
vars['--radii-aeon'] = palette.radius
|
|
113
|
+
}
|
|
114
|
+
return vars
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export type ApplyBrandPaletteOptions = {
|
|
118
|
+
mode?: AeonColorMode
|
|
119
|
+
/** Theme id stamped on <html> (default: `brand`). */
|
|
120
|
+
themeId?: string
|
|
121
|
+
/** Persist mode/theme to localStorage (default: false — brand apps usually own prefs). */
|
|
122
|
+
persist?: boolean
|
|
123
|
+
root?: HTMLElement
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Apply a product brand palette to the document so Aeon CSS vars match the app.
|
|
128
|
+
* Does not require registering a catalog theme — one call at boot is enough.
|
|
129
|
+
*
|
|
130
|
+
* @example
|
|
131
|
+
* applyBrandPalette({
|
|
132
|
+
* bg: '#000', surface: '#0a0a0a', border: '#262626',
|
|
133
|
+
* text: '#fafafa', muted: '#a1a1aa', accent: '#57ff97',
|
|
134
|
+
* }, { mode: 'dark' })
|
|
135
|
+
*/
|
|
136
|
+
export function applyBrandPalette(
|
|
137
|
+
input: BrandPaletteInput,
|
|
138
|
+
options: ApplyBrandPaletteOptions = {},
|
|
139
|
+
): BrandPalette {
|
|
140
|
+
const mode = options.mode ?? 'dark'
|
|
141
|
+
const palette = normalizeBrandPalette(input, mode)
|
|
142
|
+
if (typeof document === 'undefined') return palette
|
|
143
|
+
|
|
144
|
+
const root = options.root ?? document.documentElement
|
|
145
|
+
const themeId = options.themeId ?? 'brand'
|
|
146
|
+
root.dataset.aeonTheme = themeId
|
|
147
|
+
root.dataset.aeonMode = mode
|
|
148
|
+
root.style.colorScheme = mode
|
|
149
|
+
|
|
150
|
+
const vars = brandPaletteToCssVars(palette)
|
|
151
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
152
|
+
root.style.setProperty(key, value)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const meta = document.querySelector('meta[name="theme-color"]')
|
|
156
|
+
if (meta) meta.setAttribute('content', palette.bg)
|
|
157
|
+
|
|
158
|
+
if (options.persist) {
|
|
159
|
+
try {
|
|
160
|
+
localStorage.setItem('aeon-theme', themeId)
|
|
161
|
+
localStorage.setItem('aeon-mode', mode)
|
|
162
|
+
} catch {
|
|
163
|
+
/* private mode */
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return palette
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Serialize palette vars for injecting a `<style>` tag (SSR / non-DOM). */
|
|
171
|
+
export function brandPaletteToCssText(
|
|
172
|
+
input: BrandPaletteInput,
|
|
173
|
+
options: { mode?: AeonColorMode; selector?: string } = {},
|
|
174
|
+
): string {
|
|
175
|
+
const mode = options.mode ?? 'dark'
|
|
176
|
+
const palette = normalizeBrandPalette(input, mode)
|
|
177
|
+
const vars = brandPaletteToCssVars(palette)
|
|
178
|
+
const body = Object.entries(vars)
|
|
179
|
+
.map(([k, v]) => ` ${k}: ${v};`)
|
|
180
|
+
.join('\n')
|
|
181
|
+
const selector = options.selector ?? 'html'
|
|
182
|
+
return `${selector} {\n color-scheme: ${mode};\n${body}\n}`
|
|
183
|
+
}
|
package/packages/panda/aeon.css
CHANGED