runeforge 0.0.61 → 0.0.63

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 (67) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +1410 -1407
  3. package/dist/components/Avatar.svelte +31 -31
  4. package/dist/components/IconRenderer.svelte +73 -73
  5. package/dist/components/Modal.svelte +75 -75
  6. package/dist/components/common/Header.svelte +40 -40
  7. package/dist/components/crud/EmbeddedField.svelte +203 -203
  8. package/dist/components/crud/Field.svelte +122 -122
  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/fields/BooleanField.svelte +38 -38
  14. package/dist/components/crud/fields/DatetimeField.svelte +250 -250
  15. package/dist/components/crud/fields/DefaultField.svelte +67 -67
  16. package/dist/components/crud/fields/FileField.svelte +70 -70
  17. package/dist/components/crud/fields/MultiSelectField.svelte +70 -70
  18. package/dist/components/crud/fields/SelectField.svelte +67 -67
  19. package/dist/components/crud/fields/TextareaField.svelte +55 -55
  20. package/dist/components/crud/fields/TreeField.svelte +1 -0
  21. package/dist/components/crud/utils/resolution.js +1 -0
  22. package/dist/components/crud/utils/tree.d.ts +5 -0
  23. package/dist/components/crud/utils/tree.js +29 -0
  24. package/dist/components/crud/views/Create.svelte +239 -239
  25. package/dist/components/crud/views/Read.svelte +131 -131
  26. package/dist/components/crud/views/Update.svelte +215 -215
  27. package/dist/components/crud/views/list/List.svelte +291 -291
  28. package/dist/components/crud/views/list/Modals.svelte +56 -56
  29. package/dist/components/crud/views/list/Table.svelte +173 -173
  30. package/dist/components/crud/views/list/Toolbar.svelte +340 -340
  31. package/dist/components/form/Button.svelte +27 -27
  32. package/dist/components/form/Label.svelte +37 -37
  33. package/dist/components/form/MultiSelect.svelte +248 -248
  34. package/dist/components/form/PasswordInput.svelte +68 -68
  35. package/dist/components/form/Required.svelte +1 -1
  36. package/dist/components/form/Select.svelte +209 -209
  37. package/dist/components/form/Tree.svelte +43 -4
  38. package/dist/components/form/Tree.svelte.d.ts +1 -0
  39. package/dist/components/form/TreeNode.svelte +31 -2
  40. package/dist/components/form/TreeNode.svelte.d.ts +2 -0
  41. package/dist/components/navigation/Breadcrumbs.svelte +111 -111
  42. package/dist/components/table/ColumnFilter.svelte +168 -168
  43. package/dist/components/table/PaginatedTable.svelte +542 -542
  44. package/dist/components/table/Paginator.svelte +113 -113
  45. package/dist/components/table/SortHeader.svelte +43 -43
  46. package/dist/components/table/TableBody.svelte +150 -150
  47. package/dist/components/table/TableHeader.svelte +88 -88
  48. package/dist/components/table/state.svelte.js +7 -2
  49. package/dist/icons/defaults/Clear.svelte +6 -6
  50. package/dist/icons/defaults/Create.svelte +6 -6
  51. package/dist/icons/defaults/Delete.svelte +6 -6
  52. package/dist/icons/defaults/Download.svelte +7 -7
  53. package/dist/icons/defaults/Edit.svelte +7 -7
  54. package/dist/icons/defaults/Filter.svelte +6 -6
  55. package/dist/icons/defaults/FilterActive.svelte +6 -6
  56. package/dist/icons/defaults/Folder.svelte +6 -6
  57. package/dist/icons/defaults/Grip.svelte +7 -7
  58. package/dist/icons/defaults/Home.svelte +6 -6
  59. package/dist/icons/defaults/PasswordHide.svelte +9 -9
  60. package/dist/icons/defaults/PasswordShow.svelte +7 -7
  61. package/dist/icons/defaults/SortAsc.svelte +6 -6
  62. package/dist/icons/defaults/SortDesc.svelte +6 -6
  63. package/dist/icons/defaults/SortNone.svelte +6 -6
  64. package/dist/icons/defaults/View.svelte +7 -7
  65. package/dist/types/attribute.d.ts +3 -0
  66. package/dist/types/crud/fields.d.ts +3 -0
  67. package/package.json +1 -1
