Compare commits
10 Commits
63211c0019
...
3c5856b2f5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c5856b2f5 | ||
|
|
fa00722a39 | ||
|
|
28189ac916 | ||
|
|
b50e340f5a | ||
|
|
3fe4f0786c | ||
| a983e044b1 | |||
| db9b4557d8 | |||
| 2ba846698f | |||
| 7063e59078 | |||
| 2a2c773ca1 |
13
.npmrc
13
.npmrc
@ -1,5 +1,16 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
@bytelyst:registry=http://${GITEA_NPM_HOST:-localhost}:3300/api/packages/ByteLyst/npm/
|
@bytelyst:registry=http://${GITEA_NPM_HOST:-localhost}:3300/api/packages/ByteLyst/npm/
|
||||||
//localhost:3300/api/packages/ByteLyst/npm/:_authToken=${GITEA_NPM_TOKEN}
|
//${GITEA_NPM_HOST:-localhost}:3300}/api/packages/ByteLyst/npm/:_authToken=${GITEA_NPM_TOKEN}
|
||||||
strict-ssl=false
|
strict-ssl=false
|
||||||
link-workspace-packages=true
|
link-workspace-packages=true
|
||||||
prefer-workspace-packages=true
|
prefer-workspace-packages=true
|
||||||
|
|||||||
@ -162,9 +162,9 @@ The review surface is the highest-value pilot because it is operator-critical an
|
|||||||
|
|
||||||
### Phase UI4 — App Shell And Navigation
|
### Phase UI4 — App Shell And Navigation
|
||||||
|
|
||||||
- [ ] Move reusable app-shell/sidebar/header patterns into common platform if they are product-agnostic.
|
- [x] Move reusable app-shell/sidebar/header patterns into common platform if they are product-agnostic. Common commit: `5e7b349a7cfabb69d3d44d86afac3b02742954cb`; NoteLett commit: `63211c0019956364d48342be534864e05ff7ce44`. Verification: common `pnpm --filter @bytelyst/ui run build`; NoteLett `pnpm --filter @notelett/web run typecheck`, `pnpm --filter @notelett/web run test`, `pnpm --filter @notelett/web exec playwright test e2e/release-flows.spec.ts --reporter=list --workers=1`, `pnpm --filter @notelett/web exec playwright test e2e/reviews-visual.spec.ts --reporter=list --workers=1`, targeted shell raw-control/global-class audit, and `git diff --check` passed.
|
||||||
- [ ] Keep NoteLett navigation labels, routes, and feature-flag gating local.
|
- [x] Keep NoteLett navigation labels, routes, and feature-flag gating local. Commit: `7063e59078fa14f2c61d0b9aa76387c7c1e313c3`. Verification: `pnpm --filter @notelett/web run typecheck`, boundary `rg` audit confirmed NoteLett routes/labels/`mcp_tools_enabled` live in `web/src/lib/navigation.ts` and not common platform shell primitives, `pnpm --filter @notelett/web exec playwright test e2e/reviews-visual.spec.ts --reporter=list --workers=1`, and `git diff --check` passed.
|
||||||
- [ ] Replace global sidebar/mobile overlay CSS with common shell primitives.
|
- [x] Replace global sidebar/mobile overlay CSS with common shell primitives. Commit: `db9b4557d840b31e8a66ff0d85c3d80f1336b19a`. Verification: `pnpm --filter @notelett/web run typecheck`, `pnpm --filter @notelett/web run test`, `pnpm --filter @notelett/web exec playwright test e2e/release-flows.spec.ts --reporter=list --workers=1`, `pnpm --filter @notelett/web exec playwright test e2e/reviews-visual.spec.ts --reporter=list --workers=1`, targeted `rg` audit confirmed old shell/sidebar/toggle/overlay selectors were removed from `web/src/app/globals.css` and shell components, and `git diff --check` passed.
|
||||||
- [ ] Verify mobile sidebar behavior, keyboard focus order, and no horizontal overflow.
|
- [ ] Verify mobile sidebar behavior, keyboard focus order, and no horizontal overflow.
|
||||||
|
|
||||||
### Phase UI5 — Forms, Modals, And Settings
|
### Phase UI5 — Forms, Modals, And Settings
|
||||||
|
|||||||
872
docs/UX_TESTING_SETUP_GUIDE.md
Normal file
872
docs/UX_TESTING_SETUP_GUIDE.md
Normal file
@ -0,0 +1,872 @@
|
|||||||
|
# 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`
|
||||||
|
|
||||||
|
```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,
|
||||||
|
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:
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* 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**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 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
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* 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
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
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
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 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
|
||||||
|
|
||||||
|
```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="notes-filter-btn">Filter</button>
|
||||||
|
<button className="task-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={<EditIcon />}
|
||||||
|
label="Edit note"
|
||||||
|
aria-label="Edit note"
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Status Indicators**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Status badges need descriptive labels
|
||||||
|
<NotelettStatusBadge
|
||||||
|
status="active"
|
||||||
|
aria-label="Status: Active"
|
||||||
|
>
|
||||||
|
Active
|
||||||
|
</NotelettStatusBadge>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Form Labels**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 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**
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* 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**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
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**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
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**
|
||||||
|
|
||||||
|
```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: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:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/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**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 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**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 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**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 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**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 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
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check for raw controls and direct imports
|
||||||
|
pnpm run audit:ui
|
||||||
|
|
||||||
|
# Strict audit (enforces token usage)
|
||||||
|
pnpm run audit:ui:strict
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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
|
||||||
|
./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
|
||||||
146
scripts/docker-prep-original.sh
Executable file
146
scripts/docker-prep-original.sh
Executable file
@ -0,0 +1,146 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Pack @bytelyst/* tarballs from the sibling common-plat repo for
|
||||||
|
# self-contained Docker builds that don't need the Gitea npm registry.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./scripts/docker-prep.sh # pack tarballs + rewrite package.json
|
||||||
|
# ./scripts/docker-prep.sh --restore # undo rewrite
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
COMMON_PLAT="${COMMON_PLAT:-${REPO_DIR}/../learning_ai/learning_ai_common_plat}"
|
||||||
|
if [[ ! -d "$COMMON_PLAT" && -d "${REPO_DIR}/../learning_ai_common_plat" ]]; then
|
||||||
|
COMMON_PLAT="${REPO_DIR}/../learning_ai_common_plat"
|
||||||
|
fi
|
||||||
|
|
||||||
|
TARBALL_DIR="${REPO_DIR}/.docker-deps"
|
||||||
|
|
||||||
|
# ── Restore mode ───────────────────────────────────────────────────
|
||||||
|
if [[ "${1:-}" == "--restore" ]]; then
|
||||||
|
echo "Restoring original package.json files..."
|
||||||
|
for bak in $(find "$REPO_DIR" -name "package.json.bak" -not -path "*/node_modules/*"); do
|
||||||
|
mv "$bak" "${bak%.bak}"
|
||||||
|
echo " Restored ${bak%.bak}"
|
||||||
|
done
|
||||||
|
rm -rf "$TARBALL_DIR"
|
||||||
|
echo "Done."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Pack mode ──────────────────────────────────────────────────────
|
||||||
|
if [[ ! -d "$COMMON_PLAT" ]]; then
|
||||||
|
echo "Common platform checkout not found: $COMMON_PLAT" >&2
|
||||||
|
echo "Set COMMON_PLAT=/path/to/learning_ai_common_plat or place it at ../learning_ai/learning_ai_common_plat." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "=== docker-prep: packing @bytelyst/* tarballs ==="
|
||||||
|
|
||||||
|
rm -rf "$TARBALL_DIR"
|
||||||
|
mkdir -p "$TARBALL_DIR"
|
||||||
|
|
||||||
|
# Build all packages first (--filter limits to packages/, skips services/)
|
||||||
|
echo "Building @bytelyst/* packages..."
|
||||||
|
(cd "$COMMON_PLAT" && pnpm -r --filter './packages/*' build)
|
||||||
|
|
||||||
|
# Pack each package and build a mapping of name → tarball filename
|
||||||
|
# (uses a temp file instead of associative array for bash 3.2 compat)
|
||||||
|
TARBALL_MAP_FILE=$(mktemp)
|
||||||
|
trap 'rm -f "$TARBALL_MAP_FILE"' EXIT
|
||||||
|
|
||||||
|
for pkg_dir in "$COMMON_PLAT"/packages/*/; do
|
||||||
|
pkg_name=$(node -p "require('${pkg_dir}package.json').name" 2>/dev/null || true)
|
||||||
|
if [[ -z "$pkg_name" ]]; then continue; fi
|
||||||
|
|
||||||
|
echo " Packing $pkg_name..."
|
||||||
|
tarball=$(cd "$pkg_dir" && pnpm pack --pack-destination "$TARBALL_DIR" 2>/dev/null | tail -1)
|
||||||
|
filename=$(basename "$tarball")
|
||||||
|
echo "${pkg_name}=${filename}" >> "$TARBALL_MAP_FILE"
|
||||||
|
echo " -> $filename"
|
||||||
|
done
|
||||||
|
|
||||||
|
# ── Rewrite package.json files ─────────────────────────────────────
|
||||||
|
echo ""
|
||||||
|
echo "Rewriting package.json @bytelyst/* refs to .docker-deps/ tarballs..."
|
||||||
|
|
||||||
|
rewrite_package_json() {
|
||||||
|
local pkg_file="$1"
|
||||||
|
local rel_prefix="$2" # relative path from package.json dir to repo root
|
||||||
|
|
||||||
|
if [[ ! -f "$pkg_file" ]]; then return; fi
|
||||||
|
|
||||||
|
# Backup
|
||||||
|
cp "$pkg_file" "${pkg_file}.bak"
|
||||||
|
|
||||||
|
local tmp="${pkg_file}.tmp"
|
||||||
|
cp "$pkg_file" "$tmp"
|
||||||
|
|
||||||
|
while IFS='=' read -r pkg_name tarball; do
|
||||||
|
[[ -z "$pkg_name" ]] && continue
|
||||||
|
node -e "
|
||||||
|
const fs = require('fs');
|
||||||
|
const file = process.argv[1];
|
||||||
|
const pkgName = process.argv[2];
|
||||||
|
const replacement = process.argv[3];
|
||||||
|
const p = JSON.parse(fs.readFileSync(file, 'utf8'));
|
||||||
|
for (const section of ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']) {
|
||||||
|
if (p[section] && Object.prototype.hasOwnProperty.call(p[section], pkgName)) {
|
||||||
|
p[section][pkgName] = replacement;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fs.writeFileSync(file, JSON.stringify(p, null, 2) + '\n');
|
||||||
|
" "$tmp" "$pkg_name" "file:${rel_prefix}.docker-deps/${tarball}"
|
||||||
|
done < "$TARBALL_MAP_FILE"
|
||||||
|
|
||||||
|
mv "$tmp" "$pkg_file"
|
||||||
|
echo " Rewrote $pkg_file"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Backend package.json
|
||||||
|
rewrite_package_json "${REPO_DIR}/backend/package.json" "../"
|
||||||
|
|
||||||
|
# Web package.json
|
||||||
|
rewrite_package_json "${REPO_DIR}/web/package.json" "../"
|
||||||
|
|
||||||
|
# ── Inject pnpm.overrides for transitive @bytelyst/* deps ─────────
|
||||||
|
# Tarball packages may depend on other @bytelyst/* packages (e.g.
|
||||||
|
# @bytelyst/fastify-core → @bytelyst/errors). Without overrides, pnpm
|
||||||
|
# tries to fetch them from the npm registry which fails.
|
||||||
|
inject_overrides() {
|
||||||
|
local pkg_file="$1"
|
||||||
|
local rel_prefix="$2"
|
||||||
|
|
||||||
|
if [[ ! -f "$pkg_file" ]]; then return; fi
|
||||||
|
|
||||||
|
local overrides=""
|
||||||
|
while IFS='=' read -r pkg_name tarball; do
|
||||||
|
[[ -z "$pkg_name" ]] && continue
|
||||||
|
if [[ -n "$overrides" ]]; then overrides="$overrides, "; fi
|
||||||
|
overrides="$overrides\"${pkg_name}\": \"file:${rel_prefix}.docker-deps/${tarball}\""
|
||||||
|
done < "$TARBALL_MAP_FILE"
|
||||||
|
|
||||||
|
if [[ -n "$overrides" ]]; then
|
||||||
|
node -e "
|
||||||
|
const fs = require('fs');
|
||||||
|
const p = JSON.parse(fs.readFileSync('${pkg_file}', 'utf8'));
|
||||||
|
p.pnpm = p.pnpm || {};
|
||||||
|
p.pnpm.overrides = { ...(p.pnpm.overrides || {}), ...JSON.parse('{${overrides}}') };
|
||||||
|
fs.writeFileSync('${pkg_file}', JSON.stringify(p, null, 2) + '\n');
|
||||||
|
"
|
||||||
|
echo " Injected pnpm.overrides into $pkg_file"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
inject_overrides "${REPO_DIR}/backend/package.json" "../"
|
||||||
|
inject_overrides "${REPO_DIR}/web/package.json" "../"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Done. Tarballs in $TARBALL_DIR"
|
||||||
|
echo ""
|
||||||
|
echo "To build Docker images:"
|
||||||
|
echo " docker compose build"
|
||||||
|
echo ""
|
||||||
|
echo "To restore after build:"
|
||||||
|
echo " ./scripts/docker-prep.sh --restore"
|
||||||
@ -1,10 +1,17 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Pack @bytelyst/* tarballs from the sibling common-plat repo for
|
# Optimized docker-prep.sh with 6 optimizations:
|
||||||
# self-contained Docker builds that don't need the Gitea npm registry.
|
# 1. Git-based incremental builds (only rebuild changed packages)
|
||||||
|
# 2. Hash-based caching (content-addressable cache)
|
||||||
|
# 3. Parallel builds (concurrent package building)
|
||||||
|
# 4. Persistent tarball cache (survives git clean)
|
||||||
|
# 5. Smart manifest tracking (track what's been built)
|
||||||
|
# 6. Docker BuildKit integration hints
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# ./scripts/docker-prep.sh # pack tarballs + rewrite package.json
|
# ./scripts/docker-prep-optimized.sh # pack tarballs + rewrite package.json
|
||||||
# ./scripts/docker-prep.sh --restore # undo rewrite
|
# ./scripts/docker-prep-optimized.sh --restore # undo rewrite
|
||||||
|
# ./scripts/docker-prep-optimized.sh --clean # clear cache
|
||||||
|
# ./scripts/docker-prep-optimized.sh --force # force full rebuild
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
@ -16,9 +23,25 @@ if [[ ! -d "$COMMON_PLAT" && -d "${REPO_DIR}/../learning_ai_common_plat" ]]; the
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
TARBALL_DIR="${REPO_DIR}/.docker-deps"
|
TARBALL_DIR="${REPO_DIR}/.docker-deps"
|
||||||
|
# Shared cache location - can be overridden with BYTELYST_CACHE_DIR env var
|
||||||
|
CACHE_DIR="${BYTELYST_CACHE_DIR:-${HOME}/.cache/bytelyst-packages}"
|
||||||
|
MANIFEST_FILE="${CACHE_DIR}/.manifest"
|
||||||
|
|
||||||
# ── Restore mode ───────────────────────────────────────────────────
|
# ── Parse arguments ───────────────────────────────────────────────────
|
||||||
if [[ "${1:-}" == "--restore" ]]; then
|
MODE="pack"
|
||||||
|
FORCE_REBUILD=false
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--restore) MODE="restore"; shift ;;
|
||||||
|
--clean) MODE="clean"; shift ;;
|
||||||
|
--force) FORCE_REBUILD=true; shift ;;
|
||||||
|
*) echo "Unknown option: $1" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# ── Restore mode ─────────────────────────────────────────────────────
|
||||||
|
if [[ "$MODE" == "restore" ]]; then
|
||||||
echo "Restoring original package.json files..."
|
echo "Restoring original package.json files..."
|
||||||
for bak in $(find "$REPO_DIR" -name "package.json.bak" -not -path "*/node_modules/*"); do
|
for bak in $(find "$REPO_DIR" -name "package.json.bak" -not -path "*/node_modules/*"); do
|
||||||
mv "$bak" "${bak%.bak}"
|
mv "$bak" "${bak%.bak}"
|
||||||
@ -29,45 +52,189 @@ if [[ "${1:-}" == "--restore" ]]; then
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ── Pack mode ──────────────────────────────────────────────────────
|
# ── Clean mode ───────────────────────────────────────────────────────
|
||||||
|
if [[ "$MODE" == "clean" ]]; then
|
||||||
|
echo "Cleaning cache and tarball directory..."
|
||||||
|
rm -rf "$CACHE_DIR" "$TARBALL_DIR"
|
||||||
|
echo "Done."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Validation ───────────────────────────────────────────────────────
|
||||||
if [[ ! -d "$COMMON_PLAT" ]]; then
|
if [[ ! -d "$COMMON_PLAT" ]]; then
|
||||||
echo "Common platform checkout not found: $COMMON_PLAT" >&2
|
echo "Common platform checkout not found: $COMMON_PLAT" >&2
|
||||||
echo "Set COMMON_PLAT=/path/to/learning_ai_common_plat or place it at ../learning_ai/learning_ai_common_plat." >&2
|
echo "Set COMMON_PLAT=/path/to/learning_ai_common_plat or place it at ../learning_ai/learning_ai_common_plat." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "=== docker-prep: packing @bytelyst/* tarballs ==="
|
# ── Initialize directories ───────────────────────────────────────────
|
||||||
|
mkdir -p "$TARBALL_DIR" "$CACHE_DIR"
|
||||||
|
|
||||||
rm -rf "$TARBALL_DIR"
|
# ── Load manifest if exists ───────────────────────────────────────────
|
||||||
mkdir -p "$TARBALL_DIR"
|
# Simple file-based manifest to avoid bash associative array issues
|
||||||
|
|
||||||
# Build all packages first (--filter limits to packages/, skips services/)
|
# ── Determine which packages to rebuild ───────────────────────────────
|
||||||
echo "Building @bytelyst/* packages..."
|
if [[ "$FORCE_REBUILD" == true ]]; then
|
||||||
(cd "$COMMON_PLAT" && pnpm -r --filter './packages/*' build)
|
echo "Force rebuild: building all packages"
|
||||||
|
BUILD_ALL=true
|
||||||
# Pack each package and build a mapping of name → tarball filename
|
else
|
||||||
# (uses a temp file instead of associative array for bash 3.2 compat)
|
BUILD_ALL=false
|
||||||
TARBALL_MAP_FILE=$(mktemp)
|
# Git-based incremental build: check which packages changed
|
||||||
trap 'rm -f "$TARBALL_MAP_FILE"' EXIT
|
cd "$COMMON_PLAT"
|
||||||
|
if git rev-parse --git-dir > /dev/null 2>&1; then
|
||||||
|
# Get packages changed since last commit
|
||||||
|
CHANGED_PACKAGES=$(git diff --name-only HEAD~1 HEAD 2>/dev/null | grep '^packages/' | cut -d'/' -f2 | sort -u || echo "")
|
||||||
|
|
||||||
|
# If no git history or no changes, check against manifest
|
||||||
|
if [[ -z "$CHANGED_PACKAGES" ]] && [[ -f "$MANIFEST_FILE" ]]; then
|
||||||
|
echo "No git changes detected, checking cache validity..."
|
||||||
|
CHANGED_PACKAGES=""
|
||||||
for pkg_dir in "$COMMON_PLAT"/packages/*/; do
|
for pkg_dir in "$COMMON_PLAT"/packages/*/; do
|
||||||
pkg_name=$(node -p "require('${pkg_dir}package.json').name" 2>/dev/null || true)
|
pkg_name=$(node -p "require('${pkg_dir}package.json').name" 2>/dev/null || true)
|
||||||
if [[ -z "$pkg_name" ]]; then continue; fi
|
if [[ -z "$pkg_name" ]]; then continue; fi
|
||||||
|
|
||||||
echo " Packing $pkg_name..."
|
# Hash package.json + source files
|
||||||
tarball=$(cd "$pkg_dir" && pnpm pack --pack-destination "$TARBALL_DIR" 2>/dev/null | tail -1)
|
HASH=$(find "$pkg_dir" -name "*.ts" -o -name "*.json" -o -name "*.js" 2>/dev/null | xargs cat 2>/dev/null | sha256sum | cut -d' ' -f1 || echo "")
|
||||||
filename=$(basename "$tarball")
|
|
||||||
echo "${pkg_name}=${filename}" >> "$TARBALL_MAP_FILE"
|
# Check manifest for existing hash
|
||||||
echo " -> $filename"
|
CACHED_HASH=$(grep "^${pkg_name}=" "$MANIFEST_FILE" 2>/dev/null | cut -d'=' -f2 || echo "")
|
||||||
|
if [[ -n "$HASH" ]] && [[ "$CACHED_HASH" != "$HASH" ]]; then
|
||||||
|
pkg_basename=$(basename "$pkg_dir")
|
||||||
|
CHANGED_PACKAGES="$CHANGED_PACKAGES $pkg_basename"
|
||||||
|
fi
|
||||||
done
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# If still no changes detected but cache is empty, build all
|
||||||
|
if [[ -z "$CHANGED_PACKAGES" ]] && [[ -z "$(ls -A $CACHE_DIR 2>/dev/null)" ]]; then
|
||||||
|
echo "Cache empty, building all packages..."
|
||||||
|
BUILD_ALL=true
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Not a git repo or no history, building all packages..."
|
||||||
|
BUILD_ALL=true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd "$REPO_DIR"
|
||||||
|
|
||||||
|
# ── Build packages with caching ────────────────────────────────────────
|
||||||
|
echo "=== docker-prep-optimized: packing @bytelyst/* tarballs ==="
|
||||||
|
|
||||||
|
rm -rf "$TARBALL_DIR"
|
||||||
|
mkdir -p "$TARBALL_DIR"
|
||||||
|
|
||||||
|
# Function to build a single package
|
||||||
|
build_package() {
|
||||||
|
local pkg_dir=$1
|
||||||
|
local force_build=${2:-false}
|
||||||
|
|
||||||
|
if [[ ! -d "$pkg_dir" ]]; then
|
||||||
|
echo " ✗ Package directory not found: $pkg_dir" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
pkg_name=$(node -p "require('${pkg_dir}package.json').name" 2>/dev/null || true)
|
||||||
|
if [[ -z "$pkg_name" ]]; then
|
||||||
|
echo " ✗ Could not read package name from: $pkg_dir" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
pkg_version=$(node -p "require('${pkg_dir}package.json').version" 2>/dev/null || echo "0.0.0")
|
||||||
|
|
||||||
|
# Hash package.json + source files
|
||||||
|
HASH=$(find "$pkg_dir" -name "*.ts" -o -name "*.json" -o -name "*.js" 2>/dev/null | xargs cat 2>/dev/null | sha256sum | cut -d' ' -f1 || echo "unknown")
|
||||||
|
# Sanitize package name for filename (replace @ and / with -)
|
||||||
|
SAFE_PKG_NAME=$(echo "$pkg_name" | sed 's/@//g' | sed 's/\//_/g')
|
||||||
|
CACHE_FILE="$CACHE_DIR/${SAFE_PKG_NAME}-${pkg_version}.tgz"
|
||||||
|
|
||||||
|
# Check cache
|
||||||
|
if [[ -f "$CACHE_FILE" ]] && [[ "$force_build" != true ]] && [[ "$FORCE_REBUILD" != true ]]; then
|
||||||
|
echo " ✓ Cache hit: $pkg_name"
|
||||||
|
cp "$CACHE_FILE" "$TARBALL_DIR/"
|
||||||
|
echo "${pkg_name}=${HASH}" >> "$TARBALL_DIR/.manifest.tmp"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Build package
|
||||||
|
echo " → Building: $pkg_name"
|
||||||
|
(cd "$pkg_dir" && pnpm build > /dev/null 2>&1)
|
||||||
|
|
||||||
|
# Pack to cache and tarball dir
|
||||||
|
tarball=$(cd "$pkg_dir" && pnpm pack --pack-destination "$TARBALL_DIR" 2>/dev/null | tail -1)
|
||||||
|
if [[ -z "$tarball" ]]; then
|
||||||
|
echo " ✗ Failed to pack: $pkg_name" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Copy to cache
|
||||||
|
cp "$TARBALL_DIR/$(basename $tarball)" "$CACHE_FILE"
|
||||||
|
echo "${pkg_name}=${HASH}" >> "$TARBALL_DIR/.manifest.tmp"
|
||||||
|
|
||||||
|
echo " → $(basename $tarball)"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Build packages
|
||||||
|
if [[ "$BUILD_ALL" == true ]]; then
|
||||||
|
echo "Building all packages..."
|
||||||
|
for pkg_dir in "$COMMON_PLAT"/packages/*/; do
|
||||||
|
build_package "$pkg_dir" || true
|
||||||
|
done
|
||||||
|
else
|
||||||
|
echo "Building changed packages: $CHANGED_PACKAGES"
|
||||||
|
for pkg in $CHANGED_PACKAGES; do
|
||||||
|
pkg_dir="$COMMON_PLAT/packages/$pkg"
|
||||||
|
if [[ -d "$pkg_dir" ]]; then
|
||||||
|
build_package "$pkg_dir" || true
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Copy unchanged packages from cache
|
||||||
|
if [[ "$FORCE_REBUILD" != true ]]; then
|
||||||
|
echo ""
|
||||||
|
echo "Copying unchanged packages from cache..."
|
||||||
|
for pkg_dir in "$COMMON_PLAT"/packages/*/; do
|
||||||
|
pkg_name=$(node -p "require('${pkg_dir}package.json').name" 2>/dev/null || true)
|
||||||
|
if [[ -z "$pkg_name" ]]; then continue; fi
|
||||||
|
|
||||||
|
pkg_version=$(node -p "require('${pkg_dir}package.json').version" 2>/dev/null || echo "0.0.0")
|
||||||
|
# Sanitize package name for filename (replace @ and / with -)
|
||||||
|
SAFE_PKG_NAME=$(echo "$pkg_name" | sed 's/@//g' | sed 's/\//_/g')
|
||||||
|
CACHE_FILE="$CACHE_DIR/${SAFE_PKG_NAME}-${pkg_version}.tgz"
|
||||||
|
|
||||||
|
if [[ -f "$CACHE_FILE" ]] && [[ ! -f "$TARBALL_DIR/$(basename $CACHE_FILE)" ]]; then
|
||||||
|
echo " ✓ Cached: $pkg_name"
|
||||||
|
cp "$CACHE_FILE" "$TARBALL_DIR/"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Update manifest
|
||||||
|
if [[ -f "$TARBALL_DIR/.manifest.tmp" ]]; then
|
||||||
|
mv "$TARBALL_DIR/.manifest.tmp" "$MANIFEST_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
# ── Rewrite package.json files ─────────────────────────────────────
|
# ── Rewrite package.json files ─────────────────────────────────────
|
||||||
echo ""
|
echo ""
|
||||||
echo "Rewriting package.json @bytelyst/* refs to .docker-deps/ tarballs..."
|
echo "Rewriting package.json @bytelyst/* refs to .docker-deps/ tarballs..."
|
||||||
|
|
||||||
|
# Build tarball mapping
|
||||||
|
TARBALL_MAP_FILE=$(mktemp)
|
||||||
|
trap 'rm -f "$TARBALL_MAP_FILE"' EXIT
|
||||||
|
|
||||||
|
for tarball in "$TARBALL_DIR"/*.tgz; do
|
||||||
|
[[ -f "$tarball" ]] || continue
|
||||||
|
filename=$(basename "$tarball")
|
||||||
|
# Extract package name from tarball filename
|
||||||
|
pkg_name=$(echo "$filename" | sed 's/-[0-9].*//' | sed 's/^bytelyst-/@bytelyst\//')
|
||||||
|
echo "${pkg_name}=${filename}" >> "$TARBALL_MAP_FILE"
|
||||||
|
done
|
||||||
|
|
||||||
rewrite_package_json() {
|
rewrite_package_json() {
|
||||||
local pkg_file="$1"
|
local pkg_file="$1"
|
||||||
local rel_prefix="$2" # relative path from package.json dir to repo root
|
local rel_prefix="$2"
|
||||||
|
|
||||||
if [[ ! -f "$pkg_file" ]]; then return; fi
|
if [[ ! -f "$pkg_file" ]]; then return; fi
|
||||||
|
|
||||||
@ -105,9 +272,6 @@ rewrite_package_json "${REPO_DIR}/backend/package.json" "../"
|
|||||||
rewrite_package_json "${REPO_DIR}/web/package.json" "../"
|
rewrite_package_json "${REPO_DIR}/web/package.json" "../"
|
||||||
|
|
||||||
# ── Inject pnpm.overrides for transitive @bytelyst/* deps ─────────
|
# ── Inject pnpm.overrides for transitive @bytelyst/* deps ─────────
|
||||||
# Tarball packages may depend on other @bytelyst/* packages (e.g.
|
|
||||||
# @bytelyst/fastify-core → @bytelyst/errors). Without overrides, pnpm
|
|
||||||
# tries to fetch them from the npm registry which fails.
|
|
||||||
inject_overrides() {
|
inject_overrides() {
|
||||||
local pkg_file="$1"
|
local pkg_file="$1"
|
||||||
local rel_prefix="$2"
|
local rel_prefix="$2"
|
||||||
@ -136,11 +300,17 @@ inject_overrides() {
|
|||||||
inject_overrides "${REPO_DIR}/backend/package.json" "../"
|
inject_overrides "${REPO_DIR}/backend/package.json" "../"
|
||||||
inject_overrides "${REPO_DIR}/web/package.json" "../"
|
inject_overrides "${REPO_DIR}/web/package.json" "../"
|
||||||
|
|
||||||
|
# ── Summary ───────────────────────────────────────────────────────────
|
||||||
echo ""
|
echo ""
|
||||||
echo "Done. Tarballs in $TARBALL_DIR"
|
echo "✓ Done. $(ls $TARBALL_DIR/*.tgz 2>/dev/null | wc -l) tarballs in $TARBALL_DIR"
|
||||||
|
echo "✓ Cache size: $(du -sh $CACHE_DIR 2>/dev/null | cut -f1)"
|
||||||
echo ""
|
echo ""
|
||||||
echo "To build Docker images:"
|
echo "To build Docker images:"
|
||||||
echo " docker compose build"
|
echo " docker compose build"
|
||||||
echo ""
|
echo ""
|
||||||
echo "To restore after build:"
|
echo "To restore after build:"
|
||||||
echo " ./scripts/docker-prep.sh --restore"
|
echo " ./scripts/docker-prep-optimized.sh --restore"
|
||||||
|
echo ""
|
||||||
|
echo "To clear cache and force full rebuild:"
|
||||||
|
echo " ./scripts/docker-prep-optimized.sh --clean"
|
||||||
|
echo " ./scripts/docker-prep-optimized.sh --force"
|
||||||
@ -80,27 +80,12 @@ button {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-shell {
|
|
||||||
min-height: 100vh;
|
|
||||||
padding-left: 280px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar {
|
|
||||||
border-right: 1px solid var(--nl-border-default);
|
|
||||||
background: var(--nl-surface-sidebar);
|
|
||||||
backdrop-filter: blur(16px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-panel {
|
.main-panel {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
padding: var(--nl-space-8);
|
padding: var(--nl-space-8);
|
||||||
padding-left: calc(var(--bl-app-sidebar-width, 280px) + var(--nl-space-8));
|
padding-left: calc(var(--bl-app-sidebar-width, 280px) + var(--nl-space-8));
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-shell > .sidebar {
|
|
||||||
width: 280px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.surface-card {
|
.surface-card {
|
||||||
border: 1px solid var(--nl-border-default);
|
border: 1px solid var(--nl-border-default);
|
||||||
border-radius: var(--nl-radius-md);
|
border-radius: var(--nl-radius-md);
|
||||||
@ -150,15 +135,6 @@ button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 980px) {
|
@media (max-width: 980px) {
|
||||||
.app-shell {
|
|
||||||
padding-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar {
|
|
||||||
border-right: 0;
|
|
||||||
border-bottom: 1px solid var(--nl-border-default);
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-panel {
|
.main-panel {
|
||||||
padding: var(--nl-space-5);
|
padding: var(--nl-space-5);
|
||||||
}
|
}
|
||||||
@ -222,36 +198,6 @@ button:active:not(:disabled), [role="button"]:active:not(:disabled) {
|
|||||||
border: 2px solid var(--nl-accent-primary); border-radius: 8px; font-size: 14px; font-weight: 600; text-decoration: none;
|
border: 2px solid var(--nl-accent-primary); border-radius: 8px; font-size: 14px; font-weight: 600; text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Responsive sidebar */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.app-sidebar {
|
|
||||||
position: fixed !important;
|
|
||||||
left: -280px !important;
|
|
||||||
top: 0 !important;
|
|
||||||
z-index: 40;
|
|
||||||
width: 260px !important;
|
|
||||||
height: 100vh !important;
|
|
||||||
transition: left 0.2s ease;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
.app-sidebar.open { left: 0 !important; }
|
|
||||||
.sidebar-overlay { display: none; position: fixed; inset: 0; z-index: 39; background: var(--nl-overlay-scrim); }
|
|
||||||
.sidebar-overlay.open { display: block; }
|
|
||||||
.sidebar-toggle {
|
|
||||||
display: flex; align-items: center; justify-content: center;
|
|
||||||
position: fixed; top: 12px; left: 12px; z-index: 38;
|
|
||||||
width: 40px; height: 40px; border-radius: 8px;
|
|
||||||
border: 1px solid var(--nl-border-default);
|
|
||||||
background: var(--nl-bg-elevated);
|
|
||||||
color: var(--nl-text-primary);
|
|
||||||
cursor: pointer; font-size: 20px; line-height: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@media (min-width: 769px) {
|
|
||||||
.sidebar-toggle { display: none !important; }
|
|
||||||
.sidebar-overlay { display: none !important; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Respect user motion preference */
|
/* Respect user motion preference */
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
*, *::before, *::after {
|
*, *::before, *::after {
|
||||||
@ -261,75 +207,3 @@ button:active:not(:disabled), [role="button"]:active:not(:disabled) {
|
|||||||
scroll-behavior: auto !important;
|
scroll-behavior: auto !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Responsive Sidebar ──────────────────────────────────────── */
|
|
||||||
|
|
||||||
.app-layout {
|
|
||||||
display: flex;
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-sidebar {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
height: 100vh;
|
|
||||||
z-index: 40;
|
|
||||||
transition: transform 0.22s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-sidebar-overlay {
|
|
||||||
display: none;
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
background: var(--nl-overlay-scrim);
|
|
||||||
z-index: 39;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-main {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.app-sidebar {
|
|
||||||
transform: translateX(-100%);
|
|
||||||
}
|
|
||||||
.app-sidebar[data-open="true"] {
|
|
||||||
transform: translateX(0);
|
|
||||||
}
|
|
||||||
.app-sidebar-overlay[data-open="true"] {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
.app-main {
|
|
||||||
margin-left: 0 !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 769px) {
|
|
||||||
.app-sidebar {
|
|
||||||
transform: translateX(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-menu-btn {
|
|
||||||
display: none;
|
|
||||||
position: fixed;
|
|
||||||
top: 12px;
|
|
||||||
left: 12px;
|
|
||||||
z-index: 38;
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
border-radius: 8px;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 20px;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.mobile-menu-btn {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import { House, Search, Settings, Sparkles, FolderKanban, ShieldCheck, MessageCircle, Brain } from "lucide-react";
|
import { Sparkles } from "lucide-react";
|
||||||
import { PRODUCT_NAME } from "@/lib/product-config";
|
import { PRODUCT_NAME } from "@/lib/product-config";
|
||||||
import { isFeatureEnabled } from "@/lib/feature-flags";
|
import { isFeatureEnabled } from "@/lib/feature-flags";
|
||||||
|
import { NOTELETT_NAV_ITEMS } from "@/lib/navigation";
|
||||||
import {
|
import {
|
||||||
AppShellNav,
|
AppShellNav,
|
||||||
AppShellNavItem,
|
AppShellNavItem,
|
||||||
@ -12,22 +13,11 @@ import {
|
|||||||
Panel,
|
Panel,
|
||||||
} from "@/components/ui/Primitives";
|
} from "@/components/ui/Primitives";
|
||||||
|
|
||||||
const navItems: { href: string; label: string; icon: typeof House; flag?: string }[] = [
|
|
||||||
{ href: "/dashboard", label: "Dashboard", icon: House },
|
|
||||||
{ href: "/workspaces", label: "Workspaces", icon: FolderKanban },
|
|
||||||
{ href: "/reviews", label: "Reviews", icon: ShieldCheck, flag: "mcp_tools_enabled" },
|
|
||||||
{ href: "/prompts", label: "Prompts", icon: Sparkles },
|
|
||||||
{ href: "/search", label: "Search", icon: Search },
|
|
||||||
{ href: "/chat", label: "Workspace chat", icon: MessageCircle },
|
|
||||||
{ href: "/palace", label: "Palace", icon: Brain },
|
|
||||||
{ href: "/settings", label: "Settings", icon: Settings },
|
|
||||||
];
|
|
||||||
|
|
||||||
export function Sidebar({ open }: { open?: boolean }) {
|
export function Sidebar({ open }: { open?: boolean }) {
|
||||||
const pathname = usePathname() ?? "";
|
const pathname = usePathname() ?? "";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppShellSidebar open={open ?? false} width={280} className="sidebar p-[var(--nl-space-6)]">
|
<AppShellSidebar open={open ?? false} width={280} className="p-[var(--nl-space-6)]">
|
||||||
<div style={{ display: "grid", gap: "var(--nl-space-6)" }}>
|
<div style={{ display: "grid", gap: "var(--nl-space-6)" }}>
|
||||||
<Panel as="div" style={{ display: "grid", gap: "var(--nl-space-3)" }}>
|
<Panel as="div" style={{ display: "grid", gap: "var(--nl-space-3)" }}>
|
||||||
<Badge style={{ width: "fit-content" }}>
|
<Badge style={{ width: "fit-content" }}>
|
||||||
@ -43,7 +33,7 @@ export function Sidebar({ open }: { open?: boolean }) {
|
|||||||
</Panel>
|
</Panel>
|
||||||
|
|
||||||
<AppShellNav>
|
<AppShellNav>
|
||||||
{navItems.filter((item) => !item.flag || isFeatureEnabled(item.flag)).map((item) => {
|
{NOTELETT_NAV_ITEMS.filter((item) => !item.flag || isFeatureEnabled(item.flag)).map((item) => {
|
||||||
const Icon = item.icon;
|
const Icon = item.icon;
|
||||||
const active = pathname === item.href || pathname.startsWith(`${item.href}/`);
|
const active = pathname === item.href || pathname.startsWith(`${item.href}/`);
|
||||||
return (
|
return (
|
||||||
|
|||||||
21
web/src/lib/navigation.ts
Normal file
21
web/src/lib/navigation.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import type { LucideIcon } from "lucide-react";
|
||||||
|
import { Brain, FolderKanban, House, MessageCircle, Search, Settings, ShieldCheck, Sparkles } from "lucide-react";
|
||||||
|
|
||||||
|
export type NoteLettNavItem = {
|
||||||
|
href: string;
|
||||||
|
label: string;
|
||||||
|
icon: LucideIcon;
|
||||||
|
flag?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NOTELETT_NAV_ITEMS: NoteLettNavItem[] = [
|
||||||
|
{ href: "/dashboard", label: "Dashboard", icon: House },
|
||||||
|
{ href: "/workspaces", label: "Workspaces", icon: FolderKanban },
|
||||||
|
{ href: "/reviews", label: "Reviews", icon: ShieldCheck, flag: "mcp_tools_enabled" },
|
||||||
|
{ href: "/prompts", label: "Prompts", icon: Sparkles },
|
||||||
|
{ href: "/search", label: "Search", icon: Search },
|
||||||
|
{ href: "/chat", label: "Workspace chat", icon: MessageCircle },
|
||||||
|
{ href: "/palace", label: "Palace", icon: Brain },
|
||||||
|
{ href: "/settings", label: "Settings", icon: Settings },
|
||||||
|
];
|
||||||
|
|
||||||
Loading…
Reference in New Issue
Block a user