@node-k/ui (0.2.0)

Published 2026-09-23 16:38:18 +02:00 by registry-user in node-K/Design-System

Installation

@node-k:registry=
npm config set @node-k:registry=
npm install @node-k/ui@0.2.0
"@node-k/ui": "0.2.0"

About this package

@node-k/ui — Bausteine als Web Components

Stufe 4 aus korrekturen/paketierung.md §7. Angelegt 11.9.2026 mit nk-button als Muster (4a). Entscheidungen 11.9.: form-associated ja · Lit vendored · Workspace ui/ · .jsx-Wrapper später als eigenes Paket · diese Sitzung nur Button.

Benutzen

<script type="module" src="node_modules/@node-k/ui/src/define/nk-button.js"></script>
<link rel="stylesheet" href="node_modules/@node-k/tokens/dist/css/nodek-tokens.css">

<nk-button>Node finden</nk-button>
<nk-button variant="secondary">Mitglied werden</nk-button>
<nk-button variant="ghost" size="sm">Mehr</nk-button>
<nk-button full-width type="submit">Anmelden</nk-button>

Tokens greifen als Custom Properties durch das Shadow DOM — die Komponente bringt keine Farben mit. Ohne nodek-tokens.css ist sie unsichtbar; das ist Absicht (eine Quelle, kein Rückfall-Zweitsatz).

Attribut Werte Standard
variant primary · secondary · ghost primary
size sm · md · lg md
type button · submit · reset button
disabled boolean —
full-width boolean —
label aria-label für Icon-Buttons ohne Text —
--nk-button-surface Grundfläche hinter Umriss-Varianten var(--nk-black)

part="button" und part="label" für ::part()-Styling von außen.

Bauart — das Muster für 4b

Drei Dinge einmal richtig, in src/shared/frame.js, von jeder Komponente importiert:

  1. Doppelebene. .frame = äußere gefaste Fläche in Linienfarbe, padding: var(--nk-frame-line-<klasse>), border: 0. .fill = innere Fläche mit --nk-frame-clip-<klasse>-<richtung>-in. Richtung steht im Token-Namen: aktiv für alles Fokussierbare (Button, Input, Switch), passiv für den Rest.
  2. :focus-visible im Shadow DOM. Aufbau <span class="ring"><button class="frame">…</button></span>. Der Ring liegt auf dem ungeclippten Wrapper .ring:has(:focus-visible) (outline: var(--nk-focus), outline-offset: var(--nk-focus-offset)) — außen, auf der Bühne, von der Knopffarbe durch den Offset getrennt. Der Host hat delegatesFocus: true; kein tabindex am Host. Zwischenkorrektur 2 (12.9., F4): :host(:focus-visible) trifft bei delegatesFocus in Chromium nie (der Host trägt nur :focus-within), :host(:has(…)) sieht nicht in den eigenen Shadow Root, :host(:focus-within) zeichnet auch bei Maus. Nur der Wrapper im selben Baum erfüllt alle drei: Tab zeichnet, Maus nicht, nichts wird weggeschnitten. check.mjs weist :host(:focus-visible) seit 12.9. ab. Entscheidung Zwischenkorrektur 11.9.: das frühere Innenband (Grund + Ring) maß auf dem Primärknopf Türkis auf Orange 1,25:1. Ein Ring innen bräuchte je Fläche einen zweiten Wert; außen reicht ein Token. Gemessen am Primärknopf: Ring --nk-focus-ring #00baba gegen Bühne --nk-black #171717 = 7,45:1 — auf beiden Seiten des Rings, weil der Offset ihn von der orangen Kante trennt. Hell: #007070 auf --nk-paper #f5f5f5 = 5,36:1. Kacheln (tileLink) behielten bis G1 das Innenband (Grund --nk-surface #1c1c1c → 7,46:1; hell auf --nk-surface #ffffff → 5,80:1) — gerechnet, nicht gezeichnet: das ::after lag bis auf seine äußerste Zeile hinter .fill, gezeichnet wurde 1 px Ring, hell 3,07:1 gegen die Linie. R9 (12.9., umgesetzt G1.2 am 22.9.): der Ring der Kacheln liegt außen auf <span class="ring"> um die Kachel, .ring:has(.title-link:focus-visible) — dieselbe Bauart wie beim Knopf, ein Token auf der Bühne (dunkel 7,46:1, hell 5,29:1), Platz aus --nk-frame-gap 8 = 2 + 2 + 4. F5 (12.9.): Kachelschmuck (Linie, Lift) reagiert auf den Fokus nur noch über .frame:has(.title-link:focus-visible), nicht über :focus-within — das traf auch geslottete Knöpfe, und Titel-Halt und Knopf-Halt gaben dasselbe Bild. Regel: der Indikator markiert das Bedienelement, das den Fokus hat, nicht seine Umgebung. nk-card hat kein Tastatur-Pendant zu hover mehr; es gibt in der Karte nichts eigenes zu fokussieren.
  3. prefers-reduced-motion. transition und transform aus Tokens, unter reduce beide none.

