runeforge 0.0.58 → 0.0.59

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/dist/components/crud/EmbeddedField.svelte +33 -5
  2. package/dist/components/crud/EmbeddedField.svelte.d.ts +6 -1
  3. package/dist/components/crud/Field.svelte +76 -451
  4. package/dist/components/crud/Field.svelte.d.ts +5 -1
  5. package/dist/components/crud/GenericCRUD.svelte +4 -4
  6. package/dist/components/crud/GenericCRUD.svelte.d.ts +4 -1
  7. package/dist/components/crud/SearchInput.svelte +1 -1
  8. package/dist/components/crud/SearchInput.svelte.d.ts +1 -1
  9. package/dist/components/crud/fields/BooleanField.svelte +38 -0
  10. package/dist/components/crud/fields/BooleanField.svelte.d.ts +33 -0
  11. package/dist/components/crud/fields/DatetimeField.svelte +250 -0
  12. package/dist/components/crud/fields/DatetimeField.svelte.d.ts +34 -0
  13. package/dist/components/crud/fields/DefaultField.svelte +67 -0
  14. package/dist/components/crud/fields/DefaultField.svelte.d.ts +34 -0
  15. package/dist/components/crud/fields/FileField.svelte +70 -0
  16. package/dist/components/crud/fields/FileField.svelte.d.ts +34 -0
  17. package/dist/components/crud/fields/MultiSelectField.svelte +70 -0
  18. package/dist/components/crud/fields/MultiSelectField.svelte.d.ts +34 -0
  19. package/dist/components/crud/fields/SelectField.svelte +67 -0
  20. package/dist/components/crud/fields/SelectField.svelte.d.ts +34 -0
  21. package/dist/components/crud/fields/TextareaField.svelte +55 -0
  22. package/dist/components/crud/fields/TextareaField.svelte.d.ts +34 -0
  23. package/dist/components/crud/fields/TreeField.svelte +63 -0
  24. package/dist/components/crud/fields/TreeField.svelte.d.ts +33 -0
  25. package/dist/components/crud/utils/embedded.d.ts +1 -1
  26. package/dist/components/crud/utils/grouping.d.ts +1 -1
  27. package/dist/components/crud/utils/resolution.d.ts +1 -1
  28. package/dist/components/crud/utils/resolution.js +3 -1
  29. package/dist/components/crud/utils/validation.d.ts +1 -1
  30. package/dist/components/crud/views/Create.svelte +2 -1
  31. package/dist/components/crud/views/Create.svelte.d.ts +2 -1
  32. package/dist/components/crud/views/Read.svelte +6 -5
  33. package/dist/components/crud/views/Read.svelte.d.ts +2 -1
  34. package/dist/components/crud/views/Update.svelte +2 -1
  35. package/dist/components/crud/views/Update.svelte.d.ts +2 -1
  36. package/dist/components/crud/views/list/List.svelte +3 -3
  37. package/dist/components/crud/views/list/List.svelte.d.ts +3 -1
  38. package/dist/components/crud/views/list/Modals.svelte +1 -1
  39. package/dist/components/crud/views/list/Modals.svelte.d.ts +1 -1
  40. package/dist/components/crud/views/list/Table.svelte +3 -6
  41. package/dist/components/crud/views/list/Table.svelte.d.ts +3 -1
  42. package/dist/components/crud/views/list/Toolbar.svelte +3 -4
  43. package/dist/components/crud/views/list/Toolbar.svelte.d.ts +3 -1
  44. package/dist/components/table/ColumnFilter.svelte +1 -1
  45. package/dist/components/table/ColumnFilter.svelte.d.ts +1 -1
  46. package/dist/components/table/PaginatedTable.svelte +1 -1
  47. package/dist/components/table/PaginatedTable.svelte.d.ts +1 -1
  48. package/dist/components/table/TableBody.svelte +1 -1
  49. package/dist/components/table/TableBody.svelte.d.ts +1 -1
  50. package/dist/components/table/TableHeader.svelte +1 -1
  51. package/dist/components/table/TableHeader.svelte.d.ts +1 -1
  52. package/dist/components/table/export.d.ts +1 -1
  53. package/dist/components/table/state.svelte.d.ts +1 -1
  54. package/dist/components/table/utils.d.ts +1 -1
  55. package/dist/index.d.ts +5 -1
  56. package/dist/types/attribute.d.ts +25 -4
  57. package/dist/types/{crud.d.ts → crud/actions.d.ts} +0 -127
  58. package/dist/types/crud/columns.d.ts +23 -0
  59. package/dist/types/crud/columns.js +1 -0
  60. package/dist/types/crud/fields.d.ts +48 -0
  61. package/dist/types/crud/fields.js +1 -0
  62. package/dist/types/crud/list-config.d.ts +23 -0
  63. package/dist/types/crud/list-config.js +1 -0
  64. package/dist/types/crud/reorder.d.ts +45 -0
  65. package/dist/types/crud/reorder.js +1 -0
  66. package/package.json +1 -1
  67. /package/dist/types/{crud.js → crud/actions.js} +0 -0
