diff --git a/apps/web/src/components/base64/base64-layout.tsx b/apps/web/src/components/base64/base64-layout.tsx index 14e5873f..ac18311b 100644 --- a/apps/web/src/components/base64/base64-layout.tsx +++ b/apps/web/src/components/base64/base64-layout.tsx @@ -19,6 +19,10 @@ import { cn } from '@/lib/utils'; import { useTranslations } from 'next-intl'; import { useSearchParams } from 'next/navigation'; import { SendToMenu } from '@/components/ui/send-to-menu'; +import { IconTransform } from '@tabler/icons-react'; +import { ToolPageHeader } from '@/components/tools/tool-page-header'; +import { RevealItem } from '@/components/dashboard/dashboard-reveal'; +import { CATEGORY_ACCENT } from '@/components/dashboard/types'; type Mode = 'encode' | 'decode'; @@ -152,39 +156,44 @@ export function Base64Layout() { const outputCharCount = output.length; return ( -
- {/* Header */} -
-
-

{t('title')}

-

{t('subtitle')}

-
-
- - { - const file = e.target.files?.[0]; - if (file) handleFileUpload(file); - e.target.value = ''; - }} - /> - -
+
+
+ + + + + + {/* Mobile upload/clear actions */} +
+ + { + const file = e.target.files?.[0]; + if (file) handleFileUpload(file); + e.target.value = ''; + }} + /> +
{/* Mode Toggle */} diff --git a/apps/web/src/components/csv-excel-json/csv-excel-json-tool.tsx b/apps/web/src/components/csv-excel-json/csv-excel-json-tool.tsx index 40873600..091ee925 100644 --- a/apps/web/src/components/csv-excel-json/csv-excel-json-tool.tsx +++ b/apps/web/src/components/csv-excel-json/csv-excel-json-tool.tsx @@ -17,7 +17,10 @@ import { Card, CardContent } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/textarea"; import { Label } from "@/components/ui/label"; -import { ToolHeader } from "@/components/tools/tool-header"; +import { ToolPinButton } from "@/components/tools/tool-header"; +import { ToolPageHeader } from "@/components/tools/tool-page-header"; +import { RevealItem } from "@/components/dashboard/dashboard-reveal"; +import { CATEGORY_ACCENT } from "@/components/dashboard/types"; import { useToolUsage } from "@/hooks/use-tool-usage"; import { cn } from "@/lib/utils"; import { @@ -149,13 +152,23 @@ export function CsvExcelJsonTool() { }; return ( -
+
+
+ + +
+ + +
+
+ -
- {/* Header */} -
-
-

{t('title')}

-

{t('subtitle')}

