diff --git a/apps/desktop-ui/AGENTS.md b/apps/desktop-ui/AGENTS.md
new file mode 100644
index 00000000..643577df
--- /dev/null
+++ b/apps/desktop-ui/AGENTS.md
@@ -0,0 +1,9 @@
+
+
+# This is NOT the Next.js you know
+
+This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices.
+
+This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean.
+
+
diff --git a/apps/desktop-ui/CLAUDE.md b/apps/desktop-ui/CLAUDE.md
new file mode 100644
index 00000000..43c994c2
--- /dev/null
+++ b/apps/desktop-ui/CLAUDE.md
@@ -0,0 +1 @@
+@AGENTS.md
diff --git a/apps/desktop-ui/src/components/curl-to-code/curl-to-code-layout.tsx b/apps/desktop-ui/src/components/curl-to-code/curl-to-code-layout.tsx
index 3f366c6b..c2a95422 100644
--- a/apps/desktop-ui/src/components/curl-to-code/curl-to-code-layout.tsx
+++ b/apps/desktop-ui/src/components/curl-to-code/curl-to-code-layout.tsx
@@ -4,8 +4,6 @@ import React, { useMemo, useState } from 'react'
import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard'
import { useTranslations } from 'next-intl'
import { Button } from '@/components/ui/button'
-import { Card } from '@/components/ui/card'
-import { Label } from '@/components/ui/label'
import {
Select,
SelectContent,
@@ -13,11 +11,10 @@ import {
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
-import { Check, Copy, FileText, Trash2 } from 'lucide-react'
+import { Check, Copy, Trash2 } from 'lucide-react'
import { IconTerminal2 } 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'
+import { ToolShell } from '@/components/tools/tool-shell'
+import { ToolPanels, IOPanel, ToolTextArea } from '@/components/tools/io-panel'
import { CURL_TARGETS, curlToCode, type CurlTarget } from '@/lib/curl-to-code'
const SAMPLE = `curl -X POST https://api.example.com/v1/users \\
@@ -33,83 +30,95 @@ export function CurlToCodeLayout() {
const result = useMemo(() => curlToCode(input, target), [input, target])
- return (
-
-
-
-
-
-
+ const toolbar = (
+
+ setTarget(v as CurlTarget)}>
+
+
+
+
+ {CURL_TARGETS.map((tg) => (
+
+ {tg.label}
+
+ ))}
+
+
+
+ )
-
-
-
-
-
- {t('panels.input')}
-
-
- setInput(SAMPLE)}>
+ return (
+
+
+
+ setInput(SAMPLE)}
+ >
{t('sample')}
- setInput('')} disabled={!input} title={t('clear')}>
+ setInput('')}
+ disabled={!input}
+ title={t('clear')}
+ >
-
-
-
-
-
+ >
+ }
+ >
+
setInput(e.target.value)}
+ placeholder={t('placeholder')}
+ autoComplete="off"
+ />
+
-
-
- setTarget(v as CurlTarget)}>
-
-
-
-
- {CURL_TARGETS.map((tg) => (
-
- {tg.label}
-
- ))}
-
-
+ void copyToClipboard(result.code, { silent: true })}
>
- {copied ? : }
+ {copied ? (
+
+ ) : (
+
+ )}
{copied ? t('copied') : t('copy')}
-
-
- {result.error ? (
-
{result.error}
- ) : result.code ? (
-
{result.code}
- ) : (
-
{t('outputPlaceholder')}
- )}
-
-
-
-
+ }
+ >
+ {result.error ? (
+ {result.error}
+ ) : result.code ? (
+
+ {result.code}
+
+ ) : (
+ {t('outputPlaceholder')}
+ )}
+
+
+
)
}
diff --git a/apps/desktop-ui/src/components/http-status-codes/http-status-codes-layout.tsx b/apps/desktop-ui/src/components/http-status-codes/http-status-codes-layout.tsx
index fea26eb6..ac1e0ec7 100644
--- a/apps/desktop-ui/src/components/http-status-codes/http-status-codes-layout.tsx
+++ b/apps/desktop-ui/src/components/http-status-codes/http-status-codes-layout.tsx
@@ -2,7 +2,6 @@
import { useMemo, useState } from 'react'
import { useTranslations } from 'next-intl'
-import { Card, CardContent } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Input } from '@/components/ui/input'
import {
@@ -21,10 +20,8 @@ import {
TableRow,
} from '@/components/ui/table'
import { CopyButton } from '@/components/tools/copy-button'
-import { ToolPageHeader } from '@/components/tools/tool-page-header'
-import { ToolWrapper } from '@/components/tools/tool-wrapper'
-import { CATEGORY_ACCENT } from '@/components/dashboard/types'
import { IconListNumbers } from '@tabler/icons-react'
+import { ToolShell } from '@/components/tools/tool-shell'
import { HTTP_STATUS_CODES, type HttpStatusClass } from '@/lib/http-status-codes'
type ClassFilter = 'all' | HttpStatusClass
@@ -74,112 +71,106 @@ export function HttpStatusCodesLayout() {
})
}, [classFilter, query])
- return (
-
-
-
-
-
-
-
-
- setQuery(e.target.value)}
- placeholder={t('searchPlaceholder')}
- className="h-10"
- autoFocus
- />
-
-
- setClassFilter(v as ClassFilter)}
- >
-
-
-
-
- {t('classFilterAll')}
- {(['1xx', '2xx', '3xx', '4xx', '5xx'] as const).map((c) => (
-
- {classLabel(c)}
-
- ))}
-
-
-
-
- {t('resultsCount', { count: filtered.length, total: HTTP_STATUS_CODES.length })}
-
-
-
-
+ const toolbar = (
+
+
+
+ setQuery(e.target.value)}
+ placeholder={t('searchPlaceholder')}
+ className="h-10"
+ autoFocus
+ />
+
+
+ setClassFilter(v as ClassFilter)}
+ >
+
+
+
+
+ {t('classFilterAll')}
+ {(['1xx', '2xx', '3xx', '4xx', '5xx'] as const).map((c) => (
+
+ {classLabel(c)}
+
+ ))}
+
+
+
+
+ {t('resultsCount', { count: filtered.length, total: HTTP_STATUS_CODES.length })}
+
+
+
+ )
-
-
-
-
+ return (
+
+
+
+
+
+
+ {t('table.code')}
+ {t('table.phrase')}
+ {t('table.description')}
+ {t('table.copy')}
+
+
+
+ {filtered.length === 0 ? (
- {t('table.code')}
- {t('table.phrase')}
- {t('table.description')}
- {t('table.copy')}
+
+ {t('noResults', { query })}
+
-
-
- {filtered.length === 0 ? (
-
-
- {t('noResults', { query })}
+ ) : (
+ filtered.map((s) => (
+
+
+
+ {s.code}
+
+ {s.class}
+
+
+
+ {s.phrase}
+
+ {s.description}
+ {s.spec ? (
+
+ {t('specPrefix')} {s.spec}
+
+ ) : null}
+
+
+
- ) : (
- filtered.map((s) => (
-
-
-
- {s.code}
-
- {s.class}
-
-
-
- {s.phrase}
-
- {s.description}
- {s.spec ? (
-
- {t('specPrefix')} {s.spec}
-
- ) : null}
-
-
-
-
-
- ))
- )}
-
-
-
-
+ ))
+ )}
+
+
+
-
+
)
}
-
diff --git a/apps/desktop-ui/src/components/ip-subnet-calculator/ip-subnet-calculator-layout.tsx b/apps/desktop-ui/src/components/ip-subnet-calculator/ip-subnet-calculator-layout.tsx
index 73756c93..7c33e307 100644
--- a/apps/desktop-ui/src/components/ip-subnet-calculator/ip-subnet-calculator-layout.tsx
+++ b/apps/desktop-ui/src/components/ip-subnet-calculator/ip-subnet-calculator-layout.tsx
@@ -4,7 +4,6 @@ import { useMemo, useState } from 'react';
import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard';
import { useTranslations } from 'next-intl';
import { useDebouncedCallback } from 'use-debounce';
-import { Card } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
@@ -13,9 +12,7 @@ import { Copy, Check } from 'lucide-react';
import { computeIpv4Subnet, computeIpv6Subnet } from '@/lib/ip-subnet';
import { cn } from '@/lib/utils';
import { IconHierarchy2 } 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';
+import { ToolShell } from '@/components/tools/tool-shell';
function Row({
label,
@@ -78,16 +75,11 @@ export function IpSubnetCalculatorLayout() {
const ipv6Result = useMemo(() => computeIpv6Subnet(ipv6Debounced), [ipv6Debounced]);
return (
-
-
-
-
-
+
setFamily(v as 'ipv4' | 'ipv6')} className="flex min-h-0 flex-1 flex-col gap-4">
{t('tabIpv4')}
@@ -95,7 +87,7 @@ export function IpSubnetCalculatorLayout() {
-
+
-
+
{!ipv4Result.ok ? (
{t(`errors.${ipv4Result.errorKey}`)}
) : (
@@ -147,11 +139,11 @@ export function IpSubnetCalculatorLayout() {
)}
>
)}
-
+
-
+
-
+
{!ipv6Result.ok ? (
{t(`errors.${ipv6Result.errorKey}`)}
) : (
@@ -182,9 +174,9 @@ export function IpSubnetCalculatorLayout() {
>
)}
-
+
-
+
);
}
diff --git a/apps/desktop-ui/src/components/mime-type-lookup/mime-type-lookup-layout.tsx b/apps/desktop-ui/src/components/mime-type-lookup/mime-type-lookup-layout.tsx
index 65b561b1..ae631509 100644
--- a/apps/desktop-ui/src/components/mime-type-lookup/mime-type-lookup-layout.tsx
+++ b/apps/desktop-ui/src/components/mime-type-lookup/mime-type-lookup-layout.tsx
@@ -2,7 +2,6 @@
import { useMemo, useState } from 'react'
import { useTranslations } from 'next-intl'
-import { Card } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Button } from '@/components/ui/button'
@@ -10,9 +9,7 @@ import { Check, Copy } from 'lucide-react'
import { cn } from '@/lib/utils'
import { lookupMimeType } from '@/lib/mime-type-lookup'
import { IconFileInfo } 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'
+import { ToolShell } from '@/components/tools/tool-shell'
async function copyToClipboard(text: string) {
await navigator.clipboard.writeText(text)
@@ -38,143 +35,133 @@ export function MimeTypeLookupLayout() {
const canShow = Boolean(value.trim())
const hasMatch = Boolean(result.mimeType)
- return (
-
-
-
+
+
{t('inputLabel')}
+
setValue(e.target.value)}
+ placeholder={t('placeholder')}
+ spellCheck={false}
+ autoComplete="off"
+ inputMode="text"
/>
-
+
{t('hint')}
+
-
-
-
-
{t('inputLabel')}
-
setValue(e.target.value)}
- placeholder={t('placeholder')}
- spellCheck={false}
- autoComplete="off"
- inputMode="text"
- />
-
{t('hint')}
-
+
+ setValue('')}>
+ {t('clear')}
+
+ setValue('package.json')}>
+ {t('exampleFilename')}
+
+ setValue('.png')}>
+ {t('exampleExtension')}
+
+
+
+ )
-
-
setValue('')}>
- {t('clear')}
-
+ return (
+
+
+
+
+ {t('resultLabel')}
+
+ {hasMatch && (
setValue('package.json')}
+ variant="secondary"
+ onClick={async () => {
+ if (!result.mimeType) return
+ try {
+ await copyToClipboard(result.mimeType)
+ setCopiedKey('mimeType')
+ setTimeout(() => setCopiedKey(null), 1500)
+ } catch {
+ /* ignore */
+ }
+ }}
>
- {t('exampleFilename')}
-
- setValue('.png')}>
- {t('exampleExtension')}
+ {copiedKey === 'mimeType' ? (
+
+ ) : (
+
+ )}
+ {copiedKey === 'mimeType' ? t('copied') : t('copyMimeType')}
-
-
-
-
-
-
- {t('resultLabel')}
-
- {hasMatch && (
- {
- if (!result.mimeType) return
- try {
- await copyToClipboard(result.mimeType)
- setCopiedKey('mimeType')
- setTimeout(() => setCopiedKey(null), 1500)
- } catch {
- /* ignore */
- }
- }}
- >
- {copiedKey === 'mimeType' ? (
-
- ) : (
-
- )}
- {copiedKey === 'mimeType' ? t('copied') : t('copyMimeType')}
-
- )}
-
-
-
- {!canShow ? (
-
{t('emptyHint')}
- ) : !hasMatch ? (
-
{t('notFound')}
- ) : (
-
-
- {t('normalizedLabel')}{' '}
- {result.normalized || '—'}
-
-
-
- {items.map((it) => (
-
-
-
{it.label}
-
{it.value ?? '—'}
-
+ )}
+
+
+
+ {!canShow ? (
+
{t('emptyHint')}
+ ) : !hasMatch ? (
+
{t('notFound')}
+ ) : (
+
+
+ {t('normalizedLabel')}{' '}
+ {result.normalized || '—'}
+
- {it.value && (
-
{
- try {
- await copyToClipboard(it.value!)
- setCopiedKey(it.key)
- setTimeout(() => setCopiedKey(null), 1500)
- } catch {
- /* ignore */
- }
- }}
- title={t('copy')}
- aria-label={t('copy')}
- >
- {copiedKey === it.key ? (
-
- ) : (
-
- )}
-
- )}
+
+ {items.map((it) => (
+
+
+
{it.label}
+
{it.value ?? '—'}
- ))}
-
+
+ {it.value && (
+
{
+ try {
+ await copyToClipboard(it.value!)
+ setCopiedKey(it.key)
+ setTimeout(() => setCopiedKey(null), 1500)
+ } catch {
+ /* ignore */
+ }
+ }}
+ title={t('copy')}
+ aria-label={t('copy')}
+ >
+ {copiedKey === it.key ? (
+
+ ) : (
+
+ )}
+
+ )}
+
+ ))}
- )}
-
+
+ )}
+
-
{t('localNote')}
-
+
{t('localNote')}
-
+
)
}
-
diff --git a/apps/desktop-ui/src/components/user-agent-parser/user-agent-parser-layout.tsx b/apps/desktop-ui/src/components/user-agent-parser/user-agent-parser-layout.tsx
index aa427d89..daf61668 100644
--- a/apps/desktop-ui/src/components/user-agent-parser/user-agent-parser-layout.tsx
+++ b/apps/desktop-ui/src/components/user-agent-parser/user-agent-parser-layout.tsx
@@ -2,17 +2,13 @@
import { useMemo, useState } from 'react'
import { useTranslations } from 'next-intl'
-import { Card } from '@/components/ui/card'
-import { Label } from '@/components/ui/label'
-import { Textarea } from '@/components/ui/textarea'
import { Button } from '@/components/ui/button'
import { Check, Copy } from 'lucide-react'
import { cn } from '@/lib/utils'
import { parseUserAgent } from '@/lib/user-agent-parser'
import { IconUserSearch } 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'
+import { ToolShell } from '@/components/tools/tool-shell'
+import { ToolPanels, IOPanel, ToolTextArea } from '@/components/tools/io-panel'
async function copyToClipboard(text: string) {
await navigator.clipboard.writeText(text)
@@ -50,73 +46,73 @@ export function UserAgentParserLayout() {
const json = useMemo(() => (result ? JSON.stringify(result, null, 2) : ''), [result])
return (
-
-
-
-
-
-
-
-
-
- {t('localNote')}
-
-
-
+
+ {t('localNote')}
+
+
+
+
)
}
diff --git a/apps/desktop-ui/src/components/websocket-tester/websocket-tester-layout.tsx b/apps/desktop-ui/src/components/websocket-tester/websocket-tester-layout.tsx
index b399cffa..257a47c3 100644
--- a/apps/desktop-ui/src/components/websocket-tester/websocket-tester-layout.tsx
+++ b/apps/desktop-ui/src/components/websocket-tester/websocket-tester-layout.tsx
@@ -5,13 +5,10 @@ import { useTranslations } from 'next-intl'
import { saveAs } from 'file-saver'
import { toast } from 'sonner'
import { Button } from '@/components/ui/button'
-import { Card } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Badge } from '@/components/ui/badge'
import { Switch } from '@/components/ui/switch'
-import { ScrollArea } from '@/components/ui/scroll-area'
-import { Textarea } from '@/components/ui/textarea'
import {
ArrowDown,
ArrowUp,
@@ -28,9 +25,8 @@ import {
import { IconPlugConnected } from '@tabler/icons-react'
import { cn } from '@/lib/utils'
import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard'
-import { ToolPageHeader } from '@/components/tools/tool-page-header'
-import { RevealItem } from '@/components/dashboard/dashboard-reveal'
-import { CATEGORY_ACCENT } from '@/components/dashboard/types'
+import { ToolShell } from '@/components/tools/tool-shell'
+import { IOPanel, ToolTextArea } from '@/components/tools/io-panel'
import {
describeCloseEvent,
formatLogEntry,
@@ -252,23 +248,9 @@ export function WebsocketTesterLayout() {
const isConnected = status === 'connected'
const isConnecting = status === 'connecting'
- return (
-
-
-
-
-
-
-
- {/* Connection bar */}
-
-
+ const toolbar = (
+
+
{t('urlLabel')}
@@ -323,20 +305,28 @@ export function WebsocketTesterLayout() {
/>
{t(`status.${status}`)}
-
+
+ )
+ return (
+
{/* Message log */}
-
-
-
-
- {t('log.title')}
-
+
{entries.length}
-
-
-
-
-
-
+ >
+ }
+ >
+
{entries.length === 0 && (
{t('log.empty')}
)}
@@ -412,12 +402,11 @@ export function WebsocketTesterLayout() {
})}
-
-
+
{/* Composer */}
-
-
-
-
+
+
)
}
diff --git a/apps/desktop-ui/src/components/whois-lookup/whois-lookup-layout.tsx b/apps/desktop-ui/src/components/whois-lookup/whois-lookup-layout.tsx
index baa9abc4..b5e2ec7e 100644
--- a/apps/desktop-ui/src/components/whois-lookup/whois-lookup-layout.tsx
+++ b/apps/desktop-ui/src/components/whois-lookup/whois-lookup-layout.tsx
@@ -3,7 +3,6 @@
import React, { useCallback, useState } from 'react'
import { useTranslations } from 'next-intl'
import { motion, AnimatePresence } from 'framer-motion'
-import { Card } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Badge } from '@/components/ui/badge'
@@ -23,11 +22,8 @@ import {
Users,
} from 'lucide-react'
import { IconWorldSearch } from '@tabler/icons-react'
-import { cn } from '@/lib/utils'
import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard'
-import { ToolPageHeader } from '@/components/tools/tool-page-header'
-import { RevealItem } from '@/components/dashboard/dashboard-reveal'
-import { CATEGORY_ACCENT } from '@/components/dashboard/types'
+import { ToolShell } from '@/components/tools/tool-shell'
import { formatIsoDate, normalizeTarget, relativeTime } from '@/lib/whois-lookup'
import { rdapLookup, type WhoisResult } from '@/lib/rdap'
@@ -37,14 +33,14 @@ function DateCard({ icon: Icon, label, iso }: { icon: React.ElementType; label:
const date = iso ? formatIsoDate(iso) : null
const rel = iso ? relativeTime(iso) : null
return (
-
+
{label}
{date ?? '—'}
{rel &&
{rel}
}
-
+
)
}
@@ -80,22 +76,9 @@ export function WhoisLookupLayout() {
[input, t],
)
- return (
-
-
-
-
-
-
-
- {/* Search card */}
-
+ const toolbar = (
+
+
@@ -131,8 +114,19 @@ export function WhoisLookupLayout() {
))}
-
+
+
+ )
+ return (
+
{/* Error */}
{error && (
@@ -153,10 +147,10 @@ export function WhoisLookupLayout() {
{[0, 1, 2, 3].map((i) => (
-
+
))}
-
+
)}
@@ -172,12 +166,12 @@ export function WhoisLookupLayout() {
-
+
{result.type === 'ip' ? t('summary.network') : t('summary.registrar')}
{result.registrar ?? t('notAvailable')}
-
+
{/* Dates */}
@@ -204,7 +198,7 @@ export function WhoisLookupLayout() {
{/* Nameservers */}
{result.nameservers.length > 0 && (
-
+
@@ -221,12 +215,12 @@ export function WhoisLookupLayout() {
))}
-
+
)}
{/* Contacts */}
{result.contacts.length > 0 && (
-
+
@@ -263,12 +257,12 @@ export function WhoisLookupLayout() {
-
+
)}
{/* Raw RDAP */}
-
+
@@ -298,7 +292,7 @@ export function WhoisLookupLayout() {
{JSON.stringify(result.raw, null, 2)}
-
+
@@ -315,6 +309,6 @@ export function WhoisLookupLayout() {
)}
-
+
)
}