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,30 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import Breadcrumbs from '../navigation/Breadcrumbs.svelte';
4
+ import type { BreadcrumbItem } from '../../types/breadcrumb.js';
5
+
6
+ let {
7
+ title,
8
+ breadcrumbs = [],
9
+ buttons,
10
+ admin = false,
11
+ }: {
12
+ title: string;
13
+ breadcrumbs?: BreadcrumbItem[];
14
+ buttons?: Snippet;
15
+ admin?: boolean;
16
+ } = $props();
17
+ </script>
18
+
19
+ <div class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
20
+ <div class="flex flex-col gap-1">
21
+ <h1>{title}</h1>
22
+ <Breadcrumbs items={breadcrumbs} {admin} />
23
+ </div>
24
+
25
+ {#if buttons}
26
+ <div class="flex items-center gap-2 pt-1">
27
+ {@render buttons()}
28
+ </div>
29
+ {/if}
30
+ </div>
@@ -0,0 +1,11 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { BreadcrumbItem } from '../../types/breadcrumb.js';
3
+ type $$ComponentProps = {
4
+ title: string;
5
+ breadcrumbs?: BreadcrumbItem[];
6
+ buttons?: Snippet;
7
+ admin?: boolean;
8
+ };
9
+ declare const Header: import("svelte").Component<$$ComponentProps, {}, "">;
10
+ type Header = ReturnType<typeof Header>;
11
+ export default Header;
@@ -0,0 +1,159 @@
1
+ <script lang="ts" generics="T extends object = Record<string, unknown>">
2
+ import Avatar from '../Avatar.svelte';
3
+ import Label from '../form/Label.svelte';
4
+ import Select from '../form/Select.svelte';
5
+ import { fieldLabel, initials } from './utils/misc.js';
6
+ import type { FieldDefinition } from '../../types/crud.js';
7
+ import { getStrings } from '../../i18n/context.js';
8
+
9
+ const strings = getStrings();
10
+
11
+ let {
12
+ field,
13
+ record = $bindable({} as Record<string, unknown>),
14
+ error = '',
15
+ readonly = false,
16
+ }: {
17
+ field: FieldDefinition<T>;
18
+ record?: Record<string, unknown>;
19
+ error?: string;
20
+ readonly?: boolean;
21
+ } = $props();
22
+
23
+ const name = $derived(readonly ? undefined : field.attribute);
24
+ const labelText = $derived(fieldLabel(field));
25
+ let filePreview = $state<string | null>(null);
26
+
27
+ function onFileChange(e: Event & { currentTarget: HTMLInputElement }) {
28
+ const file = e.currentTarget.files?.[0];
29
+ if (filePreview) URL.revokeObjectURL(filePreview);
30
+ filePreview = file && file.type.startsWith('image/') ? URL.createObjectURL(file) : null;
31
+ }
32
+
33
+ $effect(() => () => {
34
+ if (filePreview) URL.revokeObjectURL(filePreview);
35
+ });
36
+
37
+ const saved = $derived(record[field.attribute]);
38
+ const preview = $derived(filePreview ?? (typeof saved === 'string' && saved ? saved : null));
39
+ const avatarInitials = $derived(initials(record.firstName as string, record.lastName as string));
40
+ const displayValue = $derived(saved == null ? '' : String(saved));
41
+ </script>
42
+
43
+ <div class="flex flex-col gap-1">
44
+ {#if field.type === 'file'}
45
+ <div class="flex justify-center">
46
+ <Avatar src={preview} text={avatarInitials} alt={labelText} class="w-32 rounded-full" textClass="text-3xl" />
47
+ </div>
48
+ {/if}
49
+
50
+ <Label
51
+ text={labelText}
52
+ for={field.attribute}
53
+ capitalize={true}
54
+ required={field.required && !readonly}
55
+ />
56
+
57
+ {#if field.type === 'boolean'}
58
+ <input
59
+ type="checkbox"
60
+ id={field.attribute}
61
+ {name}
62
+ class="toggle"
63
+ checked={!!saved}
64
+ disabled={readonly}
65
+ />
66
+ {:else if field.type === 'file'}
67
+ {#if !readonly}
68
+ <fieldset class="fieldset w-full p-0">
69
+ <input
70
+ type="file"
71
+ id={field.attribute}
72
+ {name}
73
+ class="file-input w-full"
74
+ class:file-input-error={!!error}
75
+ onchange={onFileChange}
76
+ />
77
+ {#if field.placeholder}
78
+ <Label text={field.placeholder} for={field.attribute} class="label" />
79
+ {/if}
80
+ </fieldset>
81
+ {/if}
82
+ <!-- read-only file value is shown as the avatar above -->
83
+ {:else if field.type === 'select'}
84
+ {#if readonly}
85
+ <input
86
+ type="text"
87
+ id={field.attribute}
88
+ class="input input-bordered w-full"
89
+ value={field.options?.find((o) => o.value === String(saved))?.label ?? displayValue}
90
+ disabled
91
+ />
92
+ {:else}
93
+ <Select
94
+ name={field.attribute}
95
+ bind:value={record[field.attribute] as string}
96
+ options={field.options ?? []}
97
+ placeholder={field.placeholder}
98
+ {error}
99
+ />
100
+ {/if}
101
+ {:else if field.type === 'datetime'}
102
+ {#if readonly}
103
+ <input
104
+ type="text"
105
+ id={field.attribute}
106
+ class="input input-bordered w-full"
107
+ value={displayValue}
108
+ disabled
109
+ />
110
+ {:else}
111
+ <input type="hidden" {name} value={String(record[field.attribute] ?? '')} />
112
+ <calendar-date
113
+ class="cally rounded-box border border-base-300 bg-base-100 shadow-sm"
114
+ value={String(record[field.attribute] ?? '')}
115
+ onchange={(e: Event) => { record[field.attribute] = (e as Event & { detail: { value: string } }).detail.value; }}
116
+ >
117
+ <svg aria-label={strings.previous} class="fill-current size-4" {...{"slot": "previous"}} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M15.75 19.5 8.25 12l7.5-7.5"/></svg>
118
+ <svg aria-label={strings.next} class="fill-current size-4" {...{"slot": "next"}} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="m8.25 4.5 7.5 7.5-7.5 7.5"/></svg>
119
+ <calendar-month></calendar-month>
120
+ </calendar-date>
121
+ {/if}
122
+ {:else if field.type === 'textarea'}
123
+ {#if readonly}
124
+ <textarea id={field.attribute} class="textarea textarea-bordered w-full" value={displayValue} disabled></textarea>
125
+ {:else}
126
+ <textarea
127
+ id={field.attribute}
128
+ {name}
129
+ placeholder={field.placeholder ?? ''}
130
+ bind:value={record[field.attribute]}
131
+ class="textarea textarea-bordered w-full"
132
+ class:textarea-error={!!error}
133
+ ></textarea>
134
+ {/if}
135
+ {:else if readonly}
136
+ <input
137
+ type={field.type ?? 'text'}
138
+ id={field.attribute}
139
+ class="input input-bordered w-full"
140
+ value={displayValue}
141
+ disabled
142
+ />
143
+ {:else}
144
+ <input
145
+ type={field.type ?? 'text'}
146
+ id={field.attribute}
147
+ {name}
148
+ placeholder={field.placeholder ?? ''}
149
+ bind:value={record[field.attribute]}
150
+ autocomplete={field.autocomplete}
151
+ class="input input-bordered w-full"
152
+ class:input-error={!!error}
153
+ />
154
+ {/if}
155
+
156
+ {#if error}
157
+ <span class="text-error text-xs">{error}</span>
158
+ {/if}
159
+ </div>
@@ -0,0 +1,30 @@
1
+ import type { FieldDefinition } from '../../types/crud.js';
2
+ declare function $$render<T extends object = Record<string, unknown>>(): {
3
+ props: {
4
+ field: FieldDefinition<T>;
5
+ record?: Record<string, unknown>;
6
+ error?: string;
7
+ readonly?: boolean;
8
+ };
9
+ exports: {};
10
+ bindings: "record";
11
+ slots: {};
12
+ events: {};
13
+ };
14
+ declare class __sveltets_Render<T extends object = Record<string, unknown>> {
15
+ props(): ReturnType<typeof $$render<T>>['props'];
16
+ events(): ReturnType<typeof $$render<T>>['events'];
17
+ slots(): ReturnType<typeof $$render<T>>['slots'];
18
+ bindings(): "record";
19
+ exports(): {};
20
+ }
21
+ interface $$IsomorphicComponent {
22
+ 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']>> & {
23
+ $$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
24
+ } & ReturnType<__sveltets_Render<T>['exports']>;
25
+ <T extends object = Record<string, unknown>>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
26
+ z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
27
+ }
28
+ declare const Field: $$IsomorphicComponent;
29
+ type Field<T extends object = Record<string, unknown>> = InstanceType<typeof Field<T>>;
30
+ export default Field;
@@ -0,0 +1,236 @@
1
+ <script lang="ts" generics="T extends object = Record<string, unknown>">
2
+ import { page } from '$app/state';
3
+ import { goto } from '$app/navigation';
4
+ import List from './views/List.svelte';
5
+ import Read from './views/Read.svelte';
6
+ import Create from './views/Create.svelte';
7
+ import Update from './views/Update.svelte';
8
+ import { AUTO_EXCLUDED } from './utils/constants.js';
9
+ import { resolveOptions, resolveFormatter, inferType } from './utils/resolution.js';
10
+ import type { AttributeMetadata } from '../../types/attribute.js';
11
+ import type {
12
+ ActionConfiguration,
13
+ ColumnDefinition,
14
+ CustomAction,
15
+ FieldDefinition,
16
+ } from '../../types/crud.js';
17
+
18
+ let {
19
+ data = undefined as Record<string, unknown> | undefined,
20
+ dataKey = undefined as string | undefined,
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 = undefined as ColumnDefinition<T>[] | undefined,
31
+ fields = undefined as FieldDefinition<T>[] | undefined,
32
+ meta = undefined as Partial<Record<string, AttributeMetadata>> | undefined,
33
+ form = null as { error?: string } | null,
34
+ }: {
35
+ data?: Record<string, unknown>;
36
+ dataKey?: string;
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
+ fields?: FieldDefinition<T>[];
49
+ meta?: Partial<Record<string, AttributeMetadata>>;
50
+ form?: { error?: string } | null;
51
+ } = $props();
52
+
53
+ const entityData = $derived<T[]>(
54
+ data && dataKey ? (data[dataKey] as T[] ?? []) : []
55
+ );
56
+
57
+ const viewParam = $derived(page.url.searchParams.get('view'));
58
+ const idParam = $derived(page.url.searchParams.get('id'));
59
+
60
+ const creating = $derived(viewParam === 'create');
61
+ const reading = $derived(idParam !== null && viewParam === null);
62
+ const editing = $derived(idParam !== null && viewParam === 'edit');
63
+
64
+ const singleInstance = $derived<T | undefined>(
65
+ (Object.values(page.data as Record<string, unknown>).find(
66
+ (v) => v !== null && typeof v === 'object' && !Array.isArray(v) && '_id' in (v as object)
67
+ ) as T | undefined)
68
+ );
69
+
70
+ let activeAction = $state<{ action: CustomAction<T>; item: T } | null>(null);
71
+
72
+ async function navList() { await goto('?'); }
73
+ async function navCreate() { await goto('?view=create'); }
74
+ async function navRead(item: T) {
75
+ await goto(`?id=${encodeURIComponent(String((item as Record<string, unknown>)._id ?? ''))}`);
76
+ }
77
+ async function navEdit(item: T) {
78
+ await goto(`?id=${encodeURIComponent(String((item as Record<string, unknown>)._id ?? ''))}&view=edit`);
79
+ }
80
+
81
+ const resolvedColumns: ColumnDefinition<T>[] = $derived(
82
+ columns ?? (meta
83
+ ? (Object.entries(meta) as [string, AttributeMetadata][])
84
+ .filter(([, m]) => !m.excludedFromList)
85
+ .map(([k, m]) => ({
86
+ attribute: k as keyof T & string,
87
+ title: m.label ?? k,
88
+ type: m.type,
89
+ formatter: resolveFormatter(m, data),
90
+ component: m.component,
91
+ sortable: m.sortable,
92
+ filterable: m.filterable,
93
+ }))
94
+ : entityData.length > 0
95
+ ? (Object.keys(entityData[0]) as (keyof T & string)[])
96
+ .filter((k) => k !== '_id')
97
+ .map((k) => ({ attribute: k, title: k }))
98
+ : [])
99
+ );
100
+
101
+ const resolvedFields: FieldDefinition<T>[] = $derived(
102
+ fields ?? (meta
103
+ ? (Object.entries(meta) as [string, AttributeMetadata][])
104
+ .filter(([k, m]) => !AUTO_EXCLUDED.has(k) && !m.excludedFromCreate)
105
+ .map(([k, m]) => ({
106
+ attribute: k as keyof T & string,
107
+ title: m.label,
108
+ type: m.type ?? inferType(k, undefined),
109
+ required: m.required,
110
+ autocomplete: m.autocomplete,
111
+ placeholder: m.placeholder,
112
+ default: m.default,
113
+ options: resolveOptions(m, data),
114
+ }))
115
+ : entityData.length > 0
116
+ ? (Object.entries(entityData[0]) as [string, unknown][])
117
+ .filter(([k]) => !AUTO_EXCLUDED.has(k))
118
+ .map(([k, v]) => ({ attribute: k as keyof T & string, type: inferType(k, v) }))
119
+ : [])
120
+ );
121
+
122
+ const resolvedReadFields: FieldDefinition<T>[] = $derived(
123
+ fields ?? (meta
124
+ ? (Object.entries(meta) as [string, AttributeMetadata][])
125
+ .filter(([k, m]) => !AUTO_EXCLUDED.has(k) && !m.excludedFromRead)
126
+ .map(([k, m]) => ({
127
+ attribute: k as keyof T & string,
128
+ title: m.label,
129
+ type: m.type ?? inferType(k, undefined),
130
+ required: m.required,
131
+ autocomplete: m.autocomplete,
132
+ placeholder: m.placeholder,
133
+ default: m.default,
134
+ options: resolveOptions(m, data),
135
+ }))
136
+ : entityData.length > 0
137
+ ? (Object.entries(entityData[0]) as [string, unknown][])
138
+ .filter(([k]) => !AUTO_EXCLUDED.has(k))
139
+ .map(([k, v]) => ({ attribute: k as keyof T & string, type: inferType(k, v) }))
140
+ : [])
141
+ );
142
+
143
+ const resolvedUpdateFields: FieldDefinition<T>[] = $derived(
144
+ fields ?? (meta
145
+ ? (Object.entries(meta) as [string, AttributeMetadata][])
146
+ .filter(([k, m]) => !AUTO_EXCLUDED.has(k) && !m.excludedFromUpdate)
147
+ .map(([k, m]) => ({
148
+ attribute: k as keyof T & string,
149
+ title: m.label,
150
+ type: m.type ?? inferType(k, undefined),
151
+ required: m.required,
152
+ autocomplete: m.autocomplete,
153
+ placeholder: m.placeholder,
154
+ default: m.default,
155
+ options: resolveOptions(m, data),
156
+ }))
157
+ : entityData.length > 0
158
+ ? (Object.entries(entityData[0]) as [string, unknown][])
159
+ .filter(([k]) => !AUTO_EXCLUDED.has(k))
160
+ .map(([k, v]) => ({ attribute: k as keyof T & string, type: inferType(k, v) }))
161
+ : [])
162
+ );
163
+
164
+ const serverError = $derived(
165
+ creating || reading || editing || activeAction !== null
166
+ ? (form?.error ?? '')
167
+ : ''
168
+ );
169
+ </script>
170
+
171
+ {#if creating}
172
+ <Create
173
+ {labelOne}
174
+ {labelMany}
175
+ {icon}
176
+ fields={resolvedFields}
177
+ {creation}
178
+ {serverError}
179
+ onCancel={navList}
180
+ onSuccess={navList}
181
+ />
182
+ {:else if reading}
183
+ <Read
184
+ {labelOne}
185
+ {labelMany}
186
+ {icon}
187
+ fields={resolvedReadFields}
188
+ instance={singleInstance ?? {} as T}
189
+ {read}
190
+ onCancel={navList}
191
+ />
192
+ {:else if editing}
193
+ <Update
194
+ {labelOne}
195
+ {labelMany}
196
+ {icon}
197
+ fields={resolvedUpdateFields}
198
+ instance={singleInstance ?? {} as T}
199
+ {update}
200
+ {serverError}
201
+ onCancel={navList}
202
+ onSuccess={navList}
203
+ />
204
+ {:else}
205
+ <List
206
+ data={entityData}
207
+ {labelOne}
208
+ {labelMany}
209
+ {icon}
210
+ {pageSize}
211
+ {creation}
212
+ {update}
213
+ {read}
214
+ {deletion}
215
+ {actions}
216
+ columns={resolvedColumns}
217
+ onCreate={navCreate}
218
+ onEdit={navEdit}
219
+ onView={navRead}
220
+ onAction={(action, item) => {
221
+ if (action.condition?.(item) ?? true) activeAction = { action, item };
222
+ }}
223
+ />
224
+ {/if}
225
+
226
+ {#if activeAction !== null}
227
+ {@const ActionView = activeAction.action.view}
228
+ <ActionView
229
+ instance={activeAction.item}
230
+ label={activeAction.action.label}
231
+ endpoint={activeAction.action.endpoint}
232
+ {serverError}
233
+ onCancel={() => (activeAction = null)}
234
+ onSuccess={() => (activeAction = null)}
235
+ />
236
+ {/if}
@@ -0,0 +1,44 @@
1
+ import type { AttributeMetadata } from '../../types/attribute.js';
2
+ import type { ActionConfiguration, ColumnDefinition, CustomAction, FieldDefinition } from '../../types/crud.js';
3
+ declare function $$render<T extends object = Record<string, unknown>>(): {
4
+ props: {
5
+ data?: Record<string, unknown>;
6
+ dataKey?: string;
7
+ labelOne?: string;
8
+ labelMany?: string;
9
+ icon?: any;
10
+ pageSize?: number;
11
+ creation?: ActionConfiguration<T>;
12
+ update?: ActionConfiguration<T>;
13
+ read?: ActionConfiguration<T>;
14
+ deletion?: ActionConfiguration<T>;
15
+ actions?: CustomAction<T>[];
16
+ columns?: ColumnDefinition<T>[];
17
+ fields?: FieldDefinition<T>[];
18
+ meta?: Partial<Record<string, AttributeMetadata>>;
19
+ form?: {
20
+ error?: string;
21
+ } | null;
22
+ };
23
+ exports: {};
24
+ bindings: "";
25
+ slots: {};
26
+ events: {};
27
+ };
28
+ declare class __sveltets_Render<T extends object = Record<string, unknown>> {
29
+ props(): ReturnType<typeof $$render<T>>['props'];
30
+ events(): ReturnType<typeof $$render<T>>['events'];
31
+ slots(): ReturnType<typeof $$render<T>>['slots'];
32
+ bindings(): "";
33
+ exports(): {};
34
+ }
35
+ interface $$IsomorphicComponent {
36
+ 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']>> & {
37
+ $$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
38
+ } & ReturnType<__sveltets_Render<T>['exports']>;
39
+ <T extends object = Record<string, unknown>>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
40
+ z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
41
+ }
42
+ declare const GenericCRUD: $$IsomorphicComponent;
43
+ type GenericCRUD<T extends object = Record<string, unknown>> = InstanceType<typeof GenericCRUD<T>>;
44
+ export default GenericCRUD;
@@ -0,0 +1,15 @@
1
+ <script lang="ts">
2
+ import AvatarComponent from '../../Avatar.svelte';
3
+ import { initials } from '../utils/misc.js';
4
+ import type { CellProps } from '../../../types/table.js';
5
+
6
+ type AvatarRow = { firstName?: string; lastName?: string; email?: string };
7
+
8
+ let { value, row }: CellProps<AvatarRow, string | null | undefined> = $props();
9
+ </script>
10
+
11
+ <AvatarComponent
12
+ src={value ?? null}
13
+ text={initials(row.firstName, row.lastName)}
14
+ alt={row.email ?? ''}
15
+ />
@@ -0,0 +1,8 @@
1
+ import type { CellProps } from '../../../types/table.js';
2
+ declare const Avatar: import("svelte").Component<CellProps<{
3
+ firstName?: string;
4
+ lastName?: string;
5
+ email?: string;
6
+ }, string | null | undefined>, {}, "">;
7
+ type Avatar = ReturnType<typeof Avatar>;
8
+ export default Avatar;
@@ -0,0 +1,8 @@
1
+ <script lang="ts">
2
+ import IconRenderer from '../../IconRenderer.svelte';
3
+ import type { CellProps } from '../../../types/table.js';
4
+
5
+ let { value }: CellProps<Record<string, unknown>, string> = $props();
6
+ </script>
7
+
8
+ <IconRenderer name={String(value ?? '')} />
@@ -0,0 +1,4 @@
1
+ import type { CellProps } from '../../../types/table.js';
2
+ declare const Icon: import("svelte").Component<CellProps<Record<string, unknown>, string>, {}, "">;
3
+ type Icon = ReturnType<typeof Icon>;
4
+ export default Icon;
@@ -0,0 +1 @@
1
+ export declare const AUTO_EXCLUDED: Set<string>;
@@ -0,0 +1,6 @@
1
+ export const AUTO_EXCLUDED = new Set([
2
+ '_id',
3
+ 'createdAt',
4
+ 'updatedAt',
5
+ '__v',
6
+ ]);
@@ -0,0 +1,6 @@
1
+ export declare const formatBoolean: (trueLabel?: string, falseLabel?: string) => () => (value: boolean) => string;
2
+ export declare const formatDatetime: (format?: string) => () => (value: Date) => string;
3
+ export declare function formatTruncateTextUpTo(maxLength: number): () => (value: string) => string;
4
+ export declare function formatInstance<T extends {
5
+ _id: string;
6
+ }>(attribute: keyof T & string, instances: T[], urlPath: string): (value: unknown) => string;
@@ -0,0 +1,42 @@
1
+ export const formatBoolean = (trueLabel = 'Sí', falseLabel = 'No') => () => (value) => (value ? trueLabel : falseLabel);
2
+ // Supported tokens: dd · mm · YYYY · HH · MM · ss
3
+ const TOKENS = {
4
+ dd: (d) => String(d.getDate()).padStart(2, '0'),
5
+ mm: (d) => String(d.getMonth() + 1).padStart(2, '0'),
6
+ YYYY: (d) => String(d.getFullYear()),
7
+ HH: (d) => String(d.getHours()).padStart(2, '0'),
8
+ MM: (d) => String(d.getMinutes()).padStart(2, '0'),
9
+ ss: (d) => String(d.getSeconds()).padStart(2, '0'),
10
+ };
11
+ export const formatDatetime = (format = 'dd/mm/YYYY HH:MM') => {
12
+ const fmt = (value) => {
13
+ const d = new Date(value);
14
+ if (isNaN(d.getTime()))
15
+ return '';
16
+ return format.replace(/dd|mm|YYYY|HH|MM|ss/g, (token) => TOKENS[token](d));
17
+ };
18
+ return () => fmt;
19
+ };
20
+ export function formatTruncateTextUpTo(maxLength) {
21
+ return () => (value) => {
22
+ const str = String(value ?? '');
23
+ return str.length > maxLength ? str.slice(0, maxLength) + '…' : str;
24
+ };
25
+ }
26
+ function escapeHtml(str) {
27
+ return str
28
+ .replace(/&/g, '&amp;')
29
+ .replace(/</g, '&lt;')
30
+ .replace(/>/g, '&gt;')
31
+ .replace(/"/g, '&quot;');
32
+ }
33
+ export function formatInstance(attribute, instances, urlPath) {
34
+ const map = new Map(instances.map((i) => [i._id, i]));
35
+ return (value) => {
36
+ const id = String(value ?? '');
37
+ const instance = map.get(id);
38
+ const label = instance ? String(instance[attribute] ?? id) : id;
39
+ const href = `${urlPath}?id=${encodeURIComponent(id)}`;
40
+ return `<a href="${href}" class="link link-primary">${escapeHtml(label)}</a>`;
41
+ };
42
+ }
@@ -0,0 +1,5 @@
1
+ export declare function fieldLabel(field: {
2
+ attribute: string;
3
+ title?: string;
4
+ }): string;
5
+ export declare function initials(firstName?: string, lastName?: string): string;
@@ -0,0 +1,8 @@
1
+ export function fieldLabel(field) {
2
+ return (field.title ?? field.attribute).replace(/_/g, ' ');
3
+ }
4
+ export function initials(firstName, lastName) {
5
+ const a = (firstName ?? '').trim().charAt(0);
6
+ const b = (lastName ?? '').trim().charAt(0);
7
+ return (a + b).toUpperCase() || '?';
8
+ }
@@ -0,0 +1,4 @@
1
+ import type { AttributeMetadata, AttributeType, SelectOption } from '../../../types/attribute.js';
2
+ export declare function resolveOptions(m: AttributeMetadata, d: unknown): SelectOption[] | undefined;
3
+ export declare function resolveFormatter(m: AttributeMetadata, d: unknown): import("../../../index.ts").CellFormatter<any, any> | undefined;
4
+ export declare function inferType(key: string, value: unknown): AttributeType;
@@ -0,0 +1,22 @@
1
+ export function resolveOptions(m, d) {
2
+ if (!m.options)
3
+ return undefined;
4
+ return typeof m.options === 'function' ? m.options(d) : m.options;
5
+ }
6
+ export function resolveFormatter(m, d) {
7
+ return m.formatter?.(d);
8
+ }
9
+ export function inferType(key, value) {
10
+ if (typeof value === 'boolean')
11
+ return 'boolean';
12
+ if (typeof value === 'number')
13
+ return 'number';
14
+ const k = key.toLowerCase();
15
+ if (k.includes('email'))
16
+ return 'email';
17
+ if (k.includes('password') || k.includes('hash'))
18
+ return 'password';
19
+ if (k.includes('description') || k.includes('bio') || k.includes('notes') || k.includes('content'))
20
+ return 'textarea';
21
+ return 'text';
22
+ }