runeforge 0.0.55 → 0.0.56

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.
Files changed (54) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +1342 -1342
  3. package/dist/components/Avatar.svelte +31 -31
  4. package/dist/components/IconRenderer.svelte +22 -22
  5. package/dist/components/Modal.svelte +75 -75
  6. package/dist/components/common/Header.svelte +40 -40
  7. package/dist/components/crud/EmbeddedField.svelte +175 -175
  8. package/dist/components/crud/Field.svelte +469 -376
  9. package/dist/components/crud/GenericCRUD.svelte +426 -426
  10. package/dist/components/crud/SearchInput.svelte +53 -53
  11. package/dist/components/crud/columns/Avatar.svelte +15 -15
  12. package/dist/components/crud/columns/Icon.svelte +8 -8
  13. package/dist/components/crud/views/Create.svelte +232 -232
  14. package/dist/components/crud/views/Read.svelte +124 -124
  15. package/dist/components/crud/views/Update.svelte +208 -208
  16. package/dist/components/crud/views/list/List.svelte +291 -291
  17. package/dist/components/crud/views/list/Modals.svelte +56 -56
  18. package/dist/components/crud/views/list/Table.svelte +176 -176
  19. package/dist/components/crud/views/list/Toolbar.svelte +341 -341
  20. package/dist/components/form/Button.svelte +27 -27
  21. package/dist/components/form/Label.svelte +37 -37
  22. package/dist/components/form/MultiSelect.svelte +248 -248
  23. package/dist/components/form/PasswordInput.svelte +68 -68
  24. package/dist/components/form/Required.svelte +1 -1
  25. package/dist/components/form/Select.svelte +209 -209
  26. package/dist/components/form/Tree.svelte +62 -62
  27. package/dist/components/form/TreeNode.svelte +66 -66
  28. package/dist/components/navigation/Breadcrumbs.svelte +111 -111
  29. package/dist/components/table/ColumnFilter.svelte +168 -168
  30. package/dist/components/table/PaginatedTable.svelte +536 -536
  31. package/dist/components/table/Paginator.svelte +113 -113
  32. package/dist/components/table/SortHeader.svelte +43 -43
  33. package/dist/components/table/TableBody.svelte +150 -150
  34. package/dist/components/table/TableHeader.svelte +88 -88
  35. package/dist/i18n/en.js +1 -0
  36. package/dist/i18n/es.js +1 -0
  37. package/dist/i18n/types.d.ts +1 -0
  38. package/dist/icons/defaults/Clear.svelte +6 -6
  39. package/dist/icons/defaults/Create.svelte +6 -6
  40. package/dist/icons/defaults/Delete.svelte +6 -6
  41. package/dist/icons/defaults/Download.svelte +7 -7
  42. package/dist/icons/defaults/Edit.svelte +7 -7
  43. package/dist/icons/defaults/Filter.svelte +6 -6
  44. package/dist/icons/defaults/FilterActive.svelte +6 -6
  45. package/dist/icons/defaults/Folder.svelte +6 -6
  46. package/dist/icons/defaults/Grip.svelte +7 -7
  47. package/dist/icons/defaults/Home.svelte +6 -6
  48. package/dist/icons/defaults/PasswordHide.svelte +9 -9
  49. package/dist/icons/defaults/PasswordShow.svelte +7 -7
  50. package/dist/icons/defaults/SortAsc.svelte +6 -6
  51. package/dist/icons/defaults/SortDesc.svelte +6 -6
  52. package/dist/icons/defaults/SortNone.svelte +6 -6
  53. package/dist/icons/defaults/View.svelte +7 -7
  54. package/package.json +1 -1
