@codefusion-cc/ui

中文文档
0.2.9发布于 2026/11/09MITTypeScriptES Module零依赖近 30 天下载 2150

The token contract every CodeFusion look fills, and the kit built on it: typed roles (colours, fonts, radii, shadows, control heights, motion), defineLook checked at compile time, lookProblems for WCAG 2.2 AA contrast and target sizes in light and dark, C

原始使用文档
文档目录13 节
  1. A look
  2. One mode only
  3. An app using it
  4. The cascade
  5. Another look in one part of the page
  6. An app's own tokens
  7. Checking a look
  8. The kit
  9. Windowed lists
  10. The page behind a dialog
  11. The preview
  12. Visual tests
  13. Design tokens

@codefusion-cc/ui

The design tokens every CodeFusion app is built on, and the components built on them: one contract of roles (page, ink, accent, the status colours, six chart colours, fonts, radii, shadows, control heights, motion), looks that fill it, a Vite plugin that turns the looks into one stylesheet at build time, and a React kit drawn with Tailwind in those roles. A look is checked as you write it: TypeScript refuses a role left out or one the contract does not have, and the build fails on any pair of colours under WCAG 2.2 AA.

npm install @codefusion-cc/ui
EntryWhat it holds
@codefusion-cc/ui/looksdefineLook, defineAppTokens, lookProblems, looksCss, lookTokens, the contract (ROLES, PAIRS) and its types
@codefusion-cc/ui/viteuiLooks(): the looks as CSS, and the /__looks preview in development; uiButtons(): data-button="secondary" in static HTML becomes buttonClass's classes (bare data-button is the primary); expandClassAttribute for other such attributes
@codefusion-cc/ui/button-classbuttonClass without React, for pages and build tools
@codefusion-cc/ui/reactthe kit: Button, buttonClass, buttonSizeClass, Loading, ErrorState, Spinner, UiProvider, usePageLock
@codefusion-cc/ui/wordsthe kit's words in Polish, English and German (pl, en, de)
@codefusion-cc/ui/tailwind.cssthe roles as Tailwind's theme, and the kit's sources for Tailwind to scan
@codefusion-cc/ui/tailwind-mergecreateCx: the app's cx, tailwind-merge knowing the kit's theme values (tailwindMergeConfig)
@codefusion-cc/ui/react/statesthe kit's state lists, for /__looks and visual tests
@codefusion-cc/ui/page-locklockPageBehind, pageScrollY: the page behind a modal dialog or a drawer stays still, on iOS Safari with the keyboard open too
@codefusion-cc/ui/testingassertLook, assertLookComplete, three fixture looks, defineStates and the state registry, synthetic touches (drag, touch)
@codefusion-cc/ui/clientthe types of virtual:codefusion-ui/looks.css, for an app's tsconfig
@codefusion-cc/ui/tokens.cssthe contract alone at its fallbacks, for a page with no looks of its own
@codefusion-cc/ui/tokens.jsonthe contract as Design Tokens (DTCG 2025.10)

A look

A look module default-exports defineLook(...): its name (the value of data-look), every colour and shadow for light and for dark, and the values the same in both.

// src/looks/kuznia.ts
import { defineLook } from '@codefusion-cc/ui/looks'

