@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.138.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.138.0",
20
- "@cosmicdrift/kumiko-headless": "0.138.0",
21
- "@cosmicdrift/kumiko-renderer": "0.138.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,
@@ -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
+ }
@@ -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,