@bycrux/editor 0.11.2 → 1.0.1
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 +3 -1
- package/src/ControlsInfoModal.tsx +251 -61
- package/src/__tests__/ControlsInfoModal.test.tsx +43 -0
- package/src/__tests__/adapter.test.ts +59 -1
- package/src/__tests__/schema-assignability.test.ts +93 -0
- package/src/__tests__/schema.test.ts +15 -0
- package/src/__tests__/video-adapter-contract.test.ts +128 -5
- package/src/carousel/AddElementMenu.tsx +10 -4
- package/src/carousel/CarouselEditor.tsx +46 -8
- package/src/carousel/CarouselRenderModal.tsx +15 -10
- package/src/carousel/OverlayPicker.tsx +10 -3
- package/src/carousel/SlidePropertyPanel.tsx +43 -21
- package/src/components/FilmstripScrubber.tsx +277 -0
- package/src/components/__tests__/FilmstripScrubber.test.tsx +216 -0
- package/src/engine/__tests__/audio-clock.test.ts +655 -0
- package/src/engine/__tests__/audio-worklet-source.test.ts +316 -0
- package/src/engine/__tests__/batch-planner.test.ts +298 -0
- package/src/engine/__tests__/decode-worker-source.test.ts +249 -0
- package/src/engine/__tests__/demux-ranged.test.ts +495 -0
- package/src/engine/__tests__/demux-truncated.test.ts +136 -0
- package/src/engine/__tests__/demux.test.ts +305 -0
- package/src/engine/__tests__/eligibility.test.ts +146 -0
- package/src/engine/__tests__/engine-recovery.test.ts +263 -0
- package/src/engine/__tests__/engine.test.ts +326 -0
- package/src/engine/__tests__/frame-server-ranged.test.ts +261 -0
- package/src/engine/__tests__/frame-server.test.ts +326 -0
- package/src/engine/__tests__/media-loader-ranged.test.ts +289 -0
- package/src/engine/__tests__/media-loader.test.ts +100 -0
- package/src/engine/__tests__/scheduler-crop.test.ts +353 -0
- package/src/engine/__tests__/scheduler.test.ts +1310 -0
- package/src/engine/__tests__/scrub-resolve.test.ts +96 -0
- package/src/engine/__tests__/scrub-source.test.ts +195 -0
- package/src/engine/__tests__/source-host.test.ts +455 -0
- package/src/engine/__tests__/time-stretch.test.ts +182 -0
- package/src/engine/audio-clock.ts +1179 -0
- package/src/engine/audio-worklet-source.ts +160 -0
- package/src/engine/batch-planner.ts +308 -0
- package/src/engine/debug-hud.tsx +54 -0
- package/src/engine/decode-worker-source.ts +142 -0
- package/src/engine/demux.ts +900 -0
- package/src/engine/eligibility.ts +140 -0
- package/src/engine/frame-server.ts +644 -0
- package/src/engine/index.ts +950 -0
- package/src/engine/media-loader.ts +380 -0
- package/src/engine/mp4box.d.ts +214 -0
- package/src/engine/scheduler.ts +1324 -0
- package/src/engine/scrub-resolve.ts +66 -0
- package/src/engine/scrub-source.ts +496 -0
- package/src/engine/time-stretch.ts +224 -0
- package/src/index.ts +83 -1
- package/src/preview/OverlayPreview.tsx +2 -24
- package/src/schema.ts +146 -3
- package/src/state/__tests__/use-project-sync.test.tsx +56 -0
- package/src/state/use-project-sync.ts +17 -0
- package/src/test-setup.ts +32 -0
- package/src/text/FontPicker.tsx +85 -51
- package/src/text/TextFormattingToolbar.tsx +6 -1
- package/src/text/__tests__/FontPicker.options.test.ts +43 -0
- package/src/text/__tests__/TextFormattingToolbar.test.tsx +4 -4
- package/src/theme.ts +108 -3
- package/src/types.ts +601 -22
- package/src/ui/Loader.tsx +64 -0
- package/src/ui/NumberField.tsx +254 -0
- package/src/ui/Slider.tsx +128 -0
- package/src/ui/Tooltip.tsx +118 -0
- package/src/ui/__tests__/NumberField.test.tsx +298 -0
- package/src/ui/__tests__/Slider.test.tsx +150 -0
- package/src/ui/__tests__/Tooltip.test.tsx +105 -0
- package/src/ui/__tests__/usePersistentState.test.tsx +112 -0
- package/src/ui/badge.tsx +12 -4
- package/src/ui/index.ts +6 -0
- package/src/ui/input.tsx +1 -1
- package/src/ui/select.tsx +1 -1
- package/src/ui/switch.tsx +12 -2
- package/src/ui/textarea.tsx +1 -1
- package/src/ui/usePersistentState.ts +63 -0
- package/src/video/AudioPolishModal.tsx +983 -0
- package/src/video/CaptionListPanel.test.tsx +944 -0
- package/src/video/CaptionListPanel.tsx +1106 -0
- package/src/video/CaptionRegenModal.tsx +37 -9
- package/src/video/CaptionSpecimen.tsx +160 -0
- package/src/video/CaptionStyleGallery.tsx +466 -0
- package/src/video/CommandPalette.tsx +165 -0
- package/src/video/ImageToneMenu.tsx +137 -0
- package/src/video/OverlayInspector.tsx +977 -0
- package/src/video/RenderModal.tsx +1035 -62
- package/src/video/VersionCompare.tsx +258 -0
- package/src/video/VersionPanel.tsx +117 -42
- package/src/video/VideoEditor.tsx +2229 -243
- package/src/video/__tests__/AudioPolishModal.test.tsx +825 -0
- package/src/video/__tests__/CaptionListPanel.font.test.tsx +397 -0
- package/src/video/__tests__/CaptionListPanel.generate.test.tsx +153 -0
- package/src/video/__tests__/CaptionRegenModal.test.tsx +29 -0
- package/src/video/__tests__/CaptionSpecimen.test.tsx +213 -0
- package/src/video/__tests__/CaptionStyleGallery.test.tsx +362 -0
- package/src/video/__tests__/CommandPalette.test.tsx +119 -0
- package/src/video/__tests__/OverlayInspector.test.tsx +1367 -0
- package/src/video/__tests__/RenderModal.exportControls.test.tsx +319 -0
- package/src/video/__tests__/RenderModal.options.test.tsx +562 -0
- package/src/video/__tests__/RenderModal.progress.test.ts +65 -0
- package/src/video/__tests__/VersionCompare.test.tsx +182 -0
- package/src/video/__tests__/VersionPanel.test.tsx +279 -0
- package/src/video/__tests__/VideoEditor.audioPolish.test.tsx +241 -0
- package/src/video/__tests__/VideoEditor.captionDelete.test.tsx +147 -0
- package/src/video/__tests__/VideoEditor.captionGesture.test.tsx +170 -0
- package/src/video/__tests__/VideoEditor.captionSeam.test.tsx +134 -0
- package/src/video/__tests__/VideoEditor.clipKeyframes.test.tsx +59 -0
- package/src/video/__tests__/VideoEditor.context.test.tsx +44 -0
- package/src/video/__tests__/VideoEditor.editFocus.test.tsx +55 -0
- package/src/video/__tests__/VideoEditor.keymap.test.tsx +725 -0
- package/src/video/__tests__/VideoEditor.layout.test.tsx +338 -0
- package/src/video/__tests__/VideoEditor.propertiesPanel.test.tsx +765 -0
- package/src/video/__tests__/VideoEditor.rippleDeleteCaptions.test.tsx +159 -0
- package/src/video/__tests__/VideoEditor.sourcePreview.test.tsx +122 -0
- package/src/video/__tests__/VideoEditor.test.tsx +403 -50
- package/src/video/__tests__/audioMagnet.test.ts +158 -0
- package/src/video/__tests__/audioPolish.test.ts +1202 -0
- package/src/video/__tests__/captionActiveWord.test.ts +107 -0
- package/src/video/__tests__/captionLanes.test.ts +262 -0
- package/src/video/__tests__/captionPositioning.test.tsx +8 -3
- package/src/video/__tests__/captionWordFloor.test.ts +228 -0
- package/src/video/__tests__/clipboard-ops.test.ts +430 -0
- package/src/video/__tests__/cuts.insert.test.ts +244 -0
- package/src/video/__tests__/cuts.test.ts +1213 -34
- package/src/video/__tests__/export-limits.test.ts +195 -0
- package/src/video/__tests__/hover-scrub.test.ts +163 -0
- package/src/video/__tests__/keyframeOps.canKeyframeProp.test.ts +61 -0
- package/src/video/__tests__/keyframeOps.test.ts +643 -0
- package/src/video/__tests__/keymap.test.tsx +171 -0
- package/src/video/__tests__/render-progress.test.tsx +20 -4
- package/src/video/__tests__/shuttle.test.ts +210 -0
- package/src/video/__tests__/source-preview.test.ts +60 -0
- package/src/video/__tests__/timecode.test.ts +77 -0
- package/src/video/__tests__/use-report-context.test.tsx +101 -0
- package/src/video/audioMagnet.ts +72 -0
- package/src/video/audioPolish.ts +774 -0
- package/src/video/captionActiveWord.ts +74 -0
- package/src/video/captionLanes.ts +202 -0
- package/src/video/captionRepair.ts +9 -5
- package/src/video/captionStyleDefaults.ts +100 -0
- package/src/video/captionWordFloor.ts +83 -0
- package/src/video/clipboard-ops.ts +377 -0
- package/src/video/cuts.ts +713 -37
- package/src/video/export-limits.ts +102 -0
- package/src/video/hover-scrub.ts +102 -0
- package/src/video/imageTone.ts +58 -0
- package/src/video/imageToneExamples.ts +10 -0
- package/src/video/keyframeOps.ts +384 -0
- package/src/video/keymap.ts +146 -0
- package/src/video/panels/ClipPropertiesPanel.tsx +706 -0
- package/src/video/panels/LeftPanelTabs.tsx +187 -0
- package/src/video/panels/OverlayContentPanel.tsx +351 -0
- package/src/video/panels/TabNav.tsx +60 -0
- package/src/video/panels/__tests__/ClipPropertiesPanel.test.tsx +645 -0
- package/src/video/panels/__tests__/LeftPanelTabs.test.tsx +189 -0
- package/src/video/panels/__tests__/OverlayContentPanel.test.tsx +222 -0
- package/src/video/panels/__tests__/TabNav.test.tsx +71 -0
- package/src/video/preview/CaptionPreview.tsx +57 -69
- package/src/video/preview/EngineSurface.tsx +103 -0
- package/src/video/preview/OverlayItemsLayer.tsx +315 -103
- package/src/video/preview/PreviewPlayer.tsx +337 -56
- package/src/video/preview/SocialPreviewMenu.tsx +214 -0
- package/src/video/preview/SocialSafeZoneOverlay.tsx +478 -0
- package/src/video/preview/__tests__/CaptionPreview.fonts.test.tsx +97 -0
- package/src/video/preview/__tests__/EngineSurface.test.tsx +114 -0
- package/src/video/preview/__tests__/OverlayItemsLayer.edit.test.tsx +4 -2
- package/src/video/preview/__tests__/OverlayItemsLayer.keyframes.test.tsx +363 -0
- package/src/video/preview/__tests__/OverlayItemsLayer.selection.test.tsx +329 -0
- package/src/video/preview/__tests__/PreviewPlayer.engine.test.tsx +139 -0
- package/src/video/preview/__tests__/SocialPreviewMenu.test.tsx +121 -0
- package/src/video/preview/__tests__/SocialSafeZoneOverlay.test.tsx +165 -0
- package/src/video/preview/__tests__/captionDragState.test.ts +120 -1
- package/src/video/preview/__tests__/latencyCompensation.test.tsx +451 -0
- package/src/video/preview/__tests__/proxySupport.test.ts +75 -0
- package/src/video/preview/__tests__/transformStyle.test.ts +29 -1
- package/src/video/preview/__tests__/useDragOverlay.perAxis.test.ts +197 -0
- package/src/video/preview/__tests__/useEnginePlayback.test.tsx +530 -0
- package/src/video/preview/__tests__/useVideoPlayback.corpus.test.ts +313 -0
- package/src/video/preview/__tests__/useVideoPlayback.test.ts +38 -5
- package/src/video/preview/__tests__/useVideoPlayback.trackAudio.test.ts +278 -0
- package/src/video/preview/audio-context.ts +111 -0
- package/src/video/preview/captionDragState.ts +91 -1
- package/src/video/preview/proxySupport.ts +86 -0
- package/src/video/preview/transformStyle.ts +22 -12
- package/src/video/preview/useDragOverlay.ts +92 -18
- package/src/video/preview/useEnginePlayback.ts +625 -0
- package/src/video/preview/useVideoPlayback.ts +211 -167
- package/src/video/sdrCurves.ts +56 -0
- package/src/video/shuttle.ts +159 -0
- package/src/video/source-preview.ts +66 -0
- package/src/video/timecode.ts +59 -0
- package/src/video/timeline/EditableSegment.tsx +1 -1
- package/src/video/timeline/Scrubber.tsx +46 -174
- package/src/video/timeline/SpeedControl.tsx +95 -0
- package/src/video/timeline/Timeline.tsx +1061 -328
- package/src/video/timeline/TimelineContext.ts +17 -10
- package/src/video/timeline/TrackGutter.tsx +560 -0
- package/src/video/timeline/TrackSettingsPopover.tsx +228 -0
- package/src/video/timeline/VolumeControl.tsx +113 -0
- package/src/video/timeline/__tests__/Timeline.backgroundClick.test.tsx +110 -0
- package/src/video/timeline/__tests__/Timeline.crossfade.test.tsx +104 -0
- package/src/video/timeline/__tests__/Timeline.fadeCurveMenu.test.tsx +174 -0
- package/src/video/timeline/__tests__/Timeline.keyframeDelete.test.tsx +273 -0
- package/src/video/timeline/__tests__/Timeline.keyframeFollow.test.tsx +215 -0
- package/src/video/timeline/__tests__/Timeline.keyframeMenu.test.tsx +253 -0
- package/src/video/timeline/__tests__/Timeline.keymap.test.tsx +372 -0
- package/src/video/timeline/__tests__/Timeline.subcutRegen.test.tsx +351 -0
- package/src/video/timeline/__tests__/TrackGutter.test.tsx +372 -0
- package/src/video/timeline/__tests__/_canvasSelect.test.tsx +273 -0
- package/src/video/timeline/__tests__/_canvasSelect.ts +414 -0
- package/src/video/timeline/__tests__/dragdrop-math.test.ts +135 -0
- package/src/video/timeline/__tests__/effectiveItemAudio.test.ts +50 -0
- package/src/video/timeline/__tests__/enabledTrackItems.test.ts +164 -0
- package/src/video/timeline/__tests__/moveItemAcrossTracks.test.ts +363 -0
- package/src/video/timeline/__tests__/multiSelectOps.test.ts +447 -0
- package/src/video/timeline/__tests__/placement.test.ts +278 -0
- package/src/video/timeline/__tests__/resizeWindowedItem.test.ts +140 -0
- package/src/video/timeline/__tests__/timeline-model.test.ts +576 -0
- package/src/video/timeline/__tests__/visualItemLabel.test.ts +69 -0
- package/src/video/timeline/canvas/TimelineCanvas.tsx +1447 -0
- package/src/video/timeline/canvas/__tests__/TimelineCanvas.drop.test.tsx +439 -0
- package/src/video/timeline/canvas/__tests__/TimelineCanvas.edgeScroll.test.tsx +346 -0
- package/src/video/timeline/canvas/__tests__/TimelineCanvas.panefill.test.tsx +122 -0
- package/src/video/timeline/canvas/__tests__/TimelineCanvas.pendingDrops.test.tsx +316 -0
- package/src/video/timeline/canvas/__tests__/TimelineCanvas.pointer.test.tsx +606 -0
- package/src/video/timeline/canvas/__tests__/TimelineCanvas.test.tsx +407 -0
- package/src/video/timeline/canvas/__tests__/clip-bands.test.ts +77 -0
- package/src/video/timeline/canvas/__tests__/draw.test.ts +2198 -0
- package/src/video/timeline/canvas/__tests__/fade-curve.test.ts +187 -0
- package/src/video/timeline/canvas/__tests__/filmstrips.test.ts +561 -0
- package/src/video/timeline/canvas/__tests__/hit-test.test.ts +818 -0
- package/src/video/timeline/canvas/__tests__/pending-drop.test.ts +210 -0
- package/src/video/timeline/canvas/__tests__/pointer-machine.test.ts +3358 -0
- package/src/video/timeline/canvas/__tests__/snap.test.ts +257 -0
- package/src/video/timeline/canvas/__tests__/viewport.test.ts +399 -0
- package/src/video/timeline/canvas/__tests__/waveforms.test.ts +946 -0
- package/src/video/timeline/canvas/clip-bands.ts +56 -0
- package/src/video/timeline/canvas/draw.ts +2187 -0
- package/src/video/timeline/canvas/fade-curve.ts +111 -0
- package/src/video/timeline/canvas/filmstrips.ts +418 -0
- package/src/video/timeline/canvas/hit-test.ts +501 -0
- package/src/video/timeline/canvas/keyframe-strip.ts +73 -0
- package/src/video/timeline/canvas/pointer-machine.ts +1828 -0
- package/src/video/timeline/canvas/snap.ts +232 -0
- package/src/video/timeline/canvas/viewport.ts +457 -0
- package/src/video/timeline/canvas/waveforms.ts +664 -0
- package/src/video/timeline/makeCaptionEdit.ts +5 -1
- package/src/video/timeline/multiSelectOps.ts +214 -55
- package/src/video/timeline/placement.ts +282 -0
- package/src/video/timeline/timeline-model.ts +919 -0
- package/src/video/timeline/useItemDragDrop.ts +151 -178
- package/src/video/timeline/useTimelineZoom.ts +25 -60
- package/src/video/timeline/utils.ts +0 -12
- package/src/video/use-report-context.ts +75 -0
- package/src/video/preview/OverlayPropsModal.tsx +0 -292
- package/src/video/preview/__tests__/OverlayPropsModal.test.tsx +0 -32
- package/src/video/timeline/AudioTrackRow.tsx +0 -404
- package/src/video/timeline/AudioWaveformLayer.tsx +0 -117
- package/src/video/timeline/CaptionTrackRow.tsx +0 -235
- package/src/video/timeline/PlayheadLine.tsx +0 -18
- package/src/video/timeline/TranscriptModal.tsx +0 -70
- package/src/video/timeline/TranscriptPanel.tsx +0 -273
- package/src/video/timeline/VisualTrackRow.tsx +0 -300
- package/src/video/timeline/__tests__/CaptionTrackRow.test.tsx +0 -241
- package/src/video/timeline/__tests__/PlayheadLine.test.tsx +0 -60
- package/src/video/timeline/__tests__/TranscriptModal.test.tsx +0 -41
- package/src/video/timeline/__tests__/TranscriptPanel.test.tsx +0 -184
- package/src/video/timeline/__tests__/useItemDragDrop.test.ts +0 -72
package/src/ui/index.ts
CHANGED
|
@@ -7,6 +7,10 @@ export type { ButtonProps } from './button'
|
|
|
7
7
|
export { Input, inspectorInputClass } from './input'
|
|
8
8
|
export type { InputProps } from './input'
|
|
9
9
|
export { Label } from './label'
|
|
10
|
+
export { NumberField, stepValue } from './NumberField'
|
|
11
|
+
export type { NumberFieldProps } from './NumberField'
|
|
12
|
+
export { Slider } from './Slider'
|
|
13
|
+
export type { SliderProps } from './Slider'
|
|
10
14
|
export { Select } from './select'
|
|
11
15
|
export type { SelectProps } from './select'
|
|
12
16
|
export { SwatchInput } from './SwatchInput'
|
|
@@ -16,3 +20,5 @@ export { Textarea } from './textarea'
|
|
|
16
20
|
export type { TextareaProps } from './textarea'
|
|
17
21
|
export { Badge, StatusBadge } from './badge'
|
|
18
22
|
export type { BadgeProps } from './badge'
|
|
23
|
+
export { Tooltip } from './Tooltip'
|
|
24
|
+
export type { TooltipProps } from './Tooltip'
|
package/src/ui/input.tsx
CHANGED
|
@@ -12,7 +12,7 @@ export function Input({ className, ...props }: InputProps) {
|
|
|
12
12
|
return (
|
|
13
13
|
<input
|
|
14
14
|
className={cn(
|
|
15
|
-
'flex h-9 w-full rounded-md border border-
|
|
15
|
+
'flex h-9 w-full rounded-md border border-[var(--editor-border)] bg-[var(--editor-surface)] px-3 py-1 text-sm text-[var(--editor-text)] placeholder:text-[var(--editor-text)]/60 focus:outline-none focus:ring-2 focus:ring-[var(--editor-accent)] disabled:opacity-50',
|
|
16
16
|
className,
|
|
17
17
|
)}
|
|
18
18
|
{...props}
|
package/src/ui/select.tsx
CHANGED
|
@@ -8,7 +8,7 @@ export function Select({ className, options, ...props }: SelectProps) {
|
|
|
8
8
|
return (
|
|
9
9
|
<select
|
|
10
10
|
className={cn(
|
|
11
|
-
'flex h-9 w-full rounded-md border border-
|
|
11
|
+
'flex h-9 w-full rounded-md border border-[var(--editor-border)] bg-[var(--editor-surface)] px-3 py-1 text-sm text-[var(--editor-text)] focus:outline-none focus:ring-2 focus:ring-[var(--editor-accent)] disabled:opacity-50',
|
|
12
12
|
className,
|
|
13
13
|
)}
|
|
14
14
|
{...props}
|
package/src/ui/switch.tsx
CHANGED
|
@@ -5,18 +5,28 @@ interface SwitchProps {
|
|
|
5
5
|
onCheckedChange: (v: boolean) => void
|
|
6
6
|
className?: string
|
|
7
7
|
disabled?: boolean
|
|
8
|
+
/** Passed straight through to the underlying button — this component has no
|
|
9
|
+
* visible text of its own, so a caller placing the switch beside a text
|
|
10
|
+
* label (rather than wrapping it in one) needs this for an accessible name. */
|
|
11
|
+
'aria-label'?: string
|
|
12
|
+
/** Editor theme mode — light/dark. Only affects the unchecked track's hue:
|
|
13
|
+
* `--editor-border` (#e5e7eb) on a light `--editor-surface` (#ffffff) is
|
|
14
|
+
* ~1.2:1, so an off switch with a white knob effectively disappears.
|
|
15
|
+
* Absent -> dark, matching every existing caller. */
|
|
16
|
+
mode?: 'light' | 'dark'
|
|
8
17
|
}
|
|
9
18
|
|
|
10
|
-
export function Switch({ checked, onCheckedChange, className, disabled }: SwitchProps) {
|
|
19
|
+
export function Switch({ checked, onCheckedChange, className, disabled, 'aria-label': ariaLabel, mode = 'dark' }: SwitchProps) {
|
|
11
20
|
return (
|
|
12
21
|
<button
|
|
13
22
|
role="switch"
|
|
14
23
|
aria-checked={checked}
|
|
24
|
+
aria-label={ariaLabel}
|
|
15
25
|
disabled={disabled}
|
|
16
26
|
onClick={() => onCheckedChange(!checked)}
|
|
17
27
|
className={cn(
|
|
18
28
|
'relative inline-flex h-5 w-9 items-center rounded-full transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--editor-accent)] disabled:opacity-50',
|
|
19
|
-
checked ? 'bg-[var(--editor-accent)]' : 'bg-gray-
|
|
29
|
+
checked ? 'bg-[var(--editor-accent)]' : (mode === 'light' ? 'bg-gray-400' : 'bg-[var(--editor-border)]'),
|
|
20
30
|
className,
|
|
21
31
|
)}
|
|
22
32
|
>
|
package/src/ui/textarea.tsx
CHANGED
|
@@ -6,7 +6,7 @@ export function Textarea({ className, ...props }: TextareaProps) {
|
|
|
6
6
|
return (
|
|
7
7
|
<textarea
|
|
8
8
|
className={cn(
|
|
9
|
-
'flex w-full rounded-md border border-
|
|
9
|
+
'flex w-full rounded-md border border-[var(--editor-border)] bg-[var(--editor-surface)] px-3 py-2 text-sm text-[var(--editor-text)] placeholder:text-[var(--editor-text)]/60 focus:outline-none focus:ring-2 focus:ring-[var(--editor-accent)] disabled:opacity-50 resize-none',
|
|
10
10
|
className,
|
|
11
11
|
)}
|
|
12
12
|
{...props}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { useCallback, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `useState` that survives a reload, backed by `localStorage`.
|
|
5
|
+
*
|
|
6
|
+
* Every access is wrapped: `localStorage` throws rather than returning null in
|
|
7
|
+
* Safari private mode and under some embedded/iframe policies, and a layout
|
|
8
|
+
* preference is never worth taking the editor down for. A read that fails, or
|
|
9
|
+
* that finds a value the current build can no longer parse (an older key
|
|
10
|
+
* format, a hand-edited entry), falls back to `initial` — so a bad stored value
|
|
11
|
+
* degrades to the default rather than a crash loop the user can't escape
|
|
12
|
+
* without clearing site data.
|
|
13
|
+
*
|
|
14
|
+
* The stored value is read ONCE, in the lazy `useState` initializer. This hook
|
|
15
|
+
* deliberately does not subscribe to `storage` events: two editor tabs open on
|
|
16
|
+
* the same project should each keep their own panel sizes, not fight over them.
|
|
17
|
+
*/
|
|
18
|
+
export function usePersistentState<T>(
|
|
19
|
+
key: string,
|
|
20
|
+
initial: T,
|
|
21
|
+
/** Guards what comes back out of storage. Return `null` to reject a stored
|
|
22
|
+
* value and fall back to `initial` — the parse boundary is where a stale or
|
|
23
|
+
* hand-edited entry gets caught. */
|
|
24
|
+
revive: (raw: unknown) => T | null,
|
|
25
|
+
): [T, (next: T, opts?: { persist?: boolean }) => void] {
|
|
26
|
+
const [value, setValue] = useState<T>(() => {
|
|
27
|
+
try {
|
|
28
|
+
const raw = window.localStorage.getItem(key)
|
|
29
|
+
if (raw === null) return initial
|
|
30
|
+
const parsed = revive(JSON.parse(raw))
|
|
31
|
+
return parsed === null ? initial : parsed
|
|
32
|
+
} catch {
|
|
33
|
+
return initial
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
/** `persist: false` updates the value without touching storage — for the
|
|
38
|
+
* intermediate frames of a drag, which would otherwise write to
|
|
39
|
+
* `localStorage` on every mousemove. Commit once when the gesture ends. */
|
|
40
|
+
const set = useCallback((next: T, opts?: { persist?: boolean }) => {
|
|
41
|
+
setValue(next)
|
|
42
|
+
if (opts?.persist === false) return
|
|
43
|
+
try {
|
|
44
|
+
window.localStorage.setItem(key, JSON.stringify(next))
|
|
45
|
+
} catch {
|
|
46
|
+
// Quota, private mode, or a blocked storage partition — the preference
|
|
47
|
+
// just doesn't persist. Not worth surfacing.
|
|
48
|
+
}
|
|
49
|
+
}, [key])
|
|
50
|
+
|
|
51
|
+
return [value, set]
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** `revive` for a finite number inside `[min, max]`. Rejects anything else,
|
|
55
|
+
* including a stored height from a window size that no longer exists. */
|
|
56
|
+
export function reviveNumberInRange(min: number, max: number) {
|
|
57
|
+
return (raw: unknown): number | null =>
|
|
58
|
+
typeof raw === 'number' && Number.isFinite(raw) && raw >= min && raw <= max ? raw : null
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** `revive` for a plain boolean. */
|
|
62
|
+
export const reviveBoolean = (raw: unknown): boolean | null =>
|
|
63
|
+
typeof raw === 'boolean' ? raw : null
|