Skip to content
Domphy

Typography

Index of the text-level and block-level typography patches. There is no typography() factory — apply the patch that matches the HTML tag.

Patches

PatchElementDescription
heading()h1–h6Scales font size per heading level (size can skip the tag bump)
paragraph()pLine height and spacing for body text
link()aColor, hover underline, and disabled state
strong()strongBold inline emphasis
emphasis()emItalic inline emphasis
small()smallReduced font size for fine print
subscript()subLowered baseline text (e.g. H₂O)
superscript()supRaised baseline text (e.g. x²)
abbreviation()abbrDotted underline with tooltip via title
mark()markHighlighted background
code()codeInline monospace code
keyboard()kbdKeyboard shortcut styling
blockquote()blockquoteIndented quote block with accent left edge
preformated()preMonospace preformatted block
orderedList()olNumbered list
unorderedList()ulBulleted list
descriptionList()dlTerm/description pairs
Customization

Must see the source of patch at the bottom of each patch page to understand the structure then code it still code as html native element.

There are four levels of customization, in increasing order of effort:

  1. Patch props. Each patch exposes a small, stable set of props—typically fewer than five. Lowest friction.
  2. Context attributes. Use dataTone, dataSize, and dataDensity on a container to shift tone, size, or density for an entire subtree without touching individual elements.
  3. Inline override. Native-wins merge strategy: any property set directly on the element overrides the patch value.
  4. Create a variant. Clone a similar patch and edit it. Use this only when you need a reusable custom version.
import { type PartialElement, toState, type ValueOrState } from "@domphy/core";
import {
  type ElementSize,
  type ThemeColor,
  themeColor,
  themeSize,
  themeSpacing,
} from "@domphy/theme";

const HeadingShift: Record<string, ElementSize> = {
  h6: "decrease-1",
  h5: "inherit",
  h4: "increase-1",
  h3: "increase-2",
  h2: "increase-3",
  h1: "increase-4",
};

/**
 * Styles a heading, scaling its font size by level (h1 largest … h6 smallest)
 * relative to the theme base size. Apply to a heading element `<h1>`–`<h6>`.
 *
 * @hostTag h1-h6
 * @param props.color - Theme color tone (`ValueOrState<ThemeColor>`) for the heading text. Defaults to "neutral".
 * @param props.size - Optional `ElementSize` (`"inherit"` | `"increase-N"` | `"decrease-N"`, N ≤ 7). When set, font size is `themeSize(listener, size)` including `"inherit"` (no tag bump). When omitted, follows the host tag (`h1` increase-4 … `h6` decrease-1).
 * @example { h2: "Section title", $: [heading()] }
 * @example { h3: "Panel title", $: [heading({ size: "inherit" })] }
 */
function heading(
  props: { color?: ValueOrState<ThemeColor>; size?: ElementSize } = {},
): PartialElement {
  const color = toState(props.color ?? "neutral", "color");
  const size = props.size;

  return {
    _onInsert: (node) => {
      if (!["h1", "h2", "h3", "h4", "h5", "h6"].includes(node.tagName)) {
        console.warn(
          `"heading" primitive patch must use heading tags [h1...h6]`,
        );
      }
    },
    style: {
      color: (listener) =>
        themeColor(listener, "shift-11", color.get(listener)),
      marginTop: 0,
      marginBottom: themeSpacing(2),
      fontSize: (listener) => {
        if (size !== undefined) {
          return themeSize(listener, size);
        }
        const offset = HeadingShift[listener.elementNode.tagName] || "inherit";
        return themeSize(listener, offset);
      },
    },
  };
}

export { heading };
import { type PartialElement, toState, type ValueOrState } from "@domphy/core";
import { type ThemeColor, themeColor, themeSize } from "@domphy/theme";

/**
 * Themed paragraph primitive: comfortable line-height, reset margins and themed
 * text color. Apply to a `<p>` element.
 *
 * @hostTag p
 * @param props - Optional configuration.
 * @param props.color - Color tone for the paragraph text. Defaults to `"neutral"`.
 * @example { p: "Hello world", $: [paragraph()] }
 */
