@marianmeres/stuic 3.181.0 → 3.182.0
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 +1 -1
- package/dist/components/FieldsBuilder/FieldsBuilder.svelte +101 -150
- package/dist/components/FieldsBuilder/FieldsBuilder.svelte.d.ts +10 -1
- package/dist/components/FieldsBuilder/README.md +82 -3
- package/dist/components/FieldsBuilder/_internal/ColumnsEditor.svelte +624 -0
- package/dist/components/FieldsBuilder/_internal/ColumnsEditor.svelte.d.ts +41 -0
- package/dist/components/FieldsBuilder/_internal/ExtrasEditor.svelte +174 -0
- package/dist/components/FieldsBuilder/_internal/ExtrasEditor.svelte.d.ts +20 -0
- package/dist/components/FieldsBuilder/_internal/column-meta.d.ts +27 -0
- package/dist/components/FieldsBuilder/_internal/column-meta.js +15 -0
- package/dist/components/FieldsBuilder/i18n-sk.js +17 -0
- package/dist/components/FieldsBuilder/i18n.d.ts +17 -0
- package/dist/components/FieldsBuilder/i18n.js +17 -0
- package/dist/components/FieldsBuilder/index.css +20 -0
- package/dist/components/FieldsBuilder/index.d.ts +2 -2
- package/dist/components/FieldsBuilder/types.d.ts +39 -0
- package/dist/components/FieldsBuilder/utils.d.ts +36 -1
- package/dist/components/FieldsBuilder/utils.js +180 -55
- package/dist/components/Input/FieldTable.svelte +907 -0
- package/dist/components/Input/FieldTable.svelte.d.ts +98 -0
- package/dist/components/Input/README.md +251 -0
- package/dist/components/Input/field-table-i18n-sk.d.ts +21 -0
- package/dist/components/Input/field-table-i18n-sk.js +41 -0
- package/dist/components/Input/field-table-i18n.d.ts +60 -0
- package/dist/components/Input/field-table-i18n.js +66 -0
- package/dist/components/Input/field-table-number.d.ts +48 -0
- package/dist/components/Input/field-table-number.js +86 -0
- package/dist/components/Input/index.css +597 -0
- package/dist/components/Input/index.d.ts +4 -0
- package/dist/components/Input/index.js +4 -0
- package/docs/domains/components.md +24 -18
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -175,7 +175,7 @@ AppShell, Accordion, Backdrop, Modal, ModalDialog, Drawer, Collapsible, Header,
|
|
|
175
175
|
|
|
176
176
|
### Forms & Inputs
|
|
177
177
|
|
|
178
|
-
FieldInput, FieldMoney, FieldDate, FieldDateRange, Calendar, FieldTextarea, FieldSelect, FieldCheckbox, FieldRadios, FieldFile, FieldAssets, FieldOptions, FieldKeyValues, FieldObject, FieldSwitch, FieldInputLocalized, FieldLikeButton, FieldPhoneNumber, FieldCountry, CronInput, Fieldset, LoginForm, LoginFormModal, RegisterForm, RegisterFormModal, LoginOrRegisterForm, LoginOrRegisterFormModal, EmailVerifyForm, OtpInput
|
|
178
|
+
FieldInput, FieldMoney, FieldDate, FieldDateRange, Calendar, FieldTextarea, FieldSelect, FieldCheckbox, FieldRadios, FieldFile, FieldAssets, FieldOptions, FieldKeyValues, FieldTable, FieldObject, FieldSwitch, FieldInputLocalized, FieldLikeButton, FieldPhoneNumber, FieldCountry, CronInput, Fieldset, LoginForm, LoginFormModal, RegisterForm, RegisterFormModal, LoginOrRegisterForm, LoginOrRegisterFormModal, EmailVerifyForm, OtpInput
|
|
179
179
|
|
|
180
180
|
### Buttons & Controls
|
|
181
181
|
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import type { TranslateFn } from "../../types.js";
|
|
8
8
|
import type { THC } from "../Thc/Thc.svelte";
|
|
9
9
|
import type { InputWrapClassProps } from "../Input/types.js";
|
|
10
|
-
import type { FieldDef, FieldTypeDef, LocalizedText } from "./types.js";
|
|
10
|
+
import type { FieldColumnDef, FieldDef, FieldTypeDef, LocalizedText } from "./types.js";
|
|
11
11
|
|
|
12
12
|
type SnippetWithId = Snippet<[{ id: string }]>;
|
|
13
13
|
|
|
@@ -98,6 +98,22 @@
|
|
|
98
98
|
field: FieldDef,
|
|
99
99
|
newType: string
|
|
100
100
|
) => void | false | Promise<void | false>;
|
|
101
|
+
/**
|
|
102
|
+
* Veto hook for removing a column that was present when `value` was
|
|
103
|
+
* (re)loaded — return `false` (or throw) to cancel. Not called for
|
|
104
|
+
* columns added in this session. Column removal is otherwise immediate
|
|
105
|
+
* (no mark-delete for columns); this hook is the safety net.
|
|
106
|
+
*/
|
|
107
|
+
onBeforeColumnDelete?: (
|
|
108
|
+
field: FieldDef,
|
|
109
|
+
column: FieldColumnDef
|
|
110
|
+
) => void | false | Promise<void | false>;
|
|
111
|
+
/** Veto hook for changing the type of a pre-existing column (same contract). */
|
|
112
|
+
onBeforeColumnTypeChange?: (
|
|
113
|
+
field: FieldDef,
|
|
114
|
+
column: FieldColumnDef,
|
|
115
|
+
newType: string
|
|
116
|
+
) => void | false | Promise<void | false>;
|
|
101
117
|
onChange?: (value: FieldDef[]) => void;
|
|
102
118
|
/** Renders the preview pane. Receives the current (visible) fields. */
|
|
103
119
|
preview?: Snippet<[{ fields: FieldDef[] }]>;
|
|
@@ -130,11 +146,16 @@
|
|
|
130
146
|
import { getId } from "../../utils/get-id.js";
|
|
131
147
|
import { twMerge } from "../../utils/tw-merge.js";
|
|
132
148
|
import InputWrap from "../Input/_internal/InputWrap.svelte";
|
|
149
|
+
import { loadedColumnMeta, type ColumnMeta } from "./_internal/column-meta.js";
|
|
150
|
+
import ColumnsEditor from "./_internal/ColumnsEditor.svelte";
|
|
151
|
+
import ExtrasEditor from "./_internal/ExtrasEditor.svelte";
|
|
133
152
|
import LocalizedTextInput from "./_internal/LocalizedTextInput.svelte";
|
|
134
153
|
import OptionsEditor from "./_internal/OptionsEditor.svelte";
|
|
135
154
|
import {
|
|
136
155
|
getLocalizedText,
|
|
137
156
|
isKeyReserved,
|
|
157
|
+
resolveColumnTypes,
|
|
158
|
+
seedExtraDefaults,
|
|
138
159
|
slugifyKey,
|
|
139
160
|
uniqueKey,
|
|
140
161
|
validateFieldDefs,
|
|
@@ -190,6 +211,8 @@
|
|
|
190
211
|
deleteMode = "mark",
|
|
191
212
|
onBeforeDelete,
|
|
192
213
|
onBeforeTypeChange,
|
|
214
|
+
onBeforeColumnDelete,
|
|
215
|
+
onBeforeColumnTypeChange,
|
|
193
216
|
onChange,
|
|
194
217
|
preview,
|
|
195
218
|
previewBreakpoint = 768,
|
|
@@ -223,6 +246,12 @@
|
|
|
223
246
|
emitted: boolean;
|
|
224
247
|
expanded: boolean;
|
|
225
248
|
advancedOpen: boolean;
|
|
249
|
+
/**
|
|
250
|
+
* Index-aligned with `def.columns`, kept in lockstep by `ColumnsEditor`.
|
|
251
|
+
* Owned here (not by the editor) because the row body unmounts while
|
|
252
|
+
* collapsed — see `_internal/column-meta.ts`.
|
|
253
|
+
*/
|
|
254
|
+
columnMeta: ColumnMeta[];
|
|
226
255
|
}
|
|
227
256
|
|
|
228
257
|
function cloneDef(d: FieldDef): FieldDef {
|
|
@@ -244,6 +273,7 @@
|
|
|
244
273
|
emitted: true,
|
|
245
274
|
expanded: false,
|
|
246
275
|
advancedOpen: false,
|
|
276
|
+
columnMeta: (def.columns ?? []).map(loadedColumnMeta),
|
|
247
277
|
};
|
|
248
278
|
});
|
|
249
279
|
}
|
|
@@ -254,6 +284,7 @@
|
|
|
254
284
|
let rowEls: Record<string, HTMLElement | undefined> = $state({});
|
|
255
285
|
let labelEditors: Record<string, LocalizedTextInput | undefined> = $state({});
|
|
256
286
|
let keyInputEls: Record<string, HTMLInputElement | undefined> = $state({});
|
|
287
|
+
let columnsEditors: Record<string, ColumnsEditor | undefined> = $state({});
|
|
257
288
|
let liveAnnouncement = $state("");
|
|
258
289
|
|
|
259
290
|
let rows: Row[] = $state(fromValue(value ?? []));
|
|
@@ -364,13 +395,9 @@
|
|
|
364
395
|
// add / delete / type change / extras
|
|
365
396
|
// ---------------------------------------------------------------------------
|
|
366
397
|
|
|
367
|
-
function
|
|
368
|
-
const
|
|
369
|
-
|
|
370
|
-
if (ex.default !== undefined && row.def.extras?.[ex.key] === undefined) {
|
|
371
|
-
row.def.extras = { ...(row.def.extras ?? {}), [ex.key]: ex.default };
|
|
372
|
-
}
|
|
373
|
-
}
|
|
398
|
+
function seedRowExtras(row: Row) {
|
|
399
|
+
const seeded = seedExtraDefaults(row.def.extras, typeByName.get(row.def.type));
|
|
400
|
+
if (seeded !== row.def.extras) row.def.extras = seeded;
|
|
374
401
|
}
|
|
375
402
|
|
|
376
403
|
function addField() {
|
|
@@ -386,8 +413,9 @@
|
|
|
386
413
|
emitted: false,
|
|
387
414
|
expanded: true,
|
|
388
415
|
advancedOpen: false,
|
|
416
|
+
columnMeta: [],
|
|
389
417
|
};
|
|
390
|
-
|
|
418
|
+
seedRowExtras(row);
|
|
391
419
|
rows = [...rows, row];
|
|
392
420
|
// no syncToValue: a keyless row is not part of `value` yet
|
|
393
421
|
tick().then(() => labelEditors[row.rid]?.focus?.());
|
|
@@ -440,41 +468,12 @@
|
|
|
440
468
|
}
|
|
441
469
|
}
|
|
442
470
|
row.def.type = newType;
|
|
443
|
-
// `options`/`extras` of the previous type are deliberately
|
|
444
|
-
// silently drop data; switching back restores them
|
|
445
|
-
|
|
471
|
+
// `options`/`extras`/`columns` of the previous type are deliberately
|
|
472
|
+
// kept — never silently drop data; switching back restores them
|
|
473
|
+
seedRowExtras(row);
|
|
446
474
|
syncToValue();
|
|
447
475
|
}
|
|
448
476
|
|
|
449
|
-
// `undefined` REMOVES the key (and an emptied bag removes `extras` itself):
|
|
450
|
-
// "no value" must have exactly one representation downstream — a consumer
|
|
451
|
-
// reading `extras.unit` to decide whether to render a suffix should never
|
|
452
|
-
// have to special-case `""`, nor a `{}` that means nothing.
|
|
453
|
-
function setExtra(row: Row, key: string, value: unknown) {
|
|
454
|
-
const next = { ...(row.def.extras ?? {}) };
|
|
455
|
-
if (value === undefined) delete next[key];
|
|
456
|
-
else next[key] = value;
|
|
457
|
-
row.def.extras = Object.keys(next).length ? next : undefined;
|
|
458
|
-
syncToValue();
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
/** Display value of a string/select extra (a non-string is shown, not eaten). */
|
|
462
|
-
function extraText(row: Row, key: string): string {
|
|
463
|
-
const v = row.def.extras?.[key];
|
|
464
|
-
return v == null ? "" : String(v);
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
// while typing, the RAW value is stored (trimming here would fight the
|
|
468
|
-
// caret: a written-back trimmed value makes a trailing space untypable) —
|
|
469
|
-
// only the emptiness test is trimmed; `onchange` normalizes on commit
|
|
470
|
-
function onExtraStringInput(row: Row, key: string, raw: string) {
|
|
471
|
-
setExtra(row, key, raw.trim() ? raw : undefined);
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
function onExtraStringChange(row: Row, key: string, raw: string) {
|
|
475
|
-
setExtra(row, key, raw.trim() || undefined);
|
|
476
|
-
}
|
|
477
|
-
|
|
478
477
|
function typeChanged(row: Row): boolean {
|
|
479
478
|
return !!row.initialType && row.def.type !== row.initialType;
|
|
480
479
|
}
|
|
@@ -693,6 +692,8 @@
|
|
|
693
692
|
".fb-options .fb-option-value, .fb-options .fb-add-option-btn"
|
|
694
693
|
)
|
|
695
694
|
?.focus?.();
|
|
695
|
+
} else if (errs.columns) {
|
|
696
|
+
columnsEditors[row.rid]?.focusFirstInvalid(errs.columnErrors);
|
|
696
697
|
} else if (errs.extras) {
|
|
697
698
|
rowEls[row.rid]?.querySelector<HTMLElement>(".fb-extra-input")?.focus?.();
|
|
698
699
|
}
|
|
@@ -787,6 +788,7 @@
|
|
|
787
788
|
{@const showLabelError = !!(attempted && errs?.label)}
|
|
788
789
|
{@const showKeyError = !!(errs?.key && (attempted || row.keyEdited))}
|
|
789
790
|
{@const showOptionsError = !!(attempted && errs?.options)}
|
|
791
|
+
{@const showColumnsError = !!(attempted && errs?.columns)}
|
|
790
792
|
{@const showExtrasError = !!(attempted && errs?.extras)}
|
|
791
793
|
{@const canDrag =
|
|
792
794
|
!disabled && !row.deleted && !row.def.lock?.reorder && rows.length > 1}
|
|
@@ -861,7 +863,7 @@
|
|
|
861
863
|
{displayText(entry.label)}
|
|
862
864
|
</span>
|
|
863
865
|
{/if}
|
|
864
|
-
{#if (showLabelError || showKeyError || showOptionsError || showExtrasError) && !row.deleted}
|
|
866
|
+
{#if (showLabelError || showKeyError || showOptionsError || showColumnsError || showExtrasError) && !row.deleted}
|
|
865
867
|
<span class="fb-error-text shrink-0">
|
|
866
868
|
<span aria-hidden="true"
|
|
867
869
|
>{@html iconAlertWarning({ size: 14 })}</span
|
|
@@ -1089,121 +1091,70 @@
|
|
|
1089
1091
|
</div>
|
|
1090
1092
|
{/if}
|
|
1091
1093
|
|
|
1092
|
-
{#if entry.
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
{
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
aria-describedby={showExtrasError
|
|
1134
|
-
? `${id}-extras-err-${row.rid}`
|
|
1135
|
-
: undefined}
|
|
1136
|
-
/>
|
|
1137
|
-
{:else}
|
|
1138
|
-
<select
|
|
1139
|
-
id={exId}
|
|
1140
|
-
class={twMerge(INPUT_CLS, "fb-extra-input w-full")}
|
|
1141
|
-
value={exValue}
|
|
1142
|
-
onchange={(e) =>
|
|
1143
|
-
setExtra(
|
|
1144
|
-
row,
|
|
1145
|
-
ex.key,
|
|
1146
|
-
e.currentTarget.value || undefined
|
|
1147
|
-
)}
|
|
1148
|
-
{disabled}
|
|
1149
|
-
{tabindex}
|
|
1150
|
-
>
|
|
1151
|
-
<option value="">
|
|
1152
|
-
{displayText(ex.placeholder)}
|
|
1153
|
-
</option>
|
|
1154
|
-
{#each ex.options as opt (opt.value)}
|
|
1155
|
-
<option value={opt.value}>
|
|
1156
|
-
{displayText(opt.label)}
|
|
1157
|
-
</option>
|
|
1158
|
-
{/each}
|
|
1159
|
-
<!-- a stored value outside the declared list stays
|
|
1160
|
-
visible and round-trips (same stance as an
|
|
1161
|
-
unknown field type) -->
|
|
1162
|
-
{#if exValue && !ex.options.some((o) => o.value === exValue)}
|
|
1163
|
-
<option value={exValue}>{exValue}</option>
|
|
1164
|
-
{/if}
|
|
1165
|
-
</select>
|
|
1166
|
-
{/if}
|
|
1167
|
-
{#if ex.description}
|
|
1168
|
-
<div class="fb-hint text-xs mt-0.5">
|
|
1169
|
-
{displayText(ex.description)}
|
|
1170
|
-
</div>
|
|
1171
|
-
{/if}
|
|
1172
|
-
</div>
|
|
1173
|
-
{:else}
|
|
1174
|
-
<label
|
|
1175
|
-
class="stuic-checkbox fb-extra flex items-start gap-2 cursor-pointer"
|
|
1176
|
-
>
|
|
1177
|
-
<input
|
|
1178
|
-
type="checkbox"
|
|
1179
|
-
checked={!!row.def.extras?.[ex.key]}
|
|
1180
|
-
onchange={(e) =>
|
|
1181
|
-
setExtra(row, ex.key, e.currentTarget.checked)}
|
|
1182
|
-
{disabled}
|
|
1183
|
-
{tabindex}
|
|
1184
|
-
/>
|
|
1185
|
-
<span class="text-sm">
|
|
1186
|
-
{displayText(ex.label)}
|
|
1187
|
-
{#if ex.description}
|
|
1188
|
-
<span class="fb-hint block text-xs">
|
|
1189
|
-
{displayText(ex.description)}
|
|
1190
|
-
</span>
|
|
1191
|
-
{/if}
|
|
1192
|
-
</span>
|
|
1193
|
-
</label>
|
|
1194
|
-
{/if}
|
|
1195
|
-
{/each}
|
|
1196
|
-
{#if showExtrasError}
|
|
1094
|
+
{#if entry.supportsColumns}
|
|
1095
|
+
<div
|
|
1096
|
+
class="fb-field"
|
|
1097
|
+
role="group"
|
|
1098
|
+
aria-label={String(t("columns_label"))}
|
|
1099
|
+
aria-describedby={showColumnsError
|
|
1100
|
+
? `${id}-columns-err-${row.rid}`
|
|
1101
|
+
: undefined}
|
|
1102
|
+
>
|
|
1103
|
+
<div class="fb-sub-label">{t("columns_label")}</div>
|
|
1104
|
+
<ColumnsEditor
|
|
1105
|
+
bind:columns={row.def.columns}
|
|
1106
|
+
bind:meta={row.columnMeta}
|
|
1107
|
+
bind:this={columnsEditors[row.rid]}
|
|
1108
|
+
columnTypes={resolveColumnTypes(entry, types)}
|
|
1109
|
+
maxColumns={entry.maxColumns}
|
|
1110
|
+
{languages}
|
|
1111
|
+
defaultLanguage={_defaultLanguage}
|
|
1112
|
+
displayLanguage={_displayLanguage}
|
|
1113
|
+
{languageLabels}
|
|
1114
|
+
{keyMaxLength}
|
|
1115
|
+
{deriveKeyFromLabel}
|
|
1116
|
+
{keysImmutable}
|
|
1117
|
+
{disabled}
|
|
1118
|
+
locked={!!row.def.lock?.columns}
|
|
1119
|
+
errors={attempted ? errs?.columnErrors : undefined}
|
|
1120
|
+
idPrefix="{id}-col-{row.rid}"
|
|
1121
|
+
onBeforeDelete={onBeforeColumnDelete
|
|
1122
|
+
? (c) => onBeforeColumnDelete(cloneDef(row.def), c)
|
|
1123
|
+
: undefined}
|
|
1124
|
+
onBeforeTypeChange={onBeforeColumnTypeChange
|
|
1125
|
+
? (c, nt) =>
|
|
1126
|
+
onBeforeColumnTypeChange(cloneDef(row.def), c, nt)
|
|
1127
|
+
: undefined}
|
|
1128
|
+
{tabindex}
|
|
1129
|
+
{t}
|
|
1130
|
+
onChange={syncToValue}
|
|
1131
|
+
/>
|
|
1132
|
+
<!-- the list-level message only (no columns / over the cap);
|
|
1133
|
+
a per-column message is already shown inline on its line -->
|
|
1134
|
+
{#if showColumnsError && !errs?.columnErrors}
|
|
1197
1135
|
<div
|
|
1198
|
-
id="{id}-
|
|
1199
|
-
class="fb-error-text text-sm"
|
|
1136
|
+
id="{id}-columns-err-{row.rid}"
|
|
1137
|
+
class="fb-error-text text-sm mt-0.5"
|
|
1200
1138
|
>
|
|
1201
|
-
{errs?.
|
|
1139
|
+
{errs?.columns}
|
|
1202
1140
|
</div>
|
|
1203
1141
|
{/if}
|
|
1204
1142
|
</div>
|
|
1205
1143
|
{/if}
|
|
1206
1144
|
|
|
1145
|
+
{#if entry.extras?.length}
|
|
1146
|
+
<ExtrasEditor
|
|
1147
|
+
bind:extras={row.def.extras}
|
|
1148
|
+
defs={entry.extras}
|
|
1149
|
+
displayLanguages={_displayLanguages}
|
|
1150
|
+
idPrefix="{id}-{row.rid}"
|
|
1151
|
+
{disabled}
|
|
1152
|
+
{tabindex}
|
|
1153
|
+
error={showExtrasError ? errs?.extras : undefined}
|
|
1154
|
+
onChange={syncToValue}
|
|
1155
|
+
/>
|
|
1156
|
+
{/if}
|
|
1157
|
+
|
|
1207
1158
|
<div class="fb-advanced">
|
|
1208
1159
|
<button
|
|
1209
1160
|
type="button"
|
|
@@ -3,7 +3,7 @@ import type { ValidateOptions, ValidationResult } from "../../actions/validate.s
|
|
|
3
3
|
import type { TranslateFn } from "../../types.js";
|
|
4
4
|
import type { THC } from "../Thc/Thc.svelte";
|
|
5
5
|
import type { InputWrapClassProps } from "../Input/types.js";
|
|
6
|
-
import type { FieldDef, FieldTypeDef } from "./types.js";
|
|
6
|
+
import type { FieldColumnDef, FieldDef, FieldTypeDef } from "./types.js";
|
|
7
7
|
type SnippetWithId = Snippet<[{
|
|
8
8
|
id: string;
|
|
9
9
|
}]>;
|
|
@@ -91,6 +91,15 @@ export interface Props extends InputWrapClassProps, Record<string, any> {
|
|
|
91
91
|
* session.
|
|
92
92
|
*/
|
|
93
93
|
onBeforeTypeChange?: (field: FieldDef, newType: string) => void | false | Promise<void | false>;
|
|
94
|
+
/**
|
|
95
|
+
* Veto hook for removing a column that was present when `value` was
|
|
96
|
+
* (re)loaded — return `false` (or throw) to cancel. Not called for
|
|
97
|
+
* columns added in this session. Column removal is otherwise immediate
|
|
98
|
+
* (no mark-delete for columns); this hook is the safety net.
|
|
99
|
+
*/
|
|
100
|
+
onBeforeColumnDelete?: (field: FieldDef, column: FieldColumnDef) => void | false | Promise<void | false>;
|
|
101
|
+
/** Veto hook for changing the type of a pre-existing column (same contract). */
|
|
102
|
+
onBeforeColumnTypeChange?: (field: FieldDef, column: FieldColumnDef, newType: string) => void | false | Promise<void | false>;
|
|
94
103
|
onChange?: (value: FieldDef[]) => void;
|
|
95
104
|
/** Renders the preview pane. Receives the current (visible) fields. */
|
|
96
105
|
preview?: Snippet<[{
|
|
@@ -43,6 +43,7 @@ interface FieldDef {
|
|
|
43
43
|
required?: boolean;
|
|
44
44
|
options?: { value: string; label: LocalizedText }[]; // edited for `supportsOptions` types
|
|
45
45
|
extras?: Record<string, unknown>; // per-type flags declared by the palette
|
|
46
|
+
columns?: FieldColumnDef[]; // edited for `supportsColumns` types, see Columns
|
|
46
47
|
lock?: FieldLock; // what the user may NOT change
|
|
47
48
|
}
|
|
48
49
|
```
|
|
@@ -62,7 +63,8 @@ Value membership rules:
|
|
|
62
63
|
- Palette `extras` defaults are materialized into `def.extras` when a field is added
|
|
63
64
|
or its type changes; a def loaded _without_ an extra's key renders empty/unchecked —
|
|
64
65
|
the control always reflects what `value` actually contains, never a phantom default.
|
|
65
|
-
Extras are **retained** across a type change too (same rule as `options`)
|
|
66
|
+
Extras are **retained** across a type change too (same rule as `options`), and so
|
|
67
|
+
are `columns`.
|
|
66
68
|
|
|
67
69
|
## The palette
|
|
68
70
|
|
|
@@ -74,6 +76,9 @@ interface FieldTypeDef {
|
|
|
74
76
|
icon?: string | Snippet; // html string or snippet, shown in the row's type chip
|
|
75
77
|
supportsOptions?: boolean; // renders the option editor
|
|
76
78
|
extras?: FieldTypeExtraDef[]; // per-type controls, see below
|
|
79
|
+
supportsColumns?: boolean; // renders the column editor, see Columns
|
|
80
|
+
columnTypes?: FieldTypeDef[]; // the palette a column may take
|
|
81
|
+
maxColumns?: number; // cap on the column list
|
|
77
82
|
preview?: Snippet<[FieldDef]>; // per-type preview of a single field
|
|
78
83
|
}
|
|
79
84
|
```
|
|
@@ -140,6 +145,76 @@ type FieldTypeExtraDef =
|
|
|
140
145
|
(text / longtext / number / checkbox / select / date) for demos and unopinionated
|
|
141
146
|
consumers — `types` is still a required prop, so nobody gets it by accident.
|
|
142
147
|
|
|
148
|
+
## Columns
|
|
149
|
+
|
|
150
|
+
A palette entry may declare `supportsColumns`: its definition is then a **list of typed
|
|
151
|
+
columns** — a "table" field whose value (rows) is an array of objects keyed by column.
|
|
152
|
+
Editing the rows is not this component's job; authoring the columns is.
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
interface FieldColumnDef {
|
|
156
|
+
key: string; // unique within the field's columns (not across fields)
|
|
157
|
+
type: string; // one of the entry's `columnTypes`
|
|
158
|
+
label: LocalizedText;
|
|
159
|
+
options?: FieldOptionDef[]; // edited for a `supportsOptions` column type
|
|
160
|
+
extras?: Record<string, unknown>; // driven by the column type's `extras`
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
{
|
|
166
|
+
type: "table",
|
|
167
|
+
label: "Table",
|
|
168
|
+
supportsColumns: true,
|
|
169
|
+
maxColumns: 8,
|
|
170
|
+
columnTypes: [
|
|
171
|
+
{ type: "text", label: "Text" },
|
|
172
|
+
{ type: "number", label: "Number",
|
|
173
|
+
extras: [{ key: "unit", label: "Unit", type: "string", maxlength: 16 }] },
|
|
174
|
+
{ type: "select", label: "Choice", supportsOptions: true },
|
|
175
|
+
],
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
- **A column is a field in miniature.** One line per column: label (localized, like
|
|
180
|
+
every other label), type, key, and — behind a settings toggle that appears only when
|
|
181
|
+
the column type has `supportsOptions` or `extras` — the same option editor and extras
|
|
182
|
+
controls a field gets. `columnTypes` is a full `FieldTypeDef[]` on purpose, so there is
|
|
183
|
+
no new vocabulary: a number column declares its unit exactly as a number field does.
|
|
184
|
+
Leave off the extras that only make sense per field.
|
|
185
|
+
- **Default palette.** Without `columnTypes`, a column may take any entry of `types`
|
|
186
|
+
that does not itself `supportsColumns`. Columns never nest — a `supportsColumns` entry
|
|
187
|
+
listed inside `columnTypes` is treated as a plain column type.
|
|
188
|
+
- **Keys follow the field-key policy**: derived from the label while untouched
|
|
189
|
+
(transliterated, unique among the sibling columns, bounded by `keyMaxLength`; a
|
|
190
|
+
`deriveKeyFromLabel` function is used here too), frozen by the first manual edit, and
|
|
191
|
+
with `keysImmutable` read-only for every column that was present when `value` was
|
|
192
|
+
(re)loaded — a column added this session stays editable, also after the row is
|
|
193
|
+
collapsed and re-expanded. `keyPattern` / `keyMaxLength` apply; `reservedKeys` does
|
|
194
|
+
**not** (it guards the field-key namespace, a column key lives inside one field's rows).
|
|
195
|
+
- **Removal is immediate** — there is no mark-delete for columns. The safety net is
|
|
196
|
+
`onBeforeColumnDelete(field, column)`: return `false` (or throw) to cancel. It fires
|
|
197
|
+
only for stored columns, never for one added this session. Changing a stored column's
|
|
198
|
+
type is guarded the same way (`onBeforeColumnTypeChange(field, column, newType)`, plus
|
|
199
|
+
an inline warning), and keeps the column's `options` / `extras` like a field does.
|
|
200
|
+
- **`lock.columns`** renders the column list read-only (the counterpart of
|
|
201
|
+
`lock.options`).
|
|
202
|
+
- **Retained across a field type change**: `table → text → table` restores the columns.
|
|
203
|
+
Consumers compiling the list should ignore `columns` on types without
|
|
204
|
+
`supportsColumns`.
|
|
205
|
+
- **Unknown column type** (not in the column palette): rendered as a degraded read-only
|
|
206
|
+
line with a warning, not validated, round-tripped untouched — its key still counts
|
|
207
|
+
toward uniqueness. The same stance as an unknown field type.
|
|
208
|
+
|
|
209
|
+
Validation adds, for a `supportsColumns` field: at least one column, at most
|
|
210
|
+
`maxColumns` (a seeded list above the cap is an error, never truncated), and per column
|
|
211
|
+
the label / key / options / string-extra rules above. `validateFieldDefs` reports them
|
|
212
|
+
as `rowErrors[i].columnErrors` (index-aligned with `def.columns`, `null` for a clean
|
|
213
|
+
column) and a row summary `rowErrors[i].columns` ("Column 2: Label is required").
|
|
214
|
+
`validate()` expands the row, opens the column's settings when needed, and focuses the
|
|
215
|
+
offending control. Consumer-specific budgets (say "fields plus columns ≤ 80") belong to
|
|
216
|
+
the consumer: `validate.customValidator` runs after the built-in rules.
|
|
217
|
+
|
|
143
218
|
## Keys
|
|
144
219
|
|
|
145
220
|
The key is the machine identifier; once data exists under it, renaming orphans that
|
|
@@ -175,7 +250,8 @@ data. The component treats keys accordingly:
|
|
|
175
250
|
|
|
176
251
|
Owns: key pattern / length / uniqueness / reserved, label non-empty, choice types have
|
|
177
252
|
at least one option with unique non-empty values, `maxFields`, `required` (at least one
|
|
178
|
-
field)
|
|
253
|
+
field), and for `supportsColumns` types the column rules (see Columns). `validate()`
|
|
254
|
+
expands, scrolls to and focuses the first offender.
|
|
179
255
|
|
|
180
256
|
**Does not own:** whether the resulting list is acceptable to the consumer's backend.
|
|
181
257
|
The consumer persisting the list MUST re-validate server-side — this component is a
|
|
@@ -207,6 +283,8 @@ not block validation. It is never silently dropped.
|
|
|
207
283
|
| `deleteMode` | `"mark" \| "immediate"` | `"mark"` | Delete UX (see above) |
|
|
208
284
|
| `onBeforeDelete` | `(field) => void \| false \| Promise<void \| false>` | — | Delete veto hook |
|
|
209
285
|
| `onBeforeTypeChange` | `(field, newType) => void \| false \| Promise<...>` | — | Type-change veto hook (pre-existing fields) |
|
|
286
|
+
| `onBeforeColumnDelete` | `(field, column) => void \| false \| Promise<...>` | — | Column removal veto hook (stored columns of `supportsColumns` fields) |
|
|
287
|
+
| `onBeforeColumnTypeChange` | `(field, column, newType) => void \| false \| ...` | — | Column type-change veto hook (stored columns) |
|
|
210
288
|
| `onChange` | `(value: FieldDef[]) => void` | — | Fired after every change |
|
|
211
289
|
| `preview` | `Snippet<[{ fields: FieldDef[] }]>` | — | Preview pane content (see below) |
|
|
212
290
|
| `previewBreakpoint` | `number` | `768` | Component width for side-by-side preview; `0` = below |
|
|
@@ -239,7 +317,8 @@ no built-in mapping from palette types to stuic `Field*` components.
|
|
|
239
317
|
|
|
240
318
|
## Locks
|
|
241
319
|
|
|
242
|
-
Per-field `lock` flags: `key`, `type`, `required`, `options`, `
|
|
320
|
+
Per-field `lock` flags: `key`, `type`, `required`, `options`, `columns`, `delete`,
|
|
321
|
+
`reorder`.
|
|
243
322
|
A `lock.reorder` field is position-pinned — it cannot be dragged and no other move may
|
|
244
323
|
change its index. **Label and description are always editable**, even on fully locked
|
|
245
324
|
fields: the consumer owns a system field's identity, the user owns what it is called.
|