-
-
- - { - const file = e.target.files?.[0]; - if (file && file.type.startsWith('image/')) processFile(file); - e.target.value = ''; - }} - /> - -
+
+
+ + + + + + {/* Mobile upload/clear actions */} +
+ + { + const file = e.target.files?.[0]; + if (file && file.type.startsWith('image/')) processFile(file); + e.target.value = ''; + }} + /> +
{/* Mode Toggle */} diff --git a/apps/web/src/components/number-base-converter/number-base-converter-layout.tsx b/apps/web/src/components/number-base-converter/number-base-converter-layout.tsx index 79674660..5e2e308f 100644 --- a/apps/web/src/components/number-base-converter/number-base-converter-layout.tsx +++ b/apps/web/src/components/number-base-converter/number-base-converter-layout.tsx @@ -23,6 +23,10 @@ import { parseIntegerInBase, type NumberBase, } from '@/lib/number-base'; +import { IconArrowsExchange } from '@tabler/icons-react'; +import { ToolPageHeader } from '@/components/tools/tool-page-header'; +import { RevealItem } from '@/components/dashboard/dashboard-reveal'; +import { CATEGORY_ACCENT } from '@/components/dashboard/types'; export function NumberBaseConverterLayout() { const t = useTranslations('NumberBaseConverter'); @@ -74,11 +78,17 @@ export function NumberBaseConverterLayout() { }; return ( -
-
-

{t('title')}

-

{t('subtitle')}

-
+
+
+ + + +
diff --git a/apps/web/src/components/timestamp-converter/timestamp-converter-layout.tsx b/apps/web/src/components/timestamp-converter/timestamp-converter-layout.tsx index a08e142b..9363ddd3 100644 --- a/apps/web/src/components/timestamp-converter/timestamp-converter-layout.tsx +++ b/apps/web/src/components/timestamp-converter/timestamp-converter-layout.tsx @@ -9,6 +9,10 @@ import { Label } from '@/components/ui/label'; import { formatTimestampAll, parseTimestampInput } from '@/lib/timestamp-convert'; import { Check, Copy } from 'lucide-react'; import { useTranslations } from 'next-intl'; +import { IconCalendarTime } from '@tabler/icons-react'; +import { ToolPageHeader } from '@/components/tools/tool-page-header'; +import { RevealItem } from '@/components/dashboard/dashboard-reveal'; +import { CATEGORY_ACCENT } from '@/components/dashboard/types'; function CopyField({ label, @@ -55,15 +59,19 @@ export function TimestampConverterLayout() { const setNow = () => setInput(String(Date.now())); return ( -
-
-

{t('title')}

-

- {t.rich('subtitle', { +

+
+ + + {chunks}, })} -

-
+ accent={CATEGORY_ACCENT.Converters} + /> +
diff --git a/apps/web/src/components/tools/tool-page-header.tsx b/apps/web/src/components/tools/tool-page-header.tsx index 654ec336..c9f2939c 100644 --- a/apps/web/src/components/tools/tool-page-header.tsx +++ b/apps/web/src/components/tools/tool-page-header.tsx @@ -7,7 +7,7 @@ import { CATEGORY_ACCENT } from '@/components/dashboard/types' interface ToolPageHeaderProps { icon: React.ElementType title: string - description: string + description: React.ReactNode accent?: { bg: string; text: string } className?: string } diff --git a/apps/web/src/components/unit-converter/unit-converter-layout.tsx b/apps/web/src/components/unit-converter/unit-converter-layout.tsx index 2b7479e2..d274e516 100644 --- a/apps/web/src/components/unit-converter/unit-converter-layout.tsx +++ b/apps/web/src/components/unit-converter/unit-converter-layout.tsx @@ -28,6 +28,10 @@ import { CommandList, } from '@/components/ui/command' import { convert, UNIT_CATEGORIES, getCategoryKeys, getUnitKeys } from '@/lib/unit-converter' +import { IconRuler } from '@tabler/icons-react' +import { ToolPageHeader } from '@/components/tools/tool-page-header' +import { RevealItem } from '@/components/dashboard/dashboard-reveal' +import { CATEGORY_ACCENT } from '@/components/dashboard/types' function formatResult(n: number): string { if (!isFinite(n)) return '—' @@ -84,7 +88,21 @@ export function UnitConverterLayout() { }, [resultText]) return ( -
+
+
+ + + {chunks}, + })} + accent={CATEGORY_ACCENT.Converters} + /> + + +
{/* Category — searchable combobox */}
@@ -212,6 +230,7 @@ export function UnitConverterLayout() { )} +
) } diff --git a/apps/web/src/components/url-encode/url-encode-layout.tsx b/apps/web/src/components/url-encode/url-encode-layout.tsx index 3c2364b1..d3daba94 100644 --- a/apps/web/src/components/url-encode/url-encode-layout.tsx +++ b/apps/web/src/components/url-encode/url-encode-layout.tsx @@ -19,6 +19,10 @@ import { cn } from '@/lib/utils'; import { useTranslations } from 'next-intl'; import { useSearchParams } from 'next/navigation'; import { SendToMenu } from '@/components/ui/send-to-menu'; +import { IconLink } from '@tabler/icons-react'; +import { ToolPageHeader } from '@/components/tools/tool-page-header'; +import { RevealItem } from '@/components/dashboard/dashboard-reveal'; +import { CATEGORY_ACCENT } from '@/components/dashboard/types'; type Mode = 'encode' | 'decode'; @@ -132,38 +136,43 @@ export function UrlEncodeLayout() { const outputCharCount = output.length; return ( -
-
-
-

{t('title')}

-

{t('subtitle')}

-
-
- - { - const file = e.target.files?.[0]; - if (file) handleFileUpload(file); - e.target.value = ''; - }} - /> - -
+
+
+ + + + + +
+ + { + const file = e.target.files?.[0]; + if (file) handleFileUpload(file); + e.target.value = ''; + }} + /> +
diff --git a/apps/web/src/components/url-parser/url-parser-layout.tsx b/apps/web/src/components/url-parser/url-parser-layout.tsx index 54867346..36e1b4e6 100644 --- a/apps/web/src/components/url-parser/url-parser-layout.tsx +++ b/apps/web/src/components/url-parser/url-parser-layout.tsx @@ -8,6 +8,10 @@ import { Label } from '@/components/ui/label' import { cn } from '@/lib/utils' import { Check, Copy, Download, FileText, Trash2, Upload } from 'lucide-react' import { useTranslations } from 'next-intl' +import { IconLink } from '@tabler/icons-react' +import { ToolPageHeader } from '@/components/tools/tool-page-header' +import { RevealItem } from '@/components/dashboard/dashboard-reveal' +import { CATEGORY_ACCENT } from '@/components/dashboard/types' type ParsedParam = { key: string @@ -169,38 +173,43 @@ export function UrlParserLayout() { }, [parsed]) return ( -
-
-
-

{t('title')}

-

{t('subtitle')}

-
-
- - { - const file = e.target.files?.[0] - if (file) handleFileUpload(file) - e.target.value = '' - }} - /> - -
+
+
+ + + + + +
+ + { + const file = e.target.files?.[0] + if (file) handleFileUpload(file) + e.target.value = '' + }} + /> +
diff --git a/docs/superpowers/plans/2026-07-03-converters-dashboard-alignment.md b/docs/superpowers/plans/2026-07-03-converters-dashboard-alignment.md new file mode 100644 index 00000000..f283b829 --- /dev/null +++ b/docs/superpowers/plans/2026-07-03-converters-dashboard-alignment.md @@ -0,0 +1,910 @@ +# Converters Dashboard Alignment 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:** Give the 8 tools in the "Converters" category (`base64`, `url-encode`, `url-parser`, `number-base-converter`, `timestamp-converter`, `unit-converter`, `csv-excel-json`, `image-to-base64`) a page header, icon-chip branding, accent color, ambient background, and mount animation consistent with the dashboard's visual system, without changing tool functionality. + +**Architecture:** One shared component, `ToolPageHeader`, lives in `apps/web/src/components/tools/` and gets applied to each of the 8 tool layout files one at a time. Reuses the dashboard's existing `RevealItem` motion helper, `CATEGORY_ACCENT` color map, and `dash-ambient`/`dashboard-grid-bg` CSS utilities as-is — no renaming, no new dependencies. No new Copy/Swap/Dropzone components: all 8 tools already implement copy-to-clipboard, direction-swap (where bidirectional), and drag-and-drop (where file-based) as bespoke per-tool code, and it already works — this plan does not touch it. + +**Tech Stack:** Next.js 16 / React 19, Tailwind CSS 3, shadcn/ui ("new-york" style) on Radix primitives, `@tabler/icons-react` (icon source for these 8 tools per `sidebar-data.ts`), `lucide-react` (already used inside converter layouts for non-branding icons), `framer-motion` (via the existing `RevealItem`), `next-intl`. + +## Global Constraints + +- **Scope is exactly 8 tools**, per `apps/web/src/lib/tool-categories.ts:23-30`: `base64`, `url-encode`, `url-parser`, `number-base-converter`, `timestamp-converter`, `unit-converter`, `csv-excel-json`, `image-to-base64`. `format-converter` is explicitly **excluded** — it is canonically a `Formatters`-category tool (`tool-categories.ts:20`) and is already claimed by the sibling `2026-07-03-formatters-dashboard-alignment.md` plan's own rollout (that plan is now merged to `main` via PR #236). +- **No new functionality.** Every tool already has copy-to-clipboard, and the bidirectional ones (`base64`, `url-encode`, `number-base-converter`, `unit-converter`) already have a working swap control; the file-based ones (`base64`, `url-encode`, `url-parser`, `csv-excel-json`, `image-to-base64`) already have working drag-and-drop. None of that is touched. Reuse each tool's existing `next-intl` keys (`t('title')`, `t('subtitle')`) — do not add new translation keys. +- **Shared component already exists.** The Formatters plan's Task 1 created `apps/web/src/components/tools/tool-page-header.tsx` and it is now merged to `main`, with the interface `{icon: React.ElementType, title: string, description: string, accent?: {bg, text}, className?: string}`. This plan reuses it as-is — **except** `description` must be widened from `string` to `React.ReactNode`, because `timestamp-converter` and `unit-converter` render rich-text descriptions via `t.rich(...)`. This widening is backward-compatible (a `string` is already a valid `ReactNode`), so the 9 existing Formatters callers are unaffected. Task 1 below makes only that one-line change. +- **Accent color:** all 8 tools share one accent — `CATEGORY_ACCENT.Converters` from `apps/web/src/components/dashboard/types.ts:132`: `{ bg: 'bg-amber-500/10', text: 'text-amber-600 dark:text-amber-400' }`. Import this constant; do not hand-roll new color classes. +- **Icons:** per-tool icon comes from `apps/web/src/components/sidebar/data/sidebar-data.ts:216-263` (the source of truth for tool icons), all from `@tabler/icons-react`: + | Tool | Icon | + |---|---| + | base64 | `IconTransform` | + | url-encode | `IconLink` | + | url-parser | `IconLink` | + | number-base-converter | `IconArrowsExchange` | + | timestamp-converter | `IconCalendarTime` | + | unit-converter | `IconRuler` | + | csv-excel-json | `IconFileSpreadsheet` (already imported in this file for its dropzone icon — reuse the existing import, don't duplicate it) | + | image-to-base64 | `IconPhoto` | +- **No test framework for component rendering exists in this repo.** There is no `@testing-library/react` dependency. Every task below is verified with `npx tsc --noEmit` (types), `npm run lint` (ESLint), and a manual check against the running dev server (`npm run dev`) — not a fabricated render test. Run all three from `apps/web/`. +- **Three corrections to the approved design spec** (`docs/superpowers/specs/2026-07-03-converters-dashboard-alignment-design.md`), found while reading the exact current code and the sibling Formatters plan's own corrections (the two tool families share the same underlying architecture, so the same facts apply): + 1. **No `bg-gradient-to-br from-card to-card/80` / ring-inset / hover-lift on converter Cards.** `apps/web/src/components/ui/card.tsx:5-18` shows the shared `Card` primitive already renders `bg-card` with a themed shadow — every converter `Card` already uses this primitive, so it's already visually aligned with dashboard's *base* card recipe. The extra gradient/ring/hover-lift in `dashboard-tool-card.tsx:98-103` exists specifically to sell a hoverable, clickable link card (`ToolCard`); layering it onto a static I/O panel or toolbar `Card` (not a click target) would be decoration copied from the wrong context. + 2. **No `dash-card-sheen` on converter Cards.** Its hover opacity is gated by `.group:hover .dash-card-sheen::before/::after` (`globals.css:854,873`) — it only ever renders because `dashboard-tool-card.tsx:90` wraps the card in a ``. Converter panel/toolbar Cards are not links and have no such wrapper; adding the class would be dead CSS with zero visible effect. + 3. **No spacing normalization** (the approved spec's `gap-3 md:gap-4 xl:gap-5` / `p-3 md:p-3.5` rhythm). `base64`, `url-encode`, `url-parser`, and `image-to-base64` are fixed-height (`h-full`, `min-h-0`, `flex-1`) dual-pane layouts where the textarea claims all remaining vertical space; loosening spacing would shrink usable editor height for no visual benefit. The other four (`number-base-converter`, `timestamp-converter`, `unit-converter`, `csv-excel-json`) have no spacing inconsistency worth touching either. The real, correctly-scoped visual delta this plan delivers is the page header, accent icon chip, ambient background, and motion — not a spacing or card-background change. +- **CSS utilities are reused as-is, unrenamed.** `dash-ambient` and `dashboard-grid-bg` (`apps/web/src/app/globals.css:753-765` and `823-838`) are global stylesheet rules — the `dash-` prefix is cosmetic, not a route scope. They're simply imported by class name into converter files. +- **`csv-excel-json` is a special case:** it already uses the older `ToolHeader` component (`apps/web/src/components/tools/tool-header.tsx`), which also renders a pin-to-sidebar (`ToolPinButton`) affordance the other 7 converters don't have. Task 8 swaps `ToolHeader` for `ToolPageHeader` in this file only (the shared `ToolHeader` component itself is untouched, since `http-status-codes` and `email-validator` still use it and are out of scope) and preserves the pin button by rendering `ToolPinButton` directly alongside the new header. + +--- + +## Task 1: Widen `ToolPageHeader`'s `description` prop to `React.ReactNode` + +**Files:** +- Modify: `apps/web/src/components/tools/tool-page-header.tsx` + +**Interfaces:** +- Produces: `ToolPageHeader({ icon: React.ElementType, title: string, description: React.ReactNode, accent?: { bg: string; text: string }, className?: string })` — same component, one prop type widened. + +- [ ] **Step 1: Widen the prop type** + +Change: + +```tsx +interface ToolPageHeaderProps { + icon: React.ElementType + title: string + description: string + accent?: { bg: string; text: string } + className?: string +} +``` + +to: + +```tsx +interface ToolPageHeaderProps { + icon: React.ElementType + title: string + description: React.ReactNode + accent?: { bg: string; text: string } + className?: string +} +``` + +No other line in the file changes — `{description}` is already rendered directly inside a `

