learning_ai_notes/docs/UX_TESTING_SETUP_GUIDE.md

20 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: NoteLett - Agentic note-taking workspace for notes, tasks, relationships, and workspaces


Overview

This guide documents the complete UX implementation approach for NoteLett, 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,
  Field,
  FieldContent,
  FieldDescription,
  FieldError,
  FieldGroup,
  FieldLabel,
  FieldTitle,
  Input as CommonInput,
  Select as CommonSelect,
  Textarea as CommonTextarea,
  type BadgeProps as CommonBadgeProps,
  type ButtonProps as CommonButtonProps,
  type InputProps as CommonInputProps,
  type SelectProps as CommonSelectProps,
  type TextareaProps as CommonTextareaProps,
} from '@bytelyst/ui';

// Re-export all shared primitives
export {
  ActionMenu,
  AlertBanner,
  DataList,
  DataTable,
  Drawer,
  EmptyState,
  EntityCard,
  FieldGrid,
  FilterBar,
  FormSection,
  MetricCard,
  Modal,
  PageHeader,
  Panel,
  Skeleton,
  Timeline,
  Toolbar,
  // ... all other @bytelyst/ui components
} from '@bytelyst/ui';

// Define product-specific variants for NoteLett
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;
}

// NoteLett-specific status mapping for badges
export type NotelettStatus =
  | 'active' | 'archived' | 'pinned' | 'shared' | 'private'
  | 'draft' | 'published' | 'linked' | 'orphaned' | 'synced'
  | 'syncing' | 'error' | 'success' | 'warning' | 'info' | 'neutral';

const notelettStatusTone: Record<NotelettStatus, ProductStatusTone> = {
  active: 'success',
  archived: 'neutral',
  pinned: 'info',
  shared: 'success',
  private: 'neutral',
  draft: 'warning',
  published: 'success',
  linked: 'info',
  orphaned: 'warning',
  synced: 'success',
  syncing: 'info',
  error: 'error',
  success: 'success',
  warning: 'warning',
  info: 'info',
  neutral: 'neutral',
};

