Skip to content
Merged
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
3 changes: 2 additions & 1 deletion apps/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"preview": "vite preview",
"clean": "rm -rf dist",
"lint": "tsc --noEmit",
"lint:style": "node scripts/check-style-discipline.mjs",
"lint:react": "react-doctor",
"test": "vitest run",
"test:unit": "vitest run tests/services",
Expand All @@ -19,7 +20,7 @@
"test:e2e:headed": "playwright test --config=playwright.full.config.ts --headed --reporter=list",
"test:smoke": "playwright test --reporter=list",
"test:smoke:live-gemini": "playwright test tests/smoke/upload-phase2-live-gemini.spec.ts --reporter=list",
"verify": "npm run lint && npm run test:unit && npm run build && npm run test:smoke",
"verify": "npm run lint && npm run lint:style && npm run test:unit && npm run build && npm run test:smoke",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build -o storybook-static",
"chromatic": "chromatic --exit-zero-on-changes"
Expand Down
114 changes: 114 additions & 0 deletions apps/ui/scripts/check-style-discipline.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
#!/usr/bin/env node
/**
* check-style-discipline.mjs — design-token enforcement guard.
*
* Fails when source under an enforced directory uses:
* 1. Arbitrary Tailwind font sizes → text-[13px] / text-[0.8rem]
* Use the --text-* scale tokens from src/index.css instead:
* text-nano · text-micro · text-meta · text-eyebrow · text-body-sm
* text-body · text-value · text-value-lg · text-value-xl
* 2. Raw color hex literals → #ff8800 / #1a1a1a
* Use the semantic color tokens / Tailwind color utilities instead.
*
* Why this exists: a 2026-05-13 pass removed hardcoded hex, but it silently
* returned because nothing enforced the rule. This guard is that enforcement.
* ENFORCED_DIRS is widened directory-by-directory as the UI overhaul clears
* each area, so the guard can never regress what it has already cleaned.
*
* Out of scope (allowlisted):
* - *.stories.tsx — dev-only Storybook artifacts, not shipped UI.
* - Canvas / color-math modules that compute colors CSS classes can't express.
* - src/utils/colorScales.ts — the sanctioned home for centralized palettes.
* - DenseDawConcept.tsx — the off-system ?view=daw demo (excluded from overhaul).
*/
import { readdirSync, readFileSync, statSync } from 'node:fs';
import { join, basename, relative, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';

const here = dirname(fileURLToPath(import.meta.url));
const uiRoot = join(here, '..');
const srcRoot = join(uiRoot, 'src');

// Directories currently held to the discipline. Widen as overhaul phases land.
const ENFORCED_DIRS = [join(srcRoot, 'components', 'ui')];

// Basenames exempt anywhere within enforced dirs.
const ALLOWLIST = new Set([
'colorScales.ts',
'DenseDawConcept.tsx',
'RetroVisualizer.tsx',
'SpectrogramViewer.tsx',
'ChromaHeatmap.tsx',
'SpectralEvolutionChart.tsx',
'Sparkline.tsx',
'TranscriptionPianoroll.tsx',
'WaveformPlayer.tsx',
'PianoRollCanvas.tsx',
]);

const RULES = [
{
id: 'arbitrary-text-size',
re: /text-\[[0-9.]+(?:px|rem|em)\]/g,
hint: 'use a --text-* scale token (e.g. text-meta, text-eyebrow, text-body-sm)',
},
{
id: 'raw-hex-color',
re: /#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})\b/g,
hint: 'use a semantic color token (var(--color-*) or a Tailwind color utility)',
},
];

function walk(dir) {
const out = [];
for (const entry of readdirSync(dir)) {
const full = join(dir, entry);
if (statSync(full).isDirectory()) {
out.push(...walk(full));
} else if (
/\.(ts|tsx)$/.test(entry) &&
!/\.stories\.tsx$/.test(entry) &&
!/\.test\.tsx?$/.test(entry)
) {
out.push(full);
}
}
return out;
}

const violations = [];
for (const dir of ENFORCED_DIRS) {
for (const file of walk(dir)) {
if (ALLOWLIST.has(basename(file))) continue;
const lines = readFileSync(file, 'utf8').split('\n');
lines.forEach((line, i) => {
for (const rule of RULES) {
rule.re.lastIndex = 0;
let m;
while ((m = rule.re.exec(line)) !== null) {
violations.push({
file: relative(uiRoot, file),
line: i + 1,
match: m[0],
rule: rule.id,
hint: rule.hint,
});
}
}
});
}
}

if (violations.length > 0) {
console.error(`\n✗ style-discipline: ${violations.length} violation(s)\n`);
for (const v of violations) {
console.error(` ${v.file}:${v.line} ${v.match} [${v.rule}]`);
console.error(` → ${v.hint}`);
}
console.error('');
process.exit(1);
}

console.log(
'✓ style-discipline: no arbitrary text sizes or raw hex in enforced dirs',
);
6 changes: 3 additions & 3 deletions apps/ui/src/components/ui/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@ const buttonVariants = cva(
],
},
size: {
sm: 'px-2 py-1 text-[10px]',
md: 'px-3 py-1.5 text-[11px]',
lg: 'px-6 py-2.5 text-[12px]',
sm: 'px-2 py-1 text-meta',
md: 'px-3 py-1.5 text-eyebrow',
lg: 'px-6 py-2.5 text-body-sm',
},
iconOnly: {
true: '!gap-0',
Expand Down
4 changes: 2 additions & 2 deletions apps/ui/src/components/ui/Checkbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,12 +87,12 @@ export const Checkbox = React.forwardRef<
{box}
<span className="flex flex-col gap-0.5">
{label && (
<span className="font-mono text-[11px] uppercase tracking-[0.14em] text-text-primary">
<span className="font-mono text-eyebrow uppercase tracking-[0.14em] text-text-primary">
{label}
</span>
)}
{description && (
<span className="font-mono text-[10px] text-text-secondary leading-snug">
<span className="font-mono text-meta text-text-secondary leading-snug">
{description}
</span>
)}
Expand Down
2 changes: 1 addition & 1 deletion apps/ui/src/components/ui/DeviceRack.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export const DeviceRack = React.forwardRef<HTMLDivElement, DeviceRackProps>(
<LedIndicator status={statusToLedTone(resolvedStatus)} />
<span className="truncate">{name}</span>
{subtitle && (
<span className="text-text-secondary normal-case tracking-[0.04em] text-[10px] truncate">
<span className="text-text-secondary normal-case tracking-[0.04em] text-meta truncate">
{subtitle}
</span>
)}
Expand Down
4 changes: 2 additions & 2 deletions apps/ui/src/components/ui/EmptyState.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,12 @@ export const EmptyState = React.forwardRef<HTMLDivElement, EmptyStateProps>(
>
{icon && <div className="opacity-70">{icon}</div>}
{title && (
<p className="font-mono text-[11px] uppercase tracking-[0.16em] text-text-primary">
<p className="font-mono text-eyebrow uppercase tracking-[0.16em] text-text-primary">
{title}
</p>
)}
{description && (
<p className="font-mono text-[11px] leading-snug text-text-secondary max-w-prose">
<p className="font-mono text-eyebrow leading-snug text-text-secondary max-w-prose">
{description}
</p>
)}
Expand Down
2 changes: 1 addition & 1 deletion apps/ui/src/components/ui/LedIndicator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export const LedIndicator = React.forwardRef<HTMLSpanElement, LedIndicatorProps>
{...rest}
>
<span className={ledVariants({ status, size })} aria-hidden />
<span className="font-mono text-[10px] uppercase tracking-[0.16em] text-text-secondary">
<span className="font-mono text-meta uppercase tracking-[0.16em] text-text-secondary">
{label}
</span>
</span>
Expand Down
2 changes: 1 addition & 1 deletion apps/ui/src/components/ui/MetricBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ export const MetricBar = React.forwardRef<HTMLDivElement, MetricBarProps>(functi
/>
</div>
{(leftLabel || rightLabel) && (
<div className="flex items-center justify-between text-[8px] font-mono text-text-secondary/50">
<div className="flex items-center justify-between text-nano font-mono text-text-secondary/50">
<span>{leftLabel ?? ''}</span>
<span>{rightLabel ?? ''}</span>
</div>
Expand Down
8 changes: 4 additions & 4 deletions apps/ui/src/components/ui/MetricTile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ const valueSizeClass: Record<NonNullable<MetricTileProps['size']>, string> = {
};

const unitSizeClass: Record<NonNullable<MetricTileProps['size']>, string> = {
sm: 'text-[9px]',
md: 'text-[10px]',
lg: 'text-[11px]',
sm: 'text-micro',
md: 'text-meta',
lg: 'text-eyebrow',
xl: 'text-xs',
};

Expand Down Expand Up @@ -77,7 +77,7 @@ export const MetricTile = React.forwardRef<HTMLDivElement, MetricTileProps>(
{icon && <span className="text-text-secondary shrink-0">{icon}</span>}
<span
data-text-role="eyebrow"
className="font-mono text-[10px] uppercase tracking-[0.16em] text-text-secondary truncate"
className="font-mono text-meta uppercase tracking-[0.16em] text-text-secondary truncate"
>
{label}
</span>
Expand Down
4 changes: 2 additions & 2 deletions apps/ui/src/components/ui/Pill.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ const pillVariants = cva(
ghost: 'border-transparent',
},
size: {
xs: 'px-1.5 py-0.5 text-[9px]',
sm: 'px-2 py-0.5 text-[10px]',
xs: 'px-1.5 py-0.5 text-micro',
sm: 'px-2 py-0.5 text-meta',
},
},
compoundVariants: [
Expand Down
2 changes: 1 addition & 1 deletion apps/ui/src/components/ui/SignalChain.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ export const SignalChain = React.forwardRef<HTMLDivElement, SignalChainProps>(
{stage.statusLabel && (
<span
className={cn(
'font-mono text-[10px] uppercase tracking-[0.16em] tabular-nums',
'font-mono text-meta uppercase tracking-[0.16em] tabular-nums',
stage.status === 'success' && 'text-success',
stage.status === 'active' && 'text-accent',
stage.status === 'queued' && 'text-text-muted',
Expand Down
4 changes: 2 additions & 2 deletions apps/ui/src/components/ui/TimeReadout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,14 @@ export const TimeReadout = React.forwardRef<HTMLSpanElement, TimeReadoutProps>(
<span
ref={ref}
className={cn(
'tabular-mono inline-flex items-baseline gap-1.5 text-[11px] text-text-primary',
'tabular-mono inline-flex items-baseline gap-1.5 text-eyebrow text-text-primary',
className,
)}
{...rest}
>
<span>{elapsedLabel}</span>
{hasEstimate && (
<span className="text-text-muted text-[9px] uppercase tracking-[0.14em]">
<span className="text-text-muted text-micro uppercase tracking-[0.14em]">
est {estimateLabel}
</span>
)}
Expand Down
2 changes: 1 addition & 1 deletion apps/ui/src/components/ui/Tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export function Tooltip({
sideOffset={6}
className={cn(
'z-50 max-w-xs px-3 py-2 rounded-sm border border-border bg-bg-card shadow-md',
'font-mono text-[11px] text-text-primary leading-snug',
'font-mono text-eyebrow text-text-primary leading-snug',
'animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0',
className,
)}
Expand Down
6 changes: 5 additions & 1 deletion apps/ui/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,11 @@
--space-title-strip-h: 1.5rem;
--space-rail-h: 0.875rem;

/* --- Type-scale lock (bans arbitrary text-[Npx] in components/ui/) --- */
/* --- Type-scale lock: the ONLY sanctioned font sizes. Arbitrary
text-[Npx] under src/components/** is banned by
scripts/check-style-discipline.mjs. --- */
--text-nano: 0.5rem; /* 8px — dense meter / axis labels */
--text-micro: 0.5625rem; /* 9px — compact eyebrows, pill xs */
--text-meta: 0.625rem;
--text-eyebrow: 0.6875rem;
--text-body-sm: 0.75rem;
Expand Down
Loading