diff --git a/client/src/App.tsx b/client/src/App.tsx
index 7fbfac1..a60dca5 100644
--- a/client/src/App.tsx
+++ b/client/src/App.tsx
@@ -1,6 +1,6 @@
import { NavLink, Navigate, Route, Routes } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
-import { Globe, Monitor, ClipboardList, Activity, KeySquare, Braces } from 'lucide-react';
+import { Globe, Monitor, ClipboardList, Activity, KeyRound, Braces } from 'lucide-react';
import type { LucideIcon } from 'lucide-react';
import styles from './App.module.css';
import { SidePanel, ThemeSwitcher } from './ui';
@@ -31,7 +31,7 @@ import { SnippetDetailPage } from './pages/snippet/SnippetDetailPage';
const NAV: { path: string; labelKey: string; Icon: LucideIcon }[] = [
{ path: '/environments', labelKey: 'nav.environments', Icon: Globe },
- { path: '/credentials', labelKey: 'nav.credentials', Icon: KeySquare },
+ { path: '/credentials', labelKey: 'nav.credentials', Icon: KeyRound },
{ path: '/snippets', labelKey: 'nav.snippets', Icon: Braces },
{ path: '/sessions', labelKey: 'nav.sessions', Icon: Monitor },
{ path: '/scenarios', labelKey: 'nav.scenarios', Icon: ClipboardList },
diff --git a/client/src/i18n/locales/en.json b/client/src/i18n/locales/en.json
index 35899b1..b963b61 100644
--- a/client/src/i18n/locales/en.json
+++ b/client/src/i18n/locales/en.json
@@ -32,7 +32,7 @@
"action_add": "Add environment",
"action_edit": "Edit",
"action_save": "Create",
- "action_update": "Save changes",
+ "action_update": "Save",
"action_cancel": "Cancel",
"create_title": "New Environment",
"edit_title": "Edit Environment",
@@ -57,7 +57,7 @@
"action_delete": "Delete",
"action_add": "Add credential",
"action_save": "Create",
- "action_update": "Save changes",
+ "action_update": "Save",
"action_cancel": "Cancel",
"create_title": "New Credential",
"edit_title": "Edit Credential",
@@ -123,7 +123,7 @@
"action_import": "Import",
"import_error": "Failed to import scenario",
"action_save": "Create",
- "action_update": "Save changes",
+ "action_update": "Save",
"action_cancel": "Cancel",
"create_title": "New Scenario",
"edit_title": "Edit Scenario",
@@ -167,7 +167,7 @@
"action_edit": "Edit",
"action_delete": "Delete",
"action_save": "Add step",
- "action_update": "Save changes",
+ "action_update": "Save",
"action_cancel": "Cancel",
"form_order": "Order",
"form_order_invalid": "Order must be a non-negative integer",
@@ -219,7 +219,7 @@
"action_delete": "Delete",
"action_add": "Add snippet",
"action_save": "Create",
- "action_update": "Save changes",
+ "action_update": "Save",
"action_cancel": "Cancel",
"create_title": "New Snippet",
"edit_title": "Edit Snippet",
diff --git a/client/src/pages/Page.module.css b/client/src/pages/Page.module.css
index c768059..a299cb7 100644
--- a/client/src/pages/Page.module.css
+++ b/client/src/pages/Page.module.css
@@ -99,7 +99,8 @@
display: flex;
align-items: center;
gap: var(--space-2);
- flex-wrap: wrap;
+ flex: 1;
+ min-width: 0;
}
.envCardId {
diff --git a/client/src/pages/credential/CreateCredentialPage.tsx b/client/src/pages/credential/CreateCredentialPage.tsx
index 4d6a2e6..f3b00da 100644
--- a/client/src/pages/credential/CreateCredentialPage.tsx
+++ b/client/src/pages/credential/CreateCredentialPage.tsx
@@ -1,6 +1,7 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
+import { KeyRound, X, Save } from 'lucide-react';
import { credentials } from '../../api';
import { Breadcrumbs, Button, Card, Input, CodeEditor } from '../../ui';
import styles from '../Page.module.css';
@@ -49,7 +50,7 @@ export function CreateCredentialPage() {
navigate('/credentials') },
+ { label: t('credentials.title'), icon: , onClick: () => navigate('/credentials') },
{ label: t('credentials.create_title') },
]}
/>
@@ -93,9 +94,11 @@ export function CreateCredentialPage() {
diff --git a/client/src/pages/credential/CredentialDetailPage.tsx b/client/src/pages/credential/CredentialDetailPage.tsx
index 09b7033..e0711d0 100644
--- a/client/src/pages/credential/CredentialDetailPage.tsx
+++ b/client/src/pages/credential/CredentialDetailPage.tsx
@@ -1,7 +1,7 @@
import { useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
-import { Upload, Pencil, Trash2 } from 'lucide-react';
+import { Upload, Pencil, Trash2, KeyRound } from 'lucide-react';
import { CodeBlock } from '../../ui';
import { stringify as yamlStringify } from 'yaml';
import { credentials } from '../../api';
@@ -57,7 +57,7 @@ export function CredentialDetailPage() {
navigate('/credentials') },
+ { label: t('credentials.title'), icon: , onClick: () => navigate('/credentials') },
{ label: credential?.name ?? `#${id}` },
]}
/>
@@ -96,9 +96,9 @@ export function CredentialDetailPage() {
{credential && (
<>
-
+
},
{
@@ -115,10 +115,10 @@ export function CredentialDetailPage() {
},
]}
/>
-
+
{credential.data && (
- <>
+
{t('credentials.section_data')}
- >
+
)}
>
)}
diff --git a/client/src/pages/credential/CredentialsPage.tsx b/client/src/pages/credential/CredentialsPage.tsx
index 72b4f96..e236f58 100644
--- a/client/src/pages/credential/CredentialsPage.tsx
+++ b/client/src/pages/credential/CredentialsPage.tsx
@@ -1,7 +1,7 @@
import { useEffect, useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
-import { Settings, Pencil, Trash2, Plus, Download } from 'lucide-react';
+import { Settings, Pencil, Trash2, Plus, Download, KeyRound } from 'lucide-react';
import { parse as yamlParse } from 'yaml';
import { credentials } from '../../api';
import type { Credential } from '../../api';
@@ -139,7 +139,7 @@ export function CredentialsPage() {
return (
-
+
}]} />
navigate('/credentials') },
+ { label: t('credentials.title'), icon: , onClick: () => navigate('/credentials') },
{
label: credential?.name ?? `#${id}`,
onClick: () => navigate(`/credentials/${id}`),
@@ -123,9 +124,11 @@ export function EditCredentialPage() {
variant="secondary"
onClick={() => navigate(`/credentials/${id}`)}
>
+
{t('credentials.action_cancel')}
diff --git a/client/src/pages/environment/CreateEnvironmentPage.tsx b/client/src/pages/environment/CreateEnvironmentPage.tsx
index 425810f..c6f290b 100644
--- a/client/src/pages/environment/CreateEnvironmentPage.tsx
+++ b/client/src/pages/environment/CreateEnvironmentPage.tsx
@@ -1,6 +1,7 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
+import { X, Globe, Save } from 'lucide-react';
import { environments } from '../../api';
import { Breadcrumbs, Button, Card, Input } from '../../ui';
import styles from '../Page.module.css';
@@ -44,7 +45,7 @@ export function CreateEnvironmentPage() {
navigate('/environments') },
+ { label: t('environments.title'), icon: , onClick: () => navigate('/environments') },
{ label: t('environments.create_title') },
]}
/>
@@ -92,9 +93,11 @@ export function CreateEnvironmentPage() {
diff --git a/client/src/pages/environment/EditEnvironmentPage.tsx b/client/src/pages/environment/EditEnvironmentPage.tsx
index 9e7e070..825583c 100644
--- a/client/src/pages/environment/EditEnvironmentPage.tsx
+++ b/client/src/pages/environment/EditEnvironmentPage.tsx
@@ -1,6 +1,7 @@
import { useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
+import { X, Globe, Save } from 'lucide-react';
import { environments } from '../../api';
import type { Environment } from '../../api';
import { Breadcrumbs, Button, Card, Input } from '../../ui';
@@ -66,7 +67,7 @@ export function EditEnvironmentPage() {
navigate('/environments') },
+ { label: t('environments.title'), icon: , onClick: () => navigate('/environments') },
{
label: env?.name ?? `#${id}`,
onClick: () => navigate(`/environments/${id}`),
@@ -124,9 +125,11 @@ export function EditEnvironmentPage() {
variant="secondary"
onClick={() => navigate(`/environments/${id}`)}
>
+
{t('environments.action_cancel')}
diff --git a/client/src/pages/environment/EnvironmentDetailPage.tsx b/client/src/pages/environment/EnvironmentDetailPage.tsx
index 47121ab..62efa23 100644
--- a/client/src/pages/environment/EnvironmentDetailPage.tsx
+++ b/client/src/pages/environment/EnvironmentDetailPage.tsx
@@ -1,10 +1,10 @@
import { useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
-import { Pencil, Trash2 } from 'lucide-react';
+import { Pencil, Trash2, Globe } from 'lucide-react';
import { environments } from '../../api';
import type { Environment } from '../../api';
-import { Breadcrumbs, Button, Card, DescriptionList, Notification, Timestamp } from '../../ui';
+import { Breadcrumbs, Button, Card, DescriptionList, Notification, Timestamp, UuidBadge } from '../../ui';
import styles from '../Page.module.css';
export function EnvironmentDetailPage() {
@@ -35,7 +35,7 @@ export function EnvironmentDetailPage() {
navigate('/environments') },
+ { label: t('environments.title'), icon: , onClick: () => navigate('/environments') },
{ label: env?.name ?? `#${id}` },
]}
/>
@@ -70,11 +70,11 @@ export function EnvironmentDetailPage() {
{env && (
<>
-
+
},
{
term: t('environments.field_created'),
detail: ,
@@ -85,15 +85,16 @@ export function EnvironmentDetailPage() {
},
]}
/>
-
+
- {t('environments.section_urls')}
-
+
+
{t('environments.section_urls')}
+
{Object.entries(env.urls).filter(([, v]) => v).length === 0 ? (
{t('environments.no_urls')}
) : (
v)
.map(([key, value]) => ({
@@ -107,6 +108,7 @@ export function EnvironmentDetailPage() {
/>
)}
+
>
)}
diff --git a/client/src/pages/environment/EnvironmentsPage.tsx b/client/src/pages/environment/EnvironmentsPage.tsx
index ab0e7a6..13ef5fb 100644
--- a/client/src/pages/environment/EnvironmentsPage.tsx
+++ b/client/src/pages/environment/EnvironmentsPage.tsx
@@ -1,7 +1,7 @@
import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
-import { Settings, ExternalLink, Pencil, Trash2, Plus } from 'lucide-react';
+import { Settings, ExternalLink, Pencil, Trash2, Plus, Globe } from 'lucide-react';
import { environments } from '../../api';
import type { Environment } from '../../api';
import {
@@ -121,7 +121,9 @@ export function EnvironmentsPage() {
return (
-
+
+ }]} />
+
{error &&
{error}
}
{loading ? (
{t('environments.loading')}
diff --git a/client/src/pages/run/AllRunsPage.tsx b/client/src/pages/run/AllRunsPage.tsx
index 892c0f3..3f27207 100644
--- a/client/src/pages/run/AllRunsPage.tsx
+++ b/client/src/pages/run/AllRunsPage.tsx
@@ -1,11 +1,13 @@
-import { useEffect, useRef, useState } from 'react';
+import { useCallback, useEffect, useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
+import { Activity } from 'lucide-react';
import { runs } from '../../api';
import type { ScenarioRun, ScenarioRunStep, ScenarioRunStatus } from '../../api';
import {
AutoRefreshIndicator,
Badge,
+ Breadcrumbs,
Table,
Timestamp,
UuidBadge,
@@ -42,18 +44,21 @@ export function AllRunsPage() {
const [error, setError] = useState
(null);
const [pulseKey, setPulseKey] = useState(0);
const pollRef = useRef | null>(null);
+ const hasDataRef = useRef(false);
- const load = () => {
- setLoading(true);
+ const load = useCallback(() => {
+ if (!hasDataRef.current) setLoading(true);
runs
.listAll()
.then((res) => {
setItems(res.data as AllRunRow[]);
+ setError(null);
setPulseKey((k) => k + 1);
+ hasDataRef.current = true;
})
.catch((err: Error) => setError(err.message))
.finally(() => setLoading(false));
- };
+ }, []);
useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect
@@ -104,11 +109,10 @@ export function AllRunsPage() {
return (
-
{t('runs.title')}
-
+
}]} />
+
- {error &&
{error}
}
= {
@@ -86,6 +87,18 @@ export function RunDetailPage() {
}
};
+ const manualRefresh = () => {
+ if (!id || !runId) return;
+ runs
+ .get(id, runId, logSearchRef.current)
+ .then((r) => {
+ setRun(r);
+ setError(null);
+ setPulseKey((k) => k + 1);
+ })
+ .catch((err: Error) => setError(err.message));
+ };
+
useEffect(() => {
const t = setTimeout(() => {
setDebouncedSearch(logSearch);
@@ -184,19 +197,20 @@ export function RunDetailPage() {
navigate('/scenarios') },
+ { label: t('scenarios.title'), icon: , onClick: () => navigate('/scenarios') },
{
label: scenario?.name ?? `#${id}`,
onClick: () => navigate(`/scenarios/${id}`),
},
{
label: t('runs.title'),
+ icon: ,
onClick: () => navigate(`/scenarios/${id}/runs`),
},
{ label: `#${runId}` },
]}
/>
-
+
{error && (
@@ -210,7 +224,7 @@ export function RunDetailPage() {
<>
},
{
@@ -221,7 +235,6 @@ export function RunDetailPage() {
),
},
- { term: t('runs.field_steps'), detail: run.stepRuns.length },
{ term: t('runs.field_created'), detail: },
{ term: t('runs.field_updated'), detail: },
]}
diff --git a/client/src/pages/run/RunsPage.tsx b/client/src/pages/run/RunsPage.tsx
index ca375b8..3aaf82c 100644
--- a/client/src/pages/run/RunsPage.tsx
+++ b/client/src/pages/run/RunsPage.tsx
@@ -1,7 +1,7 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
-import { Play } from 'lucide-react';
+import { Play, ClipboardList, Activity } from 'lucide-react';
import { scenarios, runs } from '../../api';
import type { Scenario, ScenarioRun, ScenarioRunStep, ScenarioRunStatus } from '../../api';
import {
@@ -44,15 +44,18 @@ export function RunsPage() {
const [error, setError] = useState(null);
const [pulseKey, setPulseKey] = useState(0);
const pollRef = useRef | null>(null);
+ const hasDataRef = useRef(false);
const load = useCallback(() => {
if (!id) return;
- setLoading(true);
+ if (!hasDataRef.current) setLoading(true);
Promise.all([scenarios.get(id!), runs.list(id!)])
.then(([sc, res]) => {
setScenario(sc);
setItems(res.data);
+ setError(null);
setPulseKey((k) => k + 1);
+ hasDataRef.current = true;
})
.catch((err: Error) => setError(err.message))
.finally(() => setLoading(false));
@@ -99,16 +102,16 @@ export function RunsPage() {
navigate('/scenarios') },
+ { label: t('scenarios.title'), icon: , onClick: () => navigate('/scenarios') },
{
label: scenario?.name ?? `#${id}`,
onClick: () => navigate(`/scenarios/${id}`),
},
- { label: t('runs.title') },
+ { label: t('runs.title'), icon: },
]}
/>
-
+
- {error && {error}
}
navigate('/scenarios') },
+ { label: t('scenarios.title'), icon: , onClick: () => navigate('/scenarios') },
{ label: t('scenarios.create_title') },
]}
/>
@@ -64,9 +65,11 @@ export function CreateScenarioPage() {
diff --git a/client/src/pages/scenario/CreateStepPage.tsx b/client/src/pages/scenario/CreateStepPage.tsx
index b08a629..c93ce64 100644
--- a/client/src/pages/scenario/CreateStepPage.tsx
+++ b/client/src/pages/scenario/CreateStepPage.tsx
@@ -1,6 +1,7 @@
import { useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
+import { X, Save, ClipboardList } from 'lucide-react';
import { scenarios, steps } from '../../api';
import type { Scenario } from '../../api';
import { Breadcrumbs, Button, Card, Input, CodeEditor } from '../../ui';
@@ -49,7 +50,7 @@ export function CreateStepPage() {
navigate('/scenarios') },
+ { label: t('scenarios.title'), icon: , onClick: () => navigate('/scenarios') },
{
label: scenario?.name ?? `#${id}`,
onClick: () => navigate(`/scenarios/${id}`),
@@ -82,9 +83,11 @@ export function CreateStepPage() {
diff --git a/client/src/pages/scenario/EditScenarioPage.tsx b/client/src/pages/scenario/EditScenarioPage.tsx
index 43fb7e4..542b2cf 100644
--- a/client/src/pages/scenario/EditScenarioPage.tsx
+++ b/client/src/pages/scenario/EditScenarioPage.tsx
@@ -1,6 +1,7 @@
import { useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
+import { X, Save, ClipboardList } from 'lucide-react';
import { scenarios } from '../../api';
import type { Scenario } from '../../api';
import { Breadcrumbs, Button, Card, Input } from '../../ui';
@@ -53,7 +54,7 @@ export function EditScenarioPage() {
navigate('/scenarios') },
+ { label: t('scenarios.title'), icon: , onClick: () => navigate('/scenarios') },
{
label: scenario?.name ?? `#${id}`,
onClick: () => navigate(`/scenarios/${id}`),
@@ -90,9 +91,11 @@ export function EditScenarioPage() {
variant="secondary"
onClick={() => navigate(`/scenarios/${id}`)}
>
+
{t('scenarios.action_cancel')}
diff --git a/client/src/pages/scenario/EditStepPage.tsx b/client/src/pages/scenario/EditStepPage.tsx
index 56b3e3e..6173b14 100644
--- a/client/src/pages/scenario/EditStepPage.tsx
+++ b/client/src/pages/scenario/EditStepPage.tsx
@@ -1,6 +1,7 @@
import { useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
+import { X, Save, ClipboardList } from 'lucide-react';
import { scenarios, steps } from '../../api';
import type { Scenario, ScenarioStep } from '../../api';
import { Breadcrumbs, Button, Card, Input, CodeEditor } from '../../ui';
@@ -57,7 +58,7 @@ export function EditStepPage() {
navigate('/scenarios') },
+ { label: t('scenarios.title'), icon: , onClick: () => navigate('/scenarios') },
{
label: scenario?.name ?? `#${id}`,
onClick: () => navigate(`/scenarios/${id}`),
@@ -96,9 +97,11 @@ export function EditStepPage() {
variant="secondary"
onClick={() => navigate(`/scenarios/${id}`)}
>
+
{t('steps.action_cancel')}
diff --git a/client/src/pages/scenario/ScenarioDetailPage.tsx b/client/src/pages/scenario/ScenarioDetailPage.tsx
index c98cb5e..501177e 100644
--- a/client/src/pages/scenario/ScenarioDetailPage.tsx
+++ b/client/src/pages/scenario/ScenarioDetailPage.tsx
@@ -1,7 +1,7 @@
import { useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
-import { Play, Pencil, Plus, History, Trash2, Upload, GripVertical } from 'lucide-react';
+import { Play, Pencil, Plus, History, Trash2, Upload, GripVertical, ClipboardList } from 'lucide-react';
import { stringify as yamlStringify } from 'yaml';
import { scenarios, steps, scenarioCredentials, credentials as credentialsApi } from '../../api';
import type { Scenario, ScenarioStep, ScenarioCredential, Credential } from '../../api';
@@ -269,7 +269,7 @@ export function ScenarioDetailPage() {
navigate('/scenarios') },
+ { label: t('scenarios.title'), icon: , onClick: () => navigate('/scenarios') },
{ label: scenario?.name ?? `#${id}` },
]}
/>
@@ -313,7 +313,7 @@ export function ScenarioDetailPage() {
<>
},
{ term: t('scenarios.field_name'), detail: scenario.name },
diff --git a/client/src/pages/scenario/ScenariosPage.tsx b/client/src/pages/scenario/ScenariosPage.tsx
index 787c32d..18f9679 100644
--- a/client/src/pages/scenario/ScenariosPage.tsx
+++ b/client/src/pages/scenario/ScenariosPage.tsx
@@ -1,7 +1,7 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
-import { Play, Plus, Trash2, Download } from 'lucide-react';
+import { Play, Plus, Trash2, Download, ClipboardList } from 'lucide-react';
import { parse as yamlParse } from 'yaml';
import { scenarios } from '../../api';
import type { Scenario } from '../../api';
@@ -89,7 +89,7 @@ export function ScenariosPage() {
return (
-
+
}]} />
navigate('/sessions') },
+ { label: t('sessions.title'), icon: , onClick: () => navigate('/sessions') },
{ label: session?.sessionName ?? `#${id}` },
]}
/>
@@ -69,7 +69,7 @@ export function SessionDetailPage() {
{session && (
},
{ term: t('sessions.field_name'), detail: session.sessionName },
diff --git a/client/src/pages/session/SessionsPage.tsx b/client/src/pages/session/SessionsPage.tsx
index 7a1deb1..9f98f09 100644
--- a/client/src/pages/session/SessionsPage.tsx
+++ b/client/src/pages/session/SessionsPage.tsx
@@ -1,7 +1,7 @@
import { useCallback, useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
-import { Trash2 } from 'lucide-react';
+import { Trash2, Monitor } from 'lucide-react';
import { sessions } from '../../api';
import type { Session } from '../../api';
import {
@@ -75,7 +75,9 @@ export function SessionsPage() {
return (
-
+
+ }]} />
+
{error &&
{error}
}
navigate('/snippets') },
+ { label: t('snippets.title'), icon: , onClick: () => navigate('/snippets') },
{ label: t('snippets.create_title') },
]}
/>
@@ -98,9 +99,11 @@ export function CreateSnippetPage() {
diff --git a/client/src/pages/snippet/EditSnippetPage.tsx b/client/src/pages/snippet/EditSnippetPage.tsx
index 2ce52c0..37d375e 100644
--- a/client/src/pages/snippet/EditSnippetPage.tsx
+++ b/client/src/pages/snippet/EditSnippetPage.tsx
@@ -1,6 +1,7 @@
import { useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
+import { X, Save, Braces } from 'lucide-react';
import { snippets } from '../../api';
import type { Snippet } from '../../api';
import { Breadcrumbs, Button, Card, Input, CodeEditor } from '../../ui';
@@ -68,7 +69,7 @@ export function EditSnippetPage() {
navigate('/snippets') },
+ { label: t('snippets.title'), icon: , onClick: () => navigate('/snippets') },
{
label: snippet?.name ?? `#${id}`,
onClick: () => navigate(`/snippets/${id}`),
@@ -122,9 +123,11 @@ export function EditSnippetPage() {
diff --git a/client/src/pages/snippet/SnippetDetailPage.tsx b/client/src/pages/snippet/SnippetDetailPage.tsx
index 7e86bbd..59ec657 100644
--- a/client/src/pages/snippet/SnippetDetailPage.tsx
+++ b/client/src/pages/snippet/SnippetDetailPage.tsx
@@ -1,7 +1,7 @@
import { useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
-import { Upload, Pencil, Trash2 } from 'lucide-react';
+import { Upload, Pencil, Trash2, Braces } from 'lucide-react';
import { CodeBlock } from '../../ui';
import { stringify as yamlStringify } from 'yaml';
import { snippets } from '../../api';
@@ -57,7 +57,7 @@ export function SnippetDetailPage() {
navigate('/snippets') },
+ { label: t('snippets.title'), icon: , onClick: () => navigate('/snippets') },
{ label: snippet?.name ?? `#${id}` },
]}
/>
@@ -96,9 +96,9 @@ export function SnippetDetailPage() {
{snippet && (
<>
-
+
},
{ term: t('snippets.field_name'), detail: snippet.name },
@@ -116,7 +116,7 @@ export function SnippetDetailPage() {
},
]}
/>
-
+
diff --git a/client/src/pages/snippet/SnippetsPage.tsx b/client/src/pages/snippet/SnippetsPage.tsx
index c94ac0d..b1d1fe8 100644
--- a/client/src/pages/snippet/SnippetsPage.tsx
+++ b/client/src/pages/snippet/SnippetsPage.tsx
@@ -1,7 +1,7 @@
import { useEffect, useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
-import { Code, Pencil, Trash2, Plus, Download } from 'lucide-react';
+import { Code, Pencil, Trash2, Plus, Download, Braces } from 'lucide-react';
import { parse as yamlParse } from 'yaml';
import { snippets } from '../../api';
import type { Snippet } from '../../api';
@@ -116,7 +116,7 @@ export function SnippetsPage() {
return (
-
+
}]} />
void;
}
export function AutoRefreshIndicator({
active,
pulseKey,
label = 'Live',
+ error = false,
+ onClick,
}: AutoRefreshIndicatorProps) {
+ const stateClass = error ? styles.error : active ? styles.active : styles.inactive;
+ const titleText = error
+ ? 'Refresh failed — click to retry'
+ : active
+ ? 'Auto-refreshing'
+ : 'Auto-refresh stopped';
+
return (
(e.key === 'Enter' || e.key === ' ') && onClick() : undefined}
>
{pulseKey !== undefined && pulseKey > 0 && }
- {label}
+ {error ? 'Error' : label}
);
}
diff --git a/client/src/ui/Badge/Badge.module.css b/client/src/ui/Badge/Badge.module.css
index 75a2777..5170b39 100644
--- a/client/src/ui/Badge/Badge.module.css
+++ b/client/src/ui/Badge/Badge.module.css
@@ -9,6 +9,7 @@
border-radius: var(--radius-full);
white-space: nowrap;
line-height: 1.6;
+ box-shadow: var(--shadow-xs);
}
.dot {
diff --git a/client/src/ui/Breadcrumbs/Breadcrumbs.module.css b/client/src/ui/Breadcrumbs/Breadcrumbs.module.css
index 8d04ee2..70c354a 100644
--- a/client/src/ui/Breadcrumbs/Breadcrumbs.module.css
+++ b/client/src/ui/Breadcrumbs/Breadcrumbs.module.css
@@ -1,4 +1,6 @@
.nav {
+ display: flex;
+ align-items: center;
font-family: var(--font-family);
}
@@ -19,6 +21,9 @@
}
.link {
+ display: inline-flex;
+ align-items: center;
+ gap: var(--space-1);
font-size: var(--font-size-sm);
color: var(--color-text-muted);
text-decoration: none;
@@ -42,6 +47,9 @@
}
.current {
+ display: inline-flex;
+ align-items: center;
+ gap: var(--space-1);
font-size: var(--font-size-sm);
font-weight: 600;
color: var(--color-text);
diff --git a/client/src/ui/Breadcrumbs/Breadcrumbs.tsx b/client/src/ui/Breadcrumbs/Breadcrumbs.tsx
index e2d38d4..d951819 100644
--- a/client/src/ui/Breadcrumbs/Breadcrumbs.tsx
+++ b/client/src/ui/Breadcrumbs/Breadcrumbs.tsx
@@ -3,6 +3,7 @@ import styles from './Breadcrumbs.module.css';
export interface BreadcrumbItem {
label: string;
+ icon?: React.ReactNode;
href?: string;
onClick?: (e: React.MouseEvent) => void;
}
@@ -35,15 +36,15 @@ export function Breadcrumbs({ items, separator, className }: BreadcrumbsProps) {
{isLast ? (
- {item.label}
+ {item.icon}{item.label}
) : item.href ? (
- {item.label}
+ {item.icon}{item.label}
) : (
)}
{!isLast && {sep}}
diff --git a/client/src/ui/Button/Button.module.css b/client/src/ui/Button/Button.module.css
index 3b973f7..02c0912 100644
--- a/client/src/ui/Button/Button.module.css
+++ b/client/src/ui/Button/Button.module.css
@@ -11,6 +11,7 @@
white-space: nowrap;
outline: none;
text-decoration: none;
+ box-shadow: var(--shadow-xs);
transition:
background var(--transition),
border-color var(--transition),
diff --git a/client/src/ui/Card/Card.module.css b/client/src/ui/Card/Card.module.css
index 69d0a47..57ecd52 100644
--- a/client/src/ui/Card/Card.module.css
+++ b/client/src/ui/Card/Card.module.css
@@ -7,6 +7,9 @@
}
.header {
+ display: flex;
+ align-items: center;
+ gap: var(--space-2);
padding: var(--space-3) var(--space-6);
font-weight: 600;
font-size: var(--font-size-sm);
diff --git a/client/src/ui/DescriptionList/DescriptionList.module.css b/client/src/ui/DescriptionList/DescriptionList.module.css
index ba1e0c8..b83ade6 100644
--- a/client/src/ui/DescriptionList/DescriptionList.module.css
+++ b/client/src/ui/DescriptionList/DescriptionList.module.css
@@ -45,7 +45,26 @@
max-width: 100%;
}
-/* ── Layout modifiers ───────────────────────────────────────── */
+/* ── Grid layout ───────────────────────────────────────────── */
+
+.grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
+ gap: var(--space-4) var(--space-6);
+}
+
+.grid .row {
+ flex-direction: column;
+ gap: var(--space-1);
+}
+
+.grid .term {
+ margin-bottom: 0;
+}
+
+.grid .detail {
+ font-size: var(--font-size-sm);
+}
.inline .row {
flex-direction: row;
diff --git a/client/src/ui/DescriptionList/DescriptionList.tsx b/client/src/ui/DescriptionList/DescriptionList.tsx
index bd25985..255ac95 100644
--- a/client/src/ui/DescriptionList/DescriptionList.tsx
+++ b/client/src/ui/DescriptionList/DescriptionList.tsx
@@ -16,8 +16,8 @@ export interface DescriptionListItem {
export interface DescriptionListProps {
items: DescriptionListItem[];
- /** 'compact' stacks term above detail; 'inline' places them side by side; 'comfortable' adds generous spacing with dividers (default) */
- layout?: 'inline' | 'compact' | 'comfortable';
+ /** 'compact' stacks term above detail; 'inline' places them side by side; 'comfortable' adds generous spacing with dividers (default); 'grid' flows items into a responsive multi-column grid */
+ layout?: 'inline' | 'compact' | 'comfortable' | 'grid';
/** Truncate detail values with ellipsis; full value shown in a tooltip on hover */
truncate?: boolean;
className?: string;
diff --git a/client/src/ui/Input/Input.module.css b/client/src/ui/Input/Input.module.css
index c7cd76e..5ee10cf 100644
--- a/client/src/ui/Input/Input.module.css
+++ b/client/src/ui/Input/Input.module.css
@@ -23,6 +23,7 @@
height: 36px;
width: 100%;
outline: none;
+ box-shadow: var(--shadow-xs);
transition: border-color var(--transition), box-shadow var(--transition);
}
diff --git a/client/src/ui/tokens.css b/client/src/ui/tokens.css
index 6a9aeba..50a1aa4 100644
--- a/client/src/ui/tokens.css
+++ b/client/src/ui/tokens.css
@@ -74,7 +74,8 @@
--border-width: 1.5px;
/* Shadows */
- --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.06);
+ --shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.08), 0 0px 1px rgba(0, 0, 0, 0.04);
+ --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.10), 0 1px 2px rgba(0, 0, 0, 0.06);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.07), 0 2px 4px -1px rgba(0, 0, 0, 0.04);
/* Transitions */