feat(ux): add UX testing setup guide and common platform integration
- Add UX_TESTING_SETUP_GUIDE.md for ChronoMind - Create .pnpmfile.cjs for local package resolution - Update .npmrc to remove repo-level Gitea auth - Create Primitives.tsx product adapter with ChronoMind-specific status types - Add ui directory for shared component imports Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
f88d9e5953
commit
ce7e04a158
5
.npmrc
5
.npmrc
@ -1,5 +1,6 @@
|
|||||||
@bytelyst:registry=http://${GITEA_NPM_HOST:-localhost}:3300/api/packages/ByteLyst/npm/
|
# Remove repo-level GITEA_NPM_TOKEN interpolation
|
||||||
//localhost:3300/api/packages/ByteLyst/npm/:_authToken=${GITEA_NPM_TOKEN}
|
# Gitea registry auth should live in user-level ~/.npmrc or CI secrets
|
||||||
|
# Only when BYTELYST_PACKAGE_SOURCE=gitea is explicitly used
|
||||||
strict-ssl=false
|
strict-ssl=false
|
||||||
link-workspace-packages=true
|
link-workspace-packages=true
|
||||||
prefer-workspace-packages=true
|
prefer-workspace-packages=true
|
||||||
|
|||||||
95
.pnpmfile.cjs
Normal file
95
.pnpmfile.cjs
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
const fs = require('node:fs');
|
||||||
|
const path = require('node:path');
|
||||||
|
|
||||||
|
const PACKAGE_SCOPE = '@bytelyst/';
|
||||||
|
const PACKAGE_SOURCE = process.env.BYTELYST_PACKAGE_SOURCE || 'common-plat';
|
||||||
|
const DEFAULT_COMMON_PLAT_ROOT = path.resolve(__dirname, '..', 'learning_ai', 'learning_ai_common_plat');
|
||||||
|
const LEGACY_COMMON_PLAT_ROOT = path.resolve(__dirname, '..', 'learning_ai_common_plat');
|
||||||
|
const COMMON_PLAT_ROOT =
|
||||||
|
process.env.BYTELYST_COMMON_PLAT_ROOT ||
|
||||||
|
(fs.existsSync(DEFAULT_COMMON_PLAT_ROOT) ? DEFAULT_COMMON_PLAT_ROOT : LEGACY_COMMON_PLAT_ROOT);
|
||||||
|
const COMMON_PLAT_PACKAGES_ROOT = path.join(COMMON_PLAT_ROOT, 'packages');
|
||||||
|
const VERSION_CACHE = new Map();
|
||||||
|
let loggedSource = false;
|
||||||
|
|
||||||
|
function packageDirFor(name) {
|
||||||
|
return name.startsWith(PACKAGE_SCOPE) ? name.slice(PACKAGE_SCOPE.length) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pathIfPackageExists(rootDir, name) {
|
||||||
|
const packageDir = packageDirFor(name);
|
||||||
|
if (!packageDir) return null;
|
||||||
|
|
||||||
|
const candidate = path.join(rootDir, packageDir);
|
||||||
|
return fs.existsSync(path.join(candidate, 'package.json')) ? candidate : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readPackageVersion(packagePath) {
|
||||||
|
if (VERSION_CACHE.has(packagePath)) {
|
||||||
|
return VERSION_CACHE.get(packagePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const packageJson = JSON.parse(fs.readFileSync(path.join(packagePath, 'package.json'), 'utf8'));
|
||||||
|
const version = packageJson.version || null;
|
||||||
|
VERSION_CACHE.set(packagePath, version);
|
||||||
|
return version;
|
||||||
|
} catch {
|
||||||
|
VERSION_CACHE.set(packagePath, null);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveSpecifier(name) {
|
||||||
|
if (!name.startsWith(PACKAGE_SCOPE)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const commonPlatPath = pathIfPackageExists(COMMON_PLAT_PACKAGES_ROOT, name);
|
||||||
|
|
||||||
|
if (PACKAGE_SOURCE === 'common-plat') {
|
||||||
|
return commonPlatPath ? 'workspace:*' : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PACKAGE_SOURCE === 'gitea') {
|
||||||
|
return commonPlatPath ? readPackageVersion(commonPlatPath) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return commonPlatPath ? 'workspace:*' : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function rewriteDependencySet(dependencies = {}) {
|
||||||
|
for (const dependencyName of Object.keys(dependencies)) {
|
||||||
|
const rewrittenSpecifier = resolveSpecifier(dependencyName);
|
||||||
|
if (rewrittenSpecifier) {
|
||||||
|
dependencies[dependencyName] = rewrittenSpecifier;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function logSourceOnce() {
|
||||||
|
if (loggedSource) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loggedSource = true;
|
||||||
|
process.stderr.write(
|
||||||
|
`[bytelyst] pnpm package source=${PACKAGE_SOURCE} commonPlatRoot=${COMMON_PLAT_ROOT}\n`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
hooks: {
|
||||||
|
readPackage(packageJson) {
|
||||||
|
logSourceOnce();
|
||||||
|
if (packageJson.name?.startsWith(PACKAGE_SCOPE)) {
|
||||||
|
return packageJson;
|
||||||
|
}
|
||||||
|
|
||||||
|
rewriteDependencySet(packageJson.dependencies);
|
||||||
|
rewriteDependencySet(packageJson.devDependencies);
|
||||||
|
rewriteDependencySet(packageJson.optionalDependencies);
|
||||||
|
return packageJson;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
815
docs/UX_TESTING_SETUP_GUIDE.md
Normal file
815
docs/UX_TESTING_SETUP_GUIDE.md
Normal file
@ -0,0 +1,815 @@
|
|||||||
|
# UX Implementation Guide
|
||||||
|
|
||||||
|
> **Purpose:** Comprehensive guide for UX implementation across ByteLyst products using common platform UI packages, design tokens, and Cipher design system integration.
|
||||||
|
|
||||||
|
> **Target Audience:** Product teams implementing UX improvements and testing infrastructure.
|
||||||
|
|
||||||
|
> **Product:** ChronoMind - AI-powered contextual clock & timer
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This guide documents the complete UX implementation approach for ChronoMind, replicating the patterns established in the trading dashboard. The approach covers:
|
||||||
|
|
||||||
|
- **Common Platform UI Integration** - Leveraging `@bytelyst/ui` shared primitives
|
||||||
|
- **Design Token Usage** - Using `@bytelyst/design-tokens` for consistent styling
|
||||||
|
- **Component Normalization** - Replacing one-off components with shared primitives
|
||||||
|
- **Accessibility Improvements** - Keyboard navigation, ARIA labels, focus management
|
||||||
|
- **Responsive Design** - Viewport matrix testing, shell breakpoints
|
||||||
|
- **Testing Infrastructure** - Playwright E2E tests, Storybook, AI-friendly reports
|
||||||
|
- **Cipher Design System** - Visual hierarchy, spacing, typography principles
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Part 1: Common Platform UI Integration
|
||||||
|
|
||||||
|
### Local Package Resolution
|
||||||
|
|
||||||
|
**Critical:** Use local common platform packages by default, not Gitea registry.
|
||||||
|
|
||||||
|
**File:** `.pnpmfile.cjs`
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// .pnpmfile.cjs
|
||||||
|
// Default to local common platform packages
|
||||||
|
function readPackage(pkg, context) {
|
||||||
|
if (!context.workspace) return pkg;
|
||||||
|
|
||||||
|
// Default to common-plat (local packages)
|
||||||
|
const packageSource = process.env.BYTELYST_PACKAGE_SOURCE || 'common-plat';
|
||||||
|
|
||||||
|
if (packageSource === 'common-plat') {
|
||||||
|
// Resolve @bytelyst/* packages from local common platform
|
||||||
|
if (pkg.name.startsWith('@bytelyst/')) {
|
||||||
|
pkg.dependencies = pkg.dependencies || {};
|
||||||
|
pkg.dependencies[pkg.name] = 'workspace:*';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pkg;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
name: 'bytelyst-package-source',
|
||||||
|
hooks: {
|
||||||
|
readPackage,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**File:** `.npmrc`
|
||||||
|
|
||||||
|
```ini
|
||||||
|
# Remove repo-level GITEA_NPM_TOKEN interpolation
|
||||||
|
# Gitea registry auth should live in user-level ~/.npmrc or CI secrets
|
||||||
|
# Only when BYTELYST_PACKAGE_SOURCE=gitea is explicitly used
|
||||||
|
```
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Default: Use local common platform packages
|
||||||
|
BYTELYST_PACKAGE_SOURCE=common-plat
|
||||||
|
|
||||||
|
# Optional: Use Gitea registry (requires auth)
|
||||||
|
BYTELYST_PACKAGE_SOURCE=gitea
|
||||||
|
# GITEA_NPM_TOKEN should be in ~/.npmrc or CI secrets
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verification Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Verify local package resolution
|
||||||
|
pnpm install @bytelyst/ui @bytelyst/design-tokens
|
||||||
|
|
||||||
|
# Verify packages resolve from local common platform
|
||||||
|
pnpm list @bytelyst/ui
|
||||||
|
# Should show: @bytelyst/ui -> link:../learning_ai_common_plat/packages/ui
|
||||||
|
```
|
||||||
|
|
||||||
|
### Product Adapter Pattern
|
||||||
|
|
||||||
|
Create a product adapter to normalize imports and extend shared primitives with product-specific variants:
|
||||||
|
|
||||||
|
**File:** `web/src/components/ui/Primitives.tsx`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import * as React from 'react';
|
||||||
|
import {
|
||||||
|
Badge as CommonBadge,
|
||||||
|
Button as CommonButton,
|
||||||
|
Input as CommonInput,
|
||||||
|
Select as CommonSelect,
|
||||||
|
type BadgeProps as CommonBadgeProps,
|
||||||
|
type ButtonProps as CommonButtonProps,
|
||||||
|
type InputProps as CommonInputProps,
|
||||||
|
type SelectProps as CommonSelectProps,
|
||||||
|
} from '@bytelyst/ui';
|
||||||
|
|
||||||
|
// Re-export all shared primitives
|
||||||
|
export {
|
||||||
|
ActionMenu,
|
||||||
|
AlertBanner,
|
||||||
|
DataList,
|
||||||
|
DataTable,
|
||||||
|
Drawer,
|
||||||
|
EmptyState,
|
||||||
|
Modal,
|
||||||
|
PageHeader,
|
||||||
|
Panel,
|
||||||
|
Skeleton,
|
||||||
|
Timeline,
|
||||||
|
Toolbar,
|
||||||
|
// ... all other @bytelyst/ui components
|
||||||
|
} from '@bytelyst/ui';
|
||||||
|
|
||||||
|
// Define product-specific variants for ChronoMind
|
||||||
|
type ProductButtonVariant = NonNullable<CommonButtonProps['variant']> | 'link';
|
||||||
|
type ProductButtonSize = NonNullable<CommonButtonProps['size']> | 'icon';
|
||||||
|
type ProductFieldVariant = 'surface' | 'muted';
|
||||||
|
type ProductFieldSize = 'sm' | 'md';
|
||||||
|
type ProductBadgeVariant = NonNullable<CommonBadgeProps['variant']> | 'danger';
|
||||||
|
type ProductStatusTone = 'success' | 'warning' | 'error' | 'info' | 'neutral';
|
||||||
|
|
||||||
|
// Extend interfaces with product-specific props
|
||||||
|
export interface ButtonProps extends Omit<CommonButtonProps, 'variant' | 'size'> {
|
||||||
|
variant?: ProductButtonVariant;
|
||||||
|
size?: ProductButtonSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IconButtonProps extends Omit<ButtonProps, 'children'> {
|
||||||
|
icon: React.ReactNode;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InputProps extends CommonInputProps {
|
||||||
|
controlSize?: ProductFieldSize;
|
||||||
|
variant?: ProductFieldVariant;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChronoMind-specific status mapping for badges
|
||||||
|
export type ChronoMindStatus =
|
||||||
|
| 'running' | 'paused' | 'stopped' | 'completed' | 'alarm'
|
||||||
|
| 'snoozed' | 'overdue' | 'upcoming' | 'recurring' | 'one-time'
|
||||||
|
| 'focus' | 'break' | 'pomodoro' | 'routine' | 'error' | 'success';
|
||||||
|
|
||||||
|
const chronomindStatusTone: Record<ChronoMindStatus, ProductStatusTone> = {
|
||||||
|
running: 'success',
|
||||||
|
paused: 'warning',
|
||||||
|
stopped: 'neutral',
|
||||||
|
completed: 'success',
|
||||||
|
alarm: 'error',
|
||||||
|
snoozed: 'warning',
|
||||||
|
overdue: 'error',
|
||||||
|
upcoming: 'info',
|
||||||
|
recurring: 'info',
|
||||||
|
'one-time': 'neutral',
|
||||||
|
focus: 'success',
|
||||||
|
break: 'info',
|
||||||
|
pomodoro: 'success',
|
||||||
|
routine: 'info',
|
||||||
|
error: 'error',
|
||||||
|
success: 'success',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper function to map timer status to tone
|
||||||
|
export function statusToneFor(status: ChronoMindStatus | string | null | undefined): ProductStatusTone {
|
||||||
|
if (!status) return 'neutral';
|
||||||
|
const normalized = status.trim().toLowerCase().replace(/[\s_]+/g, '-') as ChronoMindStatus;
|
||||||
|
return chronomindStatusTone[normalized] ?? 'neutral';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Product-specific component implementations
|
||||||
|
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
|
({ variant = 'primary', size = 'md', className, ...props }, ref) => (
|
||||||
|
<CommonButton
|
||||||
|
ref={ref}
|
||||||
|
variant={variant === 'link' ? 'ghost' : variant}
|
||||||
|
size={size === 'icon' ? 'sm' : size}
|
||||||
|
className={className}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const IconButton = React.forwardRef<HTMLButtonElement, IconButtonProps>(
|
||||||
|
({ icon, label, variant = 'ghost', size = 'icon', className, ...props }, ref) => (
|
||||||
|
<Button
|
||||||
|
ref={ref}
|
||||||
|
type="button"
|
||||||
|
aria-label={label}
|
||||||
|
variant={variant}
|
||||||
|
size={size}
|
||||||
|
className="shrink-0"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{icon}
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||||
|
({ controlSize = 'md', variant = 'surface', className, ...props }, ref) => (
|
||||||
|
<CommonInput
|
||||||
|
ref={ref}
|
||||||
|
className={className}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
export function ChronoMindStatusBadge({
|
||||||
|
status,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
status: ChronoMindStatus | string | null | undefined;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Badge variant={statusToneFor(status)} dot>
|
||||||
|
{children ?? status ?? 'Unknown'}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits of Product Adapter Pattern:**
|
||||||
|
- Centralized import point for all UI components
|
||||||
|
- Product-specific variants without modifying common platform
|
||||||
|
- Consistent styling across the application
|
||||||
|
- Easy to migrate to new common platform versions
|
||||||
|
- Type-safe extensions with TypeScript
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Part 2: Design Token Usage
|
||||||
|
|
||||||
|
### CSS Variable Integration
|
||||||
|
|
||||||
|
Use design tokens from `@bytelyst/design-tokens` via CSS custom properties with ChronoMind-specific prefixes:
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* Surface colors */
|
||||||
|
background: var(--cm-surface-card);
|
||||||
|
background: var(--cm-surface-muted);
|
||||||
|
background: var(--cm-surface-overlay);
|
||||||
|
|
||||||
|
/* Text colors */
|
||||||
|
color: var(--cm-text-primary);
|
||||||
|
color: var(--cm-text-secondary);
|
||||||
|
color: var(--cm-text-muted);
|
||||||
|
|
||||||
|
/* Border colors */
|
||||||
|
border-color: var(--cm-border);
|
||||||
|
border-color: var(--cm-border-muted);
|
||||||
|
|
||||||
|
/* Input styling */
|
||||||
|
background: var(--cm-input);
|
||||||
|
color: var(--cm-text-primary);
|
||||||
|
border-color: var(--cm-border);
|
||||||
|
|
||||||
|
/* Focus states */
|
||||||
|
border-color: var(--cm-focus-ring);
|
||||||
|
box-shadow: 0 0 0 2px var(--cm-focus-ring-muted);
|
||||||
|
|
||||||
|
/* Semantic colors */
|
||||||
|
color: var(--cm-accent);
|
||||||
|
background: var(--cm-success);
|
||||||
|
background: var(--cm-warning);
|
||||||
|
background: var(--cm-error);
|
||||||
|
|
||||||
|
/* Spacing */
|
||||||
|
padding: var(--cm-spacing-sm);
|
||||||
|
padding: var(--cm-spacing-md);
|
||||||
|
padding: var(--cm-spacing-lg);
|
||||||
|
padding: var(--cm-spacing-xl);
|
||||||
|
|
||||||
|
/* Border radius */
|
||||||
|
border-radius: var(--cm-radius-control);
|
||||||
|
border-radius: var(--cm-radius-md);
|
||||||
|
border-radius: var(--cm-radius-lg);
|
||||||
|
|
||||||
|
/* Typography */
|
||||||
|
font-size: var(--cm-text-sm);
|
||||||
|
font-size: var(--cm-text-base);
|
||||||
|
font-size: var(--cm-text-lg);
|
||||||
|
font-weight: var(--cm-font-medium);
|
||||||
|
font-weight: var(--cm-font-semibold);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Component Token Patterns
|
||||||
|
|
||||||
|
**Example: Badge Component for ChronoMind**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Using design tokens for badge styling
|
||||||
|
const badgeStyles = {
|
||||||
|
success: {
|
||||||
|
background: 'var(--cm-success-light)',
|
||||||
|
color: 'var(--cm-success-dark)',
|
||||||
|
borderColor: 'var(--cm-success)',
|
||||||
|
},
|
||||||
|
warning: {
|
||||||
|
background: 'var(--cm-warning-light)',
|
||||||
|
color: 'var(--cm-warning-dark)',
|
||||||
|
borderColor: 'var(--cm-warning)',
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
background: 'var(--cm-error-light)',
|
||||||
|
color: 'var(--cm-error-dark)',
|
||||||
|
borderColor: 'var(--cm-error)',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Part 3: Component Normalization
|
||||||
|
|
||||||
|
### Badge Normalization
|
||||||
|
|
||||||
|
**Before:** One-off CSS classes for different badge styles
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* Old approach - one-off classes */
|
||||||
|
.timer-chip {
|
||||||
|
background: #f0f0f0;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-tag {
|
||||||
|
background: #e0e0e0;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-pill {
|
||||||
|
background: #d0d0d0;
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**After:** Shared Badge component from product adapter
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Badge, ChronoMindStatusBadge } from '../components/ui/Primitives';
|
||||||
|
|
||||||
|
// Replace .timer-chip with Badge
|
||||||
|
<Badge variant="success">Running</Badge>
|
||||||
|
|
||||||
|
// Replace .category-tag with Badge
|
||||||
|
<Badge variant="info">Work</Badge>
|
||||||
|
|
||||||
|
// Replace .status-pill with ChronoMindStatusBadge
|
||||||
|
<ChronoMindStatusBadge status="pomodoro" />
|
||||||
|
```
|
||||||
|
|
||||||
|
### Alert Banner Unification
|
||||||
|
|
||||||
|
**Before:** Different alert implementations across components
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// TimerCard.tsx - custom alert
|
||||||
|
<div className="alert-banner" style={{ background: '#fff3cd' }}>
|
||||||
|
<span>⚠️</span>
|
||||||
|
<span>Warning message</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
// RoutineEditor.tsx - different alert
|
||||||
|
<div className="warning-box" style={{ border: '1px solid #ffc107' }}>
|
||||||
|
<div className="warning-icon">!</div>
|
||||||
|
<div className="warning-text">Warning text</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
**After:** Shared AlertBanner component
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { AlertBanner } from '../components/ui/Primitives';
|
||||||
|
|
||||||
|
// Both components now use shared AlertBanner
|
||||||
|
<AlertBanner tone="warning" title="Warning">
|
||||||
|
Warning message
|
||||||
|
</AlertBanner>
|
||||||
|
|
||||||
|
<AlertBanner tone="error" title="Error">
|
||||||
|
Error message
|
||||||
|
</AlertBanner>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Table Controls Standardization
|
||||||
|
|
||||||
|
**Before:** Custom table controls with inconsistent styling
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Different button styles across tables
|
||||||
|
<button className="table-action-btn-small">Edit</button>
|
||||||
|
<button className="timer-filter-btn">Filter</button>
|
||||||
|
<button className="routine-action">Complete</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
**After:** Standardized Button component
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Button, IconButton } from '../components/ui/Primitives';
|
||||||
|
|
||||||
|
// Consistent button styles
|
||||||
|
<Button variant="ghost" size="sm">Edit</Button>
|
||||||
|
<Button variant="secondary" size="sm">Filter</Button>
|
||||||
|
<IconButton icon={<CheckIcon />} label="Complete" />
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Part 4: Accessibility Improvements
|
||||||
|
|
||||||
|
### Keyboard Navigation
|
||||||
|
|
||||||
|
**Focus Management**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Ensure interactive elements are focusable
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault();
|
||||||
|
handleClick();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Action
|
||||||
|
</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Keyboard Toggles**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
<div
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault();
|
||||||
|
toggle();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
aria-pressed={isActive}
|
||||||
|
>
|
||||||
|
Toggle Option
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### ARIA Labels
|
||||||
|
|
||||||
|
**Button Labels**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Icon buttons need explicit labels
|
||||||
|
<IconButton
|
||||||
|
icon={<PlayIcon />}
|
||||||
|
label="Start timer"
|
||||||
|
aria-label="Start timer"
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Status Indicators**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Status badges need descriptive labels
|
||||||
|
<ChronoMindStatusBadge
|
||||||
|
status="running"
|
||||||
|
aria-label="Status: Running"
|
||||||
|
>
|
||||||
|
Running
|
||||||
|
</ChronoMindStatusBadge>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Form Labels**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// All inputs need associated labels
|
||||||
|
<label htmlFor="timer-name">
|
||||||
|
Timer Name
|
||||||
|
<Input id="timer-name" type="text" />
|
||||||
|
</label>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Focus Indicators
|
||||||
|
|
||||||
|
**Visible Focus States**
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* Ensure focus is visible */
|
||||||
|
*:focus-visible {
|
||||||
|
outline: 2px solid var(--cm-focus-ring);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Or use design token */
|
||||||
|
*:focus-visible {
|
||||||
|
outline: 2px solid var(--cm-focus-ring);
|
||||||
|
box-shadow: 0 0 0 2px var(--cm-focus-ring-muted);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Part 5: Responsive Design
|
||||||
|
|
||||||
|
### Shell Breakpoints
|
||||||
|
|
||||||
|
**Responsive Shell Testing**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const breakpoints = {
|
||||||
|
mobile: 'max-width: 560px',
|
||||||
|
tablet: 'max-width: 768px',
|
||||||
|
desktop: 'min-width: 769px',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Shell adapts at these breakpoints
|
||||||
|
@media (max-width: 560px) {
|
||||||
|
.timer-main {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
.timer-right-panel {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.timer-sidebar {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 60px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Viewport Matrix Testing
|
||||||
|
|
||||||
|
**Test all routes across viewports**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const routes = ['/dashboard', '/focus', '/history', '/routines', '/settings'];
|
||||||
|
const viewports = [
|
||||||
|
{ name: 'Desktop', width: 1200, height: 800 },
|
||||||
|
{ name: 'Tablet', width: 768, height: 1024 },
|
||||||
|
{ name: 'Mobile', width: 375, height: 667 },
|
||||||
|
];
|
||||||
|
|
||||||
|
routes.forEach((route) => {
|
||||||
|
viewports.forEach((viewport) => {
|
||||||
|
test(`${route} - ${viewport.name} viewport`, async ({ page }) => {
|
||||||
|
await page.setViewportSize({ width: viewport.width, height: viewport.height });
|
||||||
|
await page.goto(route);
|
||||||
|
|
||||||
|
// Check for horizontal overflow
|
||||||
|
const bodyWidth = await page.evaluate(() => document.body.scrollWidth);
|
||||||
|
expect(bodyWidth).toBeLessThanOrEqual(viewport.width + 10);
|
||||||
|
|
||||||
|
// Check main content visibility
|
||||||
|
const mainContent = page.locator('main');
|
||||||
|
await expect(mainContent).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Part 6: Testing Infrastructure
|
||||||
|
|
||||||
|
### Playwright Setup
|
||||||
|
|
||||||
|
**Install Dependencies**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd web
|
||||||
|
pnpm add -D @playwright/test
|
||||||
|
pnpm exec playwright install chromium
|
||||||
|
```
|
||||||
|
|
||||||
|
**Create Playwright Config**
|
||||||
|
|
||||||
|
Create `web/playwright.config.ts`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { defineConfig, devices } from '@playwright/test';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: './e2e',
|
||||||
|
fullyParallel: true,
|
||||||
|
forbidOnly: !!process.env.CI,
|
||||||
|
retries: process.env.CI ? 2 : 0,
|
||||||
|
workers: process.env.CI ? 1 : undefined,
|
||||||
|
reporter: 'html',
|
||||||
|
use: {
|
||||||
|
baseURL: 'http://localhost:3030',
|
||||||
|
trace: 'on-first-retry',
|
||||||
|
screenshot: 'only-on-failure',
|
||||||
|
},
|
||||||
|
projects: [
|
||||||
|
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
|
||||||
|
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
|
||||||
|
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
|
||||||
|
{ name: 'Mobile Chrome', use: { ...devices['Pixel 5'] } },
|
||||||
|
{ name: 'Mobile Safari', use: { ...devices['iPhone 12'] } },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Suite Structure
|
||||||
|
|
||||||
|
Create tests in `web/e2e/` directory:
|
||||||
|
|
||||||
|
```
|
||||||
|
web/e2e/
|
||||||
|
├── viewport-matrix.spec.ts # Viewport compliance
|
||||||
|
├── horizontal-overflow.spec.ts # Overflow detection
|
||||||
|
├── alert-positioning.spec.ts # Critical alerts positioning
|
||||||
|
├── timer-flows.spec.ts # Timer start/pause/stop flows
|
||||||
|
├── page-states.spec.ts # Loading/empty/error/success states
|
||||||
|
├── form-validation.spec.ts # Form validation
|
||||||
|
└── keyboard-navigation.spec.ts # Keyboard navigation
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Part 7: Cipher Design System Integration
|
||||||
|
|
||||||
|
### Design Principles
|
||||||
|
|
||||||
|
Follow Cipher design system principles for consistent UX across ChronoMind:
|
||||||
|
|
||||||
|
#### 1. Visual Hierarchy
|
||||||
|
|
||||||
|
**Size and Weight**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Use design tokens for consistent hierarchy
|
||||||
|
<h1 style={{ fontSize: 'var(--cm-text-2xl)', fontWeight: 'var(--cm-font-bold)' }}>
|
||||||
|
Timer Title
|
||||||
|
</h1>
|
||||||
|
<h2 style={{ fontSize: 'var(--cm-text-xl)', fontWeight: 'var(--cm-font-semibold)' }}>
|
||||||
|
Section Header
|
||||||
|
</h2>
|
||||||
|
<p style={{ fontSize: 'var(--cm-text-base)', color: 'var(--cm-text-secondary)' }}>
|
||||||
|
Timer content
|
||||||
|
</p>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Spacing System
|
||||||
|
|
||||||
|
**Consistent Spacing**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Use design token spacing
|
||||||
|
<div style={{ padding: 'var(--cm-spacing-md)' }}>
|
||||||
|
Content
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ gap: 'var(--cm-spacing-sm)' }}>
|
||||||
|
<Button>First</Button>
|
||||||
|
<Button>Second</Button>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Typography
|
||||||
|
|
||||||
|
**Font Scales**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Use design token font sizes
|
||||||
|
const textStyles = {
|
||||||
|
xs: 'var(--cm-text-xs)',
|
||||||
|
sm: 'var(--cm-text-sm)',
|
||||||
|
base: 'var(--cm-text-base)',
|
||||||
|
lg: 'var(--cm-text-lg)',
|
||||||
|
xl: 'var(--cm-text-xl)',
|
||||||
|
'2xl': 'var(--cm-text-2xl)',
|
||||||
|
};
|
||||||
|
|
||||||
|
<p style={{ fontSize: textStyles.base }}>Body text</p>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. Color System
|
||||||
|
|
||||||
|
**Semantic Colors**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Use semantic color tokens, not literal colors
|
||||||
|
const statusColors = {
|
||||||
|
success: 'var(--cm-success)',
|
||||||
|
warning: 'var(--cm-warning)',
|
||||||
|
error: 'var(--cm-error)',
|
||||||
|
info: 'var(--cm-info)',
|
||||||
|
};
|
||||||
|
|
||||||
|
<div style={{ color: statusColors.success }}>Success message</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Part 8: Implementation Roadmap
|
||||||
|
|
||||||
|
### Phase 1: Foundation Setup
|
||||||
|
|
||||||
|
**Week 1-2**
|
||||||
|
- [ ] Set up local package resolution (.pnpmfile.cjs)
|
||||||
|
- [ ] Remove repo-level GITEA_NPM_TOKEN from .npmrc
|
||||||
|
- [ ] Install common platform packages from local source
|
||||||
|
- [ ] Create product adapter (`Primitives.tsx`)
|
||||||
|
- [ ] Set up design token integration
|
||||||
|
- [ ] Configure Playwright
|
||||||
|
|
||||||
|
### Phase 2: Component Normalization
|
||||||
|
|
||||||
|
**Week 3-4**
|
||||||
|
- [ ] Replace one-off badges with Badge component
|
||||||
|
- [ ] Replace custom buttons with Button component
|
||||||
|
- [ ] Replace custom alerts with AlertBanner component
|
||||||
|
- [ ] Replace custom inputs with Input component
|
||||||
|
- [ ] Remove old CSS classes
|
||||||
|
|
||||||
|
### Phase 3: Design Token Migration
|
||||||
|
|
||||||
|
**Week 5-6**
|
||||||
|
- [ ] Identify hardcoded colors
|
||||||
|
- [ ] Map to semantic tokens
|
||||||
|
- [ ] Replace with CSS variables
|
||||||
|
- [ ] Test across themes
|
||||||
|
- [ ] Audit for missed tokens
|
||||||
|
|
||||||
|
### Phase 4: Accessibility Improvements
|
||||||
|
|
||||||
|
**Week 7**
|
||||||
|
- [ ] Add keyboard navigation
|
||||||
|
- [ ] Add ARIA labels
|
||||||
|
- [ ] Improve focus indicators
|
||||||
|
- [ ] Test with screen readers
|
||||||
|
|
||||||
|
### Phase 5: Responsive Design
|
||||||
|
|
||||||
|
**Week 8**
|
||||||
|
- [ ] Test viewport matrix
|
||||||
|
- [ ] Fix horizontal overflow
|
||||||
|
- [ ] Optimize mobile layout
|
||||||
|
- [ ] Test breakpoints
|
||||||
|
|
||||||
|
### Phase 6: Testing Infrastructure
|
||||||
|
|
||||||
|
**Week 9-10**
|
||||||
|
- [ ] Create E2E test suite
|
||||||
|
- [ ] Set up test runner script
|
||||||
|
- [ ] Configure AI-friendly reports
|
||||||
|
- [ ] Integrate with CI
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Part 9: Verification Commands
|
||||||
|
|
||||||
|
### Type Checking
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Verify TypeScript types
|
||||||
|
pnpm run typecheck
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build Verification
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Ensure build succeeds
|
||||||
|
pnpm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Execution
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run all E2E tests
|
||||||
|
cd web
|
||||||
|
pnpm test:e2e
|
||||||
|
|
||||||
|
# Run specific test suites
|
||||||
|
pnpm test:e2e:viewport
|
||||||
|
pnpm test:e2e:overflow
|
||||||
|
pnpm test:e2e:timer-flows
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
This guide provides a comprehensive approach to implementing UX improvements for ChronoMind using the ByteLyst common platform UI packages, design tokens, and Cipher design system. By following these patterns, ChronoMind will achieve:
|
||||||
|
|
||||||
|
- Consistent UI with other ByteLyst products
|
||||||
|
- Improved accessibility and keyboard navigation
|
||||||
|
- Responsive design across all viewports
|
||||||
|
- Comprehensive testing infrastructure
|
||||||
|
- Maintainable and scalable codebase
|
||||||
|
|
||||||
|
The implementation is organized in phases to allow for incremental adoption and verification at each stage.
|
||||||
369
web/src/components/ui/Primitives.tsx
Normal file
369
web/src/components/ui/Primitives.tsx
Normal file
@ -0,0 +1,369 @@
|
|||||||
|
import {
|
||||||
|
AppShell as BytelystAppShell,
|
||||||
|
AppShellMain as BytelystAppShellMain,
|
||||||
|
AppShellMobileToggle as BytelystAppShellMobileToggle,
|
||||||
|
AppShellNav as BytelystAppShellNav,
|
||||||
|
AppShellNavItem as BytelystAppShellNavItem,
|
||||||
|
AppShellOverlay as BytelystAppShellOverlay,
|
||||||
|
AppShellPageHeader as BytelystAppShellPageHeader,
|
||||||
|
AppShellSidebar as BytelystAppShellSidebar,
|
||||||
|
AppShellSkipLink as BytelystAppShellSkipLink,
|
||||||
|
Badge as BytelystBadge,
|
||||||
|
Button as BytelystButton,
|
||||||
|
Card as BytelystCard,
|
||||||
|
Checkbox,
|
||||||
|
DataList as BytelystDataList,
|
||||||
|
DataListItem as BytelystDataListItem,
|
||||||
|
DataListMeta,
|
||||||
|
DataTable,
|
||||||
|
DataTableBody,
|
||||||
|
DataTableCell,
|
||||||
|
DataTableHead,
|
||||||
|
DataTableHeader,
|
||||||
|
DataTableRow,
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuGroup,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuRadioGroup,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuSub,
|
||||||
|
DropdownMenuSubTrigger,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
EmptyState as BytelystEmptyState,
|
||||||
|
IconButton as BytelystIconButton,
|
||||||
|
Input as BytelystInput,
|
||||||
|
Label as BytelystLabel,
|
||||||
|
ListItemButton as BytelystListItemButton,
|
||||||
|
LoadingSpinner as BytelystLoadingSpinner,
|
||||||
|
Panel as BytelystPanel,
|
||||||
|
PanelBody as BytelystPanelBody,
|
||||||
|
PanelDescription as BytelystPanelDescription,
|
||||||
|
PanelHeader as BytelystPanelHeader,
|
||||||
|
PanelTitle as BytelystPanelTitle,
|
||||||
|
RadioGroup,
|
||||||
|
RadioGroupItem,
|
||||||
|
SegmentedControl,
|
||||||
|
Select as BytelystSelect,
|
||||||
|
StatusBadge as BytelystStatusBadge,
|
||||||
|
Surface as BytelystSurface,
|
||||||
|
SurfaceList as BytelystSurfaceList,
|
||||||
|
SurfaceListItem as BytelystSurfaceListItem,
|
||||||
|
Switch,
|
||||||
|
Tabs,
|
||||||
|
TabsContent,
|
||||||
|
TabsList,
|
||||||
|
TabsTrigger,
|
||||||
|
Textarea as BytelystTextarea,
|
||||||
|
Timeline as BytelystTimeline,
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
type AppShellMainProps,
|
||||||
|
type AppShellMobileToggleProps,
|
||||||
|
type AppShellNavItemProps,
|
||||||
|
type AppShellNavProps,
|
||||||
|
type AppShellOverlayProps,
|
||||||
|
type AppShellPageHeaderProps,
|
||||||
|
type AppShellProps,
|
||||||
|
type AppShellSidebarProps,
|
||||||
|
type AppShellSkipLinkProps,
|
||||||
|
type BadgeProps,
|
||||||
|
type ButtonProps,
|
||||||
|
type CardProps,
|
||||||
|
type DataListItemProps,
|
||||||
|
type DataListProps,
|
||||||
|
type EmptyStateProps,
|
||||||
|
type IconButtonProps,
|
||||||
|
type InputProps,
|
||||||
|
type LabelProps,
|
||||||
|
type ListItemButtonProps,
|
||||||
|
type LoadingSpinnerProps,
|
||||||
|
type PanelBodyProps,
|
||||||
|
type PanelDescriptionProps,
|
||||||
|
type PanelHeaderProps,
|
||||||
|
type PanelProps,
|
||||||
|
type PanelTitleProps,
|
||||||
|
type SelectProps,
|
||||||
|
type StatusBadgeProps,
|
||||||
|
type StatusTone,
|
||||||
|
type SurfaceListItemProps,
|
||||||
|
type SurfaceListProps,
|
||||||
|
type SurfaceProps,
|
||||||
|
type TextareaProps,
|
||||||
|
type TimelineProps,
|
||||||
|
} from "@bytelyst/ui";
|
||||||
|
|
||||||
|
function mergeClassNames(...classes: Array<string | undefined>) {
|
||||||
|
return classes.filter(Boolean).join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Checkbox,
|
||||||
|
DataListMeta,
|
||||||
|
DataTable,
|
||||||
|
DataTableBody,
|
||||||
|
DataTableCell,
|
||||||
|
DataTableHead,
|
||||||
|
DataTableHeader,
|
||||||
|
DataTableRow,
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuGroup,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuRadioGroup,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuSub,
|
||||||
|
DropdownMenuSubTrigger,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
RadioGroup,
|
||||||
|
RadioGroupItem,
|
||||||
|
SegmentedControl,
|
||||||
|
Switch,
|
||||||
|
Tabs,
|
||||||
|
TabsContent,
|
||||||
|
TabsList,
|
||||||
|
TabsTrigger,
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ChronoMindStatus =
|
||||||
|
| "running"
|
||||||
|
| "paused"
|
||||||
|
| "stopped"
|
||||||
|
| "completed"
|
||||||
|
| "alarm"
|
||||||
|
| "snoozed"
|
||||||
|
| "overdue"
|
||||||
|
| "upcoming"
|
||||||
|
| "recurring"
|
||||||
|
| "one-time"
|
||||||
|
| "focus"
|
||||||
|
| "break"
|
||||||
|
| "pomodoro"
|
||||||
|
| "routine"
|
||||||
|
| "error"
|
||||||
|
| "success";
|
||||||
|
|
||||||
|
const statusToneMap: Record<ChronoMindStatus, StatusTone> = {
|
||||||
|
running: "success",
|
||||||
|
paused: "warning",
|
||||||
|
stopped: "neutral",
|
||||||
|
completed: "success",
|
||||||
|
alarm: "danger",
|
||||||
|
snoozed: "warning",
|
||||||
|
overdue: "danger",
|
||||||
|
upcoming: "info",
|
||||||
|
recurring: "info",
|
||||||
|
"one-time": "neutral",
|
||||||
|
focus: "success",
|
||||||
|
break: "info",
|
||||||
|
pomodoro: "success",
|
||||||
|
routine: "info",
|
||||||
|
error: "danger",
|
||||||
|
success: "success",
|
||||||
|
};
|
||||||
|
|
||||||
|
export function AppShell({ className, ...props }: AppShellProps) {
|
||||||
|
return <BytelystAppShell className={className} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AppShellSkipLink({ className, ...props }: AppShellSkipLinkProps) {
|
||||||
|
return <BytelystAppShellSkipLink className={className} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AppShellMobileToggle({ className, ...props }: AppShellMobileToggleProps) {
|
||||||
|
return <BytelystAppShellMobileToggle className={className} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AppShellOverlay({ className, ...props }: AppShellOverlayProps) {
|
||||||
|
return <BytelystAppShellOverlay className={className} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AppShellSidebar({ className, ...props }: AppShellSidebarProps) {
|
||||||
|
return <BytelystAppShellSidebar className={className} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AppShellMain({ className, ...props }: AppShellMainProps) {
|
||||||
|
return <BytelystAppShellMain className={className} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AppShellPageHeader({ className, ...props }: AppShellPageHeaderProps) {
|
||||||
|
return <BytelystAppShellPageHeader className={className} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AppShellNav({ className, ...props }: AppShellNavProps) {
|
||||||
|
return <BytelystAppShellNav className={className} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AppShellNavItem({ className, ...props }: AppShellNavItemProps) {
|
||||||
|
return <BytelystAppShellNavItem className={className} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getStatusTone(status: ChronoMindStatus): StatusTone {
|
||||||
|
return statusToneMap[status];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Button({ className, ...props }: ButtonProps) {
|
||||||
|
return (
|
||||||
|
<BytelystButton
|
||||||
|
className={mergeClassNames("rounded-[var(--cm-radius-sm)]", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Badge({ className, ...props }: BadgeProps) {
|
||||||
|
return <BytelystBadge className={className} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Card({ className, ...props }: CardProps) {
|
||||||
|
return (
|
||||||
|
<BytelystCard
|
||||||
|
className={mergeClassNames(
|
||||||
|
"rounded-[var(--cm-radius-md)] shadow-[var(--cm-elevation-md)]",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Panel({ className, ...props }: PanelProps) {
|
||||||
|
return (
|
||||||
|
<BytelystPanel
|
||||||
|
className={mergeClassNames(
|
||||||
|
"rounded-[var(--cm-radius-md)] shadow-[var(--cm-elevation-md)]",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PanelHeader({ className, ...props }: PanelHeaderProps) {
|
||||||
|
return <BytelystPanelHeader className={mergeClassNames("gap-[var(--cm-space-3)]", className)} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PanelBody({ className, ...props }: PanelBodyProps) {
|
||||||
|
return <BytelystPanelBody className={mergeClassNames("gap-[var(--cm-space-3)]", className)} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PanelTitle({ className, ...props }: PanelTitleProps) {
|
||||||
|
return <BytelystPanelTitle className={className} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PanelDescription({ className, ...props }: PanelDescriptionProps) {
|
||||||
|
return <BytelystPanelDescription className={className} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function IconButton({ className, ...props }: IconButtonProps) {
|
||||||
|
return <BytelystIconButton className={mergeClassNames("rounded-[var(--cm-radius-sm)]", className)} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ListItemButton({ className, ...props }: ListItemButtonProps) {
|
||||||
|
return (
|
||||||
|
<BytelystListItemButton
|
||||||
|
className={mergeClassNames(
|
||||||
|
"rounded-[var(--cm-radius-sm)]",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChronoMindStatusBadgeProps extends StatusBadgeProps {
|
||||||
|
status?: ChronoMindStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function StatusBadge({ className, status, tone, ...props }: ChronoMindStatusBadgeProps) {
|
||||||
|
return <BytelystStatusBadge className={className} tone={tone ?? (status ? getStatusTone(status) : undefined)} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Input({ className, ...props }: InputProps) {
|
||||||
|
return (
|
||||||
|
<BytelystInput
|
||||||
|
className={mergeClassNames(
|
||||||
|
"rounded-[var(--cm-radius-sm)]",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Textarea({ className, ...props }: TextareaProps) {
|
||||||
|
return (
|
||||||
|
<BytelystTextarea
|
||||||
|
className={mergeClassNames(
|
||||||
|
"rounded-[var(--cm-radius-sm)]",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Select({ className, ...props }: SelectProps) {
|
||||||
|
return (
|
||||||
|
<BytelystSelect
|
||||||
|
className={mergeClassNames(
|
||||||
|
"rounded-[var(--cm-radius-sm)]",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Label({ className, ...props }: LabelProps) {
|
||||||
|
return <BytelystLabel className={className} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Timeline({ className, ...props }: TimelineProps) {
|
||||||
|
return <BytelystTimeline className={mergeClassNames("gap-[var(--cm-space-3)]", className)} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Surface({ className, padding = "md", ...props }: SurfaceProps) {
|
||||||
|
return (
|
||||||
|
<BytelystSurface
|
||||||
|
padding={padding}
|
||||||
|
className={mergeClassNames("rounded-[var(--cm-radius-md)]", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SurfaceList({ density = "normal", className, ...props }: SurfaceListProps) {
|
||||||
|
return <BytelystSurfaceList density={density} className={mergeClassNames("gap-[var(--cm-space-3)]", className)} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SurfaceListItem({ className, ...props }: SurfaceListItemProps) {
|
||||||
|
return <BytelystSurfaceListItem className={mergeClassNames("rounded-[var(--cm-radius-sm)]", className)} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DataList({ density = "normal", className, ...props }: DataListProps) {
|
||||||
|
return <BytelystDataList density={density} className={mergeClassNames("gap-[var(--cm-space-3)]", className)} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DataListItem({ className, ...props }: DataListItemProps) {
|
||||||
|
return <BytelystDataListItem className={mergeClassNames("rounded-[var(--cm-radius-sm)]", className)} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const OperationalList = DataList;
|
||||||
|
export const OperationalListItem = DataListItem;
|
||||||
|
|
||||||
|
export function EmptyState({ className, ...props }: EmptyStateProps) {
|
||||||
|
return <BytelystEmptyState className={mergeClassNames("rounded-[var(--cm-radius-md)]", className)} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LoadingSpinner({ className, ...props }: LoadingSpinnerProps) {
|
||||||
|
return <BytelystLoadingSpinner className={className} {...props} />;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user