@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.
- package/README.md +166 -0
- package/package.json +67 -0
- package/src/components/actions/Button.tsx +612 -0
- package/src/components/actions/ButtonGroup.tsx +728 -0
- package/src/components/actions/Copy.tsx +98 -0
- package/src/components/actions/DarkModeToggle.tsx +80 -0
- package/src/components/actions/Link.tsx +37 -0
- package/src/components/actions/index.ts +19 -0
- package/src/components/actions/useCopyToClipboard.ts +90 -0
- package/src/components/charts/Chart.tsx +331 -0
- package/src/components/charts/Sparkline.tsx +156 -0
- package/src/components/charts/index.ts +13 -0
- package/src/components/data-display/Avatar.tsx +208 -0
- package/src/components/data-display/AvatarGroup.tsx +228 -0
- package/src/components/data-display/Badge.tsx +70 -0
- package/src/components/data-display/Carousel.tsx +214 -0
- package/src/components/data-display/ColorSwatch.tsx +56 -0
- package/src/components/data-display/DataTable.tsx +886 -0
- package/src/components/data-display/EmptyState.tsx +61 -0
- package/src/components/data-display/Image.tsx +277 -0
- package/src/components/data-display/Kbd.tsx +114 -0
- package/src/components/data-display/Persona.tsx +78 -0
- package/src/components/data-display/StatCard.tsx +338 -0
- package/src/components/data-display/Table.tsx +147 -0
- package/src/components/data-display/Tag.tsx +91 -0
- package/src/components/data-display/Timeline.tsx +200 -0
- package/src/components/data-display/TreeView.tsx +172 -0
- package/src/components/data-display/Video.tsx +95 -0
- package/src/components/data-display/avatar-utils.ts +32 -0
- package/src/components/data-display/index.ts +81 -0
- package/src/components/feedback/Loading.tsx +159 -0
- package/src/components/feedback/Progress.tsx +321 -0
- package/src/components/feedback/Skeleton.tsx +62 -0
- package/src/components/feedback/SkeletonBlocks.tsx +222 -0
- package/src/components/feedback/Toast.tsx +648 -0
- package/src/components/feedback/index.ts +44 -0
- package/src/components/feedback/password/PasswordStrengthIndicator.tsx +232 -0
- package/src/components/feedback/password/password-strength.ts +115 -0
- package/src/components/feedback/password/password-validation-data.ts +66 -0
- package/src/components/feedback/password/password-validation.ts +93 -0
- package/src/components/forms/Autocomplete.tsx +268 -0
- package/src/components/forms/Checkbox.tsx +155 -0
- package/src/components/forms/CodeInput.tsx +237 -0
- package/src/components/forms/ColorPicker/ColorPicker.tsx +469 -0
- package/src/components/forms/ColorPicker/color-utils.ts +75 -0
- package/src/components/forms/ColorPicker/index.ts +2 -0
- package/src/components/forms/DatePicker.tsx +516 -0
- package/src/components/forms/DateRangePicker.tsx +464 -0
- package/src/components/forms/FieldPicker.tsx +64 -0
- package/src/components/forms/FileUpload.tsx +614 -0
- package/src/components/forms/FilterBuilder/FilterGroupBlock.ts +6 -0
- package/src/components/forms/FilterBuilder.tsx +16 -0
- package/src/components/forms/FilterRuleRow.tsx +68 -0
- package/src/components/forms/Input.tsx +200 -0
- package/src/components/forms/MultiSelect.tsx +361 -0
- package/src/components/forms/NumberField.tsx +145 -0
- package/src/components/forms/RadioGroup.tsx +135 -0
- package/src/components/forms/RelativeDateDefaultInput.tsx +62 -0
- package/src/components/forms/ReorderableList.tsx +163 -0
- package/src/components/forms/Select.tsx +268 -0
- package/src/components/forms/Slider.tsx +260 -0
- package/src/components/forms/Switch.tsx +135 -0
- package/src/components/forms/TextArea.tsx +202 -0
- package/src/components/forms/ViewCustomizer.tsx +44 -0
- package/src/components/forms/index.ts +43 -0
- package/src/components/layout/Accordion.tsx +110 -0
- package/src/components/layout/Alert.tsx +156 -0
- package/src/components/layout/BlockQuote.tsx +70 -0
- package/src/components/layout/Card.tsx +166 -0
- package/src/components/layout/CodeBlock/CodeBlock.tsx +477 -0
- package/src/components/layout/CodeBlock/code-block-tokens.css +104 -0
- package/src/components/layout/CodeBlock/prism.ts +81 -0
- package/src/components/layout/Collapsible.tsx +84 -0
- package/src/components/layout/Container.tsx +55 -0
- package/src/components/layout/Divider.tsx +64 -0
- package/src/components/layout/Form.tsx +39 -0
- package/src/components/layout/FormActions.tsx +50 -0
- package/src/components/layout/Grid.tsx +53 -0
- package/src/components/layout/PageHeading.tsx +46 -0
- package/src/components/layout/PromptWithAction.tsx +49 -0
- package/src/components/layout/Section.tsx +60 -0
- package/src/components/layout/TablePanel.tsx +24 -0
- package/src/components/layout/TableView/TableView.tsx +1018 -0
- package/src/components/layout/TableView/index.ts +3 -0
- package/src/components/layout/TableView/types.ts +51 -0
- package/src/components/layout/WizardStep.tsx +40 -0
- package/src/components/layout/WizardStepper.tsx +173 -0
- package/src/components/layout/index.ts +96 -0
- package/src/components/navigation/Breadcrumbs.tsx +66 -0
- package/src/components/navigation/DropdownMenu.tsx +86 -0
- package/src/components/navigation/MegaMenu.tsx +480 -0
- package/src/components/navigation/NavigationMenu.tsx +305 -0
- package/src/components/navigation/Pagination.tsx +298 -0
- package/src/components/navigation/Sidebar.tsx +280 -0
- package/src/components/navigation/Tabs.tsx +122 -0
- package/src/components/navigation/ViewSwitcher.tsx +314 -0
- package/src/components/navigation/index.ts +66 -0
- package/src/components/overlays/AlertDialog.tsx +174 -0
- package/src/components/overlays/ContextMenu.tsx +65 -0
- package/src/components/overlays/Dialog.tsx +279 -0
- package/src/components/overlays/Drawer.tsx +370 -0
- package/src/components/overlays/HoverCard.tsx +107 -0
- package/src/components/overlays/Popover.tsx +73 -0
- package/src/components/overlays/Tooltip.tsx +31 -0
- package/src/components/overlays/index.ts +71 -0
- package/src/components/typography/Code.tsx +72 -0
- package/src/components/typography/Icon.tsx +36 -0
- package/src/components/typography/index.ts +10 -0
- package/src/env.d.ts +9 -0
- package/src/index.ts +13 -0
- package/src/styles/theme.css +226 -0
- package/src/types/avatar-types.ts +11 -0
- package/src/types/filter-types.ts +35 -0
- package/src/utilities/classNames.ts +6 -0
- package/src/utilities/componentSize.ts +46 -0
- package/src/utilities/i18n.tsx +60 -0
- package/src/utilities/mergeRefs.ts +12 -0
- 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
|
+
}
|