@a4ui/core 0.16.0 → 0.18.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 (40) hide show
  1. package/dist/{Checkbox-B5Gb3h5J.js → Checkbox-BZjX1GMG.js} +153 -146
  2. package/dist/commerce/ProductCard.d.ts +10 -0
  3. package/dist/commerce/createCart.d.ts +53 -0
  4. package/dist/commerce/index.d.ts +1 -0
  5. package/dist/commerce.js +160 -133
  6. package/dist/elements.css +244 -0
  7. package/dist/elements.iife.js +3 -3
  8. package/dist/elements.js +590 -583
  9. package/dist/full.css +244 -0
  10. package/dist/index.d.ts +5 -1
  11. package/dist/index.js +3090 -2798
  12. package/dist/layout/ActionBar.d.ts +53 -0
  13. package/dist/layout/Section.d.ts +31 -0
  14. package/dist/styles.css +66 -0
  15. package/dist/ui/Badge.d.ts +6 -0
  16. package/dist/ui/BeforeAfter.d.ts +34 -0
  17. package/dist/ui/Carousel.d.ts +6 -0
  18. package/dist/ui/FloatingActionButton.d.ts +6 -0
  19. package/dist/ui/Image.d.ts +6 -0
  20. package/dist/ui/List.d.ts +5 -0
  21. package/dist/ui/PricingTable.d.ts +60 -0
  22. package/dist/ui/Skeleton.d.ts +10 -3
  23. package/package.json +1 -1
  24. package/src/commerce/ProductCard.tsx +26 -2
  25. package/src/commerce/createCart.ts +102 -0
  26. package/src/commerce/index.ts +1 -0
  27. package/src/index.ts +5 -1
  28. package/src/layout/ActionBar.tsx +121 -0
  29. package/src/layout/Section.tsx +69 -0
  30. package/src/ui/Accordion.tsx +3 -1
  31. package/src/ui/Badge.tsx +14 -2
  32. package/src/ui/BeforeAfter.tsx +162 -0
  33. package/src/ui/Carousel.tsx +48 -3
  34. package/src/ui/Collapse.tsx +13 -4
  35. package/src/ui/FloatingActionButton.tsx +16 -1
  36. package/src/ui/Image.tsx +29 -1
  37. package/src/ui/List.tsx +10 -2
  38. package/src/ui/PricingTable.tsx +196 -0
  39. package/src/ui/Skeleton.tsx +17 -5
  40. package/src/ui/SpeedDial.tsx +3 -1
