@a4ui/core 0.37.1 → 0.38.1
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/AlertDialog.tsx +6 -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/Command.tsx +6 -3
- 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 +43 -7
- 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/Image.tsx +6 -3
- 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 +47 -8
- 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,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
|
+
}
|
package/src/ui/Message.tsx
CHANGED
|
@@ -1,13 +1,21 @@
|
|
|
1
|
-
// Message — one chat message row
|
|
2
|
-
//
|
|
1
|
+
// Message — one chat message row. By default full-width with no bubble; pass
|
|
2
|
+
// `variant` for a bubble style and `align` to push it to one side. Rich content
|
|
3
|
+
// is passed as children so the caller can render markdown, code blocks, lists,
|
|
3
4
|
// etc. inside the readable prose block.
|
|
4
|
-
import { type JSX, Match, Show, Switch } from 'solid-js'
|
|
5
|
+
import { createContext, type JSX, Match, Show, Switch, useContext } from 'solid-js'
|
|
5
6
|
|
|
6
7
|
import { cn } from '../lib/cn'
|
|
7
8
|
|
|
8
9
|
/** Who sent a {@link Message}; drives its subtle visual treatment. */
|
|
9
10
|
export type ChatRole = 'user' | 'assistant' | 'system'
|
|
10
11
|
|
|
12
|
+
/** Bubble style of a {@link Message}. */
|
|
13
|
+
export type MessageVariant =
|
|
14
|
+
'default' | 'secondary' | 'muted' | 'tinted' | 'outline' | 'ghost' | 'destructive'
|
|
15
|
+
|
|
16
|
+
/** Which side of the thread a message sits on. */
|
|
17
|
+
export type MessageAlign = 'start' | 'end'
|
|
18
|
+
|
|
11
19
|
export interface MessageProps {
|
|
12
20
|
role: ChatRole
|
|
13
21
|
children: JSX.Element
|
|
@@ -17,40 +25,85 @@ export interface MessageProps {
|
|
|
17
25
|
avatar?: JSX.Element
|
|
18
26
|
/** Preformatted timestamp string, e.g. `'2:45 PM'`. */
|
|
19
27
|
timestamp?: string
|
|
28
|
+
/** Stable id, rendered as `data-message-id` so `ChatThread.scrollToMessage` can find it. */
|
|
29
|
+
id?: string
|
|
30
|
+
/**
|
|
31
|
+
* Bubble style. When omitted the legacy role-based treatment applies
|
|
32
|
+
* (`assistant` faint panel, `user` faint tint).
|
|
33
|
+
*/
|
|
34
|
+
variant?: MessageVariant
|
|
35
|
+
/** Push the message to one side and shrink it to its content (max 85% wide). */
|
|
36
|
+
align?: MessageAlign
|
|
37
|
+
/** Slot under the content, e.g. a row of reaction chips. */
|
|
38
|
+
reactions?: JSX.Element
|
|
39
|
+
/** Slot under the content for `Attachment`s (wrap several in `AttachmentGroup`). */
|
|
40
|
+
attachments?: JSX.Element
|
|
20
41
|
class?: string
|
|
21
42
|
}
|
|
22
43
|
|
|
44
|
+
const VARIANT: Record<MessageVariant, string> = {
|
|
45
|
+
default: 'border border-border bg-card text-card-foreground',
|
|
46
|
+
secondary: 'bg-secondary text-secondary-foreground',
|
|
47
|
+
muted: 'bg-muted/60 text-foreground',
|
|
48
|
+
tinted: 'border border-primary/20 bg-primary/10 text-foreground',
|
|
49
|
+
outline: 'border border-border bg-transparent text-foreground',
|
|
50
|
+
ghost: 'bg-transparent text-foreground',
|
|
51
|
+
destructive: 'border border-destructive/30 bg-destructive/10 text-destructive',
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface MessageGroupContext {
|
|
55
|
+
align?: MessageAlign
|
|
56
|
+
variant?: MessageVariant
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const GroupContext = createContext<MessageGroupContext>()
|
|
60
|
+
|
|
23
61
|
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* subtle, token-based treatment
|
|
27
|
-
* `
|
|
62
|
+
* One chat message: an optional avatar/author header followed by a prose
|
|
63
|
+
* content block, with optional `reactions` and `attachments` slots. Role is
|
|
64
|
+
* differentiated with subtle, token-based treatment — or choose a bubble
|
|
65
|
+
* `variant` and an `align` side. Inside a {@link MessageGroup} the header is
|
|
66
|
+
* suppressed (the group shows the sender once).
|
|
28
67
|
*
|
|
29
68
|
* @example
|
|
30
69
|
* ```tsx
|
|
31
|
-
* <Message role="assistant" author="Nova" timestamp="2:45 PM">
|
|
70
|
+
* <Message role="assistant" author="Nova" timestamp="2:45 PM" variant="secondary" align="start">
|
|
32
71
|
* <p>Here's the summary you asked for.</p>
|
|
33
72
|
* </Message>
|
|
34
73
|
* ```
|
|
35
74
|
*/
|
|
36
75
|
export function Message(props: MessageProps): JSX.Element {
|
|
76
|
+
const group = useContext(GroupContext)
|
|
77
|
+
const variant = (): MessageVariant | undefined => props.variant ?? group?.variant
|
|
78
|
+
const align = (): MessageAlign | undefined => props.align ?? group?.align
|
|
37
79
|
return (
|
|
38
80
|
<Switch>
|
|
39
81
|
<Match when={props.role === 'system'}>
|
|
40
|
-
<div
|
|
82
|
+
<div
|
|
83
|
+
data-message-id={props.id}
|
|
84
|
+
class={cn('text-center text-xs text-muted-foreground italic', props.class)}
|
|
85
|
+
>
|
|
41
86
|
{props.children}
|
|
42
87
|
</div>
|
|
43
88
|
</Match>
|
|
44
89
|
<Match when={true}>
|
|
45
90
|
<div
|
|
91
|
+
data-message-id={props.id}
|
|
92
|
+
data-variant={variant()}
|
|
93
|
+
data-align={align()}
|
|
46
94
|
class={cn(
|
|
47
95
|
'flex flex-col gap-1',
|
|
48
|
-
|
|
49
|
-
|
|
96
|
+
variant() ? 'rounded-2xl px-3.5 py-2.5' : 'rounded-xl',
|
|
97
|
+
variant() ? VARIANT[variant()!] : 'text-foreground',
|
|
98
|
+
!variant() && props.role === 'assistant' && 'bg-muted/40 p-3',
|
|
99
|
+
!variant() && props.role === 'user' && 'bg-primary/5 p-3',
|
|
100
|
+
align() && 'w-fit max-w-[85%]',
|
|
101
|
+
align() === 'end' && 'self-end',
|
|
102
|
+
align() === 'start' && 'self-start',
|
|
50
103
|
props.class,
|
|
51
104
|
)}
|
|
52
105
|
>
|
|
53
|
-
<Show when={props.author || props.avatar || props.timestamp}>
|
|
106
|
+
<Show when={!group && (props.author || props.avatar || props.timestamp)}>
|
|
54
107
|
<div class="flex items-center gap-2">
|
|
55
108
|
{props.avatar}
|
|
56
109
|
<Show when={props.author}>
|
|
@@ -61,9 +114,82 @@ export function Message(props: MessageProps): JSX.Element {
|
|
|
61
114
|
</Show>
|
|
62
115
|
</div>
|
|
63
116
|
</Show>
|
|
64
|
-
<div class="space-y-2 text-sm leading-relaxed
|
|
117
|
+
<div class="space-y-2 text-sm leading-relaxed">{props.children}</div>
|
|
118
|
+
<Show when={props.attachments}>
|
|
119
|
+
<div data-slot="message-attachments" class="mt-1 max-w-full">
|
|
120
|
+
{props.attachments}
|
|
121
|
+
</div>
|
|
122
|
+
</Show>
|
|
123
|
+
<Show when={props.reactions}>
|
|
124
|
+
<div data-slot="message-reactions" class="mt-1 flex flex-wrap items-center gap-1">
|
|
125
|
+
{props.reactions}
|
|
126
|
+
</div>
|
|
127
|
+
</Show>
|
|
65
128
|
</div>
|
|
66
129
|
</Match>
|
|
67
130
|
</Switch>
|
|
68
131
|
)
|
|
69
132
|
}
|
|
133
|
+
|
|
134
|
+
export interface MessageGroupProps {
|
|
135
|
+
/** Sender name, shown once above the group. */
|
|
136
|
+
author?: string
|
|
137
|
+
/** Sender avatar, shown once next to the name. */
|
|
138
|
+
avatar?: JSX.Element
|
|
139
|
+
/** Timestamp of the first message, shown once. */
|
|
140
|
+
timestamp?: string
|
|
141
|
+
/** Side for every message in the group (a message can still override it). */
|
|
142
|
+
align?: MessageAlign
|
|
143
|
+
/** Bubble style for every message in the group (a message can still override it). */
|
|
144
|
+
variant?: MessageVariant
|
|
145
|
+
/** The consecutive `Message`s from this sender. */
|
|
146
|
+
children: JSX.Element
|
|
147
|
+
class?: string
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Visually groups consecutive messages from one sender: avatar, name and time
|
|
152
|
+
* appear once, and the bubbles sit closer together than separate messages.
|
|
153
|
+
*
|
|
154
|
+
* @example
|
|
155
|
+
* ```tsx
|
|
156
|
+
* <MessageGroup author="Nova" avatar={<Avatar name="Nova" size="sm" />} variant="secondary" align="start">
|
|
157
|
+
* <Message role="assistant">First thought.</Message>
|
|
158
|
+
* <Message role="assistant">And a follow-up.</Message>
|
|
159
|
+
* </MessageGroup>
|
|
160
|
+
* ```
|
|
161
|
+
*/
|
|
162
|
+
export function MessageGroup(props: MessageGroupProps): JSX.Element {
|
|
163
|
+
return (
|
|
164
|
+
<GroupContext.Provider
|
|
165
|
+
value={{
|
|
166
|
+
get align() {
|
|
167
|
+
return props.align
|
|
168
|
+
},
|
|
169
|
+
get variant() {
|
|
170
|
+
return props.variant
|
|
171
|
+
},
|
|
172
|
+
}}
|
|
173
|
+
>
|
|
174
|
+
<div
|
|
175
|
+
role="group"
|
|
176
|
+
aria-label={props.author ? `Messages from ${props.author}` : undefined}
|
|
177
|
+
data-slot="message-group"
|
|
178
|
+
class={cn('flex flex-col gap-1', props.class)}
|
|
179
|
+
>
|
|
180
|
+
<Show when={props.author || props.avatar || props.timestamp}>
|
|
181
|
+
<div class={cn('flex items-center gap-2', props.align === 'end' && 'flex-row-reverse')}>
|
|
182
|
+
{props.avatar}
|
|
183
|
+
<Show when={props.author}>
|
|
184
|
+
<span class="text-sm font-medium text-foreground">{props.author}</span>
|
|
185
|
+
</Show>
|
|
186
|
+
<Show when={props.timestamp}>
|
|
187
|
+
<span class="text-xs text-muted-foreground">{props.timestamp}</span>
|
|
188
|
+
</Show>
|
|
189
|
+
</div>
|
|
190
|
+
</Show>
|
|
191
|
+
{props.children}
|
|
192
|
+
</div>
|
|
193
|
+
</GroupContext.Provider>
|
|
194
|
+
)
|
|
195
|
+
}
|
package/src/ui/Modal.tsx
CHANGED
|
@@ -5,14 +5,17 @@
|
|
|
5
5
|
// yes/no) where a full-height drawer would be overkill.
|
|
6
6
|
// The .drawer-content / .modal-content classes drive the slide/scale keyframes
|
|
7
7
|
// (styles.css) off Kobalte's data-expanded/data-closed presence tracking.
|
|
8
|
-
import
|
|
8
|
+
// Namespace import on purpose: Kobalte's `AlertDialog` is built with Object.assign on the
|
|
9
|
+
// same `DialogRoot` function as `Dialog`, so `Dialog.Content` depends on module load order (#118).
|
|
10
|
+
// The named exports (`Root`, `Content`, ...) are not affected.
|
|
11
|
+
import * as Dialog from '@kobalte/core/dialog'
|
|
9
12
|
import { X } from 'lucide-solid'
|
|
10
13
|
import type { JSX, ParentProps } from 'solid-js'
|
|
11
14
|
import { Show } from 'solid-js'
|
|
12
15
|
|
|
13
16
|
import { cn } from '../lib/cn'
|
|
14
17
|
|
|
15
|
-
interface ModalProps extends ParentProps {
|
|
18
|
+
export interface ModalProps extends ParentProps {
|
|
16
19
|
open: boolean
|
|
17
20
|
onOpenChange: (open: boolean) => void
|
|
18
21
|
/** Header title; omitting it hides the whole header bar. */
|
|
@@ -24,11 +27,41 @@ interface ModalProps extends ParentProps {
|
|
|
24
27
|
* @default 'drawer'
|
|
25
28
|
*/
|
|
26
29
|
variant?: 'drawer' | 'center'
|
|
30
|
+
/**
|
|
31
|
+
* Width of the dialog: `'sm'` 24rem, `'md'` 32rem, `'lg'` 42rem, `'xl'` 56rem,
|
|
32
|
+
* `'full'` the whole viewport minus margin. Omitted keeps the historical width
|
|
33
|
+
* (`variant="center"`: 32rem, `variant="drawer"`: 520px). Content taller than
|
|
34
|
+
* the viewport scrolls inside the dialog; the header stays pinned.
|
|
35
|
+
*/
|
|
36
|
+
size?: 'sm' | 'md' | 'lg' | 'xl' | 'full'
|
|
27
37
|
class?: string
|
|
28
38
|
/** Accessible label for the close button. @default 'Close' */
|
|
29
39
|
closeLabel?: string
|
|
30
40
|
}
|
|
31
41
|
|
|
42
|
+
const CENTER_WIDTH = {
|
|
43
|
+
sm: 'max-w-sm',
|
|
44
|
+
md: 'max-w-lg',
|
|
45
|
+
lg: 'max-w-2xl',
|
|
46
|
+
xl: 'max-w-4xl',
|
|
47
|
+
full: 'max-w-full',
|
|
48
|
+
} as const
|
|
49
|
+
const DRAWER_WIDTH = {
|
|
50
|
+
sm: 'max-w-sm',
|
|
51
|
+
md: 'max-w-[520px]',
|
|
52
|
+
lg: 'max-w-2xl',
|
|
53
|
+
xl: 'max-w-4xl',
|
|
54
|
+
full: 'max-w-full',
|
|
55
|
+
} as const
|
|
56
|
+
|
|
57
|
+
// Popovers inside the dialog (MultiSelect, ...) are portaled to <body>, so
|
|
58
|
+
// Kobalte sees a click on them as "outside" and would close the dialog. Those
|
|
59
|
+
// panels carry `data-a4-floating`; interacting with one is not a dismissal.
|
|
60
|
+
function keepForFloating(e: Event): void {
|
|
61
|
+
const target = (e as CustomEvent<{ originalEvent?: Event }>).detail?.originalEvent?.target ?? e.target
|
|
62
|
+
if (target instanceof Element && target.closest('[data-a4-floating]')) e.preventDefault()
|
|
63
|
+
}
|
|
64
|
+
|
|
32
65
|
/**
|
|
33
66
|
* Dialog on Kobalte's `Dialog` primitive (focus trap, portal, presence handled
|
|
34
67
|
* for you), rendered as either a slide-over drawer or a centered modal depending
|
|
@@ -45,7 +78,7 @@ interface ModalProps extends ParentProps {
|
|
|
45
78
|
export function Modal(props: ModalProps): JSX.Element {
|
|
46
79
|
const isCenter = () => props.variant === 'center'
|
|
47
80
|
return (
|
|
48
|
-
<Dialog open={props.open} onOpenChange={props.onOpenChange}>
|
|
81
|
+
<Dialog.Root open={props.open} onOpenChange={props.onOpenChange}>
|
|
49
82
|
<Dialog.Portal>
|
|
50
83
|
<Dialog.Overlay class="modal-overlay fixed inset-0 z-40 bg-background/80 backdrop-blur-sm" />
|
|
51
84
|
<Show
|
|
@@ -53,8 +86,10 @@ export function Modal(props: ModalProps): JSX.Element {
|
|
|
53
86
|
fallback={
|
|
54
87
|
<div class="fixed inset-0 z-50 flex justify-end">
|
|
55
88
|
<Dialog.Content
|
|
89
|
+
onInteractOutside={keepForFloating}
|
|
56
90
|
class={cn(
|
|
57
|
-
'drawer-content bg-glass flex h-full w-full
|
|
91
|
+
'drawer-content bg-glass flex h-full w-full flex-col overflow-y-auto border-l border-border text-card-foreground shadow-2xl',
|
|
92
|
+
DRAWER_WIDTH[props.size ?? 'md'],
|
|
58
93
|
props.class,
|
|
59
94
|
)}
|
|
60
95
|
>
|
|
@@ -78,13 +113,15 @@ export function Modal(props: ModalProps): JSX.Element {
|
|
|
78
113
|
>
|
|
79
114
|
<div class="fixed inset-0 z-50 flex items-center justify-center p-4">
|
|
80
115
|
<Dialog.Content
|
|
116
|
+
onInteractOutside={keepForFloating}
|
|
81
117
|
class={cn(
|
|
82
|
-
'modal-content
|
|
118
|
+
'modal-content flex max-h-[calc(100dvh-2rem)] w-full flex-col rounded-xl border border-border bg-card text-card-foreground shadow-sm',
|
|
119
|
+
CENTER_WIDTH[props.size ?? 'md'],
|
|
83
120
|
props.class,
|
|
84
121
|
)}
|
|
85
122
|
>
|
|
86
123
|
<Show when={props.title}>
|
|
87
|
-
<div class="flex items-center justify-between border-b border-border p-6 pb-4">
|
|
124
|
+
<div class="flex shrink-0 items-center justify-between border-b border-border p-6 pb-4">
|
|
88
125
|
<Dialog.Title class="text-lg font-semibold leading-none tracking-tight">
|
|
89
126
|
{props.title}
|
|
90
127
|
</Dialog.Title>
|
|
@@ -96,11 +133,13 @@ export function Modal(props: ModalProps): JSX.Element {
|
|
|
96
133
|
</Dialog.CloseButton>
|
|
97
134
|
</div>
|
|
98
135
|
</Show>
|
|
99
|
-
<div class="p-6 pt-4">
|
|
136
|
+
<div data-modal-body class="min-h-0 flex-1 overflow-y-auto overscroll-contain p-6 pt-4">
|
|
137
|
+
{props.children}
|
|
138
|
+
</div>
|
|
100
139
|
</Dialog.Content>
|
|
101
140
|
</div>
|
|
102
141
|
</Show>
|
|
103
142
|
</Dialog.Portal>
|
|
104
|
-
</Dialog>
|
|
143
|
+
</Dialog.Root>
|
|
105
144
|
)
|
|
106
145
|
}
|
package/src/ui/MultiSelect.tsx
CHANGED
|
@@ -188,7 +188,8 @@ export function MultiSelect(props: MultiSelectProps): JSX.Element {
|
|
|
188
188
|
left: `${pos().left}px`,
|
|
189
189
|
width: `${pos().width}px`,
|
|
190
190
|
}}
|
|
191
|
-
|
|
191
|
+
data-a4-floating
|
|
192
|
+
class="pointer-events-auto z-50 min-w-56 rounded-lg border border-border bg-card p-1 text-card-foreground shadow-lg"
|
|
192
193
|
>
|
|
193
194
|
<Show when={props.searchable ?? true}>
|
|
194
195
|
<input
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { ChevronDown } from 'lucide-solid'
|
|
2
|
+
import { createEffect, splitProps, type JSX } from 'solid-js'
|
|
3
|
+
|
|
4
|
+
import { cn } from '../lib/cn'
|
|
5
|
+
|
|
6
|
+
export type NativeSelectSize = 'sm' | 'default'
|
|
7
|
+
|
|
8
|
+
interface NativeSelectProps extends Omit<JSX.SelectHTMLAttributes<HTMLSelectElement>, 'size'> {
|
|
9
|
+
/** @default 'default' */
|
|
10
|
+
size?: NativeSelectSize
|
|
11
|
+
/** Marks the field invalid (destructive border + `aria-invalid`). */
|
|
12
|
+
invalid?: boolean
|
|
13
|
+
/** Class for the `<select>`; use `wrapperClass` for the outer box. */
|
|
14
|
+
class?: string
|
|
15
|
+
wrapperClass?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type { NativeSelectProps }
|
|
19
|
+
|
|
20
|
+
const SIZE: Record<NativeSelectSize, string> = {
|
|
21
|
+
default: 'py-2 text-sm',
|
|
22
|
+
sm: 'py-1 text-xs',
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Styled native `<select>` — gets the OS picker on mobile. Takes `<option>` and
|
|
27
|
+
* `<optgroup>` children; `value`, `onChange` and every other select attribute
|
|
28
|
+
* pass straight through.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```tsx
|
|
32
|
+
* <NativeSelect value={plan()} onChange={(e) => setPlan(e.currentTarget.value)} aria-label="Plan">
|
|
33
|
+
* <option value="free">Free</option>
|
|
34
|
+
* <option value="pro">Pro</option>
|
|
35
|
+
* </NativeSelect>
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export function NativeSelect(props: NativeSelectProps): JSX.Element {
|
|
39
|
+
const [local, rest] = splitProps(props, ['size', 'invalid', 'class', 'wrapperClass', 'children', 'value'])
|
|
40
|
+
let select: HTMLSelectElement | undefined
|
|
41
|
+
// Set after mount: a `value` attribute applied before the <option> children exist is ignored.
|
|
42
|
+
createEffect(() => {
|
|
43
|
+
if (select && local.value !== undefined) select.value = String(local.value)
|
|
44
|
+
})
|
|
45
|
+
return (
|
|
46
|
+
<div class={cn('relative w-full', local.wrapperClass)}>
|
|
47
|
+
<select
|
|
48
|
+
ref={select}
|
|
49
|
+
aria-invalid={local.invalid ? 'true' : undefined}
|
|
50
|
+
class={cn(
|
|
51
|
+
'peer w-full appearance-none rounded-md border bg-background pl-3 pr-9 text-foreground outline-none transition-colors a4-field disabled:cursor-not-allowed disabled:opacity-50',
|
|
52
|
+
local.invalid ? 'border-destructive' : 'border-input',
|
|
53
|
+
SIZE[local.size ?? 'default'],
|
|
54
|
+
local.class,
|
|
55
|
+
)}
|
|
56
|
+
{...rest}
|
|
57
|
+
>
|
|
58
|
+
{local.children}
|
|
59
|
+
</select>
|
|
60
|
+
<ChevronDown
|
|
61
|
+
aria-hidden="true"
|
|
62
|
+
class="pointer-events-none absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground peer-disabled:opacity-50"
|
|
63
|
+
/>
|
|
64
|
+
</div>
|
|
65
|
+
)
|
|
66
|
+
}
|