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
1 change: 1 addition & 0 deletions deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"docs:check-version-anchors": "deno run --allow-read tools/check-version-anchors.ts",
"docs:truth": "deno task docs:check-public && deno task docs:check-current && deno task docs:check-strategy && deno task docs:check-version-anchors && deno task release:evidence:check && deno task www:check-current-truth",
"www:check-current-truth": "deno run --allow-read tools/check-www-current-truth.ts",
"www:check-theme-tokens": "deno run --allow-read tools/check-www-theme-tokens.ts",
"www:check-artifact-truth": "deno run --allow-read tools/check-www-current-truth.ts --artifacts",
"package-surface:check": "deno run --allow-read --allow-env tools/check-package-surface.ts",
"interface:snapshot": "deno run --allow-read --allow-env tools/check-public-interface-snapshot.ts",
Expand Down
6 changes: 6 additions & 0 deletions tools/autoflow/policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,12 @@ export const GATES: readonly GateDefinition[] = [
tiers: ['ci', 'release'],
triggers: [/^docs\//, /^README/, /^www\/app\/routes\//],
},
{
name: 'www:check-theme-tokens',
command: ['deno', 'task', 'www:check-theme-tokens'],
tiers: ['dev', 'push', 'ci', 'release'],
triggers: [/^www\/app\//, /^www\/vite\.config\.ts$/, /^packages\/ui\/src\/open-props-tokens/],
},
{
name: 'docs:check-version-anchors',
command: ['deno', 'task', 'docs:check-version-anchors'],
Expand Down
52 changes: 52 additions & 0 deletions tools/check-www-theme-tokens.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { assertEquals } from '@std/assert';
import { findThemeTokenFailures } from './check-www-theme-tokens.ts';

Deno.test('theme-token gate catches hex, font-family and font-size literals', () => {
const lines = [
' .vinyl { background:#18151e; }',
' code { font-family: "JetBrains Mono", monospace; }',
' font-size: 12px;',
' font-size: .75rem;',
];
const failures = findThemeTokenFailures('x.tsx', lines);
assertEquals(failures.map((f) => f.rule), [
'hex-literal',
'font-family-literal',
'font-size-literal',
'font-size-literal',
]);
});

Deno.test('theme-token gate catches short hex only in CSS contexts', () => {
const violations = findThemeTokenFailures('x.tsx', [
'background: color-mix(in srgb, #fff 18%, transparent);',
]);
assertEquals(violations.length, 1);
const prose = findThemeTokenFailures('x.tsx', [
'External adopter #390 and continued browser evidence.',
'<strong>#390</strong>',
]);
assertEquals(prose.length, 0);
});

Deno.test('theme-token gate skips generated data files', () => {
const failures = findThemeTokenFailures('www/app/data/_generated-blog-data.ts', [
'"content": "themeColor: \'#000000\', font-size: 12px;"',
]);
// The caller-level exclusion (path filter) keeps generated content out;
// the pure function still flags it when asked directly.
assertEquals(failures.length > 0, true);
});

Deno.test('theme-token gate accepts tokens, inherit and fluid clamp()', () => {
const failures = findThemeTokenFailures('x.tsx', [
'color: var(--text-primary);',
'background: color-mix(in srgb, var(--violet-5) 18%, transparent);',
'font-family: var(--font-mono);',
'font-family: inherit;',
'font-size: var(--font-size-00);',
'font-size: clamp(4.1rem, 10vw, 10.5rem);',
'font-size: clamp(var(--font-size-7), 10vw, var(--font-size-8));',
]);
assertEquals(failures.length, 0);
});
81 changes: 81 additions & 0 deletions tools/check-www-theme-tokens.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/**
* www theme-token gate: theme values in the site must come from open-props
* tokens (packages/ui/src/open-props-tokens.css) and the www alias layer
* (www/vite.config.ts), never from hardcoded literals.
*
* Rules for sources under www/app/ and www/islands/:
* 1. No hex color literals. 6/8-digit forms always fail; 3/4-digit forms
* fail only on lines carrying a CSS property keyword, so issue
* references like `#390` in prose stay legal.
* 2. No `font-family` declarations that bypass var(); `inherit` is allowed.
* 3. No `font-size` literals in px/rem/em outside var(); clamp() fluid
* typography is allowed.
*
* Token definitions belong in www/vite.config.ts (site aliases) or
* packages/ui/src/open-props-tokens.css (source of truth).
*/

import { walk } from '@std/fs/walk';

const SCAN_ROOTS = ['www/app'];
const SOURCE = /\.(ts|tsx)$/;
const HEX_LONG = /#(?:[0-9a-fA-F]{6}|[0-9a-fA-F]{8})\b/;
const HEX_SHORT = /#(?:[0-9a-fA-F]{3,4})\b/;
const CSS_KEYWORD = /color|background|border|shadow|fill|stroke|gradient|outline/i;
const FONT_FAMILY = /font-family\s*:\s*([^;]+);/;
const FONT_SIZE_LITERAL = /font-size\s*:\s*[0-9.]+(?:px|rem|em)\b/;

export interface ThemeTokenFailure {
file: string;
line: number;
rule: string;
text: string;
}

export function findThemeTokenFailures(
file: string,
lines: string[],
): ThemeTokenFailure[] {
const failures: ThemeTokenFailure[] = [];
for (let i = 0; i < lines.length; i++) {
const text = lines[i];
if (HEX_LONG.test(text) || (HEX_SHORT.test(text) && CSS_KEYWORD.test(text))) {
failures.push({ file, line: i + 1, rule: 'hex-literal', text: text.trim() });
}
const family = FONT_FAMILY.exec(text);
if (family && !family[1].includes('var(') && !family[1].includes('inherit')) {
failures.push({ file, line: i + 1, rule: 'font-family-literal', text: text.trim() });
}
if (FONT_SIZE_LITERAL.test(text)) {
failures.push({ file, line: i + 1, rule: 'font-size-literal', text: text.trim() });
}
}
return failures;
}

async function main(): Promise<void> {
const failures: ThemeTokenFailure[] = [];
for (const root of SCAN_ROOTS) {
for await (const entry of walk(root, { exts: ['.ts', '.tsx'] })) {
if (!SOURCE.test(entry.path)) continue;
if (entry.path.includes('/data/_generated-')) continue;
const text = await Deno.readTextFile(entry.path);
failures.push(...findThemeTokenFailures(entry.path, text.split('\n')));
}
}
if (failures.length > 0) {
console.error('www theme token check failed:');
for (const failure of failures) {
console.error(`- ${failure.file}:${failure.line} [${failure.rule}] ${failure.text}`);
}
console.error(
'Theme values must come from open-props tokens or the www/vite.config.ts alias layer.',
);
Deno.exit(1);
}
console.log('www theme token check passed.');
}

if (import.meta.main) {
await main();
}
4 changes: 2 additions & 2 deletions www/app/routes/404.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,10 @@ styles.replaceSync(`
padding: 80px 20px 64px;
}
.title {
font-size: 64px;
font-size: var(--font-size-7);
}
.subtitle {
font-size: 20px;
font-size: var(--font-size-2);
}
}
`);
Expand Down
11 changes: 7 additions & 4 deletions www/app/routes/architecture/architecture.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export const tagName = 'engine-architecture';
import { OpenElement, StyleSheet } from '@openelement/element';
import '@openelement/ui/open-badge';
import '@openelement/ui/open-card';
import '@openelement/ui/open-code-block';
import { OPENELEMENT_VERSION } from '../../data/version.ts';
import '@openelement/site-ui/open-section-frame.tsx';
import '@openelement/site-ui/open-page-hero.tsx';
Expand All @@ -15,14 +16,14 @@ pageSheet.replaceSync(`
* { box-sizing:border-box; }
.eyebrow { display: flex; flex-wrap: wrap; gap: var(--size-2); margin-bottom: 20px; }
h1 { margin:0; max-width:760px; color:var(--text); font-size:clamp(3.5rem,7vw,7rem); line-height:.88; letter-spacing:-.07em; }
h2 { margin: 0; color: var(--text); font-size: 34px; line-height: 1.12; letter-spacing: 0; }
h2 { margin: 0; color: var(--text); font-size: var(--font-size-display-md); line-height: 1.12; letter-spacing: 0; }
h3 { margin: 0 0 var(--size-2); color: var(--text); }
p { color: var(--text-secondary); line-height: var(--line-height-relaxed); }
.lede { margin: 20px 0 0; font-size: var(--font-size-subhead); max-width: 650px; }
.artifact, .layer-map { border:1px solid color-mix(in srgb,var(--color-border) 72%,var(--brand)); border-radius:var(--radius-2); overflow:hidden; background:color-mix(in srgb,var(--surface-1) 82%,transparent); box-shadow:inset 0 1px 0 var(--edge-highlight),0 28px 90px color-mix(in srgb,var(--violet-10) 24%,transparent); backdrop-filter:blur(18px); }
.artifact-head { display: flex; justify-content: space-between; gap: var(--size-3); padding: 14px var(--size-4); border-bottom: 1px solid var(--color-border); font-size: var(--font-size-0); color: var(--text-muted); }
pre { margin: 0; padding: var(--size-4); overflow-x: auto; background: var(--code-bg); color: var(--code-text); font-size: var(--font-size-0); line-height: 1.65; }
code { font-family: "JetBrains Mono", monospace; }
code { font-family: var(--font-mono); }
.layer { display: grid; grid-template-columns: 170px 1fr 180px; gap: var(--size-4); padding: 14px var(--size-4); border-bottom: 1px solid var(--color-border); align-items: start; }
.layer:last-child { border-bottom: 0; }
.layer strong { color: var(--text); font-size: var(--font-size-1); }
Expand All @@ -33,7 +34,7 @@ pageSheet.replaceSync(`
.gate strong { color: var(--color-brand); font-size: var(--font-size-1); }
.gate span { color: var(--text-secondary); font-size: var(--font-size-0); line-height: 1.55; }
.nav-row { display:flex; flex-wrap:wrap; gap:10px; width:min(1180px,calc(100% - 4rem)); margin:var(--size-8) auto 0; }
@media (max-width: 900px) { .cards, .gate-grid { grid-template-columns: 1fr; } .layer { grid-template-columns: 1fr; gap: var(--size-2); } h1 { font-size: 42px; line-height: 1.06; } h2 { font-size: 28px; } }
@media (max-width: 900px) { .cards, .gate-grid { grid-template-columns: 1fr; } .layer { grid-template-columns: 1fr; gap: var(--size-2); } h1 { font-size: var(--font-size-display-lg); line-height: 1.06; } h2 { font-size: var(--font-size-display-sm); } }
@media (max-width: 560px) { .nav-row{width:calc(100% - 2rem)} .gate { grid-template-columns: 1fr; display: grid; } }
`);

Expand Down Expand Up @@ -62,7 +63,9 @@ export class ArchitecturePage extends OpenElement {
<open-artifact-panel slot='artifact'>
<span slot='label'>package graph</span>
<span slot='meta'>{OPENELEMENT_VERSION} published line</span>
<pre><code>{PACKAGE_GRAPH}</code></pre>
<open-code-block>
<pre><code>{PACKAGE_GRAPH}</code></pre>
</open-code-block>
</open-artifact-panel>
</open-page-hero>

Expand Down
2 changes: 1 addition & 1 deletion www/app/routes/architecture/islands-deep.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const routeSheet = new StyleSheet();
routeSheet.replaceSync(
pageStyles + `
.layer-card { padding: 20px var(--size-6); margin: var(--size-4) 0; border-left: 2px solid var(--color-border); background: var(--surface-1); border-radius: 0 3px 3px 0; }
.layer-card .layer-tag { font-size: 11px; font-weight: var(--font-weight-5); text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-muted); margin-bottom: 0.25rem; }
.layer-card .layer-tag { font-size: var(--font-size-overline); font-weight: var(--font-weight-5); text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-muted); margin-bottom: 0.25rem; }
.layer-card h3 { margin: 0 0 var(--size-2); }
.strategy-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--size-4); margin: var(--size-4) 0 var(--size-6); }
.strategy-item { padding: var(--size-4) 20px; border: 0.5px solid var(--color-border); border-radius: var(--radius-xs); background: var(--surface-1); }
Expand Down
4 changes: 2 additions & 2 deletions www/app/routes/blog/[slug].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ routeSheet.replaceSync(
.blog-tags { display: flex; gap: 0.375rem; flex-wrap: wrap; margin-bottom: var(--size-4); }
.blog-tag { font-size: var(--font-size-00); font-weight: var(--font-weight-6); text-transform: uppercase; letter-spacing: var(--font-letterspacing-2); padding: 0.125rem 0.375rem; border-radius: 2px; background: var(--bg-surface); border: 0.5px solid var(--border); color: var(--text-secondary); }
.blog-content { font-size: var(--font-size-3); line-height: var(--font-lineheight-4); color: var(--text-secondary); }
.blog-content h2 { margin-top: var(--size-10); color: var(--text-primary); font-size: 1.125rem; font-weight: var(--font-weight-6); }
.blog-content h2 { margin-top: var(--size-10); color: var(--text-primary); font-size: var(--font-size-article-title); font-weight: var(--font-weight-6); }
.blog-content h3 { margin-top: var(--size-8); color: var(--text-primary); font-size: var(--font-size-4); font-weight: var(--font-weight-6); }
.blog-content p { margin: var(--size-3) 0; }
.blog-content ul, .blog-content ol { padding-left: var(--size-6); margin: var(--size-3) 0; }
Expand All @@ -63,7 +63,7 @@ routeSheet.replaceSync(
.blog-content pre code { background: none; padding: 0; font-size: var(--font-size-0); line-height: 1.6; }
.blog-content table { width: 100%; border-collapse: collapse; margin: var(--size-4) 0; font-size: var(--font-size-1); }
.blog-content th, .blog-content td { padding: var(--size-2) var(--size-3); text-align: left; border-bottom: 0.5px solid var(--border); }
.blog-content th { background: var(--bg-surface); color: var(--text-secondary); font-weight: var(--font-weight-6); font-size: 0.6875rem; text-transform: uppercase; letter-spacing: var(--font-letterspacing-2); }
.blog-content th { background: var(--bg-surface); color: var(--text-secondary); font-weight: var(--font-weight-6); font-size: var(--font-size-overline); text-transform: uppercase; letter-spacing: var(--font-letterspacing-2); }
.blog-content a { color: var(--brand); text-decoration: none; }
.blog-content a:hover { text-decoration: underline; }
.blog-content hr { border: none; border-top: 0.5px solid var(--border); margin: var(--size-8) 0; }
Expand Down
Loading
Loading