runeforge 0.0.21 → 0.0.22

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.
@@ -106,6 +106,7 @@
106
106
  name={field.attribute}
107
107
  bind:value={record[field.attribute] as string}
108
108
  options={selectOptions}
109
+ search={field.search}
109
110
  placeholder={field.placeholder}
110
111
  disabled={fieldDisabled}
111
112
  {error}
@@ -121,6 +121,15 @@
121
121
 
122
122
  let activeAction = $state<{ action: CustomAction<T>; item: T } | null>(null);
123
123
 
124
+ async function runAction(action: CustomAction<T>, item: T) {
125
+ if (!(action.condition?.(item) ?? true)) return;
126
+ if (action.href) {
127
+ await goto(action.href(item));
128
+ return;
129
+ }
130
+ activeAction = { action, item };
131
+ }
132
+
124
133
  async function navList() {
125
134
  await goto('?');
126
135
  }
@@ -322,9 +331,7 @@
322
331
  onCreate={navCreate}
323
332
  onEdit={navEdit}
324
333
  onView={navRead}
325
- onAction={(action, item) => {
326
- if (action.condition?.(item) ?? true) activeAction = { action, item };
327
- }}
334
+ onAction={runAction}
328
335
  />
329
336
  {/if}
330
337
 
@@ -39,6 +39,7 @@ export function buildFieldDefinitions(meta, data, excludedFlag, excluded) {
39
39
  dependentOptions: m.dependentOptions
40
40
  ? (record) => m.dependentOptions(data, record)
41
41
  : undefined,
42
+ search: m.search,
42
43
  disabled: m.disabled,
43
44
  seed: m.seed,
44
45
  groupedAs: m.groupedAs,
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { getStrings } from '../../i18n/context.js';
3
+ import type { SearchResolver } from '../../types/attribute.js';
3
4
 
4
5
  const strings = getStrings();
5
6
 
@@ -7,6 +8,8 @@
7
8
  name,
8
9
  value = $bindable(''),
9
10
  options = [],
11
+ search: searchFn,
12
+ searchDebounceMs = 300,
10
13
  placeholder = strings.selectPlaceholder,
11
14
  error = '',
12
15
  disabled = false,
@@ -14,41 +17,84 @@
14
17
  name?: string;
15
18
  value?: string;
16
19
  options?: { value: string; label: string }[];
20
+ search?: SearchResolver;
21
+ searchDebounceMs?: number;
17
22
  placeholder?: string;
18
23
  error?: string;
19
24
  disabled?: boolean;
20
25
  } = $props();
21
26
 
22
27
  let open = $state(false);
23
- let search = $state('');
28
+ let query = $state('');
24
29
  let container: HTMLDivElement;
25
30
 
31
+ // Options resolved by `searchFn` for the current query; null while no
32
+ // server search has run yet (e.g. box just opened, query still empty).
33
+ let remoteResults = $state<{ value: string; label: string }[] | null>(null);
34
+ let searching = $state(false);
35
+ // Label of whatever was last picked from `remoteResults`, kept around so the
36
+ // closed-state button can still show it even though it isn't in `options`.
37
+ let pickedLabel = $state<string | null>(null);
38
+
39
+ let searchToken = 0;
40
+ $effect(() => {
41
+ if (!searchFn) return;
42
+ const q = query.trim();
43
+ if (!q) {
44
+ remoteResults = null;
45
+ searching = false;
46
+ return;
47
+ }
48
+ const token = ++searchToken;
49
+ searching = true;
50
+ const timer = setTimeout(() => {
51
+ searchFn(q).then((results) => {
52
+ if (token !== searchToken) return; // stale response, a newer query took over
53
+ remoteResults = results;
54
+ searching = false;
55
+ });
56
+ }, searchDebounceMs);
57
+ return () => clearTimeout(timer);
58
+ });
59
+
26
60
  const filtered = $derived(
27
- search.trim()
28
- ? options.filter((o) => o.label.toLowerCase().includes(search.toLowerCase()))
29
- : options
61
+ searchFn
62
+ ? (remoteResults ?? options)
63
+ : query.trim()
64
+ ? options.filter((o) => o.label.toLowerCase().includes(query.toLowerCase()))
65
+ : options
30
66
  );
