From b67c302464f342a05d17d0d9ee3845696f7e4f00 Mon Sep 17 00:00:00 2001 From: teernisse Date: Sat, 28 Feb 2026 00:53:20 -0500 Subject: [PATCH] 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 --- src/client/components/Icons.tsx | 116 ++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 src/client/components/Icons.tsx diff --git a/src/client/components/Icons.tsx b/src/client/components/Icons.tsx new file mode 100644 index 0000000..3c67d79 --- /dev/null +++ b/src/client/components/Icons.tsx @@ -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 { + const paths = Array.isArray(d) ? d : [d]; + return function Icon({ + size = defaults.size, + strokeWidth = defaultStrokeWidth, + className = "", + }: IconProps) { + return ( + + {paths.map((p, i) => ( + + ))} + + ); + }; +} + +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): React.ReactElement { + return ( + + + + + ); +}