function paragraph(
  props: { color?: ValueOrState<ThemeColor> } = {},
): PartialElement {
  const color = toState(props.color ?? "neutral", "color");

  return {
    _onInsert: (node) => {
      if (node.tagName !== "p") {
        console.warn(`"paragraph" primitive patch must use p tag`);
      }
    },
    style: {
      fontSize: (listener) => themeSize(listener, "inherit"),
      color: (listener) => themeColor(listener, "text", color.get(listener)),
      lineHeight: 1.5,
      marginTop: 0,
      marginBottom: 0,
    },
  };
}

export { paragraph };
import { type PartialElement, toState, type ValueOrState } from "@domphy/core";
import {
  type ThemeColor,
  themeColor,
  themeSize,
  themeSpacing,
} from "@domphy/theme";
import { focusRing } from "../utils/focusRing.js";

/**
 * Themed hyperlink primitive: styles text color, hover underline, visited,
 * focus ring and a disabled state. Apply to an `<a>` element.
 *
 * @hostTag a
 * @param props - Optional configuration.
 * @param props.color - Base color tone for the link text. Defaults to `"primary"`.
 * @param props.accentColor - Accent color tone for visited/focus states. Defaults to `"secondary"`.
 * @example { a: "Home", href: "/", $: [link()] }
 */
function link(
  props: {
    color?: ValueOrState<ThemeColor>;
    accentColor?: ValueOrState<ThemeColor>;
  } = {},
): PartialElement {
  const color = toState(props.color ?? "primary", "color");
  const accentColor = toState(props.accentColor ?? "secondary", "accentColor");
  return {
    // WAI-ARIA APG: an <a> with no `href` is not a native link — it is not
    // focusable and Enter/Space do nothing, even though it visually looks
    // and (via a caller's own onClick) behaves like one. Declared ON by
    // default (satisfies both APG for the scripted/no-href case AND, since
    // it is a plain declared value doctor's `missing-required-attribute`
    // rule can see, closes the "onClick with no href/role" diagnostic) and
    // corrected away in `_onInsert` once the real host attributes are known
    // — `href` lives on the HOST, not this patch, so its presence can't be
    // read until the merge completes. (A reactive `(listener) => …`
    // attribute function does NOT work for this: `attributes.has()` is a
    // plain imperative read, not a tracked reactive dependency, so it froze
    // at whatever `href` had been set to at the function's first,
    // merge-order-dependent evaluation — measured `false` even for a host
    // that DOES declare `href: "/"`.)
    role: "link",
    tabIndex: 0,
    _onInsert: (node) => {
      if (node.tagName !== "a") {
        console.warn(`"link" primitive patch must use a tag`);
      }
      // A real `href` makes both a no-op: `<a href>` is already natively
      // focusable with an implicit role="link" — strip the redundant pair
      // rather than leave a harmless-but-confusing explicit restatement.
      // Only strip THIS patch's own default, never a caller's native
      // override: native wins over patch (AGENTS.md merge rule), so by the
      // time `_onInsert` runs, `role` already holds the caller's value if
      // one was declared (e.g. a search result row using `$: [link()]` but
      // declaring its own `role: "option"` for a listbox) — removing it
      // unconditionally here silently threw that override away.
      if (node.attributes.has("href")) {
        if (node.attributes.get("role") === "link") {
          node.attributes.remove("role");
        }
        if (node.attributes.get("tabIndex") === 0) {
          node.attributes.remove("tabIndex");
        }
      }
    },
    // `<a disabled>` is not a native navigation block — stop click/Enter.
    onClick: (event, node) => {
      const el = node.domElement;
      if (
        el?.hasAttribute("disabled") ||
        el?.getAttribute("aria-disabled") === "true"
      ) {
        event.preventDefault();
      }
    },
    onKeyDown: (event, node) => {
      if (node.attributes.has("href")) return;
      const key = (event as KeyboardEvent).key;
      if (key !== "Enter" && key !== " ") return;
      event.preventDefault();
      node.domElement?.click();
    },
    style: {
      fontSize: (listener) => themeSize(listener, "inherit"),
      backgroundColor: (listener) => themeColor(listener),
      // shift-13 (not "text"/shift-9): brand hues need extra depth for ≥4.5:1
      // on a light surface (primary orange failed at 2.57 with shift-9).
      color: (listener) =>
        themeColor(listener, "shift-13", color.get(listener)),
      textDecoration: "none",
      "&:visited": {
        color: (listener) =>
          themeColor(listener, "shift-13", accentColor.get(listener)),
      },
      "&:hover:not([disabled])": {
        color: (listener) =>
          themeColor(listener, "shift-14", color.get(listener)),
        textDecoration: "underline",
      },
      borderRadius: themeSpacing(1),
      // Kill the browser default outline so only the shared focusRing shows.
      outline: "none",
      transition: "color 140ms ease, box-shadow 140ms ease",
      "&:focus-visible": {
        boxShadow: (listener) => focusRing(listener, accentColor.get(listener)),
      },
      // `disabled` is not a valid attribute on `<a>`; WAI-ARIA's disabled link
      // is `role="link"` + `aria-disabled="true"` (focusable, not operable).
      // Both selectors are styled so either spelling gets the same look.
      "&[disabled], &[aria-disabled=true]": {
        opacity: 0.7,
        cursor: "not-allowed",
        pointerEvents: "none",
        color: (listener) => themeColor(listener, "muted", "neutral"),
      },
    },
  };
}

