@eifi1/ui-kit 0.55.0 → 0.57.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 +9 -6
- package/dist/billing/subscription-standing.d.ts +1 -1
- package/dist/billing/subscription-standing.js.map +1 -1
- package/dist/components/button.js +1 -0
- package/dist/components/button.js.map +1 -1
- package/dist/components/data-table.d.ts +16 -0
- package/dist/components/data-table.js +17 -0
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/icon-button.d.ts +3 -3
- package/dist/components/icon-button.js +1 -0
- package/dist/components/icon-button.js.map +1 -1
- package/dist/components/legended-series-chart.d.ts +5 -2
- package/dist/components/legended-series-chart.js +9 -2
- package/dist/components/legended-series-chart.js.map +1 -1
- package/dist/components/quantity-field.d.ts +61 -0
- package/dist/components/quantity-field.js +95 -0
- package/dist/components/quantity-field.js.map +1 -0
- package/dist/components/quantity-input.d.ts +139 -0
- package/dist/components/quantity-input.js +188 -0
- package/dist/components/quantity-input.js.map +1 -0
- package/dist/components/quantity-labels.d.ts +2 -0
- package/dist/components/quantity-labels.js +11 -0
- package/dist/components/quantity-labels.js.map +1 -0
- package/dist/eslint/index.js +1 -1
- package/dist/feedback/feedback-inbox-bulk.d.ts +88 -0
- package/dist/feedback/feedback-inbox-bulk.js +316 -0
- package/dist/feedback/feedback-inbox-bulk.js.map +1 -0
- package/dist/feedback/feedback-inbox-page.d.ts +47 -3
- package/dist/feedback/feedback-inbox-page.js +19 -4
- package/dist/feedback/feedback-inbox-page.js.map +1 -1
- package/dist/feedback/feedback-row-detail.d.ts +5 -1
- package/dist/feedback/feedback-row-detail.js.map +1 -1
- package/dist/feedback/feedback-table-labels.js +10 -1
- package/dist/feedback/feedback-table-labels.js.map +1 -1
- package/dist/feedback/feedback-table.d.ts +26 -0
- package/dist/feedback/feedback-table.js.map +1 -1
- package/dist/feedback.d.ts +1 -1
- package/dist/feedback.js.map +1 -1
- package/dist/i18n/defaults.js +3 -1
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.js +20 -1
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +4 -0
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/en.js +26 -3
- package/dist/i18n/locales/en.js.map +3 -3
- package/dist/i18n/locales/es.js +20 -1
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.js +25 -1
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.js +22 -1
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.js +21 -1
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.js +20 -1
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.js +12 -1
- package/dist/i18n/review.js.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/quantity.d.ts +107 -0
- package/dist/lib/quantity.js +166 -0
- package/dist/lib/quantity.js.map +1 -0
- package/dist/rhf/fields.d.ts +28 -0
- package/dist/rhf/fields.js +82 -0
- package/dist/rhf/fields.js.map +1 -1
- package/dist/rhf.js.map +1 -1
- package/dist/theme/contrast.d.ts +1 -1
- package/dist/theme/palette-store.d.ts +1 -1
- package/dist/theme/text-size.d.ts +1 -1
- package/dist/theme/theme-store.d.ts +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import type { ReactNode, Ref } from "react";
|
|
2
|
+
import { DEFAULT_QUANTITY_LABELS } from "./quantity-labels.js";
|
|
3
|
+
import { type LabelOverride } from "../i18n/kit-labels.js";
|
|
4
|
+
import { type DataAttributes } from "../lib/data-attributes.js";
|
|
5
|
+
import { type Quantity, type QuantityFamily, type QuantityUnit } from "../lib/quantity.js";
|
|
6
|
+
/** The words of {@link QuantityInput} and {@link QuantityField} — the `quantity` namespace. */
|
|
7
|
+
export interface QuantityLabels {
|
|
8
|
+
/** The pieces unit, in the select and after a count: "pcs", "Stk.", "pce", "pz.". The
|
|
9
|
+
* one unit with a word — g, kg, ml, cl, dl and l are SI symbols in every language. */
|
|
10
|
+
pcs: string;
|
|
11
|
+
/** The unit select's accessible name — with the field's name when it has one ("Unit
|
|
12
|
+
* for “Flour”"), else alone. */
|
|
13
|
+
unit: (field?: string) => string;
|
|
14
|
+
/** The suggestion line's lead when the app names no source ({@link QuantityField}'s
|
|
15
|
+
* `suggestionFrom`). */
|
|
16
|
+
suggested: string;
|
|
17
|
+
/** The suggestion button's accessible name: take this quantity ("Use 500 g"). It holds
|
|
18
|
+
* the quantity the button shows, the words a voice user says (WCAG 2.5.3). */
|
|
19
|
+
takeSuggestion: (quantity: string) => string;
|
|
20
|
+
}
|
|
21
|
+
export { DEFAULT_QUANTITY_LABELS };
|
|
22
|
+
/** {@link QuantityInput}'s props. */
|
|
23
|
+
export interface QuantityInputProps extends DataAttributes {
|
|
24
|
+
/** The quantity: an amount (`null` for none yet) and its unit. */
|
|
25
|
+
value: Quantity;
|
|
26
|
+
/**
|
|
27
|
+
* The next quantity — when a typed amount commits (blur or Enter, as NumberField
|
|
28
|
+
* commits; never per keystroke) and when another unit is picked. The amount is
|
|
29
|
+
* already rounded to the unit's `digits` and clamped to `max`.
|
|
30
|
+
*/
|
|
31
|
+
onChange: (next: Quantity) => void;
|
|
32
|
+
/** Offer only this family's units — `"mass"` (g, kg), `"volume"` (ml, cl, dl, l) or
|
|
33
|
+
* `"count"` (pcs). Without it (and without `units`), every unit. */
|
|
34
|
+
family?: QuantityFamily;
|
|
35
|
+
/**
|
|
36
|
+
* The units to offer, explicitly — WINS over `family`, and is listed small to large
|
|
37
|
+
* whatever its order (`["ml", "dl", "l"]` is a volume list without centilitres). Each
|
|
38
|
+
* should lie inside `family` when both are given; one that does not is still offered,
|
|
39
|
+
* and a development build says so on the console. A value whose unit is not in the
|
|
40
|
+
* list (a 75 cl that `parseQuantity` read off a label) is still shown: the select lists
|
|
41
|
+
* its unit for as long as it is the value's.
|
|
42
|
+
*/
|
|
43
|
+
units?: readonly QuantityUnit[];
|
|
44
|
+
/** An emptied amount commits `null`. Without it an empty amount is no answer and snaps
|
|
45
|
+
* back to the last one, as NumberField's does. */
|
|
46
|
+
nullable?: boolean;
|
|
47
|
+
/** Refuse 0: it snaps back like text that is no number (default `true` — a shopping
|
|
48
|
+
* list has no "0 g"). A negative amount is refused either way. */
|
|
49
|
+
positive?: boolean;
|
|
50
|
+
/** The largest amount, in whichever unit is chosen — NumberField's `max`: a typed amount
|
|
51
|
+
* above it commits as `max`. A limit in a physical unit ("at most 25 kg") is the form's
|
|
52
|
+
* validation, which sees the unit too. */
|
|
53
|
+
max?: number;
|
|
54
|
+
/**
|
|
55
|
+
* Decimals per unit, over {@link QUANTITY_DIGITS} (g 0, kg 3, ml 0, cl 1, dl 2, l 3,
|
|
56
|
+
* pcs 0): `{ kg: 2 }`, `{ pcs: 1 }`. A typed amount is rounded to them on commit, and
|
|
57
|
+
* shown with at most that many. Cut to the unit's milli grid (g and ml none, cl 1, dl 2,
|
|
58
|
+
* kg, l and pcs 3), so every amount still sums exactly.
|
|
59
|
+
*/
|
|
60
|
+
digits?: Partial<Record<QuantityUnit, number>>;
|
|
61
|
+
/** BCP 47 tag for the amount's decimal mark ("1,5" in de-DE, "1.5" in de-CH); default
|
|
62
|
+
* the `<UiKitProvider locale>`, else the runtime's — NumberField's `locale`. */
|
|
63
|
+
locale?: string;
|
|
64
|
+
/**
|
|
65
|
+
* Another unit converts the amount: `true` turns 500 g into 0.5 kg. `false` (default)
|
|
66
|
+
* keeps the amount as typed — 500 g becomes 500 kg — because the usual reason to change
|
|
67
|
+
* the unit is that the unit was wrong, not the weight. Either way the amount lands on
|
|
68
|
+
* the new unit's grid (1.5 kg kept as typed in grams is 2 g); a conversion within a
|
|
69
|
+
* family is exact, and across families (`units` mixing them) the amount is kept.
|
|
70
|
+
*/
|
|
71
|
+
convertOnUnitChange?: boolean;
|
|
72
|
+
/** Both controls, the amount and the unit. */
|
|
73
|
+
disabled?: boolean;
|
|
74
|
+
/** Paints both controls as wrong — {@link Input}'s `invalid`. */
|
|
75
|
+
invalid?: boolean;
|
|
76
|
+
/** The amount `<input>`'s id — what a label's `htmlFor` points at. */
|
|
77
|
+
id?: string;
|
|
78
|
+
/** Names the amount when there is no label (a table cell). The unit select is named
|
|
79
|
+
* after it: "Unit for “<aria-label>”". */
|
|
80
|
+
"aria-label"?: string;
|
|
81
|
+
/** Set on the amount input (and the unit select, which the same caption and error
|
|
82
|
+
* describe) — a form library's control slot spreads these. */
|
|
83
|
+
"aria-describedby"?: string;
|
|
84
|
+
"aria-invalid"?: boolean | "true" | "false";
|
|
85
|
+
"aria-required"?: boolean | "true" | "false";
|
|
86
|
+
placeholder?: string;
|
|
87
|
+
/** Prop > `<UiKitProvider labels={{ quantity }}>` > English. */
|
|
88
|
+
labels?: LabelOverride<QuantityLabels>;
|
|
89
|
+
/** The root: the row holding the amount and the unit. */
|
|
90
|
+
className?: string;
|
|
91
|
+
/** The amount `<input>`. */
|
|
92
|
+
inputClassName?: string;
|
|
93
|
+
/** The unit `<select>`. */
|
|
94
|
+
selectClassName?: string;
|
|
95
|
+
/** The amount `<input>` — focus on error, `select()`. */
|
|
96
|
+
ref?: Ref<HTMLInputElement>;
|
|
97
|
+
}
|
|
98
|
+
/** @internal What {@link QuantityField} hands the row on top of the bare control's props. */
|
|
99
|
+
export interface QuantityRowProps extends QuantityInputProps {
|
|
100
|
+
/** The floating label in the amount's box. */
|
|
101
|
+
label?: ReactNode;
|
|
102
|
+
/** What rides the label's line — a resolved `labelHint` (a FieldHint, a help "?"). */
|
|
103
|
+
labelHint?: ReactNode;
|
|
104
|
+
/** The field's name for the unit select's ("Unit for “Flour”") where the label is
|
|
105
|
+
* drawn outside the row — RhfQuantityField's, above it. Default the label's text, else
|
|
106
|
+
* `aria-label`. */
|
|
107
|
+
fieldName?: string;
|
|
108
|
+
}
|
|
109
|
+
/** @internal The unit a value with none starts in — the first one the select would offer
|
|
110
|
+
* (RhfQuantityField's, for a form whose default is `null`). */
|
|
111
|
+
export declare function firstOfferedUnit(units: readonly QuantityUnit[] | undefined, family: QuantityFamily | undefined): QuantityUnit;
|
|
112
|
+
/** @internal The amount and unit row, with the floating label {@link QuantityField} gives
|
|
113
|
+
* it. Not on the barrel: QuantityInput is this without a label. */
|
|
114
|
+
export declare function QuantityRow(props: QuantityRowProps): import("react").JSX.Element;
|
|
115
|
+
/**
|
|
116
|
+
* A quantity — an amount and its unit — as one control: the amount, typed in the
|
|
117
|
+
* locale's decimal mark, beside a select of units. The bare control, for a table row or
|
|
118
|
+
* a line of a list; {@link QuantityField} is the labelled field with a caption, an error
|
|
119
|
+
* and a suggestion to take.
|
|
120
|
+
*
|
|
121
|
+
* ```tsx
|
|
122
|
+
* <QuantityInput aria-label="Flour" family="mass" value={qty} onChange={setQty} />
|
|
123
|
+
* ```
|
|
124
|
+
*
|
|
125
|
+
* THE AMOUNT is NumberField's loop over {@link NumberInput}: a draft while it is typed
|
|
126
|
+
* (grouping read off, the locale's mark kept), resolved on blur or Enter — rounded to the
|
|
127
|
+
* unit's `digits`, clamped to `max` — and `onChange` only then. Empty, or text that is no
|
|
128
|
+
* number, snaps back (empty commits `null` with `nullable`); so does 0 under `positive`
|
|
129
|
+
* (the default), and a negative amount always.
|
|
130
|
+
*
|
|
131
|
+
* THE UNIT is a native select of {@link QUANTITY_UNITS} — `family`'s, or `units`' — with
|
|
132
|
+
* pieces in the reader's word (`quantity.pcs`). Picking one is a commit of its own;
|
|
133
|
+
* `convertOnUnitChange` decides whether the amount is converted or kept as typed.
|
|
134
|
+
*
|
|
135
|
+
* The value's arithmetic, formatting and parsing are `lib/quantity`'s — {@link toBaseQuantity},
|
|
136
|
+
* {@link convertQuantity}, {@link sumQuantities}, {@link formatQuantity}, {@link parseQuantity} —
|
|
137
|
+
* for the list's totals and a comparison table outside the field.
|
|
138
|
+
*/
|
|
139
|
+
export declare function QuantityInput(props: QuantityInputProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useId, useRef, useState } from "react";
|
|
4
|
+
import { NumberInput } from "./number-input.js";
|
|
5
|
+
import { Select } from "./select.js";
|
|
6
|
+
import { FIELD_FLOATING_PAD } from "./field-base.js";
|
|
7
|
+
import { DEFAULT_QUANTITY_LABELS } from "./quantity-labels.js";
|
|
8
|
+
import { useKitLabels, useKitLocale } from "../i18n/kit-labels.js";
|
|
9
|
+
import { cn } from "../lib/cn.js";
|
|
10
|
+
import { formatResult } from "../lib/calc.js";
|
|
11
|
+
import { dataAttributes } from "../lib/data-attributes.js";
|
|
12
|
+
import { decimalMark, localizeMark } from "../lib/decimal-marks.js";
|
|
13
|
+
import { plainText } from "../lib/plain-text.js";
|
|
14
|
+
import {
|
|
15
|
+
QUANTITY_DIGITS,
|
|
16
|
+
QUANTITY_UNITS,
|
|
17
|
+
convertQuantity,
|
|
18
|
+
quantityFamily,
|
|
19
|
+
quantityMaxDigits
|
|
20
|
+
} from "../lib/quantity.js";
|
|
21
|
+
import { roundHalfAway } from "../lib/round.js";
|
|
22
|
+
function devWarnings() {
|
|
23
|
+
return Boolean(import.meta.env?.DEV) && !import.meta.env?.VITEST;
|
|
24
|
+
}
|
|
25
|
+
function isUnit(unit) {
|
|
26
|
+
return typeof unit === "string" && QUANTITY_UNITS.includes(unit);
|
|
27
|
+
}
|
|
28
|
+
function digitsFor(unit, digits) {
|
|
29
|
+
const wanted = digits?.[unit] ?? QUANTITY_DIGITS[unit] ?? 3;
|
|
30
|
+
const whole = Number.isFinite(wanted) ? Math.trunc(wanted) : QUANTITY_DIGITS[unit];
|
|
31
|
+
return Math.min(Math.max(whole, 0), isUnit(unit) ? quantityMaxDigits(unit) : 3);
|
|
32
|
+
}
|
|
33
|
+
function onGrid(amount, unit, positive) {
|
|
34
|
+
const places = isUnit(unit) ? quantityMaxDigits(unit) : 3;
|
|
35
|
+
const rounded = roundHalfAway(amount, places);
|
|
36
|
+
return positive && rounded <= 0 && amount > 0 ? 10 ** -places : rounded;
|
|
37
|
+
}
|
|
38
|
+
function offeredUnits(units, family, current) {
|
|
39
|
+
const known = units?.filter(isUnit) ?? [];
|
|
40
|
+
const allowed = known.length > 0 ? known : QUANTITY_UNITS.filter((u) => !family || quantityFamily(u) === family);
|
|
41
|
+
return QUANTITY_UNITS.filter((u) => allowed.includes(u) || u === current);
|
|
42
|
+
}
|
|
43
|
+
function firstOfferedUnit(units, family) {
|
|
44
|
+
const known = units?.filter(isUnit) ?? [];
|
|
45
|
+
return QUANTITY_UNITS.find((u) => known.length > 0 ? known.includes(u) : !family || quantityFamily(u) === family);
|
|
46
|
+
}
|
|
47
|
+
function QuantityRow(props) {
|
|
48
|
+
const {
|
|
49
|
+
ref,
|
|
50
|
+
value,
|
|
51
|
+
onChange,
|
|
52
|
+
family,
|
|
53
|
+
units,
|
|
54
|
+
nullable = false,
|
|
55
|
+
positive = true,
|
|
56
|
+
max,
|
|
57
|
+
digits,
|
|
58
|
+
locale: localeProp,
|
|
59
|
+
convertOnUnitChange = false,
|
|
60
|
+
disabled,
|
|
61
|
+
invalid,
|
|
62
|
+
id,
|
|
63
|
+
"aria-label": ariaLabel,
|
|
64
|
+
"aria-describedby": ariaDescribedBy,
|
|
65
|
+
"aria-invalid": ariaInvalid,
|
|
66
|
+
"aria-required": ariaRequired,
|
|
67
|
+
placeholder,
|
|
68
|
+
labels: labelsProp,
|
|
69
|
+
className,
|
|
70
|
+
inputClassName,
|
|
71
|
+
selectClassName,
|
|
72
|
+
label,
|
|
73
|
+
labelHint,
|
|
74
|
+
fieldName
|
|
75
|
+
} = props;
|
|
76
|
+
const labels = useKitLabels("quantity", DEFAULT_QUANTITY_LABELS, labelsProp);
|
|
77
|
+
const locale = useKitLocale(localeProp);
|
|
78
|
+
const mark = decimalMark(locale);
|
|
79
|
+
const amount = value.amount !== null && Number.isFinite(value.amount) ? value.amount : null;
|
|
80
|
+
const unit = value.unit;
|
|
81
|
+
const places = digitsFor(unit, digits);
|
|
82
|
+
const format = (n, decimals) => n === null ? "" : localizeMark(formatResult(roundHalfAway(n, decimals)), mark);
|
|
83
|
+
const [draft, setDraft] = useState(() => format(amount, places));
|
|
84
|
+
const [seen, setSeen] = useState({ amount, unit, mark, places });
|
|
85
|
+
if (!Object.is(seen.amount, amount) || seen.unit !== unit || seen.mark !== mark || seen.places !== places) {
|
|
86
|
+
setSeen({ amount, unit, mark, places });
|
|
87
|
+
setDraft(format(amount, places));
|
|
88
|
+
}
|
|
89
|
+
const commit = (text) => {
|
|
90
|
+
const trimmed = text.trim();
|
|
91
|
+
const snapBack = () => setDraft(format(amount, places));
|
|
92
|
+
let next;
|
|
93
|
+
if (trimmed === "") {
|
|
94
|
+
if (!nullable) return snapBack();
|
|
95
|
+
next = null;
|
|
96
|
+
} else {
|
|
97
|
+
const parsed = Number(trimmed.replace(/,/g, "."));
|
|
98
|
+
if (!Number.isFinite(parsed)) return snapBack();
|
|
99
|
+
next = roundHalfAway(parsed, places);
|
|
100
|
+
if (max !== void 0 && next > max) next = max;
|
|
101
|
+
if (next < 0 || positive && next === 0) return snapBack();
|
|
102
|
+
}
|
|
103
|
+
setDraft(format(next, places));
|
|
104
|
+
if (!Object.is(next, amount)) onChange({ amount: next, unit });
|
|
105
|
+
};
|
|
106
|
+
const pickUnit = (picked) => {
|
|
107
|
+
if (!isUnit(picked) || picked === unit) return;
|
|
108
|
+
let next = amount;
|
|
109
|
+
if (next !== null) {
|
|
110
|
+
if (convertOnUnitChange) next = convertQuantity({ amount: next, unit }, picked)?.amount ?? next;
|
|
111
|
+
next = onGrid(next, picked, positive);
|
|
112
|
+
}
|
|
113
|
+
onChange({ amount: next, unit: picked });
|
|
114
|
+
};
|
|
115
|
+
const offered = offeredUnits(units, family, unit);
|
|
116
|
+
const warned = useRef(false);
|
|
117
|
+
const unitsKey = units === void 0 ? void 0 : JSON.stringify(units);
|
|
118
|
+
useEffect(() => {
|
|
119
|
+
if (warned.current || !devWarnings() || unitsKey === void 0) return;
|
|
120
|
+
const list = JSON.parse(unitsKey);
|
|
121
|
+
const unknown = list.filter((u) => !isUnit(u));
|
|
122
|
+
const outside = family ? list.filter((u) => isUnit(u) && quantityFamily(u) !== family) : [];
|
|
123
|
+
if (unknown.length === 0 && outside.length === 0 && list.length > 0) return;
|
|
124
|
+
warned.current = true;
|
|
125
|
+
const problem = list.length === 0 ? "is empty, so the field offers its family's units" : unknown.length > 0 ? `names ${JSON.stringify(unknown)}, which no quantity is measured in` : `${JSON.stringify(outside)} ${outside.length === 1 ? "lies" : "lie"} outside family "${family}"`;
|
|
126
|
+
console.warn(
|
|
127
|
+
`[ui-kit] QuantityInput: \`units\` ${problem}. \`units\` wins over \`family\`: give units of the family, or leave \`family\` out. (Development builds only; once per field.)`
|
|
128
|
+
);
|
|
129
|
+
}, [unitsKey, family]);
|
|
130
|
+
const generatedId = useId();
|
|
131
|
+
const fieldId = id ?? generatedId;
|
|
132
|
+
const name = fieldName ?? plainText(label) ?? ariaLabel;
|
|
133
|
+
const isInvalid = Boolean(invalid) || ariaInvalid === true || ariaInvalid === "true";
|
|
134
|
+
const labelled = label !== void 0;
|
|
135
|
+
return (
|
|
136
|
+
// `items-end`: at Large the amount's label stands above its box (§10.17), and the
|
|
137
|
+
// select lines up with the box, not with the label.
|
|
138
|
+
/* @__PURE__ */ jsxs("div", { ...dataAttributes(props), className: cn("flex items-end", className), children: [
|
|
139
|
+
/* @__PURE__ */ jsx(
|
|
140
|
+
NumberInput,
|
|
141
|
+
{
|
|
142
|
+
ref,
|
|
143
|
+
id: fieldId,
|
|
144
|
+
label,
|
|
145
|
+
...labelHint !== void 0 ? { hint: labelHint } : {},
|
|
146
|
+
"aria-label": ariaLabel,
|
|
147
|
+
"aria-describedby": ariaDescribedBy,
|
|
148
|
+
"aria-required": ariaRequired,
|
|
149
|
+
value: draft,
|
|
150
|
+
onChange: (text) => setDraft(localizeMark(text, mark)),
|
|
151
|
+
onCommit: commit,
|
|
152
|
+
locale,
|
|
153
|
+
placeholder,
|
|
154
|
+
disabled,
|
|
155
|
+
invalid: isInvalid,
|
|
156
|
+
calculator: false,
|
|
157
|
+
className: "min-w-0 flex-1 focus-within:z-10",
|
|
158
|
+
inputClassName: cn("rounded-e-none", inputClassName)
|
|
159
|
+
}
|
|
160
|
+
),
|
|
161
|
+
/* @__PURE__ */ jsx(
|
|
162
|
+
Select,
|
|
163
|
+
{
|
|
164
|
+
id: `${fieldId}-unit`,
|
|
165
|
+
"aria-label": labels.unit(name),
|
|
166
|
+
"aria-describedby": ariaDescribedBy,
|
|
167
|
+
value: unit,
|
|
168
|
+
onChange: (e) => pickUnit(e.target.value),
|
|
169
|
+
disabled,
|
|
170
|
+
invalid: isInvalid,
|
|
171
|
+
className: "-ms-px shrink-0 focus-within:z-10",
|
|
172
|
+
selectClassName: cn("w-auto rounded-s-none", labelled && FIELD_FLOATING_PAD, selectClassName),
|
|
173
|
+
children: offered.map((u) => /* @__PURE__ */ jsx("option", { value: u, children: u === "pcs" ? labels.pcs : u }, u))
|
|
174
|
+
}
|
|
175
|
+
)
|
|
176
|
+
] })
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
function QuantityInput(props) {
|
|
180
|
+
return /* @__PURE__ */ jsx(QuantityRow, { ...props });
|
|
181
|
+
}
|
|
182
|
+
export {
|
|
183
|
+
DEFAULT_QUANTITY_LABELS,
|
|
184
|
+
QuantityInput,
|
|
185
|
+
QuantityRow,
|
|
186
|
+
firstOfferedUnit
|
|
187
|
+
};
|
|
188
|
+
//# sourceMappingURL=quantity-input.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/components/quantity-input.tsx"],"sourcesContent":["import { useEffect, useId, useRef, useState } from \"react\";\nimport type { ReactNode, Ref } from \"react\";\nimport { NumberInput } from \"./number-input\";\nimport { Select } from \"./select\";\nimport { FIELD_FLOATING_PAD } from \"./field-base\";\nimport { DEFAULT_QUANTITY_LABELS } from \"./quantity-labels\";\nimport { useKitLabels, useKitLocale, type LabelOverride } from \"../i18n/kit-labels\";\nimport { cn } from \"../lib/cn\";\nimport { formatResult } from \"../lib/calc\";\nimport { dataAttributes, type DataAttributes } from \"../lib/data-attributes\";\nimport { decimalMark, localizeMark } from \"../lib/decimal-marks\";\nimport { plainText } from \"../lib/plain-text\";\nimport {\n QUANTITY_DIGITS,\n QUANTITY_UNITS,\n convertQuantity,\n quantityFamily,\n quantityMaxDigits,\n type Quantity,\n type QuantityFamily,\n type QuantityUnit,\n} from \"../lib/quantity\";\nimport { roundHalfAway } from \"../lib/round\";\n\n/** The words of {@link QuantityInput} and {@link QuantityField} — the `quantity` namespace. */\nexport interface QuantityLabels {\n /** The pieces unit, in the select and after a count: \"pcs\", \"Stk.\", \"pce\", \"pz.\". The\n * one unit with a word — g, kg, ml, cl, dl and l are SI symbols in every language. */\n pcs: string;\n /** The unit select's accessible name — with the field's name when it has one (\"Unit\n * for “Flour”\"), else alone. */\n unit: (field?: string) => string;\n /** The suggestion line's lead when the app names no source ({@link QuantityField}'s\n * `suggestionFrom`). */\n suggested: string;\n /** The suggestion button's accessible name: take this quantity (\"Use 500 g\"). It holds\n * the quantity the button shows, the words a voice user says (WCAG 2.5.3). */\n takeSuggestion: (quantity: string) => string;\n}\n\n// Defined in ./quantity-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_QUANTITY_LABELS };\n\n/** {@link QuantityInput}'s props. */\nexport interface QuantityInputProps extends DataAttributes {\n /** The quantity: an amount (`null` for none yet) and its unit. */\n value: Quantity;\n /**\n * The next quantity — when a typed amount commits (blur or Enter, as NumberField\n * commits; never per keystroke) and when another unit is picked. The amount is\n * already rounded to the unit's `digits` and clamped to `max`.\n */\n onChange: (next: Quantity) => void;\n /** Offer only this family's units — `\"mass\"` (g, kg), `\"volume\"` (ml, cl, dl, l) or\n * `\"count\"` (pcs). Without it (and without `units`), every unit. */\n family?: QuantityFamily;\n /**\n * The units to offer, explicitly — WINS over `family`, and is listed small to large\n * whatever its order (`[\"ml\", \"dl\", \"l\"]` is a volume list without centilitres). Each\n * should lie inside `family` when both are given; one that does not is still offered,\n * and a development build says so on the console. A value whose unit is not in the\n * list (a 75 cl that `parseQuantity` read off a label) is still shown: the select lists\n * its unit for as long as it is the value's.\n */\n units?: readonly QuantityUnit[];\n /** An emptied amount commits `null`. Without it an empty amount is no answer and snaps\n * back to the last one, as NumberField's does. */\n nullable?: boolean;\n /** Refuse 0: it snaps back like text that is no number (default `true` — a shopping\n * list has no \"0 g\"). A negative amount is refused either way. */\n positive?: boolean;\n /** The largest amount, in whichever unit is chosen — NumberField's `max`: a typed amount\n * above it commits as `max`. A limit in a physical unit (\"at most 25 kg\") is the form's\n * validation, which sees the unit too. */\n max?: number;\n /**\n * Decimals per unit, over {@link QUANTITY_DIGITS} (g 0, kg 3, ml 0, cl 1, dl 2, l 3,\n * pcs 0): `{ kg: 2 }`, `{ pcs: 1 }`. A typed amount is rounded to them on commit, and\n * shown with at most that many. Cut to the unit's milli grid (g and ml none, cl 1, dl 2,\n * kg, l and pcs 3), so every amount still sums exactly.\n */\n digits?: Partial<Record<QuantityUnit, number>>;\n /** BCP 47 tag for the amount's decimal mark (\"1,5\" in de-DE, \"1.5\" in de-CH); default\n * the `<UiKitProvider locale>`, else the runtime's — NumberField's `locale`. */\n locale?: string;\n /**\n * Another unit converts the amount: `true` turns 500 g into 0.5 kg. `false` (default)\n * keeps the amount as typed — 500 g becomes 500 kg — because the usual reason to change\n * the unit is that the unit was wrong, not the weight. Either way the amount lands on\n * the new unit's grid (1.5 kg kept as typed in grams is 2 g); a conversion within a\n * family is exact, and across families (`units` mixing them) the amount is kept.\n */\n convertOnUnitChange?: boolean;\n /** Both controls, the amount and the unit. */\n disabled?: boolean;\n /** Paints both controls as wrong — {@link Input}'s `invalid`. */\n invalid?: boolean;\n /** The amount `<input>`'s id — what a label's `htmlFor` points at. */\n id?: string;\n /** Names the amount when there is no label (a table cell). The unit select is named\n * after it: \"Unit for “<aria-label>”\". */\n \"aria-label\"?: string;\n /** Set on the amount input (and the unit select, which the same caption and error\n * describe) — a form library's control slot spreads these. */\n \"aria-describedby\"?: string;\n \"aria-invalid\"?: boolean | \"true\" | \"false\";\n \"aria-required\"?: boolean | \"true\" | \"false\";\n placeholder?: string;\n /** Prop > `<UiKitProvider labels={{ quantity }}>` > English. */\n labels?: LabelOverride<QuantityLabels>;\n /** The root: the row holding the amount and the unit. */\n className?: string;\n /** The amount `<input>`. */\n inputClassName?: string;\n /** The unit `<select>`. */\n selectClassName?: string;\n /** The amount `<input>` — focus on error, `select()`. */\n ref?: Ref<HTMLInputElement>;\n}\n\n/** @internal What {@link QuantityField} hands the row on top of the bare control's props. */\nexport interface QuantityRowProps extends QuantityInputProps {\n /** The floating label in the amount's box. */\n label?: ReactNode;\n /** What rides the label's line — a resolved `labelHint` (a FieldHint, a help \"?\"). */\n labelHint?: ReactNode;\n /** The field's name for the unit select's (\"Unit for “Flour”\") where the label is\n * drawn outside the row — RhfQuantityField's, above it. Default the label's text, else\n * `aria-label`. */\n fieldName?: string;\n}\n\n/** The kit's dev switch, as `lib/logger.ts` reads it: a development build, never a\n * production one, and never under vitest. Read per call so a test can switch it on with\n * `vi.stubEnv`. Optional-chained: `import.meta.env` is a Vite injection. */\nfunction devWarnings(): boolean {\n return Boolean(import.meta.env?.DEV) && !import.meta.env?.VITEST;\n}\n\n/** A unit the kit knows — a `units` entry may be a typo, or a string off an API. */\nfunction isUnit(unit: unknown): unit is QuantityUnit {\n return typeof unit === \"string\" && (QUANTITY_UNITS as readonly string[]).includes(unit);\n}\n\n/** The decimals `unit` is entered with: the caller's, else the default, cut to the grid. */\nfunction digitsFor(unit: QuantityUnit, digits: QuantityInputProps[\"digits\"]): number {\n const wanted = digits?.[unit] ?? QUANTITY_DIGITS[unit] ?? 3;\n const whole = Number.isFinite(wanted) ? Math.trunc(wanted) : QUANTITY_DIGITS[unit];\n return Math.min(Math.max(whole, 0), isUnit(unit) ? quantityMaxDigits(unit) : 3);\n}\n\n/**\n * `amount` on `unit`'s milli grid, for a unit switch: half away from zero, except that a\n * positive amount never rounds to 0 under `positive` — 0.4 kg kept as typed in grams is\n * 1 g, the smallest amount the field would accept, not a 0 it would refuse.\n */\nfunction onGrid(amount: number, unit: QuantityUnit, positive: boolean): number {\n const places = isUnit(unit) ? quantityMaxDigits(unit) : 3;\n const rounded = roundHalfAway(amount, places);\n return positive && rounded <= 0 && amount > 0 ? 10 ** -places : rounded;\n}\n\n/** The units the select offers: `units` (known ones, deduplicated), else `family`'s,\n * else all — plus the value's own unit while it is outside them. Small to large. */\nfunction offeredUnits(\n units: readonly QuantityUnit[] | undefined,\n family: QuantityFamily | undefined,\n current: QuantityUnit,\n): QuantityUnit[] {\n const known = units?.filter(isUnit) ?? [];\n const allowed = known.length > 0 ? known : QUANTITY_UNITS.filter((u) => !family || quantityFamily(u) === family);\n return QUANTITY_UNITS.filter((u) => allowed.includes(u) || u === current);\n}\n\n/** @internal The unit a value with none starts in — the first one the select would offer\n * (RhfQuantityField's, for a form whose default is `null`). */\nexport function firstOfferedUnit(units: readonly QuantityUnit[] | undefined, family: QuantityFamily | undefined) {\n const known = units?.filter(isUnit) ?? [];\n return QUANTITY_UNITS.find((u) => (known.length > 0 ? known.includes(u) : !family || quantityFamily(u) === family))!;\n}\n\n/** @internal The amount and unit row, with the floating label {@link QuantityField} gives\n * it. Not on the barrel: QuantityInput is this without a label. */\nexport function QuantityRow(props: QuantityRowProps) {\n const {\n ref,\n value,\n onChange,\n family,\n units,\n nullable = false,\n positive = true,\n max,\n digits,\n locale: localeProp,\n convertOnUnitChange = false,\n disabled,\n invalid,\n id,\n \"aria-label\": ariaLabel,\n \"aria-describedby\": ariaDescribedBy,\n \"aria-invalid\": ariaInvalid,\n \"aria-required\": ariaRequired,\n placeholder,\n labels: labelsProp,\n className,\n inputClassName,\n selectClassName,\n label,\n labelHint,\n fieldName,\n } = props;\n const labels = useKitLabels(\"quantity\", DEFAULT_QUANTITY_LABELS, labelsProp);\n const locale = useKitLocale(localeProp);\n const mark = decimalMark(locale);\n // A number that is not finite is no amount, as NumberField reads its value (INP-11).\n const amount = value.amount !== null && Number.isFinite(value.amount) ? value.amount : null;\n const unit = value.unit;\n const places = digitsFor(unit, digits);\n // `formatResult` for fixed notation (\"1e-7\" would read back as 17), in the locale's mark.\n const format = (n: number | null, decimals: number) =>\n n === null ? \"\" : localizeMark(formatResult(roundHalfAway(n, decimals)), mark);\n\n // The draft the amount shows while it is typed — NumberField's loop: kept as typed,\n // resolved on commit, re-rendered from the value when that changes from outside (a\n // reset, a unit switch, a suggestion taken) or when the mark or the digits do.\n const [draft, setDraft] = useState(() => format(amount, places));\n const [seen, setSeen] = useState({ amount, unit, mark, places });\n if (!Object.is(seen.amount, amount) || seen.unit !== unit || seen.mark !== mark || seen.places !== places) {\n setSeen({ amount, unit, mark, places });\n setDraft(format(amount, places));\n }\n\n const commit = (text: string) => {\n const trimmed = text.trim();\n // Unparsable, refused or emptied without `nullable`: back to the value, as NumberField\n // snaps back — never a NaN, a 0 or a negative sent on for the server to reject.\n const snapBack = () => setDraft(format(amount, places));\n let next: number | null;\n if (trimmed === \"\") {\n if (!nullable) return snapBack();\n next = null;\n } else {\n // NumberInput's committed text: an evaluated calculation, dot-decimal.\n const parsed = Number(trimmed.replace(/,/g, \".\"));\n if (!Number.isFinite(parsed)) return snapBack();\n next = roundHalfAway(parsed, places);\n if (max !== undefined && next > max) next = max;\n if (next < 0 || (positive && next === 0)) return snapBack();\n }\n setDraft(format(next, places));\n if (!Object.is(next, amount)) onChange({ amount: next, unit });\n };\n\n const pickUnit = (picked: string) => {\n if (!isUnit(picked) || picked === unit) return;\n let next = amount;\n if (next !== null) {\n if (convertOnUnitChange) next = convertQuantity({ amount: next, unit }, picked)?.amount ?? next;\n next = onGrid(next, picked, positive);\n }\n onChange({ amount: next, unit: picked });\n };\n\n const offered = offeredUnits(units, family, unit);\n // The units warning, once per field, development only. Keyed on the list's text: a\n // caller's inline `units={[\"ml\", \"dl\", \"l\"]}` is a new array on every render.\n const warned = useRef(false);\n const unitsKey = units === undefined ? undefined : JSON.stringify(units);\n useEffect(() => {\n if (warned.current || !devWarnings() || unitsKey === undefined) return;\n const list = JSON.parse(unitsKey) as unknown[];\n const unknown = list.filter((u) => !isUnit(u));\n const outside = family ? list.filter((u) => isUnit(u) && quantityFamily(u) !== family) : [];\n if (unknown.length === 0 && outside.length === 0 && list.length > 0) return;\n warned.current = true;\n const problem =\n list.length === 0\n ? \"is empty, so the field offers its family's units\"\n : unknown.length > 0\n ? `names ${JSON.stringify(unknown)}, which no quantity is measured in`\n : `${JSON.stringify(outside)} ${outside.length === 1 ? \"lies\" : \"lie\"} outside family \"${family}\"`;\n console.warn(\n `[ui-kit] QuantityInput: \\`units\\` ${problem}. \\`units\\` wins over \\`family\\`: give units of the family, ` +\n `or leave \\`family\\` out. (Development builds only; once per field.)`,\n );\n }, [unitsKey, family]);\n\n const generatedId = useId();\n const fieldId = id ?? generatedId;\n const name = fieldName ?? plainText(label) ?? ariaLabel;\n const isInvalid = Boolean(invalid) || ariaInvalid === true || ariaInvalid === \"true\";\n const labelled = label !== undefined;\n\n return (\n // `items-end`: at Large the amount's label stands above its box (§10.17), and the\n // select lines up with the box, not with the label.\n <div {...dataAttributes(props)} className={cn(\"flex items-end\", className)}>\n <NumberInput\n ref={ref}\n id={fieldId}\n label={label}\n // Only when there is one: NumberInput keeps a wrapper box for a passed `hint` key,\n // and the box would take the flex item's place in this row.\n {...(labelHint !== undefined ? { hint: labelHint } : {})}\n aria-label={ariaLabel}\n aria-describedby={ariaDescribedBy}\n aria-required={ariaRequired}\n value={draft}\n onChange={(text) => setDraft(localizeMark(text, mark))}\n onCommit={commit}\n locale={locale}\n placeholder={placeholder}\n disabled={disabled}\n invalid={isInvalid}\n // A count or a weight is typed, not worked out; the trigger beside a unit select\n // in a table cell is noise. A typed \"2*500\" still evaluates on commit.\n calculator={false}\n // The focused half paints its whole frame over the shared border.\n className=\"min-w-0 flex-1 focus-within:z-10\"\n inputClassName={cn(\"rounded-e-none\", inputClassName)}\n />\n <Select\n id={`${fieldId}-unit`}\n aria-label={labels.unit(name)}\n aria-describedby={ariaDescribedBy}\n value={unit}\n onChange={(e) => pickUnit(e.target.value)}\n disabled={disabled}\n invalid={isInvalid}\n // One border between the two halves, not two side by side.\n className=\"-ms-px shrink-0 focus-within:z-10\"\n // As tall as the floating field beside it, its value on the same line.\n selectClassName={cn(\"w-auto rounded-s-none\", labelled && FIELD_FLOATING_PAD, selectClassName)}\n >\n {offered.map((u) => (\n <option key={u} value={u}>\n {u === \"pcs\" ? labels.pcs : u}\n </option>\n ))}\n </Select>\n </div>\n );\n}\n\n/**\n * A quantity — an amount and its unit — as one control: the amount, typed in the\n * locale's decimal mark, beside a select of units. The bare control, for a table row or\n * a line of a list; {@link QuantityField} is the labelled field with a caption, an error\n * and a suggestion to take.\n *\n * ```tsx\n * <QuantityInput aria-label=\"Flour\" family=\"mass\" value={qty} onChange={setQty} />\n * ```\n *\n * THE AMOUNT is NumberField's loop over {@link NumberInput}: a draft while it is typed\n * (grouping read off, the locale's mark kept), resolved on blur or Enter — rounded to the\n * unit's `digits`, clamped to `max` — and `onChange` only then. Empty, or text that is no\n * number, snaps back (empty commits `null` with `nullable`); so does 0 under `positive`\n * (the default), and a negative amount always.\n *\n * THE UNIT is a native select of {@link QUANTITY_UNITS} — `family`'s, or `units`' — with\n * pieces in the reader's word (`quantity.pcs`). Picking one is a commit of its own;\n * `convertOnUnitChange` decides whether the amount is converted or kept as typed.\n *\n * The value's arithmetic, formatting and parsing are `lib/quantity`'s — {@link toBaseQuantity},\n * {@link convertQuantity}, {@link sumQuantities}, {@link formatQuantity}, {@link parseQuantity} —\n * for the list's totals and a comparison table outside the field.\n */\nexport function QuantityInput(props: QuantityInputProps) {\n return <QuantityRow {...props} />;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
const DEFAULT_QUANTITY_LABELS = {
|
|
3
|
+
pcs: "pcs",
|
|
4
|
+
unit: (field) => field ? `Unit for \u201C${field}\u201D` : "Unit",
|
|
5
|
+
suggested: "Suggested",
|
|
6
|
+
takeSuggestion: (quantity) => `Use ${quantity}`
|
|
7
|
+
};
|
|
8
|
+
export {
|
|
9
|
+
DEFAULT_QUANTITY_LABELS
|
|
10
|
+
};
|
|
11
|
+
//# sourceMappingURL=quantity-labels.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/components/quantity-labels.ts"],"sourcesContent":["// The English words of ./quantity-input's QuantityInput and QuantityField, in a module with\n// no component code: the English catalogue (src/i18n/defaults.ts) imports them without the\n// component, so `en` loads as one chunk like the other languages (0.34, KS U8).\n// ./quantity-input re-exports them.\n\nimport type { QuantityLabels } from \"./quantity-input\";\n\nexport const DEFAULT_QUANTITY_LABELS: QuantityLabels = {\n pcs: \"pcs\",\n unit: (field) => (field ? `Unit for “${field}”` : \"Unit\"),\n suggested: \"Suggested\",\n takeSuggestion: (quantity) => `Use ${quantity}`,\n};\n"],"mappings":";AAOO,MAAM,0BAA0C;AAAA,EACrD,KAAK;AAAA,EACL,MAAM,CAAC,UAAW,QAAQ,kBAAa,KAAK,WAAM;AAAA,EAClD,WAAW;AAAA,EACX,gBAAgB,CAAC,aAAa,OAAO,QAAQ;AAC/C;","names":[]}
|
package/dist/eslint/index.js
CHANGED
|
@@ -47,7 +47,7 @@ const roleUtilities = {
|
|
|
47
47
|
};
|
|
48
48
|
}
|
|
49
49
|
};
|
|
50
|
-
const VERSION = true ? "0.
|
|
50
|
+
const VERSION = true ? "0.57.0" : "0.0.0-source";
|
|
51
51
|
const recommended = {
|
|
52
52
|
name: "@eifi1/ui-kit/recommended",
|
|
53
53
|
// Not the tests, as check:tokens skips the kit's: a test names the old class to assert it
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { DataTableProps } from "../components/data-table.js";
|
|
2
|
+
import type { CommitScope } from "../components/write-lock.js";
|
|
3
|
+
import type { LabelOverride } from "../i18n/kit-labels.js";
|
|
4
|
+
import type { FeedbackRecord } from "./feedback-record.js";
|
|
5
|
+
import type { FeedbackDetailUpdateAsync } from "./feedback-row-detail.js";
|
|
6
|
+
import type { FeedbackInboxBulk } from "./feedback-inbox-page.js";
|
|
7
|
+
import { type FeedbackPageLabels } from "./feedback-table.js";
|
|
8
|
+
import { type FeedbackCategory, type FeedbackStatus } from "./feedback-vocabulary.js";
|
|
9
|
+
/**
|
|
10
|
+
* The inbox's multi-select and bulk edit (0.57, keksdose dev #663 — Marcel: *"I want to
|
|
11
|
+
* have multiselect and bulk edit (back) for the feedback table"*; docs/feedback-harmonization.md
|
|
12
|
+
* §13.6). The inbox the kit assembles (0.55, §13) had no selection at all, so an admin
|
|
13
|
+
* changed a dozen stale reports one row at a time.
|
|
14
|
+
*
|
|
15
|
+
* **No bulk endpoint, on purpose.** keksdose has none, and none of the three needs one:
|
|
16
|
+
* each report's change has to pass the server's own rules for that report (§3.4), so the bar
|
|
17
|
+
* loops the page's ONE `update` mutation — `PATCH /feedback/{id}` per report — exactly as a
|
|
18
|
+
* tap on the status cell does. An app gets the bulk edit with no wiring of its own. It
|
|
19
|
+
* notifies nobody, its Undo neither: a status, a category or the visibility alone tells no
|
|
20
|
+
* one (decision 13, §9.11); only a reply or a comment does, and the bar writes neither.
|
|
21
|
+
*
|
|
22
|
+
* What it changes: the **status**, the **category** (never to CRASH — §3.4 answers 422;
|
|
23
|
+
* CRASH → BUG is an ordinary admin edit) and, with the inbox's `publicSwitch`, **Visible to
|
|
24
|
+
* others** (§9.3; a CRASH is private whatever its flag, and a row without `public` comes
|
|
25
|
+
* from a server without public parts — both are left alone, as the detail's switch is not
|
|
26
|
+
* drawn on them). A row already at the value is not sent.
|
|
27
|
+
*
|
|
28
|
+
* **One Undo for the batch**: the toast the single change has (§4.5, 8 s), naming how many
|
|
29
|
+
* changed; Undo puts EACH changed report back to what IT was — the selection may mix
|
|
30
|
+
* statuses — and only the reports that changed. A report whose PATCH failed (the
|
|
31
|
+
* mutation's `onError` toasts the server's sentence, as for a single change) is not in the
|
|
32
|
+
* Undo, and STAYS SELECTED, so the next press retries exactly those; the bar's count says
|
|
33
|
+
* how many.
|
|
34
|
+
*/
|
|
35
|
+
/** Which bulk change: the {@link FeedbackDetailPatch} key it sends. */
|
|
36
|
+
type BulkField = NonNullable<FeedbackInboxBulk<FeedbackRecord>["pending"]>;
|
|
37
|
+
/** How many PATCHes a bulk change keeps in flight. Each one is a full update on the
|
|
38
|
+
* server, with its rules, so a trickle rather than a burst of 20, and still about a second
|
|
39
|
+
* for the 20 rather than the 20 round trips back to back a plain loop would take.
|
|
40
|
+
*
|
|
41
|
+
* @internal Not part of `/feedback` or the barrel. */
|
|
42
|
+
export declare const FEEDBACK_BULK_CONCURRENCY = 3;
|
|
43
|
+
/**
|
|
44
|
+
* The selected rows a bulk change would actually SEND — those not at the value already
|
|
45
|
+
* (and, for the flag, those that can carry it). The bar disables a choice that would send
|
|
46
|
+
* nothing; the change sends exactly these.
|
|
47
|
+
*
|
|
48
|
+
* @internal Not part of `/feedback` or the barrel.
|
|
49
|
+
*/
|
|
50
|
+
export declare function feedbackBulkTargets<T extends FeedbackRecord>(rows: readonly T[], field: BulkField, value: FeedbackStatus | FeedbackCategory | boolean): T[];
|
|
51
|
+
/**
|
|
52
|
+
* The selection and the bulk changes, for {@link useFeedbackInbox}: the table's `selection`
|
|
53
|
+
* and `inbox.bulk`, both `undefined` while `on` is false (not the admin inbox, or the app's
|
|
54
|
+
* `selectable={false}`).
|
|
55
|
+
*
|
|
56
|
+
* @internal Not part of `/feedback` or the barrel.
|
|
57
|
+
*/
|
|
58
|
+
export declare function useFeedbackInboxBulk<T extends FeedbackRecord>({ on, rows: shown, update, publicSwitch, labels, }: {
|
|
59
|
+
on: boolean;
|
|
60
|
+
/** The rows the inbox hands its table (newest first, behind the awaiting toggle). */
|
|
61
|
+
rows: readonly T[];
|
|
62
|
+
update: FeedbackDetailUpdateAsync;
|
|
63
|
+
publicSwitch: boolean;
|
|
64
|
+
labels?: LabelOverride<FeedbackPageLabels>;
|
|
65
|
+
}): {
|
|
66
|
+
bulk?: FeedbackInboxBulk<T>;
|
|
67
|
+
selection?: DataTableProps<T>["selection"];
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* The inbox's bulk bar: the count, Clear, and Set status / Set category / Visibility, each
|
|
71
|
+
* a list of the values — the statuses in chain order with their glyphs, the categories
|
|
72
|
+
* less CRASH, on and off. Rendered unconditionally, as `BulkActionBar` asks.
|
|
73
|
+
*
|
|
74
|
+
* On a desktop the strip is OPEN from the start, at "0 selected" with its lists waiting:
|
|
75
|
+
* it sits in the flow above the table, so showing it with the first ticked row pushed every
|
|
76
|
+
* row down by its height — away from the pointer reaching for the next box — and clearing
|
|
77
|
+
* pulled them back up (the 0.57 screenshot check). On a phone the card floats over the page
|
|
78
|
+
* and moves nothing, so it still comes with the first selected card.
|
|
79
|
+
*
|
|
80
|
+
* @internal Drawn by {@link FeedbackInbox}; not part of `/feedback` or the barrel.
|
|
81
|
+
*/
|
|
82
|
+
export declare function FeedbackInboxBulkBar<T extends FeedbackRecord>({ bulk, commit, labels, }: {
|
|
83
|
+
bulk: FeedbackInboxBulk<T>;
|
|
84
|
+
/** The table's status writes' lock (`commit`): a bulk change is the same write. */
|
|
85
|
+
commit?: CommitScope;
|
|
86
|
+
labels?: LabelOverride<FeedbackPageLabels>;
|
|
87
|
+
}): import("react").JSX.Element;
|
|
88
|
+
export {};
|