runeforge 0.0.35 → 0.0.37

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.
@@ -33,6 +33,7 @@
33
33
  const datePopId = $props.id();
34
34
  const dateAnchorName = `--date-anchor-${datePopId}`;
35
35
  let datePopoverEl: HTMLElement | undefined = $state();
36
+ let calendarDateEl: (HTMLElement & { value: string }) | undefined = $state();
36
37
 
37
38
  const name = $derived(readonly ? undefined : field.attribute);
38
39
  const labelText = $derived(fieldLabel(field));
@@ -72,6 +73,22 @@
72
73
  );
73
74
  const isMultiValued = $derived(field.type === 'multiselect' || field.type === 'tree');
74
75
 
76
+ // cally's `change` event doesn't bubble, so the usual `onchange={...}` prop
77
+ // never fires — Svelte 5 delegates events like `change` to a listener on
78
+ // a shared ancestor, which only ever sees events that bubble up to it.
79
+ // Attaching directly on the element (same fix ColumnFilter's date-range
80
+ // picker already uses) sidesteps delegation entirely.
81
+ $effect(() => {
82
+ if (!calendarDateEl) return;
83
+ function handler() {
84
+ if (fieldDisabled) return;
85
+ record[field.attribute] = calendarDateEl!.value;
86
+ datePopoverEl?.hidePopover();
87
+ }
88
+ calendarDateEl.addEventListener('change', handler);
89
+ return () => calendarDateEl?.removeEventListener('change', handler);
90
+ });
91
+
75
92
  $effect(() => {
76
93
  if (!field.dependentOptions || isMultiValued) return;
77
94
  const current = record[field.attribute];
@@ -193,12 +210,8 @@
193
210
  >
194
211
  <calendar-date
195
212
  class="cally"
213
+ bind:this={calendarDateEl}
196
214
  value={String(record[field.attribute] ?? '')}
197
- onchange={(e: Event) => {
198
- if (fieldDisabled) return;
199
- record[field.attribute] = (e.currentTarget as HTMLElement & { value: string }).value;
200
- datePopoverEl?.hidePopover();
201
- }}
202
215
  >
203
216
  <svg
204
217
  aria-label={strings.previous}
@@ -121,6 +121,7 @@
121
121
  );
122
122
 
123
123
  let activeAction = $state<{ action: CustomAction<T>; item: T } | null>(null);
124
+ let activeBulkAction = $state<{ action: CustomBulkAction<T>; items: T[] } | null>(null);
124
125
 
125
126
  async function runAction(action: CustomAction<T>, item: T) {
126
127
  if (!(action.condition?.(item) ?? true)) return;
@@ -131,6 +132,10 @@
131
132
  activeAction = { action, item };
132
133
  }
133
134
 
135
+ function runBulkAction(action: CustomBulkAction<T>, items: T[]) {
136
+ activeBulkAction = { action, items };
137
+ }
138
+
134
139
  async function navList() {
135
140
  await goto('?');
136
141
  }
@@ -293,7 +298,9 @@
293
298
  );
294
299
 
295
300
  const serverError = $derived(
296
- creating || reading || editing || activeAction !== null ? (form?.error ?? '') : ''
301
+ creating || reading || editing || activeAction !== null || activeBulkAction !== null
302
+ ? (form?.error ?? '')
303
+ : ''
297
304
  );
298
305
  </script>
299
306
 
@@ -359,6 +366,7 @@
359
366
  onEdit={navEdit}
360
367
  onView={navRead}
361
368
  onAction={runAction}
369
+ onBulkAction={runBulkAction}
362
370
  />
363
371
  {/if}
364
372
 
@@ -373,3 +381,15 @@
373
381
  onSuccess={() => (activeAction = null)}
374
382
  />
375
383
  {/if}
