@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.
- package/README.md +36 -26
- package/dist/full.css +3042 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +8 -1
- package/src/charts/BarChart.tsx +55 -0
- package/src/charts/DonutChart.tsx +127 -0
- package/src/charts/Sparkline.tsx +101 -0
- package/src/charts/index.ts +5 -0
- package/src/commerce/CartLine.tsx +84 -0
- package/src/commerce/CartSummary.tsx +70 -0
- package/src/commerce/FilterGroup.tsx +70 -0
- package/src/commerce/PriceTag.tsx +62 -0
- package/src/commerce/ProductCard.tsx +121 -0
- package/src/commerce/ProductGrid.tsx +27 -0
- package/src/commerce/QuantityStepper.tsx +59 -0
- package/src/commerce/index.ts +10 -0
- package/src/elements.tsx +167 -0
- package/src/index.ts +166 -0
- package/src/layout/AppShell.tsx +102 -0
- package/src/layout/ChristmasBackground.tsx +177 -0
- package/src/layout/EffectsToggle.tsx +41 -0
- package/src/layout/NavGroup.tsx +45 -0
- package/src/layout/SnowScenery.tsx +76 -0
- package/src/layout/SpaceBackground.tsx +459 -0
- package/src/layout/ThemeToggle.tsx +40 -0
- package/src/layout/ThemedScenery.tsx +179 -0
- package/src/layout/sceneEffects.ts +49 -0
- package/src/lib/cn.ts +17 -0
- package/src/lib/effects.ts +71 -0
- package/src/lib/media.ts +27 -0
- package/src/lib/motion.ts +191 -0
- package/src/lib/theme.ts +113 -0
- package/src/lib/virtual.ts +33 -0
- package/src/styles/space.css +510 -0
- package/src/styles/tokens.css +213 -0
- package/src/themes/index.ts +102 -0
- package/src/themes/palettes.ts +368 -0
- package/src/ui/Accordion.tsx +65 -0
- package/src/ui/Affix.tsx +72 -0
- package/src/ui/Alert.tsx +55 -0
- package/src/ui/AlertDialog.tsx +58 -0
- package/src/ui/Anchor.tsx +100 -0
- package/src/ui/Avatar.tsx +34 -0
- package/src/ui/AvatarGroup.tsx +55 -0
- package/src/ui/BackToTop.tsx +51 -0
- package/src/ui/Badge.tsx +46 -0
- package/src/ui/BottomNavigation.tsx +65 -0
- package/src/ui/Breadcrumb.tsx +64 -0
- package/src/ui/Button.tsx +49 -0
- package/src/ui/Calendar.tsx +40 -0
- package/src/ui/CalendarHeatmap.tsx +180 -0
- package/src/ui/Card.tsx +84 -0
- package/src/ui/Carousel.tsx +121 -0
- package/src/ui/Cascader.tsx +158 -0
- package/src/ui/Checkbox.tsx +35 -0
- package/src/ui/Clock.tsx +221 -0
- package/src/ui/Collapse.tsx +48 -0
- package/src/ui/ColorPicker.tsx +89 -0
- package/src/ui/Combobox.tsx +67 -0
- package/src/ui/Command.tsx +158 -0
- package/src/ui/Comment.tsx +91 -0
- package/src/ui/ContextMenu.tsx +58 -0
- package/src/ui/Countdown.tsx +123 -0
- package/src/ui/DataGrid.tsx +177 -0
- package/src/ui/DateField.tsx +177 -0
- package/src/ui/DateRangePicker.tsx +204 -0
- package/src/ui/DateTimeField.tsx +68 -0
- package/src/ui/Descriptions.tsx +56 -0
- package/src/ui/Drawer.tsx +72 -0
- package/src/ui/Dropdown.tsx +71 -0
- package/src/ui/Dropzone.tsx +88 -0
- package/src/ui/Empty.tsx +53 -0
- package/src/ui/FileUpload.tsx +198 -0
- package/src/ui/FloatingActionButton.tsx +48 -0
- package/src/ui/Form.tsx +108 -0
- package/src/ui/Highlight.tsx +53 -0
- package/src/ui/HoverCard.tsx +42 -0
- package/src/ui/Image.tsx +66 -0
- package/src/ui/Input.tsx +38 -0
- package/src/ui/Kbd.tsx +25 -0
- package/src/ui/List.tsx +71 -0
- package/src/ui/Marquee.tsx +48 -0
- package/src/ui/Mentions.tsx +170 -0
- package/src/ui/Meter.tsx +57 -0
- package/src/ui/Modal.tsx +106 -0
- package/src/ui/MultiSelect.tsx +236 -0
- package/src/ui/NotificationCenter.tsx +103 -0
- package/src/ui/NumberInput.tsx +48 -0
- package/src/ui/PageHeader.tsx +60 -0
- package/src/ui/Pagination.tsx +62 -0
- package/src/ui/Popover.tsx +42 -0
- package/src/ui/Portal.tsx +34 -0
- package/src/ui/Progress.tsx +55 -0
- package/src/ui/RadioGroup.tsx +65 -0
- package/src/ui/Rating.tsx +98 -0
- package/src/ui/Result.tsx +75 -0
- package/src/ui/RingProgress.tsx +75 -0
- package/src/ui/SegmentedControl.tsx +61 -0
- package/src/ui/Select.tsx +55 -0
- package/src/ui/Separator.tsx +39 -0
- package/src/ui/Skeleton.tsx +23 -0
- package/src/ui/Slider.tsx +57 -0
- package/src/ui/Sortable.tsx +174 -0
- package/src/ui/SpeedDial.tsx +75 -0
- package/src/ui/Spinner.tsx +33 -0
- package/src/ui/Splitter.tsx +115 -0
- package/src/ui/Stat.tsx +74 -0
- package/src/ui/Stepper.tsx +134 -0
- package/src/ui/Switch.tsx +44 -0
- package/src/ui/Table.tsx +128 -0
- package/src/ui/Tabs.tsx +72 -0
- package/src/ui/TagInput.tsx +85 -0
- package/src/ui/Textarea.tsx +38 -0
- package/src/ui/TimeField.tsx +298 -0
- package/src/ui/Timeline.tsx +89 -0
- package/src/ui/Toast.tsx +68 -0
- package/src/ui/Toggle.tsx +41 -0
- package/src/ui/ToggleGroup.tsx +59 -0
- package/src/ui/Tooltip.tsx +42 -0
- package/src/ui/Tour.tsx +197 -0
- package/src/ui/Transfer.tsx +112 -0
- package/src/ui/Tree.tsx +100 -0
- package/src/ui/TreeSelect.tsx +151 -0
- package/src/ui/VirtualList.tsx +81 -0
- package/src/ui/internal/CalendarCore.tsx +276 -0
|
@@ -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'
|
package/src/elements.tsx
ADDED
|
@@ -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()
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// A4ui — public entry point.
|
|
2
|
+
//
|
|
3
|
+
// Components are extracted incrementally from the source project
|
|
4
|
+
// (sonora precision/alfredorust/solid/src/components). As each lands under
|
|
5
|
+
// src/ui/ or src/layout/, re-export it here. See CLAUDE.md → "Extraction plan".
|
|
6
|
+
//
|
|
7
|
+
// Consumers import the styles once (tokens + motion) and the components:
|
|
8
|
+
// import '@a4ui/core/styles.css'
|
|
9
|
+
// import { Button, Card, Modal } from '@a4ui/core'
|
|
10
|
+
|
|
11
|
+
export const A4UI_VERSION = '0.12.0'
|
|
12
|
+
|
|
13
|
+
// Helpers (src/lib) — generic, framework-level utilities.
|
|
14
|
+
export { cn } from './lib/cn'
|
|
15
|
+
export { useTheme, toggleTheme, setTheme, storedTheme, applyTheme, toggled, type Theme } from './lib/theme'
|
|
16
|
+
export { useEffects, isCalm, setEffects } from './lib/effects'
|
|
17
|
+
export {
|
|
18
|
+
prefersReducedMotion,
|
|
19
|
+
motionReduced,
|
|
20
|
+
useMotionForced,
|
|
21
|
+
setMotionForced,
|
|
22
|
+
createCountUp,
|
|
23
|
+
animateIn,
|
|
24
|
+
revealOnScroll,
|
|
25
|
+
// Motion (motion.dev) imperative API, re-exported so the whole app shares one
|
|
26
|
+
// engine (used by Stat's entrance / count-up; reach for these for scroll-linked
|
|
27
|
+
// reveals, staggered lists, springs, etc.).
|
|
28
|
+
animate,
|
|
29
|
+
inView,
|
|
30
|
+
scroll,
|
|
31
|
+
stagger,
|
|
32
|
+
spring,
|
|
33
|
+
} from './lib/motion'
|
|
34
|
+
export { useMediaQuery } from './lib/media'
|
|
35
|
+
export { remeasureAfterLayout } from './lib/virtual'
|
|
36
|
+
|
|
37
|
+
// UI components (src/ui) — all 18 extracted. See CLAUDE.md.
|
|
38
|
+
export { Accordion, type AccordionItem } from './ui/Accordion'
|
|
39
|
+
export { Badge, type BadgeTone } from './ui/Badge'
|
|
40
|
+
export { Button, type ButtonVariant } from './ui/Button'
|
|
41
|
+
export { Card, CardHeader, CardTitle, CardContent } from './ui/Card'
|
|
42
|
+
export { Checkbox } from './ui/Checkbox'
|
|
43
|
+
export { DateField } from './ui/DateField'
|
|
44
|
+
export { TimeField, type TimeFieldProps } from './ui/TimeField'
|
|
45
|
+
export { DateTimeField, type DateTimeFieldProps } from './ui/DateTimeField'
|
|
46
|
+
export { DateRangePicker, type DateRange, type DateRangePickerProps } from './ui/DateRangePicker'
|
|
47
|
+
export { FormField, FormLabel, FormControl, FormDescription, FormError, type FormFieldProps } from './ui/Form'
|
|
48
|
+
export { FileUpload, type UploadFile, type FileUploadProps } from './ui/FileUpload'
|
|
49
|
+
export { MultiSelect, type MultiSelectOption, type MultiSelectProps } from './ui/MultiSelect'
|
|
50
|
+
export { Drawer } from './ui/Drawer'
|
|
51
|
+
export { Dropdown, type DropdownItem } from './ui/Dropdown'
|
|
52
|
+
export { Dropzone } from './ui/Dropzone'
|
|
53
|
+
export { Input } from './ui/Input'
|
|
54
|
+
export { Modal } from './ui/Modal'
|
|
55
|
+
export { PageHeader } from './ui/PageHeader'
|
|
56
|
+
export { Pagination } from './ui/Pagination'
|
|
57
|
+
export { Select } from './ui/Select'
|
|
58
|
+
export { Spinner } from './ui/Spinner'
|
|
59
|
+
export { Stat, type StatTone } from './ui/Stat'
|
|
60
|
+
export { Table, TableHead, TableBody, TableRow, TableHeadCell, TableCell } from './ui/Table'
|
|
61
|
+
export { VirtualList } from './ui/VirtualList'
|
|
62
|
+
export { Tabs, type TabItem } from './ui/Tabs'
|
|
63
|
+
export { toast, Toaster, type ToastTone } from './ui/Toast'
|
|
64
|
+
export { Switch } from './ui/Switch'
|
|
65
|
+
export { RadioGroup, type RadioOption } from './ui/RadioGroup'
|
|
66
|
+
export { Textarea } from './ui/Textarea'
|
|
67
|
+
export { Tooltip } from './ui/Tooltip'
|
|
68
|
+
export { Popover } from './ui/Popover'
|
|
69
|
+
export { HoverCard } from './ui/HoverCard'
|
|
70
|
+
export { Alert, type AlertTone } from './ui/Alert'
|
|
71
|
+
export { Avatar } from './ui/Avatar'
|
|
72
|
+
export { Skeleton } from './ui/Skeleton'
|
|
73
|
+
export { Separator } from './ui/Separator'
|
|
74
|
+
export { Progress } from './ui/Progress'
|
|
75
|
+
export { Meter } from './ui/Meter'
|
|
76
|
+
export { Slider } from './ui/Slider'
|
|
77
|
+
export { NumberInput } from './ui/NumberInput'
|
|
78
|
+
export { Toggle } from './ui/Toggle'
|
|
79
|
+
export { ToggleGroup, type ToggleGroupOption } from './ui/ToggleGroup'
|
|
80
|
+
export { SegmentedControl, type SegmentedOption } from './ui/SegmentedControl'
|
|
81
|
+
export { Breadcrumb, type BreadcrumbItem } from './ui/Breadcrumb'
|
|
82
|
+
export { AlertDialog } from './ui/AlertDialog'
|
|
83
|
+
export { ContextMenu, type ContextMenuItem } from './ui/ContextMenu'
|
|
84
|
+
export { Combobox } from './ui/Combobox'
|
|
85
|
+
export { Carousel, type CarouselProps } from './ui/Carousel'
|
|
86
|
+
export { Stepper, type StepItem, type StepperProps } from './ui/Stepper'
|
|
87
|
+
export { Timeline, type TimelineItem, type TimelineTone, type TimelineProps } from './ui/Timeline'
|
|
88
|
+
export { Rating, type RatingProps } from './ui/Rating'
|
|
89
|
+
export { Empty, type EmptyProps } from './ui/Empty'
|
|
90
|
+
export { Calendar, type CalendarProps } from './ui/Calendar'
|
|
91
|
+
export { Tree, type TreeNode, type TreeProps } from './ui/Tree'
|
|
92
|
+
export { Kbd } from './ui/Kbd'
|
|
93
|
+
export { AvatarGroup, type AvatarGroupProps } from './ui/AvatarGroup'
|
|
94
|
+
export { Descriptions, type DescriptionItem, type DescriptionsProps } from './ui/Descriptions'
|
|
95
|
+
export { Result, type ResultStatus, type ResultProps } from './ui/Result'
|
|
96
|
+
export { Splitter, type SplitterProps } from './ui/Splitter'
|
|
97
|
+
export { Command, type CommandItem, type CommandProps } from './ui/Command'
|
|
98
|
+
export { TagInput, type TagInputProps } from './ui/TagInput'
|
|
99
|
+
export { Collapse } from './ui/Collapse'
|
|
100
|
+
export { RingProgress, type RingProgressProps } from './ui/RingProgress'
|
|
101
|
+
export { BackToTop, type BackToTopProps } from './ui/BackToTop'
|
|
102
|
+
export {
|
|
103
|
+
FloatingActionButton,
|
|
104
|
+
type FloatingActionButtonProps,
|
|
105
|
+
type FloatingActionButtonPosition,
|
|
106
|
+
} from './ui/FloatingActionButton'
|
|
107
|
+
export { Anchor, type AnchorItem, type AnchorProps } from './ui/Anchor'
|
|
108
|
+
export { Highlight, type HighlightProps } from './ui/Highlight'
|
|
109
|
+
export { List, type ListItem } from './ui/List'
|
|
110
|
+
export { Countdown, type CountdownProps } from './ui/Countdown'
|
|
111
|
+
export { Affix } from './ui/Affix'
|
|
112
|
+
export { Transfer, type TransferItem, type TransferProps } from './ui/Transfer'
|
|
113
|
+
export { Image, type ImageProps } from './ui/Image'
|
|
114
|
+
export { SpeedDial, type SpeedDialAction, type SpeedDialProps } from './ui/SpeedDial'
|
|
115
|
+
export { BottomNavigation, type BottomNavItem, type BottomNavigationProps } from './ui/BottomNavigation'
|
|
116
|
+
export { Marquee } from './ui/Marquee'
|
|
117
|
+
export { DataGrid, type DataGridColumn, type DataGridProps } from './ui/DataGrid'
|
|
118
|
+
export { TreeSelect, type TreeSelectProps } from './ui/TreeSelect'
|
|
119
|
+
export { Cascader, type CascaderOption, type CascaderProps } from './ui/Cascader'
|
|
120
|
+
export { Mentions, type MentionOption, type MentionsProps } from './ui/Mentions'
|
|
121
|
+
export { Tour, type TourStep, type TourProps } from './ui/Tour'
|
|
122
|
+
export {
|
|
123
|
+
NotificationCenter,
|
|
124
|
+
type NotificationItem,
|
|
125
|
+
type NotificationCenterProps,
|
|
126
|
+
} from './ui/NotificationCenter'
|
|
127
|
+
export { ColorPicker, type ColorPickerProps } from './ui/ColorPicker'
|
|
128
|
+
export { Comment, type CommentData, type CommentProps } from './ui/Comment'
|
|
129
|
+
export { CalendarHeatmap, type HeatmapValue, type CalendarHeatmapProps } from './ui/CalendarHeatmap'
|
|
130
|
+
export { Portal, type PortalProps } from './ui/Portal'
|
|
131
|
+
export { Sortable, type SortableProps } from './ui/Sortable'
|
|
132
|
+
export { Clock, type ClockProps } from './ui/Clock'
|
|
133
|
+
|
|
134
|
+
// Layout (src/layout) — generic shell + backdrop + toggles. App-coupled pieces
|
|
135
|
+
// (Sidebar, Topbar, CompanySwitcher, DemoBanner, CommandPalette) stay in the app.
|
|
136
|
+
export { AppShell } from './layout/AppShell'
|
|
137
|
+
export { SpaceBackground } from './layout/SpaceBackground'
|
|
138
|
+
export { ThemedScenery, type ThemedSceneryProps } from './layout/ThemedScenery'
|
|
139
|
+
export { SnowScenery } from './layout/SnowScenery'
|
|
140
|
+
export { ChristmasBackground } from './layout/ChristmasBackground'
|
|
141
|
+
export { ThemeToggle } from './layout/ThemeToggle'
|
|
142
|
+
export { EffectsToggle } from './layout/EffectsToggle'
|
|
143
|
+
export { NavGroup } from './layout/NavGroup'
|
|
144
|
+
|
|
145
|
+
// Themes (src/themes) — swappable color palettes (Space is the default). Pick one
|
|
146
|
+
// with `selectTheme('dino')`, restore the saved one with `initTheme()`, or build
|
|
147
|
+
// your own from a ThemeDefinition. Distinct from the light/dark `setTheme` above.
|
|
148
|
+
export {
|
|
149
|
+
themes,
|
|
150
|
+
space,
|
|
151
|
+
dino,
|
|
152
|
+
doctor,
|
|
153
|
+
scientist,
|
|
154
|
+
soccer,
|
|
155
|
+
snow,
|
|
156
|
+
christmas,
|
|
157
|
+
activeTheme,
|
|
158
|
+
selectTheme,
|
|
159
|
+
applyThemeDefinition,
|
|
160
|
+
initTheme,
|
|
161
|
+
themeToCss,
|
|
162
|
+
themeToJson,
|
|
163
|
+
TOKEN_ORDER,
|
|
164
|
+
type ThemeDefinition,
|
|
165
|
+
type Palette,
|
|
166
|
+
} from './themes'
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// Generic app shell: the fixed SpaceBackground (z-0) behind a flex layout of an
|
|
2
|
+
// optional sidebar slot + a content column (optional banner + topbar + routed
|
|
3
|
+
// <main>). Unlike the source app's AppShell, this is slot-based and unopinionated
|
|
4
|
+
// about the sidebar — pass your own via the `sidebar` prop, and it manages its
|
|
5
|
+
// own width/collapse (the flex layout reflows the content column automatically,
|
|
6
|
+
// so no --sidebar-w padding coupling). The <main> is wrapped in Suspense + an
|
|
7
|
+
// ErrorBoundary so a route chunk failing or still loading never blanks the shell.
|
|
8
|
+
import { ErrorBoundary, Show, Suspense, type JSX, type ParentProps } from 'solid-js'
|
|
9
|
+
|
|
10
|
+
import { Button } from '../ui/Button'
|
|
11
|
+
import { Spinner } from '../ui/Spinner'
|
|
12
|
+
import { SpaceBackground } from './SpaceBackground'
|
|
13
|
+
|
|
14
|
+
/** Props for {@link AppShell}. All slots are optional — pass only what your app needs. */
|
|
15
|
+
interface AppShellProps extends ParentProps {
|
|
16
|
+
/** Left column (e.g. a Sidebar). Rendered as a flex child; owns its own width. */
|
|
17
|
+
sidebar?: JSX.Element
|
|
18
|
+
/** Top bar inside the content column, above <main>. */
|
|
19
|
+
topbar?: JSX.Element
|
|
20
|
+
/** Full-width strip above the topbar (e.g. a demo/announcement banner). */
|
|
21
|
+
banner?: JSX.Element
|
|
22
|
+
/** Fixed z-0 backdrop. Defaults to <SpaceBackground/>; pass a node to override,
|
|
23
|
+
or `null` for a plain background. */
|
|
24
|
+
background?: JSX.Element
|
|
25
|
+
/** Max content width for <main>. Default '1400px'. */
|
|
26
|
+
maxWidth?: string
|
|
27
|
+
/** Override the default error fallback shown when a route throws. Receives
|
|
28
|
+
the caught error and a `reset` callback that re-renders the ErrorBoundary's
|
|
29
|
+
children. */
|
|
30
|
+
errorFallback?: (err: unknown, reset: () => void) => JSX.Element
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function defaultErrorFallback(err: unknown, reset: () => void): JSX.Element {
|
|
34
|
+
return (
|
|
35
|
+
<div class="mx-auto max-w-md py-20 text-center">
|
|
36
|
+
<p class="text-lg font-semibold">Something went wrong loading this page</p>
|
|
37
|
+
<p class="mt-1 break-words text-sm text-muted-foreground">{String((err as Error)?.message ?? err)}</p>
|
|
38
|
+
<div class="mt-4 flex justify-center gap-2">
|
|
39
|
+
<Button variant="outline" onClick={reset}>
|
|
40
|
+
Retry
|
|
41
|
+
</Button>
|
|
42
|
+
<Button onClick={() => window.location.reload()}>Reload</Button>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Generic app shell: a fixed {@link SpaceBackground} behind a flex layout of
|
|
50
|
+
* an optional sidebar slot plus a content column (optional banner + topbar +
|
|
51
|
+
* routed `<main>`). Slot-based and unopinionated about the sidebar — pass
|
|
52
|
+
* your own component via `sidebar` and it manages its own width/collapse.
|
|
53
|
+
* `<main>` is wrapped in `Suspense` + an `ErrorBoundary` so a route chunk
|
|
54
|
+
* failing or still loading never blanks the shell.
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```tsx
|
|
58
|
+
* <AppShell sidebar={<Sidebar />} topbar={<Topbar />}>
|
|
59
|
+
* <Route />
|
|
60
|
+
* </AppShell>
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export function AppShell(props: ParentProps<AppShellProps>): JSX.Element {
|
|
64
|
+
const fallback = (err: unknown, reset: () => void) =>
|
|
65
|
+
(props.errorFallback ?? defaultErrorFallback)(err, reset)
|
|
66
|
+
return (
|
|
67
|
+
<div class="relative min-h-screen">
|
|
68
|
+
<Show when={props.background !== null} fallback={null}>
|
|
69
|
+
{props.background ?? <SpaceBackground />}
|
|
70
|
+
</Show>
|
|
71
|
+
<div class="relative z-10 flex min-h-screen">
|
|
72
|
+
{props.sidebar}
|
|
73
|
+
<div class="flex min-w-0 flex-1 flex-col">
|
|
74
|
+
{props.banner}
|
|
75
|
+
{props.topbar}
|
|
76
|
+
<main
|
|
77
|
+
class="relative mx-auto w-full min-w-0 flex-1 px-4 py-6 sm:px-6"
|
|
78
|
+
style={{ 'max-width': props.maxWidth ?? '1400px' }}
|
|
79
|
+
>
|
|
80
|
+
{/* Never blank the content column: Suspense shows a spinner while a
|
|
81
|
+
route's chunk streams in, and the ErrorBoundary catches any other
|
|
82
|
+
runtime error with a friendly retry/reload — the shell stays put. */}
|
|
83
|
+
<ErrorBoundary fallback={fallback}>
|
|
84
|
+
<Suspense
|
|
85
|
+
fallback={
|
|
86
|
+
<div class="flex justify-center py-24 text-muted-foreground">
|
|
87
|
+
<Spinner />
|
|
88
|
+
</div>
|
|
89
|
+
}
|
|
90
|
+
>
|
|
91
|
+
{/* Routed content. No page cross-fade transition: solid-transition-group
|
|
92
|
+
left the outgoing (lazy) page mounted, which stacked its height
|
|
93
|
+
below the new page and created a huge phantom scroll region. */}
|
|
94
|
+
{props.children}
|
|
95
|
+
</Suspense>
|
|
96
|
+
</ErrorBoundary>
|
|
97
|
+
</main>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
)
|
|
102
|
+
}
|