runeforge 0.0.27 → 0.0.28
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.
- package/README.md +25 -0
- package/dist/components/crud/Field.svelte +38 -9
- package/dist/components/crud/Field.svelte.d.ts +1 -0
- package/dist/components/crud/utils/grouping.d.ts +2 -1
- package/dist/components/crud/utils/grouping.js +34 -12
- package/dist/components/crud/utils/resolution.js +3 -1
- package/dist/components/crud/views/Create.svelte +20 -4
- package/dist/components/crud/views/Read.svelte +20 -4
- package/dist/components/crud/views/Update.svelte +20 -4
- package/dist/components/form/MultiSelect.svelte +1 -1
- package/dist/components/form/Select.svelte +1 -1
- package/dist/components/form/Tree.svelte +5 -2
- package/dist/components/form/Tree.svelte.d.ts +1 -0
- package/dist/components/form/TreeNode.svelte +4 -2
- package/dist/components/form/TreeNode.svelte.d.ts +1 -0
- package/dist/components/table/ColumnFilter.svelte +1 -1
- package/dist/types/attribute.d.ts +6 -0
- package/dist/types/crud.d.ts +4 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -34,6 +34,7 @@ A SvelteKit toolkit that forges forms, tables, actions, and CRUD workflows from
|
|
|
34
34
|
- [Validation](#validation)
|
|
35
35
|
- [Conditional fields](#conditional-fields)
|
|
36
36
|
- [Field grouping](#field-grouping)
|
|
37
|
+
- [Field rows](#field-rows)
|
|
37
38
|
- [Default values](#default-values)
|
|
38
39
|
- [Select options](#select-options)
|
|
39
40
|
- [Multiselect fields](#multiselect-fields)
|
|
@@ -159,6 +160,7 @@ Responsive overrides work too:
|
|
|
159
160
|
| `--runeforge-crud-title-size` | `1.875rem` | `<h1>` inside the `Header` component |
|
|
160
161
|
| `--runeforge-breadcrumb-font-size` | `0.875rem` | Breadcrumb label text size |
|
|
161
162
|
| `--runeforge-breadcrumb-icon-size` | `1rem` | Breadcrumb icon width and height |
|
|
163
|
+
| `--runeforge-tree-max-height` | `24rem` | Max height of a `tree` field before it scrolls internally |
|
|
162
164
|
|
|
163
165
|
Modal sizing (see [Shared Components](#shared-components)) is set per-instance via props rather than a CSS variable.
|
|
164
166
|
|
|
@@ -362,6 +364,8 @@ Every entry in an `InterfaceMetadata<T>` object is an `AttributeMetadata` — a
|
|
|
362
364
|
| `disabled` | `(record) => boolean` | all | Conditionally disables the input — see [Conditional fields](#conditional-fields) |
|
|
363
365
|
| `hidden` | `boolean \| (record) => boolean` | all | Conditionally removes the field from the form entirely — not rendered, not validated, not submitted — see [Conditional fields](#conditional-fields) |
|
|
364
366
|
| `groupedAs` | `string` | all | Visually groups fields under a titled section — see [Field grouping](#field-grouping) |
|
|
367
|
+
| `row` | `string` | all | Renders fields sharing the same value side by side (desktop) / stacked (mobile) — see [Field rows](#field-rows) |
|
|
368
|
+
| `defaultExpanded` | `boolean` | `tree` | Whether parent nodes start expanded. Defaults to `true` |
|
|
365
369
|
| `options` | `SelectOption[] \| (data) => SelectOption[]` | `select`, `multiselect`, `tree` | Static or computed option list — see [Select options](#select-options). `tree` options additionally accept `parentValue` — see [Tree fields](#tree-fields) |
|
|
366
370
|
| `dependentOptions` | `(data, record) => SelectOption[]` | `select`, `multiselect`, `tree` | Options derived from other fields' current values |
|
|
367
371
|
| `search` | `(query) => Promise<SelectOption[]>` | `select`, `multiselect` | Server-side option search as the user types |
|
|
@@ -485,6 +489,25 @@ sku: {
|
|
|
485
489
|
},
|
|
486
490
|
```
|
|
487
491
|
|
|
492
|
+
### Field rows
|
|
493
|
+
|
|
494
|
+
Fields sharing the same `row` string render side by side on desktop and stacked on mobile, instead of each taking a full line. It's meant for small, related fields — a date range, a min/max pair — where a flat vertical stack wastes space.
|
|
495
|
+
|
|
496
|
+
```ts
|
|
497
|
+
createdFrom: {
|
|
498
|
+
label: 'Created from',
|
|
499
|
+
type: AttributeType.datetime,
|
|
500
|
+
row: 'createdRange',
|
|
501
|
+
},
|
|
502
|
+
createdTo: {
|
|
503
|
+
label: 'Created to',
|
|
504
|
+
type: AttributeType.datetime,
|
|
505
|
+
row: 'createdRange',
|
|
506
|
+
},
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
A `row` only merges fields that are also in the same `groupedAs` fieldset (or both ungrouped) — it never pulls fields together across two different fieldsets. If one of the fields in a row is conditionally [hidden](#conditional-fields), the remaining field(s) simply expand to fill the row instead of leaving a gap.
|
|
510
|
+
|
|
488
511
|
### Default values
|
|
489
512
|
|
|
490
513
|
`default` can be a plain value or a function of the page `data` object, evaluated once when the create form's fields are resolved — handy for defaulting a select to something derived from prefetched data.
|
|
@@ -599,6 +622,8 @@ categories: {
|
|
|
599
622
|
|
|
600
623
|
The stored value is a `string[]` of selected node values, submitted the same way as `multiselect` — a single hidden field holding a JSON array, parsed back out server-side with `JSON.parse`. `dependentOptions` and `hidden` work the same as any other field type.
|
|
601
624
|
|
|
625
|
+
By default every parent node renders expanded; set `defaultExpanded: false` to start with the whole tree collapsed instead (the user can still expand any branch — this only sets the initial state). The field itself is capped at `--runeforge-tree-max-height` (default `24rem`, see [CSS variables](#css-variables)) and scrolls internally once its content grows past that.
|
|
626
|
+
|
|
602
627
|
### Embedded fields (sub-documents)
|
|
603
628
|
|
|
604
629
|
`AttributeType.embedded` models a one-to-many list of sub-records — line items, adjustments, contacts, anything you'd otherwise store as an array of objects — entirely within one form field. It renders as a list with an "+ Add" button; each item is added/edited through a modal built from the `fields` sub-schema, and removed with a single click. The whole list is serialized to JSON and submitted as a single hidden form field.
|
|
@@ -21,13 +21,19 @@
|
|
|
21
21
|
record = $bindable({} as Record<string, unknown>),
|
|
22
22
|
error = '',
|
|
23
23
|
readonly = false,
|
|
24
|
+
class: className = '',
|
|
24
25
|
}: {
|
|
25
26
|
field: FieldDefinition<T>;
|
|
26
27
|
record?: Record<string, unknown>;
|
|
27
28
|
error?: string;
|
|
28
29
|
readonly?: boolean;
|
|
30
|
+
class?: string;
|
|
29
31
|
} = $props();
|
|
30
32
|
|
|
33
|
+
const datePopId = $props.id();
|
|
34
|
+
const dateAnchorName = `--date-anchor-${datePopId}`;
|
|
35
|
+
let datePopoverEl: HTMLElement | undefined = $state();
|
|
36
|
+
|
|
31
37
|
const name = $derived(readonly ? undefined : field.attribute);
|
|
32
38
|
const labelText = $derived(fieldLabel(field));
|
|
33
39
|
let filePreview = $state<string | null>(null);
|
|
@@ -73,7 +79,7 @@
|
|
|
73
79
|
</script>
|
|
74
80
|
|
|
75
81
|
{#if !fieldHidden}
|
|
76
|
-
<div class="flex flex-col gap-1">
|
|
82
|
+
<div class="flex flex-col gap-1 {className}">
|
|
77
83
|
{#if field.type === 'file'}
|
|
78
84
|
<div class="flex justify-center">
|
|
79
85
|
<Avatar src={preview} text={avatarInitials} alt={labelText} class="w-20 rounded-full" textClass="text-xl" />
|
|
@@ -141,15 +147,37 @@
|
|
|
141
147
|
/>
|
|
142
148
|
{:else}
|
|
143
149
|
<input type="hidden" {name} value={String(record[field.attribute] ?? '')} disabled={fieldDisabled} />
|
|
144
|
-
<
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
150
|
+
<button
|
|
151
|
+
type="button"
|
|
152
|
+
class="input input-bordered w-full text-left font-normal"
|
|
153
|
+
class:opacity-40={!record[field.attribute]}
|
|
154
|
+
disabled={fieldDisabled}
|
|
155
|
+
popovertarget={datePopId}
|
|
156
|
+
style="anchor-name:{dateAnchorName}"
|
|
157
|
+
>
|
|
158
|
+
{record[field.attribute] ? displayValue : (field.placeholder ?? '')}
|
|
159
|
+
</button>
|
|
160
|
+
<div
|
|
161
|
+
popover="auto"
|
|
162
|
+
id={datePopId}
|
|
163
|
+
bind:this={datePopoverEl}
|
|
164
|
+
style="position-anchor:{dateAnchorName}; position-try-fallbacks:flip-block;"
|
|
165
|
+
class="dropdown mt-1 rounded-box border border-base-content/10 bg-base-100 p-2 shadow-lg"
|
|
148
166
|
>
|
|
149
|
-
<
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
167
|
+
<calendar-date
|
|
168
|
+
class="cally"
|
|
169
|
+
value={String(record[field.attribute] ?? '')}
|
|
170
|
+
onchange={(e: Event) => {
|
|
171
|
+
if (fieldDisabled) return;
|
|
172
|
+
record[field.attribute] = (e.currentTarget as HTMLElement & { value: string }).value;
|
|
173
|
+
datePopoverEl?.hidePopover();
|
|
174
|
+
}}
|
|
175
|
+
>
|
|
176
|
+
<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>
|
|
177
|
+
<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>
|
|
178
|
+
<calendar-month></calendar-month>
|
|
179
|
+
</calendar-date>
|
|
180
|
+
</div>
|
|
153
181
|
{/if}
|
|
154
182
|
{:else if field.type === 'textarea'}
|
|
155
183
|
{#if readonly}
|
|
@@ -202,6 +230,7 @@
|
|
|
202
230
|
bind:value={record[field.attribute] as string[]}
|
|
203
231
|
options={selectOptions}
|
|
204
232
|
disabled={fieldDisabled}
|
|
233
|
+
defaultExpanded={field.defaultExpanded ?? true}
|
|
205
234
|
/>
|
|
206
235
|
{/if}
|
|
207
236
|
{:else if readonly}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { FieldDefinition } from '../../../types/crud.js';
|
|
2
|
+
export type FieldRow<T extends object = Record<string, unknown>> = FieldDefinition<T>[];
|
|
2
3
|
export type FieldGroup<T extends object = Record<string, unknown>> = {
|
|
3
4
|
title?: string;
|
|
4
|
-
|
|
5
|
+
rows: FieldRow<T>[];
|
|
5
6
|
};
|
|
6
7
|
export declare function groupFields<T extends object = Record<string, unknown>>(fields: FieldDefinition<T>[]): FieldGroup<T>[];
|
|
@@ -1,23 +1,45 @@
|
|
|
1
1
|
// Partitions a flat field list into visual groups for create/update/read
|
|
2
|
-
// views, based on `groupedAs
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
2
|
+
// views, based on `groupedAs`, and each group's fields into rows, based on
|
|
3
|
+
// `row`. A field without `groupedAs` renders as its own single-field group
|
|
4
|
+
// (unchanged from today's flat layout) unless it shares a `row` with another
|
|
5
|
+
// ungrouped field, in which case they share that anonymous group. Fields
|
|
6
|
+
// sharing a `groupedAs` are collected together at the position of their first
|
|
7
|
+
// occurrence, preserving overall order; within that group, fields sharing a
|
|
8
|
+
// `row` are collected into the same row. A `row` never merges fields across
|
|
9
|
+
// two different `groupedAs` groups.
|
|
6
10
|
export function groupFields(fields) {
|
|
7
11
|
const groups = [];
|
|
8
12
|
const byTitle = new Map();
|
|
13
|
+
const byUngroupedRow = new Map();
|
|
9
14
|
for (const field of fields) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
15
|
+
let group;
|
|
16
|
+
if (field.groupedAs) {
|
|
17
|
+
group = byTitle.get(field.groupedAs);
|
|
18
|
+
if (!group) {
|
|
19
|
+
group = { title: field.groupedAs, rows: [] };
|
|
20
|
+
byTitle.set(field.groupedAs, group);
|
|
21
|
+
groups.push(group);
|
|
22
|
+
}
|
|
13
23
|
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
group
|
|
17
|
-
|
|
24
|
+
else if (field.row) {
|
|
25
|
+
group = byUngroupedRow.get(field.row);
|
|
26
|
+
if (!group) {
|
|
27
|
+
group = { rows: [] };
|
|
28
|
+
byUngroupedRow.set(field.row, group);
|
|
29
|
+
groups.push(group);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
group = { rows: [] };
|
|
18
34
|
groups.push(group);
|
|
19
35
|
}
|
|
20
|
-
group.
|
|
36
|
+
const existingRow = field.row ? group.rows.find((r) => r[0]?.row === field.row) : undefined;
|
|
37
|
+
if (existingRow) {
|
|
38
|
+
existingRow.push(field);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
group.rows.push([field]);
|
|
42
|
+
}
|
|
21
43
|
}
|
|
22
44
|
return groups;
|
|
23
45
|
}
|
|
@@ -56,6 +56,8 @@ export function buildFieldDefinitions(meta, data, excludedFlag, excluded) {
|
|
|
56
56
|
fields: m.fields
|
|
57
57
|
? buildFieldDefinitions(m.fields, data, excludedFlag, new Set())
|
|
58
58
|
: undefined,
|
|
59
|
-
itemLabel: m.itemLabel
|
|
59
|
+
itemLabel: m.itemLabel,
|
|
60
|
+
defaultExpanded: m.defaultExpanded,
|
|
61
|
+
row: m.row
|
|
60
62
|
}));
|
|
61
63
|
}
|
|
@@ -113,14 +113,30 @@
|
|
|
113
113
|
<fieldset class="fieldset border border-base-300 rounded-box p-4">
|
|
114
114
|
<legend class="fieldset-legend px-2">{group.title}</legend>
|
|
115
115
|
<div class="flex flex-col gap-4">
|
|
116
|
-
{#each group.
|
|
117
|
-
|
|
116
|
+
{#each group.rows as row (row.map((f) => f.attribute).join('|'))}
|
|
117
|
+
{#if row.length > 1}
|
|
118
|
+
<div class="flex flex-col gap-4 md:flex-row">
|
|
119
|
+
{#each row as field (field.attribute)}
|
|
120
|
+
<Field {field} bind:record error={fieldErrors[field.attribute] ?? ''} class="md:min-w-0 md:flex-1" />
|
|
121
|
+
{/each}
|
|
122
|
+
</div>
|
|
123
|
+
{:else}
|
|
124
|
+
<Field field={row[0]} bind:record error={fieldErrors[row[0].attribute] ?? ''} />
|
|
125
|
+
{/if}
|
|
118
126
|
{/each}
|
|
119
127
|
</div>
|
|
120
128
|
</fieldset>
|
|
121
129
|
{:else}
|
|
122
|
-
{#each group.
|
|
123
|
-
|
|
130
|
+
{#each group.rows as row (row.map((f) => f.attribute).join('|'))}
|
|
131
|
+
{#if row.length > 1}
|
|
132
|
+
<div class="flex flex-col gap-4 md:flex-row">
|
|
133
|
+
{#each row as field (field.attribute)}
|
|
134
|
+
<Field {field} bind:record error={fieldErrors[field.attribute] ?? ''} class="md:min-w-0 md:flex-1" />
|
|
135
|
+
{/each}
|
|
136
|
+
</div>
|
|
137
|
+
{:else}
|
|
138
|
+
<Field field={row[0]} bind:record error={fieldErrors[row[0].attribute] ?? ''} />
|
|
139
|
+
{/if}
|
|
124
140
|
{/each}
|
|
125
141
|
{/if}
|
|
126
142
|
{/each}
|
|
@@ -81,14 +81,30 @@
|
|
|
81
81
|
<fieldset class="fieldset border border-base-300 rounded-box p-4">
|
|
82
82
|
<legend class="fieldset-legend px-2">{group.title}</legend>
|
|
83
83
|
<div class="flex flex-col gap-4">
|
|
84
|
-
{#each group.
|
|
85
|
-
|
|
84
|
+
{#each group.rows as row (row.map((f) => f.attribute).join('|'))}
|
|
85
|
+
{#if row.length > 1}
|
|
86
|
+
<div class="flex flex-col gap-4 md:flex-row">
|
|
87
|
+
{#each row as field (field.attribute)}
|
|
88
|
+
<Field {field} {record} readonly class="md:min-w-0 md:flex-1" />
|
|
89
|
+
{/each}
|
|
90
|
+
</div>
|
|
91
|
+
{:else}
|
|
92
|
+
<Field field={row[0]} {record} readonly />
|
|
93
|
+
{/if}
|
|
86
94
|
{/each}
|
|
87
95
|
</div>
|
|
88
96
|
</fieldset>
|
|
89
97
|
{:else}
|
|
90
|
-
{#each group.
|
|
91
|
-
|
|
98
|
+
{#each group.rows as row (row.map((f) => f.attribute).join('|'))}
|
|
99
|
+
{#if row.length > 1}
|
|
100
|
+
<div class="flex flex-col gap-4 md:flex-row">
|
|
101
|
+
{#each row as field (field.attribute)}
|
|
102
|
+
<Field {field} {record} readonly class="md:min-w-0 md:flex-1" />
|
|
103
|
+
{/each}
|
|
104
|
+
</div>
|
|
105
|
+
{:else}
|
|
106
|
+
<Field field={row[0]} {record} readonly />
|
|
107
|
+
{/if}
|
|
92
108
|
{/each}
|
|
93
109
|
{/if}
|
|
94
110
|
{/each}
|
|
@@ -129,14 +129,30 @@
|
|
|
129
129
|
<fieldset class="fieldset border border-base-300 rounded-box p-4">
|
|
130
130
|
<legend class="fieldset-legend px-2">{group.title}</legend>
|
|
131
131
|
<div class="flex flex-col gap-4">
|
|
132
|
-
{#each group.
|
|
133
|
-
|
|
132
|
+
{#each group.rows as row (row.map((f) => f.attribute).join('|'))}
|
|
133
|
+
{#if row.length > 1}
|
|
134
|
+
<div class="flex flex-col gap-4 md:flex-row">
|
|
135
|
+
{#each row as field (field.attribute)}
|
|
136
|
+
<Field {field} bind:record error={fieldErrors[field.attribute] ?? ''} class="md:min-w-0 md:flex-1" />
|
|
137
|
+
{/each}
|
|
138
|
+
</div>
|
|
139
|
+
{:else}
|
|
140
|
+
<Field field={row[0]} bind:record error={fieldErrors[row[0].attribute] ?? ''} />
|
|
141
|
+
{/if}
|
|
134
142
|
{/each}
|
|
135
143
|
</div>
|
|
136
144
|
</fieldset>
|
|
137
145
|
{:else}
|
|
138
|
-
{#each group.
|
|
139
|
-
|
|
146
|
+
{#each group.rows as row (row.map((f) => f.attribute).join('|'))}
|
|
147
|
+
{#if row.length > 1}
|
|
148
|
+
<div class="flex flex-col gap-4 md:flex-row">
|
|
149
|
+
{#each row as field (field.attribute)}
|
|
150
|
+
<Field {field} bind:record error={fieldErrors[field.attribute] ?? ''} class="md:min-w-0 md:flex-1" />
|
|
151
|
+
{/each}
|
|
152
|
+
</div>
|
|
153
|
+
{:else}
|
|
154
|
+
<Field field={row[0]} bind:record error={fieldErrors[row[0].attribute] ?? ''} />
|
|
155
|
+
{/if}
|
|
140
156
|
{/each}
|
|
141
157
|
{/if}
|
|
142
158
|
{/each}
|
|
@@ -111,7 +111,7 @@
|
|
|
111
111
|
popover="auto"
|
|
112
112
|
id={popId}
|
|
113
113
|
bind:this={popoverEl}
|
|
114
|
-
style="position-anchor:{anchorName}; width:anchor-size(width);"
|
|
114
|
+
style="position-anchor:{anchorName}; width:anchor-size(width); position-try-fallbacks:flip-block;"
|
|
115
115
|
class="dropdown mt-1 rounded-box border border-base-content/10 bg-base-100 shadow-lg"
|
|
116
116
|
ontoggle={onToggle}
|
|
117
117
|
>
|
|
@@ -115,7 +115,7 @@
|
|
|
115
115
|
popover="auto"
|
|
116
116
|
id={popId}
|
|
117
117
|
bind:this={popoverEl}
|
|
118
|
-
style="position-anchor:{anchorName}; width:anchor-size(width);"
|
|
118
|
+
style="position-anchor:{anchorName}; width:anchor-size(width); position-try-fallbacks:flip-block;"
|
|
119
119
|
class="dropdown mt-1 rounded-box border border-base-content/10 bg-base-100 shadow-lg"
|
|
120
120
|
ontoggle={onToggle}
|
|
121
121
|
>
|
|
@@ -9,12 +9,14 @@
|
|
|
9
9
|
options = [],
|
|
10
10
|
disabled = false,
|
|
11
11
|
columns = 1,
|
|
12
|
+
defaultExpanded = true,
|
|
12
13
|
}: {
|
|
13
14
|
name?: string;
|
|
14
15
|
value?: string[];
|
|
15
16
|
options?: SelectOption[];
|
|
16
17
|
disabled?: boolean;
|
|
17
18
|
columns?: number;
|
|
19
|
+
defaultExpanded?: boolean;
|
|
18
20
|
} = $props();
|
|
19
21
|
|
|
20
22
|
const childrenByParent = $derived(buildChildrenByParent(options));
|
|
@@ -33,7 +35,8 @@
|
|
|
33
35
|
</script>
|
|
34
36
|
|
|
35
37
|
<div
|
|
36
|
-
class="rounded-box border border-base-300 p-2"
|
|
38
|
+
class="overflow-y-auto rounded-box border border-base-300 p-2"
|
|
39
|
+
style="max-height: var(--runeforge-tree-max-height, 24rem)"
|
|
37
40
|
class:opacity-50={disabled}
|
|
38
41
|
class:pointer-events-none={disabled}
|
|
39
42
|
>
|
|
@@ -45,7 +48,7 @@
|
|
|
45
48
|
{/if}
|
|
46
49
|
<div class:leaf-columns={columns > 1} style={`--columns: ${columns}`}>
|
|
47
50
|
{#each roots as root (root.value)}
|
|
48
|
-
<TreeNode node={root} {childrenByParent} {selected} onToggle={toggle} {disabled} depth={0} {columns} />
|
|
51
|
+
<TreeNode node={root} {childrenByParent} {selected} onToggle={toggle} {disabled} depth={0} {columns} {defaultExpanded} />
|
|
49
52
|
{/each}
|
|
50
53
|
</div>
|
|
51
54
|
</div>
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
disabled,
|
|
11
11
|
depth,
|
|
12
12
|
columns,
|
|
13
|
+
defaultExpanded,
|
|
13
14
|
}: {
|
|
14
15
|
node: SelectOption;
|
|
15
16
|
childrenByParent: Map<string | null, SelectOption[]>;
|
|
@@ -18,9 +19,10 @@
|
|
|
18
19
|
disabled: boolean;
|
|
19
20
|
depth: number;
|
|
20
21
|
columns: number;
|
|
22
|
+
defaultExpanded: boolean;
|
|
21
23
|
} = $props();
|
|
22
24
|
|
|
23
|
-
let expanded = $state(
|
|
25
|
+
let expanded = $state(defaultExpanded);
|
|
24
26
|
|
|
25
27
|
const children = $derived(childrenByParent.get(node.value) ?? []);
|
|
26
28
|
const hasChildren = $derived(children.length > 0);
|
|
@@ -58,7 +60,7 @@
|
|
|
58
60
|
|
|
59
61
|
{#if expanded && hasChildren}
|
|
60
62
|
{#each children as child (child.value)}
|
|
61
|
-
<TreeNode node={child} {childrenByParent} {selected} {onToggle} {disabled} depth={depth + 1} {columns} />
|
|
63
|
+
<TreeNode node={child} {childrenByParent} {selected} {onToggle} {disabled} depth={depth + 1} {columns} {defaultExpanded} />
|
|
62
64
|
{/each}
|
|
63
65
|
{/if}
|
|
64
66
|
</div>
|
|
@@ -92,7 +92,7 @@
|
|
|
92
92
|
<div
|
|
93
93
|
popover="auto"
|
|
94
94
|
id={popId}
|
|
95
|
-
style="position-anchor:{anchor}"
|
|
95
|
+
style="position-anchor:{anchor}; position-try-fallbacks:flip-block;"
|
|
96
96
|
class="dropdown dropdown-end rounded-box border border-base-content/10 bg-base-100 p-3 shadow-lg"
|
|
97
97
|
class:w-56={!isDatetime}
|
|
98
98
|
>
|
|
@@ -78,4 +78,10 @@ export type AttributeMetadata = {
|
|
|
78
78
|
fields?: InterfaceMetadata<any>;
|
|
79
79
|
/** Embedded fields only: short label for an item in the list. */
|
|
80
80
|
itemLabel?: EmbeddedItemLabelResolver;
|
|
81
|
+
/** Tree fields only: whether parent nodes start expanded. Defaults to `true`. */
|
|
82
|
+
defaultExpanded?: boolean;
|
|
83
|
+
/** Fields sharing the same `row` string render side by side on desktop and
|
|
84
|
+
* stacked on mobile — see the Field rows section. Only merges fields that
|
|
85
|
+
* are also in the same `groupedAs` bucket (or both ungrouped). */
|
|
86
|
+
row?: string;
|
|
81
87
|
};
|
package/dist/types/crud.d.ts
CHANGED
|
@@ -46,6 +46,10 @@ export interface FieldDefinition<T extends object = Record<string, unknown>> {
|
|
|
46
46
|
fields?: FieldDefinition<Record<string, unknown>>[];
|
|
47
47
|
/** Embedded fields only: short label for an item in the list. */
|
|
48
48
|
itemLabel?: (item: Record<string, unknown>) => string;
|
|
49
|
+
/** Tree fields only: whether parent nodes start expanded. Defaults to `true`. */
|
|
50
|
+
defaultExpanded?: boolean;
|
|
51
|
+
/** Fields sharing the same `row` string render side by side. */
|
|
52
|
+
row?: string;
|
|
49
53
|
}
|
|
50
54
|
export interface ActionConfiguration<T extends object = Record<string, unknown>> {
|
|
51
55
|
enabled?: boolean;
|