@eifi1/ui-kit 0.15.4 → 0.16.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 +20 -15
- package/dist/chart.d.ts +3 -4
- package/dist/components/alert-banner.js +1 -1
- package/dist/components/alert-banner.js.map +1 -1
- package/dist/components/amount-input.d.ts +13 -4
- package/dist/components/amount-input.js +166 -157
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/authed-image.d.ts +14 -3
- package/dist/components/authed-image.js +32 -23
- package/dist/components/authed-image.js.map +1 -1
- package/dist/components/button-group.d.ts +3 -4
- package/dist/components/calculator.d.ts +3 -4
- package/dist/components/checkbox.js +1 -1
- package/dist/components/checkbox.js.map +1 -1
- package/dist/components/choice-card.js +1 -1
- package/dist/components/choice-card.js.map +1 -1
- package/dist/components/copy-button.d.ts +3 -4
- package/dist/components/error-boundary.js +1 -1
- package/dist/components/error-boundary.js.map +1 -1
- package/dist/components/facing-pair.d.ts +3 -4
- package/dist/components/file-button.d.ts +3 -4
- package/dist/components/file-button.js +64 -31
- package/dist/components/file-button.js.map +1 -1
- package/dist/components/file-dropzone.d.ts +3 -4
- package/dist/components/file-dropzone.js +1 -1
- package/dist/components/file-dropzone.js.map +1 -1
- package/dist/components/form-actions.d.ts +4 -5
- package/dist/components/form-actions.js +18 -6
- package/dist/components/form-actions.js.map +1 -1
- package/dist/components/line-items.d.ts +53 -2
- package/dist/components/line-items.js +73 -7
- package/dist/components/line-items.js.map +1 -1
- package/dist/components/list.d.ts +99 -5
- package/dist/components/list.js +103 -38
- package/dist/components/list.js.map +1 -1
- package/dist/components/loading-state.d.ts +19 -3
- package/dist/components/loading-state.js +14 -3
- package/dist/components/loading-state.js.map +1 -1
- package/dist/components/money-field.d.ts +3 -4
- package/dist/components/number-field.d.ts +3 -4
- package/dist/components/number-input.d.ts +3 -4
- package/dist/components/number-input.js +4 -2
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numpad-sheet.d.ts +3 -4
- package/dist/components/passkeys-setting.d.ts +30 -10
- package/dist/components/passkeys-setting.js +19 -11
- package/dist/components/passkeys-setting.js.map +1 -1
- package/dist/components/progress-bar.d.ts +12 -3
- package/dist/components/progress-bar.js +21 -11
- package/dist/components/progress-bar.js.map +1 -1
- package/dist/components/series-chart.d.ts +3 -4
- package/dist/components/settings-fields.d.ts +3 -4
- package/dist/components/signed-amount.d.ts +28 -4
- package/dist/components/signed-amount.js +17 -8
- package/dist/components/signed-amount.js.map +1 -1
- package/dist/components/stat-tile.d.ts +31 -2
- package/dist/components/stat-tile.js +37 -6
- package/dist/components/stat-tile.js.map +1 -1
- package/dist/components/status-dot.d.ts +4 -1
- package/dist/components/status-dot.js +20 -1
- package/dist/components/status-dot.js.map +1 -1
- package/dist/components/swatch-picker.d.ts +3 -1
- package/dist/components/swatch-picker.js.map +1 -1
- package/dist/components/table.d.ts +18 -13
- package/dist/components/table.js +20 -5
- package/dist/components/table.js.map +1 -1
- package/dist/components/text-link.d.ts +3 -4
- package/dist/components/time-input.d.ts +3 -4
- package/dist/components/toggle-legend.d.ts +12 -1
- package/dist/components/toggle-legend.js +6 -2
- package/dist/components/toggle-legend.js.map +1 -1
- package/dist/components/ui.d.ts +3 -4
- package/dist/components/ui.js +156 -52
- package/dist/components/ui.js.map +1 -1
- package/dist/feedback/feedback-attachment.d.ts +2 -82
- package/dist/feedback/feedback-attachment.js +285 -55
- package/dist/feedback/feedback-attachment.js.map +1 -1
- package/dist/feedback/feedback-dialog.d.ts +2 -105
- package/dist/feedback/feedback-dialog.js +57 -25
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback/feedback-inbox.d.ts +1 -1
- package/dist/feedback/feedback-thread.d.ts +2 -2
- package/dist/feedback-attachment-WFiXJ8We.d.ts +302 -0
- package/dist/feedback.d.ts +1 -2
- package/dist/hooks/use-file-drop.d.ts +3 -4
- package/dist/i18n/defaults.d.ts +3 -4
- package/dist/i18n/kit-labels.d.ts +2 -3
- package/dist/i18n/locales/de-CH-informal.d.ts +3 -4
- package/dist/i18n/locales/de-CH.d.ts +3 -4
- package/dist/i18n/locales/de-informal.d.ts +3 -4
- package/dist/i18n/locales/de.d.ts +3 -4
- package/dist/i18n/locales/de.js +5 -1
- package/dist/i18n/locales/de.js.map +1 -1
- package/dist/i18n/locales/es.d.ts +3 -4
- package/dist/i18n/locales/es.js +5 -1
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +3 -4
- package/dist/i18n/locales/fr.js +5 -1
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +3 -4
- package/dist/i18n/locales/hu.js +5 -1
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +3 -4
- package/dist/i18n/locales/it.js +5 -1
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +3 -4
- package/dist/i18n/locales/zh.js +5 -1
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/index.d.ts +10 -11
- package/dist/{kit-labels-CnnBd-2N.d.ts → kit-labels-DE--LmCk.d.ts} +154 -14
- package/dist/lib/format.d.ts +18 -3
- package/dist/lib/format.js +2 -1
- package/dist/lib/format.js.map +1 -1
- package/dist/rhf/fields.d.ts +3 -4
- package/dist/rhf/form.d.ts +3 -4
- package/dist/rhf/line-items.js.map +1 -1
- package/dist/rhf.d.ts +3 -4
- package/dist/shell/app-shell.d.ts +3 -4
- package/dist/shell/top-bar-brand.d.ts +3 -4
- package/dist/shell.d.ts +3 -4
- package/dist/theme/palette-derive.d.ts +3 -0
- package/dist/theme/palette-derive.js +7 -0
- package/dist/theme/palette-derive.js.map +1 -1
- package/dist/wizard/stepper-nav.d.ts +3 -4
- package/dist/wizard.d.ts +3 -4
- package/package.json +4 -2
- package/src/components/alert-banner.tsx +1 -1
- package/src/components/amount-input.tsx +47 -16
- package/src/components/authed-image.tsx +20 -2
- package/src/components/checkbox.tsx +1 -1
- package/src/components/choice-card.tsx +1 -1
- package/src/components/error-boundary.tsx +1 -1
- package/src/components/file-button.tsx +77 -27
- package/src/components/file-dropzone.tsx +1 -1
- package/src/components/form-actions.tsx +86 -8
- package/src/components/line-items.tsx +131 -14
- package/src/components/list.tsx +240 -50
- package/src/components/loading-state.tsx +35 -4
- package/src/components/number-input.tsx +18 -2
- package/src/components/passkeys-setting.tsx +45 -18
- package/src/components/progress-bar.tsx +46 -21
- package/src/components/signed-amount.tsx +47 -8
- package/src/components/stat-tile.tsx +76 -6
- package/src/components/status-dot.tsx +31 -0
- package/src/components/swatch-picker.tsx +3 -1
- package/src/components/table.tsx +42 -15
- package/src/components/toggle-legend.tsx +21 -4
- package/src/components/ui.tsx +280 -70
- package/src/feedback/feedback-attachment.tsx +472 -96
- package/src/feedback/feedback-dialog.tsx +162 -50
- package/src/i18n/locales/de.ts +4 -0
- package/src/i18n/locales/es.ts +4 -0
- package/src/i18n/locales/fr.ts +4 -0
- package/src/i18n/locales/hu.ts +4 -0
- package/src/i18n/locales/it.ts +4 -0
- package/src/i18n/locales/zh.ts +4 -0
- package/src/lib/format.ts +24 -3
- package/src/rhf/line-items.tsx +6 -0
- package/src/theme/palette-derive.ts +10 -0
- package/tokens.css +9 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
import { ReactNode, ReactElement } from 'react';
|
|
3
|
-
import { f as ButtonVariant } from '../kit-labels-
|
|
3
|
+
import { f as ButtonVariant } from '../kit-labels-DE--LmCk.js';
|
|
4
4
|
import { UseWizardReturn, WizardLabels } from './types.js';
|
|
5
5
|
import '../components/data-table-labels.js';
|
|
6
6
|
import '../components/mini-calendar.js';
|
|
@@ -32,8 +32,7 @@ import '../components/dialog-frame.js';
|
|
|
32
32
|
import '../components/modal.js';
|
|
33
33
|
import '../hooks/use-search-param-state.js';
|
|
34
34
|
import '../components/measured-grid.js';
|
|
35
|
-
import '../feedback
|
|
36
|
-
import '../feedback/feedback-dialog.js';
|
|
35
|
+
import '../feedback-attachment-WFiXJ8We.js';
|
|
37
36
|
import '../feedback/feedback-thread.js';
|
|
38
37
|
import '../feedback/feedback-inbox.js';
|
|
39
38
|
import '../components/account-settings-labels.js';
|
|
@@ -41,12 +40,12 @@ import '../components/confirm-dialog.js';
|
|
|
41
40
|
import '../components/floating-panel.js';
|
|
42
41
|
import '../components/bulk-action-bar.js';
|
|
43
42
|
import '../components/list.js';
|
|
43
|
+
import '../components/signed-amount.js';
|
|
44
44
|
import '../components/breadcrumbs.js';
|
|
45
45
|
import '../components/toast.js';
|
|
46
46
|
import '../components/description-list.js';
|
|
47
47
|
import '../components/line-items.js';
|
|
48
48
|
import '../components/progress-bar.js';
|
|
49
|
-
import '../components/signed-amount.js';
|
|
50
49
|
import '../components/error-boundary.js';
|
|
51
50
|
import '../components/authed-image.js';
|
|
52
51
|
import '../hooks/use-authed-src.js';
|
package/dist/wizard.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export { WizardStep, WizardStepProps } from './wizard/wizard-step.js';
|
|
|
6
6
|
export { WizardSummary } from './wizard/wizard-summary.js';
|
|
7
7
|
export { StepperNav } from './wizard/stepper-nav.js';
|
|
8
8
|
import 'react';
|
|
9
|
-
import './kit-labels-
|
|
9
|
+
import './kit-labels-DE--LmCk.js';
|
|
10
10
|
import './components/data-table-labels.js';
|
|
11
11
|
import './components/mini-calendar.js';
|
|
12
12
|
import './components/calendar-heatmap.js';
|
|
@@ -37,8 +37,7 @@ import './components/dialog-frame.js';
|
|
|
37
37
|
import './components/modal.js';
|
|
38
38
|
import './hooks/use-search-param-state.js';
|
|
39
39
|
import './components/measured-grid.js';
|
|
40
|
-
import './feedback
|
|
41
|
-
import './feedback/feedback-dialog.js';
|
|
40
|
+
import './feedback-attachment-WFiXJ8We.js';
|
|
42
41
|
import './feedback/feedback-thread.js';
|
|
43
42
|
import './feedback/feedback-inbox.js';
|
|
44
43
|
import './components/account-settings-labels.js';
|
|
@@ -46,12 +45,12 @@ import './components/confirm-dialog.js';
|
|
|
46
45
|
import './components/floating-panel.js';
|
|
47
46
|
import './components/bulk-action-bar.js';
|
|
48
47
|
import './components/list.js';
|
|
48
|
+
import './components/signed-amount.js';
|
|
49
49
|
import './components/breadcrumbs.js';
|
|
50
50
|
import './components/toast.js';
|
|
51
51
|
import './components/description-list.js';
|
|
52
52
|
import './components/line-items.js';
|
|
53
53
|
import './components/progress-bar.js';
|
|
54
|
-
import './components/signed-amount.js';
|
|
55
54
|
import './components/error-boundary.js';
|
|
56
55
|
import './components/authed-image.js';
|
|
57
56
|
import './hooks/use-authed-src.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eifi1/ui-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "App-agnostic shared design system: domain-free React components, theme and tokens.",
|
|
6
6
|
"_sideEffects": "The barrel (src/index.ts) re-exports EVERY component, including components/chart, which statically imports recharts. Without this field a bundler must assume each of those modules has side effects, so it cannot drop the unused ones: importing a Button from '@eifi1/ui-kit' dragged all 433KB of recharts into the consumer's ENTRY chunk, and Keksdose was preloading it on every page load, login included. Nothing in src/ imports for side effects (no bare imports, no CSS imports from JS, no module-level global writes) — the token stylesheet is its own './tokens.css' export — so this is safe. CSS is still listed so any future stylesheet survives. Re-checked when `flag-icons` left `dependencies` and tsup started emitting a `\"use client\"` banner: neither touches this. The flag stylesheet was never imported from JS in the first place (that was the finding), and a directive prologue is a string literal every bundler lifts out before it decides whether a module is inert.",
|
|
@@ -77,7 +77,9 @@
|
|
|
77
77
|
"check:tailwind": "node scripts/check-tailwind-scan.mjs",
|
|
78
78
|
"check:security": "npm audit --audit-level=high",
|
|
79
79
|
"check:commits": "node scripts/check-commit-range.cjs",
|
|
80
|
-
"check:
|
|
80
|
+
"check:commit-rules": "node scripts/check-commit-rules.mjs",
|
|
81
|
+
"sync:commit-rules": "node scripts/sync-commit-rules.mjs",
|
|
82
|
+
"check:static": "npm run typecheck && npm run lint && npm run check:tokens && npm run check:security && npm run check:commit-rules && npm run check:commits",
|
|
81
83
|
"check:test": "npm run test:coverage",
|
|
82
84
|
"check:build": "npm run build && npm run check:graph && npm run check:package && npm run build:showcase && npm run check:tailwind",
|
|
83
85
|
"check": "node scripts/check.mjs",
|
|
@@ -28,7 +28,7 @@ export type AlertSize = "sm" | "md";
|
|
|
28
28
|
* at any offset. Callers compensate the extra pixel in their own padding so
|
|
29
29
|
* switching tones never shifts the layout. */
|
|
30
30
|
const TONE_FRAME: Record<AlertTone, string> = {
|
|
31
|
-
danger: "border-2 border-[var(--danger-border)] bg-[var(--danger-bg)]",
|
|
31
|
+
danger: "border-2 border-[var(--danger-border-strong)] bg-[var(--danger-bg)]",
|
|
32
32
|
warning: "border-2 border-[var(--warning-border)] bg-[var(--warning-bg)]",
|
|
33
33
|
info: "border-2 border-[var(--info-border)] bg-[var(--info-bg)]",
|
|
34
34
|
success: "border-2 border-[var(--success-border)] bg-[var(--success-bg)]",
|
|
@@ -2,7 +2,7 @@ import { forwardRef, useCallback, useId, useMemo, useRef, useState } from "react
|
|
|
2
2
|
import { ChevronDown } from "lucide-react";
|
|
3
3
|
import type { ReactNode } from "react";
|
|
4
4
|
import { CURRENCIES, CurrencyFlag, currencyName, getCurrency } from "./currency-select";
|
|
5
|
-
import { FIELD_BASE, FIELD_DISPLAY, FIELD_INVALID, FLOATING_INPUT_CLASS,
|
|
5
|
+
import { FIELD_BASE, FIELD_DISPLAY, FIELD_INVALID, FLOATING_INPUT_CLASS, FloatingField, PHONE_QUERY } from "./ui";
|
|
6
6
|
import { cn } from "../lib/cn";
|
|
7
7
|
import { currencyMinorDigits } from "../lib/format";
|
|
8
8
|
import { useMediaQuery } from "../hooks/use-media-query";
|
|
@@ -48,6 +48,13 @@ interface AmountInputProps {
|
|
|
48
48
|
"aria-describedby"?: string;
|
|
49
49
|
"aria-invalid"?: boolean | "true" | "false";
|
|
50
50
|
"aria-required"?: boolean | "true" | "false";
|
|
51
|
+
/** {@link NumberInput}'s `hint`, the same rule: a {@link FieldHint} "?" rides the
|
|
52
|
+
* label's own line, beside the label (not at the far end, where the calculator and
|
|
53
|
+
* the currency chip live); plain TEXT (a string or a number) is a caption UNDER
|
|
54
|
+
* the field, attached through `aria-describedby` after the caller's own. A phone's
|
|
55
|
+
* `variant="display"` has no visible label, so a FieldHint is dropped there; a
|
|
56
|
+
* caption still shows. */
|
|
57
|
+
hint?: ReactNode;
|
|
51
58
|
/** Every user-facing string this component and the two it composes own, so a
|
|
52
59
|
* translating host can supply its own. Each key optional, and each a per-field
|
|
53
60
|
* override of the kit-wide translation in `<UiKitProvider labels>`: `currency`
|
|
@@ -100,6 +107,9 @@ interface AmountInputProps {
|
|
|
100
107
|
* Because it is rendered rather than stored it cannot be deleted (the next render
|
|
101
108
|
* puts it back), a select-all-and-retype keeps the direction, and the caret needs
|
|
102
109
|
* no minding.
|
|
110
|
+
*
|
|
111
|
+
* Since 0.15.5 it is drawn on its own too, for a read-only or disabled signed figure
|
|
112
|
+
* (an "Items total" of −5.18): no `onNegativeChange` needed just to show the minus.
|
|
103
113
|
*/
|
|
104
114
|
negative?: boolean;
|
|
105
115
|
/**
|
|
@@ -293,9 +303,13 @@ function isResultOf(previous: string, text: string, settle: (text: string) => st
|
|
|
293
303
|
}
|
|
294
304
|
|
|
295
305
|
export const AmountInput = forwardRef<HTMLInputElement, AmountInputProps>(
|
|
296
|
-
({ value, onChange, currency, onCurrencyChange, placeholder, label, disabled, invalid: invalidProp, className, id, ariaLabel, "aria-describedby": ariaDescribedBy, "aria-invalid": ariaInvalid, "aria-required": ariaRequired, autoFocus, tone = "neutral", negative = false, onNegativeChange, variant = "field", align = "start", labels, currencyNames, digits: digitsProp, min, max, onCommit }, ref) => {
|
|
306
|
+
({ value, onChange, currency, onCurrencyChange, placeholder, label, disabled, invalid: invalidProp, className, id, ariaLabel, "aria-describedby": ariaDescribedBy, "aria-invalid": ariaInvalid, "aria-required": ariaRequired, hint, autoFocus, tone = "neutral", negative = false, onNegativeChange, variant = "field", align = "start", labels, currencyNames, digits: digitsProp, min, max, onCommit }, ref) => {
|
|
297
307
|
const generatedId = useId();
|
|
298
308
|
const fieldId = id ?? generatedId;
|
|
309
|
+
const hintId = useId();
|
|
310
|
+
// Text is a caption under the field; a FieldHint rides the label line. See `hint`.
|
|
311
|
+
const textHint = (typeof hint === "string" && hint !== "") || typeof hint === "number";
|
|
312
|
+
const describedBy = textHint ? (ariaDescribedBy ? `${ariaDescribedBy} ${hintId}` : hintId) : ariaDescribedBy;
|
|
299
313
|
const invalid = Boolean(invalidProp) || ariaInvalid === true || ariaInvalid === "true";
|
|
300
314
|
const editable = !!onCurrencyChange;
|
|
301
315
|
// On phones we suppress the OS keyboard (inputMode="none" below) and show our
|
|
@@ -330,7 +344,11 @@ export const AmountInput = forwardRef<HTMLInputElement, AmountInputProps>(
|
|
|
330
344
|
// front of a bare amount: an expression being typed ("12-30") owns its own
|
|
331
345
|
// minus and has to survive keystroke-for-keystroke.
|
|
332
346
|
const signOwned = onNegativeChange !== undefined;
|
|
333
|
-
|
|
347
|
+
// The sign is DRAWN whenever `negative` says so, handler or not (keksdose live #366):
|
|
348
|
+
// a read-only or disabled total ("−5.18") used to need a dummy `onNegativeChange`
|
|
349
|
+
// just to show its minus. Without the handler there is nothing to toggle — a typed
|
|
350
|
+
// minus simply stays in the text, as in any field without a direction control.
|
|
351
|
+
const shown = negative && isBareAmount(value) ? `-${value}` : value;
|
|
334
352
|
// What the user SEES is `shown` with the locale's decimal mark (kastlan 40: a
|
|
335
353
|
// fr-CH user typed "1,5" and watched it turn into "1.5" under their fingers). Display only — `value`, `onChange`, the sign split and the settle all
|
|
336
354
|
// stay dot-decimal, because that string is the contract: RhfMoneyField's
|
|
@@ -447,8 +465,17 @@ export const AmountInput = forwardRef<HTMLInputElement, AmountInputProps>(
|
|
|
447
465
|
<div ref={wrapperRef} className={cn("w-full", className)}>
|
|
448
466
|
{/* Inner relative box holds the input + its absolutely-positioned
|
|
449
467
|
trailing controls (calculator / currency), so those stay anchored to
|
|
450
|
-
the INPUT even when the mobile math bar is rendered below (#334).
|
|
451
|
-
|
|
468
|
+
the INPUT even when the mobile math bar is rendered below (#334). It is
|
|
469
|
+
the kit's FloatingField, which draws the label — floating, or sr-only in
|
|
470
|
+
the display shape — and puts a FieldHint on the label's line, as it does
|
|
471
|
+
for NumberInput. */}
|
|
472
|
+
<FloatingField
|
|
473
|
+
className="w-full"
|
|
474
|
+
htmlFor={fieldId}
|
|
475
|
+
label={label}
|
|
476
|
+
srOnlyLabel={asDisplay}
|
|
477
|
+
hint={textHint ? undefined : hint}
|
|
478
|
+
>
|
|
452
479
|
<input
|
|
453
480
|
ref={setRefs}
|
|
454
481
|
id={fieldId}
|
|
@@ -506,18 +533,15 @@ export const AmountInput = forwardRef<HTMLInputElement, AmountInputProps>(
|
|
|
506
533
|
invalid && FIELD_INVALID,
|
|
507
534
|
)}
|
|
508
535
|
aria-invalid={invalid || undefined}
|
|
509
|
-
aria-describedby={
|
|
536
|
+
aria-describedby={describedBy}
|
|
510
537
|
aria-required={ariaRequired}
|
|
511
538
|
/>
|
|
512
|
-
{label
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
{label}
|
|
519
|
-
</label>
|
|
520
|
-
)}
|
|
539
|
+
{/* The label is FloatingField's, after these children (the floating trick
|
|
540
|
+
reads the input through `peer-*`). The display shape drops it VISUALLY,
|
|
541
|
+
not from the accessibility tree: a 36px tinted figure at the top of a
|
|
542
|
+
transaction form is legibly the amount, but a screen reader still needs
|
|
543
|
+
the name, and callers that pass `label` without `ariaLabel` would
|
|
544
|
+
otherwise be left with none. */}
|
|
521
545
|
{/* Trailing controls share one flex track so the calculator icon and the
|
|
522
546
|
currency suffix/picker sit side by side without overlapping. */}
|
|
523
547
|
{/* The display shape has no box to inset from, so the controls align to the
|
|
@@ -620,7 +644,14 @@ export const AmountInput = forwardRef<HTMLInputElement, AmountInputProps>(
|
|
|
620
644
|
})}
|
|
621
645
|
</DropdownPanel>
|
|
622
646
|
)}
|
|
623
|
-
</
|
|
647
|
+
</FloatingField>
|
|
648
|
+
{/* Outside the relative box, as NumberInput's caption is: the trailing
|
|
649
|
+
controls are `inset-y-1` in it and would stretch down over a second line. */}
|
|
650
|
+
{textHint && (
|
|
651
|
+
<p id={hintId} className="mt-1 text-[11px] leading-tight text-[var(--text-muted)]">
|
|
652
|
+
{hint}
|
|
653
|
+
</p>
|
|
654
|
+
)}
|
|
624
655
|
{showNumpad && (
|
|
625
656
|
<NumberPadSheet
|
|
626
657
|
// Localised like the field it mirrors. Keys are typing, read by the
|
|
@@ -44,8 +44,18 @@ export interface AuthedImageProps
|
|
|
44
44
|
link?: boolean;
|
|
45
45
|
/** Replaces the default skeleton while loading. */
|
|
46
46
|
loadingFallback?: ReactNode;
|
|
47
|
-
/**
|
|
47
|
+
/**
|
|
48
|
+
* Replaces the default "couldn't load" tile. `null` renders nothing in its place —
|
|
49
|
+
* an optional photo slot that simply stays empty; only `undefined` (left out) means
|
|
50
|
+
* the default tile.
|
|
51
|
+
*/
|
|
48
52
|
errorFallback?: ReactNode;
|
|
53
|
+
/**
|
|
54
|
+
* Keep a click on the image (and the Enter that follows its `link`) from reaching an
|
|
55
|
+
* ancestor — a thumbnail in a DataTable row or a clickable card opens itself instead
|
|
56
|
+
* of also running the row's action. As TextLink's and IconButton's `stopPropagation`.
|
|
57
|
+
*/
|
|
58
|
+
stopPropagation?: boolean;
|
|
49
59
|
labels?: Partial<AuthedImageLabels>;
|
|
50
60
|
}
|
|
51
61
|
|
|
@@ -68,6 +78,7 @@ export function AuthedImage({
|
|
|
68
78
|
link,
|
|
69
79
|
loadingFallback,
|
|
70
80
|
errorFallback,
|
|
81
|
+
stopPropagation,
|
|
71
82
|
labels: labelsProp,
|
|
72
83
|
onLoad,
|
|
73
84
|
onError,
|
|
@@ -85,6 +96,7 @@ export function AuthedImage({
|
|
|
85
96
|
link={link}
|
|
86
97
|
loadingFallback={loadingFallback}
|
|
87
98
|
errorFallback={errorFallback}
|
|
99
|
+
stopPropagation={stopPropagation}
|
|
88
100
|
imgProps={rest}
|
|
89
101
|
onLoad={onLoad}
|
|
90
102
|
onError={onError}
|
|
@@ -106,6 +118,7 @@ export function ImageStates({
|
|
|
106
118
|
link,
|
|
107
119
|
loadingFallback,
|
|
108
120
|
errorFallback,
|
|
121
|
+
stopPropagation,
|
|
109
122
|
imgProps,
|
|
110
123
|
onLoad,
|
|
111
124
|
onError,
|
|
@@ -118,6 +131,7 @@ export function ImageStates({
|
|
|
118
131
|
link?: boolean;
|
|
119
132
|
loadingFallback?: ReactNode;
|
|
120
133
|
errorFallback?: ReactNode;
|
|
134
|
+
stopPropagation?: boolean;
|
|
121
135
|
imgProps?: Omit<ImgHTMLAttributes<HTMLImageElement>, "src" | "alt" | "children" | "onLoad" | "onError">;
|
|
122
136
|
onLoad?: ImgHTMLAttributes<HTMLImageElement>["onLoad"];
|
|
123
137
|
onError?: ImgHTMLAttributes<HTMLImageElement>["onError"];
|
|
@@ -153,7 +167,10 @@ export function ImageStates({
|
|
|
153
167
|
);
|
|
154
168
|
|
|
155
169
|
return (
|
|
170
|
+
// eslint-disable-next-line jsx-a11y/no-static-element-interactions -- not an interaction: only fences the image's own clicks off from an ancestor's handler
|
|
156
171
|
<span
|
|
172
|
+
onClick={stopPropagation ? (e) => e.stopPropagation() : undefined}
|
|
173
|
+
onKeyDown={stopPropagation ? (e) => e.key === "Enter" && e.stopPropagation() : undefined}
|
|
157
174
|
data-image-state={failed ? "error" : waiting ? "loading" : fetched.status === "idle" ? "idle" : "ready"}
|
|
158
175
|
className={cn("relative block overflow-hidden", wrapperClassName)}
|
|
159
176
|
aria-busy={waiting || undefined}
|
|
@@ -172,7 +189,8 @@ export function ImageStates({
|
|
|
172
189
|
</span>
|
|
173
190
|
))}
|
|
174
191
|
{failed &&
|
|
175
|
-
|
|
192
|
+
// `=== undefined`, not `??`: `null` is the caller asking for nothing at all.
|
|
193
|
+
(errorFallback !== undefined ? errorFallback : (
|
|
176
194
|
<span
|
|
177
195
|
// `role="img"` with the alt as its name: the picture that failed still
|
|
178
196
|
// occupies its place in the reading order, and "couldn't be loaded" is
|
|
@@ -40,7 +40,7 @@ const BOX_MIXED = "border-[var(--brand)] bg-[var(--brand)]";
|
|
|
40
40
|
// survives fractional display scaling on every edge (see the note in ui.tsx). The
|
|
41
41
|
// focus ring turns danger too, so focusing a wrong box does not repaint it as fine.
|
|
42
42
|
const BOX_INVALID =
|
|
43
|
-
"border-[var(--danger-border)] ring-1 ring-[var(--danger-border)] checked:border-[var(--danger-border)] focus-visible:ring-[var(--danger)]";
|
|
43
|
+
"border-[var(--danger-border-strong)] ring-1 ring-[var(--danger-border-strong)] checked:border-[var(--danger-border-strong)] focus-visible:ring-[var(--danger)]";
|
|
44
44
|
|
|
45
45
|
const ERROR_CLASS = "mt-1 text-[11px] leading-tight text-[var(--danger)]";
|
|
46
46
|
|
|
@@ -102,7 +102,7 @@ const CARD =
|
|
|
102
102
|
const CARD_DISABLED = "hover:border-[var(--border)] hover:bg-[var(--bg-surface)]";
|
|
103
103
|
|
|
104
104
|
const CARD_INVALID =
|
|
105
|
-
"border-[var(--danger-border)] ring-1 ring-[var(--danger-border)] has-[:checked]:border-[var(--danger-border)] has-[:checked]:ring-[var(--danger-border)]";
|
|
105
|
+
"border-[var(--danger-border-strong)] ring-1 ring-[var(--danger-border-strong)] has-[:checked]:border-[var(--danger-border-strong)] has-[:checked]:ring-[var(--danger-border-strong)]";
|
|
106
106
|
|
|
107
107
|
const BOX =
|
|
108
108
|
"peer size-4 shrink-0 appearance-none border border-[var(--border-strong)] bg-[var(--bg-surface)] transition-colors " +
|
|
@@ -642,7 +642,7 @@ function ErrorFallback({
|
|
|
642
642
|
// program's words and can be read out or screenshotted verbatim.
|
|
643
643
|
<p
|
|
644
644
|
data-error-message=""
|
|
645
|
-
className="-mt-1 mb-1 basis-full whitespace-pre-wrap break-words rounded-md border border-[var(--danger-border)] bg-[var(--danger-bg)] px-3 py-2 text-start font-mono text-xs text-[var(--text-primary)]"
|
|
645
|
+
className="-mt-1 mb-1 basis-full whitespace-pre-wrap break-words rounded-md border border-[var(--danger-border-strong)] bg-[var(--danger-bg)] px-3 py-2 text-start font-mono text-xs text-[var(--text-primary)]"
|
|
646
646
|
>
|
|
647
647
|
{errorLine}
|
|
648
648
|
</p>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { forwardRef, useRef, useState } from "react";
|
|
1
|
+
import { forwardRef, useId, useRef, useState } from "react";
|
|
2
2
|
import type { ComponentPropsWithoutRef, DragEvent, ReactElement, ReactNode, Ref } from "react";
|
|
3
3
|
import { cn } from "../lib/cn";
|
|
4
4
|
import { useAnnounce } from "../hooks/use-announce";
|
|
@@ -418,6 +418,15 @@ export interface FileButtonProps
|
|
|
418
418
|
* Off by default: a button that silently takes drops is a surprise on a page that
|
|
419
419
|
* has a real drop target elsewhere. */
|
|
420
420
|
droppable?: boolean;
|
|
421
|
+
/**
|
|
422
|
+
* Show what was picked beside the button: the file's name, or "3 files selected"
|
|
423
|
+
* (`filePicker.selected`) for several — a native `<input type="file">` says this,
|
|
424
|
+
* and a bare button that takes a file and says nothing leaves the user to trust it.
|
|
425
|
+
* The read-out is a polite live region and describes the button, so it is heard
|
|
426
|
+
* when it changes and again on focus. It holds the last ACCEPTED pick: a refused
|
|
427
|
+
* file leaves it as it was. Off by default, so the button's layout is unchanged.
|
|
428
|
+
*/
|
|
429
|
+
showFileName?: boolean;
|
|
421
430
|
}
|
|
422
431
|
|
|
423
432
|
/**
|
|
@@ -447,6 +456,7 @@ export const FileButton = forwardRef<HTMLButtonElement, FileButtonProps>(functio
|
|
|
447
456
|
labels,
|
|
448
457
|
pending,
|
|
449
458
|
droppable,
|
|
459
|
+
showFileName = false,
|
|
450
460
|
disabled,
|
|
451
461
|
children,
|
|
452
462
|
className,
|
|
@@ -460,6 +470,9 @@ export const FileButton = forwardRef<HTMLButtonElement, FileButtonProps>(functio
|
|
|
460
470
|
ref,
|
|
461
471
|
) {
|
|
462
472
|
const inert = Boolean(disabled || pending);
|
|
473
|
+
const [picked, setPicked] = useState<readonly File[]>([]);
|
|
474
|
+
const pickedId = useId();
|
|
475
|
+
const pickerLabels = useKitLabels("filePicker", DEFAULT_FILE_PICKER_LABELS, labels);
|
|
463
476
|
const picker = useFilePicker({
|
|
464
477
|
accept,
|
|
465
478
|
multiple,
|
|
@@ -468,12 +481,19 @@ export const FileButton = forwardRef<HTMLButtonElement, FileButtonProps>(functio
|
|
|
468
481
|
maxFiles,
|
|
469
482
|
isValid,
|
|
470
483
|
invalidMessage,
|
|
471
|
-
onFiles
|
|
484
|
+
onFiles: showFileName
|
|
485
|
+
? (files) => {
|
|
486
|
+
setPicked(files);
|
|
487
|
+
onFiles?.(files);
|
|
488
|
+
}
|
|
489
|
+
: onFiles,
|
|
472
490
|
onPick,
|
|
473
491
|
onReject,
|
|
474
492
|
labels,
|
|
475
493
|
disabled: inert,
|
|
476
494
|
});
|
|
495
|
+
const pickedText =
|
|
496
|
+
picked.length === 0 ? "" : picked.length === 1 ? picked[0].name : pickerLabels.selected(picked.length, picked[0].name);
|
|
477
497
|
const [dragOver, setDragOver] = useState(false);
|
|
478
498
|
|
|
479
499
|
const hasFiles = (e: DragEvent) => Array.from(e.dataTransfer?.types ?? []).includes("Files");
|
|
@@ -514,34 +534,64 @@ export const FileButton = forwardRef<HTMLButtonElement, FileButtonProps>(functio
|
|
|
514
534
|
// declare it, hence the widening here rather than a second button implementation.
|
|
515
535
|
const refProp = { ref } as { ref?: Ref<HTMLButtonElement> };
|
|
516
536
|
|
|
537
|
+
const button = (
|
|
538
|
+
<Button
|
|
539
|
+
{...rest}
|
|
540
|
+
{...refProp}
|
|
541
|
+
{...dropHandlers}
|
|
542
|
+
type="button"
|
|
543
|
+
disabled={inert}
|
|
544
|
+
aria-busy={pending || undefined}
|
|
545
|
+
aria-describedby={
|
|
546
|
+
showFileName && pickedText
|
|
547
|
+
? [rest["aria-describedby"], pickedId].filter(Boolean).join(" ")
|
|
548
|
+
: rest["aria-describedby"]
|
|
549
|
+
}
|
|
550
|
+
data-drag-over={dragOver || undefined}
|
|
551
|
+
className={cn(
|
|
552
|
+
// A live drag gets the focus ring's colour as a ring: "let go here", in the
|
|
553
|
+
// same vocabulary the button already uses for "you are here".
|
|
554
|
+
dragOver && "ring-2 ring-[var(--brand)]",
|
|
555
|
+
// Beside a long file name it is the NAME that gives way, never the button.
|
|
556
|
+
showFileName && "shrink-0",
|
|
557
|
+
className,
|
|
558
|
+
)}
|
|
559
|
+
onClick={(e) => {
|
|
560
|
+
onClick?.(e);
|
|
561
|
+
if (!e.defaultPrevented) picker.open();
|
|
562
|
+
}}
|
|
563
|
+
>
|
|
564
|
+
{/* `label={null}`: the spinner is decoration here — `aria-busy` says the same
|
|
565
|
+
thing on the button, and a spoken "Loading" would run into its name. */}
|
|
566
|
+
{pending && <Spinner label={null} className="size-4" />}
|
|
567
|
+
{children}
|
|
568
|
+
</Button>
|
|
569
|
+
);
|
|
570
|
+
|
|
571
|
+
if (!showFileName) {
|
|
572
|
+
return (
|
|
573
|
+
<>
|
|
574
|
+
{button}
|
|
575
|
+
{picker.element}
|
|
576
|
+
</>
|
|
577
|
+
);
|
|
578
|
+
}
|
|
517
579
|
return (
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
className=
|
|
528
|
-
// A live drag gets the focus ring's colour as a ring: "let go here", in the
|
|
529
|
-
// same vocabulary the button already uses for "you are here".
|
|
530
|
-
dragOver && "ring-2 ring-[var(--brand)]",
|
|
531
|
-
className,
|
|
532
|
-
)}
|
|
533
|
-
onClick={(e) => {
|
|
534
|
-
onClick?.(e);
|
|
535
|
-
if (!e.defaultPrevented) picker.open();
|
|
536
|
-
}}
|
|
580
|
+
// One inline row, so the name sits beside the button wherever the button sits; the
|
|
581
|
+
// name truncates (full list in its title) rather than pushing the row wider.
|
|
582
|
+
<span className="inline-flex max-w-full min-w-0 items-center gap-2">
|
|
583
|
+
{button}
|
|
584
|
+
<span
|
|
585
|
+
id={pickedId}
|
|
586
|
+
aria-live="polite"
|
|
587
|
+
data-slot="file-button-name"
|
|
588
|
+
title={picked.map((f) => f.name).join(", ") || undefined}
|
|
589
|
+
className="min-w-0 truncate text-sm text-[var(--text-secondary)]"
|
|
537
590
|
>
|
|
538
|
-
{
|
|
539
|
-
|
|
540
|
-
{pending && <Spinner label={null} className="size-4" />}
|
|
541
|
-
{children}
|
|
542
|
-
</Button>
|
|
591
|
+
{pickedText}
|
|
592
|
+
</span>
|
|
543
593
|
{picker.element}
|
|
544
|
-
|
|
594
|
+
</span>
|
|
545
595
|
);
|
|
546
596
|
});
|
|
547
597
|
FileButton.displayName = "FileButton";
|
|
@@ -339,7 +339,7 @@ export function FileDropzone({
|
|
|
339
339
|
: dragOver
|
|
340
340
|
? "cursor-pointer border-[var(--border-strong)] bg-[var(--bg-active)]"
|
|
341
341
|
: showError
|
|
342
|
-
? "cursor-pointer border-[var(--danger-border)]"
|
|
342
|
+
? "cursor-pointer border-[var(--danger-border-strong)]"
|
|
343
343
|
: "cursor-pointer border-[var(--border)] hover:border-[var(--border-strong)]",
|
|
344
344
|
className,
|
|
345
345
|
)}
|