@heyramzi/ui 3.7.1 → 3.9.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 (58) hide show
  1. package/COMPONENTS.md +1 -0
  2. package/dist/charts/motion.js +2 -7
  3. package/dist/components/file-drop-zone/files.js +111 -0
  4. package/dist/components/file-drop-zone/index.js +2 -0
  5. package/package.json +1 -1
  6. package/scripts/svelte-dts.mjs +2 -0
  7. package/scripts/sync-shadcn.mjs +0 -1
  8. package/src/charts/motion.ts +2 -7
  9. package/src/components/file-drop-zone/file-drop-zone.svelte +348 -0
  10. package/src/components/file-drop-zone/file-drop-zone.svelte.d.ts +43 -0
  11. package/src/components/file-drop-zone/file-drop-zone.test.ts +148 -0
  12. package/src/components/file-drop-zone/files.test.ts +105 -0
  13. package/src/components/file-drop-zone/files.ts +144 -0
  14. package/src/components/file-drop-zone/index.ts +14 -0
  15. package/src/components/table-view/table-view.svelte +4 -1
  16. package/src/components/table-view/table-view.svelte.d.ts +2 -0
  17. package/src/shadcn/alert-dialog/alert-dialog-cancel.svelte +3 -0
  18. package/src/shadcn/calendar/calendar-month-select.svelte +2 -2
  19. package/src/shadcn/calendar/calendar-year-select.svelte +2 -2
  20. package/src/shadcn/calendar/calendar.svelte +40 -30
  21. package/src/shadcn/calendar/calendar.svelte.d.ts +0 -1
  22. package/src/shadcn/command/command-dialog.svelte +4 -0
  23. package/src/shadcn/command/command-dialog.svelte.d.ts +2 -0
  24. package/src/shadcn/dialog/dialog-content.svelte +4 -1
  25. package/src/shadcn/dialog/dialog-content.svelte.d.ts +2 -0
  26. package/src/shadcn/dialog/dialog-footer.svelte +4 -1
  27. package/src/shadcn/dialog/dialog-footer.svelte.d.ts +2 -0
  28. package/src/shadcn/input/input.svelte +1 -1
  29. package/src/shadcn/input/input.svelte.d.ts +1 -1
  30. package/src/shadcn/input/secret-input.svelte +17 -3
  31. package/src/shadcn/input/secret-input.svelte.d.ts +6 -0
  32. package/src/shadcn/input-group/input-group-input.svelte.d.ts +1 -1
  33. package/src/shadcn/input-otp/input-otp-group.svelte +1 -0
  34. package/src/shadcn/input-otp/input-otp-slot.svelte +5 -1
  35. package/src/shadcn/input-otp/input-otp.svelte +2 -0
  36. package/src/shadcn/input-otp/input-otp.svelte.d.ts +1 -1
  37. package/src/shadcn/message-scroller/message-scroller-button.svelte +9 -3
  38. package/src/shadcn/message-scroller/message-scroller-button.svelte.d.ts +5 -0
  39. package/src/shadcn/pagination/pagination-next.svelte +6 -2
  40. package/src/shadcn/pagination/pagination-next.svelte.d.ts +5 -1
  41. package/src/shadcn/pagination/pagination-previous.svelte +6 -2
  42. package/src/shadcn/pagination/pagination-previous.svelte.d.ts +5 -1
  43. package/src/shadcn/sheet/close-label.test.svelte +20 -0
  44. package/src/shadcn/sheet/close-label.test.svelte.d.ts +6 -0
  45. package/src/shadcn/sheet/close-label.test.ts +44 -0
  46. package/src/shadcn/sheet/sheet-content.svelte +4 -1
  47. package/src/shadcn/sheet/sheet-content.svelte.d.ts +2 -0
  48. package/src/shadcn/sidebar/sidebar-input.svelte.d.ts +1 -1
  49. package/src/shadcn/sidebar/sidebar-rail.svelte +7 -3
  50. package/src/shadcn/sidebar/sidebar-rail.svelte.d.ts +5 -1
  51. package/src/shadcn/sidebar/sidebar-trigger.svelte +4 -1
  52. package/src/shadcn/sidebar/sidebar-trigger.svelte.d.ts +2 -0
  53. package/src/thread/index.ts +1 -0
  54. package/src/thread/thread-events.svelte +32 -3
  55. package/src/thread/types.ts +10 -0
  56. package/src/work/task-gantt.svelte +4 -1
  57. package/src/work/task-gantt.svelte.d.ts +2 -0
  58. package/scripts/lib/svelte-modules.d.mts +0 -4
