@cosmicdrift/kumiko-renderer-web 0.191.0 → 0.192.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.191.0",
3
+ "version": "0.192.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.191.0",
20
- "@cosmicdrift/kumiko-headless": "0.191.0",
21
- "@cosmicdrift/kumiko-renderer": "0.191.0",
19
+ "@cosmicdrift/kumiko-dispatcher-live": "0.192.0",
20
+ "@cosmicdrift/kumiko-headless": "0.192.0",
21
+ "@cosmicdrift/kumiko-renderer": "0.192.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",
@@ -0,0 +1,25 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { render } from "@testing-library/react";
3
+ import { FileUploadInput } from "../file-upload";
4
+
5
+ const noop = () => {};
6
+
7
+ // alt="" makes the <img> presentational (no accessible role), so
8
+ // getByRole("img") wouldn't find it — query the DOM directly instead.
9
+ describe("FileUploadInput — image variant preview src (#1950)", () => {
10
+ test("with imageVariant set, the <img> src requests the named variant", () => {
11
+ const { container } = render(
12
+ <FileUploadInput kind="image" id="avatar" value="abc" onChange={noop} imageVariant="thumb" />,
13
+ );
14
+ expect(container.querySelector("img")?.getAttribute("src")).toBe(
15
+ "/api/files/abc/variant/thumb",
16
+ );
17
+ });
18
+
19
+ test("without imageVariant, the <img> src loads the original", () => {
20
+ const { container } = render(
21
+ <FileUploadInput kind="image" id="avatar" value="abc" onChange={noop} />,
22
+ );
23
+ expect(container.querySelector("img")?.getAttribute("src")).toBe("/api/files/abc");
24
+ });
25
+ });
@@ -1,7 +1,7 @@
1
- // File/Image-Upload-Widget für die Auto-Edit-Form. Lädt eine Datei per
2
- // multipart-POST an /api/files (vom Stack gemountet wenn ein storageProvider
3
- // gesetzt ist), bekommt die FileRef-UUID zurück und gibt sie als Field-Wert
4
- // hoch. Image → runde Avatar-Preview (GET /api/files/:id), file → Dateiname.
1
+ // File/image upload widget for the auto-edit form. Uploads via multipart POST
2
+ // to /api/files (mounted by the stack when a storageProvider is set), gets the
3
+ // FileRef UUID back and lifts it up as the field value. Image → round avatar
4
+ // preview (GET /api/files/:id, or the declared variant), file → file name.
5
5
 
6
6
  import { CSRF_HEADER_NAME, readCsrfToken } from "@cosmicdrift/kumiko-dispatcher-live";
7
7
  import { ImageIcon, Loader2, Upload } from "lucide-react";
@@ -17,6 +17,7 @@ export type FileUploadInputProps = {
17
17
  readonly disabled?: boolean;
18
18
  readonly entityType?: string;
19
19
  readonly fieldName?: string;
20
+ readonly imageVariant?: string;
20
21
  };
21
22
 
22
23
  // "jpg" → ".jpg", "image/png" bleibt. Leere Liste → kein accept-Attribut.
@@ -34,6 +35,7 @@ export function FileUploadInput({
34
35
  disabled,
35
36
  entityType,
36
37
  fieldName,
38
+ imageVariant,
37
39
  }: FileUploadInputProps): ReactNode {
38
40
  const inputRef = useRef<HTMLInputElement>(null);
39
41
  const [uploading, setUploading] = useState(false);
@@ -79,7 +81,11 @@ export function FileUploadInput({
79
81
  {kind === "image" &&
80
82
  (value !== null ? (
81
83
  <img
82
- src={`/api/files/${value}`}
84
+ src={
85
+ imageVariant !== undefined
86
+ ? `/api/files/${value}/variant/${imageVariant}`
87
+ : `/api/files/${value}`
88
+ }
83
89
  alt=""
84
90
  className="size-16 rounded-full border object-cover"
85
91
  />
@@ -433,6 +433,7 @@ function DefaultInput(props: InputProps): ReactNode {
433
433
  {...(props.disabled !== undefined && { disabled: props.disabled })}
434
434
  {...(props.entityType !== undefined && { entityType: props.entityType })}
435
435
  {...(props.fieldName !== undefined && { fieldName: props.fieldName })}
436
+ {...(props.imageVariant !== undefined && { imageVariant: props.imageVariant })}
436
437
  />
437
438
  );
438
439
  case "date":
@@ -1,7 +1,14 @@
1
1
  import type { ReactNode } from "react";
2
2
  import { cn } from "../lib/cn";
3
3
 
4
- /** Fortschrittsbalken, `value` 0..1 (wird geclampt). */
4
+ /**
5
+ * Progress bar, `value` 0..1 (clamped).
6
+ *
7
+ * The track lives in its own inner element so a parent that pads or
8
+ * stretches its direct children (e.g. RenderEdit's wizard chrome) never
9
+ * touches the track's own `h-2` — a caller's `className` reaches the
10
+ * outer wrapper, not the height-bearing track.
11
+ */
5
12
  export function ProgressBar({
6
13
  value,
7
14
  className,
@@ -13,18 +20,20 @@ export function ProgressBar({
13
20
  }): ReactNode {
14
21
  const pct = Math.max(0, Math.min(1, value));
15
22
  return (
16
- <div
17
- data-testid={testId}
18
- role="progressbar"
19
- aria-valuenow={Math.round(pct * 100)}
20
- aria-valuemin={0}
21
- aria-valuemax={100}
22
- className={cn("relative h-2 w-full overflow-hidden rounded-full bg-muted", className)}
23
- >
23
+ <div className={cn("w-full", className)}>
24
24
  <div
25
- className="absolute inset-y-0 left-0 rounded-full bg-primary"
26
- style={{ width: `${pct * 100}%` }}
27
- />
25
+ data-testid={testId}
26
+ role="progressbar"
27
+ aria-valuenow={Math.round(pct * 100)}
28
+ aria-valuemin={0}
29
+ aria-valuemax={100}
30
+ className="relative h-2 w-full overflow-hidden rounded-full bg-muted"
31
+ >
32
+ <div
33
+ className="absolute inset-y-0 left-0 rounded-full bg-primary"
34
+ style={{ width: `${pct * 100}%` }}
35
+ />
36
+ </div>
28
37
  </div>
29
38
  );
30
39
  }