export { link };
import { type PartialElement, toState, type ValueOrState } from "@domphy/core";
import {
  type ThemeColor,
  themeColor,
  themeSize,
  themeWeight,
} from "@domphy/theme";

/**
 * Styles strongly emphasized (bold) text: inherited font size, the theme's `bold` weight, and a
 * themed foreground color.
 *
 * @hostTag strong
 * @param props.color - Theme color tone for the text. Accepts a value or reactive state.
 *   Defaults to `"neutral"`.
 * @example { strong: "important", $: [strong()] }
 */
function strong(
  props: { color?: ValueOrState<ThemeColor> } = {},
): PartialElement {
  const color = toState(props.color ?? "neutral", "color");

  return {
    _onInsert: (node) => {
      if (node.tagName !== "strong") {
        console.warn(`"strong" primitive patch must use strong tag`);
      }
    },
    style: {
      fontSize: (listener) => themeSize(listener, "inherit"),
      fontWeight: themeWeight("bold"),
      color: (listener) =>
        themeColor(listener, "shift-11", color.get(listener)),
    },
  };
}

export { strong };
import { type PartialElement, toState, type ValueOrState } from "@domphy/core";
import { type ThemeColor, themeColor, themeSize } from "@domphy/theme";

/**
 * Italic emphasized inline text. Apply to an `<em>` element.
 *
 * @hostTag em
 * @param props.color - Theme color tone (`ValueOrState<ThemeColor>`) for the text. Defaults to "neutral".
 * @example { em: "important", $: [emphasis()] }
 */
function emphasis(
  props: { color?: ValueOrState<ThemeColor> } = {},
): PartialElement {
  const color = toState(props.color ?? "neutral", "color");

  return {
    _onInsert: (node) => {
      if (node.tagName !== "em") {
        console.warn(`"emphasis" primitive patch must use em tag`);
      }
    },
    style: {
      fontSize: (listener) => themeSize(listener, "inherit"),
      fontStyle: "italic",
      color: (listener) =>
        themeColor(listener, "shift-10", color.get(listener)),
    },
  };
}

export { emphasis };
import { type PartialElement, toState, type ValueOrState } from "@domphy/core";
import { type ThemeColor, themeColor, themeSize } from "@domphy/theme";

