@cosmicdrift/kumiko-renderer-web 0.193.1 → 0.194.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.
Files changed (44) hide show
  1. package/package.json +4 -4
  2. package/src/__tests__/create-app.test.tsx +1 -1
  3. package/src/__tests__/entity-edit-redirect.test.tsx +111 -0
  4. package/src/__tests__/field-hide-label.test.tsx +166 -1
  5. package/src/__tests__/kumiko-screen.test.tsx +59 -0
  6. package/src/__tests__/nav-tree.test.tsx +43 -11
  7. package/src/__tests__/primitives.test.tsx +33 -2
  8. package/src/__tests__/render-edit.test.tsx +344 -9
  9. package/src/__tests__/test-utils.tsx +20 -0
  10. package/src/__tests__/wizard-form-validation.test.tsx +129 -1
  11. package/src/app/__tests__/merge-content-editors.test.ts +2 -2
  12. package/src/app/__tests__/plain-content-editor.test.tsx +4 -0
  13. package/src/app/__tests__/rich-content-editor.test.tsx +5 -2
  14. package/src/app/__tests__/tiptap-editor.test.tsx +50 -0
  15. package/src/app/client-plugin.tsx +3 -3
  16. package/src/app/create-app.tsx +40 -49
  17. package/src/app/tiptap-editor.tsx +10 -0
  18. package/src/layout/__tests__/workspace-switcher.test.tsx +40 -0
  19. package/src/layout/sidebar-panel.tsx +1 -4
  20. package/src/layout/workspace-switcher.tsx +20 -13
  21. package/src/lib/clamp.ts +3 -0
  22. package/src/primitives/__tests__/data-table-logic.test.ts +15 -3
  23. package/src/primitives/__tests__/date-field.test.tsx +37 -0
  24. package/src/primitives/__tests__/date-parse.test.ts +14 -2
  25. package/src/primitives/__tests__/embedded-list-input.test.tsx +36 -2
  26. package/src/primitives/__tests__/file-upload.test.tsx +24 -0
  27. package/src/primitives/date-field.tsx +8 -0
  28. package/src/primitives/date-parse.ts +6 -1
  29. package/src/primitives/dropdown-menu.tsx +23 -0
  30. package/src/primitives/embedded-list-input.tsx +14 -5
  31. package/src/primitives/file-upload.tsx +3 -0
  32. package/src/primitives/index.tsx +114 -120
  33. package/src/primitives/money-input.tsx +4 -7
  34. package/src/ui/sheet.tsx +3 -1
  35. package/src/widgets/__tests__/drawer.test.tsx +293 -0
  36. package/src/widgets/__tests__/infinity-list.test.tsx +38 -0
  37. package/src/widgets/__tests__/upload-zone.test.tsx +90 -1
  38. package/src/widgets/__tests__/widgets.test.tsx +25 -0
  39. package/src/widgets/ai-text-field.tsx +3 -1
  40. package/src/widgets/drawer.tsx +54 -29
  41. package/src/widgets/infinity-list.tsx +18 -5
  42. package/src/widgets/progress-bar.tsx +3 -1
  43. package/src/widgets/step-bar.tsx +1 -1
  44. package/src/widgets/upload-zone.tsx +40 -2
