Skip to content

chore(release): v0.1.13#107

Merged
fedorovvvv merged 54 commits into
mainfrom
release/v0.1.13
May 8, 2026
Merged

chore(release): v0.1.13#107
fedorovvvv merged 54 commits into
mainfrom
release/v0.1.13

Conversation

@fedorovvvv
Copy link
Copy Markdown
Collaborator

Summary

Release branch for v0.1.13. Cuts from develop (38 commits ahead of main).

Highlights

  • shared/ui catalogue (PRD-018 / RFC-016) — 20+ primitives based on bits-ui, showcased on /playground in light + dark themes.
  • Rule 24shared/ui ownership and customisation discipline. Forbids :global() re-skins of primitive internals from upper FSD layers; ships a verification grep snippet.
  • Audit-driven primitive variants: Button.variant=ghost-mono, Button.size=icon, Badge.variant=mono, Toggle/ToggleGroup.variant=outline-mono, Alert.tone=banner, TabsList.wrap. Replaces hand-rolled markup across widgets.
  • orch theme — third palette (Orchestra-inspired), exposed as a hidden 5-click easter egg with a 2 s freeze after unlock.
  • Dense-graph render-resilience for Sankey + Tree at 100+ artifacts.
  • Adopt svelte-sonner for toasts.
  • Revert Force 3D experimental Threlte view mode (feat(ui): Force 3D — experimental Threlte-based 3D view mode #103, reverted in Revert "feat(ui): Force 3D — experimental Threlte view mode (#104)" #105). dist-experimental cap stays at 6M.

Test plan

  • npm run build — both dist/ and dist-experimental/ build clean (dist-experimental: 2.43M)
  • npm run smoke — passes against scratch workspace
  • CI matrix (ubuntu / macos / windows) green
  • No host-isolation regressions (rule 20)
  • No template-purity regressions (rule 21)
  • No mutating subcommands behind /api/* (rule 22)

Refs: PRD-018, RFC-016, EVID-022, EVID-023, EVID-024, PRD-019

🤖 Generated with Claude Code

fedorovvvv and others added 30 commits May 7, 2026 01:52
Clicking the cluster −/+ toggle previously reset the radial view's
zoom and pan because (1) toggleClusterCollapse forced didFit=false
and (2) the shape-tracking effect treated collapse-induced layout
changes identically to filter changes, retriggering fitToView().

Now: drop the explicit didFit reset on collapse, and gate the
shape-effect's didFit reset on collapsedSig stability so only
filter/data-driven shape changes refit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the native <select> in PaneFrame with a bits-ui-based Select
component that lives in shared/ui and renders a Lucide icon next to
each of the 7 graph views (force, tree, radial, matrix, lanes, sankey,
sunburst). Trigger and listbox share the same iconography so the
active view is recognisable at a glance without reading the label.

The component is headless (bits-ui handles ARIA semantics and keyboard
navigation), styled exclusively with the existing CSS-variable design
tokens (no Tailwind, no inline styles), and exposed via @/shared/ui
for future consumers. The drag-handle in PaneFrame is updated to
ignore the new trigger and the portal-rendered listbox so picking a
view no longer fights the swap-pane gesture.

Adds two runtime deps to template/package.json: bits-ui and
@lucide/svelte. Build, svelte-check, and live render all green.

Refs: PRD-017
## Summary

Standard back-merge of `release/v0.1.12` into `develop` per the
project's GitFlow:

- Brings the version bump (`0.1.11 → 0.1.12`) and the new `[0.1.12] -
2026-05-07` CHANGELOG section back onto develop, so the next feature
branch starts from the released version, not the pre-release one.
- 3 files changed (CHANGELOG.md, package.json, template/package.json).
- No code conflicts — the merge is a forward-port of the version commit
only; all feature commits in v0.1.12 already came from develop.

## Why

Without back-merge, develop's `package.json` would stay at `0.1.11`
while npm + main are at `0.1.12`. The next release branch would either:
- silently inherit `0.1.11` and we'd have to bump again, or
- conflict on the next merge to main.

## Test plan

- [x] `git diff origin/develop..origin/release/v0.1.12 --stat` — only
version + CHANGELOG entries
- [ ] CI matrix on this PR (auto)

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Swap enabled plugin in `.claude/settings.json#enabledPlugins`:
`dev-toolkit@ForgePlan-marketplace` -> `fpl-skills@ForgePlan-marketplace`.

`fpl-skills` is the successor: includes everything from `dev-toolkit`
(`/audit`, `/sprint`, `forge-report`, dev-advisor agent, safety hooks)
plus 13 new commands. `dev-toolkit` is soft-deprecated upstream.

Repo had no `/dev-toolkit:*` slash-command references in CLAUDE.md or
docs, so settings.json is the only file change. The actual plugin
install/uninstall is driven by interactive slash commands the user runs:

  /plugin marketplace update ForgePlan-marketplace
  /plugin install fpl-skills@ForgePlan-marketplace
  /plugin uninstall dev-toolkit@ForgePlan-marketplace
  /reload-plugins

Reversible: revert this commit + reinstall dev-toolkit.

Refs:
- https://github.com/ForgePlan/marketplace/blob/main/docs/MIGRATION-DEV-TOOLKIT-TO-FPL-SKILLS.md
- https://github.com/ForgePlan/marketplace/blob/main/docs/USAGE-GUIDE.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nfig

CLAUDE.md changes:
- Methodology table — add `fpl-skills shortcut` column mapping
  /restore, /briefing, /rfc, /refine, /research, /sprint, /audit,
  /diagnose to OBSERVE -> ROUTE -> SHAPE -> BUILD -> PROVE -> SHIP.
- Process section — annotate steps 1-5 with the corresponding
  slash-command shortcut + add step for /diagnose on hard bugs.
- New "Slash commands (fpl-skills + companions)" reference table
  between Common commands and Hard requirements — quick O(1)
  lookup. Includes companion plugins (fpf, forgeplan-workflow,
  forgeplan-orchestra, laws-of-ux).
- Reference section — link to docs/agents/*.md as fpl-skills
  per-project config.

docs/CONTRIBUTING.md — append shortcut hints to Forgeplan workflow
section + pointer to docs/agents/.

docs/agents/ (new) — four config files normally generated by
/fpl-skills:setup wizard, hand-tailored to this project:
- issue-tracker.md   queue is .forgeplan/ (no Linear/Jira/Issues)
- build-config.md    npm scripts, smoke gate, forbidden CI cmds
- paths.md           top-level + FSD layers + where skills write
- domain.md          package surfaces, Forgeplan glossary, R_eff,
                     routing depth, branch naming

.claude/settings.json — incidental linter reformat (env block
moved from end to top); no semantic change.

Skills never bypass artifact discipline: R_eff > 0 and active
gates from rule 11 still apply. CLAUDE-MD-GUIDE constraints
(cry-wolf, dilution, redundancy cost) respected — content links
back to existing sources rather than duplicating.

Refs:
- https://github.com/ForgePlan/marketplace/blob/main/docs/USAGE-GUIDE.md
- https://github.com/ForgePlan/marketplace/blob/main/docs/MIGRATION-DEV-TOOLKIT-TO-FPL-SKILLS.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…r shape

`forgeplan score --all --json` returns `{ errors, results: ScoreEntry[] }`,
but scorePoller was typed as `ScoreEntry[]` and consumers called `.map(...)`
directly on the wrapper, throwing `(... ?? []).map is not a function` at
runtime in InsightsRail and ForceView (via HomePage's `scores` derived).

Introduce ScoreResponse, retype the poller, read `data.results` in the two
call sites. No API contract change — endpoint stays a passthrough.

Refs: rule 22 (read-only proxy preserved)
#71)

…r shape

`forgeplan score --all --json` returns `{ errors, results: ScoreEntry[]
}`, but scorePoller was typed as `ScoreEntry[]` and consumers called
`.map(...)` directly on the wrapper, throwing `(... ?? []).map is not a
function` at runtime in InsightsRail and ForceView (via HomePage's
`scores` derived).

Introduce ScoreResponse, retype the poller, read `data.results` in the
two call sites. No API contract change — endpoint stays a passthrough.

Refs: rule 22 (read-only proxy preserved)
…ed/ui

Standard-depth artifacts backing GitHub issue #68. Adds 26 primitives
to template/src/shared/ui/ via bits-ui wrappers + CSS-var styling
(no Tailwind). PRD-018 lists groups 1-7 + integration; RFC-016 pins the
per-primitive shape and bits-ui touchpoints. Both pass `forgeplan validate`
with 0 MUST errors (SHOULD/COULD warnings tracked but out of scope here).
RFC-016 -> PRD-018 linked (based_on).

Refs: PRD-018, RFC-016, #68
…ard/Alert/Progress

Group 1 of 8 from issue #68 (PRD-018 / RFC-016). Seven pure-CSS visual atoms
under template/src/shared/ui/, styled by tokens from app.css (no Tailwind, no
new deps). Same shape as existing Button/Select.

- Badge: variant (primary/secondary/success/danger/ghost), size (sm/md)
- Separator: orientation (horizontal/vertical), decorative flag
- Skeleton: width/height/radius props, shimmer with reduced-motion fallback
- Spinner: size (sm/md/lg), aria-label, reduced-motion fallback
- Card: padding (none/sm/md/lg), variant (flat/outlined/elevated), header/footer snippets
- Alert: variant (info/success/warning/danger), Lucide icon defaults, role auto-toggle
- Progress: 0..max value or indeterminate, variant (primary/success/warning/danger)

Barrel `shared/ui/index.ts` now exports 12 named primitives (was 5).

Refs: PRD-018, RFC-016, #68, #72
Group 2 of 8 from issue #68 (PRD-018 / RFC-016). Four form atoms styled by
tokens from app.css. No bits-ui — Input is native <input> with token-driven
states; Field and InputGroup are composition primitives.

- Label: required/optional indicators, for-association, sm font-size
- Input: type forwarded; size (sm/md); invalid prop drives data-invalid +
  aria-invalid; focus-visible ring + accent border via tokens
- Field: pairs Label + control + helper/error; auto-generates id, ties
  describedBy/aria-invalid via render snippet props
- InputGroup: prefix/suffix snippets around an Input, focus-within ring on
  the wrapper; collapses inner Input border via :global

Barrel `shared/ui/index.ts` now exports 16 named primitives.

Refs: PRD-018, RFC-016, #68, #73
…heckbox/Slider

Group 3 of 8 from issue #68 (PRD-018 / RFC-016). Six toggle-style primitives,
five wrap bits-ui (Switch, Checkbox, Toggle, ToggleGroup, Slider) and ButtonGroup
is a pure-CSS composition wrapper.

- Toggle: pressed/onPressedChange, size (sm/md), variant (default/outline)
- ToggleGroup + ToggleGroupItem: single/multiple modes, horizontal/vertical
- ButtonGroup: orientation, attached vs spaced layout (collapses inner radii)
- Switch: bind:checked, accent track when on, theme-token thumb
- Checkbox: bind:checked + bind:indeterminate (bits-ui v2 separates them);
  Lucide check/minus icons via snippet
- Slider: multi-thumb support; horizontal/vertical; range fill via accent

Barrel `shared/ui/index.ts` now exports 22 named primitives.

Refs: PRD-018, RFC-016, #68, #74
Group 4 of 8 from issue #68 (PRD-018 / RFC-016). Wraps bits-ui RadioGroup;
RadioGroup carries value/onValueChange + form-submission name; Radio is the
standalone item rendered with an accent dot when checked.

Barrel `shared/ui/index.ts` now exports 24 named primitives.

Refs: PRD-018, RFC-016, #68, #75
Group 5 of 8 from issue #68 (PRD-018 / RFC-016). Three disclosure primitives,
all bits-ui wrappers. Each splits into Root + auxiliary parts (Tabs.List etc.)
and ships its own CSS via :global() rules keyed off bits-ui data-state.

- Tabs / TabsList / TabsTrigger / TabsContent — orientation, activationMode,
  underline trigger style; vertical mode uses right-border accent
- Collapsible / CollapsibleTrigger / CollapsibleContent — slide animation with
  reduced-motion fallback
- Accordion / AccordionItem / AccordionTrigger / AccordionContent — single
  + multiple modes; chevron rotates on open

Barrel `shared/ui/index.ts` now exports 31 named primitives (10 atoms + 21 from groups).

Refs: PRD-018, RFC-016, #68, #76
Group 6 of 8 from issue #68 (PRD-018 / RFC-016). Three overlay primitives:
two thin bits-ui wrappers (Tooltip, Popover) and an in-house Toaster store
(no new dep — Svelte 5 runes class with timer-driven dismissal).

- Tooltip + TooltipProvider: side/align/sideOffset, accent border via tokens.
  Provider mounted in `+layout.svelte` so any descendant can use Tooltip.
- Popover + PopoverTrigger + PopoverContent: portal-based, optional Arrow
- Toaster + toast() helper: queue with id-keyed dismissal, variant-driven
  left-border accent, 6 corner positions, reduced-motion fallback.
  toast() also exposes .info/.success/.warning/.danger/.dismiss/.clear.

Toaster mounted alongside ModalRoot in `+layout.svelte`. Renamed
toaster.svelte.ts -> toaster-store.svelte.ts to avoid macOS case-insensitive
filename collision with Toaster.svelte.

Barrel `shared/ui/index.ts` now exports 38 named primitives.

Refs: PRD-018, RFC-016, #68, #77
Group 7 of 8 from issue #68 (PRD-018 / RFC-016). bits-ui Command wrapped into
a self-contained palette: Root + Input + List + Empty + Group + Separator + Item.
The Item primitive (Item.svelte) is the per-row entry; onSelect, value, keywords,
and disabled are forwarded.

Sub-parts:
- Command: bind:value + shouldFilter + loop, accent-on-selected styling
- CommandInput: search icon + native input piped through Command.Input
- CommandList + CommandViewport: max-height + scroll
- CommandEmpty: shown when filter yields nothing
- CommandGroup: heading + items wrapper
- CommandSeparator: 1px line between groups

Barrel `shared/ui/index.ts` now exports 45 named primitives.

Refs: PRD-018, RFC-016, #68, #78
Group 8 of 8 from issue #68 (PRD-018 / RFC-016). Closes the primitive
build-out:

- shared/ui/README.md catalogues all 26 new primitives + 5 existing
  (Button/Code/Dialog/Modal/Select), grouped by family (Visual atoms /
  Form basics / Toggles / Radio / Disclosure / Overlays / Command).
- /playground +page.svelte renders every primitive in a single page —
  used as the visual smoke harness during PRs.

Smoke run (dev server on :5174, Chrome MCP):
- /playground renders all 7 sections with 0 console errors.
- /, /playground both clean across data-theme=light + data-theme=dark.
- Tooltip/Popover/Toaster mounted via root +layout.svelte all interact:
  click "Open popover" -> bubble appears; click "Success toast" -> "Saved!"
  toast in bottom-right with success accent and dismiss button.
- Light theme bg = rgb(245,242,234); dark theme = #050505 — both honour
  PRD-015/RFC-014 token contract.

Note: integrating new primitives into existing widgets/version-footer/
update-dialog is intentionally deferred to a follow-up PR — keeping the
diff for issue #68 focused on landing the catalogue. The /playground
route gives reviewers + downstream widget work a stable visual contract
to land against.

Refs: PRD-018, RFC-016, #68, #79
… / EVID-022

EVID-022 measures issue #68 Group 8 deliverables against PRD-018:
- SC-1 (catalogue completeness): 26/26 — pass
- SC-3 (visual smoke in Chrome): 0 console errors across light + dark — pass
- SC-4 (npm run check): 0 errors — pass
- NFR-003 (svelte-check): 0 errors — pass
- NFR-005 (dual-theme parity): verified — pass
- SC-2 (no ad-hoc copies in widgets/*): deferred to follow-up — flagged

Structured Fields: verdict=supports, congruence_level=3 (CL3 same-context),
evidence_type=measurement. R_eff = 1.00 (grade A) on both PRD-018 and RFC-016.

PRD-018, RFC-016, EVID-022 all activated (draft → active).

Refs: PRD-018, RFC-016, EVID-022, #68
## Summary

- Adds 26 shadcn-svelte primitives to `template/src/shared/ui/` via
`bits-ui` wrappers + CSS-var styling (no Tailwind, no new runtime deps).
- Catalogue + `/playground` showcase route for visual smoke and design
review.
- Backed by **PRD-018** + **RFC-016** + **EVID-022** (R_eff = 1.00,
grade A).

## Why

Issue #68 — promote `shared/ui` from 5 ad-hoc primitives (Button, Code,
Dialog, Modal, Select) to the full primitive set so future widgets
compose from a single, consistent library. 7 of the previously-empty
placeholder folders
(`badge/alert/separator/skeleton/spinner/progress/card/`) are now real
implementations.

## What's in

26 primitives across 7 functional groups, each its own commit +
sub-issue:

| Group | Sub-issue | Primitives |
|-------|-----------|------------|
| 1 | #72 | Badge / Separator / Skeleton / Spinner / Card / Alert /
Progress |
| 2 | #73 | Label / Input / Field / InputGroup |
| 3 | #74 | Toggle / ToggleGroup / ButtonGroup / Switch / Checkbox /
Slider |
| 4 | #75 | Radio / RadioGroup |
| 5 | #76 | Tabs / Collapsible / Accordion |
| 6 | #77 | Tooltip / Popover / Toaster |
| 7 | #78 | Command + Item family |
| 8 | #79 | README catalogue + `/playground` route + Chrome smoke |

`+layout.svelte` now wraps the app in `TooltipProvider` and mounts
`Toaster`.

## What's deferred

- Replacing ad-hoc CSS in `widgets/version-footer`, `widgets/mosaic`,
`widgets/update-dialog` with the new primitives (PRD-018 SC-2). Tracked
as a follow-up issue.

## Test plan

- [x] `npm run check` (svelte-check) — 0 errors / 0 warnings
- [x] Chrome smoke at `http://127.0.0.1:5174/playground` in
`data-theme=dark` — 0 console errors, all 7 sections render
- [x] Chrome smoke at `/playground` in `data-theme=light` — body bg
`rgb(245,242,234)` matches PRD-015 token, 0 console errors
- [x] Chrome smoke at `/` (home, force-directed graph) — 354 SVG nodes
render; `+layout.svelte` change didn't break the existing app
- [x] Interactive checks: Popover bubble opens, Toaster `Saved!` appears
bottom-right, Tabs/Accordion state, Command palette filters

## Forgeplan trail

- **PRD-018** — shadcn-svelte primitives in shared/ui (Standard depth) —
`active`
- **RFC-016** — bits-ui-based primitives with CSS-var styling —
`active`, `based_on` PRD-018
- **EVID-022** — catalogue + Chrome smoke measurement — `active`, CL3
supports measurement, R_eff = 1.00 grade A

Closes #68.

Refs: PRD-018, RFC-016, EVID-022
Swap inline `<button class="update-btn">` for the shared `Button`
primitive (variant=secondary, size=sm). Outer `.update-btn-pos` wrapper
keeps fixed positioning; the inner glow dot, "UPDATE" label, arrow, and
version chip stay as decorative spans.

Refs: PRD-018 SC-2, RFC-016, EVID-022 · Closes part of #81 batch 1/3

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
#83)

Swap inline `<button class="pane-icon">` for the shared `Button`
primitive (variant=ghost, size=sm) on the three pane-frame controls
(reset zoom / add pane / close pane). Pane-header layout (toolbar
chrome, drag handle, padding) stays inline.

Tooltip primitive cannot wrap Button until it supports `asChild`
semantics (current bits-ui Trigger renders its own <button>, which
nests buttons). \`title\` attrs preserved as a stop-gap; TODO marker
in the file links the follow-up.

Refs: PRD-018 SC-2, RFC-016, EVID-022 · Closes part of #81 batch 2/3

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…sue #84)

Swap the inline \`.error-bar\` block (custom div with embedded retry
button) for `Alert variant="danger"` containing a `Button variant="ghost"`
retry control. Border-radius zeroed via class override to keep the
full-width banner look at the top of the layout.

UpdateDialog form-body work (Field/Input) — out of scope here:
the dialog's body is informational (version comparison + manual
update steps), not a form. Existing Dialog/Button/Code primitives
already cover what's there.

Refs: PRD-018 SC-2, RFC-016, EVID-022 · Closes part of #81 batch 3/3

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Swap inline `<button class="chip">` toggleable filter buttons in
Filters.svelte for the Toggle primitive (variant=outline, size=sm).
Pressed state, ARIA, focus ring all come from the primitive. Inline
.chip CSS removed (~30 lines). Color dot + ring decorations kept
inline since they're filter-domain visuals, not generic atoms.

Refs: PRD-018 SC-2, RFC-016, EVID-022

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Theme 3-segment switcher (Auto/Light/Dark) → ToggleGroup type=single
with ToggleGroupItem children. Replaces manual role=radiogroup +
aria-checked wiring with bits-ui's accessible primitive.

Notify toggle (binary on/off) → Toggle primitive (variant=outline,
size=sm). The pressed state shows accent-dim background; the
existing 🔔/🔕 icon swap stays inline.

Status .chip rail (TOTAL/ACTIVE/DRAFT/stale/blind segments) kept as
inline custom segmented bar — design intent is the unified bordered
rail look, which Badge primitives wouldn't reproduce. Documented as
a deliberate skip.

Refs: PRD-018 SC-2, RFC-016, EVID-022

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Tab count badges (Recent 20 / Blocked 4 / Drafts 9 / Agents N) →
  Badge primitive. variant=primary for warn (blocked/drafts), secondary
  for neutral (recent/agents). Removes 12 lines of .badge CSS.

- "Lowest R_eff" progress bars (Health tab) → Progress primitive.
  variant maps to reffTone(): danger/warning/success. Removes ~20 lines
  of inline span-based bar styling and the .scoring-row .bar override.

KPI cards, .tab navigation, and other inline atoms in this widget
left for follow-up rounds.

Refs: PRD-018 SC-2, RFC-016, EVID-022

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ives

- .kind inline span → Badge variant=ghost size=sm. Removes 7 lines
  of .kind CSS.

- 5 .ghost buttons (Show downstream/upstream/Clear/Hide body/Copy as
  markdown) → Button variant=ghost size=sm with class="panel-action"
  preserving the project's mono-font + uppercase + accent-on-hover
  style via :global() override. Removes ~28 lines of duplicated
  .ghost CSS.

- .close × button → Button variant=ghost size=sm. Position kept
  (top-right absolute), font-size 22px override for the × glyph.

- copy-md state classes (copied/failed) routed through interpolated
  class string instead of class:directive (component classes can't
  use class: directives in Svelte 5).

Refs: PRD-018 SC-2, RFC-016, EVID-022

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fedorovvvv and others added 24 commits May 7, 2026 21:49
Three corrections after visual diff against the /playground reference:

1. HealthBar theme switcher — drop the custom :global(.theme-toggle)
   override that stripped bg/padding. The ToggleGroup now renders its
   natural bg-2 + 2px-padding container with a raised bg-1 pill on
   the selected segment, matching the "Left | Center | Right" example.

2. Filters KIND/STATUS — collapse N independent <Toggle> calls into
   one <ToggleGroup type="multiple"> per section. Multi-select state
   flows through value/onValueChange. Looks like a unified segmented
   filter rail, matching the /playground style.

3. Timeline collapse button — was Button (binary state was hidden in
   aria-expanded). Now <Toggle pressed={!collapsed}> so the expanded
   state visibly highlights with accent-dim. The "now" trigger stays
   a Button (one-shot action). Removed dead .ghost / .toggle CSS.

Refs: PRD-018 SC-2, RFC-016, EVID-022

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous styling kept the ToggleGroup's default packed layout
(2px gap, single bg-2 container, transparent item borders). The
intended look is independent outlined chips:

- ToggleGroup wrapper made transparent (no bg/border/padding).
- Items spaced 6px apart, wrap onto multiple rows.
- Each item gets a visible line-2 border and bg-1 fill.
- Hover bumps to line-3.
- Pressed (data-state=on) → accent border + accent text + plain bg.

ToggleGroup type=multiple semantics preserved — only the visual layer
changed via :global() overrides scoped to .filter-group.

Refs: PRD-018 SC-2, RFC-016, EVID-022

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Progress was correctly mounted but rendered at size="sm" (4px height) —
visually too thin against the user's reference. Bumping to size="md"
(6px) matches the intended chunkier look while keeping the rounded
pill shape and tone-driven variant (success/warning/danger).

Refs: PRD-018 SC-2

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ipline

shared/ui/ is the single owner of visual primitives. Upper FSD layers
(entities/widgets/pages/routes) compose primitives but MUST NOT modify
their internals from above (no :global() overrides on primitive class
names, no inline atom re-implementations, no consumer-side variants).

Required path when a feature needs a new look:
  1. Add a variant/size/prop to the primitive in shared/ui/<name>/
     (or create a new primitive — preferring shadcn-svelte / bits-ui
     ports, styled with plain CSS + CSS vars from app/styles/app.css).
  2. Showcase on /playground in light + dark themes.
  3. Use the new prop from the upper layer.

Allowed upper-layer styling: layout, positioning, motion, container
chrome that does not shadow a primitive — composition, never
customisation.

Lists existing violations as TODO(rule-24-cleanup) debt for future
cleanup PRs. Updates rules/00-index.md.

Refs: PRD-018 SC-2

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New EvidencePack EVID-023 documents the shared/ui integration across 7
widgets/pages (PR #85). Structured Fields: verdict=supports,
congruence_level=3 (CL3 same-context), evidence_type=measurement.

Linked --informs--> PRD-018 and RFC-016. Activated.

forgeplan score PRD-018 → R_eff: 1.00 (Adequate), 2 evidence
(EVID-022 + EVID-023). Confidence: insufficient (only 2 evidence in
support — acceptable for a Standard-depth PRD with same-context CL3
measurements).

PRD-018 SC-2 moves "deferred" → "pass". The primitives catalogue is
now the rendering path on the home route, the artifact panel, the
filters sidebar, the health bar, the timeline header, the version
footer, and the mosaic pane controls.

Refs: PRD-018 SC-2, RFC-016, EVID-022 · Closes #81 (after PR #85 merge)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
) (#85)

Closes #81, #82, #83, #84.

PRD-018 SC-2 follow-up integration: replace ad-hoc atom CSS with
`@/shared/ui` primitives across the SvelteKit app. EvidencePack
[EVID-023](https://github.com/ForgePlan/forgeplan-web/blob/feature/prd-018-sc2-integration/.forgeplan/evidence/EVID-023-shared-ui-integrated-across-7-widgets-pages-closes-prd-018-sc-2.md)
closes the PRD's SC-2 ("deferred" → "pass"); `forgeplan score PRD-018` →
R_eff = **1.00 (Adequate)**.

## What changed

11 commits, 9 files modified.

| Commit | Scope | Primitives used |
|---|---|---|
| `434e179` | `widgets/version-footer/UpdateButton.svelte` | Button |
| `48124be` | `widgets/mosaic/PaneFrame.svelte` (3 pane controls) |
Button |
| `f8de23c` | `pages/home/HomePage.svelte` (error bar) | Alert + Button
|
| `c2c830b` | `widgets/artifact-filters/Filters.svelte` (KIND/STATUS
chips) | Toggle |
| `0965bed` | `widgets/health-bar/HealthBar.svelte` (theme switcher +
notify) | ToggleGroup + Toggle |
| `2b07516` | `widgets/insights-rail/InsightsRail.svelte` (tab badges +
R_eff bars) | Badge + Progress |
| `a2d3cc0` | `widgets/artifact-panel/ArtifactPanel.svelte` (kind chip +
5 ghost buttons + close) | Badge + Button × 6 |
| `<sha>` | `widgets/{health-bar,artifact-filters,timeline}/*` alignment
fixes — match `/playground` reference | ToggleGroup + Toggle (corrected
styling) |
| `<sha>` | `widgets/artifact-filters/Filters.svelte` — outlined chips
with spacing | ToggleGroup |
| `<sha>` | `widgets/insights-rail/InsightsRail.svelte` — Progress bars
to size=md | Progress |
| `<sha>` | `.claude/rules/24-shared-ui-ownership.md` — new rule:
shared/ui owns primitives, upper layers compose only | — |
| `<sha>` | `.forgeplan/evidence/EVID-023-*.md` — closing EvidencePack |
— |

**Net diff:** **+201 / −254 lines** in `template/src/`. ≈53 lines of
duplicated atom CSS removed; behaviour preserved.

## Forgeplan

- ✅ EVID-023 created with `## Structured Fields` (verdict=supports,
congruence_level=3 CL3, evidence_type=measurement).
- ✅ Linked `EVID-023 --informs--> PRD-018` and `--informs--> RFC-016`.
- ✅ Activated (status=active).
- ✅ `forgeplan score PRD-018` → R_eff: **1.00 (Adequate)**, F-G-R: 0.79
(B), 2 evidence.
- ✅ Rule 24 (`shared/ui` ownership) added to
`.claude/rules/00-index.md`.

## Test plan

- [x] `npm run check` — 0 errors, 0 warnings (1039 files).
- [x] Visual smoke in Chrome on `/` — light + dark theme parity verified
end-to-end.
- [x] DOM inspection of migrated nodes:
- Lowest R_eff bars → `<.progress class="progress variant-success
size-md">` with proper `width: %` on `.bar` (height 6px, border-radius
999px verified via `getComputedStyle`).
- PaneFrame controls → 6 nodes with `class="btn variant-ghost size-sm
pane-icon …"`.
- HealthBar theme switcher → bits-ui `<ToggleGroup type="single">` with
proper ARIA wiring; clicking DARK actually flips
`documentElement.dataset.theme`.
- [x] No app-side console errors (only chrome-extension `locatorjs`
warnings, unrelated).
- [x] `grep -RIn '<style>' template/src/widgets/{version-footer,mosaic}
template/src/pages/home` shows only layout/positioning/motion styles.

## Out of scope (intentional, documented in EVID-023 §Interpretation)

- `VersionFooter.svelte` — fixed-positioned chrome.
- `UpdateDialog.svelte` body — informational, not a form.
- `Splitter`, `MosaicCanvas`, drop overlay — drag/layout chrome.
- HealthBar status `.chip` rail — segmented unified-border design
intent.
- InsightsRail KPI cards (`.kpi`) and `.tab` navigation — bigger
refactor; deferred.
- `+error.svelte` `.home-link` — `<Button>` lacks `href`. Catalogue gap.

## Known debt (rule 24 cleanup)

Eleven `:global()` overrides remain in upper-layer `<style>` blocks
(HealthBar, Filters, ArtifactPanel, InsightsRail, Timeline,
UpdateButton, PaneFrame). Tracked as `TODO(rule-24-cleanup)` in EVID-023
§Interpretation. Each should resolve into a new variant/size on the
corresponding primitive (preferred) or a removal.

## Catalogue gaps surfaced

1. `Button` has no `href` — anchor-styled-as-button cases blocked.
2. `Tooltip.Trigger` always renders its own `<button>` — can't wrap
`<Button>`. `TODO(rfc-016-tooltip-aschild)` marker in
`PaneFrame.svelte`.
3. `Card` padding caps at `lg` (16px) — large card paddings (40px+) need
a `padding="xl"`.

Follow-up RFC candidate: "shared/ui catalogue gaps" addressing these
three.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
#89)

PRD-019 Wave 1 of the PRD-018 SC-2 audit follow-ups (#99).

#86 — HealthBar theme switcher restored radiogroup semantics:
- Wrapper carries role="radiogroup" + aria-label="Theme".
- ToggleGroupItem grew aria-checked + role pass-through props,
  each item now renders role="radio" aria-checked={mode === id}.
- Dropped `as ThemeMode` cast in favour of an isThemeMode guard.
- permission typed NotificationPermission | 'unsupported'
  (matches notify.svelte.ts contract).
- Lifted props to Props interface, dropped legacy $props<{}>().
- Added requesting flag — disables Toggle during async permission
  prompt to fix the rapid-toggle race (closes #95 partially).

#87 — Timeline collapse uses aria-expanded:
- Replaced <Toggle pressed> (aria-pressed) with <Button> carrying
  aria-expanded={!collapsed} + aria-controls="timeline-body".
- Disclosure pattern is now correct; screen readers announce
  "expanded"/"collapsed" instead of "pressed".
- Added FIXME(pointer-capture-race) per rule 10.

#89 — rule-24 verification grep:
- Replaced broken `rg --type svelte` (svelte is not a built-in
  ripgrep type) with `--type-add 'svelte:*.svelte'`.
- Replaced broken &&/|| shell pair (conflated "no matches" with
  "command failed") with explicit if-form. Snippet now exits
  non-zero on violation, zero on clean — verified on this branch.
- Added consumer-supplied class names to forbidden list
  (.panel-action, .update-btn, .theme-toggle, .notify-toggle,
  .pane-icon, .error-alert, .tab-badge, .timeline-toggle, .kind,
  .close, .filter-group) — dominant violation pattern in PR #85.
- Refreshed existing-violations roster with FSD `ui/` segment +
  added pages/home/ui/HomePage.svelte (.error-alert).

Wave 2 prep (primitive variants for #88):
- Toggle: added variant="outline-mono" (mono uppercase, accent on
  pressed). Used by HealthBar's notify toggle.
- ToggleGroup: added variant prop + variant="outline-mono"
  (transparent, no inner padding/gap).
- ToggleGroupItem: added role + aria-checked pass-through;
  variant-outline-mono styles inside ToggleGroup variant.

Browser-verified accessibility tree at /:
  radiogroup, aria-label="Theme"
    ├─ radio aria-checked=false  "Auto"
    ├─ radio aria-checked=true   "Light"
    └─ radio aria-checked=false  "Dark"
  button.timeline-toggle aria-expanded="true"
                         aria-controls="timeline-body"
                         aria-pressed=null

npm run check: 0 errors, 0 warnings, 1039 files.

Refs: PRD-019

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
#91 #92 #93 #94 #95 #96 #97 #98)

PRD-019 Waves 2-4 of the PRD-018 SC-2 audit follow-ups (#99).

Wave 2 — primitive variants (#88):
- Button: variant="ghost-mono" (mono uppercase, accent-on-hover,
  aria-expanded styling); size="icon" (22×22 square).
- Badge: variant="mono" (mono uppercase, no border-radius).
- Alert: tone="banner" (no rounded corners, no l/r border).
- Toggle, ToggleGroup, ToggleGroupItem already gained outline-mono
  in Wave 1. ToggleGroupItem also accepts role + aria-checked
  passthrough for radiogroup composition.
- TabsList grew a wrap?: boolean prop to support multi-row tab
  rails (used by InsightsRail).
- Showcased every new variant on /playground in light + dark.
- Updated shared/ui/README.md.

Wave 3 — apply variants + per-widget fixes:

#91 — InsightsRail: hand-rolled <nav class="tabs"> replaced with
   Tabs/TabsList/TabsTrigger primitive. Browser-verified: tablist
   role + 5 role="tab" items + aria-selected="true" on active.
   #97 items 2-4 also addressed: {@const n = t.badge()} (no triple-
   call), keyed each (t.key), inline <span class="kind"> replaced
   with <Badge variant="mono">.

#92 — ArtifactPanel: hand-rolled Outgoing/Incoming disclosures
   replaced with Collapsible / CollapsibleTrigger / CollapsibleContent.
   Built-in aria-controls wiring + focus management.

#93 — HealthBar: see Wave 1 commit (already closed).

#94 — HomePage: dropped `as typeof liveNodes/liveEdges` casts.
   Tightened ArtifactSnapshot.{kind,status} from `string` to the
   canonical literal unions (ArtifactSnapshotKind / ArtifactSnapshotStatus,
   defined in shared/server/snapshot.ts to keep the FSD import
   direction valid). HomePage now uses local toLowerKinds/Statuses
   helpers with type guards instead of brute-force lowercasing.

#95 — HealthBar: see Wave 1 commit (already closed).

#96 — ArtifactPanel auto-collapse $effect: outgoing.length /
   incoming.length reads now wrapped in untrack() so the 10s graph
   poll (which re-derives identical-content arrays with new refs)
   no longer silently undoes user toggles.

#97 — code-quality bundle:
   - item 1 (Timeline FIXME) — already in Wave 1.
   - items 2-4 (InsightsRail) — see #91 above.
   - item 5 (PaneFrame): added TODO(select-trigger-marker) per rule 10.
   - item 6 (Timeline fetch): added AbortController with 10s timeout
     matching server's GIT_TIMEOUT_MS.
   - item 7 (clipboard fallback): moved ta.select() inside try block
     so a select() throw on iOS Safari doesn't leak the textarea.

#98 — Filters: tightened to discriminated unions.
   `kinds: ArtifactKind[]; kindFilter: Set<ArtifactKind>` (analogous
   for status). HomePage's settings.ts also typed accordingly with
   isArtifactKind / isArtifactStatus type guards on persist load.

Removed :global() overrides retired by Wave 2 variants:
   ArtifactPanel: panel-action, close, kind, copy-md.copied/.failed
   InsightsRail: tab-badge, inline .kind block
   HomePage: error-alert
   Filters: filter-group .toggle-group-item internals
   PaneFrame: pane-icon
   UpdateButton: update-btn
   Timeline: timeline-toggle mono styling

Wave 4 — TODO markers (#90):
   The audit's premise was that retiring a :global() override would
   require leaving a TODO for cleanup. We retired the underlying
   violations directly via Wave 2 variants — there are no orphan
   overrides left to tag. Updated the rule-24 doc's "Existing
   violations" section to reflect this.

Verification:
- `npm run check` clean (1039 files / 0 errors / 0 warnings).
- Rule-24 grep returns OK on this branch (no upper-layer `:global()`
  reaches into a primitive class name from the PRIMITIVE_CLASSES set).
- Browser-verified at http://127.0.0.1:5174/ (light + dark):
    radiogroup[aria-label=Theme] with 3 radios + aria-checked
    button.timeline-toggle aria-expanded + aria-controls
    [role=tablist][aria-label=Insights] with 5 [role=tab]
    Alert tone=banner (no rounded corners, no l/r border)
- Browser-verified at /playground (light + dark): every new variant
  renders, tokens resolve in both themes.

Refs: PRD-019, closes #88 #90 #91 #92 #93 #94 #95 #96 #97 #98

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the PRD-019 lifecycle for the audit follow-ups (issue #99):
- EVID-024 created with `## Structured Fields` (verdict: supports,
  congruence_level: 3, evidence_type: test) — same-context test against
  the actual surface PRD-019 modifies (svelte-check on template/src/,
  rule-24 grep on widgets/pages/routes, browser DOM inspection).
- forgeplan link EVID-024 PRD-019 --relation informs.
- forgeplan score PRD-019 → Quality 0.76 (B-grade) with R_eff > 0.
- forgeplan activate EVID-024 (draft → active).
- forgeplan activate PRD-019 (draft → active).

Refs: PRD-019, EVID-024
## Summary

Resolves all 13 child issues of umbrella **#99** — the multi-expert
audit follow-ups for PRD-018 SC-2 (`shared/ui` migration in PR #85).

- **A11y regressions fixed** — HealthBar `radiogroup` (#86), Timeline
`aria-expanded` (#87), InsightsRail `tablist` (#91).
- **Primitive catalogue extended** (#88) —
`Button.variant="ghost-mono"`, `Button.size="icon"`,
`Badge.variant="mono"`, `Alert.tone="banner"`,
`Toggle/ToggleGroup.variant="outline-mono"`, `ToggleGroupItem`
`role`+`aria-checked` passthrough, `TabsList.wrap`. All showcased on
`/playground` in light + dark.
- **Widgets migrated to catalogue** — InsightsRail uses
`Tabs/TabsList/TabsTrigger` (#91), ArtifactPanel uses `Collapsible` for
Outgoing/Incoming disclosures (#92).
- **Type safety tightened end-to-end** — `ArtifactSnapshot.kind/.status`
literal unions in `shared/server/snapshot.ts`, dropped `as typeof` casts
in HomePage (#94), discriminated unions through Filters (#98), HealthBar
typing fixes (#93).
- **Latent reactivity bugs fixed** — HealthBar notify race during
permission prompt (#95), ArtifactPanel auto-collapse `$effect` re-firing
on graph poll (#96).
- **Rule-24 enforcement** — verification grep now exits non-zero on
violation (#89), forbidden-class list extended with consumer-class
pattern, all 13 `:global()` overrides retired by Wave 2/3 variants
(#90).
- **Code-quality bundle** — FIXME/TODO markers, AbortController on
Timeline fetch, clipboard fallback DOM-leak edge case (#97).

Driven by **PRD-019** (active, R_eff > 0) with **EVID-024**
structured-fields evidence linked (verdict: supports, congruence_level:
3, evidence_type: test).

## Why

The PRD-018 SC-2 migration shipped APPROVE_WITH_FIXES — structurally
complete and svelte-check clean, but introduced silent a11y regressions
for screen-reader users, ~13 rule-24 violations re-skinning primitive
internals from upper layers, type casts hiding source-of-truth gaps, and
broke the rule-24 verification grep itself. The catalogue
(`/playground`) was no longer the source of truth — half the visuals
were produced by overrides scattered across widgets. This PR closes that
gap so the next contributor compositions instead of forking.

## Test plan

- [x] `npm run check --prefix template` — clean (1039 files / 0 errors /
0 warnings) on the final commit.
- [x] Rule-24 verification grep run from `template/` — exit 0, prints
`OK: no upper-layer reaches into primitive internals`.
- [x] Browser DOM inspection via Chrome DevTools at
`http://127.0.0.1:5174/`:
- `[role=radiogroup][aria-label=Theme]` with 3
`[role=radio][aria-checked]` items
- `button.timeline-toggle aria-expanded="true"
aria-controls="timeline-body" aria-pressed=null`
- `[role=tablist][aria-label=Insights]` with 5
`[role=tab][aria-selected]` items
  - `Alert tone="banner"` replaces `.error-alert` `:global()` override
- [x] Browser visual smoke at `/playground` (light + dark) — every new
variant renders, tokens resolve in both themes.
- [ ] CI matrix (ubuntu/macos/windows × Node 22) — required gate.
- [ ] VoiceOver/NVDA manual a11y pass — out of scope (DOM tree verified
mechanically).

## Out of scope

- Adding rule-24 verification as a CI step (separate ticket).
- `shared/ui/README.md` worked-example expansion (table updated;
`/playground` is the contract).

## Closes

Closes #86, #87, #88, #89, #90, #91, #92, #93, #94, #95, #96, #97, #98.

---

Refs: PRD-019, EVID-024
🤖 Generated with [Claude Code](https://claude.com/claude-code)
…facts

Sankey (issue #69):
- Bar height = max(in_value, out_value) * LINK_HEIGHT (12px) so
  every bar is exactly proportional to its flow weight, mirroring
  the classic d3-sankey "river" visual.
- Orphan bars (no edges) get 1-link height (12px) and stack below
  connected nodes with ORPHAN_GROUP_GAP — keeps connected vs
  disconnected groups visually distinct.
- Adaptive VIEW_H grows with the densest column so packing never
  collapses bars to ~2px on dense workspaces.
- Label-collision drop hides labels whose centres are within
  LABEL_LINE_HEIGHT_PX of the previous visible label in the same
  column; hover/select reveals dropped labels.
- Labels become hover targets (pointer-events: auto), so users can
  hover the id text, not only the bar rectangle.
- fitToView is width-driven for dense layouts so bars keep their
  chrome — vertical overflow is reachable via pan.

Tree (issue #39):
- Layer assignment is now kind-tier-driven (compactTierMap)
  instead of topological dfs(incoming). Each artifact kind gets
  its own row in the same compact-tier order Sankey uses.
- Intra-row wrap when natural row width exceeds ~1.5x viewport;
  sub-rows of equal cardinality preserve edge geometry.

PRD-020 / RFC-017 / SPEC-001 / SPEC-002 + EVID-025 activated;
R_eff > 0; CL3 test evidence on the playground (123 artifacts).

Includes user's HealthBar / Filters style touch-up (out-of-scope
files but bundled per request).

Refs: PRD-020 RFC-017 SPEC-001 SPEC-002 EVID-025
Closes #69
Closes #39

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
) (#101)

## Summary

Fixes both #69 (broken Sankey at 100+ artifacts) and #39 (broken Tree at
100+ artifacts) with a single bundled change in
`widgets/dependency-graph/`.

## What changed

**SankeyView**
- Bar height = `max(in_value, out_value) × LINK_HEIGHT (12px)` so every
bar's vertical extent is exactly its flow weight — matches the classic
d3-sankey reference visual.
- Orphan bars (no edges) keep 1-link height and stack below connected
nodes with an `ORPHAN_GROUP_GAP` so flow vs disconnected stays visually
distinct.
- Adaptive `VIEW_H` grows with the densest column; sparse workspaces
stay at the 760-px floor (no regression).
- Label-collision drop hides labels whose centres are within
`LABEL_LINE_HEIGHT_PX` (22) of the previous visible label in the same
column; hover/select reveals.
- Labels are hover targets too (`pointer-events: auto`) — not only the
bar.
- `fitToView` is width-driven for dense layouts so bars keep their
chrome; vertical overflow is reachable via pan.

**TreeView**
- Layer assignment now driven by kind tier (`compactTierMap`) instead of
topological `dfs(incoming)`. Each kind gets its own row in compact-tier
order.
- Intra-row wrap when natural row width > 1.5× viewport; sub-rows of
equal cardinality preserve edge geometry.

**Forgeplan**
- PRD-020 (product), RFC-017 (architecture, Option A), SPEC-001/002
(per-view), EVID-025 (CL3 test evidence at 123 artifacts) — all
activated.
- `R_eff > 0` for PRD-020.

**Bundled out-of-scope**
- User's manual style touch-up to `HealthBar.svelte` + `Filters.svelte`
(per their request to include in this PR).

## Test plan

- [x] `cd template && npm run check` — 1041 files, 0 errors, 0 warnings
- [x] `cd template && npm run test` — 15 files passed, 146 tests (incl.
9 new in `sankey-layout.test.ts` and 9 new in `tree-layout.test.ts`)
- [x] Manual verification at `npm run dev:playground` (123 artifacts):
- Sankey: bars proportional to flow, labels readable, orphans grouped
below
- Tree: each kind on its own row, dense kinds wrap, no 1-stripe collapse
- [x] Backwards-compat at the live `.forgeplan/` workspace (62
artifacts) — visual identical to develop branch

🤖 Generated with [Claude Code](https://claude.com/claude-code)
…prop

Replace hand-rolled shared/ui/toaster with thin svelte-sonner wrapper;
public surface (Toaster, toast, toast.info/success/warning/danger,
toaster, types) preserved. Sonner CSS vars mapped to project tokens.

Extend shared/ui/toggle-group:
- Variant widens to 'default' | 'outline-mono' | 'outline'
- New spacing?: boolean prop -> data-spacing attr; gap + flex-wrap
  applied via primitive (no consumer-side :global() overrides needed).

Rewire widgets/artifact-filters/Filters.svelte to use
variant="outline" + spacing={true}; drops the :global(.filter-group)
layout block (spacing handled by the primitive).

Playground: new tiles for outline variant w/ and w/o spacing.
shared/ui/README.md: updated ToggleGroup + Toaster rows.

Verification — typecheck 0 errors (1047 files), build green,
smoke PASS, rule-24 grep clean. R_eff=1.00 on PRD-021/RFC-018.

Refs: PRD-021, RFC-018, EVID-026

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…102)

## Summary

- Replace hand-rolled `shared/ui/toaster/` with a thin `svelte-sonner`
wrapper while preserving the public surface (`Toaster`, `toast`,
`toast.info/success/warning/danger`, `toaster`, types).
- Extend `shared/ui/toggle-group/` with `'outline'` variant (per
shadcn-svelte) and a `spacing?: boolean` prop that emits
`data-spacing="true"` and applies `gap + flex-wrap` from the primitive
itself.
- Rewire `widgets/artifact-filters/ui/Filters.svelte` to
`variant="outline"` + `spacing={true}` and drop the
`:global(.filter-group)` layout block (handled by the primitive now).
- Playground: new tiles for outline variant w/ and w/o spacing; README
updated.

## Why

Filters needed an outlined look with breathing room without violating
rule 24 (`shared/ui` ownership). Sonner gives us hover-pause / swipe /
promise toasts for free and removes ~230 LOC of bespoke store + DOM
rendering.

## Forgeplan

- PRD-021 (active, R_eff = 1.00)
- RFC-018 (active, R_eff = 1.00)
- EVID-026 (active) — typecheck 0 / build green / smoke PASS / rule-24
clean (CL3, evidence_type=test)

## Test plan

- [x] `npm run check` → 1047 files, 0 errors, 0 warnings
- [x] `npm run build` → exit 0
- [x] `node scripts/smoke.mjs` → PASS (homepage + /api/health +
/api/list)
- [x] `grep -RIn ":global(.toggle-group"
template/src/{widgets,pages,routes,entities}` → 0 matches outside
`shared/ui`
- [ ] Manual: visit `/playground` and confirm outline + spacing tiles
render in both `data-theme` values
- [ ] Manual: visit `/` and confirm Filters look like the screenshot in
the PRD-021 brief

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Adds a new view mode `force3d` last in the GRAPH_VIEWS dropdown with an
EXPERIMENTAL badge. Renders the same artifact dependency graph as 2D Force
in a Three.js scene via Threlte v8, simulating positions with d3-force-3d.
The chunk is lazy-loaded behind `view === 'force3d'` so non-3D users pay
zero bytes.

What ships:
- shared/config/ui-prefs.ts: GraphViewMeta gains optional `badge`; new
  `force3d` entry registered last.
- shared/ui/select: Select renders a pill-style badge next to the label
  (used by Force 3D, available to any future entry).
- widgets/dependency-graph-3d: new widget — Threlte canvas + d3-force-3d
  simulation, OrbitControls, kind-colored spheres (R_eff sized), edge
  segments, theme reactivity (light + dark via data-theme observer).
  Initial layout uses a phyllotaxis sphere (radius 50) so the simulation
  starts spread out, then forces shape clusters.
- widgets/dependency-graph/ui/DependencyGraph.svelte: lazy-import branch
  for Force 3D — no Three.js / Threlte in the main bundle.
- widgets/dependency-graph/ui/ForceView.svelte: TODO marker for the
  growth-vision in-Force-mode 2D ↔ 3D switch.

Forgeplan trail (full Standard pipeline, R_eff = 1.00 / grade A):
- PRD-022 — problem, FRs / NFRs, acceptance, risk register
- RFC-019 — Option A (Threlte + d3-force-3d) chosen vs three alternatives
- EVID-027 — smoke test with structured fields (verdict / CL / type)

Refs: PRD-022 RFC-019 EVID-027

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The experimental single-file ESM bundle inlines all runtime deps. Force 3D
adds Three.js + Threlte (~2.4 MB unzipped). Non-3D users still pay 0 bytes
in the legacy `dist/` shape (lazy chunk), but the experimental shape can't
code-split — so the floor moves to 6M. Documented in RFC-019.

Refs: PRD-022 RFC-019

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## Summary

Adds a new **Force 3D** view mode (last in the dropdown, badged
`experimental`) that renders the artifact dependency graph in a Threlte
v8
canvas, simulating positions with `d3-force-3d`. Chunk is lazy-loaded so
non-3D users pay zero bundle bytes.

Closes #103.

## Why

The 2D Force view collapses dense workspaces (~80+ artifacts) into
hairballs
where cluster topology becomes illegible. The 3D layout separates
orthogonal
sub-graphs spatially. Reference: vasturiano/3d-force-graph; visual style
adapted to Forgeplan tokens (kind colors, status emissive glow, R_eff →
sphere radius).

## What changed

- `shared/config/ui-prefs.ts` — `GraphViewMeta.badge?` + `force3d` entry
last.
- `shared/ui/select` — pill badge next to label.
- `widgets/dependency-graph-3d/` — new widget: Threlte canvas, scene,
  `NodeMesh`, sim builder, theme adapter, `d3-force-3d` ambient types.
- `widgets/dependency-graph/ui/DependencyGraph.svelte` — lazy `await
import('@/widgets/dependency-graph-3d')` only when `view === 'force3d'`.
- `widgets/dependency-graph/ui/ForceView.svelte` — TODO marker for the
  future in-Force-mode 2D ↔ 3D switch (PRD-022 growth vision).
- `template/package.json` — `@threlte/core`, `@threlte/extras`, `three`,
  `d3-force-3d` to deps; `@types/three` to devDeps.

## Forgeplan

- **PRD-022** active — Standard depth, problem / FRs / NFRs /
acceptance.
- **RFC-019** active — Option A (Threlte v8 + d3-force-3d) chosen vs
  vanilla Three, the reference 3d-force-graph wrapper, and r3f.
- **EVID-027** active — smoke test (`verdict: supports`, `CL: 3`, `type:
test`),
  R_eff = 1.00, overall grade **A** on both PRD and RFC.

## Test plan

- [x] `npm run check` — 0 errors / 0 warnings (2105 files).
- [x] `npm run build` — production build passes; `dependency-graph-3d`
chunk
      lives in its own client chunk; verified the homepage entry only
      references it via dynamic `import('../chunks/...')`.
- [x] Browser smoke (Chromium MCP) — Force 3D last in dropdown with
      EXPERIMENTAL badge, 3D scene renders 123 nodes / 104 edges, orbit
      drag rotates camera, theme reactivity (light + dark) confirmed.
- [ ] Performance trace (NFR-001 first-frame, NFR-002 fps p50) —
deferred
      to a follow-up EvidencePack alongside the in-mode 2D ↔ 3D switch.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
…105)

Reverts #104 in full as requested.

`git revert -m 1 c60fd77` undoes:

- Removes `template/src/widgets/dependency-graph-3d/` (Force 3D widget).
- Drops `force3d` from `GRAPH_VIEWS` + `GraphView` union.
- Restores `Select.svelte` (no badge support).
- Drops `three`, `@threlte/core`, `@threlte/extras`, `d3-force-3d`,
`@types/three` from `template/package.json`.
- Restores `template/package-lock.json`.
- Removes `// TODO(force-mode-2d3d-switch)` from `ForceView.svelte`.
- Restores `scripts/build.mjs` `DIST_EXPERIMENTAL_MAX_BYTES` to `3 *
1024 * 1024` (the 3M → 6M bump bundled in PR #104 is reverted in the
same commit).
- Removes `.forgeplan/prds/PRD-022`, `.forgeplan/rfcs/RFC-019`,
`.forgeplan/evidence/EVID-027`.

Issue #103 will be reopened after merge — the feature is no longer in
the codebase, so its tracking issue should reflect that.

## Test plan

- [x] `npm run check` — 0 errors / 0 warnings (1047 files, matches
pre-#104 baseline).
- [ ] CI matrix (ubuntu / macos / windows) green.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Extends the dual-theme system (PRD-015 / RFC-014) with a third opt-in
palette `orch`: pure-black surfaces (`--bg: #000`) and lavender accent
(`--accent: #a78bfa`). Toggle in the health bar gains a fourth position
alongside Auto / Light / Dark; `auto` mode stays binary and never
resolves to `orch`. Pre-paint inline script accepts the new mode
synchronously, so first-paint is FOUC-free for users who saved it.

The change is a pure token-swap — every CSS-var defined under
`:root[data-theme='dark']` is mirrored under `:root[data-theme='orch']`,
so no component-level code changes were required. Verified via
svelte-check (0/0 errors/warnings across 1047 files), token-name diff
(empty modulo intentionally-shared `--font-*` / `--dot-grid-*`), and
production build round-trip (compiled `_layout.css` carries all three
selectors and the lavender literal). FG/BG contrast 16.58:1 (AAA).

Refs: PRD-023, EVID-028

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
UX shape change for the orch theme (PRD-023 v2):

- The visible health-bar toggle now stays at three positions
  (Auto / Light / Dark). Orch becomes a 4th visible item only
  after the user clicks Dark five times in a row — a hidden
  Easter egg. Counter resets when any non-Dark item is clicked.
- Once Orch is active, clicking it (deselect) bounces back to
  Auto. Clicking any active item never leaves the toggle
  visually empty: a remount tick is bumped on every empty
  emit from bits-ui so the controlled `value` prop re-pins
  the visual state. Spec invariant: 100% always some pressed
  item.
- Two race conditions tamed:
  1. bits-ui ToggleGroup fires onValueChange("dark") AFTER
     our click-delegation handler has already advanced mode
     to "orch" on the 5th dark click. Sentinel
     `justUnlockedOrch` swallows that single follow-up.
  2. bits-ui's internal `data-state` desyncs from a
     controlled `value` when the user re-clicks an active
     item. `{#key toggleRemountTick + ":" + items.length}`
     forces a fresh ToggleGroup mount so the visual re-pins.

Palette softened per user feedback ("цвета слишком
контрастные") to match Orchestra's marketing references:
foreground stepping compressed (`--fg` `#fafafa` →
`#d6cdf2`, `--fg-1` `#e8e8ed` → `#b3acc8`, etc.), accent
moved to a paler lavender (`#a78bfa` → `#b9a8ff`), and
edge / canvas / good-bad tokens dimmed proportionally.
F/B contrast `--fg-1` on `--bg-1` ≈ 9.7:1 — comfortably
above WCAG AAA, but visually softer than the previous
16.6:1.

Verified in browser via dev server (5174):
- 5 Dark clicks → orch active, Orch button appears + pressed
- Click active Orch → Auto active, orch button gone
- Click active Auto / active Light → stays / falls back to Auto
- Theme persists through reload via inline pre-paint script

Refs: PRD-023, EVID-028

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Once Orch becomes visible (5th consecutive Dark click), all theme
toggle items go disabled for ORCH_LOCK_MS = 2000ms so accidental
follow-up clicks (the user keeps mashing Dark beyond the 5th unlock
click) cannot immediately bounce out of orch. Disabled state
propagates through bits-ui via the existing `disabled` prop on
ToggleGroupItem; the click-delegation handler also early-returns
during the lock as a defensive belt-and-braces.

Refs: PRD-023

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… freeze (#106)

## Summary

Adds a third theme palette `orch` (Orchestra-inspired pure-black + muted
lavender) alongside the existing `dark` and `light` palettes, hidden
behind a 5-consecutive-Dark-clicks Easter egg with a 2s post-unlock
toggle freeze.

- **Palette**: `--bg #000`, `--accent #b9a8ff`, foreground stepped from
`#d6cdf2` → `#2e2b36`. Lower contrast than `dark` per design references;
AAA on `--fg-1`/`--bg-1`.
- **UX**: Visible toggle stays at 3 positions (Auto / Light / Dark).
Orch appears as a 4th item only after 5 consecutive Dark clicks; counter
resets on any non-Dark click.
- **Lock**: After Orch activates, all toggle items go disabled for 2000
ms so accidental follow-up clicks (the user keeps mashing Dark beyond
the unlock threshold) cannot bounce out.
- **Always-some-value**: Re-clicking an active item (deselect attempt)
bounces to Auto and force-remounts the ToggleGroup so the visual re-pins
to the store. Spec invariant: never visually empty.

## Why

PRD-023 / EVID-028. Existing dual-theme system (PRD-015 / RFC-014) only
exposed warm `dark` (`#ff5a1f` accent) and cream `light`. Adds a cool
true-black palette for OLED users and long-session reviewers, gated
behind an Easter egg per user direction.

## Test plan

- [x] `cd template && npm run check` — 0 errors / 0 warnings (1047
files)
- [x] `cd template && npm run build` — built successfully; orch palette
+ lavender accent present in production CSS
- [x] Browser smoke (5174):
- 5 Dark clicks → Orch button appears + pressed, theme=orch, lavender
palette applied
  - Click active Orch → Auto active, orch button disappears
- Click active Auto / Light → mode falls back to Auto, button stays
pressed (never empty)
  - Within 2 s of orch unlock all toggle buttons are disabled

## Files

- `template/src/app/styles/app.css` — `:root[data-theme='orch']` palette
block (every dark-defined token mirrored, dimmer/cooler values)
- `template/src/shared/lib/theme.svelte.ts` — `ThemeMode` /
`ThemeEffective` widened to include `'orch'`
- `template/src/app.html` — pre-paint script accepts `'orch'`
- `template/src/widgets/health-bar/ui/HealthBar.svelte` — 5-click
streak, 2s lock, deselect-to-auto, remount-tick visual re-pin
- `.forgeplan/prds/PRD-023-...md` + `.forgeplan/evidence/EVID-028-...md`
— Forgeplan artifacts (R_eff = 1.0)

Refs: PRD-023, EVID-028

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Cuts release/v0.1.13 from develop. Highlights:

- shared/ui catalogue + /playground showcase (PRD-018, RFC-016)
- rule 24 — shared/ui ownership and customisation discipline
- new primitive variants from rule-24 audit (ghost-mono, mono, outline-mono, banner)
- orch theme (Orchestra-inspired) as 5-click easter egg + 2s freeze
- dense-graph render-resilience for Sankey + Tree
- adoption of svelte-sonner Toaster
- reverted Force 3D experimental Threlte view (#103/#104/#105)

Refs: PRD-018, RFC-016, EVID-022, EVID-023, EVID-024, PRD-019
@fedorovvvv fedorovvvv merged commit 61cbcb9 into main May 8, 2026
3 checks passed
@fedorovvvv fedorovvvv deleted the release/v0.1.13 branch May 8, 2026 13:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants