@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.
Files changed (126) hide show
  1. package/README.md +36 -26
  2. package/dist/full.css +3042 -0
  3. package/dist/index.d.ts +1 -1
  4. package/dist/index.js +1 -1
  5. package/package.json +8 -1
  6. package/src/charts/BarChart.tsx +55 -0
  7. package/src/charts/DonutChart.tsx +127 -0
  8. package/src/charts/Sparkline.tsx +101 -0
  9. package/src/charts/index.ts +5 -0
  10. package/src/commerce/CartLine.tsx +84 -0
  11. package/src/commerce/CartSummary.tsx +70 -0
  12. package/src/commerce/FilterGroup.tsx +70 -0
  13. package/src/commerce/PriceTag.tsx +62 -0
  14. package/src/commerce/ProductCard.tsx +121 -0
  15. package/src/commerce/ProductGrid.tsx +27 -0
  16. package/src/commerce/QuantityStepper.tsx +59 -0
  17. package/src/commerce/index.ts +10 -0
  18. package/src/elements.tsx +167 -0
  19. package/src/index.ts +166 -0
  20. package/src/layout/AppShell.tsx +102 -0
  21. package/src/layout/ChristmasBackground.tsx +177 -0
  22. package/src/layout/EffectsToggle.tsx +41 -0
  23. package/src/layout/NavGroup.tsx +45 -0
  24. package/src/layout/SnowScenery.tsx +76 -0
  25. package/src/layout/SpaceBackground.tsx +459 -0
  26. package/src/layout/ThemeToggle.tsx +40 -0
  27. package/src/layout/ThemedScenery.tsx +179 -0
  28. package/src/layout/sceneEffects.ts +49 -0
  29. package/src/lib/cn.ts +17 -0
  30. package/src/lib/effects.ts +71 -0
  31. package/src/lib/media.ts +27 -0
  32. package/src/lib/motion.ts +191 -0
  33. package/src/lib/theme.ts +113 -0
  34. package/src/lib/virtual.ts +33 -0
  35. package/src/styles/space.css +510 -0
  36. package/src/styles/tokens.css +213 -0
  37. package/src/themes/index.ts +102 -0
  38. package/src/themes/palettes.ts +368 -0
  39. package/src/ui/Accordion.tsx +65 -0
  40. package/src/ui/Affix.tsx +72 -0
  41. package/src/ui/Alert.tsx +55 -0
  42. package/src/ui/AlertDialog.tsx +58 -0
  43. package/src/ui/Anchor.tsx +100 -0
  44. package/src/ui/Avatar.tsx +34 -0
  45. package/src/ui/AvatarGroup.tsx +55 -0
  46. package/src/ui/BackToTop.tsx +51 -0
  47. package/src/ui/Badge.tsx +46 -0
  48. package/src/ui/BottomNavigation.tsx +65 -0
  49. package/src/ui/Breadcrumb.tsx +64 -0
  50. package/src/ui/Button.tsx +49 -0
  51. package/src/ui/Calendar.tsx +40 -0
  52. package/src/ui/CalendarHeatmap.tsx +180 -0
  53. package/src/ui/Card.tsx +84 -0
  54. package/src/ui/Carousel.tsx +121 -0
  55. package/src/ui/Cascader.tsx +158 -0
  56. package/src/ui/Checkbox.tsx +35 -0
  57. package/src/ui/Clock.tsx +221 -0
  58. package/src/ui/Collapse.tsx +48 -0
  59. package/src/ui/ColorPicker.tsx +89 -0
  60. package/src/ui/Combobox.tsx +67 -0
  61. package/src/ui/Command.tsx +158 -0
  62. package/src/ui/Comment.tsx +91 -0
  63. package/src/ui/ContextMenu.tsx +58 -0
  64. package/src/ui/Countdown.tsx +123 -0
  65. package/src/ui/DataGrid.tsx +177 -0
  66. package/src/ui/DateField.tsx +177 -0
  67. package/src/ui/DateRangePicker.tsx +204 -0
  68. package/src/ui/DateTimeField.tsx +68 -0
  69. package/src/ui/Descriptions.tsx +56 -0
  70. package/src/ui/Drawer.tsx +72 -0
  71. package/src/ui/Dropdown.tsx +71 -0
  72. package/src/ui/Dropzone.tsx +88 -0
  73. package/src/ui/Empty.tsx +53 -0
  74. package/src/ui/FileUpload.tsx +198 -0
  75. package/src/ui/FloatingActionButton.tsx +48 -0
  76. package/src/ui/Form.tsx +108 -0
  77. package/src/ui/Highlight.tsx +53 -0
  78. package/src/ui/HoverCard.tsx +42 -0
  79. package/src/ui/Image.tsx +66 -0
  80. package/src/ui/Input.tsx +38 -0
  81. package/src/ui/Kbd.tsx +25 -0
  82. package/src/ui/List.tsx +71 -0
  83. package/src/ui/Marquee.tsx +48 -0
  84. package/src/ui/Mentions.tsx +170 -0
  85. package/src/ui/Meter.tsx +57 -0
  86. package/src/ui/Modal.tsx +106 -0
  87. package/src/ui/MultiSelect.tsx +236 -0
  88. package/src/ui/NotificationCenter.tsx +103 -0
  89. package/src/ui/NumberInput.tsx +48 -0
  90. package/src/ui/PageHeader.tsx +60 -0
  91. package/src/ui/Pagination.tsx +62 -0
  92. package/src/ui/Popover.tsx +42 -0
  93. package/src/ui/Portal.tsx +34 -0
  94. package/src/ui/Progress.tsx +55 -0
  95. package/src/ui/RadioGroup.tsx +65 -0
  96. package/src/ui/Rating.tsx +98 -0
  97. package/src/ui/Result.tsx +75 -0
  98. package/src/ui/RingProgress.tsx +75 -0
  99. package/src/ui/SegmentedControl.tsx +61 -0
  100. package/src/ui/Select.tsx +55 -0
  101. package/src/ui/Separator.tsx +39 -0
  102. package/src/ui/Skeleton.tsx +23 -0
  103. package/src/ui/Slider.tsx +57 -0
  104. package/src/ui/Sortable.tsx +174 -0
  105. package/src/ui/SpeedDial.tsx +75 -0
  106. package/src/ui/Spinner.tsx +33 -0
  107. package/src/ui/Splitter.tsx +115 -0
  108. package/src/ui/Stat.tsx +74 -0
  109. package/src/ui/Stepper.tsx +134 -0
  110. package/src/ui/Switch.tsx +44 -0
  111. package/src/ui/Table.tsx +128 -0
  112. package/src/ui/Tabs.tsx +72 -0
  113. package/src/ui/TagInput.tsx +85 -0
  114. package/src/ui/Textarea.tsx +38 -0
  115. package/src/ui/TimeField.tsx +298 -0
  116. package/src/ui/Timeline.tsx +89 -0
  117. package/src/ui/Toast.tsx +68 -0
  118. package/src/ui/Toggle.tsx +41 -0
  119. package/src/ui/ToggleGroup.tsx +59 -0
  120. package/src/ui/Tooltip.tsx +42 -0
  121. package/src/ui/Tour.tsx +197 -0
  122. package/src/ui/Transfer.tsx +112 -0
  123. package/src/ui/Tree.tsx +100 -0
  124. package/src/ui/TreeSelect.tsx +151 -0
  125. package/src/ui/VirtualList.tsx +81 -0
  126. package/src/ui/internal/CalendarCore.tsx +276 -0
