@a4ui/core 0.33.0 → 0.35.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.
Files changed (58) hide show
  1. package/dist/elements.css +299 -0
  2. package/dist/full.css +299 -0
  3. package/dist/index.d.ts +27 -1
  4. package/dist/index.js +9140 -5065
  5. package/dist/ui/ActivityFeed.d.ts +49 -0
  6. package/dist/ui/AudioWaveform.d.ts +22 -0
  7. package/dist/ui/AvailabilityPicker.d.ts +35 -0
  8. package/dist/ui/CodeEditor.d.ts +25 -0
  9. package/dist/ui/CouponField.d.ts +33 -0
  10. package/dist/ui/EmojiPicker.d.ts +18 -0
  11. package/dist/ui/EventScheduler.d.ts +41 -0
  12. package/dist/ui/FollowingPointer.d.ts +24 -0
  13. package/dist/ui/GanttChart.d.ts +37 -0
  14. package/dist/ui/InteractiveMap.d.ts +50 -0
  15. package/dist/ui/JsonViewer.d.ts +24 -0
  16. package/dist/ui/Kanban.d.ts +49 -0
  17. package/dist/ui/Lamp.d.ts +22 -0
  18. package/dist/ui/Lightbox.d.ts +41 -0
  19. package/dist/ui/LocationPicker.d.ts +25 -0
  20. package/dist/ui/MaskedInput.d.ts +26 -0
  21. package/dist/ui/OnboardingChecklist.d.ts +51 -0
  22. package/dist/ui/OtpInput.d.ts +29 -0
  23. package/dist/ui/PivotTable.d.ts +37 -0
  24. package/dist/ui/PresenceAvatars.d.ts +54 -0
  25. package/dist/ui/QueryBuilder.d.ts +56 -0
  26. package/dist/ui/SheetSnap.d.ts +28 -0
  27. package/dist/ui/SignaturePad.d.ts +19 -0
  28. package/dist/ui/SpreadsheetGrid.d.ts +27 -0
  29. package/dist/ui/TreeTable.d.ts +52 -0
  30. package/dist/ui/VideoPlayerShell.d.ts +19 -0
  31. package/package.json +1 -1
  32. package/src/index.ts +60 -1
  33. package/src/ui/ActivityFeed.tsx +178 -0
  34. package/src/ui/AudioWaveform.tsx +190 -0
  35. package/src/ui/AvailabilityPicker.tsx +163 -0
  36. package/src/ui/CodeEditor.tsx +277 -0
  37. package/src/ui/CouponField.tsx +120 -0
  38. package/src/ui/EmojiPicker.tsx +424 -0
  39. package/src/ui/EventScheduler.tsx +280 -0
  40. package/src/ui/FollowingPointer.tsx +112 -0
  41. package/src/ui/GanttChart.tsx +283 -0
  42. package/src/ui/InteractiveMap.tsx +349 -0
  43. package/src/ui/JsonViewer.tsx +189 -0
  44. package/src/ui/Kanban.tsx +250 -0
  45. package/src/ui/Lamp.tsx +88 -0
  46. package/src/ui/Lightbox.tsx +261 -0
  47. package/src/ui/LocationPicker.tsx +96 -0
  48. package/src/ui/MaskedInput.tsx +108 -0
  49. package/src/ui/OnboardingChecklist.tsx +163 -0
  50. package/src/ui/OtpInput.tsx +153 -0
  51. package/src/ui/PivotTable.tsx +0 -0
  52. package/src/ui/PresenceAvatars.tsx +142 -0
  53. package/src/ui/QueryBuilder.tsx +280 -0
  54. package/src/ui/SheetSnap.tsx +264 -0
  55. package/src/ui/SignaturePad.tsx +221 -0
  56. package/src/ui/SpreadsheetGrid.tsx +304 -0
  57. package/src/ui/TreeTable.tsx +172 -0
  58. package/src/ui/VideoPlayerShell.tsx +282 -0
