From 5c3eed296a2675def40235d53370a58ebcb792b9 Mon Sep 17 00:00:00 2001 From: Josh Creek <8179928+jcreek@users.noreply.github.com> Date: Wed, 25 Feb 2026 20:58:33 +0000 Subject: [PATCH] feat(#5): replace icon key inputs with searchable modal icon picker --- src/lib/components/NetworkDiagram.svelte | 346 +++++++++++++++++------ 1 file changed, 262 insertions(+), 84 deletions(-) diff --git a/src/lib/components/NetworkDiagram.svelte b/src/lib/components/NetworkDiagram.svelte index e211495..6c863b3 100644 --- a/src/lib/components/NetworkDiagram.svelte +++ b/src/lib/components/NetworkDiagram.svelte @@ -4,7 +4,7 @@ import cytoscape from 'cytoscape'; import dagre from 'cytoscape-dagre'; - import { listIconDefinitions } from '$lib/config/iconRegistry'; + import { listIconDefinitions, resolveIconPath } from '$lib/config/iconRegistry'; import { cloneNetworkData, createEmptyDevice, @@ -95,6 +95,7 @@ let mapControlsCollapsed = false; const iconDefinitions = listIconDefinitions(); + const iconResultLimit = 100; let tooltip = { visible: false, text: '', @@ -112,13 +113,22 @@ }; const tooltipViewportPadding = 8; + function normalizeIconSearchValue(value: string): string { + return value.toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim(); + } + + $: normalizedIconSearch = normalizeIconSearchValue(iconSearch); + $: iconSearchTokens = normalizedIconSearch.split(' ').filter(Boolean); $: filteredIconDefinitions = iconDefinitions.filter((icon) => { - const needle = iconSearch.trim().toLowerCase(); - if (!needle) { + if (iconSearchTokens.length === 0) { return true; } - return icon.label.toLowerCase().includes(needle) || icon.key.toLowerCase().includes(needle); + const searchable = normalizeIconSearchValue(`${icon.label} ${icon.key}`); + return ( + iconSearchTokens.every((token) => searchable.includes(token)) + ); }); + $: visibleIconDefinitions = filteredIconDefinitions.slice(0, iconResultLimit); $: selectedMachine = selectedTarget?.type === 'machine' ? networkData.machines[selectedTarget.index] : null; @@ -145,6 +155,22 @@ Object.keys(machineVmIndex) .filter((hostId) => (machineVmIndex[hostId]?.vmCount ?? 0) > 0) .every((hostId) => isHostCollapsed(hostId)); + $: selectedTargetIconKey = + selectedTarget?.type === 'machine' + ? selectedMachine?.iconKey ?? '' + : selectedTarget?.type === 'device' + ? selectedDevice?.iconKey ?? '' + : selectedTarget?.type === 'vm' + ? selectedVm?.iconKey ?? '' + : ''; + $: selectedTargetIconPath = resolveIconPath(selectedTargetIconKey || undefined); + $: addModalIconKey = + addModalKind === 'machine' + ? newMachineDraft.iconKey ?? '' + : addModalKind === 'device' + ? newDeviceDraft.iconKey ?? '' + : ''; + $: addModalIconPath = resolveIconPath(addModalIconKey || undefined); function equalsIgnoreCase(a: string, b: string): boolean { return a.localeCompare(b, undefined, { sensitivity: 'accent' }) === 0; @@ -351,20 +377,20 @@ if (selectedTarget.type === 'machine') { if (!networkData.machines[selectedTarget.index]) { - selectedTarget = null; + closeSelectedTargetModal(); } return; } if (selectedTarget.type === 'device') { if (!networkData.devices[selectedTarget.index]) { - selectedTarget = null; + closeSelectedTargetModal(); } return; } if (!networkData.machines[selectedTarget.machineIndex]?.software.vms[selectedTarget.vmIndex]) { - selectedTarget = null; + closeSelectedTargetModal(); } } @@ -433,8 +459,8 @@ 'background-image': 'data(iconUrl)', 'background-fit': 'contain', 'background-clip': 'none', - 'background-width': '56%', - 'background-height': '56%', + 'background-width': 'auto', + 'background-height': 'auto', 'background-position-y': '30%' } }, @@ -1014,6 +1040,56 @@ applyDraft(next, { autosave: options?.autosave ?? true }); } + function setSelectedTargetIconKey(nextKey: string) { + const normalized = nextKey.trim(); + const target = selectedTarget; + if (!target) { + return; + } + + if (target.type === 'machine') { + const index = target.index; + mutateDraft( + (draft) => { + draft.machines[index].iconKey = normalized || undefined; + }, + { autosave: true } + ); + return; + } + + if (target.type === 'device') { + const index = target.index; + mutateDraft( + (draft) => { + draft.devices[index].iconKey = normalized || undefined; + }, + { autosave: true } + ); + return; + } + + const machineIndex = target.machineIndex; + const vmIndex = target.vmIndex; + mutateDraft( + (draft) => { + draft.machines[machineIndex].software.vms[vmIndex].iconKey = normalized || undefined; + }, + { autosave: true } + ); + } + + function setAddModalIconKey(nextKey: string) { + const normalized = nextKey.trim(); + if (addModalKind === 'machine') { + newMachineDraft.iconKey = normalized || undefined; + return; + } + if (addModalKind === 'device') { + newDeviceDraft.iconKey = normalized || undefined; + } + } + function nextUniqueName(base: string, existing: string[]): string { const normalized = new Set(existing.map((name) => name.toLowerCase())); if (!normalized.has(base.toLowerCase())) { @@ -1195,7 +1271,7 @@ return; } applyDraft(deleteOwner(networkData, 'machine', machine.machineName)); - selectedTarget = null; + closeSelectedTargetModal(); deleteTarget = null; } @@ -1205,7 +1281,7 @@ return; } applyDraft(deleteOwner(networkData, 'device', device.name)); - selectedTarget = null; + closeSelectedTargetModal(); deleteTarget = null; } @@ -1213,7 +1289,7 @@ mutateDraft((draft) => { draft.machines[machineIndex]?.software.vms.splice(vmIndex, 1); }); - selectedTarget = null; + closeSelectedTargetModal(); deleteTarget = null; } @@ -1248,7 +1324,7 @@ }); selectedTarget = { type: 'device', index: networkData.devices.length - 1 }; } - addModalKind = null; + closeAddEntityModal(); } function onThemeToggleChange(event: Event) { @@ -1267,6 +1343,20 @@ mapControlsCollapsed = !mapControlsCollapsed; } + function clearIconSearch() { + iconSearch = ''; + } + + function closeSelectedTargetModal() { + selectedTarget = null; + clearIconSearch(); + } + + function closeAddEntityModal() { + addModalKind = null; + clearIconSearch(); + } + function saveStateLabel(): string { if (saveState === 'saved') { return writable ? 'Saved' : 'Read-only'; @@ -1424,7 +1514,7 @@ cy.on('tap', (event) => { if (event.target === cy) { - selectedTarget = null; + closeSelectedTargetModal(); } }); @@ -1628,13 +1718,45 @@ : selectedVm?.name ?? 'Virtual Machine' } maxWidth="880px" - on:close={() => (selectedTarget = null)} + on:close={closeSelectedTargetModal} > {#if selectedTarget?.type === 'machine' && selectedMachine}
@@ -1702,23 +1824,6 @@ )} /> -
@@ -1829,7 +1934,6 @@ Icon mutateDraft(() => undefined, { autosave: true })} /> @@ -2007,22 +2111,6 @@ )} > -
@@ -2179,24 +2267,6 @@ )} /> -
+ {#if filteredIconDefinitions.length === 0} +

No icons match your search.

+ {/if}
{#if addModalKind === 'machine'}
@@ -2245,10 +2347,6 @@ Operating System -
{:else if addModalKind === 'device'}
@@ -2268,10 +2366,6 @@ Notes -
{/if} @@ -2320,12 +2414,6 @@ - - {#each filteredIconDefinitions as icon (icon.key)} - - {/each} - -