feat: add @bchen/ui v0.1.0 — dark-mode token palette and 16px base rule
- dist/tokens.css: canonical light + @media dark semantic-token palette
extracted from inventory (cross-checked against authd and buchinese).
Covers --fg, --fg-muted, --bg, --bg-elevated, --surface, --accent*,
--danger*, --border*, --input-*, --warning, primitive --gray-* scale.
- dist/base.css: `input, textarea, select { font-size: 16px; }` prevents
iOS Safari auto-zoom on focus.
- tests/tokens.test.ts: vitest — token presence in both light and dark
blocks; WCAG AA contrast (>=4.5:1 body, >=3.0:1 UI) via inline
hex-to-luminance math. All 10 tests green.
- tests/base.test.ts: vitest — asserts 16px rule covers all three selectors.
- .gitea/workflows/release.yml: tag-triggered CI — npm ci + npm test,
CHANGELOG version gate, dist/scripts security grep, Gitea release
artifact upload.
- README.md: four-step consumer integration guide with copy-pasteable
snippets (package.json dep, Dockerfile cp, HTML link tags, app CSS).
- CHANGELOG.md: v0.1.0 entry.
npm audit --omit=dev: 0 vulnerabilities
Closes #1
This commit is contained in:
69
.gitea/workflows/release.yml
Normal file
69
.gitea/workflows/release.yml
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
name: release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '24'
|
||||||
|
|
||||||
|
- name: install
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: test
|
||||||
|
run: npm test
|
||||||
|
|
||||||
|
- name: validate changelog
|
||||||
|
run: |
|
||||||
|
VERSION="${GITHUB_REF_NAME#v}"
|
||||||
|
if ! grep -qF "## [${VERSION}]" CHANGELOG.md && ! grep -qE "^## ${VERSION}$" CHANGELOG.md; then
|
||||||
|
echo "ERROR: CHANGELOG.md has no entry for version ${VERSION}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: security scan (dist/scripts)
|
||||||
|
run: |
|
||||||
|
if [ -d dist/scripts ]; then
|
||||||
|
if grep -rE 'eval\b|new Function\b|\.innerHTML\s*=' dist/scripts/; then
|
||||||
|
echo "ERROR: unsafe pattern detected in dist/scripts/"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "dist/scripts/ scan: clean"
|
||||||
|
else
|
||||||
|
echo "dist/scripts/ does not exist — scan skipped"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: package dist artifact
|
||||||
|
run: tar -czf "dist-${GITHUB_REF_NAME}.tar.gz" dist/
|
||||||
|
|
||||||
|
- name: create release and upload artifact
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||||
|
run: |
|
||||||
|
SERVER="${GITHUB_SERVER_URL}"
|
||||||
|
REPO="${GITHUB_REPOSITORY}"
|
||||||
|
TAG="${GITHUB_REF_NAME}"
|
||||||
|
|
||||||
|
RELEASE=$(curl -sf -X POST \
|
||||||
|
"${SERVER}/api/v1/repos/${REPO}/releases" \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"tag_name\":\"${TAG}\",\"name\":\"${TAG}\"}")
|
||||||
|
|
||||||
|
RELEASE_ID=$(echo "${RELEASE}" | grep -o '"id":[0-9]*' | head -1 | sed 's/"id"://')
|
||||||
|
|
||||||
|
curl -sf -X POST \
|
||||||
|
"${SERVER}/api/v1/repos/${REPO}/releases/${RELEASE_ID}/assets" \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
-F "attachment=@dist-${TAG}.tar.gz;type=application/gzip"
|
||||||
|
|
||||||
|
echo "Release ${TAG} created (id=${RELEASE_ID})"
|
||||||
11
CHANGELOG.md
Normal file
11
CHANGELOG.md
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to `@bchen/ui` will be documented in this file.
|
||||||
|
|
||||||
|
## [0.1.0] — 2026-05-13
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `dist/tokens.css` — canonical dark-mode semantic-token palette for the bchen.dev fleet. Light-mode `:root` block with primitive palette (`--black`, `--white`, `--gray-*`, `--red`) and semantic tokens (`--fg`, `--fg-muted`, `--bg`, `--bg-elevated`, `--surface`, `--accent`, `--accent-fg`, `--accent-hover`, `--danger`, `--danger-fg`, `--danger-bg`, `--warning`, `--border-color`, `--border-strong`, `--border`, `--input-*`). `@media (prefers-color-scheme: dark)` block overrides all color-sensitive tokens for OS-driven dark mode.
|
||||||
|
- `dist/base.css` — `input, textarea, select { font-size: 16px; }` rule. Prevents iOS Safari auto-zoom on focus (requires computed font-size ≥ 16px).
|
||||||
|
- WCAG AA contrast tested in CI: `--fg/--bg` and `--fg-muted/--bg` ≥ 4.5:1, `--border-strong/--bg` ≥ 3.0:1, in both light and dark.
|
||||||
129
README.md
129
README.md
@@ -1,3 +1,128 @@
|
|||||||
# bchen-ui
|
# @bchen/ui
|
||||||
|
|
||||||
Shared CSS tokens + framework-less UI components for the bchen.dev fleet. Empty placeholder created during 2026-05-13 queue migration to host the v1 spec issue; first commit pending implementer pickup.
|
Shared CSS tokens and UI components for the bchen.dev fleet.
|
||||||
|
|
||||||
|
## What's included
|
||||||
|
|
||||||
|
| Export | Contents |
|
||||||
|
|--------|----------|
|
||||||
|
| `@bchen/ui/tokens.css` | Semantic-token palette — light mode `:root` block + `@media (prefers-color-scheme: dark)` override |
|
||||||
|
| `@bchen/ui/base.css` | `input, textarea, select { font-size: 16px; }` — prevents iOS Safari auto-zoom on focus |
|
||||||
|
|
||||||
|
## Consumer integration (four steps)
|
||||||
|
|
||||||
|
### 1. Add the dependency
|
||||||
|
|
||||||
|
```json
|
||||||
|
// package.json
|
||||||
|
"dependencies": {
|
||||||
|
"@bchen/ui": "git+https://gitea.bchen.dev/brendan/bchen-ui.git#v0.1.0"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then run `npm install` (requires `git` in your build image — see Step 2).
|
||||||
|
|
||||||
|
### 2. Dockerfile — copy CSS to your public dir
|
||||||
|
|
||||||
|
After `npm ci`, copy the dist files into your served static directory so they're available at runtime without bundler involvement:
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
FROM node:24-alpine
|
||||||
|
RUN apk add --no-cache python3 make g++ git
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
# Copy @bchen/ui dist files into public vendor dir
|
||||||
|
RUN mkdir -p public/vendor/@bchen/ui && \
|
||||||
|
cp -r node_modules/@bchen/ui/dist/. public/vendor/@bchen/ui/
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. HTML — link vendor CSS before your app stylesheet
|
||||||
|
|
||||||
|
```html
|
||||||
|
<head>
|
||||||
|
<!-- vendor CSS first -->
|
||||||
|
<link rel="stylesheet" href="/vendor/@bchen/ui/tokens.css">
|
||||||
|
<link rel="stylesheet" href="/vendor/@bchen/ui/base.css">
|
||||||
|
|
||||||
|
<!-- your app CSS after -->
|
||||||
|
<link rel="stylesheet" href="/style.css">
|
||||||
|
</head>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. App CSS — your overrides come after vendor
|
||||||
|
|
||||||
|
Your `style.css` can reference the tokens directly:
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* style.css — loaded after vendor, so tokens are already defined */
|
||||||
|
body {
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--fg);
|
||||||
|
font-family: var(--font);
|
||||||
|
}
|
||||||
|
|
||||||
|
a { color: var(--accent); }
|
||||||
|
.muted { color: var(--fg-muted); }
|
||||||
|
```
|
||||||
|
|
||||||
|
Dark mode is automatic — no JavaScript required. The `tokens.css` `@media (prefers-color-scheme: dark)` block overrides the palette based on the OS setting.
|
||||||
|
|
||||||
|
## Token reference
|
||||||
|
|
||||||
|
### Primitive palette (resolve base values)
|
||||||
|
|
||||||
|
| Token | Light | Dark |
|
||||||
|
|-------|-------|------|
|
||||||
|
| `--black` | `#000` | — |
|
||||||
|
| `--white` | `#fff` | — |
|
||||||
|
| `--gray-50` | `#fafafa` | — |
|
||||||
|
| `--gray-100` | `#f0f0f0` | — |
|
||||||
|
| `--gray-200` | `#e0e0e0` | — |
|
||||||
|
| `--gray-400` | `#999` | — |
|
||||||
|
| `--gray-600` | `#555` | — |
|
||||||
|
| `--red` | `#c00` | — |
|
||||||
|
|
||||||
|
### Semantic color tokens
|
||||||
|
|
||||||
|
| Token | Light | Dark |
|
||||||
|
|-------|-------|------|
|
||||||
|
| `--fg` | `#000` | `#e5e7eb` |
|
||||||
|
| `--fg-muted` | `#555` | `#9ca3af` |
|
||||||
|
| `--bg` | `#fff` | `#0f1115` |
|
||||||
|
| `--bg-elevated` | `#fafafa` | `#1a1d23` |
|
||||||
|
| `--surface` | `#fff` | `#0f1115` |
|
||||||
|
| `--accent` | `#000` | `#e5e7eb` |
|
||||||
|
| `--accent-fg` | `#fff` | `#0f1115` |
|
||||||
|
| `--accent-hover` | `#555` | `#cbd5e1` |
|
||||||
|
| `--danger` | `#c00` | `#f87171` |
|
||||||
|
| `--danger-fg` | `#fff` | `#0f1115` |
|
||||||
|
| `--danger-bg` | `#fff5f5` | `rgba(248,113,113,0.12)` |
|
||||||
|
| `--warning` | `#555` | — |
|
||||||
|
| `--border-color` | `#e0e0e0` | `#374151` |
|
||||||
|
| `--border-strong` | `#000` | `#6b7280` |
|
||||||
|
| `--border` | `1px solid #000` | `1px solid #6b7280` |
|
||||||
|
|
||||||
|
### Input tokens
|
||||||
|
|
||||||
|
| Token | Light | Dark |
|
||||||
|
|-------|-------|------|
|
||||||
|
| `--input-bg` | `#fff` | `#1a1d23` |
|
||||||
|
| `--input-fg` | `#000` | `#e5e7eb` |
|
||||||
|
| `--input-border` | `#000` | `#6b7280` |
|
||||||
|
| `--input-bg-focus` | `#fafafa` | `#0f1115` |
|
||||||
|
|
||||||
|
## WCAG contrast guarantees
|
||||||
|
|
||||||
|
All values in `tokens.css` are tested (see `tests/tokens.test.ts`) to meet:
|
||||||
|
|
||||||
|
- **AA body text (≥ 4.5:1):** `--fg/--bg`, `--fg-muted/--bg` in both light and dark
|
||||||
|
- **UI elements (≥ 3.0:1):** `--border-strong/--bg` in both light and dark
|
||||||
|
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
See [CHANGELOG.md](./CHANGELOG.md).
|
||||||
|
|||||||
4
dist/base.css
vendored
Normal file
4
dist/base.css
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
/* @bchen/ui v0.1.0 — base resets
|
||||||
|
* Prevents iOS Safari auto-zoom on focus (requires font-size >= 16px). */
|
||||||
|
|
||||||
|
input, textarea, select { font-size: 16px; }
|
||||||
70
dist/tokens.css
vendored
Normal file
70
dist/tokens.css
vendored
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
/* @bchen/ui v0.1.0 — semantic token palette
|
||||||
|
* Canonical light + dark values extracted from the bchen.dev fleet.
|
||||||
|
* Source of truth: inventory (cross-checked against authd, buchinese). */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
/* Primitive palette */
|
||||||
|
--black: #000;
|
||||||
|
--white: #fff;
|
||||||
|
--gray-50: #fafafa;
|
||||||
|
--gray-100: #f0f0f0;
|
||||||
|
--gray-200: #e0e0e0;
|
||||||
|
--gray-400: #999;
|
||||||
|
--gray-600: #555;
|
||||||
|
--red: #c00;
|
||||||
|
|
||||||
|
/* Typography */
|
||||||
|
--font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
|
--font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||||
|
|
||||||
|
/* Layout */
|
||||||
|
--radius: 0;
|
||||||
|
--header-height: 48px;
|
||||||
|
|
||||||
|
/* Border tokens */
|
||||||
|
--border-color: var(--gray-200);
|
||||||
|
--border-strong: var(--black);
|
||||||
|
--border: 1px solid var(--border-strong);
|
||||||
|
|
||||||
|
/* Semantic color tokens */
|
||||||
|
--fg: var(--black);
|
||||||
|
--fg-muted: var(--gray-600);
|
||||||
|
--bg: var(--white);
|
||||||
|
--bg-elevated: var(--gray-50);
|
||||||
|
--surface: var(--white);
|
||||||
|
--accent: var(--black);
|
||||||
|
--accent-fg: var(--white);
|
||||||
|
--accent-hover: var(--gray-600);
|
||||||
|
--danger: var(--red);
|
||||||
|
--danger-fg: var(--white);
|
||||||
|
--danger-bg: #fff5f5;
|
||||||
|
--warning: var(--gray-600);
|
||||||
|
|
||||||
|
/* Input tokens */
|
||||||
|
--input-bg: var(--white);
|
||||||
|
--input-fg: var(--black);
|
||||||
|
--input-border: var(--black);
|
||||||
|
--input-bg-focus: var(--gray-50);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--bg: #0f1115;
|
||||||
|
--bg-elevated: #1a1d23;
|
||||||
|
--surface: #0f1115;
|
||||||
|
--fg: #e5e7eb;
|
||||||
|
--fg-muted: #9ca3af;
|
||||||
|
--border-color: #374151;
|
||||||
|
--border-strong: #6b7280;
|
||||||
|
--accent: #e5e7eb;
|
||||||
|
--accent-fg: #0f1115;
|
||||||
|
--accent-hover: #cbd5e1;
|
||||||
|
--danger: #f87171;
|
||||||
|
--danger-fg: #0f1115;
|
||||||
|
--danger-bg: rgba(248, 113, 113, 0.12);
|
||||||
|
--input-bg: #1a1d23;
|
||||||
|
--input-fg: #e5e7eb;
|
||||||
|
--input-border: #6b7280;
|
||||||
|
--input-bg-focus: #0f1115;
|
||||||
|
}
|
||||||
|
}
|
||||||
1467
package-lock.json
generated
Normal file
1467
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
19
package.json
Normal file
19
package.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"name": "@bchen/ui",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Shared CSS tokens and UI components for the bchen.dev fleet",
|
||||||
|
"type": "module",
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"exports": {
|
||||||
|
"./tokens.css": "./dist/tokens.css",
|
||||||
|
"./base.css": "./dist/base.css"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"test": "vitest run"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"vitest": "^2.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
16
tests/base.test.ts
Normal file
16
tests/base.test.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { readFileSync } from 'fs';
|
||||||
|
|
||||||
|
const css = readFileSync(new URL('../dist/base.css', import.meta.url), 'utf-8');
|
||||||
|
|
||||||
|
describe('base.css', () => {
|
||||||
|
it('sets font-size: 16px on input, textarea, select', () => {
|
||||||
|
expect(css).toMatch(/input\s*,\s*textarea\s*,\s*select\s*\{[^}]*font-size\s*:\s*16px/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('covers all three form control selectors in one rule', () => {
|
||||||
|
expect(css).toMatch(/input/);
|
||||||
|
expect(css).toMatch(/textarea/);
|
||||||
|
expect(css).toMatch(/select/);
|
||||||
|
});
|
||||||
|
});
|
||||||
133
tests/tokens.test.ts
Normal file
133
tests/tokens.test.ts
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { readFileSync } from 'fs';
|
||||||
|
|
||||||
|
const css = readFileSync(new URL('../dist/tokens.css', import.meta.url), 'utf-8');
|
||||||
|
|
||||||
|
// ---- CSS parser helpers ----
|
||||||
|
|
||||||
|
function extractRootBlock(src: string): string {
|
||||||
|
const m = src.match(/:root\s*\{([^}]+)\}/);
|
||||||
|
return m?.[1] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractDarkBlock(src: string): string {
|
||||||
|
const afterMedia = src.split('@media (prefers-color-scheme: dark)')[1] ?? '';
|
||||||
|
const m = afterMedia.match(/:root\s*\{([^}]+)\}/);
|
||||||
|
return m?.[1] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseProps(block: string): Map<string, string> {
|
||||||
|
const map = new Map<string, string>();
|
||||||
|
const re = /--([\w-]+)\s*:\s*([^;]+);/g;
|
||||||
|
let m: RegExpExecArray | null;
|
||||||
|
while ((m = re.exec(block)) !== null) {
|
||||||
|
map.set(`--${m[1]}`, m[2].trim());
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveToken(value: string, props: Map<string, string>): string {
|
||||||
|
const varRe = /var\(--([\w-]+)\)/;
|
||||||
|
let v = value;
|
||||||
|
for (let i = 0; i < 10 && varRe.test(v); i++) {
|
||||||
|
v = v.replace(varRe, (_, name) => props.get(`--${name}`) ?? 'unresolved');
|
||||||
|
}
|
||||||
|
return v.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- WCAG contrast helpers ----
|
||||||
|
|
||||||
|
function expandHex(hex: string): string {
|
||||||
|
const h = hex.replace('#', '');
|
||||||
|
return h.length === 3
|
||||||
|
? `#${h[0]}${h[0]}${h[1]}${h[1]}${h[2]}${h[2]}`
|
||||||
|
: `#${h}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function luminance(hex: string): number {
|
||||||
|
const h = expandHex(hex).replace('#', '');
|
||||||
|
const r = parseInt(h.slice(0, 2), 16) / 255;
|
||||||
|
const g = parseInt(h.slice(2, 4), 16) / 255;
|
||||||
|
const b = parseInt(h.slice(4, 6), 16) / 255;
|
||||||
|
const lin = (c: number) =>
|
||||||
|
c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
|
||||||
|
return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
function contrast(hex1: string, hex2: string): number {
|
||||||
|
const l1 = luminance(hex1);
|
||||||
|
const l2 = luminance(hex2);
|
||||||
|
const [lighter, darker] = l1 > l2 ? [l1, l2] : [l2, l1];
|
||||||
|
return (lighter + 0.05) / (darker + 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Fixtures ----
|
||||||
|
|
||||||
|
const rootProps = parseProps(extractRootBlock(css));
|
||||||
|
const darkProps = parseProps(extractDarkBlock(css));
|
||||||
|
// Merged: dark overrides on top of light primitives so var() refs resolve correctly.
|
||||||
|
const mergedDark = new Map([...rootProps, ...darkProps]);
|
||||||
|
|
||||||
|
// Tokens that must be explicitly defined in both light and dark blocks.
|
||||||
|
const DARK_MODE_TOKENS = [
|
||||||
|
'--bg', '--bg-elevated', '--surface',
|
||||||
|
'--fg', '--fg-muted',
|
||||||
|
'--border-color', '--border-strong',
|
||||||
|
'--accent', '--accent-fg', '--accent-hover',
|
||||||
|
'--danger', '--danger-fg', '--danger-bg',
|
||||||
|
'--input-bg', '--input-fg', '--input-border', '--input-bg-focus',
|
||||||
|
];
|
||||||
|
|
||||||
|
// ---- Tests ----
|
||||||
|
|
||||||
|
describe('tokens.css — token presence', () => {
|
||||||
|
it('defines all dark-mode tokens in :root (light)', () => {
|
||||||
|
for (const token of DARK_MODE_TOKENS) {
|
||||||
|
expect(rootProps.has(token), `${token} missing from :root`).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('defines all dark-mode tokens in @media (prefers-color-scheme: dark)', () => {
|
||||||
|
for (const token of DARK_MODE_TOKENS) {
|
||||||
|
expect(darkProps.has(token), `${token} missing from @media dark`).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('tokens.css — WCAG AA contrast (light)', () => {
|
||||||
|
const fg = resolveToken(rootProps.get('--fg')!, rootProps);
|
||||||
|
const fgMuted = resolveToken(rootProps.get('--fg-muted')!, rootProps);
|
||||||
|
const bg = resolveToken(rootProps.get('--bg')!, rootProps);
|
||||||
|
const borderStrong = resolveToken(rootProps.get('--border-strong')!, rootProps);
|
||||||
|
|
||||||
|
it('--fg on --bg >= 4.5:1', () => {
|
||||||
|
expect(contrast(fg, bg)).toBeGreaterThanOrEqual(4.5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('--fg-muted on --bg >= 4.5:1', () => {
|
||||||
|
expect(contrast(fgMuted, bg)).toBeGreaterThanOrEqual(4.5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('--border-strong on --bg >= 3.0:1 (UI contrast)', () => {
|
||||||
|
expect(contrast(borderStrong, bg)).toBeGreaterThanOrEqual(3.0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('tokens.css — WCAG AA contrast (dark)', () => {
|
||||||
|
const fg = resolveToken(mergedDark.get('--fg')!, mergedDark);
|
||||||
|
const fgMuted = resolveToken(mergedDark.get('--fg-muted')!, mergedDark);
|
||||||
|
const bg = resolveToken(mergedDark.get('--bg')!, mergedDark);
|
||||||
|
const borderStrong = resolveToken(mergedDark.get('--border-strong')!, mergedDark);
|
||||||
|
|
||||||
|
it('--fg on --bg >= 4.5:1', () => {
|
||||||
|
expect(contrast(fg, bg)).toBeGreaterThanOrEqual(4.5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('--fg-muted on --bg >= 4.5:1', () => {
|
||||||
|
expect(contrast(fgMuted, bg)).toBeGreaterThanOrEqual(4.5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('--border-strong on --bg >= 3.0:1 (UI contrast)', () => {
|
||||||
|
expect(contrast(borderStrong, bg)).toBeGreaterThanOrEqual(3.0);
|
||||||
|
});
|
||||||
|
});
|
||||||
9
tsconfig.json
Normal file
9
tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"target": "ESNext",
|
||||||
|
"strict": true,
|
||||||
|
"types": ["vitest/globals"]
|
||||||
|
}
|
||||||
|
}
|
||||||
7
vitest.config.ts
Normal file
7
vitest.config.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { defineConfig } from 'vitest/config';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
environment: 'node',
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user