From 9b71c3ba7648688a3c22fa723ca9bd460cda1326 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 22 Jul 2026 21:08:51 -0400 Subject: [PATCH] docs: add Slate GUI redesign implementation plan 12 tasks: vitest setup, semantic theme tokens, four in-repo primitives (ProgressBar/AppButton/Popover/Icon) with unit tests, per-component restyle, and element-plus removal. Co-Authored-By: Claude Opus 4.8 --- .../plans/2026-07-22-slate-gui-redesign.md | 1265 +++++++++++++++++ 1 file changed, 1265 insertions(+) create mode 100644 docs/superpowers/plans/2026-07-22-slate-gui-redesign.md diff --git a/docs/superpowers/plans/2026-07-22-slate-gui-redesign.md b/docs/superpowers/plans/2026-07-22-slate-gui-redesign.md new file mode 100644 index 0000000..29d3e77 --- /dev/null +++ b/docs/superpowers/plans/2026-07-22-slate-gui-redesign.md @@ -0,0 +1,1265 @@ +# Slate GUI Redesign Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Restyle the n-link desktop app with a dark-first "Slate" visual language (semantic CSS-variable theming, light/dark via OS preference, Inter type) while keeping the existing three-pane workflow unchanged. + +**Architecture:** Introduce a semantic color-token layer as CSS custom properties mapped into Tailwind, then migrate every hardcoded color utility to a token. Replace Element-Plus's `ElPopover` (its only usage) and duplicated button/progress/icon markup with four small in-repo components (`Popover`, `AppButton`, `ProgressBar`, `Icon`) that carry real logic and get unit tests. The remaining work is per-component restyle verified by build + visual check. + +**Tech Stack:** Vue 3 (` +``` + +- [ ] **Step 4: Run the test to verify it passes** + +Run: `cd desktop && npm test -- ProgressBar` +Expected: PASS — 4 passed. + +- [ ] **Step 5: Commit** + +```bash +git add n-link-core/components/ProgressBar.vue n-link-core/components/ProgressBar.spec.ts +git commit -m "feat(ui): add ProgressBar component" +``` + +--- + +## Task 4: AppButton component + +**Files:** +- Create: `n-link-core/components/AppButton.vue` +- Test: `n-link-core/components/AppButton.spec.ts` + +**Interfaces:** +- Produces: `AppButton.vue` with props `{ variant?: 'primary' | 'secondary' | 'danger' | 'success'; size?: 'md' | 'sm'; disabled?: boolean }` (defaults `'primary'`, `'md'`, `false`). Renders a `', + default: '
panel body
', + }, + }); +} + +describe('Popover', () => { + it('hides the panel initially', () => { + expect(make().find('[data-testid="panel"]').exists()).toBe(false); + }); + + it('opens when the trigger is clicked', async () => { + const wrapper = make(); + await wrapper.get('.trg').trigger('click'); + expect(wrapper.find('[data-testid="panel"]').exists()).toBe(true); + expect(wrapper.emitted('update:visible')?.at(-1)).toEqual([true]); + }); + + it('closes on Escape', async () => { + const wrapper = make(); + await wrapper.get('.trg').trigger('click'); + document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' })); + await wrapper.vm.$nextTick(); + expect(wrapper.find('[data-testid="panel"]').exists()).toBe(false); + }); + + it('closes on an outside mousedown', async () => { + const wrapper = make(); + await wrapper.get('.trg').trigger('click'); + document.body.dispatchEvent(new MouseEvent('mousedown', { bubbles: true })); + await wrapper.vm.$nextTick(); + expect(wrapper.find('[data-testid="panel"]').exists()).toBe(false); + }); + + it('applies a width style to the panel', async () => { + const wrapper = mount(Popover, { + attachTo: document.body, + props: { width: 200 }, + slots: { trigger: '', default: '
b
' }, + }); + await wrapper.get('.trg').trigger('click'); + expect(wrapper.get('[data-testid="panel"]').attributes('style')).toContain('width: 200px'); + }); +}); +``` + +- [ ] **Step 2: Run the test to verify it fails** + +Run: `cd desktop && npm test -- Popover` +Expected: FAIL — cannot resolve `./Popover.vue`. + +- [ ] **Step 3: Write the component** + +Create `n-link-core/components/Popover.vue`: +```vue + + + +``` + +- [ ] **Step 4: Run the test to verify it passes** + +Run: `cd desktop && npm test -- Popover` +Expected: PASS — 5 passed. + +- [ ] **Step 5: Commit** + +```bash +git add n-link-core/components/Popover.vue n-link-core/components/Popover.spec.ts +git commit -m "feat(ui): add Popover component to replace element-plus" +``` + +--- + +## Task 6: Icon component + +**Files:** +- Create: `n-link-core/components/Icon.vue` +- Test: `n-link-core/components/Icon.spec.ts` +- Modify: `n-link-core/package.json` + +**Interfaces:** +- Produces: `Icon.vue` with props `{ name: string; size?: number }` (default `20`). Renders the named feather icon inline (via `feather.icons[name].toSvg`), so the SVG's `stroke="currentColor"` inherits the surrounding text color. Throws `Error("unknown feather icon: ")` for an unknown name. + +- [ ] **Step 1: Declare feather-icons as a dependency of n-link-core** + +In `n-link-core/package.json`, add a `dependencies` block (the file currently has only `peerDependencies` and `devDependencies`): +```json + "dependencies": { + "feather-icons": "^4.29.2" + }, +``` +Run: `npm install` from repo root. + +- [ ] **Step 2: Write the failing test** + +Create `n-link-core/components/Icon.spec.ts`: +```ts +import { describe, it, expect } from 'vitest'; +import { mount } from '@vue/test-utils'; +import Icon from './Icon.vue'; + +describe('Icon', () => { + it('renders an inline svg that inherits currentColor', () => { + const wrapper = mount(Icon, { props: { name: 'refresh-cw' } }); + expect(wrapper.html()).toContain(' { + const wrapper = mount(Icon, { props: { name: 'chevron-right', size: 32 } }); + expect(wrapper.html()).toContain('width="32"'); + expect(wrapper.html()).toContain('height="32"'); + }); + + it('throws for an unknown icon name', () => { + expect(() => mount(Icon, { props: { name: 'not-a-real-icon' } })) + .toThrow(/unknown feather icon/); + }); +}); +``` + +- [ ] **Step 3: Run the test to verify it fails** + +Run: `cd desktop && npm test -- Icon` +Expected: FAIL — cannot resolve `./Icon.vue`. + +- [ ] **Step 4: Write the component** + +Create `n-link-core/components/Icon.vue`: +```vue + + + +``` + +- [ ] **Step 5: Run the test to verify it passes** + +Run: `cd desktop && npm test -- Icon` +Expected: PASS — 3 passed. + +- [ ] **Step 6: Commit** + +```bash +git add n-link-core/components/Icon.vue n-link-core/components/Icon.spec.ts \ + n-link-core/package.json package-lock.json +git commit -m "feat(ui): add inline Icon component for themeable feather icons" +``` + +--- + +## Task 7: Restyle DeviceSelect + +**Files:** +- Modify: `n-link-core/components/DeviceSelect.vue` + +**Interfaces:** +- Consumes: `Popover.vue` (Task 5), `Icon.vue` (Task 6), theme tokens (Task 2). + +- [ ] **Step 1: Rewrite the component with Popover + Icon + tokens** + +Replace the entire contents of `n-link-core/components/DeviceSelect.vue` with: +```vue + + + + + +``` +(The `el-popover`, its `element-plus` imports, the `feather-icons/dist/icons/refresh-cw.svg` import, and the second `