@@ -0,0 +1,282 @@
1
+ // Custom video player over the native <video> element — no media library.
2
+ // Controls (play/pause, scrub, volume, fullscreen, PiP) are hand-rolled on top
3
+ // of the video's own event listeners (timeupdate/play/pause/volumechange),
4
+ // wired up in onMount and torn down in onCleanup. The control bar auto-hides
5
+ // during playback and reappears on pointer movement or keyboard focus.
6
+ import { Maximize, Pause, PictureInPicture2, Play, Volume2, VolumeX } from 'lucide-solid'
7
+ import type { JSX } from 'solid-js'
8
+ import { createSignal, onCleanup, onMount, Show } from 'solid-js'
9
+
10
+ import { cn } from '../lib/cn'
11
+
12
+ export interface VideoPlayerShellProps {
13
+ src: string
14
+ poster?: string
15
+ class?: string
16
+ }
17
+
18
+ const AUTO_HIDE_MS = 2200
19
+ const SEEK_STEP_SECONDS = 5
20
+
21
+ function formatTime(seconds: number): string {
22
+ if (!Number.isFinite(seconds) || seconds < 0) return '0:00'
23
+ const total = Math.floor(seconds)
24
+ const m = Math.floor(total / 60)
25
+ const s = total % 60
26
+ return `${m}:${s.toString().padStart(2, '0')}`
27
+ }
28
+
29
+ /**
30
+ * Native `<video>` wrapped in a glass shell with fully custom controls: a big
31
+ * center play/pause, a bottom bar (play/pause, elapsed/duration, a scrub
32
+ * range, mute toggle, fullscreen, Picture-in-Picture), and a keyboard map
33
+ * (Space = play/pause, ArrowLeft/Right = seek ±5s). Controls auto-hide while
34
+ * playing and reappear on pointer movement.
35
+ *
36
+ * @example
37
+ * ```tsx
38
+ * <VideoPlayerShell src="/media/demo.mp4" poster="/media/demo-poster.jpg" />
39
+ * ```
40
+ */
41
+ export function VideoPlayerShell(props: VideoPlayerShellProps): JSX.Element {
42
+ let containerRef: HTMLDivElement | undefined
43
+ let videoRef: HTMLVideoElement | undefined
44
+
45
+ const [playing, setPlaying] = createSignal(false)
46
+ const [currentTime, setCurrentTime] = createSignal(0)
47
+ const [duration, setDuration] = createSignal(0)
48
+ const [muted, setMuted] = createSignal(false)
49
+ const [volume, setVolume] = createSignal(1)
50
+ const [fullscreen, setFullscreen] = createSignal(false)
51
+ const [pipSupported, setPipSupported] = createSignal(false)
52
+ const [controlsVisible, setControlsVisible] = createSignal(true)
53
+
54
+ let hideTimer: ReturnType<typeof setTimeout> | undefined
55
+
56
+ const clearHideTimer = (): void => {
57
+ if (hideTimer !== undefined) clearTimeout(hideTimer)
58
+ hideTimer = undefined
59
+ }
60
+
61
+ const scheduleHide = (): void => {
62
+ clearHideTimer()
63
+ if (!playing()) return
64
+ hideTimer = setTimeout(() => setControlsVisible(false), AUTO_HIDE_MS)
65
+ }
66
+
67
+ const revealControls = (): void => {
68
+ setControlsVisible(true)
69
+ scheduleHide()
70
+ }
71
+
72
+ const togglePlay = (): void => {
73
+ const video = videoRef
74
+ if (!video) return
75
+ if (video.paused) void video.play()
76
+ else video.pause()
77
+ }
78
+
79
+ const seekBy = (deltaSeconds: number): void => {
80
+ const video = videoRef
81
+ if (!video) return
82
+ video.currentTime = Math.min(Math.max(video.currentTime + deltaSeconds, 0), video.duration || 0)
83
+ }
84
+
85
+ const handleScrub = (event: Event & { currentTarget: HTMLInputElement }): void => {
86
+ const video = videoRef
87
+ if (!video) return
88
+ video.currentTime = Number(event.currentTarget.value)
89
+ }
90
+
91
+ const toggleMute = (): void => {
92
+ const video = videoRef
93
+ if (!video) return
94
+ video.muted = !video.muted
95
+ }
96
+
97
+ const handleVolume = (event: Event & { currentTarget: HTMLInputElement }): void => {
98
+ const video = videoRef
99
+ if (!video) return
100
+ const next = Number(event.currentTarget.value)
101
+ video.volume = next
102
+ video.muted = next === 0
103
+ }
104
+
105
+ const toggleFullscreen = (): void => {
106
+ const container = containerRef
107
+ if (!container) return
108
+ if (document.fullscreenElement) void document.exitFullscreen()
109
+ else void container.requestFullscreen()
110
+ }
111
+
112
+ const togglePip = (): void => {
113
+ const video = videoRef
114
+ if (!video || !pipSupported()) return
115
+ if (document.pictureInPictureElement) void document.exitPictureInPicture()
116
+ else void video.requestPictureInPicture()
117
+ }
118
+
119
+ const handleKeyDown = (event: KeyboardEvent): void => {
120
+ if (event.code === 'Space') {
121
+ event.preventDefault()
122
+ togglePlay()
123
+ } else if (event.code === 'ArrowLeft') {
124
+ event.preventDefault()
125
+ seekBy(-SEEK_STEP_SECONDS)
126
+ } else if (event.code === 'ArrowRight') {
127
+ event.preventDefault()
128
+ seekBy(SEEK_STEP_SECONDS)
129
+ }
130
+ revealControls()
131
+ }
132
+
133
+ onMount(() => {
134
+ const video = videoRef
135
+ if (!video) return
136
+
137
+ setPipSupported(
138
+ typeof document !== 'undefined' &&
139
+ 'pictureInPictureEnabled' in document &&
140
+ !video.disablePictureInPicture,
141
+ )
142
+
143
+ const onPlay = (): void => {
144
+ setPlaying(true)
145
+ scheduleHide()
146
+ }
147
+ const onPause = (): void => {
148
+ setPlaying(false)
149
+ clearHideTimer()
150
+ setControlsVisible(true)
151
+ }
152
+ const onTimeUpdate = (): void => {
153
+ setCurrentTime(video.currentTime)
154
+ }
155
+ const onLoadedMetadata = (): void => {
156
+ setDuration(video.duration || 0)
157
+ }
158
+ const onVolumeChange = (): void => {
159
+ setMuted(video.muted)
160
+ setVolume(video.volume)
161
+ }
162
+ const onFullscreenChange = (): void => {
163
+ setFullscreen(document.fullscreenElement === containerRef)
164
+ }
165
+
166
+ video.addEventListener('play', onPlay)
167
+ video.addEventListener('pause', onPause)
168
+ video.addEventListener('timeupdate', onTimeUpdate)
169
+ video.addEventListener('loadedmetadata', onLoadedMetadata)
170
+ video.addEventListener('volumechange', onVolumeChange)
171
+ document.addEventListener('fullscreenchange', onFullscreenChange)
172
+
173
+ onCleanup(() => {
174
+ video.removeEventListener('play', onPlay)
175
+ video.removeEventListener('pause', onPause)
176
+ video.removeEventListener('timeupdate', onTimeUpdate)
177
+ video.removeEventListener('loadedmetadata', onLoadedMetadata)
178
+ video.removeEventListener('volumechange', onVolumeChange)
179
+ document.removeEventListener('fullscreenchange', onFullscreenChange)
180
+ clearHideTimer()
181
+ })
182
+ })
183
+
184
+ return (
185
+ <div
186
+ ref={containerRef}
187
+ class={cn('card group relative aspect-video w-full overflow-hidden bg-black outline-none', props.class)}
188
+ tabIndex={0}
189
+ onKeyDown={handleKeyDown}
190
+ onPointerMove={revealControls}
191
+ onMouseLeave={() => playing() && setControlsVisible(false)}
192
+ >
193
+ <video
194
+ ref={videoRef}
195
+ src={props.src}
196
+ poster={props.poster}
197
+ class="h-full w-full object-contain"
198
+ onClick={togglePlay}
199
+ />
200
+
201
+ {/* Big center play/pause */}
202
+ <Show when={controlsVisible() || !playing()}>
203
+ <button
204
+ type="button"
205
+ aria-label={playing() ? 'Pause' : 'Play'}
206
+ onClick={togglePlay}
207
+ class="absolute inset-0 flex items-center justify-center transition-opacity duration-200"
208
+ >
209
+ <Show when={!playing()}>
210
+ <span class="flex h-16 w-16 items-center justify-center rounded-full bg-black/50 text-white backdrop-blur-sm transition-transform duration-150 hover:scale-105">
211
+ <Play class="h-7 w-7 translate-x-0.5" fill="currentColor" />
212
+ </span>
213
+ </Show>
214
+ </button>
215
+ </Show>
216
+
217
+ {/* Bottom control bar */}
218
+ <div
219
+ class={cn(
220
+ 'absolute inset-x-0 bottom-0 flex flex-col gap-1.5 bg-gradient-to-t from-black/80 via-black/40 to-transparent px-3 pb-2 pt-6 text-white transition-opacity duration-200',
221
+ controlsVisible() ? 'opacity-100' : 'pointer-events-none opacity-0',
222
+ )}
223
+ >
224
+ <input
225
+ type="range"
226
+ aria-label="Seek"
227
+ min={0}
228
+ max={duration() || 0}
229
+ step={0.1}
230
+ value={currentTime()}
231
+ onInput={handleScrub}
232
+ class="h-1.5 w-full cursor-pointer appearance-none rounded-full bg-white/25 accent-primary"
233
+ />
234
+ <div class="flex items-center gap-2">
235
+ <button type="button" aria-label={playing() ? 'Pause' : 'Play'} onClick={togglePlay} class="p-1">
236
+ <Show when={playing()} fallback={<Play class="h-4 w-4" fill="currentColor" />}>
237
+ <Pause class="h-4 w-4" fill="currentColor" />
238
+ </Show>
239
+ </button>
240
+
241
+ <span class="text-xs tabular-nums text-white/85">
242
+ {formatTime(currentTime())} / {formatTime(duration())}
243
+ </span>
244
+
245
+ <div class="ml-1 flex items-center gap-1">
246
+ <button type="button" aria-label={muted() ? 'Unmute' : 'Mute'} onClick={toggleMute} class="p-1">
247
+ <Show when={!muted() && volume() > 0} fallback={<VolumeX class="h-4 w-4" />}>
248
+ <Volume2 class="h-4 w-4" />
249
+ </Show>
250
+ </button>
251
+ <input
252
+ type="range"
253
+ aria-label="Volume"
254
+ min={0}
255
+ max={1}
256
+ step={0.05}
257
+ value={muted() ? 0 : volume()}
258
+ onInput={handleVolume}
259
+ class="h-1 w-16 cursor-pointer appearance-none rounded-full bg-white/25 accent-primary"
260
+ />
261
+ </div>
262
+
263
+ <div class="ml-auto flex items-center gap-2">
264
+ <Show when={pipSupported()}>
265
+ <button type="button" aria-label="Picture in picture" onClick={togglePip} class="p-1">
266
+ <PictureInPicture2 class="h-4 w-4" />
267
+ </button>
268
+ </Show>
269
+ <button
270
+ type="button"
271
+ aria-label={fullscreen() ? 'Exit fullscreen' : 'Enter fullscreen'}
272
+ onClick={toggleFullscreen}
273
+ class="p-1"
274
+ >
275
+ <Maximize class="h-4 w-4" />
276
+ </button>
277
+ </div>
278
+ </div>
279
+ </div>
280
+ </div>
281
+ )
282
+ }