@r2digisolutions/components 0.2.0 → 0.3.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.
Files changed (65) hide show
  1. package/README.md +10 -0
  2. package/dist/components/atoms/Playhead/Playhead.svelte +44 -5
  3. package/dist/components/atoms/Playhead/Playhead.svelte.d.ts +8 -0
  4. package/dist/components/atoms/TimeRuler/TimeRuler.svelte +75 -16
  5. package/dist/components/atoms/TimeRuler/TimeRuler.svelte.d.ts +7 -0
  6. package/dist/components/atoms/TimelineGrid/TimelineGrid.stories.js +8 -0
  7. package/dist/components/atoms/TimelineGrid/TimelineGrid.svelte +39 -0
  8. package/dist/components/atoms/TimelineGrid/TimelineGrid.svelte.d.ts +11 -0
  9. package/dist/components/atoms/TimelineGrid/TimelineGridStory.svelte +7 -0
  10. package/dist/components/atoms/TimelineGrid/TimelineGridStory.svelte.d.ts +18 -0
  11. package/dist/components/molecules/ColorPicker/ColorPicker.svelte +45 -4
  12. package/dist/components/molecules/ColorPicker/ColorPicker.svelte.d.ts +2 -0
  13. package/dist/components/molecules/ContextMenu/ContextMenu.svelte +38 -11
  14. package/dist/components/molecules/MediaAssetBrowser/MediaAssetBrowser.svelte +17 -2
  15. package/dist/components/molecules/MediaAssetBrowser/MediaAssetBrowser.svelte.d.ts +5 -0
  16. package/dist/components/molecules/MediaTimeline/MediaTimeline.svelte +362 -52
  17. package/dist/components/molecules/MediaTimeline/MediaTimeline.svelte.d.ts +22 -2
  18. package/dist/components/molecules/PdfViewer/PdfViewer.svelte.d.ts +1 -1
  19. package/dist/components/molecules/ProgramMonitor/ProgramMonitor.svelte +751 -29
  20. package/dist/components/molecules/ProgramMonitor/ProgramMonitor.svelte.d.ts +14 -1
  21. package/dist/components/molecules/SplitPane/SplitPane.svelte +2 -2
  22. package/dist/components/molecules/TimelineClip/TimelineClip.svelte +95 -18
  23. package/dist/components/molecules/TimelineClip/TimelineClip.svelte.d.ts +3 -0
  24. package/dist/components/molecules/TimelineTrack/TimelineTrack.svelte +82 -12
  25. package/dist/components/molecules/TimelineTrack/TimelineTrack.svelte.d.ts +13 -2
  26. package/dist/components/molecules/TransportControls/TransportControls.svelte +84 -2
  27. package/dist/components/molecules/TransportControls/TransportControls.svelte.d.ts +13 -1
  28. package/dist/components/molecules/TransportControls/TransportControlsStory.svelte +33 -11
  29. package/dist/components/molecules/VideoAnimationsPanel/VideoAnimationsPanel.stories.js +8 -0
  30. package/dist/components/molecules/VideoAnimationsPanel/VideoAnimationsPanel.svelte +213 -0
  31. package/dist/components/molecules/VideoAnimationsPanel/VideoAnimationsPanel.svelte.d.ts +22 -0
  32. package/dist/components/molecules/VideoAnimationsPanel/VideoAnimationsPanelStory.svelte +7 -0
  33. package/dist/components/molecules/VideoAnimationsPanel/VideoAnimationsPanelStory.svelte.d.ts +18 -0
  34. package/dist/components/molecules/VideoComponentsPanel/VideoComponentsPanel.stories.js +8 -0
  35. package/dist/components/molecules/VideoComponentsPanel/VideoComponentsPanel.svelte +524 -0
  36. package/dist/components/molecules/VideoComponentsPanel/VideoComponentsPanel.svelte.d.ts +32 -0
  37. package/dist/components/molecules/VideoComponentsPanel/VideoComponentsPanelStory.svelte +7 -0
  38. package/dist/components/molecules/VideoComponentsPanel/VideoComponentsPanelStory.svelte.d.ts +18 -0
  39. package/dist/components/molecules/VideoTrackHeader/VideoTrackHeader.svelte +85 -32
  40. package/dist/components/molecules/VideoTrackHeader/VideoTrackHeader.svelte.d.ts +6 -0
  41. package/dist/components/molecules/VideoTransitionsPanel/VideoTransitionsPanel.stories.js +8 -0
  42. package/dist/components/molecules/VideoTransitionsPanel/VideoTransitionsPanel.svelte +113 -0
  43. package/dist/components/molecules/VideoTransitionsPanel/VideoTransitionsPanel.svelte.d.ts +18 -0
  44. package/dist/components/molecules/VideoTransitionsPanel/VideoTransitionsPanelStory.svelte +7 -0
  45. package/dist/components/molecules/VideoTransitionsPanel/VideoTransitionsPanelStory.svelte.d.ts +18 -0
  46. package/dist/components/organisms/AudioEditor/AudioEditor.svelte +52 -1
  47. package/dist/components/organisms/EditorShell/EditorShell.svelte +102 -92
  48. package/dist/components/organisms/FileUploader/FileUploader.stories.d.ts +10 -0
  49. package/dist/components/organisms/FileUploader/FileUploader.stories.js +21 -0
  50. package/dist/components/organisms/FileUploader/FileUploader.svelte +61 -21
  51. package/dist/components/organisms/FileUploader/FileUploader.svelte.d.ts +9 -0
  52. package/dist/components/organisms/FileUploader/FileUploaderStory.svelte +7 -1
  53. package/dist/components/organisms/FileUploader/FileUploaderStory.svelte.d.ts +3 -0
  54. package/dist/components/organisms/VideoEditor/VideoEditor.svelte +1457 -78
  55. package/dist/components/organisms/VideoEditor/VideoEditor.svelte.d.ts +2 -1
  56. package/dist/components/organisms/VideoEditor/VideoEditorStory.svelte +3 -2
  57. package/dist/index.d.ts +11 -3
  58. package/dist/index.js +6 -2
  59. package/dist/styles.css +237 -0
  60. package/dist/utils/mediaTime.d.ts +14 -4
  61. package/dist/utils/mediaTime.js +27 -8
  62. package/dist/utils/mediaTracks.d.ts +154 -0
  63. package/dist/utils/mediaTracks.js +697 -1
  64. package/dist/utils/videoSequence.js +35 -4
  65. package/package.json +4 -2
