mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-21 00:02:30 +00:00
docs: record the icon safe-area invariant and guard the viewBox shape
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
4b38d23294
commit
6ad24b7e20
@@ -658,6 +658,16 @@ When implementing any component:
|
||||
|
||||
We use the **[Lucide icon library](https://lucide.dev/)** to ensure a consistent, modern, and lightweight visual language. Icons are line-only, aligning with our clean and technical aesthetic.
|
||||
|
||||
## Brand marks and the safe area
|
||||
|
||||
Third-party brand marks live in `frontend/src/lib/components/icons/`. They sit next to Lucide glyphs at the same requested size — in resource rows, pickers and trigger strips — so they have to occupy the same box, or the coloured mark reads as larger than the grey glyph beside it.
|
||||
|
||||
Lucide draws on a 24×24 grid and its artwork, stroke included, spans 22 of those 24 units. Every brand mark matches that: **a square `viewBox` centred on the artwork, sized so the painted artwork spans 22/24 of it**, leaving roughly 4% of the box as margin on the tightest side.
|
||||
|
||||
When adding an icon, do not paste the brand's own `viewBox` from its press kit — those are cropped to the artwork, so the mark lands edge to edge and draws about 10% larger than everything around it. Instead, measure the painted bounding box, then set the `viewBox` to a square of `max(width, height) × 24/22` centred on it. `/kitchen_sink/icons` renders the whole library with an **Icon box** toggle that outlines each icon's box, which is the quickest way to see whether a new mark sits with its neighbours.
|
||||
|
||||
`iconViewBox.test.ts` fails on a non-square `viewBox`, the usual symptom of a pasted one. The 22/24 ratio itself needs a renderer to check, so it is not enforced automatically.
|
||||
|
||||
## Do's and Don'ts
|
||||
|
||||
### ✅ Do
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { readdirSync, readFileSync } from 'node:fs'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
// Brand marks share the box lucide draws in: a square viewBox centred on the artwork, sized so
|
||||
// the painted artwork spans 22/24 of it. Squareness is the half that can be checked without a
|
||||
// renderer — the 22/24 ratio needs rasterising, so it is enforced by review, not here. A brand's
|
||||
// own viewBox pasted from a press kit is the way this drifts, and it is almost always non-square.
|
||||
const iconsDir = dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
function svelteFiles(dir: string): string[] {
|
||||
return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
|
||||
const full = join(dir, entry.name)
|
||||
if (entry.isDirectory()) return svelteFiles(full)
|
||||
return entry.name.endsWith('.svelte') ? [full] : []
|
||||
})
|
||||
}
|
||||
|
||||
describe('icon viewBox', () => {
|
||||
const boxes = svelteFiles(iconsDir).flatMap((file) => {
|
||||
const source = readFileSync(file, 'utf8')
|
||||
return [...source.matchAll(/viewBox="([^"]*)"/g)].map((m) => ({
|
||||
icon: file.slice(iconsDir.length + 1),
|
||||
viewBox: m[1]
|
||||
}))
|
||||
})
|
||||
|
||||
it('is square on every icon', () => {
|
||||
const offenders = boxes.filter(({ viewBox }) => {
|
||||
const parts = viewBox
|
||||
.trim()
|
||||
.split(/[\s,]+/)
|
||||
.map(Number)
|
||||
if (parts.length !== 4 || parts.some((n) => !Number.isFinite(n))) return true
|
||||
const [, , width, height] = parts
|
||||
return !(width > 0) || width !== height
|
||||
})
|
||||
expect(offenders.map((o) => `${o.icon}: ${o.viewBox}`)).toEqual([])
|
||||
})
|
||||
|
||||
// Guards the walk itself: a broken path would make the assertion above pass over nothing.
|
||||
it('finds the icon library', () => {
|
||||
expect(boxes.length).toBeGreaterThan(300)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user