/**
 * Styles small/secondary text: one step smaller font size (`data-size="decrease-1"`) with a
 * themed foreground color.
 *
 * The tone is `shift-10`, one step ABOVE the `"text"` floor, because this text renders below
 * the WCAG large-text threshold and so must always clear 4.5:1. Measured in Chromium on the
 * built bundle: 6.01–7.73:1 (light) and 6.33–7.73:1 (dark) on every edge-anchored surface.
 * Three things at the CALL SITE forfeit that guarantee, all measured in Chromium:
 * 1. A host-declared `style.color` wins over the patch (native beats patch) — `"text"` /
 *    `shift-9` on a `shift-1` surface is 4.23:1. Pass `color` for a different family
 *    instead of restyling.
 * 2. An ancestor's scoped `"& small": { color: … }` also wins: that descendant selector is
 *    specificity (0,1,1) and this patch's own generated class is only (0,1,0). Measured
 *    inside a correctly tone-anchored `dataTone: "shift-1"` card, with no inline style on
 *    the `<small>` at all: `& small { color: themeColor(l, "shift-8") }` resolves to
 *    `var(--neutral-9)` = #707070 on the card's #ededed = 4.23:1 (6.27:1 once the `color`
 *    line is dropped). The specificity is deliberately NOT escalated here — a patch that
 *    outranked descendant rules would leave an author no way to restyle short of
 *    `!important`. Scope layout there (`display: block`) and leave the colour to the patch.
 * 3. A surface tinted with a fixed `themeColor(l, "shift-N")` background and no `dataTone`:
 *    every child still resolves against the page root, and even `shift-10` drops to 4.34:1
 *    (shift-2 tint) and 3.67:1 (shift-3). Shift the surface with `dataTone` + `"inherit"`
 *    instead — `@domphy/doctor`'s `tone-background-inherit` rule flags exactly this.
 *
 * @hostTag small
 * @param props.color - Theme color tone for the text. Accepts a value or reactive state.
 *   Defaults to `"neutral"`.
 * @example { small: "fine print", $: [small()] }
 */
function small(
  props: { color?: ValueOrState<ThemeColor> } = {},
): PartialElement {
  const color = toState(props.color ?? "neutral", "color");

  return {
    _onInsert: (node) => {
      if (node.tagName !== "small") {
        console.warn('"small" patch must use small tag');
      }
    },
    dataSize: "decrease-1",
    style: {
      fontSize: (listener) => themeSize(listener, "inherit"),
      // shift-10 (not muted/shift-8) keeps WCAG AA ≥4.5:1 on light surfaces for small type.
      color: (listener) =>
        themeColor(listener, "shift-10", color.get(listener)),
    },
  };
}

export { small };
import { type PartialElement, toState, type ValueOrState } from "@domphy/core";
import { type ThemeColor, themeColor, themeSize } from "@domphy/theme";

/**
 * Renders subscript text (shrunk, baseline-lowered) for the host `<sub>` element.
 *
 * @hostTag sub
 * @param props.color - Theme color for the text. Optional, accepts a value or state. Defaults to `"neutral"`.
 * @example { sub: "2", $: [subscript()] }
 */
function subscript(
  props: { color?: ValueOrState<ThemeColor> } = {},
): PartialElement {
  const color = toState(props.color ?? "neutral", "color");

  return {
    _onInsert: (node) => {
      if (node.tagName !== "sub") {
        console.warn(`"subscript" primitive patch must use sub tag`);
      }
    },
    style: {
      fontSize: (listener) => themeSize(listener, "decrease-1"),
      verticalAlign: "sub",
      lineHeight: 0,
      color: (listener) => themeColor(listener, "text", color.get(listener)),
    },
  };
}

export { subscript };
import { type PartialElement, toState, type ValueOrState } from "@domphy/core";
import { type ThemeColor, themeColor, themeSize } from "@domphy/theme";

/**
 * Renders superscript text (shrunk, baseline-raised) for the host `<sup>` element.
 *
 * @hostTag sup
 * @param props.color - Theme color for the text. Optional, accepts a value or state. Defaults to `"neutral"`.
 * @example { sup: "2", $: [superscript()] }
 */