@@ -0,0 +1,148 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from "vite-plus/test";
2
+ import { flushSync, mount, unmount } from "svelte";
3
+ import FileDropZone from "./file-drop-zone.svelte";
4
+
5
+ // jsdom has no DataTransfer and its input.files takes only a FileList, so the stub returns an
6
+ // array and each input records what the zone wrote.
7
+ class DataTransferStub {
8
+ readonly list: File[] = [];
9
+ readonly items = { add: (file: File) => this.list.push(file) };
10
+ get files() {
11
+ return this.list;
12
+ }
13
+ }
14
+
15
+ beforeEach(() => {
16
+ (globalThis as { DataTransfer?: unknown }).DataTransfer = DataTransferStub;
17
+ });
18
+
19
+ afterEach(() => {
20
+ delete (globalThis as { DataTransfer?: unknown }).DataTransfer;
21
+ document.body.innerHTML = "";
22
+ });
23
+
24
+ const pdf = (name: string, size = 10) =>
25
+ new File([new Uint8Array(size)], name, { type: "application/pdf", lastModified: 1 });
26
+
27
+ function render(props: Record<string, unknown> = {}) {
28
+ const target = document.createElement("div");
29
+ document.body.appendChild(target);
30
+ const app = mount(FileDropZone, { target, props: { name: "justificatif", ...props } });
31
+ flushSync();
32
+ const input = target.querySelector<HTMLInputElement>("input[type=file]");
33
+ if (!input) throw new Error("no file input");
34
+ let written: File[] = [];
35
+ Object.defineProperty(input, "files", {
36
+ configurable: true,
37
+ get: () => written,
38
+ set: (value: File[]) => {
39
+ written = [...value];
40
+ },
41
+ });
42
+ const fire = (type: string, files: File[]) => {
43
+ const event = new Event(type, { bubbles: true, cancelable: true });
44
+ Object.defineProperty(event, "dataTransfer", {
45
+ value: { types: ["Files"], files, dropEffect: "none" },
46
+ });
47
+ target.querySelector("[data-slot=file-drop-zone-target]")?.dispatchEvent(event);
48
+ flushSync();
49
+ };
50
+ const drop = (files: File[]) => {
51
+ fire("dragenter", files);
52
+ fire("drop", files);
53
+ };
54
+ const names = () =>
55
+ [...target.querySelectorAll("[data-file-name]")].map((row) =>
56
+ row.getAttribute("data-file-name"),
57
+ );
58
+ return {
59
+ target,
60
+ input,
61
+ drop,
62
+ fire,
63
+ names,
64
+ written: () => written.map((file) => file.name),
65
+ done: () => void unmount(app),
66
+ };
67
+ }
68
+
69
+ describe("FileDropZone", () => {
70
+ it("speaks French with no setup", () => {
71
+ const { target, done } = render({ accept: ".pdf", maxFileSize: 10 * 1024 * 1024 });
72
+ const text = target.textContent ?? "";
73
+ expect(text).toContain("Glissez votre fichier ici");
74
+ expect(text).toContain("ou cliquez pour le choisir");
75
+ expect(text).toContain("PDF, 10 Mo maximum");
76
+ done();
77
+ });
78
+
79
+ it("writes dropped files into the real input, so a form post carries them", () => {
80
+ const { input, drop, names, written, done } = render({ multiple: true });
81
+ drop([pdf("bail.pdf"), pdf("quittance.pdf")]);
82
+ expect(names()).toEqual(["bail.pdf", "quittance.pdf"]);
83
+ expect(written()).toEqual(["bail.pdf", "quittance.pdf"]);
84
+ expect(input.name).toBe("justificatif");
85
+ done();
86
+ });
87
+
88
+ it("shows the drag state while a file hovers", () => {
89
+ const { target, fire, done } = render();
90
+ fire("dragenter", [pdf("bail.pdf")]);
91
+ expect(target.textContent).toContain("Déposez votre fichier");
92
+ fire("dragleave", [pdf("bail.pdf")]);
93
+ expect(target.textContent).toContain("Glissez votre fichier ici");
94
+ done();
95
+ });
96
+
97
+ it("refuses the wrong format and a file too heavy, and says so", () => {
98
+ const rejected: string[] = [];
99
+ const { target, drop, names, done } = render({
100
+ accept: ".pdf",
101
+ maxFileSize: 100,
102
+ multiple: true,
103
+ onFileRejected: (list: { reason: string }[]) => rejected.push(...list.map((r) => r.reason)),
104
+ });
105
+ const photo = new File(["x"], "photo.png", { type: "image/png" });
106
+ drop([photo, pdf("gros.pdf", 500), pdf("ok.pdf")]);
107
+ expect(names()).toEqual(["ok.pdf"]);
108
+ expect(rejected).toEqual(["type", "size"]);
109
+ expect(target.textContent).toContain("photo.png : format non accepté");
110
+ expect(target.textContent).toContain("gros.pdf : fichier trop lourd");
111
+ done();
112
+ });
113
+
114
+ it("stops at maxFiles", () => {
115
+ const { target, drop, names, done } = render({ multiple: true, maxFiles: 2 });
116
+ drop([pdf("a.pdf"), pdf("b.pdf"), pdf("c.pdf")]);
117
+ expect(names()).toEqual(["a.pdf", "b.pdf"]);
118
+ expect(target.textContent).toContain("2 fichiers au maximum");
119
+ done();
120
+ });
121
+
122
+ it("removes a file from the list and from the input", () => {
123
+ const { target, drop, names, written, done } = render({ multiple: true });
124
+ drop([pdf("a.pdf"), pdf("b.pdf")]);
125
+ target.querySelector<HTMLButtonElement>('[aria-label="Retirer a.pdf"]')?.click();
126
+ flushSync();
127
+ expect(names()).toEqual(["b.pdf"]);
128
+ expect(written()).toEqual(["b.pdf"]);
129
+ done();
130
+ });
131
+
132
+ it("puts the one file in the zone's place, and brings the zone back on removal", () => {
133
+ const { target, drop, done } = render();
134
+ drop([pdf("bail.pdf")]);
135
+ expect(target.querySelector("[data-slot=file-drop-zone-target]")).toBeNull();
136
+ target.querySelector<HTMLButtonElement>('[aria-label="Retirer bail.pdf"]')?.click();
137
+ flushSync();
138
+ expect(target.querySelector("[data-slot=file-drop-zone-target]")).not.toBeNull();
139
+ done();
140
+ });
141
+
142
+ it("ignores drops while disabled", () => {
143
+ const { drop, names, done } = render({ disabled: true });
144
+ drop([pdf("bail.pdf")]);
145
+ expect(names()).toEqual([]);
146
+ done();
147
+ });
148
+ });
@@ -0,0 +1,105 @@
1
+ import { describe, expect, it } from "vite-plus/test";
2
+ import {
3
+ checkFiles,
4
+ describeAccept,
5
+ describeLimits,
6
+ fileKind,
7
+ formatSize,
8
+ matchesAccept,
9
+ removeFile,
10
+ } from "./files.js";
11
+
12
+ const file = (name: string, type: string, size = 10) =>
13
+ new File([new Uint8Array(size)], name, { type, lastModified: 1 });
14
+
15
+ describe("matchesAccept", () => {
16
+ it("takes extensions, exact types and groups, in any case", () => {
17
+ expect(matchesAccept(file("Bail.PDF", ""), ".pdf")).toBe(true);
18
+ expect(matchesAccept(file("a.bin", "application/pdf"), "application/pdf")).toBe(true);
19
+ expect(matchesAccept(file("photo.heic", "image/heic"), "image/*")).toBe(true);
20
+ expect(matchesAccept(file("releve.csv", "text/csv"), ".pdf,image/*")).toBe(false);
21
+ });
22
+
23
+ it("lets everything through without an accept string", () => {
24
+ expect(matchesAccept(file("x.zip", "application/zip"), undefined)).toBe(true);
25
+ expect(matchesAccept(file("x.zip", "application/zip"), " ")).toBe(true);
26
+ });
27
+ });
28
+
29
+ describe("checkFiles", () => {
30
+ it("turns away the wrong type before the size", () => {
31
+ const big = file("gros.csv", "text/csv", 5000);
32
+ const result = checkFiles([big], [], { accept: ".pdf", maxFileSize: 100 });
33
+ expect(result.rejected).toEqual([{ file: big, reason: "type" }]);
34
+ });
35
+
36
+ it("turns away a file over the size limit", () => {
37
+ const big = file("gros.pdf", "application/pdf", 5000);
38
+ const small = file("petit.pdf", "application/pdf", 50);
39
+ const result = checkFiles([big, small], [], {
40
+ accept: ".pdf",
41
+ maxFileSize: 100,
42
+ multiple: true,
43
+ });
44
+ expect(result.files).toEqual([small]);
45
+ expect(result.rejected).toEqual([{ file: big, reason: "size" }]);
46
+ });
47
+
48
+ it("stops at maxFiles, counting what's already there", () => {
49
+ const a = file("a.pdf", "application/pdf");
50
+ const b = file("b.pdf", "application/pdf");
51
+ const c = file("c.pdf", "application/pdf");
52
+ const result = checkFiles([b, c], [a], { multiple: true, maxFiles: 2 });
53
+ expect(result.files).toEqual([a, b]);
54
+ expect(result.accepted).toEqual([b]);
55
+ expect(result.rejected).toEqual([{ file: c, reason: "count" }]);
56
+ });
57
+
58
+ it("replaces the one file without multiple, and keeps it when the new one is refused", () => {
59
+ const a = file("a.pdf", "application/pdf");
60
+ const b = file("b.pdf", "application/pdf");
61
+ expect(checkFiles([b], [a], { accept: ".pdf" }).files).toEqual([b]);
62
+ expect(checkFiles([file("x.png", "image/png")], [a], { accept: ".pdf" }).files).toEqual([a]);
63
+ });
64
+
65
+ it("ignores the same file picked twice", () => {
66
+ const a = file("a.pdf", "application/pdf");
67
+ const again = file("a.pdf", "application/pdf");
68
+ const result = checkFiles([again], [a], { multiple: true });
69
+ expect(result.files).toEqual([a]);
70
+ expect(result.rejected).toEqual([]);
71
+ });
72
+ });
73
+
74
+ describe("removeFile", () => {
75
+ it("drops that one file and keeps the order", () => {
76
+ const a = file("a.pdf", "application/pdf");
77
+ const b = file("b.pdf", "application/pdf");
78
+ const c = file("c.pdf", "application/pdf");
79
+ expect(removeFile([a, b, c], b)).toEqual([a, c]);
80
+ });
81
+ });
82
+
83
+ describe("words", () => {
84
+ it("writes sizes the French way", () => {
85
+ expect(formatSize(820)).toBe("820 o");
86
+ expect(formatSize(12 * 1024)).toBe("12 Ko");
87
+ expect(formatSize(1.4 * 1024 * 1024)).toBe("1,4 Mo");
88
+ expect(formatSize(10 * 1024 * 1024)).toBe("10 Mo");
89
+ });
90
+
91
+ it("names the accepted types once each", () => {
92
+ expect(describeAccept(".pdf,application/pdf")).toBe("PDF");
93
+ expect(describeAccept("image/*,.csv")).toBe("Images, CSV");
94
+ expect(describeLimits(".pdf", 10 * 1024 * 1024)).toBe("PDF, 10 Mo maximum");
95
+ expect(describeLimits(undefined, undefined)).toBe("");
96
+ });
97
+
98
+ it("picks an icon kind from the type or the name", () => {
99
+ expect(fileKind(file("a.pdf", ""))).toBe("pdf");
100
+ expect(fileKind(file("a.jpg", "image/jpeg"))).toBe("image");
101
+ expect(fileKind(file("a.csv", ""))).toBe("csv");
102
+ expect(fileKind(file("a.xlsx", ""))).toBe("sheet");
103
+ expect(fileKind(file("a.txt", "text/plain"))).toBe("other");
104
+ });
105
+ });
@@ -0,0 +1,144 @@
1
+ /** Why a file didn't make it into the list. */
2
+ export type RejectReason = "type" | "size" | "count";
3
+
4
+ export interface Rejection {
5
+ file: File;
6
+ reason: RejectReason;
7
+ }
8
+
9
+ export interface CheckOptions {
10
+ /** The input's `accept` string: extensions (`.pdf`), MIME types (`application/pdf`) or groups (`image/*`). */
11
+ accept?: string;
12
+ /** Largest file, in bytes. */
13
+ maxFileSize?: number;
14
+ /** Most files the list holds. */
15
+ maxFiles?: number;
16
+ /** Without it, a new file replaces the one already there. */
17
+ multiple?: boolean;
18
+ }
19
+
20
+ export type FileKind = "pdf" | "image" | "csv" | "sheet" | "other";
21
+
22
+ const tokens = (accept: string | undefined) =>
23
+ (accept ?? "")
24
+ .split(",")
25
+ .map((token) => token.trim().toLowerCase())
26
+ .filter(Boolean);
27
+
28
+ /** Whether a file passes the `accept` string, the way the browser's picker reads it. */
29
+ export function matchesAccept(file: File, accept: string | undefined): boolean {
30
+ const list = tokens(accept);
31
+ if (list.length === 0) return true;
32
+ const name = file.name.toLowerCase();
33
+ const type = file.type.toLowerCase();
34
+ return list.some((token) => {
35
+ if (token.startsWith(".")) return name.endsWith(token);
36
+ if (token.endsWith("/*")) return type.startsWith(token.slice(0, -1));
37
+ return type === token;
38
+ });
39
+ }
40
+
41
+ // The same file picked twice is one file: name, size and date match.
42
+ const same = (a: File, b: File) =>
43
+ a.name === b.name && a.size === b.size && a.lastModified === b.lastModified;
44
+
45
+ /**
46
+ * Sorts a drop, paste or pick into the next list and the refusals.
47
+ * A file is refused for its first fault: type, then size, then room.
48
+ */
49
+ export function checkFiles(
50
+ incoming: readonly File[],
51
+ current: readonly File[],
52
+ { accept, maxFileSize, maxFiles, multiple = false }: CheckOptions,
53
+ ): { files: File[]; accepted: File[]; rejected: Rejection[] } {
54
+ const room = multiple ? (maxFiles ?? Infinity) : 1;
55
+ const kept = multiple ? [...current] : [];
56
+ const accepted: File[] = [];
57
+ const rejected: Rejection[] = [];
58
+ for (const file of incoming) {
59
+ if (!matchesAccept(file, accept)) rejected.push({ file, reason: "type" });
60
+ else if (maxFileSize !== undefined && file.size > maxFileSize)
61
+ rejected.push({ file, reason: "size" });
62
+ else if (kept.some((other) => same(other, file))) continue;
63
+ else if (kept.length >= room) rejected.push({ file, reason: "count" });
64
+ else {
65
+ kept.push(file);
66
+ accepted.push(file);
67
+ }
68
+ }
69
+ return { files: accepted.length > 0 || multiple ? kept : [...current], accepted, rejected };
70
+ }
71
+
72
+ /** Drops one file from the list. */
73
+ export const removeFile = (files: readonly File[], file: File): File[] =>
74
+ files.filter((other) => other !== file);
75
+
76
+ const UNITS = ["o", "Ko", "Mo", "Go"] as const;
77
+
78
+ /** A byte count the way a French reader says it: "820 o", "12 Ko", "1,4 Mo". */
79
+ export function formatSize(bytes: number, locale = "fr-FR"): string {
80
+ let value = bytes;
81
+ let unit = 0;
82
+ while (value >= 1024 && unit < UNITS.length - 1) {
83
+ value /= 1024;
84
+ unit += 1;
85
+ }
86
+ const digits = unit === 0 || value >= 10 ? 0 : 1;
87
+ const number = new Intl.NumberFormat(locale, { maximumFractionDigits: digits }).format(value);
88
+ return `${number} ${UNITS[unit]}`;
89
+ }
90
+
91
+ const GROUPS: Record<string, string> = {
92
+ "image/": "Images",
93
+ "audio/": "Audio",
94
+ "video/": "Vidéos",
95
+ "text/": "Texte",
96
+ };
97
+
98
+ const MIME_NAMES: Record<string, string> = {
99
+ "application/pdf": "PDF",
100
+ "text/csv": "CSV",
101
+ "application/vnd.ms-excel": "XLS",
102
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "XLSX",
103
+ "application/msword": "DOC",
104
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document": "DOCX",
105
+ "application/zip": "ZIP",
106
+ "application/json": "JSON",
107
+ };
108
+
109
+ /** The `accept` string as a reader names it: ".pdf,application/pdf" is "PDF", "image/*" is "Images". */
110
+ export function describeAccept(accept: string | undefined): string {
111
+ const names: string[] = [];
112
+ for (const token of tokens(accept)) {
113
+ const name = token.startsWith(".")
114
+ ? token.slice(1).toUpperCase()
115
+ : token.endsWith("/*")
116
+ ? (GROUPS[token.slice(0, -1)] ?? token)
117
+ : (MIME_NAMES[token] ?? token.split("/").pop()?.toUpperCase() ?? token);
118
+ if (name === "JPEG" && names.includes("JPG")) continue;
119
+ if (!names.includes(name)) names.push(name);
120
+ }
121
+ return names.join(", ");
122
+ }
123
+
124
+ /** The muted line under the title: "PDF, 10 Mo maximum". Empty when nothing limits the pick. */
125
+ export function describeLimits(
126
+ accept: string | undefined,
127
+ maxFileSize: number | undefined,
128
+ ): string {
129
+ const types = describeAccept(accept);
130
+ const size = maxFileSize === undefined ? "" : `${formatSize(maxFileSize)} maximum`;
131
+ return [types, size].filter(Boolean).join(", ");
132
+ }
133
+
134
+ /** Which icon a file gets. */
135
+ export function fileKind(file: File): FileKind {
136
+ const name = file.name.toLowerCase();
137
+ const type = file.type.toLowerCase();
138
+ if (type === "application/pdf" || name.endsWith(".pdf")) return "pdf";
139
+ if (type.startsWith("image/")) return "image";
140
+ if (type === "text/csv" || name.endsWith(".csv")) return "csv";
141
+ if (/\.(xlsx?|ods)$/.test(name) || type.includes("spreadsheet") || type.includes("excel"))
142
+ return "sheet";
143
+ return "other";
144
+ }
@@ -0,0 +1,14 @@
1
+ export { default as FileDropZone } from "./file-drop-zone.svelte";
2
+ export {
3
+ checkFiles,
4
+ describeAccept,
5
+ describeLimits,
6
+ fileKind,
7
+ formatSize,
8
+ matchesAccept,
9
+ removeFile,
10
+ type CheckOptions,
11
+ type FileKind,
12
+ type RejectReason,
13
+ type Rejection,
14
+ } from "./files.js";
@@ -57,6 +57,8 @@
57
57
  selected?: Set<string>;
