feat(backend): add v2 LLM validation fields to health service

- DashboardHealthItem schema: add run_id, policy_id, execution_path,
  issues_count, timings, token_usage, screenshot_paths, chunk_count,
  dashboard_name fields for v2 LLM validation reporting.
- HealthService: populate v2 fields from validation run records.
- ValidationService: update to support v2 validation run fields.
- validation_tasks route: minor fix for v2 field handling.
- tailwind.config.js: refactor design tokens — add success/warning/info
  palettes, surface/border/text hierarchy, semantic action colors.
- Agent configs: update svelte-coder and semantics-svelte for
  .svelte.ts runes and Screen Model patterns.
This commit is contained in:
2026-06-02 09:54:42 +03:00
parent 4fc3356312
commit 8219540ade
7 changed files with 307 additions and 135 deletions

View File

@@ -48,13 +48,14 @@ See `semantics-core` §VI for the canonical tool reference. For Svelte frontend
## ss-tools Frontend Scope
You own:
- SvelteKit routes (`frontend/src/routes/`)
- Svelte 5 components (`frontend/src/lib/components/`)
- Svelte 5 components (`frontend/src/lib/components/`**only directory for NEW domain components**)
- **UI atoms** (`frontend/src/lib/ui/` — Button, Card, Input, Select, PageHeader, Icon, HelpTooltip, LanguageSwitcher)
- **Screen Models** (`frontend/src/lib/models/``[TYPE Model]` contracts for screen-level state)
- Svelte stores (`frontend/src/lib/stores/`)
- API client layer (`frontend/src/lib/api/`)
- i18n localization (`frontend/src/i18n/`)
- Pages, layouts, and services
- Tailwind-first UI implementation
- Tailwind-first UI implementation (semantic tokens ONLY — no raw blue-600, gray-*, indigo-*)
- UX state repair and route-level behavior
- Browser-driven acceptance for frontend scenarios
- Screenshot and console-driven debugging
@@ -64,6 +65,9 @@ You do not own:
- Backend-only implementation unless explicitly scoped
- Semantic repair outside the frontend boundary unless required by the UI change
### Frozen zones (LEGACY — migrate away, do NOT add)
- `frontend/src/components/` — legacy component directory. **Do not create new files here.** All new domain components go in `lib/components/<domain>/`.
## Required Workflow
1. **Discover or create the Model first.** For any screen with cross-widget state:
- grep `@semantics.*<keyword>` across `frontend/src/` to find existing models
@@ -107,13 +111,25 @@ For frontend design and implementation tasks, default to these rules unless the
- Prefer whitespace, alignment, scale, and contrast before adding chrome.
- Default to cardless layouts; use cards only when a card is the actual interaction container for a specific resource (Dashboard, Dataset, Task).
### Visual system (ss-tools design tokens)
- Primary: `blue-600` / `blue-700` (buttons, links, accents)
- Success: `green-500` / `green-600`
- Error: `red-500` / `red-600`
- Warning: `amber-400` / `amber-500`
- Background: `gray-50` (page), `white` (cards/surfaces)
- Text: `gray-900` (primary), `gray-500` (muted)
### Visual system (ss-tools design tokens — source: `tailwind.config.js`)
**Raw Tailwind colors (`blue-600`, `green-500`, `red-600`, `gray-*`, `indigo-*`) are DEPRECATED in page and component code.** Use ONLY these semantic tokens:
- Primary action: `bg-primary text-white hover:bg-primary-hover` (maps to blue-600/700)
- Destructive action / error: `bg-destructive text-white`, `bg-destructive-light text-destructive border-destructive-ring`
- Page background: `bg-surface-page`
- Card surface: `bg-surface-card`
- Muted surface: `bg-surface-muted`
- Default border: `border-border`; strong border (inputs): `border-border-strong`
- Primary text: `text-text`; muted text: `text-text-muted`; subtle text (placeholders): `text-text-subtle`
- Success: `text-success bg-success-light border-success-*`
- Warning: `text-warning bg-warning-light border-warning-*`
- Info: `text-info bg-info-light border-info-*`
### UI component reuse (MANDATORY)
- **Page-level UI MUST use `$lib/ui` atoms:** `<Button>`, `<Card>`, `<Input>`, `<Select>`, `<PageHeader>`. Raw `<button>` and manual `<div class="bg-white rounded...">` in page files is a violation unless there is a documented exception.
- **`src/components/` is LEGACY FROZEN.** Do not create new files there. Do not extend it. New domain components go in `src/lib/components/<domain>/`.
- **`migration/+page.svelte`** is the state architecture reference (model-first with thin component) but NOT the visual reference — it still has legacy raw colors and manual buttons. Use the canonical template in `semantics-svelte` §VI for visual patterns.
- **Button variant naming:** Use `"destructive"` (canonical). `"danger"` is a deprecated alias — prefer `"destructive"`.
### ss-tools specific pages
- **Dashboard Hub** — Git-tracked dashboards with status badges