@@ -1,239 +1,239 @@
1
- <script lang="ts" generics="T extends object = Record<string, unknown>">
2
- import { untrack } from 'svelte';
3
- import { enhance } from '$app/forms';
4
- import Field from '../Field.svelte';
5
- import Button from '../../form/Button.svelte';
6
- import Header from '../../common/Header.svelte';
7
- import { getIconSet } from '../../../icons/context.js';
8
- import { defaultIconSet } from '../../../icons/sets/default.js';
9
- import { validateAll } from '../utils/validation.js';
10
- import { groupFields } from '../utils/grouping.js';
11
- import { applyDuplicateOmit, emptyRecord, seedRecord } from '../utils/embedded.js';
12
- import type { ActionConfiguration } from '../../../types/crud/actions.js';
13
- import type { FieldDefinition } from '../../../types/crud/fields.js';
14
- import { getStrings } from '../../../i18n/context.js';
15
-
16
- const strings = getStrings();
17
-
18
- let {
19
- labelOne = '',
20
- labelMany = '',
21
- icon,
22
- fields = [] as FieldDefinition<T>[],
23
- creation = {} as ActionConfiguration<T>,
24
- serverError = '',
25
- seed = undefined as Record<string, unknown> | undefined,
26
- onCancel,
27
- onSuccess,
28
- }: {
29
- labelOne?: string;
30
- labelMany?: string;
31
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
32
- icon?: any;
33
- fields?: FieldDefinition<T>[];
34
- creation?: ActionConfiguration<T>;
35
- serverError?: string;
36
- /** Pre-fills the form from another instance's values — used when the
37
- * create view is reached via an Update form's "Duplicate" button. */
38
- seed?: Record<string, unknown>;
39
- onCancel?: () => void;
40
- onSuccess?: () => void;
41
- } = $props();
42
-
43
- const icons = $derived(getIconSet() ?? defaultIconSet);
44
- const entityIcon = $derived(icon ?? icons.folder);
45
-
46
- let fieldErrors = $state<Record<string, string>>({});
47
- let internalError = $state('');
48
- let successMessage = $state('');
49
- let successTimeout: ReturnType<typeof setTimeout> | undefined;
50
- let continueCreating = $state(false);
51
- let duplicating = $state(false);
52
-
53
- function flashSuccess() {
54
- successMessage = strings.saveSuccess;
55
- clearTimeout(successTimeout);
56
- successTimeout = setTimeout(() => (successMessage = ''), 4000);
57
- }
58
-
59
- let record = $state<Record<string, unknown>>(
60
- untrack(() => (seed ? seedRecord(fields, seed) : emptyRecord(fields)))
61
- );
62
-
63
- const groups = $derived(groupFields(fields));
64
- const hasFileField = $derived(fields.some((f) => f.type === 'file'));
65
-
66
- const continueEnabled = $derived(creation.continue?.enabled ?? true);
67
- const continueLabel = $derived(creation.continue?.label ?? strings.saveAndContinue);
68
- const continueClass = $derived(creation.continue?.class ?? '');
69
-
70
- const duplicationEnabled = $derived(creation.duplication?.enabled ?? false);
71
- const duplicationLabel = $derived(creation.duplication?.label ?? strings.duplicate);
72
- const duplicationClass = $derived(creation.duplication?.class ?? '');
73
-
74
- const errorEntries = $derived([
75
- ...((serverError || internalError) ? [['_global', internalError || serverError] as [string, string]] : []),
76
- ...Object.entries(fieldErrors),
77
- ]);
78
- </script>
79
-
80
- <div class="flex flex-col gap-6">
81
-
82
- <div class="sticky-header sticky z-10 flex flex-col gap-6 bg-base-100 pb-2">
83
- <Header
84
- title={labelMany}
85
- breadcrumbs={[
86
- { label: labelMany, icon: entityIcon, link: { href: '#', onclick: (e) => { e.preventDefault(); onCancel?.(); } }, prominent: true },
87
- { label: creation.label ?? labelOne, icon: icons.create },
88
- ]}
89
- />
90
-
91
- {#if successMessage}
92
- <div role="alert" class="alert alert-success">
93
- <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
94
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
95
- </svg>
96
- <span class="text-sm">{successMessage}</span>
97
- </div>
98
- {/if}
99
-
100
- {#if errorEntries.length > 0}
101
- <div role="alert" class="alert alert-error">
102
- <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
103
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
104
- </svg>
105
- <ul class="list-disc list-inside text-sm">
106
- {#each errorEntries as [key, msg] (key)}
107
- <li>{msg}</li>
108
- {/each}
109
- </ul>
110
- </div>
111
- {/if}
112
- </div>
113
-
114
- <form
115
- method="POST"
116
- action={creation.endpoint ?? '?/create'}
117
- enctype={hasFileField ? 'multipart/form-data' : undefined}
118
- class="mx-auto flex w-full flex-col gap-4 px-4"
119
- use:enhance={({ formData, cancel }) => {
120
- fieldErrors = {};
121
- internalError = '';
122
- successMessage = '';
123
- clearTimeout(successTimeout);
124
- const errs = validateAll(fields, formData, strings);
125
- if (Object.keys(errs).length > 0) {
126
- fieldErrors = errs;
127
- cancel();
128
- return;
129
- }
130
- return async ({ result, update }) => {
131
- if (result.type === 'success' || result.type === 'redirect') {
132
- await update({ reset: false });
133
- if (continueCreating) {
134
- record = emptyRecord(fields);
135
- fieldErrors = {};
136
- internalError = '';
137
- flashSuccess();
138
- } else if (duplicating) {
139
- fieldErrors = {};
140
- internalError = '';
141
- flashSuccess();
142
- record = applyDuplicateOmit(fields, record, creation.duplication?.omit);
143
- } else {
144
- onSuccess?.();
145
- }
146
- } else if (result.type === 'error') {
147
- internalError = result.error?.message ?? strings.serverError;
148
- } else {
149
- await update({ reset: false });
150
- }
151
- };
152
- }}
153
- >
154
- {#each groups as group, i (group.title ?? `_ungrouped_${i}`)}
155
- {#if group.title}
156
- <fieldset class="fieldset border border-base-300 rounded-box p-4">
157
- <legend class="fieldset-legend px-2">{group.title}</legend>
158
- <div class="flex flex-col gap-4">
159
- {#each group.rows as row (row.map((f) => f.attribute).join('|'))}
160
- {#if row.length > 1}
161
- <div class="flex flex-col gap-4 md:flex-row">
162
- {#each row as field (field.attribute)}
163
- <Field {field} bind:record error={fieldErrors[field.attribute] ?? ''} class="md:min-w-0 md:flex-1" />
164
- {/each}
165
- </div>
166
- {:else}
167
- <Field field={row[0]} bind:record error={fieldErrors[row[0].attribute] ?? ''} />
168
- {/if}
169
- {/each}
170
- </div>
171
- </fieldset>
172
- {:else}
173
- {#each group.rows as row (row.map((f) => f.attribute).join('|'))}
174
- {#if row.length > 1}
175
- <div class="flex flex-col gap-4 md:flex-row">
176
- {#each row as field (field.attribute)}
177
- <Field {field} bind:record error={fieldErrors[field.attribute] ?? ''} class="md:min-w-0 md:flex-1" />
178
- {/each}
179
- </div>
180
- {:else}
181
- <Field field={row[0]} bind:record error={fieldErrors[row[0].attribute] ?? ''} />
182
- {/if}
183
- {/each}
184
- {/if}
185
- {/each}
186
-
187
- <div class="flex flex-col-reverse gap-2 pt-2 sm:flex-row sm:justify-end">
188
- <Button variant="ghost" onclick={() => onCancel?.()}>
189
- {strings.cancel}
190
- </Button>
191
- {#if duplicationEnabled}
192
- <Button
193
- type="submit"
194
- variant="warning"
195
- class={duplicationClass}
196
- onclick={() => {
197
- continueCreating = false;
198
- duplicating = true;
199
- }}
200
- >
201
- {duplicationLabel}
202
- </Button>
203
- {/if}
204
- {#if continueEnabled}
205
- <Button
206
- type="submit"
207
- variant="secondary"
208
- class={continueClass}
209
- onclick={() => {
210
- continueCreating = true;
211
- duplicating = false;
212
- }}
213
- >
214
- {continueLabel}
215
- </Button>
216
- {/if}
217
- <Button
218
- type="submit"
219
- variant="primary"
220
- onclick={() => {
221
- continueCreating = false;
222
- duplicating = false;
223
- }}
224
- >
225
- {strings.save}
226
- </Button>
227
- </div>
228
- </form>
229
- </div>
230
-
231
- <style>
232
- form {
233
- max-width: var(--runeforge-form-max-width, 32rem);
234
- }
235
-
236
- .sticky-header {
237
- top: var(--runeforge-sticky-header-top, 0);
238
- }
239
- </style>
1
+ <script lang="ts" generics="T extends object = Record<string, unknown>">
2
+ import { untrack } from 'svelte';
3
+ import { enhance } from '$app/forms';
4
+ import Field from '../Field.svelte';
5
+ import Button from '../../form/Button.svelte';
6
+ import Header from '../../common/Header.svelte';
7
+ import { getIconSet } from '../../../icons/context.js';
8
+ import { defaultIconSet } from '../../../icons/sets/default.js';
9
+ import { validateAll } from '../utils/validation.js';
10
+ import { groupFields } from '../utils/grouping.js';
11
+ import { applyDuplicateOmit, emptyRecord, seedRecord } from '../utils/embedded.js';
12
+ import type { ActionConfiguration } from '../../../types/crud/actions.js';
13
+ import type { FieldDefinition } from '../../../types/crud/fields.js';
14
+ import { getStrings } from '../../../i18n/context.js';
15
+
16
+ const strings = getStrings();
17
+
18
+ let {
19
+ labelOne = '',
20
+ labelMany = '',
21
+ icon,
22
+ fields = [] as FieldDefinition<T>[],
23
+ creation = {} as ActionConfiguration<T>,
24
+ serverError = '',
25
+ seed = undefined as Record<string, unknown> | undefined,
26
+ onCancel,
27
+ onSuccess,
28
+ }: {
29
+ labelOne?: string;
30
+ labelMany?: string;
31
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
32
+ icon?: any;
33
+ fields?: FieldDefinition<T>[];
34
+ creation?: ActionConfiguration<T>;
35
+ serverError?: string;
36
+ /** Pre-fills the form from another instance's values — used when the
37
+ * create view is reached via an Update form's "Duplicate" button. */
38
+ seed?: Record<string, unknown>;
39
+ onCancel?: () => void;
40
+ onSuccess?: () => void;
41
+ } = $props();
42
+
43
+ const icons = $derived(getIconSet() ?? defaultIconSet);
44
+ const entityIcon = $derived(icon ?? icons.folder);
45
+
46
+ let fieldErrors = $state<Record<string, string>>({});
47
+ let internalError = $state('');
48
+ let successMessage = $state('');
49
+ let successTimeout: ReturnType<typeof setTimeout> | undefined;
50
+ let continueCreating = $state(false);
51
+ let duplicating = $state(false);
52
+
53
+ function flashSuccess() {
54
+ successMessage = strings.saveSuccess;
55
+ clearTimeout(successTimeout);
56
+ successTimeout = setTimeout(() => (successMessage = ''), 4000);
57
+ }
58
+
59
+ let record = $state<Record<string, unknown>>(
60
+ untrack(() => (seed ? seedRecord(fields, seed) : emptyRecord(fields)))
61
+ );
62
+
63
+ const groups = $derived(groupFields(fields));
64
+ const hasFileField = $derived(fields.some((f) => f.type === 'file'));
65
+
66
+ const continueEnabled = $derived(creation.continue?.enabled ?? true);
67
+ const continueLabel = $derived(creation.continue?.label ?? strings.saveAndContinue);
68
+ const continueClass = $derived(creation.continue?.class ?? '');
69
+
70
+ const duplicationEnabled = $derived(creation.duplication?.enabled ?? false);
71
+ const duplicationLabel = $derived(creation.duplication?.label ?? strings.duplicate);
72
+ const duplicationClass = $derived(creation.duplication?.class ?? '');
73
+
74
+ const errorEntries = $derived([
75
+ ...((serverError || internalError) ? [['_global', internalError || serverError] as [string, string]] : []),
76
+ ...Object.entries(fieldErrors),
77
+ ]);
78
+ </script>
79
+
80
+ <div class="flex flex-col gap-6">
81
+
82
+ <div class="sticky-header sticky z-10 flex flex-col gap-6 bg-base-100 pb-2">
83
+ <Header
84
+ title={labelMany}
85
+ breadcrumbs={[
86
+ { label: labelMany, icon: entityIcon, link: { href: '#', onclick: (e) => { e.preventDefault(); onCancel?.(); } }, prominent: true },
87
+ { label: creation.label ?? labelOne, icon: icons.create },
88
+ ]}
89
+ />
90
+
91
+ {#if successMessage}
92
+ <div role="alert" class="alert alert-success">
93
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
94
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
95
+ </svg>
96
+ <span class="text-sm">{successMessage}</span>
97
+ </div>
98
+ {/if}
99
+
100
+ {#if errorEntries.length > 0}
101
+ <div role="alert" class="alert alert-error">
102
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
103
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
104
+ </svg>
105
+ <ul class="list-disc list-inside text-sm">
106
+ {#each errorEntries as [key, msg] (key)}
107
+ <li>{msg}</li>
108
+ {/each}
109
+ </ul>
110
+ </div>
111
+ {/if}
112
+ </div>
113
+
114
+ <form
115
+ method="POST"
116
+ action={creation.endpoint ?? '?/create'}
117
+ enctype={hasFileField ? 'multipart/form-data' : undefined}
118
+ class="mx-auto flex w-full flex-col gap-4 px-4"
119
+ use:enhance={({ formData, cancel }) => {
120
+ fieldErrors = {};
121
+ internalError = '';
122
+ successMessage = '';
123
+ clearTimeout(successTimeout);
124
+ const errs = validateAll(fields, formData, strings);
125
+ if (Object.keys(errs).length > 0) {
126
+ fieldErrors = errs;
127
+ cancel();
128
+ return;
129
+ }
130
+ return async ({ result, update }) => {
131
+ if (result.type === 'success' || result.type === 'redirect') {
132
+ await update({ reset: false });
133
+ if (continueCreating) {
134
+ record = emptyRecord(fields);
135
+ fieldErrors = {};
136
+ internalError = '';
137
+ flashSuccess();
138
+ } else if (duplicating) {
139
+ fieldErrors = {};
140
+ internalError = '';
141
+ flashSuccess();
142
+ record = applyDuplicateOmit(fields, record, creation.duplication?.omit);
143
+ } else {
144
+ onSuccess?.();
145
+ }
146
+ } else if (result.type === 'error') {
147
+ internalError = result.error?.message ?? strings.serverError;
148
+ } else {
149
+ await update({ reset: false });
150
+ }
151
+ };
152
+ }}
153
+ >
154
+ {#each groups as group, i (group.title ?? `_ungrouped_${i}`)}
155
+ {#if group.title}
156
+ <fieldset class="fieldset border border-base-300 rounded-box p-4">
157
+ <legend class="fieldset-legend px-2">{group.title}</legend>
158
+ <div class="flex flex-col gap-4">
159
+ {#each group.rows as row (row.map((f) => f.attribute).join('|'))}
160
+ {#if row.length > 1}
161
+ <div class="flex flex-col gap-4 md:flex-row">
162
+ {#each row as field (field.attribute)}
163
+ <Field {field} bind:record error={fieldErrors[field.attribute] ?? ''} class="md:min-w-0 md:flex-1" />
164
+ {/each}
165
+ </div>
166
+ {:else}
167
+ <Field field={row[0]} bind:record error={fieldErrors[row[0].attribute] ?? ''} />
168
+ {/if}
169
+ {/each}
170
+ </div>
171
+ </fieldset>
172
+ {:else}
173
+ {#each group.rows as row (row.map((f) => f.attribute).join('|'))}
174
+ {#if row.length > 1}
175
+ <div class="flex flex-col gap-4 md:flex-row">
176
+ {#each row as field (field.attribute)}
177
+ <Field {field} bind:record error={fieldErrors[field.attribute] ?? ''} class="md:min-w-0 md:flex-1" />
178
+ {/each}
179
+ </div>
180
+ {:else}
181
+ <Field field={row[0]} bind:record error={fieldErrors[row[0].attribute] ?? ''} />
182
+ {/if}
183
+ {/each}
184
+ {/if}
185
+ {/each}
186
+
187
+ <div class="flex flex-col-reverse gap-2 pt-2 sm:flex-row sm:justify-end">
188
+ <Button variant="ghost" onclick={() => onCancel?.()}>
189
+ {strings.cancel}
190
+ </Button>
191
+ {#if duplicationEnabled}
192
+ <Button
193
+ type="submit"
194
+ variant="warning"
195
+ class={duplicationClass}
196
+ onclick={() => {
197
+ continueCreating = false;
198
+ duplicating = true;
199
+ }}
200
+ >
201
+ {duplicationLabel}
202
+ </Button>
203
+ {/if}
204
+ {#if continueEnabled}
205
+ <Button
206
+ type="submit"
207
+ variant="secondary"
208
+ class={continueClass}
209
+ onclick={() => {
210
+ continueCreating = true;
211
+ duplicating = false;
212
+ }}
213
+ >
214
+ {continueLabel}
215
+ </Button>
216
+ {/if}
217
+ <Button
218
+ type="submit"
219
+ variant="primary"
220
+ onclick={() => {
221
+ continueCreating = false;
222
+ duplicating = false;
223
+ }}
224
+ >
225
+ {strings.save}
226
+ </Button>
227
+ </div>
228
+ </form>
229
+ </div>
230
+
231
+ <style>
232
+ form {
233
+ max-width: var(--runeforge-form-max-width, 32rem);
234
+ }
235
+
236
+ .sticky-header {
237
+ top: var(--runeforge-sticky-header-top, 0);
238
+ }
239
+ </style>