export default defineLook({
  name: 'kuznia',
  light: {
    page: 'oklch(0.965 0.012 85)',
    surface: 'oklch(0.99 0.006 85)',
    raised: 'oklch(0.93 0.016 85)',
    ink: 'oklch(0.2 0.012 60)',
    muted: 'oklch(0.45 0.02 60)',
    line: 'oklch(0.86 0.016 80)',
    accent: 'oklch(0.5 0.19 35)',
    'accent-2': 'oklch(0.42 0.09 250)',
    'on-accent': 'oklch(0.99 0.006 85)',
    inverse: 'oklch(0.2 0.012 60)',
    'on-inverse': 'oklch(0.965 0.012 85)',
    media: 'oklch(0.9 0.012 85)',
    focus: 'var(--cf-accent)',
    ok: 'oklch(0.48 0.11 150)',
    warn: 'oklch(0.52 0.12 65)',
    danger: 'oklch(0.5 0.19 27)',
    info: 'oklch(0.45 0.12 250)',
    'chart-1': 'oklch(0.5 0.19 35)',
    'chart-2': 'oklch(0.42 0.09 250)',
    'chart-3': 'oklch(0.5 0.1 150)',
    'chart-4': 'oklch(0.55 0.13 65)',
    'chart-5': 'oklch(0.45 0.13 320)',
    'chart-6': 'oklch(0.35 0.02 60)',
    'shadow-card': '0 1px 2px oklch(0.2 0.012 60 / 0.08), 0 6px 20px -8px oklch(0.2 0.012 60 / 0.18)',
    'shadow-lift': '0 2px 4px oklch(0.2 0.012 60 / 0.1), 0 18px 40px -12px oklch(0.2 0.012 60 / 0.3)',
  },
  dark: {
    page: 'oklch(0.17 0.008 60)',
    surface: 'oklch(0.21 0.01 60)',
    raised: 'oklch(0.26 0.012 60)',
    ink: 'oklch(0.94 0.014 85)',
    muted: 'oklch(0.74 0.018 80)',
    line: 'oklch(0.34 0.014 60)',
    accent: 'oklch(0.74 0.15 40)',
    'accent-2': 'oklch(0.76 0.09 250)',
    'on-accent': 'oklch(0.17 0.008 60)',
    inverse: 'oklch(0.94 0.014 85)',
    'on-inverse': 'oklch(0.17 0.008 60)',
    media: 'oklch(0.3 0.012 60)',
    focus: 'var(--cf-accent)',
    ok: 'oklch(0.78 0.13 150)',
    warn: 'oklch(0.82 0.13 80)',
    danger: 'oklch(0.72 0.15 27)',
    info: 'oklch(0.76 0.1 250)',
    'chart-1': 'oklch(0.74 0.15 40)',
    'chart-2': 'oklch(0.76 0.09 250)',
    'chart-3': 'oklch(0.78 0.12 150)',
    'chart-4': 'oklch(0.82 0.13 80)',
    'chart-5': 'oklch(0.74 0.13 320)',
    'chart-6': 'oklch(0.85 0.02 85)',
    'shadow-card': '0 0 0 1px oklch(1 0 0 / 0.06), 0 6px 20px -8px oklch(0 0 0 / 0.6)',
    'shadow-lift': '0 0 0 1px oklch(1 0 0 / 0.08), 0 18px 40px -12px oklch(0 0 0 / 0.75)',
  },
  both: {
    'font-display': '"Fraunces", "Iowan Old Style", Georgia, serif',
    'font-body': '"Inter", system-ui, sans-serif',
    'font-mono': '"JetBrains Mono", ui-monospace, monospace',
    'radius-card': '14px',
    'radius-ctl': '10px',
    'control-touch': '44px',
    'control-mouse': '32px',
    'duration-press': '90ms',
    'duration-quick': '160ms',
    'duration-move': '280ms',
    'duration-settle': '480ms',
    'ease-out': 'cubic-bezier(0.2, 0.8, 0.2, 1)',
    'ease-in': 'cubic-bezier(0.4, 0, 1, 1)',
    'ease-state': 'cubic-bezier(0.34, 1.3, 0.5, 1)',
  },
})
  • Colours may be in any CSS notation (oklch, hex, rgb, hsl, lab, color(display-p3 …)), var(--cf-<role>) of another role, or color-mix() of these. They are written out as OKLCH, and what is measured is that written value read back, as Chromium, Firefox and WebKit paint it (a colour past the screen clipped channel by channel), so rounding never moves a pair across its line; a pair passes at its ratio exactly. Refused, since browsers paint them differently or they cannot be measured: a mix in hsl or hwb, none inside a mix, a colour past OKLCH chroma 1, light-dark() (a look gives each mode its own value), currentColor, system colours and relative colours. Colours that refer to each other in a loop are refused too, light and dark together, since CSS reads them as one light-dark().
  • Fonts end in a generic family (serif, sans-serif, system-ui, …), and the mono font in monospace. A family's name holds letters, digits, spaces and . _ & + -.
  • Control heights are at least 44px for touch and 24px for a mouse, in px: a rem follows the root font size, which the app sets, not the look. --cf-control is the one for the pointer in use.
  • Durations become 0s when the system asks for reduced motion.
  • Bounds: a value and an app role's description have at most 4,000 characters, a look's name, a role's name and an app's prefix 64, and color-mix() nests 8 deep at most. Lengths stay within 10,000 px or rem either way, a target's min too, so a declared min can always be met. Durations run up to a minute, and an easing's y values stay within -10 to 10. An app has at most 200 roles and 400 pairs, and looksProblems takes at most 100 looks. The bounds apply to what a look or an app gives, once: the CSS written from it may be longer (a colour written as oklch()). Something past a bound is refused before any work in proportion to it, with one exception: before a check can count an object's keys, the JavaScript engine lists all of them, so an object of a million keys costs that listing however soon it is refused.
  • Work: a check (lookProblems, looksProblems, appTokensProblems, defineAppTokens) has one work budget, WORK_BUDGET steps, and each step is a bounded amount of work: an element gone through, a problem found, a character read, a colour computed. Past the budget, it answers that the input is too large to measure, a problem that stops the look from being written. Each value is written once however often it is asked for, and the two fixture looks ten times over spend a third of the budget. A check answers at most 100 problems, those that stop a look from being written first, and a last one saying how many more there are and how many of them block.
  • Optional roles have a fallback mixed from the others, so a look sets them only to differ from it, and is measured either way: <tone>-tint (a wash on the surface), <tone>-ink (text in that tone) and <tone>-line (its edge) for accent, ok, warn, danger and info; accent-hover, inverse-hover and hover; the secondary button's control-fill, control-line, control-ink and their -hovers, the quiet one's ghost-ink (under the pointer it keeps its text and takes the hover veil), the link one's link-ink (the accent's ink); shadow-control (none); and in both, control-pad (20px), control-case (none, or uppercase for buttons set in capitals), control-tracking (0) and control-disabled (0.6, how much of a disabled or busy control shows, 0.25 to 1). A colour role a look sets is set in both modes or in neither.

