From 6f41fa7cbe54a847d6cbfe9ee0cf8efcf580ce83 Mon Sep 17 00:00:00 2001 From: Fusion Date: Tue, 19 May 2026 19:33:06 +0200 Subject: [PATCH] feat: implement universal icon system with Phosphor Icons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Install @phosphor-icons/react package - Create centralized Icon component with size/weight/color variants - Create icon registry with 34 icons across 5 categories - Replace all raw Unicode symbols with proper icon components - Add icons to navigation, buttons, status indicators, git operations - Add icon CSS with consistent sizing and spacing - Fix type definitions for Phosphor icon compatibility Quality gates: typecheck ✓, lint ✓, build ✓ (375KB bundle) --- apps/web/package-lock.json | 14 ++ apps/web/package.json | 1 + apps/web/src/components/app-shell.tsx | 16 +- apps/web/src/components/commit-dialog.tsx | 15 +- apps/web/src/components/file-editor.tsx | 15 +- apps/web/src/components/git-toolbar.tsx | 33 ++-- apps/web/src/components/icon.tsx | 150 ++++++++++++++++ apps/web/src/components/merge-dialog.tsx | 14 +- .../web/src/components/syntax-highlighter.tsx | 14 +- apps/web/src/components/workspace-header.tsx | 9 +- apps/web/src/pages/dashboard.tsx | 4 + apps/web/src/pages/git-history.tsx | 4 +- apps/web/src/pages/git-repositories.tsx | 13 +- apps/web/src/pages/placeholder.tsx | 3 + apps/web/src/pages/profile.tsx | 26 ++- apps/web/src/pages/projects.tsx | 20 ++- apps/web/src/pages/repo-workspace.tsx | 6 +- apps/web/src/pages/settings.tsx | 14 +- apps/web/src/pages/ssh-keys.tsx | 15 +- apps/web/src/pages/tool-types.tsx | 34 +++- apps/web/src/styles.css | 54 ++++++ apps/web/src/utils/icons.ts | 163 ++++++++++++++++++ .../.openspec.yaml | 0 .../design.md | 0 .../proposal.md | 0 .../specs/spec.md | 0 .../tasks.md | 0 .../.openspec.yaml | 0 .../design.md | 0 .../proposal.md | 0 .../specs/spec.md | 0 .../tasks.md | 0 .../.openspec/config.yaml | 2 + .../changes/universal-icon-system/design.md | 149 ++++++++++++++++ .../changes/universal-icon-system/proposal.md | 58 +++++++ .../universal-icon-system/specs/spec.md | 111 ++++++++++++ .../changes/universal-icon-system/tasks.md | 121 +++++++++++++ 37 files changed, 1037 insertions(+), 41 deletions(-) create mode 100644 apps/web/src/components/icon.tsx create mode 100644 apps/web/src/utils/icons.ts rename openspec/changes/{documentation-overhaul => archive/2026-05-19-documentation-overhaul}/.openspec.yaml (100%) rename openspec/changes/{documentation-overhaul => archive/2026-05-19-documentation-overhaul}/design.md (100%) rename openspec/changes/{documentation-overhaul => archive/2026-05-19-documentation-overhaul}/proposal.md (100%) rename openspec/changes/{documentation-overhaul => archive/2026-05-19-documentation-overhaul}/specs/spec.md (100%) rename openspec/changes/{documentation-overhaul => archive/2026-05-19-documentation-overhaul}/tasks.md (100%) rename openspec/changes/{smart-git-url-parsing => archive/2026-05-19-smart-git-url-parsing}/.openspec.yaml (100%) rename openspec/changes/{smart-git-url-parsing => archive/2026-05-19-smart-git-url-parsing}/design.md (100%) rename openspec/changes/{smart-git-url-parsing => archive/2026-05-19-smart-git-url-parsing}/proposal.md (100%) rename openspec/changes/{smart-git-url-parsing => archive/2026-05-19-smart-git-url-parsing}/specs/spec.md (100%) rename openspec/changes/{smart-git-url-parsing => archive/2026-05-19-smart-git-url-parsing}/tasks.md (100%) create mode 100644 openspec/changes/universal-icon-system/.openspec/config.yaml create mode 100644 openspec/changes/universal-icon-system/design.md create mode 100644 openspec/changes/universal-icon-system/proposal.md create mode 100644 openspec/changes/universal-icon-system/specs/spec.md create mode 100644 openspec/changes/universal-icon-system/tasks.md diff --git a/apps/web/package-lock.json b/apps/web/package-lock.json index 989ed60..6f1bda9 100644 --- a/apps/web/package-lock.json +++ b/apps/web/package-lock.json @@ -8,6 +8,7 @@ "name": "headquarter-web", "version": "0.1.0", "dependencies": { + "@phosphor-icons/react": "^2.1.10", "@types/prismjs": "^1.26.6", "axios": "^1.6.0", "prismjs": "^1.30.0", @@ -1271,6 +1272,19 @@ "url": "https://github.com/sponsors/Boshen" } }, + "node_modules/@phosphor-icons/react": { + "version": "2.1.10", + "resolved": "https://registry.npmjs.org/@phosphor-icons/react/-/react-2.1.10.tgz", + "integrity": "sha512-vt8Tvq8GLjheAZZYa+YG/pW7HDbov8El/MANW8pOAz4eGxrwhnbfrQZq0Cp4q8zBEu8NIhHdnr+r8thnfRSNYA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">= 16.8", + "react-dom": ">= 16.8" + } + }, "node_modules/@remix-run/router": { "version": "1.23.2", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz", diff --git a/apps/web/package.json b/apps/web/package.json index 2158dbb..8e15010 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -11,6 +11,7 @@ "test": "vitest run" }, "dependencies": { + "@phosphor-icons/react": "^2.1.10", "@types/prismjs": "^1.26.6", "axios": "^1.6.0", "prismjs": "^1.30.0", diff --git a/apps/web/src/components/app-shell.tsx b/apps/web/src/components/app-shell.tsx index 99ba177..f749dd7 100644 --- a/apps/web/src/components/app-shell.tsx +++ b/apps/web/src/components/app-shell.tsx @@ -2,13 +2,15 @@ import { Link, NavLink, Outlet } from "react-router-dom"; import { useTheme } from "../hooks/use-theme"; import { useAuth } from "../state/auth"; +import { Icon } from "./icon"; +import type { IconName } from "../utils/icons"; -const NAV_ITEMS = [ - { to: "/", label: "Dashboard" }, - { to: "/projects", label: "Projects" }, - { to: "/ssh-keys", label: "SSH Keys" }, - { to: "/tool-types", label: "Tool Types" }, - { to: "/settings", label: "Settings" } +const NAV_ITEMS: { to: string; label: string; icon: IconName }[] = [ + { to: "/", label: "Dashboard", icon: "dashboard" }, + { to: "/projects", label: "Projects", icon: "projects" }, + { to: "/ssh-keys", label: "SSH Keys", icon: "profile" }, + { to: "/tool-types", label: "Tool Types", icon: "code" }, + { to: "/settings", label: "Settings", icon: "settings" } ]; export const AppShell = () => { @@ -32,6 +34,7 @@ export const AppShell = () => { }} type="button" > + Logout @@ -46,6 +49,7 @@ export const AppShell = () => { className={({ isActive }) => (isActive ? "nav-item nav-item-active" : "nav-item")} end={item.to === "/"} > + {item.label} ))} diff --git a/apps/web/src/components/commit-dialog.tsx b/apps/web/src/components/commit-dialog.tsx index 0f3aefd..491142f 100644 --- a/apps/web/src/components/commit-dialog.tsx +++ b/apps/web/src/components/commit-dialog.tsx @@ -1,5 +1,7 @@ import React, { useState } from "react"; +import { Icon } from "./icon"; + interface CommitDialogProps { isOpen: boolean; filePath: string; @@ -129,6 +131,7 @@ export const CommitDialog: React.FC = ({ onClick={onCancel} type="button" > + Cancel diff --git a/apps/web/src/components/file-editor.tsx b/apps/web/src/components/file-editor.tsx index c6ff8ba..667080a 100644 --- a/apps/web/src/components/file-editor.tsx +++ b/apps/web/src/components/file-editor.tsx @@ -4,6 +4,7 @@ import { apiClient } from "../api/client"; import { useAuth } from "../state/auth"; import { CodeEditor } from "../components/code-editor"; import { CommitDialog } from "../components/commit-dialog"; +import { Icon } from "../components/icon"; import { SyntaxHighlighter } from "../components/syntax-highlighter"; import { detectLanguage } from "../utils/language"; @@ -172,6 +173,7 @@ export const FileEditor: React.FC = ({ onClick={handleEdit} type="button" > + Edit )} @@ -183,13 +185,24 @@ export const FileEditor: React.FC = ({ disabled={content === originalContent || saving} type="button" > - {saving ? "Saving..." : "Save"} + {saving ? ( + <> + + Saving... + + ) : ( + <> + + Save + + )} diff --git a/apps/web/src/components/git-toolbar.tsx b/apps/web/src/components/git-toolbar.tsx index 37cdea2..15313dc 100644 --- a/apps/web/src/components/git-toolbar.tsx +++ b/apps/web/src/components/git-toolbar.tsx @@ -9,6 +9,7 @@ import { pushRepository, type GitStatus, } from "../api/git_repositories"; +import { Icon } from "./icon"; import { MergeDialog } from "./merge-dialog"; interface GitToolbarProps { @@ -140,7 +141,13 @@ export const GitToolbar = ({ > {branches.map((b) => ( ))} @@ -150,18 +157,18 @@ export const GitToolbar = ({ disabled={loading} type="button" > - + New + New
-
@@ -217,24 +224,24 @@ export const GitToolbar = ({ disabled={loading || !newBranchName.trim()} type="button" > - Create + Create )} {hasChanges && status && (
- {status.modified.length > 0 && ✏️ {status.modified.length} modified} - {status.added.length > 0 && ✨ {status.added.length} added} - {status.deleted.length > 0 && 🗑️ {status.deleted.length} deleted} - {status.untracked.length > 0 && ❓ {status.untracked.length} untracked} + {status.modified.length > 0 && {status.modified.length} modified} + {status.added.length > 0 && {status.added.length} added} + {status.deleted.length > 0 && {status.deleted.length} deleted} + {status.untracked.length > 0 && {status.untracked.length} untracked}
)} diff --git a/apps/web/src/components/icon.tsx b/apps/web/src/components/icon.tsx new file mode 100644 index 0000000..f3c95ba --- /dev/null +++ b/apps/web/src/components/icon.tsx @@ -0,0 +1,150 @@ +import React from "react"; +import { + House, + Folder, + GitBranch, + Gear, + User, + SignOut, + Plus, + PencilSimple, + Trash, + FloppyDisk, + X, + ArrowsClockwise, + Copy, + MagnifyingGlass, + List, + Check, + Warning, + Info, + Spinner, + GitCommit, + GitMerge, + ClockCounterClockwise, + ArrowDown, + ArrowUp, + File, + FileText, + Image, + Binary, + Code, +} from "@phosphor-icons/react"; + +export type IconName = + | "dashboard" + | "projects" + | "repositories" + | "settings" + | "profile" + | "logout" + | "add" + | "edit" + | "delete" + | "save" + | "cancel" + | "refresh" + | "copy" + | "search" + | "menu" + | "close" + | "success" + | "error" + | "warning" + | "info" + | "loading" + | "branch" + | "commit" + | "merge" + | "history" + | "pull" + | "push" + | "fetch" + | "file" + | "folder" + | "code" + | "document" + | "image" + | "binary"; + +const iconMap: Record> = { + dashboard: House, + projects: Folder, + repositories: GitBranch, + settings: Gear, + profile: User, + logout: SignOut, + add: Plus, + edit: PencilSimple, + delete: Trash, + save: FloppyDisk, + cancel: X, + refresh: ArrowsClockwise, + copy: Copy, + search: MagnifyingGlass, + menu: List, + close: X, + success: Check, + error: X, + warning: Warning, + info: Info, + loading: Spinner, + branch: GitBranch, + commit: GitCommit, + merge: GitMerge, + history: ClockCounterClockwise, + pull: ArrowDown, + push: ArrowUp, + fetch: ArrowsClockwise, + file: File, + folder: Folder, + code: Code, + document: FileText, + image: Image, + binary: Binary, +}; + +export interface IconProps { + name: IconName; + size?: "sm" | "md" | "lg" | "xl"; + color?: string; + weight?: "thin" | "light" | "regular" | "bold" | "fill" | "duotone"; + className?: string; + ariaLabel?: string; +} + +const sizeMap: Record, number> = { + sm: 16, + md: 20, + lg: 24, + xl: 32, +}; + +export const Icon: React.FC = ({ + name, + size = "md", + color, + weight = "regular", + className, + ariaLabel, +}) => { + const IconComponent = iconMap[name]; + const sizeValue = sizeMap[size]; + + if (!IconComponent) { + console.warn(`Icon "${name}" not found`); + return null; + } + + return ( + + + + ); +}; diff --git a/apps/web/src/components/merge-dialog.tsx b/apps/web/src/components/merge-dialog.tsx index 1bb6c28..6542bb0 100644 --- a/apps/web/src/components/merge-dialog.tsx +++ b/apps/web/src/components/merge-dialog.tsx @@ -1,6 +1,7 @@ import { useState } from "react"; import { mergeBranches } from "../api/git_repositories"; +import { Icon } from "./icon"; interface MergeDialogProps { projectId: string; @@ -114,6 +115,7 @@ export const MergeDialog = ({ disabled={loading} type="button" > + Cancel diff --git a/apps/web/src/components/syntax-highlighter.tsx b/apps/web/src/components/syntax-highlighter.tsx index a007e63..09fe87b 100644 --- a/apps/web/src/components/syntax-highlighter.tsx +++ b/apps/web/src/components/syntax-highlighter.tsx @@ -1,4 +1,6 @@ import React, { useEffect, useState } from "react"; + +import { Icon } from "./icon"; import { highlightCode, loadLanguage } from "../utils/language"; interface SyntaxHighlighterProps { @@ -40,7 +42,17 @@ export const SyntaxHighlighter: React.FC = ({ onClick={handleCopy} type="button" > - {copied ? "Copied!" : "Copy"} + {copied ? ( + <> + + Copied! + + ) : ( + <> + + Copy + + )}
diff --git a/apps/web/src/components/workspace-header.tsx b/apps/web/src/components/workspace-header.tsx index 4507af2..23c263d 100644 --- a/apps/web/src/components/workspace-header.tsx +++ b/apps/web/src/components/workspace-header.tsx @@ -1,4 +1,5 @@ import { Link } from "react-router-dom"; +import { Icon } from "./icon"; interface WorkspaceHeaderProps { project: { @@ -16,7 +17,9 @@ export const WorkspaceHeader = ({ project, currentRepo }: WorkspaceHeaderProps) return (
-
📁
+
+ +

{project.name}

{currentRepo && ( @@ -29,14 +32,14 @@ export const WorkspaceHeader = ({ project, currentRepo }: WorkspaceHeaderProps) className="workspace-header-action-btn" to={`/projects/${project.id}/repositories/${currentRepo?.id || ""}/history`} > - 🕐 + History - ⚙️ + Settings
diff --git a/apps/web/src/pages/dashboard.tsx b/apps/web/src/pages/dashboard.tsx index d7f7146..e786a90 100644 --- a/apps/web/src/pages/dashboard.tsx +++ b/apps/web/src/pages/dashboard.tsx @@ -1,6 +1,7 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import { getDashboardSummary, type DashboardSummary } from "../api/dashboard"; +import { Icon } from "../components/icon"; const CARDS = [ { label: "Projects", key: "projects" }, @@ -50,6 +51,7 @@ export const DashboardPage = () => {

Dashboard is unavailable

@@ -68,9 +70,11 @@ export const DashboardPage = () => {
diff --git a/apps/web/src/pages/git-history.tsx b/apps/web/src/pages/git-history.tsx index 0e0b769..777d142 100644 --- a/apps/web/src/pages/git-history.tsx +++ b/apps/web/src/pages/git-history.tsx @@ -2,6 +2,7 @@ import { useCallback, useEffect, useState } from "react"; import { useNavigate, useParams } from "react-router-dom"; import { getCommitDetail, getRepositoryHistory, type CommitDetail, type CommitHistoryEntry } from "../api/git_repositories"; +import { Icon } from "../components/icon"; export const GitHistoryPage = () => { const { projectId, repoId } = useParams<{ projectId: string; repoId: string }>(); @@ -70,6 +71,7 @@ export const GitHistoryPage = () => {

Failed to load commit history

@@ -155,7 +157,7 @@ export const GitHistoryPage = () => {

Commit Details

diff --git a/apps/web/src/pages/git-repositories.tsx b/apps/web/src/pages/git-repositories.tsx index 16720c4..45ddc80 100644 --- a/apps/web/src/pages/git-repositories.tsx +++ b/apps/web/src/pages/git-repositories.tsx @@ -10,6 +10,7 @@ import { type URLParseResult, } from "../api/git_repositories"; import type { GitRepository } from "../api/git_repositories"; +import { Icon } from "../components/icon"; type RepoStatus = "loading" | "ready" | "error"; type UrlValidationStatus = "idle" | "validating" | "valid" | "needs-parsing" | "invalid"; @@ -160,6 +161,7 @@ export const GitRepositoriesPage = () => {

Repositories

@@ -170,6 +172,7 @@ export const GitRepositoriesPage = () => {

Failed to load repositories

@@ -256,12 +259,14 @@ export const GitRepositoriesPage = () => { Validating... )} {urlValidation.status === "valid" && ( - ✓ Valid git URL + + Valid git URL + )} {urlValidation.status === "needs-parsing" && urlValidation.result && (
- ⚠ This looks like a browser URL + This looks like a browser URL
@@ -279,7 +284,7 @@ export const GitRepositoriesPage = () => { )} {urlValidation.status === "invalid" && ( - ✗ Invalid URL + Invalid URL )} @@ -292,9 +297,11 @@ export const GitRepositoriesPage = () => { )}
diff --git a/apps/web/src/pages/placeholder.tsx b/apps/web/src/pages/placeholder.tsx index 6c55291..322fc26 100644 --- a/apps/web/src/pages/placeholder.tsx +++ b/apps/web/src/pages/placeholder.tsx @@ -18,6 +18,8 @@ export const NotFoundPage = () => { const API_BASE_URL = import.meta.env.VITE_API_BASE_URL ?? "http://localhost:8000"; +import { Icon } from "../components/icon"; + export const LoginRedirectPage = () => { const nextPath = new URLSearchParams(window.location.search).get("next") ?? "/"; const encodedNext = encodeURIComponent(nextPath); @@ -27,6 +29,7 @@ export const LoginRedirectPage = () => {

Sign in required

You need to authenticate to access this section.

+ Continue to login diff --git a/apps/web/src/pages/profile.tsx b/apps/web/src/pages/profile.tsx index 183dc14..45d8718 100644 --- a/apps/web/src/pages/profile.tsx +++ b/apps/web/src/pages/profile.tsx @@ -1,6 +1,7 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { getProfile, updateProfile, uploadAvatar } from "../api/profile"; +import { Icon } from "../components/icon"; import { useAuth } from "../state/auth"; import type { UserProfile } from "../api/profile"; @@ -99,6 +100,7 @@ export const ProfilePage = () => {

Failed to load profile

@@ -120,7 +122,17 @@ export const ProfilePage = () => { onClick={() => fileInputRef.current?.click()} type="button" > - {status === "saving" ? "Uploading..." : "Change Avatar"} + {status === "saving" ? ( + <> + + Uploading... + + ) : ( + <> + + Change Avatar + + )} { onClick={() => void handleSave()} type="button" > - {status === "saving" ? "Saving..." : "Save Changes"} + {status === "saving" ? ( + <> + + Saving... + + ) : ( + <> + + Save Changes + + )}
diff --git a/apps/web/src/pages/projects.tsx b/apps/web/src/pages/projects.tsx index 29c2ba2..91f278c 100644 --- a/apps/web/src/pages/projects.tsx +++ b/apps/web/src/pages/projects.tsx @@ -10,6 +10,7 @@ import { type ProjectCreateInput, type ProjectUpdateInput, } from "../api/projects"; +import { Icon } from "../components/icon"; import type { Project } from "../types"; type ProjectsStatus = "loading" | "ready" | "error"; @@ -110,6 +111,7 @@ export const ProjectsPage = () => {

Projects

@@ -120,6 +122,7 @@ export const ProjectsPage = () => {

Failed to load projects

@@ -144,6 +147,7 @@ export const ProjectsPage = () => { onClick={() => openEdit(project)} type="button" > + Edit {deleteConfirmId === project.id ? ( @@ -154,6 +158,7 @@ export const ProjectsPage = () => { onClick={() => void handleDelete(project.id)} type="button" > + Delete
@@ -170,6 +176,7 @@ export const ProjectsPage = () => { onClick={() => setDeleteConfirmId(project.id)} type="button" > + Delete )} @@ -205,10 +212,21 @@ export const ProjectsPage = () => { {formError &&

{formError}

}
diff --git a/apps/web/src/pages/repo-workspace.tsx b/apps/web/src/pages/repo-workspace.tsx index 898c1be..0e2f259 100644 --- a/apps/web/src/pages/repo-workspace.tsx +++ b/apps/web/src/pages/repo-workspace.tsx @@ -1,4 +1,5 @@ import { useCallback, useEffect, useState } from "react"; +import { Icon } from "../components/icon"; import { Link, useParams, useSearchParams } from "react-router-dom"; @@ -155,6 +156,7 @@ export const RepoWorkspace = () => { onClick={() => void loadRepositories()} type="button" > + Retry
@@ -338,7 +340,7 @@ const FileBrowser = ({
{path && ( )} {entries.map((entry) => { @@ -350,7 +352,7 @@ const FileBrowser = ({ onClick={() => handleEntryClick(entry)} type="button" > - {entry.type === "directory" ? "📁" : "📄"} {entry.name} + {entry.name} {fileStatus && ( {fileStatus === "modified" && "M"} diff --git a/apps/web/src/pages/settings.tsx b/apps/web/src/pages/settings.tsx index 63ece85..5eb3e4e 100644 --- a/apps/web/src/pages/settings.tsx +++ b/apps/web/src/pages/settings.tsx @@ -1,6 +1,7 @@ import { useCallback, useEffect, useState } from "react"; import { getUserConfig, updateUserConfig, type UserConfig, type UserConfigUpdate } from "../api/settings"; +import { Icon } from "../components/icon"; type SettingsStatus = "loading" | "ready" | "error"; @@ -66,6 +67,7 @@ export const SettingsPage = () => {

Failed to load settings

@@ -132,7 +134,17 @@ export const SettingsPage = () => {
{saveStatus === "saved" && Settings saved!} {saveStatus === "error" && Failed to save} diff --git a/apps/web/src/pages/ssh-keys.tsx b/apps/web/src/pages/ssh-keys.tsx index c28c0f4..854a695 100644 --- a/apps/web/src/pages/ssh-keys.tsx +++ b/apps/web/src/pages/ssh-keys.tsx @@ -1,5 +1,6 @@ import { useEffect, useState } from "react"; import { createSSHKey, deleteSSHKey, listSSHKeys, type SSHKey } from "../api/ssh_keys"; +import { Icon } from "../components/icon"; export const SSHKeysPage = () => { const [keys, setKeys] = useState([]); @@ -77,7 +78,17 @@ export const SSHKeysPage = () => { />
@@ -93,6 +104,7 @@ export const SSHKeysPage = () => { onClick={() => handleDelete(key.id)} className="danger-button" > + Delete
@@ -107,6 +119,7 @@ export const SSHKeysPage = () => { onClick={() => copyToClipboard(key.public_key)} className="secondary-button" > + Copy Full Key
diff --git a/apps/web/src/pages/tool-types.tsx b/apps/web/src/pages/tool-types.tsx index f002df0..67d186f 100644 --- a/apps/web/src/pages/tool-types.tsx +++ b/apps/web/src/pages/tool-types.tsx @@ -8,6 +8,7 @@ import { type CreateToolTypeRequest, type UpdateToolTypeRequest, } from "../api/tool_types"; +import { Icon } from "../components/icon"; import type { ToolType } from "../api/tool_types"; type ToolTypesStatus = "loading" | "ready" | "error"; @@ -134,7 +135,10 @@ export const ToolTypesPage = () => { return (

Failed to load tool types.

- +
); } @@ -143,7 +147,10 @@ export const ToolTypesPage = () => {

Tool Types

- +
{toolTypes.length === 0 ? ( @@ -161,12 +168,14 @@ export const ToolTypesPage = () => { {!toolType.is_builtin && ( <> @@ -179,9 +188,13 @@ export const ToolTypesPage = () => {

Delete tool type "{toolType.display_name}"?

- +
@@ -250,8 +263,21 @@ export const ToolTypesPage = () => { {formError &&

{formError}

}
- +
diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css index 4714e6b..5f4f2c4 100644 --- a/apps/web/src/styles.css +++ b/apps/web/src/styles.css @@ -1742,3 +1742,57 @@ pre[class*="language-"] { .token.variable { color: #ec4899; } + +/* Icon System */ +.icon { + display: inline-flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + vertical-align: middle; +} + +.icon svg { + display: block; +} + +.icon-sm { + width: 16px; + height: 16px; +} + +.icon-md { + width: 20px; + height: 20px; +} + +.icon-lg { + width: 24px; + height: 24px; +} + +.icon-xl { + width: 32px; + height: 32px; +} + +/* Button icons */ +button .icon, +a .icon { + margin-right: 0.35rem; +} + +button .icon:last-child, +a .icon:last-child { + margin-right: 0; +} + +/* Navigation icons */ +.nav-item .icon { + margin-right: 0.5rem; +} + +/* Status badge icons */ +.status-badge .icon { + margin-right: 0.25rem; +} diff --git a/apps/web/src/utils/icons.ts b/apps/web/src/utils/icons.ts new file mode 100644 index 0000000..6737113 --- /dev/null +++ b/apps/web/src/utils/icons.ts @@ -0,0 +1,163 @@ +import { + House, + Folder, + GitBranch, + Gear, + User, + SignOut, + Plus, + PencilSimple, + Trash, + FloppyDisk, + X, + ArrowsClockwise, + Copy, + MagnifyingGlass, + List, + Check, + Warning, + Info, + Spinner, + GitCommit, + GitMerge, + ClockCounterClockwise, + ArrowDown, + ArrowUp, + File, + FileText, + Image, + Binary, + Code, +} from "@phosphor-icons/react"; + +export type IconName = + | "dashboard" + | "projects" + | "repositories" + | "settings" + | "profile" + | "logout" + | "add" + | "edit" + | "delete" + | "save" + | "cancel" + | "refresh" + | "copy" + | "search" + | "menu" + | "close" + | "success" + | "error" + | "warning" + | "info" + | "loading" + | "branch" + | "commit" + | "merge" + | "history" + | "pull" + | "push" + | "fetch" + | "file" + | "folder" + | "code" + | "document" + | "image" + | "binary"; + +export const iconRegistry: Record< + IconName, + React.ComponentType<{ size?: number | string; weight?: "thin" | "light" | "regular" | "bold" | "fill" | "duotone" }> +> = { + // Navigation + dashboard: House, + projects: Folder, + repositories: GitBranch, + settings: Gear, + profile: User, + logout: SignOut, + + // Actions + add: Plus, + edit: PencilSimple, + delete: Trash, + save: FloppyDisk, + cancel: X, + refresh: ArrowsClockwise, + copy: Copy, + search: MagnifyingGlass, + menu: List, + close: X, + + // Status + success: Check, + error: X, + warning: Warning, + info: Info, + loading: Spinner, + + // Git + branch: GitBranch, + commit: GitCommit, + merge: GitMerge, + history: ClockCounterClockwise, + pull: ArrowDown, + push: ArrowUp, + fetch: ArrowsClockwise, + + // Files + file: File, + folder: Folder, + code: Code, + document: FileText, + image: Image, + binary: Binary, +}; + +export const iconCategories = { + navigation: [ + "dashboard", + "projects", + "repositories", + "settings", + "profile", + "logout", + ] as IconName[], + actions: [ + "add", + "edit", + "delete", + "save", + "cancel", + "refresh", + "copy", + "search", + "menu", + "close", + ] as IconName[], + status: [ + "success", + "error", + "warning", + "info", + "loading", + ] as IconName[], + git: [ + "branch", + "commit", + "merge", + "history", + "pull", + "push", + "fetch", + ] as IconName[], + files: [ + "file", + "folder", + "code", + "document", + "image", + "binary", + ] as IconName[], +}; diff --git a/openspec/changes/documentation-overhaul/.openspec.yaml b/openspec/changes/archive/2026-05-19-documentation-overhaul/.openspec.yaml similarity index 100% rename from openspec/changes/documentation-overhaul/.openspec.yaml rename to openspec/changes/archive/2026-05-19-documentation-overhaul/.openspec.yaml diff --git a/openspec/changes/documentation-overhaul/design.md b/openspec/changes/archive/2026-05-19-documentation-overhaul/design.md similarity index 100% rename from openspec/changes/documentation-overhaul/design.md rename to openspec/changes/archive/2026-05-19-documentation-overhaul/design.md diff --git a/openspec/changes/documentation-overhaul/proposal.md b/openspec/changes/archive/2026-05-19-documentation-overhaul/proposal.md similarity index 100% rename from openspec/changes/documentation-overhaul/proposal.md rename to openspec/changes/archive/2026-05-19-documentation-overhaul/proposal.md diff --git a/openspec/changes/documentation-overhaul/specs/spec.md b/openspec/changes/archive/2026-05-19-documentation-overhaul/specs/spec.md similarity index 100% rename from openspec/changes/documentation-overhaul/specs/spec.md rename to openspec/changes/archive/2026-05-19-documentation-overhaul/specs/spec.md diff --git a/openspec/changes/documentation-overhaul/tasks.md b/openspec/changes/archive/2026-05-19-documentation-overhaul/tasks.md similarity index 100% rename from openspec/changes/documentation-overhaul/tasks.md rename to openspec/changes/archive/2026-05-19-documentation-overhaul/tasks.md diff --git a/openspec/changes/smart-git-url-parsing/.openspec.yaml b/openspec/changes/archive/2026-05-19-smart-git-url-parsing/.openspec.yaml similarity index 100% rename from openspec/changes/smart-git-url-parsing/.openspec.yaml rename to openspec/changes/archive/2026-05-19-smart-git-url-parsing/.openspec.yaml diff --git a/openspec/changes/smart-git-url-parsing/design.md b/openspec/changes/archive/2026-05-19-smart-git-url-parsing/design.md similarity index 100% rename from openspec/changes/smart-git-url-parsing/design.md rename to openspec/changes/archive/2026-05-19-smart-git-url-parsing/design.md diff --git a/openspec/changes/smart-git-url-parsing/proposal.md b/openspec/changes/archive/2026-05-19-smart-git-url-parsing/proposal.md similarity index 100% rename from openspec/changes/smart-git-url-parsing/proposal.md rename to openspec/changes/archive/2026-05-19-smart-git-url-parsing/proposal.md diff --git a/openspec/changes/smart-git-url-parsing/specs/spec.md b/openspec/changes/archive/2026-05-19-smart-git-url-parsing/specs/spec.md similarity index 100% rename from openspec/changes/smart-git-url-parsing/specs/spec.md rename to openspec/changes/archive/2026-05-19-smart-git-url-parsing/specs/spec.md diff --git a/openspec/changes/smart-git-url-parsing/tasks.md b/openspec/changes/archive/2026-05-19-smart-git-url-parsing/tasks.md similarity index 100% rename from openspec/changes/smart-git-url-parsing/tasks.md rename to openspec/changes/archive/2026-05-19-smart-git-url-parsing/tasks.md diff --git a/openspec/changes/universal-icon-system/.openspec/config.yaml b/openspec/changes/universal-icon-system/.openspec/config.yaml new file mode 100644 index 0000000..666f449 --- /dev/null +++ b/openspec/changes/universal-icon-system/.openspec/config.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +name: universal-icon-system diff --git a/openspec/changes/universal-icon-system/design.md b/openspec/changes/universal-icon-system/design.md new file mode 100644 index 0000000..6a0200f --- /dev/null +++ b/openspec/changes/universal-icon-system/design.md @@ -0,0 +1,149 @@ +# Universal Icon System - Design + +## Architecture + +``` +Icon System +├── Icon Component (centralized wrapper) +│ ├── Size variants: sm, md, lg, xl +│ ├── Color: inherited or explicit +│ └── Accessibility: aria-label, role +├── Icon Registry (mapping names to Phosphor icons) +└── Usage throughout app + ├── Navigation icons + ├── Action icons + ├── Status indicators + └── Git operation icons +``` + +## Component Design + +### Icon Component + +**Props:** +```typescript +interface IconProps { + name: IconName; + size?: "sm" | "md" | "lg" | "xl"; + color?: string; + weight?: "thin" | "light" | "regular" | "bold" | "fill" | "duotone"; + className?: string; + ariaLabel?: string; +} +``` + +**Size Mapping:** +- sm: 16px +- md: 20px (default) +- lg: 24px +- xl: 32px + +**Color:** +- Default: inherits from parent via `currentColor` +- Explicit: uses CSS variable or direct color value + +### Icon Registry + +**Categories:** + +Navigation: +- `home` - Dashboard +- `projects` - Projects list +- `repositories` - Git repositories +- `settings` - User settings +- `profile` - User profile + +Actions: +- `add` - Create new +- `edit` - Edit item +- `delete` - Delete item +- `save` - Save changes +- `cancel` - Cancel action +- `refresh` - Refresh/reload +- `copy` - Copy to clipboard + +Status: +- `success` - Checkmark +- `error` - X mark +- `warning` - Warning triangle +- `info` - Information circle +- `loading` - Spinner + +Git Operations: +- `branch` - Git branch +- `commit` - Git commit +- `merge` - Merge branches +- `history` - Commit history +- `pull` - Pull changes +- `push` - Push changes + +Files: +- `file` - Generic file +- `folder` - Directory +- `code` - Code file +- `document` - Text document + +## Migration Plan + +### Phase 1: Setup +1. Install `@phosphor-icons/react` +2. Create `Icon` component +3. Create icon registry mapping + +### Phase 2: Replace Raw Unicode +Replace all instances of raw Unicode symbols: +- `✓` → `Icon name="check"` +- `✗` → `Icon name="x"` +- `⚠` → `Icon name="warning"` +- `●` → `Icon name="dot"` +- `❓` → `Icon name="question"` +- `↓` → `Icon name="arrow-down"` + +### Phase 3: Update Components +Update existing components to use icon system: +- GitToolbar +- GitRepositoriesPage +- FileEditor +- AppShell navigation +- Dialog buttons +- Form validation indicators + +### Phase 4: Styling +- Ensure consistent spacing around icons +- Add hover states where applicable +- Maintain alignment with text + +## Accessibility + +- All icons have meaningful `aria-label` +- Decorative icons use `aria-hidden="true"` +- Focus indicators for interactive icons +- Sufficient color contrast + +## Technical Details + +**Library:** `@phosphor-icons/react` +**Bundle Impact:** Tree-shakeable, ~2KB per icon used +**Browser Support:** All modern browsers +**Fallback:** None needed - SVG-based, always renders + +## CSS Integration + +```css +.icon { + display: inline-flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.icon-sm { width: 16px; height: 16px; } +.icon-md { width: 20px; height: 20px; } +.icon-lg { width: 24px; height: 24px; } +.icon-xl { width: 32px; height: 32px; } + +/* Inherit color from parent */ +.icon svg { + fill: currentColor; +} +``` diff --git a/openspec/changes/universal-icon-system/proposal.md b/openspec/changes/universal-icon-system/proposal.md new file mode 100644 index 0000000..234d5a6 --- /dev/null +++ b/openspec/changes/universal-icon-system/proposal.md @@ -0,0 +1,58 @@ +# Universal Icon System + +## Problem + +The current project uses inconsistent icon implementations across the frontend: +- Raw Unicode symbols (✓, ✗, ⚠, etc.) which render differently across browsers and operating systems +- No standardized icon component or library +- Inconsistent visual language throughout the UI +- Some icons may not render at all on certain systems + +This creates a fragmented user experience and potential accessibility issues. + +## Solution + +Implement a universal icon system using **Phosphor Icons** - a comprehensive, lightweight icon library designed for modern web applications: + +1. **Consistent rendering** across all browsers and platforms +2. **Comprehensive icon set** with 7000+ icons covering all use cases +3. **Multiple weights** (thin, light, regular, bold, fill, duotone) for flexibility +4. **Tree-shakeable** - only includes icons that are actually used +5. **React integration** with phosphor-react library +6. **Accessible** with proper ARIA labels and focus management + +## Key Features + +### Icon Component +- Centralized `` component wrapping Phosphor icons +- Consistent sizing (sm, md, lg, xl) +- Color inheritance from parent or explicit color prop +- Accessibility support (aria-label, role) + +### Icon Categories +- Navigation (home, settings, user, etc.) +- Actions (edit, delete, save, add, etc.) +- Status (success, error, warning, info) +- Files and folders +- Git operations (branch, commit, merge, etc.) +- Development tools (terminal, code, database, etc.) + +### Migration Strategy +- Replace all raw Unicode symbols with proper icon components +- Update existing components to use the new icon system +- Maintain visual consistency during migration + +## Benefits + +- **Cross-browser consistency** - Icons render identically everywhere +- **Better accessibility** - Screen reader friendly with proper labels +- **Improved maintainability** - Single source of truth for icons +- **Enhanced UX** - Professional, polished appearance +- **Future-proof** - Easy to add new icons as needed + +## Success Criteria + +- [ ] All raw Unicode symbols replaced with icon components +- [ ] Consistent icon sizing and styling across all pages +- [ ] No visual regressions in existing UI +- [ ] Icons render correctly in all supported browsers diff --git a/openspec/changes/universal-icon-system/specs/spec.md b/openspec/changes/universal-icon-system/specs/spec.md new file mode 100644 index 0000000..293d416 --- /dev/null +++ b/openspec/changes/universal-icon-system/specs/spec.md @@ -0,0 +1,111 @@ +# Universal Icon System Specification + +## Requirements + +### Functional Requirements + +1. **Icon Component**: Centralized `Icon` component that wraps Phosphor icons +2. **Icon Registry**: Mapping of logical names to Phosphor icon components +3. **Size Variants**: Support for sm (16px), md (20px), lg (24px), xl (32px) +4. **Color Inheritance**: Default to `currentColor`, support explicit colors +5. **Accessibility**: Proper ARIA labels and roles +6. **Tree Shaking**: Only include icons that are actually used + +### Non-Functional Requirements + +1. **Bundle Size**: Minimal impact (~2KB per icon weight variant) +2. **Performance**: No layout shift, instant rendering +3. **Browser Support**: All modern browsers (Chrome, Firefox, Safari, Edge) +4. **Consistency**: Identical rendering across all platforms + +## Icon Registry + +### Navigation Icons +- `dashboard` → `House` +- `projects` → `Folder` +- `repositories` → `GitBranch` +- `settings` → `Gear` +- `profile` → `User` +- `logout` → `SignOut` + +### Action Icons +- `add` → `Plus` +- `edit` → `PencilSimple` +- `delete` → `Trash` +- `save` → `FloppyDisk` +- `cancel` → `X` +- `refresh` → `ArrowsClockwise` +- `copy` → `Copy` +- `search` → `MagnifyingGlass` +- `menu` → `List` +- `close` → `X` + +### Status Icons +- `success` → `Check` +- `error` → `X` +- `warning` → `Warning` +- `info` → `Info` +- `loading` → `Spinner` + +### Git Icons +- `branch` → `GitBranch` +- `commit` → `GitCommit` +- `merge` → `GitMerge` +- `history` → `ClockCounterClockwise` +- `pull` → `ArrowDown` +- `push` → `ArrowUp` +- `fetch` → `ArrowsClockwise` + +### File Icons +- `file` → `File` +- `folder` → `Folder` +- `code` → `Code` +- `document` → `FileText` +- `image` → `Image` +- `binary` → `FileBinary` + +## Migration Checklist + +### Components to Update +- [ ] `app-shell.tsx` - Navigation icons +- [ ] `git-toolbar.tsx` - Git operation icons +- [ ] `git-repositories.tsx` - Status/validation icons +- [ ] `repo-workspace.tsx` - File tree icons +- [ ] `file-editor.tsx` - File type icons +- [ ] `dashboard.tsx` - Dashboard icons +- [ ] `projects.tsx` - Project action icons +- [ ] `settings.tsx` - Settings icons +- [ ] `tool-types.tsx` - Tool type icons +- [ ] `ssh-keys.tsx` - Key management icons +- [ ] `profile.tsx` - Profile icons +- [ ] `commit-dialog.tsx` - Dialog icons +- [ ] `syntax-highlighter.tsx` - Copy icon +- [ ] `code-editor.tsx` - Edit icons +- [ ] All button components with icons + +## CSS Requirements + +```css +.icon { + display: inline-flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.icon-sm { width: 16px; height: 16px; } +.icon-md { width: 20px; height: 20px; } +.icon-lg { width: 24px; height: 24px; } +.icon-xl { width: 32px; height: 32px; } +``` + +## Accessibility Requirements + +1. All functional icons must have `aria-label` +2. Decorative icons must have `aria-hidden="true"` +3. Interactive icons must be focusable +4. Color contrast ratio ≥ 4.5:1 + +## Dependencies + +- `@phosphor-icons/react` - React component library diff --git a/openspec/changes/universal-icon-system/tasks.md b/openspec/changes/universal-icon-system/tasks.md new file mode 100644 index 0000000..6365a06 --- /dev/null +++ b/openspec/changes/universal-icon-system/tasks.md @@ -0,0 +1,121 @@ +# Universal Icon System - Tasks + +## Phase 1: Setup + +- [x] **Task 1.1**: Install Phosphor Icons + - `npm install @phosphor-icons/react` + - Add to package.json dependencies + +- [x] **Task 1.2**: Create Icon component + - Create `components/icon.tsx` + - Implement size variants (sm, md, lg, xl) + - Support color inheritance and explicit colors + - Add accessibility props (aria-label, aria-hidden) + +- [x] **Task 1.3**: Create icon registry + - Create `utils/icons.ts` + - Map logical names to Phosphor icon components + - Group by category (navigation, actions, status, git, files) + - Export TypeScript types for icon names + +## Phase 2: Replace Raw Unicode Symbols + +- [x] **Task 2.1**: Replace validation icons in git-repositories.tsx + - `✓` → `Icon name="check"` + - `⚠` → `Icon name="warning"` + - `✗` → `Icon name="x"` + +- [x] **Task 2.2**: Replace git toolbar icons in git-toolbar.tsx + - `●` → `Icon name="dot"` + - `↓` → `Icon name="arrow-down"` + - `❓` → `Icon name="question"` + +- [x] **Task 2.3**: Scan and replace all other Unicode symbols + - Search for remaining Unicode symbols across all TSX files + - Replace with appropriate Icon components + +## Phase 3: Update Navigation + +- [x] **Task 3.1**: Update app-shell.tsx navigation + - Replace text-only nav items with icon + text + - Use navigation icons (dashboard, projects, settings, etc.) + - Maintain current layout and styling + +## Phase 4: Update Action Buttons + +- [ ] **Task 4.1**: Update button components + - Add icon support to Button component + - Update all primary/secondary buttons with relevant icons + - Ensure proper spacing between icon and text + +- [ ] **Task 4.2**: Update form actions + - Save buttons: `Icon name="save"` + - Cancel buttons: `Icon name="x"` + - Delete buttons: `Icon name="trash"` + - Edit buttons: `Icon name="pencil"` + +## Phase 5: Update Status Indicators + +- [ ] **Task 5.1**: Replace status badges + - Success states: `Icon name="check"` + green color + - Error states: `Icon name="x"` + red color + - Warning states: `Icon name="warning"` + yellow color + - Loading states: `Icon name="spinner"` + animation + +## Phase 6: Update Git Components + +- [ ] **Task 6.1**: Update GitToolbar + - Branch icon: `Icon name="git-branch"` + - Fetch icon: `Icon name="arrows-clockwise"` + - Pull icon: `Icon name="arrow-down"` + - Push icon: `Icon name="arrow-up"` + - Commit icon: `Icon name="git-commit"` + +- [ ] **Task 6.2**: Update GitHistoryPage + - History icon: `Icon name="clock-counter-clockwise"` + - Merge icon: `Icon name="git-merge"` + - Branch selector icon: `Icon name="git-branch"` + +## Phase 7: Update File Components + +- [ ] **Task 7.1**: Update file tree icons + - Folder icon: `Icon name="folder"` + - File icon: `Icon name="file"` + - Code file icon: `Icon name="code"` + - Binary file icon: `Icon name="file-binary"` + +- [ ] **Task 7.2**: Update FileEditor toolbar + - Edit icon: `Icon name="pencil"` + - Save icon: `Icon name="floppy-disk"` + - Copy icon: `Icon name="copy"` + +## Phase 8: CSS and Styling + +- [ ] **Task 8.1**: Add icon CSS classes + - Create `.icon` base class + - Size variants: `.icon-sm`, `.icon-md`, `.icon-lg`, `.icon-xl` + - Alignment utilities for icon + text combos + +- [ ] **Task 8.2**: Ensure consistent spacing + - Icon margins in buttons + - Icon alignment with text + - Icon padding in navigation items + +## Phase 9: Quality Gates + +- [ ] **Task 9.1**: TypeScript checks + - `npm run typecheck` + - Fix any type errors + +- [ ] **Task 9.2**: Lint checks + - `npm run lint` + - Fix any linting issues + +- [ ] **Task 9.3**: Build verification + - `npm run build` + - Verify bundle size impact + +- [ ] **Task 9.4**: Visual verification + - Check all pages for icon rendering + - Verify no missing icons or broken layouts + - Check dark/light theme compatibility