runeforge 0.0.23 → 0.0.24

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.
@@ -6,15 +6,40 @@
6
6
  title = '',
7
7
  onClose,
8
8
  children,
9
+ class: additionalClass,
10
+ width,
11
+ maxWidth,
12
+ height,
13
+ maxHeight,
9
14
  }: {
10
15
  title?: string;
11
16
  onClose?: () => void;
12
17
  children: Snippet;
18
+ /** Extra classes merged onto the modal box, e.g. Tailwind size utilities
19
+ * like `max-w-4xl` or `w-11/12`. */
20
+ class?: string;
21
+ /** Explicit size overrides (any valid CSS length, e.g. '600px', '90vw').
22
+ * Applied as inline styles, so they take priority over `class` utilities. */
23
+ width?: string;
24
+ maxWidth?: string;
25
+ height?: string;
26
+ maxHeight?: string;
13
27
  } = $props();
28
+
29
+ const boxStyle = $derived(
30
+ [
31
+ width ? `width:${width}` : '',
32
+ maxWidth ? `max-width:${maxWidth}` : '',
33
+ height ? `height:${height}` : '',
34
+ maxHeight ? `max-height:${maxHeight}` : '',
35
+ ]
36
+ .filter(Boolean)
37
+ .join(';')
38
+ );
14
39
  </script>
15
40
 
16
41
  <dialog class="modal" open>
17
- <div class="modal-box">
42
+ <div class={['modal-box', additionalClass]} style={boxStyle}>
18
43
  <div class="flex items-center justify-between gap-4">
19
44
  <h3 class="text-lg font-bold">{title}</h3>
