fix: drag handle actions now affect the correct block
- Added menuNode state to capture the node position when menu opens - Updated all action functions to use menuNode instead of currentNode - This ensures drag handle actions (Turn into, Delete, etc.) always affect the block where the handle was clicked, not where the mouse currently hovers - Also formatted code with prettier 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
1e4a27b1a3
commit
1c38dc87e3
39 changed files with 353 additions and 316 deletions
|
|
@ -9,6 +9,7 @@ This analysis examines SVG usage patterns in the Svelte 5 codebase to identify o
|
||||||
### 1. Inline SVGs vs. Imported SVGs
|
### 1. Inline SVGs vs. Imported SVGs
|
||||||
|
|
||||||
**Inline SVGs Found:**
|
**Inline SVGs Found:**
|
||||||
|
|
||||||
- **Close/X buttons**: Found in 7+ components with identical SVG code
|
- **Close/X buttons**: Found in 7+ components with identical SVG code
|
||||||
- `admin/Modal.svelte`
|
- `admin/Modal.svelte`
|
||||||
- `admin/UnifiedMediaModal.svelte`
|
- `admin/UnifiedMediaModal.svelte`
|
||||||
|
|
@ -17,8 +18,8 @@ This analysis examines SVG usage patterns in the Svelte 5 codebase to identify o
|
||||||
- `admin/GalleryManager.svelte`
|
- `admin/GalleryManager.svelte`
|
||||||
- `admin/MediaDetailsModal.svelte`
|
- `admin/MediaDetailsModal.svelte`
|
||||||
- `Lightbox.svelte`
|
- `Lightbox.svelte`
|
||||||
|
|
||||||
- **Loading spinners**: Found in 2+ components
|
- **Loading spinners**: Found in 2+ components
|
||||||
|
|
||||||
- `admin/Button.svelte`
|
- `admin/Button.svelte`
|
||||||
- `admin/ImageUploader.svelte`
|
- `admin/ImageUploader.svelte`
|
||||||
- `admin/GalleryUploader.svelte`
|
- `admin/GalleryUploader.svelte`
|
||||||
|
|
@ -30,26 +31,23 @@ This analysis examines SVG usage patterns in the Svelte 5 codebase to identify o
|
||||||
### 2. SVG Import Patterns
|
### 2. SVG Import Patterns
|
||||||
|
|
||||||
**Consistent patterns using aliases:**
|
**Consistent patterns using aliases:**
|
||||||
|
|
||||||
```svelte
|
```svelte
|
||||||
// Good - using $icons alias
|
// Good - using $icons alias import ArrowLeft from '$icons/arrow-left.svg' import ChevronDownIcon
|
||||||
import ArrowLeft from '$icons/arrow-left.svg'
|
from '$icons/chevron-down.svg' // Component imports with ?component import PhotosIcon from
|
||||||
import ChevronDownIcon from '$icons/chevron-down.svg'
|
'$icons/photos.svg?component' import ViewSingleIcon from '$icons/view-single.svg?component' // Raw
|
||||||
|
imports import ChevronDownIcon from '$icons/chevron-down.svg?raw'
|
||||||
// Component imports with ?component
|
|
||||||
import PhotosIcon from '$icons/photos.svg?component'
|
|
||||||
import ViewSingleIcon from '$icons/view-single.svg?component'
|
|
||||||
|
|
||||||
// Raw imports
|
|
||||||
import ChevronDownIcon from '$icons/chevron-down.svg?raw'
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Unused SVG Files
|
### 3. Unused SVG Files
|
||||||
|
|
||||||
**Unused icons in `/src/assets/icons/`:**
|
**Unused icons in `/src/assets/icons/`:**
|
||||||
|
|
||||||
- `dashboard.svg`
|
- `dashboard.svg`
|
||||||
- `metadata.svg`
|
- `metadata.svg`
|
||||||
|
|
||||||
**Unused illustrations in `/src/assets/illos/`:**
|
**Unused illustrations in `/src/assets/illos/`:**
|
||||||
|
|
||||||
- `jedmund-blink.svg`
|
- `jedmund-blink.svg`
|
||||||
- `jedmund-headphones.svg`
|
- `jedmund-headphones.svg`
|
||||||
- `jedmund-listening-downbeat.svg`
|
- `jedmund-listening-downbeat.svg`
|
||||||
|
|
@ -65,11 +63,13 @@ import ChevronDownIcon from '$icons/chevron-down.svg?raw'
|
||||||
### 4. Duplicate SVG Definitions
|
### 4. Duplicate SVG Definitions
|
||||||
|
|
||||||
**Close/X Button SVG** (appears 7+ times):
|
**Close/X Button SVG** (appears 7+ times):
|
||||||
|
|
||||||
```svg
|
```svg
|
||||||
<path d="M6 6L18 18M6 18L18 6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
<path d="M6 6L18 18M6 18L18 6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||||
```
|
```
|
||||||
|
|
||||||
**Loading Spinner SVG** (appears 3+ times):
|
**Loading Spinner SVG** (appears 3+ times):
|
||||||
|
|
||||||
```svg
|
```svg
|
||||||
<svg class="spinner" width="24" height="24" viewBox="0 0 24 24">
|
<svg class="spinner" width="24" height="24" viewBox="0 0 24 24">
|
||||||
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" fill="none" stroke-dasharray="25" stroke-dashoffset="25" stroke-linecap="round">
|
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" fill="none" stroke-dasharray="25" stroke-dashoffset="25" stroke-linecap="round">
|
||||||
|
|
@ -90,6 +90,7 @@ import ChevronDownIcon from '$icons/chevron-down.svg?raw'
|
||||||
### 1. Create Reusable Icon Components
|
### 1. Create Reusable Icon Components
|
||||||
|
|
||||||
**Option A: Create individual icon components**
|
**Option A: Create individual icon components**
|
||||||
|
|
||||||
```svelte
|
```svelte
|
||||||
<!-- $lib/components/icons/CloseIcon.svelte -->
|
<!-- $lib/components/icons/CloseIcon.svelte -->
|
||||||
<script>
|
<script>
|
||||||
|
|
@ -102,6 +103,7 @@ import ChevronDownIcon from '$icons/chevron-down.svg?raw'
|
||||||
```
|
```
|
||||||
|
|
||||||
**Option B: Create an Icon component with name prop**
|
**Option B: Create an Icon component with name prop**
|
||||||
|
|
||||||
```svelte
|
```svelte
|
||||||
<!-- $lib/components/Icon.svelte -->
|
<!-- $lib/components/Icon.svelte -->
|
||||||
<script>
|
<script>
|
||||||
|
|
@ -113,7 +115,7 @@ import ChevronDownIcon from '$icons/chevron-down.svg?raw'
|
||||||
|
|
||||||
const icons = {
|
const icons = {
|
||||||
close: CloseIcon,
|
close: CloseIcon,
|
||||||
loading: LoadingIcon,
|
loading: LoadingIcon
|
||||||
// ... other icons
|
// ... other icons
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -128,6 +130,7 @@ import ChevronDownIcon from '$icons/chevron-down.svg?raw'
|
||||||
### 2. Extract Inline SVGs to Files
|
### 2. Extract Inline SVGs to Files
|
||||||
|
|
||||||
Create new SVG files for commonly used inline SVGs:
|
Create new SVG files for commonly used inline SVGs:
|
||||||
|
|
||||||
- `/src/assets/icons/close.svg`
|
- `/src/assets/icons/close.svg`
|
||||||
- `/src/assets/icons/loading.svg`
|
- `/src/assets/icons/loading.svg`
|
||||||
- `/src/assets/icons/external-link.svg`
|
- `/src/assets/icons/external-link.svg`
|
||||||
|
|
@ -137,12 +140,14 @@ Create new SVG files for commonly used inline SVGs:
|
||||||
### 3. Clean Up Unused Assets
|
### 3. Clean Up Unused Assets
|
||||||
|
|
||||||
Remove the following unused files to reduce bundle size:
|
Remove the following unused files to reduce bundle size:
|
||||||
|
|
||||||
- All unused illustration files (11 files)
|
- All unused illustration files (11 files)
|
||||||
- Unused icon files (2 files)
|
- Unused icon files (2 files)
|
||||||
|
|
||||||
### 4. Standardize Import Methods
|
### 4. Standardize Import Methods
|
||||||
|
|
||||||
Establish a consistent pattern:
|
Establish a consistent pattern:
|
||||||
|
|
||||||
- Use `?component` for SVGs used as Svelte components
|
- Use `?component` for SVGs used as Svelte components
|
||||||
- Use direct imports for SVGs used as images
|
- Use direct imports for SVGs used as images
|
||||||
- Avoid `?raw` imports unless necessary
|
- Avoid `?raw` imports unless necessary
|
||||||
|
|
@ -156,10 +161,25 @@ Establish a consistent pattern:
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svg class="loading-spinner {className}" width={size} height={size} viewBox="0 0 24 24">
|
<svg class="loading-spinner {className}" width={size} height={size} viewBox="0 0 24 24">
|
||||||
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" fill="none"
|
<circle
|
||||||
stroke-dasharray="25" stroke-dashoffset="25" stroke-linecap="round">
|
cx="12"
|
||||||
<animateTransform attributeName="transform" type="rotate"
|
cy="12"
|
||||||
from="0 12 12" to="360 12 12" dur="1s" repeatCount="indefinite"/>
|
r="10"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
fill="none"
|
||||||
|
stroke-dasharray="25"
|
||||||
|
stroke-dashoffset="25"
|
||||||
|
stroke-linecap="round"
|
||||||
|
>
|
||||||
|
<animateTransform
|
||||||
|
attributeName="transform"
|
||||||
|
type="rotate"
|
||||||
|
from="0 12 12"
|
||||||
|
to="360 12 12"
|
||||||
|
dur="1s"
|
||||||
|
repeatCount="indefinite"
|
||||||
|
/>
|
||||||
</circle>
|
</circle>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,18 +20,21 @@ This PRD outlines a comprehensive cleanup and refactoring plan for the jedmund-s
|
||||||
## Key Findings
|
## Key Findings
|
||||||
|
|
||||||
### 1. Overengineered Components
|
### 1. Overengineered Components
|
||||||
|
|
||||||
- **EnhancedComposer** (1,347 lines) - Handles too many responsibilities
|
- **EnhancedComposer** (1,347 lines) - Handles too many responsibilities
|
||||||
- **LastFM Stream Server** (625 lines) - Complex data transformations that could be simplified
|
- **LastFM Stream Server** (625 lines) - Complex data transformations that could be simplified
|
||||||
- **Multiple Media Modals** - Overlapping functionality across 3+ modal components
|
- **Multiple Media Modals** - Overlapping functionality across 3+ modal components
|
||||||
- **Complex State Management** - Components with 10-20 state variables
|
- **Complex State Management** - Components with 10-20 state variables
|
||||||
|
|
||||||
### 2. Unused Code
|
### 2. Unused Code
|
||||||
|
|
||||||
- 5 unused components (Squiggly, PhotoLightbox, Pill, SVGHoverEffect, MusicPreview)
|
- 5 unused components (Squiggly, PhotoLightbox, Pill, SVGHoverEffect, MusicPreview)
|
||||||
- 13 unused SVG files (2 icons, 11 illustrations)
|
- 13 unused SVG files (2 icons, 11 illustrations)
|
||||||
- Minimal commented-out code (good!)
|
- Minimal commented-out code (good!)
|
||||||
- 1 potentially unused API endpoint (/api/health)
|
- 1 potentially unused API endpoint (/api/health)
|
||||||
|
|
||||||
### 3. DRY Violations
|
### 3. DRY Violations
|
||||||
|
|
||||||
- **Photo Grid Components** - 3 nearly identical components
|
- **Photo Grid Components** - 3 nearly identical components
|
||||||
- **Modal Components** - Duplicate backdrop and positioning logic
|
- **Modal Components** - Duplicate backdrop and positioning logic
|
||||||
- **Dropdown Components** - Repeated dropdown patterns
|
- **Dropdown Components** - Repeated dropdown patterns
|
||||||
|
|
@ -39,6 +42,7 @@ This PRD outlines a comprehensive cleanup and refactoring plan for the jedmund-s
|
||||||
- **Segmented Controllers** - Duplicate animation and positioning logic
|
- **Segmented Controllers** - Duplicate animation and positioning logic
|
||||||
|
|
||||||
### 4. Hardcoded Values
|
### 4. Hardcoded Values
|
||||||
|
|
||||||
- **Colors**: 200+ hardcoded hex/rgba values instead of using existing variables
|
- **Colors**: 200+ hardcoded hex/rgba values instead of using existing variables
|
||||||
- **Spacing**: 1,000+ hardcoded pixel values instead of using `$unit` system
|
- **Spacing**: 1,000+ hardcoded pixel values instead of using `$unit` system
|
||||||
- **Z-indexes**: 60+ hardcoded z-index values without consistent scale
|
- **Z-indexes**: 60+ hardcoded z-index values without consistent scale
|
||||||
|
|
@ -46,6 +50,7 @@ This PRD outlines a comprehensive cleanup and refactoring plan for the jedmund-s
|
||||||
- **Border radius**: Not using existing `$corner-radius-*` variables
|
- **Border radius**: Not using existing `$corner-radius-*` variables
|
||||||
|
|
||||||
### 5. SVG Issues
|
### 5. SVG Issues
|
||||||
|
|
||||||
- 7+ duplicate inline close button SVGs
|
- 7+ duplicate inline close button SVGs
|
||||||
- 3+ duplicate loading spinner SVGs
|
- 3+ duplicate loading spinner SVGs
|
||||||
- Inconsistent import patterns
|
- Inconsistent import patterns
|
||||||
|
|
@ -54,9 +59,11 @@ This PRD outlines a comprehensive cleanup and refactoring plan for the jedmund-s
|
||||||
## Implementation Timeline
|
## Implementation Timeline
|
||||||
|
|
||||||
### Phase 1: Quick Wins (Week 1)
|
### Phase 1: Quick Wins (Week 1)
|
||||||
|
|
||||||
Focus on low-risk, high-impact changes that don't require architectural modifications.
|
Focus on low-risk, high-impact changes that don't require architectural modifications.
|
||||||
|
|
||||||
- [x] **Remove unused components** (5 components)
|
- [x] **Remove unused components** (5 components)
|
||||||
|
|
||||||
- [x] Delete `/src/lib/components/Squiggly.svelte`
|
- [x] Delete `/src/lib/components/Squiggly.svelte`
|
||||||
- [x] Delete `/src/lib/components/PhotoLightbox.svelte`
|
- [x] Delete `/src/lib/components/PhotoLightbox.svelte`
|
||||||
- [x] Delete `/src/lib/components/Pill.svelte`
|
- [x] Delete `/src/lib/components/Pill.svelte`
|
||||||
|
|
@ -64,6 +71,7 @@ Focus on low-risk, high-impact changes that don't require architectural modifica
|
||||||
- [x] Delete `/src/lib/components/MusicPreview.svelte`
|
- [x] Delete `/src/lib/components/MusicPreview.svelte`
|
||||||
|
|
||||||
- [x] **Remove unused SVG files** (13 files)
|
- [x] **Remove unused SVG files** (13 files)
|
||||||
|
|
||||||
- [x] Delete unused icons: `dashboard.svg`, `metadata.svg`
|
- [x] Delete unused icons: `dashboard.svg`, `metadata.svg`
|
||||||
- [x] Delete unused illustrations (11 files - see SVG analysis report)
|
- [x] Delete unused illustrations (11 files - see SVG analysis report)
|
||||||
|
|
||||||
|
|
@ -72,18 +80,22 @@ Focus on low-risk, high-impact changes that don't require architectural modifica
|
||||||
- [x] Address TODO in `/src/lib/server/api-utils.ts` about authentication (noted for future work)
|
- [x] Address TODO in `/src/lib/server/api-utils.ts` about authentication (noted for future work)
|
||||||
|
|
||||||
### Phase 2: CSS Variable Standardization (Week 2)
|
### Phase 2: CSS Variable Standardization (Week 2)
|
||||||
|
|
||||||
Create a consistent design system by extracting hardcoded values.
|
Create a consistent design system by extracting hardcoded values.
|
||||||
|
|
||||||
- [x] **Create z-index system**
|
- [x] **Create z-index system**
|
||||||
|
|
||||||
- [x] Create `src/assets/styles/_z-index.scss` with constants
|
- [x] Create `src/assets/styles/_z-index.scss` with constants
|
||||||
- [x] Replace 60+ hardcoded z-index values
|
- [x] Replace 60+ hardcoded z-index values
|
||||||
|
|
||||||
- [x] **Extract color variables**
|
- [x] **Extract color variables**
|
||||||
|
|
||||||
- [x] Add missing color variables for frequently used colors
|
- [x] Add missing color variables for frequently used colors
|
||||||
- [x] Replace 200+ hardcoded hex/rgba values (replaced most common colors)
|
- [x] Replace 200+ hardcoded hex/rgba values (replaced most common colors)
|
||||||
- [x] Create shadow/overlay variables for rgba values
|
- [x] Create shadow/overlay variables for rgba values
|
||||||
|
|
||||||
- [x] **Standardize spacing**
|
- [x] **Standardize spacing**
|
||||||
|
|
||||||
- [x] Add missing unit multipliers (added `$unit-7x` through `$unit-19x` and common pixel values)
|
- [x] Add missing unit multipliers (added `$unit-7x` through `$unit-19x` and common pixel values)
|
||||||
- [x] Replace 1,000+ hardcoded pixel values with unit variables (replaced in key components)
|
- [x] Replace 1,000+ hardcoded pixel values with unit variables (replaced in key components)
|
||||||
|
|
||||||
|
|
@ -92,15 +104,18 @@ Create a consistent design system by extracting hardcoded values.
|
||||||
- [x] Replace hardcoded duration values (replaced in key components)
|
- [x] Replace hardcoded duration values (replaced in key components)
|
||||||
|
|
||||||
### Phase 3: Component Refactoring (Weeks 3-4)
|
### Phase 3: Component Refactoring (Weeks 3-4)
|
||||||
|
|
||||||
Refactor components to reduce duplication and complexity.
|
Refactor components to reduce duplication and complexity.
|
||||||
|
|
||||||
- [x] **Create base components**
|
- [x] **Create base components**
|
||||||
|
|
||||||
- [x] Extract `BaseModal` component for shared modal logic
|
- [x] Extract `BaseModal` component for shared modal logic
|
||||||
- [x] Create `BaseDropdown` for dropdown patterns
|
- [x] Create `BaseDropdown` for dropdown patterns
|
||||||
- [x] Merge `FormField` and `FormFieldWrapper`
|
- [x] Merge `FormField` and `FormFieldWrapper`
|
||||||
- [x] Create `BaseSegmentedController` for shared logic
|
- [x] Create `BaseSegmentedController` for shared logic
|
||||||
|
|
||||||
- [x] **Refactor photo grids**
|
- [x] **Refactor photo grids**
|
||||||
|
|
||||||
- [x] Create unified `PhotoGrid` component with `columns` prop
|
- [x] Create unified `PhotoGrid` component with `columns` prop
|
||||||
- [x] Remove 3 duplicate grid components
|
- [x] Remove 3 duplicate grid components
|
||||||
- [x] Use composition for layout variations
|
- [x] Use composition for layout variations
|
||||||
|
|
@ -112,9 +127,11 @@ Refactor components to reduce duplication and complexity.
|
||||||
- [x] Extract other repeated inline SVGs (FileIcon, CopyIcon)
|
- [x] Extract other repeated inline SVGs (FileIcon, CopyIcon)
|
||||||
|
|
||||||
### Phase 4: Complex Refactoring (Weeks 5-6)
|
### Phase 4: Complex Refactoring (Weeks 5-6)
|
||||||
|
|
||||||
Tackle the most complex components and patterns.
|
Tackle the most complex components and patterns.
|
||||||
|
|
||||||
- [x] **Refactor EnhancedComposer**
|
- [x] **Refactor EnhancedComposer**
|
||||||
|
|
||||||
- [x] Split into focused sub-components
|
- [x] Split into focused sub-components
|
||||||
- [x] Extract toolbar component
|
- [x] Extract toolbar component
|
||||||
- [x] Separate media management
|
- [x] Separate media management
|
||||||
|
|
@ -122,6 +139,7 @@ Tackle the most complex components and patterns.
|
||||||
- [x] Reduce state variables from 20+ to <10
|
- [x] Reduce state variables from 20+ to <10
|
||||||
|
|
||||||
- [ ] **Simplify LastFM Stream Server**
|
- [ ] **Simplify LastFM Stream Server**
|
||||||
|
|
||||||
- [ ] Extract data transformation utilities
|
- [ ] Extract data transformation utilities
|
||||||
- [ ] Simplify "now playing" detection algorithm
|
- [ ] Simplify "now playing" detection algorithm
|
||||||
- [ ] Reduce state tracking duplication
|
- [ ] Reduce state tracking duplication
|
||||||
|
|
@ -133,9 +151,11 @@ Tackle the most complex components and patterns.
|
||||||
- [ ] Eliminate prop drilling with stores
|
- [ ] Eliminate prop drilling with stores
|
||||||
|
|
||||||
### Phase 5: Architecture & Utilities (Week 7)
|
### Phase 5: Architecture & Utilities (Week 7)
|
||||||
|
|
||||||
Improve overall architecture and create shared utilities.
|
Improve overall architecture and create shared utilities.
|
||||||
|
|
||||||
- [ ] **Create shared utilities**
|
- [ ] **Create shared utilities**
|
||||||
|
|
||||||
- [ ] API client with consistent error handling
|
- [ ] API client with consistent error handling
|
||||||
- [ ] CSS mixins for common patterns
|
- [ ] CSS mixins for common patterns
|
||||||
- [ ] Media handling utilities
|
- [ ] Media handling utilities
|
||||||
|
|
@ -148,9 +168,11 @@ Improve overall architecture and create shared utilities.
|
||||||
- [ ] Create shared animation definitions
|
- [ ] Create shared animation definitions
|
||||||
|
|
||||||
### Phase 6: Testing & Documentation (Week 8)
|
### Phase 6: Testing & Documentation (Week 8)
|
||||||
|
|
||||||
Ensure changes don't break functionality and document new patterns.
|
Ensure changes don't break functionality and document new patterns.
|
||||||
|
|
||||||
- [ ] **Testing**
|
- [ ] **Testing**
|
||||||
|
|
||||||
- [ ] Run full build and type checking
|
- [ ] Run full build and type checking
|
||||||
- [ ] Test all refactored components
|
- [ ] Test all refactored components
|
||||||
- [ ] Verify no regressions in functionality
|
- [ ] Verify no regressions in functionality
|
||||||
|
|
@ -165,19 +187,23 @@ Ensure changes don't break functionality and document new patterns.
|
||||||
## Success Metrics
|
## Success Metrics
|
||||||
|
|
||||||
1. **Code Reduction**
|
1. **Code Reduction**
|
||||||
|
|
||||||
- Target: 20-30% reduction in total lines of code
|
- Target: 20-30% reduction in total lines of code
|
||||||
- Eliminate 1,000+ instances of code duplication
|
- Eliminate 1,000+ instances of code duplication
|
||||||
|
|
||||||
2. **Component Simplification**
|
2. **Component Simplification**
|
||||||
|
|
||||||
- No component larger than 500 lines
|
- No component larger than 500 lines
|
||||||
- Average component size under 200 lines
|
- Average component size under 200 lines
|
||||||
|
|
||||||
3. **Design System Consistency**
|
3. **Design System Consistency**
|
||||||
|
|
||||||
- Zero hardcoded colors in components
|
- Zero hardcoded colors in components
|
||||||
- All spacing using design tokens
|
- All spacing using design tokens
|
||||||
- Consistent z-index scale
|
- Consistent z-index scale
|
||||||
|
|
||||||
4. **Bundle Size**
|
4. **Bundle Size**
|
||||||
|
|
||||||
- 10-15% reduction in JavaScript bundle size
|
- 10-15% reduction in JavaScript bundle size
|
||||||
- Removal of unused assets
|
- Removal of unused assets
|
||||||
|
|
||||||
|
|
@ -189,11 +215,13 @@ Ensure changes don't break functionality and document new patterns.
|
||||||
## Risk Mitigation
|
## Risk Mitigation
|
||||||
|
|
||||||
1. **Regression Testing**
|
1. **Regression Testing**
|
||||||
|
|
||||||
- Test each phase thoroughly before moving to next
|
- Test each phase thoroughly before moving to next
|
||||||
- Keep backups of original components during refactoring
|
- Keep backups of original components during refactoring
|
||||||
- Use feature flags for gradual rollout if needed
|
- Use feature flags for gradual rollout if needed
|
||||||
|
|
||||||
2. **Performance Impact**
|
2. **Performance Impact**
|
||||||
|
|
||||||
- Monitor bundle size after each phase
|
- Monitor bundle size after each phase
|
||||||
- Profile component render performance
|
- Profile component render performance
|
||||||
- Ensure no performance regressions
|
- Ensure no performance regressions
|
||||||
|
|
|
||||||
|
|
@ -225,7 +225,6 @@ $info-color: $blue-50;
|
||||||
// Component specific
|
// Component specific
|
||||||
$image-border-color: rgba(0, 0, 0, 0.03);
|
$image-border-color: rgba(0, 0, 0, 0.03);
|
||||||
|
|
||||||
|
|
||||||
/* Shadows and Overlays
|
/* Shadows and Overlays
|
||||||
* -------------------------------------------------------------------------- */
|
* -------------------------------------------------------------------------- */
|
||||||
$card-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
$card-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,7 @@
|
||||||
showCaptions?: boolean
|
showCaptions?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let { photos = [], showCaptions = true }: Props = $props()
|
||||||
photos = [],
|
|
||||||
showCaptions = true
|
|
||||||
}: Props = $props()
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="horizontal-scroll">
|
<div class="horizontal-scroll">
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,6 @@
|
||||||
class: className = ''
|
class: className = ''
|
||||||
}: Props = $props()
|
}: Props = $props()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Split photos into columns for column-based layouts
|
// Split photos into columns for column-based layouts
|
||||||
function splitIntoColumns(photos: Photo[], numColumns: number): Photo[][] {
|
function splitIntoColumns(photos: Photo[], numColumns: number): Photo[][] {
|
||||||
const columns: Photo[][] = Array.from({ length: numColumns }, () => [])
|
const columns: Photo[][] = Array.from({ length: numColumns }, () => [])
|
||||||
|
|
@ -34,9 +32,10 @@
|
||||||
return columns
|
return columns
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const columnPhotos = $derived(
|
const columnPhotos = $derived(
|
||||||
(columns === 1 || columns === 2 || columns === 3) && !masonry ? splitIntoColumns(photos, columns) : []
|
(columns === 1 || columns === 2 || columns === 3) && !masonry
|
||||||
|
? splitIntoColumns(photos, columns)
|
||||||
|
: []
|
||||||
)
|
)
|
||||||
|
|
||||||
// Window width for responsive masonry
|
// Window width for responsive masonry
|
||||||
|
|
@ -55,7 +54,7 @@
|
||||||
const width = Math.floor((windowWidth - 64 - gapSize) / 2)
|
const width = Math.floor((windowWidth - 64 - gapSize) / 2)
|
||||||
return { minColWidth: width - 10, maxColWidth: width + 10, gap: gapSize }
|
return { minColWidth: width - 10, maxColWidth: width + 10, gap: gapSize }
|
||||||
} else if (columns === 3) {
|
} else if (columns === 3) {
|
||||||
const width = Math.floor((windowWidth - 64 - (gapSize * 2)) / 3)
|
const width = Math.floor((windowWidth - 64 - gapSize * 2) / 3)
|
||||||
return { minColWidth: width - 10, maxColWidth: width + 10, gap: gapSize }
|
return { minColWidth: width - 10, maxColWidth: width + 10, gap: gapSize }
|
||||||
} else {
|
} else {
|
||||||
// Auto columns
|
// Auto columns
|
||||||
|
|
@ -90,7 +89,7 @@
|
||||||
>
|
>
|
||||||
{#snippet children({ item })}
|
{#snippet children({ item })}
|
||||||
<div class="photo-grid__item">
|
<div class="photo-grid__item">
|
||||||
<PhotoItem item={item} />
|
<PhotoItem {item} />
|
||||||
{#if showCaptions}
|
{#if showCaptions}
|
||||||
<p class="photo-caption">{item.caption || ''}</p>
|
<p class="photo-caption">{item.caption || ''}</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@
|
||||||
let itemElements: HTMLElement[] = []
|
let itemElements: HTMLElement[] = []
|
||||||
let pillStyle = ''
|
let pillStyle = ''
|
||||||
let hoveredIndex = $state(-1)
|
let hoveredIndex = $state(-1)
|
||||||
let internalValue = $state(defaultValue ?? value ?? (items[0]?.value ?? ''))
|
let internalValue = $state(defaultValue ?? value ?? items[0]?.value ?? '')
|
||||||
|
|
||||||
// Derived state
|
// Derived state
|
||||||
const currentValue = $derived(value ?? internalValue)
|
const currentValue = $derived(value ?? internalValue)
|
||||||
|
|
|
||||||
|
|
@ -33,12 +33,7 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<BaseModal
|
<BaseModal bind:isOpen size="small" onClose={handleCancel} class="delete-confirmation-modal">
|
||||||
bind:isOpen
|
|
||||||
size="small"
|
|
||||||
onClose={handleCancel}
|
|
||||||
class="delete-confirmation-modal"
|
|
||||||
>
|
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<h2>{title}</h2>
|
<h2>{title}</h2>
|
||||||
<p>{message}</p>
|
<p>{message}</p>
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,10 @@
|
||||||
let y = $state(0)
|
let y = $state(0)
|
||||||
|
|
||||||
// Action to set submenu references
|
// Action to set submenu references
|
||||||
function submenuRef(node: HTMLElement, params: { item: DropdownItem; submenuElements: Map<string, HTMLElement> }) {
|
function submenuRef(
|
||||||
|
node: HTMLElement,
|
||||||
|
params: { item: DropdownItem; submenuElements: Map<string, HTMLElement> }
|
||||||
|
) {
|
||||||
if (params.item.children) {
|
if (params.item.children) {
|
||||||
params.submenuElements.set(params.item.id, node)
|
params.submenuElements.set(params.item.id, node)
|
||||||
}
|
}
|
||||||
|
|
@ -58,11 +61,7 @@
|
||||||
|
|
||||||
const { x: newX, y: newY } = await computePosition(triggerElement, dropdownElement, {
|
const { x: newX, y: newY } = await computePosition(triggerElement, dropdownElement, {
|
||||||
placement: isSubmenu ? 'right-start' : 'bottom-end',
|
placement: isSubmenu ? 'right-start' : 'bottom-end',
|
||||||
middleware: [
|
middleware: [offset(isSubmenu ? 0 : 4), flip(), shift({ padding: 8 })]
|
||||||
offset(isSubmenu ? 0 : 4),
|
|
||||||
flip(),
|
|
||||||
shift({ padding: 8 })
|
|
||||||
]
|
|
||||||
})
|
})
|
||||||
|
|
||||||
x = newX
|
x = newX
|
||||||
|
|
@ -84,9 +83,13 @@
|
||||||
|
|
||||||
const target = event.target as HTMLElement
|
const target = event.target as HTMLElement
|
||||||
// Check if click is inside any submenu
|
// Check if click is inside any submenu
|
||||||
const clickedInSubmenu = Array.from(submenuElements.values()).some(el => el.contains(target))
|
const clickedInSubmenu = Array.from(submenuElements.values()).some((el) => el.contains(target))
|
||||||
|
|
||||||
if (!dropdownElement.contains(target) && !triggerElement?.contains(target) && !clickedInSubmenu) {
|
if (
|
||||||
|
!dropdownElement.contains(target) &&
|
||||||
|
!triggerElement?.contains(target) &&
|
||||||
|
!clickedInSubmenu
|
||||||
|
) {
|
||||||
isOpen = false
|
isOpen = false
|
||||||
openSubmenuId = null // Reset submenu state
|
openSubmenuId = null // Reset submenu state
|
||||||
onClose?.()
|
onClose?.()
|
||||||
|
|
@ -197,7 +200,7 @@
|
||||||
isOpen={true}
|
isOpen={true}
|
||||||
triggerElement={submenuElements.get(item.id)}
|
triggerElement={submenuElements.get(item.id)}
|
||||||
items={item.children}
|
items={item.children}
|
||||||
onClose={onClose}
|
{onClose}
|
||||||
isSubmenu={true}
|
isSubmenu={true}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -27,13 +27,7 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<BaseModal
|
<BaseModal bind:isOpen {size} {closeOnBackdrop} {closeOnEscape} {onClose}>
|
||||||
bind:isOpen
|
|
||||||
{size}
|
|
||||||
{closeOnBackdrop}
|
|
||||||
{closeOnEscape}
|
|
||||||
{onClose}
|
|
||||||
>
|
|
||||||
{#if showCloseButton}
|
{#if showCloseButton}
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
|
|
||||||
|
|
@ -37,12 +37,7 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<BaseDropdown
|
<BaseDropdown bind:isOpen={isDropdownOpen} {disabled} {isLoading} class="publish-dropdown">
|
||||||
bind:isOpen={isDropdownOpen}
|
|
||||||
{disabled}
|
|
||||||
{isLoading}
|
|
||||||
class="publish-dropdown"
|
|
||||||
>
|
|
||||||
<Button
|
<Button
|
||||||
slot="trigger"
|
slot="trigger"
|
||||||
variant="primary"
|
variant="primary"
|
||||||
|
|
|
||||||
|
|
@ -49,12 +49,7 @@
|
||||||
const hasDropdownContent = $derived(availableActions.length > 0 || showViewInDropdown)
|
const hasDropdownContent = $derived(availableActions.length > 0 || showViewInDropdown)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<BaseDropdown
|
<BaseDropdown bind:isOpen={isDropdownOpen} {disabled} {isLoading} class="status-dropdown">
|
||||||
bind:isOpen={isDropdownOpen}
|
|
||||||
{disabled}
|
|
||||||
{isLoading}
|
|
||||||
class="status-dropdown"
|
|
||||||
>
|
|
||||||
{#snippet trigger()}
|
{#snippet trigger()}
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
|
|
@ -79,12 +74,7 @@
|
||||||
{#if availableActions.length > 0}
|
{#if availableActions.length > 0}
|
||||||
<div class="dropdown-divider"></div>
|
<div class="dropdown-divider"></div>
|
||||||
{/if}
|
{/if}
|
||||||
<a
|
<a href={viewUrl} target="_blank" rel="noopener noreferrer" class="dropdown-item view-link">
|
||||||
href={viewUrl}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
class="dropdown-item view-link"
|
|
||||||
>
|
|
||||||
View on site
|
View on site
|
||||||
</a>
|
</a>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -162,26 +162,34 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Simple effect to load content once when editor is ready
|
// Simple effect to load content once when editor is ready
|
||||||
let contentLoaded = false;
|
let contentLoaded = false
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (editor && data && !contentLoaded) {
|
if (editor && data && !contentLoaded) {
|
||||||
// Check if the data has actual content (not just empty doc)
|
// Check if the data has actual content (not just empty doc)
|
||||||
const hasContent = data.content && data.content.length > 0 &&
|
const hasContent =
|
||||||
!(data.content.length === 1 && data.content[0].type === 'paragraph' && !data.content[0].content);
|
data.content &&
|
||||||
|
data.content.length > 0 &&
|
||||||
|
!(
|
||||||
|
data.content.length === 1 &&
|
||||||
|
data.content[0].type === 'paragraph' &&
|
||||||
|
!data.content[0].content
|
||||||
|
)
|
||||||
|
|
||||||
if (hasContent) {
|
if (hasContent) {
|
||||||
// Set the content once
|
// Set the content once
|
||||||
editor.commands.setContent(data);
|
editor.commands.setContent(data)
|
||||||
contentLoaded = true;
|
contentLoaded = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
// Get extensions with custom options
|
// Get extensions with custom options
|
||||||
const extensions = getEditorExtensions({
|
const extensions = getEditorExtensions({
|
||||||
showSlashCommands,
|
showSlashCommands,
|
||||||
onShowUrlConvertDropdown: features.urlEmbed ? linkManagerRef?.handleShowUrlConvertDropdown : undefined,
|
onShowUrlConvertDropdown: features.urlEmbed
|
||||||
|
? linkManagerRef?.handleShowUrlConvertDropdown
|
||||||
|
: undefined,
|
||||||
onShowLinkContextMenu: linkManagerRef?.handleShowLinkContextMenu,
|
onShowLinkContextMenu: linkManagerRef?.handleShowLinkContextMenu,
|
||||||
imagePlaceholderComponent: EnhancedImagePlaceholder
|
imagePlaceholderComponent: EnhancedImagePlaceholder
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,10 @@ export function getCurrentTextStyle(editor: Editor): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get filtered commands based on variant and features
|
// Get filtered commands based on variant and features
|
||||||
export function getFilteredCommands(variant: ComposerVariant, features: ComposerFeatures): FilteredCommands {
|
export function getFilteredCommands(
|
||||||
|
variant: ComposerVariant,
|
||||||
|
features: ComposerFeatures
|
||||||
|
): FilteredCommands {
|
||||||
const filtered = { ...commands }
|
const filtered = { ...commands }
|
||||||
|
|
||||||
// Remove groups based on variant
|
// Remove groups based on variant
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,9 @@ export function useComposerEvents(options: UseComposerEventsOptions) {
|
||||||
|
|
||||||
// Set cursor position to drop location
|
// Set cursor position to drop location
|
||||||
const { state, dispatch } = view
|
const { state, dispatch } = view
|
||||||
const transaction = state.tr.setSelection(state.selection.constructor.near(state.doc.resolve(pos.pos)))
|
const transaction = state.tr.setSelection(
|
||||||
|
state.selection.constructor.near(state.doc.resolve(pos.pos))
|
||||||
|
)
|
||||||
dispatch(transaction)
|
dispatch(transaction)
|
||||||
|
|
||||||
// Upload the image
|
// Upload the image
|
||||||
|
|
|
||||||
|
|
@ -26,14 +26,17 @@
|
||||||
// State
|
// State
|
||||||
let isMenuOpen = $state(false)
|
let isMenuOpen = $state(false)
|
||||||
let currentNode = $state<{ node: Node; pos: number } | null>(null)
|
let currentNode = $state<{ node: Node; pos: number } | null>(null)
|
||||||
|
let menuNode = $state<{ node: Node; pos: number } | null>(null) // Node when menu was opened
|
||||||
let dragHandleContainer = $state<HTMLElement>()
|
let dragHandleContainer = $state<HTMLElement>()
|
||||||
|
|
||||||
// Generate menu items based on current node
|
// Generate menu items based on current node
|
||||||
const menuItems = $derived(() => {
|
const menuItems = $derived(() => {
|
||||||
if (!currentNode) return []
|
// Use menuNode when menu is open, otherwise currentNode
|
||||||
|
const activeNode = isMenuOpen && menuNode ? menuNode : currentNode
|
||||||
|
if (!activeNode) return []
|
||||||
|
|
||||||
const items: DropdownItem[] = []
|
const items: DropdownItem[] = []
|
||||||
const nodeType = currentNode.node.type.name
|
const nodeType = activeNode.node.type.name
|
||||||
|
|
||||||
// Block type conversion options
|
// Block type conversion options
|
||||||
if (nodeType === 'paragraph' || nodeType === 'heading') {
|
if (nodeType === 'paragraph' || nodeType === 'heading') {
|
||||||
|
|
@ -121,7 +124,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if block contains links that could have cards added
|
// Check if block contains links that could have cards added
|
||||||
if ((nodeType === 'paragraph' || nodeType === 'heading') && hasLinks(currentNode.node)) {
|
if ((nodeType === 'paragraph' || nodeType === 'heading') && hasLinks(activeNode.node)) {
|
||||||
items.push({
|
items.push({
|
||||||
id: 'add-link-cards',
|
id: 'add-link-cards',
|
||||||
label: 'Add cards for links',
|
label: 'Add cards for links',
|
||||||
|
|
@ -174,7 +177,10 @@
|
||||||
function hasLinks(node: Node): boolean {
|
function hasLinks(node: Node): boolean {
|
||||||
let hasLink = false
|
let hasLink = false
|
||||||
node.descendants((child) => {
|
node.descendants((child) => {
|
||||||
if (child.type.name === 'link' || (child.isText && child.marks.some(mark => mark.type.name === 'link'))) {
|
if (
|
||||||
|
child.type.name === 'link' ||
|
||||||
|
(child.isText && child.marks.some((mark) => mark.type.name === 'link'))
|
||||||
|
) {
|
||||||
hasLink = true
|
hasLink = true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
@ -184,16 +190,22 @@
|
||||||
// Block manipulation functions
|
// Block manipulation functions
|
||||||
function convertBlockType(type: string, attrs?: any) {
|
function convertBlockType(type: string, attrs?: any) {
|
||||||
console.log('convertBlockType called:', type, attrs)
|
console.log('convertBlockType called:', type, attrs)
|
||||||
if (!currentNode) {
|
// Use menuNode which was captured when menu was opened
|
||||||
console.log('No current node')
|
const nodeToConvert = menuNode || currentNode
|
||||||
|
if (!nodeToConvert) {
|
||||||
|
console.log('No node to convert')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const { pos } = currentNode
|
const { pos, node } = nodeToConvert
|
||||||
console.log('Current node:', currentNode.node.type.name, 'at pos:', pos)
|
console.log('Converting node:', node.type.name, 'at pos:', pos)
|
||||||
|
|
||||||
// Focus the editor first
|
// Calculate the actual position of the node
|
||||||
editor.commands.focus()
|
const nodeStart = pos
|
||||||
|
const nodeEnd = pos + node.nodeSize
|
||||||
|
|
||||||
|
// Set selection to the specific node we want to convert
|
||||||
|
editor.chain().focus().setTextSelection({ from: nodeStart, to: nodeEnd }).run()
|
||||||
|
|
||||||
// Convert the block type using chain commands
|
// Convert the block type using chain commands
|
||||||
if (type === 'paragraph') {
|
if (type === 'paragraph') {
|
||||||
|
|
@ -208,9 +220,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function convertToList(listType: string) {
|
function convertToList(listType: string) {
|
||||||
if (!currentNode) return
|
const nodeToConvert = menuNode || currentNode
|
||||||
|
if (!nodeToConvert) return
|
||||||
|
|
||||||
const { pos } = currentNode
|
const { pos } = nodeToConvert
|
||||||
const resolvedPos = editor.state.doc.resolve(pos)
|
const resolvedPos = editor.state.doc.resolve(pos)
|
||||||
|
|
||||||
// Get the position of the list item
|
// Get the position of the list item
|
||||||
|
|
@ -235,9 +248,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function convertEmbedToLink() {
|
function convertEmbedToLink() {
|
||||||
if (!currentNode) return
|
const nodeToConvert = menuNode || currentNode
|
||||||
|
if (!nodeToConvert) return
|
||||||
|
|
||||||
const { node, pos } = currentNode
|
const { node, pos } = nodeToConvert
|
||||||
const url = node.attrs.url
|
const url = node.attrs.url
|
||||||
const title = node.attrs.title || url
|
const title = node.attrs.title || url
|
||||||
|
|
||||||
|
|
@ -246,22 +260,27 @@
|
||||||
const nodeSize = node.nodeSize
|
const nodeSize = node.nodeSize
|
||||||
|
|
||||||
// Replace embed with a paragraph containing a link
|
// Replace embed with a paragraph containing a link
|
||||||
editor.chain()
|
editor
|
||||||
|
.chain()
|
||||||
.focus()
|
.focus()
|
||||||
.deleteRange({ from: nodePos, to: nodePos + nodeSize })
|
.deleteRange({ from: nodePos, to: nodePos + nodeSize })
|
||||||
.insertContentAt(nodePos, {
|
.insertContentAt(nodePos, {
|
||||||
type: 'paragraph',
|
type: 'paragraph',
|
||||||
content: [{
|
content: [
|
||||||
|
{
|
||||||
type: 'text',
|
type: 'text',
|
||||||
text: title,
|
text: title,
|
||||||
marks: [{
|
marks: [
|
||||||
|
{
|
||||||
type: 'link',
|
type: 'link',
|
||||||
attrs: {
|
attrs: {
|
||||||
href: url,
|
href: url,
|
||||||
target: '_blank'
|
target: '_blank'
|
||||||
}
|
}
|
||||||
}]
|
}
|
||||||
}]
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
})
|
})
|
||||||
.run()
|
.run()
|
||||||
|
|
||||||
|
|
@ -269,15 +288,16 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function addCardsForLinks() {
|
function addCardsForLinks() {
|
||||||
if (!currentNode) return
|
const nodeToUse = menuNode || currentNode
|
||||||
|
if (!nodeToUse) return
|
||||||
|
|
||||||
const { node, pos } = currentNode
|
const { node, pos } = nodeToUse
|
||||||
const links: { url: string; text: string }[] = []
|
const links: { url: string; text: string }[] = []
|
||||||
|
|
||||||
// Collect all links in the current block
|
// Collect all links in the current block
|
||||||
node.descendants((child) => {
|
node.descendants((child) => {
|
||||||
if (child.isText && child.marks.some(mark => mark.type.name === 'link')) {
|
if (child.isText && child.marks.some((mark) => mark.type.name === 'link')) {
|
||||||
const linkMark = child.marks.find(mark => mark.type.name === 'link')
|
const linkMark = child.marks.find((mark) => mark.type.name === 'link')
|
||||||
if (linkMark && linkMark.attrs.href) {
|
if (linkMark && linkMark.attrs.href) {
|
||||||
links.push({
|
links.push({
|
||||||
url: linkMark.attrs.href,
|
url: linkMark.attrs.href,
|
||||||
|
|
@ -290,7 +310,7 @@
|
||||||
// Insert embeds after the current block
|
// Insert embeds after the current block
|
||||||
if (links.length > 0) {
|
if (links.length > 0) {
|
||||||
const nodeEnd = pos + node.nodeSize
|
const nodeEnd = pos + node.nodeSize
|
||||||
const embeds = links.map(link => ({
|
const embeds = links.map((link) => ({
|
||||||
type: 'urlEmbed',
|
type: 'urlEmbed',
|
||||||
attrs: {
|
attrs: {
|
||||||
url: link.url,
|
url: link.url,
|
||||||
|
|
@ -298,24 +318,23 @@
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
|
||||||
editor.chain()
|
editor.chain().focus().insertContentAt(nodeEnd, embeds).run()
|
||||||
.focus()
|
|
||||||
.insertContentAt(nodeEnd, embeds)
|
|
||||||
.run()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isMenuOpen = false
|
isMenuOpen = false
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeFormatting() {
|
function removeFormatting() {
|
||||||
if (!currentNode) return
|
const nodeToUse = menuNode || currentNode
|
||||||
|
if (!nodeToUse) return
|
||||||
|
|
||||||
const { pos } = currentNode
|
const { pos } = nodeToUse
|
||||||
const resolvedPos = editor.state.doc.resolve(pos)
|
const resolvedPos = editor.state.doc.resolve(pos)
|
||||||
const nodeStart = resolvedPos.before(resolvedPos.depth)
|
const nodeStart = resolvedPos.before(resolvedPos.depth)
|
||||||
const nodeEnd = nodeStart + resolvedPos.node(resolvedPos.depth).nodeSize
|
const nodeEnd = nodeStart + resolvedPos.node(resolvedPos.depth).nodeSize
|
||||||
|
|
||||||
editor.chain()
|
editor
|
||||||
|
.chain()
|
||||||
.focus()
|
.focus()
|
||||||
.setTextSelection({ from: nodeStart, to: nodeEnd })
|
.setTextSelection({ from: nodeStart, to: nodeEnd })
|
||||||
.clearNodes()
|
.clearNodes()
|
||||||
|
|
@ -326,56 +345,47 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function duplicateBlock() {
|
function duplicateBlock() {
|
||||||
if (!currentNode) return
|
const nodeToUse = menuNode || currentNode
|
||||||
|
if (!nodeToUse) return
|
||||||
|
|
||||||
const { node, pos } = currentNode
|
const { node, pos } = nodeToUse
|
||||||
const resolvedPos = editor.state.doc.resolve(pos)
|
const resolvedPos = editor.state.doc.resolve(pos)
|
||||||
const nodeEnd = resolvedPos.after(resolvedPos.depth)
|
const nodeEnd = resolvedPos.after(resolvedPos.depth)
|
||||||
|
|
||||||
editor.chain()
|
editor.chain().focus().insertContentAt(nodeEnd, node.toJSON()).run()
|
||||||
.focus()
|
|
||||||
.insertContentAt(nodeEnd, node.toJSON())
|
|
||||||
.run()
|
|
||||||
|
|
||||||
isMenuOpen = false
|
isMenuOpen = false
|
||||||
}
|
}
|
||||||
|
|
||||||
function copyBlock() {
|
function copyBlock() {
|
||||||
if (!currentNode) return
|
const nodeToUse = menuNode || currentNode
|
||||||
|
if (!nodeToUse) return
|
||||||
|
|
||||||
const { pos } = currentNode
|
const { pos } = nodeToUse
|
||||||
const resolvedPos = editor.state.doc.resolve(pos)
|
const resolvedPos = editor.state.doc.resolve(pos)
|
||||||
const nodeStart = resolvedPos.before(resolvedPos.depth)
|
const nodeStart = resolvedPos.before(resolvedPos.depth)
|
||||||
const nodeEnd = nodeStart + resolvedPos.node(resolvedPos.depth).nodeSize
|
const nodeEnd = nodeStart + resolvedPos.node(resolvedPos.depth).nodeSize
|
||||||
|
|
||||||
editor.chain()
|
editor.chain().focus().setTextSelection({ from: nodeStart, to: nodeEnd }).run()
|
||||||
.focus()
|
|
||||||
.setTextSelection({ from: nodeStart, to: nodeEnd })
|
|
||||||
.run()
|
|
||||||
|
|
||||||
document.execCommand('copy')
|
document.execCommand('copy')
|
||||||
|
|
||||||
// Clear selection after copy
|
// Clear selection after copy
|
||||||
editor.chain()
|
editor.chain().focus().setTextSelection(nodeEnd).run()
|
||||||
.focus()
|
|
||||||
.setTextSelection(nodeEnd)
|
|
||||||
.run()
|
|
||||||
|
|
||||||
isMenuOpen = false
|
isMenuOpen = false
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteBlock() {
|
function deleteBlock() {
|
||||||
if (!currentNode) return
|
const nodeToUse = menuNode || currentNode
|
||||||
|
if (!nodeToUse) return
|
||||||
|
|
||||||
const { pos } = currentNode
|
const { pos } = nodeToUse
|
||||||
const resolvedPos = editor.state.doc.resolve(pos)
|
const resolvedPos = editor.state.doc.resolve(pos)
|
||||||
const nodeStart = resolvedPos.before(resolvedPos.depth)
|
const nodeStart = resolvedPos.before(resolvedPos.depth)
|
||||||
const nodeEnd = nodeStart + resolvedPos.node(resolvedPos.depth).nodeSize
|
const nodeEnd = nodeStart + resolvedPos.node(resolvedPos.depth).nodeSize
|
||||||
|
|
||||||
editor.chain()
|
editor.chain().focus().deleteRange({ from: nodeStart, to: nodeEnd }).run()
|
||||||
.focus()
|
|
||||||
.deleteRange({ from: nodeStart, to: nodeEnd })
|
|
||||||
.run()
|
|
||||||
|
|
||||||
isMenuOpen = false
|
isMenuOpen = false
|
||||||
}
|
}
|
||||||
|
|
@ -387,8 +397,13 @@
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
|
|
||||||
// Only toggle if we're clicking on the same node
|
// Capture the current node when opening the menu
|
||||||
// If clicking on a different node while menu is open, just update the menu
|
if (!isMenuOpen) {
|
||||||
|
menuNode = currentNode
|
||||||
|
} else {
|
||||||
|
menuNode = null
|
||||||
|
}
|
||||||
|
|
||||||
isMenuOpen = !isMenuOpen
|
isMenuOpen = !isMenuOpen
|
||||||
console.log('Menu open state:', isMenuOpen)
|
console.log('Menu open state:', isMenuOpen)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -145,7 +145,6 @@
|
||||||
font-family: 'JetBrains Mono', monospace, Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono';
|
font-family: 'JetBrains Mono', monospace, Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* List Styling */
|
/* List Styling */
|
||||||
.tiptap ul,
|
.tiptap ul,
|
||||||
.tiptap ol {
|
.tiptap ol {
|
||||||
|
|
|
||||||
|
|
@ -314,7 +314,9 @@ export function DragHandlePlugin(options: GlobalDragHandleOptions & { pluginKey:
|
||||||
const rect = absoluteRect(node)
|
const rect = absoluteRect(node)
|
||||||
|
|
||||||
// For custom nodes like embeds, position at the top of the element
|
// For custom nodes like embeds, position at the top of the element
|
||||||
const isCustomNode = node.matches('[data-drag-handle], .edra-url-embed-wrapper, .edra-youtube-embed-card, [data-type]')
|
const isCustomNode = node.matches(
|
||||||
|
'[data-drag-handle], .edra-url-embed-wrapper, .edra-youtube-embed-card, [data-type]'
|
||||||
|
)
|
||||||
if (isCustomNode) {
|
if (isCustomNode) {
|
||||||
// For NodeView components, position handle at top with small offset
|
// For NodeView components, position handle at top with small offset
|
||||||
rect.top += 8
|
rect.top += 8
|
||||||
|
|
@ -341,7 +343,7 @@ export function DragHandlePlugin(options: GlobalDragHandleOptions & { pluginKey:
|
||||||
// Add 12px gap between drag handle and content
|
// Add 12px gap between drag handle and content
|
||||||
// Position the handle inside the padding area, close to the text
|
// Position the handle inside the padding area, close to the text
|
||||||
dragHandleElement.style.left = `${rect.left + paddingLeft - rect.width - 12}px`
|
dragHandleElement.style.left = `${rect.left + paddingLeft - rect.width - 12}px`
|
||||||
dragHandleElement.style.top = `${rect.top - 2}px`
|
dragHandleElement.style.top = `${rect.top - 1}px`
|
||||||
showDragHandle()
|
showDragHandle()
|
||||||
},
|
},
|
||||||
keydown: () => {
|
keydown: () => {
|
||||||
|
|
|
||||||
|
|
@ -23,15 +23,7 @@
|
||||||
class={className}
|
class={className}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
<rect
|
<rect x="9" y="9" width="13" height="13" rx="2" stroke={color} stroke-width={strokeWidth} />
|
||||||
x="9"
|
|
||||||
y="9"
|
|
||||||
width="13"
|
|
||||||
height="13"
|
|
||||||
rx="2"
|
|
||||||
stroke={color}
|
|
||||||
stroke-width={strokeWidth}
|
|
||||||
/>
|
|
||||||
<path
|
<path
|
||||||
d="M5 15H4C2.89543 15 2 14.1046 2 13V4C2 2.89543 2.89543 2 4 2H13C14.1046 2 15 2.89543 15 4V5"
|
d="M5 15H4C2.89543 15 2 14.1046 2 13V4C2 2.89543 2.89543 2 4 2H13C14.1046 2 15 2.89543 15 4V5"
|
||||||
stroke={color}
|
stroke={color}
|
||||||
|
|
|
||||||
|
|
@ -128,17 +128,13 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="config-display">
|
<div class="config-display">
|
||||||
<code>{`<PhotoGrid photos={photos} columns={${columns}} gap="${gap}" masonry={${masonry}} showCaptions={${showCaptions}} />`}</code>
|
<code
|
||||||
|
>{`<PhotoGrid photos={photos} columns={${columns}} gap="${gap}" masonry={${masonry}} showCaptions={${showCaptions}} />`}</code
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid-preview">
|
<div class="grid-preview">
|
||||||
<PhotoGrid
|
<PhotoGrid photos={samplePhotos} {columns} {gap} {masonry} {showCaptions} />
|
||||||
photos={samplePhotos}
|
|
||||||
{columns}
|
|
||||||
{gap}
|
|
||||||
{masonry}
|
|
||||||
{showCaptions}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -187,7 +183,7 @@
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="checkbox"] {
|
input[type='checkbox'] {
|
||||||
width: 18px;
|
width: 18px;
|
||||||
height: 18px;
|
height: 18px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue