diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..52a6047 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,68 @@ +# Runs the test suite on every pull request and on pushes to master. +# +# Two jobs, split by cost. Roughly a third of the suite drives a real browser, +# and downloading Chrome dominates that job's runtime — so the Node-only tests +# run on their own and report in a couple of minutes, which is the feedback +# that matters for almost every change. See docs/specs/CI-SPEC.md. +# +# No secrets are needed. The tests that would use an LLM API key skip when one +# is absent, so this stays useful on pull requests from forks. +name: CI + +on: + pull_request: + push: + branches: [master] + +# A new push supersedes the previous run on the same branch. +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + fast: + name: build + tests (node ${{ matrix.node }}) + runs-on: ubuntu-latest + timeout-minutes: 15 + strategy: + fail-fast: false + matrix: + # The lower bound is what package.json's `engines` field claims, so the + # claim stays honest; the upper is what development actually runs on. + node: ['22', '24'] + env: + # This job never launches a browser, and Chrome is ~150MB of the install. + PUPPETEER_SKIP_DOWNLOAD: 'true' + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node }} + cache: npm + - run: npm ci + # `tsc` with noEmit off is the type check — it is the same compile the + # published package depends on, so a separate lint step would be noise. + - run: npm run build + - run: npm run test:fast + + browser: + name: browser tests + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '24' + cache: npm + # `puppeteer` (not puppeteer-core) is a dependency, so its postinstall + # fetches the pinned Chrome build here. Cached across runs by version, so + # only a Puppeteer bump pays the download again. + - name: Cache Puppeteer's Chrome + uses: actions/cache@v4 + with: + path: ~/.cache/puppeteer + key: puppeteer-${{ runner.os }}-${{ hashFiles('package-lock.json') }} + - run: npm ci + - run: npm run build + - run: npm run test:full diff --git a/README.md b/README.md index fbe6387..4658bba 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # framesmith -[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Release](https://img.shields.io/github/v/release/vicmaster/framesmith)](https://github.com/vicmaster/framesmith/releases) [![MCP](https://img.shields.io/badge/MCP-compatible-1f4838)](https://modelcontextprotocol.io) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Release](https://img.shields.io/github/v/release/vicmaster/framesmith)](https://github.com/vicmaster/framesmith/releases) [![MCP](https://img.shields.io/badge/MCP-compatible-1f4838)](https://modelcontextprotocol.io) [![CI](https://github.com/vicmaster/framesmith/actions/workflows/ci.yml/badge.svg)](https://github.com/vicmaster/framesmith/actions/workflows/ci.yml) An open-source MCP server that turns your AI coding agent into a capable UI designer. It gives the agent a visual canvas, a library of vetted design patterns, and a quality bar it must clear before showing you anything — so you review a real, non-slop design in the browser and agree on it *before* any framework code is written. diff --git a/VISION.md b/VISION.md index 15a21a7..a8c73af 100644 --- a/VISION.md +++ b/VISION.md @@ -543,10 +543,10 @@ _Full spec-driven breakdown in [`docs/specs/PHASE-29-SPEC.md`](docs/specs/PHASE- ### Continuous integration (infrastructure) -Nothing runs on a pull request today: no workflow, no branch protection, no checks. That held because a single disciplined maintainer ran things by hand, but it has already failed silently — the repository has 102 test files and no `test` script to run them as a suite, and two tests were found failing at the v2.0.0 tag itself. The missing runner is the root cause; a pipeline that is red on its first run only teaches you to ignore the red X. +Nothing runs on a pull request today: no workflow, no branch protection, no checks. That held because a single disciplined maintainer ran things by hand, but it has already failed silently — the repository has 102 test files and no `test` script to run them as a suite, and two tests were found failing at the v2.0.0 tag itself. The missing runner is the root cause; a pipeline that is red on its first run only teaches you to ignore the red X. Running the suite together for the first time turned up three failing tests, two of them red at the v2.0.0 tag itself; all three are fixed. -- [ ] Slice A — a tiered runner (`test:fast` / `test:full`), the interactive viewer test excluded by name, `test-cli` moved to the Chrome tier, the stale `data-table` assertions rewritten against the scaffold as it exists now, the tabular-figures advisory regression investigated and settled in whichever direction the evidence supports, and the network/API-key tests confirmed to skip rather than fail when offline or keyless -- [ ] Slice B — a GitHub Actions workflow on pull requests and pushes to master: a fast job (build as type check, plus the Chrome-free tier) and a separate Chrome job, no secrets required, with a pinned Node version and a matching `engines` field +- [x] Slice A — a tiered runner (`test:fast` / `test:full`), the interactive viewer test excluded by name, `test-cli` moved to the Chrome tier, the stale `data-table` assertions rewritten against the scaffold as it exists now, the tabular-figures advisory regression investigated and settled in whichever direction the evidence supports, and the network/API-key tests confirmed to skip rather than fail when offline or keyless +- [x] Slice B — a GitHub Actions workflow on pull requests and pushes to master: a fast job (build as type check, plus the Chrome-free tier) and a separate Chrome job, no secrets required, with a pinned Node version and a matching `engines` field _Full breakdown in [`docs/specs/CI-SPEC.md`](docs/specs/CI-SPEC.md). Deliberately out of scope: branch protection (the checks should earn trust before they can block a merge), a test framework (the plain-script convention works; rewriting 102 files is unrelated to what is broken), release automation, and coverage measurement._ diff --git a/package.json b/package.json index 4ce3cf6..2c3ae6a 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,9 @@ "dist", "docs/GUIDELINES.md" ], + "engines": { + "node": ">=22" + }, "scripts": { "build": "tsc", "prepublishOnly": "npm run build", diff --git a/src/structures.ts b/src/structures.ts index 2b564c5..7d3f372 100644 --- a/src/structures.ts +++ b/src/structures.ts @@ -885,11 +885,12 @@ const auth: Structure = { id: 'au-fields', type: 'frame', width: '100%', layout: 'vertical', gap: SPACE.md, children: [field('au-email', 'Email'), field('au-password', 'Password')], }, - { - id: 'au-submit', type: 'frame', name: 'Submit', width: '100%', layout: 'horizontal', - alignItems: 'center', justifyContent: 'center', padding: [SPACE.xs, SPACE.lg], cornerRadius: RADIUS.sm, fill: COLOR.accent, - children: [{ id: 'au-submit-label', type: 'text', content: 'Continue', fontSize: TYPE.body, fontWeight: 600, color: COLOR.bgPrimary }], - }, + // Hand-rolled until CI caught it: this was the one button in the + // library not built from `button()`, so it missed the minWidth / + // overflow / ellipsis hardening and clipped under a long label. + // It only failed on Linux — macOS font metrics left just enough + // room — which is why the Phase 29 scaffold sweep passed it. + { ...button('au-submit', 'Continue', COLOR.accent, COLOR.bgPrimary), name: 'Submit', width: '100%' }, { id: 'au-alt', type: 'text', content: 'Secondary link', fontSize: TYPE.textSm, fontWeight: 500, color: COLOR.accent, textAlign: 'center' }, ], }, @@ -1146,23 +1147,26 @@ function tableRow(id: string): SceneNode { ], }, { - id: `${id}-role`, type: 'frame', width: '18%', layout: 'horizontal', + id: `${id}-role`, type: 'frame', width: '18%', layout: 'horizontal', minWidth: 0, children: [{ id: `${id}-role-chip`, type: 'frame', layout: 'horizontal', alignItems: 'center', padding: [SPACE.xs2, SPACE.xs], - cornerRadius: 999, fill: COLOR.bgElevated, - children: [{ id: `${id}-role-text`, type: 'text', content: 'Role', fontSize: TYPE.caption, color: COLOR.textSecondary }], + cornerRadius: 999, fill: COLOR.bgElevated, minWidth: 0, overflow: 'hidden', + children: [{ id: `${id}-role-text`, type: 'text', content: 'Role', fontSize: TYPE.caption, color: COLOR.textSecondary, textOverflow: 'ellipsis' }], }], }, { - id: `${id}-status`, type: 'frame', width: '18%', layout: 'horizontal', alignItems: 'center', gap: SPACE.xs2, + id: `${id}-status`, type: 'frame', width: '18%', layout: 'horizontal', alignItems: 'center', gap: SPACE.xs2, minWidth: 0, children: [ { id: `${id}-status-dot`, type: 'ellipse', width: 8, height: 8, fill: '$success' }, - { id: `${id}-status-text`, type: 'text', content: 'Status', fontSize: TYPE.caption, color: COLOR.textSecondary }, + { id: `${id}-status-text`, type: 'text', content: 'Status', fontSize: TYPE.caption, color: COLOR.textSecondary, textOverflow: 'ellipsis' }, ], }, { - id: `${id}-amount`, type: 'frame', width: '18%', layout: 'horizontal', justifyContent: 'end', - children: [{ id: `${id}-amount-text`, type: 'text', content: 'Amount — TBD', fontSize: TYPE.textSm, color: COLOR.textPrimary, tabularNums: true }], + // The identity cell above has carried minWidth + ellipsis since Phase 29; + // these three columns never got it, and overflowed on Linux where the + // fonts are wider than the macOS metrics the sweep was run against. + id: `${id}-amount`, type: 'frame', width: '18%', layout: 'horizontal', justifyContent: 'end', minWidth: 0, + children: [{ id: `${id}-amount-text`, type: 'text', content: 'Amount — TBD', fontSize: TYPE.textSm, color: COLOR.textPrimary, tabularNums: true, textOverflow: 'ellipsis' }], }, { id: `${id}-actions`, type: 'frame', width: '12%', layout: 'horizontal', justifyContent: 'end',