@marktiderman/genesis-ui 2.14.0 → 2.16.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.
@@ -1,17 +1,280 @@
1
- export { A as AppShell, a as AppShellOptions, b as AppShellPageHeader, c as AppShellProps, d as AppShellSubNavEnd, N as NavItem, P as ParentNavigation, S as SubNavItem, e as SubNavMode, f as SubNavTabs, g as SubNavTabsProps, U as UseAppShellSubNavOptions, u as useAppShellSubNav } from '../../AppShell-HCXpi6nv.js';
1
+ export { A as AppShell, a as AppShellOptions, b as AppShellPageHeader, c as AppShellProps, d as AppShellSubNavEnd, N as NavItem, P as ParentNavigation, S as SubNavItem, e as SubNavMode, f as SubNavTabs, g as SubNavTabsProps, U as UseAppShellSubNavOptions, u as useAppShellSubNav } from '../../AppShell-C88Uj7kT.js';
2
2
  export { G as GenesisLinkProps, L as LinkComponent } from '../../navigation-vmFJ6r1m.js';
3
3
  export { P as PageHeader, a as PageHeaderOptions, b as PageHeaderProps } from '../../PageHeader-V9lVkZtF.js';
4
4
  export { DetailPage, DetailPageProps } from './detail-page.js';
5
5
  export { FormPage, FormPageProps } from './form-page.js';
6
6
  export { D as DashboardPage, a as DashboardPageProps, G as Grid, b as GridProps, g as gridVariants } from '../../dashboard-page-C0TihRl5.js';
7
7
  export { SettingsPage, SettingsPageProps, SettingsPageSection } from './settings-page.js';
8
- export { C as Container, a as ContainerProps, G as GAP_CLASSES, S as Section, b as SectionProps, c as SpaceToken, d as Stack, e as StackProps, f as containerVariants, s as sectionHeadingVariants, g as spaceScale, h as stackVariants } from '../../spacing-BS6qECD4.js';
8
+ export { BuilderLayout, BuilderLayoutProps, BuilderLayoutSlotProps } from './builder-layout.js';
9
+ import { a as ContainerProps } from '../../spacing-BS6qECD4.js';
10
+ export { C as Container, G as GAP_CLASSES, S as Section, b as SectionProps, c as SpaceToken, d as Stack, e as StackProps, f as containerVariants, s as sectionHeadingVariants, g as spaceScale, h as stackVariants } from '../../spacing-BS6qECD4.js';
9
11
  export { S as Split, a as SplitBreakpoint, b as SplitProps, c as SplitRatio, s as splitVariants } from '../../split-Dk9f4Mi0.js';
10
12
  export { Band, BandLead, BandLeadProps, BandProps, Display, DisplayProps, bandVariants, displayVariants, leadVariants } from './band.js';
13
+ import * as React from 'react';
14
+ import { Slots, SlotPropsFor } from '@marktiderman/genesis-core/slots';
11
15
  export { NavigationNode, ResolvedNavigation, resolveNavigation } from '@marktiderman/genesis-core/navigation';
12
- import 'react';
13
16
  import 'lucide-react';
14
17
  import '../../view-toggle-CahQ87PW.js';
15
18
  import '../../hooks/use-view-settings.js';
16
19
  import 'class-variance-authority/types';
17
20
  import 'class-variance-authority';
