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
6 changes: 6 additions & 0 deletions .github/workflows/python-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,12 @@ jobs:
npm ci --ignore-scripts
npm ci --ignore-scripts --prefix apps/presentation/dashboard

- name: Check the CSS custom-property classifier contract
run: node scripts/check-css-custom-properties.test.mjs

- name: Reject undefined CSS custom properties
run: node scripts/check-css-custom-properties.mjs dashboard

- name: Measure Dashboard unit and browser interactions
env:
LOOPX_DASHBOARD_COVERAGE: "1"
Expand Down
5 changes: 3 additions & 2 deletions apps/presentation/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"build": "tsc --noEmit && vite build && npm run build:chat",
"build:chat": "node ../../../scripts/chat_bundle_launcher.mjs build",
"build:desktop": "tsc --noEmit && vite build",
"check:css-custom-properties": "node ../../../scripts/check-css-custom-properties.test.mjs && node ../../../scripts/check-css-custom-properties.mjs dashboard",
"dev": "bash ../../../scripts/dashboard-dev.sh",
"dev:web": "vite --host 127.0.0.1",
"export:frontstage-share": "node ../../../examples/export-frontstage-share-bundle.mjs",
Expand All @@ -35,9 +36,9 @@
"smoke:home-route": "rm -rf /tmp/loopx-home-route-smoke && tsc --ignoreConfig --target ES2022 --module CommonJS --moduleResolution Node --ignoreDeprecations 6.0 --skipLibCheck --strict --outDir /tmp/loopx-home-route-smoke smoke/home-route-smoke.ts && node /tmp/loopx-home-route-smoke/home-route-smoke.js",
"smoke:delegation-preflight": "rm -rf node_modules/.cache/loopx-delegation-preflight-smoke && tsc --ignoreConfig --target ES2022 --module NodeNext --moduleResolution NodeNext --jsx react-jsx --skipLibCheck --strict --rootDir . --outDir node_modules/.cache/loopx-delegation-preflight-smoke smoke/delegation-preflight-smoke.tsx src/data/delegation-preflight.ts src/features/personal-workspace/delegation-preflight-status.tsx && node node_modules/.cache/loopx-delegation-preflight-smoke/smoke/delegation-preflight-smoke.js",
"smoke:delegation-preflight-browser": "node --experimental-strip-types smoke/delegation-preflight-browser-smoke.mjs",
"smoke:personal-workspace": "npm run smoke:goal-order && npm run smoke:goal-activity && npm run smoke:proposal-recency && npm run smoke:delegation-preflight && node src/features/personal-workspace/workspace-theme.test.mjs && node src/features/personal-workspace/personal-workspace-contract.test.mjs && node ../../../examples/personal-workspace-browser-smoke.mjs",
"smoke:personal-workspace": "npm run smoke:goal-order && npm run smoke:goal-activity && npm run smoke:proposal-recency && npm run smoke:delegation-preflight && node src/features/personal-workspace/workspace-theme.test.mjs && node src/features/personal-workspace/font-token.test.mjs && node src/features/personal-workspace/personal-workspace-contract.test.mjs && node ../../../examples/personal-workspace-browser-smoke.mjs",
"smoke:workspace-locale": "LOOPX_PERSONAL_WORKSPACE_SCENARIO=workspace-locale node ../../../examples/personal-workspace-browser-smoke.mjs",
"smoke:personal-workspace-packaged": "npm run smoke:goal-order && npm run smoke:goal-activity && npm run smoke:proposal-recency && npm run smoke:delegation-preflight && node src/features/personal-workspace/workspace-theme.test.mjs && node src/features/personal-workspace/personal-workspace-contract.test.mjs && LOOPX_PERSONAL_WORKSPACE_PACKAGED=1 LOOPX_PLAYWRIGHT_PACKAGE=\"$PWD/node_modules/playwright\" node ../../../examples/personal-workspace-browser-smoke.mjs",
"smoke:personal-workspace-packaged": "npm run smoke:goal-order && npm run smoke:goal-activity && npm run smoke:proposal-recency && npm run smoke:delegation-preflight && node src/features/personal-workspace/workspace-theme.test.mjs && node src/features/personal-workspace/font-token.test.mjs && node src/features/personal-workspace/personal-workspace-contract.test.mjs && LOOPX_PERSONAL_WORKSPACE_PACKAGED=1 LOOPX_PLAYWRIGHT_PACKAGE=\"$PWD/node_modules/playwright\" node ../../../examples/personal-workspace-browser-smoke.mjs",
"smoke:todo-resume-condition": "rm -rf /tmp/loopx-todo-resume-condition-smoke && tsc --ignoreConfig --target ES2022 --module commonjs --moduleResolution node --ignoreDeprecations 6.0 --skipLibCheck --strict --outDir /tmp/loopx-todo-resume-condition-smoke smoke/todo-resume-condition-smoke.ts src/features/personal-workspace/todo-resume-condition.ts && node /tmp/loopx-todo-resume-condition-smoke/smoke/todo-resume-condition-smoke.js",
"smoke:presentation-surface-schema": "rm -rf /tmp/loopx-presentation-surface-schema-smoke && tsc --ignoreConfig --target ES2022 --module CommonJS --moduleResolution Node --ignoreDeprecations 6.0 --skipLibCheck --strict --resolveJsonModule --esModuleInterop --outDir /tmp/loopx-presentation-surface-schema-smoke smoke/presentation-surface-schema-smoke.ts src/data/status.ts src/data/decision-research.ts src/data/goal-channel-frontstage.ts && NODE_PATH=\"$PWD/node_modules\" node /tmp/loopx-presentation-surface-schema-smoke/apps/presentation/dashboard/smoke/presentation-surface-schema-smoke.js",
"smoke:projection-localization": "rm -rf /tmp/loopx-projection-localization-smoke && tsc --ignoreConfig --target ES2022 --module NodeNext --moduleResolution NodeNext --skipLibCheck --strict --outDir /tmp/loopx-projection-localization-smoke smoke/projection-localization-smoke.ts src/features/personal-workspace/projection-localization.ts && node /tmp/loopx-projection-localization-smoke/smoke/projection-localization-smoke.js",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";