// Helper function to map note status to tone
export function statusToneFor(status: NotelettStatus | string | null | undefined): ProductStatusTone {
  if (!status) return 'neutral';
  const normalized = status.trim().toLowerCase().replace(/[\s_]+/g, '-') as NotelettStatus;
  return notelettStatusTone[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={cn('notelett-button', 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={cn('shrink-0', className)}
      {...props}
    >
      {icon}
    </Button>
  ),
);

export const Input = React.forwardRef<HTMLInputElement, InputProps>(
  ({ controlSize = 'md', variant = 'surface', className, ...props }, ref) => (
    <CommonInput
      ref={ref}
      className={cn(
        controlSize === 'sm' ? 'min-h-9 px-3 py-2 text-xs' : 'min-h-11 px-3.5 py-2.5 text-sm',
        variant === 'surface' ? 'bg-[var(--nl-input)]' : 'bg-[var(--nl-surface-muted)]',
        className,
      )}
      {...props}
    />
  ),
);

export function NotelettStatusBadge({
  status,
  children,
}: {
  status: NotelettStatus | 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 NoteLett-specific prefixes:

/* Surface colors */
background: var(--nl-surface-card);
background: var(--nl-surface-muted);
background: var(--nl-surface-overlay);

/* Text colors */
color: var(--nl-text-primary);
color: var(--nl-text-secondary);
color: var(--nl-text-muted);

/* Border colors */
border-color: var(--nl-border);
border-color: var(--nl-border-muted);

/* Input styling */
background: var(--nl-input);
color: var(--nl-text-primary);
border-color: var(--nl-border);

/* Focus states */
border-color: var(--nl-focus-ring);
box-shadow: 0 0 0 2px var(--nl-focus-ring-muted);

/* Semantic colors */
color: var(--nl-accent);
background: var(--nl-success);
background: var(--nl-warning);
background: var(--nl-error);

/* Spacing */
padding: var(--nl-spacing-sm);
padding: var(--nl-spacing-md);
padding: var(--nl-spacing-lg);
padding: var(--nl-spacing-xl);

/* Border radius */
border-radius: var(--nl-radius-control);
border-radius: var(--nl-radius-md);
border-radius: var(--nl-radius-lg);

/* Typography */
font-size: var(--nl-text-sm);
font-size: var(--nl-text-base);
font-size: var(--nl-text-lg);
font-weight: var(--nl-font-medium);
font-weight: var(--nl-font-semibold);

Component Token Patterns

Example: Badge Component for NoteLett

// Using design tokens for badge styling
const badgeStyles = {
  success: {
    background: 'var(--nl-success-light)',
    color: 'var(--nl-success-dark)',
    borderColor: 'var(--nl-success)',
  },
  warning: {
    background: 'var(--nl-warning-light)',
    color: 'var(--nl-warning-dark)',
    borderColor: 'var(--nl-warning)',
  },
  error: {
    background: 'var(--nl-error-light)',
    color: 'var(--nl-error-dark)',
    borderColor: 'var(--nl-error)',
  },
};

Part 3: Component Normalization

Badge Normalization

Before: One-off CSS classes for different badge styles

/* Old approach - one-off classes */
.note-chip {
  background: #f0f0f0;
  padding: 4px 8px;
  border-radius: 4px;
  font-size: 12px;
}

.workspace-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, NotelettStatusBadge } from '../components/ui/Primitives';

// Replace .note-chip with Badge
<Badge variant="success">Active</Badge>

// Replace .workspace-tag with Badge
<Badge variant="info">Personal</Badge>

// Replace .status-pill with NotelettStatusBadge
<NotelettStatusBadge status="pinned" />

Alert Banner Unification

Before: Different alert implementations across components

// NotesList.tsx - custom alert
<div className="alert-banner" style={{ background: '#fff3cd' }}>
  <span>⚠️</span>
  <span>Warning message</span>
</div>

// WorkspacesTab.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="notes-filter-btn">Filter</button>
<button className="task-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={<EditIcon />}
  label="Edit note"
  aria-label="Edit note"
/>

Status Indicators

// Status badges need descriptive labels
<NotelettStatusBadge
  status="active"
  aria-label="Status: Active"
>
  Active
</NotelettStatusBadge>

Form Labels

// All inputs need associated labels
<Field>
  <FieldLabel htmlFor="title">Note Title</FieldLabel>
  <Input id="title" type="text" />
  <FieldDescription>Enter a descriptive title for your note</FieldDescription>
</Field>

Focus Indicators

Visible Focus States

/* Ensure focus is visible */
*:focus-visible {
  outline: 2px solid var(--nl-focus-ring);
  outline-offset: 2px;
}

/* Or use design token */
*:focus-visible {
  outline: 2px solid var(--nl-focus-ring);
  box-shadow: 0 0 0 2px var(--nl-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) {
  .notes-main {
    margin-left: 0;
  }
  .notes-right-panel {
    display: none;
  }
  .notes-sidebar {
    position: fixed;
    bottom: 0;
    width: 100%;
    height: 60px;
  }
}

Viewport Matrix Testing

Test all routes across viewports

const routes = ['/notes', '/workspaces', '/tasks', '/relationships'];
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:3045',
    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
├── feedback.spec.ts             # Save/delete/update feedback
├── page-states.spec.ts          # Loading/empty/error/success states
├── form-validation.spec.ts      # Form validation
└── keyboard-navigation.spec.ts   # Keyboard navigation

Test Runner Script

Create scripts/tests/run-e2e.sh with server lifecycle management:

#!/bin/bash
set -e

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
WEB_DIR="$PROJECT_ROOT/web"
REPORTS_DIR="$SCRIPT_DIR/reports"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
PORT=3045

cd "$WEB_DIR"

# Kill existing server, start fresh, run tests, cleanup
# ... (same as trading platform script)

Part 7: Cipher Design System Integration

Design Principles

Follow Cipher design system principles for consistent UX across NoteLett:

1. Visual Hierarchy

Size and Weight

// Use design tokens for consistent hierarchy
<h1 style={{ fontSize: 'var(--nl-text-2xl)', fontWeight: 'var(--nl-font-bold)' }}>
  Note Title
</h1>
<h2 style={{ fontSize: 'var(--nl-text-xl)', fontWeight: 'var(--nl-font-semibold)' }}>
  Section Header
</h2>
<p style={{ fontSize: 'var(--nl-text-base)', color: 'var(--nl-text-secondary)' }}>
  Note content
</p>

2. Spacing System

Consistent Spacing

// Use design token spacing
<div style={{ padding: 'var(--nl-spacing-md)' }}>
  Content
</div>

<div style={{ gap: 'var(--nl-spacing-sm)' }}>
  <Button>First</Button>
  <Button>Second</Button>
</div>

3. Typography

Font Scales

// Use design token font sizes
const textStyles = {
  xs: 'var(--nl-text-xs)',
  sm: 'var(--nl-text-sm)',
  base: 'var(--nl-text-base)',
  lg: 'var(--nl-text-lg)',
  xl: 'var(--nl-text-xl)',
  '2xl': 'var(--nl-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(--nl-success)',
  warning: 'var(--nl-warning)',
  error: 'var(--nl-error)',
  info: 'var(--nl-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
  • Configure Storybook

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
  • Set up Storybook
  • Integrate with CI

Part 9: Verification Commands

UI Audit

# Check for raw controls and direct imports
pnpm run audit:ui

# Strict audit (enforces token usage)
pnpm run audit:ui:strict

Type Checking

# Verify TypeScript types
pnpm run typecheck

Build Verification

# Ensure build succeeds
pnpm run build

Test Execution

# Run all E2E tests
./scripts/tests/run-e2e.sh

# Run specific test suites
cd web
pnpm test:e2e:viewport
pnpm test:e2e:overflow
pnpm test:e2e:ui

Summary

This guide enables NoteLett to replicate the exact same UX implementation approach as the trading dashboard, leveraging common platform packages, design tokens, and Cipher design system principles.

Key Benefits:

  • Consistent UX across ByteLyst products
  • Reduced development time with shared primitives
  • Improved accessibility and responsive design
  • Automated testing ensures quality
  • AI-friendly reports enable continuous improvement

Product-Specific Adaptations:

  • NoteLett status mapping (active, archived, pinned, shared, etc.)
  • Note-specific design tokens (--nl-* prefix)
  • Note-taking workflow focused test cases
  • Workspace and relationship management patterns