Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .changeset/font-assets.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
'@workflowbuilder/ui': major
'@workflowbuilder/sdk': major
---

Fonts now ship as `.woff2` assets next to the stylesheets, with only the two dominant faces inlined.

Breaking changes:

- Preserve the published `dist/assets` directory next to copied stylesheets so their relative font URLs keep resolving.
- If a Content Security Policy exists, allow `data:` and `'self'` or the serving origin in `font-src`, or in `default-src` when `font-src` is absent.

Only Poppins latin 400 and 600 are inline. Other weights, Inter, and non-ASCII glyphs use `font-display: swap` assets and may briefly render in the fallback font; preload the relevant files when that flash of unstyled text (FOUT) is unacceptable.
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ Backend reads `DATABASE_URL` and `TEMPORAL_ADDRESS`; defaults work out of the bo
| ESLint | `pnpm lint` / `pnpm lint:fix` | Per-workspace configs |
| Prettier | `pnpm format` | Sorts imports via `@trivago/prettier-plugin-sort-imports` |
| TypeScript | `pnpm typecheck` | Per-workspace `tsconfig.json` |
| Knip | Part of `pnpm check` | Detects unused exports/dependencies |
| Knip | `pnpm exec knip` | Detects unused exports/dependencies (not part of `pnpm check` or CI) |
| Vitest | `pnpm test` | Runs in every workspace with a `test` script — recursive, so a new workspace is picked up automatically |
| Full check | `pnpm check` | Run before PR |

