@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.
Files changed (92) hide show
  1. package/package.json +13 -8
  2. package/src/blocks/SplitHero/SplitHeroMedia.tsx +2 -1
  3. package/src/stores/index.ts +8 -0
  4. package/src/stores/mediaCache.ts +464 -0
  5. package/src/tools/AudioPlayer/@refactoring/00-PLAN.md +148 -0
  6. package/src/tools/AudioPlayer/@refactoring/01-TYPES.md +301 -0
  7. package/src/tools/AudioPlayer/@refactoring/02-HOOKS.md +281 -0
  8. package/src/tools/AudioPlayer/@refactoring/03-CONTEXT.md +328 -0
  9. package/src/tools/AudioPlayer/@refactoring/04-COMPONENTS.md +251 -0
  10. package/src/tools/AudioPlayer/@refactoring/05-EFFECTS.md +427 -0
  11. package/src/tools/AudioPlayer/@refactoring/06-UTILS-AND-INDEX.md +193 -0
  12. package/src/tools/AudioPlayer/@refactoring/07-EXECUTION-CHECKLIST.md +146 -0
  13. package/src/tools/AudioPlayer/README.md +325 -0
  14. package/src/tools/AudioPlayer/components/AudioEqualizer.tsx +200 -0
  15. package/src/tools/AudioPlayer/components/AudioPlayer.tsx +231 -0
  16. package/src/tools/AudioPlayer/components/AudioShortcutsPopover.tsx +99 -0
  17. package/src/tools/AudioPlayer/components/ReactiveCover/AudioReactiveCover.tsx +147 -0
  18. package/src/tools/AudioPlayer/components/ReactiveCover/effects/GlowEffect.tsx +110 -0
  19. package/src/tools/AudioPlayer/components/ReactiveCover/effects/MeshEffect.tsx +58 -0
  20. package/src/tools/AudioPlayer/components/ReactiveCover/effects/OrbsEffect.tsx +45 -0
  21. package/src/tools/AudioPlayer/components/ReactiveCover/effects/SpotlightEffect.tsx +82 -0
  22. package/src/tools/AudioPlayer/components/ReactiveCover/effects/index.ts +8 -0
  23. package/src/tools/AudioPlayer/components/ReactiveCover/index.ts +6 -0
  24. package/src/tools/AudioPlayer/components/SimpleAudioPlayer.tsx +280 -0
  25. package/src/tools/AudioPlayer/components/VisualizationToggle.tsx +64 -0
  26. package/src/tools/AudioPlayer/components/index.ts +21 -0
  27. package/src/tools/AudioPlayer/context/AudioProvider.tsx +292 -0
  28. package/src/tools/AudioPlayer/context/index.ts +11 -0
  29. package/src/tools/AudioPlayer/context/selectors.ts +96 -0
  30. package/src/tools/AudioPlayer/effects/index.ts +412 -0
  31. package/src/tools/AudioPlayer/hooks/index.ts +29 -0
  32. package/src/tools/AudioPlayer/hooks/useAudioAnalysis.ts +110 -0
  33. package/src/tools/AudioPlayer/hooks/useAudioHotkeys.ts +149 -0
  34. package/src/tools/AudioPlayer/hooks/useSharedWebAudio.ts +106 -0
  35. package/src/tools/AudioPlayer/hooks/useVisualization.tsx +201 -0
  36. package/src/tools/AudioPlayer/index.ts +139 -0
  37. package/src/tools/AudioPlayer/types/audio.ts +107 -0
  38. package/src/tools/AudioPlayer/types/components.ts +98 -0
  39. package/src/tools/AudioPlayer/types/effects.ts +73 -0
  40. package/src/tools/AudioPlayer/types/index.ts +35 -0
  41. package/src/tools/AudioPlayer/utils/formatTime.ts +10 -0
  42. package/src/tools/AudioPlayer/utils/index.ts +5 -0
  43. package/src/tools/ImageViewer/@refactoring/00-PLAN.md +71 -0
  44. package/src/tools/ImageViewer/@refactoring/01-TYPES.md +121 -0
  45. package/src/tools/ImageViewer/@refactoring/02-UTILS.md +143 -0
  46. package/src/tools/ImageViewer/@refactoring/03-HOOKS.md +261 -0
  47. package/src/tools/ImageViewer/@refactoring/04-COMPONENTS.md +427 -0
  48. package/src/tools/ImageViewer/@refactoring/05-EXECUTION-CHECKLIST.md +126 -0
  49. package/src/tools/ImageViewer/README.md +174 -0
  50. package/src/tools/ImageViewer/components/ImageInfo.tsx +44 -0
  51. package/src/tools/ImageViewer/components/ImageToolbar.tsx +150 -0
  52. package/src/tools/ImageViewer/components/ImageViewer.tsx +235 -0
  53. package/src/tools/ImageViewer/components/index.ts +7 -0
  54. package/src/tools/ImageViewer/hooks/index.ts +9 -0
  55. package/src/tools/ImageViewer/hooks/useImageLoading.ts +153 -0
  56. package/src/tools/ImageViewer/hooks/useImageTransform.ts +101 -0
  57. package/src/tools/ImageViewer/index.ts +60 -0
  58. package/src/tools/ImageViewer/types.ts +75 -0
  59. package/src/tools/ImageViewer/utils/constants.ts +59 -0
  60. package/src/tools/ImageViewer/utils/index.ts +16 -0
  61. package/src/tools/ImageViewer/utils/lqip.ts +47 -0
  62. package/src/tools/VideoPlayer/@refactoring/00-PLAN.md +91 -0
  63. package/src/tools/VideoPlayer/@refactoring/01-TYPES.md +284 -0
  64. package/src/tools/VideoPlayer/@refactoring/02-UTILS.md +141 -0
  65. package/src/tools/VideoPlayer/@refactoring/03-HOOKS.md +178 -0
  66. package/src/tools/VideoPlayer/@refactoring/04-COMPONENTS.md +95 -0
  67. package/src/tools/VideoPlayer/@refactoring/05-EXECUTION-CHECKLIST.md +139 -0
  68. package/src/tools/VideoPlayer/README.md +212 -187
  69. package/src/tools/VideoPlayer/{VideoControls.tsx → components/VideoControls.tsx} +8 -9
  70. package/src/tools/VideoPlayer/components/VideoErrorFallback.tsx +174 -0
  71. package/src/tools/VideoPlayer/components/VideoPlayer.tsx +201 -0
  72. package/src/tools/VideoPlayer/components/index.ts +14 -0
  73. package/src/tools/VideoPlayer/context/VideoPlayerContext.tsx +52 -0
  74. package/src/tools/VideoPlayer/context/index.ts +8 -0
  75. package/src/tools/VideoPlayer/hooks/index.ts +9 -0
  76. package/src/tools/VideoPlayer/hooks/useVideoPositionCache.ts +109 -0
  77. package/src/tools/VideoPlayer/index.ts +70 -9
  78. package/src/tools/VideoPlayer/providers/NativeProvider.tsx +206 -0
  79. package/src/tools/VideoPlayer/providers/StreamProvider.tsx +401 -0
  80. package/src/tools/VideoPlayer/providers/VidstackProvider.tsx +332 -0
  81. package/src/tools/VideoPlayer/providers/index.ts +8 -0
  82. package/src/tools/VideoPlayer/types/index.ts +38 -0
  83. package/src/tools/VideoPlayer/types/player.ts +116 -0
  84. package/src/tools/VideoPlayer/types/provider.ts +93 -0
  85. package/src/tools/VideoPlayer/types/sources.ts +97 -0
  86. package/src/tools/VideoPlayer/utils/fileSource.ts +78 -0
  87. package/src/tools/VideoPlayer/utils/index.ts +11 -0
  88. package/src/tools/VideoPlayer/utils/resolvers.ts +75 -0
  89. package/src/tools/index.ts +92 -4
  90. package/src/tools/VideoPlayer/NativePlayer.tsx +0 -141
  91. package/src/tools/VideoPlayer/VideoPlayer.tsx +0 -231
  92. 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
+ }