This commit is contained in:
David Gelberg 2026-01-30 12:34:20 +00:00 committed by GitHub
commit 431173e5df
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 808 additions and 108 deletions

View File

@ -1,40 +1,40 @@
@import url("https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap");
:root {
/* Background - Warmer dark with depth */
--bg: #12141a;
--bg-accent: #14161d;
--bg-elevated: #1a1d25;
--bg-hover: #262a35;
--bg-muted: #262a35;
/* Background - Warm dark with sepia undertone */
--bg: #0D0C07;
--bg-accent: #11100A;
--bg-elevated: #1B1A16;
--bg-hover: #252419;
--bg-muted: #252419;
/* Card / Surface - More contrast between levels */
--card: #181b22;
--card-foreground: #f4f4f5;
--card-highlight: rgba(255, 255, 255, 0.05);
--popover: #181b22;
--popover-foreground: #f4f4f5;
/* Card / Surface - Clear hierarchy with warm tones */
--card: #151410;
--card-foreground: #f4f4f0;
--card-highlight: rgba(255, 252, 245, 0.05);
--popover: #151410;
--popover-foreground: #f4f4f0;
/* Panel */
--panel: #12141a;
--panel-strong: #1a1d25;
--panel-hover: #262a35;
--chrome: rgba(18, 20, 26, 0.95);
--chrome-strong: rgba(18, 20, 26, 0.98);
--panel: #0D0C07;
--panel-strong: #1B1A16;
--panel-hover: #252419;
--chrome: rgba(13, 12, 7, 0.95);
--chrome-strong: rgba(13, 12, 7, 0.98);
/* Text - Slightly warmer */
--text: #e4e4e7;
--text-strong: #fafafa;
--chat-text: #e4e4e7;
--muted: #71717a;
--muted-strong: #52525b;
--muted-foreground: #71717a;
/* Text - Warm white tones */
--text: #e8e6e0;
--text-strong: #faf9f6;
--chat-text: #e8e6e0;
--muted: #7a776d;
--muted-strong: #5c5952;
--muted-foreground: #7a776d;
/* Border - Subtle but defined */
--border: #27272a;
--border-strong: #3f3f46;
--border-hover: #52525b;
--input: #27272a;
/* Border - Warm and subtle */
--border: #2a2820;
--border-strong: #3d3a30;
--border-hover: #524f42;
--input: #2a2820;
--ring: #ff5c5c;
/* Accent - Punchy signature red */
@ -47,9 +47,9 @@
--primary: #ff5c5c;
--primary-foreground: #ffffff;
/* Secondary - Teal accent for variety */
--secondary: #1e2028;
--secondary-foreground: #f4f4f5;
/* Secondary - Warm elevated surface */
--secondary: #1B1A16;
--secondary-foreground: #f4f4f0;
--accent-2: #14b8a6;
--accent-2-muted: rgba(20, 184, 166, 0.7);
--accent-2-subtle: rgba(20, 184, 166, 0.15);
@ -74,7 +74,7 @@
--focus-glow: 0 0 0 2px var(--bg), 0 0 0 4px var(--ring), 0 0 20px var(--accent-glow);
/* Grid */
--grid-line: rgba(255, 255, 255, 0.04);
--grid-line: rgba(255, 252, 245, 0.04);
/* Theme transition */
--theme-switch-x: 50%;
@ -111,77 +111,85 @@
color-scheme: dark;
}
/* Light theme - Clean with subtle warmth */
/* Light theme - Premium with warm gray tones and clear hierarchy */
:root[data-theme="light"] {
--bg: #fafafa;
--bg-accent: #f5f5f5;
/* Warm gray backgrounds with clear hierarchy */
--bg: #f8f8f7;
--bg-accent: #f3f3f1;
--bg-elevated: #ffffff;
--bg-hover: #f0f0f0;
--bg-muted: #f0f0f0;
--bg-content: #f5f5f5;
--bg-hover: #ebebea;
--bg-muted: #eeeeed;
--bg-content: #f3f3f1;
/* Cards stand out clearly against background */
--card: #ffffff;
--card-foreground: #18181b;
--card-highlight: rgba(0, 0, 0, 0.03);
--card-foreground: #1a1a1a;
--card-highlight: rgba(255, 255, 255, 0.8);
--popover: #ffffff;
--popover-foreground: #18181b;
--popover-foreground: #1a1a1a;
--panel: #fafafa;
--panel-strong: #f5f5f5;
--panel-hover: #ebebeb;
--chrome: rgba(250, 250, 250, 0.95);
--chrome-strong: rgba(250, 250, 250, 0.98);
/* Panel - subtle warm tint */
--panel: #f8f8f7;
--panel-strong: #f3f3f1;
--panel-hover: #e8e8e6;
--chrome: rgba(248, 248, 247, 0.95);
--chrome-strong: rgba(248, 248, 247, 0.98);
--text: #3f3f46;
--text-strong: #18181b;
--chat-text: #3f3f46;
--muted: #71717a;
--muted-strong: #52525b;
--muted-foreground: #71717a;
/* Text - rich contrast */
--text: #3d3d3d;
--text-strong: #1a1a1a;
--chat-text: #3d3d3d;
--muted: #6b6b6b;
--muted-strong: #525252;
--muted-foreground: #6b6b6b;
--border: #e4e4e7;
--border-strong: #d4d4d8;
--border-hover: #a1a1aa;
--input: #e4e4e7;
/* Border - subtle definition with warm undertone */
--border: #e0e0de;
--border-strong: #c8c8c5;
--border-hover: #a0a09d;
--input: #e0e0de;
/* Accent - confident red */
--accent: #dc2626;
--accent-hover: #ef4444;
--accent-muted: #dc2626;
--accent-subtle: rgba(220, 38, 38, 0.12);
--accent-subtle: rgba(220, 38, 38, 0.1);
--accent-foreground: #ffffff;
--accent-glow: rgba(220, 38, 38, 0.15);
--accent-glow: rgba(220, 38, 38, 0.12);
--primary: #dc2626;
--primary-foreground: #ffffff;
--secondary: #f4f4f5;
--secondary-foreground: #3f3f46;
/* Secondary - warm off-white */
--secondary: #f0f0ee;
--secondary-foreground: #3d3d3d;
--accent-2: #0d9488;
--accent-2-muted: rgba(13, 148, 136, 0.75);
--accent-2-subtle: rgba(13, 148, 136, 0.12);
--accent-2-subtle: rgba(13, 148, 136, 0.1);
/* Semantic colors - slightly muted for light theme */
--ok: #16a34a;
--ok-muted: rgba(22, 163, 74, 0.75);
--ok-subtle: rgba(22, 163, 74, 0.1);
--ok-subtle: rgba(22, 163, 74, 0.08);
--destructive: #dc2626;
--destructive-foreground: #fafafa;
--warn: #d97706;
--warn-muted: rgba(217, 119, 6, 0.75);
--warn-subtle: rgba(217, 119, 6, 0.1);
--warn-subtle: rgba(217, 119, 6, 0.08);
--danger: #dc2626;
--danger-muted: rgba(220, 38, 38, 0.75);
--danger-subtle: rgba(220, 38, 38, 0.1);
--danger-subtle: rgba(220, 38, 38, 0.08);
--info: #2563eb;
--focus: rgba(220, 38, 38, 0.2);
--focus-glow: 0 0 0 2px var(--bg), 0 0 0 4px var(--ring), 0 0 16px var(--accent-glow);
--grid-line: rgba(0, 0, 0, 0.05);
--grid-line: rgba(0, 0, 0, 0.04);
/* Light shadows */
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.06);
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.08), 0 0 0 1px rgba(0, 0, 0, 0.04);
--shadow-lg: 0 12px 28px rgba(0, 0, 0, 0.12), 0 0 0 1px rgba(0, 0, 0, 0.04);
--shadow-xl: 0 24px 48px rgba(0, 0, 0, 0.15), 0 0 0 1px rgba(0, 0, 0, 0.04);
/* Light shadows - crisper with subtle warmth */
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05), 0 1px 1px rgba(0, 0, 0, 0.03);
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.06), 0 1px 3px rgba(0, 0, 0, 0.04);
--shadow-lg: 0 12px 28px rgba(0, 0, 0, 0.08), 0 4px 8px rgba(0, 0, 0, 0.04);
--shadow-xl: 0 24px 48px rgba(0, 0, 0, 0.1), 0 8px 16px rgba(0, 0, 0, 0.05);
--shadow-glow: 0 0 24px var(--accent-glow);
color-scheme: light;
@ -206,36 +214,17 @@ body {
-moz-osx-font-smoothing: grayscale;
}
/* Theme transition */
@keyframes theme-circle-transition {
0% {
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%));
}
}
html.theme-transition {
view-transition-name: theme;
}
html.theme-transition::view-transition-old(theme) {
mix-blend-mode: normal;
animation: none;
z-index: 1;
}
html.theme-transition::view-transition-new(theme) {
mix-blend-mode: normal;
z-index: 2;
animation: theme-circle-transition 0.4s var(--ease-out) forwards;
/* Theme transition - clean crossfade */
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 0.15s;
animation-timing-function: ease-out;
}
@media (prefers-reduced-motion: reduce) {
html.theme-transition::view-transition-old(theme),
html.theme-transition::view-transition-new(theme) {
animation: none !important;
::view-transition-old(root),
::view-transition-new(root) {
animation: none;
}
}

View File

@ -344,6 +344,12 @@
color: var(--text);
}
:root[data-theme="light"] .btn--icon.active {
border-color: var(--accent);
background: var(--accent-subtle);
color: var(--accent);
}
.btn--icon svg {
display: block;
width: 18px;

View File

@ -49,6 +49,8 @@
border-color var(--duration-normal) var(--ease-out),
box-shadow var(--duration-normal) var(--ease-out);
box-shadow: inset 0 1px 0 var(--card-highlight);
min-width: 0;
overflow: hidden;
}
.stat:hover {
@ -62,6 +64,9 @@
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.04em;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.stat-value {
@ -70,6 +75,9 @@
margin-top: 6px;
letter-spacing: -0.03em;
line-height: 1.1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.stat-value.ok {
@ -201,6 +209,30 @@
color: var(--danger);
}
/* Health pill - prominent status indicator */
.pill--health {
font-weight: 600;
letter-spacing: -0.01em;
}
.pill--health.ok {
border-color: rgba(34, 197, 94, 0.2);
background: var(--ok-subtle);
}
.pill--health.ok .mono {
color: var(--ok);
}
.pill--health.offline {
border-color: rgba(245, 158, 11, 0.2);
background: var(--warn-subtle);
}
.pill--health.offline .mono {
color: var(--warn);
}
/* ===========================================
Theme Toggle
=========================================== */
@ -326,7 +358,7 @@
.btn:active {
background: var(--secondary);
transform: translateY(0);
transform: translateY(0) scale(0.98);
box-shadow: none;
}
@ -447,6 +479,14 @@
.field select:focus {
border-color: var(--ring);
box-shadow: var(--focus-ring);
outline: none;
}
.field input:focus-visible,
.field textarea:focus-visible,
.field select:focus-visible {
border-color: var(--ring);
box-shadow: var(--focus-glow);
}
.field select {
@ -467,16 +507,49 @@
}
.field.checkbox {
grid-template-columns: auto 1fr;
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
}
.field.checkbox span {
order: 2;
}
.field.checkbox input[type="checkbox"] {
order: 1;
width: 16px;
height: 16px;
margin: 0;
accent-color: var(--accent);
cursor: pointer;
}
/* Checkboxes in filter rows - align to bottom like other fields */
.filters .field.checkbox {
align-self: flex-end;
padding-bottom: 10px;
}
/* Checkboxes in form grids - align to bottom of grid cell */
.form-grid .field.checkbox {
align-self: end;
padding-bottom: 8px;
}
.config-form .field.checkbox {
display: grid;
grid-template-columns: 18px minmax(0, 1fr);
column-gap: 10px;
}
.config-form .field.checkbox span {
order: unset;
}
.config-form .field.checkbox input[type="checkbox"] {
order: unset;
margin: 0;
width: 16px;
height: 16px;
@ -522,6 +595,39 @@
font-family: var(--mono);
}
/* ===========================================
Keyboard Shortcuts (kbd)
=========================================== */
kbd {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 20px;
height: 20px;
padding: 0 6px;
font-family: var(--font-body);
font-size: 11px;
font-weight: 500;
line-height: 1;
color: var(--muted);
background: var(--secondary);
border: 1px solid var(--border);
border-bottom-width: 2px;
border-radius: 4px;
box-shadow: 0 1px 0 var(--border);
}
kbd + kbd {
margin-left: 4px;
}
:root[data-theme="light"] kbd {
background: var(--bg);
border-color: var(--border-strong);
box-shadow: 0 1px 0 var(--border-strong);
}
/* ===========================================
Callouts - Informative with subtle depth
=========================================== */
@ -554,6 +660,156 @@
color: var(--ok);
}
.callout.warn {
border-color: rgba(245, 158, 11, 0.25);
background: linear-gradient(135deg, rgba(245, 158, 11, 0.08) 0%, rgba(245, 158, 11, 0.04) 100%);
color: var(--warn);
}
/* ===========================================
Empty States - Clear and helpful
=========================================== */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12px;
padding: 48px 24px;
text-align: center;
color: var(--muted);
}
.empty-state__icon {
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--radius-lg);
background: var(--secondary);
border: 1px solid var(--border);
color: var(--muted);
margin-bottom: 4px;
}
.empty-state__icon svg {
width: 24px;
height: 24px;
stroke: currentColor;
fill: none;
stroke-width: 1.5px;
stroke-linecap: round;
stroke-linejoin: round;
}
.empty-state__title {
font-size: 15px;
font-weight: 600;
color: var(--text);
letter-spacing: -0.01em;
}
.empty-state__description {
font-size: 13px;
max-width: 320px;
line-height: 1.5;
}
.empty-state__action {
margin-top: 8px;
}
/* Disconnected state - More prominent */
.disconnected-state {
display: flex;
align-items: center;
gap: 14px;
padding: 16px 20px;
border-radius: var(--radius-lg);
border: 1px dashed var(--border-strong);
background: var(--secondary);
animation: rise 0.35s var(--ease-out) backwards;
}
.disconnected-state__icon {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--radius-md);
background: var(--warn-subtle);
color: var(--warn);
flex-shrink: 0;
}
.disconnected-state__icon svg {
width: 20px;
height: 20px;
stroke: currentColor;
fill: none;
stroke-width: 2px;
stroke-linecap: round;
stroke-linejoin: round;
}
.disconnected-state__content {
flex: 1;
min-width: 0;
}
.disconnected-state__title {
font-size: 14px;
font-weight: 600;
color: var(--text);
letter-spacing: -0.01em;
}
.disconnected-state__description {
font-size: 13px;
color: var(--muted);
margin-top: 2px;
}
.disconnected-state__action {
flex-shrink: 0;
}
/* Loading state placeholder */
.loading-placeholder {
display: flex;
flex-direction: column;
gap: 12px;
padding: 24px;
}
.loading-placeholder__bar {
height: 12px;
border-radius: var(--radius-sm);
background: linear-gradient(
90deg,
var(--secondary) 25%,
var(--bg-hover) 50%,
var(--secondary) 75%
);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
}
.loading-placeholder__bar--short {
width: 60%;
}
.loading-placeholder__bar--medium {
width: 80%;
}
.loading-placeholder__bar--full {
width: 100%;
}
/* Compaction indicator */
.compaction-indicator {
font-size: 13px;
@ -603,6 +859,23 @@
background: var(--bg);
}
/* Light theme cards - subtle shadow for elevation */
:root[data-theme="light"] .card {
box-shadow: var(--shadow-sm);
}
:root[data-theme="light"] .card:hover {
box-shadow: var(--shadow-md);
}
:root[data-theme="light"] .stat {
box-shadow: var(--shadow-sm);
}
:root[data-theme="light"] .stat:hover {
box-shadow: var(--shadow-md);
}
/* ===========================================
Lists
=========================================== */
@ -1484,3 +1757,137 @@
flex-wrap: wrap;
gap: 8px;
}
/* ===========================================
Crab Loader - Processing animation
=========================================== */
.crab-loader {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 20px;
padding: 32px 24px;
}
.crab-loader__crab {
position: relative;
animation: crab-walk 0.6s ease-in-out infinite;
}
.crab-loader__svg {
display: block;
}
.crab-loader--sm .crab-loader__svg {
width: 32px;
height: auto;
}
.crab-loader--md .crab-loader__svg {
width: 56px;
height: auto;
}
.crab-loader--lg .crab-loader__svg {
width: 96px;
height: auto;
}
.crab-loader__claw-left {
transform-origin: 52px 78px;
animation: claw-pinch-left 0.4s ease-in-out infinite;
}
.crab-loader__claw-right {
transform-origin: 468px 78px;
animation: claw-pinch-right 0.4s ease-in-out infinite;
}
.crab-loader__text {
display: flex;
align-items: center;
gap: 4px;
color: var(--muted);
font-size: 14px;
font-weight: 500;
}
.crab-loader__dots {
display: flex;
gap: 3px;
margin-left: 2px;
}
.crab-loader__dot {
width: 4px;
height: 4px;
border-radius: var(--radius-full);
background: var(--muted);
animation: dot-bounce 1.2s ease-in-out infinite;
}
.crab-loader__dot:nth-child(2) {
animation-delay: 0.2s;
}
.crab-loader__dot:nth-child(3) {
animation-delay: 0.4s;
}
/* Crab walking animation - side to side sway */
@keyframes crab-walk {
0%, 100% {
transform: translateX(-2px) rotate(-2deg);
}
50% {
transform: translateX(2px) rotate(2deg);
}
}
/* Left claw pinching */
@keyframes claw-pinch-left {
0%, 100% {
transform: rotate(0deg);
}
50% {
transform: rotate(15deg);
}
}
/* Right claw pinching */
@keyframes claw-pinch-right {
0%, 100% {
transform: rotate(0deg);
}
50% {
transform: rotate(-15deg);
}
}
/* Bouncing dots */
@keyframes dot-bounce {
0%, 80%, 100% {
opacity: 0.4;
transform: translateY(0);
}
40% {
opacity: 1;
transform: translateY(-3px);
}
}
/* Reduced motion */
@media (prefers-reduced-motion: reduce) {
.crab-loader__crab,
.crab-loader__claw-left,
.crab-loader__claw-right,
.crab-loader__dot {
animation: none;
}
.crab-loader__dot {
opacity: 0.6;
}
}