20
45
  {#if onClose}
@@ -3,6 +3,15 @@ type $$ComponentProps = {
3
3
  title?: string;
4
4
  onClose?: () => void;
5
5
  children: Snippet;
6
+ /** Extra classes merged onto the modal box, e.g. Tailwind size utilities
7
+ * like `max-w-4xl` or `w-11/12`. */
8
+ class?: string;
9
+ /** Explicit size overrides (any valid CSS length, e.g. '600px', '90vw').
10
+ * Applied as inline styles, so they take priority over `class` utilities. */
11
+ width?: string;
12
+ maxWidth?: string;
13
+ height?: string;
14
+ maxHeight?: string;
6
15
  };
7
16
  declare const Modal: import("svelte").Component<$$ComponentProps, {}, "">;
8
17
  type Modal = ReturnType<typeof Modal>;
@@ -0,0 +1,144 @@
1
+ <script lang="ts" generics="T extends object = Record<string, unknown>">
2
+ import Field from './Field.svelte';
3
+ import Button from '../form/Button.svelte';
4
+ import Modal from '../Modal.svelte';
5
+ import { emptyRecord, seedField, defaultItemLabel } from './utils/embedded.js';
6
+ import { fieldLabel } from './utils/misc.js';
7
+ import { validateAll } from './utils/validation.js';
8
+ import type { FieldDefinition } from '../../types/crud.js';
9
+ import { getStrings } from '../../i18n/context.js';
10
+
11
+ const strings = getStrings();
12
+
13
+ let {
14
+ field,
15
+ record = $bindable({} as Record<string, unknown>),
16
+ readonly = false
17
+ }: {
18
+ field: FieldDefinition<T>;
19
+ record?: Record<string, unknown>;
20
+ readonly?: boolean;
21
+ } = $props();
22
+
23
+ const subFields = $derived(field.fields ?? []);
24
+ const items = $derived((record[field.attribute] as Record<string, unknown>[] | undefined) ?? []);
25
+
26
+ let modalOpen = $state(false);
27
+ // null while adding a new item; the item's index while editing an existing
28
+ // one, so saveItem knows whether to append or replace in place.
29
+ let editingIndex = $state<number | null>(null);
30
+ let draft = $state<Record<string, unknown>>({});
31
+ let draftErrors = $state<Record<string, string>>({});
32
+
33
+ function openCreateModal() {
34
+ editingIndex = null;
35
+ draft = emptyRecord(subFields);
36
+ draftErrors = {};
37
+ modalOpen = true;
38
+ }
39
+
40
+ function openEditModal(index: number) {
41
+ const item = items[index];
42
+ editingIndex = index;
43
+ draft = Object.fromEntries(
44
+ subFields.map((f) => [f.attribute, seedField(f, f.seed ? f.seed(item) : item[f.attribute])])
45
+ );
46
+ draftErrors = {};
47
+ modalOpen = true;
48
+ }
49
+
50
+ function closeModal() {
51
+ modalOpen = false;
52
+ }
53
+
54
+ function coerce(f: FieldDefinition, raw: unknown): unknown {
55
+ if (f.type === 'number') return raw === '' || raw == null ? null : Number(raw);
56
+ return raw;
57
+ }
58
+
59
+ function saveItem() {
60
+ const fd = new FormData();
61
+ for (const f of subFields) fd.set(f.attribute, String(draft[f.attribute] ?? ''));
62
+ const errs = validateAll(subFields, fd, strings);
63
+ if (Object.keys(errs).length > 0) {
64
+ draftErrors = errs;
65
+ return;
66
+ }
67
+ const item = Object.fromEntries(
68
+ subFields.map((f) => [f.attribute, coerce(f, draft[f.attribute])])
69
+ );
70
+ record[field.attribute] =
71
+ editingIndex == null
72
+ ? [...items, item]
73
+ : items.map((existing, i) => (i === editingIndex ? item : existing));
74
+ modalOpen = false;
75
+ }
76
+
77
+ function removeItem(index: number) {
78
+ record[field.attribute] = items.filter((_, i) => i !== index);
79
+ }
80
+
81
+ function itemLabel(item: Record<string, unknown>): string {
82
+ return field.itemLabel ? field.itemLabel(item) : defaultItemLabel(subFields, item);
83
+ }
84
+ </script>
85
+
86
+ <div class="flex flex-col gap-2">
87
+ {#if !readonly}
88
+ <input type="hidden" name={field.attribute} value={JSON.stringify(items)} />
89
+ {/if}
90
+
91
+ {#if items.length === 0}
92
+ <p class="text-sm text-base-content/50">{strings.noItems}</p>
93
+ {:else}
94
+ <ul class="flex flex-col gap-1">
95
+ {#each items as item, i (i)}
96
+ <li
97
+ class="flex items-center justify-between gap-2 rounded-box border border-base-300 px-3 py-2 text-sm"
98
+ >
99
+ {#if readonly}
100
+ <span>{itemLabel(item)}</span>
101
+ {:else}
102
+ <button
103
+ type="button"
104
+ class="flex-1 text-left hover:underline"
105
+ onclick={() => openEditModal(i)}
106
+ >
107
+ {itemLabel(item)}
108
+ </button>
109
+ <Button
110
+ variant="ghost"
111
+ class="btn-xs btn-circle"
112
+ aria-label={strings.remove}
113
+ onclick={() => removeItem(i)}
114
+ >
115
+
116
+ </Button>
117
+ {/if}
118
+ </li>
119
+ {/each}
120
+ </ul>
121
+ {/if}
122
+
123
+ {#if !readonly}
124
+ <Button variant="outline" class="btn-sm self-start" onclick={openCreateModal}>
125
+ + {strings.add}
126
+ </Button>
127
+ {/if}
128
+ </div>
129
+
130
+ {#if modalOpen}
131
+ <Modal title={fieldLabel(field)} onClose={closeModal}>
132
+ <div class="flex flex-col gap-4">
133
+ {#each subFields as f (f.attribute)}
134
+ <Field field={f} bind:record={draft} error={draftErrors[f.attribute] ?? ''} />
135
+ {/each}
136
+ <div class="flex flex-col-reverse gap-2 pt-2 sm:flex-row sm:justify-end">
137
+ <Button variant="ghost" onclick={closeModal}>{strings.cancel}</Button>
138
+ <Button variant="primary" onclick={saveItem}>
139
+ {editingIndex == null ? strings.add : strings.edit}
140
+ </Button>
141
+ </div>
142
+ </div>
143
+ </Modal>
144
+ {/if}
@@ -0,0 +1,29 @@
1
+ import type { FieldDefinition } from '../../types/crud.js';
2
+ declare function $$render<T extends object = Record<string, unknown>>(): {
3
+ props: {
4
+ field: FieldDefinition<T>;
5
+ record?: Record<string, unknown>;
6
+ readonly?: boolean;
7
+ };
8
+ exports: {};
9
+ bindings: "record";
10
+ slots: {};
11
+ events: {};
12
+ };
13
+ declare class __sveltets_Render<T extends object = Record<string, unknown>> {
14
+ props(): ReturnType<typeof $$render<T>>['props'];
15
+ events(): ReturnType<typeof $$render<T>>['events'];
16
+ slots(): ReturnType<typeof $$render<T>>['slots'];
17
+ bindings(): "record";
18
+ exports(): {};
19
+ }
20
+ interface $$IsomorphicComponent {
21
+ new <T extends object = Record<string, unknown>>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
22
+ $$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
23
+ } & ReturnType<__sveltets_Render<T>['exports']>;
24
+ <T extends object = Record<string, unknown>>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
25
+ z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
26
+ }
27
+ declare const EmbeddedField: $$IsomorphicComponent;
28
+ type EmbeddedField<T extends object = Record<string, unknown>> = InstanceType<typeof EmbeddedField<T>>;
29
+ export default EmbeddedField;
@@ -3,6 +3,7 @@
3
3
  import Avatar from '../Avatar.svelte';
4
4
  import Label from '../form/Label.svelte';
5
5
  import Select from '../form/Select.svelte';
6
+ import EmbeddedField from './EmbeddedField.svelte';
6
7
  import { fieldLabel, initials } from './utils/misc.js';
7
8
  import type { FieldDefinition } from '../../types/crud.js';
8
9
  import { getStrings } from '../../i18n/context.js';
@@ -147,6 +148,8 @@
147
148
  disabled={fieldDisabled}
148
149
  ></textarea>
149
150
  {/if}
151
+ {:else if field.type === 'embedded'}
152
+ <EmbeddedField {field} bind:record {readonly} />
150
153
  {:else if readonly}
151
154
  <input
152
155
  type={field.type ?? 'text'}
@@ -0,0 +1,4 @@
1
+ import type { FieldDefinition } from '../../../types/crud.js';
2
+ export declare function seedField<T extends object = Record<string, unknown>>(f: FieldDefinition<T>, raw: unknown): unknown;
3
+ export declare function emptyRecord<T extends object = Record<string, unknown>>(fields: FieldDefinition<T>[]): Record<string, unknown>;
4
+ export declare function defaultItemLabel<T extends object = Record<string, unknown>>(fields: FieldDefinition<T>[], item: Record<string, unknown>): string;
@@ -0,0 +1,40 @@
1
+ import { fieldLabel } from './misc.js';
2
+ // Shared by emptyRecord/Update.svelte's seedFromInstance/EmbeddedField's edit
3
+ // mode: converts one raw stored value into its editable draft form, using the
4
+ // same convention everywhere — booleans and embedded arrays keep their real
5
+ // type (Field.svelte's checkbox/list binds them directly), everything else
6
+ // becomes a string since Field.svelte's other inputs bind as strings.
7
+ export function seedField(f, raw) {
8
+ if (f.type === 'boolean')
9
+ return !!raw;
10
+ if (f.type === 'file')
11
+ return raw ?? null;
12
+ if (f.type === 'embedded')
13
+ return Array.isArray(raw) ? raw : [];
14
+ return String(raw ?? '');
15
+ }
16
+ // Shared by Create.svelte and EmbeddedField.svelte: both need an empty draft
17
+ // record seeded with each field's default, keyed the same way regardless of
18
+ // whether it ends up serialized as a top-level form or as one item inside an
19
+ // embedded list.
20
+ export function emptyRecord(fields) {
21
+ return Object.fromEntries(fields.map((f) => [f.attribute, seedField(f, f.default)]));
22
+ }
23
+ // Default summary shown per item in an embedded list when the field has no
24
+ // `itemLabel`: joins each sub-field's resolved display value so the list is
25
+ // at least readable out of the box.
26
+ export function defaultItemLabel(fields, item) {
27
+ return fields
28
+ .map((f) => {
29
+ const raw = item[f.attribute];
30
+ if (raw == null || raw === '')
31
+ return null;
32
+ if (f.type === 'select')
33
+ return f.options?.find((o) => o.value === String(raw))?.label ?? String(raw);
34
+ if (f.type === 'boolean')
35
+ return raw ? fieldLabel(f) : null;
36
+ return String(raw);
37
+ })
38
+ .filter((v) => !!v)
39
+ .join(' · ');
40
+ }
@@ -51,6 +51,10 @@ export function buildFieldDefinitions(meta, data, excludedFlag, excluded) {
51
51
  integer: m.integer,
52
52
  minLength: m.minLength,
53
53
  maxLength: m.maxLength,
54
- pattern: m.pattern
54
+ pattern: m.pattern,
55
+ fields: m.fields
56
+ ? buildFieldDefinitions(m.fields, data, excludedFlag, new Set())
57
+ : undefined,
58
+ itemLabel: m.itemLabel
55
59
  }));
56
60
  }
@@ -6,6 +6,19 @@ import { fieldLabel } from './misc.js';
6
6
  export function validateAll(fields, formData, strings) {
7
7
  const errors = {};
8
8
  for (const field of fields) {
9
+ if (field.type === 'embedded') {
10
+ let items;
11
+ try {
12
+ items = JSON.parse(String(formData.get(field.attribute) ?? '[]'));
13
+ }
14
+ catch {
15
+ items = [];
16
+ }
17
+ if (field.required && (!Array.isArray(items) || items.length === 0)) {
18
+ errors[field.attribute] = strings.required(fieldLabel(field));
19
+ }
20
+ continue;
21
+ }
9
22
  const val = String(formData.get(field.attribute) ?? '').trim();
10
23
  if (field.required && !val) {
11
24
  errors[field.attribute] = strings.required(fieldLabel(field));
@@ -8,6 +8,7 @@
8
8
  import { defaultIconSet } from '../../../icons/sets/default.js';
9
9
  import { validateAll } from '../utils/validation.js';
10
10
  import { groupFields } from '../utils/grouping.js';
11
+ import { emptyRecord } from '../utils/embedded.js';
11
12
  import type { ActionConfiguration, FieldDefinition } from '../../../types/crud.js';
12
13
  import { getStrings } from '../../../i18n/context.js';
13
14
 
@@ -41,18 +42,7 @@
41
42
  let internalError = $state('');
42
43
  let continueCreating = $state(false);
43
44
 
44
- function emptyRecord(): Record<string, unknown> {
45
- return Object.fromEntries(
46
- fields.map((f) => [
47
- f.attribute,
48
- f.type === 'boolean' ? !!f.default
49
- : f.type === 'file' ? (f.default ?? null)
50
- : String(f.default ?? ''),
51
- ])
52
- );
53
- }
54
-
55
- let record = $state<Record<string, unknown>>(untrack(() => emptyRecord()));
45
+ let record = $state<Record<string, unknown>>(untrack(() => emptyRecord(fields)));
56
46
 
57
47
  const groups = $derived(groupFields(fields));
58
48
  const hasFileField = $derived(fields.some((f) => f.type === 'file'));
@@ -104,7 +94,7 @@
104
94
  if (result.type === 'success' || result.type === 'redirect') {
105
95
  await update({ reset: false });
106
96
  if (continueCreating) {
107
- record = emptyRecord();
97
+ record = emptyRecord(fields);
108
98
  fieldErrors = {};
109
99
  internalError = '';
110
100
  } else {
@@ -8,6 +8,7 @@
8
8
  import { defaultIconSet } from '../../../icons/sets/default.js';
9
9
  import { validateAll } from '../utils/validation.js';
10
10
  import { groupFields } from '../utils/grouping.js';
11
+ import { seedField } from '../utils/embedded.js';
11
12
  import type { ActionConfiguration, FieldDefinition } from '../../../types/crud.js';
12
13
  import { getStrings } from '../../../i18n/context.js';
13
14
 
@@ -47,10 +48,8 @@
47
48
  function seedFromInstance(inst: Record<string, unknown>): Record<string, unknown> {
48
49
  const seeded: Record<string, unknown> = { ...inst };
49
50
  for (const f of fields) {
50
- if (f.type !== 'boolean' && f.type !== 'file') {
51
- const raw = f.seed ? f.seed(inst) : inst[f.attribute];
52
- seeded[f.attribute] = String(raw ?? '');
53
- }
51
+ const raw = f.seed ? f.seed(inst) : inst[f.attribute];
52
+ seeded[f.attribute] = seedField(f, raw);
54
53
  }
55
54
  return seeded;
56
55
  }
package/dist/i18n/en.js CHANGED
@@ -24,6 +24,9 @@ export const en = {
24
24
  saveAndContinue: 'Save and continue',
25
25
  cancel: 'Cancel',
26
26
  back: 'Back',
27
+ add: 'Add',
28
+ remove: 'Remove',
29
+ noItems: 'No items added',
27
30
  confirm: 'Confirm',
28
31
  deleteConfirm: (count, actionLabel) => `Are you sure you want to ${String(actionLabel).toLowerCase()} ${count} item${count === 1 ? '' : 's'}?`,
29
32
  required: (field) => `${field} is required`,
package/dist/i18n/es.js CHANGED
@@ -24,6 +24,9 @@ export const es = {
24
24
  saveAndContinue: 'Guardar y continuar',
25
25
  cancel: 'Cancelar',
26
26
  back: 'Volver',
27
+ add: 'Agregar',
28
+ remove: 'Quitar',
29
+ noItems: 'Sin elementos agregados',
27
30
  confirm: 'Confirmar',
28
31
  deleteConfirm: (count, actionLabel) => `¿Seguro que querés ${String(actionLabel).toLowerCase()} ${count} elemento${count === 1 ? '' : 's'}?`,
29
32
  required: (field) => `${field} es requerido`,
@@ -24,6 +24,9 @@ export interface RuneforgeStrings {
24
24
  saveAndContinue: string;
25
25
  cancel: string;
26
26
  back: string;
27
+ add: string;
28
+ remove: string;
29
+ noItems: string;
27
30
  confirm: string;
28
31
  deleteConfirm: (count: number, actionLabel: string) => string;
29
32
  required: (field: string) => string;
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export type { BreadcrumbItem } from './types/breadcrumb.js';
2
2
  export { AttributeType } from './types/attribute.js';
3
- export type { AttributeMetadata, InterfaceMetadata, SelectOption, OptionsResolver, FormatterResolver } from './types/attribute.js';
3
+ export type { AttributeMetadata, InterfaceMetadata, SelectOption, OptionsResolver, FormatterResolver, EmbeddedItemLabelResolver } from './types/attribute.js';
4
4
  export type { CellProps, CellComponent, CellFormatter, SortDirection, IndexedRow, DistinctEntry, PaginatedEnvelope, ServerPagination, FilterSnapshot, TableQuery } from './types/table.js';
5
5
  export type { ColumnDefinition, FieldDefinition, ActionConfiguration, CustomAction, CustomBulkAction, RowAction, SearchConfiguration } from './types/crud.js';
6
6
  export type { RuneforgeConfig } from './config/context.js';
@@ -32,6 +32,7 @@ export { SortState, FilterState, snapshotFilter } from './components/table/state
32
32
  export { cellRenderedText, isSortable, isFilterable, compare, distinctEntries } from './components/table/utils.js';
33
33
  export { default as GenericCRUD } from './components/crud/GenericCRUD.svelte';
34
34
  export { default as Field } from './components/crud/Field.svelte';
35
+ export { default as EmbeddedField } from './components/crud/EmbeddedField.svelte';
35
36
  export { default as Header } from './components/common/Header.svelte';
36
37
  export { default as AvatarCell } from './components/crud/columns/Avatar.svelte';
37
38
  export { default as IconCell } from './components/crud/columns/Icon.svelte';
@@ -47,3 +48,4 @@ export { formatBoolean, formatDatetime, formatTruncateTextUpTo, formatInstance }
47
48
  export { groupFields } from './components/crud/utils/grouping.js';
48
49
  export type { FieldGroup } from './components/crud/utils/grouping.js';
49
50
  export { validateAll } from './components/crud/utils/validation.js';
51
+ export { emptyRecord, defaultItemLabel } from './components/crud/utils/embedded.js';
package/dist/index.js CHANGED
@@ -29,6 +29,7 @@ export { cellRenderedText, isSortable, isFilterable, compare, distinctEntries }
29
29
  // CRUD components
30
30
  export { default as GenericCRUD } from './components/crud/GenericCRUD.svelte';
31
31
  export { default as Field } from './components/crud/Field.svelte';
32
+ export { default as EmbeddedField } from './components/crud/EmbeddedField.svelte';
32
33
  export { default as Header } from './components/common/Header.svelte';
33
34
  export { default as AvatarCell } from './components/crud/columns/Avatar.svelte';
34
35
  export { default as IconCell } from './components/crud/columns/Icon.svelte';
@@ -44,3 +45,4 @@ export { fieldLabel, initials } from './components/crud/utils/misc.js';
44
45
  export { formatBoolean, formatDatetime, formatTruncateTextUpTo, formatInstance } from './components/crud/utils/formatters.js';
45
46
  export { groupFields } from './components/crud/utils/grouping.js';
46
47
  export { validateAll } from './components/crud/utils/validation.js';
48
+ export { emptyRecord, defaultItemLabel } from './components/crud/utils/embedded.js';
@@ -1,6 +1,6 @@
1
1
  import type { FullAutoFill } from 'svelte/elements';
2
2
  import type { CellComponent, CellFormatter } from './table.js';
3
- export type AttributeType = 'text' | 'email' | 'password' | 'number' | 'boolean' | 'textarea' | 'file' | 'select' | 'datetime';
3
+ export type AttributeType = 'text' | 'email' | 'password' | 'number' | 'boolean' | 'textarea' | 'file' | 'select' | 'datetime' | 'embedded';
4
4
  export declare const AttributeType: {
5
5
  readonly text: "text";
6
6
  readonly email: "email";
@@ -11,6 +11,7 @@ export declare const AttributeType: {
11
11
  readonly file: "file";
12
12
  readonly select: "select";
13
13
  readonly datetime: "datetime";
14
+ readonly embedded: "embedded";
14
15
  };
15
16
  export type InterfaceMetadata<T> = Partial<Record<keyof T, AttributeMetadata>>;
16
17
  export type SelectOption = {
@@ -23,6 +24,9 @@ export type DependentOptionsResolver = (data: any, record: Record<string, unknow
23
24
  export type SearchResolver = (query: string) => Promise<SelectOption[]>;
24
25
  export type DisabledResolver = (record: Record<string, unknown>) => boolean;
25
26
  export type SeedResolver = (instance: any) => unknown;
27
+ /** Embedded fields only: renders a short summary for one item in the list.
28
+ * Falls back to a dash-joined summary of the item's sub-field values. */
29
+ export type EmbeddedItemLabelResolver = (item: Record<string, unknown>) => string;
26
30
  export type AttributeMetadata = {
27
31
  label?: string;
28
32
  type?: AttributeType;
@@ -56,4 +60,8 @@ export type AttributeMetadata = {
56
60
  minLength?: number;
57
61
  maxLength?: number;
58
62
  pattern?: string;
63
+ /** Embedded fields only: schema for each item added through the "+" modal. */
64
+ fields?: InterfaceMetadata<any>;
65
+ /** Embedded fields only: short label for an item in the list. */
66
+ itemLabel?: EmbeddedItemLabelResolver;
59
67
  };
@@ -8,4 +8,5 @@ export const AttributeType = {
8
8
  file: 'file',
9
9
  select: 'select',
10
10
  datetime: 'datetime',
11
+ embedded: 'embedded',
11
12
  };
@@ -39,6 +39,11 @@ export interface FieldDefinition<T extends object = Record<string, unknown>> {
39
39
  minLength?: number;
40
40
  maxLength?: number;
41
41
  pattern?: string;
42
+ /** Embedded fields only: sub-field definitions for each item, built from
43
+ * the metadata's `fields`. */
44
+ fields?: FieldDefinition<Record<string, unknown>>[];
45
+ /** Embedded fields only: short label for an item in the list. */
46
+ itemLabel?: (item: Record<string, unknown>) => string;
42
47
  }
43
48
  export interface ActionConfiguration<T extends object = Record<string, unknown>> {
44
49
  enabled?: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "runeforge",
3
- "version": "0.0.23",
3
+ "version": "0.0.24",
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",