@eifi1/ui-kit 0.14.0 → 0.14.2
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 +4 -3
- package/dist/components/amount-input.d.ts +8 -0
- package/dist/components/amount-input.js +10 -4
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/error-boundary.js +2 -1
- package/dist/components/error-boundary.js.map +1 -1
- package/dist/components/money-field.d.ts +92 -0
- package/dist/components/money-field.js +40 -0
- package/dist/components/money-field.js.map +1 -0
- package/dist/components/number-field.d.ts +12 -1
- package/dist/components/number-field.js +4 -0
- package/dist/components/number-field.js.map +1 -1
- package/dist/components/page-header.d.ts +3 -1
- package/dist/components/page-header.js +68 -31
- package/dist/components/page-header.js.map +1 -1
- package/dist/components/text-link.js +2 -1
- package/dist/components/text-link.js.map +1 -1
- package/dist/feedback/feedback-dialog.d.ts +10 -1
- package/dist/feedback/feedback-dialog.js +13 -3
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/i18n/locales/de.js +1 -0
- package/dist/i18n/locales/de.js.map +1 -1
- package/dist/i18n/locales/es.js +1 -0
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.js +1 -0
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.js +1 -0
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.js +1 -0
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.js +1 -0
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/document-navigation.d.ts +15 -0
- package/dist/lib/document-navigation.js +13 -0
- package/dist/lib/document-navigation.js.map +1 -0
- package/package.json +7 -4
- package/src/components/amount-input.tsx +18 -4
- package/src/components/error-boundary.tsx +2 -1
- package/src/components/money-field.tsx +75 -0
- package/src/components/number-field.tsx +14 -0
- package/src/components/page-header.tsx +65 -12
- package/src/components/text-link.tsx +2 -1
- package/src/feedback/feedback-dialog.tsx +20 -2
- package/src/i18n/locales/de.ts +1 -0
- package/src/i18n/locales/es.ts +1 -0
- package/src/i18n/locales/fr.ts +1 -0
- package/src/i18n/locales/hu.ts +1 -0
- package/src/i18n/locales/it.ts +1 -0
- package/src/i18n/locales/zh.ts +1 -0
- package/src/index.ts +1 -0
- package/src/lib/document-navigation.ts +17 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useLayoutEffect, useRef, useState } from "react";
|
|
1
2
|
import type { ComponentPropsWithoutRef, ElementType, ReactNode } from "react";
|
|
2
3
|
import { cn } from "../lib/cn";
|
|
3
4
|
import { SECTION_LABEL_CLASS } from "./text";
|
|
@@ -42,11 +43,15 @@ const ROW: Record<PageHeaderMobileLayout, string> = {
|
|
|
42
43
|
*/
|
|
43
44
|
export type PageHeaderActionsAlign = "start" | "center" | "end";
|
|
44
45
|
|
|
45
|
-
/**
|
|
46
|
-
*
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
46
|
+
/** Applied to the ROW, so the title block and the actions are aligned against each
|
|
47
|
+
* other on one cross axis. 0.14.0 put `self-*` on the actions box alone, which moved
|
|
48
|
+
* nothing when the actions were the taller item — they already filled the row, and
|
|
49
|
+
* the title stayed at the top (keksdose H2, reports-page's two-line labelled
|
|
50
|
+
* CurrencySelect). Per layout, because `stacked` is a column on a phone, where
|
|
51
|
+
* `items-center` would centre everything HORIZONTALLY — so it aligns from `sm` up. */
|
|
52
|
+
const ROW_ALIGN: Record<PageHeaderMobileLayout, Record<PageHeaderActionsAlign, string>> = {
|
|
53
|
+
stacked: { start: "sm:items-start", center: "sm:items-center", end: "sm:items-end" },
|
|
54
|
+
inline: { start: "items-start", center: "items-center", end: "items-end" },
|
|
50
55
|
};
|
|
51
56
|
|
|
52
57
|
export interface PageHeaderProps extends Omit<ComponentPropsWithoutRef<"div">, "title"> {
|
|
@@ -106,7 +111,9 @@ export interface PageHeaderProps extends Omit<ComponentPropsWithoutRef<"div">, "
|
|
|
106
111
|
* `CurrencySelect` as its action: a field with its label on top is taller than the
|
|
107
112
|
* title, and `stacked`'s default top alignment hangs it from the title's cap height
|
|
108
113
|
* with its control well below the title's line. `center` sets it on the title's
|
|
109
|
-
* middle, as that page's hand-written `items-center` row did.
|
|
114
|
+
* middle, as that page's hand-written `items-center` row did. It aligns the whole row
|
|
115
|
+
* (title block and actions against each other), since 0.14.1: 0.14.0 moved only the
|
|
116
|
+
* actions box, which did nothing when the actions were the taller item (keksdose H2).
|
|
110
117
|
*/
|
|
111
118
|
actionsAlign?: PageHeaderActionsAlign;
|
|
112
119
|
/**
|
|
@@ -155,7 +162,44 @@ export function PageHeader({
|
|
|
155
162
|
const Heading = as as ElementType;
|
|
156
163
|
const hasSecondary = secondaryActions != null;
|
|
157
164
|
const wrapsSecondary = hasSecondary && mobileLayout === "inline";
|
|
158
|
-
const align = actionsAlign != null ?
|
|
165
|
+
const align = actionsAlign != null ? ROW_ALIGN[mobileLayout][actionsAlign] : undefined;
|
|
166
|
+
// keksdose H1: in `inline` the title block could shrink below its longest word, and
|
|
167
|
+
// `break-words` then split "Monatsbudget" into "Monatsbudg" / "et" beside the actions.
|
|
168
|
+
// Unless it truncates, the title keeps its longest word (min-content) and the ROW
|
|
169
|
+
// wraps instead: actions that do not fit go to a line of their own, as the app's old
|
|
170
|
+
// `flex-wrap` header did.
|
|
171
|
+
const keepsWords = mobileLayout === "inline" && !truncateTitle;
|
|
172
|
+
// keksdose, after H1: once the actions have wrapped below the title, the phone's
|
|
173
|
+
// header was THREE rows — title / actions / secondary — where the app's old header
|
|
174
|
+
// shared the second row between the month nav and the toggles (~48px of phone
|
|
175
|
+
// height). So the secondary group's full-width basis (G6b: a row of its own) is
|
|
176
|
+
// dropped exactly when the actions already sit below the title; then it joins their
|
|
177
|
+
// row if it fits, and wraps under them if not. While the actions share the title's
|
|
178
|
+
// row, nothing changes: the secondary group never climbs onto the title's row.
|
|
179
|
+
//
|
|
180
|
+
// CSS cannot see "the previous item wrapped", so it is measured — by a
|
|
181
|
+
// ResizeObserver, whose first callback runs on `observe`. It is stable: the
|
|
182
|
+
// secondary group comes after the actions, so its basis cannot move them. No
|
|
183
|
+
// ResizeObserver (jsdom) or no layout keeps the basis, the 0.14.0 layout.
|
|
184
|
+
const measuresWrap = keepsWords && hasSecondary && actions != null;
|
|
185
|
+
const titleRef = useRef<HTMLDivElement>(null);
|
|
186
|
+
const actionsRef = useRef<HTMLDivElement>(null);
|
|
187
|
+
const [actionsWrapped, setActionsWrapped] = useState(false);
|
|
188
|
+
useLayoutEffect(() => {
|
|
189
|
+
const titleBox = titleRef.current;
|
|
190
|
+
const actionsBox = actionsRef.current;
|
|
191
|
+
if (!measuresWrap || !titleBox || !actionsBox || typeof ResizeObserver === "undefined") return;
|
|
192
|
+
const observer = new ResizeObserver(() => {
|
|
193
|
+
const t = titleBox.getBoundingClientRect();
|
|
194
|
+
const a = actionsBox.getBoundingClientRect();
|
|
195
|
+
// Unmeasured (display: none, no layout): keep the full-width basis.
|
|
196
|
+
setActionsWrapped(t.height > 0 && a.height > 0 && a.top >= t.bottom - 1);
|
|
197
|
+
});
|
|
198
|
+
observer.observe(titleBox);
|
|
199
|
+
observer.observe(actionsBox);
|
|
200
|
+
if (titleBox.parentElement) observer.observe(titleBox.parentElement);
|
|
201
|
+
return () => observer.disconnect();
|
|
202
|
+
}, [measuresWrap]);
|
|
159
203
|
return (
|
|
160
204
|
<div {...rest} className={cn("flex min-w-0 flex-col gap-2", className)}>
|
|
161
205
|
{breadcrumbs}
|
|
@@ -164,8 +208,15 @@ export function PageHeader({
|
|
|
164
208
|
basis would size a column item's HEIGHT); from `sm` up the row stops wrapping
|
|
165
209
|
and the order utilities seat it between the title and the actions. Without
|
|
166
210
|
one, nothing changes. */}
|
|
167
|
-
<div
|
|
168
|
-
|
|
211
|
+
<div
|
|
212
|
+
className={cn(
|
|
213
|
+
"flex min-w-0",
|
|
214
|
+
ROW[mobileLayout],
|
|
215
|
+
keepsWords ? "flex-wrap" : wrapsSecondary && "flex-wrap sm:flex-nowrap",
|
|
216
|
+
align,
|
|
217
|
+
)}
|
|
218
|
+
>
|
|
219
|
+
<div ref={titleRef} className={cn("flex-1", keepsWords ? "min-w-min" : "min-w-0")}>
|
|
169
220
|
{eyebrow != null && <p className={cn(SECTION_LABEL_CLASS.xs, "mb-1")}>{eyebrow}</p>}
|
|
170
221
|
<Heading
|
|
171
222
|
className={cn(
|
|
@@ -179,7 +230,10 @@ export function PageHeader({
|
|
|
179
230
|
{description != null && <p className="mt-1 text-sm text-[var(--text-muted)]">{description}</p>}
|
|
180
231
|
</div>
|
|
181
232
|
{actions != null && (
|
|
182
|
-
<div
|
|
233
|
+
<div
|
|
234
|
+
ref={actionsRef}
|
|
235
|
+
className={cn("flex shrink-0 flex-wrap items-center gap-2", hasSecondary && "sm:order-2")}
|
|
236
|
+
>
|
|
183
237
|
{actions}
|
|
184
238
|
</div>
|
|
185
239
|
)}
|
|
@@ -187,8 +241,7 @@ export function PageHeader({
|
|
|
187
241
|
<div
|
|
188
242
|
className={cn(
|
|
189
243
|
"flex flex-wrap items-center gap-2 sm:order-1 sm:shrink-0",
|
|
190
|
-
wrapsSecondary && "basis-full sm:basis-auto",
|
|
191
|
-
align,
|
|
244
|
+
wrapsSecondary && (measuresWrap && actionsWrapped ? "sm:basis-auto" : "basis-full sm:basis-auto"),
|
|
192
245
|
)}
|
|
193
246
|
>
|
|
194
247
|
{secondaryActions}
|
|
@@ -4,6 +4,7 @@ import type { LucideIcon } from "lucide-react";
|
|
|
4
4
|
import { cn } from "../lib/cn";
|
|
5
5
|
import { DEFAULT_COMMON_LABELS, useKitLabels, useKitLink } from "../i18n/kit-labels";
|
|
6
6
|
import type { KitLinkComponent } from "../i18n/kit-labels";
|
|
7
|
+
import { documentNavigation } from "../lib/document-navigation";
|
|
7
8
|
|
|
8
9
|
/* ── Choosing what draws a link ──────────────────────────────────────────── */
|
|
9
10
|
|
|
@@ -98,7 +99,7 @@ export function replacingClick(
|
|
|
98
99
|
onClick?.(event);
|
|
99
100
|
if (!isPlainNavigationClick(event)) return;
|
|
100
101
|
event.preventDefault();
|
|
101
|
-
|
|
102
|
+
documentNavigation.replace(href);
|
|
102
103
|
};
|
|
103
104
|
}
|
|
104
105
|
|
|
@@ -53,6 +53,9 @@ export interface FeedbackDialogTextLabels {
|
|
|
53
53
|
category: string;
|
|
54
54
|
subject: string;
|
|
55
55
|
body: string;
|
|
56
|
+
/** The body's label when `requireBody={false}`: "What happened? (optional)".
|
|
57
|
+
* Optional, so a complete `UiKitLabels` typed before 0.14.2 still compiles. */
|
|
58
|
+
bodyOptional?: string;
|
|
56
59
|
/** The heading over the attachment buttons. */
|
|
57
60
|
attachment: string;
|
|
58
61
|
/** The line beside the buttons naming the shortcut. */
|
|
@@ -66,6 +69,7 @@ export const DEFAULT_FEEDBACK_DIALOG_LABELS: FeedbackDialogTextLabels = {
|
|
|
66
69
|
category: "Category",
|
|
67
70
|
subject: "Subject",
|
|
68
71
|
body: "What happened?",
|
|
72
|
+
bodyOptional: "What happened? (optional)",
|
|
69
73
|
attachment: "Screenshot",
|
|
70
74
|
submitHint: "Ctrl/⌘ + Enter to send",
|
|
71
75
|
cancel: "Cancel",
|
|
@@ -108,6 +112,7 @@ export function FeedbackDialog({
|
|
|
108
112
|
maxAttachmentBytes = DEFAULT_MAX_ATTACHMENT_BYTES,
|
|
109
113
|
onAttachmentError,
|
|
110
114
|
onCaptureScreenshot,
|
|
115
|
+
requireBody = true,
|
|
111
116
|
}: {
|
|
112
117
|
open: boolean;
|
|
113
118
|
onClose: () => void;
|
|
@@ -129,6 +134,12 @@ export function FeedbackDialog({
|
|
|
129
134
|
* attachment"; the returned file is fed through the same validation + preview.
|
|
130
135
|
*/
|
|
131
136
|
onCaptureScreenshot?: () => Promise<File | null>;
|
|
137
|
+
/**
|
|
138
|
+
* `false`: a report with only a subject can be sent, and the body's label says it is
|
|
139
|
+
* optional (`bodyOptional`, unless `labels.body` is passed). Default `true`, the
|
|
140
|
+
* dialog's behaviour until 0.14.2 (keksdose K1: its backend takes title-only reports).
|
|
141
|
+
*/
|
|
142
|
+
requireBody?: boolean;
|
|
132
143
|
}) {
|
|
133
144
|
const [title, setTitle] = useState("");
|
|
134
145
|
const [body, setBody] = useState("");
|
|
@@ -151,7 +162,9 @@ export function FeedbackDialog({
|
|
|
151
162
|
}
|
|
152
163
|
}
|
|
153
164
|
|
|
154
|
-
|
|
165
|
+
// Trimmed: a subject or body of spaces is empty (keksdose's API answers a blank title
|
|
166
|
+
// with a 422, and a blank report is no report).
|
|
167
|
+
const canSubmit = !!title.trim() && (!requireBody || !!body.trim()) && !submitting;
|
|
155
168
|
const trySubmit = () => {
|
|
156
169
|
if (canSubmit) void onSubmit({ title, body, category, attachment });
|
|
157
170
|
};
|
|
@@ -174,7 +187,12 @@ export function FeedbackDialog({
|
|
|
174
187
|
<Input label={labels.subject} value={title} onChange={(e) => setTitle(e.target.value)} variant="display" />
|
|
175
188
|
);
|
|
176
189
|
const bodyField = (
|
|
177
|
-
<Textarea
|
|
190
|
+
<Textarea
|
|
191
|
+
rows={5}
|
|
192
|
+
label={requireBody ? labels.body : (labelsProp?.body ?? labels.bodyOptional ?? labels.body)}
|
|
193
|
+
value={body}
|
|
194
|
+
onChange={(e) => setBody(e.target.value)}
|
|
195
|
+
/>
|
|
178
196
|
);
|
|
179
197
|
const attachmentField = (
|
|
180
198
|
<FeedbackAttachmentField
|
package/src/i18n/locales/de.ts
CHANGED
|
@@ -26,6 +26,7 @@ export function uiKitLabelsDe(numberLocale = "de-DE"): UiKitLabels {
|
|
|
26
26
|
category: "Kategorie",
|
|
27
27
|
subject: "Betreff",
|
|
28
28
|
body: "Was ist passiert?",
|
|
29
|
+
bodyOptional: "Was ist passiert? (optional)",
|
|
29
30
|
attachment: "Screenshot",
|
|
30
31
|
submitHint: "Strg/⌘ + Enter zum Senden",
|
|
31
32
|
cancel: "Abbrechen",
|
package/src/i18n/locales/es.ts
CHANGED
|
@@ -27,6 +27,7 @@ export function uiKitLabelsEs(numberLocale = "es-ES"): UiKitLabels {
|
|
|
27
27
|
category: "Categoría",
|
|
28
28
|
subject: "Asunto",
|
|
29
29
|
body: "¿Qué ha pasado?",
|
|
30
|
+
bodyOptional: "¿Qué ha pasado? (opcional)",
|
|
30
31
|
attachment: "Captura de pantalla",
|
|
31
32
|
submitHint: "Ctrl/⌘ + Intro para enviar",
|
|
32
33
|
cancel: "Cancelar",
|
package/src/i18n/locales/fr.ts
CHANGED
|
@@ -27,6 +27,7 @@ export function uiKitLabelsFr(numberLocale = "fr-FR"): UiKitLabels {
|
|
|
27
27
|
category: "Catégorie",
|
|
28
28
|
subject: "Objet",
|
|
29
29
|
body: "Que s’est-il passé\u202f?",
|
|
30
|
+
bodyOptional: "Que s’est-il passé\u202f? (facultatif)",
|
|
30
31
|
attachment: "Capture d’écran",
|
|
31
32
|
submitHint: "Ctrl/⌘ + Entrée pour envoyer",
|
|
32
33
|
cancel: "Annuler",
|
package/src/i18n/locales/hu.ts
CHANGED
|
@@ -26,6 +26,7 @@ export function uiKitLabelsHu(numberLocale = "hu-HU"): UiKitLabels {
|
|
|
26
26
|
category: "Kategória",
|
|
27
27
|
subject: "Tárgy",
|
|
28
28
|
body: "Mi történt?",
|
|
29
|
+
bodyOptional: "Mi történt? (nem kötelező)",
|
|
29
30
|
attachment: "Képernyőkép",
|
|
30
31
|
submitHint: "Ctrl/⌘ + Enter a küldéshez",
|
|
31
32
|
cancel: "Mégse",
|
package/src/i18n/locales/it.ts
CHANGED
|
@@ -27,6 +27,7 @@ export function uiKitLabelsIt(numberLocale = "it-IT"): UiKitLabels {
|
|
|
27
27
|
category: "Categoria",
|
|
28
28
|
subject: "Oggetto",
|
|
29
29
|
body: "Cosa è successo?",
|
|
30
|
+
bodyOptional: "Cosa è successo? (facoltativo)",
|
|
30
31
|
attachment: "Screenshot",
|
|
31
32
|
submitHint: "Ctrl/⌘ + Invio per inviare",
|
|
32
33
|
cancel: "Annulla",
|
package/src/i18n/locales/zh.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -83,6 +83,7 @@ export * from "./components/numpad-sheet";
|
|
|
83
83
|
export * from "./components/number-input";
|
|
84
84
|
export * from "./components/currency-select";
|
|
85
85
|
export * from "./components/amount-input";
|
|
86
|
+
export * from "./components/money-field";
|
|
86
87
|
export * from "./components/combobox";
|
|
87
88
|
export * from "./components/picker-sheet";
|
|
88
89
|
export * from "./components/entity-combobox";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The full-document navigations the kit makes, behind one object the tests can spy on.
|
|
3
|
+
*
|
|
4
|
+
* jsdom implements neither `location.reload` nor `location.replace`, and its
|
|
5
|
+
* `window.location` is unforgeable: a test cannot stub it by redefining the property
|
|
6
|
+
* once the suite runs each file in its own VM context (`pool: "vmThreads"`, which took
|
|
7
|
+
* the suite from ~3 minutes to ~1). `vi.spyOn(documentNavigation, "reload")` works in
|
|
8
|
+
* every pool. Internal, not part of the package's surface.
|
|
9
|
+
*/
|
|
10
|
+
export const documentNavigation = {
|
|
11
|
+
reload(): void {
|
|
12
|
+
window.location.reload();
|
|
13
|
+
},
|
|
14
|
+
replace(href: string): void {
|
|
15
|
+
window.location.replace(href);
|
|
16
|
+
},
|
|
17
|
+
};
|