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/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uifolio",
|
|
3
|
-
"version": "0.5.
|
|
4
|
-
"description": "CLI
|
|
3
|
+
"version": "0.5.4",
|
|
4
|
+
"description": "CLI that copies React components (calendar, video/audio players, OTP, editor, upload) and their helpers into your project, with Tailwind CSS 4 tokens.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Barronakn <jeanbarronalokpon@gmail.com>",
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"file-upload",
|
|
30
30
|
"rich-text-editor",
|
|
31
31
|
"command-menu",
|
|
32
|
+
"data-table",
|
|
32
33
|
"resizable-panels",
|
|
33
34
|
"time-picker",
|
|
34
35
|
"cadence",
|
|
@@ -136,7 +136,7 @@ export function AudioPlayer({
|
|
|
136
136
|
<button
|
|
137
137
|
type="button"
|
|
138
138
|
onClick={() => void togglePlay()}
|
|
139
|
-
aria-label={playing ? '
|
|
139
|
+
aria-label={playing ? 'Pause' : 'Play'}
|
|
140
140
|
className="grid size-9 shrink-0 place-items-center rounded-full bg-[var(--ap-accent)] text-black shadow-sm transition hover:brightness-110"
|
|
141
141
|
>
|
|
142
142
|
{playing ? (
|
|
@@ -157,7 +157,7 @@ export function AudioPlayer({
|
|
|
157
157
|
step={0.05}
|
|
158
158
|
value={Math.min(currentTime, duration || 0)}
|
|
159
159
|
onChange={(event) => seek(Number(event.target.value))}
|
|
160
|
-
aria-label="
|
|
160
|
+
aria-label="Playback position"
|
|
161
161
|
className="h-1 w-full min-w-0 cursor-pointer appearance-none rounded-full"
|
|
162
162
|
style={{
|
|
163
163
|
accentColor: accent,
|
|
@@ -176,7 +176,7 @@ export function AudioPlayer({
|
|
|
176
176
|
<button
|
|
177
177
|
type="button"
|
|
178
178
|
onClick={() => void togglePlay()}
|
|
179
|
-
aria-label={playing ? '
|
|
179
|
+
aria-label={playing ? 'Pause' : 'Play'}
|
|
180
180
|
className="grid size-8 shrink-0 place-items-center rounded-full bg-[var(--ap-accent)] text-black transition hover:brightness-110"
|
|
181
181
|
>
|
|
182
182
|
{playing ? (
|
|
@@ -195,7 +195,7 @@ export function AudioPlayer({
|
|
|
195
195
|
step={0.05}
|
|
196
196
|
value={Math.min(currentTime, duration || 0)}
|
|
197
197
|
onChange={(event) => seek(Number(event.target.value))}
|
|
198
|
-
aria-label="
|
|
198
|
+
aria-label="Playback position"
|
|
199
199
|
className="mt-1.5 h-1 w-full cursor-pointer appearance-none rounded-full"
|
|
200
200
|
style={{
|
|
201
201
|
accentColor: accent,
|
|
@@ -214,7 +214,7 @@ export function AudioPlayer({
|
|
|
214
214
|
<button
|
|
215
215
|
type="button"
|
|
216
216
|
onClick={() => setMuted((value) => !value)}
|
|
217
|
-
aria-label={muted ? '
|
|
217
|
+
aria-label={muted ? 'Unmute' : 'Mute'}
|
|
218
218
|
className="inline-flex size-7 items-center justify-center rounded-md text-muted-foreground transition hover:bg-muted hover:text-foreground"
|
|
219
219
|
>
|
|
220
220
|
{effectiveVolume === 0 ? (
|
|
@@ -246,12 +246,12 @@ export function AudioPlayer({
|
|
|
246
246
|
<button
|
|
247
247
|
type="button"
|
|
248
248
|
onClick={cycleRate}
|
|
249
|
-
aria-label={`
|
|
249
|
+
aria-label={`Speed ${rate}×`}
|
|
250
250
|
className="ml-auto rounded-md px-1.5 py-0.5 text-[10px] font-semibold text-muted-foreground transition hover:bg-muted hover:text-foreground"
|
|
251
251
|
>
|
|
252
252
|
{rate}×
|
|
253
253
|
</button>
|
|
254
|
-
{error && <span className="text-[10px] text-rose-400">Source
|
|
254
|
+
{error && <span className="text-[10px] text-rose-400">Source unavailable</span>}
|
|
255
255
|
</div>
|
|
256
256
|
</div>
|
|
257
257
|
)
|
|
@@ -199,7 +199,7 @@ export function Calendar({
|
|
|
199
199
|
<>
|
|
200
200
|
<label className="flex items-center gap-2 rounded-xl border bg-background/40 px-2.5 py-1.5 focus-within:ring-2 focus-within:ring-primary/40">
|
|
201
201
|
<CalendarDays className="size-3.5 shrink-0 text-muted-foreground" />
|
|
202
|
-
<span className="sr-only">
|
|
202
|
+
<span className="sr-only">Start (MM/DD/YYYY)</span>
|
|
203
203
|
<input
|
|
204
204
|
value={fromDraft}
|
|
205
205
|
onChange={(event) => setFromDraft(event.target.value)}
|
|
@@ -210,14 +210,14 @@ export function Calendar({
|
|
|
210
210
|
applyRange(fromDraft, toDraft)
|
|
211
211
|
}
|
|
212
212
|
}}
|
|
213
|
-
placeholder="
|
|
213
|
+
placeholder="from MM/DD/YYYY"
|
|
214
214
|
inputMode="numeric"
|
|
215
215
|
className="w-full bg-transparent text-xs tabular-nums outline-none placeholder:text-muted-foreground/60"
|
|
216
216
|
/>
|
|
217
217
|
</label>
|
|
218
218
|
<label className="flex items-center gap-2 rounded-xl border bg-background/40 px-2.5 py-1.5 focus-within:ring-2 focus-within:ring-primary/40">
|
|
219
219
|
<CalendarDays className="size-3.5 shrink-0 text-muted-foreground" />
|
|
220
|
-
<span className="sr-only">
|
|
220
|
+
<span className="sr-only">End (MM/DD/YYYY)</span>
|
|
221
221
|
<input
|
|
222
222
|
value={toDraft}
|
|
223
223
|
onChange={(event) => setToDraft(event.target.value)}
|
|
@@ -228,7 +228,7 @@ export function Calendar({
|
|
|
228
228
|
applyRange(fromDraft, toDraft)
|
|
229
229
|
}
|
|
230
230
|
}}
|
|
231
|
-
placeholder="
|
|
231
|
+
placeholder="to MM/DD/YYYY"
|
|
232
232
|
inputMode="numeric"
|
|
233
233
|
className="w-full bg-transparent text-xs tabular-nums outline-none placeholder:text-muted-foreground/60"
|
|
234
234
|
/>
|
|
@@ -242,7 +242,7 @@ export function Calendar({
|
|
|
242
242
|
<button
|
|
243
243
|
type="button"
|
|
244
244
|
onClick={() => setVisibleMonth((month) => addMonths(month, -1))}
|
|
245
|
-
aria-label="
|
|
245
|
+
aria-label="Previous month"
|
|
246
246
|
className="inline-flex size-7 items-center justify-center rounded-lg text-muted-foreground transition hover:bg-muted hover:text-foreground"
|
|
247
247
|
>
|
|
248
248
|
<ChevronLeft className="size-4" />
|
|
@@ -296,7 +296,7 @@ export function ColorPicker({
|
|
|
296
296
|
type="button"
|
|
297
297
|
onClick={toggle}
|
|
298
298
|
disabled={disabled}
|
|
299
|
-
aria-label="
|
|
299
|
+
aria-label="Open the color picker"
|
|
300
300
|
aria-haspopup="dialog"
|
|
301
301
|
aria-expanded={isOpen}
|
|
302
302
|
aria-controls={panelId}
|
|
@@ -313,7 +313,7 @@ export function ColorPicker({
|
|
|
313
313
|
|
|
314
314
|
<div className="relative min-w-0 flex-1">
|
|
315
315
|
<span className="pointer-events-none absolute left-2 top-1/2 -translate-y-1/2 font-mono text-[9px] font-semibold uppercase tracking-wide text-muted-foreground">
|
|
316
|
-
{
|
|
316
|
+
{label ?? 'Color'}
|
|
317
317
|
</span>
|
|
318
318
|
<input
|
|
319
319
|
id={inputId}
|
|
@@ -336,7 +336,7 @@ export function ColorPicker({
|
|
|
336
336
|
}}
|
|
337
337
|
spellCheck={false}
|
|
338
338
|
aria-invalid={invalid}
|
|
339
|
-
aria-label="
|
|
339
|
+
aria-label="Color value"
|
|
340
340
|
placeholder={displayFormat === 'hex' ? '#10B981' : displayFormat === 'rgb' ? 'rgb(16, 185, 129)' : 'hsl(160, 84%, 39%)'}
|
|
341
341
|
className={cn(
|
|
342
342
|
'h-8 w-full rounded-sm border bg-background pl-10 pr-2 font-mono text-[10.5px] text-foreground outline-none transition placeholder:normal-case placeholder:text-muted-foreground/70 focus:border-primary/60 focus:ring-2 focus:ring-primary/15 disabled:cursor-not-allowed',
|
|
@@ -349,8 +349,8 @@ export function ColorPicker({
|
|
|
349
349
|
type="button"
|
|
350
350
|
onClick={toggle}
|
|
351
351
|
disabled={disabled}
|
|
352
|
-
aria-label="
|
|
353
|
-
title="
|
|
352
|
+
aria-label="Customize the color"
|
|
353
|
+
title="Customize the color"
|
|
354
354
|
className={cn(
|
|
355
355
|
'grid size-8 shrink-0 place-items-center rounded-sm border transition outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed',
|
|
356
356
|
isOpen
|
|
@@ -372,7 +372,7 @@ export function ColorPicker({
|
|
|
372
372
|
ref={panelRef}
|
|
373
373
|
id={panelId}
|
|
374
374
|
role="dialog"
|
|
375
|
-
aria-label="
|
|
375
|
+
aria-label="Color picker"
|
|
376
376
|
tabIndex={-1}
|
|
377
377
|
style={{
|
|
378
378
|
position: 'fixed',
|
|
@@ -382,11 +382,11 @@ export function ColorPicker({
|
|
|
382
382
|
maxHeight: 'min(620px, calc(100dvh - 20px))',
|
|
383
383
|
visibility: position ? 'visible' : 'hidden',
|
|
384
384
|
}}
|
|
385
|
-
className="z-
|
|
385
|
+
className="z-100 overflow-y-auto rounded-md border bg-card text-card-foreground shadow-2xl outline-none"
|
|
386
386
|
>
|
|
387
387
|
<div className="flex items-start justify-between gap-3 border-b px-3.5 py-3">
|
|
388
388
|
<div className="min-w-0">
|
|
389
|
-
<p className="text-[11px] font-semibold">
|
|
389
|
+
<p className="text-[11px] font-semibold">Color picker</p>
|
|
390
390
|
<p className="mt-0.5 truncate font-mono text-[9.5px] uppercase tracking-wide text-muted-foreground">
|
|
391
391
|
{formatColor(current, 'hex', showAlpha)}
|
|
392
392
|
</p>
|
|
@@ -394,7 +394,7 @@ export function ColorPicker({
|
|
|
394
394
|
<button
|
|
395
395
|
type="button"
|
|
396
396
|
onClick={close}
|
|
397
|
-
aria-label="
|
|
397
|
+
aria-label="Close the color picker"
|
|
398
398
|
className="grid size-6 shrink-0 place-items-center rounded-sm text-muted-foreground transition hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring"
|
|
399
399
|
>
|
|
400
400
|
<X className="size-3.5" />
|
|
@@ -404,7 +404,7 @@ export function ColorPicker({
|
|
|
404
404
|
<div className="space-y-3 p-3.5">
|
|
405
405
|
<div
|
|
406
406
|
role="group"
|
|
407
|
-
aria-label="Saturation
|
|
407
|
+
aria-label="Saturation and luminance"
|
|
408
408
|
tabIndex={disabled ? -1 : 0}
|
|
409
409
|
onPointerDown={(event) => {
|
|
410
410
|
if (disabled) return
|
|
@@ -444,7 +444,7 @@ export function ColorPicker({
|
|
|
444
444
|
|
|
445
445
|
<div className="space-y-2">
|
|
446
446
|
<div className="flex items-center justify-between text-[9px] font-semibold uppercase tracking-[0.12em] text-muted-foreground">
|
|
447
|
-
<span>
|
|
447
|
+
<span>Hue</span>
|
|
448
448
|
<span className="font-mono">{Math.round(hue)}°</span>
|
|
449
449
|
</div>
|
|
450
450
|
<input
|
|
@@ -458,7 +458,7 @@ export function ColorPicker({
|
|
|
458
458
|
setDraft(null)
|
|
459
459
|
emitHsv({ ...hsva, h: Number(event.target.value) })
|
|
460
460
|
}}
|
|
461
|
-
aria-label="
|
|
461
|
+
aria-label="Hue"
|
|
462
462
|
className="h-3 w-full cursor-pointer appearance-none rounded-full outline-none transition focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed [&::-moz-range-thumb]:size-4 [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:border-2 [&::-moz-range-thumb]:border-white [&::-moz-range-thumb]:bg-transparent [&::-moz-range-thumb]:shadow-md [&::-webkit-slider-thumb]:size-4 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:border-2 [&::-webkit-slider-thumb]:border-white [&::-webkit-slider-thumb]:bg-transparent [&::-webkit-slider-thumb]:shadow-md"
|
|
463
463
|
style={{
|
|
464
464
|
background:
|
|
@@ -484,7 +484,7 @@ export function ColorPicker({
|
|
|
484
484
|
setDraft(null)
|
|
485
485
|
emit({ ...current, a: Number(event.target.value) / 100 })
|
|
486
486
|
}}
|
|
487
|
-
aria-label="
|
|
487
|
+
aria-label="Transparency"
|
|
488
488
|
className="h-3 w-full cursor-pointer appearance-none rounded-full border border-border outline-none transition focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed [&::-moz-range-thumb]:size-4 [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:border-2 [&::-moz-range-thumb]:border-white [&::-moz-range-thumb]:bg-transparent [&::-moz-range-thumb]:shadow-md [&::-webkit-slider-thumb]:size-4 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:border-2 [&::-webkit-slider-thumb]:border-white [&::-webkit-slider-thumb]:bg-transparent [&::-webkit-slider-thumb]:shadow-md"
|
|
489
489
|
style={{
|
|
490
490
|
backgroundColor: 'transparent',
|
|
@@ -530,8 +530,8 @@ export function ColorPicker({
|
|
|
530
530
|
type="button"
|
|
531
531
|
disabled={disabled}
|
|
532
532
|
onClick={pickFromScreen}
|
|
533
|
-
title="
|
|
534
|
-
aria-label="
|
|
533
|
+
title="Pick a color from the screen"
|
|
534
|
+
aria-label="Pick a color from the screen"
|
|
535
535
|
className="inline-flex items-center gap-1.5 rounded-sm border border-border px-2 py-1 text-[9px] font-semibold uppercase tracking-[0.1em] text-muted-foreground transition hover:border-primary/50 hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
|
|
536
536
|
>
|
|
537
537
|
<Pipette className="size-3" />
|
|
@@ -589,7 +589,7 @@ export function ColorPicker({
|
|
|
589
589
|
emit(rgba)
|
|
590
590
|
}}
|
|
591
591
|
title={raw}
|
|
592
|
-
aria-label={`
|
|
592
|
+
aria-label={`Color ${raw}`}
|
|
593
593
|
aria-pressed={selected}
|
|
594
594
|
className={cn(
|
|
595
595
|
'relative grid size-5 place-items-center overflow-hidden rounded-sm border transition hover:scale-110 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed',
|
|
@@ -614,7 +614,7 @@ export function ColorPicker({
|
|
|
614
614
|
|
|
615
615
|
{checkerAlpha && (
|
|
616
616
|
<p className="text-[9px] leading-relaxed text-muted-foreground">
|
|
617
|
-
|
|
617
|
+
The opacity is included in the exported value.
|
|
618
618
|
</p>
|
|
619
619
|
)}
|
|
620
620
|
</div>
|
|
@@ -34,7 +34,7 @@ export function CommandMenu({
|
|
|
34
34
|
defaultOpen = false,
|
|
35
35
|
onOpenChange,
|
|
36
36
|
placeholder = 'Rechercher une commande…',
|
|
37
|
-
emptyText = '
|
|
37
|
+
emptyText = 'No result.',
|
|
38
38
|
emptyState,
|
|
39
39
|
className,
|
|
40
40
|
}: Readonly<CommandMenuProps>) {
|
|
@@ -94,7 +94,7 @@ export function CommandMenu({
|
|
|
94
94
|
|
|
95
95
|
return (
|
|
96
96
|
<div
|
|
97
|
-
className="fixed inset-0 z-
|
|
97
|
+
className="fixed inset-0 z-70 flex items-start justify-center bg-black/65 px-4 pt-[12vh] backdrop-blur-sm"
|
|
98
98
|
onPointerDown={(event) => {
|
|
99
99
|
if (event.target === event.currentTarget) setOpen(false)
|
|
100
100
|
}}
|
|
@@ -102,7 +102,7 @@ export function CommandMenu({
|
|
|
102
102
|
<div
|
|
103
103
|
role="dialog"
|
|
104
104
|
aria-modal="true"
|
|
105
|
-
aria-label="
|
|
105
|
+
aria-label="Command palette"
|
|
106
106
|
className={cn(
|
|
107
107
|
'w-full max-w-lg overflow-hidden rounded-sm border bg-card shadow-2xl shadow-black/50',
|
|
108
108
|
className,
|
|
@@ -211,8 +211,8 @@ export function CommandMenu({
|
|
|
211
211
|
|
|
212
212
|
<div className="flex items-center justify-between border-t px-3.5 py-2 text-[10px] text-muted-foreground">
|
|
213
213
|
<span className="flex items-center gap-3">
|
|
214
|
-
<span>↑↓
|
|
215
|
-
<span>↵
|
|
214
|
+
<span>↑↓ to navigate</span>
|
|
215
|
+
<span>↵ to open</span>
|
|
216
216
|
</span>
|
|
217
217
|
<span className="font-mono">ctrl / ⌘ + k</span>
|
|
218
218
|
</div>
|
|
@@ -14,7 +14,7 @@ export interface UploadItem {
|
|
|
14
14
|
progress: number
|
|
15
15
|
status: UploadStatus
|
|
16
16
|
error?: string
|
|
17
|
-
/** URL
|
|
17
|
+
/** Public URL returned by the uploader (real upload). */
|
|
18
18
|
url?: string
|
|
19
19
|
}
|
|
20
20
|
|
|
@@ -23,7 +23,7 @@ export interface FileUploadProps {
|
|
|
23
23
|
multiple?: boolean
|
|
24
24
|
maxSizeMb?: number
|
|
25
25
|
maxFiles?: number
|
|
26
|
-
/**
|
|
26
|
+
/** Returns the public file URL (real upload) or void. */
|
|
27
27
|
uploader?: (file: File, onProgress: (value: number) => void) => Promise<string | void>
|
|
28
28
|
onChange?: (items: UploadItem[]) => void
|
|
29
29
|
className?: string
|
|
@@ -142,11 +142,11 @@ export function FileUpload({
|
|
|
142
142
|
for (const file of files) {
|
|
143
143
|
if (accepted.length >= slots) break
|
|
144
144
|
if (file.size > maxSizeMb * 1024 * 1024) {
|
|
145
|
-
errors.push(`${file.name}
|
|
145
|
+
errors.push(`${file.name} exceeds ${maxSizeMb} MB`)
|
|
146
146
|
continue
|
|
147
147
|
}
|
|
148
148
|
if (accept && !matchesAccept(file, accept)) {
|
|
149
|
-
errors.push(`${file.name}
|
|
149
|
+
errors.push(`${file.name} is not an accepted type`)
|
|
150
150
|
continue
|
|
151
151
|
}
|
|
152
152
|
accepted.push(file)
|
|
@@ -218,10 +218,10 @@ export function FileUpload({
|
|
|
218
218
|
<Upload className="size-5" />
|
|
219
219
|
</span>
|
|
220
220
|
<span className="text-sm font-medium">
|
|
221
|
-
{dragging ? '
|
|
221
|
+
{dragging ? 'Drop here' : 'Drag your files or click'}
|
|
222
222
|
</span>
|
|
223
223
|
<span className="max-w-xs text-[11px] leading-relaxed text-muted-foreground">
|
|
224
|
-
{accept ? accept.replaceAll(',', ' · ') : '
|
|
224
|
+
{accept ? accept.replaceAll(',', ' · ') : 'All types'} — up to {maxSizeMb} MB
|
|
225
225
|
{multiple ? ` · ${maxFiles} fichiers` : ''}
|
|
226
226
|
</span>
|
|
227
227
|
<input
|
|
@@ -243,7 +243,7 @@ export function FileUpload({
|
|
|
243
243
|
<button
|
|
244
244
|
type="button"
|
|
245
245
|
onClick={() => setRejection(null)}
|
|
246
|
-
aria-label="
|
|
246
|
+
aria-label="Hide error"
|
|
247
247
|
className="text-rose-300/70 transition hover:text-rose-200"
|
|
248
248
|
>
|
|
249
249
|
<X className="size-3.5" />
|
|
@@ -302,9 +302,9 @@ export function FileUpload({
|
|
|
302
302
|
href={item.url}
|
|
303
303
|
target={item.url ? '_blank' : undefined}
|
|
304
304
|
rel="noreferrer"
|
|
305
|
-
title={item.url ?? '
|
|
305
|
+
title={item.url ?? 'Uploaded'}
|
|
306
306
|
className="grid size-7 shrink-0 place-items-center rounded-md text-emerald-400 transition hover:bg-emerald-400/10"
|
|
307
|
-
aria-label={item.url ? `
|
|
307
|
+
aria-label={item.url ? `Open ${item.file.name}` : 'Uploaded'}
|
|
308
308
|
>
|
|
309
309
|
<Check className="size-4" />
|
|
310
310
|
</a>
|
|
@@ -313,7 +313,7 @@ export function FileUpload({
|
|
|
313
313
|
<button
|
|
314
314
|
type="button"
|
|
315
315
|
onClick={() => retry(item)}
|
|
316
|
-
aria-label="
|
|
316
|
+
aria-label="Retry"
|
|
317
317
|
className="grid size-7 place-items-center rounded-md text-muted-foreground transition hover:bg-muted hover:text-foreground"
|
|
318
318
|
>
|
|
319
319
|
<RotateCcw className="size-3.5" />
|
|
@@ -322,7 +322,7 @@ export function FileUpload({
|
|
|
322
322
|
<button
|
|
323
323
|
type="button"
|
|
324
324
|
onClick={() => remove(item.id)}
|
|
325
|
-
aria-label={`
|
|
325
|
+
aria-label={`Remove ${item.file.name}`}
|
|
326
326
|
className="grid size-7 place-items-center rounded-md text-muted-foreground transition hover:bg-muted hover:text-foreground"
|
|
327
327
|
>
|
|
328
328
|
<X className="size-3.5" />
|
|
@@ -158,7 +158,7 @@ export function IconGlyph({
|
|
|
158
158
|
const [prevIcon, setPrevIcon] = useState(icon)
|
|
159
159
|
const loadRef = useRef<Promise<void> | null>(null)
|
|
160
160
|
|
|
161
|
-
//
|
|
161
|
+
// Render-time adjustment: an icon already cached is applied with no effect.
|
|
162
162
|
if (prevIcon !== icon) {
|
|
163
163
|
setPrevIcon(icon)
|
|
164
164
|
setData(iconCache.get(icon) ?? null)
|
|
@@ -184,7 +184,7 @@ export function IconPicker({
|
|
|
184
184
|
value,
|
|
185
185
|
color = 'var(--primary)',
|
|
186
186
|
onChange,
|
|
187
|
-
placeholder = '
|
|
187
|
+
placeholder = 'Choose an icon',
|
|
188
188
|
className,
|
|
189
189
|
}: Readonly<IconPickerProps>) {
|
|
190
190
|
const [open, setOpen] = useState(false)
|
|
@@ -206,7 +206,7 @@ export function IconPicker({
|
|
|
206
206
|
`https://api.iconify.design/search?query=${encodeURIComponent(q)}&limit=96&prefixes=${SEARCH_PREFIXES}`,
|
|
207
207
|
{ signal: controller.signal },
|
|
208
208
|
)
|
|
209
|
-
if (!res.ok) throw new Error('
|
|
209
|
+
if (!res.ok) throw new Error('Error while searching icons')
|
|
210
210
|
const data: IconSearchResponse = await res.json()
|
|
211
211
|
setIcons(data.icons ?? [])
|
|
212
212
|
} catch (err) {
|
|
@@ -245,7 +245,7 @@ export function IconPicker({
|
|
|
245
245
|
|
|
246
246
|
return (
|
|
247
247
|
<div className={cn('relative w-full max-w-md', className)}>
|
|
248
|
-
{/*
|
|
248
|
+
{/* Main button */}
|
|
249
249
|
<button
|
|
250
250
|
type="button"
|
|
251
251
|
onClick={() => setOpen((v) => !v)}
|
|
@@ -270,12 +270,12 @@ export function IconPicker({
|
|
|
270
270
|
{/* Picker */}
|
|
271
271
|
{open && (
|
|
272
272
|
<div className="absolute left-0 top-full z-50 mt-2 w-full overflow-hidden rounded-xl border border-border bg-card shadow-xl">
|
|
273
|
-
{/*
|
|
273
|
+
{/* Header */}
|
|
274
274
|
<div className="flex items-center justify-between border-b border-border px-4 py-3">
|
|
275
275
|
<div>
|
|
276
|
-
<p className="text-sm font-medium text-foreground">
|
|
276
|
+
<p className="text-sm font-medium text-foreground">Choose an icon</p>
|
|
277
277
|
<p className="text-xs text-muted-foreground">
|
|
278
|
-
|
|
278
|
+
Search among thousands of icons
|
|
279
279
|
</p>
|
|
280
280
|
</div>
|
|
281
281
|
<button
|
|
@@ -287,7 +287,7 @@ export function IconPicker({
|
|
|
287
287
|
</button>
|
|
288
288
|
</div>
|
|
289
289
|
|
|
290
|
-
{/*
|
|
290
|
+
{/* Search bar */}
|
|
291
291
|
<div className="border-b border-border p-3">
|
|
292
292
|
<div className="relative">
|
|
293
293
|
<Search
|
|
@@ -298,14 +298,14 @@ export function IconPicker({
|
|
|
298
298
|
type="text"
|
|
299
299
|
value={search}
|
|
300
300
|
onChange={(e) => setSearch(e.target.value)}
|
|
301
|
-
placeholder="
|
|
301
|
+
placeholder="Search: shirt, shopping, home..."
|
|
302
302
|
autoFocus
|
|
303
303
|
className="h-10 w-full rounded-lg border border-border bg-muted/50 pl-9 pr-3 text-sm text-foreground outline-none transition placeholder:text-muted-foreground focus:border-primary/60 focus:bg-card"
|
|
304
304
|
/>
|
|
305
305
|
</div>
|
|
306
306
|
</div>
|
|
307
307
|
|
|
308
|
-
{/*
|
|
308
|
+
{/* Icon list */}
|
|
309
309
|
<div className="max-h-36 overflow-y-auto p-3">
|
|
310
310
|
{loading ? (
|
|
311
311
|
<div className="grid grid-cols-[repeat(auto-fill,minmax(2.5rem,1fr))] gap-2">
|
|
@@ -349,10 +349,10 @@ export function IconPicker({
|
|
|
349
349
|
<div className="flex flex-col items-center justify-center py-6 text-center">
|
|
350
350
|
<Search size={26} className="mb-2 text-muted-foreground/50" />
|
|
351
351
|
<p className="text-sm font-medium text-muted-foreground">
|
|
352
|
-
|
|
352
|
+
No icon found
|
|
353
353
|
</p>
|
|
354
354
|
<p className="mt-1 text-xs text-muted-foreground/70">
|
|
355
|
-
|
|
355
|
+
Try another term
|
|
356
356
|
</p>
|
|
357
357
|
</div>
|
|
358
358
|
)}
|
|
@@ -183,7 +183,7 @@ export function ResizableHandle({ className }: Readonly<{ className?: string }>)
|
|
|
183
183
|
}}
|
|
184
184
|
className={cn('group/handle relative shrink-0 outline-none', className)}
|
|
185
185
|
>
|
|
186
|
-
<span className="sr-only">
|
|
186
|
+
<span className="sr-only">Use the arrows to resize</span>
|
|
187
187
|
</div>
|
|
188
188
|
)
|
|
189
189
|
}
|
|
@@ -38,7 +38,7 @@ export function RichTextEditor({
|
|
|
38
38
|
value,
|
|
39
39
|
defaultValue = '',
|
|
40
40
|
onChange,
|
|
41
|
-
placeholder = '
|
|
41
|
+
placeholder = 'Write something…',
|
|
42
42
|
readOnly = false,
|
|
43
43
|
toolbar = true,
|
|
44
44
|
minHeight = 220,
|
|
@@ -115,7 +115,7 @@ export function RichTextEditor({
|
|
|
115
115
|
editor.focus()
|
|
116
116
|
return
|
|
117
117
|
}
|
|
118
|
-
const input = window.prompt('
|
|
118
|
+
const input = window.prompt('Enter link URL', 'https://')
|
|
119
119
|
if (!input) return
|
|
120
120
|
const url = /^(https?:|mailto:|tel:)/i.test(input) ? input : `https://${input}`
|
|
121
121
|
exec('createLink', url)
|
|
@@ -136,21 +136,21 @@ export function RichTextEditor({
|
|
|
136
136
|
>
|
|
137
137
|
{toolbar && (
|
|
138
138
|
<div className="flex flex-wrap items-center gap-0.5 border-b bg-muted/30 p-1.5">
|
|
139
|
-
<ToolButton label="
|
|
139
|
+
<ToolButton label="Bold" active={active.bold} onClick={() => exec('bold')}>
|
|
140
140
|
<Bold className="size-3.5" />
|
|
141
141
|
</ToolButton>
|
|
142
|
-
<ToolButton label="
|
|
142
|
+
<ToolButton label="Italic" active={active.italic} onClick={() => exec('italic')}>
|
|
143
143
|
<Italic className="size-3.5" />
|
|
144
144
|
</ToolButton>
|
|
145
145
|
<ToolButton
|
|
146
|
-
label="
|
|
146
|
+
label="Underline"
|
|
147
147
|
active={active.underline}
|
|
148
148
|
onClick={() => exec('underline')}
|
|
149
149
|
>
|
|
150
150
|
<Underline className="size-3.5" />
|
|
151
151
|
</ToolButton>
|
|
152
152
|
<ToolButton
|
|
153
|
-
label="
|
|
153
|
+
label="Strikethrough"
|
|
154
154
|
active={active.strikeThrough}
|
|
155
155
|
onClick={() => exec('strikeThrough')}
|
|
156
156
|
>
|
|
@@ -159,17 +159,17 @@ export function RichTextEditor({
|
|
|
159
159
|
|
|
160
160
|
<Divider />
|
|
161
161
|
|
|
162
|
-
<ToolButton label="
|
|
162
|
+
<ToolButton label="Heading 2" active={active.h2} onClick={() => toggleBlock('h2')}>
|
|
163
163
|
<Heading2 className="size-3.5" />
|
|
164
164
|
</ToolButton>
|
|
165
|
-
<ToolButton label="
|
|
165
|
+
<ToolButton label="Heading 3" active={active.h3} onClick={() => toggleBlock('h3')}>
|
|
166
166
|
<Heading3 className="size-3.5" />
|
|
167
167
|
</ToolButton>
|
|
168
|
-
<ToolButton label="
|
|
168
|
+
<ToolButton label="Paragraph" onClick={() => toggleBlock('p')}>
|
|
169
169
|
<Pilcrow className="size-3.5" />
|
|
170
170
|
</ToolButton>
|
|
171
171
|
<ToolButton
|
|
172
|
-
label="
|
|
172
|
+
label="Quote"
|
|
173
173
|
active={active.blockquote}
|
|
174
174
|
onClick={() => toggleBlock('blockquote')}
|
|
175
175
|
>
|
|
@@ -182,37 +182,36 @@ export function RichTextEditor({
|
|
|
182
182
|
<Divider />
|
|
183
183
|
|
|
184
184
|
<ToolButton
|
|
185
|
-
label="
|
|
185
|
+
label="Bullet list"
|
|
186
186
|
active={active.insertUnorderedList}
|
|
187
187
|
onClick={() => exec('insertUnorderedList')}
|
|
188
188
|
>
|
|
189
189
|
<List className="size-3.5" />
|
|
190
190
|
</ToolButton>
|
|
191
191
|
<ToolButton
|
|
192
|
-
label="
|
|
192
|
+
label="Numbered list"
|
|
193
193
|
active={active.insertOrderedList}
|
|
194
194
|
onClick={() => exec('insertOrderedList')}
|
|
195
195
|
>
|
|
196
196
|
<ListOrdered className="size-3.5" />
|
|
197
197
|
</ToolButton>
|
|
198
|
-
<ToolButton label="
|
|
198
|
+
<ToolButton label="Link" onClick={addLink}>
|
|
199
199
|
<Link2 className="size-3.5" />
|
|
200
200
|
</ToolButton>
|
|
201
201
|
|
|
202
202
|
<Divider />
|
|
203
203
|
|
|
204
|
-
<ToolButton label="
|
|
204
|
+
<ToolButton label="Undo" onClick={() => exec('undo')}>
|
|
205
205
|
<Undo2 className="size-3.5" />
|
|
206
206
|
</ToolButton>
|
|
207
|
-
<ToolButton label="
|
|
207
|
+
<ToolButton label="Redo" onClick={() => exec('redo')}>
|
|
208
208
|
<Redo2 className="size-3.5" />
|
|
209
209
|
</ToolButton>
|
|
210
|
-
<ToolButton label="
|
|
210
|
+
<ToolButton label="Remove formatting" onClick={() => exec('removeFormat')}>
|
|
211
211
|
<RemoveFormatting className="size-3.5" />
|
|
212
212
|
</ToolButton>
|
|
213
213
|
|
|
214
|
-
<span className="ml-auto px-1.5 text-[10px] tabular-nums text-muted-foreground">
|
|
215
|
-
{characters} car.
|
|
214
|
+
<span className="ml-auto px-1.5 text-[10px] tabular-nums text-muted-foreground">{characters} chars
|
|
216
215
|
</span>
|
|
217
216
|
</div>
|
|
218
217
|
)}
|
|
@@ -51,7 +51,7 @@ export function Stories({
|
|
|
51
51
|
const setOpen = useCallback(
|
|
52
52
|
(next: boolean) => {
|
|
53
53
|
if (next) {
|
|
54
|
-
//
|
|
54
|
+
// Restarts playback at open rather than via an effect.
|
|
55
55
|
elapsedRef.current = 0
|
|
56
56
|
setPaused(false)
|
|
57
57
|
}
|
|
@@ -166,7 +166,7 @@ export function Stories({
|
|
|
166
166
|
aria-modal="true"
|
|
167
167
|
aria-label="Stories"
|
|
168
168
|
className={cn(
|
|
169
|
-
'fixed inset-0 z-
|
|
169
|
+
'fixed inset-0 z-80 flex items-center justify-center bg-black/95 p-0 backdrop-blur-sm sm:p-6',
|
|
170
170
|
className,
|
|
171
171
|
)}
|
|
172
172
|
>
|
|
@@ -204,13 +204,13 @@ export function Stories({
|
|
|
204
204
|
)}
|
|
205
205
|
<div className="min-w-0 flex-1">
|
|
206
206
|
<p className="truncate text-xs font-medium text-white">{item.title ?? 'Stories'}</p>
|
|
207
|
-
<p className="text-[10px] text-white/55"
|
|
207
|
+
<p className="text-[10px] text-white/55">now</p>
|
|
208
208
|
</div>
|
|
209
209
|
<button
|
|
210
210
|
type="button"
|
|
211
211
|
onClick={close}
|
|
212
212
|
onPointerDown={(event) => event.stopPropagation()}
|
|
213
|
-
aria-label="
|
|
213
|
+
aria-label="Close stories"
|
|
214
214
|
className="grid size-8 place-items-center rounded-full bg-black/40 text-white/80 ring-1 ring-white/15 transition hover:bg-black/60 hover:text-white"
|
|
215
215
|
>
|
|
216
216
|
<X className="size-4" />
|