@a4ui/core 0.11.1 → 0.13.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 (151) hide show
  1. package/README.md +80 -37
  2. package/dist/elements.css +79 -9
  3. package/dist/full.css +3112 -0
  4. package/dist/index.d.ts +13 -1
  5. package/dist/index.js +3392 -2537
  6. package/dist/lib/flyToCart.d.ts +25 -0
  7. package/dist/ui/Curtain.d.ts +44 -0
  8. package/dist/ui/Expandable.d.ts +34 -0
  9. package/dist/ui/FillText.d.ts +18 -0
  10. package/dist/ui/HoldToConfirm.d.ts +24 -0
  11. package/dist/ui/LoadingDots.d.ts +20 -0
  12. package/dist/ui/MultiStateBadge.d.ts +23 -0
  13. package/dist/ui/NotificationStack.d.ts +32 -0
  14. package/dist/ui/NowPlaying.d.ts +21 -0
  15. package/dist/ui/Parallax.d.ts +21 -0
  16. package/dist/ui/ScrambleText.d.ts +22 -0
  17. package/dist/ui/TextReveal.d.ts +23 -0
  18. package/package.json +8 -1
  19. package/src/charts/BarChart.tsx +55 -0
  20. package/src/charts/DonutChart.tsx +127 -0
  21. package/src/charts/Sparkline.tsx +101 -0
  22. package/src/charts/index.ts +5 -0
  23. package/src/commerce/CartLine.tsx +84 -0
  24. package/src/commerce/CartSummary.tsx +70 -0
  25. package/src/commerce/FilterGroup.tsx +70 -0
  26. package/src/commerce/PriceTag.tsx +62 -0
  27. package/src/commerce/ProductCard.tsx +121 -0
  28. package/src/commerce/ProductGrid.tsx +27 -0
  29. package/src/commerce/QuantityStepper.tsx +59 -0
  30. package/src/commerce/index.ts +10 -0
  31. package/src/elements.tsx +167 -0
  32. package/src/index.ts +186 -0
  33. package/src/layout/AppShell.tsx +102 -0
  34. package/src/layout/ChristmasBackground.tsx +177 -0
  35. package/src/layout/EffectsToggle.tsx +41 -0
  36. package/src/layout/NavGroup.tsx +45 -0
  37. package/src/layout/SnowScenery.tsx +76 -0
  38. package/src/layout/SpaceBackground.tsx +459 -0
  39. package/src/layout/ThemeToggle.tsx +40 -0
  40. package/src/layout/ThemedScenery.tsx +179 -0
  41. package/src/layout/sceneEffects.ts +49 -0
  42. package/src/lib/cn.ts +17 -0
  43. package/src/lib/effects.ts +71 -0
  44. package/src/lib/flyToCart.ts +103 -0
  45. package/src/lib/media.ts +27 -0
  46. package/src/lib/motion.ts +191 -0
  47. package/src/lib/theme.ts +113 -0
  48. package/src/lib/virtual.ts +33 -0
  49. package/src/styles/space.css +510 -0
  50. package/src/styles/tokens.css +213 -0
  51. package/src/themes/index.ts +102 -0
  52. package/src/themes/palettes.ts +368 -0
  53. package/src/ui/Accordion.tsx +65 -0
  54. package/src/ui/Affix.tsx +72 -0
  55. package/src/ui/Alert.tsx +55 -0
  56. package/src/ui/AlertDialog.tsx +58 -0
  57. package/src/ui/Anchor.tsx +100 -0
  58. package/src/ui/Avatar.tsx +34 -0
  59. package/src/ui/AvatarGroup.tsx +55 -0
  60. package/src/ui/BackToTop.tsx +51 -0
  61. package/src/ui/Badge.tsx +46 -0
  62. package/src/ui/BottomNavigation.tsx +65 -0
  63. package/src/ui/Breadcrumb.tsx +64 -0
  64. package/src/ui/Button.tsx +49 -0
  65. package/src/ui/Calendar.tsx +40 -0
  66. package/src/ui/CalendarHeatmap.tsx +180 -0
  67. package/src/ui/Card.tsx +84 -0
  68. package/src/ui/Carousel.tsx +121 -0
  69. package/src/ui/Cascader.tsx +158 -0
  70. package/src/ui/Checkbox.tsx +35 -0
  71. package/src/ui/Clock.tsx +221 -0
  72. package/src/ui/Collapse.tsx +48 -0
  73. package/src/ui/ColorPicker.tsx +89 -0
  74. package/src/ui/Combobox.tsx +67 -0
  75. package/src/ui/Command.tsx +158 -0
  76. package/src/ui/Comment.tsx +91 -0
  77. package/src/ui/ContextMenu.tsx +58 -0
  78. package/src/ui/Countdown.tsx +123 -0
  79. package/src/ui/Curtain.tsx +348 -0
  80. package/src/ui/DataGrid.tsx +177 -0
  81. package/src/ui/DateField.tsx +177 -0
  82. package/src/ui/DateRangePicker.tsx +204 -0
  83. package/src/ui/DateTimeField.tsx +68 -0
  84. package/src/ui/Descriptions.tsx +56 -0
  85. package/src/ui/Drawer.tsx +72 -0
  86. package/src/ui/Dropdown.tsx +71 -0
  87. package/src/ui/Dropzone.tsx +88 -0
  88. package/src/ui/Empty.tsx +53 -0
  89. package/src/ui/Expandable.tsx +229 -0
  90. package/src/ui/FileUpload.tsx +198 -0
  91. package/src/ui/FillText.tsx +63 -0
  92. package/src/ui/FloatingActionButton.tsx +48 -0
  93. package/src/ui/Form.tsx +108 -0
  94. package/src/ui/Highlight.tsx +53 -0
  95. package/src/ui/HoldToConfirm.tsx +141 -0
  96. package/src/ui/HoverCard.tsx +42 -0
  97. package/src/ui/Image.tsx +66 -0
  98. package/src/ui/Input.tsx +38 -0
  99. package/src/ui/Kbd.tsx +25 -0
  100. package/src/ui/List.tsx +71 -0
  101. package/src/ui/LoadingDots.tsx +75 -0
  102. package/src/ui/Marquee.tsx +48 -0
  103. package/src/ui/Mentions.tsx +170 -0
  104. package/src/ui/Meter.tsx +57 -0
  105. package/src/ui/Modal.tsx +106 -0
  106. package/src/ui/MultiSelect.tsx +236 -0
  107. package/src/ui/MultiStateBadge.tsx +93 -0
  108. package/src/ui/NotificationCenter.tsx +103 -0
  109. package/src/ui/NotificationStack.tsx +104 -0
  110. package/src/ui/NowPlaying.tsx +100 -0
  111. package/src/ui/NumberInput.tsx +48 -0
  112. package/src/ui/PageHeader.tsx +60 -0
  113. package/src/ui/Pagination.tsx +62 -0
  114. package/src/ui/Parallax.tsx +51 -0
  115. package/src/ui/Popover.tsx +42 -0
  116. package/src/ui/Portal.tsx +34 -0
  117. package/src/ui/Progress.tsx +55 -0
  118. package/src/ui/RadioGroup.tsx +65 -0
  119. package/src/ui/Rating.tsx +98 -0
  120. package/src/ui/Result.tsx +75 -0
  121. package/src/ui/RingProgress.tsx +75 -0
  122. package/src/ui/ScrambleText.tsx +95 -0
  123. package/src/ui/SegmentedControl.tsx +61 -0
  124. package/src/ui/Select.tsx +55 -0
  125. package/src/ui/Separator.tsx +39 -0
  126. package/src/ui/Skeleton.tsx +23 -0
  127. package/src/ui/Slider.tsx +57 -0
  128. package/src/ui/Sortable.tsx +174 -0
  129. package/src/ui/SpeedDial.tsx +85 -0
  130. package/src/ui/Spinner.tsx +33 -0
  131. package/src/ui/Splitter.tsx +115 -0
  132. package/src/ui/Stat.tsx +74 -0
  133. package/src/ui/Stepper.tsx +134 -0
  134. package/src/ui/Switch.tsx +44 -0
  135. package/src/ui/Table.tsx +128 -0
  136. package/src/ui/Tabs.tsx +72 -0
  137. package/src/ui/TagInput.tsx +85 -0
  138. package/src/ui/TextReveal.tsx +93 -0
  139. package/src/ui/Textarea.tsx +38 -0
  140. package/src/ui/TimeField.tsx +298 -0
  141. package/src/ui/Timeline.tsx +89 -0
  142. package/src/ui/Toast.tsx +68 -0
  143. package/src/ui/Toggle.tsx +41 -0
  144. package/src/ui/ToggleGroup.tsx +59 -0
  145. package/src/ui/Tooltip.tsx +42 -0
  146. package/src/ui/Tour.tsx +197 -0
  147. package/src/ui/Transfer.tsx +112 -0
  148. package/src/ui/Tree.tsx +100 -0
  149. package/src/ui/TreeSelect.tsx +151 -0
  150. package/src/ui/VirtualList.tsx +81 -0
  151. package/src/ui/internal/CalendarCore.tsx +276 -0
