@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
@@ -0,0 +1,77 @@
1
+ // FeatureGrid — a heading plus a responsive grid of feature cards (icon, title,
2
+ // short text).
3
+ import type { JSX } from 'solid-js'
4
+ import { For, Show } from 'solid-js'
5
+
6
+ import { cn } from '../lib/cn'
7
+ import { Card } from './Card'
8
+ import { SectionHeading } from './internal/sectionKit'
9
+
10
+ /** One feature rendered by {@link FeatureGrid}. */
11
+ export interface FeatureItem {
12
+ title: string
13
+ text: string
14
+ /** Optional icon node, shown in a tinted tile above the title. */
15
+ icon?: JSX.Element
16
+ }
17
+
18
+ export interface FeatureGridProps {
19
+ title?: string
20
+ subtitle?: string
21
+ items: FeatureItem[]
22
+ /** Columns on wide screens (one column on phones). Defaults to `3`. */
23
+ columns?: 2 | 3 | 4
24
+ /** Anchor id for in-page navigation. */
25
+ id?: string
26
+ class?: string
27
+ }
28
+
29
+ const COLS: Record<2 | 3 | 4, string> = {
30
+ 2: 'sm:grid-cols-2',
31
+ 3: 'sm:grid-cols-2 lg:grid-cols-3',
32
+ 4: 'sm:grid-cols-2 lg:grid-cols-4',
33
+ }
34
+
35
+ /**
36
+ * Section listing a product's features: an optional heading and a responsive
37
+ * grid of glass cards, each with an optional icon, a title and a short text.
38
+ *
39
+ * @example
40
+ * ```tsx
41
+ * <FeatureGrid
42
+ * title="Why teams switch"
43
+ * items={[
44
+ * { title: 'Fast', text: 'Pages load in under a second.' },
45
+ * { title: 'Accessible', text: 'Keyboard and screen-reader friendly.' },
46
+ * { title: 'Themeable', text: 'Swap palettes without touching CSS.' },
47
+ * ]}
48
+ * />
49
+ * ```
50
+ */
51
+ export function FeatureGrid(props: FeatureGridProps): JSX.Element {
52
+ return (
53
+ <section
54
+ id={props.id}
55
+ class={cn('mx-auto flex w-full max-w-6xl flex-col gap-12 px-5 py-20', props.class)}
56
+ >
57
+ <SectionHeading title={props.title} subtitle={props.subtitle} />
58
+ <ul class={cn('grid gap-6', COLS[props.columns ?? 3])}>
59
+ <For each={props.items}>
60
+ {(item) => (
61
+ <li class="flex">
62
+ <Card glass class="flex w-full flex-col gap-3 p-6">
63
+ <Show when={item.icon}>
64
+ <div class="grid h-11 w-11 place-items-center rounded-lg bg-primary/15 text-primary [&_svg]:h-5 [&_svg]:w-5">
65
+ {item.icon}
66
+ </div>
67
+ </Show>
68
+ <h3 class="text-lg font-semibold text-foreground">{item.title}</h3>
69
+ <p class="text-sm leading-relaxed text-muted-foreground">{item.text}</p>
70
+ </Card>
71
+ </li>
72
+ )}
73
+ </For>
74
+ </ul>
75
+ </section>
76
+ )
77
+ }
@@ -0,0 +1,374 @@
1
+ // Field — composable form-field layout: FieldSet/FieldLegend/FieldGroup for
2
+ // structure, Field for one labelled control. A per-field context hands out one
3
+ // control id and collects description/error ids, so the label's `for`, the
4
+ // control's `aria-describedby`, `aria-errormessage` and `aria-invalid` are
5
+ // wired automatically. The control is discovered in the DOM after mount (first
6
+ // input/textarea/select/combobox/switch inside the field), so plain, Kobalte
7
+ // and InputGroup controls all work with no render-prop.
8
+ //
9
+ // Layout note: Tailwind 3.4 (the preset's version) has no built-in `@container`
10
+ // variants, so `orientation="responsive"` and FieldGroup use viewport
11
+ // breakpoints (`sm:`), not container queries.
12
+ import type { JSX, ParentProps } from 'solid-js'
13
+ import {
14
+ createContext,
15
+ createEffect,
16
+ createSignal,
17
+ createUniqueId,
18
+ For,
19
+ onCleanup,
20
+ onMount,
21
+ Show,
22
+ splitProps,
23
+ useContext,
24
+ } from 'solid-js'
25
+
26
+ import { cn } from '../lib/cn'
27
+
28
+ /** Layout direction of a {@link Field}. */
29
+ export type FieldOrientation = 'vertical' | 'horizontal' | 'responsive'
30
+
31
+ interface FieldContextValue {
32
+ /** Id of the control (adopted from the control if it already has one). */
33
+ controlId: () => string
34
+ registerDescription: (id: string) => () => void
35
+ registerError: (id: string) => () => void
36
+ }
37
+
38
+ const FieldContext = createContext<FieldContextValue>()
39
+
40
+ const CONTROL_SELECTOR =
41
+ '[data-field-control], input:not([type="hidden"]), textarea, select, [role="combobox"], [role="switch"], [role="checkbox"]'
42
+
43
+ // Add an id to a signal-held list; returns the remover.
44
+ function register(setter: (fn: (prev: string[]) => string[]) => void, id: string): () => void {
45
+ setter((prev) => [...prev, id])
46
+ return () => setter((prev) => prev.filter((x) => x !== id))
47
+ }
48
+
49
+ const ORIENTATION_CLASSES: Record<FieldOrientation, string> = {
50
+ vertical: 'flex-col',
51
+ horizontal: 'flex-row items-center justify-between gap-3',
52
+ responsive: 'flex-col sm:flex-row sm:items-center sm:justify-between sm:gap-3',
53
+ }
54
+
55
+ export interface FieldSetProps extends ParentProps, JSX.FieldsetHTMLAttributes<HTMLFieldSetElement> {
56
+ class?: string
57
+ }
58
+
59
+ /**
60
+ * Semantic `<fieldset>` that groups related fields. Pair with {@link FieldLegend}.
61
+ *
62
+ * @example
63
+ * ```tsx
64
+ * <FieldSet>
65
+ * <FieldLegend>Profile</FieldLegend>
66
+ * <FieldGroup>...</FieldGroup>
67
+ * </FieldSet>
68
+ * ```
69
+ */
70
+ export function FieldSet(props: FieldSetProps): JSX.Element {
71
+ const [local, rest] = splitProps(props, ['class', 'children'])
72
+ return (
73
+ <fieldset class={cn('m-0 flex min-w-0 flex-col gap-4 border-0 p-0', local.class)} {...rest}>
74
+ {local.children}
75
+ </fieldset>
76
+ )
77
+ }
78
+
79
+ export interface FieldLegendProps extends ParentProps, JSX.HTMLAttributes<HTMLLegendElement> {
80
+ /** `legend` is a section heading; `label` is the smaller label size. Defaults to `legend`. */
81
+ variant?: 'legend' | 'label'
82
+ class?: string
83
+ }
84
+
85
+ /**
86
+ * Caption of a {@link FieldSet}.
87
+ *
88
+ * @example
89
+ * ```tsx
90
+ * <FieldLegend variant="label">Notifications</FieldLegend>
91
+ * ```
92
+ */
93
+ export function FieldLegend(props: FieldLegendProps): JSX.Element {
94
+ const [local, rest] = splitProps(props, ['class', 'variant', 'children'])
95
+ return (
96
+ <legend
97
+ data-variant={local.variant ?? 'legend'}
98
+ class={cn(
99
+ 'mb-1 font-semibold text-foreground',
100
+ local.variant === 'label' ? 'text-sm' : 'text-base',
101
+ local.class,
102
+ )}
103
+ {...rest}
104
+ >
105
+ {local.children}
106
+ </legend>
107
+ )
108
+ }
109
+
110
+ export interface FieldGroupProps extends ParentProps, JSX.HTMLAttributes<HTMLDivElement> {
111
+ class?: string
112
+ }
113
+
114
+ /**
115
+ * Vertical stack of {@link Field}s with consistent spacing.
116
+ *
117
+ * @example
118
+ * ```tsx
119
+ * <FieldGroup><Field>...</Field><Field>...</Field></FieldGroup>
120
+ * ```
121
+ */
122
+ export function FieldGroup(props: FieldGroupProps): JSX.Element {
123
+ const [local, rest] = splitProps(props, ['class', 'children'])
124
+ return (
125
+ <div data-slot="field-group" class={cn('flex w-full flex-col gap-5', local.class)} {...rest}>
126
+ {local.children}
127
+ </div>
128
+ )
129
+ }
130
+
131
+ export interface FieldProps extends ParentProps, JSX.HTMLAttributes<HTMLDivElement> {
132
+ /** `vertical` (default), `horizontal`, or `responsive` (stacked below `sm`, a row above). */
133
+ orientation?: FieldOrientation
134
+ /** Marks the field invalid: sets `data-invalid` and `aria-invalid` on the control. */
135
+ invalid?: boolean
136
+ class?: string
137
+ }
138
+
139
+ /**
140
+ * One labelled form field. Provides the id context used by {@link FieldLabel},
141
+ * {@link FieldDescription} and {@link FieldError}, and wires the control inside
142
+ * it (`id`, `aria-describedby`, `aria-errormessage`, `aria-invalid`).
143
+ *
144
+ * @example
145
+ * ```tsx
146
+ * <Field invalid={!!error()}>
147
+ * <FieldLabel>Email</FieldLabel>
148
+ * <Input value={email()} onInput={setEmail} />
149
+ * <FieldDescription>We never share it.</FieldDescription>
150
+ * <FieldError>{error()}</FieldError>
151
+ * </Field>
152
+ * ```
153
+ */
154
+ export function Field(props: FieldProps): JSX.Element {
155
+ const [local, rest] = splitProps(props, ['class', 'orientation', 'invalid', 'children'])
156
+ const uid = createUniqueId()
157
+ const [controlId, setControlId] = createSignal(`${uid}-control`)
158
+ const [descIds, setDescIds] = createSignal<string[]>([])
159
+ const [errIds, setErrIds] = createSignal<string[]>([])
160
+ let root!: HTMLDivElement
161
+
162
+ const ctx: FieldContextValue = {
163
+ controlId,
164
+ registerDescription: (id) => register(setDescIds, id),
165
+ registerError: (id) => register(setErrIds, id),
166
+ }
167
+
168
+ // The control is found in the DOM (any input-like element), so it may appear
169
+ // after mount (e.g. inside a <Show>). A MutationObserver re-runs the wiring.
170
+ const [control, setControl] = createSignal<HTMLElement | null>(null)
171
+ const findControl = (): HTMLElement | null => root.querySelector<HTMLElement>(CONTROL_SELECTOR)
172
+ const syncControl = (): void => {
173
+ const el = findControl()
174
+ if (el && !el.id) el.id = controlId()
175
+ if (el) setControlId(el.id)
176
+ setControl(el)
177
+ }
178
+
179
+ onMount(() => {
180
+ syncControl()
181
+ const observer = new MutationObserver(syncControl)
182
+ observer.observe(root, { childList: true, subtree: true })
183
+ onCleanup(() => observer.disconnect())
184
+ })
185
+
186
+ // Re-apply ARIA wiring whenever the control, descriptions, errors or invalid change.
187
+ createEffect(() => {
188
+ const desc = descIds()
189
+ const err = errIds()
190
+ const invalid = !!local.invalid
191
+ const el = control()
192
+ if (!el) return
193
+ const ids = [...desc, ...err]
194
+ if (ids.length) el.setAttribute('aria-describedby', ids.join(' '))
195
+ else el.removeAttribute('aria-describedby')
196
+ if (invalid) el.setAttribute('aria-invalid', 'true')
197
+ else el.removeAttribute('aria-invalid')
198
+ if (invalid && err.length) el.setAttribute('aria-errormessage', err.join(' '))
199
+ else el.removeAttribute('aria-errormessage')
200
+ })
201
+
202
+ return (
203
+ <FieldContext.Provider value={ctx}>
204
+ <div
205
+ ref={root}
206
+ role="group"
207
+ data-slot="field"
208
+ data-orientation={local.orientation ?? 'vertical'}
209
+ data-invalid={local.invalid ? '' : undefined}
210
+ class={cn(
211
+ 'group/field flex w-full gap-2',
212
+ ORIENTATION_CLASSES[local.orientation ?? 'vertical'],
213
+ local.class,
214
+ )}
215
+ {...rest}
216
+ >
217
+ {local.children}
218
+ </div>
219
+ </FieldContext.Provider>
220
+ )
221
+ }
222
+
223
+ /**
224
+ * Wrapper for a title + description block next to a control, e.g. the text half
225
+ * of a horizontal switch row.
226
+ *
227
+ * @example
228
+ * ```tsx
229
+ * <FieldContent><FieldTitle>Alerts</FieldTitle><FieldDescription>Email me.</FieldDescription></FieldContent>
230
+ * ```
231
+ */
232
+ export function FieldContent(props: ParentProps<{ class?: string }>): JSX.Element {
233
+ return <div class={cn('flex flex-1 flex-col gap-1', props.class)}>{props.children}</div>
234
+ }
235
+
236
+ export interface FieldLabelProps extends ParentProps, JSX.LabelHTMLAttributes<HTMLLabelElement> {
237
+ class?: string
238
+ }
239
+
240
+ /**
241
+ * `<label>` bound to the enclosing {@link Field}'s control through `for`.
242
+ *
243
+ * @example
244
+ * ```tsx
245
+ * <FieldLabel>Full name</FieldLabel>
246
+ * ```
247
+ */
248
+ export function FieldLabel(props: FieldLabelProps): JSX.Element {
249
+ const [local, rest] = splitProps(props, ['class', 'children', 'for'])
250
+ const ctx = useContext(FieldContext)
251
+ return (
252
+ <label
253
+ for={local.for ?? ctx?.controlId()}
254
+ class={cn(
255
+ 'text-sm font-medium leading-none text-foreground group-data-[invalid]/field:text-destructive',
256
+ local.class,
257
+ )}
258
+ {...rest}
259
+ >
260
+ {local.children}
261
+ </label>
262
+ )
263
+ }
264
+
265
+ /**
266
+ * Non-label title text for a field (use where a `<label>` would be wrong).
267
+ *
268
+ * @example
269
+ * ```tsx
270
+ * <FieldTitle>Two-factor authentication</FieldTitle>
271
+ * ```
272
+ */
273
+ export function FieldTitle(props: ParentProps<{ class?: string }>): JSX.Element {
274
+ return (
275
+ <div class={cn('text-sm font-medium leading-none text-foreground', props.class)}>{props.children}</div>
276
+ )
277
+ }
278
+
279
+ /**
280
+ * Helper text, linked to the control through `aria-describedby`.
281
+ *
282
+ * @example
283
+ * ```tsx
284
+ * <FieldDescription>Use 8 or more characters.</FieldDescription>
285
+ * ```
286
+ */
287
+ export function FieldDescription(props: ParentProps<{ class?: string; id?: string }>): JSX.Element {
288
+ const ctx = useContext(FieldContext)
289
+ // eslint-disable-next-line solid/reactivity -- id is stable for the element's lifetime
290
+ const id = props.id ?? createUniqueId()
291
+ if (ctx) onCleanup(ctx.registerDescription(id))
292
+ return (
293
+ <p id={id} class={cn('text-xs text-muted-foreground', props.class)}>
294
+ {props.children}
295
+ </p>
296
+ )
297
+ }
298
+
299
+ export interface FieldSeparatorProps {
300
+ /** Optional text shown in the middle of the rule. */
301
+ children?: JSX.Element
302
+ class?: string
303
+ }
304
+
305
+ /**
306
+ * Horizontal rule between fields, optionally with text in the middle.
307
+ *
308
+ * @example
309
+ * ```tsx
310
+ * <FieldSeparator>or continue with</FieldSeparator>
311
+ * ```
312
+ */
313
+ export function FieldSeparator(props: FieldSeparatorProps): JSX.Element {
314
+ return (
315
+ <div
316
+ role="separator"
317
+ data-slot="field-separator"
318
+ class={cn('relative flex items-center gap-3 text-xs text-muted-foreground', props.class)}
319
+ >
320
+ <span class="h-px flex-1 bg-border" />
321
+ <Show when={props.children}>
322
+ <span>{props.children}</span>
323
+ <span class="h-px flex-1 bg-border" />
324
+ </Show>
325
+ </div>
326
+ )
327
+ }
328
+
329
+ export interface FieldErrorProps {
330
+ /** Error messages; entries may be strings or `{ message }` objects (e.g. schema issues). */
331
+ errors?: Array<string | { message?: string } | undefined>
332
+ /** Custom content, used when `errors` is not given. */
333
+ children?: JSX.Element
334
+ class?: string
335
+ }
336
+
337
+ /**
338
+ * Validation message(s) for a field. Renders nothing when empty, a single line
339
+ * for one message and a list for several. `role="alert"` is set only while
340
+ * something is shown, so assistive tech announces it when it appears.
341
+ *
342
+ * @example
343
+ * ```tsx
344
+ * <FieldError errors={['Too short', 'Needs a number']} />
345
+ * ```
346
+ */
347
+ export function FieldError(props: FieldErrorProps): JSX.Element {
348
+ const ctx = useContext(FieldContext)
349
+ const id = createUniqueId()
350
+ const messages = (): string[] => {
351
+ const out: string[] = []
352
+ for (const e of props.errors ?? []) {
353
+ const m = typeof e === 'string' ? e : e?.message
354
+ if (m) out.push(m)
355
+ }
356
+ return out
357
+ }
358
+ const visible = (): boolean => messages().length > 0 || !!props.children
359
+ createEffect(() => {
360
+ if (!ctx || !visible()) return
361
+ onCleanup(ctx.registerError(id))
362
+ })
363
+ return (
364
+ <Show when={visible()}>
365
+ <div id={id} role="alert" data-slot="field-error" class={cn('text-xs text-destructive', props.class)}>
366
+ <Show when={messages().length > 1} fallback={<>{messages()[0] ?? props.children}</>}>
367
+ <ul class="ml-4 list-disc space-y-0.5">
368
+ <For each={messages()}>{(m) => <li>{m}</li>}</For>
369
+ </ul>
370
+ </Show>
371
+ </div>
372
+ </Show>
373
+ )
374
+ }
@@ -0,0 +1,146 @@
1
+ // Hero — the opening section of a landing page: headline, supporting text, up
2
+ // to two calls-to-action, and an optional image, on a plain, aurora or photo
3
+ // background.
4
+ import type { JSX } from 'solid-js'
5
+ import { Show } from 'solid-js'
6
+
7
+ import { cn } from '../lib/cn'
8
+ import { Button } from './Button'
9
+ import { SectionHeading, type SectionLink } from './internal/sectionKit'
10
+
11
+ /** Arrangement of a {@link Hero}: text centered, or text beside the image. */
12
+ export type HeroLayout = 'center' | 'split'
13
+ /** Backdrop of a {@link Hero}. `'image'` uses `image` as a full-bleed photo. */
14
+ export type HeroBackground = 'none' | 'aurora' | 'image'
15
+
16
+ export interface HeroProps {
17
+ /** Small label above the title, e.g. a category or announcement. */
18
+ eyebrow?: string
19
+ /** Main headline, rendered as the page `<h1>`. */
20
+ title: string
21
+ /** Supporting paragraph under the title. */
22
+ subtitle?: string
23
+ /** Main call-to-action (filled button). */
24
+ primary?: SectionLink
25
+ /** Secondary call-to-action (outline button). */
26
+ secondary?: SectionLink
27
+ /** Image URL. Shown beside the text in `split` layout, under it in `center`, or behind it with `background="image"`. */
28
+ image?: string
29
+ /** Alt text of `image`. Defaults to empty (decorative). */
30
+ imageAlt?: string
31
+ /** Defaults to `'center'`. */
32
+ layout?: HeroLayout
33
+ /** Defaults to `'aurora'`. */
34
+ background?: HeroBackground
35
+ /** Anchor id for in-page navigation. */
36
+ id?: string
37
+ class?: string
38
+ }
39
+
40
+ /**
41
+ * Opening section of a landing page: eyebrow, headline, subtitle and up to two
42
+ * call-to-action links, with an optional image. `layout="split"` puts the image
43
+ * beside the text on wide screens (stacked on phones); `background` picks a
44
+ * theme-tinted aurora glow, a full-bleed photo with a legibility scrim, or
45
+ * nothing.
46
+ *
47
+ * @example
48
+ * ```tsx
49
+ * <Hero
50
+ * eyebrow="New"
51
+ * title="Ship your site this week"
52
+ * subtitle="Everything you need to launch, nothing you don't."
53
+ * primary={{ label: 'Get started', href: '/signup' }}
54
+ * secondary={{ label: 'See pricing', href: '#pricing' }}
55
+ * />
56
+ * ```
57
+ */
58
+ export function Hero(props: HeroProps): JSX.Element {
59
+ const layout = () => props.layout ?? 'center'
60
+ const background = () => props.background ?? 'aurora'
61
+ const photoBackdrop = () => background() === 'image' && props.image !== undefined
62
+ const showInlineImage = () => props.image !== undefined && !photoBackdrop()
63
+
64
+ const actions = (
65
+ <Show when={props.primary || props.secondary}>
66
+ <div class={cn('flex flex-wrap gap-3', layout() === 'center' && 'justify-center')}>
67
+ <Show when={props.primary}>
68
+ {(link) => (
69
+ <Button href={link().href} ripple class="px-5 py-2.5 text-base">
70
+ {link().label}
71
+ </Button>
72
+ )}
73
+ </Show>
74
+ <Show when={props.secondary}>
75
+ {(link) => (
76
+ <Button href={link().href} variant="outline" class="px-5 py-2.5 text-base">
77
+ {link().label}
78
+ </Button>
79
+ )}
80
+ </Show>
81
+ </div>
82
+ </Show>
83
+ )
84
+
85
+ return (
86
+ <section
87
+ id={props.id}
88
+ class={cn('relative isolate overflow-hidden', photoBackdrop() && 'text-white', props.class)}
89
+ >
90
+ <Show when={background() === 'aurora'}>
91
+ <div aria-hidden="true" class="pointer-events-none absolute inset-0 -z-10">
92
+ <div class="aurora-drift absolute -left-[10%] -top-[30%] h-[28rem] w-[28rem] rounded-full bg-primary/30 blur-3xl" />
93
+ <div class="aurora-drift absolute -right-[10%] top-[10%] h-[24rem] w-[24rem] rounded-full bg-accent/25 blur-3xl [animation-delay:-9s]" />
94
+ </div>
95
+ </Show>
96
+ <Show when={photoBackdrop()}>
97
+ <img
98
+ src={props.image}
99
+ alt={props.imageAlt ?? ''}
100
+ class="absolute inset-0 -z-20 h-full w-full object-cover"
101
+ />
102
+ <div
103
+ aria-hidden="true"
104
+ class="absolute inset-0 -z-10 bg-gradient-to-b from-black/70 via-black/55 to-black/75"
105
+ />
106
+ </Show>
107
+
108
+ <div
109
+ class={cn(
110
+ 'mx-auto grid w-full max-w-6xl gap-10 px-5 py-20 sm:py-28',
111
+ layout() === 'split' && showInlineImage() && 'items-center md:grid-cols-2',
112
+ )}
113
+ >
114
+ <div
115
+ class={cn(
116
+ 'flex flex-col gap-8',
117
+ layout() === 'center' ? 'items-center text-center' : 'items-start',
118
+ )}
119
+ >
120
+ <SectionHeading
121
+ level={1}
122
+ align={layout() === 'center' ? 'center' : 'left'}
123
+ eyebrow={props.eyebrow}
124
+ title={props.title}
125
+ subtitle={props.subtitle}
126
+ class={cn(
127
+ layout() === 'center' && 'max-w-3xl',
128
+ photoBackdrop() && '[&_h1]:text-white [&_p]:text-white/85',
129
+ )}
130
+ />
131
+ {actions}
132
+ </div>
133
+ <Show when={showInlineImage()}>
134
+ <img
135
+ src={props.image}
136
+ alt={props.imageAlt ?? ''}
137
+ class={cn(
138
+ 'card w-full rounded-2xl object-cover shadow-xl',
139
+ layout() === 'center' ? 'mx-auto aspect-[16/9] max-w-4xl' : 'aspect-[4/3]',
140
+ )}
141
+ />
142
+ </Show>
143
+ </div>
144
+ </section>
145
+ )
146
+ }