@cosmicdrift/kumiko-renderer-web 0.185.0 → 0.186.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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-renderer-web",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.186.1",
|
|
4
4
|
"description": "Web-platform bindings for @cosmicdrift/kumiko-renderer. HTML default-primitives, browser history-based navigation, EventSource-backed live events, and a one-call createKumikoApp that mounts the whole stack via react-dom.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -16,9 +16,9 @@
|
|
|
16
16
|
"./styles.css": "./src/styles.css"
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@cosmicdrift/kumiko-dispatcher-live": "0.
|
|
20
|
-
"@cosmicdrift/kumiko-headless": "0.
|
|
21
|
-
"@cosmicdrift/kumiko-renderer": "0.
|
|
19
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.186.1",
|
|
20
|
+
"@cosmicdrift/kumiko-headless": "0.186.1",
|
|
21
|
+
"@cosmicdrift/kumiko-renderer": "0.186.1",
|
|
22
22
|
"@radix-ui/react-dialog": "^1.1.15",
|
|
23
23
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
|
24
24
|
"@radix-ui/react-label": "^2.1.8",
|
|
@@ -183,6 +183,13 @@ describe("defaultCellRender", () => {
|
|
|
183
183
|
expect(result).not.toMatch(/[.,]\d\d$/);
|
|
184
184
|
});
|
|
185
185
|
|
|
186
|
+
test("money → rehydrated { amount major, amountMinor } uses amountMinor (not major×100 wrong)", () => {
|
|
187
|
+
// rehydrateMoney (fw#1830): amount is major units, amountMinor is cents.
|
|
188
|
+
// formatMoney expects cents — using amount would show 100× too small.
|
|
189
|
+
const result = defaultCellRender({ amount: 119, currency: "EUR", amountMinor: 11900 }, "money");
|
|
190
|
+
expect(result.replace(/[^0-9]/g, "")).toBe("11900");
|
|
191
|
+
});
|
|
192
|
+
|
|
186
193
|
test("money → unerwarteter Value-Shape fällt auf String zurück", () => {
|
|
187
194
|
expect(defaultCellRender(45000, "money")).toBe("45000");
|
|
188
195
|
});
|
|
@@ -104,6 +104,23 @@ describe("MoneyInput — Render (Tier 2)", () => {
|
|
|
104
104
|
expect(value).not.toContain("€");
|
|
105
105
|
});
|
|
106
106
|
|
|
107
|
+
test("Focus selektiert den gesamten Draft (bewusstes Select-all-on-focus) — sonst hängt ein danach eingefügter Wert an statt sie zu ersetzen (#1856)", () => {
|
|
108
|
+
render(
|
|
109
|
+
<MoneyInput
|
|
110
|
+
id="amt"
|
|
111
|
+
name="amt"
|
|
112
|
+
value={1000}
|
|
113
|
+
onChange={() => {}}
|
|
114
|
+
currency="EUR"
|
|
115
|
+
locale="de-DE"
|
|
116
|
+
/>,
|
|
117
|
+
);
|
|
118
|
+
fireEvent.focus(inputEl());
|
|
119
|
+
const input = inputEl();
|
|
120
|
+
expect(input.selectionStart).toBe(0);
|
|
121
|
+
expect(input.selectionEnd).toBe(input.value.length);
|
|
122
|
+
});
|
|
123
|
+
|
|
107
124
|
test("Blur mit neuem Wert ruft onChange mit Minor-Units (Cents)", () => {
|
|
108
125
|
const onChange = mock((_v: number | undefined) => {});
|
|
109
126
|
render(
|
package/src/primitives/index.tsx
CHANGED
|
@@ -180,10 +180,12 @@ function DefaultBanner({
|
|
|
180
180
|
actions,
|
|
181
181
|
padded,
|
|
182
182
|
testId,
|
|
183
|
+
id,
|
|
183
184
|
}: BannerProps): ReactNode {
|
|
184
185
|
const isError = variant === "error";
|
|
185
186
|
const banner = (
|
|
186
187
|
<div
|
|
188
|
+
id={id}
|
|
187
189
|
data-testid={testId}
|
|
188
190
|
role={isError ? "alert" : undefined}
|
|
189
191
|
data-variant={variant}
|
|
@@ -1348,7 +1350,14 @@ export function isComponentRendererRef(renderer: unknown): { readonly name: stri
|
|
|
1348
1350
|
// applyFormatSpec re-exported from headless (platform-agnostic).
|
|
1349
1351
|
export { applyFormatSpec };
|
|
1350
1352
|
|
|
1351
|
-
|
|
1353
|
+
type MoneyCellValue = {
|
|
1354
|
+
amount: number;
|
|
1355
|
+
currency: string;
|
|
1356
|
+
/** Exact integer cents from rehydrateMoney (fw#1830); preferred for display. */
|
|
1357
|
+
amountMinor?: number;
|
|
1358
|
+
};
|
|
1359
|
+
|
|
1360
|
+
function isMoneyValue(value: unknown): value is MoneyCellValue {
|
|
1352
1361
|
return (
|
|
1353
1362
|
typeof value === "object" &&
|
|
1354
1363
|
value !== null &&
|
|
@@ -1383,7 +1392,12 @@ export function defaultCellRender(
|
|
|
1383
1392
|
// falls back to guessLocale() (navigator.language), so the same amount
|
|
1384
1393
|
// can render differently in a table vs. a form on the same screen.
|
|
1385
1394
|
// Mid-term: pass the app locale down here too, analogous to render-field.
|
|
1386
|
-
|
|
1395
|
+
//
|
|
1396
|
+
// formatMoney expects minor units. rehydrateMoney returns amount in MAJOR
|
|
1397
|
+
// units plus amountMinor (cents). Prefer amountMinor when present; legacy
|
|
1398
|
+
// shapes without it still carry cents in `amount`.
|
|
1399
|
+
const minor = typeof value.amountMinor === "number" ? value.amountMinor : value.amount;
|
|
1400
|
+
return formatMoney(minor, value.currency);
|
|
1387
1401
|
}
|
|
1388
1402
|
if (type === "select") {
|
|
1389
1403
|
const raw = String(value);
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
// Cent-genaue Steps will tippt halt im Focus-Modus.
|
|
16
16
|
|
|
17
17
|
import { Minus, Plus } from "lucide-react";
|
|
18
|
-
import { type
|
|
18
|
+
import { type ReactNode, useEffect, useRef, useState } from "react";
|
|
19
19
|
import { cn } from "../lib/cn";
|
|
20
20
|
|
|
21
21
|
export type MoneyInputProps = {
|
|
@@ -62,6 +62,7 @@ export function MoneyInput({
|
|
|
62
62
|
// Raw-Edit-Buffer während Focus. Sonst würde jeder Tipp-Step durch
|
|
63
63
|
// Math.round → format-Roundtrip jagen und der Cursor würde springen.
|
|
64
64
|
const [draft, setDraft] = useState<string>("");
|
|
65
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
65
66
|
|
|
66
67
|
const major = value === "" ? null : value / factor;
|
|
67
68
|
const formatted = value === "" ? "" : formatMoney(value, currency, resolvedLocale);
|
|
@@ -79,11 +80,27 @@ export function MoneyInput({
|
|
|
79
80
|
|
|
80
81
|
const editable = focused ? draft : toEditable(major);
|
|
81
82
|
|
|
82
|
-
const handleFocus = (
|
|
83
|
+
const handleFocus = (): void => {
|
|
83
84
|
setDraft(toEditable(major));
|
|
84
85
|
setFocused(true);
|
|
85
86
|
};
|
|
86
87
|
|
|
88
|
+
// Select-all-on-focus: deliberate, not just a Playwright accommodation.
|
|
89
|
+
// Focus always swaps the displayed value from the formatted string
|
|
90
|
+
// ("1.234,56 €") to the raw editable one ("1234,56"). Measured in a real
|
|
91
|
+
// browser: on this kind of value swap, the browser collapses the cursor
|
|
92
|
+
// to the *end* of the new value rather than preserving position — so
|
|
93
|
+
// without an explicit re-select, typing right after focus appends
|
|
94
|
+
// instead of replacing. That's what silently corrupted values set via
|
|
95
|
+
// Playwright's `.fill()` (framework#1856). It also matches standard
|
|
96
|
+
// money-input UX (immediate overtype on click), and sidesteps mapping a
|
|
97
|
+
// click position from the formatted view to the editable one, which has
|
|
98
|
+
// no well-defined equivalent once separators and the currency symbol
|
|
99
|
+
// are stripped.
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
if (focused) inputRef.current?.select();
|
|
102
|
+
}, [focused]);
|
|
103
|
+
|
|
87
104
|
const handleBlur = (): void => {
|
|
88
105
|
setFocused(false);
|
|
89
106
|
if (draft.trim() === "") {
|
|
@@ -103,6 +120,7 @@ export function MoneyInput({
|
|
|
103
120
|
return (
|
|
104
121
|
<div className="relative w-full">
|
|
105
122
|
<input
|
|
123
|
+
ref={inputRef}
|
|
106
124
|
id={id}
|
|
107
125
|
name={name}
|
|
108
126
|
type="text"
|