Expand Down
7 changes: 7 additions & 0 deletions DECISION-LOGS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,20 @@
- _08.04.2025_: [Lazy-loaded Icons](./apps/icons/lazy-loaded-icons-08-04-2025.decision-log.md)
- _15.04.2025_: [Internationalization implementation with i18next](./packages/sdk/src/features/i18n/i18next.decision-log.md)
- _26.05.2025_: [JSON Form Validation Strategy](./packages/sdk/src/features/json-form/form-validation.decision-log.md)
- _05.03.2026_: [Independent docs deployment strategy](./apps/docs/docs-deployment.decision-log.md)
- _13.03.2026_: [Remark plugin for automatic base path link rewriting](./apps/docs/remark-base-path-links.decision-log.md)
- _16.04.2026_: [CSP-safe Ajv replacement with @cfworker/json-schema](./packages/sdk/src/utils/validation/workflow-builder-validator-16-04-2026.decision-log.md)
- _22.04.2026_: [SDK restructuring — inversion, relocation, plugin API, config naming](./packages/sdk/sdk-restructuring.decision-log.md)
- _27.04.2026_: [Default to 127.0.0.1 binding for the reference backend](./apps/backend/local-dev-binding.decision-log.md)
- _27.04.2026_: [Workflow cancellation handling in Temporal engine](./apps/execution-worker/src/engines/temporal/workflows/cancellation-handling.decision-log.md)
- _28.04.2026_: [Topological scheduling for the graph runner](./packages/execution-core/topological-scheduling.decision-log.md)
- _29.04.2026_: [Decision executor fails fast on no matching branch](./packages/execution-core/decision-no-match.decision-log.md)
- _30.04.2026 (revised 04.05.2026 after team review)_: [Audience-based docs IA + schema authoring reference](./apps/docs/docs-restructure.decision-log.md)
- _30.04.2026_: [TypeDoc-driven API Reference for `@workflowbuilder/sdk`](./apps/docs/typedoc-api-reference.decision-log.md)
- _05.05.2026_: [Extract AI Studio from `apps/demo` into its own `apps/ai-studio` app](./apps/ai-studio/ai-studio-extraction.decision-log.md)
- _05.05.2026_: [Workspace layout — relocate libraries to `packages/`](./packages/sdk/workspace-layout.decision-log.md)
- _06.05.2026_: [Make execution-core generic over the consumer's node union](./packages/execution-core/generic-execution-core.decision-log.md)
- _15.05.2026_: [AuthPort seam for backend authn/authz](./apps/backend/auth-port.decision-log.md)
- _03.06.2026_: [TenantContextPort — multi-tenant identity seam for the reference backend](./apps/backend/tenant-context-port.decision-log.md)
- _07.08.2026_: [Keep the postcss box-sizing plugin over lint-based or selector-based alternatives](./packages/ui/postcss-box-sizing.decision-log.md)
- _31.08.2026_: [Ship common font faces inline and the rest as assets](./packages/ui/font-assets.decision-log.md)
2 changes: 2 additions & 0 deletions apps/backend/tenant-context-port.decision-log.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

### Proposed by: Kacper Cierzniewski

### Date: 03.06.2026

### Proposed: 21.05.2026 — Landed: 03.06.2026 (`fa5999dd`)

> This is the **decision** (why this shape, what was rejected, what it does and does not protect). The **how-to-wire-it** lives in [`multi-tenancy.md`](./multi-tenancy.md) — that document tracks the code and is the source of truth for current signatures and per-seam status. If a snippet here ever disagrees with the code, the code wins.
Expand Down
6 changes: 3 additions & 3 deletions apps/docs/src/components/ui-examples/component-preview.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import componentCss from '@workflowbuilder/ui/index.css?raw';
import globalCss from '@workflowbuilder/ui/styles.css?raw';
import { type ReactNode, useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';

import styles from './component-preview.module.css';

import previewCss from '../../../../../packages/ui/tmp/docs-preview.css?raw';

// Examples render in a shadow root so Starlight's rules cannot reach them and
// the library's cannot leak out. Inherited and custom properties still cross
// the boundary - that is how the docs theme reaches the examples. Inside a
// shadow root `:root` matches nothing, hence the retarget to `:host`.
const shadowCss = `${`${globalCss}\n${componentCss}`.replaceAll(':root', ':host')}
const shadowCss = `${previewCss.replaceAll(':root', ':host')}
:host > :not(style) { max-width: 100%; }`;

export function ComponentPreview({ children }: { children: ReactNode }) {
Expand Down
4 changes: 3 additions & 1 deletion apps/docs/src/content/docs/get-started/theming.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ The aggregated `style.css` ships with the SDK's default visual layer. Override C

## Typography

Poppins is bundled into `style.css` as inline base64 woff2 (latin + latin-ext, weights 300–700). No external font CDN is contacted at runtime — works under strict CSP, behind GDPR-controlled consent flows, and in air-gapped deployments.
`style.css` inlines Poppins latin 400 and 600 and references the remaining Poppins and Inter faces in the adjacent `assets` directory. Preserve that `dist` layout when copying the stylesheet. If a Content Security Policy (CSP) exists, allow both `data:` and `'self'` or the origin serving those assets in `font-src`, or in `default-src` when `font-src` is absent. No external font CDN is contacted at runtime, so the SDK still works behind consent controls and in air-gapped deployments.

Other weights, Inter, and non-ASCII glyphs use `font-display: swap` assets. They can briefly appear in the fallback font while the matching file loads; preload the relevant `.woff2` files when that flash of unstyled text (FOUT) is unacceptable.

Override `--wb-font-family` to use a different face:

Expand Down
62 changes: 51 additions & 11 deletions apps/docs/src/content/docs/ui-library/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,55 @@ your own. Everything else the components need, including `@base-ui/react`

## Styles

Importing a component from the package root injects that component's CSS
automatically, including the layer order (`@layer ui.base, ui.component`)
and typography classes - so the only thing left to add is the design
tokens:
The package has six style surfaces:

- **`@workflowbuilder/ui`** is the root barrel. It provides every component and
all component and global CSS, but not the design token values.
- **`@workflowbuilder/ui/<component>`** provides one component and only that
component's CSS. Add `styles.css` for the global reset, typography and fonts,
and `tokens.css` for the design tokens.
- **`@workflowbuilder/ui/index.css`** provides all component and global CSS,
including typography and fonts, but not the design token values.
- **`@workflowbuilder/ui/styles.css`** provides the global reset, typography and
fonts, but no component CSS or design token values.
- **`@workflowbuilder/ui/fonts.css`** provides the font faces, but no global,
component or design token rules.
- **`@workflowbuilder/ui/tokens.css`** provides the design token values, but no
component, typography or font rules.

The stylesheets reference `./assets/*.woff2`, so preserve the package's `dist`
layout when copying or serving them. The font assets' SIL Open Font License 1.1
texts ship beside them as `assets/OFL-poppins.txt` and `assets/OFL-inter.txt`.

Only Poppins latin 400 and 600 are inline. Other weights, Inter, and non-ASCII
glyphs use `font-display: swap` assets, so they can briefly appear in the
fallback font while loading.

### Shipped font faces

| Family | Weight | Subset | File name | Delivery |
| ------- | ------ | --------- | ------------------------------------ | -------- |
| Poppins | 300 | latin | `poppins-latin-300-normal.woff2` | Asset |
| Poppins | 400 | latin | `poppins-latin-400-normal.woff2` | Inline |
| Poppins | 500 | latin | `poppins-latin-500-normal.woff2` | Asset |
| Poppins | 600 | latin | `poppins-latin-600-normal.woff2` | Inline |
| Poppins | 700 | latin | `poppins-latin-700-normal.woff2` | Asset |
| Poppins | 300 | latin-ext | `poppins-latin-ext-300-normal.woff2` | Asset |
| Poppins | 400 | latin-ext | `poppins-latin-ext-400-normal.woff2` | Asset |
| Poppins | 500 | latin-ext | `poppins-latin-ext-500-normal.woff2` | Asset |
| Poppins | 600 | latin-ext | `poppins-latin-ext-600-normal.woff2` | Asset |
| Poppins | 700 | latin-ext | `poppins-latin-ext-700-normal.woff2` | Asset |
| Inter | 400 | latin | `inter-latin-400-normal.woff2` | Asset |
| Inter | 400 | latin-ext | `inter-latin-ext-400-normal.woff2` | Asset |

Preload the relevant `.woff2` files when that flash of unstyled text (FOUT) is
unacceptable:

```html
<link rel="preload" href="./assets/poppins-latin-ext-400-normal.woff2" as="font" type="font/woff2" crossorigin />
```

With the root barrel, add only the design tokens:

```ts
// Design tokens (the `--wb-*` custom properties).
Expand All @@ -39,16 +84,11 @@ import '@workflowbuilder/ui/tokens.css';
import { Button } from '@workflowbuilder/ui';
```

Need only one component's styles without the others? Import the per-component
subpath instead. That only injects the component's own CSS. Every built
stylesheet carries the cascade-layer order, so import order doesn't matter;
add the global stylesheet once if you also want the typography classes, plus
the tokens:
Every built stylesheet carries the cascade-layer order, so import order does
not matter. With a per-component subpath, add the global stylesheet and tokens:

```ts
// Optional: global typography classes.
import '@workflowbuilder/ui/styles.css';
// Design tokens (the `--wb-*` custom properties).
import '@workflowbuilder/ui/tokens.css';
```

Expand Down
3 changes: 2 additions & 1 deletion apps/docs/src/content/docs/ui-library/typography.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ Which role to reach for:
| Card or group title | `title-s`/`-m` |
| Running text and descriptions | `body-m` (`body-s` for dense UI) |
| Captions, helper text, metadata | `label-m`/`-l` |
| Buttons and chips | `label-l-emphasized` |
| Buttons | `label-l-emphasized` |
| Chips (every size) | `label-s` |
| Canvas node and port labels | `node-s`/`-m` |
| Code, token names, IDs | `code` |

Expand Down
8 changes: 3 additions & 5 deletions knip.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,6 @@ export default {
'packages/sdk': {
entry: ['src/index.ts', 'src/**/*.{ts,tsx}', 'vite.config.mts'],
project: ['src/**/*.{ts,tsx}', 'vite.config.mts'],
// @fontsource/poppins is consumed via @import statements in src/index.css.
// Knip only walks JS/TS, so it can't see the CSS reference.
ignoreDependencies: ['@fontsource/poppins'],
},
'apps/backend': {
entry: ['src/server.ts', 'drizzle.config.ts'],
Expand All @@ -60,8 +57,9 @@ export default {
entry: ['src/index.ts', 'vite.config.mts', 'scripts/check-built-css.ts'],
project: ['src/**/*.{ts,tsx}', '*.mts', 'scripts/**/*.ts'],
// Built tokens are copied by relative path (../tokens/dist) in vite.config,
// so the workspace dep is real even though it is never imported by name.
ignoreDependencies: ['@workflowbuilder/ui-tokens'],
// and the fontsource families feed the post-build font generator - real
// dependencies knip's JS/TS walk cannot see.
ignoreDependencies: ['@workflowbuilder/ui-tokens', '@fontsource/poppins', '@fontsource/inter'],
},
'packages/tokens': {
entry: ['src/index.ts'],
Expand Down
2 changes: 1 addition & 1 deletion packages/sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
"lint": "eslint",
"test": "vitest run",
"test:watch": "vitest",
"build:lib": "vite build",
"build:lib": "vite build && pnpm exec tsx ../ui/scripts/check-built-css.ts dist",
"prepublishOnly": "pnpm build:lib"
},
"peerDependencies": {
Expand Down
2 changes: 0 additions & 2 deletions packages/sdk/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@
See packages/ui/css-layers.md. */
@layer ui.base, ui.component;

/* Poppins and Inter ship with @workflowbuilder/ui (next to the wb-text-*
classes that require them) and arrive through the JS module graph. */
@import '@workflowbuilder/ui/tokens.css';

/* @workflowbuilder/ui styles arrive through the JS module graph - importing
Expand Down
52 changes: 51 additions & 1 deletion packages/sdk/vite.config.mts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
/// <reference types="vitest/config" />
import react from '@vitejs/plugin-react';
import fs from 'node:fs';
import path from 'node:path';
import { defineConfig } from 'vite';
import { type Plugin, defineConfig } from 'vite';
import dts from 'vite-plugin-dts';
import svgr from 'vite-plugin-svgr';

Expand Down Expand Up @@ -51,10 +52,59 @@ const EXTERNAL_PACKAGES = [
const isExternalPackage = (id: string) =>
EXTERNAL_PACKAGES.some((packageName) => id === packageName || id.startsWith(`${packageName}/`));

function emitUiFontAssets(): Plugin {
const distributionDirectory = path.resolve(import.meta.dirname, 'dist');
let buildFailed = false;

return {
name: 'wb-sdk:emit-ui-font-assets',
apply: 'build',
buildStart() {
buildFailed = false;
},
buildEnd(error) {
buildFailed = error !== undefined;
},
closeBundle() {
if (buildFailed) return;

const uiDistribution = path.resolve(import.meta.dirname, '../ui/dist');
const stylesheetPath = path.resolve(distributionDirectory, 'style.css');
const fontStylesPath = path.resolve(uiDistribution, 'fonts.css');
const assetsDirectory = path.resolve(distributionDirectory, 'assets');

if (!fs.existsSync(fontStylesPath)) {
throw new Error('@workflowbuilder/ui dist is missing fonts.css - build the UI first: `pnpm build:ui`');
}
if (!fs.existsSync(stylesheetPath)) {
throw new Error(
`wb-sdk:emit-ui-font-assets: ${stylesheetPath} is missing - ` +
'the Vite build emitted no SDK stylesheet to receive the font faces',
);
}

const fontStyles = fs.readFileSync(fontStylesPath, 'utf8').replace(/^@layer ui\.base, ui\.component;\s*/, '');
const stylesheet = fs
.readFileSync(stylesheetPath, 'utf8')
.replaceAll(/@font-face\s*{(?=[^{}]*font-family:\s*["']?(?:Poppins|Inter)["']?\s*;)[^{}]*}/g, '');

fs.mkdirSync(assetsDirectory, { recursive: true });
for (const file of fs.readdirSync(path.resolve(uiDistribution, 'assets'))) {
if (!file.endsWith('.woff2')) continue;
fs.copyFileSync(path.resolve(uiDistribution, 'assets', file), path.resolve(assetsDirectory, file));
}

fs.writeFileSync(stylesheetPath, stylesheet);
fs.appendFileSync(stylesheetPath, `\n${fontStyles}`);
},
};
}

export default defineConfig(({ command }) => ({
plugins: [
svgr(),
react(),
emitUiFontAssets(),
dts({
// Bundle all type declarations into a single dist/index.d.ts file
// via rollup-plugin-dts (matches the meeting decision to stop
Expand Down
Loading