diff --git a/artifacts/personal-tool-console/index.html b/artifacts/personal-tool-console/index.html index 5d4bce9..f19de2b 100644 --- a/artifacts/personal-tool-console/index.html +++ b/artifacts/personal-tool-console/index.html @@ -1,21 +1,42 @@ - + - - Personal Tool Console - + + Toolbox — Tools for text, code, media & math + - - + + - - + + - - - + + +
diff --git a/artifacts/personal-tool-console/src/App.tsx b/artifacts/personal-tool-console/src/App.tsx index a3baeaa..506b977 100644 --- a/artifacts/personal-tool-console/src/App.tsx +++ b/artifacts/personal-tool-console/src/App.tsx @@ -2,16 +2,45 @@ import { Switch, Route, Router as WouterRouter } from "wouter"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { Toaster } from "@/components/ui/toaster"; import { TooltipProvider } from "@/components/ui/tooltip"; +import { AppShell } from "@/components/layout/app-shell"; +import Catalog from "@/pages/catalog"; +import Console from "@/pages/console"; +import ToolPage from "@/pages/tool-page"; import NotFound from "@/pages/not-found"; -import Home from "@/pages/home"; const queryClient = new QueryClient(); function Router() { return ( - - + + {() => ( + + + + )} + + + {() => ( + + + + )} + + + {({ slug }) => ( + + + + )} + + + {() => ( + + + + )} + ); } diff --git a/artifacts/personal-tool-console/src/components/layout/app-shell.tsx b/artifacts/personal-tool-console/src/components/layout/app-shell.tsx new file mode 100644 index 0000000..264ad33 --- /dev/null +++ b/artifacts/personal-tool-console/src/components/layout/app-shell.tsx @@ -0,0 +1,13 @@ +import type { ReactNode } from "react"; +import { SiteHeader } from "@/components/layout/site-header"; +import { SiteFooter } from "@/components/layout/site-footer"; + +export function AppShell({ children }: { children: ReactNode }) { + return ( +
+ +
{children}
+ +
+ ); +} diff --git a/artifacts/personal-tool-console/src/components/layout/command-palette.tsx b/artifacts/personal-tool-console/src/components/layout/command-palette.tsx new file mode 100644 index 0000000..32c40c5 --- /dev/null +++ b/artifacts/personal-tool-console/src/components/layout/command-palette.tsx @@ -0,0 +1,125 @@ +import { useEffect, useMemo, useState } from "react"; +import { useLocation } from "wouter"; +import { Search, TerminalSquare } from "lucide-react"; +import { + CommandDialog, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, + CommandSeparator, +} from "@/components/ui/command"; +import { Kbd } from "@/components/ui/kbd"; +import { searchTools, getRecentTools } from "@/lib/tool-registry"; +import type { ToolDefinition } from "@/lib/tool-registry"; + +export function CommandPalette() { + const [open, setOpen] = useState(false); + const [value, setValue] = useState(""); + const [, navigate] = useLocation(); + + useEffect(() => { + const onKey = (e: KeyboardEvent) => { + if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") { + e.preventDefault(); + setOpen((o) => !o); + } + }; + window.addEventListener("keydown", onKey); + return () => window.removeEventListener("keydown", onKey); + }, []); + + const results = useMemo( + () => (value.trim() ? searchTools(value, 10) : []), + [value], + ); + const recents = useMemo( + () => (value.trim() ? [] : getRecentTools().slice(0, 5)), + [value, open], + ); + + const run = (url: string) => { + setOpen(false); + setValue(""); + const base = import.meta.env.BASE_URL.replace(/\/$/, ""); + navigate(base + url); + }; + + const ToolRow = ({ tool }: { tool: ToolDefinition }) => { + const Icon = tool.icon; + return ( + run(`/tools/${tool.slug}`)} + > + + {tool.name} + {tool.status === "coming-soon" && ( + + Soon + + )} + + ); + }; + + return ( + <> +
+ + +
+ + + + + No tools found for “{value}”. + {value.trim() ? ( + + {results.map((tool) => ( + + ))} + + ) : ( + <> + run("/console")}> + + Open command console + + {recents.length > 0 && ( + <> + + + {recents.map((tool) => ( + + ))} + + + )} + + )} + + + + ); +} diff --git a/artifacts/personal-tool-console/src/components/layout/site-footer.tsx b/artifacts/personal-tool-console/src/components/layout/site-footer.tsx new file mode 100644 index 0000000..e1bf999 --- /dev/null +++ b/artifacts/personal-tool-console/src/components/layout/site-footer.tsx @@ -0,0 +1,37 @@ +import { Link } from "wouter"; +import { Wrench } from "lucide-react"; +import { SITE_NAME } from "@/hooks/use-page-title"; + +export function SiteFooter() { + return ( + + ); +} diff --git a/artifacts/personal-tool-console/src/components/layout/site-header.tsx b/artifacts/personal-tool-console/src/components/layout/site-header.tsx new file mode 100644 index 0000000..228dbdf --- /dev/null +++ b/artifacts/personal-tool-console/src/components/layout/site-header.tsx @@ -0,0 +1,47 @@ +import { Link, useLocation } from "wouter"; +import { Wrench } from "lucide-react"; +import { CommandPalette } from "@/components/layout/command-palette"; +import { SITE_NAME } from "@/hooks/use-page-title"; +import { cn } from "@/lib/utils"; + +const NAV = [ + { href: "/", label: "Catalog" }, + { href: "/console", label: "Console" }, +]; + +export function SiteHeader() { + const [path] = useLocation(); + return ( +
+
+ + + + + {SITE_NAME} + + + +
+
+ ); +} diff --git a/artifacts/personal-tool-console/src/components/layout/tool-page-layout.tsx b/artifacts/personal-tool-console/src/components/layout/tool-page-layout.tsx new file mode 100644 index 0000000..3cc6afc --- /dev/null +++ b/artifacts/personal-tool-console/src/components/layout/tool-page-layout.tsx @@ -0,0 +1,124 @@ +import type { ReactNode } from "react"; +import { Link } from "wouter"; +import { ChevronRight } from "lucide-react"; +import type { ToolDefinition, ToolCategoryMeta } from "@/lib/tool-registry"; +import { Badge } from "@/components/ui/badge"; +import { SquareTerminal, type LucideIcon } from "lucide-react"; + +function RelatedCard({ tool }: { tool: ToolDefinition }) { + const Icon = tool.icon; + return ( + +
+ + + +
+

{tool.name}

+

+ {tool.description} +

+
+
+ + ); +} + +export function ToolPageLayout({ + tool, + category, + related, + onCopyCommand, + children, +}: { + tool: ToolDefinition; + category: ToolCategoryMeta; + related: ToolDefinition[]; + onCopyCommand?: () => void; + children: ReactNode; +}) { + const CatIcon = category.icon as LucideIcon; + return ( +
+ + +
+
+ + + +
+
+

{tool.name}

+ {tool.status === "coming-soon" && ( + + Coming soon + + )} +
+

+ {tool.description} +

+ {tool.commands && tool.commands.length > 0 && ( +
+ + Console: + + {tool.commands.slice(0, 3).map((c) => ( + + ))} +
+ )} +
+
+
+ + {children} + + {related.length > 0 && ( +
+

+ + More in {category.label} +

+
+ {related.map((t) => ( + + ))} +
+
+ )} +
+ ); +} diff --git a/artifacts/personal-tool-console/src/components/tools/base64.tsx b/artifacts/personal-tool-console/src/components/tools/base64.tsx new file mode 100644 index 0000000..363c7f0 --- /dev/null +++ b/artifacts/personal-tool-console/src/components/tools/base64.tsx @@ -0,0 +1,75 @@ +import { useState } from "react"; +import type { ToolPageProps } from "@/lib/tool-registry"; +import { base64Encode, base64Decode } from "@/lib/tools/base64"; +import { + Field, + TextArea, + SelectInput, + OutBox, + Chip, +} from "@/components/tools/shared/fields"; + +export default function Base64({ tool }: ToolPageProps) { + const [mode, setMode] = useState("Encode"); + const [input, setInput] = useState(""); + const [error, setError] = useState(null); + + const transform = () => { + if (!input) return ""; + try { + setError(null); + return mode === "Encode" ? base64Encode(input) : base64Decode(input); + } catch (e) { + setError((e as Error).message); + return ""; + } + }; + + const output = transform(); + + return ( +
+
+ { + setMode(e.target.value); + setError(null); + }} + options={["Encode", "Decode"]} + className="w-36" + /> + {tool.slug} +
+ +
+ +