From 5172eede2d68661dddfecf645b3146c3e08d2f54 Mon Sep 17 00:00:00 2001 From: flamboh Date: Thu, 8 Oct 2026 23:50:32 -0700 Subject: [PATCH 1/9] chore(web): add shadcn-svelte popover, command, range calendar, menu, toggle group, sheet and form primitives --- apps/web/package.json | 4 +- .../src/lib/components/ui/badge/badge.svelte | 50 ++++++++ apps/web/src/lib/components/ui/badge/index.ts | 2 + .../ui/command/command-dialog.svelte | 42 +++++++ .../ui/command/command-empty.svelte | 17 +++ .../ui/command/command-group.svelte | 33 ++++++ .../ui/command/command-input.svelte | 36 ++++++ .../components/ui/command/command-item.svelte | 28 +++++ .../ui/command/command-link-item.svelte | 21 ++++ .../components/ui/command/command-list.svelte | 20 ++++ .../ui/command/command-loading.svelte | 7 ++ .../ui/command/command-separator.svelte | 17 +++ .../ui/command/command-shortcut.svelte | 23 ++++ .../lib/components/ui/command/command.svelte | 28 +++++ .../src/lib/components/ui/command/index.ts | 37 ++++++ .../components/ui/dialog/dialog-close.svelte | 11 ++ .../ui/dialog/dialog-content.svelte | 48 ++++++++ .../ui/dialog/dialog-description.svelte | 20 ++++ .../components/ui/dialog/dialog-footer.svelte | 32 +++++ .../components/ui/dialog/dialog-header.svelte | 20 ++++ .../ui/dialog/dialog-overlay.svelte | 20 ++++ .../components/ui/dialog/dialog-portal.svelte | 7 ++ .../components/ui/dialog/dialog-title.svelte | 17 +++ .../ui/dialog/dialog-trigger.svelte | 11 ++ .../lib/components/ui/dialog/dialog.svelte | 7 ++ .../web/src/lib/components/ui/dialog/index.ts | 34 ++++++ .../dropdown-menu-checkbox-group.svelte | 16 +++ .../dropdown-menu-checkbox-item.svelte | 46 +++++++ .../dropdown-menu-content.svelte | 31 +++++ .../dropdown-menu-group-heading.svelte | 22 ++++ .../dropdown-menu/dropdown-menu-group.svelte | 7 ++ .../dropdown-menu/dropdown-menu-item.svelte | 27 +++++ .../dropdown-menu/dropdown-menu-label.svelte | 24 ++++ .../dropdown-menu/dropdown-menu-portal.svelte | 7 ++ .../dropdown-menu-radio-group.svelte | 16 +++ .../dropdown-menu-radio-item.svelte | 36 ++++++ .../dropdown-menu-separator.svelte | 17 +++ .../dropdown-menu-shortcut.svelte | 23 ++++ .../dropdown-menu-sub-content.svelte | 31 +++++ .../dropdown-menu-sub-trigger.svelte | 29 +++++ .../ui/dropdown-menu/dropdown-menu-sub.svelte | 7 ++ .../dropdown-menu-trigger.svelte | 7 ++ .../ui/dropdown-menu/dropdown-menu.svelte | 7 ++ .../lib/components/ui/dropdown-menu/index.ts | 54 +++++++++ .../lib/components/ui/input-group/index.ts | 22 ++++ .../ui/input-group/input-group-addon.svelte | 53 +++++++++ .../ui/input-group/input-group-button.svelte | 49 ++++++++ .../ui/input-group/input-group-input.svelte | 23 ++++ .../ui/input-group/input-group-text.svelte | 22 ++++ .../input-group/input-group-textarea.svelte | 23 ++++ .../ui/input-group/input-group.svelte | 24 ++++ apps/web/src/lib/components/ui/input/index.ts | 7 ++ .../src/lib/components/ui/input/input.svelte | 48 ++++++++ apps/web/src/lib/components/ui/kbd/index.ts | 10 ++ .../lib/components/ui/kbd/kbd-group.svelte | 20 ++++ apps/web/src/lib/components/ui/kbd/kbd.svelte | 23 ++++ apps/web/src/lib/components/ui/label/index.ts | 7 ++ .../src/lib/components/ui/label/label.svelte | 20 ++++ .../src/lib/components/ui/popover/index.ts | 28 +++++ .../ui/popover/popover-close.svelte | 7 ++ .../ui/popover/popover-content.svelte | 31 +++++ .../ui/popover/popover-description.svelte | 20 ++++ .../ui/popover/popover-header.svelte | 20 ++++ .../ui/popover/popover-portal.svelte | 7 ++ .../ui/popover/popover-title.svelte | 15 +++ .../ui/popover/popover-trigger.svelte | 17 +++ .../lib/components/ui/popover/popover.svelte | 7 ++ .../lib/components/ui/range-calendar/index.ts | 40 +++++++ .../range-calendar-caption.svelte | 76 ++++++++++++ .../range-calendar/range-calendar-cell.svelte | 19 +++ .../range-calendar/range-calendar-day.svelte | 36 ++++++ .../range-calendar-grid-body.svelte | 7 ++ .../range-calendar-grid-head.svelte | 7 ++ .../range-calendar-grid-row.svelte | 12 ++ .../range-calendar/range-calendar-grid.svelte | 16 +++ .../range-calendar-head-cell.svelte | 19 +++ .../range-calendar-header.svelte | 19 +++ .../range-calendar-heading.svelte | 16 +++ .../range-calendar-month-select.svelte | 44 +++++++ .../range-calendar-month.svelte | 15 +++ .../range-calendar-months.svelte | 19 +++ .../range-calendar/range-calendar-nav.svelte | 19 +++ .../range-calendar-next-button.svelte | 37 ++++++ .../range-calendar-prev-button.svelte | 37 ++++++ .../range-calendar-year-select.svelte | 43 +++++++ .../ui/range-calendar/range-calendar.svelte | 112 ++++++++++++++++++ apps/web/src/lib/components/ui/sheet/index.ts | 34 ++++++ .../components/ui/sheet/sheet-close.svelte | 7 ++ .../components/ui/sheet/sheet-content.svelte | 55 +++++++++ .../ui/sheet/sheet-description.svelte | 17 +++ .../components/ui/sheet/sheet-footer.svelte | 20 ++++ .../components/ui/sheet/sheet-header.svelte | 20 ++++ .../components/ui/sheet/sheet-overlay.svelte | 20 ++++ .../components/ui/sheet/sheet-portal.svelte | 7 ++ .../components/ui/sheet/sheet-title.svelte | 17 +++ .../components/ui/sheet/sheet-trigger.svelte | 7 ++ .../src/lib/components/ui/sheet/sheet.svelte | 7 ++ .../src/lib/components/ui/textarea/index.ts | 7 ++ .../components/ui/textarea/textarea.svelte | 23 ++++ .../lib/components/ui/toggle-group/index.ts | 10 ++ .../ui/toggle-group/toggle-group-item.svelte | 35 ++++++ .../ui/toggle-group/toggle-group.svelte | 75 ++++++++++++ .../web/src/lib/components/ui/toggle/index.ts | 13 ++ .../lib/components/ui/toggle/toggle.svelte | 52 ++++++++ bun.lock | 6 +- 105 files changed, 2522 insertions(+), 5 deletions(-) create mode 100644 apps/web/src/lib/components/ui/badge/badge.svelte create mode 100644 apps/web/src/lib/components/ui/badge/index.ts create mode 100644 apps/web/src/lib/components/ui/command/command-dialog.svelte create mode 100644 apps/web/src/lib/components/ui/command/command-empty.svelte create mode 100644 apps/web/src/lib/components/ui/command/command-group.svelte create mode 100644 apps/web/src/lib/components/ui/command/command-input.svelte create mode 100644 apps/web/src/lib/components/ui/command/command-item.svelte create mode 100644 apps/web/src/lib/components/ui/command/command-link-item.svelte create mode 100644 apps/web/src/lib/components/ui/command/command-list.svelte create mode 100644 apps/web/src/lib/components/ui/command/command-loading.svelte create mode 100644 apps/web/src/lib/components/ui/command/command-separator.svelte create mode 100644 apps/web/src/lib/components/ui/command/command-shortcut.svelte create mode 100644 apps/web/src/lib/components/ui/command/command.svelte create mode 100644 apps/web/src/lib/components/ui/command/index.ts create mode 100644 apps/web/src/lib/components/ui/dialog/dialog-close.svelte create mode 100644 apps/web/src/lib/components/ui/dialog/dialog-content.svelte create mode 100644 apps/web/src/lib/components/ui/dialog/dialog-description.svelte create mode 100644 apps/web/src/lib/components/ui/dialog/dialog-footer.svelte create mode 100644 apps/web/src/lib/components/ui/dialog/dialog-header.svelte create mode 100644 apps/web/src/lib/components/ui/dialog/dialog-overlay.svelte create mode 100644 apps/web/src/lib/components/ui/dialog/dialog-portal.svelte create mode 100644 apps/web/src/lib/components/ui/dialog/dialog-title.svelte create mode 100644 apps/web/src/lib/components/ui/dialog/dialog-trigger.svelte create mode 100644 apps/web/src/lib/components/ui/dialog/dialog.svelte create mode 100644 apps/web/src/lib/components/ui/dialog/index.ts create mode 100644 apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte create mode 100644 apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte create mode 100644 apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte create mode 100644 apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte create mode 100644 apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte create mode 100644 apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte create mode 100644 apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte create mode 100644 apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-portal.svelte create mode 100644 apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte create mode 100644 apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte create mode 100644 apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte create mode 100644 apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte create mode 100644 apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte create mode 100644 apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte create mode 100644 apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-sub.svelte create mode 100644 apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte create mode 100644 apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu.svelte create mode 100644 apps/web/src/lib/components/ui/dropdown-menu/index.ts create mode 100644 apps/web/src/lib/components/ui/input-group/index.ts create mode 100644 apps/web/src/lib/components/ui/input-group/input-group-addon.svelte create mode 100644 apps/web/src/lib/components/ui/input-group/input-group-button.svelte create mode 100644 apps/web/src/lib/components/ui/input-group/input-group-input.svelte create mode 100644 apps/web/src/lib/components/ui/input-group/input-group-text.svelte create mode 100644 apps/web/src/lib/components/ui/input-group/input-group-textarea.svelte create mode 100644 apps/web/src/lib/components/ui/input-group/input-group.svelte create mode 100644 apps/web/src/lib/components/ui/input/index.ts create mode 100644 apps/web/src/lib/components/ui/input/input.svelte create mode 100644 apps/web/src/lib/components/ui/kbd/index.ts create mode 100644 apps/web/src/lib/components/ui/kbd/kbd-group.svelte create mode 100644 apps/web/src/lib/components/ui/kbd/kbd.svelte create mode 100644 apps/web/src/lib/components/ui/label/index.ts create mode 100644 apps/web/src/lib/components/ui/label/label.svelte create mode 100644 apps/web/src/lib/components/ui/popover/index.ts create mode 100644 apps/web/src/lib/components/ui/popover/popover-close.svelte create mode 100644 apps/web/src/lib/components/ui/popover/popover-content.svelte create mode 100644 apps/web/src/lib/components/ui/popover/popover-description.svelte create mode 100644 apps/web/src/lib/components/ui/popover/popover-header.svelte create mode 100644 apps/web/src/lib/components/ui/popover/popover-portal.svelte create mode 100644 apps/web/src/lib/components/ui/popover/popover-title.svelte create mode 100644 apps/web/src/lib/components/ui/popover/popover-trigger.svelte create mode 100644 apps/web/src/lib/components/ui/popover/popover.svelte create mode 100644 apps/web/src/lib/components/ui/range-calendar/index.ts create mode 100644 apps/web/src/lib/components/ui/range-calendar/range-calendar-caption.svelte create mode 100644 apps/web/src/lib/components/ui/range-calendar/range-calendar-cell.svelte create mode 100644 apps/web/src/lib/components/ui/range-calendar/range-calendar-day.svelte create mode 100644 apps/web/src/lib/components/ui/range-calendar/range-calendar-grid-body.svelte create mode 100644 apps/web/src/lib/components/ui/range-calendar/range-calendar-grid-head.svelte create mode 100644 apps/web/src/lib/components/ui/range-calendar/range-calendar-grid-row.svelte create mode 100644 apps/web/src/lib/components/ui/range-calendar/range-calendar-grid.svelte create mode 100644 apps/web/src/lib/components/ui/range-calendar/range-calendar-head-cell.svelte create mode 100644 apps/web/src/lib/components/ui/range-calendar/range-calendar-header.svelte create mode 100644 apps/web/src/lib/components/ui/range-calendar/range-calendar-heading.svelte create mode 100644 apps/web/src/lib/components/ui/range-calendar/range-calendar-month-select.svelte create mode 100644 apps/web/src/lib/components/ui/range-calendar/range-calendar-month.svelte create mode 100644 apps/web/src/lib/components/ui/range-calendar/range-calendar-months.svelte create mode 100644 apps/web/src/lib/components/ui/range-calendar/range-calendar-nav.svelte create mode 100644 apps/web/src/lib/components/ui/range-calendar/range-calendar-next-button.svelte create mode 100644 apps/web/src/lib/components/ui/range-calendar/range-calendar-prev-button.svelte create mode 100644 apps/web/src/lib/components/ui/range-calendar/range-calendar-year-select.svelte create mode 100644 apps/web/src/lib/components/ui/range-calendar/range-calendar.svelte create mode 100644 apps/web/src/lib/components/ui/sheet/index.ts create mode 100644 apps/web/src/lib/components/ui/sheet/sheet-close.svelte create mode 100644 apps/web/src/lib/components/ui/sheet/sheet-content.svelte create mode 100644 apps/web/src/lib/components/ui/sheet/sheet-description.svelte create mode 100644 apps/web/src/lib/components/ui/sheet/sheet-footer.svelte create mode 100644 apps/web/src/lib/components/ui/sheet/sheet-header.svelte create mode 100644 apps/web/src/lib/components/ui/sheet/sheet-overlay.svelte create mode 100644 apps/web/src/lib/components/ui/sheet/sheet-portal.svelte create mode 100644 apps/web/src/lib/components/ui/sheet/sheet-title.svelte create mode 100644 apps/web/src/lib/components/ui/sheet/sheet-trigger.svelte create mode 100644 apps/web/src/lib/components/ui/sheet/sheet.svelte create mode 100644 apps/web/src/lib/components/ui/textarea/index.ts create mode 100644 apps/web/src/lib/components/ui/textarea/textarea.svelte create mode 100644 apps/web/src/lib/components/ui/toggle-group/index.ts create mode 100644 apps/web/src/lib/components/ui/toggle-group/toggle-group-item.svelte create mode 100644 apps/web/src/lib/components/ui/toggle-group/toggle-group.svelte create mode 100644 apps/web/src/lib/components/ui/toggle/index.ts create mode 100644 apps/web/src/lib/components/ui/toggle/toggle.svelte diff --git a/apps/web/package.json b/apps/web/package.json index 9d24911..bec8266 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -34,7 +34,7 @@ "@types/better-sqlite3": "^7.6.13", "@types/d3-scale": "4.0.9", "@types/node": "^22.15.17", - "bits-ui": "^2.16.3", + "bits-ui": "^2.18.0", "clsx": "^2.1.1", "eslint": "^9.18.0", "eslint-config-prettier": "^10.0.1", @@ -56,7 +56,7 @@ "vitest": "^5.0.1" }, "dependencies": { - "@lucide/svelte": "^1.33.0", + "@lucide/svelte": "^1.54.0", "@tanstack/charts": "1.0.0", "better-sqlite3": "^12.6.2", "d3-scale": "4.0.2", diff --git a/apps/web/src/lib/components/ui/badge/badge.svelte b/apps/web/src/lib/components/ui/badge/badge.svelte new file mode 100644 index 0000000..c09e233 --- /dev/null +++ b/apps/web/src/lib/components/ui/badge/badge.svelte @@ -0,0 +1,50 @@ + + + + + + {@render children?.()} + diff --git a/apps/web/src/lib/components/ui/badge/index.ts b/apps/web/src/lib/components/ui/badge/index.ts new file mode 100644 index 0000000..f05fb87 --- /dev/null +++ b/apps/web/src/lib/components/ui/badge/index.ts @@ -0,0 +1,2 @@ +export { default as Badge } from './badge.svelte'; +export { badgeVariants, type BadgeVariant } from './badge.svelte'; diff --git a/apps/web/src/lib/components/ui/command/command-dialog.svelte b/apps/web/src/lib/components/ui/command/command-dialog.svelte new file mode 100644 index 0000000..75faea4 --- /dev/null +++ b/apps/web/src/lib/components/ui/command/command-dialog.svelte @@ -0,0 +1,42 @@ + + + + + {title} + {description} + + + + + diff --git a/apps/web/src/lib/components/ui/command/command-empty.svelte b/apps/web/src/lib/components/ui/command/command-empty.svelte new file mode 100644 index 0000000..f6ed1b8 --- /dev/null +++ b/apps/web/src/lib/components/ui/command/command-empty.svelte @@ -0,0 +1,17 @@ + + + diff --git a/apps/web/src/lib/components/ui/command/command-group.svelte b/apps/web/src/lib/components/ui/command/command-group.svelte new file mode 100644 index 0000000..05effae --- /dev/null +++ b/apps/web/src/lib/components/ui/command/command-group.svelte @@ -0,0 +1,33 @@ + + + + {#if heading} + + {heading} + + {/if} + + diff --git a/apps/web/src/lib/components/ui/command/command-input.svelte b/apps/web/src/lib/components/ui/command/command-input.svelte new file mode 100644 index 0000000..d624058 --- /dev/null +++ b/apps/web/src/lib/components/ui/command/command-input.svelte @@ -0,0 +1,36 @@ + + +
+ + + {#snippet child({ props })} + + {/snippet} + + + + + +
diff --git a/apps/web/src/lib/components/ui/command/command-item.svelte b/apps/web/src/lib/components/ui/command/command-item.svelte new file mode 100644 index 0000000..a5a28df --- /dev/null +++ b/apps/web/src/lib/components/ui/command/command-item.svelte @@ -0,0 +1,28 @@ + + + + {@render children?.()} + + diff --git a/apps/web/src/lib/components/ui/command/command-link-item.svelte b/apps/web/src/lib/components/ui/command/command-link-item.svelte new file mode 100644 index 0000000..7b61497 --- /dev/null +++ b/apps/web/src/lib/components/ui/command/command-link-item.svelte @@ -0,0 +1,21 @@ + + + diff --git a/apps/web/src/lib/components/ui/command/command-list.svelte b/apps/web/src/lib/components/ui/command/command-list.svelte new file mode 100644 index 0000000..9e23fc8 --- /dev/null +++ b/apps/web/src/lib/components/ui/command/command-list.svelte @@ -0,0 +1,20 @@ + + + diff --git a/apps/web/src/lib/components/ui/command/command-loading.svelte b/apps/web/src/lib/components/ui/command/command-loading.svelte new file mode 100644 index 0000000..0aaf5c3 --- /dev/null +++ b/apps/web/src/lib/components/ui/command/command-loading.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/web/src/lib/components/ui/command/command-separator.svelte b/apps/web/src/lib/components/ui/command/command-separator.svelte new file mode 100644 index 0000000..3208ad2 --- /dev/null +++ b/apps/web/src/lib/components/ui/command/command-separator.svelte @@ -0,0 +1,17 @@ + + + diff --git a/apps/web/src/lib/components/ui/command/command-shortcut.svelte b/apps/web/src/lib/components/ui/command/command-shortcut.svelte new file mode 100644 index 0000000..fc4583b --- /dev/null +++ b/apps/web/src/lib/components/ui/command/command-shortcut.svelte @@ -0,0 +1,23 @@ + + + + {@render children?.()} + diff --git a/apps/web/src/lib/components/ui/command/command.svelte b/apps/web/src/lib/components/ui/command/command.svelte new file mode 100644 index 0000000..74bc6fa --- /dev/null +++ b/apps/web/src/lib/components/ui/command/command.svelte @@ -0,0 +1,28 @@ + + + diff --git a/apps/web/src/lib/components/ui/command/index.ts b/apps/web/src/lib/components/ui/command/index.ts new file mode 100644 index 0000000..15ed82b --- /dev/null +++ b/apps/web/src/lib/components/ui/command/index.ts @@ -0,0 +1,37 @@ +import Dialog from './command-dialog.svelte'; +import Empty from './command-empty.svelte'; +import Group from './command-group.svelte'; +import Input from './command-input.svelte'; +import Item from './command-item.svelte'; +import LinkItem from './command-link-item.svelte'; +import List from './command-list.svelte'; +import Loading from './command-loading.svelte'; +import Separator from './command-separator.svelte'; +import Shortcut from './command-shortcut.svelte'; +import Root from './command.svelte'; + +export { + Root, + Dialog, + Empty, + Group, + Item, + LinkItem, + Input, + List, + Separator, + Shortcut, + Loading, + // + Root as Command, + Dialog as CommandDialog, + Empty as CommandEmpty, + Group as CommandGroup, + Item as CommandItem, + LinkItem as CommandLinkItem, + Input as CommandInput, + List as CommandList, + Separator as CommandSeparator, + Shortcut as CommandShortcut, + Loading as CommandLoading +}; diff --git a/apps/web/src/lib/components/ui/dialog/dialog-close.svelte b/apps/web/src/lib/components/ui/dialog/dialog-close.svelte new file mode 100644 index 0000000..4141704 --- /dev/null +++ b/apps/web/src/lib/components/ui/dialog/dialog-close.svelte @@ -0,0 +1,11 @@ + + + diff --git a/apps/web/src/lib/components/ui/dialog/dialog-content.svelte b/apps/web/src/lib/components/ui/dialog/dialog-content.svelte new file mode 100644 index 0000000..2fc3db3 --- /dev/null +++ b/apps/web/src/lib/components/ui/dialog/dialog-content.svelte @@ -0,0 +1,48 @@ + + + + + + {@render children?.()} + {#if showCloseButton} + + {#snippet child({ props })} + + {/snippet} + + {/if} + + diff --git a/apps/web/src/lib/components/ui/dialog/dialog-description.svelte b/apps/web/src/lib/components/ui/dialog/dialog-description.svelte new file mode 100644 index 0000000..356b2da --- /dev/null +++ b/apps/web/src/lib/components/ui/dialog/dialog-description.svelte @@ -0,0 +1,20 @@ + + + diff --git a/apps/web/src/lib/components/ui/dialog/dialog-footer.svelte b/apps/web/src/lib/components/ui/dialog/dialog-footer.svelte new file mode 100644 index 0000000..f55b640 --- /dev/null +++ b/apps/web/src/lib/components/ui/dialog/dialog-footer.svelte @@ -0,0 +1,32 @@ + + +
+ {@render children?.()} + {#if showCloseButton} + + {#snippet child({ props })} + + {/snippet} + + {/if} +
diff --git a/apps/web/src/lib/components/ui/dialog/dialog-header.svelte b/apps/web/src/lib/components/ui/dialog/dialog-header.svelte new file mode 100644 index 0000000..50a27a4 --- /dev/null +++ b/apps/web/src/lib/components/ui/dialog/dialog-header.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/apps/web/src/lib/components/ui/dialog/dialog-overlay.svelte b/apps/web/src/lib/components/ui/dialog/dialog-overlay.svelte new file mode 100644 index 0000000..5d62971 --- /dev/null +++ b/apps/web/src/lib/components/ui/dialog/dialog-overlay.svelte @@ -0,0 +1,20 @@ + + + diff --git a/apps/web/src/lib/components/ui/dialog/dialog-portal.svelte b/apps/web/src/lib/components/ui/dialog/dialog-portal.svelte new file mode 100644 index 0000000..a60a85e --- /dev/null +++ b/apps/web/src/lib/components/ui/dialog/dialog-portal.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/web/src/lib/components/ui/dialog/dialog-title.svelte b/apps/web/src/lib/components/ui/dialog/dialog-title.svelte new file mode 100644 index 0000000..9042c60 --- /dev/null +++ b/apps/web/src/lib/components/ui/dialog/dialog-title.svelte @@ -0,0 +1,17 @@ + + + diff --git a/apps/web/src/lib/components/ui/dialog/dialog-trigger.svelte b/apps/web/src/lib/components/ui/dialog/dialog-trigger.svelte new file mode 100644 index 0000000..9264541 --- /dev/null +++ b/apps/web/src/lib/components/ui/dialog/dialog-trigger.svelte @@ -0,0 +1,11 @@ + + + diff --git a/apps/web/src/lib/components/ui/dialog/dialog.svelte b/apps/web/src/lib/components/ui/dialog/dialog.svelte new file mode 100644 index 0000000..c1444b2 --- /dev/null +++ b/apps/web/src/lib/components/ui/dialog/dialog.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/web/src/lib/components/ui/dialog/index.ts b/apps/web/src/lib/components/ui/dialog/index.ts new file mode 100644 index 0000000..c8c6532 --- /dev/null +++ b/apps/web/src/lib/components/ui/dialog/index.ts @@ -0,0 +1,34 @@ +import Close from './dialog-close.svelte'; +import Content from './dialog-content.svelte'; +import Description from './dialog-description.svelte'; +import Footer from './dialog-footer.svelte'; +import Header from './dialog-header.svelte'; +import Overlay from './dialog-overlay.svelte'; +import Portal from './dialog-portal.svelte'; +import Title from './dialog-title.svelte'; +import Trigger from './dialog-trigger.svelte'; +import Root from './dialog.svelte'; + +export { + Root, + Title, + Portal, + Footer, + Header, + Trigger, + Overlay, + Content, + Description, + Close, + // + Root as Dialog, + Title as DialogTitle, + Portal as DialogPortal, + Footer as DialogFooter, + Header as DialogHeader, + Trigger as DialogTrigger, + Overlay as DialogOverlay, + Content as DialogContent, + Description as DialogDescription, + Close as DialogClose +}; diff --git a/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte b/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte new file mode 100644 index 0000000..c0bf94b --- /dev/null +++ b/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte @@ -0,0 +1,16 @@ + + + diff --git a/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte b/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte new file mode 100644 index 0000000..5c34b4e --- /dev/null +++ b/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte @@ -0,0 +1,46 @@ + + + + {#snippet children({ checked, indeterminate })} + + {#if indeterminate} + + {:else if checked} + + {/if} + + {@render childrenProp?.()} + {/snippet} + diff --git a/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte b/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte new file mode 100644 index 0000000..35d0687 --- /dev/null +++ b/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte @@ -0,0 +1,31 @@ + + + + + diff --git a/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte b/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte new file mode 100644 index 0000000..94c4686 --- /dev/null +++ b/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte @@ -0,0 +1,22 @@ + + + diff --git a/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte b/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte new file mode 100644 index 0000000..261ab7e --- /dev/null +++ b/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte b/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte new file mode 100644 index 0000000..33fab89 --- /dev/null +++ b/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte @@ -0,0 +1,27 @@ + + + diff --git a/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte b/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte new file mode 100644 index 0000000..16fdbca --- /dev/null +++ b/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte @@ -0,0 +1,24 @@ + + +
+ {@render children?.()} +
diff --git a/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-portal.svelte b/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-portal.svelte new file mode 100644 index 0000000..089a25c --- /dev/null +++ b/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-portal.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte b/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte new file mode 100644 index 0000000..3e98749 --- /dev/null +++ b/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte @@ -0,0 +1,16 @@ + + + diff --git a/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte b/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte new file mode 100644 index 0000000..8782efd --- /dev/null +++ b/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte @@ -0,0 +1,36 @@ + + + + {#snippet children({ checked })} + + {#if checked} + + {/if} + + {@render childrenProp?.({ checked })} + {/snippet} + diff --git a/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte b/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte new file mode 100644 index 0000000..4aea4f4 --- /dev/null +++ b/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte @@ -0,0 +1,17 @@ + + + diff --git a/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte b/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte new file mode 100644 index 0000000..fb9a938 --- /dev/null +++ b/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte @@ -0,0 +1,23 @@ + + + + {@render children?.()} + diff --git a/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte b/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte new file mode 100644 index 0000000..91fa5c3 --- /dev/null +++ b/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte @@ -0,0 +1,31 @@ + + + + + diff --git a/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte b/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte new file mode 100644 index 0000000..1df7c1e --- /dev/null +++ b/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte @@ -0,0 +1,29 @@ + + + + {@render children?.()} + + diff --git a/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-sub.svelte b/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-sub.svelte new file mode 100644 index 0000000..9cfa3b8 --- /dev/null +++ b/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-sub.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte b/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte new file mode 100644 index 0000000..032b645 --- /dev/null +++ b/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu.svelte b/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu.svelte new file mode 100644 index 0000000..fbbb790 --- /dev/null +++ b/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/web/src/lib/components/ui/dropdown-menu/index.ts b/apps/web/src/lib/components/ui/dropdown-menu/index.ts new file mode 100644 index 0000000..c26b6a8 --- /dev/null +++ b/apps/web/src/lib/components/ui/dropdown-menu/index.ts @@ -0,0 +1,54 @@ +import CheckboxGroup from './dropdown-menu-checkbox-group.svelte'; +import CheckboxItem from './dropdown-menu-checkbox-item.svelte'; +import Content from './dropdown-menu-content.svelte'; +import GroupHeading from './dropdown-menu-group-heading.svelte'; +import Group from './dropdown-menu-group.svelte'; +import Item from './dropdown-menu-item.svelte'; +import Label from './dropdown-menu-label.svelte'; +import Portal from './dropdown-menu-portal.svelte'; +import RadioGroup from './dropdown-menu-radio-group.svelte'; +import RadioItem from './dropdown-menu-radio-item.svelte'; +import Separator from './dropdown-menu-separator.svelte'; +import Shortcut from './dropdown-menu-shortcut.svelte'; +import SubContent from './dropdown-menu-sub-content.svelte'; +import SubTrigger from './dropdown-menu-sub-trigger.svelte'; +import Sub from './dropdown-menu-sub.svelte'; +import Trigger from './dropdown-menu-trigger.svelte'; +import Root from './dropdown-menu.svelte'; + +export { + CheckboxGroup, + CheckboxItem, + Content, + Portal, + Root as DropdownMenu, + CheckboxGroup as DropdownMenuCheckboxGroup, + CheckboxItem as DropdownMenuCheckboxItem, + Content as DropdownMenuContent, + Portal as DropdownMenuPortal, + Group as DropdownMenuGroup, + Item as DropdownMenuItem, + Label as DropdownMenuLabel, + RadioGroup as DropdownMenuRadioGroup, + RadioItem as DropdownMenuRadioItem, + Separator as DropdownMenuSeparator, + Shortcut as DropdownMenuShortcut, + Sub as DropdownMenuSub, + SubContent as DropdownMenuSubContent, + SubTrigger as DropdownMenuSubTrigger, + Trigger as DropdownMenuTrigger, + GroupHeading as DropdownMenuGroupHeading, + Group, + GroupHeading, + Item, + Label, + RadioGroup, + RadioItem, + Root, + Separator, + Shortcut, + Sub, + SubContent, + SubTrigger, + Trigger +}; diff --git a/apps/web/src/lib/components/ui/input-group/index.ts b/apps/web/src/lib/components/ui/input-group/index.ts new file mode 100644 index 0000000..a1fd3f3 --- /dev/null +++ b/apps/web/src/lib/components/ui/input-group/index.ts @@ -0,0 +1,22 @@ +import Addon from './input-group-addon.svelte'; +import Button from './input-group-button.svelte'; +import Input from './input-group-input.svelte'; +import Text from './input-group-text.svelte'; +import Textarea from './input-group-textarea.svelte'; +import Root from './input-group.svelte'; + +export { + Root, + Addon, + Button, + Input, + Text, + Textarea, + // + Root as InputGroup, + Addon as InputGroupAddon, + Button as InputGroupButton, + Input as InputGroupInput, + Text as InputGroupText, + Textarea as InputGroupTextarea +}; diff --git a/apps/web/src/lib/components/ui/input-group/input-group-addon.svelte b/apps/web/src/lib/components/ui/input-group/input-group-addon.svelte new file mode 100644 index 0000000..2d03d34 --- /dev/null +++ b/apps/web/src/lib/components/ui/input-group/input-group-addon.svelte @@ -0,0 +1,53 @@ + + + + +
{ + if ((e.target as HTMLElement).closest('button')) { + return; + } + e.currentTarget.parentElement?.querySelector('input')?.focus(); + }} + {...restProps} +> + {@render children?.()} +
diff --git a/apps/web/src/lib/components/ui/input-group/input-group-button.svelte b/apps/web/src/lib/components/ui/input-group/input-group-button.svelte new file mode 100644 index 0000000..0d9e831 --- /dev/null +++ b/apps/web/src/lib/components/ui/input-group/input-group-button.svelte @@ -0,0 +1,49 @@ + + + + + diff --git a/apps/web/src/lib/components/ui/input-group/input-group-input.svelte b/apps/web/src/lib/components/ui/input-group/input-group-input.svelte new file mode 100644 index 0000000..d663b71 --- /dev/null +++ b/apps/web/src/lib/components/ui/input-group/input-group-input.svelte @@ -0,0 +1,23 @@ + + + diff --git a/apps/web/src/lib/components/ui/input-group/input-group-text.svelte b/apps/web/src/lib/components/ui/input-group/input-group-text.svelte new file mode 100644 index 0000000..97d7d30 --- /dev/null +++ b/apps/web/src/lib/components/ui/input-group/input-group-text.svelte @@ -0,0 +1,22 @@ + + + + {@render children?.()} + diff --git a/apps/web/src/lib/components/ui/input-group/input-group-textarea.svelte b/apps/web/src/lib/components/ui/input-group/input-group-textarea.svelte new file mode 100644 index 0000000..aad6314 --- /dev/null +++ b/apps/web/src/lib/components/ui/input-group/input-group-textarea.svelte @@ -0,0 +1,23 @@ + + + diff --git a/apps/web/src/lib/components/ui/toggle-group/index.ts b/apps/web/src/lib/components/ui/toggle-group/index.ts new file mode 100644 index 0000000..0ec59de --- /dev/null +++ b/apps/web/src/lib/components/ui/toggle-group/index.ts @@ -0,0 +1,10 @@ +import Item from './toggle-group-item.svelte'; +import Root from './toggle-group.svelte'; + +export { + Root, + Item, + // + Root as ToggleGroup, + Item as ToggleGroupItem +}; diff --git a/apps/web/src/lib/components/ui/toggle-group/toggle-group-item.svelte b/apps/web/src/lib/components/ui/toggle-group/toggle-group-item.svelte new file mode 100644 index 0000000..243f306 --- /dev/null +++ b/apps/web/src/lib/components/ui/toggle-group/toggle-group-item.svelte @@ -0,0 +1,35 @@ + + + diff --git a/apps/web/src/lib/components/ui/toggle-group/toggle-group.svelte b/apps/web/src/lib/components/ui/toggle-group/toggle-group.svelte new file mode 100644 index 0000000..7de4d56 --- /dev/null +++ b/apps/web/src/lib/components/ui/toggle-group/toggle-group.svelte @@ -0,0 +1,75 @@ + + + + + + diff --git a/apps/web/src/lib/components/ui/toggle/index.ts b/apps/web/src/lib/components/ui/toggle/index.ts new file mode 100644 index 0000000..0f18d3b --- /dev/null +++ b/apps/web/src/lib/components/ui/toggle/index.ts @@ -0,0 +1,13 @@ +import Root from './toggle.svelte'; +export { + toggleVariants, + type ToggleSize, + type ToggleVariant, + type ToggleVariants +} from './toggle.svelte'; + +export { + Root, + // + Root as Toggle +}; diff --git a/apps/web/src/lib/components/ui/toggle/toggle.svelte b/apps/web/src/lib/components/ui/toggle/toggle.svelte new file mode 100644 index 0000000..41c07dc --- /dev/null +++ b/apps/web/src/lib/components/ui/toggle/toggle.svelte @@ -0,0 +1,52 @@ + + + + + diff --git a/bun.lock b/bun.lock index 906add2..dd03632 100644 --- a/bun.lock +++ b/bun.lock @@ -31,7 +31,7 @@ "name": "@atlantis/web", "version": "0.0.1", "dependencies": { - "@lucide/svelte": "^1.33.0", + "@lucide/svelte": "^1.54.0", "@tanstack/charts": "1.0.0", "better-sqlite3": "^12.6.2", "d3-scale": "4.0.2", @@ -52,7 +52,7 @@ "@types/better-sqlite3": "^7.6.13", "@types/d3-scale": "4.0.9", "@types/node": "^22.15.17", - "bits-ui": "^2.16.3", + "bits-ui": "^2.18.0", "clsx": "^2.1.1", "eslint": "^9.18.0", "eslint-config-prettier": "^10.0.1", @@ -424,7 +424,7 @@ "@libsql/win32-x64-msvc": ["@libsql/win32-x64-msvc@0.5.29", "", { "os": "win32", "cpu": "x64" }, "sha512-4/0CvEdhi6+KjMxMaVbFM2n2Z44escBRoEYpR+gZg64DdetzGnYm8mcNLcoySaDJZNaBd6wz5DNdgRmcI4hXcg=="], - "@lucide/svelte": ["@lucide/svelte@1.33.0", "", { "peerDependencies": { "svelte": "^5" } }, "sha512-b+osTYG2V4dge5Lr7tnaTaahhy/4vH7Xb8zcqVjmctjwgkpXS0NNOJPkGzHHZoxtw/OO+2YAU28omeMfYCaawg=="], + "@lucide/svelte": ["@lucide/svelte@1.54.0", "", { "peerDependencies": { "svelte": "^5" } }, "sha512-PbUrcmVPU71zmcs1Kc5RlgF83SsBsSNC+MeuttAHMmvPMuoIJfyDPjit5kpAJxJwsmN2Pk1Bx75AwZxP8vkDvw=="], "@mrleebo/prisma-ast": ["@mrleebo/prisma-ast@0.13.1", "", { "dependencies": { "chevrotain": "^10.5.0", "lilconfig": "^2.1.0" } }, "sha512-XyroGQXcHrZdvmrGJvsA9KNeOOgGMg1Vg9OlheUsBOSKznLMDl+YChxbkboRHvtFYJEMRYmlV3uoo/njCw05iw=="], From 8200107afef5f4135164b8d926ec10636270b781 Mon Sep 17 00:00:00 2001 From: flamboh Date: Fri, 9 Oct 2026 00:11:46 -0700 Subject: [PATCH 2/9] feat(web): console shell, filter toolbar, KPI row and card menus on shadcn-svelte --- apps/web/src/app.css | 558 +++--------------- .../components/charts/BreakdownChart.svelte | 103 ++-- .../src/lib/components/charts/CardMenu.svelte | 56 ++ .../lib/components/charts/ChartCard.svelte | 38 +- .../components/charts/ChartCardHeader.svelte | 40 ++ .../components/charts/CoverageStrip.svelte | 15 +- .../charts/FlowCharacteristicsChart.svelte | 21 +- .../charts/PortCardinalityChart.svelte | 62 +- .../src/lib/components/charts/card-frame.ts | 16 + .../components/common/ChecklistPopover.svelte | 50 ++ .../components/common/SegmentedToggle.svelte | 52 ++ .../components/common/ToolbarPopover.svelte | 66 +++ .../components/common/ToolbarSelect.svelte | 71 +++ .../filters/DashboardToolbar.svelte | 110 ++++ .../components/filters/DateRangePicker.svelte | 126 ++++ .../lib/components/filters/MaadOptions.svelte | 74 +++ .../components/filters/SourcesFilter.svelte | 62 ++ .../filters/TrafficMetricControls.svelte | 130 ++++ .../src/lib/components/filters/date-range.ts | 61 ++ .../netflow/DashboardCardSlot.svelte | 45 ++ .../netflow/DatasetDashboardPage.svelte | 178 ++---- .../src/lib/components/netflow/KpiRow.svelte | 92 +++ .../netflow/NetflowDashboard.svelte | 318 ++-------- .../netflow/netflow-stats-data.svelte.ts | 223 +++++++ .../src/lib/components/shell/AppNav.svelte | 19 + .../lib/components/shell/CommandMenu.svelte | 95 +++ .../components/shell/DatasetSwitcher.svelte | 79 +++ .../src/lib/components/shell/app-context.ts | 54 ++ .../components/ui/empty/empty-content.svelte | 23 + .../ui/empty/empty-description.svelte | 23 + .../components/ui/empty/empty-header.svelte | 20 + .../components/ui/empty/empty-media.svelte | 41 ++ .../components/ui/empty/empty-title.svelte | 20 + .../src/lib/components/ui/empty/empty.svelte | 23 + apps/web/src/lib/components/ui/empty/index.ts | 22 + .../lib/components/ui/native-select/index.ts | 12 + .../native-select-opt-group.svelte | 20 + .../native-select/native-select-option.svelte | 20 + .../ui/native-select/native-select.svelte | 40 ++ apps/web/src/routes/+layout.svelte | 90 +-- .../src/routes/datasets/[dataset]/+page.ts | 1 + 41 files changed, 2128 insertions(+), 1041 deletions(-) create mode 100644 apps/web/src/lib/components/charts/CardMenu.svelte create mode 100644 apps/web/src/lib/components/charts/ChartCardHeader.svelte create mode 100644 apps/web/src/lib/components/charts/card-frame.ts create mode 100644 apps/web/src/lib/components/common/ChecklistPopover.svelte create mode 100644 apps/web/src/lib/components/common/SegmentedToggle.svelte create mode 100644 apps/web/src/lib/components/common/ToolbarPopover.svelte create mode 100644 apps/web/src/lib/components/common/ToolbarSelect.svelte create mode 100644 apps/web/src/lib/components/filters/DashboardToolbar.svelte create mode 100644 apps/web/src/lib/components/filters/DateRangePicker.svelte create mode 100644 apps/web/src/lib/components/filters/MaadOptions.svelte create mode 100644 apps/web/src/lib/components/filters/SourcesFilter.svelte create mode 100644 apps/web/src/lib/components/filters/TrafficMetricControls.svelte create mode 100644 apps/web/src/lib/components/filters/date-range.ts create mode 100644 apps/web/src/lib/components/netflow/DashboardCardSlot.svelte create mode 100644 apps/web/src/lib/components/netflow/KpiRow.svelte create mode 100644 apps/web/src/lib/components/netflow/netflow-stats-data.svelte.ts create mode 100644 apps/web/src/lib/components/shell/AppNav.svelte create mode 100644 apps/web/src/lib/components/shell/CommandMenu.svelte create mode 100644 apps/web/src/lib/components/shell/DatasetSwitcher.svelte create mode 100644 apps/web/src/lib/components/shell/app-context.ts create mode 100644 apps/web/src/lib/components/ui/empty/empty-content.svelte create mode 100644 apps/web/src/lib/components/ui/empty/empty-description.svelte create mode 100644 apps/web/src/lib/components/ui/empty/empty-header.svelte create mode 100644 apps/web/src/lib/components/ui/empty/empty-media.svelte create mode 100644 apps/web/src/lib/components/ui/empty/empty-title.svelte create mode 100644 apps/web/src/lib/components/ui/empty/empty.svelte create mode 100644 apps/web/src/lib/components/ui/empty/index.ts create mode 100644 apps/web/src/lib/components/ui/native-select/index.ts create mode 100644 apps/web/src/lib/components/ui/native-select/native-select-opt-group.svelte create mode 100644 apps/web/src/lib/components/ui/native-select/native-select-option.svelte create mode 100644 apps/web/src/lib/components/ui/native-select/native-select.svelte diff --git a/apps/web/src/app.css b/apps/web/src/app.css index 06a11ba..664d506 100644 --- a/apps/web/src/app.css +++ b/apps/web/src/app.css @@ -5,8 +5,13 @@ @custom-variant dark (&:where(.dark, .dark *)); @theme { - --font-body: ui-sans-serif, system-ui, sans-serif; - --font-sans: ui-sans-serif, system-ui, sans-serif; + --font-sans: + Inter, 'Inter Variable', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif; + --font-mono: ui-monospace, 'SF Mono', 'JetBrains Mono', Menlo, monospace; + --color-selection: var(--selection); + --color-selection-foreground: var(--selection-foreground); + --color-success: var(--success); + --color-warning: var(--warning); --color-sidebar-ring: var(--sidebar-ring); --color-sidebar-border: var(--sidebar-border); --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); @@ -48,44 +53,35 @@ } :root { - --neutral-0: #ffffff; - --neutral-50: #f5f7fa; - --neutral-100: #edf1f6; - --neutral-200: #dce3ec; - --neutral-300: #bac6d5; - --neutral-500: #627086; - --neutral-700: #334155; - --neutral-900: #182233; - --background: var(--neutral-50); - --foreground: var(--neutral-900); - --card: var(--neutral-0); + --background: #fafafa; + --foreground: #0a0a0a; + --card: #ffffff; --card-foreground: var(--foreground); - --popover: var(--card); + --popover: #ffffff; --popover-foreground: var(--foreground); --primary: #2563eb; --primary-foreground: #ffffff; - --secondary: var(--neutral-100); - --secondary-foreground: var(--neutral-700); - --muted: var(--neutral-100); - --muted-foreground: #56657a; - --accent: #eaf0ff; - --accent-foreground: #1d4ed8; - --destructive: #bd303d; - --success: #087f6a; - --warning: #956500; + --secondary: #f5f5f5; + --secondary-foreground: #171717; + --muted: #f5f5f5; + --muted-foreground: #666666; + --accent: #f2f2f2; + --accent-foreground: #0a0a0a; + --selection: #eff6ff; + --selection-foreground: #1d4ed8; + --destructive: #dc2626; + --success: #15803d; + --warning: #a16207; --info: #2563eb; - --border: var(--neutral-200); - --input: var(--neutral-300); - --ring: var(--primary); - --radius: 0.375rem; - --space-panel: 1rem; - --type-label: 0.75rem; - --type-body: 0.875rem; - --type-title: 1.5rem; - --elevation-popover: 0 6px 24px #18223318; - --chart-text-color: #56657a; - --chart-grid-color: #dce3ec; - --chart-grid-highlight-color: #edf1f6; + --border: #e5e5e5; + --input: #dedede; + --ring: #2563eb; + --radius: 0.5rem; + --shadow-card: 0 1px 2px #0000000a; + --elevation-popover: 0 8px 30px -8px #00000029; + --chart-text-color: #737373; + --chart-grid-color: #ebebeb; + --chart-grid-highlight-color: #f5f5f5; --chart-tooltip-bg: var(--popover); --chart-tooltip-text-color: var(--foreground); --chart-tooltip-border-color: var(--border); @@ -117,24 +113,33 @@ --sidebar-ring: var(--ring); } .dark { - --neutral-0: #171f2c; - --neutral-50: #101722; - --neutral-100: #202b3b; - --neutral-200: #303d50; - --neutral-300: #45556c; - --neutral-500: #a3b2c7; - --neutral-700: #cfdae9; - --neutral-900: #eaf0f8; - --muted-foreground: #a3b2c7; - --accent: #203359; - --accent-foreground: #a8c5ff; - --destructive: #f18d96; - --success: #62c6b1; - --warning: #e8b951; + --background: #0a0a0a; + --foreground: #ededed; + --card: #111111; + --popover: #171717; + --primary: #2563eb; + --primary-foreground: #ffffff; + --secondary: #1f1f1f; + --secondary-foreground: #ededed; + --muted: #1c1c1c; + --muted-foreground: #a1a1a1; + --accent: #232323; + --accent-foreground: #ededed; + --selection: #172554; + --selection-foreground: #93c5fd; + --destructive: #f87171; + --success: #4ade80; + --warning: #facc15; + --info: #60a5fa; + --border: #262626; + --input: #333333; + --ring: #3b82f6; + --shadow-card: 0 1px 2px #00000066; + --elevation-popover: 0 8px 30px -8px #000000b3; --chart-spectrum-lightness: 60; - --chart-text-color: #a3b2c7; - --chart-grid-color: #303d50; - --chart-grid-highlight-color: #202b3b; + --chart-text-color: #8f8f8f; + --chart-grid-color: #262626; + --chart-grid-highlight-color: #1c1c1c; --chart-series-1: #689bff; --chart-series-2: #e5b44c; --chart-series-3: #56c8bb; @@ -144,7 +149,6 @@ --chart-series-7: #b3a0e3; --chart-series-8: #b4bdc9; --chart-series-other: #8a9aaf; - --elevation-popover: 0 6px 24px #00000040; color-scheme: dark; } @@ -174,53 +178,30 @@ body { } } -.page-layout, -.shell-width { +.shell { width: 100%; max-width: 1920px; margin-inline: auto; - padding-inline: clamp(1rem, 2.5vw, 3rem); -} -.page-layout { - padding-block: 1.5rem; + padding-inline: clamp(1rem, 2.5vw, 2rem); } -.page-heading { - font-size: var(--type-title); - font-weight: 600; - letter-spacing: -0.035em; - line-height: 1.3; -} -.page-eyebrow { - color: var(--muted-foreground); - font-size: var(--type-label); - margin-bottom: 0.375rem; +.page { + padding-block: 1.25rem 2rem; } body { - font-size: var(--type-body); + font-size: 0.875rem; font-variant-numeric: tabular-nums; + -webkit-font-smoothing: antialiased; } :focus-visible { outline: 2px solid var(--ring); - outline-offset: 3px; + outline-offset: 2px; } ::selection { - background: var(--accent); - color: var(--accent-foreground); + background: var(--selection); + color: var(--selection-foreground); } [data-slot='card'] { - box-shadow: none; -} -[data-slot='card-title'] { - line-height: 1.4; -} -[data-drag-handle] { - padding-block: 0.875rem; -} -[data-drag-handle] h2 { - font-size: 0.875rem; - font-weight: 600; - letter-spacing: -0.01em; - padding-right: 5rem; + box-shadow: var(--shadow-card); } .chart-frame { background: var(--card); @@ -239,14 +220,16 @@ body { .ts-chart-host { font-variant-numeric: tabular-nums; } +.ts-chart__interactive-legend { + pointer-events: none !important; +} .ts-chart__interactive-legend button { + pointer-events: auto !important; border: 0 !important; - border-radius: 3px !important; + border-radius: 4px !important; background: transparent !important; color: var(--chart-text-color) !important; - font: - 500 12px/1.25 system-ui, - sans-serif !important; + font: 500 12px/1.25 var(--font-sans) !important; } .ts-chart__interactive-legend button:hover { background: var(--muted) !important; @@ -258,24 +241,6 @@ body { table { font-variant-numeric: tabular-nums; } -th { - color: var(--muted-foreground); - font-size: var(--type-label); - font-weight: 500; -} -@media (max-width: 639px) { - .page-layout { - padding-block: 1rem; - } - .chart-frame.traffic-frame { - --chart-height: 380px; - } - .file-filters > div { - width: 100%; - flex-direction: column; - align-items: stretch; - } -} @media (prefers-reduced-motion: reduce) { *, *::before, @@ -284,380 +249,3 @@ th { transition-duration: 0.01ms !important; } } - -.ts-chart__interactive-legend { - pointer-events: none !important; -} -.ts-chart__interactive-legend button { - pointer-events: auto !important; -} - -.app-topbar .shell-width { - padding-inline: clamp(0.75rem, 1.5vw, 1.5rem); -} -.console-page { - width: 100%; - max-width: 1920px; - margin-inline: auto; - padding: 1rem clamp(0.75rem, 1.5vw, 1.5rem) 1.5rem; -} -.console-heading { - display: flex; - align-items: end; - justify-content: space-between; - gap: 1.5rem; - margin-bottom: 1rem; -} -.console-heading .page-heading { - font-size: 1.25rem; -} -.console-heading .dataset-tabs { - border-bottom: 0; -} -.console-body { - display: grid; - grid-template-columns: 12.5rem minmax(0, 1fr); - gap: 1rem; - align-items: start; -} -.console-body.rail-collapsed { - grid-template-columns: minmax(0, 1fr); -} -.console-rail { - position: sticky; - top: 1rem; - min-width: 0; - max-height: calc(100dvh - 7rem); - overflow-y: auto; - padding: 0.75rem; - background: var(--card); - border: 1px solid var(--border); - border-top: 2px solid var(--primary); -} -.console-toolbar { - position: sticky; - top: 0; - z-index: 20; - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 0.5rem; - margin-bottom: 1rem; - padding: 0.75rem; - background: var(--card); - border: 1px solid var(--border); -} -.console-toolbar .date-range-filter { - flex: 1 1 28rem; -} -.console-toolbar .rail-toggle, -.console-toolbar .reset-view, -.console-toolbar input[type='date'] { - height: 2.25rem; -} -.console-toolbar .date-range-filter { - align-items: center; - grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); - gap: 0.5rem; -} -.console-toolbar .date-range-filter > div { - display: flex; - align-items: center; - gap: 0.5rem; -} -.console-toolbar .date-range-filter label { - margin: 0; - flex-shrink: 0; -} -.console-toolbar .date-range-filter > span { - padding: 0; -} -.filter-summary { - display: flex; - flex-wrap: wrap; - gap: 0.375rem; - flex-basis: 100%; - order: 4; - padding-top: 0.5rem; - border-top: 1px solid var(--border); - font-size: 0.6875rem; - color: var(--muted-foreground); -} -.filter-summary span { - white-space: nowrap; - padding: 0.125rem 0.5rem; - border: 1px solid var(--border); - border-radius: 0.25rem; - background: var(--muted); -} -.rail-heading { - font-size: 0.75rem; - font-weight: 600; - margin-bottom: 0.75rem; -} -.rail-section { - padding-block: 1rem; - border-bottom: 1px solid var(--border); -} -.rail-section:first-child { - padding-top: 0; -} -.primary-filters .router-filter { - display: block; -} -.primary-filters .router-filter > span { - display: block; - font-size: 0.75rem; - color: var(--muted-foreground); - margin-bottom: 0.5rem; -} -.primary-filters .router-filter > div { - flex-direction: column; - align-items: stretch; - gap: 0.25rem; -} -.primary-filters .router-filter label span { - overflow-wrap: anywhere; - min-width: 0; - font-size: 0.8125rem; -} -.primary-filters [role='group']:not([aria-label='MAAD options']) { - width: 100%; -} -.primary-filters [aria-label='Traffic direction'], -.primary-filters [aria-label='Granularity'], -.primary-filters [aria-label='MAAD measure'] { - grid-template-columns: repeat(2, minmax(0, 1fr)); -} -.primary-filters [aria-label='Granularity'] > :last-child, -.primary-filters [aria-label='Traffic direction'] > :first-child, -.primary-filters [aria-label='MAAD measure'] > :first-child { - grid-column: 1 / -1; -} -.primary-filters [role='group'] button, -.primary-filters .router-filter label { - min-height: 2.75rem; -} -.primary-filters .router-filter label { - display: flex; - align-items: center; -} - -@media (pointer: fine) { - .console-rail .primary-filters [role='group'] button { - height: 2rem; - min-height: 2rem; - } - .console-rail .primary-filters .router-filter label { - min-height: 2rem; - } - .console-rail .rail-section { - padding-block: 0.25rem; - } - .console-rail .rail-heading { - margin-bottom: 0.5rem; - } - .console-rail .rail-section:first-child > div { - margin-bottom: 0.25rem; - } - .console-rail .rail-section:first-child .rail-heading { - margin-bottom: 0; - } - .console-rail .rail-section:first-child { - padding-top: 0; - } -} - -.card-actions { - position: absolute; - top: 0.25rem; - right: 0.5rem; - z-index: 10; - display: flex; - align-items: center; - gap: 0.125rem; -} -.card-actions button { - height: 2.25rem; - width: 2.25rem; -} -.chart-panels [data-drag-handle] { - min-height: 2.75rem; - padding-right: 10.5rem; -} -.chart-panels [data-drag-handle] h2 { - padding-right: 0; -} -.chart-panels > section > [data-slot='card'] { - border-radius: 0; -} -.chart-panels [data-drag-handle] { - background: var(--card); - padding-block: 0.75rem; -} -.chart-panels [data-slot='card-content'] { - padding: 0; -} -.chart-panels .chart-frame { - padding: 0.75rem; -} -.chart-panels .metric-selector { - padding: 0.5rem 0.75rem; - border-bottom: 1px solid var(--border); - background: var(--muted); -} -.metric-toolbar > [role='group'] { - width: auto; - margin-left: auto; -} -.metric-quickselect [role='group'] { - width: auto; -} -.metric-toolbar [role='group'] { - background: var(--card); -} -.chart-panels .traffic-frame { - --chart-height: 520px; -} -.chart-panels > [data-chart-id='dashboard'][data-chart-activated='false'] { - min-height: 740px !important; -} -.chart-panels [data-slot='card-content'] > [role='group'], -.chart-panels [data-slot='card-content'] > .flex, -.chart-panels [data-slot='card-content'] > .grid { - padding: 0.75rem; - margin: 0; - border-bottom: 1px solid var(--border); -} -.control-popover { - max-height: calc(100dvh - 2rem); - overflow-y: auto; -} -.file-summary { - padding: 1rem; -} -.file-router-card { - display: grid; - grid-template-columns: minmax(16rem, 0.8fr) minmax(0, 2fr); -} -.file-router-card > .file-summary { - border-right: 1px solid var(--border); - border-bottom: 0; -} -.file-router-card .file-metrics { - grid-template-columns: minmax(0, 1fr); -} -@media (min-width: 1600px) { - .file-router-card .file-metrics { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } -} -.catalog { - border: 1px solid var(--border); - background: var(--card); -} -.catalog-row { - display: grid; - grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) auto; - align-items: center; - gap: 1rem; - padding: 1rem; - border-bottom: 1px solid var(--border); -} -.catalog-row:last-child { - border-bottom: 0; -} -.catalog-row:hover { - background: var(--accent); -} -.catalog-header { - color: var(--muted-foreground); - font-size: 0.75rem; - padding-block: 0.625rem; - background: var(--muted); -} -.file-context p { - margin-bottom: 0.5rem; - overflow-wrap: anywhere; -} -@media (max-width: 1279px) { - .file-router-card { - grid-template-columns: minmax(0, 1fr); - } - .file-router-card > .file-summary { - border-right: 0; - border-bottom: 1px solid var(--border); - } -} -@media (max-width: 1023px) { - .console-body { - grid-template-columns: minmax(0, 1fr); - } - .console-heading { - align-items: start; - } - .console-toolbar .date-range-filter { - flex: 1 1 28rem; - } - .filter-sheet .primary-filters { - width: 100%; - } -} -@media (max-width: 639px) { - .console-heading { - flex-direction: column; - gap: 0.25rem; - } - .console-heading .dataset-tabs { - width: 100%; - } - .console-toolbar { - gap: 0.5rem; - padding: 0.625rem; - } - .console-toolbar .date-range-filter { - order: 3; - flex-basis: 100%; - } - .console-toolbar .date-range-filter > div { - display: block; - } - .console-toolbar .date-range-filter label { - margin-bottom: 0.25rem; - } - .console-toolbar .date-range-filter { - align-items: end; - } - .console-toolbar .date-range-filter > span { - padding-bottom: 0.5rem; - } - .filter-summary { - flex-basis: 100%; - order: 4; - } - .metric-toolbar > [role='group'] { - margin-left: 0; - } - .metric-quickselect { - width: 100%; - } - .metric-quickselect [role='group'] { - width: 100%; - } - .chart-panels .traffic-frame { - --chart-height: 400px; - } - .chart-panels > [data-chart-id='dashboard'][data-chart-activated='false'] { - min-height: 560px !important; - } - .catalog-row { - grid-template-columns: minmax(0, 1fr) auto; - } - .catalog-row .catalog-mode { - grid-column: 1; - grid-row: 2; - } - .catalog-header { - display: none; - } -} diff --git a/apps/web/src/lib/components/charts/BreakdownChart.svelte b/apps/web/src/lib/components/charts/BreakdownChart.svelte index ac31cb1..f90f6cc 100644 --- a/apps/web/src/lib/components/charts/BreakdownChart.svelte +++ b/apps/web/src/lib/components/charts/BreakdownChart.svelte @@ -19,9 +19,10 @@ import { navigateToNetflowFile } from '#lib/utils/netflow-file-navigation.ts'; import { dateStringToEpochPST, formatDateAsPSTDateString } from '#lib/utils/timezone.ts'; import { ensureCachedWindow, readCachedWindow, type TimeRange } from '#lib/utils/window-cache.ts'; - import { Checkbox } from '#lib/components/ui/checkbox/index.ts'; import { Skeleton } from '#lib/components/ui/skeleton/index.ts'; - import SegmentedControl from '#lib/components/common/SegmentedControl.svelte'; + import ChecklistPopover from '#lib/components/common/ChecklistPopover.svelte'; + import SegmentedToggle from '#lib/components/common/SegmentedToggle.svelte'; + import ToolbarSelect from '#lib/components/common/ToolbarSelect.svelte'; import ChartCard from './ChartCard.svelte'; import { getSourceLineDash } from './flow-characteristics'; import { sourceSeriesColor, categoricalSeriesColor } from './chart-colors'; @@ -210,6 +211,10 @@ activeMetrics = next; props.onMetricsChange?.({ metrics: next as MetricsForKind }); } + function handleSetAllMetrics(checked: boolean) { + activeMetrics = checked ? config.metrics.map((metric) => metric.key) : []; + props.onMetricsChange?.({ metrics: activeMetrics as MetricsForKind }); + } function handleRouterChange(router: string) { props.onRouterChange?.({ router }); } @@ -440,65 +445,53 @@ > {#snippet controls()} {#if props.kind === 'spectrum'} -
- {#if (props.availableRouters ?? []).length === 0} -
+ {/if} + {:else if config.seriesByRouter} {@const selected = splitDimensionMetricKey( (activeMetrics[0] ?? config.defaultMetrics[0]) as DimensionMetricKey )} -
- handleDimensionChange(side, selected.order)} - ariaLabel="MAAD address side" - class="grid-cols-2" - buttonClass="sm:min-w-20" - /> - handleDimensionChange(selected.side, order)} - ariaLabel="MAAD dimension" - class="grid-cols-3" - buttonClass="sm:min-w-12" - /> -
+ handleDimensionChange(side, selected.order)} + ariaLabel="MAAD address side" + /> + handleDimensionChange(selected.side, order)} + ariaLabel="MAAD dimension" + /> {:else} -
- {#each config.metrics as metric (metric.key)} - - {/each} -
+ ({ + key: metric.key, + label: metric.label, + checked: activeMetrics.includes(metric.key) + }))} + onToggle={handleMetricToggle} + onSetAll={handleSetAllMetrics} + /> {/if} {/snippet} diff --git a/apps/web/src/lib/components/charts/CardMenu.svelte b/apps/web/src/lib/components/charts/CardMenu.svelte new file mode 100644 index 0000000..6c28abc --- /dev/null +++ b/apps/web/src/lib/components/charts/CardMenu.svelte @@ -0,0 +1,56 @@ + + + + + {#snippet child({ props })} + + {/snippet} + + { + event.preventDefault(); + // Moving a card re-inserts its DOM node, so restore focus once the list has settled. + void tick().then(() => trigger?.focus()); + }} + > + + {frame.title} + frame.move(-1)} + >Move up + frame.move(1)} + >Move down + + {#if frame.resizable} + + + frame.resize(80)}>Taller + frame.resize(-80)}>Shorter + + {/if} + + diff --git a/apps/web/src/lib/components/charts/ChartCard.svelte b/apps/web/src/lib/components/charts/ChartCard.svelte index 0d0d0cd..bd52937 100644 --- a/apps/web/src/lib/components/charts/ChartCard.svelte +++ b/apps/web/src/lib/components/charts/ChartCard.svelte @@ -1,6 +1,6 @@ + +
+
+ {#if frame}
+ {#if controls || frame} +
+ {@render controls?.()} + {#if frame}{/if} +
+ {/if} +
diff --git a/apps/web/src/lib/components/charts/CoverageStrip.svelte b/apps/web/src/lib/components/charts/CoverageStrip.svelte index 9624757..555e774 100644 --- a/apps/web/src/lib/components/charts/CoverageStrip.svelte +++ b/apps/web/src/lib/components/charts/CoverageStrip.svelte @@ -33,7 +33,7 @@
-
-

Coverage

- -
+
{#if loading}
Loading coverage...
{:else if error} -
{error}
+
{error}
{:else if visibleTimelines.length === 0}
No coverage available
{:else} diff --git a/apps/web/src/lib/components/charts/FlowCharacteristicsChart.svelte b/apps/web/src/lib/components/charts/FlowCharacteristicsChart.svelte index a1bd603..2e6d2f6 100644 --- a/apps/web/src/lib/components/charts/FlowCharacteristicsChart.svelte +++ b/apps/web/src/lib/components/charts/FlowCharacteristicsChart.svelte @@ -1,5 +1,6 @@ + + +
+

{dialogLabel}

+ {#if onSetAll} +
+ + +
+ {/if} +
+
+ {#each items as item (item.key)} + + {/each} +
+
diff --git a/apps/web/src/lib/components/common/SegmentedToggle.svelte b/apps/web/src/lib/components/common/SegmentedToggle.svelte new file mode 100644 index 0000000..e2b51f5 --- /dev/null +++ b/apps/web/src/lib/components/common/SegmentedToggle.svelte @@ -0,0 +1,52 @@ + + + + + { + if (next && next !== value) onValueChange?.(next as T); + }} + spacing={1} + aria-label={ariaLabel} + class={cn('segmented bg-muted gap-0.5 rounded-md p-0.5', className)} +> + {#each options as option (option.value)} + {option.label} + {/each} + diff --git a/apps/web/src/lib/components/common/ToolbarPopover.svelte b/apps/web/src/lib/components/common/ToolbarPopover.svelte new file mode 100644 index 0000000..52edc6b --- /dev/null +++ b/apps/web/src/lib/components/common/ToolbarPopover.svelte @@ -0,0 +1,66 @@ + + + + + {#snippet child({ props })} + + {/snippet} + + + {@render children()} + + diff --git a/apps/web/src/lib/components/common/ToolbarSelect.svelte b/apps/web/src/lib/components/common/ToolbarSelect.svelte new file mode 100644 index 0000000..210f03a --- /dev/null +++ b/apps/web/src/lib/components/common/ToolbarSelect.svelte @@ -0,0 +1,71 @@ + + + + + { + if (next && next !== value) onValueChange(next as T); + }} + items={options.map((option) => ({ + value: option.value, + label: option.label, + disabled: option.disabled + }))} +> + + {label} + {selected?.label ?? value} + + + {#each options as option (option.value)} + + + {option.label} + {#if option.description}{option.description}{/if} + + + {/each} + + diff --git a/apps/web/src/lib/components/filters/DashboardToolbar.svelte b/apps/web/src/lib/components/filters/DashboardToolbar.svelte new file mode 100644 index 0000000..91a2cde --- /dev/null +++ b/apps/web/src/lib/components/filters/DashboardToolbar.svelte @@ -0,0 +1,110 @@ + + +
+ + + + {#if showDirection} + + {/if} + {#if measure} + + {/if} + + {#if startDate > endDate}{/if} +
diff --git a/apps/web/src/lib/components/filters/DateRangePicker.svelte b/apps/web/src/lib/components/filters/DateRangePicker.svelte new file mode 100644 index 0000000..d7362a6 --- /dev/null +++ b/apps/web/src/lib/components/filters/DateRangePicker.svelte @@ -0,0 +1,126 @@ + + + { + if (next) syncDraft(); + }} + {label} + ariaLabel={`Date range: ${label}`} + dialogLabel="Date range" +> + {#snippet icon()}{/snippet} +
+
+ {#each presets as preset (preset.id)} + + {/each} + +
+
+ { + if (next.start && next.end) + apply({ startDate: next.start.toString(), endDate: next.end.toString() }); + }} + /> +
+
+ + commitField('startDate', event.currentTarget.value.trim())} + /> +
+
+ + commitField('endDate', event.currentTarget.value.trim())} + /> +
+
+
+
+
diff --git a/apps/web/src/lib/components/filters/MaadOptions.svelte b/apps/web/src/lib/components/filters/MaadOptions.svelte new file mode 100644 index 0000000..6351cc5 --- /dev/null +++ b/apps/web/src/lib/components/filters/MaadOptions.svelte @@ -0,0 +1,74 @@ + + + +
+
+

Measure

+ +

+ Addresses counts each address once and includes the spectrum. Packets and Bytes weight each + address by its traffic. +

+
+
+

Address family

+ onIpVersionChange(Number(value) as MaadIpVersion)} + ariaLabel="MAAD address family" + class="w-full" + itemClass="flex-1" + /> +
+
+
diff --git a/apps/web/src/lib/components/filters/SourcesFilter.svelte b/apps/web/src/lib/components/filters/SourcesFilter.svelte new file mode 100644 index 0000000..b4c8129 --- /dev/null +++ b/apps/web/src/lib/components/filters/SourcesFilter.svelte @@ -0,0 +1,62 @@ + + + + + + + No sources found. + {#each names as name (name)} + onRoutersChange({ ...routers, [name]: !routers[name] })} + class="gap-2.5" + > + + {name} + + {/each} + + +
+ {selectedCount} selected +
+ + +
+
+
diff --git a/apps/web/src/lib/components/filters/TrafficMetricControls.svelte b/apps/web/src/lib/components/filters/TrafficMetricControls.svelte new file mode 100644 index 0000000..b91f903 --- /dev/null +++ b/apps/web/src/lib/components/filters/TrafficMetricControls.svelte @@ -0,0 +1,130 @@ + + + + +
+

NetFlow metrics

+
+ + +
+
+
+
+ + {#each PROTOCOL_COLUMNS as protocol (protocol.value)}{protocol.label}{/each} +
+ {#each matrix as metric (metric.value)} +
+ {metric.label} + {#each metric.cells as cell (cell.value)} + + {/each} +
+ {/each} +
+
diff --git a/apps/web/src/lib/components/filters/date-range.ts b/apps/web/src/lib/components/filters/date-range.ts new file mode 100644 index 0000000..93a5d8f --- /dev/null +++ b/apps/web/src/lib/components/filters/date-range.ts @@ -0,0 +1,61 @@ +import { parseDate } from '@internationalized/date'; + +const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/; + +export function isIsoDate(value: string): boolean { + if (!ISO_DATE.test(value)) return false; + try { + return parseDate(value).toString() === value; + } catch { + return false; + } +} + +const dayFormat = new Intl.DateTimeFormat('en-US', { + month: 'short', + day: 'numeric', + timeZone: 'UTC' +}); +const dayYearFormat = new Intl.DateTimeFormat('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + timeZone: 'UTC' +}); + +function utc(date: string): Date { + return new Date(`${date}T00:00:00Z`); +} + +/** Format an inclusive YYYY-MM-DD range compactly, e.g. "Mar 1 – Mar 3, 2025". */ +export function formatDateRange(startDate: string, endDate: string): string { + if (!isIsoDate(startDate) || !isIsoDate(endDate)) return `${startDate} – ${endDate}`; + if (startDate === endDate) return dayYearFormat.format(utc(startDate)); + const sameYear = startDate.slice(0, 4) === endDate.slice(0, 4); + return `${(sameYear ? dayFormat : dayYearFormat).format(utc(startDate))} – ${dayYearFormat.format(utc(endDate))}`; +} + +export type DateRangePreset = { id: string; label: string; startDate: string; endDate: string }; + +/** Window presets end at the selected end date; "All available" spans the dataset start to today. */ +export function dateRangePresets( + endDate: string, + datasetStartDate: string, + today: string +): DateRangePreset[] { + const anchor = isIsoDate(endDate) ? parseDate(endDate) : parseDate(today); + const windows = [ + { id: '1d', label: 'Last day', days: 1 }, + { id: '7d', label: 'Last 7 days', days: 7 }, + { id: '30d', label: 'Last 30 days', days: 30 } + ]; + return [ + ...windows.map(({ id, label, days }) => ({ + id, + label, + startDate: anchor.subtract({ days: days - 1 }).toString(), + endDate: anchor.toString() + })), + { id: 'all', label: 'All available', startDate: datasetStartDate, endDate: today } + ]; +} diff --git a/apps/web/src/lib/components/netflow/DashboardCardSlot.svelte b/apps/web/src/lib/components/netflow/DashboardCardSlot.svelte new file mode 100644 index 0000000..396c726 --- /dev/null +++ b/apps/web/src/lib/components/netflow/DashboardCardSlot.svelte @@ -0,0 +1,45 @@ + + +
+ {@render children()} +
diff --git a/apps/web/src/lib/components/netflow/DatasetDashboardPage.svelte b/apps/web/src/lib/components/netflow/DatasetDashboardPage.svelte index fb1661d..0d4ec8e 100644 --- a/apps/web/src/lib/components/netflow/DatasetDashboardPage.svelte +++ b/apps/web/src/lib/components/netflow/DatasetDashboardPage.svelte @@ -2,19 +2,19 @@ import { goto } from '$app/navigation'; import { page } from '$app/state'; import { onMount, untrack } from 'svelte'; - import DatasetTabs from '#lib/components/datasets/DatasetTabs.svelte'; - import AnalysisLayout from '#lib/components/common/AnalysisLayout.svelte'; - import DateRangeFilter from '#lib/components/filters/DateRangeFilter.svelte'; + import DashboardToolbar from '#lib/components/filters/DashboardToolbar.svelte'; + import ChartCardHeader from '#lib/components/charts/ChartCardHeader.svelte'; import { Button } from '#lib/components/ui/button/index.ts'; - import PrimaryFilters from '#lib/components/filters/PrimaryFilters.svelte'; + import * as Card from '#lib/components/ui/card/index.ts'; + import DashboardCardSlot from '#lib/components/netflow/DashboardCardSlot.svelte'; + import KpiRow from '#lib/components/netflow/KpiRow.svelte'; + import { createNetflowStatsData } from '#lib/components/netflow/netflow-stats-data.svelte.ts'; import NetflowDashboard from '#lib/components/netflow/NetflowDashboard.svelte'; import BreakdownChart from '#lib/components/charts/BreakdownChart.svelte'; import FlowCharacteristicsChart from '#lib/components/charts/FlowCharacteristicsChart.svelte'; import PortCardinalityChart from '#lib/components/charts/PortCardinalityChart.svelte'; import CoverageStrip from '#lib/components/charts/CoverageStrip.svelte'; import { createFlowCharacteristicsData } from '#lib/components/charts/flow-characteristics-data.svelte.ts'; - import CardLayoutControls from '#lib/components/common/CardLayoutControls.svelte'; - import DragGrip from '#lib/components/common/DragGrip.svelte'; import { DEFAULT_DATA_OPTIONS } from '#lib/components/netflow/constants.ts'; import { createNearViewportAttachment } from '#lib/components/netflow/near-viewport.ts'; import type { DataOption, GroupByOption, RouterConfig } from '#lib/components/netflow/types.ts'; @@ -206,7 +206,7 @@ maadUnavailableCopy ?? (maadMeasureHasSpectrum(measure) ? null - : 'The spectrum is only computed for the Addresses measure. Switch MAAD to Addresses in Controls to see it.') + : 'The spectrum is only computed for the Addresses measure. Set MAAD to Addresses in the toolbar to see it.') ); const routers = $derived(Array.isArray(props.routers) ? props.routers : []); const routerStateKey = $derived(`${props.dataset}:${routers.join('\0')}`); @@ -221,6 +221,16 @@ : (availableSpectrumRouters[0] ?? '') ); const routersLoaded = $derived(Array.isArray(props.routers)); + const today = new Date().toJSON().slice(0, 10); + const netflowStats = createNetflowStatsData(() => ({ + dataset: props.dataset, + startDate, + endDate, + groupBy: selectedGroupBy, + routers: selectedRouters, + routersLoaded, + direction + })); const flowCharacteristics = createFlowCharacteristicsData(() => ({ enabled: activatedCharts.characteristics || activatedCharts.ports, dataset: props.dataset, @@ -393,18 +403,6 @@ activateChart(chartOrder[0] ?? 'dashboard'); }); - function handleStartDateChange(payload: { startDate: string }) { - updateSearch({ startDate: payload.startDate }); - } - - function handleEndDateChange(payload: { endDate: string }) { - updateSearch({ endDate: payload.endDate }); - } - - function handleGroupByChange(payload: { groupBy: GroupByOption }) { - updateSearch({ groupBy: payload.groupBy }); - } - function handleDrillDown(payload: { groupBy: GroupByOption; startDate: string; @@ -421,11 +419,6 @@ void navigateToNetflowFile(goto, slug, props.dataset, direction, ipVersion, measure); } - function handleRoutersChange(payload: { routers: RouterConfig }) { - const nextRouters = payload.routers; - selectedRouters = nextRouters; - } - function handleDataOptionsChange(payload: { options: DataOption[] }) { dataOptions = payload.options; } @@ -434,23 +427,8 @@ ipMetrics = payload.metrics; } - function handleDirectionChange(payload: { direction: FlowDirection }) { - updateSearch({ direction: payload.direction }); - } - - function handleIpVersionChange(payload: { ipVersion: MaadIpVersion }) { - updateSearch({ ipVersion: payload.ipVersion }); - } - - function handleMeasureChange(payload: { measure: MaadMeasure }) { - updateSearch({ measure: payload.measure }); - } - function handleResetView() { - updateSearch({ - ...dateRangeSearch.defaults, - endDate: new Date().toJSON().slice(0, 10) - }); + updateSearch({ ...dateRangeSearch.defaults, endDate: today }); } @@ -459,51 +437,42 @@ - - {#snippet navigation()}{/snippet} - {#snippet toolbar()} - handleStartDateChange({ startDate: date })} - onEndDateChange={(date) => handleEndDateChange({ endDate: date })} - /> -

- {availableSpectrumRouters.length}/{routers.length} sources{direction === 'all' ? 'All directions' : direction}{selectedGroupBy === 'date' ? 'Daily' : selectedGroupBy}{#if maadComputed}MAAD IPv{ipVersion} · {measure}{/if} +

+
+

{props.title ?? props.dataset}

+

+ Click a chart to drill down · drag across a chart to select a range

- - {#if startDate > endDate}{/if} - {/snippet} - {#snippet rail()} - - {/snippet} -
+
+ updateSearch(patch)} + onGroupByChange={(groupBy) => updateSearch({ groupBy })} + onRoutersChange={(routers) => (selectedRouters = routers)} + onDirectionChange={(next) => updateSearch({ direction: next })} + onMeasureChange={(next) => updateSearch({ measure: next })} + onIpVersionChange={(next) => updateSearch({ ipVersion: next })} + onReset={handleResetView} + /> + +
{#each chartOrder as chartId, index (chartId)} -
moveCardBy(chartId, offset)} + onResize={(offset) => resizeCard(chartId, offset)} data-chart-id={chartId} data-chart-activated={activatedCharts[chartId]} class={`relative rounded-lg ${dropTargetChartId === chartId && draggedChartId && draggedChartId !== chartId ? 'ring-primary ring-offset-background ring-2 ring-offset-2' : ''}`} @@ -522,30 +491,14 @@ handleChartDrop(event, chartId); }} > - moveCardBy(chartId, offset)} - onResize={(offset) => { - resizeCard(chartId, offset); - }} - /> {#if !activatedCharts[chartId]} -
-
-

{CHART_CARD_DETAILS[chartId].title}

- -
+

This chart will load as it approaches the viewport.

- +
-
+ {:else if chartId === 'dashboard'} {/if} -
+ {/each}
- +
diff --git a/apps/web/src/lib/components/netflow/KpiRow.svelte b/apps/web/src/lib/components/netflow/KpiRow.svelte new file mode 100644 index 0000000..63274e2 --- /dev/null +++ b/apps/web/src/lib/components/netflow/KpiRow.svelte @@ -0,0 +1,92 @@ + + +
+ {#each kpis as kpi (kpi.id)} + +

{kpi.label}

+ {#if ready} +

+ {kpi.value} +

+

{kpi.detail}

+ {:else if stats.loading} + + + {:else} +

—

+

Unavailable

+ {/if} +
+ {/each} +
diff --git a/apps/web/src/lib/components/netflow/NetflowDashboard.svelte b/apps/web/src/lib/components/netflow/NetflowDashboard.svelte index 1eb8568..3b4ef73 100644 --- a/apps/web/src/lib/components/netflow/NetflowDashboard.svelte +++ b/apps/web/src/lib/components/netflow/NetflowDashboard.svelte @@ -1,63 +1,53 @@ - - -

Traffic Overview

-
- -
- - - - + + {#snippet controls()} + props.onDataOptionsChange?.({ options })} + /> + {#if ipFamilyOptions.length > 0} + (requestedIpFamily = family)} + ariaLabel="Traffic IP family" + /> + {/if} + (chartType = next)} + ariaLabel="Traffic chart type" + /> + {/snippet} + + +
{#if loading} {:else if error}
-
{error}
+
{error}
{:else if results.length === 0}
-
No data available for the selected filters
+
+ No data available for the selected filters +
{:else} []; + readonly availableIpFamilies: NetflowIpFamily[]; + readonly loading: boolean; + readonly error: string | null; +}; + +export type NetflowWindowTotals = { + flows: number; + flowsTcp: number; + packets: number; + bytes: number; + buckets: number; + completeBuckets: number; + partialBuckets: number; + unknownBuckets: number; +}; + +type StatsRequest = + | { kind: 'waiting' } + | { kind: 'invalid'; error: string } + | { + kind: 'fetch'; + id: string; + key: string; + requestedRange: TimeRange; + baseParams: Record; + missing: boolean; + }; + +type FetchRequest = Extract; + +type SettledRequest = { + id: string; + results: TimeBucket[]; + availableIpFamilies: NetflowIpFamily[]; + error: string | null; +}; + +const ipFamilyCache: Record = {}; +const ALL_FAMILIES: NetflowIpFamily[] = ['all']; + +function selectedRouters(routers: RouterConfig): string[] { + return Object.entries(routers) + .filter(([, enabled]) => enabled) + .map(([router]) => router.trim()) + .filter((router) => router.length > 0) + .sort(); +} + +function describeRequest(filtersKey: string): StatsRequest { + const filters = JSON.parse(filtersKey) as NetflowStatsFilters; + if (!filters.routersLoaded) return { kind: 'waiting' }; + const routers = selectedRouters(filters.routers); + if (routers.length === 0) { + return { kind: 'invalid', error: 'Select at least one source to view NetFlow statistics' }; + } + const key = JSON.stringify({ + chart: 'netflow', + dataset: filters.dataset, + groupBy: filters.groupBy, + routers, + direction: filters.direction + }); + const requestedRange = { + start: dateStringToEpochPST(filters.startDate), + end: dateStringToEpochPST(filters.endDate, true) + }; + return { + kind: 'fetch', + id: filtersKey, + key, + requestedRange, + baseParams: { + dataset: filters.dataset, + routers: routers.join(','), + groupBy: filters.groupBy, + direction: filters.direction + }, + missing: getMissingWindowRanges(key, requestedRange).length > 0 + }; +} + +async function fetchStats( + request: FetchRequest, + signal: AbortSignal +): Promise> { + await ensureCachedWindow>({ + key: request.key, + requestedRange: request.requestedRange, + signal, + fetchRange: async (range, signal) => { + const params = new URLSearchParams({ + ...request.baseParams, + startDate: range.start.toString(), + endDate: range.end.toString() + }); + const response = await fetch(`/api/netflow/stats?${params}`, { signal }); + if (!response.ok) { + const message = await response.text(); + throw new Error(message || `Failed to load data: ${response.statusText}`); + } + const json = (await response.json()) as NetflowStatsResponse; + ipFamilyCache[request.key] = json.availableIpFamilies; + return json.result; + }, + getRecordKey: (record) => `${record.bucketStart}`, + compareRecords: (left, right) => left.bucketStart - right.bucketStart + }); + return { + results: readCachedWindow>( + request.key, + request.requestedRange, + (record, range) => record.bucketStart >= range.start && record.bucketStart < range.end + ), + availableIpFamilies: ipFamilyCache[request.key] ?? ALL_FAMILIES + }; +} + +/** Share one cached /api/netflow/stats window between the KPI row and the traffic card. */ +export function createNetflowStatsData(getFilters: () => NetflowStatsFilters): NetflowStatsData { + const filtersKey = $derived(JSON.stringify(getFilters())); + const request = $derived(describeRequest(filtersKey)); + let settled = $state.raw(null); + const requestGate = createRequestGate(); + + $effect(() => { + if (request.kind !== 'fetch') return; + const current = request; + const token = requestGate.begin(); + const controller = new AbortController(); + fetchStats(current, controller.signal).then( + (data) => { + if (requestGate.isCurrent(token)) settled = { id: current.id, ...data, error: null }; + }, + (reason: unknown) => { + if (!requestGate.isCurrent(token)) return; + if (reason instanceof DOMException && reason.name === 'AbortError') return; + settled = { + id: current.id, + results: [], + availableIpFamilies: ALL_FAMILIES, + error: `Failed to load data: ${reason instanceof Error ? reason.message : 'Unknown error'}` + }; + } + ); + return () => { + requestGate.begin(); + controller.abort(); + }; + }); + + return { + get results() { + return request.kind === 'fetch' ? (settled?.results ?? []) : []; + }, + get availableIpFamilies() { + return request.kind === 'fetch' + ? (settled?.availableIpFamilies ?? ALL_FAMILIES) + : ALL_FAMILIES; + }, + get loading() { + if (request.kind === 'waiting') return true; + return request.kind === 'fetch' && settled?.id !== request.id && request.missing; + }, + get error() { + if (request.kind === 'invalid') return request.error; + return request.kind === 'fetch' && settled?.id === request.id ? settled.error : null; + } + }; +} + +/** Sum the additive totals of every observed bucket; buckets without data contribute nothing. */ +export function sumNetflowWindow(results: readonly TimeBucket[]) { + const totals: NetflowWindowTotals = { + flows: 0, + flowsTcp: 0, + packets: 0, + bytes: 0, + buckets: results.length, + completeBuckets: 0, + partialBuckets: 0, + unknownBuckets: 0 + }; + for (const bucket of results) { + if (bucket.coverage.state === 'complete') totals.completeBuckets += 1; + else if (bucket.coverage.state === 'partial') totals.partialBuckets += 1; + else totals.unknownBuckets += 1; + if (!bucket.data) continue; + totals.flows += bucket.data.flows; + totals.flowsTcp += bucket.data.flowsTcp; + totals.packets += bucket.data.packets; + totals.bytes += bucket.data.bytes; + } + return totals; +} diff --git a/apps/web/src/lib/components/shell/AppNav.svelte b/apps/web/src/lib/components/shell/AppNav.svelte new file mode 100644 index 0000000..af5c7a9 --- /dev/null +++ b/apps/web/src/lib/components/shell/AppNav.svelte @@ -0,0 +1,19 @@ + + + diff --git a/apps/web/src/lib/components/shell/CommandMenu.svelte b/apps/web/src/lib/components/shell/CommandMenu.svelte new file mode 100644 index 0000000..b38ebe2 --- /dev/null +++ b/apps/web/src/lib/components/shell/CommandMenu.svelte @@ -0,0 +1,95 @@ + + + { + if (event.key.toLowerCase() === 'k' && (event.metaKey || event.ctrlKey) && !event.altKey) { + event.preventDefault(); + setOpen(!open); + } + }} +/> + + + + + + + No results. + + {#each sectionsFor(datasetId) as item (item.section)} + go(item.section, datasetId)} + >{item.label} + {/each} + + {#if datasets && datasets.length > 0} + + {#each datasets as dataset (dataset.datasetId)} + go('dashboard', dataset.datasetId)} + >{dataset.label} + {dataset.datasetId} + {/each} + + {/if} + + run(() => theme.toggle())} + >Switch to {theme.dark ? 'light' : 'dark'} theme + + + diff --git a/apps/web/src/lib/components/shell/DatasetSwitcher.svelte b/apps/web/src/lib/components/shell/DatasetSwitcher.svelte new file mode 100644 index 0000000..ee379d2 --- /dev/null +++ b/apps/web/src/lib/components/shell/DatasetSwitcher.svelte @@ -0,0 +1,79 @@ + + + { + if (next && !datasets) onRequestDatasets(); + }} +> + + {#snippet child({ props })} + + {/snippet} + + + + + + {datasets ? 'No datasets found.' : 'Loading datasets…'} + {#if datasets} + + {#each datasets as dataset (dataset.datasetId)} + choose(dataset.datasetId)} + > + + {dataset.label} + {dataset.datasetId} + + + {/each} + + {/if} + + + + diff --git a/apps/web/src/lib/components/shell/app-context.ts b/apps/web/src/lib/components/shell/app-context.ts new file mode 100644 index 0000000..89f801c --- /dev/null +++ b/apps/web/src/lib/components/shell/app-context.ts @@ -0,0 +1,54 @@ +import { resolve } from '$app/paths'; +import type { DatasetSummary } from '#lib/datasets.ts'; + +export type AppSection = 'datasets' | 'dashboard' | 'files' | 'alerts'; + +type PageLike = { + params: Partial>; + url: { searchParams: Pick }; + data: Record; +}; + +export function pageDatasetId(page: PageLike): string | null { + const fromData = page.data.selectedDataset ?? page.data.dataset ?? page.data.datasetId; + return ( + page.params.dataset ?? + (page.url.searchParams.get('dataset')?.trim() || null) ?? + (typeof fromData === 'string' && fromData ? fromData : null) + ); +} + +export function pageDatasets(page: PageLike): DatasetSummary[] | null { + return Array.isArray(page.data.datasets) ? (page.data.datasets as DatasetSummary[]) : null; +} + +export function pageSection(pathname: string): AppSection { + if (pathname.startsWith('/netflow/files')) return 'files'; + if (/^\/datasets\/[^/]+\/alerts/.test(pathname)) return 'alerts'; + if (pathname.startsWith('/datasets/')) return 'dashboard'; + return 'datasets'; +} + +export function sectionHref(section: AppSection, datasetId: string | null): string { + if (section === 'files') { + return datasetId + ? `${resolve('/netflow/files')}?dataset=${encodeURIComponent(datasetId)}` + : resolve('/netflow/files'); + } + if (!datasetId || section === 'datasets') return resolve('/'); + if (section === 'alerts') return resolve('/datasets/[dataset]/alerts', { dataset: datasetId }); + return resolve('/datasets/[dataset]', { dataset: datasetId }); +} + +export function sectionsFor(datasetId: string | null): { section: AppSection; label: string }[] { + return datasetId + ? [ + { section: 'dashboard', label: 'Dashboard' }, + { section: 'files', label: 'Files' }, + { section: 'alerts', label: 'Alerts' } + ] + : [ + { section: 'datasets', label: 'Datasets' }, + { section: 'files', label: 'Files' } + ]; +} diff --git a/apps/web/src/lib/components/ui/empty/empty-content.svelte b/apps/web/src/lib/components/ui/empty/empty-content.svelte new file mode 100644 index 0000000..615c54d --- /dev/null +++ b/apps/web/src/lib/components/ui/empty/empty-content.svelte @@ -0,0 +1,23 @@ + + +
+ {@render children?.()} +
diff --git a/apps/web/src/lib/components/ui/empty/empty-description.svelte b/apps/web/src/lib/components/ui/empty/empty-description.svelte new file mode 100644 index 0000000..7e72937 --- /dev/null +++ b/apps/web/src/lib/components/ui/empty/empty-description.svelte @@ -0,0 +1,23 @@ + + +
a:hover]:text-primary text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4', + className + )} + {...restProps} +> + {@render children?.()} +
diff --git a/apps/web/src/lib/components/ui/empty/empty-header.svelte b/apps/web/src/lib/components/ui/empty/empty-header.svelte new file mode 100644 index 0000000..a9b0938 --- /dev/null +++ b/apps/web/src/lib/components/ui/empty/empty-header.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/apps/web/src/lib/components/ui/empty/empty-media.svelte b/apps/web/src/lib/components/ui/empty/empty-media.svelte new file mode 100644 index 0000000..335aa23 --- /dev/null +++ b/apps/web/src/lib/components/ui/empty/empty-media.svelte @@ -0,0 +1,41 @@ + + + + +
+ {@render children?.()} +
diff --git a/apps/web/src/lib/components/ui/empty/empty-title.svelte b/apps/web/src/lib/components/ui/empty/empty-title.svelte new file mode 100644 index 0000000..582ed2d --- /dev/null +++ b/apps/web/src/lib/components/ui/empty/empty-title.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/apps/web/src/lib/components/ui/empty/empty.svelte b/apps/web/src/lib/components/ui/empty/empty.svelte new file mode 100644 index 0000000..c3f2939 --- /dev/null +++ b/apps/web/src/lib/components/ui/empty/empty.svelte @@ -0,0 +1,23 @@ + + +
+ {@render children?.()} +
diff --git a/apps/web/src/lib/components/ui/empty/index.ts b/apps/web/src/lib/components/ui/empty/index.ts new file mode 100644 index 0000000..c74f8f3 --- /dev/null +++ b/apps/web/src/lib/components/ui/empty/index.ts @@ -0,0 +1,22 @@ +import Content from './empty-content.svelte'; +import Description from './empty-description.svelte'; +import Header from './empty-header.svelte'; +import Media from './empty-media.svelte'; +import Title from './empty-title.svelte'; +import Root from './empty.svelte'; + +export { + Root, + Header, + Media, + Title, + Description, + Content, + // + Root as Empty, + Header as EmptyHeader, + Media as EmptyMedia, + Title as EmptyTitle, + Description as EmptyDescription, + Content as EmptyContent +}; diff --git a/apps/web/src/lib/components/ui/native-select/index.ts b/apps/web/src/lib/components/ui/native-select/index.ts new file mode 100644 index 0000000..a455f70 --- /dev/null +++ b/apps/web/src/lib/components/ui/native-select/index.ts @@ -0,0 +1,12 @@ +import OptGroup from './native-select-opt-group.svelte'; +import Option from './native-select-option.svelte'; +import Root from './native-select.svelte'; + +export { + Root, + Option, + OptGroup, + Root as NativeSelect, + Option as NativeSelectOption, + OptGroup as NativeSelectOptGroup +}; diff --git a/apps/web/src/lib/components/ui/native-select/native-select-opt-group.svelte b/apps/web/src/lib/components/ui/native-select/native-select-opt-group.svelte new file mode 100644 index 0000000..fef240d --- /dev/null +++ b/apps/web/src/lib/components/ui/native-select/native-select-opt-group.svelte @@ -0,0 +1,20 @@ + + + + {@render children?.()} + diff --git a/apps/web/src/lib/components/ui/native-select/native-select-option.svelte b/apps/web/src/lib/components/ui/native-select/native-select-option.svelte new file mode 100644 index 0000000..2409071 --- /dev/null +++ b/apps/web/src/lib/components/ui/native-select/native-select-option.svelte @@ -0,0 +1,20 @@ + + + diff --git a/apps/web/src/lib/components/ui/native-select/native-select.svelte b/apps/web/src/lib/components/ui/native-select/native-select.svelte new file mode 100644 index 0000000..09ff888 --- /dev/null +++ b/apps/web/src/lib/components/ui/native-select/native-select.svelte @@ -0,0 +1,40 @@ + + +
+ + +
diff --git a/apps/web/src/routes/+layout.svelte b/apps/web/src/routes/+layout.svelte index 5389347..0f7ac67 100644 --- a/apps/web/src/routes/+layout.svelte +++ b/apps/web/src/routes/+layout.svelte @@ -8,8 +8,28 @@ import * as Tooltip from '#lib/components/ui/tooltip/index.ts'; import { theme } from '#lib/stores/theme.svelte.ts'; import Logo from '#lib/components/Logo.svelte'; + import AppNav from '#lib/components/shell/AppNav.svelte'; + import CommandMenu from '#lib/components/shell/CommandMenu.svelte'; + import DatasetSwitcher from '#lib/components/shell/DatasetSwitcher.svelte'; + import { pageDatasetId, pageDatasets, pageSection } from '#lib/components/shell/app-context.ts'; + import { + getCachedDatasetSummaries, + loadDatasetSummaries, + type DatasetSummary + } from '#lib/datasets.ts'; let { children } = $props(); + let fetchedDatasets = $state.raw(null); + const datasets = $derived(pageDatasets(page) ?? fetchedDatasets ?? getCachedDatasetSummaries()); + const datasetId = $derived(pageDatasetId(page)); + const section = $derived(pageSection(page.url.pathname)); + + function requestDatasets() { + void loadDatasetSummaries().then( + (loaded) => (fetchedDatasets = loaded), + () => {} + ); + } onMount(() => { theme.syncFromDom(); @@ -18,64 +38,54 @@ -
+
-
-
-
-

- -

-
- +
+ + + +
+ +
+
{@render children()}