runeforge 0.0.1

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 (119) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +551 -0
  3. package/dist/components/Avatar.svelte +31 -0
  4. package/dist/components/Avatar.svelte.d.ts +10 -0
  5. package/dist/components/IconRenderer.svelte +22 -0
  6. package/dist/components/IconRenderer.svelte.d.ts +8 -0
  7. package/dist/components/Modal.svelte +47 -0
  8. package/dist/components/Modal.svelte.d.ts +9 -0
  9. package/dist/components/common/Header.svelte +30 -0
  10. package/dist/components/common/Header.svelte.d.ts +11 -0
  11. package/dist/components/crud/Field.svelte +159 -0
  12. package/dist/components/crud/Field.svelte.d.ts +30 -0
  13. package/dist/components/crud/GenericCRUD.svelte +236 -0
  14. package/dist/components/crud/GenericCRUD.svelte.d.ts +44 -0
  15. package/dist/components/crud/columns/Avatar.svelte +15 -0
  16. package/dist/components/crud/columns/Avatar.svelte.d.ts +8 -0
  17. package/dist/components/crud/columns/Icon.svelte +8 -0
  18. package/dist/components/crud/columns/Icon.svelte.d.ts +4 -0
  19. package/dist/components/crud/utils/constants.d.ts +1 -0
  20. package/dist/components/crud/utils/constants.js +6 -0
  21. package/dist/components/crud/utils/formatters.d.ts +6 -0
  22. package/dist/components/crud/utils/formatters.js +42 -0
  23. package/dist/components/crud/utils/misc.d.ts +5 -0
  24. package/dist/components/crud/utils/misc.js +8 -0
  25. package/dist/components/crud/utils/resolution.d.ts +4 -0
  26. package/dist/components/crud/utils/resolution.js +22 -0
  27. package/dist/components/crud/views/Create.svelte +147 -0
  28. package/dist/components/crud/views/Create.svelte.d.ts +34 -0
  29. package/dist/components/crud/views/List.svelte +170 -0
  30. package/dist/components/crud/views/List.svelte.d.ts +41 -0
  31. package/dist/components/crud/views/Read.svelte +85 -0
  32. package/dist/components/crud/views/Read.svelte.d.ts +33 -0
  33. package/dist/components/crud/views/Update.svelte +148 -0
  34. package/dist/components/crud/views/Update.svelte.d.ts +35 -0
  35. package/dist/components/form/Button.svelte +27 -0
  36. package/dist/components/form/Button.svelte.d.ts +10 -0
  37. package/dist/components/form/Label.svelte +37 -0
  38. package/dist/components/form/Label.svelte.d.ts +14 -0
  39. package/dist/components/form/PasswordInput.svelte +61 -0
  40. package/dist/components/form/PasswordInput.svelte.d.ts +13 -0
  41. package/dist/components/form/Required.svelte +1 -0
  42. package/dist/components/form/Required.svelte.d.ts +26 -0
  43. package/dist/components/form/Select.svelte +114 -0
  44. package/dist/components/form/Select.svelte.d.ts +14 -0
  45. package/dist/components/navigation/Breadcrumbs.svelte +51 -0
  46. package/dist/components/navigation/Breadcrumbs.svelte.d.ts +9 -0
  47. package/dist/components/table/ColumnFilter.svelte +140 -0
  48. package/dist/components/table/ColumnFilter.svelte.d.ts +33 -0
  49. package/dist/components/table/PaginatedTable.svelte +106 -0
  50. package/dist/components/table/PaginatedTable.svelte.d.ts +35 -0
  51. package/dist/components/table/Paginator.svelte +62 -0
  52. package/dist/components/table/Paginator.svelte.d.ts +10 -0
  53. package/dist/components/table/SortHeader.svelte +43 -0
  54. package/dist/components/table/SortHeader.svelte.d.ts +10 -0
  55. package/dist/components/table/TableBody.svelte +70 -0
  56. package/dist/components/table/TableBody.svelte.d.ts +36 -0
  57. package/dist/components/table/TableHeader.svelte +83 -0
  58. package/dist/components/table/TableHeader.svelte.d.ts +39 -0
  59. package/dist/components/table/state.svelte.d.ts +30 -0
  60. package/dist/components/table/state.svelte.js +109 -0
  61. package/dist/components/table/utils.d.ts +7 -0
  62. package/dist/components/table/utils.js +44 -0
  63. package/dist/i18n/context.d.ts +3 -0
  64. package/dist/i18n/context.js +10 -0
  65. package/dist/i18n/en.d.ts +2 -0
  66. package/dist/i18n/en.js +24 -0
  67. package/dist/i18n/es.d.ts +2 -0
  68. package/dist/i18n/es.js +24 -0
  69. package/dist/i18n/types.d.ts +24 -0
  70. package/dist/i18n/types.js +1 -0
  71. package/dist/icons/bootstrapIconSet.d.ts +1 -0
  72. package/dist/icons/bootstrapIconSet.js +1 -0
  73. package/dist/icons/context.d.ts +3 -0
  74. package/dist/icons/context.js +9 -0
  75. package/dist/icons/defaultIconSet.d.ts +1 -0
  76. package/dist/icons/defaultIconSet.js +1 -0
  77. package/dist/icons/defaults/Create.svelte +6 -0
  78. package/dist/icons/defaults/Create.svelte.d.ts +7 -0
  79. package/dist/icons/defaults/Delete.svelte +6 -0
  80. package/dist/icons/defaults/Delete.svelte.d.ts +7 -0
  81. package/dist/icons/defaults/Edit.svelte +7 -0
  82. package/dist/icons/defaults/Edit.svelte.d.ts +7 -0
  83. package/dist/icons/defaults/Filter.svelte +6 -0
  84. package/dist/icons/defaults/Filter.svelte.d.ts +7 -0
  85. package/dist/icons/defaults/FilterActive.svelte +6 -0
  86. package/dist/icons/defaults/FilterActive.svelte.d.ts +7 -0
  87. package/dist/icons/defaults/Folder.svelte +6 -0
  88. package/dist/icons/defaults/Folder.svelte.d.ts +7 -0
  89. package/dist/icons/defaults/Home.svelte +6 -0
  90. package/dist/icons/defaults/Home.svelte.d.ts +7 -0
  91. package/dist/icons/defaults/PasswordHide.svelte +9 -0
  92. package/dist/icons/defaults/PasswordHide.svelte.d.ts +7 -0
  93. package/dist/icons/defaults/PasswordShow.svelte +7 -0
  94. package/dist/icons/defaults/PasswordShow.svelte.d.ts +7 -0
  95. package/dist/icons/defaults/SortAsc.svelte +6 -0
  96. package/dist/icons/defaults/SortAsc.svelte.d.ts +7 -0
  97. package/dist/icons/defaults/SortDesc.svelte +6 -0
  98. package/dist/icons/defaults/SortDesc.svelte.d.ts +7 -0
  99. package/dist/icons/defaults/SortNone.svelte +6 -0
  100. package/dist/icons/defaults/SortNone.svelte.d.ts +7 -0
  101. package/dist/icons/defaults/View.svelte +7 -0
  102. package/dist/icons/defaults/View.svelte.d.ts +7 -0
  103. package/dist/icons/sets/bootstrap.d.ts +2 -0
  104. package/dist/icons/sets/bootstrap.js +29 -0
  105. package/dist/icons/sets/default.d.ts +2 -0
  106. package/dist/icons/sets/default.js +28 -0
  107. package/dist/icons/types.d.ts +26 -0
  108. package/dist/icons/types.js +1 -0
  109. package/dist/index.d.ts +42 -0
  110. package/dist/index.js +41 -0
  111. package/dist/types/attribute.d.ts +38 -0
  112. package/dist/types/attribute.js +11 -0
  113. package/dist/types/breadcrumb.d.ts +7 -0
  114. package/dist/types/breadcrumb.js +1 -0
  115. package/dist/types/crud.d.ts +48 -0
  116. package/dist/types/crud.js +1 -0
  117. package/dist/types/table.d.ts +16 -0
  118. package/dist/types/table.js +1 -0
  119. package/package.json +82 -0