58
58
  selectAllLabel?: string;
59
59
  selectRowLabel?: (row: Row) => string;
60
+ /** The hidden head of the last column, the one holding each row's link or button. */
61
+ openColumnLabel?: string;
60
62
  /** The items of a row menu: `ContextMenu.Item`s. Right-click, a long press, Shift+F10 or the ContextMenu key opens it. */
61
63
  rowMenu?: Snippet<[Row]>;
62
64
  /** Rows that return the same key sit under one header row, and `null` rows sit with no header. Order `rows` so a group is contiguous. */
@@ -105,6 +107,7 @@
105
107
  selected = $bindable(new Set<string>()),
106
108
  selectAllLabel = "Select all rows",
107
109
  selectRowLabel,
110
+ openColumnLabel = "Open",
108
111
  rowMenu,
109
112
  group,
110
113
  groupHeader,
@@ -408,7 +411,7 @@
408
411
  {/if}
409
412
  {/each}
410
413
  {#if opens}
411
- <th class="w-0 p-0"><span class="sr-only">Open</span></th>
414
+ <th class="w-0 p-0"><span class="sr-only">{openColumnLabel}</span></th>
412
415
  {/if}
413
416
  </Table.Row>
414
417
  </Table.Header>
@@ -46,6 +46,8 @@ declare function $$render<Row extends object>(): {
46
46
  selected?: Set<string>;
47
47
  selectAllLabel?: string;
48
48
  selectRowLabel?: (row: Row) => string;
49
+ /** The hidden head of the last column, the one holding each row's link or button. */
50
+ openColumnLabel?: string;
49
51
  /** The items of a row menu: `ContextMenu.Item`s. Right-click, a long press, Shift+F10 or the ContextMenu key opens it. */
50
52
  rowMenu?: Snippet<[Row]>;
51
53
  /** Rows that return the same key sit under one header row, and `null` rows sit with no header. Order `rows` so a group is contiguous. */
@@ -6,6 +6,8 @@
6
6
  let {
7
7
  ref = $bindable(null),
8
8
  class: className,
9
+ // Inside a form, a bare button submits it: Locameublé's Annuler archived the bien (TSK-2744).
10
+ type = "button",
9
11
  ...restProps
10
12
  }: AlertDialogPrimitive.CancelProps = $props();
11
13
  </script>
@@ -13,6 +15,7 @@
13
15
  <AlertDialogPrimitive.Cancel
14
16
  bind:ref
15
17
  data-slot="alert-dialog-cancel"
18
+ {type}
16
19
  class={cn(buttonVariants({ variant: "outline" }), className)}
17
20
  {...restProps}
18
21
  />
@@ -26,7 +26,7 @@
26
26
  value={monthItem.value}
27
27
  selected={value !== undefined
28
28
  ? monthItem.value === value
29
- : monthItem.value === selectedMonthItem.value}
29
+ : monthItem.value === selectedMonthItem?.value}
30
30
  >
31
31
  {monthItem.label}
32
32
  </option>
@@ -36,7 +36,7 @@
36
36
  class="[&>svg]:text-muted-foreground flex h-8 items-center gap-1 rounded-md ps-2 pe-1 text-sm font-medium select-none [&>svg]:size-3.5"
37
37
  aria-hidden="true"
38
38
  >
39
- {monthItems.find((item) => item.value === value)?.label || selectedMonthItem.label}
39
+ {monthItems.find((item) => item.value === value)?.label || selectedMonthItem?.label}
40
40
  <CaretDown aria-hidden="true" class="size-4" />
41
41
  </span>
42
42
  {/snippet}
@@ -25,7 +25,7 @@
25
25
  value={yearItem.value}
26
26
  selected={value !== undefined
27
27
  ? yearItem.value === value
28
- : yearItem.value === selectedYearItem.value}
28
+ : yearItem.value === selectedYearItem?.value}
29
29
  >
