import { designToken } from "@ekklesia/design-tokens"; /** * Every Ekklesia element's presentation, expressed once as a stylesheet built entirely from the * global semantic tokens. * * ## Why a stylesheet string rather than a `.css` file * * These elements are mounted in two very different places: a document (the Dashboard shell, a * standalone host page) and an isolated shadow root (a module's own UI artifact, a contributed * renderer). A `.css` file would need a bundler on both sides and a served asset on one of them; a * string is installed into whichever root the caller actually has, with the identical bytes. It is * also what lets `:focus-visible`, `:hover`, and `:disabled` be real CSS rather than JavaScript * approximations of them — which is the difference between a focus treatment that works for a * keyboard user and one that only looks like it does. * * ## Every value here is a token reference * * There is no literal colour, font stack, radius, or shadow below: each is `var(--ekklesia-…)` * resolved from the registry a host installs. That is enforced by a test, not by review, so a * hardcoded brand colour cannot quietly enter the shared elements and make a future theme editor a * lie. Structural values that carry no theme meaning — a `0`, a `100%`, a `flex` — stay literal on * purpose: they are layout, not theme. */ export const EKKLESIA_UI_STYLESHEET = ` .ek-root { color: ${designToken("text.primary")}; font-family: ${designToken("typography.family-sans")}; font-size: ${designToken("typography.size-md")}; line-height: ${designToken("typography.line-height-normal")}; } .ek-stack { display: grid; gap: ${designToken("spacing.md")}; } .ek-focusable:focus-visible { outline: ${designToken("state.focus-visible-width")} solid ${designToken("accent.focus")}; outline-offset: ${designToken("state.focus-visible-offset")}; } .ek-button { align-items: center; border: ${designToken("border.width")} solid transparent; border-radius: ${designToken("radius.md")}; cursor: pointer; display: inline-flex; font-family: inherit; font-size: ${designToken("typography.size-sm")}; font-weight: ${designToken("typography.weight-medium")}; gap: ${designToken("spacing.sm")}; height: ${designToken("control.height-md")}; justify-content: center; line-height: ${designToken("typography.line-height-tight")}; padding-inline: ${designToken("control.padding-inline")}; } .ek-button--sm { font-size: ${designToken("typography.size-xs")}; height: ${designToken("control.height-sm")}; } .ek-button--lg { font-size: ${designToken("typography.size-md")}; height: ${designToken("control.height-lg")}; } .ek-button--primary { background: ${designToken("accent.base")}; color: ${designToken("accent.foreground")}; } .ek-button--primary:hover:not(:disabled) { background: ${designToken("accent.strong")}; } .ek-button--primary:active:not(:disabled) { background: ${designToken("accent.strong")}; } .ek-button--secondary { background: ${designToken("background.surface")}; border-color: ${designToken("border.base")}; color: ${designToken("text.primary")}; } .ek-button--secondary:hover:not(:disabled) { background: ${designToken("state.hover-background")}; } .ek-button--secondary:active:not(:disabled) { background: ${designToken("state.pressed-background")}; } .ek-button--quiet { background: transparent; color: ${designToken("text.secondary")}; } .ek-button--quiet:hover:not(:disabled) { background: ${designToken("state.hover-background")}; color: ${designToken("text.primary")}; } .ek-button--quiet:active:not(:disabled) { background: ${designToken("state.pressed-background")}; } .ek-button--danger { background: ${designToken("status.danger-background")}; border-color: ${designToken("status.danger-border")}; color: ${designToken("text.danger")}; } .ek-button--danger:hover:not(:disabled) { border-color: ${designToken("text.danger")}; } .ek-button[aria-pressed="true"] { background: ${designToken("state.selected-background")}; color: ${designToken("state.selected-foreground")}; } .ek-button:disabled { cursor: not-allowed; opacity: ${designToken("state.disabled-opacity")}; } .ek-icon-button { aspect-ratio: 1; padding-inline: 0; width: ${designToken("control.height-md")}; } .ek-field { display: grid; gap: ${designToken("spacing.xs")}; } .ek-field__label { color: ${designToken("text.primary")}; font-size: ${designToken("typography.size-sm")}; font-weight: ${designToken("typography.weight-medium")}; } .ek-field__hint { color: ${designToken("text.muted")}; font-size: ${designToken("typography.size-xs")}; } .ek-field__error { color: ${designToken("text.danger")}; font-size: ${designToken("typography.size-xs")}; } .ek-field__error::before { content: ${designToken("status.danger-symbol")}; margin-inline-end: ${designToken("spacing.xs")}; } .ek-control { background: ${designToken("background.inset")}; border: ${designToken("border.width")} solid ${designToken("border.base")}; border-radius: ${designToken("radius.md")}; color: ${designToken("text.primary")}; font-family: inherit; font-size: ${designToken("typography.size-sm")}; height: ${designToken("control.height-md")}; padding-inline: ${designToken("control.padding-inline")}; width: 100%; } .ek-control:hover:not(:disabled) { border-color: ${designToken("border.strong")}; } .ek-control::placeholder { color: ${designToken("text.muted")}; } .ek-control:disabled { cursor: not-allowed; opacity: ${designToken("state.disabled-opacity")}; } .ek-control[aria-invalid="true"] { border-color: ${designToken("text.danger")}; } .ek-checkbox { align-items: center; color: ${designToken("text.primary")}; display: inline-flex; font-size: ${designToken("typography.size-sm")}; gap: ${designToken("spacing.sm")}; } .ek-checkbox__input { accent-color: ${designToken("accent.base")}; height: ${designToken("spacing.lg")}; width: ${designToken("spacing.lg")}; } .ek-checkbox__input:disabled { cursor: not-allowed; opacity: ${designToken("state.disabled-opacity")}; } .ek-panel { background: ${designToken("background.surface")}; border: ${designToken("border.width")} solid ${designToken("border.base")}; border-radius: ${designToken("radius.lg")}; box-shadow: ${designToken("shadow.sm")}; display: grid; gap: ${designToken("spacing.md")}; padding: ${designToken("spacing.lg")}; } .ek-panel--raised { background: ${designToken("background.raised")}; box-shadow: ${designToken("shadow.md")}; } .ek-panel__header { align-items: baseline; display: flex; gap: ${designToken("spacing.md")}; justify-content: space-between; } .ek-panel__title { font-size: ${designToken("typography.size-heading-3")}; font-weight: ${designToken("typography.weight-bold")}; line-height: ${designToken("typography.line-height-tight")}; margin: 0; } .ek-panel__description { color: ${designToken("text.secondary")}; font-size: ${designToken("typography.size-sm")}; margin: 0; } .ek-dialog { background: ${designToken("background.raised")}; border: ${designToken("border.width")} solid ${designToken("border.base")}; border-radius: ${designToken("radius.lg")}; box-shadow: ${designToken("shadow.lg")}; color: ${designToken("text.primary")}; display: grid; gap: ${designToken("spacing.lg")}; max-width: 32rem; padding: ${designToken("spacing.xl")}; width: 100%; } .ek-dialog__scrim { align-items: center; background: ${designToken("background.overlay")}; display: flex; inset: 0; justify-content: center; padding: ${designToken("spacing.lg")}; position: fixed; } .ek-dialog__title { font-size: ${designToken("typography.size-heading-2")}; font-weight: ${designToken("typography.weight-bold")}; line-height: ${designToken("typography.line-height-tight")}; margin: 0; } .ek-dialog__actions { display: flex; gap: ${designToken("spacing.sm")}; justify-content: flex-end; } .ek-tabs__list { border-bottom: ${designToken("border.width")} solid ${designToken("border.divider")}; display: flex; gap: ${designToken("spacing.xs")}; } .ek-tab { background: transparent; border: 0; border-bottom: ${designToken("state.focus-visible-width")} solid transparent; color: ${designToken("text.secondary")}; cursor: pointer; font-family: inherit; font-size: ${designToken("typography.size-sm")}; font-weight: ${designToken("typography.weight-medium")}; padding: ${designToken("spacing.sm")} ${designToken("spacing.md")}; } .ek-tab:hover { background: ${designToken("state.hover-background")}; color: ${designToken("text.primary")}; } .ek-tab[aria-selected="true"] { border-bottom-color: ${designToken("accent.base")}; color: ${designToken("state.selected-foreground")}; } .ek-tabs__panel { padding-block-start: ${designToken("spacing.lg")}; } .ek-table { border-collapse: collapse; font-size: ${designToken("typography.size-sm")}; width: 100%; } .ek-table th, .ek-table td { border-bottom: ${designToken("border.width")} solid ${designToken("border.divider")}; padding: ${designToken("spacing.sm")} ${designToken("spacing.md")}; text-align: start; } .ek-table th { color: ${designToken("text.secondary")}; font-weight: ${designToken("typography.weight-medium")}; } .ek-table tbody tr:hover { background: ${designToken("state.hover-background")}; } .ek-table tbody tr[aria-selected="true"] { background: ${designToken("state.selected-background")}; color: ${designToken("state.selected-foreground")}; } .ek-list { display: grid; gap: ${designToken("spacing.xs")}; list-style: none; margin: 0; padding: 0; } .ek-list__item { background: ${designToken("background.surface")}; border: ${designToken("border.width")} solid ${designToken("border.divider")}; border-radius: ${designToken("radius.md")}; padding: ${designToken("spacing.sm")} ${designToken("spacing.md")}; } .ek-badge { align-items: center; border: ${designToken("border.width")} solid transparent; border-radius: ${designToken("radius.pill")}; display: inline-flex; font-size: ${designToken("typography.size-xs")}; font-weight: ${designToken("typography.weight-medium")}; gap: ${designToken("spacing.xs")}; padding: ${designToken("spacing.2xs")} ${designToken("spacing.sm")}; } .ek-badge--neutral { background: ${designToken("background.inset")}; border-color: ${designToken("border.base")}; color: ${designToken("text.secondary")}; } .ek-badge--info { background: ${designToken("status.info-background")}; border-color: ${designToken("status.info-border")}; color: ${designToken("accent.strong")}; } .ek-badge--success { background: ${designToken("status.success-background")}; border-color: ${designToken("status.success-border")}; color: ${designToken("text.success")}; } .ek-badge--warning { background: ${designToken("status.warning-background")}; border-color: ${designToken("status.warning-border")}; color: ${designToken("text.warning")}; } .ek-badge--danger { background: ${designToken("status.danger-background")}; border-color: ${designToken("status.danger-border")}; color: ${designToken("text.danger")}; } .ek-badge__symbol { font-weight: ${designToken("typography.weight-bold")}; } .ek-toast { background: ${designToken("background.raised")}; border: ${designToken("border.width")} solid ${designToken("border.base")}; border-inline-start-width: ${designToken("spacing.xs")}; border-radius: ${designToken("radius.md")}; box-shadow: ${designToken("shadow.md")}; display: grid; gap: ${designToken("spacing.2xs")}; padding: ${designToken("spacing.md")}; } .ek-toast--success { border-inline-start-color: ${designToken("status.success-border")}; } .ek-toast--warning { border-inline-start-color: ${designToken("status.warning-border")}; } .ek-toast--danger { border-inline-start-color: ${designToken("status.danger-border")}; } .ek-toast--info { border-inline-start-color: ${designToken("status.info-border")}; } .ek-toast__title { font-size: ${designToken("typography.size-sm")}; font-weight: ${designToken("typography.weight-bold")}; } .ek-toast__body { color: ${designToken("text.secondary")}; font-size: ${designToken("typography.size-sm")}; } .ek-toast__stack { display: grid; gap: ${designToken("spacing.sm")}; } .ek-message { align-items: center; border: ${designToken("border.width")} dashed ${designToken("border.base")}; border-radius: ${designToken("radius.lg")}; display: grid; gap: ${designToken("spacing.sm")}; justify-items: center; padding: ${designToken("spacing.xl")}; text-align: center; } .ek-message__title { font-size: ${designToken("typography.size-lg")}; font-weight: ${designToken("typography.weight-bold")}; margin: 0; } .ek-message__body { color: ${designToken("text.secondary")}; font-size: ${designToken("typography.size-sm")}; margin: 0; max-width: 40ch; } .ek-message--error { border-color: ${designToken("status.danger-border")}; border-style: solid; } .ek-message--error .ek-message__title { color: ${designToken("text.danger")}; } .ek-message--error .ek-message__title::before { content: ${designToken("status.danger-symbol")}; margin-inline-end: ${designToken("spacing.xs")}; } .ek-message--loading { border-style: solid; border-color: ${designToken("border.divider")}; } .ek-spinner { animation: ek-spin 1s linear infinite; border: ${designToken("state.focus-visible-width")} solid ${designToken("border.base")}; border-block-start-color: ${designToken("accent.base")}; border-radius: ${designToken("radius.pill")}; height: ${designToken("spacing.xl")}; width: ${designToken("spacing.xl")}; } @keyframes ek-spin { to { transform: rotate(360deg); } } @media (prefers-reduced-motion: reduce) { .ek-spinner { animation: none; } } `; /** Marks a stylesheet this package installed, so installation is idempotent per root. */ export const EKKLESIA_UI_STYLE_ATTRIBUTE = "data-ekklesia-ui"; /** A document or a shadow root — the two places these elements are ever mounted. Structural rather * than nominal for the same reason the token installer's root type is. */ export interface EkklesiaUiStyleRoot { querySelector(selectors: string): Element | null; appendChild(node: Node_): Node_; readonly ownerDocument?: Document | null; } function documentFor(root: EkklesiaUiStyleRoot): Document | undefined { if (typeof (root as Partial).createElement === "function") { return root as Document; } return root.ownerDocument ?? undefined; } /** * Installs the element stylesheet into one root, once, and returns a remover for exactly what it * inserted. * * The React equivalent is ``, which is what an artifact renders so its styles * land inside its own isolated root rather than in the host's document. This imperative form exists * for a host that owns a document directly. */ export function installEkklesiaUiStyles(root: EkklesiaUiStyleRoot): () => void { if (root.querySelector(`style[${EKKLESIA_UI_STYLE_ATTRIBUTE}]`)) { return () => undefined; } const ownerDocument = documentFor(root); if (!ownerDocument) { return () => undefined; } const style = ownerDocument.createElement("style"); style.setAttribute(EKKLESIA_UI_STYLE_ATTRIBUTE, ""); style.textContent = EKKLESIA_UI_STYLESHEET; const target = (ownerDocument === (root as unknown as Document) ? ownerDocument.head : undefined) ?? root; target.appendChild(style); let removed = false; return () => { if (removed) { return; } removed = true; style.remove(); }; }