21
+
22
+ /**
23
+ * The brand block above the card. Same fields as native `AuthBrandingProps`
24
+ * (`logo`, `headline`, `subhead`), except that `headline` is required here:
25
+ * it is the page's `<h1>`.
26
+ */
27
+ interface AuthPageBrandingProps {
28
+ /** Brand mark (logo node). */
29
+ logo?: React.ReactNode;
30
+ /** "Sign in", "Reset your password", "You're invited". Renders as the page's h1. */
31
+ headline: React.ReactNode;
32
+ /** One line under the headline. */
33
+ subhead?: React.ReactNode;
34
+ testID?: string;
35
+ }
36
+ /** The parts of an {@link AuthPage} that `slots` and `slotProps` reach. Names match native `AuthScreenSlotMap`. */
37
+ type AuthPageSlotMap = {
38
+ branding: AuthPageBrandingProps & {
39
+ testID: string;
40
+ };
41
+ social: {
42
+ children: React.ReactNode;
43
+ testID: string;
44
+ };
45
+ footer: {
46
+ children: React.ReactNode;
47
+ testID: string;
48
+ };
49
+ };
50
+ interface AuthPageProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "title"> {
51
+ /** Logo, headline (h1) and subhead above the card. */
52
+ branding: AuthPageBrandingProps;
53
+ /** Sign-in provider buttons (Google, Apple…), above the form inside the card. */
54
+ social?: React.ReactNode;
55
+ /** The form: sign-in, sign-up, reset, invite acceptance, verify. The card's body. */
56
+ children?: React.ReactNode;
57
+ /** Below the card: "Forgot password?", "Create account". */
58
+ footer?: React.ReactNode;
59
+ /** The small print under everything: terms and privacy. */
60
+ legal?: React.ReactNode;
61
+ /**
62
+ * A panel beside the card on wide screens (an illustration, a quote, a
63
+ * product shot). Hidden below the `lg` breakpoint, where the card is the
64
+ * whole page.
65
+ */
66
+ aside?: React.ReactNode;
67
+ /** Which side `aside` sits on. Default `"end"`. */
68
+ asidePosition?: "start" | "end";
69
+ /** Replace (a component) or drop (`null`) one part. */
70
+ slots?: Slots<AuthPageSlotMap>;
71
+ /** More props for a default part, merged over what the page passes. */
72
+ slotProps?: SlotPropsFor<AuthPageSlotMap>;
73
+ /** testID on the root. Default `"auth-page"`; parts use `${testID}-branding`, `-card`, `-social`, `-footer`, `-legal`, `-aside`. */
74
+ testID?: string;
75
+ }
76
+ /**
77
+ * AuthPage — the web counterpart of native `AuthScreen`: a whole-screen,
78
+ * centred card for sign-in, sign-up, password reset, invite acceptance and
79
+ * email verification.
80
+ *
81
+ * ```tsx
82
+ * <AuthPage
83
+ * branding={{ logo: <Logo />, headline: "Sign in", subhead: "Welcome back." }}
84
+ * footer={<><a href="/reset">Forgot password?</a> · <a href="/sign-up">Create account</a></>}
85
+ * legal={<>By continuing you agree to the <a href="/terms">terms</a>.</>}
86
+ * aside={<img src={hero} alt="" />}
87
+ * >
88
+ * <SignInForm />
89
+ * </AuthPage>
90
+ * ```
91
+ *
92
+ * The card column is a `<main>` landmark and the headline is its single
93
+ * `<h1>`, so a screen reader lands on the form's purpose. The page fills the
94
+ * viewport (`min-height: 100dvh`) and the card shrinks to the screen with a
95
+ * 16px gutter, so nothing scrolls sideways at 375px.
96
+ *
97
+ * Native `AuthScreen` takes `form` and `providers` as data and renders its
98
+ * own `AuthForm`; this one is a **layout**, so the form and the provider
99
+ * buttons are nodes the app passes. Slots are the same contract:
100
+ * replace or drop (`null`) `branding`, `social` or `footer`.
101
+ *
102
+ * @stability Beta
103
+ */
104
+ declare const AuthPage: React.ForwardRefExoticComponent<AuthPageProps & React.RefAttributes<HTMLDivElement>>;
105
+
106
+ /**
107
+ * Brand theming for public pages: a host's brand object becomes CSS
108
+ * variables on a page root, so every Genesis component inside it (a
109
+ * `Button`, a focus ring, a link styled `text-primary`) picks up the accent
110
+ * without knowing a brand exists.
111
+ *
112
+ * The accent is the one colour a public page takes from outside the token
113
+ * set, and it is the one most likely to be unreadable: brand palettes are
114
+ * picked for logos, not for 14px text. Emerald `#10b981` with white text is
115
+ * 2.5:1, half of WCAG AA's 4.5:1. So the accent is never used as-is for
116
+ * text or for a button's fill. {@link accessibleAccent} keeps its hue and
117
+ * saturation and lowers its lightness until white text on it (and it as
118
+ * text on white) clears 4.5:1; the raw accent stays available for
119
+ * decoration (`--brand-accent`: a header rule, a border).
120
+ *
121
+ * Variables set (all HSL triplets, the same shape as Genesis tokens, so
122
+ * `hsl(var(--x))` works everywhere):
123
+ *
124
+ * - `--brand-accent`: the accent exactly as given. Decoration only.
125
+ * - `--brand-accent-strong`: the accessible shade. Text and fills.
126
+ * - `--brand-accent-foreground`: text on `--brand-accent-strong`.
127
+ * - `--primary`, `--primary-foreground`, `--ring`: the Genesis tokens,
128
+ * pointed at the accessible shade, which is what makes nested components
129
+ * follow the brand.
130
+ */
131
+ /** What a host knows about the brand a public page belongs to. */
132
+ interface PublicBrand {
133
+ /** Display name, shown in the header beside the logo (so the logo itself is decorative, `alt=""`). */
134
+ name: string;
135
+ /** Logo image URL. */
136
+ logoUrl?: string;
137
+ /**
138
+ * Accent colour: `#rgb`, `#rrggbb`, `rgb()`/`rgba()`, `hsl()`/`hsla()` or a
139
+ * bare token triplet like `"160 84% 39%"`. Alpha is ignored (the accent is
140
+ * treated as opaque). Invalid or absent: Genesis's own primary.
141
+ */
142
+ accent?: string;
143
+ /** Optional wide cover image above the header row. Decorative. */
144
+ headerImageUrl?: string;
145
+ }
146
+ /** An sRGB colour, 0-255 per channel. */
147
+ interface Rgb {
148
+ r: number;
149
+ g: number;
150
+ b: number;
151
+ }
152
+ /** WCAG AA for normal-size text. */
153
+ declare const MIN_TEXT_CONTRAST = 4.5;
154
+ /**
155
+ * Parses `#rgb`, `#rrggbb`, `rgb()`/`rgba()`, `hsl()`/`hsla()` (commas or
156
+ * spaces, optional `/` or `,` alpha, which is ignored) and bare HSL triplets
157
+ * like `"160 84% 39%"`. Anything else, including out-of-range channels: `null`.
158
+ * Never throws.
159
+ */
160
+ declare function parseColor(input: string | undefined | null): Rgb | null;
161
+ /** WCAG 2 relative luminance, 0 (black) to 1 (white). */
162
+ declare function relativeLuminance({ r, g, b }: Rgb): number;
163
+ /** WCAG 2 contrast ratio between two colours, 1 to 21. */
164
+ declare function contrastRatio(a: Rgb, b: Rgb): number;
165
+ /** The Genesis token shape: `"160 84% 39%"`, for `hsl(var(--x))`. */
166
+ declare function toHslTriplet(color: Rgb): string;
167
+ interface AccessibleAccent {
168
+ /** The accent as given. */
169
+ accent: Rgb;
170
+ /** The shade to use for text on white and for fills under white text. Equals `accent` when it already passes. */
171
+ strong: Rgb;
172
+ /**
173
+ * `strong` as the exact HSL triplet to emit (`"H S% L%"`, 0.1 precision).
174
+ * The contrast check runs on this value, not on the unrounded colour, so
175
+ * what the browser renders is what was checked.
176
+ */
177
+ strongTriplet: string;
178
+ /** Contrast of the given accent against the background. */
179
+ ratio: number;
180
+ /** Contrast of `strongTriplet`, as rendered, against the background (at least `minRatio`). */
181
+ strongRatio: number;
182
+ /** True when `strong` had to be darkened from `accent`. */
183
+ adjusted: boolean;
184
+ }
185
+ /**
186
+ * The contrast fallback. Returns the accent unchanged when its emitted
187
+ * triplet reaches `minRatio` against `background` (white by default);
188
+ * otherwise the same (rounded) hue and saturation at the highest lightness,
189
+ * to 0.1%, whose emitted triplet does. Every check runs on the value
190
+ * that is actually emitted, after rounding. It always ends: lightness 0 is
191
+ * black, 21:1 on white.
192
+ */
193
+ declare function accessibleAccent(accent: Rgb | string, { background, minRatio }?: {
194
+ background?: Rgb;
195
+ minRatio?: number;
196
+ }): AccessibleAccent | null;
197
+ /**
198
+ * The CSS variables for a brand's accent, for a page root's `style`. Empty
199
+ * when the brand has no valid accent, so the page keeps Genesis's primary.
200
+ */
201
+ declare function brandThemeStyle(brand: Pick<PublicBrand, "accent"> | undefined): React.CSSProperties;
202
+
203
+ /** The parts of a {@link PublicPage} that `slots` and `slotProps` reach. */
204
+ type PublicPageSlotMap = {
205
+ header: {
206
+ brand: PublicBrand;
207
+ testID: string;
208
+ };
209
+ footer: {
210
+ children: React.ReactNode;
211
+ poweredBy?: React.ReactNode;
212
+ testID: string;
213
+ };
214
+ };
215
+ interface PublicPageProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "title"> {
216
+ /**
217
+ * The brand the page belongs to. Its header (logo, name, cover image) shows
218
+ * at the top, and its `accent` themes the page: see `brandThemeStyle`.
219
+ */
220
+ brand?: PublicBrand;
221
+ /** Optional page heading, rendered as the page's `<h1>` above the content. Leave it out when the content brings its own (a form's title). */
222
+ title?: React.ReactNode;
223
+ /** Supporting copy under `title`. */
224
+ description?: React.ReactNode;
225
+ /** Width of the content column: a `Container` size. Default `"sm"`. */
226
+ maxWidth?: NonNullable<ContainerProps["size"]>;
227
+ /**
228
+ * Who runs the page, as "Powered by …" in the footer (a name or a link).
229
+ * Leave it out, or pass `false`, to hide it.
230
+ */
231
+ poweredBy?: React.ReactNode;
232
+ /** More footer content: privacy and terms links, an address. */
233
+ footer?: React.ReactNode;
234
+ /**
235
+ * For an iframe: no header or footer chrome, a transparent background and
236
+ * no full-viewport height, so the frame is as tall as its content. The
237
+ * brand accent still applies. A host usually sets it from `?embed=1`.
238
+ */
239
+ embedded?: boolean;
240
+ /** Replace (a component) or drop (`null`) the header or footer. */
241
+ slots?: Slots<PublicPageSlotMap>;
242
+ /** More props for a default part, merged over what the page passes. */
243
+ slotProps?: SlotPropsFor<PublicPageSlotMap>;
244
+ /** testID on the root. Default `"public-page"`; parts use `${testID}-header`, `-main`, `-footer`. */
245
+ testID?: string;
246
+ }
247
+ /**
248
+ * PublicPage — a page anyone can open without signing in: a public form, a
249
+ * QR poster, a landing page. A brand header, one content column, and a
250
+ * "Powered by …" footer, themed from a brand object.
251
+ *
252
+ * ```tsx
253
+ * <PublicPage
254
+ * brand={{ name: "Riverside Physio", logoUrl, accent: "#10b981" }}
255
+ * poweredBy={<a href="https://example.com">Example</a>}
256
+ * embedded={searchParams.get("embed") === "1"}
257
+ * >
258
+ * <Form … />
259
+ * </PublicPage>
260
+ * ```
261
+ *
262
+ * The brand's accent becomes `--primary` (darkened when it fails 4.5:1 on
263
+ * white) on the page root, so a `Button` or link inside follows the brand
264
+ * with no props. The check is against white, Genesis's light background;
265
+ * under a `.dark` theme the fill stays readable (white on the shade) but
266
+ * accent-coloured text may not, so public pages are best served light.
267
+ *
268
+ * `embedded` is the frame-ancestors case. It drops the header and footer,
269
+ * the background and `min-height: 100dvh` (inside an iframe the viewport is
270
+ * the frame, so a full-height page would never let the frame shrink to its
271
+ * content).
272
+ *
273
+ * This is a **layout**: it positions content and binds to nothing. The
274
+ * content column is a `Container` for the one-home reason.
275
+ *
276
+ * @stability Beta
277
+ */
278
+ declare const PublicPage: React.ForwardRefExoticComponent<PublicPageProps & React.RefAttributes<HTMLDivElement>>;
279
+
280
+ export { type AccessibleAccent, AuthPage, type AuthPageBrandingProps, type AuthPageProps, type AuthPageSlotMap, ContainerProps, MIN_TEXT_CONTRAST, type PublicBrand, PublicPage, type PublicPageProps, type PublicPageSlotMap, type Rgb, accessibleAccent, brandThemeStyle, contrastRatio, parseColor, relativeLuminance, toHslTriplet };
@@ -1,4 +1,4 @@
1
- export { AppShell, AppShellPageHeader, AppShellSubNavEnd, PageHeader, SubNavTabs, useAppShellSubNav } from '../../chunk-5OFA2CQR.js';
1
+ export { AppShell, AppShellPageHeader, AppShellSubNavEnd, PageHeader, SubNavTabs, useAppShellSubNav } from '../../chunk-C2IPSMNE.js';
2
2
  import '../../chunk-46TZGMFW.js';
