@aggc/ui 0.7.1 → 0.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 (48) hide show
  1. package/dist/chunks/DataTable-CIiU5Jx3.js +8688 -0
  2. package/dist/components/DataTable.styles.d.ts +48 -0
  3. package/dist/components/DataTable.types.d.ts +38 -0
  4. package/dist/components/DataTable.vue.d.ts +72 -0
  5. package/dist/components/UiAvatar.styles.d.ts +53 -0
  6. package/dist/components/UiAvatar.vue.d.ts +13 -0
  7. package/dist/components/UiModal.styles.d.ts +31 -0
  8. package/dist/components/UiModal.vue.d.ts +30 -0
  9. package/dist/components/UiToast.styles.d.ts +41 -0
  10. package/dist/components/UiToast.vue.d.ts +13 -0
  11. package/dist/components/UiToastProvider.vue.d.ts +13 -0
  12. package/dist/components/UiTooltip.styles.d.ts +1 -0
  13. package/dist/components/UiTooltip.vue.d.ts +25 -0
  14. package/dist/components/index.d.ts +11 -0
  15. package/dist/components.js +30 -12
  16. package/dist/composables/useToast.d.ts +27 -0
  17. package/dist/index.d.ts +1 -0
  18. package/dist/index.js +82 -63
  19. package/dist/ui.css +995 -294
  20. package/package.json +3 -2
  21. package/src/components/DataTable.styles.ts +493 -0
  22. package/src/components/DataTable.test.ts +249 -0
  23. package/src/components/DataTable.types.ts +42 -0
  24. package/src/components/DataTable.vue +567 -0
  25. package/src/components/UiAvatar.styles.ts +81 -0
  26. package/src/components/UiAvatar.test.ts +43 -0
  27. package/src/components/UiAvatar.vue +41 -0
  28. package/src/components/UiModal.styles.ts +143 -0
  29. package/src/components/UiModal.test.ts +64 -0
  30. package/src/components/UiModal.vue +82 -0
  31. package/src/components/UiToast.styles.ts +143 -0
  32. package/src/components/UiToast.test.ts +47 -0
  33. package/src/components/UiToast.vue +65 -0
  34. package/src/components/UiToastProvider.vue +22 -0
  35. package/src/components/UiTooltip.styles.ts +25 -0
  36. package/src/components/UiTooltip.test.ts +37 -0
  37. package/src/components/UiTooltip.vue +37 -0
  38. package/src/components/index.ts +17 -0
  39. package/src/composables/useToast.ts +43 -0
  40. package/src/css/base.css +61 -1
  41. package/src/index.ts +1 -0
  42. package/src/stories/feedback/UiToast.stories.ts +72 -0
  43. package/src/stories/layout/DataTable.stories.ts +141 -0
  44. package/src/stories/layout/UiModal.stories.ts +89 -0
  45. package/src/stories/primitives/UiAvatar.stories.ts +83 -0
  46. package/src/stories/primitives/UiTooltip.stories.ts +46 -0
  47. package/src/stories/support/sources.ts +81 -0
  48. package/dist/chunks/UiSkeleton.vue_vue_type_script_setup_true_lang-DUse1KRc.js +0 -1201