@@ -0,0 +1,147 @@
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 { fieldLabel } from '../utils/misc.js';
10
+ import type { ActionConfiguration, FieldDefinition } from '../../../types/crud.js';
11
+ import { getStrings } from '../../../i18n/context.js';
12
+
13
+ const strings = getStrings();
14
+
15
+ let {
16
+ labelOne = '',
17
+ labelMany = '',
18
+ icon,
19
+ fields = [] as FieldDefinition<T>[],
20
+ creation = {} as ActionConfiguration<T>,
21
+ serverError = '',
22
+ onCancel,
23
+ onSuccess,
24
+ }: {
25
+ labelOne?: string;
26
+ labelMany?: string;
27
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
28
+ icon?: any;
29
+ fields?: FieldDefinition<T>[];
30
+ creation?: ActionConfiguration<T>;
31
+ serverError?: string;
32
+ onCancel?: () => void;
33
+ onSuccess?: () => void;
34
+ } = $props();
35
+
36
+ const icons = $derived(getIconSet() ?? defaultIconSet);
37
+ const entityIcon = $derived(icon ?? icons.folder);
38
+
39
+ let fieldErrors = $state<Record<string, string>>({});
40
+ let internalError = $state('');
41
+ let continueCreating = $state(false);
42
+
43
+ function emptyRecord(): Record<string, unknown> {
44
+ return Object.fromEntries(
45
+ fields.map((f) => [
46
+ f.attribute,
47
+ f.type === 'boolean' ? !!f.default
48
+ : f.type === 'file' ? (f.default ?? null)
49
+ : String(f.default ?? ''),
50
+ ])
51
+ );
52
+ }
53
+
54
+ let record = $state<Record<string, unknown>>(untrack(() => emptyRecord()));
55
+
56
+ function validateAll(formData: FormData): Record<string, string> {
57
+ const errs: Record<string, string> = {};
58
+ for (const field of fields) {
59
+ if (field.required) {
60
+ const val = String(formData.get(field.attribute) ?? '').trim();
61
+ if (!val) errs[field.attribute] = strings.required(fieldLabel(field));
62
+ }
63
+ }
64
+ return errs;
65
+ }
66
+
67
+ const hasFileField = $derived(fields.some((f) => f.type === 'file'));
68
+
69
+ const errorEntries = $derived([
70
+ ...((serverError || internalError) ? [['_global', internalError || serverError] as [string, string]] : []),
71
+ ...Object.entries(fieldErrors),
72
+ ]);
73
+ </script>
74
+
75
+ <div class="flex flex-col gap-6">
76
+
77
+ <Header
78
+ admin
79
+ title={labelMany}
80
+ breadcrumbs={[
81
+ { label: labelMany, icon: entityIcon, link: { href: '#', onclick: (e) => { e.preventDefault(); onCancel?.(); } }, prominent: true },
82
+ { label: creation.label ?? labelOne, icon: icons.create },
83
+ ]}
84
+ />
85
+
86
+ {#if errorEntries.length > 0}
87
+ <div role="alert" class="alert alert-error">
88
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
89
+ <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" />
90
+ </svg>
91
+ <ul class="list-disc list-inside text-sm">
92
+ {#each errorEntries as [key, msg] (key)}
93
+ <li>{msg}</li>
94
+ {/each}
95
+ </ul>
96
+ </div>
97
+ {/if}
98
+
99
+ <form
100
+ method="POST"
101
+ action={creation.endpoint ?? '?/create'}
102
+ enctype={hasFileField ? 'multipart/form-data' : undefined}
103
+ class="mx-auto flex w-full max-w-lg flex-col gap-4 px-4"
104
+ use:enhance={({ formData, cancel }) => {
105
+ fieldErrors = {};
106
+ internalError = '';
107
+ const errs = validateAll(formData);
108
+ if (Object.keys(errs).length > 0) {
109
+ fieldErrors = errs;
110
+ cancel();
111
+ return;
112
+ }
113
+ return async ({ result, update }) => {
114
+ if (result.type === 'success' || result.type === 'redirect') {
115
+ await update({ reset: false });
116
+ if (continueCreating) {
117
+ record = emptyRecord();
118
+ fieldErrors = {};
119
+ internalError = '';
120
+ } else {
121
+ onSuccess?.();
122
+ }
123
+ } else if (result.type === 'error') {
124
+ internalError = result.error?.message ?? strings.serverError;
125
+ } else {
126
+ await update({ reset: false });
127
+ }
128
+ };
129
+ }}
130
+ >
131
+ {#each fields as field (field.attribute)}
132
+ <Field {field} bind:record error={fieldErrors[field.attribute] ?? ''} />
133
+ {/each}
134
+
135
+ <div class="flex flex-col-reverse gap-2 pt-2 sm:flex-row sm:justify-end">
136
+ <Button variant="ghost" onclick={() => onCancel?.()}>
137
+ {strings.cancel}
138
+ </Button>
139
+ <Button type="submit" variant="secondary" onclick={() => (continueCreating = true)}>
140
+ {strings.saveAndContinue}
141
+ </Button>
142
+ <Button type="submit" variant="primary" onclick={() => (continueCreating = false)}>
143
+ {strings.save}
144
+ </Button>
145
+ </div>
146
+ </form>
147
+ </div>
@@ -0,0 +1,34 @@
1
+ import type { ActionConfiguration, FieldDefinition } from '../../../types/crud.js';
2
+ declare function $$render<T extends object = Record<string, unknown>>(): {
3
+ props: {
4
+ labelOne?: string;
5
+ labelMany?: string;
6
+ icon?: any;
7
+ fields?: FieldDefinition<T>[];
8
+ creation?: ActionConfiguration<T>;
9
+ serverError?: string;
10
+ onCancel?: () => void;
11
+ onSuccess?: () => void;
12
+ };
13
+ exports: {};
14
+ bindings: "";
15
+ slots: {};
16
+ events: {};
17
+ };
18
+ declare class __sveltets_Render<T extends object = Record<string, unknown>> {
19
+ props(): ReturnType<typeof $$render<T>>['props'];
20
+ events(): ReturnType<typeof $$render<T>>['events'];
21
+ slots(): ReturnType<typeof $$render<T>>['slots'];
22
+ bindings(): "";
23
+ exports(): {};
24
+ }
25
+ interface $$IsomorphicComponent {
26
+ new <T extends object = Record<string, unknown>>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
27
+ $$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
28
+ } & ReturnType<__sveltets_Render<T>['exports']>;
29
+ <T extends object = Record<string, unknown>>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
30
+ z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
31
+ }
32
+ declare const Create: $$IsomorphicComponent;
33
+ type Create<T extends object = Record<string, unknown>> = InstanceType<typeof Create<T>>;
34
+ export default Create;
@@ -0,0 +1,170 @@
1
+ <script lang="ts" generics="T extends object = Record<string, unknown>">
2
+ import { SvelteSet } from 'svelte/reactivity';
3
+ import { invalidateAll } from '$app/navigation';
4
+ import Button from '../../form/Button.svelte';
5
+ import Header from '../../common/Header.svelte';
6
+ import PaginatedTable from '../../table/PaginatedTable.svelte';
7
+ import { getIconSet } from '../../../icons/context.js';
8
+ import { defaultIconSet } from '../../../icons/sets/default.js';
9
+ import type {
10
+ ActionConfiguration,
11
+ ColumnDefinition,
12
+ CustomAction,
13
+ RowAction
14
+ } from '../../../types/crud.js';
15
+ import { getStrings } from '../../../i18n/context.js';
16
+
17
+ const strings = getStrings();
18
+
19
+ let {
20
+ data = [] as T[],
21
+ labelOne = '',
22
+ labelMany = '',
23
+ icon,
24
+ pageSize = 10,
25
+ creation = {} as ActionConfiguration<T>,
26
+ update = {} as ActionConfiguration<T>,
27
+ read = {} as ActionConfiguration<T>,
28
+ deletion = {} as ActionConfiguration<T>,
29
+ actions = [] as CustomAction<T>[],
30
+ columns = [] as ColumnDefinition<T>[],
31
+ onCreate,
32
+ onEdit,
33
+ onView,
34
+ onAction,
35
+ }: {
36
+ data?: T[];
37
+ labelOne?: string;
38
+ labelMany?: string;
39
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
40
+ icon?: any;
41
+ pageSize?: number;
42
+ creation?: ActionConfiguration<T>;
43
+ update?: ActionConfiguration<T>;
44
+ read?: ActionConfiguration<T>;
45
+ deletion?: ActionConfiguration<T>;
46
+ actions?: CustomAction<T>[];
47
+ columns?: ColumnDefinition<T>[];
48
+ onCreate?: () => void;
49
+ onEdit?: (item: T) => void;
50
+ onView?: (item: T) => void;
51
+ onAction?: (action: CustomAction<T>, item: T) => void;
52
+ } = $props();
53
+
54
+ const icons = $derived(getIconSet() ?? defaultIconSet);
55
+ const entityIcon = $derived(icon ?? icons.folder);
56
+
57
+ const allowRead = $derived(read.enabled ?? true);
58
+ const allowUpdate = $derived(update.enabled ?? true);
59
+ const allowDelete = $derived(deletion.enabled ?? true);
60
+ const deleteLabel = $derived(deletion.label ?? strings.delete);
61
+ const updateLabel = $derived(update.label ?? strings.edit);
62
+ const readLabel = $derived(read.label ?? strings.view);
63
+ const showRowActions = $derived(allowRead || allowUpdate || allowDelete || actions.length > 0);
64
+
65
+ let selected = new SvelteSet<number>();
66
+
67
+ async function runEndpointAction(endpoint: string, items: T[]) {
68
+ await Promise.all(items.map((item) => {
69
+ const fd = new FormData();
70
+ fd.set('id', String((item as Record<string, unknown>)._id ?? ''));
71
+ return fetch(endpoint, { method: 'POST', body: fd });
72
+ }));
73
+ await invalidateAll();
74
+ }
75
+
76
+ async function runDeletion(items: T[]) {
77
+ if (deletion.endpoint) {
78
+ await runEndpointAction(deletion.endpoint, items);
79
+ } else {
80
+ await deletion.callback?.(items);
81
+ }
82
+ }
83
+
84
+ function handleCreate() { onCreate?.(); }
85
+
86
+ async function handleDelete() {
87
+ const items = [...selected].map(i => data[i]);
88
+ selected.clear();
89
+ await runDeletion(items);
90
+ }
91
+
92
+ const rowActions = $derived<RowAction<T>[]>([
93
+ ...actions.map((action) => ({
94
+ label: action.label,
95
+ icon: action.icon,
96
+ condition: action.condition,
97
+ run: (item: T) => onAction?.(action, item),
98
+ })),
99
+ ...(allowRead ? [{ label: readLabel, icon: icons.view, run: (item: T) => onView?.(item) }] : []),
100
+ ...(allowUpdate ? [{ label: updateLabel, icon: icons.edit, run: (item: T) => onEdit?.(item) }] : []),
101
+ ...(allowDelete ? [{ label: deleteLabel, icon: icons.delete, class: 'text-error', run: (item: T) => runDeletion([item]) }] : []),
102
+ ]);
103
+ </script>
104
+
105
+ {#snippet actionsCell(item: T)}
106
+ <div class="flex justify-end gap-1">
107
+ {#each rowActions as action (action.label)}
108
+ {#if action.condition?.(item) ?? true}
109
+ {@const Icon = action.icon}
110
+ <Button
111
+ variant="ghost"
112
+ class={['btn-xs', action.class]}
113
+ title={action.label}
114
+ aria-label={action.label}
115
+ onclick={(e) => { e.stopPropagation(); action.run(item); }}
116
+ >
117
+ <Icon class="size-4" />
118
+ </Button>
119
+ {/if}
120
+ {/each}
121
+ </div>
122
+ {/snippet}
123
+
124
+ <div class="flex flex-col gap-6">
125
+
126
+ <Header
127
+ admin
128
+ title={labelMany}
129
+ breadcrumbs={[
130
+ { label: labelMany, icon: entityIcon, link: { href: '#' }, prominent: true },
131
+ ]}
132
+ >
133
+ {#snippet buttons()}
134
+ {#if allowDelete}
135
+ {@const DeleteIcon = icons.delete}
136
+ <Button
137
+ variant="error"
138
+ class="btn-outline"
139
+ disabled={selected.size === 0}
140
+ onclick={handleDelete}
141
+ >
142
+ <DeleteIcon class="size-4" />
143
+ {deleteLabel} ({selected.size})
144
+ </Button>
145
+ {/if}
146
+
147
+ {@const CreateIcon = icons.create}
148
+ <Button
149
+ variant="primary"
150
+ onclick={handleCreate}
151
+ >
152
+ <CreateIcon class="size-5" />
153
+ {#if creation.label}
154
+ <span>{creation.label}</span>
155
+ {:else}
156
+ <span>{strings.create}<span class="hidden sm:inline">&nbsp;{labelOne}</span></span>
157
+ {/if}
158
+ </Button>
159
+ {/snippet}
160
+ </Header>
161
+
162
+ <PaginatedTable
163
+ {data}
164
+ {columns}
165
+ {pageSize}
166
+ selectable={allowDelete}
167
+ {selected}
168
+ rowActions={showRowActions ? actionsCell : undefined}
169
+ />
170
+ </div>
@@ -0,0 +1,41 @@
1
+ import type { ActionConfiguration, ColumnDefinition, CustomAction } from '../../../types/crud.js';
2
+ declare function $$render<T extends object = Record<string, unknown>>(): {
3
+ props: {
4
+ data?: T[];
5
+ labelOne?: string;
6
+ labelMany?: string;
7
+ icon?: any;
8
+ pageSize?: number;
9
+ creation?: ActionConfiguration<T>;
10
+ update?: ActionConfiguration<T>;
11
+ read?: ActionConfiguration<T>;
12
+ deletion?: ActionConfiguration<T>;
13
+ actions?: CustomAction<T>[];
14
+ columns?: ColumnDefinition<T>[];
15
+ onCreate?: () => void;
16
+ onEdit?: (item: T) => void;
17
+ onView?: (item: T) => void;
18
+ onAction?: (action: CustomAction<T>, item: T) => void;
19
+ };
20
+ exports: {};
21
+ bindings: "";
22
+ slots: {};
23
+ events: {};
24
+ };
25
+ declare class __sveltets_Render<T extends object = Record<string, unknown>> {
26
+ props(): ReturnType<typeof $$render<T>>['props'];
27
+ events(): ReturnType<typeof $$render<T>>['events'];
28
+ slots(): ReturnType<typeof $$render<T>>['slots'];
29
+ bindings(): "";
30
+ exports(): {};
31
+ }
32
+ interface $$IsomorphicComponent {
33
+ new <T extends object = Record<string, unknown>>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
34
+ $$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
35
+ } & ReturnType<__sveltets_Render<T>['exports']>;
36
+ <T extends object = Record<string, unknown>>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
37
+ z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
38
+ }
39
+ declare const List: $$IsomorphicComponent;
40
+ type List<T extends object = Record<string, unknown>> = InstanceType<typeof List<T>>;
41
+ export default List;
@@ -0,0 +1,85 @@
1
+ <script lang="ts" generics="T extends object = Record<string, unknown>">
2
+ import { untrack } from 'svelte';
3
+ import { deserialize } 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 type { ActionConfiguration, FieldDefinition } from '../../../types/crud.js';
10
+ import { getStrings } from '../../../i18n/context.js';
11
+
12
+ const strings = getStrings();
13
+
14
+ let {
15
+ labelOne = '',
16
+ labelMany = '',
17
+ icon,
18
+ fields = [] as FieldDefinition<T>[],
19
+ instance = {} as T,
20
+ read = {} as ActionConfiguration<T>,
21
+ onCancel,
22
+ }: {
23
+ labelOne?: string;
24
+ labelMany?: string;
25
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
26
+ icon?: any;
27
+ fields?: FieldDefinition<T>[];
28
+ instance?: T;
29
+ read?: ActionConfiguration<T>;
30
+ onCancel?: () => void;
31
+ } = $props();
32
+
33
+ const icons = $derived(getIconSet() ?? defaultIconSet);
34
+ const entityIcon = $derived(icon ?? icons.folder);
35
+
36
+ let record = $state<Record<string, unknown>>(untrack(() => instance as Record<string, unknown>));
37
+
38
+ $effect(() => {
39
+ const endpoint = read.endpoint;
40
+ const id = (instance as Record<string, unknown>)._id;
41
+ if (!endpoint || id == null) return;
42
+
43
+ let cancelled = false;
44
+ (async () => {
45
+ const fd = new FormData();
46
+ fd.set('id', String(id));
47
+ const res = await fetch(endpoint, { method: 'POST', body: fd });
48
+ const result = deserialize(await res.text());
49
+ if (!cancelled && result.type === 'success') {
50
+ const data = result.data as Record<string, unknown> | undefined;
51
+ const fetched = data
52
+ ? Object.values(data).find(
53
+ (v) => v !== null && typeof v === 'object' && !Array.isArray(v) && '_id' in (v as object)
54
+ )
55
+ : undefined;
56
+ if (fetched && typeof fetched === 'object') record = fetched as Record<string, unknown>;
57
+ }
58
+ })();
59
+ return () => { cancelled = true; };
60
+ });
61
+ </script>
62
+
63
+ <div class="flex flex-col gap-6">
64
+
65
+ <Header
66
+ admin
67
+ title={labelMany}
68
+ breadcrumbs={[
69
+ { label: labelMany, icon: entityIcon, link: { href: '#', onclick: (e) => { e.preventDefault(); onCancel?.(); } }, prominent: true },
70
+ { label: read.label ?? labelOne, icon: icons.view },
71
+ ]}
72
+ />
73
+
74
+ <div class="mx-auto flex w-full max-w-lg flex-col gap-4 px-4">
75
+ {#each fields as field (field.attribute)}
76
+ <Field {field} {record} readonly />
77
+ {/each}
78
+
79
+ <div class="flex flex-col-reverse gap-2 pt-2 sm:flex-row sm:justify-end">
80
+ <Button variant="ghost" onclick={() => onCancel?.()}>
81
+ {strings.back}
82
+ </Button>
83
+ </div>
84
+ </div>
85
+ </div>
@@ -0,0 +1,33 @@
1
+ import type { ActionConfiguration, FieldDefinition } from '../../../types/crud.js';
2
+ declare function $$render<T extends object = Record<string, unknown>>(): {
3
+ props: {
4
+ labelOne?: string;
5
+ labelMany?: string;
6
+ icon?: any;
7
+ fields?: FieldDefinition<T>[];
8
+ instance?: T;
9
+ read?: ActionConfiguration<T>;
10
+ onCancel?: () => void;
11
+ };
12
+ exports: {};
13
+ bindings: "";
14
+ slots: {};
15
+ events: {};
16
+ };
17
+ declare class __sveltets_Render<T extends object = Record<string, unknown>> {
18
+ props(): ReturnType<typeof $$render<T>>['props'];
19
+ events(): ReturnType<typeof $$render<T>>['events'];
20
+ slots(): ReturnType<typeof $$render<T>>['slots'];
21
+ bindings(): "";
22
+ exports(): {};
23
+ }
24
+ interface $$IsomorphicComponent {
25
+ new <T extends object = Record<string, unknown>>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
26
+ $$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
27
+ } & ReturnType<__sveltets_Render<T>['exports']>;
28
+ <T extends object = Record<string, unknown>>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
29
+ z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
30
+ }
31
+ declare const Read: $$IsomorphicComponent;
32
+ type Read<T extends object = Record<string, unknown>> = InstanceType<typeof Read<T>>;
33
+ export default Read;
@@ -0,0 +1,148 @@
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 { fieldLabel } from '../utils/misc.js';
10
+ import type { ActionConfiguration, FieldDefinition } from '../../../types/crud.js';
11
+ import { getStrings } from '../../../i18n/context.js';
12
+
13
+ const strings = getStrings();
14
+
15
+ let {
16
+ labelOne = '',
17
+ labelMany = '',
18
+ icon,
19
+ fields = [] as FieldDefinition<T>[],
20
+ instance = {} as T,
21
+ update = {} as ActionConfiguration<T>,
22
+ serverError = '',
23
+ onCancel,
24
+ onSuccess,
25
+ }: {
26
+ labelOne?: string;
27
+ labelMany?: string;
28
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
29
+ icon?: any;
30
+ fields?: FieldDefinition<T>[];
31
+ instance?: T;
32
+ update?: ActionConfiguration<T>;
33
+ serverError?: string;
34
+ onCancel?: () => void;
35
+ onSuccess?: () => void;
36
+ } = $props();
37
+
38
+ const icons = $derived(getIconSet() ?? defaultIconSet);
39
+ const entityIcon = $derived(icon ?? icons.folder);
40
+
41
+ let fieldErrors = $state<Record<string, string>>({});
42
+ let internalError = $state('');
43
+
44
+ function seedFromInstance(inst: Record<string, unknown>): Record<string, unknown> {
45
+ const seeded: Record<string, unknown> = { ...inst };
46
+ for (const f of fields) {
47
+ if (f.type !== 'boolean' && f.type !== 'file') {
48
+ seeded[f.attribute] = String(inst[f.attribute] ?? '');
49
+ }
50
+ }
51
+ return seeded;
52
+ }
53
+
54
+ let record = $state<Record<string, unknown>>(
55
+ untrack(() => seedFromInstance(instance as Record<string, unknown>))
56
+ );
57
+
58
+ $effect(() => {
59
+ const id = (instance as Record<string, unknown>)._id;
60
+ if (!id) return;
61
+ untrack(() => { record = seedFromInstance(instance as Record<string, unknown>); });
62
+ });
63
+
64
+ function validateAll(formData: FormData): Record<string, string> {
65
+ const errs: Record<string, string> = {};
66
+ for (const field of fields) {
67
+ if (field.required) {
68
+ const val = String(formData.get(field.attribute) ?? '').trim();
69
+ if (!val) errs[field.attribute] = strings.required(fieldLabel(field));
70
+ }
71
+ }
72
+ return errs;
73
+ }
74
+
75
+ const hasFileField = $derived(fields.some((f) => f.type === 'file'));
76
+
77
+ const errorEntries = $derived([
78
+ ...((serverError || internalError) ? [['_global', internalError || serverError] as [string, string]] : []),
79
+ ...Object.entries(fieldErrors),
80
+ ]);
81
+ </script>
82
+
83
+ <div class="flex flex-col gap-6">
84
+
85
+ <Header
86
+ admin
87
+ title={labelMany}
88
+ breadcrumbs={[
89
+ { label: labelMany, icon: entityIcon, link: { href: '#', onclick: (e) => { e.preventDefault(); onCancel?.(); } }, prominent: true },
90
+ { label: update.label ?? labelOne, icon: icons.edit },
91
+ ]}
92
+ />
93
+
94
+ {#if errorEntries.length > 0}
95
+ <div role="alert" class="alert alert-error">
96
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
97
+ <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" />
98
+ </svg>
99
+ <ul class="list-disc list-inside text-sm">
100
+ {#each errorEntries as [key, msg] (key)}
101
+ <li>{msg}</li>
102
+ {/each}
103
+ </ul>
104
+ </div>
105
+ {/if}
106
+
107
+ <form
108
+ method="POST"
109
+ action={update.endpoint ?? '?/update'}
110
+ enctype={hasFileField ? 'multipart/form-data' : undefined}
111
+ class="mx-auto flex w-full max-w-lg flex-col gap-4 px-4"
112
+ use:enhance={({ formData, cancel }) => {
113
+ fieldErrors = {};
114
+ internalError = '';
115
+ const errs = validateAll(formData);
116
+ if (Object.keys(errs).length > 0) {
117
+ fieldErrors = errs;
118
+ cancel();
119
+ return;
120
+ }
121
+ return async ({ result, update: updateForm }) => {
122
+ if (result.type === 'success' || result.type === 'redirect') {
123
+ await updateForm({ reset: false });
124
+ onSuccess?.();
125
+ } else if (result.type === 'error') {
126
+ internalError = result.error?.message ?? strings.serverError;
127
+ } else {
128
+ await updateForm({ reset: false });
129
+ }
130
+ };
131
+ }}
132
+ >
133
+ <input type="hidden" name="id" value={String(record._id ?? '')} />
134
+
135
+ {#each fields as field (field.attribute)}
136
+ <Field {field} bind:record error={fieldErrors[field.attribute] ?? ''} />
137
+ {/each}
138
+
139
+ <div class="flex flex-col-reverse gap-2 pt-2 sm:flex-row sm:justify-end">
140
+ <Button variant="ghost" onclick={() => onCancel?.()}>
141
+ {strings.cancel}
142
+ </Button>
143
+ <Button type="submit" variant="primary">
144
+ {strings.save}
145
+ </Button>
146
+ </div>
147
+ </form>
148
+ </div>