Something went wrong. Try again.
Monorepo for the nibling.run platform
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169import { adopt, define, sheet } from "../internals.ts";import type { IconName } from "./names.ts";import { ICON_SYMBOLS } from "./sprite.generated.ts";
const SVG_NS = "http://www.w3.org/2000/svg";
const iconSheet = sheet(` :host { display: inline-block; width: 1em; height: 1em; flex: none; color: inherit; } svg { display: block; width: 100%; height: 100%; fill: currentColor; }`);
/** Parses each symbol's markup once; both `ensureSymbol` and `icon` clone from this. */const parser = new DOMParser();const templates = new Map<IconName, SVGSymbolElement>();function template(name: IconName): SVGSymbolElement { let node = templates.get(name); if (!node) { const markup = ICON_SYMBOLS[name]; if (!markup) throw new Error(`Unknown icon: ${name}`); const doc = parser.parseFromString(`<svg xmlns="${SVG_NS}">${markup}</svg>`, "image/svg+xml"); templates.set(name, (node = doc.documentElement.firstElementChild as SVGSymbolElement)); } return node;}
/** A `<use>` only resolves an id within its own tree scope, so each scope needs its own sprite. */export type IconRoot = Document | ShadowRoot;
/** Each root's own hidden sprite: a `<symbol>` per icon that root has actually used, added lazily. */const sprites = new WeakMap<IconRoot, SVGSVGElement>();
function spriteFor(root: IconRoot): SVGSVGElement { let sprite = sprites.get(root); if (!sprite) { sprite = document.createElementNS(SVG_NS, "svg"); // Present in the tree (so `<use>` can resolve against it) but invisible and out of the a11y // tree: zero-sized, clipped, and aria-hidden, like the codicon dist sprite's own wrapper. sprite.setAttribute("aria-hidden", "true"); sprite.setAttribute("focusable", "false"); Object.assign(sprite.style, { position: "absolute", width: "0", height: "0", overflow: "hidden" }); sprites.set(root, sprite); (root instanceof Document ? (root.body ?? root.documentElement) : root).append(sprite); } return sprite;}
/** Adds `name`'s `<symbol>` to `root`'s sprite the first time `root` uses it; a no-op after that. */function ensureSymbol(root: IconRoot, name: IconName): SVGSymbolElement { const def = template(name); const sprite = spriteFor(root); const id = `nib-icon-${name}`; const existing = sprite.querySelector(`#${id}`); if (existing) return existing as SVGSymbolElement; const clone = def.cloneNode(true) as SVGSymbolElement; sprite.append(clone); return clone;}
/** * Builds a `<use>`-based icon referencing `name` in `root`'s sprite (injecting that one `<symbol>` * into `root` first if nothing there has used it yet). Every icon drawn into the same root after the * first shares that root's sprite instead of carrying its own copy: a tree row that shows one * chevron costs one `<symbol>` in its own shadow root, not one per row across the whole tree plus a * second per re-render. For code that builds shadow content directly (`MenuItemElement`, * `TreeItemElement`, `TabElement`) rather than using `<nib-icon>`. */export function icon(root: IconRoot, name: IconName): SVGSVGElement { const def = ensureSymbol(root, name); const svg = document.createElementNS(SVG_NS, "svg"); svg.setAttribute("width", "1em"); svg.setAttribute("height", "1em"); // Each codicon has its own native viewBox (most are 0 0 16 16, but not all — e.g. "files" and // "terminal" are 0 0 24 24); without this the generated <use> instance is sized off the element's // unscaled natural size instead of its rendered CSS box, so wider glyphs overflow and clip. svg.setAttribute("viewBox", def.getAttribute("viewBox")!); svg.setAttribute("fill", "currentColor"); svg.setAttribute("aria-hidden", "true"); svg.setAttribute("focusable", "false"); const use = document.createElementNS(SVG_NS, "use"); use.setAttribute("href", `#nib-icon-${name}`); svg.append(use); return svg;}
/** * An icon from nibling's codicon-backed sprite (see `names.ts`). Decorative by default * (`aria-hidden`); set `label` to make it an informative `role="img"` with that accessible name. * Sized by `1em` and colored by `currentColor`, so it follows the surrounding text. * * Carries its own sprite in its own shadow root (there's no other tree scope its `<use>` could share * one with). Fine for the handful of standalone icons a page has at once — a header button, an * activity bar entry — but a list that repeats the same icon per row (a tree, a tab bar, a menu) * should call `icon()` directly into the list item's own existing shadow root instead, so every row * shares that one root's sprite rather than each row carrying its own. */export class IconElement extends HTMLElement { readonly #internals = this.attachInternals(); readonly #root = this.attachShadow({ mode: "open" }); #name: IconName | undefined; #svg: SVGSVGElement | undefined;
static get observedAttributes(): string[] { return ["name", "label"]; }
constructor() { super(); adopt(this.#root, iconSheet); this.#internals.role = "none"; this.#internals.ariaHidden = "true"; }
attributeChangedCallback(attr: string, _old: string | null, value: string | null): void { if (attr === "name") this.#setName(value as IconName | null); else if (attr === "label") this.#setLabel(value); }
get name(): IconName | undefined { return this.#name; }
set name(value: IconName | undefined) { if (value === undefined) this.removeAttribute("name"); else this.setAttribute("name", value); }
get label(): string { return this.getAttribute("label") ?? ""; }
set label(value: string) { if (value) this.setAttribute("label", value); else this.removeAttribute("label"); }
#setName(value: IconName | null) { this.#name = value ?? undefined; this.#render(); }
#setLabel(value: string | null) { this.#internals.role = value ? "img" : "none"; this.#internals.ariaLabel = value; this.#internals.ariaHidden = value ? null : "true"; }
#render() { // Only the <use>-wrapping <svg> is replaced: the sprite `icon()` injected lives in this same // shadow root as its sibling, and must survive a name change so later icons keep reusing it. this.#svg?.remove(); this.#svg = this.#name ? icon(this.#root, this.#name) : undefined; if (this.#svg) this.#root.append(this.#svg); }}
declare global { interface HTMLElementTagNameMap { "nib-icon": IconElement; }}
define("nib-icon", IconElement);