@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,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
+ }
@@ -0,0 +1,121 @@
1
+ // Product summary card: square image with a corner ribbon (Sale/New/…), brand
2
+ // eyebrow, title, category tag, optional rating, price, and an "Add to cart"
3
+ // action. Frosted glass surface with the cursor-following edge glow.
4
+ import type { JSX } from 'solid-js'
5
+ import { Show } from 'solid-js'
6
+
7
+ import { cn } from '../lib/cn'
8
+ import { Button } from '../ui/Button'
9
+ import { Card } from '../ui/Card'
10
+ import { Rating } from '../ui/Rating'
11
+ import { PriceTag } from './PriceTag'
12
+
13
+ /** Corner-ribbon colour intent. */
14
+ export type ProductBadgeTone = 'sale' | 'new' | 'featured'
15
+
16
+ const RIBBON_TONE: Record<ProductBadgeTone, string> = {
17
+ sale: 'bg-destructive text-destructive-foreground',
18
+ new: 'bg-primary text-primary-foreground',
19
+ featured: 'bg-accent text-accent-foreground',
20
+ }
21
+
22
+ export interface ProductCardProps {
23
+ title: string
24
+ price: number
25
+ compareAt?: number
26
+ image: string
27
+ /** Brand name — shown as a small eyebrow above the title. */
28
+ brand?: string
29
+ /** Category — shown as a small tag under the title. */
30
+ category?: string
31
+ /** Rating from 0-5; renders a readonly {@link Rating} when present. */
32
+ rating?: number
33
+ /** Corner-ribbon label over the image top-right corner, e.g. `'Sale'`, `'New'`. */
34
+ badge?: string
35
+ /** Ribbon colour. Inferred from `badge` text when omitted (sale/new → featured). */
36
+ badgeTone?: ProductBadgeTone
37
+ currency?: string
38
+ locale?: string
39
+ onAddToCart?: () => void
40
+ class?: string
41
+ }
42
+
43
+ /** Guess a ribbon tone from the badge text when one isn't given. */
44
+ function inferTone(badge: string | undefined, tone: ProductBadgeTone | undefined): ProductBadgeTone {
45
+ if (tone) return tone
46
+ const b = (badge ?? '').toLowerCase()
47
+ if (/sale|off|%|deal/.test(b)) return 'sale'
48
+ if (/new/.test(b)) return 'new'
49
+ return 'featured'
50
+ }
51
+
52
+ /**
53
+ * Card summarizing a single product: image with a corner ribbon, brand, title,
54
+ * category, optional rating, price (with compare-at/discount), and an optional
55
+ * "Add to cart" button. Frosted glass with edge glow.
56
+ *
57
+ * @example
58
+ * ```tsx
59
+ * <ProductCard
60
+ * title="Wireless Headphones"
61
+ * brand="Acme"
62
+ * category="Audio"
63
+ * price={79.99}
64
+ * compareAt={99.99}
65
+ * image="/headphones.jpg"
66
+ * rating={4}
67
+ * badge="Sale"
68
+ * badgeTone="sale"
69
+ * onAddToCart={() => addToCart(sku)}
70
+ * />
71
+ * ```
72
+ */
73
+ export function ProductCard(props: ProductCardProps): JSX.Element {
74
+ return (
75
+ <Card glass glow class={cn('flex h-full flex-col overflow-hidden p-4', props.class)}>
76
+ <div class="relative aspect-square overflow-hidden rounded-lg bg-muted">
77
+ <img src={props.image} alt={props.title} loading="lazy" class="h-full w-full object-cover" />
78
+ <Show when={props.badge}>
79
+ {/* Diagonal corner ribbon (clipped by the overflow-hidden image box). */}
80
+ <span
81
+ class={cn(
82
+ 'absolute -right-9 top-3.5 w-32 rotate-45 py-1 text-center text-[11px] font-semibold uppercase tracking-wide shadow-md',
83
+ RIBBON_TONE[inferTone(props.badge, props.badgeTone)],
84
+ )}
85
+ >
86
+ {props.badge}
87
+ </span>
88
+ </Show>
89
+ </div>
90
+ {/* flex-1 + mt-auto on the button pins "Add to cart" to the bottom, so
91
+ cards of differing content height still line up in a grid row. */}
92
+ <div class="mt-3 flex flex-1 flex-col gap-1.5">
93
+ <Show when={props.brand}>
94
+ <span class="text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
95
+ {props.brand}
96
+ </span>
97
+ </Show>
98
+ <p class="line-clamp-1 font-medium text-foreground">{props.title}</p>
99
+ <Show when={props.category}>
100
+ <span class="w-fit rounded bg-muted px-1.5 py-0.5 text-[11px] text-muted-foreground">
101
+ {props.category}
102
+ </span>
103
+ </Show>
104
+ <Show when={props.rating !== undefined}>
105
+ <Rating value={props.rating as number} readonly max={5} />
106
+ </Show>
107
+ <PriceTag
108
+ amount={props.price}
109
+ compareAt={props.compareAt}
110
+ currency={props.currency}
111
+ locale={props.locale}
112
+ />
113
+ <Show when={props.onAddToCart}>
114
+ <Button class="mt-auto w-full" onClick={() => props.onAddToCart?.()}>
115
+ Add to cart
116
+ </Button>
117
+ </Show>
118
+ </div>
119
+ </Card>
120
+ )
121
+ }
@@ -0,0 +1,27 @@
1
+ // Responsive grid wrapper for arranging ProductCards (or any children).
2
+ import type { JSX, ParentProps } from 'solid-js'
3
+
4
+ import { cn } from '../lib/cn'
5
+
6
+ export interface ProductGridProps extends ParentProps {
7
+ class?: string
8
+ }
9
+
10
+ /**
11
+ * Responsive grid wrapper: 2 columns by default, 3 at `sm`, 4 at `lg`.
12
+ * Typically used to lay out a list of {@link ProductCard}s.
13
+ *
14
+ * @example
15
+ * ```tsx
16
+ * <ProductGrid>
17
+ * <For each={products()}>{(p) => <ProductCard {...p} />}</For>
18
+ * </ProductGrid>
19
+ * ```
20
+ */
21
+ export function ProductGrid(props: ProductGridProps): JSX.Element {
22
+ return (
23
+ <div class={cn('grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4', props.class)}>
24
+ {props.children}
25
+ </div>
26
+ )
27
+ }
@@ -0,0 +1,59 @@
1
+ // Inline quantity stepper: a bordered "−"/value/"+" group, clamped to [min, max].
2
+ import { Minus, Plus } from 'lucide-solid'
3
+ import type { JSX } from 'solid-js'
4
+
5
+ import { cn } from '../lib/cn'
6
+
7
+ export interface QuantityStepperProps {
8
+ value: number
9
+ onChange: (value: number) => void
10
+ /** Minimum allowed value. Defaults to `0`. */
11
+ min?: number
12
+ /** Maximum allowed value. Defaults to `Number.MAX_SAFE_INTEGER`. */
13
+ max?: number
14
+ disabled?: boolean
15
+ class?: string
16
+ }
17
+
18
+ /**
19
+ * Bordered "−"/value/"+" control for adjusting an integer quantity, clamped to
20
+ * `[min, max]`.
21
+ *
22
+ * @example
23
+ * ```tsx
24
+ * <QuantityStepper value={qty()} onChange={setQty} min={1} max={10} />
25
+ * ```
26
+ */
27
+ export function QuantityStepper(props: QuantityStepperProps): JSX.Element {
28
+ const min = () => props.min ?? 0
29
+ const max = () => props.max ?? Number.MAX_SAFE_INTEGER
30
+
31
+ const clamp = (n: number) => Math.min(max(), Math.max(min(), n))
32
+
33
+ const decrement = () => props.onChange(clamp(props.value - 1))
34
+ const increment = () => props.onChange(clamp(props.value + 1))
35
+
36
+ return (
37
+ <div class={cn('inline-flex items-stretch rounded-md border border-input', props.class)}>
38
+ <button
39
+ type="button"
40
+ aria-label="Decrease quantity"
41
+ class="grid h-8 w-8 place-items-center hover:bg-muted disabled:opacity-40"
42
+ disabled={props.disabled || props.value <= min()}
43
+ onClick={decrement}
44
+ >
45
+ <Minus class="h-4 w-4" />
46
+ </button>
47
+ <span class="grid min-w-[2.5rem] place-items-center tabular-nums">{props.value}</span>
48
+ <button
49
+ type="button"
50
+ aria-label="Increase quantity"
51
+ class="grid h-8 w-8 place-items-center hover:bg-muted disabled:opacity-40"
52
+ disabled={props.disabled || props.value >= max()}
53
+ onClick={increment}
54
+ >
55
+ <Plus class="h-4 w-4" />
56
+ </button>
57
+ </div>
58
+ )
59
+ }
@@ -0,0 +1,10 @@
1
+ // A4ui commerce components — storefront/cart building blocks, kept in their own
2
+ // entry (`@a4ui/core/commerce`) so the base package stays lean. Import styles the
3
+ // same way as the core (`@a4ui/core/styles.css` or the Tailwind preset).
4
+ export { PriceTag, type PriceTagProps } from './PriceTag'
5
+ export { QuantityStepper, type QuantityStepperProps } from './QuantityStepper'
6
+ export { ProductCard, type ProductCardProps, type ProductBadgeTone } from './ProductCard'
7
+ export { ProductGrid, type ProductGridProps } from './ProductGrid'
8
+ export { CartLine, type CartLineProps } from './CartLine'
9
+ export { CartSummary, type CartSummaryProps, type CartSummaryItem } from './CartSummary'
10
+ export { FilterGroup, type FilterGroupProps, type FacetOption } from './FilterGroup'
@@ -0,0 +1,167 @@
1
+ // Web Components entry (`@a4ui/core/elements`) — a curated subset of A4ui's
2
+ // presentational components registered as framework-agnostic custom elements so
3
+ // they can be used from React/Next.js, Vue, Svelte, or plain HTML.
4
+ //
5
+ // Design notes:
6
+ // - Each element renders in LIGHT DOM (noShadowDOM) so the global stylesheet
7
+ // (`@a4ui/core/elements.css`) and CSS theme variables apply normally — Shadow
8
+ // DOM would isolate the Tailwind utility classes the components rely on.
9
+ // - Props map to attributes/properties (solid-element coerces by the declared
10
+ // default's type: number/boolean/string). Text content is taken from a `label`
11
+ // or `text` attribute, falling back to the element's initial text.
12
+ // - Only leaf, primitive-prop components are wrapped. Rich/interactive/portalled
13
+ // components (Modal, Combobox, DataGrid, …) remain SolidJS-only — see the docs.
14
+ //
15
+ // This bundle is self-contained (solid-js is bundled in), so non-Solid apps need
16
+ // no Solid toolchain: `import '@a4ui/core/elements'` once, then use the tags.
17
+ import { customElement, noShadowDOM } from 'solid-element'
18
+ import type { JSX } from 'solid-js'
19
+
20
+ import { Alert, type AlertTone } from './ui/Alert'
21
+ import { Avatar } from './ui/Avatar'
22
+ import { Badge, type BadgeTone } from './ui/Badge'
23
+ import { Button, type ButtonVariant } from './ui/Button'
24
+ import { Clock } from './ui/Clock'
25
+ import { Countdown } from './ui/Countdown'
26
+ import { Kbd } from './ui/Kbd'
27
+ import { Meter } from './ui/Meter'
28
+ import { Progress } from './ui/Progress'
29
+ import { Rating } from './ui/Rating'
30
+ import { RingProgress } from './ui/RingProgress'
31
+ import { Separator } from './ui/Separator'
32
+ import { Spinner } from './ui/Spinner'
33
+ import { Stat, type StatTone } from './ui/Stat'
34
+
35
+ /** The initial text an author put between the tags, before Solid renders over it. */
36
+ function initialText(element: unknown): string {
37
+ return ((element as HTMLElement).textContent ?? '').trim()
38
+ }
39
+
40
+ /** Register every A4ui custom element. Import for its side effect. */
41
+ export function defineA4uiElements(): void {
42
+ customElement(
43
+ 'a4-button',
44
+ { variant: 'primary', disabled: false, type: 'button', label: '' },
45
+ (props, { element }): JSX.Element => {
46
+ noShadowDOM()
47
+ const text = props.label || initialText(element)
48
+ return (
49
+ <Button
50
+ variant={props.variant as ButtonVariant}
51
+ disabled={props.disabled}
52
+ type={props.type as 'button' | 'submit' | 'reset'}
53
+ >
54
+ {text}
55
+ </Button>
56
+ )
57
+ },
58
+ )
59
+
60
+ customElement('a4-badge', { tone: 'neutral', label: '' }, (props, { element }): JSX.Element => {
61
+ noShadowDOM()
62
+ return <Badge tone={props.tone as BadgeTone}>{props.label || initialText(element)}</Badge>
63
+ })
64
+
65
+ customElement(
66
+ // `heading`, not `title` — the latter is a global HTML attribute and would
67
+ // clash with the element's native `title` (tooltip) property.
68
+ 'a4-alert',
69
+ { tone: 'info', heading: '', text: '' },
70
+ (props, { element }): JSX.Element => {
71
+ noShadowDOM()
72
+ return (
73
+ <Alert tone={props.tone as AlertTone} title={props.heading || undefined}>
74
+ {props.text || initialText(element)}
75
+ </Alert>
76
+ )
77
+ },
78
+ )
79
+
80
+ customElement('a4-spinner', { label: 'Loading' }, (props): JSX.Element => {
81
+ noShadowDOM()
82
+ return <Spinner label={props.label} />
83
+ })
84
+
85
+ customElement('a4-avatar', { src: '', alt: '', fallback: '' }, (props): JSX.Element => {
86
+ noShadowDOM()
87
+ return <Avatar src={props.src || undefined} alt={props.alt} fallback={props.fallback || ''} />
88
+ })
89
+
90
+ customElement('a4-progress', { value: 0, max: 100, label: '' }, (props): JSX.Element => {
91
+ noShadowDOM()
92
+ return <Progress value={props.value} max={props.max} label={props.label || undefined} />
93
+ })
94
+
95
+ customElement('a4-meter', { value: 0, max: 100, label: '' }, (props): JSX.Element => {
96
+ noShadowDOM()
97
+ return <Meter value={props.value} max={props.max} label={props.label || undefined} />
98
+ })
99
+
100
+ customElement('a4-ring-progress', { value: 0, size: 96, thickness: 8, label: '' }, (props): JSX.Element => {
101
+ noShadowDOM()
102
+ return (
103
+ <RingProgress
104
+ value={props.value}
105
+ size={props.size}
106
+ thickness={props.thickness}
107
+ label={props.label || undefined}
108
+ />
109
+ )
110
+ })
111
+
112
+ customElement('a4-stat', { label: '', value: 0, tone: 'neutral' }, (props): JSX.Element => {
113
+ noShadowDOM()
114
+ return <Stat label={props.label} value={props.value} tone={props.tone as StatTone} />
115
+ })
116
+
117
+ customElement('a4-kbd', { label: '' }, (props, { element }): JSX.Element => {
118
+ noShadowDOM()
119
+ return <Kbd>{props.label || initialText(element)}</Kbd>
120
+ })
121
+
122
+ customElement('a4-separator', { orientation: 'horizontal' }, (props): JSX.Element => {
123
+ noShadowDOM()
124
+ return <Separator orientation={props.orientation as 'horizontal' | 'vertical'} />
125
+ })
126
+
127
+ customElement('a4-rating', { value: 0, max: 5, readonly: false }, (props, { element }): JSX.Element => {
128
+ noShadowDOM()
129
+ return (
130
+ <Rating
131
+ value={props.value}
132
+ max={props.max}
133
+ readonly={props.readonly}
134
+ onChange={(v) =>
135
+ (element as unknown as HTMLElement).dispatchEvent(new CustomEvent('change', { detail: v }))
136
+ }
137
+ />
138
+ )
139
+ })
140
+
141
+ customElement('a4-countdown', { to: '' }, (props): JSX.Element => {
142
+ noShadowDOM()
143
+ // eslint-disable-next-line solid/reactivity -- read the `to` attribute once to fix the target
144
+ const target = props.to ? new Date(props.to) : new Date(Date.now() + 86_400_000)
145
+ return <Countdown to={target} />
146
+ })
147
+
148
+ customElement(
149
+ 'a4-clock',
150
+ { variant: 'digital', seconds: true, hour12: false, timezone: '', size: 160 },
151
+ (props): JSX.Element => {
152
+ noShadowDOM()
153
+ return (
154
+ <Clock
155
+ variant={props.variant as 'digital' | 'analog'}
156
+ seconds={props.seconds}
157
+ hour12={props.hour12}
158
+ timeZone={props.timezone || undefined}
159
+ size={props.size}
160
+ />
161
+ )
162
+ },
163
+ )
164
+ }
165
+
166
+ // Registering on import is the ergonomic default for `import '@a4ui/core/elements'`.
167
+ defineA4uiElements()