3
3
  export { Stack, stackVariants } from '../../chunk-ORCI5SVM.js';
4
4
  import '../../chunk-B2GIBUQD.js';
@@ -6,10 +6,13 @@ import '../../chunk-NPNADZHM.js';
6
6
  import '../../chunk-G5YKBPBF.js';
7
7
  import '../../chunk-FK42ZGGV.js';
8
8
  import '../../chunk-22XB3Q6H.js';
9
+ import '../../chunk-NWSCYLPT.js';
9
10
  import '../../chunk-QJ6P2DW6.js';
10
11
  import '../../chunk-27ELYTGE.js';
11
12
  import '../../chunk-F47U5KNW.js';
13
+ import { Container } from '../../chunk-ZHCP2RZP.js';
12
14
  export { Band, BandLead, Container, Display, bandVariants, containerVariants, displayVariants, leadVariants } from '../../chunk-ZHCP2RZP.js';
15
+ export { BuilderLayout } from '../../chunk-TP6QNFDG.js';
13
16
  export { DashboardPage, Grid, gridVariants } from '../../chunk-ARJI2JUV.js';
14
17
  export { DetailPage } from '../../chunk-AFZ76Q26.js';
15
18
  export { FormPage } from '../../chunk-MPI33I4K.js';
@@ -17,9 +20,366 @@ import '../../chunk-OZZTPE7F.js';
17
20
  export { Section, SettingsPage, sectionHeadingVariants } from '../../chunk-P64MFQXZ.js';
