@a4ui/core 0.22.0 → 0.24.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.
@@ -0,0 +1,36 @@
1
+ import { JSX } from 'solid-js';
2
+ /** Background tone of an {@link AnnouncementBar}. */
3
+ export type AnnouncementTone = 'primary' | 'accent' | 'neutral';
4
+ export interface AnnouncementBarProps {
5
+ /** Message content. */
6
+ children: JSX.Element;
7
+ /** Background tone. Defaults to `'primary'`. */
8
+ tone?: AnnouncementTone;
9
+ /** Show a dismiss (X) button. Defaults to `false`. */
10
+ dismissible?: boolean;
11
+ /** Optional coupon code rendered as a copyable pill. */
12
+ couponCode?: string;
13
+ /** Make the whole message a link. */
14
+ href?: string;
15
+ /** Called after the bar is dismissed. */
16
+ onDismiss?: () => void;
17
+ class?: string;
18
+ }
19
+ /**
20
+ * A full-width promo/notice bar, meant to sit above or below the page header.
21
+ * Supports an optional dismiss button, an optional link wrapping the message,
22
+ * and an optional copyable coupon-code pill.
23
+ *
24
+ * @example
25
+ * ```tsx
26
+ * <AnnouncementBar
27
+ * tone="accent"
28
+ * dismissible
29
+ * couponCode="SAVE20"
30
+ * href="/sale"
31
+ * >
32
+ * Summer sale is live — 20% off everything
33
+ * </AnnouncementBar>
34
+ * ```
35
+ */
36
+ export declare function AnnouncementBar(props: AnnouncementBarProps): JSX.Element;
@@ -0,0 +1,31 @@
1
+ import { JSX } from 'solid-js';
2
+ export interface LogoItem {
3
+ src: string;
4
+ alt: string;
5
+ href?: string;
6
+ }
7
+ export interface LogoWallProps {
8
+ logos: LogoItem[];
9
+ /** Grayscale until hover. Defaults to true. */
10
+ grayscale?: boolean;
11
+ /** Columns on desktop. Defaults to a responsive auto-fit. */
12
+ columns?: number;
13
+ class?: string;
14
+ }
15
+ /**
16
+ * "As seen in" grid of press/partner logos, grayscale by default with a
17
+ * color-on-hover reveal. Each logo can optionally link out; sits neutral
18
+ * on any background.
19
+ *
20
+ * @example
21
+ * ```tsx
22
+ * <LogoWall
23
+ * logos={[
24
+ * { src: 'https://picsum.photos/seed/techdaily/160/48', alt: 'TechDaily' },
25
+ * { src: 'https://picsum.photos/seed/stylemag/160/48', alt: 'StyleMag', href: 'https://stylemag.example' },
26
+ * { src: 'https://picsum.photos/seed/marketwire/160/48', alt: 'MarketWire' },
27
+ * ]}
28
+ * />
29
+ * ```
30
+ */
31
+ export declare function LogoWall(props: LogoWallProps): JSX.Element;
@@ -0,0 +1,44 @@
1
+ import { JSX } from 'solid-js';
2
+ /** One aggregate rating rendered as a tile by {@link RatingsSummary}. */
3
+ export interface RatingSource {
4
+ /** Platform / source name, e.g. `"Google"`. */
5
+ name: string;
6
+ /** Headline score, e.g. `4.9` or `"A+"`. */
7
+ score: string | number;
8
+ /** Denominator, e.g. `5`. Shown as `score/outOf` when provided and `score` is numeric. */
9
+ outOf?: string | number;
10
+ /** Number of reviews/feedback backing the score. */
11
+ count?: number;
12
+ /** Optional logo/icon node; falls back to `name` when omitted. */
13
+ logo?: JSX.Element;
14
+ /** When set, the tile renders as a link to the source. */
15
+ href?: string;
16
+ }
17
+ export interface RatingsSummaryProps {
18
+ /** Rating sources to render, in order, as a responsive grid. */
19
+ sources: RatingSource[];
20
+ /** Columns on desktop. Defaults to a responsive 4-column grid. */
21
+ columns?: number;
22
+ class?: string;
23
+ }
24
+ /**
25
+ * Compact social-proof grid of aggregate ratings across multiple sources —
26
+ * a trust band for landing pages and checkout flows. Each source renders as
27
+ * a small glass-neutral tile with its logo/name, headline score (optionally
28
+ * `score/outOf`), a review count, and — when the score is numeric out of 5 —
29
+ * a row of filled stars. Tiles with an `href` render as links that open in
30
+ * a new tab.
31
+ *
32
+ * @example
33
+ * ```tsx
34
+ * <RatingsSummary
35
+ * sources={[
36
+ * { name: 'Google', score: 4.8, outOf: 5, count: 2140, href: 'https://example.com/reviews' },
37
+ * { name: 'Trustpilot', score: 4.6, outOf: 5, count: 980 },
38
+ * { name: 'Verified Buyers', score: 4.9, outOf: 5, count: 512 },
39
+ * { name: 'App Store', score: 'A+', count: 310 },
40
+ * ]}
41
+ * />
42
+ * ```
43
+ */
44
+ export declare function RatingsSummary(props: RatingsSummaryProps): JSX.Element;
@@ -0,0 +1,47 @@
1
+ import { JSX } from 'solid-js';
2
+ export interface SpecRow {
3
+ label: string;
4
+ value: JSX.Element | string;
5
+ }
6
+ export interface SpecGroup {
7
+ title?: string;
8
+ rows: SpecRow[];
9
+ }
10
+ export interface SpecSheetProps {
11
+ groups: SpecGroup[];
12
+ /** 1 or 2 columns of rows per group on wider screens. Defaults to 1. */
13
+ columns?: 1 | 2;
14
+ class?: string;
15
+ }
16
+ /**
17
+ * A grouped key/value specification table, for product specs, tech sheets,
18
+ * or any other structured attribute list. Rows are label/value pairs with a
19
+ * subtle divider; groups can carry an optional uppercase title and rows can
20
+ * flow in one or two columns on wider screens. Designed to sit inside a
21
+ * `Card` with a clean, glassy-neutral look.
22
+ *
23
+ * @example
24
+ * ```tsx
25
+ * <SpecSheet
26
+ * columns={2}
27
+ * groups={[
28
+ * {
29
+ * title: 'Display',
30
+ * rows: [
31
+ * { label: 'Panel', value: 'AMOLED, 6.4"' },
32
+ * { label: 'Resolution', value: '2340 x 1080' },
33
+ * { label: 'Refresh rate', value: '120 Hz' },
34
+ * ],
35
+ * },
36
+ * {
37
+ * title: 'Battery',
38
+ * rows: [
39
+ * { label: 'Capacity', value: '4500 mAh' },
40
+ * { label: 'Charging', value: '65 W wired' },
41
+ * ],
42
+ * },
43
+ * ]}
44
+ * />
45
+ * ```
46
+ */
47
+ export declare function SpecSheet(props: SpecSheetProps): JSX.Element;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@a4ui/core",
3
- "version": "0.22.0",
3
+ "version": "0.24.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",
@@ -0,0 +1,114 @@
1
+ // Horizontal grading meter for a graded score (e.g. a pre-owned item's
2
+ // condition), with a positioned marker, the resolved tier label, and
3
+ // endpoint labels under the track.
4
+ import type { JSX } from 'solid-js'
5
+ import { For, Show } from 'solid-js'
6
+
7
+ import { cn } from '../lib/cn'
8
+
9
+ export interface ConditionTier {
10
+ /** Minimum score (inclusive) at which this tier applies. */
11
+ min: number
12
+ label: string
13
+ }
14
+
15
+ export interface ConditionScaleProps {
16
+ /** Current score. */
17
+ value: number
18
+ /** Top of the scale. Defaults to 10. */
19
+ max?: number
20
+ /** Bottom of the scale. Defaults to 1. */
21
+ min?: number
22
+ /** Tier labels, highest `min` first or any order — component sorts. Defaults to a sensible New→Poor scale. */
23
+ tiers?: ConditionTier[]
24
+ /** Show numeric value (e.g. "8.5 / 10"). Defaults to true. */
25
+ showValue?: boolean
26
+ class?: string
27
+ }
28
+
29
+ const DEFAULT_TIERS: ConditionTier[] = [
30
+ { min: 10, label: 'Brand New' },
31
+ { min: 9, label: 'Excellent' },
32
+ { min: 7, label: 'Very Good' },
33
+ { min: 5, label: 'Good' },
34
+ { min: 1, label: 'Fair' },
35
+ ]
36
+
37
+ /**
38
+ * Horizontal grading meter (e.g. a pre-owned item's 1–10 condition score)
39
+ * with a marker at the current value, the resolved tier label, and endpoint
40
+ * labels under the track.
41
+ *
42
+ * @example
43
+ * ```tsx
44
+ * <ConditionScale value={8} />
45
+ * ```
46
+ *
47
+ * @example
48
+ * ```tsx
49
+ * <ConditionScale
50
+ * value={62}
51
+ * min={0}
52
+ * max={100}
53
+ * tiers={[
54
+ * { min: 90, label: 'Like New' },
55
+ * { min: 70, label: 'Lightly Used' },
56
+ * { min: 40, label: 'Worn' },
57
+ * { min: 0, label: 'Heavily Worn' },
58
+ * ]}
59
+ * />
60
+ * ```
61
+ */
62
+ export function ConditionScale(props: ConditionScaleProps): JSX.Element {
63
+ const min = () => props.min ?? 1
64
+ const max = () => props.max ?? 10
65
+ const tiers = () => (props.tiers ?? DEFAULT_TIERS).slice().sort((a, b) => a.min - b.min)
66
+
67
+ const percent = () => {
68
+ const range = max() - min()
69
+ if (range <= 0) return 0
70
+ return Math.max(0, Math.min(100, ((props.value - min()) / range) * 100))
71
+ }
72
+
73
+ const resolvedTier = () => {
74
+ const sorted = tiers()
75
+ let current = sorted[0]
76
+ for (const tier of sorted) {
77
+ if (tier.min <= props.value) current = tier
78
+ }
79
+ return current
80
+ }
81
+
82
+ return (
83
+ <div class={cn('flex flex-col gap-2', props.class)}>
84
+ <div class="flex items-end justify-between gap-2">
85
+ <span class="font-semibold text-foreground">{resolvedTier()?.label}</span>
86
+ <Show when={props.showValue ?? true}>
87
+ <span class="text-sm text-muted-foreground">
88
+ {props.value} / {max()}
89
+ </span>
90
+ </Show>
91
+ </div>
92
+ <div
93
+ class="relative mt-2 h-2 w-full rounded-full bg-muted"
94
+ role="meter"
95
+ aria-valuemin={min()}
96
+ aria-valuemax={max()}
97
+ aria-valuenow={props.value}
98
+ aria-label={resolvedTier()?.label ?? 'Condition'}
99
+ >
100
+ <div
101
+ class="absolute h-2 rounded-full bg-gradient-to-r from-primary/40 to-primary"
102
+ style={{ width: `${percent()}%` }}
103
+ />
104
+ <div
105
+ class="absolute -top-1 h-4 w-4 -translate-x-1/2 rounded-full border-2 border-background bg-primary shadow"
106
+ style={{ left: `${percent()}%` }}
107
+ />
108
+ </div>
109
+ <div class="flex items-center justify-between text-xs text-muted-foreground">
110
+ <For each={[tiers()[0], tiers()[tiers().length - 1]]}>{(tier) => <span>{tier?.label}</span>}</For>
111
+ </div>
112
+ </div>
113
+ )
114
+ }
@@ -0,0 +1,222 @@
1
+ // Full faceted-filter panel: collapsible checkbox sections (via FilterGroup),
2
+ // a min/max price range, active-filter chips, a result count, and clear-all.
3
+ // Fully controlled — the caller owns `selected` and `price`; the only local
4
+ // state is each section's collapsed/expanded flag.
5
+ import { X } from 'lucide-solid'
6
+ import type { JSX } from 'solid-js'
7
+ import { createSignal, For, Show } from 'solid-js'
8
+
9
+ import { cn } from '../lib/cn'
10
+ import { Button } from '../ui/Button'
11
+ import { Collapse } from '../ui/Collapse'
12
+ import { NumberInput } from '../ui/NumberInput'
13
+ import type { FacetOption } from './FilterGroup'
14
+ import { FilterGroup } from './FilterGroup'
15
+
16
+ export interface FacetSection {
17
+ key: string
18
+ title: string
19
+ options: FacetOption[]
20
+ /** Collapsible section. Defaults to true. */
21
+ collapsible?: boolean
22
+ /** Open by default. Defaults to true. */
23
+ defaultOpen?: boolean
24
+ }
25
+
26
+ export interface FacetPrice {
27
+ min: number
28
+ max: number
29
+ value: [number, number]
30
+ }
31
+
32
+ export interface FacetSidebarProps {
33
+ sections: FacetSection[]
34
+ /** Selected values per section key. */
35
+ selected: Record<string, string[]>
36
+ onChange: (key: string, values: string[]) => void
37
+ /** Optional price range facet. */
38
+ price?: FacetPrice
39
+ onPriceChange?: (range: [number, number]) => void
40
+ /** Result count shown in the header. */
41
+ resultCount?: number
42
+ onClearAll?: () => void
43
+ class?: string
44
+ }
45
+
46
+ interface ActiveChip {
47
+ sectionKey: string
48
+ value: string
49
+ label: string
50
+ }
51
+
52
+ /** One section's title row, collapsible or plain depending on `section.collapsible`. */
53
+ function FacetSectionPanel(props: {
54
+ section: FacetSection
55
+ selected: string[]
56
+ onChange: (values: string[]) => void
57
+ }): JSX.Element {
58
+ // eslint-disable-next-line solid/reactivity -- seed the open state once from defaultOpen; the user toggles from here
59
+ const [open, setOpen] = createSignal(props.section.defaultOpen ?? true)
60
+
61
+ return (
62
+ <Show
63
+ when={props.section.collapsible ?? true}
64
+ fallback={
65
+ <div class="space-y-2">
66
+ <p class="text-sm font-semibold text-foreground">{props.section.title}</p>
67
+ <FilterGroup
68
+ title=""
69
+ options={props.section.options}
70
+ selected={props.selected}
71
+ onChange={props.onChange}
72
+ />
73
+ </div>
74
+ }
75
+ >
76
+ <Collapse open={open()} onOpenChange={setOpen} title={props.section.title}>
77
+ <FilterGroup
78
+ title=""
79
+ options={props.section.options}
80
+ selected={props.selected}
81
+ onChange={props.onChange}
82
+ />
83
+ </Collapse>
84
+ </Show>
85
+ )
86
+ }
87
+
88
+ /**
89
+ * Full faceted-filter sidebar panel: composes {@link FilterGroup} checkbox
90
+ * sections (each optionally collapsible) with a min/max price range, a row of
91
+ * removable active-filter chips, a result count, and a clear-all action.
92
+ * Fully controlled — the caller owns `selected` and `price` state.
93
+ *
94
+ * @example
95
+ * ```tsx
96
+ * const [selected, setSelected] = createSignal<Record<string, string[]>>({ type: ['mug'] })
97
+ * const [price, setPrice] = createSignal<[number, number]>([0, 100])
98
+ * <FacetSidebar
99
+ * class="w-64"
100
+ * sections={[
101
+ * { key: 'type', title: 'Type', options: [{ value: 'mug', label: 'Mug', count: 4 }, { value: 'plate', label: 'Plate', count: 2 }] },
102
+ * { key: 'color', title: 'Color', options: [{ value: 'blue', label: 'Blue', count: 3 }, { value: 'green', label: 'Green', count: 1 }] },
103
+ * ]}
104
+ * selected={selected()}
105
+ * onChange={(key, values) => setSelected({ ...selected(), [key]: values })}
106
+ * price={{ min: 0, max: 100, value: price() }}
107
+ * onPriceChange={setPrice}
108
+ * resultCount={6}
109
+ * onClearAll={() => { setSelected({}); setPrice([0, 100]) }}
110
+ * />
111
+ * ```
112
+ */
113
+ export function FacetSidebar(props: FacetSidebarProps): JSX.Element {
114
+ const activeChips = (): ActiveChip[] => {
115
+ const chips: ActiveChip[] = []
116
+ for (const section of props.sections) {
117
+ const values = props.selected[section.key] ?? []
118
+ for (const value of values) {
119
+ const option = section.options.find((candidate) => candidate.value === value)
120
+ chips.push({ sectionKey: section.key, value, label: option?.label ?? value })
121
+ }
122
+ }
123
+ return chips
124
+ }
125
+
126
+ const removeChip = (chip: ActiveChip): void => {
127
+ const values = props.selected[chip.sectionKey] ?? []
128
+ props.onChange(
129
+ chip.sectionKey,
130
+ values.filter((value) => value !== chip.value),
131
+ )
132
+ }
133
+
134
+ const setMin = (raw: number): void => {
135
+ if (!props.price || !props.onPriceChange) return
136
+ const min = Math.min(raw, props.price.value[1])
137
+ props.onPriceChange([min, props.price.value[1]])
138
+ }
139
+
140
+ const setMax = (raw: number): void => {
141
+ if (!props.price || !props.onPriceChange) return
142
+ const max = Math.max(raw, props.price.value[0])
143
+ props.onPriceChange([props.price.value[0], max])
144
+ }
145
+
146
+ return (
147
+ <div class={cn('space-y-5', props.class)}>
148
+ <div class="flex items-center justify-between gap-2">
149
+ <Show when={props.resultCount != null}>
150
+ <p class="text-sm text-muted-foreground">{props.resultCount} results</p>
151
+ </Show>
152
+ <Show when={props.onClearAll}>
153
+ <Button
154
+ variant="ghost"
155
+ class="ml-auto h-auto px-2 py-1 text-xs"
156
+ onClick={() => props.onClearAll?.()}
157
+ >
158
+ Clear all
159
+ </Button>
160
+ </Show>
161
+ </div>
162
+
163
+ <Show when={activeChips().length > 0}>
164
+ <div class="flex flex-wrap gap-2">
165
+ <For each={activeChips()}>
166
+ {(chip) => (
167
+ <span class="inline-flex items-center gap-1 rounded-full border border-border bg-muted px-2 py-0.5 text-xs">
168
+ {chip.label}
169
+ <button
170
+ type="button"
171
+ onClick={() => removeChip(chip)}
172
+ aria-label={`Remove ${chip.label}`}
173
+ class="text-muted-foreground hover:text-foreground"
174
+ >
175
+ <X class="h-3 w-3" />
176
+ </button>
177
+ </span>
178
+ )}
179
+ </For>
180
+ </div>
181
+ </Show>
182
+
183
+ <Show when={props.price}>
184
+ {(price) => (
185
+ <div class="space-y-2">
186
+ <div class="flex items-center justify-between">
187
+ <p class="text-sm font-semibold text-foreground">Price</p>
188
+ <p class="text-xs text-muted-foreground">
189
+ ${price().value[0]} – ${price().value[1]}
190
+ </p>
191
+ </div>
192
+ <div class="flex items-center gap-2">
193
+ <NumberInput
194
+ value={price().value[0]}
195
+ onChange={setMin}
196
+ min={price().min}
197
+ max={price().value[1]}
198
+ />
199
+ <span class="text-sm text-muted-foreground">–</span>
200
+ <NumberInput
201
+ value={price().value[1]}
202
+ onChange={setMax}
203
+ min={price().value[0]}
204
+ max={price().max}
205
+ />
206
+ </div>
207
+ </div>
208
+ )}
209
+ </Show>
210
+
211
+ <For each={props.sections}>
212
+ {(section) => (
213
+ <FacetSectionPanel
214
+ section={section}
215
+ selected={props.selected[section.key] ?? []}
216
+ onChange={(values) => props.onChange(section.key, values)}
217
+ />
218
+ )}
219
+ </For>
220
+ </div>
221
+ )
222
+ }
@@ -0,0 +1,133 @@
1
+ // Richer price display than PriceTag: compare-at savings badge, a coupon
2
+ // chip with the after-coupon price, and an optional financing estimate line.
3
+ import { CreditCard } 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 { Badge } from '../ui/Badge'
9
+
10
+ /** Text size of a {@link PriceBlock}'s main amount. Defaults to `'lg'`. */
11
+ export type PriceBlockSize = 'md' | 'lg'
12
+
13
+ const SIZE_CLASSES: Record<PriceBlockSize, string> = {
14
+ md: 'text-xl',
15
+ lg: 'text-3xl',
16
+ }
17
+
18
+ export interface PriceBlockCoupon {
19
+ code: string
20
+ /** Flat amount off (currency units). If omitted, no reduction is computed. */
21
+ amount?: number
22
+ /** Optional label override. */
23
+ label?: string
24
+ }
25
+
26
+ export interface PriceBlockFinancing {
27
+ /** Number of monthly installments. */
28
+ months: number
29
+ /** Annual APR as a decimal, e.g. 0.1 for 10%. Omit/0 = interest-free split. */
30
+ apr?: number
31
+ }
32
+
33
+ export interface PriceBlockProps {
34
+ amount: number
35
+ /** Original price; struck through and used for the savings badge when > amount. */
36
+ compareAt?: number
37
+ /** ISO 4217 currency code. Defaults to `'USD'`. */
38
+ currency?: string
39
+ /** BCP 47 locale used for formatting. Defaults to `'en-US'`. */
40
+ locale?: string
41
+ /** Coupon; renders a chip and the after-coupon price. */
42
+ coupon?: PriceBlockCoupon
43
+ /** Financing estimate; renders "As low as $X/mo". */
44
+ financing?: PriceBlockFinancing
45
+ /** Extra note under the price (e.g. a wire-transfer discount line). */
46
+ note?: JSX.Element
47
+ /** Text size of the main price. Defaults to `'lg'`. */
48
+ size?: PriceBlockSize
49
+ class?: string
50
+ }
51
+
52
+ /**
53
+ * Full price display: main amount, optional struck-through compare-at price
54
+ * with a savings badge, a coupon chip with the after-coupon price, and an
55
+ * optional financing estimate ("As low as $X/mo").
56
+ *
57
+ * @example
58
+ * ```tsx
59
+ * <PriceBlock
60
+ * amount={249}
61
+ * compareAt={329}
62
+ * coupon={{ code: 'SAVE50', amount: 50 }}
63
+ * financing={{ months: 12, apr: 0.1 }}
64
+ * />
65
+ * ```
66
+ */
67
+ export function PriceBlock(props: PriceBlockProps): JSX.Element {
68
+ const format = (value: number) =>
69
+ new Intl.NumberFormat(props.locale ?? 'en-US', {
70
+ style: 'currency',
71
+ currency: props.currency ?? 'USD',
72
+ }).format(value)
73
+
74
+ const onSale = () => (props.compareAt ?? 0) > props.amount
75
+
76
+ const savings = () => (props.compareAt as number) - props.amount
77
+
78
+ const discountPercent = () => Math.round((1 - props.amount / (props.compareAt as number)) * 100)
79
+
80
+ const afterCouponAmount = () => {
81
+ const couponAmount = props.coupon?.amount ?? 0
82
+ return Math.max(0, props.amount - couponAmount)
83
+ }
84
+
85
+ const monthlyPayment = () => {
86
+ const financing = props.financing as PriceBlockFinancing
87
+ const apr = financing.apr ?? 0
88
+ if (apr > 0) {
89
+ const r = apr / 12
90
+ const n = financing.months
91
+ const m = (props.amount * r) / (1 - (1 + r) ** -n)
92
+ return Math.round(m * 100) / 100
93
+ }
94
+ return Math.round((props.amount / financing.months) * 100) / 100
95
+ }
96
+
97
+ return (
98
+ <div class={cn('flex flex-col gap-1', props.class)}>
99
+ <div class="flex flex-wrap items-center gap-2">
100
+ <span class={cn('font-bold text-foreground', SIZE_CLASSES[props.size ?? 'lg'])}>
101
+ {format(props.amount)}
102
+ </span>
103
+ <Show when={onSale()}>
104
+ <span class="text-muted-foreground line-through">{format(props.compareAt as number)}</span>
105
+ <Badge tone="danger">
106
+ Save {format(savings())} (-{discountPercent()}%)
107
+ </Badge>
108
+ </Show>
109
+ </div>
110
+ <Show when={props.coupon}>
111
+ {(coupon) => (
112
+ <div class="flex flex-wrap items-center gap-2">
113
+ <span class="inline-flex items-center gap-1 rounded-md border border-dashed border-primary/50 px-2 py-0.5 text-xs font-mono text-primary">
114
+ {coupon().label ?? 'Coupon'} {coupon().code}
115
+ </span>
116
+ <Show when={coupon().amount !== undefined}>
117
+ <span class="text-sm text-muted-foreground">
118
+ {format(afterCouponAmount())} after code {coupon().code}
119
+ </span>
120
+ </Show>
121
+ </div>
122
+ )}
123
+ </Show>
124
+ <Show when={props.financing}>
125
+ <span class="inline-flex items-center gap-1 text-sm text-muted-foreground">
126
+ <CreditCard class="h-4 w-4" />
127
+ As low as {format(monthlyPayment())}/mo
128
+ </span>
129
+ </Show>
130
+ <Show when={props.note}>{props.note}</Show>
131
+ </div>
132
+ )
133
+ }
@@ -8,4 +8,13 @@ export { ProductGrid, type ProductGridProps } from './ProductGrid'
8
8
  export { CartLine, type CartLineProps } from './CartLine'