@@ -0,0 +1,58 @@
1
+ // Confirmation dialog on Kobalte's AlertDialog primitive — centered modal,
2
+ // reuses the modal-overlay/modal-content keyframes from styles.css.
3
+ import { AlertDialog as KAlertDialog } from '@kobalte/core/alert-dialog'
4
+ import type { JSX, ParentProps } from 'solid-js'
5
+ import { Show } from 'solid-js'
6
+
7
+ import { cn } from '../lib/cn'
8
+
9
+ interface AlertDialogProps extends ParentProps {
10
+ /** Whether the dialog is currently visible. */
11
+ open: boolean
12
+ /** Called when the dialog requests to open/close (overlay click, Escape, etc). */
13
+ onOpenChange: (open: boolean) => void
14
+ /** Optional heading rendered above the description. */
15
+ title?: string
16
+ class?: string
17
+ }
18
+
19
+ /**
20
+ * Centered, focus-trapped confirmation modal built on Kobalte's `AlertDialog`
21
+ * primitive. Use for interruptive confirmations (e.g. "Delete this item?")
22
+ * rather than for general-purpose dialogs.
23
+ *
24
+ * @example
25
+ * ```tsx
26
+ * <AlertDialog open={confirmOpen()} onOpenChange={setConfirmOpen} title="Delete project?">
27
+ * This action cannot be undone.
28
+ * </AlertDialog>
29
+ * ```
30
+ */
31
+ export function AlertDialog(props: AlertDialogProps): JSX.Element {
32
+ return (
33
+ <KAlertDialog open={props.open} onOpenChange={props.onOpenChange}>
34
+ <KAlertDialog.Portal>
35
+ <KAlertDialog.Overlay class="modal-overlay fixed inset-0 z-40 bg-background/80 backdrop-blur-sm" />
36
+ <div class="fixed inset-0 z-50 flex items-center justify-center p-4">
37
+ <KAlertDialog.Content
38
+ class={cn(
39
+ 'modal-content w-full max-w-md rounded-xl border border-border bg-card text-card-foreground shadow-sm',
40
+ props.class,
41
+ )}
42
+ >
43
+ <div class="p-6">
44
+ <Show when={props.title}>
45
+ <KAlertDialog.Title class="text-lg font-semibold leading-none tracking-tight">
46
+ {props.title}
47
+ </KAlertDialog.Title>
48
+ </Show>
49
+ <KAlertDialog.Description class="mt-3 text-sm text-muted-foreground">
50
+ {props.children}
51
+ </KAlertDialog.Description>
52
+ </div>
53
+ </KAlertDialog.Content>
54
+ </div>
55
+ </KAlertDialog.Portal>
56
+ </KAlertDialog>
57
+ )
58
+ }
@@ -0,0 +1,100 @@
1
+ // Anchor — a table-of-contents nav with scroll-spy. An IntersectionObserver
2
+ // (bound in onMount, client-only, so it's SSR-safe) tracks which section is in
3
+ // view and highlights the matching link; clicks smooth-scroll to the target.
4
+ import type { JSX } from 'solid-js'
5
+ import { createSignal, For, onCleanup, onMount } from 'solid-js'
6
+
7
+ import { cn } from '../lib/cn'
8
+
9
+ export interface AnchorItem {
10
+ /** The `id` of the section element this link points to. */
11
+ id: string
12
+ /** Text shown for the link. */
13
+ label: string
14
+ }
15
+
16
+ export interface AnchorProps {
17
+ /** Ordered list of sections to build the table of contents from. */
18
+ items: AnchorItem[]
19
+ class?: string
20
+ }
21
+
22
+ /**
23
+ * A vertical table-of-contents nav with scroll-spy. Each item links to a page
24
+ * section by `id`; the link whose section is topmost in the viewport is marked
25
+ * active. Clicking a link smooth-scrolls to its section.
26
+ *
27
+ * @example
28
+ * ```tsx
29
+ * <Anchor
30
+ * items={[
31
+ * { id: 'intro', label: 'Introduction' },
32
+ * { id: 'usage', label: 'Usage' },
33
+ * ]}
34
+ * />
35
+ * ```
36
+ */
37
+ export function Anchor(props: AnchorProps): JSX.Element {
38
+ const [activeId, setActiveId] = createSignal<string>()
39
+
40
+ onMount(() => {
41
+ // Track which observed sections are currently intersecting, then pick the
42
+ // one nearest the top of the document as the active anchor.
43
+ const visible = new Map<string, number>()
44
+
45
+ const observer = new IntersectionObserver(
46
+ (entries) => {
47
+ for (const entry of entries) {
48
+ const id = entry.target.id
49
+ if (entry.isIntersecting) visible.set(id, entry.boundingClientRect.top)
50
+ else visible.delete(id)
51
+ }
52
+ let topId: string | undefined
53
+ let topOffset = Infinity
54
+ for (const [id, offset] of visible) {
55
+ if (offset < topOffset) {
56
+ topOffset = offset
57
+ topId = id
58
+ }
59
+ }
60
+ if (topId) setActiveId(topId)
61
+ },
62
+ { rootMargin: '0px 0px -70% 0px' },
63
+ )
64
+
65
+ for (const item of props.items) {
66
+ const el = document.getElementById(item.id)
67
+ if (el) observer.observe(el)
68
+ }
69
+
70
+ onCleanup(() => observer.disconnect())
71
+ })
72
+
73
+ const onClick = (e: MouseEvent, id: string) => {
74
+ e.preventDefault()
75
+ document.getElementById(id)?.scrollIntoView({ behavior: 'smooth' })
76
+ setActiveId(id)
77
+ }
78
+
79
+ return (
80
+ <nav class={cn('flex flex-col gap-0.5', props.class)}>
81
+ <For each={props.items}>
82
+ {(item) => (
83
+ <a
84
+ href={'#' + item.id}
85
+ onClick={(e) => onClick(e, item.id)}
86
+ aria-current={activeId() === item.id ? 'true' : undefined}
87
+ class={cn(
88
+ 'block rounded px-3 py-1.5 text-sm transition-colors',
89
+ activeId() === item.id
90
+ ? 'border-l-2 border-primary font-medium text-primary'
91
+ : 'text-muted-foreground hover:text-foreground',
92
+ )}
93
+ >
94
+ {item.label}
95
+ </a>
96
+ )}
97
+ </For>
98
+ </nav>
99
+ )
100
+ }
@@ -0,0 +1,34 @@
1
+ // User avatar on Kobalte's Image primitive — shows the image or initials fallback.
2
+ import { Image as KImage } from '@kobalte/core/image'
3
+ import type { JSX } from 'solid-js'
4
+
5
+ import { cn } from '../lib/cn'
6
+
7
+ interface AvatarProps {
8
+ /** Image URL. If it fails to load (or is omitted), `fallback` is shown instead. */
9
+ src?: string
10
+ alt?: string
11
+ /** Text shown when there is no image or it fails to load (typically initials). */
12
+ fallback: string
13
+ class?: string
14
+ }
15
+
16
+ /**
17
+ * Circular user avatar built on Kobalte's `Image` primitive; automatically
18
+ * falls back to initials/text when the image is missing or fails to load.
19
+ *
20
+ * @example
21
+ * ```tsx
22
+ * <Avatar src={user.avatarUrl} alt={user.name} fallback="JD" />
23
+ * ```
24
+ */
25
+ export function Avatar(props: AvatarProps): JSX.Element {
26
+ return (
27
+ <KImage class={cn('inline-flex h-9 w-9 shrink-0 overflow-hidden rounded-full bg-muted', props.class)}>
28
+ <KImage.Img src={props.src} alt={props.alt} class="h-full w-full object-cover" />
29
+ <KImage.Fallback class="grid h-full w-full place-items-center text-xs font-medium text-muted-foreground">
30
+ {props.fallback}
31
+ </KImage.Fallback>
32
+ </KImage>
33
+ )
34
+ }
@@ -0,0 +1,55 @@
1
+ // Overlapping stack of avatars with a "+N" overflow counter past `max`.
2
+ import type { JSX } from 'solid-js'
3
+ import { For } from 'solid-js'
4
+
5
+ import { cn } from '../lib/cn'
6
+ import { Avatar } from './Avatar'
7
+
8
+ export interface AvatarGroupProps {
9
+ avatars: { src?: string; fallback: string; alt?: string }[]
10
+ /** Maximum number of avatars to render before collapsing the rest into `+N`. */
11
+ max?: number
12
+ class?: string
13
+ }
14
+
15
+ /**
16
+ * Renders a row of overlapping {@link Avatar}s. When there are more avatars than
17
+ * `max` (default 4), the remainder collapse into a trailing `+N` counter.
18
+ *
19
+ * @example
20
+ * ```tsx
21
+ * <AvatarGroup
22
+ * max={3}
23
+ * avatars={[
24
+ * { src: a.avatarUrl, alt: a.name, fallback: 'AA' },
25
+ * { src: b.avatarUrl, alt: b.name, fallback: 'BB' },
26
+ * { fallback: 'CC' },
27
+ * { fallback: 'DD' },
28
+ * ]}
29
+ * />
30
+ * ```
31
+ */
32
+ export function AvatarGroup(props: AvatarGroupProps): JSX.Element {
33
+ const max = () => props.max ?? 4
34
+ const shown = () => props.avatars.slice(0, max())
35
+ const overflow = () => props.avatars.length - shown().length
36
+
37
+ return (
38
+ <div class={cn('flex items-center', props.class)}>
39
+ <For each={shown()}>
40
+ {(avatar, index) => (
41
+ <div class={cn('ring-2 ring-background rounded-full', index() > 0 && '-ml-2')}>
42
+ <Avatar src={avatar.src} alt={avatar.alt} fallback={avatar.fallback} />
43
+ </div>
44
+ )}
45
+ </For>
46
+ {overflow() > 0 && (
47
+ <div class="-ml-2 ring-2 ring-background rounded-full">
48
+ <span class="grid h-9 w-9 shrink-0 place-items-center rounded-full bg-muted text-xs font-medium text-muted-foreground">
49
+ +{overflow()}
50
+ </span>
51
+ </div>
52
+ )}
53
+ </div>
54
+ )
55
+ }
@@ -0,0 +1,51 @@
1
+ // BackToTop — a fixed circular button that scrolls the window back to the top.
2
+ // Only appears once the page has scrolled past `threshold` px. Listener is bound
3
+ // in onMount (client-only), so it's SSR-safe.
4
+ import type { JSX } from 'solid-js'
5
+ import { createSignal, onCleanup, onMount, Show } from 'solid-js'
6
+ import { ArrowUp } from 'lucide-solid'
7
+
8
+ import { cn } from '../lib/cn'
9
+
10
+ export interface BackToTopProps {
11
+ /** Scroll distance (px) after which the button appears. Defaults to `300`. */
12
+ threshold?: number
13
+ class?: string
14
+ }
15
+
16
+ /**
17
+ * A floating "scroll to top" button, fixed to the bottom-right of the viewport.
18
+ * It fades in once the user scrolls past `threshold` and smooth-scrolls back up
19
+ * on click.
20
+ *
21
+ * @example
22
+ * ```tsx
23
+ * <BackToTop threshold={500} />
24
+ * ```
25
+ */
26
+ export function BackToTop(props: BackToTopProps): JSX.Element {
27
+ const [visible, setVisible] = createSignal(false)
28
+
29
+ onMount(() => {
30
+ const onScroll = () => setVisible(window.scrollY > (props.threshold ?? 300))
31
+ onScroll()
32
+ window.addEventListener('scroll', onScroll, { passive: true })
33
+ onCleanup(() => window.removeEventListener('scroll', onScroll))
34
+ })
35
+
36
+ return (
37
+ <Show when={visible()}>
38
+ <button
39
+ type="button"
40
+ aria-label="Back to top"
41
+ onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })}
42
+ class={cn(
43
+ 'fixed bottom-6 right-6 z-40 grid h-11 w-11 place-items-center rounded-full bg-primary text-primary-foreground shadow-lg',
44
+ props.class,
45
+ )}
46
+ >
47
+ <ArrowUp class="h-5 w-5" />
48
+ </button>
49
+ </Show>
50
+ )
51
+ }
@@ -0,0 +1,46 @@
1
+ // Badge — generic tone pill. The app-specific tone/label mappers
2
+ // (flowBadgeProps, statusBadgeTone, priorityBadgeTone, …) stay in the consuming
3
+ // app: they encode business vocabulary (income/expense, project priority, user
4
+ // roles), not design-system concerns.
5
+ import { type JSX, type ParentProps, splitProps } from 'solid-js'
6
+
7
+ import { cn } from '../lib/cn'
8
+
9
+ /** Semantic tone of a {@link Badge}; drives its background/text/ring color. */
10
+ export type BadgeTone = 'neutral' | 'success' | 'warning' | 'danger' | 'info'
11
+
12
+ const TONE_CLASSES: Record<BadgeTone, string> = {
13
+ neutral: 'bg-muted text-muted-foreground ring-border',
14
+ success: 'bg-emerald-500/15 text-emerald-600 ring-emerald-500/30',
15
+ warning: 'bg-amber-500/15 text-amber-600 ring-amber-500/30',
16
+ danger: 'bg-rose-500/15 text-rose-600 ring-rose-500/30',
17
+ info: 'bg-sky-500/15 text-sky-600 ring-sky-500/30',
18
+ }
19
+
20
+ const BADGE_BASE =
21
+ 'badge inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-semibold ring-1 ring-inset whitespace-nowrap'
22
+
23
+ interface BadgeProps extends ParentProps {
24
+ /** Visual/semantic tone. Defaults to `'neutral'`. */
25
+ tone?: BadgeTone
26
+ class?: string
27
+ }
28
+
29
+ /**
30
+ * Small rounded pill for status/labels, e.g. counts, states, or tags.
31
+ * Generic design-system primitive — app-specific tone mappers (e.g. mapping a
32
+ * business status to a {@link BadgeTone}) should live in the consuming app.
33
+ *
34
+ * @example
35
+ * ```tsx
36
+ * <Badge tone="success">Active</Badge>
37
+ * ```
38
+ */
39
+ export function Badge(props: BadgeProps): JSX.Element {
40
+ const [local, rest] = splitProps(props, ['tone', 'class', 'children'])
41
+ return (
42
+ <span class={cn(BADGE_BASE, TONE_CLASSES[local.tone ?? 'neutral'], local.class)} {...rest}>
43
+ {local.children}
44
+ </span>
45
+ )
46
+ }
@@ -0,0 +1,65 @@
1
+ // Horizontal bottom navigation bar of equal-width icon + label items,
2
+ // rendered inline where placed so the consumer controls positioning.
3
+ import type { JSX } from 'solid-js'
4
+ import { For } from 'solid-js'
5
+
6
+ import { cn } from '../lib/cn'
7
+
8
+ /** A single destination within a {@link BottomNavigation}. */
9
+ export interface BottomNavItem {
10
+ value: string
11
+ label: string
12
+ icon: JSX.Element
13
+ }
14
+
15
+ export interface BottomNavigationProps {
16
+ items: BottomNavItem[]
17
+ value: string
18
+ onChange: (value: string) => void
19
+ class?: string
20
+ }
21
+
22
+ /**
23
+ * Horizontal bar of equal-width navigation items, each stacking an icon above
24
+ * a short label. The active item (matching `value`) is highlighted and marked
25
+ * with `aria-current="page"`. Renders inline where placed — the consumer is
26
+ * responsible for positioning (e.g. `fixed inset-x-0 bottom-0`).
27
+ *
28
+ * @example
29
+ * ```tsx
30
+ * <BottomNavigation
31
+ * value={tab()}
32
+ * onChange={setTab}
33
+ * items={[
34
+ * { value: 'home', label: 'Home', icon: <HomeIcon /> },
35
+ * { value: 'search', label: 'Search', icon: <SearchIcon /> },
36
+ * { value: 'profile', label: 'Profile', icon: <UserIcon /> },
37
+ * ]}
38
+ * />
39
+ * ```
40
+ */
41
+ export function BottomNavigation(props: BottomNavigationProps): JSX.Element {
42
+ return (
43
+ <nav class={cn('flex items-stretch border-t border-border bg-glass', props.class)}>
44
+ <For each={props.items}>
45
+ {(item) => {
46
+ const active = () => item.value === props.value
47
+ return (
48
+ <button
49
+ type="button"
50
+ aria-current={active() ? 'page' : undefined}
51
+ onClick={() => props.onChange(item.value)}
52
+ class={cn(
53
+ 'flex flex-1 flex-col items-center gap-0.5 py-2 text-[11px] transition-colors',
54
+ active() ? 'text-primary' : 'text-muted-foreground hover:text-foreground',
55
+ )}
56
+ >
57
+ {item.icon}
58
+ <span>{item.label}</span>
59
+ </button>
60
+ )
61
+ }}
62
+ </For>
63
+ </nav>
64
+ )
65
+ }
@@ -0,0 +1,64 @@
1
+ // Navigation trail on Kobalte's Breadcrumbs primitive. Last item is current.
2
+ import { Breadcrumbs as KBreadcrumbs } from '@kobalte/core/breadcrumbs'
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 crumb rendered by {@link Breadcrumb}. */
9
+ export interface BreadcrumbItem {
10
+ label: string
11
+ /** Link target. Omit (or leave undefined on the last item) to render plain text for the current page. */
12
+ href?: string
13
+ }
14
+
15
+ interface BreadcrumbProps {
16
+ /** Ordered trail from root to current page; the last item is treated as the current location. */
17
+ items: BreadcrumbItem[]
18
+ class?: string
19
+ }
20
+
21
+ /**
22
+ * Navigation trail built on Kobalte's `Breadcrumbs` primitive. The last item
23
+ * in `items` is rendered as plain (non-link) text to indicate the current page.
24
+ *
25
+ * @example
26
+ * ```tsx
27
+ * <Breadcrumb
28
+ * items={[
29
+ * { label: 'Projects', href: '/projects' },
30
+ * { label: 'A4ui' },
31
+ * ]}
32
+ * />
33
+ * ```
34
+ */
35
+ export function Breadcrumb(props: BreadcrumbProps): JSX.Element {
36
+ return (
37
+ <KBreadcrumbs class={cn('text-sm', props.class)}>
38
+ <ol class="flex items-center gap-2 text-muted-foreground">
39
+ <For each={props.items}>
40
+ {(item, index) => {
41
+ const isLast = () => index() === props.items.length - 1
42
+ return (
43
+ <li class="flex items-center gap-2">
44
+ <Show
45
+ when={item.href && !isLast()}
46
+ fallback={
47
+ <span class={isLast() ? 'font-medium text-foreground' : undefined}>{item.label}</span>
48
+ }
49
+ >
50
+ <KBreadcrumbs.Link href={item.href} class="transition-colors hover:text-foreground">
51
+ {item.label}
52
+ </KBreadcrumbs.Link>
53
+ </Show>
54
+ <Show when={!isLast()}>
55
+ <KBreadcrumbs.Separator class="text-muted-foreground/60" />
56
+ </Show>
57
+ </li>
58
+ )
59
+ }}
60
+ </For>
61
+ </ol>
62
+ </KBreadcrumbs>
63
+ )
64
+ }
@@ -0,0 +1,49 @@
1
+ import type { JSX, ParentProps } from 'solid-js'
2
+ import { splitProps } from 'solid-js'
3
+
4
+ import { cn } from '../lib/cn'
5
+
6
+ /** Visual style of a {@link Button}. Defaults to `'primary'`. */
7
+ export type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost'
8
+
9
+ const VARIANT_CLASSES: Record<ButtonVariant, string> = {
10
+ primary: 'bg-primary text-primary-foreground hover:bg-primary/90',
11
+ secondary: 'bg-muted text-foreground hover:bg-muted/70',
12
+ outline: 'border border-border bg-transparent text-foreground hover:bg-muted',
13
+ ghost: 'bg-transparent text-foreground hover:bg-muted',
14
+ }
15
+
16
+ // transition-colors (hover) + transform (press) — both compositor-only, CSS
17
+ // handles it so there's no JS gesture-recognition cost per button.
18
+ const BUTTON_BASE =
19
+ 'inline-flex items-center justify-center rounded-md px-3 py-2 text-sm font-medium transition-[color,background-color,transform] duration-150 active:scale-[0.97] focus:outline-none focus:ring-2 focus:ring-ring disabled:pointer-events-none disabled:opacity-50'
20
+
21
+ interface ButtonProps extends ParentProps, Omit<JSX.ButtonHTMLAttributes<HTMLButtonElement>, 'type'> {
22
+ /** Visual style. Defaults to `'primary'`. */
23
+ variant?: ButtonVariant
24
+ class?: string
25
+ /** Defaults to "button" so action buttons inside a form never submit it by accident. */
26
+ type?: 'button' | 'submit' | 'reset'
27
+ }
28
+
29
+ /**
30
+ * Base button primitive: a plain `<button>` with A4ui's variants, focus ring,
31
+ * and press/hover transitions. Accepts all standard button HTML attributes.
32
+ *
33
+ * @example
34
+ * ```tsx
35
+ * <Button variant="outline" onClick={() => save()}>Save</Button>
36
+ * ```
37
+ */
38
+ export function Button(props: ButtonProps): JSX.Element {
39
+ const [local, rest] = splitProps(props, ['variant', 'class', 'type', 'children'])
40
+ return (
41
+ <button
42
+ type={local.type ?? 'button'}
43
+ class={cn(BUTTON_BASE, VARIANT_CLASSES[local.variant ?? 'primary'], local.class)}
44
+ {...rest}
45
+ >
46
+ {local.children}
47
+ </button>
48
+ )
49
+ }
@@ -0,0 +1,40 @@
1
+ // Full-month calendar view — an always-visible wrapper around the shared
2
+ // CalendarCore (which provides the day/month/year drill-down + year jumps).
3
+ // Sunday-first weekday order; speaks `Date` rather than YYYY-MM-DD strings.
4
+ import type { JSX } from 'solid-js'
5
+
6
+ import { cn } from '../lib/cn'
7
+ import { CalendarCore } from './internal/CalendarCore'
8
+
9
+ export interface CalendarProps {
10
+ /** Currently selected day. */
11
+ value?: Date
12
+ /** Called with the newly picked day. */
13
+ onChange?: (date: Date) => void
14
+ class?: string
15
+ }
16
+
17
+ /**
18
+ * Full-month calendar: a header with month/year navigation and a 6×7 grid of
19
+ * day buttons. Click the month or year in the header to jump straight to a month
20
+ * or year picker; the double chevrons step a whole year at a time. The selected
21
+ * day is filled and "today" gets a ring. Theme-agnostic, always visible.
22
+ *
23
+ * @example
24
+ * ```tsx
25
+ * const [date, setDate] = createSignal(new Date())
26
+ * <Calendar value={date()} onChange={setDate} />
27
+ * ```
28
+ */
29
+ export function Calendar(props: CalendarProps): JSX.Element {
30
+ return (
31
+ <div class={cn('w-72', props.class)}>
32
+ <CalendarCore
33
+ selected={props.value}
34
+ initialView={props.value}
35
+ onPick={(d) => props.onChange?.(d)}
36
+ weekStart={0}
37
+ />
38
+ </div>
39
+ )
40
+ }