Dazu: static formAssociated = true + attachInternals() in jeder Komponente mit Formularbezug. Beim Button liefert das this.form ohne closest() und type="submit" löst form.requestSubmit() aus. nk-input und nk-switch melden ihren Wert über ElementInternals.setFormValue() — dann greift die Angular-Direktive nur noch mit [field] an (paketierung.md §4.2a).

Hover nur unter @media (hover: hover). Farbe nur var(--nk-*) und color-mix(…) — build/check.mjs prüft das, zusammen mit den drei P5-Bedingungen, dem Lit-Importpfad und seit P1 (22.9.) der Form: border-radius nur 0 (--nk-r-pill nur in nk-status-dot und nk-logo), kein border ≠ 0 neben background, kein clip-path neben border ≠ 0, keine nackte Fasengröße in polygon(). Der Zwilling in styles/build/check.mjs zählt dieselben Regeln im Baum gegen eine Sperrklinke (FORM_CAP). Die Prüfung folgt der Importkette: wer focusInset/motion aus shared/frame.js importiert, hat die Bedingung erfüllt; Kommentare werden vorher entfernt. Ein Selbsttest läuft bei jedem check mit (Probe A ohne Import muss anschlagen, Probe B mit Import nicht).

Frage aus dem Auftrag: ganze Kachel klickbar oder nur Titel und Aktion? Entschieden: der Titel ist der Link, die Fläche erweitert nur den Klickbereich. Umgesetzt in shared/frame.js als tileLink:

  • Die Kachel ist passiv — nicht fokussierbar, Fase oben links + unten rechts (frame('card','passiv')).
  • Der Titel ist das einzige interaktive Element: <a href> wenn href gesetzt (Nuxt, SSR, Router), sonst <button> mit Event nk-select (bubbles, composed, detail) wenn selectable gesetzt (Angular). Ohne beides ist der Titel reiner Text — nichts zu fokussieren, nichts zu versprechen.
  • .title-link::after { position:absolute; inset:0 } dehnt den Klickbereich auf die Kachel. Der Fokusring liegt seit G1.2 außen auf dem ungeclippten .ring um die Kachel (.ring:has(.title-link:focus-visible)), die Unterstreichung und der Screenreader-Name bleiben beim Titel.
  • Geslottete Tags und Aktionen (::slotted(*), .raised) liegen mit z-index:1 über dem ::after und bleiben eigenständig klickbar — der PostCard-Fall (echter Button in der Kachel) funktioniert damit ohne Sonderregel.
  • Hover (nur hover: hover) und :focus-within färben die Rahmenlinie in der Akzentfarbe; damit hat die Tastatur dasselbe Signal wie der Zeiger (P5).
  • Status in nk-node-card ist Text + Punkt, nie Farbe allein; hell trägt er --nk-link / --nk-orange-text (P9.6/9.7).
Komponente Attribute Slots Event
nk-event-row day month title datetime href target selectable default = Tags/Meta · action nk-select
nk-node-card name description status status-label href target selectable action nk-select
nk-card hover hover-color (turq·orange) accent size (card·panel) default —
nk-input value name type label placeholder prompt disabled required readonly autocomplete — input change
nk-switch checked name value label disabled default (Beschriftung) change
nk-badge tone (orange·turq·ok·danger·neutral) solid default —
nk-tag variant (topic·meta) default —
nk-mono-label color (turq·orange·dim·faint) no-prompt cursor default —
nk-status-dot status (open·event·closed) label default (eigener Text) —
nk-aktions-panel claim qr-label no-qr default (Kontaktzeilen) · qr —
nk-post-card image image-label aspect tag tag-color (turq·orange) date title excerpt cta-label href image · action nk-action
nk-logo fassung (marke·zeichen·wortmarke·signet) farbig signet (orange-ink·ink-orange·tuerkis-ink·ink-tuerkis) mono size base label — —

