diff --git a/src/app.css b/src/app.css
index 0facc70..fde7587 100644
--- a/src/app.css
+++ b/src/app.css
@@ -1,15 +1,18 @@
@import "tailwindcss";
@theme {
- --color-bg: #131010;
- --color-raised: #1a1616;
- --color-line: #2a2424;
- --color-text: #f1ecec;
- --color-muted: #b7b1b1;
- --color-faint: #7c7575;
- --color-accent: #89b5ff;
+ --color-bg: #111213;
+ --color-raised: #191a1c;
+ --color-line: #2b2d30;
+ --color-text: #f1f1ee;
+ --color-muted: #b6b7b8;
+ --color-faint: #92959a;
+ --color-accent: #78a9ff;
--color-good: #8ec07c;
--color-bad: #e0866b;
+ --radius-control: 6px;
+ --radius-surface: 10px;
+ --ease-ui: 140ms ease;
}
html,
@@ -19,6 +22,7 @@ body {
overflow: hidden;
background: var(--color-bg);
color: var(--color-text);
+ font-family: "SF Mono", "Cascadia Code", "Roboto Mono", monospace;
}
table {
@@ -33,3 +37,13 @@ table {
.chip-strip::-webkit-scrollbar {
display: none;
}
+
+@media (prefers-reduced-motion: no-preference) {
+ button,
+ input,
+ [role="menu"],
+ [role="dialog"] {
+ transition: color var(--ease-ui), background-color var(--ease-ui), border-color var(--ease-ui),
+ box-shadow var(--ease-ui), opacity var(--ease-ui);
+ }
+}
diff --git a/src/lib/ExportMenu.svelte b/src/lib/ExportMenu.svelte
index 0965c62..1554833 100644
--- a/src/lib/ExportMenu.svelte
+++ b/src/lib/ExportMenu.svelte
@@ -1,35 +1,10 @@
-
{
- if (event.key === "Escape") {
- event.preventDefault();
- onDismiss();
- } else if (["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key)) {
- event.preventDefault();
- item?.focus();
- }
- }}
->
-
-
+ {
- if (event.key === "Escape") {
- event.preventDefault();
- onDismiss();
- }
- }}
->
-
-
-
-
+
+
+
+
+
+
+
diff --git a/src/lib/SelectionActions.svelte b/src/lib/SelectionActions.svelte
new file mode 100644
index 0000000..52c4f8c
--- /dev/null
+++ b/src/lib/SelectionActions.svelte
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+ {#if menuOpen}
+
+
+
+ {/if}
+
+
+
diff --git a/src/lib/SubnetCell.svelte b/src/lib/SubnetCell.svelte
index 2555085..fc0dd60 100644
--- a/src/lib/SubnetCell.svelte
+++ b/src/lib/SubnetCell.svelte
@@ -1,7 +1,5 @@
-
+
{cidr}
diff --git a/src/lib/SubnetTable.svelte b/src/lib/SubnetTable.svelte
index 794975a..ea55cdb 100644
--- a/src/lib/SubnetTable.svelte
+++ b/src/lib/SubnetTable.svelte
@@ -245,6 +245,12 @@
untrack(() => table.setPageIndex(0));
});
+ $effect(() => {
+ if (selectedCidrs.size === 0) {
+ rangeAnchor = null;
+ }
+ });
+
$effect(() => {
const pageCount = Math.max(1, table.getPageCount());
if (pagination.pageIndex >= pageCount) {
@@ -284,12 +290,53 @@
return () => window.removeEventListener("keydown", selectByKeyboard);
});
- let menu = $state<{ cidr: string; x: number; y: number } | null>(null);
+ let menu = $state<{ cidr: string; x: number; y: number; trigger: HTMLElement | null } | null>(null);
let rangeAnchor = $state
(null);
function openMenu(event: MouseEvent, cidr: string) {
event.preventDefault();
- menu = { cidr, x: event.clientX, y: event.clientY };
+ menu = { cidr, x: event.clientX, y: event.clientY, trigger: event.currentTarget as HTMLElement };
+ }
+
+ function selectRowByKeyboard(event: KeyboardEvent, cidr: string) {
+ if (isInteractive(event.target)) {
+ return;
+ }
+ if (event.key !== "Enter" && event.key !== " ") {
+ if (event.key === "ContextMenu" || (event.shiftKey && event.key === "F10")) {
+ event.preventDefault();
+ const trigger = event.currentTarget as HTMLElement;
+ const bounds = trigger.getBoundingClientRect();
+ menu = { cidr, x: bounds.left + Math.min(24, bounds.width / 2), y: bounds.top + Math.min(24, bounds.height / 2), trigger };
+ }
+ return;
+ }
+ event.preventDefault();
+ const modifier = event.metaKey || event.ctrlKey;
+ if (event.shiftKey && rangeAnchor) {
+ const start = orderedRows.findIndex((row) => row.original.cidr === rangeAnchor);
+ const end = orderedRows.findIndex((row) => row.original.cidr === cidr);
+ if (start >= 0 && end >= 0) {
+ const range = orderedRows
+ .slice(Math.min(start, end), Math.max(start, end) + 1)
+ .map((row) => row.original.cidr);
+ onSelectionChange(modifier ? new Set([...selectedCidrs, ...range]) : new Set(range));
+ return;
+ }
+ }
+ const next = new Set(selectedCidrs);
+ if (modifier || selectedCidrs.has(cidr)) {
+ if (next.has(cidr)) {
+ next.delete(cidr);
+ } else {
+ next.add(cidr);
+ }
+ } else {
+ next.clear();
+ next.add(cidr);
+ }
+ onSelectionChange(next);
+ rangeAnchor = cidr;
}
function selectRow(event: MouseEvent, cidr: string) {
@@ -459,12 +506,13 @@
{#if header.column.getCanFilter()}
{
header.column.setFilterValue(event.currentTarget.value);
table.setPageIndex(0);
@@ -485,10 +533,12 @@
selectRow(event, row.original.cidr)}
oncontextmenu={(event) => openMenu(event, row.original.cidr)}
+ onkeydown={(event) => selectRowByKeyboard(event, row.original.cidr)}
>
{#each row.getAllCells() as cell (cell.id)}
{
const cidr = menu?.cidr;
diff --git a/src/lib/TagsCell.svelte b/src/lib/TagsCell.svelte
index 40ae80f..0023f25 100644
--- a/src/lib/TagsCell.svelte
+++ b/src/lib/TagsCell.svelte
@@ -24,9 +24,10 @@
{#each tags as tag}
{announcement}
diff --git a/src/lib/ui/Button.svelte b/src/lib/ui/Button.svelte
new file mode 100644
index 0000000..3cc23dc
--- /dev/null
+++ b/src/lib/ui/Button.svelte
@@ -0,0 +1,56 @@
+
+
+
+ {@render children()}
+
diff --git a/src/lib/ui/Dialog.svelte b/src/lib/ui/Dialog.svelte
new file mode 100644
index 0000000..e42a228
--- /dev/null
+++ b/src/lib/ui/Dialog.svelte
@@ -0,0 +1,55 @@
+
+
+
+
+ {@render children()}
+
+
diff --git a/src/lib/ui/MenuItem.svelte b/src/lib/ui/MenuItem.svelte
new file mode 100644
index 0000000..4e18280
--- /dev/null
+++ b/src/lib/ui/MenuItem.svelte
@@ -0,0 +1,25 @@
+
+
+
+ {@render children()}
+
diff --git a/src/lib/ui/MenuSurface.svelte b/src/lib/ui/MenuSurface.svelte
new file mode 100644
index 0000000..970e215
--- /dev/null
+++ b/src/lib/ui/MenuSurface.svelte
@@ -0,0 +1,57 @@
+
+
+
+ {@render children()}
+
diff --git a/src/lib/ui/Pill.svelte b/src/lib/ui/Pill.svelte
new file mode 100644
index 0000000..66541df
--- /dev/null
+++ b/src/lib/ui/Pill.svelte
@@ -0,0 +1,15 @@
+
+
+
+ {@render children()}
+
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index 030926a..f9820c7 100644
--- a/src/routes/+page.svelte
+++ b/src/routes/+page.svelte
@@ -10,7 +10,10 @@
import { emptyMetrics, withMetrics } from "$lib/import";
import SubnetTable from "$lib/SubnetTable.svelte";
import ExportMenu from "$lib/ExportMenu.svelte";
+ import SelectionActions from "$lib/SelectionActions.svelte";
import UpdateControl from "$lib/UpdateControl.svelte";
+ import Button from "$lib/ui/Button.svelte";
+ import Dialog from "$lib/ui/Dialog.svelte";
let rows = $state([]);
let hovering = $state(false);
@@ -25,7 +28,6 @@
let failed = $state(false);
let working = $state("");
let draft = $state>({});
- let targetField = $state(null);
let filtering = $state(false);
let filterCount = $state(0);
let selectedCidrs = $state>(new Set());
@@ -46,7 +48,6 @@
const locked = $derived(busy || running || updating);
const selectedScope = $derived(selectedCidrs.size > 0 ? [...selectedCidrs] : null);
- const selectedLabel = $derived(selectedCidrs.size > 0 ? `${selectedCidrs.size} selected` : "");
$effect(() => {
const available = new Set(rows.map((row) => row.cidr));
@@ -80,12 +81,6 @@
failed = false;
}
- $effect(() => {
- if (asking) {
- targetField?.focus();
- }
- });
-
onMount(() => {
let disposed = false;
let stopDrop = () => {};
@@ -187,6 +182,7 @@
}
async function exportFiles(cidrs: string[] | null = selectedScope) {
+ exportMenuOpen = false;
if (locked || rows.length === 0) {
return;
}
@@ -252,7 +248,6 @@
function closeExportMenu() {
exportMenuOpen = false;
- exportMenuTrigger?.focus();
}
$effect(() => {
@@ -309,6 +304,7 @@
if (locked || rows.length === 0) {
return;
}
+ exportMenuOpen = false;
probeCidrs = selectedScope;
asking = true;
}
@@ -322,6 +318,7 @@
if (locked) {
return;
}
+ exportMenuOpen = false;
probeCidrs = cidrs;
if (target.trim()) {
await confirmProbe();
@@ -453,18 +450,8 @@
{/if}
{#if asking}
- {
- if (event.key === "Escape") {
- event.preventDefault();
- cancelProbe();
- }
- }}
- >
+
+
{/if}
{#if rows.length === 0}
proxybench
Drop a .txt file or a folder of .txt files.
-
- Open files
-
+ Open files
{#if busy}
{working || "Importing…"}
{/if}
@@ -527,67 +494,46 @@
{#if busy && !running}
{working || "Working…"}
{:else}
- {rows.length} {rows.length === 1 ? "subnet" : "subnets"}{selectedLabel
- ? ` · ${selectedLabel}`
- : ""}
+ {rows.length} {rows.length === 1 ? "subnet" : "subnets"}
{/if}
- (filtering = !filtering)}
>
Filter{filterCount > 0 ? ` ${filterCount}` : ""}
-
-
- Open files
-
-
- void exportFiles()}
- >
- Export
-
- (exportMenuOpen = !exportMenuOpen)}
- >
- ▾
-
- {#if exportMenuOpen}
- void exportAycd()}
- onDismiss={closeExportMenu}
- />
- {/if}
-
-
- {selectedLabel ? "Probe selected" : "Probe all"}
-
+
+ Open files
+ {#if selectedCidrs.size > 0}
+ void exportFiles()}
+ onExportAycd={() => void exportAycd()}
+ onProbe={openProbe}
+ />
+ {:else}
+
+ void exportFiles()}>Export
+ (exportMenuOpen = !exportMenuOpen)}
+ >▾
+ {#if exportMenuOpen}
+ void exportAycd()} onDismiss={closeExportMenu} />
+ {/if}
+
+ Probe all
+ {/if}
{paging.label}
- paging.previous()}
- >
- ‹
-
- ‹
+ paging.next()}
- >
- ›
-
+ >›
{/if}
|