@@ -0,0 +1,249 @@
1
+ import { mount } from "@vue/test-utils";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import DataTable from "./DataTable.vue";
4
+
5
+ // Stub UiTooltip so tests don't exercise reka-ui portal/floating in jsdom.
6
+ const globalStubs = {
7
+ UiTooltip: { template: "<div><slot /></div>" },
8
+ };
9
+
10
+ interface TestItem {
11
+ id: string;
12
+ name: string;
13
+ email: string;
14
+ }
15
+
16
+ const items: TestItem[] = [
17
+ { id: "1", name: "Alice", email: "alice@example.com" },
18
+ { id: "2", name: "Bob", email: "bob@example.com" },
19
+ ];
20
+
21
+ const columns = [
22
+ { key: "name", label: "Name", width: "1fr" },
23
+ { key: "email", label: "Email", width: "1fr" },
24
+ ];
25
+
26
+ function mountTable(props: Record<string, unknown> = {}, slots: Record<string, string> = {}) {
27
+ return mount(DataTable<TestItem>, {
28
+ props: { items, columns, isLoading: false, ...props } as never,
29
+ slots,
30
+ global: { stubs: globalStubs },
31
+ });
32
+ }
33
+
34
+ describe("DataTable", () => {
35
+ it("renders column headers", () => {
36
+ const wrapper = mountTable();
37
+ expect(wrapper.text()).toContain("Name");
38
+ expect(wrapper.text()).toContain("Email");
39
+ });
40
+
41
+ it("renders rows from items", () => {
42
+ const wrapper = mountTable({}, {
43
+ "cell-name": `<template #cell-name="{ item }">{{ item.name }}</template>`,
44
+ });
45
+ expect(wrapper.text()).toContain("Alice");
46
+ expect(wrapper.text()).toContain("Bob");
47
+ });
48
+
49
+ it("renders custom cell slots", () => {
50
+ const wrapper = mountTable({}, {
51
+ "cell-name": `<template #cell-name="{ item }"><strong>{{ item.name }}</strong></template>`,
52
+ });
53
+ expect(wrapper.html()).toContain("<strong>Alice</strong>");
54
+ });
55
+
56
+ it("renders search input when searchable", () => {
57
+ const wrapper = mountTable({ searchable: true, searchFields: ["name", "email"] });
58
+ expect(wrapper.find('input[aria-label="Search"]').exists()).toBe(true);
59
+ });
60
+
61
+ it("filters items by search", async () => {
62
+ vi.useFakeTimers();
63
+ const wrapper = mountTable(
64
+ { searchable: true, searchFields: ["name"] },
65
+ { "cell-name": `<template #cell-name="{ item }">{{ item.name }}</template>` },
66
+ );
67
+
68
+ const input = wrapper.find('input[aria-label="Search"]');
69
+ await input.setValue("Alice");
70
+ vi.advanceTimersByTime(200);
71
+ await wrapper.vm.$nextTick();
72
+
73
+ expect(wrapper.text()).toContain("Alice");
74
+ expect(wrapper.text()).not.toContain("Bob");
75
+ vi.useRealTimers();
76
+ });
77
+
78
+ it("renders mobile cards on mobile viewport", async () => {
79
+ const originalWidth = window.innerWidth;
80
+ window.innerWidth = 500;
81
+ try {
82
+ const wrapper = mount(DataTable<TestItem>, {
83
+ props: { items, columns, isLoading: false },
84
+ slots: {
85
+ "mobile-card": '<template #mobile-card="{ item }"><div class="test-mobile">{{ item.name }}</div></template>',
86
+ },
87
+ global: { stubs: globalStubs },
88
+ });
89
+ // Belt-and-suspenders: fire resize so onResize runs against the mocked width.
90
+ window.dispatchEvent(new Event("resize"));
91
+ await wrapper.vm.$nextTick();
92
+
93
+ const mobileCards = wrapper.findAll(".test-mobile");
94
+ expect(mobileCards).toHaveLength(2);
95
+ expect(mobileCards[0].text()).toBe("Alice");
96
+ } finally {
97
+ window.innerWidth = originalWidth;
98
+ }
99
+ });
100
+
101
+ it("emits update:selected when a row checkbox is toggled", async () => {
102
+ const wrapper = mountTable();
103
+ // First button[role=checkbox] is "select all", second is row 1.
104
+ const checkboxes = wrapper.findAll('button[role="checkbox"]');
105
+ await checkboxes[1].trigger("click");
106
+
107
+ const emitted = wrapper.emitted("update:selected") as string[][];
108
+ expect(emitted).toBeTruthy();
109
+ expect(emitted[emitted.length - 1][0]).toContain("1");
110
+ });
111
+
112
+ it("sets aria-checked to mixed when selection is indeterminate", async () => {
113
+ const wrapper = mountTable();
114
+ const checkboxes = wrapper.findAll('button[role="checkbox"]');
115
+ // Select only row 1 → select-all becomes indeterminate.
116
+ await checkboxes[1].trigger("click");
117
+ await wrapper.vm.$nextTick();
118
+
119
+ expect(checkboxes[0].attributes("aria-checked")).toBe("mixed");
120
+ });
121
+
122
+ it("selects all visible items on Ctrl/Cmd+A", async () => {
123
+ const wrapper = mountTable();
124
+ window.dispatchEvent(new KeyboardEvent("keydown", { key: "a", ctrlKey: true }));
125
+ await wrapper.vm.$nextTick();
126
+
127
+ const emitted = wrapper.emitted("update:selected") as string[][];
128
+ const last = emitted[emitted.length - 1][0] as unknown as string[];
129
+ expect(last).toHaveLength(2);
130
+ expect(last).toEqual(expect.arrayContaining(["1", "2"]));
131
+ });
132
+
133
+ it("clears selection on Escape", async () => {
134
+ const wrapper = mountTable();
135
+ const checkboxes = wrapper.findAll('button[role="checkbox"]');
136
+ await checkboxes[1].trigger("click");
137
+
138
+ window.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape" }));
139
+ await wrapper.vm.$nextTick();
140
+
141
+ const emitted = wrapper.emitted("update:selected") as string[][];
142
+ const last = emitted[emitted.length - 1][0] as unknown as string[];
143
+ expect(last).toHaveLength(0);
144
+ });
145
+
146
+ it("shows skeleton rows and aria-busy when isLoading is true", () => {
147
+ const wrapper = mountTable({ isLoading: true, items: [] as unknown as TestItem[] });
148
+ expect(wrapper.find('[aria-busy="true"]').exists()).toBe(true);
149
+ expect(wrapper.findAll('[aria-hidden="true"]').length).toBeGreaterThan(0);
150
+ expect(wrapper.text()).not.toContain("Alice");
151
+ });
152
+
153
+ it("throws in dev when an item is missing its id", () => {
154
+ expect(() =>
155
+ mount(DataTable<TestItem>, {
156
+ props: { items: [{ name: "x" } as unknown as TestItem], columns, isLoading: false },
157
+ global: { stubs: globalStubs },
158
+ }),
159
+ ).toThrow();
160
+ });
161
+
162
+ it("renders actions slot per row", () => {
163
+ const wrapper = mountTable({}, {
164
+ actions: '<template #actions="{ item }"><button class="test-action">{{ item.name }}</button></template>',
165
+ });
166
+ const triggers = wrapper.findAllComponents({ name: "DropdownMenuTrigger" });
167
+ expect(triggers).toHaveLength(2);
168
+ });
169
+
170
+ // ── Reusability: selectable, labels, sort, pagination, bodyMaxHeight ──
171
+
172
+ it("hides the selection column when selectable is false", () => {
173
+ const wrapper = mountTable({ selectable: false });
174
+ expect(wrapper.findAll('button[role="checkbox"]')).toHaveLength(0);
175
+ });
176
+
177
+ it("hides the actions column when no actions slot is provided", () => {
178
+ const wrapper = mountTable();
179
+ // gridTemplate has no trailing 48px; no Actions columnheader rendered.
180
+ expect(wrapper.find('[aria-label="Actions"]').exists()).toBe(false);
181
+ });
182
+
183
+ it("overrides user-visible strings via the labels prop", () => {
184
+ const wrapper = mountTable({
185
+ searchable: true,
186
+ labels: { searchPlaceholder: "Buscar...", selected: "{count} elegidos" },
187
+ });
188
+ expect(wrapper.find('input[aria-label="Search"]').attributes("placeholder")).toBe("Buscar...");
189
+ });
190
+
191
+ it("sorts rows client-side when a sortable header is clicked", async () => {
192
+ const sortableColumns = [
193
+ { key: "name", label: "Name", width: "1fr", sortable: true },
194
+ ];
195
+ const wrapper = mount(DataTable<TestItem>, {
196
+ props: { items, columns: sortableColumns, isLoading: false },
197
+ slots: { "cell-name": `<template #cell-name="{ item }">{{ item.name }}</template>` },
198
+ global: { stubs: globalStubs },
199
+ });
200
+
201
+ // Initial order: Alice, Bob.
202
+ let rows = wrapper.findAll('[data-row-idx]');
203
+ expect(rows[0].text()).toContain("Alice");
204
+
205
+ // The sortable header is a <button> containing the column label.
206
+ const headerBtn = wrapper.findAll("button").find((b) => b.text().includes("Name"))!;
207
+ // Click header → asc (Alice, Bob), click again → desc (Bob, Alice).
208
+ await headerBtn.trigger("click");
209
+ rows = wrapper.findAll('[data-row-idx]');
210
+ expect(rows[0].text()).toContain("Alice");
211
+
212
+ await headerBtn.trigger("click");
213
+ rows = wrapper.findAll('[data-row-idx]');
214
+ expect(rows[0].text()).toContain("Bob");
215
+
216
+ // Emits update:sort.
217
+ const sorts = wrapper.emitted("update:sort");
218
+ expect(sorts).toBeTruthy();
219
+ });
220
+
221
+ it("paginates rows client-side when pageSize is set", async () => {
222
+ const wrapper = mount(DataTable<TestItem>, {
223
+ props: { items, columns, isLoading: false, pageSize: 1 },
224
+ slots: { "cell-name": `<template #cell-name="{ item }">{{ item.name }}</template>` },
225
+ global: { stubs: globalStubs },
226
+ });
227
+
228
+ // Page 1 shows only Alice.
229
+ let rows = wrapper.findAll('[data-row-idx]');
230
+ expect(rows).toHaveLength(1);
231
+ expect(rows[0].text()).toContain("Alice");
232
+
233
+ // Page info + next button.
234
+ expect(wrapper.text()).toContain("Page 1 of 2");
235
+ const buttons = wrapper.findAll("button");
236
+ const next = buttons[buttons.length - 1];
237
+ await next.trigger("click");
238
+
239
+ rows = wrapper.findAll('[data-row-idx]');
240
+ expect(rows[0].text()).toContain("Bob");
241
+ expect(wrapper.emitted("update:page")).toBeTruthy();
242
+ });
243
+
244
+ it("applies bodyMaxHeight as an inline style on the table body", () => {
245
+ const wrapper = mountTable({ bodyMaxHeight: "400px" });
246
+ const body = wrapper.find('[role="rowgroup"]');
247
+ expect(body.attributes("style") ?? "").toContain("max-height: 400px");
248
+ });
249
+ });
@@ -0,0 +1,42 @@
1
+ export interface DataTableColumn {
2
+ key: string;
3
+ label: string;
4
+ width: string;
5
+ hideMobile?: boolean;
6
+ /** Enable client-side sorting on this column's header. */
7
+ sortable?: boolean;
8
+ }
9
+
10
+ export type DataTableSortDir = "asc" | "desc";
11
+
12
+ export interface DataTableSortState {
13
+ key: string;
14
+ dir: DataTableSortDir;
15
+ }
16
+
17
+ /** Where sorting happens. "client" sorts in-component; "server" only emits. */
18
+ export type DataTableSortMode = "client" | "server";
19
+
20
+ /**
21
+ * All user-visible strings. Override any subset via the `labels` prop for i18n.
22
+ * Placeholders: {count}, {term}, {shown}, {total}, {page}, {pages}.
23
+ */
24
+ export interface DataTableLabels {
25
+ search: string;
26
+ searchPlaceholder: string;
27
+ clearSearch: string;
28
+ selectAll: string;
29
+ deselectAll: string;
30
+ select: string;
31
+ deselect: string;
32
+ selectRow: string;
33
+ actions: string;
34
+ selected: string;
35
+ noResultsTitle: string;
36
+ noResultsDesc: string;
37
+ clearSearchBtn: string;
38
+ footerCount: string;
39
+ previousPage: string;
40
+ nextPage: string;
41
+ pageOf: string;
42
+ }