K3 §3 (12.9.) — nk-logo, nacktes HTMLElement. Entscheidungen aus dem Formular: ein Attribut fassung; mode gestrichen; nur rund; SVG statt PNG. Umsetzung: das Element lädt die tokenisierte Quelle aus assets/svg/ und bettet sie inline in den Shadow Root — inline darf SVG var(--nk-*) tragen (CLAUDE.md), also folgen .nk-t/.nk-b/ .nk-a dem Modus des Kontexts ohne Attribut, auch verschachtelt; der helle Pol entsteht aus denselben Tokens statt aus einer zweiten Datei. Kein Zuschnitt-Kniff (die SVGs sind randlos), kein filter: mono setzt die Tokenklassen auf currentColor. Signet = app/k-*.svg (modusfest), rund über --nk-r-pill auf dem Quadrat. base zeigt auf das Verzeichnis mit assets/ (Standard: Designsystem-Wurzel; Konsumenten setzen es auf ihren @node-k/assets-Pfad). Offener Punkt: ein eigenständiges Emblem (Netz + K) existiert in assets/svg nicht, nur als PNG. zeichen ist deshalb die Wort-Bild-Marke ohne Wortmarke (.nk-t ausgeblendet) — die Wortmarke liegt im Netz, ein Ausschnitt schnitte Text mit. Soll das Emblem als Quelle nachgezeichnet werden, ist das ein eigener Auftrag. Probe: ui/probe/nk-logo.html (Fassungen, Größen, mono, feste Pole, beide Modi).

K3 (12.9.) — nk-aktions-panel, nk-post-card; nk-logo folgt nach Klärung der Fassungen. nk-aktions-panel: Klasse panel, passiv, Linie --nk-ink. Der Fehler aus K3 §1 ist behoben: Grund --nk-orange ist modusfest, Schrift deshalb --nk-ink (nicht --nk-black) — #171717 auf #fbaa2b = 9,29:1 in beiden Modi; mit --nk-black wäre hell Papier #f5f2ec auf Orange = 1,73:1. Punktraster aus Ink 15 %, Raster --nk-space-6, Punkt --nk-frame-line-xs. QR-Fläche 56 (48 + 8) in --nk-white — Modulmatrix, der eine legitime Ort. check.mjs prüft seit 12.9. zusätzlich: modusfester Grund (--nk-orange|turq|ink|paper) + modusfolgende Schrift (--nk-black|text*|surface) im selben Block ist ein Befund (Gegenprobe: Panel mit --nk-black schlägt an). nk-post-card: Klasse card, passiv; Vorbild bleibt — der geslottete nk-button (Fallback „Weiterlesen →" → nk-action, oder href) ist das einzige fokussierbare Element, kein tileLink, kein :focus-within. Hover färbt Linie in Tag-Farbe (--nk-link / --nk-orange-text) und hebt um --nk-lift-sm, unter reduced-motion aus. Tag als xs-Rahmen direkt aus den Clip-Tokens (eigene Klassennamen). Elf nackte Zahlen → --nk-fs-micro|sm|h4, --nk-lh-snug|normal, --nk-space-*. Probe: ui/probe/nk-zusammengesetzte-k3.html — Tab hält nur auf den drei Knöpfen der Karten.

