learning_ai_clock/docs/UX_TESTING_SETUP_GUIDE.md
root ce7e04a158 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>
2026-05-09 22:10:55 +00:00

19 KiB

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

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

# 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

# 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

# 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

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:

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

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

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

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

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

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

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

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

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

<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

// Icon buttons need explicit labels
<IconButton
  icon={<PlayIcon />}
  label="Start timer"
  aria-label="Start timer"
/>

Status Indicators

// Status badges need descriptive labels
<ChronoMindStatusBadge
  status="running"
  aria-label="Status: Running"
>
  Running
</ChronoMindStatusBadge>

Form Labels

// All inputs need associated labels
<label htmlFor="timer-name">
  Timer Name
  <Input id="timer-name" type="text" />
</label>

Focus Indicators

Visible Focus States

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

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

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

cd web
pnpm add -D @playwright/test
pnpm exec playwright install chromium

Create Playwright Config

Create web/playwright.config.ts:

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

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

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

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

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

# Verify TypeScript types
pnpm run typecheck

Build Verification

# Ensure build succeeds
pnpm run build

Test Execution

# 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.