生态产品多个设计器现已发布候选版本

查看生态总览
Skip to content

Design tokens and theming ​

@xihan-ui/tokens provides two things: design tokens (one artifact of CSS custom properties) and the theme runtime (which resolves user preferences into attributes on the root element). It sits on the first level of the dependency graph and depends on no other package.

The two token layers ​

primitive  ──►  semantic  ──►  component-private slots
the palette     roles with      override points of
itself          meaning         a single component

primitive: raw values independent of context. They never change in any scope and are declared once.

css
--xh-color-brand-500: oklch(0.632 0.209 277);
--xh-color-neutral-950: oklch(0.145 0.005 258);
--xh-space-4: 16px;
--xh-radius-seed: 8px;

Colors use oklch instead of hex / hsl: different hues at the same lightness look equally bright, so dark inversion and contrast adjustments need no color-by-color hand tuning.

semantic: roles with meaning, pointing at primitives. Skins may only consume this layer.

css
--xh-bg-canvas: var(--xh-color-neutral-0);
--xh-bg-surface: var(--xh-color-neutral-0);
--xh-bg-brand: var(--xh-color-brand-600);
--xh-fg-default: var(--xh-color-neutral-900);
--xh-fg-muted: var(--xh-color-neutral-600);
--xh-border-subtle: color-mix(in oklab, var(--xh-ink) 4.7%, transparent);
--xh-control-h-md: 32px;
--xh-shape-control: var(--xh-radius-md);
--xh-elevation-floating: var(--xh-shadow-md);
--xh-motion-duration-enter: var(--xh-duration-normal);
--xh-layer-modal: var(--xh-z-modal);
--xh-overlay-max-w: 20rem;

The semantic layer is grouped by role: bg-* backgrounds, fg-* foregrounds, border-* strokes, control-* control sizes, shape-* shapes, ring-* focus rings, elevation-* elevation, motion-* durations and easings, layer-* stacking, overlay-* overlay sizes and text-* typography.

The token sources are JSON in DTCG format (packages/design/tokens/tokens/), and the artifacts are generated by a build script; tokens.css / tokens.json / src/generated/tokens.ts are all committed. CI regenerates and compares them, and the build fails when sources and artifacts drift apart.

Shape scale ​

Corner radii have one semantic scale; components never write their own radius values.

TokenValueUse
--xh-shape-inset2pxInner layers embedded in a control: checkbox boxes, candidate rows of in-page lists, clear buttons inside fields, color swatches
--xh-shape-control2pxThe control itself: Button, Input, Select Trigger, Toggle, kbd, tooltip, the track of the segmented radio group; square Tags, status tags and Alert bars
--xh-shape-surface4pxStatic containers that form a surface: Card, Panel, list containers, the track of Tabs segment, the outlined squares of a pagination
--xh-shape-overlay4pxOverlays taken out of the document flow: Popover, Menu, Dialog, the card preset of Notification, the two page-facing corners of a Drawer (the two edge-hugging corners take no radius, the toast preset takes control); references surface, so changing the surface radius moves overlays along
--xh-shape-circle50%Perfect circles: avatars, round icon buttons, radio indicators
--xh-shape-pill9999pxPills: status chips such as Badge (Tags and result marks are square and take control), plus one-dimensional objects such as tracks, indicator bars, handles and scrollbar thumbs

The shape tokens reference the raw scale --xh-radius-sm / md / lg (2 / 4 / 8px): inset and control take sm, surface takes md, and overlay references surface. Ordinary buttons, fields, cards and overlays do not use pill; Tags and result marks are square. List rows inside anchored overlays and the list rows of Command and Transfer are full-width rows without radius; an inner radius never exceeds the outer radius minus the padding; joined controls drop the radius on the touching sides. Light, dark and compact density do not change the shape identity. A square box takes circle and never fakes a circle with pill.

Press feedback ​

Discrete action controls (Button, Toggle, icon buttons, pagination buttons, toolbar buttons) share one press timeline:

PhaseDurationEasingResult
Press--xh-motion-duration-press (150ms)--xh-motion-ease-pressBackground enters active; scale goes from 1 to --xh-motion-scale-press (1 by default, only the face changes)
Release--xh-motion-duration-release (200ms)--xh-motion-ease-releaseScale returns to 1; background, stroke and text color return to hover / rest over --xh-motion-duration-micro

