@djangocfg/ui-nextjs 2.1.65 → 2.1.67
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +13 -8
- package/src/blocks/SplitHero/SplitHeroMedia.tsx +2 -1
- package/src/stores/index.ts +8 -0
- package/src/stores/mediaCache.ts +464 -0
- package/src/tools/AudioPlayer/@refactoring/00-PLAN.md +148 -0
- package/src/tools/AudioPlayer/@refactoring/01-TYPES.md +301 -0
- package/src/tools/AudioPlayer/@refactoring/02-HOOKS.md +281 -0
- package/src/tools/AudioPlayer/@refactoring/03-CONTEXT.md +328 -0
- package/src/tools/AudioPlayer/@refactoring/04-COMPONENTS.md +251 -0
- package/src/tools/AudioPlayer/@refactoring/05-EFFECTS.md +427 -0
- package/src/tools/AudioPlayer/@refactoring/06-UTILS-AND-INDEX.md +193 -0
- package/src/tools/AudioPlayer/@refactoring/07-EXECUTION-CHECKLIST.md +146 -0
- package/src/tools/AudioPlayer/README.md +325 -0
- package/src/tools/AudioPlayer/components/AudioEqualizer.tsx +200 -0
- package/src/tools/AudioPlayer/components/AudioPlayer.tsx +231 -0
- package/src/tools/AudioPlayer/components/AudioShortcutsPopover.tsx +99 -0
- package/src/tools/AudioPlayer/components/ReactiveCover/AudioReactiveCover.tsx +147 -0
- package/src/tools/AudioPlayer/components/ReactiveCover/effects/GlowEffect.tsx +110 -0
- package/src/tools/AudioPlayer/components/ReactiveCover/effects/MeshEffect.tsx +58 -0
- package/src/tools/AudioPlayer/components/ReactiveCover/effects/OrbsEffect.tsx +45 -0
- package/src/tools/AudioPlayer/components/ReactiveCover/effects/SpotlightEffect.tsx +82 -0
- package/src/tools/AudioPlayer/components/ReactiveCover/effects/index.ts +8 -0
- package/src/tools/AudioPlayer/components/ReactiveCover/index.ts +6 -0
- package/src/tools/AudioPlayer/components/SimpleAudioPlayer.tsx +280 -0
- package/src/tools/AudioPlayer/components/VisualizationToggle.tsx +64 -0
- package/src/tools/AudioPlayer/components/index.ts +21 -0
- package/src/tools/AudioPlayer/context/AudioProvider.tsx +292 -0
- package/src/tools/AudioPlayer/context/index.ts +11 -0
- package/src/tools/AudioPlayer/context/selectors.ts +96 -0
- package/src/tools/AudioPlayer/effects/index.ts +412 -0
- package/src/tools/AudioPlayer/hooks/index.ts +29 -0
- package/src/tools/AudioPlayer/hooks/useAudioAnalysis.ts +110 -0
- package/src/tools/AudioPlayer/hooks/useAudioHotkeys.ts +149 -0
- package/src/tools/AudioPlayer/hooks/useSharedWebAudio.ts +106 -0
- package/src/tools/AudioPlayer/hooks/useVisualization.tsx +201 -0
- package/src/tools/AudioPlayer/index.ts +139 -0
- package/src/tools/AudioPlayer/types/audio.ts +107 -0
- package/src/tools/AudioPlayer/types/components.ts +98 -0
- package/src/tools/AudioPlayer/types/effects.ts +73 -0
- package/src/tools/AudioPlayer/types/index.ts +35 -0
- package/src/tools/AudioPlayer/utils/formatTime.ts +10 -0
- package/src/tools/AudioPlayer/utils/index.ts +5 -0
- package/src/tools/ImageViewer/@refactoring/00-PLAN.md +71 -0
- package/src/tools/ImageViewer/@refactoring/01-TYPES.md +121 -0
- package/src/tools/ImageViewer/@refactoring/02-UTILS.md +143 -0
- package/src/tools/ImageViewer/@refactoring/03-HOOKS.md +261 -0
- package/src/tools/ImageViewer/@refactoring/04-COMPONENTS.md +427 -0
- package/src/tools/ImageViewer/@refactoring/05-EXECUTION-CHECKLIST.md +126 -0
- package/src/tools/ImageViewer/README.md +174 -0
- package/src/tools/ImageViewer/components/ImageInfo.tsx +44 -0
- package/src/tools/ImageViewer/components/ImageToolbar.tsx +150 -0
- package/src/tools/ImageViewer/components/ImageViewer.tsx +235 -0
- package/src/tools/ImageViewer/components/index.ts +7 -0
- package/src/tools/ImageViewer/hooks/index.ts +9 -0
- package/src/tools/ImageViewer/hooks/useImageLoading.ts +153 -0
- package/src/tools/ImageViewer/hooks/useImageTransform.ts +101 -0
- package/src/tools/ImageViewer/index.ts +60 -0
- package/src/tools/ImageViewer/types.ts +75 -0
- package/src/tools/ImageViewer/utils/constants.ts +59 -0
- package/src/tools/ImageViewer/utils/index.ts +16 -0
- package/src/tools/ImageViewer/utils/lqip.ts +47 -0
- package/src/tools/VideoPlayer/@refactoring/00-PLAN.md +91 -0
- package/src/tools/VideoPlayer/@refactoring/01-TYPES.md +284 -0
- package/src/tools/VideoPlayer/@refactoring/02-UTILS.md +141 -0
- package/src/tools/VideoPlayer/@refactoring/03-HOOKS.md +178 -0
- package/src/tools/VideoPlayer/@refactoring/04-COMPONENTS.md +95 -0
- package/src/tools/VideoPlayer/@refactoring/05-EXECUTION-CHECKLIST.md +139 -0
- package/src/tools/VideoPlayer/README.md +212 -187
- package/src/tools/VideoPlayer/{VideoControls.tsx → components/VideoControls.tsx} +8 -9
- package/src/tools/VideoPlayer/components/VideoErrorFallback.tsx +174 -0
- package/src/tools/VideoPlayer/components/VideoPlayer.tsx +201 -0
- package/src/tools/VideoPlayer/components/index.ts +14 -0
- package/src/tools/VideoPlayer/context/VideoPlayerContext.tsx +52 -0
- package/src/tools/VideoPlayer/context/index.ts +8 -0
- package/src/tools/VideoPlayer/hooks/index.ts +9 -0
- package/src/tools/VideoPlayer/hooks/useVideoPositionCache.ts +109 -0
- package/src/tools/VideoPlayer/index.ts +70 -9
- package/src/tools/VideoPlayer/providers/NativeProvider.tsx +206 -0
- package/src/tools/VideoPlayer/providers/StreamProvider.tsx +401 -0
- package/src/tools/VideoPlayer/providers/VidstackProvider.tsx +332 -0
- package/src/tools/VideoPlayer/providers/index.ts +8 -0
- package/src/tools/VideoPlayer/types/index.ts +38 -0
- package/src/tools/VideoPlayer/types/player.ts +116 -0
- package/src/tools/VideoPlayer/types/provider.ts +93 -0
- package/src/tools/VideoPlayer/types/sources.ts +97 -0
- package/src/tools/VideoPlayer/utils/fileSource.ts +78 -0
- package/src/tools/VideoPlayer/utils/index.ts +11 -0
- package/src/tools/VideoPlayer/utils/resolvers.ts +75 -0
- package/src/tools/index.ts +92 -4
- package/src/tools/VideoPlayer/NativePlayer.tsx +0 -141
- package/src/tools/VideoPlayer/VideoPlayer.tsx +0 -231
- package/src/tools/VideoPlayer/types.ts +0 -118
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# Execution Checklist
|
|
2
|
+
|
|
3
|
+
## Pre-flight
|
|
4
|
+
|
|
5
|
+
- [ ] Read current ImageViewer.tsx
|
|
6
|
+
- [ ] Backup or rename original file
|
|
7
|
+
- [ ] Create folder structure
|
|
8
|
+
|
|
9
|
+
## Phase 1: Folder Structure
|
|
10
|
+
|
|
11
|
+
- [ ] Create `components/` directory
|
|
12
|
+
- [ ] Create `hooks/` directory
|
|
13
|
+
- [ ] Create `utils/` directory
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
mkdir -p components hooks utils
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Phase 2: Types
|
|
20
|
+
|
|
21
|
+
- [ ] Create `types.ts` with all interfaces
|
|
22
|
+
- ImageFile
|
|
23
|
+
- ImageViewerProps
|
|
24
|
+
- ImageTransform
|
|
25
|
+
- ZoomPreset
|
|
26
|
+
- ImageToolbarProps
|
|
27
|
+
- ImageInfoProps
|
|
28
|
+
- [ ] Run `pnpm check`
|
|
29
|
+
|
|
30
|
+
## Phase 3: Utils
|
|
31
|
+
|
|
32
|
+
- [ ] Create `utils/constants.ts`
|
|
33
|
+
- MAX_IMAGE_SIZE, WARN_IMAGE_SIZE
|
|
34
|
+
- MIN_ZOOM, MAX_ZOOM
|
|
35
|
+
- ZOOM_PRESETS
|
|
36
|
+
- DEFAULT_TRANSFORM
|
|
37
|
+
- [ ] Create `utils/lqip.ts`
|
|
38
|
+
- createLQIP function
|
|
39
|
+
- [ ] Create `utils/index.ts`
|
|
40
|
+
- Re-export all
|
|
41
|
+
- [ ] Run `pnpm check`
|
|
42
|
+
|
|
43
|
+
## Phase 4: Hooks
|
|
44
|
+
|
|
45
|
+
- [ ] Create `hooks/useImageTransform.ts`
|
|
46
|
+
- Transform state management
|
|
47
|
+
- rotate, flipH, flipV callbacks
|
|
48
|
+
- transformStyle computed value
|
|
49
|
+
- [ ] Create `hooks/useImageLoading.ts`
|
|
50
|
+
- Blob URL management
|
|
51
|
+
- LQIP generation
|
|
52
|
+
- Loading/error states
|
|
53
|
+
- [ ] Create `hooks/index.ts`
|
|
54
|
+
- Re-export all
|
|
55
|
+
- [ ] Run `pnpm check`
|
|
56
|
+
|
|
57
|
+
## Phase 5: Components
|
|
58
|
+
|
|
59
|
+
- [ ] Create `components/ImageToolbar.tsx`
|
|
60
|
+
- Zoom controls
|
|
61
|
+
- Rotate/flip buttons
|
|
62
|
+
- Preset dropdown
|
|
63
|
+
- [ ] Create `components/ImageInfo.tsx`
|
|
64
|
+
- Dimensions display
|
|
65
|
+
- [ ] Create `components/ImageViewer.tsx`
|
|
66
|
+
- Main component (simplified)
|
|
67
|
+
- Uses hooks
|
|
68
|
+
- Includes fullscreen dialog
|
|
69
|
+
- [ ] Create `components/index.ts`
|
|
70
|
+
- Re-export all
|
|
71
|
+
- [ ] Run `pnpm check`
|
|
72
|
+
|
|
73
|
+
## Phase 6: Main Index
|
|
74
|
+
|
|
75
|
+
- [ ] Update `index.ts` with new imports
|
|
76
|
+
- Export ImageViewer from components
|
|
77
|
+
- Export types
|
|
78
|
+
- Export hooks (if public)
|
|
79
|
+
- [ ] Run `pnpm check`
|
|
80
|
+
|
|
81
|
+
## Phase 7: Cleanup
|
|
82
|
+
|
|
83
|
+
- [ ] Delete old `ImageViewer.tsx`
|
|
84
|
+
- [ ] Verify all exports work
|
|
85
|
+
- [ ] Run `pnpm check`
|
|
86
|
+
- [ ] Run `pnpm build` (optional)
|
|
87
|
+
|
|
88
|
+
## Post-flight
|
|
89
|
+
|
|
90
|
+
- [ ] Test in browser
|
|
91
|
+
- [ ] Verify zoom/pan/rotate work
|
|
92
|
+
- [ ] Verify fullscreen works
|
|
93
|
+
- [ ] Verify LQIP loads for large images
|
|
94
|
+
- [ ] Verify error states display
|
|
95
|
+
|
|
96
|
+
## Final Structure
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
ImageViewer/
|
|
100
|
+
├── index.ts
|
|
101
|
+
├── types.ts
|
|
102
|
+
├── README.md
|
|
103
|
+
├── components/
|
|
104
|
+
│ ├── index.ts
|
|
105
|
+
│ ├── ImageViewer.tsx
|
|
106
|
+
│ ├── ImageToolbar.tsx
|
|
107
|
+
│ └── ImageInfo.tsx
|
|
108
|
+
├── hooks/
|
|
109
|
+
│ ├── index.ts
|
|
110
|
+
│ ├── useImageTransform.ts
|
|
111
|
+
│ └── useImageLoading.ts
|
|
112
|
+
└── utils/
|
|
113
|
+
├── index.ts
|
|
114
|
+
├── constants.ts
|
|
115
|
+
└── lqip.ts
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Metrics
|
|
119
|
+
|
|
120
|
+
| Metric | Before | After |
|
|
121
|
+
|--------|--------|-------|
|
|
122
|
+
| Main file lines | 589 | ~150 |
|
|
123
|
+
| Total files | 3 | 12 |
|
|
124
|
+
| Sub-components | 2 inline | 2 separate |
|
|
125
|
+
| Custom hooks | 0 | 2 |
|
|
126
|
+
| Utility files | 0 | 2 |
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# ImageViewer
|
|
2
|
+
|
|
3
|
+
Image viewer with zoom, pan, rotate, and flip functionality.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Mouse wheel zoom
|
|
8
|
+
- Drag to pan
|
|
9
|
+
- Zoom presets (25%, 50%, 100%, 200%, 400%)
|
|
10
|
+
- Rotate 90°
|
|
11
|
+
- Flip horizontal/vertical
|
|
12
|
+
- Fullscreen dialog mode
|
|
13
|
+
- Keyboard shortcuts
|
|
14
|
+
- Checkerboard background for transparency
|
|
15
|
+
- Image dimensions display
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
import { ImageViewer } from '@djangocfg/ui-nextjs';
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Basic Usage
|
|
24
|
+
|
|
25
|
+
```tsx
|
|
26
|
+
<ImageViewer
|
|
27
|
+
file={{
|
|
28
|
+
name: 'photo.jpg',
|
|
29
|
+
path: '/images/photo.jpg',
|
|
30
|
+
mimeType: 'image/jpeg',
|
|
31
|
+
}}
|
|
32
|
+
content={imageArrayBuffer}
|
|
33
|
+
/>
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Props
|
|
37
|
+
|
|
38
|
+
| Prop | Type | Default | Description |
|
|
39
|
+
|------|------|---------|-------------|
|
|
40
|
+
| `file` | `ImageFile` | required | File info (name, path, mimeType) |
|
|
41
|
+
| `content` | `string \| ArrayBuffer` | required | Image data |
|
|
42
|
+
| `inDialog` | `boolean` | `false` | Hide expand button (for nested usage) |
|
|
43
|
+
|
|
44
|
+
## ImageFile Type
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
interface ImageFile {
|
|
48
|
+
name: string; // Display name
|
|
49
|
+
path: string; // File path (for state tracking)
|
|
50
|
+
mimeType?: string; // MIME type (e.g., 'image/png')
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Content Formats
|
|
55
|
+
|
|
56
|
+
The `content` prop accepts:
|
|
57
|
+
|
|
58
|
+
- **ArrayBuffer**: Binary image data (creates blob URL)
|
|
59
|
+
- **Data URL**: Base64 encoded string starting with `data:`
|
|
60
|
+
- **Base64 string**: Raw base64 (auto-prefixed with data URL)
|
|
61
|
+
|
|
62
|
+
```tsx
|
|
63
|
+
// ArrayBuffer (from fetch or file read)
|
|
64
|
+
const response = await fetch('/image.png');
|
|
65
|
+
const buffer = await response.arrayBuffer();
|
|
66
|
+
<ImageViewer file={file} content={buffer} />
|
|
67
|
+
|
|
68
|
+
// Data URL
|
|
69
|
+
<ImageViewer file={file} content="data:image/png;base64,iVBORw0KGgo..." />
|
|
70
|
+
|
|
71
|
+
// Base64 string (auto-converted to data URL)
|
|
72
|
+
<ImageViewer file={file} content="iVBORw0KGgo..." />
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Keyboard Shortcuts
|
|
76
|
+
|
|
77
|
+
| Key | Action |
|
|
78
|
+
|-----|--------|
|
|
79
|
+
| `+` / `=` | Zoom in |
|
|
80
|
+
| `-` | Zoom out |
|
|
81
|
+
| `0` | Reset to fit |
|
|
82
|
+
| `R` | Rotate 90° |
|
|
83
|
+
|
|
84
|
+
## Toolbar Controls
|
|
85
|
+
|
|
86
|
+
The floating toolbar at the bottom provides:
|
|
87
|
+
|
|
88
|
+
- **Zoom out** button
|
|
89
|
+
- **Zoom level** dropdown with presets
|
|
90
|
+
- **Zoom in** button
|
|
91
|
+
- **Fit to view** button
|
|
92
|
+
- **Flip horizontal** toggle
|
|
93
|
+
- **Flip vertical** toggle
|
|
94
|
+
- **Rotate 90°** button
|
|
95
|
+
- **Expand** fullscreen button
|
|
96
|
+
|
|
97
|
+
## Fullscreen Mode
|
|
98
|
+
|
|
99
|
+
Click the expand button to open the image in a fullscreen dialog. The dialog includes the same toolbar and supports all interactions.
|
|
100
|
+
|
|
101
|
+
```tsx
|
|
102
|
+
// Fullscreen is automatically available unless inDialog is true
|
|
103
|
+
<ImageViewer file={file} content={content} />
|
|
104
|
+
|
|
105
|
+
// When embedding in your own dialog, disable the expand button
|
|
106
|
+
<Dialog>
|
|
107
|
+
<ImageViewer file={file} content={content} inDialog />
|
|
108
|
+
</Dialog>
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Styling
|
|
112
|
+
|
|
113
|
+
The component fills its container and displays a checkerboard pattern behind transparent images.
|
|
114
|
+
|
|
115
|
+
```tsx
|
|
116
|
+
<div className="w-full h-[500px]">
|
|
117
|
+
<ImageViewer file={file} content={content} />
|
|
118
|
+
</div>
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Error State
|
|
122
|
+
|
|
123
|
+
When content is empty or invalid, displays an error placeholder:
|
|
124
|
+
|
|
125
|
+
```tsx
|
|
126
|
+
// Shows "Failed to load image" with icon
|
|
127
|
+
<ImageViewer file={file} content="" />
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Example: File Browser Integration
|
|
131
|
+
|
|
132
|
+
```tsx
|
|
133
|
+
function FilePreview({ file, content }: { file: OpenFile; content: ArrayBuffer }) {
|
|
134
|
+
const imageFile: ImageFile = {
|
|
135
|
+
name: file.name,
|
|
136
|
+
path: file.path,
|
|
137
|
+
mimeType: file.mimeType,
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
<div className="h-full">
|
|
142
|
+
<ImageViewer file={imageFile} content={content} />
|
|
143
|
+
</div>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Architecture
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
ImageViewer/
|
|
152
|
+
├── index.ts # Public API exports
|
|
153
|
+
├── types.ts # Type definitions
|
|
154
|
+
├── components/
|
|
155
|
+
│ ├── index.ts # Component exports
|
|
156
|
+
│ ├── ImageViewer.tsx # Main viewer component
|
|
157
|
+
│ ├── ImageToolbar.tsx # Zoom/rotate/flip controls
|
|
158
|
+
│ └── ImageInfo.tsx # Dimensions display
|
|
159
|
+
├── hooks/
|
|
160
|
+
│ ├── index.ts
|
|
161
|
+
│ ├── useImageLoading.ts # Blob URL & LQIP management
|
|
162
|
+
│ └── useImageTransform.ts # Rotation/flip state
|
|
163
|
+
├── utils/
|
|
164
|
+
│ ├── index.ts
|
|
165
|
+
│ ├── constants.ts # Size limits, zoom presets
|
|
166
|
+
│ └── lqip.ts # Low-Quality Image Placeholder
|
|
167
|
+
└── README.md
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Dependencies
|
|
171
|
+
|
|
172
|
+
- `react-zoom-pan-pinch` - Zoom and pan functionality
|
|
173
|
+
- `lucide-react` - Icons
|
|
174
|
+
- `@djangocfg/ui-core` - UI components (Button, Dialog, etc.)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ImageInfo - Displays image dimensions badge
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { useEffect, useState } from 'react';
|
|
8
|
+
import { useImageCache } from '../../../stores/mediaCache';
|
|
9
|
+
import type { ImageInfoProps } from '../types';
|
|
10
|
+
|
|
11
|
+
// =============================================================================
|
|
12
|
+
// COMPONENT
|
|
13
|
+
// =============================================================================
|
|
14
|
+
|
|
15
|
+
export function ImageInfo({ src }: ImageInfoProps) {
|
|
16
|
+
const { getDimensions, cacheDimensions } = useImageCache();
|
|
17
|
+
const [dimensions, setDimensions] = useState<{ w: number; h: number } | null>(null);
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
// Check cache first
|
|
21
|
+
const cached = getDimensions(src);
|
|
22
|
+
if (cached) {
|
|
23
|
+
setDimensions({ w: cached.width, h: cached.height });
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Load and cache dimensions
|
|
28
|
+
const img = new Image();
|
|
29
|
+
img.onload = () => {
|
|
30
|
+
const dims = { w: img.naturalWidth, h: img.naturalHeight };
|
|
31
|
+
setDimensions(dims);
|
|
32
|
+
cacheDimensions(src, { width: dims.w, height: dims.h });
|
|
33
|
+
};
|
|
34
|
+
img.src = src;
|
|
35
|
+
}, [src, getDimensions, cacheDimensions]);
|
|
36
|
+
|
|
37
|
+
if (!dimensions) return null;
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<div className="absolute top-3 right-3 z-10 px-2 py-1 bg-background/80 backdrop-blur-sm border rounded text-[10px] text-muted-foreground font-mono">
|
|
41
|
+
{dimensions.w} × {dimensions.h}
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ImageToolbar - Floating toolbar for image controls
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { useMemo, useCallback } from 'react';
|
|
8
|
+
import { ZoomIn, ZoomOut, RotateCw, FlipHorizontal, FlipVertical, Maximize2, Expand } from 'lucide-react';
|
|
9
|
+
import { useControls } from 'react-zoom-pan-pinch';
|
|
10
|
+
import { Button, cn } from '@djangocfg/ui-core';
|
|
11
|
+
import {
|
|
12
|
+
DropdownMenu,
|
|
13
|
+
DropdownMenuContent,
|
|
14
|
+
DropdownMenuItem,
|
|
15
|
+
DropdownMenuTrigger,
|
|
16
|
+
} from '../../../components/dropdown-menu';
|
|
17
|
+
import { ZOOM_PRESETS } from '../utils';
|
|
18
|
+
import type { ImageToolbarProps } from '../types';
|
|
19
|
+
|
|
20
|
+
// =============================================================================
|
|
21
|
+
// COMPONENT
|
|
22
|
+
// =============================================================================
|
|
23
|
+
|
|
24
|
+
export function ImageToolbar({
|
|
25
|
+
scale,
|
|
26
|
+
transform,
|
|
27
|
+
onRotate,
|
|
28
|
+
onFlipH,
|
|
29
|
+
onFlipV,
|
|
30
|
+
onZoomPreset,
|
|
31
|
+
onExpand,
|
|
32
|
+
}: ImageToolbarProps) {
|
|
33
|
+
const { zoomIn, zoomOut, resetTransform } = useControls();
|
|
34
|
+
|
|
35
|
+
const zoomLabel = useMemo(() => {
|
|
36
|
+
const percent = Math.round(scale * 100);
|
|
37
|
+
return `${percent}%`;
|
|
38
|
+
}, [scale]);
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 z-10 flex items-center gap-0.5 bg-background/90 backdrop-blur-sm border rounded-lg p-1 shadow-lg">
|
|
42
|
+
{/* Zoom controls */}
|
|
43
|
+
<Button
|
|
44
|
+
variant="ghost"
|
|
45
|
+
size="icon"
|
|
46
|
+
className="h-7 w-7"
|
|
47
|
+
onClick={() => zoomOut()}
|
|
48
|
+
title="Zoom out"
|
|
49
|
+
>
|
|
50
|
+
<ZoomOut className="h-3.5 w-3.5" />
|
|
51
|
+
</Button>
|
|
52
|
+
|
|
53
|
+
<DropdownMenu>
|
|
54
|
+
<DropdownMenuTrigger asChild>
|
|
55
|
+
<Button variant="ghost" size="sm" className="h-7 px-2 min-w-[52px] font-mono text-xs">
|
|
56
|
+
{zoomLabel}
|
|
57
|
+
</Button>
|
|
58
|
+
</DropdownMenuTrigger>
|
|
59
|
+
<DropdownMenuContent align="center" className="min-w-[80px]">
|
|
60
|
+
{ZOOM_PRESETS.map((preset) => (
|
|
61
|
+
<DropdownMenuItem
|
|
62
|
+
key={preset.label}
|
|
63
|
+
onClick={() => onZoomPreset(preset.value)}
|
|
64
|
+
className="text-xs justify-center"
|
|
65
|
+
>
|
|
66
|
+
{preset.label}
|
|
67
|
+
</DropdownMenuItem>
|
|
68
|
+
))}
|
|
69
|
+
</DropdownMenuContent>
|
|
70
|
+
</DropdownMenu>
|
|
71
|
+
|
|
72
|
+
<Button
|
|
73
|
+
variant="ghost"
|
|
74
|
+
size="icon"
|
|
75
|
+
className="h-7 w-7"
|
|
76
|
+
onClick={() => zoomIn()}
|
|
77
|
+
title="Zoom in"
|
|
78
|
+
>
|
|
79
|
+
<ZoomIn className="h-3.5 w-3.5" />
|
|
80
|
+
</Button>
|
|
81
|
+
|
|
82
|
+
<div className="w-px h-4 bg-border mx-1" />
|
|
83
|
+
|
|
84
|
+
{/* Fit to view */}
|
|
85
|
+
<Button
|
|
86
|
+
variant="ghost"
|
|
87
|
+
size="icon"
|
|
88
|
+
className="h-7 w-7"
|
|
89
|
+
onClick={() => resetTransform()}
|
|
90
|
+
title="Fit to view"
|
|
91
|
+
>
|
|
92
|
+
<Maximize2 className="h-3.5 w-3.5" />
|
|
93
|
+
</Button>
|
|
94
|
+
|
|
95
|
+
<div className="w-px h-4 bg-border mx-1" />
|
|
96
|
+
|
|
97
|
+
{/* Transform controls */}
|
|
98
|
+
<Button
|
|
99
|
+
variant="ghost"
|
|
100
|
+
size="icon"
|
|
101
|
+
className={cn('h-7 w-7', transform.flipH && 'bg-accent')}
|
|
102
|
+
onClick={onFlipH}
|
|
103
|
+
title="Flip horizontal"
|
|
104
|
+
>
|
|
105
|
+
<FlipHorizontal className="h-3.5 w-3.5" />
|
|
106
|
+
</Button>
|
|
107
|
+
|
|
108
|
+
<Button
|
|
109
|
+
variant="ghost"
|
|
110
|
+
size="icon"
|
|
111
|
+
className={cn('h-7 w-7', transform.flipV && 'bg-accent')}
|
|
112
|
+
onClick={onFlipV}
|
|
113
|
+
title="Flip vertical"
|
|
114
|
+
>
|
|
115
|
+
<FlipVertical className="h-3.5 w-3.5" />
|
|
116
|
+
</Button>
|
|
117
|
+
|
|
118
|
+
<Button
|
|
119
|
+
variant="ghost"
|
|
120
|
+
size="icon"
|
|
121
|
+
className="h-7 w-7"
|
|
122
|
+
onClick={onRotate}
|
|
123
|
+
title="Rotate 90°"
|
|
124
|
+
>
|
|
125
|
+
<RotateCw className="h-3.5 w-3.5" />
|
|
126
|
+
</Button>
|
|
127
|
+
|
|
128
|
+
{transform.rotation !== 0 && (
|
|
129
|
+
<span className="text-[10px] text-muted-foreground font-mono pl-1">
|
|
130
|
+
{transform.rotation}°
|
|
131
|
+
</span>
|
|
132
|
+
)}
|
|
133
|
+
|
|
134
|
+
{onExpand && (
|
|
135
|
+
<>
|
|
136
|
+
<div className="w-px h-4 bg-border mx-1" />
|
|
137
|
+
<Button
|
|
138
|
+
variant="ghost"
|
|
139
|
+
size="icon"
|
|
140
|
+
className="h-7 w-7"
|
|
141
|
+
onClick={onExpand}
|
|
142
|
+
title="Open in fullscreen"
|
|
143
|
+
>
|
|
144
|
+
<Expand className="h-3.5 w-3.5" />
|
|
145
|
+
</Button>
|
|
146
|
+
</>
|
|
147
|
+
)}
|
|
148
|
+
</div>
|
|
149
|
+
);
|
|
150
|
+
}
|