From d8bb0ac5e2dce136f0fde20a2d560863b21adef2 Mon Sep 17 00:00:00 2001 From: Imani Manyara Date: Fri, 13 Nov 2026 16:33:30 -0500 Subject: [PATCH 1/4] Keep the e2e suite and guard configs out of the Composer archive #150 added e2e/browser (specs, harness pages, 11 baseline PNGs), .dependency-cruiser.cjs and tsconfig.domain.json to the archive. They are development-only, as is the host e2e check already under e2e/. --- .gitattributes | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.gitattributes b/.gitattributes index 513eca2..70a0d19 100644 --- a/.gitattributes +++ b/.gitattributes @@ -32,3 +32,10 @@ /resources/js/**/*.test.ts export-ignore /resources/js/**/*.test.tsx export-ignore /resources/js/test export-ignore + +# The browser's Playwright suite (e2e/browser, with its baselines and harness pages) and the host +# e2e check, and the two guard configs that came with them (react consolidation 3.1/3.3), are +# development-only. +/e2e export-ignore +/.dependency-cruiser.cjs export-ignore +/tsconfig.domain.json export-ignore From ca5accea2d8bab230082a4acb024f4aa14867ffa Mon Sep 17 00:00:00 2001 From: Imani Manyara Date: Fri, 13 Nov 2026 16:33:40 -0500 Subject: [PATCH 2/4] Serve the browser in dev as it ships, and build it in one command vite.inertia.config.ts scoped the browser's stylesheet and rewrote its font URLs in the build only, so npm run dev put the browser's tokens on :root, its rules on the whole host page and its fonts on a path nothing serves. The scoping now runs in dev too, and dev points the fonts at resources/assets/fonts; devStylesheet.test.ts asks the dev server for the stylesheet in middleware mode and was proven red on the old config. The production bundle is byte-identical. build and build:prod write resources/views/prepaint.html before the bundle, so a UI change is one build; build:prepaint still works alone. committed-build compares the prepaint view with public/assets. --- .github/workflows/committed-build.yml | 6 ++- package.json | 4 +- resources/js/core/devStylesheet.test.ts | 54 +++++++++++++++++++++++++ vite.inertia.config.ts | 27 +++++++++++-- 4 files changed, 83 insertions(+), 8 deletions(-) create mode 100644 resources/js/core/devStylesheet.test.ts diff --git a/.github/workflows/committed-build.yml b/.github/workflows/committed-build.yml index 2b51563..afd15f0 100644 --- a/.github/workflows/committed-build.yml +++ b/.github/workflows/committed-build.yml @@ -47,8 +47,10 @@ jobs: - name: Compare with the committed bundle run: | - if ! git diff --exit-code --stat public/assets; then - echo "::error title=Committed bundle is stale::public/assets differs from a fresh build of this tree. Run 'npm install --no-package-lock && npm run build:prod' and commit the changes under public/assets." + # build:prod also writes resources/views/prepaint.html (react consolidation 4.1), which the + # prepaint component inlines, so it is compared with the bundle. + if ! git diff --exit-code --stat public/assets resources/views/prepaint.html; then + echo "::error title=Committed bundle is stale::public/assets or resources/views/prepaint.html differs from a fresh build of this tree. Run 'npm install --no-package-lock && npm run build:prod' and commit the changes." exit 1 fi if [ -n "$(git status --porcelain --untracked-files=all public/assets)" ]; then diff --git a/package.json b/package.json index f5a97d6..74e3f6c 100644 --- a/package.json +++ b/package.json @@ -22,8 +22,8 @@ "check:config-drift": "node .dev/tools/check-config-drift.mjs eslint.base.js=config/eslint.base.js tsconfig.base.json=config/tsconfig.base.json .dev/tools/sync-vendored-library.mjs=config/tools/sync-vendored-library.mjs .dev/tools/check-config-drift.mjs=config/check-config-drift.mjs", "// clean": "Narrow on purpose. inertia-app.js/.css are CHECKED IN and no npm script or CI job rebuilds them -- only build does. rm -rf public/* destroyed them, and a failing vite build destroyed them for nothing. Delete only what the build re-emits. See issue #26.", "clean": "rm -rf public/assets/js/inertia-app*.js public/assets/css/inertia-app.css public/.vite 2>/dev/null || true", - "build": "npm run clean && echo '' && echo '🔨 Building Ichava (dev)...' && NODE_ENV=development vite build --config vite.inertia.config.ts", - "build:prod": "npm run clean && echo '' && echo '🚀 Building Ichava (production)...' && NODE_ENV=production vite build --config vite.inertia.config.ts", + "build": "npm run clean && npm run -s build:prepaint && echo '' && echo '🔨 Building Ichava (dev)...' && NODE_ENV=development vite build --config vite.inertia.config.ts", + "build:prod": "npm run clean && npm run -s build:prepaint && echo '' && echo '🚀 Building Ichava (production)...' && NODE_ENV=production vite build --config vite.inertia.config.ts", "watch": "echo '' && echo '👀 Watching Ichava for changes...' && vite build --watch --config vite.inertia.config.ts", "dev": "vite --config vite.inertia.config.ts", "test:js": "vitest run", diff --git a/resources/js/core/devStylesheet.test.ts b/resources/js/core/devStylesheet.test.ts new file mode 100644 index 0000000..529dcd2 --- /dev/null +++ b/resources/js/core/devStylesheet.test.ts @@ -0,0 +1,54 @@ +import { existsSync } from 'node:fs'; +import { join } from 'node:path'; +import { createServer, type ViteDevServer } from 'vite'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; + +/** + * `npm run dev` serves the browser's stylesheet as a host receives it (react consolidation 4.1, + * 2026-11-13): scoped to the browser's root, with fonts that load. + * + * vite.inertia.config.ts scoped the stylesheet and rewrote its font URLs in the build only, so + * under the dev server the browser's tokens sat on `:root`, its rules reached the whole host page + * and its fonts asked for `/fonts/...`, which nothing serves. This asks the dev server for the + * stylesheet in middleware mode, which needs no port, and checks what it would send. + */ +const root = process.cwd(); +let server: ViteDevServer; +let css = ''; + +beforeAll(async () => { + server = await createServer({ + configFile: join(root, 'vite.inertia.config.ts'), + server: { middlewareMode: true, hmr: false, ws: false }, + logLevel: 'error', + }); + css = (await server.transformRequest('/resources/js/styles/theme.css?direct'))?.code ?? ''; +}, 60_000); + +afterAll(async () => { + await server?.close(); +}); + +describe('the dev server serves the browser stylesheet as the build ships it', () => { + it('serves a stylesheet at all', () => { + expect(css.length).toBeGreaterThan(50_000); + }); + + it('scopes the utilities to the browser root', () => { + expect(css).toMatch(/@layer utilities\s*\{\s*@scope \(\[data-ichava-theme\]\)/); + }); + + it('declares no browser token on the host document', () => { + const onRoot = [...css.matchAll(/(?:^|[}\s]):root\b[^{]*\{([^}]*)\}/g)].filter((m) => /--(color|ich|bg|fg|accent)/.test(m[1] ?? '')); + expect(onRoot.map((m) => m[0].slice(0, 80))).toEqual([]); + }); + + it('points every font at a file the dev server can serve', () => { + const urls = [...css.matchAll(/url\((['"]?)([^'")]*\.woff2)\1\)/g)].map((m) => m[2] ?? ''); + expect(urls.length).toBeGreaterThanOrEqual(2); + for (const url of urls) { + expect(url, url).toMatch(/^\/resources\/assets\/fonts\//); + expect(existsSync(join(root, url)), url).toBe(true); + } + }); +}); diff --git a/vite.inertia.config.ts b/vite.inertia.config.ts index b34ef89..580630a 100644 --- a/vite.inertia.config.ts +++ b/vite.inertia.config.ts @@ -42,13 +42,15 @@ const BROWSER_SCOPE = '[data-ichava-theme]'; * `scopeThemeTokens` and `scopeUtilityLayer`. Those ran over every emitted stylesheet; here the * browser's CSS shares one bundle with this package's own (ichava.scss, pages.css), whose * `:root` and `body` rules must stay global. So this runs on the browser's module alone, after - * Tailwind has compiled and minified it, and before Vite joins the stylesheets. Build only, like - * the two plugins it replaces. + * Tailwind has compiled it, and before Vite joins the stylesheets. + * + * It runs under `npm run dev` too (react consolidation 4.1, 2026-11-13). It used to be build only, + * so the dev server put the browser's tokens on `:root` and its rules on the whole host page, and + * a page looked different in dev from what a host receives. `DevStylesheetTest` pins both modes. */ function browserStylesheet(): Plugin { return { name: 'ichava:browser-stylesheet', - apply: 'build', transform(code, id) { if (id.split('?')[0] !== BROWSER_STYLESHEET) return null; return { code: scopeSheet(scopeTokens(code, BROWSER_SCOPE), BROWSER_SCOPE), map: null }; @@ -85,13 +87,30 @@ function shipFonts(): Plugin { }; } +/** + * Serve the fonts under `npm run dev`. theme.css asks for `/fonts/...`, which only the build + * rewrites (shipFonts); the dev server serves the repository at its root, so in dev the URLs point + * at the files in resources/assets/fonts instead of at a path nothing serves. + */ +function devFonts(): Plugin { + return { + name: 'ichava:browser-dev-fonts', + apply: 'serve', + enforce: 'post', + transform(code, id) { + if (id.split('?')[0] !== BROWSER_STYLESHEET) return null; + return { code: code.replace(/url\((['"]?)\/fonts\//g, 'url($1/resources/assets/fonts/'), map: null }; + }, + }; +} + /** package.json's version, for the browser's `config.meta.version`. */ const VERSION = (JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8')) as { version: string }).version; export default defineConfig({ base: './', define: { __ICHAVA_BROWSER_VERSION__: JSON.stringify(VERSION) }, - plugins: [react(), tailwindcss(), browserStylesheet(), shipFonts()], + plugins: [react(), tailwindcss(), browserStylesheet(), shipFonts(), devFonts()], resolve: { alias: { '@': fileURLToPath(new URL('./resources/js', import.meta.url)), From eb00df407eb5cf24ff233f355a2607416ee9b9a9 Mon Sep 17 00:00:00 2001 From: Imani Manyara Date: Fri, 13 Nov 2026 16:33:41 -0500 Subject: [PATCH 3/4] Document frontend work on the browser: dev, checks, e2e and the build A docs/recipes page covers the dev server with hot reload, every check, the Playwright suite and its baselines, and the one build. architecture.md, release.md, getting-started.md and the README index point at it and describe the e2e harness and layering guards. --- README.md | 1 + docs/architecture.md | 21 ++++++---- docs/getting-started.md | 1 + docs/recipes/frontend-development.md | 58 ++++++++++++++++++++++++++++ docs/release.md | 6 +-- 5 files changed, 77 insertions(+), 10 deletions(-) create mode 100644 docs/recipes/frontend-development.md diff --git a/README.md b/README.md index 8633d6f..4762a7c 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,7 @@ The docs live in this repository, under [`docs/`](docs/). - [REST mode in a non-Inertia app](docs/recipes/rest-mode.md) — the API without the Inertia pages - [Grant admin access](docs/recipes/grant-admin-access.md) — one Gate for status and the cache - [Turn features off](docs/recipes/turn-features-off.md) — hide what the host does not offer +- [Work on the browser UI](docs/recipes/frontend-development.md) — dev server with hot reload, checks, e2e and the one build ### Ecosystem diff --git a/docs/architecture.md b/docs/architecture.md index 3d32488..7cb4762 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -33,11 +33,18 @@ that repository's built library, vendored under `resources/vendor/react-icon-bro radio buttons, toggle, featured icon and `uui-theme.css`) come from `@ichava/js-ui-kernel/react`, pinned to `#v0.1.0` (the tree of `bc5120c`). Their old paths are deprecated re-exports. The tabs and the dropdown stay local. -- The standalone demo (`main.tsx`, `StandaloneApp.tsx`, `embedded.tsx`, `components/boot/`) came - with the snapshot and is not built here. -- `.dev/browser/` holds the browser's build plugins, generators and the fixture catalogue its tests - read. `.dev/imported/react-icon-browser/` holds the rest of that repository's root files - (configs, docs, e2e, the standalone pages). Both are left out of the Composer archive. +- The standalone demo (`main.tsx`, `StandaloneApp.tsx`, `embedded.tsx`, `components/boot/`) is not + shipped. It is the fixture react-icon-browser's Playwright suite drives: `e2e/browser/` holds the + specs, their baselines and the two harness pages, which `e2e/browser/vite.config.ts` builds from + `resources/js` with a static catalog (`npm run e2e:browser`, `npm run visual`). +- `.dev/browser/` holds the browser's build plugins, generators, guard scripts and the fixture + catalogue its tests read. `.dev/imported/react-icon-browser/` holds the rest of that repository's + root files (its docs, configs and manifest). `.dev/`, `e2e/` and the tests are left out of the + Composer archive. +- The layering rules came across too: `.dependency-cruiser.cjs` (`npm run boundaries`, proven able + to fail by `npm run check:boundary-rules`) and the stricter `tsconfig.domain.json` for + `modules/*/domain` and `application` (`npm run typecheck:domain`). `.github/workflows/browser.yml` + runs them, the other guards and the e2e suite on every pull request, and the visual suite weekly. `resources/js` is also the Inertia adapter around the browser: @@ -87,8 +94,8 @@ baselines of the private Metronic pack) were not imported. `sync:react-icon-browser` and `check:consumers` scripts that had only printed where the work moved. The one file still read from it, `prepaint.html`, now lives at `resources/views/prepaint.html`: the `` component inlines it, -`npm run build:prepaint` writes it from `resources/js/boot/prepaint.html`, and a Vitest test -fails while the two differ. +`npm run build:prod` writes it from `resources/js/boot/prepaint.html` before it builds the bundle +(`npm run build:prepaint` does it alone), and a Vitest test fails while the two differ. The look comes with the library: since the 0.1.0 restyle the browser draws Ichava's warm greys, the teal `#0e7c66` default accent, a 6px radius, Tabler chrome icons, square tags and tabs, flat diff --git a/docs/getting-started.md b/docs/getting-started.md index c47f8cc..4e5dd51 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -54,6 +54,7 @@ Browser adapts to the host application rather than imposing a stack, so the prot ## See also +- [Work on the browser UI](recipes/frontend-development.md), to change the React source in `resources/js` with hot reload - [Configuration](configuration.md) - [Environment variables](environment.md) - [Architecture](architecture.md) diff --git a/docs/recipes/frontend-development.md b/docs/recipes/frontend-development.md new file mode 100644 index 0000000..2293218 --- /dev/null +++ b/docs/recipes/frontend-development.md @@ -0,0 +1,58 @@ +# Work on the browser UI + +Change the browser's React source in `resources/js`, see it live in a host, then check and build it with one command each. + +## Run it with hot reload + +Start the dev server from this checkout, and let a host that has the package installed load from it: + +```bash +npm install +npm run dev # Vite on http://localhost:5174, the Inertia entry resources/js/app.tsx +``` + +```dotenv +# in the host's .env (app.debug must be on) +ICHAVA_VITE_DEV=true +``` + +The layout (`resources/views/app.blade.php`) loads `resources/js/app.tsx` from the dev server, with React Fast Refresh, whenever `ICHAVA_VITE_DEV` is on, `APP_DEBUG` is on and the server answers on `ichava.icon-browser.vite.host` and `.port`. Otherwise it loads the committed bundle. The dev server serves the browser's stylesheet scoped to its root and its fonts from `resources/assets/fonts`, as the build ships them; `resources/js/core/devStylesheet.test.ts` pins that. + +## Check it + +```bash +npm run typecheck:inertia && npm run lint && npm run test:js +npm run typecheck:domain && npm run typecheck:e2e +npm run boundaries && npm run check:boundary-rules +npm run check:schema-types && npm run check:dead-links +``` + +The Playwright suite drives the browser's standalone pages, built from `resources/js` by `e2e/browser/vite.config.ts`, with a static catalog and no Laravel host: + +```bash +npx playwright install chromium +npm run e2e:browser # 98 behavioural tests +npm run visual:linux:check # 12 screenshots, inside CI's Playwright image (needs Docker) +``` + +Screenshots are compared only in `mcr.microsoft.com/playwright:v1.65.0-noble`; `npm run check:playwright-image` fails when that tag drifts between `browser.yml`, `visual-baselines.yml`, `.dev/browser/linux.sh` and `@playwright/test`. To regenerate baselines for a pull request, dispatch the seeding workflow on its branch; it commits the changed PNGs and comments the drift: + +```bash +gh workflow run visual-baselines.yml --repo ichava/icon-browser --ref -f commit=true +``` + +## Build it + +```bash +npm run build:prod # public/assets and resources/views/prepaint.html +npm run check:dead-classes # reads the stylesheet the build wrote +``` + +Commit both outputs with the source change. `committed-build.yml` rebuilds every pull request and fails when either differs. Two traps: + +- `resources/assets/styles/ichava.scss` is a second Tailwind root whose utilities are global. It scans the repository except the paths it lists under `@source not`, so a word in a new top-level folder can become a class on every host page. Rebuild and check the diff of `public/assets/css/inertia-app.css`. +- No lock file is committed, so a rebuild on another day can pick up new dependency releases. + +--- + +[← Docs index](../../README.md#documentation) diff --git a/docs/release.md b/docs/release.md index 0782060..9e3d8b7 100644 --- a/docs/release.md +++ b/docs/release.md @@ -64,11 +64,11 @@ The browser's source lives in `resources/js` since 2026-11-12 (see [Architecture ```bash npm install npm run typecheck:inertia && npm run lint && npm run test:js -npm run build:prod # then commit public/assets -npm run build:prepaint # only when resources/js/boot/prepaint.html changed +npm run e2e:browser # the browser's Playwright suite, on its standalone harness +npm run build:prod # then commit public/assets and resources/views/prepaint.html ``` -`npm run build:prepaint` writes `resources/views/prepaint.html`, which the prepaint component inlines; commit it with the bundle. The vendored `resources/vendor/react-icon-browser` and its `sync:react-icon-browser` and `build:lib` scripts were removed on 2026-11-13. +`npm run build:prod` is the one build: it writes `resources/views/prepaint.html`, which the prepaint component inlines, and then the bundle. Commit both; `committed-build.yml` fails when either differs from a fresh build. [Work on the browser UI](recipes/frontend-development.md) covers the dev server, every check and the visual baselines. The vendored `resources/vendor/react-icon-browser` and its `sync:react-icon-browser` and `build:lib` scripts were removed on 2026-11-13. The source and the committed bundle contain Untitled UI PRO components, used under the Untitled UI licence as part of this product ([LICENSE](../LICENSE), [Architecture](architecture.md#untitled-ui-pro-components)). The repository is public (decided 2026-10-30) and the package stays proprietary. A release tag on the public repository publishes normally: `release.yml` does not gate on repository visibility. Its other safeguards stand, namely the tag-only trigger, the refusal of any ref that is not a `vX.Y.Z` tag, and the changelog section it extracts and fails closed without. From 1d10df01b642addf5469d2e6dcb04bebbfef32fd Mon Sep 17 00:00:00 2001 From: Imani Manyara Date: Fri, 13 Nov 2026 16:35:54 -0500 Subject: [PATCH 4/4] Record dev parity, the single build and the archive fix --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 51cdd5d..b246b69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,11 +6,15 @@ All notable changes to `ichava/icon-browser` follow [Keep a Changelog](https://k ### Added +- **[Work on the browser UI](docs/recipes/frontend-development.md)**, a recipe for the dev server with hot reload, the checks, the Playwright suite and its baselines, and the build. - **The browser's Playwright suite and guards run here** (react consolidation 2.6, 3.1 and 3.3). react-icon-browser's 20 e2e specs and their harness pages moved to `e2e/browser/`, built from `resources/js` by `e2e/browser/vite.config.ts` and run by `npm run e2e:browser` (98 tests, as in react-icon-browser); `npm run visual` compares the 12 committed Linux baselines inside the pinned Playwright image. `.github/workflows/browser.yml` runs the guards and the e2e suite on pull requests and the visual suite weekly and by dispatch; `visual-baselines.yml` seeds baselines on a pull request's branch. New npm scripts: `typecheck:domain`, `typecheck:e2e`, `boundaries`, `check:boundary-rules`, `check:schema-types`, `schema:types`, `check:dead-classes`, `check:dead-links`, `check:playwright-image`, `visual`, `visual:update`, `visual:linux`, `visual:linux:check`, `e2e:linux`. - **The SVG rendering sample is tracked, with the public packs only** (tabler and flag, react-icon-browser's own 18 files). The svg-baseline spec no longer renders the private bundled and Metronic packs, so it covers six archetypes, not eight; `noPrivateContent.test.ts` fails if the sample holds anything else. ### Changed +- **`npm run dev` serves the browser as it ships** (react consolidation 4.1). The dev server scopes the browser's stylesheet to its root and serves its fonts, as the build always did; before, a page in dev carried the browser's tokens on `:root` and its rules on the whole host page, with fallback fonts. `devStylesheet.test.ts` pins it. +- **One build per UI change.** `npm run build` and `npm run build:prod` write `resources/views/prepaint.html` before the bundle; `npm run build:prepaint` still works alone. `committed-build.yml` compares the prepaint view as well as `public/assets`. +- **The Composer archive leaves out `e2e/`, `.dependency-cruiser.cjs` and `tsconfig.domain.json`**, which the previous change had added (484 files, from 529). - `core/minifiedBundle.ts`, used only by its test, moved to `resources/js/test/minifiedBundle.ts`; dependency-cruiser's `no-unreachable` rule reported it. ### Fixed