By default only the face changes and nothing scales; for a press bounce, set --xh-motion-scale-press back to 0.97 on the root, with no change to the timeline.

Collection items such as Menu Item, Listbox Item, Tree Node and Table Row, and disclosure triggers such as Accordion / Collapsible, follow the same rhythm (150ms to the active face on press, back to hover / rest over micro on release), but only switch the surface: they never scale the whole row, and zero feedback is not allowed. With reduced motion, --xh-motion-scale-press becomes 1 and both durations become 1ms, while the color feedback stays.

Scrolling inside components ​

Scrollbars come in only two kinds, fixed by the identity of the scrolling surface rather than decided by each component:

KindApplies toTokens
Custom-drawn bar (wired through the Scrollbar component)content / list / column under the positioner of the Overlay family; small fixed-height lists (Listbox content, Transfer list, time columns, Cascader column)type defaults to scroll-hover; thickness --xh-scrollbar-thickness-sm (4px) in overlays and --xh-scrollbar-thickness-md (6px) in pages; --xh-scrollbar-track-bg: transparent on the shell
Native thin barIn-page structural containers (Table, Tree, Transfer panels, Virtualizer viewport, Dialog / Drawer / FloatingPanel body, Layout sider / content, SideNav popout, Log / MessageFeed viewports, the calendar year grid, Typography pre) and scroll containers you build yourselfThe reset layer :where([data-scope][data-part], [data-xh-scroll]) sets scrollbar-width: thin + scrollbar-color: var(--xh-fg-scrollbar-thumb) var(--xh-bg-scrollbar-track) for all of them

The thumb has three color steps: --xh-fg-scrollbar-thumb / -hover / -active are 15% / 25% / 35% of the foreground color, shared by both kinds. Add data-xh-scroll to your own scroll container to get the same thin bar.

Boundary behavior goes by identity too: overscroll-behavior: contain only applies to overlay scrolling surfaces, modal bodies and bottom-pinned viewports, while in-page structural containers stay auto; scrollbar-gutter: stable only applies to containers whose content height changes dynamically (Log, MessageFeed, Dialog / Drawer body) and is guarded by :not([data-xh-scrollbar]); edge fading is only provided by the fade variant of ScrollArea and by Marquee. The page scrollbars of the documentation site use the same type as component scrollbars, without extra overrides.

Typography roles ​

The text-* typography tokens are picked by role. Hierarchy is expressed by size, weight, line height and spacing together, never by color alone:

RoleSize / weight / colorSpacing to neighbors
Field labels (single fields and compound single fields such as Slider, Rating, Signature, Color*)--xh-text-label-size 14 / --xh-text-label-weight 400 / --xh-fg-muted--xh-space-2 to the control when stacked; in horizontal Form layout the label column takes 5 / 24 of the row, with --xh-space-4 to the control
Group titles (RadioGroup, CheckboxGroup, Listbox, Tree, TagGroup, Descriptions)14 / --xh-text-label-weight 400 / --xh-fg-muted--xh-space-2 to the group
Field descriptions / helpers (Field, Fieldset)--xh-text-caption-size 12 / --xh-fg-subtle / --xh-leading-normal; no extra color change when disabledRight under the control; the helper row is at least --xh-space-5 high
Other descriptions (descriptions inside Card, Alert, Dialog and the like)--xh-text-secondary-size 13 / --xh-fg-muted / --xh-leading-normal; step descriptions of Steps take the secondary caption 12 / --xh-fg-subtle—
Error messages12 / --xh-fg-dangerShare the helper row under the control with the description
Titles of overlays and small surfaces (Popover, HoverCard, Popconfirm, FloatingPanel, Approval, EmptyState, Citation)14 / --xh-font-weight-medium—
Section and panel titles (Card, Descriptions, Alert, Notification, Steps; Dialog, Drawer, Tour)heading-3 (--xh-text-heading-3-*): 16 / 500—
Page titles (PageHeader)heading-2 (--xh-text-heading-2-*): 20 / 500—
Large numbers (Statistic, Timer, NumberAnimation)sm / md / lg take heading-2 / heading-1 / display (--xh-text-display-size): 20 / 24 / 28—
Secondary marks (counts, shortcuts, timestamps, ordinals)--xh-text-caption-size 12—

