@eifi1/ui-kit 0.15.5 → 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 +17 -12
- package/dist/chart.d.ts +3 -3
- package/dist/components/amount-input.d.ts +10 -3
- package/dist/components/amount-input.js +165 -156
- 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 -3
- package/dist/components/calculator.d.ts +3 -3
- package/dist/components/copy-button.d.ts +3 -3
- package/dist/components/facing-pair.d.ts +3 -3
- package/dist/components/file-button.d.ts +3 -3
- 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 -3
- package/dist/components/form-actions.d.ts +4 -4
- 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 -3
- package/dist/components/number-field.d.ts +3 -3
- package/dist/components/number-input.d.ts +3 -3
- 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 -3
- 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 -3
- package/dist/components/settings-fields.d.ts +3 -3
- 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 -3
- package/dist/components/time-input.d.ts +3 -3
- 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 -3
- package/dist/components/ui.js +149 -45
- package/dist/components/ui.js.map +1 -1
- package/dist/feedback/feedback-attachment.d.ts +1 -1
- package/dist/feedback/feedback-attachment.js +24 -11
- package/dist/feedback/feedback-attachment.js.map +1 -1
- package/dist/feedback/feedback-dialog.d.ts +1 -1
- package/dist/feedback/feedback-dialog.js +6 -5
- 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 +1 -1
- package/dist/{feedback-attachment-BYAHJMOr.d.ts → feedback-attachment-WFiXJ8We.d.ts} +21 -8
- package/dist/feedback.d.ts +1 -1
- package/dist/hooks/use-file-drop.d.ts +3 -3
- package/dist/i18n/defaults.d.ts +3 -3
- package/dist/i18n/kit-labels.d.ts +2 -2
- package/dist/i18n/locales/de-CH-informal.d.ts +3 -3
- package/dist/i18n/locales/de-CH.d.ts +3 -3
- package/dist/i18n/locales/de-informal.d.ts +3 -3
- package/dist/i18n/locales/de.d.ts +3 -3
- package/dist/i18n/locales/de.js +1 -1
- package/dist/i18n/locales/de.js.map +1 -1
- package/dist/i18n/locales/es.d.ts +3 -3
- package/dist/i18n/locales/es.js +1 -1
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +3 -3
- package/dist/i18n/locales/fr.js +1 -1
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +3 -3
- package/dist/i18n/locales/hu.js +1 -1
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +3 -3
- package/dist/i18n/locales/it.js +1 -1
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +3 -3
- package/dist/i18n/locales/zh.js +1 -1
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/index.d.ts +10 -10
- package/dist/{kit-labels-BD2F7sOz.d.ts → kit-labels-DE--LmCk.d.ts} +153 -12
- 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 -3
- package/dist/rhf/form.d.ts +3 -3
- package/dist/rhf/line-items.js.map +1 -1
- package/dist/rhf.d.ts +3 -3
- package/dist/shell/app-shell.d.ts +3 -3
- package/dist/shell/top-bar-brand.d.ts +3 -3
- package/dist/shell.d.ts +3 -3
- package/dist/wizard/stepper-nav.d.ts +3 -3
- package/dist/wizard.d.ts +3 -3
- package/package.json +1 -1
- package/src/components/amount-input.tsx +39 -15
- package/src/components/authed-image.tsx +20 -2
- package/src/components/file-button.tsx +77 -27
- 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 +273 -63
- package/src/feedback/feedback-attachment.tsx +45 -14
- package/src/feedback/feedback-dialog.tsx +31 -16
- package/src/i18n/locales/de.ts +1 -1
- package/src/i18n/locales/es.ts +1 -1
- package/src/i18n/locales/fr.ts +1 -1
- package/src/i18n/locales/hu.ts +1 -1
- package/src/i18n/locales/it.ts +1 -1
- package/src/i18n/locales/zh.ts +1 -1
- package/src/lib/format.ts +24 -3
- package/src/rhf/line-items.tsx +6 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
import { ReactNode } from 'react';
|
|
3
|
-
import {
|
|
3
|
+
import { aE as KitLinkProps, aD as KitLinkComponent } from '../kit-labels-DE--LmCk.js';
|
|
4
4
|
import '../components/data-table-labels.js';
|
|
5
5
|
import '../components/mini-calendar.js';
|
|
6
6
|
import '../components/calendar-heatmap.js';
|
|
@@ -32,7 +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-attachment-
|
|
35
|
+
import '../feedback-attachment-WFiXJ8We.js';
|
|
36
36
|
import '../feedback/feedback-thread.js';
|
|
37
37
|
import '../feedback/feedback-inbox.js';
|
|
38
38
|
import '../components/account-settings-labels.js';
|
|
@@ -40,12 +40,12 @@ import '../components/confirm-dialog.js';
|
|
|
40
40
|
import '../components/floating-panel.js';
|
|
41
41
|
import '../components/bulk-action-bar.js';
|
|
42
42
|
import '../components/list.js';
|
|
43
|
+
import '../components/signed-amount.js';
|
|
43
44
|
import '../components/breadcrumbs.js';
|
|
44
45
|
import '../components/toast.js';
|
|
45
46
|
import '../components/description-list.js';
|
|
46
47
|
import '../components/line-items.js';
|
|
47
48
|
import '../components/progress-bar.js';
|
|
48
|
-
import '../components/signed-amount.js';
|
|
49
49
|
import '../components/error-boundary.js';
|
|
50
50
|
import '../components/authed-image.js';
|
|
51
51
|
import '../hooks/use-authed-src.js';
|
package/dist/shell.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ import './theme/theme-store.js';
|
|
|
13
13
|
import 'zustand/middleware';
|
|
14
14
|
import 'zustand';
|
|
15
15
|
import 'lucide-react';
|
|
16
|
-
import './kit-labels-
|
|
16
|
+
import './kit-labels-DE--LmCk.js';
|
|
17
17
|
import './components/data-table-labels.js';
|
|
18
18
|
import './components/mini-calendar.js';
|
|
19
19
|
import './components/calendar-heatmap.js';
|
|
@@ -44,7 +44,7 @@ import './components/dialog-frame.js';
|
|
|
44
44
|
import './components/modal.js';
|
|
45
45
|
import './hooks/use-search-param-state.js';
|
|
46
46
|
import './components/measured-grid.js';
|
|
47
|
-
import './feedback-attachment-
|
|
47
|
+
import './feedback-attachment-WFiXJ8We.js';
|
|
48
48
|
import './feedback/feedback-thread.js';
|
|
49
49
|
import './feedback/feedback-inbox.js';
|
|
50
50
|
import './components/account-settings-labels.js';
|
|
@@ -52,12 +52,12 @@ import './components/confirm-dialog.js';
|
|
|
52
52
|
import './components/floating-panel.js';
|
|
53
53
|
import './components/bulk-action-bar.js';
|
|
54
54
|
import './components/list.js';
|
|
55
|
+
import './components/signed-amount.js';
|
|
55
56
|
import './components/breadcrumbs.js';
|
|
56
57
|
import './components/toast.js';
|
|
57
58
|
import './components/description-list.js';
|
|
58
59
|
import './components/line-items.js';
|
|
59
60
|
import './components/progress-bar.js';
|
|
60
|
-
import './components/signed-amount.js';
|
|
61
61
|
import './components/error-boundary.js';
|
|
62
62
|
import './components/authed-image.js';
|
|
63
63
|
import './hooks/use-authed-src.js';
|
|
@@ -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,7 +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-attachment-
|
|
35
|
+
import '../feedback-attachment-WFiXJ8We.js';
|
|
36
36
|
import '../feedback/feedback-thread.js';
|
|
37
37
|
import '../feedback/feedback-inbox.js';
|
|
38
38
|
import '../components/account-settings-labels.js';
|
|
@@ -40,12 +40,12 @@ import '../components/confirm-dialog.js';
|
|
|
40
40
|
import '../components/floating-panel.js';
|
|
41
41
|
import '../components/bulk-action-bar.js';
|
|
42
42
|
import '../components/list.js';
|
|
43
|
+
import '../components/signed-amount.js';
|
|
43
44
|
import '../components/breadcrumbs.js';
|
|
44
45
|
import '../components/toast.js';
|
|
45
46
|
import '../components/description-list.js';
|
|
46
47
|
import '../components/line-items.js';
|
|
47
48
|
import '../components/progress-bar.js';
|
|
48
|
-
import '../components/signed-amount.js';
|
|
49
49
|
import '../components/error-boundary.js';
|
|
50
50
|
import '../components/authed-image.js';
|
|
51
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,7 +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-attachment-
|
|
40
|
+
import './feedback-attachment-WFiXJ8We.js';
|
|
41
41
|
import './feedback/feedback-thread.js';
|
|
42
42
|
import './feedback/feedback-inbox.js';
|
|
43
43
|
import './components/account-settings-labels.js';
|
|
@@ -45,12 +45,12 @@ import './components/confirm-dialog.js';
|
|
|
45
45
|
import './components/floating-panel.js';
|
|
46
46
|
import './components/bulk-action-bar.js';
|
|
47
47
|
import './components/list.js';
|
|
48
|
+
import './components/signed-amount.js';
|
|
48
49
|
import './components/breadcrumbs.js';
|
|
49
50
|
import './components/toast.js';
|
|
50
51
|
import './components/description-list.js';
|
|
51
52
|
import './components/line-items.js';
|
|
52
53
|
import './components/progress-bar.js';
|
|
53
|
-
import './components/signed-amount.js';
|
|
54
54
|
import './components/error-boundary.js';
|
|
55
55
|
import './components/authed-image.js';
|
|
56
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.",
|
|
@@ -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`
|
|
@@ -296,9 +303,13 @@ function isResultOf(previous: string, text: string, settle: (text: string) => st
|
|
|
296
303
|
}
|
|
297
304
|
|
|
298
305
|
export const AmountInput = forwardRef<HTMLInputElement, AmountInputProps>(
|
|
299
|
-
({ 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) => {
|
|
300
307
|
const generatedId = useId();
|
|
301
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;
|
|
302
313
|
const invalid = Boolean(invalidProp) || ariaInvalid === true || ariaInvalid === "true";
|
|
303
314
|
const editable = !!onCurrencyChange;
|
|
304
315
|
// On phones we suppress the OS keyboard (inputMode="none" below) and show our
|
|
@@ -454,8 +465,17 @@ export const AmountInput = forwardRef<HTMLInputElement, AmountInputProps>(
|
|
|
454
465
|
<div ref={wrapperRef} className={cn("w-full", className)}>
|
|
455
466
|
{/* Inner relative box holds the input + its absolutely-positioned
|
|
456
467
|
trailing controls (calculator / currency), so those stay anchored to
|
|
457
|
-
the INPUT even when the mobile math bar is rendered below (#334).
|
|
458
|
-
|
|
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
|
+
>
|
|
459
479
|
<input
|
|
460
480
|
ref={setRefs}
|
|
461
481
|
id={fieldId}
|
|
@@ -513,18 +533,15 @@ export const AmountInput = forwardRef<HTMLInputElement, AmountInputProps>(
|
|
|
513
533
|
invalid && FIELD_INVALID,
|
|
514
534
|
)}
|
|
515
535
|
aria-invalid={invalid || undefined}
|
|
516
|
-
aria-describedby={
|
|
536
|
+
aria-describedby={describedBy}
|
|
517
537
|
aria-required={ariaRequired}
|
|
518
538
|
/>
|
|
519
|
-
{label
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
{label}
|
|
526
|
-
</label>
|
|
527
|
-
)}
|
|
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. */}
|
|
528
545
|
{/* Trailing controls share one flex track so the calculator icon and the
|
|
529
546
|
currency suffix/picker sit side by side without overlapping. */}
|
|
530
547
|
{/* The display shape has no box to inset from, so the controls align to the
|
|
@@ -627,7 +644,14 @@ export const AmountInput = forwardRef<HTMLInputElement, AmountInputProps>(
|
|
|
627
644
|
})}
|
|
628
645
|
</DropdownPanel>
|
|
629
646
|
)}
|
|
630
|
-
</
|
|
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
|
+
)}
|
|
631
655
|
{showNumpad && (
|
|
632
656
|
<NumberPadSheet
|
|
633
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
|
|
@@ -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";
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { isValidElement } from "react";
|
|
2
2
|
import type { ComponentPropsWithoutRef, ReactNode } from "react";
|
|
3
|
+
import type { LucideIcon } from "lucide-react";
|
|
3
4
|
import { cn } from "../lib/cn";
|
|
4
5
|
import { useKitLabels } from "../i18n/kit-labels";
|
|
5
|
-
import { Button, Spinner, type ButtonVariant } from "./ui";
|
|
6
|
+
import { Button, Spinner, type ButtonProps, type ButtonVariant } from "./ui";
|
|
6
7
|
|
|
7
8
|
/** The words {@link FormActions} renders on its own behalf — the `form` namespace. */
|
|
8
9
|
export interface FormActionsLabels {
|
|
@@ -28,6 +29,25 @@ export type FormActionsAlign = "start" | "center" | "end" | "between";
|
|
|
28
29
|
*/
|
|
29
30
|
export type FormActionsPlacement = "inline" | "sticky" | "dialog";
|
|
30
31
|
|
|
32
|
+
/**
|
|
33
|
+
* What a `sticky` row sticks to — see {@link FormActionsProps.stickyWithin}.
|
|
34
|
+
* - `viewport` (default): the page's (or AppShell pane's) bottom edge, lifted onto
|
|
35
|
+
* the phone's bottom nav by `--app-nav-h`.
|
|
36
|
+
* - `container`: the bottom of whatever scroll container it is in — a dialog body, a
|
|
37
|
+
* side pane, a panel with `overflow-y-auto` — at `bottom: 0`, with no nav offset.
|
|
38
|
+
*/
|
|
39
|
+
export type FormActionsStickyWithin = "viewport" | "container";
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Extra attributes for the save button — an `id`, `data-*` hooks for tests or
|
|
43
|
+
* analytics, an `aria-describedby`. The props FormActions drives itself (type, click,
|
|
44
|
+
* disabled, variant, form, children, the busy state) are not among them.
|
|
45
|
+
*/
|
|
46
|
+
export type FormActionsSubmitProps = Omit<
|
|
47
|
+
ButtonProps,
|
|
48
|
+
"type" | "onClick" | "disabled" | "disabledReason" | "variant" | "form" | "children" | "aria-busy"
|
|
49
|
+
> & { [key: `data-${string}`]: string | number | boolean | undefined };
|
|
50
|
+
|
|
31
51
|
/** The start-side action of {@link FormActionsProps.destructive}, as data. */
|
|
32
52
|
export interface FormActionsDestructive {
|
|
33
53
|
label: ReactNode;
|
|
@@ -65,10 +85,21 @@ export interface FormActionsProps extends Omit<ComponentPropsWithoutRef<"div">,
|
|
|
65
85
|
cancelLabel?: ReactNode;
|
|
66
86
|
/** The save button's text while `pending` ("Saving…"). Default: `submitLabel`. */
|
|
67
87
|
pendingLabel?: ReactNode;
|
|
88
|
+
/**
|
|
89
|
+
* A Lucide icon before the save button's text. While `pending` the spinner takes
|
|
90
|
+
* ITS place rather than being added in front of it — an icon written into
|
|
91
|
+
* `submitLabel` itself stays, so the button would show the spinner AND the glyph,
|
|
92
|
+
* two statuses side by side (keksdose's crop Apply needed a `pendingLabel` copy of
|
|
93
|
+
* its text only to drop the check mark).
|
|
94
|
+
*/
|
|
95
|
+
submitIcon?: LucideIcon;
|
|
96
|
+
/** Attributes passed through to the save button: `id`, `data-*`, `aria-*`, a
|
|
97
|
+
* `className`. See {@link FormActionsSubmitProps}. */
|
|
98
|
+
submitProps?: FormActionsSubmitProps;
|
|
68
99
|
/**
|
|
69
100
|
* The save is running: a spinner in the save button, `aria-busy` on it, and the
|
|
70
101
|
* button disabled, so a second click cannot send the form twice. Cancel stays
|
|
71
|
-
* usable.
|
|
102
|
+
* usable. The spinner goes in front of the text, or in place of {@link submitIcon}.
|
|
72
103
|
*/
|
|
73
104
|
pending?: boolean;
|
|
74
105
|
/** Disable save for a reason of the form's own (an unbalanced entry). */
|
|
@@ -85,10 +116,39 @@ export interface FormActionsProps extends Omit<ComponentPropsWithoutRef<"div">,
|
|
|
85
116
|
* is `between`-aligned unless `align` says otherwise.
|
|
86
117
|
*/
|
|
87
118
|
destructive?: ReactNode | FormActionsDestructive;
|
|
119
|
+
/**
|
|
120
|
+
* Something neutral at the START of the row — a caption ("Last saved 2 min ago",
|
|
121
|
+
* "Drag the corners to crop"), a "View activity" link. After {@link destructive}
|
|
122
|
+
* when both are given; like it, makes the row `between`-aligned unless `align` says
|
|
123
|
+
* otherwise. `children` are for further ACTIONS, beside Cancel.
|
|
124
|
+
*/
|
|
125
|
+
start?: ReactNode;
|
|
88
126
|
/** Horizontal alignment. Default `end` (`between` with a `destructive` action). */
|
|
89
127
|
align?: FormActionsAlign;
|
|
90
128
|
/** See {@link FormActionsPlacement}. Default `inline`. */
|
|
91
129
|
placement?: FormActionsPlacement;
|
|
130
|
+
/**
|
|
131
|
+
* With `placement="sticky"`: what the row sticks to. Default `viewport`.
|
|
132
|
+
*
|
|
133
|
+
* CSS sticky always sticks to the NEAREST scroll container, so the default row
|
|
134
|
+
* already holds inside a scrolling pane — but it offsets itself by `--app-nav-h`,
|
|
135
|
+
* which AppShell publishes on the document: inside a pane or a portalled dialog
|
|
136
|
+
* body on a phone it then floats a nav's height above that container's bottom,
|
|
137
|
+
* with the content scrolling through the gap. `container` drops that offset.
|
|
138
|
+
*
|
|
139
|
+
* What neither value can fix, because it is the layout, not the row:
|
|
140
|
+
* - sticky moves only within its PARENT box. Inside a DataTable expansion row the
|
|
141
|
+
* parent is the expansion cell, so the row pins to the table's own scroller
|
|
142
|
+
* (`overflow-auto`, bounded by `maxBodyHeight`) and only while the cell spans
|
|
143
|
+
* that scroller's bottom edge — not to the page.
|
|
144
|
+
* - any `overflow` other than `visible`/`clip` between the row and the scroller
|
|
145
|
+
* you mean (an `overflow-x-auto` wrapper, an `overflow-hidden` card) becomes the
|
|
146
|
+
* scroll container; if THAT box does not scroll, the row never sticks.
|
|
147
|
+
* - the offset is resolved against the scroller's content box, so a scroller with
|
|
148
|
+
* bottom padding leaves that strip uncovered; give the row a negative `bottom`
|
|
149
|
+
* through `style` and the same amount back as padding.
|
|
150
|
+
*/
|
|
151
|
+
stickyWithin?: FormActionsStickyWithin;
|
|
92
152
|
/** The `id` of the `<form>` the save button submits, when the row is rendered
|
|
93
153
|
* outside it (a dialog's footer slot). */
|
|
94
154
|
form?: string;
|
|
@@ -121,13 +181,17 @@ export function FormActions({
|
|
|
121
181
|
submitLabel,
|
|
122
182
|
cancelLabel,
|
|
123
183
|
pendingLabel,
|
|
184
|
+
submitIcon: SubmitIcon,
|
|
185
|
+
submitProps,
|
|
124
186
|
pending = false,
|
|
125
187
|
submitDisabled = false,
|
|
126
188
|
submitDisabledReason,
|
|
127
189
|
submitVariant = "brand",
|
|
128
190
|
destructive,
|
|
191
|
+
start: startSlot,
|
|
129
192
|
align,
|
|
130
193
|
placement = "inline",
|
|
194
|
+
stickyWithin = "viewport",
|
|
131
195
|
form,
|
|
132
196
|
children,
|
|
133
197
|
className,
|
|
@@ -136,8 +200,9 @@ export function FormActions({
|
|
|
136
200
|
}: FormActionsProps) {
|
|
137
201
|
const labels = useKitLabels("form", DEFAULT_FORM_ACTIONS_LABELS);
|
|
138
202
|
const hasDestructive = destructive !== undefined && destructive !== null && destructive !== false;
|
|
139
|
-
const
|
|
140
|
-
const
|
|
203
|
+
const hasStart = startSlot !== undefined && startSlot !== null && startSlot !== false;
|
|
204
|
+
const justify = align ?? (hasDestructive || hasStart ? "between" : "end");
|
|
205
|
+
const destructiveNode = !hasDestructive ? null : isDestructiveData(destructive) ? (
|
|
141
206
|
<Button
|
|
142
207
|
type="button"
|
|
143
208
|
variant="ghost"
|
|
@@ -157,6 +222,7 @@ export function FormActions({
|
|
|
157
222
|
<div
|
|
158
223
|
data-slot="form-actions"
|
|
159
224
|
data-placement={placement}
|
|
225
|
+
data-sticky-within={placement === "sticky" ? stickyWithin : undefined}
|
|
160
226
|
{...rest}
|
|
161
227
|
style={
|
|
162
228
|
placement === "sticky"
|
|
@@ -165,15 +231,22 @@ export function FormActions({
|
|
|
165
231
|
// On the phone's bottom nav, not behind it (keksdose live #361): `bottom-0`
|
|
166
232
|
// pinned the row under AppShell's nav, which covers the page's last ~56px.
|
|
167
233
|
// `--app-nav-h` is the nav's measured height (0px from md up, and outside an
|
|
168
|
-
// AppShell); the 1px overlaps the two top borders into one line.
|
|
169
|
-
|
|
234
|
+
// AppShell); the 1px overlaps the two top borders into one line. In a
|
|
235
|
+
// container the nav is not underneath: `--app-nav-h` is the document's,
|
|
236
|
+
// and would hold the row that far above the container's own edge.
|
|
237
|
+
bottom: stickyWithin === "container" ? 0 : "max(0px, calc(var(--app-nav-h, 0px) - 1px))",
|
|
170
238
|
...style,
|
|
171
239
|
}
|
|
172
240
|
: style
|
|
173
241
|
}
|
|
174
242
|
className={cn("flex flex-wrap items-center gap-2", ALIGN_CLASS[justify], PLACEMENT_CLASS[placement], className)}
|
|
175
243
|
>
|
|
176
|
-
{
|
|
244
|
+
{(hasDestructive || hasStart) && (
|
|
245
|
+
<div className="me-auto flex min-w-0 flex-wrap items-center gap-2">
|
|
246
|
+
{destructiveNode}
|
|
247
|
+
{hasStart && startSlot}
|
|
248
|
+
</div>
|
|
249
|
+
)}
|
|
177
250
|
<div className="flex flex-wrap items-center gap-2">
|
|
178
251
|
{children}
|
|
179
252
|
{onCancel && (
|
|
@@ -182,6 +255,7 @@ export function FormActions({
|
|
|
182
255
|
</Button>
|
|
183
256
|
)}
|
|
184
257
|
<Button
|
|
258
|
+
{...submitProps}
|
|
185
259
|
type={onSubmit ? "button" : "submit"}
|
|
186
260
|
form={form}
|
|
187
261
|
onClick={onSubmit}
|
|
@@ -190,7 +264,11 @@ export function FormActions({
|
|
|
190
264
|
disabledReason={pending ? undefined : submitDisabledReason}
|
|
191
265
|
aria-busy={pending || undefined}
|
|
192
266
|
>
|
|
193
|
-
{pending
|
|
267
|
+
{pending ? (
|
|
268
|
+
<Spinner label={null} className="size-4" />
|
|
269
|
+
) : (
|
|
270
|
+
SubmitIcon && <SubmitIcon aria-hidden className="size-4 shrink-0" />
|
|
271
|
+
)}
|
|
194
272
|
{pending && pendingLabel !== undefined ? pendingLabel : saveLabel}
|
|
195
273
|
</Button>
|
|
196
274
|
</div>
|