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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

3119
dist/control-ui/assets/index-DQcOTEYz.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -6,8 +6,8 @@
<title>Clawdbot Control</title>
<meta name="color-scheme" content="dark light" />
<link rel="icon" href="./favicon.ico" sizes="any" />
<script type="module" crossorigin src="./assets/index-DsXRcnEw.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-BvhR9FCb.css">
<script type="module" crossorigin src="./assets/index-DQcOTEYz.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-08nzABV3.css">
</head>
<body>
<clawdbot-app></clawdbot-app>

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">