View File

@ -430,6 +430,17 @@
background: var(--bg-content);
}
/* Light theme nav - subtle background difference */
:root[data-theme="light"] .nav {
background: var(--bg);
border-right: 1px solid var(--border);
}
:root[data-theme="light"] .topbar {
background: var(--bg);
box-shadow: 0 1px 0 var(--border);
}
.content--chat {
overflow: hidden;
padding-bottom: 0;
@ -437,19 +448,20 @@
/* Content header */
.content-header {
position: relative;
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 16px;
padding: 4px 8px;
overflow: hidden;
padding: 4px 8px 12px;
overflow: visible;
transform-origin: top center;
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: 80px;
max-height: 90px;
}
.shell--chat-focus .content-header {
@ -481,6 +493,28 @@
gap: 8px;
}
/* Page header separator - subtle gradient fade */
.content-header::after {
content: "";
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 1px;
background: linear-gradient(
90deg,
transparent 0%,
var(--border) 20%,
var(--border) 80%,
transparent 100%
);
opacity: 0.6;
}
.shell--chat-focus .content-header::after {
display: none;
}
/* Chat view header adjustments */
.content--chat .content-header {
flex-direction: row;

View File

@ -2,7 +2,7 @@ import { html } from "lit";
import { repeat } from "lit/directives/repeat.js";
import type { AppViewState } from "./app-view-state";
import { iconForTab, pathForTab, titleForTab, type Tab } from "./navigation";
import { iconForTab, pathForTab, titleForTab, TAB_GROUPS, type Tab } from "./navigation";
import { icons } from "./icons";
import { loadChatHistory } from "./controllers/chat";
import { refreshChat } from "./app-chat";
@ -29,6 +29,13 @@ export function renderTab(state: AppViewState, tab: Tab) {
return;
}
event.preventDefault();
// Auto-expand the group containing this tab
const group = TAB_GROUPS.find((g) => g.tabs.includes(tab));
if (group && state.settings.navGroupsCollapsed[group.label]) {
const next = { ...state.settings.navGroupsCollapsed };
next[group.label] = false;
state.applySettings({ ...state.settings, navGroupsCollapsed: next });
}
state.setTab(tab);
}}
title=${titleForTab(tab)}