function superscript(
  props: { color?: ValueOrState<ThemeColor> } = {},
): PartialElement {
  const color = toState(props.color ?? "neutral", "color");

  return {
    _onInsert: (node) => {
      if (node.tagName !== "sup") {
        console.warn(`"superscript" primitive patch must use sup tag`);
      }
    },
    style: {
      fontSize: (listener) => themeSize(listener, "decrease-1"),
      verticalAlign: "super",
      lineHeight: 0,
      color: (listener) => themeColor(listener, "text", color.get(listener)),
    },
  };
}

export { superscript };
import { type PartialElement, toState, type ValueOrState } from "@domphy/core";
import {
  type ThemeColor,
  themeColor,
  themeSize,
  themeSpacing,
} from "@domphy/theme";

/**
 * Styles an abbreviation/acronym with a dotted underline and a "help" cursor,
 * shifting to the accent color on hover. Apply to an `<abbr>` element.
 *
 * @hostTag abbr
 * @param props.color - Base text/decoration color tone. Optional `ValueOrState<ThemeColor>`, default "neutral".
 * @param props.accentColor - Hover color tone. Optional `ValueOrState<ThemeColor>`, default "primary".
 * @example { abbr: "HTML", title: "HyperText Markup Language", $: [abbreviation({ accentColor: "primary" })] }
 */
function abbreviation(
  props: {
    color?: ValueOrState<ThemeColor>;
    accentColor?: ValueOrState<ThemeColor>;
  } = {},
): PartialElement {
  const color = toState(props.color ?? "neutral", "color");
  const accentColor = toState(props.accentColor ?? "primary", "accentColor");

  return {
    _onInsert: (node) => {
      if (node.tagName !== "abbr") {
        console.warn(`"abbreviation" primitive patch must use abbr tag`);
      }
    },
    style: {
      fontSize: (listener) => themeSize(listener),
      backgroundColor: (listener) => themeColor(listener),
      color: (listener) =>
        themeColor(listener, "shift-10", color.get(listener)),
      textDecorationLine: "underline",
      textDecorationStyle: "dotted",
      textDecorationColor: (listener) =>
        themeColor(listener, "shift-7", color.get(listener)),
      textUnderlineOffset: themeSpacing(0.72),
      cursor: "help",
      "&:hover": {
        color: (listener) =>
          themeColor(listener, "shift-11", accentColor.get(listener)),
        textDecorationColor: (listener) =>
          themeColor(listener, "shift-9", accentColor.get(listener)),
      },
    },
  };
}

export { abbreviation };
import { type PartialElement, toState, type ValueOrState } from "@domphy/core";
import {
  type ThemeColor,
  themeColor,
  themeDensity,
  themeSize,
  themeSpacing,
} from "@domphy/theme";

/**
 * Themed highlight primitive: gives marked/highlighted inline text a tinted
 * background, rounded corners and padding. Apply to a `<mark>` element.
 *
 * @hostTag mark
 * @param props - Optional configuration.
 * @param props.accentColor - Accent color tone for the highlight fill and text. Defaults to `"highlight"`.
 * @example { mark: "important", $: [mark()] }
 */
function mark(
  props: { accentColor?: ValueOrState<ThemeColor> } = {},
): PartialElement {
  const accentColor = toState(props.accentColor ?? "highlight", "accentColor");

  return {
    _onInsert: (node) => {
      if (node.tagName !== "mark") {
        console.warn(`"mark" primitive patch must use mark tag`);
      }
    },
    dataTone: "shift-2",
    style: {
      display: "inline-flex",
      alignItems: "center",
      fontSize: (listener) => themeSize(listener, "inherit"),
      color: (listener) =>
        themeColor(listener, "text", accentColor.get(listener)),
      backgroundColor: (listener) =>
        themeColor(listener, "inherit", accentColor.get(listener)),
      // A chip inside running text keeps its own compact line box: an inherited
      // line-height (1.8, say) would otherwise overflow the fixed height.
      lineHeight: "normal",
      // Bare themeSpacing(U) at snapshot time; n = U / 1.5 (light.densities[2]).
      // minHeight, not height: at a small density the chip must still grow to
      // its own line box rather than clip the text.
      minHeight: (listener) => themeSpacing(themeDensity(listener) * (6 / 1.5)),
      borderRadius: (listener) =>
        themeSpacing(themeDensity(listener) * (1 / 1.5)),
      paddingInline: (listener) =>
        themeSpacing(themeDensity(listener) * (1.5 / 1.5)),
    },
  };
}