One mode only

A look made for one mode says so. The page then stays in that mode even when the theme asks for the other; the other mode is never made up from it.

export default defineLook({ name: 'papier', only: 'light', light: { … }, both: { … } })

TypeScript refuses a dark part on it, and /__looks shows the missing mode as such.

An app using it

// vite.config.ts
import { defineConfig } from 'vite'
import { prePaintTheme } from '@codefusion-cc/theme/vite'
import { uiLooks } from '@codefusion-cc/ui/vite'
import { themeConfig } from './src/themeConfig.ts'

export default defineConfig({
  plugins: [
    prePaintTheme(themeConfig),
    uiLooks({ looks: ['./src/looks/kuznia.ts', './src/looks/noc.ts'], default: 'kuznia' }),
  ],
})
// src/main.ts, before anything else: the contract and every look, as CSS. No look code reaches the page.
import 'virtual:codefusion-ui/looks.css'

Import it from a script, not with @import from a stylesheet another tool post-processes. Its light-dark() must reach the browser as written, and the plugin makes sure Vite's own minifier leaves it alone.

TypeScript learns of the module from the package's client types, next to Vite's:

// tsconfig.json
{ "compilerOptions": { "types": ["vite/client", "@codefusion-cc/ui/client"] } }

The theme package picks the look and the mode. It sets data-look and color-scheme on <html>, and the CSS reads both. The default look is shown on the page wherever data-look names no other look: before the theme sets it, and when it names a look the app no longer has, as a visitor's old pick may.

// src/themeConfig.ts
import { defineTheme } from '@codefusion-cc/theme'

export const themeConfig = defineTheme({
  storageKey: 'myapp-theme',
  html: { class: 'dark' },
  choices: { look: { storageKey: 'myapp-look', values: ['kuznia', 'noc'], default: 'kuznia', attribute: 'data-look' } },
})