@@ -0,0 +1,102 @@
1
+ // A4ui theme runtime — pick one of the built-in themes (or your own) and apply
2
+ // its palette by injecting a single <style id="a4ui-theme"> element that
3
+ // overrides the token variables for both dark and `[data-theme='light']`.
4
+ //
5
+ // This is separate from the light/dark *mode* switch in ./lib/theme (which flips
6
+ // `data-theme` and owns `setTheme`/`applyTheme` for the scheme). A theme recolors
7
+ // the palette underneath either mode; the two compose (pick a theme, then toggle
8
+ // light/dark within it). Named `selectTheme`/`applyThemeDefinition` to avoid
9
+ // clashing with the light/dark API.
10
+ import { createSignal } from 'solid-js'
11
+
12
+ import { space, TOKEN_ORDER, themes, type Palette, type ThemeDefinition } from './palettes'
13
+
14
+ export type { Palette, ThemeDefinition } from './palettes'
15
+ export { themes, space, dino, doctor, scientist, soccer, snow, christmas, TOKEN_ORDER } from './palettes'
16
+
17
+ // Distinct from the light/dark mode key ('a4ui-theme' in ./lib/theme) — the two
18
+ // persist independently, so picking a palette and toggling light/dark don't
19
+ // clobber each other.
20
+ const STORAGE_KEY = 'a4ui-theme-name'
21
+ const STYLE_ID = 'a4ui-theme'
22
+
23
+ const byName = (name: string): ThemeDefinition | undefined => themes.find((t) => t.name === name)
24
+
25
+ /** Serialize one palette to `--token: value;` declaration lines. */
26
+ function paletteDecls(palette: Palette): string {
27
+ return TOKEN_ORDER.map((k) => ` --${k}: ${palette[k]};`).join('\n')
28
+ }
29
+
30
+ /**
31
+ * Render a theme to the CSS you'd paste into your own stylesheet: a `:root`
32
+ * block (dark) plus a `:root[data-theme='light']` block. Same output the docs
33
+ * Theme Builder exports.
34
+ */
35
+ export function themeToCss(theme: ThemeDefinition): string {
36
+ return (
37
+ `:root {\n${paletteDecls(theme.dark)}\n}\n\n` +
38
+ `:root[data-theme='light'] {\n${paletteDecls(theme.light)}\n}`
39
+ )
40
+ }
41
+
42
+ /** Render a theme's tokens to a `{ dark, light }` JSON object. */
43
+ export function themeToJson(theme: ThemeDefinition): { dark: Palette; light: Palette } {
44
+ return { dark: theme.dark, light: theme.light }
45
+ }
46
+
47
+ const [activeTheme, setActiveThemeSignal] = createSignal<ThemeDefinition>(space)
48
+
49
+ /** Reactive accessor for the currently applied theme. */
50
+ export { activeTheme }
51
+
52
+ /**
53
+ * Apply a theme's palette globally by injecting/replacing the
54
+ * `<style id="a4ui-theme">` element. No-op during SSR. Does not persist or update
55
+ * the reactive signal — use {@link selectTheme} for the full, persisted version.
56
+ */
57
+ export function applyThemeDefinition(theme: ThemeDefinition): void {
58
+ if (typeof document === 'undefined') return
59
+ let el = document.getElementById(STYLE_ID) as HTMLStyleElement | null
60
+ if (!el) {
61
+ el = document.createElement('style')
62
+ el.id = STYLE_ID
63
+ document.head.appendChild(el)
64
+ }
65
+ el.textContent = themeToCss(theme)
66
+ }
67
+
68
+ /**
69
+ * Select the active theme by definition or name: applies it, updates the
70
+ * reactive {@link activeTheme} signal, and persists the choice to localStorage.
71
+ * Unknown names are ignored.
72
+ */
73
+ export function selectTheme(theme: ThemeDefinition | string): void {
74
+ const def = typeof theme === 'string' ? byName(theme) : theme
75
+ if (!def) return
76
+ applyThemeDefinition(def)
77
+ setActiveThemeSignal(def)
78
+ if (typeof localStorage !== 'undefined') {
79
+ try {
80
+ localStorage.setItem(STORAGE_KEY, def.name)
81
+ } catch {
82
+ /* private mode / storage disabled — ignore */
83
+ }
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Restore the persisted theme (or fall back to Space) and apply it. Call once at
89
+ * startup. No-op during SSR.
90
+ */
91
+ export function initTheme(): void {
92
+ if (typeof document === 'undefined') return
93
+ let name: string | null = null
94
+ if (typeof localStorage !== 'undefined') {
95
+ try {
96
+ name = localStorage.getItem(STORAGE_KEY)
97
+ } catch {
98
+ name = null
99
+ }
100
+ }
101
+ selectTheme((name && byName(name)) || space)
102
+ }
@@ -0,0 +1,368 @@
1
+ // A4ui themes — a "theme" is pure data: the 15 color tokens for dark + light.
2
+ // Everything else (motion, glass, fonts, radius, data colors) is theme-agnostic
3
+ // and lives in tokens.css / the Tailwind preset, so a theme only ever recolors.
4
+ //
5
+ // Values are HSL channels ("H S% L%") — the same format the tokens use and what
6
+ // `hsl(var(--x))` in the preset expects. Primary/accent lightness is kept low
7
+ // enough that white foreground text clears WCAG AA on the solid surfaces.
8
+ //
9
+ // `space` is the flagship default and matches tokens.css verbatim; applying it
10
+ // is a no-op relative to the shipped defaults. New themes override on top.
11
+
12
+ /** The 15 recolorable tokens, each an "H S% L%" channel string. */
13
+ export interface Palette {
14
+ background: string
15
+ foreground: string
16
+ card: string
17
+ 'card-foreground': string
18
+ muted: string
19
+ 'muted-foreground': string
20
+ border: string
21
+ input: string
22
+ primary: string
23
+ 'primary-foreground': string
24
+ accent: string
25
+ 'accent-foreground': string
26
+ ring: string
27
+ destructive: string
28
+ 'destructive-foreground': string
29
+ }
30
+
31
+ /** A named theme: a label plus a dark and a light palette. */
32
+ export interface ThemeDefinition {
33
+ /** URL/storage-safe slug, e.g. `space`. */
34
+ name: string
35
+ /** Human label for the picker, e.g. `Space`. */
36
+ label: string
37
+ /** One-line flavour text. */
38
+ description: string
39
+ /** Emoji shown in the theme picker. */
40
+ icon: string
41
+ dark: Palette
42
+ light: Palette
43
+ /** Motif glyphs for the generic `ThemedScenery` backdrop. Omit for themes that
44
+ ship their own bespoke scenery (e.g. `space` uses `SpaceBackground`). */
45
+ motifs?: string[]
46
+ }
47
+
48
+ // The token order used when serializing a palette to CSS/JSON.
49
+ export const TOKEN_ORDER: (keyof Palette)[] = [
50
+ 'background',
51
+ 'foreground',
52
+ 'card',
53
+ 'card-foreground',
54
+ 'muted',
55
+ 'muted-foreground',
56
+ 'border',
57
+ 'input',
58
+ 'primary',
59
+ 'primary-foreground',
60
+ 'accent',
61
+ 'accent-foreground',
62
+ 'ring',
63
+ 'destructive',
64
+ 'destructive-foreground',
65
+ ]
66
+
67
+ // --- Flagship: Space (matches tokens.css exactly) ---------------------------
68
+ export const space: ThemeDefinition = {
69
+ name: 'space',
70
+ label: 'Space',
71
+ description: 'The signature spatial-glass look: deep night sky and electric blue.',
72
+ icon: '🚀',
73
+ dark: {
74
+ background: '222 47% 7%',
75
+ foreground: '213 31% 91%',
76
+ card: '222 40% 11%',
77
+ 'card-foreground': '213 31% 91%',
78
+ muted: '217 33% 17%',
79
+ 'muted-foreground': '215 25% 75%',
80
+ border: '217 30% 22%',
81
+ input: '217 30% 22%',
82
+ primary: '217 91% 52%',
83
+ 'primary-foreground': '0 0% 100%',
84
+ accent: '199 89% 55%',
85
+ 'accent-foreground': '0 0% 100%',
86
+ ring: '217 91% 52%',
87
+ destructive: '0 72% 52%',
88
+ 'destructive-foreground': '0 0% 100%',
89
+ },
90
+ light: {
91
+ background: '214 32% 95%',
92
+ foreground: '222 47% 11%',
93
+ card: '0 0% 100%',
94
+ 'card-foreground': '222 47% 11%',
95
+ muted: '214 30% 92%',
96
+ 'muted-foreground': '215 22% 38%',
97
+ border: '214 24% 84%',
98
+ input: '214 32% 91%',
99
+ primary: '217 91% 52%',
100
+ 'primary-foreground': '0 0% 100%',
101
+ accent: '199 89% 48%',
102
+ 'accent-foreground': '0 0% 100%',
103
+ ring: '217 91% 52%',
104
+ destructive: '0 72% 50%',
105
+ 'destructive-foreground': '0 0% 100%',
106
+ },
107
+ }
108
+
109
+ // --- Dino: jurassic jungle — leaf green + amber -----------------------------
110
+ export const dino: ThemeDefinition = {
111
+ name: 'dino',
112
+ label: 'Dino',
113
+ description: 'Jurassic jungle: mossy greens with a warm amber accent.',
114
+ icon: '🦖',
115
+ motifs: ['🦕', '🦖', '🌿', '🍃', '🦴', '🌋', '🥚', '🌴'],
116
+ dark: {
117
+ background: '160 30% 8%',
118
+ foreground: '140 18% 90%',
119
+ card: '160 28% 12%',
120
+ 'card-foreground': '140 18% 90%',
121
+ muted: '160 20% 18%',
122
+ 'muted-foreground': '140 14% 74%',
123
+ border: '160 20% 24%',
124
+ input: '160 20% 24%',
125
+ primary: '140 60% 32%',
126
+ 'primary-foreground': '0 0% 100%',
127
+ accent: '35 90% 52%',
128
+ 'accent-foreground': '40 45% 12%',
129
+ ring: '140 60% 32%',
130
+ destructive: '0 72% 52%',
131
+ 'destructive-foreground': '0 0% 100%',
132
+ },
133
+ light: {
134
+ background: '90 32% 94%',
135
+ foreground: '160 32% 13%',
136
+ card: '80 30% 99%',
137
+ 'card-foreground': '160 32% 13%',
138
+ muted: '90 24% 88%',
139
+ 'muted-foreground': '160 18% 34%',
140
+ border: '90 22% 79%',
141
+ input: '90 24% 88%',
142
+ primary: '140 55% 32%',
143
+ 'primary-foreground': '0 0% 100%',
144
+ accent: '35 90% 44%',
145
+ 'accent-foreground': '40 45% 12%',
146
+ ring: '140 55% 32%',
147
+ destructive: '0 72% 48%',
148
+ 'destructive-foreground': '0 0% 100%',
149
+ },
150
+ }
151
+
152
+ // --- Doctor: clinical teal + clean white ------------------------------------
153
+ export const doctor: ThemeDefinition = {
154
+ name: 'doctor',
155
+ label: 'Doctor',
156
+ description: 'Clinical and calm: teal and clean blues on crisp white.',
157
+ icon: '🩺',
158
+ motifs: ['🩺', '💊', '🩹', '🧬', '➕', '🫀', '🌡️', '💉'],
159
+ dark: {
160
+ background: '205 40% 9%',
161
+ foreground: '200 24% 92%',
162
+ card: '205 36% 13%',
163
+ 'card-foreground': '200 24% 92%',
164
+ muted: '205 25% 19%',
165
+ 'muted-foreground': '200 16% 74%',
166
+ border: '205 22% 25%',
167
+ input: '205 22% 25%',
168
+ primary: '190 85% 32%',
169
+ 'primary-foreground': '0 0% 100%',
170
+ accent: '210 90% 46%',
171
+ 'accent-foreground': '0 0% 100%',
172
+ ring: '190 85% 32%',
173
+ destructive: '0 72% 52%',
174
+ 'destructive-foreground': '0 0% 100%',
175
+ },
176
+ light: {
177
+ background: '200 40% 97%',
178
+ foreground: '205 45% 14%',
179
+ card: '0 0% 100%',
180
+ 'card-foreground': '205 45% 14%',
181
+ muted: '200 30% 92%',
182
+ 'muted-foreground': '205 20% 36%',
183
+ border: '200 25% 84%',
184
+ input: '200 30% 91%',
185
+ primary: '190 90% 30%',
186
+ 'primary-foreground': '0 0% 100%',
187
+ accent: '210 90% 46%',
188
+ 'accent-foreground': '0 0% 100%',
189
+ ring: '190 90% 30%',
190
+ destructive: '0 72% 48%',
191
+ 'destructive-foreground': '0 0% 100%',
192
+ },
193
+ }
194
+
195
+ // --- Scientist: lab violet + cyan -------------------------------------------
196
+ export const scientist: ThemeDefinition = {
197
+ name: 'scientist',
198
+ label: 'Scientist',
199
+ description: 'Lab-grade violet with a bright cyan reaction accent.',
200
+ icon: '🔬',
201
+ motifs: ['🔬', '🧪', '⚗️', '🧫', '🧠', '🔭', '🧬', '⚛️'],
202
+ dark: {
203
+ background: '250 30% 9%',
204
+ foreground: '250 18% 92%',
205
+ card: '250 28% 13%',
206
+ 'card-foreground': '250 18% 92%',
207
+ muted: '250 20% 19%',
208
+ 'muted-foreground': '250 14% 74%',
209
+ border: '250 18% 26%',
210
+ input: '250 18% 26%',
211
+ primary: '265 65% 52%',
212
+ 'primary-foreground': '0 0% 100%',
213
+ accent: '180 80% 45%',
214
+ 'accent-foreground': '190 45% 10%',
215
+ ring: '265 65% 52%',
216
+ destructive: '0 72% 52%',
217
+ 'destructive-foreground': '0 0% 100%',
218
+ },
219
+ light: {
220
+ background: '250 40% 97%',
221
+ foreground: '255 42% 14%',
222
+ card: '0 0% 100%',
223
+ 'card-foreground': '255 42% 14%',
224
+ muted: '250 30% 93%',
225
+ 'muted-foreground': '255 18% 38%',
226
+ border: '250 25% 85%',
227
+ input: '250 30% 92%',
228
+ primary: '265 62% 48%',
229
+ 'primary-foreground': '0 0% 100%',
230
+ accent: '185 80% 34%',
231
+ 'accent-foreground': '0 0% 100%',
232
+ ring: '265 62% 48%',
233
+ destructive: '0 72% 48%',
234
+ 'destructive-foreground': '0 0% 100%',
235
+ },
236
+ }
237
+
238
+ // --- Soccer: pitch grass + lime ---------------------------------------------
239
+ export const soccer: ThemeDefinition = {
240
+ name: 'soccer',
241
+ label: 'Soccer',
242
+ description: 'Match-day pitch: vivid grass green and fresh lime.',
243
+ icon: '⚽',
244
+ motifs: ['⚽', '🥅', '🏆', '🚩', '👟', '🧤', '🟨', '🟥'],
245
+ dark: {
246
+ background: '140 35% 8%',
247
+ foreground: '120 14% 92%',
248
+ card: '140 30% 12%',
249
+ 'card-foreground': '120 14% 92%',
250
+ muted: '140 22% 18%',
251
+ 'muted-foreground': '120 12% 74%',
252
+ border: '140 20% 24%',
253
+ input: '140 20% 24%',
254
+ primary: '142 72% 31%',
255
+ 'primary-foreground': '0 0% 100%',
256
+ accent: '90 70% 48%',
257
+ 'accent-foreground': '100 45% 10%',
258
+ ring: '142 72% 31%',
259
+ destructive: '0 72% 52%',
260
+ 'destructive-foreground': '0 0% 100%',
261
+ },
262
+ light: {
263
+ background: '120 40% 96%',
264
+ foreground: '140 40% 12%',
265
+ card: '0 0% 100%',
266
+ 'card-foreground': '140 40% 12%',
267
+ muted: '120 28% 90%',
268
+ 'muted-foreground': '140 18% 34%',
269
+ border: '120 22% 81%',
270
+ input: '120 28% 90%',
271
+ primary: '142 68% 31%',
272
+ 'primary-foreground': '0 0% 100%',
273
+ accent: '90 60% 40%',
274
+ 'accent-foreground': '100 45% 10%',
275
+ ring: '142 68% 31%',
276
+ destructive: '0 72% 48%',
277
+ 'destructive-foreground': '0 0% 100%',
278
+ },
279
+ }
280
+
281
+ // --- Snow: falling-snow winter — icy blue + frost white --------------------
282
+ export const snow: ThemeDefinition = {
283
+ name: 'snow',
284
+ label: 'Snow',
285
+ description: 'A snowy winter: icy blues and frost white, with snow drifting down.',
286
+ icon: '❄️',
287
+ motifs: ['❄️', '⛄', '🌨️', '🏔️', '🧣', '☃️'],
288
+ dark: {
289
+ background: '215 45% 12%',
290
+ foreground: '210 30% 92%',
291
+ card: '215 40% 16%',
292
+ 'card-foreground': '210 30% 92%',
293
+ muted: '215 30% 22%',
294
+ 'muted-foreground': '210 20% 74%',
295
+ border: '215 25% 28%',
296
+ input: '215 25% 28%',
297
+ primary: '200 85% 38%',
298
+ 'primary-foreground': '0 0% 100%',
299
+ accent: '190 75% 50%',
300
+ 'accent-foreground': '200 45% 12%',
301
+ ring: '200 85% 38%',
302
+ destructive: '0 72% 52%',
303
+ 'destructive-foreground': '0 0% 100%',
304
+ },
305
+ light: {
306
+ background: '205 45% 96%',
307
+ foreground: '215 45% 18%',
308
+ card: '0 0% 100%',
309
+ 'card-foreground': '215 45% 18%',
310
+ muted: '205 35% 92%',
311
+ 'muted-foreground': '215 22% 38%',
312
+ border: '205 30% 85%',
313
+ input: '205 35% 91%',
314
+ primary: '205 85% 38%',
315
+ 'primary-foreground': '0 0% 100%',
316
+ accent: '190 80% 40%',
317
+ 'accent-foreground': '200 45% 12%',
318
+ ring: '205 85% 38%',
319
+ destructive: '0 72% 48%',
320
+ 'destructive-foreground': '0 0% 100%',
321
+ },
322
+ }
323
+
324
+ // --- Christmas: cozy holiday — cranberry red + pine green + candlelight -----
325
+ export const christmas: ThemeDefinition = {
326
+ name: 'christmas',
327
+ label: 'Christmas',
328
+ description: 'Cozy holiday night: cranberry red, pine green, a tree, lights, and Santa.',
329
+ icon: '🎄',
330
+ motifs: ['🎄', '🎅', '🎁', '⭐', '🔔', '❄️', '🦌', '🕯️'],
331
+ dark: {
332
+ background: '160 30% 9%',
333
+ foreground: '40 30% 92%',
334
+ card: '160 28% 13%',
335
+ 'card-foreground': '40 30% 92%',
336
+ muted: '160 20% 19%',
337
+ 'muted-foreground': '40 18% 74%',
338
+ border: '160 20% 24%',
339
+ input: '160 20% 24%',
340
+ primary: '0 72% 46%',
341
+ 'primary-foreground': '0 0% 100%',
342
+ accent: '140 55% 32%',
343
+ 'accent-foreground': '0 0% 100%',
344
+ ring: '0 72% 46%',
345
+ destructive: '0 72% 52%',
346
+ 'destructive-foreground': '0 0% 100%',
347
+ },
348
+ light: {
349
+ background: '40 44% 96%',
350
+ foreground: '160 40% 14%',
351
+ card: '0 0% 100%',
352
+ 'card-foreground': '160 40% 14%',
353
+ muted: '40 30% 92%',
354
+ 'muted-foreground': '160 18% 34%',
355
+ border: '40 25% 84%',
356
+ input: '40 30% 91%',
357
+ primary: '0 70% 44%',
358
+ 'primary-foreground': '0 0% 100%',
359
+ accent: '140 55% 32%',
360
+ 'accent-foreground': '0 0% 100%',
361
+ ring: '0 70% 44%',
362
+ destructive: '0 72% 48%',
363
+ 'destructive-foreground': '0 0% 100%',
364
+ },
365
+ }
366
+
367
+ /** All built-in themes, in picker order. Space is the default. */
368
+ export const themes: ThemeDefinition[] = [space, dino, doctor, scientist, soccer, snow, christmas]
@@ -0,0 +1,65 @@
1
+ // Generic accessible accordion on Kobalte's Accordion primitive.
2
+ import { Accordion as KAccordion } from '@kobalte/core/accordion'
3
+ import type { JSX } from 'solid-js'
4
+ import { For } from 'solid-js'
5
+
6
+ /** A single collapsible section rendered by {@link Accordion}. */
7
+ export interface AccordionItem {
8
+ /** Unique identifier for the item; used to track expanded state. */
9
+ value: string
10
+ /** Text shown in the trigger row. */
11
+ title: string
12
+ /** Content revealed when the item is expanded. */
13
+ content: JSX.Element
14
+ }
15
+
16
+ interface AccordionProps {
17
+ items: AccordionItem[]
18
+ /** Allow more than one item to be expanded at the same time. Defaults to `false` (single-open). */
19
+ multiple?: boolean
20
+ class?: string
21
+ }
22
+
23
+ /**
24
+ * Accessible, collapsible list of sections built on Kobalte's `Accordion` primitive.
25
+ * Each entry in `items` renders as a header/trigger row plus a content panel.
26
+ *
27
+ * @example
28
+ * ```tsx
29
+ * <Accordion
30
+ * items={[
31
+ * { value: 'a', title: 'What is A4ui?', content: <p>A SolidJS design system.</p> },
32
+ * { value: 'b', title: 'How do I install it?', content: <p>npm install a4ui</p> },
33
+ * ]}
34
+ * />
35
+ * ```
36
+ */
37
+ export function Accordion(props: AccordionProps): JSX.Element {
38
+ return (
39
+ <KAccordion multiple={props.multiple ?? false} class={props.class}>
40
+ <For each={props.items}>
41
+ {(item) => (
42
+ <KAccordion.Item value={item.value} class="border-b border-border">
43
+ <KAccordion.Header>
44
+ <KAccordion.Trigger class="flex w-full items-center justify-between py-3 text-sm font-medium text-foreground [&[data-expanded]>svg]:rotate-90">
45
+ {item.title}
46
+ <svg
47
+ class="h-3.5 w-3.5 shrink-0 transition-transform duration-200"
48
+ viewBox="0 0 20 20"
49
+ fill="none"
50
+ stroke="currentColor"
51
+ stroke-width="2"
52
+ stroke-linecap="round"
53
+ stroke-linejoin="round"
54
+ >
55
+ <path d="M7 5l6 5-6 5" />
56
+ </svg>
57
+ </KAccordion.Trigger>
58
+ </KAccordion.Header>
59
+ <KAccordion.Content class="pb-3 text-sm text-muted-foreground">{item.content}</KAccordion.Content>
60
+ </KAccordion.Item>
61
+ )}
62
+ </For>
63
+ </KAccordion>
64
+ )
65
+ }
@@ -0,0 +1,72 @@
1
+ // Pins its children to the top of the viewport once scrolled past their original position.
2
+ import type { JSX, ParentProps } from 'solid-js'
3
+ import { createSignal, onCleanup, onMount, Show } from 'solid-js'
4
+ import { cn } from '../lib/cn'
5
+
6
+ interface AffixProps extends ParentProps {
7
+ /** Distance in pixels from the top of the viewport to pin at once affixed. Defaults to `0`. */
8
+ offsetTop?: number
9
+ class?: string
10
+ }
11
+
12
+ /**
13
+ * Sticks its children to the top of the viewport once the user scrolls past the
14
+ * element's original position. While affixed, the children are rendered in a
15
+ * `position: fixed` container and a same-size spacer preserves the document flow
16
+ * so surrounding layout doesn't jump.
17
+ *
18
+ * @example
19
+ * ```tsx
20
+ * <Affix offsetTop={16}>
21
+ * <nav class="rounded-md bg-card px-4 py-2 text-foreground">Sticky toolbar</nav>
22
+ * </Affix>
23
+ * ```
24
+ */
25
+ export function Affix(props: AffixProps): JSX.Element {
26
+ const [affixed, setAffixed] = createSignal(false)
27
+ const [size, setSize] = createSignal({ width: 0, height: 0 })
28
+ let ref: HTMLDivElement | undefined
29
+
30
+ onMount(() => {
31
+ // Anchor is the element's original document position; captured once so the
32
+ // trigger point stays stable even after the wrapper becomes empty (affixed).
33
+ let anchorTop = 0
34
+ const measure = () => {
35
+ if (!ref) return
36
+ const rect = ref.getBoundingClientRect()
37
+ if (!affixed()) {
38
+ anchorTop = rect.top + window.scrollY
39
+ setSize({ width: rect.width, height: rect.height })
40
+ }
41
+ }
42
+ const update = () => {
43
+ const offset = props.offsetTop ?? 0
44
+ setAffixed(window.scrollY + offset > anchorTop)
45
+ }
46
+ const onScroll = () => {
47
+ measure()
48
+ update()
49
+ }
50
+
51
+ measure()
52
+ update()
53
+ window.addEventListener('scroll', onScroll, { passive: true })
54
+ window.addEventListener('resize', onScroll, { passive: true })
55
+ onCleanup(() => {
56
+ window.removeEventListener('scroll', onScroll)
57
+ window.removeEventListener('resize', onScroll)
58
+ })
59
+ })
60
+
61
+ return (
62
+ <div ref={ref} class={cn(props.class)}>
63
+ <Show when={affixed()} fallback={props.children}>
64
+ {/* Spacer keeps the original footprint so the page below doesn't shift up. */}
65
+ <div style={{ width: `${size().width}px`, height: `${size().height}px` }} aria-hidden="true" />
66
+ <div class="fixed z-40" style={{ top: `${props.offsetTop ?? 0}px`, width: `${size().width}px` }}>
67
+ {props.children}
68
+ </div>
69
+ </Show>
70
+ </div>
71
+ )
72
+ }
@@ -0,0 +1,55 @@
1
+ // Inline status banner on Kobalte's Alert primitive. The tone is carried by the
2
+ // border/background tint and a colored icon; the TEXT stays foreground/muted so
3
+ // it keeps WCAG-AA contrast in both dark and light themes (tone `-600` text was
4
+ // too dim on the dark surface).
5
+ import { Alert as KAlert } from '@kobalte/core/alert'
6
+ import { CircleCheck, CircleX, Info, TriangleAlert } from 'lucide-solid'
7
+ import type { Component, JSX, ParentProps } from 'solid-js'
8
+ import { Show } from 'solid-js'
9
+ import { Dynamic } from 'solid-js/web'
10
+
11
+ import { cn } from '../lib/cn'
12
+
13
+ /** Semantic tone of an {@link Alert}; drives the border/background tint and icon. */
14
+ export type AlertTone = 'info' | 'success' | 'warning' | 'danger'
15
+
16
+ const TONE: Record<AlertTone, { wrap: string; icon: string; Icon: Component<{ class?: string }> }> = {
17
+ info: { wrap: 'border-sky-500/30 bg-sky-500/10', icon: 'text-sky-500', Icon: Info },
18
+ success: { wrap: 'border-emerald-500/30 bg-emerald-500/10', icon: 'text-emerald-500', Icon: CircleCheck },
19
+ warning: { wrap: 'border-amber-500/30 bg-amber-500/10', icon: 'text-amber-500', Icon: TriangleAlert },
20
+ danger: { wrap: 'border-rose-500/30 bg-rose-500/10', icon: 'text-rose-500', Icon: CircleX },
21
+ }
22
+
23
+ interface AlertProps extends ParentProps {
24
+ /** Visual/semantic tone. Defaults to `'info'`. */
25
+ tone?: AlertTone
26
+ /** Optional bold heading shown above the body text. */
27
+ title?: string
28
+ class?: string
29
+ }
30
+
31
+ /**
32
+ * Inline status banner (not a toast/dialog) built on Kobalte's `Alert` primitive,
33
+ * for surfacing info/success/warning/danger messages inline in the page.
34
+ *
35
+ * @example
36
+ * ```tsx
37
+ * <Alert tone="warning" title="Heads up">
38
+ * Your session will expire in 5 minutes.
39
+ * </Alert>
40
+ * ```
41
+ */
42
+ export function Alert(props: AlertProps): JSX.Element {
43
+ const tone = () => TONE[props.tone ?? 'info']
44
+ return (
45
+ <KAlert class={cn('flex gap-3 rounded-lg border p-3 text-sm text-foreground', tone().wrap, props.class)}>
46
+ <Dynamic component={tone().Icon} class={cn('mt-0.5 h-4 w-4 shrink-0', tone().icon)} />
47
+ <div class="flex-1">
48
+ <Show when={props.title}>
49
+ <p class="font-semibold">{props.title}</p>
50
+ </Show>
51
+ <div class="text-muted-foreground">{props.children}</div>
52
+ </div>
53
+ </KAlert>
54
+ )
55
+ }