export { mark };
import { type PartialElement, toState, type ValueOrState } from "@domphy/core";
import {
  type ThemeColor,
  themeColor,
  themeDensity,
  themeFont,
  themeSize,
  themeSpacing,
} from "@domphy/theme";

/**
 * Styles an inline code snippet in the theme's monospace stack, with a subtle
 * surface background, rounded corners, and shifted tone. Apply to a `<code>` element.
 *
 * @hostTag code
 * @param props.color - Surface/text color tone. Optional `ValueOrState<ThemeColor>`, default "neutral".
 * @example { code: "npm install", $: [code({ color: "neutral" })] }
 */
function code(
  props: { color?: ValueOrState<ThemeColor> } = {},
): PartialElement {
  const color = toState(props.color ?? "neutral", "color");

  return {
    dataTone: "shift-2",
    _onInsert: (node) => {
      if (node.tagName !== "code") {
        console.warn(`"code" primitive patch must use code tag`);
      }
    },
    style: {
      display: "inline-flex",
      alignItems: "center",
      whiteSpace: "nowrap",
      fontSize: (listener) => themeSize(listener, "inherit"),
      // The theme owns the code stack; <code>/<kbd>/<pre> otherwise keep the
      // UA generic "monospace" (Courier New on Windows), so a theme that swaps
      // fontFamilies.monospace never reaches them.
      fontFamily: themeFont("monospace"),
      color: (listener) => themeColor(listener, "text", color.get(listener)),
      backgroundColor: (listener) =>
        themeColor(listener, "inherit", color.get(listener)),
      // A chip inside running text keeps its own compact line box: an inherited
      // line-height (1.8, say) would otherwise overflow the fixed height.
      lineHeight: "normal",
      // Bare themeSpacing(U) at snapshot time; n = U / 1.5 (light.densities[2]).
      // minHeight, not height: at a small density the chip must still grow to
      // its own line box rather than clip the text.
      minHeight: (listener) => themeSpacing(themeDensity(listener) * (6 / 1.5)),
      paddingInline: (listener) =>
        themeSpacing(themeDensity(listener) * (1.5 / 1.5)),
      borderRadius: (listener) =>
        themeSpacing(themeDensity(listener) * (1 / 1.5)),
    },
  };
}

export { code };
import { type PartialElement, toState, type ValueOrState } from "@domphy/core";
import {
  type ThemeColor,
  themeColor,
  themeDensity,
  themeFont,
  themeSize,
  themeSpacing,
} from "@domphy/theme";

/**
 * Renders keyboard-key styling (the theme's monospace stack, themed background,
 * border and padding) for a keystroke hint. Apply to a `<kbd>` element.
 *
 * @hostTag kbd
 * @param props - Optional configuration.
 * @param props.color - Color tone for text/background/border. Defaults to `"neutral"`.
 * @example { kbd: "Ctrl", $: [keyboard()] }
 */
function keyboard(
  props: { color?: ValueOrState<ThemeColor> } = {},
): PartialElement {
  const color = toState(props.color ?? "neutral", "color");

  return {
    _onInsert: (node) => {
      if (node.tagName !== "kbd") {
        console.warn(`"keyboard" primitive patch must use kbd tag`);
      }
    },
    style: {
      fontSize: (listener) => themeSize(listener, "inherit"),
      // The theme owns the code stack; <code>/<kbd>/<pre> otherwise keep the
      // UA generic "monospace" (Courier New on Windows), so a theme that swaps
      // fontFamilies.monospace never reaches them.
      fontFamily: themeFont("monospace"),
      color: (listener) => themeColor(listener, "text", color.get(listener)),
      backgroundColor: (listener) =>
        themeColor(listener, "inherit", color.get(listener)),
      // Bare themeSpacing(U) at snapshot time; n = U / 1.5 (light.densities[2]).
      paddingBlock: (listener) =>
        themeSpacing(themeDensity(listener) * (0.5 / 1.5)),
      paddingInline: (listener) =>
        themeSpacing(themeDensity(listener) * (1.5 / 1.5)),
      borderRadius: (listener) =>
        themeSpacing(themeDensity(listener) * (1 / 1.5)),
      outline: (listener) =>
        `1px solid ${themeColor(listener, "border-strong", color.get(listener))}`,
    },
  };
}