31
67
 
32
68
  const selectedLabel = $derived(
33
- options.find((o) => o.value === value)?.label ?? placeholder
69
+ value === ''
70
+ ? placeholder
71
+ : (options.find((o) => o.value === value)?.label ?? pickedLabel ?? placeholder)
34
72
  );
35
73
 
36
74
  function toggle() {
37
75
  if (disabled) return;
38
76
  open = !open;
39
- if (!open) search = '';
77
+ if (!open) query = '';
40
78
  }
41
79
 
42
- function pick(val: string) {
43
- value = val;
80
+ function pick(option: { value: string; label: string }) {
81
+ value = option.value;
82
+ pickedLabel = option.label;
44
83
  open = false;
45
- search = '';
84
+ query = '';
85
+ }
86
+
87
+ function clear() {
88
+ value = '';
89
+ pickedLabel = null;
90
+ open = false;
91
+ query = '';
46
92
  }
47
93
 
48
94
  function onWindowClick(e: MouseEvent) {
49
95
  if (open && !container.contains(e.target as Node)) {
50
96
  open = false;
51
- search = '';
97
+ query = '';
52
98
  }
53
99
  }
54
100
  </script>
@@ -78,7 +124,7 @@
78
124
  type="text"
79
125
  class="input input-bordered input-sm w-full"
80
126
  placeholder={strings.selectSearch}
81
- bind:value={search}
127
+ bind:value={query}
82
128
  autocomplete="off"
83
129
  />
84
130
  </div>
@@ -87,11 +133,14 @@
87
133
  <button
88
134
  type="button"
89
135
  class="w-full rounded-btn px-3 py-2 text-left text-sm text-base-content/40 hover:bg-base-200"
90
- onclick={() => pick('')}
136
+ onclick={clear}
91
137
  >
92
138
  {placeholder}
93
139
  </button>
94
140
  </li>
141
+ {#if searching}
142
+ <li class="px-3 py-2 text-sm text-base-content/40">{strings.selectSearching}</li>
143
+ {/if}
95
144
  {#each filtered as option (option.value)}
96
145
  <li>
97
146
  <button
@@ -99,13 +148,13 @@
99
148
  class="w-full rounded-btn px-3 py-2 text-left text-sm hover:bg-base-200"
100
149
  class:bg-primary={value === option.value}
101
150
  class:text-primary-content={value === option.value}
102
- onclick={() => pick(option.value)}
151
+ onclick={() => pick(option)}
103
152
  >
104
153
  {option.label}
105
154
  </button>
106
155
  </li>
107
156
  {/each}
108
- {#if filtered.length === 0}
157
+ {#if !searching && filtered.length === 0}
109
158
  <li class="px-3 py-2 text-sm text-base-content/40">{strings.selectNoResults}</li>
110
159
  {/if}
111
160
  </ul>
@@ -1,3 +1,4 @@
1
+ import type { SearchResolver } from '../../types/attribute.js';
1
2
  type $$ComponentProps = {
2
3
  name?: string;
3
4
  value?: string;
@@ -5,6 +6,8 @@ type $$ComponentProps = {
5
6
  value: string;
6
7
  label: string;
7
8
  }[];
9
+ search?: SearchResolver;
10
+ searchDebounceMs?: number;
8
11
  placeholder?: string;
9
12
  error?: string;
10
13
  disabled?: boolean;
package/dist/i18n/en.js CHANGED
@@ -10,6 +10,7 @@ export const en = {
10
10
  next: 'Next',
11
11
  selectPlaceholder: 'Select an option',
12
12
  selectSearch: 'Search...',
13
+ selectSearching: 'Searching...',
13
14
  selectNoResults: 'No results',
14
15
  view: 'View',
15
16
  edit: 'Edit',
package/dist/i18n/es.js CHANGED
@@ -10,6 +10,7 @@ export const es = {
10
10
  next: 'Siguiente',
11
11
  selectPlaceholder: 'Seleccioná una opción',
12
12
  selectSearch: 'Buscar...',
13
+ selectSearching: 'Buscando...',
13
14
  selectNoResults: 'Sin resultados',
14
15
  view: 'Ver',
15
16
  edit: 'Editar',
@@ -10,6 +10,7 @@ export interface RuneforgeStrings {
10
10
  next: string;
11
11
  selectPlaceholder: string;
12
12
  selectSearch: string;
13
+ selectSearching: string;
13
14
  selectNoResults: string;
14
15
  view: string;
15
16
  edit: string;
@@ -20,6 +20,7 @@ export type SelectOption = {
20
20
  export type OptionsResolver = SelectOption[] | ((data: any) => SelectOption[]);
21
21
  export type FormatterResolver = (data?: any) => CellFormatter<any, any>;
22
22
  export type DependentOptionsResolver = (data: any, record: Record<string, unknown>) => SelectOption[];
23
+ export type SearchResolver = (query: string) => Promise<SelectOption[]>;
23
24
  export type DisabledResolver = (record: Record<string, unknown>) => boolean;
24
25
  export type SeedResolver = (instance: any) => unknown;
25
26
  export type AttributeMetadata = {
@@ -27,6 +28,10 @@ export type AttributeMetadata = {
27
28
  type?: AttributeType;
28
29
  options?: OptionsResolver;
29
30
  dependentOptions?: DependentOptionsResolver;
31
+ /** Select fields only: fetch options matching what the user typed (e.g. a
32
+ * server-side search) instead of filtering the (possibly partial) `options`
33
+ * list in memory. Leave unset to keep the default in-memory filtering. */
34
+ search?: SearchResolver;
30
35
  disabled?: DisabledResolver;
31
36
  seed?: SeedResolver;
32
37
  component?: CellComponent<any, any>;
@@ -1,6 +1,6 @@
1
1
  import type { Component } from 'svelte';
2
2
  import type { FullAutoFill } from 'svelte/elements';
3
- import type { AttributeType } from './attribute.js';
3
+ import type { AttributeType, SearchResolver } from './attribute.js';
4
4
  import type { CellComponent, CellFormatter } from './table.js';
5
5
  export type ColumnDefinition<T extends object = Record<string, unknown>> = {
6
6
  [K in keyof T & string]: {
@@ -29,6 +29,7 @@ export interface FieldDefinition<T extends object = Record<string, unknown>> {
29
29
  value: string;
30
30
  label: string;
31
31
  }[];
32
+ search?: SearchResolver;
32
33
  disabled?: (record: Record<string, unknown>) => boolean;
33
34
  seed?: (instance: any) => unknown;
34
35
  groupedAs?: string;
@@ -51,7 +52,12 @@ export interface CustomAction<T extends object = Record<string, unknown>> {
51
52
  icon: any;
52
53
  endpoint?: string;
53
54
  condition?: (item: T) => boolean;
54
- view: Component<any>;
55
+ /** Renders as a modal-like panel when the action runs. Mutually exclusive
56
+ * with `href` — provide exactly one of the two. */
57
+ view?: Component<any>;
58
+ /** Navigates to the given URL instead of opening `view`. Takes priority
59
+ * over `view` if both are somehow set. */
60
+ href?: (item: T) => string;
55
61
  }
56
62
  export interface RowAction<T extends object = Record<string, unknown>> {
57
63
  label: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "runeforge",
3
- "version": "0.0.21",
3
+ "version": "0.0.22",
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",