From 31a34721fca2255d1fefe4f78b73dffd646089ad Mon Sep 17 00:00:00 2001 From: Cu Thanh Cam Date: Sun, 5 Jul 2026 18:53:10 +0700 Subject: [PATCH 1/5] Polish home docs and support surfaces --- src/app/router.tsx | 4 + src/app/styles.css | 48 +++++- src/pages/docs/DocsMarkdown.tsx | 31 ++++ src/pages/docs/DocsPage.tsx | 157 ++++++++++++------ src/pages/docs/docs.content.ts | 177 ++++++++++++++++++++ src/pages/docs/docs.toc.ts | 32 ++++ src/pages/home/HomePage.tsx | 226 +++++++++++++++----------- src/pages/support/SupportPage.tsx | 260 ++++++++++++++++++++++++++++++ src/widgets/sidebar/Sidebar.tsx | 64 ++++++-- 9 files changed, 840 insertions(+), 159 deletions(-) create mode 100644 src/pages/docs/DocsMarkdown.tsx create mode 100644 src/pages/docs/docs.content.ts create mode 100644 src/pages/docs/docs.toc.ts create mode 100644 src/pages/support/SupportPage.tsx diff --git a/src/app/router.tsx b/src/app/router.tsx index 0a5f728..b89d233 100644 --- a/src/app/router.tsx +++ b/src/app/router.tsx @@ -5,6 +5,7 @@ import { DocsPage } from "@/pages/docs/DocsPage"; import { HomePage } from "@/pages/home/HomePage"; import { NotFoundPage } from "@/pages/not-found/NotFoundPage"; import { SettingsPage } from "@/pages/settings/SettingsPage"; +import { SupportPage } from "@/pages/support/SupportPage"; import { ToolPlaceholderPage } from "@/pages/tools/ToolPlaceholderPage"; export function AppRouter(): JSX.Element { @@ -13,7 +14,10 @@ export function AppRouter(): JSX.Element { }> } index /> } path="docs" /> + } path="docs/:docId" /> } path="settings" /> + } path="support" /> + } path="support/:mode" /> } path="tools/:toolId" /> } path="*" /> diff --git a/src/app/styles.css b/src/app/styles.css index 3422909..0a7724c 100644 --- a/src/app/styles.css +++ b/src/app/styles.css @@ -11,6 +11,8 @@ --brand-cyan: #12c2e9; --brand-purple: #c471ed; --brand-coral: #f64f59; + --brand-pink: #e341ff; + --brand-indigo: #6a4bff; } .dark { @@ -64,10 +66,19 @@ body { .bg-gradient-brand { background-image: linear-gradient( - 135deg, - var(--brand-cyan) 0%, - var(--brand-purple) 52%, - var(--brand-coral) 100% + 90deg, + var(--brand-coral) 0%, + var(--brand-pink) 52%, + var(--brand-indigo) 100% + ); +} + +.bg-brand-gradient { + background-image: linear-gradient( + 90deg, + var(--brand-coral) 0%, + var(--brand-pink) 52%, + var(--brand-indigo) 100% ); } @@ -79,10 +90,10 @@ body { .text-gradient-brand { background-image: linear-gradient( - 135deg, - var(--brand-cyan), - var(--brand-purple), - var(--brand-coral) + 90deg, + var(--brand-coral), + var(--brand-pink), + var(--brand-indigo) ); background-clip: text; color: transparent; @@ -303,6 +314,27 @@ body { padding: 0.05rem 0.2rem; } +.docs-markdown h2, +.docs-markdown h3 { + scroll-margin-top: 5rem; +} + +.docs-markdown h1 { + margin-top: 0; + font-size: 2rem; +} + +.docs-markdown h2 { + border-top: 1px solid rgb(226 232 240); + margin-top: 2rem; + padding-top: 1.5rem; +} + +.docs-markdown table { + display: block; + overflow-x: auto; +} + .forge-markdown .katex-display { overflow-x: auto; overflow-y: hidden; diff --git a/src/pages/docs/DocsMarkdown.tsx b/src/pages/docs/DocsMarkdown.tsx new file mode 100644 index 0000000..28f0308 --- /dev/null +++ b/src/pages/docs/DocsMarkdown.tsx @@ -0,0 +1,31 @@ +import type { JSX } from "react"; +import { useMemo } from "react"; +import DOMPurify from "dompurify"; +import { marked } from "marked"; +import { slugifyHeading } from "./docs.toc"; + +interface DocsMarkdownProps { + markdown: string; +} + +export function DocsMarkdown({ markdown }: DocsMarkdownProps): JSX.Element { + const html = useMemo(() => { + return DOMPurify.sanitize(marked.parse(withHeadingIds(markdown), { async: false })); + }, [markdown]); + + return ( +
+ ); +} + +function withHeadingIds(markdown: string): string { + return markdown.replace(/^(#{2,3})\s+(.+)$/gm, (_match, hashes, title: string) => { + const level = String(hashes).length; + const id = slugifyHeading(title); + + return `${title}`; + }); +} diff --git a/src/pages/docs/DocsPage.tsx b/src/pages/docs/DocsPage.tsx index cf0e66f..24f950d 100644 --- a/src/pages/docs/DocsPage.tsx +++ b/src/pages/docs/DocsPage.tsx @@ -1,60 +1,117 @@ import type { JSX } from "react"; -import { BookOpen, FileCode2, GitPullRequest, Map } from "lucide-react"; -import { MainLayout } from "@/layouts/MainLayout"; +import { BookOpen, ChevronRight, Keyboard, Route } from "lucide-react"; +import { Link, NavLink, useParams } from "react-router"; +import { cn } from "@/shared/lib/cn"; +import { DocsMarkdown } from "./DocsMarkdown"; +import { docPages, getDocPage } from "./docs.content"; +import { createToc } from "./docs.toc"; -const docs = [ - { - href: "/docs/architecture.md", - icon: BookOpen, - title: "Architecture", - text: "Layering, dependency rules, registry ownership, and product boundaries.", - }, - { - href: "/docs/adding-a-new-tool.md", - icon: FileCode2, - title: "Adding a new tool", - text: "Feature folder shape, registry metadata, persistence rules, and tests.", - }, - { - href: "/docs/contributing.md", - icon: GitPullRequest, - title: "Contributing", - text: "Local setup, commits, pull request expectations, and review standards.", - }, - { - href: "/docs/roadmap.md", - icon: Map, - title: "Roadmap", - text: "Milestones for shell, editors, data tools, utilities, and stable release.", - }, -]; +const docIcons: Record = { + guides: Route, + overview: BookOpen, + shortcuts: Keyboard, +}; export function DocsPage(): JSX.Element { + const { docId } = useParams(); + const page = getDocPage(docId); + const toc = createToc(page.markdown); + + function scrollToHeading(id: string): void { + document.getElementById(id)?.scrollIntoView({ block: "start", behavior: "smooth" }); + window.history.replaceState(null, "", `#${id}`); + } + return ( - -
- {docs.map((doc) => { - const Icon = doc.icon; +
+
+ + +
+ +
+ +
- +
); } diff --git a/src/pages/docs/docs.content.ts b/src/pages/docs/docs.content.ts new file mode 100644 index 0000000..7cb3402 --- /dev/null +++ b/src/pages/docs/docs.content.ts @@ -0,0 +1,177 @@ +export interface DocPageContent { + description: string; + id: string; + markdown: string; + title: string; +} + +export const docPages: DocPageContent[] = [ + { + description: "How Forge is organized, why it exists, and how to move through it.", + id: "overview", + title: "Product docs", + markdown: `# Product docs + +Forge is a local-first developer workstation for everyday utility work: previewing Markdown and HTML, formatting data, decoding tokens, generating secrets, comparing text, and transforming strings without losing your place. + +The project is shaped around one idea: every tool should feel like part of the same product. The toolbar, panes, copy actions, validation states, and navigation all follow one shared design language. + +## Product principles + +- **Local-first by default.** Sensitive inputs such as JWTs, secrets, passwords, hashes, and text snippets stay in the browser. +- **Fast paths for repeated work.** Common actions such as copy, export, clear, generate, and format are placed near the thing they affect. +- **Dense but calm interfaces.** Forge is a workstation, not a marketing page. Tool surfaces prioritize scanning, comparison, and repeated use. +- **Consistent affordances.** Segmented controls, sliders, toggles, pane headers, and result cards behave the same across tools. + +## Tool families + +### Editors + +Markdown Preview, HTML Preview, and Diff Checker provide full-height workspaces with live review loops. They set the interaction model for the rest of Forge: input on one side, result on the other, with compact controls above. + +### Data + +JSON Formatter, JSON YAML Converter, and JWT Decoder focus on validation, structure, and readable output. Errors are shown inline, not hidden behind dialogs. + +### Encoding + +Base64 and URL Encoder keep encode/decode flows close together and make copy actions visible because these tools are usually used in quick loops. + +### Crypto + +JWT Secret Generator, Hash Generator, and Password Generator run locally and make generated values easy to inspect, hide, copy, or export. + +### Utilities + +UUID Generator, Timestamp Converter, Case Converter, Slugify, and Regex Tester support common implementation tasks that usually happen between coding and debugging. + +## Navigation model + +The left navigation is the product map. Tool categories stay stable, while support and documentation live in the help menu at the bottom. The header reflects the active page or tool, and each tool owns its own compact toolbar inside the workspace. + +## What to build next + +When adding a new feature, start with the workflow: + +1. What does the user paste, type, or generate? +2. What result do they need to copy or export? +3. What validation or warning prevents mistakes? +4. Which controls should be near the input, and which should be near the output? +5. Does it follow the existing ToolSurface, PaneHeader, button, toggle, and segmented-control patterns? +`, + }, + { + description: + "Implementation guidance for extending Forge without breaking the product language.", + id: "guides", + title: "Guides", + markdown: `# Guides + +These guides are written for continuing Forge development. They are intentionally practical: follow the existing patterns first, then add abstraction only when the code asks for it. + +## Add a new tool + +Every tool should have a feature folder under \`src/features/\`. + +\`\`\`txt +src/features/example-tool/ + ExampleToolPage.tsx + example-tool.service.ts + example-tool.service.test.ts + example-tool.schema.ts + index.ts +\`\`\` + +Register the tool in \`src/core/registry/tool.registry.ts\` with a stable id, route, category, keywords, icon, and status. Then connect the page in \`ToolPlaceholderPage.tsx\`. + +## Use the Forge workspace pattern + +Most tools should use: + +- \`ToolSurface\` for the outer shell. +- \`ToolToolbar\` for compact mode controls and primary actions. +- \`PaneHeader\` for input/result labels and lightweight stats. +- Two-pane grids for edit-and-result workflows. +- Result cards for copyable outputs. + +Avoid hero-style layouts inside tools. Tool screens should feel operational and repeatable. + +## Place actions by intent + +Put global actions in the toolbar: generate, export, clear, reset, download. + +Put value-specific actions next to the value: copy, show, hide, inspect, validate. This is why Password Generator and JWT Secret Generator place Copy and Show/Hide beside the generated output. + +## Validation and errors + +Validation should be visible in the workspace: + +- Use rose panels for invalid input. +- Keep the original input editable. +- Preserve partial results when useful. +- Avoid modal-only errors. + +## Tests + +Services should own deterministic behavior and test coverage. UI can stay focused when service tests cover parsing, formatting, conversion, and edge cases. + +Before finishing a feature, run: + +\`\`\`bash +pnpm typecheck +pnpm lint +pnpm test +pnpm build +\`\`\` + +## Commit shape + +Prefer small commits that tell the development story: + +1. Service and tests. +2. Workspace UI. +3. Registry or route wiring. +4. Polish or consistency fixes. +`, + }, + { + description: "Keyboard shortcuts and interaction conventions used across Forge.", + id: "shortcuts", + title: "Keyboard shortcuts", + markdown: `# Keyboard shortcuts + +Forge keeps shortcuts small and predictable. The goal is to speed up repeated work without making the product feel like it has hidden rules. + +## Global shortcuts + +| Shortcut | Action | +| --- | --- | +| \`Ctrl K\` | Open command palette | +| \`Cmd K\` | Open command palette on macOS | +| \`Esc\` | Close dialogs, menus, and popovers when supported | + +## Editor conventions + +Text-heavy tools support normal editor expectations: + +- \`Ctrl A\` selects editable content. +- \`Ctrl C\` copies selected text. +- \`Tab\` inserts indentation in JSON-style editors where implemented. +- Long outputs should support wrapping controls when horizontal inspection matters. + +## Navigation conventions + +- Tool routes live under \`/tools/:toolId\`. +- Documentation lives under \`/docs\` and \`/docs/:docId\`. +- Support pages live under \`/support/:mode\`. + +## Copy conventions + +Copy buttons should be visible beside generated or transformed output. Header copy actions are only useful when the page has a single obvious output. +`, + }, +]; + +export function getDocPage(docId?: string): DocPageContent { + return docPages.find((page) => page.id === docId) ?? docPages[0]; +} diff --git a/src/pages/docs/docs.toc.ts b/src/pages/docs/docs.toc.ts new file mode 100644 index 0000000..04dddcb --- /dev/null +++ b/src/pages/docs/docs.toc.ts @@ -0,0 +1,32 @@ +export interface TocItem { + id: string; + level: number; + title: string; +} + +export function createToc(markdown: string): TocItem[] { + return markdown + .split("\n") + .map((line) => { + const match = /^(#{2,3})\s+(.+)$/.exec(line); + + if (!match) { + return null; + } + + return { + id: slugifyHeading(match[2]), + level: match[1].length, + title: match[2], + }; + }) + .filter((item): item is TocItem => item !== null); +} + +export function slugifyHeading(value: string): string { + return value + .toLowerCase() + .replace(/`/g, "") + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-+|-+$/g, ""); +} diff --git a/src/pages/home/HomePage.tsx b/src/pages/home/HomePage.tsx index 72f6f70..e8bfa21 100644 --- a/src/pages/home/HomePage.tsx +++ b/src/pages/home/HomePage.tsx @@ -1,113 +1,157 @@ import type { JSX } from "react"; -import { ArrowRight, Boxes, Cpu, Database, Shield } from "lucide-react"; -import { Link } from "react-router"; -import { toolCategoryDefinitions } from "@/core/registry/tool.categories"; -import { toolRegistry } from "@/core/registry/tool.registry"; -import { MainLayout } from "@/layouts/MainLayout"; -import { Badge } from "@/shared/ui/badge"; +import { ArrowDown, Check, Minus } from "lucide-react"; -const highlights = [ +const notes = [ { - icon: Boxes, - label: "Registry-driven", - value: "Single source of truth for tools", + eyebrow: "Context", + text: "Small developer tasks rarely deserve a full context switch, but they happen all day.", + title: "A payload arrives messy.", }, { - icon: Shield, - label: "Private by default", - value: "Core workflows run locally", + eyebrow: "Workspace", + text: "Forge keeps previews, formatters, encoders, decoders, and generators in one calm surface.", + title: "The tool stays close.", }, { - icon: Cpu, - label: "Fast foundation", - value: "Vite, strict TypeScript, CI", + eyebrow: "Output", + text: "The final value is easy to inspect, copy, export, and bring back to the work that mattered.", + title: "You leave with the result.", }, ]; +const before = [ + "One tab for formatting.", + "Another for decoding.", + "A third for comparing text.", + "Different buttons, colors, shortcuts, and trust models.", +]; + +const after = [ + "One navigation model.", + "One toolbar language.", + "One copy/export pattern.", + "Local-first workflows for sensitive text.", +]; + export function HomePage(): JSX.Element { return ( - -
-
-
- {toolRegistry.length} planned tools -

- Build, inspect, convert, and validate without leaving the browser. -

-

- The first milestone establishes navigation, registry metadata, and a - polished white-first interface before deeper tool logic is added. -

-
- - Open JSON Formatter -
+
+
+
+
+ Forge / Developer Workstation + Local-first tools
-
- {highlights.map((item) => { - const Icon = item.icon; +
+
+

+ For the in-between work +

+

+ Developer tools + + without the tab drift. + +

+
- return ( -
-
- ); - })} +
+

+ Forge is the quiet place between writing code and shipping it: the moment + you need to inspect a token, shape JSON, compare text, generate a secret, + test a regex, or turn rough input into something usable. +

+
+
+
-
-
-
-
+ +
+ + +
- return ( -
-
-
-
-
-

{category.id}

- {count} -
-

- {category.description} -

-
-
- ); - })} +
+
+

+ Design language +

+
+

+ The interface is intentionally quiet because the input is usually noisy. +

+

+ Tool screens use dense panes, restrained borders, clear copy actions, and + predictable controls. The home page is the front door; the tools are the + workshop. Both should feel like they belong to the same place. +

+
+
+
+
+
+ ); +} + +function ComparisonCard({ + items, + marker, + subtitle, + title, +}: { + items: string[]; + marker: "check" | "minus"; + subtitle: string; + title: string; +}): JSX.Element { + const Icon = marker === "check" ? Check : Minus; + + return ( +
+

+ {subtitle} +

+

+ {title} +

+
+ {items.map((item) => ( +
+ + + {item}
-
-
-
+ ))} +
+
); } diff --git a/src/pages/support/SupportPage.tsx b/src/pages/support/SupportPage.tsx new file mode 100644 index 0000000..7f391b6 --- /dev/null +++ b/src/pages/support/SupportPage.tsx @@ -0,0 +1,260 @@ +import type { JSX } from "react"; +import { useEffect, useMemo, useState } from "react"; +import { + AlertTriangle, + Github, + HeartHandshake, + Linkedin, + Mail, + MessageCircle, +} from "lucide-react"; +import { NavLink, useParams } from "react-router"; +import { cn } from "@/shared/lib/cn"; + +const supportModes = [ + { + description: "Ask how a tool should work, clarify a workflow, or request guidance.", + icon: MessageCircle, + id: "ask", + title: "Ask a question", + }, + { + description: "Report broken behavior, incorrect output, or UI regressions.", + icon: AlertTriangle, + id: "issue", + title: "Report an issue", + }, + { + description: "Suggest improvements, missing tools, or better defaults.", + icon: HeartHandshake, + id: "feedback", + title: "Share feedback", + }, +]; + +const contact = { + email: "cuthanhcam04@gmail.com", + github: "https://github.com/cuthanhcam", + issues: "https://github.com/orcace/forge/issues", + linkedin: "https://www.linkedin.com/in/cuthanhcam/", + repository: "https://github.com/orcace/forge", +}; + +export function SupportPage(): JSX.Element { + const { mode } = useParams(); + const activeMode = supportModes.find((item) => item.id === mode) ?? supportModes[0]; + const ActiveIcon = activeMode.icon; + const [subject, setSubject] = useState(`Forge - ${activeMode.title}`); + const [message, setMessage] = useState(""); + const mailtoHref = useMemo(() => { + const body = message || defaultMessage(activeMode.id); + + return `mailto:${contact.email}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`; + }, [activeMode.id, message, subject]); + + useEffect(() => { + setSubject(`Forge - ${activeMode.title}`); + setMessage(""); + }, [activeMode.title]); + + return ( +
+
+
+
+

+ Support +

+

+ Keep the work moving without leaving the product context. +

+

+ Use the path that matches what you need: ask a question, report an issue, or + share feedback for the next Forge iteration. +

+
+ +
+ + +
+
+
+
+
+

+ {activeMode.title} +

+

+ {activeMode.description} +

+
+
+ +
+ + + +
+ +
+
+

+ Compose an email +

+

+ Write a quick note here, then open it in your mail app. For + reproducible bugs, GitHub issues are better because they stay + trackable in the real repository. +

+ +