feat(ui): require confirmation before delete actions

- add a reusable modal component and export it from the shared ui barrel

- gate all delete and remove flows behind explicit confirmation dialogs

- use a solid modal surface color token with fallback to avoid transparent body
This commit is contained in:
2026-04-10 23:46:20 +03:00
parent 7223371fae
commit d8ea2d0126
14 changed files with 458 additions and 41 deletions
@@ -11,6 +11,7 @@ import {
Button,
Card,
DescriptionList,
Modal,
Timestamp,
UuidBadge,
} from '../../ui';
@@ -23,6 +24,8 @@ export function CredentialDetailPage() {
const [credential, setCredential] = useState<Credential | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false);
const [deleting, setDeleting] = useState(false);
useEffect(() => {
if (!id) return;
@@ -35,8 +38,14 @@ export function CredentialDetailPage() {
const handleDelete = async () => {
if (!credential) return;
await credentials.remove(credential.id);
navigate('/credentials');
setDeleting(true);
try {
await credentials.remove(credential.id);
navigate('/credentials');
} finally {
setDeleting(false);
setConfirmDeleteOpen(false);
}
};
const handleExport = async () => {
@@ -74,7 +83,7 @@ export function CredentialDetailPage() {
<Pencil size={14} />
{t('credentials.action_edit')}
</Button>
<Button variant="danger" size="sm" onClick={handleDelete}>
<Button variant="danger" size="sm" onClick={() => setConfirmDeleteOpen(true)}>
<Trash2 size={14} />
{t('credentials.action_delete')}
</Button>
@@ -126,6 +135,24 @@ export function CredentialDetailPage() {
)}
</>
)}
<Modal
open={confirmDeleteOpen}
title={t('credentials.action_delete')}
onClose={() => !deleting && setConfirmDeleteOpen(false)}
footer={(
<>
<Button variant="secondary" onClick={() => setConfirmDeleteOpen(false)} disabled={deleting}>
Cancel
</Button>
<Button variant="danger" onClick={handleDelete} disabled={deleting}>
{t('credentials.action_delete')}
</Button>
</>
)}
>
Are you sure you want to delete this credential?
</Modal>
</div>
);
}
@@ -11,6 +11,7 @@ import {
Card,
ContextMenu,
DescriptionList,
Modal,
Timestamp,
UuidBadge,
} from '../../ui';
@@ -103,6 +104,8 @@ export function CredentialsPage() {
const [items, setItems] = useState<Credential[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [deleteId, setDeleteId] = useState<string | null>(null);
const [deleting, setDeleting] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
const load = () => {
@@ -117,9 +120,20 @@ export function CredentialsPage() {
load();
}, []);
const handleDelete = async (id: string) => {
await credentials.remove(id);
setItems((prev) => prev.filter((c) => c.id !== id));
const requestDelete = (id: string) => {
setDeleteId(id);
};
const handleDelete = async () => {
if (!deleteId) return;
setDeleting(true);
try {
await credentials.remove(deleteId);
setItems((prev) => prev.filter((c) => c.id !== deleteId));
setDeleteId(null);
} finally {
setDeleting(false);
}
};
const handleImport = async (e: React.ChangeEvent<HTMLInputElement>) => {
@@ -159,11 +173,29 @@ export function CredentialsPage() {
) : (
<div className={styles.envGrid}>
{items.map((credential) => (
<CredentialCard key={credential.id} credential={credential} onDelete={handleDelete} />
<CredentialCard key={credential.id} credential={credential} onDelete={requestDelete} />
))}
<AddCredentialCard />
</div>
)}
<Modal
open={deleteId != null}
title={t('credentials.action_delete')}
onClose={() => !deleting && setDeleteId(null)}
footer={(
<>
<Button variant="secondary" onClick={() => setDeleteId(null)} disabled={deleting}>
Cancel
</Button>
<Button variant="danger" onClick={handleDelete} disabled={deleting}>
{t('credentials.action_delete')}
</Button>
</>
)}
>
Are you sure you want to delete this credential?
</Modal>
</div>
);
}
@@ -5,7 +5,7 @@ import { Upload, Pencil, Trash2, Globe } from 'lucide-react';
import { stringify as yamlStringify } from 'yaml';
import { environments } from '../../api';
import type { Environment } from '../../api';
import { Breadcrumbs, Button, Card, DescriptionList, Timestamp, UuidBadge } from '../../ui';
import { Breadcrumbs, Button, Card, DescriptionList, Modal, Timestamp, UuidBadge } from '../../ui';
import styles from '../Page.module.css';
export function EnvironmentDetailPage() {
@@ -15,6 +15,8 @@ export function EnvironmentDetailPage() {
const [env, setEnv] = useState<Environment | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false);
const [deleting, setDeleting] = useState(false);
const dataEntries = Object.entries(env?.data ?? {}).filter(([, v]) => v);
useEffect(() => {
@@ -28,8 +30,14 @@ export function EnvironmentDetailPage() {
const handleDelete = async () => {
if (!env) return;
await environments.remove(env.id);
navigate('/environments');
setDeleting(true);
try {
await environments.remove(env.id);
navigate('/environments');
} finally {
setDeleting(false);
setConfirmDeleteOpen(false);
}
};
const handleExport = async () => {
@@ -67,7 +75,7 @@ export function EnvironmentDetailPage() {
<Pencil size={14} />
{t('environments.action_edit')}
</Button>
<Button variant="danger" size="sm" onClick={handleDelete}>
<Button variant="danger" size="sm" onClick={() => setConfirmDeleteOpen(true)}>
<Trash2 size={14} />
{t('environments.action_delete')}
</Button>
@@ -118,6 +126,24 @@ export function EnvironmentDetailPage() {
</div>
</>
)}
<Modal
open={confirmDeleteOpen}
title={t('environments.action_delete')}
onClose={() => !deleting && setConfirmDeleteOpen(false)}
footer={(
<>
<Button variant="secondary" onClick={() => setConfirmDeleteOpen(false)} disabled={deleting}>
Cancel
</Button>
<Button variant="danger" onClick={handleDelete} disabled={deleting}>
{t('environments.action_delete')}
</Button>
</>
)}
>
Are you sure you want to delete this environment?
</Modal>
</div>
);
}
@@ -12,6 +12,7 @@ import {
Card,
ContextMenu,
DescriptionList,
Modal,
Timestamp,
UuidBadge,
} from '../../ui';
@@ -120,6 +121,8 @@ export function EnvironmentsPage() {
const [items, setItems] = useState<Environment[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [deleteId, setDeleteId] = useState<string | null>(null);
const [deleting, setDeleting] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
const load = () => {
@@ -134,9 +137,20 @@ export function EnvironmentsPage() {
load();
}, []);
const handleDelete = async (id: string) => {
await environments.remove(id);
setItems((prev) => prev.filter((e) => e.id !== id));
const requestDelete = (id: string) => {
setDeleteId(id);
};
const handleDelete = async () => {
if (!deleteId) return;
setDeleting(true);
try {
await environments.remove(deleteId);
setItems((prev) => prev.filter((e) => e.id !== deleteId));
setDeleteId(null);
} finally {
setDeleting(false);
}
};
const handleImport = async (e: React.ChangeEvent<HTMLInputElement>) => {
@@ -176,11 +190,29 @@ export function EnvironmentsPage() {
) : (
<div className={styles.envGrid}>
{items.map((env) => (
<EnvironmentCard key={env.id} env={env} onDelete={handleDelete} />
<EnvironmentCard key={env.id} env={env} onDelete={requestDelete} />
))}
<AddEnvironmentCard />
</div>
)}
<Modal
open={deleteId != null}
title={t('environments.action_delete')}
onClose={() => !deleting && setDeleteId(null)}
footer={(
<>
<Button variant="secondary" onClick={() => setDeleteId(null)} disabled={deleting}>
Cancel
</Button>
<Button variant="danger" onClick={handleDelete} disabled={deleting}>
{t('environments.action_delete')}
</Button>
</>
)}
>
Are you sure you want to delete this environment?
</Modal>
</div>
);
}
@@ -11,6 +11,7 @@ import {
Card,
DescriptionList,
Input,
Modal,
Select,
Table,
Timestamp,
@@ -41,6 +42,10 @@ export function ScenarioDetailPage() {
const [draggedStepId, setDraggedStepId] = useState<string | null>(null);
const [dragOverStepId, setDragOverStepId] = useState<string | null>(null);
const [reorderingSteps, setReorderingSteps] = useState(false);
const [pendingDelete, setPendingDelete] = useState<
{ type: 'scenario' } | { type: 'step'; id: string } | { type: 'credential'; id: string } | null
>(null);
const [deleting, setDeleting] = useState(false);
useEffect(() => {
if (!id) return;
@@ -148,6 +153,26 @@ export function ScenarioDetailPage() {
setScenarioCreds((prev) => prev.filter((sc) => sc.id !== scCredId));
};
const confirmDelete = async () => {
if (!pendingDelete) return;
setDeleting(true);
try {
if (pendingDelete.type === 'scenario') {
await handleDelete();
return;
}
if (pendingDelete.type === 'step') {
await handleDeleteStep(pendingDelete.id);
}
if (pendingDelete.type === 'credential') {
await handleRemoveCredential(pendingDelete.id);
}
setPendingDelete(null);
} finally {
setDeleting(false);
}
};
const stepColumns: TableColumn<ScenarioStep>[] = [
{
key: 'drag',
@@ -226,7 +251,7 @@ export function ScenarioDetailPage() {
title={t('steps.action_delete')}
onClick={(e) => {
e.stopPropagation();
handleDeleteStep(s.id);
setPendingDelete({ type: 'step', id: s.id });
}}
>
<Trash2 size={14} />
@@ -261,7 +286,7 @@ export function ScenarioDetailPage() {
title={t('scenarios.cred_action_remove')}
onClick={(e) => {
e.stopPropagation();
handleRemoveCredential(sc.id);
setPendingDelete({ type: 'credential', id: sc.id });
}}
>
<Trash2 size={14} />
@@ -297,7 +322,7 @@ export function ScenarioDetailPage() {
<Pencil size={14} />
{t('scenarios.action_edit')}
</Button>
<Button variant="danger" size="sm" onClick={handleDelete}>
<Button variant="danger" size="sm" onClick={() => setPendingDelete({ type: 'scenario' })}>
<Trash2 size={14} />
{t('scenarios.action_delete')}
</Button>
@@ -456,6 +481,32 @@ export function ScenarioDetailPage() {
</div>
</>
)}
<Modal
open={pendingDelete != null}
title={
pendingDelete?.type === 'step'
? t('steps.action_delete')
: pendingDelete?.type === 'credential'
? t('scenarios.cred_action_remove')
: t('scenarios.action_delete')
}
onClose={() => !deleting && setPendingDelete(null)}
footer={(
<>
<Button variant="secondary" onClick={() => setPendingDelete(null)} disabled={deleting}>
Cancel
</Button>
<Button variant="danger" onClick={confirmDelete} disabled={deleting}>
Confirm
</Button>
</>
)}
>
{pendingDelete?.type === 'step' && 'Are you sure you want to delete this step?'}
{pendingDelete?.type === 'credential' && 'Are you sure you want to remove this credential from scenario?'}
{pendingDelete?.type === 'scenario' && 'Are you sure you want to delete this scenario?'}
</Modal>
</div>
);
}
+45 -6
View File
@@ -5,7 +5,16 @@ import { Play, Plus, Trash2, Download, ClipboardList } from 'lucide-react';
import { parse as yamlParse } from 'yaml';
import { scenarios } from '../../api';
import type { Scenario } from '../../api';
import { Breadcrumbs, Button, Table, type TableColumn, Timestamp, UuidBadge, useToast } from '../../ui';
import {
Breadcrumbs,
Button,
Modal,
Table,
type TableColumn,
Timestamp,
UuidBadge,
useToast,
} from '../../ui';
import styles from '../Page.module.css';
export function ScenariosPage() {
@@ -15,6 +24,8 @@ export function ScenariosPage() {
const [items, setItems] = useState<Scenario[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [deleteId, setDeleteId] = useState<string | null>(null);
const [deleting, setDeleting] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
@@ -40,10 +51,17 @@ export function ScenariosPage() {
}
};
const handleDelete = async (id: string) => {
await scenarios.remove(id);
setLoading(true);
load();
const handleDelete = async () => {
if (!deleteId) return;
setDeleting(true);
try {
await scenarios.remove(deleteId);
setLoading(true);
load();
setDeleteId(null);
} finally {
setDeleting(false);
}
};
const handleImport = async (e: React.ChangeEvent<HTMLInputElement>) => {
@@ -83,7 +101,10 @@ export function ScenariosPage() {
variant="danger"
size="sm"
title={t('scenarios.action_delete')}
onClick={() => handleDelete(s.id)}
onClick={(e) => {
e.stopPropagation();
setDeleteId(s.id);
}}
>
<Trash2 size={14} />
</Button>
@@ -124,6 +145,24 @@ export function ScenariosPage() {
pageSizeOptions={[10, 25, 50]}
onRowClick={(s) => navigate(`/scenarios/${s.id}`)}
/>
<Modal
open={deleteId != null}
title={t('scenarios.action_delete')}
onClose={() => !deleting && setDeleteId(null)}
footer={(
<>
<Button variant="secondary" onClick={() => setDeleteId(null)} disabled={deleting}>
Cancel
</Button>
<Button variant="danger" onClick={handleDelete} disabled={deleting}>
{t('scenarios.action_delete')}
</Button>
</>
)}
>
Are you sure you want to delete this scenario?
</Modal>
</div>
);
}
+30 -3
View File
@@ -10,6 +10,7 @@ import {
Button,
Card,
DescriptionList,
Modal,
Timestamp,
UuidBadge,
} from '../../ui';
@@ -22,6 +23,8 @@ export function SessionDetailPage() {
const [session, setSession] = useState<Session | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false);
const [deleting, setDeleting] = useState(false);
useEffect(() => {
if (!id) return;
@@ -34,8 +37,14 @@ export function SessionDetailPage() {
const handleDelete = async () => {
if (!session) return;
await sessions.remove(session.id);
navigate('/sessions');
setDeleting(true);
try {
await sessions.remove(session.id);
navigate('/sessions');
} finally {
setDeleting(false);
setConfirmDeleteOpen(false);
}
};
return (
@@ -48,7 +57,7 @@ export function SessionDetailPage() {
]}
/>
{session && (
<Button variant="danger" size="sm" onClick={handleDelete}>
<Button variant="danger" size="sm" onClick={() => setConfirmDeleteOpen(true)}>
<Trash2 size={14} />
{t('sessions.action_delete')}
</Button>
@@ -92,6 +101,24 @@ export function SessionDetailPage() {
/>
</Card>
)}
<Modal
open={confirmDeleteOpen}
title={t('sessions.action_delete')}
onClose={() => !deleting && setConfirmDeleteOpen(false)}
footer={(
<>
<Button variant="secondary" onClick={() => setConfirmDeleteOpen(false)} disabled={deleting}>
Cancel
</Button>
<Button variant="danger" onClick={handleDelete} disabled={deleting}>
{t('sessions.action_delete')}
</Button>
</>
)}
>
Are you sure you want to delete this session?
</Modal>
</div>
);
}
+36 -5
View File
@@ -8,6 +8,7 @@ import {
Badge,
Breadcrumbs,
Button,
Modal,
Table,
type TableColumn,
Timestamp,
@@ -21,6 +22,8 @@ export function SessionsPage() {
const [items, setItems] = useState<Session[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [deleteId, setDeleteId] = useState<string | null>(null);
const [deleting, setDeleting] = useState(false);
const load = useCallback(() => {
sessions
@@ -34,10 +37,17 @@ export function SessionsPage() {
load();
}, [load]);
const handleDelete = async (id: string) => {
await sessions.remove(id);
setLoading(true);
load();
const handleDelete = async () => {
if (!deleteId) return;
setDeleting(true);
try {
await sessions.remove(deleteId);
setLoading(true);
load();
setDeleteId(null);
} finally {
setDeleting(false);
}
};
const columns: TableColumn<Session>[] = [
@@ -65,7 +75,10 @@ export function SessionsPage() {
variant="danger"
size="sm"
title={t('sessions.action_delete')}
onClick={() => handleDelete(s.id)}
onClick={(e) => {
e.stopPropagation();
setDeleteId(s.id);
}}
>
<Trash2 size={14} />
</Button>
@@ -88,6 +101,24 @@ export function SessionsPage() {
pageSizeOptions={[10, 25, 50]}
onRowClick={(s) => navigate(`/sessions/${s.id}`)}
/>
<Modal
open={deleteId != null}
title={t('sessions.action_delete')}
onClose={() => !deleting && setDeleteId(null)}
footer={(
<>
<Button variant="secondary" onClick={() => setDeleteId(null)} disabled={deleting}>
Cancel
</Button>
<Button variant="danger" onClick={handleDelete} disabled={deleting}>
{t('sessions.action_delete')}
</Button>
</>
)}
>
Are you sure you want to delete this session?
</Modal>
</div>
);
}
+30 -3
View File
@@ -12,6 +12,7 @@ import {
Card,
DescriptionList,
MarkdownContent,
Modal,
Timestamp,
UuidBadge,
} from '../../ui';
@@ -24,6 +25,8 @@ export function SnippetDetailPage() {
const [snippet, setSnippet] = useState<Snippet | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false);
const [deleting, setDeleting] = useState(false);
useEffect(() => {
if (!id) return;
@@ -36,8 +39,14 @@ export function SnippetDetailPage() {
const handleDelete = async () => {
if (!snippet) return;
await snippets.remove(snippet.id);
navigate('/snippets');
setDeleting(true);
try {
await snippets.remove(snippet.id);
navigate('/snippets');
} finally {
setDeleting(false);
setConfirmDeleteOpen(false);
}
};
const handleExport = async () => {
@@ -75,7 +84,7 @@ export function SnippetDetailPage() {
<Pencil size={14} />
{t('snippets.action_edit')}
</Button>
<Button variant="danger" size="sm" onClick={handleDelete}>
<Button variant="danger" size="sm" onClick={() => setConfirmDeleteOpen(true)}>
<Trash2 size={14} />
{t('snippets.action_delete')}
</Button>
@@ -127,6 +136,24 @@ export function SnippetDetailPage() {
</div>
</>
)}
<Modal
open={confirmDeleteOpen}
title={t('snippets.action_delete')}
onClose={() => !deleting && setConfirmDeleteOpen(false)}
footer={(
<>
<Button variant="secondary" onClick={() => setConfirmDeleteOpen(false)} disabled={deleting}>
Cancel
</Button>
<Button variant="danger" onClick={handleDelete} disabled={deleting}>
{t('snippets.action_delete')}
</Button>
</>
)}
>
Are you sure you want to delete this snippet?
</Modal>
</div>
);
}
+45 -5
View File
@@ -5,7 +5,16 @@ import { Settings, Pencil, Trash2, Plus, Download, Braces } from 'lucide-react';
import { parse as yamlParse } from 'yaml';
import { snippets } from '../../api';
import type { Snippet } from '../../api';
import { Breadcrumbs, Button, Card, ContextMenu, MarkdownContent, Timestamp, UuidBadge } from '../../ui';
import {
Breadcrumbs,
Button,
Card,
ContextMenu,
MarkdownContent,
Modal,
Timestamp,
UuidBadge,
} from '../../ui';
import styles from '../Page.module.css';
function firstParagraphBlocks(markdown: string, limit = 2): { preview: string; truncated: boolean } {
@@ -105,6 +114,8 @@ export function SnippetsPage() {
const [items, setItems] = useState<Snippet[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [deleteId, setDeleteId] = useState<string | null>(null);
const [deleting, setDeleting] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
const load = () => {
@@ -119,9 +130,20 @@ export function SnippetsPage() {
load();
}, []);
const handleDelete = async (id: string) => {
await snippets.remove(id);
load();
const requestDelete = (id: string) => {
setDeleteId(id);
};
const handleDelete = async () => {
if (!deleteId) return;
setDeleting(true);
try {
await snippets.remove(deleteId);
load();
setDeleteId(null);
} finally {
setDeleting(false);
}
};
const handleImport = async (e: React.ChangeEvent<HTMLInputElement>) => {
@@ -162,12 +184,30 @@ export function SnippetsPage() {
{!loading && (
<div className={styles.envGrid}>
{items.map((s) => (
<SnippetCard key={s.id} snippet={s} onDelete={handleDelete} />
<SnippetCard key={s.id} snippet={s} onDelete={requestDelete} />
))}
{items.length === 0 && <p className={styles.muted}>{t('snippets.empty')}</p>}
<AddSnippetCard />
</div>
)}
<Modal
open={deleteId != null}
title={t('snippets.action_delete')}
onClose={() => !deleting && setDeleteId(null)}
footer={(
<>
<Button variant="secondary" onClick={() => setDeleteId(null)} disabled={deleting}>
Cancel
</Button>
<Button variant="danger" onClick={handleDelete} disabled={deleting}>
{t('snippets.action_delete')}
</Button>
</>
)}
>
Are you sure you want to delete this snippet?
</Modal>
</div>
);
}
+41
View File
@@ -0,0 +1,41 @@
.overlay {
position: fixed;
inset: 0;
z-index: 2000;
background: color-mix(in srgb, #000 45%, transparent);
display: flex;
align-items: center;
justify-content: center;
padding: var(--space-4);
}
.dialog {
width: min(520px, 100%);
background-color: var(--color-bg-subtle, #ffffff);
border: var(--border-width) solid var(--color-border);
border-radius: var(--radius-lg);
box-shadow: 0 20px 48px color-mix(in srgb, #000 28%, transparent);
overflow: hidden;
}
.header {
padding: var(--space-4) var(--space-5) var(--space-2);
}
.title {
margin: 0;
font-size: var(--font-size-md);
color: var(--color-text);
}
.body {
padding: var(--space-2) var(--space-5) var(--space-4);
color: var(--color-text-muted);
}
.footer {
padding: var(--space-3) var(--space-5) var(--space-4);
display: flex;
justify-content: flex-end;
gap: var(--space-2);
}
+41
View File
@@ -0,0 +1,41 @@
import { useEffect } from 'react';
import styles from './Modal.module.css';
export interface ModalProps {
open: boolean;
title: string;
children?: React.ReactNode;
onClose: () => void;
footer?: React.ReactNode;
}
export function Modal({ open, title, children, onClose, footer }: ModalProps) {
useEffect(() => {
if (!open) return;
const onKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') onClose();
};
window.addEventListener('keydown', onKeyDown);
return () => window.removeEventListener('keydown', onKeyDown);
}, [open, onClose]);
if (!open) return null;
return (
<div className={styles.overlay} role="presentation" onClick={onClose}>
<div
className={styles.dialog}
role="dialog"
aria-modal="true"
aria-label={title}
onClick={(event) => event.stopPropagation()}
>
<div className={styles.header}>
<h3 className={styles.title}>{title}</h3>
</div>
<div className={styles.body}>{children}</div>
{footer && <div className={styles.footer}>{footer}</div>}
</div>
</div>
);
}
+3
View File
@@ -57,6 +57,9 @@ export type { DescriptionListProps, DescriptionListItem } from './DescriptionLis
export { Notification } from './Notification/Notification';
export type { NotificationProps, NotificationVariant } from './Notification/Notification';
export { Modal } from './Modal/Modal';
export type { ModalProps } from './Modal/Modal';
export { Table } from './Table/Table';
export type { TableProps, TableColumn } from './Table/Table';
+1 -1
View File
@@ -1 +1 @@
{"root":["./src/App.tsx","./src/declarations.d.ts","./src/main.tsx","./src/api/client.ts","./src/api/index.ts","./src/api/types.ts","./src/hooks/useTheme.ts","./src/i18n/index.ts","./src/lib/hljs.ts","./src/lib/toast-events.ts","./src/pages/credential/CreateCredentialPage.tsx","./src/pages/credential/CredentialDetailPage.tsx","./src/pages/credential/CredentialsPage.tsx","./src/pages/credential/EditCredentialPage.tsx","./src/pages/environment/CreateEnvironmentPage.tsx","./src/pages/environment/EditEnvironmentPage.tsx","./src/pages/environment/EnvironmentDetailPage.tsx","./src/pages/environment/EnvironmentsPage.tsx","./src/pages/run/AllRunsPage.tsx","./src/pages/run/RunDetailPage.tsx","./src/pages/run/RunsPage.tsx","./src/pages/scenario/CreateScenarioPage.tsx","./src/pages/scenario/CreateStepPage.tsx","./src/pages/scenario/EditScenarioPage.tsx","./src/pages/scenario/EditStepPage.tsx","./src/pages/scenario/ScenarioDetailPage.tsx","./src/pages/scenario/ScenariosPage.tsx","./src/pages/session/SessionDetailPage.tsx","./src/pages/session/SessionsPage.tsx","./src/pages/snippet/CreateSnippetPage.tsx","./src/pages/snippet/EditSnippetPage.tsx","./src/pages/snippet/SnippetDetailPage.tsx","./src/pages/snippet/SnippetsPage.tsx","./src/ui/index.ts","./src/ui/AutoRefreshIndicator/AutoRefreshIndicator.tsx","./src/ui/Badge/Badge.tsx","./src/ui/Breadcrumbs/Breadcrumbs.tsx","./src/ui/Button/Button.tsx","./src/ui/Card/Card.tsx","./src/ui/CodeBlock/CodeBlock.tsx","./src/ui/CodeEditor/CodeEditor.tsx","./src/ui/ContextMenu/ContextMenu.tsx","./src/ui/DescriptionList/DescriptionList.tsx","./src/ui/Input/Input.tsx","./src/ui/MarkdownContent/MarkdownContent.tsx","./src/ui/Notification/Notification.tsx","./src/ui/Pagination/Pagination.tsx","./src/ui/Search/Search.tsx","./src/ui/Select/Select.tsx","./src/ui/SidePanel/SidePanel.tsx","./src/ui/Table/Table.tsx","./src/ui/Textarea/Textarea.tsx","./src/ui/ThemeSwitcher/ThemeSwitcher.tsx","./src/ui/Timestamp/Timestamp.tsx","./src/ui/Toast/ToastProvider.tsx","./src/ui/UuidBadge/UuidBadge.tsx"],"version":"5.8.3"}
{"root":["./src/App.tsx","./src/declarations.d.ts","./src/main.tsx","./src/api/client.ts","./src/api/index.ts","./src/api/types.ts","./src/hooks/useTheme.ts","./src/i18n/index.ts","./src/lib/hljs.ts","./src/lib/toast-events.ts","./src/pages/credential/CreateCredentialPage.tsx","./src/pages/credential/CredentialDetailPage.tsx","./src/pages/credential/CredentialsPage.tsx","./src/pages/credential/EditCredentialPage.tsx","./src/pages/environment/CreateEnvironmentPage.tsx","./src/pages/environment/EditEnvironmentPage.tsx","./src/pages/environment/EnvironmentDetailPage.tsx","./src/pages/environment/EnvironmentsPage.tsx","./src/pages/run/AllRunsPage.tsx","./src/pages/run/RunDetailPage.tsx","./src/pages/run/RunsPage.tsx","./src/pages/scenario/CreateScenarioPage.tsx","./src/pages/scenario/CreateStepPage.tsx","./src/pages/scenario/EditScenarioPage.tsx","./src/pages/scenario/EditStepPage.tsx","./src/pages/scenario/ScenarioDetailPage.tsx","./src/pages/scenario/ScenariosPage.tsx","./src/pages/session/SessionDetailPage.tsx","./src/pages/session/SessionsPage.tsx","./src/pages/snippet/CreateSnippetPage.tsx","./src/pages/snippet/EditSnippetPage.tsx","./src/pages/snippet/SnippetDetailPage.tsx","./src/pages/snippet/SnippetsPage.tsx","./src/ui/index.ts","./src/ui/AutoRefreshIndicator/AutoRefreshIndicator.tsx","./src/ui/Badge/Badge.tsx","./src/ui/Breadcrumbs/Breadcrumbs.tsx","./src/ui/Button/Button.tsx","./src/ui/Card/Card.tsx","./src/ui/CodeBlock/CodeBlock.tsx","./src/ui/CodeEditor/CodeEditor.tsx","./src/ui/ContextMenu/ContextMenu.tsx","./src/ui/DescriptionList/DescriptionList.tsx","./src/ui/Input/Input.tsx","./src/ui/MarkdownContent/MarkdownContent.tsx","./src/ui/Modal/Modal.tsx","./src/ui/Notification/Notification.tsx","./src/ui/Pagination/Pagination.tsx","./src/ui/Search/Search.tsx","./src/ui/Select/Select.tsx","./src/ui/SidePanel/SidePanel.tsx","./src/ui/Table/Table.tsx","./src/ui/Textarea/Textarea.tsx","./src/ui/ThemeSwitcher/ThemeSwitcher.tsx","./src/ui/Timestamp/Timestamp.tsx","./src/ui/Toast/ToastProvider.tsx","./src/ui/UuidBadge/UuidBadge.tsx"],"version":"5.8.3"}