/**
* Contract for the font and monospace tokens the Dashboard stylesheets reference.
*
* `docs/development/design.md` names `--font-sans` and `--font-mono` as the
* canonical families, so stylesheets reference them instead of repeating a
* stack. That only works while both tokens are actually defined: a bare
* `var(--font-mono)` with no fallback makes the browser drop the whole
* declaration, and the element silently inherits the body face. Code blocks and
* monospace metadata then render as sans-serif with nothing to notice.
*
* `scripts/check-css-custom-properties.mjs` guards the general case (any bare
* reference to any undefined token). This test pins the specific decision: what
* the two font tokens are defined as, and that the sites that regressed keep
* resolving through them rather than through a locally repeated stack.
*/

const dashboardStyles = readFileSync(new URL("../../styles.css", import.meta.url), "utf8");
const workspaceStyles = readFileSync(new URL("./personal-workspace.css", import.meta.url), "utf8");
const loopxModeStyles = readFileSync(new URL("./goal-loopx-mode.css", import.meta.url), "utf8");
const collaborationStyles = readFileSync(new URL("./collaboration-card.css", import.meta.url), "utf8");
const deliveryStyles = readFileSync(new URL("./delivery-review.css", import.meta.url), "utf8");

// The two font tokens exist, and the body face is expressed through the sans token.
assert.match(
dashboardStyles,
/--font-sans:\s*\n?\s*"Geist Variable", "Geist", Inter,/,
"The sans token is defined from the Geist Variable family",
);
assert.match(
dashboardStyles,
/--font-mono: "Geist Mono Variable", "Geist Mono", ui-monospace,/,
"The mono token is defined from the Geist Mono family with platform fallbacks",
);
assert.match(
dashboardStyles,
/font-family: var\(--font-sans\)/,
"The document body face resolves through the sans token",
);