package/README.md CHANGED
@@ -75,6 +75,16 @@ Mismo flujo que usan muchas libs con `bumpp` / `release-it` (tag → CI → npm
75
75
  pnpm add @r2digisolutions/components
76
76
  ```
77
77
 
78
+ Los componentes usan clases de Tailwind 4. En la app consumidora basta con importar el CSS del design system (incluye `@theme`, variables y el `@source` del paquete):
79
+
80
+ ```css
81
+ /* app.css / layout.css del consumidor */
82
+ @import 'tailwindcss';
83
+ @import '@r2digisolutions/components/styles.css';
84
+ ```
85
+
86
+ Ese import registra tokens (`border-border`, `bg-surface`, …) y hace que Tailwind escanee los `.svelte` del paquete. Sin él, las utilidades pueden faltar o `--border` no estar definido.
87
+
78
88
  ## Licencia
79
89
 
80
90
  MIT
@@ -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-20 w-0 touch-none select-none',
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
- <div
56
- class="absolute left-1/2 top-0 h-3 w-3 -translate-x-1/2 rounded-sm bg-red-500 shadow-sm"
57
- ></div>
58
- <div class="absolute left-1/2 top-2 h-[calc(100%-0.5rem)] w-px -translate-x-1/2 bg-red-500"></div>
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
- const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
25
- const x = e.clientX - rect.left;
26
- onclick?.(x / pxPerMs, e);
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
- <span
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
- 'mb-0.5 -translate-x-1/2 whitespace-nowrap pl-1',
48
- tick.major ? 'text-secondary' : 'text-muted/70'
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,8 @@
1
+ import TimelineGridStory from './TimelineGridStory.svelte';
2
+ const meta = {
3
+ title: 'Atoms/TimelineGrid',
4
+ component: TimelineGridStory,
5
+ tags: ['autodocs']
6
+ };
7
+ export default meta;
8
+ export const Default = {};
@@ -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,7 @@
1
+ <script lang="ts">
2
+ import TimelineGrid from './TimelineGrid.svelte';
3
+ </script>
4
+
5
+ <div class="relative h-40 overflow-hidden rounded-lg border border-border bg-surface">
6
+ <TimelineGrid durationMs={30000} pxPerMs={0.1} class="absolute inset-0" />
7
+ </div>
@@ -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 h-10 w-full items-center gap-2.5 rounded-xl border border-border bg-surface-elevated px-2.5 text-left transition-colors',
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 class="cp-checker relative h-7 w-7 shrink-0 overflow-hidden rounded-lg border border-border">
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="min-w-0 flex-1 truncate font-mono text-sm text-primary">{value}</span>
249
- <svg class="h-4 w-4 shrink-0 text-muted" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
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
- if (!menuEl) return;
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
- placeMenu(x, y);
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) showMenu();
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
- placeMenu(x, y);
269
+ highlighted = -1;
270
+ menuX = x;
271
+ menuY = y;
247
272
  requestAnimationFrame(() => {
248
273
  requestAnimationFrame(() => {
249
- if (open) showMenu();
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 handleFiles(files: File[]) {
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: URL.createObjectURL(file)
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;