@a4ui/core 0.37.0 → 0.38.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 (105) hide show
  1. package/README.md +8 -7
  2. package/dist/Collapse-Cr5cDqAl.js +375 -0
  3. package/dist/charts/AreaChart.d.ts +38 -0
  4. package/dist/charts/BarChart.d.ts +17 -1
  5. package/dist/charts/PieChart.d.ts +29 -0
  6. package/dist/charts/RadialBarChart.d.ts +33 -0
  7. package/dist/charts/index.d.ts +5 -0
  8. package/dist/charts/shared.d.ts +111 -0
  9. package/dist/charts.js +1493 -780
  10. package/dist/commerce.js +625 -634
  11. package/dist/elements.css +1076 -8
  12. package/dist/elements.iife.js +1 -6
  13. package/dist/elements.js +7774 -9133
  14. package/dist/full.css +1076 -8
  15. package/dist/index.d.ts +30 -3
  16. package/dist/index.js +17502 -15098
  17. package/dist/layout/Aurora.d.ts +16 -0
  18. package/dist/layout/EffectsToggle.d.ts +1 -1
  19. package/dist/layout/SpaceBackground.d.ts +1 -1
  20. package/dist/lib/anchored.d.ts +16 -0
  21. package/dist/motion-CTIBwMCA.js +104 -0
  22. package/dist/styles.css +110 -3
  23. package/dist/ui/AspectRatio.d.ts +19 -0
  24. package/dist/ui/Attachment.d.ts +70 -0
  25. package/dist/ui/ButtonGroup.d.ts +37 -0
  26. package/dist/ui/ChatMarker.d.ts +20 -0
  27. package/dist/ui/ChatThread.d.ts +32 -8
  28. package/dist/ui/ContactSection.d.ts +49 -0
  29. package/dist/ui/CtaBanner.d.ts +28 -0
  30. package/dist/ui/DateField.d.ts +2 -2
  31. package/dist/ui/Drawer.d.ts +5 -2
  32. package/dist/ui/Faq.d.ts +29 -0
  33. package/dist/ui/FeatureGrid.d.ts +35 -0
  34. package/dist/ui/Field.d.ts +148 -0
  35. package/dist/ui/Hero.d.ts +48 -0
  36. package/dist/ui/InputGroup.d.ts +93 -0
  37. package/dist/ui/Item.d.ts +63 -0
  38. package/dist/ui/Label.d.ts +23 -0
  39. package/dist/ui/Menubar.d.ts +83 -0
  40. package/dist/ui/Message.d.ts +51 -5
  41. package/dist/ui/Modal.d.ts +8 -2
  42. package/dist/ui/NativeSelect.d.ts +26 -0
  43. package/dist/ui/NavigationMenu.d.ts +85 -0
  44. package/dist/ui/PromptComposer.d.ts +5 -0
  45. package/dist/ui/Prose.d.ts +21 -0
  46. package/dist/ui/Questionnaire.d.ts +71 -0
  47. package/dist/ui/ScrollArea.d.ts +30 -0
  48. package/dist/ui/Sidebar.d.ts +229 -0
  49. package/dist/ui/SiteFooter.d.ts +39 -0
  50. package/dist/ui/SiteNav.d.ts +35 -0
  51. package/dist/ui/TeamGrid.d.ts +33 -0
  52. package/dist/ui/Testimonials.d.ts +37 -0
  53. package/dist/ui/internal/sectionKit.d.ts +22 -0
  54. package/dist/ui/internal/socialIcons.d.ts +9 -0
  55. package/package.json +38 -26
  56. package/preset.d.ts +23 -0
  57. package/src/charts/AreaChart.tsx +206 -0
  58. package/src/charts/BarChart.tsx +241 -5
  59. package/src/charts/DonutChart.tsx +8 -11
  60. package/src/charts/LineChart.tsx +2 -3
  61. package/src/charts/PieChart.tsx +145 -0
  62. package/src/charts/RadialBarChart.tsx +141 -0
  63. package/src/charts/index.ts +14 -0
  64. package/src/charts/shared.tsx +304 -0
  65. package/src/index.ts +192 -3
  66. package/src/layout/Aurora.tsx +36 -7
  67. package/src/layout/sceneEffects.ts +22 -3
  68. package/src/lib/anchored.ts +57 -0
  69. package/src/ui/AspectRatio.tsx +35 -0
  70. package/src/ui/Attachment.tsx +189 -0
  71. package/src/ui/ButtonGroup.tsx +89 -0
  72. package/src/ui/ChatMarker.tsx +58 -0
  73. package/src/ui/ChatThread.tsx +115 -23
  74. package/src/ui/ContactSection.tsx +199 -0
  75. package/src/ui/CtaBanner.tsx +62 -0
  76. package/src/ui/DateField.tsx +17 -7
  77. package/src/ui/DateRangePicker.tsx +15 -5
  78. package/src/ui/Drawer.tsx +37 -4
  79. package/src/ui/Faq.tsx +57 -0
  80. package/src/ui/FeatureGrid.tsx +77 -0
  81. package/src/ui/Field.tsx +374 -0
  82. package/src/ui/Hero.tsx +146 -0
  83. package/src/ui/InputGroup.tsx +219 -0
  84. package/src/ui/Item.tsx +185 -0
  85. package/src/ui/Label.tsx +50 -0
  86. package/src/ui/Menubar.tsx +212 -0
  87. package/src/ui/Message.tsx +139 -13
  88. package/src/ui/Modal.tsx +41 -5
  89. package/src/ui/MultiSelect.tsx +15 -5
  90. package/src/ui/NativeSelect.tsx +66 -0
  91. package/src/ui/NavigationMenu.tsx +262 -0
  92. package/src/ui/PromptComposer.tsx +11 -0
  93. package/src/ui/Prose.tsx +63 -0
  94. package/src/ui/Questionnaire.tsx +331 -0
  95. package/src/ui/ScrollArea.tsx +70 -0
  96. package/src/ui/Sidebar.tsx +753 -0
  97. package/src/ui/SiteFooter.tsx +112 -0
  98. package/src/ui/SiteNav.tsx +146 -0
  99. package/src/ui/TeamGrid.tsx +81 -0
  100. package/src/ui/Testimonials.tsx +103 -0
  101. package/src/ui/TimeField.tsx +15 -5
  102. package/src/ui/internal/sectionKit.tsx +72 -0
  103. package/src/ui/internal/socialIcons.tsx +41 -0
  104. package/dist/NumberInput-Bnh2u7pp.js +0 -381
  105. package/dist/motion-jYMWmZqB.js +0 -119