The required asterisk and error messages are common-layer rules: the asterisk --xh-glyph-mark-required sits before the label text, takes --xh-text-caption-size + --xh-fg-danger and is --xh-space-1 away from the text; fields with their own label do not draw a separate one. Field labels and field descriptions do not change color when disabled, other labels take --xh-fg-subtle when disabled, and single-line labels use --xh-leading-none. Icons inside controls follow the size step with --xh-glyph-size-sm / md / lg (16 / 18 / 20); --xh-glyph-size-text (1em, following the text) is only for purely inline text components such as Tag, Kbd, Breadcrumb, Typography and Highlight.

Components do not set a font; font-family is inherited from the host as usual. The library's default sans-serif stack lives in --xh-font-family-sans (system UI fonts first, then PingFang, Hiragino Sans GB, Microsoft YaHei and Noto Sans SC for Chinese) and only reaches the page root when you import the optional entry:

ts
import "@xihan-ui/styles/global.css";

This entry is not part of the main @xihan-ui/styles entry. The rule sits in the lowest xihan.reset layer with its selector wrapped in :where(), so any font the host sets still wins. Code and tabular figures use --xh-font-family-mono.

The eight-axis visual environment runtime ​

VisualEnvironmentController is the single entry for the state and DOM projection of mode, brand, density, dir, contrast, motion, transparency and material. One setPreference synchronously commits all eight axes of a scope:

ts
import { setMotionOverride } from "@xihan-ui/motion";
import {
  brandId,
  createMotionOverrideSink,
  createVisualEnvironmentController,
} from "@xihan-ui/tokens/runtime";

const visual = createVisualEnvironmentController({
  root: document.documentElement,
  storageKey: "app-visual-environment",
  onStorageError: detail => reportStorageFailure(detail),
  motionSink: createMotionOverrideSink(setMotionOverride),
  initial: {
    mode: "system",
    brand: brandId("xihan"),
    density: "comfortable",
    dir: "ltr",
    contrast: "system",
    motion: "system",
    transparency: "system",
    material: "standard",
  },
});

visual.setPreference({ mode: "dark", density: "compact" });
visual.getState();
visual.subscribe(state => console.log(state));
visual.dispose();

Enabling storageKey requires onStorageError as well. Parse failures, private browsing or an exceeded quota are reported explicitly; the controller keeps maintaining its in-memory state but never pretends a failed persistence succeeded.

The motionSink of the root controller is explicit dependency injection: it syncs the resolved motion into Presence, smooth scrolling and JS animations. Only the documentElement root scope may pass it; local controllers only change their own DOM scope and cannot implicitly pollute the global JS override.

The eight-axis state is projected into the full attribute surface that portals can bridge:

html
<html data-theme="dark" data-brand="xihan" data-density="compact" data-contrast="more" data-motion="reduce" data-transparency="reduce" data-material="liquid" dir="rtl">

Preference, state and system axes ​

VisualEnvironmentPreferenceVisualEnvironmentState
MeaningThe intent submitted by the user or the serverThe fully settled facts
undefinedClears this layer's override and inherits from the parent controllerDoes not occur; all eight axes are non-empty
'system'Only valid for mode / contrast / motion / transparencyDoes not occur; resolved to a concrete value

brand, density, dir and material have no platform media query, so they do not accept a fake system. The other four read prefers-color-scheme, prefers-contrast, prefers-reduced-motion and prefers-reduced-transparency. SSR environments without window / matchMedia use light, default contrast, default motion and default transparency, without guessing the client's preferences.

The baseline of the eight axes is light, xihan, comfortable, ltr, default, default, default, standard. contrast='default' is the official value for explicitly restoring normal contrast; the old base is no longer accepted.

DOM axisDescription
data-themeLight and dark each have a full set of semantic values; system follows the system without JS, see below
data-brandSwitches the brand color: presets work directly, custom brands are registered with registerBrand first
data-density='compact'Tightens control heights, padding and gaps without shrinking font sizes or glyphs
dirLogical properties drive the LTR / RTL layout; quantities that only know physical directions (translate, gradient angles, the left and right sides of clip-path) are multiplied by --xh-direction-sign, which inherits from the nearest dir (1 for ltr, -1 for rtl), so a subtree set back to ltr flips back; physical coordinates such as transform-origin take --xh-direction-start / --xh-direction-end (the horizontal position of the inline start / end edge: 0% / 100% for ltr, the other way round for rtl)
data-contrastmore strengthens boundaries; default explicitly returns to the normal level
data-motionreduce triggers local CSS motion reduction
data-transparencyreduce switches translucent materials opened by a theme back to solid recipes
data-materialliquid turns the navigation-layer parts floating above content (float buttons, media controls, the sticky top bar) into liquid surfaces; standard keeps the original material; the nearest ancestor wins

