runeforge 0.0.20 → 0.0.21

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.
@@ -44,7 +44,7 @@
44
44
  const avatarInitials = $derived(initials(record.firstName as string, record.lastName as string));
45
45
  const displayValue = $derived(saved == null ? '' : String(saved));
46
46
  const selectOptions = $derived(field.dependentOptions ? field.dependentOptions(record) : (field.options ?? []));
47
- const selectDisabled = $derived(field.disabled ? field.disabled(record) : false);
47
+ const fieldDisabled = $derived(field.disabled ? field.disabled(record) : false);
48
48
 
49
49
  $effect(() => {
50
50
  if (!field.dependentOptions) return;
@@ -76,7 +76,8 @@
76
76
  {name}
77
77
  class="toggle toggle-primary"
78
78
  checked={!!saved}
79
- disabled={readonly}
79
+ disabled={readonly || fieldDisabled}
80
+ onchange={(e) => { record[field.attribute] = (e.currentTarget as HTMLInputElement).checked; }}
80
81
  />
81
82
  {:else if field.type === 'file'}
82
83
  {#if !readonly}
@@ -86,6 +87,7 @@
86
87
  {name}
87
88
  class="file-input file-input-bordered w-full"
88
89
  class:file-input-error={!!error}
90
+ disabled={fieldDisabled}
89
91
  onchange={onFileChange}
90
92
  />
91
93
  {/if}
@@ -105,7 +107,7 @@
105
107
  bind:value={record[field.attribute] as string}
106
108
  options={selectOptions}
107
109
  placeholder={field.placeholder}
108
- disabled={selectDisabled}
110
+ disabled={fieldDisabled}
109
111
  {error}
110
112
  />
111
113
  {/if}
@@ -119,11 +121,11 @@
119
121
  disabled
120
122
  />
121
123
  {:else}
122
- <input type="hidden" {name} value={String(record[field.attribute] ?? '')} />
124
+ <input type="hidden" {name} value={String(record[field.attribute] ?? '')} disabled={fieldDisabled} />
123
125
  <calendar-date
124
- class="cally rounded-box border border-base-300 bg-base-100 shadow-sm"
126
+ class="cally rounded-box border border-base-300 bg-base-100 shadow-sm {fieldDisabled ? 'pointer-events-none opacity-50' : ''}"
125
127
  value={String(record[field.attribute] ?? '')}
126
- onchange={(e: Event) => { record[field.attribute] = (e.currentTarget as HTMLElement & { value: string }).value; }}
128
+ onchange={(e: Event) => { if (fieldDisabled) return; record[field.attribute] = (e.currentTarget as HTMLElement & { value: string }).value; }}
127
129
  >
128
130
  <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>
129
131
  <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>
@@ -141,6 +143,7 @@
141
143
  bind:value={record[field.attribute]}
142
144
  class="textarea textarea-bordered bg-base-100 w-full"
143
145
  class:textarea-error={!!error}
146
+ disabled={fieldDisabled}
144
147
  ></textarea>
145
148
  {/if}
146
149
  {:else if readonly}
@@ -159,8 +162,10 @@
159
162
  placeholder={field.placeholder ?? ''}
160
163
  bind:value={record[field.attribute]}
161
164
  autocomplete={field.autocomplete}
165
+ step={field.type === 'number' ? 'any' : undefined}
162
166
  class="input input-bordered w-full"
163
167
  class:input-error={!!error}
168
+ disabled={fieldDisabled}
164
169
  />
165
170
  {/if}
166
171
 
@@ -7,9 +7,9 @@
7
7
  import Update from './views/Update.svelte';
8
8
  import { AUTO_EXCLUDED } from './utils/constants.js';
9
9
  import {
10
- resolveOptions,
11
10
  resolveFormatter,
12
- inferType
11
+ inferType,
12
+ buildFieldDefinitions
13
13
  } from './utils/resolution.js';
14
14
  import { isFilterable } from '../table/utils.js';
15
15
  import type { XlsxModule } from '../table/export.js';
@@ -226,23 +226,7 @@
226
226
  const resolvedFields: FieldDefinition<T>[] = $derived(
227
227
  fields ??
228
228
  (meta
229
- ? (Object.entries(meta) as [string, AttributeMetadata][])
230
- .filter(([k, m]) => !excluded.has(k) && !m.excludedFromCreate)
231
- .map(([k, m]) => ({
232
- attribute: k as keyof T & string,
233
- title: m.label,
234
- type: m.type ?? inferType(k, undefined),
235
- required: m.required,
236
- autocomplete: m.autocomplete,
237
- placeholder: m.placeholder,
238
- default: m.default,
239
- options: resolveOptions(m, data),
240
- dependentOptions: m.dependentOptions
241
- ? (record: Record<string, unknown>) => m.dependentOptions!(data, record)
242
- : undefined,
243
- disabled: m.disabled,
244
- seed: m.seed
245
- }))
229
+ ? buildFieldDefinitions<T>(meta, data, 'excludedFromCreate', excluded)
246
230
  : entityData.length > 0
247
231
  ? (Object.entries(entityData[0]) as [string, unknown][])
248
232
  .filter(([k]) => !excluded.has(k))
@@ -253,23 +237,7 @@
253
237
  const resolvedReadFields: FieldDefinition<T>[] = $derived(
254
238
  fields ??
255
239
  (meta
256
- ? (Object.entries(meta) as [string, AttributeMetadata][])
257
- .filter(([k, m]) => !excluded.has(k) && !m.excludedFromRead)
258
- .map(([k, m]) => ({
259
- attribute: k as keyof T & string,
260
- title: m.label,
261
- type: m.type ?? inferType(k, undefined),
262
- required: m.required,
263
- autocomplete: m.autocomplete,
264
- placeholder: m.placeholder,
265
- default: m.default,
266
- options: resolveOptions(m, data),
267
- dependentOptions: m.dependentOptions
268
- ? (record: Record<string, unknown>) => m.dependentOptions!(data, record)
269
- : undefined,
270
- disabled: m.disabled,
271
- seed: m.seed
272
- }))
240
+ ? buildFieldDefinitions<T>(meta, data, 'excludedFromRead', excluded)
273
241
  : entityData.length > 0
274
242
  ? (Object.entries(entityData[0]) as [string, unknown][])
275
243
  .filter(([k]) => !excluded.has(k))
@@ -280,23 +248,7 @@
280
248
  const resolvedUpdateFields: FieldDefinition<T>[] = $derived(
281
249
  fields ??
282
250
  (meta
283
- ? (Object.entries(meta) as [string, AttributeMetadata][])
284
- .filter(([k, m]) => !excluded.has(k) && !m.excludedFromUpdate)
285
- .map(([k, m]) => ({
286
- attribute: k as keyof T & string,
287
- title: m.label,
288
- type: m.type ?? inferType(k, undefined),
289
- required: m.required,
290
- autocomplete: m.autocomplete,
291
- placeholder: m.placeholder,
292
- default: m.default,
293
- options: resolveOptions(m, data),
294
- dependentOptions: m.dependentOptions
295
- ? (record: Record<string, unknown>) => m.dependentOptions!(data, record)
296
- : undefined,
297
- disabled: m.disabled,
298
- seed: m.seed
299
- }))
251
+ ? buildFieldDefinitions<T>(meta, data, 'excludedFromUpdate', excluded)
300
252
  : entityData.length > 0
301
253
  ? (Object.entries(entityData[0]) as [string, unknown][])
302
254
  .filter(([k]) => !excluded.has(k))
@@ -0,0 +1,6 @@
1
+ import type { FieldDefinition } from '../../../types/crud.js';
2
+ export type FieldGroup<T extends object = Record<string, unknown>> = {
3
+ title?: string;
4
+ fields: FieldDefinition<T>[];
5
+ };
6
+ export declare function groupFields<T extends object = Record<string, unknown>>(fields: FieldDefinition<T>[]): FieldGroup<T>[];
@@ -0,0 +1,23 @@
1
+ // Partitions a flat field list into visual groups for create/update/read
2
+ // views, based on `groupedAs`. Fields without a `groupedAs` render as their
3
+ // own single-field group (unchanged from today's flat layout). Fields sharing
4
+ // a `groupedAs` are collected together at the position of their first
5
+ // occurrence, preserving overall order.
6
+ export function groupFields(fields) {
7
+ const groups = [];
8
+ const byTitle = new Map();
9
+ for (const field of fields) {
10
+ if (!field.groupedAs) {
11
+ groups.push({ fields: [field] });
12
+ continue;
13
+ }
14
+ let group = byTitle.get(field.groupedAs);
15
+ if (!group) {
16
+ group = { title: field.groupedAs, fields: [] };
17
+ byTitle.set(field.groupedAs, group);
18
+ groups.push(group);
19
+ }
20
+ group.fields.push(field);
21
+ }
22
+ return groups;
23
+ }
@@ -1,4 +1,6 @@
1
1
  import type { AttributeMetadata, AttributeType, SelectOption } from '../../../types/attribute.js';
2
+ import type { FieldDefinition } from '../../../types/crud.js';
2
3
  export declare function resolveOptions(m: AttributeMetadata, d: unknown): SelectOption[] | undefined;
3
4
  export declare function resolveFormatter(m: AttributeMetadata, d: unknown): import("../../../index.ts").CellFormatter<any, any> | undefined;
4
5
  export declare function inferType(key: string, value: unknown): AttributeType;
6
+ export declare function buildFieldDefinitions<T extends object = Record<string, unknown>>(meta: Partial<Record<string, AttributeMetadata>>, data: unknown, excludedFlag: 'excludedFromCreate' | 'excludedFromRead' | 'excludedFromUpdate', excluded: Set<string>): FieldDefinition<T>[];
@@ -20,3 +20,33 @@ export function inferType(key, value) {
20
20
  return 'textarea';
21
21
  return 'text';
22
22
  }
23
+ // Shared by GenericCRUD's create/read/update field resolution: they only
24
+ // differ in which `excludedFromX` flag gates a field, so any metadata
25
+ // property added here is automatically threaded through all three views
26
+ // instead of needing to be copied into three near-identical blocks.
27
+ export function buildFieldDefinitions(meta, data, excludedFlag, excluded) {
28
+ return Object.entries(meta)
29
+ .filter(([k, m]) => !excluded.has(k) && !m[excludedFlag])
30
+ .map(([k, m]) => ({
31
+ attribute: k,
32
+ title: m.label,
33
+ type: m.type ?? inferType(k, undefined),
34
+ required: m.required,
35
+ autocomplete: m.autocomplete,
36
+ placeholder: m.placeholder,
37
+ default: m.default,
38
+ options: resolveOptions(m, data),
39
+ dependentOptions: m.dependentOptions
40
+ ? (record) => m.dependentOptions(data, record)
41
+ : undefined,
42
+ disabled: m.disabled,
43
+ seed: m.seed,
44
+ groupedAs: m.groupedAs,
45
+ min: m.min,
46
+ max: m.max,
47
+ integer: m.integer,
48
+ minLength: m.minLength,
49
+ maxLength: m.maxLength,
50
+ pattern: m.pattern
51
+ }));
52
+ }
@@ -0,0 +1,3 @@
1
+ import type { FieldDefinition } from '../../../types/crud.js';
2
+ import type { RuneforgeStrings } from '../../../i18n/types.js';
3
+ export declare function validateAll<T extends object = Record<string, unknown>>(fields: FieldDefinition<T>[], formData: FormData, strings: RuneforgeStrings): Record<string, string>;
@@ -0,0 +1,44 @@
1
+ import { fieldLabel } from './misc.js';
2
+ // Shared by Create.svelte/Update.svelte's client-side pre-submit check. Errors
3
+ // are surfaced through the same `fieldErrors` mechanism already used for
4
+ // `required`, not native HTML5 constraint validation, so every message is
5
+ // styled consistently regardless of which rule failed.
6
+ export function validateAll(fields, formData, strings) {
7
+ const errors = {};
8
+ for (const field of fields) {
9
+ const val = String(formData.get(field.attribute) ?? '').trim();
10
+ if (field.required && !val) {
11
+ errors[field.attribute] = strings.required(fieldLabel(field));
12
+ continue;
13
+ }
14
+ if (!val)
15
+ continue;
16
+ if (field.type === 'number') {
17
+ const num = Number(val);
18
+ if (Number.isNaN(num)) {
19
+ errors[field.attribute] = strings.invalidNumber(fieldLabel(field));
20
+ }
21
+ else if (field.integer && !Number.isInteger(num)) {
22
+ errors[field.attribute] = strings.integer(fieldLabel(field));
23
+ }
24
+ else if (field.min != null && num < field.min) {
25
+ errors[field.attribute] = strings.min(fieldLabel(field), field.min);
26
+ }
27
+ else if (field.max != null && num > field.max) {
28
+ errors[field.attribute] = strings.max(fieldLabel(field), field.max);
29
+ }
30
+ }
31
+ else {
32
+ if (field.minLength != null && val.length < field.minLength) {
33
+ errors[field.attribute] = strings.minLength(fieldLabel(field), field.minLength);
34
+ }
35
+ else if (field.maxLength != null && val.length > field.maxLength) {
36
+ errors[field.attribute] = strings.maxLength(fieldLabel(field), field.maxLength);
37
+ }
38
+ else if (field.pattern && !new RegExp(field.pattern).test(val)) {
39
+ errors[field.attribute] = strings.pattern(fieldLabel(field));
40
+ }
41
+ }
42
+ }
43
+ return errors;
44
+ }
@@ -6,7 +6,8 @@
6
6
  import Header from '../../common/Header.svelte';
7
7
  import { getIconSet } from '../../../icons/context.js';
8
8
  import { defaultIconSet } from '../../../icons/sets/default.js';
9
- import { fieldLabel } from '../utils/misc.js';
9
+ import { validateAll } from '../utils/validation.js';
10
+ import { groupFields } from '../utils/grouping.js';
10
11
  import type { ActionConfiguration, FieldDefinition } from '../../../types/crud.js';
11
12
  import { getStrings } from '../../../i18n/context.js';
12
13
 
@@ -53,17 +54,7 @@
53
54
 
54
55
  let record = $state<Record<string, unknown>>(untrack(() => emptyRecord()));
55
56
 
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
-
57
+ const groups = $derived(groupFields(fields));
67
58
  const hasFileField = $derived(fields.some((f) => f.type === 'file'));
68
59
 
69
60
  const errorEntries = $derived([
@@ -103,7 +94,7 @@
103
94
  use:enhance={({ formData, cancel }) => {
104
95
  fieldErrors = {};
105
96
  internalError = '';
106
- const errs = validateAll(formData);
97
+ const errs = validateAll(fields, formData, strings);
107
98
  if (Object.keys(errs).length > 0) {
108
99
  fieldErrors = errs;
109
100
  cancel();
@@ -127,8 +118,21 @@
127
118
  };
128
119
  }}
129
120
  >
130
- {#each fields as field (field.attribute)}
131
- <Field {field} bind:record error={fieldErrors[field.attribute] ?? ''} />
121
+ {#each groups as group, i (group.title ?? `_ungrouped_${i}`)}
122
+ {#if group.title}
123
+ <fieldset class="fieldset border border-base-300 rounded-box p-4">
124
+ <legend class="fieldset-legend px-2">{group.title}</legend>
125
+ <div class="flex flex-col gap-4">
126
+ {#each group.fields as field (field.attribute)}
127
+ <Field {field} bind:record error={fieldErrors[field.attribute] ?? ''} />
128
+ {/each}
129
+ </div>
130
+ </fieldset>
131
+ {:else}
132
+ {#each group.fields as field (field.attribute)}
133
+ <Field {field} bind:record error={fieldErrors[field.attribute] ?? ''} />
134
+ {/each}
135
+ {/if}
132
136
  {/each}
133
137
 
134
138
  <div class="flex flex-col-reverse gap-2 pt-2 sm:flex-row sm:justify-end">
@@ -6,6 +6,7 @@
6
6
  import Header from '../../common/Header.svelte';
7
7
  import { getIconSet } from '../../../icons/context.js';
8
8
  import { defaultIconSet } from '../../../icons/sets/default.js';
9
+ import { groupFields } from '../utils/grouping.js';
9
10
  import type { ActionConfiguration, FieldDefinition } from '../../../types/crud.js';
10
11
  import { getStrings } from '../../../i18n/context.js';
11
12
 
@@ -60,6 +61,8 @@
60
61
  })();
61
62
  return () => { cancelled = true; };
62
63
  });
64
+
65
+ const groups = $derived(groupFields(fields));
63
66
  </script>
64
67
 
65
68
  <div class="flex flex-col gap-6">
@@ -73,8 +76,21 @@
73
76
  />
74
77
 
75
78
  <div class="fields-panel mx-auto flex w-full flex-col gap-4 px-4">
76
- {#each fields as field (field.attribute)}
77
- <Field {field} {record} readonly />
79
+ {#each groups as group, i (group.title ?? `_ungrouped_${i}`)}
80
+ {#if group.title}
81
+ <fieldset class="fieldset border border-base-300 rounded-box p-4">
82
+ <legend class="fieldset-legend px-2">{group.title}</legend>
83
+ <div class="flex flex-col gap-4">
84
+ {#each group.fields as field (field.attribute)}
85
+ <Field {field} {record} readonly />
86
+ {/each}
87
+ </div>
88
+ </fieldset>
89
+ {:else}
90
+ {#each group.fields as field (field.attribute)}
91
+ <Field {field} {record} readonly />
92
+ {/each}
93
+ {/if}
78
94
  {/each}
79
95
 
80
96
  <div class="flex flex-col-reverse gap-2 pt-2 sm:flex-row sm:justify-end">
@@ -6,7 +6,8 @@
6
6
  import Header from '../../common/Header.svelte';
7
7
  import { getIconSet } from '../../../icons/context.js';
8
8
  import { defaultIconSet } from '../../../icons/sets/default.js';
9
- import { fieldLabel } from '../utils/misc.js';
9
+ import { validateAll } from '../utils/validation.js';
10
+ import { groupFields } from '../utils/grouping.js';
10
11
  import type { ActionConfiguration, FieldDefinition } from '../../../types/crud.js';
11
12
  import { getStrings } from '../../../i18n/context.js';
12
13
 
@@ -64,17 +65,7 @@
64
65
  untrack(() => { record = seedFromInstance(instance as Record<string, unknown>); });
65
66
  });
66
67
 
67
- function validateAll(formData: FormData): Record<string, string> {
68
- const errs: Record<string, string> = {};
69
- for (const field of fields) {
70
- if (field.required) {
71
- const val = String(formData.get(field.attribute) ?? '').trim();
72
- if (!val) errs[field.attribute] = strings.required(fieldLabel(field));
73
- }
74
- }
75
- return errs;
76
- }
77
-
68
+ const groups = $derived(groupFields(fields));
78
69
  const hasFileField = $derived(fields.some((f) => f.type === 'file'));
79
70
 
80
71
  const errorEntries = $derived([
@@ -114,7 +105,7 @@
114
105
  use:enhance={({ formData, cancel }) => {
115
106
  fieldErrors = {};
116
107
  internalError = '';
117
- const errs = validateAll(formData);
108
+ const errs = validateAll(fields, formData, strings);
118
109
  if (Object.keys(errs).length > 0) {
119
110
  fieldErrors = errs;
120
111
  cancel();
@@ -134,8 +125,21 @@
134
125
  >
135
126
  <input type="hidden" name="id" value={String(record[idKey] ?? '')} />
136
127
 
137
- {#each fields as field (field.attribute)}
138
- <Field {field} bind:record error={fieldErrors[field.attribute] ?? ''} />
128
+ {#each groups as group, i (group.title ?? `_ungrouped_${i}`)}
129
+ {#if group.title}
130
+ <fieldset class="fieldset border border-base-300 rounded-box p-4">
131
+ <legend class="fieldset-legend px-2">{group.title}</legend>
132
+ <div class="flex flex-col gap-4">
133
+ {#each group.fields as field (field.attribute)}
134
+ <Field {field} bind:record error={fieldErrors[field.attribute] ?? ''} />
135
+ {/each}
136
+ </div>
137
+ </fieldset>
138
+ {:else}
139
+ {#each group.fields as field (field.attribute)}
140
+ <Field {field} bind:record error={fieldErrors[field.attribute] ?? ''} />
141
+ {/each}
142
+ {/if}
139
143
  {/each}
140
144
 
141
145
  <div class="flex flex-col-reverse gap-2 pt-2 sm:flex-row sm:justify-end">
package/dist/i18n/en.js CHANGED
@@ -26,5 +26,12 @@ export const en = {
26
26
  confirm: 'Confirm',
27
27
  deleteConfirm: (count, actionLabel) => `Are you sure you want to ${String(actionLabel).toLowerCase()} ${count} item${count === 1 ? '' : 's'}?`,
28
28
  required: (field) => `${field} is required`,
29
+ invalidNumber: (field) => `${field} must be a number`,
30
+ integer: (field) => `${field} must be a whole number`,
31
+ min: (field, min) => `${field} must be greater than or equal to ${min}`,
32
+ max: (field, max) => `${field} must be less than or equal to ${max}`,
33
+ minLength: (field, min) => `${field} must be at least ${min} characters`,
34
+ maxLength: (field, max) => `${field} must be at most ${max} characters`,
35
+ pattern: (field) => `${field} has an invalid format`,
29
36
  serverError: 'Unexpected server error.'
30
37
  };
package/dist/i18n/es.js CHANGED
@@ -26,5 +26,12 @@ export const es = {
26
26
  confirm: 'Confirmar',
27
27
  deleteConfirm: (count, actionLabel) => `¿Seguro que querés ${String(actionLabel).toLowerCase()} ${count} elemento${count === 1 ? '' : 's'}?`,
28
28
  required: (field) => `${field} es requerido`,
29
+ invalidNumber: (field) => `${field} debe ser un número`,
30
+ integer: (field) => `${field} debe ser un número entero`,
31
+ min: (field, min) => `${field} debe ser mayor o igual a ${min}`,
32
+ max: (field, max) => `${field} debe ser menor o igual a ${max}`,
33
+ minLength: (field, min) => `${field} debe tener al menos ${min} caracteres`,
34
+ maxLength: (field, max) => `${field} debe tener como máximo ${max} caracteres`,
35
+ pattern: (field) => `${field} tiene un formato inválido`,
29
36
  serverError: 'Error inesperado del servidor.'
30
37
  };
@@ -26,5 +26,12 @@ export interface RuneforgeStrings {
26
26
  confirm: string;
27
27
  deleteConfirm: (count: number, actionLabel: string) => string;
28
28
  required: (field: string) => string;
29
+ invalidNumber: (field: string) => string;
30
+ integer: (field: string) => string;
31
+ min: (field: string, min: number) => string;
32
+ max: (field: string, max: number) => string;
33
+ minLength: (field: string, min: number) => string;
34
+ maxLength: (field: string, max: number) => string;
35
+ pattern: (field: string) => string;
29
36
  serverError: string;
30
37
  }
package/dist/index.d.ts CHANGED
@@ -41,6 +41,9 @@ export { default as CRUDRead } from './components/crud/views/Read.svelte';
41
41
  export { default as CRUDUpdate } from './components/crud/views/Update.svelte';
42
42
  export { default as SearchInput } from './components/crud/SearchInput.svelte';
43
43
  export { AUTO_EXCLUDED } from './components/crud/utils/constants.js';
44
- export { resolveOptions, resolveFormatter, inferType } from './components/crud/utils/resolution.js';
44
+ export { resolveOptions, resolveFormatter, inferType, buildFieldDefinitions } from './components/crud/utils/resolution.js';
45
45
  export { fieldLabel, initials } from './components/crud/utils/misc.js';
46
46
  export { formatBoolean, formatDatetime, formatTruncateTextUpTo, formatInstance } from './components/crud/utils/formatters.js';
47
+ export { groupFields } from './components/crud/utils/grouping.js';
48
+ export type { FieldGroup } from './components/crud/utils/grouping.js';
49
+ export { validateAll } from './components/crud/utils/validation.js';
package/dist/index.js CHANGED
@@ -39,6 +39,8 @@ export { default as CRUDUpdate } from './components/crud/views/Update.svelte';
39
39
  export { default as SearchInput } from './components/crud/SearchInput.svelte';
40
40
  // CRUD utilities
41
41
  export { AUTO_EXCLUDED } from './components/crud/utils/constants.js';
42
- export { resolveOptions, resolveFormatter, inferType } from './components/crud/utils/resolution.js';
42
+ export { resolveOptions, resolveFormatter, inferType, buildFieldDefinitions } from './components/crud/utils/resolution.js';
43
43
  export { fieldLabel, initials } from './components/crud/utils/misc.js';
44
44
  export { formatBoolean, formatDatetime, formatTruncateTextUpTo, formatInstance } from './components/crud/utils/formatters.js';
45
+ export { groupFields } from './components/crud/utils/grouping.js';
46
+ export { validateAll } from './components/crud/utils/validation.js';
@@ -41,4 +41,11 @@ export type AttributeMetadata = {
41
41
  excludedFromRead?: boolean;
42
42
  sortable?: boolean;
43
43
  filterable?: boolean;
44
+ groupedAs?: string;
45
+ min?: number;
46
+ max?: number;
47
+ integer?: boolean;
48
+ minLength?: number;
49
+ maxLength?: number;
50
+ pattern?: string;
44
51
  };
@@ -31,6 +31,13 @@ export interface FieldDefinition<T extends object = Record<string, unknown>> {
31
31
  }[];
32
32
  disabled?: (record: Record<string, unknown>) => boolean;
33
33
  seed?: (instance: any) => unknown;
34
+ groupedAs?: string;
35
+ min?: number;
36
+ max?: number;
37
+ integer?: boolean;
38
+ minLength?: number;
39
+ maxLength?: number;
40
+ pattern?: string;
34
41
  }
35
42
  export interface ActionConfiguration<T extends object = Record<string, unknown>> {
36
43
  enabled?: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "runeforge",
3
- "version": "0.0.20",
3
+ "version": "0.0.21",
4
4
  "description": "SvelteKit toolkit for building metadata-driven CRUD interfaces with tables, forms, and actions",
5
5
  "license": "MIT",
6
6
  "author": "Ezequiel Puerta",