@@ -18,7 +18,9 @@ export function ProgressBar({
18
18
  readonly className?: string;
19
19
  readonly testId?: string;
20
20
  }): ReactNode {
21
- const pct = Math.max(0, Math.min(1, value));
21
+ // `value` is typically `done / total`; total:0 produces NaN, which would
22
+ // otherwise leak into `width: "NaN%"` and an invalid aria-valuenow.
23
+ const pct = Number.isFinite(value) ? Math.max(0, Math.min(1, value)) : 0;
22
24
  return (
23
25
  <div className={cn("w-full", className)}>
24
26
  <div
@@ -49,7 +49,7 @@ export function StepBar({
49
49
  {isDone ? (
50
50
  <>
51
51
  <Check aria-hidden="true" className="size-3.5" />
52
- <span className="sr-only">{t("kumiko.widget.stepBar.done")}</span>
52
+ <span className="sr-only">{t("kumiko.widget.step-bar.done")}</span>
53
53
  </>
54
54
  ) : (
55
55
  <span className="text-xs font-semibold">{i + 1}</span>
@@ -45,6 +45,32 @@ function toAcceptAttr(accept?: readonly string[]): string | undefined {
45
45
  return accept.map((a) => (a.startsWith(".") || a.includes("/") ? a : `.${a}`)).join(",");
46
46
  }
47
47
 
48
+ // `accept` on the native <input> only filters the file-picker dialog — a
49
+ // drag&drop drop is never routed through it, so any file type lands in
50
+ // `onUpload` regardless of what `accept` promises. Same matching rules as
51
+ // the native attribute: MIME type (with an optional "type/*" wildcard) or
52
+ // file extension.
53
+ function matchesAccept(file: File, accept?: readonly string[]): boolean {
54
+ if (accept === undefined || accept.length === 0) return true;
55
+ const name = file.name.toLowerCase();
56
+ const type = file.type.toLowerCase();
57
+ return accept.some((raw) => {
58
+ const a = raw.toLowerCase();
59
+ if (a.includes("/")) return a.endsWith("/*") ? type.startsWith(a.slice(0, -1)) : type === a;
60
+ return name.endsWith(a.startsWith(".") ? a : `.${a}`);
61
+ });
62
+ }
63
+
64
+ function partitionByAccept(
65
+ files: readonly File[],
66
+ accept: readonly string[] | undefined,
67
+ ): readonly [accepted: readonly File[], rejected: readonly File[]] {
68
+ const accepted: File[] = [];
69
+ const rejected: File[] = [];
70
+ for (const file of files) (matchesAccept(file, accept) ? accepted : rejected).push(file);
71
+ return [accepted, rejected];
72
+ }
73
+
48
74
  /** Drop zone + multi-file picker with a per-file status row (uploading/done/
49
75
  * error). For screens that accept several files and upload them
50
76
  * independently — unlike `FileField` (single file, FileRef value inside a
@@ -72,7 +98,7 @@ export function UploadZone({
72
98
  await onUpload(await resizeImageBeforeUpload(file));
73
99
  setRows((prev) => prev.map((row) => (row.id === rowId ? { ...row, status: "done" } : row)));
74
100
  } catch (cause) {
75
- const message = cause instanceof Error ? cause.message : "upload_failed";
101
+ const message = cause instanceof Error ? cause.message : t("kumiko.widget.upload.error");
76
102
  setRows((prev) =>
77
103
  prev.map((row) => (row.id === rowId ? { ...row, status: "error", error: message } : row)),
78
104
  );
@@ -82,7 +108,19 @@ export function UploadZone({
82
108
  async function uploadFiles(files: FileList | null): Promise<void> {
83
109
  if (files === null || files.length === 0) return;
84
110
  const picked = multiple ? Array.from(files) : files[0] !== undefined ? [files[0]] : [];
85
- await Promise.all(picked.map((file) => uploadOne(file)));
111
+ const [accepted, rejected] = partitionByAccept(picked, accept);
112
+ for (const file of rejected) {
113
+ setRows((prev) => [
114
+ ...prev,
115
+ {
116
+ id: crypto.randomUUID(),
117
+ fileName: file.name,
118
+ status: "error",
119
+ error: t("kumiko.widget.upload.rejected-type"),
120
+ },
121
+ ]);
122
+ }
123
+ await Promise.all(accepted.map((file) => uploadOne(file)));
86
124
  // Reset so re-picking the SAME file still fires change — the browser
87
125
  // suppresses the event otherwise.
88
126
  if (inputRef.current) inputRef.current.value = "";