Follow the system color scheme without JS ​

The controller's mode: "system" resolves light or dark only once the script runs. When a page just wants to follow the system without loading a script for it, or needs to be right before the script arrives, write data-theme="system" in the HTML:

html
<html data-theme="system">

On a light system it takes the same values as light. When the system switches to dark, the dark token values, color-scheme and the few skin rules that only hold in dark (tones, solid buttons and so on) switch over through prefers-color-scheme, without any script, and follow every later system switch. system is only a value written in the DOM; the controller still projects only light / dark, so a controller created later on the same root replaces the attribute with the resolved value.

Preset brands ​

@xihan-ui/tokens ships a few named brands. The list is BRAND_PRESETS (such as blue, green, orange, pink) and the type is BrandPreset; BRAND_PRESET_COLORS gives each preset a representative color (its brand 600), for drawing swatches and preset lists. Each preset is a [data-brand='<id>'] value block in tokens.css that only swaps the brand primitives, without registerBrand:

ts
visual.setPreference({ brand: brandId("green") });

Each preset takes the hue of the same name from the base palette, and every step is derived from the relative luminance of the same step of the default brand; indigo is the former default brand (hue 258), kept as a preset after the default moved to an indigo at hue 277. Contrasts that involve the brand (text on solid backgrounds, brand text color, the brand tint with text on it, the focus ring) therefore meet the same targets as the default brand; light, dark and both high-contrast levels are checked step by step by a gate. A value block of the same name injected by registerBrand is not layered and wins over the preset.

data-brand only switches the brand when it sits on the same element as data-theme: semantic tokens are declared at the theme boundary, so swapping brand primitives on a child element leaves the semantic tokens at the values resolved outside. The visual environment controller always writes both attributes on its root; when writing them by hand, put data-theme on the same element as well.

Parent scopes, portals and cleanup ​

Local controllers inherit explicitly through parent. When the parent state changes, a child controller only recomputes the axes without a local preference; setPreference({ motion: undefined }) removes this layer's override and restores inheritance immediately. dispose unsubscribes from the parent scope and the system media queries, and restores the eight attributes of the root element exactly as they were before it took over.

ts
const page = createVisualEnvironmentController({
  root: document.documentElement,
  initial: { mode: "dark", motion: "system" },
});
const editor = createVisualEnvironmentController({
  root: document.querySelector("#editor")!,
  parent: page,
  initial: { density: "compact", transparency: "reduce" },
});

React's XhConfigProvider and Vue's provideXhConfig accept controller options with an explicit root through config.visualEnvironment; nested providers pick up the parent controller automatically. The Web Components <xh-config> is a local scope by itself and can set mode, brand, density, direction, contrast, motion, transparency and material directly. Physical portals are still bridged with the resolved eight axes by Core's VisualBridge; the adapters keep no visual state of their own.

The five-axis legacy view ​

createThemeController, resolveTheme and applyThemeAttrs still exist, but they are only an explicit five-axis view of the new controller: they have no resolver, media query or persistence of their own. New code should use the eight-axis controller; a standalone applyThemeAttrs only writes five axes, while the controller always maintains the full eight-axis DOM contract.

The color-scheme declaration is written into the dark value block, so native controls inside nested dark regions follow along. SSR should output the eight resolved attributes on first paint to avoid a flash before the client takes over.

Material recipes ​

There are five token recipes, each providing the same nine tokens: bg, backdrop, border, highlight, shadow, separator, fg, fg-muted and focus-surface; M5 liquid has five extra tokens of its own. By default every recipe is a solid surface; translucency (a translucent background plus a backdrop filter) may only be turned on by a theme for transient overlays. There is no glass material and no compatibility alias. Two more stacking levels, raised and floating, are made of elevation tokens: they have no --xh-material-* tokens and are always written as a solid stroke + solid background + the matching elevation.