30
30
  {yearItem.label}
31
31
  </option>
@@ -35,7 +35,7 @@
35
35
  class="[&>svg]:text-muted-foreground flex h-8 items-center gap-1 rounded-md ps-2 pe-1 text-sm font-medium select-none [&>svg]:size-3.5"
36
36
  aria-hidden="true"
37
37
  >
38
- {yearItems.find((item) => item.value === value)?.label || selectedYearItem.label}
38
+ {yearItems.find((item) => item.value === value)?.label || selectedYearItem?.label || value}
39
39
  <CaretDown aria-hidden="true" class="size-4" />
40
40
  </span>
41
41
  {/snippet}
@@ -1,9 +1,19 @@
1
1
  <script lang="ts">
2
2
  import { Calendar as CalendarPrimitive } from "bits-ui";
3
- // The root component composes its own sibling sub-components through the barrel it
4
- // is re-exported from; standard shadcn-svelte shape, not an initialization hazard.
5
- // fallow-ignore-next-line circular-dependency
6
- import * as Calendar from "./index.js";
3
+ import CalendarCaption from "./calendar-caption.svelte";
4
+ import CalendarCell from "./calendar-cell.svelte";
5
+ import CalendarDay from "./calendar-day.svelte";
6
+ import CalendarGridBody from "./calendar-grid-body.svelte";
7
+ import CalendarGridHead from "./calendar-grid-head.svelte";
8
+ import CalendarGridRow from "./calendar-grid-row.svelte";
9
+ import CalendarGrid from "./calendar-grid.svelte";
10
+ import CalendarHeadCell from "./calendar-head-cell.svelte";
11
+ import CalendarHeader from "./calendar-header.svelte";
12
+ import CalendarMonth from "./calendar-month.svelte";
13
+ import CalendarMonths from "./calendar-months.svelte";
14
+ import CalendarNav from "./calendar-nav.svelte";
15
+ import CalendarNextButton from "./calendar-next-button.svelte";
16
+ import CalendarPrevButton from "./calendar-prev-button.svelte";
7
17
  import { cn, type WithoutChildrenOrChild } from "../../lib/utils.js";
