From 97a788a97cce0bc8bfc754441f5f61a003a1a1ac Mon Sep 17 00:00:00 2001 From: Cuarroc <47299143+Cuarroc@users.noreply.github.com> Date: Sat, 17 Oct 2026 07:19:01 +0200 Subject: [PATCH 1/7] test(design): DS-X-21 red tests, one icon set and the Lucide notice The brief's test name has a comma, which the commit-msg hook rejects in a Test-First trailer; the test reads 'and is hidden' instead. Test-First: src/design/icons/icon.test.tsx::every registered icon renders a 20 px svg and is hidden unless labelled Test-First: src/design/icons/icon.test.tsx::an empty label stays hidden Test-First: src/design/icons/icon.test.tsx::has an equivalent for every classic glyph and every route glyph Test-First: src/design/icons/icons.notice.test.ts::THIRD_PARTY_NOTICES lists Lucide and the licence file exists Co-Authored-By: Claude Sonnet 5.5 --- src/design/icons/Icon.tsx | 3 ++ src/design/icons/glyphs.ts | 4 +++ src/design/icons/icon.test.tsx | 41 +++++++++++++++++++++++++++ src/design/icons/icons.notice.test.ts | 19 +++++++++++++ 4 files changed, 67 insertions(+) create mode 100644 src/design/icons/Icon.tsx create mode 100644 src/design/icons/glyphs.ts create mode 100644 src/design/icons/icon.test.tsx create mode 100644 src/design/icons/icons.notice.test.ts diff --git a/src/design/icons/Icon.tsx b/src/design/icons/Icon.tsx new file mode 100644 index 00000000..afb64cbe --- /dev/null +++ b/src/design/icons/Icon.tsx @@ -0,0 +1,3 @@ +export function Icon(_props: { name: string; label?: string }) { + return null; +} diff --git a/src/design/icons/glyphs.ts b/src/design/icons/glyphs.ts new file mode 100644 index 00000000..d2fd068e --- /dev/null +++ b/src/design/icons/glyphs.ts @@ -0,0 +1,4 @@ +export type IconName = string; +export const GLYPHS: Record = {}; +export const CLASSIC_GLYPH: Record = {}; +export const ROUTE_GLYPH: Record = {}; diff --git a/src/design/icons/icon.test.tsx b/src/design/icons/icon.test.tsx new file mode 100644 index 00000000..879f3e2f --- /dev/null +++ b/src/design/icons/icon.test.tsx @@ -0,0 +1,41 @@ +import { render } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; +import { ICON_NAMES as CLASSIC_NAMES } from "../../components/Icon"; +import { ROUTES } from "../../shell/routes"; +import { Icon } from "./Icon"; +import { CLASSIC_GLYPH, GLYPHS, ROUTE_GLYPH, type IconName } from "./glyphs"; + +const names = Object.keys(GLYPHS) as IconName[]; + +describe("Icon", () => { + it("every registered icon renders a 20 px svg and is hidden unless labelled", () => { + expect(names.length).toBeGreaterThanOrEqual(24); + for (const name of names) { + const hidden = render().container.querySelector("svg")!; + expect(hidden.getAttribute("width"), name).toBe("20"); + expect(hidden.getAttribute("height"), name).toBe("20"); + expect(hidden.getAttribute("stroke"), name).toBe("currentColor"); + expect(hidden.getAttribute("aria-hidden"), name).toBe("true"); + expect(hidden.hasAttribute("role"), name).toBe(false); + expect(hidden.querySelectorAll("path").length, name).toBeGreaterThan(0); + + const named = render().container.querySelector("svg")!; + expect(named.getAttribute("role"), name).toBe("img"); + expect(named.getAttribute("aria-label"), name).toBe("Hinweis"); + expect(named.hasAttribute("aria-hidden"), name).toBe(false); + } + }); + + it("an empty label stays hidden", () => { + const svg = render().container.querySelector("svg")!; + expect(svg.getAttribute("aria-hidden")).toBe("true"); + expect(svg.hasAttribute("role")).toBe(false); + }); + + it("has an equivalent for every classic glyph and every route glyph", () => { + for (const name of CLASSIC_NAMES) expect(GLYPHS[CLASSIC_GLYPH[name]], name).toBeDefined(); + const withIcon = ROUTES.filter((route) => route.icon !== null).map((route) => route.id); + expect(withIcon.length).toBe(Object.keys(ROUTE_GLYPH).length); + for (const id of withIcon) expect(GLYPHS[ROUTE_GLYPH[id]], id).toBeDefined(); + }); +}); diff --git a/src/design/icons/icons.notice.test.ts b/src/design/icons/icons.notice.test.ts new file mode 100644 index 00000000..ef023a36 --- /dev/null +++ b/src/design/icons/icons.notice.test.ts @@ -0,0 +1,19 @@ +import { existsSync, readFileSync } from "node:fs"; +import { resolve } from "node:path"; +import { describe, expect, it } from "vitest"; + +const root = resolve(__dirname, "../../.."); +const licence = "src/design/icons/LUCIDE-LIZENZ.txt"; + +describe("Lucide notice", () => { + it("THIRD_PARTY_NOTICES lists Lucide and the licence file exists", () => { + const notices = readFileSync(resolve(root, "docs/THIRD_PARTY_NOTICES.md"), "utf8"); + expect(notices).toContain("Lucide"); + expect(notices).toContain(licence); + expect(existsSync(resolve(root, licence))).toBe(true); + const text = readFileSync(resolve(root, licence), "utf8"); + expect(text).toContain("ISC License"); + expect(text).toContain("Lucide Icons and Contributors"); + expect(text).toContain("Cole Bemis"); + }); +}); From 738254a3da907903bb0d2001b309b8a4e9deb2ad Mon Sep 17 00:00:00 2001 From: Cuarroc <47299143+Cuarroc@users.noreply.github.com> Date: Sat, 17 Oct 2026 07:21:20 +0200 Subject: [PATCH 2/7] feat(design): DS-X-21 icon set on a 20 px grid Icon renders 24 Lucide paths copied as data (ISC and Feather MIT notice shipped as LUCIDE-LIZENZ.txt), with stand-ins for the classic, route and Select glyphs. No caller migrates; the classic Icon stays until KR-15. Test-First: src/design/icons/icon.test.tsx::every registered icon renders a 20 px svg and is hidden unless labelled Test-First: src/design/icons/icon.test.tsx::an empty label stays hidden Test-First: src/design/icons/icon.test.tsx::has an equivalent for every classic glyph and every route glyph Test-First: src/design/icons/icons.notice.test.ts::THIRD_PARTY_NOTICES lists Lucide and the licence file exists Co-Authored-By: Claude Sonnet 5.5 --- docs/THIRD_PARTY_NOTICES.md | 6 ++++ src/design/gallery/Gallery.tsx | 5 +++ src/design/gallery/gallery.css | 1 + src/design/icons/Icon.tsx | 32 +++++++++++++++++-- src/design/icons/LUCIDE-LIZENZ.txt | 43 ++++++++++++++++++++++++++ src/design/icons/glyphs.ts | 49 +++++++++++++++++++++++++++--- src/design/registry.ts | 8 ++++- src/i18n/de.ts | 1 + 8 files changed, 138 insertions(+), 7 deletions(-) create mode 100644 src/design/icons/LUCIDE-LIZENZ.txt diff --git a/docs/THIRD_PARTY_NOTICES.md b/docs/THIRD_PARTY_NOTICES.md index b890e6bf..17e580a6 100644 --- a/docs/THIRD_PARTY_NOTICES.md +++ b/docs/THIRD_PARTY_NOTICES.md @@ -25,6 +25,12 @@ npx --yes license-checker-rseidelsohn@5.0.1 --production --json # npm section - **Inter font** (`public/fonts/Inter-*.otf`): SIL Open Font License 1.1. The license text ships next to the fonts as `public/fonts/INTER-LIZENZ.txt`. Copyright 2016-2023 The Inter Project Authors. +- **Lucide icons** (`src/design/icons/glyphs.ts`): about 24 icon paths copied as + data (no package installed). ISC License, Copyright (c) 2026 Lucide Icons and + Contributors; icons derived from Feather (check, chevrons, external-link, ...) + additionally carry the MIT notice, Copyright (c) 2013-present Cole Bemis. + License text, copied from https://github.com/lucide-icons/lucide/blob/main/LICENSE: + `src/design/icons/LUCIDE-LIZENZ.txt`. - **Recursive font** (`docs/dev-hq/fonts/recursive-latin-wght.woff2`): SIL Open Font License 1.1, license text next to it as `docs/dev-hq/fonts/OFL.txt`. - **prompt-master** (`src-tauri/resources/prompt-master/`): MIT License, diff --git a/src/design/gallery/Gallery.tsx b/src/design/gallery/Gallery.tsx index cf3665b3..d1f7a4f7 100644 --- a/src/design/gallery/Gallery.tsx +++ b/src/design/gallery/Gallery.tsx @@ -2,6 +2,8 @@ import { useEffect, useState, type ReactNode } from "react"; import { de, type MessageKey } from "../../i18n/de"; import { Button } from "../controls/Button"; +import { Icon } from "../icons/Icon"; +import { GLYPHS } from "../icons/glyphs"; import { motionDuration } from "../motion/tokens"; import { useMotion } from "../motion/motionMode"; import { COMPONENT_REGISTRY, type ComponentFamily } from "../registry"; @@ -103,6 +105,9 @@ function Body({ page }: { page: Page }) { ); case "motion": return ; + case "icons": return ( +
    {Object.keys(GLYPHS).map((n) =>
  • {n}
  • )}
