@eifi1/ui-kit 0.11.0 → 0.12.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 +57 -32
- package/dist/chart.d.ts +3 -1
- package/dist/chart.js +2 -0
- package/dist/chart.js.map +1 -1
- package/dist/components/account-settings-labels.d.ts +105 -0
- package/dist/components/account-settings-labels.js +81 -0
- package/dist/components/account-settings-labels.js.map +1 -0
- package/dist/components/account-settings.d.ts +36 -52
- package/dist/components/account-settings.js +44 -8
- package/dist/components/account-settings.js.map +1 -1
- package/dist/components/amount-input.d.ts +24 -4
- package/dist/components/amount-input.js +5 -2
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/authed-image.d.ts +68 -0
- package/dist/components/authed-image.js +117 -0
- package/dist/components/authed-image.js.map +1 -0
- package/dist/components/breadcrumbs.d.ts +2 -1
- package/dist/components/breadcrumbs.js +5 -2
- package/dist/components/breadcrumbs.js.map +1 -1
- package/dist/components/button-group.d.ts +51 -2
- package/dist/components/button-group.js +5 -1
- package/dist/components/button-group.js.map +1 -1
- package/dist/components/calculator.d.ts +15 -4
- package/dist/components/calendar-heatmap.d.ts +41 -3
- package/dist/components/calendar-heatmap.js +48 -7
- package/dist/components/calendar-heatmap.js.map +1 -1
- package/dist/components/chip.d.ts +2 -1
- package/dist/components/chip.js +6 -3
- package/dist/components/chip.js.map +1 -1
- package/dist/components/choice-card.d.ts +35 -3
- package/dist/components/choice-card.js +83 -25
- package/dist/components/choice-card.js.map +1 -1
- package/dist/components/combobox-core.js +2 -0
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/combobox.d.ts +2 -2
- package/dist/components/combobox.js +31 -9
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/copy-button.d.ts +51 -70
- package/dist/components/copy-button.js +1 -1
- package/dist/components/copy-button.js.map +1 -1
- package/dist/components/data-table-cells.d.ts +63 -0
- package/dist/components/data-table-cells.js +64 -0
- package/dist/components/data-table-cells.js.map +1 -0
- package/dist/components/data-table-filter-popover.d.ts +1 -1
- package/dist/components/data-table-filters.d.ts +1 -1
- package/dist/components/data-table-filters.js +31 -1
- package/dist/components/data-table-filters.js.map +1 -1
- package/dist/components/data-table-labels.d.ts +13 -0
- package/dist/components/data-table-labels.js +8 -1
- package/dist/components/data-table-labels.js.map +1 -1
- package/dist/components/data-table.d.ts +1 -1
- package/dist/components/data-table.js +102 -12
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-picker.d.ts +3 -1
- package/dist/components/date-picker.js +15 -2
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/description-list.d.ts +23 -3
- package/dist/components/description-list.js +23 -3
- package/dist/components/description-list.js.map +1 -1
- package/dist/components/dialog-frame.d.ts +1 -0
- package/dist/components/entity-combobox.d.ts +1 -1
- package/dist/components/entity-combobox.js +18 -3
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/error-boundary.d.ts +92 -0
- package/dist/components/error-boundary.js +111 -0
- package/dist/components/error-boundary.js.map +1 -0
- package/dist/components/field.d.ts +30 -4
- package/dist/components/field.js +40 -17
- package/dist/components/field.js.map +1 -1
- package/dist/components/file-button.d.ts +50 -208
- package/dist/components/file-dropzone.d.ts +49 -2
- package/dist/components/floating-panel.d.ts +13 -4
- package/dist/components/floating-panel.js +7 -3
- package/dist/components/floating-panel.js.map +1 -1
- package/dist/components/form-actions.d.ts +53 -0
- package/dist/components/form-actions.js +98 -0
- package/dist/components/form-actions.js.map +1 -0
- package/dist/components/image-grid.d.ts +67 -0
- package/dist/components/image-grid.js +112 -0
- package/dist/components/image-grid.js.map +1 -0
- package/dist/components/lightbox.d.ts +104 -0
- package/dist/components/lightbox.js +229 -0
- package/dist/components/lightbox.js.map +1 -0
- package/dist/components/line-items.d.ts +114 -0
- package/dist/components/line-items.js +238 -0
- package/dist/components/line-items.js.map +1 -0
- package/dist/components/list.d.ts +23 -2
- package/dist/components/list.js +32 -13
- package/dist/components/list.js.map +1 -1
- package/dist/components/loading-state.d.ts +24 -0
- package/dist/components/loading-state.js +32 -0
- package/dist/components/loading-state.js.map +1 -0
- package/dist/components/menu-item.d.ts +2 -1
- package/dist/components/menu-item.js +5 -1
- package/dist/components/menu-item.js.map +1 -1
- package/dist/components/mini-calendar.d.ts +13 -1
- package/dist/components/mini-calendar.js +46 -2
- package/dist/components/mini-calendar.js.map +1 -1
- package/dist/components/modal.d.ts +20 -1
- package/dist/components/modal.js +26 -2
- package/dist/components/modal.js.map +1 -1
- package/dist/components/month-picker.d.ts +21 -1
- package/dist/components/month-picker.js +139 -26
- package/dist/components/month-picker.js.map +1 -1
- package/dist/components/multi-entity-combobox.d.ts +1 -1
- package/dist/components/multi-entity-combobox.js +19 -3
- package/dist/components/multi-entity-combobox.js.map +1 -1
- package/dist/components/nav-pills.d.ts +2 -1
- package/dist/components/nav-pills.js +5 -1
- package/dist/components/nav-pills.js.map +1 -1
- package/dist/components/number-field.d.ts +24 -5
- package/dist/components/number-field.js +5 -7
- package/dist/components/number-field.js.map +1 -1
- package/dist/components/number-input.d.ts +25 -5
- package/dist/components/number-input.js +7 -1
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numpad-sheet.d.ts +15 -4
- package/dist/components/passkeys-setting.d.ts +67 -0
- package/dist/components/passkeys-setting.js +214 -0
- package/dist/components/passkeys-setting.js.map +1 -0
- package/dist/components/picker-sheet.js +2 -1
- package/dist/components/picker-sheet.js.map +1 -1
- package/dist/components/pie-chart-labels.d.ts +20 -0
- package/dist/components/pie-chart-labels.js +12 -0
- package/dist/components/pie-chart-labels.js.map +1 -0
- package/dist/components/pie-chart.d.ts +101 -0
- package/dist/components/pie-chart.js +283 -0
- package/dist/components/pie-chart.js.map +1 -0
- package/dist/components/progress-bar.d.ts +44 -3
- package/dist/components/progress-bar.js +58 -22
- package/dist/components/progress-bar.js.map +1 -1
- package/dist/components/qr-code.d.ts +34 -0
- package/dist/components/qr-code.js +57 -0
- package/dist/components/qr-code.js.map +1 -0
- package/dist/components/search-field.d.ts +15 -3
- package/dist/components/search-field.js +5 -1
- package/dist/components/search-field.js.map +1 -1
- package/dist/components/settings-fields.d.ts +49 -1
- package/dist/components/signed-amount.d.ts +121 -0
- package/dist/components/signed-amount.js +152 -0
- package/dist/components/signed-amount.js.map +1 -0
- package/dist/components/skeleton.d.ts +10 -3
- package/dist/components/skeleton.js +12 -2
- package/dist/components/skeleton.js.map +1 -1
- package/dist/components/stat-tile.d.ts +2 -1
- package/dist/components/stat-tile.js +5 -2
- package/dist/components/stat-tile.js.map +1 -1
- package/dist/components/table.d.ts +39 -5
- package/dist/components/table.js +45 -11
- package/dist/components/table.js.map +1 -1
- package/dist/components/text-link.d.ts +176 -0
- package/dist/components/text-link.js +109 -0
- package/dist/components/text-link.js.map +1 -0
- package/dist/components/time-input.d.ts +49 -1
- package/dist/components/toggle-group.d.ts +13 -0
- package/dist/components/toggle-group.js +35 -6
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/toggle-legend.d.ts +36 -3
- package/dist/components/toggle-legend.js +49 -0
- package/dist/components/toggle-legend.js.map +1 -1
- package/dist/components/ui.d.ts +51 -779
- package/dist/components/ui.js +215 -23
- package/dist/components/ui.js.map +1 -1
- package/dist/components/use-table-state.d.ts +3 -52
- package/dist/components/use-table-state.js +128 -30
- package/dist/components/use-table-state.js.map +1 -1
- package/dist/{data-table-filters-Dh9uF_S-.d.ts → data-table-Drun8Fk9.d.ts} +305 -61
- package/dist/data-table.d.ts +2 -1
- package/dist/data-table.js +5 -1
- package/dist/data-table.js.map +1 -1
- package/dist/feedback/feedback-dialog.d.ts +26 -3
- package/dist/feedback/feedback-dialog.js +16 -2
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback/feedback-thread.d.ts +133 -0
- package/dist/feedback/feedback-thread.js +237 -0
- package/dist/feedback/feedback-thread.js.map +1 -0
- package/dist/feedback.d.ts +2 -1
- package/dist/feedback.js +1 -0
- package/dist/feedback.js.map +1 -1
- package/dist/hooks/use-authed-src.d.ts +71 -0
- package/dist/hooks/use-authed-src.js +63 -0
- package/dist/hooks/use-authed-src.js.map +1 -0
- package/dist/hooks/use-file-drop.d.ts +49 -2
- package/dist/hooks/use-hotkey.d.ts +59 -0
- package/dist/hooks/use-hotkey.js +105 -0
- package/dist/hooks/use-hotkey.js.map +1 -0
- package/dist/hooks/use-search-param-state.d.ts +83 -0
- package/dist/hooks/use-search-param-state.js +94 -0
- package/dist/hooks/use-search-param-state.js.map +1 -0
- package/dist/i18n/defaults.d.ts +15 -4
- package/dist/i18n/defaults.js +31 -1
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +46 -312
- package/dist/i18n/kit-labels.js +20 -5
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/de-CH-informal.d.ts +15 -4
- package/dist/i18n/locales/de-CH.d.ts +15 -4
- package/dist/i18n/locales/de-informal.d.ts +15 -4
- package/dist/i18n/locales/de.d.ts +15 -4
- package/dist/i18n/locales/de.js +156 -3
- package/dist/i18n/locales/de.js.map +1 -1
- package/dist/i18n/locales/es.d.ts +15 -4
- package/dist/i18n/locales/es.js +153 -3
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +15 -4
- package/dist/i18n/locales/fr.js +153 -3
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +15 -4
- package/dist/i18n/locales/hu.js +153 -3
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +15 -4
- package/dist/i18n/locales/it.js +153 -3
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +15 -4
- package/dist/i18n/locales/zh.js +153 -3
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/index.d.ts +37 -17
- package/dist/index.js +40 -1
- package/dist/index.js.map +1 -1
- package/dist/kit-labels-v3biUF1L.d.ts +1645 -0
- package/dist/lib/format.d.ts +144 -0
- package/dist/lib/format.js +149 -0
- package/dist/lib/format.js.map +1 -0
- package/dist/lib/qr-encode.d.ts +44 -0
- package/dist/lib/qr-encode.js +338 -0
- package/dist/lib/qr-encode.js.map +1 -0
- package/dist/rhf/fields.d.ts +241 -0
- package/dist/rhf/fields.js +601 -0
- package/dist/rhf/fields.js.map +1 -0
- package/dist/rhf/form.d.ts +49 -1
- package/dist/rhf/line-items.d.ts +31 -0
- package/dist/rhf/line-items.js +59 -0
- package/dist/rhf/line-items.js.map +1 -0
- package/dist/rhf.d.ts +61 -1
- package/dist/rhf.js +2 -0
- package/dist/rhf.js.map +1 -1
- package/dist/shell/app-shell.d.ts +99 -2
- package/dist/shell/app-shell.js +58 -3
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/shell/auth-layout.d.ts +74 -0
- package/dist/shell/auth-layout.js +97 -0
- package/dist/shell/auth-layout.js.map +1 -0
- package/dist/shell/top-bar-brand.d.ts +81 -0
- package/dist/shell/top-bar-brand.js +47 -0
- package/dist/shell/top-bar-brand.js.map +1 -0
- package/dist/shell/topbar-action-menu.d.ts +12 -1
- package/dist/shell/topbar-action-menu.js +16 -1
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell.d.ts +56 -1
- package/dist/shell.js +2 -0
- package/dist/shell.js.map +1 -1
- package/dist/wizard/stepper-nav.d.ts +48 -1
- package/dist/wizard/wizard-context.d.ts +24 -1
- package/dist/wizard/wizard-context.js +19 -2
- package/dist/wizard/wizard-context.js.map +1 -1
- package/dist/wizard/wizard-step.d.ts +20 -5
- package/dist/wizard/wizard-step.js +16 -3
- package/dist/wizard/wizard-step.js.map +1 -1
- package/dist/wizard.d.ts +50 -3
- package/package.json +1 -1
- package/src/chart.ts +4 -0
- package/src/components/account-settings-labels.ts +190 -0
- package/src/components/account-settings.tsx +110 -63
- package/src/components/amount-input.tsx +13 -1
- package/src/components/authed-image.tsx +190 -0
- package/src/components/breadcrumbs.tsx +8 -2
- package/src/components/button-group.tsx +7 -2
- package/src/components/calendar-heatmap.tsx +105 -10
- package/src/components/chip.tsx +8 -4
- package/src/components/choice-card.tsx +136 -25
- package/src/components/combobox-core.tsx +5 -0
- package/src/components/combobox.tsx +31 -9
- package/src/components/copy-button.tsx +3 -2
- package/src/components/data-table-cells.tsx +119 -0
- package/src/components/data-table-filters.ts +77 -0
- package/src/components/data-table-labels.ts +23 -0
- package/src/components/data-table.tsx +224 -13
- package/src/components/date-picker.tsx +19 -3
- package/src/components/description-list.tsx +52 -3
- package/src/components/entity-combobox.tsx +18 -3
- package/src/components/error-boundary.tsx +216 -0
- package/src/components/field.tsx +56 -5
- package/src/components/floating-panel.tsx +20 -7
- package/src/components/form-actions.tsx +191 -0
- package/src/components/image-grid.tsx +181 -0
- package/src/components/lightbox.tsx +374 -0
- package/src/components/line-items.tsx +385 -0
- package/src/components/list.tsx +65 -16
- package/src/components/loading-state.tsx +47 -0
- package/src/components/menu-item.tsx +7 -2
- package/src/components/mini-calendar.tsx +72 -3
- package/src/components/modal.tsx +61 -3
- package/src/components/month-picker.tsx +181 -26
- package/src/components/multi-entity-combobox.tsx +19 -3
- package/src/components/nav-pills.tsx +8 -1
- package/src/components/number-field.tsx +15 -10
- package/src/components/number-input.tsx +16 -1
- package/src/components/passkeys-setting.tsx +331 -0
- package/src/components/picker-sheet.tsx +7 -1
- package/src/components/pie-chart-labels.ts +32 -0
- package/src/components/pie-chart.tsx +479 -0
- package/src/components/progress-bar.tsx +129 -19
- package/src/components/qr-code.tsx +83 -0
- package/src/components/search-field.tsx +20 -4
- package/src/components/signed-amount.tsx +286 -0
- package/src/components/skeleton.tsx +21 -3
- package/src/components/stat-tile.tsx +8 -2
- package/src/components/table.tsx +102 -13
- package/src/components/text-link.tsx +248 -0
- package/src/components/toggle-group.tsx +58 -10
- package/src/components/toggle-legend.tsx +87 -2
- package/src/components/ui.tsx +426 -36
- package/src/components/use-table-state.ts +265 -33
- package/src/data-table.ts +10 -0
- package/src/feedback/feedback-dialog.tsx +42 -3
- package/src/feedback/feedback-thread.tsx +434 -0
- package/src/feedback.ts +1 -0
- package/src/hooks/use-authed-src.ts +161 -0
- package/src/hooks/use-hotkey.ts +172 -0
- package/src/hooks/use-search-param-state.ts +187 -0
- package/src/i18n/defaults.ts +30 -0
- package/src/i18n/kit-labels.tsx +102 -8
- package/src/i18n/locales/de.ts +153 -0
- package/src/i18n/locales/es.ts +150 -0
- package/src/i18n/locales/fr.ts +153 -0
- package/src/i18n/locales/hu.ts +151 -0
- package/src/i18n/locales/it.ts +151 -0
- package/src/i18n/locales/zh.ts +150 -0
- package/src/index.ts +59 -0
- package/src/lib/format.ts +325 -0
- package/src/lib/qr-encode.ts +420 -0
- package/src/rhf/fields.tsx +1054 -0
- package/src/rhf/line-items.tsx +130 -0
- package/src/rhf.ts +6 -0
- package/src/shell/app-shell.tsx +147 -2
- package/src/shell/auth-layout.tsx +190 -0
- package/src/shell/top-bar-brand.tsx +76 -0
- package/src/shell/topbar-action-menu.tsx +21 -0
- package/src/shell.ts +2 -0
- package/src/wizard/wizard-context.tsx +41 -1
- package/src/wizard/wizard-step.tsx +43 -6
- package/tokens.css +12 -0
|
@@ -91,6 +91,8 @@ export interface DateTriggerAttributes {
|
|
|
91
91
|
"aria-label"?: string;
|
|
92
92
|
"aria-describedby"?: string;
|
|
93
93
|
"aria-invalid"?: true;
|
|
94
|
+
/** The field's required state — `Field required` hands it down with the id pair. */
|
|
95
|
+
"aria-required"?: boolean | "true" | "false";
|
|
94
96
|
disabled?: boolean;
|
|
95
97
|
onClick: () => void;
|
|
96
98
|
className: string;
|
|
@@ -132,6 +134,7 @@ function DateFieldTrigger({
|
|
|
132
134
|
padded,
|
|
133
135
|
disabled,
|
|
134
136
|
invalid,
|
|
137
|
+
required,
|
|
135
138
|
panelId,
|
|
136
139
|
aria,
|
|
137
140
|
renderTrigger,
|
|
@@ -153,6 +156,8 @@ function DateFieldTrigger({
|
|
|
153
156
|
padded: boolean;
|
|
154
157
|
disabled?: boolean;
|
|
155
158
|
invalid?: boolean;
|
|
159
|
+
/** `aria-required`, on the trigger with the rest of the field's ARIA. */
|
|
160
|
+
required?: boolean | "true" | "false";
|
|
156
161
|
}) {
|
|
157
162
|
const wasOpen = useRef(false);
|
|
158
163
|
useEffect(() => {
|
|
@@ -177,6 +182,7 @@ function DateFieldTrigger({
|
|
|
177
182
|
// One attribute object, worn either by the kit's own button below or by a caller's
|
|
178
183
|
// (`renderTrigger`) — so a custom trigger cannot quietly fall out of step with the
|
|
179
184
|
// naming and state this one announces.
|
|
185
|
+
const isInvalid = Boolean(invalid) || aria["aria-invalid"] === true || aria["aria-invalid"] === "true";
|
|
180
186
|
const attrs: DateTriggerAttributes = {
|
|
181
187
|
ref: triggerRef,
|
|
182
188
|
type: "button",
|
|
@@ -209,14 +215,15 @@ function DateFieldTrigger({
|
|
|
209
215
|
"aria-haspopup": "dialog",
|
|
210
216
|
"aria-controls": panelId,
|
|
211
217
|
"aria-expanded": open,
|
|
212
|
-
"aria-invalid":
|
|
213
|
-
|
|
218
|
+
"aria-invalid": isInvalid || undefined,
|
|
219
|
+
"aria-required": required,
|
|
214
220
|
className: cn(
|
|
215
221
|
FIELD_TRIGGER,
|
|
216
222
|
"pe-9",
|
|
217
223
|
padded && FIELD_FLOATING_PAD,
|
|
218
224
|
disabled && "cursor-not-allowed opacity-50",
|
|
219
|
-
invalid
|
|
225
|
+
// An `aria-invalid` from a Field spread paints too, as on Input and AmountInput.
|
|
226
|
+
isInvalid && FIELD_INVALID,
|
|
220
227
|
),
|
|
221
228
|
};
|
|
222
229
|
if (renderTrigger) return <CustomTrigger render={renderTrigger} attrs={attrs} valueId={valueId} />;
|
|
@@ -279,6 +286,10 @@ function DateField({
|
|
|
279
286
|
sheetBackCloses = true,
|
|
280
287
|
renderTrigger,
|
|
281
288
|
children,
|
|
289
|
+
// The trigger's, not the wrapper's: `Field`'s render-prop spreads it with the id
|
|
290
|
+
// pair, and a required date read as optional while it sat on the role-less div.
|
|
291
|
+
// Taken here rather than in `splitTriggerAria`, which MonthPicker shares.
|
|
292
|
+
"aria-required": ariaRequired,
|
|
282
293
|
...rest
|
|
283
294
|
}: Omit<ComponentPropsWithoutRef<"div">, "children"> & {
|
|
284
295
|
triggerText: string;
|
|
@@ -393,6 +404,7 @@ function DateField({
|
|
|
393
404
|
padded={label !== undefined}
|
|
394
405
|
disabled={disabled}
|
|
395
406
|
invalid={invalid}
|
|
407
|
+
required={ariaRequired}
|
|
396
408
|
renderTrigger={renderTrigger}
|
|
397
409
|
/>
|
|
398
410
|
);
|
|
@@ -577,6 +589,9 @@ export function DatePicker({
|
|
|
577
589
|
clearLabel,
|
|
578
590
|
disabled,
|
|
579
591
|
invalid,
|
|
592
|
+
// Named so it reaches the field (and so its trigger) when a step/today row is the
|
|
593
|
+
// root and takes the rest of the caller's props.
|
|
594
|
+
"aria-required": ariaRequired,
|
|
580
595
|
...rest
|
|
581
596
|
}: DatePickerProps) {
|
|
582
597
|
const locale = useKitLocale(localeProp);
|
|
@@ -593,6 +608,7 @@ export function DatePicker({
|
|
|
593
608
|
// With step/today buttons the flex row is the root and takes the caller's props —
|
|
594
609
|
// except the ones that name the field, which still belong on its trigger.
|
|
595
610
|
{...(wrapped ? triggerAria : rest)}
|
|
611
|
+
aria-required={ariaRequired}
|
|
596
612
|
label={label}
|
|
597
613
|
clearable={clearable}
|
|
598
614
|
clearLabel={text.clear}
|
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
import { createContext, useContext } from "react";
|
|
2
2
|
import type { ComponentPropsWithoutRef, CSSProperties, ReactNode } from "react";
|
|
3
3
|
import { cn } from "../lib/cn";
|
|
4
|
+
import { useKitLabels } from "../i18n/kit-labels";
|
|
4
5
|
import { FieldHint } from "./ui";
|
|
5
6
|
|
|
7
|
+
/** The words a {@link DescriptionList} renders on its own behalf. */
|
|
8
|
+
export interface DescriptionListLabels {
|
|
9
|
+
/** What an item with no value shows (see {@link DescriptionItemProps.placeholder}).
|
|
10
|
+
* Default "—", an em dash. */
|
|
11
|
+
empty: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const DEFAULT_DESCRIPTION_LIST_LABELS: DescriptionListLabels = {
|
|
15
|
+
empty: "—",
|
|
16
|
+
};
|
|
17
|
+
|
|
6
18
|
export type DescriptionListLayout = "rows" | "cards" | "stacked";
|
|
7
19
|
export type DescriptionListDensity = "comfortable" | "compact" | "tight";
|
|
8
20
|
/** How many columns a `stacked` or `cards` list may use. */
|
|
@@ -14,6 +26,7 @@ interface ListContextValue {
|
|
|
14
26
|
prose: boolean;
|
|
15
27
|
density: DescriptionListDensity;
|
|
16
28
|
columns: DescriptionListColumns;
|
|
29
|
+
placeholder?: ReactNode;
|
|
17
30
|
}
|
|
18
31
|
|
|
19
32
|
const ListContext = createContext<ListContextValue>({
|
|
@@ -103,6 +116,9 @@ export interface DescriptionListProps extends ComponentPropsWithoutRef<"dl"> {
|
|
|
103
116
|
* all would not be an improvement. Per item: {@link DescriptionItemProps.prose}.
|
|
104
117
|
*/
|
|
105
118
|
prose?: boolean;
|
|
119
|
+
/** What an item with no value shows, for every item that does not set its own —
|
|
120
|
+
* see {@link DescriptionItemProps.placeholder}. */
|
|
121
|
+
placeholder?: ReactNode;
|
|
106
122
|
}
|
|
107
123
|
|
|
108
124
|
/**
|
|
@@ -129,6 +145,7 @@ export function DescriptionList({
|
|
|
129
145
|
density = "comfortable",
|
|
130
146
|
minCardWidth = "10rem",
|
|
131
147
|
columns,
|
|
148
|
+
placeholder,
|
|
132
149
|
className,
|
|
133
150
|
style,
|
|
134
151
|
...rest
|
|
@@ -147,7 +164,7 @@ export function DescriptionList({
|
|
|
147
164
|
const grid: CSSProperties | undefined = cards
|
|
148
165
|
? { gridTemplateColumns: `repeat(auto-fill, minmax(min(100%, ${track}), 1fr))` }
|
|
149
166
|
: undefined;
|
|
150
|
-
const ctx: ListContextValue = { layout, numeric, prose, density, columns: columns ?? 2 };
|
|
167
|
+
const ctx: ListContextValue = { layout, numeric, prose, density, columns: columns ?? 2, placeholder };
|
|
151
168
|
const text = density === "tight" ? "text-[11px] leading-4" : density === "compact" ? "text-xs" : "text-sm";
|
|
152
169
|
|
|
153
170
|
if (stacked) {
|
|
@@ -215,6 +232,20 @@ export interface DescriptionItemProps extends Omit<ComponentPropsWithoutRef<"div
|
|
|
215
232
|
* because a card grid's column count is not known to CSS ahead of layout.
|
|
216
233
|
*/
|
|
217
234
|
span?: number | "full";
|
|
235
|
+
/**
|
|
236
|
+
* Shown in place of a value that is not there — `children` of `null`, `undefined`,
|
|
237
|
+
* `false` or `""` (a `0` is a value and is shown). Default: the list's own
|
|
238
|
+
* `placeholder`, else `descriptionList.empty` from the provider, else "—". `null`
|
|
239
|
+
* shows nothing, as an empty detail did before. kastlan's DetailField put its
|
|
240
|
+
* `EMPTY_VALUE` into every one of its call sites' values by hand.
|
|
241
|
+
*
|
|
242
|
+
* Drawn in the muted ink, so an absent value does not read as a value of "—".
|
|
243
|
+
*/
|
|
244
|
+
placeholder?: ReactNode;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function isEmptyValue(node: ReactNode): boolean {
|
|
248
|
+
return node === undefined || node === null || node === false || node === "";
|
|
218
249
|
}
|
|
219
250
|
|
|
220
251
|
const ROW_PAD: Record<DescriptionListDensity, string> = {
|
|
@@ -236,11 +267,23 @@ export function DescriptionItem({
|
|
|
236
267
|
numeric: numericProp,
|
|
237
268
|
prose: proseProp,
|
|
238
269
|
span,
|
|
270
|
+
placeholder: placeholderProp,
|
|
239
271
|
detailClassName,
|
|
240
272
|
className,
|
|
241
273
|
...rest
|
|
242
274
|
}: DescriptionItemProps) {
|
|
243
|
-
const {
|
|
275
|
+
const {
|
|
276
|
+
layout,
|
|
277
|
+
numeric: listNumeric,
|
|
278
|
+
prose: listProse,
|
|
279
|
+
density,
|
|
280
|
+
columns,
|
|
281
|
+
placeholder: listPlaceholder,
|
|
282
|
+
} = useContext(ListContext);
|
|
283
|
+
const labels = useKitLabels("descriptionList", DEFAULT_DESCRIPTION_LIST_LABELS);
|
|
284
|
+
const empty = isEmptyValue(children);
|
|
285
|
+
const placeholder =
|
|
286
|
+
placeholderProp !== undefined ? placeholderProp : listPlaceholder !== undefined ? listPlaceholder : labels.empty;
|
|
244
287
|
const numeric = numericProp ?? listNumeric;
|
|
245
288
|
const prose = proseProp ?? listProse;
|
|
246
289
|
const cards = layout === "cards";
|
|
@@ -277,7 +320,13 @@ export function DescriptionItem({
|
|
|
277
320
|
detailClassName,
|
|
278
321
|
)}
|
|
279
322
|
>
|
|
280
|
-
{
|
|
323
|
+
{empty && !isEmptyValue(placeholder) ? (
|
|
324
|
+
<span data-slot="description-empty" className="font-normal text-[var(--text-muted)]">
|
|
325
|
+
{placeholder}
|
|
326
|
+
</span>
|
|
327
|
+
) : (
|
|
328
|
+
children
|
|
329
|
+
)}
|
|
281
330
|
</dd>
|
|
282
331
|
);
|
|
283
332
|
|
|
@@ -105,6 +105,14 @@ export function EntityCombobox<V extends string | number, C extends ComboClearVa
|
|
|
105
105
|
debounceMs,
|
|
106
106
|
loadErrorLabel,
|
|
107
107
|
"aria-label": ariaLabel,
|
|
108
|
+
// The control's own wiring, taken off `rest` so it lands on the TRIGGER rather than
|
|
109
|
+
// the wrapper — see MultiEntityCombobox: `Field`'s render-prop spreads `{ id,
|
|
110
|
+
// aria-describedby, aria-invalid, aria-required }`, and on the role-less wrapper
|
|
111
|
+
// div the label's `htmlFor` named nothing and the hint described nothing.
|
|
112
|
+
id,
|
|
113
|
+
"aria-describedby": ariaDescribedBy,
|
|
114
|
+
"aria-invalid": ariaInvalid,
|
|
115
|
+
"aria-required": ariaRequired,
|
|
108
116
|
...rest
|
|
109
117
|
}: EntityComboboxProps<V, C>) {
|
|
110
118
|
const core = useComboboxCore<V>({
|
|
@@ -115,7 +123,11 @@ export function EntityCombobox<V extends string | number, C extends ComboClearVa
|
|
|
115
123
|
minChars,
|
|
116
124
|
debounceMs,
|
|
117
125
|
});
|
|
118
|
-
const field = useComboboxFieldError(
|
|
126
|
+
const field = useComboboxFieldError(
|
|
127
|
+
error,
|
|
128
|
+
invalid || ariaInvalid === true || ariaInvalid === "true",
|
|
129
|
+
ariaDescribedBy,
|
|
130
|
+
);
|
|
119
131
|
// The props are the per-instance overrides, the provider the app-wide ones; a
|
|
120
132
|
// prop left `undefined` falls through to the provider rather than masking it.
|
|
121
133
|
const labels = useKitLabels("combobox", DEFAULT_COMBOBOX_LABELS, {
|
|
@@ -151,13 +163,15 @@ export function EntityCombobox<V extends string | number, C extends ComboClearVa
|
|
|
151
163
|
};
|
|
152
164
|
|
|
153
165
|
return (
|
|
154
|
-
// `rest` dresses the wrapper, which has no role; the accessible NAME
|
|
155
|
-
//
|
|
166
|
+
// `rest` dresses the wrapper, which has no role; the accessible NAME — and the
|
|
167
|
+
// id, description, invalid and required state above — go on the trigger, which
|
|
168
|
+
// has one. Spread FIRST so the trigger's ARIA and the
|
|
156
169
|
// handlers that open the panel cannot be clobbered from outside.
|
|
157
170
|
<div {...rest} className={cn("relative", className)}>
|
|
158
171
|
{label !== undefined && <FieldLabel>{label}</FieldLabel>}
|
|
159
172
|
<button
|
|
160
173
|
ref={core.triggerRef}
|
|
174
|
+
id={id}
|
|
161
175
|
type="button"
|
|
162
176
|
// A combobox, not a button. The distinction is not pedantry: this control
|
|
163
177
|
// carried `aria-invalid`, which `button` does not support, so a required
|
|
@@ -185,6 +199,7 @@ export function EntityCombobox<V extends string | number, C extends ComboClearVa
|
|
|
185
199
|
disabled={disabled}
|
|
186
200
|
aria-invalid={field.isInvalid || undefined}
|
|
187
201
|
aria-describedby={field.describedBy}
|
|
202
|
+
aria-required={ariaRequired}
|
|
188
203
|
onClick={() => !disabled && setOpen((o) => !o)}
|
|
189
204
|
// Down/Up opens the list from the closed trigger, per the APG. Enter and
|
|
190
205
|
// Space already do it through the button's own click.
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { Component } from "react";
|
|
2
|
+
import type { ErrorInfo, ReactNode } from "react";
|
|
3
|
+
import { AlertTriangle } from "lucide-react";
|
|
4
|
+
import { useKitLabels } from "../i18n/kit-labels";
|
|
5
|
+
import { Button, EmptyState } from "./ui";
|
|
6
|
+
import { Disclosure } from "./disclosure";
|
|
7
|
+
|
|
8
|
+
/** The words of the default fallback. */
|
|
9
|
+
export interface ErrorBoundaryLabels {
|
|
10
|
+
/** The fallback's heading. */
|
|
11
|
+
title: string;
|
|
12
|
+
/** The line under it. */
|
|
13
|
+
message: string;
|
|
14
|
+
/** The button that clears the error and renders the children again. */
|
|
15
|
+
retry: string;
|
|
16
|
+
/** The disclosure that opens the error's own text (`showDetails`). */
|
|
17
|
+
details: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const DEFAULT_ERROR_BOUNDARY_LABELS: ErrorBoundaryLabels = {
|
|
21
|
+
title: "Something went wrong",
|
|
22
|
+
message: "This part of the page could not be shown. Try again, or reload the page.",
|
|
23
|
+
retry: "Try again",
|
|
24
|
+
details: "Error details",
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* What was thrown, as flat strings, read ONCE when it arrives.
|
|
29
|
+
*
|
|
30
|
+
* React stores whatever was thrown, hostile or not — `throw { get message() { throw … } }`,
|
|
31
|
+
* `throw { name: Symbol() }`, `throw undefined` — and reading one of those during the
|
|
32
|
+
* fallback's own render would throw INSIDE the boundary, escalating to the next one up
|
|
33
|
+
* (keksdose's error-boundary.tsx learned this as feedback #160: at the app level there is
|
|
34
|
+
* none, and the result is the white screen the boundary exists to prevent).
|
|
35
|
+
*/
|
|
36
|
+
export interface ErrorBoundaryDetails {
|
|
37
|
+
name: string;
|
|
38
|
+
message: string;
|
|
39
|
+
stack?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function read(get: () => unknown): string | undefined {
|
|
43
|
+
try {
|
|
44
|
+
const v = get();
|
|
45
|
+
return typeof v === "string" ? v : v === undefined || v === null ? undefined : String(v);
|
|
46
|
+
} catch {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** {@link ErrorBoundaryDetails} of any thrown value, without ever throwing. */
|
|
52
|
+
export function describeThrown(error: unknown): ErrorBoundaryDetails {
|
|
53
|
+
if (typeof error === "object" && error !== null) {
|
|
54
|
+
const e = error as { name?: unknown; message?: unknown; stack?: unknown };
|
|
55
|
+
return {
|
|
56
|
+
name: read(() => e.name) ?? "Error",
|
|
57
|
+
message: read(() => e.message) ?? "",
|
|
58
|
+
stack: read(() => e.stack),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
return { name: "Error", message: read(() => error) ?? "" };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** What a render-prop `fallback` is handed. */
|
|
65
|
+
export interface ErrorBoundaryFallbackProps {
|
|
66
|
+
/** The thrown value, as it was thrown. Read it with care — see {@link describeThrown}. */
|
|
67
|
+
error: unknown;
|
|
68
|
+
details: ErrorBoundaryDetails;
|
|
69
|
+
/** Clear the error and render the children again. */
|
|
70
|
+
reset: () => void;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface ErrorBoundaryProps {
|
|
74
|
+
children?: ReactNode;
|
|
75
|
+
/** Replaces the default fallback: a node, or a render function handed the error and
|
|
76
|
+
* a `reset`. */
|
|
77
|
+
fallback?: ReactNode | ((props: ErrorBoundaryFallbackProps) => ReactNode);
|
|
78
|
+
/** Called once per caught error — log it, file a crash report. */
|
|
79
|
+
onError?: (error: unknown, info: ErrorInfo) => void;
|
|
80
|
+
/** Called when the error is cleared, by Retry, `reset` or a `resetKeys` change —
|
|
81
|
+
* drop a cache, refetch. */
|
|
82
|
+
onReset?: () => void;
|
|
83
|
+
/**
|
|
84
|
+
* When any of these changes (compared with `Object.is`) AFTER an error was caught, the
|
|
85
|
+
* error is cleared and the children render again — a change in the same update that
|
|
86
|
+
* threw does not count (it is what the error was shown under) — pass the route path so leaving a broken page does not
|
|
87
|
+
* carry its fallback to the next one (keksdose's `resetKey`).
|
|
88
|
+
*/
|
|
89
|
+
resetKeys?: ReadonlyArray<unknown>;
|
|
90
|
+
/** A disclosure under the message with the error's name, message and stack — for an
|
|
91
|
+
* internal tool, or while developing. Off by default: a stack means nothing to a user. */
|
|
92
|
+
showDetails?: boolean;
|
|
93
|
+
/** The default fallback's heading level. Default `h2`: the fallback often IS the page. */
|
|
94
|
+
headingAs?: "h2" | "h3" | "h4" | "h5" | "h6";
|
|
95
|
+
/** Classes for the default fallback's box (`m-8 min-h-[400px]`, kastlan). */
|
|
96
|
+
className?: string;
|
|
97
|
+
labels?: Partial<ErrorBoundaryLabels>;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
interface ErrorBoundaryState {
|
|
101
|
+
/** A flag of its own, so `throw undefined` is still an error shown. */
|
|
102
|
+
failed: boolean;
|
|
103
|
+
error: unknown;
|
|
104
|
+
details: ErrorBoundaryDetails | null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const CLEAR: ErrorBoundaryState = { failed: false, error: null, details: null };
|
|
108
|
+
|
|
109
|
+
function keysChanged(a: ReadonlyArray<unknown> | undefined, b: ReadonlyArray<unknown> | undefined): boolean {
|
|
110
|
+
if (a === b) return false;
|
|
111
|
+
if (!a || !b || a.length !== b.length) return true;
|
|
112
|
+
return a.some((v, i) => !Object.is(v, b[i]));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Catch a render error below it and show a fallback instead of an empty page.
|
|
117
|
+
*
|
|
118
|
+
* kastlan (shared/components/error-boundary.tsx) and keksdose (app/error-boundary.tsx)
|
|
119
|
+
* each built one on `EmptyState tone="danger"`: a warning glyph, a title, a line of
|
|
120
|
+
* explanation and a Retry. This is that fallback, translated through the provider
|
|
121
|
+
* (`errorBoundary.*`), with the hooks an app hangs its own behaviour on — `onError`
|
|
122
|
+
* for the crash report, `resetKeys` for the route change, `fallback` for anything else.
|
|
123
|
+
*
|
|
124
|
+
* The fallback is `role="alert"`: a page replaced by an error is news, and a reader
|
|
125
|
+
* that was on it is told.
|
|
126
|
+
*/
|
|
127
|
+
export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
|
128
|
+
state: ErrorBoundaryState = CLEAR;
|
|
129
|
+
|
|
130
|
+
static getDerivedStateFromError(error: unknown): Partial<ErrorBoundaryState> {
|
|
131
|
+
return { failed: true, error, details: describeThrown(error) };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
componentDidCatch(error: unknown, info: ErrorInfo): void {
|
|
135
|
+
this.props.onError?.(error, info);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
componentDidUpdate(prev: ErrorBoundaryProps, prevState: ErrorBoundaryState): void {
|
|
139
|
+
// Only a change AFTER the error was caught clears it (react-error-boundary's rule).
|
|
140
|
+
// `prevState.failed` is false in the update that caught it: that update may well be
|
|
141
|
+
// the one that changed the keys too — a navigation to a page that throws, with
|
|
142
|
+
// `resetKeys={[pathname]}` — and resetting then would fire `onReset` for nothing and
|
|
143
|
+
// render the failing child a second time. From then on `prev.resetKeys` are the
|
|
144
|
+
// keys the error was shown under, so any change clears it.
|
|
145
|
+
if (this.state.failed && prevState.failed && keysChanged(prev.resetKeys, this.props.resetKeys)) {
|
|
146
|
+
this.reset();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
reset = (): void => {
|
|
151
|
+
this.props.onReset?.();
|
|
152
|
+
this.setState(CLEAR);
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
render(): ReactNode {
|
|
156
|
+
const { failed, error, details } = this.state;
|
|
157
|
+
if (!failed || !details) return this.props.children;
|
|
158
|
+
const { fallback } = this.props;
|
|
159
|
+
if (typeof fallback === "function") return fallback({ error, details, reset: this.reset });
|
|
160
|
+
if (fallback !== undefined) return fallback;
|
|
161
|
+
return (
|
|
162
|
+
<ErrorFallback
|
|
163
|
+
details={details}
|
|
164
|
+
reset={this.reset}
|
|
165
|
+
showDetails={this.props.showDetails ?? false}
|
|
166
|
+
headingAs={this.props.headingAs ?? "h2"}
|
|
167
|
+
className={this.props.className}
|
|
168
|
+
labels={this.props.labels}
|
|
169
|
+
/>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function ErrorFallback({
|
|
175
|
+
details,
|
|
176
|
+
reset,
|
|
177
|
+
showDetails,
|
|
178
|
+
headingAs,
|
|
179
|
+
className,
|
|
180
|
+
labels,
|
|
181
|
+
}: {
|
|
182
|
+
details: ErrorBoundaryDetails;
|
|
183
|
+
reset: () => void;
|
|
184
|
+
showDetails: boolean;
|
|
185
|
+
headingAs: NonNullable<ErrorBoundaryProps["headingAs"]>;
|
|
186
|
+
className?: string;
|
|
187
|
+
labels?: Partial<ErrorBoundaryLabels>;
|
|
188
|
+
}) {
|
|
189
|
+
const text = useKitLabels("errorBoundary", DEFAULT_ERROR_BOUNDARY_LABELS, labels);
|
|
190
|
+
return (
|
|
191
|
+
<EmptyState
|
|
192
|
+
role="alert"
|
|
193
|
+
tone="danger"
|
|
194
|
+
headingAs={headingAs}
|
|
195
|
+
className={className}
|
|
196
|
+
icon={<AlertTriangle />}
|
|
197
|
+
title={text.title}
|
|
198
|
+
hint={text.message}
|
|
199
|
+
action={
|
|
200
|
+
<>
|
|
201
|
+
<Button variant="secondary" onClick={reset}>
|
|
202
|
+
{text.retry}
|
|
203
|
+
</Button>
|
|
204
|
+
{showDetails && (
|
|
205
|
+
<Disclosure variant="bare" title={text.details} className="basis-full text-start">
|
|
206
|
+
<pre className="max-h-60 overflow-auto whitespace-pre-wrap break-words text-xs text-[var(--text-secondary)]">
|
|
207
|
+
{details.message ? `${details.name}: ${details.message}` : details.name}
|
|
208
|
+
{details.stack ? `\n\n${details.stack}` : null}
|
|
209
|
+
</pre>
|
|
210
|
+
</Disclosure>
|
|
211
|
+
)}
|
|
212
|
+
</>
|
|
213
|
+
}
|
|
214
|
+
/>
|
|
215
|
+
);
|
|
216
|
+
}
|
package/src/components/field.tsx
CHANGED
|
@@ -55,6 +55,40 @@ export interface FieldControlProps {
|
|
|
55
55
|
"aria-required": true | undefined;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
/**
|
|
59
|
+
* The second argument of a `Field`'s render-prop children: what a GROUP needs that
|
|
60
|
+
* a single control does not. `htmlFor` names an `<input>`, but a `<label>` pointing
|
|
61
|
+
* at a `role="radiogroup"` div names nothing — so a group (a {@link ToggleGroup})
|
|
62
|
+
* takes `aria-labelledby={labelId}` instead. Kept out of {@link FieldControlProps}
|
|
63
|
+
* so the spread every existing control takes is unchanged.
|
|
64
|
+
*/
|
|
65
|
+
export interface FieldRenderMeta {
|
|
66
|
+
/** The label element's id, or `undefined` when the field has no label. */
|
|
67
|
+
labelId: string | undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Where the label is visible. `"visible"` (default); `"sr-only"`: never drawn, still
|
|
72
|
+
* the control's name; `"below-sm"` / `"below-md"` / `"below-lg"`: drawn only under
|
|
73
|
+
* that breakpoint and screen-reader-only from it up.
|
|
74
|
+
*
|
|
75
|
+
* For table-like editors — kastlan's journal entry lines
|
|
76
|
+
* (accounting/components/wizard/journal-entry-lines-step.tsx) — where a desktop
|
|
77
|
+
* row sits under column headers that already say "Account" and "Amount", but the
|
|
78
|
+
* phone layout stacks each line as a card and needs the labels back. The name stays
|
|
79
|
+
* on the control at every width; only the ink changes.
|
|
80
|
+
*/
|
|
81
|
+
export type FieldLabelVisibility = "visible" | "sr-only" | "below-sm" | "below-md" | "below-lg";
|
|
82
|
+
|
|
83
|
+
// Literal class strings so Tailwind's scanner sees each one.
|
|
84
|
+
const LABEL_VISIBILITY: Record<FieldLabelVisibility, string | undefined> = {
|
|
85
|
+
visible: undefined,
|
|
86
|
+
"sr-only": "sr-only",
|
|
87
|
+
"below-sm": "sm:sr-only",
|
|
88
|
+
"below-md": "md:sr-only",
|
|
89
|
+
"below-lg": "lg:sr-only",
|
|
90
|
+
};
|
|
91
|
+
|
|
58
92
|
export interface FieldProps {
|
|
59
93
|
/** The label above the control. Omit it for a group whose control is labelled
|
|
60
94
|
* some other way; the hint and error still render. */
|
|
@@ -74,10 +108,13 @@ export interface FieldProps {
|
|
|
74
108
|
disabled?: boolean;
|
|
75
109
|
/** The label's size: `sm` for a dense row, `md` otherwise. */
|
|
76
110
|
labelSize?: "sm" | "md";
|
|
111
|
+
/** See {@link FieldLabelVisibility}. */
|
|
112
|
+
labelVisibility?: FieldLabelVisibility;
|
|
77
113
|
/** Wrapper className. */
|
|
78
114
|
className?: string;
|
|
79
|
-
/** The control. A function receives {@link FieldControlProps} to spread on it
|
|
80
|
-
|
|
115
|
+
/** The control. A function receives {@link FieldControlProps} to spread on it, and
|
|
116
|
+
* {@link FieldRenderMeta} (the label's id, for a group). */
|
|
117
|
+
children: ReactNode | ((control: FieldControlProps, meta: FieldRenderMeta) => ReactNode);
|
|
81
118
|
}
|
|
82
119
|
|
|
83
120
|
const isShown = (node: ReactNode) =>
|
|
@@ -91,6 +128,7 @@ export function Field({
|
|
|
91
128
|
htmlFor,
|
|
92
129
|
disabled,
|
|
93
130
|
labelSize,
|
|
131
|
+
labelVisibility = "visible",
|
|
94
132
|
className,
|
|
95
133
|
children,
|
|
96
134
|
}: FieldProps) {
|
|
@@ -98,6 +136,7 @@ export function Field({
|
|
|
98
136
|
const id = htmlFor ?? `${generated}-control`;
|
|
99
137
|
const hintId = `${generated}-hint`;
|
|
100
138
|
const errorId = `${generated}-error`;
|
|
139
|
+
const labelId = `${generated}-label`;
|
|
101
140
|
const hasHint = isShown(hint);
|
|
102
141
|
const hasError = isShown(error);
|
|
103
142
|
const describedBy = [hasHint && hintId, hasError && errorId].filter(Boolean).join(" ");
|
|
@@ -108,20 +147,32 @@ export function Field({
|
|
|
108
147
|
"aria-required": required || undefined,
|
|
109
148
|
};
|
|
110
149
|
return (
|
|
111
|
-
<div
|
|
150
|
+
<div
|
|
151
|
+
data-slot="field"
|
|
152
|
+
className={cn(
|
|
153
|
+
"grid gap-1.5",
|
|
154
|
+
// A hidden label is `position: absolute`; `relative` keeps its containing
|
|
155
|
+
// block here rather than the page's (see sr-only-containment.test).
|
|
156
|
+
labelVisibility !== "visible" && "relative",
|
|
157
|
+
className,
|
|
158
|
+
)}
|
|
159
|
+
>
|
|
112
160
|
{label !== undefined && (
|
|
113
161
|
<Label
|
|
162
|
+
id={labelId}
|
|
114
163
|
htmlFor={id}
|
|
115
164
|
required={required}
|
|
116
165
|
disabled={disabled}
|
|
117
166
|
size={labelSize}
|
|
118
167
|
data-error={hasError || undefined}
|
|
119
|
-
className="data-[error=true]:text-[var(--danger)]"
|
|
168
|
+
className={cn("data-[error=true]:text-[var(--danger)]", LABEL_VISIBILITY[labelVisibility])}
|
|
120
169
|
>
|
|
121
170
|
{label}
|
|
122
171
|
</Label>
|
|
123
172
|
)}
|
|
124
|
-
{typeof children === "function"
|
|
173
|
+
{typeof children === "function"
|
|
174
|
+
? children(control, { labelId: label !== undefined ? labelId : undefined })
|
|
175
|
+
: children}
|
|
125
176
|
{hasHint && (
|
|
126
177
|
<p id={hintId} className="text-[11px] leading-tight text-[var(--text-muted)]">
|
|
127
178
|
{hint}
|
|
@@ -15,7 +15,8 @@ import { X } from "lucide-react";
|
|
|
15
15
|
|
|
16
16
|
import { cn } from "../lib/cn";
|
|
17
17
|
import { dirOf, type Direction } from "../lib/direction";
|
|
18
|
-
import { useKitLabels } from "../i18n/kit-labels";
|
|
18
|
+
import { useKitLabels, useKitLink } from "../i18n/kit-labels";
|
|
19
|
+
import { pickLinkRenderer } from "./text-link";
|
|
19
20
|
import { Tooltip, type TooltipSide } from "./tooltip";
|
|
20
21
|
|
|
21
22
|
export interface FloatingPanelLabels {
|
|
@@ -588,8 +589,17 @@ export interface FloatingActionProps extends Omit<ButtonHTMLAttributes<HTMLButto
|
|
|
588
589
|
label: string;
|
|
589
590
|
icon: ReactNode;
|
|
590
591
|
/** Make it a toggle: `aria-pressed` and the brand "on" look (the brand glyph on the
|
|
591
|
-
* quiet brand fill), as {@link FloatingActionButtonProps.pressed}. Ignored on a link.
|
|
592
|
+
* quiet brand fill), as {@link FloatingActionButtonProps.pressed}. Ignored on a link.
|
|
593
|
+
* For the state without the tint, see {@link pressedStyle}. */
|
|
592
594
|
pressed?: boolean;
|
|
595
|
+
/**
|
|
596
|
+
* How `pressed` looks. `tint` (default): the brand "on" look. `plain`: the member
|
|
597
|
+
* keeps its quiet look in both states and only `aria-pressed` changes — for a toggle
|
|
598
|
+
* whose icon already says which state it is in (keksdose's /transactions "Upcoming"
|
|
599
|
+
* swaps two icons; a tint on top said it twice). The same as passing `aria-pressed`
|
|
600
|
+
* without `pressed`, which also rides through untinted.
|
|
601
|
+
*/
|
|
602
|
+
pressedStyle?: "tint" | "plain";
|
|
593
603
|
/**
|
|
594
604
|
* `primary`: the brand-filled member, for the group's main action (the /transactions
|
|
595
605
|
* "+"). Default: the quiet one.
|
|
@@ -611,8 +621,8 @@ export interface FloatingActionProps extends Omit<ButtonHTMLAttributes<HTMLButto
|
|
|
611
621
|
tooltip?: boolean;
|
|
612
622
|
/** Where the tooltip opens. Default `top` — the group sits at the bottom of the screen. */
|
|
613
623
|
tooltipSide?: TooltipSide;
|
|
614
|
-
/** Make it a link — the /transactions invoices member.
|
|
615
|
-
* `
|
|
624
|
+
/** Make it a link — the /transactions invoices member. Drawn by `renderLink`, else
|
|
625
|
+
* the `<UiKitProvider linkComponent>` (an in-app `href`), else a plain `<a>`. */
|
|
616
626
|
href?: string;
|
|
617
627
|
/** Render the link through your router's `Link` — see {@link FloatingActionLinkProps}. */
|
|
618
628
|
renderLink?: (props: FloatingActionLinkProps) => ReactElement;
|
|
@@ -637,6 +647,7 @@ export function FloatingAction({
|
|
|
637
647
|
label,
|
|
638
648
|
icon,
|
|
639
649
|
pressed,
|
|
650
|
+
pressedStyle = "tint",
|
|
640
651
|
variant = "default",
|
|
641
652
|
badge,
|
|
642
653
|
badgeLabel,
|
|
@@ -652,6 +663,7 @@ export function FloatingAction({
|
|
|
652
663
|
...rest
|
|
653
664
|
}: FloatingActionProps) {
|
|
654
665
|
const labels = useKitLabels("floatingPanel", DEFAULT_FLOATING_PANEL_LABELS);
|
|
666
|
+
const kitLink = useKitLink();
|
|
655
667
|
const counted = badge !== undefined && badge > 0;
|
|
656
668
|
const name = counted ? `${label}, ${badgeLabel ?? labels.badge(badge)}` : label;
|
|
657
669
|
const isLink = href !== undefined;
|
|
@@ -661,7 +673,7 @@ export function FloatingAction({
|
|
|
661
673
|
"disabled:cursor-not-allowed disabled:opacity-50 [&_svg]:size-5",
|
|
662
674
|
variant === "primary"
|
|
663
675
|
? "bg-[var(--brand)] text-[var(--brand-contrast)] hover:bg-[var(--brand-hover)]"
|
|
664
|
-
: pressed && !isLink
|
|
676
|
+
: pressed && !isLink && pressedStyle === "tint"
|
|
665
677
|
? "bg-[var(--brand-bg)] text-[var(--brand)] hover:bg-[var(--brand-bg-hover)]"
|
|
666
678
|
: "text-[var(--text-secondary)] hover:bg-[var(--bg-hover)] hover:text-[var(--text-primary)]",
|
|
667
679
|
className,
|
|
@@ -697,8 +709,9 @@ export function FloatingAction({
|
|
|
697
709
|
children: body,
|
|
698
710
|
};
|
|
699
711
|
const { children: content, ...anchor } = linkProps;
|
|
700
|
-
|
|
701
|
-
|
|
712
|
+
const render = pickLinkRenderer(renderLink, kitLink, href);
|
|
713
|
+
member = render ? (
|
|
714
|
+
<RenderedFloatingLink render={render} {...linkProps} />
|
|
702
715
|
) : (
|
|
703
716
|
<a {...anchor}>{content}</a>
|
|
704
717
|
);
|