@a4ui/core 0.37.1 → 0.38.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -7
- package/dist/Collapse-Cr5cDqAl.js +375 -0
- package/dist/charts/AreaChart.d.ts +38 -0
- package/dist/charts/BarChart.d.ts +17 -1
- package/dist/charts/PieChart.d.ts +29 -0
- package/dist/charts/RadialBarChart.d.ts +33 -0
- package/dist/charts/index.d.ts +5 -0
- package/dist/charts/shared.d.ts +111 -0
- package/dist/charts.js +1493 -780
- package/dist/commerce.js +625 -634
- package/dist/elements.css +1076 -8
- package/dist/elements.iife.js +1 -6
- package/dist/elements.js +7774 -9133
- package/dist/full.css +1076 -8
- package/dist/index.d.ts +30 -3
- package/dist/index.js +17502 -15126
- package/dist/layout/Aurora.d.ts +16 -0
- package/dist/layout/EffectsToggle.d.ts +1 -1
- package/dist/layout/SpaceBackground.d.ts +1 -1
- package/dist/motion-CTIBwMCA.js +104 -0
- package/dist/styles.css +110 -3
- package/dist/ui/AspectRatio.d.ts +19 -0
- package/dist/ui/Attachment.d.ts +70 -0
- package/dist/ui/ButtonGroup.d.ts +37 -0
- package/dist/ui/ChatMarker.d.ts +20 -0
- package/dist/ui/ChatThread.d.ts +32 -8
- package/dist/ui/ContactSection.d.ts +49 -0
- package/dist/ui/CtaBanner.d.ts +28 -0
- package/dist/ui/Drawer.d.ts +5 -2
- package/dist/ui/Faq.d.ts +29 -0
- package/dist/ui/FeatureGrid.d.ts +35 -0
- package/dist/ui/Field.d.ts +148 -0
- package/dist/ui/Hero.d.ts +48 -0
- package/dist/ui/InputGroup.d.ts +93 -0
- package/dist/ui/Item.d.ts +63 -0
- package/dist/ui/Label.d.ts +23 -0
- package/dist/ui/Menubar.d.ts +83 -0
- package/dist/ui/Message.d.ts +51 -5
- package/dist/ui/Modal.d.ts +8 -2
- package/dist/ui/NativeSelect.d.ts +26 -0
- package/dist/ui/NavigationMenu.d.ts +85 -0
- package/dist/ui/PromptComposer.d.ts +5 -0
- package/dist/ui/Prose.d.ts +21 -0
- package/dist/ui/Questionnaire.d.ts +71 -0
- package/dist/ui/ScrollArea.d.ts +30 -0
- package/dist/ui/Sidebar.d.ts +229 -0
- package/dist/ui/SiteFooter.d.ts +39 -0
- package/dist/ui/SiteNav.d.ts +35 -0
- package/dist/ui/TeamGrid.d.ts +33 -0
- package/dist/ui/Testimonials.d.ts +37 -0
- package/dist/ui/internal/sectionKit.d.ts +22 -0
- package/dist/ui/internal/socialIcons.d.ts +9 -0
- package/package.json +38 -26
- package/preset.d.ts +23 -0
- package/src/charts/AreaChart.tsx +206 -0
- package/src/charts/BarChart.tsx +241 -5
- package/src/charts/DonutChart.tsx +8 -11
- package/src/charts/LineChart.tsx +2 -3
- package/src/charts/PieChart.tsx +145 -0
- package/src/charts/RadialBarChart.tsx +141 -0
- package/src/charts/index.ts +14 -0
- package/src/charts/shared.tsx +304 -0
- package/src/index.ts +192 -3
- package/src/layout/Aurora.tsx +36 -7
- package/src/layout/sceneEffects.ts +22 -3
- package/src/ui/AlertDialog.tsx +6 -3
- package/src/ui/AspectRatio.tsx +35 -0
- package/src/ui/Attachment.tsx +189 -0
- package/src/ui/ButtonGroup.tsx +89 -0
- package/src/ui/ChatMarker.tsx +58 -0
- package/src/ui/ChatThread.tsx +115 -23
- package/src/ui/Command.tsx +6 -3
- package/src/ui/ContactSection.tsx +199 -0
- package/src/ui/CtaBanner.tsx +62 -0
- package/src/ui/DateField.tsx +2 -1
- package/src/ui/DateRangePicker.tsx +2 -1
- package/src/ui/Drawer.tsx +43 -7
- package/src/ui/Faq.tsx +57 -0
- package/src/ui/FeatureGrid.tsx +77 -0
- package/src/ui/Field.tsx +374 -0
- package/src/ui/Hero.tsx +146 -0
- package/src/ui/Image.tsx +6 -3
- package/src/ui/InputGroup.tsx +219 -0
- package/src/ui/Item.tsx +185 -0
- package/src/ui/Label.tsx +50 -0
- package/src/ui/Menubar.tsx +212 -0
- package/src/ui/Message.tsx +139 -13
- package/src/ui/Modal.tsx +47 -8
- package/src/ui/MultiSelect.tsx +2 -1
- package/src/ui/NativeSelect.tsx +66 -0
- package/src/ui/NavigationMenu.tsx +262 -0
- package/src/ui/PromptComposer.tsx +11 -0
- package/src/ui/Prose.tsx +63 -0
- package/src/ui/Questionnaire.tsx +331 -0
- package/src/ui/ScrollArea.tsx +70 -0
- package/src/ui/Sidebar.tsx +753 -0
- package/src/ui/SiteFooter.tsx +112 -0
- package/src/ui/SiteNav.tsx +146 -0
- package/src/ui/TeamGrid.tsx +81 -0
- package/src/ui/Testimonials.tsx +103 -0
- package/src/ui/TimeField.tsx +2 -1
- package/src/ui/internal/sectionKit.tsx +72 -0
- package/src/ui/internal/socialIcons.tsx +41 -0
- package/dist/NumberInput-Bnh2u7pp.js +0 -381
- package/dist/motion-jYMWmZqB.js +0 -119
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// SiteFooter — the bottom of a marketing site: brand and tagline, link columns,
|
|
2
|
+
// social links and a copyright line.
|
|
3
|
+
import type { JSX } from 'solid-js'
|
|
4
|
+
import { For, Show } from 'solid-js'
|
|
5
|
+
|
|
6
|
+
import { cn } from '../lib/cn'
|
|
7
|
+
import type { SectionLink } from './internal/sectionKit'
|
|
8
|
+
import { SOCIAL_LABEL, SocialIcon, type SocialNetwork } from './internal/socialIcons'
|
|
9
|
+
|
|
10
|
+
export type { SocialNetwork }
|
|
11
|
+
|
|
12
|
+
/** A titled group of links in a {@link SiteFooter}. */
|
|
13
|
+
export interface FooterColumn {
|
|
14
|
+
title: string
|
|
15
|
+
links: SectionLink[]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** A social profile link in a {@link SiteFooter}. */
|
|
19
|
+
export interface SocialLink {
|
|
20
|
+
network: SocialNetwork
|
|
21
|
+
href: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface SiteFooterProps {
|
|
25
|
+
brand: string
|
|
26
|
+
/** One line under the brand. */
|
|
27
|
+
tagline?: string
|
|
28
|
+
columns?: FooterColumn[]
|
|
29
|
+
social?: SocialLink[]
|
|
30
|
+
/** Copyright line. Defaults to `© <current year> <brand>`. */
|
|
31
|
+
copyright?: string
|
|
32
|
+
class?: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Site footer: brand and tagline, columns of links, social network icons and a
|
|
37
|
+
* copyright line on a glass band. Columns wrap to two per row on phones.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```tsx
|
|
41
|
+
* <SiteFooter
|
|
42
|
+
* brand="Northwind"
|
|
43
|
+
* tagline="Websites for small businesses."
|
|
44
|
+
* columns={[{ title: 'Company', links: [{ label: 'About', href: '/about' }] }]}
|
|
45
|
+
* social={[{ network: 'instagram', href: 'https://instagram.com/northwind' }]}
|
|
46
|
+
* />
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export function SiteFooter(props: SiteFooterProps): JSX.Element {
|
|
50
|
+
const copyright = () => props.copyright ?? `© ${new Date().getFullYear()} ${props.brand}`
|
|
51
|
+
return (
|
|
52
|
+
<footer class={cn('bg-glass w-full border-t border-border/60 backdrop-blur-xl', props.class)}>
|
|
53
|
+
<div class="mx-auto flex w-full max-w-6xl flex-col gap-10 px-5 py-14">
|
|
54
|
+
<div class="flex flex-col gap-10 lg:flex-row lg:justify-between">
|
|
55
|
+
<div class="flex max-w-xs flex-col gap-3">
|
|
56
|
+
<p class="text-lg font-semibold tracking-tight text-foreground">{props.brand}</p>
|
|
57
|
+
<Show when={props.tagline}>
|
|
58
|
+
<p class="text-sm text-muted-foreground">{props.tagline}</p>
|
|
59
|
+
</Show>
|
|
60
|
+
</div>
|
|
61
|
+
<Show when={props.columns?.length}>
|
|
62
|
+
<div class="grid grid-cols-2 gap-8 sm:grid-cols-3 lg:gap-16">
|
|
63
|
+
<For each={props.columns}>
|
|
64
|
+
{(column) => (
|
|
65
|
+
<nav aria-label={column.title} class="flex flex-col gap-3">
|
|
66
|
+
<p class="text-sm font-semibold text-foreground">{column.title}</p>
|
|
67
|
+
<ul class="flex flex-col gap-2">
|
|
68
|
+
<For each={column.links}>
|
|
69
|
+
{(link) => (
|
|
70
|
+
<li>
|
|
71
|
+
<a
|
|
72
|
+
href={link.href}
|
|
73
|
+
class="text-sm text-muted-foreground transition-colors hover:text-foreground"
|
|
74
|
+
>
|
|
75
|
+
{link.label}
|
|
76
|
+
</a>
|
|
77
|
+
</li>
|
|
78
|
+
)}
|
|
79
|
+
</For>
|
|
80
|
+
</ul>
|
|
81
|
+
</nav>
|
|
82
|
+
)}
|
|
83
|
+
</For>
|
|
84
|
+
</div>
|
|
85
|
+
</Show>
|
|
86
|
+
</div>
|
|
87
|
+
<div class="flex flex-col-reverse items-start gap-4 border-t border-border/60 pt-6 sm:flex-row sm:items-center sm:justify-between">
|
|
88
|
+
<p class="text-xs text-muted-foreground">{copyright()}</p>
|
|
89
|
+
<Show when={props.social?.length}>
|
|
90
|
+
<ul class="flex flex-wrap gap-1">
|
|
91
|
+
<For each={props.social}>
|
|
92
|
+
{(item) => (
|
|
93
|
+
<li>
|
|
94
|
+
<a
|
|
95
|
+
href={item.href}
|
|
96
|
+
target="_blank"
|
|
97
|
+
rel="noreferrer"
|
|
98
|
+
aria-label={SOCIAL_LABEL[item.network]}
|
|
99
|
+
class="grid h-10 w-10 place-items-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
|
100
|
+
>
|
|
101
|
+
<SocialIcon network={item.network} class="h-4 w-4" />
|
|
102
|
+
</a>
|
|
103
|
+
</li>
|
|
104
|
+
)}
|
|
105
|
+
</For>
|
|
106
|
+
</ul>
|
|
107
|
+
</Show>
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
</footer>
|
|
111
|
+
)
|
|
112
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// SiteNav — the top navigation bar of a marketing site: brand, links, an
|
|
2
|
+
// optional call-to-action, and a disclosure menu below `md`.
|
|
3
|
+
import { Menu, X } from 'lucide-solid'
|
|
4
|
+
import type { JSX } from 'solid-js'
|
|
5
|
+
import { createSignal, createUniqueId, For, onCleanup, onMount, Show } from 'solid-js'
|
|
6
|
+
|
|
7
|
+
import { cn } from '../lib/cn'
|
|
8
|
+
import { Button } from './Button'
|
|
9
|
+
import type { SectionLink } from './internal/sectionKit'
|
|
10
|
+
|
|
11
|
+
export interface SiteNavProps {
|
|
12
|
+
/** Brand name shown at the left; also the home link's text. */
|
|
13
|
+
brand: string
|
|
14
|
+
/** Logo image URL shown before the brand name. */
|
|
15
|
+
logo?: string
|
|
16
|
+
/** Where the brand links to. Defaults to `'/'`. */
|
|
17
|
+
brandHref?: string
|
|
18
|
+
/** Navigation links, in order. */
|
|
19
|
+
links: SectionLink[]
|
|
20
|
+
/** Call-to-action button at the right (and at the bottom of the mobile menu). */
|
|
21
|
+
cta?: SectionLink
|
|
22
|
+
/** Stick to the top of the viewport while scrolling. Defaults to `true`. */
|
|
23
|
+
sticky?: boolean
|
|
24
|
+
/** Accessible label of the mobile menu button. Defaults to `'Menu'`. */
|
|
25
|
+
menuLabel?: string
|
|
26
|
+
class?: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const LINK =
|
|
30
|
+
'rounded-md px-3 py-2 text-sm font-medium text-muted-foreground transition-colors hover:text-foreground'
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Top navigation bar for a marketing site: brand (with optional logo), links and
|
|
34
|
+
* a call-to-action on a frosted glass bar. Below the `md` breakpoint the links
|
|
35
|
+
* collapse into a menu button that opens a panel under the bar; it closes on
|
|
36
|
+
* Escape, on outside click and when a link is followed.
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```tsx
|
|
40
|
+
* <SiteNav
|
|
41
|
+
* brand="Northwind"
|
|
42
|
+
* links={[{ label: 'Features', href: '#features' }, { label: 'Pricing', href: '#pricing' }]}
|
|
43
|
+
* cta={{ label: 'Get started', href: '/signup' }}
|
|
44
|
+
* />
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export function SiteNav(props: SiteNavProps): JSX.Element {
|
|
48
|
+
const [open, setOpen] = createSignal(false)
|
|
49
|
+
const panelId = createUniqueId()
|
|
50
|
+
let root: HTMLElement | undefined
|
|
51
|
+
|
|
52
|
+
onMount(() => {
|
|
53
|
+
const onKey = (event: KeyboardEvent) => {
|
|
54
|
+
if (event.key === 'Escape') setOpen(false)
|
|
55
|
+
}
|
|
56
|
+
const onDown = (event: PointerEvent) => {
|
|
57
|
+
if (open() && root && !root.contains(event.target as Node)) setOpen(false)
|
|
58
|
+
}
|
|
59
|
+
document.addEventListener('keydown', onKey)
|
|
60
|
+
document.addEventListener('pointerdown', onDown)
|
|
61
|
+
onCleanup(() => {
|
|
62
|
+
document.removeEventListener('keydown', onKey)
|
|
63
|
+
document.removeEventListener('pointerdown', onDown)
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<header
|
|
69
|
+
ref={root}
|
|
70
|
+
class={cn(
|
|
71
|
+
'bg-glass z-40 w-full border-b border-border/60 backdrop-blur-xl',
|
|
72
|
+
props.sticky !== false && 'sticky top-0',
|
|
73
|
+
props.class,
|
|
74
|
+
)}
|
|
75
|
+
>
|
|
76
|
+
<nav
|
|
77
|
+
aria-label="Main"
|
|
78
|
+
class="mx-auto flex h-16 w-full max-w-6xl items-center justify-between gap-4 px-5"
|
|
79
|
+
>
|
|
80
|
+
<a href={props.brandHref ?? '/'} class="flex min-w-0 items-center gap-2 text-foreground">
|
|
81
|
+
<Show when={props.logo}>
|
|
82
|
+
<img src={props.logo} alt="" class="h-8 w-8 shrink-0 rounded-md object-contain" />
|
|
83
|
+
</Show>
|
|
84
|
+
<span class="truncate text-base font-semibold tracking-tight">{props.brand}</span>
|
|
85
|
+
</a>
|
|
86
|
+
|
|
87
|
+
<ul class="hidden items-center gap-1 md:flex">
|
|
88
|
+
<For each={props.links}>
|
|
89
|
+
{(link) => (
|
|
90
|
+
<li>
|
|
91
|
+
<a href={link.href} class={LINK}>
|
|
92
|
+
{link.label}
|
|
93
|
+
</a>
|
|
94
|
+
</li>
|
|
95
|
+
)}
|
|
96
|
+
</For>
|
|
97
|
+
</ul>
|
|
98
|
+
|
|
99
|
+
<div class="flex items-center gap-2">
|
|
100
|
+
<Show when={props.cta}>
|
|
101
|
+
{(cta) => (
|
|
102
|
+
<Button href={cta().href} class="hidden md:inline-flex">
|
|
103
|
+
{cta().label}
|
|
104
|
+
</Button>
|
|
105
|
+
)}
|
|
106
|
+
</Show>
|
|
107
|
+
<button
|
|
108
|
+
type="button"
|
|
109
|
+
class="inline-flex h-10 w-10 items-center justify-center rounded-md text-foreground hover:bg-muted focus:outline-none focus:ring-2 focus:ring-ring md:hidden"
|
|
110
|
+
aria-label={props.menuLabel ?? 'Menu'}
|
|
111
|
+
aria-expanded={open()}
|
|
112
|
+
aria-controls={panelId}
|
|
113
|
+
onClick={() => setOpen((value) => !value)}
|
|
114
|
+
>
|
|
115
|
+
<Show when={open()} fallback={<Menu class="h-5 w-5" aria-hidden="true" />}>
|
|
116
|
+
<X class="h-5 w-5" aria-hidden="true" />
|
|
117
|
+
</Show>
|
|
118
|
+
</button>
|
|
119
|
+
</div>
|
|
120
|
+
</nav>
|
|
121
|
+
|
|
122
|
+
<div id={panelId} hidden={!open()} class="border-t border-border/60 md:hidden">
|
|
123
|
+
<ul class="mx-auto flex max-w-6xl flex-col gap-1 px-5 py-3">
|
|
124
|
+
<For each={props.links}>
|
|
125
|
+
{(link) => (
|
|
126
|
+
<li>
|
|
127
|
+
<a href={link.href} class={cn(LINK, 'block py-3 text-base')} onClick={() => setOpen(false)}>
|
|
128
|
+
{link.label}
|
|
129
|
+
</a>
|
|
130
|
+
</li>
|
|
131
|
+
)}
|
|
132
|
+
</For>
|
|
133
|
+
<Show when={props.cta}>
|
|
134
|
+
{(cta) => (
|
|
135
|
+
<li class="pt-2">
|
|
136
|
+
<Button href={cta().href} class="w-full" onClick={() => setOpen(false)}>
|
|
137
|
+
{cta().label}
|
|
138
|
+
</Button>
|
|
139
|
+
</li>
|
|
140
|
+
)}
|
|
141
|
+
</Show>
|
|
142
|
+
</ul>
|
|
143
|
+
</div>
|
|
144
|
+
</header>
|
|
145
|
+
)
|
|
146
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// TeamGrid — the people behind a business: photo, name, role and a short bio.
|
|
2
|
+
import type { JSX } from 'solid-js'
|
|
3
|
+
import { For, Show } from 'solid-js'
|
|
4
|
+
|
|
5
|
+
import { cn } from '../lib/cn'
|
|
6
|
+
import { Avatar } from './Avatar'
|
|
7
|
+
import { Card } from './Card'
|
|
8
|
+
import { initials, SectionHeading } from './internal/sectionKit'
|
|
9
|
+
|
|
10
|
+
/** One person rendered by {@link TeamGrid}. */
|
|
11
|
+
export interface TeamMember {
|
|
12
|
+
name: string
|
|
13
|
+
role: string
|
|
14
|
+
/** Photo URL. Falls back to the name's initials. */
|
|
15
|
+
photo?: string
|
|
16
|
+
/** One or two sentences about the person. */
|
|
17
|
+
bio?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface TeamGridProps {
|
|
21
|
+
title?: string
|
|
22
|
+
subtitle?: string
|
|
23
|
+
members: TeamMember[]
|
|
24
|
+
/** Columns on wide screens. Defaults to `3`. */
|
|
25
|
+
columns?: 2 | 3 | 4
|
|
26
|
+
/** Anchor id for in-page navigation. */
|
|
27
|
+
id?: string
|
|
28
|
+
class?: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const COLS: Record<2 | 3 | 4, string> = {
|
|
32
|
+
2: 'sm:grid-cols-2',
|
|
33
|
+
3: 'sm:grid-cols-2 lg:grid-cols-3',
|
|
34
|
+
4: 'sm:grid-cols-2 lg:grid-cols-4',
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Team section: a responsive grid of glass cards with each person's photo (or
|
|
39
|
+
* initials), name, role and an optional short bio.
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```tsx
|
|
43
|
+
* <TeamGrid
|
|
44
|
+
* title="Our team"
|
|
45
|
+
* members={[{ name: 'Ana Ruiz', role: 'Founder', bio: 'Ten years building for small shops.' }]}
|
|
46
|
+
* />
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export function TeamGrid(props: TeamGridProps): JSX.Element {
|
|
50
|
+
return (
|
|
51
|
+
<section
|
|
52
|
+
id={props.id}
|
|
53
|
+
class={cn('mx-auto flex w-full max-w-6xl flex-col gap-12 px-5 py-20', props.class)}
|
|
54
|
+
>
|
|
55
|
+
<SectionHeading title={props.title} subtitle={props.subtitle} />
|
|
56
|
+
<ul class={cn('grid gap-6', COLS[props.columns ?? 3])}>
|
|
57
|
+
<For each={props.members}>
|
|
58
|
+
{(member) => (
|
|
59
|
+
<li class="flex">
|
|
60
|
+
<Card glass class="flex w-full flex-col items-center gap-3 p-6 text-center">
|
|
61
|
+
<Avatar
|
|
62
|
+
src={member.photo}
|
|
63
|
+
alt=""
|
|
64
|
+
fallback={initials(member.name)}
|
|
65
|
+
class="h-24 w-24 text-lg"
|
|
66
|
+
/>
|
|
67
|
+
<div>
|
|
68
|
+
<h3 class="text-base font-semibold text-foreground">{member.name}</h3>
|
|
69
|
+
<p class="text-sm text-primary">{member.role}</p>
|
|
70
|
+
</div>
|
|
71
|
+
<Show when={member.bio}>
|
|
72
|
+
<p class="text-sm leading-relaxed text-muted-foreground">{member.bio}</p>
|
|
73
|
+
</Show>
|
|
74
|
+
</Card>
|
|
75
|
+
</li>
|
|
76
|
+
)}
|
|
77
|
+
</For>
|
|
78
|
+
</ul>
|
|
79
|
+
</section>
|
|
80
|
+
)
|
|
81
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// Testimonials — customer quotes as a grid of cards or a scrolling marquee.
|
|
2
|
+
import type { JSX } from 'solid-js'
|
|
3
|
+
import { For, Show } from 'solid-js'
|
|
4
|
+
|
|
5
|
+
import { cn } from '../lib/cn'
|
|
6
|
+
import { Avatar } from './Avatar'
|
|
7
|
+
import { Card } from './Card'
|
|
8
|
+
import { initials, SectionHeading } from './internal/sectionKit'
|
|
9
|
+
import { Marquee } from './Marquee'
|
|
10
|
+
|
|
11
|
+
/** One quote rendered by {@link Testimonials}. */
|
|
12
|
+
export interface TestimonialItem {
|
|
13
|
+
quote: string
|
|
14
|
+
/** Who said it. */
|
|
15
|
+
name: string
|
|
16
|
+
/** Job title and/or company, e.g. `"CTO, Northwind"`. */
|
|
17
|
+
role?: string
|
|
18
|
+
/** Avatar image URL. Falls back to the name's initials. */
|
|
19
|
+
avatar?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Arrangement of {@link Testimonials}. */
|
|
23
|
+
export type TestimonialsLayout = 'grid' | 'marquee'
|
|
24
|
+
|
|
25
|
+
export interface TestimonialsProps {
|
|
26
|
+
title?: string
|
|
27
|
+
subtitle?: string
|
|
28
|
+
items: TestimonialItem[]
|
|
29
|
+
/** `'grid'` (default) or a horizontally scrolling `'marquee'` (still under reduced motion). */
|
|
30
|
+
layout?: TestimonialsLayout
|
|
31
|
+
/** Anchor id for in-page navigation. */
|
|
32
|
+
id?: string
|
|
33
|
+
class?: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function Quote(props: { item: TestimonialItem; class?: string }): JSX.Element {
|
|
37
|
+
return (
|
|
38
|
+
<Card glass class={cn('flex h-full flex-col justify-between gap-6 p-6', props.class)}>
|
|
39
|
+
<blockquote class="text-pretty text-base leading-relaxed text-foreground">
|
|
40
|
+
<p>“{props.item.quote}”</p>
|
|
41
|
+
</blockquote>
|
|
42
|
+
<figcaption class="flex items-center gap-3">
|
|
43
|
+
<Avatar src={props.item.avatar} alt="" fallback={initials(props.item.name)} class="h-10 w-10" />
|
|
44
|
+
<div class="min-w-0">
|
|
45
|
+
<p class="truncate text-sm font-semibold text-foreground">{props.item.name}</p>
|
|
46
|
+
<Show when={props.item.role}>
|
|
47
|
+
<p class="truncate text-xs text-muted-foreground">{props.item.role}</p>
|
|
48
|
+
</Show>
|
|
49
|
+
</div>
|
|
50
|
+
</figcaption>
|
|
51
|
+
</Card>
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Social-proof section of customer quotes, each with the person's name, role
|
|
57
|
+
* and avatar (initials when no image). Lay them out as a responsive grid of
|
|
58
|
+
* glass cards, or as a marquee that scrolls sideways and pauses on hover.
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```tsx
|
|
62
|
+
* <Testimonials
|
|
63
|
+
* title="Loved by small teams"
|
|
64
|
+
* items={[{ quote: 'We launched in two days.', name: 'Ana Ruiz', role: 'Founder, Brisa' }]}
|
|
65
|
+
* />
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
export function Testimonials(props: TestimonialsProps): JSX.Element {
|
|
69
|
+
return (
|
|
70
|
+
<section id={props.id} class={cn('mx-auto flex w-full max-w-6xl flex-col gap-12 py-20', props.class)}>
|
|
71
|
+
<SectionHeading title={props.title} subtitle={props.subtitle} class="px-5" />
|
|
72
|
+
<Show
|
|
73
|
+
when={props.layout === 'marquee'}
|
|
74
|
+
fallback={
|
|
75
|
+
<ul class="grid gap-6 px-5 sm:grid-cols-2 lg:grid-cols-3">
|
|
76
|
+
<For each={props.items}>
|
|
77
|
+
{(item) => (
|
|
78
|
+
<li>
|
|
79
|
+
<figure class="h-full">
|
|
80
|
+
<Quote item={item} />
|
|
81
|
+
</figure>
|
|
82
|
+
</li>
|
|
83
|
+
)}
|
|
84
|
+
</For>
|
|
85
|
+
</ul>
|
|
86
|
+
}
|
|
87
|
+
>
|
|
88
|
+
<Marquee
|
|
89
|
+
speed={Math.max(20, props.items.length * 8)}
|
|
90
|
+
class="[mask-image:linear-gradient(to_right,transparent,black_8%,black_92%,transparent)]"
|
|
91
|
+
>
|
|
92
|
+
<For each={props.items}>
|
|
93
|
+
{(item) => (
|
|
94
|
+
<figure class="mx-3 w-[18rem] shrink-0 sm:w-[22rem]">
|
|
95
|
+
<Quote item={item} />
|
|
96
|
+
</figure>
|
|
97
|
+
)}
|
|
98
|
+
</For>
|
|
99
|
+
</Marquee>
|
|
100
|
+
</Show>
|
|
101
|
+
</section>
|
|
102
|
+
)
|
|
103
|
+
}
|
package/src/ui/TimeField.tsx
CHANGED
|
@@ -223,9 +223,10 @@ export function TimeField(props: TimeFieldProps): JSX.Element {
|
|
|
223
223
|
<Portal>
|
|
224
224
|
<div
|
|
225
225
|
ref={popRef}
|
|
226
|
+
data-a4-floating
|
|
226
227
|
style={{ position: 'fixed', top: `${pos().top}px`, left: `${pos().left}px` }}
|
|
227
228
|
class={cn(
|
|
228
|
-
'z-50 flex gap-1 divide-x divide-border rounded-lg border border-border bg-card p-2 text-card-foreground shadow-lg',
|
|
229
|
+
'pointer-events-auto z-50 flex gap-1 divide-x divide-border rounded-lg border border-border bg-card p-2 text-card-foreground shadow-lg',
|
|
229
230
|
props.hour12 ? 'w-56' : 'w-44',
|
|
230
231
|
)}
|
|
231
232
|
>
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Shared pieces of the page-section blocks (Hero, FeatureGrid, Testimonials,
|
|
2
|
+
// Faq, CtaBanner, ContactSection, TeamGrid, SiteNav, SiteFooter): the plain-data
|
|
3
|
+
// link type and the centered heading every section opens with.
|
|
4
|
+
import type { JSX } from 'solid-js'
|
|
5
|
+
import { Show } from 'solid-js'
|
|
6
|
+
|
|
7
|
+
import { cn } from '../../lib/cn'
|
|
8
|
+
|
|
9
|
+
/** A plain link used by the page-section blocks: visible text plus a URL. */
|
|
10
|
+
export interface SectionLink {
|
|
11
|
+
/** Visible text. */
|
|
12
|
+
label: string
|
|
13
|
+
/** Destination URL, in-page anchor (`#features`) or `mailto:`/`tel:` link. */
|
|
14
|
+
href: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface SectionHeadingProps {
|
|
18
|
+
eyebrow?: string
|
|
19
|
+
title?: string
|
|
20
|
+
subtitle?: string
|
|
21
|
+
/** Heading level of `title`. Defaults to `2`. */
|
|
22
|
+
level?: 1 | 2
|
|
23
|
+
align?: 'center' | 'left'
|
|
24
|
+
class?: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Eyebrow + title + subtitle stack. Renders nothing when all three are empty. */
|
|
28
|
+
export function SectionHeading(props: SectionHeadingProps): JSX.Element {
|
|
29
|
+
const center = () => (props.align ?? 'center') === 'center'
|
|
30
|
+
return (
|
|
31
|
+
<Show when={props.eyebrow || props.title || props.subtitle}>
|
|
32
|
+
<div
|
|
33
|
+
class={cn(
|
|
34
|
+
'flex flex-col gap-3',
|
|
35
|
+
center() && 'mx-auto max-w-2xl items-center text-center',
|
|
36
|
+
props.class,
|
|
37
|
+
)}
|
|
38
|
+
>
|
|
39
|
+
<Show when={props.eyebrow}>
|
|
40
|
+
<p class="text-xs font-semibold uppercase tracking-[0.2em] text-primary">{props.eyebrow}</p>
|
|
41
|
+
</Show>
|
|
42
|
+
<Show when={props.title}>
|
|
43
|
+
<Show
|
|
44
|
+
when={props.level === 1}
|
|
45
|
+
fallback={
|
|
46
|
+
<h2 class="text-balance text-3xl font-semibold tracking-tight text-foreground sm:text-4xl">
|
|
47
|
+
{props.title}
|
|
48
|
+
</h2>
|
|
49
|
+
}
|
|
50
|
+
>
|
|
51
|
+
<h1 class="text-balance text-4xl font-bold tracking-tight text-foreground sm:text-5xl lg:text-6xl">
|
|
52
|
+
{props.title}
|
|
53
|
+
</h1>
|
|
54
|
+
</Show>
|
|
55
|
+
</Show>
|
|
56
|
+
<Show when={props.subtitle}>
|
|
57
|
+
<p class="text-pretty text-base text-muted-foreground sm:text-lg">{props.subtitle}</p>
|
|
58
|
+
</Show>
|
|
59
|
+
</div>
|
|
60
|
+
</Show>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Up-to-two-letter initials for avatar fallbacks. */
|
|
65
|
+
export function initials(name: string): string {
|
|
66
|
+
return name
|
|
67
|
+
.split(/\s+/)
|
|
68
|
+
.filter(Boolean)
|
|
69
|
+
.slice(0, 2)
|
|
70
|
+
.map((part) => part[0]?.toUpperCase() ?? '')
|
|
71
|
+
.join('')
|
|
72
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Social network glyphs for SiteFooter. Path data from Simple Icons (CC0 1.0),
|
|
2
|
+
// inlined so the library does not depend on an icon package that ships brand marks.
|
|
3
|
+
import type { JSX } from 'solid-js'
|
|
4
|
+
|
|
5
|
+
/** Social networks {@link SiteFooter} can link to. */
|
|
6
|
+
export type SocialNetwork = 'x' | 'instagram' | 'facebook' | 'linkedin' | 'github' | 'youtube' | 'tiktok'
|
|
7
|
+
|
|
8
|
+
const PATHS: Record<SocialNetwork, string> = {
|
|
9
|
+
x: 'M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z',
|
|
10
|
+
instagram:
|
|
11
|
+
'M7.0301.084c-1.2768.0602-2.1487.264-2.911.5634-.7888.3075-1.4575.72-2.1228 1.3877-.6652.6677-1.075 1.3368-1.3802 2.127-.2954.7638-.4956 1.6365-.552 2.914-.0564 1.2775-.0689 1.6882-.0626 4.947.0062 3.2586.0206 3.6671.0825 4.9473.061 1.2765.264 2.1482.5635 2.9107.308.7889.72 1.4573 1.388 2.1228.6679.6655 1.3365 1.0743 2.1285 1.38.7632.295 1.6361.4961 2.9134.552 1.2773.056 1.6884.069 4.9462.0627 3.2578-.0062 3.668-.0207 4.9478-.0814 1.28-.0607 2.147-.2652 2.9098-.5633.7889-.3086 1.4578-.72 2.1228-1.3881.665-.6682 1.0745-1.3378 1.3795-2.1284.2957-.7632.4966-1.636.552-2.9124.056-1.2809.0692-1.6898.063-4.948-.0063-3.2583-.021-3.6668-.0817-4.9465-.0607-1.2797-.264-2.1487-.5633-2.9117-.3084-.7889-.72-1.4568-1.3876-2.1228C21.2982 1.33 20.628.9208 19.8378.6165 19.074.321 18.2017.1197 16.9244.0645 15.6471.0093 15.236-.005 11.977.0014 8.718.0076 8.31.0215 7.0301.0839m.1402 21.6932c-1.17-.0509-1.8053-.2453-2.2287-.408-.5606-.216-.96-.4771-1.3819-.895-.422-.4178-.6811-.8186-.9-1.378-.1644-.4234-.3624-1.058-.4171-2.228-.0595-1.2645-.072-1.6442-.079-4.848-.007-3.2037.0053-3.583.0607-4.848.05-1.169.2456-1.805.408-2.2282.216-.5613.4762-.96.895-1.3816.4188-.4217.8184-.6814 1.3783-.9003.423-.1651 1.0575-.3614 2.227-.4171 1.2655-.06 1.6447-.072 4.848-.079 3.2033-.007 3.5835.005 4.8495.0608 1.169.0508 1.8053.2445 2.228.408.5608.216.96.4754 1.3816.895.4217.4194.6816.8176.9005 1.3787.1653.4217.3617 1.056.4169 2.2263.0602 1.2655.0739 1.645.0796 4.848.0058 3.203-.0055 3.5834-.061 4.848-.051 1.17-.245 1.8055-.408 2.2294-.216.5604-.4763.96-.8954 1.3814-.419.4215-.8181.6811-1.3783.9-.4224.1649-1.0577.3617-2.2262.4174-1.2656.0595-1.6448.072-4.8493.079-3.2045.007-3.5825-.006-4.848-.0608M16.953 5.5864A1.44 1.44 0 1 0 18.39 4.144a1.44 1.44 0 0 0-1.437 1.4424M5.8385 12.012c.0067 3.4032 2.7706 6.1557 6.173 6.1493 3.4026-.0065 6.157-2.7701 6.1506-6.1733-.0065-3.4032-2.771-6.1565-6.174-6.1498-3.403.0067-6.156 2.771-6.1496 6.1738M8 12.0077a4 4 0 1 1 4.008 3.9921A3.9996 3.9996 0 0 1 8 12.0077',
|
|
12
|
+
facebook:
|
|
13
|
+
'M9.101 23.691v-7.98H6.627v-3.667h2.474v-1.58c0-4.085 1.848-5.978 5.858-5.978.401 0 .955.042 1.468.103a8.68 8.68 0 0 1 1.141.195v3.325a8.623 8.623 0 0 0-.653-.036 26.805 26.805 0 0 0-.733-.009c-.707 0-1.259.096-1.675.309a1.686 1.686 0 0 0-.679.622c-.258.42-.374.995-.374 1.752v1.297h3.919l-.386 2.103-.287 1.564h-3.246v8.245C19.396 23.238 24 18.179 24 12.044c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.628 3.874 10.35 9.101 11.647Z',
|
|
14
|
+
linkedin:
|
|
15
|
+
'M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z',
|
|
16
|
+
github:
|
|
17
|
+
'M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12',
|
|
18
|
+
youtube:
|
|
19
|
+
'M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z',
|
|
20
|
+
tiktok:
|
|
21
|
+
'M12.525.02c1.31-.02 2.61-.01 3.91-.02.08 1.53.63 3.09 1.75 4.17 1.12 1.11 2.7 1.62 4.24 1.79v4.03c-1.44-.05-2.89-.35-4.2-.97-.57-.26-1.1-.59-1.62-.93-.01 2.92.01 5.84-.02 8.75-.08 1.4-.54 2.79-1.35 3.94-1.31 1.92-3.58 3.17-5.91 3.21-1.43.08-2.86-.31-4.08-1.03-2.02-1.19-3.44-3.37-3.65-5.71-.02-.5-.03-1-.01-1.49.18-1.9 1.12-3.72 2.58-4.96 1.66-1.44 3.98-2.13 6.15-1.72.02 1.48-.04 2.96-.04 4.44-.99-.32-2.15-.23-3.02.37-.63.41-1.11 1.04-1.36 1.75-.21.51-.15 1.07-.14 1.61.24 1.64 1.82 3.02 3.5 2.87 1.12-.01 2.19-.66 2.77-1.61.19-.33.4-.67.41-1.06.1-1.79.06-3.57.07-5.36.01-4.03-.01-8.05.02-12.07z',
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Human-readable network names, used as accessible link labels. */
|
|
25
|
+
export const SOCIAL_LABEL: Record<SocialNetwork, string> = {
|
|
26
|
+
x: 'X',
|
|
27
|
+
instagram: 'Instagram',
|
|
28
|
+
facebook: 'Facebook',
|
|
29
|
+
linkedin: 'LinkedIn',
|
|
30
|
+
github: 'GitHub',
|
|
31
|
+
youtube: 'YouTube',
|
|
32
|
+
tiktok: 'TikTok',
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function SocialIcon(props: { network: SocialNetwork; class?: string }): JSX.Element {
|
|
36
|
+
return (
|
|
37
|
+
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" class={props.class}>
|
|
38
|
+
<path d={PATHS[props.network]} />
|
|
39
|
+
</svg>
|
|
40
|
+
)
|
|
41
|
+
}
|