@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
@@ -1,13 +1,21 @@
1
- // Message — one chat message row, full-width (no bubble). Rich content is
2
- // passed as children so the caller can render markdown, code blocks, lists,
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
- * Full-width chat message row with no bubble: an optional avatar/author
25
- * header line followed by a prose content block. Role is differentiated with
26
- * subtle, token-based treatment only — `assistant` gets a faint muted panel,
27
- * `user` stays plain, `system` is a small centered note.
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 class={cn('text-center text-xs text-muted-foreground italic', props.class)}>
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
- props.role === 'assistant' && 'rounded-xl bg-muted/40 p-3',
49
- props.role === 'user' && 'rounded-xl bg-primary/5 p-3',
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 text-foreground">{props.children}</div>
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 max-w-[520px] flex-col overflow-y-auto border-l border-border text-card-foreground shadow-2xl',
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 w-full max-w-lg rounded-xl border border-border bg-card text-card-foreground shadow-sm',
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">{props.children}</div>
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>
@@ -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
- class="z-50 min-w-56 rounded-lg border border-border bg-card p-1 text-card-foreground shadow-lg"
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"