8
18
  import type { ButtonVariant } from "../button/button.svelte";
9
19
  import { isEqualMonth, type DateValue } from "@internationalized/date";
@@ -68,15 +78,15 @@ get along, so we shut typescript up by casting `value` to `never`.
68
78
  {...restProps}
69
79
  >
70
80
  {#snippet children({ months, weekdays })}
71
- <Calendar.Months>
72
- <Calendar.Nav>
73
- <Calendar.PrevButton variant={buttonVariant} />
74
- <Calendar.NextButton variant={buttonVariant} />
75
- </Calendar.Nav>
81
+ <CalendarMonths>
82
+ <CalendarNav>
83
+ <CalendarPrevButton variant={buttonVariant} />
84
+ <CalendarNextButton variant={buttonVariant} />
85
+ </CalendarNav>
76
86
  {#each months as month, monthIndex (month)}
77
- <Calendar.Month>
78
- <Calendar.Header>
79
- <Calendar.Caption
87
+ <CalendarMonth>
88
+ <CalendarHeader>
89
+ <CalendarCaption
80
90
  {captionLayout}
81
91
  months={monthsProp}
82
92
  {monthFormat}
@@ -87,38 +97,38 @@ get along, so we shut typescript up by casting `value` to `never`.
87
97
  {locale}
88
98
  {monthIndex}
89
99
  />
90
- </Calendar.Header>
91
- <Calendar.Grid>
92
- <Calendar.GridHead>
93
- <Calendar.GridRow class="select-none">
100
+ </CalendarHeader>
101
+ <CalendarGrid>
102
+ <CalendarGridHead>
103
+ <CalendarGridRow class="select-none">
94
104
  {#each weekdays as weekday (weekday)}
95
- <Calendar.HeadCell>
105
+ <CalendarHeadCell>
96
106
  {weekday.slice(0, 2)}
97
- </Calendar.HeadCell>
107
+ </CalendarHeadCell>
98
108
  {/each}
99
- </Calendar.GridRow>
100
- </Calendar.GridHead>
101
- <Calendar.GridBody>
109
+ </CalendarGridRow>
110
+ </CalendarGridHead>
111
+ <CalendarGridBody>
102
112
  {#each month.weeks as weekDates (weekDates)}
103
- <Calendar.GridRow class="mt-2 w-full">
113
+ <CalendarGridRow class="mt-2 w-full">
104
114
  {#each weekDates as date (date)}
105
- <Calendar.Cell {date} month={month.value}>
115
+ <CalendarCell {date} month={month.value}>
106
116
  {#if day}
107
117
  {@render day({
108
118
  day: date,
109
119
  outsideMonth: !isEqualMonth(date, month.value),
110
120
  })}
111
121
  {:else}
112
- <Calendar.Day />
122
+ <CalendarDay />
113
123
  {/if}
114
- </Calendar.Cell>
124
+ </CalendarCell>
115
125
  {/each}
116
- </Calendar.GridRow>
126
+ </CalendarGridRow>
117
127
  {/each}
118
- </Calendar.GridBody>
119
- </Calendar.Grid>
120
- </Calendar.Month>
128
+ </CalendarGridBody>
129
+ </CalendarGrid>
130
+ </CalendarMonth>
121
131
  {/each}
122
- </Calendar.Months>
132
+ </CalendarMonths>
123
133
  {/snippet}
124
134
  </CalendarPrimitive.Root>
@@ -1,5 +1,4 @@
1
1
  import { Calendar as CalendarPrimitive } from "bits-ui";
2
- import * as Calendar from "./index.js";
3
2
  import { type WithoutChildrenOrChild } from "../../lib/utils.js";
4
3
  import type { ButtonVariant } from "../button/button.svelte";
5
4
  import { type DateValue } from "@internationalized/date";
@@ -12,6 +12,7 @@
12
12
  title = "Command Palette",
13
13
  description = "Search for a command to run...",
14
14
  showCloseButton = false,
15
+ closeLabel = "Close",
15
16
  portalProps,
16
17
  children,
17
18
  class: className,
@@ -23,6 +24,8 @@
23
24
  title?: string;
24
25
  description?: string;
25
26
  showCloseButton?: boolean;
27
+ /** What a screen reader calls the close button, in the app's language. */
28
+ closeLabel?: string;
26
29
  class?: string;
27
30
  } = $props();
28
31
  </script>
@@ -51,6 +54,7 @@
51
54
  data-command-dialog="true"
52
55
  overlayClass="t-instant"
53
56
  {showCloseButton}
57
+ {closeLabel}
54
58
  {portalProps}
55
59
  >
56
60
  <!-- WHY the root's opaque `bg-popover`, no glass: `.t-modal` on the content sets `will-change:
@@ -7,6 +7,8 @@ type $$ComponentProps = WithoutChildrenOrChild<DialogPrimitive.RootProps> & With
7
7
  title?: string;
8
8
  description?: string;
9
9
  showCloseButton?: boolean;
10
+ /** What a screen reader calls the close button, in the app's language. */
11
+ closeLabel?: string;
10
12
  class?: string;
11
13
  };
12
14
  declare const $$Component: import("svelte").Component<$$ComponentProps, {}, "value" | "ref" | "open">;