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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
47 changes: 27 additions & 20 deletions .github/scripts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,51 +57,58 @@ Vector editor for SVG icons.

Registry files are never modified. Use `Copy SVG` or `Download` to persist changes off-server.

### `/graph`

Read-only dashboard of registry-wide metrics:

- Total icons, categories, average shapes/icon, total & average SVG source size
- Icons per category (horizontal bar chart)
- Complexity distribution (bucketed by shape count)
- Size distribution (bucketed by SVG source bytes)
- Top 10 largest icons

## Project layout

```
.github/scripts/
├── demo-server.ts # Static file server (paths + fallback to repo root)
├── demo-server.ts # Static file server + on-demand Tailwind compile
├── demo/
│ ├── browse.html # /browse
│ ├── studio.html # /studio
│ ├── graph.html # /graph
│ └── assets/
│ ├── css/
│ │ └── main.css # Shared styles for all three pages
│ │ └── tailwind.css # Tailwind v4 source (@theme, @source, @custom-variant)
│ └── js/
│ ├── data.js # Icon loading (index.json → *.json → SVGs)
│ ├── shell.js # Nav active state, stats badge, toast
│ ├── browse.js # /browse logic
│ ├── studio.js # /studio editor (drag, undo/redo, props, new icon)
│ └── graph.js # /graph metrics + charts
│ └── studio.js # /studio editor (drag, undo/redo, props, new icon)
├── update-category.ts # Regenerates <category>.json index files
├── package.json # Scripts: demo, update-category, build
└── README.md
```

## Styling — Tailwind CSS v4 (no CDN)

The demo used to load Tailwind from `cdn.tailwindcss.com`. It now uses local
Tailwind v4 compiled by `demo-server.ts` on demand:

- `demo/assets/css/tailwind.css` is the source. It uses `@import "tailwindcss"`,
a `@theme` block for the KFE color palette / fonts / radii, `@source`
globs pointing at `*.html` + `assets/js/*.js`, and a `@custom-variant dark`
so `<html class="dark">` still opts pages into dark styling.
- On each request for `/assets/css/tailwind.css`, the server compiles via
`@tailwindcss/node` and rescans class candidates via `@tailwindcss/oxide`.
Output is cached and reused until the source CSS or any scanned file's
mtime changes, so edits appear on the next reload without a build step.

Add a new custom color? Add `--color-<name>: #hex;` under `@theme` in
`tailwind.css` — no server restart needed, just reload the page.

## How the server routes requests

`demo-server.ts` handles four kinds of requests:
`demo-server.ts` handles five kinds of requests:

1. `/` — 302 redirect to `/browse`
2. `/browse`, `/studio`, `/graph` — served from `demo/*.html`
3. `/assets/**` — served from `demo/assets/**`
4. Anything else — served from the repo root (so `/index.json`, `/brands.json`, `/brands/react.json` etc. all work)
2. `/browse`, `/studio` — served from `demo/*.html`
3. `GET /assets/css/tailwind.css` — compiled on demand from the Tailwind source
4. `/assets/**` — otherwise served from `demo/assets/**`
5. Anything else — served from the repo root (so `/index.json`, `/brands.json`, `/brands/react.json` etc. all work)

Path traversal is blocked by requiring the resolved path to stay inside the intended root.

## Notes

- No build step, no runtime dependencies. Everything is vanilla ES modules loaded directly by the browser.
- Zero client-side build step. The browser still loads only vanilla ES modules;
Tailwind is compiled server-side on request.
- Custom icons are stored under the `kfe-custom-icons` `localStorage` key. Clear browser storage to reset.
79 changes: 77 additions & 2 deletions .github/scripts/demo-server.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import http from 'http';
import fs from 'fs';
import path from 'path';
import { compile } from '@tailwindcss/node';
import { Scanner } from '@tailwindcss/oxide';