export { keyboard };
import { type PartialElement, toState, type ValueOrState } from "@domphy/core";
import {
  type ThemeColor,
  themeColor,
  themeDensity,
  themeSize,
  themeSpacing,
} from "@domphy/theme";

/**
 * Styles a quotation block with a colored inset side bar, padded surface, and
 * shifted tone. Apply to a `<blockquote>` element.
 *
 * @hostTag blockquote
 * @param props.color - Surface/bar color tone. Optional `ValueOrState<ThemeColor>`, default "neutral".
 * @example { blockquote: "Design is how it works.", $: [blockquote({ color: "primary" })] }
 */
function blockquote(
  props: { color?: ValueOrState<ThemeColor> } = {},
): PartialElement {
  const color = toState(props.color ?? "neutral", "color");

  return {
    _onInsert: (node) => {
      if (node.tagName !== "blockquote") {
        console.warn(`"blockquote" primitive patch must use blockquote tag`);
      }
    },
    dataTone: "shift-2",
    style: {
      fontSize: (listener) => themeSize(listener, "inherit"),
      backgroundColor: (listener) =>
        themeColor(listener, "inherit", color.get(listener)),
      color: (listener) => themeColor(listener, "text", color.get(listener)),
      boxShadow: (listener) =>
        `inset ${themeSpacing(1)} 0 0 0 ${themeColor(listener, "shift-4", color.get(listener))}`,
      border: "none",
      paddingBlock: (listener) => themeSpacing(themeDensity(listener) * 2),
      paddingInline: (listener) => themeSpacing(themeDensity(listener) * 4),
      margin: 0,
    },
  };
}

export { blockquote };
import { type PartialElement, toState, type ValueOrState } from "@domphy/core";
import {
  type ThemeColor,
  themeColor,
  themeDensity,
  themeFont,
  themeSize,
  themeSpacing,
} from "@domphy/theme";

/**
 * Styles a preformatted text block: inherited font size, the theme's monospace
 * stack, themed foreground/background,
 * no border, density-scaled padding and rounded corners.
 *
 * @hostTag pre
 * @param props.color - Theme color tone for text and background. Accepts a value or reactive
 *   state. Defaults to `"neutral"`.
 * @example { pre: "const x = 1", $: [preformated()] }
 */
function preformated(
  props: { color?: ValueOrState<ThemeColor> } = {},
): PartialElement {
  const color = toState(props.color ?? "neutral", "color");

  return {
    dataTone: "shift-2",
    _onInsert: (node) => {
      if (node.tagName !== "pre") {
        console.warn(`"preformated" primitive patch must use pre tag`);
      }
    },
    style: {
      fontSize: (listener) => themeSize(listener, "inherit"),
      // The theme owns the code stack; <code>/<kbd>/<pre> otherwise keep the
      // UA generic "monospace" (Courier New on Windows), so a theme that swaps
      // fontFamilies.monospace never reaches them.
      fontFamily: themeFont("monospace"),
      color: (listener) => themeColor(listener, "text", color.get(listener)),
      backgroundColor: (listener) =>
        themeColor(listener, "inherit", color.get(listener)),
      border: "none",
      paddingBlock: (listener) => themeSpacing(themeDensity(listener) * 2),
      paddingInline: (listener) => themeSpacing(themeDensity(listener) * 3),
      borderRadius: (listener) => themeSpacing(themeDensity(listener) * 2),
    },
  };
}

export { preformated };
import { type PartialElement, toState, type ValueOrState } from "@domphy/core";
import {
  type ThemeColor,
  themeColor,
  themeSize,
  themeSpacing,
} from "@domphy/theme";

