@torch-ui/solid 0.1.3

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 (118) hide show
  1. package/README.md +166 -0
  2. package/package.json +67 -0
  3. package/src/components/actions/Button.tsx +612 -0
  4. package/src/components/actions/ButtonGroup.tsx +728 -0
  5. package/src/components/actions/Copy.tsx +98 -0
  6. package/src/components/actions/DarkModeToggle.tsx +80 -0
  7. package/src/components/actions/Link.tsx +37 -0
  8. package/src/components/actions/index.ts +19 -0
  9. package/src/components/actions/useCopyToClipboard.ts +90 -0
  10. package/src/components/charts/Chart.tsx +331 -0
  11. package/src/components/charts/Sparkline.tsx +156 -0
  12. package/src/components/charts/index.ts +13 -0
  13. package/src/components/data-display/Avatar.tsx +208 -0
  14. package/src/components/data-display/AvatarGroup.tsx +228 -0
  15. package/src/components/data-display/Badge.tsx +70 -0
  16. package/src/components/data-display/Carousel.tsx +214 -0
  17. package/src/components/data-display/ColorSwatch.tsx +56 -0
  18. package/src/components/data-display/DataTable.tsx +886 -0
  19. package/src/components/data-display/EmptyState.tsx +61 -0
  20. package/src/components/data-display/Image.tsx +277 -0
  21. package/src/components/data-display/Kbd.tsx +114 -0
  22. package/src/components/data-display/Persona.tsx +78 -0
  23. package/src/components/data-display/StatCard.tsx +338 -0
  24. package/src/components/data-display/Table.tsx +147 -0
  25. package/src/components/data-display/Tag.tsx +91 -0
  26. package/src/components/data-display/Timeline.tsx +200 -0
  27. package/src/components/data-display/TreeView.tsx +172 -0
  28. package/src/components/data-display/Video.tsx +95 -0
  29. package/src/components/data-display/avatar-utils.ts +32 -0
  30. package/src/components/data-display/index.ts +81 -0
  31. package/src/components/feedback/Loading.tsx +159 -0
  32. package/src/components/feedback/Progress.tsx +321 -0
  33. package/src/components/feedback/Skeleton.tsx +62 -0
  34. package/src/components/feedback/SkeletonBlocks.tsx +222 -0
  35. package/src/components/feedback/Toast.tsx +648 -0
  36. package/src/components/feedback/index.ts +44 -0
  37. package/src/components/feedback/password/PasswordStrengthIndicator.tsx +232 -0
  38. package/src/components/feedback/password/password-strength.ts +115 -0
  39. package/src/components/feedback/password/password-validation-data.ts +66 -0
  40. package/src/components/feedback/password/password-validation.ts +93 -0
  41. package/src/components/forms/Autocomplete.tsx +268 -0
  42. package/src/components/forms/Checkbox.tsx +155 -0
  43. package/src/components/forms/CodeInput.tsx +237 -0
  44. package/src/components/forms/ColorPicker/ColorPicker.tsx +469 -0
  45. package/src/components/forms/ColorPicker/color-utils.ts +75 -0
  46. package/src/components/forms/ColorPicker/index.ts +2 -0
  47. package/src/components/forms/DatePicker.tsx +516 -0
  48. package/src/components/forms/DateRangePicker.tsx +464 -0
  49. package/src/components/forms/FieldPicker.tsx +64 -0
  50. package/src/components/forms/FileUpload.tsx +614 -0
  51. package/src/components/forms/FilterBuilder/FilterGroupBlock.ts +6 -0
  52. package/src/components/forms/FilterBuilder.tsx +16 -0
  53. package/src/components/forms/FilterRuleRow.tsx +68 -0
  54. package/src/components/forms/Input.tsx +200 -0
  55. package/src/components/forms/MultiSelect.tsx +361 -0
  56. package/src/components/forms/NumberField.tsx +145 -0
  57. package/src/components/forms/RadioGroup.tsx +135 -0
  58. package/src/components/forms/RelativeDateDefaultInput.tsx +62 -0
  59. package/src/components/forms/ReorderableList.tsx +163 -0
  60. package/src/components/forms/Select.tsx +268 -0
  61. package/src/components/forms/Slider.tsx +260 -0
  62. package/src/components/forms/Switch.tsx +135 -0
  63. package/src/components/forms/TextArea.tsx +202 -0
  64. package/src/components/forms/ViewCustomizer.tsx +44 -0
  65. package/src/components/forms/index.ts +43 -0
  66. package/src/components/layout/Accordion.tsx +110 -0
  67. package/src/components/layout/Alert.tsx +156 -0
  68. package/src/components/layout/BlockQuote.tsx +70 -0
  69. package/src/components/layout/Card.tsx +166 -0
  70. package/src/components/layout/CodeBlock/CodeBlock.tsx +477 -0
  71. package/src/components/layout/CodeBlock/code-block-tokens.css +104 -0
  72. package/src/components/layout/CodeBlock/prism.ts +81 -0
  73. package/src/components/layout/Collapsible.tsx +84 -0
  74. package/src/components/layout/Container.tsx +55 -0
  75. package/src/components/layout/Divider.tsx +64 -0
  76. package/src/components/layout/Form.tsx +39 -0
  77. package/src/components/layout/FormActions.tsx +50 -0
  78. package/src/components/layout/Grid.tsx +53 -0
  79. package/src/components/layout/PageHeading.tsx +46 -0
  80. package/src/components/layout/PromptWithAction.tsx +49 -0
  81. package/src/components/layout/Section.tsx +60 -0
  82. package/src/components/layout/TablePanel.tsx +24 -0
  83. package/src/components/layout/TableView/TableView.tsx +1018 -0
  84. package/src/components/layout/TableView/index.ts +3 -0
  85. package/src/components/layout/TableView/types.ts +51 -0
  86. package/src/components/layout/WizardStep.tsx +40 -0
  87. package/src/components/layout/WizardStepper.tsx +173 -0
  88. package/src/components/layout/index.ts +96 -0
  89. package/src/components/navigation/Breadcrumbs.tsx +66 -0
  90. package/src/components/navigation/DropdownMenu.tsx +86 -0
  91. package/src/components/navigation/MegaMenu.tsx +480 -0
  92. package/src/components/navigation/NavigationMenu.tsx +305 -0
  93. package/src/components/navigation/Pagination.tsx +298 -0
  94. package/src/components/navigation/Sidebar.tsx +280 -0
  95. package/src/components/navigation/Tabs.tsx +122 -0
  96. package/src/components/navigation/ViewSwitcher.tsx +314 -0
  97. package/src/components/navigation/index.ts +66 -0
  98. package/src/components/overlays/AlertDialog.tsx +174 -0
  99. package/src/components/overlays/ContextMenu.tsx +65 -0
  100. package/src/components/overlays/Dialog.tsx +279 -0
  101. package/src/components/overlays/Drawer.tsx +370 -0
  102. package/src/components/overlays/HoverCard.tsx +107 -0
  103. package/src/components/overlays/Popover.tsx +73 -0
  104. package/src/components/overlays/Tooltip.tsx +31 -0
  105. package/src/components/overlays/index.ts +71 -0
  106. package/src/components/typography/Code.tsx +72 -0
  107. package/src/components/typography/Icon.tsx +36 -0
  108. package/src/components/typography/index.ts +10 -0
  109. package/src/env.d.ts +9 -0
  110. package/src/index.ts +13 -0
  111. package/src/styles/theme.css +226 -0
  112. package/src/types/avatar-types.ts +11 -0
  113. package/src/types/filter-types.ts +35 -0
  114. package/src/utilities/classNames.ts +6 -0
  115. package/src/utilities/componentSize.ts +46 -0
  116. package/src/utilities/i18n.tsx +60 -0
  117. package/src/utilities/mergeRefs.ts +12 -0
  118. package/src/utilities/relativeDateDefault.ts +14 -0