@@ -1,4 +1,5 @@
1
1
  <script lang="ts" generics="T extends object = Record<string, unknown>">
2
+ import { untrack } from 'svelte';
2
3
  import Field from './Field.svelte';
3
4
  import Button from '../form/Button.svelte';
4
5
  import Modal from '../Modal.svelte';
@@ -6,7 +7,7 @@
6
7
  import { fieldLabel } from './utils/misc.js';
7
8
  import { validateAll } from './utils/validation.js';
8
9
  import { groupFields } from './utils/grouping.js';
9
- import type { FieldDefinition } from '../../types/crud.js';
10
+ import type { FieldDefinition } from '../../types/crud/fields.js';
10
11
  import { getStrings } from '../../i18n/context.js';
11
12
 
12
13
  const strings = getStrings();
@@ -17,6 +18,11 @@
17
18
  readonly = false
18
19
  }: {
19
20
  field: FieldDefinition<T>;
21
+ /** The record that owns this embedded field (the form record for a
22
+ * top-level embedded field, or the outer item's draft for one nested
23
+ * inside another embedded field) — passed through to each sub-field's
24
+ * `hidden`/`disabled`/`required`/`dependentOptions` as their `parent`
25
+ * argument, and to `field.revalidate` as its own parent argument. */
20
26
  record?: Record<string, unknown>;
21
27
  readonly?: boolean;
22
28
  } = $props();
@@ -25,6 +31,28 @@
25
31
  const subGroups = $derived(groupFields(subFields));
26
32
  const items = $derived((record[field.attribute] as Record<string, unknown>[] | undefined) ?? []);
27
33
 
34
+ // Re-derives the embedded list right after `field.dependsOn` changes value
35
+ // on the parent record — e.g. dropping items that no longer apply once a
36
+ // sibling "owner kind" select flips — instead of leaving them stale until
37
+ // the user happens to touch this field again. Scoped to exactly that one
38
+ // parent attribute: reading it is this effect's only tracked dependency,
39
+ // and everything else (including its own write to `record[field.attribute]`)
40
+ // runs inside `untrack` so the effect never re-triggers itself.
41
+ $effect(() => {
42
+ if (!field.dependsOn || !field.revalidate) return;
43
+ // Read (only) to register as this effect's sole tracked dependency —
44
+ // everything that actually acts on it happens inside untrack() below,
45
+ // so the effect reruns exactly when this one value changes and never
46
+ // because of its own write to record[field.attribute].
47
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
48
+ const dependencyValue = record[field.dependsOn];
49
+ untrack(() => {
50
+ const current = (record[field.attribute] as Record<string, unknown>[] | undefined) ?? [];
51
+ const next = field.revalidate!(current, record);
52
+ if (next !== current) record[field.attribute] = next;
53
+ });
54
+ });
55
+
28
56
  let modalOpen = $state(false);
29
57
  // null while adding a new item; the item's index while editing an existing
30
58
  // one, so saveItem knows whether to append or replace in place.
