uifolio 0.5.1 → 0.5.4
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/README.md +70 -269
- package/cli/uifolio.mjs +446 -164
- package/package.json +3 -2
- package/src/components/ui/audio-player.tsx +7 -7
- package/src/components/ui/calendar.tsx +5 -5
- package/src/components/ui/color-picker.tsx +17 -17
- package/src/components/ui/command-menu.tsx +5 -5
- package/src/components/ui/file-upload.tsx +11 -11
- package/src/components/ui/icon-picker.tsx +12 -12
- package/src/components/ui/resizable.tsx +1 -1
- package/src/components/ui/rich-text-editor.tsx +17 -18
- package/src/components/ui/stories.tsx +4 -4
- package/src/components/ui/table.tsx +164 -0
- package/src/components/ui/video-player.tsx +49 -49
- package/src/lib/date.ts +30 -30
- package/src/lib/uploader.ts +13 -13
- package/src/lib/use-controllable.ts +9 -9
- package/src/lib/use-hotkeys.ts +1 -1
- package/src/lib/use-theme.ts +2 -2
- package/src/lib/video.ts +12 -12
- package/src/lib/vtt.ts +1 -1
package/src/lib/uploader.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
export interface UploaderOptions {
|
|
2
|
-
/**
|
|
2
|
+
/** Signing endpoint. Default: /api/upload/sign */
|
|
3
3
|
signEndpoint?: string
|
|
4
|
-
/**
|
|
4
|
+
/** Number of extra attempts on failure. Default: 2 */
|
|
5
5
|
retries?: number
|
|
6
|
-
/**
|
|
6
|
+
/** Base delay between attempts (ms). Default: 800 */
|
|
7
7
|
retryDelayMs?: number
|
|
8
8
|
}
|
|
9
9
|
|
|
@@ -42,7 +42,7 @@ function putWithProgress(
|
|
|
42
42
|
onProgress(1)
|
|
43
43
|
resolvePromise()
|
|
44
44
|
} else {
|
|
45
|
-
reject(new UploadError(`
|
|
45
|
+
reject(new UploadError(`The server returned ${request.status}`, request.status))
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
request.onerror = () => reject(new UploadError('Connexion interrompue pendant l’envoi'))
|
|
@@ -52,12 +52,12 @@ function putWithProgress(
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
/**
|
|
55
|
-
*
|
|
56
|
-
* 1.
|
|
57
|
-
* 2.
|
|
58
|
-
* 3.
|
|
55
|
+
* “Real” uploader wired to /api/upload/sign:
|
|
56
|
+
* 1. asks the server for a signed URL,
|
|
57
|
+
* 2. sends the file with a PUT and progress (XHR),
|
|
58
|
+
* 3. retries automatically, reusing the same signed URL (resume).
|
|
59
59
|
*
|
|
60
|
-
*
|
|
60
|
+
* Returns the public URL of the uploaded file.
|
|
61
61
|
*/
|
|
62
62
|
export function createUploader(options: UploaderOptions = {}) {
|
|
63
63
|
const {
|
|
@@ -73,7 +73,7 @@ export function createUploader(options: UploaderOptions = {}) {
|
|
|
73
73
|
body: JSON.stringify({ name: file.name, size: file.size, type: file.type }),
|
|
74
74
|
})
|
|
75
75
|
if (!signResponse.ok) {
|
|
76
|
-
throw new UploadError(`Signature
|
|
76
|
+
throw new UploadError(`Signature refused (${signResponse.status})`, signResponse.status)
|
|
77
77
|
}
|
|
78
78
|
const signed = (await signResponse.json()) as SignedUpload
|
|
79
79
|
|
|
@@ -85,7 +85,7 @@ export function createUploader(options: UploaderOptions = {}) {
|
|
|
85
85
|
} catch (error) {
|
|
86
86
|
lastError = error
|
|
87
87
|
const status = error instanceof UploadError ? error.status : undefined
|
|
88
|
-
// 4xx =
|
|
88
|
+
// 4xx = final refusal (size, key, auth): no point retrying
|
|
89
89
|
if (status !== undefined && status >= 400 && status < 500) break
|
|
90
90
|
if (attempt < retries) {
|
|
91
91
|
await new Promise((resolvePromise) =>
|
|
@@ -96,11 +96,11 @@ export function createUploader(options: UploaderOptions = {}) {
|
|
|
96
96
|
}
|
|
97
97
|
throw lastError instanceof Error
|
|
98
98
|
? lastError
|
|
99
|
-
: new UploadError('
|
|
99
|
+
: new UploadError('Upload failed after several attempts')
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
/**
|
|
103
|
+
/** Queue per signed URL: allows resuming an interrupted upload. */
|
|
104
104
|
export function resumeUpload(
|
|
105
105
|
signed: SignedUpload,
|
|
106
106
|
file: File,
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* Returns local state driven by a prop when that prop is explicit.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* When the parent explicitly provides the prop, the local state stays
|
|
7
|
+
* in sync with it. When the prop is `undefined`, the component keeps
|
|
8
|
+
* its own local value.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* This version is compatible with React Compiler: it never reads
|
|
11
|
+
* refs during render.
|
|
12
12
|
*/
|
|
13
13
|
export function useControllableValue<T>(
|
|
14
14
|
prop: T | undefined,
|
|
@@ -21,14 +21,14 @@ export function useControllableValue<T>(
|
|
|
21
21
|
|
|
22
22
|
const isControlled = prop !== undefined
|
|
23
23
|
|
|
24
|
-
//
|
|
24
|
+
// Keeps the local state in sync when the parent control changes.
|
|
25
25
|
useEffect(() => {
|
|
26
26
|
controlledRef.current = isControlled
|
|
27
27
|
previousPropRef.current = prop
|
|
28
28
|
})
|
|
29
29
|
|
|
30
|
-
//
|
|
31
|
-
//
|
|
30
|
+
// First controlled render: the prop must replace the local state.
|
|
31
|
+
// Later renders: only sync when the prop changed.
|
|
32
32
|
useEffect(() => {
|
|
33
33
|
if (!isControlled) return
|
|
34
34
|
const next = prop as T
|
package/src/lib/use-hotkeys.ts
CHANGED
|
@@ -25,7 +25,7 @@ function matches(pattern: string, event: KeyboardEvent): boolean {
|
|
|
25
25
|
return event.key.toLowerCase() === key
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
/**
|
|
28
|
+
/** Registers global shortcuts: `mod+k`, `shift+?`, `escape`, `arrowup`… */
|
|
29
29
|
export function useHotkeys(map: HotkeyMap, options: { enabled?: boolean } = {}) {
|
|
30
30
|
const handlers = useRef(map)
|
|
31
31
|
const enabled = options.enabled ?? true
|
package/src/lib/use-theme.ts
CHANGED
|
@@ -21,8 +21,8 @@ function applyTheme(theme: Theme) {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
24
|
+
* Persistent light/dark theme. `system` follows the system preference
|
|
25
|
+
* until the user makes an explicit choice.
|
|
26
26
|
*/
|
|
27
27
|
export function useTheme() {
|
|
28
28
|
const [theme, setThemeState] = useState<Theme>(readTheme)
|
package/src/lib/video.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Video helpers shared by the library components.
|
|
3
|
+
* No dependency: copying this file along with the component is enough.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
export type PlayerSource =
|
|
@@ -28,7 +28,7 @@ export function toSourceList(input: PlayerSource | PlayerSource[]): PlayerSource
|
|
|
28
28
|
return Array.isArray(input) ? input : [input]
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
/**
|
|
31
|
+
/** Stable signature, so object URLs are not recreated on every render. */
|
|
32
32
|
export function sourceSignature(input: PlayerSource | PlayerSource[]): string {
|
|
33
33
|
return toSourceList(input)
|
|
34
34
|
.map((source) => {
|
|
@@ -75,7 +75,7 @@ export function youtubeThumbnailFallback(id: string): string {
|
|
|
75
75
|
loop?: boolean
|
|
76
76
|
muted?: boolean
|
|
77
77
|
controls?: boolean
|
|
78
|
-
/**
|
|
78
|
+
/** Host page origin: the YouTube iframe API matches postMessage commands to it. */
|
|
79
79
|
origin?: string
|
|
80
80
|
playsinline?: boolean
|
|
81
81
|
}
|
|
@@ -123,7 +123,7 @@ export function buildEmbedUrl(
|
|
|
123
123
|
|
|
124
124
|
|
|
125
125
|
/* -------------------------------------------------------------------------- */
|
|
126
|
-
/*
|
|
126
|
+
/* Driving the embeds (YouTube / Vimeo) through postMessage */
|
|
127
127
|
/* -------------------------------------------------------------------------- */
|
|
128
128
|
|
|
129
129
|
export type EmbedKind = 'youtube' | 'vimeo'
|
|
@@ -132,12 +132,12 @@ export type EmbedCommand =
|
|
|
132
132
|
| { type: 'play' }
|
|
133
133
|
| { type: 'pause' }
|
|
134
134
|
| { type: 'seek'; time: number }
|
|
135
|
-
/**
|
|
135
|
+
/** Normalized volume 0–1, converted into each platform's unit. */
|
|
136
136
|
| { type: 'volume'; volume: number }
|
|
137
137
|
| { type: 'mute'; muted: boolean }
|
|
138
138
|
| { type: 'rate'; rate: number }
|
|
139
139
|
|
|
140
|
-
/** Message
|
|
140
|
+
/** Message to post into the iframe: a JSON string for YouTube, an object for Vimeo. */
|
|
141
141
|
export function embedCommand(
|
|
142
142
|
kind: EmbedKind,
|
|
143
143
|
command: EmbedCommand,
|
|
@@ -190,10 +190,10 @@ const VIMEO_EVENTS = [
|
|
|
190
190
|
'playbackratechange',
|
|
191
191
|
]
|
|
192
192
|
|
|
193
|
-
/** Messages
|
|
193
|
+
/** Messages to send to the iframe to receive its state and events. */
|
|
194
194
|
export function embedHandshake(kind: EmbedKind): Array<Record<string, unknown> | string> {
|
|
195
195
|
if (kind === 'youtube') {
|
|
196
|
-
//
|
|
196
|
+
// Starts the `infoDelivery` messages (time, duration, state).
|
|
197
197
|
return [JSON.stringify({ event: 'listening', id: 1, channel: 'widget' })]
|
|
198
198
|
}
|
|
199
199
|
return [
|
|
@@ -249,7 +249,7 @@ function parseYoutubeStatus(raw: unknown): EmbedStatus | null {
|
|
|
249
249
|
if (duration !== undefined && duration > 0) status.duration = duration
|
|
250
250
|
|
|
251
251
|
const playerState = toNumber(info.playerState)
|
|
252
|
-
// -1
|
|
252
|
+
// -1 unstarted, 0 ended, 1 playing, 2 paused, 3 buffering, 5 cued.
|
|
253
253
|
if (playerState !== undefined) status.playing = playerState === 1 || playerState === 3
|
|
254
254
|
|
|
255
255
|
const muted = info.muted
|
|
@@ -312,7 +312,7 @@ function parseVimeoStatus(raw: unknown): EmbedStatus | null {
|
|
|
312
312
|
return null
|
|
313
313
|
}
|
|
314
314
|
|
|
315
|
-
/**
|
|
315
|
+
/** Decodes a postMessage sent by the iframe and extracts the playback state. */
|
|
316
316
|
export function parseEmbedMessage(kind: EmbedKind, raw: unknown): EmbedStatus | null {
|
|
317
317
|
return kind === 'youtube' ? parseYoutubeStatus(raw) : parseVimeoStatus(raw)
|
|
318
318
|
}
|
|
@@ -335,7 +335,7 @@ export function formatTime(value: number): string {
|
|
|
335
335
|
return `${minutes}:${paddedSeconds}`
|
|
336
336
|
}
|
|
337
337
|
|
|
338
|
-
/** "16/9" | 16/9 | 1.77 | "auto" ->
|
|
338
|
+
/** "16/9" | 16/9 | 1.77 | "auto" -> CSS aspect-ratio value. */
|
|
339
339
|
export function toAspectRatio(value: string | number | undefined): string | undefined {
|
|
340
340
|
if (value == null) return undefined
|
|
341
341
|
if (typeof value === 'number') return String(value)
|
package/src/lib/vtt.ts
CHANGED
|
@@ -57,7 +57,7 @@ function cleanCueText(lines: string[]): string {
|
|
|
57
57
|
.trim()
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
/**
|
|
60
|
+
/** Parses a WebVTT file into cues. Tolerant: skips headers, notes, styles and invalid blocks. */
|
|
61
61
|
export function parseVtt(raw: string): VttCue[] {
|
|
62
62
|
const lines = raw
|
|
63
63
|
.replace(/^\uFEFF/, '')
|