9
9
  export { CartSummary, type CartSummaryProps, type CartSummaryItem } from './CartSummary'
10
10
  export { FilterGroup, type FilterGroupProps, type FacetOption } from './FilterGroup'
11
+ export { FacetSidebar, type FacetSidebarProps, type FacetSection, type FacetPrice } from './FacetSidebar'
12
+ export {
13
+ PriceBlock,
14
+ type PriceBlockProps,
15
+ type PriceBlockCoupon,
16
+ type PriceBlockFinancing,
17
+ type PriceBlockSize,
18
+ } from './PriceBlock'
19
+ export { ConditionScale, type ConditionScaleProps, type ConditionTier } from './ConditionScale'
11
20
  export { createCart, type Cart, type CartProduct, type CartEntry } from './createCart'
package/src/index.ts CHANGED
@@ -8,7 +8,7 @@
8
8
  // import '@a4ui/core/styles.css'
9
9
  // import { Button, Card, Modal } from '@a4ui/core'
10
10
 
11
- export const A4UI_VERSION = '0.22.0'
11
+ export const A4UI_VERSION = '0.24.0'
12
12
 
13
13
  // Helpers (src/lib) — generic, framework-level utilities.
14
14
  export { cn } from './lib/cn'
@@ -68,6 +68,7 @@ export { Tooltip } from './ui/Tooltip'
68
68
  export { Popover } from './ui/Popover'