18
21
  export { Split, splitVariants } from '../../chunk-D2LIXA2Y.js';
19
22
  export { GAP_CLASSES, spaceScale } from '../../chunk-AVFZ4P4B.js';
20
- import '../../chunk-NWSCYLPT.js';
21
- import '../../chunk-NZ5WBA4R.js';
23
+ import { cn } from '../../chunk-NZ5WBA4R.js';
22
24
  export { resolveNavigation } from '../../chunk-J45FHL7D.js';
23
25
  import '../../chunk-424PT5DM.js';
26
+ import * as React from 'react';
27
+ import { renderSlot } from '@marktiderman/genesis-core/slots';
28
+ import { jsxs, jsx } from 'react/jsx-runtime';
29
+
30
+ var AuthPage = React.forwardRef(
31
+ ({
32
+ branding,
33
+ social,
34
+ children,
35
+ footer,
36
+ legal,
37
+ aside,
38
+ asidePosition = "end",
39
+ slots = {},
40
+ slotProps = {},
41
+ testID = "auth-page",
42
+ className,
43
+ ...props
44
+ }, ref) => /* @__PURE__ */ jsxs(
45
+ "div",
46
+ {
47
+ ref,
48
+ "data-slot": "auth-page",
49
+ "data-testid": testID,
50
+ className: cn(
51
+ "flex min-h-dvh w-full min-w-0 bg-background text-foreground",
52
+ aside ? "lg:grid lg:grid-cols-2" : null,
53
+ className
54
+ ),
55
+ ...props,
56
+ children: [
57
+ /* @__PURE__ */ jsx(
58
+ "main",
59
+ {
60
+ "data-slot": "auth-page-main",
61
+ className: "flex min-w-0 flex-1 flex-col items-center justify-center px-4 py-10 sm:px-6",
62
+ children: /* @__PURE__ */ jsxs("div", { className: "flex w-full max-w-sm min-w-0 flex-col gap-6", children: [
63
+ renderSlot(
64
+ DefaultBranding,
65
+ slots.branding,
66
+ { ...branding, testID: branding.testID ?? `${testID}-branding` },
67
+ slotProps.branding
68
+ ),
69
+ /* @__PURE__ */ jsxs(
70
+ "div",
71
+ {
72
+ "data-slot": "auth-page-card",
73
+ "data-testid": `${testID}-card`,
74
+ className: "flex min-w-0 flex-col gap-6 rounded-xl border border-border bg-card p-6 text-card-foreground shadow-sm",
75
+ children: [
76
+ social ? renderSlot(DefaultSocial, slots.social, { children: social, testID: `${testID}-social` }, slotProps.social) : null,
77
+ children
78
+ ]
79
+ }
80
+ ),
81
+ footer ? renderSlot(DefaultFooter, slots.footer, { children: footer, testID: `${testID}-footer` }, slotProps.footer) : null,
82
+ legal ? /* @__PURE__ */ jsx(
83
+ "p",
84
+ {
85
+ "data-slot": "auth-page-legal",
86
+ "data-testid": `${testID}-legal`,
87
+ className: "text-center text-xs text-muted-foreground",
88
+ children: legal
89
+ }
90
+ ) : null
91
+ ] })
92
+ }
93
+ ),
94
+ aside ? /* @__PURE__ */ jsx(
95
+ "aside",
96
+ {
97
+ "data-slot": "auth-page-aside",
98
+ "data-testid": `${testID}-aside`,
99
+ className: cn(
100
+ "relative hidden min-w-0 overflow-hidden bg-muted lg:flex lg:items-center lg:justify-center",
101
+ asidePosition === "start" ? "lg:order-first" : null
102
+ ),
103
+ children: aside
104
+ }
105
+ ) : null
106
+ ]
107
+ }
108
+ )
109
+ );
110
+ AuthPage.displayName = "AuthPage";
111
+ function DefaultBranding({ logo, headline, subhead, testID }) {
112
+ return /* @__PURE__ */ jsxs("header", { "data-slot": "auth-page-branding", "data-testid": testID, className: "flex flex-col items-center gap-3 text-center", children: [
113
+ logo ? /* @__PURE__ */ jsx("div", { "data-testid": `${testID}-logo`, className: "mb-1", children: logo }) : null,
114
+ /* @__PURE__ */ jsx("h1", { "data-testid": `${testID}-headline`, className: "text-2xl font-bold tracking-tight break-words", children: headline }),
115
+ subhead ? /* @__PURE__ */ jsx("p", { "data-testid": `${testID}-subhead`, className: "text-sm text-muted-foreground", children: subhead }) : null
116
+ ] });
117
+ }
118
+ function DefaultSocial({ children, testID }) {
119
+ return /* @__PURE__ */ jsx("div", { "data-slot": "auth-page-social", "data-testid": testID, className: "flex flex-col gap-2", children });
120
+ }
121
+ function DefaultFooter({ children, testID }) {
122
+ return /* @__PURE__ */ jsx(
123
+ "div",
124
+ {
125
+ "data-slot": "auth-page-footer",
126
+ "data-testid": testID,
127
+ className: "flex flex-wrap items-center justify-center gap-x-4 gap-y-2 text-center text-sm text-muted-foreground",
128
+ children
129
+ }
130
+ );
131
+ }
132
+
133
+ // src/components/layout/brand-theme.ts
134
+ var MIN_TEXT_CONTRAST = 4.5;
135
+ var WHITE = { r: 255, g: 255, b: 255 };
136
+ var NUM = String.raw`[+-]?(?:\d+\.?\d*|\.\d+)`;
137
+ var HUE = `(${NUM})(?:deg)?`;
138
+ var PCT = `(${NUM})%?`;
139
+ var ALPHA = String.raw`(?:\s*[,/]\s*${NUM}%?)?`;
140
+ var SEP = String.raw`(?:\s*,\s*|\s+)`;
141
+ var HSL_FN = new RegExp(String.raw`^hsla?\(\s*${HUE}${SEP}${PCT}${SEP}${PCT}${ALPHA}\s*\)$`);
142
+ var HSL_BARE = new RegExp(String.raw`^${HUE}\s+(${NUM})%\s+(${NUM})%$`);
143
+ function parseColor(input) {
144
+ if (typeof input !== "string") return null;
145
+ const value = input.trim().toLowerCase();
146
+ if (!value) return null;
147
+ const hex = /^#([0-9a-f]{3}|[0-9a-f]{6})$/.exec(value);
148
+ if (hex) {
149
+ const digits = hex[1];
150
+ const full = digits.length === 3 ? digits.replace(/./g, (c) => c + c) : digits;
151
+ return {
152
+ r: parseInt(full.slice(0, 2), 16),
153
+ g: parseInt(full.slice(2, 4), 16),
154
+ b: parseInt(full.slice(4, 6), 16)
155
+ };
156
+ }
157
+ const fn = /^rgba?\(\s*(\d{1,3})[\s,]+(\d{1,3})[\s,]+(\d{1,3})\s*(?:[,/]\s*[\d.]+%?\s*)?\)$/.exec(value);
158
+ if (fn) {
159
+ const [r, g, b] = [fn[1], fn[2], fn[3]].map(Number);
160
+ if ([r, g, b].every((c) => c <= 255)) return { r, g, b };
161
+ return null;
162
+ }
163
+ const hsl = HSL_FN.exec(value) ?? HSL_BARE.exec(value);
164
+ if (hsl) {
165
+ const [h, sat, l] = [hsl[1], hsl[2], hsl[3]].map(Number);
166
+ if (![h, sat, l].every(Number.isFinite) || sat < 0 || sat > 100 || l < 0 || l > 100) return null;
167
+ return hslToRgb({ h, s: sat, l });
168
+ }
169
+ return null;
170
+ }
171
+ function relativeLuminance({ r, g, b }) {
172
+ const lin = (c) => {
173
+ const s = c / 255;
174
+ return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
175
+ };
176
+ return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);
177
+ }
178
+ function contrastRatio(a, b) {
179
+ const [hi, lo] = [relativeLuminance(a), relativeLuminance(b)].sort((x, y) => y - x);
180
+ return (hi + 0.05) / (lo + 0.05);
181
+ }
182
+ function rgbToHsl({ r, g, b }) {
183
+ const [rn, gn, bn] = [r / 255, g / 255, b / 255];
184
+ const max = Math.max(rn, gn, bn);
185
+ const min = Math.min(rn, gn, bn);
186
+ const l = (max + min) / 2;
187
+ if (max === min) return { h: 0, s: 0, l: l * 100 };
188
+ const d = max - min;
189
+ const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
190
+ let h;
191
+ if (max === rn) h = (gn - bn) / d + (gn < bn ? 6 : 0);
192
+ else if (max === gn) h = (bn - rn) / d + 2;
193
+ else h = (rn - gn) / d + 4;
194
+ return { h: h * 60, s: s * 100, l: l * 100 };
195
+ }
196
+ function hslToRgb(hsl) {
197
+ const { r, g, b } = hslToRgbExact(hsl);
198
+ return { r: Math.round(r), g: Math.round(g), b: Math.round(b) };
199
+ }
200
+ function toHslTriplet(color) {
201
+ const { h, s, l } = rgbToHsl(color);
202
+ return `${round(h)} ${round(s)}% ${round(l)}%`;
203
+ }
204
+ var round = (n) => Math.round(n * 10) / 10;
205
+ function hslToRgbExact({ h, s, l }) {
206
+ const hn = (h % 360 + 360) % 360;
207
+ const sn = s / 100;
208
+ const ln = l / 100;
209
+ const k = (n) => (n + hn / 30) % 12;
210
+ const a = sn * Math.min(ln, 1 - ln);
211
+ const f = (n) => ln - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)));
212
+ return { r: f(0) * 255, g: f(8) * 255, b: f(4) * 255 };
213
+ }
214
+ function emittedRatio(hsl, background) {
215
+ return Math.min(contrastRatio(hslToRgbExact(hsl), background), contrastRatio(hslToRgb(hsl), background));
216
+ }
217
+ var tripletOf = ({ h, s, l }) => `${h} ${s}% ${l}%`;
218
+ function accessibleAccent(accent, { background = WHITE, minRatio = MIN_TEXT_CONTRAST } = {}) {
219
+ const base = typeof accent === "string" ? parseColor(accent) : accent;
220
+ if (!base) return null;
221
+ const ratio = contrastRatio(base, background);
222
+ const exact = rgbToHsl(base);
223
+ const h = round(exact.h);
224
+ const s = round(exact.s);
225
+ const l = round(exact.l);
226
+ const emitted = emittedRatio({ h, s, l }, background);
227
+ if (emitted >= minRatio) {
228
+ return { accent: base, strong: base, strongTriplet: tripletOf({ h, s, l }), ratio, strongRatio: emitted, adjusted: false };
229
+ }
230
+ const start = Math.round(l * 10);
231
+ const pick = (tenths) => {
232
+ const candidate = { h, s, l: tenths / 10 };
233
+ const strongRatio = emittedRatio(candidate, background);
234
+ return strongRatio >= minRatio ? { candidate, strongRatio } : null;
235
+ };
236
+ for (let coarse = start - 10; coarse > -10; coarse -= 10) {
237
+ const floor = Math.max(coarse, 0);
238
+ const passing = pick(floor);
239
+ if (!passing) continue;
240
+ let best = passing;
241
+ for (let fine = Math.min(floor + 9, start - 1); fine > floor; fine -= 1) {
242
+ const found = pick(fine);
243
+ if (found) {
244
+ best = found;
245
+ break;
246
+ }
247
+ }
248
+ const { candidate, strongRatio } = best;
249
+ return { accent: base, strong: hslToRgb(candidate), strongTriplet: tripletOf(candidate), ratio, strongRatio, adjusted: true };
250
+ }
251
+ const black = { h: 0, s: 0, l: 0 };
252
+ return {
253
+ accent: base,
254
+ strong: { r: 0, g: 0, b: 0 },
255
+ strongTriplet: tripletOf(black),
256
+ ratio,
257
+ strongRatio: emittedRatio(black, background),
258
+ adjusted: true
259
+ };
260
+ }
261
+ function brandThemeStyle(brand) {
262
+ const result = brand?.accent ? accessibleAccent(brand.accent) : null;
263
+ if (!result) return {};
264
+ const strong = result.strongTriplet;
265
+ const foreground = toHslTriplet(WHITE);
266
+ return {
267
+ "--brand-accent": toHslTriplet(result.accent),
268
+ "--brand-accent-strong": strong,
269
+ "--brand-accent-foreground": foreground,
270
+ "--primary": strong,
271
+ "--primary-foreground": foreground,
272
+ "--ring": strong
273
+ };
274
+ }
275
+ var PublicPage = React.forwardRef(
276
+ ({
277
+ brand,
278
+ title,
279
+ description,
280
+ maxWidth = "sm",
281
+ poweredBy,
282
+ footer,
283
+ embedded = false,
284
+ slots = {},
285
+ slotProps = {},
286
+ testID = "public-page",
287
+ className,
288
+ style,
289
+ children,
290
+ ...props
291
+ }, ref) => {
292
+ const showPoweredBy = poweredBy !== void 0 && poweredBy !== null && poweredBy !== false;
293
+ const hasFooter = showPoweredBy || footer !== void 0 && footer !== null && footer !== false;
294
+ return /* @__PURE__ */ jsxs(
295
+ "div",
296
+ {
297
+ ref,
298
+ "data-slot": "public-page",
299
+ "data-embedded": embedded ? "true" : void 0,
300
+ "data-testid": testID,
301
+ className: cn(
302
+ "flex w-full min-w-0 flex-col text-foreground",
303
+ embedded ? "bg-transparent" : "min-h-dvh bg-background",
304
+ className
305
+ ),
306
+ style: { ...brandThemeStyle(brand), ...style },
307
+ ...props,
308
+ children: [
309
+ !embedded && brand ? renderSlot(DefaultHeader, slots.header, { brand, testID: `${testID}-header` }, slotProps.header) : null,
310
+ /* @__PURE__ */ jsx(
311
+ "main",
312
+ {
313
+ "data-slot": "public-page-main",
314
+ "data-testid": `${testID}-main`,
315
+ className: cn("flex min-w-0 flex-1 flex-col", embedded ? "py-4" : "py-8 sm:py-12"),
316
+ children: /* @__PURE__ */ jsxs(Container, { size: maxWidth, className: "flex min-w-0 flex-col gap-6", children: [
317
+ title ? /* @__PURE__ */ jsxs("div", { "data-slot": "public-page-heading", className: "flex min-w-0 flex-col gap-1", children: [
318
+ /* @__PURE__ */ jsx("h1", { className: "text-2xl font-bold tracking-tight break-words sm:text-3xl", children: title }),
319
+ description ? /* @__PURE__ */ jsx("p", { className: "text-muted-foreground", children: description }) : null
320
+ ] }) : null,
321
+ children
322
+ ] })
323
+ }
324
+ ),
325
+ !embedded && hasFooter ? renderSlot(
326
+ DefaultFooter2,
327
+ slots.footer,
328
+ { children: footer, poweredBy: showPoweredBy ? poweredBy : void 0, testID: `${testID}-footer` },
329
+ slotProps.footer
330
+ ) : null
331
+ ]
332
+ }
333
+ );
334
+ }
335
+ );
336
+ PublicPage.displayName = "PublicPage";
337
+ function DefaultHeader({ brand, testID }) {
338
+ return /* @__PURE__ */ jsxs(
339
+ "header",
340
+ {
341
+ "data-slot": "public-page-header",
342
+ "data-testid": testID,
343
+ className: "border-b border-border border-t-4 border-t-[hsl(var(--brand-accent,var(--primary)))] bg-background",
344
+ children: [
345
+ brand.headerImageUrl ? /* @__PURE__ */ jsx(
346
+ "img",
347
+ {
348
+ src: brand.headerImageUrl,
349
+ alt: "",
350
+ "data-slot": "public-page-cover",
351
+ className: "block h-28 w-full object-cover sm:h-44"
352
+ }
353
+ ) : null,
354
+ /* @__PURE__ */ jsxs(Container, { size: "lg", className: "flex min-h-14 items-center gap-3 py-3", children: [
355
+ brand.logoUrl ? (
356
+ // The name is beside it, so the image is decorative: an alt of the
357
+ // name would read it twice.
358
+ /* @__PURE__ */ jsx("img", { src: brand.logoUrl, alt: "", "data-slot": "public-page-logo", className: "h-8 w-auto max-w-32 shrink-0 object-contain" })
359
+ ) : null,
360
+ /* @__PURE__ */ jsx("span", { "data-slot": "public-page-brand-name", className: "min-w-0 truncate font-semibold", children: brand.name })
361
+ ] })
362
+ ]
363
+ }
364
+ );
365
+ }
366
+ function DefaultFooter2({ children, poweredBy, testID }) {
367
+ return /* @__PURE__ */ jsx("footer", { "data-slot": "public-page-footer", "data-testid": testID, className: "border-t border-border", children: /* @__PURE__ */ jsxs(
368
+ Container,
369
+ {
370
+ size: "lg",
371
+ className: "flex flex-col items-center gap-2 py-6 text-center text-sm text-muted-foreground sm:flex-row sm:justify-between sm:text-left",
372
+ children: [
373
+ children ? /* @__PURE__ */ jsx("div", { className: "min-w-0", children }) : null,
374
+ poweredBy ? /* @__PURE__ */ jsxs("p", { "data-slot": "public-page-powered-by", className: "sm:ml-auto", children: [
375
+ "Powered by ",
376
+ poweredBy
377
+ ] }) : null
378
+ ]
379
+ }
380
+ ) });
381
+ }
382
+
383
+ export { AuthPage, MIN_TEXT_CONTRAST, PublicPage, accessibleAccent, brandThemeStyle, contrastRatio, parseColor, relativeLuminance, toHslTriplet };
24
384
  //# sourceMappingURL=index.js.map
25
385
  //# sourceMappingURL=index.js.map