runeforge 0.0.57 → 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.
- package/README.md +51 -0
- package/dist/components/crud/EmbeddedField.svelte +33 -5
- package/dist/components/crud/EmbeddedField.svelte.d.ts +6 -1
- package/dist/components/crud/Field.svelte +76 -423
- package/dist/components/crud/Field.svelte.d.ts +5 -1
- package/dist/components/crud/GenericCRUD.svelte +4 -4
- package/dist/components/crud/GenericCRUD.svelte.d.ts +4 -1
- package/dist/components/crud/SearchInput.svelte +1 -1
- package/dist/components/crud/SearchInput.svelte.d.ts +1 -1
- package/dist/components/crud/fields/BooleanField.svelte +38 -0
- package/dist/components/crud/fields/BooleanField.svelte.d.ts +33 -0
- package/dist/components/crud/fields/DatetimeField.svelte +250 -0
- package/dist/components/crud/fields/DatetimeField.svelte.d.ts +34 -0
- package/dist/components/crud/fields/DefaultField.svelte +67 -0
- package/dist/components/crud/fields/DefaultField.svelte.d.ts +34 -0
- package/dist/components/crud/fields/FileField.svelte +70 -0
- package/dist/components/crud/fields/FileField.svelte.d.ts +34 -0
- package/dist/components/crud/fields/MultiSelectField.svelte +70 -0
- package/dist/components/crud/fields/MultiSelectField.svelte.d.ts +34 -0
- package/dist/components/crud/fields/SelectField.svelte +67 -0
- package/dist/components/crud/fields/SelectField.svelte.d.ts +34 -0
- package/dist/components/crud/fields/TextareaField.svelte +55 -0
- package/dist/components/crud/fields/TextareaField.svelte.d.ts +34 -0
- package/dist/components/crud/fields/TreeField.svelte +63 -0
- package/dist/components/crud/fields/TreeField.svelte.d.ts +33 -0
- package/dist/components/crud/utils/embedded.d.ts +1 -1
- package/dist/components/crud/utils/grouping.d.ts +1 -1
- package/dist/components/crud/utils/resolution.d.ts +1 -1
- package/dist/components/crud/utils/resolution.js +6 -2
- package/dist/components/crud/utils/validation.d.ts +1 -1
- package/dist/components/crud/utils/validation.js +10 -0
- package/dist/components/crud/views/Create.svelte +37 -30
- package/dist/components/crud/views/Create.svelte.d.ts +2 -1
- package/dist/components/crud/views/Read.svelte +19 -12
- package/dist/components/crud/views/Read.svelte.d.ts +2 -1
- package/dist/components/crud/views/Update.svelte +28 -21
- package/dist/components/crud/views/Update.svelte.d.ts +2 -1
- package/dist/components/crud/views/list/List.svelte +3 -3
- package/dist/components/crud/views/list/List.svelte.d.ts +3 -1
- package/dist/components/crud/views/list/Modals.svelte +1 -1
- package/dist/components/crud/views/list/Modals.svelte.d.ts +1 -1
- package/dist/components/crud/views/list/Table.svelte +3 -6
- package/dist/components/crud/views/list/Table.svelte.d.ts +3 -1
- package/dist/components/crud/views/list/Toolbar.svelte +3 -4
- package/dist/components/crud/views/list/Toolbar.svelte.d.ts +3 -1
- package/dist/components/table/ColumnFilter.svelte +1 -1
- package/dist/components/table/ColumnFilter.svelte.d.ts +1 -1
- package/dist/components/table/PaginatedTable.svelte +1 -1
- package/dist/components/table/PaginatedTable.svelte.d.ts +1 -1
- package/dist/components/table/TableBody.svelte +1 -1
- package/dist/components/table/TableBody.svelte.d.ts +1 -1
- package/dist/components/table/TableHeader.svelte +1 -1
- package/dist/components/table/TableHeader.svelte.d.ts +1 -1
- package/dist/components/table/export.d.ts +1 -1
- package/dist/components/table/state.svelte.d.ts +1 -1
- package/dist/components/table/utils.d.ts +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/types/attribute.d.ts +56 -4
- package/dist/types/{crud.d.ts → crud/actions.d.ts} +0 -123
- package/dist/types/crud/columns.d.ts +23 -0
- package/dist/types/crud/columns.js +1 -0
- package/dist/types/crud/fields.d.ts +48 -0
- package/dist/types/crud/fields.js +1 -0
- package/dist/types/crud/list-config.d.ts +23 -0
- package/dist/types/crud/list-config.js +1 -0
- package/dist/types/crud/reorder.d.ts +45 -0
- package/dist/types/crud/reorder.js +1 -0
- package/package.json +1 -1
- /package/dist/types/{crud.js → crud/actions.js} +0 -0
package/README.md
CHANGED
|
@@ -164,9 +164,12 @@ Responsive overrides work too:
|
|
|
164
164
|
| `--runeforge-breadcrumb-font-size` | `0.875rem` | Breadcrumb label text size |
|
|
165
165
|
| `--runeforge-breadcrumb-icon-size` | `1rem` | Breadcrumb icon width and height |
|
|
166
166
|
| `--runeforge-tree-max-height` | `24rem` | Max height of a `tree` field before it scrolls internally |
|
|
167
|
+
| `--runeforge-sticky-header-top` | `0` | Offset of the sticky title/breadcrumbs/alert block in Create, Update, and Read views — raise it if your app already has its own sticky top bar taking up space |
|
|
167
168
|
|
|
168
169
|
Modal sizing (see [Shared Components](#shared-components)) is set per-instance via props rather than a CSS variable.
|
|
169
170
|
|
|
171
|
+
The title, breadcrumbs and any error/success alert at the top of the Create, Update and Read views stay pinned (`position: sticky`) to the top of the nearest scrolling ancestor, so they — and a validation error that just appeared — stay visible while a long form scrolls underneath. Set `--runeforge-sticky-header-top` if that ancestor already has its own fixed/sticky bar above this block.
|
|
172
|
+
|
|
170
173
|
---
|
|
171
174
|
|
|
172
175
|
## Configuration
|
|
@@ -364,6 +367,8 @@ Every entry in an `InterfaceMetadata<T>` object is an `AttributeMetadata` — a
|
|
|
364
367
|
| `integer` | `boolean` | `number` | Rejects non-whole numbers |
|
|
365
368
|
| `minLength` / `maxLength` | `number` | text-like | Character-count validation |
|
|
366
369
|
| `pattern` | `string` | text-like | Regex the value must match (`new RegExp(pattern)`) |
|
|
370
|
+
| `validate` | `(value, record) => string \| undefined` | all | Custom validation, including cross-field rules — see [Validation](#validation) |
|
|
371
|
+
| `actions` | `FieldButtonAction[]` | all | Extra buttons rendered next to the field's label — see [Field actions](#field-actions) |
|
|
367
372
|
| `disabled` | `(record) => boolean` | all | Conditionally disables the input — see [Conditional fields](#conditional-fields) |
|
|
368
373
|
| `hidden` | `boolean \| (record) => boolean` | all | Conditionally removes the field from the form entirely — not rendered, not validated, not submitted — see [Conditional fields](#conditional-fields) |
|
|
369
374
|
| `groupedAs` | `string` | all | Visually groups fields under a titled section — see [Field grouping](#field-grouping) |
|
|
@@ -428,6 +433,27 @@ quantity: {
|
|
|
428
433
|
|
|
429
434
|
The label's required marker and the submit-time check both re-evaluate the same way `disabled` does — see [Conditional fields](#conditional-fields).
|
|
430
435
|
|
|
436
|
+
For anything the built-in rules above don't cover — including a rule that depends on another field, not just this one — pass `validate`. It runs after this field's built-in rules pass (and is skipped if one of them already failed), and receives both the field's own value and the full draft record, so the same hook covers a lone-field rule and a cross-field one alike:
|
|
437
|
+
|
|
438
|
+
```ts
|
|
439
|
+
submissionDate: {
|
|
440
|
+
label: 'Submission date',
|
|
441
|
+
type: AttributeType.datetime,
|
|
442
|
+
},
|
|
443
|
+
extensionDate: {
|
|
444
|
+
label: 'Extension date',
|
|
445
|
+
type: AttributeType.datetime,
|
|
446
|
+
validate: (value, record) =>
|
|
447
|
+
typeof value === 'string' &&
|
|
448
|
+
typeof record.submissionDate === 'string' &&
|
|
449
|
+
value <= record.submissionDate
|
|
450
|
+
? 'Extension date must be later than the submission date'
|
|
451
|
+
: undefined,
|
|
452
|
+
},
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
There's deliberately no separate, form-wide validation hook — every error belongs to the field whose value is wrong, even when the rule reads a sibling's value to decide that, so a per-field `validate` is all that's needed.
|
|
456
|
+
|
|
431
457
|
> [!TIP]
|
|
432
458
|
> Client-side validation is a UX nicety, not a security boundary — always re-validate in your form actions.
|
|
433
459
|
|
|
@@ -476,6 +502,31 @@ cardExpiry: {
|
|
|
476
502
|
|
|
477
503
|
Switching `paymentMethod` between `card` and `cash` swaps which fields are present, live, in the same create/edit view — no separate step or modal needed to collect the payment-specific details.
|
|
478
504
|
|
|
505
|
+
### Field actions
|
|
506
|
+
|
|
507
|
+
`actions` renders one or more buttons next to a field's label, for a client-side transform that doesn't belong to any field of its own — nothing is submitted, nothing hits the server, the button just runs synchronously and updates the form. Useful for things like normalizing free text as the user types it:
|
|
508
|
+
|
|
509
|
+
```ts
|
|
510
|
+
import Edit from './icons/Edit.svelte';
|
|
511
|
+
|
|
512
|
+
description: {
|
|
513
|
+
label: 'Description',
|
|
514
|
+
type: AttributeType.textarea,
|
|
515
|
+
actions: [
|
|
516
|
+
{
|
|
517
|
+
label: 'Capitalize',
|
|
518
|
+
icon: Edit,
|
|
519
|
+
run: (value, record, setField) => {
|
|
520
|
+
const str = String(value ?? '');
|
|
521
|
+
setField('description', str ? str[0].toUpperCase() + str.slice(1).toLowerCase() : str);
|
|
522
|
+
},
|
|
523
|
+
},
|
|
524
|
+
],
|
|
525
|
+
},
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
`run` receives the field's current value, the form's full draft record, and a `setField(attribute, value)` setter — which can just as well target a sibling attribute instead of the field the button sits next to. `icon` is a Svelte component, same convention as [custom row actions](#custom-row-actions). `condition` hides the button in certain conditions, re-evaluated live like `disabled`. Actions never render in the read-only view.
|
|
529
|
+
|
|
479
530
|
### Field grouping
|
|
480
531
|
|
|
481
532
|
Fields sharing the same `groupedAs` string render together inside a titled `fieldset`, at the position of the group's first field. Fields without `groupedAs` keep the original flat layout.
|
|
@@ -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
|
};
|