/**
 * Themed ordered-list primitive: decimal markers positioned outside, reset
 * margins and themed text color. Apply to an `<ol>` element.
 *
 * @hostTag ol
 * @param props - Optional configuration.
 * @param props.color - Color tone for the list text. Defaults to `"neutral"`.
 * @example { ol: [{ li: "First" }, { li: "Second" }], $: [orderedList()] }
 */
function orderedList(
  props: { color?: ValueOrState<ThemeColor> } = {},
): PartialElement {
  const color = toState(props.color ?? "neutral", "color");

  return {
    _onInsert: (node) => {
      if (node.tagName !== "ol") {
        console.warn(`"orderedList" primitive patch must use ol tag`);
      }
    },
    style: {
      fontSize: (listener) => themeSize(listener, "inherit"),
      color: (listener) => themeColor(listener, "text", color.get(listener)),
      marginTop: 0,
      marginBottom: 0,
      paddingInlineStart: themeSpacing(3),
      listStyleType: "decimal",
      listStylePosition: "outside",
    },
  };
}

export { orderedList };
import { type PartialElement, toState, type ValueOrState } from "@domphy/core";
import {
  type ThemeColor,
  themeColor,
  themeSize,
  themeSpacing,
} from "@domphy/theme";

/**
 * Styles a bulleted list (disc markers, reset margins, themed text) on the host
 * `<ul>` element.
 *
 * @hostTag ul
 * @param props.color - Theme color for the list text. Optional, accepts a value or state. Defaults to `"neutral"`.
 * @example { ul: null, $: [unorderedList()] }
 */
function unorderedList(
  props: { color?: ValueOrState<ThemeColor> } = {},
): PartialElement {
  const color = toState(props.color ?? "neutral", "color");

  return {
    _onInsert: (node) => {
      if (node.tagName !== "ul") {
        console.warn(`"unorderedList" primitive patch must use ul tag`);
      }
    },
    style: {
      fontSize: (listener) => themeSize(listener, "inherit"),
      color: (listener) => themeColor(listener, "text", color.get(listener)),
      marginTop: 0,
      marginBottom: 0,
      paddingInlineStart: themeSpacing(3),
      listStyleType: "disc",
      listStylePosition: "outside",
    },
  };
}

export { unorderedList };
import { type PartialElement, toState, type ValueOrState } from "@domphy/core";
import {
  type ThemeColor,
  themeColor,
  themeSize,
  themeSpacing,
  themeWeight,
} from "@domphy/theme";

/**
 * Styles a description list as a two-column grid (terms in the first column,
 * descriptions in the second), theming the nested `<dt>`/`<dd>` elements.
 * Apply to a `<dl>` element.
 *
 * @hostTag dl
 * @param props.color - Theme color tone (`ValueOrState<ThemeColor>`) for the term/description text. Defaults to "neutral".
 * @example { dl: [{ dt: "Name" }, { dd: "Domphy" }], $: [descriptionList()] }
 */
function descriptionList(
  props: { color?: ValueOrState<ThemeColor> } = {},
): PartialElement {
  const color = toState(props.color ?? "neutral", "color");

  return {
    _onInsert: (node) => {
      if (node.tagName !== "dl") {
        console.warn(`"descriptionList" primitive patch must use dl tag`);
      }
    },
    style: {
      display: "grid",
      gridTemplateColumns: `minmax(${themeSpacing(24)}, max-content) 1fr`,
      columnGap: themeSpacing(4),
      margin: 0,
      "& dt": {
        margin: 0,
        fontWeight: themeWeight("semibold"),
        fontSize: (listener) => themeSize(listener, "inherit"),
        color: (listener) =>
          themeColor(listener, "shift-10", color.get(listener)),
      },
      "& dd": {
        margin: 0,
        fontSize: (listener) => themeSize(listener, "inherit"),
        color: (listener) => themeColor(listener, "text", color.get(listener)),
      },
    },
  };
}

export { descriptionList };