384
+
385
+ {#if activeBulkAction !== null}
386
+ {@const BulkActionView = activeBulkAction.action.view}
387
+ <BulkActionView
388
+ items={activeBulkAction.items}
389
+ label={activeBulkAction.action.label}
390
+ endpoint={activeBulkAction.action.endpoint}
391
+ {serverError}
392
+ onCancel={() => (activeBulkAction = null)}
393
+ onSuccess={() => (activeBulkAction = null)}
394
+ />
395
+ {/if}
@@ -52,7 +52,8 @@
52
52
  onCreate,
53
53
  onEdit,
54
54
  onView,
55
- onAction
55
+ onAction,
56
+ onBulkAction
56
57
  }: {
57
58
  data?: T[];
58
59
  labelOne?: string;
@@ -85,6 +86,7 @@
85
86
  onEdit?: (item: T) => void;
86
87
  onView?: (item: T) => void;
87
88
  onAction?: (action: CustomAction<T>, item: T) => void;
89
+ onBulkAction?: (action: CustomBulkAction<T>, items: T[]) => void;
88
90
  } = $props();
89
91
 
90
92
  const icons = $derived(getIconSet() ?? defaultIconSet);
@@ -97,7 +99,7 @@
97
99
  const updateLabel = $derived(update.label ?? strings.edit);
98
100
  const readLabel = $derived(read.label ?? strings.view);
99
101
  const showRowActions = $derived(allowRead || allowUpdate || allowDelete || actions.length > 0);
100
- const allowSelection = $derived(allowDelete || customBulkActions.length > 0);
102
+ const allowSelection = $derived(allowDelete || customBulkActions.some((action) => !action.view));
101
103
 
102
104
  let selected = new SvelteSet<number>();
103
105
  let pendingDeletion = $state<T[] | null>(null);
@@ -179,6 +181,7 @@
179
181
  }
180
182
 
181
183
  async function runBulkAction(action: CustomBulkAction<T>, items: T[]) {
184
+ if (!action.endpoint) return;
182
185
  await runEndpointAction(action.endpoint, items);
183
186
  }
184
187
 
@@ -192,6 +195,10 @@
192
195
 
193
196
  function handleBulkAction(action: CustomBulkAction<T>) {
194
197
  const items = selectedItems;
198
+ if (action.view) {
199
+ onBulkAction?.(action, items);
200
+ return;
201
+ }
195
202
  selected.clear();
196
203
  requestBulkAction(action, items);
197
204
  }
@@ -324,11 +331,13 @@
324
331
  <Button
325
332
  variant={bulkAction.variant ?? 'ghost'}
326
333
  class="btn-outline"
327
- disabled={selected.size === 0}
334
+ disabled={!bulkAction.view && selected.size === 0}
328
335
  onclick={() => handleBulkAction(bulkAction)}
329
336
  >
330
337
  <BulkIcon class="size-4" />
331
- {bulkAction.label} ({selected.size})
338
+ {bulkAction.label}{#if !bulkAction.view}
339
+ ({selected.size})
340
+ {/if}
332
341
  </Button>
333
342
  {/if}
334
343
  {/each}
@@ -36,6 +36,7 @@ declare function $$render<T extends object = Record<string, unknown>>(): {
36
36
  onEdit?: (item: T) => void;
37
37
  onView?: (item: T) => void;
38
38
  onAction?: (action: CustomAction<T>, item: T) => void;
39
+ onBulkAction?: (action: CustomBulkAction<T>, items: T[]) => void;
39
40
  };
40
41
  exports: {};
41
42
  bindings: "";
@@ -102,10 +102,20 @@ export interface RowAction<T extends object = Record<string, unknown>> {
102
102
  export interface CustomBulkAction<T extends object = Record<string, unknown>> {
103
103
  label: string;
104
104
  icon: any;
105
- endpoint: string;
105
+ /** Required unless `view` is set. */
106
+ endpoint?: string;
106
107
  variant?: string;
107
108
  confirm?: boolean;
108
109
  condition?: (items: T[]) => boolean;
110
+ /** Renders as a modal-like panel when the action runs, instead of POSTing
111
+ * to `endpoint` once per selected item. Mutually exclusive with
112
+ * `endpoint` — provide exactly one of the two, mirroring `CustomAction.view`.
113
+ *
114
+ * Unlike the endpoint-loop behavior, a `view` action never requires a
115
+ * pre-existing selection: the button is enabled even with nothing
116
+ * selected, and `confirm` is ignored (the view owns its own flow). The
117
+ * component receives the current selection as `items` (possibly empty). */
118
+ view?: Component<any>;
109
119
  }
110
120
  export interface SearchConfiguration {
111
121
  param?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "runeforge",
3
- "version": "0.0.35",
3
+ "version": "0.0.37",
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",