@rnine/core 0.1.0 → 0.2.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.
- package/README.md +12 -2
- package/dist/_variables.scss +1 -0
- package/dist/components/Card/Card.module.js.map +1 -1
- package/dist/components/Skeleton/Skeleton.module.js.map +1 -1
- package/dist/components.css +1 -1
- package/dist/hooks/cardLift.d.ts +24 -0
- package/dist/hooks/cardLift.js +82 -0
- package/dist/hooks/cardLift.js.map +1 -0
- package/dist/hooks/mood.d.ts +5 -29
- package/dist/hooks/mood.js +59 -69
- package/dist/hooks/mood.js.map +1 -1
- package/dist/hooks/moodBoot.d.ts +44 -0
- package/dist/hooks/moodBoot.js +24 -0
- package/dist/hooks/moodBoot.js.map +1 -0
- package/dist/hooks/motionSetting.d.ts +2 -7
- package/dist/hooks/motionSetting.js +8 -7
- package/dist/hooks/motionSetting.js.map +1 -1
- package/dist/index.js +4 -3
- package/dist/kit.d.ts +2 -0
- package/dist/kit.js +3 -2
- package/dist/motion/TiltCard.d.ts +2 -1
- package/dist/motion/TiltCard.js +20 -19
- package/dist/motion/TiltCard.js.map +1 -1
- package/dist/motion/TiltCard.module.js.map +1 -1
- package/dist/pages/create.d.ts +31 -0
- package/dist/pages/create.js +25 -0
- package/dist/pages/create.js.map +1 -0
- package/dist/pages/email.d.ts +49 -0
- package/dist/pages/email.js +43 -0
- package/dist/pages/email.js.map +1 -0
- package/dist/pages/icon.d.ts +31 -0
- package/dist/pages/icon.js +24 -0
- package/dist/pages/icon.js.map +1 -0
- package/dist/pages/page.js +6 -0
- package/dist/pages/page.js.map +1 -0
- package/dist/pages/status.d.ts +60 -0
- package/dist/pages/status.js +57 -0
- package/dist/pages/status.js.map +1 -0
- package/dist/pages/theme.d.ts +77 -0
- package/dist/pages/theme.js +45 -0
- package/dist/pages/theme.js.map +1 -0
- package/dist/pages.d.ts +16 -0
- package/dist/pages.js +13 -0
- package/dist/pages.js.map +1 -0
- package/dist/provider/ThemeProvider.d.ts +3 -0
- package/dist/provider/ThemeProvider.js.map +1 -1
- package/dist/styles.css +2 -1
- package/dist/tokens.css +1 -0
- package/dist/tokens.d.ts +11 -1
- package/dist/tokens.js +10 -1
- package/package.json +6 -2
- package/scripts/build-tokens.mjs +18 -3
- package/scss/_mixins.scss +56 -4
- package/tokens/shape.json +5 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"create.js","names":[],"sources":["../../src/pages/create.ts"],"sourcesContent":["import { bootScript, type BootScriptOptions } from '../hooks/moodBoot';\nimport { emailTheme, renderEmailLayout, type EmailLayoutOptions, type EmailTheme } from './email';\nimport { renderMarkIcon, type MarkIconOptions } from './icon';\nimport { renderStatusPage, themeColorMeta, type StatusPageOptions } from './status';\nimport type { PageFont, PageTheme } from './theme';\n\nexport type Pages = {\n /** A complete, self-contained status page (maintenance, coming soon, an error). */\n statusPage(options: StatusPageOptions): string;\n /** The frame every email shares, in the library's look. */\n emailLayout(options: EmailLayoutOptions): string;\n /** The library's look as resolved values, for the parts of an email you write yourself. */\n emailTheme(options?: { palette?: string | null }): EmailTheme;\n /** The site's mark on an accent plate in the library's corner shape: a favicon or app icon (SVG). */\n markIcon(options: MarkIconOptions): string;\n /** The inline script that sets the mood before first paint, with the library's Auto rule. */\n moodBootScript(options?: BootScriptOptions): string;\n /** <meta name=\"theme-color\"> for both moods. */\n themeColorMeta(options?: { palette?: string | null }): string;\n /** The font files the pages and emails use: serve each at <fontsUrl><id>.woff2. */\n pageFonts: readonly PageFont[];\n};\n\n/**\n * A library's static pages: status pages, emails, icons. Each library's\n * \"<library>/pages\" export is createPages with its tokens (core's own is\n * Plain), so the same calls take the look of whichever library is imported.\n */\nexport function createPages(theme: PageTheme): Pages {\n const { tokens } = theme;\n return {\n statusPage: (options) => renderStatusPage(theme, options),\n emailLayout: (options) => renderEmailLayout(tokens, options),\n emailTheme: (options) => emailTheme(tokens, options),\n markIcon: (options) => renderMarkIcon(tokens, options),\n moodBootScript: (options = {}) => bootScript({ ...options, auto: options.auto ?? tokens.auto, names: options.names ?? tokens.moods }),\n themeColorMeta: (options) => themeColorMeta(tokens, options),\n pageFonts: tokens.fonts ?? [],\n };\n}\n"],"mappings":";;;;;AA4BA,SAAgB,EAAY,GAAyB;CACnD,IAAM,EAAE,cAAW;CACnB,OAAO;EACL,aAAa,MAAY,EAAiB,GAAO,CAAO;EACxD,cAAc,MAAY,EAAkB,GAAQ,CAAO;EAC3D,aAAa,MAAY,EAAW,GAAQ,CAAO;EACnD,WAAW,MAAY,EAAe,GAAQ,CAAO;EACrD,iBAAiB,IAAU,CAAC,MAAM,EAAW;GAAE,GAAG;GAAS,MAAM,EAAQ,QAAQ,EAAO;GAAM,OAAO,EAAQ,SAAS,EAAO;EAAM,CAAC;EACpI,iBAAiB,MAAY,EAAe,GAAQ,CAAO;EAC3D,WAAW,EAAO,SAAS,CAAC;CAC9B;AACF"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { type PageTokens } from './theme';
|
|
2
|
+
/**
|
|
3
|
+
* A library's look as an email can carry it. Email clients don't read CSS
|
|
4
|
+
* custom properties, clip-path or (mostly) web fonts, so every value here is
|
|
5
|
+
* resolved: the light mood's colors (inboxes are light), in a palette.
|
|
6
|
+
*/
|
|
7
|
+
export type EmailTheme = {
|
|
8
|
+
/** Every mood role, resolved: colors.bg, colors.accent, colors['fg-muted'], ... */
|
|
9
|
+
colors: Record<string, string>;
|
|
10
|
+
/** Font stacks, quoted with ' so they fit inside style="..." */
|
|
11
|
+
fonts: {
|
|
12
|
+
display: string;
|
|
13
|
+
body: string;
|
|
14
|
+
mono: string;
|
|
15
|
+
};
|
|
16
|
+
/** Corner radius for buttons and cards ("0" where the library cuts corners) */
|
|
17
|
+
radius: string;
|
|
18
|
+
/** The accent mark's radius: square or round, as the library's ornament */
|
|
19
|
+
ornamentRadius: string;
|
|
20
|
+
/** @font-face rules for clients that load fonts (Apple Mail), from fontsUrl (ending in "/") */
|
|
21
|
+
fontFaces(fontsUrl: string): string;
|
|
22
|
+
};
|
|
23
|
+
export declare function emailTheme(tokens: PageTokens, { palette }?: {
|
|
24
|
+
palette?: string | null;
|
|
25
|
+
}): EmailTheme;
|
|
26
|
+
export type EmailLayoutOptions = {
|
|
27
|
+
/** Small mono line above the title (plain text) */
|
|
28
|
+
eyebrow: string;
|
|
29
|
+
/** The title, also the document title (plain text) */
|
|
30
|
+
title: string;
|
|
31
|
+
/** The preview line inbox lists show (plain text) */
|
|
32
|
+
preheader: string;
|
|
33
|
+
/** The message, as HTML (inline styles; use emailTheme for its colors) */
|
|
34
|
+
body: string;
|
|
35
|
+
/** One button (plain text label) */
|
|
36
|
+
action?: {
|
|
37
|
+
label: string;
|
|
38
|
+
href: string;
|
|
39
|
+
};
|
|
40
|
+
/** Small print at the bottom, as HTML */
|
|
41
|
+
footer: string;
|
|
42
|
+
/** Where the page fonts are served (absolute, ending in "/"). None: the fonts' fallbacks. */
|
|
43
|
+
fontsUrl?: string;
|
|
44
|
+
/** A palette of the library's; the default if it has none by that id. */
|
|
45
|
+
palette?: string | null;
|
|
46
|
+
lang?: string;
|
|
47
|
+
};
|
|
48
|
+
/** The frame every email shares: eyebrow, title, the body, an action, the footer. Tables and inline styles. */
|
|
49
|
+
export declare function renderEmailLayout(tokens: PageTokens, o: EmailLayoutOptions): string;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { fontFaces as e, fontStacks as t, roleValues as n, shape as r, tokenValue as i } from "./theme.js";
|
|
2
|
+
import { escapeHtml as a } from "./status.js";
|
|
3
|
+
//#region src/pages/email.ts
|
|
4
|
+
var o = (e) => `${Math.round(e * 10) / 10}px`.replace(/^0px$/, "0");
|
|
5
|
+
function s(a, { palette: s } = {}) {
|
|
6
|
+
let { round: c } = r(a), l = t(a), u = (e) => e.replace(/"/g, "'");
|
|
7
|
+
return {
|
|
8
|
+
colors: n(a, {
|
|
9
|
+
scheme: "light",
|
|
10
|
+
palette: s
|
|
11
|
+
}),
|
|
12
|
+
fonts: {
|
|
13
|
+
display: u(l.display),
|
|
14
|
+
body: u(l.body),
|
|
15
|
+
mono: u(l.mono)
|
|
16
|
+
},
|
|
17
|
+
radius: o(parseFloat(i(a, "corner", "corner-sm")) * c),
|
|
18
|
+
ornamentRadius: c ? `${Math.round(c * 100)}%` : "0",
|
|
19
|
+
fontFaces: (t) => e(a, t, "'")
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function c(e, t) {
|
|
23
|
+
let n = s(e, { palette: t.palette }), r = n.colors, i = n.fonts, o = t.action ? `<tr><td style="padding:8px 0 0 0;"><a href="${a(t.action.href)}" style="display:inline-block;background:${r.accent};color:${r["on-accent"]};font:600 15px/1 ${i.body};text-decoration:none;padding:14px 20px;border-radius:${n.radius};">${a(t.action.label)} →</a></td></tr>` : "";
|
|
24
|
+
return `<!doctype html>
|
|
25
|
+
<html lang="${a(t.lang ?? "en")}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="color-scheme" content="light"><meta name="supported-color-schemes" content="light">
|
|
26
|
+
<title>${a(t.title)}</title>
|
|
27
|
+
<style>${t.fontsUrl ? n.fontFaces(t.fontsUrl) : ""}a{color:${r["accent-text"]};}</style></head>
|
|
28
|
+
<body style="margin:0;padding:0;background:${r.bg};color:${r.fg};">
|
|
29
|
+
<div style="display:none;max-height:0;overflow:hidden;">${a(t.preheader)}</div>
|
|
30
|
+
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:${r.bg};">
|
|
31
|
+
<tr><td align="center" style="padding:32px 16px;">
|
|
32
|
+
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="max-width:600px;">
|
|
33
|
+
<tr><td style="padding:0 0 12px 0;font:400 13px/1 ${i.mono};letter-spacing:.12em;text-transform:uppercase;color:${r["fg-muted"]};"><span style="display:inline-block;width:6px;height:6px;background:${r.accent};border-radius:${n.ornamentRadius};margin-right:8px;vertical-align:middle;"></span>${a(t.eyebrow)}</td></tr>
|
|
34
|
+
<tr><td style="padding:0 0 24px 0;font:600 26px/1.2 ${i.display};color:${r.fg};">${a(t.title)}</td></tr>
|
|
35
|
+
<tr><td style="padding:0 0 16px 0;font:400 16px/1.6 ${i.body};color:${r.fg};">${t.body}</td></tr>
|
|
36
|
+
${o}
|
|
37
|
+
<tr><td style="padding:32px 0 0 0;"><div style="border-top:1px solid ${r.line};padding-top:16px;font:400 13px/1.5 ${i.body};color:${r["fg-muted"]};">${t.footer}</div></td></tr>
|
|
38
|
+
</table></td></tr></table></body></html>`;
|
|
39
|
+
}
|
|
40
|
+
//#endregion
|
|
41
|
+
export { s as emailTheme, c as renderEmailLayout };
|
|
42
|
+
|
|
43
|
+
//# sourceMappingURL=email.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"email.js","names":[],"sources":["../../src/pages/email.ts"],"sourcesContent":["import { escapeHtml } from './status';\nimport { fontFaces, fontStacks, roleValues, shape, tokenValue, type PageTokens } from './theme';\n\n/**\n * A library's look as an email can carry it. Email clients don't read CSS\n * custom properties, clip-path or (mostly) web fonts, so every value here is\n * resolved: the light mood's colors (inboxes are light), in a palette.\n */\nexport type EmailTheme = {\n /** Every mood role, resolved: colors.bg, colors.accent, colors['fg-muted'], ... */\n colors: Record<string, string>;\n /** Font stacks, quoted with ' so they fit inside style=\"...\" */\n fonts: { display: string; body: string; mono: string };\n /** Corner radius for buttons and cards (\"0\" where the library cuts corners) */\n radius: string;\n /** The accent mark's radius: square or round, as the library's ornament */\n ornamentRadius: string;\n /** @font-face rules for clients that load fonts (Apple Mail), from fontsUrl (ending in \"/\") */\n fontFaces(fontsUrl: string): string;\n};\n\nconst px = (n: number) => `${Math.round(n * 10) / 10}px`.replace(/^0px$/, '0');\n\nexport function emailTheme(tokens: PageTokens, { palette }: { palette?: string | null } = {}): EmailTheme {\n const { round } = shape(tokens);\n const stacks = fontStacks(tokens);\n const single = (stack: string) => stack.replace(/\"/g, \"'\");\n return {\n colors: roleValues(tokens, { scheme: 'light', palette }),\n fonts: { display: single(stacks.display), body: single(stacks.body), mono: single(stacks.mono) },\n radius: px(parseFloat(tokenValue(tokens, 'corner', 'corner-sm')) * round),\n ornamentRadius: round ? `${Math.round(round * 100)}%` : '0',\n fontFaces: (fontsUrl) => fontFaces(tokens, fontsUrl, \"'\"),\n };\n}\n\nexport type EmailLayoutOptions = {\n /** Small mono line above the title (plain text) */\n eyebrow: string;\n /** The title, also the document title (plain text) */\n title: string;\n /** The preview line inbox lists show (plain text) */\n preheader: string;\n /** The message, as HTML (inline styles; use emailTheme for its colors) */\n body: string;\n /** One button (plain text label) */\n action?: { label: string; href: string };\n /** Small print at the bottom, as HTML */\n footer: string;\n /** Where the page fonts are served (absolute, ending in \"/\"). None: the fonts' fallbacks. */\n fontsUrl?: string;\n /** A palette of the library's; the default if it has none by that id. */\n palette?: string | null;\n lang?: string;\n};\n\n/** The frame every email shares: eyebrow, title, the body, an action, the footer. Tables and inline styles. */\nexport function renderEmailLayout(tokens: PageTokens, o: EmailLayoutOptions): string {\n const t = emailTheme(tokens, { palette: o.palette });\n const C = t.colors;\n const F = t.fonts;\n const action = o.action\n ? `<tr><td style=\"padding:8px 0 0 0;\"><a href=\"${escapeHtml(o.action.href)}\" style=\"display:inline-block;background:${C.accent};color:${C['on-accent']};font:600 15px/1 ${F.body};text-decoration:none;padding:14px 20px;border-radius:${t.radius};\">${escapeHtml(o.action.label)} →</a></td></tr>`\n : '';\n return `<!doctype html>\n<html lang=\"${escapeHtml(o.lang ?? 'en')}\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><meta name=\"color-scheme\" content=\"light\"><meta name=\"supported-color-schemes\" content=\"light\">\n<title>${escapeHtml(o.title)}</title>\n<style>${o.fontsUrl ? t.fontFaces(o.fontsUrl) : ''}a{color:${C['accent-text']};}</style></head>\n<body style=\"margin:0;padding:0;background:${C.bg};color:${C.fg};\">\n<div style=\"display:none;max-height:0;overflow:hidden;\">${escapeHtml(o.preheader)}</div>\n<table role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"background:${C.bg};\">\n<tr><td align=\"center\" style=\"padding:32px 16px;\">\n<table role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"max-width:600px;\">\n<tr><td style=\"padding:0 0 12px 0;font:400 13px/1 ${F.mono};letter-spacing:.12em;text-transform:uppercase;color:${C['fg-muted']};\"><span style=\"display:inline-block;width:6px;height:6px;background:${C.accent};border-radius:${t.ornamentRadius};margin-right:8px;vertical-align:middle;\"></span>${escapeHtml(o.eyebrow)}</td></tr>\n<tr><td style=\"padding:0 0 24px 0;font:600 26px/1.2 ${F.display};color:${C.fg};\">${escapeHtml(o.title)}</td></tr>\n<tr><td style=\"padding:0 0 16px 0;font:400 16px/1.6 ${F.body};color:${C.fg};\">${o.body}</td></tr>\n${action}\n<tr><td style=\"padding:32px 0 0 0;\"><div style=\"border-top:1px solid ${C.line};padding-top:16px;font:400 13px/1.5 ${F.body};color:${C['fg-muted']};\">${o.footer}</div></td></tr>\n</table></td></tr></table></body></html>`;\n}\n"],"mappings":";;;AAqBA,IAAM,KAAM,MAAc,GAAG,KAAK,MAAM,IAAI,EAAE,IAAI,GAAG,IAAI,QAAQ,SAAS,GAAG;AAE7E,SAAgB,EAAW,GAAoB,EAAE,eAAyC,CAAC,GAAe;CACxG,IAAM,EAAE,aAAU,EAAM,CAAM,GACxB,IAAS,EAAW,CAAM,GAC1B,KAAU,MAAkB,EAAM,QAAQ,MAAM,GAAG;CACzD,OAAO;EACL,QAAQ,EAAW,GAAQ;GAAE,QAAQ;GAAS;EAAQ,CAAC;EACvD,OAAO;GAAE,SAAS,EAAO,EAAO,OAAO;GAAG,MAAM,EAAO,EAAO,IAAI;GAAG,MAAM,EAAO,EAAO,IAAI;EAAE;EAC/F,QAAQ,EAAG,WAAW,EAAW,GAAQ,UAAU,WAAW,CAAC,IAAI,CAAK;EACxE,gBAAgB,IAAQ,GAAG,KAAK,MAAM,IAAQ,GAAG,EAAE,KAAK;EACxD,YAAY,MAAa,EAAU,GAAQ,GAAU,GAAG;CAC1D;AACF;AAuBA,SAAgB,EAAkB,GAAoB,GAA+B;CACnF,IAAM,IAAI,EAAW,GAAQ,EAAE,SAAS,EAAE,QAAQ,CAAC,GAC7C,IAAI,EAAE,QACN,IAAI,EAAE,OACN,IAAS,EAAE,SACb,+CAA+C,EAAW,EAAE,OAAO,IAAI,EAAE,2CAA2C,EAAE,OAAO,SAAS,EAAE,aAAa,mBAAmB,EAAE,KAAK,wDAAwD,EAAE,OAAO,KAAK,EAAW,EAAE,OAAO,KAAK,EAAE,yBAChR;CACJ,OAAO;cACK,EAAW,EAAE,QAAQ,IAAI,EAAE;SAChC,EAAW,EAAE,KAAK,EAAE;SACpB,EAAE,WAAW,EAAE,UAAU,EAAE,QAAQ,IAAI,GAAG,UAAU,EAAE,eAAe;6CACjC,EAAE,GAAG,SAAS,EAAE,GAAG;0DACN,EAAW,EAAE,SAAS,EAAE;4FACU,EAAE,GAAG;;;oDAG7C,EAAE,KAAK,uDAAuD,EAAE,YAAY,uEAAuE,EAAE,OAAO,iBAAiB,EAAE,eAAe,mDAAmD,EAAW,EAAE,OAAO,EAAE;sDACrQ,EAAE,QAAQ,SAAS,EAAE,GAAG,KAAK,EAAW,EAAE,KAAK,EAAE;sDACjD,EAAE,KAAK,SAAS,EAAE,GAAG,KAAK,EAAE,KAAK;EACrF,EAAO;uEAC8D,EAAE,KAAK,sCAAsC,EAAE,KAAK,SAAS,EAAE,YAAY,KAAK,EAAE,OAAO;;AAEhK"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type PageTokens } from './theme';
|
|
2
|
+
type Corner = 'tl' | 'tr' | 'br' | 'bl';
|
|
3
|
+
export type MarkIconOptions = {
|
|
4
|
+
/** The mark: its viewBox (crop it to the drawing) and its <path> elements, which are filled in the on-accent color. */
|
|
5
|
+
mark: {
|
|
6
|
+
viewBox: string;
|
|
7
|
+
paths: string;
|
|
8
|
+
};
|
|
9
|
+
/** Where the mark sits on the plate, in the plate's units. Default: centered, half the plate. */
|
|
10
|
+
inset?: {
|
|
11
|
+
x: number;
|
|
12
|
+
y: number;
|
|
13
|
+
width: number;
|
|
14
|
+
height: number;
|
|
15
|
+
};
|
|
16
|
+
/** The plate's size (its viewBox is 0 0 size size). Default 64. */
|
|
17
|
+
size?: number;
|
|
18
|
+
/** The corner size, in the plate's units, before the library's shape rule (cut and round multiples). Default 12. */
|
|
19
|
+
corner?: number;
|
|
20
|
+
/** Which corners get cut, where the library cuts. Default tl and br, as the corner() mixin. */
|
|
21
|
+
corners?: Corner[];
|
|
22
|
+
/** A palette of the library's; the default if it has none by that id. */
|
|
23
|
+
palette?: string | null;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* The site's mark as an app icon or favicon (SVG): an accent plate in the
|
|
27
|
+
* library's corner shape, with the mark in the on-accent color, in the light
|
|
28
|
+
* mood (browser tabs and home screens don't switch).
|
|
29
|
+
*/
|
|
30
|
+
export declare function renderMarkIcon(tokens: PageTokens, { mark, size, inset, corner, corners, palette }: MarkIconOptions): string;
|
|
31
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { roleValues as e, shape as t } from "./theme.js";
|
|
2
|
+
import { escapeHtml as n } from "./status.js";
|
|
3
|
+
//#region src/pages/icon.ts
|
|
4
|
+
var r = (e) => String(Math.round(e * 100) / 100);
|
|
5
|
+
function i(e, t, n, i) {
|
|
6
|
+
let a = e, o = (e) => t > 0 && i.includes(e) ? t : 0, s = (e) => o(e) ? 0 : n, c = `M${r(o("tl") || s("tl"))} 0`;
|
|
7
|
+
return c += o("tr") ? `H${r(a - o("tr"))}L${r(a)} ${r(o("tr"))}` : s("tr") ? `H${r(a - s("tr"))}A${r(s("tr"))} ${r(s("tr"))} 0 0 1 ${r(a)} ${r(s("tr"))}` : `H${r(a)}`, c += o("br") ? `V${r(a - o("br"))}L${r(a - o("br"))} ${r(a)}` : s("br") ? `V${r(a - s("br"))}A${r(s("br"))} ${r(s("br"))} 0 0 1 ${r(a - s("br"))} ${r(a)}` : `V${r(a)}`, c += o("bl") ? `H${r(o("bl"))}L0 ${r(a - o("bl"))}` : s("bl") ? `H${r(s("bl"))}A${r(s("bl"))} ${r(s("bl"))} 0 0 1 0 ${r(a - s("bl"))}` : "H0", c += o("tl") ? `V${r(o("tl"))}` : s("tl") ? `V${r(s("tl"))}A${r(s("tl"))} ${r(s("tl"))} 0 0 1 ${r(s("tl"))} 0` : "V0", `${c}Z`;
|
|
8
|
+
}
|
|
9
|
+
function a(a, { mark: o, size: s = 64, inset: c, corner: l = 12, corners: u = ["tl", "br"], palette: d }) {
|
|
10
|
+
let f = e(a, {
|
|
11
|
+
scheme: "light",
|
|
12
|
+
palette: d
|
|
13
|
+
}), { cut: p, round: m } = t(a), h = c ?? {
|
|
14
|
+
x: s / 4,
|
|
15
|
+
y: s / 4,
|
|
16
|
+
width: s / 2,
|
|
17
|
+
height: s / 2
|
|
18
|
+
}, g = o.paths.replace(/<\/path>/g, "").replace(/<path\b([^>]*?)\/?>/g, (e, t) => `<path fill="${f["on-accent"]}"${t.replace(/\s+fill="[^"]*"/g, "").replace(/\s+$/, "")}/>`);
|
|
19
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${r(s)} ${r(s)}"><path d="${i(s, l * p, l * m, u)}" fill="${f.accent}"/><svg x="${r(h.x)}" y="${r(h.y)}" width="${r(h.width)}" height="${r(h.height)}" viewBox="${n(o.viewBox)}">${g}</svg></svg>`;
|
|
20
|
+
}
|
|
21
|
+
//#endregion
|
|
22
|
+
export { a as renderMarkIcon };
|
|
23
|
+
|
|
24
|
+
//# sourceMappingURL=icon.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"icon.js","names":[],"sources":["../../src/pages/icon.ts"],"sourcesContent":["import { escapeHtml } from './status';\nimport { roleValues, shape, type PageTokens } from './theme';\n\ntype Corner = 'tl' | 'tr' | 'br' | 'bl';\n\nexport type MarkIconOptions = {\n /** The mark: its viewBox (crop it to the drawing) and its <path> elements, which are filled in the on-accent color. */\n mark: { viewBox: string; paths: string };\n /** Where the mark sits on the plate, in the plate's units. Default: centered, half the plate. */\n inset?: { x: number; y: number; width: number; height: number };\n /** The plate's size (its viewBox is 0 0 size size). Default 64. */\n size?: number;\n /** The corner size, in the plate's units, before the library's shape rule (cut and round multiples). Default 12. */\n corner?: number;\n /** Which corners get cut, where the library cuts. Default tl and br, as the corner() mixin. */\n corners?: Corner[];\n /** A palette of the library's; the default if it has none by that id. */\n palette?: string | null;\n};\n\nconst n = (v: number) => String(Math.round(v * 100) / 100);\n\n/** The plate's outline: cut corners where the library cuts, rounded ones where it rounds. */\nfunction platePath(size: number, cut: number, round: number, corners: Corner[]): string {\n const S = size;\n const c = (k: Corner) => (cut > 0 && corners.includes(k) ? cut : 0);\n const r = (k: Corner) => (c(k) ? 0 : round);\n let d = `M${n(c('tl') || r('tl'))} 0`;\n d += c('tr') ? `H${n(S - c('tr'))}L${n(S)} ${n(c('tr'))}` : r('tr') ? `H${n(S - r('tr'))}A${n(r('tr'))} ${n(r('tr'))} 0 0 1 ${n(S)} ${n(r('tr'))}` : `H${n(S)}`;\n d += c('br') ? `V${n(S - c('br'))}L${n(S - c('br'))} ${n(S)}` : r('br') ? `V${n(S - r('br'))}A${n(r('br'))} ${n(r('br'))} 0 0 1 ${n(S - r('br'))} ${n(S)}` : `V${n(S)}`;\n d += c('bl') ? `H${n(c('bl'))}L0 ${n(S - c('bl'))}` : r('bl') ? `H${n(r('bl'))}A${n(r('bl'))} ${n(r('bl'))} 0 0 1 0 ${n(S - r('bl'))}` : 'H0';\n d += c('tl') ? `V${n(c('tl'))}` : r('tl') ? `V${n(r('tl'))}A${n(r('tl'))} ${n(r('tl'))} 0 0 1 ${n(r('tl'))} 0` : 'V0';\n return `${d}Z`;\n}\n\n/**\n * The site's mark as an app icon or favicon (SVG): an accent plate in the\n * library's corner shape, with the mark in the on-accent color, in the light\n * mood (browser tabs and home screens don't switch).\n */\nexport function renderMarkIcon(tokens: PageTokens, { mark, size = 64, inset, corner = 12, corners = ['tl', 'br'], palette }: MarkIconOptions): string {\n const colors = roleValues(tokens, { scheme: 'light', palette });\n const { cut, round } = shape(tokens);\n const box = inset ?? { x: size / 4, y: size / 4, width: size / 2, height: size / 2 };\n const paths = mark.paths.replace(/<\\/path>/g, '').replace(/<path\\b([^>]*?)\\/?>/g, (_, attrs: string) => `<path fill=\"${colors['on-accent']}\"${attrs.replace(/\\s+fill=\"[^\"]*\"/g, '').replace(/\\s+$/, '')}/>`);\n return (\n `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 ${n(size)} ${n(size)}\">` +\n `<path d=\"${platePath(size, corner * cut, corner * round, corners)}\" fill=\"${colors.accent}\"/>` +\n `<svg x=\"${n(box.x)}\" y=\"${n(box.y)}\" width=\"${n(box.width)}\" height=\"${n(box.height)}\" viewBox=\"${escapeHtml(mark.viewBox)}\">${paths}</svg>` +\n `</svg>`\n );\n}\n"],"mappings":";;;AAoBA,IAAM,KAAK,MAAc,OAAO,KAAK,MAAM,IAAI,GAAG,IAAI,GAAG;AAGzD,SAAS,EAAU,GAAc,GAAa,GAAe,GAA2B;CACtF,IAAM,IAAI,GACJ,KAAK,MAAe,IAAM,KAAK,EAAQ,SAAS,CAAC,IAAI,IAAM,GAC3D,KAAK,MAAe,EAAE,CAAC,IAAI,IAAI,GACjC,IAAI,IAAI,EAAE,EAAE,IAAI,KAAK,EAAE,IAAI,CAAC,EAAE;CAKlC,OAJA,KAAK,EAAE,IAAI,IAAI,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,EAAE,IAAI,CAAC,MAAM,EAAE,IAAI,IAAI,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,GAAG,EAAE,EAAE,IAAI,CAAC,EAAE,GAAG,EAAE,EAAE,IAAI,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,GAAG,EAAE,EAAE,IAAI,CAAC,MAAM,IAAI,EAAE,CAAC,KAC5J,KAAK,EAAE,IAAI,IAAI,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,GAAG,EAAE,CAAC,MAAM,EAAE,IAAI,IAAI,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,GAAG,EAAE,EAAE,IAAI,CAAC,EAAE,GAAG,EAAE,EAAE,IAAI,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,GAAG,EAAE,CAAC,MAAM,IAAI,EAAE,CAAC,KACpK,KAAK,EAAE,IAAI,IAAI,IAAI,EAAE,EAAE,IAAI,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,CAAC,MAAM,EAAE,IAAI,IAAI,IAAI,EAAE,EAAE,IAAI,CAAC,EAAE,GAAG,EAAE,EAAE,IAAI,CAAC,EAAE,GAAG,EAAE,EAAE,IAAI,CAAC,EAAE,WAAW,EAAE,IAAI,EAAE,IAAI,CAAC,MAAM,MACzI,KAAK,EAAE,IAAI,IAAI,IAAI,EAAE,EAAE,IAAI,CAAC,MAAM,EAAE,IAAI,IAAI,IAAI,EAAE,EAAE,IAAI,CAAC,EAAE,GAAG,EAAE,EAAE,IAAI,CAAC,EAAE,GAAG,EAAE,EAAE,IAAI,CAAC,EAAE,SAAS,EAAE,EAAE,IAAI,CAAC,EAAE,MAAM,MAC1G,GAAG,EAAE;AACd;AAOA,SAAgB,EAAe,GAAoB,EAAE,SAAM,UAAO,IAAI,UAAO,YAAS,IAAI,aAAU,CAAC,MAAM,IAAI,GAAG,cAAoC;CACpJ,IAAM,IAAS,EAAW,GAAQ;EAAE,QAAQ;EAAS;CAAQ,CAAC,GACxD,EAAE,QAAK,aAAU,EAAM,CAAM,GAC7B,IAAM,KAAS;EAAE,GAAG,IAAO;EAAG,GAAG,IAAO;EAAG,OAAO,IAAO;EAAG,QAAQ,IAAO;CAAE,GAC7E,IAAQ,EAAK,MAAM,QAAQ,aAAa,EAAE,EAAE,QAAQ,yBAAyB,GAAG,MAAkB,eAAe,EAAO,aAAa,GAAG,EAAM,QAAQ,oBAAoB,EAAE,EAAE,QAAQ,QAAQ,EAAE,EAAE,GAAG;CAC3M,OACE,wDAAwD,EAAE,CAAI,EAAE,GAAG,EAAE,CAAI,EAAE,aAC/D,EAAU,GAAM,IAAS,GAAK,IAAS,GAAO,CAAO,EAAE,UAAU,EAAO,OAAO,aAChF,EAAE,EAAI,CAAC,EAAE,OAAO,EAAE,EAAI,CAAC,EAAE,WAAW,EAAE,EAAI,KAAK,EAAE,YAAY,EAAE,EAAI,MAAM,EAAE,aAAa,EAAW,EAAK,OAAO,EAAE,IAAI,EAAM;AAG1I"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
//#region src/pages/page.scss?inline
|
|
2
|
+
var e = "*,:before,:after{box-sizing:border-box}html{-webkit-text-size-adjust:100%;-moz-text-size-adjust:100%;text-size-adjust:100%}body{background:var(--r9-bg);color:var(--r9-fg);font-family:var(--r9-type-body-font-family);font-weight:var(--r9-type-body-font-weight);font-size:var(--r9-type-body-font-size);line-height:var(--r9-type-body-line-height);letter-spacing:var(--r9-type-body-letter-spacing);-webkit-font-smoothing:antialiased;text-rendering:optimizelegibility;margin:0}h1,h2,h3,h4,h5,h6{font-family:var(--r9-font-display);text-wrap:balance;margin:0}p{margin:0}img,svg,video,canvas{max-width:100%}code,kbd,samp,pre{font-family:var(--r9-font-mono)}a{color:inherit}:focus-visible{outline:2px solid var(--r9-accent);outline-offset:3px}@keyframes r9-reveal{0%{opacity:0;transform:translateY(14px)}to{opacity:1;transform:none}}@keyframes r9-pulse{0%,to{opacity:1}50%{opacity:.25}}@keyframes r9-blink{50%{opacity:0}}@keyframes r9-sheen{0%{background-position:120% 0}45%,to{background-position:-30% 0}}:root[data-motion=reduced] *,:root[data-motion=reduced] :before,:root[data-motion=reduced] :after{scroll-behavior:auto!important;transition-duration:.01ms!important;animation-duration:.01ms!important;animation-iteration-count:1!important}@media (prefers-reduced-motion:reduce){*,:before,:after{scroll-behavior:auto!important;transition-duration:.01ms!important;animation-duration:.01ms!important;animation-iteration-count:1!important}}body{background-image:radial-gradient(var(--r9-dot) 1px, transparent 1.5px);background-size:22px 22px;grid-template-rows:auto 1fr;min-height:100dvh;display:grid}body:after{content:\"\";pointer-events:none;background-image:repeating-linear-gradient(0deg, var(--r9-scan) 0 1px, transparent 1px 3px);position:fixed;inset:0}::selection{background:var(--r9-accent-soft)}a{color:var(--r9-accent-text);text-underline-offset:3px}.hud{justify-content:space-between;align-items:center;gap:var(--r9-space-4);padding:max(16px, env(safe-area-inset-top)) max(16px, env(safe-area-inset-right)) 0 max(16px, env(safe-area-inset-left));display:flex}.brand{color:var(--r9-fg);align-items:center;gap:10px;display:inline-flex}.brand svg{width:28px;height:28px}.brand span{font-family:var(--r9-type-wordmark-font-family);font-weight:var(--r9-type-wordmark-font-weight);font-size:var(--r9-type-wordmark-font-size);line-height:var(--r9-type-wordmark-line-height);letter-spacing:var(--r9-type-wordmark-letter-spacing);text-transform:uppercase}.sys{gap:var(--r9-space-2);font:400 12px var(--r9-font-mono);color:var(--r9-fg-muted);font-variant-numeric:tabular-nums;align-items:center;display:inline-flex}.pulse{background:var(--r9-signal);border-radius:calc(var(--r9-shape-round) * 100%);width:6px;height:6px;animation:2.4s ease-in-out infinite r9-pulse}main{padding:32px max(16px, env(safe-area-inset-right)) 48px max(16px, env(safe-area-inset-left));place-items:center;display:grid}.panel{background:var(--r9-glass);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);width:100%;max-width:36rem;box-shadow:inset 0 0 0 var(--r9-border-hair) var(--r9-line);clip-path:polygon(0px 0, calc(100% - calc(var(--r9-corner-lg) * var(--r9-shape-cut))) 0, 100% calc(var(--r9-corner-lg) * var(--r9-shape-cut)), 100% calc(100% - 0px), calc(100% - 0px) 100%, 0px 100%, 0 calc(100% - 0px), 0 0px);border-radius:calc(var(--r9-corner-lg) * var(--r9-shape-round));flex-direction:column;gap:18px;padding:32px;display:flex;position:relative}.panel:after{content:\"\";width:var(--r9-corner-lg);height:var(--r9-corner-lg);background:var(--r9-accent);opacity:var(--r9-shape-cut);pointer-events:none;clip-path:polygon(0 0,100% 100%,calc(100% - 4px) 100%,0 4px);position:absolute;top:0;right:0}.head{font:400 11px var(--r9-font-mono);letter-spacing:.06em;flex-wrap:wrap;align-items:center;gap:10px;display:flex}.chip{background:var(--r9-inverse-bg);color:var(--r9-inverse-fg);text-transform:uppercase;border-radius:calc(var(--r9-corner-sm) * var(--r9-shape-round));padding:3px 8px}.code{color:var(--r9-fg-subtle)}h1{font-family:var(--r9-type-h1-font-family);font-weight:var(--r9-type-h1-font-weight);font-size:var(--r9-type-h1-font-size);line-height:var(--r9-type-h1-line-height);letter-spacing:var(--r9-type-h1-letter-spacing)}p{color:var(--r9-fg-muted);max-width:62ch}.term{background:var(--r9-surface);box-shadow:inset 0 0 0 var(--r9-border-hair) var(--r9-line);border-radius:calc(var(--r9-corner-sm) * var(--r9-shape-round));font:400 14px/1.75 var(--r9-font-mono);color:var(--r9-fg-muted);white-space:pre-wrap;overflow-wrap:anywhere;margin:4px 0 0;padding:12px 14px}.term b{color:var(--r9-accent-text);font-weight:400}.term .cmd{color:var(--r9-fg)}.term .ok{color:var(--r9-signal-text);align-items:center;gap:8px;display:inline-flex}.term .ok:before{content:\"\";background:var(--r9-signal);border-radius:50%;width:6px;height:6px}.term .err{color:var(--r9-accent-text)}.caret{vertical-align:-.2em;background:var(--r9-accent);width:.55em;height:1.1em;animation:1.1s step-end infinite r9-blink;display:inline-block}.note{font:400 12px var(--r9-font-mono);color:var(--r9-fg-subtle)}@media (width<=480px){.panel{padding:24px 20px}.brand span{display:none}}@media (prefers-reduced-motion:reduce){.pulse,.caret{animation:none}}:root[data-motion=reduced] .pulse,:root[data-motion=reduced] .caret{animation:none}";
|
|
3
|
+
//#endregion
|
|
4
|
+
export { e as default };
|
|
5
|
+
|
|
6
|
+
//# sourceMappingURL=page.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"page.js","names":[],"sources":["../../src/pages/page.scss?inline"],"sourcesContent":["// The status page's styles (@rnine/core/pages statusPage), compiled once in\n// core: every value is a token's custom property, so the same CSS takes each\n// library's look from its tokens.css (inlined before it). Global CSS, not a\n// module: keyframes by their bare names (r9-pulse).\n@use \"../../scss/base\";\n@use \"../../scss/index\" as r9;\n\nbody {\n min-height: 100vh;\n min-height: 100dvh;\n display: grid;\n grid-template-rows: auto 1fr;\n @include r9.texture(field, 22px);\n}\n\n// The library's scanlines (transparent where it has none)\nbody::after {\n content: \"\";\n position: fixed;\n inset: 0;\n pointer-events: none;\n @include r9.texture(lines);\n}\n\n::selection { background: r9.$accent-soft; }\na { color: r9.$accent-text; text-underline-offset: 3px; }\n\n// Header: the site's mark and name, a live clock\n.hud {\n display: flex;\n justify-content: space-between;\n align-items: center;\n gap: r9.$space-4;\n padding: max(16px, env(safe-area-inset-top)) max(16px, env(safe-area-inset-right)) 0 max(16px, env(safe-area-inset-left));\n}\n.brand { display: inline-flex; align-items: center; gap: 10px; color: r9.$fg; }\n.brand svg { width: 28px; height: 28px; }\n.brand span { @include r9.type(wordmark); }\n.sys {\n display: inline-flex;\n gap: r9.$space-2;\n align-items: center;\n font: 400 12px r9.$font-mono;\n color: r9.$fg-muted;\n font-variant-numeric: tabular-nums;\n}\n.pulse {\n width: 6px;\n height: 6px;\n background: r9.$signal;\n border-radius: calc(#{r9.$shape-round} * 100%);\n animation: r9-pulse 2.4s ease-in-out infinite;\n}\n\nmain {\n display: grid;\n place-items: center;\n padding: 32px max(16px, env(safe-area-inset-right)) 48px max(16px, env(safe-area-inset-left));\n}\n\n// The panel: the library's card corner and its corner ornament\n.panel {\n position: relative;\n width: 100%;\n max-width: 36rem;\n display: flex;\n flex-direction: column;\n gap: 18px;\n padding: 32px;\n @include r9.glass;\n @include r9.hairline;\n @include r9.corner(lg, tr);\n}\n.panel::after { @include r9.corner-ornament(lg); }\n\n.head {\n display: flex;\n gap: 10px;\n align-items: center;\n flex-wrap: wrap;\n font: 400 11px r9.$font-mono;\n letter-spacing: .06em;\n}\n.chip {\n padding: 3px 8px;\n background: r9.$inverse-bg;\n color: r9.$inverse-fg;\n text-transform: uppercase;\n border-radius: calc(#{r9.$corner-sm} * #{r9.$shape-round});\n}\n.code { color: r9.$fg-subtle; }\n\nh1 { @include r9.type(h1); }\np { color: r9.$fg-muted; max-width: 62ch; }\n\n// The status line: a command and its answer\n.term {\n margin: 4px 0 0;\n padding: 12px 14px;\n background: r9.$surface;\n @include r9.hairline;\n border-radius: calc(#{r9.$corner-sm} * #{r9.$shape-round});\n font: 400 14px/1.75 r9.$font-mono;\n color: r9.$fg-muted;\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n}\n.term b { color: r9.$accent-text; font-weight: 400; }\n.term .cmd { color: r9.$fg; }\n.term .ok { color: r9.$signal-text; display: inline-flex; gap: 8px; align-items: center; }\n.term .ok::before { content: \"\"; width: 6px; height: 6px; border-radius: 50%; background: r9.$signal; }\n.term .err { color: r9.$accent-text; }\n.caret {\n display: inline-block;\n width: .55em;\n height: 1.1em;\n vertical-align: -.2em;\n background: r9.$accent;\n animation: r9-blink 1.1s steps(1) infinite;\n}\n.note { font: 400 12px r9.$font-mono; color: r9.$fg-subtle; }\n\n@media (max-width: 480px) {\n .panel { padding: 24px 20px; }\n .brand span { display: none; }\n}\n@media (prefers-reduced-motion: reduce) {\n .pulse, .caret { animation: none; }\n}\n:root[data-motion=\"reduced\"] .pulse,\n:root[data-motion=\"reduced\"] .caret { animation: none; }\n"],"mappings":""}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { type BootScriptOptions } from '../hooks/moodBoot';
|
|
2
|
+
import { type PageTheme, type PageTokens } from './theme';
|
|
3
|
+
/** Escapes text for HTML (element content and attribute values). */
|
|
4
|
+
export declare const escapeHtml: (text: unknown) => string;
|
|
5
|
+
export type StatusPageOptions = {
|
|
6
|
+
/** Content, as HTML (escape anything you didn't write: escapeHtml). */
|
|
7
|
+
title: string;
|
|
8
|
+
/** The panel's label ("Maintenance") */
|
|
9
|
+
label: string;
|
|
10
|
+
/** Small text next to the label ("HTTP 503") */
|
|
11
|
+
code?: string;
|
|
12
|
+
heading: string;
|
|
13
|
+
paragraphs: string[];
|
|
14
|
+
/**
|
|
15
|
+
* The status line, written like a command and its answer: "> status
|
|
16
|
+
* example.com" / "updating". tone ok (default): a live signal dot, it's
|
|
17
|
+
* being handled; error: something's wrong with the request itself.
|
|
18
|
+
*/
|
|
19
|
+
status: {
|
|
20
|
+
command?: string;
|
|
21
|
+
text: string;
|
|
22
|
+
tone?: 'ok' | 'error';
|
|
23
|
+
};
|
|
24
|
+
/** Small text at the bottom */
|
|
25
|
+
note?: string;
|
|
26
|
+
/** Plain text from here on (escaped for you), except mark and the extra head/script. */
|
|
27
|
+
/** The site's name (read by screen readers, shown in the header) and its mark, as inline SVG drawn in currentColor. */
|
|
28
|
+
brand?: {
|
|
29
|
+
name: string;
|
|
30
|
+
mark?: string;
|
|
31
|
+
};
|
|
32
|
+
/** The status line's prompt. Default ">". */
|
|
33
|
+
prompt?: string;
|
|
34
|
+
/** The favicon's address (a data: URL works: markIcon). */
|
|
35
|
+
icon?: string;
|
|
36
|
+
/** Where the page fonts are served (pageFonts), ending in "/". None: the fonts' fallbacks. */
|
|
37
|
+
fontsUrl?: string;
|
|
38
|
+
/** Where the visitor's mood and motion choices are kept (as in ThemeProvider's mood). */
|
|
39
|
+
mood?: Pick<BootScriptOptions, 'storageKey' | 'motionStorageKey' | 'auto'>;
|
|
40
|
+
/** A palette of the library's (data-palette); the default if it has none by that id. */
|
|
41
|
+
palette?: string | null;
|
|
42
|
+
/** The page's language. Default "en". */
|
|
43
|
+
lang?: string;
|
|
44
|
+
/** Seconds: check the address this often and reload once it answers below 500 (it's back). For pages that answer 5xx. */
|
|
45
|
+
retry?: number;
|
|
46
|
+
/** Extra tags for <head> and inline JS for the end of <body>, as given. */
|
|
47
|
+
head?: string;
|
|
48
|
+
script?: string;
|
|
49
|
+
};
|
|
50
|
+
/** <meta name="theme-color"> for both moods (the page background). */
|
|
51
|
+
export declare function themeColorMeta(tokens: PageTokens, { palette }?: {
|
|
52
|
+
palette?: string | null;
|
|
53
|
+
}): string;
|
|
54
|
+
/**
|
|
55
|
+
* A complete, self-contained status page (maintenance, coming soon, an
|
|
56
|
+
* error): one HTML document with its styles and scripts inline, in the
|
|
57
|
+
* library's look and both its moods. Nothing loads from elsewhere except the
|
|
58
|
+
* page fonts (fontsUrl), so it works while the rest of the site is down.
|
|
59
|
+
*/
|
|
60
|
+
export declare function renderStatusPage(theme: PageTheme, o: StatusPageOptions): string;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { bootScript as e } from "../hooks/moodBoot.js";
|
|
2
|
+
import { fontFaces as t, fontFor as n, paletteId as r, roleValues as i } from "./theme.js";
|
|
3
|
+
//#region src/pages/status.ts
|
|
4
|
+
var a = (e) => String(e).replace(/[&<>"']/g, (e) => ({
|
|
5
|
+
"&": "&",
|
|
6
|
+
"<": "<",
|
|
7
|
+
">": ">",
|
|
8
|
+
"\"": """,
|
|
9
|
+
"'": "'"
|
|
10
|
+
})[e]);
|
|
11
|
+
function o(e, { palette: t } = {}) {
|
|
12
|
+
return ["light", "dark"].map((n) => `<meta name="theme-color" content="${i(e, {
|
|
13
|
+
scheme: n,
|
|
14
|
+
palette: t
|
|
15
|
+
}).bg}" media="(prefers-color-scheme: ${n})">`).join("\n");
|
|
16
|
+
}
|
|
17
|
+
var s = "(function(){var c=document.getElementById('clock');if(!c)return;function t(){var d=new Date();c.textContent='SYS '+[d.getHours(),d.getMinutes(),d.getSeconds()].map(function(n){return('0'+n).slice(-2)}).join(':')}t();setInterval(t,1000)})();", c = (e) => `setInterval(function(){fetch(location.href,{method:'HEAD',cache:'no-store',redirect:'manual'}).then(function(r){if(r.status<500)location.reload()}).catch(function(){})},${Math.round(e * 1e3)});`;
|
|
18
|
+
function l(i, l) {
|
|
19
|
+
let { tokens: u } = i, d = r(u, l.palette), f = a(l.prompt ?? ">"), p = l.fontsUrl ? t(u, a(l.fontsUrl)) : "", m = l.fontsUrl ? n(u, "h1") : void 0, h = l.brand ? `<span class="brand" aria-label="${a(l.brand.name)}">${l.brand.mark ?? ""}<span aria-hidden="true">${a(l.brand.name)}</span></span>` : "<span></span>", g = l.status.command ? ` <span class="cmd">${l.status.command}</span>` : "";
|
|
20
|
+
return `<!doctype html>
|
|
21
|
+
<html lang="${a(l.lang ?? "en")}"${d ? ` data-palette="${d}"` : ""}>
|
|
22
|
+
<head>
|
|
23
|
+
<meta charset="utf-8">
|
|
24
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
25
|
+
<meta name="robots" content="noindex">
|
|
26
|
+
${o(u, { palette: d })}
|
|
27
|
+
<script>${e({
|
|
28
|
+
auto: u.auto,
|
|
29
|
+
names: u.moods,
|
|
30
|
+
...l.mood
|
|
31
|
+
})}<\/script>
|
|
32
|
+
<title>${l.title}</title>
|
|
33
|
+
${l.head ?? ""}${l.icon ? `<link rel="icon" type="image/svg+xml" href="${a(l.icon)}">\n` : ""}${m ? `<link rel="preload" href="${a(l.fontsUrl)}${m.id}.woff2" as="font" type="font/woff2" crossorigin>\n` : ""}<style>${p}${i.css}</style>
|
|
34
|
+
</head>
|
|
35
|
+
<body>
|
|
36
|
+
<header class="hud">
|
|
37
|
+
${h}
|
|
38
|
+
<span class="sys"><span class="pulse" aria-hidden="true"></span><span id="clock">SYS --:--:--</span></span>
|
|
39
|
+
</header>
|
|
40
|
+
<main>
|
|
41
|
+
<section class="panel" aria-labelledby="title">
|
|
42
|
+
<div class="head"><span class="chip">${l.label}</span>${l.code ? `<span class="code">${l.code}</span>` : ""}</div>
|
|
43
|
+
<h1 id="title" data-reveal>${l.heading}</h1>
|
|
44
|
+
${l.paragraphs.map((e) => ` <p>${e}</p>`).join("\n")}
|
|
45
|
+
<div class="term" role="status"><b>${f}</b>${g}
|
|
46
|
+
<span class="${l.status.tone === "error" ? "err" : "ok"}">${l.status.text}</span> <span class="caret" aria-hidden="true"></span></div>
|
|
47
|
+
${l.note ? ` <p class="note">${l.note}</p>\n` : ""} </section>
|
|
48
|
+
</main>
|
|
49
|
+
<script>${s}${i.reveal ?? ""}${l.retry ? c(l.retry) : ""}${l.script ?? ""}<\/script>
|
|
50
|
+
</body>
|
|
51
|
+
</html>
|
|
52
|
+
`;
|
|
53
|
+
}
|
|
54
|
+
//#endregion
|
|
55
|
+
export { a as escapeHtml, l as renderStatusPage, o as themeColorMeta };
|
|
56
|
+
|
|
57
|
+
//# sourceMappingURL=status.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"status.js","names":[],"sources":["../../src/pages/status.ts"],"sourcesContent":["import { bootScript, type BootScriptOptions } from '../hooks/moodBoot';\nimport { fontFaces, fontFor, paletteId, roleValues, type PageTheme, type PageTokens } from './theme';\n\n/** Escapes text for HTML (element content and attribute values). */\nexport const escapeHtml = (text: unknown): string =>\n String(text).replace(/[&<>\"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '\"': '"', \"'\": ''' })[c]!);\n\nexport type StatusPageOptions = {\n /** Content, as HTML (escape anything you didn't write: escapeHtml). */\n title: string;\n /** The panel's label (\"Maintenance\") */\n label: string;\n /** Small text next to the label (\"HTTP 503\") */\n code?: string;\n heading: string;\n paragraphs: string[];\n /**\n * The status line, written like a command and its answer: \"> status\n * example.com\" / \"updating\". tone ok (default): a live signal dot, it's\n * being handled; error: something's wrong with the request itself.\n */\n status: { command?: string; text: string; tone?: 'ok' | 'error' };\n /** Small text at the bottom */\n note?: string;\n\n /** Plain text from here on (escaped for you), except mark and the extra head/script. */\n /** The site's name (read by screen readers, shown in the header) and its mark, as inline SVG drawn in currentColor. */\n brand?: { name: string; mark?: string };\n /** The status line's prompt. Default \">\". */\n prompt?: string;\n /** The favicon's address (a data: URL works: markIcon). */\n icon?: string;\n /** Where the page fonts are served (pageFonts), ending in \"/\". None: the fonts' fallbacks. */\n fontsUrl?: string;\n /** Where the visitor's mood and motion choices are kept (as in ThemeProvider's mood). */\n mood?: Pick<BootScriptOptions, 'storageKey' | 'motionStorageKey' | 'auto'>;\n /** A palette of the library's (data-palette); the default if it has none by that id. */\n palette?: string | null;\n /** The page's language. Default \"en\". */\n lang?: string;\n /** Seconds: check the address this often and reload once it answers below 500 (it's back). For pages that answer 5xx. */\n retry?: number;\n /** Extra tags for <head> and inline JS for the end of <body>, as given. */\n head?: string;\n script?: string;\n};\n\n/** <meta name=\"theme-color\"> for both moods (the page background). */\nexport function themeColorMeta(tokens: PageTokens, { palette }: { palette?: string | null } = {}): string {\n return (['light', 'dark'] as const)\n .map((scheme) => `<meta name=\"theme-color\" content=\"${roleValues(tokens, { scheme, palette }).bg}\" media=\"(prefers-color-scheme: ${scheme})\">`)\n .join('\\n');\n}\n\n// The header clock (as LiveClock: \"SYS 17:04:33\")\nconst CLOCK = `(function(){var c=document.getElementById('clock');if(!c)return;function t(){var d=new Date();c.textContent='SYS '+[d.getHours(),d.getMinutes(),d.getSeconds()].map(function(n){return('0'+n).slice(-2)}).join(':')}t();setInterval(t,1000)})();`;\n\nconst reloadWhenBack = (seconds: number) =>\n `setInterval(function(){fetch(location.href,{method:'HEAD',cache:'no-store',redirect:'manual'}).then(function(r){if(r.status<500)location.reload()}).catch(function(){})},${Math.round(seconds * 1000)});`;\n\n/**\n * A complete, self-contained status page (maintenance, coming soon, an\n * error): one HTML document with its styles and scripts inline, in the\n * library's look and both its moods. Nothing loads from elsewhere except the\n * page fonts (fontsUrl), so it works while the rest of the site is down.\n */\nexport function renderStatusPage(theme: PageTheme, o: StatusPageOptions): string {\n const { tokens } = theme;\n const palette = paletteId(tokens, o.palette);\n const prompt = escapeHtml(o.prompt ?? '>');\n const fonts = o.fontsUrl ? fontFaces(tokens, escapeHtml(o.fontsUrl)) : '';\n const titleFont = o.fontsUrl ? fontFor(tokens, 'h1') : undefined;\n const brand = o.brand\n ? `<span class=\"brand\" aria-label=\"${escapeHtml(o.brand.name)}\">${o.brand.mark ?? ''}<span aria-hidden=\"true\">${escapeHtml(o.brand.name)}</span></span>`\n : '<span></span>';\n const command = o.status.command ? ` <span class=\"cmd\">${o.status.command}</span>` : '';\n return `<!doctype html>\n<html lang=\"${escapeHtml(o.lang ?? 'en')}\"${palette ? ` data-palette=\"${palette}\"` : ''}>\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1, viewport-fit=cover\">\n<meta name=\"robots\" content=\"noindex\">\n${themeColorMeta(tokens, { palette })}\n<script>${bootScript({ auto: tokens.auto, names: tokens.moods, ...o.mood })}</script>\n<title>${o.title}</title>\n${o.head ?? ''}${o.icon ? `<link rel=\"icon\" type=\"image/svg+xml\" href=\"${escapeHtml(o.icon)}\">\\n` : ''}${titleFont ? `<link rel=\"preload\" href=\"${escapeHtml(o.fontsUrl)}${titleFont.id}.woff2\" as=\"font\" type=\"font/woff2\" crossorigin>\\n` : ''}<style>${fonts}${theme.css}</style>\n</head>\n<body>\n<header class=\"hud\">\n ${brand}\n <span class=\"sys\"><span class=\"pulse\" aria-hidden=\"true\"></span><span id=\"clock\">SYS --:--:--</span></span>\n</header>\n<main>\n <section class=\"panel\" aria-labelledby=\"title\">\n <div class=\"head\"><span class=\"chip\">${o.label}</span>${o.code ? `<span class=\"code\">${o.code}</span>` : ''}</div>\n <h1 id=\"title\" data-reveal>${o.heading}</h1>\n${o.paragraphs.map((p) => ` <p>${p}</p>`).join('\\n')}\n <div class=\"term\" role=\"status\"><b>${prompt}</b>${command}\n<span class=\"${o.status.tone === 'error' ? 'err' : 'ok'}\">${o.status.text}</span> <span class=\"caret\" aria-hidden=\"true\"></span></div>\n${o.note ? ` <p class=\"note\">${o.note}</p>\\n` : ''} </section>\n</main>\n<script>${CLOCK}${theme.reveal ?? ''}${o.retry ? reloadWhenBack(o.retry) : ''}${o.script ?? ''}</script>\n</body>\n</html>\n`;\n}\n"],"mappings":";;;AAIA,IAAa,KAAc,MACzB,OAAO,CAAI,EAAE,QAAQ,aAAa,OAAO;CAAE,KAAK;CAAS,KAAK;CAAQ,KAAK;CAAQ,MAAK;CAAU,KAAK;AAAQ,GAAG,EAAG;AA2CvH,SAAgB,EAAe,GAAoB,EAAE,eAAyC,CAAC,GAAW;CACxG,OAAQ,CAAC,SAAS,MAAM,EACrB,KAAK,MAAW,qCAAqC,EAAW,GAAQ;EAAE;EAAQ;CAAQ,CAAC,EAAE,GAAG,kCAAkC,EAAO,IAAI,EAC7I,KAAK,IAAI;AACd;AAGA,IAAM,IAAQ,oPAER,KAAkB,MACtB,4KAA4K,KAAK,MAAM,IAAU,GAAI,EAAE;AAQzM,SAAgB,EAAiB,GAAkB,GAA8B;CAC/E,IAAM,EAAE,cAAW,GACb,IAAU,EAAU,GAAQ,EAAE,OAAO,GACrC,IAAS,EAAW,EAAE,UAAU,GAAG,GACnC,IAAQ,EAAE,WAAW,EAAU,GAAQ,EAAW,EAAE,QAAQ,CAAC,IAAI,IACjE,IAAY,EAAE,WAAW,EAAQ,GAAQ,IAAI,IAAI,KAAA,GACjD,IAAQ,EAAE,QACZ,mCAAmC,EAAW,EAAE,MAAM,IAAI,EAAE,IAAI,EAAE,MAAM,QAAQ,GAAG,2BAA2B,EAAW,EAAE,MAAM,IAAI,EAAE,kBACvI,iBACE,IAAU,EAAE,OAAO,UAAU,sBAAsB,EAAE,OAAO,QAAQ,WAAW;CACrF,OAAO;cACK,EAAW,EAAE,QAAQ,IAAI,EAAE,GAAG,IAAU,kBAAkB,EAAQ,KAAK,GAAG;;;;;EAKtF,EAAe,GAAQ,EAAE,WAAQ,CAAC,EAAE;UAC5B,EAAW;EAAE,MAAM,EAAO;EAAM,OAAO,EAAO;EAAO,GAAG,EAAE;CAAK,CAAC,EAAE;SACnE,EAAE,MAAM;EACf,EAAE,QAAQ,KAAK,EAAE,OAAO,+CAA+C,EAAW,EAAE,IAAI,EAAE,QAAQ,KAAK,IAAY,6BAA6B,EAAW,EAAE,QAAQ,IAAI,EAAU,GAAG,sDAAsD,GAAG,SAAS,IAAQ,EAAM,IAAI;;;;IAIxQ,EAAM;;;;;2CAKiC,EAAE,MAAM,SAAS,EAAE,OAAO,sBAAsB,EAAE,KAAK,WAAW,GAAG;iCAC/E,EAAE,QAAQ;EACzC,EAAE,WAAW,KAAK,MAAM,UAAU,EAAE,KAAK,EAAE,KAAK,IAAI,EAAE;yCACf,EAAO,MAAM,EAAQ;eAC/C,EAAE,OAAO,SAAS,UAAU,QAAQ,KAAK,IAAI,EAAE,OAAO,KAAK;EACxE,EAAE,OAAO,uBAAuB,EAAE,KAAK,UAAU,GAAG;;UAE5C,IAAQ,EAAM,UAAU,KAAK,EAAE,QAAQ,EAAe,EAAE,KAAK,IAAI,KAAK,EAAE,UAAU,GAAG;;;;AAI/F"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { AutoRule, MoodNames, Scheme } from '../hooks/moodBoot';
|
|
2
|
+
export type TokenEntry = {
|
|
3
|
+
name: string;
|
|
4
|
+
cssVar: string;
|
|
5
|
+
value: string | number;
|
|
6
|
+
type?: string;
|
|
7
|
+
ref?: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
};
|
|
10
|
+
/** A font file static pages and emails serve themselves (theme.json "fonts"). */
|
|
11
|
+
export type PageFont = {
|
|
12
|
+
/** The name it's served as: <fontsUrl><id>.woff2 */
|
|
13
|
+
id: string;
|
|
14
|
+
family: string;
|
|
15
|
+
/** A weight or a range for a variable font ("100 900") */
|
|
16
|
+
weight: string;
|
|
17
|
+
style: string;
|
|
18
|
+
/** The woff2 file, as a package path (e.g. @fontsource/x/files/x-latin-400-normal.woff2) */
|
|
19
|
+
src: string;
|
|
20
|
+
};
|
|
21
|
+
/** The parts of a library's built tokens (tokens.js) static pages use. */
|
|
22
|
+
export type PageTokens = {
|
|
23
|
+
name: string;
|
|
24
|
+
moods: MoodNames;
|
|
25
|
+
auto: AutoRule;
|
|
26
|
+
groups: Record<string, readonly TokenEntry[]>;
|
|
27
|
+
schemes: Record<Scheme, readonly TokenEntry[]>;
|
|
28
|
+
typeRoles: readonly {
|
|
29
|
+
name: string;
|
|
30
|
+
fontFamily: string;
|
|
31
|
+
fontWeight: string | number;
|
|
32
|
+
}[];
|
|
33
|
+
palettes?: readonly {
|
|
34
|
+
id: string;
|
|
35
|
+
label: string;
|
|
36
|
+
default?: boolean;
|
|
37
|
+
schemes: Record<Scheme, readonly TokenEntry[]>;
|
|
38
|
+
}[];
|
|
39
|
+
fonts?: readonly PageFont[];
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* A library, as static pages see it: its tokens, its stylesheet for them
|
|
43
|
+
* (tokens.css plus core's page styles, then anything of its own), and the
|
|
44
|
+
* inline script of its title effect (the TextReveal role; none: the title
|
|
45
|
+
* shows as it is).
|
|
46
|
+
*/
|
|
47
|
+
export type PageTheme = {
|
|
48
|
+
tokens: PageTokens;
|
|
49
|
+
/** The CSS every static page inlines. */
|
|
50
|
+
css: string;
|
|
51
|
+
/** Inline JS run on status pages: animates h1[data-reveal]. It must check reduced motion itself. */
|
|
52
|
+
reveal?: string;
|
|
53
|
+
};
|
|
54
|
+
/** The palette id to use: the one asked for if the library has it, else none (the default). */
|
|
55
|
+
export declare function paletteId(tokens: PageTokens, palette?: string | null): string | undefined;
|
|
56
|
+
/** Every mood role's value in one scheme and palette: { bg: '#FFFFFF', accent: ..., ... } */
|
|
57
|
+
export declare function roleValues(tokens: PageTokens, { scheme, palette }?: {
|
|
58
|
+
scheme?: Scheme;
|
|
59
|
+
palette?: string | null;
|
|
60
|
+
}): Record<string, string>;
|
|
61
|
+
/** A token's value by group and name (e.g. 'shape', 'shape-cut'). */
|
|
62
|
+
export declare function tokenValue(tokens: PageTokens, group: string, name: string): string;
|
|
63
|
+
/** The library's corner rule: how much of a corner size it cuts and rounds (multiples). */
|
|
64
|
+
export declare function shape(tokens: PageTokens): {
|
|
65
|
+
cut: number;
|
|
66
|
+
round: number;
|
|
67
|
+
};
|
|
68
|
+
/** The font stacks: display, body and mono. */
|
|
69
|
+
export declare function fontStacks(tokens: PageTokens): {
|
|
70
|
+
display: string;
|
|
71
|
+
body: string;
|
|
72
|
+
mono: string;
|
|
73
|
+
};
|
|
74
|
+
/** @font-face rules for the library's page fonts, served from fontsUrl (ending in /). */
|
|
75
|
+
export declare function fontFaces(tokens: PageTokens, fontsUrl: string, quote?: string): string;
|
|
76
|
+
/** The page font that draws a type role's first family at its weight, if the library serves one. */
|
|
77
|
+
export declare function fontFor(tokens: PageTokens, role: string): PageFont | undefined;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
//#region src/pages/theme.ts
|
|
2
|
+
function e(e, t) {
|
|
3
|
+
if (!t || !e.palettes?.length) return;
|
|
4
|
+
let n = e.palettes.find((e) => e.id === t);
|
|
5
|
+
return n && !n.default ? n.id : void 0;
|
|
6
|
+
}
|
|
7
|
+
function t(t, { scheme: n = "light", palette: r } = {}) {
|
|
8
|
+
let i = e(t, r), a = i && t.palettes?.find((e) => e.id === i)?.schemes[n] || t.schemes[n];
|
|
9
|
+
return Object.fromEntries(a.map((e) => [e.name, String(e.value)]));
|
|
10
|
+
}
|
|
11
|
+
function n(e, t, n) {
|
|
12
|
+
let r = e.groups[t]?.find((e) => e.name === n);
|
|
13
|
+
if (!r) throw Error(`${e.name}: no token ${n} in ${t}`);
|
|
14
|
+
return String(r.value);
|
|
15
|
+
}
|
|
16
|
+
function r(e) {
|
|
17
|
+
return {
|
|
18
|
+
cut: Number(n(e, "shape", "shape-cut")),
|
|
19
|
+
round: Number(n(e, "shape", "shape-round"))
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function i(e) {
|
|
23
|
+
return {
|
|
24
|
+
display: n(e, "font", "font-display"),
|
|
25
|
+
body: n(e, "font", "font-body"),
|
|
26
|
+
mono: n(e, "font", "font-mono")
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function a(e, t, n = "\"") {
|
|
30
|
+
return (e.fonts ?? []).map((e) => `@font-face{font-family:${n}${e.family}${n};src:url(${n}${t}${e.id}.woff2${n}) format(${n}woff2${n});font-weight:${e.weight};font-style:${e.style};font-display:swap}`).join("");
|
|
31
|
+
}
|
|
32
|
+
function o(e, t) {
|
|
33
|
+
let n = e.typeRoles.find((e) => e.name === t);
|
|
34
|
+
if (!n) return;
|
|
35
|
+
let r = n.fontFamily.split(",")[0].trim().replace(/^["']|["']$/g, ""), i = Number(n.fontWeight);
|
|
36
|
+
return (e.fonts ?? []).find((e) => {
|
|
37
|
+
if (e.family !== r) return !1;
|
|
38
|
+
let [t, n = t] = e.weight.split(/\s+/).map(Number);
|
|
39
|
+
return i >= t && i <= n;
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
//#endregion
|
|
43
|
+
export { a as fontFaces, o as fontFor, i as fontStacks, e as paletteId, t as roleValues, r as shape, n as tokenValue };
|
|
44
|
+
|
|
45
|
+
//# sourceMappingURL=theme.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"theme.js","names":[],"sources":["../../src/pages/theme.ts"],"sourcesContent":["// What static pages read from a library's built tokens (its \"<library>/tokens\"\n// export), and small helpers over it. No React, no DOM: runs in Workers and Node.\nimport type { AutoRule, MoodNames, Scheme } from '../hooks/moodBoot';\n\nexport type TokenEntry = { name: string; cssVar: string; value: string | number; type?: string; ref?: string; description?: string };\n\n/** A font file static pages and emails serve themselves (theme.json \"fonts\"). */\nexport type PageFont = {\n /** The name it's served as: <fontsUrl><id>.woff2 */\n id: string;\n family: string;\n /** A weight or a range for a variable font (\"100 900\") */\n weight: string;\n style: string;\n /** The woff2 file, as a package path (e.g. @fontsource/x/files/x-latin-400-normal.woff2) */\n src: string;\n};\n\n/** The parts of a library's built tokens (tokens.js) static pages use. */\nexport type PageTokens = {\n name: string;\n moods: MoodNames;\n auto: AutoRule;\n groups: Record<string, readonly TokenEntry[]>;\n schemes: Record<Scheme, readonly TokenEntry[]>;\n typeRoles: readonly { name: string; fontFamily: string; fontWeight: string | number }[];\n palettes?: readonly { id: string; label: string; default?: boolean; schemes: Record<Scheme, readonly TokenEntry[]> }[];\n fonts?: readonly PageFont[];\n};\n\n/**\n * A library, as static pages see it: its tokens, its stylesheet for them\n * (tokens.css plus core's page styles, then anything of its own), and the\n * inline script of its title effect (the TextReveal role; none: the title\n * shows as it is).\n */\nexport type PageTheme = {\n tokens: PageTokens;\n /** The CSS every static page inlines. */\n css: string;\n /** Inline JS run on status pages: animates h1[data-reveal]. It must check reduced motion itself. */\n reveal?: string;\n};\n\n/** The palette id to use: the one asked for if the library has it, else none (the default). */\nexport function paletteId(tokens: PageTokens, palette?: string | null): string | undefined {\n if (!palette || !tokens.palettes?.length) return undefined;\n const found = tokens.palettes.find((p) => p.id === palette);\n return found && !found.default ? found.id : undefined;\n}\n\n/** Every mood role's value in one scheme and palette: { bg: '#FFFFFF', accent: ..., ... } */\nexport function roleValues(tokens: PageTokens, { scheme = 'light', palette }: { scheme?: Scheme; palette?: string | null } = {}): Record<string, string> {\n const id = paletteId(tokens, palette);\n const list = (id && tokens.palettes?.find((p) => p.id === id)?.schemes[scheme]) || tokens.schemes[scheme];\n return Object.fromEntries(list.map((r) => [r.name, String(r.value)]));\n}\n\n/** A token's value by group and name (e.g. 'shape', 'shape-cut'). */\nexport function tokenValue(tokens: PageTokens, group: string, name: string): string {\n const found = tokens.groups[group]?.find((t) => t.name === name);\n if (!found) throw new Error(`${tokens.name}: no token ${name} in ${group}`);\n return String(found.value);\n}\n\n/** The library's corner rule: how much of a corner size it cuts and rounds (multiples). */\nexport function shape(tokens: PageTokens) {\n return { cut: Number(tokenValue(tokens, 'shape', 'shape-cut')), round: Number(tokenValue(tokens, 'shape', 'shape-round')) };\n}\n\n/** The font stacks: display, body and mono. */\nexport function fontStacks(tokens: PageTokens) {\n return {\n display: tokenValue(tokens, 'font', 'font-display'),\n body: tokenValue(tokens, 'font', 'font-body'),\n mono: tokenValue(tokens, 'font', 'font-mono'),\n };\n}\n\n/** @font-face rules for the library's page fonts, served from fontsUrl (ending in /). */\nexport function fontFaces(tokens: PageTokens, fontsUrl: string, quote = '\"'): string {\n return (tokens.fonts ?? [])\n .map(\n (f) =>\n `@font-face{font-family:${quote}${f.family}${quote};src:url(${quote}${fontsUrl}${f.id}.woff2${quote}) format(${quote}woff2${quote});` +\n `font-weight:${f.weight};font-style:${f.style};font-display:swap}`,\n )\n .join('');\n}\n\n/** The page font that draws a type role's first family at its weight, if the library serves one. */\nexport function fontFor(tokens: PageTokens, role: string): PageFont | undefined {\n const type = tokens.typeRoles.find((r) => r.name === role);\n if (!type) return undefined;\n const family = type.fontFamily.split(',')[0].trim().replace(/^[\"']|[\"']$/g, '');\n const weight = Number(type.fontWeight);\n return (tokens.fonts ?? []).find((f) => {\n if (f.family !== family) return false;\n const [lo, hi = lo] = f.weight.split(/\\s+/).map(Number);\n return weight >= lo && weight <= hi;\n });\n}\n"],"mappings":";AA6CA,SAAgB,EAAU,GAAoB,GAA6C;CACzF,IAAI,CAAC,KAAW,CAAC,EAAO,UAAU,QAAQ;CAC1C,IAAM,IAAQ,EAAO,SAAS,MAAM,MAAM,EAAE,OAAO,CAAO;CAC1D,OAAO,KAAS,CAAC,EAAM,UAAU,EAAM,KAAK,KAAA;AAC9C;AAGA,SAAgB,EAAW,GAAoB,EAAE,YAAS,SAAS,eAA0D,CAAC,GAA2B;CACvJ,IAAM,IAAK,EAAU,GAAQ,CAAO,GAC9B,IAAQ,KAAM,EAAO,UAAU,MAAM,MAAM,EAAE,OAAO,CAAE,GAAG,QAAQ,MAAY,EAAO,QAAQ;CAClG,OAAO,OAAO,YAAY,EAAK,KAAK,MAAM,CAAC,EAAE,MAAM,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC;AACtE;AAGA,SAAgB,EAAW,GAAoB,GAAe,GAAsB;CAClF,IAAM,IAAQ,EAAO,OAAO,IAAQ,MAAM,MAAM,EAAE,SAAS,CAAI;CAC/D,IAAI,CAAC,GAAO,MAAU,MAAM,GAAG,EAAO,KAAK,aAAa,EAAK,MAAM,GAAO;CAC1E,OAAO,OAAO,EAAM,KAAK;AAC3B;AAGA,SAAgB,EAAM,GAAoB;CACxC,OAAO;EAAE,KAAK,OAAO,EAAW,GAAQ,SAAS,WAAW,CAAC;EAAG,OAAO,OAAO,EAAW,GAAQ,SAAS,aAAa,CAAC;CAAE;AAC5H;AAGA,SAAgB,EAAW,GAAoB;CAC7C,OAAO;EACL,SAAS,EAAW,GAAQ,QAAQ,cAAc;EAClD,MAAM,EAAW,GAAQ,QAAQ,WAAW;EAC5C,MAAM,EAAW,GAAQ,QAAQ,WAAW;CAC9C;AACF;AAGA,SAAgB,EAAU,GAAoB,GAAkB,IAAQ,MAAa;CACnF,QAAQ,EAAO,SAAS,CAAC,GACtB,KACE,MACC,0BAA0B,IAAQ,EAAE,SAAS,EAAM,WAAW,IAAQ,IAAW,EAAE,GAAG,QAAQ,EAAM,WAAW,EAAM,OAAO,EAAM,gBACnH,EAAE,OAAO,cAAc,EAAE,MAAM,oBAClD,EACC,KAAK,EAAE;AACZ;AAGA,SAAgB,EAAQ,GAAoB,GAAoC;CAC9E,IAAM,IAAO,EAAO,UAAU,MAAM,MAAM,EAAE,SAAS,CAAI;CACzD,IAAI,CAAC,GAAM;CACX,IAAM,IAAS,EAAK,WAAW,MAAM,GAAG,EAAE,GAAG,KAAK,EAAE,QAAQ,gBAAgB,EAAE,GACxE,IAAS,OAAO,EAAK,UAAU;CACrC,QAAQ,EAAO,SAAS,CAAC,GAAG,MAAM,MAAM;EACtC,IAAI,EAAE,WAAW,GAAQ,OAAO;EAChC,IAAM,CAAC,GAAI,IAAK,KAAM,EAAE,OAAO,MAAM,KAAK,EAAE,IAAI,MAAM;EACtD,OAAO,KAAU,KAAM,KAAU;CACnC,CAAC;AACH"}
|
package/dist/pages.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { createPages } from './pages/create';
|
|
2
|
+
/** Core's page styles (base styles, then the status page's), all token custom properties: put a library's tokens.css first. */
|
|
3
|
+
export declare const PAGE_CSS: string;
|
|
4
|
+
export { createPages };
|
|
5
|
+
export type { Pages } from './pages/create';
|
|
6
|
+
export { escapeHtml } from './pages/status';
|
|
7
|
+
export type { StatusPageOptions } from './pages/status';
|
|
8
|
+
export type { EmailLayoutOptions, EmailTheme } from './pages/email';
|
|
9
|
+
export type { MarkIconOptions } from './pages/icon';
|
|
10
|
+
export type { PageFont, PageTheme, PageTokens, TokenEntry } from './pages/theme';
|
|
11
|
+
export type { AutoRule, BootScriptOptions, MoodName, MoodNames, Scheme } from './hooks/moodBoot';
|
|
12
|
+
export declare const statusPage: (options: import("./pages").StatusPageOptions) => string, emailLayout: (options: import("./pages").EmailLayoutOptions) => string, emailTheme: (options?: {
|
|
13
|
+
palette?: string | null;
|
|
14
|
+
}) => import("./pages").EmailTheme, markIcon: (options: import("./pages").MarkIconOptions) => string, moodBootScript: (options?: import(".").BootScriptOptions) => string, themeColorMeta: (options?: {
|
|
15
|
+
palette?: string | null;
|
|
16
|
+
}) => string, pageFonts: readonly import("./pages").PageFont[];
|
package/dist/pages.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { escapeHtml as e } from "./pages/status.js";
|
|
2
|
+
import { createPages as t } from "./pages/create.js";
|
|
3
|
+
import n from "./pages/page.js";
|
|
4
|
+
import { css as r, tokens as i } from "@rnine/core/tokens";
|
|
5
|
+
//#region src/pages.ts
|
|
6
|
+
var a = n, { statusPage: o, emailLayout: s, emailTheme: c, markIcon: l, moodBootScript: u, themeColorMeta: d, pageFonts: f } = t({
|
|
7
|
+
tokens: i,
|
|
8
|
+
css: r + a
|
|
9
|
+
});
|
|
10
|
+
//#endregion
|
|
11
|
+
export { a as PAGE_CSS, t as createPages, s as emailLayout, c as emailTheme, e as escapeHtml, l as markIcon, u as moodBootScript, f as pageFonts, o as statusPage, d as themeColorMeta };
|
|
12
|
+
|
|
13
|
+
//# sourceMappingURL=pages.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pages.js","names":[],"sources":["../src/pages.ts"],"sourcesContent":["// @rnine/core/pages: static pages from a library's tokens, for where React\n// doesn't run: status and error pages (served by a Worker or nginx), emails,\n// favicons and app icons, the mood boot script and theme-color metas. No\n// React, no DOM: runs in Workers and Node.\n//\n// Every library exports the same names from \"<library>/pages\", in its look\n// (core's are Plain's):\n//\n// import { statusPage } from '@rnine/nineos/pages';\n// const html = statusPage({ title: 'Back soon', label: 'Maintenance', heading: 'Back soon',\n// paragraphs: ['We’re updating the site.'], status: { command: 'status', text: 'updating' } });\n//\n// A library makes its own with createPages({ tokens, css, reveal }): css is\n// its tokens.css followed by PAGE_CSS (and anything of its own).\nimport { css as tokensCss, tokens } from '@rnine/core/tokens';\nimport { createPages } from './pages/create';\nimport pageCss from './pages/page.scss?inline';\n\n/** Core's page styles (base styles, then the status page's), all token custom properties: put a library's tokens.css first. */\nexport const PAGE_CSS: string = pageCss;\nexport { createPages };\nexport type { Pages } from './pages/create';\nexport { escapeHtml } from './pages/status';\nexport type { StatusPageOptions } from './pages/status';\nexport type { EmailLayoutOptions, EmailTheme } from './pages/email';\nexport type { MarkIconOptions } from './pages/icon';\nexport type { PageFont, PageTheme, PageTokens, TokenEntry } from './pages/theme';\nexport type { AutoRule, BootScriptOptions, MoodName, MoodNames, Scheme } from './hooks/moodBoot';\n\nconst plain = createPages({ tokens, css: tokensCss + PAGE_CSS });\n\nexport const { statusPage, emailLayout, emailTheme, markIcon, moodBootScript, themeColorMeta, pageFonts } = plain;\n"],"mappings":";;;;;AAmBA,IAAa,IAAmB,GAYnB,EAAE,eAAY,gBAAa,eAAY,aAAU,mBAAgB,mBAAgB,iBAFhF,EAAY;CAAE;CAAQ,KAAK,IAAY;AAAS,CAE8C"}
|
|
@@ -2,6 +2,7 @@ import { type ComponentType, type ReactNode } from 'react';
|
|
|
2
2
|
import { type AutoRule, type MoodNames, type MoodOptions } from '../hooks/mood';
|
|
3
3
|
import { type TextRevealProps } from '../motion/TextReveal';
|
|
4
4
|
import { type MarkArtwork } from './mark';
|
|
5
|
+
import type { CardLift } from '../hooks/cardLift';
|
|
5
6
|
/** The effects core components draw with: each library brings its own. */
|
|
6
7
|
export type ThemeSlots = {
|
|
7
8
|
/** Titles in Section, Panel and MenuRail. */
|
|
@@ -16,6 +17,8 @@ export type ThemeConfig = {
|
|
|
16
17
|
moods: MoodNames;
|
|
17
18
|
auto: AutoRule;
|
|
18
19
|
slots: ThemeSlots;
|
|
20
|
+
/** Cards lift their cut corner toward the pointer (TiltCard, useCardLift). Default: off. */
|
|
21
|
+
lift?: CardLift;
|
|
19
22
|
};
|
|
20
23
|
/** Plain, core's own look: the placeholder every library starts from. Matches theme.json (a test checks). */
|
|
21
24
|
export declare const PLAIN_THEME: ThemeConfig;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ThemeProvider.js","names":[],"sources":["../../src/provider/ThemeProvider.tsx"],"sourcesContent":["import { createContext, useContext, useEffect, type ComponentType, type ReactNode } from 'react';\nimport { configureMood, setMoodDefaults, watchMood, type AutoRule, type MoodNames, type MoodOptions } from '../hooks/mood';\nimport { ForceReducedMotion, RevealOnce } from '../hooks/motion';\nimport { applyMotion } from '../hooks/motionSetting';\nimport { PageEffects } from '../motion/PageEffects';\nimport { TextReveal, type TextRevealProps } from '../motion/TextReveal';\nimport { MarkProvider, type MarkArtwork } from './mark';\n\n/** The effects core components draw with: each library brings its own. */\nexport type ThemeSlots = {\n /** Titles in Section, Panel and MenuRail. */\n TextReveal: ComponentType<TextRevealProps>;\n /** Page-wide effects ThemeProvider mounts (effects prop). */\n PageEffects: ComponentType;\n};\n\n/** What a library is: its name, mood names, Auto rule and effects. */\nexport type ThemeConfig = {\n /** The library's name, e.g. for the Wordmark default. */\n name: string;\n moods: MoodNames;\n auto: AutoRule;\n slots: ThemeSlots;\n};\n\n/** Plain, core's own look: the placeholder every library starts from. Matches theme.json (a test checks). */\nexport const PLAIN_THEME: ThemeConfig = {\n name: 'Plain',\n moods: {\n light: { id: 'light', label: 'Light', description: 'Neutral grays on white, one blue accent.' },\n dark: { id: 'dark', label: 'Dark', description: 'The same roles on near-black.' },\n },\n auto: 'system',\n slots: { TextReveal, PageEffects },\n};\n\nconst ThemeContext = createContext<ThemeConfig>(PLAIN_THEME);\n\n/**\n * Gives components a library's config (its effects, mood names) without\n * watching the mood: for previews like Storybook, where something else sets\n * the page's data-scheme. Apps use ThemeProvider.\n */\nexport function ThemeConfigProvider({ config, children }: { config: ThemeConfig; children: ReactNode }) {\n setMoodDefaults({ names: config.moods, auto: config.auto });\n return <ThemeContext.Provider value={config}>{children}</ThemeContext.Provider>;\n}\n\n/** The library in use (the nearest ThemeProvider's, else Plain). */\nexport function useTheme(): ThemeConfig {\n return useContext(ThemeContext);\n}\n\nexport type ThemeProviderProps = {\n children: ReactNode;\n /** Turn motion off for this app, whatever the system setting (e.g. a \"motion off\" toggle). */\n reducedMotion?: boolean;\n /** The library's page-wide effects (NineOS: Cipher scanlines and click sparks). Default on. */\n effects?: boolean;\n /** Panel titles reveal only the first time each appears in a session (then they're simply there). Default off. */\n revealOnce?: boolean;\n /** The app's own mark, drawn by every Mark. Default: a placeholder frame. */\n mark?: MarkArtwork;\n /** Where the chosen mood is kept (storage key, a cookie domain to share it between hosts) and the Auto rule. See configureMood. */\n mood?: MoodOptions;\n};\n\n/**\n * For libraries: the provider with a library's config. A library exports\n * `ThemeProvider = (props) => <ThemeRoot config={ITS_THEME} {...props} />`.\n */\nexport function ThemeRoot({ config, children, reducedMotion = false, effects = true, revealOnce = false, mark, mood }: ThemeProviderProps & { config: ThemeConfig }) {\n // Before any child reads the mood (unchanged values do nothing)\n setMoodDefaults({ names: config.moods, auto: config.auto });\n if (mood) configureMood(mood);\n useEffect(() => watchMood(), []);\n useEffect(() => applyMotion(), []);\n const Effects = config.slots.PageEffects;\n const content = (\n <ThemeContext.Provider value={config}>\n <ForceReducedMotion.Provider value={reducedMotion}>\n <RevealOnce.Provider value={revealOnce}>\n {children}\n {effects && <Effects />}\n </RevealOnce.Provider>\n </ForceReducedMotion.Provider>\n </ThemeContext.Provider>\n );\n return mark ? <MarkProvider mark={mark}>{content}</MarkProvider> : content;\n}\n\n/**\n * Wrap the app once. It keeps the mood on <html data-scheme=\"light|dark\">:\n * the viewer's saved choice, or the library's Auto rule (following the\n * system, and for the night rule the clock too). See useMood. It also applies\n * the viewer's saved motion setting (MotionToggle) and gives every Mark the\n * app's artwork.\n *\n * <ThemeProvider mark={{ full: markSvg }} mood={{ cookieDomain: 'example.com' }}>\n * <App />\n * </ThemeProvider>\n */\nexport function ThemeProvider(props: ThemeProviderProps) {\n return <ThemeRoot config={PLAIN_THEME} {...props} />;\n}\n"],"mappings":";;;;;;;;;
|
|
1
|
+
{"version":3,"file":"ThemeProvider.js","names":[],"sources":["../../src/provider/ThemeProvider.tsx"],"sourcesContent":["import { createContext, useContext, useEffect, type ComponentType, type ReactNode } from 'react';\nimport { configureMood, setMoodDefaults, watchMood, type AutoRule, type MoodNames, type MoodOptions } from '../hooks/mood';\nimport { ForceReducedMotion, RevealOnce } from '../hooks/motion';\nimport { applyMotion } from '../hooks/motionSetting';\nimport { PageEffects } from '../motion/PageEffects';\nimport { TextReveal, type TextRevealProps } from '../motion/TextReveal';\nimport { MarkProvider, type MarkArtwork } from './mark';\nimport type { CardLift } from '../hooks/cardLift';\n\n/** The effects core components draw with: each library brings its own. */\nexport type ThemeSlots = {\n /** Titles in Section, Panel and MenuRail. */\n TextReveal: ComponentType<TextRevealProps>;\n /** Page-wide effects ThemeProvider mounts (effects prop). */\n PageEffects: ComponentType;\n};\n\n/** What a library is: its name, mood names, Auto rule and effects. */\nexport type ThemeConfig = {\n /** The library's name, e.g. for the Wordmark default. */\n name: string;\n moods: MoodNames;\n auto: AutoRule;\n slots: ThemeSlots;\n /** Cards lift their cut corner toward the pointer (TiltCard, useCardLift). Default: off. */\n lift?: CardLift;\n};\n\n/** Plain, core's own look: the placeholder every library starts from. Matches theme.json (a test checks). */\nexport const PLAIN_THEME: ThemeConfig = {\n name: 'Plain',\n moods: {\n light: { id: 'light', label: 'Light', description: 'Neutral grays on white, one blue accent.' },\n dark: { id: 'dark', label: 'Dark', description: 'The same roles on near-black.' },\n },\n auto: 'system',\n slots: { TextReveal, PageEffects },\n};\n\nconst ThemeContext = createContext<ThemeConfig>(PLAIN_THEME);\n\n/**\n * Gives components a library's config (its effects, mood names) without\n * watching the mood: for previews like Storybook, where something else sets\n * the page's data-scheme. Apps use ThemeProvider.\n */\nexport function ThemeConfigProvider({ config, children }: { config: ThemeConfig; children: ReactNode }) {\n setMoodDefaults({ names: config.moods, auto: config.auto });\n return <ThemeContext.Provider value={config}>{children}</ThemeContext.Provider>;\n}\n\n/** The library in use (the nearest ThemeProvider's, else Plain). */\nexport function useTheme(): ThemeConfig {\n return useContext(ThemeContext);\n}\n\nexport type ThemeProviderProps = {\n children: ReactNode;\n /** Turn motion off for this app, whatever the system setting (e.g. a \"motion off\" toggle). */\n reducedMotion?: boolean;\n /** The library's page-wide effects (NineOS: Cipher scanlines and click sparks). Default on. */\n effects?: boolean;\n /** Panel titles reveal only the first time each appears in a session (then they're simply there). Default off. */\n revealOnce?: boolean;\n /** The app's own mark, drawn by every Mark. Default: a placeholder frame. */\n mark?: MarkArtwork;\n /** Where the chosen mood is kept (storage key, a cookie domain to share it between hosts) and the Auto rule. See configureMood. */\n mood?: MoodOptions;\n};\n\n/**\n * For libraries: the provider with a library's config. A library exports\n * `ThemeProvider = (props) => <ThemeRoot config={ITS_THEME} {...props} />`.\n */\nexport function ThemeRoot({ config, children, reducedMotion = false, effects = true, revealOnce = false, mark, mood }: ThemeProviderProps & { config: ThemeConfig }) {\n // Before any child reads the mood (unchanged values do nothing)\n setMoodDefaults({ names: config.moods, auto: config.auto });\n if (mood) configureMood(mood);\n useEffect(() => watchMood(), []);\n useEffect(() => applyMotion(), []);\n const Effects = config.slots.PageEffects;\n const content = (\n <ThemeContext.Provider value={config}>\n <ForceReducedMotion.Provider value={reducedMotion}>\n <RevealOnce.Provider value={revealOnce}>\n {children}\n {effects && <Effects />}\n </RevealOnce.Provider>\n </ForceReducedMotion.Provider>\n </ThemeContext.Provider>\n );\n return mark ? <MarkProvider mark={mark}>{content}</MarkProvider> : content;\n}\n\n/**\n * Wrap the app once. It keeps the mood on <html data-scheme=\"light|dark\">:\n * the viewer's saved choice, or the library's Auto rule (following the\n * system, and for the night rule the clock too). See useMood. It also applies\n * the viewer's saved motion setting (MotionToggle) and gives every Mark the\n * app's artwork.\n *\n * <ThemeProvider mark={{ full: markSvg }} mood={{ cookieDomain: 'example.com' }}>\n * <App />\n * </ThemeProvider>\n */\nexport function ThemeProvider(props: ThemeProviderProps) {\n return <ThemeRoot config={PLAIN_THEME} {...props} />;\n}\n"],"mappings":";;;;;;;;;AA6BA,IAAa,IAA2B;CACtC,MAAM;CACN,OAAO;EACL,OAAO;GAAE,IAAI;GAAS,OAAO;GAAS,aAAa;EAA2C;EAC9F,MAAM;GAAE,IAAI;GAAQ,OAAO;GAAQ,aAAa;EAAgC;CAClF;CACA,MAAM;CACN,OAAO;EAAE;EAAY;CAAY;AACnC,GAEM,IAAe,EAA2B,CAAW;AAO3D,SAAgB,EAAoB,EAAE,WAAQ,eAA0D;CAEtG,OADA,EAAgB;EAAE,OAAO,EAAO;EAAO,MAAM,EAAO;CAAK,CAAC,GACnD,kBAAC,EAAa,UAAd;EAAuB,OAAO;EAAS;CAAgC,CAAA;AAChF;AAGA,SAAgB,IAAwB;CACtC,OAAO,EAAW,CAAY;AAChC;AAoBA,SAAgB,EAAU,EAAE,WAAQ,aAAU,mBAAgB,IAAO,aAAU,IAAM,gBAAa,IAAO,SAAM,WAAsD;CAKnK,AAHA,EAAgB;EAAE,OAAO,EAAO;EAAO,MAAM,EAAO;CAAK,CAAC,GACtD,KAAM,EAAc,CAAI,GAC5B,QAAgB,EAAU,GAAG,CAAC,CAAC,GAC/B,QAAgB,EAAY,GAAG,CAAC,CAAC;CACjC,IAAM,IAAU,EAAO,MAAM,aACvB,IACJ,kBAAC,EAAa,UAAd;EAAuB,OAAO;YAC5B,kBAAC,EAAmB,UAApB;GAA6B,OAAO;aAClC,kBAAC,EAAW,UAAZ;IAAqB,OAAO;cAA5B,CACG,GACA,KAAW,kBAAC,GAAD,CAAU,CAAA,CACH;;EACM,CAAA;CACR,CAAA;CAEzB,OAAO,IAAO,kBAAC,GAAD;EAAoB;YAAO;CAAsB,CAAA,IAAI;AACrE;AAaA,SAAgB,EAAc,GAA2B;CACvD,OAAO,kBAAC,GAAD;EAAW,QAAQ;EAAa,GAAI;CAAQ,CAAA;AACrD"}
|