@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,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
|
+
}
|
package/src/ui/Badge.tsx
ADDED
|
@@ -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
|
+
}
|