@@ -0,0 +1,196 @@
1
+ // PricingTable — a responsive grid of pricing tiers, each a Card with a
2
+ // feature checklist and a CTA Button. When any tier carries an annual price,
3
+ // a monthly/annual toggle renders above the grid (controlled or uncontrolled).
4
+ import { Check } from 'lucide-solid'
5
+ import type { JSX } from 'solid-js'
6
+ import { For, Show, createSignal, splitProps } from 'solid-js'
7
+
8
+ import { cn } from '../lib/cn'
9
+ import { Badge } from './Badge'
10
+ import { Button } from './Button'
11
+ import { Card, CardContent, CardHeader, CardTitle } from './Card'
12
+ import { spawnRipple } from './Ripple'
13
+
14
+ // Mirrors Button's `primary`/`outline` classes for the `href` case, where the
15
+ // CTA must render as an `<a>` — Button itself only renders a `<button>`.
16
+ const CTA_LINK_BASE =
17
+ 'relative inline-flex w-full items-center justify-center overflow-hidden rounded-md px-3 py-2 text-sm font-medium transition-[color,background-color,transform] duration-150 active:scale-[0.97] focus:outline-none focus:ring-2 focus:ring-ring'
18
+ const CTA_LINK_VARIANT = {
19
+ primary: 'bg-primary text-primary-foreground hover:bg-primary/90',
20
+ outline: 'border border-border bg-transparent text-foreground hover:bg-muted',
21
+ }
22
+
23
+ /** Billing period a {@link PricingTable} can show. */
24
+ export type PricingPeriod = 'monthly' | 'annual'
25
+
26
+ /** One plan/tier rendered by {@link PricingTable}. */
27
+ export interface PricingTier {
28
+ /** Plan name, e.g. `"Pro"`. */
29
+ name: string
30
+ /** Pre-formatted price for the monthly period, e.g. `"$0"` or `"$99/mes"`. */
31
+ price: string
32
+ /** Pre-formatted price for the annual period. When ANY tier sets this, the toggle renders. */
33
+ priceAnnual?: string
34
+ /** Short one-line description under the plan name. */
35
+ description?: string
36
+ /** Bullet list of included features, each rendered with a check icon. */
37
+ features: string[]
38
+ /** Renders this tier emphasized: `glass` surface, a ring, and a "Popular" {@link Badge}. */
39
+ highlighted?: boolean
40
+ /** Call-to-action button. Rendered as an `<a>` when `href` is set, otherwise a `<button>`. */
41
+ cta?: { label: string; href?: string; onClick?: () => void }
42
+ }
43
+
44
+ interface PricingTableProps {
45
+ /** Tiers to render, in order, as a responsive grid. */
46
+ tiers: PricingTier[]
47
+ /** Controlled billing period. Omit to let the toggle manage its own state. */
48
+ period?: PricingPeriod
49
+ /** Fired when the toggle changes period, controlled or uncontrolled. */
50
+ onPeriodChange?: (period: PricingPeriod) => void
51
+ class?: string
52
+ }
53
+
54
+ /**
55
+ * Responsive pricing grid: one {@link Card} per tier, a feature checklist, and
56
+ * a CTA {@link Button}. The highlighted tier gets a frosted glass surface, a
57
+ * ring, and a "Popular" {@link Badge}. When any tier sets `priceAnnual`, a
58
+ * monthly/annual toggle renders above the grid — pass `period` +
59
+ * `onPeriodChange` to control it, or omit both to let it manage its own state.
60
+ *
61
+ * @example
62
+ * ```tsx
63
+ * <PricingTable
64
+ * tiers={[
65
+ * { name: 'Free', price: '$0', features: ['1 project', 'Community support'], cta: { label: 'Start' } },
66
+ * {
67
+ * name: 'Pro',
68
+ * price: '$19/mo',
69
+ * priceAnnual: '$190/yr',
70
+ * description: 'For growing teams',
71
+ * features: ['Unlimited projects', 'Priority support'],
72
+ * highlighted: true,
73
+ * cta: { label: 'Upgrade', href: '/upgrade' },
74
+ * },
75
+ * ]}
76
+ * />
77
+ * ```
78
+ */
79
+ export function PricingTable(props: PricingTableProps): JSX.Element {
80
+ const [local] = splitProps(props, ['tiers', 'period', 'onPeriodChange', 'class'])
81
+
82
+ const [internalPeriod, setInternalPeriod] = createSignal<PricingPeriod>('monthly')
83
+ const period = () => local.period ?? internalPeriod()
84
+ const setPeriod = (next: PricingPeriod) => {
85
+ if (local.period === undefined) setInternalPeriod(next)
86
+ local.onPeriodChange?.(next)
87
+ }
88
+
89
+ const showToggle = () => local.tiers.some((tier) => tier.priceAnnual !== undefined)
90
+ const priceFor = (tier: PricingTier) =>
91
+ period() === 'annual' ? (tier.priceAnnual ?? tier.price) : tier.price
92
+
93
+ return (
94
+ <div class={cn('flex flex-col items-center gap-8', local.class)}>
95
+ <Show when={showToggle()}>
96
+ <div class="inline-flex items-center gap-1 rounded-full border border-border bg-muted/40 p-1 text-sm">
97
+ <button
98
+ type="button"
99
+ class={cn(
100
+ 'rounded-full px-3 py-1.5 font-medium transition-colors',
101
+ period() === 'monthly'
102
+ ? 'bg-primary text-primary-foreground'
103
+ : 'text-muted-foreground hover:text-foreground',
104
+ )}
105
+ aria-pressed={period() === 'monthly'}
106
+ onClick={() => setPeriod('monthly')}
107
+ >
108
+ Monthly
109
+ </button>
110
+ <button
111
+ type="button"
112
+ class={cn(
113
+ 'rounded-full px-3 py-1.5 font-medium transition-colors',
114
+ period() === 'annual'
115
+ ? 'bg-primary text-primary-foreground'
116
+ : 'text-muted-foreground hover:text-foreground',
117
+ )}
118
+ aria-pressed={period() === 'annual'}
119
+ onClick={() => setPeriod('annual')}
120
+ >
121
+ Annual
122
+ </button>
123
+ </div>
124
+ </Show>
125
+
126
+ <div class="grid w-full gap-6 sm:grid-cols-2 lg:grid-cols-3">
127
+ <For each={local.tiers}>
128
+ {(tier) => (
129
+ <Card
130
+ glass={tier.highlighted}
131
+ class={cn('relative flex flex-col', tier.highlighted && 'ring-2 ring-primary')}
132
+ >
133
+ <Show when={tier.highlighted}>
134
+ <Badge tone="info" class="absolute -top-3 right-6">
135
+ Popular
136
+ </Badge>
137
+ </Show>
138
+ <CardHeader>
139
+ <CardTitle>{tier.name}</CardTitle>
140
+ <Show when={tier.description}>
141
+ <p class="text-sm text-muted-foreground">{tier.description}</p>
142
+ </Show>
143
+ <p class="pt-2 text-3xl font-bold text-foreground">{priceFor(tier)}</p>
144
+ </CardHeader>
145
+ <CardContent class="flex flex-1 flex-col gap-6">
146
+ <ul class="flex flex-1 flex-col gap-2">
147
+ <For each={tier.features}>
148
+ {(feature) => (
149
+ <li class="flex items-start gap-2 text-sm text-foreground">
150
+ <Check class="mt-0.5 h-4 w-4 shrink-0 text-primary" aria-hidden="true" />
151
+ {feature}
152
+ </li>
153
+ )}
154
+ </For>
155
+ </ul>
156
+ <Show when={tier.cta}>
157
+ {(cta) => (
158
+ <Show
159
+ when={cta().href}
160
+ fallback={
161
+ <Button
162
+ ripple
163
+ onClick={cta().onClick}
164
+ variant={tier.highlighted ? 'primary' : 'outline'}
165
+ class="w-full"
166
+ >
167
+ {cta().label}
168
+ </Button>
169
+ }
170
+ >
171
+ {(href) => (
172
+ <a
173
+ href={href()}
174
+ onClick={cta().onClick}
175
+ onPointerDown={(event) =>
176
+ spawnRipple(event.currentTarget, event, { opacity: 0.35 })
177
+ }
178
+ class={cn(
179
+ CTA_LINK_BASE,
180
+ CTA_LINK_VARIANT[tier.highlighted ? 'primary' : 'outline'],
181
+ )}
182
+ >
183
+ {cta().label}
184
+ </a>
185
+ )}
186
+ </Show>
187
+ )}
188
+ </Show>
189
+ </CardContent>
190
+ </Card>
191
+ )}
192
+ </For>
193
+ </div>
194
+ </div>
195
+ )
196
+ }
@@ -1,4 +1,5 @@
1
- // Pulsing placeholder for loading content — plain div, no primitive.
1
+ // Placeholder for loading content — plain div, no primitive. Pulses by default;
2
+ // opt into a sweeping shimmer band with `shimmer`.
2
3
  import type { JSX } from 'solid-js'
