Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
53 changes: 48 additions & 5 deletions packages/web/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -266,10 +266,53 @@ td {
white-space: nowrap;
}

tbody {
td {
color: var(--color-text-tertiary);
}
tbody td {
color: var(--color-text-tertiary);
}

/* Checkbox column */
td:first-child,
th:first-child {
width: 36px;
text-align: center;
padding: 0.75rem 0.5rem;
}

/* Row selection highlight */
tr.selected-row {
background-color: var(--color-surface);
font-weight: 500;
border-left: 2px solid var(--color-brand);
}

/* Checkbox style */
input[type="checkbox"].row-checkbox,
#select-all {
accent-color: var(--color-brand);
width: 1.15em;
height: 1.15em;
cursor: pointer;
border-radius: 2px;
}

/* Filter button style - matches "How to use" button */
#filter-selected {
flex: 0 0 auto;
cursor: pointer;
border: none;
background-color: var(--color-brand);
color: var(--color-text-invert);
font-size: 0.8125rem;
line-height: 1.1;
height: 2rem;
padding: 0.5rem 0.75rem;
border-radius: 0.25rem;
}
#filter-selected:disabled {
opacity: 0.5;
cursor: not-allowed;
}


td:nth-child(1) {
font-weight: 500;
Expand Down Expand Up @@ -410,7 +453,7 @@ tbody {
.modality-icon:hover::after {
opacity: 1;
}
}


dialog::backdrop {
backdrop-filter: blur(8px);
Expand Down
7 changes: 6 additions & 1 deletion packages/web/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { initializeSelection } from "./selection.js";

const modal = document.getElementById("modal") as HTMLDialogElement;
const modalClose = document.getElementById("close")!;
const help = document.getElementById("help")!;
Expand Down Expand Up @@ -236,5 +238,8 @@ function initializeFromURL() {
})();
}

document.addEventListener("DOMContentLoaded", initializeFromURL);
document.addEventListener("DOMContentLoaded", () => {
initializeFromURL();
initializeSelection();
});
window.addEventListener("popstate", initializeFromURL);
8 changes: 7 additions & 1 deletion packages/web/src/render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -206,12 +206,14 @@ export const Rendered = renderToString(
<input type="text" id="search" placeholder="Filter by model" />
<span class="search-shortcut">⌘K</span>
</div>
<button id="filter-selected" disabled>Show Selected Only</button>
<button id="help">How to use</button>
</div>
</header>
<table>
<thead>
<tr>
<th style="width:32px;"><input type="checkbox" id="select-all" aria-label="Select all models" /></th>
<th class="sortable" data-type="text">
Provider <span class="sort-indicator"></span>
</th>
Expand Down Expand Up @@ -321,7 +323,10 @@ export const Rendered = renderToString(
modelA.name.localeCompare(modelB.name)
)
.map(([modelId, model]) => (
<tr key={`${providerId}-${modelId}`}>
<tr key={`${providerId}-${modelId}`} data-provider-id={providerId} data-model-id={modelId}>
<td>
<input type="checkbox" class="row-checkbox" aria-label="Select model" tabindex="0" />
</td>
<td>
<div class="provider-cell">
{renderProviderLogo(providerId)}
Expand Down Expand Up @@ -520,5 +525,6 @@ export const Rendered = renderToString(
</a>
</div>
</dialog>

</Fragment>
);
103 changes: 103 additions & 0 deletions packages/web/src/selection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// Selection and Filter Logic for Models Table
export function initializeSelection() {
const rowCheckboxes = () => Array.from(document.querySelectorAll('.row-checkbox')) as HTMLInputElement[];
const selectAll = document.getElementById('select-all') as HTMLInputElement;
const filterBtn = document.getElementById('filter-selected') as HTMLButtonElement;
const selected = new Set<string>();
let filterActive = false;

// Helper: get row key
function getRowKey(tr: HTMLTableRowElement): string {
return tr.getAttribute('data-provider-id') + '::' + tr.getAttribute('data-model-id')!;
}

// Update selection state and UI
function updateSelectionUI() {
rowCheckboxes().forEach(cb => {
const tr = cb.closest('tr') as HTMLTableRowElement;
const key = getRowKey(tr);
if (selected.has(key)) {
cb.checked = true;
tr.classList.add('selected-row');
} else {
cb.checked = false;
tr.classList.remove('selected-row');
}
});
// Update select-all
if (selected.size === 0) {
selectAll.checked = false;
selectAll.indeterminate = false;
} else if (selected.size === rowCheckboxes().length) {
selectAll.checked = true;
selectAll.indeterminate = false;
} else {
selectAll.checked = false;
selectAll.indeterminate = true;
}
selectAll.disabled = filterActive;
// Update filter button
filterBtn.disabled = selectAll.checked;
filterBtn.textContent = filterActive ? 'Show All' : 'Show Selected Only';
}

// Row checkbox click
document.addEventListener('change', function (e) {
const target = e.target as HTMLInputElement;
if (target.classList.contains('row-checkbox')) {
const tr = target.closest('tr') as HTMLTableRowElement;
const key = getRowKey(tr);
if (target.checked) {
selected.add(key);
} else {
selected.delete(key);
}
updateSelectionUI();
if (filterActive) applyFilter();
}
});

// Select all checkbox
selectAll.addEventListener('change', function (e) {
if (selectAll.checked) {
rowCheckboxes().forEach(cb => {
const tr = cb.closest('tr') as HTMLTableRowElement;
selected.add(getRowKey(tr));
});
} else {
selected.clear();
}
updateSelectionUI();
if (filterActive) applyFilter();
});

// Filter button
filterBtn.addEventListener('click', function () {
filterActive = !filterActive;
applyFilter();
updateSelectionUI();
});

function applyFilter() {
rowCheckboxes().forEach(cb => {
const tr = cb.closest('tr') as HTMLTableRowElement;
const key = getRowKey(tr);
if (filterActive) {
tr.style.display = selected.has(key) ? '' : 'none';
} else {
tr.style.display = '';
}
});
}

// Keyboard accessibility: space/enter on row
document.addEventListener('keydown', function (e) {
if ((e.key === ' ' || e.key === 'Enter') && (document.activeElement as HTMLElement)?.classList.contains('row-checkbox')) {
e.preventDefault();
(document.activeElement as HTMLInputElement).click();
}
});

// Initial UI update
updateSelectionUI();
}