496 lines
28 KiB
Markdown
496 lines
28 KiB
Markdown
---
|
||
name: semantics-svelte
|
||
description: Svelte 5 (Runes) protocol for superset-tools: UX State Machines, Tailwind components, stores, and browser-driven visual validation.
|
||
---
|
||
|
||
#region Std.Semantics.Svelte [C:5] [TYPE Skill] [SEMANTICS frontend,svelte,ui,ux,tailwind]
|
||
@BRIEF HOW to build Svelte 5 (Runes) Components for superset-tools with UX State Machines, Tailwind CSS, store topology, and visual-interactive validation.
|
||
@RELATION DEPENDS_ON -> [Std.Semantics.Core]
|
||
@RELATION DEPENDS_ON -> [Std.Semantics.MolecularCoTLogging]
|
||
@RELATION DISPATCHES -> [Std.Semantics.Testing]
|
||
@RESTRICTION EXAMPLES ONLY — this file provides language-specific code patterns. All protocol rules (tier definitions, tag catalog, anchor syntax) are defined exclusively in `semantics-core`. UX contract tags are defined here as examples; the tag catalog lives in `semantics-core` §III. This file MUST NOT redefine or contradict any rule from `semantics-core`.
|
||
@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. superset-tools internal API wrappers (fetchApi/requestApi) chosen over native fetch to enforce auth, error normalisation, and trace_id propagation. Model-first architecture chosen because event-handler spaghetti is the #1 Transformer failure mode in UI code: the agent scatters logic across onclick/onchange in 5 files — KV-cache cannot hold cross-component relationships, creating invisible coupling that breaks silently.
|
||
@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 and dominates agent training data, causing silent regression. 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 superset-tools middleware chain (auth, trace_id, error normalisation). Plain-text logging rejected per MolecularCoTLogging §VII — JSON lines are mandatory for agent-parsable traces. Component-first architecture for complex screens rejected — the Model-first approach (model.svelte.ts → component) keeps system logic in one file where the agent's attention can find it via grep + search_contracts.
|
||
@INVARIANT Frontend components MUST be verifiable by the browser toolset via `chrome-devtools` MCP.
|
||
@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 All domain components go in `src/lib/components/<domain>/`. The legacy `src/components/` zone has been removed.
|
||
@INVARIANT Native `fetch` is forbidden — use `requestApi`/`fetchApi` wrappers.
|
||
|
||
## 0. SVELTE 5 PARADIGM & UX PHILOSOPHY (SS-TOOLS)
|
||
|
||
- **TYPESCRIPT-FIRST:** TypeScript is the default language for ALL frontend code. Components use `<script lang="ts">`. Screen Models use `.svelte.ts` extension. API DTOs live in `frontend/src/types/` with explicit interfaces. Types are the contract between the model and the component — without them, the model-first architecture has no enforcement layer. `any` is forbidden at external boundaries; use `unknown` with runtime narrowing.
|
||
- **STRICT RUNES ONLY (PROJECT RULE):** Our project deliberately chooses Svelte 5 runes exclusively — `$state()`, `$derived()`, `$effect()`, `$props()`, `$bindable()`. This is a codebase-wide architectural decision, not a Svelte 5 limitation. Every component follows the same reactive pattern; every model is a predictable `$state` container. Mixed styles create agent confusion and verification gaps.
|
||
- **FORBIDDEN SYNTAX (PROJECT RULE):** Do NOT use `export let`, `on:event`, `createEventDispatcher`, or the legacy `$:` reactivity. These are banned for architectural consistency — not because Svelte 5 cannot run them.
|
||
- **EVENT ARCHITECTURE:** DOM events use native attributes (`onclick`, `onchange`, `onsubmit`). Component-to-component communication uses typed callback props — NEVER `createEventDispatcher` or `on:event` directives. This keeps component interfaces explicit, type-checkable, and free of runtime event bus ambiguity.
|
||
- **$effect SCOPE:** `$effect` is for browser-side side effects only — DOM measurements, WebSocket subscriptions, external library bindings, synchronising state that cannot be expressed as `$derived`. For route-level data loading, use SvelteKit `load` functions in `+page.ts`. Using `$effect` for initial data fetch breaks SSR and creates unpredictable loading order.
|
||
- **UX AS A STATE MACHINE:** Every component is a Finite State Machine (FSM). Declare visual states in the contract BEFORE writing implementation.
|
||
- **MODEL-FIRST FOR COMPLEX SCREENS:** For screens with cross-widget logic (filters, pagination, search, multi-step forms), create a `[TYPE Model]` FIRST. The model is the source of truth — components only render model state and pass user intentions via `model.action()`. See §IIIa for the full RSM protocol.
|
||
- **RESOURCE-CENTRIC:** Navigation and actions revolve around Resources (Dashboards, Datasets, Tasks). Every action MUST be traceable.
|
||
- **SS-TOOLS SPECIFIC:** This is an Apache Superset automation dashboard — components deal with migrations, Git operations, task monitoring, dataset mapping, and plugin management.
|
||
|
||
## I. PROJECT ARCHITECTURAL INVARIANTS (SS-TOOLS)
|
||
|
||
You are bound by strict repository-level design rules:
|
||
1. **Styling:** Tailwind CSS utility classes are MANDATORY. Minimize scoped `<style>`. If custom CSS is absolutely necessary, use `@apply` directives.
|
||
2. **Localization:** All user-facing text MUST use the `$t` store from `src/lib/i18n`. No hardcoded UI strings.
|
||
3. **API Layer:** You MUST use the internal `fetchApi`/`requestApi` wrappers from `$lib/api`. Using native `fetch()` is a fatal violation.
|
||
4. **SvelteKit Routing:** Pages live under `src/routes/`. Components live under `src/lib/components/`. Stores under `src/lib/stores/`.
|
||
5. **Testing:** Use Vitest with `@testing-library/svelte` for component tests.
|
||
6. **Component Reuse:** Before creating any new component, scan the existing library:
|
||
- **Atoms:** `$lib/ui/Button.svelte`, `$lib/ui/Select.svelte`, `$lib/ui/Input.svelte`, `$lib/ui/Card.svelte`
|
||
- **Widgets:** `$lib/components/ui/SearchableMultiSelect.svelte`, `$lib/components/ui/MultiSelect.svelte`
|
||
- **Infrastructure:** `addToast()` from `$lib/toasts.js` (Toast already mounted in root layout)
|
||
- **Patterns (no component needed):** badges (`rounded-full px-2.5 py-0.5 text-xs font-medium`), tooltips (native `title`), skeletons (`animate-pulse bg-gray-200`), collapsibles (`<details><summary>`), empty states (`border-dashed bg-gray-50`), confirmations (`confirm()`)
|
||
Refer to `.agents/commands/speckit.plan.md` §"Frontend Component Reuse Scan" for the mandatory scan workflow.
|
||
|
||
## II. UX CONTRACTS (STRICT UI BEHAVIOR)
|
||
|
||
Every component MUST define its behavioral contract in the header.
|
||
- **`@UX_STATE:`** Maps FSM state names to visual behavior. *Example:* `@UX_STATE Loading -> Spinner visible, btn disabled, aria-busy=true`.
|
||
- **`@UX_FEEDBACK:`** Defines external system reactions (Toast, Shake, RedBorder, Modal).
|
||
- **`@UX_RECOVERY:`** Defines the user's recovery path. *Example:* `@UX_RECOVERY Retry button, Clear filters, Reload page`.
|
||
- **`@UX_REACTIVITY:`** Explicitly declares the state source. *Example:* `@UX_REACTIVITY: Props -> $props(), LocalState -> $state(...)`.
|
||
- **`@UX_TEST:`** Defines the interaction scenario for the automated Judge Agent. *Example:* `@UX_TEST: Idle -> {click: submit, expected: Loading}`.
|
||
|
||
## III. STATE MANAGEMENT & STORE TOPOLOGY (SS-TOOLS STORES)
|
||
|
||
Key stores in `frontend/src/lib/stores/` (bind with `@RELATION BINDS_TO` only when the component actually reads/writes them):
|
||
- `taskDrawerStore` — Background task monitoring drawer
|
||
- `sidebarStore` — Navigation sidebar state
|
||
- `assistantChat` (`assistantChat.svelte.ts`) — assistant conversation id / chrome
|
||
- `maintenanceStore` — Maintenance window state
|
||
- `healthStore` — Health probe summary
|
||
- `translationRunStore` — Active translation run
|
||
- `activityStore` — Activity feed
|
||
- `environmentContext` — Selected environment
|
||
- Toasts: `addToast()` / `notifications` from `$lib/toasts` — not a domain store named `notificationStore`
|
||
- Screen-level dashboards/migration/git state lives in `[TYPE Model]` (`DashboardHubModel`, `MigrationModel`, `GitManagerModel`, `AgentChatModel`), not in a global `dashboardStore` / `migrationStore`
|
||
|
||
**Store subscription rules:**
|
||
- Use the `$` prefix for reactive store access (e.g., `$sidebarStore.collapsed`).
|
||
- **Graph Linkage:** Whenever a component reads or writes to a global store, declare it:
|
||
`@RELATION BINDS_TO -> [Store_ID]`
|
||
|
||
## IIIa. REACTIVE SCREEN MODELS (RSM) — MODEL-FIRST STATE ARCHITECTURE
|
||
|
||
### Why Models
|
||
|
||
The component-first approach forces you to encode system logic in event handlers (`onclick`, `onchange`), scattering it across JSX and hooks. For LLM agents, this means holding dozens of cross-component relationships in context — a primary source of errors.
|
||
|
||
**Model-first approach:** The entire screen is a pure function of state. The Model declares atoms, invariants, and relationships between them. Components only render the current state and pass user intentions back to the Model.
|
||
|
||
**What this means for you, the agent:**
|
||
- **Findability:** grep `[SEMANTICS` plus the domain keyword (e.g. `users`) → all models related to users. The contract is single-source, not scattered across HTML.
|
||
- **Testability:** Model invariants (`@INVARIANT changing filter resets pagination`) are verified in vitest without browser render — milliseconds, not seconds.
|
||
- **CSA resilience:** `#region Users.ListModel [C:N] [SEMANTICS ...]` on line 1 = maximum density for top‑k attention selection. Closing `#endregion Users.ListModel` duplicates the identifier — safe after aggressive context compression.
|
||
- **Component simplicity:** When a component contains only `$state`, `$derived`, and `model.action()` calls, its contract is predictable. No guessing which side effect hides in `onchange`.
|
||
|
||
### Model Contract Template
|
||
|
||
A Model is a **contract** — `#region ModelName [C:N] [TYPE Model] [SEMANTICS tags]` with mandatory `@BRIEF` and `@INVARIANT`.
|
||
|
||
Models use the **`.svelte.ts`** extension (not plain `.ts`) because they rely on Svelte 5 reactive primitives (`$state`, `$derived`). The Svelte compiler processes `.svelte.ts` files and transforms these runes into proper reactive code.
|
||
|
||
```typescript
|
||
// frontend/src/lib/models/UsersListModel.svelte.ts
|
||
// #region Users.ListModel [C:4] [TYPE Model] [SEMANTICS users,list,screen-model]
|
||
// @ingroup Users
|
||
// @BRIEF State model for the user list screen — declares atoms, invariants, and actions.
|
||
// @INVARIANT Changing filter (search, role, status) resets pagination to page 1.
|
||
// @INVARIANT Deleting a user removes it from the list and decrements total count atomically.
|
||
// @INVARIANT The list never contains deleted users (idempotent delete).
|
||
// @STATE idle — Initial state, no data loaded.
|
||
// @STATE loading — API call in progress, list is stale or empty.
|
||
// @STATE loaded — Data fetched successfully, list populated.
|
||
// @STATE empty — Query returned zero results, filtering is active.
|
||
// @STATE error — API call failed, error message available.
|
||
// @ACTION search(query) — Full-text search with debounce, resets pagination.
|
||
// @ACTION setFilter(key, value) — Sets a filter atom, resets pagination.
|
||
// @ACTION deleteUser(id) — Deletes user, removes from list, decrements count.
|
||
// @ACTION loadPage(n) — Loads a specific page of results.
|
||
// @ACTION retry() — Re-runs the last failed operation.
|
||
// @ACTION reset() — Clears all filters and search, reloads page 1.
|
||
// @RELATION DEPENDS_ON -> [userApi]
|
||
// @RELATION CALLS -> [log]
|
||
|
||
import { requestApi } from "$lib/api";
|
||
|
||
// ── Type definitions (boundary types) ───────────────────────────
|
||
|
||
type ScreenState = "idle" | "loading" | "loaded" | "empty" | "error";
|
||
|
||
interface User {
|
||
id: string;
|
||
name: string;
|
||
email: string;
|
||
}
|
||
|
||
interface UserFilters {
|
||
role: string | null;
|
||
status: string | null;
|
||
}
|
||
|
||
interface UserListResponse {
|
||
data: User[];
|
||
meta: { total: number };
|
||
}
|
||
|
||
export class UsersListModel {
|
||
// ── Atoms (reactive state, all typed) ──────────────────────────
|
||
users: User[] = $state([]);
|
||
totalCount: number = $state(0);
|
||
page: number = $state(1);
|
||
perPage: number = $state(20);
|
||
searchQuery: string = $state("");
|
||
filters: UserFilters = $state({ role: null, status: null });
|
||
error: string | null = $state(null);
|
||
screenState: ScreenState = $state("idle");
|
||
|
||
// ── Derived ────────────────────────────────────────────────────
|
||
totalPages: number = $derived(Math.ceil(this.totalCount / this.perPage));
|
||
|
||
// ── Actions (typed public API for components) ──────────────────
|
||
async search(query: string): Promise<void> {
|
||
this.searchQuery = query;
|
||
this.page = 1; // @INVARIANT: reset pagination
|
||
await this._fetch();
|
||
}
|
||
|
||
setFilter<K extends keyof UserFilters>(key: K, value: UserFilters[K]): void {
|
||
this.filters[key] = value;
|
||
this.page = 1; // @INVARIANT: reset pagination
|
||
this._fetch();
|
||
}
|
||
|
||
async deleteUser(id: string): Promise<void> {
|
||
try {
|
||
await requestApi(`/api/users/${id}`, { method: "DELETE" });
|
||
// @INVARIANT: atomic removal
|
||
this.users = this.users.filter((u: User) => u.id !== id);
|
||
this.totalCount--;
|
||
} catch (e: unknown) {
|
||
this.error = e instanceof Error ? e.message : "Delete failed";
|
||
this.screenState = "error";
|
||
}
|
||
}
|
||
|
||
async loadPage(n: number): Promise<void> {
|
||
this.page = n;
|
||
await this._fetch();
|
||
}
|
||
|
||
async retry(): Promise<void> { await this._fetch(); }
|
||
|
||
reset(): void {
|
||
this.searchQuery = "";
|
||
this.filters = { role: null, status: null };
|
||
this.page = 1;
|
||
this._fetch();
|
||
}
|
||
|
||
// ── Private ────────────────────────────────────────────────────
|
||
private async _fetch(): Promise<void> {
|
||
this.screenState = "loading";
|
||
this.error = null;
|
||
try {
|
||
const params = new URLSearchParams({
|
||
q: this.searchQuery,
|
||
page: String(this.page),
|
||
per_page: String(this.perPage),
|
||
...this.filters
|
||
} as Record<string, string>);
|
||
const res: UserListResponse = await requestApi(`/api/users?${params}`);
|
||
this.users = res.data;
|
||
this.totalCount = res.meta.total;
|
||
this.screenState = this.users.length === 0 ? "empty" : "loaded";
|
||
} catch (e: unknown) {
|
||
this.error = e instanceof Error ? e.message : "Fetch failed";
|
||
this.screenState = "error";
|
||
}
|
||
}
|
||
}
|
||
// #endregion Users.ListModel
|
||
```
|
||
|
||
### Component Binds to Model (RSM pattern)
|
||
|
||
The component contract declares: `@RELATION BINDS_TO -> [ModelId]`. The component code is minimal — it renders model state and calls `model.action()` on user intent. No side-effect logic lives in event handlers.
|
||
|
||
For route-level data loading, use SvelteKit `load()` in `+page.ts` — NOT `$effect` (per §0: `$effect` is for browser-side side effects only).
|
||
|
||
```svelte
|
||
<!-- #region Users.ListPage [C:3] [TYPE Component] [SEMANTICS users,list,page] -->
|
||
<!-- @BRIEF User list page — renders Users.ListModel state, delegates all logic to the model. -->
|
||
<!-- @RELATION BINDS_TO -> [Users.ListModel] -->
|
||
<!-- @UX_TEST: Loaded -> {click: "delete", expected: User removed, count decremented}. -->
|
||
<script lang="ts">
|
||
import { onMount } from "svelte";
|
||
import { Button } from "$lib/ui";
|
||
import { UsersListModel } from "./UsersListModel.svelte.ts";
|
||
|
||
const model = new UsersListModel();
|
||
|
||
onMount(() => {
|
||
model.loadPage(1);
|
||
});
|
||
</script>
|
||
|
||
<div class="max-w-7xl mx-auto px-4 py-6">
|
||
{#if model.screenState === "error"}
|
||
<div role="alert" class="text-destructive">{model.error}</div>
|
||
<Button variant="primary" size="sm" onclick={() => model.retry()}>Retry</Button>
|
||
{:else if model.screenState === "empty"}
|
||
<p class="text-text-muted">No users found.</p>
|
||
{:else}
|
||
<ul>
|
||
{#each model.users as user (user.id)}
|
||
<li>
|
||
{user.name}
|
||
<Button variant="ghost" size="sm" onclick={() => model.deleteUser(user.id)}>Delete</Button>
|
||
</li>
|
||
{/each}
|
||
</ul>
|
||
<nav>Page {model.page} of {model.totalPages}</nav>
|
||
{/if}
|
||
</div>
|
||
<!-- #endregion Users.ListPage -->
|
||
```
|
||
|
||
### Searching for Models
|
||
|
||
```
|
||
# Quick grep across frontend files (anchor keyword, not @SEMANTICS)
|
||
grep -RIn -E "\[SEMANTICS[^]]*users" frontend/src
|
||
|
||
# Axiom semantic search when connected
|
||
search_contracts query="users" type="Model"
|
||
```
|
||
|
||
### When to Use a Model vs. a Store vs. Inline Component State
|
||
|
||
| Pattern | Use Case |
|
||
|---------|----------|
|
||
| **Model** (`[TYPE Model]`) | Screen-level state with cross-widget invariants (filters, pagination, search). Multiple components read/write the same atoms. |
|
||
| **Store** (`BINDS_TO -> [storeId]`) | Global cross-route state (auth, notifications, task drawer). Persists across navigation. |
|
||
| **Inline $state** | Local component UI state (accordion open, tooltip visible, input focus). No cross-component invariants. |
|
||
|
||
### Model Decomposition Gate
|
||
|
||
Models accumulate methods as features grow. To prevent "god object" anti-pattern:
|
||
|
||
| Threshold | Action |
|
||
|-----------|--------|
|
||
| Model > **400 lines** | Decompose — extract domain helpers or split into submodels |
|
||
| Model > **40 public methods** | Split into submodels by responsibility (e.g. `FiltersModel`, `SelectionModel`, `GitActionsModel`) |
|
||
|
||
**Submodel split example for `Dashboards.Hub`:**
|
||
- `Dashboards.FiltersModel` — search, column filters, sort
|
||
- `Dashboards.SelectionModel` — checkbox, select all/visible, bulk actions
|
||
- `Dashboards.GitActionsModel` — git init, sync, commit, pull, push
|
||
|
||
Before decomposition, the model MUST carry `@INVARIANT DECOMPOSITION GATE` with the split plan and line count.
|
||
|
||
## IV. IMPLEMENTATION & ACCESSIBILITY (A11Y)
|
||
|
||
1. **Event Handling:** Use native attributes (e.g., `onclick={handler}`, `onchange={handler}`).
|
||
2. **Transitions:** Use Svelte's built-in transitions (`fade`, `slide`, `fly`) for UI state changes.
|
||
3. **Async Logic:** Every async task (API calls) MUST be handled within a `try/catch` block that:
|
||
- Sets `isLoading = $state(true)` before the call
|
||
- Catches errors and transitions to `Error` `@UX_STATE`
|
||
- Provides `@UX_FEEDBACK` (Toast notification)
|
||
- Sets `isLoading = $state(false)` in `finally`
|
||
4. **A11Y:** Proper ARIA roles (`aria-busy`, `aria-invalid`, `aria-describedby`). Semantic HTML (`<nav>`, `<main>`, `<section>`). Keyboard navigation for modals and drawers.
|
||
|
||
## V. LOGGING (MOLECULAR-COT FOR UI)
|
||
|
||
Frontend logging uses `log()` from `$lib/cot-logger` per **MolecularCoTLogging** protocol. Import: `import { log } from "$lib/cot-logger"`. Full wire-format spec, marker reference, and invariants → `molecular-cot-logging` skill §I-VII.
|
||
|
||
## VI. CANONICAL SVELTE 5 COMPONENT TEMPLATE (SS-TOOLS)
|
||
|
||
Region format for HTML/Svelte comments:
|
||
|
||
```html
|
||
<!-- #region Std.Semantics.MigrationTaskCard [C:3] [TYPE Component] [SEMANTICS ui,migration,task] -->
|
||
<!-- @BRIEF Card displaying a migration task with status, progress, and action buttons. -->
|
||
<!-- @LAYER UI -->
|
||
<!-- @RELATION DEPENDS_ON -> [StatusBadge] -->
|
||
<!-- @RELATION DEPENDS_ON -> [ProgressBar] -->
|
||
<!-- @RELATION BINDS_TO -> [taskDrawerStore] -->
|
||
<!-- @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 -> 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}. -->
|
||
<!-- @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";
|
||
import ProgressBar from "./ProgressBar.svelte";
|
||
|
||
let { taskId, dashboardName, sourceEnv, targetEnv } = $props();
|
||
|
||
let isLoading = $state(false);
|
||
let error: string | null = $state(null);
|
||
let status: "idle" | "loading" | "success" | "error" = $state("idle");
|
||
|
||
async function handleRunMigration() {
|
||
isLoading = true;
|
||
status = "loading";
|
||
error = null;
|
||
log("MigrationTaskCard", "REASON", "Starting migration", {
|
||
taskId, dashboardName, sourceEnv, targetEnv
|
||
});
|
||
try {
|
||
const result = await fetchApi(`/api/tasks/${taskId}/run`, { method: "POST" });
|
||
status = "success";
|
||
log("MigrationTaskCard", "REFLECT", "Migration completed", { taskId, result });
|
||
notificationStore.add({ type: "success", message: $t("migration.completed", { name: dashboardName }) });
|
||
} catch (e) {
|
||
status = "error";
|
||
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;
|
||
}
|
||
}
|
||
|
||
function handleViewLogs() {
|
||
log("MigrationTaskCard", "REASON", "Opening task drawer", { taskId });
|
||
taskDrawerStore.open(taskId);
|
||
}
|
||
</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 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-text">{dashboardName}</h3>
|
||
<StatusBadge status={status} />
|
||
</div>
|
||
|
||
<div class="text-sm text-text-muted mb-3">
|
||
{$t("migration.from")}: {sourceEnv} → {$t("migration.to")}: {targetEnv}
|
||
</div>
|
||
|
||
{#if status === "loading"}
|
||
<ProgressBar />
|
||
{/if}
|
||
|
||
{#if error}
|
||
<p class="text-sm text-destructive mb-2" aria-live="polite">{error}</p>
|
||
{/if}
|
||
|
||
<div class="flex gap-2 mt-3">
|
||
<Button
|
||
variant="primary"
|
||
size="sm"
|
||
onclick={handleRunMigration}
|
||
isLoading={isLoading}
|
||
>
|
||
{status === "error" ? $t("actions.retry") : $t("actions.run")}
|
||
</Button>
|
||
<Button variant="secondary" size="sm" onclick={handleViewLogs}>
|
||
{$t("actions.view_logs")}
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
<!-- #endregion Std.Semantics.MigrationTaskCard -->
|
||
```
|
||
|
||
## VII. SS-TOOLS DESIGN TOKEN CANON & COMPONENT REUSE
|
||
|
||
### Design tokens (source of truth: `frontend/tailwind.config.js`)
|
||
|
||
**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** | All domain components go in `src/lib/components/<domain>/`. The legacy `src/components/` zone has been removed. |
|
||
| **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
|
||
|
||
### Two Testing Layers
|
||
|
||
| Layer | What | Where | Runs |
|
||
|-------|------|-------|------|
|
||
| **Model invariants** | `@INVARIANT` rules, `@ACTION` side effects, `@STATE` transitions | vitest unit test — **no render** | ~10ms |
|
||
| **UX contracts** | `@UX_STATE`, `@UX_FEEDBACK`, `@UX_RECOVERY` visual behavior | vitest with `@testing-library/svelte` + browser validation | ~500ms |
|
||
|
||
**Rule:** Model invariants MUST be verified without render. UX contracts MAY use render + browser. This eliminates the confusion that slows down the feedback loop — a filter-reset invariant doesn't need a DOM.
|
||
|
||
Full test templates (Model invariant + Component UX) → `semantics-testing` §VI-VII.
|
||
|
||
## IX. FRONTEND VERIFICATION
|
||
|
||
```bash
|
||
# From frontend/ directory
|
||
npm run test # Vitest (unit/component tests)
|
||
npm run build # Production build check
|
||
npm run dev # Development server (for browser validation)
|
||
```
|
||
|
||
#endregion Std.Semantics.Svelte
|