@calliopelabs/ui 0.0.0-stage → 0.3.1

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 (105) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +334 -2
  3. package/dist/appearance/AppearancePicker.d.ts +10 -0
  4. package/dist/appearance/AppearancePicker.js +75 -0
  5. package/dist/appearance/AppearanceProvider.d.ts +53 -0
  6. package/dist/appearance/AppearanceProvider.js +113 -0
  7. package/dist/appearance/AppearanceScript.d.ts +16 -0
  8. package/dist/appearance/AppearanceScript.js +14 -0
  9. package/dist/appearance/appearance.d.ts +90 -0
  10. package/dist/appearance/appearance.js +151 -0
  11. package/dist/appearance/boot.d.ts +30 -0
  12. package/dist/appearance/boot.js +91 -0
  13. package/dist/appearance/brands.d.ts +187 -0
  14. package/dist/appearance/brands.js +150 -0
  15. package/dist/appearance/product.d.ts +89 -0
  16. package/dist/appearance/product.js +96 -0
  17. package/dist/data/DataTable.d.ts +66 -0
  18. package/dist/data/DataTable.js +148 -0
  19. package/dist/data/List.d.ts +55 -0
  20. package/dist/data/List.js +41 -0
  21. package/dist/data/types.d.ts +92 -0
  22. package/dist/data/types.js +2 -0
  23. package/dist/data/use-row-selection.d.ts +17 -0
  24. package/dist/data/use-row-selection.js +51 -0
  25. package/dist/index.d.ts +50 -0
  26. package/dist/index.js +61 -0
  27. package/dist/lib/cn.d.ts +3 -0
  28. package/dist/lib/cn.js +6 -0
  29. package/dist/lib/local-store.d.ts +12 -0
  30. package/dist/lib/local-store.js +54 -0
  31. package/dist/nav/Nav.d.ts +21 -0
  32. package/dist/nav/Nav.js +97 -0
  33. package/dist/nav/model.d.ts +77 -0
  34. package/dist/nav/model.js +72 -0
  35. package/dist/overlays/ConfirmDialog.d.ts +37 -0
  36. package/dist/overlays/ConfirmDialog.js +70 -0
  37. package/dist/overlays/Dialog.d.ts +18 -0
  38. package/dist/overlays/Dialog.js +39 -0
  39. package/dist/overlays/Drawer.d.ts +24 -0
  40. package/dist/overlays/Drawer.js +42 -0
  41. package/dist/page/Breadcrumbs.d.ts +20 -0
  42. package/dist/page/Breadcrumbs.js +25 -0
  43. package/dist/page/PageFrame.d.ts +17 -0
  44. package/dist/page/PageFrame.js +14 -0
  45. package/dist/page/PageHeader.d.ts +28 -0
  46. package/dist/page/PageHeader.js +13 -0
  47. package/dist/page/Section.d.ts +18 -0
  48. package/dist/page/Section.js +12 -0
  49. package/dist/page/TabNav.d.ts +19 -0
  50. package/dist/page/TabNav.js +16 -0
  51. package/dist/primitives/Button.d.ts +16 -0
  52. package/dist/primitives/Button.js +36 -0
  53. package/dist/primitives/Checkbox.d.ts +8 -0
  54. package/dist/primitives/Checkbox.js +13 -0
  55. package/dist/primitives/DropdownMenu.d.ts +11 -0
  56. package/dist/primitives/DropdownMenu.js +25 -0
  57. package/dist/primitives/Input.d.ts +2 -0
  58. package/dist/primitives/Input.js +5 -0
  59. package/dist/primitives/Popover.d.ts +5 -0
  60. package/dist/primitives/Popover.js +13 -0
  61. package/dist/primitives/Skeleton.d.ts +3 -0
  62. package/dist/primitives/Skeleton.js +6 -0
  63. package/dist/primitives/Tabs.d.ts +12 -0
  64. package/dist/primitives/Tabs.js +22 -0
  65. package/dist/provider/UIProvider.d.ts +29 -0
  66. package/dist/provider/UIProvider.js +21 -0
  67. package/dist/provider/messages.d.ts +106 -0
  68. package/dist/provider/messages.js +104 -0
  69. package/dist/shell/AppShell.d.ts +43 -0
  70. package/dist/shell/AppShell.js +62 -0
  71. package/dist/shell/BrandMark.d.ts +14 -0
  72. package/dist/shell/BrandMark.js +14 -0
  73. package/dist/shell/RailPanel.d.ts +39 -0
  74. package/dist/shell/RailPanel.js +62 -0
  75. package/dist/shell/RailTree.d.ts +35 -0
  76. package/dist/shell/RailTree.js +32 -0
  77. package/dist/shell/Sidebar.d.ts +26 -0
  78. package/dist/shell/Sidebar.js +21 -0
  79. package/dist/shell/UserMenu.d.ts +22 -0
  80. package/dist/shell/UserMenu.js +31 -0
  81. package/dist/shell/placement.d.ts +10 -0
  82. package/dist/shell/placement.js +6 -0
  83. package/dist/shell/rail-state.d.ts +13 -0
  84. package/dist/shell/rail-state.js +28 -0
  85. package/dist/states/EmptyState.d.ts +20 -0
  86. package/dist/states/EmptyState.js +15 -0
  87. package/dist/states/ErrorState.d.ts +23 -0
  88. package/dist/states/ErrorState.js +16 -0
  89. package/dist/states/LoadingState.d.ts +14 -0
  90. package/dist/states/LoadingState.js +14 -0
  91. package/dist/status/StatusBadge.d.ts +19 -0
  92. package/dist/status/StatusBadge.js +13 -0
  93. package/dist/status/StatusDot.d.ts +14 -0
  94. package/dist/status/StatusDot.js +7 -0
  95. package/dist/status/tones.d.ts +17 -0
  96. package/dist/status/tones.js +29 -0
  97. package/dist/styles/preset.css +175 -0
  98. package/dist/styles/theme.css +339 -0
  99. package/dist/theme/contrast.d.ts +8 -0
  100. package/dist/theme/contrast.js +18 -0
  101. package/dist/theme/css.d.ts +23 -0
  102. package/dist/theme/css.js +116 -0
  103. package/dist/theme/tokens.d.ts +195 -0
  104. package/dist/theme/tokens.js +294 -0
  105. package/package.json +112 -4