// Sites that previously regressed must reference the token, not a repeated stack.
assert.ok(
deliveryStyles.includes(".delivery-acceptance-content code { font-family: var(--font-mono);"),
"Delivery review code resolves through the mono token",
);
assert.ok(
collaborationStyles.includes(".personal-collaboration small { display: block; font-family: var(--font-mono);"),
"Collaboration card metadata resolves through the mono token",
);
assert.ok(
workspaceStyles.includes(".personal-operator-credential-status { color: var(--pw-muted); font-family: var(--font-mono);"),
"Operator credential status resolves through the mono token, not a misspelled private one",
);

// Where a fallback is given, it must be the same family — a bare `monospace`
// keyword would quietly render a different face from every other code surface.
const fallbacks = [...loopxModeStyles.matchAll(/var\(--font-mono,\s*([^)]*)\)/g)].map((match) => match[1]);
assert.ok(fallbacks.length > 0, "Goal LoopX mode keeps an explicit mono fallback");
for (const fallback of fallbacks) {
assert.match(
fallback,
/"Geist Mono Variable"/,
`The mono fallback names the Geist Mono family rather than a bare keyword: ${fallback}`,
);
}

// The private token names that had *bare* references and no definition must not
// come back. `--pw-surface` is deliberately not listed: it stays valid as an
// optional token because `.personal-manager-team-result` supplies a fallback
// (`var(--pw-surface, #fff)`), so the general check — which only rejects bare
// references — is the right guard for it.
//
// Match on a token boundary so `--pw-surface-soft`, a different real token,
// cannot be mistaken for a dead name.
const escapeForRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
for (const [name, source] of [
["--pw-font-mono", workspaceStyles],
["--pw-danger", workspaceStyles],
["--pw-border", workspaceStyles],
["--pw-canvas", workspaceStyles],
]) {
const pattern = new RegExp(`${escapeForRegExp(name)}(?![A-Za-z0-9_-])`);
assert.ok(
!pattern.test(source),
`${name} was never defined anywhere; use the existing token instead of reintroducing it`,
);
}

// The bare `var(--pw-surface)` in the operator credential was the regression;
// the optional use with a fallback is legitimate and must stay.
assert.ok(
!workspaceStyles.includes("background: var(--pw-surface);"),
"The operator credential reads the card token rather than the optional surface token",
);