K2 (12.9.) — die vier Kleinteile, keines fokussierbar. nk-badge und nk-tag: Lit + frame('chip','passiv'), Fase oben links + unten rechts, Linie 1,5px aus --nk-frame-line-chip; kein focusInset, kein motion (kein Übergang); nk-check: nicht-fokussierbar ohne-bewegung im Kopf. Badge-Schrift: Tönung aus dem Text-Token des Tons (--nk-orange-text, hell #8f5a00 auf #f7e2b8 = 5,9:1 — --nk-orange läge bei 1,5:1); gefüllt Orange → --nk-ink (9,3:1, modusfest, §6), gefüllt turq/ok/danger/neutral → --nk-black (modusfolgend: dunkel Ink auf hellen Signalfarben, hell Paper auf den dunklen Rollenfarben #007070/#186f50/#bf1400/#565656 = 5,0–6,6:1; Ink läge dort bei 2,8–3,0:1). nk-mono-label und nk-status-dot sind nackte HTMLElements ohne Lit — gleiche Entscheidung wie nk-netz/nk-qr: keine Fläche, kein Zustand, kein Bedienelement, alle Attribute wirken über :host([…])-CSS; das einzige JS ist der Standardtext je Status im Punkt. Lit brächte nur Gewicht. Der Cursor des Labels blinkt über @keyframes im Shadow Root und steht unter prefers-reduced-motion still. --nk-r-pill nur auf dem quadratischen Punkt. Größen: Punkt über --nk-status-dot-size, Label über geerbte font-size. Probe: ui/probe/nk-kleinteile.html — Tab hält nur am Modusschalter und am nativen Knopf am Ende.

K1 (12.9.) — nk-input, nk-switch, beide form-associated. static formAssociated = true, attachInternals(), setFormValue() in updated(); formDisabledCallback (auch <fieldset disabled>), formResetCallback, formStateRestoreCallback. nk-input: Klasse btn, aktiv; Pflichtfeld über setValidity({valueMissing}), Ungültigkeit als invalid-Attribut gespiegelt, erst nach change (nicht beim Tippen). Linie bei Fokus Türkis über .frame:has(input:focus-visible) — am eigenen Feld, keine Umgebung. nk-switch: natives <button role="switch" aria-checked> — Leertaste und Enter schalten ohne eigenen Tastatur-Code; Text im <label> um den Button, Klick auf den Text schaltet. Formularwert wie Checkbox: value (Standard on) nur wenn checked. Seit G1.1 (22.9.) gefast statt rund: Doppelebene Klasse xs, aktiv — Spur 40×24 (Fase 4, Linie 1), innen 38×22 (Innenfase 3,41), Knopf 16 mit Formstufe xxs (Fase 3 — am Bild entschieden, 2 px las sich bei 1 dpr als Rundung), Weg 16, alles aus --nk-space-* und --nk-frame-*; aus = umrissen (--nk-border-strong/--nk-surface), ein = gefüllt --nk-accent-turq, Knopf dort --nk-ink (P1 §7: 7,46:1 modusfest; --nk-text maß dunkel 2,15:1). Probe: ui/probe/nk-formular.html (FormData, reset, fieldset disabled), ui/probe/g1-geometrie.html (Maße, Fasen, Kontraste).

Tastatur-Ablauf nk-formular.html: Tab → Name (Ring + Linie Türkis), Tippen → input, Tab weg → change, leer + required → invalid (Linie --nk-danger) · … → Newsletter-Schalter (Ring um die Spur), Leertaste → aus, Enter → an, change am Host · Deaktivierte werden übersprungen · Absenden → FormData {name, mail, cmd, ro, news} (pub fehlt, weil aus) · Zurücksetzen → Startwerte, Schalter checked wie im Markup.

nk-card (Basis für PostCard, 11.9.): passiver Behälter, nicht fokussierbar; Akzent als Innenkante der Füllfläche statt als dickerer Rand, damit Fase und Kante nicht kollidieren. :focus-within im Slot gibt dasselbe Signal wie Hover. --nk-card-surface übersteuert den Grund. Probe: ui/probe/nk-card.html.

Offen in 4b: Logo, PostCard, AktionsPanel; nk-tag und nk-status-dot aus 4a, damit die Kacheln sie statt Inline-Markup slotten.

4c · Die zwei Sonderfälle: nk-netz, nk-qr (entschieden 11.9.)

Frage aus dem Auftrag: brauchen die beiden Lit oder genügt ein nacktes Custom Element? Nacktes HTMLElement. Beide haben kein Template zu diffen, nichts zu fokussieren, keine Doppelebene — Lit brächte nur Gewicht. Die Kerne bleiben Quelle in components/netz/net-device.js und components/qr/nodek-qr.js; npm run vendor:core -w ui kopiert sie nach vendor/core/ (check schlägt an, wenn Kopie und Quelle abweichen). src/** importiert nur ../../vendor/core/…. Im Arbeitsbereich liegt für nodek-qr.js nur ein Durchreich-Platzhalter (export * from ../../../components/qr/nodek-qr.js), weil der Design-System-Compiler sonst doppelte Exporte meldet; vendor:core ersetzt ihn durch die echte Kopie. net-device.js (UMD, keine Exporte) liegt direkt als Kopie.

Befund beim Prüfen: components/netz/NetCanvas.jsx war keine Hülle um den Kern — es rechnete ein eigenes Mini-Netz mit rgba()-Literalen und ohne Seed. nk-netz wrappt den echten Kern (makeNet/drawNet): seeded, gebündelte Pfade, optionale Form (el.shape = { points, fit:'contain' } für Kärnten). nodek-qr.js bringt mit defineGenerator() schon ein Custom Element mit — das backt den Stil aber zur Definitionszeit ein und kennt keine Tokens. nk-qr liest die Farben je Render aus den Custom Properties des Hosts.

  • Farben ausschließlich aus --nk-orange, --nk-turq, --nk-black am Host (getComputedStyle(this)). Fehlen die Tokens, greift beim Netz der Fallback des Kerns (dort begründet), beim QR der Renderer-Default. In src/** steht kein Literal.
  • nk-netz: Bewegung nur wenn sichtbar (IntersectionObserver), nicht static und prefers-reduced-motion nicht gesetzt — sonst genau ein Standbild. Das ist die Regel für alle bewegten Bausteine. Host aria-hidden="true", part="canvas", DPR bis 2. Moduswechsel: el.refresh().
  • nk-qr: Shadow-div mit role="img" und aria-label = title, sonst Inhalt (bei strukturierten Typen „QR-Code", damit kein WLAN-Passwort vorgelesen wird). part="qr". Rendert neu bei Attributänderung, refresh() und Wechsel von prefers-color-scheme; result liefert { svg, geometry, moduleCount, version, ecc }.
  • check.mjs: die P5-Pflichten gelten weiter, Ausnahmen erklärt die Komponente im Kopf — nk-check: nicht-fokussierbar (kein :focus-visible nötig) und nk-check: ohne-bewegung (kein prefers-reduced-motion nötig). Nur mit Begründung.
Komponente Attribute Eigenschaften
nk-netz density seed speed link-dist opacity glow static shape (setzen) · sim · refresh()
nk-qr content type fields (JSON) ecc size frame print logo logo-scale title transparent result · refresh()

Probe: ui/probe/sonderfaelle.html — Netz hinter Inhalt, static, glow="0", drei QR-Fassungen, Modusschalter.

Lit: vendored, nicht transitiv

vendor/lit/lit.js ist ein einzelnes ESM-Bündel (LitElement, html, css, svg, nothing, unsafeCSS, classMap, ifDefined), erzeugt von npm run vendor -w ui aus der devDependency lit@3.3.1. Konsumenten bekommen genau diese Fassung, ohne eigene Lit-Auflösung — eine Version in allen drei Apps. src/** importiert ausschließlich ../../vendor/lit/lit.js; jeder andere Lit-Import fällt im check.

Stand 11.9.: die Registry ist aus dem Designsystem heraus nicht erreichbar. vendor/lit/lit.js reicht deshalb den Shim lit.shim.js durch — eine Minimal-Oberfläche derselben API ohne Diffing, für Browserproben ausreichend, für Produktion nicht. Der Shim exportiert dieselben Namen wie das echte Bündel (LitElement, nicht mehr litElementBase), damit die Komponenten unverändert gegen beides laufen. Regel seit der Zwischenkorrektur: kein „fertig" ohne echtes Lit — Abnahme nur nach:

npm ci
npm run vendor -w ui          # esbuild ist devDependency; ersetzt lit.js durch das echte Bündel, löscht den Shim, kopiert die Kerne
npm run check -w ui           # Exit 0, keine Shim-/Platzhalter-Warnung mehr

Nachweis im Browser: Probe öffnen, customElements.get('nk-button') liefert die Klasse. Jede Komponente exportiert ihre Klasse als nk<Name>Element (klein), weil der Design-System-Compiler Großbuchstaben-Exporte hebt (P1.3).

Probe

ui/probe/nk-button.html — alle Varianten und Größen, Modusschalter (:root.light), Fokus- und Formularprobe. ui/probe/zusammengesetzte.html — nk-event-row, nk-node-card: Link-, Event- und passive Fassung, Slots, Fokus am Titel. ui/probe/sonderfaelle.html — nk-netz, nk-qr. Ohne Bundler: ../src/define/nk-button.js als <script type="module">, Tokens aus ../../tokens/dist/css/.

Abnahme 4a

  • npm ci && npm run verify → ui-check: … keine Verstöße
  • npm run vendor -w ui → vendor/lit/VERSION = 3.3.1, Probe rendert unverändert
  • Tab erreicht den Button, Ring außen sichtbar (≥ 3:1 gegen Bühne); Enter/Space lösen click am Host aus
  • customElements.get('nk-button') gegen echtes Lit (nicht Shim) liefert die Klasse
  • type="submit" in <form> → submit-Event des Formulars
  • prefers-reduced-motion: reduce → kein Lift, keine Transition
  • Kontrast Beschriftung: primär Ink auf Orange 9,29:1 · sekundär --nk-link auf Grund (dunkel = Türkis 7,46:1 · hell #007070 auf Papier), nicht --nk-turq — P9.7

Dependencies

Dependencies

ID Version
@node-k/assets 0.2.0
@node-k/tokens 0.2.0

Development dependencies

ID Version
esbuild 0.25.9
lit 3.3.1
Details
npm
2026-09-23 16:38:18 +02:00
8
2026-11-05 16:23:59 +01:00
MIT
69 KiB
Assets (1)
ui-0.2.0.tgz 69 KiB
Versions (9) View all
0.2.8 2026-10-11
0.2.7 2026-10-04
0.2.6 2026-10-04
0.2.5 2026-09-30
0.2.4 2026-09-29