@a4ui/core 0.11.1 → 0.12.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 +36 -26
- package/dist/full.css +3042 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- 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 +166 -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/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/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/FileUpload.tsx +198 -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/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/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/NotificationCenter.tsx +103 -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/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/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 +75 -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/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
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const A4UI_VERSION = "0.
|
|
1
|
+
export declare const A4UI_VERSION = "0.12.0";
|
|
2
2
|
export { cn } from './lib/cn';
|
|
3
3
|
export { useTheme, toggleTheme, setTheme, storedTheme, applyTheme, toggled, type Theme } from './lib/theme';
|
|
4
4
|
export { useEffects, isCalm, setEffects } from './lib/effects';
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@a4ui/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "A4ui — Spatial Glass design system & component library for SolidJS (Kobalte behavior + Tailwind glass tokens + motion).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -29,6 +29,9 @@
|
|
|
29
29
|
],
|
|
30
30
|
"files": [
|
|
31
31
|
"dist",
|
|
32
|
+
"src",
|
|
33
|
+
"!src/**/*.test.ts",
|
|
34
|
+
"!src/**/*.test.tsx",
|
|
32
35
|
"preset.js"
|
|
33
36
|
],
|
|
34
37
|
"main": "./dist/index.js",
|
|
@@ -37,16 +40,19 @@
|
|
|
37
40
|
"exports": {
|
|
38
41
|
".": {
|
|
39
42
|
"types": "./dist/index.d.ts",
|
|
43
|
+
"solid": "./src/index.ts",
|
|
40
44
|
"import": "./dist/index.js",
|
|
41
45
|
"default": "./dist/index.js"
|
|
42
46
|
},
|
|
43
47
|
"./commerce": {
|
|
44
48
|
"types": "./dist/commerce/index.d.ts",
|
|
49
|
+
"solid": "./src/commerce/index.ts",
|
|
45
50
|
"import": "./dist/commerce.js",
|
|
46
51
|
"default": "./dist/commerce.js"
|
|
47
52
|
},
|
|
48
53
|
"./charts": {
|
|
49
54
|
"types": "./dist/charts/index.d.ts",
|
|
55
|
+
"solid": "./src/charts/index.ts",
|
|
50
56
|
"import": "./dist/charts.js",
|
|
51
57
|
"default": "./dist/charts.js"
|
|
52
58
|
},
|
|
@@ -55,6 +61,7 @@
|
|
|
55
61
|
"default": "./preset.js"
|
|
56
62
|
},
|
|
57
63
|
"./styles.css": "./dist/styles.css",
|
|
64
|
+
"./full.css": "./dist/full.css",
|
|
58
65
|
"./elements": {
|
|
59
66
|
"import": "./dist/elements.js",
|
|
60
67
|
"default": "./dist/elements.js"
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// BarChart — a simple vertical bar chart built from plain flex `div`s (no
|
|
2
|
+
// SVG, no charting library). Bar heights are percentages of the tallest
|
|
3
|
+
// value so the whole chart reflows with its container, and height changes
|
|
4
|
+
// animate via `transition-all`. Colors come from theme tokens only.
|
|
5
|
+
import { For, type JSX } from 'solid-js'
|
|
6
|
+
|
|
7
|
+
import { cn } from '../lib/cn'
|
|
8
|
+
|
|
9
|
+
export interface BarDatum {
|
|
10
|
+
label: string
|
|
11
|
+
value: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface BarChartProps {
|
|
15
|
+
data: BarDatum[]
|
|
16
|
+
/** Height of the plot area in px. Default 160. */
|
|
17
|
+
height?: number
|
|
18
|
+
/** Fill color token. Default 'primary'. */
|
|
19
|
+
tone?: 'primary' | 'accent'
|
|
20
|
+
class?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Vertical bar chart rendered with flexbox: each bar's height is `value /
|
|
25
|
+
* max` of the plot area, filled with a theme token, with its label truncated
|
|
26
|
+
* beneath it and its raw value available via a native tooltip.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```tsx
|
|
30
|
+
* <BarChart data={[{ label: 'Mon', value: 12 }, { label: 'Tue', value: 30 }]} tone="accent" />
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export function BarChart(props: BarChartProps): JSX.Element {
|
|
34
|
+
const height = () => props.height ?? 160
|
|
35
|
+
const tone = () => props.tone ?? 'primary'
|
|
36
|
+
const max = () => Math.max(...props.data.map((d) => d.value), 0) || 1
|
|
37
|
+
const barClass = () => (tone() === 'accent' ? 'bg-accent' : 'bg-primary')
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<div class={cn('flex w-full items-end gap-2', props.class)} style={{ height: `${height()}px` }}>
|
|
41
|
+
<For each={props.data}>
|
|
42
|
+
{(datum) => (
|
|
43
|
+
<div class="flex h-full min-w-0 flex-1 flex-col items-center justify-end gap-1">
|
|
44
|
+
<div
|
|
45
|
+
title={`${datum.label}: ${datum.value}`}
|
|
46
|
+
class={cn('w-full rounded-t transition-all', barClass())}
|
|
47
|
+
style={{ height: `${Math.max((datum.value / max()) * 100, 0)}%` }}
|
|
48
|
+
/>
|
|
49
|
+
<span class="w-full truncate text-center text-xs text-muted-foreground">{datum.label}</span>
|
|
50
|
+
</div>
|
|
51
|
+
)}
|
|
52
|
+
</For>
|
|
53
|
+
</div>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// DonutChart — a native SVG donut/ring chart (no charting library). Each
|
|
2
|
+
// segment is a `<circle>` stroked with `stroke-dasharray`/`stroke-dashoffset`
|
|
3
|
+
// to draw an arc proportional to `value / total`; the whole group is rotated
|
|
4
|
+
// so the first segment starts at 12 o'clock and segments run clockwise in
|
|
5
|
+
// order. Colors come from theme tokens, either per-segment `tone` or, when
|
|
6
|
+
// omitted, cycled across the `--data-*` series tokens by index.
|
|
7
|
+
import { For, Show, type JSX } from 'solid-js'
|
|
8
|
+
|
|
9
|
+
import { cn } from '../lib/cn'
|
|
10
|
+
|
|
11
|
+
export type DonutTone = 'primary' | 'accent' | 'destructive' | 'emit' | 'received' | 'net'
|
|
12
|
+
|
|
13
|
+
export interface DonutSegment {
|
|
14
|
+
label: string
|
|
15
|
+
value: number
|
|
16
|
+
tone?: DonutTone
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface DonutChartProps {
|
|
20
|
+
data: DonutSegment[]
|
|
21
|
+
/** Overall SVG width/height in px. Default 160. */
|
|
22
|
+
size?: number
|
|
23
|
+
/** Ring stroke width in px. Default 20. */
|
|
24
|
+
thickness?: number
|
|
25
|
+
/** Show the summed total in the center. Default true. */
|
|
26
|
+
showTotal?: boolean
|
|
27
|
+
class?: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Explicit tone -> CSS custom property name.
|
|
31
|
+
const TONE_TOKENS: Record<DonutTone, string> = {
|
|
32
|
+
primary: '--primary',
|
|
33
|
+
accent: '--accent',
|
|
34
|
+
destructive: '--destructive',
|
|
35
|
+
emit: '--data-emit',
|
|
36
|
+
received: '--data-received',
|
|
37
|
+
net: '--data-net',
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Fallback rotation used when a segment has no explicit `tone`.
|
|
41
|
+
const CYCLE_TOKENS = ['--data-emit', '--data-received', '--data-net'] as const
|
|
42
|
+
|
|
43
|
+
/** Resolve the CSS custom property a segment should stroke with. */
|
|
44
|
+
function tokenFor(segment: DonutSegment, index: number): string {
|
|
45
|
+
if (segment.tone) return TONE_TOKENS[segment.tone]
|
|
46
|
+
return CYCLE_TOKENS[index % CYCLE_TOKENS.length]
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* SVG donut chart: one arc per segment, sized by `value / total` of the
|
|
51
|
+
* circle's circumference, starting at 12 o'clock and running clockwise.
|
|
52
|
+
* Segment colors come from each datum's `tone`, or cycle through the
|
|
53
|
+
* `--data-*` series tokens by index when omitted.
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```tsx
|
|
57
|
+
* <DonutChart
|
|
58
|
+
* data={[
|
|
59
|
+
* { label: 'Sent', value: 62, tone: 'emit' },
|
|
60
|
+
* { label: 'Received', value: 38, tone: 'received' },
|
|
61
|
+
* ]}
|
|
62
|
+
* />
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
export function DonutChart(props: DonutChartProps): JSX.Element {
|
|
66
|
+
const size = () => props.size ?? 160
|
|
67
|
+
const thickness = () => props.thickness ?? 20
|
|
68
|
+
const showTotal = () => props.showTotal ?? true
|
|
69
|
+
const center = () => size() / 2
|
|
70
|
+
const radius = () => Math.max(size() / 2 - thickness() / 2, 0)
|
|
71
|
+
const circumference = () => 2 * Math.PI * radius()
|
|
72
|
+
const total = () => props.data.reduce((sum, d) => sum + d.value, 0)
|
|
73
|
+
|
|
74
|
+
const arcs = () => {
|
|
75
|
+
const grandTotal = total() || 1
|
|
76
|
+
let cumulative = 0
|
|
77
|
+
return props.data.map((segment, index) => {
|
|
78
|
+
const fraction = segment.value / grandTotal
|
|
79
|
+
const arcLength = fraction * circumference()
|
|
80
|
+
const offset = cumulative
|
|
81
|
+
cumulative += arcLength
|
|
82
|
+
return { segment, index, arcLength, offset }
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<div
|
|
88
|
+
class={cn('relative inline-flex items-center justify-center', props.class)}
|
|
89
|
+
style={{ width: `${size()}px`, height: `${size()}px` }}
|
|
90
|
+
>
|
|
91
|
+
<svg width={size()} height={size()} viewBox={`0 0 ${size()} ${size()}`} role="img">
|
|
92
|
+
<g transform={`rotate(-90 ${center()} ${center()})`}>
|
|
93
|
+
<circle
|
|
94
|
+
cx={center()}
|
|
95
|
+
cy={center()}
|
|
96
|
+
r={radius()}
|
|
97
|
+
fill="none"
|
|
98
|
+
stroke="hsl(var(--muted))"
|
|
99
|
+
stroke-width={thickness()}
|
|
100
|
+
/>
|
|
101
|
+
<For each={arcs()}>
|
|
102
|
+
{(arc) => (
|
|
103
|
+
<circle
|
|
104
|
+
cx={center()}
|
|
105
|
+
cy={center()}
|
|
106
|
+
r={radius()}
|
|
107
|
+
fill="none"
|
|
108
|
+
stroke={`hsl(var(${tokenFor(arc.segment, arc.index)}))`}
|
|
109
|
+
stroke-width={thickness()}
|
|
110
|
+
stroke-linecap="butt"
|
|
111
|
+
stroke-dasharray={`${arc.arcLength} ${Math.max(circumference() - arc.arcLength, 0)}`}
|
|
112
|
+
stroke-dashoffset={-arc.offset}
|
|
113
|
+
>
|
|
114
|
+
<title>{`${arc.segment.label}: ${arc.segment.value}`}</title>
|
|
115
|
+
</circle>
|
|
116
|
+
)}
|
|
117
|
+
</For>
|
|
118
|
+
</g>
|
|
119
|
+
</svg>
|
|
120
|
+
<Show when={showTotal()}>
|
|
121
|
+
<div class="absolute inset-0 flex items-center justify-center text-lg font-semibold tabular-nums text-foreground">
|
|
122
|
+
{total()}
|
|
123
|
+
</div>
|
|
124
|
+
</Show>
|
|
125
|
+
</div>
|
|
126
|
+
)
|
|
127
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// Sparkline — a tiny inline trend line rendered as a native SVG polyline (no
|
|
2
|
+
// charting library). Values are scaled to fill the viewBox (min -> bottom,
|
|
3
|
+
// max -> top, with a couple px of padding so the line never clips), stroked
|
|
4
|
+
// with a theme token so it recolors with the active palette. An optional
|
|
5
|
+
// faint area fill traces the same points down to the baseline.
|
|
6
|
+
import { Show, type JSX } from 'solid-js'
|
|
7
|
+
|
|
8
|
+
import { cn } from '../lib/cn'
|
|
9
|
+
|
|
10
|
+
export interface SparklineProps {
|
|
11
|
+
data: number[]
|
|
12
|
+
/** SVG viewBox width in px. Default 120. */
|
|
13
|
+
width?: number
|
|
14
|
+
/** SVG viewBox height in px. Default 32. */
|
|
15
|
+
height?: number
|
|
16
|
+
/** Stroke color token. Default 'primary'. */
|
|
17
|
+
tone?: 'primary' | 'accent' | 'destructive'
|
|
18
|
+
/** Fill the area under the line faintly. Default false. */
|
|
19
|
+
area?: boolean
|
|
20
|
+
class?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const PAD = 2
|
|
24
|
+
|
|
25
|
+
/** Build the `x,y` point list for `data` scaled into `w`×`h` (with padding). */
|
|
26
|
+
function pointsFor(data: number[], w: number, h: number): { x: number; y: number }[] {
|
|
27
|
+
if (data.length === 0) return []
|
|
28
|
+
if (data.length === 1) {
|
|
29
|
+
return [{ x: w / 2, y: h / 2 }]
|
|
30
|
+
}
|
|
31
|
+
const min = Math.min(...data)
|
|
32
|
+
const max = Math.max(...data)
|
|
33
|
+
const span = max - min || 1
|
|
34
|
+
const innerH = Math.max(h - PAD * 2, 0)
|
|
35
|
+
const stepX = w / (data.length - 1)
|
|
36
|
+
return data.map((value, i) => ({
|
|
37
|
+
x: i * stepX,
|
|
38
|
+
y: PAD + innerH - ((value - min) / span) * innerH,
|
|
39
|
+
}))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Minimal inline trend line: an SVG polyline scaled to fit `width`×`height`,
|
|
44
|
+
* stroked with a theme token so it recolors with the active palette. Pass
|
|
45
|
+
* `area` for a faint fill under the line.
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```tsx
|
|
49
|
+
* <Sparkline data={[3, 5, 4, 8, 6, 9, 7]} tone="accent" area />
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
export function Sparkline(props: SparklineProps): JSX.Element {
|
|
53
|
+
const width = () => props.width ?? 120
|
|
54
|
+
const height = () => props.height ?? 32
|
|
55
|
+
const tone = () => props.tone ?? 'primary'
|
|
56
|
+
const points = () => pointsFor(props.data, width(), height())
|
|
57
|
+
|
|
58
|
+
const polylinePoints = () =>
|
|
59
|
+
points()
|
|
60
|
+
.map((p) => `${p.x},${p.y}`)
|
|
61
|
+
.join(' ')
|
|
62
|
+
|
|
63
|
+
const areaPath = () => {
|
|
64
|
+
const pts = points()
|
|
65
|
+
if (pts.length === 0) return ''
|
|
66
|
+
const line = pts.map((p) => `${p.x},${p.y}`).join(' L ')
|
|
67
|
+
const first = pts[0]
|
|
68
|
+
const last = pts[pts.length - 1]
|
|
69
|
+
return `M ${first.x},${height()} L ${line} L ${last.x},${height()} Z`
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<svg
|
|
74
|
+
class={cn('overflow-visible', props.class)}
|
|
75
|
+
width={width()}
|
|
76
|
+
height={height()}
|
|
77
|
+
viewBox={`0 0 ${width()} ${height()}`}
|
|
78
|
+
preserveAspectRatio="none"
|
|
79
|
+
role="img"
|
|
80
|
+
>
|
|
81
|
+
<Show when={points().length > 0}>
|
|
82
|
+
<Show when={props.area}>
|
|
83
|
+
<path d={areaPath()} fill={`hsl(var(--${tone()}) / 0.15)`} stroke="none" />
|
|
84
|
+
</Show>
|
|
85
|
+
<Show
|
|
86
|
+
when={points().length > 1}
|
|
87
|
+
fallback={<circle cx={points()[0]?.x} cy={points()[0]?.y} r={2} fill={`hsl(var(--${tone()}))`} />}
|
|
88
|
+
>
|
|
89
|
+
<polyline
|
|
90
|
+
points={polylinePoints()}
|
|
91
|
+
fill="none"
|
|
92
|
+
stroke={`hsl(var(--${tone()}))`}
|
|
93
|
+
stroke-width={2}
|
|
94
|
+
stroke-linecap="round"
|
|
95
|
+
stroke-linejoin="round"
|
|
96
|
+
/>
|
|
97
|
+
</Show>
|
|
98
|
+
</Show>
|
|
99
|
+
</svg>
|
|
100
|
+
)
|
|
101
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// Barrel for the native-SVG chart set: theme-tokened, no external chart
|
|
2
|
+
// library. Re-exports each component alongside its prop types.
|
|
3
|
+
export { Sparkline, type SparklineProps } from './Sparkline'
|
|
4
|
+
export { BarChart, type BarDatum, type BarChartProps } from './BarChart'
|
|
5
|
+
export { DonutChart, type DonutSegment, type DonutChartProps } from './DonutChart'
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// A single cart row: thumbnail, title + unit price, quantity stepper, line
|
|
2
|
+
// total, and an optional remove button.
|
|
3
|
+
import { Trash2 } from 'lucide-solid'
|
|
4
|
+
import type { JSX } from 'solid-js'
|
|
5
|
+
import { Show } from 'solid-js'
|
|
6
|
+
|
|
7
|
+
import { cn } from '../lib/cn'
|
|
8
|
+
import { PriceTag } from './PriceTag'
|
|
9
|
+
import { QuantityStepper } from './QuantityStepper'
|
|
10
|
+
|
|
11
|
+
export interface CartLineProps {
|
|
12
|
+
title: string
|
|
13
|
+
/** Unit price. */
|
|
14
|
+
price: number
|
|
15
|
+
quantity: number
|
|
16
|
+
image?: string
|
|
17
|
+
/** ISO 4217 currency code. Defaults to `'USD'`. */
|
|
18
|
+
currency?: string
|
|
19
|
+
/** BCP 47 locale used for formatting. Defaults to `'en-US'`. */
|
|
20
|
+
locale?: string
|
|
21
|
+
onQuantityChange?: (quantity: number) => void
|
|
22
|
+
onRemove?: () => void
|
|
23
|
+
class?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* One line item in a shopping cart: thumbnail, title with unit price, a
|
|
28
|
+
* {@link QuantityStepper}, the line total, and an optional remove action.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```tsx
|
|
32
|
+
* <CartLine
|
|
33
|
+
* title="Wireless Mouse"
|
|
34
|
+
* price={29.99}
|
|
35
|
+
* quantity={2}
|
|
36
|
+
* image="/mouse.jpg"
|
|
37
|
+
* onQuantityChange={setQty}
|
|
38
|
+
* onRemove={() => removeLine(id)}
|
|
39
|
+
* />
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export function CartLine(props: CartLineProps): JSX.Element {
|
|
43
|
+
const lineTotal = () => props.price * props.quantity
|
|
44
|
+
|
|
45
|
+
const format = (value: number) =>
|
|
46
|
+
new Intl.NumberFormat(props.locale ?? 'en-US', {
|
|
47
|
+
style: 'currency',
|
|
48
|
+
currency: props.currency ?? 'USD',
|
|
49
|
+
}).format(value)
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<div class={cn('flex flex-wrap items-center gap-4', props.class)}>
|
|
53
|
+
<Show when={props.image}>
|
|
54
|
+
<img
|
|
55
|
+
src={props.image}
|
|
56
|
+
alt={props.title}
|
|
57
|
+
class="h-16 w-16 shrink-0 rounded-md bg-muted object-cover"
|
|
58
|
+
/>
|
|
59
|
+
</Show>
|
|
60
|
+
<div class="min-w-[8rem] flex-1">
|
|
61
|
+
<p class="line-clamp-1 font-medium text-foreground">{props.title}</p>
|
|
62
|
+
<PriceTag amount={props.price} currency={props.currency} locale={props.locale} size="sm" />
|
|
63
|
+
</div>
|
|
64
|
+
<QuantityStepper
|
|
65
|
+
value={props.quantity}
|
|
66
|
+
onChange={(quantity) => props.onQuantityChange?.(quantity)}
|
|
67
|
+
min={1}
|
|
68
|
+
/>
|
|
69
|
+
<span class="min-w-[4rem] text-right font-medium tabular-nums text-foreground">
|
|
70
|
+
{format(lineTotal())}
|
|
71
|
+
</span>
|
|
72
|
+
<Show when={props.onRemove}>
|
|
73
|
+
<button
|
|
74
|
+
type="button"
|
|
75
|
+
aria-label="Remove item"
|
|
76
|
+
class="text-muted-foreground hover:text-foreground"
|
|
77
|
+
onClick={() => props.onRemove?.()}
|
|
78
|
+
>
|
|
79
|
+
<Trash2 class="h-4 w-4" />
|
|
80
|
+
</button>
|
|
81
|
+
</Show>
|
|
82
|
+
</div>
|
|
83
|
+
)
|
|
84
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Order summary panel: itemized lines (subtotal, shipping, tax, ...), a
|
|
2
|
+
// divider, a bold total row, and an optional checkout button.
|
|
3
|
+
import type { JSX } from 'solid-js'
|
|
4
|
+
import { For, Show } from 'solid-js'
|
|
5
|
+
|
|
6
|
+
import { cn } from '../lib/cn'
|
|
7
|
+
import { Button } from '../ui/Button'
|
|
8
|
+
|
|
9
|
+
export interface CartSummaryItem {
|
|
10
|
+
label: string
|
|
11
|
+
amount: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface CartSummaryProps {
|
|
15
|
+
/** Itemized rows, e.g. Subtotal, Shipping, Tax. */
|
|
16
|
+
lines: CartSummaryItem[]
|
|
17
|
+
total: number
|
|
18
|
+
/** ISO 4217 currency code. Defaults to `'USD'`. */
|
|
19
|
+
currency?: string
|
|
20
|
+
/** BCP 47 locale used for formatting. Defaults to `'en-US'`. */
|
|
21
|
+
locale?: string
|
|
22
|
+
/** Label for the checkout button. Defaults to `'Checkout'`. */
|
|
23
|
+
checkoutLabel?: string
|
|
24
|
+
onCheckout?: () => void
|
|
25
|
+
class?: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Order summary panel listing itemized amounts (subtotal, shipping, tax,
|
|
30
|
+
* ...), a bold total, and an optional full-width checkout button.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```tsx
|
|
34
|
+
* <CartSummary
|
|
35
|
+
* lines={[{ label: 'Subtotal', amount: 59.98 }, { label: 'Shipping', amount: 4.99 }]}
|
|
36
|
+
* total={64.97}
|
|
37
|
+
* onCheckout={() => goToCheckout()}
|
|
38
|
+
* />
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export function CartSummary(props: CartSummaryProps): JSX.Element {
|
|
42
|
+
const format = (value: number) =>
|
|
43
|
+
new Intl.NumberFormat(props.locale ?? 'en-US', {
|
|
44
|
+
style: 'currency',
|
|
45
|
+
currency: props.currency ?? 'USD',
|
|
46
|
+
}).format(value)
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<div class={cn('card glow-edge rounded-xl p-5 text-card-foreground', props.class)}>
|
|
50
|
+
<For each={props.lines}>
|
|
51
|
+
{(line) => (
|
|
52
|
+
<div class="flex items-center justify-between py-1 text-sm">
|
|
53
|
+
<span class="text-muted-foreground">{line.label}</span>
|
|
54
|
+
<span class="tabular-nums text-foreground">{format(line.amount)}</span>
|
|
55
|
+
</div>
|
|
56
|
+
)}
|
|
57
|
+
</For>
|
|
58
|
+
<div class="my-3 border-t border-border" />
|
|
59
|
+
<div class="flex items-center justify-between text-base font-bold text-foreground">
|
|
60
|
+
<span>Total</span>
|
|
61
|
+
<span class="tabular-nums">{format(props.total)}</span>
|
|
62
|
+
</div>
|
|
63
|
+
<Show when={props.onCheckout}>
|
|
64
|
+
<Button class="mt-4 w-full" onClick={() => props.onCheckout?.()}>
|
|
65
|
+
{props.checkoutLabel ?? 'Checkout'}
|
|
66
|
+
</Button>
|
|
67
|
+
</Show>
|
|
68
|
+
</div>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Titled group of checkbox facet options (e.g. size, color, brand) with
|
|
2
|
+
// optional result counts, for faceted search/filter sidebars.
|
|
3
|
+
import type { JSX } from 'solid-js'
|
|
4
|
+
import { For, Show } from 'solid-js'
|
|
5
|
+
|
|
6
|
+
import { cn } from '../lib/cn'
|
|
7
|
+
import { Checkbox } from '../ui/Checkbox'
|
|
8
|
+
|
|
9
|
+
export interface FacetOption {
|
|
10
|
+
value: string
|
|
11
|
+
label: string
|
|
12
|
+
count?: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface FilterGroupProps {
|
|
16
|
+
title: string
|
|
17
|
+
options: FacetOption[]
|
|
18
|
+
selected: string[]
|
|
19
|
+
onChange: (selected: string[]) => void
|
|
20
|
+
class?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Titled group of checkbox facet options (e.g. size, color, brand), each with
|
|
25
|
+
* an optional result count, for faceted search/filter sidebars.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```tsx
|
|
29
|
+
* <FilterGroup
|
|
30
|
+
* title="Size"
|
|
31
|
+
* options={[{ value: 'sm', label: 'Small', count: 12 }, { value: 'md', label: 'Medium', count: 8 }]}
|
|
32
|
+
* selected={selected()}
|
|
33
|
+
* onChange={setSelected}
|
|
34
|
+
* />
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export function FilterGroup(props: FilterGroupProps): JSX.Element {
|
|
38
|
+
const toggle = (value: string, checked: boolean) => {
|
|
39
|
+
const next = props.selected.slice()
|
|
40
|
+
const index = next.indexOf(value)
|
|
41
|
+
if (checked) {
|
|
42
|
+
if (index === -1) next.push(value)
|
|
43
|
+
} else if (index !== -1) {
|
|
44
|
+
next.splice(index, 1)
|
|
45
|
+
}
|
|
46
|
+
props.onChange(next)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<div class={cn('space-y-2', props.class)}>
|
|
51
|
+
<p class="text-sm font-semibold text-foreground">{props.title}</p>
|
|
52
|
+
<div class="space-y-1.5">
|
|
53
|
+
<For each={props.options}>
|
|
54
|
+
{(option) => (
|
|
55
|
+
<div class="flex items-center justify-between gap-2">
|
|
56
|
+
<Checkbox
|
|
57
|
+
checked={props.selected.includes(option.value)}
|
|
58
|
+
onChange={(checked) => toggle(option.value, checked)}
|
|
59
|
+
label={option.label}
|
|
60
|
+
/>
|
|
61
|
+
<Show when={option.count != null}>
|
|
62
|
+
<span class="text-xs text-muted-foreground">{option.count}</span>
|
|
63
|
+
</Show>
|
|
64
|
+
</div>
|
|
65
|
+
)}
|
|
66
|
+
</For>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Price display: current amount, optional struck-through compare-at price, and
|
|
2
|
+
// a danger Badge showing the discount percent when the item is on sale.
|
|
3
|
+
import type { JSX } from 'solid-js'
|
|
4
|
+
import { Show } from 'solid-js'
|
|
5
|
+
|
|
6
|
+
import { cn } from '../lib/cn'
|
|
7
|
+
import { Badge } from '../ui/Badge'
|
|
8
|
+
|
|
9
|
+
/** Text size of a {@link PriceTag}'s main amount. Defaults to `'md'`. */
|
|
10
|
+
export type PriceTagSize = 'sm' | 'md' | 'lg'
|
|
11
|
+
|
|
12
|
+
const SIZE_CLASSES: Record<PriceTagSize, string> = {
|
|
13
|
+
sm: 'text-sm',
|
|
14
|
+
md: 'text-lg',
|
|
15
|
+
lg: 'text-2xl',
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface PriceTagProps {
|
|
19
|
+
amount: number
|
|
20
|
+
/** Original price; shown struck-through when greater than `amount`. */
|
|
21
|
+
compareAt?: number
|
|
22
|
+
/** ISO 4217 currency code. Defaults to `'USD'`. */
|
|
23
|
+
currency?: string
|
|
24
|
+
/** BCP 47 locale used for formatting. Defaults to `'en-US'`. */
|
|
25
|
+
locale?: string
|
|
26
|
+
/** Text size of the main price. Defaults to `'md'`. */
|
|
27
|
+
size?: PriceTagSize
|
|
28
|
+
class?: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Formatted price display with an optional struck-through compare-at price
|
|
33
|
+
* and a discount-percent badge when the item is on sale.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```tsx
|
|
37
|
+
* <PriceTag amount={29.99} compareAt={39.99} currency="USD" />
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export function PriceTag(props: PriceTagProps): JSX.Element {
|
|
41
|
+
const format = (value: number) =>
|
|
42
|
+
new Intl.NumberFormat(props.locale ?? 'en-US', {
|
|
43
|
+
style: 'currency',
|
|
44
|
+
currency: props.currency ?? 'USD',
|
|
45
|
+
}).format(value)
|
|
46
|
+
|
|
47
|
+
const onSale = () => (props.compareAt ?? 0) > props.amount
|
|
48
|
+
|
|
49
|
+
const discountPercent = () => Math.round((1 - props.amount / (props.compareAt as number)) * 100)
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<div class={cn('inline-flex items-center gap-2', props.class)}>
|
|
53
|
+
<span class={cn('font-bold text-foreground', SIZE_CLASSES[props.size ?? 'md'])}>
|
|
54
|
+
{format(props.amount)}
|
|
55
|
+
</span>
|
|
56
|
+
<Show when={onSale()}>
|
|
57
|
+
<span class="text-muted-foreground line-through">{format(props.compareAt as number)}</span>
|
|
58
|
+
<Badge tone="danger">-{discountPercent()}%</Badge>
|
|
59
|
+
</Show>
|
|
60
|
+
</div>
|
|
61
|
+
)
|
|
62
|
+
}
|