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
7 changes: 7 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -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
6 changes: 4 additions & 2 deletions .github/workflows/committed-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
21 changes: 14 additions & 7 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down Expand Up @@ -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 `<x-ichava-icon-browser::prepaint />` 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
Expand Down
1 change: 1 addition & 0 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
58 changes: 58 additions & 0 deletions docs/recipes/frontend-development.md
Original file line number Diff line number Diff line change
@@ -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 <your-branch> -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)
6 changes: 3 additions & 3 deletions docs/release.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
54 changes: 54 additions & 0 deletions resources/js/core/devStylesheet.test.ts
Original file line number Diff line number Diff line change
@@ -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);
}
});
});
27 changes: 23 additions & 4 deletions vite.inertia.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand Down Expand Up @@ -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)),
Expand Down
Loading