const args = process.argv.slice(2);
const getArg = (flag: string) => {
Expand All @@ -16,6 +18,8 @@ const HOST = getArg('--host') || process.env.HOST || '0.0.0.0';
const PORT = Number(getArg('--port') || process.env.PORT) || 5173;
const REPO_ROOT = path.resolve(__dirname, '../..');
const DEMO_ROOT = path.join(__dirname, 'demo');
const TW_CSS_SRC = path.join(DEMO_ROOT, 'assets', 'css', 'tailwind.css');
const TW_CSS_BASE = path.dirname(TW_CSS_SRC);

const MIME: Record<string, string> = {
'.html': 'text/html; charset=utf-8',
Expand All @@ -35,15 +39,85 @@ const PAGE_ROUTES: Record<string, string> = {
'/browse/': 'browse.html',
'/studio': 'studio.html',
'/studio/': 'studio.html',
'/graph': 'graph.html',
'/graph/': 'graph.html',
};

const SLUG = /^[a-z0-9][a-z0-9-]*$/;

// Tailwind on-the-fly compiler. Rebuilds when the source CSS or any scanned
// content file changes since the last request; otherwise returns the cached
// output. Compile object is rebuilt only when the source CSS itself changes,
// since `@source` resolution happens at compile-time.
type TwState = {
css: string;
// File → mtimeMs snapshot at the moment `css` was produced.
fingerprint: Map<string, number>;
};
let twCache: TwState | null = null;
let twCompilerSrcMtime = 0;
let twCompiler: Awaited<ReturnType<typeof compile>> | null = null;

async function buildTailwindCss(): Promise<string> {
const cssSource = fs.readFileSync(TW_CSS_SRC, 'utf8');
const srcStat = fs.statSync(TW_CSS_SRC);

// Rebuild the compiler only when the source CSS changes — otherwise reuse
// it and just rescan candidates. Compilation is the expensive step.
if (!twCompiler || twCompilerSrcMtime !== srcStat.mtimeMs) {
twCompiler = await compile(cssSource, {
base: TW_CSS_BASE,
from: TW_CSS_SRC,
onDependency: () => {},
});
twCompilerSrcMtime = srcStat.mtimeMs;
twCache = null;
}

const scanner = new Scanner({ sources: twCompiler.sources });
const candidates = scanner.scan();

// Fingerprint: source CSS + every file the scanner touched. If any mtime
// matches the previous run we can short-circuit the build.
const fingerprint = new Map<string, number>();
fingerprint.set(TW_CSS_SRC, srcStat.mtimeMs);
for (const f of scanner.files) {
try {
fingerprint.set(f, fs.statSync(f).mtimeMs);
} catch {
// File vanished between scan and stat; ignore.
}
}

if (twCache && sameFingerprint(twCache.fingerprint, fingerprint)) {
return twCache.css;
}

const css = twCompiler.build(candidates);
twCache = { css, fingerprint };
return css;
}

function sameFingerprint(a: Map<string, number>, b: Map<string, number>): boolean {
if (a.size !== b.size) return false;
for (const [k, v] of a) {
if (b.get(k) !== v) return false;
}
return true;
}

const server = http.createServer((req, res) => {
const url = decodeURIComponent((req.url ?? '/').split('?')[0]);

// GET /assets/css/tailwind.css — compile-on-demand, no build step needed.
if (req.method === 'GET' && url === '/assets/css/tailwind.css') {
buildTailwindCss()
.then(css => send(res, 200, MIME['.css'], css))
.catch(err => {
console.error('[tailwind] compile failed:', err);
send(res, 500, 'text/plain', `Tailwind compile error: ${String((err as Error)?.message ?? err)}`);
});
return;
}

// POST /api/save — writes <repo-root>/<category>/<name>.json
if (req.method === 'POST' && url === '/api/save') {
let body = '';
Expand Down Expand Up @@ -122,4 +196,5 @@ server.listen(PORT, HOST, () => {
console.log(`KFE icons demo → http://${HOST === '0.0.0.0' ? 'localhost' : HOST}:${PORT}/`);
console.log(` demo root: ${DEMO_ROOT}`);
console.log(` repo root: ${REPO_ROOT}`);
console.log(` tailwind : ${TW_CSS_SRC} (compiled on-demand at /assets/css/tailwind.css)`);
});
82 changes: 0 additions & 82 deletions .github/scripts/demo/assets/js/browse.js

This file was deleted.

73 changes: 0 additions & 73 deletions .github/scripts/demo/assets/js/graph.js

This file was deleted.

Loading
Loading