@arbidocs/blocks 0.3.138 → 0.3.139
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/dist/{chunk-AHWIZJ4M.cjs → chunk-6MFI2HPX.cjs} +107 -89
- package/dist/chunk-6MFI2HPX.cjs.map +1 -0
- package/dist/{chunk-L6GG4KN2.js → chunk-ETIYX2MO.js} +107 -90
- package/dist/chunk-ETIYX2MO.js.map +1 -0
- package/dist/index-BzrF2JbQ.d.cts +697 -0
- package/dist/index-BzrF2JbQ.d.ts +697 -0
- package/dist/index.cjs +300 -131
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +49 -11
- package/dist/index.d.ts +49 -11
- package/dist/index.js +170 -6
- package/dist/index.js.map +1 -1
- package/dist/site/index.cjs +53 -53
- package/dist/site/index.d.cts +4 -688
- package/dist/site/index.d.ts +4 -688
- package/dist/site/index.js +1 -1
- package/package.json +3 -3
- package/dist/chunk-AHWIZJ4M.cjs.map +0 -1
- package/dist/chunk-L6GG4KN2.js.map +0 -1
|
@@ -0,0 +1,697 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ComponentType, ReactNode } from 'react';
|
|
3
|
+
import { LucideIcon } from 'lucide-react';
|
|
4
|
+
import { UseBoundStore, StoreApi } from 'zustand';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The theme-token model. Firm-agnostic: the token *catalog*, default values and
|
|
8
|
+
* presets are supplied by the consuming app (via {@link createThemeStore}); this
|
|
9
|
+
* module owns the shared types and the logic for writing tokens onto the live
|
|
10
|
+
* document.
|
|
11
|
+
*
|
|
12
|
+
* There are two emit models, selected per-app by {@link ThemeEmitMode}:
|
|
13
|
+
*
|
|
14
|
+
* - `'flat'` (default): each colour key is written as an inline `--color-<key>`
|
|
15
|
+
* custom property on `<html>`, plus `--font-*` / `--radius`. Inline styles
|
|
16
|
+
* override the compiled `@theme` defaults, so an edit previews live across the
|
|
17
|
+
* whole app (and the @arbidocs AI kit) and reverts cleanly. Apps whose CSS sets
|
|
18
|
+
* `--color-*` HEX directly rely on this.
|
|
19
|
+
*
|
|
20
|
+
* - `'cssVars'`: each colour key is written as a raw
|
|
21
|
+
* HSL-triplet custom property (`--<key>: <h s l>`) into an injected
|
|
22
|
+
* `<style id="arbi-theme-vars">` appended to `<head>`. The monolith's source
|
|
23
|
+
* of truth is HSL triplets wrapped as `hsl(var(--x))`, with a real `.dark`
|
|
24
|
+
* variant; writing resolved hex inline would pin the colour and defeat both
|
|
25
|
+
* the indirection and the dark-class swap. The injected sheet is *unlayered*,
|
|
26
|
+
* so it wins over the monolith's `@layer base` `colors.css` at equal
|
|
27
|
+
* specificity, and a `:root { light } / :root.dark { dark }` structure keeps
|
|
28
|
+
* the dark cascade alive.
|
|
29
|
+
*/
|
|
30
|
+
interface ColorToken {
|
|
31
|
+
/** Key without the `--color-` prefix, e.g. "primary" → `--color-primary`. */
|
|
32
|
+
key: string;
|
|
33
|
+
label: string;
|
|
34
|
+
/** Grouping id for the panel layout (e.g. "shadcn" | "brand" | "ai"). */
|
|
35
|
+
group: string;
|
|
36
|
+
}
|
|
37
|
+
interface ThemeFonts {
|
|
38
|
+
display: string;
|
|
39
|
+
sans: string;
|
|
40
|
+
}
|
|
41
|
+
interface ThemeTokens {
|
|
42
|
+
/** Colour values keyed by token key (hex). */
|
|
43
|
+
colors: Record<string, string>;
|
|
44
|
+
fonts: ThemeFonts;
|
|
45
|
+
/** Base radius in rem. */
|
|
46
|
+
radius: number;
|
|
47
|
+
}
|
|
48
|
+
interface ThemePreset {
|
|
49
|
+
id: string;
|
|
50
|
+
label: string;
|
|
51
|
+
tokens: ThemeTokens;
|
|
52
|
+
}
|
|
53
|
+
/** How a theme is written onto the document. See the module doc for the models. */
|
|
54
|
+
type ThemeEmitMode = 'flat' | 'cssVars';
|
|
55
|
+
interface ApplyThemeOptions {
|
|
56
|
+
/** Emit model. Defaults to `'flat'`. */
|
|
57
|
+
mode?: ThemeEmitMode;
|
|
58
|
+
/**
|
|
59
|
+
* `cssVars` mode only: dark-variant colour values keyed by token key. Emitted
|
|
60
|
+
* under `:root.dark`. When omitted, only `:root` is emitted — note that an
|
|
61
|
+
* overridden light token then also applies in dark (the injected `:root` is
|
|
62
|
+
* unlayered and beats the app's own `.dark` rules), so a firm with a dark mode
|
|
63
|
+
* MUST supply this for every editable token to keep dark mode intact.
|
|
64
|
+
*/
|
|
65
|
+
dark?: Record<string, string>;
|
|
66
|
+
/** `flat` mode only: target element. Defaults to `document.documentElement`. */
|
|
67
|
+
root?: HTMLElement;
|
|
68
|
+
}
|
|
69
|
+
/** Id of the injected `<style>` that carries the `cssVars`-mode custom properties. */
|
|
70
|
+
declare const THEME_STYLE_ID = "arbi-theme-vars";
|
|
71
|
+
/**
|
|
72
|
+
* Convert a colour to the monolith's `H S% L%` HSL-triplet format.
|
|
73
|
+
*
|
|
74
|
+
* Handles `#rgb` / `#rrggbb`, `rgb()` / `rgba()`, and passes an existing triplet
|
|
75
|
+
* through untouched (so a firm can seed exact `colors.css` triplets for a
|
|
76
|
+
* byte-identical no-op). Values it cannot parse — notably `oklch(...)`, which the
|
|
77
|
+
* status tokens use — are returned unchanged so they remain usable as a raw
|
|
78
|
+
* custom-property value; converting those cleanly is out of scope.
|
|
79
|
+
*/
|
|
80
|
+
declare function toHslTriplet(color: string): string;
|
|
81
|
+
/**
|
|
82
|
+
* Write a theme onto the live document.
|
|
83
|
+
*
|
|
84
|
+
* In `flat` mode (default) writes inline `--color-<key>` / `--font-*` / `--radius`
|
|
85
|
+
* on `document.documentElement` (or a supplied root). In `cssVars` mode writes
|
|
86
|
+
* raw HSL-triplet vars into an injected `<style#arbi-theme-vars>` (see the module
|
|
87
|
+
* doc). The legacy `applyThemeVars(theme, rootElement)` call form still works.
|
|
88
|
+
*/
|
|
89
|
+
declare function applyThemeVars(theme: ThemeTokens, optionsOrRoot?: ApplyThemeOptions | HTMLElement): void;
|
|
90
|
+
/**
|
|
91
|
+
* Set ONLY the typography vars, as inline styles on the root element.
|
|
92
|
+
*
|
|
93
|
+
* Typography is often chosen independently of colour — an app may let a reader
|
|
94
|
+
* pick a typeface while its palette is owned elsewhere. Writing the two vars
|
|
95
|
+
* inline (rather than through `applyThemeVars`) keeps this orthogonal in two
|
|
96
|
+
* ways: it touches no colour or radius token, and an inline style outranks any
|
|
97
|
+
* selector, so a font choice survives a `cssVars`-mode theme that emits its own
|
|
98
|
+
* `:root` block (the ARBI studio's theme editor does exactly that).
|
|
99
|
+
*/
|
|
100
|
+
declare function applyFontVars(fonts: ThemeFonts, root?: HTMLElement): void;
|
|
101
|
+
/** Undo {@link applyFontVars}, reverting to the stylesheet's own font tokens. */
|
|
102
|
+
declare function clearFontVars(root?: HTMLElement): void;
|
|
103
|
+
/**
|
|
104
|
+
* Remove a theme's overrides, reverting to the stylesheet defaults.
|
|
105
|
+
*
|
|
106
|
+
* In `flat` mode removes the inline overrides for the theme's keys. In `cssVars`
|
|
107
|
+
* mode removes the injected `<style#arbi-theme-vars>` in full (restoring the
|
|
108
|
+
* app's own `colors.css` exactly).
|
|
109
|
+
*/
|
|
110
|
+
declare function clearThemeVars(theme: ThemeTokens, optionsOrRoot?: ApplyThemeOptions | HTMLElement): void;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The shared theme store factory. A firm calls {@link createThemeStore} once
|
|
114
|
+
* with its token catalog, default values and presets; the returned bundle (a
|
|
115
|
+
* zustand hook + metadata) is the single source of truth consumed by every
|
|
116
|
+
* theme surface (the back-office builder panel and the studio drawer), so the
|
|
117
|
+
* token model, presets and apply logic live in exactly one place.
|
|
118
|
+
*/
|
|
119
|
+
|
|
120
|
+
interface ThemeState extends ThemeTokens {
|
|
121
|
+
activePresetId: string | null;
|
|
122
|
+
setColor: (key: string, value: string) => void;
|
|
123
|
+
setFont: (which: keyof ThemeFonts, value: string) => void;
|
|
124
|
+
/** Apply a curated pairing, setting the display and body faces together. */
|
|
125
|
+
setFontPairing: (pairingId: string) => void;
|
|
126
|
+
setRadius: (radius: number) => void;
|
|
127
|
+
applyPreset: (presetId: string) => void;
|
|
128
|
+
reset: () => void;
|
|
129
|
+
/** Snapshot of the editable tokens (for export / live apply). */
|
|
130
|
+
snapshot: () => ThemeTokens;
|
|
131
|
+
}
|
|
132
|
+
interface ThemeStoreConfig {
|
|
133
|
+
/** The editable token catalog (keys + labels + groups). */
|
|
134
|
+
tokens: ColorToken[];
|
|
135
|
+
/** Group id → heading, for the grouped panel layout. */
|
|
136
|
+
groupLabels: Record<string, string>;
|
|
137
|
+
/** Shipped defaults (the firm's brand values). */
|
|
138
|
+
defaultTheme: ThemeTokens;
|
|
139
|
+
/** Starting points the user can apply then fine-tune. */
|
|
140
|
+
presets: ThemePreset[];
|
|
141
|
+
/** localStorage key for persistence (namespaced per firm). */
|
|
142
|
+
storageKey: string;
|
|
143
|
+
/**
|
|
144
|
+
* How edits are written onto the document. Defaults to `'flat'` (inline
|
|
145
|
+
* `--color-*`). `'cssVars'` emits raw HSL-triplet vars into an injected
|
|
146
|
+
* `<style>` so an app's own `hsl(var(--x))` indirection + `.dark` variant
|
|
147
|
+
* survive. See {@link applyThemeVars}.
|
|
148
|
+
*/
|
|
149
|
+
mode?: ThemeEmitMode;
|
|
150
|
+
/**
|
|
151
|
+
* `cssVars` mode only: the dark-variant colour values keyed by token key,
|
|
152
|
+
* emitted under `:root.dark`. Required (per editable token) for any firm with a
|
|
153
|
+
* dark mode, otherwise a light-token override leaks into dark.
|
|
154
|
+
*/
|
|
155
|
+
darkColors?: Record<string, string>;
|
|
156
|
+
}
|
|
157
|
+
interface ThemeBundle extends ThemeStoreConfig {
|
|
158
|
+
useStore: UseBoundStore<StoreApi<ThemeState>>;
|
|
159
|
+
}
|
|
160
|
+
/** Build a firm-branded theme store bundle. */
|
|
161
|
+
declare function createThemeStore(config: ThemeStoreConfig): ThemeBundle;
|
|
162
|
+
/** Apply a bundle's current store to the live document, honouring its emit mode. */
|
|
163
|
+
declare function applyStoredTheme(bundle: ThemeBundle): void;
|
|
164
|
+
|
|
165
|
+
interface ThemeEditorProps {
|
|
166
|
+
/** The firm's theme store bundle (from createThemeStore). */
|
|
167
|
+
bundle: ThemeBundle;
|
|
168
|
+
variant?: 'panel' | 'drawer';
|
|
169
|
+
/** Firm identity shown in the panel's live-preview header. */
|
|
170
|
+
preview?: {
|
|
171
|
+
title: string;
|
|
172
|
+
subtitle?: string;
|
|
173
|
+
};
|
|
174
|
+
/** Optional persistence side-channel (e.g. save theme to ARBI); returns a "via" label. */
|
|
175
|
+
onSave?: () => Promise<string | void>;
|
|
176
|
+
/** Stable testid namespace, e.g. "builder-theme" | "studio-theme". */
|
|
177
|
+
testIdPrefix?: string;
|
|
178
|
+
}
|
|
179
|
+
declare function ThemeEditor({ bundle, variant, preview, onSave, testIdPrefix, }: ThemeEditorProps): react.JSX.Element;
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Curated typography pairings — the choice a user actually makes.
|
|
183
|
+
*
|
|
184
|
+
* A pairing is a display face + a body face that are known to sit well together,
|
|
185
|
+
* so picking typography means picking a *look*, not authoring two CSS font
|
|
186
|
+
* stacks. The raw stacks remain editable for power users.
|
|
187
|
+
*
|
|
188
|
+
* DATA ONLY. This module names families; it never loads them, so the kit takes no
|
|
189
|
+
* font dependency. Web fonts are self-hosted per app (`@fontsource-variable/*`
|
|
190
|
+
* imported from the app's entry or stylesheet); `fontPackages()` reports exactly
|
|
191
|
+
* which packages a set of pairings needs. A pairing whose family an app has not
|
|
192
|
+
* imported does not error — it falls back down its own stack — so an app that
|
|
193
|
+
* offers a pairing must also ship its fonts.
|
|
194
|
+
*/
|
|
195
|
+
interface FontPairing {
|
|
196
|
+
id: string;
|
|
197
|
+
/** Shown in the picker, e.g. "Newsreader / Inter". */
|
|
198
|
+
label: string;
|
|
199
|
+
/** One line on the character of the pairing. */
|
|
200
|
+
description: string;
|
|
201
|
+
/** Font stack for headings (`--font-display`). */
|
|
202
|
+
display: string;
|
|
203
|
+
/** Font stack for body copy (`--font-sans`). */
|
|
204
|
+
sans: string;
|
|
205
|
+
/**
|
|
206
|
+
* The npm packages an app must install + import to actually render this
|
|
207
|
+
* pairing, e.g. `['@fontsource-variable/inter']`. Empty when the pairing uses
|
|
208
|
+
* only system faces.
|
|
209
|
+
*/
|
|
210
|
+
packages: string[];
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* The shipped catalog. Every entry uses variable fonts (one file per family)
|
|
214
|
+
* rather than per-weight static files.
|
|
215
|
+
*/
|
|
216
|
+
declare const FONT_PAIRINGS: FontPairing[];
|
|
217
|
+
/** Look a pairing up by id. Returns undefined for an unknown id. */
|
|
218
|
+
declare function getFontPairing(id: string): FontPairing | undefined;
|
|
219
|
+
/**
|
|
220
|
+
* The pairing whose stacks match `fonts`, or undefined when the stacks have been
|
|
221
|
+
* hand-edited away from every preset. Lets a picker show "Custom" honestly.
|
|
222
|
+
*/
|
|
223
|
+
declare function matchFontPairing(fonts: {
|
|
224
|
+
display: string;
|
|
225
|
+
sans: string;
|
|
226
|
+
}): FontPairing | undefined;
|
|
227
|
+
/** Every npm package needed to render the given pairings (deduped). */
|
|
228
|
+
declare function fontPackages(pairings?: FontPairing[]): string[];
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Compose a stable, kebab-case data-testid from parts.
|
|
232
|
+
* Convention: {area}-{element}[-{qualifier}...]
|
|
233
|
+
* tid("matters", "row", "CLM-2026-0042") -> "matters-row-clm-2026-0042"
|
|
234
|
+
* Keeps Playwright selectors predictable across a firm app.
|
|
235
|
+
*/
|
|
236
|
+
declare function tid(...parts: Array<string | number | undefined | false>): string;
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Firm-app formatting helpers — currency, initials, dates, durations and
|
|
240
|
+
* percentages. UK-oriented defaults (en-GB / GBP / "d MMM yyyy") suit an English
|
|
241
|
+
* law firm; nothing here is firm-branded, so every firm app shares them.
|
|
242
|
+
*
|
|
243
|
+
* Dependency-free on purpose (uses the platform `Intl` APIs) so the package
|
|
244
|
+
* stays light and needs no date library.
|
|
245
|
+
*/
|
|
246
|
+
/** Format a number as GBP, e.g. £1,240,000 or £12.4k depending on `compact`. */
|
|
247
|
+
declare function gbp(value: number, compact?: boolean): string;
|
|
248
|
+
/** Initials from a person's name, e.g. "Amara N. Okafor" -> "AO". */
|
|
249
|
+
declare function initials(name: string): string;
|
|
250
|
+
/** "12 Mar 2026" */
|
|
251
|
+
declare function fmtDate(iso: string): string;
|
|
252
|
+
/** "12 Mar 2026, 14:30" */
|
|
253
|
+
declare function fmtDateTime(iso: string): string;
|
|
254
|
+
/** "3 days ago" / "in 2 hours" — largest whole unit, like date-fns strict. */
|
|
255
|
+
declare function fromNow(iso: string, from?: Date): string;
|
|
256
|
+
/** Signed day count to a date from a reference day. Negative = overdue. */
|
|
257
|
+
declare function daysUntil(iso: string, from?: Date): number;
|
|
258
|
+
/** Compact hours, e.g. 12.5h */
|
|
259
|
+
declare function hrs(n: number): string;
|
|
260
|
+
/** Percentage with no decimals, e.g. 82% */
|
|
261
|
+
declare function pct(n: number): string;
|
|
262
|
+
/**
|
|
263
|
+
* Page references for a set of cited pages: "p 2", "pp 2, 5", "pp 2–4, 9".
|
|
264
|
+
*
|
|
265
|
+
* Pages, not passage counts — the page is what a reader turns to; how many
|
|
266
|
+
* passages sit inside it is detail they did not ask for. Consecutive pages
|
|
267
|
+
* collapse into a range, the same way a contiguous run of passages collapses
|
|
268
|
+
* into a single mark in the viewer.
|
|
269
|
+
*/
|
|
270
|
+
declare function formatPageRefs(pages: number[]): string;
|
|
271
|
+
|
|
272
|
+
/** Editorial heading block for marketing sections: eyebrow + title + lead. */
|
|
273
|
+
declare function SectionHeading({ eyebrow, title, lead, align, className, invert, eyebrowClassName, }: {
|
|
274
|
+
eyebrow?: string;
|
|
275
|
+
title: string;
|
|
276
|
+
lead?: string;
|
|
277
|
+
align?: 'left' | 'center';
|
|
278
|
+
className?: string;
|
|
279
|
+
invert?: boolean;
|
|
280
|
+
/** Override the eyebrow styling (e.g. a firm brand accent). Default: token classes. */
|
|
281
|
+
eyebrowClassName?: string;
|
|
282
|
+
}): react.JSX.Element;
|
|
283
|
+
|
|
284
|
+
type BlockImageAspect = '16:9' | '4:3' | '1:1' | '21:9' | '4:5';
|
|
285
|
+
type BlockImageRounded = 'none' | 'md' | 'lg' | 'full';
|
|
286
|
+
interface BlockImageProps {
|
|
287
|
+
src?: string;
|
|
288
|
+
alt?: string;
|
|
289
|
+
aspect?: BlockImageAspect;
|
|
290
|
+
rounded?: BlockImageRounded;
|
|
291
|
+
/** Editorial "photo frame" treatment: a matte border ring + soft shadow. */
|
|
292
|
+
framed?: boolean;
|
|
293
|
+
className?: string;
|
|
294
|
+
testId?: string;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Image with a graceful, self-contained fallback: when `src` is empty it renders a
|
|
298
|
+
* token-clean gradient placeholder (no network needed) so the palette always looks
|
|
299
|
+
* good in the editor and in demos. Token-only classes so it themes per firm.
|
|
300
|
+
*/
|
|
301
|
+
declare function BlockImage({ src, alt, aspect, rounded, framed, className, testId, }: BlockImageProps): react.JSX.Element;
|
|
302
|
+
|
|
303
|
+
type HeroTone = 'default' | 'muted' | 'ink';
|
|
304
|
+
type HeroAlign = 'left' | 'center';
|
|
305
|
+
interface HeroCta {
|
|
306
|
+
label?: string;
|
|
307
|
+
href?: string;
|
|
308
|
+
}
|
|
309
|
+
interface HeroProps {
|
|
310
|
+
eyebrow?: string;
|
|
311
|
+
title: string;
|
|
312
|
+
/** A word/phrase within the title to accent (rendered in the primary colour). */
|
|
313
|
+
emphasis?: string;
|
|
314
|
+
subtitle?: string;
|
|
315
|
+
primaryCta?: HeroCta;
|
|
316
|
+
secondaryCta?: HeroCta;
|
|
317
|
+
align?: HeroAlign;
|
|
318
|
+
tone?: HeroTone;
|
|
319
|
+
/** Side visual (ignored when `backgroundImage` is set). */
|
|
320
|
+
imageUrl?: string;
|
|
321
|
+
/** Full-bleed background image URL/data-URI. Turns the hero into a cover band. */
|
|
322
|
+
backgroundImage?: string;
|
|
323
|
+
/** Darkening scrim over the background image, for legible copy. Default true. */
|
|
324
|
+
overlay?: boolean;
|
|
325
|
+
/** Override the eyebrow styling (e.g. a firm brand accent). Default: token classes. */
|
|
326
|
+
eyebrowClassName?: string;
|
|
327
|
+
testId?: string;
|
|
328
|
+
}
|
|
329
|
+
/** Editorial marketing hero: eyebrow, headline, subtitle, two CTAs and a visual. */
|
|
330
|
+
declare function Hero({ eyebrow, title, emphasis, subtitle, primaryCta, secondaryCta, align, tone, imageUrl, backgroundImage, overlay, eyebrowClassName, testId, }: HeroProps): react.JSX.Element;
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Firm-agnostic link injection for marketing blocks. A card that navigates to a
|
|
334
|
+
* detail page renders an internal `<a href>` by default, but a consuming app can
|
|
335
|
+
* inject its router's link (e.g. react-router's `Link`, adapted to `href`) via
|
|
336
|
+
* `linkComponent` so navigation stays in-SPA. When no `href` is given the block
|
|
337
|
+
* renders its children unwrapped, so this is fully opt-in and backward compatible.
|
|
338
|
+
*/
|
|
339
|
+
type BlockLinkComponent = ComponentType<{
|
|
340
|
+
href: string;
|
|
341
|
+
className?: string;
|
|
342
|
+
children: ReactNode;
|
|
343
|
+
}>;
|
|
344
|
+
interface BlockLinkProps {
|
|
345
|
+
href?: string;
|
|
346
|
+
linkComponent?: BlockLinkComponent;
|
|
347
|
+
className?: string;
|
|
348
|
+
children: ReactNode;
|
|
349
|
+
}
|
|
350
|
+
declare function BlockLink({ href, linkComponent: Link, className, children }: BlockLinkProps): react.JSX.Element;
|
|
351
|
+
|
|
352
|
+
interface FeatureItem {
|
|
353
|
+
icon?: LucideIcon;
|
|
354
|
+
title: string;
|
|
355
|
+
description?: string;
|
|
356
|
+
/** When set, the card becomes a link to this destination. */
|
|
357
|
+
href?: string;
|
|
358
|
+
}
|
|
359
|
+
interface FeatureGridProps {
|
|
360
|
+
columns?: '2' | '3';
|
|
361
|
+
items: FeatureItem[];
|
|
362
|
+
/** Inject a router link (adapted to `href`) so linked cards navigate in-SPA. */
|
|
363
|
+
linkComponent?: BlockLinkComponent;
|
|
364
|
+
testId?: string;
|
|
365
|
+
}
|
|
366
|
+
/** Responsive grid of icon + title + description feature cards. */
|
|
367
|
+
declare function FeatureGrid({ columns, items, linkComponent, testId }: FeatureGridProps): react.JSX.Element;
|
|
368
|
+
|
|
369
|
+
interface TestimonialProps {
|
|
370
|
+
quote: string;
|
|
371
|
+
author?: string;
|
|
372
|
+
role?: string;
|
|
373
|
+
avatarUrl?: string;
|
|
374
|
+
testId?: string;
|
|
375
|
+
}
|
|
376
|
+
/** A single pull-quote testimonial with an author and optional avatar. */
|
|
377
|
+
declare function Testimonial({ quote, author, role, avatarUrl, testId }: TestimonialProps): react.JSX.Element;
|
|
378
|
+
|
|
379
|
+
type CTABannerTone = 'default' | 'muted' | 'ink';
|
|
380
|
+
interface CTABannerProps {
|
|
381
|
+
title: string;
|
|
382
|
+
subtitle?: string;
|
|
383
|
+
cta?: {
|
|
384
|
+
label?: string;
|
|
385
|
+
href?: string;
|
|
386
|
+
};
|
|
387
|
+
tone?: CTABannerTone;
|
|
388
|
+
/** Full-bleed background image URL/data-URI. Overrides the tone surface. */
|
|
389
|
+
backgroundImage?: string;
|
|
390
|
+
/** Darkening scrim over the background image. Default true. */
|
|
391
|
+
overlay?: boolean;
|
|
392
|
+
testId?: string;
|
|
393
|
+
}
|
|
394
|
+
/** Full-width call-to-action banner. */
|
|
395
|
+
declare function CTABanner({ title, subtitle, cta, tone, backgroundImage, overlay, testId, }: CTABannerProps): react.JSX.Element;
|
|
396
|
+
|
|
397
|
+
interface LogoItem {
|
|
398
|
+
label: string;
|
|
399
|
+
}
|
|
400
|
+
interface LogoCloudProps {
|
|
401
|
+
items: LogoItem[];
|
|
402
|
+
testId?: string;
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Text-based "logo" wall — brand names rendered as muted pill marks. Self-contained
|
|
406
|
+
* (no external images) so the palette looks complete offline.
|
|
407
|
+
*/
|
|
408
|
+
declare function LogoCloud({ items, testId }: LogoCloudProps): react.JSX.Element;
|
|
409
|
+
|
|
410
|
+
interface FaqItem {
|
|
411
|
+
question: string;
|
|
412
|
+
answer?: string;
|
|
413
|
+
}
|
|
414
|
+
interface FAQProps {
|
|
415
|
+
items: FaqItem[];
|
|
416
|
+
testId?: string;
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Accessible disclosure list. Built on native `<details>`/`<summary>` so it works
|
|
420
|
+
* without client state (correct in the Puck preview and static renders) and stays
|
|
421
|
+
* keyboard-accessible. Token-only styling.
|
|
422
|
+
*/
|
|
423
|
+
declare function FAQ({ items, testId }: FAQProps): react.JSX.Element;
|
|
424
|
+
|
|
425
|
+
interface RichTextBlockProps {
|
|
426
|
+
content: string;
|
|
427
|
+
testId?: string;
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Prose block. Reuses the shared markdown renderer (`AiMarkdown`) so long-form copy
|
|
431
|
+
* gets the one house style for headings, lists, tables and emphasis — token-clean
|
|
432
|
+
* and themeable per firm.
|
|
433
|
+
*/
|
|
434
|
+
declare function RichTextBlock({ content, testId }: RichTextBlockProps): react.JSX.Element;
|
|
435
|
+
|
|
436
|
+
type CalloutVariant = 'info' | 'success' | 'warning' | 'danger';
|
|
437
|
+
interface CalloutProps {
|
|
438
|
+
variant?: CalloutVariant;
|
|
439
|
+
title?: string;
|
|
440
|
+
body?: string;
|
|
441
|
+
testId?: string;
|
|
442
|
+
}
|
|
443
|
+
/** Inline notice box in one of four semantic tones. */
|
|
444
|
+
declare function Callout({ variant, title, body, testId }: CalloutProps): react.JSX.Element;
|
|
445
|
+
|
|
446
|
+
type AvatarBlockSize = 'sm' | 'md' | 'lg' | 'xl';
|
|
447
|
+
interface AvatarBlockProps {
|
|
448
|
+
src?: string;
|
|
449
|
+
name?: string;
|
|
450
|
+
size?: AvatarBlockSize;
|
|
451
|
+
testId?: string;
|
|
452
|
+
}
|
|
453
|
+
/** Avatar with an initials fallback when no image is supplied. */
|
|
454
|
+
declare function AvatarBlock({ src, name, size, testId }: AvatarBlockProps): react.JSX.Element;
|
|
455
|
+
|
|
456
|
+
interface NavLink {
|
|
457
|
+
label: string;
|
|
458
|
+
href?: string;
|
|
459
|
+
}
|
|
460
|
+
interface NavbarProps {
|
|
461
|
+
brand: string;
|
|
462
|
+
links: NavLink[];
|
|
463
|
+
cta?: {
|
|
464
|
+
label?: string;
|
|
465
|
+
href?: string;
|
|
466
|
+
};
|
|
467
|
+
testId?: string;
|
|
468
|
+
}
|
|
469
|
+
/** Marketing top navigation: brand, inline links and an optional CTA button. */
|
|
470
|
+
declare function Navbar({ brand, links, cta, testId }: NavbarProps): react.JSX.Element;
|
|
471
|
+
|
|
472
|
+
interface FooterLink {
|
|
473
|
+
label: string;
|
|
474
|
+
href?: string;
|
|
475
|
+
}
|
|
476
|
+
interface FooterColumn {
|
|
477
|
+
heading: string;
|
|
478
|
+
links: FooterLink[];
|
|
479
|
+
}
|
|
480
|
+
interface FooterProps {
|
|
481
|
+
columns: FooterColumn[];
|
|
482
|
+
copyright?: string;
|
|
483
|
+
testId?: string;
|
|
484
|
+
}
|
|
485
|
+
/** Site footer: a row of link columns plus a copyright line. */
|
|
486
|
+
declare function Footer({ columns, copyright, testId }: FooterProps): react.JSX.Element;
|
|
487
|
+
|
|
488
|
+
interface ListItem {
|
|
489
|
+
text: string;
|
|
490
|
+
}
|
|
491
|
+
interface ListBlockProps {
|
|
492
|
+
ordered?: boolean;
|
|
493
|
+
items: ListItem[];
|
|
494
|
+
testId?: string;
|
|
495
|
+
}
|
|
496
|
+
/** Ordered or unordered prose list. Token-clean. */
|
|
497
|
+
declare function ListBlock({ ordered, items, testId }: ListBlockProps): react.JSX.Element;
|
|
498
|
+
|
|
499
|
+
type ColumnsCount = 2 | 3 | 4;
|
|
500
|
+
type ColumnsGap = 'sm' | 'md' | 'lg';
|
|
501
|
+
interface ColumnsProps {
|
|
502
|
+
count?: ColumnsCount;
|
|
503
|
+
gap?: ColumnsGap;
|
|
504
|
+
columns: ReactNode[];
|
|
505
|
+
testId?: string;
|
|
506
|
+
}
|
|
507
|
+
/** Responsive N-column row. Each column hosts a nested Puck slot. */
|
|
508
|
+
declare function Columns({ count, gap, columns, testId }: ColumnsProps): react.JSX.Element;
|
|
509
|
+
|
|
510
|
+
type SpacerSize = 'sm' | 'md' | 'lg' | 'xl';
|
|
511
|
+
interface SpacerProps {
|
|
512
|
+
size?: SpacerSize;
|
|
513
|
+
testId?: string;
|
|
514
|
+
}
|
|
515
|
+
/** Vertical whitespace. */
|
|
516
|
+
declare function Spacer({ size, testId }: SpacerProps): react.JSX.Element;
|
|
517
|
+
|
|
518
|
+
type ContainerWidth = 'narrow' | 'default' | 'wide' | 'full';
|
|
519
|
+
type ContainerPadding = 'none' | 'sm' | 'md' | 'lg';
|
|
520
|
+
interface ContainerProps {
|
|
521
|
+
width?: ContainerWidth;
|
|
522
|
+
padding?: ContainerPadding;
|
|
523
|
+
children: ReactNode;
|
|
524
|
+
testId?: string;
|
|
525
|
+
}
|
|
526
|
+
/** Max-width wrapper that centres and pads a nested Puck slot. */
|
|
527
|
+
declare function Container({ width, padding, children, testId }: ContainerProps): react.JSX.Element;
|
|
528
|
+
|
|
529
|
+
interface PricingPlan {
|
|
530
|
+
name: string;
|
|
531
|
+
price: string;
|
|
532
|
+
period?: string;
|
|
533
|
+
description?: string;
|
|
534
|
+
features?: string[];
|
|
535
|
+
cta?: {
|
|
536
|
+
label?: string;
|
|
537
|
+
href?: string;
|
|
538
|
+
};
|
|
539
|
+
/** Highlight this plan as the recommended one. */
|
|
540
|
+
featured?: boolean;
|
|
541
|
+
}
|
|
542
|
+
interface PricingTableProps {
|
|
543
|
+
plans: PricingPlan[];
|
|
544
|
+
testId?: string;
|
|
545
|
+
}
|
|
546
|
+
/** A row of pricing plan cards with feature lists and a CTA per plan. */
|
|
547
|
+
declare function PricingTable({ plans, testId }: PricingTableProps): react.JSX.Element;
|
|
548
|
+
|
|
549
|
+
interface TabItem {
|
|
550
|
+
label: string;
|
|
551
|
+
content: string;
|
|
552
|
+
}
|
|
553
|
+
interface TabsProps {
|
|
554
|
+
items: TabItem[];
|
|
555
|
+
testId?: string;
|
|
556
|
+
}
|
|
557
|
+
/** A simple tabbed panel: a row of tab buttons over the active tab's content. */
|
|
558
|
+
declare function Tabs({ items, testId }: TabsProps): react.JSX.Element;
|
|
559
|
+
|
|
560
|
+
interface AccordionItem {
|
|
561
|
+
title: string;
|
|
562
|
+
content: string;
|
|
563
|
+
}
|
|
564
|
+
interface AccordionProps {
|
|
565
|
+
items: AccordionItem[];
|
|
566
|
+
/** Allow multiple panels open at once. Default false (single-open). */
|
|
567
|
+
allowMultiple?: boolean;
|
|
568
|
+
testId?: string;
|
|
569
|
+
}
|
|
570
|
+
/** A stack of collapsible title/content panels. */
|
|
571
|
+
declare function Accordion({ items, allowMultiple, testId }: AccordionProps): react.JSX.Element;
|
|
572
|
+
|
|
573
|
+
interface GalleryImage {
|
|
574
|
+
src?: string;
|
|
575
|
+
alt?: string;
|
|
576
|
+
caption?: string;
|
|
577
|
+
}
|
|
578
|
+
interface GalleryProps {
|
|
579
|
+
images: GalleryImage[];
|
|
580
|
+
columns?: '2' | '3' | '4';
|
|
581
|
+
aspect?: BlockImageProps['aspect'];
|
|
582
|
+
rounded?: BlockImageProps['rounded'];
|
|
583
|
+
testId?: string;
|
|
584
|
+
}
|
|
585
|
+
/** A responsive image grid. Each tile previews via BlockImage with a caption. */
|
|
586
|
+
declare function Gallery({ images, columns, aspect, rounded, testId, }: GalleryProps): react.JSX.Element;
|
|
587
|
+
|
|
588
|
+
type VideoAspect = '16:9' | '4:3' | '1:1';
|
|
589
|
+
interface VideoEmbedProps {
|
|
590
|
+
/** A YouTube / Vimeo / Loom page URL, or a direct video file URL. */
|
|
591
|
+
url: string;
|
|
592
|
+
title?: string;
|
|
593
|
+
aspect?: VideoAspect;
|
|
594
|
+
rounded?: boolean;
|
|
595
|
+
testId?: string;
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* Resolve a shareable video URL to an embeddable iframe src. Supports YouTube,
|
|
599
|
+
* Vimeo and Loom; returns null for anything else (rendered as a `<video>`).
|
|
600
|
+
*/
|
|
601
|
+
declare function toEmbedUrl(url: string): string | null;
|
|
602
|
+
/** Embed a video from a YouTube/Vimeo/Loom link or a direct file, in a set aspect. */
|
|
603
|
+
declare function VideoEmbed({ url, title, aspect, rounded, testId, }: VideoEmbedProps): react.JSX.Element;
|
|
604
|
+
|
|
605
|
+
interface ContactField {
|
|
606
|
+
name: string;
|
|
607
|
+
label: string;
|
|
608
|
+
type?: 'text' | 'email' | 'tel' | 'textarea';
|
|
609
|
+
required?: boolean;
|
|
610
|
+
}
|
|
611
|
+
interface ContactFormProps {
|
|
612
|
+
fields: ContactField[];
|
|
613
|
+
submitLabel?: string;
|
|
614
|
+
/** Optional POST endpoint. When set, the form submits natively to it. */
|
|
615
|
+
action?: string;
|
|
616
|
+
/** Message shown after a client-side (no-action) submit. */
|
|
617
|
+
successMessage?: string;
|
|
618
|
+
testId?: string;
|
|
619
|
+
}
|
|
620
|
+
/** A configurable lead/contact form. Posts to `action`, or confirms client-side. */
|
|
621
|
+
declare function ContactForm({ fields, submitLabel, action, successMessage, testId, }: ContactFormProps): react.JSX.Element;
|
|
622
|
+
|
|
623
|
+
type BannerTone = 'primary' | 'muted' | 'ink';
|
|
624
|
+
interface BannerProps {
|
|
625
|
+
text: string;
|
|
626
|
+
cta?: {
|
|
627
|
+
label?: string;
|
|
628
|
+
href?: string;
|
|
629
|
+
};
|
|
630
|
+
tone?: BannerTone;
|
|
631
|
+
testId?: string;
|
|
632
|
+
}
|
|
633
|
+
/** A slim full-width announcement strip with optional inline CTA. */
|
|
634
|
+
declare function Banner({ text, cta, tone, testId }: BannerProps): react.JSX.Element;
|
|
635
|
+
|
|
636
|
+
interface MediaTextProps {
|
|
637
|
+
eyebrow?: string;
|
|
638
|
+
title: string;
|
|
639
|
+
body?: string;
|
|
640
|
+
imageUrl?: string;
|
|
641
|
+
/** Which side the media sits on (desktop). */
|
|
642
|
+
mediaSide?: 'left' | 'right';
|
|
643
|
+
cta?: {
|
|
644
|
+
label?: string;
|
|
645
|
+
href?: string;
|
|
646
|
+
};
|
|
647
|
+
testId?: string;
|
|
648
|
+
}
|
|
649
|
+
/** A split media-and-copy row: an image on one side, prose + CTA on the other. */
|
|
650
|
+
declare function MediaText({ eyebrow, title, body, imageUrl, mediaSide, cta, testId, }: MediaTextProps): react.JSX.Element;
|
|
651
|
+
|
|
652
|
+
interface TeamMember {
|
|
653
|
+
name: string;
|
|
654
|
+
role?: string;
|
|
655
|
+
bio?: string;
|
|
656
|
+
avatarUrl?: string;
|
|
657
|
+
/** When set, the card becomes a link to this person's detail page. */
|
|
658
|
+
href?: string;
|
|
659
|
+
}
|
|
660
|
+
interface TeamGridProps {
|
|
661
|
+
members: TeamMember[];
|
|
662
|
+
columns?: '2' | '3' | '4';
|
|
663
|
+
/** Inject a router link (adapted to `href`) so linked cards navigate in-SPA. */
|
|
664
|
+
linkComponent?: BlockLinkComponent;
|
|
665
|
+
testId?: string;
|
|
666
|
+
}
|
|
667
|
+
/** A grid of people cards: avatar, name, role and an optional short bio. */
|
|
668
|
+
declare function TeamGrid({ members, columns, linkComponent, testId }: TeamGridProps): react.JSX.Element;
|
|
669
|
+
|
|
670
|
+
interface StepItem {
|
|
671
|
+
title: string;
|
|
672
|
+
description?: string;
|
|
673
|
+
}
|
|
674
|
+
interface StepsProps {
|
|
675
|
+
steps: StepItem[];
|
|
676
|
+
/** `timeline` draws a connected vertical rail; `numbered` is a horizontal grid. */
|
|
677
|
+
variant?: 'numbered' | 'timeline';
|
|
678
|
+
testId?: string;
|
|
679
|
+
}
|
|
680
|
+
/** A process / how-it-works sequence, as numbered cards or a vertical timeline. */
|
|
681
|
+
declare function Steps({ steps, variant, testId }: StepsProps): react.JSX.Element;
|
|
682
|
+
|
|
683
|
+
interface TestimonialItem {
|
|
684
|
+
quote: string;
|
|
685
|
+
author?: string;
|
|
686
|
+
role?: string;
|
|
687
|
+
avatarUrl?: string;
|
|
688
|
+
}
|
|
689
|
+
interface TestimonialGridProps {
|
|
690
|
+
items: TestimonialItem[];
|
|
691
|
+
columns?: '2' | '3';
|
|
692
|
+
testId?: string;
|
|
693
|
+
}
|
|
694
|
+
/** A grid of multiple pull-quote testimonials. */
|
|
695
|
+
declare function TestimonialGrid({ items, columns, testId }: TestimonialGridProps): react.JSX.Element;
|
|
696
|
+
|
|
697
|
+
export { type NavLink as $, Accordion as A, Banner as B, CTABanner as C, type FaqItem as D, FeatureGrid as E, FAQ as F, type FeatureGridProps as G, type FeatureItem as H, type FontPairing as I, Footer as J, type FooterColumn as K, type FooterLink as L, type FooterProps as M, Gallery as N, type GalleryImage as O, type GalleryProps as P, Hero as Q, type HeroCta as R, type HeroProps as S, ListBlock as T, type ListBlockProps as U, type ListItem as V, LogoCloud as W, type LogoCloudProps as X, type LogoItem as Y, MediaText as Z, type MediaTextProps as _, type AccordionItem as a, Navbar as a0, type NavbarProps as a1, type PricingPlan as a2, PricingTable as a3, type PricingTableProps as a4, RichTextBlock as a5, type RichTextBlockProps as a6, SectionHeading as a7, Spacer as a8, type SpacerProps as a9, type VideoEmbedProps as aA, applyFontVars as aB, applyStoredTheme as aC, applyThemeVars as aD, clearFontVars as aE, clearThemeVars as aF, createThemeStore as aG, daysUntil as aH, fmtDate as aI, fmtDateTime as aJ, fontPackages as aK, formatPageRefs as aL, fromNow as aM, gbp as aN, getFontPairing as aO, hrs as aP, initials as aQ, matchFontPairing as aR, pct as aS, tid as aT, toEmbedUrl as aU, toHslTriplet as aV, type StepItem as aa, Steps as ab, type StepsProps as ac, THEME_STYLE_ID as ad, type TabItem as ae, Tabs as af, type TabsProps as ag, TeamGrid as ah, type TeamGridProps as ai, type TeamMember as aj, Testimonial as ak, TestimonialGrid as al, type TestimonialGridProps as am, type TestimonialItem as an, type TestimonialProps as ao, type ThemeBundle as ap, ThemeEditor as aq, type ThemeEditorProps as ar, type ThemeEmitMode as as, type ThemeFonts as at, type ThemePreset as au, type ThemeState as av, type ThemeStoreConfig as aw, type ThemeTokens as ax, type VideoAspect as ay, VideoEmbed as az, type AccordionProps as b, type ApplyThemeOptions as c, AvatarBlock as d, type AvatarBlockProps as e, type BannerProps as f, type BannerTone as g, BlockImage as h, type BlockImageProps as i, BlockLink as j, type BlockLinkComponent as k, type BlockLinkProps as l, type CTABannerProps as m, Callout as n, type CalloutProps as o, type CalloutVariant as p, type ColorToken as q, Columns as r, type ColumnsProps as s, type ContactField as t, ContactForm as u, type ContactFormProps as v, Container as w, type ContainerProps as x, type FAQProps as y, FONT_PAIRINGS as z };
|