69
69
  export { HoverCard } from './ui/HoverCard'
70
70
  export { Alert, type AlertTone } from './ui/Alert'
71
+ export { AnnouncementBar, type AnnouncementBarProps, type AnnouncementTone } from './ui/AnnouncementBar'
71
72
  export { Avatar } from './ui/Avatar'
72
73
  export { Skeleton } from './ui/Skeleton'
73
74
  export { Separator } from './ui/Separator'
@@ -102,6 +103,8 @@ export { Tree, type TreeNode, type TreeProps } from './ui/Tree'
102
103
  export { Kbd } from './ui/Kbd'
103
104
  export { AvatarGroup, type AvatarGroupProps } from './ui/AvatarGroup'
104
105
  export { Descriptions, type DescriptionItem, type DescriptionsProps } from './ui/Descriptions'
106
+ export { SpecSheet, type SpecSheetProps, type SpecGroup, type SpecRow } from './ui/SpecSheet'
107
+ export { RatingsSummary, type RatingsSummaryProps, type RatingSource } from './ui/RatingsSummary'
105
108
  export { Result, type ResultStatus, type ResultProps } from './ui/Result'
106
109
  export { Splitter, type SplitterProps } from './ui/Splitter'
107
110
  export { Command, type CommandItem, type CommandProps } from './ui/Command'
@@ -124,6 +127,7 @@ export { Image, type ImageProps } from './ui/Image'
124
127
  export { SpeedDial, type SpeedDialAction, type SpeedDialProps } from './ui/SpeedDial'
125
128
  export { BottomNavigation, type BottomNavItem, type BottomNavigationProps } from './ui/BottomNavigation'
126
129
  export { Marquee } from './ui/Marquee'
130
+ export { LogoWall, type LogoWallProps, type LogoItem } from './ui/LogoWall'
127
131
  export { DataGrid, type DataGridColumn, type DataGridProps } from './ui/DataGrid'
128
132
  export { TreeSelect, type TreeSelectProps } from './ui/TreeSelect'
129
133
  export { Cascader, type CascaderOption, type CascaderProps } from './ui/Cascader'