@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.
- package/README.md +80 -37
- package/dist/elements.css +79 -9
- package/dist/full.css +3112 -0
- package/dist/index.d.ts +13 -1
- package/dist/index.js +3392 -2537
- package/dist/lib/flyToCart.d.ts +25 -0
- package/dist/ui/Curtain.d.ts +44 -0
- package/dist/ui/Expandable.d.ts +34 -0
- package/dist/ui/FillText.d.ts +18 -0
- package/dist/ui/HoldToConfirm.d.ts +24 -0
- package/dist/ui/LoadingDots.d.ts +20 -0
- package/dist/ui/MultiStateBadge.d.ts +23 -0
- package/dist/ui/NotificationStack.d.ts +32 -0
- package/dist/ui/NowPlaying.d.ts +21 -0
- package/dist/ui/Parallax.d.ts +21 -0
- package/dist/ui/ScrambleText.d.ts +22 -0
- package/dist/ui/TextReveal.d.ts +23 -0
- 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 +186 -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/flyToCart.ts +103 -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/Curtain.tsx +348 -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/Expandable.tsx +229 -0
- package/src/ui/FileUpload.tsx +198 -0
- package/src/ui/FillText.tsx +63 -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/HoldToConfirm.tsx +141 -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/LoadingDots.tsx +75 -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/MultiStateBadge.tsx +93 -0
- package/src/ui/NotificationCenter.tsx +103 -0
- package/src/ui/NotificationStack.tsx +104 -0
- package/src/ui/NowPlaying.tsx +100 -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/Parallax.tsx +51 -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/ScrambleText.tsx +95 -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 +85 -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/TextReveal.tsx +93 -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,60 @@
|
|
|
1
|
+
// Consistent page header: optional breadcrumb (last crumb highlighted), title,
|
|
2
|
+
// subtitle, and a right-aligned actions slot.
|
|
3
|
+
import { For, Show, type JSX } from 'solid-js'
|
|
4
|
+
|
|
5
|
+
interface PageHeaderProps {
|
|
6
|
+
title: string
|
|
7
|
+
subtitle?: string
|
|
8
|
+
/** Trail of crumb labels, in order; the last one is rendered highlighted as the current page. */
|
|
9
|
+
breadcrumb?: string[]
|
|
10
|
+
/** Right-aligned slot for page-level actions (buttons, menus, etc.). */
|
|
11
|
+
actions?: JSX.Element
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Consistent top-of-page header: optional breadcrumb trail, title, subtitle,
|
|
16
|
+
* and a right-aligned actions slot. Use at the top of a route/page for a
|
|
17
|
+
* uniform layout across the app.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```tsx
|
|
21
|
+
* <PageHeader
|
|
22
|
+
* title="Invoices"
|
|
23
|
+
* subtitle="Manage billing documents"
|
|
24
|
+
* breadcrumb={['Home', 'Billing', 'Invoices']}
|
|
25
|
+
* actions={<Button>New invoice</Button>}
|
|
26
|
+
* />
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export function PageHeader(props: PageHeaderProps): JSX.Element {
|
|
30
|
+
return (
|
|
31
|
+
<div class="flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
|
|
32
|
+
<div>
|
|
33
|
+
<Show when={props.breadcrumb?.length}>
|
|
34
|
+
<div class="flex items-center gap-2 text-[12px] text-muted-foreground">
|
|
35
|
+
<For each={props.breadcrumb}>
|
|
36
|
+
{(crumb, i) => (
|
|
37
|
+
<>
|
|
38
|
+
<Show when={i() > 0}>
|
|
39
|
+
{/* Same "/" separator as the standalone Breadcrumb component. */}
|
|
40
|
+
<span class="text-muted-foreground/60">/</span>
|
|
41
|
+
</Show>
|
|
42
|
+
<span classList={{ 'text-foreground': i() === (props.breadcrumb?.length ?? 0) - 1 }}>
|
|
43
|
+
{crumb}
|
|
44
|
+
</span>
|
|
45
|
+
</>
|
|
46
|
+
)}
|
|
47
|
+
</For>
|
|
48
|
+
</div>
|
|
49
|
+
</Show>
|
|
50
|
+
<h1 class="mt-1 text-2xl font-bold tracking-tight">{props.title}</h1>
|
|
51
|
+
<Show when={props.subtitle}>
|
|
52
|
+
<p class="mt-0.5 text-[13px] text-muted-foreground">{props.subtitle}</p>
|
|
53
|
+
</Show>
|
|
54
|
+
</div>
|
|
55
|
+
<Show when={props.actions}>
|
|
56
|
+
<div class="flex items-center gap-2">{props.actions}</div>
|
|
57
|
+
</Show>
|
|
58
|
+
</div>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Prev/next pager with a "Page X of Y" label and an optional left-aligned
|
|
2
|
+
// summary slot (e.g. "1,234 registros · 1–50"). Client-side; the parent owns
|
|
3
|
+
// the page signal and slices its own data.
|
|
4
|
+
import { ChevronLeft, ChevronRight } from 'lucide-solid'
|
|
5
|
+
import type { JSX } from 'solid-js'
|
|
6
|
+
|
|
7
|
+
interface PaginationProps {
|
|
8
|
+
/** Current page, 1-indexed. */
|
|
9
|
+
page: number
|
|
10
|
+
totalPages: number
|
|
11
|
+
onChange: (page: number) => void
|
|
12
|
+
/** Left-aligned slot, e.g. a row-count summary like "1,234 registros · 1–50". */
|
|
13
|
+
summary?: JSX.Element
|
|
14
|
+
/** Override default English labels/text for i18n. */
|
|
15
|
+
labels?: {
|
|
16
|
+
previous?: string
|
|
17
|
+
next?: string
|
|
18
|
+
/** Formats the center label; default is `` `Page ${page} of ${total}` ``. */
|
|
19
|
+
page?: (page: number, total: number) => string
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Prev/next pager with a "Page X of Y" label and an optional summary slot.
|
|
25
|
+
* Purely client-side and controlled — the parent owns the `page` signal and
|
|
26
|
+
* is responsible for slicing its own data to match.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```tsx
|
|
30
|
+
* <Pagination page={page()} totalPages={totalPages()} onChange={setPage} />
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export function Pagination(props: PaginationProps): JSX.Element {
|
|
34
|
+
return (
|
|
35
|
+
<div class="flex flex-col gap-3 border-t border-border p-3 sm:flex-row sm:items-center sm:justify-between">
|
|
36
|
+
<div class="text-[12px] text-muted-foreground">{props.summary}</div>
|
|
37
|
+
<div class="flex items-center gap-1">
|
|
38
|
+
<button
|
|
39
|
+
type="button"
|
|
40
|
+
class="grid h-8 w-8 place-items-center rounded-lg border border-input transition hover:bg-muted disabled:pointer-events-none disabled:opacity-40"
|
|
41
|
+
disabled={props.page <= 1}
|
|
42
|
+
aria-label={props.labels?.previous ?? 'Previous page'}
|
|
43
|
+
onClick={() => props.onChange(props.page - 1)}
|
|
44
|
+
>
|
|
45
|
+
<ChevronLeft class="h-4 w-4" />
|
|
46
|
+
</button>
|
|
47
|
+
<span class="px-2 font-mono text-[12px] tabular-nums">
|
|
48
|
+
{(props.labels?.page ?? ((p, t) => `Page ${p} of ${t}`))(props.page, props.totalPages)}
|
|
49
|
+
</span>
|
|
50
|
+
<button
|
|
51
|
+
type="button"
|
|
52
|
+
class="grid h-8 w-8 place-items-center rounded-lg border border-input transition hover:bg-muted disabled:pointer-events-none disabled:opacity-40"
|
|
53
|
+
disabled={props.page >= props.totalPages}
|
|
54
|
+
aria-label={props.labels?.next ?? 'Next page'}
|
|
55
|
+
onClick={() => props.onChange(props.page + 1)}
|
|
56
|
+
>
|
|
57
|
+
<ChevronRight class="h-4 w-4" />
|
|
58
|
+
</button>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Parallax — moves its children at a fraction of scroll speed for depth,
|
|
2
|
+
// driven by Motion's `scroll` progress callback (0..1) as the element passes
|
|
3
|
+
// through the viewport. No-op (static) under reduced motion.
|
|
4
|
+
import { onCleanup, onMount, type JSX } from 'solid-js'
|
|
5
|
+
|
|
6
|
+
import { cn } from '../lib/cn'
|
|
7
|
+
import { motionReduced, scroll } from '../lib/motion'
|
|
8
|
+
|
|
9
|
+
export interface ParallaxProps {
|
|
10
|
+
children: JSX.Element
|
|
11
|
+
/** Parallax strength; positive moves slower/opposite. Pixels of travel across the scroll range. @default 80 */
|
|
12
|
+
amount?: number
|
|
13
|
+
class?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Wraps `children` in an element that drifts vertically as the page scrolls,
|
|
18
|
+
* creating a depth effect. Tracks scroll progress of the element itself
|
|
19
|
+
* against the viewport via Motion's `scroll`; respects `prefers-reduced-motion`
|
|
20
|
+
* (renders static).
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```tsx
|
|
24
|
+
* <Parallax amount={120}>
|
|
25
|
+
* <img src="/hero.png" alt="" />
|
|
26
|
+
* </Parallax>
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export function Parallax(props: ParallaxProps): JSX.Element {
|
|
30
|
+
let root!: HTMLDivElement
|
|
31
|
+
|
|
32
|
+
onMount(() => {
|
|
33
|
+
if (motionReduced()) return
|
|
34
|
+
|
|
35
|
+
const amount = props.amount ?? 80
|
|
36
|
+
const stop = scroll(
|
|
37
|
+
(progress: number) => {
|
|
38
|
+
root.style.transform = `translateY(${(progress - 0.5) * -2 * amount}px)`
|
|
39
|
+
},
|
|
40
|
+
{ target: root },
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
onCleanup(() => stop())
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<div ref={root} class={cn('will-change-transform', props.class)}>
|
|
48
|
+
{props.children}
|
|
49
|
+
</div>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Click-triggered floating panel on Kobalte's Popover primitive.
|
|
2
|
+
import { Popover as KPopover } from '@kobalte/core/popover'
|
|
3
|
+
import type { JSX, ParentProps } from 'solid-js'
|
|
4
|
+
|
|
5
|
+
import { cn } from '../lib/cn'
|
|
6
|
+
|
|
7
|
+
interface PopoverProps extends ParentProps {
|
|
8
|
+
/** Element that opens the popover on click; wrapped in an inline-flex trigger. */
|
|
9
|
+
trigger: JSX.Element
|
|
10
|
+
class?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Click-triggered floating panel, built on Kobalte's `Popover` primitive
|
|
15
|
+
* (handles positioning, focus, dismiss, and portaling). Use for contextual
|
|
16
|
+
* content anchored to a trigger element, such as a filter panel or menu.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```tsx
|
|
20
|
+
* <Popover trigger={<Button>Filters</Button>}>
|
|
21
|
+
* <p>Panel content</p>
|
|
22
|
+
* </Popover>
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export function Popover(props: PopoverProps): JSX.Element {
|
|
26
|
+
return (
|
|
27
|
+
<KPopover>
|
|
28
|
+
<KPopover.Trigger class="inline-flex">{props.trigger}</KPopover.Trigger>
|
|
29
|
+
<KPopover.Portal>
|
|
30
|
+
<KPopover.Content
|
|
31
|
+
class={cn(
|
|
32
|
+
'bg-glass z-50 rounded-lg border border-border p-4 text-card-foreground shadow-lg',
|
|
33
|
+
props.class,
|
|
34
|
+
)}
|
|
35
|
+
>
|
|
36
|
+
<KPopover.Arrow />
|
|
37
|
+
{props.children}
|
|
38
|
+
</KPopover.Content>
|
|
39
|
+
</KPopover.Portal>
|
|
40
|
+
</KPopover>
|
|
41
|
+
)
|
|
42
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Renders children into a detached DOM node (default: <body>), escaping overflow
|
|
2
|
+
// and stacking-context traps. A thin wrapper over Solid's own Portal so it's
|
|
3
|
+
// available alongside the rest of the library.
|
|
4
|
+
import type { JSX } from 'solid-js'
|
|
5
|
+
import { Portal as SolidPortal } from 'solid-js/web'
|
|
6
|
+
|
|
7
|
+
/** Props for {@link Portal}. */
|
|
8
|
+
export interface PortalProps {
|
|
9
|
+
/** Where to mount. Defaults to `document.body`. */
|
|
10
|
+
mount?: Node
|
|
11
|
+
/** Use a Shadow DOM root for the portal container. */
|
|
12
|
+
useShadow?: boolean
|
|
13
|
+
children: JSX.Element
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Renders its children into a different place in the DOM (by default
|
|
18
|
+
* `document.body`), so overlays aren't clipped by an ancestor's `overflow` or
|
|
19
|
+
* transformed stacking context. Built on Solid's `Portal`.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```tsx
|
|
23
|
+
* <Portal>
|
|
24
|
+
* <div class="fixed inset-0 grid place-items-center">Floating layer</div>
|
|
25
|
+
* </Portal>
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export function Portal(props: PortalProps): JSX.Element {
|
|
29
|
+
return (
|
|
30
|
+
<SolidPortal mount={props.mount} useShadow={props.useShadow}>
|
|
31
|
+
{props.children}
|
|
32
|
+
</SolidPortal>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Determinate progress bar on Kobalte's Progress primitive.
|
|
2
|
+
import { Progress as KProgress } from '@kobalte/core/progress'
|
|
3
|
+
import type { JSX } from 'solid-js'
|
|
4
|
+
import { Show } from 'solid-js'
|
|
5
|
+
|
|
6
|
+
import { cn } from '../lib/cn'
|
|
7
|
+
|
|
8
|
+
interface ProgressProps {
|
|
9
|
+
value: number
|
|
10
|
+
/** Upper bound of `value`. Default: 100. */
|
|
11
|
+
max?: number
|
|
12
|
+
/** Label shown above the bar, paired with an auto-generated value label (e.g. "50%"). */
|
|
13
|
+
label?: string
|
|
14
|
+
class?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Determinate progress bar, built on Kobalte's `Progress` primitive. Use to
|
|
19
|
+
* show completion of a known-length task (upload, multi-step form, etc.).
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```tsx
|
|
23
|
+
* <Progress value={uploaded()} max={totalBytes()} label="Uploading" />
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export function Progress(props: ProgressProps): JSX.Element {
|
|
27
|
+
// Kobalte's Progress.Fill has no intrinsic width, so a bare block div fills the
|
|
28
|
+
// whole track (every bar reads 100%). Set the width from value/max ourselves.
|
|
29
|
+
const percent = () => {
|
|
30
|
+
const max = props.max ?? 100
|
|
31
|
+
if (max <= 0) return 0
|
|
32
|
+
return Math.max(0, Math.min(100, (props.value / max) * 100))
|
|
33
|
+
}
|
|
34
|
+
return (
|
|
35
|
+
<KProgress
|
|
36
|
+
value={props.value}
|
|
37
|
+
minValue={0}
|
|
38
|
+
maxValue={props.max ?? 100}
|
|
39
|
+
class={cn('flex flex-col gap-1.5', props.class)}
|
|
40
|
+
>
|
|
41
|
+
<Show when={props.label}>
|
|
42
|
+
<div class="flex items-center justify-between text-sm text-foreground">
|
|
43
|
+
<KProgress.Label>{props.label}</KProgress.Label>
|
|
44
|
+
<KProgress.ValueLabel class="text-muted-foreground" />
|
|
45
|
+
</div>
|
|
46
|
+
</Show>
|
|
47
|
+
<KProgress.Track class="h-2 overflow-hidden rounded-full bg-muted">
|
|
48
|
+
<KProgress.Fill
|
|
49
|
+
class="h-full rounded-full bg-primary transition-all duration-500"
|
|
50
|
+
style={{ width: `${percent()}%` }}
|
|
51
|
+
/>
|
|
52
|
+
</KProgress.Track>
|
|
53
|
+
</KProgress>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Accessible single-choice group on Kobalte's RadioGroup primitive.
|
|
2
|
+
import { RadioGroup as KRadioGroup } from '@kobalte/core/radio-group'
|
|
3
|
+
import type { JSX } from 'solid-js'
|
|
4
|
+
import { For, Show } from 'solid-js'
|
|
5
|
+
|
|
6
|
+
import { cn } from '../lib/cn'
|
|
7
|
+
|
|
8
|
+
/** A single selectable choice within a {@link RadioGroup}. */
|
|
9
|
+
export interface RadioOption {
|
|
10
|
+
value: string
|
|
11
|
+
label: string
|
|
12
|
+
disabled?: boolean
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface RadioGroupProps {
|
|
16
|
+
value: string
|
|
17
|
+
onChange: (value: string) => void
|
|
18
|
+
options: RadioOption[]
|
|
19
|
+
/** Group label rendered above the options. */
|
|
20
|
+
label?: string
|
|
21
|
+
class?: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Accessible single-choice group, built on Kobalte's `RadioGroup` primitive
|
|
26
|
+
* (arrow-key navigation, ARIA roles). Use when all options should stay
|
|
27
|
+
* visible at once, unlike {@link Select} which hides options until opened.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```tsx
|
|
31
|
+
* <RadioGroup
|
|
32
|
+
* label="Plan"
|
|
33
|
+
* value={plan()}
|
|
34
|
+
* onChange={setPlan}
|
|
35
|
+
* options={[
|
|
36
|
+
* { value: 'free', label: 'Free' },
|
|
37
|
+
* { value: 'pro', label: 'Pro' },
|
|
38
|
+
* ]}
|
|
39
|
+
* />
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export function RadioGroup(props: RadioGroupProps): JSX.Element {
|
|
43
|
+
return (
|
|
44
|
+
<KRadioGroup value={props.value} onChange={props.onChange} class={cn('flex flex-col gap-2', props.class)}>
|
|
45
|
+
<Show when={props.label}>
|
|
46
|
+
<KRadioGroup.Label class="text-sm font-medium text-foreground">{props.label}</KRadioGroup.Label>
|
|
47
|
+
</Show>
|
|
48
|
+
<For each={props.options}>
|
|
49
|
+
{(option) => (
|
|
50
|
+
<KRadioGroup.Item
|
|
51
|
+
value={option.value}
|
|
52
|
+
disabled={option.disabled}
|
|
53
|
+
class="flex items-center gap-2 text-sm text-foreground data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50"
|
|
54
|
+
>
|
|
55
|
+
<KRadioGroup.ItemInput />
|
|
56
|
+
<KRadioGroup.ItemControl class="grid h-4 w-4 place-items-center rounded-full border border-input transition-colors data-[checked]:border-primary">
|
|
57
|
+
<KRadioGroup.ItemIndicator class="h-2 w-2 rounded-full bg-primary" />
|
|
58
|
+
</KRadioGroup.ItemControl>
|
|
59
|
+
<KRadioGroup.ItemLabel>{option.label}</KRadioGroup.ItemLabel>
|
|
60
|
+
</KRadioGroup.Item>
|
|
61
|
+
)}
|
|
62
|
+
</For>
|
|
63
|
+
</KRadioGroup>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// Star rating input. Filled/empty stars are driven by the current value (or a
|
|
2
|
+
// transient hover preview); interactive mode renders each star as a keyboard-
|
|
3
|
+
// accessible button, readonly mode renders a plain, labelled row of icons.
|
|
4
|
+
import { Star } from 'lucide-solid'
|
|
5
|
+
import type { JSX } from 'solid-js'
|
|
6
|
+
import { createSignal, For, Show } from 'solid-js'
|
|
7
|
+
|
|
8
|
+
import { cn } from '../lib/cn'
|
|
9
|
+
|
|
10
|
+
export interface RatingProps {
|
|
11
|
+
/** Currently selected rating, from 0 to `max`. */
|
|
12
|
+
value: number
|
|
13
|
+
/** Called with the new rating when a star is clicked or set via keyboard. */
|
|
14
|
+
onChange?: (value: number) => void
|
|
15
|
+
/** Number of stars to render. Default: 5. */
|
|
16
|
+
max?: number
|
|
17
|
+
/** Render a non-interactive, display-only rating. Default: false. */
|
|
18
|
+
readonly?: boolean
|
|
19
|
+
class?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Star rating control. In interactive mode each star is a button: click or use
|
|
24
|
+
* the Left/Right arrow keys to set the rating, and hovering previews it. In
|
|
25
|
+
* `readonly` mode it renders as a static, labelled row of stars.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```tsx
|
|
29
|
+
* <Rating value={rating()} onChange={setRating} max={5} />
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export function Rating(props: RatingProps): JSX.Element {
|
|
33
|
+
const [hover, setHover] = createSignal(0)
|
|
34
|
+
const max = () => props.max ?? 5
|
|
35
|
+
const stars = () => Array.from({ length: max() }, (_, i) => i + 1)
|
|
36
|
+
|
|
37
|
+
// The value used to paint stars: hover preview takes precedence when active.
|
|
38
|
+
const active = () => (hover() > 0 ? hover() : props.value)
|
|
39
|
+
|
|
40
|
+
const set = (n: number) => {
|
|
41
|
+
if (props.readonly) return
|
|
42
|
+
props.onChange?.(n)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const starClass = (index: number) =>
|
|
46
|
+
cn(
|
|
47
|
+
'h-5 w-5 transition-colors',
|
|
48
|
+
index <= active() ? 'fill-primary text-primary' : 'fill-transparent text-muted-foreground',
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<Show
|
|
53
|
+
when={!props.readonly}
|
|
54
|
+
fallback={
|
|
55
|
+
<div
|
|
56
|
+
class={cn('inline-flex items-center gap-1', props.class)}
|
|
57
|
+
role="img"
|
|
58
|
+
aria-label={`Rating ${props.value} of ${max()}`}
|
|
59
|
+
>
|
|
60
|
+
<For each={stars()}>{(n) => <Star class={starClass(n)} aria-hidden="true" />}</For>
|
|
61
|
+
</div>
|
|
62
|
+
}
|
|
63
|
+
>
|
|
64
|
+
<div
|
|
65
|
+
class={cn('inline-flex items-center gap-1', props.class)}
|
|
66
|
+
role="radiogroup"
|
|
67
|
+
onMouseLeave={() => setHover(0)}
|
|
68
|
+
>
|
|
69
|
+
<For each={stars()}>
|
|
70
|
+
{(n) => (
|
|
71
|
+
<button
|
|
72
|
+
type="button"
|
|
73
|
+
role="radio"
|
|
74
|
+
aria-checked={props.value === n}
|
|
75
|
+
aria-label={`Rate ${n} of ${max()}`}
|
|
76
|
+
class="rounded outline-none focus-visible:ring-2 focus-visible:ring-ring/40"
|
|
77
|
+
onMouseEnter={() => setHover(n)}
|
|
78
|
+
onFocus={() => setHover(n)}
|
|
79
|
+
onBlur={() => setHover(0)}
|
|
80
|
+
onClick={() => set(n)}
|
|
81
|
+
onKeyDown={(e) => {
|
|
82
|
+
if (e.key === 'ArrowRight' || e.key === 'ArrowUp') {
|
|
83
|
+
e.preventDefault()
|
|
84
|
+
set(Math.min(props.value + 1, max()))
|
|
85
|
+
} else if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') {
|
|
86
|
+
e.preventDefault()
|
|
87
|
+
set(Math.max(props.value - 1, 0))
|
|
88
|
+
}
|
|
89
|
+
}}
|
|
90
|
+
>
|
|
91
|
+
<Star class={starClass(n)} aria-hidden="true" />
|
|
92
|
+
</button>
|
|
93
|
+
)}
|
|
94
|
+
</For>
|
|
95
|
+
</div>
|
|
96
|
+
</Show>
|
|
97
|
+
)
|
|
98
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Full-status result screen: a centered vertical stack for terminal states like
|
|
2
|
+
// success confirmations, errors, or HTTP 404/500 pages. A large status icon (or
|
|
3
|
+
// the big HTTP number) sits in a tinted circle above the title/description; the
|
|
4
|
+
// fixed semantic status colors mirror Alert (emerald/amber/rose) while the rest
|
|
5
|
+
// stays theme-agnostic via semantic tokens.
|
|
6
|
+
import { CircleCheck, CircleX, Compass, Info, ServerCrash, TriangleAlert } from 'lucide-solid'
|
|
7
|
+
import type { Component, JSX } from 'solid-js'
|
|
8
|
+
import { Show } from 'solid-js'
|
|
9
|
+
import { Dynamic } from 'solid-js/web'
|
|
10
|
+
|
|
11
|
+
import { cn } from '../lib/cn'
|
|
12
|
+
|
|
13
|
+
/** Semantic status of a {@link Result}; drives the icon, tint, and optional HTTP number. */
|
|
14
|
+
export type ResultStatus = 'success' | 'error' | 'info' | 'warning' | '404' | '500'
|
|
15
|
+
|
|
16
|
+
export interface ResultProps {
|
|
17
|
+
/** Result status; drives the icon and color. Defaults to `'info'`. */
|
|
18
|
+
status?: ResultStatus
|
|
19
|
+
/** Bold headline describing the outcome. */
|
|
20
|
+
title: string
|
|
21
|
+
/** Optional supporting line under the title. */
|
|
22
|
+
description?: string
|
|
23
|
+
/** Optional action slot (e.g. Buttons) rendered below the text. */
|
|
24
|
+
actions?: JSX.Element
|
|
25
|
+
class?: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const STATUS: Record<
|
|
29
|
+
ResultStatus,
|
|
30
|
+
{ Icon: Component<{ class?: string }>; icon: string; tint: string; code?: string }
|
|
31
|
+
> = {
|
|
32
|
+
success: { Icon: CircleCheck, icon: 'text-emerald-500', tint: 'bg-emerald-500/10' },
|
|
33
|
+
error: { Icon: CircleX, icon: 'text-destructive', tint: 'bg-destructive/10' },
|
|
34
|
+
warning: { Icon: TriangleAlert, icon: 'text-amber-500', tint: 'bg-amber-500/10' },
|
|
35
|
+
info: { Icon: Info, icon: 'text-primary', tint: 'bg-primary/10' },
|
|
36
|
+
'404': { Icon: Compass, icon: 'text-muted-foreground', tint: 'bg-muted', code: '404' },
|
|
37
|
+
'500': { Icon: ServerCrash, icon: 'text-destructive', tint: 'bg-destructive/10', code: '500' },
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Full-status result screen — a large status icon in a tinted circle, a title,
|
|
42
|
+
* optional description, and an optional actions slot — for terminal states such
|
|
43
|
+
* as success/error confirmations or HTTP 404/500 pages. For `'404'`/`'500'` the
|
|
44
|
+
* big HTTP number is shown above the icon.
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```tsx
|
|
48
|
+
* <Result
|
|
49
|
+
* status="success"
|
|
50
|
+
* title="Payment complete"
|
|
51
|
+
* description="A receipt has been sent to your email."
|
|
52
|
+
* actions={<Button>Back to dashboard</Button>}
|
|
53
|
+
* />
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
export function Result(props: ResultProps): JSX.Element {
|
|
57
|
+
const status = () => STATUS[props.status ?? 'info']
|
|
58
|
+
return (
|
|
59
|
+
<div class={cn('flex flex-col items-center py-12 text-center', props.class)}>
|
|
60
|
+
<Show when={status().code}>
|
|
61
|
+
<p class="text-5xl font-bold text-muted-foreground">{status().code}</p>
|
|
62
|
+
</Show>
|
|
63
|
+
<div class={cn('grid h-16 w-16 place-items-center rounded-full', status().tint)}>
|
|
64
|
+
<Dynamic component={status().Icon} class={cn('h-8 w-8', status().icon)} />
|
|
65
|
+
</div>
|
|
66
|
+
<p class="mt-4 text-xl font-bold text-foreground">{props.title}</p>
|
|
67
|
+
<Show when={props.description}>
|
|
68
|
+
<p class="mt-2 max-w-md text-muted-foreground">{props.description}</p>
|
|
69
|
+
</Show>
|
|
70
|
+
<Show when={props.actions}>
|
|
71
|
+
<div class="mt-6">{props.actions}</div>
|
|
72
|
+
</Show>
|
|
73
|
+
</div>
|
|
74
|
+
)
|
|
75
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Circular (radial) progress ring rendered as an SVG with a centered label.
|
|
2
|
+
import type { JSX } from 'solid-js'
|
|
3
|
+
import { Show } from 'solid-js'
|
|
4
|
+
|
|
5
|
+
import { cn } from '../lib/cn'
|
|
6
|
+
|
|
7
|
+
export interface RingProgressProps {
|
|
8
|
+
value: number
|
|
9
|
+
/** Outer diameter of the ring in px. @default 96 */
|
|
10
|
+
size?: number
|
|
11
|
+
/** Stroke width of the track and arc in px. @default 8 */
|
|
12
|
+
thickness?: number
|
|
13
|
+
/** Content centered over the ring. Defaults to the rounded percent. */
|
|
14
|
+
label?: JSX.Element
|
|
15
|
+
class?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Circular progress indicator drawn with two concentric SVG circles: a muted
|
|
20
|
+
* track and a primary arc whose length is derived from `value` (0–100). Use for
|
|
21
|
+
* compact, radial completion feedback where a linear bar would be too wide.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```tsx
|
|
25
|
+
* <RingProgress value={72} />
|
|
26
|
+
* <RingProgress value={40} size={128} thickness={12} label={<span>4/10</span>} />
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export function RingProgress(props: RingProgressProps): JSX.Element {
|
|
30
|
+
const size = () => props.size ?? 96
|
|
31
|
+
const thickness = () => props.thickness ?? 8
|
|
32
|
+
const clamped = () => Math.min(100, Math.max(0, props.value))
|
|
33
|
+
const radius = () => (size() - thickness()) / 2
|
|
34
|
+
const circumference = () => 2 * Math.PI * radius()
|
|
35
|
+
const offset = () => circumference() * (1 - clamped() / 100)
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div
|
|
39
|
+
role="progressbar"
|
|
40
|
+
aria-valuenow={clamped()}
|
|
41
|
+
aria-valuemin={0}
|
|
42
|
+
aria-valuemax={100}
|
|
43
|
+
class={cn('relative inline-flex items-center justify-center', props.class)}
|
|
44
|
+
style={{ width: `${size()}px`, height: `${size()}px` }}
|
|
45
|
+
>
|
|
46
|
+
<svg width={size()} height={size()} viewBox={`0 0 ${size()} ${size()}`} class="-rotate-90">
|
|
47
|
+
<circle
|
|
48
|
+
cx={size() / 2}
|
|
49
|
+
cy={size() / 2}
|
|
50
|
+
r={radius()}
|
|
51
|
+
fill="none"
|
|
52
|
+
stroke="hsl(var(--muted))"
|
|
53
|
+
stroke-width={thickness()}
|
|
54
|
+
/>
|
|
55
|
+
<circle
|
|
56
|
+
cx={size() / 2}
|
|
57
|
+
cy={size() / 2}
|
|
58
|
+
r={radius()}
|
|
59
|
+
fill="none"
|
|
60
|
+
stroke="hsl(var(--primary))"
|
|
61
|
+
stroke-width={thickness()}
|
|
62
|
+
stroke-linecap="round"
|
|
63
|
+
stroke-dasharray={String(circumference())}
|
|
64
|
+
stroke-dashoffset={String(offset())}
|
|
65
|
+
class="transition-all"
|
|
66
|
+
/>
|
|
67
|
+
</svg>
|
|
68
|
+
<div class="absolute inset-0 flex items-center justify-center text-sm font-medium text-foreground">
|
|
69
|
+
<Show when={props.label} fallback={`${Math.round(clamped())}%`}>
|
|
70
|
+
{props.label}
|
|
71
|
+
</Show>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
)
|
|
75
|
+
}
|