@@ -141,11 +169,11 @@
141
169
  {#if row.length > 1}
142
170
  <div class="flex flex-col gap-4 md:flex-row">
143
171
  {#each row as f (f.attribute)}
144
- <Field field={f} bind:record={draft} error={draftErrors[f.attribute] ?? ''} class="md:min-w-0 md:flex-1" />
172
+ <Field field={f} bind:record={draft} parentRecord={record} error={draftErrors[f.attribute] ?? ''} class="md:min-w-0 md:flex-1" />
145
173
  {/each}
146
174
  </div>
147
175
  {:else}
148
- <Field field={row[0]} bind:record={draft} error={draftErrors[row[0].attribute] ?? ''} />
176
+ <Field field={row[0]} bind:record={draft} parentRecord={record} error={draftErrors[row[0].attribute] ?? ''} />
149
177
  {/if}
150
178
  {/each}
151
179
  </div>
@@ -155,11 +183,11 @@
155
183
  {#if row.length > 1}
156
184
  <div class="flex flex-col gap-4 md:flex-row">
157
185
  {#each row as f (f.attribute)}
158
- <Field field={f} bind:record={draft} error={draftErrors[f.attribute] ?? ''} class="md:min-w-0 md:flex-1" />
186
+ <Field field={f} bind:record={draft} parentRecord={record} error={draftErrors[f.attribute] ?? ''} class="md:min-w-0 md:flex-1" />
159
187
  {/each}
160
188
  </div>
161
189
  {:else}
162
- <Field field={row[0]} bind:record={draft} error={draftErrors[row[0].attribute] ?? ''} />
190
+ <Field field={row[0]} bind:record={draft} parentRecord={record} error={draftErrors[row[0].attribute] ?? ''} />
163
191
  {/if}
164
192
  {/each}
165
193
  {/if}
@@ -1,7 +1,12 @@
1
- import type { FieldDefinition } from '../../types/crud.js';
1
+ import type { FieldDefinition } from '../../types/crud/fields.js';
2
2
  declare function $$render<T extends object = Record<string, unknown>>(): {
3
3
  props: {
4
4
  field: FieldDefinition<T>;
5
+ /** The record that owns this embedded field (the form record for a
6
+ * top-level embedded field, or the outer item's draft for one nested
7
+ * inside another embedded field) — passed through to each sub-field's
8
+ * `hidden`/`disabled`/`required`/`dependentOptions` as their `parent`
9
+ * argument, and to `field.revalidate` as its own parent argument. */
5
10
  record?: Record<string, unknown>;
6
11
  readonly?: boolean;
7
12
  };
@@ -1,497 +1,122 @@
1
1
  <script lang="ts" generics="T extends object = Record<string, unknown>">
2
- import { onMount } from 'svelte';
3
- import Avatar from '../Avatar.svelte';
4
2
  import Button from '../form/Button.svelte';
5
3
  import Label from '../form/Label.svelte';
6
- import Select from '../form/Select.svelte';
7
- import MultiSelect from '../form/MultiSelect.svelte';
8
- import Tree from '../form/Tree.svelte';
9
4
  import EmbeddedField from './EmbeddedField.svelte';
10
- import { fieldLabel, initials } from './utils/misc.js';
11
- import type { FieldDefinition } from '../../types/crud.js';
12
- import { getStrings } from '../../i18n/context.js';
13
-
14
- const strings = getStrings();
15
-
16
- onMount(() => {
17
- import('cally');
18
- });
5
+ import BooleanField from './fields/BooleanField.svelte';
6
+ import FileField from './fields/FileField.svelte';
7
+ import SelectField from './fields/SelectField.svelte';
8
+ import MultiSelectField from './fields/MultiSelectField.svelte';
9
+ import TreeField from './fields/TreeField.svelte';
10
+ import DatetimeField from './fields/DatetimeField.svelte';
11
+ import TextareaField from './fields/TextareaField.svelte';
12
+ import DefaultField from './fields/DefaultField.svelte';
13
+ import { fieldLabel } from './utils/misc.js';
14
+ import type { FieldDefinition } from '../../types/crud/fields.js';
19
15
 
20
16
  let {
21
17
  field,
22
18
  record = $bindable({} as Record<string, unknown>),
19
+ parentRecord = undefined,
23
20
  error = '',
24
21
  readonly = false,
25
22
  class: className = ''
26
23
  }: {
27
24
  field: FieldDefinition<T>;
28
25
  record?: Record<string, unknown>;
26
+ /** Set only when this Field renders a sub-field of an `embedded` field
27
+ * — the outer form's own record, passed through to `hidden`/`disabled`/
28
+ * `required`/`dependentOptions` as their second argument. */
29
+ parentRecord?: Record<string, unknown>;
29
30
  error?: string;
30
31
  readonly?: boolean;
31
32
  class?: string;
32
33
  } = $props();
33
34
 
34
- const datePopId = $props.id();
35
- const dateAnchorName = `--date-anchor-${datePopId}`;
36
- let datePopoverEl: HTMLElement | undefined = $state();
37
- let calendarDateEl: (HTMLElement & { value: string }) | undefined = $state();
38
-
39
- const name = $derived(readonly ? undefined : field.attribute);
40
35
  const labelText = $derived(fieldLabel(field));
41
- let filePreview = $state<string | null>(null);
42
-
43
- function onFileChange(e: Event & { currentTarget: HTMLInputElement }) {
44
- const file = e.currentTarget.files?.[0];
45
- if (filePreview) URL.revokeObjectURL(filePreview);
46
- filePreview = file && file.type.startsWith('image/') ? URL.createObjectURL(file) : null;
47
- }
48
-
49
- $effect(() => () => {
50
- if (filePreview) URL.revokeObjectURL(filePreview);
51
- });
52
-
53
- const saved = $derived(record[field.attribute]);
54
- const preview = $derived(filePreview ?? (typeof saved === 'string' && saved ? saved : null));
55
- const avatarInitials = $derived(initials(record.firstName as string, record.lastName as string));
56
- const displayValue = $derived(saved == null ? '' : String(saved));
57
- // Only for readonly branches with no other resolution mechanism (select/
58
- // multiselect/tree already resolve their own label from `options`, and
59
- // some of their formatters return HTML meant for a table cell, not plain
60
- // input text). Falls back to `displayValue` so a value with no formatter
61
- // still renders instead of going blank.
62
- const formattedValue = $derived(
63
- readonly && field.formatter ? field.formatter(saved, record) : displayValue
64
- );
65
- const selectOptions = $derived(
66
- field.dependentOptions ? field.dependentOptions(record) : (field.options ?? [])
67
- );
68
- const fieldDisabled = $derived(field.disabled ? field.disabled(record) : false);
69
36
  const fieldRequired = $derived(
70
- typeof field.required === 'function' ? field.required(record) : !!field.required
37
+ typeof field.required === 'function' ? field.required(record, parentRecord) : !!field.required
71
38
  );
72
39
  const fieldHidden = $derived(
73
- typeof field.hidden === 'function' ? field.hidden(record) : !!field.hidden
40
+ typeof field.hidden === 'function' ? field.hidden(record, parentRecord) : !!field.hidden
74
41
  );
75
- const isMultiValued = $derived(field.type === 'multiselect' || field.type === 'tree');
76
42
  const fieldActions = $derived(
77
43
  (field.actions ?? []).filter((action) => action.condition?.(record) ?? true)
78
44
  );
79
-
80
- // cally's `change` event doesn't bubble, so the usual `onchange={...}` prop
81
- // never fires — Svelte 5 delegates events like `change` to a listener on
82
- // a shared ancestor, which only ever sees events that bubble up to it.
83
- // Attaching directly on the element (same fix ColumnFilter's date-range
84
- // picker already uses) sidesteps delegation entirely.
85
- $effect(() => {
86
- if (!calendarDateEl) return;
87
- function handler() {
88
- if (fieldDisabled) return;
89
- setDateTime(calendarDateEl!.value, timePart);
90
- datePopoverEl?.hidePopover();
91
- }
92
- calendarDateEl.addEventListener('change', handler);
93
- return () => calendarDateEl?.removeEventListener('change', handler);
94
- });
95
-
96
- function pad(n: number): string {
97
- return String(n).padStart(2, '0');
98
- }
99
-
100
- // ISO (yyyy-mm-dd) <-> display (dd/mm/yyyy) conversion and typing mask.
101
- function isoDateToDisplay(iso: string): string {
102
- const [year, month, day] = iso.split('-');
103
- return `${day}/${month}/${year}`;
104
- }
105
-
106
- function displayToIsoDate(display: string): string | null {
107
- const match = display.trim().match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/);
108
- if (!match) return null;
109
- const [, dd, mm, yyyy] = match;
110
- const day = Number(dd);
111
- const month = Number(mm);
112
- const year = Number(yyyy);
113
- // Rejects invalid dates (e.g. 31/02).
114
- const date = new Date(Date.UTC(year, month - 1, day));
115
- if (
116
- date.getUTCFullYear() !== year ||
117
- date.getUTCMonth() !== month - 1 ||
118
- date.getUTCDate() !== day
119
- ) {
120
- return null;
121
- }
122
- return `${yyyy}-${mm.padStart(2, '0')}-${dd.padStart(2, '0')}`;
123
- }
124
-
125
- // Inserts "/" as digits are typed: "21" -> "21/", "211" -> "21/1".
126
- function maskDateDigits(digits: string): string {
127
- const day = digits.slice(0, 2);
128
- const month = digits.slice(2, 4);
129
- const year = digits.slice(4, 8);
130
- let out = day;
131
- if (digits.length >= 2) out += '/';
132
- out += month;
133
- if (digits.length >= 4) out += '/';
134
- return out + year;
135
- }
136
-
137
- // `record[field.attribute]` may be empty, a bare "YYYY-MM-DD" (freshly
138
- // picked, before any time is set) or a full ISO datetime (loaded from an
139
- // existing instance) — parsed once here so the date button, the time
140
- // input and cally's own `value` all derive from the same local-time
141
- // breakdown instead of each re-parsing the raw string their own way.
142
- const parsedDateTime = $derived.by(() => {
143
- const raw = record[field.attribute];
144
- if (raw == null || raw === '') return null;
145
- const d = new Date(String(raw));
146
- return isNaN(d.getTime()) ? null : d;
147
- });
148
- const datePart = $derived(
149
- parsedDateTime
150
- ? `${parsedDateTime.getFullYear()}-${pad(parsedDateTime.getMonth() + 1)}-${pad(parsedDateTime.getDate())}`
151
- : ''
152
- );
153
- const timePart = $derived(
154
- parsedDateTime ? `${pad(parsedDateTime.getHours())}:${pad(parsedDateTime.getMinutes())}` : ''
155
- );
156
-
157
- function setDateTime(nextDatePart: string, nextTimePart: string) {
158
- record[field.attribute] = nextDatePart ? `${nextDatePart}T${nextTimePart || '00:00'}` : '';
159
- }
160
-
161
- // Typed display text for the date part, synced from `datePart`.
162
- // eslint-disable-next-line svelte/prefer-writable-derived
163
- let dateTextValue = $state('');
164
- $effect(() => {
165
- dateTextValue = datePart ? isoDateToDisplay(datePart) : '';
166
- });
167
-
168
- function onDateTextInput(event: Event) {
169
- const raw = (event.currentTarget as HTMLInputElement).value;
170
- const prevDigits = dateTextValue.replace(/\D/g, '');
171
- let digits = raw.replace(/\D/g, '');
172
- // Drops one extra digit when backspacing over an inserted "/".
173
- if (raw.length < dateTextValue.length && digits.length === prevDigits.length) {
174
- digits = digits.slice(0, -1);
175
- }
176
- digits = digits.slice(0, 8);
177
- dateTextValue = maskDateDigits(digits);
178
- if (!digits) {
179
- setDateTime('', timePart);
180
- return;
181
- }
182
- const parsed = displayToIsoDate(dateTextValue);
183
- if (parsed) setDateTime(parsed, timePart);
184
- }
185
-
186
- function onDateTextBlur() {
187
- if (!dateTextValue.trim()) {
188
- setDateTime('', timePart);
189
- return;
190
- }
191
- if (!displayToIsoDate(dateTextValue)) {
192
- // Discards invalid input.
193
- dateTextValue = datePart ? isoDateToDisplay(datePart) : '';
194
- }
195
- }
196
-
197
- $effect(() => {
198
- if (!field.dependentOptions || isMultiValued) return;
199
- const current = record[field.attribute];
200
- if (current && !selectOptions.some((o) => o.value === String(current))) {
201
- record[field.attribute] = '';
202
- }
203
- });
204
-
205
- $effect(() => {
206
- if (!field.dependentOptions || !isMultiValued) return;
207
- const current = record[field.attribute];
208
- if (!Array.isArray(current)) return;
209
- const validValues = new Set(selectOptions.map((o) => o.value));
210
- const pruned = current.filter((v) => validValues.has(String(v)));
211
- if (pruned.length !== current.length) {
212
- record[field.attribute] = pruned;
213
- }
214
- });
215
45
  </script>
216
46
 
217
- {#if !fieldHidden}
218
- <div class="flex flex-col gap-1 {className}">
219
- {#if field.type === 'file'}
220
- <div class="flex justify-center">
221
- <Avatar
222
- src={preview}
223
- text={avatarInitials}
224
- alt={labelText}
225
- class="w-20 rounded-full"
226
- textClass="text-xl"
227
- />
47
+ <!-- Shared by every field type below, always rendered in the same spot
48
+ (right after an optional type-specific lead-in, e.g. FileField's avatar
49
+ preview) so each type-specific component just brackets its own markup
50
+ with these two instead of re-declaring the label/error chrome itself. -->
51
+ {#snippet labelRow()}
52
+ <div class="flex items-center justify-between gap-2">
53
+ <Label
54
+ text={labelText}
55
+ for={field.attribute}
56
+ capitalize={true}
57
+ required={fieldRequired && !readonly}
58
+ />
59
+ {#if !readonly && fieldActions.length > 0}
60
+ <div class="flex items-center gap-1">
61
+ {#each fieldActions as action (action.label)}
62
+ <Button
63
+ type="button"
64
+ variant="ghost"
65
+ class={['btn-xs', action.class]}
66
+ title={action.label}
67
+ onclick={() =>
68
+ action.run(record[field.attribute], record, (attribute, value) => {
69
+ record[attribute] = value;
70
+ })}
71
+ >
72
+ {#if action.icon}
73
+ {@const Icon = action.icon}
74
+ <Icon class="size-4" />
75
+ {/if}
76
+ {action.label}
77
+ </Button>
78
+ {/each}
228
79
  </div>
229
80
  {/if}
81
+ </div>
82
+ {/snippet}
230
83
 
231
- <div class="flex items-center justify-between gap-2">
232
- <Label
233
- text={labelText}
234
- for={field.attribute}
235
- capitalize={true}
236
- required={fieldRequired && !readonly}
237
- />
238
- {#if !readonly && fieldActions.length > 0}
239
- <div class="flex items-center gap-1">
240
- {#each fieldActions as action (action.label)}
241
- <Button
242
- type="button"
243
- variant="ghost"
244
- class={['btn-xs', action.class]}
245
- title={action.label}
246
- onclick={() =>
247
- action.run(record[field.attribute], record, (attribute, value) => {
248
- record[attribute] = value;
249
- })}
250
- >
251
- {#if action.icon}
252
- {@const Icon = action.icon}
253
- <Icon class="size-4" />
254
- {/if}
255
- {action.label}
256
- </Button>
257
- {/each}
258
- </div>
259
- {/if}
260
- </div>
84
+ {#snippet errorRow()}
85
+ {#if error}
86
+ <span class="text-error text-xs">{error}</span>
87
+ {/if}
88
+ {/snippet}
261
89
 
90
+ {#if !fieldHidden}
91
+ <div class="flex flex-col gap-1 {className}">
262
92
  {#if field.type === 'boolean'}
263
- <input
264
- type="checkbox"
265
- id={field.attribute}
266
- {name}
267
- class="toggle toggle-primary"
268
- checked={!!saved}
269
- disabled={readonly || fieldDisabled}
270
- onchange={(e) => {
271
- record[field.attribute] = (e.currentTarget as HTMLInputElement).checked;
272
- }}
273
- />
93
+ <BooleanField {field} bind:record {parentRecord} {readonly} {labelRow} {errorRow} />
274
94
  {:else if field.type === 'file'}
275
- {#if !readonly}
276
- <input
277
- type="file"
278
- id={field.attribute}
279
- {name}
280
- class="file-input file-input-bordered w-full"
281
- class:file-input-error={!!error}
282
- disabled={fieldDisabled}
283
- onchange={onFileChange}
284
- />
285
- {/if}
286
- <!-- read-only file value is shown as the avatar above -->
95
+ <FileField {field} bind:record {parentRecord} {error} {readonly} {labelRow} {errorRow} />
287
96
  {:else if field.type === 'select'}
288
- {#if readonly}
289
- <input
290
- type="text"
291
- id={field.attribute}
292
- class="input input-bordered w-full"
293
- value={selectOptions.find((o) => o.value === String(saved))?.label ?? displayValue}
294
- disabled
295
- />
296
- {:else}
297
- <Select
298
- id={field.attribute}
299
- name={field.attribute}
300
- bind:value={record[field.attribute] as string}
301
- options={selectOptions}
302
- search={field.search}
303
- placeholder={field.placeholder}
304
- disabled={fieldDisabled}
305
- {error}
306
- />
307
- {/if}
97
+ <SelectField {field} bind:record {parentRecord} {error} {readonly} {labelRow} {errorRow} />
308
98
  {:else if field.type === 'datetime'}
309
- {#if readonly}
310
- <input
311
- type="text"
312
- id={field.attribute}
313
- class="input input-bordered w-full"
314
- value={formattedValue}
315
- disabled
316
- />
317
- {:else}
318
- <input
319
- type="hidden"
320
- {name}
321
- value={String(record[field.attribute] ?? '')}
322
- disabled={fieldDisabled}
323
- />
324
- <div class="flex gap-2">
325
- <div class="relative flex-1" style="anchor-name:{dateAnchorName}">
326
- <input
327
- type="text"
328
- id={field.attribute}
329
- placeholder={field.placeholder ?? 'dd/mm/aaaa'}
330
- value={dateTextValue}
331
- oninput={onDateTextInput}
332
- onblur={onDateTextBlur}
333
- autocomplete="off"
334
- disabled={fieldDisabled}
335
- class="input input-bordered w-full pr-9"
336
- class:input-error={!!error}
337
- />
338
- <button
339
- type="button"
340
- popovertarget={datePopId}
341
- aria-label={strings.chooseDate}
342
- disabled={fieldDisabled}
343
- class="absolute inset-y-0 right-0 flex items-center px-2.5 text-base-content/50 hover:text-base-content disabled:pointer-events-none disabled:opacity-40"
344
- >
345
- <svg
346
- class="size-4"
347
- xmlns="http://www.w3.org/2000/svg"
348
- viewBox="0 0 24 24"
349
- fill="none"
350
- stroke="currentColor"
351
- stroke-width="2"
352
- stroke-linecap="round"
353
- stroke-linejoin="round"
354
- >
355
- <rect x="3" y="4" width="18" height="18" rx="2" />
356
- <path d="M16 2v4M8 2v4M3 10h18" />
357
- </svg>
358
- </button>
359
- </div>
360
- <input
361
- type="time"
362
- class="input input-bordered w-32 shrink-0"
363
- value={timePart}
364
- disabled={fieldDisabled || !record[field.attribute]}
365
- onchange={(e) => setDateTime(datePart, (e.currentTarget as HTMLInputElement).value)}
366
- />
367
- </div>
368
- <div
369
- popover="auto"
370
- id={datePopId}
371
- bind:this={datePopoverEl}
372
- style="position-anchor:{dateAnchorName}; position-try-fallbacks:flip-block;"
373
- class="dropdown mt-1 rounded-box border border-base-content/10 bg-base-100 p-2 shadow-lg"
374
- >
375
- <calendar-date class="cally" bind:this={calendarDateEl} value={datePart}>
376
- <svg
377
- aria-label={strings.previous}
378
- class="fill-current size-4"
379
- {...{ slot: 'previous' }}
380
- xmlns="http://www.w3.org/2000/svg"
381
- viewBox="0 0 24 24"><path fill="currentColor" d="M15.75 19.5 8.25 12l7.5-7.5" /></svg
382
- >
383
- <svg
384
- aria-label={strings.next}
385
- class="fill-current size-4"
386
- {...{ slot: 'next' }}
387
- xmlns="http://www.w3.org/2000/svg"
388
- viewBox="0 0 24 24"><path fill="currentColor" d="m8.25 4.5 7.5 7.5-7.5 7.5" /></svg
389
- >
390
- <calendar-month></calendar-month>
391
- </calendar-date>
392
- </div>
393
- {/if}
99
+ <DatetimeField {field} bind:record {parentRecord} {error} {readonly} {labelRow} {errorRow} />
394
100
  {:else if field.type === 'textarea'}
395
- {#if readonly}
396
- <textarea
397
- id={field.attribute}
398
- rows={field.rows}
399
- class="textarea textarea-bordered bg-base-100 w-full"
400
- value={formattedValue}
401
- disabled
402
- ></textarea>
403
- {:else}
404
- <textarea
405
- id={field.attribute}
406
- {name}
407
- rows={field.rows}
408
- placeholder={field.placeholder ?? ''}
409
- bind:value={record[field.attribute]}
410
- class="textarea textarea-bordered bg-base-100 w-full"
411
- class:textarea-error={!!error}
412
- disabled={fieldDisabled}
413
- ></textarea>
414
- {/if}
101
+ <TextareaField {field} bind:record {parentRecord} {error} {readonly} {labelRow} {errorRow} />
415
102
  {:else if field.type === 'embedded'}
103
+ {@render labelRow()}
416
104
  <EmbeddedField {field} bind:record {readonly} />
105
+ {@render errorRow()}
417
106
  {:else if field.type === 'multiselect'}
418
- {#if readonly}
419
- <input
420
- type="text"
421
- id={field.attribute}
422
- class="input input-bordered w-full"
423
- value={(Array.isArray(saved) ? saved : [])
424
- .map((v) => selectOptions.find((o) => o.value === String(v))?.label ?? String(v))
425
- .join(', ')}
426
- disabled
427
- />
428
- {:else}
429
- <MultiSelect
430
- id={field.attribute}
431
- name={field.attribute}
432
- bind:value={record[field.attribute] as string[]}
433
- options={selectOptions}
434
- search={field.search}
435
- placeholder={field.placeholder}
436
- disabled={fieldDisabled}
437
- {error}
438
- />
439
- {/if}
107
+ <MultiSelectField
108
+ {field}
109
+ bind:record
110
+ {parentRecord}
111
+ {error}
112
+ {readonly}
113
+ {labelRow}
114
+ {errorRow}
115
+ />
440
116
  {:else if field.type === 'tree'}
441
- {#if readonly}
442
- <input
443
- type="text"
444
- id={field.attribute}
445
- class="input input-bordered w-full"
446
- value={(Array.isArray(saved) ? saved : [])
447
- .map((v) => selectOptions.find((o) => o.value === String(v))?.label ?? String(v))
448
- .join(', ')}
449
- disabled
450
- />
451
- {:else}
452
- <Tree
453
- name={field.attribute}
454
- bind:value={record[field.attribute] as string[]}
455
- options={selectOptions}
456
- disabled={fieldDisabled}
457
- defaultExpanded={field.defaultExpanded ?? true}
458
- />
459
- {/if}
460
- {:else if readonly}
461
- {#if field.formatter}
462
- <div
463
- id={field.attribute}
464
- class="input input-bordered w-full bg-base-200 border-base-200 text-base-content/40 shadow-none cursor-not-allowed"
465
- >
466
- <!-- eslint-disable-next-line svelte/no-at-html-tags -->
467
- {@html formattedValue}
468
- </div>
469
- {:else}
470
- <input
471
- type={field.type ?? 'text'}
472
- id={field.attribute}
473
- class="input input-bordered w-full"
474
- value={formattedValue}
475
- disabled
476
- />
477
- {/if}
117
+ <TreeField {field} bind:record {parentRecord} {readonly} {labelRow} {errorRow} />
478
118
  {:else}
479
- <input
480
- type={field.type ?? 'text'}
481
- id={field.attribute}
482
- {name}
483
- placeholder={field.placeholder ?? ''}
484
- bind:value={record[field.attribute]}
485
- autocomplete={field.autocomplete}
486
- step={field.type === 'number' ? 'any' : undefined}
487
- class="input input-bordered w-full"
488
- class:input-error={!!error}
489
- disabled={fieldDisabled}
490
- />
491
- {/if}
492
-
493
- {#if error}
494
- <span class="text-error text-xs">{error}</span>
119
+ <DefaultField {field} bind:record {parentRecord} {error} {readonly} {labelRow} {errorRow} />
495
120
  {/if}
496
121
  </div>
497
122
  {/if}
@@ -1,8 +1,12 @@
1
- import type { FieldDefinition } from '../../types/crud.js';
1
+ import type { FieldDefinition } from '../../types/crud/fields.js';
2
2
  declare function $$render<T extends object = Record<string, unknown>>(): {
3
3
  props: {
4
4
  field: FieldDefinition<T>;
5
5
  record?: Record<string, unknown>;
6
+ /** Set only when this Field renders a sub-field of an `embedded` field
7
+ * — the outer form's own record, passed through to `hidden`/`disabled`/
8
+ * `required`/`dependentOptions` as their second argument. */
9
+ parentRecord?: Record<string, unknown>;
6
10
  error?: string;
7
11
  readonly?: boolean;
8
12
  class?: string;