@marktiderman/genesis-ui 2.15.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,4 +1,4 @@
1
- export { AppShell, AppShellPageHeader, AppShellSubNavEnd, PageHeader, SubNavTabs, useAppShellSubNav } from '../../chunk-DTQHAZWN.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';
@@ -133,9 +133,17 @@ function DefaultFooter({ children, testID }) {
133
133
  // src/components/layout/brand-theme.ts
134
134
  var MIN_TEXT_CONTRAST = 4.5;
135
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})%$`);
136
143
  function parseColor(input) {
137
- if (!input) return null;
144
+ if (typeof input !== "string") return null;
138
145
  const value = input.trim().toLowerCase();
146
+ if (!value) return null;
139
147
  const hex = /^#([0-9a-f]{3}|[0-9a-f]{6})$/.exec(value);
140
148
  if (hex) {
141
149
  const digits = hex[1];
@@ -150,6 +158,13 @@ function parseColor(input) {
150
158
  if (fn) {
151
159
  const [r, g, b] = [fn[1], fn[2], fn[3]].map(Number);
152
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 });
153
168
  }
154
169
  return null;
155
170
  }
@@ -178,42 +193,75 @@ function rgbToHsl({ r, g, b }) {
178
193
  else h = (rn - gn) / d + 4;
179
194
  return { h: h * 60, s: s * 100, l: l * 100 };
180
195
  }
181
- function hslToRgb({ h, s, l }) {
182
- const sn = s / 100;
183
- const ln = l / 100;
184
- const k = (n) => (n + h / 30) % 12;
185
- const a = sn * Math.min(ln, 1 - ln);
186
- const f = (n) => ln - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)));
187
- return { r: Math.round(f(0) * 255), g: Math.round(f(8) * 255), b: Math.round(f(4) * 255) };
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) };
188
199
  }
189
200
  function toHslTriplet(color) {
190
201
  const { h, s, l } = rgbToHsl(color);
191
202
  return `${round(h)} ${round(s)}% ${round(l)}%`;
192
203
  }
193
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}%`;
194
218
  function accessibleAccent(accent, { background = WHITE, minRatio = MIN_TEXT_CONTRAST } = {}) {
195
219
  const base = typeof accent === "string" ? parseColor(accent) : accent;
196
220
  if (!base) return null;
197
221
  const ratio = contrastRatio(base, background);
198
- if (ratio >= minRatio) return { accent: base, strong: base, ratio, strongRatio: ratio, adjusted: false };
199
- const { h, s, l } = rgbToHsl(base);
200
- let strong = base;
201
- let strongRatio = ratio;
202
- for (let next = l - 0.5; next >= 0; next -= 0.5) {
203
- strong = hslToRgb({ h, s, l: next });
204
- strongRatio = contrastRatio(strong, background);
205
- if (strongRatio >= minRatio) break;
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 };
206
229
  }
207
- if (strongRatio < minRatio) {
208
- strong = { r: 0, g: 0, b: 0 };
209
- strongRatio = contrastRatio(strong, background);
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 };
210
250
  }
211
- return { accent: base, strong, ratio, strongRatio, adjusted: true };
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
+ };
212
260
  }