@@ -0,0 +1,480 @@
1
+ import { type JSX, Show, splitProps, onMount, createContext, useContext } from 'solid-js'
2
+ import { Dynamic } from 'solid-js/web'
3
+ import { NavigationMenu as KobalteNavigationMenu } from '@kobalte/core/navigation-menu'
4
+ import { ChevronDown } from 'lucide-solid'
5
+ import { cn } from '../../utilities/classNames'
6
+
7
+ /** ─── Variant context ───────────────────────────────────────────────────────── */
8
+ type MenuVariant = 'default' | 'underline' | 'ghost'
9
+ const VariantContext = createContext<MenuVariant>('default')
10
+
11
+ /** ─── Injected styles ───────────────────────────────────────────────────────── */
12
+ function injectMegaMenuStyles() {
13
+ const id = 'torchui-mega-menu-styles'
14
+ if (typeof document === 'undefined') return
15
+ let el = document.getElementById(id) as HTMLStyleElement | null
16
+ if (!el) {
17
+ el = document.createElement('style')
18
+ el.id = id
19
+ document.head.appendChild(el)
20
+ }
21
+ el.textContent = `
22
+ .torchui-mm-viewport {
23
+ transform-origin: var(--kb-menu-content-transform-origin);
24
+ pointer-events: none;
25
+ opacity: 0;
26
+ overflow-x: clip;
27
+ overflow-y: visible;
28
+ transition: opacity 180ms ease, height 200ms ease, width 200ms ease;
29
+ }
30
+ .torchui-mm-viewport[data-expanded] { pointer-events: auto; opacity: 1; }
31
+ .torchui-mm-content {
32
+ position: absolute;
33
+ top: 0;
34
+ left: 0;
35
+ z-index: 1;
36
+ animation-duration: 100ms;
37
+ animation-timing-function: ease;
38
+ animation-fill-mode: forwards;
39
+ pointer-events: none;
40
+ }
41
+ .torchui-mm-content:not([data-expanded]):not([data-motion]) { opacity: 0; pointer-events: none; }
42
+ .torchui-mm-content[data-expanded] { pointer-events: auto; z-index: 2; }
43
+ .torchui-mm-content[data-motion="from-end"] { animation-name: torchui-mm-from-end; z-index: 2; }
44
+ .torchui-mm-content[data-motion="from-start"] { animation-name: torchui-mm-from-start; z-index: 2; }
45
+ .torchui-mm-content[data-motion="to-end"] { animation-name: torchui-mm-to-end; z-index: 1; }
46
+ .torchui-mm-content[data-motion="to-start"] { animation-name: torchui-mm-to-start; z-index: 1; }
47
+ @keyframes torchui-mm-from-end { from { opacity: 0; transform: translateX( 20px) } to { opacity: 1; transform: translateX(0) } }
48
+ @keyframes torchui-mm-from-start { from { opacity: 0; transform: translateX(-20px) } to { opacity: 1; transform: translateX(0) } }
49
+ @keyframes torchui-mm-to-end { from { opacity: 1; transform: translateX(0) } to { opacity: 0; transform: translateX( 20px) } }
50
+ @keyframes torchui-mm-to-start { from { opacity: 1; transform: translateX(0) } to { opacity: 0; transform: translateX(-20px) } }
51
+ .torchui-mm-root { display: flex; gap: 0.25rem; position: relative; height: 100%; align-items: stretch; width: max-content; }
52
+ .torchui-mm-root > div { height: 100%; }
53
+ .torchui-mm-root > div > li { height: 100%; display: flex; }
54
+ .torchui-mm-root[data-variant="underline"] { align-items: stretch; }
55
+ .torchui-mm-root[data-variant="default"],
56
+ .torchui-mm-root[data-variant="ghost"] { align-items: center; }
57
+ .torchui-mm-root ul[role="menubar"] { display: flex; gap: inherit; align-items: inherit; }
58
+ .torchui-mm-root[data-variant="underline"] button[role="menuitem"],
59
+ .torchui-mm-root[data-variant="underline"] a { padding-top: 2px; }
60
+ `
61
+ }
62
+
63
+ /** ─── MegaMenuBar ───────────────────────────────────────────────────────────── */
64
+ export interface MegaMenuBarProps {
65
+ class?: string
66
+ children?: JSX.Element
67
+ /** Visual variant applied to all triggers and bar links. Default: 'default' */
68
+ variant?: MenuVariant
69
+ /** Stretch the dropdown to full viewport width */
70
+ fullWidth?: boolean
71
+ /** Horizontal alignment of nav items */
72
+ justify?: 'start' | 'center' | 'end'
73
+ /** Standard Kobalte NavigationMenu props */
74
+ id?: string
75
+ disabled?: boolean
76
+ orientation?: 'horizontal' | 'vertical'
77
+ }
78
+
79
+ export function MegaMenuBar(props: MegaMenuBarProps) {
80
+ const [local, others] = splitProps(props, ['class', 'children', 'variant', 'fullWidth', 'justify'])
81
+ const variant = () => local.variant ?? 'default'
82
+ const isUnderline = () => variant() === 'underline'
83
+
84
+ onMount(injectMegaMenuStyles)
85
+
86
+ return (
87
+ <div class={cn(
88
+ 'relative flex h-full self-stretch',
89
+ variant() === 'underline' ? 'items-stretch' : 'items-center',
90
+ local.justify === 'center' && 'justify-center',
91
+ local.justify === 'end' && 'justify-end',
92
+ local.justify && 'w-full',
93
+ local.class,
94
+ )}>
95
+ <KobalteNavigationMenu
96
+ data-variant={variant()}
97
+ class="torchui-mm-root"
98
+ {...others}
99
+ >
100
+ <VariantContext.Provider value={variant()}>
101
+ {local.children}
102
+ </VariantContext.Provider>
103
+
104
+ {/* Viewport anchor */}
105
+ <div
106
+ class={cn(
107
+ 'absolute top-full z-[9999] pointer-events-none',
108
+ local.fullWidth
109
+ ? 'left-1/2 -translate-x-1/2 w-[100vw] max-w-[100vw] flex justify-center'
110
+ : 'left-0 flex w-full justify-center',
111
+ )}
112
+ style={{ perspective: '800px' }}
113
+ >
114
+ <KobalteNavigationMenu.Viewport
115
+ class={cn(
116
+ 'torchui-mm-viewport',
117
+ 'relative border border-surface-border bg-surface-raised shadow-lg',
118
+ isUnderline() ? 'mt-0' : 'mt-3',
119
+ local.fullWidth
120
+ ? 'w-screen rounded-none h-[var(--kb-navigation-menu-viewport-height)]'
121
+ : 'rounded-xl h-[var(--kb-navigation-menu-viewport-height)] w-[var(--kb-navigation-menu-viewport-width)]',
122
+ )}
123
+ >
124
+ <KobalteNavigationMenu.Arrow />
125
+ </KobalteNavigationMenu.Viewport>
126
+ </div>
127
+ </KobalteNavigationMenu>
128
+ </div>
129
+ )
130
+ }
131
+
132
+ /** ─── MegaMenuMenu ──────────────────────────────────────────────────────────── */
133
+ export function MegaMenuMenu(props: { class?: string } & Record<string, any>) {
134
+ const [local, others] = splitProps(props, ['class'])
135
+ const variant = useContext(VariantContext)
136
+ return (
137
+ <div class={cn(
138
+ 'flex items-stretch',
139
+ variant === 'underline' && 'self-stretch',
140
+ local.class,
141
+ )}>
142
+ <KobalteNavigationMenu.Menu {...others} />
143
+ </div>
144
+ )
145
+ }
146
+
147
+ /** ─── MegaMenuTrigger ───────────────────────────────────────────────────────── */
148
+ export interface MegaMenuTriggerProps {
149
+ class?: string
150
+ children?: JSX.Element
151
+ noChevron?: boolean
152
+ /** Overrides the bar-level variant for this trigger only */
153
+ variant?: MenuVariant
154
+ /** Optional icon element */
155
+ icon?: JSX.Element
156
+ /** Icon placement relative to label. Default: 'start' */
157
+ iconPosition?: 'start' | 'end' | 'top' | 'bottom'
158
+ }
159
+
160
+ export function MegaMenuTrigger(props: MegaMenuTriggerProps) {
161
+ const [local, others] = splitProps(props, ['class', 'children', 'noChevron', 'variant', 'icon', 'iconPosition'])
162
+ const contextVariant = useContext(VariantContext)
163
+ const v = () => local.variant ?? contextVariant
164
+ const ip = () => local.iconPosition ?? 'start'
165
+ const isStacked = () => ip() === 'top' || ip() === 'bottom'
166
+
167
+ return (
168
+ <KobalteNavigationMenu.Trigger
169
+ class={cn(
170
+ 'group relative flex flex-row items-center gap-1.5 text-sm font-medium text-ink-700 transition-colors',
171
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500/50',
172
+ v() === 'default' && [
173
+ !isStacked() && 'h-9',
174
+ 'rounded-md px-3 py-2',
175
+ 'hover:bg-surface-overlay hover:text-ink-900',
176
+ 'data-[expanded]:bg-surface-overlay data-[expanded]:text-ink-900',
177
+ ],
178
+ v() === 'underline' && [
179
+ 'h-full rounded-none px-4',
180
+ isStacked() && 'py-2',
181
+ 'border-b-2 border-transparent',
182
+ 'hover:border-primary-500 hover:text-primary-600',
183
+ 'data-[expanded]:border-primary-500 data-[expanded]:text-primary-600',
184
+ ],
185
+ v() === 'ghost' && [
186
+ !isStacked() && 'h-9',
187
+ 'rounded-md px-3 py-2',
188
+ 'hover:text-primary-600 data-[expanded]:text-primary-600',
189
+ ],
190
+ local.class,
191
+ )}
192
+ {...others}
193
+ >
194
+ {/* top/bottom: icon+label stacked in a flex-col block, chevron to the right */}
195
+ <Show when={isStacked()}>
196
+ <span class="flex flex-col items-center gap-1 translate-y-1">
197
+ <Show when={local.icon && ip() === 'top'}>
198
+ <span class="flex h-4 w-4 shrink-0 items-center justify-center [&>svg]:h-full [&>svg]:w-full">{local.icon}</span>
199
+ </Show>
200
+ <span class="text-xs leading-none">{local.children}</span>
201
+ <Show when={local.icon && ip() === 'bottom'}>
202
+ <span class="flex h-4 w-4 shrink-0 items-center justify-center [&>svg]:h-full [&>svg]:w-full">{local.icon}</span>
203
+ </Show>
204
+ </span>
205
+ </Show>
206
+ {/* start/end: normal inline layout */}
207
+ <Show when={!isStacked()}>
208
+ <Show when={local.icon && ip() === 'start'}>
209
+ <span class="flex h-4 w-4 shrink-0 items-center justify-center [&>svg]:h-full [&>svg]:w-full">{local.icon}</span>
210
+ </Show>
211
+ <span>{local.children}</span>
212
+ <Show when={local.icon && ip() === 'end'}>
213
+ <span class="flex h-4 w-4 shrink-0 items-center justify-center [&>svg]:h-full [&>svg]:w-full">{local.icon}</span>
214
+ </Show>
215
+ </Show>
216
+ <Show when={!local.noChevron && !isStacked()}>
217
+ <ChevronDown
218
+ class="relative h-3.5 w-3.5 shrink-0 text-ink-400 transition-transform duration-200 group-data-[expanded]:rotate-180"
219
+ aria-hidden="true"
220
+ />
221
+ </Show>
222
+ </KobalteNavigationMenu.Trigger>
223
+ )
224
+ }
225
+
226
+ /** ─── MegaMenuContent ───────────────────────────────────────────────────────── */
227
+ export interface MegaMenuContentProps {
228
+ class?: string
229
+ children?: JSX.Element
230
+ }
231
+
232
+ export function MegaMenuContent(props: MegaMenuContentProps) {
233
+ const [local, others] = splitProps(props, ['class', 'children'])
234
+ return (
235
+ <KobalteNavigationMenu.Portal>
236
+ <KobalteNavigationMenu.Content
237
+ class={cn('torchui-mm-content', local.class)}
238
+ {...others}
239
+ >
240
+ {local.children}
241
+ </KobalteNavigationMenu.Content>
242
+ </KobalteNavigationMenu.Portal>
243
+ )
244
+ }
245
+
246
+ /** ─── MegaMenuPanel ─────────────────────────────────────────────────────────── */
247
+ export interface MegaMenuPanelProps {
248
+ /** Number of columns. Default: 3 */
249
+ columns?: 2 | 3 | 4
250
+ fullWidth?: boolean
251
+ /** Max content width when fullWidth is true. Default: 1280px */
252
+ maxWidth?: string
253
+ class?: string
254
+ children: JSX.Element
255
+ }
256
+
257
+ export function MegaMenuPanel(props: MegaMenuPanelProps) {
258
+ const cols = () => props.columns ?? 3
259
+ const gridClass = () => ({ 2: 'grid-cols-2', 3: 'grid-cols-3', 4: 'grid-cols-4' }[cols()] ?? 'grid-cols-3')
260
+
261
+ if (props.fullWidth) {
262
+ return (
263
+ <div class={cn('w-full px-6 py-5', props.class)}>
264
+ <div class={cn('mx-auto grid gap-x-8 gap-y-2', gridClass())} style={{ 'max-width': props.maxWidth ?? '1280px' }}>
265
+ {props.children}
266
+ </div>
267
+ </div>
268
+ )
269
+ }
270
+
271
+ return (
272
+ <div class={cn('grid gap-x-6 gap-y-2 p-5', gridClass(), props.class)}>
273
+ {props.children}
274
+ </div>
275
+ )
276
+ }
277
+
278
+ /** ─── MegaMenuColumn ────────────────────────────────────────────────────────── */
279
+ export function MegaMenuColumn(props: { class?: string; children: JSX.Element }) {
280
+ return (
281
+ <div class={cn('flex flex-col gap-0.5', props.class)}>
282
+ {props.children}
283
+ </div>
284
+ )
285
+ }
286
+
287
+ /** ─── MegaMenuSection ───────────────────────────────────────────────────────── */
288
+ export function MegaMenuSection(props: { label: string; class?: string; children: JSX.Element }) {
289
+ return (
290
+ <div class={cn('', props.class)}>
291
+ <div class="mb-1 px-3 text-[11px] font-semibold uppercase tracking-widest text-ink-400">
292
+ {props.label}
293
+ </div>
294
+ {props.children}
295
+ </div>
296
+ )
297
+ }
298
+
299
+ /** ─── MegaMenuItem ──────────────────────────────────────────────────────────── */
300
+ export interface MegaMenuItemProps {
301
+ href?: string
302
+ icon?: JSX.Element
303
+ label: JSX.Element
304
+ description?: string
305
+ badge?: string
306
+ active?: boolean
307
+ disabled?: boolean
308
+ onClick?: () => void
309
+ class?: string
310
+ }
311
+
312
+ export function MegaMenuItem(props: MegaMenuItemProps) {
313
+ return (
314
+ <Dynamic
315
+ component={props.href && !props.disabled ? 'a' : 'button'}
316
+ href={props.href && !props.disabled ? props.href : undefined}
317
+ type={props.href && !props.disabled ? undefined : 'button'}
318
+ tabIndex={props.disabled ? -1 : undefined}
319
+ onClick={(e: Event) => {
320
+ if (props.disabled) { e.preventDefault(); return }
321
+ props.onClick?.()
322
+ }}
323
+ aria-disabled={props.disabled || undefined}
324
+ class={cn(
325
+ 'group flex w-full items-start gap-3 rounded-lg px-3 py-2.5 text-sm outline-none transition-colors',
326
+ props.active ? 'bg-primary-50 dark:bg-primary-500/10' : 'hover:bg-surface-overlay',
327
+ props.disabled && 'pointer-events-none opacity-40',
328
+ props.class,
329
+ )}
330
+ >
331
+ <Show when={props.icon}>
332
+ <span class={cn(
333
+ 'mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-lg transition-colors [&>svg]:h-4 [&>svg]:w-4',
334
+ props.active
335
+ ? 'bg-primary-100 text-primary-600 dark:bg-primary-500/20 dark:text-primary-400'
336
+ : 'bg-surface-overlay text-ink-500 group-hover:bg-surface-dim group-hover:text-ink-700',
337
+ )}>
338
+ {props.icon}
339
+ </span>
340
+ </Show>
341
+ <div class="min-w-0 flex-1">
342
+ <div class="flex items-center gap-2">
343
+ <span class={cn('font-medium leading-none', props.active ? 'text-primary-700 dark:text-primary-400' : 'text-ink-900')}>
344
+ {props.label}
345
+ </span>
346
+ <Show when={props.badge}>
347
+ <span class="rounded-full bg-primary-100 px-1.5 py-0.5 text-[10px] font-semibold text-primary-700 dark:bg-primary-500/20 dark:text-primary-300">
348
+ {props.badge}
349
+ </span>
350
+ </Show>
351
+ </div>
352
+ <Show when={props.description}>
353
+ <p class="mt-0.5 text-xs leading-relaxed text-ink-500">{props.description}</p>
354
+ </Show>
355
+ </div>
356
+ </Dynamic>
357
+ )
358
+ }
359
+
360
+ /** ─── MegaMenuFeatured ──────────────────────────────────────────────────────── */
361
+ export interface MegaMenuFeaturedProps {
362
+ href?: string
363
+ title: string
364
+ description?: string
365
+ /** Background color class. Default: primary gradient */
366
+ backgroundClass?: string
367
+ image?: JSX.Element
368
+ /** CTA label. Default: 'Learn more' */
369
+ cta?: string
370
+ class?: string
371
+ }
372
+
373
+ export function MegaMenuFeatured(props: MegaMenuFeaturedProps) {
374
+ return (
375
+ <Dynamic
376
+ component={props.href ? 'a' : 'div'}
377
+ href={props.href}
378
+ class={cn(
379
+ 'group relative flex flex-col justify-between overflow-hidden rounded-xl p-5 outline-none transition-opacity hover:opacity-90',
380
+ 'focus-visible:ring-2 focus-visible:ring-white/70 focus-visible:ring-offset-2 focus-visible:ring-offset-transparent',
381
+ props.backgroundClass ?? 'bg-gradient-to-br from-primary-500 to-primary-600',
382
+ props.class,
383
+ )}
384
+ >
385
+ <Show when={props.image}>
386
+ <div class="pointer-events-none absolute inset-0 opacity-20">{props.image}</div>
387
+ </Show>
388
+ <div class="relative">
389
+ <p class="text-sm font-semibold text-white">{props.title}</p>
390
+ <Show when={props.description}>
391
+ <p class="mt-1 text-xs text-white/70 leading-relaxed">{props.description}</p>
392
+ </Show>
393
+ </div>
394
+ <div class="relative mt-4 flex items-center gap-1 text-xs font-semibold text-white">
395
+ {props.cta ?? 'Learn more'}
396
+ <svg class="h-3 w-3 transition-transform group-hover:translate-x-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
397
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M9 5l7 7-7 7" />
398
+ </svg>
399
+ </div>
400
+ </Dynamic>
401
+ )
402
+ }
403
+
404
+ /** ─── MegaMenuDivider ───────────────────────────────────────────────────────── */
405
+ export function MegaMenuDivider(props: { class?: string }) {
406
+ return <div role="separator" aria-orientation="horizontal" class={cn('h-px bg-surface-border', props.class)} />
407
+ }
408
+
409
+ /** ─── MegaMenuFooter ────────────────────────────────────────────────────────── */
410
+ export function MegaMenuFooter(props: {
411
+ class?: string
412
+ children: JSX.Element
413
+ fullWidth?: boolean
414
+ maxWidth?: string
415
+ }) {
416
+ return (
417
+ <div class={cn('border-t border-surface-border', props.fullWidth ? 'px-6 py-3' : 'px-5 py-3', props.class)}>
418
+ <div
419
+ class="flex items-center gap-2"
420
+ style={props.fullWidth ? { 'max-width': props.maxWidth ?? '1280px', margin: '0 auto' } : {}}
421
+ >
422
+ {props.children}
423
+ </div>
424
+ </div>
425
+ )
426
+ }
427
+
428
+ /** ─── MegaMenuFooterLink ────────────────────────────────────────────────────── */
429
+ export function MegaMenuFooterLink(props: { href?: string; onClick?: () => void; children: JSX.Element; class?: string }) {
430
+ return (
431
+ <Dynamic
432
+ component={props.href ? 'a' : 'button'}
433
+ href={props.href}
434
+ type={props.href ? undefined : 'button'}
435
+ onClick={props.onClick}
436
+ class={cn(
437
+ 'text-xs font-medium text-ink-500 hover:text-primary-600 dark:hover:text-primary-400 transition-colors',
438
+ props.class,
439
+ )}
440
+ >
441
+ {props.children}
442
+ </Dynamic>
443
+ )
444
+ }
445
+
446
+ /** ─── MegaMenuBarLink ───────────────────────────────────────────────────────── */
447
+ export interface MegaMenuBarLinkProps {
448
+ href: string
449
+ class?: string
450
+ children?: JSX.Element
451
+ /** Overrides the bar-level variant for this link only */
452
+ variant?: MenuVariant
453
+ }
454
+
455
+ export function MegaMenuBarLink(props: MegaMenuBarLinkProps) {
456
+ const contextVariant = useContext(VariantContext)
457
+ const v = () => props.variant ?? contextVariant
458
+
459
+ return (
460
+ <div class={cn('flex items-stretch', v() === 'underline' && 'h-full')}>
461
+ <a
462
+ href={props.href}
463
+ class={cn(
464
+ 'flex items-center text-sm font-medium text-ink-700 transition-colors',
465
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500/50',
466
+ v() === 'default' && 'h-9 rounded-md px-3 py-2 hover:bg-surface-overlay hover:text-ink-900',
467
+ v() === 'underline' && [
468
+ 'h-full rounded-none px-4',
469
+ 'border-b-2 border-transparent',
470
+ 'hover:border-primary-500 hover:text-primary-600',
471
+ ],
472
+ v() === 'ghost' && 'h-9 rounded-md px-3 py-2 hover:text-primary-600',
473
+ props.class,
474
+ )}
475
+ >
476
+ {props.children}
477
+ </a>
478
+ </div>
479
+ )
480
+ }