diff --git a/apps/web/components.json b/apps/web/components.json new file mode 100644 index 00000000..44fad997 --- /dev/null +++ b/apps/web/components.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://shadcn-svelte.com/schema.json", + "tailwind": { + "css": "src/app.css", + "baseColor": "neutral" + }, + "aliases": { + "components": "$lib/components", + "utils": "$lib/utils", + "ui": "$lib/components/ui", + "hooks": "$lib/hooks", + "lib": "$lib" + }, + "typescript": true, + "registry": "https://shadcn-svelte.com/registry", + "style": "vega", + "iconLibrary": "lucide", + "menuColor": "default", + "menuAccent": "subtle" +} diff --git a/apps/web/package.json b/apps/web/package.json index 7eb112cd..20db95d3 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -31,6 +31,7 @@ "@cloudflare/workers-types": "^4.20260511.1", "@eslint/compat": "^1.2.5", "@eslint/js": "^9.18.0", + "@internationalized/date": "^3.12.0", "@playwright/test": "1.52.0", "@sveltejs/adapter-cloudflare": "^7.2.8", "@sveltejs/kit": "^2.16.0", @@ -38,6 +39,8 @@ "@tailwindcss/vite": "^4.0.0", "@types/better-sqlite3": "^7.6.13", "@types/node": "^22.15.17", + "bits-ui": "^2.16.3", + "clsx": "^2.1.1", "eslint": "^9.18.0", "eslint-config-prettier": "^10.0.1", "eslint-plugin-svelte": "^3.0.0", @@ -45,9 +48,13 @@ "prettier": "^3.8.1", "prettier-plugin-svelte": "^3.5.1", "prettier-plugin-tailwindcss": "^0.7.2", + "shadcn-svelte": "^1.5.0", "svelte": "^5.0.0", "svelte-check": "^4.0.0", + "tailwind-merge": "^3.6.0", + "tailwind-variants": "^3.3.1", "tailwindcss": "^4.0.0", + "tw-animate-css": "^1.4.0", "typescript": "^5.0.0", "typescript-eslint": "^8.20.0", "vite": "^6.2.6", @@ -55,7 +62,7 @@ "wrangler": "^4.90.0" }, "dependencies": { - "@lucide/svelte": "^0.511.0", + "@lucide/svelte": "^1.33.0", "better-sqlite3": "^12.6.2", "chart.js": "^4.4.9", "chartjs-plugin-annotation": "^3.1.0", diff --git a/apps/web/src/app.css b/apps/web/src/app.css index 6191ce8f..5f967fef 100644 --- a/apps/web/src/app.css +++ b/apps/web/src/app.css @@ -1,17 +1,51 @@ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap'); @import 'tailwindcss'; +@import 'tw-animate-css'; +@import 'shadcn-svelte/tailwind.css'; @custom-variant dark (&:where(.dark, .dark *)); @theme { --font-body: 'Inter', sans-serif; - - --color-cisco-blue: #009edc; - - --color-dark-bg: #0a0a0a; - --color-dark-surface: #111111; - --color-dark-subtle: #1a1a1a; - --color-dark-border: #2d2d2d; + --font-sans: 'Inter', sans-serif; + --color-sidebar-ring: var(--sidebar-ring); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar: var(--sidebar); + --color-chart-5: var(--chart-5); + --color-chart-4: var(--chart-4); + --color-chart-3: var(--chart-3); + --color-chart-2: var(--chart-2); + --color-chart-1: var(--chart-1); + --color-ring: var(--ring); + --color-input: var(--input); + --color-border: var(--border); + --color-destructive: var(--destructive); + --color-accent-foreground: var(--accent-foreground); + --color-accent: var(--accent); + --color-muted-foreground: var(--muted-foreground); + --color-muted: var(--muted); + --color-secondary-foreground: var(--secondary-foreground); + --color-secondary: var(--secondary); + --color-primary-foreground: var(--primary-foreground); + --color-primary: var(--primary); + --color-popover-foreground: var(--popover-foreground); + --color-popover: var(--popover); + --color-card-foreground: var(--card-foreground); + --color-card: var(--card); + --color-foreground: var(--foreground); + --color-background: var(--background); + --radius-sm: calc(var(--radius) * 0.6); + --radius-md: calc(var(--radius) * 0.8); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) * 1.4); + --radius-2xl: calc(var(--radius) * 1.8); + --radius-3xl: calc(var(--radius) * 2.2); + --radius-4xl: calc(var(--radius) * 2.6); } :root { @@ -21,6 +55,38 @@ --chart-tooltip-bg: rgba(17, 24, 39, 0.9); --chart-tooltip-text-color: #f9fafb; --chart-tooltip-border-color: rgba(107, 114, 128, 0.7); + --background: #ffffff; + --foreground: #111827; + --card: #ffffff; + --card-foreground: #111827; + --popover: #ffffff; + --popover-foreground: #111827; + --primary: #2563eb; + --primary-foreground: #ffffff; + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: #f3f4f6; + --muted-foreground: #6b7280; + --accent: #f3f4f6; + --accent-foreground: #111827; + --destructive: #dc2626; + --border: #e5e7eb; + --input: #e5e7eb; + --ring: #2563eb; + --chart-1: oklch(0.87 0 0); + --chart-2: oklch(0.556 0 0); + --chart-3: oklch(0.439 0 0); + --chart-4: oklch(0.371 0 0); + --chart-5: oklch(0.269 0 0); + --radius: 0.625rem; + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); } .dark { @@ -30,6 +96,37 @@ --chart-tooltip-bg: rgba(3, 7, 18, 0.92); --chart-tooltip-text-color: #f3f4f6; --chart-tooltip-border-color: rgba(75, 85, 99, 0.85); + --background: #0a0a0a; + --foreground: #fafafa; + --card: #111111; + --card-foreground: #fafafa; + --popover: #111111; + --popover-foreground: #fafafa; + --primary: #2563eb; + --primary-foreground: #ffffff; + --secondary: #1a1a1a; + --secondary-foreground: oklch(0.985 0 0); + --muted: #1a1a1a; + --muted-foreground: #a3a3a3; + --accent: #1a1a1a; + --accent-foreground: #fafafa; + --destructive: #dc2626; + --border: #2d2d2d; + --input: #2d2d2d; + --ring: #2563eb; + --chart-1: oklch(0.87 0 0); + --chart-2: oklch(0.556 0 0); + --chart-3: oklch(0.439 0 0); + --chart-4: oklch(0.371 0 0); + --chart-5: oklch(0.269 0 0); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.556 0 0); } html { @@ -45,3 +142,15 @@ body { .app-shell__main { scrollbar-gutter: stable; } + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } + html { + @apply font-sans; + } +} diff --git a/apps/web/src/lib/components/charts/SpectrumStatsChart.svelte b/apps/web/src/lib/components/charts/BreakdownChart.svelte similarity index 57% rename from apps/web/src/lib/components/charts/SpectrumStatsChart.svelte rename to apps/web/src/lib/components/charts/BreakdownChart.svelte index 3200b24c..2e013947 100644 --- a/apps/web/src/lib/components/charts/SpectrumStatsChart.svelte +++ b/apps/web/src/lib/components/charts/BreakdownChart.svelte @@ -1,28 +1,50 @@ - -
-
-