View File

@@ -11,7 +11,10 @@ description: Svelte 5 (Runes) protocol for ss-tools: UX State Machines, Tailwind
@RATIONALE Svelte 5 runes ($state, $derived, $effect, $props) chosen for reactive precision and native compiler optimisations over Svelte 4 legacy reactivity ($:). Tailwind CSS selected for zero-runtime utility-first styling and rapid visual validation via chrome-devtools MCP. FSM-based UX contracts (@UX_STATE, @UX_FEEDBACK, @UX_RECOVERY) chosen to create verifiable state-transition tests that the browser Judge Agent can execute deterministically. ss-tools internal API wrappers (fetchApi/requestApi) chosen over native fetch to enforce auth, error normalisation, and trace_id propagation.
@REJECTED React (JSX) rejected — Svelte's compiler-first approach yields smaller bundles and native reactivity without virtual DOM overhead. Vue rejected — Svelte 5 runes provide simpler mental model. Legacy Svelte 4 syntax (export let, $:, on:event) rejected — incompatible with Svelte 5 runes mode. CSS Modules / styled-components rejected in favour of Tailwind's utility-first approach, which avoids style leakage and simplifies chrome-devtools visual diffing. Native fetch() rejected — bypasses ss-tools middleware chain (auth, trace_id, error normalisation). Plain-text logging rejected per MolecularCoTLogging §VII — JSON lines are mandatory for agent-parsable traces.
@INVARIANT Frontend components MUST be verifiable by the browser toolset via `chrome-devtools` MCP.
@INVARIANT Use Tailwind CSS exclusively. Native `fetch` is forbidden — use `requestApi`/`fetchApi` wrappers.
@INVARIANT Use Tailwind CSS exclusively. Raw Tailwind color classes (`blue-600`, `green-500`, `red-600`, `gray-*`, `indigo-*`) are DEPRECATED in page and component code — use semantic tokens from `tailwind.config.js` only (`primary`, `destructive`, `success`, `warning`, `surface-*`, `border-*`, `text-*`).
@INVARIANT Page-level UI MUST use `$lib/ui` atoms: `<Button>`, `<Card>`, `<Input>`, `<Select>`, `<PageHeader>`. Raw `<button>` elements and manual card `<div>` containers in page files are a violation.
@INVARIANT `src/components/` is LEGACY FROZEN. New domain components go in `src/lib/components/<domain>/`. Do not create new files under `src/components/`.
@INVARIANT Native `fetch` is forbidden — use `requestApi`/`fetchApi` wrappers.
## 0. SVELTE 5 PARADIGM & UX PHILOSOPHY (SS-TOOLS)
@@ -319,17 +322,19 @@ Region format for HTML/Svelte comments:
<!-- @RELATION BINDS_TO -> [notificationStore] -->
<!-- @UX_STATE Idle -> Default card view with task summary. -->
<!-- @UX_STATE Loading -> Action button disabled, spinner active, progress bar animated. -->
<!-- @UX_STATE Error -> Red border, error icon, retry button visible. -->
<!-- @UX_STATE Success -> Green border, checkmark, duration displayed. -->
<!-- @UX_STATE Error -> Card border + bg use destructive tokens, retry button visible. -->
<!-- @UX_STATE Success -> Card border + bg use success tokens, checkmark, duration displayed. -->
<!-- @UX_FEEDBACK Toast notification on start/fail/complete. -->
<!-- @UX_FEEDBACK Drawer opens on "View Logs" click. -->
<!-- @UX_RECOVERY Retry button on error. Clear/Cancel on running task. -->
<!-- @UX_REACTIVITY Props -> $props(), LocalState -> $state(isLoading, error). -->
<!-- @UX_TEST: Idle -> {click: "Run Migration", expected: Loading -> Success toast}. -->
<script>
<!-- @RATIONALE Uses semantic tokens (destructive-light, success-light, border, surface-card) instead of raw Tailwind (red-*, green-*, gray-*). Uses $lib/ui Button instead of raw <button>. This is the canonical visual reference for all components. -->
<script lang="ts">
import { fetchApi } from "$lib/api";
import { log } from "$lib/cot-logger";
import { t } from "$lib/i18n";
import { Button } from "$lib/ui";
import { taskDrawerStore } from "$lib/stores";
import { notificationStore } from "$lib/stores";
import StatusBadge from "./StatusBadge.svelte";
@@ -338,8 +343,8 @@ Region format for HTML/Svelte comments:
let { taskId, dashboardName, sourceEnv, targetEnv } = $props();
let isLoading = $state(false);
let error = $state(null);
let status = $state("idle");
let error: string | null = $state(null);
let status: "idle" | "loading" | "success" | "error" = $state("idle");
async function handleRunMigration() {
isLoading = true;
@@ -355,8 +360,8 @@ Region format for HTML/Svelte comments:
notificationStore.add({ type: "success", message: $t("migration.completed", { name: dashboardName }) });
} catch (e) {
status = "error";
error = e.message;
log("MigrationTaskCard", "EXPLORE", "Migration failed", { taskId }, error = e.message);
error = e instanceof Error ? e.message : "Migration failed";
log("MigrationTaskCard", "EXPLORE", "Migration failed", { taskId }, error);
notificationStore.add({ type: "error", message: $t("migration.failed", { name: dashboardName }) });
} finally {
isLoading = false;
@@ -369,17 +374,21 @@ Region format for HTML/Svelte comments:
}
</script>
<!-- @RATIONALE Card uses semantic surface/border/text tokens. Dynamic state uses destructive/success token families. Button component from $lib/ui instead of raw <button> tags. -->
<div
class="rounded-lg border p-4 transition-colors {status === 'error' ? 'border-red-500 bg-red-50' : ''} {status === 'success' ? 'border-green-500 bg-green-50' : 'border-gray-200 bg-white'}"
class="rounded-lg p-4 transition-colors
{status === 'error' ? 'border border-destructive-ring bg-destructive-light' : ''}
{status === 'success' ? 'border border-success-DEFAULT bg-success-light' : ''}
{status !== 'error' && status !== 'success' ? 'border border-border bg-surface-card' : ''}"
role="region"
aria-label={$t("migration.task_card", { name: dashboardName })}
>
<div class="flex items-center justify-between mb-2">
<h3 class="font-semibold text-gray-900">{dashboardName}</h3>
<h3 class="font-semibold text-text">{dashboardName}</h3>
<StatusBadge status={status} />
</div>
<div class="text-sm text-gray-500 mb-3">
<div class="text-sm text-text-muted mb-3">
{$t("migration.from")}: {sourceEnv} → {$t("migration.to")}: {targetEnv}
</div>
@@ -388,45 +397,80 @@ Region format for HTML/Svelte comments:
{/if}
{#if error}
<p class="text-sm text-red-600 mb-2" aria-live="polite">{error}</p>
<p class="text-sm text-destructive mb-2" aria-live="polite">{error}</p>
{/if}
<div class="flex gap-2 mt-3">
<button
class="btn-primary text-sm"
<Button
variant="primary"
size="sm"
onclick={handleRunMigration}
disabled={isLoading}
aria-busy={isLoading}
isLoading={isLoading}
>
{#if isLoading}
<span class="spinner mr-1" aria-hidden="true"></span>
{/if}
{status === "error" ? $t("actions.retry") : $t("actions.run")}
</button>
<button class="btn-secondary text-sm" onclick={handleViewLogs}>
</Button>
<Button variant="secondary" size="sm" onclick={handleViewLogs}>
{$t("actions.view_logs")}
</button>
</Button>
</div>
</div>
<!-- #endregion MigrationTaskCard -->
```
## VII. SS-TOOLS TAILWIND CONVENTIONS
## VII. SS-TOOLS DESIGN TOKEN CANON & COMPONENT REUSE
### Design tokens (from project config)
- Primary: `blue-600` / `blue-700` (buttons, links, accents)
- Success: `green-500` / `green-600`
- Error: `red-500` / `red-600`
- Warning: `amber-400` / `amber-500`
- Background: `gray-50` (page), `white` (cards)
- Text: `gray-900` (primary), `gray-500` (muted)
### Design tokens (source of truth: `frontend/tailwind.config.js`)
### Component class conventions
- Page layout: `max-w-7xl mx-auto px-4 py-6`
- Card: `bg-white rounded-lg shadow-sm border border-gray-200 p-4`
- Button primary: `bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 disabled:opacity-50`
- Button secondary: `border border-gray-300 text-gray-700 px-4 py-2 rounded-md hover:bg-gray-50`
- Table: `min-w-full divide-y divide-gray-200`
**Raw Tailwind colors (`blue-600`, `green-500`, `red-600`, `gray-*`, `indigo-*`) are DEPRECATED in page-level and component code.** Use ONLY semantic tokens below.
| Purpose | Token | Maps to |
|---------|-------|---------|
| Primary action | `text-primary bg-primary hover:bg-primary-hover` | blue-600/700 |
| Destructive action / error surface | `text-destructive bg-destructive-light border-destructive-ring` | red family |
| Page background | `bg-surface-page` | near-white |
| Card surface | `bg-surface-card` | white |
| Muted surface (hover/filter bars) | `bg-surface-muted` | gray-100 |
| Default border | `border-border` | gray-200 |
| Strong border (inputs, focus) | `border-border-strong` | gray-300 |
| Primary text | `text-text` | near-black |
| Muted text (secondary, captions) | `text-text-muted` | gray-500 |
| Subtle text (placeholders) | `text-text-subtle` | gray-400 |
| Success | `text-success bg-success-light border-success-*` | green family |
| Warning | `text-warning bg-warning-light border-warning-*` | amber family |
| Info | `text-info bg-info-light border-info-*` | sky family |
### Component reuse rules (MANDATORY for page-level code)
| Rule | Requirement |
|------|------------|
| **$lib/ui mandatory** | All page files (`src/routes/**/+page.svelte`) MUST import from `$lib/ui` for buttons, cards, inputs, selects, page headers. Raw `<button>` and `<div class="bg-white rounded...">` in page files are a violation unless covered by a documented exception. |
| **Component directory** | New domain components go in `src/lib/components/<domain>/`. `src/components/` is **LEGACY FROZEN** — do not add new files, do not extend, migrate out only. |
| **Button variants** | Use `<Button variant="primary">` (default), `<Button variant="secondary">`, `<Button variant="destructive">`, `<Button variant="ghost">`. The string `"danger"` is kept as a deprecated alias for `"destructive"` — prefer `"destructive"`. |
| **Page layout** | `<div class="max-w-7xl mx-auto px-4 py-6">` or `<div class="mx-auto w-full px-4 lg:px-8 space-y-6">`. |
| **Table pattern** | `min-w-full divide-y divide-border` — border via token. |
### Canonical semantic token reference (copy-paste for agents)
```
// ✅ CORRECT — semantic tokens
bg-surface-page // page background
bg-surface-card // card container
bg-surface-muted // filter bar, secondary button hover
border-border // card border, table divider
border-border-strong // input border, select border
text-text // heading, body
text-text-muted // secondary label, caption
text-text-subtle // placeholder
bg-primary text-white // primary action button
bg-destructive-light // error surface
text-destructive // error text
border-destructive-ring // error border
// ❌ WRONG — raw Tailwind (deprecated in page/component code)
bg-blue-600 text-blue-700 bg-gray-50 bg-white border-gray-200
text-gray-900 text-gray-500 bg-red-50 border-red-400 bg-green-50
bg-indigo-50 text-indigo-700 bg-gradient-to-br from-slate-50 via-white to-sky-50
```
## VIII. VITEST CONVENTIONS

View File

@@ -308,6 +308,7 @@ async def list_all_runs(
page_size: int = Query(20, ge=1, le=100),
status_filter: str | None = Query(None, alias="status", description="Filter by status"),
environment_id: str | None = Query(None, description="Filter by environment"),
dashboard_id: str | None = Query(None, description="Filter by dashboard ID"),
current_user: User = Depends(get_current_user),
_=Depends(has_permission("validation.run", "VIEW")),
service: ValidationTaskService = Depends(_get_task_service),
@@ -319,7 +320,8 @@ async def list_all_runs(
)
try:
total, raw_items = service.list_all_runs(
status=status_filter, environment_id=environment_id, page=page, page_size=page_size,
status=status_filter, environment_id=environment_id, dashboard_id=dashboard_id,
page=page, page_size=page_size,
)
return RecordListResponse(items=raw_items, total=total, page=page, page_size=page_size)
except ValueError as e:

View File

@@ -1,15 +1,16 @@
# #region HealthSchemas [C:3] [TYPE Module] [SEMANTICS pydantic, health, schema, dashboard, dashboard-health-item]
# @BRIEF Pydantic schemas for dashboard health summary.
# @BRIEF Pydantic schemas for dashboard health summary — includes v2 LLM validation fields.
# @LAYER Domain
# @RELATION DEPENDS_ON -> [EXT:Library:pydantic]
from datetime import datetime
from typing import Any
from pydantic import BaseModel, Field
# #region DashboardHealthItem [TYPE Class]
# @BRIEF Represents the latest health status of a single dashboard.
# @BRIEF Represents the latest health status of a single dashboard, with v2 LLM validation data.
class DashboardHealthItem(BaseModel):
record_id: str | None = None
dashboard_id: str
@@ -19,7 +20,17 @@ class DashboardHealthItem(BaseModel):
status: str = Field(..., pattern="^(PASS|WARN|FAIL|UNKNOWN)$")
last_check: datetime
task_id: str | None = None
run_id: str | None = None
policy_id: str | None = None
summary: str | None = None
# v2 LLM validation fields
execution_path: str | None = None # "screenshot" or "text_only"
issues_count: int = 0
timings: dict[str, Any] | None = None
token_usage: dict[str, Any] | None = None
screenshot_paths: list[str] | None = None
chunk_count: int | None = None
dashboard_name: str | None = None # human-readable alias for dashboard_title
# #endregion DashboardHealthItem

View File

@@ -7,6 +7,7 @@
# @RELATION DEPENDS_ON -> [TaskManager]
from datetime import UTC
import json
import os
import time
from typing import Any, cast
@@ -260,7 +261,24 @@ class HealthService:
if timestamp is not None and timestamp.tzinfo is None:
timestamp = timestamp.replace(tzinfo=UTC)
# ── v2 fields ──────────────────────────────────────────────
execution_path = str(record.execution_path) if record.execution_path else None
issues_count = len(record.issues) if record.issues else 0
timings = dict(record.timings) if record.timings else None
token_usage = dict(record.token_usage) if record.token_usage else None
screenshot_paths = list(record.screenshot_paths) if record.screenshot_paths else None
# Extract chunk_count from raw_response (stored as JSON in result_payload)
chunk_count: int | None = None
if record.raw_response:
try:
raw = json.loads(record.raw_response)
chunk_count = int(raw["chunk_count"]) if raw.get("chunk_count") else None
except (json.JSONDecodeError, KeyError, TypeError, ValueError):
pass
meta = self._resolve_dashboard_meta(dashboard_id, resolved_environment_id)
dashboard_name = meta.get("title") or meta.get("slug") or dashboard_id
items.append(
DashboardHealthItem(
record_id=record_id,
@@ -271,7 +289,16 @@ class HealthService:
status=status,
last_check=timestamp,
task_id=task_id,
run_id=str(record.run_id) if record.run_id else None,
policy_id=str(record.policy_id) if record.policy_id else None,
summary=summary,
execution_path=execution_path,
issues_count=issues_count,
timings=timings,
token_usage=token_usage,
screenshot_paths=screenshot_paths,
chunk_count=chunk_count,
dashboard_name=dashboard_name,
)
)

View File

@@ -141,6 +141,30 @@ class ValidationTaskService:
# #endregion _parse_superset_link
# #region _resolve_dashboard_title [C:2] [TYPE Function]
# @BRIEF Fetch a dashboard title from the Superset API by its numeric ID.
# @SIDE_EFFECT Makes an HTTP GET to Superset REST API via the config-manager.
# @RETURNS The dashboard title string, or None if it cannot be resolved.
def _resolve_dashboard_title(self, environment_id: str, dashboard_id: str) -> str | None:
try:
from ..core.superset_client import SupersetClient
if not self.config_manager:
return None
env = self.config_manager.get_environment(environment_id)
if not env:
return None
client = SupersetClient(env)
resp = client.get_dashboard(dashboard_id)
# Superset API returns { "result": { "dashboard_title": "..." } }
result = resp.get("result", resp)
title = result.get("dashboard_title") or result.get("title")
return str(title) if title else None
except Exception:
return None
# #endregion _resolve_dashboard_title
# #region _resolve_sources [C:3] [TYPE Function]
# @RATIONALE Resolves SourceInput list into ValidationSource ORM objects. Dashboard_url sources
# are parsed via SupersetContextExtractor for validation and context extraction.
@@ -173,6 +197,10 @@ class ValidationTaskService:
elif s.type == "dashboard_id":
resolved_dashboard_id = s.value
# Resolve dashboard title from Superset API
if resolved_dashboard_id and not title:
title = self._resolve_dashboard_title(environment_id, resolved_dashboard_id)
sources.append(ValidationSource(
policy_id=policy_id,
type=s.type,
@@ -185,12 +213,14 @@ class ValidationTaskService:
# Fallback: dashboard_ids (v1 backward compat)
elif dashboard_ids:
for did in dashboard_ids:
title = self._resolve_dashboard_title(environment_id, str(did))
sources.append(ValidationSource(
policy_id=policy_id,
type="dashboard_id",
value=str(did),
status="valid",
resolved_dashboard_id=str(did),
title=title,
))
return sources
@@ -301,20 +331,23 @@ class ValidationTaskService:
def _record_to_dict(self, record: ValidationRecord) -> dict:
# Try to resolve dashboard title from sources
dashboard_title = record.dashboard_id
if record.policy_id:
try:
source = (
self.db.query(ValidationSource)
.filter(
ValidationSource.policy_id == record.policy_id,
ValidationSource.resolved_dashboard_id == record.dashboard_id,
)
.first()
)
if source and source.title:
dashboard_title = source.title
except Exception:
pass
try:
source = (
self.db.query(ValidationSource)
.filter(ValidationSource.resolved_dashboard_id == record.dashboard_id)
.first()
)
if source and source.title:
dashboard_title = source.title
elif source and record.environment_id:
# Fallback: resolve from Superset API and cache back to source
resolved = self._resolve_dashboard_title(record.environment_id, record.dashboard_id)
if resolved:
dashboard_title = resolved
source.title = resolved
self.db.flush()
except Exception:
pass
return {
"id": record.id,
"run_id": record.run_id,
@@ -681,6 +714,7 @@ class ValidationTaskService:
self,
status: str | None = None,
environment_id: str | None = None,
dashboard_id: str | None = None,
page: int = 1,
page_size: int = 20,
) -> tuple[int, list[dict]]:
@@ -690,6 +724,8 @@ class ValidationTaskService:
query = query.filter(ValidationRecord.status == status.upper())
if environment_id:
query = query.filter(ValidationRecord.environment_id == environment_id)
if dashboard_id:
query = query.filter(ValidationRecord.dashboard_id == dashboard_id)
total = query.count()
records = (

View File

@@ -1,73 +1,109 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{svelte,js,ts,jsx,tsx}",
],
theme: {
extend: {
colors: {
// Semantic UI colors (light surfaces)
primary: {
DEFAULT: '#2563eb', // blue-600
hover: '#1d4ed8', // blue-700
ring: '#3b82f6', // blue-500
light: '#eff6ff', // blue-50
},
secondary: {
DEFAULT: '#f3f4f6', // gray-100
hover: '#e5e7eb', // gray-200
text: '#111827', // gray-900
ring: '#6b7280', // gray-500
},
destructive: {
DEFAULT: '#dc2626', // red-600
hover: '#b91c1c', // red-700
ring: '#ef4444', // red-500
light: '#fef2f2', // red-50
},
ghost: {
hover: '#f3f4f6', // gray-100
text: '#374151', // gray-700
ring: '#6b7280', // gray-500
},
// Dark terminal palette (log viewer, task drawer)
terminal: {
bg: '#0f172a', // slate-900
surface: '#1e293b', // slate-800
border: '#334155', // slate-700
text: {
DEFAULT: '#cbd5e1', // slate-300
muted: '#475569', // slate-600
subtle: '#64748b', // slate-500
bright: '#e2e8f0', // slate-200
heading: '#f1f5f9', // slate-100
},
accent: '#22d3ee', // cyan-400
},
// Log level palette
log: {
debug: '#64748b',
info: '#38bdf8',
warning: '#fbbf24',
error: '#f87171',
},
// Log source palette
source: {
plugin: '#4ade80',
api: '#c084fc',
git: '#fb923c',
system: '#38bdf8',
},
},
width: {
sidebar: '240px',
'sidebar-collapsed': '64px',
},
fontFamily: {
mono: ['"JetBrains Mono"', '"Fira Code"', 'monospace'],
},
},
},
plugins: [],
}
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{svelte,js,ts,jsx,tsx}",
],
theme: {
extend: {
colors: {
// ── Semantic action palette (light surfaces) ──────────────
primary: {
DEFAULT: '#2563eb', // blue-600
hover: '#1d4ed8', // blue-700
ring: '#3b82f6', // blue-500
light: '#eff6ff', // blue-50
},
secondary: {
DEFAULT: '#f3f4f6', // gray-100
hover: '#e5e7eb', // gray-200
text: '#111827', // gray-900
ring: '#6b7280', // gray-500
},
destructive: {
DEFAULT: '#dc2626', // red-600
hover: '#b91c1c', // red-700
ring: '#ef4444', // red-500
light: '#fef2f2', // red-50
},
success: {
DEFAULT: '#22c55e', // green-500
hover: '#16a34a', // green-600
ring: '#4ade80', // green-400
light: '#f0fdf4', // green-50
},
warning: {
DEFAULT: '#f59e0b', // amber-500
hover: '#d97706', // amber-600
ring: '#fbbf24', // amber-400
light: '#fffbeb', // amber-50
},
info: {
DEFAULT: '#0ea5e9', // sky-500
hover: '#0284c7', // sky-600
ring: '#38bdf8', // sky-400
light: '#f0f9ff', // sky-50
},
ghost: {
hover: '#f3f4f6', // gray-100
text: '#374151', // gray-700
ring: '#6b7280', // gray-500
},
// ── Surface hierarchy ─────────────────────────────────────
surface: {
page: '#f8fafc', // slate-50
card: '#ffffff', // white
muted: '#f1f5f9', // slate-100
},
// ── Border hierarchy ──────────────────────────────────────
border: {
DEFAULT: '#e2e8f0', // slate-200
strong: '#cbd5e1', // slate-300
},
// ── Text hierarchy ────────────────────────────────────────
text: {
DEFAULT: '#0f172a', // slate-900
muted: '#64748b', // slate-500
subtle: '#94a3b8', // slate-400
inverse: '#ffffff', // white
},
// ── Dark terminal palette (log viewer, task drawer) ───────
terminal: {
bg: '#0f172a', // slate-900
surface: '#1e293b', // slate-800
border: '#334155', // slate-700
text: {
DEFAULT: '#cbd5e1', // slate-300
muted: '#475569', // slate-600
subtle: '#64748b', // slate-500
bright: '#e2e8f0', // slate-200
heading: '#f1f5f9', // slate-100
},
accent: '#22d3ee', // cyan-400
},
// ── Log level palette ─────────────────────────────────────
log: {
debug: '#64748b',
info: '#38bdf8',
warning: '#fbbf24',
error: '#f87171',
},
// ── Log source palette ────────────────────────────────────
source: {
plugin: '#4ade80',
api: '#c084fc',
git: '#fb923c',
system: '#38bdf8',
},
},
width: {
sidebar: '240px',
'sidebar-collapsed': '64px',
},
fontFamily: {
mono: ['"JetBrains Mono"', '"Fira Code"', 'monospace'],
},
},
},
plugins: [],
}