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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "uifolio",
3
- "version": "0.5.1",
4
- "description": "CLI qui copie des composants React (calendrier, lecteurs vidéo/audio, OTP, éditeur, upload) et leurs helpers dans ton projet, avec des tokens Tailwind CSS 4.",
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 ? 'Mettre en pause' : 'Lire'}
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="Position de lecture"
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 ? 'Mettre en pause' : 'Lire'}
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="Position de lecture"
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 ? 'Activer le son' : 'Couper le son'}
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={`Vitesse ${rate}×`}
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 indisponible</span>}
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">Début (jj/mm/aaaa)</span>
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="du jj/mm/aaaa"
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">Fin (jj/mm/aaaa)</span>
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="au jj/mm/aaaa"
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="Mois précédent"
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="Ouvrir le sélecteur de couleur"
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
- {displayFormat}
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="Valeur de la couleur"
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="Personnaliser la couleur"
353
- title="Personnaliser la couleur"
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="Sélecteur de couleur"
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-[100] overflow-y-auto rounded-md border bg-card text-card-foreground shadow-2xl outline-none"
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">Sélecteur de couleur</p>
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="Fermer le sélecteur de couleur"
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 et luminosité"
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>Teinte</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="Teinte"
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="Transparence"
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="Prélever une couleur à l’écran"
534
- aria-label="Prélever une couleur à l’écran"
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={`Couleur ${raw}`}
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
- La transparence est incluse dans la valeur exportée.
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 = 'Aucun résultat.',
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-[70] flex items-start justify-center bg-black/65 px-4 pt-[12vh] backdrop-blur-sm"
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="Palette de commandes"
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>↑↓ naviguer</span>
215
- <span>↵ ouvrir</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 publique renvoyée par l'uploader (upload réel). */
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
- /** Renvoie l'URL publique du fichier (upload réel) ou void. */
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} dépasse ${maxSizeMb} Mo`)
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} n'est pas un type accepté`)
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 ? 'Dépose ici' : 'Glisse tes fichiers ou clique'}
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(',', ' · ') : 'Tous types'} — jusqu'à {maxSizeMb} Mo
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="Masquer l'erreur"
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 ?? 'Envoyé'}
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 ? `Ouvrir ${item.file.name}` : 'Envoyé'}
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="Réessayer"
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={`Retirer ${item.file.name}`}
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
- // Ajustement pendant le rendu : une icône déjà en cache s'applique sans effet.
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 = 'Choisir une icône',
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('Erreur lors de la recherche des icônes')
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
- {/* Bouton principal */}
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
- {/* En-tête */}
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">Choisir une icône</p>
276
+ <p className="text-sm font-medium text-foreground">Choose an icon</p>
277
277
  <p className="text-xs text-muted-foreground">
278
- Recherchez parmi des milliers d'icônes
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
- {/* Barre de recherche */}
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="Rechercher : shirt, shopping, home..."
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
- {/* Liste d'icônes */}
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
- Aucune icône trouvée
352
+ No icon found
353
353
  </p>
354
354
  <p className="mt-1 text-xs text-muted-foreground/70">
355
- Essayez un autre terme
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">Utilise les flèches pour redimensionner</span>
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 = 'Écris quelque chose…',
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('URL du lien', 'https://')
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="Gras" active={active.bold} onClick={() => exec('bold')}>
139
+ <ToolButton label="Bold" active={active.bold} onClick={() => exec('bold')}>
140
140
  <Bold className="size-3.5" />
141
141
  </ToolButton>
142
- <ToolButton label="Italique" active={active.italic} onClick={() => exec('italic')}>
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="Souligné"
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="Barré"
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="Titre 2" active={active.h2} onClick={() => toggleBlock('h2')}>
162
+ <ToolButton label="Heading 2" active={active.h2} onClick={() => toggleBlock('h2')}>
163
163
  <Heading2 className="size-3.5" />
164
164
  </ToolButton>
165
- <ToolButton label="Titre 3" active={active.h3} onClick={() => toggleBlock('h3')}>
165
+ <ToolButton label="Heading 3" active={active.h3} onClick={() => toggleBlock('h3')}>
166
166
  <Heading3 className="size-3.5" />
167
167
  </ToolButton>
168
- <ToolButton label="Paragraphe" onClick={() => toggleBlock('p')}>
168
+ <ToolButton label="Paragraph" onClick={() => toggleBlock('p')}>
169
169
  <Pilcrow className="size-3.5" />
170
170
  </ToolButton>
171
171
  <ToolButton
172
- label="Citation"
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="Liste à puces"
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="Liste numérotée"
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="Lien" onClick={addLink}>
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="Annuler" onClick={() => exec('undo')}>
204
+ <ToolButton label="Undo" onClick={() => exec('undo')}>
205
205
  <Undo2 className="size-3.5" />
206
206
  </ToolButton>
207
- <ToolButton label="Rétablir" onClick={() => exec('redo')}>
207
+ <ToolButton label="Redo" onClick={() => exec('redo')}>
208
208
  <Redo2 className="size-3.5" />
209
209
  </ToolButton>
210
- <ToolButton label="Effacer la mise en forme" onClick={() => exec('removeFormat')}>
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
- // Réinitialise la lecture à l'ouverture plutôt que par un effet.
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-[80] flex items-center justify-center bg-black/95 p-0 backdrop-blur-sm sm:p-6',
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">à l'instant</p>
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="Fermer les stories"
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" />