IdTokensUseOptics
M0 solid--xh-material-solid-*Default for static content surfaces: border-default stroke + surface background + no shadow; fields at rest are stroked the same way on --xh-bg-field (white by default)Solid background, no highlight, no shadow
M1 soft--xh-material-soft-*Secondary surfaces with a soft tint; not for Card or fields. Button and Tag are flat surfaces and do not take its top light or shadowSolid background, a subtle top light and a two-step contact shadow, no backdrop blur
M2 frosted--xh-material-frosted-*Anchored transient overlays such as short lists, menus, tooltips and bubbles; anchored panels with grids or several columns use solid + border-default + --xh-elevation-floating insteadSolid surface background, no backdrop blur, 1px border-default stroke, a medium (md) shadow, no top light
M4 elevated--xh-material-elevated-*Modal and strong-feedback surfaces (sheets) such as Dialog, Drawer, Command, Tour and Notification; always with a 1px strokeSolid surface background, no backdrop blur, a large (lg) shadow
M5 liquid--xh-material-liquid-*Only under data-material="liquid": the navigation layer floating above content — float buttons (FloatButton, BackTop, the scroll-to-bottom of MessageFeed / Log), media controls (Carousel controls and pagination bar, the ImageViewer control layer), the sticky Layout headerReadable floor of 0.48 (light) / 0.61 (dark) opacity, 8px blur, 140% saturation, an ink hairline plus a 1px rim light, edge refraction in Chromium
raised (stacking level)solid stroke + solid background + --xh-elevation-raisedCard and parts that can be lifted or dragged (the slider of Tabs segment, switch thumbs and the like), registered per part; the stroke must be there and the shadow is only a bonus; hover lifting only when interactiveSolid background; --xh-elevation-raised takes the small (sm) shadow
floating (stacking level)solid background + --xh-border-default + --xh-elevation-floatingAnchored panels with grids or several columns: NavigationMenu content, Date / Time / DateRange / TimeRange picker contentSolid background, no translucency, a medium (md) shadow
SuffixUse
bg / backdropBackground color / backdrop filter
border / highlight / shadowOuter boundary, top highlight and shadow
separatorInner divider lines
fg / fg-mutedBody and secondary text, always opaque
focus-surfaceA solid isolation layer laid inside the focus ring on keyboard focus

M2 frosted has a single implementation in the library: the material family recipe @xihan-ui/styles/material.css. Components project data-xh-material="frosted" onto a part; the recipe paints the stroke, background, foreground and shadow of the content surface of anchored overlays, while the backdrop filter and the 1px top light are empty by default and left to themes that want translucency. The surfaces of float buttons belong to Action Control, and the recipe gives them the interaction steps (tint one and two on hover and press, focus-surface on keyboard focus). A custom overlay surface joins by writing the same attribute and overrides with --xh-frosted-bg, --xh-frosted-border, --xh-frosted-fg, --xh-frosted-shadow, --xh-frosted-backdrop and --xh-frosted-highlight.

Card has only three forms: outline (default, solid stroke + surface background + --xh-elevation-raised, the small shadow), subtle (--xh-bg-subtle + a transparent placeholder border + no shadow) and ghost (no border, background or shadow; only dividers are allowed). Boundaries are drawn only by the stroke; neither shadows nor tints serve as boundaries.

The environment axes degrade in place under the same token names: high contrast strengthens boundaries; reduced transparency and print force solid backgrounds and turn off backdrop filters (even when a theme turned translucency on); forced colors express everything through Canvas / CanvasText. All four levels are verified in both the light and the dark theme against black, white, mid gray, the page and the brand background: body text at least 4.5:1 and the focus ring at least 3:1 against its isolation layer.

Theme and contrast can be declared on different levels: each level takes the nearest data-theme and data-contrast. A child theme does not cancel the high contrast of an ancestor, data-contrast="default" restores the normal level locally, and sibling regions do not affect each other.

Where frosted overlay surfaces are used ​

  • The name stays frosted, but the default value is a solid popup layer: it does not sample the content behind it and has no top boundary light; a 1px stroke carries the visible boundary, and layering never relies on the shadow alone.
  • Only for anchored transient overlays; never for body text, the main part of forms, Card, Table, Notification or the main reading surface of Dialog.
  • When a theme wants frosted translucency, give --xh-material-frosted-bg a translucent background, give backdrop a blur (no more than 16px, saturate no more than 1.08) and give highlight an inner top light of at most 1px. Once translucency is on, do not put backdrop-filter on the page root, large scrolling areas or overlapping layers, do not animate the blur radius, and pair the focus ring with the solid isolation surface of focus-surface.

