diff --git a/apps/api-runner/node_modules/.bin/tsc b/apps/api-runner/node_modules/.bin/tsc index bc3dbe24..83c6daaa 100755 --- a/apps/api-runner/node_modules/.bin/tsc +++ b/apps/api-runner/node_modules/.bin/tsc @@ -6,9 +6,9 @@ case `uname` in esac if [ -z "$NODE_PATH" ]; then - export NODE_PATH="/Users/max/Works/Personal/mydevtools.tech/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/bin/node_modules:/Users/max/Works/Personal/mydevtools.tech/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/Users/max/Works/Personal/mydevtools.tech/node_modules/.pnpm/typescript@5.9.3/node_modules:/Users/max/Works/Personal/mydevtools.tech/node_modules/.pnpm/node_modules" + export NODE_PATH="/Users/max/Works/Personal/mydevtools.tech/.claude/worktrees/formatters-dashboard-alignment/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/bin/node_modules:/Users/max/Works/Personal/mydevtools.tech/.claude/worktrees/formatters-dashboard-alignment/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/Users/max/Works/Personal/mydevtools.tech/.claude/worktrees/formatters-dashboard-alignment/node_modules/.pnpm/typescript@5.9.3/node_modules:/Users/max/Works/Personal/mydevtools.tech/.claude/worktrees/formatters-dashboard-alignment/node_modules/.pnpm/node_modules" else - export NODE_PATH="/Users/max/Works/Personal/mydevtools.tech/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/bin/node_modules:/Users/max/Works/Personal/mydevtools.tech/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/Users/max/Works/Personal/mydevtools.tech/node_modules/.pnpm/typescript@5.9.3/node_modules:/Users/max/Works/Personal/mydevtools.tech/node_modules/.pnpm/node_modules:$NODE_PATH" + export NODE_PATH="/Users/max/Works/Personal/mydevtools.tech/.claude/worktrees/formatters-dashboard-alignment/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/bin/node_modules:/Users/max/Works/Personal/mydevtools.tech/.claude/worktrees/formatters-dashboard-alignment/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/Users/max/Works/Personal/mydevtools.tech/.claude/worktrees/formatters-dashboard-alignment/node_modules/.pnpm/typescript@5.9.3/node_modules:/Users/max/Works/Personal/mydevtools.tech/.claude/worktrees/formatters-dashboard-alignment/node_modules/.pnpm/node_modules:$NODE_PATH" fi if [ -x "$basedir/node" ]; then exec "$basedir/node" "$basedir/../typescript/bin/tsc" "$@" diff --git a/apps/api-runner/node_modules/.bin/tsserver b/apps/api-runner/node_modules/.bin/tsserver index 03c2f186..07973412 100755 --- a/apps/api-runner/node_modules/.bin/tsserver +++ b/apps/api-runner/node_modules/.bin/tsserver @@ -6,9 +6,9 @@ case `uname` in esac if [ -z "$NODE_PATH" ]; then - export NODE_PATH="/Users/max/Works/Personal/mydevtools.tech/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/bin/node_modules:/Users/max/Works/Personal/mydevtools.tech/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/Users/max/Works/Personal/mydevtools.tech/node_modules/.pnpm/typescript@5.9.3/node_modules:/Users/max/Works/Personal/mydevtools.tech/node_modules/.pnpm/node_modules" + export NODE_PATH="/Users/max/Works/Personal/mydevtools.tech/.claude/worktrees/formatters-dashboard-alignment/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/bin/node_modules:/Users/max/Works/Personal/mydevtools.tech/.claude/worktrees/formatters-dashboard-alignment/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/Users/max/Works/Personal/mydevtools.tech/.claude/worktrees/formatters-dashboard-alignment/node_modules/.pnpm/typescript@5.9.3/node_modules:/Users/max/Works/Personal/mydevtools.tech/.claude/worktrees/formatters-dashboard-alignment/node_modules/.pnpm/node_modules" else - export NODE_PATH="/Users/max/Works/Personal/mydevtools.tech/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/bin/node_modules:/Users/max/Works/Personal/mydevtools.tech/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/Users/max/Works/Personal/mydevtools.tech/node_modules/.pnpm/typescript@5.9.3/node_modules:/Users/max/Works/Personal/mydevtools.tech/node_modules/.pnpm/node_modules:$NODE_PATH" + export NODE_PATH="/Users/max/Works/Personal/mydevtools.tech/.claude/worktrees/formatters-dashboard-alignment/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/bin/node_modules:/Users/max/Works/Personal/mydevtools.tech/.claude/worktrees/formatters-dashboard-alignment/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/Users/max/Works/Personal/mydevtools.tech/.claude/worktrees/formatters-dashboard-alignment/node_modules/.pnpm/typescript@5.9.3/node_modules:/Users/max/Works/Personal/mydevtools.tech/.claude/worktrees/formatters-dashboard-alignment/node_modules/.pnpm/node_modules:$NODE_PATH" fi if [ -x "$basedir/node" ]; then exec "$basedir/node" "$basedir/../typescript/bin/tsserver" "$@" diff --git a/apps/web/package.json b/apps/web/package.json index 6792b3af..a10d38a7 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -42,6 +42,7 @@ "@radix-ui/react-toggle": "^1.1.10", "@radix-ui/react-toggle-group": "^1.1.11", "@radix-ui/react-tooltip": "^1.2.8", + "@radix-ui/react-visually-hidden": "^1.2.7", "@simplewebauthn/browser": "^11.0.0", "@space-man/react-theme-animation": "^1.1.1", "@tabler/icons-react": "^3.35.0", diff --git a/apps/web/src/components/diff-checker/diff-checker-layout.tsx b/apps/web/src/components/diff-checker/diff-checker-layout.tsx index 722abf4c..4fdcf6de 100644 --- a/apps/web/src/components/diff-checker/diff-checker-layout.tsx +++ b/apps/web/src/components/diff-checker/diff-checker-layout.tsx @@ -14,6 +14,10 @@ import { cn } from '@/lib/utils'; import { Trash2 } from 'lucide-react'; import { useTranslations } from 'next-intl'; import { useIsMobile } from '@/components/hooks/use-mobile'; +import { IconGitCompare } from '@tabler/icons-react'; +import { ToolPageHeader } from '@/components/tools/tool-page-header'; +import { ToolMobileTabs } from '@/components/tools/tool-mobile-tabs'; +import { RevealItem } from '@/components/dashboard/dashboard-reveal'; const SAMPLE_A = `function greet(name) { console.log("Hello, " + name); @@ -150,11 +154,11 @@ export function DiffCheckerLayout() { ); return ( -
-
-

{t('title')}

-

{t('subtitle')}

-
+
+
+ + + {/* Status + clear row */}
@@ -177,21 +181,15 @@ export function DiffCheckerLayout() { {isMobile ? ( /* ── Mobile: 3-tab layout (Original / Modified / Diff) ── */ <> -
- {(['original', 'modified', 'diff'] as const).map((tab) => ( - - ))} -
+
{mobileTab === 'original' && ( diff --git a/apps/web/src/components/format-converter/format-converter-layout.tsx b/apps/web/src/components/format-converter/format-converter-layout.tsx index eb6f649f..3e3b0377 100644 --- a/apps/web/src/components/format-converter/format-converter-layout.tsx +++ b/apps/web/src/components/format-converter/format-converter-layout.tsx @@ -7,6 +7,10 @@ import dynamic from 'next/dynamic'; import { useTheme } from 'next-themes'; import { useTranslations } from 'next-intl'; import { ArrowLeftRight, Copy, Check, AlertCircle } from 'lucide-react'; +import { IconFileCode } from '@tabler/icons-react'; +import { ToolPageHeader } from '@/components/tools/tool-page-header'; +import { ToolMobileTabs } from '@/components/tools/tool-mobile-tabs'; +import { RevealItem } from '@/components/dashboard/dashboard-reveal'; import { Card } from '@/components/ui/card'; import { Label } from '@/components/ui/label'; import { Button } from '@/components/ui/button'; @@ -95,29 +99,21 @@ export function FormatConverterLayout() { }, [to]); return ( -
-
-

{t('title')}

-

{t('subtitle')}

-
+
+
+ + + {isMobile && ( -
- {(['input', 'output'] as const).map((tab) => ( - - ))} -
+ )}
diff --git a/apps/web/src/components/graphql-formatter/graphql-formatter-layout.tsx b/apps/web/src/components/graphql-formatter/graphql-formatter-layout.tsx index 71b4cdca..b8d3e857 100644 --- a/apps/web/src/components/graphql-formatter/graphql-formatter-layout.tsx +++ b/apps/web/src/components/graphql-formatter/graphql-formatter-layout.tsx @@ -2,13 +2,18 @@ import dynamic from 'next/dynamic'; import { parse, print, stripIgnoredCharacters } from 'graphql'; -import { AlertCircle, Check, Copy, Trash2, Wand2 } from 'lucide-react'; +import { AlertCircle, Trash2, Wand2 } from 'lucide-react'; import { useTheme } from 'next-themes'; import { useCallback, useState } from 'react'; import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard'; import { useTranslations } from 'next-intl'; import { registerGraphqlMonarch } from '@/components/graphql-formatter/register-graphql-monarch'; import { useIsMobile } from '@/components/hooks/use-mobile'; +import { IconBrandGraphql } from '@tabler/icons-react'; +import { ToolPageHeader } from '@/components/tools/tool-page-header'; +import { ToolMobileTabs } from '@/components/tools/tool-mobile-tabs'; +import { CopyIconButton } from '@/components/tools/copy-icon-button'; +import { RevealItem } from '@/components/dashboard/dashboard-reveal'; import { Button } from '@/components/ui/button'; import { Card } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; @@ -122,11 +127,11 @@ export function GraphqlFormatterLayout() { }; return ( -
-
-

{t('title')}

-

{t('subtitle')}

-
+
+
+ + + setPanelTab(v as 'format' | 'build')}> @@ -244,30 +249,14 @@ export function GraphqlFormatterLayout() { )} {isMobile && ( -
- - -
+ )}
{t('outputPanel')} - +
- - -
-
-
- -
-
-

- {t('title')} -

-

- {t('description')} -

-
-
-
-
-
+
+
+ + + + - {/* Mobile tab switcher */} {isMobile && ( -
- - -
+ )} {isMobile ? ( diff --git a/apps/web/src/components/json-schema-generator/json-schema-generator-layout.tsx b/apps/web/src/components/json-schema-generator/json-schema-generator-layout.tsx index 7f87edad..921e742f 100644 --- a/apps/web/src/components/json-schema-generator/json-schema-generator-layout.tsx +++ b/apps/web/src/components/json-schema-generator/json-schema-generator-layout.tsx @@ -4,7 +4,11 @@ import { useCallback, useMemo, useState } from 'react'; import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard'; import { useIsMobile } from '@/components/hooks/use-mobile'; import { useTranslations } from 'next-intl'; -import { AlertCircle, Check, Copy, Download, FileJson, Trash2 } from 'lucide-react'; +import { AlertCircle, Check, Copy, Download, Trash2 } from 'lucide-react'; +import { IconBraces } from '@tabler/icons-react'; +import { ToolPageHeader } from '@/components/tools/tool-page-header'; +import { ToolMobileTabs } from '@/components/tools/tool-mobile-tabs'; +import { RevealItem } from '@/components/dashboard/dashboard-reveal'; import { Button } from '@/components/ui/button'; import { Card } from '@/components/ui/card'; import { Label } from '@/components/ui/label'; @@ -107,18 +111,11 @@ export function JsonSchemaGeneratorLayout() { }; return ( -
-
-
-
- -
-
-

{t('title')}

-

{t('subtitle')}

-
-
-
+
+
+ + +
@@ -167,22 +164,14 @@ export function JsonSchemaGeneratorLayout() {
{isMobile && ( -
- {(['input', 'output'] as const).map((tab) => ( - - ))} -
+ )}
diff --git a/apps/web/src/components/markdown-preview-html/markdown-preview-html-layout.tsx b/apps/web/src/components/markdown-preview-html/markdown-preview-html-layout.tsx index d77d7f47..92639879 100644 --- a/apps/web/src/components/markdown-preview-html/markdown-preview-html-layout.tsx +++ b/apps/web/src/components/markdown-preview-html/markdown-preview-html-layout.tsx @@ -11,6 +11,9 @@ import { Textarea } from '@/components/ui/textarea'; import { Button } from '@/components/ui/button'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Copy, Check, Download, Eye, Code } from 'lucide-react'; +import { IconMarkdown } from '@tabler/icons-react'; +import { ToolPageHeader } from '@/components/tools/tool-page-header'; +import { RevealItem } from '@/components/dashboard/dashboard-reveal'; // Configure marked for safe, synchronous rendering marked.setOptions({ async: false }); @@ -104,11 +107,11 @@ export function MarkdownPreviewLayout() { }, [convertedMarkdown, copyMd]); return ( -
-
-

{t('title')}

-

{t('subtitle')}

-
+
+
+ + + diff --git a/apps/web/src/components/regex-tester/regex-tester-layout.tsx b/apps/web/src/components/regex-tester/regex-tester-layout.tsx index 20ef3d46..174fb8e8 100644 --- a/apps/web/src/components/regex-tester/regex-tester-layout.tsx +++ b/apps/web/src/components/regex-tester/regex-tester-layout.tsx @@ -20,6 +20,9 @@ import { } from '@/lib/regex-tester'; import { cn } from '@/lib/utils'; import { AlertCircle, ChevronLeft, ChevronRight, Trash2 } from 'lucide-react'; +import { IconRegex } from '@tabler/icons-react'; +import { ToolPageHeader } from '@/components/tools/tool-page-header'; +import { RevealItem } from '@/components/dashboard/dashboard-reveal'; import { useTranslations } from 'next-intl'; function HighlightedText({ @@ -123,11 +126,11 @@ export function RegexTesterLayout() { }; return ( -
-
-

{t('title')}

-

{t('subtitle')}

-
+
+
+ + +
diff --git a/apps/web/src/components/sql-formatter/sql-formatter-layout.tsx b/apps/web/src/components/sql-formatter/sql-formatter-layout.tsx index 00aa87af..3df7a03a 100644 --- a/apps/web/src/components/sql-formatter/sql-formatter-layout.tsx +++ b/apps/web/src/components/sql-formatter/sql-formatter-layout.tsx @@ -7,6 +7,11 @@ import { Button } from '@/components/ui/button'; import { Card } from '@/components/ui/card'; import { Label } from '@/components/ui/label'; import { useIsMobile } from '@/components/hooks/use-mobile'; +import { IconSql } from '@tabler/icons-react'; +import { ToolPageHeader } from '@/components/tools/tool-page-header'; +import { ToolMobileTabs } from '@/components/tools/tool-mobile-tabs'; +import { CopyIconButton } from '@/components/tools/copy-icon-button'; +import { RevealItem } from '@/components/dashboard/dashboard-reveal'; import { Select, SelectContent, @@ -14,7 +19,7 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select'; -import { AlertCircle, Check, Copy, Trash2, Wand2 } from 'lucide-react'; +import { AlertCircle, Trash2, Wand2 } from 'lucide-react'; import { useTranslations } from 'next-intl'; import CodeEditor from '@/components/ui/code-editor'; @@ -82,11 +87,11 @@ export function SqlFormatterLayout() { }; return ( -
-
-

{t('title')}

-

{t('subtitle')}

-
+
+
+ + +
@@ -171,28 +176,14 @@ export function SqlFormatterLayout() { )} {isMobile && ( -
- - -
+ )}
@@ -219,17 +210,7 @@ export function SqlFormatterLayout() { - +
void + copied: boolean + disabled?: boolean + label: string + className?: string +} + +/** + * Ghost icon-only copy button for a tool output panel header — the pattern + * already shared identically by the YAML/SQL/GraphQL formatters, extracted + * to a single source of truth. + */ +export function CopyIconButton({ + onCopy, + copied, + disabled, + label, + className, +}: CopyIconButtonProps) { + return ( + + ) +} diff --git a/apps/web/src/components/tools/tool-mobile-tabs.tsx b/apps/web/src/components/tools/tool-mobile-tabs.tsx new file mode 100644 index 00000000..2137bb46 --- /dev/null +++ b/apps/web/src/components/tools/tool-mobile-tabs.tsx @@ -0,0 +1,36 @@ +'use client' + +import * as React from 'react' +import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs' + +interface ToolMobileTabsProps { + value: T + onValueChange: (value: T) => void + tabs: { value: T; label: string }[] + className?: string +} + +/** + * Shared mobile input/output switcher for tool pages — replaces each tool's + * hand-rolled pill-button toggle (`bg-muted/40 p-1` div) with the shadcn Tabs + * primitive the rest of the app already uses (e.g. dashboard's Apps/Analytics + * switch, markdown-preview-html's format switch). + */ +export function ToolMobileTabs({ + value, + onValueChange, + tabs, + className, +}: ToolMobileTabsProps) { + return ( + onValueChange(v as T)} className={className}> + + {tabs.map((tab) => ( + + {tab.label} + + ))} + + + ) +} diff --git a/apps/web/src/components/tools/tool-page-header.tsx b/apps/web/src/components/tools/tool-page-header.tsx new file mode 100644 index 00000000..654ec336 --- /dev/null +++ b/apps/web/src/components/tools/tool-page-header.tsx @@ -0,0 +1,44 @@ +'use client' + +import * as React from 'react' +import { cn } from '@/lib/utils' +import { CATEGORY_ACCENT } from '@/components/dashboard/types' + +interface ToolPageHeaderProps { + icon: React.ElementType + title: string + description: string + accent?: { bg: string; text: string } + className?: string +} + +/** + * Page-level header for a tool page: accented icon chip + title + description, + * shown at every breakpoint. Mirrors DashboardSectionHeader's icon-chip + * hierarchy so a tool page reads as part of the same product as the dashboard. + */ +export function ToolPageHeader({ + icon: Icon, + title, + description, + accent = CATEGORY_ACCENT.Formatters, + className, +}: ToolPageHeaderProps) { + return ( +
+ + + +
+

{title}

+

{description}

+
+
+ ) +} diff --git a/apps/web/src/components/yaml-formatter/yaml-formatter-layout.tsx b/apps/web/src/components/yaml-formatter/yaml-formatter-layout.tsx index c4bc539e..f0691f80 100644 --- a/apps/web/src/components/yaml-formatter/yaml-formatter-layout.tsx +++ b/apps/web/src/components/yaml-formatter/yaml-formatter-layout.tsx @@ -7,6 +7,11 @@ import { Button } from '@/components/ui/button' import { Card } from '@/components/ui/card' import { Label } from '@/components/ui/label' import { useIsMobile } from '@/components/hooks/use-mobile' +import { IconFileCode } from '@tabler/icons-react' +import { ToolPageHeader } from '@/components/tools/tool-page-header' +import { ToolMobileTabs } from '@/components/tools/tool-mobile-tabs' +import { CopyIconButton } from '@/components/tools/copy-icon-button' +import { RevealItem } from '@/components/dashboard/dashboard-reveal' import { Select, SelectContent, @@ -14,7 +19,7 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select' -import { AlertCircle, Check, Copy, Trash2, Wand2 } from 'lucide-react' +import { AlertCircle, Trash2, Wand2 } from 'lucide-react' import { useTranslations } from 'next-intl' import CodeEditor from '@/components/ui/code-editor' @@ -92,11 +97,11 @@ export function YamlFormatterLayout() { } return ( -
-
-

{t('title')}

-

{t('subtitle')}

-
+
+
+ + +
@@ -163,28 +168,14 @@ export function YamlFormatterLayout() { )} {isMobile && ( -
- - -
+ )}
@@ -211,17 +202,7 @@ export function YamlFormatterLayout() { - +
**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 9 tools in the "Formatters" category (json-formatter, json-schema-generator, sql-formatter, graphql-formatter, yaml-formatter, markdown-preview-html, format-converter, diff-checker, regex-tester) 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:** Three small shared components (`ToolPageHeader`, `ToolMobileTabs`, `CopyIconButton`) live in a new `apps/web/src/components/tools/` folder and get applied to each of the 9 tool layout files one at a time, pilot-tested against the hardest case (JSON formatter) first. 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. + +**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 9 tools per `sidebar-data.ts`), `lucide-react` (already used inside formatter layouts for non-branding icons), `framer-motion` (via the existing `RevealItem`), `next-intl`. + +## Global Constraints + +- **Scope is exactly 9 tools**, per `apps/web/src/lib/tool-categories.ts:14-22`: `json-formatter`, `json-schema-generator`, `sql-formatter`, `graphql-formatter`, `yaml-formatter`, `markdown-preview-html`, `format-converter`, `diff-checker`, `regex-tester`. No converters, no other categories. +- **No new functionality.** No new save/load, no history, no new action buttons beyond what a tool already has. Reuse each tool's existing `next-intl` keys (`t('title')`, and `t('subtitle')` or `t('description')` — whichever the tool already defines) — do not add new translation keys. +- **Accent color:** all 9 tools share one accent — `CATEGORY_ACCENT.Formatters` from `apps/web/src/components/dashboard/types.ts:131`: `{ bg: 'bg-emerald-500/10', text: 'text-emerald-600 dark:text-emerald-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:152-210` (the actual source of truth for tool icons), all from `@tabler/icons-react`: + | Tool | Icon | + |---|---| + | json-formatter | `IconJson` | + | json-schema-generator | `IconBraces` | + | sql-formatter | `IconSql` | + | graphql-formatter | `IconBrandGraphql` | + | yaml-formatter | `IconFileCode` | + | markdown-preview-html | `IconMarkdown` | + | format-converter | `IconFileCode` | + | diff-checker | `IconGitCompare` | + | regex-tester | `IconRegex` | +- **No test framework for component rendering exists in this repo.** `jest.config.js` sets `testEnvironment: 'jest-environment-node'` and there is no `@testing-library/react` dependency — existing `__tests__` files test pure logic/utils only, never render React components. Every task below is therefore 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/`. +- **Two corrections to the approved design spec** (`docs/superpowers/specs/2026-07-03-formatters-dashboard-alignment-design.md`), found while reading the exact current code — noted here so they aren't silently dropped: + 1. **Drop `dash-card-sheen` from formatter toolbar/panel Cards.** Reading `apps/web/src/app/globals.css:854` and `873` shows the sheen's opacity is gated by `.group:hover .dash-card-sheen::before/::after` — it only ever renders because `dashboard-tool-card.tsx:90` wraps the card in ``. Formatter toolbar/panel Cards are not links and have no such wrapper; adding the class would add dead CSS with no visible effect. The visual win instead comes from the new accent icon chip, ambient background, and motion (items below) — sheen is skipped, not replaced. + 2. **Skip importing dashboard's page-level spacing rhythm** (`space-y-5 md:space-y-8`, `p-3 md:p-3.5`). Dashboard is a scrolling page; formatter tool pages are fixed-height (`h-full`, `min-h-0`, `flex-1`) dual-pane layouts where the editor claims all remaining vertical space. Loosening spacing to dashboard's rhythm would shrink usable editor height for no visual benefit. The one real spacing inconsistency — `diff-checker` using `gap-3` at its root while the other 8 use `gap-4` — is fixed in Task 10 for internal formatter-family consistency, not to match dashboard. +- **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. No rename task exists; they're simply imported by class name into non-dashboard files. +- **Third correction: no `bg-gradient-to-br from-card to-card/80` on toolbar/panel Cards.** The design spec called for adopting this gradient (from `dashboard-tool-card.tsx:98`) on formatter Cards. Reading `apps/web/src/components/ui/card.tsx:5-18` shows the shared `Card` primitive already renders `bg-card` with a themed shadow — every formatter Card already uses this same primitive, so it's already visually identical to dashboard's *base* card recipe. The extra gradient in `dashboard-tool-card.tsx` exists specifically to sell a hoverable/clickable link card; layering it onto a static toolbar or editor-panel Card (which isn't a single click target) would be decoration copied from the wrong context. The real, correctly-scoped visual delta this plan delivers is the page header, accent icon chip, ambient background, and motion — not a card-background change. + +--- + +## Task 1: `ToolPageHeader` shared component + +**Files:** +- Create: `apps/web/src/components/tools/tool-page-header.tsx` + +**Interfaces:** +- Produces: `ToolPageHeader({ icon: React.ElementType, title: string, description: string, accent?: { bg: string; text: string }, className?: string })` — a JSX component. Default `accent` is `CATEGORY_ACCENT.Formatters`. +- Consumes: `CATEGORY_ACCENT` from `apps/web/src/components/dashboard/types.ts` (existing export, confirmed at line 128-138), `cn` from `apps/web/src/lib/utils`. + +- [ ] **Step 1: Create the component** + +```tsx +'use client' + +import * as React from 'react' +import { cn } from '@/lib/utils' +import { CATEGORY_ACCENT } from '@/components/dashboard/types' + +interface ToolPageHeaderProps { + icon: React.ElementType + title: string + description: string + accent?: { bg: string; text: string } + className?: string +} + +/** + * Page-level header for a tool page: accented icon chip + title + description, + * shown at every breakpoint. Mirrors DashboardSectionHeader's icon-chip + * hierarchy so a tool page reads as part of the same product as the dashboard. + */ +export function ToolPageHeader({ + icon: Icon, + title, + description, + accent = CATEGORY_ACCENT.Formatters, + className, +}: ToolPageHeaderProps) { + return ( +
+ + + +
+

{title}

+

{description}

+
+
+ ) +} +``` + +- [ ] **Step 2: Typecheck and lint** + +Run: `cd apps/web && npx tsc --noEmit && npm run lint` +Expected: no errors related to `tool-page-header.tsx`. + +- [ ] **Step 3: Commit** + +```bash +git add apps/web/src/components/tools/tool-page-header.tsx +git commit -m "feat: add shared ToolPageHeader component for formatter pages" +``` + +--- + +## Task 2: `ToolMobileTabs` shared component + +**Files:** +- Create: `apps/web/src/components/tools/tool-mobile-tabs.tsx` + +**Interfaces:** +- Produces: `ToolMobileTabs({ value: T, onValueChange: (v: T) => void, tabs: { value: T; label: string }[], className?: string })`. +- Consumes: `Tabs`, `TabsList`, `TabsTrigger` from `apps/web/src/components/ui/tabs.tsx` (existing, confirmed lines 8, 10-23, 25-38). + +- [ ] **Step 1: Create the component** + +```tsx +'use client' + +import * as React from 'react' +import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs' + +interface ToolMobileTabsProps { + value: T + onValueChange: (value: T) => void + tabs: { value: T; label: string }[] + className?: string +} + +/** + * Shared mobile input/output switcher for tool pages — replaces each tool's + * hand-rolled pill-button toggle (`bg-muted/40 p-1` div) with the shadcn Tabs + * primitive the rest of the app already uses (e.g. dashboard's Apps/Analytics + * switch, markdown-preview-html's format switch). + */ +export function ToolMobileTabs({ + value, + onValueChange, + tabs, + className, +}: ToolMobileTabsProps) { + return ( + onValueChange(v as T)} className={className}> + + {tabs.map((tab) => ( + + {tab.label} + + ))} + + + ) +} +``` + +- [ ] **Step 2: Typecheck and lint** + +Run: `cd apps/web && npx tsc --noEmit && npm run lint` +Expected: no errors related to `tool-mobile-tabs.tsx`. + +- [ ] **Step 3: Commit** + +```bash +git add apps/web/src/components/tools/tool-mobile-tabs.tsx +git commit -m "feat: add shared ToolMobileTabs component for formatter pages" +``` + +--- + +## Task 3: `CopyIconButton` shared component + +**Files:** +- Create: `apps/web/src/components/tools/copy-icon-button.tsx` + +**Interfaces:** +- Produces: `CopyIconButton({ onCopy: () => void, copied: boolean, disabled?: boolean, label: string, className?: string })`. +- Consumes: `Button` from `apps/web/src/components/ui/button.tsx`. +- Note: this extracts the exact pattern already duplicated identically in `yaml-formatter-layout.tsx:214-224`, `sql-formatter-layout.tsx:222-232`, and `graphql-formatter-layout.tsx:300-310`. It does **not** apply to tools where copy is one of several labeled buttons in a row (json-schema-generator, markdown-preview-html, format-converter) — those are a different UI shape (multi-button labeled row vs. lone icon action in a panel header) and are left as-is per YAGNI. + +- [ ] **Step 1: Create the component** + +```tsx +'use client' + +import * as React from 'react' +import { Check, Copy } from 'lucide-react' +import { Button } from '@/components/ui/button' + +interface CopyIconButtonProps { + onCopy: () => void + copied: boolean + disabled?: boolean + label: string + className?: string +} + +/** + * Ghost icon-only copy button for a tool output panel header — the pattern + * already shared identically by the YAML/SQL/GraphQL formatters, extracted + * to a single source of truth. + */ +export function CopyIconButton({ + onCopy, + copied, + disabled, + label, + className, +}: CopyIconButtonProps) { + return ( + + ) +} +``` + +- [ ] **Step 2: Typecheck and lint** + +Run: `cd apps/web && npx tsc --noEmit && npm run lint` +Expected: no errors related to `copy-icon-button.tsx`. + +- [ ] **Step 3: Commit** + +```bash +git add apps/web/src/components/tools/copy-icon-button.tsx +git commit -m "feat: add shared CopyIconButton component for formatter output panels" +``` + +--- + +## Task 4: Apply to JSON Formatter (pilot) + +This is the hardest case (resizable dual-pane via `react-resizable-panels`, save/load modal) — proving the pattern here first surfaces integration problems before the cheaper batch tasks. + +**Files:** +- Modify: `apps/web/src/components/json-formatter/json-formatter-layout.tsx` + +**Interfaces:** +- Consumes: `ToolPageHeader` (Task 1), `ToolMobileTabs` (Task 2), `RevealItem` from `apps/web/src/components/dashboard/dashboard-reveal.tsx` (existing, confirmed). + +- [ ] **Step 1: Update imports** + +Replace line 18: +```tsx +import { Card, CardHeader } from '@/components/ui/card' +``` +with (drop `Card`/`CardHeader` — after Step 2 they're no longer used anywhere in this file): +```tsx +import { ToolPageHeader } from '@/components/tools/tool-page-header' +import { ToolMobileTabs } from '@/components/tools/tool-mobile-tabs' +import { RevealItem } from '@/components/dashboard/dashboard-reveal' +``` + +- [ ] **Step 2: Replace the header block, mobile tab switcher, and add the page shell wrapper** + +Replace lines 320-366 (the full `return (` opening through the end of the mobile tab-switcher `{isMobile && (...)}` block) with: + +```tsx + return ( +
+
+ + + + + + {isMobile && ( + + + + )} +``` + +(The rest of the file — the `{isMobile ? ( ... ) : ( ... )}` resizable-panel block and the `` — is unchanged. Only the opening `
` through the old mobile-tab-switcher block is replaced.) + +- [ ] **Step 3: Typecheck and lint** + +Run: `cd apps/web && npx tsc --noEmit && npm run lint` +Expected: no errors. If `IconJson` or `Card`/`CardHeader` show unused-import or undefined errors, confirm `IconJson` is still imported at line 12 (it is — used for the header icon now instead of the old inline chip) and that `Card`/`CardHeader` have no remaining references in the file. + +- [ ] **Step 4: Manual verification** + +Run: `cd apps/web && npm run dev`, visit `/app/json-formatter`. +Expected: +- Desktop: page now shows an icon-chip + "JSON Formatter" title + description at the top (previously only visible on mobile). +- Icon chip uses the emerald Formatters accent, not the old primary/violet gradient. +- Toggle to a narrow viewport (< 768px): the input/output switcher now renders as shadcn `Tabs` pills instead of the old hand-rolled buttons, and still switches the Text/Tree pane correctly. +- Toggle dark mode: header and ambient background render correctly in both themes. +- Enable "reduce motion" in OS accessibility settings (or devtools emulation) and reload: header/tabs appear immediately with no fade-up (per `RevealItem`'s `useReducedMotion` check). +- Save/load modal (click "Save"/"Load" in the per-pane toolbar) still opens and works — this feature must be untouched by this refactor. + +- [ ] **Step 5: Commit** + +```bash +git add apps/web/src/components/json-formatter/json-formatter-layout.tsx +git commit -m "refactor: apply shared ToolPageHeader/ToolMobileTabs to JSON formatter" +``` + +--- + +## Task 5: Apply to YAML Formatter (+ extract CopyIconButton) + +**Files:** +- Modify: `apps/web/src/components/yaml-formatter/yaml-formatter-layout.tsx` + +- [ ] **Step 1: Update imports** + +Add after line 9 (`import { useIsMobile } from '@/components/hooks/use-mobile'`): +```tsx +import { IconFileCode } from '@tabler/icons-react' +import { ToolPageHeader } from '@/components/tools/tool-page-header' +import { ToolMobileTabs } from '@/components/tools/tool-mobile-tabs' +import { CopyIconButton } from '@/components/tools/copy-icon-button' +import { RevealItem } from '@/components/dashboard/dashboard-reveal' +``` + +- [ ] **Step 2: Replace the header block and wrap the root wrapper** + +Replace lines 94-99: +```tsx + return ( +
+
+

{t('title')}

+

{t('subtitle')}

+
+``` +with: +```tsx + return ( +
+
+ + + +``` + +- [ ] **Step 3: Replace the mobile tab switcher** + +Replace lines 165-188: +```tsx + {isMobile && ( +
+ + +
+ )} +``` +with: +```tsx + {isMobile && ( + + )} +``` + +- [ ] **Step 4: Replace the inline copy button** + +Replace lines 214-224: +```tsx + +``` +with: +```tsx + +``` + +Then remove `Check` and `Copy` from the lucide-react import on line 17 if no longer used elsewhere in the file (they aren't — `AlertCircle`, `Trash2`, `Wand2` remain): +```tsx +import { AlertCircle, Trash2, Wand2 } from 'lucide-react' +``` + +- [ ] **Step 5: Typecheck and lint** + +Run: `cd apps/web && npx tsc --noEmit && npm run lint` +Expected: no errors. + +- [ ] **Step 6: Manual verification** + +Run: `cd apps/web && npm run dev`, visit `/app/yaml-formatter`. Confirm: desktop header now visible with emerald icon chip; mobile switcher is shadcn Tabs; Copy button in output panel still copies and still shows the check-mark toggle; Format/Clear buttons unaffected. + +- [ ] **Step 7: Commit** + +```bash +git add apps/web/src/components/yaml-formatter/yaml-formatter-layout.tsx +git commit -m "refactor: apply shared tool-page components to YAML formatter" +``` + +--- + +## Task 6: Apply to SQL Formatter + +**Files:** +- Modify: `apps/web/src/components/sql-formatter/sql-formatter-layout.tsx` + +- [ ] **Step 1: Update imports** + +Add after line 9 (`import { useIsMobile } from '@/components/hooks/use-mobile';`): +```tsx +import { IconSql } from '@tabler/icons-react'; +import { ToolPageHeader } from '@/components/tools/tool-page-header'; +import { ToolMobileTabs } from '@/components/tools/tool-mobile-tabs'; +import { CopyIconButton } from '@/components/tools/copy-icon-button'; +import { RevealItem } from '@/components/dashboard/dashboard-reveal'; +``` + +- [ ] **Step 2: Replace the header block** + +Replace lines 84-89: +```tsx + return ( +
+
+

{t('title')}

+

{t('subtitle')}

+
+``` +with: +```tsx + return ( +
+
+ + + +``` + +- [ ] **Step 3: Replace the mobile tab switcher** + +Replace lines 173-196: +```tsx + {isMobile && ( +
+ + +
+ )} +``` +with: +```tsx + {isMobile && ( + + )} +``` + +- [ ] **Step 4: Replace the inline copy button** + +Replace lines 222-232: +```tsx + +``` +with: +```tsx + +``` + +Remove `Check` and `Copy` from line 17's lucide-react import (keep `AlertCircle`, `Trash2`, `Wand2`): +```tsx +import { AlertCircle, Trash2, Wand2 } from 'lucide-react'; +``` + +- [ ] **Step 5: Typecheck and lint** + +Run: `cd apps/web && npx tsc --noEmit && npm run lint` + +- [ ] **Step 6: Manual verification** + +Visit `/app/sql-formatter`: header visible on desktop, mobile Tabs switch input/formatted panes, Copy button still works, dialect/keyword-case/indent Selects and Format/Clear buttons unaffected. + +- [ ] **Step 7: Commit** + +```bash +git add apps/web/src/components/sql-formatter/sql-formatter-layout.tsx +git commit -m "refactor: apply shared tool-page components to SQL formatter" +``` + +--- + +## Task 7: Apply to GraphQL Formatter + +**Files:** +- Modify: `apps/web/src/components/graphql-formatter/graphql-formatter-layout.tsx` + +- [ ] **Step 1: Update imports** + +Add after line 11 (`import { useIsMobile } from '@/components/hooks/use-mobile';`): +```tsx +import { IconBrandGraphql } from '@tabler/icons-react'; +import { ToolPageHeader } from '@/components/tools/tool-page-header'; +import { ToolMobileTabs } from '@/components/tools/tool-mobile-tabs'; +import { CopyIconButton } from '@/components/tools/copy-icon-button'; +import { RevealItem } from '@/components/dashboard/dashboard-reveal'; +``` + +- [ ] **Step 2: Replace the header block** + +Replace lines 124-129: +```tsx + return ( +
+
+

{t('title')}

+

{t('subtitle')}

+
+``` +with: +```tsx + return ( +
+
+ + + +``` + +- [ ] **Step 3: Replace the mobile tab switcher** + +Replace lines 246-271: +```tsx + {isMobile && ( +
+ + +
+ )} +``` +with: +```tsx + {isMobile && ( + + )} +``` + +- [ ] **Step 4: Replace the inline copy button** + +Replace lines 300-310: +```tsx + +``` +with: +```tsx + +``` + +Remove `Check` and `Copy` from line 5's lucide-react import (keep `AlertCircle`, `Trash2`, `Wand2`): +```tsx +import { AlertCircle, Trash2, Wand2 } from 'lucide-react'; +``` + +- [ ] **Step 5: Typecheck and lint** + +Run: `cd apps/web && npx tsc --noEmit && npm run lint` + +- [ ] **Step 6: Manual verification** + +Visit `/app/graphql-formatter`: header visible on desktop, mobile Tabs switch input/output, Copy button works, the Format/Build sub-tabs (`panelTab` state, unrelated to `mobileTab`) still work. + +- [ ] **Step 7: Commit** + +```bash +git add apps/web/src/components/graphql-formatter/graphql-formatter-layout.tsx +git commit -m "refactor: apply shared tool-page components to GraphQL formatter" +``` + +--- + +## Task 8: Apply to JSON Schema Generator + +**Files:** +- Modify: `apps/web/src/components/json-schema-generator/json-schema-generator-layout.tsx` + +- [ ] **Step 1: Update imports** + +Replace line 7: +```tsx +import { AlertCircle, Check, Copy, Download, FileJson, Trash2 } from 'lucide-react'; +``` +with (drop `FileJson`, the only lucide icon that was used for the old header chip; keep the rest): +```tsx +import { AlertCircle, Check, Copy, Download, Trash2 } from 'lucide-react'; +import { IconBraces } from '@tabler/icons-react'; +import { ToolPageHeader } from '@/components/tools/tool-page-header'; +import { ToolMobileTabs } from '@/components/tools/tool-mobile-tabs'; +import { RevealItem } from '@/components/dashboard/dashboard-reveal'; +``` + +- [ ] **Step 2: Replace the header block** + +Replace lines 109-121: +```tsx + return ( +
+
+
+
+ +
+
+

{t('title')}

+

{t('subtitle')}

+
+
+
+``` +with: +```tsx + return ( +
+
+ + + +``` + +- [ ] **Step 3: Replace the mobile tab switcher** + +Replace lines 169-186: +```tsx + {isMobile && ( +
+ {(['input', 'output'] as const).map((tab) => ( + + ))} +
+ )} +``` +with: +```tsx + {isMobile && ( + + )} +``` + +Note: the Clear/Copy/Download button row (lines 144-166) is left unchanged — it's a 3-button labeled row, not the lone-icon-in-panel-header shape `CopyIconButton` targets (see Task 3's note). + +- [ ] **Step 4: Typecheck and lint** + +Run: `cd apps/web && npx tsc --noEmit && npm run lint` + +- [ ] **Step 5: Manual verification** + +Visit `/app/json-schema-generator`: header visible on desktop with emerald `IconBraces` chip (replacing the old primary-colored `FileJson` chip that only showed on mobile), mobile Tabs switch input/output, Clear/Copy/Download row unaffected. + +- [ ] **Step 6: Commit** + +```bash +git add apps/web/src/components/json-schema-generator/json-schema-generator-layout.tsx +git commit -m "refactor: apply shared ToolPageHeader/ToolMobileTabs to JSON Schema Generator" +``` + +--- + +## Task 9: Apply to Markdown Preview + +**Files:** +- Modify: `apps/web/src/components/markdown-preview-html/markdown-preview-html-layout.tsx` + +This tool has no mobile input/output switcher (`ToolMobileTabs` doesn't apply) — only the header changes. + +- [ ] **Step 1: Update imports** + +Add after line 13 (`import { Copy, Check, Download, Eye, Code } from 'lucide-react';`): +```tsx +import { IconMarkdown } from '@tabler/icons-react'; +import { ToolPageHeader } from '@/components/tools/tool-page-header'; +import { RevealItem } from '@/components/dashboard/dashboard-reveal'; +``` + +- [ ] **Step 2: Replace the header block** + +Replace lines 106-111: +```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/markdown-preview-html`: header visible on desktop; both the "Markdown → HTML" and "HTML → Markdown" tabs (unrelated `Tabs` component, unchanged) still work; Copy/Export buttons unaffected. + +- [ ] **Step 5: Commit** + +```bash +git add apps/web/src/components/markdown-preview-html/markdown-preview-html-layout.tsx +git commit -m "refactor: apply shared ToolPageHeader to Markdown Preview" +``` + +--- + +## Task 10: Apply to Diff Checker (+ spacing fix) + +**Files:** +- Modify: `apps/web/src/components/diff-checker/diff-checker-layout.tsx` + +- [ ] **Step 1: Update imports** + +Add after line 16 (`import { useIsMobile } from '@/components/hooks/use-mobile';`): +```tsx +import { IconGitCompare } from '@tabler/icons-react'; +import { ToolPageHeader } from '@/components/tools/tool-page-header'; +import { ToolMobileTabs } from '@/components/tools/tool-mobile-tabs'; +import { RevealItem } from '@/components/dashboard/dashboard-reveal'; +``` + +- [ ] **Step 2: Replace the header block and fix the root gap** + +Replace lines 152-157: +```tsx + return ( +
+
+

{t('title')}

+

{t('subtitle')}

+
+``` +with (note `gap-3` → `gap-4`, matching the other 8 tools): +```tsx + return ( +
+
+ + + +``` + +- [ ] **Step 3: Replace the 3-way mobile tab switcher** + +Replace lines 180-194: +```tsx +
+ {(['original', 'modified', 'diff'] as const).map((tab) => ( + + ))} +
+``` +with: +```tsx + +``` + +- [ ] **Step 4: Typecheck and lint** + +Run: `cd apps/web && npx tsc --noEmit && npm run lint` + +- [ ] **Step 5: Manual verification** + +Visit `/app/diff-checker`: header visible on desktop; on mobile, the Original/Modified/Diff Tabs switch correctly; desktop stacked-inputs-plus-diff-below layout is unaffected; Clear button and diff stats badge unaffected. + +- [ ] **Step 6: Commit** + +```bash +git add apps/web/src/components/diff-checker/diff-checker-layout.tsx +git commit -m "refactor: apply shared tool-page components to Diff Checker" +``` + +--- + +## Task 11: Apply to Regex Tester + +**Files:** +- Modify: `apps/web/src/components/regex-tester/regex-tester-layout.tsx` + +This tool has no mobile input/output switcher (single column at all breakpoints) — only the header changes. + +- [ ] **Step 1: Update imports** + +Add after line 22 (`import { AlertCircle, ChevronLeft, ChevronRight, Trash2 } from 'lucide-react';`): +```tsx +import { IconRegex } from '@tabler/icons-react'; +import { ToolPageHeader } from '@/components/tools/tool-page-header'; +import { RevealItem } from '@/components/dashboard/dashboard-reveal'; +``` + +- [ ] **Step 2: Replace the header block** + +Replace lines 125-130: +```tsx +
+
+

{t('title')}

+

{t('subtitle')}

+
+``` +with: +```tsx +
+
+ + + +``` + +- [ ] **Step 3: Typecheck and lint** + +Run: `cd apps/web && npx tsc --noEmit && npm run lint` + +- [ ] **Step 4: Manual verification** + +Visit `/app/regex-tester`: header visible on desktop; pattern input, flag checkboxes, match navigation, and highlighted test-text area all unaffected. + +- [ ] **Step 5: Commit** + +```bash +git add apps/web/src/components/regex-tester/regex-tester-layout.tsx +git commit -m "refactor: apply shared ToolPageHeader to Regex Tester" +``` + +--- + +## Task 12: Apply to Format Converter + +**Files:** +- Modify: `apps/web/src/components/format-converter/format-converter-layout.tsx` + +- [ ] **Step 1: Update imports** + +Add after line 9 (`import { ArrowLeftRight, Copy, Check, AlertCircle } from 'lucide-react';`): +```tsx +import { IconFileCode } from '@tabler/icons-react'; +import { ToolPageHeader } from '@/components/tools/tool-page-header'; +import { ToolMobileTabs } from '@/components/tools/tool-mobile-tabs'; +import { RevealItem } from '@/components/dashboard/dashboard-reveal'; +``` + +- [ ] **Step 2: Replace the header block** + +Replace lines 97-102: +```tsx +
+
+

{t('title')}

+

{t('subtitle')}

+
+``` +with: +```tsx +
+
+ + + +``` + +- [ ] **Step 3: Replace the mobile tab switcher** + +Replace lines 104-121: +```tsx + {isMobile && ( +
+ {(['input', 'output'] as const).map((tab) => ( + + ))} +
+ )} +``` +with: +```tsx + {isMobile && ( + + )} +``` + +- [ ] **Step 4: Typecheck and lint** + +Run: `cd apps/web && npx tsc --noEmit && npm run lint` + +- [ ] **Step 5: Manual verification** + +Visit `/app/format-converter`: header visible on desktop; mobile Tabs switch input/output (still auto-advances to output tab on typing, per the existing `setMobileTab('output')` call in the `onChange` handler); Swap/Copy buttons and from/to Selects unaffected. + +- [ ] **Step 6: Commit** + +```bash +git add apps/web/src/components/format-converter/format-converter-layout.tsx +git commit -m "refactor: apply shared tool-page components to Format Converter" +``` + +--- + +## Task 13: 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 (catches any cross-file regression missed in a single-tool step). + +- [ ] **Step 2: Visual sweep across all 9 tools** + +Run: `cd apps/web && npm run dev`. For each of the 9 routes (`/app/json-formatter`, `/app/json-schema-generator`, `/app/sql-formatter`, `/app/graphql-formatter`, `/app/yaml-formatter`, `/app/markdown-preview-html`, `/app/format-converter`, `/app/diff-checker`, `/app/regex-tester`), confirm: +- Desktop page header renders with the emerald Formatters 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 `ToolMobileTabs` switcher (json-formatter, yaml, sql, graphql, json-schema-generator, diff-checker, format-converter) switches panes correctly; markdown-preview-html and regex-tester (no switcher) still lay out correctly in a single column. +- 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 features** + +Confirm JSON formatter's save/load modal (requires being logged in, or confirm the "login required" toast fires when logged out) still opens, lists, and loads documents correctly — this is the one tool with backend-persisted state and the highest risk of an accidental regression from Task 4's edit. + +- [ ] **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 formatters dashboard-alignment QA pass" +``` diff --git a/docs/superpowers/specs/2026-07-03-formatters-dashboard-alignment-design.md b/docs/superpowers/specs/2026-07-03-formatters-dashboard-alignment-design.md new file mode 100644 index 00000000..44c06723 --- /dev/null +++ b/docs/superpowers/specs/2026-07-03-formatters-dashboard-alignment-design.md @@ -0,0 +1,71 @@ +# Formatters UI/UX Alignment with Dashboard Theme + +**Date:** 2026-07-03 +**Status:** Approved (design phase) + +## Problem + +The "Formatters" tool category (`json-formatter`, `json-schema-generator`, `sql-formatter`, `graphql-formatter`, `yaml-formatter`, `markdown-preview-html`, `format-converter`, `diff-checker`, `regex-tester` — as defined in `apps/web/src/lib/tool-categories.ts`) shares the dashboard's outer shell (`ClientLayout`: sidebar + nav) but the page *content* looks like a bare utility screen: no desktop page title, no icon branding, no color accents, no motion, no ambient background. The dashboard (`apps/web/src/app/dashboard/`) has a fully developed visual system: gradient hero text, icon-chip section headers, hover-sheen gradient cards, staggered `framer-motion` reveals, and an ambient radial-gradient background. + +This creates a visible quality gap when navigating from the dashboard into any formatter tool. + +## Scope + +**In scope:** the 9 tools in the "Formatters" category exactly as defined in `tool-categories.ts`. Converters (`base64`, `unit-converter`, etc.) are a separate category and are explicitly **out of scope**. + +**Functional scope:** consistency only. No new features. Specifically: +- Normalize the action button set/order (Format, Clear, Copy, Download; Save only where backend persistence already exists — i.e. JSON formatter today, not invented elsewhere). +- Normalize error-banner markup/styling across tools. +- No history/undo, no save/load expansion to other tools — explicitly deferred, not part of this plan. + +**Visual scope:** full parity with dashboard's visual system (chosen over lighter alternatives): icon-chip page header, gradient/accent card treatment, ambient background, `framer-motion` reveal-on-mount, hover effects — with one carve-out (see below) for code-editor panels. + +## Design + +### 1. Shared component layer (build once, reuse across all 9) + +Rather than editing each tool's layout file independently (guarantees drift on the next redesign), extract shared pieces — mirroring how the dashboard itself already reuses `DashboardSectionHeader`, `ToolCard`, `RevealItem` across sections instead of inlining per-section markup. + +- **`ToolPageHeader`** (new, e.g. `apps/web/src/components/tools/tool-page-header.tsx`): icon chip + title + short description, styled like a compact dashboard hero. Reuses each tool's existing icon from the tools registry (`apps/web/src/lib/tools-registry.ts`) — no new icon choices. This is shown on **desktop**, closing the current gap where only a mobile-only `

` exists and desktop has no page identity at all. +- **Promoted CSS utilities**: `dash-ambient`, `dashboard-grid-bg`, and the `dash-card-sheen` hover effect currently live in `globals.css` scoped conceptually to the dashboard. Generalize/rename so they're usable on any tool page shell (mechanical CSS move, same visual output). +- **`RevealItem`** (`apps/web/src/components/dashboard/dashboard-reveal.tsx`): reuse as-is or relocate to a shared location if that file is dashboard-specific in name only. Respects `prefers-reduced-motion` already — no new work needed there. +- **`ToolActionBar`** (new): normalizes the Format/Clear/Copy/Download/Save button set, order, and icon styling. Today each tool hand-rolls its own subset and order. +- **Mobile input/output switcher**: replace the hand-rolled pill toggle (`bg-muted/40 p-1` two-button div) with shadcn `Tabs`, matching how the dashboard already uses `Tabs` for its Apps/Analytics switch. One fewer bespoke pattern in the codebase. + +### 2. Card treatment + +Toolbar `Card` and I/O panel `Card`s adopt dashboard's `bg-gradient-to-br from-card to-card/80`, `ring-1 ring-inset ring-border/50` icon chips, and hover border/shadow glow. + +**Carve-out:** the cursor-tracked `dash-card-sheen` effect applies to toolbar/header cards only — **not** the code-editor input/output panels themselves, since that effect is distracting on a surface the user is actively typing/reading in. + +### 3. Spacing normalization + +Replace formatter's tighter `gap-4` / `p-4` / `px-4 py-2.5` rhythm with dashboard's `gap-3 md:gap-4 xl:gap-5` (grids) and `p-3 md:p-3.5` (card padding). + +### 4. Color accents + +Each tool gets the accent color already assigned to it in the existing registry/category data — no new color assignments invented for this project. + +### 5. Motion + +`RevealItem` stagger-fade on mount: header → toolbar → panels, in that order, matching the dashboard's section stagger pattern. + +## Rollout order + +1. Build the shared pieces (`ToolPageHeader`, promoted CSS utilities, `ToolActionBar`) against **JSON formatter** as the pilot — it's the hardest case (resizable dual-pane via `react-resizable-panels`, existing save/load modal), so proving the pattern here first surfaces integration problems early. +2. Batch-apply to `yaml-formatter`, `sql-formatter`, `graphql-formatter` — these three are near-identical structurally today, so once the pattern lands on one it should apply cheaply to the rest. +3. Apply to `json-schema-generator`, `markdown-preview-html`, `diff-checker`, `regex-tester`. +4. Apply to `format-converter` last — structurally furthest from the code-editor-pair pattern the others share. + +## Testing + +- Light/dark mode, and the accent-color theme variants (`.blue`/`.purple`/`.green` etc. on ``). +- Mobile breakpoints (the new `ToolPageHeader` and `Tabs`-based switcher in particular). +- `prefers-reduced-motion` — reveal animation must no-op correctly. +- Regression check: JSON formatter's save/load modal must continue working unmodified after the pilot refactor. + +## Explicitly out of scope + +- Converters category (separate design if ever pursued). +- New functionality: save/load expansion beyond JSON formatter, history/undo, recents. +- New icon or accent-color assignments beyond what's already registered per tool.