feat(client): add UI kit, data layer, app shell, and Table component

- design token system (Atlantis, Kimberly, Powder Ash palette)
- Button, Badge, Input, Select, Card, SidePanel, Breadcrumbs components
- generic typed Table<T> component with loading/empty states
- API data layer: typed fetch client for environments, keys, sessions, scenarios
- Vite dev proxy targeting server on port 13000
- App shell with SidePanel nav and four entity pages (Environments, Keys, Sessions, Scenarios)
- Storybook config with dark/light theme toggle and a11y addon
This commit is contained in:
2026-04-09 00:11:55 +03:00
parent 5cc16725fb
commit 9916ef5aaf
47 changed files with 3910 additions and 10 deletions
+89
View File
@@ -0,0 +1,89 @@
.button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--space-2);
font-family: var(--font-family);
font-weight: 700;
border: var(--border-width) solid transparent;
border-radius: var(--radius-md);
cursor: pointer;
white-space: nowrap;
outline: none;
text-decoration: none;
transition:
background var(--transition),
border-color var(--transition),
color var(--transition),
opacity var(--transition);
}
.button:focus-visible {
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.35);
}
.button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Sizes */
.sm {
font-size: var(--font-size-sm);
padding: var(--space-1) var(--space-3);
height: 28px;
}
.md {
font-size: var(--font-size-md);
padding: var(--space-2) var(--space-4);
height: 36px;
}
.lg {
font-size: var(--font-size-lg);
padding: var(--space-3) var(--space-5);
height: 44px;
}
/* Variants */
.primary {
background: var(--color-primary);
color: var(--color-primary-fg);
}
.primary:hover:not(:disabled) { background: var(--color-primary-hover); }
.primary:active:not(:disabled) { background: var(--color-primary-active); }
.secondary {
background: var(--color-secondary);
color: var(--color-secondary-fg);
border-color: var(--color-secondary-border);
}
.secondary:hover:not(:disabled) { background: var(--color-secondary-hover); }
.secondary:active:not(:disabled) { background: var(--color-secondary-active); }
.ghost {
background: transparent;
color: var(--color-text);
}
.ghost:hover:not(:disabled) { background: var(--color-secondary); }
.ghost:active:not(:disabled) { background: var(--color-secondary-hover); }
.danger {
background: var(--color-danger);
color: var(--color-danger-fg);
}
.danger:hover:not(:disabled) { background: var(--color-danger-hover); }
/* Loading spinner */
.spinner {
width: 12px;
height: 12px;
border: 2px solid currentColor;
border-top-color: transparent;
border-radius: 50%;
animation: spin 0.6s linear infinite;
flex-shrink: 0;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
+26
View File
@@ -0,0 +1,26 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { Button } from './Button';
const meta: Meta<typeof Button> = {
title: 'UI/Button',
component: Button,
parameters: { layout: 'centered' },
tags: ['autodocs'],
argTypes: {
variant: { control: 'select', options: ['primary', 'secondary', 'ghost', 'danger'] },
size: { control: 'select', options: ['sm', 'md', 'lg'] },
loading: { control: 'boolean' },
disabled: { control: 'boolean' },
},
};
export default meta;
type Story = StoryObj<typeof Button>;
export const Primary: Story = { args: { children: 'Button', variant: 'primary' } };
export const Secondary: Story = { args: { children: 'Button', variant: 'secondary' } };
export const Ghost: Story = { args: { children: 'Button', variant: 'ghost' } };
export const Danger: Story = { args: { children: 'Delete', variant: 'danger' } };
export const Small: Story = { args: { children: 'Small', size: 'sm' } };
export const Large: Story = { args: { children: 'Large', size: 'lg' } };
export const Loading: Story = { args: { children: 'Saving…', loading: true } };
export const Disabled: Story = { args: { children: 'Button', disabled: true } };
+29
View File
@@ -0,0 +1,29 @@
import React from 'react';
import styles from './Button.module.css';
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'ghost' | 'danger';
size?: 'sm' | 'md' | 'lg';
loading?: boolean;
}
export function Button({
variant = 'primary',
size = 'md',
loading = false,
disabled,
children,
className,
...props
}: ButtonProps) {
return (
<button
className={[styles.button, styles[variant], styles[size], className].filter(Boolean).join(' ')}
disabled={disabled || loading}
{...props}
>
{loading && <span className={styles.spinner} aria-hidden="true" />}
{children}
</button>
);
}