Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
cdf0413
feat(web): add @tanstack/table-core and @tanstack/virtual-core
gianpaj Feb 24, 2026
aa07bd8
feat(web): add Row type and flattenProviders utility
gianpaj Feb 24, 2026
229bdbf
feat(web): add URL state serialization utilities
gianpaj Feb 24, 2026
1918e49
feat(web): strip render.tsx to shell — table rows moved to client
gianpaj Feb 24, 2026
21a7b50
feat(web): add Tanstack Table column definitions (safe DOM construction)
gianpaj Feb 24, 2026
fdfe83c
feat: rewrite index.ts with Tanstack Table + Virtual
gianpaj Feb 24, 2026
51b481d
feat: update CSS for virtual scroll and column picker; add data-colum…
gianpaj Feb 24, 2026
8dc67e4
fix: call virtualizer._willUpdate() and add /api.json dev route
gianpaj Feb 24, 2026
c2c0560
fix column container and auto formatting with biome
gianpaj Feb 24, 2026
27da1d7
feat: inline JSON data at build time
sunipan Feb 25, 2026
2e1c2e1
feat: add default column visibility with localStorage persistence
sunipan Feb 25, 2026
f8fc2de
feat: add MiniSearch fuzzy search module with debounce
sunipan Feb 25, 2026
cf03a2b
feat: integrate search, debounce, and column persistence into client
sunipan Feb 25, 2026
6fb9754
test: add tests for search, debounce, and column visibility
sunipan Feb 25, 2026
3df9ad9
feat: full-width table with proportional column flex-grow
sunipan Feb 25, 2026
145436b
feat: mobile-friendly header with full-width search bar on second row
sunipan Feb 25, 2026
060cf40
Merge branch 'dev' into feat/perf-improvements
sunipan Feb 25, 2026
c1bb22c
fix: iOS sticky table header scroll desync and opaque header background
sunipan Feb 25, 2026
86cf580
fix: auto-size table columns to fit content
sunipan Feb 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 11 additions & 10 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
</head>
<body>
<!--static-->
<script id="model-data" type="application/json"></script>
<script src="./src/index.ts" type="module"></script>
</body>
</html>
3 changes: 3 additions & 0 deletions packages/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@
"build": "./script/build.ts"
},
"dependencies": {
"@tanstack/table-core": "^8",
"@tanstack/virtual-core": "^3",
"hono": "^4.8.0",
"minisearch": "^7.2.0",
"models.dev": "workspace:*"
},
"devDependencies": {
Expand Down
4 changes: 4 additions & 0 deletions packages/web/script/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ for (const entry of entries) {

let html = await Bun.file("./dist/index.html").text();
html = html.replace("<!--static-->", Rendered);
html = html.replace(
'<script id="model-data" type="application/json"></script>',
'<script id="model-data" type="application/json">' + JSON.stringify(Providers) + '</script>'
);
await Bun.write("./dist/index.html", html);
await Bun.write("./dist/api.json", JSON.stringify(Providers));

Expand Down
70 changes: 70 additions & 0 deletions packages/web/src/columns.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { describe, expect, it } from "bun:test";
import {
ALL_COLUMN_IDS,
DEFAULT_COLUMN_IDS,
parseUrlState,
} from "./url-state.js";

describe("DEFAULT_COLUMN_IDS", () => {
it("has exactly 9 items", () => {
expect(DEFAULT_COLUMN_IDS).toHaveLength(9);
});

it("contains the expected default columns", () => {
expect(DEFAULT_COLUMN_IDS).toContain("provider");
expect(DEFAULT_COLUMN_IDS).toContain("model");
expect(DEFAULT_COLUMN_IDS).toContain("family");
expect(DEFAULT_COLUMN_IDS).toContain("model-id");
expect(DEFAULT_COLUMN_IDS).toContain("tool-call");
expect(DEFAULT_COLUMN_IDS).toContain("reasoning");
expect(DEFAULT_COLUMN_IDS).toContain("input-cost");
expect(DEFAULT_COLUMN_IDS).toContain("output-cost");
expect(DEFAULT_COLUMN_IDS).toContain("context-limit");
});

it("does NOT contain non-default columns", () => {
expect(DEFAULT_COLUMN_IDS).not.toContain("provider-id");
expect(DEFAULT_COLUMN_IDS).not.toContain("cache-read-cost");
expect(DEFAULT_COLUMN_IDS).not.toContain("audio-input-cost");
expect(DEFAULT_COLUMN_IDS).not.toContain("audio-output-cost");
expect(DEFAULT_COLUMN_IDS).not.toContain("input-limit");
expect(DEFAULT_COLUMN_IDS).not.toContain("output-limit");
expect(DEFAULT_COLUMN_IDS).not.toContain("knowledge");
expect(DEFAULT_COLUMN_IDS).not.toContain("release-date");
expect(DEFAULT_COLUMN_IDS).not.toContain("last-updated");
});

it("all entries are valid column IDs (present in ALL_COLUMN_IDS)", () => {
const allIds = [...ALL_COLUMN_IDS] as string[];
for (const id of DEFAULT_COLUMN_IDS) {
expect(allIds).toContain(id);
}
});
});

describe("column visibility via parseUrlState", () => {
it("cols=provider,model returns exactly those 2 columns", () => {
const state = parseUrlState(new URLSearchParams("cols=provider,model"));
expect(state.cols).toEqual(["provider", "model"]);
expect(state.cols).toHaveLength(2);
});

it("invalid column IDs in cols param are filtered out", () => {
const state = parseUrlState(
new URLSearchParams("cols=provider,not-a-real-column,model"),
);
expect(state.cols).toEqual(["provider", "model"]);
});

it("cols param with all invalid IDs falls back to defaults", () => {
const state = parseUrlState(
new URLSearchParams("cols=fake-col,another-fake"),
);
expect(state.cols).toEqual([...DEFAULT_COLUMN_IDS]);
});

it("no cols param returns DEFAULT_COLUMN_IDS", () => {
const state = parseUrlState(new URLSearchParams(""));
expect(state.cols).toEqual([...DEFAULT_COLUMN_IDS]);
});
});
Loading