@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.
Files changed (102) 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 -15126
  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/motion-CTIBwMCA.js +104 -0
  21. package/dist/styles.css +110 -3
  22. package/dist/ui/AspectRatio.d.ts +19 -0
  23. package/dist/ui/Attachment.d.ts +70 -0
  24. package/dist/ui/ButtonGroup.d.ts +37 -0
  25. package/dist/ui/ChatMarker.d.ts +20 -0
  26. package/dist/ui/ChatThread.d.ts +32 -8
  27. package/dist/ui/ContactSection.d.ts +49 -0
  28. package/dist/ui/CtaBanner.d.ts +28 -0
  29. package/dist/ui/Drawer.d.ts +5 -2
  30. package/dist/ui/Faq.d.ts +29 -0
  31. package/dist/ui/FeatureGrid.d.ts +35 -0
  32. package/dist/ui/Field.d.ts +148 -0
  33. package/dist/ui/Hero.d.ts +48 -0
  34. package/dist/ui/InputGroup.d.ts +93 -0
  35. package/dist/ui/Item.d.ts +63 -0
  36. package/dist/ui/Label.d.ts +23 -0
  37. package/dist/ui/Menubar.d.ts +83 -0
  38. package/dist/ui/Message.d.ts +51 -5
  39. package/dist/ui/Modal.d.ts +8 -2
  40. package/dist/ui/NativeSelect.d.ts +26 -0
  41. package/dist/ui/NavigationMenu.d.ts +85 -0
  42. package/dist/ui/PromptComposer.d.ts +5 -0
  43. package/dist/ui/Prose.d.ts +21 -0
  44. package/dist/ui/Questionnaire.d.ts +71 -0
  45. package/dist/ui/ScrollArea.d.ts +30 -0
  46. package/dist/ui/Sidebar.d.ts +229 -0
  47. package/dist/ui/SiteFooter.d.ts +39 -0
  48. package/dist/ui/SiteNav.d.ts +35 -0
  49. package/dist/ui/TeamGrid.d.ts +33 -0
  50. package/dist/ui/Testimonials.d.ts +37 -0
  51. package/dist/ui/internal/sectionKit.d.ts +22 -0
  52. package/dist/ui/internal/socialIcons.d.ts +9 -0
  53. package/package.json +38 -26
  54. package/preset.d.ts +23 -0
  55. package/src/charts/AreaChart.tsx +206 -0
  56. package/src/charts/BarChart.tsx +241 -5
  57. package/src/charts/DonutChart.tsx +8 -11
  58. package/src/charts/LineChart.tsx +2 -3
  59. package/src/charts/PieChart.tsx +145 -0
  60. package/src/charts/RadialBarChart.tsx +141 -0
  61. package/src/charts/index.ts +14 -0
  62. package/src/charts/shared.tsx +304 -0
  63. package/src/index.ts +192 -3
  64. package/src/layout/Aurora.tsx +36 -7
  65. package/src/layout/sceneEffects.ts +22 -3
  66. package/src/ui/AspectRatio.tsx +35 -0
  67. package/src/ui/Attachment.tsx +189 -0
  68. package/src/ui/ButtonGroup.tsx +89 -0
  69. package/src/ui/ChatMarker.tsx +58 -0
  70. package/src/ui/ChatThread.tsx +115 -23
  71. package/src/ui/ContactSection.tsx +199 -0
  72. package/src/ui/CtaBanner.tsx +62 -0
  73. package/src/ui/DateField.tsx +2 -1
  74. package/src/ui/DateRangePicker.tsx +2 -1
  75. package/src/ui/Drawer.tsx +37 -4
  76. package/src/ui/Faq.tsx +57 -0
  77. package/src/ui/FeatureGrid.tsx +77 -0
  78. package/src/ui/Field.tsx +374 -0
  79. package/src/ui/Hero.tsx +146 -0
  80. package/src/ui/InputGroup.tsx +219 -0
  81. package/src/ui/Item.tsx +185 -0
  82. package/src/ui/Label.tsx +50 -0
  83. package/src/ui/Menubar.tsx +212 -0
  84. package/src/ui/Message.tsx +139 -13
  85. package/src/ui/Modal.tsx +41 -5
  86. package/src/ui/MultiSelect.tsx +2 -1
  87. package/src/ui/NativeSelect.tsx +66 -0
  88. package/src/ui/NavigationMenu.tsx +262 -0
  89. package/src/ui/PromptComposer.tsx +11 -0
  90. package/src/ui/Prose.tsx +63 -0
  91. package/src/ui/Questionnaire.tsx +331 -0
  92. package/src/ui/ScrollArea.tsx +70 -0
  93. package/src/ui/Sidebar.tsx +753 -0
  94. package/src/ui/SiteFooter.tsx +112 -0
  95. package/src/ui/SiteNav.tsx +146 -0
  96. package/src/ui/TeamGrid.tsx +81 -0
  97. package/src/ui/Testimonials.tsx +103 -0
  98. package/src/ui/TimeField.tsx +2 -1
  99. package/src/ui/internal/sectionKit.tsx +72 -0
  100. package/src/ui/internal/socialIcons.tsx +41 -0
  101. package/dist/NumberInput-Bnh2u7pp.js +0 -381
  102. 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
+ }
@@ -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
+ }
@@ -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
+ }