@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
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
|
@@ -12,7 +12,7 @@ import { Show } from 'solid-js'
|
|
|
12
12
|
|
|
13
13
|
import { cn } from '../lib/cn'
|
|
14
14
|
|
|
15
|
-
interface ModalProps extends ParentProps {
|
|
15
|
+
export interface ModalProps extends ParentProps {
|
|
16
16
|
open: boolean
|
|
17
17
|
onOpenChange: (open: boolean) => void
|
|
18
18
|
/** Header title; omitting it hides the whole header bar. */
|
|
@@ -24,11 +24,41 @@ interface ModalProps extends ParentProps {
|
|
|
24
24
|
* @default 'drawer'
|
|
25
25
|
*/
|
|
26
26
|
variant?: 'drawer' | 'center'
|
|
27
|
+
/**
|
|
28
|
+
* Width of the dialog: `'sm'` 24rem, `'md'` 32rem, `'lg'` 42rem, `'xl'` 56rem,
|
|
29
|
+
* `'full'` the whole viewport minus margin. Omitted keeps the historical width
|
|
30
|
+
* (`variant="center"`: 32rem, `variant="drawer"`: 520px). Content taller than
|
|
31
|
+
* the viewport scrolls inside the dialog; the header stays pinned.
|
|
32
|
+
*/
|
|
33
|
+
size?: 'sm' | 'md' | 'lg' | 'xl' | 'full'
|
|
27
34
|
class?: string
|
|
28
35
|
/** Accessible label for the close button. @default 'Close' */
|
|
29
36
|
closeLabel?: string
|
|
30
37
|
}
|
|
31
38
|
|
|
39
|
+
const CENTER_WIDTH = {
|
|
40
|
+
sm: 'max-w-sm',
|
|
41
|
+
md: 'max-w-lg',
|
|
42
|
+
lg: 'max-w-2xl',
|
|
43
|
+
xl: 'max-w-4xl',
|
|
44
|
+
full: 'max-w-full',
|
|
45
|
+
} as const
|
|
46
|
+
const DRAWER_WIDTH = {
|
|
47
|
+
sm: 'max-w-sm',
|
|
48
|
+
md: 'max-w-[520px]',
|
|
49
|
+
lg: 'max-w-2xl',
|
|
50
|
+
xl: 'max-w-4xl',
|
|
51
|
+
full: 'max-w-full',
|
|
52
|
+
} as const
|
|
53
|
+
|
|
54
|
+
// Popovers inside the dialog (MultiSelect, ...) are portaled to <body>, so
|
|
55
|
+
// Kobalte sees a click on them as "outside" and would close the dialog. Those
|
|
56
|
+
// panels carry `data-a4-floating`; interacting with one is not a dismissal.
|
|
57
|
+
function keepForFloating(e: Event): void {
|
|
58
|
+
const target = (e as CustomEvent<{ originalEvent?: Event }>).detail?.originalEvent?.target ?? e.target
|
|
59
|
+
if (target instanceof Element && target.closest('[data-a4-floating]')) e.preventDefault()
|
|
60
|
+
}
|
|
61
|
+
|
|
32
62
|
/**
|
|
33
63
|
* Dialog on Kobalte's `Dialog` primitive (focus trap, portal, presence handled
|
|
34
64
|
* for you), rendered as either a slide-over drawer or a centered modal depending
|
|
@@ -53,8 +83,10 @@ export function Modal(props: ModalProps): JSX.Element {
|
|
|
53
83
|
fallback={
|
|
54
84
|
<div class="fixed inset-0 z-50 flex justify-end">
|
|
55
85
|
<Dialog.Content
|
|
86
|
+
onInteractOutside={keepForFloating}
|
|
56
87
|
class={cn(
|
|
57
|
-
'drawer-content bg-glass flex h-full w-full
|
|
88
|
+
'drawer-content bg-glass flex h-full w-full flex-col overflow-y-auto border-l border-border text-card-foreground shadow-2xl',
|
|
89
|
+
DRAWER_WIDTH[props.size ?? 'md'],
|
|
58
90
|
props.class,
|
|
59
91
|
)}
|
|
60
92
|
>
|
|
@@ -78,13 +110,15 @@ export function Modal(props: ModalProps): JSX.Element {
|
|
|
78
110
|
>
|
|
79
111
|
<div class="fixed inset-0 z-50 flex items-center justify-center p-4">
|
|
80
112
|
<Dialog.Content
|
|
113
|
+
onInteractOutside={keepForFloating}
|
|
81
114
|
class={cn(
|
|
82
|
-
'modal-content
|
|
115
|
+
'modal-content flex max-h-[calc(100dvh-2rem)] w-full flex-col rounded-xl border border-border bg-card text-card-foreground shadow-sm',
|
|
116
|
+
CENTER_WIDTH[props.size ?? 'md'],
|
|
83
117
|
props.class,
|
|
84
118
|
)}
|
|
85
119
|
>
|
|
86
120
|
<Show when={props.title}>
|
|
87
|
-
<div class="flex items-center justify-between border-b border-border p-6 pb-4">
|
|
121
|
+
<div class="flex shrink-0 items-center justify-between border-b border-border p-6 pb-4">
|
|
88
122
|
<Dialog.Title class="text-lg font-semibold leading-none tracking-tight">
|
|
89
123
|
{props.title}
|
|
90
124
|
</Dialog.Title>
|
|
@@ -96,7 +130,9 @@ export function Modal(props: ModalProps): JSX.Element {
|
|
|
96
130
|
</Dialog.CloseButton>
|
|
97
131
|
</div>
|
|
98
132
|
</Show>
|
|
99
|
-
<div class="p-6 pt-4">
|
|
133
|
+
<div data-modal-body class="min-h-0 flex-1 overflow-y-auto overscroll-contain p-6 pt-4">
|
|
134
|
+
{props.children}
|
|
135
|
+
</div>
|
|
100
136
|
</Dialog.Content>
|
|
101
137
|
</div>
|
|
102
138
|
</Show>
|
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
|
+
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
// Site navigation with rich dropdown panels on Kobalte's NavigationMenu
|
|
2
|
+
// primitive. Desktop: triggers open panels rendered into ONE shared viewport
|
|
3
|
+
// that animates its size between panels. Mobile (< md): collapses to a
|
|
4
|
+
// hamburger button that opens a Drawer with an Accordion of the sections.
|
|
5
|
+
import { NavigationMenu as KNav } from '@kobalte/core/navigation-menu'
|
|
6
|
+
import { ChevronDown, Menu as MenuIcon } from 'lucide-solid'
|
|
7
|
+
import type { JSX, ParentProps } from 'solid-js'
|
|
8
|
+
import { createSignal, For, Show } from 'solid-js'
|
|
9
|
+
|
|
10
|
+
import { cn } from '../lib/cn'
|
|
11
|
+
import { useMediaQuery } from '../lib/media'
|
|
12
|
+
import { Accordion } from './Accordion'
|
|
13
|
+
import { Button } from './Button'
|
|
14
|
+
import { Drawer } from './Drawer'
|
|
15
|
+
|
|
16
|
+
const TRIGGER =
|
|
17
|
+
'inline-flex items-center gap-1 rounded-lg px-3 py-2 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'
|
|
18
|
+
|
|
19
|
+
/** One section of the collapsed (mobile) navigation. */
|
|
20
|
+
export interface NavigationMenuMobileSection {
|
|
21
|
+
/** Section title shown on the accordion trigger (or the link label). */
|
|
22
|
+
label: string
|
|
23
|
+
/** Destination when the section is a plain link with no children. */
|
|
24
|
+
href?: string
|
|
25
|
+
/** Child links; when present the section becomes an accordion entry. */
|
|
26
|
+
links?: { title: string; href: string; description?: string }[]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Props of {@link NavigationMenu}. */
|
|
30
|
+
export interface NavigationMenuProps extends ParentProps {
|
|
31
|
+
/** Accessible name of the navigation landmark. @default 'Main' */
|
|
32
|
+
label?: string
|
|
33
|
+
/**
|
|
34
|
+
* The same sections as `children`, as data. Used below the `md` breakpoint,
|
|
35
|
+
* where the menu collapses into a hamburger + drawer. Required: the mobile
|
|
36
|
+
* layout cannot be derived from the desktop JSX.
|
|
37
|
+
*/
|
|
38
|
+
mobileSections: NavigationMenuMobileSection[]
|
|
39
|
+
/** Drawer title. @default 'Menu' */
|
|
40
|
+
mobileTitle?: string
|
|
41
|
+
/** Accessible label of the hamburger button. @default 'Open navigation' */
|
|
42
|
+
mobileButtonLabel?: string
|
|
43
|
+
class?: string
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Site navigation bar whose triggers open rich panels (links with icon, title
|
|
48
|
+
* and description, featured cards) inside a shared, size-animating viewport.
|
|
49
|
+
* Hover or click opens a panel, Escape closes it, and the links inside are
|
|
50
|
+
* reachable with Tab. The root is the `<ul>` list of items.
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```tsx
|
|
54
|
+
* <NavigationMenu mobileSections={[{ label: 'Docs', href: '/docs' }]}>
|
|
55
|
+
* <NavigationMenuItem>
|
|
56
|
+
* <NavigationMenuTrigger>Product</NavigationMenuTrigger>
|
|
57
|
+
* <NavigationMenuContent>
|
|
58
|
+
* <NavigationMenuPanelLink href="/tour" title="Tour" description="A quick walkthrough" />
|
|
59
|
+
* </NavigationMenuContent>
|
|
60
|
+
* </NavigationMenuItem>
|
|
61
|
+
* <NavigationMenuLink href="/docs">Docs</NavigationMenuLink>
|
|
62
|
+
* </NavigationMenu>
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
export function NavigationMenu(props: NavigationMenuProps): JSX.Element {
|
|
66
|
+
const isDesktop = useMediaQuery('(min-width: 768px)')
|
|
67
|
+
return (
|
|
68
|
+
<Show when={isDesktop()} fallback={<MobileNavigation {...props} />}>
|
|
69
|
+
<KNav aria-label={props.label ?? 'Main'} class={cn('relative flex items-center gap-1', props.class)}>
|
|
70
|
+
{props.children}
|
|
71
|
+
<KNav.Viewport class="navmenu-viewport bg-glass relative z-50 mt-2 h-[var(--kb-navigation-menu-viewport-height)] w-[var(--kb-navigation-menu-viewport-width)] overflow-hidden rounded-2xl shadow-2xl transition-[width,height] duration-300 ease-out">
|
|
72
|
+
<KNav.Arrow class="hidden" />
|
|
73
|
+
</KNav.Viewport>
|
|
74
|
+
</KNav>
|
|
75
|
+
</Show>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function MobileNavigation(props: NavigationMenuProps): JSX.Element {
|
|
80
|
+
const [open, setOpen] = createSignal(false)
|
|
81
|
+
const withLinks = () => props.mobileSections.filter((s) => s.links?.length)
|
|
82
|
+
const plain = () => props.mobileSections.filter((s) => !s.links?.length)
|
|
83
|
+
return (
|
|
84
|
+
<nav aria-label={props.label ?? 'Main'} class={props.class}>
|
|
85
|
+
<Button
|
|
86
|
+
variant="outline"
|
|
87
|
+
aria-label={props.mobileButtonLabel ?? 'Open navigation'}
|
|
88
|
+
aria-haspopup="dialog"
|
|
89
|
+
aria-expanded={open()}
|
|
90
|
+
onClick={() => setOpen(true)}
|
|
91
|
+
>
|
|
92
|
+
<MenuIcon class="h-4 w-4" />
|
|
93
|
+
</Button>
|
|
94
|
+
<Drawer side="left" open={open()} onOpenChange={setOpen} title={props.mobileTitle ?? 'Menu'}>
|
|
95
|
+
<div
|
|
96
|
+
class="p-5"
|
|
97
|
+
onKeyDown={(e) => {
|
|
98
|
+
// Escape typed while an accordion trigger has focus does not reach
|
|
99
|
+
// the dialog's own dismiss handler, so close explicitly.
|
|
100
|
+
if (e.key === 'Escape') setOpen(false)
|
|
101
|
+
}}
|
|
102
|
+
>
|
|
103
|
+
<Accordion
|
|
104
|
+
items={withLinks().map((s) => ({
|
|
105
|
+
value: s.label,
|
|
106
|
+
title: s.label,
|
|
107
|
+
content: (
|
|
108
|
+
<ul class="space-y-1">
|
|
109
|
+
<For each={s.links}>
|
|
110
|
+
{(l) => (
|
|
111
|
+
<li>
|
|
112
|
+
<a
|
|
113
|
+
href={l.href}
|
|
114
|
+
class="block rounded-md px-2 py-1.5 outline-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring"
|
|
115
|
+
>
|
|
116
|
+
<span class="block text-sm font-medium text-foreground">{l.title}</span>
|
|
117
|
+
<Show when={l.description}>
|
|
118
|
+
<span class="block text-xs text-muted-foreground">{l.description}</span>
|
|
119
|
+
</Show>
|
|
120
|
+
</a>
|
|
121
|
+
</li>
|
|
122
|
+
)}
|
|
123
|
+
</For>
|
|
124
|
+
</ul>
|
|
125
|
+
),
|
|
126
|
+
}))}
|
|
127
|
+
/>
|
|
128
|
+
<ul class="mt-2 space-y-1">
|
|
129
|
+
<For each={plain()}>
|
|
130
|
+
{(s) => (
|
|
131
|
+
<li>
|
|
132
|
+
<a
|
|
133
|
+
href={s.href}
|
|
134
|
+
class="block rounded-md px-2 py-3 text-sm font-medium text-foreground outline-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring"
|
|
135
|
+
>
|
|
136
|
+
{s.label}
|
|
137
|
+
</a>
|
|
138
|
+
</li>
|
|
139
|
+
)}
|
|
140
|
+
</For>
|
|
141
|
+
</ul>
|
|
142
|
+
</div>
|
|
143
|
+
</Drawer>
|
|
144
|
+
</nav>
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** One top-level entry: pairs a trigger with its panel. */
|
|
149
|
+
export function NavigationMenuItem(props: ParentProps): JSX.Element {
|
|
150
|
+
return <KNav.Menu>{props.children}</KNav.Menu>
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Button that opens the sibling {@link NavigationMenuContent}; shows a chevron. */
|
|
154
|
+
export function NavigationMenuTrigger(props: ParentProps & { class?: string }): JSX.Element {
|
|
155
|
+
return (
|
|
156
|
+
<KNav.Trigger class={cn(TRIGGER, 'group', props.class)}>
|
|
157
|
+
{props.children}
|
|
158
|
+
<KNav.Icon class="transition-transform duration-200 group-data-[expanded]:rotate-180">
|
|
159
|
+
<ChevronDown class="h-3.5 w-3.5" />
|
|
160
|
+
</KNav.Icon>
|
|
161
|
+
</KNav.Trigger>
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Props of {@link NavigationMenuContent}. */
|
|
166
|
+
export interface NavigationMenuContentProps extends ParentProps {
|
|
167
|
+
class?: string
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* The panel of one item. Rendered inside the shared viewport, which resizes to
|
|
172
|
+
* fit it. Put {@link NavigationMenuPanelLink}s in it, laid out with a grid.
|
|
173
|
+
*/
|
|
174
|
+
export function NavigationMenuContent(props: NavigationMenuContentProps): JSX.Element {
|
|
175
|
+
// Kobalte's menu content swallows Tab, which would make the links unreachable
|
|
176
|
+
// with a plain Tab press. Walk the panel's links with Tab / Shift+Tab, and
|
|
177
|
+
// when the end is passed close the menu (focus returns to its trigger) so
|
|
178
|
+
// the key never traps focus.
|
|
179
|
+
const onTab = (e: KeyboardEvent & { currentTarget: HTMLElement }): void => {
|
|
180
|
+
if (e.key !== 'Tab') return
|
|
181
|
+
const links = Array.from(e.currentTarget.querySelectorAll<HTMLElement>('a[role="menuitem"]'))
|
|
182
|
+
const at = links.indexOf(document.activeElement as HTMLElement)
|
|
183
|
+
const next = at + (e.shiftKey ? -1 : 1)
|
|
184
|
+
e.preventDefault()
|
|
185
|
+
// Deferred: Kobalte's own Shift+Tab handling re-focuses the panel right after this handler.
|
|
186
|
+
const target = links[next]
|
|
187
|
+
if (target) setTimeout(() => target.focus(), 0)
|
|
188
|
+
else e.currentTarget.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))
|
|
189
|
+
}
|
|
190
|
+
return (
|
|
191
|
+
<KNav.Portal>
|
|
192
|
+
<KNav.Content
|
|
193
|
+
class={cn(
|
|
194
|
+
'navmenu-content absolute left-0 top-0 w-max max-w-[min(90vw,40rem)] p-3 outline-none',
|
|
195
|
+
props.class,
|
|
196
|
+
)}
|
|
197
|
+
onKeyDown={onTab}
|
|
198
|
+
>
|
|
199
|
+
{props.children}
|
|
200
|
+
</KNav.Content>
|
|
201
|
+
</KNav.Portal>
|
|
202
|
+
)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Props of {@link NavigationMenuLink}. */
|
|
206
|
+
export interface NavigationMenuLinkProps extends ParentProps {
|
|
207
|
+
href: string
|
|
208
|
+
class?: string
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** A top-level link with no panel; takes part in the trigger roving focus. */
|
|
212
|
+
export function NavigationMenuLink(props: NavigationMenuLinkProps): JSX.Element {
|
|
213
|
+
return (
|
|
214
|
+
<KNav.Menu value={`link-trigger-${props.href}`}>
|
|
215
|
+
<KNav.Trigger as="a" href={props.href} class={cn(TRIGGER, props.class)}>
|
|
216
|
+
{props.children}
|
|
217
|
+
</KNav.Trigger>
|
|
218
|
+
</KNav.Menu>
|
|
219
|
+
)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Props of {@link NavigationMenuPanelLink}. */
|
|
223
|
+
export interface NavigationMenuPanelLinkProps {
|
|
224
|
+
href: string
|
|
225
|
+
title: string
|
|
226
|
+
description?: string
|
|
227
|
+
/** Leading icon element (e.g. a lucide icon). */
|
|
228
|
+
icon?: JSX.Element
|
|
229
|
+
/** Renders as a large highlighted card, typically spanning several grid rows. */
|
|
230
|
+
featured?: boolean
|
|
231
|
+
class?: string
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** A link inside a panel: icon + title + description, or a featured card. */
|
|
235
|
+
export function NavigationMenuPanelLink(props: NavigationMenuPanelLinkProps): JSX.Element {
|
|
236
|
+
return (
|
|
237
|
+
<KNav.Item
|
|
238
|
+
href={props.href}
|
|
239
|
+
class={cn(
|
|
240
|
+
'flex gap-3 rounded-xl p-3 text-left no-underline outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring',
|
|
241
|
+
props.featured
|
|
242
|
+
? 'flex-col justify-end bg-gradient-to-br from-primary/30 to-accent/20 hover:from-primary/40'
|
|
243
|
+
: 'items-start hover:bg-muted data-[highlighted]:bg-muted',
|
|
244
|
+
props.class,
|
|
245
|
+
)}
|
|
246
|
+
>
|
|
247
|
+
<Show when={props.icon}>
|
|
248
|
+
<span class="mt-0.5 grid h-8 w-8 shrink-0 place-items-center rounded-lg bg-primary/15 text-primary">
|
|
249
|
+
{props.icon}
|
|
250
|
+
</span>
|
|
251
|
+
</Show>
|
|
252
|
+
<span class="min-w-0">
|
|
253
|
+
<span class={cn('block font-medium text-foreground', props.featured ? 'text-base' : 'text-sm')}>
|
|
254
|
+
{props.title}
|
|
255
|
+
</span>
|
|
256
|
+
<Show when={props.description}>
|
|
257
|
+
<span class="mt-0.5 block text-xs leading-snug text-muted-foreground">{props.description}</span>
|
|
258
|
+
</Show>
|
|
259
|
+
</span>
|
|
260
|
+
</KNav.Item>
|
|
261
|
+
)
|
|
262
|
+
}
|
|
@@ -18,6 +18,11 @@ export interface PromptComposerProps {
|
|
|
18
18
|
/** Attachment labels/filenames shown as removable chips above the input. */
|
|
19
19
|
attachments?: string[]
|
|
20
20
|
onRemoveAttachment?: (index: number) => void
|
|
21
|
+
/**
|
|
22
|
+
* Rich attachment slot rendered above the input (alongside the chips above).
|
|
23
|
+
* Pass `Attachment`s, ideally wrapped in an `AttachmentGroup`.
|
|
24
|
+
*/
|
|
25
|
+
attachmentItems?: JSX.Element
|
|
21
26
|
/** When set, shows a paperclip button that calls this on click. */
|
|
22
27
|
onAttach?: () => void
|
|
23
28
|
class?: string
|
|
@@ -92,6 +97,12 @@ export function PromptComposer(props: PromptComposerProps): JSX.Element {
|
|
|
92
97
|
</div>
|
|
93
98
|
</Show>
|
|
94
99
|
|
|
100
|
+
<Show when={props.attachmentItems}>
|
|
101
|
+
<div data-slot="composer-attachments" class="mb-2">
|
|
102
|
+
{props.attachmentItems}
|
|
103
|
+
</div>
|
|
104
|
+
</Show>
|
|
105
|
+
|
|
95
106
|
<textarea
|
|
96
107
|
ref={textareaRef}
|
|
97
108
|
aria-label="Message"
|