@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.
- package/README.md +8 -7
- package/dist/Collapse-Cr5cDqAl.js +375 -0
- package/dist/charts/AreaChart.d.ts +38 -0
- package/dist/charts/BarChart.d.ts +17 -1
- package/dist/charts/PieChart.d.ts +29 -0
- package/dist/charts/RadialBarChart.d.ts +33 -0
- package/dist/charts/index.d.ts +5 -0
- package/dist/charts/shared.d.ts +111 -0
- package/dist/charts.js +1493 -780
- package/dist/commerce.js +625 -634
- package/dist/elements.css +1076 -8
- package/dist/elements.iife.js +1 -6
- package/dist/elements.js +7774 -9133
- package/dist/full.css +1076 -8
- package/dist/index.d.ts +30 -3
- package/dist/index.js +17502 -15098
- package/dist/layout/Aurora.d.ts +16 -0
- package/dist/layout/EffectsToggle.d.ts +1 -1
- package/dist/layout/SpaceBackground.d.ts +1 -1
- package/dist/lib/anchored.d.ts +16 -0
- package/dist/motion-CTIBwMCA.js +104 -0
- package/dist/styles.css +110 -3
- package/dist/ui/AspectRatio.d.ts +19 -0
- package/dist/ui/Attachment.d.ts +70 -0
- package/dist/ui/ButtonGroup.d.ts +37 -0
- package/dist/ui/ChatMarker.d.ts +20 -0
- package/dist/ui/ChatThread.d.ts +32 -8
- package/dist/ui/ContactSection.d.ts +49 -0
- package/dist/ui/CtaBanner.d.ts +28 -0
- package/dist/ui/DateField.d.ts +2 -2
- package/dist/ui/Drawer.d.ts +5 -2
- package/dist/ui/Faq.d.ts +29 -0
- package/dist/ui/FeatureGrid.d.ts +35 -0
- package/dist/ui/Field.d.ts +148 -0
- package/dist/ui/Hero.d.ts +48 -0
- package/dist/ui/InputGroup.d.ts +93 -0
- package/dist/ui/Item.d.ts +63 -0
- package/dist/ui/Label.d.ts +23 -0
- package/dist/ui/Menubar.d.ts +83 -0
- package/dist/ui/Message.d.ts +51 -5
- package/dist/ui/Modal.d.ts +8 -2
- package/dist/ui/NativeSelect.d.ts +26 -0
- package/dist/ui/NavigationMenu.d.ts +85 -0
- package/dist/ui/PromptComposer.d.ts +5 -0
- package/dist/ui/Prose.d.ts +21 -0
- package/dist/ui/Questionnaire.d.ts +71 -0
- package/dist/ui/ScrollArea.d.ts +30 -0
- package/dist/ui/Sidebar.d.ts +229 -0
- package/dist/ui/SiteFooter.d.ts +39 -0
- package/dist/ui/SiteNav.d.ts +35 -0
- package/dist/ui/TeamGrid.d.ts +33 -0
- package/dist/ui/Testimonials.d.ts +37 -0
- package/dist/ui/internal/sectionKit.d.ts +22 -0
- package/dist/ui/internal/socialIcons.d.ts +9 -0
- package/package.json +38 -26
- package/preset.d.ts +23 -0
- package/src/charts/AreaChart.tsx +206 -0
- package/src/charts/BarChart.tsx +241 -5
- package/src/charts/DonutChart.tsx +8 -11
- package/src/charts/LineChart.tsx +2 -3
- package/src/charts/PieChart.tsx +145 -0
- package/src/charts/RadialBarChart.tsx +141 -0
- package/src/charts/index.ts +14 -0
- package/src/charts/shared.tsx +304 -0
- package/src/index.ts +192 -3
- package/src/layout/Aurora.tsx +36 -7
- package/src/layout/sceneEffects.ts +22 -3
- package/src/lib/anchored.ts +57 -0
- package/src/ui/AspectRatio.tsx +35 -0
- package/src/ui/Attachment.tsx +189 -0
- package/src/ui/ButtonGroup.tsx +89 -0
- package/src/ui/ChatMarker.tsx +58 -0
- package/src/ui/ChatThread.tsx +115 -23
- package/src/ui/ContactSection.tsx +199 -0
- package/src/ui/CtaBanner.tsx +62 -0
- package/src/ui/DateField.tsx +17 -7
- package/src/ui/DateRangePicker.tsx +15 -5
- package/src/ui/Drawer.tsx +37 -4
- package/src/ui/Faq.tsx +57 -0
- package/src/ui/FeatureGrid.tsx +77 -0
- package/src/ui/Field.tsx +374 -0
- package/src/ui/Hero.tsx +146 -0
- package/src/ui/InputGroup.tsx +219 -0
- package/src/ui/Item.tsx +185 -0
- package/src/ui/Label.tsx +50 -0
- package/src/ui/Menubar.tsx +212 -0
- package/src/ui/Message.tsx +139 -13
- package/src/ui/Modal.tsx +41 -5
- package/src/ui/MultiSelect.tsx +15 -5
- package/src/ui/NativeSelect.tsx +66 -0
- package/src/ui/NavigationMenu.tsx +262 -0
- package/src/ui/PromptComposer.tsx +11 -0
- package/src/ui/Prose.tsx +63 -0
- package/src/ui/Questionnaire.tsx +331 -0
- package/src/ui/ScrollArea.tsx +70 -0
- package/src/ui/Sidebar.tsx +753 -0
- package/src/ui/SiteFooter.tsx +112 -0
- package/src/ui/SiteNav.tsx +146 -0
- package/src/ui/TeamGrid.tsx +81 -0
- package/src/ui/Testimonials.tsx +103 -0
- package/src/ui/TimeField.tsx +15 -5
- package/src/ui/internal/sectionKit.tsx +72 -0
- package/src/ui/internal/socialIcons.tsx +41 -0
- package/dist/NumberInput-Bnh2u7pp.js +0 -381
- package/dist/motion-jYMWmZqB.js +0 -119
package/src/ui/Hero.tsx
ADDED
|
@@ -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
|
+
}
|
package/src/ui/Item.tsx
ADDED
|
@@ -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
|
+
}
|
package/src/ui/Label.tsx
ADDED
|
@@ -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
|
+
}
|