@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.
Files changed (151) hide show
  1. package/README.md +80 -37
  2. package/dist/elements.css +79 -9
  3. package/dist/full.css +3112 -0
  4. package/dist/index.d.ts +13 -1
  5. package/dist/index.js +3392 -2537
  6. package/dist/lib/flyToCart.d.ts +25 -0
  7. package/dist/ui/Curtain.d.ts +44 -0
  8. package/dist/ui/Expandable.d.ts +34 -0
  9. package/dist/ui/FillText.d.ts +18 -0
  10. package/dist/ui/HoldToConfirm.d.ts +24 -0
  11. package/dist/ui/LoadingDots.d.ts +20 -0
  12. package/dist/ui/MultiStateBadge.d.ts +23 -0
  13. package/dist/ui/NotificationStack.d.ts +32 -0
  14. package/dist/ui/NowPlaying.d.ts +21 -0
  15. package/dist/ui/Parallax.d.ts +21 -0
  16. package/dist/ui/ScrambleText.d.ts +22 -0
  17. package/dist/ui/TextReveal.d.ts +23 -0
  18. package/package.json +8 -1
  19. package/src/charts/BarChart.tsx +55 -0
  20. package/src/charts/DonutChart.tsx +127 -0
  21. package/src/charts/Sparkline.tsx +101 -0
  22. package/src/charts/index.ts +5 -0
  23. package/src/commerce/CartLine.tsx +84 -0
  24. package/src/commerce/CartSummary.tsx +70 -0
  25. package/src/commerce/FilterGroup.tsx +70 -0
  26. package/src/commerce/PriceTag.tsx +62 -0
  27. package/src/commerce/ProductCard.tsx +121 -0
  28. package/src/commerce/ProductGrid.tsx +27 -0
  29. package/src/commerce/QuantityStepper.tsx +59 -0
  30. package/src/commerce/index.ts +10 -0
  31. package/src/elements.tsx +167 -0
  32. package/src/index.ts +186 -0
  33. package/src/layout/AppShell.tsx +102 -0
  34. package/src/layout/ChristmasBackground.tsx +177 -0
  35. package/src/layout/EffectsToggle.tsx +41 -0
  36. package/src/layout/NavGroup.tsx +45 -0
  37. package/src/layout/SnowScenery.tsx +76 -0
  38. package/src/layout/SpaceBackground.tsx +459 -0
  39. package/src/layout/ThemeToggle.tsx +40 -0
  40. package/src/layout/ThemedScenery.tsx +179 -0
  41. package/src/layout/sceneEffects.ts +49 -0
  42. package/src/lib/cn.ts +17 -0
  43. package/src/lib/effects.ts +71 -0
  44. package/src/lib/flyToCart.ts +103 -0
  45. package/src/lib/media.ts +27 -0
  46. package/src/lib/motion.ts +191 -0
  47. package/src/lib/theme.ts +113 -0
  48. package/src/lib/virtual.ts +33 -0
  49. package/src/styles/space.css +510 -0
  50. package/src/styles/tokens.css +213 -0
  51. package/src/themes/index.ts +102 -0
  52. package/src/themes/palettes.ts +368 -0
  53. package/src/ui/Accordion.tsx +65 -0
  54. package/src/ui/Affix.tsx +72 -0
  55. package/src/ui/Alert.tsx +55 -0
  56. package/src/ui/AlertDialog.tsx +58 -0
  57. package/src/ui/Anchor.tsx +100 -0
  58. package/src/ui/Avatar.tsx +34 -0
  59. package/src/ui/AvatarGroup.tsx +55 -0
  60. package/src/ui/BackToTop.tsx +51 -0
  61. package/src/ui/Badge.tsx +46 -0
  62. package/src/ui/BottomNavigation.tsx +65 -0
  63. package/src/ui/Breadcrumb.tsx +64 -0
  64. package/src/ui/Button.tsx +49 -0
  65. package/src/ui/Calendar.tsx +40 -0
  66. package/src/ui/CalendarHeatmap.tsx +180 -0
  67. package/src/ui/Card.tsx +84 -0
  68. package/src/ui/Carousel.tsx +121 -0
  69. package/src/ui/Cascader.tsx +158 -0
  70. package/src/ui/Checkbox.tsx +35 -0
  71. package/src/ui/Clock.tsx +221 -0
  72. package/src/ui/Collapse.tsx +48 -0
  73. package/src/ui/ColorPicker.tsx +89 -0
  74. package/src/ui/Combobox.tsx +67 -0
  75. package/src/ui/Command.tsx +158 -0
  76. package/src/ui/Comment.tsx +91 -0
  77. package/src/ui/ContextMenu.tsx +58 -0
  78. package/src/ui/Countdown.tsx +123 -0
  79. package/src/ui/Curtain.tsx +348 -0
  80. package/src/ui/DataGrid.tsx +177 -0
  81. package/src/ui/DateField.tsx +177 -0
  82. package/src/ui/DateRangePicker.tsx +204 -0
  83. package/src/ui/DateTimeField.tsx +68 -0
  84. package/src/ui/Descriptions.tsx +56 -0
  85. package/src/ui/Drawer.tsx +72 -0
  86. package/src/ui/Dropdown.tsx +71 -0
  87. package/src/ui/Dropzone.tsx +88 -0
  88. package/src/ui/Empty.tsx +53 -0
  89. package/src/ui/Expandable.tsx +229 -0
  90. package/src/ui/FileUpload.tsx +198 -0
  91. package/src/ui/FillText.tsx +63 -0
  92. package/src/ui/FloatingActionButton.tsx +48 -0
  93. package/src/ui/Form.tsx +108 -0
  94. package/src/ui/Highlight.tsx +53 -0
  95. package/src/ui/HoldToConfirm.tsx +141 -0
  96. package/src/ui/HoverCard.tsx +42 -0
  97. package/src/ui/Image.tsx +66 -0
  98. package/src/ui/Input.tsx +38 -0
  99. package/src/ui/Kbd.tsx +25 -0
  100. package/src/ui/List.tsx +71 -0
  101. package/src/ui/LoadingDots.tsx +75 -0
  102. package/src/ui/Marquee.tsx +48 -0
  103. package/src/ui/Mentions.tsx +170 -0
  104. package/src/ui/Meter.tsx +57 -0
  105. package/src/ui/Modal.tsx +106 -0
  106. package/src/ui/MultiSelect.tsx +236 -0
  107. package/src/ui/MultiStateBadge.tsx +93 -0
  108. package/src/ui/NotificationCenter.tsx +103 -0
  109. package/src/ui/NotificationStack.tsx +104 -0
  110. package/src/ui/NowPlaying.tsx +100 -0
  111. package/src/ui/NumberInput.tsx +48 -0
  112. package/src/ui/PageHeader.tsx +60 -0
  113. package/src/ui/Pagination.tsx +62 -0
  114. package/src/ui/Parallax.tsx +51 -0
  115. package/src/ui/Popover.tsx +42 -0
  116. package/src/ui/Portal.tsx +34 -0
  117. package/src/ui/Progress.tsx +55 -0
  118. package/src/ui/RadioGroup.tsx +65 -0
  119. package/src/ui/Rating.tsx +98 -0
  120. package/src/ui/Result.tsx +75 -0
  121. package/src/ui/RingProgress.tsx +75 -0
  122. package/src/ui/ScrambleText.tsx +95 -0
  123. package/src/ui/SegmentedControl.tsx +61 -0
  124. package/src/ui/Select.tsx +55 -0
  125. package/src/ui/Separator.tsx +39 -0
  126. package/src/ui/Skeleton.tsx +23 -0
  127. package/src/ui/Slider.tsx +57 -0
  128. package/src/ui/Sortable.tsx +174 -0
  129. package/src/ui/SpeedDial.tsx +85 -0
  130. package/src/ui/Spinner.tsx +33 -0
  131. package/src/ui/Splitter.tsx +115 -0
  132. package/src/ui/Stat.tsx +74 -0
  133. package/src/ui/Stepper.tsx +134 -0
  134. package/src/ui/Switch.tsx +44 -0
  135. package/src/ui/Table.tsx +128 -0
  136. package/src/ui/Tabs.tsx +72 -0
  137. package/src/ui/TagInput.tsx +85 -0
  138. package/src/ui/TextReveal.tsx +93 -0
  139. package/src/ui/Textarea.tsx +38 -0
  140. package/src/ui/TimeField.tsx +298 -0
  141. package/src/ui/Timeline.tsx +89 -0
  142. package/src/ui/Toast.tsx +68 -0
  143. package/src/ui/Toggle.tsx +41 -0
  144. package/src/ui/ToggleGroup.tsx +59 -0
  145. package/src/ui/Tooltip.tsx +42 -0
  146. package/src/ui/Tour.tsx +197 -0
  147. package/src/ui/Transfer.tsx +112 -0
  148. package/src/ui/Tree.tsx +100 -0
  149. package/src/ui/TreeSelect.tsx +151 -0
  150. package/src/ui/VirtualList.tsx +81 -0
  151. 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
+ }