@@ -0,0 +1,146 @@
1
+ // Hero — the opening section of a landing page: headline, supporting text, up
2
+ // to two calls-to-action, and an optional image, on a plain, aurora or photo
3
+ // background.
4
+ import type { JSX } from 'solid-js'
5
+ import { Show } from 'solid-js'
6
+
7
+ import { cn } from '../lib/cn'
8
+ import { Button } from './Button'
9
+ import { SectionHeading, type SectionLink } from './internal/sectionKit'
10
+
11
+ /** Arrangement of a {@link Hero}: text centered, or text beside the image. */
12
+ export type HeroLayout = 'center' | 'split'
13
+ /** Backdrop of a {@link Hero}. `'image'` uses `image` as a full-bleed photo. */
14
+ export type HeroBackground = 'none' | 'aurora' | 'image'
15
+
16
+ export interface HeroProps {
17
+ /** Small label above the title, e.g. a category or announcement. */
18
+ eyebrow?: string
19
+ /** Main headline, rendered as the page `<h1>`. */
20
+ title: string
21
+ /** Supporting paragraph under the title. */
22
+ subtitle?: string
23
+ /** Main call-to-action (filled button). */
24
+ primary?: SectionLink
25
+ /** Secondary call-to-action (outline button). */
26
+ secondary?: SectionLink
27
+ /** Image URL. Shown beside the text in `split` layout, under it in `center`, or behind it with `background="image"`. */
28
+ image?: string
29
+ /** Alt text of `image`. Defaults to empty (decorative). */
30
+ imageAlt?: string
31
+ /** Defaults to `'center'`. */
32
+ layout?: HeroLayout
33
+ /** Defaults to `'aurora'`. */
34
+ background?: HeroBackground
35
+ /** Anchor id for in-page navigation. */
36
+ id?: string
37
+ class?: string
38
+ }
39
+
40
+ /**
41
+ * Opening section of a landing page: eyebrow, headline, subtitle and up to two
42
+ * call-to-action links, with an optional image. `layout="split"` puts the image
43
+ * beside the text on wide screens (stacked on phones); `background` picks a
44
+ * theme-tinted aurora glow, a full-bleed photo with a legibility scrim, or
45
+ * nothing.
46
+ *
47
+ * @example
48
+ * ```tsx
49
+ * <Hero
50
+ * eyebrow="New"
51
+ * title="Ship your site this week"
52
+ * subtitle="Everything you need to launch, nothing you don't."
53
+ * primary={{ label: 'Get started', href: '/signup' }}
54
+ * secondary={{ label: 'See pricing', href: '#pricing' }}
55
+ * />
56
+ * ```
57
+ */
58
+ export function Hero(props: HeroProps): JSX.Element {
59
+ const layout = () => props.layout ?? 'center'
60
+ const background = () => props.background ?? 'aurora'
61
+ const photoBackdrop = () => background() === 'image' && props.image !== undefined
62
+ const showInlineImage = () => props.image !== undefined && !photoBackdrop()
63
+
64
+ const actions = (
65
+ <Show when={props.primary || props.secondary}>
66
+ <div class={cn('flex flex-wrap gap-3', layout() === 'center' && 'justify-center')}>
67
+ <Show when={props.primary}>
68
+ {(link) => (
69
+ <Button href={link().href} ripple class="px-5 py-2.5 text-base">
70
+ {link().label}
71
+ </Button>
72
+ )}
73
+ </Show>
74
+ <Show when={props.secondary}>
75
+ {(link) => (
76
+ <Button href={link().href} variant="outline" class="px-5 py-2.5 text-base">
77
+ {link().label}
78
+ </Button>
79
+ )}
80
+ </Show>
81
+ </div>
82
+ </Show>
83
+ )
84
+
85
+ return (
86
+ <section
87
+ id={props.id}
88
+ class={cn('relative isolate overflow-hidden', photoBackdrop() && 'text-white', props.class)}
89
+ >
90
+ <Show when={background() === 'aurora'}>
91
+ <div aria-hidden="true" class="pointer-events-none absolute inset-0 -z-10">
92
+ <div class="aurora-drift absolute -left-[10%] -top-[30%] h-[28rem] w-[28rem] rounded-full bg-primary/30 blur-3xl" />
93
+ <div class="aurora-drift absolute -right-[10%] top-[10%] h-[24rem] w-[24rem] rounded-full bg-accent/25 blur-3xl [animation-delay:-9s]" />
94
+ </div>
95
+ </Show>
96
+ <Show when={photoBackdrop()}>
97
+ <img
98
+ src={props.image}
99
+ alt={props.imageAlt ?? ''}
100
+ class="absolute inset-0 -z-20 h-full w-full object-cover"
101
+ />
102
+ <div
103
+ aria-hidden="true"
104
+ class="absolute inset-0 -z-10 bg-gradient-to-b from-black/70 via-black/55 to-black/75"
105
+ />
106
+ </Show>
107
+
108
+ <div
109
+ class={cn(
110
+ 'mx-auto grid w-full max-w-6xl gap-10 px-5 py-20 sm:py-28',
111
+ layout() === 'split' && showInlineImage() && 'items-center md:grid-cols-2',
112
+ )}
113
+ >
114
+ <div
115
+ class={cn(
116
+ 'flex flex-col gap-8',
117
+ layout() === 'center' ? 'items-center text-center' : 'items-start',
118
+ )}
119
+ >
120
+ <SectionHeading
121
+ level={1}
122
+ align={layout() === 'center' ? 'center' : 'left'}
123
+ eyebrow={props.eyebrow}
124
+ title={props.title}
125
+ subtitle={props.subtitle}
126
+ class={cn(
127
+ layout() === 'center' && 'max-w-3xl',
128
+ photoBackdrop() && '[&_h1]:text-white [&_p]:text-white/85',
129
+ )}
130
+ />
131
+ {actions}
132
+ </div>
133
+ <Show when={showInlineImage()}>
134
+ <img
135
+ src={props.image}
136
+ alt={props.imageAlt ?? ''}
137
+ class={cn(
138
+ 'card w-full rounded-2xl object-cover shadow-xl',
139
+ layout() === 'center' ? 'mx-auto aspect-[16/9] max-w-4xl' : 'aspect-[4/3]',
140
+ )}
141
+ />
142
+ </Show>
143
+ </div>
144
+ </section>
145
+ )
146
+ }
@@ -0,0 +1,219 @@
1
+ // InputGroup — one bordered, focus-ringed shell around a borderless input or
2
+ // textarea plus addons (icons, text, buttons, hints) on any side. The group
3
+ // owns the border and the focus ring (focus-within via the shared `a4-field`
4
+ // class), so the inner controls draw none of their own.
5
+ import type { JSX, ParentProps } from 'solid-js'
6
+ import { splitProps } from 'solid-js'
7
+
8
+ import { cn } from '../lib/cn'
9
+ import { Button } from './Button'
10
+
11
+ export interface InputGroupProps extends ParentProps, JSX.HTMLAttributes<HTMLDivElement> {
12
+ class?: string
13
+ }
14
+
15
+ /**
16
+ * Wrapper that owns a single border and focus ring for an input and its addons.
17
+ * Addons are placed by their `align` prop, whatever their order in the DOM.
18
+ *
19
+ * @example
20
+ * ```tsx
21
+ * <InputGroup>
22
+ * <InputGroupAddon><Search class="h-4 w-4" /></InputGroupAddon>
23
+ * <InputGroupInput value={q()} onInput={setQ} placeholder="Search" />
24
+ * </InputGroup>
25
+ * ```
26
+ */
27
+ export function InputGroup(props: InputGroupProps): JSX.Element {
28
+ const [local, rest] = splitProps(props, ['class', 'children'])
29
+ return (
30
+ <div
31
+ role="group"
32
+ data-slot="input-group"
33
+ class={cn(
34
+ 'a4-field group/input-group relative flex w-full min-w-0 flex-wrap items-center rounded-md border border-input bg-background text-sm text-foreground',
35
+ 'has-[[data-slot=input-group-control][aria-invalid=true]]:border-destructive',
36
+ 'has-[[data-slot=input-group-control]:disabled]:opacity-50',
37
+ local.class,
38
+ )}
39
+ {...rest}
40
+ >
41
+ {local.children}
42
+ </div>
43
+ )
44
+ }
45
+
46
+ export type InputGroupAddonAlign = 'inline-start' | 'inline-end' | 'block-start' | 'block-end'
47
+
48
+ export interface InputGroupAddonProps extends ParentProps, JSX.HTMLAttributes<HTMLDivElement> {
49
+ /** Where the addon sits. Defaults to `inline-start`. */
50
+ align?: InputGroupAddonAlign
51
+ class?: string
52
+ }
53
+
54
+ // Inline addons share the control's row (control is flex-1); block addons take
55
+ // a full row above/below it via `basis-full` + flex-wrap on the group.
56
+ const ADDON_ALIGN: Record<InputGroupAddonAlign, string> = {
57
+ 'inline-start': 'order-1 pl-3',
58
+ 'inline-end': 'order-3 pr-3',
59
+ 'block-start': 'order-0 basis-full px-3 pt-2',
60
+ 'block-end': 'order-4 basis-full px-3 pb-2',
61
+ }
62
+
63
+ /**
64
+ * Adornment around the control. Clicking anywhere on it (except on a button)
65
+ * focuses the group's input or textarea.
66
+ *
67
+ * @example
68
+ * ```tsx
69
+ * <InputGroupAddon align="inline-end"><Kbd>/</Kbd></InputGroupAddon>
70
+ * ```
71
+ */
72
+ export function InputGroupAddon(props: InputGroupAddonProps): JSX.Element {
73
+ const [local, rest] = splitProps(props, ['class', 'align', 'children', 'onClick'])
74
+ const align = (): InputGroupAddonAlign => local.align ?? 'inline-start'
75
+ return (
76
+ <div
77
+ role="presentation"
78
+ data-slot="input-group-addon"
79
+ data-align={align()}
80
+ class={cn(
81
+ 'flex cursor-text select-none items-center gap-2 py-1.5 text-sm text-muted-foreground [&>svg]:h-4 [&>svg]:w-4',
82
+ ADDON_ALIGN[align()],
83
+ local.class,
84
+ )}
85
+ onClick={(e) => {
86
+ if (typeof local.onClick === 'function') local.onClick(e)
87
+ if ((e.target as HTMLElement).closest('button')) return
88
+ e.currentTarget.parentElement?.querySelector<HTMLElement>('[data-slot=input-group-control]')?.focus()
89
+ }}
90
+ {...rest}
91
+ >
92
+ {local.children}
93
+ </div>
94
+ )
95
+ }
96
+
97
+ /**
98
+ * Plain text inside an addon (a prefix like `https://` or a unit).
99
+ *
100
+ * @example
101
+ * ```tsx
102
+ * <InputGroupAddon><InputGroupText>https://</InputGroupText></InputGroupAddon>
103
+ * ```
104
+ */
105
+ export function InputGroupText(props: ParentProps<{ class?: string }>): JSX.Element {
106
+ return (
107
+ <span
108
+ class={cn('flex items-center gap-2 text-sm text-muted-foreground [&>svg]:h-4 [&>svg]:w-4', props.class)}
109
+ >
110
+ {props.children}
111
+ </span>
112
+ )
113
+ }
114
+
115
+ /** Compact sizes for {@link InputGroupButton}. */
116
+ export type InputGroupButtonSize = 'xs' | 'sm' | 'icon-xs' | 'icon-sm'
117
+
118
+ const BUTTON_SIZE: Record<InputGroupButtonSize, string> = {
119
+ xs: 'h-6 gap-1 rounded px-2 py-0 text-xs',
120
+ sm: 'h-7 gap-1.5 px-2.5 py-0 text-sm',
121
+ 'icon-xs': 'h-6 w-6 rounded p-0',
122
+ 'icon-sm': 'h-7 w-7 p-0',
123
+ }
124
+
125
+ export interface InputGroupButtonProps
126
+ extends ParentProps, Omit<JSX.ButtonHTMLAttributes<HTMLButtonElement>, 'type'> {
127
+ /** Compact size. Defaults to `xs`. */
128
+ size?: InputGroupButtonSize
129
+ /** Button variant; ghost by default so it blends into the group. */
130
+ variant?: 'primary' | 'secondary' | 'outline' | 'ghost'
131
+ type?: 'button' | 'submit' | 'reset'
132
+ class?: string
133
+ }
134
+
135
+ /**
136
+ * Compact {@link Button} for use inside an addon. Ghost by default.
137
+ *
138
+ * @example
139
+ * ```tsx
140
+ * <InputGroupButton size="icon-xs" aria-label="Show password"><Eye /></InputGroupButton>
141
+ * ```
142
+ */
143
+ export function InputGroupButton(props: InputGroupButtonProps): JSX.Element {
144
+ const [local, rest] = splitProps(props, ['class', 'size', 'variant', 'children'])
145
+ return (
146
+ <Button
147
+ variant={local.variant ?? 'ghost'}
148
+ class={cn('shadow-none [&>svg]:h-3.5 [&>svg]:w-3.5', BUTTON_SIZE[local.size ?? 'xs'], local.class)}
149
+ {...rest}
150
+ >
151
+ {local.children}
152
+ </Button>
153
+ )
154
+ }
155
+
156
+ const CONTROL_BASE =
157
+ 'order-2 min-w-0 flex-1 border-0 bg-transparent px-3 py-2 text-sm text-foreground shadow-none outline-none ring-0 placeholder:text-muted-foreground disabled:cursor-not-allowed'
158
+
159
+ export interface InputGroupInputProps extends Omit<
160
+ JSX.InputHTMLAttributes<HTMLInputElement>,
161
+ 'value' | 'onInput'
162
+ > {
163
+ value: string
164
+ /** Called with the raw string value on every input event. */
165
+ onInput: (value: string) => void
166
+ class?: string
167
+ }
168
+
169
+ /**
170
+ * Borderless `<input>` for an {@link InputGroup} (same value/onInput API as `Input`).
171
+ *
172
+ * @example
173
+ * ```tsx
174
+ * <InputGroupInput value={url()} onInput={setUrl} placeholder="example.com" />
175
+ * ```
176
+ */
177
+ export function InputGroupInput(props: InputGroupInputProps): JSX.Element {
178
+ const [local, rest] = splitProps(props, ['value', 'onInput', 'class'])
179
+ return (
180
+ <input
181
+ data-slot="input-group-control"
182
+ class={cn(CONTROL_BASE, local.class)}
183
+ value={local.value}
184
+ onInput={(e) => local.onInput(e.currentTarget.value)}
185
+ {...rest}
186
+ />
187
+ )
188
+ }
189
+
190
+ export interface InputGroupTextareaProps extends Omit<
191
+ JSX.TextareaHTMLAttributes<HTMLTextAreaElement>,
192
+ 'value' | 'onInput'
193
+ > {
194
+ value: string
195
+ /** Called with the new string value on every input event. */
196
+ onInput: (value: string) => void
197
+ class?: string
198
+ }
199
+
200
+ /**
201
+ * Borderless `<textarea>` for an {@link InputGroup}.
202
+ *
203
+ * @example
204
+ * ```tsx
205
+ * <InputGroupTextarea value={msg()} onInput={setMsg} rows={3} />
206
+ * ```
207
+ */
208
+ export function InputGroupTextarea(props: InputGroupTextareaProps): JSX.Element {
209
+ const [local, rest] = splitProps(props, ['value', 'onInput', 'class'])
210
+ return (
211
+ <textarea
212
+ data-slot="input-group-control"
213
+ class={cn(CONTROL_BASE, 'min-h-[72px] resize-none', local.class)}
214
+ value={local.value}
215
+ onInput={(e) => local.onInput(e.currentTarget.value)}
216
+ {...rest}
217
+ />
218
+ )
219
+ }
@@ -0,0 +1,185 @@
1
+ // Composable row primitive: media + title/description + actions, with optional
2
+ // header/footer. Plain HTML + Tailwind. `List` stays the data-driven shortcut;
3
+ // `Item` is the building block when a row needs custom structure (attachments,
4
+ // settings rows, sidebar entries, notification rows).
5
+ import { splitProps, type JSX, type ParentProps } from 'solid-js'
6
+ import { Dynamic } from 'solid-js/web'
7
+
8
+ import { cn } from '../lib/cn'
9
+
10
+ export type ItemVariant = 'default' | 'outline' | 'muted'
11
+ export type ItemSize = 'default' | 'sm' | 'xs'
12
+
13
+ interface ItemProps extends ParentProps {
14
+ /** @default 'default' (transparent) */
15
+ variant?: ItemVariant
16
+ /** Padding + gap density. @default 'default' */
17
+ size?: ItemSize
18
+ /**
19
+ * Render the row as a link. The whole row becomes the hit target and gets a
20
+ * hover/focus state.
21
+ */
22
+ href?: string
23
+ /** Render the row as a `<button>` with this click handler (when no `href`). */
24
+ onClick?: JSX.EventHandlerUnion<HTMLElement, MouseEvent>
25
+ /** Element to render when neither `href` nor `onClick` is set. @default 'div' */
26
+ as?: 'div' | 'li' | 'article' | 'section'
27
+ class?: string
28
+ }
29
+
30
+ export type { ItemProps }
31
+
32
+ const VARIANT: Record<ItemVariant, string> = {
33
+ default: 'border border-transparent',
34
+ outline: 'border border-border',
35
+ muted: 'border border-transparent bg-muted/50',
36
+ }
37
+
38
+ const SIZE: Record<ItemSize, string> = {
39
+ default: 'gap-4 p-4',
40
+ sm: 'gap-3 px-3 py-2.5',
41
+ xs: 'gap-2 px-2 py-1.5 text-sm',
42
+ }
43
+
44
+ /**
45
+ * A row of content: optional `ItemMedia`, an `ItemContent` column with
46
+ * `ItemTitle`/`ItemDescription`, and trailing `ItemActions`. `ItemHeader` and
47
+ * `ItemFooter` span the full width above/below. Group rows with `ItemGroup`.
48
+ *
49
+ * @example
50
+ * ```tsx
51
+ * <Item variant="outline">
52
+ * <ItemMedia variant="icon"><Bell /></ItemMedia>
53
+ * <ItemContent>
54
+ * <ItemTitle>Weekly digest</ItemTitle>
55
+ * <ItemDescription>Every Monday at 9:00</ItemDescription>
56
+ * </ItemContent>
57
+ * <ItemActions><Switch checked /></ItemActions>
58
+ * </Item>
59
+ * ```
60
+ */
61
+ export function Item(props: ItemProps): JSX.Element {
62
+ const [local] = splitProps(props, ['variant', 'size', 'href', 'onClick', 'as', 'class', 'children'])
63
+ const interactive = (): boolean => local.href !== undefined || local.onClick !== undefined
64
+ const tag = (): string => (local.href !== undefined ? 'a' : local.onClick ? 'button' : (local.as ?? 'div'))
65
+ return (
66
+ <Dynamic
67
+ component={tag()}
68
+ href={local.href}
69
+ type={tag() === 'button' ? 'button' : undefined}
70
+ onClick={local.onClick}
71
+ data-slot="item"
72
+ data-variant={local.variant ?? 'default'}
73
+ data-size={local.size ?? 'default'}
74
+ class={cn(
75
+ 'group/item flex w-full flex-wrap items-center rounded-lg text-left text-foreground outline-none transition-colors',
76
+ VARIANT[local.variant ?? 'default'],
77
+ SIZE[local.size ?? 'default'],
78
+ interactive() && 'cursor-pointer hover:bg-muted/60 focus-visible:ring-2 focus-visible:ring-ring',
79
+ local.class,
80
+ )}
81
+ >
82
+ {local.children}
83
+ </Dynamic>
84
+ )
85
+ }
86
+
87
+ interface SlotProps extends ParentProps {
88
+ class?: string
89
+ }
90
+
91
+ /** Vertical stack of `Item`s. */
92
+ export function ItemGroup(props: SlotProps): JSX.Element {
93
+ return (
94
+ <div data-slot="item-group" class={cn('flex flex-col', props.class)}>
95
+ {props.children}
96
+ </div>
97
+ )
98
+ }
99
+
100
+ /** Thin divider between `Item`s inside an `ItemGroup`. */
101
+ export function ItemSeparator(props: { class?: string }): JSX.Element {
102
+ return <div role="separator" data-slot="item-separator" class={cn('mx-4 h-px bg-border', props.class)} />
103
+ }
104
+
105
+ export type ItemMediaVariant = 'default' | 'icon' | 'image'
106
+
107
+ const MEDIA: Record<ItemMediaVariant, string> = {
108
+ default: '',
109
+ icon: 'grid h-9 w-9 place-items-center rounded-md border border-border bg-muted text-muted-foreground [&>svg]:h-4 [&>svg]:w-4',
110
+ image: 'h-10 w-10 overflow-hidden rounded-md [&>img]:h-full [&>img]:w-full [&>img]:object-cover',
111
+ }
112
+
113
+ /** Leading visual: an icon tile, an image thumbnail, or anything (`default`). */
114
+ export function ItemMedia(props: SlotProps & { variant?: ItemMediaVariant }): JSX.Element {
115
+ return (
116
+ <div
117
+ data-slot="item-media"
118
+ class={cn('flex shrink-0 items-center justify-center', MEDIA[props.variant ?? 'default'], props.class)}
119
+ >
120
+ {props.children}
121
+ </div>
122
+ )
123
+ }
124
+
125
+ /** The flexible text column (title + description). */
126
+ export function ItemContent(props: SlotProps): JSX.Element {
127
+ return (
128
+ <div data-slot="item-content" class={cn('flex min-w-0 flex-1 flex-col gap-0.5', props.class)}>
129
+ {props.children}
130
+ </div>
131
+ )
132
+ }
133
+
134
+ export function ItemTitle(props: SlotProps): JSX.Element {
135
+ return (
136
+ <div
137
+ data-slot="item-title"
138
+ class={cn('flex items-center gap-2 text-sm font-medium leading-snug', props.class)}
139
+ >
140
+ {props.children}
141
+ </div>
142
+ )
143
+ }
144
+
145
+ /** Muted secondary text, clamped to two lines. */
146
+ export function ItemDescription(props: SlotProps): JSX.Element {
147
+ return (
148
+ <p data-slot="item-description" class={cn('line-clamp-2 text-sm text-muted-foreground', props.class)}>
149
+ {props.children}
150
+ </p>
151
+ )
152
+ }
153
+
154
+ /** Trailing controls (buttons, a switch, a badge). */
155
+ export function ItemActions(props: SlotProps): JSX.Element {
156
+ return (
157
+ <div data-slot="item-actions" class={cn('flex shrink-0 items-center gap-2', props.class)}>
158
+ {props.children}
159
+ </div>
160
+ )
161
+ }
162
+
163
+ /** Full-width row above the media/content line. */
164
+ export function ItemHeader(props: SlotProps): JSX.Element {
165
+ return (
166
+ <div
167
+ data-slot="item-header"
168
+ class={cn('flex basis-full items-center justify-between gap-2', props.class)}
169
+ >
170
+ {props.children}
171
+ </div>
172
+ )
173
+ }
174
+
175
+ /** Full-width row below the media/content line. */
176
+ export function ItemFooter(props: SlotProps): JSX.Element {
177
+ return (
178
+ <div
179
+ data-slot="item-footer"
180
+ class={cn('flex basis-full items-center justify-between gap-2', props.class)}
181
+ >
182
+ {props.children}
183
+ </div>
184
+ )
185
+ }
@@ -0,0 +1,50 @@
1
+ import { Show, splitProps, type JSX, type ParentProps } from 'solid-js'
2
+
3
+ import { cn } from '../lib/cn'
4
+
5
+ interface LabelProps extends ParentProps, Omit<JSX.LabelHTMLAttributes<HTMLLabelElement>, 'for'> {
6
+ /** Id of the control this label names. */
7
+ for?: string
8
+ /** Appends an `*` marker (hidden from assistive tech; set `required` on the control too). */
9
+ required?: boolean
10
+ /** Dims the label. It also dims when it follows a disabled `.peer` control. */
11
+ disabled?: boolean
12
+ class?: string
13
+ }
14
+
15
+ export type { LabelProps }
16
+
17
+ /**
18
+ * Styled `<label>`. Clicking it focuses/activates the control matching `for`.
19
+ * Dims when `disabled`, inside a `.group` with `data-disabled`, or after a
20
+ * disabled `.peer` control.
21
+ *
22
+ * @example
23
+ * ```tsx
24
+ * <Label for="email" required>Email</Label>
25
+ * <Input id="email" value={email()} onInput={setEmail} required />
26
+ * ```
27
+ */
28
+ export function Label(props: LabelProps): JSX.Element {
29
+ const [local, rest] = splitProps(props, ['required', 'disabled', 'class', 'children'])
30
+ return (
31
+ <label
32
+ data-disabled={local.disabled ? '' : undefined}
33
+ class={cn(
34
+ 'inline-flex select-none items-center gap-1 text-sm font-medium leading-none text-foreground',
35
+ 'data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50',
36
+ 'group-data-[disabled]:cursor-not-allowed group-data-[disabled]:opacity-50',
37
+ 'peer-disabled:cursor-not-allowed peer-disabled:opacity-50',
38
+ local.class,
39
+ )}
40
+ {...rest}
41
+ >
42
+ {local.children}
43
+ <Show when={local.required}>
44
+ <span aria-hidden="true" class="text-destructive">
45
+ *
46
+ </span>
47
+ </Show>
48
+ </label>
49
+ )
50
+ }