@nighthq/icons
v0.5.0
Published
Nightglass line-work icon set and brand mark, as framework-agnostic path data.
Readme
@nighthq/icons
59 line-work icons and the brand mark, as tiered icon data. No framework.
import { iconToSvg, iconNames, markToSvg } from '@nighthq/icons';
el.innerHTML = iconToSvg('search', { title: 'Search sessions' });Prebuilt files are on disk at @nighthq/icons/svg/<name>.svg, generated from
the same data the JS renders — one drawing, not two.
Where an icon lives
One icon, one document. set/<name>.json is the icon: an IconDef as
JSON, the file name its name, shipped in the tarball so a tool or a person can
read one on its own. It opens with "schema": "ng.icon/1" — the envelope's id,
which versions the file's conventions (an IconDef body, a tiers block that
declares form at the version the icon was authored against, $icon
references) separately from the tiers, which each version their own concepts —
and the build refuses a document with no schema, another schema, a version it
does not implement, or no form, naming the file. Everything else is generated from the documents and held
to them by check:generated — the TypeScript literal in src/set/icons.ts
(kept for its type; IconName and IconsWith<T> are computed from it) and the
.svg files. A document may reference another icon's base drawing as
{ "$icon": "<name>" } where an element list is expected, so a drawing that
is both a standalone icon and another icon's variant is authored once. The
documents export carries every document's text by name, references intact,
for a page that shows what an icon is authored as. The brand mark is the one
drawing without a document: it is computed from a construction in
src/set/mark.ts, and the construction is its source.
What the build refuses is one callable, and you can call it too.
validateHouse holds every rule that makes a document a nightglass icon rather
than merely a valid IconDef — the envelope, the tier laws, the viewBox rule
across the base and every cut and variant, element-id uniqueness, and the
denotation floor — and parseDocument reads a document's text without touching
the filesystem. The build runs it, the corpus tests assert it, and a tool that
generates icons validates against it rather than against a second copy of the
rules. Findings carry a stable code from HOUSE_CODES, so a generator can
decide what is worth another attempt.
set/README.md is the manual for the directory — the document format, how to add an icon, what the build refuses by file name, and the design record that used to sit beside each definition as comments.
A landed pack lives beside the set. packs/<pack>/ is what the icon factory
(nightmill, at the repository root) publishes through its export verb: a
manifest.json naming the pack's icons, icons/<name>.json — documents in the
same envelope as set/'s, with the provenance tier filled — and the pack's own
svg/, sheet, measures, changelog and evidence, which the build does not read.
The build reads the documents into ONE set under their plain names: iconNames
is one union and iconToSvg('cloud-upload') does not care who drew it. A name
held twice — by set/ and a pack, or by two packs — is refused by file name
(name.duplicate), as is a pack whose manifest and icons/ disagree. set/
stays curated by hand; nothing in the factory writes into it.
The model
Icons implement the tiered icon model
(docs/0002-tiered-icon-model.md). The
mandatory floor is form: one ordered element list — { id?, geom, mode? },
list order is draw order, geometry a lossless union (path, circle, rect,
line, polyline, polygon, ellipse), mode the whole floor paint
(stroke | fill). Everything above the floor is an opt-in tier, declared
per icon and gated in both directions (src/model/tiers.ts): a declared tier
must be fulfilled, and a field whose tier is undeclared is refused. Eleven are
registered, each with its own version and stability channel:
- stable —
denotation(label, keywords, category; carried for pickers and search, never auto-applied as a render title; the whole set opts in),detail(named cuts of one drawing; the mark'ssmallcut), andstate(typed attributes, toggle pairs, replacement or patch variants;eyeflips betweenvisibleandhidden). - experimental, each carrying the question it exists to answer —
structure,parts,anchors,tokens,paint,motionandprovenance. The mark declares six of the seven;alertbinds its dot to a colour token;eyeblinks.
The floor also carries extensions, the one unchecked bag: keys are
<namespace>.<name> by law, values are never validated, and nothing that draws
ever reads it.
realize(def, context) is the one deterministic step from definition and
context — detail level, state configuration, token overrides, rendered size —
to a realization: resolved elements in a resolved space, colour as an
abstract source, transforms as values. The model ends there. Five backends spell
it — SVG (the string renderer, the generated files, the Svelte binding),
Canvas2D, a mesh backend that turns every stroke into triangles because a GPU
only fills, and WebGL and WebGPU on top of that mesh — and none knows the corpus
or interprets a tier. Two GPU backends over one tessellation is what makes the
mesh a shared substrate rather than one backend's private step. Selection is
loud — an unknown level, an out-of-domain value or an unregistered
operation id throws rather than falling back. IconsWith<T> narrows the name
union to a capability at compile time.
Conventions
- a 24x24 grid; stroke only,
fill="none"; - stroke-width 1.6, the mark's own weight, so an icon set beside the logo reads as the same drawing rather than as clip art next to a logo;
currentColor, so an icon is coloured by the token on its container and a theme change reaches it without a second asset;- round caps and joins — the one place the system's "sharp corners" rule does not apply. That rule governs container radii; a butt-capped 1.6px terminal at 16px renders as a visibly chipped edge.
Accessibility
title is the switch. Without it an icon is decorative and marked
aria-hidden — the right default, because most icons sit beside a label that
already says the word, and announcing it twice is noise. Give a title only when
the icon is the only thing carrying the meaning.
The mark
A nightglass graticule: an aperture ring, a hollow centre, four spokes, and seven radial ticks swept 65 degrees off-axis. The seven and the 65 are inherited from the mark this design language descends from — the lineage is in the construction, not in a resemblance. The hollow centre is the point: the instrument is the thing you see through, not the thing you look at.
markToSvg({ small: true }) drops the ticks and thickens the stroke. Below about
32px the ticks close into a grey halo, so the small cut keeps the mark legible
rather than keeping it identical.
Geometry
pathPoints, pathHull and pathBounds parse path data properly rather than
scraping numbers out of it, because scraping is wrong in a way that looks right:
M12 3.5A8.5 8.5 0 1120.5 121120.5 there is the large-arc flag, the sweep flag, and x=20.5 — SVG lets
single-digit flags run together. A scrape also reads arc radii and rotations as
coordinates, so it can pass an icon that clips and fail one that does not. Curves
are bounded by their control hull (conservative, which is the correct direction
for a clipping check); unknown commands throw rather than being skipped.
The family
Five packages, published together from one repository:
| package | what it is |
| -------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
| @nighthq/tokens | the colour system, and the base stylesheets that depend only on it |
| @nighthq/components | the Svelte 5 interface primitives, plus an opt-in atmospheric tier |
| @nighthq/icons | line-work icons and the brand mark, as tiered icon data — no framework |
| @nighthq/model | the framework-neutral view kernel: one semantic model, two presentations |
| @nighthq/host | the effect boundary — history, titles, announcements, focus |
Start with tokens + components. Add icons when you need glyphs, and
model + host when one screen has to render as both a desktop and a compact
presentation.
The repository is private, so homepage and repository links on these npm
pages will not resolve for you — these READMEs and the tarballs are the whole
documented surface, which is why they carry more than a link would.
(No counts in this table on purpose: a number repeated across five files is
five places for it to go stale, and only each package's own README has a
check-docs claim behind its figures.)
