feat: implement universal icon system with Phosphor Icons
- 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)
This commit is contained in:
@@ -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"
|
||||
>
|
||||
<Icon name="logout" size="sm" />
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
@@ -46,6 +49,7 @@ export const AppShell = () => {
|
||||
className={({ isActive }) => (isActive ? "nav-item nav-item-active" : "nav-item")}
|
||||
end={item.to === "/"}
|
||||
>
|
||||
<Icon name={item.icon} size="sm" />
|
||||
{item.label}
|
||||
</NavLink>
|
||||
))}
|
||||
|
||||
@@ -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<CommitDialogProps> = ({
|
||||
onClick={onCancel}
|
||||
type="button"
|
||||
>
|
||||
<Icon name="cancel" size="sm" />
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
@@ -137,7 +140,17 @@ export const CommitDialog: React.FC<CommitDialogProps> = ({
|
||||
disabled={loading || !hasChanges || !message.trim()}
|
||||
type="button"
|
||||
>
|
||||
{loading ? "Committing..." : "Commit Changes"}
|
||||
{loading ? (
|
||||
<>
|
||||
<Icon name="loading" size="sm" />
|
||||
Committing...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Icon name="commit" size="sm" />
|
||||
Commit Changes
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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<FileEditorProps> = ({
|
||||
onClick={handleEdit}
|
||||
type="button"
|
||||
>
|
||||
<Icon name="edit" size="sm" />
|
||||
Edit
|
||||
</button>
|
||||
)}
|
||||
@@ -183,13 +185,24 @@ export const FileEditor: React.FC<FileEditorProps> = ({
|
||||
disabled={content === originalContent || saving}
|
||||
type="button"
|
||||
>
|
||||
{saving ? "Saving..." : "Save"}
|
||||
{saving ? (
|
||||
<>
|
||||
<Icon name="loading" size="sm" />
|
||||
Saving...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Icon name="save" size="sm" />
|
||||
Save
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
className="btn-secondary"
|
||||
onClick={handleCancel}
|
||||
type="button"
|
||||
>
|
||||
<Icon name="cancel" size="sm" />
|
||||
Cancel
|
||||
</button>
|
||||
</>
|
||||
|
||||
@@ -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) => (
|
||||
<option key={b} value={b}>
|
||||
{b === currentBranch ? `● ${b}` : b}
|
||||
{b === currentBranch ? (
|
||||
<>
|
||||
<Icon name="branch" size="sm" /> {b}
|
||||
</>
|
||||
) : (
|
||||
b
|
||||
)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
@@ -150,18 +157,18 @@ export const GitToolbar = ({
|
||||
disabled={loading}
|
||||
type="button"
|
||||
>
|
||||
+ New
|
||||
<Icon name="add" size="sm" /> New
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="toolbar-group">
|
||||
<button
|
||||
<button
|
||||
className="toolbar-button"
|
||||
onClick={handleFetch}
|
||||
disabled={loading}
|
||||
type="button"
|
||||
>
|
||||
↓ Fetch
|
||||
<Icon name="fetch" size="sm" /> Fetch
|
||||
</button>
|
||||
<button
|
||||
className="toolbar-button"
|
||||
@@ -169,7 +176,7 @@ export const GitToolbar = ({
|
||||
disabled={loading}
|
||||
type="button"
|
||||
>
|
||||
⇣ Pull
|
||||
<Icon name="pull" size="sm" /> Pull
|
||||
{status?.behind ? <span className="badge">{status.behind}</span> : null}
|
||||
</button>
|
||||
<button
|
||||
@@ -178,7 +185,7 @@ export const GitToolbar = ({
|
||||
disabled={loading || !status?.ahead}
|
||||
type="button"
|
||||
>
|
||||
⇡ Push
|
||||
<Icon name="push" size="sm" /> Push
|
||||
{status?.ahead ? <span className="badge">{status.ahead}</span> : null}
|
||||
</button>
|
||||
<button
|
||||
@@ -187,7 +194,7 @@ export const GitToolbar = ({
|
||||
disabled={loading}
|
||||
type="button"
|
||||
>
|
||||
🔀 Merge
|
||||
<Icon name="merge" size="sm" /> Merge
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -217,24 +224,24 @@ export const GitToolbar = ({
|
||||
disabled={loading || !newBranchName.trim()}
|
||||
type="button"
|
||||
>
|
||||
Create
|
||||
<Icon name="add" size="sm" /> Create
|
||||
</button>
|
||||
<button
|
||||
className="toolbar-button"
|
||||
onClick={() => setShowNewBranch(false)}
|
||||
type="button"
|
||||
>
|
||||
Cancel
|
||||
<Icon name="cancel" size="sm" /> Cancel
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hasChanges && status && (
|
||||
<div className="toolbar-row status-summary">
|
||||
{status.modified.length > 0 && <span className="status-badge modified">✏️ {status.modified.length} modified</span>}
|
||||
{status.added.length > 0 && <span className="status-badge added">✨ {status.added.length} added</span>}
|
||||
{status.deleted.length > 0 && <span className="status-badge deleted">🗑️ {status.deleted.length} deleted</span>}
|
||||
{status.untracked.length > 0 && <span className="status-badge untracked">❓ {status.untracked.length} untracked</span>}
|
||||
{status.modified.length > 0 && <span className="status-badge modified"><Icon name="edit" size="sm" /> {status.modified.length} modified</span>}
|
||||
{status.added.length > 0 && <span className="status-badge added"><Icon name="add" size="sm" /> {status.added.length} added</span>}
|
||||
{status.deleted.length > 0 && <span className="status-badge deleted"><Icon name="delete" size="sm" /> {status.deleted.length} deleted</span>}
|
||||
{status.untracked.length > 0 && <span className="status-badge untracked"><Icon name="warning" size="sm" /> {status.untracked.length} untracked</span>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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<IconName, React.ComponentType<{ size?: number | string; weight?: "thin" | "light" | "regular" | "bold" | "fill" | "duotone" }>> = {
|
||||
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<NonNullable<IconProps["size"]>, number> = {
|
||||
sm: 16,
|
||||
md: 20,
|
||||
lg: 24,
|
||||
xl: 32,
|
||||
};
|
||||
|
||||
export const Icon: React.FC<IconProps> = ({
|
||||
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 (
|
||||
<span
|
||||
className={`icon icon-${size}${className ? ` ${className}` : ""}`}
|
||||
style={{ color }}
|
||||
aria-label={ariaLabel}
|
||||
aria-hidden={!ariaLabel}
|
||||
role="img"
|
||||
>
|
||||
<IconComponent size={sizeValue} weight={weight} />
|
||||
</span>
|
||||
);
|
||||
};
|
||||
@@ -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"
|
||||
>
|
||||
<Icon name="cancel" size="sm" />
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
@@ -122,7 +124,17 @@ export const MergeDialog = ({
|
||||
disabled={loading || !sourceBranch}
|
||||
type="button"
|
||||
>
|
||||
{loading ? "Merging..." : "Merge"}
|
||||
{loading ? (
|
||||
<>
|
||||
<Icon name="loading" size="sm" />
|
||||
Merging...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Icon name="merge" size="sm" />
|
||||
Merge
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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<SyntaxHighlighterProps> = ({
|
||||
onClick={handleCopy}
|
||||
type="button"
|
||||
>
|
||||
{copied ? "Copied!" : "Copy"}
|
||||
{copied ? (
|
||||
<>
|
||||
<Icon name="success" size="sm" />
|
||||
Copied!
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Icon name="copy" size="sm" />
|
||||
Copy
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<div className="code-container">
|
||||
|
||||
@@ -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 (
|
||||
<div className="workspace-header">
|
||||
<div className="workspace-header-left">
|
||||
<div className="workspace-header-icon">📁</div>
|
||||
<div className="workspace-header-icon">
|
||||
<Icon name="folder" size="lg" />
|
||||
</div>
|
||||
<div className="workspace-header-info">
|
||||
<h1 className="workspace-header-title">{project.name}</h1>
|
||||
{currentRepo && (
|
||||
@@ -29,14 +32,14 @@ export const WorkspaceHeader = ({ project, currentRepo }: WorkspaceHeaderProps)
|
||||
className="workspace-header-action-btn"
|
||||
to={`/projects/${project.id}/repositories/${currentRepo?.id || ""}/history`}
|
||||
>
|
||||
<span className="icon">🕐</span>
|
||||
<Icon name="history" size="sm" />
|
||||
History
|
||||
</Link>
|
||||
<Link
|
||||
className="workspace-header-action-btn"
|
||||
to={`/projects/${project.id}/settings`}
|
||||
>
|
||||
<span className="icon">⚙️</span>
|
||||
<Icon name="settings" size="sm" />
|
||||
Settings
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user