563 lines
17 KiB
TypeScript
563 lines
17 KiB
TypeScript
import {
|
|
createDesignTokenRegistry,
|
|
type DesignTokenInput,
|
|
type DesignTokenScope,
|
|
} from "./token.js";
|
|
|
|
/**
|
|
* Ekklesia's global semantic theme.
|
|
*
|
|
* These are **semantic** tokens, not a palette. Nothing here is named for the colour it currently
|
|
* happens to be, because the whole point is that a future theme editor can change the value without
|
|
* every consumer's meaning changing with it: `background.raised` stays "the surface that sits above
|
|
* an ordinary surface" whatever a church's brand makes it.
|
|
*
|
|
* The default values are Ekklesia's calm, teal-accented light theme. They remain semantic defaults —
|
|
* a deployment overrides them by installing different values at its host root, and this pass
|
|
* deliberately ships no theme editor, brand builder, or dark-mode variant to do that with.
|
|
*
|
|
* ## Status is never colour alone
|
|
*
|
|
* Each status family carries a `symbol` token alongside its colours. A status that is only red is
|
|
* invisible to a person who cannot distinguish it, and unreadable on a projector or a washed-out
|
|
* lobby screen. Component code renders the symbol next to the label, so the meaning survives losing
|
|
* the colour entirely (WCAG 2.2 §1.4.1).
|
|
*/
|
|
const declarations = [
|
|
// ---------------------------------------------------------------------------------------------
|
|
// Backgrounds — the elevation ladder, from the page behind everything to the overlay above it.
|
|
// ---------------------------------------------------------------------------------------------
|
|
{
|
|
description: "The furthest-back background of a whole page or a full-viewport display.",
|
|
label: "Page background",
|
|
name: "background.page",
|
|
value: "#f3f7f6",
|
|
},
|
|
{
|
|
description:
|
|
"A large working area inside a page — a scrolling list region, a preview area, an editor body.",
|
|
label: "Canvas background",
|
|
name: "background.canvas",
|
|
value: "#f8faf9",
|
|
},
|
|
{
|
|
description: "An ordinary panel, card, table, or dialog body sitting on the page.",
|
|
label: "Surface background",
|
|
name: "background.surface",
|
|
value: "#ffffff",
|
|
},
|
|
{
|
|
description: "A surface deliberately lifted above an ordinary surface, such as a popover.",
|
|
label: "Raised surface background",
|
|
name: "background.raised",
|
|
value: "#ffffff",
|
|
},
|
|
{
|
|
description: "The scrim drawn behind a modal dialog to separate it from the page beneath.",
|
|
label: "Overlay background",
|
|
name: "background.overlay",
|
|
value: "rgba(23, 23, 21, 0.55)",
|
|
},
|
|
{
|
|
description: "A recessed area such as a text input, a code block, or a well.",
|
|
label: "Inset background",
|
|
name: "background.inset",
|
|
value: "#edf3f1",
|
|
},
|
|
|
|
// ---------------------------------------------------------------------------------------------
|
|
// Text.
|
|
// ---------------------------------------------------------------------------------------------
|
|
{
|
|
description: "Body copy and headings — the default foreground for readable content.",
|
|
label: "Primary text",
|
|
name: "text.primary",
|
|
value: "#182321",
|
|
},
|
|
{
|
|
description: "Supporting text that is still content: captions, secondary columns, help text.",
|
|
label: "Secondary text",
|
|
name: "text.secondary",
|
|
value: "#4f5f5b",
|
|
},
|
|
{
|
|
description: "De-emphasised text such as placeholders, timestamps, and disabled labels.",
|
|
label: "Muted text",
|
|
name: "text.muted",
|
|
value: "#6c7b77",
|
|
},
|
|
{
|
|
description: "Text drawn on a strongly coloured background, such as a primary button.",
|
|
label: "Inverse text",
|
|
name: "text.inverse",
|
|
value: "#ffffff",
|
|
},
|
|
{
|
|
description: "Text reporting a successful outcome, paired with the success symbol.",
|
|
label: "Success text",
|
|
name: "text.success",
|
|
value: "#166534",
|
|
},
|
|
{
|
|
description: "Text reporting a caution or a degraded state, paired with the warning symbol.",
|
|
label: "Warning text",
|
|
name: "text.warning",
|
|
value: "#854d0e",
|
|
},
|
|
{
|
|
description: "Text reporting a failure or a destructive consequence, paired with its symbol.",
|
|
label: "Danger text",
|
|
name: "text.danger",
|
|
value: "#9f1239",
|
|
},
|
|
|
|
// ---------------------------------------------------------------------------------------------
|
|
// Accent and interactive states.
|
|
// ---------------------------------------------------------------------------------------------
|
|
{
|
|
description: "The primary action colour: primary buttons, selected tabs, active links.",
|
|
label: "Accent",
|
|
name: "accent.base",
|
|
value: "#4fb3a8",
|
|
},
|
|
{
|
|
description: "The accent background under a hovered primary action.",
|
|
label: "Accent hover",
|
|
name: "accent.hover",
|
|
value: "#5ab9ae",
|
|
},
|
|
{
|
|
description: "The accent background while a primary action is pressed.",
|
|
label: "Accent active",
|
|
name: "accent.active",
|
|
value: "#45a59b",
|
|
},
|
|
{
|
|
description: "A deeper accent for pressed primary actions and high-emphasis emphasis.",
|
|
label: "Strong accent",
|
|
name: "accent.strong",
|
|
value: "#246b64",
|
|
},
|
|
{
|
|
description: "A faint accent wash for selected rows and subtle accent backgrounds.",
|
|
label: "Subtle accent",
|
|
name: "accent.subtle",
|
|
value: "#def2ef",
|
|
},
|
|
{
|
|
description: "Foreground drawn on top of the accent background.",
|
|
label: "Accent foreground",
|
|
name: "accent.foreground",
|
|
value: "#102a27",
|
|
},
|
|
{
|
|
description: "The focus ring colour. Never the only signal that something has focus.",
|
|
label: "Focus ring",
|
|
name: "accent.focus",
|
|
value: "#287a72",
|
|
},
|
|
|
|
// ---------------------------------------------------------------------------------------------
|
|
// Borders.
|
|
// ---------------------------------------------------------------------------------------------
|
|
{
|
|
description: "The default border around panels, inputs, and table cells.",
|
|
label: "Border",
|
|
name: "border.base",
|
|
value: "#d3dedb",
|
|
},
|
|
{
|
|
description: "A heavier border for emphasis, active inputs, and selected outlines.",
|
|
label: "Strong border",
|
|
name: "border.strong",
|
|
value: "#91a6a1",
|
|
},
|
|
{
|
|
description: "A hairline rule separating rows or sections inside one surface.",
|
|
label: "Divider",
|
|
name: "border.divider",
|
|
value: "#e2eae8",
|
|
},
|
|
{
|
|
description: "The border width used by controls and panels.",
|
|
label: "Border width",
|
|
name: "border.width",
|
|
value: "1px",
|
|
},
|
|
|
|
// ---------------------------------------------------------------------------------------------
|
|
// Status families. Each carries a symbol so meaning never depends on colour.
|
|
// ---------------------------------------------------------------------------------------------
|
|
{
|
|
description: "Background of a success badge, banner, or row highlight.",
|
|
label: "Success background",
|
|
name: "status.success-background",
|
|
value: "#dcfce7",
|
|
},
|
|
{
|
|
description: "Border of a success badge or banner.",
|
|
label: "Success border",
|
|
name: "status.success-border",
|
|
value: "#86efac",
|
|
},
|
|
{
|
|
description: "The non-colour signal for success, rendered beside the label.",
|
|
label: "Success symbol",
|
|
name: "status.success-symbol",
|
|
value: '"✓"',
|
|
},
|
|
{
|
|
description: "Background of a warning badge, banner, or row highlight.",
|
|
label: "Warning background",
|
|
name: "status.warning-background",
|
|
value: "#fef3c7",
|
|
},
|
|
{
|
|
description: "Border of a warning badge or banner.",
|
|
label: "Warning border",
|
|
name: "status.warning-border",
|
|
value: "#fcd34d",
|
|
},
|
|
{
|
|
description: "The non-colour signal for a warning, rendered beside the label.",
|
|
label: "Warning symbol",
|
|
name: "status.warning-symbol",
|
|
value: '"!"',
|
|
},
|
|
{
|
|
description: "Background of a danger badge, banner, or row highlight.",
|
|
label: "Danger background",
|
|
name: "status.danger-background",
|
|
value: "#ffe4e6",
|
|
},
|
|
{
|
|
description: "Border of a danger badge or banner.",
|
|
label: "Danger border",
|
|
name: "status.danger-border",
|
|
value: "#fda4af",
|
|
},
|
|
{
|
|
description: "The non-colour signal for danger or failure, rendered beside the label.",
|
|
label: "Danger symbol",
|
|
name: "status.danger-symbol",
|
|
value: '"✕"',
|
|
},
|
|
{
|
|
description: "Background of a neutral informational badge or banner.",
|
|
label: "Info background",
|
|
name: "status.info-background",
|
|
value: "#def2ef",
|
|
},
|
|
{
|
|
description: "Border of a neutral informational badge or banner.",
|
|
label: "Info border",
|
|
name: "status.info-border",
|
|
value: "#9bd5ce",
|
|
},
|
|
{
|
|
description: "The non-colour signal for information, rendered beside the label.",
|
|
label: "Info symbol",
|
|
name: "status.info-symbol",
|
|
value: '"i"',
|
|
},
|
|
|
|
// ---------------------------------------------------------------------------------------------
|
|
// Interaction states.
|
|
// ---------------------------------------------------------------------------------------------
|
|
{
|
|
description: "Background wash under a hovered control or row.",
|
|
label: "Hover background",
|
|
name: "state.hover-background",
|
|
value: "#edf3f1",
|
|
},
|
|
{
|
|
description: "Background wash under a control being pressed.",
|
|
label: "Pressed background",
|
|
name: "state.pressed-background",
|
|
value: "#e1ebe8",
|
|
},
|
|
{
|
|
description: "Background of a selected row, tab, or list item.",
|
|
label: "Selected background",
|
|
name: "state.selected-background",
|
|
value: "#def2ef",
|
|
},
|
|
{
|
|
description: "Foreground of a selected row, tab, or list item.",
|
|
label: "Selected foreground",
|
|
name: "state.selected-foreground",
|
|
value: "#205f59",
|
|
},
|
|
{
|
|
description: "Opacity applied to a disabled control. Disabled state is never colour alone.",
|
|
label: "Disabled opacity",
|
|
name: "state.disabled-opacity",
|
|
value: "0.55",
|
|
},
|
|
{
|
|
description: "Thickness of the keyboard focus ring.",
|
|
label: "Focus ring width",
|
|
name: "state.focus-visible-width",
|
|
value: "2px",
|
|
},
|
|
{
|
|
description: "Gap between a focused control and its focus ring.",
|
|
label: "Focus ring offset",
|
|
name: "state.focus-visible-offset",
|
|
value: "2px",
|
|
},
|
|
|
|
// ---------------------------------------------------------------------------------------------
|
|
// Typography.
|
|
// ---------------------------------------------------------------------------------------------
|
|
{
|
|
description: "The interface font stack for all ordinary text.",
|
|
label: "Interface font family",
|
|
name: "typography.family-sans",
|
|
value: "system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif",
|
|
},
|
|
{
|
|
description: "The monospace stack for identifiers, keys, and code.",
|
|
label: "Monospace font family",
|
|
name: "typography.family-mono",
|
|
value: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
|
|
},
|
|
{
|
|
description: "Smallest supported text size, for dense metadata only.",
|
|
label: "Extra small font size",
|
|
name: "typography.size-xs",
|
|
value: "0.75rem",
|
|
},
|
|
{
|
|
description: "Small text: captions, table metadata, help text.",
|
|
label: "Small font size",
|
|
name: "typography.size-sm",
|
|
value: "0.875rem",
|
|
},
|
|
{
|
|
description: "The default body text size.",
|
|
label: "Body font size",
|
|
name: "typography.size-md",
|
|
value: "1rem",
|
|
},
|
|
{
|
|
description: "Large body text and small headings.",
|
|
label: "Large font size",
|
|
name: "typography.size-lg",
|
|
value: "1.125rem",
|
|
},
|
|
{
|
|
description: "Third-level heading size.",
|
|
label: "Heading 3 font size",
|
|
name: "typography.size-heading-3",
|
|
value: "1.25rem",
|
|
},
|
|
{
|
|
description: "Second-level heading size.",
|
|
label: "Heading 2 font size",
|
|
name: "typography.size-heading-2",
|
|
value: "1.5rem",
|
|
},
|
|
{
|
|
description: "First-level heading size — one per screen.",
|
|
label: "Heading 1 font size",
|
|
name: "typography.size-heading-1",
|
|
value: "1.875rem",
|
|
},
|
|
{
|
|
description: "Normal text weight.",
|
|
label: "Regular font weight",
|
|
name: "typography.weight-regular",
|
|
value: "400",
|
|
},
|
|
{
|
|
description: "Weight for labels, table headers, and emphasised body text.",
|
|
label: "Medium font weight",
|
|
name: "typography.weight-medium",
|
|
value: "500",
|
|
},
|
|
{
|
|
description: "Weight for headings and primary actions.",
|
|
label: "Bold font weight",
|
|
name: "typography.weight-bold",
|
|
value: "600",
|
|
},
|
|
{
|
|
description: "Tight line height for headings and single-line controls.",
|
|
label: "Tight line height",
|
|
name: "typography.line-height-tight",
|
|
value: "1.2",
|
|
},
|
|
{
|
|
description: "Default line height for body copy.",
|
|
label: "Normal line height",
|
|
name: "typography.line-height-normal",
|
|
value: "1.5",
|
|
},
|
|
{
|
|
description: "Relaxed line height for long-form reading.",
|
|
label: "Relaxed line height",
|
|
name: "typography.line-height-relaxed",
|
|
value: "1.7",
|
|
},
|
|
|
|
// ---------------------------------------------------------------------------------------------
|
|
// Spacing scale.
|
|
// ---------------------------------------------------------------------------------------------
|
|
{
|
|
description: "No space. Named so a component never hardcodes a bare zero.",
|
|
label: "No spacing",
|
|
name: "spacing.none",
|
|
value: "0",
|
|
},
|
|
{
|
|
description: "Hairline spacing between tightly related inline elements.",
|
|
label: "Extra extra small spacing",
|
|
name: "spacing.2xs",
|
|
value: "0.125rem",
|
|
},
|
|
{
|
|
description: "Spacing inside a compact control.",
|
|
label: "Extra small spacing",
|
|
name: "spacing.xs",
|
|
value: "0.375rem",
|
|
},
|
|
{
|
|
description: "Spacing between a label and its field.",
|
|
label: "Small spacing",
|
|
name: "spacing.sm",
|
|
value: "0.5rem",
|
|
},
|
|
{
|
|
description: "The default gap between related elements.",
|
|
label: "Medium spacing",
|
|
name: "spacing.md",
|
|
value: "0.75rem",
|
|
},
|
|
{
|
|
description: "Padding inside a panel or dialog.",
|
|
label: "Large spacing",
|
|
name: "spacing.lg",
|
|
value: "1rem",
|
|
},
|
|
{
|
|
description: "Spacing between sections of one screen.",
|
|
label: "Extra large spacing",
|
|
name: "spacing.xl",
|
|
value: "1.5rem",
|
|
},
|
|
{
|
|
description: "Spacing between major regions of a page.",
|
|
label: "Extra extra large spacing",
|
|
name: "spacing.2xl",
|
|
value: "2.5rem",
|
|
},
|
|
|
|
// ---------------------------------------------------------------------------------------------
|
|
// Radii and shadows.
|
|
// ---------------------------------------------------------------------------------------------
|
|
{
|
|
description: "Square corners.",
|
|
label: "No radius",
|
|
name: "radius.none",
|
|
value: "0",
|
|
},
|
|
{
|
|
description: "Corner radius for badges and small inline elements.",
|
|
label: "Small radius",
|
|
name: "radius.sm",
|
|
value: "0.25rem",
|
|
},
|
|
{
|
|
description: "Corner radius for controls, inputs, and buttons.",
|
|
label: "Medium radius",
|
|
name: "radius.md",
|
|
value: "0.625rem",
|
|
},
|
|
{
|
|
description: "Corner radius for panels and dialogs.",
|
|
label: "Large radius",
|
|
name: "radius.lg",
|
|
value: "0.875rem",
|
|
},
|
|
{
|
|
description: "Fully rounded ends, for pills and status dots.",
|
|
label: "Pill radius",
|
|
name: "radius.pill",
|
|
value: "9999px",
|
|
},
|
|
{
|
|
description: "No shadow.",
|
|
label: "No shadow",
|
|
name: "shadow.none",
|
|
value: "none",
|
|
},
|
|
{
|
|
description: "Subtle lift for a resting panel.",
|
|
label: "Small shadow",
|
|
name: "shadow.sm",
|
|
value: "0 1px 3px rgba(24, 35, 33, 0.08)",
|
|
},
|
|
{
|
|
description: "Shadow for a popover or a raised surface.",
|
|
label: "Medium shadow",
|
|
name: "shadow.md",
|
|
value: "0 8px 24px rgba(24, 35, 33, 0.12)",
|
|
},
|
|
{
|
|
description: "Shadow for a modal dialog.",
|
|
label: "Large shadow",
|
|
name: "shadow.lg",
|
|
value: "0 18px 48px rgba(24, 35, 33, 0.2)",
|
|
},
|
|
|
|
// ---------------------------------------------------------------------------------------------
|
|
// Control sizing.
|
|
// ---------------------------------------------------------------------------------------------
|
|
{
|
|
description: "Height of a compact control.",
|
|
label: "Small control height",
|
|
name: "control.height-sm",
|
|
value: "2rem",
|
|
},
|
|
{
|
|
description: "Height of an ordinary button, input, or select.",
|
|
label: "Medium control height",
|
|
name: "control.height-md",
|
|
value: "2.5rem",
|
|
},
|
|
{
|
|
description: "Height of a prominent primary control.",
|
|
label: "Large control height",
|
|
name: "control.height-lg",
|
|
value: "3rem",
|
|
},
|
|
{
|
|
description: "Horizontal padding inside a control.",
|
|
label: "Control padding",
|
|
name: "control.padding-inline",
|
|
value: "0.75rem",
|
|
},
|
|
] as const satisfies readonly DesignTokenInput[];
|
|
|
|
/** Every registered token name, as a union — the compile-time half of "modules consume registered
|
|
* tokens and never invent new global theme variables". */
|
|
export type EkklesiaDesignTokenName = (typeof declarations)[number]["name"];
|
|
|
|
/**
|
|
* The one global semantic token registry. Built (and therefore validated) at module evaluation, so
|
|
* an invalid declaration fails where it was written rather than in a browser.
|
|
*/
|
|
export const ekklesiaDesignTokens = createDesignTokenRegistry(declarations);
|
|
|
|
/** `var(--ekklesia-…)` for a registered token. The typed reference component and module code use;
|
|
* an unregistered name is a compile error, and also a runtime `DesignTokenError`. */
|
|
export function designToken(name: EkklesiaDesignTokenName): string {
|
|
return ekklesiaDesignTokens.reference(name);
|
|
}
|
|
|
|
/** The complete default theme as one installable CSS rule. */
|
|
export function ekklesiaDesignTokenStylesheet(scope: DesignTokenScope = "root"): string {
|
|
return ekklesiaDesignTokens.stylesheet(scope);
|
|
}
|