mirror of
https://github.com/The-Low-Code-Foundation/OpenNoodl.git
synced 2026-01-11 23:02:56 +01:00
Finished component sidebar updates, with one small bug remaining and documented
This commit is contained in:
@@ -0,0 +1,343 @@
|
||||
/* =============================================================================
|
||||
NOODL DESIGN SYSTEM - COLORS
|
||||
Minimal palette: Red + Black + White
|
||||
============================================================================= */
|
||||
|
||||
:root {
|
||||
/* ---------------------------------------------------------------------------
|
||||
BASE COLORS
|
||||
A deliberately minimal palette - one accent, pure neutrals
|
||||
--------------------------------------------------------------------------- */
|
||||
|
||||
/* Primary - Noodl Red */
|
||||
--base-color-red-100: #fef2f3;
|
||||
--base-color-red-200: #fde3e5;
|
||||
--base-color-red-300: #fbc5c9;
|
||||
--base-color-red-400: #f7969e;
|
||||
--base-color-red-500: #ef5662;
|
||||
--base-color-red-600: #d21f3c;
|
||||
--base-color-red-700: #b91830;
|
||||
--base-color-red-800: #9a1729;
|
||||
--base-color-red-900: #801827;
|
||||
--base-color-red-950: #460a11;
|
||||
|
||||
/* Neutrals - Pure black to white, no color tint */
|
||||
--base-color-neutral-0: #000000;
|
||||
--base-color-neutral-50: #0a0a0a;
|
||||
--base-color-neutral-100: #121212;
|
||||
--base-color-neutral-200: #1a1a1a;
|
||||
--base-color-neutral-300: #262626;
|
||||
--base-color-neutral-400: #333333;
|
||||
--base-color-neutral-500: #525252;
|
||||
--base-color-neutral-600: #737373;
|
||||
--base-color-neutral-700: #a3a3a3;
|
||||
--base-color-neutral-800: #d4d4d4;
|
||||
--base-color-neutral-900: #e5e5e5;
|
||||
--base-color-neutral-950: #f5f5f5;
|
||||
--base-color-neutral-1000: #ffffff;
|
||||
|
||||
/* Transparent variants */
|
||||
--base-color-black-transparent-90: rgba(0, 0, 0, 0.9);
|
||||
--base-color-black-transparent-80: rgba(0, 0, 0, 0.8);
|
||||
--base-color-black-transparent-50: rgba(0, 0, 0, 0.5);
|
||||
--base-color-white-transparent-10: rgba(255, 255, 255, 0.1);
|
||||
--base-color-white-transparent-15: rgba(255, 255, 255, 0.15);
|
||||
--base-color-white-transparent-50: rgba(255, 255, 255, 0.5);
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
SEMANTIC COLORS (Status indicators)
|
||||
--------------------------------------------------------------------------- */
|
||||
|
||||
/* Success - Keeping a green for semantic meaning */
|
||||
--base-color-success-100: #ecfdf5;
|
||||
--base-color-success-200: #a7f3d0;
|
||||
--base-color-success-300: #6ee7b7;
|
||||
--base-color-success-400: #34d399;
|
||||
--base-color-success-500: #10b981;
|
||||
--base-color-success-600: #059669;
|
||||
--base-color-success-700: #047857;
|
||||
--base-color-success-800: #065f46;
|
||||
--base-color-success-900: #064e3b;
|
||||
--base-color-success-1000: #022c22;
|
||||
|
||||
/* Error - Uses the brand red */
|
||||
--base-color-error-100: var(--base-color-red-100);
|
||||
--base-color-error-200: var(--base-color-red-200);
|
||||
--base-color-error-300: var(--base-color-red-300);
|
||||
--base-color-error-400: var(--base-color-red-400);
|
||||
--base-color-error-500: var(--base-color-red-500);
|
||||
--base-color-error-600: var(--base-color-red-600);
|
||||
--base-color-error-700: var(--base-color-red-700);
|
||||
--base-color-error-800: var(--base-color-red-800);
|
||||
--base-color-error-900: var(--base-color-red-900);
|
||||
--base-color-error-1000: var(--base-color-red-950);
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
NODE TYPE COLORS
|
||||
Subtle variations to distinguish node types on canvas
|
||||
Using desaturated colors so they don't compete with the red accent
|
||||
--------------------------------------------------------------------------- */
|
||||
|
||||
/* Node-Pink - For Custom/User nodes */
|
||||
--base-color-node-pink-100: #fdf2f8;
|
||||
--base-color-node-pink-200: #f5d0e5;
|
||||
--base-color-node-pink-300: #e8a8ca;
|
||||
--base-color-node-pink-400: #d87caa;
|
||||
--base-color-node-pink-500: #c2578a;
|
||||
--base-color-node-pink-600: #a63d6f;
|
||||
--base-color-node-pink-700: #862d56;
|
||||
--base-color-node-pink-800: #6b2445;
|
||||
--base-color-node-pink-900: #521c35;
|
||||
--base-color-node-pink-1000: #2d0e1c;
|
||||
|
||||
/* Node-Purple - For Component nodes */
|
||||
--base-color-node-purple-100: #f8f5fa;
|
||||
--base-color-node-purple-200: #e8dff0;
|
||||
--base-color-node-purple-300: #d4c4e3;
|
||||
--base-color-node-purple-400: #b8a0cf;
|
||||
--base-color-node-purple-500: #9a7bb8;
|
||||
--base-color-node-purple-600: #7d5a9e;
|
||||
--base-color-node-purple-700: #624382;
|
||||
--base-color-node-purple-800: #4b3366;
|
||||
--base-color-node-purple-900: #37264b;
|
||||
--base-color-node-purple-1000: #1e1429;
|
||||
|
||||
/* Node-Green - For Data nodes */
|
||||
--base-color-node-green-100: #f4f7f4;
|
||||
--base-color-node-green-200: #d8e5d8;
|
||||
--base-color-node-green-300: #b5cfb5;
|
||||
--base-color-node-green-400: #8eb58e;
|
||||
--base-color-node-green-500: #6a996a;
|
||||
--base-color-node-green-600: #4d7d4d;
|
||||
--base-color-node-green-700: #3a613a;
|
||||
--base-color-node-green-800: #2c4a2c;
|
||||
--base-color-node-green-900: #203520;
|
||||
--base-color-node-green-1000: #111c11;
|
||||
|
||||
/* Node-Gray - For Logic nodes */
|
||||
--base-color-node-grey-100: #f5f5f5;
|
||||
--base-color-node-grey-200: #e0e0e0;
|
||||
--base-color-node-grey-300: #c2c2c2;
|
||||
--base-color-node-grey-400: #9e9e9e;
|
||||
--base-color-node-grey-500: #757575;
|
||||
--base-color-node-grey-600: #5c5c5c;
|
||||
--base-color-node-grey-700: #454545;
|
||||
--base-color-node-grey-800: #333333;
|
||||
--base-color-node-grey-900: #212121;
|
||||
--base-color-node-grey-1000: #0d0d0d;
|
||||
|
||||
/* Node-Blue - For Visual nodes */
|
||||
--base-color-node-blue-100: #f4f6f8;
|
||||
--base-color-node-blue-200: #dce3eb;
|
||||
--base-color-node-blue-300: #bccad9;
|
||||
--base-color-node-blue-400: #96adc2;
|
||||
--base-color-node-blue-500: #7090a9;
|
||||
--base-color-node-blue-600: #53758f;
|
||||
--base-color-node-blue-700: #3e5a72;
|
||||
--base-color-node-blue-800: #2f4557;
|
||||
--base-color-node-blue-900: #22323f;
|
||||
--base-color-node-blue-1000: #121b22;
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
LEGACY ALIASES - For backwards compatibility
|
||||
--------------------------------------------------------------------------- */
|
||||
|
||||
/* Grey -> Neutral */
|
||||
--base-color-grey-100: var(--base-color-neutral-950);
|
||||
--base-color-grey-100-transparent: var(--base-color-white-transparent-10);
|
||||
--base-color-grey-200: var(--base-color-neutral-800);
|
||||
--base-color-grey-300: var(--base-color-neutral-700);
|
||||
--base-color-grey-400: var(--base-color-neutral-600);
|
||||
--base-color-grey-500: var(--base-color-neutral-500);
|
||||
--base-color-grey-600: var(--base-color-neutral-400);
|
||||
--base-color-grey-700: var(--base-color-neutral-300);
|
||||
--base-color-grey-800: var(--base-color-neutral-200);
|
||||
--base-color-grey-900: var(--base-color-neutral-100);
|
||||
--base-color-grey-1000: var(--base-color-neutral-50);
|
||||
--base-color-grey-1000-transparent: var(--base-color-black-transparent-80);
|
||||
--base-color-grey-1000-transparent-2: var(--base-color-black-transparent-50);
|
||||
|
||||
/* Teal -> Neutral (secondary is now white/gray) */
|
||||
--base-color-teal-100: var(--base-color-neutral-1000);
|
||||
--base-color-teal-200: var(--base-color-neutral-900);
|
||||
--base-color-teal-300: var(--base-color-neutral-800);
|
||||
--base-color-teal-400: var(--base-color-neutral-700);
|
||||
--base-color-teal-500: var(--base-color-neutral-600);
|
||||
--base-color-teal-600: var(--base-color-neutral-500);
|
||||
--base-color-teal-700: var(--base-color-neutral-400);
|
||||
--base-color-teal-800: var(--base-color-neutral-300);
|
||||
--base-color-teal-900: var(--base-color-neutral-200);
|
||||
--base-color-teal-1000: var(--base-color-neutral-100);
|
||||
|
||||
/* Yellow -> Red (primary is now red) */
|
||||
--base-color-yellow-100: var(--base-color-red-100);
|
||||
--base-color-yellow-200: var(--base-color-red-200);
|
||||
--base-color-yellow-300: var(--base-color-red-400);
|
||||
--base-color-yellow-400: var(--base-color-red-500);
|
||||
--base-color-yellow-500: var(--base-color-red-600);
|
||||
--base-color-yellow-600: var(--base-color-red-700);
|
||||
--base-color-yellow-700: var(--base-color-red-800);
|
||||
--base-color-yellow-800: var(--base-color-red-900);
|
||||
--base-color-yellow-900: var(--base-color-red-950);
|
||||
--base-color-yellow-1000: var(--base-color-red-950);
|
||||
}
|
||||
|
||||
|
||||
/* =============================================================================
|
||||
THEME COLOR TOKENS - USE THESE IN COMPONENTS
|
||||
============================================================================= */
|
||||
|
||||
:root {
|
||||
/* ---------------------------------------------------------------------------
|
||||
BACKGROUNDS
|
||||
Pure blacks with subtle elevation through lightness
|
||||
--------------------------------------------------------------------------- */
|
||||
--theme-color-bg-0: #000000;
|
||||
--theme-color-bg-1: var(--base-color-neutral-50);
|
||||
--theme-color-bg-1-transparent: var(--base-color-black-transparent-80);
|
||||
--theme-color-bg-1-transparent-2: var(--base-color-black-transparent-50);
|
||||
--theme-color-bg-2: var(--base-color-neutral-100);
|
||||
--theme-color-bg-3: var(--base-color-neutral-200);
|
||||
--theme-color-bg-4: var(--base-color-neutral-300);
|
||||
--theme-color-bg-5: var(--base-color-neutral-400);
|
||||
--theme-color-bg-hover: var(--base-color-white-transparent-10);
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
FOREGROUNDS
|
||||
Pure whites with subtle hierarchy
|
||||
--------------------------------------------------------------------------- */
|
||||
--theme-color-fg-highlight: #ffffff;
|
||||
--theme-color-fg-default-contrast: var(--base-color-neutral-900);
|
||||
--theme-color-fg-default: var(--base-color-neutral-800);
|
||||
--theme-color-fg-default-shy: var(--base-color-neutral-700);
|
||||
--theme-color-fg-muted: var(--base-color-neutral-600);
|
||||
--theme-color-fg-transparent: var(--base-color-white-transparent-15);
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
PRIMARY - Noodl Red
|
||||
The one accent color - used sparingly for maximum impact
|
||||
--------------------------------------------------------------------------- */
|
||||
--theme-color-primary: #d21f3c;
|
||||
--theme-color-primary-highlight: var(--base-color-red-500);
|
||||
--theme-color-primary-dim: var(--base-color-red-800);
|
||||
--theme-color-on-primary: #ffffff;
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
SECONDARY - White/Light
|
||||
For secondary actions, using white as the complement to red
|
||||
--------------------------------------------------------------------------- */
|
||||
--theme-color-secondary: #ffffff;
|
||||
--theme-color-secondary-dim: var(--base-color-neutral-700);
|
||||
--theme-color-secondary-highlight: #ffffff;
|
||||
--theme-color-secondary-bright: #ffffff;
|
||||
--theme-color-secondary-as-fg: var(--base-color-neutral-800);
|
||||
--theme-color-on-secondary: var(--base-color-neutral-100);
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
NODE COLORS
|
||||
Muted, desaturated to not compete with the red accent
|
||||
--------------------------------------------------------------------------- */
|
||||
|
||||
/* Data nodes - Muted Green */
|
||||
--theme-color-node-data-1: var(--base-color-node-green-700);
|
||||
--theme-color-node-data-2: var(--base-color-node-green-600);
|
||||
--theme-color-node-data-3: var(--base-color-node-green-500);
|
||||
--theme-color-node-data-dim: var(--base-color-node-green-900);
|
||||
|
||||
/* Visual nodes - Muted Blue */
|
||||
--theme-color-node-visual-1: var(--base-color-node-blue-700);
|
||||
--theme-color-node-visual-2: var(--base-color-node-blue-600);
|
||||
--theme-color-node-visual-2-highlight: var(--base-color-node-blue-500);
|
||||
--theme-color-node-visual-highlight: var(--base-color-node-blue-200);
|
||||
--theme-color-node-visual-default: var(--base-color-node-blue-300);
|
||||
--theme-color-node-visual-shy: var(--base-color-node-blue-400);
|
||||
--theme-color-node-visual-dim: var(--base-color-node-blue-900);
|
||||
|
||||
/* Custom nodes - Muted Pink */
|
||||
--theme-color-node-custom-1: var(--base-color-node-pink-700);
|
||||
--theme-color-node-custom-2: var(--base-color-node-pink-600);
|
||||
--theme-color-node-custom-dim: var(--base-color-node-pink-900);
|
||||
|
||||
/* Logic nodes - Gray */
|
||||
--theme-color-node-logic-1: var(--base-color-node-grey-700);
|
||||
--theme-color-node-logic-2: var(--base-color-node-grey-600);
|
||||
--theme-color-node-logic-dim: var(--base-color-node-grey-900);
|
||||
|
||||
/* Component nodes - Muted Purple */
|
||||
--theme-color-node-component-1: var(--base-color-node-purple-700);
|
||||
--theme-color-node-component-2: var(--base-color-node-purple-600);
|
||||
--theme-color-node-component-dim: var(--base-color-node-purple-900);
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
STATUS COLORS
|
||||
Success stays green, everything else maps to the palette
|
||||
--------------------------------------------------------------------------- */
|
||||
--theme-color-success: var(--base-color-success-400);
|
||||
--theme-color-success-dim: var(--base-color-success-600);
|
||||
--theme-color-success-bg: var(--base-color-success-900);
|
||||
|
||||
--theme-color-notice: var(--base-color-red-400);
|
||||
--theme-color-notice-dim: var(--base-color-red-600);
|
||||
--theme-color-notice-bg: var(--base-color-red-950);
|
||||
|
||||
--theme-color-danger: var(--base-color-red-500);
|
||||
--theme-color-danger-light: var(--base-color-red-400);
|
||||
--theme-color-danger-dim: var(--base-color-red-700);
|
||||
--theme-color-danger-bg: var(--base-color-red-950);
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
CONNECTION COLORS
|
||||
--------------------------------------------------------------------------- */
|
||||
--theme-color-signal: var(--base-color-red-500);
|
||||
--theme-color-data: var(--base-color-neutral-700);
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
BORDERS
|
||||
Subtle white borders for dark backgrounds
|
||||
--------------------------------------------------------------------------- */
|
||||
--theme-color-border-default: var(--base-color-neutral-300);
|
||||
--theme-color-border-subtle: var(--base-color-neutral-200);
|
||||
--theme-color-border-strong: var(--base-color-neutral-400);
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
FOCUS
|
||||
Red focus ring for accessibility
|
||||
--------------------------------------------------------------------------- */
|
||||
--theme-color-focus-ring: #d21f3c;
|
||||
--theme-color-focus-ring-offset: var(--base-color-neutral-50);
|
||||
}
|
||||
|
||||
|
||||
/* =============================================================================
|
||||
FUTURE: LIGHT THEME
|
||||
=============================================================================
|
||||
|
||||
.theme-light {
|
||||
--theme-color-bg-0: #ffffff;
|
||||
--theme-color-bg-1: var(--base-color-neutral-950);
|
||||
--theme-color-bg-1-transparent: rgba(255, 255, 255, 0.9);
|
||||
--theme-color-bg-1-transparent-2: rgba(255, 255, 255, 0.5);
|
||||
--theme-color-bg-2: #ffffff;
|
||||
--theme-color-bg-3: var(--base-color-neutral-900);
|
||||
--theme-color-bg-4: var(--base-color-neutral-800);
|
||||
--theme-color-bg-5: var(--base-color-neutral-700);
|
||||
--theme-color-bg-hover: rgba(0, 0, 0, 0.04);
|
||||
|
||||
--theme-color-fg-highlight: #000000;
|
||||
--theme-color-fg-default-contrast: var(--base-color-neutral-100);
|
||||
--theme-color-fg-default: var(--base-color-neutral-200);
|
||||
--theme-color-fg-default-shy: var(--base-color-neutral-400);
|
||||
--theme-color-fg-muted: var(--base-color-neutral-500);
|
||||
|
||||
--theme-color-primary: #d21f3c;
|
||||
--theme-color-on-primary: #ffffff;
|
||||
|
||||
--theme-color-secondary: var(--base-color-neutral-100);
|
||||
--theme-color-on-secondary: #ffffff;
|
||||
|
||||
--theme-color-border-default: var(--base-color-neutral-800);
|
||||
--theme-color-border-subtle: var(--base-color-neutral-900);
|
||||
--theme-color-border-strong: var(--base-color-neutral-700);
|
||||
}
|
||||
|
||||
*/
|
||||
@@ -0,0 +1,866 @@
|
||||
# Task: Noodl Design System Modernization
|
||||
|
||||
## Overview
|
||||
|
||||
Comprehensive overhaul of Noodl's visual design system to create a modern, clean, professional appearance. Moving from the dated 2015-era dark gray aesthetic to a contemporary design language inspired by tools like Linear, Raycast, and Figma.
|
||||
|
||||
**Primary Goals:**
|
||||
- Clean, modern color palette (Rose + Violet with Zinc neutrals)
|
||||
- Consistent token usage throughout the codebase
|
||||
- Foundation for future light/dark theme switching
|
||||
- Better visual hierarchy and spacing
|
||||
- Improved component aesthetics
|
||||
|
||||
**Brand Direction:**
|
||||
- Primary: Rose (`#f43f5e`) - Modern, bold, distinctive
|
||||
- Secondary: Violet (`#a78bfa`) - Complementary, contemporary
|
||||
- Neutrals: Zinc palette (clean grays, no brown/warm tints)
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Token Consolidation & Color Refresh
|
||||
|
||||
**Priority: CRITICAL**
|
||||
**Effort: 1-2 hours**
|
||||
**Risk: Low**
|
||||
|
||||
### Problem
|
||||
|
||||
The editor has duplicate color token files. The core-ui tokens are commented out and the editor uses its own copy:
|
||||
|
||||
```typescript
|
||||
// packages/noodl-editor/src/editor/index.ts
|
||||
//Design tokens for later
|
||||
// import '../../../noodl-core-ui/src/styles/custom-properties/animations.css';
|
||||
// import '../../../noodl-core-ui/src/styles/custom-properties/fonts.css';
|
||||
// import '../../../noodl-core-ui/src/styles/custom-properties/colors.css';
|
||||
import '../editor/src/styles/custom-properties/animations.css';
|
||||
import '../editor/src/styles/custom-properties/fonts.css';
|
||||
import '../editor/src/styles/custom-properties/colors.css';
|
||||
```
|
||||
|
||||
### Tasks
|
||||
|
||||
#### 1.1 Consolidate to Single Source of Truth
|
||||
|
||||
1. Replace the contents of `packages/noodl-editor/src/editor/src/styles/custom-properties/colors.css` with the new modern palette (see Appendix A)
|
||||
|
||||
2. Also update `packages/noodl-core-ui/src/styles/custom-properties/colors.css` with the same content
|
||||
|
||||
3. Verify the viewer frame also uses the correct colors:
|
||||
- Check `packages/noodl-editor/src/frames/viewer-frame/index.js`
|
||||
|
||||
#### 1.2 Verify Token Application
|
||||
|
||||
After replacing, verify these key tokens are working:
|
||||
|
||||
| Token | Expected Value | Where to Check |
|
||||
|-------|---------------|----------------|
|
||||
| `--theme-color-bg-1` | `#09090b` (near black) | Main app background |
|
||||
| `--theme-color-bg-2` | `#18181b` | Panel backgrounds |
|
||||
| `--theme-color-bg-3` | `#27272a` | Card/input backgrounds |
|
||||
| `--theme-color-primary` | `#f43f5e` (rose) | CTA buttons |
|
||||
| `--theme-color-secondary` | `#a78bfa` (violet) | Secondary elements |
|
||||
|
||||
### Testing Checklist
|
||||
|
||||
- [ ] App background is clean dark (not brownish)
|
||||
- [ ] Primary buttons are rose colored
|
||||
- [ ] Text is readable with good contrast
|
||||
- [ ] Node colors on canvas still distinguishable
|
||||
- [ ] Success/error/warning states still visible
|
||||
- [ ] No console errors related to missing CSS variables
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Hardcoded Color Audit & Cleanup
|
||||
|
||||
**Priority: HIGH**
|
||||
**Effort: 3-4 hours**
|
||||
**Risk: Low-Medium**
|
||||
|
||||
### Problem
|
||||
|
||||
Many components have hardcoded hex colors instead of using design tokens. This breaks consistency and prevents theming.
|
||||
|
||||
### Tasks
|
||||
|
||||
#### 2.1 Find All Hardcoded Colors
|
||||
|
||||
Search the codebase for hardcoded hex colors in these locations:
|
||||
|
||||
```
|
||||
packages/noodl-editor/src/editor/src/styles/
|
||||
packages/noodl-editor/src/editor/src/views/
|
||||
packages/noodl-core-ui/src/components/
|
||||
```
|
||||
|
||||
Common patterns to find:
|
||||
```css
|
||||
/* Bad - hardcoded */
|
||||
background-color: #383838;
|
||||
background: #444444;
|
||||
border: 1px solid #2a2a2a;
|
||||
color: #b9b9b9;
|
||||
|
||||
/* Good - tokenized */
|
||||
background-color: var(--theme-color-bg-3);
|
||||
```
|
||||
|
||||
#### 2.2 Create Mapping Reference
|
||||
|
||||
Map discovered hardcoded colors to appropriate tokens:
|
||||
|
||||
| Hardcoded | Replace With |
|
||||
|-----------|--------------|
|
||||
| `#000000` | `var(--theme-color-bg-0)` |
|
||||
| `#151414`, `#151515` | `var(--theme-color-bg-1)` |
|
||||
| `#292828`, `#2a2a2a` | `var(--theme-color-bg-2)` |
|
||||
| `#383838`, `#3c3c3c` | `var(--theme-color-bg-3)` |
|
||||
| `#444444`, `#4a4a4a` | `var(--theme-color-bg-4)` |
|
||||
| `#666666`, `#6a6a6a` | `var(--theme-color-fg-muted)` |
|
||||
| `#999999`, `#9a9a9a` | `var(--theme-color-fg-default-shy)` |
|
||||
| `#b8b8b8`, `#b9b9b9` | `var(--theme-color-fg-default)` |
|
||||
| `#d4d4d4` | `var(--theme-color-fg-default-contrast)` |
|
||||
| `#f5f5f5`, `#ffffff` | `var(--theme-color-fg-highlight)` |
|
||||
|
||||
#### 2.3 Priority Files to Fix
|
||||
|
||||
Start with these high-impact files:
|
||||
|
||||
1. **Popup Layer Styles**
|
||||
- `packages/noodl-editor/src/editor/src/styles/popuplayer.css`
|
||||
|
||||
2. **Property Editor**
|
||||
- `packages/noodl-editor/src/editor/src/styles/propertyeditor.css`
|
||||
|
||||
3. **Node Graph Editor**
|
||||
- `packages/noodl-editor/src/editor/src/views/nodegrapheditor/` (all .css/.scss files)
|
||||
|
||||
4. **Inspect Popup**
|
||||
- `packages/noodl-editor/src/editor/src/views/nodegrapheditor/InspectJSONView/InspectPopup.module.scss`
|
||||
|
||||
5. **Connection Popup**
|
||||
- `packages/noodl-editor/src/editor/src/views/ConnectionPopup/ConnectionPopup.module.scss`
|
||||
|
||||
### Testing Checklist
|
||||
|
||||
- [ ] All replaced colors render correctly
|
||||
- [ ] Hover states still work
|
||||
- [ ] Focus states visible
|
||||
- [ ] No visual regressions in property panel
|
||||
- [ ] Popups/modals look correct
|
||||
- [ ] Node graph colors unaffected
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Typography & Spacing Refresh
|
||||
|
||||
**Priority: MEDIUM**
|
||||
**Effort: 2-3 hours**
|
||||
**Risk: Low**
|
||||
|
||||
### Problem
|
||||
|
||||
Current typography feels cramped and dated. Font sizes are small and spacing is inconsistent.
|
||||
|
||||
### Tasks
|
||||
|
||||
#### 3.1 Update Font Tokens
|
||||
|
||||
File: `packages/noodl-core-ui/src/styles/custom-properties/fonts.css`
|
||||
|
||||
```css
|
||||
:root {
|
||||
--font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
--font-family-code: 'JetBrains Mono', Menlo, Monaco, 'Courier New', monospace;
|
||||
|
||||
--font-weight-light: 300;
|
||||
--font-weight-regular: 400;
|
||||
--font-weight-medium: 500;
|
||||
--font-weight-semibold: 600;
|
||||
--font-weight-bold: 700;
|
||||
|
||||
/* New: Font size scale */
|
||||
--font-size-xs: 10px;
|
||||
--font-size-sm: 12px;
|
||||
--font-size-base: 13px;
|
||||
--font-size-md: 14px;
|
||||
--font-size-lg: 16px;
|
||||
--font-size-xl: 18px;
|
||||
--font-size-2xl: 24px;
|
||||
|
||||
/* New: Line height scale */
|
||||
--line-height-tight: 1.2;
|
||||
--line-height-normal: 1.5;
|
||||
--line-height-relaxed: 1.625;
|
||||
|
||||
/* New: Letter spacing */
|
||||
--letter-spacing-tight: -0.02em;
|
||||
--letter-spacing-normal: 0;
|
||||
--letter-spacing-wide: 0.02em;
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.2 Add Spacing Tokens
|
||||
|
||||
Create new file: `packages/noodl-core-ui/src/styles/custom-properties/spacing.css`
|
||||
|
||||
```css
|
||||
:root {
|
||||
--spacing-0: 0;
|
||||
--spacing-1: 4px;
|
||||
--spacing-2: 8px;
|
||||
--spacing-3: 12px;
|
||||
--spacing-4: 16px;
|
||||
--spacing-5: 20px;
|
||||
--spacing-6: 24px;
|
||||
--spacing-8: 32px;
|
||||
--spacing-10: 40px;
|
||||
--spacing-12: 48px;
|
||||
--spacing-16: 64px;
|
||||
|
||||
/* Component-specific spacing */
|
||||
--spacing-panel-padding: var(--spacing-4);
|
||||
--spacing-card-padding: var(--spacing-3);
|
||||
--spacing-input-padding-x: var(--spacing-2);
|
||||
--spacing-input-padding-y: var(--spacing-1);
|
||||
--spacing-button-padding-x: var(--spacing-3);
|
||||
--spacing-button-padding-y: var(--spacing-2);
|
||||
|
||||
/* Border radius */
|
||||
--radius-sm: 2px;
|
||||
--radius-default: 4px;
|
||||
--radius-md: 6px;
|
||||
--radius-lg: 8px;
|
||||
--radius-xl: 12px;
|
||||
--radius-full: 9999px;
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.3 Import New Token Files
|
||||
|
||||
Update imports in:
|
||||
- `packages/noodl-editor/src/editor/index.ts`
|
||||
- `packages/noodl-core-ui/.storybook/preview.ts`
|
||||
|
||||
### Testing Checklist
|
||||
|
||||
- [ ] Text is readable at all sizes
|
||||
- [ ] Spacing feels balanced
|
||||
- [ ] Components don't overflow
|
||||
- [ ] Modal/dialog layouts intact
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Component Visual Updates
|
||||
|
||||
**Priority: MEDIUM**
|
||||
**Effort: 4-6 hours**
|
||||
**Risk: Medium**
|
||||
|
||||
### Problem
|
||||
|
||||
Individual components need visual refinement beyond just color tokens.
|
||||
|
||||
### Tasks
|
||||
|
||||
#### 4.1 Button Refinements
|
||||
|
||||
File: `packages/noodl-core-ui/src/components/inputs/PrimaryButton/PrimaryButton.module.scss`
|
||||
|
||||
Updates needed:
|
||||
- Slightly rounded corners (`border-radius: 6px`)
|
||||
- Subtle shadow on hover
|
||||
- Better disabled state (not just opacity)
|
||||
- Smooth transitions
|
||||
|
||||
```scss
|
||||
.Root {
|
||||
border-radius: var(--radius-md);
|
||||
transition: all 150ms ease;
|
||||
|
||||
&.is-variant-cta {
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 4.2 Input Field Refinements
|
||||
|
||||
File: `packages/noodl-core-ui/src/components/inputs/TextInput/TextInput.module.scss`
|
||||
|
||||
Updates needed:
|
||||
- Subtle border (not just background change)
|
||||
- Focus ring using new token
|
||||
- Better placeholder styling
|
||||
|
||||
```scss
|
||||
.InputArea {
|
||||
border: 1px solid var(--theme-color-border-subtle);
|
||||
border-radius: var(--radius-default);
|
||||
transition: border-color 150ms ease, box-shadow 150ms ease;
|
||||
|
||||
&.is-focused {
|
||||
border-color: var(--theme-color-focus-ring);
|
||||
box-shadow: 0 0 0 2px rgba(244, 63, 94, 0.15);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 4.3 Dialog/Modal Refinements
|
||||
|
||||
File: `packages/noodl-core-ui/src/components/layout/BaseDialog/BaseDialog.module.scss`
|
||||
|
||||
Updates needed:
|
||||
- Subtle border
|
||||
- Refined shadow
|
||||
- Better backdrop blur (if supported)
|
||||
|
||||
```scss
|
||||
.VisibleDialog {
|
||||
border: 1px solid var(--theme-color-border-subtle);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow:
|
||||
0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
||||
0 10px 15px -3px rgba(0, 0, 0, 0.2),
|
||||
0 20px 25px -5px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.Root.has-backdrop {
|
||||
backdrop-filter: blur(4px);
|
||||
-webkit-backdrop-filter: blur(4px);
|
||||
}
|
||||
```
|
||||
|
||||
#### 4.4 Panel/Section Refinements
|
||||
|
||||
Files:
|
||||
- `packages/noodl-core-ui/src/components/sidebar/BasePanel/`
|
||||
- `packages/noodl-core-ui/src/components/sidebar/Section/`
|
||||
|
||||
Updates needed:
|
||||
- Consistent padding using spacing tokens
|
||||
- Subtle dividers between sections
|
||||
- Better header styling
|
||||
|
||||
### Testing Checklist
|
||||
|
||||
- [ ] Buttons look polished and modern
|
||||
- [ ] Inputs have clear focus states
|
||||
- [ ] Dialogs/modals feel elevated
|
||||
- [ ] Panels have clear visual hierarchy
|
||||
- [ ] All interactive states (hover, focus, active, disabled) work
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: Migration Dialog Specific Fixes
|
||||
|
||||
**Priority: HIGH** (User-facing feature)
|
||||
**Effort: 2-3 hours**
|
||||
**Risk: Low**
|
||||
|
||||
### Problem
|
||||
|
||||
The React 19 migration dialog needs specific attention beyond global token changes.
|
||||
|
||||
### Tasks
|
||||
|
||||
#### 5.1 Identify Migration Dialog Files
|
||||
|
||||
Search for migration-related components:
|
||||
```bash
|
||||
find . -name "*.tsx" -o -name "*.jsx" | xargs grep -l -i "migrat"
|
||||
```
|
||||
|
||||
#### 5.2 Dialog Structure Improvements
|
||||
|
||||
The migration wizard should have:
|
||||
- Clear step indicator (not just numbered text list)
|
||||
- Progress visualization
|
||||
- Distinct sections with proper spacing
|
||||
- Better icon usage
|
||||
- Clear primary/secondary actions
|
||||
|
||||
#### 5.3 Suggested Component Structure
|
||||
|
||||
```tsx
|
||||
<DialogContainer>
|
||||
<DialogHeader>
|
||||
<Title>Migrate Project to React 19</Title>
|
||||
<Subtitle>Migration Complete</Subtitle>
|
||||
</DialogHeader>
|
||||
|
||||
<StepIndicator
|
||||
steps={['Confirm', 'Scan', 'Report', 'Migrate', 'Complete']}
|
||||
currentStep={4}
|
||||
/>
|
||||
|
||||
<DialogBody>
|
||||
<SuccessBanner>
|
||||
<Icon name="checkmark-circle" />
|
||||
<Text>Your project has been migrated successfully</Text>
|
||||
</SuccessBanner>
|
||||
|
||||
<StatsCard>
|
||||
<Stat value={62} label="Migrated" status="success" />
|
||||
</StatsCard>
|
||||
|
||||
<Section title="Project Locations">
|
||||
<LocationItem icon="lock" label="Original" path="..." />
|
||||
<LocationItem icon="folder" label="Migrated" path="..." />
|
||||
</Section>
|
||||
|
||||
<Section title="What's Next?">
|
||||
<ChecklistItem>Test your app thoroughly</ChecklistItem>
|
||||
<ChecklistItem>Archive or delete original when ready</ChecklistItem>
|
||||
</Section>
|
||||
</DialogBody>
|
||||
|
||||
<DialogFooter>
|
||||
<PrimaryButton label="Open Migrated Project" />
|
||||
</DialogFooter>
|
||||
</DialogContainer>
|
||||
```
|
||||
|
||||
### Testing Checklist
|
||||
|
||||
- [ ] All wizard steps render correctly
|
||||
- [ ] Progress is clear
|
||||
- [ ] Success/error states are obvious
|
||||
- [ ] Actions are clear
|
||||
- [ ] Dialog is responsive to content length
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: Light Theme Foundation
|
||||
|
||||
**Priority: LOW** (Future enhancement)
|
||||
**Effort: 3-4 hours**
|
||||
**Risk: Medium**
|
||||
|
||||
### Problem
|
||||
|
||||
Currently no infrastructure for theme switching.
|
||||
|
||||
### Tasks
|
||||
|
||||
#### 6.1 Theme Provider Setup
|
||||
|
||||
Create theme context and provider for React components.
|
||||
|
||||
#### 6.2 CSS Theme Classes
|
||||
|
||||
The colors.css file already includes a commented `.theme-light` block. Uncomment and refine.
|
||||
|
||||
#### 6.3 Theme Toggle
|
||||
|
||||
Add settings option to switch between light/dark.
|
||||
|
||||
#### 6.4 Persist Preference
|
||||
|
||||
Store theme preference in localStorage.
|
||||
|
||||
### Testing Checklist
|
||||
|
||||
- [ ] Theme toggle works
|
||||
- [ ] All components respect theme
|
||||
- [ ] No hardcoded colors breaking theme
|
||||
- [ ] Preference persists across sessions
|
||||
|
||||
---
|
||||
|
||||
## Appendix A: Complete colors.css File
|
||||
|
||||
See the Rose + Violet palette file provided separately. Key values:
|
||||
|
||||
```css
|
||||
/* Primary - Rose */
|
||||
--theme-color-primary: #f43f5e;
|
||||
--theme-color-primary-highlight: #fb7185;
|
||||
--theme-color-primary-dim: #be123c;
|
||||
--theme-color-on-primary: #ffffff;
|
||||
|
||||
/* Secondary - Violet */
|
||||
--theme-color-secondary: #a78bfa;
|
||||
--theme-color-secondary-dim: #7c3aed;
|
||||
--theme-color-secondary-highlight: #c4b5fd;
|
||||
--theme-color-on-secondary: #ffffff;
|
||||
|
||||
/* Backgrounds - Zinc */
|
||||
--theme-color-bg-0: #000000;
|
||||
--theme-color-bg-1: #09090b;
|
||||
--theme-color-bg-2: #18181b;
|
||||
--theme-color-bg-3: #27272a;
|
||||
--theme-color-bg-4: #3f3f46;
|
||||
|
||||
/* Foregrounds */
|
||||
--theme-color-fg-highlight: #ffffff;
|
||||
--theme-color-fg-default-contrast: #f4f4f5;
|
||||
--theme-color-fg-default: #d4d4d8;
|
||||
--theme-color-fg-default-shy: #a1a1aa;
|
||||
--theme-color-fg-muted: #71717a;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Appendix B: File Locations Quick Reference
|
||||
|
||||
### Token Files
|
||||
- `packages/noodl-core-ui/src/styles/custom-properties/colors.css`
|
||||
- `packages/noodl-core-ui/src/styles/custom-properties/fonts.css`
|
||||
- `packages/noodl-core-ui/src/styles/custom-properties/animations.css`
|
||||
- `packages/noodl-editor/src/editor/src/styles/custom-properties/colors.css` (duplicate - primary)
|
||||
|
||||
### Entry Points
|
||||
- `packages/noodl-editor/src/editor/index.ts` (main editor)
|
||||
- `packages/noodl-editor/src/frames/viewer-frame/index.js` (viewer)
|
||||
- `packages/noodl-core-ui/.storybook/preview.ts` (storybook)
|
||||
|
||||
### Key Component Directories
|
||||
- `packages/noodl-core-ui/src/components/inputs/` (buttons, inputs)
|
||||
- `packages/noodl-core-ui/src/components/layout/` (dialogs, containers)
|
||||
- `packages/noodl-core-ui/src/components/sidebar/` (panels, sections)
|
||||
- `packages/noodl-core-ui/src/components/typography/` (text, labels)
|
||||
|
||||
### Legacy Style Files (need hardcoded color audit)
|
||||
- `packages/noodl-editor/src/editor/src/styles/`
|
||||
- `packages/noodl-editor/src/editor/src/views/`
|
||||
|
||||
---
|
||||
|
||||
## Appendix C: Full colors.css Replacement
|
||||
|
||||
```css
|
||||
/* =============================================================================
|
||||
NOODL DESIGN SYSTEM - COLORS
|
||||
Modern refresh: Rose + Violet palette
|
||||
============================================================================= */
|
||||
|
||||
/* =============================================================================
|
||||
BASE COLORS
|
||||
These are the raw palette values. DO NOT use directly in components.
|
||||
Use the THEME COLOR TOKENS below instead.
|
||||
============================================================================= */
|
||||
|
||||
:root {
|
||||
/* ---------------------------------------------------------------------------
|
||||
SEMANTIC COLORS
|
||||
--------------------------------------------------------------------------- */
|
||||
|
||||
/* Success - Modern Emerald */
|
||||
--base-color-success-100: #ecfdf5;
|
||||
--base-color-success-200: #a7f3d0;
|
||||
--base-color-success-300: #6ee7b7;
|
||||
--base-color-success-400: #34d399;
|
||||
--base-color-success-500: #10b981;
|
||||
--base-color-success-600: #059669;
|
||||
--base-color-success-700: #047857;
|
||||
--base-color-success-800: #065f46;
|
||||
--base-color-success-900: #064e3b;
|
||||
--base-color-success-1000: #022c22;
|
||||
|
||||
/* Error - Red (distinct from primary rose) */
|
||||
--base-color-error-100: #fef2f2;
|
||||
--base-color-error-200: #fecaca;
|
||||
--base-color-error-300: #fca5a5;
|
||||
--base-color-error-400: #f87171;
|
||||
--base-color-error-500: #ef4444;
|
||||
--base-color-error-600: #dc2626;
|
||||
--base-color-error-700: #b91c1c;
|
||||
--base-color-error-800: #991b1b;
|
||||
--base-color-error-900: #7f1d1d;
|
||||
--base-color-error-1000: #450a0a;
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
NODE TYPE COLORS
|
||||
--------------------------------------------------------------------------- */
|
||||
|
||||
/* Node-Pink - For Custom/User nodes */
|
||||
--base-color-node-pink-100: #fdf2f8;
|
||||
--base-color-node-pink-200: #fbcfe8;
|
||||
--base-color-node-pink-300: #f9a8d4;
|
||||
--base-color-node-pink-400: #f472b6;
|
||||
--base-color-node-pink-500: #ec4899;
|
||||
--base-color-node-pink-600: #db2777;
|
||||
--base-color-node-pink-700: #be185d;
|
||||
--base-color-node-pink-800: #9d174d;
|
||||
--base-color-node-pink-900: #831843;
|
||||
--base-color-node-pink-1000: #500724;
|
||||
|
||||
/* Node-Purple - For Component nodes */
|
||||
--base-color-node-purple-100: #faf5ff;
|
||||
--base-color-node-purple-200: #e9d5ff;
|
||||
--base-color-node-purple-300: #d8b4fe;
|
||||
--base-color-node-purple-400: #c084fc;
|
||||
--base-color-node-purple-500: #a855f7;
|
||||
--base-color-node-purple-600: #9333ea;
|
||||
--base-color-node-purple-700: #7c3aed;
|
||||
--base-color-node-purple-800: #6d28d9;
|
||||
--base-color-node-purple-900: #5b21b6;
|
||||
--base-color-node-purple-1000: #2e1065;
|
||||
|
||||
/* Node-Green - For Data nodes */
|
||||
--base-color-node-green-100: #f0fdf4;
|
||||
--base-color-node-green-200: #bbf7d0;
|
||||
--base-color-node-green-300: #86efac;
|
||||
--base-color-node-green-400: #4ade80;
|
||||
--base-color-node-green-500: #22c55e;
|
||||
--base-color-node-green-600: #16a34a;
|
||||
--base-color-node-green-700: #15803d;
|
||||
--base-color-node-green-800: #166534;
|
||||
--base-color-node-green-900: #14532d;
|
||||
--base-color-node-green-1000: #052e16;
|
||||
|
||||
/* Node-Gray - For Logic nodes */
|
||||
--base-color-node-grey-100: #f4f4f5;
|
||||
--base-color-node-grey-200: #e4e4e7;
|
||||
--base-color-node-grey-300: #d4d4d8;
|
||||
--base-color-node-grey-400: #a1a1aa;
|
||||
--base-color-node-grey-500: #71717a;
|
||||
--base-color-node-grey-600: #52525b;
|
||||
--base-color-node-grey-700: #3f3f46;
|
||||
--base-color-node-grey-800: #27272a;
|
||||
--base-color-node-grey-900: #18181b;
|
||||
--base-color-node-grey-1000: #09090b;
|
||||
|
||||
/* Node-Blue - For Visual nodes */
|
||||
--base-color-node-blue-100: #eff6ff;
|
||||
--base-color-node-blue-200: #dbeafe;
|
||||
--base-color-node-blue-300: #bfdbfe;
|
||||
--base-color-node-blue-400: #93c5fd;
|
||||
--base-color-node-blue-500: #60a5fa;
|
||||
--base-color-node-blue-600: #3b82f6;
|
||||
--base-color-node-blue-700: #2563eb;
|
||||
--base-color-node-blue-800: #1d4ed8;
|
||||
--base-color-node-blue-900: #1e40af;
|
||||
--base-color-node-blue-1000: #172554;
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
BRAND COLORS
|
||||
--------------------------------------------------------------------------- */
|
||||
|
||||
/* Primary - Rose (Modern pink-red) */
|
||||
--base-color-rose-100: #fff1f2;
|
||||
--base-color-rose-200: #fecdd3;
|
||||
--base-color-rose-300: #fda4af;
|
||||
--base-color-rose-400: #fb7185;
|
||||
--base-color-rose-500: #f43f5e;
|
||||
--base-color-rose-600: #e11d48;
|
||||
--base-color-rose-700: #be123c;
|
||||
--base-color-rose-800: #9f1239;
|
||||
--base-color-rose-900: #881337;
|
||||
--base-color-rose-1000: #4c0519;
|
||||
|
||||
/* Secondary - Violet */
|
||||
--base-color-violet-100: #f5f3ff;
|
||||
--base-color-violet-200: #ede9fe;
|
||||
--base-color-violet-300: #ddd6fe;
|
||||
--base-color-violet-400: #c4b5fd;
|
||||
--base-color-violet-500: #a78bfa;
|
||||
--base-color-violet-600: #8b5cf6;
|
||||
--base-color-violet-700: #7c3aed;
|
||||
--base-color-violet-800: #6d28d9;
|
||||
--base-color-violet-900: #5b21b6;
|
||||
--base-color-violet-1000: #2e1065;
|
||||
|
||||
/* Amber - For warnings/notices */
|
||||
--base-color-amber-100: #fffbeb;
|
||||
--base-color-amber-200: #fef3c7;
|
||||
--base-color-amber-300: #fcd34d;
|
||||
--base-color-amber-400: #fbbf24;
|
||||
--base-color-amber-500: #f59e0b;
|
||||
--base-color-amber-600: #d97706;
|
||||
--base-color-amber-700: #b45309;
|
||||
--base-color-amber-800: #92400e;
|
||||
--base-color-amber-900: #78350f;
|
||||
--base-color-amber-1000: #451a03;
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
UI NEUTRALS - Clean Zinc palette
|
||||
--------------------------------------------------------------------------- */
|
||||
--base-color-zinc-50: #fafafa;
|
||||
--base-color-zinc-100: #f4f4f5;
|
||||
--base-color-zinc-200: #e4e4e7;
|
||||
--base-color-zinc-300: #d4d4d8;
|
||||
--base-color-zinc-400: #a1a1aa;
|
||||
--base-color-zinc-500: #71717a;
|
||||
--base-color-zinc-600: #52525b;
|
||||
--base-color-zinc-700: #3f3f46;
|
||||
--base-color-zinc-800: #27272a;
|
||||
--base-color-zinc-900: #18181b;
|
||||
--base-color-zinc-950: #09090b;
|
||||
|
||||
/* Transparent variants */
|
||||
--base-color-zinc-950-transparent: rgba(9, 9, 11, 0.85);
|
||||
--base-color-zinc-950-transparent-light: rgba(9, 9, 11, 0.5);
|
||||
--base-color-white-transparent: rgba(255, 255, 255, 0.08);
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
LEGACY ALIASES - For backwards compatibility
|
||||
--------------------------------------------------------------------------- */
|
||||
|
||||
--base-color-grey-100: var(--base-color-zinc-100);
|
||||
--base-color-grey-100-transparent: rgba(244, 244, 245, 0.13);
|
||||
--base-color-grey-200: var(--base-color-zinc-200);
|
||||
--base-color-grey-300: var(--base-color-zinc-300);
|
||||
--base-color-grey-400: var(--base-color-zinc-400);
|
||||
--base-color-grey-500: var(--base-color-zinc-500);
|
||||
--base-color-grey-600: var(--base-color-zinc-600);
|
||||
--base-color-grey-700: var(--base-color-zinc-700);
|
||||
--base-color-grey-800: var(--base-color-zinc-800);
|
||||
--base-color-grey-900: var(--base-color-zinc-900);
|
||||
--base-color-grey-1000: var(--base-color-zinc-950);
|
||||
--base-color-grey-1000-transparent: var(--base-color-zinc-950-transparent);
|
||||
--base-color-grey-1000-transparent-2: var(--base-color-zinc-950-transparent-light);
|
||||
|
||||
--base-color-teal-100: var(--base-color-violet-100);
|
||||
--base-color-teal-200: var(--base-color-violet-200);
|
||||
--base-color-teal-300: var(--base-color-violet-300);
|
||||
--base-color-teal-400: var(--base-color-violet-400);
|
||||
--base-color-teal-500: var(--base-color-violet-500);
|
||||
--base-color-teal-600: var(--base-color-violet-600);
|
||||
--base-color-teal-700: var(--base-color-violet-700);
|
||||
--base-color-teal-800: var(--base-color-violet-800);
|
||||
--base-color-teal-900: var(--base-color-violet-900);
|
||||
--base-color-teal-1000: var(--base-color-violet-1000);
|
||||
|
||||
--base-color-yellow-100: var(--base-color-rose-100);
|
||||
--base-color-yellow-200: var(--base-color-rose-200);
|
||||
--base-color-yellow-300: var(--base-color-rose-300);
|
||||
--base-color-yellow-400: var(--base-color-rose-400);
|
||||
--base-color-yellow-500: var(--base-color-rose-500);
|
||||
--base-color-yellow-600: var(--base-color-rose-600);
|
||||
--base-color-yellow-700: var(--base-color-rose-700);
|
||||
--base-color-yellow-800: var(--base-color-rose-800);
|
||||
--base-color-yellow-900: var(--base-color-rose-900);
|
||||
--base-color-yellow-1000: var(--base-color-rose-1000);
|
||||
}
|
||||
|
||||
|
||||
/* =============================================================================
|
||||
THEME COLOR TOKENS - USE THESE IN COMPONENTS
|
||||
============================================================================= */
|
||||
|
||||
:root {
|
||||
/* Backgrounds */
|
||||
--theme-color-bg-0: #000000;
|
||||
--theme-color-bg-1: var(--base-color-zinc-950);
|
||||
--theme-color-bg-1-transparent: var(--base-color-zinc-950-transparent);
|
||||
--theme-color-bg-1-transparent-2: var(--base-color-zinc-950-transparent-light);
|
||||
--theme-color-bg-2: var(--base-color-zinc-900);
|
||||
--theme-color-bg-3: var(--base-color-zinc-800);
|
||||
--theme-color-bg-4: var(--base-color-zinc-700);
|
||||
--theme-color-bg-5: var(--base-color-zinc-600);
|
||||
--theme-color-bg-hover: var(--base-color-white-transparent);
|
||||
|
||||
/* Foregrounds */
|
||||
--theme-color-fg-highlight: #ffffff;
|
||||
--theme-color-fg-default-contrast: var(--base-color-zinc-100);
|
||||
--theme-color-fg-default: var(--base-color-zinc-300);
|
||||
--theme-color-fg-default-shy: var(--base-color-zinc-400);
|
||||
--theme-color-fg-muted: var(--base-color-zinc-500);
|
||||
--theme-color-fg-transparent: var(--base-color-grey-100-transparent);
|
||||
|
||||
/* Primary - Rose */
|
||||
--theme-color-primary: var(--base-color-rose-500);
|
||||
--theme-color-primary-highlight: var(--base-color-rose-400);
|
||||
--theme-color-primary-dim: var(--base-color-rose-700);
|
||||
--theme-color-on-primary: #ffffff;
|
||||
|
||||
/* Secondary - Violet */
|
||||
--theme-color-secondary: var(--base-color-violet-500);
|
||||
--theme-color-secondary-dim: var(--base-color-violet-700);
|
||||
--theme-color-secondary-highlight: var(--base-color-violet-400);
|
||||
--theme-color-secondary-bright: var(--base-color-violet-300);
|
||||
--theme-color-secondary-as-fg: var(--base-color-violet-400);
|
||||
--theme-color-on-secondary: #ffffff;
|
||||
|
||||
/* Node Colors */
|
||||
--theme-color-node-data-1: var(--base-color-node-green-700);
|
||||
--theme-color-node-data-2: var(--base-color-node-green-600);
|
||||
--theme-color-node-data-3: var(--base-color-node-green-500);
|
||||
--theme-color-node-data-dim: var(--base-color-node-green-900);
|
||||
|
||||
--theme-color-node-visual-1: var(--base-color-node-blue-700);
|
||||
--theme-color-node-visual-2: var(--base-color-node-blue-600);
|
||||
--theme-color-node-visual-2-highlight: var(--base-color-node-blue-500);
|
||||
--theme-color-node-visual-highlight: var(--base-color-node-blue-200);
|
||||
--theme-color-node-visual-default: var(--base-color-node-blue-300);
|
||||
--theme-color-node-visual-shy: var(--base-color-node-blue-400);
|
||||
--theme-color-node-visual-dim: var(--base-color-node-blue-900);
|
||||
|
||||
--theme-color-node-custom-1: var(--base-color-node-pink-700);
|
||||
--theme-color-node-custom-2: var(--base-color-node-pink-600);
|
||||
--theme-color-node-custom-dim: var(--base-color-node-pink-900);
|
||||
|
||||
--theme-color-node-logic-1: var(--base-color-node-grey-700);
|
||||
--theme-color-node-logic-2: var(--base-color-node-grey-600);
|
||||
--theme-color-node-logic-dim: var(--base-color-node-grey-900);
|
||||
|
||||
--theme-color-node-component-1: var(--base-color-node-purple-700);
|
||||
--theme-color-node-component-2: var(--base-color-node-purple-600);
|
||||
--theme-color-node-component-dim: var(--base-color-node-purple-900);
|
||||
|
||||
/* Status Colors */
|
||||
--theme-color-success: var(--base-color-success-400);
|
||||
--theme-color-success-dim: var(--base-color-success-600);
|
||||
--theme-color-success-bg: var(--base-color-success-900);
|
||||
|
||||
--theme-color-notice: var(--base-color-amber-400);
|
||||
--theme-color-notice-dim: var(--base-color-amber-600);
|
||||
--theme-color-notice-bg: var(--base-color-amber-900);
|
||||
|
||||
--theme-color-danger: var(--base-color-error-400);
|
||||
--theme-color-danger-light: var(--base-color-error-300);
|
||||
--theme-color-danger-dim: var(--base-color-error-600);
|
||||
--theme-color-danger-bg: var(--base-color-error-900);
|
||||
|
||||
/* Connection Colors */
|
||||
--theme-color-signal: var(--base-color-rose-400);
|
||||
--theme-color-data: var(--base-color-violet-500);
|
||||
|
||||
/* Border Colors */
|
||||
--theme-color-border-default: var(--base-color-zinc-700);
|
||||
--theme-color-border-subtle: var(--base-color-zinc-800);
|
||||
--theme-color-border-strong: var(--base-color-zinc-600);
|
||||
|
||||
/* Focus Ring */
|
||||
--theme-color-focus-ring: var(--base-color-rose-500);
|
||||
--theme-color-focus-ring-offset: var(--base-color-zinc-950);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria
|
||||
|
||||
### Visual
|
||||
- [ ] App feels modern and professional
|
||||
- [ ] Colors are consistent throughout
|
||||
- [ ] Good contrast and readability
|
||||
- [ ] Visual hierarchy is clear
|
||||
|
||||
### Technical
|
||||
- [ ] All colors use design tokens
|
||||
- [ ] No hardcoded hex colors in component styles
|
||||
- [ ] Token system supports future theming
|
||||
- [ ] No visual regressions
|
||||
|
||||
### User Experience
|
||||
- [ ] Migration dialog is clear and professional
|
||||
- [ ] Interactive states (hover, focus) are obvious
|
||||
- [ ] Success/error feedback is clear
|
||||
- [ ] Overall polish matches modern dev tools
|
||||
@@ -0,0 +1,125 @@
|
||||
# TASK-000: Design System Modernization - Task Index
|
||||
|
||||
## Overview
|
||||
|
||||
This is the master task for OpenNoodl's UI overhaul, broken down into 8 sub-tasks for incremental implementation.
|
||||
|
||||
**Color Scheme**: RED-MINIMAL palette
|
||||
- **Primary**: Noodl Red (`#d21f3c`)
|
||||
- **Secondary**: White (`#ffffff`)
|
||||
- **Neutrals**: Pure black/gray (no color tint)
|
||||
|
||||
---
|
||||
|
||||
## Sub-Task Summary
|
||||
|
||||
| Task | Name | Priority | Effort | Dependencies |
|
||||
|------|------|----------|--------|--------------|
|
||||
| **000A** | Token Consolidation & Color Refresh | CRITICAL | 30 min | None |
|
||||
| **000B** | Hardcoded Colors - Legacy Styles | HIGH | 1-2 hrs | 000A |
|
||||
| **000C** | Hardcoded Colors - Node Graph | HIGH | 1-2 hrs | 000A |
|
||||
| **000D** | Hardcoded Colors - Core UI | HIGH | 1-2 hrs | 000A |
|
||||
| **000E** | Typography & Spacing Tokens | MEDIUM | 1 hr | 000A |
|
||||
| **000F** | Component Updates - Buttons/Inputs | MEDIUM | 1-2 hrs | 000A, 000D, 000E |
|
||||
| **000G** | Component Updates - Dialogs/Panels | MEDIUM | 1-2 hrs | 000A, 000D, 000E |
|
||||
| **000H** | Migration Wizard Polish | HIGH | 1-2 hrs | 000A-000G |
|
||||
|
||||
**Total Estimated Effort**: 8-14 hours
|
||||
|
||||
---
|
||||
|
||||
## Recommended Execution Order
|
||||
|
||||
### Phase 1: Foundation (Do First)
|
||||
1. **TASK-000A** - Token Consolidation & Color Refresh
|
||||
- This is the foundation - everything else depends on it
|
||||
- Location: `../TASK-000A-token-consolidation/OVERVIEW.md`
|
||||
|
||||
### Phase 2: Color Audit (Can Parallelize)
|
||||
These can be done in any order after 000A:
|
||||
|
||||
2. **TASK-000B** - Hardcoded Colors - Legacy Styles
|
||||
- Location: `../TASK-000B-hardcoded-colors-legacy/OVERVIEW.md`
|
||||
|
||||
3. **TASK-000C** - Hardcoded Colors - Node Graph
|
||||
- Location: `../TASK-000C-hardcoded-colors-nodegraph/OVERVIEW.md`
|
||||
|
||||
4. **TASK-000D** - Hardcoded Colors - Core UI
|
||||
- Location: `../TASK-000D-hardcoded-colors-coreui/OVERVIEW.md`
|
||||
|
||||
5. **TASK-000E** - Typography & Spacing Tokens
|
||||
- Can be done independently
|
||||
- Location: `../TASK-000E-typography-spacing/OVERVIEW.md`
|
||||
|
||||
### Phase 3: Visual Polish (After Color Audit)
|
||||
6. **TASK-000F** - Component Updates - Buttons/Inputs
|
||||
- Location: `../TASK-000F-component-buttons-inputs/OVERVIEW.md`
|
||||
|
||||
7. **TASK-000G** - Component Updates - Dialogs/Panels
|
||||
- Location: `../TASK-000G-component-dialogs-panels/OVERVIEW.md`
|
||||
|
||||
### Phase 4: Final Polish
|
||||
8. **TASK-000H** - Migration Wizard Polish
|
||||
- Should be last as it benefits from all prior work
|
||||
- Location: `../TASK-000H-migration-wizard-polish/OVERVIEW.md`
|
||||
|
||||
---
|
||||
|
||||
## Key Files Reference
|
||||
|
||||
### Color Token Files
|
||||
- `packages/noodl-editor/src/editor/src/styles/custom-properties/colors.css` (primary)
|
||||
- `packages/noodl-core-ui/src/styles/custom-properties/colors.css` (secondary)
|
||||
|
||||
### Color Source
|
||||
- `dev-docs/tasks/phase-3/TASK-000-styles-overhaul/COLORS-RED-MINIMAL.md`
|
||||
|
||||
### Entry Points to Verify
|
||||
- `packages/noodl-editor/src/editor/index.ts`
|
||||
- `packages/noodl-core-ui/.storybook/preview.ts`
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria (All Tasks Complete)
|
||||
|
||||
### Technical
|
||||
- [ ] All colors use CSS variables (no hardcoded hex in styles)
|
||||
- [ ] Token system supports future light theme
|
||||
- [ ] Typography and spacing tokens available
|
||||
|
||||
### Visual
|
||||
- [ ] App uses consistent RED-MINIMAL palette
|
||||
- [ ] Pure dark backgrounds (no warm/brown tint)
|
||||
- [ ] Primary accent is red (`#d21f3c`)
|
||||
- [ ] Good contrast and readability
|
||||
|
||||
### User Experience
|
||||
- [ ] Migration wizard looks polished
|
||||
- [ ] Interactive states are obvious
|
||||
- [ ] Overall feel matches modern dev tools
|
||||
|
||||
---
|
||||
|
||||
## Verification Commands
|
||||
|
||||
After all tasks complete:
|
||||
|
||||
```bash
|
||||
# Check for remaining hardcoded colors in styles
|
||||
grep -rn "#[0-9a-fA-F]\{6\}" packages/noodl-editor/src/editor/src/styles/ --include="*.css" --include="*.scss" | wc -l
|
||||
|
||||
# Check views directory
|
||||
grep -rn "#[0-9a-fA-F]\{6\}" packages/noodl-editor/src/editor/src/views/ --include="*.css" --include="*.scss" | wc -l
|
||||
|
||||
# Check core-ui components
|
||||
grep -rn "#[0-9a-fA-F]\{6\}" packages/noodl-core-ui/src/components/ --include="*.css" --include="*.scss" | wc -l
|
||||
```
|
||||
|
||||
**Target: Near-zero hardcoded colors** (some node-specific colors may remain intentionally)
|
||||
|
||||
---
|
||||
|
||||
## Related Documents
|
||||
|
||||
- `DESIGN-SYSTEM-MODERNISATION.md` - Original detailed planning document
|
||||
- `COLORS-RED-MINIMAL.md` - Complete CSS palette to use
|
||||
@@ -0,0 +1,131 @@
|
||||
# TASK-000A: Token Consolidation & Color Refresh
|
||||
|
||||
## Overview
|
||||
|
||||
Replace the color token files with the new RED-MINIMAL palette. This is the foundation task - all other style tasks depend on this being completed first.
|
||||
|
||||
**Priority:** CRITICAL
|
||||
**Effort:** 30 minutes
|
||||
**Risk:** Low
|
||||
**Dependencies:** None
|
||||
|
||||
---
|
||||
|
||||
## Objective
|
||||
|
||||
Consolidate color definitions to a single source of truth using the RED-MINIMAL palette:
|
||||
- **Primary**: Noodl Red (`#d21f3c`)
|
||||
- **Secondary**: White (`#ffffff`)
|
||||
- **Neutrals**: Pure black/gray (no color tint)
|
||||
|
||||
---
|
||||
|
||||
## Files to Modify
|
||||
|
||||
### Primary Target
|
||||
```
|
||||
packages/noodl-editor/src/editor/src/styles/custom-properties/colors.css
|
||||
```
|
||||
This is the file actually imported by the editor.
|
||||
|
||||
### Secondary Target
|
||||
```
|
||||
packages/noodl-core-ui/src/styles/custom-properties/colors.css
|
||||
```
|
||||
Should contain identical content for Storybook and component development.
|
||||
|
||||
### Verify These Entry Points
|
||||
- `packages/noodl-editor/src/editor/index.ts` - Confirm which colors.css is imported
|
||||
- `packages/noodl-editor/src/frames/viewer-frame/index.js` - Verify viewer uses same tokens
|
||||
- `packages/noodl-core-ui/.storybook/preview.ts` - Verify Storybook imports
|
||||
|
||||
---
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### Step 1: Backup Current Colors
|
||||
Before making changes, note what the current colors look like for visual comparison.
|
||||
|
||||
### Step 2: Replace Editor colors.css
|
||||
Copy the contents from `dev-docs/tasks/phase-3/TASK-000-styles-overhaul/COLORS-RED-MINIMAL.md` to:
|
||||
```
|
||||
packages/noodl-editor/src/editor/src/styles/custom-properties/colors.css
|
||||
```
|
||||
|
||||
Note: The COLORS-RED-MINIMAL.md file contains CSS in markdown format. Copy the CSS content only.
|
||||
|
||||
### Step 3: Update Core UI colors.css
|
||||
Copy the same content to:
|
||||
```
|
||||
packages/noodl-core-ui/src/styles/custom-properties/colors.css
|
||||
```
|
||||
|
||||
### Step 4: Verify Imports
|
||||
Confirm in `packages/noodl-editor/src/editor/index.ts`:
|
||||
```typescript
|
||||
// Should be using the editor's copy (not core-ui)
|
||||
import '../editor/src/styles/custom-properties/colors.css';
|
||||
```
|
||||
|
||||
### Step 5: Build & Test
|
||||
```bash
|
||||
npm run build:editor
|
||||
npm run dev
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key Color Mappings
|
||||
|
||||
| Token | Value | Usage |
|
||||
|-------|-------|-------|
|
||||
| `--theme-color-primary` | `#d21f3c` | Primary buttons, CTAs, focus rings |
|
||||
| `--theme-color-secondary` | `#ffffff` | Secondary actions |
|
||||
| `--theme-color-bg-1` | `#0a0a0a` | Main app background |
|
||||
| `--theme-color-bg-2` | `#121212` | Panel backgrounds |
|
||||
| `--theme-color-bg-3` | `#1a1a1a` | Card/input backgrounds |
|
||||
| `--theme-color-fg-default` | `#d4d4d4` | Default text color |
|
||||
| `--theme-color-fg-muted` | `#737373` | Secondary text |
|
||||
|
||||
---
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
- [ ] App compiles without errors
|
||||
- [ ] App background is pure dark (not brownish/warm)
|
||||
- [ ] Primary buttons show red color (`#d21f3c`)
|
||||
- [ ] Text is readable with good contrast
|
||||
- [ ] Node colors on canvas still distinguishable
|
||||
- [ ] Success states show green
|
||||
- [ ] Error states show red
|
||||
- [ ] No console errors about missing CSS variables
|
||||
- [ ] Storybook still works
|
||||
|
||||
---
|
||||
|
||||
## Expected Visual Changes
|
||||
|
||||
After applying:
|
||||
- **Backgrounds**: Will shift from warm/brownish grays to pure neutral blacks
|
||||
- **Primary Color**: Yellow/teal accent → Red accent
|
||||
- **Secondary Color**: Teal → White/neutral
|
||||
- **Overall Feel**: Cleaner, more modern, higher contrast
|
||||
|
||||
---
|
||||
|
||||
## Rollback Plan
|
||||
|
||||
If something breaks, the original color file can be restored from git:
|
||||
```bash
|
||||
git checkout HEAD -- packages/noodl-editor/src/editor/src/styles/custom-properties/colors.css
|
||||
git checkout HEAD -- packages/noodl-core-ui/src/styles/custom-properties/colors.css
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- [ ] Both color files contain identical RED-MINIMAL palette
|
||||
- [ ] Editor runs without visual regressions
|
||||
- [ ] All existing functionality unchanged
|
||||
- [ ] Ready for hardcoded color audit (next tasks)
|
||||
@@ -0,0 +1,197 @@
|
||||
# TASK-000B: Hardcoded Color Audit - Legacy Styles
|
||||
|
||||
## Overview
|
||||
|
||||
Find and replace all hardcoded hex colors in the legacy styles directory. This eliminates inconsistencies and ensures all colors can be changed via design tokens.
|
||||
|
||||
**Priority:** HIGH
|
||||
**Effort:** 1-2 hours
|
||||
**Risk:** Low-Medium
|
||||
**Dependencies:** TASK-000A (Token Consolidation)
|
||||
|
||||
---
|
||||
|
||||
## Objective
|
||||
|
||||
Replace all hardcoded hex color values with CSS variable references in the legacy styles directory, ensuring centralized color control.
|
||||
|
||||
---
|
||||
|
||||
## Target Directory
|
||||
|
||||
```
|
||||
packages/noodl-editor/src/editor/src/styles/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Find All Hardcoded Colors
|
||||
|
||||
Run this search to identify all hardcoded hex colors:
|
||||
|
||||
```bash
|
||||
# Find all hex colors in CSS/SCSS files
|
||||
grep -rn "#[0-9a-fA-F]\{3,8\}" packages/noodl-editor/src/editor/src/styles/ --include="*.css" --include="*.scss"
|
||||
```
|
||||
|
||||
Or use VSCode search with regex:
|
||||
```
|
||||
#[0-9a-fA-F]{3,8}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Color Mapping Reference
|
||||
|
||||
Use this mapping to convert hardcoded colors to tokens:
|
||||
|
||||
### Background Colors
|
||||
| Hardcoded | Token | Notes |
|
||||
|-----------|-------|-------|
|
||||
| `#000000`, `#000` | `var(--theme-color-bg-0)` | Pure black |
|
||||
| `#0a0a0a`, `#0d0d0d`, `#111`, `#111111` | `var(--theme-color-bg-1)` | Near black |
|
||||
| `#121212`, `#151515`, `#141414` | `var(--theme-color-bg-2)` | Dark panels |
|
||||
| `#1a1a1a`, `#191919`, `#1c1c1c` | `var(--theme-color-bg-3)` | Elevated panels |
|
||||
| `#262626`, `#252525`, `#282828` | `var(--theme-color-bg-4)` | Cards |
|
||||
| `#333333`, `#303030`, `#363636` | `var(--theme-color-bg-5)` | Highest elevation |
|
||||
|
||||
### Text/Foreground Colors
|
||||
| Hardcoded | Token | Notes |
|
||||
|-----------|-------|-------|
|
||||
| `#ffffff`, `#fff` | `var(--theme-color-fg-highlight)` | Bright white |
|
||||
| `#e5e5e5`, `#eaeaea`, `#eeeeee` | `var(--theme-color-fg-default-contrast)` | High contrast text |
|
||||
| `#d4d4d4`, `#cccccc`, `#c8c8c8` | `var(--theme-color-fg-default)` | Default text |
|
||||
| `#a3a3a3`, `#aaaaaa`, `#9e9e9e` | `var(--theme-color-fg-default-shy)` | Secondary text |
|
||||
| `#737373`, `#666666`, `#707070` | `var(--theme-color-fg-muted)` | Muted/disabled text |
|
||||
|
||||
### Border Colors
|
||||
| Hardcoded | Token | Notes |
|
||||
|-----------|-------|-------|
|
||||
| `#262626`, `#2a2a2a` | `var(--theme-color-border-subtle)` | Subtle borders |
|
||||
| `#333333`, `#363636` | `var(--theme-color-border-default)` | Default borders |
|
||||
| `#444444`, `#4a4a4a` | `var(--theme-color-border-strong)` | Strong borders |
|
||||
|
||||
### Accent Colors
|
||||
| Hardcoded | Token | Notes |
|
||||
|-----------|-------|-------|
|
||||
| `#d21f3c`, `#e11d48`, `#dc2626` | `var(--theme-color-primary)` | Primary red |
|
||||
| Any teal/cyan colors | `var(--theme-color-secondary)` | Now white |
|
||||
|
||||
### Status Colors
|
||||
| Hardcoded | Token | Notes |
|
||||
|-----------|-------|-------|
|
||||
| `#10b981`, `#22c55e` (green) | `var(--theme-color-success)` | Success |
|
||||
| `#ef4444`, `#dc2626` (red) | `var(--theme-color-danger)` | Danger/Error |
|
||||
| `#f59e0b`, `#fbbf24` (yellow) | `var(--theme-color-notice)` | Warning |
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Priority Files to Fix
|
||||
|
||||
Process these files in order of importance:
|
||||
|
||||
### Critical (High Impact)
|
||||
1. **`popuplayer.css`** - All popup/dropdown backgrounds
|
||||
2. **`propertyeditor.css`** - Property panel styling
|
||||
3. **`common.css`** / `base.css` - Global styles
|
||||
|
||||
### Important
|
||||
4. **`projectsview.css`** - Dashboard/projects list
|
||||
5. **`sidepanel.css`** - Side panel backgrounds
|
||||
6. **`menubar.css`** - Top menu styling
|
||||
|
||||
### Secondary
|
||||
7. All remaining `.css` and `.scss` files in the directory
|
||||
|
||||
---
|
||||
|
||||
## Step 4: Implementation Pattern
|
||||
|
||||
For each hardcoded color found:
|
||||
|
||||
```css
|
||||
/* BEFORE - Hardcoded */
|
||||
.popup {
|
||||
background: #1a1a1a;
|
||||
border: 1px solid #333333;
|
||||
color: #d4d4d4;
|
||||
}
|
||||
|
||||
/* AFTER - Tokenized */
|
||||
.popup {
|
||||
background: var(--theme-color-bg-3);
|
||||
border: 1px solid var(--theme-color-border-default);
|
||||
color: var(--theme-color-fg-default);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 5: Handle Edge Cases
|
||||
|
||||
### Colors Not in Token System
|
||||
If you find a color that doesn't map to any token:
|
||||
1. Check if it's close enough to an existing token
|
||||
2. If unique and necessary, add it to colors.css
|
||||
3. Document why the new token was needed
|
||||
|
||||
### RGBA Colors
|
||||
Convert rgba values too:
|
||||
```css
|
||||
/* BEFORE */
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
|
||||
/* AFTER */
|
||||
background: var(--base-color-black-transparent-80);
|
||||
```
|
||||
|
||||
### Gradient Colors
|
||||
For gradients, replace each color in the gradient:
|
||||
```css
|
||||
/* BEFORE */
|
||||
background: linear-gradient(#1a1a1a, #121212);
|
||||
|
||||
/* AFTER */
|
||||
background: linear-gradient(var(--theme-color-bg-3), var(--theme-color-bg-2));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
After each file is updated:
|
||||
|
||||
- [ ] No CSS compilation errors
|
||||
- [ ] Visual appearance matches original (or is intentionally improved)
|
||||
- [ ] Hover states still work
|
||||
- [ ] Focus states visible
|
||||
- [ ] No missing backgrounds (transparent where should be solid)
|
||||
- [ ] Text contrast is acceptable
|
||||
|
||||
### Full Test After All Changes
|
||||
- [ ] Open/close all popup types
|
||||
- [ ] Property editor functions correctly
|
||||
- [ ] Menus display correctly
|
||||
- [ ] No visual regressions in editor
|
||||
|
||||
---
|
||||
|
||||
## Verification Command
|
||||
|
||||
After completing, run this to ensure no hardcoded colors remain:
|
||||
|
||||
```bash
|
||||
# Should return minimal results (only node-specific colors are acceptable)
|
||||
grep -rn "#[0-9a-fA-F]\{6\}" packages/noodl-editor/src/editor/src/styles/ --include="*.css" --include="*.scss" | grep -v "node-" | wc -l
|
||||
```
|
||||
|
||||
**Target: 0 hardcoded UI colors remaining**
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- [ ] All UI colors in `packages/noodl-editor/src/editor/src/styles/` use CSS variables
|
||||
- [ ] No visual regressions
|
||||
- [ ] Grep search returns no hardcoded hex colors (except node-specific)
|
||||
- [ ] Ready for component-level audit (TASK-000C, 000D)
|
||||
@@ -0,0 +1,202 @@
|
||||
# TASK-000C: Hardcoded Color Audit - Node Graph Editor
|
||||
|
||||
## Overview
|
||||
|
||||
Find and replace all hardcoded hex colors in the node graph editor views directory. This is a high-visibility area where users spend most of their time.
|
||||
|
||||
**Priority:** HIGH
|
||||
**Effort:** 1-2 hours
|
||||
**Risk:** Low-Medium
|
||||
**Dependencies:** TASK-000A (Token Consolidation)
|
||||
|
||||
---
|
||||
|
||||
## Objective
|
||||
|
||||
Replace all hardcoded hex color values in the node graph editor views with CSS variable references.
|
||||
|
||||
---
|
||||
|
||||
## Target Directory
|
||||
|
||||
```
|
||||
packages/noodl-editor/src/editor/src/views/
|
||||
```
|
||||
|
||||
Focus especially on:
|
||||
```
|
||||
packages/noodl-editor/src/editor/src/views/nodegrapheditor/
|
||||
packages/noodl-editor/src/editor/src/views/ConnectionPopup/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Find All Hardcoded Colors
|
||||
|
||||
```bash
|
||||
# Find all hex colors in views directory
|
||||
grep -rn "#[0-9a-fA-F]\{3,8\}" packages/noodl-editor/src/editor/src/views/ --include="*.css" --include="*.scss"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Priority Files
|
||||
|
||||
### Critical Files
|
||||
1. **`InspectPopup.module.scss`**
|
||||
- `packages/noodl-editor/src/editor/src/views/nodegrapheditor/InspectJSONView/InspectPopup.module.scss`
|
||||
- Used for debugging/inspecting node values
|
||||
|
||||
2. **`ConnectionPopup.module.scss`**
|
||||
- `packages/noodl-editor/src/editor/src/views/ConnectionPopup/ConnectionPopup.module.scss`
|
||||
- Shown when creating connections between nodes
|
||||
|
||||
3. **Node Graph Editor Styles**
|
||||
- Any `.css` or `.scss` files in `nodegrapheditor/` directory
|
||||
|
||||
### Other View Files
|
||||
4. **Migration Wizard files** (if any remain hardcoded)
|
||||
- `packages/noodl-editor/src/editor/src/views/migration/`
|
||||
|
||||
5. **Project views**
|
||||
- `packages/noodl-editor/src/editor/src/views/projectsview.ts` (check inline styles)
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Node-Specific Color Handling
|
||||
|
||||
**IMPORTANT**: Some colors in node graph views are intentionally distinct for different node types. These should use the node-specific tokens, not general UI tokens:
|
||||
|
||||
### Node Type Color Tokens
|
||||
```css
|
||||
/* Data nodes - Green */
|
||||
var(--theme-color-node-data-1)
|
||||
var(--theme-color-node-data-2)
|
||||
var(--theme-color-node-data-3)
|
||||
|
||||
/* Visual nodes - Blue */
|
||||
var(--theme-color-node-visual-1)
|
||||
var(--theme-color-node-visual-2)
|
||||
|
||||
/* Custom nodes - Pink */
|
||||
var(--theme-color-node-custom-1)
|
||||
var(--theme-color-node-custom-2)
|
||||
|
||||
/* Logic nodes - Gray */
|
||||
var(--theme-color-node-logic-1)
|
||||
var(--theme-color-node-logic-2)
|
||||
|
||||
/* Component nodes - Purple */
|
||||
var(--theme-color-node-component-1)
|
||||
var(--theme-color-node-component-2)
|
||||
```
|
||||
|
||||
### Connection Color Tokens
|
||||
```css
|
||||
/* Signal connections (events) */
|
||||
var(--theme-color-signal) /* Red */
|
||||
|
||||
/* Data connections (values) */
|
||||
var(--theme-color-data) /* Gray/neutral */
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 4: Color Mapping for Views
|
||||
|
||||
### Background Colors (same as TASK-000B)
|
||||
| Hardcoded | Token |
|
||||
|-----------|-------|
|
||||
| `#0a0a0a`, `#111111` | `var(--theme-color-bg-1)` |
|
||||
| `#121212`, `#151515` | `var(--theme-color-bg-2)` |
|
||||
| `#1a1a1a`, `#191919` | `var(--theme-color-bg-3)` |
|
||||
| `#262626`, `#282828` | `var(--theme-color-bg-4)` |
|
||||
| `#333333`, `#363636` | `var(--theme-color-bg-5)` |
|
||||
|
||||
### Foreground Colors
|
||||
| Hardcoded | Token |
|
||||
|-----------|-------|
|
||||
| `#ffffff`, `#fff` | `var(--theme-color-fg-highlight)` |
|
||||
| `#d4d4d4`, `#cccccc` | `var(--theme-color-fg-default)` |
|
||||
| `#a3a3a3`, `#999999` | `var(--theme-color-fg-default-shy)` |
|
||||
| `#737373`, `#666666` | `var(--theme-color-fg-muted)` |
|
||||
|
||||
### Popup/Dialog Colors
|
||||
| Hardcoded | Token |
|
||||
|-----------|-------|
|
||||
| Dark backgrounds | `var(--theme-color-bg-3)` |
|
||||
| Borders | `var(--theme-color-border-default)` |
|
||||
| Hover states | `var(--theme-color-bg-hover)` |
|
||||
|
||||
---
|
||||
|
||||
## Step 5: Check for Inline Styles in TSX/JSX
|
||||
|
||||
Some TypeScript/React files may have inline styles with hardcoded colors:
|
||||
|
||||
```bash
|
||||
# Find hardcoded colors in TypeScript files
|
||||
grep -rn "['\"](#[0-9a-fA-F]\{3,8\})['\"]" packages/noodl-editor/src/editor/src/views/ --include="*.tsx" --include="*.ts"
|
||||
```
|
||||
|
||||
If found, convert to CSS class or use CSS variables:
|
||||
|
||||
```tsx
|
||||
// BEFORE - Inline hardcoded
|
||||
<div style={{ background: '#1a1a1a' }}>
|
||||
|
||||
// AFTER - Use CSS variable
|
||||
<div style={{ background: 'var(--theme-color-bg-3)' }}>
|
||||
|
||||
// BEST - Use CSS class
|
||||
<div className={styles.container}>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
### InspectPopup
|
||||
- [ ] Opens correctly when debugging nodes
|
||||
- [ ] Text is readable
|
||||
- [ ] JSON syntax highlighting still works (if applicable)
|
||||
- [ ] Scrollable content works
|
||||
|
||||
### ConnectionPopup
|
||||
- [ ] Opens when dragging connections
|
||||
- [ ] List items readable and clickable
|
||||
- [ ] Hover states visible
|
||||
- [ ] Search/filter works (if applicable)
|
||||
|
||||
### Node Graph Editor
|
||||
- [ ] Node colors are distinguishable by type
|
||||
- [ ] Connection lines render correctly
|
||||
- [ ] Selection highlights visible
|
||||
- [ ] Canvas background correct
|
||||
- [ ] Zoom/pan doesn't break colors
|
||||
|
||||
### General
|
||||
- [ ] No CSS errors in console
|
||||
- [ ] No visual regressions
|
||||
- [ ] All interactive states work
|
||||
|
||||
---
|
||||
|
||||
## Verification Command
|
||||
|
||||
```bash
|
||||
# Check for remaining hardcoded colors in views
|
||||
grep -rn "#[0-9a-fA-F]\{6\}" packages/noodl-editor/src/editor/src/views/ --include="*.css" --include="*.scss" | grep -v "node-color" | wc -l
|
||||
```
|
||||
|
||||
**Target: 0 hardcoded UI colors (only intentional node-specific colors allowed)**
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- [ ] All UI colors in views directory use CSS variables
|
||||
- [ ] Node-specific colors use appropriate node tokens
|
||||
- [ ] Connection colors use signal/data tokens
|
||||
- [ ] Popups look consistent with rest of UI
|
||||
- [ ] No visual regressions in node editor
|
||||
@@ -0,0 +1,262 @@
|
||||
# TASK-000D: Hardcoded Color Audit - Core UI Components
|
||||
|
||||
## Overview
|
||||
|
||||
Find and replace all hardcoded hex colors in the shared Core UI component library. These components are used throughout the editor, so fixing them has wide impact.
|
||||
|
||||
**Priority:** HIGH
|
||||
**Effort:** 1-2 hours
|
||||
**Risk:** Low-Medium
|
||||
**Dependencies:** TASK-000A (Token Consolidation)
|
||||
|
||||
---
|
||||
|
||||
## Objective
|
||||
|
||||
Replace all hardcoded hex color values in `noodl-core-ui` components with CSS variable references, ensuring consistent theming across all shared components.
|
||||
|
||||
---
|
||||
|
||||
## Target Directory
|
||||
|
||||
```
|
||||
packages/noodl-core-ui/src/components/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Find All Hardcoded Colors
|
||||
|
||||
```bash
|
||||
# Find all hex colors in core-ui components
|
||||
grep -rn "#[0-9a-fA-F]\{3,8\}" packages/noodl-core-ui/src/components/ --include="*.css" --include="*.scss"
|
||||
```
|
||||
|
||||
List total files to fix:
|
||||
```bash
|
||||
grep -rl "#[0-9a-fA-F]\{3,8\}" packages/noodl-core-ui/src/components/ --include="*.css" --include="*.scss"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Component Categories to Audit
|
||||
|
||||
### Input Components (`inputs/`)
|
||||
Priority files:
|
||||
- `PrimaryButton/PrimaryButton.module.scss`
|
||||
- `TextInput/TextInput.module.scss`
|
||||
- `Select/Select.module.scss`
|
||||
- `Checkbox/Checkbox.module.scss`
|
||||
- `Slider/Slider.module.scss`
|
||||
|
||||
### Layout Components (`layout/`)
|
||||
Priority files:
|
||||
- `BaseDialog/BaseDialog.module.scss`
|
||||
- `DialogRenderRoot/DialogRenderRoot.module.scss`
|
||||
- `Container/Container.module.scss`
|
||||
|
||||
### Sidebar Components (`sidebar/`)
|
||||
Priority files:
|
||||
- `BasePanel/BasePanel.module.scss`
|
||||
- `Section/Section.module.scss`
|
||||
- `SidebarItem/SidebarItem.module.scss`
|
||||
|
||||
### Typography Components (`typography/`)
|
||||
- `Text/Text.module.scss`
|
||||
- `Label/Label.module.scss`
|
||||
- `Title/Title.module.scss`
|
||||
|
||||
### Common Components (`common/`)
|
||||
- `Icon/Icon.module.scss`
|
||||
- `Tooltip/Tooltip.module.scss`
|
||||
- Any other shared components
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Color Mapping Reference
|
||||
|
||||
### Background Colors
|
||||
| Hardcoded | Token | Usage |
|
||||
|-----------|-------|-------|
|
||||
| `#000000` | `var(--theme-color-bg-0)` | Darkest backgrounds |
|
||||
| `#0a0a0a` | `var(--theme-color-bg-1)` | App background |
|
||||
| `#121212` | `var(--theme-color-bg-2)` | Panel backgrounds |
|
||||
| `#1a1a1a` | `var(--theme-color-bg-3)` | Input/card backgrounds |
|
||||
| `#262626` | `var(--theme-color-bg-4)` | Elevated elements |
|
||||
| `#333333` | `var(--theme-color-bg-5)` | Highest elevation |
|
||||
|
||||
### Foreground Colors
|
||||
| Hardcoded | Token | Usage |
|
||||
|-----------|-------|-------|
|
||||
| `#ffffff` | `var(--theme-color-fg-highlight)` | Bright text |
|
||||
| `#e5e5e5` | `var(--theme-color-fg-default-contrast)` | High contrast |
|
||||
| `#d4d4d4` | `var(--theme-color-fg-default)` | Default text |
|
||||
| `#a3a3a3` | `var(--theme-color-fg-default-shy)` | Secondary text |
|
||||
| `#737373` | `var(--theme-color-fg-muted)` | Muted/disabled |
|
||||
|
||||
### Primary/Accent Colors
|
||||
| Hardcoded | Token | Usage |
|
||||
|-----------|-------|-------|
|
||||
| Old yellow/teal | `var(--theme-color-primary)` | Now red primary |
|
||||
| `#d21f3c` | `var(--theme-color-primary)` | Primary buttons |
|
||||
| Any purple/violet | `var(--theme-color-secondary)` | Now white |
|
||||
|
||||
### Button-Specific
|
||||
| State | Token |
|
||||
|-------|-------|
|
||||
| Default BG | `var(--theme-color-bg-4)` or `var(--theme-color-primary)` |
|
||||
| Hover BG | `var(--theme-color-bg-5)` or `var(--theme-color-primary-highlight)` |
|
||||
| Active BG | `var(--theme-color-bg-3)` or `var(--theme-color-primary-dim)` |
|
||||
| Disabled | Use opacity or `var(--theme-color-fg-muted)` |
|
||||
|
||||
### Input-Specific
|
||||
| Element | Token |
|
||||
|---------|-------|
|
||||
| Background | `var(--theme-color-bg-3)` |
|
||||
| Border | `var(--theme-color-border-default)` |
|
||||
| Border focused | `var(--theme-color-focus-ring)` |
|
||||
| Placeholder | `var(--theme-color-fg-muted)` |
|
||||
|
||||
---
|
||||
|
||||
## Step 4: Examples
|
||||
|
||||
### Button Before/After
|
||||
```scss
|
||||
// BEFORE
|
||||
.button {
|
||||
background: #363636;
|
||||
color: #ffffff;
|
||||
|
||||
&:hover {
|
||||
background: #444444;
|
||||
}
|
||||
}
|
||||
|
||||
// AFTER
|
||||
.button {
|
||||
background: var(--theme-color-bg-5);
|
||||
color: var(--theme-color-fg-highlight);
|
||||
|
||||
&:hover {
|
||||
background: var(--theme-color-bg-hover);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Input Before/After
|
||||
```scss
|
||||
// BEFORE
|
||||
.input {
|
||||
background: #1a1a1a;
|
||||
border: 1px solid #333333;
|
||||
color: #d4d4d4;
|
||||
|
||||
&::placeholder {
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: #d21f3c;
|
||||
}
|
||||
}
|
||||
|
||||
// AFTER
|
||||
.input {
|
||||
background: var(--theme-color-bg-3);
|
||||
border: 1px solid var(--theme-color-border-default);
|
||||
color: var(--theme-color-fg-default);
|
||||
|
||||
&::placeholder {
|
||||
color: var(--theme-color-fg-muted);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: var(--theme-color-focus-ring);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 5: Check Storybook
|
||||
|
||||
After updating components, verify in Storybook:
|
||||
|
||||
```bash
|
||||
npm run storybook
|
||||
```
|
||||
|
||||
Navigate to each updated component and check:
|
||||
- Default state renders correctly
|
||||
- All variants look correct
|
||||
- Interactive states work (hover, focus, active, disabled)
|
||||
- Dark theme shows proper contrast
|
||||
|
||||
---
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
### Per Component Type
|
||||
|
||||
#### Buttons
|
||||
- [ ] Primary button is red (`#d21f3c`)
|
||||
- [ ] Secondary button is neutral/white
|
||||
- [ ] Hover states visible
|
||||
- [ ] Focus ring visible
|
||||
- [ ] Disabled state clear
|
||||
|
||||
#### Inputs
|
||||
- [ ] Input background visible
|
||||
- [ ] Border visible
|
||||
- [ ] Focus state shows red ring
|
||||
- [ ] Placeholder text visible but muted
|
||||
- [ ] Error state shows red
|
||||
|
||||
#### Dialogs
|
||||
- [ ] Dialog background distinct from page
|
||||
- [ ] Backdrop visible
|
||||
- [ ] Header/body/footer sections clear
|
||||
- [ ] Close button visible
|
||||
|
||||
#### Panels/Sidebar
|
||||
- [ ] Panel backgrounds correct
|
||||
- [ ] Section headers readable
|
||||
- [ ] Hover states on items
|
||||
- [ ] Active/selected state visible
|
||||
|
||||
### Global Tests
|
||||
- [ ] Storybook renders without errors
|
||||
- [ ] All component stories pass visual check
|
||||
- [ ] No broken contrast (text unreadable)
|
||||
|
||||
---
|
||||
|
||||
## Verification Command
|
||||
|
||||
```bash
|
||||
# Check for remaining hardcoded colors
|
||||
grep -rn "#[0-9a-fA-F]\{6\}" packages/noodl-core-ui/src/components/ --include="*.css" --include="*.scss" | wc -l
|
||||
```
|
||||
|
||||
**Target: 0 hardcoded colors**
|
||||
|
||||
---
|
||||
|
||||
## Documentation
|
||||
|
||||
If you need to add any new tokens to handle edge cases, document them:
|
||||
|
||||
1. Add token to `colors.css`
|
||||
2. Update this task's notes
|
||||
3. Add comment explaining the token's purpose
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- [ ] All components in `noodl-core-ui` use CSS variables
|
||||
- [ ] Storybook shows all components correctly
|
||||
- [ ] No hardcoded hex colors in component styles
|
||||
- [ ] Consistent appearance across all components
|
||||
- [ ] Ready for visual refinements (TASK-000F, 000G)
|
||||
@@ -0,0 +1,337 @@
|
||||
# TASK-000E: Typography & Spacing Tokens
|
||||
|
||||
## Overview
|
||||
|
||||
Add comprehensive typography and spacing token systems to enable consistent sizing across the application. This lays the foundation for future UI refinements.
|
||||
|
||||
**Priority:** MEDIUM
|
||||
**Effort:** 1 hour
|
||||
**Risk:** Low
|
||||
**Dependencies:** TASK-000A (Token Consolidation)
|
||||
|
||||
---
|
||||
|
||||
## Objective
|
||||
|
||||
Create a robust system of typography and spacing tokens that components can use for consistent sizing, spacing, and visual rhythm throughout the editor.
|
||||
|
||||
---
|
||||
|
||||
## Part 1: Update Font Tokens
|
||||
|
||||
### File to Modify
|
||||
```
|
||||
packages/noodl-core-ui/src/styles/custom-properties/fonts.css
|
||||
```
|
||||
|
||||
### New Font Token System
|
||||
|
||||
Replace contents with:
|
||||
|
||||
```css
|
||||
/* =============================================================================
|
||||
NOODL DESIGN SYSTEM - TYPOGRAPHY
|
||||
============================================================================= */
|
||||
|
||||
:root {
|
||||
/* ---------------------------------------------------------------------------
|
||||
FONT FAMILIES
|
||||
--------------------------------------------------------------------------- */
|
||||
--font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
--font-family-code: 'JetBrains Mono', 'Fira Code', Menlo, Monaco, 'Courier New', monospace;
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
FONT WEIGHTS
|
||||
--------------------------------------------------------------------------- */
|
||||
--font-weight-light: 300;
|
||||
--font-weight-regular: 400;
|
||||
--font-weight-medium: 500;
|
||||
--font-weight-semibold: 600;
|
||||
--font-weight-bold: 700;
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
FONT SIZES
|
||||
Fluid scale from 10px to 24px
|
||||
--------------------------------------------------------------------------- */
|
||||
--font-size-xs: 10px; /* Small labels, hints */
|
||||
--font-size-sm: 11px; /* Secondary text, captions */
|
||||
--font-size-base: 12px; /* Default body text */
|
||||
--font-size-md: 13px; /* Emphasized body text */
|
||||
--font-size-lg: 14px; /* Section titles, important */
|
||||
--font-size-xl: 16px; /* Panel titles */
|
||||
--font-size-2xl: 18px; /* Dialog titles */
|
||||
--font-size-3xl: 24px; /* Page titles, hero text */
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
LINE HEIGHTS
|
||||
--------------------------------------------------------------------------- */
|
||||
--line-height-none: 1;
|
||||
--line-height-tight: 1.2;
|
||||
--line-height-snug: 1.375;
|
||||
--line-height-normal: 1.5;
|
||||
--line-height-relaxed: 1.625;
|
||||
--line-height-loose: 2;
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
LETTER SPACING
|
||||
--------------------------------------------------------------------------- */
|
||||
--letter-spacing-tighter: -0.05em;
|
||||
--letter-spacing-tight: -0.025em;
|
||||
--letter-spacing-normal: 0;
|
||||
--letter-spacing-wide: 0.025em;
|
||||
--letter-spacing-wider: 0.05em;
|
||||
--letter-spacing-widest: 0.1em;
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
SEMANTIC TEXT STYLES
|
||||
Pre-composed styles for common use cases
|
||||
--------------------------------------------------------------------------- */
|
||||
|
||||
/* Body text */
|
||||
--text-body-size: var(--font-size-base);
|
||||
--text-body-weight: var(--font-weight-regular);
|
||||
--text-body-line-height: var(--line-height-normal);
|
||||
|
||||
/* Small text */
|
||||
--text-small-size: var(--font-size-sm);
|
||||
--text-small-weight: var(--font-weight-regular);
|
||||
--text-small-line-height: var(--line-height-normal);
|
||||
|
||||
/* Labels */
|
||||
--text-label-size: var(--font-size-xs);
|
||||
--text-label-weight: var(--font-weight-medium);
|
||||
--text-label-letter-spacing: var(--letter-spacing-wide);
|
||||
|
||||
/* Code */
|
||||
--text-code-size: var(--font-size-sm);
|
||||
--text-code-family: var(--font-family-code);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Part 2: Create Spacing Tokens
|
||||
|
||||
### File to Create
|
||||
```
|
||||
packages/noodl-core-ui/src/styles/custom-properties/spacing.css
|
||||
```
|
||||
|
||||
### Content
|
||||
|
||||
```css
|
||||
/* =============================================================================
|
||||
NOODL DESIGN SYSTEM - SPACING
|
||||
============================================================================= */
|
||||
|
||||
:root {
|
||||
/* ---------------------------------------------------------------------------
|
||||
SPACING SCALE
|
||||
4px base unit system
|
||||
--------------------------------------------------------------------------- */
|
||||
--spacing-0: 0;
|
||||
--spacing-px: 1px;
|
||||
--spacing-0-5: 2px;
|
||||
--spacing-1: 4px;
|
||||
--spacing-1-5: 6px;
|
||||
--spacing-2: 8px;
|
||||
--spacing-2-5: 10px;
|
||||
--spacing-3: 12px;
|
||||
--spacing-3-5: 14px;
|
||||
--spacing-4: 16px;
|
||||
--spacing-5: 20px;
|
||||
--spacing-6: 24px;
|
||||
--spacing-7: 28px;
|
||||
--spacing-8: 32px;
|
||||
--spacing-9: 36px;
|
||||
--spacing-10: 40px;
|
||||
--spacing-11: 44px;
|
||||
--spacing-12: 48px;
|
||||
--spacing-14: 56px;
|
||||
--spacing-16: 64px;
|
||||
--spacing-20: 80px;
|
||||
--spacing-24: 96px;
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
SEMANTIC SPACING
|
||||
Component-specific spacing aliases
|
||||
--------------------------------------------------------------------------- */
|
||||
|
||||
/* Panel spacing */
|
||||
--spacing-panel-padding: var(--spacing-4);
|
||||
--spacing-panel-gap: var(--spacing-3);
|
||||
|
||||
/* Card spacing */
|
||||
--spacing-card-padding: var(--spacing-3);
|
||||
--spacing-card-gap: var(--spacing-2);
|
||||
|
||||
/* Section spacing */
|
||||
--spacing-section-gap: var(--spacing-6);
|
||||
--spacing-section-padding: var(--spacing-4);
|
||||
|
||||
/* Input spacing */
|
||||
--spacing-input-padding-x: var(--spacing-2);
|
||||
--spacing-input-padding-y: var(--spacing-1-5);
|
||||
--spacing-input-gap: var(--spacing-2);
|
||||
|
||||
/* Button spacing */
|
||||
--spacing-button-padding-x: var(--spacing-3);
|
||||
--spacing-button-padding-y: var(--spacing-2);
|
||||
--spacing-button-gap: var(--spacing-2);
|
||||
|
||||
/* Icon spacing */
|
||||
--spacing-icon-gap: var(--spacing-2);
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
BORDER RADIUS
|
||||
--------------------------------------------------------------------------- */
|
||||
--radius-none: 0;
|
||||
--radius-sm: 2px;
|
||||
--radius-default: 4px;
|
||||
--radius-md: 6px;
|
||||
--radius-lg: 8px;
|
||||
--radius-xl: 12px;
|
||||
--radius-2xl: 16px;
|
||||
--radius-3xl: 24px;
|
||||
--radius-full: 9999px;
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
SHADOWS
|
||||
--------------------------------------------------------------------------- */
|
||||
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
--shadow-default: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1);
|
||||
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1);
|
||||
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1);
|
||||
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
|
||||
--shadow-2xl: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
||||
--shadow-inner: inset 0 2px 4px 0 rgba(0, 0, 0, 0.05);
|
||||
|
||||
/* Dialog/popup shadow */
|
||||
--shadow-popup: 0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
||||
0 10px 15px -3px rgba(0, 0, 0, 0.2),
|
||||
0 20px 25px -5px rgba(0, 0, 0, 0.15);
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
TRANSITIONS
|
||||
--------------------------------------------------------------------------- */
|
||||
--transition-fast: 100ms;
|
||||
--transition-default: 150ms;
|
||||
--transition-slow: 300ms;
|
||||
--transition-ease: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--transition-ease-in: cubic-bezier(0.4, 0, 1, 1);
|
||||
--transition-ease-out: cubic-bezier(0, 0, 0.2, 1);
|
||||
--transition-ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
Z-INDEX SCALE
|
||||
--------------------------------------------------------------------------- */
|
||||
--z-base: 0;
|
||||
--z-dropdown: 100;
|
||||
--z-sticky: 200;
|
||||
--z-fixed: 300;
|
||||
--z-modal-backdrop: 400;
|
||||
--z-modal: 500;
|
||||
--z-popover: 600;
|
||||
--z-tooltip: 700;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Part 3: Update Import Statements
|
||||
|
||||
### Editor Entry Point
|
||||
File: `packages/noodl-editor/src/editor/index.ts`
|
||||
|
||||
Add import:
|
||||
```typescript
|
||||
import '../editor/src/styles/custom-properties/spacing.css';
|
||||
```
|
||||
|
||||
### Core UI Entry (if exists)
|
||||
Check `packages/noodl-core-ui/src/index.ts` or similar and add spacing import.
|
||||
|
||||
### Storybook Preview
|
||||
File: `packages/noodl-core-ui/.storybook/preview.ts`
|
||||
|
||||
Ensure spacing.css is imported:
|
||||
```typescript
|
||||
import '../src/styles/custom-properties/spacing.css';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Part 4: Also Update Editor's Font File
|
||||
|
||||
File: `packages/noodl-editor/src/editor/src/styles/custom-properties/fonts.css`
|
||||
|
||||
Should contain the same content as the core-ui fonts.css (or be deleted and import from core-ui).
|
||||
|
||||
---
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
### Token Availability
|
||||
- [ ] All font tokens accessible in CSS (`var(--font-size-base)` works)
|
||||
- [ ] All spacing tokens accessible (`var(--spacing-4)` works)
|
||||
- [ ] Shadow tokens work
|
||||
- [ ] Transition tokens work
|
||||
|
||||
### Visual Check
|
||||
- [ ] Text sizes look appropriate
|
||||
- [ ] Default body text is readable
|
||||
- [ ] Code blocks use monospace font
|
||||
- [ ] Spacing feels balanced
|
||||
|
||||
### Build Check
|
||||
- [ ] No CSS compilation errors
|
||||
- [ ] No missing variable warnings
|
||||
- [ ] Storybook loads correctly
|
||||
- [ ] Editor builds successfully
|
||||
|
||||
---
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Using Font Tokens
|
||||
```scss
|
||||
.title {
|
||||
font-family: var(--font-family);
|
||||
font-size: var(--font-size-xl);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
line-height: var(--line-height-tight);
|
||||
}
|
||||
|
||||
.code {
|
||||
font-family: var(--font-family-code);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
```
|
||||
|
||||
### Using Spacing Tokens
|
||||
```scss
|
||||
.panel {
|
||||
padding: var(--spacing-panel-padding);
|
||||
}
|
||||
|
||||
.button {
|
||||
padding: var(--spacing-button-padding-y) var(--spacing-button-padding-x);
|
||||
gap: var(--spacing-button-gap);
|
||||
}
|
||||
|
||||
.card {
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- [ ] fonts.css contains comprehensive typography tokens
|
||||
- [ ] spacing.css is created with full spacing system
|
||||
- [ ] Both files imported in editor and storybook
|
||||
- [ ] No build errors
|
||||
- [ ] Tokens are usable in components
|
||||
- [ ] Ready for component visual updates (TASK-000F, 000G)
|
||||
@@ -0,0 +1,378 @@
|
||||
# TASK-000F: Component Visual Updates - Buttons & Inputs
|
||||
|
||||
## Overview
|
||||
|
||||
Apply visual refinements to button and input components to achieve a modern, polished feel. This builds on the token work done in previous tasks.
|
||||
|
||||
**Priority:** MEDIUM
|
||||
**Effort:** 1-2 hours
|
||||
**Risk:** Medium
|
||||
**Dependencies:** TASK-000A, TASK-000D, TASK-000E (Color tokens, Core UI audit, Spacing tokens)
|
||||
|
||||
---
|
||||
|
||||
## Objective
|
||||
|
||||
Make buttons and inputs feel modern and polished with:
|
||||
- Subtle rounded corners
|
||||
- Smooth transitions
|
||||
- Clear hover/focus states
|
||||
- Better disabled appearances
|
||||
- Consistent spacing using tokens
|
||||
|
||||
---
|
||||
|
||||
## Part 1: Button Refinements
|
||||
|
||||
### File
|
||||
```
|
||||
packages/noodl-core-ui/src/components/inputs/PrimaryButton/PrimaryButton.module.scss
|
||||
```
|
||||
|
||||
### Improvements to Apply
|
||||
|
||||
```scss
|
||||
.Root {
|
||||
/* Use spacing tokens for padding */
|
||||
padding: var(--spacing-button-padding-y) var(--spacing-button-padding-x);
|
||||
gap: var(--spacing-button-gap);
|
||||
|
||||
/* Modern rounded corners */
|
||||
border-radius: var(--radius-md);
|
||||
|
||||
/* Smooth transitions for all interactive states */
|
||||
transition:
|
||||
background-color var(--transition-default) var(--transition-ease),
|
||||
border-color var(--transition-default) var(--transition-ease),
|
||||
box-shadow var(--transition-default) var(--transition-ease),
|
||||
transform var(--transition-fast) var(--transition-ease);
|
||||
|
||||
/* Font styling */
|
||||
font-weight: var(--font-weight-medium);
|
||||
font-size: var(--font-size-base);
|
||||
line-height: var(--line-height-tight);
|
||||
|
||||
/* CTA (Primary Red) variant */
|
||||
&.is-variant-cta {
|
||||
background-color: var(--theme-color-primary);
|
||||
color: var(--theme-color-on-primary);
|
||||
border: 1px solid transparent;
|
||||
box-shadow: var(--shadow-sm);
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: var(--theme-color-primary-highlight);
|
||||
box-shadow: var(--shadow-default);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
background-color: var(--theme-color-primary-dim);
|
||||
box-shadow: none;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Secondary variant */
|
||||
&.is-variant-secondary {
|
||||
background-color: var(--theme-color-bg-4);
|
||||
color: var(--theme-color-fg-default);
|
||||
border: 1px solid var(--theme-color-border-default);
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: var(--theme-color-bg-5);
|
||||
border-color: var(--theme-color-border-strong);
|
||||
}
|
||||
}
|
||||
|
||||
/* Ghost variant */
|
||||
&.is-variant-ghost {
|
||||
background-color: transparent;
|
||||
color: var(--theme-color-fg-default);
|
||||
border: 1px solid transparent;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: var(--theme-color-bg-hover);
|
||||
}
|
||||
}
|
||||
|
||||
/* Disabled state - consistent across all variants */
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
transform: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
/* Focus visible - accessibility */
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--theme-color-focus-ring);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Other Button Components to Check
|
||||
- `SecondaryButton` (if exists)
|
||||
- `IconButton` (if exists)
|
||||
- `TextButton` (if exists)
|
||||
|
||||
---
|
||||
|
||||
## Part 2: Input Field Refinements
|
||||
|
||||
### File
|
||||
```
|
||||
packages/noodl-core-ui/src/components/inputs/TextInput/TextInput.module.scss
|
||||
```
|
||||
|
||||
### Improvements to Apply
|
||||
|
||||
```scss
|
||||
.Root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-1);
|
||||
}
|
||||
|
||||
.InputArea {
|
||||
/* Use spacing tokens */
|
||||
padding: var(--spacing-input-padding-y) var(--spacing-input-padding-x);
|
||||
|
||||
/* Background and border */
|
||||
background-color: var(--theme-color-bg-3);
|
||||
border: 1px solid var(--theme-color-border-default);
|
||||
border-radius: var(--radius-default);
|
||||
|
||||
/* Typography */
|
||||
font-family: var(--font-family);
|
||||
font-size: var(--font-size-base);
|
||||
color: var(--theme-color-fg-default);
|
||||
|
||||
/* Transitions */
|
||||
transition:
|
||||
border-color var(--transition-default) var(--transition-ease),
|
||||
box-shadow var(--transition-default) var(--transition-ease);
|
||||
|
||||
/* Placeholder styling */
|
||||
&::placeholder {
|
||||
color: var(--theme-color-fg-muted);
|
||||
}
|
||||
|
||||
/* Hover state */
|
||||
&:hover:not(:disabled):not(:focus) {
|
||||
border-color: var(--theme-color-border-strong);
|
||||
}
|
||||
|
||||
/* Focus state - prominent red ring */
|
||||
&:focus {
|
||||
border-color: var(--theme-color-focus-ring);
|
||||
box-shadow: 0 0 0 2px rgba(210, 31, 60, 0.15);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Error state */
|
||||
&.has-error {
|
||||
border-color: var(--theme-color-danger);
|
||||
|
||||
&:focus {
|
||||
box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
/* Disabled state */
|
||||
&:disabled {
|
||||
background-color: var(--theme-color-bg-2);
|
||||
color: var(--theme-color-fg-muted);
|
||||
cursor: not-allowed;
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
/* Label styling */
|
||||
.Label {
|
||||
font-size: var(--text-label-size);
|
||||
font-weight: var(--text-label-weight);
|
||||
letter-spacing: var(--text-label-letter-spacing);
|
||||
color: var(--theme-color-fg-default-shy);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
/* Helper/error text */
|
||||
.HelperText {
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--theme-color-fg-muted);
|
||||
|
||||
&.is-error {
|
||||
color: var(--theme-color-danger);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Other Input Components to Update
|
||||
- `Select/Select.module.scss`
|
||||
- `Checkbox/Checkbox.module.scss`
|
||||
- `Slider/Slider.module.scss`
|
||||
- `NumberInput` (if exists)
|
||||
- `TextArea` (if exists)
|
||||
|
||||
---
|
||||
|
||||
## Part 3: Select/Dropdown Refinements
|
||||
|
||||
### File
|
||||
```
|
||||
packages/noodl-core-ui/src/components/inputs/Select/Select.module.scss
|
||||
```
|
||||
|
||||
### Key Styles
|
||||
|
||||
```scss
|
||||
.SelectTrigger {
|
||||
/* Same base styling as TextInput */
|
||||
padding: var(--spacing-input-padding-y) var(--spacing-input-padding-x);
|
||||
background-color: var(--theme-color-bg-3);
|
||||
border: 1px solid var(--theme-color-border-default);
|
||||
border-radius: var(--radius-default);
|
||||
|
||||
/* Flex layout for icon */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--spacing-2);
|
||||
|
||||
transition: border-color var(--transition-default) var(--transition-ease);
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
border-color: var(--theme-color-border-strong);
|
||||
}
|
||||
|
||||
&.is-open {
|
||||
border-color: var(--theme-color-focus-ring);
|
||||
}
|
||||
}
|
||||
|
||||
.DropdownMenu {
|
||||
background-color: var(--theme-color-bg-3);
|
||||
border: 1px solid var(--theme-color-border-default);
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: var(--shadow-popup);
|
||||
|
||||
/* Ensure dropdown appears above other content */
|
||||
z-index: var(--z-dropdown);
|
||||
}
|
||||
|
||||
.DropdownItem {
|
||||
padding: var(--spacing-2) var(--spacing-3);
|
||||
color: var(--theme-color-fg-default);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--theme-color-bg-hover);
|
||||
}
|
||||
|
||||
&.is-selected {
|
||||
background-color: var(--theme-color-primary);
|
||||
color: var(--theme-color-on-primary);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Part 4: Checkbox Refinements
|
||||
|
||||
### File
|
||||
```
|
||||
packages/noodl-core-ui/src/components/inputs/Checkbox/Checkbox.module.scss
|
||||
```
|
||||
|
||||
### Key Styles
|
||||
|
||||
```scss
|
||||
.CheckboxBox {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 1px solid var(--theme-color-border-strong);
|
||||
border-radius: var(--radius-sm);
|
||||
background-color: var(--theme-color-bg-3);
|
||||
transition:
|
||||
background-color var(--transition-default) var(--transition-ease),
|
||||
border-color var(--transition-default) var(--transition-ease);
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
border-color: var(--theme-color-focus-ring);
|
||||
}
|
||||
|
||||
&.is-checked {
|
||||
background-color: var(--theme-color-primary);
|
||||
border-color: var(--theme-color-primary);
|
||||
|
||||
/* Checkmark icon should be white */
|
||||
color: var(--theme-color-on-primary);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
### Buttons
|
||||
- [ ] Primary (CTA) button is red with white text
|
||||
- [ ] Hover state brightens and lifts slightly
|
||||
- [ ] Active state darkens
|
||||
- [ ] Disabled state is clearly disabled (50% opacity)
|
||||
- [ ] Focus ring is visible and red
|
||||
- [ ] Secondary button has visible border
|
||||
- [ ] Ghost button has no background until hover
|
||||
|
||||
### Text Inputs
|
||||
- [ ] Input has visible background and border
|
||||
- [ ] Placeholder text is muted gray
|
||||
- [ ] Hover state shows stronger border
|
||||
- [ ] Focus state shows red ring
|
||||
- [ ] Error state shows red border
|
||||
- [ ] Disabled state is clearly disabled
|
||||
|
||||
### Selects
|
||||
- [ ] Dropdown trigger looks like input
|
||||
- [ ] Dropdown menu has shadow and border
|
||||
- [ ] Items have hover states
|
||||
- [ ] Selected item is highlighted
|
||||
|
||||
### Checkboxes
|
||||
- [ ] Unchecked box has visible border
|
||||
- [ ] Checked state shows red background
|
||||
- [ ] Hover state on unchecked shows border change
|
||||
|
||||
### General
|
||||
- [ ] All components use consistent spacing
|
||||
- [ ] All components use consistent border radius
|
||||
- [ ] Transitions are smooth, not jarring
|
||||
- [ ] Storybook shows all states correctly
|
||||
|
||||
---
|
||||
|
||||
## Verification in Storybook
|
||||
|
||||
```bash
|
||||
npm run storybook
|
||||
```
|
||||
|
||||
Navigate to:
|
||||
- Inputs / PrimaryButton
|
||||
- Inputs / TextInput
|
||||
- Inputs / Select
|
||||
- Inputs / Checkbox
|
||||
|
||||
Review all stories and variants.
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- [ ] Buttons feel modern and responsive
|
||||
- [ ] Inputs have clear, accessible focus states
|
||||
- [ ] All interactive states are smooth
|
||||
- [ ] Disabled states are obvious
|
||||
- [ ] Consistent use of tokens throughout
|
||||
- [ ] No visual regressions from previous functionality
|
||||
@@ -0,0 +1,437 @@
|
||||
# TASK-000G: Component Visual Updates - Dialogs & Panels
|
||||
|
||||
## Overview
|
||||
|
||||
Apply visual refinements to dialog, modal, and panel components to create a modern, elevated UI feel. These are high-visibility components that frame content throughout the editor.
|
||||
|
||||
**Priority:** MEDIUM
|
||||
**Effort:** 1-2 hours
|
||||
**Risk:** Medium
|
||||
**Dependencies:** TASK-000A, TASK-000D, TASK-000E (Color tokens, Core UI audit, Spacing tokens)
|
||||
|
||||
---
|
||||
|
||||
## Objective
|
||||
|
||||
Make dialogs and panels feel modern and elevated with:
|
||||
- Subtle borders for definition
|
||||
- Refined shadows for depth
|
||||
- Better backdrop styling
|
||||
- Consistent header/body/footer structure
|
||||
- Proper spacing using tokens
|
||||
|
||||
---
|
||||
|
||||
## Part 1: Dialog/Modal Refinements
|
||||
|
||||
### File
|
||||
```
|
||||
packages/noodl-core-ui/src/components/layout/BaseDialog/BaseDialog.module.scss
|
||||
```
|
||||
|
||||
### Improvements to Apply
|
||||
|
||||
```scss
|
||||
/* Dialog wrapper - handles backdrop */
|
||||
.Root {
|
||||
/* Backdrop styling */
|
||||
&.has-backdrop {
|
||||
background-color: var(--base-color-black-transparent-80);
|
||||
|
||||
/* Optional: subtle blur for modern feel */
|
||||
/* Note: may have performance implications */
|
||||
backdrop-filter: blur(4px);
|
||||
-webkit-backdrop-filter: blur(4px);
|
||||
}
|
||||
}
|
||||
|
||||
/* The visible dialog box */
|
||||
.VisibleDialog {
|
||||
/* Background */
|
||||
background-color: var(--theme-color-bg-2);
|
||||
|
||||
/* Border for definition against backdrop */
|
||||
border: 1px solid var(--theme-color-border-subtle);
|
||||
|
||||
/* Modern rounded corners */
|
||||
border-radius: var(--radius-lg);
|
||||
|
||||
/* Elevated shadow */
|
||||
box-shadow: var(--shadow-popup);
|
||||
|
||||
/* Overflow handling */
|
||||
overflow: hidden;
|
||||
|
||||
/* Maximum size constraints */
|
||||
max-height: 90vh;
|
||||
max-width: 90vw;
|
||||
}
|
||||
|
||||
/* Dialog header */
|
||||
.DialogHeader {
|
||||
padding: var(--spacing-4) var(--spacing-5);
|
||||
border-bottom: 1px solid var(--theme-color-border-subtle);
|
||||
|
||||
/* Flex layout for title + close button */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--spacing-4);
|
||||
}
|
||||
|
||||
.DialogTitle {
|
||||
font-size: var(--font-size-2xl);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--theme-color-fg-highlight);
|
||||
line-height: var(--line-height-tight);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.DialogSubtitle {
|
||||
font-size: var(--font-size-base);
|
||||
color: var(--theme-color-fg-default-shy);
|
||||
margin-top: var(--spacing-1);
|
||||
}
|
||||
|
||||
/* Dialog body */
|
||||
.DialogBody {
|
||||
padding: var(--spacing-5);
|
||||
overflow-y: auto;
|
||||
|
||||
/* Smooth scrolling */
|
||||
scroll-behavior: smooth;
|
||||
|
||||
/* Scrollbar styling (webkit) */
|
||||
&::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: var(--theme-color-bg-3);
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--theme-color-bg-5);
|
||||
border-radius: var(--radius-full);
|
||||
|
||||
&:hover {
|
||||
background: var(--theme-color-fg-muted);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Dialog footer */
|
||||
.DialogFooter {
|
||||
padding: var(--spacing-4) var(--spacing-5);
|
||||
border-top: 1px solid var(--theme-color-border-subtle);
|
||||
background-color: var(--theme-color-bg-1);
|
||||
|
||||
/* Flex layout for buttons */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: var(--spacing-3);
|
||||
}
|
||||
|
||||
/* Close button in header */
|
||||
.CloseButton {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: var(--radius-default);
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--theme-color-fg-muted);
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background-color var(--transition-default) var(--transition-ease),
|
||||
color var(--transition-default) var(--transition-ease);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--theme-color-bg-hover);
|
||||
color: var(--theme-color-fg-default);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Part 2: Panel Refinements
|
||||
|
||||
### Files
|
||||
```
|
||||
packages/noodl-core-ui/src/components/sidebar/BasePanel/BasePanel.module.scss
|
||||
packages/noodl-core-ui/src/components/sidebar/Section/Section.module.scss
|
||||
```
|
||||
|
||||
### BasePanel Improvements
|
||||
|
||||
```scss
|
||||
.Root {
|
||||
/* Background */
|
||||
background-color: var(--theme-color-bg-2);
|
||||
|
||||
/* Border for definition */
|
||||
border: 1px solid var(--theme-color-border-subtle);
|
||||
border-radius: var(--radius-md);
|
||||
|
||||
/* Consistent padding */
|
||||
padding: var(--spacing-panel-padding);
|
||||
|
||||
/* Panel gap between children */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-panel-gap);
|
||||
}
|
||||
|
||||
.PanelHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-bottom: var(--spacing-3);
|
||||
border-bottom: 1px solid var(--theme-color-border-subtle);
|
||||
margin-bottom: var(--spacing-2);
|
||||
}
|
||||
|
||||
.PanelTitle {
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--theme-color-fg-default-contrast);
|
||||
}
|
||||
```
|
||||
|
||||
### Section Improvements
|
||||
|
||||
```scss
|
||||
.Root {
|
||||
/* Section spacing */
|
||||
padding: var(--spacing-section-padding) 0;
|
||||
|
||||
/* Border between sections */
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px solid var(--theme-color-border-subtle);
|
||||
}
|
||||
}
|
||||
|
||||
.SectionHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: var(--spacing-3);
|
||||
}
|
||||
|
||||
.SectionTitle {
|
||||
font-size: var(--text-label-size);
|
||||
font-weight: var(--text-label-weight);
|
||||
letter-spacing: var(--text-label-letter-spacing);
|
||||
color: var(--theme-color-fg-muted);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.SectionContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-2);
|
||||
}
|
||||
|
||||
/* Collapsible section */
|
||||
.SectionToggle {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
|
||||
&:hover .SectionTitle {
|
||||
color: var(--theme-color-fg-default-shy);
|
||||
}
|
||||
}
|
||||
|
||||
.CollapseIcon {
|
||||
transition: transform var(--transition-default) var(--transition-ease);
|
||||
|
||||
&.is-collapsed {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Part 3: Sidebar Item Refinements
|
||||
|
||||
### File
|
||||
```
|
||||
packages/noodl-core-ui/src/components/sidebar/SidebarItem/SidebarItem.module.scss
|
||||
```
|
||||
|
||||
### Improvements
|
||||
|
||||
```scss
|
||||
.Root {
|
||||
padding: var(--spacing-2) var(--spacing-3);
|
||||
border-radius: var(--radius-default);
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
|
||||
color: var(--theme-color-fg-default);
|
||||
|
||||
transition:
|
||||
background-color var(--transition-default) var(--transition-ease),
|
||||
color var(--transition-default) var(--transition-ease);
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--theme-color-bg-hover);
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
background-color: var(--theme-color-primary);
|
||||
color: var(--theme-color-on-primary);
|
||||
}
|
||||
|
||||
&.is-selected {
|
||||
background-color: var(--theme-color-bg-4);
|
||||
color: var(--theme-color-fg-highlight);
|
||||
}
|
||||
}
|
||||
|
||||
.ItemIcon {
|
||||
flex-shrink: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: inherit;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.ItemLabel {
|
||||
flex: 1;
|
||||
font-size: var(--font-size-base);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.ItemBadge {
|
||||
flex-shrink: 0;
|
||||
font-size: var(--font-size-xs);
|
||||
padding: var(--spacing-0-5) var(--spacing-1);
|
||||
background-color: var(--theme-color-bg-5);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--theme-color-fg-default-shy);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Part 4: Tooltip Refinements
|
||||
|
||||
### File
|
||||
```
|
||||
packages/noodl-core-ui/src/components/common/Tooltip/Tooltip.module.scss
|
||||
```
|
||||
|
||||
### Improvements
|
||||
|
||||
```scss
|
||||
.Root {
|
||||
background-color: var(--theme-color-bg-4);
|
||||
color: var(--theme-color-fg-default);
|
||||
|
||||
padding: var(--spacing-1-5) var(--spacing-2);
|
||||
border-radius: var(--radius-default);
|
||||
|
||||
font-size: var(--font-size-sm);
|
||||
line-height: var(--line-height-normal);
|
||||
|
||||
box-shadow: var(--shadow-md);
|
||||
border: 1px solid var(--theme-color-border-default);
|
||||
|
||||
/* Ensure tooltip is above everything */
|
||||
z-index: var(--z-tooltip);
|
||||
|
||||
/* Max width for long content */
|
||||
max-width: 250px;
|
||||
}
|
||||
|
||||
/* Arrow/pointer if applicable */
|
||||
.Arrow {
|
||||
fill: var(--theme-color-bg-4);
|
||||
stroke: var(--theme-color-border-default);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
### Dialogs/Modals
|
||||
- [ ] Dialog has visible border
|
||||
- [ ] Shadow creates sense of elevation
|
||||
- [ ] Backdrop is semi-transparent dark
|
||||
- [ ] Backdrop blur works (if enabled)
|
||||
- [ ] Header/body/footer clearly separated
|
||||
- [ ] Title text is prominent
|
||||
- [ ] Close button works and has hover state
|
||||
- [ ] Footer buttons aligned correctly
|
||||
- [ ] Scrollable body works for long content
|
||||
- [ ] Focus trapped inside dialog
|
||||
|
||||
### Panels
|
||||
- [ ] Panel has subtle border
|
||||
- [ ] Header section distinct from content
|
||||
- [ ] Section titles are uppercase/muted
|
||||
- [ ] Content areas have proper spacing
|
||||
- [ ] Collapsible sections animate smoothly
|
||||
|
||||
### Sidebar Items
|
||||
- [ ] Items have hover states
|
||||
- [ ] Active item clearly highlighted (red)
|
||||
- [ ] Selected item distinct from hover
|
||||
- [ ] Icons aligned with text
|
||||
- [ ] Overflow text truncates with ellipsis
|
||||
|
||||
### Tooltips
|
||||
- [ ] Tooltip has border and shadow
|
||||
- [ ] Text is readable
|
||||
- [ ] Position correctly relative to trigger
|
||||
- [ ] Arrow points to trigger (if applicable)
|
||||
|
||||
### General
|
||||
- [ ] Consistent border radius across all components
|
||||
- [ ] Consistent border colors
|
||||
- [ ] Smooth transitions
|
||||
- [ ] Storybook shows all variations correctly
|
||||
|
||||
---
|
||||
|
||||
## Verification in Storybook
|
||||
|
||||
```bash
|
||||
npm run storybook
|
||||
```
|
||||
|
||||
Navigate to:
|
||||
- Layout / BaseDialog
|
||||
- Sidebar / BasePanel
|
||||
- Sidebar / Section
|
||||
- Sidebar / SidebarItem
|
||||
- Common / Tooltip
|
||||
|
||||
Review all stories and variants.
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- [ ] Dialogs feel elevated and professional
|
||||
- [ ] Panels have clear visual structure
|
||||
- [ ] Sections organize content clearly
|
||||
- [ ] Sidebar items are interactive and obvious
|
||||
- [ ] Tooltips are readable and well-positioned
|
||||
- [ ] Consistent use of tokens throughout
|
||||
- [ ] No visual regressions from previous functionality
|
||||
@@ -0,0 +1,535 @@
|
||||
# TASK-000H: Migration Wizard Polish
|
||||
|
||||
## Overview
|
||||
|
||||
Final polish pass on the React 19 Migration Wizard dialog to ensure it looks professional and provides clear user guidance. This is an important user-facing feature.
|
||||
|
||||
**Priority:** HIGH (User-facing feature)
|
||||
**Effort:** 1-2 hours
|
||||
**Risk:** Low
|
||||
**Dependencies:** TASK-000A through TASK-000G (All token and component updates)
|
||||
|
||||
---
|
||||
|
||||
## Objective
|
||||
|
||||
Ensure the Migration Wizard:
|
||||
- Uses the new design tokens consistently
|
||||
- Has clear visual hierarchy
|
||||
- Provides obvious progress indication
|
||||
- Shows success/error states clearly
|
||||
- Looks polished and professional
|
||||
|
||||
---
|
||||
|
||||
## Migration Wizard Files
|
||||
|
||||
### Main Component
|
||||
```
|
||||
packages/noodl-editor/src/editor/src/views/migration/MigrationWizard.tsx
|
||||
packages/noodl-editor/src/editor/src/views/migration/MigrationWizard.module.scss
|
||||
```
|
||||
|
||||
### Step Components
|
||||
```
|
||||
packages/noodl-editor/src/editor/src/views/migration/steps/ConfirmStep.tsx
|
||||
packages/noodl-editor/src/editor/src/views/migration/steps/ConfirmStep.module.scss
|
||||
|
||||
packages/noodl-editor/src/editor/src/views/migration/steps/ScanningStep.tsx
|
||||
packages/noodl-editor/src/editor/src/views/migration/steps/ScanningStep.module.scss
|
||||
|
||||
packages/noodl-editor/src/editor/src/views/migration/steps/ReportStep.tsx
|
||||
packages/noodl-editor/src/editor/src/views/migration/steps/ReportStep.module.scss
|
||||
|
||||
packages/noodl-editor/src/editor/src/views/migration/steps/CompleteStep.tsx
|
||||
packages/noodl-editor/src/editor/src/views/migration/steps/CompleteStep.module.scss
|
||||
|
||||
packages/noodl-editor/src/editor/src/views/migration/steps/FailedStep.tsx
|
||||
packages/noodl-editor/src/editor/src/views/migration/steps/FailedStep.module.scss
|
||||
```
|
||||
|
||||
### Supporting Components
|
||||
```
|
||||
packages/noodl-editor/src/editor/src/views/migration/components/WizardProgress.tsx
|
||||
packages/noodl-editor/src/editor/src/views/migration/components/WizardProgress.module.scss
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Part 1: Wizard Progress Indicator
|
||||
|
||||
### Ensure Progress Uses Tokens
|
||||
|
||||
```scss
|
||||
/* WizardProgress.module.scss */
|
||||
|
||||
.Root {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
padding: var(--spacing-3) var(--spacing-4);
|
||||
background-color: var(--theme-color-bg-1);
|
||||
border-bottom: 1px solid var(--theme-color-border-subtle);
|
||||
}
|
||||
|
||||
.StepItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
}
|
||||
|
||||
.StepNumber {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: var(--radius-full);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-medium);
|
||||
transition:
|
||||
background-color var(--transition-default) var(--transition-ease),
|
||||
color var(--transition-default) var(--transition-ease);
|
||||
|
||||
/* Default (pending) state */
|
||||
background-color: var(--theme-color-bg-4);
|
||||
color: var(--theme-color-fg-muted);
|
||||
|
||||
/* Active state */
|
||||
&.is-active {
|
||||
background-color: var(--theme-color-primary);
|
||||
color: var(--theme-color-on-primary);
|
||||
}
|
||||
|
||||
/* Completed state */
|
||||
&.is-complete {
|
||||
background-color: var(--theme-color-success);
|
||||
color: var(--theme-color-fg-highlight);
|
||||
}
|
||||
|
||||
/* Error state */
|
||||
&.is-error {
|
||||
background-color: var(--theme-color-danger);
|
||||
color: var(--theme-color-fg-highlight);
|
||||
}
|
||||
}
|
||||
|
||||
.StepLabel {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--theme-color-fg-muted);
|
||||
|
||||
&.is-active {
|
||||
color: var(--theme-color-fg-default);
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
&.is-complete {
|
||||
color: var(--theme-color-success);
|
||||
}
|
||||
}
|
||||
|
||||
.StepConnector {
|
||||
flex: 1;
|
||||
height: 2px;
|
||||
background-color: var(--theme-color-bg-4);
|
||||
min-width: 20px;
|
||||
|
||||
&.is-complete {
|
||||
background-color: var(--theme-color-success);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Part 2: Step Containers
|
||||
|
||||
### Shared Step Styles
|
||||
|
||||
Create a shared pattern for all step containers:
|
||||
|
||||
```scss
|
||||
/* Shared concept for each step's .module.scss */
|
||||
|
||||
.StepContainer {
|
||||
padding: var(--spacing-6);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-4);
|
||||
}
|
||||
|
||||
.StepTitle {
|
||||
font-size: var(--font-size-xl);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--theme-color-fg-highlight);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.StepDescription {
|
||||
font-size: var(--font-size-base);
|
||||
color: var(--theme-color-fg-default-shy);
|
||||
line-height: var(--line-height-relaxed);
|
||||
}
|
||||
|
||||
.StepContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-4);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Part 3: Success Banner (CompleteStep)
|
||||
|
||||
```scss
|
||||
/* CompleteStep.module.scss */
|
||||
|
||||
.SuccessBanner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-3);
|
||||
padding: var(--spacing-4);
|
||||
background-color: var(--theme-color-success-bg);
|
||||
border: 1px solid var(--theme-color-success-dim);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.SuccessIcon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
color: var(--theme-color-success);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.SuccessText {
|
||||
font-size: var(--font-size-base);
|
||||
color: var(--theme-color-success);
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
/* Stats display */
|
||||
.StatsCard {
|
||||
background-color: var(--theme-color-bg-3);
|
||||
border: 1px solid var(--theme-color-border-default);
|
||||
border-radius: var(--radius-md);
|
||||
padding: var(--spacing-4);
|
||||
}
|
||||
|
||||
.StatItem {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: var(--spacing-2);
|
||||
}
|
||||
|
||||
.StatValue {
|
||||
font-size: var(--font-size-2xl);
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--theme-color-fg-highlight);
|
||||
}
|
||||
|
||||
.StatLabel {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--theme-color-fg-muted);
|
||||
}
|
||||
|
||||
/* What's next section */
|
||||
.NextStepsSection {
|
||||
padding-top: var(--spacing-4);
|
||||
border-top: 1px solid var(--theme-color-border-subtle);
|
||||
}
|
||||
|
||||
.NextStepsTitle {
|
||||
font-size: var(--text-label-size);
|
||||
font-weight: var(--text-label-weight);
|
||||
letter-spacing: var(--text-label-letter-spacing);
|
||||
color: var(--theme-color-fg-muted);
|
||||
text-transform: uppercase;
|
||||
margin-bottom: var(--spacing-3);
|
||||
}
|
||||
|
||||
.ChecklistItem {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--spacing-2);
|
||||
padding: var(--spacing-1-5) 0;
|
||||
|
||||
font-size: var(--font-size-base);
|
||||
color: var(--theme-color-fg-default);
|
||||
}
|
||||
|
||||
.ChecklistIcon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: var(--theme-color-fg-muted);
|
||||
flex-shrink: 0;
|
||||
margin-top: 2px;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Part 4: Error/Failed State (FailedStep)
|
||||
|
||||
```scss
|
||||
/* FailedStep.module.scss */
|
||||
|
||||
.ErrorBanner {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--spacing-3);
|
||||
padding: var(--spacing-4);
|
||||
background-color: var(--theme-color-danger-bg);
|
||||
border: 1px solid var(--theme-color-danger-dim);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.ErrorIcon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
color: var(--theme-color-danger);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.ErrorContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-2);
|
||||
}
|
||||
|
||||
.ErrorTitle {
|
||||
font-size: var(--font-size-base);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--theme-color-danger-light);
|
||||
}
|
||||
|
||||
.ErrorMessage {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--theme-color-fg-default);
|
||||
line-height: var(--line-height-normal);
|
||||
}
|
||||
|
||||
/* Error details (collapsible) */
|
||||
.ErrorDetails {
|
||||
margin-top: var(--spacing-3);
|
||||
padding: var(--spacing-3);
|
||||
background-color: var(--theme-color-bg-1);
|
||||
border-radius: var(--radius-default);
|
||||
font-family: var(--font-family-code);
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--theme-color-fg-default-shy);
|
||||
overflow-x: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Part 5: Scanning/Loading State (ScanningStep)
|
||||
|
||||
```scss
|
||||
/* ScanningStep.module.scss */
|
||||
|
||||
.LoadingContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--spacing-8) var(--spacing-4);
|
||||
gap: var(--spacing-4);
|
||||
}
|
||||
|
||||
.Spinner {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: 3px solid var(--theme-color-bg-4);
|
||||
border-top-color: var(--theme-color-primary);
|
||||
border-radius: var(--radius-full);
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.LoadingText {
|
||||
font-size: var(--font-size-base);
|
||||
color: var(--theme-color-fg-default-shy);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Progress bar (if applicable) */
|
||||
.ProgressBar {
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
height: 4px;
|
||||
background-color: var(--theme-color-bg-4);
|
||||
border-radius: var(--radius-full);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ProgressFill {
|
||||
height: 100%;
|
||||
background-color: var(--theme-color-primary);
|
||||
border-radius: var(--radius-full);
|
||||
transition: width var(--transition-slow) var(--transition-ease);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Part 6: Report Step
|
||||
|
||||
```scss
|
||||
/* ReportStep.module.scss */
|
||||
|
||||
.ReportContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-4);
|
||||
}
|
||||
|
||||
.SummaryCard {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||||
gap: var(--spacing-3);
|
||||
}
|
||||
|
||||
.SummaryItem {
|
||||
background-color: var(--theme-color-bg-3);
|
||||
padding: var(--spacing-3);
|
||||
border-radius: var(--radius-md);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.SummaryValue {
|
||||
font-size: var(--font-size-2xl);
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--theme-color-fg-highlight);
|
||||
}
|
||||
|
||||
.SummaryLabel {
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--theme-color-fg-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: var(--letter-spacing-wide);
|
||||
}
|
||||
|
||||
/* Issue list */
|
||||
.IssueList {
|
||||
border: 1px solid var(--theme-color-border-default);
|
||||
border-radius: var(--radius-md);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.IssueItem {
|
||||
padding: var(--spacing-3);
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--spacing-3);
|
||||
border-bottom: 1px solid var(--theme-color-border-subtle);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.IssueIcon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
|
||||
&.is-warning {
|
||||
color: var(--theme-color-notice);
|
||||
}
|
||||
|
||||
&.is-error {
|
||||
color: var(--theme-color-danger);
|
||||
}
|
||||
|
||||
&.is-info {
|
||||
color: var(--theme-color-fg-muted);
|
||||
}
|
||||
}
|
||||
|
||||
.IssueContent {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.IssuePath {
|
||||
font-family: var(--font-family-code);
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--theme-color-fg-muted);
|
||||
}
|
||||
|
||||
.IssueMessage {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--theme-color-fg-default);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
### Wizard Progress
|
||||
- [ ] Current step is clearly highlighted (red)
|
||||
- [ ] Completed steps show green checkmarks
|
||||
- [ ] Pending steps are muted
|
||||
- [ ] Connectors show completion state
|
||||
|
||||
### Success State (CompleteStep)
|
||||
- [ ] Green success banner is prominent
|
||||
- [ ] Stats are easy to read
|
||||
- [ ] Next steps are clear
|
||||
- [ ] Primary action button is obvious
|
||||
|
||||
### Error State (FailedStep)
|
||||
- [ ] Red error banner catches attention
|
||||
- [ ] Error message is readable
|
||||
- [ ] Technical details are available but not overwhelming
|
||||
- [ ] Retry/close actions are clear
|
||||
|
||||
### Scanning State
|
||||
- [ ] Spinner animates smoothly
|
||||
- [ ] Progress indication is clear
|
||||
- [ ] User knows something is happening
|
||||
|
||||
### Report Step
|
||||
- [ ] Summary is scannable
|
||||
- [ ] Issues are categorized by severity
|
||||
- [ ] File paths are readable
|
||||
- [ ] Continue action is clear
|
||||
|
||||
### General
|
||||
- [ ] All steps use consistent spacing
|
||||
- [ ] Typography is readable
|
||||
- [ ] Colors match new palette
|
||||
- [ ] Transitions are smooth
|
||||
|
||||
---
|
||||
|
||||
## Visual Audit Process
|
||||
|
||||
1. **Start Migration Wizard** from a test project
|
||||
2. **Walk through each step** observing:
|
||||
- Progress indicator updates
|
||||
- Content layout and spacing
|
||||
- Button prominence
|
||||
- Color usage
|
||||
3. **Test error scenarios** if possible
|
||||
4. **Compare against modern UI** (Linear, Raycast, etc.)
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- [ ] Wizard uses design tokens throughout
|
||||
- [ ] Progress is obvious at a glance
|
||||
- [ ] Success state feels rewarding
|
||||
- [ ] Error state is informative but not alarming
|
||||
- [ ] Overall experience feels polished and professional
|
||||
- [ ] No hardcoded colors in migration wizard files
|
||||
@@ -0,0 +1,220 @@
|
||||
# DASH-001: Tabbed Navigation System
|
||||
|
||||
## Overview
|
||||
|
||||
Replace the current single-view dashboard with a proper tabbed interface. This is the foundation task that enables all other dashboard improvements.
|
||||
|
||||
## Context
|
||||
|
||||
The current Noodl editor dashboard (`projectsview.ts`) uses a basic pane-switching mechanism with jQuery. A new launcher is being developed in `packages/noodl-core-ui/src/preview/launcher/` using React, which already has a sidebar-based navigation but needs proper tab support for the main content area.
|
||||
|
||||
This task focuses on the **new React-based launcher** only. The old jQuery launcher will be deprecated.
|
||||
|
||||
## Current State
|
||||
|
||||
### Existing New Launcher Structure
|
||||
```
|
||||
packages/noodl-core-ui/src/preview/launcher/
|
||||
├── Launcher/
|
||||
│ ├── Launcher.tsx # Main component with PAGES array
|
||||
│ ├── components/
|
||||
│ │ ├── LauncherSidebar/ # Left navigation
|
||||
│ │ ├── LauncherPage/ # Page wrapper
|
||||
│ │ ├── LauncherProjectCard/
|
||||
│ │ └── LauncherSearchBar/
|
||||
│ └── views/
|
||||
│ ├── Projects.tsx # Current projects view
|
||||
│ └── LearningCenter.tsx # Empty learning view
|
||||
└── template/
|
||||
└── LauncherApp/ # App shell template
|
||||
```
|
||||
|
||||
### Current Page Definition
|
||||
```typescript
|
||||
// In Launcher.tsx
|
||||
export enum LauncherPageId {
|
||||
LocalProjects,
|
||||
LearningCenter
|
||||
}
|
||||
|
||||
export const PAGES: LauncherPageMetaData[] = [
|
||||
{ id: LauncherPageId.LocalProjects, displayName: 'Recent Projects', icon: IconName.CircleDot },
|
||||
{ id: LauncherPageId.LearningCenter, displayName: 'Learn', icon: IconName.Rocket }
|
||||
];
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
### Functional Requirements
|
||||
|
||||
1. **Tab Bar Component**
|
||||
- Horizontal tab bar at the top of the main content area
|
||||
- Visual indicator for active tab
|
||||
- Smooth transition when switching tabs
|
||||
- Keyboard navigation support (arrow keys, Enter)
|
||||
|
||||
2. **Tab Configuration**
|
||||
- Projects tab (default, opens first)
|
||||
- Learn tab (tutorials, guides)
|
||||
- Templates tab (project starters)
|
||||
- Extensible for future tabs (Marketplace, Settings)
|
||||
|
||||
3. **State Persistence**
|
||||
- Remember last active tab across sessions
|
||||
- Store in localStorage or electron-store
|
||||
|
||||
4. **URL/Deep Linking (Optional)**
|
||||
- Support for `noodl://dashboard/projects` style deep links
|
||||
- Query params for tab state
|
||||
|
||||
### Non-Functional Requirements
|
||||
|
||||
- Tab switching should feel instant (<100ms)
|
||||
- No layout shift when switching tabs
|
||||
- Accessible (WCAG 2.1 AA compliant)
|
||||
- Consistent with existing noodl-core-ui design system
|
||||
|
||||
## Technical Approach
|
||||
|
||||
### 1. Create Tab Bar Component
|
||||
|
||||
Create a new component in `noodl-core-ui` that can be reused:
|
||||
|
||||
```
|
||||
packages/noodl-core-ui/src/components/layout/TabBar/
|
||||
├── TabBar.tsx
|
||||
├── TabBar.module.scss
|
||||
├── TabBar.stories.tsx
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
### 2. Update Launcher Structure
|
||||
|
||||
```typescript
|
||||
// New page structure
|
||||
export enum LauncherPageId {
|
||||
Projects = 'projects',
|
||||
Learn = 'learn',
|
||||
Templates = 'templates'
|
||||
}
|
||||
|
||||
export interface LauncherTab {
|
||||
id: LauncherPageId;
|
||||
label: string;
|
||||
icon?: IconName;
|
||||
component: React.ComponentType;
|
||||
}
|
||||
|
||||
export const LAUNCHER_TABS: LauncherTab[] = [
|
||||
{ id: LauncherPageId.Projects, label: 'Projects', icon: IconName.Folder, component: Projects },
|
||||
{ id: LauncherPageId.Learn, label: 'Learn', icon: IconName.Book, component: LearningCenter },
|
||||
{ id: LauncherPageId.Templates, label: 'Templates', icon: IconName.Components, component: Templates }
|
||||
];
|
||||
```
|
||||
|
||||
### 3. State Management
|
||||
|
||||
Use React context for tab state:
|
||||
|
||||
```typescript
|
||||
// LauncherContext.tsx
|
||||
interface LauncherContextValue {
|
||||
activeTab: LauncherPageId;
|
||||
setActiveTab: (tab: LauncherPageId) => void;
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Persistence Hook
|
||||
|
||||
```typescript
|
||||
// usePersistentTab.ts
|
||||
function usePersistentTab(key: string, defaultTab: LauncherPageId) {
|
||||
// Load from localStorage on mount
|
||||
// Save to localStorage on change
|
||||
}
|
||||
```
|
||||
|
||||
## Files to Create
|
||||
|
||||
1. `packages/noodl-core-ui/src/components/layout/TabBar/TabBar.tsx`
|
||||
2. `packages/noodl-core-ui/src/components/layout/TabBar/TabBar.module.scss`
|
||||
3. `packages/noodl-core-ui/src/components/layout/TabBar/TabBar.stories.tsx`
|
||||
4. `packages/noodl-core-ui/src/components/layout/TabBar/index.ts`
|
||||
5. `packages/noodl-core-ui/src/preview/launcher/Launcher/LauncherContext.tsx`
|
||||
6. `packages/noodl-core-ui/src/preview/launcher/Launcher/hooks/usePersistentTab.ts`
|
||||
7. `packages/noodl-core-ui/src/preview/launcher/Launcher/views/Templates.tsx`
|
||||
|
||||
## Files to Modify
|
||||
|
||||
1. `packages/noodl-core-ui/src/preview/launcher/Launcher/Launcher.tsx`
|
||||
- Import and use TabBar
|
||||
- Implement tab switching logic
|
||||
- Wrap with LauncherContext
|
||||
|
||||
2. `packages/noodl-core-ui/src/components/layout/index.ts`
|
||||
- Export TabBar component
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### Phase 1: TabBar Component
|
||||
1. Create TabBar component with basic functionality
|
||||
2. Add styling consistent with noodl-core-ui
|
||||
3. Write Storybook stories for testing
|
||||
4. Add keyboard navigation
|
||||
|
||||
### Phase 2: Launcher Integration
|
||||
1. Create LauncherContext
|
||||
2. Create usePersistentTab hook
|
||||
3. Integrate TabBar into Launcher.tsx
|
||||
4. Create empty Templates view
|
||||
|
||||
### Phase 3: Polish
|
||||
1. Add tab transition animations
|
||||
2. Test accessibility
|
||||
3. Add deep link support (if time permits)
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
- [ ] Tabs render correctly
|
||||
- [ ] Clicking tab switches content
|
||||
- [ ] Active tab is visually indicated
|
||||
- [ ] Keyboard navigation works (Tab, Arrow keys, Enter)
|
||||
- [ ] Tab state persists after closing/reopening
|
||||
- [ ] No layout shift on tab switch
|
||||
- [ ] Works at different viewport sizes
|
||||
- [ ] Screen reader announces tab changes
|
||||
|
||||
## Design Reference
|
||||
|
||||
The tab bar should follow the existing Tabs component style in noodl-core-ui but be optimized for the launcher context (larger, more prominent).
|
||||
|
||||
See: `packages/noodl-core-ui/src/components/layout/Tabs/`
|
||||
|
||||
## Dependencies
|
||||
|
||||
- None (this is a foundation task)
|
||||
|
||||
## Blocked By
|
||||
|
||||
- None
|
||||
|
||||
## Blocks
|
||||
|
||||
- DASH-002 (Project List Redesign)
|
||||
- DASH-003 (Project Organization)
|
||||
- DASH-004 (Tutorial Section Redesign)
|
||||
|
||||
## Estimated Effort
|
||||
|
||||
- Component creation: 2-3 hours
|
||||
- Launcher integration: 2-3 hours
|
||||
- Polish and testing: 1-2 hours
|
||||
- **Total: 5-8 hours**
|
||||
|
||||
## Success Criteria
|
||||
|
||||
1. User can switch between Projects, Learn, and Templates tabs
|
||||
2. Tab state persists across sessions
|
||||
3. Component is reusable for other contexts
|
||||
4. Passes accessibility audit
|
||||
5. Matches existing design system aesthetics
|
||||
@@ -0,0 +1,292 @@
|
||||
# DASH-002: Project List Redesign
|
||||
|
||||
## Overview
|
||||
|
||||
Transform the project list from a thumbnail grid into a more functional table/list view optimized for users with many projects. Add sorting, better information density, and optional view modes.
|
||||
|
||||
## Context
|
||||
|
||||
The current dashboard shows projects as large cards with auto-generated thumbnails. This works for users with a few projects but becomes unwieldy with many projects. The thumbnails add visual noise without providing much value.
|
||||
|
||||
The new launcher in `noodl-core-ui/src/preview/launcher/` already has the beginnings of a table layout with columns for Name, Version Control, and Contributors.
|
||||
|
||||
## Current State
|
||||
|
||||
### Existing LauncherProjectCard
|
||||
```typescript
|
||||
// From LauncherProjectCard.tsx
|
||||
export interface LauncherProjectData {
|
||||
id: string;
|
||||
title: string;
|
||||
cloudSyncMeta: {
|
||||
type: CloudSyncType;
|
||||
source?: string;
|
||||
};
|
||||
localPath: string;
|
||||
lastOpened: string;
|
||||
pullAmount?: number;
|
||||
pushAmount?: number;
|
||||
uncommittedChangesAmount?: number;
|
||||
imageSrc: string;
|
||||
contributors?: UserBadgeProps[];
|
||||
}
|
||||
```
|
||||
|
||||
### Current Layout (Projects.tsx)
|
||||
- Table header with Name, Version control, Contributors columns
|
||||
- Cards with thumbnail images
|
||||
- Basic search functionality via LauncherSearchBar
|
||||
|
||||
## Requirements
|
||||
|
||||
### Functional Requirements
|
||||
|
||||
1. **List View (Primary)**
|
||||
- Compact row-based layout
|
||||
- Columns: Name, Last Modified, Git Status, Local Path (truncated)
|
||||
- Row hover state with quick actions
|
||||
- Sortable columns (click header to sort)
|
||||
- Resizable columns (stretch goal)
|
||||
|
||||
2. **Grid View (Secondary)**
|
||||
- Card-based layout for visual preference
|
||||
- Smaller cards than current (2-3x more per row)
|
||||
- Optional thumbnails (can be disabled)
|
||||
- View toggle in toolbar
|
||||
|
||||
3. **Sorting**
|
||||
- Sort by Name (A-Z, Z-A)
|
||||
- Sort by Last Modified (newest, oldest)
|
||||
- Sort by Git Status (synced first, needs attention first)
|
||||
- Persist sort preference
|
||||
|
||||
4. **Information Display**
|
||||
- Project name (primary)
|
||||
- Last modified timestamp (relative: "2 hours ago")
|
||||
- Git status indicator (icon + tooltip)
|
||||
- Local path (truncated with tooltip for full path)
|
||||
- Quick action buttons on hover (Open, Folder, Settings, Delete)
|
||||
|
||||
5. **Empty State**
|
||||
- Friendly message when no projects exist
|
||||
- Call-to-action to create new project or import
|
||||
|
||||
### Non-Functional Requirements
|
||||
|
||||
- Handle 100+ projects smoothly (virtual scrolling if needed)
|
||||
- Row click opens project
|
||||
- Right-click context menu
|
||||
- Responsive to window resize
|
||||
|
||||
## Technical Approach
|
||||
|
||||
### 1. Data Layer
|
||||
|
||||
Create a hook for project data with sorting:
|
||||
|
||||
```typescript
|
||||
// useProjectList.ts
|
||||
interface UseProjectListOptions {
|
||||
sortField: 'name' | 'lastModified' | 'gitStatus';
|
||||
sortDirection: 'asc' | 'desc';
|
||||
filter?: string;
|
||||
}
|
||||
|
||||
interface UseProjectListReturn {
|
||||
projects: LauncherProjectData[];
|
||||
isLoading: boolean;
|
||||
sortField: string;
|
||||
sortDirection: string;
|
||||
setSorting: (field: string, direction: string) => void;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. List View Component
|
||||
|
||||
```
|
||||
packages/noodl-core-ui/src/preview/launcher/Launcher/components/
|
||||
├── ProjectList/
|
||||
│ ├── ProjectList.tsx # Main list component
|
||||
│ ├── ProjectListRow.tsx # Individual row
|
||||
│ ├── ProjectListHeader.tsx # Sortable header
|
||||
│ ├── ProjectList.module.scss
|
||||
│ └── index.ts
|
||||
```
|
||||
|
||||
### 3. View Mode Toggle
|
||||
|
||||
```typescript
|
||||
// ViewModeToggle.tsx
|
||||
export enum ViewMode {
|
||||
List = 'list',
|
||||
Grid = 'grid'
|
||||
}
|
||||
|
||||
interface ViewModeToggleProps {
|
||||
mode: ViewMode;
|
||||
onChange: (mode: ViewMode) => void;
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Git Status Display
|
||||
|
||||
```typescript
|
||||
// GitStatusBadge.tsx
|
||||
export enum GitStatusType {
|
||||
NotInitialized = 'not-initialized',
|
||||
LocalOnly = 'local-only',
|
||||
Synced = 'synced',
|
||||
Ahead = 'ahead', // Have local commits to push
|
||||
Behind = 'behind', // Have remote commits to pull
|
||||
Diverged = 'diverged', // Both ahead and behind
|
||||
Uncommitted = 'uncommitted'
|
||||
}
|
||||
|
||||
interface GitStatusBadgeProps {
|
||||
status: GitStatusType;
|
||||
details?: {
|
||||
ahead?: number;
|
||||
behind?: number;
|
||||
uncommitted?: number;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Files to Create
|
||||
|
||||
1. `packages/noodl-core-ui/src/preview/launcher/Launcher/components/ProjectList/ProjectList.tsx`
|
||||
2. `packages/noodl-core-ui/src/preview/launcher/Launcher/components/ProjectList/ProjectListRow.tsx`
|
||||
3. `packages/noodl-core-ui/src/preview/launcher/Launcher/components/ProjectList/ProjectListHeader.tsx`
|
||||
4. `packages/noodl-core-ui/src/preview/launcher/Launcher/components/ProjectList/ProjectList.module.scss`
|
||||
5. `packages/noodl-core-ui/src/preview/launcher/Launcher/components/ProjectList/index.ts`
|
||||
6. `packages/noodl-core-ui/src/preview/launcher/Launcher/components/ViewModeToggle/ViewModeToggle.tsx`
|
||||
7. `packages/noodl-core-ui/src/preview/launcher/Launcher/components/GitStatusBadge/GitStatusBadge.tsx`
|
||||
8. `packages/noodl-core-ui/src/preview/launcher/Launcher/hooks/useProjectList.ts`
|
||||
9. `packages/noodl-core-ui/src/preview/launcher/Launcher/components/EmptyProjectsState/EmptyProjectsState.tsx`
|
||||
|
||||
## Files to Modify
|
||||
|
||||
1. `packages/noodl-core-ui/src/preview/launcher/Launcher/views/Projects.tsx`
|
||||
- Replace current layout with ProjectList component
|
||||
- Add view mode toggle
|
||||
- Wire up sorting
|
||||
|
||||
2. `packages/noodl-core-ui/src/preview/launcher/Launcher/components/LauncherProjectCard/LauncherProjectCard.tsx`
|
||||
- Refactor for grid view (smaller)
|
||||
- Make thumbnail optional
|
||||
|
||||
3. `packages/noodl-core-ui/src/preview/launcher/Launcher/Launcher.tsx`
|
||||
- Update mock data if needed
|
||||
- Add view mode to context
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### Phase 1: Core List View
|
||||
1. Create ProjectListHeader with sortable columns
|
||||
2. Create ProjectListRow with project info
|
||||
3. Create ProjectList combining header and rows
|
||||
4. Add basic sorting logic
|
||||
|
||||
### Phase 2: Git Status Display
|
||||
1. Create GitStatusBadge component
|
||||
2. Define status types and icons
|
||||
3. Add tooltips with details
|
||||
|
||||
### Phase 3: View Modes
|
||||
1. Create ViewModeToggle component
|
||||
2. Refactor LauncherProjectCard for grid mode
|
||||
3. Add view mode to Projects view
|
||||
4. Persist preference
|
||||
|
||||
### Phase 4: Polish
|
||||
1. Add empty state
|
||||
2. Add hover actions
|
||||
3. Implement virtual scrolling (if needed)
|
||||
4. Test with large project counts
|
||||
|
||||
## Component Specifications
|
||||
|
||||
### ProjectListHeader
|
||||
|
||||
| Column | Width | Sortable | Content |
|
||||
|--------|-------|----------|---------|
|
||||
| Name | 40% | Yes | Project name |
|
||||
| Last Modified | 20% | Yes | Relative timestamp |
|
||||
| Git Status | 15% | Yes | Status badge |
|
||||
| Path | 25% | No | Truncated local path |
|
||||
|
||||
### ProjectListRow
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────────────┐
|
||||
│ 📁 My Project Name 2 hours ago ⚡ Ahead (3) ~/dev/... │
|
||||
│ [hover: Open 📂 ⚙️ 🗑️] │
|
||||
└──────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### GitStatusBadge Icons
|
||||
|
||||
| Status | Icon | Color | Tooltip |
|
||||
|--------|------|-------|---------|
|
||||
| not-initialized | ⚪ | Gray | "No version control" |
|
||||
| local-only | 💾 | Yellow | "Local git only, not synced" |
|
||||
| synced | ✅ | Green | "Up to date with remote" |
|
||||
| ahead | ⬆️ | Blue | "3 commits to push" |
|
||||
| behind | ⬇️ | Orange | "5 commits to pull" |
|
||||
| diverged | ⚠️ | Red | "3 ahead, 5 behind" |
|
||||
| uncommitted | ● | Yellow | "Uncommitted changes" |
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
- [ ] List renders with mock data
|
||||
- [ ] Clicking row opens project (or shows FIXME alert)
|
||||
- [ ] Sorting by each column works
|
||||
- [ ] Sort direction toggles on repeated click
|
||||
- [ ] Sort preference persists
|
||||
- [ ] View mode toggle switches layouts
|
||||
- [ ] View mode preference persists
|
||||
- [ ] Git status badges display correctly
|
||||
- [ ] Tooltips show on hover
|
||||
- [ ] Right-click shows context menu
|
||||
- [ ] Empty state shows when no projects
|
||||
- [ ] Search filters projects correctly
|
||||
- [ ] Performance acceptable with 100+ mock projects
|
||||
|
||||
## Dependencies
|
||||
|
||||
- DASH-001 (Tabbed Navigation System) - for launcher context
|
||||
|
||||
## Blocked By
|
||||
|
||||
- DASH-001
|
||||
|
||||
## Blocks
|
||||
|
||||
- DASH-003 (needs list infrastructure for folder/tag filtering)
|
||||
|
||||
## Estimated Effort
|
||||
|
||||
- ProjectList components: 3-4 hours
|
||||
- GitStatusBadge: 1-2 hours
|
||||
- View mode toggle: 1-2 hours
|
||||
- Sorting & persistence: 2-3 hours
|
||||
- Polish & testing: 2-3 hours
|
||||
- **Total: 9-14 hours**
|
||||
|
||||
## Success Criteria
|
||||
|
||||
1. Projects display in a compact, sortable list
|
||||
2. Git status is immediately visible
|
||||
3. Users can switch to grid view if preferred
|
||||
4. Sorting and view preferences persist
|
||||
5. Empty state guides new users
|
||||
6. Context menu provides quick actions
|
||||
|
||||
## Design Notes
|
||||
|
||||
The list view should feel similar to:
|
||||
- VS Code's file explorer
|
||||
- macOS Finder list view
|
||||
- GitHub repository list
|
||||
|
||||
Keep information density high but avoid clutter. Use icons where possible to save space, with tooltips for details.
|
||||
@@ -0,0 +1,357 @@
|
||||
# DASH-003: Project Organization - Folders & Tags
|
||||
|
||||
## Overview
|
||||
|
||||
Add the ability to organize projects using folders and tags. This enables users with many projects to group related work, filter their view, and find projects quickly.
|
||||
|
||||
## Context
|
||||
|
||||
Currently, projects are displayed in a flat list sorted by recency. Users with many projects (10+) struggle to find specific projects. There's no way to group related projects (e.g., "Client Work", "Personal", "Tutorials").
|
||||
|
||||
This task adds a folder/tag system that works entirely client-side, storing metadata separately from the Noodl projects themselves.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Functional Requirements
|
||||
|
||||
1. **Folders**
|
||||
- Create, rename, delete folders
|
||||
- Drag-and-drop projects into folders
|
||||
- Nested folders (1 level deep max)
|
||||
- "All Projects" virtual folder (shows everything)
|
||||
- "Uncategorized" virtual folder (shows unorganized projects)
|
||||
- Folder displayed in sidebar
|
||||
|
||||
2. **Tags**
|
||||
- Create, rename, delete tags
|
||||
- Assign multiple tags per project
|
||||
- Color-coded tags
|
||||
- Tag filtering (show projects with specific tags)
|
||||
- Tags displayed as pills on project rows
|
||||
|
||||
3. **Filtering**
|
||||
- Filter by folder (sidebar click)
|
||||
- Filter by tag (tag click or dropdown)
|
||||
- Combine folder + tag filters
|
||||
- Search within filtered view
|
||||
- Clear all filters button
|
||||
|
||||
4. **Persistence**
|
||||
- Store folder/tag data in electron-store (not in project files)
|
||||
- Data structure keyed by project path (stable identifier)
|
||||
- Export/import organization data (stretch goal)
|
||||
|
||||
### Non-Functional Requirements
|
||||
|
||||
- Organization changes feel instant
|
||||
- Drag-and-drop is smooth
|
||||
- Works offline
|
||||
- Survives app restart
|
||||
|
||||
## Data Model
|
||||
|
||||
### Storage Structure
|
||||
|
||||
```typescript
|
||||
// Stored in electron-store under 'projectOrganization'
|
||||
interface ProjectOrganizationData {
|
||||
version: 1;
|
||||
folders: Folder[];
|
||||
tags: Tag[];
|
||||
projectMeta: Record<string, ProjectMeta>; // keyed by project path
|
||||
}
|
||||
|
||||
interface Folder {
|
||||
id: string;
|
||||
name: string;
|
||||
parentId: string | null; // null = root level
|
||||
order: number;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
interface Tag {
|
||||
id: string;
|
||||
name: string;
|
||||
color: string; // hex color
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
interface ProjectMeta {
|
||||
folderId: string | null;
|
||||
tagIds: string[];
|
||||
customName?: string; // optional override
|
||||
notes?: string; // stretch goal
|
||||
}
|
||||
```
|
||||
|
||||
### Color Palette for Tags
|
||||
|
||||
```typescript
|
||||
const TAG_COLORS = [
|
||||
'#EF4444', // Red
|
||||
'#F97316', // Orange
|
||||
'#EAB308', // Yellow
|
||||
'#22C55E', // Green
|
||||
'#06B6D4', // Cyan
|
||||
'#3B82F6', // Blue
|
||||
'#8B5CF6', // Purple
|
||||
'#EC4899', // Pink
|
||||
'#6B7280', // Gray
|
||||
];
|
||||
```
|
||||
|
||||
## Technical Approach
|
||||
|
||||
### 1. Storage Service
|
||||
|
||||
```typescript
|
||||
// packages/noodl-editor/src/editor/src/services/ProjectOrganizationService.ts
|
||||
|
||||
class ProjectOrganizationService {
|
||||
private static instance: ProjectOrganizationService;
|
||||
|
||||
// Folder operations
|
||||
createFolder(name: string, parentId?: string): Folder;
|
||||
renameFolder(id: string, name: string): void;
|
||||
deleteFolder(id: string): void;
|
||||
reorderFolder(id: string, newOrder: number): void;
|
||||
|
||||
// Tag operations
|
||||
createTag(name: string, color: string): Tag;
|
||||
renameTag(id: string, name: string): void;
|
||||
deleteTag(id: string): void;
|
||||
changeTagColor(id: string, color: string): void;
|
||||
|
||||
// Project organization
|
||||
moveProjectToFolder(projectPath: string, folderId: string | null): void;
|
||||
addTagToProject(projectPath: string, tagId: string): void;
|
||||
removeTagFromProject(projectPath: string, tagId: string): void;
|
||||
|
||||
// Queries
|
||||
getFolders(): Folder[];
|
||||
getTags(): Tag[];
|
||||
getProjectMeta(projectPath: string): ProjectMeta | null;
|
||||
getProjectsInFolder(folderId: string | null): string[];
|
||||
getProjectsWithTag(tagId: string): string[];
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Sidebar Folder Tree
|
||||
|
||||
```
|
||||
packages/noodl-core-ui/src/preview/launcher/Launcher/components/
|
||||
├── FolderTree/
|
||||
│ ├── FolderTree.tsx # Tree container
|
||||
│ ├── FolderTreeItem.tsx # Individual folder row
|
||||
│ ├── FolderTree.module.scss
|
||||
│ └── index.ts
|
||||
```
|
||||
|
||||
### 3. Tag Components
|
||||
|
||||
```
|
||||
├── TagPill/
|
||||
│ ├── TagPill.tsx # Small colored tag display
|
||||
│ └── TagPill.module.scss
|
||||
├── TagSelector/
|
||||
│ ├── TagSelector.tsx # Dropdown to add/remove tags
|
||||
│ └── TagSelector.module.scss
|
||||
├── TagFilter/
|
||||
│ ├── TagFilter.tsx # Filter bar with active tags
|
||||
│ └── TagFilter.module.scss
|
||||
```
|
||||
|
||||
### 4. Drag and Drop
|
||||
|
||||
Use `@dnd-kit/core` for drag-and-drop:
|
||||
|
||||
```typescript
|
||||
// DragDropContext for launcher
|
||||
import { DndContext, DragOverlay } from '@dnd-kit/core';
|
||||
|
||||
// Draggable project row
|
||||
import { useDraggable } from '@dnd-kit/core';
|
||||
|
||||
// Droppable folder
|
||||
import { useDroppable } from '@dnd-kit/core';
|
||||
```
|
||||
|
||||
## Files to Create
|
||||
|
||||
1. `packages/noodl-editor/src/editor/src/services/ProjectOrganizationService.ts`
|
||||
2. `packages/noodl-core-ui/src/preview/launcher/Launcher/components/FolderTree/FolderTree.tsx`
|
||||
3. `packages/noodl-core-ui/src/preview/launcher/Launcher/components/FolderTree/FolderTreeItem.tsx`
|
||||
4. `packages/noodl-core-ui/src/preview/launcher/Launcher/components/FolderTree/FolderTree.module.scss`
|
||||
5. `packages/noodl-core-ui/src/preview/launcher/Launcher/components/TagPill/TagPill.tsx`
|
||||
6. `packages/noodl-core-ui/src/preview/launcher/Launcher/components/TagSelector/TagSelector.tsx`
|
||||
7. `packages/noodl-core-ui/src/preview/launcher/Launcher/components/TagFilter/TagFilter.tsx`
|
||||
8. `packages/noodl-core-ui/src/preview/launcher/Launcher/hooks/useProjectOrganization.ts`
|
||||
9. `packages/noodl-core-ui/src/preview/launcher/Launcher/components/CreateFolderModal/CreateFolderModal.tsx`
|
||||
10. `packages/noodl-core-ui/src/preview/launcher/Launcher/components/CreateTagModal/CreateTagModal.tsx`
|
||||
|
||||
## Files to Modify
|
||||
|
||||
1. `packages/noodl-core-ui/src/preview/launcher/Launcher/Launcher.tsx`
|
||||
- Add DndContext wrapper
|
||||
- Add organization state to context
|
||||
|
||||
2. `packages/noodl-core-ui/src/preview/launcher/Launcher/components/LauncherSidebar/LauncherSidebar.tsx`
|
||||
- Add FolderTree component
|
||||
- Add "Create Folder" button
|
||||
|
||||
3. `packages/noodl-core-ui/src/preview/launcher/Launcher/views/Projects.tsx`
|
||||
- Add TagFilter bar
|
||||
- Filter projects based on folder/tag selection
|
||||
- Make project rows draggable
|
||||
|
||||
4. `packages/noodl-core-ui/src/preview/launcher/Launcher/components/ProjectList/ProjectListRow.tsx`
|
||||
- Add tag pills
|
||||
- Add tag selector on hover/context menu
|
||||
- Make row draggable
|
||||
|
||||
## UI Mockups
|
||||
|
||||
### Sidebar with Folders
|
||||
|
||||
```
|
||||
┌─────────────────────────┐
|
||||
│ 📁 All Projects (24) │
|
||||
│ 📁 Uncategorized (5) │
|
||||
├─────────────────────────┤
|
||||
│ + Create Folder │
|
||||
├─────────────────────────┤
|
||||
│ 📂 Client Work (8) │
|
||||
│ └─ 📁 Acme Corp (3) │
|
||||
│ └─ 📁 BigCo (5) │
|
||||
│ 📂 Personal (6) │
|
||||
│ 📂 Tutorials (5) │
|
||||
└─────────────────────────┘
|
||||
```
|
||||
|
||||
### Project Row with Tags
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────────────────┐
|
||||
│ 📁 E-commerce Dashboard 2h ago ✅ [🔴 Urgent] [🔵 Client] ~/dev/... │
|
||||
└──────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Tag Filter Bar
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ Filters: [🔴 Urgent ×] [🔵 Client ×] [+ Add Filter] [Clear All] │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### Phase 1: Storage Foundation
|
||||
1. Create ProjectOrganizationService
|
||||
2. Define data model and storage
|
||||
3. Create useProjectOrganization hook
|
||||
4. Add to launcher context
|
||||
|
||||
### Phase 2: Folders
|
||||
1. Create FolderTree component
|
||||
2. Add to sidebar
|
||||
3. Create folder modal
|
||||
4. Implement folder filtering
|
||||
5. Add context menu (rename, delete)
|
||||
|
||||
### Phase 3: Tags
|
||||
1. Create TagPill component
|
||||
2. Create TagSelector dropdown
|
||||
3. Create TagFilter bar
|
||||
4. Add tags to project rows
|
||||
5. Implement tag filtering
|
||||
|
||||
### Phase 4: Drag and Drop
|
||||
1. Add dnd-kit dependency
|
||||
2. Wrap launcher in DndContext
|
||||
3. Make project rows draggable
|
||||
4. Make folders droppable
|
||||
5. Handle drop events
|
||||
|
||||
### Phase 5: Polish
|
||||
1. Add keyboard shortcuts
|
||||
2. Improve animations
|
||||
3. Handle edge cases (deleted projects, etc.)
|
||||
4. Test thoroughly
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
- [ ] Can create a folder
|
||||
- [ ] Can rename a folder
|
||||
- [ ] Can delete a folder (projects go to Uncategorized)
|
||||
- [ ] Can create nested folder
|
||||
- [ ] Clicking folder filters project list
|
||||
- [ ] Can create a tag
|
||||
- [ ] Can assign tag to project
|
||||
- [ ] Can remove tag from project
|
||||
- [ ] Clicking tag filters project list
|
||||
- [ ] Can combine folder + tag filters
|
||||
- [ ] Search works within filtered view
|
||||
- [ ] Clear filters button works
|
||||
- [ ] Drag project to folder works
|
||||
- [ ] Data persists after app restart
|
||||
- [ ] Removing project from disk shows appropriate state
|
||||
|
||||
## Dependencies
|
||||
|
||||
- DASH-001 (Tabbed Navigation System)
|
||||
- DASH-002 (Project List Redesign) - for project rows
|
||||
|
||||
### External Dependencies
|
||||
|
||||
Add to `package.json`:
|
||||
```json
|
||||
{
|
||||
"@dnd-kit/core": "^6.0.0",
|
||||
"@dnd-kit/sortable": "^7.0.0"
|
||||
}
|
||||
```
|
||||
|
||||
## Blocked By
|
||||
|
||||
- DASH-002
|
||||
|
||||
## Blocks
|
||||
|
||||
- None (this is end of the DASH chain)
|
||||
|
||||
## Estimated Effort
|
||||
|
||||
- Storage service: 2-3 hours
|
||||
- Folder tree UI: 3-4 hours
|
||||
- Tag components: 3-4 hours
|
||||
- Drag and drop: 3-4 hours
|
||||
- Filtering logic: 2-3 hours
|
||||
- Polish & testing: 3-4 hours
|
||||
- **Total: 16-22 hours**
|
||||
|
||||
## Success Criteria
|
||||
|
||||
1. Users can create folders and organize projects
|
||||
2. Users can create tags and assign them to projects
|
||||
3. Filtering by folder and tag works correctly
|
||||
4. Drag-and-drop feels natural
|
||||
5. Organization data persists across sessions
|
||||
6. System handles edge cases gracefully (deleted projects, etc.)
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- Export/import organization data
|
||||
- Folder color customization
|
||||
- Project notes/descriptions
|
||||
- Bulk operations (move/tag multiple projects)
|
||||
- Smart folders (auto-organize by criteria)
|
||||
|
||||
## Design Notes
|
||||
|
||||
The folder tree should feel familiar like:
|
||||
- macOS Finder sidebar
|
||||
- VS Code Explorer
|
||||
- Notion page tree
|
||||
|
||||
Keep interactions lightweight - organization should help, not hinder, the workflow of quickly opening projects.
|
||||
@@ -0,0 +1,413 @@
|
||||
# DASH-004: Tutorial Section Redesign
|
||||
|
||||
## Overview
|
||||
|
||||
Redesign the tutorial section (Learn tab) to be more compact, informative, and useful. Move from large tiles to a structured learning center with categories, progress tracking, and better discoverability.
|
||||
|
||||
## Context
|
||||
|
||||
The current tutorial section (`projectsview.ts` and lessons model) shows tutorials as large tiles with progress bars. The tiles take up significant screen space, making it hard to browse many tutorials. There's no categorization beyond a linear list.
|
||||
|
||||
The new launcher has an empty `LearningCenter.tsx` view that needs to be built out.
|
||||
|
||||
### Current Tutorial System
|
||||
|
||||
The existing system uses:
|
||||
- `LessonProjectsModel` - manages lesson templates and progress
|
||||
- `lessonprojectsmodel.ts` - fetches from docs endpoint
|
||||
- Templates stored in docs repo with progress in localStorage
|
||||
|
||||
## Requirements
|
||||
|
||||
### Functional Requirements
|
||||
|
||||
1. **Category Organization**
|
||||
- Categories: Getting Started, Building UIs, Data & Logic, Advanced Topics, Integrations
|
||||
- Collapsible category sections
|
||||
- Category icons/colors
|
||||
|
||||
2. **Tutorial Cards (Compact)**
|
||||
- Title
|
||||
- Short description (1-2 lines)
|
||||
- Estimated duration
|
||||
- Difficulty level (Beginner, Intermediate, Advanced)
|
||||
- Progress indicator (not started, in progress, completed)
|
||||
- Thumbnail (small, optional)
|
||||
|
||||
3. **Progress Tracking**
|
||||
- Visual progress bar per tutorial
|
||||
- Overall progress stats ("5 of 12 completed")
|
||||
- "Continue where you left off" section at top
|
||||
- Reset progress option
|
||||
|
||||
4. **Filtering & Search**
|
||||
- Search tutorials by name/description
|
||||
- Filter by difficulty
|
||||
- Filter by category
|
||||
- Filter by progress (Not Started, In Progress, Completed)
|
||||
|
||||
5. **Tutorial Detail View**
|
||||
- Expanded description
|
||||
- Learning objectives
|
||||
- Prerequisites
|
||||
- "Start Tutorial" / "Continue" / "Restart" button
|
||||
- Estimated time remaining (for in-progress)
|
||||
|
||||
6. **Additional Content Types**
|
||||
- Video tutorials (embedded or linked)
|
||||
- Written guides
|
||||
- Interactive lessons (existing)
|
||||
- External resources
|
||||
|
||||
### Non-Functional Requirements
|
||||
|
||||
- Fast loading (tutorials list cached)
|
||||
- Works offline for previously loaded tutorials
|
||||
- Responsive layout
|
||||
- Accessible navigation
|
||||
|
||||
## Data Model
|
||||
|
||||
### Enhanced Tutorial Structure
|
||||
|
||||
```typescript
|
||||
interface Tutorial {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
longDescription?: string;
|
||||
category: TutorialCategory;
|
||||
difficulty: 'beginner' | 'intermediate' | 'advanced';
|
||||
estimatedMinutes: number;
|
||||
type: 'interactive' | 'video' | 'guide';
|
||||
thumbnailUrl?: string;
|
||||
objectives?: string[];
|
||||
prerequisites?: string[];
|
||||
|
||||
// For interactive tutorials
|
||||
templateUrl?: string;
|
||||
|
||||
// For video tutorials
|
||||
videoUrl?: string;
|
||||
|
||||
// For guides
|
||||
guideUrl?: string;
|
||||
}
|
||||
|
||||
interface TutorialCategory {
|
||||
id: string;
|
||||
name: string;
|
||||
icon: IconName;
|
||||
color: string;
|
||||
order: number;
|
||||
}
|
||||
|
||||
interface TutorialProgress {
|
||||
tutorialId: string;
|
||||
status: 'not-started' | 'in-progress' | 'completed';
|
||||
lastAccessedAt: string;
|
||||
completedAt?: string;
|
||||
currentStep?: number;
|
||||
totalSteps?: number;
|
||||
}
|
||||
```
|
||||
|
||||
### Default Categories
|
||||
|
||||
```typescript
|
||||
const TUTORIAL_CATEGORIES: TutorialCategory[] = [
|
||||
{ id: 'getting-started', name: 'Getting Started', icon: IconName.Rocket, color: '#22C55E', order: 0 },
|
||||
{ id: 'ui', name: 'Building UIs', icon: IconName.Palette, color: '#3B82F6', order: 1 },
|
||||
{ id: 'data', name: 'Data & Logic', icon: IconName.Database, color: '#8B5CF6', order: 2 },
|
||||
{ id: 'advanced', name: 'Advanced Topics', icon: IconName.Cog, color: '#F97316', order: 3 },
|
||||
{ id: 'integrations', name: 'Integrations', icon: IconName.Plug, color: '#EC4899', order: 4 },
|
||||
];
|
||||
```
|
||||
|
||||
## Technical Approach
|
||||
|
||||
### 1. Tutorial Service
|
||||
|
||||
Extend or replace `LessonProjectsModel`:
|
||||
|
||||
```typescript
|
||||
// packages/noodl-editor/src/editor/src/services/TutorialService.ts
|
||||
|
||||
class TutorialService {
|
||||
private static instance: TutorialService;
|
||||
|
||||
// Data fetching
|
||||
async fetchTutorials(): Promise<Tutorial[]>;
|
||||
async getTutorialById(id: string): Promise<Tutorial | null>;
|
||||
|
||||
// Progress
|
||||
getProgress(tutorialId: string): TutorialProgress;
|
||||
updateProgress(tutorialId: string, progress: Partial<TutorialProgress>): void;
|
||||
resetProgress(tutorialId: string): void;
|
||||
|
||||
// Queries
|
||||
getInProgressTutorials(): Tutorial[];
|
||||
getCompletedTutorials(): Tutorial[];
|
||||
getTutorialsByCategory(categoryId: string): Tutorial[];
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Component Structure
|
||||
|
||||
```
|
||||
packages/noodl-core-ui/src/preview/launcher/Launcher/
|
||||
├── views/
|
||||
│ └── LearningCenter/
|
||||
│ ├── LearningCenter.tsx # Main view
|
||||
│ ├── LearningCenter.module.scss
|
||||
│ ├── ContinueLearning.tsx # "Continue" section
|
||||
│ ├── TutorialCategory.tsx # Category section
|
||||
│ └── TutorialFilters.tsx # Filter bar
|
||||
├── components/
|
||||
│ ├── TutorialCard/
|
||||
│ │ ├── TutorialCard.tsx # Compact card
|
||||
│ │ ├── TutorialCard.module.scss
|
||||
│ │ └── index.ts
|
||||
│ ├── TutorialDetailModal/
|
||||
│ │ ├── TutorialDetailModal.tsx # Expanded detail view
|
||||
│ │ └── TutorialDetailModal.module.scss
|
||||
│ ├── DifficultyBadge/
|
||||
│ │ └── DifficultyBadge.tsx # Beginner/Intermediate/Advanced
|
||||
│ ├── ProgressRing/
|
||||
│ │ └── ProgressRing.tsx # Circular progress indicator
|
||||
│ └── DurationLabel/
|
||||
│ └── DurationLabel.tsx # "15 min" display
|
||||
```
|
||||
|
||||
### 3. Learning Center Layout
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ Learn [🔍 Search... ] │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ Filters: [All ▾] [All Difficulties ▾] [All Progress ▾] [Clear] │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ ⏸️ Continue Learning │
|
||||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ 📚 OpenNoodl Basics 47% [●●●●●○○○○○] [Continue →] │ │
|
||||
│ │ Data-driven Components 12% [●○○○○○○○○○] [Continue →] │ │
|
||||
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ 🚀 Getting Started ▼ │
|
||||
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
||||
│ │ AI Walkthru │ │ Basics │ │ Layout │ │
|
||||
│ │ 🟢 Beginner │ │ 🟢 Beginner │ │ 🟢 Beginner │ │
|
||||
│ │ 15 min │ │ 15 min │ │ 15 min │ │
|
||||
│ │ ✓ Complete │ │ ● 47% │ │ ○ Not started│ │
|
||||
│ └──────────────┘ └──────────────┘ └──────────────┘ │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ 🎨 Building UIs ▼ │
|
||||
│ ... │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Files to Create
|
||||
|
||||
1. `packages/noodl-editor/src/editor/src/services/TutorialService.ts`
|
||||
2. `packages/noodl-core-ui/src/preview/launcher/Launcher/views/LearningCenter/LearningCenter.tsx`
|
||||
3. `packages/noodl-core-ui/src/preview/launcher/Launcher/views/LearningCenter/LearningCenter.module.scss`
|
||||
4. `packages/noodl-core-ui/src/preview/launcher/Launcher/views/LearningCenter/ContinueLearning.tsx`
|
||||
5. `packages/noodl-core-ui/src/preview/launcher/Launcher/views/LearningCenter/TutorialCategory.tsx`
|
||||
6. `packages/noodl-core-ui/src/preview/launcher/Launcher/views/LearningCenter/TutorialFilters.tsx`
|
||||
7. `packages/noodl-core-ui/src/preview/launcher/Launcher/components/TutorialCard/TutorialCard.tsx`
|
||||
8. `packages/noodl-core-ui/src/preview/launcher/Launcher/components/TutorialCard/TutorialCard.module.scss`
|
||||
9. `packages/noodl-core-ui/src/preview/launcher/Launcher/components/TutorialDetailModal/TutorialDetailModal.tsx`
|
||||
10. `packages/noodl-core-ui/src/preview/launcher/Launcher/components/DifficultyBadge/DifficultyBadge.tsx`
|
||||
11. `packages/noodl-core-ui/src/preview/launcher/Launcher/components/ProgressRing/ProgressRing.tsx`
|
||||
12. `packages/noodl-core-ui/src/preview/launcher/Launcher/components/DurationLabel/DurationLabel.tsx`
|
||||
13. `packages/noodl-core-ui/src/preview/launcher/Launcher/hooks/useTutorials.ts`
|
||||
|
||||
## Files to Modify
|
||||
|
||||
1. `packages/noodl-core-ui/src/preview/launcher/Launcher/views/LearningCenter.tsx`
|
||||
- Replace empty component with full implementation
|
||||
- Move to folder structure
|
||||
|
||||
2. `packages/noodl-core-ui/src/preview/launcher/Launcher/Launcher.tsx`
|
||||
- Update import for LearningCenter
|
||||
|
||||
3. `packages/noodl-editor/src/editor/src/models/lessonprojectsmodel.ts`
|
||||
- Either extend or create adapter for new TutorialService
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### Phase 1: Data Layer
|
||||
1. Create TutorialService
|
||||
2. Define data types
|
||||
3. Create useTutorials hook
|
||||
4. Migrate existing lesson data structure
|
||||
|
||||
### Phase 2: Core Components
|
||||
1. Create TutorialCard component
|
||||
2. Create DifficultyBadge
|
||||
3. Create ProgressRing
|
||||
4. Create DurationLabel
|
||||
|
||||
### Phase 3: Main Layout
|
||||
1. Build LearningCenter view
|
||||
2. Create TutorialCategory sections
|
||||
3. Add ContinueLearning section
|
||||
4. Implement category collapse/expand
|
||||
|
||||
### Phase 4: Filtering
|
||||
1. Create TutorialFilters component
|
||||
2. Implement search
|
||||
3. Implement filter dropdowns
|
||||
4. Wire up filter state
|
||||
|
||||
### Phase 5: Detail View
|
||||
1. Create TutorialDetailModal
|
||||
2. Add start/continue/restart logic
|
||||
3. Show objectives and prerequisites
|
||||
|
||||
### Phase 6: Polish
|
||||
1. Add loading states
|
||||
2. Add empty states
|
||||
3. Smooth animations
|
||||
4. Accessibility review
|
||||
|
||||
## Component Specifications
|
||||
|
||||
### TutorialCard
|
||||
|
||||
```
|
||||
┌────────────────────────────┐
|
||||
│ [📹] OpenNoodl Basics │ <- Type icon + Title
|
||||
│ Learn the fundamentals │ <- Description (truncated)
|
||||
│ 🟢 Beginner ⏱️ 15 min │ <- Difficulty + Duration
|
||||
│ [●●●●●○○○○○] 47% │ <- Progress bar
|
||||
└────────────────────────────┘
|
||||
```
|
||||
|
||||
Props:
|
||||
- `tutorial: Tutorial`
|
||||
- `progress: TutorialProgress`
|
||||
- `onClick: () => void`
|
||||
- `variant?: 'compact' | 'expanded'`
|
||||
|
||||
### DifficultyBadge
|
||||
|
||||
| Level | Color | Icon |
|
||||
|-------|-------|------|
|
||||
| Beginner | Green (#22C55E) | 🟢 |
|
||||
| Intermediate | Yellow (#EAB308) | 🟡 |
|
||||
| Advanced | Red (#EF4444) | 🔴 |
|
||||
|
||||
### ProgressRing
|
||||
|
||||
Small circular progress indicator:
|
||||
- Size: 24px
|
||||
- Stroke width: 3px
|
||||
- Background: gray
|
||||
- Fill: green (completing), green (complete)
|
||||
- Center: percentage or checkmark
|
||||
|
||||
## Compatibility Notes
|
||||
|
||||
### Existing Lesson System
|
||||
|
||||
The current system uses:
|
||||
```typescript
|
||||
// lessonprojectsmodel.ts
|
||||
interface LessonTemplate {
|
||||
name: string;
|
||||
description: string;
|
||||
iconURL: string;
|
||||
templateURL: string;
|
||||
progress?: number;
|
||||
}
|
||||
```
|
||||
|
||||
The new system should:
|
||||
1. Be backwards compatible with existing templates
|
||||
2. Migrate progress data from old format
|
||||
3. Support new enhanced metadata
|
||||
|
||||
### Migration Path
|
||||
|
||||
1. Keep `lessonprojectsmodel.ts` working during transition
|
||||
2. Create adapter in TutorialService to read old data
|
||||
3. Enhance existing tutorials with new metadata
|
||||
4. Eventually deprecate old model
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
- [ ] Tutorials load from docs endpoint
|
||||
- [ ] Categories display correctly
|
||||
- [ ] Category collapse/expand works
|
||||
- [ ] Progress displays correctly
|
||||
- [ ] Continue Learning section shows in-progress tutorials
|
||||
- [ ] Search filters tutorials
|
||||
- [ ] Difficulty filter works
|
||||
- [ ] Progress filter works
|
||||
- [ ] Clicking card shows detail modal
|
||||
- [ ] Start Tutorial launches tutorial
|
||||
- [ ] Continue Tutorial resumes from last point
|
||||
- [ ] Restart Tutorial resets progress
|
||||
- [ ] Progress persists across sessions
|
||||
- [ ] Empty states display appropriately
|
||||
- [ ] Responsive at different window sizes
|
||||
|
||||
## Dependencies
|
||||
|
||||
- DASH-001 (Tabbed Navigation System)
|
||||
|
||||
### External Dependencies
|
||||
|
||||
None - uses existing noodl-core-ui components.
|
||||
|
||||
## Blocked By
|
||||
|
||||
- DASH-001
|
||||
|
||||
## Blocks
|
||||
|
||||
- None
|
||||
|
||||
## Estimated Effort
|
||||
|
||||
- TutorialService: 2-3 hours
|
||||
- TutorialCard components: 2-3 hours
|
||||
- LearningCenter layout: 3-4 hours
|
||||
- Filtering: 2-3 hours
|
||||
- Detail modal: 2-3 hours
|
||||
- Polish & testing: 2-3 hours
|
||||
- **Total: 13-19 hours**
|
||||
|
||||
## Success Criteria
|
||||
|
||||
1. Tutorials are organized by category
|
||||
2. Users can easily find tutorials by search/filter
|
||||
3. Progress is clearly visible
|
||||
4. "Continue Learning" helps users resume work
|
||||
5. Tutorial cards are compact but informative
|
||||
6. Detail modal provides all needed information
|
||||
7. System is backwards compatible with existing tutorials
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- Video tutorial playback within app
|
||||
- Community-contributed tutorials
|
||||
- Tutorial recommendations based on usage
|
||||
- Learning paths (curated sequences)
|
||||
- Achievements/badges for completion
|
||||
- Tutorial ratings/feedback
|
||||
|
||||
## Design Notes
|
||||
|
||||
The learning center should feel like:
|
||||
- Duolingo's course browser (compact, progress-focused)
|
||||
- Coursera's course catalog (categorized, searchable)
|
||||
- VS Code's Getting Started (helpful, not overwhelming)
|
||||
|
||||
Prioritize getting users to relevant content quickly. The most common flow is:
|
||||
1. See "Continue Learning" → resume last tutorial
|
||||
2. Browse category → find new tutorial → start
|
||||
3. Search for specific topic → find tutorial → start
|
||||
|
||||
Don't make users click through multiple screens to start learning.
|
||||
@@ -0,0 +1,150 @@
|
||||
# DASH Series: Dashboard UX Foundation
|
||||
|
||||
## Overview
|
||||
|
||||
The DASH series modernizes the OpenNoodl editor dashboard, transforming it from a basic project launcher into a proper workspace management hub. These tasks focus on the **new React 19 launcher** in `packages/noodl-core-ui/src/preview/launcher/`.
|
||||
|
||||
## Target Environment
|
||||
|
||||
- **Editor**: React 19 version only
|
||||
- **Runtime**: React 19 version (if applicable)
|
||||
- **Backwards Compatibility**: Not required for old launcher
|
||||
|
||||
## Task Dependency Graph
|
||||
|
||||
```
|
||||
DASH-001 (Tabbed Navigation)
|
||||
│
|
||||
├── DASH-002 (Project List Redesign)
|
||||
│ │
|
||||
│ └── DASH-003 (Project Organization)
|
||||
│
|
||||
└── DASH-004 (Tutorial Section Redesign)
|
||||
```
|
||||
|
||||
## Task Summary
|
||||
|
||||
| Task ID | Name | Est. Hours | Priority |
|
||||
|---------|------|------------|----------|
|
||||
| DASH-001 | Tabbed Navigation System | 5-8 | Critical |
|
||||
| DASH-002 | Project List Redesign | 9-14 | High |
|
||||
| DASH-003 | Project Organization | 16-22 | Medium |
|
||||
| DASH-004 | Tutorial Section Redesign | 13-19 | Medium |
|
||||
|
||||
**Total Estimated: 43-63 hours**
|
||||
|
||||
## Implementation Order
|
||||
|
||||
### Week 1: Foundation
|
||||
1. **DASH-001** - Tabbed navigation (foundation for everything)
|
||||
2. **DASH-004** - Tutorial redesign (can parallel with DASH-002)
|
||||
|
||||
### Week 2: Project Management
|
||||
3. **DASH-002** - Project list redesign
|
||||
4. **DASH-003** - Folders and tags
|
||||
|
||||
## Key Technical Decisions
|
||||
|
||||
### Location
|
||||
All new components go in:
|
||||
```
|
||||
packages/noodl-core-ui/src/preview/launcher/Launcher/
|
||||
```
|
||||
|
||||
### State Management
|
||||
- Use React Context for launcher-wide state
|
||||
- Use electron-store for persistence
|
||||
- Keep component state minimal
|
||||
|
||||
### Styling
|
||||
- Use existing noodl-core-ui components
|
||||
- CSS Modules for custom styling
|
||||
- Follow existing color/spacing tokens
|
||||
|
||||
### Data
|
||||
- Services in `packages/noodl-editor/src/editor/src/services/`
|
||||
- Hooks in launcher `hooks/` folder
|
||||
- Types in component folders or shared types file
|
||||
|
||||
## Shared Components to Create
|
||||
|
||||
These components will be reused across DASH tasks:
|
||||
|
||||
| Component | Created In | Used By |
|
||||
|-----------|------------|---------|
|
||||
| TabBar | DASH-001 | All views |
|
||||
| GitStatusBadge | DASH-002 | Project list |
|
||||
| ViewModeToggle | DASH-002 | Project list |
|
||||
| FolderTree | DASH-003 | Sidebar |
|
||||
| TagPill | DASH-003 | Project rows |
|
||||
| ProgressRing | DASH-004 | Tutorial cards |
|
||||
| DifficultyBadge | DASH-004 | Tutorial cards |
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
Each task includes a testing checklist. Additionally:
|
||||
|
||||
1. **Visual Testing**: Use Storybook for component development
|
||||
2. **Integration Testing**: Test in actual launcher context
|
||||
3. **Persistence Testing**: Verify data survives app restart
|
||||
4. **Performance Testing**: Check with 100+ projects/tutorials
|
||||
|
||||
## Cline Usage Notes
|
||||
|
||||
### Before Starting Each Task
|
||||
|
||||
1. Read the task document completely
|
||||
2. Explore the existing code in `packages/noodl-core-ui/src/preview/launcher/`
|
||||
3. Check existing components in `packages/noodl-core-ui/src/components/`
|
||||
4. Understand the data flow
|
||||
|
||||
### During Implementation
|
||||
|
||||
1. Create components incrementally with Storybook stories
|
||||
2. Test in isolation before integration
|
||||
3. Update imports/exports in index files
|
||||
4. Follow existing code style
|
||||
|
||||
### Confidence Checkpoints
|
||||
|
||||
Rate confidence (1-10) at these points:
|
||||
- After reading task document
|
||||
- After exploring existing code
|
||||
- Before creating first component
|
||||
- After completing each phase
|
||||
- Before marking task complete
|
||||
|
||||
### Common Gotchas
|
||||
|
||||
1. **Mock Data**: The launcher currently uses mock data - don't try to connect to real data yet
|
||||
2. **FIXME Alerts**: Many click handlers are `alert('FIXME: ...')` - that's expected
|
||||
3. **Storybook**: Run `npm run storybook` in noodl-core-ui to test components
|
||||
4. **Imports**: noodl-core-ui uses path aliases - check existing imports for patterns
|
||||
|
||||
## Success Criteria (Series Complete)
|
||||
|
||||
1. ✅ Launcher has tabbed navigation (Projects, Learn, Templates)
|
||||
2. ✅ Projects display in sortable list with git status
|
||||
3. ✅ Projects can be organized with folders and tags
|
||||
4. ✅ Tutorials are organized by category with progress tracking
|
||||
5. ✅ All preferences persist across sessions
|
||||
6. ✅ UI is responsive and accessible
|
||||
7. ✅ New components are reusable
|
||||
|
||||
## Future Work (Post-DASH)
|
||||
|
||||
The DASH series sets up infrastructure for:
|
||||
- **GIT series**: GitHub integration, sync status
|
||||
- **COMP series**: Shared components system
|
||||
- **AI series**: AI project creation
|
||||
- **DEPLOY series**: Deployment automation
|
||||
|
||||
These will be documented separately.
|
||||
|
||||
## Files in This Series
|
||||
|
||||
- `DASH-001-tabbed-navigation.md`
|
||||
- `DASH-002-project-list-redesign.md`
|
||||
- `DASH-003-project-organization.md`
|
||||
- `DASH-004-tutorial-section-redesign.md`
|
||||
- `DASH-OVERVIEW.md` (this file)
|
||||
@@ -0,0 +1,335 @@
|
||||
# GIT-001: GitHub OAuth Integration
|
||||
|
||||
## Overview
|
||||
|
||||
Add GitHub OAuth as an authentication method alongside the existing Personal Access Token (PAT) approach. This provides a smoother onboarding experience and enables access to GitHub's API for advanced features like repository browsing and organization access.
|
||||
|
||||
## Context
|
||||
|
||||
Currently, Noodl uses Personal Access Tokens for GitHub authentication:
|
||||
- Stored per-project in `GitStore` (encrypted locally)
|
||||
- Prompted via `GitProviderPopout` component
|
||||
- Used by `trampoline-askpass-handler` for git operations
|
||||
|
||||
OAuth provides advantages:
|
||||
- No need to manually create and copy PATs
|
||||
- Automatic token refresh
|
||||
- Access to GitHub API (not just git operations)
|
||||
- Org/repo scope selection
|
||||
|
||||
## Current State
|
||||
|
||||
### Existing Authentication Flow
|
||||
```
|
||||
User → GitProviderPopout → Enter PAT → GitStore.set() → Git operations use PAT
|
||||
```
|
||||
|
||||
### Key Files
|
||||
- `packages/noodl-editor/src/editor/src/views/panels/VersionControlPanel/components/GitProviderPopout/`
|
||||
- `packages/noodl-store/src/GitStore.ts` (assumed location)
|
||||
- `packages/noodl-git/src/core/trampoline/trampoline-askpass-handler.ts`
|
||||
|
||||
## Requirements
|
||||
|
||||
### Functional Requirements
|
||||
|
||||
1. **OAuth Flow**
|
||||
- "Connect with GitHub" button in settings/dashboard
|
||||
- Opens GitHub OAuth in system browser
|
||||
- Handles callback via custom protocol (`noodl://github-callback`)
|
||||
- Exchanges code for access token
|
||||
- Stores token securely
|
||||
|
||||
2. **Scope Selection**
|
||||
- Request appropriate scopes: `repo`, `read:org`, `read:user`
|
||||
- Display what permissions are being requested
|
||||
- Option to request additional scopes later
|
||||
|
||||
3. **Account Management**
|
||||
- Show connected GitHub account (avatar, username)
|
||||
- "Disconnect" option
|
||||
- Support multiple accounts (stretch goal)
|
||||
|
||||
4. **Organization Access**
|
||||
- List user's organizations
|
||||
- Allow selecting which orgs to access
|
||||
- Remember org selection
|
||||
|
||||
5. **Token Management**
|
||||
- Secure storage using electron's safeStorage or keytar
|
||||
- Automatic token refresh (GitHub OAuth tokens don't expire but can be revoked)
|
||||
- Handle token revocation gracefully
|
||||
|
||||
6. **Fallback to PAT**
|
||||
- Keep existing PAT flow as alternative
|
||||
- "Use Personal Access Token instead" option
|
||||
- Clear migration path from PAT to OAuth
|
||||
|
||||
### Non-Functional Requirements
|
||||
|
||||
- OAuth flow completes in <30 seconds
|
||||
- Token stored securely (encrypted at rest)
|
||||
- Works behind corporate proxies
|
||||
- Graceful offline handling
|
||||
|
||||
## Technical Approach
|
||||
|
||||
### 1. GitHub OAuth App Setup
|
||||
|
||||
Register OAuth App in GitHub:
|
||||
- Application name: "OpenNoodl"
|
||||
- Homepage URL: `https://opennoodl.net`
|
||||
- Callback URL: `noodl://github-callback`
|
||||
|
||||
Store Client ID in app (Client Secret not needed for public clients using PKCE).
|
||||
|
||||
### 2. OAuth Flow Implementation
|
||||
|
||||
```typescript
|
||||
// packages/noodl-editor/src/editor/src/services/GitHubOAuthService.ts
|
||||
|
||||
class GitHubOAuthService {
|
||||
private static instance: GitHubOAuthService;
|
||||
|
||||
// OAuth flow
|
||||
async initiateOAuth(): Promise<void>;
|
||||
async handleCallback(code: string, state: string): Promise<GitHubToken>;
|
||||
|
||||
// Token management
|
||||
async getToken(): Promise<string | null>;
|
||||
async refreshToken(): Promise<string>;
|
||||
async revokeToken(): Promise<void>;
|
||||
|
||||
// Account info
|
||||
async getCurrentUser(): Promise<GitHubUser>;
|
||||
async getOrganizations(): Promise<GitHubOrg[]>;
|
||||
|
||||
// State
|
||||
isAuthenticated(): boolean;
|
||||
onAuthStateChanged(callback: (authenticated: boolean) => void): void;
|
||||
}
|
||||
```
|
||||
|
||||
### 3. PKCE Flow (Recommended for Desktop Apps)
|
||||
|
||||
```typescript
|
||||
// Generate PKCE challenge
|
||||
function generatePKCE(): { verifier: string; challenge: string } {
|
||||
const verifier = crypto.randomBytes(32).toString('base64url');
|
||||
const challenge = crypto
|
||||
.createHash('sha256')
|
||||
.update(verifier)
|
||||
.digest('base64url');
|
||||
return { verifier, challenge };
|
||||
}
|
||||
|
||||
// OAuth URL
|
||||
function getAuthorizationUrl(state: string, challenge: string): string {
|
||||
const params = new URLSearchParams({
|
||||
client_id: GITHUB_CLIENT_ID,
|
||||
redirect_uri: 'noodl://github-callback',
|
||||
scope: 'repo read:org read:user',
|
||||
state,
|
||||
code_challenge: challenge,
|
||||
code_challenge_method: 'S256'
|
||||
});
|
||||
return `https://github.com/login/oauth/authorize?${params}`;
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Deep Link Handler
|
||||
|
||||
```typescript
|
||||
// packages/noodl-editor/src/main/main.js
|
||||
|
||||
// Register protocol handler
|
||||
app.setAsDefaultProtocolClient('noodl');
|
||||
|
||||
// Handle deep links
|
||||
app.on('open-url', (event, url) => {
|
||||
event.preventDefault();
|
||||
if (url.startsWith('noodl://github-callback')) {
|
||||
const params = new URL(url).searchParams;
|
||||
const code = params.get('code');
|
||||
const state = params.get('state');
|
||||
handleGitHubCallback(code, state);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 5. Secure Token Storage
|
||||
|
||||
```typescript
|
||||
// Use electron's safeStorage API
|
||||
import { safeStorage } from 'electron';
|
||||
|
||||
async function storeToken(token: string): Promise<void> {
|
||||
const encrypted = safeStorage.encryptString(token);
|
||||
await store.set('github.token', encrypted.toString('base64'));
|
||||
}
|
||||
|
||||
async function getToken(): Promise<string | null> {
|
||||
const encrypted = await store.get('github.token');
|
||||
if (!encrypted) return null;
|
||||
return safeStorage.decryptString(Buffer.from(encrypted, 'base64'));
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Integration with Existing Git Auth
|
||||
|
||||
```typescript
|
||||
// packages/noodl-utils/LocalProjectsModel.ts
|
||||
|
||||
setCurrentGlobalGitAuth(projectId: string) {
|
||||
const func = async (endpoint: string) => {
|
||||
if (endpoint.includes('github.com')) {
|
||||
// Try OAuth token first
|
||||
const oauthToken = await GitHubOAuthService.instance.getToken();
|
||||
if (oauthToken) {
|
||||
return {
|
||||
username: 'oauth2',
|
||||
password: oauthToken
|
||||
};
|
||||
}
|
||||
|
||||
// Fall back to PAT
|
||||
const config = await GitStore.get('github', projectId);
|
||||
return {
|
||||
username: 'noodl',
|
||||
password: config?.password
|
||||
};
|
||||
}
|
||||
// ... rest of existing logic
|
||||
};
|
||||
|
||||
setRequestGitAccount(func);
|
||||
}
|
||||
```
|
||||
|
||||
## Files to Create
|
||||
|
||||
1. `packages/noodl-editor/src/editor/src/services/GitHubOAuthService.ts`
|
||||
2. `packages/noodl-editor/src/editor/src/services/GitHubApiClient.ts`
|
||||
3. `packages/noodl-core-ui/src/preview/launcher/Launcher/components/GitHubAccountCard/GitHubAccountCard.tsx`
|
||||
4. `packages/noodl-core-ui/src/preview/launcher/Launcher/components/GitHubConnectButton/GitHubConnectButton.tsx`
|
||||
5. `packages/noodl-core-ui/src/preview/launcher/Launcher/components/OrgSelector/OrgSelector.tsx`
|
||||
6. `packages/noodl-editor/src/editor/src/views/panels/VersionControlPanel/components/GitProviderPopout/sections/OAuthSection.tsx`
|
||||
|
||||
## Files to Modify
|
||||
|
||||
1. `packages/noodl-editor/src/main/main.js`
|
||||
- Add deep link protocol handler for `noodl://`
|
||||
|
||||
2. `packages/noodl-utils/LocalProjectsModel.ts`
|
||||
- Update `setCurrentGlobalGitAuth` to prefer OAuth token
|
||||
|
||||
3. `packages/noodl-editor/src/editor/src/views/panels/VersionControlPanel/components/GitProviderPopout/GitProviderPopout.tsx`
|
||||
- Add OAuth option alongside PAT
|
||||
|
||||
4. `packages/noodl-core-ui/src/preview/launcher/Launcher/components/LauncherSidebar/LauncherSidebar.tsx`
|
||||
- Add GitHub account display/connect button
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### Phase 1: OAuth Service Foundation
|
||||
1. Create GitHubOAuthService class
|
||||
2. Implement PKCE flow
|
||||
3. Set up deep link handler in main process
|
||||
4. Implement secure token storage
|
||||
|
||||
### Phase 2: UI Components
|
||||
1. Create GitHubConnectButton
|
||||
2. Create GitHubAccountCard
|
||||
3. Add OAuth section to GitProviderPopout
|
||||
4. Add account display to launcher sidebar
|
||||
|
||||
### Phase 3: API Integration
|
||||
1. Create GitHubApiClient for REST API calls
|
||||
2. Implement user info fetching
|
||||
3. Implement organization listing
|
||||
4. Create OrgSelector component
|
||||
|
||||
### Phase 4: Git Integration
|
||||
1. Update LocalProjectsModel auth function
|
||||
2. Test with git operations
|
||||
3. Handle token expiry/revocation
|
||||
4. Add fallback to PAT
|
||||
|
||||
### Phase 5: Polish
|
||||
1. Error handling and messages
|
||||
2. Offline handling
|
||||
3. Loading states
|
||||
4. Settings persistence
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **PKCE**: Use PKCE flow instead of client secret (more secure for desktop apps)
|
||||
2. **Token Storage**: Use electron's safeStorage API (OS-level encryption)
|
||||
3. **State Parameter**: Verify state to prevent CSRF attacks
|
||||
4. **Scope Limitation**: Request minimum required scopes
|
||||
5. **Token Exposure**: Never log tokens, clear from memory when not needed
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
- [ ] OAuth flow completes successfully
|
||||
- [ ] Token stored securely
|
||||
- [ ] Token retrieved correctly for git operations
|
||||
- [ ] Clone works with OAuth token
|
||||
- [ ] Push works with OAuth token
|
||||
- [ ] Pull works with OAuth token
|
||||
- [ ] Disconnect clears token
|
||||
- [ ] Fallback to PAT works
|
||||
- [ ] Organizations listed correctly
|
||||
- [ ] Deep link works on macOS
|
||||
- [ ] Deep link works on Windows
|
||||
- [ ] Handles network errors gracefully
|
||||
- [ ] Handles token revocation gracefully
|
||||
|
||||
## Dependencies
|
||||
|
||||
- DASH-001 (for launcher context to display account)
|
||||
|
||||
### External Dependencies
|
||||
|
||||
May need to add:
|
||||
```json
|
||||
{
|
||||
"keytar": "^7.9.0" // Alternative to safeStorage for older Electron
|
||||
}
|
||||
```
|
||||
|
||||
## Blocked By
|
||||
|
||||
- DASH-001 (Tabbed Navigation) - for launcher UI placement
|
||||
|
||||
## Blocks
|
||||
|
||||
- GIT-003 (Repository Cloning) - needs auth for private repos
|
||||
- COMP-004 (Organization Components) - needs org access
|
||||
|
||||
## Estimated Effort
|
||||
|
||||
- OAuth service: 4-6 hours
|
||||
- Deep link handler: 2-3 hours
|
||||
- UI components: 3-4 hours
|
||||
- Git integration: 2-3 hours
|
||||
- Testing & polish: 3-4 hours
|
||||
- **Total: 14-20 hours**
|
||||
|
||||
## Success Criteria
|
||||
|
||||
1. Users can authenticate with GitHub via OAuth
|
||||
2. OAuth tokens are stored securely
|
||||
3. Git operations work with OAuth tokens
|
||||
4. Users can see their connected account
|
||||
5. Users can disconnect and reconnect
|
||||
6. PAT remains available as fallback
|
||||
7. Flow works on both macOS and Windows
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- Multiple GitHub account support
|
||||
- GitLab OAuth
|
||||
- Bitbucket OAuth
|
||||
- GitHub Enterprise support
|
||||
- Fine-grained personal access tokens
|
||||
@@ -0,0 +1,426 @@
|
||||
# GIT-002: Git Status Dashboard Visibility
|
||||
|
||||
## Overview
|
||||
|
||||
Surface git status information directly in the project list on the dashboard, allowing users to see at a glance which projects need attention (uncommitted changes, unpushed commits, available updates) without opening each project.
|
||||
|
||||
## Context
|
||||
|
||||
Currently, git status is only visible inside the VersionControlPanel after opening a project. Users with many projects have no way to know which ones have uncommitted changes or need syncing.
|
||||
|
||||
The new launcher already has mock data for git sync status in `LauncherProjectCard`, but it's not connected to real data.
|
||||
|
||||
### Existing Infrastructure
|
||||
|
||||
From `LauncherProjectCard.tsx`:
|
||||
```typescript
|
||||
export enum CloudSyncType {
|
||||
None = 'none',
|
||||
Git = 'git'
|
||||
}
|
||||
|
||||
export interface LauncherProjectData {
|
||||
cloudSyncMeta: {
|
||||
type: CloudSyncType;
|
||||
source?: string; // Remote URL
|
||||
};
|
||||
pullAmount?: number;
|
||||
pushAmount?: number;
|
||||
uncommittedChangesAmount?: number;
|
||||
}
|
||||
```
|
||||
|
||||
From `VersionControlPanel/context/fetch.context.ts`:
|
||||
```typescript
|
||||
// Already calculates:
|
||||
localCommitCount // Commits ahead of remote
|
||||
remoteCommitCount // Commits behind remote
|
||||
workingDirectoryStatus // Uncommitted files
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
### Functional Requirements
|
||||
|
||||
1. **Status Indicators in Project List**
|
||||
- Not Initialized: Gray indicator, no version control
|
||||
- Local Only: Yellow indicator, git but no remote
|
||||
- Synced: Green checkmark, up to date
|
||||
- Has Uncommitted Changes: Yellow dot, local modifications
|
||||
- Ahead: Blue up arrow, local commits to push
|
||||
- Behind: Orange down arrow, remote commits to pull
|
||||
- Diverged: Red warning, both ahead and behind
|
||||
|
||||
2. **Status Details**
|
||||
- Tooltip showing details on hover
|
||||
- "3 commits to push, 2 to pull"
|
||||
- "5 uncommitted files"
|
||||
- Last sync time
|
||||
|
||||
3. **Quick Actions**
|
||||
- Quick sync button (fetch + show status)
|
||||
- Link to open Version Control panel
|
||||
|
||||
4. **Background Refresh**
|
||||
- Check status on dashboard load
|
||||
- Periodic refresh (every 5 minutes)
|
||||
- Manual refresh button
|
||||
- Status cached to avoid repeated git operations
|
||||
|
||||
5. **Performance**
|
||||
- Parallel status checks for multiple projects
|
||||
- Debounced/throttled to avoid overwhelming git
|
||||
- Cached results with TTL
|
||||
|
||||
### Non-Functional Requirements
|
||||
|
||||
- Status check per project: <500ms
|
||||
- Dashboard load not blocked by status checks
|
||||
- Works offline (shows cached/stale data)
|
||||
|
||||
## Data Model
|
||||
|
||||
### Git Status Types
|
||||
|
||||
```typescript
|
||||
enum ProjectGitStatus {
|
||||
Unknown = 'unknown', // Haven't checked yet
|
||||
NotInitialized = 'not-init', // Not a git repo
|
||||
LocalOnly = 'local-only', // Git but no remote
|
||||
Synced = 'synced', // Up to date with remote
|
||||
Uncommitted = 'uncommitted', // Has local changes
|
||||
Ahead = 'ahead', // Has commits to push
|
||||
Behind = 'behind', // Has commits to pull
|
||||
Diverged = 'diverged', // Both ahead and behind
|
||||
Error = 'error' // Failed to check
|
||||
}
|
||||
|
||||
interface ProjectGitStatusDetails {
|
||||
status: ProjectGitStatus;
|
||||
aheadCount?: number;
|
||||
behindCount?: number;
|
||||
uncommittedCount?: number;
|
||||
lastFetchTime?: number;
|
||||
remoteUrl?: string;
|
||||
currentBranch?: string;
|
||||
error?: string;
|
||||
}
|
||||
```
|
||||
|
||||
### Cache Structure
|
||||
|
||||
```typescript
|
||||
interface GitStatusCache {
|
||||
[projectPath: string]: {
|
||||
status: ProjectGitStatusDetails;
|
||||
checkedAt: number;
|
||||
isStale: boolean;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Technical Approach
|
||||
|
||||
### 1. Git Status Service
|
||||
|
||||
```typescript
|
||||
// packages/noodl-editor/src/editor/src/services/ProjectGitStatusService.ts
|
||||
|
||||
class ProjectGitStatusService {
|
||||
private static instance: ProjectGitStatusService;
|
||||
private cache: GitStatusCache = {};
|
||||
private checkQueue: Set<string> = new Set();
|
||||
private isChecking = false;
|
||||
|
||||
// Check single project
|
||||
async checkStatus(projectPath: string): Promise<ProjectGitStatusDetails>;
|
||||
|
||||
// Check multiple projects (batched)
|
||||
async checkStatusBatch(projectPaths: string[]): Promise<Map<string, ProjectGitStatusDetails>>;
|
||||
|
||||
// Get cached status
|
||||
getCachedStatus(projectPath: string): ProjectGitStatusDetails | null;
|
||||
|
||||
// Clear cache
|
||||
invalidateCache(projectPath?: string): void;
|
||||
|
||||
// Subscribe to status changes
|
||||
onStatusChanged(callback: (path: string, status: ProjectGitStatusDetails) => void): () => void;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Status Check Implementation
|
||||
|
||||
```typescript
|
||||
async checkStatus(projectPath: string): Promise<ProjectGitStatusDetails> {
|
||||
const git = new Git(mergeProject);
|
||||
|
||||
try {
|
||||
// Check if it's a git repo
|
||||
const gitPath = await getTopLevelWorkingDirectory(projectPath);
|
||||
if (!gitPath) {
|
||||
return { status: ProjectGitStatus.NotInitialized };
|
||||
}
|
||||
|
||||
await git.openRepository(projectPath);
|
||||
|
||||
// Check for remote
|
||||
const remoteName = await git.getRemoteName();
|
||||
if (!remoteName) {
|
||||
return { status: ProjectGitStatus.LocalOnly };
|
||||
}
|
||||
|
||||
// Get working directory status
|
||||
const workingStatus = await git.status();
|
||||
const uncommittedCount = workingStatus.length;
|
||||
|
||||
// Get commit counts (requires fetch for accuracy)
|
||||
const commits = await git.getCommitsCurrentBranch();
|
||||
const aheadCount = commits.filter(c => c.isLocalAhead).length;
|
||||
const behindCount = commits.filter(c => c.isRemoteAhead).length;
|
||||
|
||||
// Determine status
|
||||
let status: ProjectGitStatus;
|
||||
if (uncommittedCount > 0) {
|
||||
status = ProjectGitStatus.Uncommitted;
|
||||
} else if (aheadCount > 0 && behindCount > 0) {
|
||||
status = ProjectGitStatus.Diverged;
|
||||
} else if (aheadCount > 0) {
|
||||
status = ProjectGitStatus.Ahead;
|
||||
} else if (behindCount > 0) {
|
||||
status = ProjectGitStatus.Behind;
|
||||
} else {
|
||||
status = ProjectGitStatus.Synced;
|
||||
}
|
||||
|
||||
return {
|
||||
status,
|
||||
aheadCount,
|
||||
behindCount,
|
||||
uncommittedCount,
|
||||
lastFetchTime: Date.now(),
|
||||
remoteUrl: git.OriginUrl,
|
||||
currentBranch: await git.getCurrentBranchName()
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
status: ProjectGitStatus.Error,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Dashboard Integration Hook
|
||||
|
||||
```typescript
|
||||
// packages/noodl-core-ui/src/preview/launcher/Launcher/hooks/useProjectGitStatus.ts
|
||||
|
||||
function useProjectGitStatus(projectPaths: string[]) {
|
||||
const [statuses, setStatuses] = useState<Map<string, ProjectGitStatusDetails>>(new Map());
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
// Initial check
|
||||
ProjectGitStatusService.instance
|
||||
.checkStatusBatch(projectPaths)
|
||||
.then(setStatuses)
|
||||
.finally(() => setIsLoading(false));
|
||||
|
||||
// Subscribe to updates
|
||||
const unsubscribe = ProjectGitStatusService.instance.onStatusChanged((path, status) => {
|
||||
setStatuses(prev => new Map(prev).set(path, status));
|
||||
});
|
||||
|
||||
return unsubscribe;
|
||||
}, [projectPaths]);
|
||||
|
||||
const refresh = useCallback(() => {
|
||||
ProjectGitStatusService.instance.invalidateCache();
|
||||
// Re-trigger check
|
||||
}, []);
|
||||
|
||||
return { statuses, isLoading, refresh };
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Visual Status Badge
|
||||
|
||||
Already started in DASH-002 as `GitStatusBadge`, but needs real data connection:
|
||||
|
||||
```typescript
|
||||
// Enhanced GitStatusBadge props
|
||||
interface GitStatusBadgeProps {
|
||||
status: ProjectGitStatus;
|
||||
details: ProjectGitStatusDetails;
|
||||
showTooltip?: boolean;
|
||||
size?: 'small' | 'medium';
|
||||
}
|
||||
```
|
||||
|
||||
## Files to Create
|
||||
|
||||
1. `packages/noodl-editor/src/editor/src/services/ProjectGitStatusService.ts`
|
||||
2. `packages/noodl-core-ui/src/preview/launcher/Launcher/hooks/useProjectGitStatus.ts`
|
||||
3. `packages/noodl-core-ui/src/preview/launcher/Launcher/components/GitStatusBadge/GitStatusBadge.tsx` (if not created in DASH-002)
|
||||
4. `packages/noodl-core-ui/src/preview/launcher/Launcher/components/GitStatusBadge/GitStatusBadge.module.scss`
|
||||
5. `packages/noodl-core-ui/src/preview/launcher/Launcher/components/GitStatusTooltip/GitStatusTooltip.tsx`
|
||||
|
||||
## Files to Modify
|
||||
|
||||
1. `packages/noodl-core-ui/src/preview/launcher/Launcher/views/Projects.tsx`
|
||||
- Use `useProjectGitStatus` hook
|
||||
- Pass status to project cards/rows
|
||||
|
||||
2. `packages/noodl-core-ui/src/preview/launcher/Launcher/components/ProjectList/ProjectListRow.tsx`
|
||||
- Display GitStatusBadge with real data
|
||||
|
||||
3. `packages/noodl-core-ui/src/preview/launcher/Launcher/components/LauncherProjectCard/LauncherProjectCard.tsx`
|
||||
- Update to use real status data (for grid view)
|
||||
|
||||
4. `packages/noodl-core-ui/src/preview/launcher/Launcher/Launcher.tsx`
|
||||
- Replace mock project data with real data connection
|
||||
|
||||
## Visual Specifications
|
||||
|
||||
### Status Badge Icons & Colors
|
||||
|
||||
| Status | Icon | Color | Background |
|
||||
|--------|------|-------|------------|
|
||||
| Unknown | ◌ (spinner) | Gray | Transparent |
|
||||
| Not Initialized | ⊘ | Gray (#6B7280) | Transparent |
|
||||
| Local Only | 💾 | Yellow (#EAB308) | Yellow/10 |
|
||||
| Synced | ✓ | Green (#22C55E) | Green/10 |
|
||||
| Uncommitted | ● | Yellow (#EAB308) | Yellow/10 |
|
||||
| Ahead | ↑ | Blue (#3B82F6) | Blue/10 |
|
||||
| Behind | ↓ | Orange (#F97316) | Orange/10 |
|
||||
| Diverged | ⚠ | Red (#EF4444) | Red/10 |
|
||||
| Error | ✕ | Red (#EF4444) | Red/10 |
|
||||
|
||||
### Tooltip Content
|
||||
|
||||
```
|
||||
┌─────────────────────────────────┐
|
||||
│ main branch │
|
||||
│ ↑ 3 commits to push │
|
||||
│ ↓ 2 commits to pull │
|
||||
│ ● 5 uncommitted files │
|
||||
│ │
|
||||
│ Last synced: 10 minutes ago │
|
||||
│ Remote: github.com/user/repo │
|
||||
└─────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### Phase 1: Service Foundation
|
||||
1. Create ProjectGitStatusService
|
||||
2. Implement single project status check
|
||||
3. Add caching logic
|
||||
4. Create batch checking with parallelization
|
||||
|
||||
### Phase 2: Hook & Data Flow
|
||||
1. Create useProjectGitStatus hook
|
||||
2. Connect to Projects view
|
||||
3. Replace mock data with real data
|
||||
4. Add loading states
|
||||
|
||||
### Phase 3: Visual Components
|
||||
1. Create/update GitStatusBadge
|
||||
2. Create GitStatusTooltip
|
||||
3. Integrate into ProjectListRow
|
||||
4. Integrate into LauncherProjectCard
|
||||
|
||||
### Phase 4: Refresh & Background
|
||||
1. Add manual refresh button
|
||||
2. Implement periodic background refresh
|
||||
3. Add refresh on window focus
|
||||
4. Handle offline state
|
||||
|
||||
### Phase 5: Polish
|
||||
1. Performance optimization
|
||||
2. Error handling
|
||||
3. Stale data indicators
|
||||
4. Animation on status change
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
1. **Parallel Checking**: Check up to 5 projects simultaneously
|
||||
2. **Debouncing**: Don't re-check same project within 10 seconds
|
||||
3. **Cache TTL**: Status valid for 5 minutes, stale after
|
||||
4. **Lazy Loading**: Only check visible projects first
|
||||
5. **Background Priority**: Use requestIdleCallback for non-visible
|
||||
|
||||
```typescript
|
||||
// Throttled batch check
|
||||
async checkStatusBatch(projectPaths: string[]): Promise<Map<string, ProjectGitStatusDetails>> {
|
||||
const CONCURRENCY = 5;
|
||||
const results = new Map();
|
||||
|
||||
for (let i = 0; i < projectPaths.length; i += CONCURRENCY) {
|
||||
const batch = projectPaths.slice(i, i + CONCURRENCY);
|
||||
const batchResults = await Promise.all(
|
||||
batch.map(path => this.checkStatus(path))
|
||||
);
|
||||
batch.forEach((path, idx) => results.set(path, batchResults[idx]));
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
- [ ] Detects non-git project correctly
|
||||
- [ ] Detects git project without remote
|
||||
- [ ] Shows synced status when up to date
|
||||
- [ ] Shows uncommitted when local changes exist
|
||||
- [ ] Shows ahead when local commits exist
|
||||
- [ ] Shows behind when remote commits exist
|
||||
- [ ] Shows diverged when both ahead and behind
|
||||
- [ ] Tooltip shows correct details
|
||||
- [ ] Refresh updates status
|
||||
- [ ] Status persists across dashboard navigation
|
||||
- [ ] Handles deleted projects gracefully
|
||||
- [ ] Handles network errors gracefully
|
||||
- [ ] Performance acceptable with 20+ projects
|
||||
|
||||
## Dependencies
|
||||
|
||||
- DASH-002 (Project List Redesign) - for UI integration
|
||||
|
||||
## Blocked By
|
||||
|
||||
- DASH-002
|
||||
|
||||
## Blocks
|
||||
|
||||
- GIT-004 (Auto-initialization) - needs status detection
|
||||
- GIT-005 (Enhanced Push/Pull) - shares status infrastructure
|
||||
|
||||
## Estimated Effort
|
||||
|
||||
- Status service: 3-4 hours
|
||||
- Hook & data flow: 2-3 hours
|
||||
- Visual components: 2-3 hours
|
||||
- Background refresh: 2-3 hours
|
||||
- Polish & testing: 2-3 hours
|
||||
- **Total: 11-16 hours**
|
||||
|
||||
## Success Criteria
|
||||
|
||||
1. Git status visible at a glance in project list
|
||||
2. Status updates without manual refresh
|
||||
3. Tooltip provides actionable details
|
||||
4. Performance acceptable with many projects
|
||||
5. Works offline with cached data
|
||||
6. Handles edge cases gracefully
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- Quick commit from dashboard
|
||||
- Quick push/pull buttons per project
|
||||
- Bulk sync all projects
|
||||
- Branch indicator
|
||||
- Last commit message preview
|
||||
- Contributor avatars (from git log)
|
||||
@@ -0,0 +1,346 @@
|
||||
# GIT-003: Repository Cloning
|
||||
|
||||
## Overview
|
||||
|
||||
Add the ability to clone GitHub repositories directly from the Noodl dashboard, similar to how VS Code handles cloning. Users can browse their repositories, select one, choose a local folder, and have the project cloned and opened automatically.
|
||||
|
||||
## Context
|
||||
|
||||
Currently, to work with an existing Noodl project from GitHub, users must:
|
||||
1. Clone the repo manually using git CLI or another tool
|
||||
2. Open Noodl
|
||||
3. Use "Open folder" to navigate to the cloned project
|
||||
|
||||
This task streamlines that to:
|
||||
1. Click "Clone from GitHub"
|
||||
2. Select repository
|
||||
3. Choose folder
|
||||
4. Project opens automatically
|
||||
|
||||
### Existing Infrastructure
|
||||
|
||||
The `noodl-git` package already has clone functionality:
|
||||
```typescript
|
||||
// From git.ts
|
||||
async clone({ url, directory, singleBranch, onProgress }: GitCloneOptions): Promise<void>
|
||||
```
|
||||
|
||||
And clone tests show it working:
|
||||
```typescript
|
||||
await git.clone({
|
||||
url: 'https://github.com/github/testrepo.git',
|
||||
directory: tempDir,
|
||||
onProgress: (progress) => { result.push(progress); }
|
||||
});
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
### Functional Requirements
|
||||
|
||||
1. **Clone Entry Points**
|
||||
- "Clone Repository" button in dashboard toolbar
|
||||
- "Clone from GitHub" option in "Create Project" menu
|
||||
- Right-click empty area → "Clone Repository"
|
||||
|
||||
2. **Repository Browser**
|
||||
- List user's repositories (requires OAuth from GIT-001)
|
||||
- List organization repositories
|
||||
- Search/filter repositories
|
||||
- Show repo details: name, description, visibility, last updated
|
||||
- "Clone URL" input for direct URL entry
|
||||
|
||||
3. **Folder Selection**
|
||||
- Native folder picker dialog
|
||||
- Remember last used parent folder
|
||||
- Validate folder is empty or doesn't exist
|
||||
- Show full path before cloning
|
||||
|
||||
4. **Clone Process**
|
||||
- Progress indicator with stages
|
||||
- Cancel button
|
||||
- Error handling with clear messages
|
||||
- Retry option on failure
|
||||
|
||||
5. **Post-Clone Actions**
|
||||
- Automatically open project in editor
|
||||
- Add to recent projects
|
||||
- Show success notification
|
||||
|
||||
6. **Branch Selection (Optional)**
|
||||
- Default to main/master
|
||||
- Option to select different branch
|
||||
- Shallow clone option for large repos
|
||||
|
||||
### Non-Functional Requirements
|
||||
|
||||
- Clone progress updates smoothly
|
||||
- Cancellation works immediately
|
||||
- Handles large repositories
|
||||
- Works with private repositories (with auth)
|
||||
- Clear error messages for common failures
|
||||
|
||||
## Technical Approach
|
||||
|
||||
### 1. Clone Service
|
||||
|
||||
```typescript
|
||||
// packages/noodl-editor/src/editor/src/services/CloneService.ts
|
||||
|
||||
interface CloneOptions {
|
||||
url: string;
|
||||
directory: string;
|
||||
branch?: string;
|
||||
shallow?: boolean;
|
||||
onProgress?: (progress: CloneProgress) => void;
|
||||
}
|
||||
|
||||
interface CloneProgress {
|
||||
phase: 'counting' | 'compressing' | 'receiving' | 'resolving' | 'checking-out';
|
||||
percent: number;
|
||||
message: string;
|
||||
}
|
||||
|
||||
interface CloneResult {
|
||||
success: boolean;
|
||||
projectPath?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
class CloneService {
|
||||
private static instance: CloneService;
|
||||
private activeClone: AbortController | null = null;
|
||||
|
||||
async clone(options: CloneOptions): Promise<CloneResult>;
|
||||
cancel(): void;
|
||||
|
||||
// GitHub API integration
|
||||
async listUserRepos(): Promise<GitHubRepo[]>;
|
||||
async listOrgRepos(orgName: string): Promise<GitHubRepo[]>;
|
||||
async searchRepos(query: string): Promise<GitHubRepo[]>;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Repository Browser Component
|
||||
|
||||
```typescript
|
||||
// RepoBrowser.tsx
|
||||
|
||||
interface RepoBrowserProps {
|
||||
onSelect: (repo: GitHubRepo) => void;
|
||||
onUrlSubmit: (url: string) => void;
|
||||
}
|
||||
|
||||
interface GitHubRepo {
|
||||
id: number;
|
||||
name: string;
|
||||
fullName: string;
|
||||
description: string;
|
||||
private: boolean;
|
||||
htmlUrl: string;
|
||||
cloneUrl: string;
|
||||
sshUrl: string;
|
||||
defaultBranch: string;
|
||||
updatedAt: string;
|
||||
owner: {
|
||||
login: string;
|
||||
avatarUrl: string;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Clone Modal Flow
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ Clone Repository [×] │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ [🔍 Search repositories... ] │ │
|
||||
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ [Your Repositories ▾] [Organizations: acme-corp ▾] │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ 📁 noodl-project-template ★ 12 2 days ago │ │
|
||||
│ │ A starter template for Noodl projects [Private 🔒] │ │
|
||||
│ ├─────────────────────────────────────────────────────────────────┤ │
|
||||
│ │ 📁 my-awesome-app ★ 5 1 week ago │ │
|
||||
│ │ An awesome application built with Noodl [Public 🌍] │ │
|
||||
│ ├─────────────────────────────────────────────────────────────────┤ │
|
||||
│ │ 📁 client-dashboard ★ 0 3 weeks ago │ │
|
||||
│ │ Dashboard for client project [Private 🔒] │ │
|
||||
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ─── OR enter repository URL ───────────────────────────────────── │
|
||||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ https://github.com/user/repo.git │ │
|
||||
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ [Cancel] [Next →] │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 4. Folder Selection Step
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ Clone Repository [×] │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ Repository: github.com/user/my-awesome-app │
|
||||
│ │
|
||||
│ Clone to: │
|
||||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ /Users/richard/Projects/my-awesome-app [Browse...] │ │
|
||||
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ☐ Clone only the default branch (faster) │
|
||||
│ │
|
||||
│ [← Back] [Cancel] [Clone]│
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 5. Progress Step
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ Cloning Repository [×] │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Cloning my-awesome-app... │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||||
│ │████████████████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░│ 42% │ │
|
||||
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ Receiving objects: 1,234 of 2,891 │
|
||||
│ │
|
||||
│ [Cancel] │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Files to Create
|
||||
|
||||
1. `packages/noodl-editor/src/editor/src/services/CloneService.ts`
|
||||
2. `packages/noodl-core-ui/src/preview/launcher/Launcher/components/CloneModal/CloneModal.tsx`
|
||||
3. `packages/noodl-core-ui/src/preview/launcher/Launcher/components/CloneModal/CloneModal.module.scss`
|
||||
4. `packages/noodl-core-ui/src/preview/launcher/Launcher/components/CloneModal/RepoBrowser.tsx`
|
||||
5. `packages/noodl-core-ui/src/preview/launcher/Launcher/components/CloneModal/FolderSelector.tsx`
|
||||
6. `packages/noodl-core-ui/src/preview/launcher/Launcher/components/CloneModal/CloneProgress.tsx`
|
||||
7. `packages/noodl-core-ui/src/preview/launcher/Launcher/components/RepoCard/RepoCard.tsx`
|
||||
8. `packages/noodl-editor/src/editor/src/services/GitHubApiClient.ts` (if not created in GIT-001)
|
||||
|
||||
## Files to Modify
|
||||
|
||||
1. `packages/noodl-core-ui/src/preview/launcher/Launcher/views/Projects.tsx`
|
||||
- Add "Clone Repository" button to toolbar
|
||||
|
||||
2. `packages/noodl-core-ui/src/preview/launcher/Launcher/Launcher.tsx`
|
||||
- Add clone modal state and rendering
|
||||
|
||||
3. `packages/noodl-utils/LocalProjectsModel.ts`
|
||||
- Add cloned project to recent projects list
|
||||
|
||||
4. `packages/noodl-editor/src/editor/src/views/projectsview.ts`
|
||||
- Ensure cloned project can be opened (may already work)
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### Phase 1: Clone Service
|
||||
1. Create CloneService wrapper around noodl-git
|
||||
2. Add progress normalization
|
||||
3. Add cancellation support
|
||||
4. Test with public repository
|
||||
|
||||
### Phase 2: URL-Based Cloning
|
||||
1. Create basic CloneModal with URL input
|
||||
2. Create FolderSelector component
|
||||
3. Create CloneProgress component
|
||||
4. Wire up clone flow
|
||||
|
||||
### Phase 3: Repository Browser
|
||||
1. Create GitHubApiClient (or extend from GIT-001)
|
||||
2. Create RepoBrowser component
|
||||
3. Create RepoCard component
|
||||
4. Add search/filter functionality
|
||||
|
||||
### Phase 4: Integration
|
||||
1. Add clone button to dashboard
|
||||
2. Open cloned project automatically
|
||||
3. Add to recent projects
|
||||
4. Handle errors gracefully
|
||||
|
||||
### Phase 5: Polish
|
||||
1. Remember last folder
|
||||
2. Add branch selection
|
||||
3. Add shallow clone option
|
||||
4. Improve error messages
|
||||
|
||||
## Error Handling
|
||||
|
||||
| Error | User Message | Recovery |
|
||||
|-------|--------------|----------|
|
||||
| Network error | "Unable to connect. Check your internet connection." | Retry button |
|
||||
| Auth required | "This repository requires authentication. Connect your GitHub account." | Link to OAuth |
|
||||
| Repo not found | "Repository not found. Check the URL and try again." | Edit URL |
|
||||
| Permission denied | "You don't have access to this repository." | Suggest checking permissions |
|
||||
| Folder not empty | "The selected folder is not empty. Choose an empty folder." | Folder picker |
|
||||
| Disk full | "Not enough disk space to clone this repository." | Show required space |
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
- [ ] Clone public repository via URL
|
||||
- [ ] Clone private repository with OAuth token
|
||||
- [ ] Clone private repository with PAT
|
||||
- [ ] Repository browser shows user repos
|
||||
- [ ] Repository browser shows org repos
|
||||
- [ ] Search/filter works
|
||||
- [ ] Folder picker opens and works
|
||||
- [ ] Progress updates smoothly
|
||||
- [ ] Cancel stops clone in progress
|
||||
- [ ] Cloned project opens automatically
|
||||
- [ ] Project appears in recent projects
|
||||
- [ ] Error messages are helpful
|
||||
- [ ] Works with various repo sizes
|
||||
- [ ] Handles repos with submodules
|
||||
|
||||
## Dependencies
|
||||
|
||||
- GIT-001 (GitHub OAuth) - for repository browser with private repos
|
||||
- DASH-001 (Tabbed Navigation) - for dashboard integration
|
||||
|
||||
## Blocked By
|
||||
|
||||
- GIT-001 (partially - URL cloning works without OAuth)
|
||||
|
||||
## Blocks
|
||||
|
||||
- COMP-004 (Organization Components) - uses similar repo browsing
|
||||
|
||||
## Estimated Effort
|
||||
|
||||
- Clone service: 2-3 hours
|
||||
- URL-based clone modal: 3-4 hours
|
||||
- Repository browser: 4-5 hours
|
||||
- Integration & auto-open: 2-3 hours
|
||||
- Polish & error handling: 2-3 hours
|
||||
- **Total: 13-18 hours**
|
||||
|
||||
## Success Criteria
|
||||
|
||||
1. Users can clone by entering a URL
|
||||
2. Users can browse and select their repositories
|
||||
3. Clone progress is visible and accurate
|
||||
4. Cloned projects open automatically
|
||||
5. Private repos work with authentication
|
||||
6. Errors are handled gracefully
|
||||
7. Process can be cancelled
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- Clone from other providers (GitLab, Bitbucket)
|
||||
- Clone specific branch/tag
|
||||
- Clone with submodules options
|
||||
- Clone into new project template
|
||||
- Clone history (recently cloned repos)
|
||||
- Detect Noodl projects vs generic repos
|
||||
@@ -0,0 +1,388 @@
|
||||
# GIT-004: Auto-Initialization & Commit Encouragement
|
||||
|
||||
## Overview
|
||||
|
||||
Make version control a default part of the Noodl workflow by automatically initializing git for new projects and gently encouraging regular commits. This helps users avoid losing work and prepares them for collaboration.
|
||||
|
||||
## Context
|
||||
|
||||
Currently:
|
||||
- New projects are not git-initialized by default
|
||||
- Users must manually open Version Control panel and initialize
|
||||
- There's no prompting to commit changes
|
||||
- Closing a project with uncommitted changes has no warning
|
||||
|
||||
Many Noodl users are designers or low-code developers who may not be familiar with git. By making version control automatic and unobtrusive, we help them develop good habits without requiring git expertise.
|
||||
|
||||
### Existing Infrastructure
|
||||
|
||||
From `LocalProjectsModel.ts`:
|
||||
```typescript
|
||||
async isGitProject(project: ProjectModel): Promise<boolean> {
|
||||
const gitPath = await getTopLevelWorkingDirectory(project._retainedProjectDirectory);
|
||||
return gitPath !== null;
|
||||
}
|
||||
```
|
||||
|
||||
From `git.ts`:
|
||||
```typescript
|
||||
async initNewRepo(baseDir: string, options?: { bare: boolean }): Promise<void> {
|
||||
if (this.baseDir) return;
|
||||
this.baseDir = await init(baseDir, options);
|
||||
await this._setupRepository();
|
||||
}
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
### Functional Requirements
|
||||
|
||||
1. **Auto-Initialization**
|
||||
- New projects are git-initialized by default
|
||||
- Initial commit with project creation
|
||||
- Option to disable in settings
|
||||
- Existing non-git projects can be initialized easily
|
||||
|
||||
2. **Commit Encouragement**
|
||||
- Periodic reminder when changes are uncommitted
|
||||
- Reminder appears as subtle notification, not modal
|
||||
- "Commit now" quick action
|
||||
- "Remind me later" option
|
||||
- Configurable reminder interval
|
||||
|
||||
3. **Quick Commit**
|
||||
- One-click commit from notification
|
||||
- Simple commit message input
|
||||
- Default message suggestion
|
||||
- Option to open full Version Control panel
|
||||
|
||||
4. **Close Warning**
|
||||
- Warning when closing project with uncommitted changes
|
||||
- Show number of uncommitted files
|
||||
- Options: "Commit & Close", "Close Anyway", "Cancel"
|
||||
- Can be disabled in settings
|
||||
|
||||
5. **Settings**
|
||||
- Enable/disable auto-initialization
|
||||
- Enable/disable commit reminders
|
||||
- Reminder interval (15min, 30min, 1hr, 2hr)
|
||||
- Enable/disable close warning
|
||||
|
||||
### Non-Functional Requirements
|
||||
|
||||
- Reminders are non-intrusive
|
||||
- Quick commit is fast (<2 seconds)
|
||||
- Auto-init doesn't slow project creation
|
||||
- Works offline
|
||||
|
||||
## Technical Approach
|
||||
|
||||
### 1. Auto-Initialization in Project Creation
|
||||
|
||||
```typescript
|
||||
// packages/noodl-editor/src/editor/src/models/projectmodel.ts
|
||||
|
||||
async createNewProject(name: string, template?: string): Promise<ProjectModel> {
|
||||
const project = await this._createProject(name, template);
|
||||
|
||||
// Auto-initialize git if enabled
|
||||
if (EditorSettings.instance.get('git.autoInitialize') !== false) {
|
||||
try {
|
||||
const git = new Git(mergeProject);
|
||||
await git.initNewRepo(project._retainedProjectDirectory);
|
||||
await git.commit('Initial commit');
|
||||
} catch (error) {
|
||||
console.warn('Failed to auto-initialize git:', error);
|
||||
// Don't fail project creation if git init fails
|
||||
}
|
||||
}
|
||||
|
||||
return project;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Commit Reminder Service
|
||||
|
||||
```typescript
|
||||
// packages/noodl-editor/src/editor/src/services/CommitReminderService.ts
|
||||
|
||||
class CommitReminderService {
|
||||
private static instance: CommitReminderService;
|
||||
private reminderTimer: NodeJS.Timer | null = null;
|
||||
private lastRemindedAt: number = 0;
|
||||
|
||||
// Start monitoring for uncommitted changes
|
||||
start(): void;
|
||||
stop(): void;
|
||||
|
||||
// Check if reminder should show
|
||||
shouldShowReminder(): Promise<boolean>;
|
||||
|
||||
// Show/dismiss reminder
|
||||
showReminder(): void;
|
||||
dismissReminder(snoozeMinutes?: number): void;
|
||||
|
||||
// Events
|
||||
onReminderTriggered(callback: () => void): () => void;
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Quick Commit Component
|
||||
|
||||
```typescript
|
||||
// packages/noodl-core-ui/src/components/git/QuickCommitPopup/QuickCommitPopup.tsx
|
||||
|
||||
interface QuickCommitPopupProps {
|
||||
uncommittedCount: number;
|
||||
suggestedMessage: string;
|
||||
onCommit: (message: string) => Promise<void>;
|
||||
onDismiss: () => void;
|
||||
onOpenFullPanel: () => void;
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Close Warning Dialog
|
||||
|
||||
```typescript
|
||||
// packages/noodl-core-ui/src/components/git/UnsavedChangesDialog/UnsavedChangesDialog.tsx
|
||||
|
||||
interface UnsavedChangesDialogProps {
|
||||
uncommittedCount: number;
|
||||
onCommitAndClose: () => Promise<void>;
|
||||
onCloseAnyway: () => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Default Commit Messages
|
||||
|
||||
```typescript
|
||||
// Smart default commit message generation
|
||||
function generateDefaultCommitMessage(changes: GitStatus[]): string {
|
||||
const added = changes.filter(c => c.status === 'added');
|
||||
const modified = changes.filter(c => c.status === 'modified');
|
||||
const deleted = changes.filter(c => c.status === 'deleted');
|
||||
|
||||
const parts: string[] = [];
|
||||
|
||||
if (added.length > 0) {
|
||||
if (added.length === 1) {
|
||||
parts.push(`Add ${getComponentName(added[0].path)}`);
|
||||
} else {
|
||||
parts.push(`Add ${added.length} files`);
|
||||
}
|
||||
}
|
||||
|
||||
if (modified.length > 0) {
|
||||
if (modified.length === 1) {
|
||||
parts.push(`Update ${getComponentName(modified[0].path)}`);
|
||||
} else {
|
||||
parts.push(`Update ${modified.length} files`);
|
||||
}
|
||||
}
|
||||
|
||||
if (deleted.length > 0) {
|
||||
parts.push(`Remove ${deleted.length} files`);
|
||||
}
|
||||
|
||||
return parts.join(', ') || 'Update project';
|
||||
}
|
||||
```
|
||||
|
||||
## UI Mockups
|
||||
|
||||
### Commit Reminder Notification
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 💾 You have 5 uncommitted changes │
|
||||
│ │
|
||||
│ It's been 30 minutes since your last commit. │
|
||||
│ │
|
||||
│ [Commit Now] [Remind Me Later ▾] [Dismiss] │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Quick Commit Popup
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Quick Commit [×] │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ 5 files changed │
|
||||
│ │
|
||||
│ Message: │
|
||||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||
│ │ Update LoginPage and add UserProfile component │ │
|
||||
│ └─────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ [Open Version Control] [Cancel] [Commit] │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Close Warning Dialog
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ ⚠️ Uncommitted Changes [×] │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ You have 5 uncommitted changes in this project. │
|
||||
│ │
|
||||
│ These changes will be preserved locally but not versioned. │
|
||||
│ To keep a history of your work, commit before closing. │
|
||||
│ │
|
||||
│ ☐ Don't show this again │
|
||||
│ │
|
||||
│ [Cancel] [Close Anyway] [Commit & Close] │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Files to Create
|
||||
|
||||
1. `packages/noodl-editor/src/editor/src/services/CommitReminderService.ts`
|
||||
2. `packages/noodl-core-ui/src/components/git/QuickCommitPopup/QuickCommitPopup.tsx`
|
||||
3. `packages/noodl-core-ui/src/components/git/QuickCommitPopup/QuickCommitPopup.module.scss`
|
||||
4. `packages/noodl-core-ui/src/components/git/UnsavedChangesDialog/UnsavedChangesDialog.tsx`
|
||||
5. `packages/noodl-core-ui/src/components/git/CommitReminderToast/CommitReminderToast.tsx`
|
||||
6. `packages/noodl-editor/src/editor/src/utils/git/defaultCommitMessage.ts`
|
||||
|
||||
## Files to Modify
|
||||
|
||||
1. `packages/noodl-editor/src/editor/src/models/projectmodel.ts`
|
||||
- Add auto-initialization in project creation
|
||||
|
||||
2. `packages/noodl-editor/src/editor/src/pages/EditorPage/EditorPage.tsx`
|
||||
- Add close warning handler
|
||||
- Integrate CommitReminderService
|
||||
|
||||
3. `packages/noodl-utils/editorsettings.ts`
|
||||
- Add git-related settings
|
||||
|
||||
4. `packages/noodl-editor/src/editor/src/views/panels/EditorSettingsPanel/`
|
||||
- Add git settings section
|
||||
|
||||
5. `packages/noodl-editor/src/main/main.js`
|
||||
- Handle close event for warning
|
||||
|
||||
## Settings Schema
|
||||
|
||||
```typescript
|
||||
interface GitSettings {
|
||||
// Auto-initialization
|
||||
'git.autoInitialize': boolean; // default: true
|
||||
|
||||
// Commit reminders
|
||||
'git.commitReminders.enabled': boolean; // default: true
|
||||
'git.commitReminders.intervalMinutes': number; // default: 30
|
||||
|
||||
// Close warning
|
||||
'git.closeWarning.enabled': boolean; // default: true
|
||||
|
||||
// Quick commit
|
||||
'git.quickCommit.suggestMessage': boolean; // default: true
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### Phase 1: Auto-Initialization
|
||||
1. Add git.autoInitialize setting
|
||||
2. Modify project creation to init git
|
||||
3. Add initial commit
|
||||
4. Test with new projects
|
||||
|
||||
### Phase 2: Settings UI
|
||||
1. Add Git section to Editor Settings panel
|
||||
2. Implement all settings toggles
|
||||
3. Store settings in EditorSettings
|
||||
|
||||
### Phase 3: Commit Reminder Service
|
||||
1. Create CommitReminderService
|
||||
2. Add timer-based reminder check
|
||||
3. Create CommitReminderToast component
|
||||
4. Integrate with editor lifecycle
|
||||
|
||||
### Phase 4: Quick Commit
|
||||
1. Create QuickCommitPopup component
|
||||
2. Implement default message generation
|
||||
3. Wire up commit action
|
||||
4. Add "Open full panel" option
|
||||
|
||||
### Phase 5: Close Warning
|
||||
1. Create UnsavedChangesDialog
|
||||
2. Hook into project close event
|
||||
3. Implement "Commit & Close" flow
|
||||
4. Add "Don't show again" option
|
||||
|
||||
### Phase 6: Polish
|
||||
1. Snooze functionality
|
||||
2. Notification stacking
|
||||
3. Animation/transitions
|
||||
4. Edge case handling
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
- [ ] New project is git-initialized by default
|
||||
- [ ] Initial commit is created
|
||||
- [ ] Auto-init can be disabled
|
||||
- [ ] Commit reminder appears after interval
|
||||
- [ ] Reminder shows correct uncommitted count
|
||||
- [ ] "Commit Now" opens quick commit popup
|
||||
- [ ] "Remind Me Later" snoozes correctly
|
||||
- [ ] Quick commit works with default message
|
||||
- [ ] Quick commit works with custom message
|
||||
- [ ] Close warning appears with uncommitted changes
|
||||
- [ ] "Commit & Close" works
|
||||
- [ ] "Close Anyway" works
|
||||
- [ ] "Don't show again" persists
|
||||
- [ ] Settings toggle all features correctly
|
||||
- [ ] Works when offline
|
||||
|
||||
## Edge Cases
|
||||
|
||||
1. **Project already has git**: Don't re-initialize, just work with existing
|
||||
2. **Template with git**: Use template's git if present, else init fresh
|
||||
3. **Init fails**: Log warning, don't block project creation
|
||||
4. **Commit fails**: Show error, offer to open Version Control panel
|
||||
5. **Large commit**: Show progress, don't block UI
|
||||
6. **No changes on reminder check**: Don't show reminder
|
||||
|
||||
## Dependencies
|
||||
|
||||
- GIT-002 (Git Status Dashboard) - for status detection infrastructure
|
||||
|
||||
## Blocked By
|
||||
|
||||
- GIT-002 (shares status checking code)
|
||||
|
||||
## Blocks
|
||||
|
||||
- None
|
||||
|
||||
## Estimated Effort
|
||||
|
||||
- Auto-initialization: 2-3 hours
|
||||
- Settings UI: 2-3 hours
|
||||
- Commit reminder service: 3-4 hours
|
||||
- Quick commit popup: 2-3 hours
|
||||
- Close warning: 2-3 hours
|
||||
- Polish: 2-3 hours
|
||||
- **Total: 13-19 hours**
|
||||
|
||||
## Success Criteria
|
||||
|
||||
1. New projects have git by default
|
||||
2. Users are gently reminded to commit
|
||||
3. Committing is easy and fast
|
||||
4. Users are warned before losing work
|
||||
5. All features can be disabled
|
||||
6. Non-intrusive to workflow
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- Commit streak/gamification
|
||||
- Auto-commit on significant changes
|
||||
- Commit templates
|
||||
- Branch suggestions
|
||||
- Integration with cloud backup
|
||||
@@ -0,0 +1,388 @@
|
||||
# GIT-005: Enhanced Push/Pull UI
|
||||
|
||||
## Overview
|
||||
|
||||
Improve the push/pull experience with better visibility, branch management, conflict previews, and dashboard-level sync controls. Make syncing with remotes more intuitive and less error-prone.
|
||||
|
||||
## Context
|
||||
|
||||
The current Version Control panel has push/pull functionality via `GitStatusButton`, but:
|
||||
- Only visible when the panel is open
|
||||
- Branch switching is buried in menus
|
||||
- No preview of what will be pulled
|
||||
- Conflict resolution is complex
|
||||
|
||||
This task brings sync operations to the forefront and adds safeguards.
|
||||
|
||||
### Existing Infrastructure
|
||||
|
||||
From `GitStatusButton.tsx`:
|
||||
```typescript
|
||||
// Status kinds: 'default', 'fetch', 'error-fetch', 'pull', 'push', 'push-repository', 'set-authorization'
|
||||
|
||||
case 'push': {
|
||||
label = localCommitCount === 1 ? `Push 1 local commit` : `Push ${localCommitCount} local commits`;
|
||||
}
|
||||
|
||||
case 'pull': {
|
||||
label = remoteCommitCount === 1 ? `Pull 1 remote commit` : `Pull ${remoteCommitCount} remote commits`;
|
||||
}
|
||||
```
|
||||
|
||||
From `fetch.context.ts`:
|
||||
```typescript
|
||||
localCommitCount // Commits ahead of remote
|
||||
remoteCommitCount // Commits behind remote
|
||||
currentBranch // Current branch info
|
||||
branches // All branches
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
### Functional Requirements
|
||||
|
||||
1. **Dashboard Sync Button**
|
||||
- Visible sync button in project row (from GIT-002)
|
||||
- One-click fetch & show status
|
||||
- Quick push/pull from dashboard
|
||||
|
||||
2. **Branch Selector**
|
||||
- Dropdown showing current branch
|
||||
- Quick switch between branches
|
||||
- Create new branch option
|
||||
- Branch search for projects with many branches
|
||||
- Remote branch indicators
|
||||
|
||||
3. **Pull Preview**
|
||||
- Show what commits will be pulled
|
||||
- List affected files
|
||||
- Warning for potential conflicts
|
||||
- "Preview" mode before actual pull
|
||||
|
||||
4. **Conflict Prevention**
|
||||
- Check for conflicts before pull
|
||||
- Suggest stashing changes first
|
||||
- Clear conflict resolution workflow
|
||||
- "Abort" option during conflicts
|
||||
|
||||
5. **Push Confirmation**
|
||||
- Show commits being pushed
|
||||
- Branch protection warning (if pushing to main)
|
||||
- Force push warning (if needed)
|
||||
|
||||
6. **Sync Status Header**
|
||||
- Always-visible status in editor header
|
||||
- Current branch display
|
||||
- Quick sync actions
|
||||
- Connection indicator
|
||||
|
||||
### Non-Functional Requirements
|
||||
|
||||
- Sync operations don't block UI
|
||||
- Progress visible for long operations
|
||||
- Works offline (queues operations)
|
||||
- Clear error messages
|
||||
|
||||
## Technical Approach
|
||||
|
||||
### 1. Sync Status Header Component
|
||||
|
||||
```typescript
|
||||
// packages/noodl-core-ui/src/components/git/SyncStatusHeader/SyncStatusHeader.tsx
|
||||
|
||||
interface SyncStatusHeaderProps {
|
||||
currentBranch: string;
|
||||
aheadCount: number;
|
||||
behindCount: number;
|
||||
hasUncommitted: boolean;
|
||||
isOnline: boolean;
|
||||
lastFetchTime: number;
|
||||
onPush: () => void;
|
||||
onPull: () => void;
|
||||
onFetch: () => void;
|
||||
onBranchChange: (branch: string) => void;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Branch Selector Component
|
||||
|
||||
```typescript
|
||||
// packages/noodl-core-ui/src/components/git/BranchSelector/BranchSelector.tsx
|
||||
|
||||
interface BranchSelectorProps {
|
||||
currentBranch: Branch;
|
||||
branches: Branch[];
|
||||
onSelect: (branch: Branch) => void;
|
||||
onCreate: (name: string) => void;
|
||||
}
|
||||
|
||||
interface Branch {
|
||||
name: string;
|
||||
nameWithoutRemote: string;
|
||||
isLocal: boolean;
|
||||
isRemote: boolean;
|
||||
isCurrent: boolean;
|
||||
lastCommit?: {
|
||||
sha: string;
|
||||
message: string;
|
||||
date: string;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Pull Preview Modal
|
||||
|
||||
```typescript
|
||||
// packages/noodl-core-ui/src/components/git/PullPreviewModal/PullPreviewModal.tsx
|
||||
|
||||
interface PullPreviewModalProps {
|
||||
commits: Commit[];
|
||||
affectedFiles: FileChange[];
|
||||
hasConflicts: boolean;
|
||||
conflictFiles?: string[];
|
||||
onPull: () => Promise<void>;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
interface Commit {
|
||||
sha: string;
|
||||
message: string;
|
||||
author: string;
|
||||
date: string;
|
||||
}
|
||||
|
||||
interface FileChange {
|
||||
path: string;
|
||||
status: 'added' | 'modified' | 'deleted';
|
||||
hasConflict: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Conflict Resolution Flow
|
||||
|
||||
```typescript
|
||||
// packages/noodl-editor/src/editor/src/services/ConflictResolutionService.ts
|
||||
|
||||
class ConflictResolutionService {
|
||||
// Check for potential conflicts before pull
|
||||
async previewConflicts(): Promise<ConflictPreview>;
|
||||
|
||||
// Handle stashing
|
||||
async stashAndPull(): Promise<void>;
|
||||
|
||||
// Resolution strategies
|
||||
async resolveWithOurs(file: string): Promise<void>;
|
||||
async resolveWithTheirs(file: string): Promise<void>;
|
||||
async openMergeTool(file: string): Promise<void>;
|
||||
|
||||
// Abort
|
||||
async abortMerge(): Promise<void>;
|
||||
}
|
||||
```
|
||||
|
||||
## UI Mockups
|
||||
|
||||
### Sync Status Header (Editor)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ [main ▾] ↑3 ↓2 ●5 uncommitted 🟢 Connected [Fetch] [Pull] [Push] │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Branch Selector Dropdown
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ 🔍 Search branches... │
|
||||
├─────────────────────────────────────┤
|
||||
│ LOCAL │
|
||||
│ ✓ main │
|
||||
│ feature/new-login │
|
||||
│ bugfix/header-styling │
|
||||
├─────────────────────────────────────┤
|
||||
│ REMOTE │
|
||||
│ origin/develop │
|
||||
│ origin/release-1.0 │
|
||||
├─────────────────────────────────────┤
|
||||
│ + Create new branch... │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Pull Preview Modal
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ Pull Preview [×] │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ Pulling 3 commits from origin/main │
|
||||
│ │
|
||||
│ COMMITS │
|
||||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ a1b2c3d Fix login validation John Doe 2 hours ago │ │
|
||||
│ │ d4e5f6g Add password reset flow Jane Smith 5 hours ago │ │
|
||||
│ │ h7i8j9k Update dependencies John Doe 1 day ago │ │
|
||||
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ FILES CHANGED (12) │
|
||||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ M components/LoginPage.ndjson │ │
|
||||
│ │ M components/Header.ndjson │ │
|
||||
│ │ A components/PasswordReset.ndjson │ │
|
||||
│ │ D components/OldLogin.ndjson │ │
|
||||
│ │ ⚠️ M project.json (potential conflict) │ │
|
||||
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ⚠️ You have uncommitted changes. They will be stashed before pull. │
|
||||
│ │
|
||||
│ [Cancel] [Pull Now] │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Conflict Warning
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ ⚠️ Potential Conflicts Detected [×] │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ The following files have been modified both locally and remotely: │
|
||||
│ │
|
||||
│ • project.json │
|
||||
│ • components/LoginPage.ndjson │
|
||||
│ │
|
||||
│ Noodl will attempt to merge these changes automatically, but you │
|
||||
│ may need to resolve conflicts manually. │
|
||||
│ │
|
||||
│ Recommended: Commit your local changes first for a cleaner merge. │
|
||||
│ │
|
||||
│ [Cancel] [Commit First] [Pull Anyway] │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Files to Create
|
||||
|
||||
1. `packages/noodl-core-ui/src/components/git/SyncStatusHeader/SyncStatusHeader.tsx`
|
||||
2. `packages/noodl-core-ui/src/components/git/SyncStatusHeader/SyncStatusHeader.module.scss`
|
||||
3. `packages/noodl-core-ui/src/components/git/BranchSelector/BranchSelector.tsx`
|
||||
4. `packages/noodl-core-ui/src/components/git/BranchSelector/BranchSelector.module.scss`
|
||||
5. `packages/noodl-core-ui/src/components/git/PullPreviewModal/PullPreviewModal.tsx`
|
||||
6. `packages/noodl-core-ui/src/components/git/PushConfirmModal/PushConfirmModal.tsx`
|
||||
7. `packages/noodl-core-ui/src/components/git/ConflictWarningModal/ConflictWarningModal.tsx`
|
||||
8. `packages/noodl-editor/src/editor/src/services/ConflictResolutionService.ts`
|
||||
|
||||
## Files to Modify
|
||||
|
||||
1. `packages/noodl-editor/src/editor/src/pages/EditorPage/EditorPage.tsx`
|
||||
- Add SyncStatusHeader to editor layout
|
||||
|
||||
2. `packages/noodl-editor/src/editor/src/views/panels/VersionControlPanel/VersionControlPanel.tsx`
|
||||
- Integrate new BranchSelector
|
||||
- Add pull preview before pulling
|
||||
|
||||
3. `packages/noodl-editor/src/editor/src/views/panels/VersionControlPanel/components/GitStatusButton.tsx`
|
||||
- Update to use new pull/push flows
|
||||
|
||||
4. `packages/noodl-editor/src/editor/src/views/panels/VersionControlPanel/context/fetch.context.ts`
|
||||
- Add preview fetch logic
|
||||
- Add conflict detection
|
||||
|
||||
5. `packages/noodl-core-ui/src/preview/launcher/Launcher/components/ProjectList/ProjectListRow.tsx`
|
||||
- Add quick sync button (if not in GIT-002)
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### Phase 1: Branch Selector
|
||||
1. Create BranchSelector component
|
||||
2. Implement search/filter
|
||||
3. Add create branch flow
|
||||
4. Integrate into Version Control panel
|
||||
|
||||
### Phase 2: Sync Status Header
|
||||
1. Create SyncStatusHeader component
|
||||
2. Add to editor layout
|
||||
3. Wire up actions
|
||||
4. Add connection indicator
|
||||
|
||||
### Phase 3: Pull Preview
|
||||
1. Create PullPreviewModal
|
||||
2. Implement commit/file listing
|
||||
3. Add conflict detection
|
||||
4. Wire up pull action
|
||||
|
||||
### Phase 4: Conflict Handling
|
||||
1. Create ConflictWarningModal
|
||||
2. Create ConflictResolutionService
|
||||
3. Implement stash-before-pull
|
||||
4. Add abort functionality
|
||||
|
||||
### Phase 5: Push Enhancements
|
||||
1. Create PushConfirmModal
|
||||
2. Add branch protection warning
|
||||
3. Show commit list
|
||||
4. Handle force push
|
||||
|
||||
### Phase 6: Dashboard Integration
|
||||
1. Add sync button to project rows
|
||||
2. Quick push/pull from dashboard
|
||||
3. Update status after sync
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
- [ ] Branch selector shows all branches
|
||||
- [ ] Branch search filters correctly
|
||||
- [ ] Switching branches works
|
||||
- [ ] Creating new branch works
|
||||
- [ ] Sync status header shows correct counts
|
||||
- [ ] Fetch updates status
|
||||
- [ ] Pull preview shows correct commits
|
||||
- [ ] Pull preview shows affected files
|
||||
- [ ] Conflict warning appears when appropriate
|
||||
- [ ] Stash-before-pull works
|
||||
- [ ] Pull completes successfully
|
||||
- [ ] Push confirmation shows commits
|
||||
- [ ] Push completes successfully
|
||||
- [ ] Dashboard sync button works
|
||||
- [ ] Offline state handled gracefully
|
||||
|
||||
## Dependencies
|
||||
|
||||
- GIT-002 (Git Status Dashboard) - for dashboard integration
|
||||
- GIT-001 (GitHub OAuth) - for authenticated operations
|
||||
|
||||
## Blocked By
|
||||
|
||||
- GIT-002
|
||||
|
||||
## Blocks
|
||||
|
||||
- None
|
||||
|
||||
## Estimated Effort
|
||||
|
||||
- Branch selector: 3-4 hours
|
||||
- Sync status header: 2-3 hours
|
||||
- Pull preview: 4-5 hours
|
||||
- Conflict handling: 4-5 hours
|
||||
- Push enhancements: 2-3 hours
|
||||
- Dashboard integration: 2-3 hours
|
||||
- **Total: 17-23 hours**
|
||||
|
||||
## Success Criteria
|
||||
|
||||
1. Branch switching is easy and visible
|
||||
2. Users can preview what will be pulled
|
||||
3. Conflict potential is detected before pull
|
||||
4. Stashing is automatic when needed
|
||||
5. Push shows what's being pushed
|
||||
6. Quick sync available from dashboard
|
||||
7. Status always visible in editor
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- Pull request creation
|
||||
- Branch comparison
|
||||
- Revert/cherry-pick commits
|
||||
- Squash commits before push
|
||||
- Auto-sync on save (optional)
|
||||
- Branch naming conventions/templates
|
||||
@@ -0,0 +1,248 @@
|
||||
# GIT Series: Git & GitHub Integration
|
||||
|
||||
## Overview
|
||||
|
||||
The GIT series transforms Noodl's version control experience from a manual, expert-only feature into a seamless, integrated part of the development workflow. By adding GitHub OAuth, surfacing git status in the dashboard, and encouraging good version control habits, we make collaboration accessible to all Noodl users.
|
||||
|
||||
## Target Environment
|
||||
|
||||
- **Editor**: React 19 version only
|
||||
- **Runtime**: Not affected (git is editor-only)
|
||||
- **Backwards Compatibility**: Existing git projects continue to work
|
||||
|
||||
## Task Dependency Graph
|
||||
|
||||
```
|
||||
GIT-001 (GitHub OAuth)
|
||||
│
|
||||
├──────────────────────────┐
|
||||
│ │
|
||||
▼ ▼
|
||||
GIT-002 (Dashboard Status) GIT-003 (Repository Cloning)
|
||||
│
|
||||
├──────────────────────────┐
|
||||
│ │
|
||||
▼ ▼
|
||||
GIT-004 (Auto-Init) GIT-005 (Enhanced Push/Pull)
|
||||
```
|
||||
|
||||
## Task Summary
|
||||
|
||||
| Task ID | Name | Est. Hours | Priority |
|
||||
|---------|------|------------|----------|
|
||||
| GIT-001 | GitHub OAuth Integration | 14-20 | Critical |
|
||||
| GIT-002 | Git Status Dashboard Visibility | 11-16 | High |
|
||||
| GIT-003 | Repository Cloning | 13-18 | High |
|
||||
| GIT-004 | Auto-Initialization & Commit Encouragement | 13-19 | Medium |
|
||||
| GIT-005 | Enhanced Push/Pull UI | 17-23 | Medium |
|
||||
|
||||
**Total Estimated: 68-96 hours**
|
||||
|
||||
## Implementation Order
|
||||
|
||||
### Week 1-2: Authentication & Status
|
||||
1. **GIT-001** - GitHub OAuth (foundation for GitHub API access)
|
||||
2. **GIT-002** - Dashboard status (leverages DASH-002 project list)
|
||||
|
||||
### Week 3: Cloning & Basic Flow
|
||||
3. **GIT-003** - Repository cloning (depends on OAuth for private repos)
|
||||
|
||||
### Week 4: Polish & Encouragement
|
||||
4. **GIT-004** - Auto-initialization (depends on status detection)
|
||||
5. **GIT-005** - Enhanced push/pull (depends on status infrastructure)
|
||||
|
||||
## Existing Infrastructure
|
||||
|
||||
The codebase already has solid git foundations to build on:
|
||||
|
||||
### noodl-git Package
|
||||
```
|
||||
packages/noodl-git/src/
|
||||
├── git.ts # Main Git class
|
||||
├── core/
|
||||
│ ├── clone.ts # Clone operations
|
||||
│ ├── push.ts # Push operations
|
||||
│ ├── pull.ts # Pull operations
|
||||
│ └── ...
|
||||
├── actions/ # Higher-level actions
|
||||
└── constants.ts
|
||||
```
|
||||
|
||||
Key existing methods:
|
||||
- `git.initNewRepo()` - Initialize new repository
|
||||
- `git.clone()` - Clone with progress
|
||||
- `git.push()` - Push with progress
|
||||
- `git.pull()` - Pull with rebase
|
||||
- `git.status()` - Working directory status
|
||||
- `git.getBranches()` - List branches
|
||||
- `git.getCommitsCurrentBranch()` - Commit history
|
||||
|
||||
### Version Control Panel
|
||||
```
|
||||
packages/noodl-editor/src/editor/src/views/panels/VersionControlPanel/
|
||||
├── VersionControlPanel.tsx
|
||||
├── components/
|
||||
│ ├── GitStatusButton.tsx # Push/pull status
|
||||
│ ├── GitProviderPopout/ # Credentials management
|
||||
│ ├── LocalChanges.tsx # Uncommitted files
|
||||
│ ├── History.tsx # Commit history
|
||||
│ └── BranchMerge.tsx # Branch operations
|
||||
└── context/
|
||||
└── fetch.context.ts # Git state management
|
||||
```
|
||||
|
||||
### Credentials Storage
|
||||
- `GitStore` - Stores credentials per-project encrypted
|
||||
- `trampoline-askpass-handler` - Handles git credential prompts
|
||||
- Currently uses PAT (Personal Access Token) for GitHub
|
||||
|
||||
## Key Technical Decisions
|
||||
|
||||
### OAuth vs PAT
|
||||
|
||||
**Current**: Personal Access Token per project
|
||||
- User creates PAT on GitHub
|
||||
- Copies to Noodl per project
|
||||
- Stored encrypted in GitStore
|
||||
|
||||
**New (GIT-001)**: OAuth + PAT fallback
|
||||
- One-click GitHub OAuth
|
||||
- Token stored globally
|
||||
- PAT remains for non-GitHub remotes
|
||||
|
||||
### Status Checking Strategy
|
||||
|
||||
**Approach**: Batch + Cache
|
||||
- Check multiple projects in parallel
|
||||
- Cache results with TTL
|
||||
- Background refresh
|
||||
|
||||
**Why**: Git status requires opening each repo, which is slow. Caching makes dashboard responsive while keeping data fresh.
|
||||
|
||||
### Auto-Initialization
|
||||
|
||||
**Approach**: Opt-out
|
||||
- Git initialized by default
|
||||
- Initial commit created automatically
|
||||
- Can disable in settings
|
||||
|
||||
**Why**: Most users benefit from version control. Making it default reduces "I lost my work" issues.
|
||||
|
||||
## Services to Create
|
||||
|
||||
| Service | Location | Purpose |
|
||||
|---------|----------|---------|
|
||||
| GitHubOAuthService | noodl-editor/services | OAuth flow, token management |
|
||||
| GitHubApiClient | noodl-editor/services | GitHub REST API calls |
|
||||
| ProjectGitStatusService | noodl-editor/services | Batch status checking, caching |
|
||||
| CloneService | noodl-editor/services | Clone wrapper with progress |
|
||||
| CommitReminderService | noodl-editor/services | Periodic commit reminders |
|
||||
| ConflictResolutionService | noodl-editor/services | Conflict detection, resolution |
|
||||
|
||||
## Components to Create
|
||||
|
||||
| Component | Package | Purpose |
|
||||
|-----------|---------|---------|
|
||||
| GitHubConnectButton | noodl-core-ui | OAuth trigger button |
|
||||
| GitHubAccountCard | noodl-core-ui | Connected account display |
|
||||
| GitStatusBadge | noodl-core-ui | Status indicator in list |
|
||||
| CloneModal | noodl-core-ui | Clone flow modal |
|
||||
| RepoBrowser | noodl-core-ui | Repository list/search |
|
||||
| QuickCommitPopup | noodl-core-ui | Fast commit dialog |
|
||||
| SyncStatusHeader | noodl-core-ui | Editor header sync status |
|
||||
| BranchSelector | noodl-core-ui | Branch dropdown |
|
||||
| PullPreviewModal | noodl-core-ui | Preview before pull |
|
||||
|
||||
## Dependencies
|
||||
|
||||
### On DASH Series
|
||||
- GIT-002 → DASH-002 (project list for status display)
|
||||
- GIT-001 → DASH-001 (launcher context for account display)
|
||||
|
||||
### External Packages
|
||||
May need:
|
||||
```json
|
||||
{
|
||||
"@octokit/rest": "^20.0.0" // GitHub API client (optional)
|
||||
}
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **OAuth Tokens**: Store with electron's safeStorage API
|
||||
2. **PKCE Flow**: Use PKCE for OAuth (no client secret in app)
|
||||
3. **Token Scope**: Request minimum necessary (repo, read:org, read:user)
|
||||
4. **Credential Cache**: Clear on logout/disconnect
|
||||
5. **PAT Fallback**: Encrypted per-project storage continues
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Unit Tests
|
||||
- OAuth token exchange
|
||||
- Status calculation logic
|
||||
- Conflict detection
|
||||
- Default commit message generation
|
||||
|
||||
### Integration Tests
|
||||
- Clone from public repo
|
||||
- Clone from private repo with auth
|
||||
- Push/pull with mock remote
|
||||
- Branch operations
|
||||
|
||||
### Manual Testing
|
||||
- Full OAuth flow
|
||||
- Dashboard status refresh
|
||||
- Clone flow end-to-end
|
||||
- Commit reminder timing
|
||||
- Conflict resolution
|
||||
|
||||
## Cline Usage Notes
|
||||
|
||||
### Before Starting Each Task
|
||||
|
||||
1. Read the task document completely
|
||||
2. Review existing git infrastructure:
|
||||
- `packages/noodl-git/src/git.ts`
|
||||
- `packages/noodl-editor/src/editor/src/views/panels/VersionControlPanel/`
|
||||
3. Check GitStore and credential handling
|
||||
|
||||
### Key Gotchas
|
||||
|
||||
1. **Git operations are async**: Always use try/catch, git can fail
|
||||
2. **Repository paths**: Use `_retainedProjectDirectory` from ProjectModel
|
||||
3. **Merge strategy**: Noodl has custom merge for project.json (`mergeProject`)
|
||||
4. **Auth caching**: Credentials cached by trampoline, may need clearing
|
||||
5. **Electron context**: Some git ops need main process (deep links)
|
||||
|
||||
### Testing Git Operations
|
||||
|
||||
```bash
|
||||
# In tests directory, run git tests
|
||||
npm run test:editor -- --grep="Git"
|
||||
```
|
||||
|
||||
## Success Criteria (Series Complete)
|
||||
|
||||
1. ✅ Users can authenticate with GitHub via OAuth
|
||||
2. ✅ Git status visible in project dashboard
|
||||
3. ✅ Users can clone repositories from UI
|
||||
4. ✅ New projects have git by default
|
||||
5. ✅ Users are reminded to commit regularly
|
||||
6. ✅ Pull/push is intuitive with previews
|
||||
7. ✅ Branch management is accessible
|
||||
|
||||
## Future Work (Post-GIT)
|
||||
|
||||
The GIT series enables:
|
||||
- **COMP series**: Shared component repositories
|
||||
- **DEPLOY series**: Auto-push to frontend repo on deploy
|
||||
- **Community features**: Public component sharing
|
||||
|
||||
## Files in This Series
|
||||
|
||||
- `GIT-001-github-oauth.md`
|
||||
- `GIT-002-dashboard-git-status.md`
|
||||
- `GIT-003-repository-cloning.md`
|
||||
- `GIT-004-auto-init-commit-encouragement.md`
|
||||
- `GIT-005-enhanced-push-pull.md`
|
||||
- `GIT-OVERVIEW.md` (this file)
|
||||
@@ -0,0 +1,408 @@
|
||||
# COMP-001: Prefab System Refactoring
|
||||
|
||||
## Overview
|
||||
|
||||
Refactor the existing prefab system to support multiple sources (not just the docs endpoint). This creates the foundation for built-in prefabs, personal repositories, organization repositories, and community contributions.
|
||||
|
||||
## Context
|
||||
|
||||
The current prefab system is tightly coupled to the docs endpoint:
|
||||
- `ModuleLibraryModel` fetches from `${docsEndpoint}/library/prefabs/index.json`
|
||||
- Prefabs are zip files hosted on the docs site
|
||||
- No support for alternative sources
|
||||
|
||||
This task creates an abstraction layer that allows prefabs to come from multiple sources while maintaining the existing user experience.
|
||||
|
||||
### Current Architecture
|
||||
|
||||
```
|
||||
User clicks "Clone" in NodePicker
|
||||
↓
|
||||
ModuleLibraryModel.installPrefab(url)
|
||||
↓
|
||||
getModuleTemplateRoot(url) ← Downloads & extracts zip
|
||||
↓
|
||||
ProjectImporter.listComponentsAndDependencies()
|
||||
↓
|
||||
ProjectImporter.checkForCollisions()
|
||||
↓
|
||||
_showImportPopup() if collisions
|
||||
↓
|
||||
_doImport()
|
||||
```
|
||||
|
||||
### Key Files
|
||||
|
||||
- `packages/noodl-editor/src/editor/src/models/modulelibrarymodel.ts`
|
||||
- `packages/noodl-editor/src/editor/src/utils/projectimporter.js`
|
||||
- `packages/noodl-editor/src/editor/src/views/NodePicker/`
|
||||
|
||||
## Requirements
|
||||
|
||||
### Functional Requirements
|
||||
|
||||
1. **Source Abstraction**
|
||||
- Define `PrefabSource` interface for different sources
|
||||
- Support multiple sources simultaneously
|
||||
- Each source provides: list, search, fetch, metadata
|
||||
|
||||
2. **Source Types**
|
||||
- `DocsSource` - Existing docs endpoint (default)
|
||||
- `BuiltInSource` - Bundled with editor (COMP-002)
|
||||
- `GitHubSource` - GitHub repositories (COMP-003+)
|
||||
- `LocalSource` - Local filesystem (for development)
|
||||
|
||||
3. **Unified Prefab Model**
|
||||
- Consistent metadata across all sources
|
||||
- Version information
|
||||
- Source tracking (where did this come from?)
|
||||
- Dependencies and requirements
|
||||
|
||||
4. **Enhanced Metadata**
|
||||
- Author information
|
||||
- Version number
|
||||
- Noodl version compatibility
|
||||
- Screenshots/previews
|
||||
- Changelog
|
||||
- License
|
||||
|
||||
5. **Backwards Compatibility**
|
||||
- Existing prefabs continue to work
|
||||
- No changes to user workflow
|
||||
- Migration path for enhanced metadata
|
||||
|
||||
### Non-Functional Requirements
|
||||
|
||||
- Source fetching is async and non-blocking
|
||||
- Caching for performance
|
||||
- Graceful degradation if source unavailable
|
||||
- Extensible for future sources
|
||||
|
||||
## Technical Approach
|
||||
|
||||
### 1. Prefab Source Interface
|
||||
|
||||
```typescript
|
||||
// packages/noodl-editor/src/editor/src/models/prefab/PrefabSource.ts
|
||||
|
||||
interface PrefabMetadata {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
version: string;
|
||||
author?: {
|
||||
name: string;
|
||||
email?: string;
|
||||
url?: string;
|
||||
};
|
||||
noodlVersion?: string; // Minimum compatible version
|
||||
tags: string[];
|
||||
icon?: string;
|
||||
screenshots?: string[];
|
||||
docs?: string;
|
||||
license?: string;
|
||||
repository?: string;
|
||||
dependencies?: string[]; // Other prefabs this depends on
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
interface PrefabSourceConfig {
|
||||
id: string;
|
||||
name: string;
|
||||
priority: number; // Higher = shown first
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
interface PrefabSource {
|
||||
readonly config: PrefabSourceConfig;
|
||||
|
||||
// Lifecycle
|
||||
initialize(): Promise<void>;
|
||||
dispose(): void;
|
||||
|
||||
// Listing
|
||||
listPrefabs(): Promise<PrefabMetadata[]>;
|
||||
searchPrefabs(query: string): Promise<PrefabMetadata[]>;
|
||||
|
||||
// Fetching
|
||||
getPrefabDetails(id: string): Promise<PrefabMetadata>;
|
||||
downloadPrefab(id: string): Promise<string>; // Returns local path to extracted content
|
||||
|
||||
// State
|
||||
isAvailable(): boolean;
|
||||
getLastError(): Error | null;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Source Implementations
|
||||
|
||||
```typescript
|
||||
// DocsSource - existing functionality wrapped
|
||||
class DocsPrefabSource implements PrefabSource {
|
||||
config = {
|
||||
id: 'docs',
|
||||
name: 'Community Prefabs',
|
||||
priority: 50,
|
||||
enabled: true
|
||||
};
|
||||
|
||||
async listPrefabs(): Promise<PrefabMetadata[]> {
|
||||
// Existing fetch logic from ModuleLibraryModel
|
||||
const endpoint = getDocsEndpoint();
|
||||
const response = await fetch(`${endpoint}/library/prefabs/index.json`);
|
||||
const items = await response.json();
|
||||
|
||||
// Transform to new metadata format
|
||||
return items.map(item => this.transformLegacyItem(item));
|
||||
}
|
||||
|
||||
private transformLegacyItem(item: IModule): PrefabMetadata {
|
||||
return {
|
||||
id: `docs:${item.label}`,
|
||||
name: item.label,
|
||||
description: item.desc,
|
||||
version: '1.0.0', // Legacy items don't have versions
|
||||
tags: item.tags || [],
|
||||
icon: item.icon,
|
||||
docs: item.docs
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// BuiltInSource - for COMP-002
|
||||
class BuiltInPrefabSource implements PrefabSource {
|
||||
config = {
|
||||
id: 'builtin',
|
||||
name: 'Built-in Prefabs',
|
||||
priority: 100,
|
||||
enabled: true
|
||||
};
|
||||
|
||||
// Implementation in COMP-002
|
||||
}
|
||||
|
||||
// GitHubSource - for COMP-003+
|
||||
class GitHubPrefabSource implements PrefabSource {
|
||||
config = {
|
||||
id: 'github',
|
||||
name: 'GitHub',
|
||||
priority: 75,
|
||||
enabled: true
|
||||
};
|
||||
|
||||
constructor(private repoUrl: string) {}
|
||||
|
||||
// Implementation in COMP-003
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Prefab Registry
|
||||
|
||||
```typescript
|
||||
// packages/noodl-editor/src/editor/src/models/prefab/PrefabRegistry.ts
|
||||
|
||||
class PrefabRegistry {
|
||||
private static instance: PrefabRegistry;
|
||||
private sources: Map<string, PrefabSource> = new Map();
|
||||
private cache: Map<string, PrefabMetadata[]> = new Map();
|
||||
|
||||
// Source management
|
||||
registerSource(source: PrefabSource): void;
|
||||
unregisterSource(sourceId: string): void;
|
||||
getSource(sourceId: string): PrefabSource | undefined;
|
||||
getSources(): PrefabSource[];
|
||||
|
||||
// Aggregated operations
|
||||
async getAllPrefabs(): Promise<PrefabMetadata[]>;
|
||||
async searchAllPrefabs(query: string): Promise<PrefabMetadata[]>;
|
||||
|
||||
// Installation
|
||||
async installPrefab(prefabId: string, options?: InstallOptions): Promise<void>;
|
||||
|
||||
// Cache
|
||||
invalidateCache(sourceId?: string): void;
|
||||
|
||||
// Events
|
||||
onSourcesChanged(callback: () => void): () => void;
|
||||
onPrefabsUpdated(callback: () => void): () => void;
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Updated ModuleLibraryModel
|
||||
|
||||
```typescript
|
||||
// Refactored to use PrefabRegistry
|
||||
|
||||
export class ModuleLibraryModel extends Model {
|
||||
private registry: PrefabRegistry;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.registry = PrefabRegistry.instance;
|
||||
|
||||
// Register default sources
|
||||
this.registry.registerSource(new DocsPrefabSource());
|
||||
this.registry.registerSource(new BuiltInPrefabSource());
|
||||
|
||||
// Listen for updates
|
||||
this.registry.onPrefabsUpdated(() => {
|
||||
this.notifyListeners('libraryUpdated');
|
||||
});
|
||||
}
|
||||
|
||||
// Backwards compatible API
|
||||
get prefabs(): IModule[] {
|
||||
return this.registry.getAllPrefabsSync()
|
||||
.map(p => this.transformToLegacy(p));
|
||||
}
|
||||
|
||||
async installPrefab(url: string, ...): Promise<void> {
|
||||
// Detect source from URL or use legacy path
|
||||
const prefabId = this.detectPrefabId(url);
|
||||
await this.registry.installPrefab(prefabId);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Files to Create
|
||||
|
||||
1. `packages/noodl-editor/src/editor/src/models/prefab/PrefabSource.ts` - Interface definitions
|
||||
2. `packages/noodl-editor/src/editor/src/models/prefab/PrefabRegistry.ts` - Central registry
|
||||
3. `packages/noodl-editor/src/editor/src/models/prefab/sources/DocsPrefabSource.ts` - Docs implementation
|
||||
4. `packages/noodl-editor/src/editor/src/models/prefab/sources/BuiltInPrefabSource.ts` - Stub for COMP-002
|
||||
5. `packages/noodl-editor/src/editor/src/models/prefab/sources/GitHubPrefabSource.ts` - Stub for COMP-003+
|
||||
6. `packages/noodl-editor/src/editor/src/models/prefab/sources/LocalPrefabSource.ts` - For development
|
||||
7. `packages/noodl-editor/src/editor/src/models/prefab/index.ts` - Barrel exports
|
||||
|
||||
## Files to Modify
|
||||
|
||||
1. `packages/noodl-editor/src/editor/src/models/modulelibrarymodel.ts`
|
||||
- Refactor to use PrefabRegistry
|
||||
- Maintain backwards compatible API
|
||||
- Delegate to sources
|
||||
|
||||
2. `packages/noodl-editor/src/editor/src/views/NodePicker/tabs/NodePickerSearchView/NodePickerSearchView.tsx`
|
||||
- Update to work with new metadata format
|
||||
- Add source indicators
|
||||
|
||||
3. `packages/noodl-editor/src/editor/src/views/NodePicker/components/ModuleCard/ModuleCard.tsx`
|
||||
- Add source badge
|
||||
- Add version display
|
||||
- Handle enhanced metadata
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### Phase 1: Interfaces & Registry
|
||||
1. Define PrefabSource interface
|
||||
2. Define PrefabMetadata interface
|
||||
3. Create PrefabRegistry class
|
||||
4. Add source registration
|
||||
|
||||
### Phase 2: Docs Source Migration
|
||||
1. Create DocsPrefabSource
|
||||
2. Migrate existing fetch logic
|
||||
3. Add metadata transformation
|
||||
4. Test backwards compatibility
|
||||
|
||||
### Phase 3: ModuleLibraryModel Refactor
|
||||
1. Integrate PrefabRegistry
|
||||
2. Maintain backwards compatible API
|
||||
3. Update install methods
|
||||
4. Add source detection
|
||||
|
||||
### Phase 4: UI Updates
|
||||
1. Add source indicators to cards
|
||||
2. Show version information
|
||||
3. Handle multiple sources in search
|
||||
|
||||
### Phase 5: Stub Sources
|
||||
1. Create BuiltInPrefabSource stub
|
||||
2. Create GitHubPrefabSource stub
|
||||
3. Create LocalPrefabSource for development
|
||||
|
||||
## Metadata Schema
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"required": ["id", "name", "version"],
|
||||
"properties": {
|
||||
"id": { "type": "string" },
|
||||
"name": { "type": "string" },
|
||||
"description": { "type": "string" },
|
||||
"version": { "type": "string", "pattern": "^\\d+\\.\\d+\\.\\d+$" },
|
||||
"author": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": { "type": "string" },
|
||||
"email": { "type": "string" },
|
||||
"url": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"noodlVersion": { "type": "string" },
|
||||
"tags": { "type": "array", "items": { "type": "string" } },
|
||||
"icon": { "type": "string" },
|
||||
"screenshots": { "type": "array", "items": { "type": "string" } },
|
||||
"docs": { "type": "string" },
|
||||
"license": { "type": "string" },
|
||||
"repository": { "type": "string" },
|
||||
"dependencies": { "type": "array", "items": { "type": "string" } }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
- [ ] PrefabRegistry initializes correctly
|
||||
- [ ] DocsPrefabSource fetches from docs endpoint
|
||||
- [ ] Legacy prefabs continue to work
|
||||
- [ ] Metadata transformation preserves data
|
||||
- [ ] Multiple sources aggregate correctly
|
||||
- [ ] Search works across sources
|
||||
- [ ] Install works from any source
|
||||
- [ ] Source indicators display correctly
|
||||
- [ ] Cache invalidation works
|
||||
- [ ] Error handling for unavailable sources
|
||||
|
||||
## Dependencies
|
||||
|
||||
- None (foundation task)
|
||||
|
||||
## Blocked By
|
||||
|
||||
- None
|
||||
|
||||
## Blocks
|
||||
|
||||
- COMP-002 (Built-in Prefabs)
|
||||
- COMP-003 (Component Export)
|
||||
- COMP-004 (Organization Components)
|
||||
|
||||
## Estimated Effort
|
||||
|
||||
- Interfaces & types: 2-3 hours
|
||||
- PrefabRegistry: 3-4 hours
|
||||
- DocsPrefabSource: 2-3 hours
|
||||
- ModuleLibraryModel refactor: 3-4 hours
|
||||
- UI updates: 2-3 hours
|
||||
- Testing: 2-3 hours
|
||||
- **Total: 14-20 hours**
|
||||
|
||||
## Success Criteria
|
||||
|
||||
1. New source abstraction in place
|
||||
2. Existing prefabs continue to work identically
|
||||
3. Multiple sources can be registered
|
||||
4. UI shows source indicators
|
||||
5. Foundation ready for built-in and GitHub sources
|
||||
|
||||
## Future Considerations
|
||||
|
||||
- Source priority/ordering configuration
|
||||
- Source enable/disable in settings
|
||||
- Custom source plugins
|
||||
- Prefab ratings/popularity
|
||||
- Usage analytics per source
|
||||
@@ -0,0 +1,394 @@
|
||||
# COMP-002: Built-in Prefabs
|
||||
|
||||
## Overview
|
||||
|
||||
Bundle essential prefabs directly with the OpenNoodl editor, so they're available immediately without network access. This improves the onboarding experience and ensures core functionality is always available.
|
||||
|
||||
## Context
|
||||
|
||||
Currently, all prefabs are fetched from the docs endpoint at runtime:
|
||||
- Requires network connectivity
|
||||
- Adds latency on first load
|
||||
- No prefabs available offline
|
||||
- New users see empty prefab library initially
|
||||
|
||||
By bundling prefabs with the editor:
|
||||
- Instant availability
|
||||
- Works offline
|
||||
- Consistent experience for all users
|
||||
- Core prefabs versioned with editor releases
|
||||
|
||||
### Existing Export/Import
|
||||
|
||||
From `exportProjectComponents.ts` and `projectimporter.js`:
|
||||
- Components exported as zip files
|
||||
- Import handles collision detection
|
||||
- Styles, variants, resources included
|
||||
- Dependency tracking exists
|
||||
|
||||
## Requirements
|
||||
|
||||
### Functional Requirements
|
||||
|
||||
1. **Built-in Prefab Bundle**
|
||||
- Essential prefabs bundled in editor distribution
|
||||
- Loaded from local filesystem, not network
|
||||
- Versioned with editor releases
|
||||
|
||||
2. **Prefab Selection**
|
||||
- Form components (Input, Button, Checkbox, etc.)
|
||||
- Layout helpers (Card, Modal, Drawer)
|
||||
- Data utilities (REST caller, LocalStorage, etc.)
|
||||
- Authentication flows (basic patterns)
|
||||
- Navigation patterns
|
||||
|
||||
3. **UI Distinction**
|
||||
- "Built-in" badge on bundled prefabs
|
||||
- Shown first in prefab list
|
||||
- Separate section or filter option
|
||||
|
||||
4. **Update Mechanism**
|
||||
- Built-in prefabs update with editor
|
||||
- No manual update needed
|
||||
- Changelog visible for what's new
|
||||
|
||||
5. **Offline First**
|
||||
- Available immediately on fresh install
|
||||
- No network request needed
|
||||
- Graceful handling when docs unavailable
|
||||
|
||||
### Non-Functional Requirements
|
||||
|
||||
- Bundle size impact < 5MB
|
||||
- Load time < 500ms
|
||||
- No runtime network dependency
|
||||
- Works in air-gapped environments
|
||||
|
||||
## Technical Approach
|
||||
|
||||
### 1. Bundle Structure
|
||||
|
||||
```
|
||||
packages/noodl-editor/
|
||||
├── static/
|
||||
│ └── builtin-prefabs/
|
||||
│ ├── index.json # Manifest of built-in prefabs
|
||||
│ └── prefabs/
|
||||
│ ├── form-input/
|
||||
│ │ ├── prefab.json # Metadata
|
||||
│ │ └── components/ # Component files
|
||||
│ ├── form-button/
|
||||
│ ├── card-layout/
|
||||
│ ├── modal-dialog/
|
||||
│ ├── rest-client/
|
||||
│ └── ...
|
||||
```
|
||||
|
||||
### 2. Manifest Format
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"noodlVersion": "2.10.0",
|
||||
"prefabs": [
|
||||
{
|
||||
"id": "builtin:form-input",
|
||||
"name": "Form Input",
|
||||
"description": "Styled text input with label, validation, and error states",
|
||||
"version": "1.0.0",
|
||||
"category": "Forms",
|
||||
"tags": ["form", "input", "text", "validation"],
|
||||
"icon": "input-icon.svg",
|
||||
"path": "prefabs/form-input"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 3. BuiltInPrefabSource Implementation
|
||||
|
||||
```typescript
|
||||
// packages/noodl-editor/src/editor/src/models/prefab/sources/BuiltInPrefabSource.ts
|
||||
|
||||
import { platform } from '@noodl/platform';
|
||||
|
||||
class BuiltInPrefabSource implements PrefabSource {
|
||||
config = {
|
||||
id: 'builtin',
|
||||
name: 'Built-in',
|
||||
priority: 100, // Highest priority - show first
|
||||
enabled: true
|
||||
};
|
||||
|
||||
private manifest: BuiltInManifest | null = null;
|
||||
private basePath: string;
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
// Get path to bundled prefabs
|
||||
this.basePath = platform.getBuiltInPrefabsPath();
|
||||
|
||||
// Load manifest
|
||||
const manifestPath = path.join(this.basePath, 'index.json');
|
||||
const content = await fs.readFile(manifestPath, 'utf-8');
|
||||
this.manifest = JSON.parse(content);
|
||||
}
|
||||
|
||||
async listPrefabs(): Promise<PrefabMetadata[]> {
|
||||
if (!this.manifest) await this.initialize();
|
||||
|
||||
return this.manifest.prefabs.map(p => ({
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
description: p.description,
|
||||
version: p.version,
|
||||
tags: p.tags,
|
||||
icon: this.resolveIcon(p.icon),
|
||||
source: 'builtin',
|
||||
category: p.category
|
||||
}));
|
||||
}
|
||||
|
||||
async downloadPrefab(id: string): Promise<string> {
|
||||
// No download needed - return local path
|
||||
const prefab = this.manifest.prefabs.find(p => p.id === id);
|
||||
return path.join(this.basePath, prefab.path);
|
||||
}
|
||||
|
||||
private resolveIcon(iconPath: string): string {
|
||||
return `file://${path.join(this.basePath, 'icons', iconPath)}`;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Build-time Prefab Bundling
|
||||
|
||||
```typescript
|
||||
// scripts/bundle-prefabs.ts
|
||||
|
||||
/**
|
||||
* Run during build to prepare built-in prefabs
|
||||
* 1. Reads prefab source projects
|
||||
* 2. Exports components
|
||||
* 3. Generates manifest
|
||||
* 4. Copies to static directory
|
||||
*/
|
||||
|
||||
async function bundlePrefabs() {
|
||||
const prefabSources = await glob('prefab-sources/*');
|
||||
const manifest: BuiltInManifest = {
|
||||
version: packageJson.version,
|
||||
noodlVersion: packageJson.version,
|
||||
prefabs: []
|
||||
};
|
||||
|
||||
for (const source of prefabSources) {
|
||||
const metadata = await readPrefabMetadata(source);
|
||||
const outputPath = path.join(OUTPUT_DIR, metadata.id);
|
||||
|
||||
await exportPrefabComponents(source, outputPath);
|
||||
|
||||
manifest.prefabs.push({
|
||||
id: `builtin:${metadata.id}`,
|
||||
name: metadata.name,
|
||||
description: metadata.description,
|
||||
version: metadata.version,
|
||||
category: metadata.category,
|
||||
tags: metadata.tags,
|
||||
icon: metadata.icon,
|
||||
path: metadata.id
|
||||
});
|
||||
}
|
||||
|
||||
await writeManifest(manifest);
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Prefab Categories
|
||||
|
||||
```typescript
|
||||
enum PrefabCategory {
|
||||
Forms = 'Forms',
|
||||
Layout = 'Layout',
|
||||
Navigation = 'Navigation',
|
||||
Data = 'Data',
|
||||
Authentication = 'Authentication',
|
||||
Feedback = 'Feedback',
|
||||
Media = 'Media'
|
||||
}
|
||||
|
||||
const BUILT_IN_PREFABS: BuiltInPrefabConfig[] = [
|
||||
// Forms
|
||||
{ id: 'form-input', category: PrefabCategory.Forms },
|
||||
{ id: 'form-textarea', category: PrefabCategory.Forms },
|
||||
{ id: 'form-checkbox', category: PrefabCategory.Forms },
|
||||
{ id: 'form-radio', category: PrefabCategory.Forms },
|
||||
{ id: 'form-select', category: PrefabCategory.Forms },
|
||||
{ id: 'form-button', category: PrefabCategory.Forms },
|
||||
|
||||
// Layout
|
||||
{ id: 'card', category: PrefabCategory.Layout },
|
||||
{ id: 'modal', category: PrefabCategory.Layout },
|
||||
{ id: 'drawer', category: PrefabCategory.Layout },
|
||||
{ id: 'accordion', category: PrefabCategory.Layout },
|
||||
{ id: 'tabs', category: PrefabCategory.Layout },
|
||||
|
||||
// Navigation
|
||||
{ id: 'navbar', category: PrefabCategory.Navigation },
|
||||
{ id: 'sidebar', category: PrefabCategory.Navigation },
|
||||
{ id: 'breadcrumb', category: PrefabCategory.Navigation },
|
||||
{ id: 'pagination', category: PrefabCategory.Navigation },
|
||||
|
||||
// Data
|
||||
{ id: 'rest-client', category: PrefabCategory.Data },
|
||||
{ id: 'local-storage', category: PrefabCategory.Data },
|
||||
{ id: 'data-table', category: PrefabCategory.Data },
|
||||
|
||||
// Feedback
|
||||
{ id: 'toast', category: PrefabCategory.Feedback },
|
||||
{ id: 'loading-spinner', category: PrefabCategory.Feedback },
|
||||
{ id: 'progress-bar', category: PrefabCategory.Feedback },
|
||||
];
|
||||
```
|
||||
|
||||
## Files to Create
|
||||
|
||||
1. `packages/noodl-editor/static/builtin-prefabs/index.json` - Manifest
|
||||
2. `packages/noodl-editor/static/builtin-prefabs/prefabs/` - Prefab directories
|
||||
3. `packages/noodl-editor/src/editor/src/models/prefab/sources/BuiltInPrefabSource.ts` - Source implementation
|
||||
4. `scripts/bundle-prefabs.ts` - Build script
|
||||
5. `prefab-sources/` - Source projects for built-in prefabs
|
||||
|
||||
## Files to Modify
|
||||
|
||||
1. `packages/noodl-editor/src/editor/src/models/prefab/PrefabRegistry.ts`
|
||||
- Register BuiltInPrefabSource
|
||||
- Add category support
|
||||
|
||||
2. `packages/noodl-editor/src/editor/src/views/NodePicker/tabs/NodePickerSearchView/NodePickerSearchView.tsx`
|
||||
- Add category filtering
|
||||
- Show "Built-in" badge
|
||||
|
||||
3. `packages/noodl-editor/src/editor/src/views/NodePicker/components/ModuleCard/ModuleCard.tsx`
|
||||
- Add "Built-in" badge styling
|
||||
- Show category
|
||||
|
||||
4. `package.json`
|
||||
- Add bundle-prefabs script
|
||||
|
||||
5. `webpack.config.js` or equivalent
|
||||
- Include static/builtin-prefabs in build
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### Phase 1: Infrastructure
|
||||
1. Create bundle directory structure
|
||||
2. Implement BuiltInPrefabSource
|
||||
3. Create manifest format
|
||||
4. Register source in PrefabRegistry
|
||||
|
||||
### Phase 2: Build Pipeline
|
||||
1. Create bundle-prefabs script
|
||||
2. Add to build process
|
||||
3. Test bundling works
|
||||
|
||||
### Phase 3: Initial Prefabs
|
||||
1. Create Form Input prefab
|
||||
2. Create Form Button prefab
|
||||
3. Create Card layout prefab
|
||||
4. Test import/collision handling
|
||||
|
||||
### Phase 4: UI Updates
|
||||
1. Add "Built-in" badge
|
||||
2. Add category filter
|
||||
3. Show built-in prefabs first
|
||||
|
||||
### Phase 5: Full Prefab Set
|
||||
1. Create remaining form prefabs
|
||||
2. Create layout prefabs
|
||||
3. Create data prefabs
|
||||
4. Create navigation prefabs
|
||||
|
||||
### Phase 6: Documentation
|
||||
1. Document built-in prefabs
|
||||
2. Add usage examples
|
||||
3. Create component docs
|
||||
|
||||
## Initial Built-in Prefabs
|
||||
|
||||
### Priority 1 (MVP)
|
||||
| Prefab | Category | Components |
|
||||
|--------|----------|------------|
|
||||
| Form Input | Forms | TextInput, Label, ErrorMessage |
|
||||
| Form Button | Forms | Button, LoadingState |
|
||||
| Card | Layout | Card, CardHeader, CardBody |
|
||||
| Modal | Layout | Modal, ModalTrigger, ModalContent |
|
||||
| REST Client | Data | RESTRequest, ResponseHandler |
|
||||
|
||||
### Priority 2
|
||||
| Prefab | Category | Components |
|
||||
|--------|----------|------------|
|
||||
| Form Textarea | Forms | Textarea, CharCount |
|
||||
| Form Checkbox | Forms | Checkbox, CheckboxGroup |
|
||||
| Form Select | Forms | Select, Option |
|
||||
| Drawer | Layout | Drawer, DrawerTrigger |
|
||||
| Toast | Feedback | Toast, ToastContainer |
|
||||
|
||||
### Priority 3
|
||||
| Prefab | Category | Components |
|
||||
|--------|----------|------------|
|
||||
| Tabs | Layout | TabBar, TabPanel |
|
||||
| Accordion | Layout | Accordion, AccordionItem |
|
||||
| Navbar | Navigation | Navbar, NavItem |
|
||||
| Data Table | Data | Table, Column, Row, Cell |
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
- [ ] Built-in prefabs load without network
|
||||
- [ ] Prefabs appear first in list
|
||||
- [ ] "Built-in" badge displays correctly
|
||||
- [ ] Category filter works
|
||||
- [ ] Import works for each prefab
|
||||
- [ ] Collision detection works
|
||||
- [ ] Styles import correctly
|
||||
- [ ] Works in air-gapped environment
|
||||
- [ ] Bundle size is acceptable
|
||||
- [ ] Load time is acceptable
|
||||
|
||||
## Dependencies
|
||||
|
||||
- COMP-001 (Prefab System Refactoring)
|
||||
|
||||
## Blocked By
|
||||
|
||||
- COMP-001
|
||||
|
||||
## Blocks
|
||||
|
||||
- None (can proceed in parallel with COMP-003+)
|
||||
|
||||
## Estimated Effort
|
||||
|
||||
- Infrastructure: 3-4 hours
|
||||
- Build pipeline: 2-3 hours
|
||||
- BuiltInPrefabSource: 2-3 hours
|
||||
- MVP prefabs (5): 8-10 hours
|
||||
- UI updates: 2-3 hours
|
||||
- Testing: 2-3 hours
|
||||
- **Total: 19-26 hours**
|
||||
|
||||
## Success Criteria
|
||||
|
||||
1. Built-in prefabs available immediately
|
||||
2. Work offline without network
|
||||
3. Clear "Built-in" distinction in UI
|
||||
4. Categories organize prefabs logically
|
||||
5. Import flow works smoothly
|
||||
6. Bundle size < 5MB
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- User can hide built-in prefabs
|
||||
- Community voting for built-in inclusion
|
||||
- Per-category enable/disable
|
||||
- Built-in prefab updates notification
|
||||
- Prefab source code viewing
|
||||
@@ -0,0 +1,380 @@
|
||||
# COMP-003: Component Export to Repository
|
||||
|
||||
## Overview
|
||||
|
||||
Enable users to export components from their project to a GitHub repository, creating a personal component library. This allows sharing components across projects and with team members.
|
||||
|
||||
## Context
|
||||
|
||||
Currently, component sharing is manual:
|
||||
1. Export components as zip (Cmd+Shift+E)
|
||||
2. Manually upload to GitHub or share file
|
||||
3. Others download and import
|
||||
|
||||
This task streamlines the process:
|
||||
1. Right-click component → "Export to Repository"
|
||||
2. Select target repository
|
||||
3. Component is committed with metadata
|
||||
4. Available in NodePicker for other projects
|
||||
|
||||
### Existing Export Flow
|
||||
|
||||
From `exportProjectComponents.ts`:
|
||||
```typescript
|
||||
export function exportProjectComponents() {
|
||||
ProjectImporter.instance.listComponentsAndDependencies(
|
||||
ProjectModel.instance._retainedProjectDirectory,
|
||||
(components) => {
|
||||
// Shows export popup
|
||||
// User selects components
|
||||
// Creates zip file
|
||||
}
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
### Functional Requirements
|
||||
|
||||
1. **Export Entry Points**
|
||||
- Right-click component → "Export to Repository"
|
||||
- Component sheet context menu → "Export Sheet to Repository"
|
||||
- File menu → "Export Components to Repository"
|
||||
|
||||
2. **Repository Selection**
|
||||
- List user's GitHub repositories
|
||||
- "Create new repository" option
|
||||
- Remember last used repository
|
||||
- Suggest `noodl-components` naming convention
|
||||
|
||||
3. **Component Selection**
|
||||
- Select individual components
|
||||
- Select entire sheets
|
||||
- Auto-select dependencies
|
||||
- Preview what will be exported
|
||||
|
||||
4. **Metadata Entry**
|
||||
- Component name (prefilled)
|
||||
- Description
|
||||
- Tags
|
||||
- Version (auto-increment option)
|
||||
- Category selection
|
||||
|
||||
5. **Export Process**
|
||||
- Create component directory structure
|
||||
- Generate prefab.json manifest
|
||||
- Commit to repository
|
||||
- Optional: Push immediately or stage
|
||||
|
||||
6. **Repository Structure**
|
||||
- Standard directory layout
|
||||
- index.json manifest for discovery
|
||||
- README generation
|
||||
- License file option
|
||||
|
||||
### Non-Functional Requirements
|
||||
|
||||
- Export completes in < 30 seconds
|
||||
- Works with existing repositories
|
||||
- Handles large components (100+ nodes)
|
||||
- Conflict detection with existing exports
|
||||
|
||||
## Technical Approach
|
||||
|
||||
### 1. Repository Structure Convention
|
||||
|
||||
```
|
||||
my-noodl-components/
|
||||
├── index.json # Repository manifest
|
||||
├── README.md # Auto-generated docs
|
||||
├── LICENSE # Optional license
|
||||
└── components/
|
||||
├── my-button/
|
||||
│ ├── prefab.json # Component metadata
|
||||
│ ├── component.ndjson # Noodl component data
|
||||
│ ├── dependencies/ # Style/variant dependencies
|
||||
│ └── assets/ # Images, fonts
|
||||
├── my-card/
|
||||
│ └── ...
|
||||
└── my-form/
|
||||
└── ...
|
||||
```
|
||||
|
||||
### 2. Repository Manifest (index.json)
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://opennoodl.net/schemas/component-repo-v1.json",
|
||||
"name": "My Noodl Components",
|
||||
"description": "Personal component library",
|
||||
"author": {
|
||||
"name": "John Doe",
|
||||
"github": "johndoe"
|
||||
},
|
||||
"version": "1.0.0",
|
||||
"noodlVersion": ">=2.10.0",
|
||||
"components": [
|
||||
{
|
||||
"id": "my-button",
|
||||
"name": "My Button",
|
||||
"description": "Custom styled button",
|
||||
"version": "1.2.0",
|
||||
"path": "components/my-button",
|
||||
"tags": ["form", "button"],
|
||||
"category": "Forms"
|
||||
}
|
||||
],
|
||||
"updatedAt": "2024-01-15T10:30:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Component Export Service
|
||||
|
||||
```typescript
|
||||
// packages/noodl-editor/src/editor/src/services/ComponentExportService.ts
|
||||
|
||||
interface ExportOptions {
|
||||
components: ComponentModel[];
|
||||
repository: GitHubRepo;
|
||||
metadata: {
|
||||
description: string;
|
||||
tags: string[];
|
||||
category: string;
|
||||
version?: string;
|
||||
};
|
||||
commitMessage?: string;
|
||||
pushImmediately?: boolean;
|
||||
}
|
||||
|
||||
interface ExportResult {
|
||||
success: boolean;
|
||||
exportedComponents: string[];
|
||||
commitSha?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
class ComponentExportService {
|
||||
private static instance: ComponentExportService;
|
||||
|
||||
// Export flow
|
||||
async exportToRepository(options: ExportOptions): Promise<ExportResult>;
|
||||
|
||||
// Repository management
|
||||
async listUserRepositories(): Promise<GitHubRepo[]>;
|
||||
async createComponentRepository(name: string): Promise<GitHubRepo>;
|
||||
async validateRepository(repo: GitHubRepo): Promise<boolean>;
|
||||
|
||||
// Component preparation
|
||||
async prepareExport(components: ComponentModel[]): Promise<ExportPackage>;
|
||||
async resolveExportDependencies(components: ComponentModel[]): Promise<ComponentModel[]>;
|
||||
|
||||
// File generation
|
||||
generatePrefabManifest(component: ComponentModel, metadata: ExportMetadata): PrefabManifest;
|
||||
generateRepoManifest(repo: GitHubRepo, components: PrefabManifest[]): RepoManifest;
|
||||
generateReadme(repo: GitHubRepo, components: PrefabManifest[]): string;
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Export Modal Flow
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ Export to Repository [×] │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ COMPONENTS TO EXPORT │
|
||||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ ☑ MyButton + 2 dependencies │ │
|
||||
│ │ └─ ☑ ButtonStyles (variant) │ │
|
||||
│ │ └─ ☑ PrimaryColor (color style) │ │
|
||||
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ TARGET REPOSITORY │
|
||||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ johndoe/noodl-components [▾] │ │
|
||||
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||
│ [+ Create new repository] │
|
||||
│ │
|
||||
│ METADATA │
|
||||
│ Name: [My Button ] │
|
||||
│ Description: [Custom styled button with loading state ] │
|
||||
│ Tags: [form] [button] [+] │
|
||||
│ Category: [Forms ▾] │
|
||||
│ Version: [1.0.0 ] ☑ Auto-increment │
|
||||
│ │
|
||||
│ COMMIT │
|
||||
│ Message: [Add MyButton component ] │
|
||||
│ ☑ Push to GitHub immediately │
|
||||
│ │
|
||||
│ [Cancel] [Export] │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 5. Export Process Flow
|
||||
|
||||
```typescript
|
||||
async exportToRepository(options: ExportOptions): Promise<ExportResult> {
|
||||
const { components, repository, metadata } = options;
|
||||
|
||||
// 1. Clone or open repository locally
|
||||
const localRepo = await this.getLocalRepository(repository);
|
||||
|
||||
// 2. Resolve all dependencies
|
||||
const allComponents = await this.resolveExportDependencies(components);
|
||||
|
||||
// 3. Generate component files
|
||||
for (const component of allComponents) {
|
||||
const componentDir = path.join(localRepo.path, 'components', component.id);
|
||||
|
||||
// Export component data
|
||||
await this.exportComponentData(component, componentDir);
|
||||
|
||||
// Generate prefab manifest
|
||||
const manifest = this.generatePrefabManifest(component, metadata);
|
||||
await fs.writeJson(path.join(componentDir, 'prefab.json'), manifest);
|
||||
}
|
||||
|
||||
// 4. Update repository manifest
|
||||
const repoManifest = await this.updateRepoManifest(localRepo, allComponents);
|
||||
|
||||
// 5. Update README
|
||||
await this.updateReadme(localRepo, repoManifest);
|
||||
|
||||
// 6. Commit changes
|
||||
const git = new Git(mergeProject);
|
||||
await git.openRepository(localRepo.path);
|
||||
await git.commit(options.commitMessage || `Add ${components[0].name}`);
|
||||
|
||||
// 7. Push if requested
|
||||
if (options.pushImmediately) {
|
||||
await git.push({});
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
exportedComponents: allComponents.map(c => c.name),
|
||||
commitSha: await git.getHeadCommitId()
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Files to Create
|
||||
|
||||
1. `packages/noodl-editor/src/editor/src/services/ComponentExportService.ts`
|
||||
2. `packages/noodl-core-ui/src/components/modals/ExportToRepoModal/ExportToRepoModal.tsx`
|
||||
3. `packages/noodl-core-ui/src/components/modals/ExportToRepoModal/ComponentSelector.tsx`
|
||||
4. `packages/noodl-core-ui/src/components/modals/ExportToRepoModal/RepoSelector.tsx`
|
||||
5. `packages/noodl-core-ui/src/components/modals/ExportToRepoModal/MetadataForm.tsx`
|
||||
6. `packages/noodl-core-ui/src/components/modals/CreateRepoModal/CreateRepoModal.tsx`
|
||||
7. `packages/noodl-editor/src/editor/src/utils/componentExporter.ts` - Low-level export utilities
|
||||
|
||||
## Files to Modify
|
||||
|
||||
1. `packages/noodl-editor/src/editor/src/views/nodegrapheditor.js`
|
||||
- Add right-click context menu option
|
||||
|
||||
2. `packages/noodl-editor/src/editor/src/views/panels/componentspanel.tsx`
|
||||
- Add export option to component context menu
|
||||
|
||||
3. `packages/noodl-editor/src/editor/src/utils/exportProjectComponents.ts`
|
||||
- Refactor to share code with repository export
|
||||
|
||||
4. `packages/noodl-editor/src/editor/src/models/prefab/sources/GitHubPrefabSource.ts`
|
||||
- Implement full source for reading from component repos
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### Phase 1: Export Service Foundation
|
||||
1. Create ComponentExportService
|
||||
2. Implement dependency resolution
|
||||
3. Create file generation utilities
|
||||
4. Define repository structure
|
||||
|
||||
### Phase 2: Repository Management
|
||||
1. List user repositories (via GitHub API)
|
||||
2. Create new repository flow
|
||||
3. Local repository management
|
||||
4. Clone/pull existing repos
|
||||
|
||||
### Phase 3: Export Modal
|
||||
1. Create ExportToRepoModal
|
||||
2. Create ComponentSelector
|
||||
3. Create RepoSelector
|
||||
4. Create MetadataForm
|
||||
|
||||
### Phase 4: Git Integration
|
||||
1. Stage exported files
|
||||
2. Commit with message
|
||||
3. Push to remote
|
||||
4. Handle conflicts
|
||||
|
||||
### Phase 5: Context Menu Integration
|
||||
1. Add to component right-click menu
|
||||
2. Add to sheet context menu
|
||||
3. Add to File menu
|
||||
|
||||
### Phase 6: Testing & Polish
|
||||
1. Test with various component types
|
||||
2. Test dependency resolution
|
||||
3. Error handling
|
||||
4. Progress indication
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
- [ ] Export single component works
|
||||
- [ ] Export multiple components works
|
||||
- [ ] Dependencies auto-selected
|
||||
- [ ] Repository selection lists repos
|
||||
- [ ] Create new repository works
|
||||
- [ ] Metadata saved correctly
|
||||
- [ ] Files committed to repo
|
||||
- [ ] Push to GitHub works
|
||||
- [ ] Repository manifest updated
|
||||
- [ ] README generated/updated
|
||||
- [ ] Handles existing components (update)
|
||||
- [ ] Version auto-increment works
|
||||
- [ ] Error messages helpful
|
||||
|
||||
## Dependencies
|
||||
|
||||
- COMP-001 (Prefab System Refactoring)
|
||||
- GIT-001 (GitHub OAuth) - for repository access
|
||||
|
||||
## Blocked By
|
||||
|
||||
- COMP-001
|
||||
- GIT-001
|
||||
|
||||
## Blocks
|
||||
|
||||
- COMP-004 (Organization Components)
|
||||
- COMP-005 (Component Import with Version Control)
|
||||
|
||||
## Estimated Effort
|
||||
|
||||
- Export service: 4-5 hours
|
||||
- Repository management: 3-4 hours
|
||||
- Export modal: 4-5 hours
|
||||
- Git integration: 3-4 hours
|
||||
- Context menu: 2-3 hours
|
||||
- Testing & polish: 3-4 hours
|
||||
- **Total: 19-25 hours**
|
||||
|
||||
## Success Criteria
|
||||
|
||||
1. Components can be exported via right-click
|
||||
2. Dependencies are automatically included
|
||||
3. Repository structure is consistent
|
||||
4. Manifests are generated correctly
|
||||
5. Git operations work smoothly
|
||||
6. Components are importable via COMP-004+
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- Export to npm package
|
||||
- Export to Noodl marketplace
|
||||
- Batch export multiple components
|
||||
- Export templates/starters
|
||||
- Preview component before export
|
||||
- Export history/versioning
|
||||
@@ -0,0 +1,396 @@
|
||||
# COMP-004: Organization Components Repository
|
||||
|
||||
## Overview
|
||||
|
||||
Enable teams to share a central component repository at the organization level. When a user belongs to a GitHub organization, they can access shared components from that org's component repository, creating a design system that's consistent across all team projects.
|
||||
|
||||
## Context
|
||||
|
||||
Individual developers can export components to personal repos (COMP-003), but teams need:
|
||||
- Shared component library accessible to all org members
|
||||
- Consistent design system across projects
|
||||
- Centralized component governance
|
||||
- Version control for team components
|
||||
|
||||
This task adds organization-level component repositories to the prefab source system.
|
||||
|
||||
### Organization Flow
|
||||
|
||||
```
|
||||
User authenticates with GitHub (GIT-001)
|
||||
↓
|
||||
System detects user's organizations
|
||||
↓
|
||||
For each org, check for `noodl-components` repo
|
||||
↓
|
||||
Register as prefab source if found
|
||||
↓
|
||||
Components appear in NodePicker
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
### Functional Requirements
|
||||
|
||||
1. **Organization Detection**
|
||||
- Detect user's GitHub organizations
|
||||
- Check for component repository in each org
|
||||
- Support custom repo names (configurable)
|
||||
- Handle multiple organizations
|
||||
|
||||
2. **Repository Discovery**
|
||||
- Auto-detect `{org}/noodl-components` repos
|
||||
- Validate repository structure
|
||||
- Read repository manifest
|
||||
- Cache organization components
|
||||
|
||||
3. **Component Access**
|
||||
- List org components in NodePicker
|
||||
- Show org badge on components
|
||||
- Filter by organization
|
||||
- Search across all org repos
|
||||
|
||||
4. **Permission Handling**
|
||||
- Respect GitHub permissions
|
||||
- Handle private repositories
|
||||
- Clear error messages for access issues
|
||||
- Re-auth prompt when needed
|
||||
|
||||
5. **Organization Settings**
|
||||
- Enable/disable specific org repos
|
||||
- Priority ordering between orgs
|
||||
- Refresh/sync controls
|
||||
- View org repo on GitHub
|
||||
|
||||
### Non-Functional Requirements
|
||||
|
||||
- Org components load within 3 seconds
|
||||
- Cached for offline use after first load
|
||||
- Handles orgs with 100+ components
|
||||
- Works with GitHub Enterprise (future)
|
||||
|
||||
## Technical Approach
|
||||
|
||||
### 1. Organization Prefab Source
|
||||
|
||||
```typescript
|
||||
// packages/noodl-editor/src/editor/src/models/prefab/sources/OrganizationPrefabSource.ts
|
||||
|
||||
interface OrganizationConfig {
|
||||
orgName: string;
|
||||
repoName: string;
|
||||
enabled: boolean;
|
||||
priority: number;
|
||||
}
|
||||
|
||||
class OrganizationPrefabSource implements PrefabSource {
|
||||
config: PrefabSourceConfig;
|
||||
|
||||
constructor(private orgConfig: OrganizationConfig) {
|
||||
this.config = {
|
||||
id: `org:${orgConfig.orgName}`,
|
||||
name: orgConfig.orgName,
|
||||
priority: orgConfig.priority,
|
||||
enabled: orgConfig.enabled
|
||||
};
|
||||
}
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
// Verify repo access
|
||||
const hasAccess = await this.verifyRepoAccess();
|
||||
if (!hasAccess) {
|
||||
throw new PrefabSourceError('No access to organization repository');
|
||||
}
|
||||
|
||||
// Load manifest
|
||||
await this.loadManifest();
|
||||
}
|
||||
|
||||
async listPrefabs(): Promise<PrefabMetadata[]> {
|
||||
const manifest = await this.getManifest();
|
||||
return manifest.components.map(c => ({
|
||||
...c,
|
||||
id: `org:${this.orgConfig.orgName}:${c.id}`,
|
||||
source: 'organization',
|
||||
organization: this.orgConfig.orgName
|
||||
}));
|
||||
}
|
||||
|
||||
async downloadPrefab(id: string): Promise<string> {
|
||||
// Clone specific component from repo
|
||||
const componentPath = this.getComponentPath(id);
|
||||
return await this.downloadFromGitHub(componentPath);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Organization Discovery Service
|
||||
|
||||
```typescript
|
||||
// packages/noodl-editor/src/editor/src/services/OrganizationService.ts
|
||||
|
||||
interface Organization {
|
||||
name: string;
|
||||
displayName: string;
|
||||
avatarUrl: string;
|
||||
hasComponentRepo: boolean;
|
||||
componentRepoUrl?: string;
|
||||
memberCount?: number;
|
||||
}
|
||||
|
||||
class OrganizationService {
|
||||
private static instance: OrganizationService;
|
||||
|
||||
// Discovery
|
||||
async discoverOrganizations(): Promise<Organization[]>;
|
||||
async checkForComponentRepo(orgName: string): Promise<boolean>;
|
||||
async validateComponentRepo(orgName: string, repoName: string): Promise<boolean>;
|
||||
|
||||
// Registration
|
||||
async registerOrgSource(org: Organization): Promise<void>;
|
||||
async unregisterOrgSource(orgName: string): Promise<void>;
|
||||
|
||||
// Settings
|
||||
getOrgSettings(orgName: string): OrganizationConfig;
|
||||
updateOrgSettings(orgName: string, settings: Partial<OrganizationConfig>): void;
|
||||
|
||||
// Refresh
|
||||
async refreshOrgComponents(orgName: string): Promise<void>;
|
||||
async refreshAllOrgs(): Promise<void>;
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Auto-Registration on Login
|
||||
|
||||
```typescript
|
||||
// Integration with GitHub OAuth
|
||||
|
||||
async function onGitHubAuthenticated(token: string): Promise<void> {
|
||||
const orgService = OrganizationService.instance;
|
||||
const registry = PrefabRegistry.instance;
|
||||
|
||||
// Discover user's organizations
|
||||
const orgs = await orgService.discoverOrganizations();
|
||||
|
||||
for (const org of orgs) {
|
||||
// Check for component repo
|
||||
const hasRepo = await orgService.checkForComponentRepo(org.name);
|
||||
|
||||
if (hasRepo) {
|
||||
// Register as prefab source
|
||||
const source = new OrganizationPrefabSource({
|
||||
orgName: org.name,
|
||||
repoName: 'noodl-components',
|
||||
enabled: true,
|
||||
priority: 80 // Below built-in, above docs
|
||||
});
|
||||
|
||||
registry.registerSource(source);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Organization Settings UI
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ Organization Components │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Connected Organizations │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ [🏢] Acme Corp │ │
|
||||
│ │ noodl-components • 24 components • Last synced: 2h ago │ │
|
||||
│ │ [☑ Enabled] [⚙️ Settings] [🔄 Sync] [↗️ View on GitHub] │ │
|
||||
│ ├─────────────────────────────────────────────────────────────────┤ │
|
||||
│ │ [🏢] StartupXYZ │ │
|
||||
│ │ noodl-components • 8 components • Last synced: 1d ago │ │
|
||||
│ │ [☑ Enabled] [⚙️ Settings] [🔄 Sync] [↗️ View on GitHub] │ │
|
||||
│ ├─────────────────────────────────────────────────────────────────┤ │
|
||||
│ │ [🏢] OpenSource Collective │ │
|
||||
│ │ ⚠️ No component repository found │ │
|
||||
│ │ [Create Repository] │ │
|
||||
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ [🔄 Refresh Organizations] │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 5. NodePicker Integration
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ Prefabs │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ 🔍 Search prefabs... │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ Source: [All Sources ▾] Category: [All ▾] │
|
||||
│ • All Sources │
|
||||
│ • Built-in │
|
||||
│ • Acme Corp │
|
||||
│ • StartupXYZ │
|
||||
│ • Community │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ACME CORP │
|
||||
│ ┌────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ 🏢 AcmeButton v2.1.0 [Clone] │ │
|
||||
│ │ Standard button following Acme design system │ │
|
||||
│ ├────────────────────────────────────────────────────────────────┤ │
|
||||
│ │ 🏢 AcmeCard v1.3.0 [Clone] │ │
|
||||
│ │ Card component with Acme styling │ │
|
||||
│ └────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ BUILT-IN │
|
||||
│ ┌────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ 📦 Form Input v1.0.0 [Clone] │ │
|
||||
│ │ Standard form input with validation │ │
|
||||
│ └────────────────────────────────────────────────────────────────┘ │
|
||||
```
|
||||
|
||||
## Files to Create
|
||||
|
||||
1. `packages/noodl-editor/src/editor/src/models/prefab/sources/OrganizationPrefabSource.ts`
|
||||
2. `packages/noodl-editor/src/editor/src/services/OrganizationService.ts`
|
||||
3. `packages/noodl-core-ui/src/components/settings/OrganizationSettings/OrganizationSettings.tsx`
|
||||
4. `packages/noodl-core-ui/src/components/settings/OrganizationSettings/OrgCard.tsx`
|
||||
5. `packages/noodl-core-ui/src/preview/launcher/Launcher/views/OrganizationsView.tsx`
|
||||
|
||||
## Files to Modify
|
||||
|
||||
1. `packages/noodl-editor/src/editor/src/services/GitHubOAuthService.ts`
|
||||
- Trigger org discovery on auth
|
||||
|
||||
2. `packages/noodl-editor/src/editor/src/models/prefab/PrefabRegistry.ts`
|
||||
- Handle org sources dynamically
|
||||
- Add source filtering
|
||||
|
||||
3. `packages/noodl-editor/src/editor/src/views/NodePicker/tabs/NodePickerSearchView/NodePickerSearchView.tsx`
|
||||
- Add source filter dropdown
|
||||
- Show org badges
|
||||
|
||||
4. `packages/noodl-editor/src/editor/src/views/NodePicker/components/ModuleCard/ModuleCard.tsx`
|
||||
- Show organization name
|
||||
- Different styling for org components
|
||||
|
||||
5. `packages/noodl-core-ui/src/preview/launcher/Launcher/Launcher.tsx`
|
||||
- Add Organizations section/page
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### Phase 1: Organization Discovery
|
||||
1. Create OrganizationService
|
||||
2. Implement GitHub org listing
|
||||
3. Check for component repos
|
||||
4. Store org data
|
||||
|
||||
### Phase 2: Organization Source
|
||||
1. Create OrganizationPrefabSource
|
||||
2. Implement manifest loading
|
||||
3. Implement component downloading
|
||||
4. Add to PrefabRegistry
|
||||
|
||||
### Phase 3: Auto-Registration
|
||||
1. Hook into OAuth flow
|
||||
2. Auto-register on login
|
||||
3. Handle permission changes
|
||||
4. Persist org settings
|
||||
|
||||
### Phase 4: Settings UI
|
||||
1. Create OrganizationSettings component
|
||||
2. Create OrgCard component
|
||||
3. Add to Settings panel
|
||||
4. Implement enable/disable
|
||||
|
||||
### Phase 5: NodePicker Integration
|
||||
1. Add source filter
|
||||
2. Show org grouping
|
||||
3. Add org badges
|
||||
4. Update search
|
||||
|
||||
### Phase 6: Polish
|
||||
1. Sync/refresh functionality
|
||||
2. Error handling
|
||||
3. Offline support
|
||||
4. Performance optimization
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
- [ ] Organizations discovered on login
|
||||
- [ ] Component repos detected
|
||||
- [ ] Source registered for orgs with repos
|
||||
- [ ] Components appear in NodePicker
|
||||
- [ ] Source filter works
|
||||
- [ ] Org badge displays
|
||||
- [ ] Enable/disable works
|
||||
- [ ] Sync refreshes components
|
||||
- [ ] Private repos accessible
|
||||
- [ ] Permission errors handled
|
||||
- [ ] Works with multiple orgs
|
||||
- [ ] Caching works offline
|
||||
- [ ] Settings persist
|
||||
|
||||
## Dependencies
|
||||
|
||||
- COMP-001 (Prefab System Refactoring)
|
||||
- COMP-003 (Component Export) - for repository structure
|
||||
- GIT-001 (GitHub OAuth) - for organization access
|
||||
|
||||
## Blocked By
|
||||
|
||||
- COMP-001
|
||||
- GIT-001
|
||||
|
||||
## Blocks
|
||||
|
||||
- COMP-005 (depends on org repos existing)
|
||||
- COMP-006 (depends on org repos existing)
|
||||
|
||||
## Estimated Effort
|
||||
|
||||
- Organization discovery: 3-4 hours
|
||||
- OrganizationPrefabSource: 4-5 hours
|
||||
- Auto-registration: 2-3 hours
|
||||
- Settings UI: 3-4 hours
|
||||
- NodePicker integration: 3-4 hours
|
||||
- Polish & testing: 3-4 hours
|
||||
- **Total: 18-24 hours**
|
||||
|
||||
## Success Criteria
|
||||
|
||||
1. Orgs auto-detected on GitHub login
|
||||
2. Component repos discovered automatically
|
||||
3. Org components appear in NodePicker
|
||||
4. Can filter by organization
|
||||
5. Settings allow enable/disable
|
||||
6. Works with private repositories
|
||||
7. Clear error messages for access issues
|
||||
|
||||
## Repository Setup Guide (For Users)
|
||||
|
||||
To create an organization component repository:
|
||||
|
||||
1. Create repo named `noodl-components` in your org
|
||||
2. Add `index.json` manifest file:
|
||||
```json
|
||||
{
|
||||
"name": "Acme Components",
|
||||
"version": "1.0.0",
|
||||
"components": []
|
||||
}
|
||||
```
|
||||
3. Export components using COMP-003
|
||||
4. Noodl will auto-detect the repository
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- GitHub Enterprise support
|
||||
- Repository templates
|
||||
- Permission levels (read/write per component)
|
||||
- Component approval workflow
|
||||
- Usage analytics per org
|
||||
- Component deprecation notices
|
||||
- Multi-repo per org support
|
||||
@@ -0,0 +1,414 @@
|
||||
# COMP-005: Component Import with Version Control
|
||||
|
||||
## Overview
|
||||
|
||||
Track the source and version of imported components, enabling update notifications, selective updates, and clear understanding of component provenance. When a component is imported from a repository, remember where it came from and notify users when updates are available.
|
||||
|
||||
## Context
|
||||
|
||||
Currently, imported components lose connection to their source:
|
||||
- No tracking of where component came from
|
||||
- No awareness of available updates
|
||||
- No way to re-sync with source
|
||||
- Manual process to check for new versions
|
||||
|
||||
This task adds version tracking and update management:
|
||||
- Track component source (built-in, org, personal, docs)
|
||||
- Store version information
|
||||
- Check for updates periodically
|
||||
- Enable selective component updates
|
||||
|
||||
### Import Flow Today
|
||||
|
||||
```
|
||||
User clicks "Clone" → Component imported → No source tracking
|
||||
```
|
||||
|
||||
### Import Flow After This Task
|
||||
|
||||
```
|
||||
User clicks "Clone" → Component imported → Source/version tracked
|
||||
↓
|
||||
Background: Check for updates periodically
|
||||
↓
|
||||
Notification: "2 components have updates available"
|
||||
↓
|
||||
User reviews and selects updates
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
### Functional Requirements
|
||||
|
||||
1. **Source Tracking**
|
||||
- Record source repository/location for each import
|
||||
- Store version at time of import
|
||||
- Track import timestamp
|
||||
- Handle components without source (legacy)
|
||||
|
||||
2. **Version Information**
|
||||
- Display current version in component panel
|
||||
- Show source badge (Built-in, Org name, etc.)
|
||||
- Link to source documentation
|
||||
- View changelog
|
||||
|
||||
3. **Update Detection**
|
||||
- Background check for available updates
|
||||
- Badge/indicator for components with updates
|
||||
- List all updatable components
|
||||
- Compare current vs available version
|
||||
|
||||
4. **Update Process**
|
||||
- Preview what changes in update
|
||||
- Selective update (choose which to update)
|
||||
- Backup current before update
|
||||
- Rollback option if update fails
|
||||
|
||||
5. **Import Metadata Storage**
|
||||
- Store in project metadata
|
||||
- Survive project export/import
|
||||
- Handle renamed components
|
||||
|
||||
### Non-Functional Requirements
|
||||
|
||||
- Update check < 5 seconds
|
||||
- No performance impact on project load
|
||||
- Works offline (shows cached status)
|
||||
- Handles 100+ tracked components
|
||||
|
||||
## Technical Approach
|
||||
|
||||
### 1. Import Metadata Schema
|
||||
|
||||
```typescript
|
||||
// Stored in project.json metadata
|
||||
interface ComponentImportMetadata {
|
||||
components: ImportedComponent[];
|
||||
lastUpdateCheck: string; // ISO timestamp
|
||||
}
|
||||
|
||||
interface ImportedComponent {
|
||||
componentId: string; // Internal Noodl component ID
|
||||
componentName: string; // Display name at import time
|
||||
source: ComponentSource;
|
||||
importedVersion: string;
|
||||
importedAt: string; // ISO timestamp
|
||||
lastUpdatedAt?: string; // When user last updated
|
||||
updateAvailable?: string; // Available version if any
|
||||
checksum?: string; // For detecting local modifications
|
||||
}
|
||||
|
||||
interface ComponentSource {
|
||||
type: 'builtin' | 'organization' | 'personal' | 'docs' | 'unknown';
|
||||
repository?: string; // GitHub repo URL
|
||||
organization?: string; // Org name if type is 'organization'
|
||||
prefabId: string; // ID in source manifest
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Import Tracking Service
|
||||
|
||||
```typescript
|
||||
// packages/noodl-editor/src/editor/src/services/ComponentTrackingService.ts
|
||||
|
||||
class ComponentTrackingService {
|
||||
private static instance: ComponentTrackingService;
|
||||
|
||||
// On import
|
||||
async trackImport(
|
||||
componentId: string,
|
||||
source: ComponentSource,
|
||||
version: string
|
||||
): Promise<void>;
|
||||
|
||||
// Queries
|
||||
getImportedComponents(): ImportedComponent[];
|
||||
getComponentSource(componentId: string): ComponentSource | null;
|
||||
getComponentsWithUpdates(): ImportedComponent[];
|
||||
|
||||
// Update checking
|
||||
async checkForUpdates(): Promise<UpdateCheckResult>;
|
||||
async checkComponentUpdate(componentId: string): Promise<UpdateInfo | null>;
|
||||
|
||||
// Update application
|
||||
async updateComponent(componentId: string): Promise<UpdateResult>;
|
||||
async updateAllComponents(componentIds: string[]): Promise<UpdateResult[]>;
|
||||
async rollbackUpdate(componentId: string): Promise<void>;
|
||||
|
||||
// Metadata
|
||||
async saveMetadata(): Promise<void>;
|
||||
async loadMetadata(): Promise<void>;
|
||||
}
|
||||
|
||||
interface UpdateCheckResult {
|
||||
checked: number;
|
||||
updatesAvailable: number;
|
||||
components: {
|
||||
componentId: string;
|
||||
currentVersion: string;
|
||||
availableVersion: string;
|
||||
changelogUrl?: string;
|
||||
}[];
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Update Check Process
|
||||
|
||||
```typescript
|
||||
async checkForUpdates(): Promise<UpdateCheckResult> {
|
||||
const imported = this.getImportedComponents();
|
||||
const result: UpdateCheckResult = {
|
||||
checked: 0,
|
||||
updatesAvailable: 0,
|
||||
components: []
|
||||
};
|
||||
|
||||
// Group by source for efficient checking
|
||||
const bySource = groupBy(imported, c => c.source.repository);
|
||||
|
||||
for (const [repo, components] of Object.entries(bySource)) {
|
||||
const source = PrefabRegistry.instance.getSource(repo);
|
||||
if (!source) continue;
|
||||
|
||||
// Fetch latest manifest
|
||||
const manifest = await source.getManifest();
|
||||
|
||||
for (const component of components) {
|
||||
result.checked++;
|
||||
|
||||
const latest = manifest.components.find(
|
||||
c => c.id === component.source.prefabId
|
||||
);
|
||||
|
||||
if (latest && semver.gt(latest.version, component.importedVersion)) {
|
||||
result.updatesAvailable++;
|
||||
result.components.push({
|
||||
componentId: component.componentId,
|
||||
currentVersion: component.importedVersion,
|
||||
availableVersion: latest.version,
|
||||
changelogUrl: latest.changelog
|
||||
});
|
||||
|
||||
// Update metadata
|
||||
component.updateAvailable = latest.version;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await this.saveMetadata();
|
||||
return result;
|
||||
}
|
||||
```
|
||||
|
||||
### 4. UI Components
|
||||
|
||||
#### Component Panel Badge
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ Components │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ ├── Pages │
|
||||
│ │ └── HomePage │
|
||||
│ │ └── LoginPage │
|
||||
│ ├── Components │
|
||||
│ │ └── AcmeButton [🏢 v2.1.0] [⬆️ Update] │
|
||||
│ │ └── AcmeCard [🏢 v1.3.0] │
|
||||
│ │ └── MyCustomButton │
|
||||
│ │ └── FormInput [📦 v1.0.0] │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
#### Update Available Notification
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ 🔔 Component Updates Available │
|
||||
│ │
|
||||
│ 2 components have updates available from your organization. │
|
||||
│ │
|
||||
│ [View Updates] [Remind Me Later] │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
#### Update Modal
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ Component Updates [×] │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Available Updates │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ ☑ AcmeButton │ │
|
||||
│ │ Current: v2.1.0 → Available: v2.2.0 │ │
|
||||
│ │ Source: Acme Corp │ │
|
||||
│ │ Changes: Added loading state, fixed hover color │ │
|
||||
│ │ [View Full Changelog] │ │
|
||||
│ ├─────────────────────────────────────────────────────────────────┤ │
|
||||
│ │ ☑ AcmeCard │ │
|
||||
│ │ Current: v1.3.0 → Available: v1.4.0 │ │
|
||||
│ │ Source: Acme Corp │ │
|
||||
│ │ Changes: Added shadow variants │ │
|
||||
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ⚠️ Updates will replace your imported components. Local │
|
||||
│ modifications may be lost. │
|
||||
│ │
|
||||
│ [Cancel] [Update Selected (2)] │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 5. Integration Points
|
||||
|
||||
```typescript
|
||||
// Hook into existing import flow
|
||||
// packages/noodl-editor/src/editor/src/models/modulelibrarymodel.ts
|
||||
|
||||
async installPrefab(prefabId: string, options?: InstallOptions): Promise<void> {
|
||||
// ... existing import logic ...
|
||||
|
||||
// After successful import, track it
|
||||
const source = this.detectSource(prefabId);
|
||||
const version = await this.getPrefabVersion(prefabId);
|
||||
|
||||
for (const componentId of importedComponentIds) {
|
||||
await ComponentTrackingService.instance.trackImport(
|
||||
componentId,
|
||||
source,
|
||||
version
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Files to Create
|
||||
|
||||
1. `packages/noodl-editor/src/editor/src/services/ComponentTrackingService.ts`
|
||||
2. `packages/noodl-core-ui/src/components/common/ComponentSourceBadge/ComponentSourceBadge.tsx`
|
||||
3. `packages/noodl-core-ui/src/components/modals/ComponentUpdatesModal/ComponentUpdatesModal.tsx`
|
||||
4. `packages/noodl-core-ui/src/components/modals/ComponentUpdatesModal/UpdateItem.tsx`
|
||||
5. `packages/noodl-core-ui/src/components/notifications/UpdateAvailableToast/UpdateAvailableToast.tsx`
|
||||
|
||||
## Files to Modify
|
||||
|
||||
1. `packages/noodl-editor/src/editor/src/models/modulelibrarymodel.ts`
|
||||
- Track imports after install
|
||||
- Add version detection
|
||||
|
||||
2. `packages/noodl-editor/src/editor/src/views/panels/componentspanel.tsx`
|
||||
- Show source badge
|
||||
- Show update indicator
|
||||
- Add "Check for Updates" action
|
||||
|
||||
3. `packages/noodl-editor/src/editor/src/models/projectmodel.ts`
|
||||
- Store/load import metadata
|
||||
- Add to project.json
|
||||
|
||||
4. `packages/noodl-editor/src/editor/src/pages/EditorPage/EditorPage.tsx`
|
||||
- Periodic update check
|
||||
- Show update notification
|
||||
|
||||
5. `packages/noodl-editor/src/editor/src/utils/projectimporter.js`
|
||||
- Return component IDs after import
|
||||
- Support update (re-import)
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### Phase 1: Tracking Infrastructure
|
||||
1. Create ComponentTrackingService
|
||||
2. Define metadata schema
|
||||
3. Add to project.json structure
|
||||
4. Implement track/load/save
|
||||
|
||||
### Phase 2: Import Integration
|
||||
1. Hook into installPrefab
|
||||
2. Extract version from manifest
|
||||
3. Track after successful import
|
||||
4. Handle import errors
|
||||
|
||||
### Phase 3: Update Checking
|
||||
1. Implement checkForUpdates
|
||||
2. Compare versions (semver)
|
||||
3. Store update availability
|
||||
4. Background check timer
|
||||
|
||||
### Phase 4: UI - Badges & Indicators
|
||||
1. Create ComponentSourceBadge
|
||||
2. Add to component panel
|
||||
3. Show update indicator
|
||||
4. Add "Check for Updates" button
|
||||
|
||||
### Phase 5: UI - Update Modal
|
||||
1. Create ComponentUpdatesModal
|
||||
2. Show changelog summaries
|
||||
3. Selective update checkboxes
|
||||
4. Implement update action
|
||||
|
||||
### Phase 6: Update Application
|
||||
1. Backup current component
|
||||
2. Re-import from source
|
||||
3. Update metadata
|
||||
4. Handle errors/rollback
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
- [ ] Import tracks source correctly
|
||||
- [ ] Version stored in metadata
|
||||
- [ ] Badge shows in component panel
|
||||
- [ ] Update check finds updates
|
||||
- [ ] Notification appears when updates available
|
||||
- [ ] Update modal lists all updates
|
||||
- [ ] Selective update works
|
||||
- [ ] Update replaces component correctly
|
||||
- [ ] Changelog link works
|
||||
- [ ] Rollback restores previous
|
||||
- [ ] Works with built-in prefabs
|
||||
- [ ] Works with org prefabs
|
||||
- [ ] Legacy imports show "unknown" source
|
||||
- [ ] Offline shows cached status
|
||||
|
||||
## Dependencies
|
||||
|
||||
- COMP-001 (Prefab System Refactoring)
|
||||
- COMP-002 (Built-in Prefabs) - for version tracking
|
||||
- COMP-004 (Organization Components) - for org tracking
|
||||
|
||||
## Blocked By
|
||||
|
||||
- COMP-001
|
||||
- COMP-002
|
||||
|
||||
## Blocks
|
||||
|
||||
- COMP-006 (extends tracking for forking)
|
||||
|
||||
## Estimated Effort
|
||||
|
||||
- Tracking service: 4-5 hours
|
||||
- Import integration: 3-4 hours
|
||||
- Update checking: 3-4 hours
|
||||
- UI badges/indicators: 3-4 hours
|
||||
- Update modal: 3-4 hours
|
||||
- Update application: 3-4 hours
|
||||
- **Total: 19-25 hours**
|
||||
|
||||
## Success Criteria
|
||||
|
||||
1. Imported components track their source
|
||||
2. Version visible in component panel
|
||||
3. Updates detected automatically
|
||||
4. Users notified of available updates
|
||||
5. Selective update works smoothly
|
||||
6. Update preserves project integrity
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- Auto-update option (for trusted sources)
|
||||
- Diff view before update
|
||||
- Local modification detection
|
||||
- Update scheduling
|
||||
- Update history
|
||||
- Component dependency updates
|
||||
- Breaking change warnings
|
||||
@@ -0,0 +1,498 @@
|
||||
# COMP-006: Component Forking & PR Workflow
|
||||
|
||||
## Overview
|
||||
|
||||
Enable users to fork imported components, make modifications, and contribute changes back to the source repository via pull requests. This creates a collaborative component ecosystem where improvements can flow back to the team or community.
|
||||
|
||||
## Context
|
||||
|
||||
With COMP-005, users can import components and track their source. But when they need to modify a component:
|
||||
- Modifications are local only
|
||||
- No way to share improvements back
|
||||
- No way to propose changes to org components
|
||||
- Forked components lose connection to source
|
||||
|
||||
This task enables:
|
||||
- Fork components with upstream tracking
|
||||
- Local modifications tracked separately
|
||||
- Contribute changes via PR workflow
|
||||
- Merge upstream updates into forked components
|
||||
|
||||
### Forking Flow
|
||||
|
||||
```
|
||||
Import component (COMP-005)
|
||||
↓
|
||||
User modifies component
|
||||
↓
|
||||
System detects local modifications ("forked")
|
||||
↓
|
||||
User can:
|
||||
- Submit PR to upstream
|
||||
- Merge upstream updates into fork
|
||||
- Revert to upstream version
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
### Functional Requirements
|
||||
|
||||
1. **Fork Detection**
|
||||
- Detect when imported component is modified
|
||||
- Mark as "forked" in tracking metadata
|
||||
- Track original vs modified state
|
||||
- Calculate diff from upstream
|
||||
|
||||
2. **Fork Management**
|
||||
- View fork status in component panel
|
||||
- See what changed from upstream
|
||||
- Option to "unfork" (reset to upstream)
|
||||
- Maintain fork while pulling upstream updates
|
||||
|
||||
3. **PR Creation**
|
||||
- "Contribute Back" action on forked components
|
||||
- Opens PR creation flow
|
||||
- Exports component changes
|
||||
- Creates branch in upstream repo
|
||||
- Opens GitHub PR interface
|
||||
|
||||
4. **Upstream Sync**
|
||||
- Pull upstream changes into fork
|
||||
- Merge or rebase local changes
|
||||
- Conflict detection
|
||||
- Selective merge (choose what to pull)
|
||||
|
||||
5. **Visual Indicators**
|
||||
- "Forked" badge on modified components
|
||||
- "Modified from v2.1.0" indicator
|
||||
- Diff count ("3 changes")
|
||||
- PR status if submitted
|
||||
|
||||
### Non-Functional Requirements
|
||||
|
||||
- Fork detection < 1 second
|
||||
- Diff calculation < 3 seconds
|
||||
- Works with large components (100+ nodes)
|
||||
- No performance impact on editing
|
||||
|
||||
## Technical Approach
|
||||
|
||||
### 1. Fork Tracking Extension
|
||||
|
||||
```typescript
|
||||
// Extension to COMP-005 ImportedComponent
|
||||
interface ImportedComponent {
|
||||
// ... existing fields ...
|
||||
|
||||
// Fork tracking
|
||||
isFork: boolean;
|
||||
forkStatus?: ForkStatus;
|
||||
originalChecksum?: string; // Checksum at import time
|
||||
currentChecksum?: string; // Checksum of current state
|
||||
upstreamVersion?: string; // Latest upstream version
|
||||
|
||||
// PR tracking
|
||||
activePR?: {
|
||||
number: number;
|
||||
url: string;
|
||||
status: 'open' | 'merged' | 'closed';
|
||||
branch: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface ForkStatus {
|
||||
changesCount: number;
|
||||
lastModified: string;
|
||||
canMergeUpstream: boolean;
|
||||
hasConflicts: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Fork Detection Service
|
||||
|
||||
```typescript
|
||||
// packages/noodl-editor/src/editor/src/services/ComponentForkService.ts
|
||||
|
||||
class ComponentForkService {
|
||||
private static instance: ComponentForkService;
|
||||
|
||||
// Fork detection
|
||||
async detectForks(): Promise<ForkDetectionResult>;
|
||||
async isComponentForked(componentId: string): Promise<boolean>;
|
||||
async calculateDiff(componentId: string): Promise<ComponentDiff>;
|
||||
|
||||
// Fork management
|
||||
async markAsForked(componentId: string): Promise<void>;
|
||||
async unfork(componentId: string): Promise<void>; // Reset to upstream
|
||||
|
||||
// Upstream sync
|
||||
async canMergeUpstream(componentId: string): Promise<MergeCheck>;
|
||||
async mergeUpstream(componentId: string): Promise<MergeResult>;
|
||||
async previewMerge(componentId: string): Promise<MergePreview>;
|
||||
|
||||
// PR workflow
|
||||
async createContribution(componentId: string): Promise<ContributionResult>;
|
||||
async checkPRStatus(componentId: string): Promise<PRStatus>;
|
||||
|
||||
// Diff/comparison
|
||||
async exportDiff(componentId: string): Promise<ComponentDiff>;
|
||||
async compareWithUpstream(componentId: string): Promise<ComparisonResult>;
|
||||
}
|
||||
|
||||
interface ComponentDiff {
|
||||
componentId: string;
|
||||
changes: Change[];
|
||||
nodesAdded: number;
|
||||
nodesRemoved: number;
|
||||
nodesModified: number;
|
||||
propertiesChanged: number;
|
||||
}
|
||||
|
||||
interface Change {
|
||||
type: 'added' | 'removed' | 'modified';
|
||||
path: string; // Path in component tree
|
||||
description: string;
|
||||
before?: any;
|
||||
after?: any;
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Checksum Calculation
|
||||
|
||||
```typescript
|
||||
// Calculate stable checksum for component state
|
||||
function calculateComponentChecksum(component: ComponentModel): string {
|
||||
// Serialize component in stable order
|
||||
const serialized = stableSerialize({
|
||||
nodes: component.nodes.map(serializeNode),
|
||||
connections: component.connections.map(serializeConnection),
|
||||
properties: component.properties,
|
||||
// Exclude metadata that changes (ids, timestamps)
|
||||
});
|
||||
|
||||
return crypto.createHash('sha256').update(serialized).digest('hex');
|
||||
}
|
||||
|
||||
// Detect if component was modified
|
||||
async function detectModification(componentId: string): Promise<boolean> {
|
||||
const metadata = ComponentTrackingService.instance.getComponentSource(componentId);
|
||||
if (!metadata?.originalChecksum) return false;
|
||||
|
||||
const component = ProjectModel.instance.getComponentWithId(componentId);
|
||||
const currentChecksum = calculateComponentChecksum(component);
|
||||
|
||||
return currentChecksum !== metadata.originalChecksum;
|
||||
}
|
||||
```
|
||||
|
||||
### 4. PR Creation Flow
|
||||
|
||||
```typescript
|
||||
async createContribution(componentId: string): Promise<ContributionResult> {
|
||||
const tracking = ComponentTrackingService.instance;
|
||||
const metadata = tracking.getComponentSource(componentId);
|
||||
|
||||
if (!metadata?.source.repository) {
|
||||
throw new Error('Cannot contribute: no upstream repository');
|
||||
}
|
||||
|
||||
// 1. Export modified component
|
||||
const component = ProjectModel.instance.getComponentWithId(componentId);
|
||||
const exportedFiles = await exportComponent(component);
|
||||
|
||||
// 2. Create branch in upstream repo
|
||||
const branchName = `component-update/${metadata.componentName}-${Date.now()}`;
|
||||
const github = GitHubApiClient.instance;
|
||||
|
||||
await github.createBranch(
|
||||
metadata.source.repository,
|
||||
branchName,
|
||||
'main'
|
||||
);
|
||||
|
||||
// 3. Commit changes to branch
|
||||
await github.commitFiles(
|
||||
metadata.source.repository,
|
||||
branchName,
|
||||
exportedFiles,
|
||||
`Update ${metadata.componentName} component`
|
||||
);
|
||||
|
||||
// 4. Create PR
|
||||
const pr = await github.createPullRequest(
|
||||
metadata.source.repository,
|
||||
{
|
||||
title: `Update ${metadata.componentName} component`,
|
||||
body: generatePRDescription(metadata, exportedFiles),
|
||||
head: branchName,
|
||||
base: 'main'
|
||||
}
|
||||
);
|
||||
|
||||
// 5. Track PR in metadata
|
||||
metadata.activePR = {
|
||||
number: pr.number,
|
||||
url: pr.html_url,
|
||||
status: 'open',
|
||||
branch: branchName
|
||||
};
|
||||
await tracking.saveMetadata();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
prUrl: pr.html_url,
|
||||
prNumber: pr.number
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### 5. UI Components
|
||||
|
||||
#### Fork Badge in Component Panel
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ Components │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ ├── AcmeButton [🏢 v2.1.0] [🔀 Forked +3] │
|
||||
│ │ ├── Right-click options: │
|
||||
│ │ │ • View Changes from Upstream │
|
||||
│ │ │ • Merge Upstream Changes │
|
||||
│ │ │ • Contribute Changes (Create PR) │
|
||||
│ │ │ • Reset to Upstream │
|
||||
│ │ │ ────────────────────── │
|
||||
│ │ │ • PR #42 Open ↗ │
|
||||
│ │ └── │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
#### Diff View Modal
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ Changes in AcmeButton [×] │
|
||||
│ Forked from v2.1.0 │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Summary: 3 nodes modified, 1 added, 0 removed │
|
||||
│ │
|
||||
│ CHANGES │
|
||||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ + Added: LoadingSpinner node │ │
|
||||
│ │ └─ Displays while button action is processing │ │
|
||||
│ ├─────────────────────────────────────────────────────────────────┤ │
|
||||
│ │ ~ Modified: Button/backgroundColor │ │
|
||||
│ │ └─ #3B82F6 → #2563EB (darker blue) │ │
|
||||
│ ├─────────────────────────────────────────────────────────────────┤ │
|
||||
│ │ ~ Modified: Button/borderRadius │ │
|
||||
│ │ └─ 4px → 8px │ │
|
||||
│ ├─────────────────────────────────────────────────────────────────┤ │
|
||||
│ │ ~ Modified: HoverState/scale │ │
|
||||
│ │ └─ 1.02 → 1.05 │ │
|
||||
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ [Reset to Upstream] [Contribute Changes] [Close] │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
#### PR Creation Modal
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ Contribute Changes [×] │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ You're about to create a Pull Request to: │
|
||||
│ 🏢 acme-corp/noodl-components │
|
||||
│ │
|
||||
│ Component: AcmeButton │
|
||||
│ Changes: 3 modifications, 1 addition │
|
||||
│ │
|
||||
│ PR Title: │
|
||||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Update AcmeButton: add loading state, adjust styling │ │
|
||||
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ Description: │
|
||||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ This PR updates the AcmeButton component with: │ │
|
||||
│ │ - Added loading spinner during async actions │ │
|
||||
│ │ - Darker blue for better contrast │ │
|
||||
│ │ - Larger border radius for modern look │ │
|
||||
│ │ - More pronounced hover effect │ │
|
||||
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ☑ Open PR in browser after creation │
|
||||
│ │
|
||||
│ [Cancel] [Create PR] │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 6. Upstream Merge Flow
|
||||
|
||||
```typescript
|
||||
async mergeUpstream(componentId: string): Promise<MergeResult> {
|
||||
const tracking = ComponentTrackingService.instance;
|
||||
const metadata = tracking.getComponentSource(componentId);
|
||||
|
||||
// 1. Get upstream version
|
||||
const source = PrefabRegistry.instance.getSource(metadata.source.repository);
|
||||
const upstreamPath = await source.downloadPrefab(metadata.source.prefabId);
|
||||
|
||||
// 2. Get current component
|
||||
const currentComponent = ProjectModel.instance.getComponentWithId(componentId);
|
||||
|
||||
// 3. Get original version (at import time)
|
||||
const originalPath = await this.getOriginalVersion(componentId);
|
||||
|
||||
// 4. Three-way merge
|
||||
const mergeResult = await mergeComponents(
|
||||
originalPath, // Base
|
||||
upstreamPath, // Theirs (upstream)
|
||||
currentComponent // Ours (local modifications)
|
||||
);
|
||||
|
||||
if (mergeResult.hasConflicts) {
|
||||
// Show conflict resolution UI
|
||||
return { success: false, conflicts: mergeResult.conflicts };
|
||||
}
|
||||
|
||||
// 5. Apply merged result
|
||||
await applyMergedComponent(componentId, mergeResult.merged);
|
||||
|
||||
// 6. Update metadata
|
||||
metadata.importedVersion = upstreamVersion;
|
||||
metadata.originalChecksum = calculateChecksum(mergeResult.merged);
|
||||
await tracking.saveMetadata();
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
```
|
||||
|
||||
## Files to Create
|
||||
|
||||
1. `packages/noodl-editor/src/editor/src/services/ComponentForkService.ts`
|
||||
2. `packages/noodl-editor/src/editor/src/utils/componentChecksum.ts`
|
||||
3. `packages/noodl-editor/src/editor/src/utils/componentMerge.ts`
|
||||
4. `packages/noodl-core-ui/src/components/modals/ComponentDiffModal/ComponentDiffModal.tsx`
|
||||
5. `packages/noodl-core-ui/src/components/modals/CreatePRModal/CreatePRModal.tsx`
|
||||
6. `packages/noodl-core-ui/src/components/modals/MergeUpstreamModal/MergeUpstreamModal.tsx`
|
||||
7. `packages/noodl-core-ui/src/components/common/ForkBadge/ForkBadge.tsx`
|
||||
|
||||
## Files to Modify
|
||||
|
||||
1. `packages/noodl-editor/src/editor/src/services/ComponentTrackingService.ts`
|
||||
- Add fork tracking fields
|
||||
- Add checksum calculation
|
||||
- Integration with ForkService
|
||||
|
||||
2. `packages/noodl-editor/src/editor/src/views/panels/componentspanel.tsx`
|
||||
- Add fork badge
|
||||
- Add fork-related context menu items
|
||||
|
||||
3. `packages/noodl-editor/src/editor/src/services/GitHubApiClient.ts`
|
||||
- Add branch creation
|
||||
- Add file commit
|
||||
- Add PR creation
|
||||
|
||||
4. `packages/noodl-editor/src/editor/src/models/projectmodel.ts`
|
||||
- Hook component save to detect modifications
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### Phase 1: Fork Detection
|
||||
1. Implement checksum calculation
|
||||
2. Store original checksum on import
|
||||
3. Detect modifications on component save
|
||||
4. Mark forked components
|
||||
|
||||
### Phase 2: Diff Calculation
|
||||
1. Implement component diff algorithm
|
||||
2. Create human-readable change descriptions
|
||||
3. Calculate change counts
|
||||
|
||||
### Phase 3: UI - Fork Indicators
|
||||
1. Create ForkBadge component
|
||||
2. Add to component panel
|
||||
3. Add context menu items
|
||||
4. Show fork status
|
||||
|
||||
### Phase 4: UI - Diff View
|
||||
1. Create ComponentDiffModal
|
||||
2. Show changes list
|
||||
3. Add action buttons
|
||||
|
||||
### Phase 5: PR Workflow
|
||||
1. Implement branch creation
|
||||
2. Implement file commit
|
||||
3. Implement PR creation
|
||||
4. Create CreatePRModal
|
||||
|
||||
### Phase 6: Upstream Merge
|
||||
1. Implement three-way merge
|
||||
2. Create MergeUpstreamModal
|
||||
3. Handle conflicts
|
||||
4. Update metadata after merge
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
- [ ] Modification detected correctly
|
||||
- [ ] Fork badge appears
|
||||
- [ ] Diff calculated accurately
|
||||
- [ ] Diff modal shows changes
|
||||
- [ ] PR creation works
|
||||
- [ ] PR opens in browser
|
||||
- [ ] PR status tracked
|
||||
- [ ] Upstream merge works (no conflicts)
|
||||
- [ ] Conflict detection works
|
||||
- [ ] Reset to upstream works
|
||||
- [ ] Multiple forks tracked
|
||||
- [ ] Works with org repos
|
||||
- [ ] Works with personal repos
|
||||
- [ ] Checksum stable across saves
|
||||
|
||||
## Dependencies
|
||||
|
||||
- COMP-003 (Component Export)
|
||||
- COMP-004 (Organization Components)
|
||||
- COMP-005 (Component Import Version Control)
|
||||
- GIT-001 (GitHub OAuth)
|
||||
|
||||
## Blocked By
|
||||
|
||||
- COMP-005
|
||||
|
||||
## Blocks
|
||||
|
||||
- None (final task in COMP series)
|
||||
|
||||
## Estimated Effort
|
||||
|
||||
- Fork detection & checksum: 4-5 hours
|
||||
- Diff calculation: 4-5 hours
|
||||
- Fork UI (badges, menus): 3-4 hours
|
||||
- Diff view modal: 3-4 hours
|
||||
- PR workflow: 5-6 hours
|
||||
- Upstream merge: 5-6 hours
|
||||
- Testing & polish: 4-5 hours
|
||||
- **Total: 28-35 hours**
|
||||
|
||||
## Success Criteria
|
||||
|
||||
1. Modified components detected as forks
|
||||
2. Fork badge visible in UI
|
||||
3. Diff view shows changes clearly
|
||||
4. PR creation works end-to-end
|
||||
5. PR status tracked
|
||||
6. Upstream merge works smoothly
|
||||
7. Conflict handling is clear
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- Visual diff editor
|
||||
- Partial contribution (select changes for PR)
|
||||
- Auto-update after PR merged
|
||||
- Fork from fork (nested forks)
|
||||
- Component version branches
|
||||
- Conflict resolution UI
|
||||
- PR review integration
|
||||
@@ -0,0 +1,339 @@
|
||||
# COMP Series: Shared Component System
|
||||
|
||||
## Overview
|
||||
|
||||
The COMP series transforms Noodl's component sharing from manual zip file exchanges into a modern, Git-based collaborative ecosystem. Teams can share design systems via organization repositories, individuals can build personal component libraries, and improvements can flow back upstream via pull requests.
|
||||
|
||||
## Target Environment
|
||||
|
||||
- **Editor**: React 19 version only
|
||||
- **Runtime**: Not affected (components work in any runtime)
|
||||
- **Backwards Compatibility**: Existing prefabs continue to work
|
||||
|
||||
## Task Dependency Graph
|
||||
|
||||
```
|
||||
COMP-001 (Prefab System Refactoring)
|
||||
│
|
||||
├────────────────────────┬───────────────────────┐
|
||||
↓ ↓ ↓
|
||||
COMP-002 (Built-in) COMP-003 (Export) GIT-001 (OAuth)
|
||||
│ │ │
|
||||
↓ ↓ │
|
||||
│ COMP-004 (Org Components) ←───┘
|
||||
│ │
|
||||
└────────────┬───────────┘
|
||||
↓
|
||||
COMP-005 (Version Control)
|
||||
│
|
||||
↓
|
||||
COMP-006 (Forking & PR)
|
||||
```
|
||||
|
||||
## Task Summary
|
||||
|
||||
| Task ID | Name | Est. Hours | Priority |
|
||||
|---------|------|------------|----------|
|
||||
| COMP-001 | Prefab System Refactoring | 14-20 | Critical |
|
||||
| COMP-002 | Built-in Prefabs | 19-26 | High |
|
||||
| COMP-003 | Component Export to Repository | 19-25 | High |
|
||||
| COMP-004 | Organization Components Repository | 18-24 | High |
|
||||
| COMP-005 | Component Import with Version Control | 19-25 | Medium |
|
||||
| COMP-006 | Component Forking & PR Workflow | 28-35 | Medium |
|
||||
|
||||
**Total Estimated: 117-155 hours**
|
||||
|
||||
## Implementation Order
|
||||
|
||||
### Phase 1: Foundation (Weeks 1-2)
|
||||
1. **COMP-001** - Refactor prefab system for multiple sources
|
||||
|
||||
### Phase 2: Local & Built-in (Weeks 3-4)
|
||||
2. **COMP-002** - Bundle essential prefabs with editor
|
||||
|
||||
### Phase 3: Export & Organization (Weeks 5-7)
|
||||
3. **COMP-003** - Enable exporting to GitHub repositories
|
||||
4. **COMP-004** - Auto-detect and load organization repos
|
||||
|
||||
### Phase 4: Version Control & Collaboration (Weeks 8-10)
|
||||
5. **COMP-005** - Track imports, detect updates
|
||||
6. **COMP-006** - Fork detection, PR workflow
|
||||
|
||||
## Existing Infrastructure
|
||||
|
||||
### ModuleLibraryModel
|
||||
|
||||
```typescript
|
||||
// Current implementation
|
||||
class ModuleLibraryModel {
|
||||
modules: IModule[]; // External libraries
|
||||
prefabs: IModule[]; // Component bundles
|
||||
|
||||
fetchModules(type: 'modules' | 'prefabs'): Promise<IModule[]>;
|
||||
installModule(path: string): Promise<void>;
|
||||
installPrefab(path: string): Promise<void>;
|
||||
}
|
||||
```
|
||||
|
||||
### ProjectImporter
|
||||
|
||||
```typescript
|
||||
// Handles actual component import
|
||||
class ProjectImporter {
|
||||
listComponentsAndDependencies(dir, callback);
|
||||
checkForCollisions(imports, callback);
|
||||
import(dir, imports, callback);
|
||||
}
|
||||
```
|
||||
|
||||
### NodePicker
|
||||
|
||||
```
|
||||
packages/noodl-editor/src/editor/src/views/NodePicker/
|
||||
├── NodePicker.tsx # Main component
|
||||
├── NodePicker.context.tsx # State management
|
||||
├── tabs/
|
||||
│ ├── NodeLibrary/ # Built-in nodes
|
||||
│ ├── NodePickerSearchView/ # Prefabs & modules
|
||||
│ └── ImportFromProject/ # Import from other project
|
||||
└── components/
|
||||
└── ModuleCard/ # Prefab/module display card
|
||||
```
|
||||
|
||||
### Export Functionality
|
||||
|
||||
```typescript
|
||||
// exportProjectComponents.ts
|
||||
export function exportProjectComponents() {
|
||||
// Shows export popup
|
||||
// User selects components
|
||||
// Creates zip file with dependencies
|
||||
}
|
||||
```
|
||||
|
||||
## New Architecture
|
||||
|
||||
### PrefabRegistry (COMP-001)
|
||||
|
||||
Central hub for all prefab sources:
|
||||
|
||||
```typescript
|
||||
class PrefabRegistry {
|
||||
private sources: Map<string, PrefabSource>;
|
||||
|
||||
registerSource(source: PrefabSource): void;
|
||||
getAllPrefabs(): Promise<PrefabMetadata[]>;
|
||||
installPrefab(id: string): Promise<void>;
|
||||
}
|
||||
```
|
||||
|
||||
### Source Types
|
||||
|
||||
| Source | Priority | Description |
|
||||
|--------|----------|-------------|
|
||||
| BuiltInPrefabSource | 100 | Bundled with editor |
|
||||
| OrganizationPrefabSource | 80 | Team component repos |
|
||||
| PersonalPrefabSource | 70 | User's own repos |
|
||||
| DocsPrefabSource | 50 | Community prefabs |
|
||||
|
||||
### Component Tracking (COMP-005)
|
||||
|
||||
```typescript
|
||||
interface ImportedComponent {
|
||||
componentId: string;
|
||||
source: ComponentSource;
|
||||
importedVersion: string;
|
||||
isFork: boolean;
|
||||
updateAvailable?: string;
|
||||
activePR?: PRInfo;
|
||||
}
|
||||
```
|
||||
|
||||
## Repository Structure Convention
|
||||
|
||||
All component repositories follow this structure:
|
||||
|
||||
```
|
||||
noodl-components/
|
||||
├── index.json # Repository manifest
|
||||
├── README.md # Documentation
|
||||
├── LICENSE # License file
|
||||
└── components/
|
||||
├── component-name/
|
||||
│ ├── prefab.json # Component metadata
|
||||
│ ├── component.ndjson # Component data
|
||||
│ ├── dependencies/ # Styles, variants
|
||||
│ └── assets/ # Images, fonts
|
||||
└── ...
|
||||
```
|
||||
|
||||
### Manifest Format (index.json)
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Acme Design System",
|
||||
"version": "2.1.0",
|
||||
"noodlVersion": ">=2.10.0",
|
||||
"components": [
|
||||
{
|
||||
"id": "acme-button",
|
||||
"name": "Acme Button",
|
||||
"version": "2.1.0",
|
||||
"path": "components/acme-button"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Key User Flows
|
||||
|
||||
### 1. Team Member Imports Org Component
|
||||
|
||||
```
|
||||
User opens NodePicker
|
||||
↓
|
||||
Sees "Acme Corp" section with org components
|
||||
↓
|
||||
Clicks "Clone" on AcmeButton
|
||||
↓
|
||||
Component imported, source tracked
|
||||
↓
|
||||
Later: notification "AcmeButton update available"
|
||||
```
|
||||
|
||||
### 2. Developer Shares Component
|
||||
|
||||
```
|
||||
User right-clicks component
|
||||
↓
|
||||
Selects "Export to Repository"
|
||||
↓
|
||||
Chooses personal repo or org repo
|
||||
↓
|
||||
Fills metadata (description, tags)
|
||||
↓
|
||||
Component committed and pushed
|
||||
```
|
||||
|
||||
### 3. Developer Improves Org Component
|
||||
|
||||
```
|
||||
User modifies imported AcmeButton
|
||||
↓
|
||||
System detects fork, shows badge
|
||||
↓
|
||||
User right-clicks → "Contribute Changes"
|
||||
↓
|
||||
PR created in org repo
|
||||
↓
|
||||
Team reviews and merges
|
||||
```
|
||||
|
||||
## Services to Create
|
||||
|
||||
| Service | Purpose |
|
||||
|---------|---------|
|
||||
| PrefabRegistry | Central source management |
|
||||
| ComponentTrackingService | Import/version tracking |
|
||||
| ComponentExportService | Export to repositories |
|
||||
| OrganizationService | Org detection & management |
|
||||
| ComponentForkService | Fork detection & PR workflow |
|
||||
|
||||
## UI Components to Create
|
||||
|
||||
| Component | Location | Purpose |
|
||||
|-----------|----------|---------|
|
||||
| ComponentSourceBadge | noodl-core-ui | Show source (Built-in, Org, etc.) |
|
||||
| ForkBadge | noodl-core-ui | Show fork status |
|
||||
| ExportToRepoModal | noodl-core-ui | Export workflow |
|
||||
| ComponentUpdatesModal | noodl-core-ui | Update selection |
|
||||
| ComponentDiffModal | noodl-core-ui | View changes |
|
||||
| CreatePRModal | noodl-core-ui | PR creation |
|
||||
| OrganizationSettings | noodl-core-ui | Org repo settings |
|
||||
|
||||
## Dependencies on Other Series
|
||||
|
||||
### Required from GIT Series
|
||||
- GIT-001 (GitHub OAuth) - Required for COMP-003, COMP-004
|
||||
|
||||
### Enables for Future
|
||||
- Community marketplace
|
||||
- Component ratings/reviews
|
||||
- Usage analytics
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Unit Tests
|
||||
- Source registration
|
||||
- Metadata parsing
|
||||
- Checksum calculation
|
||||
- Version comparison
|
||||
|
||||
### Integration Tests
|
||||
- Full import flow
|
||||
- Export to repo flow
|
||||
- Update detection
|
||||
- PR creation
|
||||
|
||||
### Manual Testing
|
||||
- Multiple organizations
|
||||
- Large component libraries
|
||||
- Offline scenarios
|
||||
- Permission edge cases
|
||||
|
||||
## Cline Usage Notes
|
||||
|
||||
### Before Starting Each Task
|
||||
|
||||
1. Read task document completely
|
||||
2. Review existing prefab system:
|
||||
- `modulelibrarymodel.ts`
|
||||
- `projectimporter.js`
|
||||
- `NodePicker/` views
|
||||
3. Understand export flow:
|
||||
- `exportProjectComponents.ts`
|
||||
|
||||
### Key Gotchas
|
||||
|
||||
1. **Singleton Pattern**: `ModuleLibraryModel.instance` is used everywhere
|
||||
2. **Async Import**: Import process is callback-based, not Promise
|
||||
3. **Collision Handling**: Existing collision detection must be preserved
|
||||
4. **File Paths**: Components use relative paths internally
|
||||
|
||||
### Testing Prefabs
|
||||
|
||||
```bash
|
||||
# Run editor tests
|
||||
npm run test:editor
|
||||
|
||||
# Manual: Open NodePicker, try importing prefab
|
||||
```
|
||||
|
||||
## Success Criteria (Series Complete)
|
||||
|
||||
1. ✅ Multiple prefab sources supported
|
||||
2. ✅ Built-in prefabs available offline
|
||||
3. ✅ Components exportable to GitHub
|
||||
4. ✅ Organization repos auto-detected
|
||||
5. ✅ Import source/version tracked
|
||||
6. ✅ Updates detected and installable
|
||||
7. ✅ Forks can create PRs upstream
|
||||
|
||||
## Future Work (Post-COMP)
|
||||
|
||||
The COMP series enables:
|
||||
- **Marketplace**: Paid/free component marketplace
|
||||
- **Analytics**: Usage tracking per component
|
||||
- **Ratings**: Community ratings and reviews
|
||||
- **Templates**: Project templates from components
|
||||
- **Subscriptions**: Organization component subscriptions
|
||||
|
||||
## Files in This Series
|
||||
|
||||
- `COMP-001-prefab-system-refactoring.md`
|
||||
- `COMP-002-builtin-prefabs.md`
|
||||
- `COMP-003-component-export.md`
|
||||
- `COMP-004-organization-components.md`
|
||||
- `COMP-005-component-import-version-control.md`
|
||||
- `COMP-006-forking-pr-workflow.md`
|
||||
- `COMP-OVERVIEW.md` (this file)
|
||||
@@ -0,0 +1,481 @@
|
||||
# AI-001: AI Project Scaffolding
|
||||
|
||||
## Overview
|
||||
|
||||
Enable users to describe their project idea in natural language and have AI generate a complete project scaffold with pages, components, data models, and basic styling. This transforms the "blank canvas" experience into an intelligent starting point.
|
||||
|
||||
## Context
|
||||
|
||||
Currently, project creation offers:
|
||||
- Blank "Hello World" template
|
||||
- Pre-built template gallery (limited selection)
|
||||
- Manual component-by-component building
|
||||
|
||||
New users face a steep learning curve:
|
||||
- Don't know where to start
|
||||
- Overwhelmed by node options
|
||||
- No guidance on structure
|
||||
|
||||
AI scaffolding provides:
|
||||
- Describe idea → Get working structure
|
||||
- Industry best practices baked in
|
||||
- Learning through example
|
||||
- Faster time-to-prototype
|
||||
|
||||
### Existing Infrastructure
|
||||
|
||||
From `AiAssistantModel.ts`:
|
||||
```typescript
|
||||
// Existing AI templates
|
||||
docsTemplates = [
|
||||
{ label: 'REST API', template: 'rest' },
|
||||
{ label: 'Form Validation', template: 'function-form-validation' },
|
||||
{ label: 'AI Function', template: 'function' },
|
||||
// ...
|
||||
]
|
||||
```
|
||||
|
||||
From `TemplateRegistry`:
|
||||
```typescript
|
||||
// Download and extract project templates
|
||||
templateRegistry.download({ templateUrl }) → zipPath
|
||||
```
|
||||
|
||||
From `LocalProjectsModel`:
|
||||
```typescript
|
||||
// Create new project from template
|
||||
newProject(callback, { name, path, projectTemplate })
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
### Functional Requirements
|
||||
|
||||
1. **Natural Language Input**
|
||||
- Free-form text description of project
|
||||
- Example prompts for inspiration
|
||||
- Clarifying questions from AI
|
||||
- Refinement through conversation
|
||||
|
||||
2. **Project Analysis**
|
||||
- Identify project type (app, dashboard, form, etc.)
|
||||
- Extract features and functionality
|
||||
- Determine data models needed
|
||||
- Suggest appropriate structure
|
||||
|
||||
3. **Scaffold Generation**
|
||||
- Create page structure
|
||||
- Generate component hierarchy
|
||||
- Set up navigation flow
|
||||
- Create placeholder data models
|
||||
- Apply appropriate styling
|
||||
|
||||
4. **Preview & Refinement**
|
||||
- Preview generated structure before creation
|
||||
- Modify/refine via chat
|
||||
- Accept or regenerate parts
|
||||
- Explain what was generated
|
||||
|
||||
5. **Project Creation**
|
||||
- Create actual Noodl project
|
||||
- Import generated components
|
||||
- Set up routing/navigation
|
||||
- Open in editor
|
||||
|
||||
### Non-Functional Requirements
|
||||
|
||||
- Generation completes in < 30 seconds
|
||||
- Works with Claude API (Anthropic)
|
||||
- Graceful handling of API errors
|
||||
- Clear progress indication
|
||||
- Cost-effective token usage
|
||||
|
||||
## Technical Approach
|
||||
|
||||
### 1. AI Scaffolding Service
|
||||
|
||||
```typescript
|
||||
// packages/noodl-editor/src/editor/src/services/AiScaffoldingService.ts
|
||||
|
||||
interface ProjectDescription {
|
||||
rawText: string;
|
||||
clarifications?: Record<string, string>;
|
||||
}
|
||||
|
||||
interface ScaffoldResult {
|
||||
projectType: ProjectType;
|
||||
pages: PageDefinition[];
|
||||
components: ComponentDefinition[];
|
||||
dataModels: DataModelDefinition[];
|
||||
navigation: NavigationDefinition;
|
||||
styling: StylingDefinition;
|
||||
explanation: string;
|
||||
}
|
||||
|
||||
interface PageDefinition {
|
||||
name: string;
|
||||
route: string;
|
||||
description: string;
|
||||
components: string[]; // Component names used
|
||||
layout: 'stack' | 'grid' | 'sidebar' | 'tabs';
|
||||
}
|
||||
|
||||
interface ComponentDefinition {
|
||||
name: string;
|
||||
type: 'visual' | 'logic' | 'data';
|
||||
description: string;
|
||||
inputs: PortDefinition[];
|
||||
outputs: PortDefinition[];
|
||||
children?: ComponentDefinition[];
|
||||
prefab?: string; // Use existing prefab if available
|
||||
}
|
||||
|
||||
class AiScaffoldingService {
|
||||
private static instance: AiScaffoldingService;
|
||||
|
||||
// Main flow
|
||||
async analyzeDescription(description: string): Promise<AnalysisResult>;
|
||||
async generateScaffold(description: ProjectDescription): Promise<ScaffoldResult>;
|
||||
async refineScaffold(scaffold: ScaffoldResult, feedback: string): Promise<ScaffoldResult>;
|
||||
|
||||
// Project creation
|
||||
async createProject(scaffold: ScaffoldResult, name: string, path: string): Promise<ProjectModel>;
|
||||
|
||||
// Conversation
|
||||
async askClarification(description: string): Promise<ClarificationQuestion[]>;
|
||||
async chat(messages: ChatMessage[]): Promise<ChatResponse>;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Prompt Engineering
|
||||
|
||||
```typescript
|
||||
// packages/noodl-editor/src/editor/src/services/ai/prompts/scaffolding.ts
|
||||
|
||||
const SYSTEM_PROMPT = `You are an expert Noodl application architect.
|
||||
Your task is to analyze project descriptions and generate detailed scaffolds
|
||||
for visual low-code applications.
|
||||
|
||||
Noodl is a visual programming platform with:
|
||||
- Pages (screens/routes)
|
||||
- Components (reusable UI elements)
|
||||
- Nodes (visual programming blocks)
|
||||
- Data models (objects, arrays, variables)
|
||||
- Logic nodes (conditions, loops, functions)
|
||||
|
||||
When generating scaffolds, consider:
|
||||
1. User experience and navigation flow
|
||||
2. Data management and state
|
||||
3. Reusability of components
|
||||
4. Mobile-first responsive design
|
||||
5. Performance and loading states
|
||||
|
||||
Output JSON following the ScaffoldResult schema.`;
|
||||
|
||||
const ANALYSIS_PROMPT = `Analyze this project description and identify:
|
||||
1. Project type (app, dashboard, form, e-commerce, etc.)
|
||||
2. Main features/functionality
|
||||
3. User roles/personas
|
||||
4. Data entities needed
|
||||
5. Key user flows
|
||||
6. Potential complexity areas
|
||||
|
||||
Description: {description}`;
|
||||
|
||||
const SCAFFOLD_PROMPT = `Generate a complete Noodl project scaffold for:
|
||||
|
||||
Project Type: {projectType}
|
||||
Features: {features}
|
||||
Data Models: {dataModels}
|
||||
|
||||
Create:
|
||||
1. Page structure with routes
|
||||
2. Component hierarchy
|
||||
3. Navigation flow
|
||||
4. Data model definitions
|
||||
5. Styling theme
|
||||
|
||||
Use these available prefabs when appropriate:
|
||||
{availablePrefabs}`;
|
||||
```
|
||||
|
||||
### 3. Scaffold to Project Converter
|
||||
|
||||
```typescript
|
||||
// packages/noodl-editor/src/editor/src/services/ai/ScaffoldConverter.ts
|
||||
|
||||
class ScaffoldConverter {
|
||||
// Convert scaffold definitions to actual Noodl components
|
||||
async convertToProject(scaffold: ScaffoldResult): Promise<ProjectFiles> {
|
||||
const project = new ProjectModel();
|
||||
|
||||
// Create pages
|
||||
for (const page of scaffold.pages) {
|
||||
const pageComponent = await this.createPage(page);
|
||||
project.addComponent(pageComponent);
|
||||
}
|
||||
|
||||
// Create reusable components
|
||||
for (const component of scaffold.components) {
|
||||
const comp = await this.createComponent(component);
|
||||
project.addComponent(comp);
|
||||
}
|
||||
|
||||
// Set up navigation
|
||||
await this.setupNavigation(project, scaffold.navigation);
|
||||
|
||||
// Apply styling
|
||||
await this.applyStyles(project, scaffold.styling);
|
||||
|
||||
return project;
|
||||
}
|
||||
|
||||
private async createPage(page: PageDefinition): Promise<ComponentModel> {
|
||||
// Create component with page layout
|
||||
const component = ComponentModel.create({
|
||||
name: page.name,
|
||||
type: 'page'
|
||||
});
|
||||
|
||||
// Add layout container based on page.layout
|
||||
const layout = this.createLayout(page.layout);
|
||||
component.addChild(layout);
|
||||
|
||||
// Add referenced components
|
||||
for (const compName of page.components) {
|
||||
const ref = this.createComponentReference(compName);
|
||||
layout.addChild(ref);
|
||||
}
|
||||
|
||||
return component;
|
||||
}
|
||||
|
||||
private async createComponent(def: ComponentDefinition): Promise<ComponentModel> {
|
||||
// Check if we can use a prefab
|
||||
if (def.prefab) {
|
||||
return await this.importPrefab(def.prefab, def);
|
||||
}
|
||||
|
||||
// Create custom component
|
||||
const component = ComponentModel.create({
|
||||
name: def.name,
|
||||
type: def.type
|
||||
});
|
||||
|
||||
// Add ports
|
||||
for (const input of def.inputs) {
|
||||
component.addInput(input);
|
||||
}
|
||||
for (const output of def.outputs) {
|
||||
component.addOutput(output);
|
||||
}
|
||||
|
||||
// Add children
|
||||
if (def.children) {
|
||||
for (const child of def.children) {
|
||||
const childComp = await this.createComponent(child);
|
||||
component.addChild(childComp);
|
||||
}
|
||||
}
|
||||
|
||||
return component;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. UI Flow
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ Create New Project [×] │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ○ Start from scratch │
|
||||
│ ○ Use a template │
|
||||
│ ● Describe your project (AI) │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Describe what you want to build... │ │
|
||||
│ │ │ │
|
||||
│ │ I want to build a task management app where users can create │ │
|
||||
│ │ projects, add tasks with due dates, and track progress with │ │
|
||||
│ │ a kanban board view. │ │
|
||||
│ │ │ │
|
||||
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ 💡 Examples: │
|
||||
│ • "A recipe app with categories and favorites" │
|
||||
│ • "An e-commerce dashboard with sales charts" │
|
||||
│ • "A booking system for a salon" │
|
||||
│ │
|
||||
│ [Cancel] [Generate Project] │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 5. Preview & Refinement
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ Project Preview [×] │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ STRUCTURE │ CHAT │
|
||||
│ ┌─────────────────────────────────┐ │ ┌─────────────────────────────┐│
|
||||
│ │ 📁 Pages │ │ │ 🤖 I've created a task ││
|
||||
│ │ 📄 HomePage │ │ │ management app with: ││
|
||||
│ │ 📄 ProjectsPage │ │ │ ││
|
||||
│ │ 📄 KanbanBoard │ │ │ • 4 pages for navigation ││
|
||||
│ │ 📄 TaskDetail │ │ │ • Kanban board component ││
|
||||
│ │ │ │ │ • Task and Project models ││
|
||||
│ │ 📁 Components │ │ │ • Drag-and-drop ready ││
|
||||
│ │ 🧩 TaskCard │ │ │ ││
|
||||
│ │ 🧩 KanbanColumn │ │ │ Want me to add anything? ││
|
||||
│ │ 🧩 ProjectCard │ │ ├─────────────────────────────┤│
|
||||
│ │ 🧩 NavBar │ │ │ Add a calendar view too ││
|
||||
│ │ │ │ │ [Send]││
|
||||
│ │ 📁 Data Models │ │ └─────────────────────────────┘│
|
||||
│ │ 📊 Task │ │ │
|
||||
│ │ 📊 Project │ │ │
|
||||
│ │ 📊 User │ │ │
|
||||
│ └─────────────────────────────────┘ │ │
|
||||
│ │
|
||||
│ [Regenerate] [Edit Manually] [Create] │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Files to Create
|
||||
|
||||
1. `packages/noodl-editor/src/editor/src/services/AiScaffoldingService.ts`
|
||||
2. `packages/noodl-editor/src/editor/src/services/ai/prompts/scaffolding.ts`
|
||||
3. `packages/noodl-editor/src/editor/src/services/ai/ScaffoldConverter.ts`
|
||||
4. `packages/noodl-editor/src/editor/src/services/ai/AnthropicClient.ts`
|
||||
5. `packages/noodl-core-ui/src/components/modals/AiProjectModal/AiProjectModal.tsx`
|
||||
6. `packages/noodl-core-ui/src/components/modals/AiProjectModal/ProjectDescriptionInput.tsx`
|
||||
7. `packages/noodl-core-ui/src/components/modals/AiProjectModal/ScaffoldPreview.tsx`
|
||||
8. `packages/noodl-core-ui/src/components/modals/AiProjectModal/RefinementChat.tsx`
|
||||
|
||||
## Files to Modify
|
||||
|
||||
1. `packages/noodl-editor/src/editor/src/views/projectsview.ts`
|
||||
- Add "Describe your project" option
|
||||
- Launch AiProjectModal
|
||||
|
||||
2. `packages/noodl-core-ui/src/preview/launcher/Launcher/Launcher.tsx`
|
||||
- Add AI project creation button
|
||||
|
||||
3. `packages/noodl-editor/src/editor/src/utils/LocalProjectsModel.ts`
|
||||
- Add `newProjectFromScaffold()` method
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### Phase 1: AI Infrastructure
|
||||
1. Create AnthropicClient wrapper
|
||||
2. Implement prompt templates
|
||||
3. Set up API key management
|
||||
4. Create scaffolding service skeleton
|
||||
|
||||
### Phase 2: Scaffold Generation
|
||||
1. Implement analyzeDescription
|
||||
2. Implement generateScaffold
|
||||
3. Test with various descriptions
|
||||
4. Refine prompts based on results
|
||||
|
||||
### Phase 3: Scaffold Converter
|
||||
1. Implement page creation
|
||||
2. Implement component creation
|
||||
3. Implement navigation setup
|
||||
4. Implement styling application
|
||||
|
||||
### Phase 4: UI - Input Phase
|
||||
1. Create AiProjectModal
|
||||
2. Create ProjectDescriptionInput
|
||||
3. Add example prompts
|
||||
4. Integrate with launcher
|
||||
|
||||
### Phase 5: UI - Preview Phase
|
||||
1. Create ScaffoldPreview
|
||||
2. Create structure tree view
|
||||
3. Create RefinementChat
|
||||
4. Add edit/regenerate actions
|
||||
|
||||
### Phase 6: Project Creation
|
||||
1. Implement createProject
|
||||
2. Handle prefab imports
|
||||
3. Open project in editor
|
||||
4. Show success/onboarding
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
- [ ] Description analysis extracts features correctly
|
||||
- [ ] Scaffold generation produces valid structure
|
||||
- [ ] Prefabs used when appropriate
|
||||
- [ ] Converter creates valid components
|
||||
- [ ] Pages have correct routing
|
||||
- [ ] Navigation works between pages
|
||||
- [ ] Styling applied consistently
|
||||
- [ ] Refinement chat updates scaffold
|
||||
- [ ] Project opens in editor
|
||||
- [ ] Error handling for API failures
|
||||
- [ ] Rate limiting handled gracefully
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Anthropic API access (Claude)
|
||||
- COMP-002 (Built-in Prefabs) - for scaffold components
|
||||
|
||||
## Blocked By
|
||||
|
||||
- None (can start immediately)
|
||||
|
||||
## Blocks
|
||||
|
||||
- AI-002 (Component Suggestions)
|
||||
- AI-003 (Natural Language Editing)
|
||||
|
||||
## Estimated Effort
|
||||
|
||||
- AI infrastructure: 4-5 hours
|
||||
- Scaffold generation: 6-8 hours
|
||||
- Scaffold converter: 6-8 hours
|
||||
- UI input phase: 4-5 hours
|
||||
- UI preview phase: 5-6 hours
|
||||
- Project creation: 3-4 hours
|
||||
- Testing & refinement: 4-5 hours
|
||||
- **Total: 32-41 hours**
|
||||
|
||||
## Success Criteria
|
||||
|
||||
1. Users can describe project in natural language
|
||||
2. AI generates appropriate structure
|
||||
3. Preview shows clear scaffold
|
||||
4. Refinement chat enables adjustments
|
||||
5. Created project is functional
|
||||
6. Time from idea to working scaffold < 2 minutes
|
||||
|
||||
## Example Prompts & Outputs
|
||||
|
||||
### Example 1: Task Manager
|
||||
|
||||
**Input:** "A task management app where users can create projects, add tasks with due dates, and track progress with a kanban board"
|
||||
|
||||
**Output:**
|
||||
- Pages: Home, Projects, Kanban, TaskDetail
|
||||
- Components: TaskCard, KanbanColumn, ProjectCard, NavBar
|
||||
- Data: Task (title, description, dueDate, status), Project (name, tasks[])
|
||||
|
||||
### Example 2: Recipe App
|
||||
|
||||
**Input:** "A recipe app with categories, favorites, and a shopping list generator"
|
||||
|
||||
**Output:**
|
||||
- Pages: Home, Categories, RecipeDetail, Favorites, ShoppingList
|
||||
- Components: RecipeCard, CategoryTile, IngredientList, AddToFavoritesButton
|
||||
- Data: Recipe, Category, Ingredient, ShoppingItem
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- Voice input for description
|
||||
- Screenshot/mockup to scaffold
|
||||
- Integration with design systems
|
||||
- Multi-language support
|
||||
- Template learning from user projects
|
||||
@@ -0,0 +1,507 @@
|
||||
# AI-002: AI Component Suggestions
|
||||
|
||||
## Overview
|
||||
|
||||
Provide intelligent, context-aware component suggestions as users build their projects. When a user is working on a component, AI analyzes the context and suggests relevant nodes, connections, or entire sub-components that would complement what they're building.
|
||||
|
||||
## Context
|
||||
|
||||
Currently, users must:
|
||||
- Know what node they need
|
||||
- Search through the node picker
|
||||
- Understand which nodes work together
|
||||
- Manually create common patterns
|
||||
|
||||
This creates friction for:
|
||||
- New users learning the platform
|
||||
- Experienced users building repetitive patterns
|
||||
- Anyone implementing common UI patterns
|
||||
|
||||
AI suggestions provide:
|
||||
- "What you might need next" recommendations
|
||||
- Common pattern recognition
|
||||
- Learning through suggestion
|
||||
- Faster workflow for experts
|
||||
|
||||
### Integration with Existing AI
|
||||
|
||||
From `AiAssistantModel.ts`:
|
||||
```typescript
|
||||
// Existing AI node templates
|
||||
templates: AiTemplate[] = docsTemplates.map(...)
|
||||
|
||||
// Activity tracking
|
||||
addActivity({ id, type, title, prompt, node, graph })
|
||||
```
|
||||
|
||||
This task extends the AI capabilities to work alongside normal editing, not just through dedicated AI nodes.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Functional Requirements
|
||||
|
||||
1. **Context Analysis**
|
||||
- Analyze current component structure
|
||||
- Identify incomplete patterns
|
||||
- Detect user intent from recent actions
|
||||
- Consider project-wide context
|
||||
|
||||
2. **Suggestion Types**
|
||||
- **Node suggestions**: "Add a Loading state?"
|
||||
- **Connection suggestions**: "Connect this to..."
|
||||
- **Pattern completion**: "Complete this form with validation?"
|
||||
- **Prefab suggestions**: "Use the Form Input prefab?"
|
||||
|
||||
3. **Suggestion Display**
|
||||
- Non-intrusive inline hints
|
||||
- Expandable detail panel
|
||||
- One-click insertion
|
||||
- Keyboard shortcuts
|
||||
|
||||
4. **Learning & Relevance**
|
||||
- Learn from user accepts/rejects
|
||||
- Improve relevance over time
|
||||
- Consider user skill level
|
||||
- Avoid repetitive suggestions
|
||||
|
||||
5. **Control & Settings**
|
||||
- Enable/disable suggestions
|
||||
- Suggestion frequency
|
||||
- Types of suggestions
|
||||
- Reset learned preferences
|
||||
|
||||
### Non-Functional Requirements
|
||||
|
||||
- Suggestions appear within 500ms
|
||||
- No blocking of user actions
|
||||
- Minimal API calls (batch/cache)
|
||||
- Works offline (basic patterns)
|
||||
|
||||
## Technical Approach
|
||||
|
||||
### 1. Suggestion Service
|
||||
|
||||
```typescript
|
||||
// packages/noodl-editor/src/editor/src/services/AiSuggestionService.ts
|
||||
|
||||
interface SuggestionContext {
|
||||
component: ComponentModel;
|
||||
selectedNodes: NodeGraphNode[];
|
||||
recentActions: EditorAction[];
|
||||
projectContext: ProjectContext;
|
||||
userPreferences: UserPreferences;
|
||||
}
|
||||
|
||||
interface Suggestion {
|
||||
id: string;
|
||||
type: 'node' | 'connection' | 'pattern' | 'prefab';
|
||||
confidence: number; // 0-1
|
||||
title: string;
|
||||
description: string;
|
||||
preview?: string; // Visual preview
|
||||
action: SuggestionAction;
|
||||
dismissable: boolean;
|
||||
}
|
||||
|
||||
interface SuggestionAction {
|
||||
type: 'insert_node' | 'create_connection' | 'insert_pattern' | 'import_prefab';
|
||||
payload: any;
|
||||
}
|
||||
|
||||
class AiSuggestionService {
|
||||
private static instance: AiSuggestionService;
|
||||
private suggestionCache: Map<string, Suggestion[]> = new Map();
|
||||
private userFeedback: UserFeedbackStore;
|
||||
|
||||
// Main API
|
||||
async getSuggestions(context: SuggestionContext): Promise<Suggestion[]>;
|
||||
async applySuggestion(suggestion: Suggestion): Promise<void>;
|
||||
async dismissSuggestion(suggestion: Suggestion): Promise<void>;
|
||||
|
||||
// Feedback
|
||||
recordAccept(suggestion: Suggestion): void;
|
||||
recordReject(suggestion: Suggestion): void;
|
||||
recordIgnore(suggestion: Suggestion): void;
|
||||
|
||||
// Settings
|
||||
setEnabled(enabled: boolean): void;
|
||||
setFrequency(frequency: SuggestionFrequency): void;
|
||||
getSuggestionSettings(): SuggestionSettings;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Context Analyzer
|
||||
|
||||
```typescript
|
||||
// packages/noodl-editor/src/editor/src/services/ai/ContextAnalyzer.ts
|
||||
|
||||
interface AnalysisResult {
|
||||
componentType: ComponentType;
|
||||
currentPattern: Pattern | null;
|
||||
incompletePatterns: IncompletePattern[];
|
||||
missingConnections: MissingConnection[];
|
||||
suggestedEnhancements: Enhancement[];
|
||||
}
|
||||
|
||||
class ContextAnalyzer {
|
||||
// Pattern detection
|
||||
detectPatterns(component: ComponentModel): Pattern[];
|
||||
detectIncompletePatterns(component: ComponentModel): IncompletePattern[];
|
||||
|
||||
// Connection analysis
|
||||
findMissingConnections(nodes: NodeGraphNode[]): MissingConnection[];
|
||||
findOrphanedNodes(component: ComponentModel): NodeGraphNode[];
|
||||
|
||||
// Intent inference
|
||||
inferUserIntent(recentActions: EditorAction[]): UserIntent;
|
||||
|
||||
// Project context
|
||||
getRelatedComponents(component: ComponentModel): ComponentModel[];
|
||||
getDataModelContext(component: ComponentModel): DataModel[];
|
||||
}
|
||||
|
||||
// Common patterns to detect
|
||||
const PATTERNS = {
|
||||
FORM_INPUT: {
|
||||
nodes: ['TextInput', 'Label'],
|
||||
missing: ['Validation', 'ErrorDisplay'],
|
||||
suggestion: 'Add form validation?'
|
||||
},
|
||||
LIST_ITEM: {
|
||||
nodes: ['Repeater', 'Group'],
|
||||
missing: ['ItemClick', 'DeleteAction'],
|
||||
suggestion: 'Add item interactions?'
|
||||
},
|
||||
DATA_FETCH: {
|
||||
nodes: ['REST'],
|
||||
missing: ['LoadingState', 'ErrorState'],
|
||||
suggestion: 'Add loading and error states?'
|
||||
},
|
||||
// ... more patterns
|
||||
};
|
||||
```
|
||||
|
||||
### 3. Suggestion Engine
|
||||
|
||||
```typescript
|
||||
// packages/noodl-editor/src/editor/src/services/ai/SuggestionEngine.ts
|
||||
|
||||
class SuggestionEngine {
|
||||
private contextAnalyzer: ContextAnalyzer;
|
||||
private patternLibrary: PatternLibrary;
|
||||
private prefabMatcher: PrefabMatcher;
|
||||
|
||||
async generateSuggestions(context: SuggestionContext): Promise<Suggestion[]> {
|
||||
const suggestions: Suggestion[] = [];
|
||||
|
||||
// 1. Local pattern matching (no API)
|
||||
const localSuggestions = this.getLocalSuggestions(context);
|
||||
suggestions.push(...localSuggestions);
|
||||
|
||||
// 2. AI-powered suggestions (API call)
|
||||
if (this.shouldCallApi(context)) {
|
||||
const aiSuggestions = await this.getAiSuggestions(context);
|
||||
suggestions.push(...aiSuggestions);
|
||||
}
|
||||
|
||||
// 3. Prefab matching
|
||||
const prefabSuggestions = this.getPrefabSuggestions(context);
|
||||
suggestions.push(...prefabSuggestions);
|
||||
|
||||
// 4. Rank and filter
|
||||
return this.rankSuggestions(suggestions, context);
|
||||
}
|
||||
|
||||
private getLocalSuggestions(context: SuggestionContext): Suggestion[] {
|
||||
const analysis = this.contextAnalyzer.analyze(context.component);
|
||||
const suggestions: Suggestion[] = [];
|
||||
|
||||
// Pattern completion
|
||||
for (const incomplete of analysis.incompletePatterns) {
|
||||
suggestions.push({
|
||||
type: 'pattern',
|
||||
title: incomplete.completionTitle,
|
||||
description: incomplete.description,
|
||||
confidence: incomplete.confidence,
|
||||
action: {
|
||||
type: 'insert_pattern',
|
||||
payload: incomplete.completionNodes
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Missing connections
|
||||
for (const missing of analysis.missingConnections) {
|
||||
suggestions.push({
|
||||
type: 'connection',
|
||||
title: `Connect ${missing.from} to ${missing.to}`,
|
||||
confidence: missing.confidence,
|
||||
action: {
|
||||
type: 'create_connection',
|
||||
payload: missing
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return suggestions;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. UI Components
|
||||
|
||||
#### Inline Suggestion Hint
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ Canvas │
|
||||
│ │
|
||||
│ ┌─────────────┐ ┌─────────────┐ │
|
||||
│ │ TextInput │──────│ Variable │ │
|
||||
│ └─────────────┘ └─────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────────────────────────────┐ │
|
||||
│ │ 💡 Add form validation? [+ Add]│ │
|
||||
│ │ Validate input and show errors │ │
|
||||
│ └─────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
#### Suggestion Panel
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ 💡 Suggestions [×] │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Based on your current component: │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ 🧩 Complete Form Pattern [+ Apply] │ │
|
||||
│ │ Add validation, error states, and submit handling │ │
|
||||
│ │ Confidence: ████████░░ 85% │ │
|
||||
│ ├─────────────────────────────────────────────────────────────────┤ │
|
||||
│ │ 📦 Use "Form Input" Prefab [+ Apply] │ │
|
||||
│ │ Replace with pre-built form input component │ │
|
||||
│ │ Confidence: ███████░░░ 75% │ │
|
||||
│ ├─────────────────────────────────────────────────────────────────┤ │
|
||||
│ │ 🔗 Connect to Submit button [+ Apply] │ │
|
||||
│ │ Wire up the form submission flow │ │
|
||||
│ │ Confidence: ██████░░░░ 65% │ │
|
||||
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ [⚙️ Suggestion Settings] │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 5. Pattern Library
|
||||
|
||||
```typescript
|
||||
// packages/noodl-editor/src/editor/src/services/ai/PatternLibrary.ts
|
||||
|
||||
interface PatternDefinition {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
trigger: PatternTrigger;
|
||||
completion: PatternCompletion;
|
||||
examples: string[];
|
||||
}
|
||||
|
||||
const PATTERNS: PatternDefinition[] = [
|
||||
{
|
||||
id: 'form-validation',
|
||||
name: 'Form Validation',
|
||||
description: 'Add input validation with error display',
|
||||
trigger: {
|
||||
hasNodes: ['TextInput', 'Variable'],
|
||||
missingNodes: ['Function', 'Condition', 'Text'],
|
||||
nodeCount: { min: 2, max: 5 }
|
||||
},
|
||||
completion: {
|
||||
nodes: [
|
||||
{ type: 'Function', name: 'Validate' },
|
||||
{ type: 'Condition', name: 'IsValid' },
|
||||
{ type: 'Text', name: 'ErrorMessage' }
|
||||
],
|
||||
connections: [
|
||||
{ from: 'TextInput.value', to: 'Validate.input' },
|
||||
{ from: 'Validate.result', to: 'IsValid.condition' },
|
||||
{ from: 'IsValid.false', to: 'ErrorMessage.visible' }
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'loading-state',
|
||||
name: 'Loading State',
|
||||
description: 'Add loading indicator during async operations',
|
||||
trigger: {
|
||||
hasNodes: ['REST'],
|
||||
missingNodes: ['Condition', 'Group'],
|
||||
},
|
||||
completion: {
|
||||
nodes: [
|
||||
{ type: 'Variable', name: 'IsLoading' },
|
||||
{ type: 'Group', name: 'LoadingSpinner' },
|
||||
{ type: 'Condition', name: 'ShowContent' }
|
||||
],
|
||||
connections: [
|
||||
{ from: 'REST.fetch', to: 'IsLoading.set(true)' },
|
||||
{ from: 'REST.success', to: 'IsLoading.set(false)' },
|
||||
{ from: 'IsLoading.value', to: 'LoadingSpinner.visible' }
|
||||
]
|
||||
}
|
||||
},
|
||||
// ... more patterns
|
||||
];
|
||||
```
|
||||
|
||||
## Files to Create
|
||||
|
||||
1. `packages/noodl-editor/src/editor/src/services/AiSuggestionService.ts`
|
||||
2. `packages/noodl-editor/src/editor/src/services/ai/ContextAnalyzer.ts`
|
||||
3. `packages/noodl-editor/src/editor/src/services/ai/SuggestionEngine.ts`
|
||||
4. `packages/noodl-editor/src/editor/src/services/ai/PatternLibrary.ts`
|
||||
5. `packages/noodl-editor/src/editor/src/services/ai/PrefabMatcher.ts`
|
||||
6. `packages/noodl-core-ui/src/components/ai/SuggestionHint/SuggestionHint.tsx`
|
||||
7. `packages/noodl-core-ui/src/components/ai/SuggestionPanel/SuggestionPanel.tsx`
|
||||
8. `packages/noodl-core-ui/src/components/ai/SuggestionCard/SuggestionCard.tsx`
|
||||
|
||||
## Files to Modify
|
||||
|
||||
1. `packages/noodl-editor/src/editor/src/views/nodegrapheditor.js`
|
||||
- Hook into node selection/creation
|
||||
- Trigger suggestion generation
|
||||
- Display suggestion hints
|
||||
|
||||
2. `packages/noodl-editor/src/editor/src/pages/EditorPage/EditorPage.tsx`
|
||||
- Add suggestion panel toggle
|
||||
- Handle suggestion keybindings
|
||||
|
||||
3. `packages/noodl-editor/src/editor/src/models/AiAssistant/AiAssistantModel.ts`
|
||||
- Integrate suggestion service
|
||||
- Share context with AI nodes
|
||||
|
||||
4. `packages/noodl-editor/src/editor/src/stores/EditorSettings.ts`
|
||||
- Add suggestion settings
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### Phase 1: Context Analysis
|
||||
1. Create ContextAnalyzer
|
||||
2. Implement pattern detection
|
||||
3. Implement connection analysis
|
||||
4. Test with various components
|
||||
|
||||
### Phase 2: Pattern Library
|
||||
1. Define pattern schema
|
||||
2. Create initial patterns (10-15)
|
||||
3. Implement pattern matching
|
||||
4. Test pattern triggers
|
||||
|
||||
### Phase 3: Suggestion Engine
|
||||
1. Create SuggestionEngine
|
||||
2. Implement local suggestions
|
||||
3. Implement AI suggestions
|
||||
4. Add ranking/filtering
|
||||
|
||||
### Phase 4: UI - Inline Hints
|
||||
1. Create SuggestionHint component
|
||||
2. Position near relevant nodes
|
||||
3. Add apply/dismiss actions
|
||||
4. Animate appearance
|
||||
|
||||
### Phase 5: UI - Panel
|
||||
1. Create SuggestionPanel
|
||||
2. Create SuggestionCard
|
||||
3. Add settings access
|
||||
4. Handle keyboard shortcuts
|
||||
|
||||
### Phase 6: Feedback & Learning
|
||||
1. Track accept/reject
|
||||
2. Adjust confidence scores
|
||||
3. Improve relevance
|
||||
4. Add user settings
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
- [ ] Patterns detected correctly
|
||||
- [ ] Suggestions appear at right time
|
||||
- [ ] Apply action works correctly
|
||||
- [ ] Dismiss removes suggestion
|
||||
- [ ] Inline hint positions correctly
|
||||
- [ ] Panel shows all suggestions
|
||||
- [ ] Settings persist
|
||||
- [ ] Works offline (local patterns)
|
||||
- [ ] API suggestions enhance local
|
||||
- [ ] Feedback recorded
|
||||
- [ ] Performance < 500ms
|
||||
|
||||
## Dependencies
|
||||
|
||||
- AI-001 (AI Project Scaffolding) - for AI infrastructure
|
||||
- COMP-002 (Built-in Prefabs) - for prefab matching
|
||||
|
||||
## Blocked By
|
||||
|
||||
- AI-001 (for AnthropicClient)
|
||||
|
||||
## Blocks
|
||||
|
||||
- AI-003 (Natural Language Editing)
|
||||
|
||||
## Estimated Effort
|
||||
|
||||
- Context analyzer: 4-5 hours
|
||||
- Pattern library: 4-5 hours
|
||||
- Suggestion engine: 5-6 hours
|
||||
- UI inline hints: 3-4 hours
|
||||
- UI panel: 4-5 hours
|
||||
- Feedback system: 3-4 hours
|
||||
- Testing & refinement: 4-5 hours
|
||||
- **Total: 27-34 hours**
|
||||
|
||||
## Success Criteria
|
||||
|
||||
1. Suggestions appear contextually
|
||||
2. Pattern completion works smoothly
|
||||
3. Prefab matching finds relevant prefabs
|
||||
4. Apply action inserts correctly
|
||||
5. Users can control suggestions
|
||||
6. Suggestions improve over time
|
||||
|
||||
## Pattern Categories
|
||||
|
||||
### Forms
|
||||
- Form validation
|
||||
- Form submission
|
||||
- Input formatting
|
||||
- Error display
|
||||
|
||||
### Data
|
||||
- Loading states
|
||||
- Error handling
|
||||
- Refresh/retry
|
||||
- Pagination
|
||||
|
||||
### Navigation
|
||||
- Page transitions
|
||||
- Breadcrumbs
|
||||
- Tab navigation
|
||||
- Modal flows
|
||||
|
||||
### Lists
|
||||
- Item selection
|
||||
- Delete/edit actions
|
||||
- Drag and drop
|
||||
- Filtering
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- Real-time suggestions while typing
|
||||
- Team-shared patterns
|
||||
- Auto-apply for obvious patterns
|
||||
- Pattern creation from selection
|
||||
- AI-powered custom patterns
|
||||
@@ -0,0 +1,565 @@
|
||||
# AI-003: Natural Language Editing
|
||||
|
||||
## Overview
|
||||
|
||||
Enable users to modify their projects using natural language commands. Instead of manually finding and configuring nodes, users can say "make this button blue" or "add a loading spinner when fetching data" and have AI make the changes.
|
||||
|
||||
## Context
|
||||
|
||||
Current editing workflow:
|
||||
1. Select node in canvas
|
||||
2. Find property in sidebar
|
||||
3. Understand property options
|
||||
4. Make change
|
||||
5. Repeat for related nodes
|
||||
|
||||
Natural language editing:
|
||||
1. Select component or node
|
||||
2. Describe what you want
|
||||
3. AI makes the changes
|
||||
4. Review and accept/modify
|
||||
|
||||
This is especially powerful for:
|
||||
- Styling changes across multiple elements
|
||||
- Logic modifications that span nodes
|
||||
- Refactoring component structure
|
||||
- Complex multi-step changes
|
||||
|
||||
### Existing AI Foundation
|
||||
|
||||
From `AiAssistantModel.ts`:
|
||||
```typescript
|
||||
// Chat history for AI interactions
|
||||
class ChatHistory {
|
||||
messages: ChatMessage[];
|
||||
add(message: ChatMessage): void;
|
||||
}
|
||||
|
||||
// AI context per node
|
||||
class AiCopilotContext {
|
||||
template: AiTemplate;
|
||||
chatHistory: ChatHistory;
|
||||
node: NodeGraphNode;
|
||||
}
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
### Functional Requirements
|
||||
|
||||
1. **Command Input**
|
||||
- Command palette (Cmd+K style)
|
||||
- Inline text input on selection
|
||||
- Voice input (optional)
|
||||
- Recent commands history
|
||||
|
||||
2. **Command Understanding**
|
||||
- Style changes: "make it red", "add shadow"
|
||||
- Structure changes: "add a header", "wrap in a card"
|
||||
- Logic changes: "show loading while fetching"
|
||||
- Data changes: "sort by date", "filter active items"
|
||||
|
||||
3. **Change Preview**
|
||||
- Show what will change before applying
|
||||
- Highlight affected nodes
|
||||
- Before/after comparison
|
||||
- Explanation of changes
|
||||
|
||||
4. **Change Application**
|
||||
- Apply changes atomically
|
||||
- Support undo/redo
|
||||
- Handle errors gracefully
|
||||
- Learn from corrections
|
||||
|
||||
5. **Scope Selection**
|
||||
- Selected node(s) only
|
||||
- Current component
|
||||
- Related components
|
||||
- Entire project
|
||||
|
||||
### Non-Functional Requirements
|
||||
|
||||
- Response time < 3 seconds
|
||||
- Changes are reversible
|
||||
- Works on any component type
|
||||
- Graceful degradation without API
|
||||
|
||||
## Technical Approach
|
||||
|
||||
### 1. Natural Language Command Service
|
||||
|
||||
```typescript
|
||||
// packages/noodl-editor/src/editor/src/services/NaturalLanguageService.ts
|
||||
|
||||
interface CommandContext {
|
||||
selection: NodeGraphNode[];
|
||||
component: ComponentModel;
|
||||
project: ProjectModel;
|
||||
recentCommands: Command[];
|
||||
}
|
||||
|
||||
interface Command {
|
||||
id: string;
|
||||
text: string;
|
||||
timestamp: Date;
|
||||
result: CommandResult;
|
||||
}
|
||||
|
||||
interface CommandResult {
|
||||
success: boolean;
|
||||
changes: Change[];
|
||||
explanation: string;
|
||||
undoAction?: () => void;
|
||||
}
|
||||
|
||||
interface Change {
|
||||
type: 'property' | 'node' | 'connection' | 'structure';
|
||||
target: string;
|
||||
before: any;
|
||||
after: any;
|
||||
description: string;
|
||||
}
|
||||
|
||||
class NaturalLanguageService {
|
||||
private static instance: NaturalLanguageService;
|
||||
|
||||
// Main API
|
||||
async parseCommand(text: string, context: CommandContext): Promise<ParsedCommand>;
|
||||
async previewChanges(command: ParsedCommand): Promise<ChangePreview>;
|
||||
async applyChanges(preview: ChangePreview): Promise<CommandResult>;
|
||||
async undoCommand(commandId: string): Promise<void>;
|
||||
|
||||
// Command history
|
||||
getRecentCommands(): Command[];
|
||||
searchCommands(query: string): Command[];
|
||||
|
||||
// Learning
|
||||
recordCorrection(commandId: string, correction: Change[]): void;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Command Parser
|
||||
|
||||
```typescript
|
||||
// packages/noodl-editor/src/editor/src/services/ai/CommandParser.ts
|
||||
|
||||
interface ParsedCommand {
|
||||
intent: CommandIntent;
|
||||
targets: CommandTarget[];
|
||||
modifications: Modification[];
|
||||
confidence: number;
|
||||
}
|
||||
|
||||
enum CommandIntent {
|
||||
STYLE_CHANGE = 'style_change',
|
||||
STRUCTURE_CHANGE = 'structure_change',
|
||||
LOGIC_CHANGE = 'logic_change',
|
||||
DATA_CHANGE = 'data_change',
|
||||
CREATE = 'create',
|
||||
DELETE = 'delete',
|
||||
CONNECT = 'connect',
|
||||
UNKNOWN = 'unknown'
|
||||
}
|
||||
|
||||
interface CommandTarget {
|
||||
type: 'node' | 'component' | 'property' | 'connection';
|
||||
selector: string; // How to find it
|
||||
resolved?: any; // Actual reference
|
||||
}
|
||||
|
||||
class CommandParser {
|
||||
private patterns: CommandPattern[];
|
||||
|
||||
async parse(text: string, context: CommandContext): Promise<ParsedCommand> {
|
||||
// 1. Try local pattern matching first (fast)
|
||||
const localMatch = this.matchLocalPatterns(text);
|
||||
if (localMatch.confidence > 0.9) {
|
||||
return localMatch;
|
||||
}
|
||||
|
||||
// 2. Use AI for complex commands
|
||||
const aiParsed = await this.aiParse(text, context);
|
||||
|
||||
// 3. Merge and validate
|
||||
return this.mergeAndValidate(localMatch, aiParsed, context);
|
||||
}
|
||||
|
||||
private matchLocalPatterns(text: string): ParsedCommand {
|
||||
// Pattern: "make [target] [color]"
|
||||
// Pattern: "add [element] to [target]"
|
||||
// Pattern: "connect [source] to [destination]"
|
||||
// etc.
|
||||
}
|
||||
|
||||
private async aiParse(text: string, context: CommandContext): Promise<ParsedCommand> {
|
||||
const prompt = `Parse this Noodl editing command:
|
||||
Command: "${text}"
|
||||
|
||||
Current selection: ${context.selection.map(n => n.type.localName).join(', ')}
|
||||
Component: ${context.component.name}
|
||||
|
||||
Output JSON with: intent, targets, modifications`;
|
||||
|
||||
const response = await this.anthropicClient.complete(prompt);
|
||||
return JSON.parse(response);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Change Generator
|
||||
|
||||
```typescript
|
||||
// packages/noodl-editor/src/editor/src/services/ai/ChangeGenerator.ts
|
||||
|
||||
class ChangeGenerator {
|
||||
// Generate actual changes from parsed command
|
||||
async generateChanges(command: ParsedCommand, context: CommandContext): Promise<Change[]> {
|
||||
const changes: Change[] = [];
|
||||
|
||||
switch (command.intent) {
|
||||
case CommandIntent.STYLE_CHANGE:
|
||||
changes.push(...this.generateStyleChanges(command, context));
|
||||
break;
|
||||
case CommandIntent.STRUCTURE_CHANGE:
|
||||
changes.push(...await this.generateStructureChanges(command, context));
|
||||
break;
|
||||
case CommandIntent.LOGIC_CHANGE:
|
||||
changes.push(...await this.generateLogicChanges(command, context));
|
||||
break;
|
||||
// ...
|
||||
}
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
private generateStyleChanges(command: ParsedCommand, context: CommandContext): Change[] {
|
||||
const changes: Change[] = [];
|
||||
|
||||
for (const target of command.targets) {
|
||||
const node = this.resolveTarget(target, context);
|
||||
|
||||
for (const mod of command.modifications) {
|
||||
const propertyName = this.mapToNoodlProperty(mod.property);
|
||||
const newValue = this.parseValue(mod.value, propertyName);
|
||||
|
||||
changes.push({
|
||||
type: 'property',
|
||||
target: node.id,
|
||||
before: node.parameters[propertyName],
|
||||
after: newValue,
|
||||
description: `Change ${propertyName} to ${newValue}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
private async generateStructureChanges(
|
||||
command: ParsedCommand,
|
||||
context: CommandContext
|
||||
): Promise<Change[]> {
|
||||
// Use AI to generate node structure
|
||||
const prompt = `Generate Noodl node structure for:
|
||||
"${command.modifications.map(m => m.description).join(', ')}"
|
||||
|
||||
Current context: ${JSON.stringify(context.selection.map(n => n.type.localName))}
|
||||
|
||||
Output JSON array of nodes to create and connections`;
|
||||
|
||||
const response = await this.anthropicClient.complete(prompt);
|
||||
return this.parseStructureResponse(response);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. UI Components
|
||||
|
||||
#### Command Palette
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ 🔮 What do you want to do? [×] │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Make the button larger and add a hover effect │ │
|
||||
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ Selected: Button, Text │
|
||||
│ │
|
||||
│ Recent: │
|
||||
│ • "Add loading spinner to the form" │
|
||||
│ • "Make all headers blue" │
|
||||
│ • "Connect the submit button to the API" │
|
||||
│ │
|
||||
│ Examples: │
|
||||
│ • "Wrap this in a card with shadow" │
|
||||
│ • "Add validation to all inputs" │
|
||||
│ • "Show error message when API fails" │
|
||||
│ │
|
||||
│ [Preview Changes] │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
#### Change Preview
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ Preview Changes [×] │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Command: "Make the button larger and add a hover effect" │
|
||||
│ │
|
||||
│ Changes to apply: │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ ✓ Button │ │
|
||||
│ │ • width: 100px → 150px │ │
|
||||
│ │ • height: 40px → 50px │ │
|
||||
│ │ • fontSize: 14px → 16px │ │
|
||||
│ ├─────────────────────────────────────────────────────────────────┤ │
|
||||
│ │ + New: HoverState │ │
|
||||
│ │ • scale: 1.05 │ │
|
||||
│ │ • transition: 200ms │ │
|
||||
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ 🤖 "I'll increase the button size by 50% and add a subtle scale │
|
||||
│ effect on hover with a smooth transition." │
|
||||
│ │
|
||||
│ [Cancel] [Modify] [Apply Changes] │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
#### Inline Command
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ Canvas │
|
||||
│ │
|
||||
│ ┌─────────────────┐ │
|
||||
│ │ [Button] │ ← Selected │
|
||||
│ │ Click me │ │
|
||||
│ └─────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌──────┴──────────────────────────────────────────────┐ │
|
||||
│ │ 🔮 Make it red with rounded corners [Enter] │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 5. Command Patterns
|
||||
|
||||
```typescript
|
||||
// packages/noodl-editor/src/editor/src/services/ai/CommandPatterns.ts
|
||||
|
||||
const COMMAND_PATTERNS: CommandPattern[] = [
|
||||
// Style patterns
|
||||
{
|
||||
pattern: /make (?:it |this |the )?(.+?) (red|blue|green|...)/i,
|
||||
intent: CommandIntent.STYLE_CHANGE,
|
||||
extract: (match) => ({
|
||||
target: match[1] || 'selection',
|
||||
property: 'backgroundColor',
|
||||
value: match[2]
|
||||
})
|
||||
},
|
||||
{
|
||||
pattern: /(?:set |change )?(?:the )?(.+?) (?:to |=) (.+)/i,
|
||||
intent: CommandIntent.STYLE_CHANGE,
|
||||
extract: (match) => ({
|
||||
property: match[1],
|
||||
value: match[2]
|
||||
})
|
||||
},
|
||||
|
||||
// Structure patterns
|
||||
{
|
||||
pattern: /add (?:a |an )?(.+?) (?:to |inside |in) (.+)/i,
|
||||
intent: CommandIntent.STRUCTURE_CHANGE,
|
||||
extract: (match) => ({
|
||||
nodeType: match[1],
|
||||
target: match[2]
|
||||
})
|
||||
},
|
||||
{
|
||||
pattern: /wrap (?:it |this |selection )?in (?:a |an )?(.+)/i,
|
||||
intent: CommandIntent.STRUCTURE_CHANGE,
|
||||
extract: (match) => ({
|
||||
action: 'wrap',
|
||||
wrapper: match[1]
|
||||
})
|
||||
},
|
||||
|
||||
// Logic patterns
|
||||
{
|
||||
pattern: /show (.+?) when (.+)/i,
|
||||
intent: CommandIntent.LOGIC_CHANGE,
|
||||
extract: (match) => ({
|
||||
action: 'conditional_show',
|
||||
target: match[1],
|
||||
condition: match[2]
|
||||
})
|
||||
},
|
||||
{
|
||||
pattern: /connect (.+?) to (.+)/i,
|
||||
intent: CommandIntent.CONNECT,
|
||||
extract: (match) => ({
|
||||
source: match[1],
|
||||
destination: match[2]
|
||||
})
|
||||
}
|
||||
];
|
||||
```
|
||||
|
||||
## Files to Create
|
||||
|
||||
1. `packages/noodl-editor/src/editor/src/services/NaturalLanguageService.ts`
|
||||
2. `packages/noodl-editor/src/editor/src/services/ai/CommandParser.ts`
|
||||
3. `packages/noodl-editor/src/editor/src/services/ai/ChangeGenerator.ts`
|
||||
4. `packages/noodl-editor/src/editor/src/services/ai/CommandPatterns.ts`
|
||||
5. `packages/noodl-core-ui/src/components/ai/CommandPalette/CommandPalette.tsx`
|
||||
6. `packages/noodl-core-ui/src/components/ai/ChangePreview/ChangePreview.tsx`
|
||||
7. `packages/noodl-core-ui/src/components/ai/InlineCommand/InlineCommand.tsx`
|
||||
|
||||
## Files to Modify
|
||||
|
||||
1. `packages/noodl-editor/src/editor/src/views/nodegrapheditor.js`
|
||||
- Add command palette trigger (Cmd+K)
|
||||
- Add inline command on selection
|
||||
- Handle change application
|
||||
|
||||
2. `packages/noodl-editor/src/editor/src/pages/EditorPage/EditorPage.tsx`
|
||||
- Add keyboard shortcut handler
|
||||
- Mount command palette
|
||||
|
||||
3. `packages/noodl-editor/src/editor/src/models/NodeGraphModel.ts`
|
||||
- Add atomic change application
|
||||
- Support undo for AI changes
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### Phase 1: Command Infrastructure
|
||||
1. Create NaturalLanguageService
|
||||
2. Implement command history
|
||||
3. Set up undo/redo support
|
||||
|
||||
### Phase 2: Command Parser
|
||||
1. Create CommandParser
|
||||
2. Define local patterns
|
||||
3. Implement AI parsing
|
||||
4. Test parsing accuracy
|
||||
|
||||
### Phase 3: Change Generator
|
||||
1. Create ChangeGenerator
|
||||
2. Implement style changes
|
||||
3. Implement structure changes
|
||||
4. Implement logic changes
|
||||
|
||||
### Phase 4: UI - Command Palette
|
||||
1. Create CommandPalette component
|
||||
2. Add keyboard shortcut
|
||||
3. Show recent/examples
|
||||
4. Handle input
|
||||
|
||||
### Phase 5: UI - Change Preview
|
||||
1. Create ChangePreview component
|
||||
2. Show before/after
|
||||
3. Add explanation
|
||||
4. Handle apply/cancel
|
||||
|
||||
### Phase 6: UI - Inline Command
|
||||
1. Create InlineCommand component
|
||||
2. Position near selection
|
||||
3. Handle quick commands
|
||||
|
||||
### Phase 7: Learning & Improvement
|
||||
1. Track command success
|
||||
2. Record corrections
|
||||
3. Improve pattern matching
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
- [ ] Style commands work correctly
|
||||
- [ ] Structure commands create nodes
|
||||
- [ ] Logic commands set up conditions
|
||||
- [ ] Preview shows accurate changes
|
||||
- [ ] Apply actually makes changes
|
||||
- [ ] Undo reverts changes
|
||||
- [ ] Keyboard shortcuts work
|
||||
- [ ] Recent commands saved
|
||||
- [ ] Error handling graceful
|
||||
- [ ] Complex commands work
|
||||
- [ ] Multi-target commands work
|
||||
|
||||
## Dependencies
|
||||
|
||||
- AI-001 (AI Project Scaffolding) - for AnthropicClient
|
||||
- AI-002 (Component Suggestions) - for context analysis
|
||||
|
||||
## Blocked By
|
||||
|
||||
- AI-001
|
||||
|
||||
## Blocks
|
||||
|
||||
- AI-004 (AI Design Assistance)
|
||||
|
||||
## Estimated Effort
|
||||
|
||||
- Command service: 4-5 hours
|
||||
- Command parser: 5-6 hours
|
||||
- Change generator: 6-8 hours
|
||||
- UI command palette: 4-5 hours
|
||||
- UI change preview: 4-5 hours
|
||||
- UI inline command: 3-4 hours
|
||||
- Testing & refinement: 4-5 hours
|
||||
- **Total: 30-38 hours**
|
||||
|
||||
## Success Criteria
|
||||
|
||||
1. Natural language commands understood
|
||||
2. Preview shows accurate changes
|
||||
3. Changes applied correctly
|
||||
4. Undo/redo works
|
||||
5. < 3 second response time
|
||||
6. 80%+ command success rate
|
||||
|
||||
## Command Examples
|
||||
|
||||
### Style Changes
|
||||
- "Make it red"
|
||||
- "Add a shadow"
|
||||
- "Increase font size to 18"
|
||||
- "Round the corners"
|
||||
- "Make all buttons blue"
|
||||
|
||||
### Structure Changes
|
||||
- "Add a header"
|
||||
- "Wrap in a card"
|
||||
- "Add a loading spinner"
|
||||
- "Create a sidebar"
|
||||
- "Split into two columns"
|
||||
|
||||
### Logic Changes
|
||||
- "Show loading while fetching"
|
||||
- "Hide when empty"
|
||||
- "Disable until form is valid"
|
||||
- "Navigate to home on click"
|
||||
- "Show error message on failure"
|
||||
|
||||
### Data Changes
|
||||
- "Sort by date"
|
||||
- "Filter completed items"
|
||||
- "Group by category"
|
||||
- "Limit to 10 items"
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- Voice input
|
||||
- Multi-language support
|
||||
- Command macros
|
||||
- Batch changes
|
||||
- Command sharing
|
||||
- Context-aware autocomplete
|
||||
@@ -0,0 +1,681 @@
|
||||
# AI-004: AI Design Assistance
|
||||
|
||||
## Overview
|
||||
|
||||
Provide AI-powered design feedback and improvements. Analyze components for design issues (accessibility, consistency, spacing) and suggest or auto-apply fixes. Transform rough layouts into polished designs.
|
||||
|
||||
## Context
|
||||
|
||||
Many Noodl users are developers or designers who may not have deep expertise in both areas. Common issues include:
|
||||
- Inconsistent spacing and alignment
|
||||
- Accessibility problems (contrast, touch targets)
|
||||
- Missing hover/focus states
|
||||
- Unbalanced layouts
|
||||
- Poor color combinations
|
||||
|
||||
AI Design Assistance provides:
|
||||
- Automated design review
|
||||
- One-click fixes for common issues
|
||||
- Style consistency enforcement
|
||||
- Accessibility compliance checking
|
||||
- Layout optimization suggestions
|
||||
|
||||
## Requirements
|
||||
|
||||
### Functional Requirements
|
||||
|
||||
1. **Design Analysis**
|
||||
- Scan component/page for issues
|
||||
- Categorize by severity (error, warning, info)
|
||||
- Group by type (spacing, color, typography, etc.)
|
||||
- Provide explanations
|
||||
|
||||
2. **Issue Categories**
|
||||
- **Accessibility**: Contrast, touch targets, labels
|
||||
- **Consistency**: Spacing, colors, typography
|
||||
- **Layout**: Alignment, balance, whitespace
|
||||
- **Interaction**: Hover, focus, active states
|
||||
- **Responsiveness**: Breakpoint issues
|
||||
|
||||
3. **Fix Application**
|
||||
- One-click fix for individual issues
|
||||
- "Fix all" for category
|
||||
- Preview before applying
|
||||
- Explain what was fixed
|
||||
|
||||
4. **Design Improvement**
|
||||
- "Polish this" command
|
||||
- Transform rough layouts
|
||||
- Suggest design alternatives
|
||||
- Apply consistent styling
|
||||
|
||||
5. **Design System Enforcement**
|
||||
- Check against project styles
|
||||
- Suggest using existing styles
|
||||
- Identify one-off values
|
||||
- Propose new styles
|
||||
|
||||
### Non-Functional Requirements
|
||||
|
||||
- Analysis completes in < 5 seconds
|
||||
- Fixes don't break functionality
|
||||
- Respects existing design intent
|
||||
- Works with any component structure
|
||||
|
||||
## Technical Approach
|
||||
|
||||
### 1. Design Analysis Service
|
||||
|
||||
```typescript
|
||||
// packages/noodl-editor/src/editor/src/services/DesignAnalysisService.ts
|
||||
|
||||
interface DesignIssue {
|
||||
id: string;
|
||||
type: IssueType;
|
||||
severity: 'error' | 'warning' | 'info';
|
||||
category: IssueCategory;
|
||||
node: NodeGraphNode;
|
||||
property?: string;
|
||||
message: string;
|
||||
explanation: string;
|
||||
fix?: DesignFix;
|
||||
}
|
||||
|
||||
enum IssueCategory {
|
||||
ACCESSIBILITY = 'accessibility',
|
||||
CONSISTENCY = 'consistency',
|
||||
LAYOUT = 'layout',
|
||||
INTERACTION = 'interaction',
|
||||
RESPONSIVENESS = 'responsiveness'
|
||||
}
|
||||
|
||||
interface DesignFix {
|
||||
description: string;
|
||||
changes: PropertyChange[];
|
||||
preview?: string;
|
||||
}
|
||||
|
||||
class DesignAnalysisService {
|
||||
private static instance: DesignAnalysisService;
|
||||
private analyzers: DesignAnalyzer[] = [];
|
||||
|
||||
// Analysis
|
||||
async analyzeComponent(component: ComponentModel): Promise<DesignIssue[]>;
|
||||
async analyzePage(page: ComponentModel): Promise<DesignIssue[]>;
|
||||
async analyzeProject(project: ProjectModel): Promise<DesignIssue[]>;
|
||||
|
||||
// Fixes
|
||||
async applyFix(issue: DesignIssue): Promise<void>;
|
||||
async applyAllFixes(issues: DesignIssue[]): Promise<void>;
|
||||
async previewFix(issue: DesignIssue): Promise<FixPreview>;
|
||||
|
||||
// Polish
|
||||
async polishComponent(component: ComponentModel): Promise<PolishResult>;
|
||||
async suggestImprovements(component: ComponentModel): Promise<Improvement[]>;
|
||||
|
||||
// Design system
|
||||
async checkDesignSystem(component: ComponentModel): Promise<DesignSystemIssue[]>;
|
||||
async suggestStyles(component: ComponentModel): Promise<StyleSuggestion[]>;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Design Analyzers
|
||||
|
||||
```typescript
|
||||
// packages/noodl-editor/src/editor/src/services/ai/analyzers/
|
||||
|
||||
// Base analyzer
|
||||
interface DesignAnalyzer {
|
||||
name: string;
|
||||
category: IssueCategory;
|
||||
analyze(component: ComponentModel): DesignIssue[];
|
||||
}
|
||||
|
||||
// Accessibility Analyzer
|
||||
class AccessibilityAnalyzer implements DesignAnalyzer {
|
||||
name = 'Accessibility';
|
||||
category = IssueCategory.ACCESSIBILITY;
|
||||
|
||||
analyze(component: ComponentModel): DesignIssue[] {
|
||||
const issues: DesignIssue[] = [];
|
||||
|
||||
component.forEachNode(node => {
|
||||
// Check contrast
|
||||
if (this.hasTextAndBackground(node)) {
|
||||
const contrast = this.calculateContrast(
|
||||
node.parameters.color,
|
||||
node.parameters.backgroundColor
|
||||
);
|
||||
if (contrast < 4.5) {
|
||||
issues.push({
|
||||
type: 'low-contrast',
|
||||
severity: contrast < 3 ? 'error' : 'warning',
|
||||
category: IssueCategory.ACCESSIBILITY,
|
||||
node,
|
||||
message: `Low color contrast (${contrast.toFixed(1)}:1)`,
|
||||
explanation: 'WCAG requires 4.5:1 for normal text',
|
||||
fix: {
|
||||
description: 'Adjust colors for better contrast',
|
||||
changes: this.suggestContrastFix(node)
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Check touch targets
|
||||
if (this.isInteractive(node)) {
|
||||
const size = this.getSize(node);
|
||||
if (size.width < 44 || size.height < 44) {
|
||||
issues.push({
|
||||
type: 'small-touch-target',
|
||||
severity: 'warning',
|
||||
category: IssueCategory.ACCESSIBILITY,
|
||||
node,
|
||||
message: 'Touch target too small',
|
||||
explanation: 'Minimum 44x44px recommended for touch',
|
||||
fix: {
|
||||
description: 'Increase size to 44x44px minimum',
|
||||
changes: [
|
||||
{ property: 'width', value: Math.max(size.width, 44) },
|
||||
{ property: 'height', value: Math.max(size.height, 44) }
|
||||
]
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Check labels
|
||||
if (this.isFormInput(node) && !this.hasLabel(node)) {
|
||||
issues.push({
|
||||
type: 'missing-label',
|
||||
severity: 'error',
|
||||
category: IssueCategory.ACCESSIBILITY,
|
||||
node,
|
||||
message: 'Form input missing label',
|
||||
explanation: 'Screen readers need labels to identify inputs'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return issues;
|
||||
}
|
||||
}
|
||||
|
||||
// Consistency Analyzer
|
||||
class ConsistencyAnalyzer implements DesignAnalyzer {
|
||||
name = 'Consistency';
|
||||
category = IssueCategory.CONSISTENCY;
|
||||
|
||||
analyze(component: ComponentModel): DesignIssue[] {
|
||||
const issues: DesignIssue[] = [];
|
||||
|
||||
// Collect all values
|
||||
const spacings = this.collectSpacings(component);
|
||||
const colors = this.collectColors(component);
|
||||
const fontSizes = this.collectFontSizes(component);
|
||||
|
||||
// Check for one-offs
|
||||
const spacingOneOffs = this.findOneOffs(spacings, SPACING_SCALE);
|
||||
const colorOneOffs = this.findOneOffs(colors, component.colorStyles);
|
||||
const fontOneOffs = this.findOneOffs(fontSizes, FONT_SCALE);
|
||||
|
||||
// Report issues
|
||||
for (const oneOff of spacingOneOffs) {
|
||||
issues.push({
|
||||
type: 'inconsistent-spacing',
|
||||
severity: 'info',
|
||||
category: IssueCategory.CONSISTENCY,
|
||||
node: oneOff.node,
|
||||
property: oneOff.property,
|
||||
message: `Non-standard spacing: ${oneOff.value}`,
|
||||
explanation: 'Consider using a standard spacing value',
|
||||
fix: {
|
||||
description: `Change to ${oneOff.suggestion}`,
|
||||
changes: [{ property: oneOff.property, value: oneOff.suggestion }]
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return issues;
|
||||
}
|
||||
}
|
||||
|
||||
// Layout Analyzer
|
||||
class LayoutAnalyzer implements DesignAnalyzer {
|
||||
name = 'Layout';
|
||||
category = IssueCategory.LAYOUT;
|
||||
|
||||
analyze(component: ComponentModel): DesignIssue[] {
|
||||
const issues: DesignIssue[] = [];
|
||||
|
||||
// Check alignment
|
||||
const alignmentIssues = this.checkAlignment(component);
|
||||
issues.push(...alignmentIssues);
|
||||
|
||||
// Check whitespace balance
|
||||
const whitespaceIssues = this.checkWhitespace(component);
|
||||
issues.push(...whitespaceIssues);
|
||||
|
||||
// Check visual hierarchy
|
||||
const hierarchyIssues = this.checkHierarchy(component);
|
||||
issues.push(...hierarchyIssues);
|
||||
|
||||
return issues;
|
||||
}
|
||||
}
|
||||
|
||||
// Interaction Analyzer
|
||||
class InteractionAnalyzer implements DesignAnalyzer {
|
||||
name = 'Interaction';
|
||||
category = IssueCategory.INTERACTION;
|
||||
|
||||
analyze(component: ComponentModel): DesignIssue[] {
|
||||
const issues: DesignIssue[] = [];
|
||||
|
||||
component.forEachNode(node => {
|
||||
if (this.isInteractive(node)) {
|
||||
// Check hover state
|
||||
if (!this.hasHoverState(node)) {
|
||||
issues.push({
|
||||
type: 'missing-hover',
|
||||
severity: 'warning',
|
||||
category: IssueCategory.INTERACTION,
|
||||
node,
|
||||
message: 'Missing hover state',
|
||||
explanation: 'Interactive elements should have hover feedback',
|
||||
fix: {
|
||||
description: 'Add subtle hover effect',
|
||||
changes: this.generateHoverState(node)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Check focus state
|
||||
if (!this.hasFocusState(node)) {
|
||||
issues.push({
|
||||
type: 'missing-focus',
|
||||
severity: 'error',
|
||||
category: IssueCategory.INTERACTION,
|
||||
node,
|
||||
message: 'Missing focus state',
|
||||
explanation: 'Keyboard users need visible focus indicators'
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return issues;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. AI Polish Engine
|
||||
|
||||
```typescript
|
||||
// packages/noodl-editor/src/editor/src/services/ai/PolishEngine.ts
|
||||
|
||||
interface PolishResult {
|
||||
before: ComponentSnapshot;
|
||||
after: ComponentSnapshot;
|
||||
changes: Change[];
|
||||
explanation: string;
|
||||
}
|
||||
|
||||
class PolishEngine {
|
||||
async polishComponent(component: ComponentModel): Promise<PolishResult> {
|
||||
// 1. Analyze current state
|
||||
const issues = await DesignAnalysisService.instance.analyzeComponent(component);
|
||||
|
||||
// 2. Apply automatic fixes
|
||||
const autoFixable = issues.filter(i => i.fix && i.severity !== 'error');
|
||||
for (const issue of autoFixable) {
|
||||
await this.applyFix(issue);
|
||||
}
|
||||
|
||||
// 3. Use AI for creative improvements
|
||||
const aiImprovements = await this.getAiImprovements(component);
|
||||
|
||||
// 4. Apply AI suggestions
|
||||
for (const improvement of aiImprovements) {
|
||||
await this.applyImprovement(improvement);
|
||||
}
|
||||
|
||||
return {
|
||||
before: this.originalSnapshot,
|
||||
after: this.currentSnapshot,
|
||||
changes: this.recordedChanges,
|
||||
explanation: this.generateExplanation()
|
||||
};
|
||||
}
|
||||
|
||||
private async getAiImprovements(component: ComponentModel): Promise<Improvement[]> {
|
||||
const prompt = `Analyze this Noodl component and suggest design improvements:
|
||||
|
||||
Component structure:
|
||||
${this.serializeComponent(component)}
|
||||
|
||||
Current styles:
|
||||
${this.serializeStyles(component)}
|
||||
|
||||
Suggest improvements for:
|
||||
1. Visual hierarchy
|
||||
2. Whitespace and breathing room
|
||||
3. Color harmony
|
||||
4. Typography refinement
|
||||
5. Micro-interactions
|
||||
|
||||
Output JSON array of improvements with property changes.`;
|
||||
|
||||
const response = await this.anthropicClient.complete(prompt);
|
||||
return JSON.parse(response);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. UI Components
|
||||
|
||||
#### Design Review Panel
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ Design Review [×] │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ 📊 Overview │
|
||||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ 🔴 2 Errors 🟡 5 Warnings 🔵 3 Info │ │
|
||||
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ 🔴 ERRORS [Fix All (2)] │
|
||||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ ♿ Low color contrast on "Submit" button [Fix] │ │
|
||||
│ │ Contrast ratio 2.1:1, needs 4.5:1 │ │
|
||||
│ ├─────────────────────────────────────────────────────────────────┤ │
|
||||
│ │ ♿ Missing label on email input [Fix] │ │
|
||||
│ │ Screen readers cannot identify this input │ │
|
||||
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ 🟡 WARNINGS [Fix All (5)] │
|
||||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ 📐 Inconsistent spacing (12px vs 16px scale) [Fix] │ │
|
||||
│ │ 👆 Touch target too small (32x32px) [Fix] │ │
|
||||
│ │ ✨ Missing hover state on buttons [Fix] │ │
|
||||
│ │ ... │ │
|
||||
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ [Analyze Again] [✨ Polish All] │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
#### Polish Preview
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ ✨ Polish Preview [×] │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ BEFORE AFTER │
|
||||
│ ┌────────────────────────┐ ┌────────────────────────┐ │
|
||||
│ │ ┌──────────────────┐ │ │ ┌──────────────────┐ │ │
|
||||
│ │ │ Cramped card │ │ │ │ │ │ │
|
||||
│ │ │ No shadow │ │ → │ │ Polished card │ │ │
|
||||
│ │ │ Basic button │ │ │ │ with shadow │ │ │
|
||||
│ │ └──────────────────┘ │ │ │ and spacing │ │ │
|
||||
│ │ │ │ └──────────────────┘ │ │
|
||||
│ └────────────────────────┘ └────────────────────────┘ │
|
||||
│ │
|
||||
│ Changes Applied: │
|
||||
│ • Added 24px padding to card │
|
||||
│ • Added subtle shadow (0 2px 8px rgba(0,0,0,0.1)) │
|
||||
│ • Increased button padding (12px 24px) │
|
||||
│ • Added hover state with 0.95 scale │
|
||||
│ • Adjusted border radius to 12px │
|
||||
│ │
|
||||
│ [Revert] [Apply Polish] │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 5. Design Rules Engine
|
||||
|
||||
```typescript
|
||||
// packages/noodl-editor/src/editor/src/services/ai/DesignRules.ts
|
||||
|
||||
interface DesignRule {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
category: IssueCategory;
|
||||
severity: 'error' | 'warning' | 'info';
|
||||
check: (node: NodeGraphNode, context: DesignContext) => RuleViolation | null;
|
||||
fix?: (violation: RuleViolation) => PropertyChange[];
|
||||
}
|
||||
|
||||
const DESIGN_RULES: DesignRule[] = [
|
||||
// Accessibility
|
||||
{
|
||||
id: 'min-contrast',
|
||||
name: 'Minimum Color Contrast',
|
||||
description: 'Text must have sufficient contrast with background',
|
||||
category: IssueCategory.ACCESSIBILITY,
|
||||
severity: 'error',
|
||||
check: (node, ctx) => {
|
||||
if (!hasTextAndBackground(node)) return null;
|
||||
const contrast = calculateContrast(node.parameters.color, node.parameters.backgroundColor);
|
||||
if (contrast < 4.5) {
|
||||
return { node, contrast, required: 4.5 };
|
||||
}
|
||||
return null;
|
||||
},
|
||||
fix: (violation) => suggestContrastFix(violation.node, violation.required)
|
||||
},
|
||||
|
||||
{
|
||||
id: 'min-touch-target',
|
||||
name: 'Minimum Touch Target Size',
|
||||
description: 'Interactive elements must be at least 44x44px',
|
||||
category: IssueCategory.ACCESSIBILITY,
|
||||
severity: 'warning',
|
||||
check: (node) => {
|
||||
if (!isInteractive(node)) return null;
|
||||
const size = getSize(node);
|
||||
if (size.width < 44 || size.height < 44) {
|
||||
return { node, size, required: { width: 44, height: 44 } };
|
||||
}
|
||||
return null;
|
||||
},
|
||||
fix: (violation) => [
|
||||
{ property: 'width', value: Math.max(violation.size.width, 44) },
|
||||
{ property: 'height', value: Math.max(violation.size.height, 44) }
|
||||
]
|
||||
},
|
||||
|
||||
// Consistency
|
||||
{
|
||||
id: 'spacing-scale',
|
||||
name: 'Use Spacing Scale',
|
||||
description: 'Spacing should follow the design system scale',
|
||||
category: IssueCategory.CONSISTENCY,
|
||||
severity: 'info',
|
||||
check: (node) => {
|
||||
const spacing = getSpacingValues(node);
|
||||
const nonStandard = spacing.filter(s => !SPACING_SCALE.includes(s));
|
||||
if (nonStandard.length > 0) {
|
||||
return { node, nonStandard, scale: SPACING_SCALE };
|
||||
}
|
||||
return null;
|
||||
},
|
||||
fix: (violation) => violation.nonStandard.map(s => ({
|
||||
property: s.property,
|
||||
value: findClosest(s.value, SPACING_SCALE)
|
||||
}))
|
||||
},
|
||||
|
||||
// Interaction
|
||||
{
|
||||
id: 'hover-state',
|
||||
name: 'Interactive Hover State',
|
||||
description: 'Interactive elements should have hover feedback',
|
||||
category: IssueCategory.INTERACTION,
|
||||
severity: 'warning',
|
||||
check: (node) => {
|
||||
if (isInteractive(node) && !hasHoverState(node)) {
|
||||
return { node };
|
||||
}
|
||||
return null;
|
||||
},
|
||||
fix: (violation) => generateDefaultHoverState(violation.node)
|
||||
},
|
||||
|
||||
// ... more rules
|
||||
];
|
||||
```
|
||||
|
||||
## Files to Create
|
||||
|
||||
1. `packages/noodl-editor/src/editor/src/services/DesignAnalysisService.ts`
|
||||
2. `packages/noodl-editor/src/editor/src/services/ai/analyzers/AccessibilityAnalyzer.ts`
|
||||
3. `packages/noodl-editor/src/editor/src/services/ai/analyzers/ConsistencyAnalyzer.ts`
|
||||
4. `packages/noodl-editor/src/editor/src/services/ai/analyzers/LayoutAnalyzer.ts`
|
||||
5. `packages/noodl-editor/src/editor/src/services/ai/analyzers/InteractionAnalyzer.ts`
|
||||
6. `packages/noodl-editor/src/editor/src/services/ai/PolishEngine.ts`
|
||||
7. `packages/noodl-editor/src/editor/src/services/ai/DesignRules.ts`
|
||||
8. `packages/noodl-core-ui/src/components/ai/DesignReviewPanel/DesignReviewPanel.tsx`
|
||||
9. `packages/noodl-core-ui/src/components/ai/DesignIssueCard/DesignIssueCard.tsx`
|
||||
10. `packages/noodl-core-ui/src/components/ai/PolishPreview/PolishPreview.tsx`
|
||||
|
||||
## Files to Modify
|
||||
|
||||
1. `packages/noodl-editor/src/editor/src/pages/EditorPage/EditorPage.tsx`
|
||||
- Add Design Review panel toggle
|
||||
- Add menu option
|
||||
|
||||
2. `packages/noodl-editor/src/editor/src/views/panels/propertiespanel/`
|
||||
- Add issue indicators on properties
|
||||
- Quick fix buttons
|
||||
|
||||
3. `packages/noodl-editor/src/editor/src/views/nodegrapheditor.js`
|
||||
- Highlight nodes with issues
|
||||
- Add "Polish" context menu
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### Phase 1: Analysis Infrastructure
|
||||
1. Create DesignAnalysisService
|
||||
2. Define issue types and categories
|
||||
3. Create analyzer base class
|
||||
4. Implement fix application
|
||||
|
||||
### Phase 2: Core Analyzers
|
||||
1. Implement AccessibilityAnalyzer
|
||||
2. Implement ConsistencyAnalyzer
|
||||
3. Implement LayoutAnalyzer
|
||||
4. Implement InteractionAnalyzer
|
||||
|
||||
### Phase 3: Polish Engine
|
||||
1. Create PolishEngine
|
||||
2. Implement auto-fix application
|
||||
3. Add AI improvement suggestions
|
||||
4. Generate explanations
|
||||
|
||||
### Phase 4: UI - Review Panel
|
||||
1. Create DesignReviewPanel
|
||||
2. Create DesignIssueCard
|
||||
3. Group issues by category
|
||||
4. Add fix buttons
|
||||
|
||||
### Phase 5: UI - Polish Preview
|
||||
1. Create PolishPreview
|
||||
2. Show before/after
|
||||
3. List changes
|
||||
4. Apply/revert actions
|
||||
|
||||
### Phase 6: Integration
|
||||
1. Add to editor menus
|
||||
2. Highlight issues on canvas
|
||||
3. Add keyboard shortcuts
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
- [ ] Accessibility issues detected
|
||||
- [ ] Contrast calculation accurate
|
||||
- [ ] Touch target check works
|
||||
- [ ] Consistency issues found
|
||||
- [ ] Fixes don't break layout
|
||||
- [ ] Polish improves design
|
||||
- [ ] Preview accurate
|
||||
- [ ] Undo works
|
||||
- [ ] Performance acceptable
|
||||
- [ ] Works on all component types
|
||||
|
||||
## Dependencies
|
||||
|
||||
- AI-001 (AI Project Scaffolding) - for AnthropicClient
|
||||
- AI-003 (Natural Language Editing) - for change application
|
||||
|
||||
## Blocked By
|
||||
|
||||
- AI-001
|
||||
|
||||
## Blocks
|
||||
|
||||
- None (final task in AI series)
|
||||
|
||||
## Estimated Effort
|
||||
|
||||
- Analysis service: 4-5 hours
|
||||
- Accessibility analyzer: 4-5 hours
|
||||
- Consistency analyzer: 3-4 hours
|
||||
- Layout analyzer: 3-4 hours
|
||||
- Interaction analyzer: 3-4 hours
|
||||
- Polish engine: 5-6 hours
|
||||
- UI review panel: 4-5 hours
|
||||
- UI polish preview: 3-4 hours
|
||||
- Integration: 3-4 hours
|
||||
- **Total: 32-41 hours**
|
||||
|
||||
## Success Criteria
|
||||
|
||||
1. Issues detected accurately
|
||||
2. Fixes don't break functionality
|
||||
3. Polish improves design quality
|
||||
4. Accessibility issues caught
|
||||
5. One-click fixes work
|
||||
6. Preview shows accurate changes
|
||||
|
||||
## Design Rules Categories
|
||||
|
||||
### Accessibility (WCAG)
|
||||
- Color contrast (4.5:1 text, 3:1 large)
|
||||
- Touch targets (44x44px)
|
||||
- Focus indicators
|
||||
- Label associations
|
||||
- Alt text for images
|
||||
|
||||
### Consistency
|
||||
- Spacing scale adherence
|
||||
- Color from palette
|
||||
- Typography scale
|
||||
- Border radius consistency
|
||||
- Shadow consistency
|
||||
|
||||
### Layout
|
||||
- Alignment on grid
|
||||
- Balanced whitespace
|
||||
- Visual hierarchy
|
||||
- Content grouping
|
||||
|
||||
### Interaction
|
||||
- Hover states
|
||||
- Focus states
|
||||
- Active states
|
||||
- Loading states
|
||||
- Error states
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- Design system integration
|
||||
- Custom rule creation
|
||||
- Team design standards
|
||||
- A/B testing suggestions
|
||||
- Animation review
|
||||
- Performance impact analysis
|
||||
@@ -0,0 +1,425 @@
|
||||
# AI Series: AI-Powered Development
|
||||
|
||||
## Overview
|
||||
|
||||
The AI series transforms OpenNoodl from a visual development tool into an intelligent development partner. Users can describe what they want to build, receive contextual suggestions, edit with natural language, and get automatic design feedback—all powered by Claude AI.
|
||||
|
||||
## Target Environment
|
||||
|
||||
- **Editor**: React 19 version only
|
||||
- **Runtime**: Not affected
|
||||
- **API**: Anthropic Claude API
|
||||
- **Fallback**: Graceful degradation without API
|
||||
|
||||
## Task Dependency Graph
|
||||
|
||||
```
|
||||
AI-001 (Project Scaffolding)
|
||||
│
|
||||
├──────────────────────────┐
|
||||
↓ ↓
|
||||
AI-002 (Suggestions) AI-003 (NL Editing)
|
||||
│ │
|
||||
└──────────┬───────────────┘
|
||||
↓
|
||||
AI-004 (Design Assistance)
|
||||
```
|
||||
|
||||
## Task Summary
|
||||
|
||||
| Task ID | Name | Est. Hours | Priority |
|
||||
|---------|------|------------|----------|
|
||||
| AI-001 | AI Project Scaffolding | 32-41 | Critical |
|
||||
| AI-002 | AI Component Suggestions | 27-34 | High |
|
||||
| AI-003 | Natural Language Editing | 30-38 | High |
|
||||
| AI-004 | AI Design Assistance | 32-41 | Medium |
|
||||
|
||||
**Total Estimated: 121-154 hours**
|
||||
|
||||
## Implementation Order
|
||||
|
||||
### Phase 1: Foundation (Weeks 1-3)
|
||||
1. **AI-001** - Project scaffolding with AI
|
||||
- Establishes Anthropic API integration
|
||||
- Creates core AI services
|
||||
- Delivers immediate user value
|
||||
|
||||
### Phase 2: In-Editor Intelligence (Weeks 4-6)
|
||||
2. **AI-002** - Component suggestions
|
||||
- Context-aware recommendations
|
||||
- Pattern library foundation
|
||||
3. **AI-003** - Natural language editing
|
||||
- Command palette for AI edits
|
||||
- Change preview and application
|
||||
|
||||
### Phase 3: Design Quality (Weeks 7-8)
|
||||
4. **AI-004** - Design assistance
|
||||
- Automated design review
|
||||
- Polish and improvements
|
||||
|
||||
## Existing Infrastructure
|
||||
|
||||
### AiAssistantModel
|
||||
|
||||
```typescript
|
||||
// Current AI node system
|
||||
class AiAssistantModel {
|
||||
templates: AiTemplate[]; // REST, Function, Form Validation, etc.
|
||||
|
||||
createNode(templateId, parentModel, pos);
|
||||
createContext(node);
|
||||
send(context);
|
||||
}
|
||||
```
|
||||
|
||||
### AI Templates
|
||||
|
||||
```typescript
|
||||
docsTemplates = [
|
||||
{ label: 'REST API', template: 'rest' },
|
||||
{ label: 'Form Validation', template: 'function-form-validation' },
|
||||
{ label: 'AI Function', template: 'function' },
|
||||
{ label: 'Write to database', template: 'function-crud' }
|
||||
];
|
||||
```
|
||||
|
||||
### Template Registry
|
||||
|
||||
```typescript
|
||||
// Project template system
|
||||
templateRegistry.list({}); // List available templates
|
||||
templateRegistry.download({ templateUrl }); // Download template
|
||||
```
|
||||
|
||||
### LocalProjectsModel
|
||||
|
||||
```typescript
|
||||
// Project creation
|
||||
LocalProjectsModel.newProject(callback, {
|
||||
name,
|
||||
path,
|
||||
projectTemplate
|
||||
});
|
||||
```
|
||||
|
||||
## New Architecture
|
||||
|
||||
### Core AI Services
|
||||
|
||||
```
|
||||
packages/noodl-editor/src/editor/src/services/
|
||||
├── ai/
|
||||
│ ├── AnthropicClient.ts # Claude API wrapper
|
||||
│ ├── prompts/ # Prompt templates
|
||||
│ │ ├── scaffolding.ts
|
||||
│ │ ├── suggestions.ts
|
||||
│ │ └── editing.ts
|
||||
│ ├── ContextAnalyzer.ts # Component analysis
|
||||
│ ├── PatternLibrary.ts # Known patterns
|
||||
│ ├── CommandParser.ts # NL command parsing
|
||||
│ ├── ChangeGenerator.ts # Generate changes
|
||||
│ └── analyzers/ # Design analyzers
|
||||
│ ├── AccessibilityAnalyzer.ts
|
||||
│ ├── ConsistencyAnalyzer.ts
|
||||
│ └── ...
|
||||
├── AiScaffoldingService.ts
|
||||
├── AiSuggestionService.ts
|
||||
├── NaturalLanguageService.ts
|
||||
└── DesignAnalysisService.ts
|
||||
```
|
||||
|
||||
### Service Hierarchy
|
||||
|
||||
| Service | Purpose |
|
||||
|---------|---------|
|
||||
| AnthropicClient | Claude API communication |
|
||||
| AiScaffoldingService | Project generation |
|
||||
| AiSuggestionService | Context-aware suggestions |
|
||||
| NaturalLanguageService | Command parsing & execution |
|
||||
| DesignAnalysisService | Design review & fixes |
|
||||
|
||||
## API Integration
|
||||
|
||||
### Anthropic Client
|
||||
|
||||
```typescript
|
||||
class AnthropicClient {
|
||||
private apiKey: string;
|
||||
private model = 'claude-sonnet-4-20250514';
|
||||
|
||||
async complete(prompt: string, options?: CompletionOptions): Promise<string>;
|
||||
async chat(messages: Message[]): Promise<Message>;
|
||||
async stream(prompt: string, onChunk: (chunk: string) => void): Promise<void>;
|
||||
}
|
||||
```
|
||||
|
||||
### API Key Management
|
||||
|
||||
```typescript
|
||||
// Settings storage
|
||||
interface AiSettings {
|
||||
apiKey: string; // Stored securely
|
||||
enabled: boolean;
|
||||
features: {
|
||||
scaffolding: boolean;
|
||||
suggestions: boolean;
|
||||
naturalLanguage: boolean;
|
||||
designAssistance: boolean;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Key User Flows
|
||||
|
||||
### 1. Create Project from Description
|
||||
|
||||
```
|
||||
User opens "New Project"
|
||||
↓
|
||||
Selects "Describe your project"
|
||||
↓
|
||||
Types: "A task management app with kanban board"
|
||||
↓
|
||||
AI generates scaffold
|
||||
↓
|
||||
User previews & refines via chat
|
||||
↓
|
||||
Creates actual project
|
||||
```
|
||||
|
||||
### 2. Get Suggestions While Building
|
||||
|
||||
```
|
||||
User adds TextInput node
|
||||
↓
|
||||
System detects incomplete form pattern
|
||||
↓
|
||||
Shows suggestion: "Add form validation?"
|
||||
↓
|
||||
User clicks "Apply"
|
||||
↓
|
||||
Validation nodes added automatically
|
||||
```
|
||||
|
||||
### 3. Edit with Natural Language
|
||||
|
||||
```
|
||||
User selects Button node
|
||||
↓
|
||||
Presses Cmd+K
|
||||
↓
|
||||
Types: "Make it larger with a hover effect"
|
||||
↓
|
||||
Preview shows changes
|
||||
↓
|
||||
User clicks "Apply"
|
||||
```
|
||||
|
||||
### 4. Design Review & Polish
|
||||
|
||||
```
|
||||
User opens Design Review panel
|
||||
↓
|
||||
AI analyzes component
|
||||
↓
|
||||
Shows: "2 accessibility issues, 3 warnings"
|
||||
↓
|
||||
User clicks "Fix All" or "Polish"
|
||||
↓
|
||||
Changes applied automatically
|
||||
```
|
||||
|
||||
## UI Components to Create
|
||||
|
||||
| Component | Package | Purpose |
|
||||
|-----------|---------|---------|
|
||||
| AiProjectModal | noodl-core-ui | Project scaffolding UI |
|
||||
| ScaffoldPreview | noodl-core-ui | Preview generated structure |
|
||||
| SuggestionHint | noodl-core-ui | Inline suggestion display |
|
||||
| SuggestionPanel | noodl-core-ui | Full suggestions list |
|
||||
| CommandPalette | noodl-core-ui | NL command input |
|
||||
| ChangePreview | noodl-core-ui | Show pending changes |
|
||||
| DesignReviewPanel | noodl-core-ui | Design issues list |
|
||||
| PolishPreview | noodl-core-ui | Before/after comparison |
|
||||
|
||||
## Prompt Engineering
|
||||
|
||||
### System Prompts
|
||||
|
||||
```typescript
|
||||
// Scaffolding
|
||||
const SCAFFOLD_SYSTEM = `You are an expert Noodl application architect.
|
||||
Generate detailed project scaffolds for visual low-code applications.
|
||||
Consider: UX flow, data management, reusability, performance.`;
|
||||
|
||||
// Suggestions
|
||||
const SUGGESTION_SYSTEM = `You analyze Noodl components and suggest
|
||||
improvements. Focus on: pattern completion, best practices,
|
||||
common UI patterns, data handling.`;
|
||||
|
||||
// Natural Language
|
||||
const NL_SYSTEM = `You parse natural language commands for editing
|
||||
Noodl visual components. Output structured changes that can be
|
||||
applied to the node graph.`;
|
||||
|
||||
// Design
|
||||
const DESIGN_SYSTEM = `You are a design expert analyzing Noodl
|
||||
components for accessibility, consistency, and visual quality.
|
||||
Suggest concrete property changes.`;
|
||||
```
|
||||
|
||||
### Context Serialization
|
||||
|
||||
```typescript
|
||||
// Serialize component for AI context
|
||||
function serializeForAi(component: ComponentModel): string {
|
||||
return JSON.stringify({
|
||||
name: component.name,
|
||||
nodes: component.nodes.map(n => ({
|
||||
type: n.type.localName,
|
||||
id: n.id,
|
||||
parameters: n.parameters,
|
||||
children: n.children?.map(c => c.id)
|
||||
})),
|
||||
connections: component.connections.map(c => ({
|
||||
from: `${c.sourceNode.id}.${c.sourcePort}`,
|
||||
to: `${c.targetNode.id}.${c.targetPort}`
|
||||
}))
|
||||
}, null, 2);
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Token Management
|
||||
- Keep prompts concise
|
||||
- Truncate large components
|
||||
- Cache common patterns locally
|
||||
- Batch similar requests
|
||||
|
||||
### Response Times
|
||||
- Scaffold generation: < 30 seconds
|
||||
- Suggestions: < 500ms (local), < 3s (AI)
|
||||
- NL parsing: < 3 seconds
|
||||
- Design analysis: < 5 seconds
|
||||
|
||||
### Offline Support
|
||||
- Local pattern library for suggestions
|
||||
- Cached design rules
|
||||
- Basic NL patterns
|
||||
- Graceful degradation
|
||||
|
||||
## Settings & Configuration
|
||||
|
||||
```typescript
|
||||
interface AiConfiguration {
|
||||
// API
|
||||
apiKey: string;
|
||||
apiEndpoint: string; // For custom/proxy
|
||||
model: string;
|
||||
|
||||
// Features
|
||||
features: {
|
||||
scaffolding: boolean;
|
||||
suggestions: boolean;
|
||||
naturalLanguage: boolean;
|
||||
designAssistance: boolean;
|
||||
};
|
||||
|
||||
// Suggestions
|
||||
suggestions: {
|
||||
enabled: boolean;
|
||||
frequency: 'always' | 'sometimes' | 'manual';
|
||||
showInline: boolean;
|
||||
showPanel: boolean;
|
||||
};
|
||||
|
||||
// Design
|
||||
design: {
|
||||
autoAnalyze: boolean;
|
||||
showInCanvas: boolean;
|
||||
strictAccessibility: boolean;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Unit Tests
|
||||
- Prompt generation
|
||||
- Response parsing
|
||||
- Pattern matching
|
||||
- Change generation
|
||||
|
||||
### Integration Tests
|
||||
- Full scaffold flow
|
||||
- Suggestion pipeline
|
||||
- NL command execution
|
||||
- Design analysis
|
||||
|
||||
### Manual Testing
|
||||
- Various project descriptions
|
||||
- Edge case components
|
||||
- Complex NL commands
|
||||
- Accessibility scenarios
|
||||
|
||||
## Cline Usage Notes
|
||||
|
||||
### Before Starting Each Task
|
||||
|
||||
1. Read existing AI infrastructure:
|
||||
- `AiAssistantModel.ts`
|
||||
- Related AI components in `noodl-core-ui`
|
||||
2. Understand prompt patterns from existing templates
|
||||
3. Review how changes are applied to node graph
|
||||
|
||||
### Key Integration Points
|
||||
|
||||
1. **Node Graph**: All changes go through `NodeGraphModel`
|
||||
2. **Undo/Redo**: Must integrate with `UndoManager`
|
||||
3. **Project Model**: Scaffolds create full project structure
|
||||
4. **Settings**: Store in `EditorSettings`
|
||||
|
||||
### API Key Handling
|
||||
|
||||
- Never log API keys
|
||||
- Store securely (electron safeStorage)
|
||||
- Clear from memory after use
|
||||
- Support environment variable override
|
||||
|
||||
## Success Criteria (Series Complete)
|
||||
|
||||
1. ✅ Users can create projects from descriptions
|
||||
2. ✅ Contextual suggestions appear while building
|
||||
3. ✅ Natural language commands modify components
|
||||
4. ✅ Design issues automatically detected
|
||||
5. ✅ One-click fixes for common issues
|
||||
6. ✅ Works offline with reduced functionality
|
||||
|
||||
## Future Work (Post-AI Series)
|
||||
|
||||
The AI series enables:
|
||||
- **Voice Control**: Voice input for commands
|
||||
- **Image to Project**: Screenshot to scaffold
|
||||
- **Code Generation**: Export to React/Vue
|
||||
- **AI Debugging**: Debug logic issues
|
||||
- **Performance Optimization**: AI-suggested optimizations
|
||||
|
||||
## Files in This Series
|
||||
|
||||
- `AI-001-ai-project-scaffolding.md`
|
||||
- `AI-002-ai-component-suggestions.md`
|
||||
- `AI-003-natural-language-editing.md`
|
||||
- `AI-004-ai-design-assistance.md`
|
||||
- `AI-OVERVIEW.md` (this file)
|
||||
|
||||
## External Dependencies
|
||||
|
||||
### Anthropic API
|
||||
- Model: claude-sonnet-4-20250514 (default)
|
||||
- Rate limits: Handle gracefully
|
||||
- Costs: Optimize token usage
|
||||
|
||||
### No Additional Packages Required
|
||||
- Uses existing HTTP infrastructure
|
||||
- No additional AI libraries needed
|
||||
@@ -0,0 +1,579 @@
|
||||
# DEPLOY-001: One-Click Deploy Integrations
|
||||
|
||||
## Overview
|
||||
|
||||
Add one-click deployment to popular hosting platforms (Netlify, Vercel, GitHub Pages). Users can deploy their frontend directly from the editor without manual file handling or CLI tools.
|
||||
|
||||
## Context
|
||||
|
||||
Currently, deployment requires:
|
||||
1. Deploy to local folder
|
||||
2. Manually upload to hosting platform
|
||||
3. Configure hosting settings separately
|
||||
4. Repeat for every deployment
|
||||
|
||||
This friction discourages frequent deployments and makes it harder for non-technical users to share their work.
|
||||
|
||||
### Existing Infrastructure
|
||||
|
||||
From `deployer.ts`:
|
||||
```typescript
|
||||
export async function deployToFolder({
|
||||
project,
|
||||
direntry,
|
||||
environment,
|
||||
baseUrl,
|
||||
envVariables,
|
||||
runtimeType = 'deploy'
|
||||
}: DeployToFolderOptions)
|
||||
```
|
||||
|
||||
From `compilation.ts`:
|
||||
```typescript
|
||||
class Compilation {
|
||||
deployToFolder(direntry, options): Promise<void>;
|
||||
// Build scripts for pre/post deploy
|
||||
}
|
||||
```
|
||||
|
||||
From `DeployToFolderTab.tsx`:
|
||||
- Current UI for folder selection
|
||||
- Environment selection dropdown
|
||||
|
||||
## Requirements
|
||||
|
||||
### Functional Requirements
|
||||
|
||||
1. **Platform Integrations**
|
||||
- Netlify (OAuth + API)
|
||||
- Vercel (OAuth + API)
|
||||
- GitHub Pages (via GitHub API)
|
||||
- Cloudflare Pages (OAuth + API)
|
||||
|
||||
2. **Deploy Flow**
|
||||
- One-click deploy from editor
|
||||
- Platform selection dropdown
|
||||
- Site/project selection or creation
|
||||
- Environment variables configuration
|
||||
- Deploy progress indication
|
||||
|
||||
3. **Site Management**
|
||||
- List user's sites on each platform
|
||||
- Create new site from editor
|
||||
- Link project to existing site
|
||||
- View deployment history
|
||||
|
||||
4. **Configuration**
|
||||
- Environment variables per platform
|
||||
- Custom domain display
|
||||
- Build settings (if needed)
|
||||
- Deploy hooks
|
||||
|
||||
5. **Status & History**
|
||||
- Deploy status in editor
|
||||
- Link to live site
|
||||
- Deployment history
|
||||
- Rollback option (if supported)
|
||||
|
||||
### Non-Functional Requirements
|
||||
|
||||
- Deploy completes in < 2 minutes
|
||||
- Works with existing deploy-to-folder logic
|
||||
- Secure token storage
|
||||
- Clear error messages
|
||||
|
||||
## Technical Approach
|
||||
|
||||
### 1. Deploy Service Architecture
|
||||
|
||||
```typescript
|
||||
// packages/noodl-editor/src/editor/src/services/DeployService.ts
|
||||
|
||||
interface DeployTarget {
|
||||
id: string;
|
||||
name: string;
|
||||
platform: DeployPlatform;
|
||||
siteId: string;
|
||||
siteName: string;
|
||||
url: string;
|
||||
customDomain?: string;
|
||||
lastDeployedAt?: string;
|
||||
envVariables?: Record<string, string>;
|
||||
}
|
||||
|
||||
enum DeployPlatform {
|
||||
NETLIFY = 'netlify',
|
||||
VERCEL = 'vercel',
|
||||
GITHUB_PAGES = 'github_pages',
|
||||
CLOUDFLARE = 'cloudflare',
|
||||
LOCAL_FOLDER = 'local_folder'
|
||||
}
|
||||
|
||||
interface DeployResult {
|
||||
success: boolean;
|
||||
deployId: string;
|
||||
url: string;
|
||||
buildTime: number;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
class DeployService {
|
||||
private static instance: DeployService;
|
||||
private providers: Map<DeployPlatform, DeployProvider> = new Map();
|
||||
|
||||
// Provider management
|
||||
registerProvider(provider: DeployProvider): void;
|
||||
getProvider(platform: DeployPlatform): DeployProvider;
|
||||
|
||||
// Authentication
|
||||
async authenticate(platform: DeployPlatform): Promise<void>;
|
||||
async disconnect(platform: DeployPlatform): Promise<void>;
|
||||
isAuthenticated(platform: DeployPlatform): boolean;
|
||||
|
||||
// Site management
|
||||
async listSites(platform: DeployPlatform): Promise<Site[]>;
|
||||
async createSite(platform: DeployPlatform, name: string): Promise<Site>;
|
||||
async linkSite(project: ProjectModel, target: DeployTarget): Promise<void>;
|
||||
|
||||
// Deployment
|
||||
async deploy(project: ProjectModel, target: DeployTarget): Promise<DeployResult>;
|
||||
async getDeployStatus(deployId: string): Promise<DeployStatus>;
|
||||
async getDeployHistory(target: DeployTarget): Promise<Deployment[]>;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Deploy Provider Interface
|
||||
|
||||
```typescript
|
||||
// packages/noodl-editor/src/editor/src/services/deploy/DeployProvider.ts
|
||||
|
||||
interface DeployProvider {
|
||||
readonly platform: DeployPlatform;
|
||||
readonly name: string;
|
||||
readonly icon: string;
|
||||
|
||||
// Authentication
|
||||
authenticate(): Promise<AuthResult>;
|
||||
disconnect(): Promise<void>;
|
||||
isAuthenticated(): boolean;
|
||||
getUser(): Promise<User | null>;
|
||||
|
||||
// Sites
|
||||
listSites(): Promise<Site[]>;
|
||||
createSite(name: string, options?: CreateSiteOptions): Promise<Site>;
|
||||
deleteSite(siteId: string): Promise<void>;
|
||||
|
||||
// Deployment
|
||||
deploy(siteId: string, files: DeployFiles): Promise<DeployResult>;
|
||||
getDeployStatus(deployId: string): Promise<DeployStatus>;
|
||||
getDeployHistory(siteId: string): Promise<Deployment[]>;
|
||||
cancelDeploy(deployId: string): Promise<void>;
|
||||
|
||||
// Configuration
|
||||
getEnvVariables(siteId: string): Promise<Record<string, string>>;
|
||||
setEnvVariables(siteId: string, vars: Record<string, string>): Promise<void>;
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Netlify Provider
|
||||
|
||||
```typescript
|
||||
// packages/noodl-editor/src/editor/src/services/deploy/providers/NetlifyProvider.ts
|
||||
|
||||
class NetlifyProvider implements DeployProvider {
|
||||
platform = DeployPlatform.NETLIFY;
|
||||
name = 'Netlify';
|
||||
icon = 'netlify-icon.svg';
|
||||
|
||||
private clientId = 'YOUR_NETLIFY_CLIENT_ID';
|
||||
private redirectUri = 'noodl://netlify-callback';
|
||||
private token: string | null = null;
|
||||
|
||||
async authenticate(): Promise<AuthResult> {
|
||||
// OAuth flow
|
||||
const authUrl = `https://app.netlify.com/authorize?` +
|
||||
`client_id=${this.clientId}&` +
|
||||
`response_type=token&` +
|
||||
`redirect_uri=${encodeURIComponent(this.redirectUri)}`;
|
||||
|
||||
// Open in browser, handle callback via deep link
|
||||
const token = await this.handleOAuthCallback(authUrl);
|
||||
this.token = token;
|
||||
|
||||
await this.storeToken(token);
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
async listSites(): Promise<Site[]> {
|
||||
const response = await fetch('https://api.netlify.com/api/v1/sites', {
|
||||
headers: { Authorization: `Bearer ${this.token}` }
|
||||
});
|
||||
|
||||
const sites = await response.json();
|
||||
return sites.map(s => ({
|
||||
id: s.id,
|
||||
name: s.name,
|
||||
url: s.ssl_url || s.url,
|
||||
customDomain: s.custom_domain,
|
||||
updatedAt: s.updated_at
|
||||
}));
|
||||
}
|
||||
|
||||
async createSite(name: string): Promise<Site> {
|
||||
const response = await fetch('https://api.netlify.com/api/v1/sites', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ name })
|
||||
});
|
||||
|
||||
const site = await response.json();
|
||||
return {
|
||||
id: site.id,
|
||||
name: site.name,
|
||||
url: site.ssl_url || site.url
|
||||
};
|
||||
}
|
||||
|
||||
async deploy(siteId: string, files: DeployFiles): Promise<DeployResult> {
|
||||
// Create deploy
|
||||
const deploy = await this.createDeploy(siteId);
|
||||
|
||||
// Upload files using Netlify's digest-based upload
|
||||
const fileHashes = await this.calculateHashes(files);
|
||||
const required = await this.getRequiredFiles(deploy.id, fileHashes);
|
||||
|
||||
for (const file of required) {
|
||||
await this.uploadFile(deploy.id, file);
|
||||
}
|
||||
|
||||
// Finalize deploy
|
||||
return await this.finalizeDeploy(deploy.id);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Vercel Provider
|
||||
|
||||
```typescript
|
||||
// packages/noodl-editor/src/editor/src/services/deploy/providers/VercelProvider.ts
|
||||
|
||||
class VercelProvider implements DeployProvider {
|
||||
platform = DeployPlatform.VERCEL;
|
||||
name = 'Vercel';
|
||||
icon = 'vercel-icon.svg';
|
||||
|
||||
private clientId = 'YOUR_VERCEL_CLIENT_ID';
|
||||
private token: string | null = null;
|
||||
|
||||
async authenticate(): Promise<AuthResult> {
|
||||
// Vercel uses OAuth 2.0
|
||||
const state = this.generateState();
|
||||
const authUrl = `https://vercel.com/integrations/noodl/new?` +
|
||||
`state=${state}`;
|
||||
|
||||
const token = await this.handleOAuthCallback(authUrl);
|
||||
this.token = token;
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
async deploy(projectId: string, files: DeployFiles): Promise<DeployResult> {
|
||||
// Vercel deployment API
|
||||
const deployment = await fetch('https://api.vercel.com/v13/deployments', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: projectId,
|
||||
files: await this.prepareFiles(files),
|
||||
projectSettings: {
|
||||
framework: null // Static site
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
const result = await deployment.json();
|
||||
return {
|
||||
success: true,
|
||||
deployId: result.id,
|
||||
url: `https://${result.url}`,
|
||||
buildTime: 0
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. GitHub Pages Provider
|
||||
|
||||
```typescript
|
||||
// packages/noodl-editor/src/editor/src/services/deploy/providers/GitHubPagesProvider.ts
|
||||
|
||||
class GitHubPagesProvider implements DeployProvider {
|
||||
platform = DeployPlatform.GITHUB_PAGES;
|
||||
name = 'GitHub Pages';
|
||||
icon = 'github-icon.svg';
|
||||
|
||||
async authenticate(): Promise<AuthResult> {
|
||||
// Reuse GitHub OAuth from GIT-001
|
||||
const githubService = GitHubOAuthService.instance;
|
||||
if (!githubService.isAuthenticated()) {
|
||||
await githubService.authenticate();
|
||||
}
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
async listSites(): Promise<Site[]> {
|
||||
// List repos with GitHub Pages enabled
|
||||
const repos = await this.githubApi.listRepos();
|
||||
const pagesRepos = repos.filter(r => r.has_pages);
|
||||
|
||||
return pagesRepos.map(r => ({
|
||||
id: r.full_name,
|
||||
name: r.name,
|
||||
url: `https://${r.owner.login}.github.io/${r.name}`,
|
||||
repo: r.full_name
|
||||
}));
|
||||
}
|
||||
|
||||
async deploy(repoFullName: string, files: DeployFiles): Promise<DeployResult> {
|
||||
const [owner, repo] = repoFullName.split('/');
|
||||
|
||||
// Create/update gh-pages branch
|
||||
const branch = 'gh-pages';
|
||||
|
||||
// Get current tree (if exists)
|
||||
let baseTree: string | null = null;
|
||||
try {
|
||||
const ref = await this.githubApi.getRef(owner, repo, `heads/${branch}`);
|
||||
const commit = await this.githubApi.getCommit(owner, repo, ref.object.sha);
|
||||
baseTree = commit.tree.sha;
|
||||
} catch {
|
||||
// Branch doesn't exist yet
|
||||
}
|
||||
|
||||
// Create blobs for all files
|
||||
const tree = await this.createTree(owner, repo, files, baseTree);
|
||||
|
||||
// Create commit
|
||||
const commit = await this.githubApi.createCommit(owner, repo, {
|
||||
message: 'Deploy from Noodl',
|
||||
tree: tree.sha,
|
||||
parents: baseTree ? [baseTree] : []
|
||||
});
|
||||
|
||||
// Update branch reference
|
||||
await this.githubApi.updateRef(owner, repo, `heads/${branch}`, commit.sha);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
deployId: commit.sha,
|
||||
url: `https://${owner}.github.io/${repo}`,
|
||||
buildTime: 0
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6. UI Components
|
||||
|
||||
#### Deploy Panel
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ Deploy [×] │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ DEPLOY TARGET │
|
||||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ 🌐 Netlify [▾] │ │
|
||||
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ SITE │
|
||||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ my-noodl-app [▾] │ │
|
||||
│ │ https://my-noodl-app.netlify.app │ │
|
||||
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||
│ [+ Create New Site] │
|
||||
│ │
|
||||
│ ENVIRONMENT │
|
||||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Production [▾] │ │
|
||||
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Last deployed: 2 hours ago │ │
|
||||
│ │ Deploy time: 45 seconds │ │
|
||||
│ │ [View Site ↗] [View Deploy History] │ │
|
||||
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ [🚀 Deploy Now] │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
#### Deploy Progress
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ Deploying to Netlify... │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 35% │
|
||||
│ │
|
||||
│ ✓ Building project │
|
||||
│ ✓ Exporting files (127 files) │
|
||||
│ ◐ Uploading to Netlify... │
|
||||
│ ○ Finalizing deploy │
|
||||
│ │
|
||||
│ [Cancel] │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Files to Create
|
||||
|
||||
1. `packages/noodl-editor/src/editor/src/services/DeployService.ts`
|
||||
2. `packages/noodl-editor/src/editor/src/services/deploy/DeployProvider.ts`
|
||||
3. `packages/noodl-editor/src/editor/src/services/deploy/providers/NetlifyProvider.ts`
|
||||
4. `packages/noodl-editor/src/editor/src/services/deploy/providers/VercelProvider.ts`
|
||||
5. `packages/noodl-editor/src/editor/src/services/deploy/providers/GitHubPagesProvider.ts`
|
||||
6. `packages/noodl-editor/src/editor/src/services/deploy/providers/CloudflareProvider.ts`
|
||||
7. `packages/noodl-core-ui/src/components/deploy/DeployPanel/DeployPanel.tsx`
|
||||
8. `packages/noodl-core-ui/src/components/deploy/DeployProgress/DeployProgress.tsx`
|
||||
9. `packages/noodl-core-ui/src/components/deploy/SiteSelector/SiteSelector.tsx`
|
||||
10. `packages/noodl-core-ui/src/components/deploy/PlatformSelector/PlatformSelector.tsx`
|
||||
|
||||
## Files to Modify
|
||||
|
||||
1. `packages/noodl-editor/src/editor/src/views/DeployPopup/DeployPopup.tsx`
|
||||
- Add platform tabs
|
||||
- Integrate new deploy flow
|
||||
|
||||
2. `packages/noodl-editor/src/editor/src/utils/compilation/compilation.ts`
|
||||
- Add deploy to platform method
|
||||
- Hook into build scripts
|
||||
|
||||
3. `packages/noodl-editor/src/main/src/main.js`
|
||||
- Add deep link handlers for OAuth callbacks
|
||||
|
||||
4. `packages/noodl-editor/src/editor/src/models/projectmodel.ts`
|
||||
- Store deploy target configuration
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### Phase 1: Service Architecture
|
||||
1. Create DeployService
|
||||
2. Define DeployProvider interface
|
||||
3. Implement provider registration
|
||||
4. Set up token storage
|
||||
|
||||
### Phase 2: Netlify Integration
|
||||
1. Implement NetlifyProvider
|
||||
2. Add OAuth flow
|
||||
3. Implement site listing
|
||||
4. Implement deployment
|
||||
|
||||
### Phase 3: Vercel Integration
|
||||
1. Implement VercelProvider
|
||||
2. Add OAuth flow
|
||||
3. Implement deployment
|
||||
|
||||
### Phase 4: GitHub Pages Integration
|
||||
1. Implement GitHubPagesProvider
|
||||
2. Reuse GitHub OAuth
|
||||
3. Implement gh-pages deployment
|
||||
|
||||
### Phase 5: UI Components
|
||||
1. Create DeployPanel
|
||||
2. Create platform/site selectors
|
||||
3. Create progress indicator
|
||||
4. Integrate with existing popup
|
||||
|
||||
### Phase 6: Testing & Polish
|
||||
1. Test each provider
|
||||
2. Error handling
|
||||
3. Progress accuracy
|
||||
4. Deploy history
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
- [ ] Netlify OAuth works
|
||||
- [ ] Netlify site listing works
|
||||
- [ ] Netlify deployment succeeds
|
||||
- [ ] Vercel OAuth works
|
||||
- [ ] Vercel deployment succeeds
|
||||
- [ ] GitHub Pages deployment works
|
||||
- [ ] Progress indicator accurate
|
||||
- [ ] Error messages helpful
|
||||
- [ ] Deploy history shows
|
||||
- [ ] Site links work
|
||||
- [ ] Token storage secure
|
||||
- [ ] Disconnect works
|
||||
|
||||
## Dependencies
|
||||
|
||||
- GIT-001 (GitHub OAuth) - for GitHub Pages
|
||||
|
||||
## Blocked By
|
||||
|
||||
- None (can start immediately)
|
||||
|
||||
## Blocks
|
||||
|
||||
- DEPLOY-002 (Preview Deployments)
|
||||
- DEPLOY-003 (Deploy Settings)
|
||||
|
||||
## Estimated Effort
|
||||
|
||||
- Service architecture: 4-5 hours
|
||||
- Netlify provider: 5-6 hours
|
||||
- Vercel provider: 4-5 hours
|
||||
- GitHub Pages provider: 4-5 hours
|
||||
- Cloudflare provider: 4-5 hours
|
||||
- UI components: 5-6 hours
|
||||
- Testing & polish: 4-5 hours
|
||||
- **Total: 30-37 hours**
|
||||
|
||||
## Success Criteria
|
||||
|
||||
1. One-click deploy to Netlify works
|
||||
2. One-click deploy to Vercel works
|
||||
3. One-click deploy to GitHub Pages works
|
||||
4. Site creation from editor works
|
||||
5. Deploy progress visible
|
||||
6. Deploy history accessible
|
||||
|
||||
## Platform-Specific Notes
|
||||
|
||||
### Netlify
|
||||
- Uses digest-based uploads (efficient)
|
||||
- Supports deploy previews (branch deploys)
|
||||
- Has good API documentation
|
||||
- Free tier: 100GB bandwidth/month
|
||||
|
||||
### Vercel
|
||||
- File-based deployment API
|
||||
- Automatic HTTPS
|
||||
- Edge functions support
|
||||
- Free tier: 100GB bandwidth/month
|
||||
|
||||
### GitHub Pages
|
||||
- No OAuth app needed (reuse GitHub)
|
||||
- Limited to public repos on free tier
|
||||
- Jekyll processing (can disable with .nojekyll)
|
||||
- Free for public repos
|
||||
|
||||
### Cloudflare Pages
|
||||
- Similar to Netlify/Vercel
|
||||
- Global CDN
|
||||
- Free tier generous
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- AWS S3 + CloudFront
|
||||
- Firebase Hosting
|
||||
- Surge.sh
|
||||
- Custom server deployment (SFTP/SSH)
|
||||
- Docker container deployment
|
||||
@@ -0,0 +1,510 @@
|
||||
# DEPLOY-002: Preview Deployments
|
||||
|
||||
## Overview
|
||||
|
||||
Enable automatic preview deployments for each git branch or commit. When users push changes, a preview URL is automatically generated so stakeholders can review before merging to production.
|
||||
|
||||
## Context
|
||||
|
||||
Currently, sharing work-in-progress requires:
|
||||
1. Manual deploy to a staging site
|
||||
2. Share URL with stakeholders
|
||||
3. Remember which deploy corresponds to which version
|
||||
4. Manually clean up old deploys
|
||||
|
||||
Preview deployments provide:
|
||||
- Automatic URL per branch/PR
|
||||
- Easy sharing with stakeholders
|
||||
- Visual history of changes
|
||||
- Automatic cleanup
|
||||
|
||||
This is especially valuable for:
|
||||
- Design reviews
|
||||
- QA testing
|
||||
- Client approvals
|
||||
- Team collaboration
|
||||
|
||||
### Integration with GIT Series
|
||||
|
||||
From GIT-002:
|
||||
- Git status tracking per project
|
||||
- Branch awareness
|
||||
- Commit detection
|
||||
|
||||
This task leverages that infrastructure to trigger preview deploys.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Functional Requirements
|
||||
|
||||
1. **Automatic Previews**
|
||||
- Deploy preview on branch push
|
||||
- Unique URL per branch
|
||||
- Update preview on new commits
|
||||
- Delete preview on branch delete
|
||||
|
||||
2. **Manual Previews**
|
||||
- "Deploy Preview" button in editor
|
||||
- Generate shareable URL
|
||||
- Named previews (optional)
|
||||
- Expiration settings
|
||||
|
||||
3. **Preview Management**
|
||||
- List all active previews
|
||||
- View preview URL
|
||||
- Delete individual previews
|
||||
- Set auto-cleanup rules
|
||||
|
||||
4. **Sharing**
|
||||
- Copy preview URL
|
||||
- QR code for mobile
|
||||
- Optional password protection
|
||||
- Expiration timer
|
||||
|
||||
5. **Integration with PRs**
|
||||
- Comment preview URL on PR
|
||||
- Update comment on new commits
|
||||
- Status check integration
|
||||
|
||||
### Non-Functional Requirements
|
||||
|
||||
- Preview available within 2 minutes
|
||||
- Support 10+ concurrent previews
|
||||
- Auto-cleanup after configurable period
|
||||
- Works with all deploy providers
|
||||
|
||||
## Technical Approach
|
||||
|
||||
### 1. Preview Service
|
||||
|
||||
```typescript
|
||||
// packages/noodl-editor/src/editor/src/services/PreviewDeployService.ts
|
||||
|
||||
interface PreviewDeployment {
|
||||
id: string;
|
||||
projectId: string;
|
||||
branch: string;
|
||||
commitSha: string;
|
||||
url: string;
|
||||
platform: DeployPlatform;
|
||||
siteId: string;
|
||||
status: PreviewStatus;
|
||||
createdAt: string;
|
||||
expiresAt?: string;
|
||||
password?: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
enum PreviewStatus {
|
||||
PENDING = 'pending',
|
||||
BUILDING = 'building',
|
||||
READY = 'ready',
|
||||
FAILED = 'failed',
|
||||
EXPIRED = 'expired'
|
||||
}
|
||||
|
||||
interface PreviewConfig {
|
||||
enabled: boolean;
|
||||
autoDeployBranches: boolean;
|
||||
excludeBranches: string[]; // e.g., ['main', 'master']
|
||||
expirationDays: number;
|
||||
maxPreviews: number;
|
||||
passwordProtect: boolean;
|
||||
commentOnPR: boolean;
|
||||
}
|
||||
|
||||
class PreviewDeployService {
|
||||
private static instance: PreviewDeployService;
|
||||
|
||||
// Preview management
|
||||
async createPreview(options: CreatePreviewOptions): Promise<PreviewDeployment>;
|
||||
async updatePreview(previewId: string): Promise<PreviewDeployment>;
|
||||
async deletePreview(previewId: string): Promise<void>;
|
||||
async listPreviews(projectId: string): Promise<PreviewDeployment[]>;
|
||||
|
||||
// Auto-deployment
|
||||
async onBranchPush(projectId: string, branch: string, commitSha: string): Promise<void>;
|
||||
async onBranchDelete(projectId: string, branch: string): Promise<void>;
|
||||
|
||||
// PR integration
|
||||
async commentOnPR(preview: PreviewDeployment): Promise<void>;
|
||||
async updatePRComment(preview: PreviewDeployment): Promise<void>;
|
||||
|
||||
// Cleanup
|
||||
async cleanupExpiredPreviews(): Promise<void>;
|
||||
async enforceMaxPreviews(projectId: string): Promise<void>;
|
||||
|
||||
// Configuration
|
||||
getConfig(projectId: string): PreviewConfig;
|
||||
setConfig(projectId: string, config: Partial<PreviewConfig>): void;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Branch-Based Preview Naming
|
||||
|
||||
```typescript
|
||||
// Generate preview URLs based on branch
|
||||
function generatePreviewUrl(platform: DeployPlatform, branch: string, projectName: string): string {
|
||||
const sanitizedBranch = sanitizeBranchName(branch);
|
||||
|
||||
switch (platform) {
|
||||
case DeployPlatform.NETLIFY:
|
||||
// Netlify: branch--sitename.netlify.app
|
||||
return `https://${sanitizedBranch}--${projectName}.netlify.app`;
|
||||
|
||||
case DeployPlatform.VERCEL:
|
||||
// Vercel: project-branch-hash.vercel.app
|
||||
return `https://${projectName}-${sanitizedBranch}.vercel.app`;
|
||||
|
||||
case DeployPlatform.GITHUB_PAGES:
|
||||
// GitHub Pages: use subdirectory or separate branch
|
||||
return `https://${owner}.github.io/${repo}/preview/${sanitizedBranch}`;
|
||||
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function sanitizeBranchName(branch: string): string {
|
||||
return branch
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9-]/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.substring(0, 50);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Git Integration Hook
|
||||
|
||||
```typescript
|
||||
// packages/noodl-editor/src/editor/src/services/PreviewDeployService.ts
|
||||
|
||||
// Hook into git operations
|
||||
class PreviewDeployService {
|
||||
constructor() {
|
||||
// Listen for git events
|
||||
EventDispatcher.instance.on('git.push.success', this.handlePush.bind(this));
|
||||
EventDispatcher.instance.on('git.branch.delete', this.handleBranchDelete.bind(this));
|
||||
}
|
||||
|
||||
private async handlePush(event: GitPushEvent): Promise<void> {
|
||||
const config = this.getConfig(event.projectId);
|
||||
|
||||
if (!config.enabled || !config.autoDeployBranches) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if branch is excluded
|
||||
if (config.excludeBranches.includes(event.branch)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we already have a preview for this branch
|
||||
const existing = await this.findPreviewByBranch(event.projectId, event.branch);
|
||||
|
||||
if (existing) {
|
||||
// Update existing preview
|
||||
await this.updatePreview(existing.id);
|
||||
} else {
|
||||
// Create new preview
|
||||
await this.createPreview({
|
||||
projectId: event.projectId,
|
||||
branch: event.branch,
|
||||
commitSha: event.commitSha
|
||||
});
|
||||
}
|
||||
|
||||
// Comment on PR if enabled
|
||||
if (config.commentOnPR) {
|
||||
const pr = await this.findPRForBranch(event.projectId, event.branch);
|
||||
if (pr) {
|
||||
await this.commentOnPR(existing || await this.getLatestPreview(event.projectId, event.branch));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async handleBranchDelete(event: GitBranchDeleteEvent): Promise<void> {
|
||||
const preview = await this.findPreviewByBranch(event.projectId, event.branch);
|
||||
if (preview) {
|
||||
await this.deletePreview(preview.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. PR Comment Integration
|
||||
|
||||
```typescript
|
||||
// packages/noodl-editor/src/editor/src/services/PreviewDeployService.ts
|
||||
|
||||
async commentOnPR(preview: PreviewDeployment): Promise<void> {
|
||||
const github = GitHubApiClient.instance;
|
||||
const project = ProjectModel.instance;
|
||||
const remote = project.getRemoteUrl();
|
||||
|
||||
if (!remote || !remote.includes('github.com')) {
|
||||
return; // Only GitHub PRs supported
|
||||
}
|
||||
|
||||
const { owner, repo } = parseGitHubUrl(remote);
|
||||
const pr = await github.findPRByBranch(owner, repo, preview.branch);
|
||||
|
||||
if (!pr) {
|
||||
return;
|
||||
}
|
||||
|
||||
const commentBody = this.generatePRComment(preview);
|
||||
|
||||
// Check for existing Noodl comment
|
||||
const existingComment = await github.findComment(owner, repo, pr.number, '<!-- noodl-preview -->');
|
||||
|
||||
if (existingComment) {
|
||||
await github.updateComment(owner, repo, existingComment.id, commentBody);
|
||||
} else {
|
||||
await github.createComment(owner, repo, pr.number, commentBody);
|
||||
}
|
||||
}
|
||||
|
||||
private generatePRComment(preview: PreviewDeployment): string {
|
||||
return `<!-- noodl-preview -->
|
||||
## 🚀 Noodl Preview Deployment
|
||||
|
||||
| Status | URL |
|
||||
|--------|-----|
|
||||
| ${this.getStatusEmoji(preview.status)} ${preview.status} | [${preview.url}](${preview.url}) |
|
||||
|
||||
**Branch:** \`${preview.branch}\`
|
||||
**Commit:** \`${preview.commitSha.substring(0, 7)}\`
|
||||
**Updated:** ${new Date(preview.createdAt).toLocaleString()}
|
||||
|
||||
---
|
||||
<sub>Deployed automatically by Noodl</sub>`;
|
||||
}
|
||||
```
|
||||
|
||||
### 5. UI Components
|
||||
|
||||
#### Preview Manager Panel
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ Preview Deployments [×] │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ACTIVE PREVIEWS (3) │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ 🌿 feature/new-dashboard [Copy URL] │ │
|
||||
│ │ https://feature-new-dashboard--myapp.netlify.app │ │
|
||||
│ │ Updated 10 minutes ago • Commit abc1234 │ │
|
||||
│ │ [Open ↗] [QR Code] [Delete] │ │
|
||||
│ ├─────────────────────────────────────────────────────────────────┤ │
|
||||
│ │ 🌿 feature/login-redesign [Copy URL] │ │
|
||||
│ │ https://feature-login-redesign--myapp.netlify.app │ │
|
||||
│ │ Updated 2 hours ago • Commit def5678 │ │
|
||||
│ │ [Open ↗] [QR Code] [Delete] │ │
|
||||
│ ├─────────────────────────────────────────────────────────────────┤ │
|
||||
│ │ 🌿 bugfix/form-validation [Copy URL] │ │
|
||||
│ │ https://bugfix-form-validation--myapp.netlify.app │ │
|
||||
│ │ Updated yesterday • Commit ghi9012 │ │
|
||||
│ │ [Open ↗] [QR Code] [Delete] │ │
|
||||
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ SETTINGS │
|
||||
│ ☑ Auto-deploy branches │
|
||||
│ ☑ Comment preview URL on PRs │
|
||||
│ Exclude branches: main, master │
|
||||
│ Auto-delete after: [7 days ▾] │
|
||||
│ │
|
||||
│ [+ Create Manual Preview] │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
#### QR Code Modal
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ Share Preview [×] │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌─────────────────┐ │
|
||||
│ │ ▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄│ │
|
||||
│ │ █ █ █ █│ │
|
||||
│ │ █ ███ █ █ ███ █│ Scan to open │
|
||||
│ │ █ █ █ █│ on mobile │
|
||||
│ │ ▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀│ │
|
||||
│ └─────────────────┘ │
|
||||
│ │
|
||||
│ feature/new-dashboard │
|
||||
│ https://feature-new-dashboard--myapp.netlify.app │
|
||||
│ │
|
||||
│ [Copy URL] [Download QR] [Close] │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
#### Create Manual Preview
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ Create Preview [×] │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Preview Name (optional): │
|
||||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ client-review-dec-15 │ │
|
||||
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ Expires in: │
|
||||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ 7 days [▾] │ │
|
||||
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ☐ Password protect │
|
||||
│ Password: [•••••••• ] │
|
||||
│ │
|
||||
│ Deploy from: │
|
||||
│ ○ Current state (uncommitted changes included) │
|
||||
│ ● Current branch (feature/new-dashboard) │
|
||||
│ ○ Specific commit: [abc1234 ▾] │
|
||||
│ │
|
||||
│ [Cancel] [Create Preview] │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Files to Create
|
||||
|
||||
1. `packages/noodl-editor/src/editor/src/services/PreviewDeployService.ts`
|
||||
2. `packages/noodl-core-ui/src/components/deploy/PreviewManager/PreviewManager.tsx`
|
||||
3. `packages/noodl-core-ui/src/components/deploy/PreviewCard/PreviewCard.tsx`
|
||||
4. `packages/noodl-core-ui/src/components/deploy/CreatePreviewModal/CreatePreviewModal.tsx`
|
||||
5. `packages/noodl-core-ui/src/components/deploy/QRCodeModal/QRCodeModal.tsx`
|
||||
6. `packages/noodl-core-ui/src/components/deploy/PreviewSettings/PreviewSettings.tsx`
|
||||
|
||||
## Files to Modify
|
||||
|
||||
1. `packages/noodl-editor/src/editor/src/services/DeployService.ts`
|
||||
- Add preview deployment methods
|
||||
- Integrate with deploy providers
|
||||
|
||||
2. `packages/noodl-editor/src/editor/src/views/DeployPopup/DeployPopup.tsx`
|
||||
- Add "Previews" tab
|
||||
- Integrate PreviewManager
|
||||
|
||||
3. `packages/noodl-editor/src/editor/src/models/projectmodel.ts`
|
||||
- Store preview configuration
|
||||
- Track active previews
|
||||
|
||||
4. `packages/noodl-editor/src/editor/src/services/GitHubApiClient.ts`
|
||||
- Add PR comment methods
|
||||
- Add PR lookup by branch
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### Phase 1: Preview Service
|
||||
1. Create PreviewDeployService
|
||||
2. Implement preview creation
|
||||
3. Implement preview deletion
|
||||
4. Add configuration storage
|
||||
|
||||
### Phase 2: Git Integration
|
||||
1. Hook into push events
|
||||
2. Hook into branch delete events
|
||||
3. Implement auto-deployment
|
||||
4. Test with branches
|
||||
|
||||
### Phase 3: PR Integration
|
||||
1. Implement PR comment creation
|
||||
2. Implement comment updating
|
||||
3. Add status emoji handling
|
||||
4. Test with GitHub PRs
|
||||
|
||||
### Phase 4: UI - Preview Manager
|
||||
1. Create PreviewManager component
|
||||
2. Create PreviewCard component
|
||||
3. Add copy/share functionality
|
||||
4. Implement delete action
|
||||
|
||||
### Phase 5: UI - Create Preview
|
||||
1. Create CreatePreviewModal
|
||||
2. Add expiration options
|
||||
3. Add password protection
|
||||
4. Add source selection
|
||||
|
||||
### Phase 6: UI - QR & Sharing
|
||||
1. Create QRCodeModal
|
||||
2. Add QR code generation
|
||||
3. Add download option
|
||||
4. Polish sharing UX
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
- [ ] Auto-preview on push works
|
||||
- [ ] Preview URL is correct
|
||||
- [ ] PR comment created
|
||||
- [ ] PR comment updated on new commit
|
||||
- [ ] Manual preview creation works
|
||||
- [ ] Preview deletion works
|
||||
- [ ] Auto-cleanup works
|
||||
- [ ] QR code generates correctly
|
||||
- [ ] Password protection works
|
||||
- [ ] Expiration works
|
||||
- [ ] Multiple previews supported
|
||||
|
||||
## Dependencies
|
||||
|
||||
- DEPLOY-001 (One-Click Deploy) - for deploy providers
|
||||
- GIT-001 (GitHub OAuth) - for PR comments
|
||||
- GIT-002 (Git Status) - for branch awareness
|
||||
|
||||
## Blocked By
|
||||
|
||||
- DEPLOY-001
|
||||
|
||||
## Blocks
|
||||
|
||||
- None
|
||||
|
||||
## Estimated Effort
|
||||
|
||||
- Preview service: 5-6 hours
|
||||
- Git integration: 4-5 hours
|
||||
- PR integration: 3-4 hours
|
||||
- UI preview manager: 4-5 hours
|
||||
- UI create preview: 3-4 hours
|
||||
- UI QR/sharing: 2-3 hours
|
||||
- Testing & polish: 3-4 hours
|
||||
- **Total: 24-31 hours**
|
||||
|
||||
## Success Criteria
|
||||
|
||||
1. Auto-preview deploys on branch push
|
||||
2. Preview URL unique per branch
|
||||
3. PR comments posted automatically
|
||||
4. Manual previews can be created
|
||||
5. QR codes work for mobile testing
|
||||
6. Expired previews auto-cleaned
|
||||
|
||||
## Platform-Specific Implementation
|
||||
|
||||
### Netlify
|
||||
- Branch deploys built-in
|
||||
- URL pattern: `branch--site.netlify.app`
|
||||
- Easy configuration
|
||||
|
||||
### Vercel
|
||||
- Preview deployments automatic
|
||||
- URL pattern: `project-branch-hash.vercel.app`
|
||||
- Good GitHub integration
|
||||
|
||||
### GitHub Pages
|
||||
- Need separate approach (subdirectory or deploy to different branch)
|
||||
- Less native support for branch previews
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- Visual regression testing
|
||||
- Screenshot comparison
|
||||
- Performance metrics per preview
|
||||
- A/B testing setup
|
||||
- Preview environments (staging, QA)
|
||||
- Slack/Teams notifications
|
||||
@@ -0,0 +1,533 @@
|
||||
# DEPLOY-003: Deploy Settings & Environment Variables
|
||||
|
||||
## Overview
|
||||
|
||||
Provide comprehensive deployment configuration including environment variables, build settings, custom domains, and deployment rules. Users can manage different environments (development, staging, production) with different configurations.
|
||||
|
||||
## Context
|
||||
|
||||
Currently:
|
||||
- Environment variables set per deploy manually
|
||||
- No persistent environment configuration
|
||||
- No distinction between environments
|
||||
- Custom domain setup requires external configuration
|
||||
|
||||
This task adds:
|
||||
- Persistent environment variable management
|
||||
- Multiple environment profiles
|
||||
- Custom domain configuration
|
||||
- Build optimization settings
|
||||
- Deploy rules and triggers
|
||||
|
||||
## Requirements
|
||||
|
||||
### Functional Requirements
|
||||
|
||||
1. **Environment Variables**
|
||||
- Add/edit/delete variables
|
||||
- Sensitive variable masking
|
||||
- Import from .env file
|
||||
- Export to .env file
|
||||
- Variable validation
|
||||
|
||||
2. **Environment Profiles**
|
||||
- Development, Staging, Production presets
|
||||
- Custom profiles
|
||||
- Variables per profile
|
||||
- Easy switching
|
||||
|
||||
3. **Custom Domains**
|
||||
- View current domains
|
||||
- Add custom domain
|
||||
- SSL certificate status
|
||||
- DNS configuration help
|
||||
|
||||
4. **Build Settings**
|
||||
- Output directory
|
||||
- Base URL configuration
|
||||
- Asset optimization
|
||||
- Source maps (dev only)
|
||||
|
||||
5. **Deploy Rules**
|
||||
- Auto-deploy on push
|
||||
- Branch-based rules
|
||||
- Deploy schedule
|
||||
- Deploy hooks/webhooks
|
||||
|
||||
### Non-Functional Requirements
|
||||
|
||||
- Variables encrypted at rest
|
||||
- Sensitive values never logged
|
||||
- Sync with platform settings
|
||||
- Works offline (cached)
|
||||
|
||||
## Technical Approach
|
||||
|
||||
### 1. Environment Configuration Service
|
||||
|
||||
```typescript
|
||||
// packages/noodl-editor/src/editor/src/services/EnvironmentConfigService.ts
|
||||
|
||||
interface EnvironmentVariable {
|
||||
key: string;
|
||||
value: string;
|
||||
sensitive: boolean; // Masked in UI
|
||||
scope: VariableScope;
|
||||
}
|
||||
|
||||
enum VariableScope {
|
||||
BUILD = 'build', // Available during build
|
||||
RUNTIME = 'runtime', // Injected into app
|
||||
BOTH = 'both'
|
||||
}
|
||||
|
||||
interface EnvironmentProfile {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
variables: EnvironmentVariable[];
|
||||
isDefault: boolean;
|
||||
platform?: DeployPlatform; // If linked to a platform
|
||||
}
|
||||
|
||||
interface DeploySettings {
|
||||
outputDirectory: string;
|
||||
baseUrl: string;
|
||||
assetOptimization: boolean;
|
||||
sourceMaps: boolean;
|
||||
cleanUrls: boolean;
|
||||
trailingSlash: boolean;
|
||||
}
|
||||
|
||||
class EnvironmentConfigService {
|
||||
private static instance: EnvironmentConfigService;
|
||||
|
||||
// Profiles
|
||||
async getProfiles(projectId: string): Promise<EnvironmentProfile[]>;
|
||||
async createProfile(projectId: string, profile: Omit<EnvironmentProfile, 'id'>): Promise<EnvironmentProfile>;
|
||||
async updateProfile(projectId: string, profileId: string, updates: Partial<EnvironmentProfile>): Promise<void>;
|
||||
async deleteProfile(projectId: string, profileId: string): Promise<void>;
|
||||
|
||||
// Variables
|
||||
async getVariables(projectId: string, profileId: string): Promise<EnvironmentVariable[]>;
|
||||
async setVariable(projectId: string, profileId: string, variable: EnvironmentVariable): Promise<void>;
|
||||
async deleteVariable(projectId: string, profileId: string, key: string): Promise<void>;
|
||||
async importFromEnvFile(projectId: string, profileId: string, content: string): Promise<void>;
|
||||
async exportToEnvFile(projectId: string, profileId: string): Promise<string>;
|
||||
|
||||
// Build settings
|
||||
async getDeploySettings(projectId: string): Promise<DeploySettings>;
|
||||
async updateDeploySettings(projectId: string, settings: Partial<DeploySettings>): Promise<void>;
|
||||
|
||||
// Platform sync
|
||||
async syncWithPlatform(projectId: string, profileId: string, platform: DeployPlatform): Promise<void>;
|
||||
async pullFromPlatform(projectId: string, platform: DeployPlatform): Promise<EnvironmentVariable[]>;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Environment Storage
|
||||
|
||||
```typescript
|
||||
// Store in project metadata, encrypted
|
||||
interface ProjectDeployConfig {
|
||||
profiles: EnvironmentProfile[];
|
||||
activeProfileId: string;
|
||||
deploySettings: DeploySettings;
|
||||
domains: CustomDomain[];
|
||||
deployRules: DeployRule[];
|
||||
}
|
||||
|
||||
// Encryption for sensitive values
|
||||
class SecureStorage {
|
||||
async encrypt(value: string): Promise<string>;
|
||||
async decrypt(value: string): Promise<string>;
|
||||
}
|
||||
|
||||
// Store encrypted in project.json
|
||||
{
|
||||
"metadata": {
|
||||
"deployConfig": {
|
||||
"profiles": [
|
||||
{
|
||||
"id": "prod",
|
||||
"name": "Production",
|
||||
"variables": [
|
||||
{
|
||||
"key": "API_URL",
|
||||
"value": "https://api.example.com", // Plain text
|
||||
"sensitive": false
|
||||
},
|
||||
{
|
||||
"key": "API_KEY",
|
||||
"value": "encrypted:abc123...", // Encrypted
|
||||
"sensitive": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Domain Configuration
|
||||
|
||||
```typescript
|
||||
interface CustomDomain {
|
||||
domain: string;
|
||||
platform: DeployPlatform;
|
||||
siteId: string;
|
||||
status: DomainStatus;
|
||||
sslStatus: SSLStatus;
|
||||
dnsRecords?: DNSRecord[];
|
||||
}
|
||||
|
||||
enum DomainStatus {
|
||||
PENDING = 'pending',
|
||||
ACTIVE = 'active',
|
||||
FAILED = 'failed'
|
||||
}
|
||||
|
||||
enum SSLStatus {
|
||||
PENDING = 'pending',
|
||||
ACTIVE = 'active',
|
||||
EXPIRED = 'expired',
|
||||
FAILED = 'failed'
|
||||
}
|
||||
|
||||
interface DNSRecord {
|
||||
type: 'A' | 'CNAME' | 'TXT';
|
||||
name: string;
|
||||
value: string;
|
||||
required: boolean;
|
||||
}
|
||||
|
||||
class DomainService {
|
||||
async addDomain(siteId: string, domain: string): Promise<CustomDomain>;
|
||||
async verifyDomain(domainId: string): Promise<DomainStatus>;
|
||||
async getDNSInstructions(domain: string): Promise<DNSRecord[]>;
|
||||
async checkSSL(domainId: string): Promise<SSLStatus>;
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Deploy Rules
|
||||
|
||||
```typescript
|
||||
interface DeployRule {
|
||||
id: string;
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
trigger: DeployTrigger;
|
||||
conditions: DeployCondition[];
|
||||
actions: DeployAction[];
|
||||
}
|
||||
|
||||
interface DeployTrigger {
|
||||
type: 'push' | 'schedule' | 'manual' | 'webhook';
|
||||
config: PushConfig | ScheduleConfig | WebhookConfig;
|
||||
}
|
||||
|
||||
interface PushConfig {
|
||||
branches: string[]; // Glob patterns
|
||||
paths?: string[]; // Only deploy if these paths changed
|
||||
}
|
||||
|
||||
interface ScheduleConfig {
|
||||
cron: string; // Cron expression
|
||||
timezone: string;
|
||||
}
|
||||
|
||||
interface DeployCondition {
|
||||
type: 'branch' | 'tag' | 'path' | 'message';
|
||||
operator: 'equals' | 'contains' | 'matches';
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface DeployAction {
|
||||
type: 'deploy' | 'notify' | 'webhook';
|
||||
config: DeployActionConfig | NotifyConfig | WebhookConfig;
|
||||
}
|
||||
```
|
||||
|
||||
### 5. UI Components
|
||||
|
||||
#### Environment Variables Panel
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ Environment Variables [×] │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Profile: [Production ▾] [+ New Profile] │
|
||||
│ │
|
||||
│ VARIABLES [Import .env] │
|
||||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Key Value Scope [⋯] │ │
|
||||
│ ├─────────────────────────────────────────────────────────────────┤ │
|
||||
│ │ API_URL https://api.example.com Runtime [✎🗑] │ │
|
||||
│ │ API_KEY •••••••••••••••••••••• Runtime [✎🗑] │ │
|
||||
│ │ ANALYTICS_ID UA-12345678-1 Runtime [✎🗑] │ │
|
||||
│ │ DEBUG false Build [✎🗑] │ │
|
||||
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ [+ Add Variable] [Export .env] │
|
||||
│ │
|
||||
│ ☑ Sync with Netlify │
|
||||
│ Last synced: 5 minutes ago [Sync Now] │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
#### Build Settings Panel
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ Build Settings [×] │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ OUTPUT │
|
||||
│ Output Directory: [dist ] │
|
||||
│ Base URL: [/ ] │
|
||||
│ │
|
||||
│ OPTIMIZATION │
|
||||
│ ☑ Optimize assets (minify JS/CSS) │
|
||||
│ ☐ Generate source maps (increases build size) │
|
||||
│ ☑ Clean URLs (remove .html extension) │
|
||||
│ ☐ Trailing slash on URLs │
|
||||
│ │
|
||||
│ ADVANCED │
|
||||
│ Build Command: [npm run build ] │
|
||||
│ Publish Directory: [build ] │
|
||||
│ │
|
||||
│ NODE VERSION │
|
||||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ 18 (LTS) [▾] │ │
|
||||
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ [Save Settings] │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
#### Custom Domains Panel
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ Custom Domains [×] │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ CONNECTED DOMAINS │
|
||||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ 🌐 myapp.com │ │
|
||||
│ │ ✓ DNS Configured ✓ SSL Active │ │
|
||||
│ │ Primary domain [Remove] │ │
|
||||
│ ├─────────────────────────────────────────────────────────────────┤ │
|
||||
│ │ 🌐 www.myapp.com │ │
|
||||
│ │ ✓ DNS Configured ✓ SSL Active │ │
|
||||
│ │ Redirects to myapp.com [Remove] │ │
|
||||
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ [+ Add Custom Domain] │
|
||||
│ │
|
||||
│ DEFAULT DOMAIN │
|
||||
│ https://myapp.netlify.app [Copy] │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
#### Add Domain Modal
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ Add Custom Domain [×] │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Domain: │
|
||||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ app.example.com │ │
|
||||
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ DNS CONFIGURATION REQUIRED │
|
||||
│ │
|
||||
│ Add these records to your DNS provider: │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Type Name Value [Copy] │ │
|
||||
│ ├─────────────────────────────────────────────────────────────────┤ │
|
||||
│ │ CNAME app myapp.netlify.app [Copy] │ │
|
||||
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ⚠️ DNS changes can take up to 48 hours to propagate │
|
||||
│ │
|
||||
│ Status: ⏳ Waiting for DNS verification... │
|
||||
│ │
|
||||
│ [Cancel] [Verify Domain] [Done] │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
#### Deploy Rules Panel
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ Deploy Rules [×] │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ RULES [+ Add Rule] │
|
||||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ ☑ Auto-deploy production │ │
|
||||
│ │ When: Push to main │ │
|
||||
│ │ Deploy to: Production [Edit 🗑] │ │
|
||||
│ ├─────────────────────────────────────────────────────────────────┤ │
|
||||
│ │ ☑ Preview branches │ │
|
||||
│ │ When: Push to feature/* │ │
|
||||
│ │ Deploy to: Preview [Edit 🗑] │ │
|
||||
│ ├─────────────────────────────────────────────────────────────────┤ │
|
||||
│ │ ☐ Scheduled deploy │ │
|
||||
│ │ When: Daily at 2:00 AM UTC │ │
|
||||
│ │ Deploy to: Production [Edit 🗑] │ │
|
||||
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ WEBHOOKS [+ Add Webhook] │
|
||||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Build hook URL: │ │
|
||||
│ │ https://api.netlify.com/build_hooks/abc123 [Copy] [🔄] │ │
|
||||
│ │ Trigger: POST request to this URL │ │
|
||||
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Files to Create
|
||||
|
||||
1. `packages/noodl-editor/src/editor/src/services/EnvironmentConfigService.ts`
|
||||
2. `packages/noodl-editor/src/editor/src/services/DomainService.ts`
|
||||
3. `packages/noodl-editor/src/editor/src/services/DeployRulesService.ts`
|
||||
4. `packages/noodl-core-ui/src/components/deploy/EnvironmentVariables/EnvironmentVariables.tsx`
|
||||
5. `packages/noodl-core-ui/src/components/deploy/EnvironmentVariables/VariableRow.tsx`
|
||||
6. `packages/noodl-core-ui/src/components/deploy/EnvironmentVariables/ProfileSelector.tsx`
|
||||
7. `packages/noodl-core-ui/src/components/deploy/BuildSettings/BuildSettings.tsx`
|
||||
8. `packages/noodl-core-ui/src/components/deploy/CustomDomains/CustomDomains.tsx`
|
||||
9. `packages/noodl-core-ui/src/components/deploy/CustomDomains/AddDomainModal.tsx`
|
||||
10. `packages/noodl-core-ui/src/components/deploy/DeployRules/DeployRules.tsx`
|
||||
11. `packages/noodl-core-ui/src/components/deploy/DeployRules/RuleEditor.tsx`
|
||||
|
||||
## Files to Modify
|
||||
|
||||
1. `packages/noodl-editor/src/editor/src/views/DeployPopup/DeployPopup.tsx`
|
||||
- Add settings tabs
|
||||
- Integrate new panels
|
||||
|
||||
2. `packages/noodl-editor/src/editor/src/utils/compilation/compilation.ts`
|
||||
- Use environment variables from config
|
||||
- Apply build settings
|
||||
|
||||
3. `packages/noodl-editor/src/editor/src/models/projectmodel.ts`
|
||||
- Store deploy configuration
|
||||
- Load/save config
|
||||
|
||||
4. `packages/noodl-editor/src/editor/src/services/DeployService.ts`
|
||||
- Apply environment variables to deploy
|
||||
- Handle domain configuration
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### Phase 1: Environment Variables
|
||||
1. Create EnvironmentConfigService
|
||||
2. Implement variable storage
|
||||
3. Implement encryption for sensitive values
|
||||
4. Add import/export .env
|
||||
|
||||
### Phase 2: Environment Profiles
|
||||
1. Add profile management
|
||||
2. Implement profile switching
|
||||
3. Add default profiles (dev/staging/prod)
|
||||
4. UI for profile management
|
||||
|
||||
### Phase 3: UI - Variables Panel
|
||||
1. Create EnvironmentVariables component
|
||||
2. Create VariableRow component
|
||||
3. Add add/edit/delete functionality
|
||||
4. Add import/export buttons
|
||||
|
||||
### Phase 4: Build Settings
|
||||
1. Create build settings storage
|
||||
2. Create BuildSettings component
|
||||
3. Integrate with compilation
|
||||
4. Test with deployments
|
||||
|
||||
### Phase 5: Custom Domains
|
||||
1. Create DomainService
|
||||
2. Implement platform-specific domain APIs
|
||||
3. Create CustomDomains component
|
||||
4. Create AddDomainModal
|
||||
|
||||
### Phase 6: Deploy Rules
|
||||
1. Create DeployRulesService
|
||||
2. Implement rule evaluation
|
||||
3. Create DeployRules component
|
||||
4. Create RuleEditor
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
- [ ] Variables saved correctly
|
||||
- [ ] Sensitive values encrypted
|
||||
- [ ] Variables applied to deploy
|
||||
- [ ] Import .env works
|
||||
- [ ] Export .env works
|
||||
- [ ] Profile switching works
|
||||
- [ ] Build settings applied
|
||||
- [ ] Custom domain setup works
|
||||
- [ ] DNS verification works
|
||||
- [ ] Deploy rules trigger correctly
|
||||
- [ ] Webhooks work
|
||||
- [ ] Platform sync works
|
||||
|
||||
## Dependencies
|
||||
|
||||
- DEPLOY-001 (One-Click Deploy) - for platform integration
|
||||
|
||||
## Blocked By
|
||||
|
||||
- DEPLOY-001
|
||||
|
||||
## Blocks
|
||||
|
||||
- None (final DEPLOY task)
|
||||
|
||||
## Estimated Effort
|
||||
|
||||
- Environment config service: 4-5 hours
|
||||
- Variable storage/encryption: 3-4 hours
|
||||
- Environment profiles: 3-4 hours
|
||||
- UI variables panel: 4-5 hours
|
||||
- Build settings: 3-4 hours
|
||||
- Custom domains: 4-5 hours
|
||||
- Deploy rules: 4-5 hours
|
||||
- Testing & polish: 3-4 hours
|
||||
- **Total: 28-36 hours**
|
||||
|
||||
## Success Criteria
|
||||
|
||||
1. Environment variables persist across deploys
|
||||
2. Sensitive values properly secured
|
||||
3. Multiple profiles supported
|
||||
4. Import/export .env works
|
||||
5. Custom domains configurable
|
||||
6. Deploy rules automate deployments
|
||||
7. Settings sync with platforms
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- Sensitive values encrypted at rest
|
||||
- Never log sensitive values
|
||||
- Use platform-native secret storage where available
|
||||
- Clear memory after use
|
||||
- Validate input to prevent injection
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- Environment variable inheritance
|
||||
- Secret rotation reminders
|
||||
- Integration with secret managers (Vault, AWS Secrets)
|
||||
- A/B testing configuration
|
||||
- Feature flags integration
|
||||
- Monitoring/alerting integration
|
||||
@@ -0,0 +1,385 @@
|
||||
# DEPLOY Series: Deployment Automation
|
||||
|
||||
## Overview
|
||||
|
||||
The DEPLOY series transforms Noodl's deployment from manual folder exports into a modern, automated deployment pipeline. Users can deploy to popular hosting platforms with one click, get automatic preview URLs for each branch, and manage environment variables and domains directly from the editor.
|
||||
|
||||
## Target Environment
|
||||
|
||||
- **Editor**: React 19 version only
|
||||
- **Platforms**: Netlify, Vercel, GitHub Pages, Cloudflare Pages
|
||||
- **Backwards Compatibility**: Existing deploy-to-folder continues to work
|
||||
|
||||
## Task Dependency Graph
|
||||
|
||||
```
|
||||
DEPLOY-001 (One-Click Deploy)
|
||||
│
|
||||
├─────────────────────┐
|
||||
↓ ↓
|
||||
DEPLOY-002 (Previews) DEPLOY-003 (Settings)
|
||||
```
|
||||
|
||||
## Task Summary
|
||||
|
||||
| Task ID | Name | Est. Hours | Priority |
|
||||
|---------|------|------------|----------|
|
||||
| DEPLOY-001 | One-Click Deploy Integrations | 30-37 | Critical |
|
||||
| DEPLOY-002 | Preview Deployments | 24-31 | High |
|
||||
| DEPLOY-003 | Deploy Settings & Environment Variables | 28-36 | High |
|
||||
|
||||
**Total Estimated: 82-104 hours**
|
||||
|
||||
## Implementation Order
|
||||
|
||||
### Phase 1: Core Deployment (Weeks 1-2)
|
||||
1. **DEPLOY-001** - One-click deploy to platforms
|
||||
- Establishes provider architecture
|
||||
- OAuth flows for each platform
|
||||
- Core deployment functionality
|
||||
|
||||
### Phase 2: Preview & Settings (Weeks 3-4)
|
||||
2. **DEPLOY-002** - Preview deployments
|
||||
- Branch-based previews
|
||||
- PR integration
|
||||
- Sharing features
|
||||
|
||||
3. **DEPLOY-003** - Deploy settings
|
||||
- Environment variables
|
||||
- Custom domains
|
||||
- Deploy rules
|
||||
|
||||
## Existing Infrastructure
|
||||
|
||||
### Deployer
|
||||
|
||||
```typescript
|
||||
// packages/noodl-editor/src/editor/src/utils/compilation/build/deployer.ts
|
||||
export async function deployToFolder({
|
||||
project,
|
||||
direntry,
|
||||
environment,
|
||||
baseUrl,
|
||||
envVariables,
|
||||
runtimeType = 'deploy'
|
||||
}: DeployToFolderOptions)
|
||||
```
|
||||
|
||||
### Compilation
|
||||
|
||||
```typescript
|
||||
// packages/noodl-editor/src/editor/src/utils/compilation/compilation.ts
|
||||
class Compilation {
|
||||
deployToFolder(direntry, options): Promise<void>;
|
||||
// Build scripts system for pre/post deploy
|
||||
}
|
||||
```
|
||||
|
||||
### Deploy UI
|
||||
|
||||
```typescript
|
||||
// Current deploy popup with folder selection
|
||||
DeployToFolderTab.tsx
|
||||
DeployPopup.tsx
|
||||
```
|
||||
|
||||
### Cloud Services
|
||||
|
||||
```typescript
|
||||
// packages/noodl-editor/src/editor/src/models/CloudServices.ts
|
||||
interface Environment {
|
||||
id: string;
|
||||
name: string;
|
||||
url: string;
|
||||
appId: string;
|
||||
masterKey?: string;
|
||||
}
|
||||
```
|
||||
|
||||
## New Architecture
|
||||
|
||||
### Service Layer
|
||||
|
||||
```
|
||||
packages/noodl-editor/src/editor/src/services/
|
||||
├── DeployService.ts # Central deployment service
|
||||
├── PreviewDeployService.ts # Preview management
|
||||
├── EnvironmentConfigService.ts # Env vars & profiles
|
||||
├── DomainService.ts # Custom domain management
|
||||
├── DeployRulesService.ts # Automation rules
|
||||
└── deploy/
|
||||
├── DeployProvider.ts # Provider interface
|
||||
└── providers/
|
||||
├── NetlifyProvider.ts
|
||||
├── VercelProvider.ts
|
||||
├── GitHubPagesProvider.ts
|
||||
└── CloudflareProvider.ts
|
||||
```
|
||||
|
||||
### Provider Interface
|
||||
|
||||
```typescript
|
||||
interface DeployProvider {
|
||||
readonly platform: DeployPlatform;
|
||||
readonly name: string;
|
||||
|
||||
// Authentication
|
||||
authenticate(): Promise<AuthResult>;
|
||||
isAuthenticated(): boolean;
|
||||
|
||||
// Sites
|
||||
listSites(): Promise<Site[]>;
|
||||
createSite(name: string): Promise<Site>;
|
||||
|
||||
// Deployment
|
||||
deploy(siteId: string, files: DeployFiles): Promise<DeployResult>;
|
||||
getDeployStatus(deployId: string): Promise<DeployStatus>;
|
||||
|
||||
// Configuration
|
||||
getEnvVariables(siteId: string): Promise<Record<string, string>>;
|
||||
setEnvVariables(siteId: string, vars: Record<string, string>): Promise<void>;
|
||||
}
|
||||
```
|
||||
|
||||
## Platform Comparison
|
||||
|
||||
| Feature | Netlify | Vercel | GitHub Pages | Cloudflare |
|
||||
|---------|---------|--------|--------------|------------|
|
||||
| OAuth | ✓ | ✓ | Via GitHub | ✓ |
|
||||
| Preview Deploys | ✓ | ✓ | Manual | ✓ |
|
||||
| Custom Domains | ✓ | ✓ | ✓ | ✓ |
|
||||
| Env Variables | ✓ | ✓ | Secrets only | ✓ |
|
||||
| Deploy Hooks | ✓ | ✓ | Actions | ✓ |
|
||||
| Free Tier | 100GB/mo | 100GB/mo | Unlimited* | 100K/day |
|
||||
|
||||
*GitHub Pages: Free for public repos, requires Pro for private
|
||||
|
||||
## Key User Flows
|
||||
|
||||
### 1. First-Time Deploy
|
||||
|
||||
```
|
||||
User clicks "Deploy"
|
||||
↓
|
||||
Select platform (Netlify, Vercel, etc.)
|
||||
↓
|
||||
Authenticate with platform (OAuth)
|
||||
↓
|
||||
Create new site or select existing
|
||||
↓
|
||||
Configure environment variables
|
||||
↓
|
||||
Deploy → Get live URL
|
||||
```
|
||||
|
||||
### 2. Subsequent Deploys
|
||||
|
||||
```
|
||||
User clicks "Deploy"
|
||||
↓
|
||||
Site already linked
|
||||
↓
|
||||
One-click deploy
|
||||
↓
|
||||
Progress indicator
|
||||
↓
|
||||
Success → Link to site
|
||||
```
|
||||
|
||||
### 3. Preview Workflow
|
||||
|
||||
```
|
||||
User pushes feature branch
|
||||
↓
|
||||
Auto-deploy preview
|
||||
↓
|
||||
Comment on PR with preview URL
|
||||
↓
|
||||
Stakeholder reviews
|
||||
↓
|
||||
Merge → Production deploy
|
||||
↓
|
||||
Preview auto-cleaned
|
||||
```
|
||||
|
||||
## UI Components to Create
|
||||
|
||||
| Component | Package | Purpose |
|
||||
|-----------|---------|---------|
|
||||
| DeployPanel | noodl-core-ui | Main deploy interface |
|
||||
| PlatformSelector | noodl-core-ui | Platform choice |
|
||||
| SiteSelector | noodl-core-ui | Site choice |
|
||||
| DeployProgress | noodl-core-ui | Progress indicator |
|
||||
| PreviewManager | noodl-core-ui | Preview list |
|
||||
| EnvironmentVariables | noodl-core-ui | Var management |
|
||||
| CustomDomains | noodl-core-ui | Domain setup |
|
||||
| DeployRules | noodl-core-ui | Automation rules |
|
||||
|
||||
## Data Models
|
||||
|
||||
### Deploy Target
|
||||
|
||||
```typescript
|
||||
interface DeployTarget {
|
||||
id: string;
|
||||
name: string;
|
||||
platform: DeployPlatform;
|
||||
siteId: string;
|
||||
siteName: string;
|
||||
url: string;
|
||||
customDomain?: string;
|
||||
lastDeployedAt?: string;
|
||||
}
|
||||
```
|
||||
|
||||
### Preview Deployment
|
||||
|
||||
```typescript
|
||||
interface PreviewDeployment {
|
||||
id: string;
|
||||
projectId: string;
|
||||
branch: string;
|
||||
commitSha: string;
|
||||
url: string;
|
||||
status: PreviewStatus;
|
||||
createdAt: string;
|
||||
expiresAt?: string;
|
||||
}
|
||||
```
|
||||
|
||||
### Environment Profile
|
||||
|
||||
```typescript
|
||||
interface EnvironmentProfile {
|
||||
id: string;
|
||||
name: string;
|
||||
variables: EnvironmentVariable[];
|
||||
isDefault: boolean;
|
||||
}
|
||||
|
||||
interface EnvironmentVariable {
|
||||
key: string;
|
||||
value: string;
|
||||
sensitive: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Token Storage
|
||||
- OAuth tokens stored in electron safeStorage
|
||||
- Never logged or displayed
|
||||
- Cleared on disconnect
|
||||
|
||||
### Sensitive Variables
|
||||
- Encrypted at rest
|
||||
- Masked in UI (•••••)
|
||||
- Never exported to .env without warning
|
||||
|
||||
### Platform Security
|
||||
- Minimum OAuth scopes
|
||||
- Token refresh handling
|
||||
- Secure redirect handling
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Unit Tests
|
||||
- Provider method isolation
|
||||
- Token handling
|
||||
- File preparation
|
||||
|
||||
### Integration Tests
|
||||
- OAuth flow mocking
|
||||
- Deploy API mocking
|
||||
- Full deploy cycle
|
||||
|
||||
### Manual Testing
|
||||
- Real deployments to each platform
|
||||
- Custom domain setup
|
||||
- Preview workflow
|
||||
|
||||
## Cline Usage Notes
|
||||
|
||||
### Before Starting Each Task
|
||||
|
||||
1. Review existing deployment infrastructure:
|
||||
- `deployer.ts`
|
||||
- `compilation.ts`
|
||||
- `DeployPopup/`
|
||||
|
||||
2. Understand build output:
|
||||
- How project.json is exported
|
||||
- How bundles are created
|
||||
- How index.html is generated
|
||||
|
||||
### Key Integration Points
|
||||
|
||||
1. **Compilation**: Use existing `deployToFolder` for file preparation
|
||||
2. **Cloud Services**: Existing environment model can inform design
|
||||
3. **Git Integration**: Leverage GIT series for branch awareness
|
||||
|
||||
### Platform API Notes
|
||||
|
||||
- **Netlify**: Uses digest-based upload (SHA1 hashes)
|
||||
- **Vercel**: File-based deployment API
|
||||
- **GitHub Pages**: Git-based via GitHub API
|
||||
- **Cloudflare**: Similar to Netlify/Vercel
|
||||
|
||||
## Success Criteria (Series Complete)
|
||||
|
||||
1. ✅ One-click deploy to 4 platforms
|
||||
2. ✅ OAuth authentication flow works
|
||||
3. ✅ Site creation from editor works
|
||||
4. ✅ Preview deploys auto-generated
|
||||
5. ✅ PR comments posted automatically
|
||||
6. ✅ Environment variables managed
|
||||
7. ✅ Custom domains configurable
|
||||
8. ✅ Deploy rules automate workflow
|
||||
|
||||
## Future Work (Post-DEPLOY)
|
||||
|
||||
The DEPLOY series enables:
|
||||
- **CI/CD Integration**: Connect to GitHub Actions, GitLab CI
|
||||
- **Performance Monitoring**: Lighthouse scores per deploy
|
||||
- **A/B Testing**: Deploy variants to subsets
|
||||
- **Rollback**: One-click rollback to previous deploy
|
||||
- **Multi-Region**: Deploy to multiple regions
|
||||
|
||||
## Files in This Series
|
||||
|
||||
- `DEPLOY-001-one-click-deploy.md`
|
||||
- `DEPLOY-002-preview-deployments.md`
|
||||
- `DEPLOY-003-deploy-settings.md`
|
||||
- `DEPLOY-OVERVIEW.md` (this file)
|
||||
|
||||
## External Dependencies
|
||||
|
||||
### Platform OAuth
|
||||
|
||||
| Platform | OAuth Type | Client ID Required |
|
||||
|----------|------------|-------------------|
|
||||
| Netlify | OAuth 2.0 | Yes |
|
||||
| Vercel | OAuth 2.0 | Yes |
|
||||
| GitHub | OAuth 2.0 | From GIT-001 |
|
||||
| Cloudflare | OAuth 2.0 | Yes |
|
||||
|
||||
### API Endpoints
|
||||
|
||||
- Netlify: `api.netlify.com/api/v1`
|
||||
- Vercel: `api.vercel.com/v13`
|
||||
- GitHub: `api.github.com`
|
||||
- Cloudflare: `api.cloudflare.com/client/v4`
|
||||
|
||||
## Complete Roadmap Summary
|
||||
|
||||
With the DEPLOY series complete, the full OpenNoodl modernization roadmap includes:
|
||||
|
||||
| Series | Tasks | Hours | Focus |
|
||||
|--------|-------|-------|-------|
|
||||
| DASH | 4 tasks | 43-63 | Dashboard UX |
|
||||
| GIT | 5 tasks | 68-96 | Git integration |
|
||||
| COMP | 6 tasks | 117-155 | Shared components |
|
||||
| AI | 4 tasks | 121-154 | AI assistance |
|
||||
| DEPLOY | 3 tasks | 82-104 | Deployment |
|
||||
|
||||
**Grand Total: 22 tasks, 431-572 hours**
|
||||
@@ -0,0 +1,738 @@
|
||||
# TASK: Enhanced Expression Node & Expression Evaluator Foundation
|
||||
|
||||
## Overview
|
||||
|
||||
Upgrade the existing Expression node to support full JavaScript expressions with access to `Noodl.Variables`, `Noodl.Objects`, and `Noodl.Arrays`, plus reactive dependency tracking. This establishes the foundation for Phase 2 (inline expression properties throughout the editor).
|
||||
|
||||
**Estimated effort:** 2-3 weeks
|
||||
**Priority:** High - Foundation for Expression Properties feature
|
||||
**Dependencies:** None
|
||||
|
||||
---
|
||||
|
||||
## Background & Motivation
|
||||
|
||||
### Current Expression Node Limitations
|
||||
|
||||
The existing Expression node (`packages/noodl-runtime/src/nodes/std-library/expression.js`):
|
||||
|
||||
1. **Limited context** - Only provides Math helpers (min, max, cos, sin, etc.)
|
||||
2. **No Noodl globals** - Cannot access `Noodl.Variables.X`, `Noodl.Objects.Y`, `Noodl.Arrays.Z`
|
||||
3. **Boolean-focused outputs** - Primarily `isTrue`/`isFalse`, though `result` exists as `*` type
|
||||
4. **Workaround required** - Users must create connected input ports to pass in variable values
|
||||
5. **No reactive updates** - Doesn't automatically re-evaluate when referenced Variables/Objects change
|
||||
|
||||
### Desired State
|
||||
|
||||
Users should be able to write expressions like:
|
||||
```javascript
|
||||
Noodl.Variables.isLoggedIn ? `Welcome, ${Noodl.Variables.userName}!` : "Please log in"
|
||||
```
|
||||
|
||||
And have the expression automatically re-evaluate whenever `isLoggedIn` or `userName` changes.
|
||||
|
||||
---
|
||||
|
||||
## Files to Analyze First
|
||||
|
||||
Before making changes, thoroughly read and understand these files:
|
||||
|
||||
### Core Expression Implementation
|
||||
```
|
||||
@packages/noodl-runtime/src/nodes/std-library/expression.js
|
||||
```
|
||||
- Current expression compilation using `new Function()`
|
||||
- `functionPreamble` that injects Math helpers
|
||||
- `parsePorts()` for extracting variable references
|
||||
- Scheduling and caching mechanisms
|
||||
|
||||
### Noodl Global APIs
|
||||
```
|
||||
@packages/noodl-runtime/src/model.js
|
||||
@packages/noodl-viewer-react/src/noodl-js-api.js
|
||||
@packages/noodl-viewer-cloud/src/noodl-js-api.js
|
||||
```
|
||||
- How `Noodl.Variables`, `Noodl.Objects`, `Noodl.Arrays` are implemented
|
||||
- The Model class and its change event system
|
||||
- `Model.get('--ndl--global-variables')` pattern
|
||||
|
||||
### Type Definitions (for autocomplete later)
|
||||
```
|
||||
@packages/noodl-viewer-react/static/viewer/global.d.ts.keep
|
||||
@packages/noodl-viewer-cloud/static/viewer/global.d.ts.keep
|
||||
```
|
||||
- TypeScript definitions for the Noodl namespace
|
||||
- Documentation of the API surface
|
||||
|
||||
### JavaScript/Function Node (reference)
|
||||
```
|
||||
@packages/noodl-runtime/src/nodes/std-library/javascriptfunction.js
|
||||
```
|
||||
- How full JavaScript nodes access Noodl context
|
||||
- Pattern for providing richer execution context
|
||||
|
||||
---
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### Step 1: Create Expression Evaluator Module
|
||||
|
||||
Create a new shared module that handles expression compilation, dependency tracking, and evaluation.
|
||||
|
||||
**Create file:** `packages/noodl-runtime/src/expression-evaluator.js`
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Expression Evaluator
|
||||
*
|
||||
* Compiles JavaScript expressions with access to Noodl globals
|
||||
* and tracks dependencies for reactive updates.
|
||||
*
|
||||
* Features:
|
||||
* - Full Noodl.Variables, Noodl.Objects, Noodl.Arrays access
|
||||
* - Math helpers (min, max, cos, sin, etc.)
|
||||
* - Dependency detection and change subscription
|
||||
* - Expression versioning for future compatibility
|
||||
* - Caching of compiled functions
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const Model = require('./model');
|
||||
|
||||
// Expression system version - increment when context changes
|
||||
const EXPRESSION_VERSION = 1;
|
||||
|
||||
// Cache for compiled functions
|
||||
const compiledFunctionsCache = new Map();
|
||||
|
||||
// Math helpers to inject
|
||||
const mathHelpers = {
|
||||
min: Math.min,
|
||||
max: Math.max,
|
||||
cos: Math.cos,
|
||||
sin: Math.sin,
|
||||
tan: Math.tan,
|
||||
sqrt: Math.sqrt,
|
||||
pi: Math.PI,
|
||||
round: Math.round,
|
||||
floor: Math.floor,
|
||||
ceil: Math.ceil,
|
||||
abs: Math.abs,
|
||||
random: Math.random,
|
||||
pow: Math.pow,
|
||||
log: Math.log,
|
||||
exp: Math.exp
|
||||
};
|
||||
|
||||
/**
|
||||
* Detect dependencies in an expression string
|
||||
* Returns { variables: string[], objects: string[], arrays: string[] }
|
||||
*/
|
||||
function detectDependencies(expression) {
|
||||
const dependencies = {
|
||||
variables: [],
|
||||
objects: [],
|
||||
arrays: []
|
||||
};
|
||||
|
||||
// Remove strings to avoid false matches
|
||||
const exprWithoutStrings = expression
|
||||
.replace(/"([^"\\]|\\.)*"/g, '""')
|
||||
.replace(/'([^'\\]|\\.)*'/g, "''")
|
||||
.replace(/`([^`\\]|\\.)*`/g, '``');
|
||||
|
||||
// Match Noodl.Variables.X or Noodl.Variables["X"]
|
||||
const variableMatches = exprWithoutStrings.matchAll(
|
||||
/Noodl\.Variables\.([a-zA-Z_$][a-zA-Z0-9_$]*)|Noodl\.Variables\[["']([^"']+)["']\]/g
|
||||
);
|
||||
for (const match of variableMatches) {
|
||||
const varName = match[1] || match[2];
|
||||
if (varName && !dependencies.variables.includes(varName)) {
|
||||
dependencies.variables.push(varName);
|
||||
}
|
||||
}
|
||||
|
||||
// Match Noodl.Objects.X or Noodl.Objects["X"]
|
||||
const objectMatches = exprWithoutStrings.matchAll(
|
||||
/Noodl\.Objects\.([a-zA-Z_$][a-zA-Z0-9_$]*)|Noodl\.Objects\[["']([^"']+)["']\]/g
|
||||
);
|
||||
for (const match of objectMatches) {
|
||||
const objId = match[1] || match[2];
|
||||
if (objId && !dependencies.objects.includes(objId)) {
|
||||
dependencies.objects.push(objId);
|
||||
}
|
||||
}
|
||||
|
||||
// Match Noodl.Arrays.X or Noodl.Arrays["X"]
|
||||
const arrayMatches = exprWithoutStrings.matchAll(
|
||||
/Noodl\.Arrays\.([a-zA-Z_$][a-zA-Z0-9_$]*)|Noodl\.Arrays\[["']([^"']+)["']\]/g
|
||||
);
|
||||
for (const match of arrayMatches) {
|
||||
const arrId = match[1] || match[2];
|
||||
if (arrId && !dependencies.arrays.includes(arrId)) {
|
||||
dependencies.arrays.push(arrId);
|
||||
}
|
||||
}
|
||||
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the Noodl context object for expression evaluation
|
||||
*/
|
||||
function createNoodlContext(modelScope) {
|
||||
const scope = modelScope || Model;
|
||||
|
||||
return {
|
||||
Variables: scope.get('--ndl--global-variables')?.data || {},
|
||||
Objects: new Proxy({}, {
|
||||
get(target, prop) {
|
||||
const obj = scope.get(prop);
|
||||
return obj ? obj.data : undefined;
|
||||
}
|
||||
}),
|
||||
Arrays: new Proxy({}, {
|
||||
get(target, prop) {
|
||||
const arr = scope.get(prop);
|
||||
return arr ? arr.data : undefined;
|
||||
}
|
||||
}),
|
||||
Object: scope
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile an expression string into a callable function
|
||||
*/
|
||||
function compileExpression(expression) {
|
||||
const cacheKey = `v${EXPRESSION_VERSION}:${expression}`;
|
||||
|
||||
if (compiledFunctionsCache.has(cacheKey)) {
|
||||
return compiledFunctionsCache.get(cacheKey);
|
||||
}
|
||||
|
||||
// Build parameter list for the function
|
||||
const paramNames = ['Noodl', ...Object.keys(mathHelpers)];
|
||||
|
||||
// Wrap expression in return statement
|
||||
const functionBody = `
|
||||
"use strict";
|
||||
try {
|
||||
return (${expression});
|
||||
} catch (e) {
|
||||
console.error('Expression evaluation error:', e.message);
|
||||
return undefined;
|
||||
}
|
||||
`;
|
||||
|
||||
try {
|
||||
const fn = new Function(...paramNames, functionBody);
|
||||
compiledFunctionsCache.set(cacheKey, fn);
|
||||
return fn;
|
||||
} catch (e) {
|
||||
console.error('Expression compilation error:', e.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate a compiled expression with the current context
|
||||
*/
|
||||
function evaluateExpression(compiledFn, modelScope) {
|
||||
if (!compiledFn) return undefined;
|
||||
|
||||
const noodlContext = createNoodlContext(modelScope);
|
||||
const mathValues = Object.values(mathHelpers);
|
||||
|
||||
try {
|
||||
return compiledFn(noodlContext, ...mathValues);
|
||||
} catch (e) {
|
||||
console.error('Expression evaluation error:', e.message);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to changes in expression dependencies
|
||||
* Returns an unsubscribe function
|
||||
*/
|
||||
function subscribeToChanges(dependencies, callback, modelScope) {
|
||||
const scope = modelScope || Model;
|
||||
const listeners = [];
|
||||
|
||||
// Subscribe to variable changes
|
||||
if (dependencies.variables.length > 0) {
|
||||
const variablesModel = scope.get('--ndl--global-variables');
|
||||
if (variablesModel) {
|
||||
const handler = (args) => {
|
||||
// Check if any of our dependencies changed
|
||||
if (dependencies.variables.some(v => args.name === v || !args.name)) {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
variablesModel.on('change', handler);
|
||||
listeners.push(() => variablesModel.off('change', handler));
|
||||
}
|
||||
}
|
||||
|
||||
// Subscribe to object changes
|
||||
for (const objId of dependencies.objects) {
|
||||
const objModel = scope.get(objId);
|
||||
if (objModel) {
|
||||
const handler = () => callback();
|
||||
objModel.on('change', handler);
|
||||
listeners.push(() => objModel.off('change', handler));
|
||||
}
|
||||
}
|
||||
|
||||
// Subscribe to array changes
|
||||
for (const arrId of dependencies.arrays) {
|
||||
const arrModel = scope.get(arrId);
|
||||
if (arrModel) {
|
||||
const handler = () => callback();
|
||||
arrModel.on('change', handler);
|
||||
listeners.push(() => arrModel.off('change', handler));
|
||||
}
|
||||
}
|
||||
|
||||
// Return unsubscribe function
|
||||
return () => {
|
||||
listeners.forEach(unsub => unsub());
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate expression syntax without executing
|
||||
*/
|
||||
function validateExpression(expression) {
|
||||
try {
|
||||
new Function(`return (${expression})`);
|
||||
return { valid: true, error: null };
|
||||
} catch (e) {
|
||||
return { valid: false, error: e.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current expression system version
|
||||
*/
|
||||
function getExpressionVersion() {
|
||||
return EXPRESSION_VERSION;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
detectDependencies,
|
||||
compileExpression,
|
||||
evaluateExpression,
|
||||
subscribeToChanges,
|
||||
validateExpression,
|
||||
createNoodlContext,
|
||||
getExpressionVersion,
|
||||
EXPRESSION_VERSION
|
||||
};
|
||||
```
|
||||
|
||||
### Step 2: Upgrade Expression Node
|
||||
|
||||
Modify the existing Expression node to use the new evaluator and support reactive updates.
|
||||
|
||||
**Modify file:** `packages/noodl-runtime/src/nodes/std-library/expression.js`
|
||||
|
||||
Key changes:
|
||||
1. Use `expression-evaluator.js` for compilation
|
||||
2. Add Noodl globals to the function preamble
|
||||
3. Implement dependency detection
|
||||
4. Subscribe to changes for automatic re-evaluation
|
||||
5. Add new typed outputs (`asString`, `asNumber`)
|
||||
6. Clean up subscriptions on node deletion
|
||||
|
||||
```javascript
|
||||
// Key additions to the expression node:
|
||||
|
||||
const ExpressionEvaluator = require('../../expression-evaluator');
|
||||
|
||||
// In initialize():
|
||||
internal.unsubscribe = null;
|
||||
internal.dependencies = { variables: [], objects: [], arrays: [] };
|
||||
|
||||
// In the expression input setter:
|
||||
// After compiling the expression:
|
||||
internal.dependencies = ExpressionEvaluator.detectDependencies(value);
|
||||
|
||||
// Set up reactive subscription
|
||||
if (internal.unsubscribe) {
|
||||
internal.unsubscribe();
|
||||
}
|
||||
|
||||
if (internal.dependencies.variables.length > 0 ||
|
||||
internal.dependencies.objects.length > 0 ||
|
||||
internal.dependencies.arrays.length > 0) {
|
||||
internal.unsubscribe = ExpressionEvaluator.subscribeToChanges(
|
||||
internal.dependencies,
|
||||
() => this._scheduleEvaluateExpression(),
|
||||
this.context?.modelScope
|
||||
);
|
||||
}
|
||||
|
||||
// Add cleanup in _onNodeDeleted or add a delete listener
|
||||
```
|
||||
|
||||
### Step 3: Update Function Preamble
|
||||
|
||||
Update the preamble to include Noodl globals:
|
||||
|
||||
```javascript
|
||||
var functionPreamble = [
|
||||
// Math helpers (existing)
|
||||
'var min = Math.min,',
|
||||
' max = Math.max,',
|
||||
' cos = Math.cos,',
|
||||
' sin = Math.sin,',
|
||||
' tan = Math.tan,',
|
||||
' sqrt = Math.sqrt,',
|
||||
' pi = Math.PI,',
|
||||
' round = Math.round,',
|
||||
' floor = Math.floor,',
|
||||
' ceil = Math.ceil,',
|
||||
' abs = Math.abs,',
|
||||
' pow = Math.pow,',
|
||||
' log = Math.log,',
|
||||
' exp = Math.exp,',
|
||||
' random = Math.random;',
|
||||
// Noodl context shortcuts (new)
|
||||
'var Variables = Noodl.Variables,',
|
||||
' Objects = Noodl.Objects,',
|
||||
' Arrays = Noodl.Arrays;'
|
||||
].join('\n');
|
||||
```
|
||||
|
||||
### Step 4: Add New Outputs
|
||||
|
||||
Add typed output alternatives for better downstream compatibility:
|
||||
|
||||
```javascript
|
||||
outputs: {
|
||||
// Existing outputs (keep for backward compatibility)
|
||||
result: { /* ... */ },
|
||||
isTrue: { /* ... */ },
|
||||
isFalse: { /* ... */ },
|
||||
isTrueEv: { /* ... */ },
|
||||
isFalseEv: { /* ... */ },
|
||||
|
||||
// New typed outputs
|
||||
asString: {
|
||||
group: 'Typed Results',
|
||||
type: 'string',
|
||||
displayName: 'As String',
|
||||
getter: function() {
|
||||
const val = this._internal.cachedValue;
|
||||
return val !== undefined && val !== null ? String(val) : '';
|
||||
}
|
||||
},
|
||||
asNumber: {
|
||||
group: 'Typed Results',
|
||||
type: 'number',
|
||||
displayName: 'As Number',
|
||||
getter: function() {
|
||||
const val = this._internal.cachedValue;
|
||||
return typeof val === 'number' ? val : Number(val) || 0;
|
||||
}
|
||||
},
|
||||
asBoolean: {
|
||||
group: 'Typed Results',
|
||||
type: 'boolean',
|
||||
displayName: 'As Boolean',
|
||||
getter: function() {
|
||||
return !!this._internal.cachedValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 5: Add Expression Validation in Editor
|
||||
|
||||
Enhance the editor-side validation to provide better error messages:
|
||||
|
||||
**Modify file:** `packages/noodl-runtime/src/nodes/std-library/expression.js` (setup function)
|
||||
|
||||
```javascript
|
||||
// In the setup function, enhance evalCompileWarnings:
|
||||
function evalCompileWarnings(editorConnection, node) {
|
||||
const expression = node.parameters.expression;
|
||||
if (!expression) {
|
||||
editorConnection.clearWarning(node.component.name, node.id, 'expression-compile-error');
|
||||
return;
|
||||
}
|
||||
|
||||
const validation = ExpressionEvaluator.validateExpression(expression);
|
||||
|
||||
if (!validation.valid) {
|
||||
editorConnection.sendWarning(node.component.name, node.id, 'expression-compile-error', {
|
||||
message: `Syntax error: ${validation.error}`
|
||||
});
|
||||
} else {
|
||||
editorConnection.clearWarning(node.component.name, node.id, 'expression-compile-error');
|
||||
|
||||
// Also show detected dependencies as info (optional)
|
||||
const deps = ExpressionEvaluator.detectDependencies(expression);
|
||||
if (deps.variables.length > 0 || deps.objects.length > 0 || deps.arrays.length > 0) {
|
||||
// Could show this as info, not warning
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 6: Add Tests
|
||||
|
||||
**Create file:** `packages/noodl-runtime/test/expression-evaluator.test.js`
|
||||
|
||||
```javascript
|
||||
const ExpressionEvaluator = require('../src/expression-evaluator');
|
||||
|
||||
describe('Expression Evaluator', () => {
|
||||
describe('detectDependencies', () => {
|
||||
test('detects Noodl.Variables references', () => {
|
||||
const deps = ExpressionEvaluator.detectDependencies(
|
||||
'Noodl.Variables.isLoggedIn ? Noodl.Variables.userName : "guest"'
|
||||
);
|
||||
expect(deps.variables).toContain('isLoggedIn');
|
||||
expect(deps.variables).toContain('userName');
|
||||
});
|
||||
|
||||
test('detects bracket notation', () => {
|
||||
const deps = ExpressionEvaluator.detectDependencies(
|
||||
'Noodl.Variables["my variable"]'
|
||||
);
|
||||
expect(deps.variables).toContain('my variable');
|
||||
});
|
||||
|
||||
test('ignores references inside strings', () => {
|
||||
const deps = ExpressionEvaluator.detectDependencies(
|
||||
'"Noodl.Variables.notReal"'
|
||||
);
|
||||
expect(deps.variables).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('detects Noodl.Objects references', () => {
|
||||
const deps = ExpressionEvaluator.detectDependencies(
|
||||
'Noodl.Objects.CurrentUser.name'
|
||||
);
|
||||
expect(deps.objects).toContain('CurrentUser');
|
||||
});
|
||||
|
||||
test('detects Noodl.Arrays references', () => {
|
||||
const deps = ExpressionEvaluator.detectDependencies(
|
||||
'Noodl.Arrays.items.length'
|
||||
);
|
||||
expect(deps.arrays).toContain('items');
|
||||
});
|
||||
});
|
||||
|
||||
describe('compileExpression', () => {
|
||||
test('compiles valid expression', () => {
|
||||
const fn = ExpressionEvaluator.compileExpression('1 + 1');
|
||||
expect(fn).not.toBeNull();
|
||||
});
|
||||
|
||||
test('returns null for invalid expression', () => {
|
||||
const fn = ExpressionEvaluator.compileExpression('1 +');
|
||||
expect(fn).toBeNull();
|
||||
});
|
||||
|
||||
test('caches compiled functions', () => {
|
||||
const fn1 = ExpressionEvaluator.compileExpression('2 + 2');
|
||||
const fn2 = ExpressionEvaluator.compileExpression('2 + 2');
|
||||
expect(fn1).toBe(fn2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateExpression', () => {
|
||||
test('validates correct syntax', () => {
|
||||
const result = ExpressionEvaluator.validateExpression('a > b ? 1 : 0');
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test('catches syntax errors', () => {
|
||||
const result = ExpressionEvaluator.validateExpression('a >');
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.error).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('evaluateExpression', () => {
|
||||
test('evaluates math expressions', () => {
|
||||
const fn = ExpressionEvaluator.compileExpression('min(10, 5) + max(1, 2)');
|
||||
const result = ExpressionEvaluator.evaluateExpression(fn);
|
||||
expect(result).toBe(7);
|
||||
});
|
||||
|
||||
test('handles pi constant', () => {
|
||||
const fn = ExpressionEvaluator.compileExpression('round(pi * 100) / 100');
|
||||
const result = ExpressionEvaluator.evaluateExpression(fn);
|
||||
expect(result).toBe(3.14);
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Step 7: Update TypeScript Definitions
|
||||
|
||||
**Modify file:** `packages/noodl-editor/src/editor/src/utils/CodeEditor/model.ts`
|
||||
|
||||
Add the enhanced context for Expression nodes in the Monaco editor:
|
||||
|
||||
```typescript
|
||||
// In registerOrUpdate_Expression function, add more complete typings
|
||||
function registerOrUpdate_Expression(): TypescriptModule {
|
||||
return {
|
||||
uri: 'expression-context.d.ts',
|
||||
source: `
|
||||
declare const Noodl: {
|
||||
Variables: Record<string, any>;
|
||||
Objects: Record<string, any>;
|
||||
Arrays: Record<string, any>;
|
||||
};
|
||||
declare const Variables: Record<string, any>;
|
||||
declare const Objects: Record<string, any>;
|
||||
declare const Arrays: Record<string, any>;
|
||||
|
||||
declare const min: typeof Math.min;
|
||||
declare const max: typeof Math.max;
|
||||
declare const cos: typeof Math.cos;
|
||||
declare const sin: typeof Math.sin;
|
||||
declare const tan: typeof Math.tan;
|
||||
declare const sqrt: typeof Math.sqrt;
|
||||
declare const pi: number;
|
||||
declare const round: typeof Math.round;
|
||||
declare const floor: typeof Math.floor;
|
||||
declare const ceil: typeof Math.ceil;
|
||||
declare const abs: typeof Math.abs;
|
||||
declare const pow: typeof Math.pow;
|
||||
declare const log: typeof Math.log;
|
||||
declare const exp: typeof Math.exp;
|
||||
declare const random: typeof Math.random;
|
||||
`,
|
||||
libs: []
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria
|
||||
|
||||
### Functional Requirements
|
||||
|
||||
- [ ] Expression node can evaluate `Noodl.Variables.X` syntax
|
||||
- [ ] Expression node can evaluate `Noodl.Objects.X.property` syntax
|
||||
- [ ] Expression node can evaluate `Noodl.Arrays.X` syntax
|
||||
- [ ] Shorthand aliases work (`Variables.X`, `Objects.X`, `Arrays.X`)
|
||||
- [ ] Expression auto-re-evaluates when referenced Variable changes
|
||||
- [ ] Expression auto-re-evaluates when referenced Object property changes
|
||||
- [ ] Expression auto-re-evaluates when referenced Array changes
|
||||
- [ ] New typed outputs (`asString`, `asNumber`, `asBoolean`) work correctly
|
||||
- [ ] Backward compatibility - existing expressions continue to work
|
||||
- [ ] Math helpers continue to work (min, max, cos, sin, etc.)
|
||||
- [ ] Syntax errors show clear warning messages in editor
|
||||
|
||||
### Non-Functional Requirements
|
||||
|
||||
- [ ] Compiled functions are cached for performance
|
||||
- [ ] Memory cleanup - subscriptions are removed when node is deleted
|
||||
- [ ] Expression version is tracked for future migration support
|
||||
- [ ] No performance regression for expressions without Noodl globals
|
||||
|
||||
---
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
### Manual Testing
|
||||
|
||||
1. **Basic Math Expression**
|
||||
- Create Expression node with `min(10, 5) + max(1, 2)`
|
||||
- Verify result output is 7
|
||||
|
||||
2. **Variable Reference**
|
||||
- Set `Noodl.Variables.testVar = 42` in a Function node
|
||||
- Create Expression node with `Noodl.Variables.testVar * 2`
|
||||
- Verify result is 84
|
||||
|
||||
3. **Reactive Update**
|
||||
- Create Expression with `Noodl.Variables.counter`
|
||||
- Connect a button to increment `Noodl.Variables.counter`
|
||||
- Verify Expression result updates automatically on button click
|
||||
|
||||
4. **Object Property Access**
|
||||
- Create an Object with ID "TestObject" and property "name"
|
||||
- Create Expression with `Noodl.Objects.TestObject.name`
|
||||
- Verify result shows the name value
|
||||
|
||||
5. **Ternary with Variables**
|
||||
- Set `Noodl.Variables.isAdmin = true`
|
||||
- Create Expression: `Noodl.Variables.isAdmin ? "Admin" : "User"`
|
||||
- Verify result is "Admin"
|
||||
- Toggle isAdmin to false, verify result changes to "User"
|
||||
|
||||
6. **Template Literals**
|
||||
- Set `Noodl.Variables.name = "Alice"`
|
||||
- Create Expression: `` `Hello, ${Noodl.Variables.name}!` ``
|
||||
- Verify result is "Hello, Alice!"
|
||||
|
||||
7. **Syntax Error Handling**
|
||||
- Create Expression with invalid syntax `1 +`
|
||||
- Verify warning appears in editor
|
||||
- Verify node doesn't crash
|
||||
|
||||
8. **Typed Outputs**
|
||||
- Create Expression: `"42"`
|
||||
- Connect `asNumber` output to a Number display
|
||||
- Verify it shows 42 as number
|
||||
|
||||
### Automated Testing
|
||||
|
||||
- [ ] Run `npm test` in packages/noodl-runtime
|
||||
- [ ] All expression-evaluator tests pass
|
||||
- [ ] Existing expression.test.js tests pass
|
||||
- [ ] No TypeScript errors in editor package
|
||||
|
||||
---
|
||||
|
||||
## Rollback Plan
|
||||
|
||||
If issues are discovered:
|
||||
|
||||
1. The expression-evaluator.js module is additive - can be removed without breaking existing code
|
||||
2. Expression node changes are backward compatible - old expressions work
|
||||
3. New outputs are additive - removing them won't break existing connections
|
||||
4. Keep original functionPreamble as fallback option
|
||||
|
||||
---
|
||||
|
||||
## Notes for Implementer
|
||||
|
||||
### Important Patterns to Preserve
|
||||
|
||||
1. **Input port generation** - The expression node dynamically creates input ports for referenced variables. This behavior should be preserved for explicit inputs while also supporting implicit Noodl.Variables access.
|
||||
|
||||
2. **Scheduling** - Use `scheduleAfterInputsHaveUpdated` pattern for batching evaluations.
|
||||
|
||||
3. **Caching** - The existing `cachedValue` pattern prevents unnecessary output updates.
|
||||
|
||||
### Edge Cases to Handle
|
||||
|
||||
1. **Circular dependencies** - What if Variable A's expression references Variable B and vice versa?
|
||||
2. **Missing variables** - Handle gracefully when referenced variable doesn't exist
|
||||
3. **Type coercion** - Be consistent with JavaScript's type coercion rules
|
||||
4. **Async expressions** - Current system is sync-only, keep it that way
|
||||
|
||||
### Questions to Resolve During Implementation
|
||||
|
||||
1. Should the shorthand `Variables.X` work without `Noodl.` prefix?
|
||||
- **Recommendation:** Yes, add to preamble for convenience
|
||||
|
||||
2. Should we detect unused input ports and warn?
|
||||
- **Recommendation:** Not in this phase
|
||||
|
||||
3. How to handle expressions that error at runtime?
|
||||
- **Recommendation:** Return undefined, log error, don't crash
|
||||
@@ -0,0 +1,960 @@
|
||||
# TASK: Inline Expression Properties in Property Panel
|
||||
|
||||
## Overview
|
||||
|
||||
Add the ability to toggle any node property between "fixed value" and "expression mode" directly in the property panel - similar to n8n's approach. When in expression mode, users can write JavaScript expressions that evaluate at runtime with full access to Noodl globals.
|
||||
|
||||
**Estimated effort:** 3-4 weeks
|
||||
**Priority:** High - Major UX modernization
|
||||
**Dependencies:** Phase 1 (Enhanced Expression Node) must be complete
|
||||
|
||||
---
|
||||
|
||||
## Background & Motivation
|
||||
|
||||
### The Problem Today
|
||||
|
||||
To make any property dynamic in Noodl, users must:
|
||||
1. Create a separate Expression, Variable, or Function node
|
||||
2. Configure that node with the logic
|
||||
3. Draw a connection cable to the target property
|
||||
4. Repeat for every dynamic value
|
||||
|
||||
**Result:** Canvas cluttered with helper nodes, hard to understand data flow.
|
||||
|
||||
### The Solution
|
||||
|
||||
Every property input gains a toggle between:
|
||||
- **Fixed Mode** (default): Traditional static value editing
|
||||
- **Expression Mode**: JavaScript expression evaluated at runtime
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Margin Left │
|
||||
│ ┌────────┬────────────────────────────────────────────┬───┐ │
|
||||
│ │ Fixed │ 16 │ ⚡ │ │
|
||||
│ └────────┴────────────────────────────────────────────┴───┘ │
|
||||
│ │
|
||||
│ After clicking ⚡ toggle: │
|
||||
│ │
|
||||
│ ┌────────┬────────────────────────────────────────────┬───┐ │
|
||||
│ │ fx │ Noodl.Variables.isMobile ? 8 : 16 │ ⚡ │ │
|
||||
│ └────────┴────────────────────────────────────────────┴───┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Files to Analyze First
|
||||
|
||||
### Phase 1 Foundation (must be complete)
|
||||
```
|
||||
@packages/noodl-runtime/src/expression-evaluator.js
|
||||
```
|
||||
- Expression compilation and evaluation
|
||||
- Dependency detection
|
||||
- Change subscription
|
||||
|
||||
### Property Panel Architecture
|
||||
```
|
||||
@packages/noodl-core-ui/src/components/property-panel/PropertyPanelInput/PropertyPanelInput.tsx
|
||||
@packages/noodl-core-ui/src/components/property-panel/README.md
|
||||
@packages/noodl-editor/src/editor/src/views/panels/propertyeditor/propertyeditor.ts
|
||||
@packages/noodl-editor/src/editor/src/views/panels/propertyeditor/index.tsx
|
||||
```
|
||||
- Property panel component structure
|
||||
- How different property types are rendered
|
||||
- Property value flow from model to UI and back
|
||||
|
||||
### Type-Specific Editors
|
||||
```
|
||||
@packages/noodl-editor/src/editor/src/views/panels/propertyeditor/DataTypes/BasicType.ts
|
||||
@packages/noodl-editor/src/editor/src/views/panels/propertyeditor/DataTypes/BooleanType.ts
|
||||
@packages/noodl-editor/src/editor/src/views/panels/propertyeditor/DataTypes/ColorType.ts
|
||||
@packages/noodl-editor/src/editor/src/views/panels/propertyeditor/DataTypes/VariableType.ts
|
||||
```
|
||||
- Pattern for different input types
|
||||
- How values are stored and retrieved
|
||||
|
||||
### Node Model & Parameters
|
||||
```
|
||||
@packages/noodl-editor/src/editor/src/models/nodegraphmodel/NodeGraphNode.ts
|
||||
@packages/noodl-runtime/src/node.js
|
||||
```
|
||||
- How parameters are stored
|
||||
- Parameter update events
|
||||
- Visual state parameter patterns (`paramName_stateName`)
|
||||
|
||||
### Port/Connection System
|
||||
```
|
||||
@packages/noodl-editor/src/editor/src/models/nodelibrary/nodelibrary.ts
|
||||
```
|
||||
- Port type definitions
|
||||
- Connection state detection (`isConnected`)
|
||||
|
||||
---
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### Step 1: Extend Parameter Storage Model
|
||||
|
||||
Parameters need to support both simple values and expression metadata.
|
||||
|
||||
**Modify:** Node model parameter handling
|
||||
|
||||
```typescript
|
||||
// New parameter value types
|
||||
interface FixedParameter {
|
||||
value: any;
|
||||
}
|
||||
|
||||
interface ExpressionParameter {
|
||||
mode: 'expression';
|
||||
expression: string;
|
||||
fallback?: any; // Value to use if expression errors
|
||||
version?: number; // Expression system version for migration
|
||||
}
|
||||
|
||||
type ParameterValue = any | ExpressionParameter;
|
||||
|
||||
// Helper to check if parameter is expression
|
||||
function isExpressionParameter(param: any): param is ExpressionParameter {
|
||||
return param && typeof param === 'object' && param.mode === 'expression';
|
||||
}
|
||||
|
||||
// Helper to get display value
|
||||
function getParameterDisplayValue(param: ParameterValue): any {
|
||||
if (isExpressionParameter(param)) {
|
||||
return param.expression;
|
||||
}
|
||||
return param;
|
||||
}
|
||||
```
|
||||
|
||||
**Ensure backward compatibility:**
|
||||
- Simple values (strings, numbers, etc.) continue to work as-is
|
||||
- Expression parameters are objects with `mode: 'expression'`
|
||||
- Serialization/deserialization handles both formats
|
||||
|
||||
### Step 2: Create Expression Toggle Component
|
||||
|
||||
**Create file:** `packages/noodl-core-ui/src/components/property-panel/ExpressionToggle/ExpressionToggle.tsx`
|
||||
|
||||
```tsx
|
||||
import React from 'react';
|
||||
import { IconButton, IconButtonVariant } from '../../inputs/IconButton';
|
||||
import { IconName, IconSize } from '../../common/Icon';
|
||||
import { Tooltip } from '../../popups/Tooltip';
|
||||
import css from './ExpressionToggle.module.scss';
|
||||
|
||||
export interface ExpressionToggleProps {
|
||||
mode: 'fixed' | 'expression';
|
||||
isConnected?: boolean; // Port has cable connection
|
||||
onToggle: () => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export function ExpressionToggle({
|
||||
mode,
|
||||
isConnected,
|
||||
onToggle,
|
||||
disabled
|
||||
}: ExpressionToggleProps) {
|
||||
// If connected via cable, show connection indicator instead
|
||||
if (isConnected) {
|
||||
return (
|
||||
<Tooltip content="Connected via cable">
|
||||
<div className={css.connectionIndicator}>
|
||||
<Icon name={IconName.Connection} size={IconSize.Tiny} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
const tooltipText = mode === 'expression'
|
||||
? 'Switch to fixed value'
|
||||
: 'Switch to expression';
|
||||
|
||||
return (
|
||||
<Tooltip content={tooltipText}>
|
||||
<IconButton
|
||||
icon={mode === 'expression' ? IconName.Function : IconName.Lightning}
|
||||
size={IconSize.Tiny}
|
||||
variant={mode === 'expression'
|
||||
? IconButtonVariant.Active
|
||||
: IconButtonVariant.OpaqueOnHover}
|
||||
onClick={onToggle}
|
||||
isDisabled={disabled}
|
||||
UNSAFE_className={mode === 'expression' ? css.expressionActive : undefined}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Create file:** `packages/noodl-core-ui/src/components/property-panel/ExpressionToggle/ExpressionToggle.module.scss`
|
||||
|
||||
```scss
|
||||
.connectionIndicator {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.expressionActive {
|
||||
background-color: var(--theme-color-expression-bg, #6366f1);
|
||||
color: white;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--theme-color-expression-bg-hover, #4f46e5);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 3: Create Expression Input Component
|
||||
|
||||
**Create file:** `packages/noodl-core-ui/src/components/property-panel/ExpressionInput/ExpressionInput.tsx`
|
||||
|
||||
```tsx
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import { TextInput, TextInputVariant } from '../../inputs/TextInput';
|
||||
import { IconButton } from '../../inputs/IconButton';
|
||||
import { IconName, IconSize } from '../../common/Icon';
|
||||
import { Tooltip } from '../../popups/Tooltip';
|
||||
import css from './ExpressionInput.module.scss';
|
||||
|
||||
export interface ExpressionInputProps {
|
||||
expression: string;
|
||||
onChange: (expression: string) => void;
|
||||
onOpenBuilder?: () => void;
|
||||
expectedType?: string; // 'string', 'number', 'boolean', 'color'
|
||||
hasError?: boolean;
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
export function ExpressionInput({
|
||||
expression,
|
||||
onChange,
|
||||
onOpenBuilder,
|
||||
expectedType,
|
||||
hasError,
|
||||
errorMessage
|
||||
}: ExpressionInputProps) {
|
||||
const [localValue, setLocalValue] = useState(expression);
|
||||
|
||||
const handleBlur = useCallback(() => {
|
||||
if (localValue !== expression) {
|
||||
onChange(localValue);
|
||||
}
|
||||
}, [localValue, expression, onChange]);
|
||||
|
||||
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
onChange(localValue);
|
||||
}
|
||||
}, [localValue, onChange]);
|
||||
|
||||
return (
|
||||
<div className={css.container}>
|
||||
<span className={css.badge}>fx</span>
|
||||
<TextInput
|
||||
value={localValue}
|
||||
onChange={(e) => setLocalValue(e.target.value)}
|
||||
onBlur={handleBlur}
|
||||
onKeyDown={handleKeyDown}
|
||||
variant={TextInputVariant.Transparent}
|
||||
placeholder="Enter expression..."
|
||||
UNSAFE_style={{ fontFamily: 'monospace', fontSize: '12px' }}
|
||||
UNSAFE_className={hasError ? css.hasError : undefined}
|
||||
/>
|
||||
{onOpenBuilder && (
|
||||
<Tooltip content="Open expression builder (Cmd+Shift+E)">
|
||||
<IconButton
|
||||
icon={IconName.Expand}
|
||||
size={IconSize.Tiny}
|
||||
onClick={onOpenBuilder}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
{hasError && errorMessage && (
|
||||
<Tooltip content={errorMessage}>
|
||||
<div className={css.errorIndicator}>!</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Create file:** `packages/noodl-core-ui/src/components/property-panel/ExpressionInput/ExpressionInput.module.scss`
|
||||
|
||||
```scss
|
||||
.container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
background-color: var(--theme-color-expression-input-bg, rgba(99, 102, 241, 0.1));
|
||||
border-radius: 4px;
|
||||
padding: 0 4px;
|
||||
border: 1px solid var(--theme-color-expression-border, rgba(99, 102, 241, 0.3));
|
||||
}
|
||||
|
||||
.badge {
|
||||
font-family: monospace;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: var(--theme-color-expression-badge, #6366f1);
|
||||
padding: 2px 4px;
|
||||
background-color: var(--theme-color-expression-badge-bg, rgba(99, 102, 241, 0.2));
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.hasError {
|
||||
border-color: var(--theme-color-error, #ef4444);
|
||||
}
|
||||
|
||||
.errorIndicator {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: var(--theme-color-error, #ef4444);
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
}
|
||||
```
|
||||
|
||||
### Step 4: Integrate with PropertyPanelInput
|
||||
|
||||
**Modify:** `packages/noodl-core-ui/src/components/property-panel/PropertyPanelInput/PropertyPanelInput.tsx`
|
||||
|
||||
```tsx
|
||||
// Add to imports
|
||||
import { ExpressionToggle } from '../ExpressionToggle';
|
||||
import { ExpressionInput } from '../ExpressionInput';
|
||||
|
||||
// Extend props interface
|
||||
export interface PropertyPanelInputProps extends Omit<PropertyPanelBaseInputProps, 'type'> {
|
||||
label: string;
|
||||
inputType: PropertyPanelInputType;
|
||||
properties: TSFixme;
|
||||
|
||||
// Expression support (new)
|
||||
supportsExpression?: boolean; // Default true for most types
|
||||
expressionMode?: 'fixed' | 'expression';
|
||||
expression?: string;
|
||||
isConnected?: boolean;
|
||||
onExpressionModeChange?: (mode: 'fixed' | 'expression') => void;
|
||||
onExpressionChange?: (expression: string) => void;
|
||||
}
|
||||
|
||||
export function PropertyPanelInput({
|
||||
label,
|
||||
value,
|
||||
inputType = PropertyPanelInputType.Text,
|
||||
properties,
|
||||
isChanged,
|
||||
isConnected,
|
||||
onChange,
|
||||
// Expression props
|
||||
supportsExpression = true,
|
||||
expressionMode = 'fixed',
|
||||
expression,
|
||||
onExpressionModeChange,
|
||||
onExpressionChange
|
||||
}: PropertyPanelInputProps) {
|
||||
|
||||
// Determine if we should show expression UI
|
||||
const showExpressionToggle = supportsExpression && !isConnected;
|
||||
const isExpressionMode = expressionMode === 'expression';
|
||||
|
||||
// Handle mode toggle
|
||||
const handleToggleMode = () => {
|
||||
if (onExpressionModeChange) {
|
||||
onExpressionModeChange(isExpressionMode ? 'fixed' : 'expression');
|
||||
}
|
||||
};
|
||||
|
||||
// Render expression input or standard input
|
||||
const renderInput = () => {
|
||||
if (isExpressionMode && onExpressionChange) {
|
||||
return (
|
||||
<ExpressionInput
|
||||
expression={expression || ''}
|
||||
onChange={onExpressionChange}
|
||||
expectedType={inputType}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Standard input rendering (existing code)
|
||||
const Input = useMemo(() => {
|
||||
switch (inputType) {
|
||||
case PropertyPanelInputType.Text:
|
||||
return PropertyPanelTextInput;
|
||||
// ... rest of existing switch
|
||||
}
|
||||
}, [inputType]);
|
||||
|
||||
return (
|
||||
<Input
|
||||
value={value}
|
||||
properties={properties}
|
||||
isChanged={isChanged}
|
||||
isConnected={isConnected}
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={css.container}>
|
||||
<label className={css.label}>{label}</label>
|
||||
<div className={css.inputRow}>
|
||||
{renderInput()}
|
||||
{showExpressionToggle && (
|
||||
<ExpressionToggle
|
||||
mode={expressionMode}
|
||||
isConnected={isConnected}
|
||||
onToggle={handleToggleMode}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Step 5: Wire Up to Property Editor
|
||||
|
||||
**Modify:** `packages/noodl-editor/src/editor/src/views/panels/propertyeditor/DataTypes/BasicType.ts`
|
||||
|
||||
This is where the connection between the model and property panel happens. Add expression support:
|
||||
|
||||
```typescript
|
||||
// In the render or value handling logic:
|
||||
|
||||
// Check if current parameter value is an expression
|
||||
const paramValue = parent.model.parameters[port.name];
|
||||
const isExpressionMode = isExpressionParameter(paramValue);
|
||||
|
||||
// When mode changes:
|
||||
onExpressionModeChange(mode) {
|
||||
if (mode === 'expression') {
|
||||
// Convert current value to expression parameter
|
||||
const currentValue = parent.model.parameters[port.name];
|
||||
parent.model.setParameter(port.name, {
|
||||
mode: 'expression',
|
||||
expression: String(currentValue || ''),
|
||||
fallback: currentValue,
|
||||
version: ExpressionEvaluator.EXPRESSION_VERSION
|
||||
});
|
||||
} else {
|
||||
// Convert back to fixed value
|
||||
const param = parent.model.parameters[port.name];
|
||||
const fixedValue = isExpressionParameter(param) ? param.fallback : param;
|
||||
parent.model.setParameter(port.name, fixedValue);
|
||||
}
|
||||
}
|
||||
|
||||
// When expression changes:
|
||||
onExpressionChange(expression) {
|
||||
const param = parent.model.parameters[port.name];
|
||||
if (isExpressionParameter(param)) {
|
||||
parent.model.setParameter(port.name, {
|
||||
...param,
|
||||
expression
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 6: Runtime Expression Evaluation
|
||||
|
||||
**Modify:** `packages/noodl-runtime/src/node.js`
|
||||
|
||||
Add expression evaluation to the parameter update flow:
|
||||
|
||||
```javascript
|
||||
// In Node.prototype._onNodeModelParameterUpdated or similar:
|
||||
|
||||
Node.prototype._evaluateExpressionParameter = function(paramName, paramValue) {
|
||||
const ExpressionEvaluator = require('./expression-evaluator');
|
||||
|
||||
if (!paramValue || paramValue.mode !== 'expression') {
|
||||
return paramValue;
|
||||
}
|
||||
|
||||
// Compile and evaluate
|
||||
const compiled = ExpressionEvaluator.compileExpression(paramValue.expression);
|
||||
if (!compiled) {
|
||||
return paramValue.fallback;
|
||||
}
|
||||
|
||||
const result = ExpressionEvaluator.evaluateExpression(compiled, this.context?.modelScope);
|
||||
|
||||
// Set up reactive subscription if not already
|
||||
if (!this._expressionSubscriptions) {
|
||||
this._expressionSubscriptions = {};
|
||||
}
|
||||
|
||||
if (!this._expressionSubscriptions[paramName]) {
|
||||
const deps = ExpressionEvaluator.detectDependencies(paramValue.expression);
|
||||
if (deps.variables.length > 0 || deps.objects.length > 0 || deps.arrays.length > 0) {
|
||||
this._expressionSubscriptions[paramName] = ExpressionEvaluator.subscribeToChanges(
|
||||
deps,
|
||||
() => {
|
||||
// Re-evaluate and update
|
||||
const newResult = ExpressionEvaluator.evaluateExpression(compiled, this.context?.modelScope);
|
||||
this.queueInput(paramName, newResult);
|
||||
},
|
||||
this.context?.modelScope
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return result !== undefined ? result : paramValue.fallback;
|
||||
};
|
||||
|
||||
// Clean up subscriptions on delete
|
||||
Node.prototype._onNodeDeleted = function() {
|
||||
// ... existing cleanup ...
|
||||
|
||||
// Clean up expression subscriptions
|
||||
if (this._expressionSubscriptions) {
|
||||
for (const unsub of Object.values(this._expressionSubscriptions)) {
|
||||
if (typeof unsub === 'function') unsub();
|
||||
}
|
||||
this._expressionSubscriptions = null;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### Step 7: Expression Builder Modal (Optional Enhancement)
|
||||
|
||||
**Create file:** `packages/noodl-editor/src/editor/src/views/panels/propertyeditor/ExpressionBuilder/ExpressionBuilder.tsx`
|
||||
|
||||
A full-featured modal for complex expression editing:
|
||||
|
||||
```tsx
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import { Modal } from '@noodl-core-ui/components/layout/Modal';
|
||||
import { MonacoEditor } from '@noodl-core-ui/components/inputs/MonacoEditor';
|
||||
import { TreeView } from '@noodl-core-ui/components/tree/TreeView';
|
||||
import css from './ExpressionBuilder.module.scss';
|
||||
|
||||
interface ExpressionBuilderProps {
|
||||
isOpen: boolean;
|
||||
expression: string;
|
||||
expectedType?: string;
|
||||
onApply: (expression: string) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export function ExpressionBuilder({
|
||||
isOpen,
|
||||
expression: initialExpression,
|
||||
expectedType,
|
||||
onApply,
|
||||
onCancel
|
||||
}: ExpressionBuilderProps) {
|
||||
const [expression, setExpression] = useState(initialExpression);
|
||||
const [preview, setPreview] = useState<{ result: any; error?: string }>({ result: null });
|
||||
|
||||
// Build available completions tree
|
||||
const completionsTree = useMemo(() => {
|
||||
// This would be populated from actual project data
|
||||
return [
|
||||
{
|
||||
label: 'Noodl',
|
||||
children: [
|
||||
{
|
||||
label: 'Variables',
|
||||
children: [] // Populated from Noodl.Variables
|
||||
},
|
||||
{
|
||||
label: 'Objects',
|
||||
children: [] // Populated from known Object IDs
|
||||
},
|
||||
{
|
||||
label: 'Arrays',
|
||||
children: [] // Populated from known Array IDs
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Math',
|
||||
children: [
|
||||
{ label: 'min(a, b)', insertText: 'min()' },
|
||||
{ label: 'max(a, b)', insertText: 'max()' },
|
||||
{ label: 'round(n)', insertText: 'round()' },
|
||||
{ label: 'floor(n)', insertText: 'floor()' },
|
||||
{ label: 'ceil(n)', insertText: 'ceil()' },
|
||||
{ label: 'abs(n)', insertText: 'abs()' },
|
||||
{ label: 'sqrt(n)', insertText: 'sqrt()' },
|
||||
{ label: 'pow(base, exp)', insertText: 'pow()' },
|
||||
{ label: 'pi', insertText: 'pi' },
|
||||
{ label: 'random()', insertText: 'random()' }
|
||||
]
|
||||
}
|
||||
];
|
||||
}, []);
|
||||
|
||||
// Live preview
|
||||
useEffect(() => {
|
||||
const ExpressionEvaluator = require('@noodl/runtime/src/expression-evaluator');
|
||||
const validation = ExpressionEvaluator.validateExpression(expression);
|
||||
|
||||
if (!validation.valid) {
|
||||
setPreview({ result: null, error: validation.error });
|
||||
return;
|
||||
}
|
||||
|
||||
const compiled = ExpressionEvaluator.compileExpression(expression);
|
||||
if (compiled) {
|
||||
const result = ExpressionEvaluator.evaluateExpression(compiled);
|
||||
setPreview({ result, error: undefined });
|
||||
}
|
||||
}, [expression]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={onCancel}
|
||||
title="Expression Builder"
|
||||
size="large"
|
||||
>
|
||||
<div className={css.container}>
|
||||
<div className={css.editor}>
|
||||
<MonacoEditor
|
||||
value={expression}
|
||||
onChange={setExpression}
|
||||
language="javascript"
|
||||
options={{
|
||||
minimap: { enabled: false },
|
||||
lineNumbers: 'off',
|
||||
fontSize: 14,
|
||||
wordWrap: 'on'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={css.sidebar}>
|
||||
<div className={css.completions}>
|
||||
<h4>Available</h4>
|
||||
<TreeView
|
||||
items={completionsTree}
|
||||
onItemClick={(item) => {
|
||||
// Insert at cursor
|
||||
if (item.insertText) {
|
||||
setExpression(prev => prev + item.insertText);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={css.preview}>
|
||||
<h4>Preview</h4>
|
||||
{preview.error ? (
|
||||
<div className={css.error}>{preview.error}</div>
|
||||
) : (
|
||||
<div className={css.result}>
|
||||
<div className={css.resultLabel}>Result:</div>
|
||||
<div className={css.resultValue}>
|
||||
{JSON.stringify(preview.result)}
|
||||
</div>
|
||||
<div className={css.resultType}>
|
||||
Type: {typeof preview.result}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={css.actions}>
|
||||
<button onClick={onCancel}>Cancel</button>
|
||||
<button onClick={() => onApply(expression)}>Apply</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Step 8: Add Keyboard Shortcuts
|
||||
|
||||
**Modify:** `packages/noodl-editor/src/editor/src/constants/Keybindings.ts`
|
||||
|
||||
```typescript
|
||||
export namespace Keybindings {
|
||||
// ... existing keybindings ...
|
||||
|
||||
// Expression shortcuts (new)
|
||||
export const TOGGLE_EXPRESSION_MODE = new Keybinding(KeyMod.CtrlCmd, KeyCode.KEY_E);
|
||||
export const OPEN_EXPRESSION_BUILDER = new Keybinding(KeyMod.CtrlCmd, KeyMod.Shift, KeyCode.KEY_E);
|
||||
}
|
||||
```
|
||||
|
||||
### Step 9: Handle Property Types
|
||||
|
||||
Different property types need type-appropriate expression handling:
|
||||
|
||||
| Property Type | Expression Returns | Coercion |
|
||||
|--------------|-------------------|----------|
|
||||
| `string` | Any → String | `String(result)` |
|
||||
| `number` | Number | `Number(result) \|\| fallback` |
|
||||
| `boolean` | Truthy/Falsy | `!!result` |
|
||||
| `color` | Hex/RGB string | Validate format |
|
||||
| `enum` | Enum value string | Validate against options |
|
||||
| `component` | Component name | Validate exists |
|
||||
|
||||
**Create file:** `packages/noodl-runtime/src/expression-type-coercion.js`
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Coerce expression result to expected property type
|
||||
*/
|
||||
function coerceToType(value, expectedType, fallback, enumOptions) {
|
||||
if (value === undefined || value === null) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
switch (expectedType) {
|
||||
case 'string':
|
||||
return String(value);
|
||||
|
||||
case 'number':
|
||||
const num = Number(value);
|
||||
return isNaN(num) ? fallback : num;
|
||||
|
||||
case 'boolean':
|
||||
return !!value;
|
||||
|
||||
case 'color':
|
||||
const str = String(value);
|
||||
// Basic validation for hex or rgb
|
||||
if (/^#[0-9A-Fa-f]{6}$/.test(str) || /^rgba?\(/.test(str)) {
|
||||
return str;
|
||||
}
|
||||
return fallback;
|
||||
|
||||
case 'enum':
|
||||
const enumVal = String(value);
|
||||
if (enumOptions && enumOptions.some(opt =>
|
||||
opt === enumVal || opt.value === enumVal
|
||||
)) {
|
||||
return enumVal;
|
||||
}
|
||||
return fallback;
|
||||
|
||||
default:
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { coerceToType };
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria
|
||||
|
||||
### Functional Requirements
|
||||
|
||||
- [ ] Expression toggle button appears on supported property types
|
||||
- [ ] Toggle switches between fixed and expression modes
|
||||
- [ ] Expression mode shows `fx` badge and code-style input
|
||||
- [ ] Expression evaluates correctly at runtime
|
||||
- [ ] Expression re-evaluates when dependencies change
|
||||
- [ ] Connected ports (via cables) disable expression mode
|
||||
- [ ] Type coercion works for each property type
|
||||
- [ ] Invalid expressions show error state
|
||||
- [ ] Copy/paste expressions works
|
||||
- [ ] Expression builder modal opens (Cmd+Shift+E)
|
||||
- [ ] Undo/redo works for expression changes
|
||||
|
||||
### Property Types Supported
|
||||
|
||||
- [ ] String (`PropertyPanelTextInput`)
|
||||
- [ ] Number (`PropertyPanelNumberInput`)
|
||||
- [ ] Number with units (`PropertyPanelLengthUnitInput`)
|
||||
- [ ] Boolean (`PropertyPanelCheckbox`)
|
||||
- [ ] Select/Enum (`PropertyPanelSelectInput`)
|
||||
- [ ] Slider (`PropertyPanelSliderInput`)
|
||||
- [ ] Color (`ColorType` / color picker)
|
||||
|
||||
### Non-Functional Requirements
|
||||
|
||||
- [ ] No performance regression in property panel rendering
|
||||
- [ ] Expressions compile once, evaluate efficiently
|
||||
- [ ] Memory cleanup when nodes are deleted
|
||||
- [ ] Backward compatibility with existing projects
|
||||
|
||||
---
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
### Manual Testing
|
||||
|
||||
1. **Basic Toggle**
|
||||
- Select a Group node
|
||||
- Find the "Margin Left" property
|
||||
- Click expression toggle button
|
||||
- Verify UI changes to expression mode
|
||||
- Toggle back to fixed mode
|
||||
- Verify original value is preserved
|
||||
|
||||
2. **Expression Evaluation**
|
||||
- Set a Group's margin to expression mode
|
||||
- Enter: `Noodl.Variables.spacing || 16`
|
||||
- Set `Noodl.Variables.spacing = 32` in a Function node
|
||||
- Verify margin updates to 32
|
||||
|
||||
3. **Reactive Updates**
|
||||
- Create expression: `Noodl.Variables.isExpanded ? 200 : 50`
|
||||
- Add button that toggles `Noodl.Variables.isExpanded`
|
||||
- Click button, verify property updates
|
||||
|
||||
4. **Connected Port Behavior**
|
||||
- Connect an output to a property input
|
||||
- Verify expression toggle is disabled/hidden
|
||||
- Disconnect
|
||||
- Verify toggle is available again
|
||||
|
||||
5. **Type Coercion**
|
||||
- Number property with expression returning string "42"
|
||||
- Verify it coerces to number 42
|
||||
- Boolean property with expression returning "yes"
|
||||
- Verify it coerces to true
|
||||
|
||||
6. **Error Handling**
|
||||
- Enter invalid expression: `1 +`
|
||||
- Verify error indicator appears
|
||||
- Verify property uses fallback value
|
||||
- Fix expression
|
||||
- Verify error clears
|
||||
|
||||
7. **Undo/Redo**
|
||||
- Change property to expression mode
|
||||
- Undo (Cmd+Z)
|
||||
- Verify returns to fixed mode
|
||||
- Redo
|
||||
- Verify returns to expression mode
|
||||
|
||||
8. **Project Save/Load**
|
||||
- Create property with expression
|
||||
- Save project
|
||||
- Close and reopen project
|
||||
- Verify expression is preserved and working
|
||||
|
||||
### Property Type Coverage
|
||||
|
||||
- [ ] Text input with expression
|
||||
- [ ] Number input with expression
|
||||
- [ ] Number with units (px, %, etc.) with expression
|
||||
- [ ] Checkbox/boolean with expression
|
||||
- [ ] Dropdown/select with expression
|
||||
- [ ] Color picker with expression
|
||||
- [ ] Slider with expression
|
||||
|
||||
### Edge Cases
|
||||
|
||||
- [ ] Expression referencing non-existent variable
|
||||
- [ ] Expression with runtime error (division by zero)
|
||||
- [ ] Very long expression
|
||||
- [ ] Expression with special characters
|
||||
- [ ] Expression in visual state parameter
|
||||
- [ ] Expression in variant parameter
|
||||
|
||||
---
|
||||
|
||||
## Migration Considerations
|
||||
|
||||
### Existing Projects
|
||||
|
||||
- Existing projects have simple parameter values
|
||||
- These continue to work as-is (backward compatible)
|
||||
- No automatic migration needed
|
||||
|
||||
### Future Expression Version Changes
|
||||
|
||||
If we need to change the expression context in the future:
|
||||
1. Increment `EXPRESSION_VERSION` in expression-evaluator.js
|
||||
2. Add migration logic to handle old version expressions
|
||||
3. Show warning for expressions with old version
|
||||
|
||||
---
|
||||
|
||||
## Notes for Implementer
|
||||
|
||||
### Important Patterns
|
||||
|
||||
1. **Model-View Separation**
|
||||
- Property panel is the view
|
||||
- NodeGraphNode.parameters is the model
|
||||
- Changes go through `setParameter()` for undo support
|
||||
|
||||
2. **Port Connection Priority**
|
||||
- Connected ports take precedence over expressions
|
||||
- Connected ports take precedence over fixed values
|
||||
- This is existing behavior, preserve it
|
||||
|
||||
3. **Visual States**
|
||||
- Visual state parameters use `paramName_stateName` pattern
|
||||
- Expression parameters in visual states need same pattern
|
||||
- Example: `marginLeft_hover` could be an expression
|
||||
|
||||
### Edge Cases to Handle
|
||||
|
||||
1. **Expression references port that's also connected**
|
||||
- Expression should still work
|
||||
- Connected value might be available via `this.inputs.X`
|
||||
|
||||
2. **Circular expressions**
|
||||
- Expression A references Variable that's set by Expression B
|
||||
- Shouldn't cause infinite loop (dependency tracking prevents)
|
||||
|
||||
3. **Expressions in cloud runtime**
|
||||
- Cloud uses different Noodl.js API
|
||||
- Ensure expression-evaluator works in both contexts
|
||||
|
||||
### Questions to Resolve
|
||||
|
||||
1. **Which property types should NOT support expressions?**
|
||||
- Recommendation: component picker, image picker
|
||||
- These need special UI that doesn't fit expression pattern
|
||||
|
||||
2. **Should expressions work in style properties?**
|
||||
- Recommendation: Yes, if using inputCss pattern
|
||||
- CSS values often need to be dynamic
|
||||
|
||||
3. **Mobile/responsive expressions?**
|
||||
- Recommendation: Expressions can reference `Noodl.Variables.screenWidth`
|
||||
- Combine with existing variants system
|
||||
|
||||
---
|
||||
|
||||
## Files Created/Modified Summary
|
||||
|
||||
### New Files
|
||||
- `packages/noodl-core-ui/src/components/property-panel/ExpressionToggle/ExpressionToggle.tsx`
|
||||
- `packages/noodl-core-ui/src/components/property-panel/ExpressionToggle/ExpressionToggle.module.scss`
|
||||
- `packages/noodl-core-ui/src/components/property-panel/ExpressionInput/ExpressionInput.tsx`
|
||||
- `packages/noodl-core-ui/src/components/property-panel/ExpressionInput/ExpressionInput.module.scss`
|
||||
- `packages/noodl-editor/src/editor/src/views/panels/propertyeditor/ExpressionBuilder/ExpressionBuilder.tsx`
|
||||
- `packages/noodl-runtime/src/expression-type-coercion.js`
|
||||
|
||||
### Modified Files
|
||||
- `packages/noodl-core-ui/src/components/property-panel/PropertyPanelInput/PropertyPanelInput.tsx`
|
||||
- `packages/noodl-editor/src/editor/src/views/panels/propertyeditor/DataTypes/BasicType.ts`
|
||||
- `packages/noodl-editor/src/editor/src/views/panels/propertyeditor/DataTypes/BooleanType.ts`
|
||||
- `packages/noodl-editor/src/editor/src/views/panels/propertyeditor/DataTypes/ColorType.ts`
|
||||
- `packages/noodl-editor/src/editor/src/models/nodegraphmodel/NodeGraphNode.ts`
|
||||
- `packages/noodl-runtime/src/node.js`
|
||||
- `packages/noodl-editor/src/editor/src/constants/Keybindings.ts`
|
||||
Reference in New Issue
Block a user