@@ -0,0 +1,175 @@
1
+ /*
2
+ * @calliopelabs/ui Tailwind 4 preset.
3
+ *
4
+ * @import "tailwindcss";
5
+ * @import "@calliopelabs/ui/preset.css";
6
+ *
7
+ * It brings the theme tokens, maps them onto Tailwind's colour, radius, font,
8
+ * type and motion scales, points Tailwind at this package's components so
9
+ * their classes are generated, and sets the family's base layer.
10
+ */
11
+ @import "./theme.css";
12
+
13
+ /* The package's own components: their classes are generated with the app's. */
14
+ @source "../";
15
+
16
+ /* Dark follows the ground, not the OS: `data-mode` on <html> says which. */
17
+ @custom-variant dark (&:where([data-mode="dark"], [data-mode="dark"] *));
18
+
19
+ @theme inline {
20
+ --color-background: var(--background);
21
+ --color-foreground: var(--foreground);
22
+ --color-card: var(--card);
23
+ --color-card-foreground: var(--card-foreground);
24
+ --color-popover: var(--popover);
25
+ --color-popover-foreground: var(--popover-foreground);
26
+ --color-primary: var(--primary);
27
+ --color-primary-foreground: var(--primary-foreground);
28
+ --color-secondary: var(--secondary);
29
+ --color-secondary-foreground: var(--secondary-foreground);
30
+ --color-muted: var(--muted);
31
+ --color-muted-foreground: var(--muted-foreground);
32
+ --color-accent: var(--accent);
33
+ --color-accent-foreground: var(--accent-foreground);
34
+ --color-destructive: var(--destructive);
35
+ --color-border: var(--border);
36
+ --color-input: var(--input);
37
+ --color-ring: var(--ring);
38
+ --color-chart-1: var(--chart-1);
39
+ --color-chart-2: var(--chart-2);
40
+ --color-chart-3: var(--chart-3);
41
+ --color-chart-4: var(--chart-4);
42
+ --color-chart-5: var(--chart-5);
43
+ --color-sidebar: var(--sidebar);
44
+ --color-sidebar-foreground: var(--sidebar-foreground);
45
+ --color-sidebar-primary: var(--sidebar-primary);
46
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
47
+ --color-sidebar-accent: var(--sidebar-accent);
48
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
49
+ --color-sidebar-border: var(--sidebar-border);
50
+ --color-sidebar-ring: var(--sidebar-ring);
51
+ --color-surface-0: var(--surface-0);
52
+ --color-surface-1: var(--surface-1);
53
+ --color-surface-2: var(--surface-2);
54
+
55
+ /* Semantic status: solid base, -fg text on the tint, -bg fill, -border hairline. */
56
+ --color-success: var(--success);
57
+ --color-success-fg: var(--success-fg);
58
+ --color-success-bg: var(--success-bg);
59
+ --color-success-border: var(--success-border);
60
+ --color-warning: var(--warning);
61
+ --color-warning-fg: var(--warning-fg);
62
+ --color-warning-bg: var(--warning-bg);
63
+ --color-warning-border: var(--warning-border);
64
+ --color-info: var(--info);
65
+ --color-info-fg: var(--info-fg);
66
+ --color-info-bg: var(--info-bg);
67
+ --color-info-border: var(--info-border);
68
+ --color-danger: var(--danger);
69
+ --color-danger-fg: var(--danger-fg);
70
+ --color-danger-bg: var(--danger-bg);
71
+ --color-danger-border: var(--danger-border);
72
+
73
+ /* IBM Plex Sans for UI, IBM Plex Mono for anything technical, DM Sans for headings. */
74
+ --font-sans: var(--font-plex-sans, "IBM Plex Sans"), system-ui, sans-serif;
75
+ --font-mono:
76
+ var(--font-plex-mono, "IBM Plex Mono"), ui-monospace, SFMono-Regular, Menlo, monospace;
77
+ --font-head: var(--font-dm-sans, "DM Sans"), system-ui, sans-serif;
78
+
79
+ /* `rounded-lg` is the corners setting itself; clamped so 0px never goes negative. */
80
+ --radius-sm: max(0px, calc(var(--radius) - 4px));
81
+ --radius-md: max(0px, calc(var(--radius) - 2px));
82
+ --radius-lg: var(--radius);
83
+ --radius-xl: calc(var(--radius) + 4px);
84
+ --radius-2xl: calc(var(--radius) + 8px);
85
+ --radius-3xl: calc(var(--radius) + 12px);
86
+ --radius-4xl: calc(var(--radius) + 16px);
87
+ }
88
+
89
+ @theme {
90
+ /* The micro step below xs, for eyebrows, counts and table heads. */
91
+ --text-2xs: 0.6875rem;
92
+ --text-2xs--line-height: 1rem;
93
+
94
+ --ease-standard: cubic-bezier(0.2, 0, 0, 1);
95
+ --ease-emphasized: cubic-bezier(0.3, 0, 0, 1);
96
+
97
+ /* Overlays arrive calmly and leave at once. */
98
+ --animate-cui-fade-in: cui-fade-in var(--motion-fast) var(--ease-standard);
99
+ --animate-cui-pop-in: cui-pop-in var(--motion-base) var(--ease-emphasized);
100
+ --animate-cui-slide-in-right: cui-slide-in-right var(--motion-base) var(--ease-emphasized);
101
+ --animate-cui-slide-in-left: cui-slide-in-left var(--motion-base) var(--ease-emphasized);
102
+ --animate-cui-slide-in-bottom: cui-slide-in-bottom var(--motion-base) var(--ease-emphasized);
103
+ /* A table's rows being replaced: a short bar travelling along the top edge. */
104
+ --animate-cui-progress: cui-progress 1.4s var(--ease-standard) infinite;
105
+
106
+ @keyframes cui-fade-in {
107
+ from {
108
+ opacity: 0;
109
+ }
110
+ }
111
+ /* `transform` composes with the `translate` that centres a dialog. */
112
+ @keyframes cui-pop-in {
113
+ from {
114
+ opacity: 0;
115
+ transform: scale(0.98);
116
+ }
117
+ }
118
+ @keyframes cui-slide-in-right {
119
+ from {
120
+ transform: translateX(1.5rem);
121
+ opacity: 0;
122
+ }
123
+ }
124
+ @keyframes cui-slide-in-left {
125
+ from {
126
+ transform: translateX(-1.5rem);
127
+ opacity: 0;
128
+ }
129
+ }
130
+ @keyframes cui-progress {
131
+ from {
132
+ translate: -100% 0;
133
+ }
134
+ to {
135
+ translate: 300% 0;
136
+ }
137
+ }
138
+ @keyframes cui-slide-in-bottom {
139
+ from {
140
+ transform: translateY(1.5rem);
141
+ opacity: 0;
142
+ }
143
+ }
144
+ }
145
+
146
+ @layer base {
147
+ *,
148
+ ::before,
149
+ ::after {
150
+ border-color: var(--border);
151
+ outline-color: color-mix(in oklab, var(--ring) 50%, transparent);
152
+ }
153
+
154
+ body {
155
+ @apply bg-background font-sans text-foreground;
156
+ }
157
+
158
+ h1,
159
+ h2,
160
+ h3 {
161
+ @apply font-head;
162
+ }
163
+
164
+ /* Motion is never the only carrier of meaning; with reduced motion it stops. */
165
+ @media (prefers-reduced-motion: reduce) {
166
+ *,
167
+ ::before,
168
+ ::after {
169
+ animation-duration: 0.01ms !important;
170
+ animation-iteration-count: 1 !important;
171
+ transition-duration: 0.01ms !important;
172
+ scroll-behavior: auto !important;
173
+ }
174
+ }
175
+ }
@@ -0,0 +1,339 @@
1
+ /*
2
+ * @calliopelabs/ui theme tokens.
3
+ * Generated by `npm run theme` from src/theme/tokens.ts. Do not edit by hand.
4
+ *
5
+ * The appearance axes ride on attributes of <html>, set before first paint by
6
+ * AppearanceScript and kept in step by AppearanceProvider:
7
+ * data-mode dark | light (follows the ground)
8
+ * data-ground the surface ramp
9
+ * data-accent a family accent, the product's brand accent, or custom
10
+ * data-density compact | cards (any element may carry its own)
11
+ * --radius the corners setting, inline
12
+ * Semantic status tokens are defined per mode only: no ground and no accent
13
+ * may change what ok, warn, info or error look like.
14
+ */
15
+
16
+ /* Mode-independent tokens */
17
+ :root {
18
+ --radius: 2px;
19
+ --motion-fast: 120ms;
20
+ --motion-base: 200ms;
21
+ --motion-slow: 320ms;
22
+ }
23
+
24
+ /* Dark: black ground, green accent */
25
+ :root,
26
+ [data-mode="dark"] {
27
+ color-scheme: dark;
28
+ --background: #000000;
29
+ --card: #0a0a0a;
30
+ --popover: #141414;
31
+ --secondary: #161616;
32
+ --muted: #111111;
33
+ --accent: #1b1b1b;
34
+ --muted-foreground: #848b88;
35
+ --border: rgba(255, 255, 255, 0.1);
36
+ --input: rgba(255, 255, 255, 0.1);
37
+ --sidebar: var(--brand-sidebar, #0a0a0a);
38
+ --sidebar-accent: color-mix(in oklab, var(--primary) 16%, transparent);
39
+ --sidebar-border: rgba(255, 255, 255, 0.08);
40
+ --foreground: #f0f2f1;
41
+ --card-foreground: #f0f2f1;
42
+ --popover-foreground: #f0f2f1;
43
+ --secondary-foreground: #f0f2f1;
44
+ --accent-foreground: #f0f2f1;
45
+ --brand-primary: #2f9e52;
46
+ --brand-on-primary: #04241b;
47
+ --primary: var(--brand-primary);
48
+ --primary-foreground: var(--brand-on-primary);
49
+ --destructive: #ff6b6b;
50
+ --ring: var(--brand-primary);
51
+ --chart-1: var(--brand-primary);
52
+ --chart-2: #8fd82a;
53
+ --chart-3: #cbf39d;
54
+ --chart-4: #e0b34a;
55
+ --chart-5: #0b6644;
56
+ --sidebar-foreground: #f0f2f1;
57
+ --sidebar-primary: var(--brand-primary);
58
+ --sidebar-primary-foreground: var(--brand-on-primary);
59
+ --sidebar-accent-foreground: #f0f2f1;
60
+ --sidebar-ring: var(--brand-primary);
61
+ --surface-0: var(--background);
62
+ --surface-1: var(--card);
63
+ --surface-2: var(--popover);
64
+ --success: #8fd82a;
65
+ --success-fg: #a6e34d;
66
+ --success-bg: rgba(143, 216, 42, 0.1);
67
+ --success-border: rgba(143, 216, 42, 0.35);
68
+ --warning: #f0c34e;
69
+ --warning-fg: #f0c34e;
70
+ --warning-bg: rgba(240, 195, 78, 0.1);
71
+ --warning-border: rgba(240, 195, 78, 0.35);
72
+ --info: #2f9e52;
73
+ --info-fg: #5cbb79;
74
+ --info-bg: rgba(47, 158, 82, 0.14);
75
+ --info-border: rgba(47, 158, 82, 0.4);
76
+ --danger: #ff6b6b;
77
+ --danger-fg: #ff8a8a;
78
+ --danger-bg: rgba(255, 107, 107, 0.1);
79
+ --danger-border: rgba(255, 107, 107, 0.35);
80
+ }
81
+
82
+ /* Light: neutral ground, green accent */
83
+ [data-mode="light"] {
84
+ color-scheme: light;
85
+ --background: #fafaf9;
86
+ --card: #ffffff;
87
+ --popover: #ffffff;
88
+ --secondary: #f1f1f0;
89
+ --muted: #f1f1f0;
90
+ --accent: #f3ebe5;
91
+ --muted-foreground: #5c5b58;
92
+ --border: #dfdedc;
93
+ --input: #dfdedc;
94
+ --sidebar: #f4f3f1;
95
+ --sidebar-accent: #f3ebe5;
96
+ --sidebar-border: #dfdedc;
97
+ --foreground: #1e1d1b;
98
+ --card-foreground: #1e1d1b;
99
+ --popover-foreground: #1e1d1b;
100
+ --secondary-foreground: #1e1d1b;
101
+ --accent-foreground: #1e1d1b;
102
+ --brand-primary: #26803f;
103
+ --brand-on-primary: #f3fbf7;
104
+ --primary: var(--brand-primary);
105
+ --primary-foreground: var(--brand-on-primary);
106
+ --destructive: #d40c1a;
107
+ --ring: var(--brand-primary);
108
+ --chart-1: var(--brand-primary);
109
+ --chart-2: #5c9c1a;
110
+ --chart-3: #8a7a5c;
111
+ --chart-4: #b8860b;
112
+ --chart-5: #26803f;
113
+ --sidebar-foreground: #1e1d1b;
114
+ --sidebar-primary: var(--brand-primary);
115
+ --sidebar-primary-foreground: var(--brand-on-primary);
116
+ --sidebar-accent-foreground: #1e1d1b;
117
+ --sidebar-ring: var(--brand-primary);
118
+ --surface-0: var(--background);
119
+ --surface-1: var(--card);
120
+ --surface-2: var(--popover);
121
+ --success: #508d23;
122
+ --success-fg: #305a12;
123
+ --success-bg: #e3f5da;
124
+ --success-border: #b4d7a2;
125
+ --warning: #cb7a00;
126
+ --warning-fg: #824c12;
127
+ --warning-bg: #fef0d4;
128
+ --warning-border: #e8c184;
129
+ --info: #2f8543;
130
+ --info-fg: #1b622d;
131
+ --info-bg: #e2f4e3;
132
+ --info-border: #acd8b1;
133
+ --danger: #df2225;
134
+ --danger-fg: #bb0916;
135
+ --danger-bg: #ffeae9;
136
+ --danger-border: #ffb1aa;
137
+ }
138
+
139
+ /* Ground: Black (dark) */
140
+ [data-mode="dark"][data-ground="black"] {
141
+ --background: #000000;
142
+ --card: #0a0a0a;
143
+ --popover: #141414;
144
+ --secondary: #161616;
145
+ --muted: #111111;
146
+ --accent: #1b1b1b;
147
+ --muted-foreground: #848b88;
148
+ --border: rgba(255, 255, 255, 0.1);
149
+ --input: rgba(255, 255, 255, 0.1);
150
+ --sidebar: var(--brand-sidebar, #0a0a0a);
151
+ --sidebar-accent: color-mix(in oklab, var(--primary) 16%, transparent);
152
+ --sidebar-border: rgba(255, 255, 255, 0.08);
153
+ }
154
+
155
+ /* Ground: Charcoal (dark) */
156
+ [data-mode="dark"][data-ground="charcoal"] {
157
+ --background: #0e0f10;
158
+ --card: #161819;
159
+ --popover: #1e2022;
160
+ --secondary: #202325;
161
+ --muted: #191b1c;
162
+ --accent: #232628;
163
+ --muted-foreground: #8b9194;
164
+ --border: rgba(255, 255, 255, 0.1);
165
+ --input: rgba(255, 255, 255, 0.1);
166
+ --sidebar: var(--brand-sidebar, #161819);
167
+ --sidebar-accent: color-mix(in oklab, var(--primary) 16%, transparent);
168
+ --sidebar-border: rgba(255, 255, 255, 0.08);
169
+ }
170
+
171
+ /* Ground: Graphite (dark) */
172
+ [data-mode="dark"][data-ground="graphite"] {
173
+ --background: #111214;
174
+ --card: #18191b;
175
+ --popover: #202124;
176
+ --secondary: #222326;
177
+ --muted: #1b1c1e;
178
+ --accent: #26272a;
179
+ --muted-foreground: #8e9296;
180
+ --border: rgba(255, 255, 255, 0.09);
181
+ --input: rgba(255, 255, 255, 0.1);
182
+ --sidebar: #161719;
183
+ --sidebar-accent: color-mix(in oklab, var(--primary) 14%, transparent);
184
+ --sidebar-border: rgba(255, 255, 255, 0.07);
185
+ }
186
+
187
+ /* Ground: Emerald (dark) */
188
+ [data-mode="dark"][data-ground="emerald"] {
189
+ --background: #050b08;
190
+ --card: #0a1310;
191
+ --popover: #0d1814;
192
+ --secondary: #121d18;
193
+ --muted: #101a15;
194
+ --accent: #16261f;
195
+ --muted-foreground: #8ba199;
196
+ --border: rgba(255, 255, 255, 0.1);
197
+ --input: rgba(255, 255, 255, 0.1);
198
+ --sidebar: var(--brand-sidebar, #0a1310);
199
+ --sidebar-accent: color-mix(in oklab, var(--primary) 16%, transparent);
200
+ --sidebar-border: rgba(255, 255, 255, 0.08);
201
+ }
202
+
203
+ /* Ground: Paper (light) */
204
+ [data-mode="light"][data-ground="paper"] {
205
+ --background: #faf6ee;
206
+ --card: #fffdf9;
207
+ --popover: #fffdf9;
208
+ --secondary: #f3eee4;
209
+ --muted: #f3eee4;
210
+ --accent: #f0e6da;
211
+ --muted-foreground: #5c5b58;
212
+ --border: #dedad2;
213
+ --input: #dedad2;
214
+ --sidebar: #f5f0e4;
215
+ --sidebar-accent: #f0e6da;
216
+ --sidebar-border: #dedad2;
217
+ }
218
+
219
+ /* Ground: Mist (light) */
220
+ [data-mode="light"][data-ground="mist"] {
221
+ --background: #f2f7f9;
222
+ --card: #ffffff;
223
+ --popover: #ffffff;
224
+ --secondary: #e9eff2;
225
+ --muted: #e9eff2;
226
+ --accent: #dfeaf0;
227
+ --muted-foreground: #5c5b58;
228
+ --border: #d6dcdf;
229
+ --input: #d6dcdf;
230
+ --sidebar: #e9f1f5;
231
+ --sidebar-accent: #dfeaf0;
232
+ --sidebar-border: #d6dcdf;
233
+ }
234
+
235
+ /* Accent: Green (dark) */
236
+ [data-mode="dark"][data-accent="green"] {
237
+ --brand-primary: #2f9e52;
238
+ --brand-on-primary: #04241b;
239
+ }
240
+
241
+ /* Accent: Green (light) */
242
+ [data-mode="light"][data-accent="green"] {
243
+ --brand-primary: #26803f;
244
+ --brand-on-primary: #f3fbf7;
245
+ }
246
+
247
+ /* Accent: Copper (dark) */
248
+ [data-mode="dark"][data-accent="copper"] {
249
+ --brand-primary: #c97b4a;
250
+ --brand-on-primary: #140a04;
251
+ --chart-3: #d9c7a3;
252
+ --chart-5: #2f9e52;
253
+ }
254
+
255
+ /* Accent: Copper (light) */
256
+ [data-mode="light"][data-accent="copper"] {
257
+ --brand-primary: #9c5730;
258
+ --brand-on-primary: #f3fbf7;
259
+ }
260
+
261
+ /* Accent: Ice (dark) */
262
+ [data-mode="dark"][data-accent="ice"] {
263
+ --brand-primary: #6e9fc4;
264
+ --brand-on-primary: #06131c;
265
+ --chart-3: #b9d4e4;
266
+ --chart-5: #2f9e52;
267
+ }
268
+
269
+ /* Accent: Ice (light) */
270
+ [data-mode="light"][data-accent="ice"] {
271
+ --brand-primary: #3d6f95;
272
+ --brand-on-primary: #f3fbf7;
273
+ }
274
+
275
+ /* Accent: Periwinkle (dark) */
276
+ [data-mode="dark"][data-accent="periwinkle"] {
277
+ --brand-primary: #8b8fd9;
278
+ --brand-on-primary: #0b0c1a;
279
+ --chart-3: #c3c5ec;
280
+ --chart-5: #2f9e52;
281
+ }
282
+
283
+ /* Accent: Periwinkle (light) */
284
+ [data-mode="light"][data-accent="periwinkle"] {
285
+ --brand-primary: #5a5fae;
286
+ --brand-on-primary: #f3fbf7;
287
+ }
288
+
289
+ /* Accent: Amber (dark) */
290
+ [data-mode="dark"][data-accent="amber"] {
291
+ --brand-primary: #e0b34a;
292
+ --brand-on-primary: #1a1204;
293
+ --chart-4: #c97b4a;
294
+ }
295
+
296
+ /* Accent: Amber (light) */
297
+ [data-mode="light"][data-accent="amber"] {
298
+ --brand-primary: #8a6410;
299
+ --brand-on-primary: #f3fbf7;
300
+ }
301
+
302
+ /* Density: Compact, dense rows with hairline dividers */
303
+ :root,
304
+ [data-density="compact"] {
305
+ --row-pad-y: 0.5rem;
306
+ --row-pad-x: 0.75rem;
307
+ --stack-gap: 0.75rem;
308
+ --panel-pad: 1rem;
309
+ --list-columns: minmax(0, 1fr);
310
+ --list-gap: 0px;
311
+ --list-frame-border: 1px;
312
+ --list-frame-radius: var(--radius);
313
+ --list-item-border: 0px;
314
+ --list-item-radius: 0px;
315
+ --list-item-bg: transparent;
316
+ --list-item-pad: var(--row-pad-y) var(--row-pad-x);
317
+ --list-item-direction: row;
318
+ --list-item-align: center;
319
+ --list-item-trail: auto;
320
+ }
321
+
322
+ /* Density: Cards, boxed items that flow into columns */
323
+ [data-density="cards"] {
324
+ --row-pad-y: 0.9rem;
325
+ --row-pad-x: 1rem;
326
+ --stack-gap: 1rem;
327
+ --panel-pad: 1.25rem;
328
+ --list-columns: repeat(auto-fill, minmax(min(100%, 16rem), 1fr));
329
+ --list-gap: var(--stack-gap);
330
+ --list-frame-border: 0px;
331
+ --list-frame-radius: 0px;
332
+ --list-item-border: 1px;
333
+ --list-item-radius: var(--radius);
334
+ --list-item-bg: var(--card);
335
+ --list-item-pad: var(--panel-pad);
336
+ --list-item-direction: column;
337
+ --list-item-align: stretch;
338
+ --list-item-trail: 0px;
339
+ }
@@ -0,0 +1,8 @@
1
+ /** Relative luminance of a `#rrggbb` colour (WCAG 2.2). */
2
+ export declare function luminance(hex: string): number;
3
+ /** The WCAG contrast ratio between two `#rrggbb` colours, 1 to 21. */
4
+ export declare function contrastRatio(a: string, b: string): number;
5
+ /** WCAG 2.2 §1.4.11: a control or a status mark stands 3:1 off what it sits on. */
6
+ export declare const MIN_NON_TEXT_CONTRAST = 3;
7
+ /** WCAG 2.2 §1.4.3: body text stands 4.5:1 off its background. */
8
+ export declare const MIN_TEXT_CONTRAST = 4.5;
@@ -0,0 +1,18 @@
1
+ /** Relative luminance of a `#rrggbb` colour (WCAG 2.2). */
2
+ export function luminance(hex) {
3
+ const n = parseInt(hex.slice(1), 16);
4
+ const [r, g, b] = [(n >> 16) & 255, (n >> 8) & 255, n & 255].map((c) => {
5
+ const s = c / 255;
6
+ return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
7
+ });
8
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
9
+ }
10
+ /** The WCAG contrast ratio between two `#rrggbb` colours, 1 to 21. */
11
+ export function contrastRatio(a, b) {
12
+ const [hi, lo] = [luminance(a), luminance(b)].sort((x, y) => y - x);
13
+ return (hi + 0.05) / (lo + 0.05);
14
+ }
15
+ /** WCAG 2.2 §1.4.11: a control or a status mark stands 3:1 off what it sits on. */
16
+ export const MIN_NON_TEXT_CONTRAST = 3;
17
+ /** WCAG 2.2 §1.4.3: body text stands 4.5:1 off its background. */
18
+ export const MIN_TEXT_CONTRAST = 4.5;
@@ -0,0 +1,23 @@
1
+ import { type Mode } from "./tokens.js";
2
+ type Declarations = readonly (readonly [property: string, value: string])[];
3
+ /** The accent tokens one accent paints in one mode. */
4
+ export declare function accentDeclarations(mode: Mode, accent: {
5
+ dark: string;
6
+ onDark: string;
7
+ light: string;
8
+ onLight: string;
9
+ }, chart?: Partial<Record<string, string>>): Declarations;
10
+ /** The whole of theme.css. */
11
+ export declare function renderThemeCss(): string;
12
+ /** The brand tokens a product adds on top of theme.css: its sidebar, and its own accent when it has one. */
13
+ export declare function renderProductCss(product: {
14
+ sidebar: string;
15
+ brandAccent?: {
16
+ key: string;
17
+ dark: string;
18
+ onDark: string;
19
+ light: string;
20
+ onLight: string;
21
+ } | null;
22
+ }): string;
23
+ export {};
@@ -0,0 +1,116 @@
1
+ import { ACCENTS, ACCENT_KEYS, BASE_SURFACES, DEFAULT_ACCENT, DENSITIES, GROUNDS, GROUND_KEYS, MODE_TOKENS, STATIC_TOKENS, SURFACES, } from "./tokens.js";
2
+ const STATUS_NAMES = ["success", "warning", "info", "danger"];
3
+ function block(selector, declarations, comment) {
4
+ const head = comment ? `/* ${comment} */\n` : "";
5
+ const body = declarations.map(([property, value]) => ` ${property}: ${value};`).join("\n");
6
+ return `${head}${selector} {\n${body}\n}`;
7
+ }
8
+ function surfaceDeclarations(s) {
9
+ return [
10
+ ["--background", s.background],
11
+ ["--card", s.card],
12
+ ["--popover", s.popover],
13
+ ["--secondary", s.secondary],
14
+ ["--muted", s.muted],
15
+ ["--accent", s.accent],
16
+ ["--muted-foreground", s.mutedForeground],
17
+ ["--border", s.border],
18
+ ["--input", s.input],
19
+ ["--sidebar", s.sidebar === "brand" ? `var(--brand-sidebar, ${s.card})` : s.sidebar],
20
+ ["--sidebar-accent", s.sidebarAccent],
21
+ ["--sidebar-border", s.sidebarBorder],
22
+ ];
23
+ }
24
+ function modeDeclarations(mode) {
25
+ const m = MODE_TOKENS[mode];
26
+ const accent = ACCENTS[DEFAULT_ACCENT];
27
+ return [
28
+ ["color-scheme", mode],
29
+ ...surfaceDeclarations(BASE_SURFACES[mode]),
30
+ ["--foreground", m.foreground],
31
+ ["--card-foreground", m.foreground],
32
+ ["--popover-foreground", m.foreground],
33
+ ["--secondary-foreground", m.foreground],
34
+ ["--accent-foreground", m.foreground],
35
+ ["--brand-primary", mode === "dark" ? accent.dark : accent.light],
36
+ ["--brand-on-primary", mode === "dark" ? accent.onDark : accent.onLight],
37
+ ["--primary", "var(--brand-primary)"],
38
+ ["--primary-foreground", "var(--brand-on-primary)"],
39
+ ["--destructive", m.destructive],
40
+ ["--ring", "var(--brand-primary)"],
41
+ ["--chart-1", "var(--brand-primary)"],
42
+ ...Object.entries(m.chart).map(([key, value]) => [`--${key}`, value]),
43
+ ["--sidebar-foreground", m.sidebarForeground],
44
+ ["--sidebar-primary", "var(--brand-primary)"],
45
+ ["--sidebar-primary-foreground", "var(--brand-on-primary)"],
46
+ ["--sidebar-accent-foreground", m.foreground],
47
+ ["--sidebar-ring", "var(--brand-primary)"],
48
+ ["--surface-0", "var(--background)"],
49
+ ["--surface-1", "var(--card)"],
50
+ ["--surface-2", "var(--popover)"],
51
+ ...STATUS_NAMES.flatMap((name) => {
52
+ const s = m.status[name];
53
+ return [
54
+ [`--${name}`, s.base],
55
+ [`--${name}-fg`, s.fg],
56
+ [`--${name}-bg`, s.bg],
57
+ [`--${name}-border`, s.border],
58
+ ];
59
+ }),
60
+ ];
61
+ }
62
+ /** The accent tokens one accent paints in one mode. */
63
+ export function accentDeclarations(mode, accent, chart) {
64
+ return [
65
+ ["--brand-primary", mode === "dark" ? accent.dark : accent.light],
66
+ ["--brand-on-primary", mode === "dark" ? accent.onDark : accent.onLight],
67
+ ...Object.entries(chart ?? {}).map(([key, value]) => [`--${key}`, value ?? ""]),
68
+ ];
69
+ }
70
+ const HEADER = `/*
71
+ * @calliopelabs/ui theme tokens.
72
+ * Generated by \`npm run theme\` from src/theme/tokens.ts. Do not edit by hand.
73
+ *
74
+ * The appearance axes ride on attributes of <html>, set before first paint by
75
+ * AppearanceScript and kept in step by AppearanceProvider:
76
+ * data-mode dark | light (follows the ground)
77
+ * data-ground the surface ramp
78
+ * data-accent a family accent, the product's brand accent, or custom
79
+ * data-density compact | cards (any element may carry its own)
80
+ * --radius the corners setting, inline
81
+ * Semantic status tokens are defined per mode only: no ground and no accent
82
+ * may change what ok, warn, info or error look like.
83
+ */`;
84
+ /** The whole of theme.css. */
85
+ export function renderThemeCss() {
86
+ const blocks = [HEADER];
87
+ blocks.push(block(":root", Object.entries(STATIC_TOKENS).map(([key, value]) => [`--${key}`, value]), "Mode-independent tokens"));
88
+ blocks.push(block(':root,\n[data-mode="dark"]', modeDeclarations("dark"), "Dark: black ground, green accent"));
89
+ blocks.push(block('[data-mode="light"]', modeDeclarations("light"), "Light: neutral ground, green accent"));
90
+ for (const ground of GROUND_KEYS) {
91
+ const { mode, label } = GROUNDS[ground];
92
+ blocks.push(block(`[data-mode="${mode}"][data-ground="${ground}"]`, surfaceDeclarations(SURFACES[ground]), `Ground: ${label} (${mode})`));
93
+ }
94
+ for (const key of ACCENT_KEYS) {
95
+ const accent = ACCENTS[key];
96
+ const chart = "chart" in accent ? accent.chart : {};
97
+ for (const mode of ["dark", "light"]) {
98
+ blocks.push(block(`[data-mode="${mode}"][data-accent="${key}"]`, accentDeclarations(mode, accent, chart[mode]), `Accent: ${accent.label} (${mode})`));
99
+ }
100
+ }
101
+ blocks.push(block(':root,\n[data-density="compact"]', Object.entries(DENSITIES.compact.tokens).map(([key, value]) => [`--${key}`, value]), `Density: ${DENSITIES.compact.label}, ${DENSITIES.compact.hint.toLowerCase()}`));
102
+ blocks.push(block('[data-density="cards"]', Object.entries(DENSITIES.cards.tokens).map(([key, value]) => [`--${key}`, value]), `Density: ${DENSITIES.cards.label}, ${DENSITIES.cards.hint.toLowerCase()}`));
103
+ return `${blocks.join("\n\n")}\n`;
104
+ }
105
+ /** The brand tokens a product adds on top of theme.css: its sidebar, and its own accent when it has one. */
106
+ export function renderProductCss(product) {
107
+ const inline = (selector, declarations) => `${selector}{${declarations.map(([property, value]) => `${property}:${value}`).join(";")}}`;
108
+ const rules = [inline(":root", [["--brand-sidebar", product.sidebar]])];
109
+ const accent = product.brandAccent;
110
+ if (accent) {
111
+ for (const mode of ["dark", "light"]) {
112
+ rules.push(inline(`[data-mode="${mode}"][data-accent="${accent.key}"]`, accentDeclarations(mode, accent)));
113
+ }
114
+ }
115
+ return rules.join("");
116
+ }