feat(ui): add breadcrumb icons, fix alignment, and remove duplicate form headers
- add section icons to all breadcrumbs across every page - fix breadcrumb nav display:flex so icons align vertically with buttons - wrap all page breadcrumbs in pageToolbar div for consistent layout - add Breadcrumbs to AllRunsPage which previously used a plain h1 - fix envCardHeader flex:1 so settings button hugs the right edge - remove card header from environment create/edit forms (duplicated breadcrumb)
This commit is contained in:
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
import { NavLink, Navigate, Route, Routes } from 'react-router-dom';
|
import { NavLink, Navigate, Route, Routes } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
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 type { LucideIcon } from 'lucide-react';
|
||||||
import styles from './App.module.css';
|
import styles from './App.module.css';
|
||||||
import { SidePanel, ThemeSwitcher } from './ui';
|
import { SidePanel, ThemeSwitcher } from './ui';
|
||||||
@@ -31,7 +31,7 @@ import { SnippetDetailPage } from './pages/snippet/SnippetDetailPage';
|
|||||||
|
|
||||||
const NAV: { path: string; labelKey: string; Icon: LucideIcon }[] = [
|
const NAV: { path: string; labelKey: string; Icon: LucideIcon }[] = [
|
||||||
{ path: '/environments', labelKey: 'nav.environments', Icon: Globe },
|
{ 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: '/snippets', labelKey: 'nav.snippets', Icon: Braces },
|
||||||
{ path: '/sessions', labelKey: 'nav.sessions', Icon: Monitor },
|
{ path: '/sessions', labelKey: 'nav.sessions', Icon: Monitor },
|
||||||
{ path: '/scenarios', labelKey: 'nav.scenarios', Icon: ClipboardList },
|
{ path: '/scenarios', labelKey: 'nav.scenarios', Icon: ClipboardList },
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
"action_add": "Add environment",
|
"action_add": "Add environment",
|
||||||
"action_edit": "Edit",
|
"action_edit": "Edit",
|
||||||
"action_save": "Create",
|
"action_save": "Create",
|
||||||
"action_update": "Save changes",
|
"action_update": "Save",
|
||||||
"action_cancel": "Cancel",
|
"action_cancel": "Cancel",
|
||||||
"create_title": "New Environment",
|
"create_title": "New Environment",
|
||||||
"edit_title": "Edit Environment",
|
"edit_title": "Edit Environment",
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
"action_delete": "Delete",
|
"action_delete": "Delete",
|
||||||
"action_add": "Add credential",
|
"action_add": "Add credential",
|
||||||
"action_save": "Create",
|
"action_save": "Create",
|
||||||
"action_update": "Save changes",
|
"action_update": "Save",
|
||||||
"action_cancel": "Cancel",
|
"action_cancel": "Cancel",
|
||||||
"create_title": "New Credential",
|
"create_title": "New Credential",
|
||||||
"edit_title": "Edit Credential",
|
"edit_title": "Edit Credential",
|
||||||
@@ -123,7 +123,7 @@
|
|||||||
"action_import": "Import",
|
"action_import": "Import",
|
||||||
"import_error": "Failed to import scenario",
|
"import_error": "Failed to import scenario",
|
||||||
"action_save": "Create",
|
"action_save": "Create",
|
||||||
"action_update": "Save changes",
|
"action_update": "Save",
|
||||||
"action_cancel": "Cancel",
|
"action_cancel": "Cancel",
|
||||||
"create_title": "New Scenario",
|
"create_title": "New Scenario",
|
||||||
"edit_title": "Edit Scenario",
|
"edit_title": "Edit Scenario",
|
||||||
@@ -167,7 +167,7 @@
|
|||||||
"action_edit": "Edit",
|
"action_edit": "Edit",
|
||||||
"action_delete": "Delete",
|
"action_delete": "Delete",
|
||||||
"action_save": "Add step",
|
"action_save": "Add step",
|
||||||
"action_update": "Save changes",
|
"action_update": "Save",
|
||||||
"action_cancel": "Cancel",
|
"action_cancel": "Cancel",
|
||||||
"form_order": "Order",
|
"form_order": "Order",
|
||||||
"form_order_invalid": "Order must be a non-negative integer",
|
"form_order_invalid": "Order must be a non-negative integer",
|
||||||
@@ -219,7 +219,7 @@
|
|||||||
"action_delete": "Delete",
|
"action_delete": "Delete",
|
||||||
"action_add": "Add snippet",
|
"action_add": "Add snippet",
|
||||||
"action_save": "Create",
|
"action_save": "Create",
|
||||||
"action_update": "Save changes",
|
"action_update": "Save",
|
||||||
"action_cancel": "Cancel",
|
"action_cancel": "Cancel",
|
||||||
"create_title": "New Snippet",
|
"create_title": "New Snippet",
|
||||||
"edit_title": "Edit Snippet",
|
"edit_title": "Edit Snippet",
|
||||||
|
|||||||
@@ -99,7 +99,8 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--space-2);
|
gap: var(--space-2);
|
||||||
flex-wrap: wrap;
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.envCardId {
|
.envCardId {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { KeyRound, X, Save } from 'lucide-react';
|
||||||
import { credentials } from '../../api';
|
import { credentials } from '../../api';
|
||||||
import { Breadcrumbs, Button, Card, Input, CodeEditor } from '../../ui';
|
import { Breadcrumbs, Button, Card, Input, CodeEditor } from '../../ui';
|
||||||
import styles from '../Page.module.css';
|
import styles from '../Page.module.css';
|
||||||
@@ -49,7 +50,7 @@ export function CreateCredentialPage() {
|
|||||||
<div className={styles.pageToolbar}>
|
<div className={styles.pageToolbar}>
|
||||||
<Breadcrumbs
|
<Breadcrumbs
|
||||||
items={[
|
items={[
|
||||||
{ label: t('credentials.title'), onClick: () => navigate('/credentials') },
|
{ label: t('credentials.title'), icon: <KeyRound size={14} />, onClick: () => navigate('/credentials') },
|
||||||
{ label: t('credentials.create_title') },
|
{ label: t('credentials.create_title') },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
@@ -93,9 +94,11 @@ export function CreateCredentialPage() {
|
|||||||
|
|
||||||
<div className={styles.formActions}>
|
<div className={styles.formActions}>
|
||||||
<Button type="button" variant="secondary" onClick={() => navigate('/credentials')}>
|
<Button type="button" variant="secondary" onClick={() => navigate('/credentials')}>
|
||||||
|
<X size={14} />
|
||||||
{t('credentials.action_cancel')}
|
{t('credentials.action_cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" loading={saving}>
|
<Button type="submit" loading={saving}>
|
||||||
|
<Save size={14} />
|
||||||
{t('credentials.action_save')}
|
{t('credentials.action_save')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
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 { CodeBlock } from '../../ui';
|
||||||
import { stringify as yamlStringify } from 'yaml';
|
import { stringify as yamlStringify } from 'yaml';
|
||||||
import { credentials } from '../../api';
|
import { credentials } from '../../api';
|
||||||
@@ -57,7 +57,7 @@ export function CredentialDetailPage() {
|
|||||||
<div className={styles.pageToolbar}>
|
<div className={styles.pageToolbar}>
|
||||||
<Breadcrumbs
|
<Breadcrumbs
|
||||||
items={[
|
items={[
|
||||||
{ label: t('credentials.title'), onClick: () => navigate('/credentials') },
|
{ label: t('credentials.title'), icon: <KeyRound size={14} />, onClick: () => navigate('/credentials') },
|
||||||
{ label: credential?.name ?? `#${id}` },
|
{ label: credential?.name ?? `#${id}` },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
@@ -96,9 +96,9 @@ export function CredentialDetailPage() {
|
|||||||
|
|
||||||
{credential && (
|
{credential && (
|
||||||
<>
|
<>
|
||||||
<div className={styles.detailMeta}>
|
<Card>
|
||||||
<DescriptionList
|
<DescriptionList
|
||||||
layout="comfortable"
|
layout="grid"
|
||||||
items={[
|
items={[
|
||||||
{ term: t('credentials.field_id'), detail: <UuidBadge id={credential.id} /> },
|
{ term: t('credentials.field_id'), detail: <UuidBadge id={credential.id} /> },
|
||||||
{
|
{
|
||||||
@@ -115,10 +115,10 @@ export function CredentialDetailPage() {
|
|||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</Card>
|
||||||
|
|
||||||
{credential.data && (
|
{credential.data && (
|
||||||
<>
|
<div className={styles.stepsSection}>
|
||||||
<h2 className={styles.sectionHeading}>{t('credentials.section_data')}</h2>
|
<h2 className={styles.sectionHeading}>{t('credentials.section_data')}</h2>
|
||||||
<Card>
|
<Card>
|
||||||
<CodeBlock
|
<CodeBlock
|
||||||
@@ -132,7 +132,7 @@ export function CredentialDetailPage() {
|
|||||||
})()}
|
})()}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
</>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
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 { parse as yamlParse } from 'yaml';
|
||||||
import { credentials } from '../../api';
|
import { credentials } from '../../api';
|
||||||
import type { Credential } from '../../api';
|
import type { Credential } from '../../api';
|
||||||
@@ -139,7 +139,7 @@ export function CredentialsPage() {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className={styles.pageToolbar}>
|
<div className={styles.pageToolbar}>
|
||||||
<Breadcrumbs items={[{ label: t('credentials.title') }]} className={styles.breadcrumbs} />
|
<Breadcrumbs items={[{ label: t('credentials.title'), icon: <KeyRound size={14} /> }]} />
|
||||||
<div className={styles.toolbarActions}>
|
<div className={styles.toolbarActions}>
|
||||||
<input
|
<input
|
||||||
ref={fileInputRef}
|
ref={fileInputRef}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { KeyRound, X, Save } from 'lucide-react';
|
||||||
import { credentials } from '../../api';
|
import { credentials } from '../../api';
|
||||||
import type { Credential } from '../../api';
|
import type { Credential } from '../../api';
|
||||||
import { Breadcrumbs, Button, Card, Input, CodeEditor } from '../../ui';
|
import { Breadcrumbs, Button, Card, Input, CodeEditor } from '../../ui';
|
||||||
@@ -69,7 +70,7 @@ export function EditCredentialPage() {
|
|||||||
<div className={styles.pageToolbar}>
|
<div className={styles.pageToolbar}>
|
||||||
<Breadcrumbs
|
<Breadcrumbs
|
||||||
items={[
|
items={[
|
||||||
{ label: t('credentials.title'), onClick: () => navigate('/credentials') },
|
{ label: t('credentials.title'), icon: <KeyRound size={14} />, onClick: () => navigate('/credentials') },
|
||||||
{
|
{
|
||||||
label: credential?.name ?? `#${id}`,
|
label: credential?.name ?? `#${id}`,
|
||||||
onClick: () => navigate(`/credentials/${id}`),
|
onClick: () => navigate(`/credentials/${id}`),
|
||||||
@@ -123,9 +124,11 @@ export function EditCredentialPage() {
|
|||||||
variant="secondary"
|
variant="secondary"
|
||||||
onClick={() => navigate(`/credentials/${id}`)}
|
onClick={() => navigate(`/credentials/${id}`)}
|
||||||
>
|
>
|
||||||
|
<X size={14} />
|
||||||
{t('credentials.action_cancel')}
|
{t('credentials.action_cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" loading={saving}>
|
<Button type="submit" loading={saving}>
|
||||||
|
<Save size={14} />
|
||||||
{t('credentials.action_update')}
|
{t('credentials.action_update')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { X, Globe, Save } from 'lucide-react';
|
||||||
import { environments } from '../../api';
|
import { environments } from '../../api';
|
||||||
import { Breadcrumbs, Button, Card, Input } from '../../ui';
|
import { Breadcrumbs, Button, Card, Input } from '../../ui';
|
||||||
import styles from '../Page.module.css';
|
import styles from '../Page.module.css';
|
||||||
@@ -44,7 +45,7 @@ export function CreateEnvironmentPage() {
|
|||||||
<div className={styles.pageToolbar}>
|
<div className={styles.pageToolbar}>
|
||||||
<Breadcrumbs
|
<Breadcrumbs
|
||||||
items={[
|
items={[
|
||||||
{ label: t('environments.title'), onClick: () => navigate('/environments') },
|
{ label: t('environments.title'), icon: <Globe size={14} />, onClick: () => navigate('/environments') },
|
||||||
{ label: t('environments.create_title') },
|
{ label: t('environments.create_title') },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
@@ -92,9 +93,11 @@ export function CreateEnvironmentPage() {
|
|||||||
|
|
||||||
<div className={styles.formActions}>
|
<div className={styles.formActions}>
|
||||||
<Button type="button" variant="secondary" onClick={() => navigate('/environments')}>
|
<Button type="button" variant="secondary" onClick={() => navigate('/environments')}>
|
||||||
|
<X size={14} />
|
||||||
{t('environments.action_cancel')}
|
{t('environments.action_cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" loading={saving}>
|
<Button type="submit" loading={saving}>
|
||||||
|
<Save size={14} />
|
||||||
{t('environments.action_save')}
|
{t('environments.action_save')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { X, Globe, Save } from 'lucide-react';
|
||||||
import { environments } from '../../api';
|
import { environments } from '../../api';
|
||||||
import type { Environment } from '../../api';
|
import type { Environment } from '../../api';
|
||||||
import { Breadcrumbs, Button, Card, Input } from '../../ui';
|
import { Breadcrumbs, Button, Card, Input } from '../../ui';
|
||||||
@@ -66,7 +67,7 @@ export function EditEnvironmentPage() {
|
|||||||
<div className={styles.pageToolbar}>
|
<div className={styles.pageToolbar}>
|
||||||
<Breadcrumbs
|
<Breadcrumbs
|
||||||
items={[
|
items={[
|
||||||
{ label: t('environments.title'), onClick: () => navigate('/environments') },
|
{ label: t('environments.title'), icon: <Globe size={14} />, onClick: () => navigate('/environments') },
|
||||||
{
|
{
|
||||||
label: env?.name ?? `#${id}`,
|
label: env?.name ?? `#${id}`,
|
||||||
onClick: () => navigate(`/environments/${id}`),
|
onClick: () => navigate(`/environments/${id}`),
|
||||||
@@ -124,9 +125,11 @@ export function EditEnvironmentPage() {
|
|||||||
variant="secondary"
|
variant="secondary"
|
||||||
onClick={() => navigate(`/environments/${id}`)}
|
onClick={() => navigate(`/environments/${id}`)}
|
||||||
>
|
>
|
||||||
|
<X size={14} />
|
||||||
{t('environments.action_cancel')}
|
{t('environments.action_cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" loading={saving}>
|
<Button type="submit" loading={saving}>
|
||||||
|
<Save size={14} />
|
||||||
{t('environments.action_update')}
|
{t('environments.action_update')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Pencil, Trash2 } from 'lucide-react';
|
import { Pencil, Trash2, Globe } from 'lucide-react';
|
||||||
import { environments } from '../../api';
|
import { environments } from '../../api';
|
||||||
import type { Environment } 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';
|
import styles from '../Page.module.css';
|
||||||
|
|
||||||
export function EnvironmentDetailPage() {
|
export function EnvironmentDetailPage() {
|
||||||
@@ -35,7 +35,7 @@ export function EnvironmentDetailPage() {
|
|||||||
<div className={styles.pageToolbar}>
|
<div className={styles.pageToolbar}>
|
||||||
<Breadcrumbs
|
<Breadcrumbs
|
||||||
items={[
|
items={[
|
||||||
{ label: t('environments.title'), onClick: () => navigate('/environments') },
|
{ label: t('environments.title'), icon: <Globe size={14} />, onClick: () => navigate('/environments') },
|
||||||
{ label: env?.name ?? `#${id}` },
|
{ label: env?.name ?? `#${id}` },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
@@ -70,11 +70,11 @@ export function EnvironmentDetailPage() {
|
|||||||
|
|
||||||
{env && (
|
{env && (
|
||||||
<>
|
<>
|
||||||
<div className={styles.detailMeta}>
|
<Card>
|
||||||
<DescriptionList
|
<DescriptionList
|
||||||
layout="comfortable"
|
layout="grid"
|
||||||
items={[
|
items={[
|
||||||
{ term: t('environments.field_id'), detail: env.id },
|
{ term: t('environments.field_id'), detail: <UuidBadge id={env.id} /> },
|
||||||
{
|
{
|
||||||
term: t('environments.field_created'),
|
term: t('environments.field_created'),
|
||||||
detail: <Timestamp value={env.createdAt} />,
|
detail: <Timestamp value={env.createdAt} />,
|
||||||
@@ -85,15 +85,16 @@ export function EnvironmentDetailPage() {
|
|||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</Card>
|
||||||
|
|
||||||
<h2 className={styles.sectionHeading}>{t('environments.section_urls')}</h2>
|
<div className={styles.stepsSection}>
|
||||||
<Card>
|
<h2 className={styles.sectionHeading}>{t('environments.section_urls')}</h2>
|
||||||
|
<Card>
|
||||||
{Object.entries(env.urls).filter(([, v]) => v).length === 0 ? (
|
{Object.entries(env.urls).filter(([, v]) => v).length === 0 ? (
|
||||||
<p className={styles.muted}>{t('environments.no_urls')}</p>
|
<p className={styles.muted}>{t('environments.no_urls')}</p>
|
||||||
) : (
|
) : (
|
||||||
<DescriptionList
|
<DescriptionList
|
||||||
layout="comfortable"
|
layout="grid"
|
||||||
items={Object.entries(env.urls)
|
items={Object.entries(env.urls)
|
||||||
.filter(([, v]) => v)
|
.filter(([, v]) => v)
|
||||||
.map(([key, value]) => ({
|
.map(([key, value]) => ({
|
||||||
@@ -107,6 +108,7 @@ export function EnvironmentDetailPage() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
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 { environments } from '../../api';
|
||||||
import type { Environment } from '../../api';
|
import type { Environment } from '../../api';
|
||||||
import {
|
import {
|
||||||
@@ -121,7 +121,9 @@ export function EnvironmentsPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Breadcrumbs items={[{ label: t('environments.title') }]} className={styles.breadcrumbs} />
|
<div className={styles.pageToolbar}>
|
||||||
|
<Breadcrumbs items={[{ label: t('environments.title'), icon: <Globe size={14} /> }]} />
|
||||||
|
</div>
|
||||||
{error && <p className={styles.error}>{error}</p>}
|
{error && <p className={styles.error}>{error}</p>}
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<p className={styles.muted}>{t('environments.loading')}</p>
|
<p className={styles.muted}>{t('environments.loading')}</p>
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Activity } from 'lucide-react';
|
||||||
import { runs } from '../../api';
|
import { runs } from '../../api';
|
||||||
import type { ScenarioRun, ScenarioRunStep, ScenarioRunStatus } from '../../api';
|
import type { ScenarioRun, ScenarioRunStep, ScenarioRunStatus } from '../../api';
|
||||||
import {
|
import {
|
||||||
AutoRefreshIndicator,
|
AutoRefreshIndicator,
|
||||||
Badge,
|
Badge,
|
||||||
|
Breadcrumbs,
|
||||||
Table,
|
Table,
|
||||||
Timestamp,
|
Timestamp,
|
||||||
UuidBadge,
|
UuidBadge,
|
||||||
@@ -42,18 +44,21 @@ export function AllRunsPage() {
|
|||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [pulseKey, setPulseKey] = useState(0);
|
const [pulseKey, setPulseKey] = useState(0);
|
||||||
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||||
|
const hasDataRef = useRef(false);
|
||||||
|
|
||||||
const load = () => {
|
const load = useCallback(() => {
|
||||||
setLoading(true);
|
if (!hasDataRef.current) setLoading(true);
|
||||||
runs
|
runs
|
||||||
.listAll()
|
.listAll()
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
setItems(res.data as AllRunRow[]);
|
setItems(res.data as AllRunRow[]);
|
||||||
|
setError(null);
|
||||||
setPulseKey((k) => k + 1);
|
setPulseKey((k) => k + 1);
|
||||||
|
hasDataRef.current = true;
|
||||||
})
|
})
|
||||||
.catch((err: Error) => setError(err.message))
|
.catch((err: Error) => setError(err.message))
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||||
@@ -104,11 +109,10 @@ export function AllRunsPage() {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className={styles.pageToolbar}>
|
<div className={styles.pageToolbar}>
|
||||||
<h1 className={styles.pageTitle}>{t('runs.title')}</h1>
|
<Breadcrumbs items={[{ label: t('runs.title'), icon: <Activity size={14} /> }]} />
|
||||||
<AutoRefreshIndicator active pulseKey={pulseKey} />
|
<AutoRefreshIndicator active pulseKey={pulseKey} error={!!error} onClick={load} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && <p className={styles.error}>{error}</p>}
|
|
||||||
<Table
|
<Table
|
||||||
columns={columns}
|
columns={columns}
|
||||||
data={items}
|
data={items}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import {
|
|||||||
type TableColumn,
|
type TableColumn,
|
||||||
} from '../../ui';
|
} from '../../ui';
|
||||||
import type { BadgeVariant } from '../../ui';
|
import type { BadgeVariant } from '../../ui';
|
||||||
|
import { ClipboardList, Activity } from 'lucide-react';
|
||||||
import styles from '../Page.module.css';
|
import styles from '../Page.module.css';
|
||||||
|
|
||||||
const RUN_STATUS_VARIANT: Record<ScenarioRunStatus, BadgeVariant> = {
|
const RUN_STATUS_VARIANT: Record<ScenarioRunStatus, BadgeVariant> = {
|
||||||
@@ -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(() => {
|
useEffect(() => {
|
||||||
const t = setTimeout(() => {
|
const t = setTimeout(() => {
|
||||||
setDebouncedSearch(logSearch);
|
setDebouncedSearch(logSearch);
|
||||||
@@ -184,19 +197,20 @@ export function RunDetailPage() {
|
|||||||
<div className={styles.pageToolbar}>
|
<div className={styles.pageToolbar}>
|
||||||
<Breadcrumbs
|
<Breadcrumbs
|
||||||
items={[
|
items={[
|
||||||
{ label: t('scenarios.title'), onClick: () => navigate('/scenarios') },
|
{ label: t('scenarios.title'), icon: <ClipboardList size={14} />, onClick: () => navigate('/scenarios') },
|
||||||
{
|
{
|
||||||
label: scenario?.name ?? `#${id}`,
|
label: scenario?.name ?? `#${id}`,
|
||||||
onClick: () => navigate(`/scenarios/${id}`),
|
onClick: () => navigate(`/scenarios/${id}`),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t('runs.title'),
|
label: t('runs.title'),
|
||||||
|
icon: <Activity size={14} />,
|
||||||
onClick: () => navigate(`/scenarios/${id}/runs`),
|
onClick: () => navigate(`/scenarios/${id}/runs`),
|
||||||
},
|
},
|
||||||
{ label: `#${runId}` },
|
{ label: `#${runId}` },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<AutoRefreshIndicator active={polling} pulseKey={pulseKey} />
|
<AutoRefreshIndicator active={polling} pulseKey={pulseKey} error={!!error} onClick={manualRefresh} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
@@ -210,7 +224,7 @@ export function RunDetailPage() {
|
|||||||
<>
|
<>
|
||||||
<Card>
|
<Card>
|
||||||
<DescriptionList
|
<DescriptionList
|
||||||
layout="comfortable"
|
layout="grid"
|
||||||
items={[
|
items={[
|
||||||
{ term: t('runs.field_id'), detail: <UuidBadge id={run.id} /> },
|
{ term: t('runs.field_id'), detail: <UuidBadge id={run.id} /> },
|
||||||
{
|
{
|
||||||
@@ -221,7 +235,6 @@ export function RunDetailPage() {
|
|||||||
</Badge>
|
</Badge>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{ term: t('runs.field_steps'), detail: run.stepRuns.length },
|
|
||||||
{ term: t('runs.field_created'), detail: <Timestamp value={run.createdAt} /> },
|
{ term: t('runs.field_created'), detail: <Timestamp value={run.createdAt} /> },
|
||||||
{ term: t('runs.field_updated'), detail: <Timestamp value={run.updatedAt} /> },
|
{ term: t('runs.field_updated'), detail: <Timestamp value={run.updatedAt} /> },
|
||||||
]}
|
]}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Play } from 'lucide-react';
|
import { Play, ClipboardList, Activity } from 'lucide-react';
|
||||||
import { scenarios, runs } from '../../api';
|
import { scenarios, runs } from '../../api';
|
||||||
import type { Scenario, ScenarioRun, ScenarioRunStep, ScenarioRunStatus } from '../../api';
|
import type { Scenario, ScenarioRun, ScenarioRunStep, ScenarioRunStatus } from '../../api';
|
||||||
import {
|
import {
|
||||||
@@ -44,15 +44,18 @@ export function RunsPage() {
|
|||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [pulseKey, setPulseKey] = useState(0);
|
const [pulseKey, setPulseKey] = useState(0);
|
||||||
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||||
|
const hasDataRef = useRef(false);
|
||||||
|
|
||||||
const load = useCallback(() => {
|
const load = useCallback(() => {
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
setLoading(true);
|
if (!hasDataRef.current) setLoading(true);
|
||||||
Promise.all([scenarios.get(id!), runs.list(id!)])
|
Promise.all([scenarios.get(id!), runs.list(id!)])
|
||||||
.then(([sc, res]) => {
|
.then(([sc, res]) => {
|
||||||
setScenario(sc);
|
setScenario(sc);
|
||||||
setItems(res.data);
|
setItems(res.data);
|
||||||
|
setError(null);
|
||||||
setPulseKey((k) => k + 1);
|
setPulseKey((k) => k + 1);
|
||||||
|
hasDataRef.current = true;
|
||||||
})
|
})
|
||||||
.catch((err: Error) => setError(err.message))
|
.catch((err: Error) => setError(err.message))
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
@@ -99,16 +102,16 @@ export function RunsPage() {
|
|||||||
<div className={styles.pageToolbar}>
|
<div className={styles.pageToolbar}>
|
||||||
<Breadcrumbs
|
<Breadcrumbs
|
||||||
items={[
|
items={[
|
||||||
{ label: t('scenarios.title'), onClick: () => navigate('/scenarios') },
|
{ label: t('scenarios.title'), icon: <ClipboardList size={14} />, onClick: () => navigate('/scenarios') },
|
||||||
{
|
{
|
||||||
label: scenario?.name ?? `#${id}`,
|
label: scenario?.name ?? `#${id}`,
|
||||||
onClick: () => navigate(`/scenarios/${id}`),
|
onClick: () => navigate(`/scenarios/${id}`),
|
||||||
},
|
},
|
||||||
{ label: t('runs.title') },
|
{ label: t('runs.title'), icon: <Activity size={14} /> },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<div className={styles.toolbarActions}>
|
<div className={styles.toolbarActions}>
|
||||||
<AutoRefreshIndicator active pulseKey={pulseKey} />
|
<AutoRefreshIndicator active pulseKey={pulseKey} error={!!error} onClick={load} />
|
||||||
<Button size="sm" onClick={handleRun}>
|
<Button size="sm" onClick={handleRun}>
|
||||||
<Play size={14} />
|
<Play size={14} />
|
||||||
{t('runs.action_run')}
|
{t('runs.action_run')}
|
||||||
@@ -116,7 +119,6 @@ export function RunsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && <p className={styles.error}>{error}</p>}
|
|
||||||
<Table
|
<Table
|
||||||
columns={columns}
|
columns={columns}
|
||||||
data={items}
|
data={items}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { X, Save, ClipboardList } from 'lucide-react';
|
||||||
import { scenarios } from '../../api';
|
import { scenarios } from '../../api';
|
||||||
import { Breadcrumbs, Button, Card, Input } from '../../ui';
|
import { Breadcrumbs, Button, Card, Input } from '../../ui';
|
||||||
import styles from '../Page.module.css';
|
import styles from '../Page.module.css';
|
||||||
@@ -37,7 +38,7 @@ export function CreateScenarioPage() {
|
|||||||
<div className={styles.pageToolbar}>
|
<div className={styles.pageToolbar}>
|
||||||
<Breadcrumbs
|
<Breadcrumbs
|
||||||
items={[
|
items={[
|
||||||
{ label: t('scenarios.title'), onClick: () => navigate('/scenarios') },
|
{ label: t('scenarios.title'), icon: <ClipboardList size={14} />, onClick: () => navigate('/scenarios') },
|
||||||
{ label: t('scenarios.create_title') },
|
{ label: t('scenarios.create_title') },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
@@ -64,9 +65,11 @@ export function CreateScenarioPage() {
|
|||||||
|
|
||||||
<div className={styles.formActions}>
|
<div className={styles.formActions}>
|
||||||
<Button type="button" variant="secondary" onClick={() => navigate('/scenarios')}>
|
<Button type="button" variant="secondary" onClick={() => navigate('/scenarios')}>
|
||||||
|
<X size={14} />
|
||||||
{t('scenarios.action_cancel')}
|
{t('scenarios.action_cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" loading={saving}>
|
<Button type="submit" loading={saving}>
|
||||||
|
<Save size={14} />
|
||||||
{t('scenarios.action_save')}
|
{t('scenarios.action_save')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { X, Save, ClipboardList } from 'lucide-react';
|
||||||
import { scenarios, steps } from '../../api';
|
import { scenarios, steps } from '../../api';
|
||||||
import type { Scenario } from '../../api';
|
import type { Scenario } from '../../api';
|
||||||
import { Breadcrumbs, Button, Card, Input, CodeEditor } from '../../ui';
|
import { Breadcrumbs, Button, Card, Input, CodeEditor } from '../../ui';
|
||||||
@@ -49,7 +50,7 @@ export function CreateStepPage() {
|
|||||||
<div className={styles.pageToolbar}>
|
<div className={styles.pageToolbar}>
|
||||||
<Breadcrumbs
|
<Breadcrumbs
|
||||||
items={[
|
items={[
|
||||||
{ label: t('scenarios.title'), onClick: () => navigate('/scenarios') },
|
{ label: t('scenarios.title'), icon: <ClipboardList size={14} />, onClick: () => navigate('/scenarios') },
|
||||||
{
|
{
|
||||||
label: scenario?.name ?? `#${id}`,
|
label: scenario?.name ?? `#${id}`,
|
||||||
onClick: () => navigate(`/scenarios/${id}`),
|
onClick: () => navigate(`/scenarios/${id}`),
|
||||||
@@ -82,9 +83,11 @@ export function CreateStepPage() {
|
|||||||
|
|
||||||
<div className={styles.formActions}>
|
<div className={styles.formActions}>
|
||||||
<Button type="button" variant="secondary" onClick={() => navigate(`/scenarios/${id}`)}>
|
<Button type="button" variant="secondary" onClick={() => navigate(`/scenarios/${id}`)}>
|
||||||
|
<X size={14} />
|
||||||
{t('steps.action_cancel')}
|
{t('steps.action_cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" loading={saving}>
|
<Button type="submit" loading={saving}>
|
||||||
|
<Save size={14} />
|
||||||
{t('steps.action_save')}
|
{t('steps.action_save')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { X, Save, ClipboardList } from 'lucide-react';
|
||||||
import { scenarios } from '../../api';
|
import { scenarios } from '../../api';
|
||||||
import type { Scenario } from '../../api';
|
import type { Scenario } from '../../api';
|
||||||
import { Breadcrumbs, Button, Card, Input } from '../../ui';
|
import { Breadcrumbs, Button, Card, Input } from '../../ui';
|
||||||
@@ -53,7 +54,7 @@ export function EditScenarioPage() {
|
|||||||
<div className={styles.pageToolbar}>
|
<div className={styles.pageToolbar}>
|
||||||
<Breadcrumbs
|
<Breadcrumbs
|
||||||
items={[
|
items={[
|
||||||
{ label: t('scenarios.title'), onClick: () => navigate('/scenarios') },
|
{ label: t('scenarios.title'), icon: <ClipboardList size={14} />, onClick: () => navigate('/scenarios') },
|
||||||
{
|
{
|
||||||
label: scenario?.name ?? `#${id}`,
|
label: scenario?.name ?? `#${id}`,
|
||||||
onClick: () => navigate(`/scenarios/${id}`),
|
onClick: () => navigate(`/scenarios/${id}`),
|
||||||
@@ -90,9 +91,11 @@ export function EditScenarioPage() {
|
|||||||
variant="secondary"
|
variant="secondary"
|
||||||
onClick={() => navigate(`/scenarios/${id}`)}
|
onClick={() => navigate(`/scenarios/${id}`)}
|
||||||
>
|
>
|
||||||
|
<X size={14} />
|
||||||
{t('scenarios.action_cancel')}
|
{t('scenarios.action_cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" loading={saving}>
|
<Button type="submit" loading={saving}>
|
||||||
|
<Save size={14} />
|
||||||
{t('scenarios.action_update')}
|
{t('scenarios.action_update')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { X, Save, ClipboardList } from 'lucide-react';
|
||||||
import { scenarios, steps } from '../../api';
|
import { scenarios, steps } from '../../api';
|
||||||
import type { Scenario, ScenarioStep } from '../../api';
|
import type { Scenario, ScenarioStep } from '../../api';
|
||||||
import { Breadcrumbs, Button, Card, Input, CodeEditor } from '../../ui';
|
import { Breadcrumbs, Button, Card, Input, CodeEditor } from '../../ui';
|
||||||
@@ -57,7 +58,7 @@ export function EditStepPage() {
|
|||||||
<div className={styles.pageToolbar}>
|
<div className={styles.pageToolbar}>
|
||||||
<Breadcrumbs
|
<Breadcrumbs
|
||||||
items={[
|
items={[
|
||||||
{ label: t('scenarios.title'), onClick: () => navigate('/scenarios') },
|
{ label: t('scenarios.title'), icon: <ClipboardList size={14} />, onClick: () => navigate('/scenarios') },
|
||||||
{
|
{
|
||||||
label: scenario?.name ?? `#${id}`,
|
label: scenario?.name ?? `#${id}`,
|
||||||
onClick: () => navigate(`/scenarios/${id}`),
|
onClick: () => navigate(`/scenarios/${id}`),
|
||||||
@@ -96,9 +97,11 @@ export function EditStepPage() {
|
|||||||
variant="secondary"
|
variant="secondary"
|
||||||
onClick={() => navigate(`/scenarios/${id}`)}
|
onClick={() => navigate(`/scenarios/${id}`)}
|
||||||
>
|
>
|
||||||
|
<X size={14} />
|
||||||
{t('steps.action_cancel')}
|
{t('steps.action_cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" loading={saving}>
|
<Button type="submit" loading={saving}>
|
||||||
|
<Save size={14} />
|
||||||
{t('steps.action_update')}
|
{t('steps.action_update')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
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 { stringify as yamlStringify } from 'yaml';
|
||||||
import { scenarios, steps, scenarioCredentials, credentials as credentialsApi } from '../../api';
|
import { scenarios, steps, scenarioCredentials, credentials as credentialsApi } from '../../api';
|
||||||
import type { Scenario, ScenarioStep, ScenarioCredential, Credential } from '../../api';
|
import type { Scenario, ScenarioStep, ScenarioCredential, Credential } from '../../api';
|
||||||
@@ -269,7 +269,7 @@ export function ScenarioDetailPage() {
|
|||||||
<div className={styles.pageToolbar}>
|
<div className={styles.pageToolbar}>
|
||||||
<Breadcrumbs
|
<Breadcrumbs
|
||||||
items={[
|
items={[
|
||||||
{ label: t('scenarios.title'), onClick: () => navigate('/scenarios') },
|
{ label: t('scenarios.title'), icon: <ClipboardList size={14} />, onClick: () => navigate('/scenarios') },
|
||||||
{ label: scenario?.name ?? `#${id}` },
|
{ label: scenario?.name ?? `#${id}` },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
@@ -313,7 +313,7 @@ export function ScenarioDetailPage() {
|
|||||||
<>
|
<>
|
||||||
<Card>
|
<Card>
|
||||||
<DescriptionList
|
<DescriptionList
|
||||||
layout="comfortable"
|
layout="grid"
|
||||||
items={[
|
items={[
|
||||||
{ term: t('scenarios.field_id'), detail: <UuidBadge id={scenario.id} /> },
|
{ term: t('scenarios.field_id'), detail: <UuidBadge id={scenario.id} /> },
|
||||||
{ term: t('scenarios.field_name'), detail: scenario.name },
|
{ term: t('scenarios.field_name'), detail: scenario.name },
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
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 { parse as yamlParse } from 'yaml';
|
||||||
import { scenarios } from '../../api';
|
import { scenarios } from '../../api';
|
||||||
import type { Scenario } from '../../api';
|
import type { Scenario } from '../../api';
|
||||||
@@ -89,7 +89,7 @@ export function ScenariosPage() {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className={styles.pageToolbar}>
|
<div className={styles.pageToolbar}>
|
||||||
<Breadcrumbs items={[{ label: t('scenarios.title') }]} className={styles.breadcrumbs} />
|
<Breadcrumbs items={[{ label: t('scenarios.title'), icon: <ClipboardList size={14} /> }]} />
|
||||||
<div className={styles.toolbarActions}>
|
<div className={styles.toolbarActions}>
|
||||||
<input
|
<input
|
||||||
ref={fileInputRef}
|
ref={fileInputRef}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Trash2 } from 'lucide-react';
|
import { Trash2, Monitor } from 'lucide-react';
|
||||||
import { sessions } from '../../api';
|
import { sessions } from '../../api';
|
||||||
import type { Session } from '../../api';
|
import type { Session } from '../../api';
|
||||||
import {
|
import {
|
||||||
@@ -44,7 +44,7 @@ export function SessionDetailPage() {
|
|||||||
<div className={styles.pageToolbar}>
|
<div className={styles.pageToolbar}>
|
||||||
<Breadcrumbs
|
<Breadcrumbs
|
||||||
items={[
|
items={[
|
||||||
{ label: t('sessions.title'), onClick: () => navigate('/sessions') },
|
{ label: t('sessions.title'), icon: <Monitor size={14} />, onClick: () => navigate('/sessions') },
|
||||||
{ label: session?.sessionName ?? `#${id}` },
|
{ label: session?.sessionName ?? `#${id}` },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
@@ -69,7 +69,7 @@ export function SessionDetailPage() {
|
|||||||
{session && (
|
{session && (
|
||||||
<Card>
|
<Card>
|
||||||
<DescriptionList
|
<DescriptionList
|
||||||
layout="comfortable"
|
layout="grid"
|
||||||
items={[
|
items={[
|
||||||
{ term: t('sessions.field_id'), detail: <UuidBadge id={session.id} /> },
|
{ term: t('sessions.field_id'), detail: <UuidBadge id={session.id} /> },
|
||||||
{ term: t('sessions.field_name'), detail: session.sessionName },
|
{ term: t('sessions.field_name'), detail: session.sessionName },
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Trash2 } from 'lucide-react';
|
import { Trash2, Monitor } from 'lucide-react';
|
||||||
import { sessions } from '../../api';
|
import { sessions } from '../../api';
|
||||||
import type { Session } from '../../api';
|
import type { Session } from '../../api';
|
||||||
import {
|
import {
|
||||||
@@ -75,7 +75,9 @@ export function SessionsPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Breadcrumbs items={[{ label: t('sessions.title') }]} className={styles.breadcrumbs} />
|
<div className={styles.pageToolbar}>
|
||||||
|
<Breadcrumbs items={[{ label: t('sessions.title'), icon: <Monitor size={14} /> }]} />
|
||||||
|
</div>
|
||||||
{error && <p className={styles.error}>{error}</p>}
|
{error && <p className={styles.error}>{error}</p>}
|
||||||
<Table
|
<Table
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { X, Save, Braces } from 'lucide-react';
|
||||||
import { snippets } from '../../api';
|
import { snippets } from '../../api';
|
||||||
import { Breadcrumbs, Button, Card, Input, CodeEditor } from '../../ui';
|
import { Breadcrumbs, Button, Card, Input, CodeEditor } from '../../ui';
|
||||||
import styles from '../Page.module.css';
|
import styles from '../Page.module.css';
|
||||||
@@ -50,7 +51,7 @@ export function CreateSnippetPage() {
|
|||||||
<div className={styles.pageToolbar}>
|
<div className={styles.pageToolbar}>
|
||||||
<Breadcrumbs
|
<Breadcrumbs
|
||||||
items={[
|
items={[
|
||||||
{ label: t('snippets.title'), onClick: () => navigate('/snippets') },
|
{ label: t('snippets.title'), icon: <Braces size={14} />, onClick: () => navigate('/snippets') },
|
||||||
{ label: t('snippets.create_title') },
|
{ label: t('snippets.create_title') },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
@@ -98,9 +99,11 @@ export function CreateSnippetPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className={styles.formActions}>
|
<div className={styles.formActions}>
|
||||||
<Button type="button" variant="secondary" onClick={() => navigate('/snippets')}>
|
<Button type="button" variant="secondary" onClick={() => navigate('/snippets')}>
|
||||||
|
<X size={14} />
|
||||||
{t('snippets.action_cancel')}
|
{t('snippets.action_cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" disabled={saving}>
|
<Button type="submit" disabled={saving}>
|
||||||
|
<Save size={14} />
|
||||||
{t('snippets.action_save')}
|
{t('snippets.action_save')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { X, Save, Braces } from 'lucide-react';
|
||||||
import { snippets } from '../../api';
|
import { snippets } from '../../api';
|
||||||
import type { Snippet } from '../../api';
|
import type { Snippet } from '../../api';
|
||||||
import { Breadcrumbs, Button, Card, Input, CodeEditor } from '../../ui';
|
import { Breadcrumbs, Button, Card, Input, CodeEditor } from '../../ui';
|
||||||
@@ -68,7 +69,7 @@ export function EditSnippetPage() {
|
|||||||
<div className={styles.pageToolbar}>
|
<div className={styles.pageToolbar}>
|
||||||
<Breadcrumbs
|
<Breadcrumbs
|
||||||
items={[
|
items={[
|
||||||
{ label: t('snippets.title'), onClick: () => navigate('/snippets') },
|
{ label: t('snippets.title'), icon: <Braces size={14} />, onClick: () => navigate('/snippets') },
|
||||||
{
|
{
|
||||||
label: snippet?.name ?? `#${id}`,
|
label: snippet?.name ?? `#${id}`,
|
||||||
onClick: () => navigate(`/snippets/${id}`),
|
onClick: () => navigate(`/snippets/${id}`),
|
||||||
@@ -122,9 +123,11 @@ export function EditSnippetPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className={styles.formActions}>
|
<div className={styles.formActions}>
|
||||||
<Button type="button" variant="secondary" onClick={() => navigate(`/snippets/${id}`)}>
|
<Button type="button" variant="secondary" onClick={() => navigate(`/snippets/${id}`)}>
|
||||||
|
<X size={14} />
|
||||||
{t('snippets.action_cancel')}
|
{t('snippets.action_cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" disabled={saving}>
|
<Button type="submit" disabled={saving}>
|
||||||
|
<Save size={14} />
|
||||||
{t('snippets.action_update')}
|
{t('snippets.action_update')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
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 { CodeBlock } from '../../ui';
|
||||||
import { stringify as yamlStringify } from 'yaml';
|
import { stringify as yamlStringify } from 'yaml';
|
||||||
import { snippets } from '../../api';
|
import { snippets } from '../../api';
|
||||||
@@ -57,7 +57,7 @@ export function SnippetDetailPage() {
|
|||||||
<div className={styles.pageToolbar}>
|
<div className={styles.pageToolbar}>
|
||||||
<Breadcrumbs
|
<Breadcrumbs
|
||||||
items={[
|
items={[
|
||||||
{ label: t('snippets.title'), onClick: () => navigate('/snippets') },
|
{ label: t('snippets.title'), icon: <Braces size={14} />, onClick: () => navigate('/snippets') },
|
||||||
{ label: snippet?.name ?? `#${id}` },
|
{ label: snippet?.name ?? `#${id}` },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
@@ -96,9 +96,9 @@ export function SnippetDetailPage() {
|
|||||||
|
|
||||||
{snippet && (
|
{snippet && (
|
||||||
<>
|
<>
|
||||||
<div className={styles.detailMeta}>
|
<Card>
|
||||||
<DescriptionList
|
<DescriptionList
|
||||||
layout="comfortable"
|
layout="grid"
|
||||||
items={[
|
items={[
|
||||||
{ term: t('snippets.field_id'), detail: <UuidBadge id={snippet.id} /> },
|
{ term: t('snippets.field_id'), detail: <UuidBadge id={snippet.id} /> },
|
||||||
{ term: t('snippets.field_name'), detail: snippet.name },
|
{ term: t('snippets.field_name'), detail: snippet.name },
|
||||||
@@ -116,7 +116,7 @@ export function SnippetDetailPage() {
|
|||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</Card>
|
||||||
|
|
||||||
<div className={styles.stepsSection}>
|
<div className={styles.stepsSection}>
|
||||||
<div className={styles.sectionHeader}>
|
<div className={styles.sectionHeader}>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
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 { parse as yamlParse } from 'yaml';
|
||||||
import { snippets } from '../../api';
|
import { snippets } from '../../api';
|
||||||
import type { Snippet } from '../../api';
|
import type { Snippet } from '../../api';
|
||||||
@@ -116,7 +116,7 @@ export function SnippetsPage() {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className={styles.pageToolbar}>
|
<div className={styles.pageToolbar}>
|
||||||
<Breadcrumbs items={[{ label: t('snippets.title') }]} />
|
<Breadcrumbs items={[{ label: t('snippets.title'), icon: <Braces size={14} /> }]} />
|
||||||
<div className={styles.toolbarActions}>
|
<div className={styles.toolbarActions}>
|
||||||
<input
|
<input
|
||||||
ref={fileInputRef}
|
ref={fileInputRef}
|
||||||
|
|||||||
@@ -45,6 +45,25 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Clickable / hover ────────────────────────────────────── */
|
||||||
|
|
||||||
|
.clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 99px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
margin: -2px -6px;
|
||||||
|
transition: background var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.clickable:hover {
|
||||||
|
background: color-mix(in srgb, var(--color-text) 8%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.clickable:focus-visible {
|
||||||
|
outline: 2px solid var(--color-primary);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Active state ─────────────────────────────────────────── */
|
/* ── Active state ─────────────────────────────────────────── */
|
||||||
|
|
||||||
.active .dot::before {
|
.active .dot::before {
|
||||||
@@ -67,3 +86,18 @@
|
|||||||
color: var(--color-text-muted);
|
color: var(--color-text-muted);
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Error state ──────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.error .dot::before {
|
||||||
|
background: var(--color-error-fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.error .ring {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error .label {
|
||||||
|
color: var(--color-error-fg);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,24 +4,39 @@ export interface AutoRefreshIndicatorProps {
|
|||||||
active: boolean;
|
active: boolean;
|
||||||
pulseKey?: number;
|
pulseKey?: number;
|
||||||
label?: string;
|
label?: string;
|
||||||
|
error?: boolean;
|
||||||
|
onClick?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AutoRefreshIndicator({
|
export function AutoRefreshIndicator({
|
||||||
active,
|
active,
|
||||||
pulseKey,
|
pulseKey,
|
||||||
label = 'Live',
|
label = 'Live',
|
||||||
|
error = false,
|
||||||
|
onClick,
|
||||||
}: AutoRefreshIndicatorProps) {
|
}: 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 (
|
return (
|
||||||
<div
|
<div
|
||||||
className={[styles.root, active ? styles.active : styles.inactive].join(' ')}
|
className={[styles.root, stateClass, onClick ? styles.clickable : ''].join(' ')}
|
||||||
title={active ? 'Auto-refreshing' : 'Auto-refresh stopped'}
|
title={titleText}
|
||||||
aria-label={active ? 'Auto-refreshing' : 'Auto-refresh stopped'}
|
aria-label={titleText}
|
||||||
aria-live="polite"
|
aria-live="polite"
|
||||||
|
role={onClick ? 'button' : undefined}
|
||||||
|
tabIndex={onClick ? 0 : undefined}
|
||||||
|
onClick={onClick}
|
||||||
|
onKeyDown={onClick ? (e) => (e.key === 'Enter' || e.key === ' ') && onClick() : undefined}
|
||||||
>
|
>
|
||||||
<span className={styles.dot}>
|
<span className={styles.dot}>
|
||||||
{pulseKey !== undefined && pulseKey > 0 && <span key={pulseKey} className={styles.ring} />}
|
{pulseKey !== undefined && pulseKey > 0 && <span key={pulseKey} className={styles.ring} />}
|
||||||
</span>
|
</span>
|
||||||
<span className={styles.label}>{label}</span>
|
<span className={styles.label}>{error ? 'Error' : label}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
border-radius: var(--radius-full);
|
border-radius: var(--radius-full);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
|
box-shadow: var(--shadow-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dot {
|
.dot {
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
.nav {
|
.nav {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
font-family: var(--font-family);
|
font-family: var(--font-family);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19,6 +21,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.link {
|
.link {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-1);
|
||||||
font-size: var(--font-size-sm);
|
font-size: var(--font-size-sm);
|
||||||
color: var(--color-text-muted);
|
color: var(--color-text-muted);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
@@ -42,6 +47,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.current {
|
.current {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-1);
|
||||||
font-size: var(--font-size-sm);
|
font-size: var(--font-size-sm);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import styles from './Breadcrumbs.module.css';
|
|||||||
|
|
||||||
export interface BreadcrumbItem {
|
export interface BreadcrumbItem {
|
||||||
label: string;
|
label: string;
|
||||||
|
icon?: React.ReactNode;
|
||||||
href?: string;
|
href?: string;
|
||||||
onClick?: (e: React.MouseEvent) => void;
|
onClick?: (e: React.MouseEvent) => void;
|
||||||
}
|
}
|
||||||
@@ -35,15 +36,15 @@ export function Breadcrumbs({ items, separator, className }: BreadcrumbsProps) {
|
|||||||
<li key={i} className={styles.item}>
|
<li key={i} className={styles.item}>
|
||||||
{isLast ? (
|
{isLast ? (
|
||||||
<span className={styles.current} aria-current="page">
|
<span className={styles.current} aria-current="page">
|
||||||
{item.label}
|
{item.icon}{item.label}
|
||||||
</span>
|
</span>
|
||||||
) : item.href ? (
|
) : item.href ? (
|
||||||
<a href={item.href} className={styles.link} onClick={item.onClick}>
|
<a href={item.href} className={styles.link} onClick={item.onClick}>
|
||||||
{item.label}
|
{item.icon}{item.label}
|
||||||
</a>
|
</a>
|
||||||
) : (
|
) : (
|
||||||
<button type="button" className={styles.link} onClick={item.onClick}>
|
<button type="button" className={styles.link} onClick={item.onClick}>
|
||||||
{item.label}
|
{item.icon}{item.label}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{!isLast && <span className={styles.separator}>{sep}</span>}
|
{!isLast && <span className={styles.separator}>{sep}</span>}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
outline: none;
|
outline: none;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
box-shadow: var(--shadow-xs);
|
||||||
transition:
|
transition:
|
||||||
background var(--transition),
|
background var(--transition),
|
||||||
border-color var(--transition),
|
border-color var(--transition),
|
||||||
|
|||||||
@@ -7,6 +7,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-2);
|
||||||
padding: var(--space-3) var(--space-6);
|
padding: var(--space-3) var(--space-6);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: var(--font-size-sm);
|
font-size: var(--font-size-sm);
|
||||||
|
|||||||
@@ -45,7 +45,26 @@
|
|||||||
max-width: 100%;
|
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 {
|
.inline .row {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ export interface DescriptionListItem {
|
|||||||
|
|
||||||
export interface DescriptionListProps {
|
export interface DescriptionListProps {
|
||||||
items: DescriptionListItem[];
|
items: DescriptionListItem[];
|
||||||
/** 'compact' stacks term above detail; 'inline' places them side by side; 'comfortable' adds generous spacing with dividers (default) */
|
/** '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';
|
layout?: 'inline' | 'compact' | 'comfortable' | 'grid';
|
||||||
/** Truncate detail values with ellipsis; full value shown in a tooltip on hover */
|
/** Truncate detail values with ellipsis; full value shown in a tooltip on hover */
|
||||||
truncate?: boolean;
|
truncate?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
height: 36px;
|
height: 36px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
outline: none;
|
outline: none;
|
||||||
|
box-shadow: var(--shadow-xs);
|
||||||
transition: border-color var(--transition), box-shadow var(--transition);
|
transition: border-color var(--transition), box-shadow var(--transition);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -74,7 +74,8 @@
|
|||||||
--border-width: 1.5px;
|
--border-width: 1.5px;
|
||||||
|
|
||||||
/* Shadows */
|
/* 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);
|
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.07), 0 2px 4px -1px rgba(0, 0, 0, 0.04);
|
||||||
|
|
||||||
/* Transitions */
|
/* Transitions */
|
||||||
|
|||||||
Reference in New Issue
Block a user