3
4
 
4
5
  import { cn } from '../lib/cn'
@@ -6,18 +7,29 @@ import { cn } from '../lib/cn'
6
7
  interface SkeletonProps {
7
8
  /** Size/shape the placeholder to match the content it stands in for, e.g. `"h-4 w-32 rounded-full"`. */
8
9
  class?: string
10
+ /**
11
+ * Use a light band sweeping across the placeholder instead of the default
12
+ * pulse. Pure CSS (`.skeleton-shimmer`), token-tinted, reduced-motion aware.
13
+ */
14
+ shimmer?: boolean
9
15
  }
10
16
 
11
17
  /**
12
- * Pulsing placeholder block for content that is still loading. Plain `div`,
13
- * no Kobalte primitive — size it via `class` to match the shape of the real
14
- * content (text line, avatar, card, etc.).
18
+ * Placeholder block for content that is still loading. Plain `div`, no Kobalte
19
+ * primitive — size it via `class` to match the shape of the real content (text
20
+ * line, avatar, card, etc.). Pulses by default; pass `shimmer` for a sweeping
21
+ * light band instead.
15
22
  *
16
23
  * @example
17
24
  * ```tsx
18
25
  * <Skeleton class="h-4 w-48" />
26
+ * <Skeleton shimmer class="h-4 w-48" /> // sweeping band instead of pulse
19
27
  * ```
20
28
  */
21
29
  export function Skeleton(props: SkeletonProps): JSX.Element {
22
- return <div class={cn('animate-pulse rounded-md bg-muted', props.class)} />
30
+ return (
31
+ <div
32
+ class={cn('rounded-md bg-muted', props.shimmer ? 'skeleton-shimmer' : 'animate-pulse', props.class)}
33
+ />
34
+ )
23
35
  }
@@ -5,6 +5,7 @@ import { createSignal, For, Show } from 'solid-js'
5
5
 
6
6
  import { cn } from '../lib/cn'
7
7
  import { animate, motionReduced } from '../lib/motion'
8
+ import { spawnRipple } from './Ripple'
8
9
 
9
10
  /** A single action in a {@link SpeedDial} — an icon button with an accessible label. */
10
11
  export interface SpeedDialAction {
@@ -74,7 +75,8 @@ export function SpeedDial(props: SpeedDialProps): JSX.Element {
74
75
  aria-label={open() ? 'Close actions' : 'Open actions'}
75
76
  aria-expanded={open()}
76
77
  onClick={() => setOpen((v) => !v)}
77
- class="flex h-14 w-14 items-center justify-center rounded-full bg-primary text-primary-foreground shadow-lg transition-transform duration-150 active:scale-[0.97] focus:outline-none focus:ring-2 focus:ring-ring"
78
+ onPointerDown={(event) => spawnRipple(event.currentTarget, event, { opacity: 0.35 })}
79
+ class="relative flex h-14 w-14 items-center justify-center overflow-hidden rounded-full bg-primary text-primary-foreground shadow-lg transition-transform duration-150 active:scale-[0.97] focus:outline-none focus:ring-2 focus:ring-ring"
78
80
  >
79
81
  <span class={cn('inline-flex transition-transform duration-150', open() && 'rotate-45')}>
80
82
  {props.icon ?? <Plus class="h-6 w-6" />}