@a4ui/core 0.37.0 → 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 (105) 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 -15098
  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/lib/anchored.d.ts +16 -0
  21. package/dist/motion-CTIBwMCA.js +104 -0
  22. package/dist/styles.css +110 -3
  23. package/dist/ui/AspectRatio.d.ts +19 -0
  24. package/dist/ui/Attachment.d.ts +70 -0
  25. package/dist/ui/ButtonGroup.d.ts +37 -0
  26. package/dist/ui/ChatMarker.d.ts +20 -0
  27. package/dist/ui/ChatThread.d.ts +32 -8
  28. package/dist/ui/ContactSection.d.ts +49 -0
  29. package/dist/ui/CtaBanner.d.ts +28 -0
  30. package/dist/ui/DateField.d.ts +2 -2
  31. package/dist/ui/Drawer.d.ts +5 -2
  32. package/dist/ui/Faq.d.ts +29 -0
  33. package/dist/ui/FeatureGrid.d.ts +35 -0
  34. package/dist/ui/Field.d.ts +148 -0
  35. package/dist/ui/Hero.d.ts +48 -0
  36. package/dist/ui/InputGroup.d.ts +93 -0
  37. package/dist/ui/Item.d.ts +63 -0
  38. package/dist/ui/Label.d.ts +23 -0
  39. package/dist/ui/Menubar.d.ts +83 -0
  40. package/dist/ui/Message.d.ts +51 -5
  41. package/dist/ui/Modal.d.ts +8 -2
  42. package/dist/ui/NativeSelect.d.ts +26 -0
  43. package/dist/ui/NavigationMenu.d.ts +85 -0
  44. package/dist/ui/PromptComposer.d.ts +5 -0
  45. package/dist/ui/Prose.d.ts +21 -0
  46. package/dist/ui/Questionnaire.d.ts +71 -0
  47. package/dist/ui/ScrollArea.d.ts +30 -0
  48. package/dist/ui/Sidebar.d.ts +229 -0
  49. package/dist/ui/SiteFooter.d.ts +39 -0
  50. package/dist/ui/SiteNav.d.ts +35 -0
  51. package/dist/ui/TeamGrid.d.ts +33 -0
  52. package/dist/ui/Testimonials.d.ts +37 -0
  53. package/dist/ui/internal/sectionKit.d.ts +22 -0
  54. package/dist/ui/internal/socialIcons.d.ts +9 -0
  55. package/package.json +38 -26
  56. package/preset.d.ts +23 -0
  57. package/src/charts/AreaChart.tsx +206 -0
  58. package/src/charts/BarChart.tsx +241 -5
  59. package/src/charts/DonutChart.tsx +8 -11
  60. package/src/charts/LineChart.tsx +2 -3
  61. package/src/charts/PieChart.tsx +145 -0
  62. package/src/charts/RadialBarChart.tsx +141 -0
  63. package/src/charts/index.ts +14 -0
  64. package/src/charts/shared.tsx +304 -0
  65. package/src/index.ts +192 -3
  66. package/src/layout/Aurora.tsx +36 -7
  67. package/src/layout/sceneEffects.ts +22 -3
  68. package/src/lib/anchored.ts +57 -0
  69. package/src/ui/AspectRatio.tsx +35 -0
  70. package/src/ui/Attachment.tsx +189 -0
  71. package/src/ui/ButtonGroup.tsx +89 -0
  72. package/src/ui/ChatMarker.tsx +58 -0
  73. package/src/ui/ChatThread.tsx +115 -23
  74. package/src/ui/ContactSection.tsx +199 -0
  75. package/src/ui/CtaBanner.tsx +62 -0
  76. package/src/ui/DateField.tsx +17 -7
  77. package/src/ui/DateRangePicker.tsx +15 -5
  78. package/src/ui/Drawer.tsx +37 -4
  79. package/src/ui/Faq.tsx +57 -0
  80. package/src/ui/FeatureGrid.tsx +77 -0
  81. package/src/ui/Field.tsx +374 -0
  82. package/src/ui/Hero.tsx +146 -0
  83. package/src/ui/InputGroup.tsx +219 -0
  84. package/src/ui/Item.tsx +185 -0
  85. package/src/ui/Label.tsx +50 -0
  86. package/src/ui/Menubar.tsx +212 -0
  87. package/src/ui/Message.tsx +139 -13
  88. package/src/ui/Modal.tsx +41 -5
  89. package/src/ui/MultiSelect.tsx +15 -5
  90. package/src/ui/NativeSelect.tsx +66 -0
  91. package/src/ui/NavigationMenu.tsx +262 -0
  92. package/src/ui/PromptComposer.tsx +11 -0
  93. package/src/ui/Prose.tsx +63 -0
  94. package/src/ui/Questionnaire.tsx +331 -0
  95. package/src/ui/ScrollArea.tsx +70 -0
  96. package/src/ui/Sidebar.tsx +753 -0
  97. package/src/ui/SiteFooter.tsx +112 -0
  98. package/src/ui/SiteNav.tsx +146 -0
  99. package/src/ui/TeamGrid.tsx +81 -0
  100. package/src/ui/Testimonials.tsx +103 -0
  101. package/src/ui/TimeField.tsx +15 -5
  102. package/src/ui/internal/sectionKit.tsx +72 -0
  103. package/src/ui/internal/socialIcons.tsx +41 -0
  104. package/dist/NumberInput-Bnh2u7pp.js +0 -381
  105. 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
