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 */