Spectrum

- -
-
-
-
- {#if (props.availableRouters ?? []).length === 0} - {#each Array(4) as _, index (index)} - - {/each} - {:else} - {#each props.availableRouters ?? [] as routerName (routerName)} -
- - -
+ {/if} + {/snippet} + diff --git a/apps/web/src/lib/components/charts/ChartCard.svelte b/apps/web/src/lib/components/charts/ChartCard.svelte new file mode 100644 index 00000000..d41bba1c --- /dev/null +++ b/apps/web/src/lib/components/charts/ChartCard.svelte @@ -0,0 +1,114 @@ + + + + +

{title}

+ +
+ + + {@render controls?.()} + + + +
diff --git a/apps/web/src/lib/components/charts/ChartContainer.svelte b/apps/web/src/lib/components/charts/ChartContainer.svelte index 46334e5b..a4433b2f 100644 --- a/apps/web/src/lib/components/charts/ChartContainer.svelte +++ b/apps/web/src/lib/components/charts/ChartContainer.svelte @@ -6,8 +6,9 @@ import { resolve } from '$app/paths'; import { Chart } from './chart-registry'; import { crosshairStore } from '$lib/stores/crosshair'; - import { rangeSelectionStore, type RangeSelectionState } from '$lib/stores/rangeSelection'; + import { rangeSelection } from '$lib/stores/rangeSelection.svelte'; import { theme } from '$lib/stores/theme.svelte'; + import { cancelDrawFrame, requestDrawFrame } from '$lib/utils/animation-frame'; import { SvelteSet } from 'svelte/reactivity'; import { NETFLOW_DATA_OPTION_FIELDS } from '$lib/components/netflow/constants'; import { @@ -66,21 +67,16 @@ let rangeDrag = $state(createRangeDragState()); let selectionLeft = $derived(Math.min(rangeDrag.dragStartX, rangeDrag.dragCurrentX)); let selectionWidth = $derived(Math.abs(rangeDrag.dragCurrentX - rangeDrag.dragStartX)); - let mirroredRange = $state(null); + let mirroredRange = $derived(rangeSelection.selection); + let rangeMoveFrame: number | null = null; + let pendingRangeMoveEvent: MouseEvent | null = null; type MetricFamily = 'flows' | 'packets' | 'bytes'; - $effect(() => { - const unsubscribe = rangeSelectionStore.subscribe((value) => { - mirroredRange = value; - }); - return unsubscribe; - }); - function publishRangeSelection(startIndex: number, endIndex: number) { const labels = getSelectionLabels(chart, startIndex, endIndex); if (!labels) return; - rangeSelectionStore.set({ sourceChartId: CHART_ID, ...labels }); + rangeSelection.set({ sourceChartId: CHART_ID, ...labels }); } function applyRangeDrilldown(startIndex: number, endIndex: number) { @@ -110,16 +106,49 @@ } function handleRangeMouseDown(event: MouseEvent) { + cancelPendingRangeMove(); beginRangeDrag(rangeDrag, event, chartCanvas, chart, publishRangeSelection); } + function applyPendingRangeMove() { + rangeMoveFrame = null; + const event = pendingRangeMoveEvent; + pendingRangeMoveEvent = null; + if (event) { + updateRangeDrag(rangeDrag, event, chartCanvas, chart, publishRangeSelection); + } + } + + function cancelPendingRangeMove() { + if (rangeMoveFrame !== null) { + cancelDrawFrame(rangeMoveFrame); + rangeMoveFrame = null; + } + pendingRangeMoveEvent = null; + } + + function flushPendingRangeMove() { + if (rangeMoveFrame === null) { + return; + } + cancelDrawFrame(rangeMoveFrame); + applyPendingRangeMove(); + } + function handleRangeMouseMove(event: MouseEvent) { - updateRangeDrag(rangeDrag, event, chartCanvas, chart, publishRangeSelection); + if (!rangeDrag.isDraggingRange) { + return; + } + pendingRangeMoveEvent = event; + if (rangeMoveFrame === null) { + rangeMoveFrame = requestDrawFrame(applyPendingRangeMove); + } } function finishRangeSelection() { + flushPendingRangeMove(); endRangeDrag(rangeDrag, chart, applyRangeDrilldown); - rangeSelectionStore.set(null); + rangeSelection.clear(); } let mirroredSelectionStyle = $derived( @@ -813,9 +842,10 @@ }); onDestroy(() => { + cancelPendingRangeMove(); crosshairStore.unregister(CHART_ID); if (mirroredRange?.sourceChartId === CHART_ID) { - rangeSelectionStore.set(null); + rangeSelection.clear(); } chart?.destroy(); chart = null; @@ -858,13 +888,13 @@ {#if rangeDrag.isDraggingRange && selectionWidth >= MIN_DRAG_PIXELS}
{/if} {#if !rangeDrag.isDraggingRange && mirroredSelectionStyle !== null}
{/if} diff --git a/apps/web/src/lib/components/charts/CoverageStrip.svelte b/apps/web/src/lib/components/charts/CoverageStrip.svelte index c70640fd..2767142a 100644 --- a/apps/web/src/lib/components/charts/CoverageStrip.svelte +++ b/apps/web/src/lib/components/charts/CoverageStrip.svelte @@ -383,26 +383,23 @@ }); -
+
-

Coverage

+

Coverage

{#if loading} -
Loading coverage...
+
Loading coverage...
{:else if error}
{error}
{:else if visibleTimelines.length === 0} -
No coverage available
+
No coverage available
{:else}
import DragGrip from '$lib/components/common/DragGrip.svelte'; + import { Checkbox } from '$lib/components/ui/checkbox'; import MetricLinePanel, { type MetricLineSeries } from './MetricLinePanel.svelte'; import { dateStringToEpochPST } from '$lib/utils/timezone'; import { SvelteSet } from 'svelte/reactivity'; @@ -219,14 +220,14 @@ ); -
+
-

Flow Characteristics

-

+

Flow Characteristics

+

Weighted flow observations and exact unique port counts

@@ -234,25 +235,25 @@
{#if loading} -
+
Loading flow characteristics…
{:else if error} -
+
{error}
{:else}
-

Observations

+

Observations

{#each ['all', 'ipv4', 'ipv6'] as const as family (family)}
-
+
-

Unique Ports

-

+

Unique Ports

+

Cardinality is resolved from an exact logical source; separate sources are never added.

{#each ['ipv4', 'ipv6'] as const as family (family)} @@ -312,15 +313,13 @@ > {#each PORT_OPTIONS as option (`${option.side}-${option.range}`)} {/each}
diff --git a/apps/web/src/lib/components/charts/IPChart.svelte b/apps/web/src/lib/components/charts/IPChart.svelte deleted file mode 100644 index 9401954e..00000000 --- a/apps/web/src/lib/components/charts/IPChart.svelte +++ /dev/null @@ -1,872 +0,0 @@ - - -
-
-

Unique IP Counts

- -
-
-
- {#each IP_METRIC_OPTIONS as option (option.key)} - - {/each} -
- - -
-
diff --git a/apps/web/src/lib/components/charts/MetricLinePanel.svelte b/apps/web/src/lib/components/charts/MetricLinePanel.svelte index 099031d7..bbe9b580 100644 --- a/apps/web/src/lib/components/charts/MetricLinePanel.svelte +++ b/apps/web/src/lib/components/charts/MetricLinePanel.svelte @@ -184,9 +184,9 @@
-

{props.title}

+

{props.title}

{#if props.bucketStarts.length === 0 || props.series.length === 0} -
+
No data for this metric
{:else} diff --git a/apps/web/src/lib/components/charts/ProtocolChart.svelte b/apps/web/src/lib/components/charts/ProtocolChart.svelte deleted file mode 100644 index 379ae034..00000000 --- a/apps/web/src/lib/components/charts/ProtocolChart.svelte +++ /dev/null @@ -1,827 +0,0 @@ - - -
-
-

Unique Protocol Counts

- -
-
-
- {#each ['uniqueProtocolsIpv4', 'uniqueProtocolsIpv6'] as const as key (key)} - - {/each} -
- - -
-
diff --git a/apps/web/src/lib/components/charts/SpectrumChart.svelte b/apps/web/src/lib/components/charts/SpectrumChart.svelte index a7ab86d5..9d71f052 100644 --- a/apps/web/src/lib/components/charts/SpectrumChart.svelte +++ b/apps/web/src/lib/components/charts/SpectrumChart.svelte @@ -196,16 +196,14 @@
-
+
{#if data.metadata.uniqueIPCount && data.metadata.uniqueIPCount > 0} -

+

✓ Real NetFlow Data Analysis - {data.metadata.uniqueIPCount.toLocaleString()} unique IP addresses analyzed

{:else if data.metadata.uniqueIPCount !== -1} -

- ⚠ Using test data from MAAD sample set -

+

⚠ Using test data from MAAD sample set

{/if}
diff --git a/apps/web/src/lib/components/charts/StructureFunctionChart.svelte b/apps/web/src/lib/components/charts/StructureFunctionChart.svelte index 229e873f..8875733b 100644 --- a/apps/web/src/lib/components/charts/StructureFunctionChart.svelte +++ b/apps/web/src/lib/components/charts/StructureFunctionChart.svelte @@ -227,16 +227,14 @@
-
+
{#if data.metadata.uniqueIPCount && data.metadata.uniqueIPCount > 0} -

+

✓ Real NetFlow Data Analysis - {data.metadata.uniqueIPCount.toLocaleString()} unique IP addresses analyzed

{:else if data.metadata.uniqueIPCount !== -1} -

- ⚠ Using test data from MAAD sample set -

+

⚠ Using test data from MAAD sample set

{/if}
diff --git a/apps/web/src/lib/components/charts/breakdown-chart-config.ts b/apps/web/src/lib/components/charts/breakdown-chart-config.ts new file mode 100644 index 00000000..28d54919 --- /dev/null +++ b/apps/web/src/lib/components/charts/breakdown-chart-config.ts @@ -0,0 +1,156 @@ +import { + IP_METRIC_OPTIONS, + type IpGranularity, + type IpMetricKey, + type IpStatsBucket, + type ProtocolMetricKey, + type ProtocolStatsBucket +} from '$lib/types/types'; + +export type BreakdownChartKind = 'ip' | 'protocol' | 'spectrum'; +export type BreakdownMetricKey = IpMetricKey | ProtocolMetricKey; +export type LineBucketData = IpStatsBucket | ProtocolStatsBucket; + +export interface LineMetricConfig { + key: BreakdownMetricKey; + label: string; + seriesLabel: string; + color: { + hue: number; + saturation: number; + lightness: number; + }; +} + +export interface BreakdownChartConfig { + kind: BreakdownChartKind; + chartId: BreakdownChartKind; + title: string; + endpoint: string; + defaultGranularity: IpGranularity; + defaultMetrics: BreakdownMetricKey[]; + metrics: LineMetricConfig[]; + routerHueStep: number; + fillAlpha: number; + yAxisTitle: string; + formatYAxisTicks: boolean; + loadingCopy: string; + emptyCopy: string; + noMetricsCopy: string; + noSourceCopy: string; + fetchErrorCopy: string; + unexpectedErrorCopy: string; + canvasLabel: string; +} + +const ipMetricLabels: Record = { + saIpv4Count: 'Src IPv4', + daIpv4Count: 'Dst IPv4', + saIpv6Count: 'Src IPv6', + daIpv6Count: 'Dst IPv6' +}; + +const ipColors: Record = { + saIpv4Count: { hue: 130, saturation: 65, lightness: 58 }, + daIpv4Count: { hue: 130, saturation: 65, lightness: 38 }, + saIpv6Count: { hue: 25, saturation: 72, lightness: 60 }, + daIpv6Count: { hue: 25, saturation: 72, lightness: 40 } +}; + +const IP_CONFIG: BreakdownChartConfig = { + kind: 'ip', + chartId: 'ip', + title: 'Unique IP Counts', + endpoint: '/api/ip/stats', + defaultGranularity: '1d', + defaultMetrics: ['saIpv4Count', 'daIpv4Count'], + metrics: IP_METRIC_OPTIONS.map((option) => ({ + key: option.key, + label: option.label, + seriesLabel: ipMetricLabels[option.key], + color: ipColors[option.key] + })), + routerHueStep: 70, + fillAlpha: 0.18, + yAxisTitle: 'Unique IPs', + formatYAxisTicks: true, + loadingCopy: 'Loading IP data...', + emptyCopy: 'No IP data for the selected window.', + noMetricsCopy: 'Select at least one metric to display.', + noSourceCopy: 'Select at least one source to view IP statistics', + fetchErrorCopy: 'Failed to load IP statistics', + unexpectedErrorCopy: 'Unexpected error loading IP statistics', + canvasLabel: 'IP chart' +}; + +const PROTOCOL_CONFIG: BreakdownChartConfig = { + kind: 'protocol', + chartId: 'protocol', + title: 'Unique Protocol Counts', + endpoint: '/api/protocol/stats', + defaultGranularity: '1h', + defaultMetrics: ['uniqueProtocolsIpv4', 'uniqueProtocolsIpv6'], + metrics: [ + { + key: 'uniqueProtocolsIpv4', + label: 'Unique Protocols IPv4', + seriesLabel: 'IPv4', + color: { hue: 210, saturation: 70, lightness: 50 } + }, + { + key: 'uniqueProtocolsIpv6', + label: 'Unique Protocols IPv6', + seriesLabel: 'IPv6', + color: { hue: 35, saturation: 75, lightness: 48 } + } + ], + routerHueStep: 110, + fillAlpha: 0.2, + yAxisTitle: 'Unique Protocols', + formatYAxisTicks: false, + loadingCopy: 'Loading protocol data...', + emptyCopy: 'No protocol data for the selected window.', + noMetricsCopy: 'Select at least one metric to display.', + noSourceCopy: 'Select at least one source to view protocol statistics', + fetchErrorCopy: 'Failed to load protocol statistics', + unexpectedErrorCopy: 'Unexpected error loading protocol statistics', + canvasLabel: 'Protocol chart' +}; + +const SPECTRUM_CONFIG: BreakdownChartConfig = { + kind: 'spectrum', + chartId: 'spectrum', + title: 'Spectrum', + endpoint: '/api/netflow/spectrum-stats', + defaultGranularity: '1h', + defaultMetrics: [], + metrics: [], + routerHueStep: 0, + fillAlpha: 0, + yAxisTitle: 'alpha', + formatYAxisTicks: false, + loadingCopy: 'Loading spectrum data...', + emptyCopy: 'No spectrum data for the selected window.', + noMetricsCopy: '', + noSourceCopy: 'Select at least one source to view spectrum statistics', + fetchErrorCopy: 'Failed to load spectrum statistics', + unexpectedErrorCopy: 'Unexpected error loading spectrum statistics', + canvasLabel: 'Spectrum chart' +}; + +export const BREAKDOWN_CHART_CONFIGS: Record = { + ip: IP_CONFIG, + protocol: PROTOCOL_CONFIG, + spectrum: SPECTRUM_CONFIG +}; + +export function readLineMetric( + data: LineBucketData | null, + key: BreakdownMetricKey +): number | null { + if (!data || !(key in data)) { + return null; + } + const value = data[key as keyof LineBucketData]; + return typeof value === 'number' ? value : null; +} diff --git a/apps/web/src/lib/components/charts/chart-utils.ts b/apps/web/src/lib/components/charts/chart-utils.ts index 95800a7a..e41d6370 100644 --- a/apps/web/src/lib/components/charts/chart-utils.ts +++ b/apps/web/src/lib/components/charts/chart-utils.ts @@ -1,6 +1,6 @@ import type { Chart } from 'chart.js'; import type { GroupByOption, NetflowDataPoint } from '$lib/components/netflow/types.ts'; -import type { RangeSelectionState } from '$lib/stores/rangeSelection'; +import type { RangeSelectionState } from '$lib/stores/rangeSelection.svelte'; import { parseLabelToPSTComponents, parseLabelToDateForDrilldown, diff --git a/apps/web/src/lib/components/common/SegmentedControl.svelte b/apps/web/src/lib/components/common/SegmentedControl.svelte new file mode 100644 index 00000000..3147084f --- /dev/null +++ b/apps/web/src/lib/components/common/SegmentedControl.svelte @@ -0,0 +1,95 @@ + + + + +
+ {#each options as option (option.value)} + {#if option.disabledReason} + + + {#snippet child({ props })} + + {/snippet} + + {option.disabledReason} + + {:else} + + {/if} + {/each} +
diff --git a/apps/web/src/lib/components/datasets/DatasetTabs.svelte b/apps/web/src/lib/components/datasets/DatasetTabs.svelte index 4c84d4ef..b7bcfd80 100644 --- a/apps/web/src/lib/components/datasets/DatasetTabs.svelte +++ b/apps/web/src/lib/components/datasets/DatasetTabs.svelte @@ -1,30 +1,31 @@ -
- to + to
@@ -40,7 +40,7 @@ type="date" value={endDate} onchange={handleEndDateChange} - class="dark:border-dark-border dark:bg-dark-subtle min-w-[9.5rem] rounded-md border border-gray-300 px-3 py-1 text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none dark:text-gray-100 dark:[color-scheme:dark]" + class="border-input bg-background text-foreground focus-visible:ring-ring min-w-[9.5rem] rounded-md border px-3 py-1 text-sm focus-visible:ring-2 focus-visible:outline-none" />
diff --git a/apps/web/src/lib/components/filters/MetricSelector.svelte b/apps/web/src/lib/components/filters/MetricSelector.svelte index f5830d0d..91b8387f 100644 --- a/apps/web/src/lib/components/filters/MetricSelector.svelte +++ b/apps/web/src/lib/components/filters/MetricSelector.svelte @@ -1,4 +1,6 @@ -
-
-

Controls

-
- - + + +
+ Controls + + + {#snippet child({ props: triggerProps })} + + {/snippet} + + + {navigationTip} + +
-
+ -
+ - - -
- - {#each groupByOptions as option (option.value)} - {@const disabledReason = getGranularityDisabledReason(option)} -
- - {#if disabledReason} - - {/if} -
- {/each} -
+ + + props.onGroupByChange?.({ groupBy: value })} + class="min-w-[17rem] grid-cols-4" + buttonClass="px-3 py-1 text-sm" + /> - + -
-
+ + + diff --git a/apps/web/src/lib/components/filters/RouterFilter.svelte b/apps/web/src/lib/components/filters/RouterFilter.svelte index c3b70911..fa89b2b8 100644 --- a/apps/web/src/lib/components/filters/RouterFilter.svelte +++ b/apps/web/src/lib/components/filters/RouterFilter.svelte @@ -1,4 +1,6 @@
- Sources: + Sources:
{#if routerNames.length === 0} {#each Array(4) as _, index (index)} - +
+ diff --git a/apps/web/src/routes/+page.svelte b/apps/web/src/routes/+page.svelte index 1dca3c53..de9bc190 100644 --- a/apps/web/src/routes/+page.svelte +++ b/apps/web/src/routes/+page.svelte @@ -1,6 +1,8 @@
-

NetFlow Files

+

NetFlow Files

-
-

Navigate to File by Timestamp

-
-
- - -
-
- - -
- {error || ' '} + + +

Navigate to File by Timestamp

+
+ +
+
+ + +
+
+ + +
+ {error || ' '} +
+
+
+
-
- -
-
-

- Choose a dataset, then enter the exact 12-digit timestamp from NetFlow filenames (e.g., - `nfcapd.202601011200`). -

-
+

+ Choose a dataset, then enter the exact 12-digit timestamp from NetFlow filenames (e.g., + `nfcapd.202601011200`). +

+ +
diff --git a/bun.lock b/bun.lock index d257fb6b..7d37ac74 100644 --- a/bun.lock +++ b/bun.lock @@ -31,7 +31,7 @@ "name": "@atlantis/web", "version": "0.0.1", "dependencies": { - "@lucide/svelte": "^0.511.0", + "@lucide/svelte": "^1.33.0", "better-sqlite3": "^12.6.2", "chart.js": "^4.4.9", "chartjs-plugin-annotation": "^3.1.0", @@ -43,6 +43,7 @@ "@cloudflare/workers-types": "^4.20260511.1", "@eslint/compat": "^1.2.5", "@eslint/js": "^9.18.0", + "@internationalized/date": "^3.12.0", "@playwright/test": "1.52.0", "@sveltejs/adapter-cloudflare": "^7.2.8", "@sveltejs/kit": "^2.16.0", @@ -50,6 +51,8 @@ "@tailwindcss/vite": "^4.0.0", "@types/better-sqlite3": "^7.6.13", "@types/node": "^22.15.17", + "bits-ui": "^2.16.3", + "clsx": "^2.1.1", "eslint": "^9.18.0", "eslint-config-prettier": "^10.0.1", "eslint-plugin-svelte": "^3.0.0", @@ -57,9 +60,13 @@ "prettier": "^3.8.1", "prettier-plugin-svelte": "^3.5.1", "prettier-plugin-tailwindcss": "^0.7.2", + "shadcn-svelte": "^1.5.0", "svelte": "^5.0.0", "svelte-check": "^4.0.0", + "tailwind-merge": "^3.6.0", + "tailwind-variants": "^3.3.1", "tailwindcss": "^4.0.0", + "tw-animate-css": "^1.4.0", "typescript": "^5.0.0", "typescript-eslint": "^8.20.0", "vite": "^6.2.6", @@ -197,6 +204,12 @@ "@eslint/plugin-kit": ["@eslint/plugin-kit@0.6.1", "", { "dependencies": { "@eslint/core": "^1.1.1", "levn": "^0.4.1" } }, "sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ=="], + "@floating-ui/core": ["@floating-ui/core@1.8.0", "", { "dependencies": { "@floating-ui/utils": "^0.2.12" } }, "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ=="], + + "@floating-ui/dom": ["@floating-ui/dom@1.8.0", "", { "dependencies": { "@floating-ui/core": "^1.8.0", "@floating-ui/utils": "^0.2.12" } }, "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg=="], + + "@floating-ui/utils": ["@floating-ui/utils@0.2.12", "", {}, "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww=="], + "@gar/promisify": ["@gar/promisify@1.1.3", "", {}, "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw=="], "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], @@ -257,6 +270,8 @@ "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="], + "@internationalized/date": ["@internationalized/date@3.12.3", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-fuLX+3ZKLsxI73y8b01EG/WjHb6gE6weCqlfawPO27kBWGMh9G1yH6Csv1uU7/cac9H2GHmOMt6CjmuQ1aia4Q=="], + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], @@ -269,7 +284,7 @@ "@kurkle/color": ["@kurkle/color@0.3.4", "", {}, "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w=="], - "@lucide/svelte": ["@lucide/svelte@0.511.0", "", { "peerDependencies": { "svelte": "^5" } }, "sha512-aLCSPMUJmHlCuLXzXENXa4Z1NV2mN1iAZAFKk4bEbey+/MdsNlu+/DqwVkgW3Yvj6p8y8Vn5xZ2v9CLmPlA6Vw=="], + "@lucide/svelte": ["@lucide/svelte@1.33.0", "", { "peerDependencies": { "svelte": "^5" } }, "sha512-b+osTYG2V4dge5Lr7tnaTaahhy/4vH7Xb8zcqVjmctjwgkpXS0NNOJPkGzHHZoxtw/OO+2YAU28omeMfYCaawg=="], "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], @@ -379,6 +394,8 @@ "@sveltejs/vite-plugin-svelte-inspector": ["@sveltejs/vite-plugin-svelte-inspector@4.0.1", "", { "dependencies": { "debug": "^4.3.7" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^5.0.0", "svelte": "^5.0.0", "vite": "^6.0.0" } }, "sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw=="], + "@swc/helpers": ["@swc/helpers@0.5.23", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw=="], + "@tailwindcss/node": ["@tailwindcss/node@4.2.2", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.2" } }, "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA=="], "@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.2", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.2", "@tailwindcss/oxide-darwin-arm64": "4.2.2", "@tailwindcss/oxide-darwin-x64": "4.2.2", "@tailwindcss/oxide-freebsd-x64": "4.2.2", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.2", "@tailwindcss/oxide-linux-arm64-musl": "4.2.2", "@tailwindcss/oxide-linux-x64-gnu": "4.2.2", "@tailwindcss/oxide-linux-x64-musl": "4.2.2", "@tailwindcss/oxide-wasm32-wasi": "4.2.2", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.2", "@tailwindcss/oxide-win32-x64-msvc": "4.2.2" } }, "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg=="], @@ -531,6 +548,8 @@ "bindings": ["bindings@1.5.0", "", { "dependencies": { "file-uri-to-path": "1.0.0" } }, "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ=="], + "bits-ui": ["bits-ui@2.19.0", "", { "dependencies": { "@floating-ui/core": "^1.7.1", "@floating-ui/dom": "^1.7.1", "esm-env": "^1.1.2", "runed": "^0.35.1", "svelte-toolbelt": "^0.10.6", "tabbable": "^6.2.0" }, "peerDependencies": { "@internationalized/date": "^3.8.1", "svelte": "^5.33.0" } }, "sha512-dHDm5Jw2NZJgLRvHAwPeuR4BnZij+AqTIq33OqVfo+qiMXontoC/yIvGT5TZaviv7OuBdJ2QUy2JPQ2yKBvF6w=="], + "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], "blake3-wasm": ["blake3-wasm@2.1.5", "", {}, "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g=="], @@ -591,7 +610,7 @@ "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], - "commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="], + "commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], "common-ancestor-path": ["common-ancestor-path@2.0.0", "", {}, "sha512-dnN3ibLeoRf2HNC+OlCiNc5d2zxbLJXOtiZUudNFSXZrNSydxcCsSpRzXwfu7BBWCIfHPw+xTayeBvJCP/D8Ng=="], @@ -849,6 +868,8 @@ "ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], + "inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="], + "ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="], "iron-webcrypto": ["iron-webcrypto@1.2.1", "", {}, "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg=="], @@ -1263,6 +1284,8 @@ "set-cookie-parser": ["set-cookie-parser@3.1.0", "", {}, "sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw=="], + "shadcn-svelte": ["shadcn-svelte@1.5.0", "", { "dependencies": { "commander": "^14.0.0", "node-fetch-native": "^1.6.4", "postcss": "^8.5.10", "tailwind-merge": "^3.6.0" }, "peerDependencies": { "svelte": "^5.0.0" }, "bin": { "shadcn-svelte": "dist/index.mjs" } }, "sha512-fcxTeUwEIvELkL/h0Vg2i/9Sf6exAmLG5u3GXc0aN8CA+3fNffKQnfrTdMgkjUEmx9XP619I+wcJOvlr5aDWYQ=="], + "sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="], "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], @@ -1325,6 +1348,8 @@ "strip-literal": ["strip-literal@3.1.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg=="], + "style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="], + "suf-log": ["suf-log@2.5.3", "", { "dependencies": { "s.color": "0.0.15" } }, "sha512-KvC8OPjzdNOe+xQ4XWJV2whQA0aM1kGVczMQ8+dStAO6KfEB140JEVQ9dE76ONZ0/Ylf67ni4tILPJB41U0eow=="], "supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], @@ -1335,10 +1360,18 @@ "svelte-eslint-parser": ["svelte-eslint-parser@1.6.0", "", { "dependencies": { "eslint-scope": "^8.2.0", "eslint-visitor-keys": "^4.0.0", "espree": "^10.0.0", "postcss": "^8.4.49", "postcss-scss": "^4.0.9", "postcss-selector-parser": "^7.0.0", "semver": "^7.7.2" }, "peerDependencies": { "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" }, "optionalPeers": ["svelte"] }, "sha512-qoB1ehychT6OxEtQAqc/guSqLS20SlA53Uijl7x375s8nlUT0lb9ol/gzraEEatQwsyPTJo87s2CmKL9Xab+Uw=="], + "svelte-toolbelt": ["svelte-toolbelt@0.10.6", "", { "dependencies": { "clsx": "^2.1.1", "runed": "^0.35.1", "style-to-object": "^1.0.8" }, "peerDependencies": { "svelte": "^5.30.2" } }, "sha512-YWuX+RE+CnWYx09yseAe4ZVMM7e7GRFZM6OYWpBKOb++s+SQ8RBIMMe+Bs/CznBMc0QPLjr+vDBxTAkozXsFXQ=="], + "svgo": ["svgo@4.0.1", "", { "dependencies": { "commander": "^11.1.0", "css-select": "^5.1.0", "css-tree": "^3.0.1", "css-what": "^6.1.0", "csso": "^5.0.5", "picocolors": "^1.1.1", "sax": "^1.5.0" }, "bin": "./bin/svgo.js" }, "sha512-XDpWUOPC6FEibaLzjfe0ucaV0YrOjYotGJO1WpF0Zd+n6ZGEQUsSugaoLq9QkEZtAfQIxT42UChcssDVPP3+/w=="], "synckit": ["synckit@0.11.12", "", { "dependencies": { "@pkgr/core": "^0.2.9" } }, "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ=="], + "tabbable": ["tabbable@6.5.0", "", {}, "sha512-wieBHXygIm7OyQOu5hQlkk62/WyCFYGlWg7L6/ZCUZwx0o398Zkn4pVmMyfYhfMG8kGrj/Krt8eIk6UKC6VzwA=="], + + "tailwind-merge": ["tailwind-merge@3.6.0", "", {}, "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w=="], + + "tailwind-variants": ["tailwind-variants@3.3.1", "", { "peerDependencies": { "tailwind-merge": ">=3.0.0", "tailwindcss": "*" }, "optionalPeers": ["tailwind-merge", "tailwindcss"] }, "sha512-4pAvwUtM4HKBiRZftncAbpn6V9Hhwoa5Fl7O2u5zbp7Z5Cvu+/o/6+176WY3WCEES209543quG8zFIcXCsc5Jw=="], + "tailwindcss": ["tailwindcss@4.2.2", "", {}, "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q=="], "tapable": ["tapable@2.3.2", "", {}, "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA=="], @@ -1385,6 +1418,8 @@ "tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="], + "tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="], + "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], @@ -1553,6 +1588,8 @@ "astro-eslint-parser/espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="], + "bits-ui/runed": ["runed@0.35.1", "", { "dependencies": { "dequal": "^2.0.3", "esm-env": "^1.0.0", "lz-string": "^1.5.0" }, "peerDependencies": { "@sveltejs/kit": "^2.21.0", "svelte": "^5.7.0" }, "optionalPeers": ["@sveltejs/kit"] }, "sha512-2F4Q/FZzbeJTFdIS/PuOoPRSm92sA2LhzTnv6FXhCoENb3huf5+fDuNOg1LNvGOouy3u/225qxmuJvcV3IZK5Q=="], + "cacache/chownr": ["chownr@2.0.0", "", {}, "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="], "cacache/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], @@ -1599,6 +1636,8 @@ "rc/strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], + "shadcn-svelte/postcss": ["postcss@8.5.26", "", { "dependencies": { "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ=="], + "sitemap/@types/node": ["@types/node@24.12.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ=="], "ssri/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], @@ -1611,6 +1650,10 @@ "svelte-eslint-parser/espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="], + "svelte-toolbelt/runed": ["runed@0.35.1", "", { "dependencies": { "dequal": "^2.0.3", "esm-env": "^1.0.0", "lz-string": "^1.5.0" }, "peerDependencies": { "@sveltejs/kit": "^2.21.0", "svelte": "^5.7.0" }, "optionalPeers": ["@sveltejs/kit"] }, "sha512-2F4Q/FZzbeJTFdIS/PuOoPRSm92sA2LhzTnv6FXhCoENb3huf5+fDuNOg1LNvGOouy3u/225qxmuJvcV3IZK5Q=="], + + "svgo/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="], + "tar/chownr": ["chownr@2.0.0", "", {}, "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="], "tsx/esbuild": ["esbuild@0.27.7", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.7", "@esbuild/android-arm": "0.27.7", "@esbuild/android-arm64": "0.27.7", "@esbuild/android-x64": "0.27.7", "@esbuild/darwin-arm64": "0.27.7", "@esbuild/darwin-x64": "0.27.7", "@esbuild/freebsd-arm64": "0.27.7", "@esbuild/freebsd-x64": "0.27.7", "@esbuild/linux-arm": "0.27.7", "@esbuild/linux-arm64": "0.27.7", "@esbuild/linux-ia32": "0.27.7", "@esbuild/linux-loong64": "0.27.7", "@esbuild/linux-mips64el": "0.27.7", "@esbuild/linux-ppc64": "0.27.7", "@esbuild/linux-riscv64": "0.27.7", "@esbuild/linux-s390x": "0.27.7", "@esbuild/linux-x64": "0.27.7", "@esbuild/netbsd-arm64": "0.27.7", "@esbuild/netbsd-x64": "0.27.7", "@esbuild/openbsd-arm64": "0.27.7", "@esbuild/openbsd-x64": "0.27.7", "@esbuild/openharmony-arm64": "0.27.7", "@esbuild/sunos-x64": "0.27.7", "@esbuild/win32-arm64": "0.27.7", "@esbuild/win32-ia32": "0.27.7", "@esbuild/win32-x64": "0.27.7" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w=="], @@ -1763,6 +1806,8 @@ "p-locate/p-limit/yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + "shadcn-svelte/postcss/nanoid": ["nanoid@3.3.18", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w=="], + "sitemap/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], "tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.7", "", { "os": "aix", "cpu": "ppc64" }, "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg=="],