Then use the roles anywhere:

.card {
  background: var(--cf-surface);
  color: var(--cf-ink);
  border-radius: var(--cf-radius-card);
  box-shadow: var(--cf-shadow-card);
}
.card:hover { background: color-mix(in oklab, var(--cf-surface), var(--cf-hover)); }
button { min-height: var(--cf-control); transition: background var(--cf-duration-quick) var(--cf-ease-out); }
:focus-visible { outline: 2px solid var(--cf-focus); outline-offset: 2px; }

With Tailwind 4.1 or later, import the roles as Tailwind's theme once, after Tailwind, and write bg-surface, text-muted, border-danger-line, rounded-card, min-h-control, shadow-lift, ease-state:

/* src/index.css */
@import "tailwindcss";
@import "@codefusion-cc/ui/tailwind.css";

Every colour role is a colour (bg-<role>, text-<role>, border-<role>, bg-accent/40), and the shadows, radii (rounded-card, rounded-ctl), fonts (font-display, font-body), min-h-control, px-control-pad, tracking-control and ease-state are there too. Each class reads its custom property itself, so a part of the page in another look follows it. Nothing of Tailwind's own is redefined: font-sans, shadow-sm and rounded-xl stay as they were. The file also points Tailwind at the kit's sources, so the kit's classes land in the app's stylesheet once, shared with its screens.

The cascade

The stylesheet declares one layer, cf.tokens, and the looks are in it. Roles are custom properties, so the layer only decides who sets them: an app's own --cf-* wins over the looks whatever its selectors.

Another look in one part of the page

A colour role is computed where data-look is set. A part of the page shown in another look or mode sets both on the same element:

<section data-look="noc" style="color-scheme: dark">…</section>

An app's own tokens

What only one app has (a conversation's two sides, a drawn piece's sizes) goes under the app's prefix, typed and checked like the contract's:

// src/looks/tokens.ts
import { defineAppTokens } from '@codefusion-cc/ui/looks'

export const hh = defineAppTokens(
  'hh',
  {
    me: { type: 'color', description: 'Your side of a conversation.', fallback: { light: '#b8460f', dark: '#f19359' } },
    ring: { type: 'dimension', description: 'The width of the speaking ring.', fallback: '3px' },
  },
  { pairs: [{ fg: '--hh-me', bg: '--cf-page', kind: 'ui' }] },
)

// in a look: app: { tokens: hh, light: { me: '#b8460f' }, dark: { me: '#f19359' }, both: { ring: '3px' } }

They are written as --hh-me and --hh-ring, registered and given their fallbacks like the contract's. Every look sets them, a look without its own values at their fallbacks, so a look inside another takes nothing from it. A fallback is checked as a look's value is when the tokens are defined (its type, a target's min, the pairs it is in, over the contract's fallbacks), and the build measures every look with each other app's fallbacks too.

Checking a look

The build fails on any problem, whether or not the app imports the stylesheet, and the dev server warns of them. looksProblems(looks) returns the same list and lookProblems(look) one look's (neither throws), for looks loaded from anywhere. In an app's tests:

import { assertLook } from '@codefusion-cc/ui/testing'
import kuznia from '../src/looks/kuznia.ts'

test('kuznia passes', () => assertLook(kuznia))

