Files
waggle-os/docs/ux-refactor/path-exec-phase-A-spec-2026-07-07.md
Oleg Maslov 0c3e2ead3b
Some checks failed
Installer Smoke / installer-smoke (push) Has been cancelled
moving
2026-09-02 10:10:29 +02:00

9.8 KiB
Raw Permalink Blame History

Path-to-9 execution · PHASE A — motion vocabulary (Phase-0) + AA/affordance floor (Pillar 4)

Contract: docs/ux-refactor/path-to-9-2026-07-07.md (v3, 5/5 panel-endorsed). Same honesty

rules as waves PX: surgical, both themes, testids/aria survive, reduced-motion gated.

Two sequential stages (stage 2 depends on stage 1's exports). Per-lane gate: related vitest green + eslint 0 errors on touched files. NO benchmarks/**, NO packages/hive-mind-*.

STAGE 1 (parallel lanes)

Lane M — motion tokens + spring family + spec page (Phase-0 items 1,2,3,5,7)

NEW files only + index.css additions. Files: apps/web/src/lib/motion/tokens.ts (new), apps/web/src/lib/motion/tokens.test.ts (new), apps/web/src/pages/MotionSpec.tsx (new, DEV-only route), route wiring (find the router config; gate the route on import.meta.env.DEV), apps/web/src/index.css (append motion custom props ONLY).

  1. The exported vocabulary (lib/motion/tokens.ts) — THE single motion source:
    export const SPRING = {
      micro:      { type: 'spring', stiffness: 550, damping: 35 },  // chips, presses, ≤150ms feel
      standard:   { type: 'spring', stiffness: 380, damping: 30 },  // hovers, panel/route fades
      expressive: { type: 'spring', stiffness: 260, damping: 24 },  // hero morphs, settle gesture
    } as const;
    export const DUR = { fast: 0.15, base: 0.2, slow: 0.32, settle: 0.4 } as const; // seconds
    export const EASE_OUT = [0.22, 1, 0.36, 1] as const; // cubic-bezier for non-spring CSS
    export const STAGGER = { list: 0.04, brief: 0.08 } as const;    // Wave W's 40ms / briefing 80ms
    /** Signature-moment frequency taxonomy (Phase-0.7): FULL settle only on rare
     *  accrual milestones; MICRO for high-frequency actions; per-session cooldown. */
    export const SIGNATURE = {
      full:  { moments: ['memory-saved-first-of-session', 'install-success', 'agent-spawned'], durS: DUR.settle, perSessionCooldownMs: 60_000 },
      micro: { moments: ['send-arm', 'selection'], durS: DUR.fast },
    } as const;
    /** Reduced-motion mapping per tier (Phase-0.3) — documented AND consumed. */
    export const REDUCED = {
      routeTransition: 'crossfade-only', hover: 'color-shadow-only-no-transform',
      settle: 'instant-state-color-pulse', streamingCaret: 'static',
      countUp: 'instant-set', ambient: 'off',
    } as const;
    
    Tune values to FEEL right (the numbers above are starting points — validate on the spec page); all three springs must share one physical character (same "material").
  2. CSS custom props appended to index.css :root (both themes inherit — motion is theme-invariant): --mo-fast: 150ms; --mo-base: 200ms; --mo-slow: 320ms; --mo-settle: 400ms; --mo-ease: cubic-bezier(0.22,1,0.36,1); Nothing else in this file.
  3. Motion-spec page (/motion-spec, DEV-only): sections demonstrating each spring variant (click-to-replay), hover tier on a sample card, entrance stagger, exit choreography (AnimatePresence in/out samples), the reduced-motion mapping table, and an INVENTORY table of already-shipped motion (see Lane R's list) with its migration status. This page is the arc's reviewable single source of motion truth.
  4. Tests: token invariants (all springs share type, DUR ascending, SIGNATURE moments disjoint, REDUCED covers every tier key).

Lane T — text-tertiary + focus-ring token tiers + offender migration (Pillar 4 items 1 + 3)

Files: apps/web/src/index.css (token definitions ONLY — coordinate with Lane M: Lane M appends motion props, Lane T appends color tokens; both are pure additions in different blocks, no shared lines), os/StatusBar.tsx, overlays/onboarding/WelcomeStep.tsx, overlays/LoginBriefing.tsx ("Don't show again"), plus the specific offender usages listed below. Tests updated where classes are asserted.

  1. --text-tertiary: a tier that is ≥4.5:1 on EVERY surface it appears over (--bg, --bg-2, --surface, --surface-2) in BOTH themes. Compute (WCAG relative luminance) and document the math in a comment next to the token. Dark: needs ≥ #a3987f-level; light: ≤ #6e6552-level. Verify per pair; show ratios.
  2. --focus-ring + --line-affordance (Pillar 4.3, WCAG 1.4.11): non-text tier ≥3:1 against adjacent effective background BOTH themes — the light-theme honey ring is the known risk (honey-on-ivory); compute and pick the light value accordingly (e.g. a darker honey/ochre for light). Document ratios in-comment.
  3. Migrate the recurring offender families onto --text-tertiary (verify each is still a REAL failure first — several were fixed in Waves V/X; do not churn compliant code): top utility bar (StatusBar plan/search/date cluster), onboarding subtitle + privacy footnote ("Skip setup" was fixed — verify), "Don't show again", unselected onboarding chips (raise rest contrast), card timestamps if any remain sub-AA. Compute before/after ratios in the lane report.

Lane G — the CI gates (Pillar 4 item 2 + verification plumbing)

NEW files: scripts/ux-gates/contrast-tokens.mjs, scripts/ux-gates/text-color-guard.mjs, scripts/ux-gates/contrast-runtime.mjs, scripts/ux-gates/README.md; package.json (root): three npm scripts (ux:contrast, ux:color-guard, ux:contrast-runtime). Do NOT touch .github/workflows (wiring into CI is a follow-up once scripts are proven).

  1. contrast-tokens.mjs: parse index.css (+ waggle-theme.css if it defines text tokens), compute WCAG ratios for every (text-token × surface-token) pair in both themes, assert the documented floors (--text/--text-2/--text-muted/--text-tertiary ≥4.5:1 on their allowed surfaces; --focus-ring/--line-affordance ≥3:1). Exit 1 on failure with a table. Must run green against Lane T's final tokens.
  2. text-color-guard.mjs (the generation-vector ban): scan apps/web/src (app surfaces; exclude tests, the tokens file, MotionSpec) for NEW non-token text colors: text-[#, raw hex in inline style color/background of text elements, text-hive-* palette classes, and opacity-modified text tokens BELOW the safe floor (text-[var(--text-dim)]/40 style patterns; allow ≥/60 with a warning list). Baseline file (scripts/ux-gates/color-guard-baseline.json) freezes today's grandfathered instances; the gate fails only on NEW instances — a ratchet, not a big-bang.
  3. contrast-runtime.mjs (composition-aware, Playwright): against the dev server, for each judged surface (/home, /workspaces, /memory, /agents, /marketplace, /settings, chat), walk visible text nodes, compute EFFECTIVE fg/bg (composite opacity up the tree; sample wallpaper/gradient via screenshot pixel at element center when bg is an image), report all <4.5:1 (text) and <3:1 (focus indicators — tab to 10 interactive elements per surface and measure the ring). Output a JSON + human table; exit 1 on NEW failures vs a baseline. Seed localStorage onboarding like the capture kit; run both themes.

STAGE 2 (parallel lanes; imports from Lane M's tokens — stage 1 merged first)

Lane R1 — retrofit: cards + entrances (Phase-0.6)

Files: apps/AllWorkspacesApp.tsx, apps/agents/SuggestedAgentCards.tsx, apps/extend/ExtensionCard.tsx, their tests. Migrate every duration/easing to the vocabulary: hover transitions → --mo-fast/--mo-base

  • --mo-ease (or SPRING.standard where framer-motion), entrance stagger → STAGGER.list
  • DUR.slow, lift/bloom timings → tokens. Acceptance: grep -E "duration-(75|100|150|200|300|500)|duration: '?0\.[0-9]|ease-(linear|in-out|out)\b" on these files returns ONLY token-backed or justified-in-comment instances; zero raw magic numbers.

Lane R2 — retrofit: memory + briefing + boot + theme (Phase-0.6)

Files: apps/memory/MemoryTrustManage.tsx, apps/memory/MemoryCenterTab.tsx, os/overlays/LoginBriefing.tsx, os/BootScreen.tsx, providers/ThemeProvider.tsx (+ index.css .theme-transition duration → var), tests. Same acceptance as R1. The 600ms count-up, 360ms crossfade, briefing staggers, boot phase timings all move onto DUR/STAGGER/custom props (keep the VALUES if they feel right — tokenize, don't retune; note any value you deliberately change).

Lane R3 — retrofit: chat + status chrome + keyframe sweep (Phase-0.6)

Files: apps/ChatApp.tsx, os/StatusBar.tsx (transition durations only — Lane T owns its colors), apps/WorkspaceDesktopApp.tsx, index.css @keyframes durations where referenced with magic numbers in components, tests. Same acceptance. Also produce THE INVENTORY: a table (append to the motion-spec page's inventory section — coordinate: R3 owns the final inventory content) of every motion in the judged surfaces: name, file, tier, duration token, reduced-motion behavior.

VERIFY STAGE (after stage 2; parallel)

  • Reviewer V1 (motion): adversarially verify Phase-0 acceptance — grep the judged surfaces for non-token durations/easings (list any survivor with justification status), confirm the spec page renders every tier, confirm reduced-motion mappings are implemented not just documented (inspect the code paths).
  • Reviewer V2 (floor): run node scripts/ux-gates/contrast-tokens.mjs and node scripts/ux-gates/text-color-guard.mjs; start dev servers if needed and run contrast-runtime.mjs on at least /home + /settings both themes; report the tables. Verify Lane T's in-comment math independently (recompute 3 spot pairs).

Orchestrator after verify

Full tsc (web), full apps/web vitest, eslint on all touched files, browser smoke (motion-spec page renders; one hover; theme flip), commit per stage or as one Phase-A commit. Then Phase B (Pillar 2) spec.

Deferred in Phase A (explicitly)

Competitive teardown side-by-sides (needs founder's Claude/ChatGPT sessions — flagged, not skipped silently); .github/workflows wiring (after scripts prove stable); the waggle-settle prototype (Pillar 1.3 — its own commissioned mini-arc later).