@gentleduck/registry-ui 0.2.6 → 0.2.8
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/.turbo/turbo-test.log +7 -7
- package/CHANGELOG.md +8 -0
- package/package.json +7 -7
- package/src/alert-dialog/alert-dialog.tsx +11 -4
- package/src/aspect-ratio/aspect-ratio.tsx +9 -11
- package/src/audio/audio-visualizer.tsx +2 -0
- package/src/calendar/calendar.tsx +161 -141
- package/src/chart/chart.tsx +11 -6
- package/src/combobox/combobox.tsx +96 -69
- package/src/command/command.tsx +34 -37
- package/src/context-menu/context-menu.tsx +11 -3
- package/src/dialog/dialog-responsive.tsx +9 -0
- package/src/dialog/dialog.tsx +12 -4
- package/src/dropdown-menu/dropdown-menu.tsx +11 -3
- package/src/empty/empty.tsx +30 -17
- package/src/field/field.tsx +136 -109
- package/src/json-editor/json-editor.tsx +1 -0
- package/src/json-editor/json-editor.view.tsx +1 -0
- package/src/menubar/menubar.tsx +10 -3
- package/src/popover/popover.tsx +4 -0
- package/src/preview-panel/preview-panel-dialog.tsx +86 -80
- package/src/preview-panel/preview-panel.tsx +280 -273
- package/src/resizable/resizable.tsx +17 -15
- package/src/select/select.tsx +3 -0
- package/src/sheet/sheet.tsx +16 -4
- package/src/sidebar/sidebar.tsx +434 -376
- package/src/slider/slider.tsx +7 -10
- package/src/sonner/sonner.chunks.tsx +2 -0
- package/src/sonner/sonner.tsx +23 -20
- package/src/toggle/toggle.constants.ts +2 -2
- package/src/tooltip/tooltip.tsx +3 -0
- package/tsconfig.json +0 -1
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { cn } from '@gentleduck/libs/cn'
|
|
4
4
|
import { type Direction, useDirection } from '@gentleduck/primitives/direction'
|
|
5
5
|
import { Minus, Plus, RotateCcw } from 'lucide-react'
|
|
6
|
-
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
6
|
+
import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
7
7
|
import { Badge } from '../badge'
|
|
8
8
|
import { Button } from '../button'
|
|
9
9
|
import { ButtonGroup } from '../button-group'
|
|
@@ -89,300 +89,307 @@ const ZoomControls = memo(function ZoomControls({
|
|
|
89
89
|
// All transforms bypass React via direct DOM writes for zero re-renders
|
|
90
90
|
// during continuous interactions (drag, wheel, pinch).
|
|
91
91
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
// Stable ref for the callback so effects never re-subscribe
|
|
132
|
-
const onStateChangeRef = useRef(onStateChange)
|
|
133
|
-
onStateChangeRef.current = onStateChange
|
|
134
|
-
|
|
135
|
-
// Flush a pending state emission. Called from RAF or directly by button handlers.
|
|
136
|
-
const flushEmit = useCallback(() => {
|
|
137
|
-
if (!s.current.emitPending) return
|
|
138
|
-
s.current.emitPending = false
|
|
139
|
-
onStateChangeRef.current?.({
|
|
140
|
-
zoom: s.current.zoom,
|
|
141
|
-
x: s.current.x,
|
|
142
|
-
y: s.current.y,
|
|
92
|
+
const PreviewPanel = React.forwardRef<HTMLDivElement, PreviewPanelProps>(
|
|
93
|
+
(
|
|
94
|
+
{
|
|
95
|
+
maxHeight,
|
|
96
|
+
minZoom = 0.25,
|
|
97
|
+
maxZoom = 4,
|
|
98
|
+
initialZoom = 1,
|
|
99
|
+
showControls = true,
|
|
100
|
+
html,
|
|
101
|
+
children,
|
|
102
|
+
className,
|
|
103
|
+
style,
|
|
104
|
+
onStateChange,
|
|
105
|
+
syncState,
|
|
106
|
+
dir,
|
|
107
|
+
...rest
|
|
108
|
+
},
|
|
109
|
+
ref,
|
|
110
|
+
) => {
|
|
111
|
+
const containerRef = useRef<HTMLDivElement>(null)
|
|
112
|
+
const contentRef = useRef<HTMLDivElement>(null)
|
|
113
|
+
|
|
114
|
+
// All mutable interaction state lives in a single ref object.
|
|
115
|
+
// Nothing here triggers React re-renders.
|
|
116
|
+
const s = useRef({
|
|
117
|
+
zoom: initialZoom,
|
|
118
|
+
x: 0,
|
|
119
|
+
y: 0,
|
|
120
|
+
dragging: false,
|
|
121
|
+
dragStartX: 0,
|
|
122
|
+
dragStartY: 0,
|
|
123
|
+
posStartX: 0,
|
|
124
|
+
posStartY: 0,
|
|
125
|
+
rafId: 0,
|
|
126
|
+
emitPending: false,
|
|
127
|
+
pinchDist: 0,
|
|
128
|
+
pinchZoom: initialZoom,
|
|
129
|
+
willChangeTimer: 0,
|
|
143
130
|
})
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
},
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
//
|
|
190
|
-
|
|
191
|
-
return
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
syncDisplay()
|
|
224
|
-
markDirty()
|
|
225
|
-
flushEmit()
|
|
226
|
-
}, [applyTransform, syncDisplay, markDirty, flushEmit, initialZoom])
|
|
227
|
-
|
|
228
|
-
// -- Pointer drag --
|
|
229
|
-
|
|
230
|
-
useEffect(() => {
|
|
231
|
-
const el = containerRef.current
|
|
232
|
-
if (!el) return
|
|
233
|
-
|
|
234
|
-
const onDown = (e: PointerEvent) => {
|
|
235
|
-
if (e.button !== 0) return
|
|
236
|
-
if ((e.target as HTMLElement).closest('[data-slot="preview-panel-controls"]')) return
|
|
237
|
-
e.preventDefault()
|
|
238
|
-
el.setPointerCapture(e.pointerId)
|
|
239
|
-
s.current.dragging = true
|
|
240
|
-
s.current.dragStartX = e.clientX
|
|
241
|
-
s.current.dragStartY = e.clientY
|
|
242
|
-
s.current.posStartX = s.current.x
|
|
243
|
-
s.current.posStartY = s.current.y
|
|
244
|
-
el.style.cursor = 'grabbing'
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
const onMove = (e: PointerEvent) => {
|
|
248
|
-
if (!s.current.dragging) return
|
|
249
|
-
s.current.x = s.current.posStartX + (e.clientX - s.current.dragStartX)
|
|
250
|
-
s.current.y = s.current.posStartY + (e.clientY - s.current.dragStartY)
|
|
131
|
+
|
|
132
|
+
// Only React state: the zoom label percentage
|
|
133
|
+
const [displayZoom, setDisplayZoom] = useState(initialZoom)
|
|
134
|
+
|
|
135
|
+
// Stable ref for the callback so effects never re-subscribe
|
|
136
|
+
const onStateChangeRef = useRef(onStateChange)
|
|
137
|
+
onStateChangeRef.current = onStateChange
|
|
138
|
+
|
|
139
|
+
// Flush a pending state emission. Called from RAF or directly by button handlers.
|
|
140
|
+
const flushEmit = useCallback(() => {
|
|
141
|
+
if (!s.current.emitPending) return
|
|
142
|
+
s.current.emitPending = false
|
|
143
|
+
onStateChangeRef.current?.({
|
|
144
|
+
zoom: s.current.zoom,
|
|
145
|
+
x: s.current.x,
|
|
146
|
+
y: s.current.y,
|
|
147
|
+
})
|
|
148
|
+
}, [])
|
|
149
|
+
|
|
150
|
+
// Mark state as dirty so the next RAF tick emits it.
|
|
151
|
+
// For continuous interactions (drag, wheel, pinch) this batches
|
|
152
|
+
// multiple events into one React state update per frame.
|
|
153
|
+
const markDirty = useCallback(() => {
|
|
154
|
+
s.current.emitPending = true
|
|
155
|
+
}, [])
|
|
156
|
+
|
|
157
|
+
// Write transform directly to the DOM element.
|
|
158
|
+
const applyTransform = useCallback((animate: boolean) => {
|
|
159
|
+
const el = contentRef.current
|
|
160
|
+
if (!el) return
|
|
161
|
+
const { x, y, zoom } = s.current
|
|
162
|
+
el.style.transform = `translate3d(${x}px,${y}px,0) scale(${zoom})`
|
|
163
|
+
el.style.transition = animate ? 'transform 0.15s ease-out' : 'none'
|
|
164
|
+
// GPU-composite during interaction, then clear so browser
|
|
165
|
+
// re-rasterizes at the new zoom for crisp SVG text
|
|
166
|
+
el.style.willChange = 'transform'
|
|
167
|
+
clearTimeout(s.current.willChangeTimer)
|
|
168
|
+
s.current.willChangeTimer = window.setTimeout(() => {
|
|
169
|
+
if (contentRef.current && !s.current.dragging) {
|
|
170
|
+
contentRef.current.style.willChange = 'auto'
|
|
171
|
+
}
|
|
172
|
+
}, 200)
|
|
173
|
+
}, [])
|
|
174
|
+
|
|
175
|
+
// Batch DOM writes behind a single requestAnimationFrame.
|
|
176
|
+
// Also flushes any pending state emission in the same frame.
|
|
177
|
+
const scheduleApply = useCallback(() => {
|
|
178
|
+
if (s.current.rafId) return
|
|
179
|
+
s.current.rafId = requestAnimationFrame(() => {
|
|
180
|
+
s.current.rafId = 0
|
|
181
|
+
applyTransform(false)
|
|
182
|
+
flushEmit()
|
|
183
|
+
})
|
|
184
|
+
}, [applyTransform, flushEmit])
|
|
185
|
+
|
|
186
|
+
const syncDisplay = useCallback(() => setDisplayZoom(s.current.zoom), [])
|
|
187
|
+
|
|
188
|
+
// -- Sync from external state (receives changes from a paired panel) --
|
|
189
|
+
|
|
190
|
+
useEffect(() => {
|
|
191
|
+
if (!syncState) return
|
|
192
|
+
const { zoom, x, y } = syncState
|
|
193
|
+
// Epsilon check prevents applying our own emitted state back
|
|
194
|
+
if (Math.abs(s.current.zoom - zoom) < 0.001 && Math.abs(s.current.x - x) < 0.5 && Math.abs(s.current.y - y) < 0.5)
|
|
195
|
+
return
|
|
196
|
+
s.current.zoom = zoom
|
|
197
|
+
s.current.x = x
|
|
198
|
+
s.current.y = y
|
|
199
|
+
// Apply silently without emitting back to avoid ping-pong
|
|
200
|
+
applyTransform(true)
|
|
201
|
+
syncDisplay()
|
|
202
|
+
}, [syncState, applyTransform, syncDisplay])
|
|
203
|
+
|
|
204
|
+
// -- Button handlers (discrete, emit immediately) --
|
|
205
|
+
|
|
206
|
+
const handleZoomIn = useCallback(() => {
|
|
207
|
+
s.current.zoom = clamp(s.current.zoom + ZOOM_STEP_BUTTON, minZoom, maxZoom)
|
|
208
|
+
applyTransform(true)
|
|
209
|
+
syncDisplay()
|
|
251
210
|
markDirty()
|
|
252
|
-
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
el.style.cursor = 'grab'
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
const onLeave = () => {
|
|
263
|
-
if (!s.current.dragging) return
|
|
264
|
-
s.current.dragging = false
|
|
265
|
-
el.style.cursor = 'grab'
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
el.addEventListener('pointerdown', onDown)
|
|
269
|
-
el.addEventListener('pointermove', onMove)
|
|
270
|
-
el.addEventListener('pointerup', onUp)
|
|
271
|
-
el.addEventListener('pointerleave', onLeave)
|
|
272
|
-
return () => {
|
|
273
|
-
el.removeEventListener('pointerdown', onDown)
|
|
274
|
-
el.removeEventListener('pointermove', onMove)
|
|
275
|
-
el.removeEventListener('pointerup', onUp)
|
|
276
|
-
el.removeEventListener('pointerleave', onLeave)
|
|
277
|
-
}
|
|
278
|
-
}, [markDirty, scheduleApply])
|
|
279
|
-
|
|
280
|
-
// -- Wheel zoom (passive: false to preventDefault page scroll) --
|
|
281
|
-
|
|
282
|
-
useEffect(() => {
|
|
283
|
-
const el = containerRef.current
|
|
284
|
-
if (!el) return
|
|
285
|
-
|
|
286
|
-
const onWheel = (e: WheelEvent) => {
|
|
287
|
-
e.preventDefault()
|
|
288
|
-
e.stopPropagation()
|
|
289
|
-
const delta = e.deltaY > 0 ? -ZOOM_STEP_WHEEL : ZOOM_STEP_WHEEL
|
|
290
|
-
s.current.zoom = clamp(s.current.zoom + delta, minZoom, maxZoom)
|
|
211
|
+
flushEmit()
|
|
212
|
+
}, [applyTransform, syncDisplay, markDirty, flushEmit, minZoom, maxZoom])
|
|
213
|
+
|
|
214
|
+
const handleZoomOut = useCallback(() => {
|
|
215
|
+
s.current.zoom = clamp(s.current.zoom - ZOOM_STEP_BUTTON, minZoom, maxZoom)
|
|
216
|
+
applyTransform(true)
|
|
217
|
+
syncDisplay()
|
|
291
218
|
markDirty()
|
|
292
|
-
|
|
219
|
+
flushEmit()
|
|
220
|
+
}, [applyTransform, syncDisplay, markDirty, flushEmit, minZoom, maxZoom])
|
|
221
|
+
|
|
222
|
+
const handleReset = useCallback(() => {
|
|
223
|
+
s.current.zoom = initialZoom
|
|
224
|
+
s.current.x = 0
|
|
225
|
+
s.current.y = 0
|
|
226
|
+
applyTransform(true)
|
|
293
227
|
syncDisplay()
|
|
294
|
-
|
|
228
|
+
markDirty()
|
|
229
|
+
flushEmit()
|
|
230
|
+
}, [applyTransform, syncDisplay, markDirty, flushEmit, initialZoom])
|
|
295
231
|
|
|
296
|
-
|
|
297
|
-
return () => el.removeEventListener('wheel', onWheel)
|
|
298
|
-
}, [markDirty, scheduleApply, syncDisplay, minZoom, maxZoom])
|
|
232
|
+
// -- Pointer drag --
|
|
299
233
|
|
|
300
|
-
|
|
234
|
+
useEffect(() => {
|
|
235
|
+
const el = containerRef.current
|
|
236
|
+
if (!el) return
|
|
301
237
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
238
|
+
const onDown = (e: PointerEvent) => {
|
|
239
|
+
if (e.button !== 0) return
|
|
240
|
+
if ((e.target as HTMLElement).closest('[data-slot="preview-panel-controls"]')) return
|
|
241
|
+
e.preventDefault()
|
|
242
|
+
el.setPointerCapture(e.pointerId)
|
|
243
|
+
s.current.dragging = true
|
|
244
|
+
s.current.dragStartX = e.clientX
|
|
245
|
+
s.current.dragStartY = e.clientY
|
|
246
|
+
s.current.posStartX = s.current.x
|
|
247
|
+
s.current.posStartY = s.current.y
|
|
248
|
+
el.style.cursor = 'grabbing'
|
|
249
|
+
}
|
|
305
250
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
251
|
+
const onMove = (e: PointerEvent) => {
|
|
252
|
+
if (!s.current.dragging) return
|
|
253
|
+
s.current.x = s.current.posStartX + (e.clientX - s.current.dragStartX)
|
|
254
|
+
s.current.y = s.current.posStartY + (e.clientY - s.current.dragStartY)
|
|
255
|
+
markDirty()
|
|
256
|
+
scheduleApply()
|
|
257
|
+
}
|
|
311
258
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
259
|
+
const onUp = (e: PointerEvent) => {
|
|
260
|
+
if (!s.current.dragging) return
|
|
261
|
+
s.current.dragging = false
|
|
262
|
+
el.releasePointerCapture(e.pointerId)
|
|
263
|
+
el.style.cursor = 'grab'
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const onLeave = () => {
|
|
267
|
+
if (!s.current.dragging) return
|
|
268
|
+
s.current.dragging = false
|
|
269
|
+
el.style.cursor = 'grab'
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
el.addEventListener('pointerdown', onDown)
|
|
273
|
+
el.addEventListener('pointermove', onMove)
|
|
274
|
+
el.addEventListener('pointerup', onUp)
|
|
275
|
+
el.addEventListener('pointerleave', onLeave)
|
|
276
|
+
return () => {
|
|
277
|
+
el.removeEventListener('pointerdown', onDown)
|
|
278
|
+
el.removeEventListener('pointermove', onMove)
|
|
279
|
+
el.removeEventListener('pointerup', onUp)
|
|
280
|
+
el.removeEventListener('pointerleave', onLeave)
|
|
317
281
|
}
|
|
318
|
-
}
|
|
282
|
+
}, [markDirty, scheduleApply])
|
|
319
283
|
|
|
320
|
-
|
|
321
|
-
|
|
284
|
+
// -- Wheel zoom (passive: false to preventDefault page scroll) --
|
|
285
|
+
|
|
286
|
+
useEffect(() => {
|
|
287
|
+
const el = containerRef.current
|
|
288
|
+
if (!el) return
|
|
289
|
+
|
|
290
|
+
const onWheel = (e: WheelEvent) => {
|
|
322
291
|
e.preventDefault()
|
|
323
|
-
|
|
324
|
-
|
|
292
|
+
e.stopPropagation()
|
|
293
|
+
const delta = e.deltaY > 0 ? -ZOOM_STEP_WHEEL : ZOOM_STEP_WHEEL
|
|
294
|
+
s.current.zoom = clamp(s.current.zoom + delta, minZoom, maxZoom)
|
|
325
295
|
markDirty()
|
|
326
296
|
scheduleApply()
|
|
327
297
|
syncDisplay()
|
|
328
298
|
}
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
el.addEventListener('touchstart', onTouchStart, { passive: false })
|
|
332
|
-
el.addEventListener('touchmove', onTouchMove, { passive: false })
|
|
333
|
-
return () => {
|
|
334
|
-
el.removeEventListener('touchstart', onTouchStart)
|
|
335
|
-
el.removeEventListener('touchmove', onTouchMove)
|
|
336
|
-
}
|
|
337
|
-
}, [markDirty, scheduleApply, syncDisplay, minZoom, maxZoom])
|
|
338
|
-
|
|
339
|
-
// Cleanup on unmount
|
|
340
|
-
useEffect(() => {
|
|
341
|
-
return () => {
|
|
342
|
-
if (s.current.rafId) cancelAnimationFrame(s.current.rafId)
|
|
343
|
-
clearTimeout(s.current.willChangeTimer)
|
|
344
|
-
}
|
|
345
|
-
}, [])
|
|
346
299
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
[html, children],
|
|
351
|
-
)
|
|
300
|
+
el.addEventListener('wheel', onWheel, { passive: false })
|
|
301
|
+
return () => el.removeEventListener('wheel', onWheel)
|
|
302
|
+
}, [markDirty, scheduleApply, syncDisplay, minZoom, maxZoom])
|
|
352
303
|
|
|
353
|
-
|
|
354
|
-
const containerStyle = useMemo(
|
|
355
|
-
() => ({ maxHeight, cursor: 'grab' as const, touchAction: 'none' as const }),
|
|
356
|
-
[maxHeight],
|
|
357
|
-
)
|
|
358
|
-
const direction = useDirection(dir as Direction)
|
|
304
|
+
// -- Pinch to zoom (two-finger touch) --
|
|
359
305
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
306
|
+
useEffect(() => {
|
|
307
|
+
const el = containerRef.current
|
|
308
|
+
if (!el) return
|
|
309
|
+
|
|
310
|
+
const dist = (e: TouchEvent) => {
|
|
311
|
+
const a = e.touches[0] ?? { clientX: 0, clientY: 0 }
|
|
312
|
+
const b = e.touches[1] ?? { clientX: 0, clientY: 0 }
|
|
313
|
+
return Math.hypot(a.clientX - b.clientX, a.clientY - b.clientY)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const onTouchStart = (e: TouchEvent) => {
|
|
317
|
+
if (e.touches.length === 2) {
|
|
318
|
+
e.preventDefault()
|
|
319
|
+
s.current.pinchDist = dist(e)
|
|
320
|
+
s.current.pinchZoom = s.current.zoom
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const onTouchMove = (e: TouchEvent) => {
|
|
325
|
+
if (e.touches.length === 2) {
|
|
326
|
+
e.preventDefault()
|
|
327
|
+
const scale = dist(e) / s.current.pinchDist
|
|
328
|
+
s.current.zoom = clamp(s.current.pinchZoom * scale, minZoom, maxZoom)
|
|
329
|
+
markDirty()
|
|
330
|
+
scheduleApply()
|
|
331
|
+
syncDisplay()
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
el.addEventListener('touchstart', onTouchStart, { passive: false })
|
|
336
|
+
el.addEventListener('touchmove', onTouchMove, { passive: false })
|
|
337
|
+
return () => {
|
|
338
|
+
el.removeEventListener('touchstart', onTouchStart)
|
|
339
|
+
el.removeEventListener('touchmove', onTouchMove)
|
|
340
|
+
}
|
|
341
|
+
}, [markDirty, scheduleApply, syncDisplay, minZoom, maxZoom])
|
|
342
|
+
|
|
343
|
+
// Cleanup on unmount
|
|
344
|
+
useEffect(() => {
|
|
345
|
+
return () => {
|
|
346
|
+
if (s.current.rafId) cancelAnimationFrame(s.current.rafId)
|
|
347
|
+
clearTimeout(s.current.willChangeTimer)
|
|
348
|
+
}
|
|
349
|
+
}, [])
|
|
350
|
+
|
|
351
|
+
// Stable content props - only changes when html/children identity changes
|
|
352
|
+
const contentProps = useMemo(
|
|
353
|
+
() => (html ? { dangerouslySetInnerHTML: { __html: html } } : { children }),
|
|
354
|
+
[html, children],
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
// Stable inline style for the container
|
|
358
|
+
const containerStyle = useMemo(
|
|
359
|
+
() => ({ maxHeight, cursor: 'grab' as const, touchAction: 'none' as const }),
|
|
360
|
+
[maxHeight],
|
|
361
|
+
)
|
|
362
|
+
const direction = useDirection(dir as Direction)
|
|
363
|
+
|
|
364
|
+
return (
|
|
372
365
|
<div
|
|
373
|
-
ref={
|
|
374
|
-
|
|
375
|
-
|
|
366
|
+
ref={ref}
|
|
367
|
+
data-slot="preview-panel"
|
|
368
|
+
className={cn('relative flex flex-col', className)}
|
|
369
|
+
dir={direction}
|
|
370
|
+
style={style}
|
|
371
|
+
{...rest}>
|
|
372
|
+
{showControls && (
|
|
373
|
+
<div className="absolute end-3 top-3 z-10">
|
|
374
|
+
<ZoomControls onZoomIn={handleZoomIn} onZoomOut={handleZoomOut} onReset={handleReset} zoom={displayZoom} />
|
|
375
|
+
</div>
|
|
376
|
+
)}
|
|
376
377
|
<div
|
|
377
|
-
ref={
|
|
378
|
-
className="flex
|
|
379
|
-
style={
|
|
380
|
-
|
|
381
|
-
|
|
378
|
+
ref={containerRef}
|
|
379
|
+
className="flex flex-1 items-center justify-center overflow-hidden"
|
|
380
|
+
style={containerStyle}>
|
|
381
|
+
<div
|
|
382
|
+
ref={contentRef}
|
|
383
|
+
className="flex w-full items-center justify-center p-6"
|
|
384
|
+
style={CONTENT_STYLE}
|
|
385
|
+
{...contentProps}
|
|
386
|
+
/>
|
|
387
|
+
</div>
|
|
382
388
|
</div>
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
389
|
+
)
|
|
390
|
+
},
|
|
391
|
+
)
|
|
392
|
+
PreviewPanel.displayName = 'PreviewPanel'
|
|
386
393
|
|
|
387
394
|
const CONTENT_STYLE = { transformOrigin: 'center center' } as const
|
|
388
395
|
|
|
@@ -3,35 +3,36 @@
|
|
|
3
3
|
import { cn } from '@gentleduck/libs/cn'
|
|
4
4
|
import { type Direction, useDirection } from '@gentleduck/primitives/direction'
|
|
5
5
|
import { GripVertical } from 'lucide-react'
|
|
6
|
-
import
|
|
6
|
+
import React from 'react'
|
|
7
7
|
import * as ResizablePrimitive from 'react-resizable-panels'
|
|
8
8
|
|
|
9
|
-
const ResizablePanelGroup =
|
|
10
|
-
|
|
11
|
-
dir
|
|
12
|
-
|
|
13
|
-
}: React.ComponentProps<typeof ResizablePrimitive.Group> & { dir?: Direction }) => {
|
|
9
|
+
const ResizablePanelGroup = React.forwardRef<
|
|
10
|
+
HTMLDivElement,
|
|
11
|
+
React.ComponentPropsWithoutRef<typeof ResizablePrimitive.Group> & { dir?: Direction }
|
|
12
|
+
>(({ className, dir, ...props }, ref) => {
|
|
14
13
|
const direction = useDirection(dir as Direction)
|
|
15
14
|
return (
|
|
16
15
|
<ResizablePrimitive.Group
|
|
17
16
|
className={cn('flex h-full w-full data-[panel-group-direction=vertical]:flex-col', className)}
|
|
18
17
|
data-slot="panel-group"
|
|
19
18
|
dir={direction}
|
|
19
|
+
elementRef={ref}
|
|
20
20
|
{...props}
|
|
21
21
|
/>
|
|
22
22
|
)
|
|
23
|
-
}
|
|
23
|
+
})
|
|
24
|
+
ResizablePanelGroup.displayName = 'ResizablePanelGroup'
|
|
24
25
|
|
|
25
26
|
const ResizablePanel = ResizablePrimitive.Panel
|
|
26
27
|
|
|
27
|
-
const ResizableHandle =
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
}) => (
|
|
28
|
+
const ResizableHandle = React.forwardRef<
|
|
29
|
+
HTMLDivElement,
|
|
30
|
+
React.ComponentPropsWithoutRef<typeof ResizablePrimitive.Separator> & {
|
|
31
|
+
withHandle?: boolean
|
|
32
|
+
}
|
|
33
|
+
>(({ withHandle, className, ...props }, ref) => (
|
|
34
34
|
<ResizablePrimitive.Separator
|
|
35
|
+
elementRef={ref}
|
|
35
36
|
aria-label="Resize panels"
|
|
36
37
|
className={cn(
|
|
37
38
|
'relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:translate-x-0 data-[panel-group-direction=vertical]:after:-translate-y-1/2 [&[data-panel-group-direction=vertical]>div]:rotate-90',
|
|
@@ -47,6 +48,7 @@ const ResizableHandle = ({
|
|
|
47
48
|
</div>
|
|
48
49
|
)}
|
|
49
50
|
</ResizablePrimitive.Separator>
|
|
50
|
-
)
|
|
51
|
+
))
|
|
52
|
+
ResizableHandle.displayName = 'ResizableHandle'
|
|
51
53
|
|
|
52
54
|
export { ResizablePanelGroup, ResizablePanel, ResizableHandle }
|
package/src/select/select.tsx
CHANGED
|
@@ -6,10 +6,13 @@ import { Check, ChevronDown, ChevronUp } from 'lucide-react'
|
|
|
6
6
|
import * as React from 'react'
|
|
7
7
|
|
|
8
8
|
const Select = SelectPrimitive.Root
|
|
9
|
+
Select.displayName = 'Select'
|
|
9
10
|
|
|
10
11
|
const SelectGroup = SelectPrimitive.Group
|
|
12
|
+
SelectGroup.displayName = 'SelectGroup'
|
|
11
13
|
|
|
12
14
|
const SelectValue = SelectPrimitive.Value
|
|
15
|
+
SelectValue.displayName = 'SelectValue'
|
|
13
16
|
|
|
14
17
|
const SelectTrigger = React.forwardRef<
|
|
15
18
|
React.ComponentRef<typeof SelectPrimitive.Trigger>,
|