It checks every role in both modes; each text pair (ink and muted on page, surface and raised; on-accent on accent; each tone's ink on its tint, …) against 4.5:1, and each part of the interface (accent, focus, status and chart colours on their grounds) against 3:1. Translucent colours are blended over what they are painted on, and a colour outside sRGB is measured on both an sRGB and a Display P3 screen. line is decorative and not measured.

The kit

React components in the look's roles, styled with Tailwind classes the app's own Tailwind generates (the import above). They need React 19 with React DOM, and Tailwind 4.1 with its preflight.

import { Button, ErrorState, Loading, UiProvider, buttonClass } from '@codefusion-cc/ui/react'
import { pl } from '@codefusion-cc/ui/words'

<UiProvider words={pl}>
  <Button pending={saving} pendingLabel="Zapisywanie…" onClick={save}>Zapisz</Button>
  <Link to="/koszyk" className={buttonClass({ tone: 'secondary' })}>Koszyk</Link>
  {orders.failed ? <ErrorState onRetry={reload}>Nie udało się wczytać zamówień.</ErrorState> : <Loading />}
</UiProvider>
  • Button: tone is primary (the accent, the one action of a screen), inverse, secondary (outlined), ghost (no fill until hovered), link or plain (the shape alone). size is md (--cf-control tall: 44px at least on touch), sm or icon (40px; an icon-only button takes an aria-label). It is type="button" unless told otherwise. While pending it keeps its focus and its place in the tab order but takes no click and submits no form (a second press, or Enter in a field, does nothing), turns its icon or shows a spinner, and tells a screen reader it is working. disabled is the browser's own: no focus, no press. No tone changes under the pointer while disabled or pending; a hover an app adds to plain (or to any tone) takes the same guard, not-disabled:not-aria-disabled:hover:…, or a busy button lights up as if it could be pressed.
  • link: a quiet action among words ("Try again" after a failure, "Remove" in a row), drawn as a link. No fill, edge, padding or text size of its own: it is as big as the text around it and sits on its baseline, in link-ink, semibold, underlined under the pointer. It is still a button to the keyboard and a screen reader, with the focus ring, pending and disabled states of the others, and keeps its underline in forced colours. It takes no size or height. Another colour is the app's: cx(buttonClass({ tone: 'link' }), 'text-muted').
  • Sizes: sm and icon stay 40px on touch screens too. WCAG 2.2 AA (2.5.8, target size minimum) asks for 24px; 44px is AAA. The apps' small buttons are 40px today, and making them 44px on phones would change their looks, which the kit must not do. Do not raise them to 44px without each app's owner agreeing.
  • height="padding": for an app whose buttons are as tall as their padding and label make them (44px for md with a line of text-sm, 36px for sm), whatever the pointer; the default, control, gives md the look's control height and sm 40px at least. With padding the app answers for the target size itself.
  • buttonClass({ tone, size, height }): the same classes for a link or anything else that should look like a button. buttonSizeClass(size, { height }): a size's classes alone, for a size added to a tone of the app's own.
  • Loading: a spinner and "Loading…" (or label), announced politely (role="status").
  • ErrorState: the failure in the danger tone, announced at once (role="alert"), with a "Try again" button when given onRetry. When onRetry returns a promise the button waits for it, fulfilled or rejected; retrying is for a retry the app tracks itself. It takes a div's attributes and ref too: an id for a field's aria-describedby, tabIndex={-1} for a form that moves the focus to its message (which shows no ring).
  • Words: UiProvider words={pl | en | de} sets the few words the kit says itself; English without one.
  • className places a component (margin, width, a grid or flex item). It does not restyle one: two classes for one property apply in Tailwind's order, not in the order they are written. To restyle buttonClass() on an element of the app's own, merge the classes with tailwind-merge (the app's, 3 or later), which needs to know the kit's theme values: export const cx = createCx() from @codefusion-cc/ui/tailwind-merge, then cx(buttonClass({ size: 'sm' }), 'min-h-11 px-5'). createCx({ extend: { theme: { radius: ['panel'] } } }) adds the app's own values, as tailwind-merge's extendTailwindMerge takes them.
  • A word too long for the whole line breaks instead of pushing the page sideways; in a crowded row a button keeps its words whole, as text beside it does. That holds for a label of plain text (Cart ({count}) too); give an icon through icon rather than among the words. The spinner turns slowly where motion is to be reduced, and in forced colours every button keeps an edge (a link its underline).

Windowed lists

WindowedList mounts only the fixed-height rows in the window plus overscan (4 by default) and the focused row. It scrolls with the page, with no inner scroll box. Rows absent from the page are absent from browser find-in-page, so provide a search or filter for large collections. Each row is an li with its position and the full list size. rowClassName supplies its role classes; as="ol" uses an ordered list. Other attributes go on the list itself: aria-label names it, aria-busy says a page is loading. Stable, unique keys keep the reader's first visible row in place when rows are inserted or removed above it, or its breakpoint height changes. An appended page needs no scroll correction. While a list is mounted, the browser's own scroll anchoring is off for the page (overflow-anchor: none on <html>): it would anchor on what follows the list and carry a reader at the end along with every page appended.

import { useCallback } from 'react'
import { WindowedList } from '@codefusion-cc/ui/react'

const getKey = useCallback((index: number) => orders[index].id, [orders])

<WindowedList
  aria-label="Orders"
  count={orders.length}
  getKey={getKey}
  className="[--row-height:56px] sm:[--row-height:64px]"
  renderRow={index => <a href={`/orders/${orders[index].id}`}>{orders[index].number}</a>}
/>

Use a positive rowHeight in pixels, or a positive px-only --row-height on the list. While --row-height cannot be read (its stylesheet still loading), the first rows stay, one error is logged, and the next resize measures again. CSS is read once per ResizeObserver callback; content must fit that fixed row height. Lists whose contents wrap to different heights need a different layout or a variable-height implementation. The list owns its positioning and total height; keep its own padding at zero. Put row padding inside the fixed height, through rowClassName or its contents. A scroll, a jump, a Tab step or a breakpoint change renders its new rows before the browser paints that frame, so keep renderRow cheap.

useWindowedList({ count, rowHeight?, overscan?, getKey }) exposes { listProps, rows, scrollToIndex } for a list that supplies its own row attributes. Spread listProps on the ul or ol; give each li its row's key and style, aria-setsize={count}, and aria-posinset={index + 1}. Keep keys stable and unique across updates. Every key is rebuilt when getKey changes, so memoize it with useCallback, for the hook and the component alike. Tab and Shift+Tab mount the adjacent row at a window boundary, while focus keeps a row mounted after it leaves the viewport. scrollToIndex(i) scrolls smoothly unless motion is reduced, when it jumps (behavior: 'instant', which a page's scroll-behavior: smooth does not animate). On the server the first overscan rows render without measuring CSS; hydration measures before the first paint. windowRange({ scrollY, listTop, viewport, rowHeight, count, overscan? }) returns the pure half-open range. Invalid geometry, duplicate keys, or an out-of-range scroll index are programming errors and throw.

The page behind a dialog

overflow: hidden on the body does not hold on iOS Safari: with the keyboard open, a drag on the dialog moves the page behind it. lockPageBehind(dialog) pins the body at its scroll offset and cancels any one-finger drag that nothing inside the topmost dialog can scroll, unless it lands on a slider or an element with touch-action: none. Two fingers still zoom. Locks nest: the last unlock puts the body's styles and the scroll offset back. Every copy of the package on the page shares one lock.

import { usePageLock } from '@codefusion-cc/ui/react'

usePageLock(dialogRef, open)

Code outside React calls lockPageBehind(element) of @codefusion-cc/ui/page-lock and the function it returns. The pinned body keeps sticky elements in place (Firefox moves them with the page), but window.scrollY reads 0 while it is pinned: a header that changes its look once the page is scrolled reads pageScrollY() of @codefusion-cc/ui/page-lock instead, so it stays the same behind the dialog. usePageLock pins the page after the dialog's first paint, so the frame that answers the tap does not wait for it, and gives it back in a layout effect, so a dialog closed by a navigation gives the page back before a router's scroll restoration runs.

A test drives it with drag(target, from, to) from @codefusion-cc/ui/testing, which is true when the move was cancelled. Headless engines have no touch screen, so what iOS itself does is checked on a phone.

The preview

npm run dev, then open /__looks: the token sheet and every registered state list, in each look, light and dark side by side, with the contrast of each pair. A look file changed updates it in place. ?look=kuznia shows one look; ?list=button&look=kuznia&mode=dark shows one list in one look and mode, the whole page in it, with nothing around: what a visual test shoots.

State lists are how the shared components (and an app's own) show each state they can be in. Name the modules in uiLooks({ states: ['./src/states/buttons.ts'] }), and for states drawn with the app's classes, its stylesheet in styles. The kit's own lists are @codefusion-cc/ui/react/states:

uiLooks({ looks: ['./src/looks/kuznia.ts'], states: ['@codefusion-cc/ui/react/states', './src/states/buttons.ts'], styles: ['./src/index.css'] })

A list names the page widths its visual test shoots it at (default [390], a phone; add 1280 where the layout changes).

// src/states/buttons.ts
import { defineStates } from '@codefusion-cc/ui/testing'

export default defineStates({
  id: 'buttons',
  title: 'Buttons',
  states: [
    { name: 'Idle', props: { label: 'Save' } },
    { name: 'Busy', props: { label: 'Saving', busy: true } },
  ],
  render(target, props) {
    const button = document.createElement('button')
    button.textContent = props.label
    button.disabled = Boolean(props.busy)
    target.append(button)
  },
})

Visual tests

packages/web/ui/visual/ is an app of the three fixture looks and the kit's state lists. Its suite shoots every list in every look, light and dark, at each of its widths, one forced-colours picture a list, and checks that every look and mode gives a screen reader the same tree. The pictures are compared in the pinned Playwright image only (@codefusion-cc/testing), through a container:

pnpm visual

The Visual workflow runs it on every pull request that touches packages/web/ui, in the same image, on Tenki's Linux runner (the repository variable CI_RUNNER_DOCKER names another): our own runners give a job no Docker, and GitHub's hosted runners are refused at the organization's $0 spending limit. Run it locally too before a look change goes up. On a Mac it needs a container runtime that runs linux/amd64 images, such as Colima with Rosetta (colima start --vm-type vz --vz-rosetta).

A change that alters how the kit looks on purpose takes the pictures again with pnpm visual:update and commits them; the pull request shows each one changed. Without a container runtime, or for a new Playwright image, the Visual workflow takes them in CI: gh workflow run visual.yml --ref <branch> -f update=true, then commit what its visual-baselines artifact holds.

Design tokens

@codefusion-cc/ui/tokens.json is the contract in the DTCG format, and lookTokens(look) turns a look into the same format, for design tools. Colours are OKLCH components with their hex. A colour that only names another role stays an alias ({color.accent}). Both modes and the custom property are under $extensions["cc.codefusion"], and an app's tokens under app.<prefix>.<type> (app.hh.color.me).

版本与发布记录

@codefusion-cc/ui 共发布 11 个版本,首次发布于 2026/10/13;过去 12 个月发布了 11 个版本。

版本发布日期距今
0.2.9latest2026/11/091 天前
0.2.82026/11/018 天前
0.2.62026/10/2219 天前
0.2.52026/10/2219 天前
0.2.42026/10/1724 天前
0.2.32026/10/1724 天前

关于 @codefusion-cc/ui 的常见问题

@codefusion-cc/ui 是什么?

The token contract every CodeFusion look fills, and the kit built on it: typed roles (colours, fonts, radii, shadows, control heights, motion), defineLook checked at compile time, lookProblems for WCAG 2.2 AA contrast and target sizes in light and dark, C

如何安装 @codefusion-cc/ui?

在项目目录运行 npm install @codefusion-cc/ui,或使用 pnpm add / yarn add / bun add。

@codefusion-cc/ui 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@codefusion-cc/ui 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@codefusion-cc/ui 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=20。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@codefusion-cc/ui 可以商用吗?

@codefusion-cc/ui 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@codefusion-cc/ui 的下载量有多少?

根据 npm 官方下载统计,@codefusion-cc/ui 在 2026/10/10 – 2026/11/08 这 30 天内被下载 2,150 次,日均约 72 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@codefusion-cc/ui 还在维护吗?

最新版本 0.2.9 发布于 2026/11/09(1 天前);至今共发布 11 个版本,其中过去 12 个月发布了 11 个;首次发布于 2026/10/13。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。