@eifi1/ui-kit 0.22.0 → 0.23.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 +42 -25
- package/dist/chart.d.ts +11 -8
- package/dist/components/amount-input.d.ts +28 -8
- package/dist/components/amount-input.js +3 -1
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/autocomplete.d.ts +21 -0
- package/dist/components/autocomplete.js +128 -103
- package/dist/components/autocomplete.js.map +1 -1
- package/dist/components/button-group.d.ts +11 -8
- package/dist/components/calculator.d.ts +11 -8
- package/dist/components/column-mapper.d.ts +182 -0
- package/dist/components/column-mapper.js +375 -0
- package/dist/components/column-mapper.js.map +1 -0
- package/dist/components/combobox-core.d.ts +39 -2
- package/dist/components/combobox-core.js +29 -6
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/combobox.d.ts +80 -2
- package/dist/components/combobox.js +448 -311
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/confirm-dialog.d.ts +11 -8
- package/dist/components/confirm-dialog.js +7 -8
- package/dist/components/confirm-dialog.js.map +1 -1
- package/dist/components/copy-button.d.ts +8 -5
- package/dist/components/country-select.d.ts +50 -7
- package/dist/components/country-select.js +65 -9
- package/dist/components/country-select.js.map +1 -1
- package/dist/components/danger-confirm.d.ts +11 -8
- package/dist/components/danger-confirm.js +13 -3
- package/dist/components/danger-confirm.js.map +1 -1
- package/dist/components/date-picker.d.ts +41 -0
- package/dist/components/date-picker.js +269 -8
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/entity-combobox.d.ts +35 -1
- package/dist/components/entity-combobox.js +148 -112
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/facing-pair.d.ts +11 -8
- package/dist/components/field-parts.d.ts +116 -6
- package/dist/components/field-parts.js +84 -0
- package/dist/components/field-parts.js.map +1 -1
- package/dist/components/field-strip.d.ts +105 -0
- package/dist/components/field-strip.js +46 -0
- package/dist/components/field-strip.js.map +1 -0
- package/dist/components/file-button.d.ts +11 -8
- package/dist/components/file-dropzone.d.ts +11 -8
- package/dist/components/form-actions.d.ts +10 -7
- package/dist/components/form-actions.js +8 -2
- package/dist/components/form-actions.js.map +1 -1
- package/dist/components/iban-input.d.ts +11 -8
- package/dist/components/icon-picker.d.ts +10 -1
- package/dist/components/icon-picker.js +12 -4
- package/dist/components/icon-picker.js.map +1 -1
- package/dist/components/language-select.d.ts +11 -8
- package/dist/components/money-field.d.ts +14 -8
- package/dist/components/money-field.js.map +1 -1
- package/dist/components/month-picker.js +2 -1
- package/dist/components/month-picker.js.map +1 -1
- package/dist/components/multi-entity-combobox.d.ts +26 -1
- package/dist/components/multi-entity-combobox.js +143 -102
- package/dist/components/multi-entity-combobox.js.map +1 -1
- package/dist/components/number-field.d.ts +11 -8
- package/dist/components/number-input.d.ts +11 -8
- package/dist/components/numpad-sheet.d.ts +11 -8
- package/dist/components/phone-input.d.ts +11 -8
- package/dist/components/reauth-dialog.d.ts +2 -2
- package/dist/components/reauth-dialog.js +6 -8
- package/dist/components/reauth-dialog.js.map +1 -1
- package/dist/components/series-chart.d.ts +11 -8
- package/dist/components/settings-fields.d.ts +11 -8
- package/dist/components/share-card.d.ts +10 -7
- package/dist/components/swatch-picker.d.ts +17 -1
- package/dist/components/swatch-picker.js +10 -4
- package/dist/components/swatch-picker.js.map +1 -1
- package/dist/components/text-link.d.ts +10 -7
- package/dist/components/tile-radio.d.ts +20 -0
- package/dist/components/tile-radio.js +25 -8
- package/dist/components/tile-radio.js.map +1 -1
- package/dist/components/time-input.d.ts +11 -8
- package/dist/components/toggle-group.js +2 -2
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/trigger-aria.d.ts +3 -0
- package/dist/components/trigger-aria.js +3 -1
- package/dist/components/trigger-aria.js.map +1 -1
- package/dist/components/ui.d.ts +8 -5
- package/dist/components/ui.js +5 -11
- package/dist/components/ui.js.map +1 -1
- package/dist/feedback/feedback-attachment.d.ts +1 -1
- package/dist/feedback/feedback-attachment.js +156 -56
- package/dist/feedback/feedback-attachment.js.map +1 -1
- package/dist/feedback/feedback-dialog.d.ts +1 -1
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback/feedback-inbox.d.ts +3 -2
- package/dist/feedback/feedback-inbox.js.map +1 -1
- package/dist/feedback/feedback-thread.d.ts +68 -287
- package/dist/feedback/feedback-thread.js +8 -1
- package/dist/feedback/feedback-thread.js.map +1 -1
- package/dist/{kit-labels-R8mIAc9N.d.ts → feedback-BxeQVzwq.d.ts} +405 -22
- package/dist/{feedback-attachment-NhUm7Zga.d.ts → feedback-attachment-fGAzZPf0.d.ts} +112 -10
- package/dist/feedback.d.ts +66 -2
- package/dist/hooks/use-file-drop.d.ts +11 -8
- package/dist/i18n/defaults.d.ts +11 -8
- package/dist/i18n/defaults.js +3 -1
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.d.ts +11 -8
- package/dist/i18n/german.js +36 -2
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +7 -4
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/languages.d.ts +11 -8
- package/dist/i18n/locales/de-CH.d.ts +11 -8
- package/dist/i18n/locales/en.d.ts +11 -8
- package/dist/i18n/locales/en.js +7 -0
- package/dist/i18n/locales/en.js.map +1 -1
- package/dist/i18n/locales/es.d.ts +11 -8
- package/dist/i18n/locales/es.js +37 -2
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +11 -8
- package/dist/i18n/locales/fr.js +36 -2
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +11 -8
- package/dist/i18n/locales/hu.js +38 -2
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +11 -8
- package/dist/i18n/locales/it.js +37 -2
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +11 -8
- package/dist/i18n/locales/zh.js +34 -2
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.d.ts +11 -8
- package/dist/i18n/review.js +20 -1
- package/dist/i18n/review.js.map +1 -1
- package/dist/index.d.ts +7 -3
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/column-mapping.d.ts +81 -0
- package/dist/lib/column-mapping.js +108 -0
- package/dist/lib/column-mapping.js.map +1 -0
- package/dist/lib/table-text.d.ts +109 -1
- package/dist/lib/table-text.js +122 -1
- package/dist/lib/table-text.js.map +1 -1
- package/dist/rhf/fields.d.ts +89 -11
- package/dist/rhf/fields.js +124 -0
- package/dist/rhf/fields.js.map +1 -1
- package/dist/rhf/form.d.ts +11 -8
- package/dist/rhf.d.ts +12 -9
- package/dist/rhf.js.map +1 -1
- package/dist/shell/app-shell.d.ts +10 -7
- package/dist/shell/top-bar-brand.d.ts +11 -8
- package/dist/shell/topbar-action-menu.d.ts +36 -3
- package/dist/shell/topbar-action-menu.js +74 -33
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell.d.ts +10 -7
- package/dist/table-text.d.ts +1 -1
- package/dist/wizard/stepper-nav.d.ts +41 -10
- package/dist/wizard/stepper-nav.js +4 -0
- package/dist/wizard/stepper-nav.js.map +1 -1
- package/dist/wizard.d.ts +11 -8
- package/package.json +1 -1
- package/src/components/amount-input.tsx +20 -1
- package/src/components/autocomplete.tsx +172 -111
- package/src/components/column-mapper.tsx +666 -0
- package/src/components/combobox-core.tsx +82 -9
- package/src/components/combobox.tsx +614 -332
- package/src/components/confirm-dialog.tsx +12 -8
- package/src/components/country-select.tsx +135 -22
- package/src/components/danger-confirm.tsx +91 -12
- package/src/components/date-picker.tsx +423 -10
- package/src/components/entity-combobox.tsx +217 -126
- package/src/components/field-parts.tsx +224 -5
- package/src/components/field-strip.tsx +149 -0
- package/src/components/form-actions.tsx +32 -4
- package/src/components/icon-picker.tsx +23 -4
- package/src/components/money-field.tsx +3 -0
- package/src/components/month-picker.tsx +2 -1
- package/src/components/multi-entity-combobox.tsx +207 -120
- package/src/components/reauth-dialog.tsx +17 -18
- package/src/components/swatch-picker.tsx +29 -5
- package/src/components/tile-radio.tsx +74 -13
- package/src/components/toggle-group.tsx +2 -2
- package/src/components/trigger-aria.ts +5 -0
- package/src/components/ui.tsx +7 -33
- package/src/feedback/feedback-attachment.tsx +308 -61
- package/src/feedback/feedback-dialog.tsx +7 -3
- package/src/feedback/feedback-inbox.tsx +3 -2
- package/src/feedback/feedback-thread.tsx +47 -4
- package/src/i18n/defaults.ts +2 -0
- package/src/i18n/german.ts +42 -0
- package/src/i18n/kit-labels.tsx +4 -0
- package/src/i18n/locales/en.ts +27 -5
- package/src/i18n/locales/es.ts +41 -0
- package/src/i18n/locales/fr.ts +42 -0
- package/src/i18n/locales/hu.ts +37 -0
- package/src/i18n/locales/it.ts +41 -0
- package/src/i18n/locales/zh.ts +32 -0
- package/src/i18n/review.ts +19 -0
- package/src/index.ts +18 -0
- package/src/lib/column-mapping.ts +234 -0
- package/src/lib/table-text.ts +271 -0
- package/src/rhf/fields.tsx +254 -0
- package/src/rhf.ts +2 -1
- package/src/shell/topbar-action-menu.tsx +134 -38
- package/src/wizard/stepper-nav.tsx +35 -1
- package/dist/components/field-anatomy.d.ts +0 -95
- package/dist/components/field-anatomy.js +0 -84
- package/dist/components/field-anatomy.js.map +0 -1
- package/src/components/field-anatomy.tsx +0 -190
package/src/rhf/fields.tsx
CHANGED
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
* <RhfTimeInput name="meetingTime" label="Time" />
|
|
14
14
|
* <RhfDateRangePicker fromName="periodFrom" toName="periodTo" label="Period" />
|
|
15
15
|
* <RhfToggleGroup name="interval" label="Interval" options={INTERVALS} />
|
|
16
|
+
* <RhfCountrySelect name="country" label="Country" preferred={["CH", "DE"]} />
|
|
17
|
+
* <RhfMonthPicker name="fiscalYear" label="Fiscal year" mode="year" valueAsNumber />
|
|
16
18
|
* </Form>
|
|
17
19
|
* ```
|
|
18
20
|
*
|
|
@@ -66,6 +68,8 @@ import {
|
|
|
66
68
|
import { TimeInput, type TimeInputProps } from "../components/time-input";
|
|
67
69
|
import { IbanInput, type IbanInputProps } from "../components/iban-input";
|
|
68
70
|
import { PhoneInput, type PhoneInputProps } from "../components/phone-input";
|
|
71
|
+
import { CountrySelect, type CountrySelectProps } from "../components/country-select";
|
|
72
|
+
import { MonthPicker, type MonthPickerProps } from "../components/month-picker";
|
|
69
73
|
import {
|
|
70
74
|
ToggleGroup,
|
|
71
75
|
type ToggleGroupBaseProps,
|
|
@@ -571,6 +575,10 @@ export type RhfMoneyFieldProps<
|
|
|
571
575
|
* with `rules` as well if a clamp needs explaining. */
|
|
572
576
|
min?: number;
|
|
573
577
|
max?: number;
|
|
578
|
+
/** Classes for the `<input>` — `className` is the item's box. {@link RhfTextField},
|
|
579
|
+
* {@link RhfTextarea} and {@link RhfNumberField} take it; since 0.23 AmountInput
|
|
580
|
+
* does too (keksdose G7: `text-end` for a money column). */
|
|
581
|
+
inputClassName?: string;
|
|
574
582
|
};
|
|
575
583
|
|
|
576
584
|
/** The amount a text reads as, or `undefined` for a draft that is not one yet. */
|
|
@@ -1197,6 +1205,252 @@ export function RhfPhoneInput<
|
|
|
1197
1205
|
);
|
|
1198
1206
|
}
|
|
1199
1207
|
|
|
1208
|
+
// ── country and month (0.23) ─────────────────────────────────────────────────
|
|
1209
|
+
|
|
1210
|
+
type OwnCountrySelectProps = Omit<
|
|
1211
|
+
CountrySelectProps<boolean>,
|
|
1212
|
+
| "value"
|
|
1213
|
+
| "onChange"
|
|
1214
|
+
| "onBlur"
|
|
1215
|
+
| "label"
|
|
1216
|
+
| "hint"
|
|
1217
|
+
| "error"
|
|
1218
|
+
| "invalid"
|
|
1219
|
+
| "disabled"
|
|
1220
|
+
| "className"
|
|
1221
|
+
| "id"
|
|
1222
|
+
| "ref"
|
|
1223
|
+
| "clearable"
|
|
1224
|
+
>;
|
|
1225
|
+
|
|
1226
|
+
export type RhfCountrySelectProps<
|
|
1227
|
+
TFieldValues extends FieldValues = FieldValues,
|
|
1228
|
+
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
|
1229
|
+
TTransformed = TFieldValues,
|
|
1230
|
+
> = RhfFieldBaseProps<TFieldValues, TName, TTransformed> &
|
|
1231
|
+
OwnCountrySelectProps & {
|
|
1232
|
+
/** Offer the picker's clear "×" (and Delete on the trigger) while a country is
|
|
1233
|
+
* chosen. A clear stores `clearValue`. */
|
|
1234
|
+
clearable?: boolean;
|
|
1235
|
+
/** What a clear stores. Default `null`; `""` for a schema that spells "no
|
|
1236
|
+
* country" as an empty string (a zod `z.string().max(2)`, kastlan's). The picker
|
|
1237
|
+
* reads either back as empty. */
|
|
1238
|
+
clearValue?: ComboClearValue;
|
|
1239
|
+
/** Classes for the picker — `className` is the item's box. */
|
|
1240
|
+
selectClassName?: string;
|
|
1241
|
+
};
|
|
1242
|
+
|
|
1243
|
+
/**
|
|
1244
|
+
* {@link CountrySelect} bound to one field, which stores the ISO 3166-1 alpha-2 code
|
|
1245
|
+
* ("CH", always upper-case), or `clearValue` once cleared (`clearable`). kastlan asked
|
|
1246
|
+
* for it (0.22 adoption): its address form wired a `CountrySelect` through
|
|
1247
|
+
* {@link RhfField}'s render, and with no ref on the picker react-hook-form's
|
|
1248
|
+
* focus-on-error had nothing to focus — the one field on the form a failed submit
|
|
1249
|
+
* could not take you to.
|
|
1250
|
+
*
|
|
1251
|
+
* Bound the way {@link RhfIbanInput} and {@link RhfPhoneInput} are: `field.ref` reaches
|
|
1252
|
+
* the trigger (the picker's `ref`, 0.23), the form's error paints the trigger and is
|
|
1253
|
+
* the shell's message under it, the hint is the shell's description — so neither is
|
|
1254
|
+
* ever handed to the picker, and its box never changes when one comes or goes. The
|
|
1255
|
+
* form's label names the trigger together with the chosen country, and is the phone
|
|
1256
|
+
* sheet's title. `commit` and `disabledReason` are the picker's own: a country that
|
|
1257
|
+
* saves itself stays focusable under a lock and says why.
|
|
1258
|
+
*
|
|
1259
|
+
* ```tsx
|
|
1260
|
+
* <RhfCountrySelect name="country" label="Country" required
|
|
1261
|
+
* preferred={["CH", "LI", "DE", "AT", "FR", "IT"]} rules={{ required: "Choose a country" }} />
|
|
1262
|
+
* ```
|
|
1263
|
+
*/
|
|
1264
|
+
export function RhfCountrySelect<
|
|
1265
|
+
TFieldValues extends FieldValues = FieldValues,
|
|
1266
|
+
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
|
1267
|
+
TTransformed = TFieldValues,
|
|
1268
|
+
>({
|
|
1269
|
+
name,
|
|
1270
|
+
control,
|
|
1271
|
+
rules,
|
|
1272
|
+
label,
|
|
1273
|
+
hint,
|
|
1274
|
+
required,
|
|
1275
|
+
disabled,
|
|
1276
|
+
excludeWhenDisabled,
|
|
1277
|
+
className,
|
|
1278
|
+
clearable,
|
|
1279
|
+
clearValue = null,
|
|
1280
|
+
selectClassName,
|
|
1281
|
+
labels,
|
|
1282
|
+
...countryProps
|
|
1283
|
+
}: RhfCountrySelectProps<TFieldValues, TName, TTransformed>) {
|
|
1284
|
+
const labelled = hasContent(label);
|
|
1285
|
+
// A string label is also what the field IS — the picker's `country` word, which
|
|
1286
|
+
// titles the phone sheet. A caller's own `labels.country` still wins.
|
|
1287
|
+
const ownLabels = typeof label === "string" ? { country: label, ...labels } : labels;
|
|
1288
|
+
return (
|
|
1289
|
+
<RhfField
|
|
1290
|
+
{...{ name, control, rules, label, hint, required, disabled, excludeWhenDisabled, className }}
|
|
1291
|
+
render={({ field, invalid, labelId }) => {
|
|
1292
|
+
const value: unknown = field.value;
|
|
1293
|
+
return (
|
|
1294
|
+
// FormControl clones this element with `id` / `aria-describedby` /
|
|
1295
|
+
// `aria-invalid`, which the picker routes to its trigger.
|
|
1296
|
+
<CountrySelect<boolean>
|
|
1297
|
+
aria-required={required || undefined}
|
|
1298
|
+
// The form's label, then the country (see the picker's `aria-labelledby`).
|
|
1299
|
+
aria-labelledby={labelled ? labelId : undefined}
|
|
1300
|
+
{...countryProps}
|
|
1301
|
+
labels={ownLabels}
|
|
1302
|
+
className={selectClassName}
|
|
1303
|
+
ref={field.ref}
|
|
1304
|
+
value={typeof value === "string" ? value : null}
|
|
1305
|
+
clearable={clearable}
|
|
1306
|
+
onChange={(code) => field.onChange(code ?? clearValue)}
|
|
1307
|
+
// Focus leaving the picker (the wrapper's focusout) marks the field touched.
|
|
1308
|
+
onBlur={field.onBlur}
|
|
1309
|
+
disabled={field.disabled}
|
|
1310
|
+
invalid={invalid}
|
|
1311
|
+
/>
|
|
1312
|
+
);
|
|
1313
|
+
}}
|
|
1314
|
+
/>
|
|
1315
|
+
);
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
type OwnMonthPickerProps = Omit<
|
|
1319
|
+
MonthPickerProps,
|
|
1320
|
+
"value" | "onChange" | "onBlur" | "label" | "hint" | "error" | "invalid" | "disabled" | "className" | "id" | "mode"
|
|
1321
|
+
>;
|
|
1322
|
+
|
|
1323
|
+
/** `mode` picks what the form stores, and `valueAsNumber` is only offered for a year. */
|
|
1324
|
+
type RhfMonthPickerMode =
|
|
1325
|
+
| {
|
|
1326
|
+
/** A month, stored as `"YYYY-MM"` (the default). */
|
|
1327
|
+
mode?: "month";
|
|
1328
|
+
valueAsNumber?: undefined;
|
|
1329
|
+
}
|
|
1330
|
+
| {
|
|
1331
|
+
/** A year, stored as `"YYYY"` — or as a number with `valueAsNumber`. */
|
|
1332
|
+
mode: "year";
|
|
1333
|
+
/** Store the year as a number (`2026`) rather than `"2026"` — kastlan's budget
|
|
1334
|
+
* `fiscal_year` is an integer column. A number in the form reads back as its
|
|
1335
|
+
* year. */
|
|
1336
|
+
valueAsNumber?: boolean;
|
|
1337
|
+
};
|
|
1338
|
+
|
|
1339
|
+
export type RhfMonthPickerProps<
|
|
1340
|
+
TFieldValues extends FieldValues = FieldValues,
|
|
1341
|
+
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
|
1342
|
+
TTransformed = TFieldValues,
|
|
1343
|
+
> = RhfFieldBaseProps<TFieldValues, TName, TTransformed> &
|
|
1344
|
+
OwnMonthPickerProps & {
|
|
1345
|
+
/** Classes for the picker — `className` is the item's box. As {@link RhfDateField}'s. */
|
|
1346
|
+
inputClassName?: string;
|
|
1347
|
+
} & RhfMonthPickerMode;
|
|
1348
|
+
|
|
1349
|
+
function MonthControl({
|
|
1350
|
+
field,
|
|
1351
|
+
invalid,
|
|
1352
|
+
id,
|
|
1353
|
+
valueAsNumber,
|
|
1354
|
+
mode,
|
|
1355
|
+
inputClassName,
|
|
1356
|
+
pickerProps,
|
|
1357
|
+
required,
|
|
1358
|
+
...aria
|
|
1359
|
+
}: {
|
|
1360
|
+
field: ControllerRenderProps;
|
|
1361
|
+
invalid: boolean;
|
|
1362
|
+
id: string;
|
|
1363
|
+
valueAsNumber?: boolean;
|
|
1364
|
+
mode?: "month" | "year";
|
|
1365
|
+
inputClassName?: string;
|
|
1366
|
+
pickerProps: OwnMonthPickerProps;
|
|
1367
|
+
required?: boolean;
|
|
1368
|
+
"aria-describedby"?: string;
|
|
1369
|
+
"aria-invalid"?: MonthPickerProps["aria-invalid"];
|
|
1370
|
+
}) {
|
|
1371
|
+
// MonthPicker forwards no ref; it routes `id` to its trigger, where the handle finds
|
|
1372
|
+
// it — RhfDateField's way.
|
|
1373
|
+
useFocusHandle(field.ref, () => document.getElementById(id));
|
|
1374
|
+
const value: unknown = field.value;
|
|
1375
|
+
return (
|
|
1376
|
+
<MonthPicker
|
|
1377
|
+
{...pickerProps}
|
|
1378
|
+
{...aria}
|
|
1379
|
+
// MonthPicker routes `aria-required` to its trigger since 0.23.0.
|
|
1380
|
+
aria-required={required || undefined}
|
|
1381
|
+
id={id}
|
|
1382
|
+
mode={mode}
|
|
1383
|
+
value={value === null || value === undefined ? "" : String(value)}
|
|
1384
|
+
onChange={(key) => {
|
|
1385
|
+
field.onChange(mode === "year" && valueAsNumber ? Number(key) : key);
|
|
1386
|
+
// A pick is the whole interaction — there is no text to leave — so it touches.
|
|
1387
|
+
field.onBlur();
|
|
1388
|
+
}}
|
|
1389
|
+
disabled={field.disabled}
|
|
1390
|
+
invalid={invalid}
|
|
1391
|
+
className={inputClassName}
|
|
1392
|
+
/>
|
|
1393
|
+
);
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
/**
|
|
1397
|
+
* {@link MonthPicker} bound to one field: a month as `"YYYY-MM"`, or with `mode="year"` a
|
|
1398
|
+
* year as `"YYYY"` (`valueAsNumber` stores `2026`). kastlan asked for it with
|
|
1399
|
+
* {@link RhfCountrySelect}: its budget wizard's fiscal year was a `MonthPicker
|
|
1400
|
+
* mode="year"` inside {@link RhfField}'s render — the `String()` in, the `Number()`
|
|
1401
|
+
* out, `invalid` by hand — and a failed submit could not focus it, because the picker
|
|
1402
|
+
* forwards no ref.
|
|
1403
|
+
*
|
|
1404
|
+
* Bound as {@link RhfDateField} is: a focus handle finds the trigger by its id, so
|
|
1405
|
+
* focus-on-error lands there; the form's error paints the trigger and is the shell's
|
|
1406
|
+
* message; the hint is the shell's description; neither is handed to the picker, so
|
|
1407
|
+
* its box never changes. Picking marks the field touched.
|
|
1408
|
+
*
|
|
1409
|
+
* ```tsx
|
|
1410
|
+
* <RhfMonthPicker name="fiscalYear" label="Fiscal year" mode="year" valueAsNumber
|
|
1411
|
+
* min="2020" max="2029" rules={{ required: "Choose a year" }} />
|
|
1412
|
+
* ```
|
|
1413
|
+
*/
|
|
1414
|
+
export function RhfMonthPicker<
|
|
1415
|
+
TFieldValues extends FieldValues = FieldValues,
|
|
1416
|
+
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
|
1417
|
+
TTransformed = TFieldValues,
|
|
1418
|
+
>({
|
|
1419
|
+
name,
|
|
1420
|
+
control,
|
|
1421
|
+
rules,
|
|
1422
|
+
label,
|
|
1423
|
+
hint,
|
|
1424
|
+
required,
|
|
1425
|
+
disabled,
|
|
1426
|
+
excludeWhenDisabled,
|
|
1427
|
+
className,
|
|
1428
|
+
inputClassName,
|
|
1429
|
+
mode,
|
|
1430
|
+
valueAsNumber,
|
|
1431
|
+
...pickerProps
|
|
1432
|
+
}: RhfMonthPickerProps<TFieldValues, TName, TTransformed>) {
|
|
1433
|
+
return (
|
|
1434
|
+
<RhfField
|
|
1435
|
+
{...{ name, control, rules, label, hint, required, disabled, excludeWhenDisabled, className }}
|
|
1436
|
+
render={({ field, invalid, id }) => (
|
|
1437
|
+
// FormControl clones this element with `aria-describedby` / `aria-invalid`,
|
|
1438
|
+
// which MonthControl hands on to the picker's trigger.
|
|
1439
|
+
<MonthControl
|
|
1440
|
+
field={field as unknown as ControllerRenderProps}
|
|
1441
|
+
invalid={invalid}
|
|
1442
|
+
id={id}
|
|
1443
|
+
mode={mode}
|
|
1444
|
+
valueAsNumber={valueAsNumber}
|
|
1445
|
+
inputClassName={inputClassName}
|
|
1446
|
+
pickerProps={pickerProps}
|
|
1447
|
+
required={required}
|
|
1448
|
+
/>
|
|
1449
|
+
)}
|
|
1450
|
+
/>
|
|
1451
|
+
);
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1200
1454
|
// ── select ───────────────────────────────────────────────────────────────────
|
|
1201
1455
|
|
|
1202
1456
|
export interface RhfSelectOption {
|
package/src/rhf.ts
CHANGED
|
@@ -11,7 +11,8 @@ export * from "./rhf/form";
|
|
|
11
11
|
export * from "./rhf/use-rhf-wizard-step";
|
|
12
12
|
// Bound fields — one line per field (RhfTextField, RhfNumberField, RhfIntegerField, RhfMoneyField,
|
|
13
13
|
// RhfDateField, RhfTextarea, RhfSelect, RhfCheckbox, RhfCombobox, RhfTextCombobox; 0.22: RhfTimeInput,
|
|
14
|
-
// RhfDateRangePicker, RhfToggleGroup, RhfIbanInput, RhfPhoneInput
|
|
14
|
+
// RhfDateRangePicker, RhfToggleGroup, RhfIbanInput, RhfPhoneInput; 0.23: RhfCountrySelect,
|
|
15
|
+
// RhfMonthPicker)
|
|
15
16
|
// and RhfField, the shell they are built on.
|
|
16
17
|
export * from "./rhf/fields";
|
|
17
18
|
// LineItems over useFieldArray.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useLayoutEffect, useRef } from "react";
|
|
1
|
+
import { useId, useLayoutEffect, useRef } from "react";
|
|
2
2
|
import type { KeyboardEvent as ReactKeyboardEvent, ReactNode } from "react";
|
|
3
3
|
import { Link } from "react-router";
|
|
4
4
|
import { cn } from "../lib/cn";
|
|
@@ -11,9 +11,9 @@ import { TOPBAR_TRIGGER_CLASS } from "./topbar-controls";
|
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* One row of a {@link TopBarActionMenu}. An `action` (default) runs `onSelect`, a
|
|
14
|
-
* `link` navigates via react-router,
|
|
15
|
-
* rows close the menu automatically when chosen.
|
|
16
|
-
* actions, keeping the common case terse.
|
|
14
|
+
* `link` navigates via react-router, a `divider` draws a rule, and a `heading` names
|
|
15
|
+
* the rows after it. Action and link rows close the menu automatically when chosen.
|
|
16
|
+
* `kind` may be omitted for actions, keeping the common case terse.
|
|
17
17
|
*
|
|
18
18
|
* The rows are {@link MenuItem}s, so they take its options: `tone: "danger"` for the
|
|
19
19
|
* log-out row, `checked` (+ `checkable`) for a choice, `current` for the link to the
|
|
@@ -46,7 +46,67 @@ export type TopBarMenuEntry =
|
|
|
46
46
|
current?: boolean;
|
|
47
47
|
disabled?: boolean;
|
|
48
48
|
}
|
|
49
|
-
| { kind: "divider"; key: string }
|
|
49
|
+
| { kind: "divider"; key: string }
|
|
50
|
+
| {
|
|
51
|
+
/**
|
|
52
|
+
* A group heading (0.23, keksdose's guided-tours menu): the rows after it, up to
|
|
53
|
+
* the next heading, the next `divider` or the end of `entries`, are ONE group
|
|
54
|
+
* named by `label`. keksdose files its tours by topic and separated the blocks
|
|
55
|
+
* with bare dividers — "the kit's menu has no heading row to name them with"
|
|
56
|
+
* (tour-menu.tsx) — so a sighted reader had to guess what each block was and a
|
|
57
|
+
* screen reader was told nothing at all.
|
|
58
|
+
*
|
|
59
|
+
* Not a row: it takes no focus, the arrow keys (and Home/End) pass over it, and it
|
|
60
|
+
* never closes the menu. It is announced the way the kit's other grouped lists
|
|
61
|
+
* announce theirs (`CommandPalette`): the group's rows sit in a `role="group"`
|
|
62
|
+
* whose `aria-labelledby` is this heading, so a reader says "On this page, group"
|
|
63
|
+
* when the focus enters the first row, and nothing between rows.
|
|
64
|
+
*
|
|
65
|
+
* A divider ENDS the group, so a closing row such as "All tours…" behind a rule
|
|
66
|
+
* belongs to no topic. A heading with no rows under it is not drawn — an empty
|
|
67
|
+
* group names nothing; build the list from data and it may well be empty.
|
|
68
|
+
*
|
|
69
|
+
* The menu-wide `heading` prop is the panel's title, drawn once above everything;
|
|
70
|
+
* with group headings it is usually redundant (two stacked caps lines read as one),
|
|
71
|
+
* and the trigger's `ariaLabel` already names the menu.
|
|
72
|
+
*/
|
|
73
|
+
kind: "heading";
|
|
74
|
+
key: string;
|
|
75
|
+
/** The group's name, shown above its rows and announced as the group's name. Keep
|
|
76
|
+
* it short: it is set in small caps and wraps on a narrow panel. */
|
|
77
|
+
label: ReactNode;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
type HeadingEntry = Extract<TopBarMenuEntry, { kind: "heading" }>;
|
|
81
|
+
type DividerEntry = Extract<TopBarMenuEntry, { kind: "divider" }>;
|
|
82
|
+
type RowEntry = Exclude<TopBarMenuEntry, HeadingEntry | DividerEntry>;
|
|
83
|
+
|
|
84
|
+
/** What the panel draws from `entries`: loose rows and rules, and headed groups. */
|
|
85
|
+
type Block = { kind: "loose"; entry: RowEntry | DividerEntry } | { kind: "group"; heading: HeadingEntry; rows: RowEntry[] };
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Cut `entries` into blocks: a heading opens a group that takes the rows after it until
|
|
89
|
+
* the next heading or divider; everything else stands alone, as it always has.
|
|
90
|
+
*/
|
|
91
|
+
function toBlocks(entries: readonly TopBarMenuEntry[]): Block[] {
|
|
92
|
+
const blocks: Block[] = [];
|
|
93
|
+
let group: Extract<Block, { kind: "group" }> | null = null;
|
|
94
|
+
for (const entry of entries) {
|
|
95
|
+
if (entry.kind === "heading") {
|
|
96
|
+
group = { kind: "group", heading: entry, rows: [] };
|
|
97
|
+
blocks.push(group);
|
|
98
|
+
} else if (entry.kind === "divider") {
|
|
99
|
+
group = null;
|
|
100
|
+
blocks.push({ kind: "loose", entry });
|
|
101
|
+
} else if (group) {
|
|
102
|
+
group.rows.push(entry);
|
|
103
|
+
} else {
|
|
104
|
+
blocks.push({ kind: "loose", entry });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// A heading with nothing under it names nothing: not drawn (see the `heading` kind).
|
|
108
|
+
return blocks.filter((block) => block.kind === "loose" || block.rows.length > 0);
|
|
109
|
+
}
|
|
50
110
|
|
|
51
111
|
/**
|
|
52
112
|
* The non-interactive identity block at the top of an account menu — kastlan's name +
|
|
@@ -130,6 +190,11 @@ function keepTabInsidePanel(e: ReactKeyboardEvent<HTMLUListElement>) {
|
|
|
130
190
|
* It is also the ACCOUNT menu kastlan and keksdose each hand-build on `HoverMenu`: a
|
|
131
191
|
* custom `trigger` (the avatar), a `header` (name + role / email), the rows, and a
|
|
132
192
|
* free-content `footer` (the legal links).
|
|
193
|
+
*
|
|
194
|
+
* A long menu is grouped by topic with `{ kind: "heading" }` entries (0.23) — see
|
|
195
|
+
* {@link TopBarMenuEntry}. There is no separate phone layout: the panel is the same
|
|
196
|
+
* hover panel at 390px, kept inside the viewport by `HoverMenu`, and a heading wraps
|
|
197
|
+
* rather than widening it.
|
|
133
198
|
*/
|
|
134
199
|
export function TopBarActionMenu({
|
|
135
200
|
icon,
|
|
@@ -206,6 +271,10 @@ export function TopBarActionMenu({
|
|
|
206
271
|
align?: "start" | "end" | "left" | "right";
|
|
207
272
|
panelClassName?: string;
|
|
208
273
|
}) {
|
|
274
|
+
// Names the groups' headings for `aria-labelledby`. Positional rather than from the
|
|
275
|
+
// entry's `key`, which is the caller's text and may hold a space — and a space in
|
|
276
|
+
// `aria-labelledby` is a list separator.
|
|
277
|
+
const uid = useId();
|
|
209
278
|
return (
|
|
210
279
|
<HoverMenu
|
|
211
280
|
ariaLabel={ariaLabel}
|
|
@@ -258,6 +327,40 @@ export function TopBarActionMenu({
|
|
|
258
327
|
const headerExtra = typeof header?.extra === "function" ? header.extra(close) : header?.extra;
|
|
259
328
|
const footerClass =
|
|
260
329
|
"flex flex-wrap gap-x-3 gap-y-1 text-xs text-[var(--text-muted)] [&_a:hover]:text-[var(--text-primary)] [&_a]:rounded-sm [&_a:focus-visible]:outline-none [&_a:focus-visible]:ring-2 [&_a:focus-visible]:ring-[var(--brand)]";
|
|
330
|
+
/** One action or link row — loose, or inside a heading's group. */
|
|
331
|
+
const row = (entry: RowEntry) => (
|
|
332
|
+
<li key={entry.key}>
|
|
333
|
+
{entry.kind === "link" ? (
|
|
334
|
+
<MenuItem
|
|
335
|
+
href={entry.to}
|
|
336
|
+
renderLink={({ href, ...p }) => <Link to={href} {...p} />}
|
|
337
|
+
onClick={close}
|
|
338
|
+
leading={entry.icon}
|
|
339
|
+
trailing={entry.trailing}
|
|
340
|
+
tone={entry.tone}
|
|
341
|
+
current={entry.current}
|
|
342
|
+
disabled={entry.disabled}
|
|
343
|
+
>
|
|
344
|
+
{entry.label}
|
|
345
|
+
</MenuItem>
|
|
346
|
+
) : (
|
|
347
|
+
<MenuItem
|
|
348
|
+
onClick={() => {
|
|
349
|
+
entry.onSelect();
|
|
350
|
+
close();
|
|
351
|
+
}}
|
|
352
|
+
leading={entry.icon}
|
|
353
|
+
trailing={entry.trailing}
|
|
354
|
+
tone={entry.tone}
|
|
355
|
+
checked={entry.checked}
|
|
356
|
+
checkable={entry.checkable}
|
|
357
|
+
disabled={entry.disabled}
|
|
358
|
+
>
|
|
359
|
+
{entry.label}
|
|
360
|
+
</MenuItem>
|
|
361
|
+
)}
|
|
362
|
+
</li>
|
|
363
|
+
);
|
|
261
364
|
return (
|
|
262
365
|
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- a Tab bubbling up from a row or footer link, see keepTabInsidePanel
|
|
263
366
|
<ul className={cn("py-1", panelClassName)} onKeyDown={keepTabInsidePanel}>
|
|
@@ -275,41 +378,34 @@ export function TopBarActionMenu({
|
|
|
275
378
|
{heading}
|
|
276
379
|
</li>
|
|
277
380
|
)}
|
|
278
|
-
{entries.map((
|
|
279
|
-
if (
|
|
280
|
-
|
|
381
|
+
{toBlocks(entries).map((block, i, blocks) => {
|
|
382
|
+
if (block.kind === "loose") {
|
|
383
|
+
const entry = block.entry;
|
|
384
|
+
if (entry.kind === "divider") {
|
|
385
|
+
return <li key={entry.key} className="my-1 border-t border-[var(--border)]" />;
|
|
386
|
+
}
|
|
387
|
+
return row(entry);
|
|
281
388
|
}
|
|
389
|
+
const headingId = `${uid}-group-${i}`;
|
|
390
|
+
const before = blocks[i - 1];
|
|
391
|
+
// Under a rule (or at the top of the rows) the rule's margin is the gap;
|
|
392
|
+
// straight under another group's last row the heading makes its own.
|
|
393
|
+
const tight = !before || (before.kind === "loose" && before.entry.kind === "divider");
|
|
282
394
|
return (
|
|
283
|
-
<li key={
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
) : (
|
|
298
|
-
<MenuItem
|
|
299
|
-
onClick={() => {
|
|
300
|
-
entry.onSelect();
|
|
301
|
-
close();
|
|
302
|
-
}}
|
|
303
|
-
leading={entry.icon}
|
|
304
|
-
trailing={entry.trailing}
|
|
305
|
-
tone={entry.tone}
|
|
306
|
-
checked={entry.checked}
|
|
307
|
-
checkable={entry.checkable}
|
|
308
|
-
disabled={entry.disabled}
|
|
309
|
-
>
|
|
310
|
-
{entry.label}
|
|
311
|
-
</MenuItem>
|
|
312
|
-
)}
|
|
395
|
+
<li key={block.heading.key} role="none">
|
|
396
|
+
<div
|
|
397
|
+
id={headingId}
|
|
398
|
+
role="presentation"
|
|
399
|
+
className={cn(
|
|
400
|
+
"px-3 pb-0.5 text-[11px] font-semibold uppercase tracking-wide text-[var(--text-muted)] [overflow-wrap:anywhere]",
|
|
401
|
+
tight ? "pt-1" : "pt-2.5",
|
|
402
|
+
)}
|
|
403
|
+
>
|
|
404
|
+
{block.heading.label}
|
|
405
|
+
</div>
|
|
406
|
+
<ul role="group" aria-labelledby={headingId}>
|
|
407
|
+
{block.rows.map(row)}
|
|
408
|
+
</ul>
|
|
313
409
|
</li>
|
|
314
410
|
);
|
|
315
411
|
})}
|
|
@@ -31,7 +31,7 @@ import type { UseWizardReturn, WizardLabels } from "./types";
|
|
|
31
31
|
* `isCommitStep`), so the policy options — `cancellable`, `onExit`, `commits`,
|
|
32
32
|
* `onDone` — are all set on `useWizard`. What is set HERE is how the Finish button
|
|
33
33
|
* looks and what wraps it, because that is presentation and usually depends on
|
|
34
|
-
* state the app holds (a "replace everything" toggle, a write lock).
|
|
34
|
+
* state the app holds (a "replace everything" toggle, a write lock — `finishCommit`).
|
|
35
35
|
*/
|
|
36
36
|
export function StepperNav<TData extends Record<string, unknown>>({
|
|
37
37
|
wizard,
|
|
@@ -42,6 +42,8 @@ export function StepperNav<TData extends Record<string, unknown>>({
|
|
|
42
42
|
labels,
|
|
43
43
|
finishVariant = "brand",
|
|
44
44
|
finishDisabled = false,
|
|
45
|
+
finishCommit,
|
|
46
|
+
finishDisabledReason,
|
|
45
47
|
doneDisabled = false,
|
|
46
48
|
renderFinish,
|
|
47
49
|
}: {
|
|
@@ -57,6 +59,30 @@ export function StepperNav<TData extends Record<string, unknown>>({
|
|
|
57
59
|
/** Disable Finish for a reason of the app's own, on top of the kit's gates
|
|
58
60
|
* (`canFinish`, submitting, validating). */
|
|
59
61
|
finishDisabled?: boolean;
|
|
62
|
+
/**
|
|
63
|
+
* Finish COMMITS — {@link Button}'s `commit`, handed to the Finish button. Under a
|
|
64
|
+
* locked {@link WriteLockProvider} it is `aria-disabled` but focusable, with the lock's
|
|
65
|
+
* reason in the kit Tooltip, and a click (or Enter) does not run `wizard.finish`.
|
|
66
|
+
* Every other button of the bar stays live: the steps before the commit write nothing,
|
|
67
|
+
* so the whole wizard can still be walked through on a read-only page.
|
|
68
|
+
*
|
|
69
|
+
* keksdose G1: its YNAB import runs the mapping wizard on the read-only demo (dev#497)
|
|
70
|
+
* and locks only the confirm step's Finish — by cloning `commit` into the element
|
|
71
|
+
* `renderFinish` hands it, because StepperNav took no `commit` of its own. That clone
|
|
72
|
+
* still works; this is the same thing without one. Named `finishCommit` beside
|
|
73
|
+
* `finishVariant` / `finishDisabled` rather than a bare `commit`, which on a component
|
|
74
|
+
* with five buttons would not say which one saves — and would read like `useWizard`'s
|
|
75
|
+
* `commits`, which is a step's policy, not a lock.
|
|
76
|
+
*/
|
|
77
|
+
finishCommit?: boolean;
|
|
78
|
+
/**
|
|
79
|
+
* Why Finish is not available — {@link Button}'s `disabledReason` on the Finish button:
|
|
80
|
+
* `aria-disabled` and focusable, the reason in the kit Tooltip and on the button's
|
|
81
|
+
* `aria-describedby`, `wizard.finish` not run. The explained form of `finishDisabled`
|
|
82
|
+
* (a native `disabled` takes the button out of the tab order, so its reason never
|
|
83
|
+
* reaches a keyboard). Under a lock with `finishCommit`, the lock's reason wins.
|
|
84
|
+
*/
|
|
85
|
+
finishDisabledReason?: ReactNode;
|
|
60
86
|
/**
|
|
61
87
|
* Disable Done — the button of the last step after a commit — while the step still
|
|
62
88
|
* has work of its own running. keksdose's YNAB import ends on a recurring step whose
|
|
@@ -82,6 +108,10 @@ export function StepperNav<TData extends Record<string, unknown>>({
|
|
|
82
108
|
* re-derive (the problem this component exists to end); `onBeforeFinish` can veto
|
|
83
109
|
* a click but cannot explain a disabled button. The element is a plain `<Button>`,
|
|
84
110
|
* so a wrapper that clones it with `disabled` works as expected.
|
|
111
|
+
*
|
|
112
|
+
* For the write lock itself there is no need to wrap any more: `finishCommit` (and
|
|
113
|
+
* `finishDisabledReason`) put the kit Button's own Tooltip on Finish. An element
|
|
114
|
+
* cloned with `commit` here keeps working the same way.
|
|
85
115
|
*/
|
|
86
116
|
renderFinish?: (button: ReactElement) => ReactNode;
|
|
87
117
|
}) {
|
|
@@ -134,6 +164,10 @@ export function StepperNav<TData extends Record<string, unknown>>({
|
|
|
134
164
|
variant={finishVariant}
|
|
135
165
|
data-tour="wizard-finish"
|
|
136
166
|
onClick={wizard.finish}
|
|
167
|
+
// The kit Button's lock: a reason (its own or, with `commit`, the provider's)
|
|
168
|
+
// wins over `disabled` and keeps the button reachable, saying why.
|
|
169
|
+
commit={finishCommit}
|
|
170
|
+
disabledReason={finishDisabledReason}
|
|
137
171
|
disabled={wizard.isSubmitting || pending || !canFinish || finishDisabled}
|
|
138
172
|
aria-busy={wizard.isSubmitting || pending || undefined}
|
|
139
173
|
>
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import * as react from 'react';
|
|
2
|
-
import { ReactNode } from 'react';
|
|
3
|
-
|
|
4
|
-
/** `undefined`, `null`, `false` and `""` are what a caller's `cond && "…"` evaluates to
|
|
5
|
-
* when there is nothing to say; none of them is content. A numeric `0` is. */
|
|
6
|
-
declare function hasContent(node: ReactNode): boolean;
|
|
7
|
-
/**
|
|
8
|
-
* A `hint` that is plain TEXT is a caption under the field; anything else (a
|
|
9
|
-
* `FieldHint` "?") rides the label line — the rule `Select` set in 0.15: the label
|
|
10
|
-
* line is 11px of strip shared with the label, and a sentence placed there was set on
|
|
11
|
-
* top of both the label and the value.
|
|
12
|
-
*/
|
|
13
|
-
declare function isTextHint(hint: ReactNode): boolean;
|
|
14
|
-
interface FieldMessages {
|
|
15
|
-
/** `invalid`, a message, or an `aria-invalid` from outside (a form library). */
|
|
16
|
-
isInvalid: boolean;
|
|
17
|
-
/** The caller's description, then the caption, then the error — the standing
|
|
18
|
-
* advice before the news, as `useFieldError` in ui.tsx orders them. */
|
|
19
|
-
describedBy: string | undefined;
|
|
20
|
-
/** The caption and the error, for under the field; `null` when there is neither. */
|
|
21
|
-
below: ReactNode;
|
|
22
|
-
/** The hint for the LABEL line (a `FieldHint`), or `undefined` when the hint is a
|
|
23
|
-
* caption or there is none. */
|
|
24
|
-
labelHint: ReactNode;
|
|
25
|
-
}
|
|
26
|
-
/** `hint` / `error` / `invalid` resolved the way `Select` resolves them. */
|
|
27
|
-
declare function useFieldMessages({ hint, error, invalid, describedBy, ariaInvalid, }: {
|
|
28
|
-
hint?: ReactNode;
|
|
29
|
-
error?: ReactNode;
|
|
30
|
-
invalid?: boolean;
|
|
31
|
-
describedBy?: string;
|
|
32
|
-
ariaInvalid?: unknown;
|
|
33
|
-
}): FieldMessages;
|
|
34
|
-
/**
|
|
35
|
-
* The field and what is under it, as one box — `FieldGroup` in ui.tsx, for the same
|
|
36
|
-
* reasons written there. Nothing of its own while there is nothing under the field and
|
|
37
|
-
* the caller passed no `error` key (the DOM is exactly what it was before these props);
|
|
38
|
-
* once `error` is passed the box is kept even with no message, so the trigger is never
|
|
39
|
-
* reparented — and remounted, losing focus — as the message comes and goes.
|
|
40
|
-
*/
|
|
41
|
-
declare function FieldBox({ below, reserve, children, }: {
|
|
42
|
-
below: ReactNode;
|
|
43
|
-
reserve: boolean;
|
|
44
|
-
children: ReactNode;
|
|
45
|
-
}): react.JSX.Element;
|
|
46
|
-
/**
|
|
47
|
-
* The static label in a trigger field's top strip — a {@link FieldLabel}, or, with a
|
|
48
|
-
* `FieldHint`, the label and its "?" laid out on ONE flex row (dev#468: two absolute
|
|
49
|
-
* offsets guessing at the same baseline put the "?" three pixels above its word).
|
|
50
|
-
* `FloatingField`'s static-label row, except that it stops at `end-9`: the date and
|
|
51
|
-
* month triggers keep that column (`pe-9`) for the calendar glyph or the clear ×, and a
|
|
52
|
-
* long label truncates before it rather than putting the "?" on top of it.
|
|
53
|
-
*/
|
|
54
|
-
declare function FieldLabelLine({ label, hint }: {
|
|
55
|
-
label: ReactNode;
|
|
56
|
-
hint?: ReactNode;
|
|
57
|
-
}): react.JSX.Element;
|
|
58
|
-
/**
|
|
59
|
-
* How tall the strip over a bare group is.
|
|
60
|
-
*
|
|
61
|
-
* - `"toggle"` — 16px. keksdose's `ClearedStatusPicker` (live #288 rework, #431,
|
|
62
|
-
* measured): *"16px of label strip plus a 26px group is exactly a labelled Input"*,
|
|
63
|
-
* so a status group in a row of 42px fields ends where they end. The label's line
|
|
64
|
-
* box runs into the group's top hairline, but the ACTIVE fill — the thing feedback
|
|
65
|
-
* #117 was about — starts at 19px, below the label's 17.75px.
|
|
66
|
-
* - `"tiles"` — 20px, keksdose's `FlagPicker` and location field (`pt-5`). A tile has
|
|
67
|
-
* no inset fill: its selection IS its frame, a 2px dark border-and-ring from the
|
|
68
|
-
* tile's top edge, so the tile has to start below the whole label line.
|
|
69
|
-
*/
|
|
70
|
-
declare const LABEL_STRIP_PAD: {
|
|
71
|
-
readonly toggle: "pt-4";
|
|
72
|
-
readonly tiles: "pt-5";
|
|
73
|
-
};
|
|
74
|
-
/**
|
|
75
|
-
* The 11px label (and its `FieldHint`) over a group that is NOT a field — keksdose K2:
|
|
76
|
-
* a segmented control or a row of swatches standing in a form row beside labelled
|
|
77
|
-
* inputs, whose label must sit on the same line as theirs (`top-1`, `inset-x-3`, the
|
|
78
|
-
* type of a field's static label) without the group growing a field's border round
|
|
79
|
-
* it. `id` goes on the label so the group can be `aria-labelledby` it.
|
|
80
|
-
*
|
|
81
|
-
* `relative` is load-bearing twice over: the row is positioned against it, and a
|
|
82
|
-
* caller's `sr-only` label cannot escape it (sr-only-containment).
|
|
83
|
-
*/
|
|
84
|
-
declare function LabelStrip({ labelId, label, hint, disabled, pad, className, children, }: {
|
|
85
|
-
labelId: string;
|
|
86
|
-
label: ReactNode;
|
|
87
|
-
hint?: ReactNode;
|
|
88
|
-
/** Dim the label with its group, as `peer-disabled:` dims a field's own. */
|
|
89
|
-
disabled?: boolean;
|
|
90
|
-
pad: keyof typeof LABEL_STRIP_PAD;
|
|
91
|
-
className?: string;
|
|
92
|
-
children: ReactNode;
|
|
93
|
-
}): react.JSX.Element;
|
|
94
|
-
|
|
95
|
-
export { FieldBox, FieldLabelLine, type FieldMessages, LABEL_STRIP_PAD, LabelStrip, hasContent, isTextHint, useFieldMessages };
|