@eifi1/ui-kit 0.23.0 → 0.24.1
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 +6 -6
- package/dist/chart.d.ts +2 -4
- package/dist/components/amount-input.d.ts +16 -9
- package/dist/components/amount-input.js +13 -11
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/autocomplete.d.ts +5 -0
- package/dist/components/autocomplete.js +39 -37
- package/dist/components/autocomplete.js.map +1 -1
- package/dist/components/button-group.d.ts +2 -4
- package/dist/components/calculator.d.ts +2 -4
- package/dist/components/column-mapper.d.ts +62 -9
- package/dist/components/column-mapper.js +85 -49
- package/dist/components/column-mapper.js.map +1 -1
- package/dist/components/combobox.d.ts +33 -1
- package/dist/components/combobox.js +8 -2
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/confirm-dialog.d.ts +2 -4
- package/dist/components/copy-button.d.ts +2 -4
- package/dist/components/danger-confirm.d.ts +2 -4
- package/dist/components/danger-confirm.js +52 -5
- package/dist/components/danger-confirm.js.map +1 -1
- package/dist/components/entity-combobox.d.ts +13 -1
- package/dist/components/entity-combobox.js +13 -4
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/facing-pair.d.ts +2 -4
- package/dist/components/file-button.d.ts +2 -4
- package/dist/components/file-dropzone.d.ts +2 -4
- package/dist/components/form-actions.d.ts +1 -3
- package/dist/components/form-actions.js +15 -1
- package/dist/components/form-actions.js.map +1 -1
- package/dist/components/iban-input.d.ts +2 -4
- package/dist/components/language-select.d.ts +2 -4
- package/dist/components/money-field.d.ts +2 -4
- package/dist/components/multi-entity-combobox.d.ts +5 -1
- package/dist/components/multi-entity-combobox.js +11 -3
- package/dist/components/multi-entity-combobox.js.map +1 -1
- package/dist/components/number-field.d.ts +2 -4
- package/dist/components/number-input.d.ts +2 -4
- package/dist/components/numpad-sheet.d.ts +2 -4
- package/dist/components/phone-input.d.ts +2 -4
- package/dist/components/series-chart.d.ts +2 -4
- package/dist/components/settings-fields.d.ts +2 -4
- package/dist/components/share-card.d.ts +1 -3
- package/dist/components/text-link.d.ts +1 -3
- package/dist/components/time-input.d.ts +2 -4
- package/dist/components/ui.d.ts +2 -4
- package/dist/components/ui.js +55 -17
- package/dist/components/ui.js.map +1 -1
- package/dist/feedback/feedback-attachment.d.ts +66 -1
- package/dist/feedback/feedback-attachment.js +32 -12
- package/dist/feedback/feedback-attachment.js.map +1 -1
- package/dist/feedback/feedback-dialog.d.ts +66 -1
- package/dist/feedback/feedback-inbox.d.ts +67 -253
- package/dist/feedback/feedback-thread.d.ts +1 -3
- package/dist/feedback/feedback-thread.js +8 -2
- package/dist/feedback/feedback-thread.js.map +1 -1
- package/dist/{feedback-BxeQVzwq.d.ts → feedback-DOwPu-Il.d.ts} +878 -18
- package/dist/feedback.d.ts +2 -4
- package/dist/hooks/use-file-drop.d.ts +2 -4
- package/dist/i18n/defaults.d.ts +2 -4
- package/dist/i18n/german.d.ts +2 -4
- package/dist/i18n/kit-labels.d.ts +1 -3
- package/dist/i18n/languages.d.ts +2 -4
- package/dist/i18n/locales/de-CH.d.ts +2 -4
- package/dist/i18n/locales/en.d.ts +2 -4
- package/dist/i18n/locales/es.d.ts +2 -4
- package/dist/i18n/locales/fr.d.ts +2 -4
- package/dist/i18n/locales/hu.d.ts +2 -4
- package/dist/i18n/locales/it.d.ts +2 -4
- package/dist/i18n/locales/zh.d.ts +2 -4
- package/dist/i18n/review.d.ts +2 -4
- package/dist/index.d.ts +2 -4
- package/dist/lib/strip-fade.d.ts +2 -2
- package/dist/lib/strip-fade.js.map +1 -1
- package/dist/rhf/fields.d.ts +59 -8
- package/dist/rhf/fields.js +61 -3
- package/dist/rhf/fields.js.map +1 -1
- package/dist/rhf/form.d.ts +2 -4
- package/dist/rhf.d.ts +3 -5
- package/dist/rhf.js.map +1 -1
- package/dist/shell/app-shell.d.ts +1 -3
- package/dist/shell/top-bar-brand.d.ts +2 -4
- package/dist/shell.d.ts +1 -3
- package/dist/wizard/stepper-nav.d.ts +2 -4
- package/dist/wizard.d.ts +2 -4
- package/package.json +2 -2
- package/src/components/amount-input.tsx +37 -20
- package/src/components/autocomplete.tsx +16 -5
- package/src/components/column-mapper.tsx +181 -79
- package/src/components/combobox.tsx +41 -3
- package/src/components/danger-confirm.tsx +193 -22
- package/src/components/entity-combobox.tsx +28 -5
- package/src/components/form-actions.tsx +20 -1
- package/src/components/multi-entity-combobox.tsx +16 -3
- package/src/components/ui.tsx +107 -0
- package/src/feedback/feedback-attachment.tsx +65 -12
- package/src/feedback/feedback-thread.tsx +66 -6
- package/src/lib/strip-fade.ts +2 -2
- package/src/rhf/fields.tsx +167 -6
- package/src/rhf.ts +1 -1
- package/dist/feedback-attachment-fGAzZPf0.d.ts +0 -469
package/dist/wizard.d.ts
CHANGED
|
@@ -6,10 +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 './feedback-
|
|
10
|
-
import './feedback-attachment-fGAzZPf0.js';
|
|
11
|
-
import './feedback/feedback-inbox.js';
|
|
12
|
-
import 'lucide-react';
|
|
9
|
+
import './feedback-DOwPu-Il.js';
|
|
13
10
|
import './components/tooltip.js';
|
|
14
11
|
import './hooks/use-anchored-rect.js';
|
|
15
12
|
import './lib/clipping.js';
|
|
@@ -20,6 +17,7 @@ import './components/mini-calendar.js';
|
|
|
20
17
|
import './components/calendar-heatmap.js';
|
|
21
18
|
import './components/popover.js';
|
|
22
19
|
import './components/chip.js';
|
|
20
|
+
import 'lucide-react';
|
|
23
21
|
import './components/field-sync.js';
|
|
24
22
|
import './tour/tour.js';
|
|
25
23
|
import './search/command-palette.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eifi1/ui-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.1",
|
|
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.",
|
|
@@ -117,7 +117,7 @@
|
|
|
117
117
|
"_dependencies": "Only what the package imports AND owns the version of. `lucide-react` is a PEER: every consumer already declares `lucide-react: ^0.511.0` itself, so a hard dependency here bought each of them a second copy of the icon set in node_modules — and pinned to 0.511.x it held all three behind a release line that is now at 1.x. The range is deliberately wide (`>=0.400.0 <2`): the kit imports named icon components and nothing else, which has been stable across that whole span, so the app picks the version. `flag-icons` was never imported at all — 5.6 MB of stylesheet and SVG installed into every consumer to support class NAMES (`fi fi-xx`) that CurrencyFlag, CurrencySelect and LanguageMenu render as strings. The host supplies that stylesheet; see the dependency section of README.md. It stays a devDependency because the showcase imports it.",
|
|
118
118
|
"dependencies": {
|
|
119
119
|
"clsx": "^2.1.1",
|
|
120
|
-
"tailwind-merge": "^3.
|
|
120
|
+
"tailwind-merge": "^3.7.0",
|
|
121
121
|
"zustand": "^5.0.5"
|
|
122
122
|
},
|
|
123
123
|
"devDependencies": {
|
|
@@ -60,17 +60,26 @@ interface AmountInputProps {
|
|
|
60
60
|
className?: string;
|
|
61
61
|
/**
|
|
62
62
|
* Classes for the `<input>` itself — {@link NumberInput}'s and {@link Input}'s prop of
|
|
63
|
-
* the same name, merged the same way: after the field
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
63
|
+
* the same name, merged the same way: after everything the field sets for LOOKS — the
|
|
64
|
+
* base classes, the plain end padding, the display shape's centring and the `tone`
|
|
65
|
+
* colour — so it can restyle the figure (`text-end` for a money column whose decimal
|
|
66
|
+
* points line up, `font-medium`, a tighter `py-1` or a symmetric `px-2` in a table
|
|
67
|
+
* cell, `text-transparent` while a figure is being re-fetched), and before what the
|
|
68
|
+
* field needs to WORK: the end padding the calculator and the currency chip reserve,
|
|
69
|
+
* and the invalid border. Neither of those can be taken away by accident — a caller's
|
|
70
|
+
* `px-2` keeps its start edge and the digits still stop short of the chip, and a
|
|
71
|
+
* borderless cell's `border-transparent` still turns red when the value is wrong.
|
|
68
72
|
*
|
|
69
73
|
* keksdose G7: the invoice's VAT cell right-aligns its figure, and with only
|
|
70
74
|
* `className` (the wrapper) it reached the element through `[&_input]:text-end` — a
|
|
71
75
|
* descendant selector that depends on the field's inner markup, which the kit is free
|
|
72
76
|
* to change, and that a reader of the call site has to decode. {@link MoneyField}
|
|
73
77
|
* passes it through, and `RhfMoneyField` takes it too.
|
|
78
|
+
*
|
|
79
|
+
* keksdose 0.24: until then it sat BEFORE the plain end padding (`pe-3`) and the tone,
|
|
80
|
+
* so the budget's assigned cell could not make its padding symmetric (`px-2` came out
|
|
81
|
+
* `px-2 pe-3`) and the holdings price could not hide an outflow-toned figure while
|
|
82
|
+
* re-fetching — both were back on `[&_input]:` selectors on the wrapper.
|
|
74
83
|
*/
|
|
75
84
|
inputClassName?: string;
|
|
76
85
|
id?: string;
|
|
@@ -337,6 +346,17 @@ export const AmountInput = forwardRef<HTMLInputElement, AmountInputProps>(
|
|
|
337
346
|
// fighting itself through responsive overrides. `useMediaQuery` reads
|
|
338
347
|
// synchronously on mount, so there is no field→figure flash.
|
|
339
348
|
const asDisplay = variant === "display" && isMobile;
|
|
349
|
+
// Room for the trailing controls — the calculator and the currency chip — or
|
|
350
|
+
// nothing when neither trails. showCalc is always false on a phone, so the display
|
|
351
|
+
// shape only ever has to clear the currency chip — and it clears it by the chip's
|
|
352
|
+
// actual width (a text-sm code plus a chevron, ~58px) rather than the 5rem the
|
|
353
|
+
// boxed shape reserves. On a 375px screen those 16px are the difference between a
|
|
354
|
+
// readable figure and a clipped one (feedback #430 rework).
|
|
355
|
+
const trailingRoom = asDisplay
|
|
356
|
+
? currency ? (editable ? "pe-16" : "pe-12") : undefined
|
|
357
|
+
: showCalc
|
|
358
|
+
? currency ? (editable ? "pe-24" : "pe-16") : "pe-10"
|
|
359
|
+
: currency ? (editable ? "pe-20" : "pe-14") : undefined;
|
|
340
360
|
const innerRef = useRef<HTMLInputElement>(null);
|
|
341
361
|
const setRefs = useCallback(
|
|
342
362
|
(el: HTMLInputElement | null) => {
|
|
@@ -521,27 +541,24 @@ export const AmountInput = forwardRef<HTMLInputElement, AmountInputProps>(
|
|
|
521
541
|
// (feedback #314). FLOATING_INPUT_CLASS already bundles the peer +
|
|
522
542
|
// transparent-placeholder bits.
|
|
523
543
|
asDisplay ? DISPLAY_INPUT_CLASS : label !== undefined ? FLOATING_INPUT_CLASS : FIELD_BASE,
|
|
524
|
-
// The
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
// so the display shape only ever has to clear the currency chip — and
|
|
528
|
-
// it clears it by the chip's actual width (a text-sm code plus a
|
|
529
|
-
// chevron, ~58px) rather than the 5rem the boxed shape reserves. On a
|
|
530
|
-
// 375px screen those 16px are the difference between a readable figure
|
|
531
|
-
// and a clipped one (feedback #430 rework).
|
|
532
|
-
asDisplay
|
|
533
|
-
? currency ? (editable ? "pe-16" : "pe-12") : "pe-0"
|
|
534
|
-
: showCalc
|
|
535
|
-
? currency ? (editable ? "pe-24" : "pe-16") : "pe-10"
|
|
536
|
-
: currency ? (editable ? "pe-20" : "pe-14") : "pe-3",
|
|
544
|
+
// The plain end padding, with nothing trailing: cosmetic, so the caller's
|
|
545
|
+
// to change (keksdose 0.24 — a symmetric `px-2` in a budget cell).
|
|
546
|
+
!trailingRoom && (asDisplay ? "pe-0" : "pe-3"),
|
|
537
547
|
// Centred display shape: centre in what is LEFT of the currency chip.
|
|
538
548
|
// `text-center` alone would centre the figure in the whole box, i.e.
|
|
539
|
-
// partly underneath the chip; the padding
|
|
549
|
+
// partly underneath the chip; the padding below is what takes the chip
|
|
540
550
|
// out of the centring, and nothing is added at the start because a
|
|
541
551
|
// mirrored reservation would spend the width twice (see `align`).
|
|
542
552
|
asDisplay && align === "center" && "text-center",
|
|
543
|
-
//
|
|
553
|
+
// Over the base class's own text colour.
|
|
544
554
|
TONE_CLASS[tone],
|
|
555
|
+
// The caller's, over all of the above — base, plain padding, alignment and
|
|
556
|
+
// tone — and under only what the field needs to work (see `inputClassName`).
|
|
557
|
+
inputClassName,
|
|
558
|
+
// Room for the trailing controls, after the caller's so a `px-2` cannot run
|
|
559
|
+
// the digits under the chip (NumberInput's order, which reserves its unit
|
|
560
|
+
// and calculator the same way).
|
|
561
|
+
trailingRoom,
|
|
545
562
|
invalid && FIELD_INVALID,
|
|
546
563
|
)}
|
|
547
564
|
aria-invalid={invalid || undefined}
|
|
@@ -14,6 +14,7 @@ import { useAnchorDir } from "./use-anchor-dir";
|
|
|
14
14
|
import {
|
|
15
15
|
ComboboxFieldLabel,
|
|
16
16
|
DISABLED_ROW_CLASS,
|
|
17
|
+
endHintRowProps,
|
|
17
18
|
isOptionEnabled,
|
|
18
19
|
stepEnabled,
|
|
19
20
|
useActiveOptionScroll,
|
|
@@ -120,6 +121,11 @@ export interface AutocompleteProps<V extends string | number = string>
|
|
|
120
121
|
* `aria-describedby`, before any error; a {@link FieldHint} "?" rides the label line,
|
|
121
122
|
* or with no label sits at the field's end edge, outside the box. Not the list's
|
|
122
123
|
* `status` line, which talks about the lookup, not the field.
|
|
124
|
+
*
|
|
125
|
+
* Pass it — even as `undefined` — on a field whose hint comes and goes (0.24): the
|
|
126
|
+
* unlabelled field then keeps its end-hint row whether or not a "?" is in it, so the
|
|
127
|
+
* `<input>` is never re-parented, and focus and caret survive the toggle. The rule
|
|
128
|
+
* the rest of the family has had since 0.23 (and the fields since 0.22).
|
|
123
129
|
*/
|
|
124
130
|
hint?: ReactNode;
|
|
125
131
|
/**
|
|
@@ -159,7 +165,10 @@ export interface AutocompleteProps<V extends string | number = string>
|
|
|
159
165
|
}
|
|
160
166
|
|
|
161
167
|
function AutocompleteInner<V extends string | number = string>(
|
|
162
|
-
|
|
168
|
+
props: AutocompleteProps<V>,
|
|
169
|
+
ref: ForwardedRef<HTMLInputElement>,
|
|
170
|
+
) {
|
|
171
|
+
const {
|
|
163
172
|
value,
|
|
164
173
|
onChange,
|
|
165
174
|
options,
|
|
@@ -194,9 +203,7 @@ function AutocompleteInner<V extends string | number = string>(
|
|
|
194
203
|
"aria-labelledby": ariaLabelledBy,
|
|
195
204
|
"aria-describedby": ariaDescribedBy,
|
|
196
205
|
...rest
|
|
197
|
-
}
|
|
198
|
-
ref: ForwardedRef<HTMLInputElement>,
|
|
199
|
-
) {
|
|
206
|
+
} = props;
|
|
200
207
|
const generated = useId();
|
|
201
208
|
const fieldId = id ?? generated;
|
|
202
209
|
// Off the GENERATED id: a caller's `id` is theirs to collide with.
|
|
@@ -303,7 +310,11 @@ function AutocompleteInner<V extends string | number = string>(
|
|
|
303
310
|
{label}
|
|
304
311
|
</ComboboxFieldLabel>
|
|
305
312
|
)}
|
|
306
|
-
|
|
313
|
+
{/* `"hint" in props`: the family's 0.22 rule (see `endHintRowProps`) — passed at
|
|
314
|
+
all, even as `undefined`, a hint keeps the unlabelled field's end-hint row, so
|
|
315
|
+
a "?" coming and going never re-parents the <input> under the caret (0.24, the
|
|
316
|
+
last of the family to get it; the siblings had it in 0.23). */}
|
|
317
|
+
<EndHintRow {...endHintRowProps("hint" in props, hasLabel, hintParts.labelHint)}>
|
|
307
318
|
{withLock(
|
|
308
319
|
<div ref={fieldRef} className={cn("relative", locked && "cursor-not-allowed")}>
|
|
309
320
|
{icon && (
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { useEffect, useEffectEvent, useState } from "react";
|
|
2
|
-
import type { ChangeEvent, DragEvent, ReactNode } from "react";
|
|
1
|
+
import { useCallback, useEffect, useEffectEvent, useRef, useState } from "react";
|
|
2
|
+
import type { ChangeEvent, DragEvent, HTMLAttributes, ReactNode } from "react";
|
|
3
3
|
import { Upload } from "lucide-react";
|
|
4
4
|
|
|
5
5
|
import { useKitLabels, useKitLocale } from "../i18n/kit-labels";
|
|
@@ -17,6 +17,7 @@ import type {
|
|
|
17
17
|
ColumnMapping,
|
|
18
18
|
ColumnRole,
|
|
19
19
|
} from "../lib/column-mapping";
|
|
20
|
+
import { scrollIntoStrip, useStripFade } from "../lib/strip-fade";
|
|
20
21
|
import { parseTextTable } from "../lib/table-text";
|
|
21
22
|
import type { TableSeparator, TextTable } from "../lib/table-text";
|
|
22
23
|
import { AlertBanner } from "./alert-banner";
|
|
@@ -100,7 +101,8 @@ export interface ColumnMapperLabels {
|
|
|
100
101
|
roleOf: (column: string) => string;
|
|
101
102
|
/** The role select's "no role" option. */
|
|
102
103
|
ignore: string;
|
|
103
|
-
/** A required role in the select
|
|
104
|
+
/** A required role in the select — only while some role is NOT required: when
|
|
105
|
+
* every one is, the mark tells no role from another (0.24, Kurvenschmiede). */
|
|
104
106
|
requiredRole: (role: string) => string;
|
|
105
107
|
/** Under the preview, when it shows fewer rows than were read. */
|
|
106
108
|
previewOf: (shown: number, total: number) => string;
|
|
@@ -176,6 +178,29 @@ function formatList(
|
|
|
176
178
|
|
|
177
179
|
// ── ColumnRoleTable ───────────────────────────────────────────────────────────
|
|
178
180
|
|
|
181
|
+
/** A `data-*` attribute, typed so `{ "data-private": "" }` needs no cast. */
|
|
182
|
+
type DataAttributes = { [key: `data-${string}`]: string | number | boolean | undefined };
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* What {@link ColumnRoleTableProps.bodyProps} puts on the preview's `<tbody>`: `data-*`
|
|
186
|
+
* hooks, an `id`, a class, a style.
|
|
187
|
+
*/
|
|
188
|
+
export type ColumnRoleTableBodyProps = Omit<
|
|
189
|
+
HTMLAttributes<HTMLTableSectionElement>,
|
|
190
|
+
"children" | "dangerouslySetInnerHTML"
|
|
191
|
+
> &
|
|
192
|
+
DataAttributes;
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* What {@link ColumnRoleTableProps.rowProps} returns for one preview row's `<tr>` —
|
|
196
|
+
* the same shape as LineItems' `LineItemsRowProps`, on a table row.
|
|
197
|
+
*/
|
|
198
|
+
export type ColumnRoleTableRowProps = Omit<
|
|
199
|
+
HTMLAttributes<HTMLTableRowElement>,
|
|
200
|
+
"children" | "dangerouslySetInnerHTML"
|
|
201
|
+
> &
|
|
202
|
+
DataAttributes;
|
|
203
|
+
|
|
179
204
|
export interface ColumnRoleTableProps<
|
|
180
205
|
R extends string,
|
|
181
206
|
M extends ColumnMapping<R> = ColumnMapping<R>,
|
|
@@ -202,6 +227,26 @@ export interface ColumnRoleTableProps<
|
|
|
202
227
|
/** Every role select disabled — keksdose locks them while a re-sniff replaces the
|
|
203
228
|
* table they point into. */
|
|
204
229
|
disabled?: boolean;
|
|
230
|
+
/**
|
|
231
|
+
* Extra attributes for the preview's `<tbody>` (0.24) — keksdose: the sample rows are
|
|
232
|
+
* the user's own bank data (payees, memos, amounts), and its demo mode blurs exactly
|
|
233
|
+
* that through `.demo-mode [data-private]` (#336). With nowhere to put the attribute,
|
|
234
|
+
* its map step reached into the table with a `useLayoutEffect` that set it on the
|
|
235
|
+
* `<tbody>` after every commit; `bodyProps={{ "data-private": "" }}` declares it.
|
|
236
|
+
* There is deliberately no head counterpart: the column names and the role selects
|
|
237
|
+
* are what such a mode is there to show.
|
|
238
|
+
*
|
|
239
|
+
* Spread on the kit `TableBody`; a `className` is added to its own.
|
|
240
|
+
*/
|
|
241
|
+
bodyProps?: ColumnRoleTableBodyProps;
|
|
242
|
+
/**
|
|
243
|
+
* Extra attributes for one preview row's `<tr>`, from its cells and its index in the
|
|
244
|
+
* preview (0.24) — the contract of LineItems' `rowProps` (keksdose K20), on a table:
|
|
245
|
+
* `data-*` hooks (a per-row `data-private`, a test id), an `id`, a class. Called for
|
|
246
|
+
* the rows drawn, the first {@link previewRows}. The row has no attributes of its own
|
|
247
|
+
* to protect; a `className` is added after its classes.
|
|
248
|
+
*/
|
|
249
|
+
rowProps?: (row: readonly string[], index: number) => ColumnRoleTableRowProps | undefined;
|
|
205
250
|
labels?: Partial<ColumnMapperLabels>;
|
|
206
251
|
className?: string;
|
|
207
252
|
}
|
|
@@ -217,13 +262,25 @@ export interface ColumnRoleTableProps<
|
|
|
217
262
|
* not after the import. A column with no role says "Ignore" and its values are dimmed:
|
|
218
263
|
* the state that most needs to be visible in a twelve-column bank export.
|
|
219
264
|
*
|
|
220
|
-
* Picking a role another column holds MOVES it ({@link assignColumnRole}).
|
|
221
|
-
*
|
|
222
|
-
*
|
|
265
|
+
* Picking a role another column holds MOVES it ({@link assignColumnRole}). What the
|
|
266
|
+
* table still needs is named under it — a required group as "either Amount, Debit or
|
|
267
|
+
* Credit". Required roles say "(required)" in the list only where some role is not
|
|
268
|
+
* (0.24): Kurvenschmiede's roles are all required, so the mark told no option from
|
|
269
|
+
* another, and on a phone it cut the role's own name short ("Zeit (s) (erforderl…")
|
|
270
|
+
* — the part of the option the reader has to read. The "Still needed" line already
|
|
271
|
+
* says what is missing; a mark that distinguishes nothing is only noise.
|
|
223
272
|
*
|
|
224
273
|
* The table scrolls sideways inside its own box (the kit {@link Table}'s wrapper, a
|
|
225
274
|
* named, keyboard-reachable region while it overflows), so a wide export never widens
|
|
226
|
-
* the page at 390px.
|
|
275
|
+
* the page at 390px. An edge with columns behind it fades out (0.24, Kurvenschmiede:
|
|
276
|
+
* on a phone the fourth column's select sat off-screen and nothing said there was more
|
|
277
|
+
* to the right; phone scrollbars are overlays that show only while you drag). It is
|
|
278
|
+
* the kit Tabs strip's measured fade: drawn only while something is hidden on that
|
|
279
|
+
* side, so a table that fits is painted as before; both ends once scrolled into the
|
|
280
|
+
* middle; the reading direction's own in RTL; never animated, so there is nothing for
|
|
281
|
+
* reduced motion to turn off; and a `mask-image`, which changes no layout and needs no
|
|
282
|
+
* background colour to fade to. A role select focused behind the fade is scrolled
|
|
283
|
+
* clear of it.
|
|
227
284
|
*/
|
|
228
285
|
export function ColumnRoleTable<
|
|
229
286
|
R extends string,
|
|
@@ -238,6 +295,8 @@ export function ColumnRoleTable<
|
|
|
238
295
|
previewRows = 5,
|
|
239
296
|
totalRows,
|
|
240
297
|
disabled,
|
|
298
|
+
bodyProps,
|
|
299
|
+
rowProps,
|
|
241
300
|
labels: labelsProp,
|
|
242
301
|
className,
|
|
243
302
|
}: ColumnRoleTableProps<R, M>) {
|
|
@@ -265,86 +324,119 @@ export function ColumnRoleTable<
|
|
|
265
324
|
? labelOf(gap[0])
|
|
266
325
|
: labels.oneOf(formatList(gap.map(labelOf), "disjunction", locale)),
|
|
267
326
|
);
|
|
327
|
+
// "(required)" only where it tells one option from another.
|
|
328
|
+
const markRequired = roles.some((role) => role.required !== true);
|
|
329
|
+
|
|
330
|
+
// The Table's own scroll wrapper — the `<table>`'s parent — is what overflows; the
|
|
331
|
+
// fade is measured on it and drawn on the box round it, which has the same edges.
|
|
332
|
+
const scroller = useRef<HTMLElement | null>(null);
|
|
333
|
+
const tableRef = useCallback((table: HTMLTableElement | null) => {
|
|
334
|
+
scroller.current = table?.parentElement ?? null;
|
|
335
|
+
}, []);
|
|
336
|
+
const fade = useStripFade(scroller);
|
|
268
337
|
|
|
269
338
|
return (
|
|
270
339
|
<div
|
|
271
340
|
className={cn("min-w-0 space-y-1.5", className)}
|
|
272
341
|
data-slot="column-role-table"
|
|
273
342
|
>
|
|
274
|
-
<
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
column,
|
|
301
|
-
event.target.value === ""
|
|
302
|
-
? null
|
|
303
|
-
: (event.target.value as R),
|
|
304
|
-
),
|
|
305
|
-
)
|
|
306
|
-
}
|
|
343
|
+
<div
|
|
344
|
+
data-slot="column-role-table-scroll"
|
|
345
|
+
// In reading-direction terms ("start", "end", "both"), for a caller's styling.
|
|
346
|
+
data-overflow={fade.overflow}
|
|
347
|
+
style={
|
|
348
|
+
fade.mask
|
|
349
|
+
? { maskImage: fade.mask, WebkitMaskImage: fade.mask }
|
|
350
|
+
: undefined
|
|
351
|
+
}
|
|
352
|
+
>
|
|
353
|
+
<Table
|
|
354
|
+
ref={tableRef}
|
|
355
|
+
density="compact"
|
|
356
|
+
framed
|
|
357
|
+
aria-label={labels.table}
|
|
358
|
+
>
|
|
359
|
+
<TableHead>
|
|
360
|
+
<TableRow>
|
|
361
|
+
{columns.map((column) => {
|
|
362
|
+
const role = roleOfColumn(roles, mapping, column);
|
|
363
|
+
const name = nameOf(column);
|
|
364
|
+
return (
|
|
365
|
+
<TableHeaderCell
|
|
366
|
+
key={column}
|
|
367
|
+
scope="col"
|
|
368
|
+
className="min-w-36 py-2 align-top"
|
|
307
369
|
>
|
|
308
|
-
<
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
370
|
+
<span className="block max-w-56 truncate pb-1 text-[11px] font-normal text-[var(--text-muted)]">
|
|
371
|
+
{name}
|
|
372
|
+
</span>
|
|
373
|
+
<Select
|
|
374
|
+
size="sm"
|
|
375
|
+
className="w-full"
|
|
376
|
+
aria-label={labels.roleOf(name)}
|
|
377
|
+
value={role ?? ""}
|
|
378
|
+
disabled={disabled}
|
|
379
|
+
// Focus brings a select behind the fade clear of it — the browser's
|
|
380
|
+
// own scroll stops at the edge, under the fade.
|
|
381
|
+
onFocus={(event) => {
|
|
382
|
+
const cell = event.currentTarget.closest("th");
|
|
383
|
+
if (scroller.current && cell)
|
|
384
|
+
scrollIntoStrip(scroller.current, cell);
|
|
385
|
+
}}
|
|
386
|
+
onChange={(event) =>
|
|
387
|
+
onMappingChange(
|
|
388
|
+
assignColumnRole(
|
|
389
|
+
roles,
|
|
390
|
+
mapping,
|
|
391
|
+
column,
|
|
392
|
+
event.target.value === ""
|
|
393
|
+
? null
|
|
394
|
+
: (event.target.value as R),
|
|
395
|
+
),
|
|
396
|
+
)
|
|
397
|
+
}
|
|
398
|
+
>
|
|
399
|
+
<option value="">{labels.ignore}</option>
|
|
400
|
+
{roles.map((option) => (
|
|
401
|
+
<option key={option.value} value={option.value}>
|
|
402
|
+
{option.required === true && markRequired
|
|
403
|
+
? labels.requiredRole(option.label)
|
|
404
|
+
: option.label}
|
|
405
|
+
</option>
|
|
406
|
+
))}
|
|
407
|
+
</Select>
|
|
408
|
+
</TableHeaderCell>
|
|
409
|
+
);
|
|
410
|
+
})}
|
|
344
411
|
</TableRow>
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
412
|
+
</TableHead>
|
|
413
|
+
<TableBody {...bodyProps}>
|
|
414
|
+
{shown.map((row, index) => (
|
|
415
|
+
<TableRow {...rowProps?.(row, index)} key={index}>
|
|
416
|
+
{columns.map((column) => (
|
|
417
|
+
<TableCell
|
|
418
|
+
key={column}
|
|
419
|
+
data-ignored={
|
|
420
|
+
roleOfColumn(roles, mapping, column) === null || undefined
|
|
421
|
+
}
|
|
422
|
+
className={cn(
|
|
423
|
+
"font-mono whitespace-nowrap",
|
|
424
|
+
// Dimmed, not hidden: its values are what tell you whether ignoring
|
|
425
|
+
// it was right.
|
|
426
|
+
roleOfColumn(roles, mapping, column) === null &&
|
|
427
|
+
"text-[var(--text-muted)]",
|
|
428
|
+
)}
|
|
429
|
+
>
|
|
430
|
+
<span className="block max-w-56 truncate">
|
|
431
|
+
{row[column] ?? ""}
|
|
432
|
+
</span>
|
|
433
|
+
</TableCell>
|
|
434
|
+
))}
|
|
435
|
+
</TableRow>
|
|
436
|
+
))}
|
|
437
|
+
</TableBody>
|
|
438
|
+
</Table>
|
|
439
|
+
</div>
|
|
348
440
|
{total > shown.length && (
|
|
349
441
|
<p className="text-xs text-[var(--text-muted)]">
|
|
350
442
|
{labels.previewOf(shown.length, total)}
|
|
@@ -392,6 +484,12 @@ export interface ColumnMapperProps<R extends string> {
|
|
|
392
484
|
* order. */
|
|
393
485
|
guess?: (table: TextTable) => ColumnMapping<R>;
|
|
394
486
|
disabled?: boolean;
|
|
487
|
+
/** The preview table's `<tbody>` attributes — {@link ColumnRoleTableProps.bodyProps}
|
|
488
|
+
* (0.24): `{ "data-private": "" }` for a table of someone's own data. */
|
|
489
|
+
bodyProps?: ColumnRoleTableBodyProps;
|
|
490
|
+
/** One preview row's `<tr>` attributes — {@link ColumnRoleTableProps.rowProps}
|
|
491
|
+
* (0.24). `row` is the row as read, `index` its place in the preview. */
|
|
492
|
+
rowProps?: (row: readonly string[], index: number) => ColumnRoleTableRowProps | undefined;
|
|
395
493
|
labels?: Partial<ColumnMapperLabels>;
|
|
396
494
|
className?: string;
|
|
397
495
|
}
|
|
@@ -439,6 +537,8 @@ export function ColumnMapper<R extends string>({
|
|
|
439
537
|
previewRows = 5,
|
|
440
538
|
guess,
|
|
441
539
|
disabled = false,
|
|
540
|
+
bodyProps,
|
|
541
|
+
rowProps,
|
|
442
542
|
labels: labelsProp,
|
|
443
543
|
className,
|
|
444
544
|
}: ColumnMapperProps<R>) {
|
|
@@ -632,6 +732,8 @@ export function ColumnMapper<R extends string>({
|
|
|
632
732
|
onMappingChange={(mapping) => commit({ ...state, mapping })}
|
|
633
733
|
previewRows={previewRows}
|
|
634
734
|
disabled={disabled}
|
|
735
|
+
bodyProps={bodyProps}
|
|
736
|
+
rowProps={rowProps}
|
|
635
737
|
labels={labelsProp}
|
|
636
738
|
/>
|
|
637
739
|
{unread.length > 0 && (
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Fragment, useId, useMemo, useRef, useState } from "react";
|
|
2
|
-
import type { ComponentPropsWithoutRef, ReactNode, RefObject } from "react";
|
|
2
|
+
import type { ComponentPropsWithoutRef, ReactNode, Ref, RefObject } from "react";
|
|
3
3
|
import { createPortal } from "react-dom";
|
|
4
4
|
import { ChevronDown, Plus, X } from "lucide-react";
|
|
5
5
|
import { FIELD_BASE, FIELD_FLOATING_PAD, FIELD_INVALID, PHONE_QUERY } from "./ui";
|
|
@@ -244,6 +244,14 @@ export interface ComboboxProps
|
|
|
244
244
|
maxSuggestions?: number;
|
|
245
245
|
searchPlaceholder?: string;
|
|
246
246
|
closeLabel?: string;
|
|
247
|
+
/**
|
|
248
|
+
* The phone sheet's heading — and, as a string, the dialog's accessible name — for a
|
|
249
|
+
* field whose label is drawn by someone else: a form's `FormLabel` above it,
|
|
250
|
+
* which `RhfTextCombobox` hands over here (0.24). Default: `label`. Without either, the
|
|
251
|
+
* full-screen sheet opened untitled and unnamed, the one screen on a phone that has to
|
|
252
|
+
* say what it is asking for (live #200).
|
|
253
|
+
*/
|
|
254
|
+
sheetTitle?: ReactNode;
|
|
247
255
|
/**
|
|
248
256
|
* Label for the row that COMMITS a value the list does not contain — e.g.
|
|
249
257
|
* `(v) => \`Add "${v}" as payee\``. Supplying it turns the free text into
|
|
@@ -260,6 +268,15 @@ export interface ComboboxProps
|
|
|
260
268
|
* values are all supposed to already exist.
|
|
261
269
|
*/
|
|
262
270
|
createLabel?: (value: string) => string;
|
|
271
|
+
/**
|
|
272
|
+
* The `<input>` — the control a reader types in and the one focus belongs on — as a
|
|
273
|
+
* React 19 ref prop, the way {@link CountrySelect} takes its trigger's (0.24, kastlan:
|
|
274
|
+
* react-hook-form's focus-on-error calls `focus()` on whatever `field.ref` is handed,
|
|
275
|
+
* and a combobox with no ref inside a hand-wired `RhfField` gave it nothing, so a
|
|
276
|
+
* failed submit could not take the user to it). Not the wrapper: a ref is for focus,
|
|
277
|
+
* measurement and selection, and all three are the input's. `RhfTextCombobox` passes it.
|
|
278
|
+
*/
|
|
279
|
+
ref?: Ref<HTMLInputElement>;
|
|
263
280
|
}
|
|
264
281
|
|
|
265
282
|
/**
|
|
@@ -285,6 +302,7 @@ export function Combobox(props: ComboboxProps) {
|
|
|
285
302
|
maxSuggestions,
|
|
286
303
|
searchPlaceholder,
|
|
287
304
|
closeLabel,
|
|
305
|
+
sheetTitle,
|
|
288
306
|
createLabel,
|
|
289
307
|
invalid,
|
|
290
308
|
error,
|
|
@@ -296,6 +314,7 @@ export function Combobox(props: ComboboxProps) {
|
|
|
296
314
|
autoFocus,
|
|
297
315
|
onBlur,
|
|
298
316
|
onSubmit,
|
|
317
|
+
ref,
|
|
299
318
|
"aria-label": ariaLabel,
|
|
300
319
|
// Off `rest` and onto the <input>, which is the combobox a reader meets: `Field`'s
|
|
301
320
|
// render-prop spreads `{ id, aria-describedby, aria-invalid, aria-required }`, and
|
|
@@ -481,6 +500,7 @@ export function Combobox(props: ComboboxProps) {
|
|
|
481
500
|
`peer`, so it is dimmed by hand above). */
|
|
482
501
|
<div ref={fieldRef} className={cn("relative", inert && "cursor-not-allowed opacity-50")}>
|
|
483
502
|
<input
|
|
503
|
+
ref={ref}
|
|
484
504
|
id={fieldId}
|
|
485
505
|
value={value}
|
|
486
506
|
placeholder={placeholder}
|
|
@@ -616,7 +636,7 @@ export function Combobox(props: ComboboxProps) {
|
|
|
616
636
|
<PickerSheet
|
|
617
637
|
open={open && !inert}
|
|
618
638
|
onClose={() => setOpen(false)}
|
|
619
|
-
title={label}
|
|
639
|
+
title={sheetTitle ?? label}
|
|
620
640
|
// The sheet's input IS the field: this is a free-text control, so what is
|
|
621
641
|
// typed here is the value (a brand-new payee is just a name nothing
|
|
622
642
|
// matches), and the list below narrows as it changes.
|
|
@@ -794,6 +814,11 @@ export interface InlineEntityComboboxProps<V extends string | number, C extends
|
|
|
794
814
|
* list simply does not open. Default: `combobox.noResults` from the provider. */
|
|
795
815
|
emptyLabel?: string;
|
|
796
816
|
closeLabel?: string;
|
|
817
|
+
/** The phone sheet's heading (and, as a string, its accessible name) for a field
|
|
818
|
+
* whose label is drawn by someone else — a form's `FormLabel`;
|
|
819
|
+
* `RhfInlineEntityCombobox` hands its label over here (0.24). Default: `label`.
|
|
820
|
+
* See {@link Combobox}'s `sheetTitle`. */
|
|
821
|
+
sheetTitle?: ReactNode;
|
|
797
822
|
/** Offer a clear "×" in place of the chevron whenever something is selected.
|
|
798
823
|
*
|
|
799
824
|
* Emptying the text already clears (see {@link reconcile}), and on a desktop that
|
|
@@ -870,6 +895,16 @@ export interface InlineEntityComboboxProps<V extends string | number, C extends
|
|
|
870
895
|
* `commit`: a locked field opens no list at all.
|
|
871
896
|
*/
|
|
872
897
|
createCommit?: boolean;
|
|
898
|
+
/**
|
|
899
|
+
* The `<input>` — the focusable control, the one a reader types in — as a React 19
|
|
900
|
+
* ref prop (0.24, kastlan). Its account picker is this component inside an
|
|
901
|
+
* `RhfField` render, and react-hook-form's focus-on-error calls `focus()` on whatever
|
|
902
|
+
* `field.ref` is handed: with no ref there was nothing to hand it, so a line-item form
|
|
903
|
+
* whose account was missing failed its submit and left the caret where it was. Pass
|
|
904
|
+
* `field.ref` here, or use `RhfInlineEntityCombobox`, which does. The wrapper is not
|
|
905
|
+
* reachable this way — focus and selection are the input's. See {@link Combobox}'s `ref`.
|
|
906
|
+
*/
|
|
907
|
+
ref?: Ref<HTMLInputElement>;
|
|
873
908
|
}
|
|
874
909
|
|
|
875
910
|
/**
|
|
@@ -907,6 +942,7 @@ export function InlineEntityCombobox<V extends string | number, C extends ComboC
|
|
|
907
942
|
searchPlaceholder,
|
|
908
943
|
emptyLabel,
|
|
909
944
|
closeLabel,
|
|
945
|
+
sheetTitle,
|
|
910
946
|
clearable,
|
|
911
947
|
clearLabel,
|
|
912
948
|
disabledReason,
|
|
@@ -918,6 +954,7 @@ export function InlineEntityCombobox<V extends string | number, C extends ComboC
|
|
|
918
954
|
invalid,
|
|
919
955
|
error,
|
|
920
956
|
hint,
|
|
957
|
+
ref,
|
|
921
958
|
"aria-label": ariaLabel,
|
|
922
959
|
// Off `rest` and onto the <input>, which is the combobox a reader meets: `Field`'s
|
|
923
960
|
// render-prop spreads `{ id, aria-describedby, aria-invalid, aria-required }`, and
|
|
@@ -1146,6 +1183,7 @@ export function InlineEntityCombobox<V extends string | number, C extends ComboC
|
|
|
1146
1183
|
`peer`, so it is dimmed by hand above). */
|
|
1147
1184
|
<div ref={fieldRef} className={cn("relative", inert && "cursor-not-allowed opacity-50")}>
|
|
1148
1185
|
<input
|
|
1186
|
+
ref={ref}
|
|
1149
1187
|
id={fieldId}
|
|
1150
1188
|
value={shown}
|
|
1151
1189
|
placeholder={placeholder}
|
|
@@ -1300,7 +1338,7 @@ export function InlineEntityCombobox<V extends string | number, C extends ComboC
|
|
|
1300
1338
|
// Closing without choosing keeps the value: `text` was never emptied, so
|
|
1301
1339
|
// reconcile has nothing to undo — it just puts the label back.
|
|
1302
1340
|
onClose={reconcile}
|
|
1303
|
-
title={label}
|
|
1341
|
+
title={sheetTitle ?? label}
|
|
1304
1342
|
query={sheetQuery}
|
|
1305
1343
|
onQueryChange={(v) => {
|
|
1306
1344
|
setSheetQuery(v);
|