`, which accepts `ReactNode` today. + +- [ ] **Step 2: Typecheck and lint** + +Run: `cd apps/web && npx tsc --noEmit && npm run lint` +Expected: no errors — this is a type widening, not a behavior change. Confirm no existing caller (the 9 Formatters tools) breaks: they all pass plain strings, which remain valid under `ReactNode`. + +- [ ] **Step 3: Commit** + +```bash +git add apps/web/src/components/tools/tool-page-header.tsx +git commit -m "fix: widen ToolPageHeader description prop to ReactNode for converters" +``` + +--- + +## Task 2: Apply to Base64 Encoder + +**Files:** +- Modify: `apps/web/src/components/base64/base64-layout.tsx` + +**Interfaces:** +- Consumes: `ToolPageHeader` (Task 1), `RevealItem` from `apps/web/src/components/dashboard/dashboard-reveal.tsx` (existing), `CATEGORY_ACCENT` from `apps/web/src/components/dashboard/types.ts` (existing), `IconTransform` from `@tabler/icons-react`. + +- [ ] **Step 1: Add imports** + +After line 21 (`import { SendToMenu } from '@/components/ui/send-to-menu';`), add: + +```tsx +import { IconTransform } from '@tabler/icons-react'; +import { ToolPageHeader } from '@/components/tools/tool-page-header'; +import { RevealItem } from '@/components/dashboard/dashboard-reveal'; +import { CATEGORY_ACCENT } from '@/components/dashboard/types'; +``` + +- [ ] **Step 2: Replace the header block** + +Replace lines 154-189: + +```tsx + return ( +

+ {/* Header */} +
+
+

{t('title')}

+

{t('subtitle')}

+
+
+ + { + const file = e.target.files?.[0]; + if (file) handleFileUpload(file); + e.target.value = ''; + }} + /> + +
+
+``` + +with: + +```tsx + return ( +
+
+ + + + + + {/* Mobile upload/clear actions */} +
+ + { + const file = e.target.files?.[0]; + if (file) handleFileUpload(file); + e.target.value = ''; + }} + /> + +
+``` + +- [ ] **Step 3: Typecheck and lint** + +Run: `cd apps/web && npx tsc --noEmit && npm run lint` + +- [ ] **Step 4: Manual verification** + +Run: `cd apps/web && npm run dev`, visit `/app/base64`. +Expected: +- Desktop: an amber icon chip + "Base64 Encoder" title + description now shows above the encode/decode toggle (previously only visible on mobile). +- Mobile (< 768px): Upload/Clear buttons still appear, still work; header still shows above them. +- Dark mode: header and ambient background render correctly. +- Reduce-motion emulation: header appears without a fade-up. +- Encode/decode toggle, swap, copy, download, drag-and-drop file upload all still work exactly as before. + +- [ ] **Step 5: Commit** + +```bash +git add apps/web/src/components/base64/base64-layout.tsx +git commit -m "refactor: apply shared ToolPageHeader to Base64 Encoder" +``` + +--- + +## Task 3: Apply to URL Encoder + +**Files:** +- Modify: `apps/web/src/components/url-encode/url-encode-layout.tsx` + +- [ ] **Step 1: Add imports** + +After line 21 (`import { SendToMenu } from '@/components/ui/send-to-menu';`), add: + +```tsx +import { IconLink } from '@tabler/icons-react'; +import { ToolPageHeader } from '@/components/tools/tool-page-header'; +import { RevealItem } from '@/components/dashboard/dashboard-reveal'; +import { CATEGORY_ACCENT } from '@/components/dashboard/types'; +``` + +- [ ] **Step 2: Replace the header block** + +Replace lines 134-167: + +```tsx + return ( +
+
+
+

{t('title')}

+

{t('subtitle')}

+
+
+ + { + const file = e.target.files?.[0]; + if (file) handleFileUpload(file); + e.target.value = ''; + }} + /> + +
+
+``` + +with: + +```tsx + return ( +
+
+ + + + + +
+ + { + const file = e.target.files?.[0]; + if (file) handleFileUpload(file); + e.target.value = ''; + }} + /> + +
+``` + +- [ ] **Step 3: Typecheck and lint** + +Run: `cd apps/web && npx tsc --noEmit && npm run lint` + +- [ ] **Step 4: Manual verification** + +Visit `/app/url-encode`. Same checklist as Task 2 Step 4 (header at all breakpoints, mobile buttons still work, dark mode, reduce-motion, encode/decode/swap/copy/download/drag-drop all unaffected). + +- [ ] **Step 5: Commit** + +```bash +git add apps/web/src/components/url-encode/url-encode-layout.tsx +git commit -m "refactor: apply shared ToolPageHeader to URL Encoder" +``` + +--- + +## Task 4: Apply to URL Parser + +**Files:** +- Modify: `apps/web/src/components/url-parser/url-parser-layout.tsx` + +- [ ] **Step 1: Add imports** + +After line 10 (`import { useTranslations } from 'next-intl'`), add: + +```tsx +import { IconLink } from '@tabler/icons-react' +import { ToolPageHeader } from '@/components/tools/tool-page-header' +import { RevealItem } from '@/components/dashboard/dashboard-reveal' +import { CATEGORY_ACCENT } from '@/components/dashboard/types' +``` + +- [ ] **Step 2: Replace the header block** + +Replace lines 171-204: + +```tsx + return ( +
+
+
+

{t('title')}

+

{t('subtitle')}

+
+
+ + { + const file = e.target.files?.[0] + if (file) handleFileUpload(file) + e.target.value = '' + }} + /> + +
+
+``` + +with: + +```tsx + return ( +
+
+ + + + + +
+ + { + const file = e.target.files?.[0] + if (file) handleFileUpload(file) + e.target.value = '' + }} + /> + +
+``` + +- [ ] **Step 3: Typecheck and lint** + +Run: `cd apps/web && npx tsc --noEmit && npm run lint` + +- [ ] **Step 4: Manual verification** + +Visit `/app/url-parser`. Header at all breakpoints; mobile Upload/Clear unaffected; parsed field copy buttons and JSON download unaffected; drag-drop still works. + +- [ ] **Step 5: Commit** + +```bash +git add apps/web/src/components/url-parser/url-parser-layout.tsx +git commit -m "refactor: apply shared ToolPageHeader to URL Parser" +``` + +--- + +## Task 5: Apply to Number Base Converter + +**Files:** +- Modify: `apps/web/src/components/number-base-converter/number-base-converter-layout.tsx` + +- [ ] **Step 1: Add imports** + +After line 25 (the closing `} from '@/lib/number-base';`), add: + +```tsx +import { IconArrowsExchange } from '@tabler/icons-react'; +import { ToolPageHeader } from '@/components/tools/tool-page-header'; +import { RevealItem } from '@/components/dashboard/dashboard-reveal'; +import { CATEGORY_ACCENT } from '@/components/dashboard/types'; +``` + +- [ ] **Step 2: Replace the header block** + +Replace lines 76-81: + +```tsx + return ( +
+
+

{t('title')}

+

{t('subtitle')}

+
+``` + +with: + +```tsx + return ( +
+
+ + + + +``` + +- [ ] **Step 3: Typecheck and lint** + +Run: `cd apps/web && npx tsc --noEmit && npm run lint` + +- [ ] **Step 4: Manual verification** + +Visit `/app/number-base-converter`. Header at all breakpoints (previously mobile-only); base selects, swap button, copy button all unaffected. + +- [ ] **Step 5: Commit** + +```bash +git add apps/web/src/components/number-base-converter/number-base-converter-layout.tsx +git commit -m "refactor: apply shared ToolPageHeader to Number Base Converter" +``` + +--- + +## Task 6: Apply to Timestamp Converter + +**Files:** +- Modify: `apps/web/src/components/timestamp-converter/timestamp-converter-layout.tsx` + +- [ ] **Step 1: Add imports** + +After line 11 (`import { useTranslations } from 'next-intl';`), add: + +```tsx +import { IconCalendarTime } from '@tabler/icons-react'; +import { ToolPageHeader } from '@/components/tools/tool-page-header'; +import { RevealItem } from '@/components/dashboard/dashboard-reveal'; +import { CATEGORY_ACCENT } from '@/components/dashboard/types'; +``` + +- [ ] **Step 2: Replace the header block** + +Replace lines 57-66: + +```tsx + return ( +
+
+

{t('title')}

+

+ {t.rich('subtitle', { + code: (chunks) => {chunks}, + })} +

+
+``` + +with: + +```tsx + return ( +
+
+ + + {chunks}, + })} + accent={CATEGORY_ACCENT.Converters} + /> + +``` + +- [ ] **Step 3: Typecheck and lint** + +Run: `cd apps/web && npx tsc --noEmit && npm run lint` +Expected: no errors. This is the first tool passing a `t.rich(...)` result (a `ReactNode`, not a `string`) as `description` — if this fails to typecheck, confirm Task 1 actually widened `ToolPageHeader`'s `description` prop to `React.ReactNode`. + +- [ ] **Step 4: Manual verification** + +Visit `/app/timestamp-converter`. Header shows at all breakpoints, with the `ISO-8601` segment of the description still rendered as inline code (not literal angle brackets). "Now" button, per-field copy buttons unaffected. + +- [ ] **Step 5: Commit** + +```bash +git add apps/web/src/components/timestamp-converter/timestamp-converter-layout.tsx +git commit -m "refactor: apply shared ToolPageHeader to Timestamp Converter" +``` + +--- + +## Task 7: Apply to Unit Converter + +This is the biggest gap: the tool currently renders no in-page title/description at all (desktop or mobile). + +**Files:** +- Modify: `apps/web/src/components/unit-converter/unit-converter-layout.tsx` + +- [ ] **Step 1: Add imports** + +After line 30 (`import { convert, UNIT_CATEGORIES, getCategoryKeys, getUnitKeys } from '@/lib/unit-converter'`), add: + +```tsx +import { IconRuler } from '@tabler/icons-react' +import { ToolPageHeader } from '@/components/tools/tool-page-header' +import { RevealItem } from '@/components/dashboard/dashboard-reveal' +import { CATEGORY_ACCENT } from '@/components/dashboard/types' +``` + +- [ ] **Step 2: Add the header and wrap the existing content in a new outer shell** + +Replace line 87: + +```tsx +
+``` + +with: + +```tsx +
+
+ + + {chunks}, + })} + accent={CATEGORY_ACCENT.Converters} + /> + + +
+``` + +(Line 86, ` return (`, is unchanged — this only replaces the single opening `
` line with the block above, which opens two nested divs: the new full-bleed ambient wrapper, and the original centered content wrapper preserved as-is inside it.) + +- [ ] **Step 3: Close the new outer wrapper** + +Replace the file's closing lines 214-216: + +```tsx + )} +
+ ) +} +``` + +with: + +```tsx + )} +
+
+ ) +} +``` + +(This closes the original `max-w-2xl` div — now indented one level deeper — followed by the new outer wrapper div added in Step 2.) + +- [ ] **Step 4: Typecheck and lint** + +Run: `cd apps/web && npx tsc --noEmit && npm run lint` +Expected: no unclosed-JSX-tag errors. If there's a mismatch, count divs: the original file had exactly one `return (
...
)`; after this task it must have `return (
...(original content)...
)` — two real `
` elements deep at the point Step 3 closes, plus the self-closing ambient div and the `RevealItem` (which is not a div). + +- [ ] **Step 5: Manual verification** + +Visit `/app/unit-converter`. A page header (icon chip + "Unit Converter" + description with "323 units" / "43 categories" bolded) now renders above the category picker — previously nothing was shown here at any breakpoint. Category combobox, from/to selects, swap button, value input, and copy-result button all unaffected. + +- [ ] **Step 6: Commit** + +```bash +git add apps/web/src/components/unit-converter/unit-converter-layout.tsx +git commit -m "feat: add page header to Unit Converter (previously had none)" +``` + +--- + +## Task 8: Apply to CSV / Excel ↔ JSON + +**Files:** +- Modify: `apps/web/src/components/csv-excel-json/csv-excel-json-tool.tsx` + +**Interfaces:** +- Consumes: `ToolPinButton` from `apps/web/src/components/tools/tool-header.tsx` (existing export, unchanged) to preserve the pin-to-sidebar affordance this tool currently gets from `ToolHeader`. + +- [ ] **Step 1: Update imports** + +Replace line 20: + +```tsx +import { ToolHeader } from "@/components/tools/tool-header"; +``` + +with: + +```tsx +import { ToolPinButton } from "@/components/tools/tool-header"; +import { ToolPageHeader } from "@/components/tools/tool-page-header"; +import { RevealItem } from "@/components/dashboard/dashboard-reveal"; +import { CATEGORY_ACCENT } from "@/components/dashboard/types"; +``` + +(`IconFileSpreadsheet` is already imported at line 10 for the dropzone icon — reuse it, don't add a second import.) + +- [ ] **Step 2: Replace the header block** + +Replace lines 151-159: + +```tsx + return ( +
+ + + +``` + +with: + +```tsx + return ( +
+
+ + +
+ + +
+
+ + + +``` + +- [ ] **Step 3: Typecheck and lint** + +Run: `cd apps/web && npx tsc --noEmit && npm run lint` +Expected: no errors. If `ToolHeader` shows an unused-import error, confirm Step 1 fully removed it (it's replaced, not kept alongside the new imports). + +- [ ] **Step 4: Manual verification** + +Visit `/app/csv-excel-json`. Icon-chip header renders above the Card, with the pin (heart) button to its right — pinning/unpinning still adds/removes the tool from the sidebar's pinned section exactly as before. Dropzone, sample-load, format/clear/copy/downloadCsv/downloadXlsx buttons all unaffected. + +- [ ] **Step 5: Commit** + +```bash +git add apps/web/src/components/csv-excel-json/csv-excel-json-tool.tsx +git commit -m "refactor: apply shared ToolPageHeader to CSV/Excel<->JSON, preserve pin button" +``` + +--- + +## Task 9: Apply to Image to Base64 + +**Files:** +- Modify: `apps/web/src/components/image-to-base64/image-to-base64-layout.tsx` + +- [ ] **Step 1: Add imports** + +After line 19 (`import { useTranslations } from 'next-intl';`), add: + +```tsx +import { IconPhoto } from '@tabler/icons-react'; +import { ToolPageHeader } from '@/components/tools/tool-page-header'; +import { RevealItem } from '@/components/dashboard/dashboard-reveal'; +import { CATEGORY_ACCENT } from '@/components/dashboard/types'; +``` + +- [ ] **Step 2: Replace the header block** + +Replace lines 99-133: + +```tsx + return ( +
+ {/* Header */} +
+
+

{t('title')}

+

{t('subtitle')}

+
+
+ + { + const file = e.target.files?.[0]; + if (file && file.type.startsWith('image/')) processFile(file); + e.target.value = ''; + }} + /> + +
+
+``` + +with: + +```tsx + return ( +
+
+ + + + + + {/* Mobile upload/clear actions */} +
+ + { + const file = e.target.files?.[0]; + if (file && file.type.startsWith('image/')) processFile(file); + e.target.value = ''; + }} + /> + +
+``` + +- [ ] **Step 3: Typecheck and lint** + +Run: `cd apps/web && npx tsc --noEmit && npm run lint` + +- [ ] **Step 4: Manual verification** + +Visit `/app/image-to-base64`. Header at all breakpoints; Data URI / raw string mode toggle, drag-and-drop image upload, copy, and download all unaffected. + +- [ ] **Step 5: Commit** + +```bash +git add apps/web/src/components/image-to-base64/image-to-base64-layout.tsx +git commit -m "refactor: apply shared ToolPageHeader to Image to Base64" +``` + +--- + +## Task 10: Cross-tool QA pass + +**Files:** none (verification only). + +- [ ] **Step 1: Full-suite typecheck and lint** + +Run: `cd apps/web && npx tsc --noEmit && npm run lint` +Expected: zero errors across the whole app. + +- [ ] **Step 2: Visual sweep across all 8 tools** + +Run: `cd apps/web && npm run dev`. For each of the 8 routes (`/app/base64`, `/app/url-encode`, `/app/url-parser`, `/app/number-base-converter`, `/app/timestamp-converter`, `/app/unit-converter`, `/app/csv-excel-json`, `/app/image-to-base64`), confirm: +- Desktop page header renders with the amber Converters icon chip, correct per-tool icon, title, and description. +- Light and dark mode both render the ambient background and icon chip with sufficient contrast. +- Mobile width (< 768px): header stays legible; any mobile-only action row (Upload/Clear on `base64`/`url-encode`/`url-parser`/`image-to-base64`) still renders below the header and still works. +- No horizontal scroll or layout overflow introduced by the header/ambient background at any of 375px, 768px, 1024px, 1440px widths. +- `prefers-reduced-motion` (devtools "Emulate CSS media feature") removes the header fade-up on every tool, per `RevealItem`. + +- [ ] **Step 3: Regression check on stateful/special-case features** + +- `csv-excel-json`: pin/unpin still adds/removes the tool from the sidebar's pinned section (this is the one tool whose header swap could plausibly have dropped functionality). +- `unit-converter`: confirm no unclosed-tag/layout break from the Task 7 wrapper restructuring — category combobox, swap, and copy-result must all still work. +- All 5 file-based tools (`base64`, `url-encode`, `url-parser`, `image-to-base64`, `csv-excel-json`): drag-and-drop still accepts files and still shows the drag-over visual state. +- All 4 bidirectional tools (`base64`, `url-encode`, `number-base-converter`, `unit-converter`): swap still flips direction/state correctly. + +- [ ] **Step 4: Commit** (only if Steps 1-3 surfaced fixes; otherwise this task ends at Step 3 with nothing to commit) + +```bash +git add -A +git commit -m "fix: address issues found in converters dashboard-alignment QA pass" +``` diff --git a/docs/superpowers/specs/2026-07-03-converters-dashboard-alignment-design.md b/docs/superpowers/specs/2026-07-03-converters-dashboard-alignment-design.md new file mode 100644 index 00000000..ddce7c18 --- /dev/null +++ b/docs/superpowers/specs/2026-07-03-converters-dashboard-alignment-design.md @@ -0,0 +1,94 @@ +# Converters UI/UX Alignment with Dashboard Theme + +**Date:** 2026-07-03 +**Status:** Approved (design phase) + +## Problem + +The "Converters" tool category (`base64`, `url-encode`, `url-parser`, `number-base-converter`, `timestamp-converter`, `unit-converter`, `csv-excel-json`, `image-to-base64` — as defined in `apps/web/src/lib/tool-categories.ts`) shares the dashboard's outer shell (sidebar + nav via `client-layout.tsx`) but each tool page is a bare shadcn form: `Card`/`Label`/`Input`/`Select` with no desktop page header, no icon/accent branding, no motion, no ambient background. The dashboard (`apps/web/src/app/dashboard/`) has a developed visual system: icon-chip section headers, gradient/ring-inset hover-glow cards, staggered `framer-motion` reveals, ambient background layers. On top of the visual gap, common utility affordances (copy, swap direction, file drag-drop) are missing or inconsistent tool-to-tool. + +This is the same class of gap already identified and approved for the Formatters category in `2026-07-03-formatters-dashboard-alignment-design.md`, which explicitly scoped Converters **out** as a separate plan. This document is that plan. + +## Scope + +**In scope:** the 8 tools in the "Converters" category exactly as defined in `tool-categories.ts`: Base64 Encoder (`base64`), URL Encoder (`url-encode`), URL Parser (`url-parser`), Number Base Converter (`number-base-converter`), Timestamp Converter (`timestamp-converter`), Unit Converter (`unit-converter`), CSV/Excel↔JSON (`csv-excel-json`), Image to Base64 (`image-to-base64`). + +**Explicitly excluded:** `format-converter`. It reads as a converter but is canonically categorized as `Formatters` in `tool-categories.ts:20`, and is already claimed by the approved Formatters plan's own rollout (its step 4). Touching it here would duplicate or conflict with that work. + +**Functional scope:** +- Visual parity with dashboard's visual system: icon-chip page header, gradient/ring-inset card treatment, ambient background, `framer-motion` reveal-on-mount, hover effects. +- Common utility features, applied wherever each tool's shape supports them: + - Copy-to-clipboard on every output field. + - Swap/reverse-direction control on bidirectional conversions. + - Drag-and-drop file input on file-based tools. +- No history/recents panel — explicitly declined for this plan. +- No new icon or accent-color assignments — reuse what's already registered (see below). + +## Design + +### 1. Shared component layer — reuse Formatters' pieces, don't fork them + +The Formatters plan already designs `ToolPageHeader`, promoted CSS utilities (`dash-ambient`, `dashboard-grid-bg`, `dash-card-sheen`, generalized from dashboard-only scope), and reuse of `RevealItem`. As of this writing none of these exist yet (`ToolPageHeader`/`ToolActionBar` not found in the tree) — the Formatters plan is approved but unimplemented. + +Rule for whichever plan executes first: build these shared pieces under a category-neutral location (not `components/dashboard/` or `components/formatters/`), e.g. `apps/web/src/components/tools/tool-page-header.tsx`, with generalized CSS utility names already specified by the Formatters plan. The second plan to execute reuses them as-is — no parallel `ConverterPageHeader` fork. + +**New, Converters-specific additions** (not covered by the Formatters plan, since Formatters tools don't need them): +- **`CopyButton`** (`apps/web/src/components/tools/copy-button.tsx`): icon button, `navigator.clipboard.writeText`, checkmark/toast feedback on success, toast on failure. Generic — takes the string to copy as a prop, so any tool's output field can use it. +- **`SwapButton`**: icon button with rotate-on-click animation, takes a `onSwap` callback. Generic — each tool wires its own swap logic since state shape differs per tool (see below). +- **`FileDropzone`**: dashed-border drop target wrapping/replacing existing file ``, click-to-browse fallback, hover state on drag-over. + +### 2. Per-tool integration + +All 8 get `ToolPageHeader` (icon from `sidebar-data.ts`'s existing per-tool icon, e.g. `IconTransform` for Base64) and the dashboard card treatment. Copy/swap/dropzone applied per tool's actual shape — confirmed at implementation time, best current read: + +| Tool | Copy | Swap | Dropzone | +|---|---|---|---| +| Base64 Encoder | output field | encode ↔ decode | — | +| URL Encoder | output field | encode ↔ decode | — | +| URL Parser | per parsed field | — (parse-only, one direction) | — | +| Number Base Converter | output field | from-base ↔ to-base | — | +| Timestamp Converter | per format output | likely N/A — shows multiple formats at once, not A→B (confirm during implementation) | — | +| Unit Converter | output field | from-unit ↔ to-unit | — | +| CSV/Excel↔JSON | JSON/CSV output | conversion direction toggle | file upload | +| Image to Base64 | base64 output | — (one-directional: image → string only) | image upload | + +### 3. Card treatment + +Toolbar and result `Card`s adopt dashboard's `bg-gradient-to-br from-card to-card/80`, `ring-1 ring-inset ring-border/50`, and hover border/shadow glow — same recipe as the Formatters plan, not a new one. + +### 4. Spacing normalization + +Match dashboard's `gap-3 md:gap-4 xl:gap-5` (grouped fields) and `p-3 md:p-3.5` (card padding) rhythm, replacing each tool's current ad hoc spacing. + +### 5. Color accent + +Converters category accent is already registered: `bg-amber-500/10` / `text-amber-600 dark:text-amber-400` (`apps/web/src/components/dashboard/types.ts:132`, `categoryAccent('Converters')`). Applied to each `ToolPageHeader` icon chip — no new accent color invented. + +### 6. Motion + +`RevealItem` stagger-fade on mount: header → toolbar → result card, matching the dashboard's and Formatters' section stagger pattern. Must respect `prefers-reduced-motion` (already handled by `RevealItem`). + +## Rollout order + +Build shared pieces first (or reuse them if the Formatters plan lands first), then batch-apply to all 8 Converters tools in one pass — no separate pilot-then-rollout phase. + +## Error handling + +- Clipboard write failure (permissions/insecure context): catch, show error toast, don't throw uncaught. +- Dropzone: reject non-matching file types with each tool's existing inline validation error pattern — no new validation framework. +- Swap: no-op safely if there's nothing meaningful to swap (e.g., empty input). + +## Testing + +No test framework in the repo (`apps/web/package.json` has no `test` script) — verification is manual: +- Light/dark mode and the accent-color theme variants (`.blue`/`.purple`/`.green` etc. on ``). +- Mobile breakpoints for `ToolPageHeader` and any switcher UI. +- `prefers-reduced-motion` — reveal animation must no-op correctly. +- Exercise all 8 tools: copy works on every output field, swap works where applicable, dropzone accepts/rejects files correctly on the 2 file-based tools. +- `eslint .` clean. + +## Explicitly out of scope + +- `format-converter` — owned by the Formatters plan. +- History/recents panel. +- New icon or accent-color assignments beyond what's already registered per tool/category.