@cosmicdrift/kumiko-renderer-web 0.138.0 → 0.139.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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-renderer-web",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.139.0",
|
|
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.139.0",
|
|
20
|
+
"@cosmicdrift/kumiko-headless": "0.139.0",
|
|
21
|
+
"@cosmicdrift/kumiko-renderer": "0.139.0",
|
|
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",
|
package/src/index.ts
CHANGED
|
@@ -159,10 +159,12 @@ export type {
|
|
|
159
159
|
ComparisonMetric,
|
|
160
160
|
DateFieldProps,
|
|
161
161
|
FeedRow,
|
|
162
|
+
FileFieldProps,
|
|
162
163
|
NumberFieldProps,
|
|
163
164
|
ProgressListRow,
|
|
164
165
|
QueryTableColumn,
|
|
165
166
|
QueryTableProps,
|
|
167
|
+
RangeFieldProps,
|
|
166
168
|
ResultColumn,
|
|
167
169
|
SelectFieldProps,
|
|
168
170
|
StatDelta,
|
|
@@ -182,6 +184,7 @@ export {
|
|
|
182
184
|
EmptyState,
|
|
183
185
|
ErrorState,
|
|
184
186
|
FeedList,
|
|
187
|
+
FileField,
|
|
185
188
|
LoadingState,
|
|
186
189
|
MiniStat,
|
|
187
190
|
ModeSwitch,
|
|
@@ -191,6 +194,7 @@ export {
|
|
|
191
194
|
ProgressBar,
|
|
192
195
|
ProgressList,
|
|
193
196
|
QueryTable,
|
|
197
|
+
RangeField,
|
|
194
198
|
ResultPanel,
|
|
195
199
|
ResultTable,
|
|
196
200
|
SectionCard,
|
package/src/primitives/index.tsx
CHANGED
|
@@ -111,12 +111,19 @@ const BUTTON_VARIANT = {
|
|
|
111
111
|
link: "link",
|
|
112
112
|
} as const;
|
|
113
113
|
|
|
114
|
+
const BUTTON_SIZE = {
|
|
115
|
+
sm: "sm",
|
|
116
|
+
md: "default",
|
|
117
|
+
icon: "icon",
|
|
118
|
+
} as const;
|
|
119
|
+
|
|
114
120
|
function DefaultButton({
|
|
115
121
|
type = "button",
|
|
116
122
|
onClick,
|
|
117
123
|
disabled,
|
|
118
124
|
loading,
|
|
119
125
|
variant = "primary",
|
|
126
|
+
size = "md",
|
|
120
127
|
children,
|
|
121
128
|
testId,
|
|
122
129
|
}: ButtonProps): ReactNode {
|
|
@@ -128,6 +135,7 @@ function DefaultButton({
|
|
|
128
135
|
data-testid={testId}
|
|
129
136
|
data-loading={loading === true ? "true" : undefined}
|
|
130
137
|
variant={BUTTON_VARIANT[variant]}
|
|
138
|
+
size={BUTTON_SIZE[size]}
|
|
131
139
|
// link-Variant rendert text-artig (Inline-Link im Fließtext/Banner),
|
|
132
140
|
// nicht als gepolsterte Buttonfläche.
|
|
133
141
|
className={variant === "link" ? "h-auto px-0 py-0" : undefined}
|
|
@@ -292,6 +300,19 @@ function DefaultInput(props: InputProps): ReactNode {
|
|
|
292
300
|
className="text-right tabular-nums"
|
|
293
301
|
/>
|
|
294
302
|
);
|
|
303
|
+
case "range":
|
|
304
|
+
return (
|
|
305
|
+
<input
|
|
306
|
+
type="range"
|
|
307
|
+
{...common}
|
|
308
|
+
min={props.min}
|
|
309
|
+
max={props.max}
|
|
310
|
+
step={props.step ?? 1}
|
|
311
|
+
value={props.value}
|
|
312
|
+
onChange={(e: ChangeEvent<HTMLInputElement>) => props.onChange(Number(e.target.value))}
|
|
313
|
+
className="w-full accent-primary"
|
|
314
|
+
/>
|
|
315
|
+
);
|
|
295
316
|
case "boolean":
|
|
296
317
|
return (
|
|
297
318
|
<Checkbox
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { describe, expect, mock, test } from "bun:test";
|
|
2
|
+
import { usePrimitives } from "@cosmicdrift/kumiko-renderer";
|
|
3
|
+
import type { ReactNode } from "react";
|
|
2
4
|
import { act, fireEvent, render, renderHook, screen } from "../../__tests__/test-utils";
|
|
3
5
|
import { DetailList } from "../detail-list";
|
|
4
6
|
import {
|
|
5
7
|
BooleanField,
|
|
6
8
|
DateField,
|
|
9
|
+
FileField,
|
|
7
10
|
MoneyField,
|
|
8
11
|
NumberField,
|
|
9
12
|
PercentField,
|
|
13
|
+
RangeField,
|
|
10
14
|
SelectField,
|
|
11
15
|
TextareaField,
|
|
12
16
|
TextField,
|
|
@@ -185,6 +189,51 @@ describe("ResultPanel footer", () => {
|
|
|
185
189
|
});
|
|
186
190
|
});
|
|
187
191
|
|
|
192
|
+
describe("RangeField", () => {
|
|
193
|
+
test("meldet den Slider-Wert als Zahl", () => {
|
|
194
|
+
const onChange = mock();
|
|
195
|
+
render(
|
|
196
|
+
<RangeField
|
|
197
|
+
label="Abruf"
|
|
198
|
+
id="r"
|
|
199
|
+
name="r"
|
|
200
|
+
value={20}
|
|
201
|
+
min={0}
|
|
202
|
+
max={100}
|
|
203
|
+
step={5}
|
|
204
|
+
onChange={onChange}
|
|
205
|
+
/>,
|
|
206
|
+
);
|
|
207
|
+
expect(screen.getByText("Abruf")).toBeTruthy();
|
|
208
|
+
fireEvent.change(screen.getByRole("slider"), { target: { value: "45" } });
|
|
209
|
+
expect(onChange).toHaveBeenCalledWith(45);
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
describe("FileField", () => {
|
|
214
|
+
test("rendert Label", () => {
|
|
215
|
+
render(
|
|
216
|
+
<FileField label="Bild" id="f" name="f" value={null} onChange={() => {}} variant="image" />,
|
|
217
|
+
);
|
|
218
|
+
expect(screen.getByText("Bild")).toBeTruthy();
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
describe("Button size", () => {
|
|
223
|
+
function SizedButton({ size }: { readonly size: "sm" | "md" | "icon" }): ReactNode {
|
|
224
|
+
const { Button } = usePrimitives();
|
|
225
|
+
return (
|
|
226
|
+
<Button size={size} onClick={() => {}}>
|
|
227
|
+
X
|
|
228
|
+
</Button>
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
test("size=sm rendert die kompakte shadcn-Größe (h-8)", () => {
|
|
232
|
+
render(<SizedButton size="sm" />);
|
|
233
|
+
expect(screen.getByRole("button", { name: "X" }).className).toContain("h-8");
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
188
237
|
describe("ComparisonTable", () => {
|
|
189
238
|
test("transponierte Matrix mit Best-Highlight je Kennzahl", () => {
|
|
190
239
|
const cols = [
|
|
@@ -241,3 +241,83 @@ export function TextareaField({
|
|
|
241
241
|
</Field>
|
|
242
242
|
);
|
|
243
243
|
}
|
|
244
|
+
|
|
245
|
+
export interface RangeFieldProps extends FieldBase {
|
|
246
|
+
readonly value: number;
|
|
247
|
+
readonly onChange: (v: number) => void;
|
|
248
|
+
readonly min: number;
|
|
249
|
+
readonly max: number;
|
|
250
|
+
readonly step?: number;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Schieberegler = Field + Input(kind:"range"). */
|
|
254
|
+
export function RangeField({
|
|
255
|
+
label,
|
|
256
|
+
id,
|
|
257
|
+
name,
|
|
258
|
+
value,
|
|
259
|
+
onChange,
|
|
260
|
+
min,
|
|
261
|
+
max,
|
|
262
|
+
step,
|
|
263
|
+
required,
|
|
264
|
+
disabled,
|
|
265
|
+
testId,
|
|
266
|
+
}: RangeFieldProps): ReactNode {
|
|
267
|
+
const { Field, Input } = usePrimitives();
|
|
268
|
+
return (
|
|
269
|
+
<Field id={id} label={label} required={required} testId={testId}>
|
|
270
|
+
<Input
|
|
271
|
+
kind="range"
|
|
272
|
+
id={id}
|
|
273
|
+
name={name}
|
|
274
|
+
value={value}
|
|
275
|
+
onChange={onChange}
|
|
276
|
+
min={min}
|
|
277
|
+
max={max}
|
|
278
|
+
required={required}
|
|
279
|
+
disabled={disabled}
|
|
280
|
+
{...(step !== undefined && { step })}
|
|
281
|
+
/>
|
|
282
|
+
</Field>
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export interface FileFieldProps extends FieldBase {
|
|
287
|
+
/** FileRef-UUID der gespeicherten Datei, oder null. */
|
|
288
|
+
readonly value: string | null;
|
|
289
|
+
readonly onChange: (fileId: string | null) => void;
|
|
290
|
+
readonly accept?: readonly string[];
|
|
291
|
+
/** "image" zeigt eine Vorschau, "file" nur den Dateinamen. Default "file". */
|
|
292
|
+
readonly variant?: "file" | "image";
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** Datei-Upload = Field + Input(kind:"file"|"image") — FileRef-basiert. */
|
|
296
|
+
export function FileField({
|
|
297
|
+
label,
|
|
298
|
+
id,
|
|
299
|
+
name,
|
|
300
|
+
value,
|
|
301
|
+
onChange,
|
|
302
|
+
accept,
|
|
303
|
+
variant = "file",
|
|
304
|
+
required,
|
|
305
|
+
disabled,
|
|
306
|
+
testId,
|
|
307
|
+
}: FileFieldProps): ReactNode {
|
|
308
|
+
const { Field, Input } = usePrimitives();
|
|
309
|
+
return (
|
|
310
|
+
<Field id={id} label={label} required={required} testId={testId}>
|
|
311
|
+
<Input
|
|
312
|
+
kind={variant}
|
|
313
|
+
id={id}
|
|
314
|
+
name={name}
|
|
315
|
+
value={value}
|
|
316
|
+
onChange={onChange}
|
|
317
|
+
required={required}
|
|
318
|
+
disabled={disabled}
|
|
319
|
+
{...(accept !== undefined && { accept })}
|
|
320
|
+
/>
|
|
321
|
+
</Field>
|
|
322
|
+
);
|
|
323
|
+
}
|
package/src/widgets/index.ts
CHANGED
|
@@ -18,10 +18,14 @@ export {
|
|
|
18
18
|
type BooleanFieldProps,
|
|
19
19
|
DateField,
|
|
20
20
|
type DateFieldProps,
|
|
21
|
+
FileField,
|
|
22
|
+
type FileFieldProps,
|
|
21
23
|
MoneyField,
|
|
22
24
|
NumberField,
|
|
23
25
|
type NumberFieldProps,
|
|
24
26
|
PercentField,
|
|
27
|
+
RangeField,
|
|
28
|
+
type RangeFieldProps,
|
|
25
29
|
SelectField,
|
|
26
30
|
type SelectFieldProps,
|
|
27
31
|
TextareaField,
|