@super-ic/icon-contracts
v0.1.3
Published
Semantic icon-name contract for SuperIC surfaces: a fixed set of dotted names, the accessible Icon component behind them, and the derived vendor catalog.
Readme
@super-ic/icon-contracts
The icon contract for SuperIC surfaces. Product code never names a glyph, a
vendor or a file. It names a semantic role (action.delete,
feedback.warning, navigation.back) and this package decides which glyph
draws it.
That indirection is the whole point. Swapping the icon vendor, or repointing one role at a different glyph, is a change inside this package and nowhere else.
Install
npm install @super-ic/icon-contractsA clean install succeeds without a Nucleo licence. Glyphs are an optional peer.
Peer dependencies
| Peer | Range | Why |
| --- | --- | --- |
| react | >=19.0.0 | Icon returns JSX and uses the React 19 automatic runtime. |
| nucleo-ui-outline-18 | 1.5.0 (optional) | Licensed glyph package. Required only when the app imports @super-ic/icon-contracts/nucleo. |
react-dom is not a peer dependency. Nothing in dist/ imports it.
Bind glyphs once at the application root, before anything renders:
import "@super-ic/icon-contracts/nucleo";
import { Icon } from "@super-ic/icon-contracts";That /nucleo import needs [email protected] installed and the
environment variable NUCLEO_LICENSE_KEY set for the vendor's own install
check. This README names the variable only. If nothing has bound, Icon
throws IconGlyphsNotBoundError naming the import to add. It never renders a
placeholder glyph.
The licensed glyph dependency
[email protected] is an optional peer. Installing
@super-ic/icon-contracts does not install it and does not run its licence
check. The vendor package is installed only by consumers who import /nucleo.
The foundation token layer
This package needs no CSS. It emits no className and no
var(--token); glyphs paint with currentColor and inherit the text colour
of whatever wraps them. Size comes from the size prop, stroke weight from
strokeWidth (default 1.5).
The surfaces you draw icons on will almost always need the token layer, so your application still imports it once, high up:
import "@super-ic/foundation/tokens.css";That import is a requirement of @super-ic/foundation and of the pattern
packages, not of this one.
The semantic-name contract
CORE_ICON_NAMES is a frozen, exhaustive list of 36 dotted names in five
namespaces (21 action, 4 feedback, 5 navigation, 4 state,
1 status). IconName is its union type, so a typo is a compile error and
an unknown name at runtime throws UnknownIconNameError (code:
"unknown-icon-name") rather than rendering nothing.
| Namespace | Names |
| --- | --- |
| action.* | add, calendar, camera, close, confirm, copy, delete, edit, favorite, filter, more, open-external, pause, resume, retry, search, share, sign-out, submit, upload, view |
| feedback.* | error, info, success, warning |
| navigation.* | back, down, forward, menu, up |
| state.* | empty, offline, permission, recovery |
| status.* | loading |
Rules the contract enforces:
- A name describes intent, not appearance.
action.delete, neveraction.trash-can. When two roles share a glyph today (state.offlineandfeedback.warningboth draw the warning triangle) they stay separate names, because they will diverge. - The list is closed. Product code may not invent a name. A new role is
a change to
CORE_ICON_NAMEShere, reviewed with the rest of the contract. - Vendor markers never reach a public surface. No
Nucleo, noOutline18, nolucide:prefix in any exported name. - Every rendered icon declares its accessibility posture. See below.
The decorative discriminator
IconProps is a discriminated union with no third state, and Icon throws
rather than guess:
import { Icon } from "@super-ic/icon-contracts";
// Decorative: adjacent text already carries the meaning.
<Icon name="action.search" decorative /> // aria-hidden="true"
// Meaningful: the icon IS the label. `label` is required and must be non-empty.
<Icon name="action.delete" decorative={false} label="Delete listing" />| Call | Result |
| --- | --- |
| decorative omitted | throws icon decorative discriminator must be true or false |
| decorative with a label | throws decorative icons cannot carry a label |
| decorative={false} with no / blank label | throws meaningful icon label must be a non-empty string |
aria-hidden, aria-label and role are stripped from the accepted prop
type: the discriminator owns them. Every rendered glyph also carries
data-semantic-icon and data-icon-size, which is how visual tests assert
that a surface used the contract instead of a raw glyph import.
ICON_SIZES is [12, 14, 16, 18, 20, 24, 32] and size defaults to 16.
Entry points
| Specifier | Contents |
| --- | --- |
| @super-ic/icon-contracts | Values: Icon, CORE_ICON_NAMES, ICON_NAMES, CORE_ICON_VENDOR_EXPORTS, ICON_SIZES, bindCoreGlyphs, getNucleoIcon, getIcon, resolveCompatibleIconName, NucleoCompatibilityMap, UnknownIconNameError, IconGlyphsNotBoundError, IconGlyphsIncompleteError. Types: IconProps, CoreIconProps, IconName, CoreIconName, IconSize, IconComponent, NucleoIconComponent |
| @super-ic/icon-contracts/nucleo | Named vendor imports plus bindCoreGlyphs at module scope. Import once at the application root. |
| @super-ic/icon-contracts/catalog | Vendor-free generated inventory: CATALOG_ICON_NAMES, CATALOG_ICON_COUNT, createCatalog(glyphs), plus the category derivation re-exports. No vendor import. |
| @super-ic/icon-contracts/catalog/nucleo | Binds the catalog through the licensed vendor package. For the icon browser only. Needs the licence. |
| @super-ic/icon-contracts/lucide-compat | lucide-react name → glyph map, for incremental migration off Lucide |
./catalog versus the root entry
The root entry is the contract: a small closed set of governed roles, and the only surface product code should reach for.
./catalog is the inventory: kebab-case names and a createCatalog
factory. It does not import the vendor. ./catalog/nucleo is the licensed
binding used by the icon browser. Reaching for a catalog glyph in a product
surface routes around the contract. If a surface needs a glyph, promote it to
a semantic name first.
The catalog is generated from the installed glyph package, never hand-edited, and the build fails if it drifts from what that package installs. It carries names and component references only, no glyph path data.
Package shape
ESM only ("type": "module"), sideEffects: false, types alongside the
JavaScript in dist/. The tarball is dist/ plus this README and the
manifest: no sources, no tests, and no build tooling. The intake script that
regenerates the catalog reads and writes the filesystem at module load and is
excluded from emit.
