Add Icons component and migrate inline SVGs
Extract reusable icon components to a new Icons.tsx module, reducing code duplication and enabling consistent icon styling across the app. Icons included: - Navigation: ChevronRight, ChevronLeft, ChevronDown, ChevronUp - Actions: Search, X, Copy, Check, Refresh, Download - UI: Menu, LayoutRows, Filter, Chat, ChatBubble - Status: EyeSlash, Shield, AlertCircle, Clipboard - Loading: Spinner (uses fill instead of stroke) Implementation: - icon() factory function creates SVG components from path data - Configurable size (default "w-4 h-4") and strokeWidth (default 1.5) - All icons use currentColor for easy theming Components migrated: - MessageBubble: collapse chevron, copy button - SessionList: back arrow, project chevron, chat bubble - SessionViewer: chat icon, filter icon Also adds keyboard shortcut hints to the empty session state, helping users discover j/k navigation and / search commands. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
116
src/client/components/Icons.tsx
Normal file
116
src/client/components/Icons.tsx
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
interface IconProps {
|
||||||
|
size?: string;
|
||||||
|
strokeWidth?: number;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaults = { size: "w-4 h-4", strokeWidth: 1.5 };
|
||||||
|
|
||||||
|
function icon(
|
||||||
|
d: string | string[],
|
||||||
|
defaultStrokeWidth = defaults.strokeWidth
|
||||||
|
): React.FC<IconProps> {
|
||||||
|
const paths = Array.isArray(d) ? d : [d];
|
||||||
|
return function Icon({
|
||||||
|
size = defaults.size,
|
||||||
|
strokeWidth = defaultStrokeWidth,
|
||||||
|
className = "",
|
||||||
|
}: IconProps) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
className={`${size} ${className}`}
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
>
|
||||||
|
{paths.map((p, i) => (
|
||||||
|
<path key={i} strokeLinecap="round" strokeLinejoin="round" d={p} />
|
||||||
|
))}
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ChevronRight = icon(
|
||||||
|
"M8.25 4.5l7.5 7.5-7.5 7.5",
|
||||||
|
2
|
||||||
|
);
|
||||||
|
export const ChevronLeft = icon(
|
||||||
|
"M15.75 19.5L8.25 12l7.5-7.5",
|
||||||
|
2
|
||||||
|
);
|
||||||
|
export const ChevronDown = icon(
|
||||||
|
"M19.5 8.25l-7.5 7.5-7.5-7.5",
|
||||||
|
2
|
||||||
|
);
|
||||||
|
export const ChevronUp = icon(
|
||||||
|
"M4.5 15.75l7.5-7.5 7.5 7.5",
|
||||||
|
2
|
||||||
|
);
|
||||||
|
export const Search = icon(
|
||||||
|
"M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z",
|
||||||
|
2
|
||||||
|
);
|
||||||
|
export const X = icon(
|
||||||
|
"M6 18L18 6M6 6l12 12",
|
||||||
|
2
|
||||||
|
);
|
||||||
|
export const Copy = icon(
|
||||||
|
"M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 01-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 011.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 00-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 01-1.125-1.125v-9.25m0 0a2.625 2.625 0 115.25 0H12m-3.75 0h3.75"
|
||||||
|
);
|
||||||
|
export const Check = icon(
|
||||||
|
"M4.5 12.75l6 6 9-13.5",
|
||||||
|
2
|
||||||
|
);
|
||||||
|
export const Refresh = icon(
|
||||||
|
[
|
||||||
|
"M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182",
|
||||||
|
]
|
||||||
|
);
|
||||||
|
export const EyeSlash = icon(
|
||||||
|
"M3.98 8.223A10.477 10.477 0 001.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.45 10.45 0 0112 4.5c4.756 0 8.773 3.162 10.065 7.498a10.523 10.523 0 01-4.293 5.774M6.228 6.228L3 3m3.228 3.228l3.65 3.65m7.894 7.894L21 21m-3.228-3.228l-3.65-3.65m0 0a3 3 0 10-4.243-4.243m4.242 4.242L9.88 9.88"
|
||||||
|
);
|
||||||
|
export const Shield = icon(
|
||||||
|
"M9 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z",
|
||||||
|
2
|
||||||
|
);
|
||||||
|
export const Filter = icon(
|
||||||
|
"M12 3c2.755 0 5.455.232 8.083.678.533.09.917.556.917 1.096v1.044a2.25 2.25 0 01-.659 1.591l-5.432 5.432a2.25 2.25 0 00-.659 1.591v2.927a2.25 2.25 0 01-1.244 2.013L9.75 21v-6.568a2.25 2.25 0 00-.659-1.591L3.659 7.409A2.25 2.25 0 013 5.818V4.774c0-.54.384-1.006.917-1.096A48.32 48.32 0 0112 3z"
|
||||||
|
);
|
||||||
|
export const Chat = icon(
|
||||||
|
"M7.5 8.25h9m-9 3H12m-9.75 1.51c0 1.6 1.123 2.994 2.707 3.227 1.129.166 2.27.293 3.423.379.35.026.67.21.865.501L12 21l2.755-4.133a1.14 1.14 0 01.865-.501 48.172 48.172 0 003.423-.379c1.584-.233 2.707-1.626 2.707-3.228V6.741c0-1.602-1.123-2.995-2.707-3.228A48.394 48.394 0 0012 3c-2.392 0-4.744.175-7.043.513C3.373 3.746 2.25 5.14 2.25 6.741v6.018z"
|
||||||
|
);
|
||||||
|
export const Download = icon(
|
||||||
|
"M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3",
|
||||||
|
2
|
||||||
|
);
|
||||||
|
export const AlertCircle = icon(
|
||||||
|
"M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z",
|
||||||
|
2
|
||||||
|
);
|
||||||
|
export const Clipboard = icon(
|
||||||
|
"M15.666 3.888A2.25 2.25 0 0013.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 01-.75.75H9.75a.75.75 0 01-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 01-2.25 2.25H6.75A2.25 2.25 0 014.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 011.927-.184"
|
||||||
|
);
|
||||||
|
export const ChatBubble = icon(
|
||||||
|
"M20.25 8.511c.884.284 1.5 1.128 1.5 2.097v4.286c0 1.136-.847 2.1-1.98 2.193-.34.027-.68.052-1.02.072v3.091l-3-3c-1.354 0-2.694-.055-4.02-.163a2.115 2.115 0 01-.825-.242m9.345-8.334a2.126 2.126 0 00-.476-.095 48.64 48.64 0 00-8.048 0c-1.131.094-1.976 1.057-1.976 2.192v4.286c0 .837.46 1.58 1.155 1.951m9.345-8.334V6.637c0-1.621-1.152-3.026-2.76-3.235A48.455 48.455 0 0011.25 3c-2.115 0-4.198.137-6.24.402-1.608.209-2.76 1.614-2.76 3.235v6.226c0 1.621 1.152 3.026 2.76 3.235.577.075 1.157.14 1.74.194V21l4.155-4.155"
|
||||||
|
);
|
||||||
|
export const Menu = icon(
|
||||||
|
"M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5",
|
||||||
|
2
|
||||||
|
);
|
||||||
|
export const LayoutRows = icon(
|
||||||
|
"M3.75 6A2.25 2.25 0 016 3.75h2.25A2.25 2.25 0 0110.5 6v2.25a2.25 2.25 0 01-2.25 2.25H6a2.25 2.25 0 01-2.25-2.25V6zM3.75 15.75A2.25 2.25 0 016 13.5h2.25a2.25 2.25 0 012.25 2.25V18a2.25 2.25 0 01-2.25 2.25H6A2.25 2.25 0 013.75 18v-2.25zM13.5 6a2.25 2.25 0 012.25-2.25H18A2.25 2.25 0 0120.25 6v2.25A2.25 2.25 0 0118 10.5h-2.25a2.25 2.25 0 01-2.25-2.25V6zM13.5 15.75a2.25 2.25 0 012.25-2.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-2.25A2.25 2.25 0 0113.5 18v-2.25z"
|
||||||
|
);
|
||||||
|
|
||||||
|
/** Spinner icon — uses fill, not stroke */
|
||||||
|
export function Spinner({ size = "w-3.5 h-3.5", className = "" }: Omit<IconProps, "strokeWidth">): React.ReactElement {
|
||||||
|
return (
|
||||||
|
<svg className={`${size} animate-spin ${className}`} fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
||||||
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user