@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.
- package/README.md +80 -37
- package/dist/elements.css +79 -9
- package/dist/full.css +3112 -0
- package/dist/index.d.ts +13 -1
- package/dist/index.js +3392 -2537
- package/dist/lib/flyToCart.d.ts +25 -0
- package/dist/ui/Curtain.d.ts +44 -0
- package/dist/ui/Expandable.d.ts +34 -0
- package/dist/ui/FillText.d.ts +18 -0
- package/dist/ui/HoldToConfirm.d.ts +24 -0
- package/dist/ui/LoadingDots.d.ts +20 -0
- package/dist/ui/MultiStateBadge.d.ts +23 -0
- package/dist/ui/NotificationStack.d.ts +32 -0
- package/dist/ui/NowPlaying.d.ts +21 -0
- package/dist/ui/Parallax.d.ts +21 -0
- package/dist/ui/ScrambleText.d.ts +22 -0
- package/dist/ui/TextReveal.d.ts +23 -0
- package/package.json +8 -1
- package/src/charts/BarChart.tsx +55 -0
- package/src/charts/DonutChart.tsx +127 -0
- package/src/charts/Sparkline.tsx +101 -0
- package/src/charts/index.ts +5 -0
- package/src/commerce/CartLine.tsx +84 -0
- package/src/commerce/CartSummary.tsx +70 -0
- package/src/commerce/FilterGroup.tsx +70 -0
- package/src/commerce/PriceTag.tsx +62 -0
- package/src/commerce/ProductCard.tsx +121 -0
- package/src/commerce/ProductGrid.tsx +27 -0
- package/src/commerce/QuantityStepper.tsx +59 -0
- package/src/commerce/index.ts +10 -0
- package/src/elements.tsx +167 -0
- package/src/index.ts +186 -0
- package/src/layout/AppShell.tsx +102 -0
- package/src/layout/ChristmasBackground.tsx +177 -0
- package/src/layout/EffectsToggle.tsx +41 -0
- package/src/layout/NavGroup.tsx +45 -0
- package/src/layout/SnowScenery.tsx +76 -0
- package/src/layout/SpaceBackground.tsx +459 -0
- package/src/layout/ThemeToggle.tsx +40 -0
- package/src/layout/ThemedScenery.tsx +179 -0
- package/src/layout/sceneEffects.ts +49 -0
- package/src/lib/cn.ts +17 -0
- package/src/lib/effects.ts +71 -0
- package/src/lib/flyToCart.ts +103 -0
- package/src/lib/media.ts +27 -0
- package/src/lib/motion.ts +191 -0
- package/src/lib/theme.ts +113 -0
- package/src/lib/virtual.ts +33 -0
- package/src/styles/space.css +510 -0
- package/src/styles/tokens.css +213 -0
- package/src/themes/index.ts +102 -0
- package/src/themes/palettes.ts +368 -0
- package/src/ui/Accordion.tsx +65 -0
- package/src/ui/Affix.tsx +72 -0
- package/src/ui/Alert.tsx +55 -0
- package/src/ui/AlertDialog.tsx +58 -0
- package/src/ui/Anchor.tsx +100 -0
- package/src/ui/Avatar.tsx +34 -0
- package/src/ui/AvatarGroup.tsx +55 -0
- package/src/ui/BackToTop.tsx +51 -0
- package/src/ui/Badge.tsx +46 -0
- package/src/ui/BottomNavigation.tsx +65 -0
- package/src/ui/Breadcrumb.tsx +64 -0
- package/src/ui/Button.tsx +49 -0
- package/src/ui/Calendar.tsx +40 -0
- package/src/ui/CalendarHeatmap.tsx +180 -0
- package/src/ui/Card.tsx +84 -0
- package/src/ui/Carousel.tsx +121 -0
- package/src/ui/Cascader.tsx +158 -0
- package/src/ui/Checkbox.tsx +35 -0
- package/src/ui/Clock.tsx +221 -0
- package/src/ui/Collapse.tsx +48 -0
- package/src/ui/ColorPicker.tsx +89 -0
- package/src/ui/Combobox.tsx +67 -0
- package/src/ui/Command.tsx +158 -0
- package/src/ui/Comment.tsx +91 -0
- package/src/ui/ContextMenu.tsx +58 -0
- package/src/ui/Countdown.tsx +123 -0
- package/src/ui/Curtain.tsx +348 -0
- package/src/ui/DataGrid.tsx +177 -0
- package/src/ui/DateField.tsx +177 -0
- package/src/ui/DateRangePicker.tsx +204 -0
- package/src/ui/DateTimeField.tsx +68 -0
- package/src/ui/Descriptions.tsx +56 -0
- package/src/ui/Drawer.tsx +72 -0
- package/src/ui/Dropdown.tsx +71 -0
- package/src/ui/Dropzone.tsx +88 -0
- package/src/ui/Empty.tsx +53 -0
- package/src/ui/Expandable.tsx +229 -0
- package/src/ui/FileUpload.tsx +198 -0
- package/src/ui/FillText.tsx +63 -0
- package/src/ui/FloatingActionButton.tsx +48 -0
- package/src/ui/Form.tsx +108 -0
- package/src/ui/Highlight.tsx +53 -0
- package/src/ui/HoldToConfirm.tsx +141 -0
- package/src/ui/HoverCard.tsx +42 -0
- package/src/ui/Image.tsx +66 -0
- package/src/ui/Input.tsx +38 -0
- package/src/ui/Kbd.tsx +25 -0
- package/src/ui/List.tsx +71 -0
- package/src/ui/LoadingDots.tsx +75 -0
- package/src/ui/Marquee.tsx +48 -0
- package/src/ui/Mentions.tsx +170 -0
- package/src/ui/Meter.tsx +57 -0
- package/src/ui/Modal.tsx +106 -0
- package/src/ui/MultiSelect.tsx +236 -0
- package/src/ui/MultiStateBadge.tsx +93 -0
- package/src/ui/NotificationCenter.tsx +103 -0
- package/src/ui/NotificationStack.tsx +104 -0
- package/src/ui/NowPlaying.tsx +100 -0
- package/src/ui/NumberInput.tsx +48 -0
- package/src/ui/PageHeader.tsx +60 -0
- package/src/ui/Pagination.tsx +62 -0
- package/src/ui/Parallax.tsx +51 -0
- package/src/ui/Popover.tsx +42 -0
- package/src/ui/Portal.tsx +34 -0
- package/src/ui/Progress.tsx +55 -0
- package/src/ui/RadioGroup.tsx +65 -0
- package/src/ui/Rating.tsx +98 -0
- package/src/ui/Result.tsx +75 -0
- package/src/ui/RingProgress.tsx +75 -0
- package/src/ui/ScrambleText.tsx +95 -0
- package/src/ui/SegmentedControl.tsx +61 -0
- package/src/ui/Select.tsx +55 -0
- package/src/ui/Separator.tsx +39 -0
- package/src/ui/Skeleton.tsx +23 -0
- package/src/ui/Slider.tsx +57 -0
- package/src/ui/Sortable.tsx +174 -0
- package/src/ui/SpeedDial.tsx +85 -0
- package/src/ui/Spinner.tsx +33 -0
- package/src/ui/Splitter.tsx +115 -0
- package/src/ui/Stat.tsx +74 -0
- package/src/ui/Stepper.tsx +134 -0
- package/src/ui/Switch.tsx +44 -0
- package/src/ui/Table.tsx +128 -0
- package/src/ui/Tabs.tsx +72 -0
- package/src/ui/TagInput.tsx +85 -0
- package/src/ui/TextReveal.tsx +93 -0
- package/src/ui/Textarea.tsx +38 -0
- package/src/ui/TimeField.tsx +298 -0
- package/src/ui/Timeline.tsx +89 -0
- package/src/ui/Toast.tsx +68 -0
- package/src/ui/Toggle.tsx +41 -0
- package/src/ui/ToggleGroup.tsx +59 -0
- package/src/ui/Tooltip.tsx +42 -0
- package/src/ui/Tour.tsx +197 -0
- package/src/ui/Transfer.tsx +112 -0
- package/src/ui/Tree.tsx +100 -0
- package/src/ui/TreeSelect.tsx +151 -0
- package/src/ui/VirtualList.tsx +81 -0
- 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
|
+
}
|
package/src/ui/Affix.tsx
ADDED
|
@@ -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
|
+
}
|
package/src/ui/Alert.tsx
ADDED
|
@@ -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
|
+
}
|