runeforge 0.0.53 → 0.0.55
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.
- package/LICENSE +21 -21
- package/README.md +1342 -1222
- package/dist/components/Avatar.svelte +31 -31
- package/dist/components/IconRenderer.svelte +22 -22
- package/dist/components/Modal.svelte +75 -75
- package/dist/components/common/Header.svelte +40 -40
- package/dist/components/crud/EmbeddedField.svelte +175 -175
- package/dist/components/crud/Field.svelte +376 -376
- package/dist/components/crud/GenericCRUD.svelte +426 -435
- package/dist/components/crud/GenericCRUD.svelte.d.ts +6 -9
- package/dist/components/crud/SearchInput.svelte +53 -53
- package/dist/components/crud/columns/Avatar.svelte +15 -15
- package/dist/components/crud/columns/Icon.svelte +8 -8
- package/dist/components/crud/utils/reorder.d.ts +4 -0
- package/dist/components/crud/utils/reorder.js +13 -0
- package/dist/components/crud/views/Create.svelte +232 -232
- package/dist/components/crud/views/Read.svelte +124 -124
- package/dist/components/crud/views/Update.svelte +208 -208
- package/dist/components/crud/views/list/List.svelte +291 -0
- package/dist/components/crud/views/{List.svelte.d.ts → list/List.svelte.d.ts} +4 -14
- package/dist/components/crud/views/list/Modals.svelte +56 -0
- package/dist/components/crud/views/list/Modals.svelte.d.ts +36 -0
- package/dist/components/crud/views/list/Table.svelte +176 -0
- package/dist/components/crud/views/list/Table.svelte.d.ts +59 -0
- package/dist/components/crud/views/list/Toolbar.svelte +341 -0
- package/dist/components/crud/views/list/Toolbar.svelte.d.ts +46 -0
- package/dist/components/form/Button.svelte +27 -27
- package/dist/components/form/Label.svelte +37 -37
- package/dist/components/form/MultiSelect.svelte +248 -248
- package/dist/components/form/PasswordInput.svelte +68 -68
- package/dist/components/form/Required.svelte +1 -1
- package/dist/components/form/Select.svelte +209 -209
- package/dist/components/form/Tree.svelte +62 -62
- package/dist/components/form/TreeNode.svelte +66 -66
- package/dist/components/navigation/Breadcrumbs.svelte +111 -111
- package/dist/components/table/ColumnFilter.svelte +168 -168
- package/dist/components/table/PaginatedTable.svelte +536 -215
- package/dist/components/table/PaginatedTable.svelte.d.ts +11 -2
- package/dist/components/table/Paginator.svelte +113 -113
- package/dist/components/table/SortHeader.svelte +43 -43
- package/dist/components/table/TableBody.svelte +150 -70
- package/dist/components/table/TableBody.svelte.d.ts +14 -1
- package/dist/components/table/TableHeader.svelte +88 -83
- package/dist/components/table/TableHeader.svelte.d.ts +1 -0
- package/dist/components/table/sortable.d.ts +27 -0
- package/dist/components/table/sortable.js +55 -0
- package/dist/components/table/utils.d.ts +28 -1
- package/dist/components/table/utils.js +75 -0
- package/dist/i18n/en.js +2 -0
- package/dist/i18n/es.js +2 -0
- package/dist/i18n/types.d.ts +2 -0
- package/dist/icons/defaults/Clear.svelte +6 -6
- package/dist/icons/defaults/Create.svelte +6 -6
- package/dist/icons/defaults/Delete.svelte +6 -6
- package/dist/icons/defaults/Download.svelte +7 -7
- package/dist/icons/defaults/Edit.svelte +7 -7
- package/dist/icons/defaults/Filter.svelte +6 -6
- package/dist/icons/defaults/FilterActive.svelte +6 -6
- package/dist/icons/defaults/Folder.svelte +6 -6
- package/dist/icons/defaults/Grip.svelte +7 -0
- package/dist/icons/defaults/Grip.svelte.d.ts +7 -0
- package/dist/icons/defaults/Home.svelte +6 -6
- package/dist/icons/defaults/PasswordHide.svelte +9 -9
- package/dist/icons/defaults/PasswordShow.svelte +7 -7
- package/dist/icons/defaults/SortAsc.svelte +6 -6
- package/dist/icons/defaults/SortDesc.svelte +6 -6
- package/dist/icons/defaults/SortNone.svelte +6 -6
- package/dist/icons/defaults/View.svelte +7 -7
- package/dist/icons/sets/bootstrap.js +2 -1
- package/dist/icons/sets/default.js +2 -0
- package/dist/icons/types.d.ts +3 -0
- package/dist/index.d.ts +5 -4
- package/dist/index.js +3 -2
- package/dist/types/attribute.d.ts +9 -0
- package/dist/types/crud.d.ts +72 -1
- package/dist/types/table.d.ts +56 -0
- package/package.json +3 -1
- package/dist/components/crud/views/List.svelte +0 -584
|
@@ -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}
|