console.log("font-token contract: ok");
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
.goal-team-evidence { margin-block: 16px; padding: 16px; border: 1px solid var(--pw-line, #ebebeb); border-radius: 12px; background: var(--pw-surface, #fff); }
.goal-team-evidence h3, .goal-team-evidence h4 { margin: 0; font-size: 13px; }
.goal-team-evidence article { margin-block: 16px; }
.goal-team-evidence pre { max-height: 320px; overflow: auto; white-space: pre-wrap; overflow-wrap: anywhere; font: 12px/1.6 var(--font-mono, monospace); padding: 12px; border: 1px solid var(--pw-line, #ebebeb); border-radius: 6px; }
.goal-team-evidence pre { max-height: 320px; overflow: auto; white-space: pre-wrap; overflow-wrap: anywhere; font: 12px/1.6 var(--font-mono, "Geist Mono Variable", "Geist Mono", ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace); padding: 12px; border: 1px solid var(--pw-line, #ebebeb); border-radius: 6px; }
.goal-team-evidence label { display: grid; gap: 8px; font-weight: 500; }
.goal-team-evidence textarea { box-sizing: border-box; resize: vertical; min-height: 88px; width: 100%; padding: 8px; border: 1px solid var(--pw-line, #ebebeb); border-radius: 6px; background: var(--pw-bg, #fafafa); color: inherit; font: inherit; }
.goal-team-evidence textarea:focus-visible, .goal-team-evidence pre:focus-visible { outline: 2px solid #0070f3; outline-offset: 2px; }
Expand Down Expand Up @@ -107,7 +107,7 @@
.goal-team-results summary { cursor: pointer; padding: 12px 0; font-size: 12px; min-height: 44px; }
.goal-team-results code { overflow-wrap: anywhere; }
.goal-team-report table code { white-space: pre; overflow-wrap: normal; word-break: normal; }
.goal-team-results pre { padding: 16px; max-height: 400px; overflow: auto; white-space: pre-wrap; overflow-wrap: anywhere; font: 13px/1.7 var(--font-mono, monospace); background: var(--pw-bg); }
.goal-team-results pre { padding: 16px; max-height: 400px; overflow: auto; white-space: pre-wrap; overflow-wrap: anywhere; font: 13px/1.7 var(--font-mono, "Geist Mono Variable", "Geist Mono", ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace); background: var(--pw-bg); }
.goal-team-results h3 { font-size: 20px; letter-spacing: -.02em; }
.goal-team-results > header > button { display: inline-flex; align-items: center; gap: 8px; }
.goal-team-results-layout { display: grid; grid-template-columns: minmax(150px, 190px) minmax(0, 1fr); margin-top: 24px; gap: 24px; }
Expand All @@ -116,7 +116,7 @@
.goal-team-result-list button > svg { flex-shrink: 0; margin-top: 2px; }
.goal-team-result-list button > span { min-width: 0; display: grid; gap: 4px; }
.goal-team-result-list button strong { font-size: 13px; font-weight: 500; }
.goal-team-result-list button small { color: var(--pw-muted, #666); font: 11px/1.5 var(--font-mono, monospace); }
.goal-team-result-list button small { color: var(--pw-muted, #666); font: 11px/1.5 var(--font-mono, "Geist Mono Variable", "Geist Mono", ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace); }
.goal-team-result-check { margin-left: auto; visibility: hidden; }
.goal-team-result-list button[aria-pressed="true"] .goal-team-result-check { visibility: visible; }
.goal-team-result-reader { min-width: 0; border-left: 1px solid var(--pw-line); padding-left: 24px; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@
.personal-goal-lifecycle:disabled { opacity: .45; cursor: wait; }
.personal-goal-lifecycle.is-pending svg { animation: personal-goal-lifecycle-spin .8s linear infinite; }
@keyframes personal-goal-lifecycle-spin { to { transform: rotate(360deg); } }
.personal-goal-delete:hover { color: var(--pw-danger); }
.personal-goal-delete:hover { color: var(--pw-red); }
.personal-goal-link-copy { display: grid; min-width: 0; gap: 2px; }
.personal-goal-link-copy strong, .personal-goal-link-copy small { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
/* Two lines keep the distinguishing tail of similar Goal titles visible. */
Expand Down Expand Up @@ -270,7 +270,7 @@ button.personal-execution-chip:focus-visible { outline: 2px solid #0070f3; outli
.personal-action-feedback button { display: grid; place-items: center; border: 0; background: transparent; color: inherit; cursor: pointer; }
.personal-refresh-control { display: inline-flex; flex: 0 0 auto; align-items: center; gap: 6px; }
.personal-refresh-control small { color: var(--pw-muted); font-size: 11px; white-space: nowrap; }
.personal-refresh-control.is-error small { color: var(--pw-danger); }
.personal-refresh-control.is-error small { color: var(--pw-red); }
.personal-icon-button { display: inline-grid; flex: 0 0 36px; place-items: center; width: 36px; min-width: 36px; height: 36px; min-height: 36px; padding: 0; border: 1px solid var(--pw-line); border-radius: 10px; background: #fff; color: var(--pw-muted); cursor: pointer; }
.personal-icon-button:hover { border-color: var(--pw-line-strong); color: var(--pw-text); }
.personal-channel-scroll { overflow: auto; min-width: 0; min-height: 0; padding: 22px max(26px, calc((100% - 820px) / 2)); }
Expand Down Expand Up @@ -832,17 +832,17 @@ button.personal-execution-chip:focus-visible { outline: 2px solid #0070f3; outli
/* The operator credential is a write-only machine setting: the panel keeps the
redacted readback and the two inputs on one hairline surface so an operator
can compare "what is configured" with "what I am about to store". */
.personal-operator-credential { display: grid; gap: 12px; margin: 16px 0 24px; padding: 16px; border: 1px solid var(--pw-border); border-radius: 10px; background: var(--pw-surface); }
.personal-operator-credential { display: grid; gap: 12px; margin: 16px 0 24px; padding: 16px; border: 1px solid var(--pw-line-strong); border-radius: 10px; background: var(--pw-card); }
.personal-operator-credential > header { display: grid; grid-template-columns: auto 1fr auto; gap: 10px; align-items: start; }
.personal-operator-credential > header strong { display: block; font-size: 13px; }
.personal-operator-credential > header p { margin: 4px 0 0; color: var(--pw-muted); font-size: 12px; line-height: 1.5; }
.personal-operator-credential-status { color: var(--pw-muted); font-family: var(--pw-font-mono); font-size: 11px; }
.personal-operator-credential-status { color: var(--pw-muted); font-family: var(--font-mono); font-size: 11px; }
.personal-operator-credential-readback { display: grid; gap: 6px; margin: 0; font-size: 12px; }
.personal-operator-credential-readback > div { display: grid; grid-template-columns: 140px 1fr; gap: 10px; }
.personal-operator-credential-readback dt { color: var(--pw-muted); }
.personal-operator-credential-readback dd { margin: 0; overflow-wrap: anywhere; }
.personal-operator-credential label { display: grid; gap: 4px; font-size: 12px; }
.personal-operator-credential input { padding: 7px 9px; border: 1px solid var(--pw-border); border-radius: 8px; background: var(--pw-canvas); color: inherit; font-size: 12px; }
.personal-operator-credential input { padding: 7px 9px; border: 1px solid var(--pw-line-strong); border-radius: 8px; background: var(--pw-bg); color: inherit; font-size: 12px; }
.personal-operator-credential input:disabled { opacity: 0.6; }
.personal-capability-scope-note p { margin: 0; font-size: 12px; line-height: 1.55; }
.personal-capability-scope-note strong { display: block; color: var(--pw-text); }
Expand Down
16 changes: 15 additions & 1 deletion apps/presentation/dashboard/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,25 @@

@custom-variant dark (&:where(.dark, .dark *));

/*
* Font tokens. `docs/development/design.md` names these two variables as the
* canonical families, so a stylesheet can reference them instead of repeating
* a stack. Both are declared here because a bare `var(--font-mono)` with no
* fallback drops the whole declaration when the token is missing, and the
* browser then falls back to the body face — which is how code blocks and
* monospace metadata silently rendered as sans-serif.
*
* `--font-mono` intentionally leads with the variable-font family name so it
* matches the `@fontsource-variable/geist-mono` import above, with the static
* and platform faces as fallbacks.
*/
:root {
color-scheme: light;
font-family:
--font-sans:
"Geist Variable", "Geist", Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
sans-serif;
--font-mono: "Geist Mono Variable", "Geist Mono", ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
font-family: var(--font-sans);
}

.dark {
Expand Down
23 changes: 21 additions & 2 deletions docs/development/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,13 +111,32 @@ Dark mode is an inverse of the same system, not a separate visual identity:
Use **Geist Sans** for UI and prose and **Geist Mono** for code, data, compact
technical labels, and section eyebrows.

Fallbacks:
Fallbacks are owned per surface, because each bundles its own font files. A
stylesheet must reference the token rather than repeat the stack, and the token
must be defined on the surface that uses it — a bare `var(--font-mono)` with no
fallback drops the whole declaration when the token is missing, and the element
silently inherits the body face instead of failing visibly.

Dashboard (`apps/presentation/dashboard/src/styles.css`):

```css
--font-sans:
"Geist Variable", "Geist", Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
sans-serif;
--font-mono: "Geist Mono Variable", "Geist Mono", ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
```

Marketing site (`apps/presentation/site/src/styles.css`):

```css
--font-sans: "Geist", "Inter", "Helvetica Neue", Arial, sans-serif;
--font-mono: "Geist Mono", "JetBrains Mono", "SFMono-Regular", monospace;
```

`scripts/check-css-custom-properties.mjs` rejects any bare reference to a custom
property that no stylesheet or inline style defines. Tokens that are genuinely
optional may keep a fallback (`var(--pw-surface, #fff)`); the check only rejects
references that would be dropped.

### Type Scale

| Token | Size / line height | Weight | Tracking | Use |
Expand Down
Loading
Loading