@a4ui/core 0.37.1 → 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 -15126
- 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/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/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/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 +2 -1
- package/src/ui/DateRangePicker.tsx +2 -1
- 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 +2 -1
- 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 +2 -1
- 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
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
// Application menubar (File / Edit / View …) on Kobalte's Menubar primitive:
|
|
2
|
+
// roving focus across the top-level triggers, hover-to-switch once a menu is
|
|
3
|
+
// open, checkbox/radio items and nested submenus. Styled with the same glass
|
|
4
|
+
// overlay surface as the other menus.
|
|
5
|
+
import { Menubar as KMenubar } from '@kobalte/core/menubar'
|
|
6
|
+
import { Check, ChevronRight, Circle } from 'lucide-solid'
|
|
7
|
+
import type { JSX, ParentProps } from 'solid-js'
|
|
8
|
+
import { For, Show, splitProps } from 'solid-js'
|
|
9
|
+
|
|
10
|
+
import { cn } from '../lib/cn'
|
|
11
|
+
import { Kbd } from './Kbd'
|
|
12
|
+
|
|
13
|
+
const CONTENT =
|
|
14
|
+
'bg-glass z-50 min-w-[12rem] overflow-hidden rounded-xl p-1 text-sm text-foreground shadow-xl outline-none'
|
|
15
|
+
const ITEM =
|
|
16
|
+
'relative flex cursor-pointer select-none items-center gap-2 rounded-md py-1.5 pl-8 pr-2 outline-none transition-colors data-[highlighted]:bg-muted data-[expanded]:bg-muted data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus-visible:ring-2 focus-visible:ring-ring'
|
|
17
|
+
|
|
18
|
+
interface ClassProps extends ParentProps {
|
|
19
|
+
class?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Props of {@link Menubar}. */
|
|
23
|
+
export interface MenubarProps extends ClassProps {
|
|
24
|
+
/** Accessible name of the menubar. @default 'Application menu' */
|
|
25
|
+
label?: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Horizontal bar of top-level menus. Arrow Left/Right moves between the
|
|
30
|
+
* triggers; once one menu is open, hovering another trigger switches to it.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```tsx
|
|
34
|
+
* <Menubar>
|
|
35
|
+
* <MenubarMenu label="File">
|
|
36
|
+
* <MenubarItem shortcut={['⌘', 'N']}>New project</MenubarItem>
|
|
37
|
+
* <MenubarSeparator />
|
|
38
|
+
* <MenubarItem>Quit</MenubarItem>
|
|
39
|
+
* </MenubarMenu>
|
|
40
|
+
* </Menubar>
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export function Menubar(props: MenubarProps): JSX.Element {
|
|
44
|
+
return (
|
|
45
|
+
<KMenubar
|
|
46
|
+
aria-label={props.label ?? 'Application menu'}
|
|
47
|
+
class={cn(
|
|
48
|
+
'inline-flex items-center gap-0.5 rounded-xl border border-border bg-card/60 p-1',
|
|
49
|
+
props.class,
|
|
50
|
+
)}
|
|
51
|
+
>
|
|
52
|
+
{props.children}
|
|
53
|
+
</KMenubar>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Props of {@link MenubarMenu}. */
|
|
58
|
+
export interface MenubarMenuProps extends ClassProps {
|
|
59
|
+
/** Text of the top-level trigger. */
|
|
60
|
+
label: string
|
|
61
|
+
/** Extra classes for the popover content. */
|
|
62
|
+
contentClass?: string
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** One top-level menu: a trigger button plus its popover of items. */
|
|
66
|
+
export function MenubarMenu(props: MenubarMenuProps): JSX.Element {
|
|
67
|
+
return (
|
|
68
|
+
<KMenubar.Menu>
|
|
69
|
+
<KMenubar.Trigger
|
|
70
|
+
class={cn(
|
|
71
|
+
'rounded-lg px-3 py-1.5 text-sm font-medium text-foreground outline-none transition-colors hover:bg-muted data-[expanded]:bg-muted focus-visible:ring-2 focus-visible:ring-ring',
|
|
72
|
+
props.class,
|
|
73
|
+
)}
|
|
74
|
+
>
|
|
75
|
+
{props.label}
|
|
76
|
+
</KMenubar.Trigger>
|
|
77
|
+
<KMenubar.Portal>
|
|
78
|
+
<KMenubar.Content class={cn(CONTENT, props.contentClass)}>{props.children}</KMenubar.Content>
|
|
79
|
+
</KMenubar.Portal>
|
|
80
|
+
</KMenubar.Menu>
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Props of {@link MenubarItem}. */
|
|
85
|
+
export interface MenubarItemProps extends ClassProps {
|
|
86
|
+
disabled?: boolean
|
|
87
|
+
/** Called when the item is chosen (click, Enter or Space). */
|
|
88
|
+
onSelect?: () => void
|
|
89
|
+
/** Keys shown right-aligned as keycaps, e.g. `['⌘', 'N']`. */
|
|
90
|
+
shortcut?: string[]
|
|
91
|
+
/** Styles the item as destructive (e.g. "Delete"). */
|
|
92
|
+
destructive?: boolean
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function Shortcut(props: { keys?: string[] }): JSX.Element {
|
|
96
|
+
return (
|
|
97
|
+
<Show when={props.keys?.length}>
|
|
98
|
+
<span class="ml-auto flex gap-0.5 pl-6" aria-hidden="true">
|
|
99
|
+
<For each={props.keys}>{(k) => <Kbd class="min-w-[1.25rem] px-1 py-0 text-[11px]">{k}</Kbd>}</For>
|
|
100
|
+
</span>
|
|
101
|
+
</Show>
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** A plain action row, with an optional keyboard-shortcut hint. */
|
|
106
|
+
export function MenubarItem(props: MenubarItemProps): JSX.Element {
|
|
107
|
+
return (
|
|
108
|
+
<KMenubar.Item
|
|
109
|
+
class={cn(ITEM, props.destructive && 'text-destructive', props.class)}
|
|
110
|
+
disabled={props.disabled}
|
|
111
|
+
onSelect={() => props.onSelect?.()}
|
|
112
|
+
>
|
|
113
|
+
{props.children}
|
|
114
|
+
<Shortcut keys={props.shortcut} />
|
|
115
|
+
</KMenubar.Item>
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Props of {@link MenubarCheckboxItem}. */
|
|
120
|
+
export interface MenubarCheckboxItemProps extends ClassProps {
|
|
121
|
+
checked?: boolean
|
|
122
|
+
onChange?: (checked: boolean) => void
|
|
123
|
+
disabled?: boolean
|
|
124
|
+
shortcut?: string[]
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** A toggle row exposing `role="menuitemcheckbox"` / `aria-checked`. */
|
|
128
|
+
export function MenubarCheckboxItem(props: MenubarCheckboxItemProps): JSX.Element {
|
|
129
|
+
return (
|
|
130
|
+
<KMenubar.CheckboxItem
|
|
131
|
+
class={cn(ITEM, props.class)}
|
|
132
|
+
checked={props.checked}
|
|
133
|
+
onChange={(v) => props.onChange?.(v)}
|
|
134
|
+
disabled={props.disabled}
|
|
135
|
+
>
|
|
136
|
+
<KMenubar.ItemIndicator class="absolute left-2 inline-flex items-center">
|
|
137
|
+
<Check class="h-3.5 w-3.5" />
|
|
138
|
+
</KMenubar.ItemIndicator>
|
|
139
|
+
{props.children}
|
|
140
|
+
<Shortcut keys={props.shortcut} />
|
|
141
|
+
</KMenubar.CheckboxItem>
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Props of {@link MenubarRadioGroup}. */
|
|
146
|
+
export interface MenubarRadioGroupProps extends ParentProps {
|
|
147
|
+
value?: string
|
|
148
|
+
onChange?: (value: string) => void
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Groups {@link MenubarRadioItem}s so exactly one is selected. */
|
|
152
|
+
export function MenubarRadioGroup(props: MenubarRadioGroupProps): JSX.Element {
|
|
153
|
+
return (
|
|
154
|
+
<KMenubar.RadioGroup value={props.value} onChange={(v) => props.onChange?.(v)}>
|
|
155
|
+
{props.children}
|
|
156
|
+
</KMenubar.RadioGroup>
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Props of {@link MenubarRadioItem}. */
|
|
161
|
+
export interface MenubarRadioItemProps extends ClassProps {
|
|
162
|
+
value: string
|
|
163
|
+
disabled?: boolean
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** A single-choice row inside a {@link MenubarRadioGroup} (`role="menuitemradio"`). */
|
|
167
|
+
export function MenubarRadioItem(props: MenubarRadioItemProps): JSX.Element {
|
|
168
|
+
return (
|
|
169
|
+
<KMenubar.RadioItem class={cn(ITEM, props.class)} value={props.value} disabled={props.disabled}>
|
|
170
|
+
<KMenubar.ItemIndicator class="absolute left-2 inline-flex items-center">
|
|
171
|
+
<Circle class="h-2 w-2 fill-current" />
|
|
172
|
+
</KMenubar.ItemIndicator>
|
|
173
|
+
{props.children}
|
|
174
|
+
</KMenubar.RadioItem>
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Props of {@link MenubarSub}. */
|
|
179
|
+
export interface MenubarSubProps extends ClassProps {
|
|
180
|
+
/** Text of the submenu trigger row. */
|
|
181
|
+
label: string
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** A nested submenu: opens with ArrowRight / hover, closes with ArrowLeft. */
|
|
185
|
+
export function MenubarSub(props: MenubarSubProps): JSX.Element {
|
|
186
|
+
return (
|
|
187
|
+
<KMenubar.Sub overlap gutter={4} shift={-5}>
|
|
188
|
+
<KMenubar.SubTrigger class={cn(ITEM, props.class)}>
|
|
189
|
+
{props.label}
|
|
190
|
+
<ChevronRight class="ml-auto h-3.5 w-3.5 text-muted-foreground" />
|
|
191
|
+
</KMenubar.SubTrigger>
|
|
192
|
+
<KMenubar.Portal>
|
|
193
|
+
<KMenubar.SubContent class={CONTENT}>{props.children}</KMenubar.SubContent>
|
|
194
|
+
</KMenubar.Portal>
|
|
195
|
+
</KMenubar.Sub>
|
|
196
|
+
)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Thin divider between groups of items. */
|
|
200
|
+
export function MenubarSeparator(props: { class?: string }): JSX.Element {
|
|
201
|
+
return <KMenubar.Separator class={cn('-mx-1 my-1 h-px border-0 bg-border', props.class)} />
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Non-interactive section heading inside a menu. */
|
|
205
|
+
export function MenubarLabel(props: ClassProps): JSX.Element {
|
|
206
|
+
const [local] = splitProps(props, ['class', 'children'])
|
|
207
|
+
return (
|
|
208
|
+
<div class={cn('px-2 py-1.5 pl-8 text-xs font-semibold text-muted-foreground', local.class)}>
|
|
209
|
+
{local.children}
|
|
210
|
+
</div>
|
|
211
|
+
)
|
|
212
|
+
}
|