@r2digisolutions/components 0.6.3 → 0.6.7

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.
@@ -0,0 +1,22 @@
1
+ import type { StoryObj } from '@storybook/svelte';
2
+ declare const meta: {
3
+ title: string;
4
+ component: import("svelte").Component<{
5
+ size?: import("./Dock.svelte.ts").DockSize;
6
+ }, {}, "">;
7
+ tags: string[];
8
+ argTypes: {
9
+ size: {
10
+ control: "select";
11
+ options: string[];
12
+ };
13
+ };
14
+ args: {
15
+ size: "sm";
16
+ };
17
+ };
18
+ export default meta;
19
+ type Story = StoryObj<typeof meta>;
20
+ export declare const Default: Story;
21
+ export declare const Mini: Story;
22
+ export declare const Medium: Story;
@@ -0,0 +1,14 @@
1
+ import DockStory from './DockStory.svelte';
2
+ const meta = {
3
+ title: 'Molecules/Dock',
4
+ component: DockStory,
5
+ tags: ['autodocs'],
6
+ argTypes: {
7
+ size: { control: 'select', options: ['mini', 'sm', 'md'] }
8
+ },
9
+ args: { size: 'sm' }
10
+ };
11
+ export default meta;
12
+ export const Default = {};
13
+ export const Mini = { args: { size: 'mini' } };
14
+ export const Medium = { args: { size: 'md' } };
@@ -0,0 +1,29 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+
4
+ export type DockSize = 'mini' | 'sm' | 'md';
5
+
6
+ interface DockProps {
7
+ size?: DockSize;
8
+ class?: string;
9
+ children?: Snippet;
10
+ }
11
+
12
+ let { size = 'mini', class: className = '', children }: DockProps = $props();
13
+
14
+ const sizes: Record<DockSize, string> = {
15
+ mini: 'rounded-full p-0.5',
16
+ sm: 'rounded-full p-0.5',
17
+ md: 'rounded-2xl p-1'
18
+ };
19
+ </script>
20
+
21
+ <div
22
+ class={[
23
+ 'border-border/70 bg-surface-overlay/80 shadow-sm backdrop-blur-md isolate inline-flex items-center overflow-hidden border',
24
+ sizes[size],
25
+ className
26
+ ]}
27
+ >
28
+ {@render children?.()}
29
+ </div>
@@ -0,0 +1,10 @@
1
+ import type { Snippet } from 'svelte';
2
+ export type DockSize = 'mini' | 'sm' | 'md';
3
+ interface DockProps {
4
+ size?: DockSize;
5
+ class?: string;
6
+ children?: Snippet;
7
+ }
8
+ declare const Dock: import("svelte").Component<DockProps, {}, "">;
9
+ type Dock = ReturnType<typeof Dock>;
10
+ export default Dock;
@@ -0,0 +1,69 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import type { DockSize } from './Dock.svelte';
4
+
5
+ interface DockItemProps {
6
+ as?: 'button' | 'a';
7
+ href?: string;
8
+ onclick?: (e: MouseEvent) => void;
9
+ disabled?: boolean;
10
+ active?: boolean;
11
+ size?: DockSize;
12
+ ariaLabel?: string;
13
+ class?: string;
14
+ children?: Snippet;
15
+ }
16
+
17
+ let {
18
+ as = 'button',
19
+ href,
20
+ onclick,
21
+ disabled = false,
22
+ active = false,
23
+ size = 'mini',
24
+ ariaLabel,
25
+ class: className = '',
26
+ children
27
+ }: DockItemProps = $props();
28
+
29
+ const sizes: Record<DockSize, string> = {
30
+ mini: 'h-7 px-2 text-[11px]',
31
+ sm: 'h-8 px-2.5 text-xs',
32
+ md: 'h-9 px-3 text-sm'
33
+ };
34
+
35
+ const classes = $derived([
36
+ 'relative inline-flex items-center justify-center gap-1.5 whitespace-nowrap select-none font-medium transition-all duration-150',
37
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500/30',
38
+ sizes[size],
39
+ active
40
+ ? 'bg-surface-elevated text-primary shadow-sm ring-1 ring-border'
41
+ : 'text-secondary hover:bg-surface-elevated/70 hover:text-primary',
42
+ disabled ? 'pointer-events-none opacity-50' : 'cursor-pointer',
43
+ '[&[data-active]:first-child]:rounded-l-full',
44
+ '[&[data-active]:last-child]:rounded-r-full',
45
+ '[&[data-active]:not(:first-child):not(:last-child)]:rounded-none',
46
+ '[&:not([data-active]):first-child:hover]:rounded-l-full',
47
+ '[&:not([data-active]):last-child:hover]:rounded-r-full',
48
+ '[&:not([data-active]):not(:first-child):not(:last-child):hover]:rounded-none',
49
+ className
50
+ ]);
51
+ </script>
52
+
53
+ {#if as === 'a' || href}
54
+ <a {href} aria-label={ariaLabel} data-active={active || undefined} class={classes}>
55
+ {@render children?.()}
56
+ </a>
57
+ {:else}
58
+ <button
59
+ type="button"
60
+ {disabled}
61
+ aria-label={ariaLabel}
62
+ aria-pressed={active}
63
+ data-active={active || undefined}
64
+ {onclick}
65
+ class={classes}
66
+ >
67
+ {@render children?.()}
68
+ </button>
69
+ {/if}
@@ -0,0 +1,16 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { DockSize } from './Dock.svelte';
3
+ interface DockItemProps {
4
+ as?: 'button' | 'a';
5
+ href?: string;
6
+ onclick?: (e: MouseEvent) => void;
7
+ disabled?: boolean;
8
+ active?: boolean;
9
+ size?: DockSize;
10
+ ariaLabel?: string;
11
+ class?: string;
12
+ children?: Snippet;
13
+ }
14
+ declare const DockItem: import("svelte").Component<DockItemProps, {}, "">;
15
+ type DockItem = ReturnType<typeof DockItem>;
16
+ export default DockItem;
@@ -0,0 +1,25 @@
1
+ <script lang="ts">
2
+ import Dock from './Dock.svelte';
3
+ import DockItem from './DockItem.svelte';
4
+ import type { DockSize } from './Dock.svelte';
5
+
6
+ let { size = 'sm' }: { size?: DockSize } = $props();
7
+
8
+ let value = $state('es');
9
+ const langs = ['es', 'en', 'ca', 'fr', 'de'];
10
+ const filled = new Set(['es', 'en']);
11
+ </script>
12
+
13
+ <div class="gap-4 flex flex-col">
14
+ <Dock {size}>
15
+ {#each langs as lang (lang)}
16
+ <DockItem {size} active={value === lang} onclick={() => (value = lang)}>
17
+ {lang.toUpperCase()}
18
+ {#if filled.has(lang)}
19
+ <span class="h-1.5 w-1.5 bg-emerald-500 rounded-full" aria-hidden="true"></span>
20
+ {/if}
21
+ </DockItem>
22
+ {/each}
23
+ </Dock>
24
+ <p class="text-xs text-muted">Selected: {value.toUpperCase()}</p>
25
+ </div>
@@ -0,0 +1,7 @@
1
+ import type { DockSize } from './Dock.svelte';
2
+ type $$ComponentProps = {
3
+ size?: DockSize;
4
+ };
5
+ declare const DockStory: import("svelte").Component<$$ComponentProps, {}, "">;
6
+ type DockStory = ReturnType<typeof DockStory>;
7
+ export default DockStory;
@@ -2,7 +2,7 @@ import type { StoryObj } from '@storybook/svelte';
2
2
  declare const meta: {
3
3
  title: string;
4
4
  component: import("svelte").Component<{
5
- example?: "destructive" | "toolbar" | "media" | "compose";
5
+ example?: "destructive" | "media" | "toolbar" | "compose";
6
6
  position?: import("./SpeedDial.svelte.ts").SpeedDialPosition;
7
7
  direction?: import("./SpeedDial.svelte.ts").SpeedDialDirection;
8
8
  variant?: import("./SpeedDial.svelte.ts").SpeedDialVariant;
@@ -77,7 +77,7 @@ declare function $$render<T extends Record<string, unknown> = Record<string, unk
77
77
  onrowdblclick?: (row: T) => void;
78
78
  };
79
79
  exports: {};
80
- bindings: "notes" | "selection" | "marks" | "formatRules" | "filterQuery" | "columnFilters" | "expandedRows" | "viewMode";
80
+ bindings: "viewMode" | "notes" | "selection" | "marks" | "formatRules" | "filterQuery" | "columnFilters" | "expandedRows";
81
81
  slots: {};
82
82
  events: {};
83
83
  };
@@ -85,7 +85,7 @@ declare class __sveltets_Render<T extends Record<string, unknown> = Record<strin
85
85
  props(): ReturnType<typeof $$render<T>>['props'];
86
86
  events(): ReturnType<typeof $$render<T>>['events'];
87
87
  slots(): ReturnType<typeof $$render<T>>['slots'];
88
- bindings(): "notes" | "selection" | "marks" | "formatRules" | "filterQuery" | "columnFilters" | "expandedRows" | "viewMode";
88
+ bindings(): "viewMode" | "notes" | "selection" | "marks" | "formatRules" | "filterQuery" | "columnFilters" | "expandedRows";
89
89
  exports(): {};
90
90
  }
91
91
  interface $$IsomorphicComponent {
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { onDestroy } from 'svelte';
3
+ import { i18n } from '../../../utils/i18n.svelte.js';
3
4
 
4
5
  type FileKind = 'image' | 'video' | 'audio' | 'pdf' | 'doc' | 'other';
5
6
  type UploaderVariant = 'multiple' | 'single' | 'avatar';
@@ -50,8 +51,8 @@
50
51
 
51
52
  let {
52
53
  id = `upload-${Math.random().toString(36).slice(2, 9)}`,
53
- label = 'Upload files',
54
- helperText = 'SVG, PNG, JPG or GIF (max. 10MB)',
54
+ label,
55
+ helperText,
55
56
  accept = 'image/*',
56
57
  variant = 'multiple',
57
58
  view = $bindable('list'),
@@ -75,13 +76,13 @@
75
76
 
76
77
  const isMultiple = $derived(variant === 'multiple');
77
78
  const hasFile = $derived(fileList.length > 0);
79
+ const resolvedLabel = $derived(label ?? i18n.t('uploadFiles'));
80
+ const resolvedHelperText = $derived(helperText ?? i18n.t('uploadHelper'));
78
81
  const primaryFile = $derived(fileList[0] as FileItem | undefined);
79
82
  const externalSrc = $derived(src?.trim() || '');
80
- const showSinglePreview = $derived(
81
- variant === 'single' && (!!primaryFile || !!externalSrc)
82
- );
83
+ const showSinglePreview = $derived(variant === 'single' && (!!primaryFile || !!externalSrc));
83
84
  const singlePreviewUrl = $derived(primaryFile?.previewUrl || externalSrc);
84
- const singlePreviewName = $derived(primaryFile?.name || srcName || 'Media');
85
+ const singlePreviewName = $derived(primaryFile?.name || srcName || i18n.t('media'));
85
86
  const singlePreviewMeta = $derived(primaryFile?.sizeFormatted || '');
86
87
 
87
88
  const labelId = $derived(`${id}-label`);
@@ -103,7 +104,8 @@
103
104
  primaryFile?.kind === 'audio' ||
104
105
  (!primaryFile &&
105
106
  !!externalSrc &&
106
- (resolvedAccept.includes('audio') || /\.(mp3|wav|ogg|m4a|aac|flac)(\?|$)/i.test(externalSrc)))
107
+ (resolvedAccept.includes('audio') ||
108
+ /\.(mp3|wav|ogg|m4a|aac|flac)(\?|$)/i.test(externalSrc)))
107
109
  );
108
110
  const dropLayout = $derived(layout === 'compact' ? 'compact' : layout);
109
111
 
@@ -142,9 +144,12 @@
142
144
  function kindBadgeClass(kind: FileKind) {
143
145
  if (kind === 'pdf') return 'bg-red-50 text-red-600 dark:bg-red-950/50 dark:text-red-300';
144
146
  if (kind === 'doc') return 'bg-sky-50 text-sky-700 dark:bg-sky-950/50 dark:text-sky-300';
145
- if (kind === 'image') return 'bg-brand-50 text-brand-700 dark:bg-brand-950/50 dark:text-brand-300';
146
- if (kind === 'video') return 'bg-violet-50 text-violet-700 dark:bg-violet-950/50 dark:text-violet-300';
147
- if (kind === 'audio') return 'bg-emerald-50 text-emerald-700 dark:bg-emerald-950/50 dark:text-emerald-300';
147
+ if (kind === 'image')
148
+ return 'bg-brand-50 text-brand-700 dark:bg-brand-950/50 dark:text-brand-300';
149
+ if (kind === 'video')
150
+ return 'bg-violet-50 text-violet-700 dark:bg-violet-950/50 dark:text-violet-300';
151
+ if (kind === 'audio')
152
+ return 'bg-emerald-50 text-emerald-700 dark:bg-emerald-950/50 dark:text-emerald-300';
148
153
  return 'bg-surface-overlay text-secondary';
149
154
  }
150
155
 
@@ -180,15 +185,15 @@
180
185
 
181
186
  for (const file of incoming) {
182
187
  if (variant === 'avatar' && !file.type.startsWith('image/')) {
183
- rejected.push(`${file.name}: se requiere una imagen`);
188
+ rejected.push(i18n.t('imageRequired', { name: file.name }));
184
189
  continue;
185
190
  }
186
191
  if (!matchesAccept(file)) {
187
- rejected.push(`${file.name}: tipo no permitido`);
192
+ rejected.push(i18n.t('typeNotAllowed', { name: file.name }));
188
193
  continue;
189
194
  }
190
195
  if (file.size > maxBytes) {
191
- rejected.push(`${file.name}: supera ${maxSizeMb}MB`);
196
+ rejected.push(i18n.t('exceedsMaxSize', { name: file.name, max: maxSizeMb }));
192
197
  continue;
193
198
  }
194
199
 
@@ -296,7 +301,7 @@
296
301
  {#snippet fileBadge(kind: FileKind, className = '')}
297
302
  <div
298
303
  class={[
299
- 'flex items-center justify-center text-[10px] font-bold uppercase tracking-wide',
304
+ 'font-bold tracking-wide flex items-center justify-center text-[10px] uppercase',
300
305
  kindBadgeClass(kind),
301
306
  className
302
307
  ]}
@@ -313,8 +318,8 @@
313
318
  <div
314
319
  role="button"
315
320
  tabindex={disabled ? -1 : 0}
316
- aria-labelledby={label ? labelId : undefined}
317
- aria-describedby={[helperText ? helperId : '', errorMessage ? errorId : '']
321
+ aria-labelledby={resolvedLabel ? labelId : undefined}
322
+ aria-describedby={[resolvedHelperText ? helperId : '', errorMessage ? errorId : '']
318
323
  .filter(Boolean)
319
324
  .join(' ') || undefined}
320
325
  aria-disabled={disabled || undefined}
@@ -332,27 +337,27 @@
332
337
  onclick={openPicker}
333
338
  class={[
334
339
  'relative overflow-hidden border border-dashed transition-colors duration-150 outline-none',
335
- 'focus-visible:ring-2 focus-visible:ring-brand-500/30 focus-visible:border-brand-500',
340
+ 'focus-visible:ring-brand-500/30 focus-visible:border-brand-500 focus-visible:ring-2',
336
341
  'rounded-xl',
337
342
  isDragging
338
343
  ? 'border-brand-500 bg-brand-500/5 dark:bg-brand-950/40'
339
344
  : 'border-border bg-surface-elevated hover:border-border-strong hover:bg-surface-overlay/60',
340
- disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'
345
+ disabled ? 'cursor-not-allowed opacity-50' : 'cursor-pointer'
341
346
  ]}
342
347
  >
343
348
  <div
344
349
  class={[
345
350
  'flex',
346
351
  vertical
347
- ? 'flex-col items-center gap-2 px-3 py-5 text-center'
352
+ ? 'gap-2 px-3 py-5 flex-col items-center text-center'
348
353
  : compact
349
- ? 'flex-row items-center gap-3 px-3 py-2.5'
350
- : 'flex-row items-center gap-4 px-4 py-5 sm:px-5'
354
+ ? 'gap-3 px-3 py-2.5 flex-row items-center'
355
+ : 'gap-4 px-4 py-5 sm:px-5 flex-row items-center'
351
356
  ]}
352
357
  >
353
358
  <div
354
359
  class={[
355
- 'flex shrink-0 items-center justify-center rounded-lg transition-colors duration-150',
360
+ 'rounded-lg flex shrink-0 items-center justify-center transition-colors duration-150',
356
361
  vertical ? 'h-10 w-10' : compact ? 'h-8 w-8' : 'h-11 w-11',
357
362
  isDragging
358
363
  ? 'bg-brand-600 text-white'
@@ -384,47 +389,44 @@
384
389
  ]}
385
390
  >
386
391
  {#if isDragging}
387
- Drop files to upload
392
+ {i18n.t('dropToUpload')}
388
393
  {:else}
389
- Drop files or
390
- <span class="text-brand-700 dark:text-brand-300"> browse</span>
394
+ {i18n.t('dropOrBrowse')}
395
+ <span class="text-brand-700 dark:text-brand-300"> {i18n.t('browse')}</span>
391
396
  {/if}
392
397
  </p>
393
- {#if helperText}
398
+ {#if resolvedHelperText}
394
399
  <p
395
400
  id={helperId}
396
- class={[
397
- 'mt-0.5 text-[11px] leading-relaxed text-secondary',
398
- vertical && 'text-center'
399
- ]}
401
+ class={['mt-0.5 leading-relaxed text-secondary text-[11px]', vertical && 'text-center']}
400
402
  >
401
- {helperText}
403
+ {resolvedHelperText}
402
404
  </p>
403
405
  {/if}
404
406
  </div>
405
407
 
406
408
  {#if !disabled && !compact && !vertical}
407
409
  <span
408
- class="hidden sm:inline-flex shrink-0 items-center rounded-lg border border-border bg-surface-elevated px-3 py-1.5 text-xs font-medium text-primary pointer-events-none"
410
+ class="sm:inline-flex rounded-lg border-border bg-surface-elevated px-3 py-1.5 text-xs font-medium text-primary pointer-events-none hidden shrink-0 items-center border"
409
411
  >
410
- Browse
412
+ {i18n.t('browseButton')}
411
413
  </span>
412
414
  {/if}
413
415
  </div>
414
416
 
415
417
  {#if isDragging}
416
418
  <div
417
- class="pointer-events-none absolute inset-0 ring-2 ring-inset ring-brand-500/40 rounded-xl"
419
+ class="inset-0 ring-brand-500/40 rounded-xl pointer-events-none absolute ring-2 ring-inset"
418
420
  aria-hidden="true"
419
421
  ></div>
420
422
  {/if}
421
423
  </div>
422
424
  {/snippet}
423
425
 
424
- <div class={['flex flex-col gap-3 w-full', className]}>
426
+ <div class={['gap-3 flex w-full flex-col', className]}>
425
427
  <input
426
428
  bind:this={fileInputNode}
427
- id={id}
429
+ {id}
428
430
  type="file"
429
431
  accept={resolvedAccept}
430
432
  multiple={isMultiple}
@@ -435,31 +437,35 @@
435
437
  aria-hidden="true"
436
438
  />
437
439
 
438
- {#if label}
439
- <div class="flex items-center justify-between gap-3">
440
- <div id={labelId} class="text-sm font-medium text-primary">{label}</div>
440
+ {#if resolvedLabel}
441
+ <div class="gap-3 flex items-center justify-between">
442
+ <div id={labelId} class="text-sm font-medium text-primary">{resolvedLabel}</div>
441
443
 
442
444
  {#if isMultiple && hasFile}
443
- <div class="flex items-center gap-2">
445
+ <div class="gap-2 flex items-center">
444
446
  {#if showViewToggle}
445
447
  <div
446
- class="inline-flex items-center rounded-lg border border-border bg-surface-elevated p-0.5"
448
+ class="rounded-lg border-border bg-surface-elevated p-0.5 inline-flex items-center border"
447
449
  role="group"
448
- aria-label="View mode"
450
+ aria-label={i18n.t('viewMode')}
449
451
  >
450
452
  <button
451
453
  type="button"
452
454
  onclick={() => (view = 'list')}
453
455
  class={[
454
456
  'rounded-md p-1.5 transition-colors',
455
- view === 'list'
456
- ? 'bg-brand-600 text-white'
457
- : 'text-secondary hover:text-primary'
457
+ view === 'list' ? 'bg-brand-600 text-white' : 'text-secondary hover:text-primary'
458
458
  ]}
459
459
  aria-pressed={view === 'list'}
460
- aria-label="List view"
460
+ aria-label={i18n.t('listView')}
461
461
  >
462
- <svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
462
+ <svg
463
+ class="h-3.5 w-3.5"
464
+ viewBox="0 0 24 24"
465
+ fill="none"
466
+ stroke="currentColor"
467
+ stroke-width="2"
468
+ >
463
469
  <path stroke-linecap="round" d="M4 6h16M4 12h16M4 18h16" />
464
470
  </svg>
465
471
  </button>
@@ -468,17 +474,13 @@
468
474
  onclick={() => (view = 'grid')}
469
475
  class={[
470
476
  'rounded-md p-1.5 transition-colors',
471
- view === 'grid'
472
- ? 'bg-brand-600 text-white'
473
- : 'text-secondary hover:text-primary'
477
+ view === 'grid' ? 'bg-brand-600 text-white' : 'text-secondary hover:text-primary'
474
478
  ]}
475
479
  aria-pressed={view === 'grid'}
476
- aria-label="Grid view"
480
+ aria-label={i18n.t('gridView')}
477
481
  >
478
482
  <svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="currentColor">
479
- <path
480
- d="M4 4h6v6H4V4zm10 0h6v6h-6V4zM4 14h6v6H4v-6zm10 0h6v6h-6v-6z"
481
- />
483
+ <path d="M4 4h6v6H4V4zm10 0h6v6h-6V4zM4 14h6v6H4v-6zm10 0h6v6h-6v-6z" />
482
484
  </svg>
483
485
  </button>
484
486
  </div>
@@ -490,7 +492,7 @@
490
492
  onclick={clearAll}
491
493
  class="text-xs font-medium text-secondary hover:text-primary transition-colors"
492
494
  >
493
- Clear all
495
+ {i18n.t('clearAll')}
494
496
  </button>
495
497
  {/if}
496
498
  </div>
@@ -500,13 +502,13 @@
500
502
 
501
503
  <!-- Avatar variant -->
502
504
  {#if variant === 'avatar'}
503
- <div class="flex items-center gap-4">
505
+ <div class="gap-4 flex items-center">
504
506
  <!-- svelte-ignore a11y_no_static_element_interactions -->
505
507
  <div
506
508
  role="button"
507
509
  tabindex={disabled ? -1 : 0}
508
- aria-labelledby={label ? labelId : undefined}
509
- aria-describedby={helperText ? helperId : undefined}
510
+ aria-labelledby={resolvedLabel ? labelId : undefined}
511
+ aria-describedby={resolvedHelperText ? helperId : undefined}
510
512
  aria-disabled={disabled || undefined}
511
513
  ondragenter={handleDragEnter}
512
514
  ondragover={handleDragOver}
@@ -521,14 +523,14 @@
521
523
  }}
522
524
  onclick={openPicker}
523
525
  class={[
524
- 'relative group h-24 w-24 shrink-0 rounded-full border-2 border-dashed overflow-hidden transition-colors outline-none',
525
- 'focus-visible:ring-2 focus-visible:ring-brand-500/30 focus-visible:border-brand-500',
526
+ 'group h-24 w-24 relative shrink-0 overflow-hidden rounded-full border-2 border-dashed transition-colors outline-none',
527
+ 'focus-visible:ring-brand-500/30 focus-visible:border-brand-500 focus-visible:ring-2',
526
528
  isDragging
527
529
  ? 'border-brand-500 bg-brand-500/10'
528
530
  : hasFile || externalSrc
529
531
  ? 'border-transparent'
530
532
  : 'border-border bg-surface-elevated hover:border-border-strong',
531
- disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'
533
+ disabled ? 'cursor-not-allowed opacity-50' : 'cursor-pointer'
532
534
  ]}
533
535
  >
534
536
  {#if primaryFile?.previewUrl || externalSrc}
@@ -539,13 +541,19 @@
539
541
  />
540
542
  {#if !disabled}
541
543
  <div
542
- class="absolute inset-0 flex items-center justify-center bg-black/45 opacity-0 transition-opacity group-hover:opacity-100 group-focus-visible:opacity-100"
544
+ class="inset-0 bg-black/45 absolute flex items-center justify-center opacity-0 transition-opacity group-hover:opacity-100 group-focus-visible:opacity-100"
543
545
  >
544
546
  <span
545
- class="inline-flex h-8 w-8 items-center justify-center rounded-full bg-white text-primary shadow-sm"
547
+ class="h-8 w-8 bg-white text-primary shadow-sm inline-flex items-center justify-center rounded-full"
546
548
  aria-hidden="true"
547
549
  >
548
- <svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
550
+ <svg
551
+ class="h-4 w-4"
552
+ fill="none"
553
+ viewBox="0 0 24 24"
554
+ stroke="currentColor"
555
+ stroke-width="2"
556
+ >
549
557
  <path
550
558
  stroke-linecap="round"
551
559
  stroke-linejoin="round"
@@ -556,115 +564,135 @@
556
564
  </div>
557
565
  {/if}
558
566
  {:else}
559
- <div class="flex h-full w-full flex-col items-center justify-center gap-1 text-secondary">
560
- <svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.75">
567
+ <div class="gap-1 text-secondary flex h-full w-full flex-col items-center justify-center">
568
+ <svg
569
+ class="h-6 w-6"
570
+ fill="none"
571
+ viewBox="0 0 24 24"
572
+ stroke="currentColor"
573
+ stroke-width="1.75"
574
+ >
561
575
  <path
562
576
  stroke-linecap="round"
563
577
  stroke-linejoin="round"
564
578
  d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0"
565
579
  />
566
580
  </svg>
567
- <span class="text-[10px] font-medium">Upload</span>
581
+ <span class="font-medium text-[10px]">{i18n.t('upload')}</span>
568
582
  </div>
569
583
  {/if}
570
584
  </div>
571
585
 
572
586
  <div class="min-w-0 flex-1">
573
587
  {#if primaryFile}
574
- <p class="truncate text-sm font-medium text-primary">{primaryFile.name}</p>
588
+ <p class="text-sm font-medium text-primary truncate">{primaryFile.name}</p>
575
589
  <p class="text-xs text-secondary">{primaryFile.sizeFormatted}</p>
576
- <div class="mt-2 flex items-center gap-2">
590
+ <div class="mt-2 gap-2 flex items-center">
577
591
  <button
578
592
  type="button"
579
593
  onclick={openPicker}
580
- disabled={disabled}
581
- class="inline-flex items-center gap-1.5 rounded-lg border border-border bg-surface-elevated px-2.5 py-1.5 text-xs font-medium text-primary transition-colors hover:bg-surface-overlay disabled:opacity-50"
594
+ {disabled}
595
+ class="gap-1.5 rounded-lg border-border bg-surface-elevated px-2.5 py-1.5 text-xs font-medium text-primary hover:bg-surface-overlay inline-flex items-center border transition-colors disabled:opacity-50"
582
596
  >
583
- <svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
597
+ <svg
598
+ class="h-3.5 w-3.5"
599
+ fill="none"
600
+ viewBox="0 0 24 24"
601
+ stroke="currentColor"
602
+ stroke-width="2"
603
+ >
584
604
  <path
585
605
  stroke-linecap="round"
586
606
  stroke-linejoin="round"
587
607
  d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931z"
588
608
  />
589
609
  </svg>
590
- Change
610
+ {i18n.t('change')}
591
611
  </button>
592
612
  <button
593
613
  type="button"
594
614
  onclick={() => primaryFile && removeFile(primaryFile.id)}
595
- disabled={disabled}
596
- class="inline-flex items-center rounded-lg px-2.5 py-1.5 text-xs font-medium text-red-600 transition-colors hover:bg-red-50 dark:text-red-400 dark:hover:bg-red-950/40 disabled:opacity-50"
615
+ {disabled}
616
+ class="rounded-lg px-2.5 py-1.5 text-xs font-medium text-red-600 hover:bg-red-50 dark:text-red-400 dark:hover:bg-red-950/40 inline-flex items-center transition-colors disabled:opacity-50"
597
617
  >
598
- Remove
618
+ {i18n.t('remove')}
599
619
  </button>
600
620
  </div>
601
621
  {:else}
602
- <p class="text-sm font-medium text-primary">Add a profile photo</p>
603
- {#if helperText}
604
- <p id={helperId} class="mt-0.5 text-xs text-secondary">{helperText}</p>
622
+ <p class="text-sm font-medium text-primary">{i18n.t('addProfilePhoto')}</p>
623
+ {#if resolvedHelperText}
624
+ <p id={helperId} class="mt-0.5 text-xs text-secondary">{resolvedHelperText}</p>
605
625
  {/if}
606
626
  {/if}
607
627
  </div>
608
628
  </div>
609
629
 
610
- <!-- Single variant: preview card with edit -->
630
+ <!-- Single variant: preview card with edit -->
611
631
  {:else if showSinglePreview}
612
632
  <div
613
633
  class={[
614
- 'overflow-hidden rounded-xl border border-border bg-surface-elevated shadow-sm',
615
- isDragging && 'ring-2 ring-brand-500/40'
634
+ 'rounded-xl border-border bg-surface-elevated shadow-sm overflow-hidden border',
635
+ isDragging && 'ring-brand-500/40 ring-2'
616
636
  ]}
617
637
  ondragenter={handleDragEnter}
618
638
  ondragover={handleDragOver}
619
639
  ondragleave={handleDragLeave}
620
640
  ondrop={handleDrop}
621
641
  role="group"
622
- aria-labelledby={label ? labelId : undefined}
642
+ aria-labelledby={resolvedLabel ? labelId : undefined}
623
643
  >
624
- <div class="relative aspect-video bg-surface-overlay">
644
+ <div class="aspect-video bg-surface-overlay relative">
625
645
  {#if singlePreviewUrl && singleIsVideo}
626
- <video
627
- src={singlePreviewUrl}
628
- class="h-full w-full object-cover"
629
- muted
630
- playsinline
646
+ <video src={singlePreviewUrl} class="h-full w-full object-cover" muted playsinline
631
647
  ></video>
632
648
  {:else if singlePreviewUrl && singleIsAudio}
633
- <div class="flex h-full w-full flex-col items-center justify-center gap-3 px-4">
649
+ <div class="gap-3 px-4 flex h-full w-full flex-col items-center justify-center">
634
650
  {@render fileBadge('audio', 'h-14 w-14 rounded-xl text-xs')}
635
- <audio src={singlePreviewUrl} controls class="w-full max-w-xs"></audio>
651
+ <audio src={singlePreviewUrl} controls class="max-w-xs w-full"></audio>
636
652
  </div>
637
653
  {:else if singlePreviewUrl}
638
654
  <img src={singlePreviewUrl} alt="" class="h-full w-full object-cover" />
639
655
  {:else}
640
- <div class="flex h-full w-full flex-col items-center justify-center gap-2">
656
+ <div class="gap-2 flex h-full w-full flex-col items-center justify-center">
641
657
  {@render fileBadge(primaryFile?.kind ?? 'other', 'h-14 w-14 rounded-xl text-xs')}
642
658
  </div>
643
659
  {/if}
644
660
 
645
661
  {#if !disabled}
646
- <div class="absolute right-3 top-3 flex items-center gap-1.5">
662
+ <div class="right-3 top-3 gap-1.5 absolute flex items-center">
647
663
  <button
648
664
  type="button"
649
665
  onclick={openPicker}
650
- class="inline-flex items-center gap-1.5 rounded-lg bg-white/95 px-2.5 py-1.5 text-xs font-medium text-primary shadow-sm backdrop-blur transition hover:bg-white dark:bg-slate-900/95 dark:hover:bg-slate-900"
666
+ class="gap-1.5 rounded-lg bg-white/95 px-2.5 py-1.5 text-xs font-medium text-primary shadow-sm backdrop-blur hover:bg-white dark:bg-slate-900/95 dark:hover:bg-slate-900 inline-flex items-center transition"
651
667
  >
652
- <svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
668
+ <svg
669
+ class="h-3.5 w-3.5"
670
+ fill="none"
671
+ viewBox="0 0 24 24"
672
+ stroke="currentColor"
673
+ stroke-width="2"
674
+ >
653
675
  <path
654
676
  stroke-linecap="round"
655
677
  stroke-linejoin="round"
656
678
  d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931z"
657
679
  />
658
680
  </svg>
659
- Edit
681
+ {i18n.t('edit')}
660
682
  </button>
661
683
  <button
662
684
  type="button"
663
685
  onclick={clearSingle}
664
- class="inline-flex items-center justify-center rounded-lg bg-white/95 p-1.5 text-secondary shadow-sm backdrop-blur transition hover:bg-white hover:text-red-600 dark:bg-slate-900/95 dark:hover:bg-slate-900 dark:hover:text-red-400"
665
- aria-label={`Remove ${singlePreviewName}`}
686
+ class="rounded-lg bg-white/95 p-1.5 text-secondary shadow-sm backdrop-blur hover:bg-white hover:text-red-600 dark:bg-slate-900/95 dark:hover:bg-slate-900 dark:hover:text-red-400 inline-flex items-center justify-center transition"
687
+ aria-label={i18n.t('removeNamed', { name: singlePreviewName })}
666
688
  >
667
- <svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
689
+ <svg
690
+ class="h-3.5 w-3.5"
691
+ fill="none"
692
+ viewBox="0 0 24 24"
693
+ stroke="currentColor"
694
+ stroke-width="2"
695
+ >
668
696
  <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
669
697
  </svg>
670
698
  </button>
@@ -672,20 +700,20 @@
672
700
  {/if}
673
701
  </div>
674
702
 
675
- <div class="flex items-center justify-between gap-3 border-t border-border px-4 py-3">
703
+ <div class="gap-3 border-border px-4 py-3 flex items-center justify-between border-t">
676
704
  <div class="min-w-0">
677
- <p class="truncate text-sm font-medium text-primary">{singlePreviewName}</p>
705
+ <p class="text-sm font-medium text-primary truncate">{singlePreviewName}</p>
678
706
  {#if singlePreviewMeta}
679
707
  <p class="text-xs text-secondary">{singlePreviewMeta}</p>
680
708
  {/if}
681
709
  </div>
682
- {#if helperText}
683
- <p id={helperId} class="shrink-0 text-xs text-secondary">{helperText}</p>
710
+ {#if resolvedHelperText}
711
+ <p id={helperId} class="text-xs text-secondary shrink-0">{resolvedHelperText}</p>
684
712
  {/if}
685
713
  </div>
686
714
  </div>
687
715
 
688
- <!-- Dropzone: hide the big one when multiple already has files (compact “add more” below) -->
716
+ <!-- Dropzone: hide the big one when multiple already has files (compact “add more” below) -->
689
717
  {:else if !(isMultiple && hasFile && showFileList)}
690
718
  {@render dropzone()}
691
719
  {/if}
@@ -697,12 +725,12 @@
697
725
  <!-- Multiple file cards (optional — MediaAssetBrowser owns its own grid) -->
698
726
  {#if isMultiple && hasFile && showFileList}
699
727
  {#if view === 'grid'}
700
- <ul class="grid grid-cols-2 gap-2.5 sm:grid-cols-3" aria-label="Selected files">
728
+ <ul class="gap-2.5 sm:grid-cols-3 grid grid-cols-2" aria-label={i18n.t('selectedFiles')}>
701
729
  {#each fileList as item (item.id)}
702
730
  <li
703
- class="group relative overflow-hidden rounded-xl border border-border bg-surface-elevated shadow-sm transition hover:border-border-strong"
731
+ class="group rounded-xl border-border bg-surface-elevated shadow-sm hover:border-border-strong relative overflow-hidden border transition"
704
732
  >
705
- <div class="relative aspect-square bg-surface-overlay">
733
+ <div class="bg-surface-overlay relative aspect-square">
706
734
  {#if item.previewUrl}
707
735
  <img src={item.previewUrl} alt="" class="h-full w-full object-cover" />
708
736
  {:else}
@@ -715,51 +743,63 @@
715
743
  <button
716
744
  type="button"
717
745
  onclick={() => removeFile(item.id)}
718
- class="absolute right-2 top-2 inline-flex h-7 w-7 items-center justify-center rounded-lg bg-white/95 text-secondary opacity-0 shadow-sm backdrop-blur transition group-hover:opacity-100 hover:text-red-600 focus-visible:opacity-100 dark:bg-slate-900/95 dark:hover:text-red-400"
719
- aria-label={`Remove ${item.name}`}
746
+ class="right-2 top-2 h-7 w-7 rounded-lg bg-white/95 text-secondary shadow-sm backdrop-blur hover:text-red-600 dark:bg-slate-900/95 dark:hover:text-red-400 absolute inline-flex items-center justify-center opacity-0 transition group-hover:opacity-100 focus-visible:opacity-100"
747
+ aria-label={i18n.t('removeNamed', { name: item.name })}
720
748
  >
721
- <svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
749
+ <svg
750
+ class="h-3.5 w-3.5"
751
+ fill="none"
752
+ viewBox="0 0 24 24"
753
+ stroke="currentColor"
754
+ stroke-width="2"
755
+ >
722
756
  <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
723
757
  </svg>
724
758
  </button>
725
759
  {/if}
726
760
  </div>
727
- <div class="border-t border-border px-2.5 py-2">
728
- <p class="truncate text-xs font-medium text-primary">{item.name}</p>
761
+ <div class="border-border px-2.5 py-2 border-t">
762
+ <p class="text-xs font-medium text-primary truncate">{item.name}</p>
729
763
  <p class="text-xs text-secondary">{item.sizeFormatted}</p>
730
764
  </div>
731
765
  </li>
732
766
  {/each}
733
767
  </ul>
734
768
  {:else}
735
- <ul class="flex flex-col gap-2" aria-label="Selected files">
769
+ <ul class="gap-2 flex flex-col" aria-label={i18n.t('selectedFiles')}>
736
770
  {#each fileList as item (item.id)}
737
771
  <li
738
- class="group flex items-center gap-3 rounded-xl border border-border bg-surface-elevated p-2.5 shadow-sm transition hover:border-border-strong"
772
+ class="group gap-3 rounded-xl border-border bg-surface-elevated p-2.5 shadow-sm hover:border-border-strong flex items-center border transition"
739
773
  >
740
774
  {#if item.previewUrl}
741
775
  <img
742
776
  src={item.previewUrl}
743
777
  alt=""
744
- class="h-12 w-12 shrink-0 rounded-lg object-cover border border-border"
778
+ class="h-12 w-12 rounded-lg border-border shrink-0 border object-cover"
745
779
  />
746
780
  {:else}
747
781
  {@render fileBadge(item.kind, 'h-12 w-12 shrink-0 rounded-lg')}
748
782
  {/if}
749
783
 
750
784
  <div class="min-w-0 flex-1">
751
- <p class="truncate text-sm font-medium text-primary">{item.name}</p>
785
+ <p class="text-sm font-medium text-primary truncate">{item.name}</p>
752
786
  <p class="text-xs text-secondary">{item.sizeFormatted} · {kindLabel(item.kind)}</p>
753
787
  </div>
754
788
 
755
789
  <button
756
790
  type="button"
757
791
  onclick={() => removeFile(item.id)}
758
- disabled={disabled}
759
- class="shrink-0 rounded-lg p-2 text-secondary transition hover:bg-red-50 hover:text-red-600 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500/30 dark:hover:bg-red-950/40 dark:hover:text-red-400 disabled:opacity-50"
760
- aria-label={`Remove ${item.name}`}
792
+ {disabled}
793
+ class="rounded-lg p-2 text-secondary hover:bg-red-50 hover:text-red-600 focus-visible:ring-brand-500/30 dark:hover:bg-red-950/40 dark:hover:text-red-400 shrink-0 transition focus-visible:ring-2 focus-visible:outline-none disabled:opacity-50"
794
+ aria-label={i18n.t('removeNamed', { name: item.name })}
761
795
  >
762
- <svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
796
+ <svg
797
+ class="h-4 w-4"
798
+ fill="none"
799
+ viewBox="0 0 24 24"
800
+ stroke="currentColor"
801
+ stroke-width="2"
802
+ >
763
803
  <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
764
804
  </svg>
765
805
  </button>
package/dist/index.d.ts CHANGED
@@ -186,6 +186,9 @@ export { default as PasswordInput } from './components/molecules/PasswordInput/P
186
186
  export { default as CopyButton } from './components/molecules/CopyButton/CopyButton.svelte';
187
187
  export { default as SegmentedControl } from './components/molecules/SegmentedControl/SegmentedControl.svelte';
188
188
  export type { SegmentItem } from './components/molecules/SegmentedControl/SegmentedControl.svelte';
189
+ export { default as Dock } from './components/molecules/Dock/Dock.svelte';
190
+ export type { DockSize } from './components/molecules/Dock/Dock.svelte';
191
+ export { default as DockItem } from './components/molecules/Dock/DockItem.svelte';
189
192
  export { default as Popover } from './components/molecules/Popover/Popover.svelte';
190
193
  export type { PopoverPlacement, PopoverAlign } from './components/molecules/Popover/Popover.svelte';
191
194
  export { default as HoverCard } from './components/molecules/HoverCard/HoverCard.svelte';
package/dist/index.js CHANGED
@@ -150,6 +150,8 @@ export { default as SearchInput } from './components/molecules/SearchInput/Searc
150
150
  export { default as PasswordInput } from './components/molecules/PasswordInput/PasswordInput.svelte';
151
151
  export { default as CopyButton } from './components/molecules/CopyButton/CopyButton.svelte';
152
152
  export { default as SegmentedControl } from './components/molecules/SegmentedControl/SegmentedControl.svelte';
153
+ export { default as Dock } from './components/molecules/Dock/Dock.svelte';
154
+ export { default as DockItem } from './components/molecules/Dock/DockItem.svelte';
153
155
  export { default as Popover } from './components/molecules/Popover/Popover.svelte';
154
156
  export { default as HoverCard } from './components/molecules/HoverCard/HoverCard.svelte';
155
157
  export { default as Timeline } from './components/molecules/Timeline/Timeline.svelte';
@@ -1,4 +1,4 @@
1
- export type LocaleCode = 'en' | 'es' | 'pt' | 'fr' | 'de' | (string & {});
1
+ export type LocaleCode = 'en' | 'es' | 'ca' | 'pt' | 'fr' | 'de' | (string & {});
2
2
  /** Built-in UI / form / validation strings. Apps can merge overrides. */
3
3
  export interface UiMessages {
4
4
  save: string;
@@ -14,6 +14,28 @@ export interface UiMessages {
14
14
  addItem: string;
15
15
  removeItem: string;
16
16
  noItems: string;
17
+ /** FileUploader */
18
+ uploadFiles: string;
19
+ uploadHelper: string;
20
+ dropToUpload: string;
21
+ dropOrBrowse: string;
22
+ browse: string;
23
+ browseButton: string;
24
+ viewMode: string;
25
+ listView: string;
26
+ gridView: string;
27
+ clearAll: string;
28
+ upload: string;
29
+ change: string;
30
+ remove: string;
31
+ edit: string;
32
+ removeNamed: string;
33
+ selectedFiles: string;
34
+ addProfilePhoto: string;
35
+ media: string;
36
+ imageRequired: string;
37
+ typeNotAllowed: string;
38
+ exceedsMaxSize: string;
17
39
  /** Validation */
18
40
  fieldRequired: string;
19
41
  invalidEmail: string;
@@ -12,6 +12,27 @@ const en = {
12
12
  addItem: 'Add item',
13
13
  removeItem: 'Remove item',
14
14
  noItems: 'No items yet',
15
+ uploadFiles: 'Upload files',
16
+ uploadHelper: 'SVG, PNG, JPG or GIF (max. 10MB)',
17
+ dropToUpload: 'Drop files to upload',
18
+ dropOrBrowse: 'Drop files or',
19
+ browse: 'browse',
20
+ browseButton: 'Browse',
21
+ viewMode: 'View mode',
22
+ listView: 'List view',
23
+ gridView: 'Grid view',
24
+ clearAll: 'Clear all',
25
+ upload: 'Upload',
26
+ change: 'Change',
27
+ remove: 'Remove',
28
+ edit: 'Edit',
29
+ removeNamed: 'Remove {name}',
30
+ selectedFiles: 'Selected files',
31
+ addProfilePhoto: 'Add a profile photo',
32
+ media: 'Media',
33
+ imageRequired: '{name}: an image is required',
34
+ typeNotAllowed: '{name}: file type not allowed',
35
+ exceedsMaxSize: '{name}: exceeds {max}MB',
15
36
  fieldRequired: 'This field is required',
16
37
  invalidEmail: 'Enter a valid email address',
17
38
  invalidUrl: 'Enter a valid URL',
@@ -35,6 +56,27 @@ const es = {
35
56
  addItem: 'Añadir elemento',
36
57
  removeItem: 'Eliminar elemento',
37
58
  noItems: 'Sin elementos todavía',
59
+ uploadFiles: 'Subir archivos',
60
+ uploadHelper: 'SVG, PNG, JPG o GIF (máx. 10MB)',
61
+ dropToUpload: 'Suelta los archivos para subirlos',
62
+ dropOrBrowse: 'Suelta archivos o',
63
+ browse: 'explora',
64
+ browseButton: 'Explorar',
65
+ viewMode: 'Modo de vista',
66
+ listView: 'Vista de lista',
67
+ gridView: 'Vista de cuadrícula',
68
+ clearAll: 'Borrar todo',
69
+ upload: 'Subir',
70
+ change: 'Cambiar',
71
+ remove: 'Quitar',
72
+ edit: 'Editar',
73
+ removeNamed: 'Quitar {name}',
74
+ selectedFiles: 'Archivos seleccionados',
75
+ addProfilePhoto: 'Añade una foto de perfil',
76
+ media: 'Multimedia',
77
+ imageRequired: '{name}: se requiere una imagen',
78
+ typeNotAllowed: '{name}: tipo no permitido',
79
+ exceedsMaxSize: '{name}: supera {max}MB',
38
80
  fieldRequired: 'Este campo es obligatorio',
39
81
  invalidEmail: 'Introduce un email válido',
40
82
  invalidUrl: 'Introduce una URL válida',
@@ -58,6 +100,27 @@ const pt = {
58
100
  addItem: 'Adicionar item',
59
101
  removeItem: 'Remover item',
60
102
  noItems: 'Ainda sem itens',
103
+ uploadFiles: 'Carregar ficheiros',
104
+ uploadHelper: 'SVG, PNG, JPG ou GIF (máx. 10MB)',
105
+ dropToUpload: 'Largue os ficheiros para carregar',
106
+ dropOrBrowse: 'Largue ficheiros ou',
107
+ browse: 'procurar',
108
+ browseButton: 'Procurar',
109
+ viewMode: 'Modo de visualização',
110
+ listView: 'Vista de lista',
111
+ gridView: 'Vista de grelha',
112
+ clearAll: 'Limpar tudo',
113
+ upload: 'Carregar',
114
+ change: 'Alterar',
115
+ remove: 'Remover',
116
+ edit: 'Editar',
117
+ removeNamed: 'Remover {name}',
118
+ selectedFiles: 'Ficheiros selecionados',
119
+ addProfilePhoto: 'Adicione uma foto de perfil',
120
+ media: 'Multimédia',
121
+ imageRequired: '{name}: é necessária uma imagem',
122
+ typeNotAllowed: '{name}: tipo não permitido',
123
+ exceedsMaxSize: '{name}: excede {max}MB',
61
124
  fieldRequired: 'Este campo é obrigatório',
62
125
  invalidEmail: 'Introduza um email válido',
63
126
  invalidUrl: 'Introduza um URL válido',
@@ -81,6 +144,27 @@ const fr = {
81
144
  addItem: 'Ajouter un élément',
82
145
  removeItem: 'Supprimer l’élément',
83
146
  noItems: 'Aucun élément pour l’instant',
147
+ uploadFiles: 'Téléverser des fichiers',
148
+ uploadHelper: 'SVG, PNG, JPG ou GIF (max. 10MB)',
149
+ dropToUpload: 'Déposez les fichiers pour les téléverser',
150
+ dropOrBrowse: 'Déposez des fichiers ou',
151
+ browse: 'parcourir',
152
+ browseButton: 'Parcourir',
153
+ viewMode: 'Mode d’affichage',
154
+ listView: 'Vue liste',
155
+ gridView: 'Vue grille',
156
+ clearAll: 'Tout effacer',
157
+ upload: 'Téléverser',
158
+ change: 'Changer',
159
+ remove: 'Retirer',
160
+ edit: 'Modifier',
161
+ removeNamed: 'Retirer {name}',
162
+ selectedFiles: 'Fichiers sélectionnés',
163
+ addProfilePhoto: 'Ajoutez une photo de profil',
164
+ media: 'Médias',
165
+ imageRequired: '{name} : une image est requise',
166
+ typeNotAllowed: '{name} : type non autorisé',
167
+ exceedsMaxSize: '{name} : dépasse {max}MB',
84
168
  fieldRequired: 'Ce champ est obligatoire',
85
169
  invalidEmail: 'Saisissez une adresse e-mail valide',
86
170
  invalidUrl: 'Saisissez une URL valide',
@@ -104,6 +188,27 @@ const de = {
104
188
  addItem: 'Element hinzufügen',
105
189
  removeItem: 'Element entfernen',
106
190
  noItems: 'Noch keine Elemente',
191
+ uploadFiles: 'Dateien hochladen',
192
+ uploadHelper: 'SVG, PNG, JPG oder GIF (max. 10MB)',
193
+ dropToUpload: 'Dateien zum Hochladen ablegen',
194
+ dropOrBrowse: 'Dateien ablegen oder',
195
+ browse: 'durchsuchen',
196
+ browseButton: 'Durchsuchen',
197
+ viewMode: 'Ansicht',
198
+ listView: 'Listenansicht',
199
+ gridView: 'Rasteransicht',
200
+ clearAll: 'Alle löschen',
201
+ upload: 'Hochladen',
202
+ change: 'Ändern',
203
+ remove: 'Entfernen',
204
+ edit: 'Bearbeiten',
205
+ removeNamed: '{name} entfernen',
206
+ selectedFiles: 'Ausgewählte Dateien',
207
+ addProfilePhoto: 'Profilfoto hinzufügen',
208
+ media: 'Medien',
209
+ imageRequired: '{name}: ein Bild ist erforderlich',
210
+ typeNotAllowed: '{name}: Dateityp nicht erlaubt',
211
+ exceedsMaxSize: '{name}: überschreitet {max}MB',
107
212
  fieldRequired: 'Dieses Feld ist erforderlich',
108
213
  invalidEmail: 'Geben Sie eine gültige E-Mail-Adresse ein',
109
214
  invalidUrl: 'Geben Sie eine gültige URL ein',
@@ -113,10 +218,55 @@ const de = {
113
218
  maxValue: 'Darf höchstens {max} sein',
114
219
  patternMismatch: 'Ungültiges Format'
115
220
  };
221
+ const ca = {
222
+ save: 'Desar',
223
+ cancel: 'Cancel·lar',
224
+ delete: 'Eliminar',
225
+ reset: 'Restablir',
226
+ submit: 'Enviar',
227
+ close: 'Tancar',
228
+ optional: 'Opcional',
229
+ required: 'Obligatori',
230
+ loading: 'S’està carregant…',
231
+ errorSummary: 'Corregeix el següent',
232
+ addItem: 'Afegir element',
233
+ removeItem: 'Eliminar element',
234
+ noItems: 'Encara sense elements',
235
+ uploadFiles: 'Pujar fitxers',
236
+ uploadHelper: 'SVG, PNG, JPG o GIF (màx. 10MB)',
237
+ dropToUpload: 'Deixa anar els fitxers per pujar-los',
238
+ dropOrBrowse: 'Deixa anar fitxers o',
239
+ browse: 'explora',
240
+ browseButton: 'Explorar',
241
+ viewMode: 'Mode de vista',
242
+ listView: 'Vista de llista',
243
+ gridView: 'Vista de graella',
244
+ clearAll: 'Esborrar-ho tot',
245
+ upload: 'Pujar',
246
+ change: 'Canviar',
247
+ remove: 'Treure',
248
+ edit: 'Editar',
249
+ removeNamed: 'Treure {name}',
250
+ selectedFiles: 'Fitxers seleccionats',
251
+ addProfilePhoto: 'Afegeix una foto de perfil',
252
+ media: 'Multimèdia',
253
+ imageRequired: '{name}: cal una imatge',
254
+ typeNotAllowed: '{name}: tipus no permès',
255
+ exceedsMaxSize: '{name}: supera {max}MB',
256
+ fieldRequired: 'Aquest camp és obligatori',
257
+ invalidEmail: 'Introdueix un email vàlid',
258
+ invalidUrl: 'Introdueix un URL vàlid',
259
+ minLength: 'Usa almenys {min} caràcters',
260
+ maxLength: 'Usa com a màxim {max} caràcters',
261
+ minValue: 'Ha de ser almenys {min}',
262
+ maxValue: 'Ha de ser com a màxim {max}',
263
+ patternMismatch: 'Format no vàlid'
264
+ };
116
265
  /** Built-in dictionaries keyed by primary language subtag (`es-MX` → `es`). */
117
266
  export const UI_DICTIONARIES = {
118
267
  en,
119
268
  es,
269
+ ca,
120
270
  pt,
121
271
  fr,
122
272
  de
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@r2digisolutions/components",
3
- "version": "0.6.3",
3
+ "version": "0.6.7",
4
4
  "private": false,
5
5
  "description": "R2DigiSolutions Svelte 5 component library — Atomic Design, Tailwind 4, Light/Dark mode",
6
6
  "license": "MIT",
@@ -81,7 +81,7 @@
81
81
  "@tailwindcss/vite": "^4.3.3",
82
82
  "@types/node": "^26.2.0",
83
83
  "@vitest/browser-playwright": "^4.1.11",
84
- "bumpp": "^12.1.1",
84
+ "bumpp": "~11.1.0",
85
85
  "eslint": "^10.8.1",
86
86
  "eslint-config-prettier": "^10.1.8",
87
87
  "eslint-plugin-svelte": "^3.23.0",