@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.
Files changed (126) hide show
  1. package/README.md +36 -26
  2. package/dist/full.css +3042 -0
  3. package/dist/index.d.ts +1 -1
  4. package/dist/index.js +1 -1
  5. package/package.json +8 -1
  6. package/src/charts/BarChart.tsx +55 -0
  7. package/src/charts/DonutChart.tsx +127 -0
  8. package/src/charts/Sparkline.tsx +101 -0
  9. package/src/charts/index.ts +5 -0
  10. package/src/commerce/CartLine.tsx +84 -0
  11. package/src/commerce/CartSummary.tsx +70 -0
  12. package/src/commerce/FilterGroup.tsx +70 -0
  13. package/src/commerce/PriceTag.tsx +62 -0
  14. package/src/commerce/ProductCard.tsx +121 -0
  15. package/src/commerce/ProductGrid.tsx +27 -0
  16. package/src/commerce/QuantityStepper.tsx +59 -0
  17. package/src/commerce/index.ts +10 -0
  18. package/src/elements.tsx +167 -0
  19. package/src/index.ts +166 -0
  20. package/src/layout/AppShell.tsx +102 -0
  21. package/src/layout/ChristmasBackground.tsx +177 -0
  22. package/src/layout/EffectsToggle.tsx +41 -0
  23. package/src/layout/NavGroup.tsx +45 -0
  24. package/src/layout/SnowScenery.tsx +76 -0
  25. package/src/layout/SpaceBackground.tsx +459 -0
  26. package/src/layout/ThemeToggle.tsx +40 -0
  27. package/src/layout/ThemedScenery.tsx +179 -0
  28. package/src/layout/sceneEffects.ts +49 -0
  29. package/src/lib/cn.ts +17 -0
  30. package/src/lib/effects.ts +71 -0
  31. package/src/lib/media.ts +27 -0
  32. package/src/lib/motion.ts +191 -0
  33. package/src/lib/theme.ts +113 -0
  34. package/src/lib/virtual.ts +33 -0
  35. package/src/styles/space.css +510 -0
  36. package/src/styles/tokens.css +213 -0
  37. package/src/themes/index.ts +102 -0
  38. package/src/themes/palettes.ts +368 -0
  39. package/src/ui/Accordion.tsx +65 -0
  40. package/src/ui/Affix.tsx +72 -0
  41. package/src/ui/Alert.tsx +55 -0
  42. package/src/ui/AlertDialog.tsx +58 -0
  43. package/src/ui/Anchor.tsx +100 -0
  44. package/src/ui/Avatar.tsx +34 -0
  45. package/src/ui/AvatarGroup.tsx +55 -0
  46. package/src/ui/BackToTop.tsx +51 -0
  47. package/src/ui/Badge.tsx +46 -0
  48. package/src/ui/BottomNavigation.tsx +65 -0
  49. package/src/ui/Breadcrumb.tsx +64 -0
  50. package/src/ui/Button.tsx +49 -0
  51. package/src/ui/Calendar.tsx +40 -0
  52. package/src/ui/CalendarHeatmap.tsx +180 -0
  53. package/src/ui/Card.tsx +84 -0
  54. package/src/ui/Carousel.tsx +121 -0
  55. package/src/ui/Cascader.tsx +158 -0
  56. package/src/ui/Checkbox.tsx +35 -0
  57. package/src/ui/Clock.tsx +221 -0
  58. package/src/ui/Collapse.tsx +48 -0
  59. package/src/ui/ColorPicker.tsx +89 -0
  60. package/src/ui/Combobox.tsx +67 -0
  61. package/src/ui/Command.tsx +158 -0
  62. package/src/ui/Comment.tsx +91 -0
  63. package/src/ui/ContextMenu.tsx +58 -0
  64. package/src/ui/Countdown.tsx +123 -0
  65. package/src/ui/DataGrid.tsx +177 -0
  66. package/src/ui/DateField.tsx +177 -0
  67. package/src/ui/DateRangePicker.tsx +204 -0
  68. package/src/ui/DateTimeField.tsx +68 -0
  69. package/src/ui/Descriptions.tsx +56 -0
  70. package/src/ui/Drawer.tsx +72 -0
  71. package/src/ui/Dropdown.tsx +71 -0
  72. package/src/ui/Dropzone.tsx +88 -0
  73. package/src/ui/Empty.tsx +53 -0
  74. package/src/ui/FileUpload.tsx +198 -0
  75. package/src/ui/FloatingActionButton.tsx +48 -0
  76. package/src/ui/Form.tsx +108 -0
  77. package/src/ui/Highlight.tsx +53 -0
  78. package/src/ui/HoverCard.tsx +42 -0
  79. package/src/ui/Image.tsx +66 -0
  80. package/src/ui/Input.tsx +38 -0
  81. package/src/ui/Kbd.tsx +25 -0
  82. package/src/ui/List.tsx +71 -0
  83. package/src/ui/Marquee.tsx +48 -0
  84. package/src/ui/Mentions.tsx +170 -0
  85. package/src/ui/Meter.tsx +57 -0
  86. package/src/ui/Modal.tsx +106 -0
  87. package/src/ui/MultiSelect.tsx +236 -0
  88. package/src/ui/NotificationCenter.tsx +103 -0
  89. package/src/ui/NumberInput.tsx +48 -0
  90. package/src/ui/PageHeader.tsx +60 -0
  91. package/src/ui/Pagination.tsx +62 -0
  92. package/src/ui/Popover.tsx +42 -0
  93. package/src/ui/Portal.tsx +34 -0
  94. package/src/ui/Progress.tsx +55 -0
  95. package/src/ui/RadioGroup.tsx +65 -0
  96. package/src/ui/Rating.tsx +98 -0
  97. package/src/ui/Result.tsx +75 -0
  98. package/src/ui/RingProgress.tsx +75 -0
  99. package/src/ui/SegmentedControl.tsx +61 -0
  100. package/src/ui/Select.tsx +55 -0
  101. package/src/ui/Separator.tsx +39 -0
  102. package/src/ui/Skeleton.tsx +23 -0
  103. package/src/ui/Slider.tsx +57 -0
  104. package/src/ui/Sortable.tsx +174 -0
  105. package/src/ui/SpeedDial.tsx +75 -0
  106. package/src/ui/Spinner.tsx +33 -0
  107. package/src/ui/Splitter.tsx +115 -0
  108. package/src/ui/Stat.tsx +74 -0
  109. package/src/ui/Stepper.tsx +134 -0
  110. package/src/ui/Switch.tsx +44 -0
  111. package/src/ui/Table.tsx +128 -0
  112. package/src/ui/Tabs.tsx +72 -0
  113. package/src/ui/TagInput.tsx +85 -0
  114. package/src/ui/Textarea.tsx +38 -0
  115. package/src/ui/TimeField.tsx +298 -0
  116. package/src/ui/Timeline.tsx +89 -0
  117. package/src/ui/Toast.tsx +68 -0
  118. package/src/ui/Toggle.tsx +41 -0
  119. package/src/ui/ToggleGroup.tsx +59 -0
  120. package/src/ui/Tooltip.tsx +42 -0
  121. package/src/ui/Tour.tsx +197 -0
  122. package/src/ui/Transfer.tsx +112 -0
  123. package/src/ui/Tree.tsx +100 -0
  124. package/src/ui/TreeSelect.tsx +151 -0
  125. package/src/ui/VirtualList.tsx +81 -0
  126. 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.11.1";
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
@@ -5623,7 +5623,7 @@ function Pc() {
5623
5623
  }
5624
5624
  ks(e && or(e) || nt);
5625
5625
  }
5626
- const Bc = "0.11.1";
5626
+ const Bc = "0.12.0";
5627
5627
  export {
5628
5628
  Bc as A4UI_VERSION,
5629
5629
  id as Accordion,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@a4ui/core",
3
- "version": "0.11.1",
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
+ }