Skip to content
Merged
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
28 changes: 21 additions & 7 deletions src/app.css
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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 {
Expand All @@ -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);
}
}
37 changes: 6 additions & 31 deletions src/lib/ExportMenu.svelte
Original file line number Diff line number Diff line change
@@ -1,35 +1,10 @@
<script lang="ts">
let { locked, onAycd, onDismiss }: { locked: boolean; onAycd: () => void; onDismiss: () => void } = $props();
import MenuItem from "./ui/MenuItem.svelte";
import MenuSurface from "./ui/MenuSurface.svelte";

let item = $state<HTMLButtonElement | null>(null);

$effect(() => {
item?.focus();
});
let { locked, trigger, onAycd, onDismiss }: { locked: boolean; trigger: HTMLElement | null; onAycd: () => void; onDismiss: () => void } = $props();
</script>

<div
class="absolute right-0 top-full z-30 mt-1 min-w-48 rounded-md border border-line bg-raised py-1 text-xs text-text shadow-none"
role="menu"
tabindex="-1"
onkeydown={(event) => {
if (event.key === "Escape") {
event.preventDefault();
onDismiss();
} else if (["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key)) {
event.preventDefault();
item?.focus();
}
}}
>
<button
bind:this={item}
type="button"
class="block w-full px-3 py-1.5 text-left hover:bg-bg disabled:opacity-40"
disabled={locked}
role="menuitem"
onclick={onAycd}
>
Export for AYCD (.json)
</button>
</div>
<MenuSurface class="absolute right-0 top-full mt-1 min-w-48" {trigger} {onDismiss}>
<MenuItem disabled={locked} onclick={onAycd}>Export for AYCD (.json)</MenuItem>
</MenuSurface>
6 changes: 3 additions & 3 deletions src/lib/Mark.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script lang="ts">
import Pill from "./ui/Pill.svelte";

let { value }: { value: string } = $props();
</script>

<span class="inline-flex h-[22px] max-w-full items-center truncate rounded-md border border-line bg-raised px-1.5 font-mono text-xs text-text">
{value}
</span>
<Pill tone="country">{value}</Pill>
66 changes: 13 additions & 53 deletions src/lib/RowMenu.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
<script lang="ts">
import MenuItem from "./ui/MenuItem.svelte";
import MenuSurface from "./ui/MenuSurface.svelte";

let {
x,
y,
trigger,
locked,
onProbe,
onExport,
Expand All @@ -11,6 +15,7 @@
}: {
x: number;
y: number;
trigger: HTMLElement | null;
locked: boolean;
onProbe: () => void;
onExport: () => void;
Expand All @@ -23,64 +28,19 @@

$effect(() => {
const node = root;
if (!node) {
return;
}
if (!node) return;
const left = Math.min(x, window.innerWidth - node.offsetWidth - 8);
const top = Math.min(y, window.innerHeight - node.offsetHeight - 8);
node.style.left = `${Math.max(8, left)}px`;
node.style.top = `${Math.max(8, top)}px`;
node.focus();
});
</script>

<div
bind:this={root}
class="fixed z-30 min-w-32 rounded-md border border-line bg-raised py-1 text-xs text-text shadow-none"
style="left: {x}px; top: {y}px"
role="menu"
tabindex="-1"
onkeydown={(event) => {
if (event.key === "Escape") {
event.preventDefault();
onDismiss();
}
}}
>
<button
type="button"
class="block w-full px-3 py-1.5 text-left hover:bg-bg disabled:opacity-40"
disabled={locked}
role="menuitem"
onclick={onProbe}
>
Probe
</button>
<button
type="button"
class="block w-full px-3 py-1.5 text-left hover:bg-bg disabled:opacity-40"
disabled={locked}
role="menuitem"
onclick={onExport}
>
Export
</button>
<button
type="button"
class="block w-full px-3 py-1.5 text-left hover:bg-bg disabled:opacity-40"
disabled={locked}
role="menuitem"
onclick={onExportAycd}
>
Export for AYCD (.json)
</button>
<button
type="button"
class="block w-full px-3 py-1.5 text-left text-bad hover:bg-bg disabled:opacity-40"
disabled={locked}
role="menuitem"
onclick={onRemove}
>
Remove
</button>
<div bind:this={root} class="fixed z-30" style="left: {x}px; top: {y}px">
<MenuSurface class="min-w-32" {trigger} {onDismiss}>
<MenuItem disabled={locked} onclick={onProbe}>Probe</MenuItem>
<MenuItem disabled={locked} onclick={onExport}>Export</MenuItem>
<MenuItem disabled={locked} onclick={onExportAycd}>Export for AYCD (.json)</MenuItem>
<MenuItem disabled={locked} danger onclick={onRemove}>Remove</MenuItem>
</MenuSurface>
</div>
47 changes: 47 additions & 0 deletions src/lib/SelectionActions.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<script lang="ts">
import Button from "./ui/Button.svelte";
import MenuItem from "./ui/MenuItem.svelte";
import MenuSurface from "./ui/MenuSurface.svelte";

let { locked, onExport, onExportAycd, onProbe }: { locked: boolean; onExport: () => void; onExportAycd: () => void; onProbe: () => void } = $props();

let menuOpen = $state(false);
let split = $state<HTMLDivElement | null>(null);
let trigger = $state<HTMLButtonElement | null>(null);

$effect(() => {
if (!menuOpen) return;
function dismiss(event: MouseEvent) {
if (split && event.target instanceof Node && !split.contains(event.target)) menuOpen = false;
}
window.addEventListener("click", dismiss);
return () => window.removeEventListener("click", dismiss);
});

function closeMenu() {
menuOpen = false;
}

function exportText() {
closeMenu();
onExport();
}

function probe() {
closeMenu();
onProbe();
}
</script>

<div class="flex items-center gap-1.5">
<div bind:this={split} class="relative flex">
<Button variant="outline" joined="start" disabled={locked} onclick={exportText}>Export</Button>
<Button bind:ref={trigger} variant="outline" size="narrowIcon" joined="end" aria-label="More export options" aria-expanded={menuOpen} aria-haspopup="menu" disabled={locked} onclick={() => (menuOpen = !menuOpen)}>▾</Button>
{#if menuOpen}
<MenuSurface class="absolute right-0 top-full mt-1 min-w-48" {trigger} onDismiss={closeMenu}>
<MenuItem disabled={locked} onclick={() => { closeMenu(); onExportAycd(); }}>Export for AYCD (.json)</MenuItem>
</MenuSurface>
{/if}
</div>
<Button variant="primary" disabled={locked} onclick={probe}>Probe selected</Button>
</div>
4 changes: 1 addition & 3 deletions src/lib/SubnetCell.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
<script lang="ts">
import Mark from "./Mark.svelte";

let { cidr }: { cidr: string } = $props();
</script>

<Mark value={cidr} />
<span class="font-mono text-xs text-text">{cidr}</span>
59 changes: 55 additions & 4 deletions src/lib/SubnetTable.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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<string | null>(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) {
Expand Down Expand Up @@ -459,12 +506,13 @@
<div class="flex h-8 items-center">
{#if header.column.getCanFilter()}
<input
class="block h-6 w-full rounded-md bg-raised px-1 text-xs font-normal text-text focus:outline-none focus:ring-1 focus:ring-accent {numericIds.has(
class="block h-6 w-full rounded-[var(--radius-control)] border border-transparent bg-raised px-1 text-xs font-normal text-text focus:border-line focus:outline-none focus:ring-1 focus:ring-accent {numericIds.has(
header.column.id,
)
? 'text-right'
: ''}"
value={String(header.column.getFilterValue() ?? "")}
aria-label={`Filter ${String(header.column.columnDef.header)}`}
oninput={(event) => {
header.column.setFilterValue(event.currentTarget.value);
table.setPageIndex(0);
Expand All @@ -485,10 +533,12 @@
<tr
class="group h-8 border-t border-line {selectedCidrs.has(row.original.cidr)
? 'bg-accent/10 shadow-[inset_3px_0_0_var(--color-accent)] hover:bg-accent/15'
: 'hover:bg-raised'}"
: 'hover:bg-raised/80'} focus-visible:relative focus-visible:z-10 focus-visible:outline-none focus-visible:shadow-[inset_0_0_0_1px_var(--color-accent)]"
tabindex="0"
aria-selected={selectedCidrs.has(row.original.cidr)}
onclick={(event) => 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)}
<td
Expand Down Expand Up @@ -527,6 +577,7 @@
<RowMenu
x={menu.x}
y={menu.y}
trigger={menu.trigger}
{locked}
onProbe={() => {
const cidr = menu?.cidr;
Expand Down
6 changes: 4 additions & 2 deletions src/lib/TagsCell.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,10 @@
{#each tags as tag}
<button
type="button"
class="inline-flex h-[22px] max-w-[104px] shrink-0 items-center rounded-md border border-line bg-raised px-1.5 font-mono text-xs text-text hover:text-muted disabled:opacity-40"
class="inline-flex h-[22px] max-w-[104px] shrink-0 items-center rounded-[var(--radius-control)] border border-line bg-raised px-1.5 font-mono text-xs text-text hover:border-muted hover:text-muted disabled:opacity-40"
disabled={locked}
title={tag}
aria-label={`Remove tag ${tag}`}
onclick={() => onRemove(cidr, tag)}
>
<span class="truncate">{tag}</span>
Expand All @@ -35,8 +36,9 @@
{/each}
</div>
<input
class="h-6 w-20 shrink-0 rounded-md bg-transparent px-1 text-xs placeholder:text-faint focus:outline-none focus:ring-1 focus:ring-accent disabled:opacity-40"
class="h-6 w-20 shrink-0 rounded-[var(--radius-control)] bg-transparent px-1 text-xs placeholder:text-faint focus:outline-none focus:ring-1 focus:ring-accent disabled:opacity-40"
placeholder="add"
aria-label={`Add tag to ${cidr}`}
disabled={locked}
bind:value={draft[cidr]}
onkeydown={(event) => {
Expand Down
Loading