@@ -1,175 +1,175 @@
1
- <script lang="ts" generics="T extends object = Record<string, unknown>">
2
- import Field from './Field.svelte';
3
- import Button from '../form/Button.svelte';
4
- import Modal from '../Modal.svelte';
5
- import { emptyRecord, seedField, defaultItemLabel } from './utils/embedded.js';
6
- import { fieldLabel } from './utils/misc.js';
7
- import { validateAll } from './utils/validation.js';
8
- import { groupFields } from './utils/grouping.js';
9
- import type { FieldDefinition } from '../../types/crud.js';
10
- import { getStrings } from '../../i18n/context.js';
11
-
12
- const strings = getStrings();
13
-
14
- let {
15
- field,
16
- record = $bindable({} as Record<string, unknown>),
17
- readonly = false
18
- }: {
19
- field: FieldDefinition<T>;
20
- record?: Record<string, unknown>;
21
- readonly?: boolean;
22
- } = $props();
23
-
24
- const subFields = $derived(field.fields ?? []);
25
- const subGroups = $derived(groupFields(subFields));
26
- const items = $derived((record[field.attribute] as Record<string, unknown>[] | undefined) ?? []);
27
-
28
- let modalOpen = $state(false);
29
- // null while adding a new item; the item's index while editing an existing
30
- // one, so saveItem knows whether to append or replace in place.
31
- let editingIndex = $state<number | null>(null);
32
- let draft = $state<Record<string, unknown>>({});
33
- let draftErrors = $state<Record<string, string>>({});
34
-
35
- function openCreateModal() {
36
- editingIndex = null;
37
- draft = emptyRecord(subFields);
38
- draftErrors = {};
39
- modalOpen = true;
40
- }
41
-
42
- function openEditModal(index: number) {
43
- const item = items[index];
44
- editingIndex = index;
45
- draft = Object.fromEntries(
46
- subFields.map((f) => [f.attribute, seedField(f, f.seed ? f.seed(item) : item[f.attribute])])
47
- );
48
- draftErrors = {};
49
- modalOpen = true;
50
- }
51
-
52
- function closeModal() {
53
- modalOpen = false;
54
- }
55
-
56
- function coerce(f: FieldDefinition, raw: unknown): unknown {
57
- if (f.type === 'number') return raw === '' || raw == null ? null : Number(raw);
58
- return raw;
59
- }
60
-
61
- function saveItem() {
62
- const fd = new FormData();
63
- for (const f of subFields) fd.set(f.attribute, String(draft[f.attribute] ?? ''));
64
- const errs = validateAll(subFields, fd, strings);
65
- if (Object.keys(errs).length > 0) {
66
- draftErrors = errs;
67
- return;
68
- }
69
- const item = Object.fromEntries(
70
- subFields.map((f) => [f.attribute, coerce(f, draft[f.attribute])])
71
- );
72
- record[field.attribute] =
73
- editingIndex == null
74
- ? [...items, item]
75
- : items.map((existing, i) => (i === editingIndex ? item : existing));
76
- modalOpen = false;
77
- }
78
-
79
- function removeItem(index: number) {
80
- record[field.attribute] = items.filter((_, i) => i !== index);
81
- }
82
-
83
- function itemLabel(item: Record<string, unknown>): string {
84
- return field.itemLabel ? field.itemLabel(item) : defaultItemLabel(subFields, item);
85
- }
86
- </script>
87
-
88
- <div class="flex flex-col gap-2">
89
- {#if !readonly}
90
- <input type="hidden" name={field.attribute} value={JSON.stringify(items)} />
91
- {/if}
92
-
93
- {#if items.length === 0}
94
- <p class="text-sm text-base-content/50">{strings.noItems}</p>
95
- {:else}
96
- <ul class="flex flex-col gap-1">
97
- {#each items as item, i (i)}
98
- <li
99
- class="flex items-center justify-between gap-2 rounded-box border border-base-300 px-3 py-2 text-sm"
100
- >
101
- {#if readonly}
102
- <span>{itemLabel(item)}</span>
103
- {:else}
104
- <button
105
- type="button"
106
- class="flex-1 text-left hover:underline"
107
- onclick={() => openEditModal(i)}
108
- >
109
- {itemLabel(item)}
110
- </button>
111
- <Button
112
- variant="ghost"
113
- class="btn-xs btn-circle"
114
- aria-label={strings.remove}
115
- onclick={() => removeItem(i)}
116
- >
117
-
118
- </Button>
119
- {/if}
120
- </li>
121
- {/each}
122
- </ul>
123
- {/if}
124
-
125
- {#if !readonly}
126
- <Button variant="outline" class="btn-sm self-start" onclick={openCreateModal}>
127
- + {strings.add}
128
- </Button>
129
- {/if}
130
- </div>
131
-
132
- {#if modalOpen}
133
- <Modal title={fieldLabel(field)} onClose={closeModal}>
134
- <div class="flex flex-col gap-4">
135
- {#each subGroups as group, i (group.title ?? `_ungrouped_${i}`)}
136
- {#if group.title}
137
- <fieldset class="fieldset border border-base-300 rounded-box p-4">
138
- <legend class="fieldset-legend px-2">{group.title}</legend>
139
- <div class="flex flex-col gap-4">
140
- {#each group.rows as row (row.map((f) => f.attribute).join('|'))}
141
- {#if row.length > 1}
142
- <div class="flex flex-col gap-4 md:flex-row">
143
- {#each row as f (f.attribute)}
144
- <Field field={f} bind:record={draft} error={draftErrors[f.attribute] ?? ''} class="md:min-w-0 md:flex-1" />
145
- {/each}
146
- </div>
147
- {:else}
148
- <Field field={row[0]} bind:record={draft} error={draftErrors[row[0].attribute] ?? ''} />
149
- {/if}
150
- {/each}
151
- </div>
152
- </fieldset>
153
- {:else}
154
- {#each group.rows as row (row.map((f) => f.attribute).join('|'))}
155
- {#if row.length > 1}
156
- <div class="flex flex-col gap-4 md:flex-row">
157
- {#each row as f (f.attribute)}
158
- <Field field={f} bind:record={draft} error={draftErrors[f.attribute] ?? ''} class="md:min-w-0 md:flex-1" />
159
- {/each}
160
- </div>
161
- {:else}
162
- <Field field={row[0]} bind:record={draft} error={draftErrors[row[0].attribute] ?? ''} />
163
- {/if}
164
- {/each}
165
- {/if}
166
- {/each}
167
- <div class="flex flex-col-reverse gap-2 pt-2 sm:flex-row sm:justify-end">
168
- <Button variant="ghost" onclick={closeModal}>{strings.cancel}</Button>
169
- <Button variant="primary" onclick={saveItem}>
170
- {editingIndex == null ? strings.add : strings.edit}
171
- </Button>
172
- </div>
173
- </div>
174
- </Modal>
175
- {/if}
1
+ <script lang="ts" generics="T extends object = Record<string, unknown>">
2
+ import Field from './Field.svelte';
3
+ import Button from '../form/Button.svelte';
4
+ import Modal from '../Modal.svelte';
5
+ import { emptyRecord, seedField, defaultItemLabel } from './utils/embedded.js';
6
+ import { fieldLabel } from './utils/misc.js';
7
+ import { validateAll } from './utils/validation.js';
8
+ import { groupFields } from './utils/grouping.js';
9
+ import type { FieldDefinition } from '../../types/crud.js';
10
+ import { getStrings } from '../../i18n/context.js';
11
+
12
+ const strings = getStrings();
13
+
14
+ let {
15
+ field,
16
+ record = $bindable({} as Record<string, unknown>),
17
+ readonly = false
18
+ }: {
19
+ field: FieldDefinition<T>;
20
+ record?: Record<string, unknown>;
21
+ readonly?: boolean;
22
+ } = $props();
23
+
24
+ const subFields = $derived(field.fields ?? []);
25
+ const subGroups = $derived(groupFields(subFields));
26
+ const items = $derived((record[field.attribute] as Record<string, unknown>[] | undefined) ?? []);
27
+
28
+ let modalOpen = $state(false);
29
+ // null while adding a new item; the item's index while editing an existing
30
+ // one, so saveItem knows whether to append or replace in place.
31
+ let editingIndex = $state<number | null>(null);
32
+ let draft = $state<Record<string, unknown>>({});
33
+ let draftErrors = $state<Record<string, string>>({});
34
+
35
+ function openCreateModal() {
36
+ editingIndex = null;
37
+ draft = emptyRecord(subFields);
38
+ draftErrors = {};
39
+ modalOpen = true;
40
+ }
41
+
42
+ function openEditModal(index: number) {
43
+ const item = items[index];
44
+ editingIndex = index;
45
+ draft = Object.fromEntries(
46
+ subFields.map((f) => [f.attribute, seedField(f, f.seed ? f.seed(item) : item[f.attribute])])
47
+ );
48
+ draftErrors = {};
49
+ modalOpen = true;
50
+ }
51
+
52
+ function closeModal() {
53
+ modalOpen = false;
54
+ }
55
+
56
+ function coerce(f: FieldDefinition, raw: unknown): unknown {
57
+ if (f.type === 'number') return raw === '' || raw == null ? null : Number(raw);
58
+ return raw;
59
+ }
60
+
61
+ function saveItem() {
62
+ const fd = new FormData();
63
+ for (const f of subFields) fd.set(f.attribute, String(draft[f.attribute] ?? ''));
64
+ const errs = validateAll(subFields, fd, strings);
65
+ if (Object.keys(errs).length > 0) {
66
+ draftErrors = errs;
67
+ return;
68
+ }
69
+ const item = Object.fromEntries(
70
+ subFields.map((f) => [f.attribute, coerce(f, draft[f.attribute])])
71
+ );
72
+ record[field.attribute] =
73
+ editingIndex == null
74
+ ? [...items, item]
75
+ : items.map((existing, i) => (i === editingIndex ? item : existing));
76
+ modalOpen = false;
77
+ }
78
+
79
+ function removeItem(index: number) {
80
+ record[field.attribute] = items.filter((_, i) => i !== index);
81
+ }
82
+
83
+ function itemLabel(item: Record<string, unknown>): string {
84
+ return field.itemLabel ? field.itemLabel(item) : defaultItemLabel(subFields, item);
85
+ }
86
+ </script>
87
+
88
+ <div class="flex flex-col gap-2">
89
+ {#if !readonly}
90
+ <input type="hidden" name={field.attribute} value={JSON.stringify(items)} />
91
+ {/if}
92
+
93
+ {#if items.length === 0}
94
+ <p class="text-sm text-base-content/50">{strings.noItems}</p>
95
+ {:else}
96
+ <ul class="flex flex-col gap-1">
97
+ {#each items as item, i (i)}
98
+ <li
99
+ class="flex items-center justify-between gap-2 rounded-box border border-base-300 px-3 py-2 text-sm"
100
+ >
101
+ {#if readonly}
102
+ <span>{itemLabel(item)}</span>
103
+ {:else}
104
+ <button
105
+ type="button"
106
+ class="flex-1 text-left hover:underline"
107
+ onclick={() => openEditModal(i)}
108
+ >
109
+ {itemLabel(item)}
110
+ </button>
111
+ <Button
112
+ variant="ghost"
113
+ class="btn-xs btn-circle"
114
+ aria-label={strings.remove}
115
+ onclick={() => removeItem(i)}
116
+ >
117
+
118
+ </Button>
119
+ {/if}
120
+ </li>
121
+ {/each}
122
+ </ul>
123
+ {/if}
124
+
125
+ {#if !readonly}
126
+ <Button variant="outline" class="btn-sm self-start" onclick={openCreateModal}>
127
+ + {strings.add}
128
+ </Button>
129
+ {/if}
130
+ </div>
131
+
132
+ {#if modalOpen}
133
+ <Modal title={fieldLabel(field)} onClose={closeModal}>
134
+ <div class="flex flex-col gap-4">
135
+ {#each subGroups as group, i (group.title ?? `_ungrouped_${i}`)}
136
+ {#if group.title}
137
+ <fieldset class="fieldset border border-base-300 rounded-box p-4">
138
+ <legend class="fieldset-legend px-2">{group.title}</legend>
139
+ <div class="flex flex-col gap-4">
140
+ {#each group.rows as row (row.map((f) => f.attribute).join('|'))}
141
+ {#if row.length > 1}
142
+ <div class="flex flex-col gap-4 md:flex-row">
143
+ {#each row as f (f.attribute)}
144
+ <Field field={f} bind:record={draft} error={draftErrors[f.attribute] ?? ''} class="md:min-w-0 md:flex-1" />
145
+ {/each}
146
+ </div>
147
+ {:else}
148
+ <Field field={row[0]} bind:record={draft} error={draftErrors[row[0].attribute] ?? ''} />
149
+ {/if}
150
+ {/each}
151
+ </div>
152
+ </fieldset>
153
+ {:else}
154
+ {#each group.rows as row (row.map((f) => f.attribute).join('|'))}
155
+ {#if row.length > 1}
156
+ <div class="flex flex-col gap-4 md:flex-row">
157
+ {#each row as f (f.attribute)}
158
+ <Field field={f} bind:record={draft} error={draftErrors[f.attribute] ?? ''} class="md:min-w-0 md:flex-1" />
159
+ {/each}
160
+ </div>
161
+ {:else}
162
+ <Field field={row[0]} bind:record={draft} error={draftErrors[row[0].attribute] ?? ''} />
163
+ {/if}
164
+ {/each}
165
+ {/if}
166
+ {/each}
167
+ <div class="flex flex-col-reverse gap-2 pt-2 sm:flex-row sm:justify-end">
168
+ <Button variant="ghost" onclick={closeModal}>{strings.cancel}</Button>
169
+ <Button variant="primary" onclick={saveItem}>
170
+ {editingIndex == null ? strings.add : strings.edit}
171
+ </Button>
172
+ </div>
173
+ </div>
174
+ </Modal>
175
+ {/if}