UI: refresh design system with new color palette and icons (#1745)

- Replace orange accent (#f59f4a) with signature red (#ff4d4d)
- Switch from IBM Plex/Unbounded/Work Sans to Inter/JetBrains Mono
- Replace emoji icons with Lucide-style SVG icons throughout
- Add comprehensive CSS design tokens (colors, borders, semantic states)
- Update tool-display.json to use icon names instead of emoji
- Rebuild control-ui dist bundle
This commit is contained in:
Nicolas Zullo
2026-01-25 11:04:50 +01:00
committed by GitHub
parent 0f662c2935
commit 9fbee08590
29 changed files with 4834 additions and 7152 deletions

View File

@@ -1,60 +1,172 @@
@import url("https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=Unbounded:wght@400;500;600&family=Work+Sans:wght@400;500;600;700&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap");
:root {
--bg: #0a0f14;
--bg-accent: #111826;
--bg-grad-1: #162031;
--bg-grad-2: #1f2a22;
--bg-overlay: rgba(255, 255, 255, 0.05);
--bg-glow: rgba(245, 159, 74, 0.12);
--panel: rgba(14, 20, 30, 0.88);
--panel-strong: rgba(18, 26, 38, 0.96);
--chrome: rgba(9, 14, 20, 0.72);
--chrome-strong: rgba(9, 14, 20, 0.86);
--text: rgba(244, 246, 251, 0.96);
--chat-text: rgba(231, 237, 244, 0.92);
--muted: rgba(156, 169, 189, 0.72);
--border: rgba(255, 255, 255, 0.09);
--border-strong: rgba(255, 255, 255, 0.16);
--accent: #f59f4a;
--accent-2: #34c7b7;
--ok: #2bd97f;
--warn: #f2c94c;
--danger: #ff6b6b;
--focus: rgba(245, 159, 74, 0.35);
--grid-line: rgba(255, 255, 255, 0.04);
/* Background - Deep Navy Slate */
--bg: #0c0d12;
--bg-accent: #0d0e14;
--bg-elevated: #181a21;
--bg-hover: #252830;
--bg-muted: #252830;
/* Card / Surface */
--card: #13151c;
--card-foreground: #f8fafc;
--card-highlight: rgba(255, 255, 255, 0.04);
--popover: #13151c;
--popover-foreground: #f8fafc;
/* Panel */
--panel: #0c0d12;
--panel-strong: #181a21;
--panel-hover: #252830;
--chrome: rgba(12, 13, 18, 0.95);
--chrome-strong: rgba(12, 13, 18, 0.98);
/* Text */
--text: #f8fafc;
--text-strong: #ffffff;
--chat-text: #f8fafc;
--muted: #94a3b8;
--muted-strong: #64748b;
--muted-foreground: #94a3b8;
/* Border */
--border: #333842;
--border-strong: #454d5c;
--border-hover: #5a6373;
--input: #333842;
--ring: #ff4d4d;
/* Accent - The signature red */
--accent: #ff4d4d;
--accent-hover: #ff6666;
--accent-muted: #ff4d4d;
--accent-subtle: rgba(255, 77, 77, 0.12);
--accent-foreground: #f8fafc;
--primary: #ff4d4d;
--primary-foreground: #ffffff;
/* Secondary */
--secondary: #252830;
--secondary-foreground: #f8fafc;
--accent-2: #3b82f6;
--accent-2-muted: rgba(59, 130, 246, 0.7);
/* Semantic */
--ok: #22c55e;
--ok-muted: rgba(34, 197, 94, 0.7);
--ok-subtle: rgba(34, 197, 94, 0.1);
--destructive: #ef4444;
--destructive-foreground: #fafafa;
--warn: #eab308;
--warn-muted: rgba(234, 179, 8, 0.7);
--warn-subtle: rgba(234, 179, 8, 0.1);
--danger: #ef4444;
--danger-muted: rgba(239, 68, 68, 0.7);
--danger-subtle: rgba(239, 68, 68, 0.1);
--info: #3b82f6;
/* Focus */
--focus: rgba(255, 77, 77, 0.2);
--focus-ring: 0 0 0 2px var(--bg), 0 0 0 4px var(--ring);
/* Grid */
--grid-line: rgba(255, 255, 255, 0.03);
/* Theme transition */
--theme-switch-x: 50%;
--theme-switch-y: 50%;
--mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
"Liberation Mono", "Courier New", monospace;
--font-body: "Work Sans", system-ui, sans-serif;
--font-display: "Unbounded", "Times New Roman", serif;
/* Typography */
--mono: "JetBrains Mono", ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, Consolas, monospace;
--font-body: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
--font-display: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
/* Shadows - minimal */
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1);
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
/* Radii - shadcn uses smaller radii */
--radius-sm: 4px;
--radius-md: 6px;
--radius-lg: 8px;
--radius-xl: 12px;
--radius-full: 9999px;
--radius: 6px;
/* Transitions */
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
--duration-fast: 150ms;
--duration-normal: 200ms;
--duration-slow: 300ms;
color-scheme: dark;
}
/* Light theme */
:root[data-theme="light"] {
--bg: #f5f1ea;
--bg-accent: #ffffff;
--bg-grad-1: #f1e6d6;
--bg-grad-2: #e5eef4;
--bg-overlay: rgba(28, 32, 46, 0.05);
--bg-glow: rgba(52, 199, 183, 0.14);
--panel: rgba(255, 255, 255, 0.9);
--panel-strong: rgba(255, 255, 255, 0.97);
--chrome: rgba(255, 255, 255, 0.75);
--chrome-strong: rgba(255, 255, 255, 0.88);
--text: rgba(27, 36, 50, 0.98);
--chat-text: rgba(36, 48, 66, 0.9);
--muted: rgba(80, 94, 114, 0.7);
--border: rgba(18, 24, 40, 0.12);
--border-strong: rgba(18, 24, 40, 0.2);
--accent: #e28a3f;
--accent-2: #1ba99d;
--ok: #1aa86c;
--warn: #b3771c;
--danger: #d44848;
--focus: rgba(226, 138, 63, 0.35);
--grid-line: rgba(18, 24, 40, 0.06);
--bg: #f8f8f7;
--bg-accent: #f3f2f0;
--bg-elevated: #ffffff;
--bg-hover: #eae8e6;
--bg-muted: #eae8e6;
--bg-content: #f0efed;
--card: #ffffff;
--card-foreground: #1c1917;
--card-highlight: rgba(0, 0, 0, 0.04);
--popover: #ffffff;
--popover-foreground: #1c1917;
--panel: #f8f8f7;
--panel-strong: #f0efed;
--panel-hover: #e5e3e1;
--chrome: rgba(248, 248, 247, 0.95);
--chrome-strong: rgba(248, 248, 247, 0.98);
--text: #44403c;
--text-strong: #292524;
--chat-text: #44403c;
--muted: #5c5856;
--muted-strong: #44403c;
--muted-foreground: #5c5856;
--border: #e0dedc;
--border-strong: #d6d3d1;
--border-hover: #a8a5a0;
--input: #e0dedc;
--accent: #b91c1c;
--accent-hover: #dc2626;
--accent-muted: #b91c1c;
--accent-subtle: rgba(185, 28, 28, 0.18);
--accent-foreground: #ffffff;
--primary: #b91c1c;
--primary-foreground: #ffffff;
--secondary: #eae8e6;
--secondary-foreground: #44403c;
--ok: #15803d;
--ok-muted: rgba(21, 128, 61, 0.75);
--ok-subtle: rgba(21, 128, 61, 0.12);
--destructive: #b91c1c;
--destructive-foreground: #fafafa;
--warn: #a16207;
--warn-muted: rgba(161, 98, 7, 0.75);
--warn-subtle: rgba(161, 98, 7, 0.12);
--danger: #b91c1c;
--danger-muted: rgba(185, 28, 28, 0.75);
--danger-subtle: rgba(185, 28, 28, 0.12);
--info: #1d4ed8;
--focus: rgba(185, 28, 28, 0.25);
--grid-line: rgba(0, 0, 0, 0.06);
color-scheme: light;
}
@@ -69,44 +181,21 @@ body {
body {
margin: 0;
font: 15px/1.5 var(--font-body);
background:
radial-gradient(1200px 900px at 15% -10%, var(--bg-grad-1) 0%, transparent 55%)
fixed,
radial-gradient(900px 700px at 80% 10%, var(--bg-grad-2) 0%, transparent 60%)
fixed,
linear-gradient(160deg, var(--bg) 0%, var(--bg-accent) 100%) fixed;
font: 400 14px/1.5 var(--font-body);
letter-spacing: -0.011em;
background: var(--bg);
color: var(--text);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body::before {
content: "";
position: fixed;
inset: 0;
background:
linear-gradient(
140deg,
var(--bg-overlay) 0%,
rgba(255, 255, 255, 0) 40%
),
radial-gradient(620px 420px at 75% 75%, var(--bg-glow), transparent 60%);
pointer-events: none;
z-index: 0;
}
/* Grid overlay removed for cleaner look */
/* Theme transition */
@keyframes theme-circle-transition {
0% {
clip-path: circle(
0% at var(--theme-switch-x, 50%) var(--theme-switch-y, 50%)
);
clip-path: circle(0% at var(--theme-switch-x, 50%) var(--theme-switch-y, 50%));
}
100% {
clip-path: circle(
150% at var(--theme-switch-x, 50%) var(--theme-switch-y, 50%)
);
clip-path: circle(150% at var(--theme-switch-x, 50%) var(--theme-switch-y, 50%));
}
}
@@ -123,7 +212,7 @@ html.theme-transition::view-transition-old(theme) {
html.theme-transition::view-transition-new(theme) {
mix-blend-mode: normal;
z-index: 2;
animation: theme-circle-transition 0.45s ease-out forwards;
animation: theme-circle-transition 0.4s var(--ease-out) forwards;
}
@media (prefers-reduced-motion: reduce) {
@@ -141,7 +230,12 @@ clawdbot-app {
}
a {
color: inherit;
color: var(--accent);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
button,
@@ -152,10 +246,35 @@ select {
color: inherit;
}
::selection {
background: var(--accent-subtle);
color: var(--text-strong);
}
/* Scrollbar styling */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: var(--radius-full);
}
::-webkit-scrollbar-thumb:hover {
background: var(--border-strong);
}
/* Animations */
@keyframes rise {
from {
opacity: 0;
transform: translateY(6px);
transform: translateY(4px);
}
to {
opacity: 1;
@@ -163,13 +282,48 @@ select {
}
}
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes scale-in {
from {
opacity: 0;
transform: scale(0.96);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes dashboard-enter {
from {
opacity: 0;
transform: translateY(12px);
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes shimmer {
0% {
background-position: -200% 0;
}
100% {
background-position: 200% 0;
}
}
/* Focus visible styles */
:focus-visible {
outline: none;
box-shadow: var(--focus-ring);
}

View File

@@ -8,7 +8,7 @@
gap: 12px;
align-items: flex-start;
margin-bottom: 16px;
margin-left: 16px;
margin-left: 4px;
margin-right: 16px;
}
@@ -70,23 +70,23 @@
}
.chat-avatar.user {
background: rgba(245, 159, 74, 0.2);
color: rgba(245, 159, 74, 1);
background: var(--accent-subtle);
color: var(--accent);
}
.chat-avatar.assistant {
background: rgba(52, 199, 183, 0.2);
color: rgba(52, 199, 183, 1);
background: var(--secondary);
color: var(--muted);
}
.chat-avatar.other {
background: rgba(150, 150, 150, 0.2);
color: rgba(150, 150, 150, 1);
background: var(--secondary);
color: var(--muted);
}
.chat-avatar.tool {
background: rgba(134, 142, 150, 0.2);
color: rgba(134, 142, 150, 1);
background: var(--secondary);
color: var(--muted);
}
/* Image avatar support */
@@ -100,9 +100,9 @@ img.chat-avatar {
.chat-bubble {
position: relative;
display: inline-block;
border: 1px solid var(--border);
background: rgba(0, 0, 0, 0.12);
border-radius: 12px;
border: 1px solid transparent;
background: var(--card);
border-radius: var(--radius-lg);
padding: 10px 14px;
box-shadow: none;
transition: background 150ms ease-out, border-color 150ms ease-out;
@@ -119,9 +119,9 @@ img.chat-avatar {
top: 6px;
right: 8px;
border: 1px solid var(--border);
background: rgba(0, 0, 0, 0.22);
background: var(--bg);
color: var(--muted);
border-radius: 8px;
border-radius: var(--radius-md);
padding: 4px 6px;
font-size: 14px;
line-height: 1;
@@ -132,9 +132,40 @@ img.chat-avatar {
}
.chat-copy-btn__icon {
display: inline-block;
width: 1em;
text-align: center;
display: inline-flex;
width: 14px;
height: 14px;
position: relative;
}
.chat-copy-btn__icon svg {
width: 14px;
height: 14px;
stroke: currentColor;
fill: none;
stroke-width: 1.5px;
stroke-linecap: round;
stroke-linejoin: round;
}
.chat-copy-btn__icon-copy,
.chat-copy-btn__icon-check {
position: absolute;
top: 0;
left: 0;
transition: opacity 150ms ease;
}
.chat-copy-btn__icon-check {
opacity: 0;
}
.chat-copy-btn[data-copied="1"] .chat-copy-btn__icon-copy {
opacity: 0;
}
.chat-copy-btn[data-copied="1"] .chat-copy-btn__icon-check {
opacity: 1;
}
.chat-bubble:hover .chat-copy-btn {
@@ -143,7 +174,7 @@ img.chat-avatar {
}
.chat-copy-btn:hover {
background: rgba(0, 0, 0, 0.3);
background: var(--bg-hover);
}
.chat-copy-btn[data-copying="1"] {
@@ -154,17 +185,17 @@ img.chat-avatar {
.chat-copy-btn[data-error="1"] {
opacity: 1;
pointer-events: auto;
border-color: rgba(255, 69, 58, 0.8);
background: rgba(255, 69, 58, 0.18);
color: rgba(255, 69, 58, 1);
border-color: var(--danger-subtle);
background: var(--danger-subtle);
color: var(--danger);
}
.chat-copy-btn[data-copied="1"] {
opacity: 1;
pointer-events: auto;
border-color: rgba(52, 199, 183, 0.8);
background: rgba(52, 199, 183, 0.18);
color: rgba(52, 199, 183, 1);
border-color: var(--ok-subtle);
background: var(--ok-subtle);
color: var(--ok);
}
.chat-copy-btn:focus-visible {
@@ -181,18 +212,29 @@ img.chat-avatar {
}
}
/* Light mode: restore borders */
:root[data-theme="light"] .chat-bubble {
border-color: var(--border);
box-shadow: inset 0 1px 0 var(--card-highlight);
}
.chat-bubble:hover {
background: rgba(0, 0, 0, 0.18);
background: var(--bg-hover);
}
/* User bubbles have different styling */
.chat-group.user .chat-bubble {
background: rgba(245, 159, 74, 0.15);
border-color: rgba(245, 159, 74, 0.3);
background: var(--accent-subtle);
border-color: transparent;
}
:root[data-theme="light"] .chat-group.user .chat-bubble {
border-color: rgba(234, 88, 12, 0.2);
background: rgba(251, 146, 60, 0.12);
}
.chat-group.user .chat-bubble:hover {
background: rgba(245, 159, 74, 0.22);
background: rgba(255, 77, 77, 0.15);
}
/* Streaming animation */

View File

@@ -52,8 +52,8 @@
flex: 1 1 0; /* Grow, shrink, and use 0 base for proper scrolling */
overflow-y: auto;
overflow-x: hidden;
padding: 12px;
margin: 0 -12px;
padding: 12px 4px;
margin: 0 -4px;
min-height: 0; /* Allow shrinking for flex scroll behavior */
border-radius: 12px;
background: transparent;
@@ -87,23 +87,39 @@
border-color: var(--accent);
}
.chat-focus-exit svg {
width: 16px;
height: 16px;
stroke: currentColor;
fill: none;
stroke-width: 2px;
stroke-linecap: round;
stroke-linejoin: round;
}
/* Chat compose - sticky at bottom */
.chat-compose {
position: sticky;
bottom: 0;
flex-shrink: 0;
display: flex;
align-items: flex-end;
align-items: stretch;
gap: 12px;
margin-top: auto; /* Push to bottom of flex container */
padding: 16px 0 4px;
padding: 12px 4px 4px;
background: linear-gradient(to bottom, transparent, var(--bg) 20%);
z-index: 10;
}
:root[data-theme="light"] .chat-compose {
background: linear-gradient(to bottom, transparent, var(--bg-content) 20%);
}
.chat-compose__field {
flex: 1 1 auto;
min-width: 0;
display: flex;
align-items: stretch;
}
/* Hide the "Message" label - keep textarea only */
@@ -114,10 +130,11 @@
/* Override .field textarea min-height (180px) from components.css */
.chat-compose .chat-compose__field textarea {
width: 100%;
min-height: 36px;
height: 40px;
min-height: 40px;
max-height: 150px;
padding: 8px 12px;
border-radius: 10px;
padding: 9px 12px;
border-radius: 8px;
resize: vertical;
white-space: pre-wrap;
font-family: var(--font-body);
@@ -134,13 +151,18 @@
flex-shrink: 0;
display: flex;
align-items: stretch;
gap: 8px;
}
.chat-compose .chat-compose__actions .btn {
padding: 8px 16px;
padding: 0 16px;
font-size: 13px;
min-height: 36px;
height: 40px;
min-height: 40px;
max-height: 40px;
line-height: 1;
white-space: nowrap;
box-sizing: border-box;
}
/* Chat controls - moved to content-header area, left aligned */
@@ -194,20 +216,27 @@
/* Light theme icon button overrides */
:root[data-theme="light"] .btn--icon {
background: rgba(255, 255, 255, 0.9);
border-color: rgba(16, 24, 40, 0.2);
background: #ffffff;
border-color: var(--border);
box-shadow: 0 1px 2px rgba(16, 24, 40, 0.05);
color: rgba(16, 24, 40, 0.7);
color: var(--muted);
}
:root[data-theme="light"] .btn--icon:hover {
background: rgba(255, 255, 255, 1);
border-color: rgba(16, 24, 40, 0.3);
color: rgba(16, 24, 40, 0.9);
background: #ffffff;
border-color: var(--border-strong);
color: var(--text);
}
.btn--icon svg {
display: block;
width: 18px;
height: 18px;
stroke: currentColor;
fill: none;
stroke-width: 1.5px;
stroke-linecap: round;
stroke-linejoin: round;
}
.chat-controls__session select {

View File

@@ -14,8 +14,8 @@
}
:root[data-theme="light"] .chat-thinking {
border-color: rgba(16, 24, 40, 0.18);
background: rgba(16, 24, 40, 0.03);
border-color: rgba(16, 24, 40, 0.25);
background: rgba(16, 24, 40, 0.04);
}
.chat-text {
@@ -75,9 +75,46 @@
}
.chat-text :where(blockquote) {
border-left: 3px solid var(--border);
border-left: 3px solid var(--border-strong);
padding-left: 12px;
margin-left: 0;
color: var(--muted);
background: rgba(255, 255, 255, 0.02);
padding: 8px 12px;
border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
}
.chat-text :where(blockquote blockquote) {
margin-top: 8px;
border-left-color: var(--border-hover);
background: rgba(255, 255, 255, 0.03);
}
.chat-text :where(blockquote blockquote blockquote) {
border-left-color: var(--muted-strong);
background: rgba(255, 255, 255, 0.04);
}
:root[data-theme="light"] .chat-text :where(blockquote) {
background: rgba(0, 0, 0, 0.03);
}
:root[data-theme="light"] .chat-text :where(blockquote blockquote) {
background: rgba(0, 0, 0, 0.05);
}
:root[data-theme="light"] .chat-text :where(blockquote blockquote blockquote) {
background: rgba(0, 0, 0, 0.04);
}
:root[data-theme="light"] .chat-text :where(:not(pre) > code) {
background: rgba(0, 0, 0, 0.08);
border: 1px solid rgba(0, 0, 0, 0.1);
}
:root[data-theme="light"] .chat-text :where(pre) {
background: rgba(0, 0, 0, 0.05);
border: 1px solid rgba(0, 0, 0, 0.1);
}
.chat-text :where(hr) {

View File

@@ -4,6 +4,8 @@
border-radius: 8px;
padding: 12px;
margin-top: 8px;
background: var(--card);
box-shadow: inset 0 1px 0 var(--card-highlight);
transition: border-color 150ms ease-out, background 150ms ease-out;
/* Fixed max-height to ensure cards don't expand too much */
max-height: 120px;
@@ -11,8 +13,8 @@
}
.chat-tool-card:hover {
border-color: var(--accent);
background: rgba(0, 0, 0, 0.06);
border-color: var(--border-strong);
background: var(--bg-hover);
}
/* First tool card in a group - no top margin */
@@ -50,33 +52,63 @@
display: inline-flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
font-size: 14px;
line-height: 1;
font-family: "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", sans-serif;
vertical-align: middle;
width: 16px;
height: 16px;
flex-shrink: 0;
}
.chat-tool-card__icon svg {
width: 14px;
height: 14px;
stroke: currentColor;
fill: none;
stroke-width: 1.5px;
stroke-linecap: round;
stroke-linejoin: round;
}
/* "View >" action link */
.chat-tool-card__action {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 12px;
color: var(--accent);
opacity: 0.8;
transition: opacity 150ms ease-out;
}
.chat-tool-card__action svg {
width: 12px;
height: 12px;
stroke: currentColor;
fill: none;
stroke-width: 1.5px;
stroke-linecap: round;
stroke-linejoin: round;
}
.chat-tool-card--clickable:hover .chat-tool-card__action {
opacity: 1;
}
/* Status indicator for completed/empty results */
.chat-tool-card__status {
font-size: 14px;
display: inline-flex;
align-items: center;
color: var(--ok);
}
.chat-tool-card__status svg {
width: 14px;
height: 14px;
stroke: currentColor;
fill: none;
stroke-width: 2px;
stroke-linecap: round;
stroke-linejoin: round;
}
.chat-tool-card__status-text {
font-size: 11px;
margin-top: 4px;
@@ -94,18 +126,18 @@
color: var(--muted);
margin-top: 8px;
padding: 8px 10px;
background: rgba(0, 0, 0, 0.08);
border-radius: 6px;
background: var(--secondary);
border-radius: var(--radius-md);
white-space: pre-wrap;
overflow: hidden;
max-height: 44px;
line-height: 1.4;
border: 1px solid rgba(255, 255, 255, 0.04);
border: 1px solid var(--border);
}
.chat-tool-card--clickable:hover .chat-tool-card__preview {
background: rgba(0, 0, 0, 0.12);
border-color: rgba(255, 255, 255, 0.08);
background: var(--bg-hover);
border-color: var(--border-strong);
}
/* Short inline output */
@@ -114,8 +146,8 @@
color: var(--text);
margin-top: 6px;
padding: 6px 8px;
background: rgba(0, 0, 0, 0.06);
border-radius: 4px;
background: var(--secondary);
border-radius: var(--radius-sm);
white-space: pre-wrap;
word-break: break-word;
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,14 @@
/* ===========================================
Shell Layout
=========================================== */
.shell {
--shell-pad: 16px;
--shell-gap: 16px;
--shell-nav-width: 220px;
--shell-topbar-height: 56px;
--shell-focus-duration: 220ms;
--shell-focus-ease: cubic-bezier(0.2, 0.85, 0.25, 1);
--shell-focus-duration: 200ms;
--shell-focus-ease: var(--ease-out);
height: 100vh;
display: grid;
grid-template-columns: var(--shell-nav-width) minmax(0, 1fr);
@@ -13,7 +17,7 @@
"topbar topbar"
"nav content";
gap: 0;
animation: dashboard-enter 0.6s ease-out;
animation: dashboard-enter 0.4s var(--ease-out);
transition: grid-template-columns var(--shell-focus-duration) var(--shell-focus-ease);
overflow: hidden;
}
@@ -61,6 +65,10 @@
gap: 0;
}
/* ===========================================
Topbar
=========================================== */
.topbar {
grid-area: topbar;
position: sticky;
@@ -73,8 +81,7 @@
padding: 0 20px;
height: var(--shell-topbar-height);
border-bottom: 1px solid var(--border);
background: var(--panel);
backdrop-filter: blur(18px);
background: var(--bg);
}
.topbar-left {
@@ -84,49 +91,83 @@
}
.topbar .nav-collapse-toggle {
width: 44px;
height: 44px;
width: 36px;
height: 36px;
margin-bottom: 0;
}
.topbar .nav-collapse-toggle__icon {
font-size: 22px;
width: 20px;
height: 20px;
}
.topbar .nav-collapse-toggle__icon svg {
width: 20px;
height: 20px;
}
/* Brand */
.brand {
display: flex;
align-items: center;
gap: 10px;
}
.brand-logo {
width: 28px;
height: 28px;
flex-shrink: 0;
}
.brand-logo img {
width: 100%;
height: 100%;
object-fit: contain;
}
.brand-text {
display: flex;
flex-direction: column;
gap: 2px;
gap: 1px;
}
.brand-title {
font-family: var(--font-display);
font-size: 16px;
letter-spacing: 1px;
text-transform: uppercase;
font-size: 15px;
font-weight: 600;
letter-spacing: -0.02em;
line-height: 1.1;
color: var(--text-strong);
}
.brand-sub {
font-size: 10px;
font-size: 11px;
font-weight: 500;
color: var(--muted);
letter-spacing: 0.8px;
text-transform: uppercase;
letter-spacing: 0.02em;
line-height: 1;
}
/* Topbar status */
.topbar-status {
display: flex;
align-items: center;
gap: 8px;
}
/* Smaller pill and theme toggle in topbar */
.topbar-status .pill {
padding: 4px 10px;
padding: 6px 10px;
gap: 6px;
font-size: 11px;
font-size: 12px;
font-weight: 500;
height: 32px;
box-sizing: border-box;
}
.topbar-status .pill .mono {
display: flex;
align-items: center;
line-height: 1;
margin-top: 0px;
}
.topbar-status .statusDot {
@@ -135,9 +176,9 @@
}
.topbar-status .theme-toggle {
--theme-item: 22px;
--theme-gap: 4px;
--theme-pad: 4px;
--theme-item: 24px;
--theme-gap: 2px;
--theme-pad: 3px;
}
.topbar-status .theme-icon {
@@ -145,17 +186,22 @@
height: 12px;
}
/* ===========================================
Navigation Sidebar
=========================================== */
.nav {
grid-area: nav;
overflow-y: auto;
overflow-x: hidden;
padding: 16px;
padding: 16px 12px;
border-right: 1px solid var(--border);
background: var(--panel);
backdrop-filter: blur(18px);
transition: width var(--shell-focus-duration) var(--shell-focus-ease),
padding var(--shell-focus-duration) var(--shell-focus-ease);
min-height: 0; /* Allow grid item to shrink and enable scrolling */
background: var(--bg);
transition:
width var(--shell-focus-duration) var(--shell-focus-ease),
padding var(--shell-focus-duration) var(--shell-focus-ease),
opacity var(--shell-focus-duration) var(--shell-focus-ease);
min-height: 0;
}
.shell--chat-focus .nav {
@@ -164,9 +210,9 @@
border-width: 0;
overflow: hidden;
pointer-events: none;
opacity: 0;
}
/* Collapsed nav sidebar - completely hidden */
.nav--collapsed {
width: 0;
min-width: 0;
@@ -177,7 +223,7 @@
pointer-events: none;
}
/* Nav collapse toggle button */
/* Nav collapse toggle */
.nav-collapse-toggle {
width: 32px;
height: 32px;
@@ -186,71 +232,88 @@
justify-content: center;
background: transparent;
border: 1px solid transparent;
border-radius: 6px;
border-radius: var(--radius-md);
cursor: pointer;
transition: background 150ms ease, border-color 150ms ease;
transition:
background var(--duration-fast) ease,
border-color var(--duration-fast) ease;
margin-bottom: 16px;
}
.nav-collapse-toggle:hover {
background: rgba(255, 255, 255, 0.08);
background: var(--bg-hover);
border-color: var(--border);
}
:root[data-theme="light"] .nav-collapse-toggle:hover {
background: rgba(0, 0, 0, 0.06);
}
.nav-collapse-toggle__icon {
font-size: 16px;
display: flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
color: var(--muted);
transition: color var(--duration-fast) ease;
}
.nav-collapse-toggle__icon svg {
width: 18px;
height: 18px;
stroke: currentColor;
fill: none;
stroke-width: 1.5px;
stroke-linecap: round;
stroke-linejoin: round;
}
.nav-collapse-toggle:hover .nav-collapse-toggle__icon {
color: var(--text);
}
/* Nav groups */
.nav-group {
margin-bottom: 18px;
margin-bottom: 20px;
display: grid;
gap: 6px;
padding-bottom: 12px;
border-bottom: 1px dashed rgba(255, 255, 255, 0.08);
gap: 2px;
}
.nav-group:last-child {
margin-bottom: 0;
padding-bottom: 0;
border-bottom: none;
}
.nav-group__items {
display: grid;
gap: 4px;
gap: 1px;
}
.nav-group--collapsed .nav-group__items {
display: none;
}
/* Nav label */
.nav-label {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
width: 100%;
padding: 4px 0;
padding: 6px 10px;
font-size: 11px;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 1.4px;
color: var(--text);
opacity: 0.7;
color: var(--muted);
margin-bottom: 4px;
background: transparent;
border: none;
cursor: pointer;
text-align: left;
border-radius: var(--radius-sm);
transition:
color var(--duration-fast) ease,
background var(--duration-fast) ease;
}
.nav-label:hover {
opacity: 1;
color: var(--text);
background: var(--bg-hover);
}
.nav-label--static {
@@ -258,7 +321,8 @@
}
.nav-label--static:hover {
opacity: 0.7;
color: var(--muted);
background: transparent;
}
.nav-label__text {
@@ -266,131 +330,152 @@
}
.nav-label__chevron {
font-size: 12px;
opacity: 0.6;
font-size: 10px;
opacity: 0.5;
transition: transform var(--duration-fast) ease;
}
.nav-group--collapsed .nav-label__chevron {
transform: rotate(-90deg);
}
/* Nav items */
.nav-item {
position: relative;
display: flex;
align-items: center;
justify-content: flex-start;
gap: 8px;
padding: 10px 12px 10px 14px;
border-radius: 12px;
gap: 10px;
padding: 8px 10px;
border-radius: var(--radius-md);
border: 1px solid transparent;
background: transparent;
color: var(--muted);
cursor: pointer;
text-decoration: none;
transition: border-color 160ms ease, background 160ms ease, color 160ms ease;
transition:
border-color var(--duration-fast) ease,
background var(--duration-fast) ease,
color var(--duration-fast) ease;
}
.nav-item__icon {
font-size: 16px;
width: 18px;
height: 18px;
width: 16px;
height: 16px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
opacity: 0.7;
transition: opacity var(--duration-fast) ease;
}
.nav-item__icon svg {
width: 16px;
height: 16px;
stroke: currentColor;
fill: none;
stroke-width: 1.5px;
stroke-linecap: round;
stroke-linejoin: round;
}
.nav-item__text {
font-size: 13px;
font-weight: 500;
white-space: nowrap;
}
.nav-item:hover {
color: var(--text);
border-color: rgba(255, 255, 255, 0.12);
background: rgba(255, 255, 255, 0.06);
background: var(--bg-hover);
text-decoration: none;
}
.nav-item::before {
content: "";
position: absolute;
left: 0;
top: 50%;
width: 4px;
height: 60%;
border-radius: 0 999px 999px 0;
transform: translateY(-50%);
background: transparent;
.nav-item:hover .nav-item__icon {
opacity: 1;
}
.nav-item.active {
color: var(--text);
border-color: rgba(245, 159, 74, 0.45);
background: rgba(245, 159, 74, 0.12);
color: var(--text-strong);
background: var(--accent-subtle);
}
.nav-item.active::before {
background: var(--accent);
box-shadow: 0 0 12px rgba(245, 159, 74, 0.4);
.nav-item.active .nav-item__icon {
opacity: 1;
color: var(--accent);
}
/* ===========================================
Content Area
=========================================== */
.content {
grid-area: content;
padding: 8px 6px 20px;
padding: 8px 8px 24px;
display: flex;
flex-direction: column;
gap: 20px;
min-height: 0;
overflow-y: auto; /* Enable vertical scrolling for pages with long content */
overflow-y: auto;
overflow-x: hidden;
}
/* Chat handles its own scrolling (chat-thread); avoid double scrollbars. */
:root[data-theme="light"] .content {
background: var(--bg-content);
}
.content--chat {
overflow: hidden;
padding-bottom: 0;
}
.shell--chat .content {
/* No-op: keep chat layout consistent with other tabs */
}
/* Content header */
.content-header {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 12px;
padding: 0 6px;
gap: 16px;
padding: 4px 8px;
overflow: hidden;
transform-origin: top center;
transition: opacity var(--shell-focus-duration) var(--shell-focus-ease),
transition:
opacity var(--shell-focus-duration) var(--shell-focus-ease),
transform var(--shell-focus-duration) var(--shell-focus-ease),
max-height var(--shell-focus-duration) var(--shell-focus-ease),
padding var(--shell-focus-duration) var(--shell-focus-ease);
max-height: 90px;
max-height: 80px;
}
.shell--chat-focus .content-header {
opacity: 0;
transform: translateY(-10px);
transform: translateY(-8px);
max-height: 0px;
padding: 0;
pointer-events: none;
}
.page-title {
font-family: var(--font-display);
font-size: 26px;
letter-spacing: 0.6px;
font-size: 24px;
font-weight: 600;
letter-spacing: -0.02em;
line-height: 1.2;
color: var(--text-strong);
}
.page-sub {
color: var(--muted);
font-size: 12px;
letter-spacing: 0.4px;
font-size: 13px;
font-weight: 400;
margin-top: 4px;
}
.page-meta {
display: flex;
gap: 10px;
gap: 8px;
}
/* Chat view: header and controls side by side */
/* Chat view header adjustments */
.content--chat .content-header {
flex-direction: row;
align-items: center;
@@ -410,9 +495,13 @@
flex-shrink: 0;
}
/* ===========================================
Grid Utilities
=========================================== */
.grid {
display: grid;
gap: 18px;
gap: 16px;
}
.grid-cols-2 {
@@ -425,39 +514,42 @@
.stat-grid {
display: grid;
gap: 14px;
gap: 12px;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
}
.note-grid {
display: grid;
gap: 14px;
gap: 12px;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
}
.row {
display: flex;
gap: 12px;
gap: 10px;
align-items: center;
}
.stack {
display: grid;
gap: 14px;
gap: 12px;
}
.filters {
display: flex;
flex-wrap: wrap;
gap: 10px;
gap: 8px;
align-items: center;
}
/* ===========================================
Responsive - Tablet
=========================================== */
@media (max-width: 1100px) {
.shell {
--shell-pad: 12px;
--shell-gap: 12px;
--shell-nav-col: 1fr;
grid-template-columns: 1fr;
grid-template-rows: auto auto 1fr;
grid-template-areas:
@@ -470,17 +562,18 @@
position: static;
max-height: none;
display: flex;
gap: 16px;
gap: 6px;
overflow-x: auto;
border-right: none;
padding: 12px;
border-bottom: 1px solid var(--border);
padding: 10px 14px;
background: var(--bg);
}
.nav-group {
grid-auto-flow: column;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
border-bottom: none;
padding-bottom: 0;
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
margin-bottom: 0;
}
.grid-cols-2,
@@ -490,13 +583,11 @@
.topbar {
position: static;
flex-direction: column;
align-items: flex-start;
gap: 12px;
padding: 12px 14px;
gap: 10px;
}
.topbar-status {
width: 100%;
flex-wrap: wrap;
}

View File

@@ -1,12 +1,15 @@
/* Tablet/Mobile nav fix (under 1100px) - single horizontal scroll row */
/* ===========================================
Mobile Layout
=========================================== */
/* Tablet: Horizontal nav */
@media (max-width: 1100px) {
/* Flatten nav into single horizontal scroll */
.nav {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
gap: 6px;
padding: 10px 12px;
gap: 4px;
padding: 10px 14px;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
@@ -16,21 +19,18 @@
display: none;
}
/* Nav groups should flow inline, not stack */
.nav-group {
display: contents; /* Flatten group wrapper - items flow directly into .nav */
display: contents;
}
.nav-group__items {
display: contents; /* Flatten items wrapper too */
display: contents;
}
/* Hide group labels on tablet/mobile */
.nav-label {
display: none;
}
/* Don't hide nav items even if group is "collapsed" */
.nav-group--collapsed .nav-group__items {
display: contents;
}
@@ -38,27 +38,22 @@
.nav-item {
padding: 8px 14px;
font-size: 13px;
border-radius: 10px;
border-radius: var(--radius-md);
white-space: nowrap;
flex-shrink: 0;
}
.nav-item::before {
display: none;
}
}
/* Mobile-specific improvements */
/* Mobile-specific styles */
@media (max-width: 600px) {
.shell {
--shell-pad: 8px;
--shell-gap: 8px;
}
/* Compact topbar for mobile */
/* Topbar */
.topbar {
padding: 10px 12px;
border-radius: 12px;
gap: 8px;
flex-direction: row;
flex-wrap: wrap;
@@ -72,8 +67,7 @@
}
.brand-title {
font-size: 15px;
letter-spacing: 0.3px;
font-size: 14px;
}
.brand-sub {
@@ -100,11 +94,10 @@
display: none;
}
/* Horizontal scrollable nav for mobile */
/* Nav */
.nav {
padding: 8px;
border-radius: 12px;
gap: 8px;
padding: 8px 10px;
gap: 4px;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
}
@@ -122,18 +115,14 @@
}
.nav-item {
padding: 7px 10px;
padding: 6px 10px;
font-size: 12px;
border-radius: 8px;
border-radius: var(--radius-md);
white-space: nowrap;
flex-shrink: 0;
}
.nav-item::before {
display: none;
}
/* Hide page title on mobile - nav already shows where you are */
/* Content */
.content-header {
display: none;
}
@@ -143,17 +132,17 @@
gap: 12px;
}
/* Smaller cards on mobile */
/* Cards */
.card {
padding: 12px;
border-radius: 12px;
border-radius: var(--radius-md);
}
.card-title {
font-size: 14px;
font-size: 13px;
}
/* Stat grid adjustments */
/* Stats */
.stat-grid {
gap: 8px;
grid-template-columns: repeat(2, 1fr);
@@ -161,24 +150,24 @@
.stat {
padding: 10px;
border-radius: 10px;
border-radius: var(--radius-md);
}
.stat-label {
font-size: 10px;
font-size: 11px;
}
.stat-value {
font-size: 16px;
font-size: 18px;
}
/* Notes grid */
/* Notes */
.note-grid {
grid-template-columns: 1fr;
gap: 10px;
gap: 8px;
}
/* Form fields */
/* Forms */
.form-grid {
grid-template-columns: 1fr;
gap: 10px;
@@ -188,14 +177,14 @@
.field textarea,
.field select {
padding: 8px 10px;
border-radius: 10px;
border-radius: var(--radius-md);
font-size: 14px;
}
/* Buttons */
.btn {
padding: 8px 12px;
font-size: 13px;
font-size: 12px;
}
/* Pills */
@@ -204,7 +193,7 @@
font-size: 12px;
}
/* Chat-specific mobile improvements */
/* Chat */
.chat-header {
flex-direction: column;
align-items: stretch;
@@ -227,17 +216,16 @@
.chat-thread {
margin-top: 8px;
padding: 10px 8px;
border-radius: 12px;
padding: 12px 8px;
}
.chat-msg {
max-width: 92%;
max-width: 90%;
}
.chat-bubble {
padding: 8px 10px;
border-radius: 12px;
padding: 8px 12px;
border-radius: var(--radius-md);
}
.chat-compose {
@@ -247,14 +235,14 @@
.chat-compose__field textarea {
min-height: 60px;
padding: 8px 10px;
border-radius: 12px;
border-radius: var(--radius-md);
font-size: 14px;
}
/* Log stream mobile */
/* Log stream */
.log-stream {
border-radius: 10px;
max-height: 400px;
border-radius: var(--radius-md);
max-height: 380px;
}
.log-row {
@@ -279,14 +267,14 @@
font-size: 12px;
}
/* List items */
/* Lists */
.list-item {
padding: 10px;
border-radius: 10px;
border-radius: var(--radius-md);
}
.list-title {
font-size: 14px;
font-size: 13px;
}
.list-sub {
@@ -296,19 +284,91 @@
/* Code blocks */
.code-block {
padding: 8px;
border-radius: 10px;
border-radius: var(--radius-md);
font-size: 11px;
}
/* Theme toggle smaller */
/* Theme toggle */
.theme-toggle {
--theme-item: 24px;
--theme-gap: 4px;
--theme-pad: 4px;
--theme-gap: 2px;
--theme-pad: 3px;
}
.theme-icon {
width: 14px;
height: 14px;
width: 12px;
height: 12px;
}
}
/* Small mobile */
@media (max-width: 400px) {
.shell {
--shell-pad: 4px;
}
.topbar {
padding: 8px 10px;
}
.brand-title {
font-size: 13px;
}
.nav {
padding: 6px 8px;
}
.nav-item {
padding: 6px 8px;
font-size: 11px;
}
.content {
padding: 4px 4px 12px;
gap: 10px;
}
.card {
padding: 10px;
}
.stat {
padding: 8px;
}
.stat-value {
font-size: 16px;
}
.chat-bubble {
padding: 8px 10px;
}
.chat-compose__field textarea {
min-height: 52px;
padding: 8px 10px;
font-size: 13px;
}
.btn {
padding: 6px 10px;
font-size: 11px;
}
.topbar-status .pill {
padding: 3px 6px;
font-size: 10px;
}
.theme-toggle {
--theme-item: 22px;
--theme-gap: 2px;
--theme-pad: 2px;
}
.theme-icon {
width: 11px;
height: 11px;
}
}

View File

@@ -3,6 +3,7 @@ import { repeat } from "lit/directives/repeat.js";
import type { AppViewState } from "./app-view-state";
import { iconForTab, pathForTab, titleForTab, type Tab } from "./navigation";
import { icons } from "./icons";
import { loadChatHistory } from "./controllers/chat";
import { syncUrlWithSessionKey } from "./app-settings";
import type { SessionsListResult } from "./types";
@@ -31,7 +32,7 @@ export function renderTab(state: AppViewState, tab: Tab) {
}}
title=${titleForTab(tab)}
>
<span class="nav-item__icon" aria-hidden="true">${iconForTab(tab)}</span>
<span class="nav-item__icon" aria-hidden="true">${icons[iconForTab(tab)]}</span>
<span class="nav-item__text">${titleForTab(tab)}</span>
</a>
`;
@@ -108,7 +109,7 @@ export function renderChatControls(state: AppViewState) {
? "Disabled during onboarding"
: "Toggle assistant thinking/working output"}
>
🧠
${icons.brain}
</button>
<button
class="btn btn--sm btn--icon ${focusActive ? "active" : ""}"

View File

@@ -11,6 +11,7 @@ import {
titleForTab,
type Tab,
} from "./navigation";
import { icons } from "./icons";
import type { UiSettings } from "./storage";
import type { ThemeMode } from "./theme";
import type { ThemeTransitionContext } from "./theme-transition";
@@ -124,11 +125,16 @@ export function renderApp(state: AppViewState) {
title="${state.settings.navCollapsed ? "Expand sidebar" : "Collapse sidebar"}"
aria-label="${state.settings.navCollapsed ? "Expand sidebar" : "Collapse sidebar"}"
>
<span class="nav-collapse-toggle__icon"></span>
<span class="nav-collapse-toggle__icon">${icons.menu}</span>
</button>
<div class="brand">
<div class="brand-title">CLAWDBOT</div>
<div class="brand-sub">Gateway Dashboard</div>
<div class="brand-logo">
<img src="https://mintcdn.com/clawdhub/4rYvG-uuZrMK_URE/assets/pixel-lobster.svg?fit=max&auto=format&n=4rYvG-uuZrMK_URE&q=85&s=da2032e9eac3b5d9bfe7eb96ca6a8a26" alt="Clawdbot" />
</div>
<div class="brand-text">
<div class="brand-title">CLAWDBOT</div>
<div class="brand-sub">Gateway Dashboard</div>
</div>
</div>
</div>
<div class="topbar-status">
@@ -179,7 +185,7 @@ export function renderApp(state: AppViewState) {
rel="noreferrer"
title="Docs (opens in new tab)"
>
<span class="nav-item__icon" aria-hidden="true">📚</span>
<span class="nav-item__icon" aria-hidden="true">${icons.book}</span>
<span class="nav-item__text">Docs</span>
</a>
</div>

View File

@@ -1,14 +1,11 @@
import { html, type TemplateResult } from "lit";
import { renderEmojiIcon, setEmojiIcon } from "../icons";
import { icons } from "../icons";
const COPIED_FOR_MS = 1500;
const ERROR_FOR_MS = 2000;
const COPY_LABEL = "Copy as markdown";
const COPIED_LABEL = "Copied";
const ERROR_LABEL = "Copy failed";
const COPY_ICON = "📋";
const COPIED_ICON = "✓";
const ERROR_ICON = "!";
type CopyButtonOptions = {
text: () => string;
@@ -41,7 +38,7 @@ function createCopyButton(options: CopyButtonOptions): TemplateResult {
aria-label=${idleLabel}
@click=${async (e: Event) => {
const btn = e.currentTarget as HTMLButtonElement | null;
const icon = btn?.querySelector(
const iconContainer = btn?.querySelector(
".chat-copy-btn__icon",
) as HTMLElement | null;
@@ -61,30 +58,29 @@ function createCopyButton(options: CopyButtonOptions): TemplateResult {
if (!copied) {
btn.dataset.error = "1";
setButtonLabel(btn, ERROR_LABEL);
setEmojiIcon(icon, ERROR_ICON);
window.setTimeout(() => {
if (!btn.isConnected) return;
delete btn.dataset.error;
setButtonLabel(btn, idleLabel);
setEmojiIcon(icon, COPY_ICON);
}, ERROR_FOR_MS);
return;
}
btn.dataset.copied = "1";
setButtonLabel(btn, COPIED_LABEL);
setEmojiIcon(icon, COPIED_ICON);
window.setTimeout(() => {
if (!btn.isConnected) return;
delete btn.dataset.copied;
setButtonLabel(btn, idleLabel);
setEmojiIcon(icon, COPY_ICON);
}, COPIED_FOR_MS);
}}
>
${renderEmojiIcon(COPY_ICON, "chat-copy-btn__icon")}
<span class="chat-copy-btn__icon" aria-hidden="true">
<span class="chat-copy-btn__icon-copy">${icons.copy}</span>
<span class="chat-copy-btn__icon-check">${icons.check}</span>
</span>
</button>
`;
}

View File

@@ -1,6 +1,7 @@
import { html, nothing } from "lit";
import { formatToolDetail, resolveToolDisplay } from "../tool-display";
import { icons } from "../icons";
import type { ToolCard } from "../types/chat-types";
import { TOOL_INLINE_THRESHOLD } from "./constants";
import {
@@ -95,13 +96,13 @@ export function renderToolCardSidebar(
>
<div class="chat-tool-card__header">
<div class="chat-tool-card__title">
<span class="chat-tool-card__icon">${display.emoji}</span>
<span class="chat-tool-card__icon">${icons[display.icon]}</span>
<span>${display.label}</span>
</div>
${canClick
? html`<span class="chat-tool-card__action">${hasText ? "View " : ""}</span>`
? html`<span class="chat-tool-card__action">${hasText ? "View" : ""} ${icons.check}</span>`
: nothing}
${isEmpty && !canClick ? html`<span class="chat-tool-card__status"></span>` : nothing}
${isEmpty && !canClick ? html`<span class="chat-tool-card__status">${icons.check}</span>` : nothing}
</div>
${detail
? html`<div class="chat-tool-card__detail">${detail}</div>`

View File

@@ -1,7 +1,59 @@
import { html, type TemplateResult } from "lit";
export function renderEmojiIcon(icon: string, className: string): TemplateResult {
return html`<span class=${className} aria-hidden="true">${icon}</span>`;
// Lucide-style SVG icons
// All icons use currentColor for stroke
export const icons = {
// Navigation icons
messageSquare: html`<svg viewBox="0 0 24 24"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>`,
barChart: html`<svg viewBox="0 0 24 24"><line x1="12" x2="12" y1="20" y2="10"/><line x1="18" x2="18" y1="20" y2="4"/><line x1="6" x2="6" y1="20" y2="16"/></svg>`,
link: html`<svg viewBox="0 0 24 24"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>`,
radio: html`<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="2"/><path d="M16.24 7.76a6 6 0 0 1 0 8.49m-8.48-.01a6 6 0 0 1 0-8.49m11.31-2.82a10 10 0 0 1 0 14.14m-14.14 0a10 10 0 0 1 0-14.14"/></svg>`,
fileText: html`<svg viewBox="0 0 24 24"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/><polyline points="14 2 14 8 20 8"/><line x1="16" x2="8" y1="13" y2="13"/><line x1="16" x2="8" y1="17" y2="17"/><line x1="10" x2="8" y1="9" y2="9"/></svg>`,
zap: html`<svg viewBox="0 0 24 24"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>`,
monitor: html`<svg viewBox="0 0 24 24"><rect width="20" height="14" x="2" y="3" rx="2"/><line x1="8" x2="16" y1="21" y2="21"/><line x1="12" x2="12" y1="17" y2="21"/></svg>`,
settings: html`<svg viewBox="0 0 24 24"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/></svg>`,
bug: html`<svg viewBox="0 0 24 24"><path d="m8 2 1.88 1.88"/><path d="M14.12 3.88 16 2"/><path d="M9 7.13v-1a3.003 3.003 0 1 1 6 0v1"/><path d="M12 20c-3.3 0-6-2.7-6-6v-3a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v3c0 3.3-2.7 6-6 6"/><path d="M12 20v-9"/><path d="M6.53 9C4.6 8.8 3 7.1 3 5"/><path d="M6 13H2"/><path d="M3 21c0-2.1 1.7-3.9 3.8-4"/><path d="M20.97 5c0 2.1-1.6 3.8-3.5 4"/><path d="M22 13h-4"/><path d="M17.2 17c2.1.1 3.8 1.9 3.8 4"/></svg>`,
scrollText: html`<svg viewBox="0 0 24 24"><path d="M8 21h12a2 2 0 0 0 2-2v-2H10v2a2 2 0 1 1-4 0V5a2 2 0 1 0-4 0v3h4"/><path d="M19 17V5a2 2 0 0 0-2-2H4"/><path d="M15 8h-5"/><path d="M15 12h-5"/></svg>`,
folder: html`<svg viewBox="0 0 24 24"><path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/></svg>`,
// UI icons
menu: html`<svg viewBox="0 0 24 24"><line x1="4" x2="20" y1="12" y2="12"/><line x1="4" x2="20" y1="6" y2="6"/><line x1="4" x2="20" y1="18" y2="18"/></svg>`,
x: html`<svg viewBox="0 0 24 24"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>`,
check: html`<svg viewBox="0 0 24 24"><path d="M20 6 9 17l-5-5"/></svg>`,
copy: html`<svg viewBox="0 0 24 24"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>`,
search: html`<svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>`,
brain: html`<svg viewBox="0 0 24 24"><path d="M12 5a3 3 0 1 0-5.997.125 4 4 0 0 0-2.526 5.77 4 4 0 0 0 .556 6.588A4 4 0 1 0 12 18Z"/><path d="M12 5a3 3 0 1 1 5.997.125 4 4 0 0 1 2.526 5.77 4 4 0 0 1-.556 6.588A4 4 0 1 1 12 18Z"/><path d="M15 13a4.5 4.5 0 0 1-3-4 4.5 4.5 0 0 1-3 4"/><path d="M17.599 6.5a3 3 0 0 0 .399-1.375"/><path d="M6.003 5.125A3 3 0 0 0 6.401 6.5"/><path d="M3.477 10.896a4 4 0 0 1 .585-.396"/><path d="M19.938 10.5a4 4 0 0 1 .585.396"/><path d="M6 18a4 4 0 0 1-1.967-.516"/><path d="M19.967 17.484A4 4 0 0 1 18 18"/></svg>`,
book: html`<svg viewBox="0 0 24 24"><path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H20v20H6.5a2.5 2.5 0 0 1 0-5H20"/></svg>`,
loader: html`<svg viewBox="0 0 24 24"><path d="M12 2v4"/><path d="m16.2 7.8 2.9-2.9"/><path d="M18 12h4"/><path d="m16.2 16.2 2.9 2.9"/><path d="M12 18v4"/><path d="m4.9 19.1 2.9-2.9"/><path d="M2 12h4"/><path d="m4.9 4.9 2.9 2.9"/></svg>`,
// Tool icons
wrench: html`<svg viewBox="0 0 24 24"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg>`,
fileCode: html`<svg viewBox="0 0 24 24"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/><polyline points="14 2 14 8 20 8"/><path d="m10 13-2 2 2 2"/><path d="m14 17 2-2-2-2"/></svg>`,
edit: html`<svg viewBox="0 0 24 24"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>`,
penLine: html`<svg viewBox="0 0 24 24"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>`,
paperclip: html`<svg viewBox="0 0 24 24"><path d="m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l8.57-8.57A4 4 0 1 1 18 8.84l-8.59 8.57a2 2 0 0 1-2.83-2.83l8.49-8.48"/></svg>`,
globe: html`<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20"/><path d="M2 12h20"/></svg>`,
image: html`<svg viewBox="0 0 24 24"><rect width="18" height="18" x="3" y="3" rx="2" ry="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"/></svg>`,
smartphone: html`<svg viewBox="0 0 24 24"><rect width="14" height="20" x="5" y="2" rx="2" ry="2"/><path d="M12 18h.01"/></svg>`,
plug: html`<svg viewBox="0 0 24 24"><path d="M12 22v-5"/><path d="M9 8V2"/><path d="M15 8V2"/><path d="M18 8v5a4 4 0 0 1-4 4h-4a4 4 0 0 1-4-4V8Z"/></svg>`,
circle: html`<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/></svg>`,
puzzle: html`<svg viewBox="0 0 24 24"><path d="M19.439 7.85c-.049.322.059.648.289.878l1.568 1.568c.47.47.706 1.087.706 1.704s-.235 1.233-.706 1.704l-1.611 1.611a.98.98 0 0 1-.837.276c-.47-.07-.802-.48-.968-.925a2.501 2.501 0 1 0-3.214 3.214c.446.166.855.497.925.968a.979.979 0 0 1-.276.837l-1.61 1.61a2.404 2.404 0 0 1-1.705.707 2.402 2.402 0 0 1-1.704-.706l-1.568-1.568a1.026 1.026 0 0 0-.877-.29c-.493.074-.84.504-1.02.968a2.5 2.5 0 1 1-3.237-3.237c.464-.18.894-.527.967-1.02a1.026 1.026 0 0 0-.289-.877l-1.568-1.568A2.402 2.402 0 0 1 1.998 12c0-.617.236-1.234.706-1.704L4.23 8.77c.24-.24.581-.353.917-.303.515.076.874.54 1.02 1.02a2.5 2.5 0 1 0 3.237-3.237c-.48-.146-.944-.505-1.02-1.02a.98.98 0 0 1 .303-.917l1.526-1.526A2.402 2.402 0 0 1 11.998 2c.617 0 1.234.236 1.704.706l1.568 1.568c.23.23.556.338.877.29.493-.074.84-.504 1.02-.968a2.5 2.5 0 1 1 3.236 3.236c-.464.18-.894.527-.967 1.02Z"/></svg>`,
} as const;
export type IconName = keyof typeof icons;
export function icon(name: IconName): TemplateResult {
return icons[name];
}
export function renderIcon(name: IconName, className = "nav-item__icon"): TemplateResult {
return html`<span class=${className} aria-hidden="true">${icons[name]}</span>`;
}
// Legacy function for compatibility
export function renderEmojiIcon(iconContent: string | TemplateResult, className: string): TemplateResult {
return html`<span class=${className} aria-hidden="true">${iconContent}</span>`;
}
export function setEmojiIcon(target: HTMLElement | null, icon: string): void {

View File

@@ -1,3 +1,5 @@
import type { IconName } from "./icons.js";
export const TAB_GROUPS = [
{ label: "Chat", tabs: ["chat"] },
{
@@ -98,32 +100,32 @@ export function inferBasePathFromPathname(pathname: string): string {
return `/${segments.join("/")}`;
}
export function iconForTab(tab: Tab): string {
export function iconForTab(tab: Tab): IconName {
switch (tab) {
case "chat":
return "💬";
return "messageSquare";
case "overview":
return "📊";
return "barChart";
case "channels":
return "🔗";
return "link";
case "instances":
return "📡";
return "radio";
case "sessions":
return "📄";
return "fileText";
case "cron":
return "";
return "loader";
case "skills":
return "⚡️";
return "zap";
case "nodes":
return "🖥️";
return "monitor";
case "config":
return "⚙️";
return "settings";
case "debug":
return "🐞";
return "bug";
case "logs":
return "🧾";
return "scrollText";
default:
return "📁";
return "folder";
}
}

View File

@@ -1,7 +1,7 @@
{
"version": 1,
"fallback": {
"emoji": "🧩",
"icon": "puzzle",
"detailKeys": [
"command",
"path",
@@ -26,37 +26,37 @@
},
"tools": {
"bash": {
"emoji": "🛠️",
"icon": "wrench",
"title": "Bash",
"detailKeys": ["command"]
},
"process": {
"emoji": "🧰",
"icon": "wrench",
"title": "Process",
"detailKeys": ["sessionId"]
},
"read": {
"emoji": "📖",
"icon": "fileText",
"title": "Read",
"detailKeys": ["path"]
},
"write": {
"emoji": "✍️",
"icon": "edit",
"title": "Write",
"detailKeys": ["path"]
},
"edit": {
"emoji": "📝",
"icon": "penLine",
"title": "Edit",
"detailKeys": ["path"]
},
"attach": {
"emoji": "📎",
"icon": "paperclip",
"title": "Attach",
"detailKeys": ["path", "url", "fileName"]
},
"browser": {
"emoji": "🌐",
"icon": "globe",
"title": "Browser",
"actions": {
"status": { "label": "status" },
@@ -95,7 +95,7 @@
}
},
"canvas": {
"emoji": "🖼️",
"icon": "image",
"title": "Canvas",
"actions": {
"present": { "label": "present", "detailKeys": ["target", "node", "nodeId"] },
@@ -108,7 +108,7 @@
}
},
"nodes": {
"emoji": "📱",
"icon": "smartphone",
"title": "Nodes",
"actions": {
"status": { "label": "status" },
@@ -127,7 +127,7 @@
}
},
"cron": {
"emoji": "",
"icon": "loader",
"title": "Cron",
"actions": {
"status": { "label": "status" },
@@ -144,7 +144,7 @@
}
},
"gateway": {
"emoji": "🔌",
"icon": "plug",
"title": "Gateway",
"actions": {
"restart": { "label": "restart", "detailKeys": ["reason", "delayMs"] },
@@ -161,7 +161,7 @@
}
},
"whatsapp_login": {
"emoji": "🟢",
"icon": "circle",
"title": "WhatsApp Login",
"actions": {
"start": { "label": "start" },
@@ -169,7 +169,7 @@
}
},
"discord": {
"emoji": "💬",
"icon": "messageSquare",
"title": "Discord",
"actions": {
"react": { "label": "react", "detailKeys": ["channelId", "messageId", "emoji"] },
@@ -204,7 +204,7 @@
}
},
"slack": {
"emoji": "💬",
"icon": "messageSquare",
"title": "Slack",
"actions": {
"react": { "label": "react", "detailKeys": ["channelId", "messageId", "emoji"] },

View File

@@ -1,4 +1,5 @@
import rawConfig from "./tool-display.json";
import type { IconName } from "./icons";
type ToolDisplayActionSpec = {
label?: string;
@@ -6,7 +7,7 @@ type ToolDisplayActionSpec = {
};
type ToolDisplaySpec = {
emoji?: string;
icon?: string;
title?: string;
label?: string;
detailKeys?: string[];
@@ -21,7 +22,7 @@ type ToolDisplayConfig = {
export type ToolDisplay = {
name: string;
emoji: string;
icon: IconName;
title: string;
label: string;
verb?: string;
@@ -29,7 +30,7 @@ export type ToolDisplay = {
};
const TOOL_DISPLAY_CONFIG = rawConfig as ToolDisplayConfig;
const FALLBACK = TOOL_DISPLAY_CONFIG.fallback ?? { emoji: "🧩" };
const FALLBACK = TOOL_DISPLAY_CONFIG.fallback ?? { icon: "puzzle" };
const TOOL_MAP = TOOL_DISPLAY_CONFIG.tools ?? {};
function normalizeToolName(name?: string): string {
@@ -135,7 +136,7 @@ export function resolveToolDisplay(params: {
const name = normalizeToolName(params.name);
const key = name.toLowerCase();
const spec = TOOL_MAP[key];
const emoji = spec?.emoji ?? FALLBACK.emoji ?? "🧩";
const icon = (spec?.icon ?? FALLBACK.icon ?? "puzzle") as IconName;
const title = spec?.title ?? defaultTitle(name);
const label = spec?.label ?? name;
const actionRaw =
@@ -168,7 +169,7 @@ export function resolveToolDisplay(params: {
return {
name,
emoji,
icon,
title,
label,
verb,
@@ -186,9 +187,7 @@ export function formatToolDetail(display: ToolDisplay): string | undefined {
export function formatToolSummary(display: ToolDisplay): string {
const detail = formatToolDetail(display);
return detail
? `${display.emoji} ${display.label}: ${detail}`
: `${display.emoji} ${display.label}`;
return detail ? `${display.label}: ${detail}` : display.label;
}
function shortenHomeInString(input: string): string {

View File

@@ -3,6 +3,7 @@ import { repeat } from "lit/directives/repeat.js";
import type { SessionsListResult } from "../types";
import type { ChatQueueItem } from "../ui-types";
import type { ChatItem, MessageGroup } from "../types/chat-types";
import { icons } from "../icons";
import {
normalizeMessage,
normalizeRoleForGrouping,
@@ -74,18 +75,18 @@ function renderCompactionIndicator(status: CompactionIndicatorStatus | null | un
if (status.active) {
return html`
<div class="callout info compaction-indicator compaction-indicator--active">
🧹 Compacting context...
${icons.loader} Compacting context...
</div>
`;
}
// Show "compaction complete" briefly after completion
if (status.completedAt) {
const elapsed = Date.now() - status.completedAt;
if (elapsed < COMPACTION_TOAST_DURATION_MS) {
return html`
<div class="callout success compaction-indicator compaction-indicator--complete">
🧹 Context compacted
${icons.check} Context compacted
</div>
`;
}
@@ -171,7 +172,7 @@ export function renderChat(props: ChatProps) {
aria-label="Exit focus mode"
title="Exit focus mode"
>
${icons.x}
</button>
`
: nothing}
@@ -223,7 +224,7 @@ export function renderChat(props: ChatProps) {
aria-label="Remove queued message"
@click=${() => props.onQueueRemove(item.id)}
>
${icons.x}
</button>
</div>
`,
@@ -265,7 +266,7 @@ export function renderChat(props: ChatProps) {
?disabled=${!props.connected}
@click=${props.onSend}
>
${isBusy ? "Queue" : "Send"}
${isBusy ? "Queue" : "Send"}<kbd class="btn-kbd">↵</kbd>
</button>
</div>
</div>

View File

@@ -1,5 +1,6 @@
import { html, nothing } from "lit";
import type { ConfigUiHints } from "../types";
import { icons } from "../icons";
import {
hintForPath,
humanize,
@@ -189,7 +190,7 @@ export function renderConfigForm(props: ConfigFormProps) {
if (filteredEntries.length === 0) {
return html`
<div class="config-empty">
<div class="config-empty__icon">🔍</div>
<div class="config-empty__icon">${icons.search}</div>
<div class="config-empty__text">
${searchQuery
? `No settings match "${searchQuery}"`

View File

@@ -1,6 +1,7 @@
import { html, nothing } from "lit";
import { unsafeHTML } from "lit/directives/unsafe-html.js";
import { icons } from "../icons";
import { toSanitizedMarkdownHtml } from "../markdown";
export type MarkdownSidebarProps = {
@@ -16,7 +17,7 @@ export function renderMarkdownSidebar(props: MarkdownSidebarProps) {
<div class="sidebar-header">
<div class="sidebar-title">Tool Output</div>
<button @click=${props.onClose} class="btn" title="Close sidebar">
${icons.x}
</button>
</div>
<div class="sidebar-content">