summaryrefslogtreecommitdiffhomepage
path: root/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'src/components')
-rw-r--r--src/components/Table.svelte42
-rw-r--r--src/components/Table.test.ts35
2 files changed, 77 insertions, 0 deletions
diff --git a/src/components/Table.svelte b/src/components/Table.svelte
new file mode 100644
index 0000000..7c56e69
--- /dev/null
+++ b/src/components/Table.svelte
@@ -0,0 +1,42 @@
+<script lang="ts">
+ // Generic, purely presentational table. Props in → markup out; zero logic,
+ // zero data-fetching. Shared by the surface custom-field "table" renderer and
+ // the frontend "Loaded Modules" view, so neither feature depends on the other.
+ let {
+ columns,
+ rows,
+ empty = "No data",
+ }: {
+ readonly columns: readonly string[];
+ readonly rows: readonly (readonly string[])[];
+ /** Text shown when there are no rows. */
+ readonly empty?: string;
+ } = $props();
+</script>
+
+<div class="overflow-x-auto">
+ <table class="table table-sm">
+ <thead>
+ <tr>
+ {#each columns as col, i (i)}
+ <th>{col}</th>
+ {/each}
+ </tr>
+ </thead>
+ <tbody>
+ {#if rows.length === 0}
+ <tr>
+ <td colspan={Math.max(columns.length, 1)} class="opacity-60">{empty}</td>
+ </tr>
+ {:else}
+ {#each rows as row, r (r)}
+ <tr>
+ {#each row as cell, c (c)}
+ <td>{cell}</td>
+ {/each}
+ </tr>
+ {/each}
+ {/if}
+ </tbody>
+ </table>
+</div>
diff --git a/src/components/Table.test.ts b/src/components/Table.test.ts
new file mode 100644
index 0000000..9fbecd3
--- /dev/null
+++ b/src/components/Table.test.ts
@@ -0,0 +1,35 @@
+import { render, screen, within } from "@testing-library/svelte";
+import { describe, expect, it } from "vitest";
+import Table from "./Table.svelte";
+
+describe("Table", () => {
+ it("renders a header cell per column", () => {
+ render(Table, { props: { columns: ["Name", "Version"], rows: [] } });
+ const headers = screen.getAllByRole("columnheader");
+ expect(headers.map((h) => h.textContent)).toEqual(["Name", "Version"]);
+ });
+
+ it("renders one row per data row with aligned cells", () => {
+ render(Table, {
+ props: {
+ columns: ["Name", "Version"],
+ rows: [
+ ["alpha", "1.0"],
+ ["beta", "2.3"],
+ ],
+ },
+ });
+ const body = screen.getAllByRole("rowgroup")[1];
+ if (body === undefined) throw new Error("expected a tbody rowgroup");
+ const rows = within(body).getAllByRole("row");
+ expect(rows).toHaveLength(2);
+ expect(within(rows[0] as HTMLElement).getByText("alpha")).toBeInTheDocument();
+ expect(within(rows[0] as HTMLElement).getByText("1.0")).toBeInTheDocument();
+ expect(within(rows[1] as HTMLElement).getByText("beta")).toBeInTheDocument();
+ });
+
+ it("shows the empty message when there are no rows", () => {
+ render(Table, { props: { columns: ["A"], rows: [], empty: "Nothing loaded" } });
+ expect(screen.getByText("Nothing loaded")).toBeInTheDocument();
+ });
+});