213
261
  function brandThemeStyle(brand) {
214
262
  const result = brand?.accent ? accessibleAccent(brand.accent) : null;
215
263
  if (!result) return {};
216
- const strong = toHslTriplet(result.strong);
264
+ const strong = result.strongTriplet;
217
265
  const foreground = toHslTriplet(WHITE);
218
266
  return {
219
267
  "--brand-accent": toHslTriplet(result.accent),
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/components/layout/auth-page.tsx","../../../src/components/layout/brand-theme.ts","../../../src/components/layout/public-page.tsx"],"names":["React2","jsxs","renderSlot","jsx","DefaultFooter"],"mappings":"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","file":"index.js","sourcesContent":["import * as React from \"react\";\nimport { renderSlot, type SlotPropsFor, type Slots } from \"@marktiderman/genesis-core/slots\";\nimport { cn } from \"../../utils\";\n\n/**\n * The brand block above the card. Same fields as native `AuthBrandingProps`\n * (`logo`, `headline`, `subhead`), except that `headline` is required here:\n * it is the page's `<h1>`.\n */\nexport interface AuthPageBrandingProps {\n /** Brand mark (logo node). */\n logo?: React.ReactNode;\n /** \"Sign in\", \"Reset your password\", \"You're invited\". Renders as the page's h1. */\n headline: React.ReactNode;\n /** One line under the headline. */\n subhead?: React.ReactNode;\n testID?: string;\n}\n\n/** The parts of an {@link AuthPage} that `slots` and `slotProps` reach. Names match native `AuthScreenSlotMap`. */\nexport type AuthPageSlotMap = {\n branding: AuthPageBrandingProps & { testID: string };\n social: { children: React.ReactNode; testID: string };\n footer: { children: React.ReactNode; testID: string };\n};\n\nexport interface AuthPageProps extends Omit<React.HTMLAttributes<HTMLDivElement>, \"title\"> {\n /** Logo, headline (h1) and subhead above the card. */\n branding: AuthPageBrandingProps;\n /** Sign-in provider buttons (Google, Apple…), above the form inside the card. */\n social?: React.ReactNode;\n /** The form: sign-in, sign-up, reset, invite acceptance, verify. The card's body. */\n children?: React.ReactNode;\n /** Below the card: \"Forgot password?\", \"Create account\". */\n footer?: React.ReactNode;\n /** The small print under everything: terms and privacy. */\n legal?: React.ReactNode;\n /**\n * A panel beside the card on wide screens (an illustration, a quote, a\n * product shot). Hidden below the `lg` breakpoint, where the card is the\n * whole page.\n */\n aside?: React.ReactNode;\n /** Which side `aside` sits on. Default `\"end\"`. */\n asidePosition?: \"start\" | \"end\";\n /** Replace (a component) or drop (`null`) one part. */\n slots?: Slots<AuthPageSlotMap>;\n /** More props for a default part, merged over what the page passes. */\n slotProps?: SlotPropsFor<AuthPageSlotMap>;\n /** testID on the root. Default `\"auth-page\"`; parts use `${testID}-branding`, `-card`, `-social`, `-footer`, `-legal`, `-aside`. */\n testID?: string;\n}\n\n/**\n * AuthPage — the web counterpart of native `AuthScreen`: a whole-screen,\n * centred card for sign-in, sign-up, password reset, invite acceptance and\n * email verification.\n *\n * ```tsx\n * <AuthPage\n * branding={{ logo: <Logo />, headline: \"Sign in\", subhead: \"Welcome back.\" }}\n * footer={<><a href=\"/reset\">Forgot password?</a> · <a href=\"/sign-up\">Create account</a></>}\n * legal={<>By continuing you agree to the <a href=\"/terms\">terms</a>.</>}\n * aside={<img src={hero} alt=\"\" />}\n * >\n * <SignInForm />\n * </AuthPage>\n * ```\n *\n * The card column is a `<main>` landmark and the headline is its single\n * `<h1>`, so a screen reader lands on the form's purpose. The page fills the\n * viewport (`min-height: 100dvh`) and the card shrinks to the screen with a\n * 16px gutter, so nothing scrolls sideways at 375px.\n *\n * Native `AuthScreen` takes `form` and `providers` as data and renders its\n * own `AuthForm`; this one is a **layout**, so the form and the provider\n * buttons are nodes the app passes. Slots are the same contract:\n * replace or drop (`null`) `branding`, `social` or `footer`.\n *\n * @stability Beta\n */\nexport const AuthPage = React.forwardRef<HTMLDivElement, AuthPageProps>(\n (\n {\n branding,\n social,\n children,\n footer,\n legal,\n aside,\n asidePosition = \"end\",\n slots = {},\n slotProps = {},\n testID = \"auth-page\",\n className,\n ...props\n },\n ref,\n ) => (\n <div\n ref={ref}\n data-slot=\"auth-page\"\n data-testid={testID}\n className={cn(\n \"flex min-h-dvh w-full min-w-0 bg-background text-foreground\",\n aside ? \"lg:grid lg:grid-cols-2\" : null,\n className,\n )}\n {...props}\n >\n <main\n data-slot=\"auth-page-main\"\n className=\"flex min-w-0 flex-1 flex-col items-center justify-center px-4 py-10 sm:px-6\"\n >\n <div className=\"flex w-full max-w-sm min-w-0 flex-col gap-6\">\n {renderSlot(\n DefaultBranding,\n slots.branding,\n { ...branding, testID: branding.testID ?? `${testID}-branding` },\n slotProps.branding,\n )}\n <div\n data-slot=\"auth-page-card\"\n data-testid={`${testID}-card`}\n className=\"flex min-w-0 flex-col gap-6 rounded-xl border border-border bg-card p-6 text-card-foreground shadow-sm\"\n >\n {social ? renderSlot(DefaultSocial, slots.social, { children: social, testID: `${testID}-social` }, slotProps.social) : null}\n {children}\n </div>\n {footer\n ? renderSlot(DefaultFooter, slots.footer, { children: footer, testID: `${testID}-footer` }, slotProps.footer)\n : null}\n {legal ? (\n <p\n data-slot=\"auth-page-legal\"\n data-testid={`${testID}-legal`}\n className=\"text-center text-xs text-muted-foreground\"\n >\n {legal}\n </p>\n ) : null}\n </div>\n </main>\n {aside ? (\n <aside\n data-slot=\"auth-page-aside\"\n data-testid={`${testID}-aside`}\n className={cn(\n \"relative hidden min-w-0 overflow-hidden bg-muted lg:flex lg:items-center lg:justify-center\",\n asidePosition === \"start\" ? \"lg:order-first\" : null,\n )}\n >\n {aside}\n </aside>\n ) : null}\n </div>\n ),\n);\nAuthPage.displayName = \"AuthPage\";\n\nfunction DefaultBranding({ logo, headline, subhead, testID }: AuthPageSlotMap[\"branding\"]) {\n return (\n <header data-slot=\"auth-page-branding\" data-testid={testID} className=\"flex flex-col items-center gap-3 text-center\">\n {logo ? (\n <div data-testid={`${testID}-logo`} className=\"mb-1\">\n {logo}\n </div>\n ) : null}\n <h1 data-testid={`${testID}-headline`} className=\"text-2xl font-bold tracking-tight break-words\">\n {headline}\n </h1>\n {subhead ? (\n <p data-testid={`${testID}-subhead`} className=\"text-sm text-muted-foreground\">\n {subhead}\n </p>\n ) : null}\n </header>\n );\n}\n\nfunction DefaultSocial({ children, testID }: AuthPageSlotMap[\"social\"]) {\n return (\n <div data-slot=\"auth-page-social\" data-testid={testID} className=\"flex flex-col gap-2\">\n {children}\n </div>\n );\n}\n\nfunction DefaultFooter({ children, testID }: AuthPageSlotMap[\"footer\"]) {\n return (\n <div\n data-slot=\"auth-page-footer\"\n data-testid={testID}\n className=\"flex flex-wrap items-center justify-center gap-x-4 gap-y-2 text-center text-sm text-muted-foreground\"\n >\n {children}\n </div>\n );\n}\n","import type * as React from \"react\";\n\n/**\n * Brand theming for public pages: a host's brand object becomes CSS\n * variables on a page root, so every Genesis component inside it (a\n * `Button`, a focus ring, a link styled `text-primary`) picks up the accent\n * without knowing a brand exists.\n *\n * The accent is the one colour a public page takes from outside the token\n * set, and it is the one most likely to be unreadable: brand palettes are\n * picked for logos, not for 14px text. Emerald `#10b981` with white text is\n * 2.5:1, half of WCAG AA's 4.5:1. So the accent is never used as-is for\n * text or for a button's fill. {@link accessibleAccent} keeps its hue and\n * saturation and lowers its lightness until white text on it (and it as\n * text on white) clears 4.5:1; the raw accent stays available for\n * decoration (`--brand-accent`: a header rule, a border).\n *\n * Variables set (all HSL triplets, the same shape as Genesis tokens, so\n * `hsl(var(--x))` works everywhere):\n *\n * - `--brand-accent`: the accent exactly as given. Decoration only.\n * - `--brand-accent-strong`: the accessible shade. Text and fills.\n * - `--brand-accent-foreground`: text on `--brand-accent-strong`.\n * - `--primary`, `--primary-foreground`, `--ring`: the Genesis tokens,\n * pointed at the accessible shade, which is what makes nested components\n * follow the brand.\n */\n\n/** What a host knows about the brand a public page belongs to. */\nexport interface PublicBrand {\n /** Display name, shown in the header and used as the logo's alt text. */\n name: string;\n /** Logo image URL. */\n logoUrl?: string;\n /** Accent colour: `#rgb`, `#rrggbb` or `rgb(r, g, b)`. Invalid or absent: Genesis's own primary. */\n accent?: string;\n /** Optional wide cover image above the header row. Decorative. */\n headerImageUrl?: string;\n}\n\n/** An sRGB colour, 0-255 per channel. */\nexport interface Rgb {\n r: number;\n g: number;\n b: number;\n}\n\n/** WCAG AA for normal-size text. */\nexport const MIN_TEXT_CONTRAST = 4.5;\n\nconst WHITE: Rgb = { r: 255, g: 255, b: 255 };\n\n/** Parses `#rgb`, `#rrggbb` or `rgb(r, g, b)` / `rgb(r g b)`. Anything else: `null`. */\nexport function parseColor(input: string | undefined | null): Rgb | null {\n if (!input) return null;\n const value = input.trim().toLowerCase();\n const hex = /^#([0-9a-f]{3}|[0-9a-f]{6})$/.exec(value);\n if (hex) {\n const digits = hex[1]!;\n const full = digits.length === 3 ? digits.replace(/./g, (c) => c + c) : digits;\n return {\n r: parseInt(full.slice(0, 2), 16),\n g: parseInt(full.slice(2, 4), 16),\n b: parseInt(full.slice(4, 6), 16),\n };\n }\n const fn = /^rgba?\\(\\s*(\\d{1,3})[\\s,]+(\\d{1,3})[\\s,]+(\\d{1,3})\\s*(?:[,/]\\s*[\\d.]+%?\\s*)?\\)$/.exec(value);\n if (fn) {\n const [r, g, b] = [fn[1], fn[2], fn[3]].map(Number) as [number, number, number];\n if ([r, g, b].every((c) => c <= 255)) return { r, g, b };\n }\n return null;\n}\n\n/** WCAG 2 relative luminance, 0 (black) to 1 (white). */\nexport function relativeLuminance({ r, g, b }: Rgb): number {\n const lin = (c: number) => {\n const s = c / 255;\n return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;\n };\n return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);\n}\n\n/** WCAG 2 contrast ratio between two colours, 1 to 21. */\nexport function contrastRatio(a: Rgb, b: Rgb): number {\n const [hi, lo] = [relativeLuminance(a), relativeLuminance(b)].sort((x, y) => y - x) as [number, number];\n return (hi + 0.05) / (lo + 0.05);\n}\n\n/** sRGB to HSL: hue 0-360, saturation and lightness 0-100. */\nexport function rgbToHsl({ r, g, b }: Rgb): { h: number; s: number; l: number } {\n const [rn, gn, bn] = [r / 255, g / 255, b / 255];\n const max = Math.max(rn, gn, bn);\n const min = Math.min(rn, gn, bn);\n const l = (max + min) / 2;\n if (max === min) return { h: 0, s: 0, l: l * 100 };\n const d = max - min;\n const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);\n let h: number;\n if (max === rn) h = (gn - bn) / d + (gn < bn ? 6 : 0);\n else if (max === gn) h = (bn - rn) / d + 2;\n else h = (rn - gn) / d + 4;\n return { h: h * 60, s: s * 100, l: l * 100 };\n}\n\n/** HSL (hue 0-360, saturation and lightness 0-100) to sRGB. */\nexport function hslToRgb({ h, s, l }: { h: number; s: number; l: number }): Rgb {\n const sn = s / 100;\n const ln = l / 100;\n const k = (n: number) => (n + h / 30) % 12;\n const a = sn * Math.min(ln, 1 - ln);\n const f = (n: number) => ln - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)));\n return { r: Math.round(f(0) * 255), g: Math.round(f(8) * 255), b: Math.round(f(4) * 255) };\n}\n\n/** The Genesis token shape: `\"160 84% 39%\"`, for `hsl(var(--x))`. */\nexport function toHslTriplet(color: Rgb): string {\n const { h, s, l } = rgbToHsl(color);\n return `${round(h)} ${round(s)}% ${round(l)}%`;\n}\n\nconst round = (n: number) => Math.round(n * 10) / 10;\n\nexport interface AccessibleAccent {\n /** The accent as given. */\n accent: Rgb;\n /** The shade to use for text on white and for fills under white text. Equals `accent` when it already passes. */\n strong: Rgb;\n /** Contrast of the given accent against the background. */\n ratio: number;\n /** Contrast of `strong` against the background (at least `minRatio`). */\n strongRatio: number;\n /** True when `strong` had to be darkened from `accent`. */\n adjusted: boolean;\n}\n\n/**\n * The contrast fallback. Returns the accent unchanged when it reaches\n * `minRatio` against `background` (white by default); otherwise the same hue\n * and saturation at the highest lightness that does. The search walks\n * lightness down in 0.5% steps, so the shade stays as close to the brand as\n * the ratio allows, and it always ends: lightness 0 is black, 21:1.\n */\nexport function accessibleAccent(\n accent: Rgb | string,\n { background = WHITE, minRatio = MIN_TEXT_CONTRAST }: { background?: Rgb; minRatio?: number } = {},\n): AccessibleAccent | null {\n const base = typeof accent === \"string\" ? parseColor(accent) : accent;\n if (!base) return null;\n const ratio = contrastRatio(base, background);\n if (ratio >= minRatio) return { accent: base, strong: base, ratio, strongRatio: ratio, adjusted: false };\n const { h, s, l } = rgbToHsl(base);\n let strong = base;\n let strongRatio = ratio;\n for (let next = l - 0.5; next >= 0; next -= 0.5) {\n strong = hslToRgb({ h, s, l: next });\n strongRatio = contrastRatio(strong, background);\n if (strongRatio >= minRatio) break;\n }\n if (strongRatio < minRatio) {\n strong = { r: 0, g: 0, b: 0 };\n strongRatio = contrastRatio(strong, background);\n }\n return { accent: base, strong, ratio, strongRatio, adjusted: true };\n}\n\n/**\n * The CSS variables for a brand's accent, for a page root's `style`. Empty\n * when the brand has no valid accent, so the page keeps Genesis's primary.\n */\nexport function brandThemeStyle(brand: Pick<PublicBrand, \"accent\"> | undefined): React.CSSProperties {\n const result = brand?.accent ? accessibleAccent(brand.accent) : null;\n if (!result) return {};\n const strong = toHslTriplet(result.strong);\n const foreground = toHslTriplet(WHITE);\n return {\n \"--brand-accent\": toHslTriplet(result.accent),\n \"--brand-accent-strong\": strong,\n \"--brand-accent-foreground\": foreground,\n \"--primary\": strong,\n \"--primary-foreground\": foreground,\n \"--ring\": strong,\n } as React.CSSProperties;\n}\n","import * as React from \"react\";\nimport { renderSlot, type SlotPropsFor, type Slots } from \"@marktiderman/genesis-core/slots\";\nimport { cn } from \"../../utils\";\nimport { Container, type ContainerProps } from \"./container\";\nimport { brandThemeStyle, type PublicBrand } from \"./brand-theme\";\n\n/** The parts of a {@link PublicPage} that `slots` and `slotProps` reach. */\nexport type PublicPageSlotMap = {\n header: { brand: PublicBrand; testID: string };\n footer: { children: React.ReactNode; poweredBy?: React.ReactNode; testID: string };\n};\n\nexport interface PublicPageProps extends Omit<React.HTMLAttributes<HTMLDivElement>, \"title\"> {\n /**\n * The brand the page belongs to. Its header (logo, name, cover image) shows\n * at the top, and its `accent` themes the page: see `brandThemeStyle`.\n */\n brand?: PublicBrand;\n /** 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). */\n title?: React.ReactNode;\n /** Supporting copy under `title`. */\n description?: React.ReactNode;\n /** Width of the content column: a `Container` size. Default `\"sm\"`. */\n maxWidth?: NonNullable<ContainerProps[\"size\"]>;\n /**\n * Who runs the page, as \"Powered by …\" in the footer (a name or a link).\n * Leave it out, or pass `false`, to hide it.\n */\n poweredBy?: React.ReactNode;\n /** More footer content: privacy and terms links, an address. */\n footer?: React.ReactNode;\n /**\n * For an iframe: no header or footer chrome, a transparent background and\n * no full-viewport height, so the frame is as tall as its content. The\n * brand accent still applies. A host usually sets it from `?embed=1`.\n */\n embedded?: boolean;\n /** Replace (a component) or drop (`null`) the header or footer. */\n slots?: Slots<PublicPageSlotMap>;\n /** More props for a default part, merged over what the page passes. */\n slotProps?: SlotPropsFor<PublicPageSlotMap>;\n /** testID on the root. Default `\"public-page\"`; parts use `${testID}-header`, `-main`, `-footer`. */\n testID?: string;\n}\n\n/**\n * PublicPage — a page anyone can open without signing in: a public form, a\n * QR poster, a landing page. A brand header, one content column, and a\n * \"Powered by …\" footer, themed from a brand object.\n *\n * ```tsx\n * <PublicPage\n * brand={{ name: \"Riverside Physio\", logoUrl, accent: \"#10b981\" }}\n * poweredBy={<a href=\"https://example.com\">Example</a>}\n * embedded={searchParams.get(\"embed\") === \"1\"}\n * >\n * <Form … />\n * </PublicPage>\n * ```\n *\n * The brand's accent becomes `--primary` (darkened when it fails 4.5:1 on\n * white) on the page root, so a `Button` or link inside follows the brand\n * with no props. The check is against white, Genesis's light background;\n * under a `.dark` theme the fill stays readable (white on the shade) but\n * accent-coloured text may not, so public pages are best served light.\n *\n * `embedded` is the frame-ancestors case. It drops the header and footer,\n * the background and `min-height: 100dvh` (inside an iframe the viewport is\n * the frame, so a full-height page would never let the frame shrink to its\n * content).\n *\n * This is a **layout**: it positions content and binds to nothing. The\n * content column is a `Container` for the one-home reason.\n *\n * @stability Beta\n */\nexport const PublicPage = React.forwardRef<HTMLDivElement, PublicPageProps>(\n (\n {\n brand,\n title,\n description,\n maxWidth = \"sm\",\n poweredBy,\n footer,\n embedded = false,\n slots = {},\n slotProps = {},\n testID = \"public-page\",\n className,\n style,\n children,\n ...props\n },\n ref,\n ) => {\n const showPoweredBy = poweredBy !== undefined && poweredBy !== null && poweredBy !== false;\n const hasFooter = showPoweredBy || (footer !== undefined && footer !== null && footer !== false);\n return (\n <div\n ref={ref}\n data-slot=\"public-page\"\n data-embedded={embedded ? \"true\" : undefined}\n data-testid={testID}\n className={cn(\n \"flex w-full min-w-0 flex-col text-foreground\",\n embedded ? \"bg-transparent\" : \"min-h-dvh bg-background\",\n className,\n )}\n style={{ ...brandThemeStyle(brand), ...style }}\n {...props}\n >\n {!embedded && brand\n ? renderSlot(DefaultHeader, slots.header, { brand, testID: `${testID}-header` }, slotProps.header)\n : null}\n <main\n data-slot=\"public-page-main\"\n data-testid={`${testID}-main`}\n className={cn(\"flex min-w-0 flex-1 flex-col\", embedded ? \"py-4\" : \"py-8 sm:py-12\")}\n >\n <Container size={maxWidth} className=\"flex min-w-0 flex-col gap-6\">\n {title ? (\n <div data-slot=\"public-page-heading\" className=\"flex min-w-0 flex-col gap-1\">\n <h1 className=\"text-2xl font-bold tracking-tight break-words sm:text-3xl\">{title}</h1>\n {description ? <p className=\"text-muted-foreground\">{description}</p> : null}\n </div>\n ) : null}\n {children}\n </Container>\n </main>\n {!embedded && hasFooter\n ? renderSlot(\n DefaultFooter,\n slots.footer,\n { children: footer, poweredBy: showPoweredBy ? poweredBy : undefined, testID: `${testID}-footer` },\n slotProps.footer,\n )\n : null}\n </div>\n );\n },\n);\nPublicPage.displayName = \"PublicPage\";\n\nfunction DefaultHeader({ brand, testID }: PublicPageSlotMap[\"header\"]) {\n return (\n <header\n data-slot=\"public-page-header\"\n data-testid={testID}\n className=\"border-b border-border border-t-4 border-t-[hsl(var(--brand-accent,var(--primary)))] bg-background\"\n >\n {brand.headerImageUrl ? (\n <img\n src={brand.headerImageUrl}\n alt=\"\"\n data-slot=\"public-page-cover\"\n className=\"block h-28 w-full object-cover sm:h-44\"\n />\n ) : null}\n <Container size=\"lg\" className=\"flex min-h-14 items-center gap-3 py-3\">\n {brand.logoUrl ? (\n // The name is beside it, so the image is decorative: an alt of the\n // name would read it twice.\n <img src={brand.logoUrl} alt=\"\" data-slot=\"public-page-logo\" className=\"h-8 w-auto max-w-32 shrink-0 object-contain\" />\n ) : null}\n <span data-slot=\"public-page-brand-name\" className=\"min-w-0 truncate font-semibold\">\n {brand.name}\n </span>\n </Container>\n </header>\n );\n}\n\nfunction DefaultFooter({ children, poweredBy, testID }: PublicPageSlotMap[\"footer\"]) {\n return (\n <footer data-slot=\"public-page-footer\" data-testid={testID} className=\"border-t border-border\">\n <Container\n size=\"lg\"\n 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\"\n >\n {children ? <div className=\"min-w-0\">{children}</div> : null}\n {poweredBy ? (\n <p data-slot=\"public-page-powered-by\" className=\"sm:ml-auto\">\n Powered by {poweredBy}\n </p>\n ) : null}\n </Container>\n </footer>\n );\n}\n"]}
1
+ {"version":3,"sources":["../../../src/components/layout/auth-page.tsx","../../../src/components/layout/brand-theme.ts","../../../src/components/layout/public-page.tsx"],"names":["React2","jsxs","renderSlot","jsx","DefaultFooter"],"mappings":"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","file":"index.js","sourcesContent":["import * as React from \"react\";\nimport { renderSlot, type SlotPropsFor, type Slots } from \"@marktiderman/genesis-core/slots\";\nimport { cn } from \"../../utils\";\n\n/**\n * The brand block above the card. Same fields as native `AuthBrandingProps`\n * (`logo`, `headline`, `subhead`), except that `headline` is required here:\n * it is the page's `<h1>`.\n */\nexport interface AuthPageBrandingProps {\n /** Brand mark (logo node). */\n logo?: React.ReactNode;\n /** \"Sign in\", \"Reset your password\", \"You're invited\". Renders as the page's h1. */\n headline: React.ReactNode;\n /** One line under the headline. */\n subhead?: React.ReactNode;\n testID?: string;\n}\n\n/** The parts of an {@link AuthPage} that `slots` and `slotProps` reach. Names match native `AuthScreenSlotMap`. */\nexport type AuthPageSlotMap = {\n branding: AuthPageBrandingProps & { testID: string };\n social: { children: React.ReactNode; testID: string };\n footer: { children: React.ReactNode; testID: string };\n};\n\nexport interface AuthPageProps extends Omit<React.HTMLAttributes<HTMLDivElement>, \"title\"> {\n /** Logo, headline (h1) and subhead above the card. */\n branding: AuthPageBrandingProps;\n /** Sign-in provider buttons (Google, Apple…), above the form inside the card. */\n social?: React.ReactNode;\n /** The form: sign-in, sign-up, reset, invite acceptance, verify. The card's body. */\n children?: React.ReactNode;\n /** Below the card: \"Forgot password?\", \"Create account\". */\n footer?: React.ReactNode;\n /** The small print under everything: terms and privacy. */\n legal?: React.ReactNode;\n /**\n * A panel beside the card on wide screens (an illustration, a quote, a\n * product shot). Hidden below the `lg` breakpoint, where the card is the\n * whole page.\n */\n aside?: React.ReactNode;\n /** Which side `aside` sits on. Default `\"end\"`. */\n asidePosition?: \"start\" | \"end\";\n /** Replace (a component) or drop (`null`) one part. */\n slots?: Slots<AuthPageSlotMap>;\n /** More props for a default part, merged over what the page passes. */\n slotProps?: SlotPropsFor<AuthPageSlotMap>;\n /** testID on the root. Default `\"auth-page\"`; parts use `${testID}-branding`, `-card`, `-social`, `-footer`, `-legal`, `-aside`. */\n testID?: string;\n}\n\n/**\n * AuthPage — the web counterpart of native `AuthScreen`: a whole-screen,\n * centred card for sign-in, sign-up, password reset, invite acceptance and\n * email verification.\n *\n * ```tsx\n * <AuthPage\n * branding={{ logo: <Logo />, headline: \"Sign in\", subhead: \"Welcome back.\" }}\n * footer={<><a href=\"/reset\">Forgot password?</a> · <a href=\"/sign-up\">Create account</a></>}\n * legal={<>By continuing you agree to the <a href=\"/terms\">terms</a>.</>}\n * aside={<img src={hero} alt=\"\" />}\n * >\n * <SignInForm />\n * </AuthPage>\n * ```\n *\n * The card column is a `<main>` landmark and the headline is its single\n * `<h1>`, so a screen reader lands on the form's purpose. The page fills the\n * viewport (`min-height: 100dvh`) and the card shrinks to the screen with a\n * 16px gutter, so nothing scrolls sideways at 375px.\n *\n * Native `AuthScreen` takes `form` and `providers` as data and renders its\n * own `AuthForm`; this one is a **layout**, so the form and the provider\n * buttons are nodes the app passes. Slots are the same contract:\n * replace or drop (`null`) `branding`, `social` or `footer`.\n *\n * @stability Beta\n */\nexport const AuthPage = React.forwardRef<HTMLDivElement, AuthPageProps>(\n (\n {\n branding,\n social,\n children,\n footer,\n legal,\n aside,\n asidePosition = \"end\",\n slots = {},\n slotProps = {},\n testID = \"auth-page\",\n className,\n ...props\n },\n ref,\n ) => (\n <div\n ref={ref}\n data-slot=\"auth-page\"\n data-testid={testID}\n className={cn(\n \"flex min-h-dvh w-full min-w-0 bg-background text-foreground\",\n aside ? \"lg:grid lg:grid-cols-2\" : null,\n className,\n )}\n {...props}\n >\n <main\n data-slot=\"auth-page-main\"\n className=\"flex min-w-0 flex-1 flex-col items-center justify-center px-4 py-10 sm:px-6\"\n >\n <div className=\"flex w-full max-w-sm min-w-0 flex-col gap-6\">\n {renderSlot(\n DefaultBranding,\n slots.branding,\n { ...branding, testID: branding.testID ?? `${testID}-branding` },\n slotProps.branding,\n )}\n <div\n data-slot=\"auth-page-card\"\n data-testid={`${testID}-card`}\n className=\"flex min-w-0 flex-col gap-6 rounded-xl border border-border bg-card p-6 text-card-foreground shadow-sm\"\n >\n {social ? renderSlot(DefaultSocial, slots.social, { children: social, testID: `${testID}-social` }, slotProps.social) : null}\n {children}\n </div>\n {footer\n ? renderSlot(DefaultFooter, slots.footer, { children: footer, testID: `${testID}-footer` }, slotProps.footer)\n : null}\n {legal ? (\n <p\n data-slot=\"auth-page-legal\"\n data-testid={`${testID}-legal`}\n className=\"text-center text-xs text-muted-foreground\"\n >\n {legal}\n </p>\n ) : null}\n </div>\n </main>\n {aside ? (\n <aside\n data-slot=\"auth-page-aside\"\n data-testid={`${testID}-aside`}\n className={cn(\n \"relative hidden min-w-0 overflow-hidden bg-muted lg:flex lg:items-center lg:justify-center\",\n asidePosition === \"start\" ? \"lg:order-first\" : null,\n )}\n >\n {aside}\n </aside>\n ) : null}\n </div>\n ),\n);\nAuthPage.displayName = \"AuthPage\";\n\nfunction DefaultBranding({ logo, headline, subhead, testID }: AuthPageSlotMap[\"branding\"]) {\n return (\n <header data-slot=\"auth-page-branding\" data-testid={testID} className=\"flex flex-col items-center gap-3 text-center\">\n {logo ? (\n <div data-testid={`${testID}-logo`} className=\"mb-1\">\n {logo}\n </div>\n ) : null}\n <h1 data-testid={`${testID}-headline`} className=\"text-2xl font-bold tracking-tight break-words\">\n {headline}\n </h1>\n {subhead ? (\n <p data-testid={`${testID}-subhead`} className=\"text-sm text-muted-foreground\">\n {subhead}\n </p>\n ) : null}\n </header>\n );\n}\n\nfunction DefaultSocial({ children, testID }: AuthPageSlotMap[\"social\"]) {\n return (\n <div data-slot=\"auth-page-social\" data-testid={testID} className=\"flex flex-col gap-2\">\n {children}\n </div>\n );\n}\n\nfunction DefaultFooter({ children, testID }: AuthPageSlotMap[\"footer\"]) {\n return (\n <div\n data-slot=\"auth-page-footer\"\n data-testid={testID}\n className=\"flex flex-wrap items-center justify-center gap-x-4 gap-y-2 text-center text-sm text-muted-foreground\"\n >\n {children}\n </div>\n );\n}\n","import type * as React from \"react\";\n\n/**\n * Brand theming for public pages: a host's brand object becomes CSS\n * variables on a page root, so every Genesis component inside it (a\n * `Button`, a focus ring, a link styled `text-primary`) picks up the accent\n * without knowing a brand exists.\n *\n * The accent is the one colour a public page takes from outside the token\n * set, and it is the one most likely to be unreadable: brand palettes are\n * picked for logos, not for 14px text. Emerald `#10b981` with white text is\n * 2.5:1, half of WCAG AA's 4.5:1. So the accent is never used as-is for\n * text or for a button's fill. {@link accessibleAccent} keeps its hue and\n * saturation and lowers its lightness until white text on it (and it as\n * text on white) clears 4.5:1; the raw accent stays available for\n * decoration (`--brand-accent`: a header rule, a border).\n *\n * Variables set (all HSL triplets, the same shape as Genesis tokens, so\n * `hsl(var(--x))` works everywhere):\n *\n * - `--brand-accent`: the accent exactly as given. Decoration only.\n * - `--brand-accent-strong`: the accessible shade. Text and fills.\n * - `--brand-accent-foreground`: text on `--brand-accent-strong`.\n * - `--primary`, `--primary-foreground`, `--ring`: the Genesis tokens,\n * pointed at the accessible shade, which is what makes nested components\n * follow the brand.\n */\n\n/** What a host knows about the brand a public page belongs to. */\nexport interface PublicBrand {\n /** Display name, shown in the header beside the logo (so the logo itself is decorative, `alt=\"\"`). */\n name: string;\n /** Logo image URL. */\n logoUrl?: string;\n /**\n * Accent colour: `#rgb`, `#rrggbb`, `rgb()`/`rgba()`, `hsl()`/`hsla()` or a\n * bare token triplet like `\"160 84% 39%\"`. Alpha is ignored (the accent is\n * treated as opaque). Invalid or absent: Genesis's own primary.\n */\n accent?: string;\n /** Optional wide cover image above the header row. Decorative. */\n headerImageUrl?: string;\n}\n\n/** An sRGB colour, 0-255 per channel. */\nexport interface Rgb {\n r: number;\n g: number;\n b: number;\n}\n\n/** WCAG AA for normal-size text. */\nexport const MIN_TEXT_CONTRAST = 4.5;\n\nconst WHITE: Rgb = { r: 255, g: 255, b: 255 };\n\nconst NUM = String.raw`[+-]?(?:\\d+\\.?\\d*|\\.\\d+)`;\nconst HUE = `(${NUM})(?:deg)?`;\nconst PCT = `(${NUM})%?`;\nconst ALPHA = String.raw`(?:\\s*[,/]\\s*${NUM}%?)?`;\nconst SEP = String.raw`(?:\\s*,\\s*|\\s+)`;\nconst HSL_FN = new RegExp(String.raw`^hsla?\\(\\s*${HUE}${SEP}${PCT}${SEP}${PCT}${ALPHA}\\s*\\)$`);\n// Bare shadcn-style token triplet: percentages required so it can't be read as rgb.\nconst HSL_BARE = new RegExp(String.raw`^${HUE}\\s+(${NUM})%\\s+(${NUM})%$`);\n\n/**\n * Parses `#rgb`, `#rrggbb`, `rgb()`/`rgba()`, `hsl()`/`hsla()` (commas or\n * spaces, optional `/` or `,` alpha, which is ignored) and bare HSL triplets\n * like `\"160 84% 39%\"`. Anything else, including out-of-range channels: `null`.\n * Never throws.\n */\nexport function parseColor(input: string | undefined | null): Rgb | null {\n if (typeof input !== \"string\") return null;\n const value = input.trim().toLowerCase();\n if (!value) return null;\n const hex = /^#([0-9a-f]{3}|[0-9a-f]{6})$/.exec(value);\n if (hex) {\n const digits = hex[1]!;\n const full = digits.length === 3 ? digits.replace(/./g, (c) => c + c) : digits;\n return {\n r: parseInt(full.slice(0, 2), 16),\n g: parseInt(full.slice(2, 4), 16),\n b: parseInt(full.slice(4, 6), 16),\n };\n }\n const fn = /^rgba?\\(\\s*(\\d{1,3})[\\s,]+(\\d{1,3})[\\s,]+(\\d{1,3})\\s*(?:[,/]\\s*[\\d.]+%?\\s*)?\\)$/.exec(value);\n if (fn) {\n const [r, g, b] = [fn[1], fn[2], fn[3]].map(Number) as [number, number, number];\n if ([r, g, b].every((c) => c <= 255)) return { r, g, b };\n return null;\n }\n const hsl = HSL_FN.exec(value) ?? HSL_BARE.exec(value);\n if (hsl) {\n const [h, sat, l] = [hsl[1], hsl[2], hsl[3]].map(Number) as [number, number, number];\n if (![h, sat, l].every(Number.isFinite) || sat < 0 || sat > 100 || l < 0 || l > 100) return null;\n return hslToRgb({ h, s: sat, l });\n }\n return null;\n}\n\n/** WCAG 2 relative luminance, 0 (black) to 1 (white). */\nexport function relativeLuminance({ r, g, b }: Rgb): number {\n const lin = (c: number) => {\n const s = c / 255;\n return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;\n };\n return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);\n}\n\n/** WCAG 2 contrast ratio between two colours, 1 to 21. */\nexport function contrastRatio(a: Rgb, b: Rgb): number {\n const [hi, lo] = [relativeLuminance(a), relativeLuminance(b)].sort((x, y) => y - x) as [number, number];\n return (hi + 0.05) / (lo + 0.05);\n}\n\n/** sRGB to HSL: hue 0-360, saturation and lightness 0-100. */\nexport function rgbToHsl({ r, g, b }: Rgb): { h: number; s: number; l: number } {\n const [rn, gn, bn] = [r / 255, g / 255, b / 255];\n const max = Math.max(rn, gn, bn);\n const min = Math.min(rn, gn, bn);\n const l = (max + min) / 2;\n if (max === min) return { h: 0, s: 0, l: l * 100 };\n const d = max - min;\n const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);\n let h: number;\n if (max === rn) h = (gn - bn) / d + (gn < bn ? 6 : 0);\n else if (max === gn) h = (bn - rn) / d + 2;\n else h = (rn - gn) / d + 4;\n return { h: h * 60, s: s * 100, l: l * 100 };\n}\n\n/** HSL (hue 0-360, saturation and lightness 0-100) to sRGB, channels rounded to 8 bits. */\nexport function hslToRgb(hsl: { h: number; s: number; l: number }): Rgb {\n const { r, g, b } = hslToRgbExact(hsl);\n return { r: Math.round(r), g: Math.round(g), b: Math.round(b) };\n}\n\n/** The Genesis token shape: `\"160 84% 39%\"`, for `hsl(var(--x))`. */\nexport function toHslTriplet(color: Rgb): string {\n const { h, s, l } = rgbToHsl(color);\n return `${round(h)} ${round(s)}% ${round(l)}%`;\n}\n\nconst round = (n: number) => Math.round(n * 10) / 10;\n\nexport interface AccessibleAccent {\n /** The accent as given. */\n accent: Rgb;\n /** The shade to use for text on white and for fills under white text. Equals `accent` when it already passes. */\n strong: Rgb;\n /**\n * `strong` as the exact HSL triplet to emit (`\"H S% L%\"`, 0.1 precision).\n * The contrast check runs on this value, not on the unrounded colour, so\n * what the browser renders is what was checked.\n */\n strongTriplet: string;\n /** Contrast of the given accent against the background. */\n ratio: number;\n /** Contrast of `strongTriplet`, as rendered, against the background (at least `minRatio`). */\n strongRatio: number;\n /** True when `strong` had to be darkened from `accent`. */\n adjusted: boolean;\n}\n\n/** HSL to sRGB without rounding the channels, for checking what a browser may render. */\nfunction hslToRgbExact({ h, s, l }: { h: number; s: number; l: number }): Rgb {\n const hn = ((h % 360) + 360) % 360;\n const sn = s / 100;\n const ln = l / 100;\n const k = (n: number) => (n + hn / 30) % 12;\n const a = sn * Math.min(ln, 1 - ln);\n const f = (n: number) => ln - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)));\n return { r: f(0) * 255, g: f(8) * 255, b: f(4) * 255 };\n}\n\n/**\n * Contrast of an emitted HSL triplet against `background`: the worse of the\n * exact colour and its 8-bit rendering, so neither a float nor a quantising\n * renderer can land under the target.\n */\nfunction emittedRatio(hsl: { h: number; s: number; l: number }, background: Rgb): number {\n return Math.min(contrastRatio(hslToRgbExact(hsl), background), contrastRatio(hslToRgb(hsl), background));\n}\n\nconst tripletOf = ({ h, s, l }: { h: number; s: number; l: number }) => `${h} ${s}% ${l}%`;\n\n/**\n * The contrast fallback. Returns the accent unchanged when its emitted\n * triplet reaches `minRatio` against `background` (white by default);\n * otherwise the same (rounded) hue and saturation at the highest lightness,\n * to 0.1%, whose emitted triplet does. Every check runs on the value\n * that is actually emitted, after rounding. It always ends: lightness 0 is\n * black, 21:1 on white.\n */\nexport function accessibleAccent(\n accent: Rgb | string,\n { background = WHITE, minRatio = MIN_TEXT_CONTRAST }: { background?: Rgb; minRatio?: number } = {},\n): AccessibleAccent | null {\n const base = typeof accent === \"string\" ? parseColor(accent) : accent;\n if (!base) return null;\n const ratio = contrastRatio(base, background);\n const exact = rgbToHsl(base);\n const h = round(exact.h);\n const s = round(exact.s);\n const l = round(exact.l);\n const emitted = emittedRatio({ h, s, l }, background);\n if (emitted >= minRatio) {\n return { accent: base, strong: base, strongTriplet: tripletOf({ h, s, l }), ratio, strongRatio: emitted, adjusted: false };\n }\n // Lightness in integer tenths (no float drift). Walk down in whole\n // percents to the first passing shade, then take the lightest passing\n // tenth above it, so the result stays as close to the brand as it can.\n const start = Math.round(l * 10);\n const pick = (tenths: number) => {\n const candidate = { h, s, l: tenths / 10 };\n const strongRatio = emittedRatio(candidate, background);\n return strongRatio >= minRatio ? { candidate, strongRatio } : null;\n };\n for (let coarse = start - 10; coarse > -10; coarse -= 10) {\n const floor = Math.max(coarse, 0);\n const passing = pick(floor);\n if (!passing) continue;\n let best = passing;\n for (let fine = Math.min(floor + 9, start - 1); fine > floor; fine -= 1) {\n const found = pick(fine);\n if (found) {\n best = found;\n break;\n }\n }\n const { candidate, strongRatio } = best;\n return { accent: base, strong: hslToRgb(candidate), strongTriplet: tripletOf(candidate), ratio, strongRatio, adjusted: true };\n }\n const black = { h: 0, s: 0, l: 0 };\n return {\n accent: base,\n strong: { r: 0, g: 0, b: 0 },\n strongTriplet: tripletOf(black),\n ratio,\n strongRatio: emittedRatio(black, background),\n adjusted: true,\n };\n}\n\n/**\n * The CSS variables for a brand's accent, for a page root's `style`. Empty\n * when the brand has no valid accent, so the page keeps Genesis's primary.\n */\nexport function brandThemeStyle(brand: Pick<PublicBrand, \"accent\"> | undefined): React.CSSProperties {\n const result = brand?.accent ? accessibleAccent(brand.accent) : null;\n if (!result) return {};\n const strong = result.strongTriplet;\n const foreground = toHslTriplet(WHITE);\n return {\n \"--brand-accent\": toHslTriplet(result.accent),\n \"--brand-accent-strong\": strong,\n \"--brand-accent-foreground\": foreground,\n \"--primary\": strong,\n \"--primary-foreground\": foreground,\n \"--ring\": strong,\n } as React.CSSProperties;\n}\n","import * as React from \"react\";\nimport { renderSlot, type SlotPropsFor, type Slots } from \"@marktiderman/genesis-core/slots\";\nimport { cn } from \"../../utils\";\nimport { Container, type ContainerProps } from \"./container\";\nimport { brandThemeStyle, type PublicBrand } from \"./brand-theme\";\n\n/** The parts of a {@link PublicPage} that `slots` and `slotProps` reach. */\nexport type PublicPageSlotMap = {\n header: { brand: PublicBrand; testID: string };\n footer: { children: React.ReactNode; poweredBy?: React.ReactNode; testID: string };\n};\n\nexport interface PublicPageProps extends Omit<React.HTMLAttributes<HTMLDivElement>, \"title\"> {\n /**\n * The brand the page belongs to. Its header (logo, name, cover image) shows\n * at the top, and its `accent` themes the page: see `brandThemeStyle`.\n */\n brand?: PublicBrand;\n /** 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). */\n title?: React.ReactNode;\n /** Supporting copy under `title`. */\n description?: React.ReactNode;\n /** Width of the content column: a `Container` size. Default `\"sm\"`. */\n maxWidth?: NonNullable<ContainerProps[\"size\"]>;\n /**\n * Who runs the page, as \"Powered by …\" in the footer (a name or a link).\n * Leave it out, or pass `false`, to hide it.\n */\n poweredBy?: React.ReactNode;\n /** More footer content: privacy and terms links, an address. */\n footer?: React.ReactNode;\n /**\n * For an iframe: no header or footer chrome, a transparent background and\n * no full-viewport height, so the frame is as tall as its content. The\n * brand accent still applies. A host usually sets it from `?embed=1`.\n */\n embedded?: boolean;\n /** Replace (a component) or drop (`null`) the header or footer. */\n slots?: Slots<PublicPageSlotMap>;\n /** More props for a default part, merged over what the page passes. */\n slotProps?: SlotPropsFor<PublicPageSlotMap>;\n /** testID on the root. Default `\"public-page\"`; parts use `${testID}-header`, `-main`, `-footer`. */\n testID?: string;\n}\n\n/**\n * PublicPage — a page anyone can open without signing in: a public form, a\n * QR poster, a landing page. A brand header, one content column, and a\n * \"Powered by …\" footer, themed from a brand object.\n *\n * ```tsx\n * <PublicPage\n * brand={{ name: \"Riverside Physio\", logoUrl, accent: \"#10b981\" }}\n * poweredBy={<a href=\"https://example.com\">Example</a>}\n * embedded={searchParams.get(\"embed\") === \"1\"}\n * >\n * <Form … />\n * </PublicPage>\n * ```\n *\n * The brand's accent becomes `--primary` (darkened when it fails 4.5:1 on\n * white) on the page root, so a `Button` or link inside follows the brand\n * with no props. The check is against white, Genesis's light background;\n * under a `.dark` theme the fill stays readable (white on the shade) but\n * accent-coloured text may not, so public pages are best served light.\n *\n * `embedded` is the frame-ancestors case. It drops the header and footer,\n * the background and `min-height: 100dvh` (inside an iframe the viewport is\n * the frame, so a full-height page would never let the frame shrink to its\n * content).\n *\n * This is a **layout**: it positions content and binds to nothing. The\n * content column is a `Container` for the one-home reason.\n *\n * @stability Beta\n */\nexport const PublicPage = React.forwardRef<HTMLDivElement, PublicPageProps>(\n (\n {\n brand,\n title,\n description,\n maxWidth = \"sm\",\n poweredBy,\n footer,\n embedded = false,\n slots = {},\n slotProps = {},\n testID = \"public-page\",\n className,\n style,\n children,\n ...props\n },\n ref,\n ) => {\n const showPoweredBy = poweredBy !== undefined && poweredBy !== null && poweredBy !== false;\n const hasFooter = showPoweredBy || (footer !== undefined && footer !== null && footer !== false);\n return (\n <div\n ref={ref}\n data-slot=\"public-page\"\n data-embedded={embedded ? \"true\" : undefined}\n data-testid={testID}\n className={cn(\n \"flex w-full min-w-0 flex-col text-foreground\",\n embedded ? \"bg-transparent\" : \"min-h-dvh bg-background\",\n className,\n )}\n style={{ ...brandThemeStyle(brand), ...style }}\n {...props}\n >\n {!embedded && brand\n ? renderSlot(DefaultHeader, slots.header, { brand, testID: `${testID}-header` }, slotProps.header)\n : null}\n <main\n data-slot=\"public-page-main\"\n data-testid={`${testID}-main`}\n className={cn(\"flex min-w-0 flex-1 flex-col\", embedded ? \"py-4\" : \"py-8 sm:py-12\")}\n >\n <Container size={maxWidth} className=\"flex min-w-0 flex-col gap-6\">\n {title ? (\n <div data-slot=\"public-page-heading\" className=\"flex min-w-0 flex-col gap-1\">\n <h1 className=\"text-2xl font-bold tracking-tight break-words sm:text-3xl\">{title}</h1>\n {description ? <p className=\"text-muted-foreground\">{description}</p> : null}\n </div>\n ) : null}\n {children}\n </Container>\n </main>\n {!embedded && hasFooter\n ? renderSlot(\n DefaultFooter,\n slots.footer,\n { children: footer, poweredBy: showPoweredBy ? poweredBy : undefined, testID: `${testID}-footer` },\n slotProps.footer,\n )\n : null}\n </div>\n );\n },\n);\nPublicPage.displayName = \"PublicPage\";\n\nfunction DefaultHeader({ brand, testID }: PublicPageSlotMap[\"header\"]) {\n return (\n <header\n data-slot=\"public-page-header\"\n data-testid={testID}\n className=\"border-b border-border border-t-4 border-t-[hsl(var(--brand-accent,var(--primary)))] bg-background\"\n >\n {brand.headerImageUrl ? (\n <img\n src={brand.headerImageUrl}\n alt=\"\"\n data-slot=\"public-page-cover\"\n className=\"block h-28 w-full object-cover sm:h-44\"\n />\n ) : null}\n <Container size=\"lg\" className=\"flex min-h-14 items-center gap-3 py-3\">\n {brand.logoUrl ? (\n // The name is beside it, so the image is decorative: an alt of the\n // name would read it twice.\n <img src={brand.logoUrl} alt=\"\" data-slot=\"public-page-logo\" className=\"h-8 w-auto max-w-32 shrink-0 object-contain\" />\n ) : null}\n <span data-slot=\"public-page-brand-name\" className=\"min-w-0 truncate font-semibold\">\n {brand.name}\n </span>\n </Container>\n </header>\n );\n}\n\nfunction DefaultFooter({ children, poweredBy, testID }: PublicPageSlotMap[\"footer\"]) {\n return (\n <footer data-slot=\"public-page-footer\" data-testid={testID} className=\"border-t border-border\">\n <Container\n size=\"lg\"\n 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\"\n >\n {children ? <div className=\"min-w-0\">{children}</div> : null}\n {poweredBy ? (\n <p data-slot=\"public-page-powered-by\" className=\"sm:ml-auto\">\n Powered by {poweredBy}\n </p>\n ) : null}\n </Container>\n </footer>\n );\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marktiderman/genesis-ui",
3
- "version": "2.15.0",
3
+ "version": "2.16.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -394,7 +394,7 @@
394
394
  "vaul": "^1.0.0"
395
395
  },
396
396
  "dependencies": {
397
- "@marktiderman/genesis-core": "^1.9.0",
397
+ "@marktiderman/genesis-core": "^1.10.0",
398
398
  "@tanstack/react-table": "^8.21.3",
399
399
  "@tanstack/react-virtual": "^3.14.0",
400
400
  "class-variance-authority": "^0.7.1",