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); }