+ ); default: return ( <>

{de["gallery.storiesLater"]}

diff --git a/src/design/gallery/gallery.css b/src/design/gallery/gallery.css index 38029eca..9c813d0b 100644 --- a/src/design/gallery/gallery.css +++ b/src/design/gallery/gallery.css @@ -20,3 +20,4 @@ .g-gal-track { width: 320px; padding: var(--g-space-4); border-radius: var(--g-r-pill); background: var(--g-tint); } .g-gal-dot { display: block; width: 24px; height: 24px; border-radius: var(--g-r-pill); background: var(--g-accent); transition: transform var(--g-dur-5) var(--g-ease-move); } .g-gal-dot[data-moved="on"] { transform: translateX(280px); } +.g-gal-icon { display: flex; flex-direction: column; align-items: center; gap: var(--g-space-2); inline-size: 8rem; color: var(--g-ink); } diff --git a/src/design/icons/Icon.tsx b/src/design/icons/Icon.tsx index afb64cbe..3015bc19 100644 --- a/src/design/icons/Icon.tsx +++ b/src/design/icons/Icon.tsx @@ -1,3 +1,31 @@ -export function Icon(_props: { name: string; label?: string }) { - return null; +import type { SVGProps } from "react"; +import { GLYPHS, type IconName } from "./glyphs"; + +export interface IconProps extends Omit, "name" | "children" | "width" | "height"> { + name: IconName; + /** Accessible name. Without one the icon is hidden from assistive technology. */ + label?: string; +} + +/** Lucide's 24-unit artboard drawn at 20 px. Colour is `currentColor`; name the control, not the glyph, unless it stands alone. */ +export function Icon({ name, label, className, style, ...rest }: IconProps) { + return ( + + {GLYPHS[name].map((d) => )} + + ); } diff --git a/src/design/icons/LUCIDE-LIZENZ.txt b/src/design/icons/LUCIDE-LIZENZ.txt new file mode 100644 index 00000000..718bb3f0 --- /dev/null +++ b/src/design/icons/LUCIDE-LIZENZ.txt @@ -0,0 +1,43 @@ +ISC License + +Copyright (c) 2026 Lucide Icons and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +--- + +The following Lucide icons are derived from the Feather project: + +airplay, alert-circle, alert-octagon, alert-triangle, aperture, arrow-down-circle, arrow-down-left, arrow-down-right, arrow-down, arrow-left-circle, arrow-left, arrow-right-circle, arrow-right, arrow-up-circle, arrow-up-left, arrow-up-right, arrow-up, at-sign, calendar, cast, check, chevron-down, chevron-left, chevron-right, chevron-up, chevrons-down, chevrons-left, chevrons-right, chevrons-up, circle, clipboard, clock, code, columns, command, compass, corner-down-left, corner-down-right, corner-left-down, corner-left-up, corner-right-down, corner-right-up, corner-up-left, corner-up-right, crosshair, database, divide-circle, divide-square, dollar-sign, download, external-link, feather, frown, hash, headphones, help-circle, info, italic, key, layout, life-buoy, link-2, link, loader, lock, log-in, log-out, maximize, meh, minimize, minimize-2, minus-circle, minus-square, minus, monitor, moon, more-horizontal, more-vertical, move, music, navigation-2, navigation, octagon, pause-circle, percent, plus-circle, plus-square, plus, power, radio, rss, search, server, share, shopping-bag, sidebar, smartphone, smile, square, table-2, tablet, target, terminal, trash-2, trash, triangle, tv, type, upload, x-circle, x-octagon, x-square, x, zoom-in, zoom-out + +The MIT License (MIT) (for the icons listed above) + +Copyright (c) 2013-present Cole Bemis + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/design/icons/glyphs.ts b/src/design/icons/glyphs.ts index d2fd068e..9e766566 100644 --- a/src/design/icons/glyphs.ts +++ b/src/design/icons/glyphs.ts @@ -1,4 +1,45 @@ -export type IconName = string; -export const GLYPHS: Record = {}; -export const CLASSIC_GLYPH: Record = {}; -export const ROUTE_GLYPH: Record = {}; +// DS-X-21: path data copied from Lucide (ISC, with the Feather MIT notice for derived icons). +// Notice: LUCIDE-LIZENZ.txt next to this file and docs/THIRD_PARTY_NOTICES.md. Each glyph is +// one array of path strings on Lucide's 24-unit artboard (rect, circle and line turned into paths). + +export const GLYPHS = { + "panel-left": ["M5 3h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z", "M9 3v18"], + "columns-2": ["M5 3h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z", "M12 3v18"], + "maximize-2": ["M15 3h6v6", "m21 3-7 7", "m3 21 7-7", "M9 21H3v-6"], + "refresh-cw": ["M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8", "M21 3v5h-5", "M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16", "M8 16H3v5"], + "settings": ["M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915", "M9 12a3 3 0 1 0 6 0a3 3 0 1 0 -6 0"], + "x": ["M18 6 6 18", "m6 6 12 12"], + "chevron-left": ["m15 18-6-6 6-6"], + "chevron-right": ["m9 18 6-6-6-6"], + "chevron-down": ["m6 9 6 6 6-6"], + "chevron-up": ["m18 15-6-6-6 6"], + "plug": ["M12 22v-5", "M15 8V2", "M17 8a1 1 0 0 1 1 1v4a4 4 0 0 1-4 4h-4a4 4 0 0 1-4-4V9a1 1 0 0 1 1-1z", "M9 8V2"], + "external-link": ["M15 3h6v6", "M10 14 21 3", "M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"], + "check": ["M20 6 9 17l-5-5"], + "ellipsis": ["M11 12a1 1 0 1 0 2 0a1 1 0 1 0 -2 0", "M18 12a1 1 0 1 0 2 0a1 1 0 1 0 -2 0", "M4 12a1 1 0 1 0 2 0a1 1 0 1 0 -2 0"], + "gauge": ["m12 14 4-4", "M3.34 19a10 10 0 1 1 17.32 0"], + "flag": ["M4 22V4a1 1 0 0 1 .4-.8A6 6 0 0 1 8 2c3 0 5 2 7.333 2q2 0 3.067-.8A1 1 0 0 1 20 4v10a1 1 0 0 1-.4.8A6 6 0 0 1 16 16c-3 0-5-2-8-2a6 6 0 0 0-4 1.528"], + "shield-check": ["M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z", "m9 12 2 2 4-4"], + "list": ["M3 5h.01", "M3 12h.01", "M3 19h.01", "M8 5h13", "M8 12h13", "M8 19h13"], + "users": ["M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2", "M16 3.128a4 4 0 0 1 0 7.744", "M22 21v-2a4 4 0 0 0-3-3.87", "M5 7a4 4 0 1 0 8 0a4 4 0 1 0 -8 0"], + "zap": ["M15.914 4a1.5 1.5 0 00-2.474-1.561l-9 9A1.5 1.5 0 005.5 14h4.002a.5.5 0 01.471.666L8.086 20a1.5 1.5 0 002.475 1.56l9-9A1.5 1.5 0 0018.5 10h-3.997a.5.5 0 01-.472-.667z"], + "hexagon": ["M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"], + "timer": ["M10 2L14 2", "M12 14L15 11", "M4 14a8 8 0 1 0 16 0a8 8 0 1 0 -16 0"], + "book-open": ["M12 5v16", "M20.001 19A2 2 0 0022 17V5a2 2 0 00-1.999-2L16 3.002A5 5 0 0012 5a5 5 0 00-4-2H4a2 2 0 00-2 2v12a2 2 0 001.999 2H8a5 5 0 014 2 5 5 0 014-2z"], + "sliders-horizontal": ["M10 5H3", "M12 19H3", "M14 3v4", "M16 17v4", "M21 12h-9", "M21 19h-5", "M21 5h-7", "M8 10v4", "M8 12H3"], +} as const satisfies Record; + +export type IconName = keyof typeof GLYPHS; + +/** Lucide stand-ins for the 14 classic glyphs of src/components/Icon.tsx (callers move in KR-15). */ +export const CLASSIC_GLYPH = { + rail: "panel-left", split: "columns-2", expand: "maximize-2", refresh: "refresh-cw", gear: "settings", + close: "x", chevronLeft: "chevron-left", chevronRight: "chevron-right", chevronDown: "chevron-down", + chevronUp: "chevron-up", plug: "plug", external: "external-link", check: "check", more: "ellipsis", +} as const satisfies Record; + +/** Lucide stand-ins for the sidebar glyphs in src/shell/routes.ts, by route id; the Select chevron is chevron-down. */ +export const ROUTE_GLYPH: Record = { + leitstand: "gauge", eingang: "flag", beweise: "shield-check", betrieb: "list", team: "users", + automatik: "zap", core: "hexagon", steuerung: "timer", gedaechtnis: "book-open", einstellungen: "sliders-horizontal", +}; diff --git a/src/design/registry.ts b/src/design/registry.ts index 8f4a8c65..8153d0bf 100644 --- a/src/design/registry.ts +++ b/src/design/registry.ts @@ -1,7 +1,7 @@ // DS-X-14: the one catalogue of design components. `registry.coverage.test.ts` // keeps it in step with the component exports (function, const, default) under src/design. -export type ComponentFamily = "controls" | "data" | "fabrik" | "forms" | "gallery" | "hall" | "inputs" | "layout" | "state" | "surface"; +export type ComponentFamily = "controls" | "data" | "fabrik" | "forms" | "gallery" | "hall" | "icons" | "inputs" | "layout" | "state" | "surface"; export interface ComponentEntry { /** The exported function name. */ @@ -235,4 +235,10 @@ export const COMPONENT_REGISTRY: readonly ComponentEntry[] = [ a11y: "Static text, never focusable; the count or tag is the signal, so colour is never alone. A bare count needs a word beside it.", text: "Count or children from the caller; not a state word (use StateMark) and not proof (use ProofChip).", }, + { + name: "Icon", family: "icons", + variants: ["name from the Lucide set in glyphs.ts, 20 px"], states: ["label given or not"], + a11y: "aria-hidden without a label; with one it is role=\"img\" and aria-label. Colour follows currentColor.", + text: "No text; the label is the caller's. Licence: LUCIDE-LIZENZ.txt, docs/THIRD_PARTY_NOTICES.md.", + }, ]; diff --git a/src/i18n/de.ts b/src/i18n/de.ts index 2b0a9e5d..971f21e4 100644 --- a/src/i18n/de.ts +++ b/src/i18n/de.ts @@ -233,6 +233,7 @@ export const de = { "gallery.family.forms": "Formulare", "gallery.family.gallery": "Galerie", "gallery.family.hall": "Halle", + "gallery.family.icons": "Symbole", "gallery.family.inputs": "Eingaben", "gallery.family.layout": "Layout", "gallery.family.state": "Zustand", From dbe5dc9fe39be39d1f351fa44eb5a5f6d6ce5bc6 Mon Sep 17 00:00:00 2001 From: Cuarroc <47299143+Cuarroc@users.noreply.github.com> Date: Sat, 17 Oct 2026 07:22:22 +0200 Subject: [PATCH 3/7] docs(plan): DS-X-21 row No-Test: plan text only Co-Authored-By: Claude Sonnet 5.5 --- docs/PLAN.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/PLAN.md b/docs/PLAN.md index 6f515fb4..c5f5e6cd 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -256,6 +256,7 @@ Größe vor dem Start messen: `store.mjs` (570), `app.js` (504/527) und `store.t | DS-X-13 | Eine Zustandskarte: `lampFor` liefert eine `StateForm` (`LAMP_FORM` in `lamps/lampFor.ts`), die Kopien `LAMP_STATE` in `AgentCard.tsx` und `flow/FlowTab.tsx` entfallen; die Agentenkarte zeigt genau eine Zustandsmarke; der Beweisstreifen klappt zu einer Zeile zusammen, wenn alle vier Beweise unbekannt sind; „Braucht dich“ sagt „Nichts offen“ nur einmal | Implementierer · Claude; Root nimmt ab | DS-Blueprint-Schnitt 16.10. (DS-X-13; S8, S9, S17) | Server | B | Erledigt: Entwurfs-PR `claude/ds-x-13`; `Lamp` bleibt für Anbieter und Kontingent; die Sperre `state-token` in `scripts/ci/ratchets.json` bleibt bei 191, weil sie `--state-` in `src/styles.css` zählt und dieses Paket dort nichts berührt | Tests rot→grün; `vitest run src/screens/leitstand`, typecheck, `contrast-check`, `plan-check --offline` und prepush Exit 0; Bilder der Agentenkarten hell und dunkel; ein Stufe-B-Review einer Nicht-Anthropic-Familie | – | | DS-X-14b | Register-Abdeckung gehärtet und Galerie-Routentest (Review-Nachträge zu #1656/#1662): der Abdeckungstest erkennt jede exportierte Designkomponente (`export function`, `export const`, `export default function`), schlägt bei einem nicht erkennbaren Exportstil laut fehl, ohne Zählschwelle, Familie = oberster Ordner unter `src/design`, Pfade portabel verglichen; `isGallery` ignoriert `?query`; Einhänge-Test: `#/designsystem` zeigt die Galerie | Implementierer · Claude; Root nimmt ab | Root-Dispositionen #1656 (R1656-K1/G1, K2, K5/G2, G3) und #1662 (R1662-K1, K2) | Server | B | Erledigt: #1671 (merged 2026-10-17); die schärfere Suche fand keinen fehlenden Registereintrag | Tests rot→grün; `vitest run src/design src/shell`, typecheck, `plan-check --offline` und prepush Exit 0; zwei Stufe-B-Reviews fremder Nicht-Anthropic-Familien | – | | DS-X-07 | Austrittsübergänge: `useExitTransition` (`src/design/motion/`) hält einen schließenden Knoten für `--g-dur-exit` eingehängt und setzt `data-state="closing"`, bei reduzierter Bewegung (System oder Aus) wird sofort ausgehängt; `Sheet` und `Toast` nutzen ihn, `motion.css` trägt die Austrittsregeln; die `.g-is-new`-Einblendung für neue Agentenzeilen bleibt aus (nicht in diesem Paket) | Implementierer · Claude; Root nimmt ab | DS-X-03 (#1606); DS-Blueprint-Schnitt 16.10. (Zeile DS-X-07) | Server | B | In Arbeit: Draft-PR | red-first Vitest-Tests; `vitest run src/design`, typecheck, contrast-check, `plan-check --offline` und prepush Exit 0; ein unabhängiger Stufe-B-Review (Root beauftragt) | – | +| DS-X-21 | Ein Symbolsatz auf 20-px-Raster: `Icon` in `src/design/icons/` (ohne Beschriftung für Hilfstechnik verborgen, mit `label` als `role="img"`, Farbe über `currentColor`) und 24 Lucide-Pfade als Daten (`glyphs.ts`), dazu Entsprechungen für die 14 klassischen Symbole, die Seitenleisten-Symbole und den Auswahl-Pfeil; Lizenztext `LUCIDE-LIZENZ.txt` neben den Symbolen und Eintrag in `docs/THIRD_PARTY_NOTICES.md` | Implementierer · Claude; Root nimmt ab | DS-Blueprint-Schnitt 16.10. (DS-X-21), Root-Entscheidung 17.10.: Pfade kopieren, kein Paket | Server | B + Lizenzprüfung | Entwurfs-PR offen (17.10.): kein Aufrufer umgestellt, `src/components/Icon.tsx` bleibt bis KR-15; Galerie zeigt den Satz unter „Symbole“ | Tests rot→grün; `vitest run src/design`, typecheck, `contrast-check`, `plan-check --offline` und prepush Exit 0; Bilder der Galerie hell und dunkel; ein Stufe-B-Review einer Nicht-Anthropic-Familie, Root prüft den Lizenzhinweis | – | | DS-T-03 | Vertrauensfehler P1-b/e: die Kartenlampe sagt, was bewiesen ist (`lampFor.ts`, `lamps/texts.ts`, `AgentCard.tsx`, `FlowTab.tsx`) | Implementierer · Claude; Root nimmt ab | Lampenform aus DS-T-02 (#1610) | Server | B | Erledigt: #1624 (merged 2026-10-16); unbekannte Spalte oder unbekannter Worker-Status zeigt auf Karte und Fabrik-Knoten die graue Unbekannt-Lampe statt „Läuft“, die grüne Lampe heißt „Test bestanden“ mit dem Alter des Testlaufs; welcher Stand getestet wurde, bleibt ein späteres Nahtpaket | fünf Test-First-Tests rot an der Merge-Base, grün am Kopf (`red-first.sh` Exit 0); typecheck, `node scripts/contrast-check.mjs` und prepush Exit 0; zwei Stufe-B-Reviews verschiedener Nicht-Autor-Familien: Subscription + OpenRouter; Root-Abnahme | – | Historisch (08.10.); Quotenteil ersetzt durch AGENTS.md „Routing“, Reviewweg durch `docs/decisions.md` „Reviewer count per tier“ (beide Stand 15.10.): Routing-Freigabe Nutzer 08.10. ~21:25Z: Codex frei bis tatsächlich 98 % nutzen; From b3c4900d59519fe71d5557a500bb2851953a80cc Mon Sep 17 00:00:00 2001 From: Cuarroc <47299143+Cuarroc@users.noreply.github.com> Date: Sun, 18 Oct 2026 19:38:31 +0200 Subject: [PATCH 4/7] fix(design): restore Gallery default-branch indentation, test the icons page (R1701-ROOT1, R1701-F1) No-Test: adds coverage for existing behaviour (review R1701-F1) Co-Authored-By: Claude Sonnet 5.5 --- src/design/gallery/Gallery.tsx | 10 +++++----- src/design/gallery/gallery.test.tsx | 10 ++++++++++ 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/design/gallery/Gallery.tsx b/src/design/gallery/Gallery.tsx index 2ad65691..f47335f8 100644 --- a/src/design/gallery/Gallery.tsx +++ b/src/design/gallery/Gallery.tsx @@ -140,11 +140,11 @@ function Body({ page }: { page: Page }) { ); case "hall": return ; default: return ( - <> -

{de["gallery.storiesLater"]}

-
    {COMPONENT_REGISTRY.filter((e) => e.family === page).map((e) =>
  • {e.name}
  • )}
- - ); + <> +

{de["gallery.storiesLater"]}

+
    {COMPONENT_REGISTRY.filter((e) => e.family === page).map((e) =>
  • {e.name}
  • )}
+ + ); } } diff --git a/src/design/gallery/gallery.test.tsx b/src/design/gallery/gallery.test.tsx index cf2267b0..e02cb78c 100644 --- a/src/design/gallery/gallery.test.tsx +++ b/src/design/gallery/gallery.test.tsx @@ -4,6 +4,7 @@ import { cleanup, fireEvent, render, screen, within } from "@testing-library/rea import { afterEach, beforeAll, describe, expect, it, vi } from "vitest"; import { de } from "../../i18n/de"; +import { GLYPHS } from "../icons/glyphs"; import { COMPONENT_REGISTRY } from "../registry"; import { Gallery } from "./Gallery"; @@ -35,6 +36,15 @@ describe("design gallery (DS-X-15)", () => { expect(screen.getByText(de["gallery.sample"])).toBeTruthy(); }); + it("the icons page lists every registered glyph", () => { + render(); + fireEvent.click(within(screen.getByRole("navigation", { name: de["gallery.nav"] })).getByRole("button", { name: de["gallery.family.icons"] })); + const cells = document.querySelectorAll(".g-gal-icon"); + expect(cells.length).toBe(Object.keys(GLYPHS).length); + expect(cells.length).toBeGreaterThan(0); + for (const cell of cells) expect(cell.querySelector("svg")).not.toBeNull(); + }); + it("reduced motion stops the motion demo", () => { reduce(true); render(); From 77c3cfbd7d5c6e2935714774a312fa14a7ca70ff Mon Sep 17 00:00:00 2001 From: Cuarroc <47299143+Cuarroc@users.noreply.github.com> Date: Sun, 18 Oct 2026 20:32:47 +0200 Subject: [PATCH 5/7] test(design): require the Lucide notice in the production build output (R1701-L1) Test-First: src/design/icons/icons.notice.test.ts::a Vite build ships the glyph paths together with both copyright notices Co-Authored-By: Claude Sonnet 5.5 --- src/design/icons/icons.notice.test.ts | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/design/icons/icons.notice.test.ts b/src/design/icons/icons.notice.test.ts index ef023a36..ea02e048 100644 --- a/src/design/icons/icons.notice.test.ts +++ b/src/design/icons/icons.notice.test.ts @@ -1,5 +1,7 @@ -import { existsSync, readFileSync } from "node:fs"; -import { resolve } from "node:path"; +import { execFileSync } from "node:child_process"; +import { existsSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join, resolve } from "node:path"; import { describe, expect, it } from "vitest"; const root = resolve(__dirname, "../../.."); @@ -17,3 +19,22 @@ describe("Lucide notice", () => { expect(text).toContain("Cole Bemis"); }); }); + +describe("Lucide notice in the production build (R1701-L1)", () => { + it("a Vite build ships the glyph paths together with both copyright notices", () => { + const out = mkdtempSync(join(tmpdir(), "pa-lucide-")); + try { + execFileSync("npx", ["vite", "build", "--outDir", out, "--emptyOutDir"], { cwd: root, stdio: "pipe" }); + const files = readdirSync(out, { recursive: true, encoding: "utf8" }) + .map((f) => join(out, f)) + .filter((f) => statSync(f).isFile()) + .map((f) => readFileSync(f, "utf8")); + const some = (needle: string) => files.some((c) => c.includes(needle)); + expect(some("M9.671 4.136a2.34")).toBe(true); + expect(some("Lucide Icons and Contributors")).toBe(true); + expect(some("Cole Bemis")).toBe(true); + } finally { + rmSync(out, { recursive: true, force: true }); + } + }, 120_000); +}); From 3b3a3abd0f7ae5ac1d16caace245aff6402c20ec Mon Sep 17 00:00:00 2001 From: Cuarroc <47299143+Cuarroc@users.noreply.github.com> Date: Sun, 18 Oct 2026 20:36:23 +0200 Subject: [PATCH 6/7] fix(design): ship the Lucide ISC/MIT notice in the production build as a legal comment (R1701-L1) Regression-For: 77c3cfbd Co-Authored-By: Claude Sonnet 5.5 --- src/design/icons/glyphs.ts | 47 +++++++++++++++++++++++++++ src/design/icons/icons.notice.test.ts | 2 ++ 2 files changed, 49 insertions(+) diff --git a/src/design/icons/glyphs.ts b/src/design/icons/glyphs.ts index 9e766566..df58624f 100644 --- a/src/design/icons/glyphs.ts +++ b/src/design/icons/glyphs.ts @@ -1,7 +1,54 @@ // DS-X-21: path data copied from Lucide (ISC, with the Feather MIT notice for derived icons). // Notice: LUCIDE-LIZENZ.txt next to this file and docs/THIRD_PARTY_NOTICES.md. Each glyph is // one array of path strings on Lucide's 24-unit artboard (rect, circle and line turned into paths). +// The /*! block is a legal comment: the bundler keeps it, so the built app carries the notice +// (icons.notice.test.ts builds and checks). It must repeat LUCIDE-LIZENZ.txt line for line. +/*! + ISC License + + Copyright (c) 2026 Lucide Icons and Contributors + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + --- + + The following Lucide icons are derived from the Feather project: + + airplay, alert-circle, alert-octagon, alert-triangle, aperture, arrow-down-circle, arrow-down-left, arrow-down-right, arrow-down, arrow-left-circle, arrow-left, arrow-right-circle, arrow-right, arrow-up-circle, arrow-up-left, arrow-up-right, arrow-up, at-sign, calendar, cast, check, chevron-down, chevron-left, chevron-right, chevron-up, chevrons-down, chevrons-left, chevrons-right, chevrons-up, circle, clipboard, clock, code, columns, command, compass, corner-down-left, corner-down-right, corner-left-down, corner-left-up, corner-right-down, corner-right-up, corner-up-left, corner-up-right, crosshair, database, divide-circle, divide-square, dollar-sign, download, external-link, feather, frown, hash, headphones, help-circle, info, italic, key, layout, life-buoy, link-2, link, loader, lock, log-in, log-out, maximize, meh, minimize, minimize-2, minus-circle, minus-square, minus, monitor, moon, more-horizontal, more-vertical, move, music, navigation-2, navigation, octagon, pause-circle, percent, plus-circle, plus-square, plus, power, radio, rss, search, server, share, shopping-bag, sidebar, smartphone, smile, square, table-2, tablet, target, terminal, trash-2, trash, triangle, tv, type, upload, x-circle, x-octagon, x-square, x, zoom-in, zoom-out + + The MIT License (MIT) (for the icons listed above) + + Copyright (c) 2013-present Cole Bemis + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + */ export const GLYPHS = { "panel-left": ["M5 3h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z", "M9 3v18"], "columns-2": ["M5 3h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z", "M12 3v18"], diff --git a/src/design/icons/icons.notice.test.ts b/src/design/icons/icons.notice.test.ts index ea02e048..3520c87d 100644 --- a/src/design/icons/icons.notice.test.ts +++ b/src/design/icons/icons.notice.test.ts @@ -17,6 +17,8 @@ describe("Lucide notice", () => { expect(text).toContain("ISC License"); expect(text).toContain("Lucide Icons and Contributors"); expect(text).toContain("Cole Bemis"); + const glyphs = readFileSync(resolve(root, "src/design/icons/glyphs.ts"), "utf8"); + for (const line of text.split("\n").filter((l) => l.trim())) expect(glyphs).toContain(line); }); }); From 2f0c300d7f5d74bf3a69cca5a351c94dc8683887 Mon Sep 17 00:00:00 2001 From: Cuarroc <47299143+Cuarroc@users.noreply.github.com> Date: Sun, 18 Oct 2026 21:39:53 +0200 Subject: [PATCH 7/7] test(design): start the licence build test via node and vite.js so it runs on Windows (R1701-T1) execFileSync("npx") without a shell cannot start the npx.cmd wrapper on Windows. Run process.execPath with node_modules/vite/bin/vite.js instead. Regression-For: 77c3cfbd7d5c6e2935714774a312fa14a7ca70ff Co-Authored-By: Claude Sonnet 5.5 --- src/design/icons/icons.notice.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/design/icons/icons.notice.test.ts b/src/design/icons/icons.notice.test.ts index 3520c87d..93c86ab3 100644 --- a/src/design/icons/icons.notice.test.ts +++ b/src/design/icons/icons.notice.test.ts @@ -26,7 +26,9 @@ describe("Lucide notice in the production build (R1701-L1)", () => { it("a Vite build ships the glyph paths together with both copyright notices", () => { const out = mkdtempSync(join(tmpdir(), "pa-lucide-")); try { - execFileSync("npx", ["vite", "build", "--outDir", out, "--emptyOutDir"], { cwd: root, stdio: "pipe" }); + // node + vite.js, not npx: without a shell Windows cannot start the npx.cmd wrapper + const vite = resolve(root, "node_modules/vite/bin/vite.js"); + execFileSync(process.execPath, [vite, "build", "--outDir", out, "--emptyOutDir"], { cwd: root, stdio: "pipe" }); const files = readdirSync(out, { recursive: true, encoding: "utf8" }) .map((f) => join(out, f)) .filter((f) => statSync(f).isFile())