From ffdf4c0fcb440ba798918417c9367d20510634d1 Mon Sep 17 00:00:00 2001 From: Danny Banks Date: Fri, 3 Apr 2026 21:23:53 -0700 Subject: [PATCH 1/2] TypeStyles: Instance mode --- .changeset/instance-styles-tokens-api.md | 17 ++ README.md | 31 ++- docs/DOC_STRUCTURE.md | 3 +- docs/content/docs/api-reference.md | 208 +++++++----------- docs/content/docs/class-naming.md | 62 +++--- docs/content/docs/testing.md | 2 +- docs/content/docs/tokens.md | 29 ++- examples/design-system/README.md | 61 +++-- .../design-system/src/components/alert.ts | 3 +- .../design-system/src/components/badge.ts | 2 +- .../design-system/src/components/button.ts | 2 +- examples/design-system/src/components/card.ts | 2 +- .../design-system/src/components/checkbox.ts | 2 +- .../design-system/src/components/codeBlock.ts | 2 +- .../src/components/codeHighlight.ts | 2 +- .../src/components/commandPalette.ts | 2 +- .../design-system/src/components/dialog.ts | 2 +- .../design-system/src/components/fileTree.ts | 2 +- examples/design-system/src/components/link.ts | 2 +- .../src/components/proseContent.ts | 2 +- .../design-system/src/components/radio.ts | 2 +- .../design-system/src/components/select.ts | 2 +- .../design-system/src/components/steps.ts | 2 +- .../design-system/src/components/styles.ts | 2 +- .../design-system/src/components/switch.ts | 2 +- examples/design-system/src/components/tabs.ts | 2 +- .../src/components/textAreaField.ts | 2 +- .../design-system/src/components/textField.ts | 2 +- examples/design-system/src/create-theme.ts | 3 +- examples/design-system/src/index.ts | 1 + examples/design-system/src/runtime.ts | 9 + examples/design-system/src/tokens/index.ts | 3 +- packages/typestyles/CHANGELOG.md | 10 + packages/typestyles/src/class-naming.test.ts | 56 ++--- packages/typestyles/src/class-naming.ts | 40 ++-- packages/typestyles/src/component.test.ts | 66 +++--- packages/typestyles/src/component.ts | 74 +++++-- packages/typestyles/src/index.ts | 47 ++-- packages/typestyles/src/server.test.ts | 11 +- packages/typestyles/src/styles.test.ts | 41 ++-- packages/typestyles/src/styles.ts | 85 ++++++- packages/typestyles/src/theme.ts | 43 ++-- packages/typestyles/src/tokens.test.ts | 50 +++-- packages/typestyles/src/tokens.ts | 130 ++++++----- scripts/generate-api-reference.js | 48 +++- 45 files changed, 682 insertions(+), 489 deletions(-) create mode 100644 .changeset/instance-styles-tokens-api.md create mode 100644 examples/design-system/src/runtime.ts diff --git a/.changeset/instance-styles-tokens-api.md b/.changeset/instance-styles-tokens-api.md new file mode 100644 index 0000000..74fce99 --- /dev/null +++ b/.changeset/instance-styles-tokens-api.md @@ -0,0 +1,17 @@ +--- +'typestyles': major +--- + +Replace global class naming with **instance-based** APIs. + +**Breaking:** Remove `configureClassNaming`, `getClassNamingConfig`, and `resetClassNaming`. Use **`createStyles({ mode?, prefix?, scopeId? })`** for a dedicated style API (same surface as the default `styles` export). The default `import { styles } from 'typestyles'` is `createStyles()` with default options. + +**Breaking:** **`createTokens({ scopeId? })`** returns the token and theme API (`create`, `use`, `createTheme`, `createDarkMode`, `when`, `colorMode`, plus read-only `scopeId`). The default `import { tokens } from 'typestyles'` is `createTokens()`. When `scopeId` is set, emitted custom properties and theme class segments are prefixed (sanitized) so multiple bundles on one page do not collide. + +**Breaking:** Low-level **`createComponent`**, **`createClass`**, and **`createHashClass`** now take **`ClassNamingConfig`** as the first argument when imported from implementation modules; application code should use `createStyles()` or the default `styles` object. + +**Breaking:** **`createTheme`** and **`createDarkMode`** accept an optional third argument **`scopeId`** for unscoped usage; instances from `createTokens({ scopeId })` bind scope automatically. + +New exports: **`mergeClassNaming`**, **`defaultClassNamingConfig`**, **`scopedTokenNamespace`**, **`StylesApi`**, **`TokensApi`**, **`CreateTokensOptions`**. Style instances expose read-only **`classNaming`**. + +Documentation, examples, and the design-system package are updated to describe and use the new pattern. diff --git a/README.md b/README.md index 67b1978..db8125c 100644 --- a/README.md +++ b/README.md @@ -189,9 +189,7 @@ const text = styles.component('text', { // Destructure and compose with cx() const { base, heading, bold, muted } = text; -

- {/* class="text-base text-heading text-bold" */} -

; +

{/* class="text-base text-heading text-bold" */}

; ``` Use the built-in `cx` utility to combine classes from different sources: @@ -377,21 +375,36 @@ spacing.sm; // "var(--spacing-sm)" (typed as a CSS value) spacing.md; // "var(--spacing-md)" ``` -### `tokens.createTheme(name, overrides)` +### `tokens.createTheme(name, config)` -Creates a theme class that overrides token values. +Creates a theme class that overrides token values (nested token shapes under `base`, plus optional `modes` / `colorMode`). ```tsx const dark = tokens.createTheme('dark', { - color: { - surface: '#1a1a2e', - text: '#e0e0e0', + base: { + color: { + surface: '#1a1a2e', + text: '#e0e0e0', + }, }, }); -
{/* class="theme-dark" */} +
+``` + +### `createStyles(options?)` and `createTokens(options?)` + +For **libraries, design systems, or micro-frontends**, create your own instances so class names and CSS variables stay isolated—no global configuration: + +```tsx +import { createStyles, createTokens } from 'typestyles'; + +export const styles = createStyles({ scopeId: 'my-ui', mode: 'hashed', prefix: 'ui' }); +export const tokens = createTokens({ scopeId: 'my-ui' }); ``` +The default `import { styles, tokens } from 'typestyles'` remains `createStyles()` / `createTokens()` with default options for single-app use. + ### `tokens.use(namespace)` References tokens defined elsewhere. Useful for consuming shared tokens without importing the definition file. diff --git a/docs/DOC_STRUCTURE.md b/docs/DOC_STRUCTURE.md index 5cba7e3..2baebf5 100644 --- a/docs/DOC_STRUCTURE.md +++ b/docs/DOC_STRUCTURE.md @@ -9,8 +9,9 @@ - styles.md - components.md +- compose.md +- atomic-css.md - tokens.md -- open-props.md - keyframes.md - color.md diff --git a/docs/content/docs/api-reference.md b/docs/content/docs/api-reference.md index 3976833..f3c3847 100644 --- a/docs/content/docs/api-reference.md +++ b/docs/content/docs/api-reference.md @@ -5,52 +5,78 @@ description: Complete API reference for typestyles # API Reference -Reference for the main typestyles APIs (kept in sync with the published package). +Auto-generated documentation for all typestyles APIs. ## Core Exports ### `styles` -Style creation and composition API. +Default style API (semantic class names, empty `scopeId`). Prefer `createStyles({ scopeId, mode, prefix })` +per package or micro-frontend for isolation. **Methods:** -- `styles.component(namespace, config)`: Creates component styles (flat or dimensioned). Returns a CVA-style object that is both callable and destructurable. -- `styles.class(name, style)`: Creates a single registered class from one style object -- `styles.hashClass(styles, label?)`: Emits a hashed class from a style object (see [Class naming](/docs/class-naming)) -- `styles.withUtils(utils)`: Returns utility-aware `component`, `class`, and `hashClass` helpers -- `styles.compose(...selectors)`: Combines multiple selector functions or class strings +- `styles.component(namespace, config)`: Create multi-variant component styles (CVA-style) +- `styles.class(name, properties)`: Create a single class +- `styles.hashClass(properties, label?)`: Create a deterministic hashed class +- `styles.compose(...fns)`: Compose multiple style functions +- `styles.withUtils(utils)`: Create utility-aware styles API +- `styles.classNaming`: Read-only resolved naming config for the default `styles` instance -### `cx` +### `createStyles(options?)` -Built-in class joining utility: +Returns a new style API (same shape as `styles`) with its own class naming config. Pass `Partial`: `mode` (`'semantic' | 'hashed' | 'atomic'`), `prefix`, `scopeId`. Use one instance per package or micro-frontend. -```ts -import { cx } from 'typestyles'; +The default `import { styles } from 'typestyles'` is `createStyles()` with default options. -cx('class-a', isActive && 'class-b', undefined, 'class-c'); -// "class-a class-b class-c" (falsy values are filtered out) -``` +### `tokens` -### Class naming +Default token API (unscoped custom properties). Prefer `createTokens({ scopeId })` when multiple +bundles share a page. -Global options for emitted class strings (used by `styles.component`, `styles.class`): +**Methods:** -- `configureClassNaming(options)`: Set `mode` (`'semantic' | 'hashed' | 'atomic'`), optional `prefix`, optional `scopeId` -- `getClassNamingConfig()`: Read current config -- `resetClassNaming()`: Restore defaults (mainly for tests) +- `tokens.create(namespace, values)`: Creates CSS custom properties +- `tokens.use(namespace)`: References existing tokens +- `tokens.createTheme(name, config)`: Registers a theme class that overrides token custom properties +- `tokens.createDarkMode(name, darkOverrides)`: Shorthand theme with a single dark `@media` branch +- `tokens.when` / `tokens.colorMode`: Condition helpers for themes +- `tokens.scopeId`: The scope passed to `createTokens`, if any -See [Class naming](/docs/class-naming). +### `createTokens(options?)` -### `tokens` +Returns a token + theme API bound to an optional `scopeId`. When set, `tokens.create('color', …)` emits `--{scopeId}-color-*` variables and `tokens.createTheme('dark', …)` registers `.theme-{scopeId}-dark` (sanitized segments). + +The default `import { tokens } from 'typestyles'` is `createTokens()` (no scope). -Design token API using CSS custom properties. +### `keyframes` + +Keyframe animation API. **Methods:** -- `tokens.create(namespace, values)`: Registers tokens on `:root` and returns `var(--namespace-key)` references -- `tokens.use(namespace)`: Returns `var(--namespace-key)` references without emitting CSS (for shared tokens defined elsewhere) -- `tokens.createTheme(name, overrides)`: Registers a `.theme-{name}` class that overrides token custom properties for a subtree +- `keyframes.create(name, stops)`: Creates @keyframes animation + +### `color` + +Type-safe CSS color function helpers. +Each function returns a plain CSS color string — no runtime color math. +Composes naturally with token references. + +**Functions:** + +- `color.rgb(r, g, b, alpha?)`: RGB color +- `color.hsl(h, s, l, alpha?)`: HSL color +- `color.oklch(l, c, h, alpha?)`: OKLCH color +- `color.oklab(l, a, b, alpha?)`: OKLAB color +- `color.lab(l, a, b, alpha?)`: LAB color +- `color.lch(l, c, h, alpha?)`: LCH color +- `color.hwb(h, w, b, alpha?)`: HWB color +- `color.mix(c1, c2, p?, space?)`: Mix two colors +- `color.lightDark(light, dark)`: Light/dark mode color +- `color.alpha(color, opacity, space?)`: Adjust opacity + +See [Color](/docs/color). ### `global` @@ -59,10 +85,6 @@ Global CSS helpers (not scoped to a component class): - `global.style(selector, styles)`: Insert rules for an arbitrary selector - `global.fontFace(family, props)`: Register `@font-face` -### `color` - -Type-safe helpers that return CSS color strings (`rgb`, `hsl`, `oklch`, `mix`, `alpha`, `lightDark`, etc.). See [Color](/docs/color). - ### `cx(...parts)` Joins class name parts into a single string, filtering out falsy values (`false`, `undefined`, `null`, `0`, `''`). @@ -72,13 +94,12 @@ Use `cx` to combine TypeStyles classes with external class strings and condition ```ts import { cx, styles } from 'typestyles'; -const card = styles.create('card', { +const card = styles.component('card', { base: { padding: '16px' }, - active: { borderColor: 'blue' }, + elevated: { boxShadow: '0 4px 12px rgba(0,0,0,0.1)' }, }); -cx(card('base'), isActive && card('active'), externalClassName); -// => "card-base card-active my-external-class" +cx(card('base'), isElevated && card('elevated'), externalClassName); ``` ### CSS variables (advanced) @@ -91,61 +112,32 @@ cx(card('base'), isActive && card('active'), externalClassName); - `reset()`, `flushSync()`, `ensureDocumentStylesAttached()`: Primarily for tests and advanced setup; see [Testing](/docs/testing) - `insertRules(rules)`: Low-level rule insertion (mainly for library authors) -### `keyframes` - -Keyframe animation API. +### Class naming helpers -**Methods:** +- `mergeClassNaming(partial?)`: Build a full `ClassNamingConfig` from partial options +- `defaultClassNamingConfig`: Default `mode`, `prefix`, and `scopeId` +- `scopedTokenNamespace(scopeId, logicalNamespace)`: CSS variable namespace segment for scoped token instances -- `keyframes.create(name, stops)`: Creates @keyframes animation +See [Class naming](/docs/class-naming). ## Usage Examples -### Creating Component Styles (flat config) - -```ts -import { styles } from 'typestyles'; - -const card = styles.component('card', { - base: { padding: '16px', borderRadius: '8px' }, - elevated: { boxShadow: '0 4px 12px rgba(0,0,0,0.1)' }, -}); - -// Call as function (base auto-applied): -card(); - -// Destructure: -const { base, elevated } = card; -``` - -### Creating Component Styles (dimensioned config) +### Creating Styles ```ts import { styles } from 'typestyles'; const button = styles.component('button', { - base: { borderRadius: '8px' }, + base: { padding: '8px 16px' }, variants: { - intent: { - primary: { backgroundColor: '#2563eb', color: 'white' }, - ghost: { backgroundColor: 'transparent', color: '#111827' }, - }, - }, - defaultVariants: { - intent: 'primary', + intent: { primary: { backgroundColor: '#0066ff' } }, }, + defaultVariants: { intent: 'primary' }, }); button(); // "button-base button-intent-primary" -button({ intent: 'ghost' }); // "button-base button-intent-ghost" -``` - -### Joining Classes with cx() - -```ts -import { cx } from 'typestyles'; - -const className = cx('base-class', isActive && 'active', isPrimary && 'primary'); +button({ intent: 'primary' }); // same +const { base } = button; // destructure class strings ``` ### Creating Tokens @@ -161,6 +153,15 @@ const color = tokens.create('color', { color.primary; // "var(--color-primary)" ``` +### Scoped instances (libraries / micro-frontends) + +```ts +import { createStyles, createTokens } from 'typestyles'; + +export const styles = createStyles({ scopeId: 'my-ds', mode: 'hashed', prefix: 'ds' }); +export const tokens = createTokens({ scopeId: 'my-ds' }); +``` + ### Creating Animations ```ts @@ -175,64 +176,7 @@ const fadeIn = keyframes.create('fadeIn', { animation: `${fadeIn} 300ms ease`; ``` -### Composing Styles - -```ts -import { styles } from 'typestyles'; - -const base = styles.component('base', { - base: { padding: '8px' }, -}); - -const primary = styles.component('primary', { - base: { color: 'blue' }, -}); - -const button = styles.compose(base, primary); -``` - -## @typestyles/props - -Atomic CSS utility generator for type-safe utility classes. - -### `defineProperties(config)` - -Define a collection of CSS properties with allowed values. - -```ts -import { defineProperties } from '@typestyles/props'; - -const utilities = defineProperties({ - conditions: { - mobile: { '@media': '(min-width: 768px)' }, - }, - properties: { - display: ['flex', 'block', 'grid'], - padding: { 0: '0', 1: '4px', 2: '8px' }, - }, - shorthands: { - p: ['padding'], - }, -}); -``` - -### `createProps(namespace, ...collections)` - -Generate atomic utility classes from property collections. - -```ts -import { createProps } from '@typestyles/props'; - -const atoms = createProps('atom', utilities); - -atoms({ - display: 'flex', - padding: 2, - p: { mobile: 1 }, -}); -// Returns: "atom-display-flex atom-padding-2 atom-p-mobile-1" -``` - --- -_Last reviewed: 2026-04-02_ +_This API reference was auto-generated from source code._ +_Last updated: 2026-04-04_ diff --git a/docs/content/docs/class-naming.md b/docs/content/docs/class-naming.md index 981d7da..567e8ed 100644 --- a/docs/content/docs/class-naming.md +++ b/docs/content/docs/class-naming.md @@ -1,6 +1,6 @@ --- title: Class naming -description: Configure semantic, hashed, or atomic-style class names for styles.create, styles.class, and styles.component +description: Per-instance semantic, hashed, or atomic class names via createStyles; scoped tokens via createTokens --- # Class naming @@ -9,7 +9,6 @@ By default, typestyles emits **readable semantic** class names: `button-base`, ` Naming applies to: -- [`styles.create`](/docs/styles) - [`styles.class`](/docs/styles) - [`styles.component`](/docs/components) (single-part components and [multipart `slots`](/docs/components)) @@ -17,39 +16,51 @@ It does **not** change [`@typestyles/props`](/docs/atomic-css) utility naming; t ## Quick start -Call **`configureClassNaming`** once at app or package entry (for example your design system `index.ts` or root `main.tsx`) **before** modules register styles: +**Class names** are configured per **`createStyles()`** instance (not with a global singleton). Create one instance per package, design system, or micro-frontend and import that everywhere in the package: ```ts -import { configureClassNaming } from 'typestyles'; +import { createStyles } from 'typestyles'; -configureClassNaming({ +export const styles = createStyles({ mode: 'hashed', prefix: 'ds', scopeId: '@acme/design-system', }); ``` -Then existing `styles.create` / `styles.component` calls keep the same TypeScript API; only the emitted `class` strings and generated selectors change. +Use `styles.component`, `styles.class`, `styles.hashClass`, and `styles.withUtils` from that object. The default `import { styles } from 'typestyles'` is simply `createStyles()` with default options—fine for apps that own the whole page. + +**Tokens and themes** use the same idea: **`createTokens({ scopeId })`** so custom properties and theme classes do not collide when multiple bundles share one document: + +```ts +import { createTokens } from 'typestyles'; + +export const tokens = createTokens({ scopeId: '@acme/design-system' }); +``` + +With `scopeId` set, `tokens.create('color', …)` emits variables like `--acme-design-system-color-primary` (sanitized), and `tokens.createTheme('dark', …)` registers a theme class whose segment includes the scope. ## API -### `configureClassNaming(options)` +### `createStyles(options?)` -Merges into the current global config (partial updates are allowed). +Returns a style API with the same methods as the default `styles` export. Options are a partial **`ClassNamingConfig`** merged onto defaults: | Option | Type | Default | Description | | --------- | ------------------------------------ | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | | `mode` | `'semantic' \| 'hashed' \| 'atomic'` | `'semantic'` | How class strings are built (see below). | -| `prefix` | `string` | `'ts'` | Leading segment for hashed/atomic output and for [`styles.hashClass`](#styleshashclass). | +| `prefix` | `string` | `'ts'` | Leading segment for hashed/atomic output and for `hashClass`. | | `scopeId` | `string` | `''` | Optional id (package name, app name) mixed into the hash input so two packages can reuse the same logical namespace without sharing the same class string. | -### `getClassNamingConfig()` +The instance also exposes **`styles.classNaming`**: a read-only snapshot of the resolved config (useful for debugging). + +### `mergeClassNaming(partial?)` and `defaultClassNamingConfig` -Returns a read-only snapshot of the active config (useful for debugging). +Use these when you need the resolved config object without creating a full API (for example tests or tooling). -### `resetClassNaming()` +### `scopedTokenNamespace(scopeId, logicalNamespace)` -Restores defaults. Intended for **tests** so one suite cannot leak naming mode into another; not something you typically call in application code. +Returns the CSS custom property namespace segment used for `tokens.create` when `scopeId` is set (sanitized). Advanced / library use. ## Modes @@ -57,7 +68,7 @@ Restores defaults. Intended for **tests** so one suite cannot leak naming mode i Human-readable, stable names derived from the namespace and variant segment: -- `styles.create('card', { base: { … } })` → `card-base` +- `styles.class('card', { … })` → `card` - `styles.component('button', { … })` → `button-base`, `button-intent-primary`, etc. - Components with `slots` → `{namespace}-{slot}`, `{namespace}-{slot}-{dimension}-{option}`, etc. @@ -75,29 +86,29 @@ This mode is a **prototype** for hash-only ergonomics: each component rule is st ## `styles.hashClass` -[`styles.hashClass(styles, label?)`](/docs/styles) always emits a hashed class from the style object. It uses the configured **`prefix`**. If **`scopeId`** is non-empty, it is included in the hash input so scoped packages do not collide. - -When `scopeId` is the default empty string, the hash input matches the previous behavior (properties only, plus label handling), so existing class strings stay stable if you only adopt `prefix` or other naming modes for `create` / `component`. +`hashClass` on a given instance uses that instance’s **`prefix`** and **`scopeId`**. If **`scopeId`** is empty, the hash input matches the historical behavior (properties only, plus label handling) for the same style shape. ## Monorepos and `scopeId` -Two packages might both use `styles.create('button', …)` or `styles.component('button', …)`. With **`semantic`** mode, you rely on distinct namespaces. With **`hashed`** / **`atomic`**, set a different **`scopeId`** per package (for example the npm package name) so identical style objects in different packages do not map to the same class string. +Two packages might both use `styles.component('button', …)`. With **`semantic`** mode, you rely on distinct namespaces or separate bundles. With **`hashed`** / **`atomic`**, give each package its own **`createStyles({ scopeId: '…' })`** so identical style objects in different packages do not map to the same class string. -## SSR and entry order +For tokens, use **`createTokens({ scopeId })`** per package so `--color-*` and `.theme-*` rules do not overwrite each other on `:root` or clash by name. -Naming is **global** for the loaded bundle. Ensure **`configureClassNaming`** runs before any module that calls `styles.create`, `styles.class`, or `styles.component` during that load. In SSR, the server bundle should apply the same configuration as the client so class names and injected CSS match. +## SSR + +Use the **same** `createStyles` / `createTokens` options (including `scopeId`) on the server and the client so class names, custom property names, and injected CSS match. ## Testing -If tests call **`configureClassNaming`**, reset in **`beforeEach`** (or **`afterEach`**) so other tests keep the default: +Use a **dedicated** `createStyles({ … })` per test file or suite when you need hashed/atomic mode. There is no global naming state to reset—only call **`reset()`** (and related sheet helpers) to clear injected CSS between tests. Default **`import { styles } from 'typestyles'`** is still shared across tests, so prefer a local `createStyles()` when asserting on class strings under non-semantic modes. ```ts -import { resetClassNaming } from 'typestyles'; -import { reset } from 'typestyles'; +import { createStyles, reset } from 'typestyles'; + +const styles = createStyles({ mode: 'hashed', prefix: 't', scopeId: 'test-a' }); beforeEach(() => { reset(); - resetClassNaming(); }); ``` @@ -107,7 +118,8 @@ See also [Testing](/docs/testing). ## Related -- [Styles](/docs/styles) — `styles.create`, `styles.class`, `compose`, `withUtils` +- [Styles](/docs/styles) — `styles.class`, `compose`, `withUtils` - [Components](/docs/components) — `styles.component` and `slots` +- [Tokens](/docs/tokens) — `createTokens` and scoped custom properties - [Atomic CSS Utilities](/docs/atomic-css) — `@typestyles/props` (separate naming scheme) - [API Reference](/docs/api-reference) — export list diff --git a/docs/content/docs/testing.md b/docs/content/docs/testing.md index 3cadc45..6b7f908 100644 --- a/docs/content/docs/testing.md +++ b/docs/content/docs/testing.md @@ -133,7 +133,7 @@ it('matches snapshot', () => { ### Class naming mode -If you use [hashed or atomic class naming](/docs/class-naming) in tests, call **`resetClassNaming()`** alongside any stylesheet reset (for example `reset()` from `typestyles`) in `beforeEach` so naming options do not leak between files. Assertions that depend on exact class strings may need snapshots or prefix-based checks when `mode` is not `semantic`. +If you use [hashed or atomic class naming](/docs/class-naming) in tests, prefer a dedicated **`createStyles({ mode, prefix, scopeId })`** instance in that file so naming options never leak. The default **`import { styles } from 'typestyles'`** is a single shared instance. Always call **`reset()`** (from `typestyles`) in `beforeEach` when tests inject CSS. Assertions that depend on exact class strings may need snapshots or prefix-based checks when `mode` is not `semantic`. ## CSS testing strategies diff --git a/docs/content/docs/tokens.md b/docs/content/docs/tokens.md index 14c3879..43692ed 100644 --- a/docs/content/docs/tokens.md +++ b/docs/content/docs/tokens.md @@ -7,6 +7,21 @@ description: Design tokens and theming with tokens.create and createTheme Tokens are design primitives (colors, spacing, etc.) exposed as CSS custom properties. They keep your styles consistent and make theming straightforward. +## Scoped token instances + +The default `import { tokens } from 'typestyles'` is unscoped. For a **package or micro-frontend** that shares the page with other TypeStyles bundles, call **`createTokens({ scopeId })`** once and reuse that instance so custom properties and theme classes do not collide: + +```ts +import { createTokens } from 'typestyles'; + +export const tokens = createTokens({ scopeId: 'acme-ui' }); + +const color = tokens.create('color', { primary: '#0066ff' }); +// var(--acme-ui-color-primary) +``` + +See [Class naming](/docs/class-naming) for how this pairs with `createStyles({ scopeId })` for styles. + ## Creating tokens Use `tokens.create(prefix, object)` to define a set of tokens: @@ -41,16 +56,18 @@ When tokens are created in another module or package, use `tokens.use(namespace) ## Theming -Use `tokens.createTheme(name, overrides)` to define a theme that overrides token values: +Use `tokens.createTheme(name, config)` with a **`base`** object (and optional `modes` or `colorMode`) to override token values: ```ts const dark = tokens.createTheme('dark', { - color: { - primary: '#66b3ff', - text: '#e0e0e0', - surface: '#1a1a2e', + base: { + color: { + primary: '#66b3ff', + text: '#e0e0e0', + surface: '#1a1a2e', + }, }, }); ``` -Apply the theme by adding the theme class to a parent (e.g. `document.body.classList.add(dark)`). All token references under that subtree will use the overridden values. +Apply the theme by adding **`dark.className`** to a parent (e.g. `document.body.classList.add(dark.className)`). All token references under that subtree will use the overridden values. With a scoped `createTokens({ scopeId })`, the class name includes that scope (sanitized), for example `.theme-acme-ui-dark`. diff --git a/examples/design-system/README.md b/examples/design-system/README.md index cf99e3f..cf40bbe 100644 --- a/examples/design-system/README.md +++ b/examples/design-system/README.md @@ -18,44 +18,37 @@ Tokens are grouped for clarity; recipes consume the flat `designTokens` object ( **`designMotion`** is a convenience handle: `designMotion.duration.fast`, `designMotion.transition.panelEnter`, etc. New presets include `transition.colorShift` (links) and `transition.controlSurface` (buttons). -## Theme classes +## TypeStyles runtime (`src/runtime.ts`) + +Recipes import **`styles`** and **`tokens`** from this module instead of the default `typestyles` exports. That gives the package its own instances (no global naming API) and lets you add **`scopeId`** later if this bundle ever shares a page with another TypeStyles consumer. -First-class theme classes (all are strings you add to a root element, typically ``): - -| Export | Class | Purpose | -| ----------------------------- | ------------------- | ---------------------------------------------------------------- | -| `lightThemeClass` | `theme-ds-light` | Explicit light palette + light syntax (matches `:root` defaults) | -| `darkThemeClass` | `theme-ds-dark` | Default dark `color` tokens + dark syntax | -| `highContrastLightThemeClass` | `theme-ds-hc-light` | Stronger borders and body contrast on light surfaces | -| `highContrastDarkThemeClass` | `theme-ds-hc-dark` | Stronger borders and body contrast on dark surfaces | - -Strip conflicting classes before applying another (the docs app keeps a fixed list for palette/mode switching). - -### Astro (no React context) - -Use a tiny inline script or a client island to flip classes on `document.documentElement`. Persist mode in `localStorage` if you want it sticky: - -```astro ---- -import { lightThemeClass, darkThemeClass } from '@examples/design-system'; ---- - +```ts +export const styles = createStyles(); +export const tokens = createTokens(); +// With collision risk: createTokens({ scopeId: 'acme-ds' }) / createStyles({ scopeId: 'acme-ds' }) ``` -`import.meta.env.SSR` stays irrelevant: the snippet runs in the browser only. Adjust class lists if you also use palette themes (see `docs/src/tokens.ts` in this repo). +## Theme classes + +Palette themes are created with `createDesignTheme` and expose a **`className`** (and string coercion) for the root element: + +| Export | Typical `className` | Purpose | +| -------------- | ------------------- | ----------------------------- | +| `defaultTheme` | `theme-default` | Default slate palette + modes | +| `forestTheme` | `theme-forest` | Forest palette | +| `roseTheme` | `theme-rose` | Rose palette | +| `amberTheme` | `theme-amber` | Amber palette | + +Light/dark behavior for each theme is driven by the theme config (e.g. `data-mode`); use the theme object’s `className` from your app shell. Strip conflicting theme classes before switching palettes. ## Theming helpers ```ts -import { mergeDesignThemeOverrides, createBrandAccentOverrides } from '@examples/design-system'; -import { tokens } from 'typestyles'; +import { + mergeDesignThemeOverrides, + createBrandAccentOverrides, + tokens, +} from '@examples/design-system'; const brand = createBrandAccentOverrides({ accent: '#7c3aed', @@ -67,10 +60,12 @@ const subtleCodeBlock = mergeDesignThemeOverrides(brand, { codeBlock: { rootBg: '#0c1222', headerBg: '#111827' }, }); -export const themeAcme = tokens.createTheme('ds-acme', subtleCodeBlock); +export const themeAcme = tokens.createTheme('ds-acme', { + base: subtleCodeBlock, +}); ``` -Use `tokens.createTheme('ds-', overrides)` with the same namespace keys as `DesignThemeOverrides`: `color`, `codeSyntax`, `doc`, `codeBlock`, plus primitives. +Use `tokens.createTheme` with the same namespace keys as `DesignThemeOverrides`: `color`, `syntax`, `doc`, `codeBlock`, plus primitives. Prefer importing **`tokens`** from this package rather than from `typestyles` directly so apps and the design system share one token instance. ## Extending tokens safely diff --git a/examples/design-system/src/components/alert.ts b/examples/design-system/src/components/alert.ts index d0ef3f7..e53d288 100644 --- a/examples/design-system/src/components/alert.ts +++ b/examples/design-system/src/components/alert.ts @@ -1,4 +1,5 @@ -import { styles, type CSSProperties } from 'typestyles'; +import type { CSSProperties } from 'typestyles'; +import { styles } from '../runtime'; import { designTokens as t } from '../tokens'; export const alert = styles.component('alert', { diff --git a/examples/design-system/src/components/badge.ts b/examples/design-system/src/components/badge.ts index de55be4..7051c18 100644 --- a/examples/design-system/src/components/badge.ts +++ b/examples/design-system/src/components/badge.ts @@ -1,4 +1,4 @@ -import { styles } from 'typestyles'; +import { styles } from '../runtime'; import { designTokens as t } from '../tokens'; export const badge = styles.component('badge', { diff --git a/examples/design-system/src/components/button.ts b/examples/design-system/src/components/button.ts index c61349b..cb46616 100644 --- a/examples/design-system/src/components/button.ts +++ b/examples/design-system/src/components/button.ts @@ -1,4 +1,4 @@ -import { styles } from 'typestyles'; +import { styles } from '../runtime'; import { designTokens as t } from '../tokens'; export const button = styles.component('button', { diff --git a/examples/design-system/src/components/card.ts b/examples/design-system/src/components/card.ts index bfc8bb3..043f36a 100644 --- a/examples/design-system/src/components/card.ts +++ b/examples/design-system/src/components/card.ts @@ -1,4 +1,4 @@ -import { styles } from 'typestyles'; +import { styles } from '../runtime'; import { designTokens as t } from '../tokens'; export const card = styles.component('card', { diff --git a/examples/design-system/src/components/checkbox.ts b/examples/design-system/src/components/checkbox.ts index 25fd0fb..d676bb1 100644 --- a/examples/design-system/src/components/checkbox.ts +++ b/examples/design-system/src/components/checkbox.ts @@ -1,4 +1,4 @@ -import { styles } from 'typestyles'; +import { styles } from '../runtime'; import { designTokens as t } from '../tokens'; export const checkbox = styles.component('checkbox', { diff --git a/examples/design-system/src/components/codeBlock.ts b/examples/design-system/src/components/codeBlock.ts index 1b8b7a9..67a717d 100644 --- a/examples/design-system/src/components/codeBlock.ts +++ b/examples/design-system/src/components/codeBlock.ts @@ -1,4 +1,4 @@ -import { styles } from 'typestyles'; +import { styles } from '../runtime'; import { designTokens as t } from '../tokens'; export const codeBlock = styles.component('code-block', { diff --git a/examples/design-system/src/components/codeHighlight.ts b/examples/design-system/src/components/codeHighlight.ts index 46d0110..5bd0f9f 100644 --- a/examples/design-system/src/components/codeHighlight.ts +++ b/examples/design-system/src/components/codeHighlight.ts @@ -1,4 +1,4 @@ -import { styles } from 'typestyles'; +import { styles } from '../runtime'; import { designTokens as t } from '../tokens'; const s = t.syntax; diff --git a/examples/design-system/src/components/commandPalette.ts b/examples/design-system/src/components/commandPalette.ts index 25ee22e..d78d55e 100644 --- a/examples/design-system/src/components/commandPalette.ts +++ b/examples/design-system/src/components/commandPalette.ts @@ -1,4 +1,4 @@ -import { styles } from 'typestyles'; +import { styles } from '../runtime'; import { designTokens as t } from '../tokens'; const slots = [ diff --git a/examples/design-system/src/components/dialog.ts b/examples/design-system/src/components/dialog.ts index cccb711..4f83015 100644 --- a/examples/design-system/src/components/dialog.ts +++ b/examples/design-system/src/components/dialog.ts @@ -1,4 +1,4 @@ -import { styles } from 'typestyles'; +import { styles } from '../runtime'; import { designTokens as t } from '../tokens'; export const dialog = styles.component('dialog', { diff --git a/examples/design-system/src/components/fileTree.ts b/examples/design-system/src/components/fileTree.ts index 23f6535..91661b4 100644 --- a/examples/design-system/src/components/fileTree.ts +++ b/examples/design-system/src/components/fileTree.ts @@ -1,4 +1,4 @@ -import { styles } from 'typestyles'; +import { styles } from '../runtime'; import { designTokens as t } from '../tokens'; export const fileTree = styles.component('fileTree', { diff --git a/examples/design-system/src/components/link.ts b/examples/design-system/src/components/link.ts index 01b750e..f400e8b 100644 --- a/examples/design-system/src/components/link.ts +++ b/examples/design-system/src/components/link.ts @@ -1,4 +1,4 @@ -import { styles } from 'typestyles'; +import { styles } from '../runtime'; import { designTokens as t } from '../tokens'; export const link = styles.class('link', { diff --git a/examples/design-system/src/components/proseContent.ts b/examples/design-system/src/components/proseContent.ts index 212c07f..83fea31 100644 --- a/examples/design-system/src/components/proseContent.ts +++ b/examples/design-system/src/components/proseContent.ts @@ -1,4 +1,4 @@ -import { styles } from 'typestyles'; +import { styles } from '../runtime'; import { designTokens as t } from '../tokens'; const bp = '@media (max-width: 768px)'; diff --git a/examples/design-system/src/components/radio.ts b/examples/design-system/src/components/radio.ts index 1acb6a6..2f23e48 100644 --- a/examples/design-system/src/components/radio.ts +++ b/examples/design-system/src/components/radio.ts @@ -1,4 +1,4 @@ -import { styles } from 'typestyles'; +import { styles } from '../runtime'; import { designTokens as t } from '../tokens'; export const radio = styles.component('radio', { diff --git a/examples/design-system/src/components/select.ts b/examples/design-system/src/components/select.ts index 0b72a21..8d00f5a 100644 --- a/examples/design-system/src/components/select.ts +++ b/examples/design-system/src/components/select.ts @@ -1,4 +1,4 @@ -import { styles } from 'typestyles'; +import { styles } from '../runtime'; import { designTokens as t } from '../tokens'; export const select = styles.component('select', { diff --git a/examples/design-system/src/components/steps.ts b/examples/design-system/src/components/steps.ts index 18b8bd4..57793eb 100644 --- a/examples/design-system/src/components/steps.ts +++ b/examples/design-system/src/components/steps.ts @@ -1,4 +1,4 @@ -import { styles } from 'typestyles'; +import { styles } from '../runtime'; import { designTokens as t } from '../tokens'; export const steps = styles.component('steps', { diff --git a/examples/design-system/src/components/styles.ts b/examples/design-system/src/components/styles.ts index 2e80fb2..80b2640 100644 --- a/examples/design-system/src/components/styles.ts +++ b/examples/design-system/src/components/styles.ts @@ -1,4 +1,4 @@ -import { styles } from 'typestyles'; +import { styles } from '../runtime'; import { designTokens as t } from '../tokens'; export const layout = styles.component('ds-layout', { diff --git a/examples/design-system/src/components/switch.ts b/examples/design-system/src/components/switch.ts index 2ebd86f..351da40 100644 --- a/examples/design-system/src/components/switch.ts +++ b/examples/design-system/src/components/switch.ts @@ -1,4 +1,4 @@ -import { styles } from 'typestyles'; +import { styles } from '../runtime'; import { designTokens as t } from '../tokens'; export const switchStyles = styles.component('switch', { diff --git a/examples/design-system/src/components/tabs.ts b/examples/design-system/src/components/tabs.ts index e1a8017..f8129d2 100644 --- a/examples/design-system/src/components/tabs.ts +++ b/examples/design-system/src/components/tabs.ts @@ -1,4 +1,4 @@ -import { styles } from 'typestyles'; +import { styles } from '../runtime'; import { designTokens as t } from '../tokens'; export const tabs = styles.component('tabs', { diff --git a/examples/design-system/src/components/textAreaField.ts b/examples/design-system/src/components/textAreaField.ts index 52cb914..38ee2d3 100644 --- a/examples/design-system/src/components/textAreaField.ts +++ b/examples/design-system/src/components/textAreaField.ts @@ -1,4 +1,4 @@ -import { styles } from 'typestyles'; +import { styles } from '../runtime'; import { designTokens as t } from '../tokens'; export const textAreaField = styles.component('text-area-field', { diff --git a/examples/design-system/src/components/textField.ts b/examples/design-system/src/components/textField.ts index 33f093b..ab2cd31 100644 --- a/examples/design-system/src/components/textField.ts +++ b/examples/design-system/src/components/textField.ts @@ -1,4 +1,4 @@ -import { styles } from 'typestyles'; +import { styles } from '../runtime'; import { designTokens as t } from '../tokens'; export const textField = styles.component('text-field', { diff --git a/examples/design-system/src/create-theme.ts b/examples/design-system/src/create-theme.ts index 3255bcd..a3f987d 100644 --- a/examples/design-system/src/create-theme.ts +++ b/examples/design-system/src/create-theme.ts @@ -1,4 +1,5 @@ -import { tokens, type ThemeSurface, type TokenValues } from 'typestyles'; +import type { ThemeSurface, TokenValues } from 'typestyles'; +import { tokens } from './runtime'; import type { DeepPartial, DesignPrimitiveOverrides, diff --git a/examples/design-system/src/index.ts b/examples/design-system/src/index.ts index 3ce20f7..ba33858 100644 --- a/examples/design-system/src/index.ts +++ b/examples/design-system/src/index.ts @@ -1,5 +1,6 @@ export * from './components'; export * from './create-theme'; +export * from './runtime'; export * from './types'; export * from './tokens'; export * from './themes'; diff --git a/examples/design-system/src/runtime.ts b/examples/design-system/src/runtime.ts new file mode 100644 index 0000000..66fafae --- /dev/null +++ b/examples/design-system/src/runtime.ts @@ -0,0 +1,9 @@ +import { createStyles, createTokens } from 'typestyles'; + +/** + * Dedicated TypeStyles instances for this package (no global class-naming mutation). + * When this library may share a page with another TypeStyles bundle, pass + * `{ scopeId: 'your-package' }` to both factories so variables and theme classes stay unique. + */ +export const styles = createStyles(); +export const tokens = createTokens(); diff --git a/examples/design-system/src/tokens/index.ts b/examples/design-system/src/tokens/index.ts index 62af573..c32adf8 100644 --- a/examples/design-system/src/tokens/index.ts +++ b/examples/design-system/src/tokens/index.ts @@ -1,4 +1,5 @@ -import { insertRules, tokens } from 'typestyles'; +import { insertRules } from 'typestyles'; +import { tokens } from '../runtime'; import { codeBlockValues } from './component'; import { basePaletteTokenValues } from './palette'; import { diff --git a/packages/typestyles/CHANGELOG.md b/packages/typestyles/CHANGELOG.md index 855b0b1..9494b57 100644 --- a/packages/typestyles/CHANGELOG.md +++ b/packages/typestyles/CHANGELOG.md @@ -1,5 +1,15 @@ # typestyles +## Unreleased + +### Breaking changes + +- **Instance-based APIs replace global class naming.** Removed `configureClassNaming`, `getClassNamingConfig`, and `resetClassNaming`. + - Use **`createStyles({ mode?, prefix?, scopeId? })`** for a dedicated style API (same surface as the default `styles` export). Default `import { styles } from 'typestyles'` is `createStyles()`. + - Use **`createTokens({ scopeId? })`** for a dedicated token + theme API. Default `import { tokens } from 'typestyles'` is `createTokens()`. When `scopeId` is set, `tokens.create` / `createTheme` emit scoped `--{scope}-namespace-*` variables and `.theme-{scope}-{name}` classes (sanitized segments). + - New exports: `mergeClassNaming`, `defaultClassNamingConfig`, `scopedTokenNamespace`, and types `StylesApi`, `TokensApi`, `CreateTokensOptions`. +- Low-level **`createComponent`**, **`createClass`**, and **`createHashClass`** now take **`ClassNamingConfig`** as the first argument when imported from internal modules; app code should use `createStyles()` or the default `styles` object instead. + ## 0.4.0 ### Minor Changes diff --git a/packages/typestyles/src/class-naming.test.ts b/packages/typestyles/src/class-naming.test.ts index 83f7028..dd1cec8 100644 --- a/packages/typestyles/src/class-naming.test.ts +++ b/packages/typestyles/src/class-naming.test.ts @@ -1,7 +1,5 @@ -import { describe, it, expect, beforeEach, afterEach } from 'vitest'; -import { configureClassNaming, resetClassNaming } from './class-naming.js'; -import { createClass, createHashClass } from './styles.js'; -import { createComponent } from './component.js'; +import { describe, it, expect, beforeEach } from 'vitest'; +import { createStyles } from './styles.js'; import { reset, flushSync } from './sheet.js'; import { registeredNamespaces } from './registry.js'; @@ -9,16 +7,11 @@ describe('class naming modes', () => { beforeEach(() => { reset(); registeredNamespaces.clear(); - resetClassNaming(); - }); - - afterEach(() => { - resetClassNaming(); }); it('semantic mode keeps readable component() class strings', () => { - configureClassNaming({ mode: 'semantic' }); - const button = createComponent('btn', { + const styles = createStyles({ mode: 'semantic' }); + const button = styles.component('btn', { base: { color: 'red' }, primary: { backgroundColor: 'blue' }, }); @@ -27,12 +20,12 @@ describe('class naming modes', () => { }); it('hashed mode yields stable prefixed class names for component()', () => { - configureClassNaming({ mode: 'hashed', prefix: 'app' }); - const a = createComponent('card', { + const styles = createStyles({ mode: 'hashed', prefix: 'app' }); + const a = styles.component('card', { root: { padding: '8px' }, }); registeredNamespaces.clear(); - const b = createComponent('card', { + const b = styles.component('card', { root: { padding: '8px' }, }); expect(a.root).toMatch(/^app-card-/); @@ -40,19 +33,18 @@ describe('class naming modes', () => { }); it('scopeId changes hashed output for the same logical component styles', () => { - configureClassNaming({ mode: 'hashed', scopeId: 'pkg-a' }); - const x = createComponent('box', { main: { margin: 0 } }).main; + const sa = createStyles({ mode: 'hashed', scopeId: 'pkg-a' }); + const x = sa.component('box', { main: { margin: 0 } }).main; reset(); registeredNamespaces.clear(); - resetClassNaming(); - configureClassNaming({ mode: 'hashed', scopeId: 'pkg-b' }); - const y = createComponent('box', { main: { margin: 0 } }).main; + const sb = createStyles({ mode: 'hashed', scopeId: 'pkg-b' }); + const y = sb.component('box', { main: { margin: 0 } }).main; expect(x).not.toBe(y); }); it('atomic mode omits the namespace slug in class strings', () => { - configureClassNaming({ mode: 'atomic', prefix: 'x' }); - const button = createComponent('btn', { + const styles = createStyles({ mode: 'atomic', prefix: 'x' }); + const button = styles.component('btn', { base: { color: 'red' }, }); expect(button.base).toMatch(/^x-[a-z0-9]+$/); @@ -60,14 +52,14 @@ describe('class naming modes', () => { }); it('styles.class respects naming mode', () => { - configureClassNaming({ mode: 'hashed', prefix: 't' }); - const cls = createClass('hero', { display: 'flex' }); + const styles = createStyles({ mode: 'hashed', prefix: 't' }); + const cls = styles.class('hero', { display: 'flex' }); expect(cls).toMatch(/^t-hero-/); }); it('createComponent resolves variant keys to hashed classes', () => { - configureClassNaming({ mode: 'hashed', prefix: 'c' }); - const btn = createComponent('cb', { + const styles = createStyles({ mode: 'hashed', prefix: 'c' }); + const btn = styles.component('cb', { base: { padding: '4px' }, variants: { intent: { @@ -89,19 +81,19 @@ describe('class naming modes', () => { } }); - it('createHashClass stays backward compatible when scopeId is empty', () => { - configureClassNaming({ scopeId: '' }); - const cls = createHashClass({ color: 'red' }, 'lbl'); + it('createHashClass uses default prefix when scopeId is empty', () => { + const styles = createStyles({ scopeId: '' }); + const cls = styles.hashClass({ color: 'red' }, 'lbl'); expect(cls.startsWith('ts-lbl-')).toBe(true); }); it('createHashClass includes scopeId in the hash when set', () => { - const a = createHashClass({ width: 10 }, 'x'); + const sa = createStyles(); + const a = sa.hashClass({ width: 10 }, 'x'); reset(); registeredNamespaces.clear(); - resetClassNaming(); - configureClassNaming({ scopeId: 's1' }); - const b = createHashClass({ width: 10 }, 'x'); + const sb = createStyles({ scopeId: 's1' }); + const b = sb.hashClass({ width: 10 }, 'x'); expect(a).not.toBe(b); }); }); diff --git a/packages/typestyles/src/class-naming.ts b/packages/typestyles/src/class-naming.ts index 1c38a4b..9401afa 100644 --- a/packages/typestyles/src/class-naming.ts +++ b/packages/typestyles/src/class-naming.ts @@ -21,29 +21,15 @@ export type ClassNamingConfig = { scopeId: string; }; -const defaultConfig: ClassNamingConfig = { +/** Default naming options used by `createStyles()` when no overrides are passed. */ +export const defaultClassNamingConfig: ClassNamingConfig = { mode: 'semantic', prefix: 'ts', scopeId: '', }; -let current: ClassNamingConfig = { ...defaultConfig }; - -export function getClassNamingConfig(): Readonly { - return current; -} - -/** - * Set global class naming options. Call once at app or package entry - * (e.g. design-system `index.ts`) for per-package adoption in a monorepo. - */ -export function configureClassNaming(partial: Partial): void { - current = { ...current, ...partial }; -} - -/** Restore defaults (primarily for tests). */ -export function resetClassNaming(): void { - current = { ...defaultConfig }; +export function mergeClassNaming(partial?: Partial): ClassNamingConfig { + return { ...defaultClassNamingConfig, ...partial }; } export function stableSerialize(value: unknown): string { @@ -75,9 +61,21 @@ export function sanitizeClassSegment(label: string): string { return normalized.replace(/-+/g, '-').replace(/^-|-$/g, '') || 'style'; } +/** CSS custom property namespace segment for `tokens.create` / `createTheme` when `scopeId` is set. */ +export function scopedTokenNamespace( + scopeId: string | undefined, + logicalNamespace: string, +): string { + if (!scopeId || !scopeId.trim()) return logicalNamespace; + return `${sanitizeClassSegment(scopeId)}-${logicalNamespace}`; +} + /** `styles.class(name, …)` */ -export function buildSingleClassName(name: string, properties: CSSProperties): string { - const cfg = getClassNamingConfig(); +export function buildSingleClassName( + cfg: ClassNamingConfig, + name: string, + properties: CSSProperties, +): string { if (cfg.mode === 'semantic') return name; const payload = stableSerialize({ @@ -96,11 +94,11 @@ export function buildSingleClassName(name: string, properties: CSSProperties): s * a variant segment (`base`, `intent-primary`, `root-trigger-primary`, …). */ export function buildComponentClassName( + cfg: ClassNamingConfig, namespace: string, suffix: string, properties: CSSProperties, ): string { - const cfg = getClassNamingConfig(); if (cfg.mode === 'semantic') return `${namespace}-${suffix}`; const payload = stableSerialize({ diff --git a/packages/typestyles/src/component.test.ts b/packages/typestyles/src/component.test.ts index 3136b47..7a1b0a3 100644 --- a/packages/typestyles/src/component.test.ts +++ b/packages/typestyles/src/component.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect, beforeEach } from 'vitest'; import { createComponent } from './component.js'; -import { resetClassNaming } from './class-naming.js'; +import { defaultClassNamingConfig } from './class-naming.js'; import { reset, flushSync, getRegisteredCss } from './sheet.js'; import { registeredNamespaces } from './registry.js'; @@ -8,11 +8,10 @@ describe('createComponent — dimensioned variants', () => { beforeEach(() => { reset(); registeredNamespaces.clear(); - resetClassNaming(); }); it('returns a callable function', () => { - const btn = createComponent('btn', { + const btn = createComponent(defaultClassNamingConfig, 'btn', { base: { padding: '8px' }, variants: { intent: { primary: { color: 'blue' } } }, }); @@ -20,7 +19,7 @@ describe('createComponent — dimensioned variants', () => { }); it('includes base class when called with no args', () => { - const btn = createComponent('btn', { + const btn = createComponent(defaultClassNamingConfig, 'btn', { base: { padding: '8px' }, }); expect(btn()).toBe('btn-base'); @@ -28,7 +27,7 @@ describe('createComponent — dimensioned variants', () => { }); it('is destructurable — base property returns the base class string', () => { - const btn = createComponent('btn', { + const btn = createComponent(defaultClassNamingConfig, 'btn', { base: { padding: '8px' }, variants: { intent: { primary: { color: 'blue' } }, @@ -38,7 +37,7 @@ describe('createComponent — dimensioned variants', () => { }); it('is destructurable — variant properties return individual class strings', () => { - const btn = createComponent('btn', { + const btn = createComponent(defaultClassNamingConfig, 'btn', { base: { padding: '8px' }, variants: { intent: { @@ -59,7 +58,7 @@ describe('createComponent — dimensioned variants', () => { }); it('supports Object.keys() for enumeration', () => { - const btn = createComponent('btn', { + const btn = createComponent(defaultClassNamingConfig, 'btn', { base: { padding: '8px' }, variants: { intent: { primary: { color: 'blue' } }, @@ -72,7 +71,7 @@ describe('createComponent — dimensioned variants', () => { }); it('does not include base class when base is not defined', () => { - const btn = createComponent('btn-nobase', { + const btn = createComponent(defaultClassNamingConfig, 'btn-nobase', { variants: { intent: { primary: { color: 'blue' } }, }, @@ -81,7 +80,7 @@ describe('createComponent — dimensioned variants', () => { }); it('applies variant classes from selections', () => { - const btn = createComponent('vbtn', { + const btn = createComponent(defaultClassNamingConfig, 'vbtn', { base: { padding: '8px' }, variants: { intent: { @@ -102,7 +101,7 @@ describe('createComponent — dimensioned variants', () => { }); it('applies defaultVariants when selection is omitted', () => { - const btn = createComponent('dbtn', { + const btn = createComponent(defaultClassNamingConfig, 'dbtn', { base: { padding: '8px' }, variants: { intent: { @@ -123,7 +122,7 @@ describe('createComponent — dimensioned variants', () => { }); it('explicit selection overrides defaultVariants', () => { - const btn = createComponent('obtn', { + const btn = createComponent(defaultClassNamingConfig, 'obtn', { variants: { intent: { primary: { color: 'blue' }, @@ -137,7 +136,7 @@ describe('createComponent — dimensioned variants', () => { }); it('applies compound variant class when all keys match', () => { - const btn = createComponent('cbtn', { + const btn = createComponent(defaultClassNamingConfig, 'cbtn', { base: { padding: '8px' }, variants: { intent: { @@ -169,7 +168,7 @@ describe('createComponent — dimensioned variants', () => { }); it('does not apply compound variant when only partial match', () => { - const btn = createComponent('pbtn', { + const btn = createComponent(defaultClassNamingConfig, 'pbtn', { variants: { intent: { primary: { color: 'blue' }, @@ -193,7 +192,7 @@ describe('createComponent — dimensioned variants', () => { }); it('injects CSS for base and variants into the stylesheet', () => { - createComponent('style-test', { + createComponent(defaultClassNamingConfig, 'style-test', { base: { display: 'flex' }, variants: { intent: { primary: { color: 'blue' } }, @@ -207,7 +206,7 @@ describe('createComponent — dimensioned variants', () => { }); it('injects CSS for compound variants', () => { - createComponent('cv-test', { + createComponent(defaultClassNamingConfig, 'cv-test', { variants: { intent: { primary: { color: 'blue' } }, size: { lg: { fontSize: '18px' } }, @@ -223,7 +222,7 @@ describe('createComponent — dimensioned variants', () => { }); it('matches compound variants with array values', () => { - const btn = createComponent('arrbtn', { + const btn = createComponent(defaultClassNamingConfig, 'arrbtn', { variants: { intent: { primary: { color: 'blue' }, @@ -249,7 +248,7 @@ describe('createComponent — dimensioned variants', () => { }); it('supports boolean variant keys in selections and defaults', () => { - const btn = createComponent('boolbtn', { + const btn = createComponent(defaultClassNamingConfig, 'boolbtn', { variants: { outlined: { true: { border: '1px solid currentColor' }, @@ -269,11 +268,10 @@ describe('createComponent — flat variants', () => { beforeEach(() => { reset(); registeredNamespaces.clear(); - resetClassNaming(); }); it('returns a callable function', () => { - const card = createComponent('card', { + const card = createComponent(defaultClassNamingConfig, 'card', { base: { padding: '16px' }, elevated: { boxShadow: '0 4px 12px rgba(0,0,0,0.1)' }, }); @@ -281,7 +279,7 @@ describe('createComponent — flat variants', () => { }); it('base always auto-applied on function call', () => { - const card = createComponent('card', { + const card = createComponent(defaultClassNamingConfig, 'card', { base: { padding: '16px' }, elevated: { boxShadow: '0 4px 12px rgba(0,0,0,0.1)' }, }); @@ -289,7 +287,7 @@ describe('createComponent — flat variants', () => { }); it('applies flat variants via boolean selections', () => { - const card = createComponent('card', { + const card = createComponent(defaultClassNamingConfig, 'card', { base: { padding: '16px' }, elevated: { boxShadow: '0 4px 12px rgba(0,0,0,0.1)' }, compact: { padding: '8px' }, @@ -301,7 +299,7 @@ describe('createComponent — flat variants', () => { }); it('is destructurable', () => { - const card = createComponent('card', { + const card = createComponent(defaultClassNamingConfig, 'card', { base: { padding: '16px' }, elevated: { boxShadow: '0 4px 12px rgba(0,0,0,0.1)' }, }); @@ -311,7 +309,7 @@ describe('createComponent — flat variants', () => { }); it('supports Object.keys() for enumeration', () => { - const card = createComponent('card', { + const card = createComponent(defaultClassNamingConfig, 'card', { base: { padding: '16px' }, elevated: { boxShadow: '0 4px 12px rgba(0,0,0,0.1)' }, }); @@ -322,7 +320,7 @@ describe('createComponent — flat variants', () => { }); it('injects CSS for all flat variants', () => { - createComponent('flatcss', { + createComponent(defaultClassNamingConfig, 'flatcss', { base: { display: 'block' }, active: { borderColor: 'blue' }, }); @@ -334,7 +332,7 @@ describe('createComponent — flat variants', () => { }); it('works without base', () => { - const card = createComponent('nobase', { + const card = createComponent(defaultClassNamingConfig, 'nobase', { elevated: { boxShadow: '0 4px 12px rgba(0,0,0,0.1)' }, }); @@ -347,11 +345,10 @@ describe('createComponent with slots', () => { beforeEach(() => { reset(); registeredNamespaces.clear(); - resetClassNaming(); }); it('returns per-slot class maps with defaults', () => { - const tabs = createComponent('tabs', { + const tabs = createComponent(defaultClassNamingConfig, 'tabs', { slots: ['root', 'trigger', 'content'] as const, base: { root: { display: 'grid' }, @@ -383,7 +380,7 @@ describe('createComponent with slots', () => { }); it('applies slot compound variants to targeted slots', () => { - const tabs = createComponent('tabs-cv', { + const tabs = createComponent(defaultClassNamingConfig, 'tabs-cv', { slots: ['root', 'trigger', 'content'] as const, variants: { intent: { @@ -413,7 +410,7 @@ describe('createComponent with slots', () => { }); it('injects per-slot CSS class rules', () => { - createComponent('tabs-css', { + createComponent(defaultClassNamingConfig, 'tabs-css', { slots: ['root', 'trigger'] as const, base: { root: { display: 'grid' }, @@ -444,11 +441,10 @@ describe('createComponent — multi-slot (no variants)', () => { beforeEach(() => { reset(); registeredNamespaces.clear(); - resetClassNaming(); }); it('returns a callable function that returns slot classes', () => { - const checkbox = createComponent('checkbox', { + const checkbox = createComponent(defaultClassNamingConfig, 'checkbox', { slots: ['root', 'box', 'label'] as const, root: { display: 'flex', gap: '8px' }, box: { width: '18px', height: '18px' }, @@ -462,7 +458,7 @@ describe('createComponent — multi-slot (no variants)', () => { }); it('is destructurable — each slot returns its class string', () => { - const checkbox = createComponent('chk', { + const checkbox = createComponent(defaultClassNamingConfig, 'chk', { slots: ['root', 'box'] as const, root: { display: 'flex' }, box: { width: '20px' }, @@ -473,7 +469,7 @@ describe('createComponent — multi-slot (no variants)', () => { }); it('supports optional slots with no styles', () => { - const dialog = createComponent('dialog', { + const dialog = createComponent(defaultClassNamingConfig, 'dialog', { slots: ['overlay', 'modal', 'content'] as const, overlay: { position: 'fixed' }, modal: { padding: '16px' }, @@ -486,7 +482,7 @@ describe('createComponent — multi-slot (no variants)', () => { }); it('supports Object.keys() for enumeration', () => { - const card = createComponent('mscard', { + const card = createComponent(defaultClassNamingConfig, 'mscard', { slots: ['root', 'title', 'body'] as const, root: { padding: '16px' }, title: { fontWeight: 'bold' }, @@ -499,7 +495,7 @@ describe('createComponent — multi-slot (no variants)', () => { }); it('injects CSS for all slots', () => { - createComponent('mscss', { + createComponent(defaultClassNamingConfig, 'mscss', { slots: ['root', 'box'] as const, root: { display: 'flex' }, box: { width: '24px' }, diff --git a/packages/typestyles/src/component.ts b/packages/typestyles/src/component.ts index e8cf862..5e5d075 100644 --- a/packages/typestyles/src/component.ts +++ b/packages/typestyles/src/component.ts @@ -14,7 +14,7 @@ import type { import { serializeStyle } from './css.js'; import { insertRules } from './sheet.js'; import { registeredNamespaces } from './registry.js'; -import { buildComponentClassName } from './class-naming.js'; +import { buildComponentClassName, type ClassNamingConfig } from './class-naming.js'; // --------------------------------------------------------------------------- // Reserved keys that signal a dimensioned config (not flat variant keys) @@ -106,53 +106,72 @@ function isMultiSlotConfig(config: Record): config is MultiSlot * ``` */ export function createComponent( + classNaming: ClassNamingConfig, namespace: string, config: ComponentConfig, ): ComponentReturn; export function createComponent( + classNaming: ClassNamingConfig, namespace: string, config: FlatComponentConfig, ): FlatComponentReturn; export function createComponent>( + classNaming: ClassNamingConfig, namespace: string, config: SlotComponentConfig, ): SlotComponentFunction; export function createComponent( + classNaming: ClassNamingConfig, namespace: string, config: MultiSlotConfig, ): MultiSlotReturn; -export function createComponent(namespace: string, config: Record): unknown { +export function createComponent( + classNaming: ClassNamingConfig, + namespace: string, + config: Record, +): unknown { if (isMultiSlotConfig(config)) { - return createMultiSlotComponent(namespace, config as MultiSlotConfig); + return createMultiSlotComponent(classNaming, namespace, config as MultiSlotConfig); } if (isSlotWithVariantsConfig(config)) { return createSlotComponent( + classNaming, namespace, config as SlotComponentConfig>, ); } if (isDimensionedConfig(config)) { - return createDimensionedComponent(namespace, config as ComponentConfig); + return createDimensionedComponent( + classNaming, + namespace, + config as ComponentConfig, + ); } - return createFlatComponent(namespace, config as FlatComponentConfig); + return createFlatComponent(classNaming, namespace, config as FlatComponentConfig); } // --------------------------------------------------------------------------- // Dimensioned variant component (the primary path) // --------------------------------------------------------------------------- +function registryKeyForComponent(classNaming: ClassNamingConfig, namespace: string): string { + const scope = classNaming.scopeId || 'default'; + return `${scope}:${namespace}`; +} + function createDimensionedComponent( + classNaming: ClassNamingConfig, namespace: string, config: ComponentConfig, ): ComponentReturn { const { base, variants = {} as V, compoundVariants = [], defaultVariants = {} } = config; - warnDuplicate(namespace); - registeredNamespaces.add(namespace); + warnDuplicate(classNaming, namespace); + registeredNamespaces.add(registryKeyForComponent(classNaming, namespace)); const rules: Array<{ key: string; css: string }> = []; @@ -162,7 +181,7 @@ function createDimensionedComponent( // 1. Base let baseClassName: string | undefined; if (base) { - baseClassName = buildComponentClassName(namespace, 'base', base); + baseClassName = buildComponentClassName(classNaming, namespace, 'base', base); classMap['base'] = baseClassName; rules.push(...serializeStyle(`.${baseClassName}`, base)); } @@ -172,7 +191,7 @@ function createDimensionedComponent( for (const [dimension, options] of Object.entries(variants)) { for (const [option, properties] of Object.entries(options as Record)) { const segment = `${dimension}-${option}`; - const className = buildComponentClassName(namespace, segment, properties); + const className = buildComponentClassName(classNaming, namespace, segment, properties); variantClassByKey[segment] = className; classMap[segment] = className; rules.push(...serializeStyle(`.${className}`, properties)); @@ -183,7 +202,12 @@ function createDimensionedComponent( const compoundClassByIndex: string[] = []; (compoundVariants as Array<{ variants: Record; style: CSSProperties }>).forEach( (cv, index) => { - const className = buildComponentClassName(namespace, `compound-${index}`, cv.style); + const className = buildComponentClassName( + classNaming, + namespace, + `compound-${index}`, + cv.style, + ); compoundClassByIndex[index] = className; rules.push(...serializeStyle(`.${className}`, cv.style)); }, @@ -254,11 +278,12 @@ function createDimensionedComponent( // --------------------------------------------------------------------------- function createFlatComponent( + classNaming: ClassNamingConfig, namespace: string, config: FlatComponentConfig, ): FlatComponentReturn { - warnDuplicate(namespace); - registeredNamespaces.add(namespace); + warnDuplicate(classNaming, namespace); + registeredNamespaces.add(registryKeyForComponent(classNaming, namespace)); const rules: Array<{ key: string; css: string }> = []; const classMap: Record = {}; @@ -268,7 +293,7 @@ function createFlatComponent( if (RESERVED_KEYS.has(key) && key !== 'base') continue; const props = properties as CSSProperties; const segment = key; - const className = buildComponentClassName(namespace, segment, props); + const className = buildComponentClassName(classNaming, namespace, segment, props); classMap[segment] = className; rules.push(...serializeStyle(`.${className}`, props)); if (key !== 'base') { @@ -306,13 +331,14 @@ function createFlatComponent( // --------------------------------------------------------------------------- function createMultiSlotComponent( + classNaming: ClassNamingConfig, namespace: string, config: MultiSlotConfig, ): MultiSlotReturn { const { slots } = config; - warnDuplicate(namespace); - registeredNamespaces.add(namespace); + warnDuplicate(classNaming, namespace); + registeredNamespaces.add(registryKeyForComponent(classNaming, namespace)); const rules: Array<{ key: string; css: string }> = []; const slotClassMap: Record = {}; @@ -320,7 +346,7 @@ function createMultiSlotComponent( for (const slot of slots) { const properties = (config as Record)[slot]; if (properties) { - const className = buildComponentClassName(namespace, slot, properties); + const className = buildComponentClassName(classNaming, namespace, slot, properties); slotClassMap[slot] = className; rules.push(...serializeStyle(`.${className}`, properties)); } else { @@ -367,6 +393,7 @@ function makeMultiSlotObject( // --------------------------------------------------------------------------- function createSlotComponent>( + classNaming: ClassNamingConfig, namespace: string, config: SlotComponentConfig, ): SlotComponentFunction { @@ -378,14 +405,14 @@ function createSlotComponent = []; const baseClassBySlot: Record = {}; for (const [slot, properties] of Object.entries(base as Record)) { - const className = buildComponentClassName(namespace, slot, properties); + const className = buildComponentClassName(classNaming, namespace, slot, properties); baseClassBySlot[slot] = className; rules.push(...serializeStyle(`.${className}`, properties)); } @@ -397,7 +424,7 @@ function createSlotComponent { for (const [slot, properties] of Object.entries(cv.style)) { const segment = `${slot}-compound-${index}`; - const className = buildComponentClassName(namespace, segment, properties); + const className = buildComponentClassName(classNaming, namespace, segment, properties); slotCompoundClassByKey[`${slot}::${index}`] = className; rules.push(...serializeStyle(`.${className}`, properties)); } @@ -505,9 +532,10 @@ function normalizeSelection(value: unknown, options: Record): s return String(value); } -function warnDuplicate(namespace: string): void { +function warnDuplicate(classNaming: ClassNamingConfig, namespace: string): void { if (process.env.NODE_ENV !== 'production') { - if (registeredNamespaces.has(namespace)) { + const key = registryKeyForComponent(classNaming, namespace); + if (registeredNamespaces.has(key)) { console.warn( `[typestyles] styles.component('${namespace}', ...) called more than once. ` + `This will cause class name collisions. Each namespace should be unique.`, diff --git a/packages/typestyles/src/index.ts b/packages/typestyles/src/index.ts index 1cc5c69..dcbac89 100644 --- a/packages/typestyles/src/index.ts +++ b/packages/typestyles/src/index.ts @@ -1,5 +1,5 @@ -import { createClass, createHashClass, compose, createStylesWithUtils } from './styles.js'; -import { createTokens, useTokens } from './tokens.js'; +import { createStyles } from './styles.js'; +import { createTokens } from './tokens.js'; import { createTheme, createDarkMode, when, colorMode } from './theme.js'; import { createKeyframes } from './keyframes.js'; import * as colorFns from './color.js'; @@ -10,13 +10,21 @@ import { flushSync, ensureDocumentStylesAttached, } from './sheet.js'; -import { createComponent } from './component.js'; import { globalStyle, globalFontFace } from './global.js'; import { createVar, assignVars } from './vars.js'; import { cx } from './cx.js'; +export type { StylesApi } from './styles.js'; +export type { CreateTokensOptions, TokensApi } from './tokens.js'; + export type { ClassNamingConfig, ClassNamingMode } from './class-naming.js'; -export { configureClassNaming, getClassNamingConfig, resetClassNaming } from './class-naming.js'; +export { + mergeClassNaming, + defaultClassNamingConfig, + scopedTokenNamespace, +} from './class-naming.js'; + +export { createStyles, createTokens }; export type { CSSProperties, @@ -64,8 +72,11 @@ export { createVar, assignVars }; export type { ColorMixSpace } from './color.js'; +export { createTheme, createDarkMode, when, colorMode }; + /** - * Style creation API. + * Default style API (semantic class names, empty `scopeId`). Prefer `createStyles({ scopeId, mode, prefix })` + * per package or micro-frontend for isolation. * * @example * ```ts @@ -79,23 +90,12 @@ export type { ColorMixSpace } from './color.js'; * defaultVariants: { intent: 'primary', size: 'sm' }, * }); * - * // Call as function — base always included * button({ intent: 'primary', size: 'lg' }) * - * // Destructure individual class strings - * const { base, 'intent-primary': primary } = button; - * - * // Single class (no variants) * const card = styles.class('card', { padding: '1rem' }); * ``` */ -export const styles = { - component: createComponent, - class: createClass, - hashClass: createHashClass, - withUtils: createStylesWithUtils, - compose, -} as const; +export const styles = createStyles(); /** * Global CSS API for arbitrary selectors and font-face declarations. @@ -112,7 +112,8 @@ export const global = { } as const; /** - * Design token API using CSS custom properties. + * Default token API (unscoped custom properties). Prefer `createTokens({ scopeId })` when multiple + * bundles share a page. * * @example * ```ts @@ -123,17 +124,9 @@ export const global = { * base: { color: { primary: '#ff6600' } }, * colorMode: tokens.colorMode.mediaOnly({ dark: darkOverrides }), * }); - * // acme.className === 'theme-acme' * ``` */ -export const tokens = { - create: createTokens, - use: useTokens, - createTheme, - createDarkMode, - when, - colorMode, -} as const; +export const tokens = createTokens(); /** * Keyframe animation API. diff --git a/packages/typestyles/src/server.test.ts b/packages/typestyles/src/server.test.ts index b0842e9..2a37326 100644 --- a/packages/typestyles/src/server.test.ts +++ b/packages/typestyles/src/server.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect, beforeEach } from 'vitest'; import { collectStyles } from './server.js'; -import { resetClassNaming } from './class-naming.js'; +import { defaultClassNamingConfig } from './class-naming.js'; import { createComponent } from './component.js'; import { createTokens } from './tokens.js'; import { reset } from './sheet.js'; @@ -8,12 +8,11 @@ import { reset } from './sheet.js'; describe('collectStyles', () => { beforeEach(() => { reset(); - resetClassNaming(); }); it('collects CSS from styles created during render', () => { const { html, css } = collectStyles(() => { - createComponent('ssr-btn', { + createComponent(defaultClassNamingConfig, 'ssr-btn', { base: { color: 'red' }, }); return ''; @@ -26,7 +25,7 @@ describe('collectStyles', () => { it('collects CSS from tokens created during render', () => { const { css } = collectStyles(() => { - createTokens('ssr-color', { primary: '#0066ff' }); + createTokens().create('ssr-color', { primary: '#0066ff' }); return ''; }); @@ -36,8 +35,8 @@ describe('collectStyles', () => { it('collects both tokens and styles', () => { const { css } = collectStyles(() => { - const color = createTokens('ssr-theme', { bg: '#fff' }); - createComponent('ssr-card', { + const color = createTokens().create('ssr-theme', { bg: '#fff' }); + createComponent(defaultClassNamingConfig, 'ssr-card', { root: { backgroundColor: color.bg }, }); return ''; diff --git a/packages/typestyles/src/styles.test.ts b/packages/typestyles/src/styles.test.ts index 29fd9f6..020ea4e 100644 --- a/packages/typestyles/src/styles.test.ts +++ b/packages/typestyles/src/styles.test.ts @@ -2,7 +2,7 @@ import { describe, it, expect, beforeEach } from 'vitest'; import { createClass, createHashClass, compose, createStylesWithUtils } from './styles.js'; import { cx } from './index.js'; import { createComponent } from './component.js'; -import { resetClassNaming } from './class-naming.js'; +import { defaultClassNamingConfig } from './class-naming.js'; import { reset, flushSync } from './sheet.js'; import { registeredNamespaces } from './registry.js'; @@ -10,16 +10,18 @@ describe('createClass', () => { beforeEach(() => { reset(); registeredNamespaces.clear(); - resetClassNaming(); }); it('returns the class name string', () => { - const card = createClass('card', { padding: '1rem', borderRadius: '8px' }); + const card = createClass(defaultClassNamingConfig, 'card', { + padding: '1rem', + borderRadius: '8px', + }); expect(card).toBe('card'); }); it('injects CSS into the style sheet', () => { - createClass('class-test', { color: 'red', fontSize: '14px' }); + createClass(defaultClassNamingConfig, 'class-test', { color: 'red', fontSize: '14px' }); flushSync(); const style = document.getElementById('typestyles') as HTMLStyleElement; @@ -32,7 +34,7 @@ describe('createClass', () => { }); it('supports nested selectors', () => { - createClass('hover-card', { + createClass(defaultClassNamingConfig, 'hover-card', { padding: '1rem', '&:hover': { color: 'blue' }, }); @@ -50,28 +52,31 @@ describe('createHashClass', () => { beforeEach(() => { reset(); registeredNamespaces.clear(); - resetClassNaming(); }); it('returns deterministic class names for identical styles', () => { - const a = createHashClass({ color: 'red', padding: '8px' }); - const b = createHashClass({ color: 'red', padding: '8px' }); + const a = createHashClass(defaultClassNamingConfig, { color: 'red', padding: '8px' }); + const b = createHashClass(defaultClassNamingConfig, { color: 'red', padding: '8px' }); expect(a).toBe(b); }); it('returns different class names for different styles', () => { - const a = createHashClass({ color: 'red' }); - const b = createHashClass({ color: 'blue' }); + const a = createHashClass(defaultClassNamingConfig, { color: 'red' }); + const b = createHashClass(defaultClassNamingConfig, { color: 'blue' }); expect(a).not.toBe(b); }); it('supports labels for readability', () => { - const cls = createHashClass({ color: 'red' }, 'button-primary'); + const cls = createHashClass(defaultClassNamingConfig, { color: 'red' }, 'button-primary'); expect(cls.startsWith('ts-button-primary-')).toBe(true); }); it('injects CSS for the hashed selector', () => { - const cls = createHashClass({ color: 'red', fontSize: '14px' }, 'hash-test'); + const cls = createHashClass( + defaultClassNamingConfig, + { color: 'red', fontSize: '14px' }, + 'hash-test', + ); flushSync(); const style = document.getElementById('typestyles') as HTMLStyleElement; @@ -108,26 +113,27 @@ describe('compose', () => { beforeEach(() => { reset(); registeredNamespaces.clear(); - resetClassNaming(); }); it('composes multiple component functions', () => { - const base = createComponent('base', { base: { padding: '8px' } }); - const primary = createComponent('primary', { base: { color: 'blue' } }); + const base = createComponent(defaultClassNamingConfig, 'base', { base: { padding: '8px' } }); + const primary = createComponent(defaultClassNamingConfig, 'primary', { + base: { color: 'blue' }, + }); const button = compose(base, primary); expect(button()).toBe('base-base primary-base'); }); it('composes functions with strings', () => { - const base = createComponent('base2', { base: { padding: '8px' } }); + const base = createComponent(defaultClassNamingConfig, 'base2', { base: { padding: '8px' } }); const composed = compose(base, 'custom-class'); expect(composed()).toBe('base2-base custom-class'); }); it('filters falsy values', () => { - const base = createComponent('base3', { base: { padding: '8px' } }); + const base = createComponent(defaultClassNamingConfig, 'base3', { base: { padding: '8px' } }); const composed = compose(base, false, null, undefined, 'valid'); expect(composed()).toBe('base3-base valid'); @@ -143,7 +149,6 @@ describe('createStylesWithUtils', () => { beforeEach(() => { reset(); registeredNamespaces.clear(); - resetClassNaming(); }); it('expands utility keys in class()', () => { diff --git a/packages/typestyles/src/styles.ts b/packages/typestyles/src/styles.ts index 5b8f768..957dad5 100644 --- a/packages/typestyles/src/styles.ts +++ b/packages/typestyles/src/styles.ts @@ -18,10 +18,12 @@ import { insertRules } from './sheet.js'; import { registeredNamespaces } from './registry.js'; import { buildSingleClassName, - getClassNamingConfig, + defaultClassNamingConfig, hashString, + mergeClassNaming, sanitizeClassSegment, stableSerialize, + type ClassNamingConfig, } from './class-naming.js'; import { createComponent } from './component.js'; @@ -41,18 +43,28 @@ import { createComponent } from './component.js'; *
// class="card" * ``` */ -export function createClass(name: string, properties: CSSProperties): string { +function registryKeyForClass(classNaming: ClassNamingConfig, name: string): string { + const scope = classNaming.scopeId || 'default'; + return `${scope}:${name}`; +} + +export function createClass( + classNaming: ClassNamingConfig, + name: string, + properties: CSSProperties, +): string { + const regKey = registryKeyForClass(classNaming, name); if (process.env.NODE_ENV !== 'production') { - if (registeredNamespaces.has(name)) { + if (registeredNamespaces.has(regKey)) { console.warn( `[typestyles] styles.class('${name}', ...) called more than once. ` + `This will cause class name collisions. Each class name should be unique.`, ); } } - registeredNamespaces.add(name); + registeredNamespaces.add(regKey); - const className = buildSingleClassName(name, properties); + const className = buildSingleClassName(classNaming, name, properties); const selector = `.${className}`; const rules = serializeStyle(selector, properties); insertRules(rules); @@ -79,8 +91,12 @@ export function createClass(name: string, properties: CSSProperties): string { * ); * ``` */ -export function createHashClass(properties: CSSProperties, label?: string): string { - const cfg = getClassNamingConfig(); +export function createHashClass( + classNaming: ClassNamingConfig, + properties: CSSProperties, + label?: string, +): string { + const cfg = classNaming; const serialized = cfg.scopeId !== '' ? stableSerialize({ scope: cfg.scopeId, properties }) @@ -137,6 +153,46 @@ export function compose( // withUtils // --------------------------------------------------------------------------- +export type StylesApi = { + /** Resolved naming config for this instance (useful for debugging). */ + readonly classNaming: Readonly; + class: (name: string, properties: CSSProperties) => string; + hashClass: (properties: CSSProperties, label?: string) => string; + component: { + ( + namespace: string, + config: ComponentConfig, + ): ComponentReturn; + (namespace: string, config: FlatComponentConfig): FlatComponentReturn; + >( + namespace: string, + config: SlotComponentConfig, + ): SlotComponentFunction; + (namespace: string, config: MultiSlotConfig): MultiSlotReturn; + }; + withUtils: (utils: U) => StylesWithUtilsApi; + compose: typeof compose; +}; + +/** + * Create a styles API with its own class naming config (scope, mode, prefix). + * Use one instance per package or micro-frontend so hashed names and dev warnings + * stay isolated without global mutation. + */ +export function createStyles(partial?: Partial): StylesApi { + const classNaming = mergeClassNaming(partial); + + return { + classNaming, + class: (name, properties) => createClass(classNaming, name, properties), + hashClass: (properties, label) => createHashClass(classNaming, properties, label), + component: ((namespace: string, config: Record) => + createComponent(classNaming, namespace, config)) as StylesApi['component'], + withUtils: (utils) => createStylesWithUtils(utils, classNaming), + compose, + }; +} + export type StylesWithUtilsApi = { class: (name: string, properties: CSSPropertiesWithUtils) => string; hashClass: (properties: CSSPropertiesWithUtils, label?: string) => string; @@ -170,7 +226,10 @@ export type StylesWithUtilsApi = { * }); * ``` */ -export function createStylesWithUtils(utils: U): StylesWithUtilsApi { +export function createStylesWithUtils( + utils: U, + classNaming: ClassNamingConfig = defaultClassNamingConfig, +): StylesWithUtilsApi { const apply = (properties: CSSPropertiesWithUtils): CSSProperties => expandStyleWithUtils(properties, utils); @@ -210,12 +269,16 @@ export function createStylesWithUtils(utils: U): StylesWit } } - return createComponent(namespace, transformed as ComponentConfig); + return createComponent( + classNaming, + namespace, + transformed as ComponentConfig, + ); } return { - class: (name, properties) => createClass(name, apply(properties)), - hashClass: (properties, label) => createHashClass(apply(properties), label), + class: (name, properties) => createClass(classNaming, name, apply(properties)), + hashClass: (properties, label) => createHashClass(classNaming, apply(properties), label), component: component as StylesWithUtilsApi['component'], compose, }; diff --git a/packages/typestyles/src/theme.ts b/packages/typestyles/src/theme.ts index af8eb85..e110504 100644 --- a/packages/typestyles/src/theme.ts +++ b/packages/typestyles/src/theme.ts @@ -14,6 +14,7 @@ import type { TokenValues, } from './types.js'; import { flattenTokenEntries } from './types.js'; +import { sanitizeClassSegment, scopedTokenNamespace } from './class-naming.js'; import { insertRule, insertRules } from './sheet.js'; // --------------------------------------------------------------------------- @@ -288,17 +289,24 @@ function mergeCompiled(a: CompiledCondition, b: CompiledCondition): CompiledCond // CSS declaration building // --------------------------------------------------------------------------- -function buildDeclarations(overrides: ThemeOverrides): string { +function buildDeclarations(scopeId: string | undefined, overrides: ThemeOverrides): string { const parts: string[] = []; for (const [namespace, values] of Object.entries(overrides)) { if (values === null || values === undefined) continue; + const cssNs = scopedTokenNamespace(scopeId, namespace); for (const [key, value] of flattenTokenEntries(values as TokenValues)) { - parts.push(`--${namespace}-${key}: ${value}`); + parts.push(`--${cssNs}-${key}: ${value}`); } } return parts.join('; '); } +function themeSegment(scopeId: string | undefined, name: string): string { + const n = sanitizeClassSegment(name); + if (!scopeId) return n; + return `${sanitizeClassSegment(scopeId)}-${n}`; +} + function buildSelector(themeClass: string, compiled: CompiledCondition): string { const prefix = compiled.selectorPrefix ? `${compiled.selectorPrefix} ` : ''; const suffix = compiled.selectorSuffix ?? ''; @@ -486,7 +494,7 @@ function createThemeSurface(name: string, className: string): ThemeSurface { * // `${acme}` === 'theme-acme' * ``` */ -export function createTheme(name: string, config: ThemeConfig): ThemeSurface { +export function createTheme(name: string, config: ThemeConfig, scopeId?: string): ThemeSurface { if (process.env.NODE_ENV !== 'production') { if (config.modes && config.colorMode) { throw new Error( @@ -496,15 +504,16 @@ export function createTheme(name: string, config: ThemeConfig): ThemeSurface { } } - const className = `theme-${name}`; + const segment = themeSegment(scopeId, name); + const className = `theme-${segment}`; // 1. Base rule - const baseDecls = config.base ? buildDeclarations(config.base) : ''; + const baseDecls = config.base ? buildDeclarations(scopeId, config.base) : ''; if (baseDecls) { - insertRule(`theme:${name}:base`, `.${className} { ${baseDecls}; }`); + insertRule(`theme:${segment}:base`, `.${className} { ${baseDecls}; }`); } else { // Emit an empty base rule so the class always exists in the sheet - insertRule(`theme:${name}:base`, `.${className} { }`); + insertRule(`theme:${segment}:base`, `.${className} { }`); } // 2. Mode layers @@ -512,7 +521,7 @@ export function createTheme(name: string, config: ThemeConfig): ThemeSurface { const rules: Array<{ key: string; css: string }> = []; for (const mode of modes) { - const decls = buildDeclarations(mode.overrides); + const decls = buildDeclarations(scopeId, mode.overrides); if (!decls) { if (process.env.NODE_ENV !== 'production') { console.warn( @@ -527,7 +536,7 @@ export function createTheme(name: string, config: ThemeConfig): ThemeSurface { for (let i = 0; i < compiledBranches.length; i++) { const branch = compiledBranches[i]; const selector = buildSelector(className, branch); - const key = `theme:${name}:mode:${mode.id}:branch:${i}`; + const key = `theme:${segment}:mode:${mode.id}:branch:${i}`; rules.push({ key, css: buildRule(selector, decls, branch.media) }); } } @@ -554,8 +563,16 @@ export function createTheme(name: string, config: ThemeConfig): ThemeSurface { * }); * ``` */ -export function createDarkMode(name: string, darkOverrides: ThemeOverrides): ThemeSurface { - return createTheme(name, { - modes: [{ id: 'dark', overrides: darkOverrides, when: when.prefersDark }], - }); +export function createDarkMode( + name: string, + darkOverrides: ThemeOverrides, + scopeId?: string, +): ThemeSurface { + return createTheme( + name, + { + modes: [{ id: 'dark', overrides: darkOverrides, when: when.prefersDark }], + }, + scopeId, + ); } diff --git a/packages/typestyles/src/tokens.test.ts b/packages/typestyles/src/tokens.test.ts index 2e4b09e..688507f 100644 --- a/packages/typestyles/src/tokens.test.ts +++ b/packages/typestyles/src/tokens.test.ts @@ -1,15 +1,29 @@ import { describe, it, expect, beforeEach } from 'vitest'; -import { createTokens, useTokens } from './tokens.js'; +import { createTokens } from './tokens.js'; import { createTheme } from './theme.js'; import { reset, flushSync } from './sheet.js'; -describe('createTokens', () => { +describe('createTokens factory', () => { + it('exposes scopeId on the instance', () => { + expect(createTokens().scopeId).toBeUndefined(); + expect(createTokens({ scopeId: 'ds' }).scopeId).toBe('ds'); + }); + + it('prefixes CSS variable namespaces when scopeId is set', () => { + const t = createTokens({ scopeId: 'my-pkg' }); + const color = t.create('color', { primary: '#0066ff' }); + expect(color.primary).toBe('var(--my-pkg-color-primary)'); + }); +}); + +describe('tokens.create', () => { beforeEach(() => { reset(); }); it('returns an object with var() references', () => { - const color = createTokens('color', { + const api = createTokens(); + const color = api.create('color', { primary: '#0066ff', secondary: '#6b7280', }); @@ -19,7 +33,8 @@ describe('createTokens', () => { }); it('injects :root CSS with custom properties', () => { - createTokens('spacing', { + const api = createTokens(); + api.create('spacing', { sm: '8px', md: '16px', lg: '24px', @@ -37,13 +52,15 @@ describe('createTokens', () => { }); it('token values are usable as CSS strings', () => { - const size = createTokens('size', { base: '16px' }); + const api = createTokens(); + const size = api.create('size', { base: '16px' }); expect(typeof size.base).toBe('string'); expect(size.base).toContain('var('); }); it('supports nested token objects', () => { - const color = createTokens('color', { + const api = createTokens(); + const color = api.create('color', { text: { primary: '#111827', secondary: '#6b7280' }, background: { surface: '#ffffff', subtle: '#f9fafb' }, border: { default: '#e5e7eb' }, @@ -65,7 +82,8 @@ describe('createTokens', () => { }); it('supports deeply nested token objects', () => { - const color = createTokens('color', { + const api = createTokens(); + const color = api.create('color', { brand: { primary: { DEFAULT: '#0066ff', hover: '#0052cc' }, }, @@ -76,7 +94,8 @@ describe('createTokens', () => { }); it('supports flat values alongside nested objects', () => { - const tokens = createTokens('test', { + const api = createTokens(); + const tokens = api.create('test', { simple: 'value', nested: { key: 'nested-value' }, }); @@ -86,19 +105,20 @@ describe('createTokens', () => { }); }); -describe('useTokens', () => { +describe('tokens.use', () => { beforeEach(() => { reset(); }); it('returns var() references without injecting CSS', () => { - createTokens('theme-color', { primary: '#000' }); + const api = createTokens(); + api.create('theme-color', { primary: '#000' }); flushSync(); const stylesBefore = document.getElementById('typestyles') as HTMLStyleElement; const ruleCountBefore = stylesBefore?.sheet?.cssRules.length ?? 0; - const color = useTokens('theme-color'); + const color = api.use('theme-color'); flushSync(); const rulesAfter = stylesBefore?.sheet?.cssRules.length ?? 0; @@ -108,15 +128,17 @@ describe('useTokens', () => { }); it('creates var() references for any property name', () => { - const tokens = useTokens('anything'); + const api = createTokens(); + const tokens = api.use('anything'); expect(tokens.foo).toBe('var(--anything-foo)'); expect(tokens.bar).toBe('var(--anything-bar)'); }); it('supports nested access for useTokens when tokens are created first', () => { - createTokens('color', { text: { primary: '#000' } }); + const api = createTokens(); + api.create('color', { text: { primary: '#000' } }); flushSync(); - const tokens = useTokens('color'); + const tokens = api.use('color'); expect(tokens.text.primary).toBe('var(--color-text-primary)'); }); }); diff --git a/packages/typestyles/src/tokens.ts b/packages/typestyles/src/tokens.ts index 8181ff8..71c5b03 100644 --- a/packages/typestyles/src/tokens.ts +++ b/packages/typestyles/src/tokens.ts @@ -1,14 +1,35 @@ -import type { TokenValues, TokenRef } from './types.js'; +import type { TokenValues, TokenRef, ThemeConfig, ThemeSurface, ThemeOverrides } from './types.js'; import { flattenTokenEntries } from './types.js'; +import { scopedTokenNamespace } from './class-naming.js'; import { insertRule } from './sheet.js'; +import { createTheme, createDarkMode, when, colorMode } from './theme.js'; + +export type CreateTokensOptions = { + /** + * Prefix for CSS custom property namespaces and theme class segments so multiple + * packages on one page do not share `--color-*` or `.theme-*` collisions. + */ + scopeId?: string; +}; /** - * Registry tracking which token namespaces have been created, - * so tokens.use() can provide warnings in development. + * Design token and theme API bound to an optional `scopeId`. */ -const registeredNamespaces = new Set(); -const createdTokenKeys = new Map>(); +export type TokensApi = { + /** Same `scopeId` passed to `createTokens`, if any. */ + readonly scopeId: string | undefined; + create: (namespace: string, values: T) => TokenRef; + use: (namespace: string) => TokenRef; + createTheme: (name: string, config: ThemeConfig) => ThemeSurface; + createDarkMode: (name: string, darkOverrides: ThemeOverrides) => ThemeSurface; + when: typeof when; + colorMode: typeof colorMode; +}; +/** + * Registry tracking which token namespaces have been created per instance, + * so `use()` can provide warnings in development. + */ function getAllKeys(obj: TokenValues, prefix = ''): Set { const keys = new Set(); @@ -88,66 +109,65 @@ function createTokenProxy(namespace: string, prefix: string, allKeys: Set(namespace: string, values: T): TokenRef { - registeredNamespaces.add(namespace); +export function createTokens(options: CreateTokensOptions = {}): TokensApi { + const scopeId = options.scopeId?.trim() || undefined; - const flatEntries = flattenTokenEntries(values); - const declarations = flatEntries - .map(([key, value]) => `--${namespace}-${key}: ${value}`) - .join('; '); + const registeredNamespaces = new Set(); + const createdTokenKeys = new Map>(); - const css = `:root { ${declarations}; }`; - insertRule(`tokens:${namespace}`, css); + function create(namespace: string, values: T): TokenRef { + registeredNamespaces.add(namespace); - const allKeys = getAllKeys(values); - createdTokenKeys.set(namespace, allKeys); + const cssNs = scopedTokenNamespace(scopeId, namespace); + const flatEntries = flattenTokenEntries(values); + const declarations = flatEntries + .map(([key, value]) => `--${cssNs}-${key}: ${value}`) + .join('; '); - return createTokenProxy(namespace, '', allKeys) as TokenRef; -} + const css = `:root { ${declarations}; }`; + insertRule(`tokens:${cssNs}`, css); -/** - * Reference tokens defined elsewhere without injecting CSS. - * - * Returns a typed proxy that produces var() references. - * Useful for consuming shared tokens from a different module. - * - * @example - * ```ts - * const color = useTokens('color'); - * color.primary // "var(--color-primary)" - * ``` - */ -export function useTokens(namespace: string): TokenRef { - if ( - process.env.NODE_ENV !== 'production' && - registeredNamespaces.size > 0 && - !registeredNamespaces.has(namespace) - ) { - console.warn( - `[typestyles] tokens.use('${namespace}') references a namespace that hasn't been created yet. ` + - `Make sure tokens.create('${namespace}', ...) is called before using these tokens.`, - ); + const allKeys = getAllKeys(values); + createdTokenKeys.set(namespace, allKeys); + + return createTokenProxy(cssNs, '', allKeys) as TokenRef; } - const allKeys = createdTokenKeys.get(namespace) ?? new Set(); - return createTokenProxy(namespace, '', allKeys) as TokenRef; -} + function use(namespace: string): TokenRef { + if ( + process.env.NODE_ENV !== 'production' && + registeredNamespaces.size > 0 && + !registeredNamespaces.has(namespace) + ) { + console.warn( + `[typestyles] tokens.use('${namespace}') references a namespace that hasn't been created yet. ` + + `Make sure tokens.create('${namespace}', ...) is called before using these tokens.`, + ); + } + + const cssNs = scopedTokenNamespace(scopeId, namespace); + const allKeys = createdTokenKeys.get(namespace) ?? new Set(); + return createTokenProxy(cssNs, '', allKeys) as TokenRef; + } -// createTheme has moved to theme.ts — re-exported via index.ts on the tokens namespace. + return { + scopeId, + create, + use, + createTheme: (name, config) => createTheme(name, config, scopeId), + createDarkMode: (name, darkOverrides) => createDarkMode(name, darkOverrides, scopeId), + when, + colorMode, + }; +} diff --git a/scripts/generate-api-reference.js b/scripts/generate-api-reference.js index 1157ca3..dade4eb 100755 --- a/scripts/generate-api-reference.js +++ b/scripts/generate-api-reference.js @@ -111,7 +111,12 @@ Auto-generated documentation for all typestyles APIs. apiDoc += `## Core Exports\n\n`; // Document styles - const stylesDoc = jsdocs.find((j) => j.description.includes('Style creation')); + const stylesDoc = jsdocs.find( + (j) => + j.description.includes('style API') || + j.description.includes('Style creation') || + j.description.includes('Default style'), + ); if (stylesDoc) { apiDoc += `### \`styles\`\n\n${stylesDoc.description}\n\n`; apiDoc += `**Methods:**\n\n`; @@ -119,21 +124,38 @@ Auto-generated documentation for all typestyles APIs. apiDoc += `- \`styles.class(name, properties)\`: Create a single class\n`; apiDoc += `- \`styles.hashClass(properties, label?)\`: Create a deterministic hashed class\n`; apiDoc += `- \`styles.compose(...fns)\`: Compose multiple style functions\n`; - apiDoc += `- \`styles.withUtils(utils)\`: Create utility-aware styles API\n\n`; + apiDoc += `- \`styles.withUtils(utils)\`: Create utility-aware styles API\n`; + apiDoc += `- \`styles.classNaming\`: Read-only resolved naming config for the default \`styles\` instance\n\n`; apiDoc += `\n`; } + apiDoc += `### \`createStyles(options?)\`\n\n`; + apiDoc += `Returns a new style API (same shape as \`styles\`) with its own class naming config. Pass \`Partial\`: \`mode\` (\`'semantic' | 'hashed' | 'atomic'\`), \`prefix\`, \`scopeId\`. Use one instance per package or micro-frontend.\n\n`; + apiDoc += `The default \`import { styles } from 'typestyles'\` is \`createStyles()\` with default options.\n\n`; + // Document tokens - const tokensDoc = jsdocs.find((j) => j.description.includes('Design token')); + const tokensDoc = jsdocs.find( + (j) => + j.description.includes('Design token') || + j.description.includes('token API') || + j.description.includes('Default token'), + ); if (tokensDoc) { apiDoc += `### \`tokens\`\n\n${tokensDoc.description}\n\n`; apiDoc += `**Methods:**\n\n`; apiDoc += `- \`tokens.create(namespace, values)\`: Creates CSS custom properties\n`; apiDoc += `- \`tokens.use(namespace)\`: References existing tokens\n`; - apiDoc += `- \`tokens.createTheme(name, overrides)\`: Creates theme class\n`; + apiDoc += `- \`tokens.createTheme(name, config)\`: Registers a theme class that overrides token custom properties\n`; + apiDoc += `- \`tokens.createDarkMode(name, darkOverrides)\`: Shorthand theme with a single dark \`@media\` branch\n`; + apiDoc += `- \`tokens.when\` / \`tokens.colorMode\`: Condition helpers for themes\n`; + apiDoc += `- \`tokens.scopeId\`: The scope passed to \`createTokens\`, if any\n\n`; apiDoc += `\n`; } + apiDoc += `### \`createTokens(options?)\`\n\n`; + apiDoc += `Returns a token + theme API bound to an optional \`scopeId\`. When set, \`tokens.create('color', …)\` emits \`--{scopeId}-color-*\` variables and \`tokens.createTheme('dark', …)\` registers \`.theme-{scopeId}-dark\` (sanitized segments).\n\n`; + apiDoc += `The default \`import { tokens } from 'typestyles'\` is \`createTokens()\` (no scope).\n\n`; + // Document keyframes const keyframesDoc = jsdocs.find((j) => j.description.includes('Keyframe')); if (keyframesDoc) { @@ -144,7 +166,11 @@ Auto-generated documentation for all typestyles APIs. } // Document color - const colorDoc = jsdocs.find((j) => j.description.includes('Color')); + const colorDoc = jsdocs.find( + (j) => + j.description.includes('Type-safe CSS color') || + (j.description.includes('Color') && j.description.includes('function')), + ); if (colorDoc) { apiDoc += `### \`color\`\n\n${colorDoc.description}\n\n`; apiDoc += `**Functions:**\n\n`; @@ -161,6 +187,12 @@ Auto-generated documentation for all typestyles APIs. apiDoc += `\n`; } + apiDoc += `### Class naming helpers\n\n`; + apiDoc += `- \`mergeClassNaming(partial?)\`: Build a full \`ClassNamingConfig\` from partial options\n`; + apiDoc += `- \`defaultClassNamingConfig\`: Default \`mode\`, \`prefix\`, and \`scopeId\`\n`; + apiDoc += `- \`scopedTokenNamespace(scopeId, logicalNamespace)\`: CSS variable namespace segment for scoped token instances\n\n`; + apiDoc += `See [Class naming](/docs/class-naming).\n\n`; + // Add example usage apiDoc += `## Usage Examples\n\n`; apiDoc += `### Creating Styles\n\n\`\`\`ts\n`; @@ -186,6 +218,12 @@ Auto-generated documentation for all typestyles APIs. apiDoc += `color.primary; // "var(--color-primary)"\n`; apiDoc += `\`\`\`\n\n`; + apiDoc += `### Scoped instances (libraries / micro-frontends)\n\n\`\`\`ts\n`; + apiDoc += `import { createStyles, createTokens } from 'typestyles';\n\n`; + apiDoc += `export const styles = createStyles({ scopeId: 'my-ds', mode: 'hashed', prefix: 'ds' });\n`; + apiDoc += `export const tokens = createTokens({ scopeId: 'my-ds' });\n`; + apiDoc += `\`\`\`\n\n`; + apiDoc += `### Creating Animations\n\n\`\`\`ts\n`; apiDoc += `import { keyframes } from 'typestyles';\n\n`; apiDoc += `const fadeIn = keyframes.create('fadeIn', {\n`; From 47ca9e29793ff5a6093f3cc6faeb8ec15466f044 Mon Sep 17 00:00:00 2001 From: Danny Banks Date: Fri, 3 Apr 2026 21:37:38 -0700 Subject: [PATCH 2/2] fixes --- examples/design-system/src/create-theme.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/design-system/src/create-theme.ts b/examples/design-system/src/create-theme.ts index 5cb6889..4b29a31 100644 --- a/examples/design-system/src/create-theme.ts +++ b/examples/design-system/src/create-theme.ts @@ -1,4 +1,4 @@ -import { tokens } from 'typestyles'; +import { tokens } from './runtime'; import type { DesignTheme, DesignThemeConfig } from './types'; /**