runeforge 0.0.39 → 0.0.41

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.
@@ -1,4 +1,4 @@
1
1
  export declare const formatBoolean: (trueLabel?: string, falseLabel?: string) => () => (value: boolean) => string;
2
- export declare const formatDatetime: (format?: string) => () => (value: Date) => string;
2
+ export declare const formatDatetime: (format?: string) => (() => (value: Date) => string);
3
3
  export declare function formatTruncateTextUpTo(maxLength: number): () => (value: string) => string;
4
4
  export declare function formatInstance<T extends Record<string, unknown>>(attribute: keyof T & string, instances: T[], urlPath: string, idKey?: keyof T & string): (value: unknown) => string;
@@ -1,4 +1,4 @@
1
- export const formatBoolean = (trueLabel = 'Sí', falseLabel = 'No') => () => (value) => (value ? trueLabel : falseLabel);
1
+ export const formatBoolean = (trueLabel = 'Sí', falseLabel = 'No') => () => (value) => value ? trueLabel : falseLabel;
2
2
  // Supported tokens: dd · mm · YYYY · HH · MM · ss
3
3
  const TOKENS = {
4
4
  dd: (d) => String(d.getDate()).padStart(2, '0'),
@@ -6,10 +6,15 @@ const TOKENS = {
6
6
  YYYY: (d) => String(d.getFullYear()),
7
7
  HH: (d) => String(d.getHours()).padStart(2, '0'),
8
8
  MM: (d) => String(d.getMinutes()).padStart(2, '0'),
9
- ss: (d) => String(d.getSeconds()).padStart(2, '0'),
9
+ ss: (d) => String(d.getSeconds()).padStart(2, '0')
10
10
  };
11
11
  export const formatDatetime = (format = 'dd/mm/YYYY HH:MM') => {
12
12
  const fmt = (value) => {
13
+ // `null`/`undefined`/`''` (an unset field) must render blank, not epoch 0
14
+ // — `new Date(null)` is 1970-01-01, a "valid" Date whose getTime() isn't
15
+ // NaN, so it would otherwise slip past the invalid-date check below.
16
+ if (value === null || value === undefined || value === '')
17
+ return '';
13
18
  const d = new Date(value);
14
19
  if (isNaN(d.getTime()))
15
20
  return '';
@@ -1,9 +1,13 @@
1
1
  <script lang="ts">
2
2
  import { SvelteMap } from 'svelte/reactivity';
3
3
  import { getStrings } from '../../i18n/context.js';
4
+ import { getIconSet } from '../../icons/context.js';
5
+ import { defaultIconSet } from '../../icons/sets/default.js';
6
+ import Button from './Button.svelte';
4
7
  import type { SearchResolver, SelectOption } from '../../types/attribute.js';
5
8
 
6
9
  const strings = getStrings();
10
+ const icons = $derived(getIconSet() ?? defaultIconSet);
7
11
 
8
12
  let {
9
13
  name,
@@ -14,6 +18,12 @@
14
18
  placeholder = strings.selectPlaceholder,
15
19
  error = '',
16
20
  disabled = false,
21
+ // When passed, the dropdown's top row selects every option instead of
22
+ // clearing the selection (labelled `strings.selectAll` instead of
23
+ // `placeholder`) — clearing moves to the trigger's own × button instead.
24
+ // Left unset, existing callers keep today's behavior unchanged: top row
25
+ // clears, no × button.
26
+ onSelectAll,
17
27
  }: {
18
28
  name?: string;
19
29
  value?: string[];
@@ -23,6 +33,7 @@
23
33
  placeholder?: string;
24
34
  error?: string;
25
35
  disabled?: boolean;
36
+ onSelectAll?: () => void;
26
37
  } = $props();
27
38
 
28
39
  const popId = $props.id();
@@ -87,6 +98,17 @@
87
98
  function onToggle(e: Event) {
88
99
  if ((e as ToggleEvent).newState === 'closed') query = '';
89
100
  }
101
+
102
+ const hasSelection = $derived(value.length > 0);
103
+
104
+ function clearFromTrigger(e: MouseEvent) {
105
+ // Stop the click from also reaching the popover-trigger button
106
+ // underneath (they're overlapping siblings, not nested) so clearing
107
+ // doesn't toggle the dropdown open at the same time.
108
+ e.preventDefault();
109
+ e.stopPropagation();
110
+ clear();
111
+ }
90
112
  </script>
91
113
 
92
114
  <div class="relative w-full">
@@ -94,18 +116,34 @@
94
116
  <input type="hidden" {name} value={JSON.stringify(value)} />
95
117
  {/if}
96
118
 
97
- <button
98
- type="button"
99
- class="select select-bordered w-full text-left font-normal"
100
- class:select-error={!!error}
101
- class:opacity-40={value.length === 0}
102
- {disabled}
103
- popovertarget={popId}
104
- style="anchor-name:{anchorName}"
105
- title={selectedLabels.join(', ')}
106
- >
107
- {buttonLabel}
108
- </button>
119
+ <div class="relative">
120
+ <button
121
+ type="button"
122
+ class="select select-bordered w-full text-left font-normal"
123
+ class:select-error={!!error}
124
+ class:opacity-40={value.length === 0}
125
+ class:pr-8={hasSelection}
126
+ {disabled}
127
+ popovertarget={popId}
128
+ style="anchor-name:{anchorName}"
129
+ title={selectedLabels.join(', ')}
130
+ >
131
+ {buttonLabel}
132
+ </button>
133
+
134
+ {#if hasSelection && !disabled}
135
+ {@const Icon = icons.clear}
136
+ <Button
137
+ variant="ghost"
138
+ class="btn-xs btn-square btn-circle absolute top-1/2 right-6 -translate-y-1/2"
139
+ aria-label={strings.selectClear}
140
+ title={strings.selectClear}
141
+ onclick={clearFromTrigger}
142
+ >
143
+ <Icon class="size-3" />
144
+ </Button>
145
+ {/if}
146
+ </div>
109
147
 
110
148
  <div
111
149
  popover="auto"
@@ -129,9 +167,9 @@
129
167
  <button
130
168
  type="button"
131
169
  class="w-full rounded-btn px-3 py-2 text-left text-sm text-base-content/40 hover:bg-base-200"
132
- onclick={clear}
170
+ onclick={onSelectAll ?? clear}
133
171
  >
134
- {placeholder}
172
+ {onSelectAll ? strings.selectAll : placeholder}
135
173
  </button>
136
174
  </li>
137
175
  {#if searching}
@@ -8,6 +8,7 @@ type $$ComponentProps = {
8
8
  placeholder?: string;
9
9
  error?: string;
10
10
  disabled?: boolean;
11
+ onSelectAll?: () => void;
11
12
  };
12
13
  declare const MultiSelect: import("svelte").Component<$$ComponentProps, {}, "value">;
13
14
  type MultiSelect = ReturnType<typeof MultiSelect>;
package/dist/i18n/en.js CHANGED
@@ -13,6 +13,8 @@ export const en = {
13
13
  selectSearching: 'Searching...',
14
14
  selectNoResults: 'No results',
15
15
  selectedCount: (count) => `${count} selected`,
16
+ selectAll: 'Select all',
17
+ selectClear: 'Clear selection',
16
18
  view: 'View',
17
19
  edit: 'Edit',
18
20
  delete: 'Delete',
package/dist/i18n/es.js CHANGED
@@ -13,6 +13,8 @@ export const es = {
13
13
  selectSearching: 'Buscando...',
14
14
  selectNoResults: 'Sin resultados',
15
15
  selectedCount: (count) => `${count} seleccionado${count === 1 ? '' : 's'}`,
16
+ selectAll: 'Seleccionar todo',
17
+ selectClear: 'Limpiar selección',
16
18
  view: 'Ver',
17
19
  edit: 'Editar',
18
20
  delete: 'Eliminar',
@@ -13,6 +13,8 @@ export interface RuneforgeStrings {
13
13
  selectSearching: string;
14
14
  selectNoResults: string;
15
15
  selectedCount: (count: number) => string;
16
+ selectAll: string;
17
+ selectClear: string;
16
18
  view: string;
17
19
  edit: string;
18
20
  delete: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "runeforge",
3
- "version": "0.0.39",
3
+ "version": "0.0.41",
4
4
  "description": "SvelteKit toolkit for building metadata-driven CRUD interfaces with tables, forms, and actions",
5
5
  "license": "MIT",
6
6
  "author": "Ezequiel Puerta",