View File

@ -139,7 +139,7 @@ export function renderApp(state: AppViewState) {
</div>
</div>
<div class="topbar-status">
<div class="pill">
<div class="pill pill--health ${state.connected ? "ok" : "offline"}">
<span class="statusDot ${state.connected ? "ok" : ""}"></span>
<span>Health</span>
<span class="mono">${state.connected ? "OK" : "Offline"}</span>
@ -150,9 +150,8 @@ export function renderApp(state: AppViewState) {
<aside class="nav ${state.settings.navCollapsed ? "nav--collapsed" : ""}">
${TAB_GROUPS.map((group) => {
const isGroupCollapsed = state.settings.navGroupsCollapsed[group.label] ?? false;
const hasActiveTab = group.tabs.some((tab) => tab === state.tab);
return html`
<div class="nav-group ${isGroupCollapsed && !hasActiveTab ? "nav-group--collapsed" : ""}">
<div class="nav-group ${isGroupCollapsed ? "nav-group--collapsed" : ""}">
<button
class="nav-label"
@click=${() => {

Binary file not shown.

After

Width:  |  Height:  |  Size: 374 KiB

View File

@ -0,0 +1,257 @@
import { html, TemplateResult } from "lit";
export type CrabLoaderSize = "sm" | "md" | "lg";
export type CrabLoaderProps = {
message?: string;
size?: CrabLoaderSize;
};
/**
* Renders the Clawdbot crab loader animation.
*
* @example
* ```ts
* import { renderCrabLoader } from "./components/crab-loader";
*
* // Default usage
* ${renderCrabLoader()}
*
* // With custom message
* ${renderCrabLoader({ message: "Loading sessions" })}
*
* // Different sizes: "sm" | "md" | "lg"
* ${renderCrabLoader({ size: "lg" })}
* ```
*/
export function renderCrabLoader(props: CrabLoaderProps = {}): TemplateResult {
const { message = "Getting my claws on it", size = "md" } = props;
return html`
<div class="crab-loader crab-loader--${size}">
<div class="crab-loader__crab">
${renderCrabSvg()}
</div>
<div class="crab-loader__text">
<span>${message}</span>
<span class="crab-loader__dots">
<span class="crab-loader__dot"></span>
<span class="crab-loader__dot"></span>
<span class="crab-loader__dot"></span>
</span>
</div>
</div>
`;
}
function renderCrabSvg(): TemplateResult {
return html`
<svg
class="crab-loader__svg"
viewBox="0 0 520 442"
fill="none"
xmlns="http://www.w3.org/2000/svg"
shape-rendering="crispEdges"
>
<!-- Left claw -->
<g class="crab-loader__claw-left">
<rect x="26" y="156" width="26" height="26" fill="#F41641" />
<rect y="130" width="26" height="26" fill="#F41641" />
<rect y="104" width="26" height="26" fill="#F41641" />
<rect y="78" width="26" height="26" fill="#F41641" />
<rect y="52" width="26" height="26" fill="#F41641" />
<rect y="26" width="26" height="26" fill="#F41641" />
<rect x="26" y="78" width="26" height="26" fill="#F41641" />
<rect x="52" width="26" height="26" fill="#F41641" />
<rect x="78" width="26" height="26" fill="#F41641" />
<rect x="52" y="26" width="26" height="26" fill="#F41641" />
<rect x="52" y="78" width="26" height="26" fill="#C50026" />
<rect x="78" y="52" width="26" height="26" fill="#C50026" />
<rect x="26" y="52" width="26" height="26" fill="#F41641" />
<rect x="26" y="26" width="26" height="26" fill="#F41641" />
<rect x="26" width="26" height="26" fill="#F41641" />
</g>
<!-- Right claw -->
<g class="crab-loader__claw-right">
<rect width="26" height="26" transform="matrix(-1 0 0 1 520 130)" fill="#F41641" />
<rect width="26" height="26" transform="matrix(-1 0 0 1 520 104)" fill="#F41641" />
<rect width="26" height="26" transform="matrix(-1 0 0 1 520 78)" fill="#F41641" />
<rect width="26" height="26" transform="matrix(-1 0 0 1 520 52)" fill="#F41641" />
<rect width="26" height="26" transform="matrix(-1 0 0 1 520 26)" fill="#F41641" />
<rect width="26" height="26" transform="matrix(-1 0 0 1 494 78)" fill="#F41641" />
<rect width="26" height="26" transform="matrix(-1 0 0 1 468 0)" fill="#F41641" />
<rect width="26" height="26" transform="matrix(-1 0 0 1 442 0)" fill="#F41641" />
<rect width="26" height="26" transform="matrix(-1 0 0 1 468 26)" fill="#F41641" />
<rect width="26" height="26" transform="matrix(-1 0 0 1 468 78)" fill="#C50026" />
<rect width="26" height="26" transform="matrix(-1 0 0 1 442 52)" fill="#C50026" />
<rect width="26" height="26" transform="matrix(-1 0 0 1 494 52)" fill="#F41641" />
<rect width="26" height="26" transform="matrix(-1 0 0 1 494 26)" fill="#F41641" />
<rect width="26" height="26" transform="matrix(-1 0 0 1 494 0)" fill="#F41641" />
</g>
<!-- Eyes -->
<rect x="130" y="182" width="26" height="26" fill="#F41641" />
<rect x="156" y="182" width="26" height="26" fill="#F41641" />
<rect x="156" y="156" width="26" height="26" fill="#F41641" />
<rect x="156" y="130" width="26" height="26" fill="#F41641" />
<rect x="156" y="78" width="26" height="26" fill="#6A0014" />
<rect x="130" y="78" width="26" height="26" fill="#FFC1CD" />
<rect x="130" y="104" width="26" height="26" fill="#FFC1CD" />
<rect x="156" y="104" width="26" height="26" fill="#FFC1CD" />
<rect width="26" height="26" transform="matrix(-1 0 0 1 364 182)" fill="#F41641" />
<rect width="26" height="26" transform="matrix(-1 0 0 1 364 156)" fill="#F41641" />
<rect width="26" height="26" transform="matrix(-1 0 0 1 364 130)" fill="#F41641" />
<rect width="26" height="26" transform="matrix(-1 0 0 1 364 78)" fill="#6A0014" />
<rect width="26" height="26" transform="matrix(-1 0 0 1 390 78)" fill="#FFC1CD" />
<rect width="26" height="26" transform="matrix(-1 0 0 1 390 104)" fill="#FFC1CD" />
<rect width="26" height="26" transform="matrix(-1 0 0 1 364 104)" fill="#FFC1CD" />
<!-- Body -->
<rect x="182" y="182" width="26" height="26" fill="#F41641" />
<rect x="52" y="182" width="26" height="26" fill="#F41641" />
<rect x="78" y="182" width="26" height="26" fill="#F41641" />
<rect x="104" y="182" width="26" height="26" fill="#F41641" />
<rect x="286" y="182" width="26" height="26" fill="#F41641" />
<rect x="286" y="156" width="26" height="26" fill="#F41641" />
<rect x="312" y="182" width="26" height="26" fill="#F41641" />
<rect x="208" y="182" width="26" height="26" fill="#F41641" />
<rect x="208" y="156" width="26" height="26" fill="#F41641" />
<rect x="234" y="182" width="26" height="26" fill="#F41641" />
<rect x="260" y="182" width="26" height="26" fill="#F41641" />
<rect x="416" y="182" width="26" height="26" fill="#F41641" />
<rect x="442" y="182" width="26" height="26" fill="#F41641" />
<rect x="468" y="156" width="26" height="26" fill="#F41641" />
<rect x="364" y="182" width="26" height="26" fill="#F41641" />
<rect x="390" y="182" width="26" height="26" fill="#F41641" />
<!-- Body rows -->
<rect x="130" y="260" width="26" height="26" fill="#F41641" />
<rect x="156" y="260" width="26" height="26" fill="#F41641" />
<rect width="26" height="26" transform="matrix(-1 0 0 1 364 260)" fill="#F41641" />
<rect x="182" y="260" width="26" height="26" fill="#F41641" />
<rect x="78" y="260" width="26" height="26" fill="#F41641" />
<rect x="104" y="260" width="26" height="26" fill="#F41641" />
<rect x="286" y="260" width="26" height="26" fill="#F41641" />
<rect x="312" y="260" width="26" height="26" fill="#F41641" />
<rect x="208" y="260" width="26" height="26" fill="#F41641" />
<rect x="234" y="260" width="26" height="26" fill="#F41641" />
<rect x="260" y="260" width="26" height="26" fill="#F41641" />
<rect x="416" y="260" width="26" height="26" fill="#F41641" />
<rect x="364" y="260" width="26" height="26" fill="#F41641" />
<rect x="390" y="260" width="26" height="26" fill="#F41641" />
<rect x="130" y="234" width="26" height="26" fill="#F41641" />
<rect x="156" y="234" width="26" height="26" fill="#F41641" />
<rect width="26" height="26" transform="matrix(-1 0 0 1 364 234)" fill="#F41641" />
<rect x="182" y="234" width="26" height="26" fill="#F41641" />
<rect x="78" y="234" width="26" height="26" fill="#F41641" />
<rect x="104" y="234" width="26" height="26" fill="#F41641" />
<rect x="286" y="234" width="26" height="26" fill="#F41641" />
<rect x="312" y="234" width="26" height="26" fill="#F41641" />
<rect x="208" y="234" width="26" height="26" fill="#F41641" />
<rect x="234" y="234" width="26" height="26" fill="#F41641" />
<rect x="260" y="234" width="26" height="26" fill="#F41641" />
<rect x="416" y="234" width="26" height="26" fill="#F41641" />
<rect x="364" y="234" width="26" height="26" fill="#F41641" />
<rect x="390" y="234" width="26" height="26" fill="#F41641" />
<rect x="130" y="208" width="26" height="26" fill="#F41641" />
<rect x="156" y="208" width="26" height="26" fill="#F41641" />
<rect width="26" height="26" transform="matrix(-1 0 0 1 364 208)" fill="#F41641" />
<rect x="182" y="208" width="26" height="26" fill="#F41641" />
<rect x="78" y="208" width="26" height="26" fill="#F41641" />
<rect x="104" y="208" width="26" height="26" fill="#F41641" />
<rect x="286" y="208" width="26" height="26" fill="#F41641" />
<rect x="312" y="208" width="26" height="26" fill="#F41641" />
<rect x="208" y="208" width="26" height="26" fill="#F41641" />
<rect x="234" y="208" width="26" height="26" fill="#F41641" />
<rect x="260" y="208" width="26" height="26" fill="#F41641" />
<rect x="416" y="208" width="26" height="26" fill="#F41641" />
<rect x="364" y="208" width="26" height="26" fill="#F41641" />
<rect x="390" y="208" width="26" height="26" fill="#F41641" />
<!-- Lower body and legs -->
<rect x="130" y="390" width="26" height="26" fill="#F41641" />
<rect x="156" y="390" width="26" height="26" fill="#F41641" />
<rect width="26" height="26" transform="matrix(-1 0 0 1 364 390)" fill="#F41641" />
<rect x="182" y="390" width="26" height="26" fill="#F41641" />
<rect x="104" y="390" width="26" height="26" fill="#F41641" />
<rect x="286" y="390" width="26" height="26" fill="#F41641" />
<rect x="312" y="390" width="26" height="26" fill="#F41641" />
<rect x="208" y="390" width="26" height="26" fill="#F41641" />
<rect x="234" y="390" width="26" height="26" fill="#F41641" />
<rect x="260" y="390" width="26" height="26" fill="#F41641" />
<rect x="364" y="390" width="26" height="26" fill="#F41641" />
<rect x="390" y="390" width="26" height="26" fill="#F41641" />
<rect x="130" y="364" width="26" height="26" fill="#F41641" />
<rect x="156" y="364" width="26" height="26" fill="#F41641" />
<rect width="26" height="26" transform="matrix(-1 0 0 1 364 364)" fill="#F41641" />
<rect x="182" y="364" width="26" height="26" fill="#F41641" />
<rect x="78" y="364" width="26" height="26" fill="#F41641" />
<rect x="104" y="364" width="26" height="26" fill="#F41641" />
<rect x="286" y="364" width="26" height="26" fill="#F41641" />
<rect x="312" y="364" width="26" height="26" fill="#F41641" />
<rect x="208" y="364" width="26" height="26" fill="#F41641" />
<rect x="234" y="364" width="26" height="26" fill="#F41641" />
<rect x="260" y="364" width="26" height="26" fill="#F41641" />
<rect x="416" y="364" width="26" height="26" fill="#F41641" />
<rect x="364" y="364" width="26" height="26" fill="#F41641" />
<rect x="390" y="364" width="26" height="26" fill="#F41641" />
<rect x="130" y="338" width="26" height="26" fill="#F41641" />
<rect x="156" y="338" width="26" height="26" fill="#F41641" />
<rect width="26" height="26" transform="matrix(-1 0 0 1 364 338)" fill="#F41641" />
<rect x="182" y="338" width="26" height="26" fill="#F41641" />
<rect x="78" y="338" width="26" height="26" fill="#F41641" />
<rect x="104" y="338" width="26" height="26" fill="#F41641" />
<rect x="286" y="338" width="26" height="26" fill="#F41641" />
<rect x="312" y="338" width="26" height="26" fill="#F41641" />
<rect x="208" y="338" width="26" height="26" fill="#F41641" />
<rect x="234" y="338" width="26" height="26" fill="#F41641" />
<rect x="260" y="338" width="26" height="26" fill="#F41641" />
<rect x="416" y="338" width="26" height="26" fill="#F41641" />
<rect x="442" y="338" width="26" height="26" fill="#F41641" />
<rect x="468" y="338" width="26" height="26" fill="#F41641" />
<rect x="494" y="364" width="26" height="26" fill="#F41641" />
<rect x="494" y="390" width="26" height="26" fill="#F41641" />
<rect x="494" y="416" width="26" height="26" fill="#F41641" />
<rect x="442" y="390" width="26" height="26" fill="#F41641" />
<rect x="442" y="416" width="26" height="26" fill="#F41641" />
<rect width="26" height="26" transform="matrix(-1 0 0 1 78 338)" fill="#F41641" />
<rect width="26" height="26" transform="matrix(-1 0 0 1 52 338)" fill="#F41641" />
<rect width="26" height="26" transform="matrix(-1 0 0 1 26 364)" fill="#F41641" />
<rect width="26" height="26" transform="matrix(-1 0 0 1 26 390)" fill="#F41641" />
<rect width="26" height="26" transform="matrix(-1 0 0 1 26 416)" fill="#F41641" />
<rect width="26" height="26" transform="matrix(-1 0 0 1 78 390)" fill="#F41641" />
<rect width="26" height="26" transform="matrix(-1 0 0 1 78 416)" fill="#F41641" />
<rect x="364" y="338" width="26" height="26" fill="#F41641" />
<rect x="390" y="338" width="26" height="26" fill="#F41641" />
<rect x="130" y="312" width="26" height="26" fill="#F41641" />
<rect x="156" y="312" width="26" height="26" fill="#F41641" />
<rect width="26" height="26" transform="matrix(-1 0 0 1 364 312)" fill="#F41641" />
<rect x="182" y="312" width="26" height="26" fill="#F41641" />
<rect x="78" y="312" width="26" height="26" fill="#F41641" />
<rect x="104" y="312" width="26" height="26" fill="#F41641" />
<rect x="286" y="312" width="26" height="26" fill="#F41641" />
<rect x="312" y="312" width="26" height="26" fill="#F41641" />
<rect x="208" y="312" width="26" height="26" fill="#F41641" />
<rect x="234" y="312" width="26" height="26" fill="#F41641" />
<rect x="260" y="312" width="26" height="26" fill="#F41641" />
<rect x="416" y="312" width="26" height="26" fill="#F41641" />
<rect x="364" y="312" width="26" height="26" fill="#F41641" />
<rect x="390" y="312" width="26" height="26" fill="#F41641" />
<rect x="130" y="286" width="26" height="26" fill="#F41641" />
<rect x="156" y="286" width="26" height="26" fill="#F41641" />
<rect width="26" height="26" transform="matrix(-1 0 0 1 364 286)" fill="#F41641" />
<rect x="182" y="286" width="26" height="26" fill="#F41641" />
<rect x="78" y="286" width="26" height="26" fill="#F41641" />
<rect x="104" y="286" width="26" height="26" fill="#F41641" />
<rect x="286" y="286" width="26" height="26" fill="#F41641" />
<rect x="312" y="286" width="26" height="26" fill="#F41641" />
<rect x="208" y="286" width="26" height="26" fill="#F41641" />
<rect x="234" y="286" width="26" height="26" fill="#F41641" />
<rect x="260" y="286" width="26" height="26" fill="#F41641" />
<rect x="416" y="286" width="26" height="26" fill="#F41641" />
<rect x="364" y="286" width="26" height="26" fill="#F41641" />
<rect x="390" y="286" width="26" height="26" fill="#F41641" />
</svg>
`;
}

View File

@ -117,13 +117,13 @@ export function renderCron(props: CronProps) {
/>
</label>
<label class="field checkbox">
<span>Enabled</span>
<input
type="checkbox"
.checked=${props.form.enabled}
@change=${(e: Event) =>
props.onFormChange({ enabled: (e.target as HTMLInputElement).checked })}
/>
<span>Enabled</span>
</label>
<label class="field">
<span>Schedule</span>

View File

@ -257,5 +257,6 @@ export function renderOverview(props: OverviewProps) {
</div>
</div>
</section>
`;
}