+ }
@@ -8,6 +8,7 @@ import type { JSX } from 'solid-js'
8
8
  import { For, Show, createEffect, createMemo, createSignal, onCleanup } from 'solid-js'
9
9
  import { Portal } from 'solid-js/web'
10
10
 
11
+ import { createAnchorReflow } from '../lib/anchored'
11
12
  import { cn } from '../lib/cn'
12
13
 
13
14
  type AmPm = 'AM' | 'PM'
@@ -146,17 +147,25 @@ export function TimeField(props: TimeFieldProps): JSX.Element {
146
147
 
147
148
  // Wire/tear-down the global listeners strictly around the open state. Scroll/
148
149
  // resize close the popover (its fixed position would otherwise drift).
150
+ // Follow the trigger rather than closing. The panel is `fixed` at the
151
+ // trigger's rect, so a scroll makes that rect stale — it does not make the
152
+ // panel unwanted. Closing on any captured scroll made this unusable on a
153
+ // scrolling page: clicking a trigger below the fold scrolls it into view,
154
+ // and that scroll arrived right after the panel opened and shut it in the
155
+ // same gesture. See `createAnchorReflow`.
156
+ const reflow = createAnchorReflow({ anchor: () => btnRef, place, close })
157
+
149
158
  const toggleListeners = (isOpen: boolean) => {
150
159
  if (isOpen) {
151
160
  document.addEventListener('mousedown', onDocPointer)
152
161
  document.addEventListener('keydown', onKey)
153
- window.addEventListener('scroll', close, true)
154
- window.addEventListener('resize', close)
162
+ window.addEventListener('scroll', reflow, true)
163
+ window.addEventListener('resize', reflow)
155
164
  } else {
156
165
  document.removeEventListener('mousedown', onDocPointer)
157
166
  document.removeEventListener('keydown', onKey)
158
- window.removeEventListener('scroll', close, true)
159
- window.removeEventListener('resize', close)
167
+ window.removeEventListener('scroll', reflow, true)
168
+ window.removeEventListener('resize', reflow)
160
169
  }
161
170
  }
162
171
  // Reactively attach when open flips; always clean up on unmount.
@@ -214,9 +223,10 @@ export function TimeField(props: TimeFieldProps): JSX.Element {
214
223
  <Portal>
215
224
  <div
216
225
  ref={popRef}
226
+ data-a4-floating
217
227
  style={{ position: 'fixed', top: `${pos().top}px`, left: `${pos().left}px` }}
218
228
  class={cn(
219
- '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',
220
230
  props.hour12 ? 'w-56' : 'w-44',
221
231
  )}
222
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
+ }