The compact recipe (Tooltip) ​

Tooltip is not inverted: it uses the same overlay face as Popover, taking its background, text and edge from the frosted material (M2) and combining three compact recipes. Only when tone is set does it switch to a solid tone fill, and then a 20% mix of the on-color carries the 1px visible boundary (a neutral stroke is invisible on a colored fill):

RecipeNormal value
--xh-material-frosted-compact-alphaOpaque (--xh-alpha-opaque)
--xh-material-frosted-compact-backdropnone, no blur
--xh-material-frosted-compact-shadowThe medium (md) shadow (--xh-shadow-md in light, the same three dark layers in dark)

High contrast, forced colors and print remove the shadow; reduced transparency keeps a small shadow to show the overlay's position. Components consume the optical parameters only through these three recipes and never pick alpha or blur primitives directly.

Liquid material ​

data-material is the application-level material axis, standard by default. With liquid, the navigation-layer parts floating above content turn into liquid surfaces. It can sit on any ancestor and the nearest one wins; the portal visual bridge carries it to the instance shell; under standard the same parts keep their original material. When you use the visual environment controller, set it through its material preference (the controller always maintains this attribute on the root).

The current liquid parts: the FloatButton trigger and its expanded actions, BackTop, the scroll-to-bottom of MessageFeed / Log, the page and play buttons and the pagination bar of Carousel (resting on a liquid capsule), the toolbar, counter, page and close buttons of ImageViewer, and the header of a fixed Layout. Transient overlays, modals, Notification, Card, Table, forms and body containers never use liquid, which the check-material-scope gate enforces.

html
<html data-material="liquid">
  <!-- For images, videos, canvases and other areas whose colors cannot be read, the author declares how light the layer below is -->
  <section data-xh-backdrop="dark" data-xh-backdrop-busy>…</section>
</html>

The author only writes this one attribute. Once the components are mounted, the liquid surface of @xihan-ui/core/visual-environment takes over; the parts of one document share one set of listeners, with nothing extra to install or call:

  • The tint follows the computed color of the layer under the part and the author's declaration: a light layer below gets the light tint, a dark one the dark tint, and the part becomes a black-ink or white-ink zone accordingly (relative luminance 0.179 ± 0.04 with hysteresis, so content scrolling across the boundary does not flicker).
  • When the layer below is uniform and its tint is known, the surface switches to the clear level (0.24 light / 0.34 dark); when the layer below has text, gradients, images, videos, canvases or cannot be read, it stays at the readable floor, and labels are at least 4.5:1 on any layer below. Content set inert on the page (absent from the hit stack) is treated as unreadable too.
  • The layer below is re-read on scroll, window resize, image load, the end of transitions or animations, and when state-machine-driven translations settle (carousel paging, image viewer inertia).
  • With a fine pointer, the 1px rim light turns toward the pointer; with a coarse pointer, no pointer or reduced motion it stays fixed at the top left (top right in RTL).
  • In Chromium the layer below is refracted within 18px of the edge; in other engines, above 640 × 120, or with more than 3 in one viewport, it only blurs without refraction.
Extra tokenUse
--xh-material-liquid-tintTint of the clear level
--xh-material-liquid-alpha-clear / -alpha-floorOpacity of the clear level and of the readable floor
--xh-material-liquid-rim-farThe weak bright edge on the side away from the light
--xh-material-liquid-bezelWidth of the refraction band

Server rendering and the state before mounting output the static form: the tint follows the theme and the opacity takes the readable floor. With reduced transparency and in print the opacity is 1 without backdrop filters or refraction; high contrast uses opacity 1, and forced colors express it through system colors.

Using the tokens directly ​

ts
import type { TokenName } from "@xihan-ui/tokens";
// Typed token names on the TypeScript side
import { tokens } from "@xihan-ui/tokens";

// The machine-readable artifact: for Figma variables, a Tailwind theme or design annotations
import tokens from "@xihan-ui/tokens/tokens.json" with { type: "json" };

Released under The MIT License