@r2digisolutions/components 0.2.0 → 0.3.0
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/dist/components/atoms/Playhead/Playhead.svelte +44 -5
- package/dist/components/atoms/Playhead/Playhead.svelte.d.ts +8 -0
- package/dist/components/atoms/TimeRuler/TimeRuler.svelte +75 -16
- package/dist/components/atoms/TimeRuler/TimeRuler.svelte.d.ts +7 -0
- package/dist/components/atoms/TimelineGrid/TimelineGrid.stories.js +8 -0
- package/dist/components/atoms/TimelineGrid/TimelineGrid.svelte +39 -0
- package/dist/components/atoms/TimelineGrid/TimelineGrid.svelte.d.ts +11 -0
- package/dist/components/atoms/TimelineGrid/TimelineGridStory.svelte +7 -0
- package/dist/components/atoms/TimelineGrid/TimelineGridStory.svelte.d.ts +18 -0
- package/dist/components/molecules/ColorPicker/ColorPicker.svelte +45 -4
- package/dist/components/molecules/ColorPicker/ColorPicker.svelte.d.ts +2 -0
- package/dist/components/molecules/ContextMenu/ContextMenu.svelte +38 -11
- package/dist/components/molecules/MediaAssetBrowser/MediaAssetBrowser.svelte +17 -2
- package/dist/components/molecules/MediaAssetBrowser/MediaAssetBrowser.svelte.d.ts +5 -0
- package/dist/components/molecules/MediaTimeline/MediaTimeline.svelte +362 -52
- package/dist/components/molecules/MediaTimeline/MediaTimeline.svelte.d.ts +22 -2
- package/dist/components/molecules/PdfViewer/PdfViewer.svelte.d.ts +1 -1
- package/dist/components/molecules/ProgramMonitor/ProgramMonitor.svelte +751 -29
- package/dist/components/molecules/ProgramMonitor/ProgramMonitor.svelte.d.ts +14 -1
- package/dist/components/molecules/SplitPane/SplitPane.svelte +2 -2
- package/dist/components/molecules/TimelineClip/TimelineClip.svelte +95 -18
- package/dist/components/molecules/TimelineClip/TimelineClip.svelte.d.ts +3 -0
- package/dist/components/molecules/TimelineTrack/TimelineTrack.svelte +82 -12
- package/dist/components/molecules/TimelineTrack/TimelineTrack.svelte.d.ts +13 -2
- package/dist/components/molecules/TransportControls/TransportControls.svelte +84 -2
- package/dist/components/molecules/TransportControls/TransportControls.svelte.d.ts +13 -1
- package/dist/components/molecules/TransportControls/TransportControlsStory.svelte +33 -11
- package/dist/components/molecules/VideoAnimationsPanel/VideoAnimationsPanel.stories.js +8 -0
- package/dist/components/molecules/VideoAnimationsPanel/VideoAnimationsPanel.svelte +213 -0
- package/dist/components/molecules/VideoAnimationsPanel/VideoAnimationsPanel.svelte.d.ts +22 -0
- package/dist/components/molecules/VideoAnimationsPanel/VideoAnimationsPanelStory.svelte +7 -0
- package/dist/components/molecules/VideoAnimationsPanel/VideoAnimationsPanelStory.svelte.d.ts +18 -0
- package/dist/components/molecules/VideoComponentsPanel/VideoComponentsPanel.stories.js +8 -0
- package/dist/components/molecules/VideoComponentsPanel/VideoComponentsPanel.svelte +524 -0
- package/dist/components/molecules/VideoComponentsPanel/VideoComponentsPanel.svelte.d.ts +32 -0
- package/dist/components/molecules/VideoComponentsPanel/VideoComponentsPanelStory.svelte +7 -0
- package/dist/components/molecules/VideoComponentsPanel/VideoComponentsPanelStory.svelte.d.ts +18 -0
- package/dist/components/molecules/VideoTrackHeader/VideoTrackHeader.svelte +85 -32
- package/dist/components/molecules/VideoTrackHeader/VideoTrackHeader.svelte.d.ts +6 -0
- package/dist/components/molecules/VideoTransitionsPanel/VideoTransitionsPanel.stories.js +8 -0
- package/dist/components/molecules/VideoTransitionsPanel/VideoTransitionsPanel.svelte +113 -0
- package/dist/components/molecules/VideoTransitionsPanel/VideoTransitionsPanel.svelte.d.ts +18 -0
- package/dist/components/molecules/VideoTransitionsPanel/VideoTransitionsPanelStory.svelte +7 -0
- package/dist/components/molecules/VideoTransitionsPanel/VideoTransitionsPanelStory.svelte.d.ts +18 -0
- package/dist/components/organisms/AudioEditor/AudioEditor.svelte +52 -1
- package/dist/components/organisms/EditorShell/EditorShell.svelte +102 -92
- package/dist/components/organisms/FileUploader/FileUploader.stories.d.ts +10 -0
- package/dist/components/organisms/FileUploader/FileUploader.stories.js +21 -0
- package/dist/components/organisms/FileUploader/FileUploader.svelte +61 -21
- package/dist/components/organisms/FileUploader/FileUploader.svelte.d.ts +9 -0
- package/dist/components/organisms/FileUploader/FileUploaderStory.svelte +7 -1
- package/dist/components/organisms/FileUploader/FileUploaderStory.svelte.d.ts +3 -0
- package/dist/components/organisms/VideoEditor/VideoEditor.svelte +1457 -78
- package/dist/components/organisms/VideoEditor/VideoEditor.svelte.d.ts +2 -1
- package/dist/components/organisms/VideoEditor/VideoEditorStory.svelte +3 -2
- package/dist/index.d.ts +11 -3
- package/dist/index.js +6 -2
- package/dist/utils/mediaTime.d.ts +14 -4
- package/dist/utils/mediaTime.js +27 -8
- package/dist/utils/mediaTracks.d.ts +154 -0
- package/dist/utils/mediaTracks.js +697 -1
- package/dist/utils/videoSequence.js +35 -4
- package/package.json +1 -1
|
@@ -3,6 +3,14 @@
|
|
|
3
3
|
/** Position in px from left of timeline content */
|
|
4
4
|
x?: number;
|
|
5
5
|
height?: number | string;
|
|
6
|
+
/**
|
|
7
|
+
* `full` — head + needle (spans ruler+tracks)
|
|
8
|
+
* `head` — caret on the ruler only
|
|
9
|
+
* `line` — needle through tracks only
|
|
10
|
+
*/
|
|
11
|
+
variant?: 'full' | 'head' | 'line';
|
|
12
|
+
/** Height of the head area that sits on the ruler (px) */
|
|
13
|
+
headSize?: number;
|
|
6
14
|
dragging?: boolean;
|
|
7
15
|
class?: string;
|
|
8
16
|
ondragstart?: (e: PointerEvent) => void;
|
|
@@ -13,6 +21,8 @@
|
|
|
13
21
|
let {
|
|
14
22
|
x = 0,
|
|
15
23
|
height = '100%',
|
|
24
|
+
variant = 'full',
|
|
25
|
+
headSize = 28,
|
|
16
26
|
dragging = false,
|
|
17
27
|
class: className = '',
|
|
18
28
|
ondragstart,
|
|
@@ -20,7 +30,12 @@
|
|
|
20
30
|
ondragend
|
|
21
31
|
}: PlayheadProps = $props();
|
|
22
32
|
|
|
33
|
+
const showHead = $derived(variant === 'full' || variant === 'head');
|
|
34
|
+
const showLine = $derived(variant === 'full' || variant === 'line');
|
|
35
|
+
|
|
23
36
|
function onPointerDown(e: PointerEvent) {
|
|
37
|
+
e.preventDefault();
|
|
38
|
+
e.stopPropagation();
|
|
24
39
|
(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
|
|
25
40
|
ondragstart?.(e);
|
|
26
41
|
}
|
|
@@ -42,7 +57,8 @@
|
|
|
42
57
|
aria-valuenow={Math.round(x)}
|
|
43
58
|
tabindex="0"
|
|
44
59
|
class={[
|
|
45
|
-
'absolute top-0 z-
|
|
60
|
+
'absolute top-0 z-40 -translate-x-1/2 touch-none select-none',
|
|
61
|
+
variant === 'line' ? 'w-3' : 'w-4',
|
|
46
62
|
dragging ? 'cursor-grabbing' : 'cursor-ew-resize',
|
|
47
63
|
className
|
|
48
64
|
]}
|
|
@@ -52,8 +68,31 @@
|
|
|
52
68
|
onpointermove={onPointerMove}
|
|
53
69
|
onpointerup={onPointerUp}
|
|
54
70
|
>
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
71
|
+
{#if showHead}
|
|
72
|
+
<div
|
|
73
|
+
class="absolute left-1/2 top-0 z-10 flex -translate-x-1/2 flex-col items-center justify-center"
|
|
74
|
+
style:height={`${headSize}px`}
|
|
75
|
+
>
|
|
76
|
+
<svg
|
|
77
|
+
viewBox="0 0 14 18"
|
|
78
|
+
class="h-[16px] w-[12px] drop-shadow-sm"
|
|
79
|
+
aria-hidden="true"
|
|
80
|
+
>
|
|
81
|
+
<path
|
|
82
|
+
d="M1 1.5h12v8.2L7 16.5 1 9.7V1.5z"
|
|
83
|
+
fill="#ef4444"
|
|
84
|
+
stroke="#b91c1c"
|
|
85
|
+
stroke-width="1"
|
|
86
|
+
stroke-linejoin="round"
|
|
87
|
+
/>
|
|
88
|
+
</svg>
|
|
89
|
+
</div>
|
|
90
|
+
{/if}
|
|
91
|
+
{#if showLine}
|
|
92
|
+
<div
|
|
93
|
+
class="absolute left-1/2 w-0.5 -translate-x-1/2 bg-red-500"
|
|
94
|
+
style:top={variant === 'full' ? `${headSize - 2}px` : '0'}
|
|
95
|
+
style:bottom="0"
|
|
96
|
+
></div>
|
|
97
|
+
{/if}
|
|
59
98
|
</div>
|
|
@@ -2,6 +2,14 @@ interface PlayheadProps {
|
|
|
2
2
|
/** Position in px from left of timeline content */
|
|
3
3
|
x?: number;
|
|
4
4
|
height?: number | string;
|
|
5
|
+
/**
|
|
6
|
+
* `full` — head + needle (spans ruler+tracks)
|
|
7
|
+
* `head` — caret on the ruler only
|
|
8
|
+
* `line` — needle through tracks only
|
|
9
|
+
*/
|
|
10
|
+
variant?: 'full' | 'head' | 'line';
|
|
11
|
+
/** Height of the head area that sits on the ruler (px) */
|
|
12
|
+
headSize?: number;
|
|
5
13
|
dragging?: boolean;
|
|
6
14
|
class?: string;
|
|
7
15
|
ondragstart?: (e: PointerEvent) => void;
|
|
@@ -1,57 +1,116 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { formatTimecode, ticksForDuration } from '../../../utils/mediaTime.js';
|
|
2
|
+
import { formatTimecode, ticksForDuration, type TimeTick } from '../../../utils/mediaTime.js';
|
|
3
3
|
|
|
4
4
|
interface TimeRulerProps {
|
|
5
5
|
durationMs?: number;
|
|
6
6
|
pxPerMs?: number;
|
|
7
7
|
height?: number;
|
|
8
|
+
/** Precomputed ticks; defaults to ticksForDuration. */
|
|
9
|
+
ticks?: TimeTick[];
|
|
8
10
|
class?: string;
|
|
9
11
|
onclick?: (ms: number, e: MouseEvent) => void;
|
|
12
|
+
/** Scrub while dragging on the ruler */
|
|
13
|
+
onscrub?: (ms: number, e: PointerEvent) => void;
|
|
14
|
+
onscrubstart?: (ms: number, e: PointerEvent) => void;
|
|
15
|
+
onscrubend?: (ms: number, e: PointerEvent) => void;
|
|
10
16
|
}
|
|
11
17
|
|
|
12
18
|
let {
|
|
13
19
|
durationMs = 30_000,
|
|
14
20
|
pxPerMs = 0.05,
|
|
15
21
|
height = 28,
|
|
22
|
+
ticks: ticksProp,
|
|
16
23
|
class: className = '',
|
|
17
|
-
onclick
|
|
24
|
+
onclick,
|
|
25
|
+
onscrub,
|
|
26
|
+
onscrubstart,
|
|
27
|
+
onscrubend
|
|
18
28
|
}: TimeRulerProps = $props();
|
|
19
29
|
|
|
20
|
-
const ticks = $derived(ticksForDuration(durationMs, pxPerMs));
|
|
30
|
+
const ticks = $derived(ticksProp ?? ticksForDuration(durationMs, pxPerMs));
|
|
21
31
|
const width = $derived(Math.max(1, durationMs * pxPerMs));
|
|
22
32
|
|
|
33
|
+
let scrubbing = $state(false);
|
|
34
|
+
|
|
35
|
+
function msFromClientX(el: HTMLElement, clientX: number): number {
|
|
36
|
+
const rect = el.getBoundingClientRect();
|
|
37
|
+
const x = clientX - rect.left;
|
|
38
|
+
return Math.max(0, Math.min(durationMs, x / pxPerMs));
|
|
39
|
+
}
|
|
40
|
+
|
|
23
41
|
function handleClick(e: MouseEvent) {
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
onclick?.(
|
|
42
|
+
if (scrubbing) return;
|
|
43
|
+
const ms = msFromClientX(e.currentTarget as HTMLElement, e.clientX);
|
|
44
|
+
onclick?.(ms, e);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function onPointerDown(e: PointerEvent) {
|
|
48
|
+
if (e.button !== 0) return;
|
|
49
|
+
e.preventDefault();
|
|
50
|
+
const el = e.currentTarget as HTMLElement;
|
|
51
|
+
el.setPointerCapture(e.pointerId);
|
|
52
|
+
scrubbing = true;
|
|
53
|
+
const ms = msFromClientX(el, e.clientX);
|
|
54
|
+
onscrubstart?.(ms, e);
|
|
55
|
+
onscrub?.(ms, e);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function onPointerMove(e: PointerEvent) {
|
|
59
|
+
const el = e.currentTarget as HTMLElement;
|
|
60
|
+
if (!el.hasPointerCapture(e.pointerId)) return;
|
|
61
|
+
const ms = msFromClientX(el, e.clientX);
|
|
62
|
+
onscrub?.(ms, e);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function onPointerUp(e: PointerEvent) {
|
|
66
|
+
const el = e.currentTarget as HTMLElement;
|
|
67
|
+
if (!el.hasPointerCapture(e.pointerId)) return;
|
|
68
|
+
el.releasePointerCapture(e.pointerId);
|
|
69
|
+
const ms = msFromClientX(el, e.clientX);
|
|
70
|
+
onscrubend?.(ms, e);
|
|
71
|
+
scrubbing = false;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function tickHeight(level: TimeTick['level']): string {
|
|
75
|
+
if (level === 'major') return 'h-3.5';
|
|
76
|
+
if (level === 'mid') return 'h-2.5';
|
|
77
|
+
return 'h-1.5';
|
|
27
78
|
}
|
|
28
79
|
</script>
|
|
29
80
|
|
|
81
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
30
82
|
<div
|
|
31
83
|
role="presentation"
|
|
32
84
|
class={[
|
|
33
|
-
'relative shrink-0 select-none border-b border-border bg-surface text-[10px] text-muted',
|
|
85
|
+
'relative shrink-0 cursor-ew-resize select-none border-b border-border bg-surface text-[10px] text-muted',
|
|
86
|
+
scrubbing && 'cursor-grabbing',
|
|
34
87
|
className
|
|
35
88
|
]}
|
|
36
89
|
style:height={`${height}px`}
|
|
37
90
|
style:width={`${width}px`}
|
|
38
91
|
onclick={handleClick}
|
|
92
|
+
onpointerdown={onPointerDown}
|
|
93
|
+
onpointermove={onPointerMove}
|
|
94
|
+
onpointerup={onPointerUp}
|
|
95
|
+
onpointercancel={onPointerUp}
|
|
39
96
|
>
|
|
40
97
|
{#each ticks as tick (tick.ms)}
|
|
41
98
|
<div
|
|
42
|
-
class="absolute top-0 flex h-full flex-col justify-end"
|
|
99
|
+
class="pointer-events-none absolute top-0 flex h-full flex-col justify-end"
|
|
43
100
|
style:left={`${tick.ms * pxPerMs}px`}
|
|
44
101
|
>
|
|
45
|
-
|
|
102
|
+
{#if tick.level === 'major'}
|
|
103
|
+
<span class="mb-0.5 -translate-x-1/2 whitespace-nowrap pl-1 text-secondary">
|
|
104
|
+
{formatTimecode(tick.ms)}
|
|
105
|
+
</span>
|
|
106
|
+
{/if}
|
|
107
|
+
<div
|
|
46
108
|
class={[
|
|
47
|
-
'
|
|
48
|
-
tick.
|
|
109
|
+
'w-px bg-border-strong',
|
|
110
|
+
tickHeight(tick.level),
|
|
111
|
+
tick.level === 'minor' && 'opacity-40',
|
|
112
|
+
tick.level === 'mid' && 'opacity-70'
|
|
49
113
|
]}
|
|
50
|
-
>
|
|
51
|
-
{formatTimecode(tick.ms)}
|
|
52
|
-
</span>
|
|
53
|
-
<div
|
|
54
|
-
class={['w-px bg-border-strong', tick.major ? 'h-3' : 'h-2 opacity-60']}
|
|
55
114
|
></div>
|
|
56
115
|
</div>
|
|
57
116
|
{/each}
|
|
@@ -1,9 +1,16 @@
|
|
|
1
|
+
import { type TimeTick } from '../../../utils/mediaTime.js';
|
|
1
2
|
interface TimeRulerProps {
|
|
2
3
|
durationMs?: number;
|
|
3
4
|
pxPerMs?: number;
|
|
4
5
|
height?: number;
|
|
6
|
+
/** Precomputed ticks; defaults to ticksForDuration. */
|
|
7
|
+
ticks?: TimeTick[];
|
|
5
8
|
class?: string;
|
|
6
9
|
onclick?: (ms: number, e: MouseEvent) => void;
|
|
10
|
+
/** Scrub while dragging on the ruler */
|
|
11
|
+
onscrub?: (ms: number, e: PointerEvent) => void;
|
|
12
|
+
onscrubstart?: (ms: number, e: PointerEvent) => void;
|
|
13
|
+
onscrubend?: (ms: number, e: PointerEvent) => void;
|
|
7
14
|
}
|
|
8
15
|
declare const TimeRuler: import("svelte").Component<TimeRulerProps, {}, "">;
|
|
9
16
|
type TimeRuler = ReturnType<typeof TimeRuler>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { ticksForDuration, type TimeTick } from '../../../utils/mediaTime.js';
|
|
3
|
+
|
|
4
|
+
interface TimelineGridProps {
|
|
5
|
+
durationMs?: number;
|
|
6
|
+
pxPerMs?: number;
|
|
7
|
+
/** Precomputed ticks; defaults to ticksForDuration. */
|
|
8
|
+
ticks?: TimeTick[];
|
|
9
|
+
class?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let {
|
|
13
|
+
durationMs = 30_000,
|
|
14
|
+
pxPerMs = 0.05,
|
|
15
|
+
ticks: ticksProp,
|
|
16
|
+
class: className = ''
|
|
17
|
+
}: TimelineGridProps = $props();
|
|
18
|
+
|
|
19
|
+
const ticks = $derived(ticksProp ?? ticksForDuration(durationMs, pxPerMs));
|
|
20
|
+
const width = $derived(Math.max(1, durationMs * pxPerMs));
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<div
|
|
24
|
+
aria-hidden="true"
|
|
25
|
+
class={['pointer-events-none absolute inset-y-0 left-0 z-0', className]}
|
|
26
|
+
style:width={`${width}px`}
|
|
27
|
+
>
|
|
28
|
+
{#each ticks as tick (tick.ms)}
|
|
29
|
+
<div
|
|
30
|
+
class={[
|
|
31
|
+
'absolute inset-y-0 w-px',
|
|
32
|
+
tick.level === 'major' && 'bg-border-strong/50',
|
|
33
|
+
tick.level === 'mid' && 'bg-border/40',
|
|
34
|
+
tick.level === 'minor' && 'bg-border/20'
|
|
35
|
+
]}
|
|
36
|
+
style:left={`${tick.ms * pxPerMs}px`}
|
|
37
|
+
></div>
|
|
38
|
+
{/each}
|
|
39
|
+
</div>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type TimeTick } from '../../../utils/mediaTime.js';
|
|
2
|
+
interface TimelineGridProps {
|
|
3
|
+
durationMs?: number;
|
|
4
|
+
pxPerMs?: number;
|
|
5
|
+
/** Precomputed ticks; defaults to ticksForDuration. */
|
|
6
|
+
ticks?: TimeTick[];
|
|
7
|
+
class?: string;
|
|
8
|
+
}
|
|
9
|
+
declare const TimelineGrid: import("svelte").Component<TimelineGridProps, {}, "">;
|
|
10
|
+
type TimelineGrid = ReturnType<typeof TimelineGrid>;
|
|
11
|
+
export default TimelineGrid;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
|
|
2
|
+
new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
|
|
3
|
+
$$bindings?: Bindings;
|
|
4
|
+
} & Exports;
|
|
5
|
+
(internal: unknown, props: {
|
|
6
|
+
$$events?: Events;
|
|
7
|
+
$$slots?: Slots;
|
|
8
|
+
}): Exports & {
|
|
9
|
+
$set?: any;
|
|
10
|
+
$on?: any;
|
|
11
|
+
};
|
|
12
|
+
z_$$bindings?: Bindings;
|
|
13
|
+
}
|
|
14
|
+
declare const TimelineGridStory: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
|
|
15
|
+
[evt: string]: CustomEvent<any>;
|
|
16
|
+
}, {}, {}, string>;
|
|
17
|
+
type TimelineGridStory = InstanceType<typeof TimelineGridStory>;
|
|
18
|
+
export default TimelineGridStory;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
export type ColorFormat = 'hex' | 'rgb' | 'hsl';
|
|
3
|
+
type ColorPickerSize = 'sm' | 'md' | 'lg';
|
|
3
4
|
|
|
4
5
|
interface ColorPickerProps {
|
|
5
6
|
value?: string;
|
|
@@ -11,6 +12,7 @@
|
|
|
11
12
|
format?: ColorFormat;
|
|
12
13
|
/** Show preset swatches. */
|
|
13
14
|
showSwatches?: boolean;
|
|
15
|
+
size?: ColorPickerSize;
|
|
14
16
|
disabled?: boolean;
|
|
15
17
|
open?: boolean;
|
|
16
18
|
class?: string;
|
|
@@ -37,12 +39,37 @@
|
|
|
37
39
|
alpha = false,
|
|
38
40
|
format = 'hex',
|
|
39
41
|
showSwatches = true,
|
|
42
|
+
size = 'md',
|
|
40
43
|
disabled = false,
|
|
41
44
|
open = $bindable(false),
|
|
42
45
|
class: className = '',
|
|
43
46
|
onchange
|
|
44
47
|
}: ColorPickerProps = $props();
|
|
45
48
|
|
|
49
|
+
const triggerSizeClasses: Record<ColorPickerSize, string> = {
|
|
50
|
+
sm: 'h-8 gap-2 rounded-lg px-2',
|
|
51
|
+
md: 'h-10 gap-2.5 rounded-xl px-2.5',
|
|
52
|
+
lg: 'h-12 gap-3 rounded-xl px-3'
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const swatchSizeClasses: Record<ColorPickerSize, string> = {
|
|
56
|
+
sm: 'h-5 w-5 rounded-md',
|
|
57
|
+
md: 'h-7 w-7 rounded-lg',
|
|
58
|
+
lg: 'h-8 w-8 rounded-lg'
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const valueSizeClasses: Record<ColorPickerSize, string> = {
|
|
62
|
+
sm: 'text-sm',
|
|
63
|
+
md: 'text-sm',
|
|
64
|
+
lg: 'text-base'
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const chevronSizeClasses: Record<ColorPickerSize, string> = {
|
|
68
|
+
sm: 'h-3.5 w-3.5',
|
|
69
|
+
md: 'h-4 w-4',
|
|
70
|
+
lg: 'h-4 w-4'
|
|
71
|
+
};
|
|
72
|
+
|
|
46
73
|
let rootEl = $state<HTMLDivElement | null>(null);
|
|
47
74
|
let svEl = $state<HTMLDivElement | null>(null);
|
|
48
75
|
let triggerEl = $state<HTMLButtonElement | null>(null);
|
|
@@ -236,17 +263,31 @@
|
|
|
236
263
|
aria-haspopup="dialog"
|
|
237
264
|
onclick={togglePanel}
|
|
238
265
|
class={[
|
|
239
|
-
'flex
|
|
266
|
+
'flex w-full items-center border border-border bg-surface-elevated text-left transition-colors',
|
|
267
|
+
triggerSizeClasses[size],
|
|
240
268
|
'hover:bg-surface-overlay focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500/20',
|
|
241
269
|
open && 'border-brand-500 ring-2 ring-brand-500/20',
|
|
242
270
|
disabled && 'cursor-not-allowed opacity-60'
|
|
243
271
|
]}
|
|
244
272
|
>
|
|
245
|
-
<span
|
|
273
|
+
<span
|
|
274
|
+
class={[
|
|
275
|
+
'cp-checker relative shrink-0 overflow-hidden border border-border',
|
|
276
|
+
swatchSizeClasses[size]
|
|
277
|
+
]}
|
|
278
|
+
>
|
|
246
279
|
<span class="absolute inset-0" style:background={preview}></span>
|
|
247
280
|
</span>
|
|
248
|
-
<span class=
|
|
249
|
-
|
|
281
|
+
<span class={['min-w-0 flex-1 truncate font-mono text-primary', valueSizeClasses[size]]}>
|
|
282
|
+
{value}
|
|
283
|
+
</span>
|
|
284
|
+
<svg
|
|
285
|
+
class={['shrink-0 text-muted', chevronSizeClasses[size]]}
|
|
286
|
+
viewBox="0 0 24 24"
|
|
287
|
+
fill="none"
|
|
288
|
+
stroke="currentColor"
|
|
289
|
+
stroke-width="2"
|
|
290
|
+
>
|
|
250
291
|
<path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7" />
|
|
251
292
|
</svg>
|
|
252
293
|
</button>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export type ColorFormat = 'hex' | 'rgb' | 'hsl';
|
|
2
|
+
type ColorPickerSize = 'sm' | 'md' | 'lg';
|
|
2
3
|
interface ColorPickerProps {
|
|
3
4
|
value?: string;
|
|
4
5
|
swatches?: string[];
|
|
@@ -9,6 +10,7 @@ interface ColorPickerProps {
|
|
|
9
10
|
format?: ColorFormat;
|
|
10
11
|
/** Show preset swatches. */
|
|
11
12
|
showSwatches?: boolean;
|
|
13
|
+
size?: ColorPickerSize;
|
|
12
14
|
disabled?: boolean;
|
|
13
15
|
open?: boolean;
|
|
14
16
|
class?: string;
|
|
@@ -105,16 +105,33 @@
|
|
|
105
105
|
.filter((i) => i !== -1)
|
|
106
106
|
);
|
|
107
107
|
|
|
108
|
+
function clampMenuPosition(x: number, y: number) {
|
|
109
|
+
if (!menuEl) return;
|
|
110
|
+
const pad = 8;
|
|
111
|
+
const vw = window.innerWidth;
|
|
112
|
+
const vh = window.innerHeight;
|
|
113
|
+
const mw = menuEl.offsetWidth;
|
|
114
|
+
const mh = menuEl.offsetHeight;
|
|
115
|
+
if (mw <= 0 || mh <= 0) {
|
|
116
|
+
menuX = x;
|
|
117
|
+
menuY = y;
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
let nextX = x;
|
|
121
|
+
let nextY = y;
|
|
122
|
+
// Flip if overflowing right / bottom
|
|
123
|
+
if (nextX + mw > vw - pad) nextX = x - mw;
|
|
124
|
+
if (nextY + mh > vh - pad) nextY = y - mh;
|
|
125
|
+
// Always keep fully inside the viewport
|
|
126
|
+
menuX = Math.min(Math.max(pad, nextX), Math.max(pad, vw - mw - pad));
|
|
127
|
+
menuY = Math.min(Math.max(pad, nextY), Math.max(pad, vh - mh - pad));
|
|
128
|
+
}
|
|
129
|
+
|
|
108
130
|
function placeMenu(x: number, y: number) {
|
|
109
131
|
menuX = x;
|
|
110
132
|
menuY = y;
|
|
111
133
|
requestAnimationFrame(() => {
|
|
112
|
-
|
|
113
|
-
const vw = window.innerWidth;
|
|
114
|
-
const vh = window.innerHeight;
|
|
115
|
-
const { offsetWidth: mw, offsetHeight: mh } = menuEl;
|
|
116
|
-
menuX = x + mw > vw - 8 ? Math.max(8, x - mw) : x;
|
|
117
|
-
menuY = y + mh > vh - 8 ? Math.max(8, y - mh) : y;
|
|
134
|
+
clampMenuPosition(x, y);
|
|
118
135
|
});
|
|
119
136
|
}
|
|
120
137
|
|
|
@@ -132,11 +149,15 @@
|
|
|
132
149
|
open = true;
|
|
133
150
|
highlighted = -1;
|
|
134
151
|
query = '';
|
|
135
|
-
|
|
152
|
+
menuX = x;
|
|
153
|
+
menuY = y;
|
|
136
154
|
// Defer past the contextmenu/mouseup that would light-dismiss a popover=auto.
|
|
155
|
+
// Measure after showPopover — closed popovers have display:none (0×0).
|
|
137
156
|
requestAnimationFrame(() => {
|
|
138
157
|
requestAnimationFrame(() => {
|
|
139
|
-
if (open)
|
|
158
|
+
if (!open) return;
|
|
159
|
+
showMenu();
|
|
160
|
+
requestAnimationFrame(() => clampMenuPosition(x, y));
|
|
140
161
|
});
|
|
141
162
|
});
|
|
142
163
|
}
|
|
@@ -180,9 +201,11 @@
|
|
|
180
201
|
|
|
181
202
|
/** Manual popover: close on outside pointer. Keep open on scroll —
|
|
182
203
|
* the menu is position:fixed, and closing on scroll also kills scrolling
|
|
183
|
-
* inside a long/searchable menu.
|
|
204
|
+
* inside a long/searchable menu.
|
|
205
|
+
* Right-click (button 2) is ignored so retargeting another item keeps the menu open. */
|
|
184
206
|
function onDocPointerDown(event: PointerEvent) {
|
|
185
207
|
if (!open) return;
|
|
208
|
+
if (event.button === 2) return;
|
|
186
209
|
const t = event.target;
|
|
187
210
|
if (t instanceof Node && menuEl?.contains(t)) return;
|
|
188
211
|
close();
|
|
@@ -243,10 +266,14 @@
|
|
|
243
266
|
}
|
|
244
267
|
const x = anchor.x;
|
|
245
268
|
const y = anchor.y;
|
|
246
|
-
|
|
269
|
+
highlighted = -1;
|
|
270
|
+
menuX = x;
|
|
271
|
+
menuY = y;
|
|
247
272
|
requestAnimationFrame(() => {
|
|
248
273
|
requestAnimationFrame(() => {
|
|
249
|
-
if (open)
|
|
274
|
+
if (!open) return;
|
|
275
|
+
showMenu();
|
|
276
|
+
requestAnimationFrame(() => clampMenuPosition(x, y));
|
|
250
277
|
});
|
|
251
278
|
});
|
|
252
279
|
});
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import {
|
|
6
6
|
createMediaAsset,
|
|
7
7
|
kindFromFile,
|
|
8
|
+
probeMediaSrc,
|
|
8
9
|
type MediaAsset,
|
|
9
10
|
type MediaAssetKind
|
|
10
11
|
} from '../../../utils/mediaTracks.js';
|
|
@@ -15,6 +16,11 @@
|
|
|
15
16
|
accept?: string;
|
|
16
17
|
helperText?: string;
|
|
17
18
|
allowText?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Upload dropzone layout. Prefer `vertical` in narrow sidebars.
|
|
21
|
+
* @see FileUploader `layout`
|
|
22
|
+
*/
|
|
23
|
+
uploadLayout?: 'horizontal' | 'vertical' | 'compact';
|
|
18
24
|
class?: string;
|
|
19
25
|
onassetschange?: (assets: MediaAsset[]) => void;
|
|
20
26
|
onselect?: (id: string) => void;
|
|
@@ -27,6 +33,7 @@
|
|
|
27
33
|
accept = 'image/*,video/*,audio/*',
|
|
28
34
|
helperText = 'Images, video or audio',
|
|
29
35
|
allowText = false,
|
|
36
|
+
uploadLayout = 'vertical',
|
|
30
37
|
class: className = '',
|
|
31
38
|
onassetschange,
|
|
32
39
|
onselect,
|
|
@@ -38,15 +45,22 @@
|
|
|
38
45
|
onassetschange?.(next);
|
|
39
46
|
}
|
|
40
47
|
|
|
41
|
-
function
|
|
48
|
+
function probeMedia(src: string, kind: MediaAssetKind): Promise<Partial<MediaAsset>> {
|
|
49
|
+
return probeMediaSrc(src, kind);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function handleFiles(files: File[]) {
|
|
42
53
|
const created: MediaAsset[] = [];
|
|
43
54
|
for (const file of files) {
|
|
44
55
|
const kind = kindFromFile(file);
|
|
45
56
|
if (kind === 'text') continue;
|
|
57
|
+
const src = URL.createObjectURL(file);
|
|
58
|
+
const meta = await probeMedia(src, kind);
|
|
46
59
|
const asset = createMediaAsset({
|
|
47
60
|
kind,
|
|
48
61
|
name: file.name,
|
|
49
|
-
src
|
|
62
|
+
src,
|
|
63
|
+
...meta
|
|
50
64
|
});
|
|
51
65
|
created.push(asset);
|
|
52
66
|
}
|
|
@@ -65,6 +79,7 @@
|
|
|
65
79
|
label="Upload"
|
|
66
80
|
{accept}
|
|
67
81
|
{helperText}
|
|
82
|
+
layout={uploadLayout}
|
|
68
83
|
variant="multiple"
|
|
69
84
|
view="grid"
|
|
70
85
|
showViewToggle={false}
|
|
@@ -5,6 +5,11 @@ interface MediaAssetBrowserProps {
|
|
|
5
5
|
accept?: string;
|
|
6
6
|
helperText?: string;
|
|
7
7
|
allowText?: boolean;
|
|
8
|
+
/**
|
|
9
|
+
* Upload dropzone layout. Prefer `vertical` in narrow sidebars.
|
|
10
|
+
* @see FileUploader `layout`
|
|
11
|
+
*/
|
|
12
|
+
uploadLayout?: 'horizontal' | 'vertical' | 'compact';
|
|
8
13
|
class?: string;
|
|
9
14
|
onassetschange?: (assets: MediaAsset[]) => void;
|
|
10
15
|
onselect?: (id: string) => void;
|