@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.
- package/dist/{Checkbox-B5Gb3h5J.js → Checkbox-BZjX1GMG.js} +153 -146
- package/dist/commerce/ProductCard.d.ts +10 -0
- package/dist/commerce/createCart.d.ts +53 -0
- package/dist/commerce/index.d.ts +1 -0
- package/dist/commerce.js +160 -133
- package/dist/elements.css +244 -0
- package/dist/elements.iife.js +3 -3
- package/dist/elements.js +590 -583
- package/dist/full.css +244 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.js +3090 -2798
- package/dist/layout/ActionBar.d.ts +53 -0
- package/dist/layout/Section.d.ts +31 -0
- package/dist/styles.css +66 -0
- package/dist/ui/Badge.d.ts +6 -0
- package/dist/ui/BeforeAfter.d.ts +34 -0
- package/dist/ui/Carousel.d.ts +6 -0
- package/dist/ui/FloatingActionButton.d.ts +6 -0
- package/dist/ui/Image.d.ts +6 -0
- package/dist/ui/List.d.ts +5 -0
- package/dist/ui/PricingTable.d.ts +60 -0
- package/dist/ui/Skeleton.d.ts +10 -3
- package/package.json +1 -1
- package/src/commerce/ProductCard.tsx +26 -2
- package/src/commerce/createCart.ts +102 -0
- package/src/commerce/index.ts +1 -0
- package/src/index.ts +5 -1
- package/src/layout/ActionBar.tsx +121 -0
- package/src/layout/Section.tsx +69 -0
- package/src/ui/Accordion.tsx +3 -1
- package/src/ui/Badge.tsx +14 -2
- package/src/ui/BeforeAfter.tsx +162 -0
- package/src/ui/Carousel.tsx +48 -3
- package/src/ui/Collapse.tsx +13 -4
- package/src/ui/FloatingActionButton.tsx +16 -1
- package/src/ui/Image.tsx +29 -1
- package/src/ui/List.tsx +10 -2
- package/src/ui/PricingTable.tsx +196 -0
- package/src/ui/Skeleton.tsx +17 -5
- 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
|
+
}
|
package/src/ui/Skeleton.tsx
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
//
|
|
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
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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
|
|
30
|
+
return (
|
|
31
|
+
<div
|
|
32
|
+
class={cn('rounded-md bg-muted', props.shimmer ? 'skeleton-shimmer' : 'animate-pulse', props.class)}
|
|
33
|
+
/>
|
|
34
|
+
)
|
|
23
35
|
}
|
package/src/ui/SpeedDial.tsx
CHANGED
|
@@ -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
|
-
|
|
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" />}
|