build(ci): remove webui asset pipeline
- commit
- a021447f96063e20b2cdc1785814e219a0c0c996
- author
- brice · · 2026-04-11 19:18 UTC
- committer
- brice · · 2026-04-12 04:52 UTC
- parent
- 5d04e80
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
129 files · +93 -16767
M .github/dependabot.yml +0 -9
| @@ -9,15 +9,6 @@ updates: | |||
| 9 | 9 | patterns: | |
| 10 | 10 | - "*" | |
| 11 | 11 | ||
| 12 | - | - package-ecosystem: npm | |
| 13 | - | directory: /webui | |
| 14 | - | schedule: | |
| 15 | - | interval: weekly | |
| 16 | - | groups: | |
| 17 | - | npm-dependencies: | |
| 18 | - | patterns: | |
| 19 | - | - "*" | |
| 20 | - | ||
| 21 | 12 | - package-ecosystem: github-actions | |
| 22 | 13 | directory: / | |
| 23 | 14 | schedule: | |
M .github/workflows/aur.yml +1 -1
| @@ -51,7 +51,7 @@ jobs: | |||
| 51 | 51 | "arch=('x86_64')" \ | |
| 52 | 52 | 'url="https://github.com/bnema/dumber"' \ | |
| 53 | 53 | "license=('MIT')" \ | |
| 54 | - | "makedepends=('go>=1.25' 'npm' 'git')" \ | |
| 54 | + | "makedepends=('go>=1.25' 'git')" \ | |
| 55 | 55 | "depends=('gtk4' 'webkitgtk-6.0')" \ | |
| 56 | 56 | 'optdepends=(' \ | |
| 57 | 57 | " 'gst-plugins-base: Base media codecs'" \ | |
M .github/workflows/ci.yml +6 -22
| @@ -7,7 +7,6 @@ on: | |||
| 7 | 7 | - '**.go' | |
| 8 | 8 | - 'go.mod' | |
| 9 | 9 | - 'go.sum' | |
| 10 | - | - 'webui/**' | |
| 11 | 10 | - '.golangci.yml' | |
| 12 | 11 | - 'Makefile' | |
| 13 | 12 | - 'sqlc.yaml' | |
| @@ -21,7 +20,6 @@ on: | |||
| 21 | 20 | - '**.go' | |
| 22 | 21 | - 'go.mod' | |
| 23 | 22 | - 'go.sum' | |
| 24 | - | - 'webui/**' | |
| 25 | 23 | - '.golangci.yml' | |
| 26 | 24 | - 'Makefile' | |
| 27 | 25 | - 'sqlc.yaml' | |
| @@ -40,7 +38,7 @@ jobs: | |||
| 40 | 38 | - name: Install dependencies | |
| 41 | 39 | run: | | |
| 42 | 40 | pacman -Syu --noconfirm | |
| 43 | - | pacman -S --noconfirm git go nodejs npm webkitgtk-6.0 gtk4 gobject-introspection base-devel | |
| 41 | + | pacman -S --noconfirm git go webkitgtk-6.0 gtk4 gobject-introspection base-devel | |
| 44 | 42 | - uses: actions/checkout@v6 | |
| 45 | 43 | - name: Cache Go modules | |
| 46 | 44 | uses: actions/cache@v5 | |
| @@ -56,15 +54,8 @@ jobs: | |||
| 56 | 54 | key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }} | |
| 57 | 55 | restore-keys: | | |
| 58 | 56 | ${{ runner.os }}-go-build- | |
| 59 | - | - name: Cache npm | |
| 60 | - | uses: actions/cache@v5 | |
| 61 | - | with: | |
| 62 | - | path: ~/.npm | |
| 63 | - | key: ${{ runner.os }}-npm-${{ hashFiles('webui/package-lock.json') }} | |
| 64 | - | restore-keys: | | |
| 65 | - | ${{ runner.os }}-npm- | |
| 66 | - | - name: Build frontend assets | |
| 67 | - | run: make build-frontend | |
| 57 | + | - name: Build systemviews assets | |
| 58 | + | run: make build-systemviews | |
| 68 | 59 | - name: Install golangci-lint | |
| 69 | 60 | run: | | |
| 70 | 61 | curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/HEAD/install.sh | sh -s -- -b /usr/local/bin v2.9.0 | |
| @@ -81,7 +72,7 @@ jobs: | |||
| 81 | 72 | - name: Install dependencies | |
| 82 | 73 | run: | | |
| 83 | 74 | pacman -Syu --noconfirm | |
| 84 | - | pacman -S --noconfirm git go nodejs npm webkitgtk-6.0 gtk4 libadwaita gobject-introspection base-devel | |
| 75 | + | pacman -S --noconfirm git go webkitgtk-6.0 gtk4 libadwaita gobject-introspection base-devel | |
| 85 | 76 | - uses: actions/checkout@v6 | |
| 86 | 77 | - name: Cache Go modules | |
| 87 | 78 | uses: actions/cache@v5 | |
| @@ -97,15 +88,8 @@ jobs: | |||
| 97 | 88 | key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }} | |
| 98 | 89 | restore-keys: | | |
| 99 | 90 | ${{ runner.os }}-go-build- | |
| 100 | - | - name: Cache npm | |
| 101 | - | uses: actions/cache@v5 | |
| 102 | - | with: | |
| 103 | - | path: ~/.npm | |
| 104 | - | key: ${{ runner.os }}-npm-${{ hashFiles('webui/package-lock.json') }} | |
| 105 | - | restore-keys: | | |
| 106 | - | ${{ runner.os }}-npm- | |
| 107 | - | - name: Build frontend assets | |
| 108 | - | run: make build-frontend | |
| 91 | + | - name: Build systemviews assets | |
| 92 | + | run: make build-systemviews | |
| 109 | 93 | - name: Run tests | |
| 110 | 94 | env: | |
| 111 | 95 | GDK_BACKEND: wayland | |
D .github/workflows/webui.yml +0 -30
| @@ -1,30 +0,0 @@ | |||
| 1 | - | name: WebUI CI | |
| 2 | - | ||
| 3 | - | on: | |
| 4 | - | push: | |
| 5 | - | branches: [dev, main] | |
| 6 | - | paths: | |
| 7 | - | - 'webui/**' | |
| 8 | - | pull_request: | |
| 9 | - | paths: | |
| 10 | - | - 'webui/**' | |
| 11 | - | ||
| 12 | - | jobs: | |
| 13 | - | check: | |
| 14 | - | runs-on: ubuntu-latest | |
| 15 | - | steps: | |
| 16 | - | - uses: actions/checkout@v6 | |
| 17 | - | ||
| 18 | - | - uses: actions/setup-node@v6 | |
| 19 | - | with: | |
| 20 | - | node-version: '24' | |
| 21 | - | cache: 'npm' | |
| 22 | - | cache-dependency-path: 'webui/package-lock.json' | |
| 23 | - | ||
| 24 | - | - name: Install dependencies | |
| 25 | - | run: npm ci | |
| 26 | - | working-directory: webui | |
| 27 | - | ||
| 28 | - | - name: Run checks | |
| 29 | - | run: npm run check | |
| 30 | - | working-directory: webui | |
M .goreleaser.yaml +2 -2
| @@ -4,8 +4,8 @@ version: 2 | |||
| 4 | 4 | ||
| 5 | 5 | before: | |
| 6 | 6 | hooks: | |
| 7 | - | # Build frontend assets before building binaries | |
| 8 | - | - make build-frontend | |
| 7 | + | # Build systemviews assets before building binaries | |
| 8 | + | - make build-systemviews | |
| 9 | 9 | ||
| 10 | 10 | builds: | |
| 11 | 11 | # Pure Go build (no CGO required) | |
M CONTRIBUTING.md +2 -4
| @@ -34,10 +34,8 @@ git push origin my-fix | |||
| 34 | 34 | 2. Run `make test` and ensure tests pass | |
| 35 | 35 | 3. Run `go fmt` on any Go code changes | |
| 36 | 36 | ||
| 37 | - | **WebUI code (in `webui/`):** | |
| 38 | - | 1. Run `npm run lint` to fix linting issues | |
| 39 | - | 2. Run `npm run fmt` to format with Prettier | |
| 40 | - | 3. Run `npm run check` to run svelte-check | |
| 37 | + | **Systemviews assets:** | |
| 38 | + | 1. Run `make build-systemviews` to regenerate embedded WASM assets | |
| 41 | 39 | ||
| 42 | 40 | Keep commits focused and use single-line commit messages. | |
| 43 | 41 | ||
M Makefile +5 -12
| @@ -1,6 +1,6 @@ | |||
| 1 | 1 | # Makefile for dumber (Clean Architecture - puregotk) | |
| 2 | 2 | ||
| 3 | - | .PHONY: build build-frontend build-systemviews test lint clean install-tools dev generate help init man flatpak-deps flatpak-build flatpak-install flatpak-run flatpak-clean stress-omnibox-callbacks verify-purego check | |
| 3 | + | .PHONY: build build-systemviews test lint clean install-tools dev generate help init man flatpak-deps flatpak-build flatpak-install flatpak-run flatpak-clean stress-omnibox-callbacks verify-purego check | |
| 4 | 4 | ||
| 5 | 5 | # Load local overrides from .env.local if present (Makefile syntax) | |
| 6 | 6 | ifneq (,$(wildcard .env.local)) | |
| @@ -38,18 +38,13 @@ help: ## Show this help message | |||
| 38 | 38 | @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf " \033[36m%-15s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) | |
| 39 | 39 | ||
| 40 | 40 | # Build targets | |
| 41 | - | build: build-frontend build-systemviews ## Build the application (pure Go, no CGO) | |
| 41 | + | build: build-systemviews ## Build the application (pure Go, no CGO) | |
| 42 | 42 | @echo "Building $(BINARY_NAME) $(VERSION) using $(NPROCS) cores..." | |
| 43 | 43 | @mkdir -p $(DIST_DIR) | |
| 44 | 44 | CGO_ENABLED=0 go build -p $(NPROCS) $(GCFLAGS) $(LDFLAGS) -o $(DIST_DIR)/$(BINARY_NAME) $(MAIN_PATH) | |
| 45 | 45 | CGO_ENABLED=0 go build -p $(NPROCS) -ldflags "-s -w" -o $(DIST_DIR)/cef-helper ./cmd/cef-helper | |
| 46 | 46 | @echo "Build successful! Binary: $(DIST_DIR)/$(BINARY_NAME)" | |
| 47 | 47 | ||
| 48 | - | build-frontend: ## Build homepage and error pages | |
| 49 | - | @echo "Building webui pages (homepage + error)..." | |
| 50 | - | @cd webui && npm install --silent && npm run build | |
| 51 | - | @echo "Frontend build complete" | |
| 52 | - | ||
| 53 | 48 | build-systemviews: ## Build the WASM systemviews runtime | |
| 54 | 49 | @echo "Building systemviews wasm assets..." | |
| 55 | 50 | @mkdir -p assets/systemviews | |
| @@ -57,8 +52,8 @@ build-systemviews: ## Build the WASM systemviews runtime | |||
| 57 | 52 | GOOS=js GOARCH=wasm go build -o assets/systemviews/systemviews.wasm ./cmd/systemviews | |
| 58 | 53 | @echo "Systemviews build complete" | |
| 59 | 54 | ||
| 60 | - | build-quick: ## Build without frontend (faster for backend development) | |
| 61 | - | @echo "Building $(BINARY_NAME) $(VERSION) (quick, no frontend)..." | |
| 55 | + | build-quick: ## Build quickly for backend development | |
| 56 | + | @echo "Building $(BINARY_NAME) $(VERSION) (quick)..." | |
| 62 | 57 | @mkdir -p $(DIST_DIR) | |
| 63 | 58 | CGO_ENABLED=0 go build -p $(NPROCS) $(GCFLAGS) $(LDFLAGS) -o $(DIST_DIR)/$(BINARY_NAME) $(MAIN_PATH) | |
| 64 | 59 | CGO_ENABLED=0 go build -p $(NPROCS) -ldflags "-s -w" -o $(DIST_DIR)/cef-helper ./cmd/cef-helper | |
| @@ -130,8 +125,6 @@ clean: ## Clean build artifacts | |||
| 130 | 125 | @echo "Cleaning build artifacts..." | |
| 131 | 126 | rm -rf $(DIST_DIR) | |
| 132 | 127 | rm -f $(BINARY_NAME) | |
| 133 | - | rm -rf webui/dist webui/node_modules | |
| 134 | - | rm -f assets/webui/homepage.min.js assets/webui/error.min.js assets/webui/config.min.js assets/webui/index.html assets/webui/error.html assets/webui/config.html assets/webui/style.css | |
| 135 | 128 | rm -f assets/systemviews/wasm_exec.js assets/systemviews/systemviews.wasm | |
| 136 | 129 | rm -f coverage.out coverage.html | |
| 137 | 130 | go clean -cache | |
| @@ -168,7 +161,7 @@ man: build-quick ## Install man pages to ~/.local/share/man/man1/ | |||
| 168 | 161 | # Native release targets | |
| 169 | 162 | .PHONY: release-snapshot release | |
| 170 | 163 | ||
| 171 | - | release-snapshot: build-frontend ## Build snapshot using goreleaser | |
| 164 | + | release-snapshot: build-systemviews ## Build snapshot using goreleaser | |
| 172 | 165 | @echo "Building snapshot with goreleaser..." | |
| 173 | 166 | goreleaser release --snapshot --clean | |
| 174 | 167 | ||
M assets/webui_embed.go +4 -4
| @@ -4,11 +4,11 @@ import ( | |||
| 4 | 4 | "embed" | |
| 5 | 5 | ) | |
| 6 | 6 | ||
| 7 | - | // WebUIAssets contains the embedded webui and systemviews build output. | |
| 8 | - | // Used by the scheme handlers during the transition to serve dumb://home, | |
| 9 | - | // dumb://error, dumb://config, dumb://webrtc, dumb://history, and dumb://favorites. | |
| 7 | + | // WebUIAssets contains the embedded systemviews build output. | |
| 8 | + | // Used by the scheme handlers to serve dumb://error, dumb://config, | |
| 9 | + | // dumb://history, and dumb://favorites. | |
| 10 | 10 | // | |
| 11 | - | //go:embed webui/* systemviews/* | |
| 11 | + | //go:embed systemviews/* | |
| 12 | 12 | var WebUIAssets embed.FS | |
| 13 | 13 | ||
| 14 | 14 | // LogoSVG contains the dumber logo as SVG for desktop icon installation. | |
M aur/dumber-browser-git/.SRCINFO +0 -1
| @@ -6,7 +6,6 @@ pkgbase = dumber-browser-git | |||
| 6 | 6 | arch = x86_64 | |
| 7 | 7 | license = MIT | |
| 8 | 8 | makedepends = go>=1.25 | |
| 9 | - | makedepends = npm | |
| 10 | 9 | makedepends = git | |
| 11 | 10 | depends = gtk4 | |
| 12 | 11 | depends = webkitgtk-6.0 | |
M aur/dumber-browser-git/PKGBUILD +1 -1
| @@ -6,7 +6,7 @@ pkgdesc="A minimal keyboard-driven browser for tiling WMs (git version)" | |||
| 6 | 6 | arch=('x86_64') | |
| 7 | 7 | url="https://github.com/bnema/dumber" | |
| 8 | 8 | license=('MIT') | |
| 9 | - | makedepends=('go>=1.25' 'npm' 'git') | |
| 9 | + | makedepends=('go>=1.25' 'git') | |
| 10 | 10 | depends=('gtk4' 'webkitgtk-6.0') | |
| 11 | 11 | optdepends=( | |
| 12 | 12 | 'gst-plugins-base: Base media codecs' | |
M docs/config/index.md +1 -1
| @@ -201,7 +201,7 @@ border = "#363636" | |||
| 201 | 201 | **Example:** | |
| 202 | 202 | ```toml | |
| 203 | 203 | [workspace] | |
| 204 | - | new_pane_url = "dumb://home" | |
| 204 | + | new_pane_url = "dumb://history" | |
| 205 | 205 | ``` | |
| 206 | 206 | ||
| 207 | 207 | ### Pane Mode | |
M internal/domain/url/normalize_test.go +4 -4
| @@ -34,8 +34,8 @@ func TestNormalize(t *testing.T) { | |||
| 34 | 34 | }, | |
| 35 | 35 | { | |
| 36 | 36 | name: "dumb scheme unchanged", | |
| 37 | - | input: "dumb://home", | |
| 38 | - | want: "dumb://home", | |
| 37 | + | input: "dumb://history", | |
| 38 | + | want: "dumb://history", | |
| 39 | 39 | }, | |
| 40 | 40 | { | |
| 41 | 41 | name: "about scheme unchanged", | |
| @@ -347,7 +347,7 @@ func TestLooksLikeURL(t *testing.T) { | |||
| 347 | 347 | }, | |
| 348 | 348 | { | |
| 349 | 349 | name: "dumb URL", | |
| 350 | - | input: "dumb://home", | |
| 350 | + | input: "dumb://history", | |
| 351 | 351 | want: true, | |
| 352 | 352 | }, | |
| 353 | 353 | { | |
| @@ -506,7 +506,7 @@ func TestIsExternalScheme(t *testing.T) { | |||
| 506 | 506 | }, | |
| 507 | 507 | { | |
| 508 | 508 | name: "dumb scheme", | |
| 509 | - | input: "dumb://home", | |
| 509 | + | input: "dumb://history", | |
| 510 | 510 | want: false, | |
| 511 | 511 | }, | |
| 512 | 512 | { | |
M internal/infrastructure/cef/internal_urls.go +4 -4
| @@ -39,7 +39,7 @@ func toActualInternalURL(raw string) string { | |||
| 39 | 39 | if page == "" { | |
| 40 | 40 | page = parsed.Opaque | |
| 41 | 41 | } | |
| 42 | - | if page == "" { | |
| 42 | + | if page == "" || !isInternalPageHost(page) { | |
| 43 | 43 | return raw | |
| 44 | 44 | } | |
| 45 | 45 | ||
| @@ -47,8 +47,8 @@ func toActualInternalURL(raw string) string { | |||
| 47 | 47 | trimmedPath := strings.Trim(parsed.Path, "/") | |
| 48 | 48 | switch { | |
| 49 | 49 | case trimmedPath == "": | |
| 50 | - | // Keep page roots at /home instead of /home/ so relative assets resolve | |
| 51 | - | // from the origin root. | |
| 50 | + | // Keep page roots at /history instead of /history/ so relative assets | |
| 51 | + | // resolve from the origin root. | |
| 52 | 52 | case strings.HasPrefix(trimmedPath, "api/"): | |
| 53 | 53 | actualPath = "/" + trimmedPath | |
| 54 | 54 | case path.Ext(trimmedPath) != "": | |
| @@ -102,7 +102,7 @@ func toConceptualInternalURL(raw string) string { | |||
| 102 | 102 | ||
| 103 | 103 | func isInternalPageHost(host string) bool { | |
| 104 | 104 | switch host { | |
| 105 | - | case homePath, historyPath, favoritesPath, configPath, webrtcPath, errorPath: | |
| 105 | + | case historyPath, favoritesPath, configPath, errorPath: | |
| 106 | 106 | return true | |
| 107 | 107 | default: | |
| 108 | 108 | return false | |
M internal/infrastructure/cef/internal_urls_test.go +20 -10
| @@ -12,14 +12,19 @@ func TestToActualInternalURL(t *testing.T) { | |||
| 12 | 12 | }{ | |
| 13 | 13 | { | |
| 14 | 14 | name: "page root", | |
| 15 | - | in: "dumb://home", | |
| 16 | - | want: "https://dumber.invalid/home", | |
| 15 | + | in: "dumb://history", | |
| 16 | + | want: "https://dumber.invalid/history", | |
| 17 | 17 | }, | |
| 18 | 18 | { | |
| 19 | 19 | name: "history page root", | |
| 20 | 20 | in: "dumb://history", | |
| 21 | 21 | want: "https://dumber.invalid/history", | |
| 22 | 22 | }, | |
| 23 | + | { | |
| 24 | + | name: "error page root", | |
| 25 | + | in: "dumb://error", | |
| 26 | + | want: "https://dumber.invalid/error", | |
| 27 | + | }, | |
| 23 | 28 | { | |
| 24 | 29 | name: "favorites page root", | |
| 25 | 30 | in: "dumb://favorites", | |
| @@ -32,18 +37,18 @@ func TestToActualInternalURL(t *testing.T) { | |||
| 32 | 37 | }, | |
| 33 | 38 | { | |
| 34 | 39 | name: "api path stays at origin root", | |
| 35 | - | in: "dumb://home/api/message", | |
| 40 | + | in: "dumb://history/api/message", | |
| 36 | 41 | want: "https://dumber.invalid/api/message", | |
| 37 | 42 | }, | |
| 38 | 43 | { | |
| 39 | 44 | name: "root asset stays at origin root", | |
| 40 | - | in: "dumb://home/favicon.ico", | |
| 45 | + | in: "dumb://history/favicon.ico", | |
| 41 | 46 | want: "https://dumber.invalid/favicon.ico", | |
| 42 | 47 | }, | |
| 43 | 48 | { | |
| 44 | 49 | name: "page subroute stays under page namespace", | |
| 45 | - | in: "dumb://home/crash?url=https%3A%2F%2Fexample.com", | |
| 46 | - | want: "https://dumber.invalid/home/crash?url=https%3A%2F%2Fexample.com", | |
| 50 | + | in: "dumb://history/crash?url=https%3A%2F%2Fexample.com", | |
| 51 | + | want: "https://dumber.invalid/history/crash?url=https%3A%2F%2Fexample.com", | |
| 47 | 52 | }, | |
| 48 | 53 | { | |
| 49 | 54 | name: "actual internal URL unchanged", | |
| @@ -72,14 +77,19 @@ func TestToConceptualInternalURL(t *testing.T) { | |||
| 72 | 77 | }{ | |
| 73 | 78 | { | |
| 74 | 79 | name: "page root", | |
| 75 | - | in: "https://dumber.invalid/home", | |
| 76 | - | want: "dumb://home", | |
| 80 | + | in: "https://dumber.invalid/history", | |
| 81 | + | want: "dumb://history", | |
| 77 | 82 | }, | |
| 78 | 83 | { | |
| 79 | 84 | name: "history page root", | |
| 80 | 85 | in: "https://dumber.invalid/history", | |
| 81 | 86 | want: "dumb://history", | |
| 82 | 87 | }, | |
| 88 | + | { | |
| 89 | + | name: "error page root", | |
| 90 | + | in: "https://dumber.invalid/error", | |
| 91 | + | want: "dumb://error", | |
| 92 | + | }, | |
| 83 | 93 | { | |
| 84 | 94 | name: "favorites page root", | |
| 85 | 95 | in: "https://dumber.invalid/favorites", | |
| @@ -92,8 +102,8 @@ func TestToConceptualInternalURL(t *testing.T) { | |||
| 92 | 102 | }, | |
| 93 | 103 | { | |
| 94 | 104 | name: "page subroute", | |
| 95 | - | in: "https://dumber.invalid/home/crash?url=https%3A%2F%2Fexample.com", | |
| 96 | - | want: "dumb://home/crash?url=https%3A%2F%2Fexample.com", | |
| 105 | + | in: "https://dumber.invalid/history/crash?url=https%3A%2F%2Fexample.com", | |
| 106 | + | want: "dumb://history/crash?url=https%3A%2F%2Fexample.com", | |
| 97 | 107 | }, | |
| 98 | 108 | { | |
| 99 | 109 | name: "root asset is left unchanged", | |
M internal/infrastructure/cef/scheme_handler.go +10 -15
| @@ -26,11 +26,9 @@ import ( | |||
| 26 | 26 | ||
| 27 | 27 | // Scheme path constants matching WebKit's naming. | |
| 28 | 28 | const ( | |
| 29 | - | homePath = "home" | |
| 30 | 29 | historyPath = "history" | |
| 31 | 30 | favoritesPath = "favorites" | |
| 32 | 31 | configPath = "config" | |
| 33 | - | webrtcPath = "webrtc" | |
| 34 | 32 | errorPath = "error" | |
| 35 | 33 | indexHTML = "index.html" | |
| 36 | 34 | maxSchemeTruncatedURLLength = 240 | |
| @@ -38,12 +36,10 @@ const ( | |||
| 38 | 36 | ||
| 39 | 37 | // pageRootFiles maps internal page hosts/paths to their HTML entry points. | |
| 40 | 38 | var pageRootFiles = map[string]string{ | |
| 41 | - | homePath: indexHTML, | |
| 42 | 39 | historyPath: indexHTML, | |
| 43 | 40 | favoritesPath: indexHTML, | |
| 44 | 41 | configPath: indexHTML, | |
| 45 | - | webrtcPath: "webrtc.html", | |
| 46 | - | errorPath: "error.html", | |
| 42 | + | errorPath: indexHTML, | |
| 47 | 43 | } | |
| 48 | 44 | ||
| 49 | 45 | // dumbSchemeHandler serves both the conceptual dumb:// URLs and the actual | |
| @@ -85,14 +81,14 @@ func newDumbSchemeHandler( | |||
| 85 | 81 | ctx: ctx, | |
| 86 | 82 | messageRouter: router, | |
| 87 | 83 | transcoder: transcoder, | |
| 88 | - | assetDir: "webui", | |
| 84 | + | assetDir: "systemviews", | |
| 89 | 85 | logger: log.With().Str("component", "scheme-handler").Logger(), | |
| 90 | 86 | currentConfigPayload: currentConfigPayload, | |
| 91 | 87 | defaultConfigPayload: defaultConfigPayload, | |
| 92 | 88 | }, nil | |
| 93 | 89 | } | |
| 94 | 90 | ||
| 95 | - | // setAssets sets the embedded filesystem containing webui assets. | |
| 91 | + | // setAssets sets the embedded filesystem containing systemviews assets. | |
| 96 | 92 | func (h *dumbSchemeHandler) setAssets(assets embed.FS) { | |
| 97 | 93 | h.mu.Lock() | |
| 98 | 94 | defer h.mu.Unlock() | |
| @@ -325,7 +321,7 @@ func (h *dumbSchemeHandler) handleAsset(u *url.URL) purecef.ResourceHandler { | |||
| 325 | 321 | } | |
| 326 | 322 | ||
| 327 | 323 | // resolveAssetPath maps either a dumb:// URL or the actual internal HTTPS URL | |
| 328 | - | // to a relative asset path inside assets/webui. | |
| 324 | + | // to a relative asset path inside assets/systemviews. | |
| 329 | 325 | func resolveAssetPath(u *url.URL) (assetDir, relPath string, ok bool) { | |
| 330 | 326 | if u == nil { | |
| 331 | 327 | return "", "", false | |
| @@ -378,7 +374,7 @@ func resolveActualAssetPath(u *url.URL) (assetDir, relPath string, ok bool) { | |||
| 378 | 374 | if len(parts) == 1 { | |
| 379 | 375 | return assetDirForPageHost(page), root, true | |
| 380 | 376 | } | |
| 381 | - | if page == homePath && parts[1] == "crash" { | |
| 377 | + | if page == historyPath && parts[1] == "crash" { | |
| 382 | 378 | return "", "", false | |
| 383 | 379 | } | |
| 384 | 380 | return assetDirForPageHost(page), parts[1], true | |
| @@ -388,16 +384,15 @@ func resolveActualAssetPath(u *url.URL) (assetDir, relPath string, ok bool) { | |||
| 388 | 384 | return "", "", false | |
| 389 | 385 | } | |
| 390 | 386 | ||
| 391 | - | // Serve root assets like homepage.min.js, style.css and favicon.png. | |
| 392 | - | return "webui", path, true | |
| 387 | + | return "", "", false | |
| 393 | 388 | } | |
| 394 | 389 | ||
| 395 | 390 | func assetDirForPageHost(host string) string { | |
| 396 | 391 | switch host { | |
| 397 | - | case historyPath, favoritesPath, configPath: | |
| 392 | + | case historyPath, favoritesPath, configPath, errorPath: | |
| 398 | 393 | return "systemviews" | |
| 399 | 394 | default: | |
| 400 | - | return "webui" | |
| 395 | + | return "" | |
| 401 | 396 | } | |
| 402 | 397 | } | |
| 403 | 398 | ||
| @@ -408,8 +403,8 @@ func isCEFCrashPageURL(u *url.URL) bool { | |||
| 408 | 403 | ||
| 409 | 404 | switch { | |
| 410 | 405 | case strings.EqualFold(u.Scheme, actualInternalScheme) && strings.EqualFold(u.Host, actualInternalHost): | |
| 411 | - | return strings.Trim(u.Path, "/") == homePath+"/crash" | |
| 412 | - | case strings.EqualFold(u.Scheme, "dumb") && u.Host == homePath: | |
| 406 | + | return strings.Trim(u.Path, "/") == historyPath+"/crash" | |
| 407 | + | case strings.EqualFold(u.Scheme, "dumb") && u.Host == historyPath: | |
| 413 | 408 | return strings.Trim(u.Path, "/") == "crash" | |
| 414 | 409 | default: | |
| 415 | 410 | return false | |
M internal/infrastructure/handlers/config.go +1 -1
| @@ -39,7 +39,7 @@ func (h *ConfigHandler) Handle(ctx context.Context, _ port.WebViewID, payload js | |||
| 39 | 39 | return nil, fmt.Errorf("invalid config format: %w", err) | |
| 40 | 40 | } | |
| 41 | 41 | ||
| 42 | - | log.Info().Msg("saving appearance configuration from webui") | |
| 42 | + | log.Info().Msg("saving appearance configuration") | |
| 43 | 43 | ||
| 44 | 44 | if err := h.saveConfig(ctx, payloadCfg); err != nil { | |
| 45 | 45 | log.Error().Err(err).Msg("failed to save config") | |
M internal/infrastructure/webkit/scheme_handler.go +15 -26
| @@ -21,16 +21,13 @@ import ( | |||
| 21 | 21 | ||
| 22 | 22 | // Scheme path constants | |
| 23 | 23 | const ( | |
| 24 | - | HomePath = "home" | |
| 25 | - | HistoryPath = "history" | |
| 26 | - | FavoritesPath = "favorites" | |
| 27 | - | ConfigPath = "config" | |
| 28 | - | WebRTCPath = "webrtc" | |
| 29 | - | ErrorPath = "error" | |
| 30 | - | CrashPath = "crash" | |
| 31 | - | IndexHTML = "index.html" | |
| 32 | - | SystemViewsIndexHTML = "systemviews/index.html" | |
| 33 | - | httpGET = "GET" | |
| 24 | + | HistoryPath = "history" | |
| 25 | + | FavoritesPath = "favorites" | |
| 26 | + | ConfigPath = "config" | |
| 27 | + | ErrorPath = "error" | |
| 28 | + | CrashPath = "crash" | |
| 29 | + | IndexHTML = "index.html" | |
| 30 | + | httpGET = "GET" | |
| 34 | 31 | ) | |
| 35 | 32 | ||
| 36 | 33 | // SchemeRequest represents a request to a custom URI scheme. | |
| @@ -65,7 +62,7 @@ func (f PageHandlerFunc) Handle(req *SchemeRequest) *SchemeResponse { | |||
| 65 | 62 | type DumbSchemeHandler struct { | |
| 66 | 63 | handlers map[string]PageHandler | |
| 67 | 64 | assets embed.FS | |
| 68 | - | assetDir string // default subdirectory within embed.FS (e.g., "webui") | |
| 65 | + | assetDir string // default subdirectory within embed.FS (e.g., "systemviews") | |
| 69 | 66 | logger zerolog.Logger | |
| 70 | 67 | mu sync.RWMutex | |
| 71 | 68 | currentConfigPayload func() ([]byte, error) | |
| @@ -78,7 +75,7 @@ func NewDumbSchemeHandler(ctx context.Context) *DumbSchemeHandler { | |||
| 78 | 75 | ||
| 79 | 76 | h := &DumbSchemeHandler{ | |
| 80 | 77 | handlers: make(map[string]PageHandler), | |
| 81 | - | assetDir: "webui", | |
| 78 | + | assetDir: "systemviews", | |
| 82 | 79 | logger: log.With().Str("component", "scheme-handler").Logger(), | |
| 83 | 80 | } | |
| 84 | 81 | ||
| @@ -88,7 +85,7 @@ func NewDumbSchemeHandler(ctx context.Context) *DumbSchemeHandler { | |||
| 88 | 85 | return h | |
| 89 | 86 | } | |
| 90 | 87 | ||
| 91 | - | // SetAssets sets the embedded filesystem containing webui assets. | |
| 88 | + | // SetAssets sets the embedded filesystem containing systemviews assets. | |
| 92 | 89 | func (h *DumbSchemeHandler) SetAssets(assets embed.FS) { | |
| 93 | 90 | h.mu.Lock() | |
| 94 | 91 | defer h.mu.Unlock() | |
| @@ -138,7 +135,7 @@ func (h *DumbSchemeHandler) registerDefaults() { | |||
| 138 | 135 | h.mu.RLock() | |
| 139 | 136 | build := h.currentConfigPayload | |
| 140 | 137 | h.mu.RUnlock() | |
| 141 | - | return h.buildConfigResponse(build) | |
| 138 | + | return buildConfigResponse(build) | |
| 142 | 139 | })) | |
| 143 | 140 | ||
| 144 | 141 | // API: Get default config (used by Reset Defaults in dumb://config) | |
| @@ -150,7 +147,7 @@ func (h *DumbSchemeHandler) registerDefaults() { | |||
| 150 | 147 | h.mu.RLock() | |
| 151 | 148 | build := h.defaultConfigPayload | |
| 152 | 149 | h.mu.RUnlock() | |
| 153 | - | return h.buildConfigResponse(build) | |
| 150 | + | return buildConfigResponse(build) | |
| 154 | 151 | })) | |
| 155 | 152 | } | |
| 156 | 153 | ||
| @@ -173,7 +170,7 @@ func buildCrashPageHTML(originalURI string) string { | |||
| 173 | 170 | return webutil.BuildCrashPageHTML(originalURI) | |
| 174 | 171 | } | |
| 175 | 172 | ||
| 176 | - | func (h *DumbSchemeHandler) buildConfigResponse(build func() ([]byte, error)) *SchemeResponse { | |
| 173 | + | func buildConfigResponse(build func() ([]byte, error)) *SchemeResponse { | |
| 177 | 174 | if build == nil { | |
| 178 | 175 | return &SchemeResponse{ | |
| 179 | 176 | Data: []byte(`{"error": "config payload builder not configured"}`), | |
| @@ -333,12 +330,10 @@ func resolveAssetPath(u *url.URL) (assetDir, relPath string, ok bool) { | |||
| 333 | 330 | assetDir string | |
| 334 | 331 | file string | |
| 335 | 332 | }{ | |
| 336 | - | HomePath: {assetDir: "webui", file: IndexHTML}, | |
| 337 | 333 | HistoryPath: {assetDir: "systemviews", file: IndexHTML}, | |
| 338 | 334 | FavoritesPath: {assetDir: "systemviews", file: IndexHTML}, | |
| 339 | 335 | ConfigPath: {assetDir: "systemviews", file: IndexHTML}, | |
| 340 | - | WebRTCPath: {assetDir: "webui", file: "webrtc.html"}, | |
| 341 | - | ErrorPath: {assetDir: "webui", file: "error.html"}, | |
| 336 | + | ErrorPath: {assetDir: "systemviews", file: IndexHTML}, | |
| 342 | 337 | } | |
| 343 | 338 | ||
| 344 | 339 | if root, ok := rootByHost[u.Host]; ok { | |
| @@ -350,14 +345,8 @@ func resolveAssetPath(u *url.URL) (assetDir, relPath string, ok bool) { | |||
| 350 | 345 | } | |
| 351 | 346 | ||
| 352 | 347 | switch u.Opaque { | |
| 353 | - | case HomePath: | |
| 354 | - | return "webui", IndexHTML, true | |
| 355 | - | case HistoryPath, FavoritesPath, ConfigPath: | |
| 348 | + | case HistoryPath, FavoritesPath, ConfigPath, ErrorPath: | |
| 356 | 349 | return "systemviews", IndexHTML, true | |
| 357 | - | case ErrorPath: | |
| 358 | - | return "webui", "error.html", true | |
| 359 | - | case WebRTCPath: | |
| 360 | - | return "webui", "webrtc.html", true | |
| 361 | 350 | default: | |
| 362 | 351 | return "", "", false | |
| 363 | 352 | } | |
M internal/infrastructure/webkit/scheme_handler_test.go +8 -32
| @@ -12,8 +12,8 @@ import ( | |||
| 12 | 12 | ) | |
| 13 | 13 | ||
| 14 | 14 | func TestCrashOriginalURI(t *testing.T) { | |
| 15 | - | assert.Equal(t, "https://example.com/path?q=1", crashOriginalURI("dumb://home/crash?url=https%3A%2F%2Fexample.com%2Fpath%3Fq%3D1")) | |
| 16 | - | assert.Empty(t, crashOriginalURI("dumb://home/crash")) | |
| 15 | + | assert.Equal(t, "https://example.com/path?q=1", crashOriginalURI("dumb://history/crash?url=https%3A%2F%2Fexample.com%2Fpath%3Fq%3D1")) | |
| 16 | + | assert.Empty(t, crashOriginalURI("dumb://history/crash")) | |
| 17 | 17 | assert.Empty(t, crashOriginalURI("%%%")) | |
| 18 | 18 | } | |
| 19 | 19 | ||
| @@ -38,8 +38,8 @@ func TestBuildCrashPageHTMLRejectsUnsafeReloadScheme(t *testing.T) { | |||
| 38 | 38 | } | |
| 39 | 39 | ||
| 40 | 40 | func TestBuildCrashPageHTMLAllowsDumbSchemeTarget(t *testing.T) { | |
| 41 | - | body := buildCrashPageHTML("dumb://home") | |
| 42 | - | assert.Contains(t, body, `data-target="dumb://home"`) | |
| 41 | + | body := buildCrashPageHTML("dumb://history") | |
| 42 | + | assert.Contains(t, body, `data-target="dumb://history"`) | |
| 43 | 43 | } | |
| 44 | 44 | ||
| 45 | 45 | func TestBuildCrashPageHTMLEscapesScriptBreakoutPayload(t *testing.T) { | |
| @@ -60,7 +60,7 @@ func TestRegisterDefaultsIncludesCrashHandler(t *testing.T) { | |||
| 60 | 60 | require.NotNil(t, crashHandler) | |
| 61 | 61 | ||
| 62 | 62 | resp := crashHandler.Handle(&SchemeRequest{ | |
| 63 | - | URI: "dumb://home/crash?url=https%3A%2F%2Fexample.com", | |
| 63 | + | URI: "dumb://history/crash?url=https%3A%2F%2Fexample.com", | |
| 64 | 64 | Path: "/crash", | |
| 65 | 65 | Method: "GET", | |
| 66 | 66 | Scheme: "dumb", | |
| @@ -82,7 +82,7 @@ func TestCrashHandlerSanitizesUnsafeURLQuery(t *testing.T) { | |||
| 82 | 82 | require.NotNil(t, crashHandler) | |
| 83 | 83 | ||
| 84 | 84 | resp := crashHandler.Handle(&SchemeRequest{ | |
| 85 | - | URI: "dumb://home/crash?url=javascript%3Aalert(1)", | |
| 85 | + | URI: "dumb://history/crash?url=javascript%3Aalert(1)", | |
| 86 | 86 | Path: "/crash", | |
| 87 | 87 | Method: "GET", | |
| 88 | 88 | Scheme: "dumb", | |
| @@ -91,32 +91,6 @@ func TestCrashHandlerSanitizesUnsafeURLQuery(t *testing.T) { | |||
| 91 | 91 | assert.Contains(t, string(resp.Data), `data-target=""`) | |
| 92 | 92 | } | |
| 93 | 93 | ||
| 94 | - | func TestHandleAsset_WebRTCRootServesInternalWebRTCTester(t *testing.T) { | |
| 95 | - | h := NewDumbSchemeHandler(context.Background()) | |
| 96 | - | h.SetAssets(assets.WebUIAssets) | |
| 97 | - | ||
| 98 | - | u, err := url.Parse("dumb://webrtc/") | |
| 99 | - | require.NoError(t, err) | |
| 100 | - | ||
| 101 | - | resp := h.handleAsset(u) | |
| 102 | - | require.NotNil(t, resp) | |
| 103 | - | assert.Equal(t, "text/html; charset=utf-8", resp.ContentType) | |
| 104 | - | assert.Contains(t, string(resp.Data), "webrtc.min.js") | |
| 105 | - | } | |
| 106 | - | ||
| 107 | - | func TestHandleAsset_WebRTCOpaqueFormServesInternalWebRTCTester(t *testing.T) { | |
| 108 | - | h := NewDumbSchemeHandler(context.Background()) | |
| 109 | - | h.SetAssets(assets.WebUIAssets) | |
| 110 | - | ||
| 111 | - | u, err := url.Parse("dumb:webrtc") | |
| 112 | - | require.NoError(t, err) | |
| 113 | - | ||
| 114 | - | resp := h.handleAsset(u) | |
| 115 | - | require.NotNil(t, resp) | |
| 116 | - | assert.Equal(t, "text/html; charset=utf-8", resp.ContentType) | |
| 117 | - | assert.Contains(t, string(resp.Data), "webrtc.min.js") | |
| 118 | - | } | |
| 119 | - | ||
| 120 | 94 | func TestHandleAsset_ConfigOpaqueFormServesSystemviewsShell(t *testing.T) { | |
| 121 | 95 | h := NewDumbSchemeHandler(context.Background()) | |
| 122 | 96 | h.SetAssets(assets.WebUIAssets) | |
| @@ -141,6 +115,8 @@ func TestHandleAsset_SystemviewsRootsServeShell(t *testing.T) { | |||
| 141 | 115 | "dumb:favorites", | |
| 142 | 116 | "dumb://config", | |
| 143 | 117 | "dumb:config", | |
| 118 | + | "dumb://error", | |
| 119 | + | "dumb:error", | |
| 144 | 120 | } | |
| 145 | 121 | ||
| 146 | 122 | for _, raw := range tests { | |
M internal/ui/app.go +1 -1
| @@ -1408,7 +1408,7 @@ func (a *App) createInitialTab(ctx context.Context) { | |||
| 1408 | 1408 | } | |
| 1409 | 1409 | ||
| 1410 | 1410 | // Create an initial tab using coordinator. | |
| 1411 | - | initialURL := "dumb://home" | |
| 1411 | + | initialURL := "dumb://history" | |
| 1412 | 1412 | if a.deps != nil && a.deps.InitialURL != "" { | |
| 1413 | 1413 | initialURL = a.deps.InitialURL | |
| 1414 | 1414 | } | |
M internal/ui/coordinator/content/appearance.go +1 -1
| @@ -14,7 +14,7 @@ import ( | |||
| 14 | 14 | ||
| 15 | 15 | const ( | |
| 16 | 16 | aboutBlankURI = "about:blank" | |
| 17 | - | crashPageURI = "dumb://home/crash" | |
| 17 | + | crashPageURI = "dumb://history/crash" | |
| 18 | 18 | logURLMaxLen = 80 | |
| 19 | 19 | oauthParentRefreshDebounce = 200 * time.Millisecond | |
| 20 | 20 | ||
M internal/ui/coordinator/content/callbacks.go +2 -2
| @@ -12,8 +12,8 @@ import ( | |||
| 12 | 12 | "github.com/bnema/puregotk/v4/gdk" | |
| 13 | 13 | ) | |
| 14 | 14 | ||
| 15 | - | // internalSchemePath is the path used in dumb:// URIs (replaces webkit.HomePath). | |
| 16 | - | const internalSchemePath = "home" | |
| 15 | + | // internalSchemePath is the host used in dumb:// crash-page URIs. | |
| 16 | + | const internalSchemePath = "history" | |
| 17 | 17 | ||
| 18 | 18 | func shouldRenderCrashPage(reason port.WebProcessTerminationReason) bool { | |
| 19 | 19 | switch reason { | |
M internal/ui/coordinator/content/content_termination_test.go +4 -4
| @@ -15,13 +15,13 @@ func TestShouldRenderCrashPage(t *testing.T) { | |||
| 15 | 15 | } | |
| 16 | 16 | ||
| 17 | 17 | func TestExtractOriginalURIFromCrashPage(t *testing.T) { | |
| 18 | - | assert.Equal(t, "https://example.com/path?q=1", extractOriginalURIFromCrashPage("dumb://home/crash?url=https%3A%2F%2Fexample.com%2Fpath%3Fq%3D1")) | |
| 19 | - | assert.Empty(t, extractOriginalURIFromCrashPage("dumb://home/crash")) | |
| 18 | + | assert.Equal(t, "https://example.com/path?q=1", extractOriginalURIFromCrashPage("dumb://history/crash?url=https%3A%2F%2Fexample.com%2Fpath%3Fq%3D1")) | |
| 19 | + | assert.Empty(t, extractOriginalURIFromCrashPage("dumb://history/crash")) | |
| 20 | 20 | assert.Equal(t, "https://example.com", extractOriginalURIFromCrashPage("https://example.com")) | |
| 21 | 21 | assert.Equal(t, "%%%bad", extractOriginalURIFromCrashPage("%%%bad")) | |
| 22 | 22 | } | |
| 23 | 23 | ||
| 24 | 24 | func TestBuildCrashPageURI(t *testing.T) { | |
| 25 | - | assert.Equal(t, "dumb://home/crash", buildCrashPageURI("")) | |
| 26 | - | assert.Equal(t, "dumb://home/crash?url=https%3A%2F%2Fexample.com%2Ffoo%3Fa%3D1%26b%3D2", buildCrashPageURI("https://example.com/foo?a=1&b=2")) | |
| 25 | + | assert.Equal(t, "dumb://history/crash", buildCrashPageURI("")) | |
| 26 | + | assert.Equal(t, "dumb://history/crash?url=https%3A%2F%2Fexample.com%2Ffoo%3Fa%3D1%26b%3D2", buildCrashPageURI("https://example.com/foo?a=1&b=2")) | |
| 27 | 27 | } | |
M internal/ui/standalone_runtime_test.go +1 -1
| @@ -74,7 +74,7 @@ func TestStandaloneExternalSchemeRules_CustomSchemeIsExternal(t *testing.T) { | |||
| 74 | 74 | } | |
| 75 | 75 | ||
| 76 | 76 | func TestStandaloneExternalSchemeRules_InternalSchemesStayInternal(t *testing.T) { | |
| 77 | - | for _, rawURL := range []string{"dumb://home", "file:///tmp/demo", "about:blank", "data:text/plain,hi"} { | |
| 77 | + | for _, rawURL := range []string{"dumb://history", "dumb://error", "file:///tmp/demo", "about:blank", "data:text/plain,hi"} { | |
| 78 | 78 | if urlutil.IsExternalScheme(rawURL) { | |
| 79 | 79 | t.Fatalf("expected %q to stay internal", rawURL) | |
| 80 | 80 | } | |
D webui/components.json +0 -16
| @@ -1,16 +0,0 @@ | |||
| 1 | - | { | |
| 2 | - | "$schema": "https://shadcn-svelte.com/schema.json", | |
| 3 | - | "tailwind": { | |
| 4 | - | "css": "src/styles/app.css", | |
| 5 | - | "baseColor": "neutral" | |
| 6 | - | }, | |
| 7 | - | "aliases": { | |
| 8 | - | "components": "$lib/components", | |
| 9 | - | "utils": "$lib/utils", | |
| 10 | - | "ui": "$lib/components/ui", | |
| 11 | - | "hooks": "$lib/hooks", | |
| 12 | - | "lib": "$lib" | |
| 13 | - | }, | |
| 14 | - | "typescript": true, | |
| 15 | - | "registry": "https://shadcn-svelte.com/registry" | |
| 16 | - | } | |
D webui/eslint.config.js +0 -59
| @@ -1,59 +0,0 @@ | |||
| 1 | - | import js from "@eslint/js"; | |
| 2 | - | import svelte from "eslint-plugin-svelte"; | |
| 3 | - | import globals from "globals"; | |
| 4 | - | import tseslint from "typescript-eslint"; | |
| 5 | - | import svelteConfig from "./svelte.config.js"; | |
| 6 | - | ||
| 7 | - | export default tseslint.config( | |
| 8 | - | js.configs.recommended, | |
| 9 | - | ...tseslint.configs.recommended, | |
| 10 | - | ...svelte.configs.recommended, | |
| 11 | - | { | |
| 12 | - | languageOptions: { | |
| 13 | - | globals: { | |
| 14 | - | ...globals.browser, | |
| 15 | - | }, | |
| 16 | - | }, | |
| 17 | - | }, | |
| 18 | - | { | |
| 19 | - | files: ["**/*.svelte", "**/*.svelte.ts", "**/*.svelte.js"], | |
| 20 | - | languageOptions: { | |
| 21 | - | parserOptions: { | |
| 22 | - | extraFileExtensions: [".svelte"], | |
| 23 | - | parser: tseslint.parser, | |
| 24 | - | svelteConfig, | |
| 25 | - | }, | |
| 26 | - | }, | |
| 27 | - | }, | |
| 28 | - | { | |
| 29 | - | files: ["**/*.{js,mjs,cjs,ts}"], | |
| 30 | - | languageOptions: { | |
| 31 | - | ecmaVersion: 2024, | |
| 32 | - | sourceType: "module", | |
| 33 | - | }, | |
| 34 | - | rules: { | |
| 35 | - | // Modern TypeScript best practices | |
| 36 | - | "@typescript-eslint/no-unused-vars": [ | |
| 37 | - | "error", | |
| 38 | - | { | |
| 39 | - | argsIgnorePattern: "^_", | |
| 40 | - | varsIgnorePattern: "^_", | |
| 41 | - | }, | |
| 42 | - | ], | |
| 43 | - | "@typescript-eslint/no-explicit-any": "warn", | |
| 44 | - | "@typescript-eslint/no-empty-function": "off", | |
| 45 | - | "@typescript-eslint/consistent-type-imports": "error", | |
| 46 | - | "@typescript-eslint/no-import-type-side-effects": "error", | |
| 47 | - | ||
| 48 | - | // Modern JavaScript/TypeScript patterns | |
| 49 | - | "prefer-const": "error", | |
| 50 | - | "no-var": "error", | |
| 51 | - | "object-shorthand": "error", | |
| 52 | - | "prefer-template": "error", | |
| 53 | - | }, | |
| 54 | - | }, | |
| 55 | - | { | |
| 56 | - | files: ["**/*.js", "**/*.mjs"], | |
| 57 | - | ...tseslint.configs.disableTypeChecked, | |
| 58 | - | }, | |
| 59 | - | ); | |
D webui/package-lock.json +0 -3903
| @@ -1,3903 +0,0 @@ | |||
| 1 | - | { | |
| 2 | - | "name": "@dumber/gui", | |
| 3 | - | "version": "0.1.0", | |
| 4 | - | "lockfileVersion": 3, | |
| 5 | - | "requires": true, | |
| 6 | - | "packages": { | |
| 7 | - | "": { | |
| 8 | - | "name": "@dumber/gui", | |
| 9 | - | "version": "0.1.0", | |
| 10 | - | "dependencies": { | |
| 11 | - | "bits-ui": "^2.16.3", | |
| 12 | - | "clsx": "^2.1.1", | |
| 13 | - | "frappe-charts": "^1.6.2", | |
| 14 | - | "mode-watcher": "^1.1.0", | |
| 15 | - | "tailwind-merge": "^3.5.0" | |
| 16 | - | }, | |
| 17 | - | "devDependencies": { | |
| 18 | - | "@internationalized/date": "^3.12.0", | |
| 19 | - | "@lucide/svelte": "^0.577.0", | |
| 20 | - | "@sveltejs/vite-plugin-svelte": "^7.0.0", | |
| 21 | - | "@tailwindcss/cli": "^4.2.2", | |
| 22 | - | "@tailwindcss/postcss": "^4.2.2", | |
| 23 | - | "@types/node": "^25.5.0", | |
| 24 | - | "@typescript-eslint/eslint-plugin": "^8.57.1", | |
| 25 | - | "@typescript-eslint/parser": "^8.57.1", | |
| 26 | - | "autoprefixer": "^10.4.27", | |
| 27 | - | "eslint": "^10.0.3", | |
| 28 | - | "eslint-plugin-svelte": "^3.15.2", | |
| 29 | - | "globals": "^17.4.0", | |
| 30 | - | "postcss": "^8.5.8", | |
| 31 | - | "svelte": "^5.54.0", | |
| 32 | - | "svelte-awesome-color-picker": "^4.1.0", | |
| 33 | - | "svelte-check": "^4.4.5", | |
| 34 | - | "tailwind-variants": "^3.2.2", | |
| 35 | - | "tailwindcss": "^4.0.0", | |
| 36 | - | "tw-animate-css": "^1.4.0", | |
| 37 | - | "typescript": "^5.3.0", | |
| 38 | - | "typescript-eslint": "^8.57.1", | |
| 39 | - | "vite": "^8.0.0" | |
| 40 | - | } | |
| 41 | - | }, | |
| 42 | - | "node_modules/@alloc/quick-lru": { | |
| 43 | - | "version": "5.2.0", | |
| 44 | - | "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", | |
| 45 | - | "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", | |
| 46 | - | "dev": true, | |
| 47 | - | "license": "MIT", | |
| 48 | - | "engines": { | |
| 49 | - | "node": ">=10" | |
| 50 | - | }, | |
| 51 | - | "funding": { | |
| 52 | - | "url": "https://github.com/sponsors/sindresorhus" | |
| 53 | - | } | |
| 54 | - | }, | |
| 55 | - | "node_modules/@emnapi/core": { | |
| 56 | - | "version": "1.9.0", | |
| 57 | - | "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.0.tgz", | |
| 58 | - | "integrity": "sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w==", | |
| 59 | - | "dev": true, | |
| 60 | - | "license": "MIT", | |
| 61 | - | "optional": true, | |
| 62 | - | "dependencies": { | |
| 63 | - | "@emnapi/wasi-threads": "1.2.0", | |
| 64 | - | "tslib": "^2.4.0" | |
| 65 | - | } | |
| 66 | - | }, | |
| 67 | - | "node_modules/@emnapi/runtime": { | |
| 68 | - | "version": "1.9.0", | |
| 69 | - | "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.0.tgz", | |
| 70 | - | "integrity": "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==", | |
| 71 | - | "dev": true, | |
| 72 | - | "license": "MIT", | |
| 73 | - | "optional": true, | |
| 74 | - | "dependencies": { | |
| 75 | - | "tslib": "^2.4.0" | |
| 76 | - | } | |
| 77 | - | }, | |
| 78 | - | "node_modules/@emnapi/wasi-threads": { | |
| 79 | - | "version": "1.2.0", | |
| 80 | - | "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz", | |
| 81 | - | "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==", | |
| 82 | - | "dev": true, | |
| 83 | - | "license": "MIT", | |
| 84 | - | "optional": true, | |
| 85 | - | "dependencies": { | |
| 86 | - | "tslib": "^2.4.0" | |
| 87 | - | } | |
| 88 | - | }, | |
| 89 | - | "node_modules/@eslint-community/eslint-utils": { | |
| 90 | - | "version": "4.9.1", | |
| 91 | - | "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", | |
| 92 | - | "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", | |
| 93 | - | "dev": true, | |
| 94 | - | "license": "MIT", | |
| 95 | - | "dependencies": { | |
| 96 | - | "eslint-visitor-keys": "^3.4.3" | |
| 97 | - | }, | |
| 98 | - | "engines": { | |
| 99 | - | "node": "^12.22.0 || ^14.17.0 || >=16.0.0" | |
| 100 | - | }, | |
| 101 | - | "funding": { | |
| 102 | - | "url": "https://opencollective.com/eslint" | |
| 103 | - | }, | |
| 104 | - | "peerDependencies": { | |
| 105 | - | "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" | |
| 106 | - | } | |
| 107 | - | }, | |
| 108 | - | "node_modules/@eslint-community/regexpp": { | |
| 109 | - | "version": "4.12.2", | |
| 110 | - | "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", | |
| 111 | - | "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", | |
| 112 | - | "dev": true, | |
| 113 | - | "license": "MIT", | |
| 114 | - | "engines": { | |
| 115 | - | "node": "^12.0.0 || ^14.0.0 || >=16.0.0" | |
| 116 | - | } | |
| 117 | - | }, | |
| 118 | - | "node_modules/@eslint/config-array": { | |
| 119 | - | "version": "0.23.3", | |
| 120 | - | "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.3.tgz", | |
| 121 | - | "integrity": "sha512-j+eEWmB6YYLwcNOdlwQ6L2OsptI/LO6lNBuLIqe5R7RetD658HLoF+Mn7LzYmAWWNNzdC6cqP+L6r8ujeYXWLw==", | |
| 122 | - | "dev": true, | |
| 123 | - | "license": "Apache-2.0", | |
| 124 | - | "dependencies": { | |
| 125 | - | "@eslint/object-schema": "^3.0.3", | |
| 126 | - | "debug": "^4.3.1", | |
| 127 | - | "minimatch": "^10.2.4" | |
| 128 | - | }, | |
| 129 | - | "engines": { | |
| 130 | - | "node": "^20.19.0 || ^22.13.0 || >=24" | |
| 131 | - | } | |
| 132 | - | }, | |
| 133 | - | "node_modules/@eslint/config-helpers": { | |
| 134 | - | "version": "0.5.2", | |
| 135 | - | "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.2.tgz", | |
| 136 | - | "integrity": "sha512-a5MxrdDXEvqnIq+LisyCX6tQMPF/dSJpCfBgBauY+pNZ28yCtSsTvyTYrMhaI+LK26bVyCJfJkT0u8KIj2i1dQ==", | |
| 137 | - | "dev": true, | |
| 138 | - | "license": "Apache-2.0", | |
| 139 | - | "dependencies": { | |
| 140 | - | "@eslint/core": "^1.1.0" | |
| 141 | - | }, | |
| 142 | - | "engines": { | |
| 143 | - | "node": "^20.19.0 || ^22.13.0 || >=24" | |
| 144 | - | } | |
| 145 | - | }, | |
| 146 | - | "node_modules/@eslint/core": { | |
| 147 | - | "version": "1.1.1", | |
| 148 | - | "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.1.1.tgz", | |
| 149 | - | "integrity": "sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ==", | |
| 150 | - | "dev": true, | |
| 151 | - | "license": "Apache-2.0", | |
| 152 | - | "dependencies": { | |
| 153 | - | "@types/json-schema": "^7.0.15" | |
| 154 | - | }, | |
| 155 | - | "engines": { | |
| 156 | - | "node": "^20.19.0 || ^22.13.0 || >=24" | |
| 157 | - | } | |
| 158 | - | }, | |
| 159 | - | "node_modules/@eslint/object-schema": { | |
| 160 | - | "version": "3.0.3", | |
| 161 | - | "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.3.tgz", | |
| 162 | - | "integrity": "sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ==", | |
| 163 | - | "dev": true, | |
| 164 | - | "license": "Apache-2.0", | |
| 165 | - | "engines": { | |
| 166 | - | "node": "^20.19.0 || ^22.13.0 || >=24" | |
| 167 | - | } | |
| 168 | - | }, | |
| 169 | - | "node_modules/@eslint/plugin-kit": { | |
| 170 | - | "version": "0.6.1", | |
| 171 | - | "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.6.1.tgz", | |
| 172 | - | "integrity": "sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ==", | |
| 173 | - | "dev": true, | |
| 174 | - | "license": "Apache-2.0", | |
| 175 | - | "dependencies": { | |
| 176 | - | "@eslint/core": "^1.1.1", | |
| 177 | - | "levn": "^0.4.1" | |
| 178 | - | }, | |
| 179 | - | "engines": { | |
| 180 | - | "node": "^20.19.0 || ^22.13.0 || >=24" | |
| 181 | - | } | |
| 182 | - | }, | |
| 183 | - | "node_modules/@floating-ui/core": { | |
| 184 | - | "version": "1.7.3", | |
| 185 | - | "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", | |
| 186 | - | "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", | |
| 187 | - | "license": "MIT", | |
| 188 | - | "dependencies": { | |
| 189 | - | "@floating-ui/utils": "^0.2.10" | |
| 190 | - | } | |
| 191 | - | }, | |
| 192 | - | "node_modules/@floating-ui/dom": { | |
| 193 | - | "version": "1.7.4", | |
| 194 | - | "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", | |
| 195 | - | "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", | |
| 196 | - | "license": "MIT", | |
| 197 | - | "dependencies": { | |
| 198 | - | "@floating-ui/core": "^1.7.3", | |
| 199 | - | "@floating-ui/utils": "^0.2.10" | |
| 200 | - | } | |
| 201 | - | }, | |
| 202 | - | "node_modules/@floating-ui/utils": { | |
| 203 | - | "version": "0.2.10", | |
| 204 | - | "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", | |
| 205 | - | "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", | |
| 206 | - | "license": "MIT" | |
| 207 | - | }, | |
| 208 | - | "node_modules/@humanfs/core": { | |
| 209 | - | "version": "0.19.1", | |
| 210 | - | "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", | |
| 211 | - | "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", | |
| 212 | - | "dev": true, | |
| 213 | - | "license": "Apache-2.0", | |
| 214 | - | "engines": { | |
| 215 | - | "node": ">=18.18.0" | |
| 216 | - | } | |
| 217 | - | }, | |
| 218 | - | "node_modules/@humanfs/node": { | |
| 219 | - | "version": "0.16.7", | |
| 220 | - | "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", | |
| 221 | - | "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", | |
| 222 | - | "dev": true, | |
| 223 | - | "license": "Apache-2.0", | |
| 224 | - | "dependencies": { | |
| 225 | - | "@humanfs/core": "^0.19.1", | |
| 226 | - | "@humanwhocodes/retry": "^0.4.0" | |
| 227 | - | }, | |
| 228 | - | "engines": { | |
| 229 | - | "node": ">=18.18.0" | |
| 230 | - | } | |
| 231 | - | }, | |
| 232 | - | "node_modules/@humanwhocodes/module-importer": { | |
| 233 | - | "version": "1.0.1", | |
| 234 | - | "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", | |
| 235 | - | "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", | |
| 236 | - | "dev": true, | |
| 237 | - | "license": "Apache-2.0", | |
| 238 | - | "engines": { | |
| 239 | - | "node": ">=12.22" | |
| 240 | - | }, | |
| 241 | - | "funding": { | |
| 242 | - | "type": "github", | |
| 243 | - | "url": "https://github.com/sponsors/nzakas" | |
| 244 | - | } | |
| 245 | - | }, | |
| 246 | - | "node_modules/@humanwhocodes/retry": { | |
| 247 | - | "version": "0.4.3", | |
| 248 | - | "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", | |
| 249 | - | "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", | |
| 250 | - | "dev": true, | |
| 251 | - | "license": "Apache-2.0", | |
| 252 | - | "engines": { | |
| 253 | - | "node": ">=18.18" | |
| 254 | - | }, | |
| 255 | - | "funding": { | |
| 256 | - | "type": "github", | |
| 257 | - | "url": "https://github.com/sponsors/nzakas" | |
| 258 | - | } | |
| 259 | - | }, | |
| 260 | - | "node_modules/@internationalized/date": { | |
| 261 | - | "version": "3.12.0", | |
| 262 | - | "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.12.0.tgz", | |
| 263 | - | "integrity": "sha512-/PyIMzK29jtXaGU23qTvNZxvBXRtKbNnGDFD+PY6CZw/Y8Ex8pFUzkuCJCG9aOqmShjqhS9mPqP6Dk5onQY8rQ==", | |
| 264 | - | "license": "Apache-2.0", | |
| 265 | - | "peer": true, | |
| 266 | - | "dependencies": { | |
| 267 | - | "@swc/helpers": "^0.5.0" | |
| 268 | - | } | |
| 269 | - | }, | |
| 270 | - | "node_modules/@jridgewell/gen-mapping": { | |
| 271 | - | "version": "0.3.13", | |
| 272 | - | "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", | |
| 273 | - | "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", | |
| 274 | - | "license": "MIT", | |
| 275 | - | "dependencies": { | |
| 276 | - | "@jridgewell/sourcemap-codec": "^1.5.0", | |
| 277 | - | "@jridgewell/trace-mapping": "^0.3.24" | |
| 278 | - | } | |
| 279 | - | }, | |
| 280 | - | "node_modules/@jridgewell/remapping": { | |
| 281 | - | "version": "2.3.5", | |
| 282 | - | "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", | |
| 283 | - | "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", | |
| 284 | - | "license": "MIT", | |
| 285 | - | "dependencies": { | |
| 286 | - | "@jridgewell/gen-mapping": "^0.3.5", | |
| 287 | - | "@jridgewell/trace-mapping": "^0.3.24" | |
| 288 | - | } | |
| 289 | - | }, | |
| 290 | - | "node_modules/@jridgewell/resolve-uri": { | |
| 291 | - | "version": "3.1.2", | |
| 292 | - | "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", | |
| 293 | - | "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", | |
| 294 | - | "license": "MIT", | |
| 295 | - | "engines": { | |
| 296 | - | "node": ">=6.0.0" | |
| 297 | - | } | |
| 298 | - | }, | |
| 299 | - | "node_modules/@jridgewell/sourcemap-codec": { | |
| 300 | - | "version": "1.5.5", | |
| 301 | - | "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", | |
| 302 | - | "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", | |
| 303 | - | "license": "MIT" | |
| 304 | - | }, | |
| 305 | - | "node_modules/@jridgewell/trace-mapping": { | |
| 306 | - | "version": "0.3.31", | |
| 307 | - | "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", | |
| 308 | - | "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", | |
| 309 | - | "license": "MIT", | |
| 310 | - | "dependencies": { | |
| 311 | - | "@jridgewell/resolve-uri": "^3.1.0", | |
| 312 | - | "@jridgewell/sourcemap-codec": "^1.4.14" | |
| 313 | - | } | |
| 314 | - | }, | |
| 315 | - | "node_modules/@lucide/svelte": { | |
| 316 | - | "version": "0.577.0", | |
| 317 | - | "resolved": "https://registry.npmjs.org/@lucide/svelte/-/svelte-0.577.0.tgz", | |
| 318 | - | "integrity": "sha512-0P6mkySd2MapIEgq08tADPmcN4DHndC/02PWwaLkOerXlx5Sv9aT4BxyXLIY+eccr0g/nEyCYiJesqS61YdBZQ==", | |
| 319 | - | "dev": true, | |
| 320 | - | "license": "ISC", | |
| 321 | - | "peerDependencies": { | |
| 322 | - | "svelte": "^5" | |
| 323 | - | } | |
| 324 | - | }, | |
| 325 | - | "node_modules/@napi-rs/wasm-runtime": { | |
| 326 | - | "version": "1.1.1", | |
| 327 | - | "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", | |
| 328 | - | "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==", | |
| 329 | - | "dev": true, | |
| 330 | - | "license": "MIT", | |
| 331 | - | "optional": true, | |
| 332 | - | "dependencies": { | |
| 333 | - | "@emnapi/core": "^1.7.1", | |
| 334 | - | "@emnapi/runtime": "^1.7.1", | |
| 335 | - | "@tybys/wasm-util": "^0.10.1" | |
| 336 | - | }, | |
| 337 | - | "funding": { | |
| 338 | - | "type": "github", | |
| 339 | - | "url": "https://github.com/sponsors/Brooooooklyn" | |
| 340 | - | } | |
| 341 | - | }, | |
| 342 | - | "node_modules/@oxc-project/runtime": { | |
| 343 | - | "version": "0.115.0", | |
| 344 | - | "resolved": "https://registry.npmjs.org/@oxc-project/runtime/-/runtime-0.115.0.tgz", | |
| 345 | - | "integrity": "sha512-Rg8Wlt5dCbXhQnsXPrkOjL1DTSvXLgb2R/KYfnf1/K+R0k6UMLEmbQXPM+kwrWqSmWA2t0B1EtHy2/3zikQpvQ==", | |
| 346 | - | "dev": true, | |
| 347 | - | "license": "MIT", | |
| 348 | - | "engines": { | |
| 349 | - | "node": "^20.19.0 || >=22.12.0" | |
| 350 | - | } | |
| 351 | - | }, | |
| 352 | - | "node_modules/@oxc-project/types": { | |
| 353 | - | "version": "0.115.0", | |
| 354 | - | "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.115.0.tgz", | |
| 355 | - | "integrity": "sha512-4n91DKnebUS4yjUHl2g3/b2T+IUdCfmoZGhmwsovZCDaJSs+QkVAM+0AqqTxHSsHfeiMuueT75cZaZcT/m0pSw==", | |
| 356 | - | "dev": true, | |
| 357 | - | "license": "MIT", | |
| 358 | - | "funding": { | |
| 359 | - | "url": "https://github.com/sponsors/Boshen" | |
| 360 | - | } | |
| 361 | - | }, | |
| 362 | - | "node_modules/@parcel/watcher": { | |
| 363 | - | "version": "2.5.4", | |
| 364 | - | "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.4.tgz", | |
| 365 | - | "integrity": "sha512-WYa2tUVV5HiArWPB3ydlOc4R2ivq0IDrlqhMi3l7mVsFEXNcTfxYFPIHXHXIh/ca/y/V5N4E1zecyxdIBjYnkQ==", | |
| 366 | - | "dev": true, | |
| 367 | - | "hasInstallScript": true, | |
| 368 | - | "license": "MIT", | |
| 369 | - | "dependencies": { | |
| 370 | - | "detect-libc": "^2.0.3", | |
| 371 | - | "is-glob": "^4.0.3", | |
| 372 | - | "node-addon-api": "^7.0.0", | |
| 373 | - | "picomatch": "^4.0.3" | |
| 374 | - | }, | |
| 375 | - | "engines": { | |
| 376 | - | "node": ">= 10.0.0" | |
| 377 | - | }, | |
| 378 | - | "funding": { | |
| 379 | - | "type": "opencollective", | |
| 380 | - | "url": "https://opencollective.com/parcel" | |
| 381 | - | }, | |
| 382 | - | "optionalDependencies": { | |
| 383 | - | "@parcel/watcher-android-arm64": "2.5.4", | |
| 384 | - | "@parcel/watcher-darwin-arm64": "2.5.4", | |
| 385 | - | "@parcel/watcher-darwin-x64": "2.5.4", | |
| 386 | - | "@parcel/watcher-freebsd-x64": "2.5.4", | |
| 387 | - | "@parcel/watcher-linux-arm-glibc": "2.5.4", | |
| 388 | - | "@parcel/watcher-linux-arm-musl": "2.5.4", | |
| 389 | - | "@parcel/watcher-linux-arm64-glibc": "2.5.4", | |
| 390 | - | "@parcel/watcher-linux-arm64-musl": "2.5.4", | |
| 391 | - | "@parcel/watcher-linux-x64-glibc": "2.5.4", | |
| 392 | - | "@parcel/watcher-linux-x64-musl": "2.5.4", | |
| 393 | - | "@parcel/watcher-win32-arm64": "2.5.4", | |
| 394 | - | "@parcel/watcher-win32-ia32": "2.5.4", | |
| 395 | - | "@parcel/watcher-win32-x64": "2.5.4" | |
| 396 | - | } | |
| 397 | - | }, | |
| 398 | - | "node_modules/@parcel/watcher-android-arm64": { | |
| 399 | - | "version": "2.5.4", | |
| 400 | - | "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.4.tgz", | |
| 401 | - | "integrity": "sha512-hoh0vx4v+b3BNI7Cjoy2/B0ARqcwVNrzN/n7DLq9ZB4I3lrsvhrkCViJyfTj/Qi5xM9YFiH4AmHGK6pgH1ss7g==", | |
| 402 | - | "cpu": [ | |
| 403 | - | "arm64" | |
| 404 | - | ], | |
| 405 | - | "dev": true, | |
| 406 | - | "license": "MIT", | |
| 407 | - | "optional": true, | |
| 408 | - | "os": [ | |
| 409 | - | "android" | |
| 410 | - | ], | |
| 411 | - | "engines": { | |
| 412 | - | "node": ">= 10.0.0" | |
| 413 | - | }, | |
| 414 | - | "funding": { | |
| 415 | - | "type": "opencollective", | |
| 416 | - | "url": "https://opencollective.com/parcel" | |
| 417 | - | } | |
| 418 | - | }, | |
| 419 | - | "node_modules/@parcel/watcher-darwin-arm64": { | |
| 420 | - | "version": "2.5.4", | |
| 421 | - | "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.4.tgz", | |
| 422 | - | "integrity": "sha512-kphKy377pZiWpAOyTgQYPE5/XEKVMaj6VUjKT5VkNyUJlr2qZAn8gIc7CPzx+kbhvqHDT9d7EqdOqRXT6vk0zw==", | |
| 423 | - | "cpu": [ | |
| 424 | - | "arm64" | |
| 425 | - | ], | |
| 426 | - | "dev": true, | |
| 427 | - | "license": "MIT", | |
| 428 | - | "optional": true, | |
| 429 | - | "os": [ | |
| 430 | - | "darwin" | |
| 431 | - | ], | |
| 432 | - | "engines": { | |
| 433 | - | "node": ">= 10.0.0" | |
| 434 | - | }, | |
| 435 | - | "funding": { | |
| 436 | - | "type": "opencollective", | |
| 437 | - | "url": "https://opencollective.com/parcel" | |
| 438 | - | } | |
| 439 | - | }, | |
| 440 | - | "node_modules/@parcel/watcher-darwin-x64": { | |
| 441 | - | "version": "2.5.4", | |
| 442 | - | "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.4.tgz", | |
| 443 | - | "integrity": "sha512-UKaQFhCtNJW1A9YyVz3Ju7ydf6QgrpNQfRZ35wNKUhTQ3dxJ/3MULXN5JN/0Z80V/KUBDGa3RZaKq1EQT2a2gg==", | |
| 444 | - | "cpu": [ | |
| 445 | - | "x64" | |
| 446 | - | ], | |
| 447 | - | "dev": true, | |
| 448 | - | "license": "MIT", | |
| 449 | - | "optional": true, | |
| 450 | - | "os": [ | |
| 451 | - | "darwin" | |
| 452 | - | ], | |
| 453 | - | "engines": { | |
| 454 | - | "node": ">= 10.0.0" | |
| 455 | - | }, | |
| 456 | - | "funding": { | |
| 457 | - | "type": "opencollective", | |
| 458 | - | "url": "https://opencollective.com/parcel" | |
| 459 | - | } | |
| 460 | - | }, | |
| 461 | - | "node_modules/@parcel/watcher-freebsd-x64": { | |
| 462 | - | "version": "2.5.4", | |
| 463 | - | "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.4.tgz", | |
| 464 | - | "integrity": "sha512-Dib0Wv3Ow/m2/ttvLdeI2DBXloO7t3Z0oCp4bAb2aqyqOjKPPGrg10pMJJAQ7tt8P4V2rwYwywkDhUia/FgS+Q==", | |
| 465 | - | "cpu": [ | |
| 466 | - | "x64" | |
| 467 | - | ], | |
| 468 | - | "dev": true, | |
| 469 | - | "license": "MIT", | |
| 470 | - | "optional": true, | |
| 471 | - | "os": [ | |
| 472 | - | "freebsd" | |
| 473 | - | ], | |
| 474 | - | "engines": { | |
| 475 | - | "node": ">= 10.0.0" | |
| 476 | - | }, | |
| 477 | - | "funding": { | |
| 478 | - | "type": "opencollective", | |
| 479 | - | "url": "https://opencollective.com/parcel" | |
| 480 | - | } | |
| 481 | - | }, | |
| 482 | - | "node_modules/@parcel/watcher-linux-arm-glibc": { | |
| 483 | - | "version": "2.5.4", | |
| 484 | - | "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.4.tgz", | |
| 485 | - | "integrity": "sha512-I5Vb769pdf7Q7Sf4KNy8Pogl/URRCKu9ImMmnVKYayhynuyGYMzuI4UOWnegQNa2sGpsPSbzDsqbHNMyeyPCgw==", | |
| 486 | - | "cpu": [ | |
| 487 | - | "arm" | |
| 488 | - | ], | |
| 489 | - | "dev": true, | |
| 490 | - | "license": "MIT", | |
| 491 | - | "optional": true, | |
| 492 | - | "os": [ | |
| 493 | - | "linux" | |
| 494 | - | ], | |
| 495 | - | "engines": { | |
| 496 | - | "node": ">= 10.0.0" | |
| 497 | - | }, | |
| 498 | - | "funding": { | |
| 499 | - | "type": "opencollective", | |
| 500 | - | "url": "https://opencollective.com/parcel" | |
| 501 | - | } | |
| 502 | - | }, | |
| 503 | - | "node_modules/@parcel/watcher-linux-arm-musl": { | |
| 504 | - | "version": "2.5.4", | |
| 505 | - | "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.4.tgz", | |
| 506 | - | "integrity": "sha512-kGO8RPvVrcAotV4QcWh8kZuHr9bXi9a3bSZw7kFarYR0+fGliU7hd/zevhjw8fnvIKG3J9EO5G6sXNGCSNMYPQ==", | |
| 507 | - | "cpu": [ | |
| 508 | - | "arm" | |
| 509 | - | ], | |
| 510 | - | "dev": true, | |
| 511 | - | "license": "MIT", | |
| 512 | - | "optional": true, | |
| 513 | - | "os": [ | |
| 514 | - | "linux" | |
| 515 | - | ], | |
| 516 | - | "engines": { | |
| 517 | - | "node": ">= 10.0.0" | |
| 518 | - | }, | |
| 519 | - | "funding": { | |
| 520 | - | "type": "opencollective", | |
| 521 | - | "url": "https://opencollective.com/parcel" | |
| 522 | - | } | |
| 523 | - | }, | |
| 524 | - | "node_modules/@parcel/watcher-linux-arm64-glibc": { | |
| 525 | - | "version": "2.5.4", | |
| 526 | - | "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.4.tgz", | |
| 527 | - | "integrity": "sha512-KU75aooXhqGFY2W5/p8DYYHt4hrjHZod8AhcGAmhzPn/etTa+lYCDB2b1sJy3sWJ8ahFVTdy+EbqSBvMx3iFlw==", | |
| 528 | - | "cpu": [ | |
| 529 | - | "arm64" | |
| 530 | - | ], | |
| 531 | - | "dev": true, | |
| 532 | - | "license": "MIT", | |
| 533 | - | "optional": true, | |
| 534 | - | "os": [ | |
| 535 | - | "linux" | |
| 536 | - | ], | |
| 537 | - | "engines": { | |
| 538 | - | "node": ">= 10.0.0" | |
| 539 | - | }, | |
| 540 | - | "funding": { | |
| 541 | - | "type": "opencollective", | |
| 542 | - | "url": "https://opencollective.com/parcel" | |
| 543 | - | } | |
| 544 | - | }, | |
| 545 | - | "node_modules/@parcel/watcher-linux-arm64-musl": { | |
| 546 | - | "version": "2.5.4", | |
| 547 | - | "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.4.tgz", | |
| 548 | - | "integrity": "sha512-Qx8uNiIekVutnzbVdrgSanM+cbpDD3boB1f8vMtnuG5Zau4/bdDbXyKwIn0ToqFhIuob73bcxV9NwRm04/hzHQ==", | |
| 549 | - | "cpu": [ | |
| 550 | - | "arm64" | |
| 551 | - | ], | |
| 552 | - | "dev": true, | |
| 553 | - | "license": "MIT", | |
| 554 | - | "optional": true, | |
| 555 | - | "os": [ | |
| 556 | - | "linux" | |
| 557 | - | ], | |
| 558 | - | "engines": { | |
| 559 | - | "node": ">= 10.0.0" | |
| 560 | - | }, | |
| 561 | - | "funding": { | |
| 562 | - | "type": "opencollective", | |
| 563 | - | "url": "https://opencollective.com/parcel" | |
| 564 | - | } | |
| 565 | - | }, | |
| 566 | - | "node_modules/@parcel/watcher-linux-x64-glibc": { | |
| 567 | - | "version": "2.5.4", | |
| 568 | - | "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.4.tgz", | |
| 569 | - | "integrity": "sha512-UYBQvhYmgAv61LNUn24qGQdjtycFBKSK3EXr72DbJqX9aaLbtCOO8+1SkKhD/GNiJ97ExgcHBrukcYhVjrnogA==", | |
| 570 | - | "cpu": [ | |
| 571 | - | "x64" | |
| 572 | - | ], | |
| 573 | - | "dev": true, | |
| 574 | - | "license": "MIT", | |
| 575 | - | "optional": true, | |
| 576 | - | "os": [ | |
| 577 | - | "linux" | |
| 578 | - | ], | |
| 579 | - | "engines": { | |
| 580 | - | "node": ">= 10.0.0" | |
| 581 | - | }, | |
| 582 | - | "funding": { | |
| 583 | - | "type": "opencollective", | |
| 584 | - | "url": "https://opencollective.com/parcel" | |
| 585 | - | } | |
| 586 | - | }, | |
| 587 | - | "node_modules/@parcel/watcher-linux-x64-musl": { | |
| 588 | - | "version": "2.5.4", | |
| 589 | - | "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.4.tgz", | |
| 590 | - | "integrity": "sha512-YoRWCVgxv8akZrMhdyVi6/TyoeeMkQ0PGGOf2E4omODrvd1wxniXP+DBynKoHryStks7l+fDAMUBRzqNHrVOpg==", | |
| 591 | - | "cpu": [ | |
| 592 | - | "x64" | |
| 593 | - | ], | |
| 594 | - | "dev": true, | |
| 595 | - | "license": "MIT", | |
| 596 | - | "optional": true, | |
| 597 | - | "os": [ | |
| 598 | - | "linux" | |
| 599 | - | ], | |
| 600 | - | "engines": { | |
| 601 | - | "node": ">= 10.0.0" | |
| 602 | - | }, | |
| 603 | - | "funding": { | |
| 604 | - | "type": "opencollective", | |
| 605 | - | "url": "https://opencollective.com/parcel" | |
| 606 | - | } | |
| 607 | - | }, | |
| 608 | - | "node_modules/@parcel/watcher-win32-arm64": { | |
| 609 | - | "version": "2.5.4", | |
| 610 | - | "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.4.tgz", | |
| 611 | - | "integrity": "sha512-iby+D/YNXWkiQNYcIhg8P5hSjzXEHaQrk2SLrWOUD7VeC4Ohu0WQvmV+HDJokZVJ2UjJ4AGXW3bx7Lls9Ln4TQ==", | |
| 612 | - | "cpu": [ | |
| 613 | - | "arm64" | |
| 614 | - | ], | |
| 615 | - | "dev": true, | |
| 616 | - | "license": "MIT", | |
| 617 | - | "optional": true, | |
| 618 | - | "os": [ | |
| 619 | - | "win32" | |
| 620 | - | ], | |
| 621 | - | "engines": { | |
| 622 | - | "node": ">= 10.0.0" | |
| 623 | - | }, | |
| 624 | - | "funding": { | |
| 625 | - | "type": "opencollective", | |
| 626 | - | "url": "https://opencollective.com/parcel" | |
| 627 | - | } | |
| 628 | - | }, | |
| 629 | - | "node_modules/@parcel/watcher-win32-ia32": { | |
| 630 | - | "version": "2.5.4", | |
| 631 | - | "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.4.tgz", | |
| 632 | - | "integrity": "sha512-vQN+KIReG0a2ZDpVv8cgddlf67J8hk1WfZMMP7sMeZmJRSmEax5xNDNWKdgqSe2brOKTQQAs3aCCUal2qBHAyg==", | |
| 633 | - | "cpu": [ | |
| 634 | - | "ia32" | |
| 635 | - | ], | |
| 636 | - | "dev": true, | |
| 637 | - | "license": "MIT", | |
| 638 | - | "optional": true, | |
| 639 | - | "os": [ | |
| 640 | - | "win32" | |
| 641 | - | ], | |
| 642 | - | "engines": { | |
| 643 | - | "node": ">= 10.0.0" | |
| 644 | - | }, | |
| 645 | - | "funding": { | |
| 646 | - | "type": "opencollective", | |
| 647 | - | "url": "https://opencollective.com/parcel" | |
| 648 | - | } | |
| 649 | - | }, | |
| 650 | - | "node_modules/@parcel/watcher-win32-x64": { | |
| 651 | - | "version": "2.5.4", | |
| 652 | - | "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.4.tgz", | |
| 653 | - | "integrity": "sha512-3A6efb6BOKwyw7yk9ro2vus2YTt2nvcd56AuzxdMiVOxL9umDyN5PKkKfZ/gZ9row41SjVmTVQNWQhaRRGpOKw==", | |
| 654 | - | "cpu": [ | |
| 655 | - | "x64" | |
| 656 | - | ], | |
| 657 | - | "dev": true, | |
| 658 | - | "license": "MIT", | |
| 659 | - | "optional": true, | |
| 660 | - | "os": [ | |
| 661 | - | "win32" | |
| 662 | - | ], | |
| 663 | - | "engines": { | |
| 664 | - | "node": ">= 10.0.0" | |
| 665 | - | }, | |
| 666 | - | "funding": { | |
| 667 | - | "type": "opencollective", | |
| 668 | - | "url": "https://opencollective.com/parcel" | |
| 669 | - | } | |
| 670 | - | }, | |
| 671 | - | "node_modules/@rolldown/binding-android-arm64": { | |
| 672 | - | "version": "1.0.0-rc.9", | |
| 673 | - | "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.9.tgz", | |
| 674 | - | "integrity": "sha512-lcJL0bN5hpgJfSIz/8PIf02irmyL43P+j1pTCfbD1DbLkmGRuFIA4DD3B3ZOvGqG0XiVvRznbKtN0COQVaKUTg==", | |
| 675 | - | "cpu": [ | |
| 676 | - | "arm64" | |
| 677 | - | ], | |
| 678 | - | "dev": true, | |
| 679 | - | "license": "MIT", | |
| 680 | - | "optional": true, | |
| 681 | - | "os": [ | |
| 682 | - | "android" | |
| 683 | - | ], | |
| 684 | - | "engines": { | |
| 685 | - | "node": "^20.19.0 || >=22.12.0" | |
| 686 | - | } | |
| 687 | - | }, | |
| 688 | - | "node_modules/@rolldown/binding-darwin-arm64": { | |
| 689 | - | "version": "1.0.0-rc.9", | |
| 690 | - | "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.9.tgz", | |
| 691 | - | "integrity": "sha512-J7Zk3kLYFsLtuH6U+F4pS2sYVzac0qkjcO5QxHS7OS7yZu2LRs+IXo+uvJ/mvpyUljDJ3LROZPoQfgBIpCMhdQ==", | |
| 692 | - | "cpu": [ | |
| 693 | - | "arm64" | |
| 694 | - | ], | |
| 695 | - | "dev": true, | |
| 696 | - | "license": "MIT", | |
| 697 | - | "optional": true, | |
| 698 | - | "os": [ | |
| 699 | - | "darwin" | |
| 700 | - | ], | |
| 701 | - | "engines": { | |
| 702 | - | "node": "^20.19.0 || >=22.12.0" | |
| 703 | - | } | |
| 704 | - | }, | |
| 705 | - | "node_modules/@rolldown/binding-darwin-x64": { | |
| 706 | - | "version": "1.0.0-rc.9", | |
| 707 | - | "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.9.tgz", | |
| 708 | - | "integrity": "sha512-iwtmmghy8nhfRGeNAIltcNXzD0QMNaaA5U/NyZc1Ia4bxrzFByNMDoppoC+hl7cDiUq5/1CnFthpT9n+UtfFyg==", | |
| 709 | - | "cpu": [ | |
| 710 | - | "x64" | |
| 711 | - | ], | |
| 712 | - | "dev": true, | |
| 713 | - | "license": "MIT", | |
| 714 | - | "optional": true, | |
| 715 | - | "os": [ | |
| 716 | - | "darwin" | |
| 717 | - | ], | |
| 718 | - | "engines": { | |
| 719 | - | "node": "^20.19.0 || >=22.12.0" | |
| 720 | - | } | |
| 721 | - | }, | |
| 722 | - | "node_modules/@rolldown/binding-freebsd-x64": { | |
| 723 | - | "version": "1.0.0-rc.9", | |
| 724 | - | "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.9.tgz", | |
| 725 | - | "integrity": "sha512-DLFYI78SCiZr5VvdEplsVC2Vx53lnA4/Ga5C65iyldMVaErr86aiqCoNBLl92PXPfDtUYjUh+xFFor40ueNs4Q==", | |
| 726 | - | "cpu": [ | |
| 727 | - | "x64" | |
| 728 | - | ], | |
| 729 | - | "dev": true, | |
| 730 | - | "license": "MIT", | |
| 731 | - | "optional": true, | |
| 732 | - | "os": [ | |
| 733 | - | "freebsd" | |
| 734 | - | ], | |
| 735 | - | "engines": { | |
| 736 | - | "node": "^20.19.0 || >=22.12.0" | |
| 737 | - | } | |
| 738 | - | }, | |
| 739 | - | "node_modules/@rolldown/binding-linux-arm-gnueabihf": { | |
| 740 | - | "version": "1.0.0-rc.9", | |
| 741 | - | "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.9.tgz", | |
| 742 | - | "integrity": "sha512-CsjTmTwd0Hri6iTw/DRMK7kOZ7FwAkrO4h8YWKoX/kcj833e4coqo2wzIFywtch/8Eb5enQ/lwLM7w6JX1W5RQ==", | |
| 743 | - | "cpu": [ | |
| 744 | - | "arm" | |
| 745 | - | ], | |
| 746 | - | "dev": true, | |
| 747 | - | "license": "MIT", | |
| 748 | - | "optional": true, | |
| 749 | - | "os": [ | |
| 750 | - | "linux" | |
| 751 | - | ], | |
| 752 | - | "engines": { | |
| 753 | - | "node": "^20.19.0 || >=22.12.0" | |
| 754 | - | } | |
| 755 | - | }, | |
| 756 | - | "node_modules/@rolldown/binding-linux-arm64-gnu": { | |
| 757 | - | "version": "1.0.0-rc.9", | |
| 758 | - | "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.9.tgz", | |
| 759 | - | "integrity": "sha512-2x9O2JbSPxpxMDhP9Z74mahAStibTlrBMW0520+epJH5sac7/LwZW5Bmg/E6CXuEF53JJFW509uP+lSedaUNxg==", | |
| 760 | - | "cpu": [ | |
| 761 | - | "arm64" | |
| 762 | - | ], | |
| 763 | - | "dev": true, | |
| 764 | - | "license": "MIT", | |
| 765 | - | "optional": true, | |
| 766 | - | "os": [ | |
| 767 | - | "linux" | |
| 768 | - | ], | |
| 769 | - | "engines": { | |
| 770 | - | "node": "^20.19.0 || >=22.12.0" | |
| 771 | - | } | |
| 772 | - | }, | |
| 773 | - | "node_modules/@rolldown/binding-linux-arm64-musl": { | |
| 774 | - | "version": "1.0.0-rc.9", | |
| 775 | - | "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.9.tgz", | |
| 776 | - | "integrity": "sha512-JA1QRW31ogheAIRhIg9tjMfsYbglXXYGNPLdPEYrwFxdbkQCAzvpSCSHCDWNl4hTtrol8WeboCSEpjdZK8qrCg==", | |
| 777 | - | "cpu": [ | |
| 778 | - | "arm64" | |
| 779 | - | ], | |
| 780 | - | "dev": true, | |
| 781 | - | "license": "MIT", | |
| 782 | - | "optional": true, | |
| 783 | - | "os": [ | |
| 784 | - | "linux" | |
| 785 | - | ], | |
| 786 | - | "engines": { | |
| 787 | - | "node": "^20.19.0 || >=22.12.0" | |
| 788 | - | } | |
| 789 | - | }, | |
| 790 | - | "node_modules/@rolldown/binding-linux-ppc64-gnu": { | |
| 791 | - | "version": "1.0.0-rc.9", | |
| 792 | - | "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.9.tgz", | |
| 793 | - | "integrity": "sha512-aOKU9dJheda8Kj8Y3w9gnt9QFOO+qKPAl8SWd7JPHP+Cu0EuDAE5wokQubLzIDQWg2myXq2XhTpOVS07qqvT+w==", | |
| 794 | - | "cpu": [ | |
| 795 | - | "ppc64" | |
| 796 | - | ], | |
| 797 | - | "dev": true, | |
| 798 | - | "license": "MIT", | |
| 799 | - | "optional": true, | |
| 800 | - | "os": [ | |
| 801 | - | "linux" | |
| 802 | - | ], | |
| 803 | - | "engines": { | |
| 804 | - | "node": "^20.19.0 || >=22.12.0" | |
| 805 | - | } | |
| 806 | - | }, | |
| 807 | - | "node_modules/@rolldown/binding-linux-s390x-gnu": { | |
| 808 | - | "version": "1.0.0-rc.9", | |
| 809 | - | "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.9.tgz", | |
| 810 | - | "integrity": "sha512-OalO94fqj7IWRn3VdXWty75jC5dk4C197AWEuMhIpvVv2lw9fiPhud0+bW2ctCxb3YoBZor71QHbY+9/WToadA==", | |
| 811 | - | "cpu": [ | |
| 812 | - | "s390x" | |
| 813 | - | ], | |
| 814 | - | "dev": true, | |
| 815 | - | "license": "MIT", | |
| 816 | - | "optional": true, | |
| 817 | - | "os": [ | |
| 818 | - | "linux" | |
| 819 | - | ], | |
| 820 | - | "engines": { | |
| 821 | - | "node": "^20.19.0 || >=22.12.0" | |
| 822 | - | } | |
| 823 | - | }, | |
| 824 | - | "node_modules/@rolldown/binding-linux-x64-gnu": { | |
| 825 | - | "version": "1.0.0-rc.9", | |
| 826 | - | "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.9.tgz", | |
| 827 | - | "integrity": "sha512-cVEl1vZtBsBZna3YMjGXNvnYYrOJ7RzuWvZU0ffvJUexWkukMaDuGhUXn0rjnV0ptzGVkvc+vW9Yqy6h8YX4pg==", | |
| 828 | - | "cpu": [ | |
| 829 | - | "x64" | |
| 830 | - | ], | |
| 831 | - | "dev": true, | |
| 832 | - | "license": "MIT", | |
| 833 | - | "optional": true, | |
| 834 | - | "os": [ | |
| 835 | - | "linux" | |
| 836 | - | ], | |
| 837 | - | "engines": { | |
| 838 | - | "node": "^20.19.0 || >=22.12.0" | |
| 839 | - | } | |
| 840 | - | }, | |
| 841 | - | "node_modules/@rolldown/binding-linux-x64-musl": { | |
| 842 | - | "version": "1.0.0-rc.9", | |
| 843 | - | "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.9.tgz", | |
| 844 | - | "integrity": "sha512-UzYnKCIIc4heAKgI4PZ3dfBGUZefGCJ1TPDuLHoCzgrMYPb5Rv6TLFuYtyM4rWyHM7hymNdsg5ik2C+UD9VDbA==", | |
| 845 | - | "cpu": [ | |
| 846 | - | "x64" | |
| 847 | - | ], | |
| 848 | - | "dev": true, | |
| 849 | - | "license": "MIT", | |
| 850 | - | "optional": true, | |
| 851 | - | "os": [ | |
| 852 | - | "linux" | |
| 853 | - | ], | |
| 854 | - | "engines": { | |
| 855 | - | "node": "^20.19.0 || >=22.12.0" | |
| 856 | - | } | |
| 857 | - | }, | |
| 858 | - | "node_modules/@rolldown/binding-openharmony-arm64": { | |
| 859 | - | "version": "1.0.0-rc.9", | |
| 860 | - | "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.9.tgz", | |
| 861 | - | "integrity": "sha512-+6zoiF+RRyf5cdlFQP7nm58mq7+/2PFaY2DNQeD4B87N36JzfF/l9mdBkkmTvSYcYPE8tMh/o3cRlsx1ldLfog==", | |
| 862 | - | "cpu": [ | |
| 863 | - | "arm64" | |
| 864 | - | ], | |
| 865 | - | "dev": true, | |
| 866 | - | "license": "MIT", | |
| 867 | - | "optional": true, | |
| 868 | - | "os": [ | |
| 869 | - | "openharmony" | |
| 870 | - | ], | |
| 871 | - | "engines": { | |
| 872 | - | "node": "^20.19.0 || >=22.12.0" | |
| 873 | - | } | |
| 874 | - | }, | |
| 875 | - | "node_modules/@rolldown/binding-wasm32-wasi": { | |
| 876 | - | "version": "1.0.0-rc.9", | |
| 877 | - | "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.9.tgz", | |
| 878 | - | "integrity": "sha512-rgFN6sA/dyebil3YTlL2evvi/M+ivhfnyxec7AccTpRPccno/rPoNlqybEZQBkcbZu8Hy+eqNJCqfBR8P7Pg8g==", | |
| 879 | - | "cpu": [ | |
| 880 | - | "wasm32" | |
| 881 | - | ], | |
| 882 | - | "dev": true, | |
| 883 | - | "license": "MIT", | |
| 884 | - | "optional": true, | |
| 885 | - | "dependencies": { | |
| 886 | - | "@napi-rs/wasm-runtime": "^1.1.1" | |
| 887 | - | }, | |
| 888 | - | "engines": { | |
| 889 | - | "node": ">=14.0.0" | |
| 890 | - | } | |
| 891 | - | }, | |
| 892 | - | "node_modules/@rolldown/binding-win32-arm64-msvc": { | |
| 893 | - | "version": "1.0.0-rc.9", | |
| 894 | - | "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.9.tgz", | |
| 895 | - | "integrity": "sha512-lHVNUG/8nlF1IQk1C0Ci574qKYyty2goMiPlRqkC5R+3LkXDkL5Dhx8ytbxq35m+pkHVIvIxviD+TWLdfeuadA==", | |
| 896 | - | "cpu": [ | |
| 897 | - | "arm64" | |
| 898 | - | ], | |
| 899 | - | "dev": true, | |
| 900 | - | "license": "MIT", | |
| 901 | - | "optional": true, | |
| 902 | - | "os": [ | |
| 903 | - | "win32" | |
| 904 | - | ], | |
| 905 | - | "engines": { | |
| 906 | - | "node": "^20.19.0 || >=22.12.0" | |
| 907 | - | } | |
| 908 | - | }, | |
| 909 | - | "node_modules/@rolldown/binding-win32-x64-msvc": { | |
| 910 | - | "version": "1.0.0-rc.9", | |
| 911 | - | "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.9.tgz", | |
| 912 | - | "integrity": "sha512-G0oA4+w1iY5AGi5HcDTxWsoxF509hrFIPB2rduV5aDqS9FtDg1CAfa7V34qImbjfhIcA8C+RekocJZA96EarwQ==", | |
| 913 | - | "cpu": [ | |
| 914 | - | "x64" | |
| 915 | - | ], | |
| 916 | - | "dev": true, | |
| 917 | - | "license": "MIT", | |
| 918 | - | "optional": true, | |
| 919 | - | "os": [ | |
| 920 | - | "win32" | |
| 921 | - | ], | |
| 922 | - | "engines": { | |
| 923 | - | "node": "^20.19.0 || >=22.12.0" | |
| 924 | - | } | |
| 925 | - | }, | |
| 926 | - | "node_modules/@rolldown/pluginutils": { | |
| 927 | - | "version": "1.0.0-rc.9", | |
| 928 | - | "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.9.tgz", | |
| 929 | - | "integrity": "sha512-w6oiRWgEBl04QkFZgmW+jnU1EC9b57Oihi2ot3HNWIQRqgHp5PnYDia5iZ5FF7rpa4EQdiqMDXjlqKGXBhsoXw==", | |
| 930 | - | "dev": true, | |
| 931 | - | "license": "MIT" | |
| 932 | - | }, | |
| 933 | - | "node_modules/@sveltejs/acorn-typescript": { | |
| 934 | - | "version": "1.0.8", | |
| 935 | - | "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.8.tgz", | |
| 936 | - | "integrity": "sha512-esgN+54+q0NjB0Y/4BomT9samII7jGwNy/2a3wNZbT2A2RpmXsXwUt24LvLhx6jUq2gVk4cWEvcRO6MFQbOfNA==", | |
| 937 | - | "license": "MIT", | |
| 938 | - | "peerDependencies": { | |
| 939 | - | "acorn": "^8.9.0" | |
| 940 | - | } | |
| 941 | - | }, | |
| 942 | - | "node_modules/@sveltejs/vite-plugin-svelte": { | |
| 943 | - | "version": "7.0.0", | |
| 944 | - | "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-7.0.0.tgz", | |
| 945 | - | "integrity": "sha512-ILXmxC7HAsnkK2eslgPetrqqW1BKSL7LktsFgqzNj83MaivMGZzluWq32m25j2mDOjmSKX7GGWahePhuEs7P/g==", | |
| 946 | - | "dev": true, | |
| 947 | - | "license": "MIT", | |
| 948 | - | "dependencies": { | |
| 949 | - | "deepmerge": "^4.3.1", | |
| 950 | - | "magic-string": "^0.30.21", | |
| 951 | - | "obug": "^2.1.0", | |
| 952 | - | "vitefu": "^1.1.2" | |
| 953 | - | }, | |
| 954 | - | "engines": { | |
| 955 | - | "node": "^20.19 || ^22.12 || >=24" | |
| 956 | - | }, | |
| 957 | - | "peerDependencies": { | |
| 958 | - | "svelte": "^5.46.4", | |
| 959 | - | "vite": "^8.0.0-beta.7 || ^8.0.0" | |
| 960 | - | } | |
| 961 | - | }, | |
| 962 | - | "node_modules/@swc/helpers": { | |
| 963 | - | "version": "0.5.18", | |
| 964 | - | "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.18.tgz", | |
| 965 | - | "integrity": "sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==", | |
| 966 | - | "license": "Apache-2.0", | |
| 967 | - | "dependencies": { | |
| 968 | - | "tslib": "^2.8.0" | |
| 969 | - | } | |
| 970 | - | }, | |
| 971 | - | "node_modules/@tailwindcss/cli": { | |
| 972 | - | "version": "4.2.2", | |
| 973 | - | "resolved": "https://registry.npmjs.org/@tailwindcss/cli/-/cli-4.2.2.tgz", | |
| 974 | - | "integrity": "sha512-iJS+8kAFZ8HPqnh0O5DHCLjo4L6dD97DBQEkrhfSO4V96xeefUus2jqsBs1dUMt3OU9Ks4qIkiY0mpL5UW+4LQ==", | |
| 975 | - | "dev": true, | |
| 976 | - | "license": "MIT", | |
| 977 | - | "dependencies": { | |
| 978 | - | "@parcel/watcher": "^2.5.1", | |
| 979 | - | "@tailwindcss/node": "4.2.2", | |
| 980 | - | "@tailwindcss/oxide": "4.2.2", | |
| 981 | - | "enhanced-resolve": "^5.19.0", | |
| 982 | - | "mri": "^1.2.0", | |
| 983 | - | "picocolors": "^1.1.1", | |
| 984 | - | "tailwindcss": "4.2.2" | |
| 985 | - | }, | |
| 986 | - | "bin": { | |
| 987 | - | "tailwindcss": "dist/index.mjs" | |
| 988 | - | } | |
| 989 | - | }, | |
| 990 | - | "node_modules/@tailwindcss/node": { | |
| 991 | - | "version": "4.2.2", | |
| 992 | - | "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.2.tgz", | |
| 993 | - | "integrity": "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==", | |
| 994 | - | "dev": true, | |
| 995 | - | "license": "MIT", | |
| 996 | - | "dependencies": { | |
| 997 | - | "@jridgewell/remapping": "^2.3.5", | |
| 998 | - | "enhanced-resolve": "^5.19.0", | |
| 999 | - | "jiti": "^2.6.1", | |
| 1000 | - | "lightningcss": "1.32.0", | |
| 1001 | - | "magic-string": "^0.30.21", | |
| 1002 | - | "source-map-js": "^1.2.1", | |
| 1003 | - | "tailwindcss": "4.2.2" | |
| 1004 | - | } | |
| 1005 | - | }, | |
| 1006 | - | "node_modules/@tailwindcss/oxide": { | |
| 1007 | - | "version": "4.2.2", | |
| 1008 | - | "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.2.tgz", | |
| 1009 | - | "integrity": "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==", | |
| 1010 | - | "dev": true, | |
| 1011 | - | "license": "MIT", | |
| 1012 | - | "engines": { | |
| 1013 | - | "node": ">= 20" | |
| 1014 | - | }, | |
| 1015 | - | "optionalDependencies": { | |
| 1016 | - | "@tailwindcss/oxide-android-arm64": "4.2.2", | |
| 1017 | - | "@tailwindcss/oxide-darwin-arm64": "4.2.2", | |
| 1018 | - | "@tailwindcss/oxide-darwin-x64": "4.2.2", | |
| 1019 | - | "@tailwindcss/oxide-freebsd-x64": "4.2.2", | |
| 1020 | - | "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2", | |
| 1021 | - | "@tailwindcss/oxide-linux-arm64-gnu": "4.2.2", | |
| 1022 | - | "@tailwindcss/oxide-linux-arm64-musl": "4.2.2", | |
| 1023 | - | "@tailwindcss/oxide-linux-x64-gnu": "4.2.2", | |
| 1024 | - | "@tailwindcss/oxide-linux-x64-musl": "4.2.2", | |
| 1025 | - | "@tailwindcss/oxide-wasm32-wasi": "4.2.2", | |
| 1026 | - | "@tailwindcss/oxide-win32-arm64-msvc": "4.2.2", | |
| 1027 | - | "@tailwindcss/oxide-win32-x64-msvc": "4.2.2" | |
| 1028 | - | } | |
| 1029 | - | }, | |
| 1030 | - | "node_modules/@tailwindcss/oxide-android-arm64": { | |
| 1031 | - | "version": "4.2.2", | |
| 1032 | - | "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.2.tgz", | |
| 1033 | - | "integrity": "sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==", | |
| 1034 | - | "cpu": [ | |
| 1035 | - | "arm64" | |
| 1036 | - | ], | |
| 1037 | - | "dev": true, | |
| 1038 | - | "license": "MIT", | |
| 1039 | - | "optional": true, | |
| 1040 | - | "os": [ | |
| 1041 | - | "android" | |
| 1042 | - | ], | |
| 1043 | - | "engines": { | |
| 1044 | - | "node": ">= 20" | |
| 1045 | - | } | |
| 1046 | - | }, | |
| 1047 | - | "node_modules/@tailwindcss/oxide-darwin-arm64": { | |
| 1048 | - | "version": "4.2.2", | |
| 1049 | - | "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.2.tgz", | |
| 1050 | - | "integrity": "sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==", | |
| 1051 | - | "cpu": [ | |
| 1052 | - | "arm64" | |
| 1053 | - | ], | |
| 1054 | - | "dev": true, | |
| 1055 | - | "license": "MIT", | |
| 1056 | - | "optional": true, | |
| 1057 | - | "os": [ | |
| 1058 | - | "darwin" | |
| 1059 | - | ], | |
| 1060 | - | "engines": { | |
| 1061 | - | "node": ">= 20" | |
| 1062 | - | } | |
| 1063 | - | }, | |
| 1064 | - | "node_modules/@tailwindcss/oxide-darwin-x64": { | |
| 1065 | - | "version": "4.2.2", | |
| 1066 | - | "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.2.tgz", | |
| 1067 | - | "integrity": "sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==", | |
| 1068 | - | "cpu": [ | |
| 1069 | - | "x64" | |
| 1070 | - | ], | |
| 1071 | - | "dev": true, | |
| 1072 | - | "license": "MIT", | |
| 1073 | - | "optional": true, | |
| 1074 | - | "os": [ | |
| 1075 | - | "darwin" | |
| 1076 | - | ], | |
| 1077 | - | "engines": { | |
| 1078 | - | "node": ">= 20" | |
| 1079 | - | } | |
| 1080 | - | }, | |
| 1081 | - | "node_modules/@tailwindcss/oxide-freebsd-x64": { | |
| 1082 | - | "version": "4.2.2", | |
| 1083 | - | "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.2.tgz", | |
| 1084 | - | "integrity": "sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==", | |
| 1085 | - | "cpu": [ | |
| 1086 | - | "x64" | |
| 1087 | - | ], | |
| 1088 | - | "dev": true, | |
| 1089 | - | "license": "MIT", | |
| 1090 | - | "optional": true, | |
| 1091 | - | "os": [ | |
| 1092 | - | "freebsd" | |
| 1093 | - | ], | |
| 1094 | - | "engines": { | |
| 1095 | - | "node": ">= 20" | |
| 1096 | - | } | |
| 1097 | - | }, | |
| 1098 | - | "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { | |
| 1099 | - | "version": "4.2.2", | |
| 1100 | - | "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.2.tgz", | |
| 1101 | - | "integrity": "sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==", | |
| 1102 | - | "cpu": [ | |
| 1103 | - | "arm" | |
| 1104 | - | ], | |
| 1105 | - | "dev": true, | |
| 1106 | - | "license": "MIT", | |
| 1107 | - | "optional": true, | |
| 1108 | - | "os": [ | |
| 1109 | - | "linux" | |
| 1110 | - | ], | |
| 1111 | - | "engines": { | |
| 1112 | - | "node": ">= 20" | |
| 1113 | - | } | |
| 1114 | - | }, | |
| 1115 | - | "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { | |
| 1116 | - | "version": "4.2.2", | |
| 1117 | - | "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.2.tgz", | |
| 1118 | - | "integrity": "sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==", | |
| 1119 | - | "cpu": [ | |
| 1120 | - | "arm64" | |
| 1121 | - | ], | |
| 1122 | - | "dev": true, | |
| 1123 | - | "license": "MIT", | |
| 1124 | - | "optional": true, | |
| 1125 | - | "os": [ | |
| 1126 | - | "linux" | |
| 1127 | - | ], | |
| 1128 | - | "engines": { | |
| 1129 | - | "node": ">= 20" | |
| 1130 | - | } | |
| 1131 | - | }, | |
| 1132 | - | "node_modules/@tailwindcss/oxide-linux-arm64-musl": { | |
| 1133 | - | "version": "4.2.2", | |
| 1134 | - | "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.2.tgz", | |
| 1135 | - | "integrity": "sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==", | |
| 1136 | - | "cpu": [ | |
| 1137 | - | "arm64" | |
| 1138 | - | ], | |
| 1139 | - | "dev": true, | |
| 1140 | - | "license": "MIT", | |
| 1141 | - | "optional": true, | |
| 1142 | - | "os": [ | |
| 1143 | - | "linux" | |
| 1144 | - | ], | |
| 1145 | - | "engines": { | |
| 1146 | - | "node": ">= 20" | |
| 1147 | - | } | |
| 1148 | - | }, | |
| 1149 | - | "node_modules/@tailwindcss/oxide-linux-x64-gnu": { | |
| 1150 | - | "version": "4.2.2", | |
| 1151 | - | "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.2.tgz", | |
| 1152 | - | "integrity": "sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==", | |
| 1153 | - | "cpu": [ | |
| 1154 | - | "x64" | |
| 1155 | - | ], | |
| 1156 | - | "dev": true, | |
| 1157 | - | "license": "MIT", | |
| 1158 | - | "optional": true, | |
| 1159 | - | "os": [ | |
| 1160 | - | "linux" | |
| 1161 | - | ], | |
| 1162 | - | "engines": { | |
| 1163 | - | "node": ">= 20" | |
| 1164 | - | } | |
| 1165 | - | }, | |
| 1166 | - | "node_modules/@tailwindcss/oxide-linux-x64-musl": { | |
| 1167 | - | "version": "4.2.2", | |
| 1168 | - | "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.2.tgz", | |
| 1169 | - | "integrity": "sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==", | |
| 1170 | - | "cpu": [ | |
| 1171 | - | "x64" | |
| 1172 | - | ], | |
| 1173 | - | "dev": true, | |
| 1174 | - | "license": "MIT", | |
| 1175 | - | "optional": true, | |
| 1176 | - | "os": [ | |
| 1177 | - | "linux" | |
| 1178 | - | ], | |
| 1179 | - | "engines": { | |
| 1180 | - | "node": ">= 20" | |
| 1181 | - | } | |
| 1182 | - | }, | |
| 1183 | - | "node_modules/@tailwindcss/oxide-wasm32-wasi": { | |
| 1184 | - | "version": "4.2.2", | |
| 1185 | - | "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.2.tgz", | |
| 1186 | - | "integrity": "sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==", | |
| 1187 | - | "bundleDependencies": [ | |
| 1188 | - | "@napi-rs/wasm-runtime", | |
| 1189 | - | "@emnapi/core", | |
| 1190 | - | "@emnapi/runtime", | |
| 1191 | - | "@tybys/wasm-util", | |
| 1192 | - | "@emnapi/wasi-threads", | |
| 1193 | - | "tslib" | |
| 1194 | - | ], | |
| 1195 | - | "cpu": [ | |
| 1196 | - | "wasm32" | |
| 1197 | - | ], | |
| 1198 | - | "dev": true, | |
| 1199 | - | "license": "MIT", | |
| 1200 | - | "optional": true, | |
| 1201 | - | "dependencies": { | |
| 1202 | - | "@emnapi/core": "^1.8.1", | |
| 1203 | - | "@emnapi/runtime": "^1.8.1", | |
| 1204 | - | "@emnapi/wasi-threads": "^1.1.0", | |
| 1205 | - | "@napi-rs/wasm-runtime": "^1.1.1", | |
| 1206 | - | "@tybys/wasm-util": "^0.10.1", | |
| 1207 | - | "tslib": "^2.8.1" | |
| 1208 | - | }, | |
| 1209 | - | "engines": { | |
| 1210 | - | "node": ">=14.0.0" | |
| 1211 | - | } | |
| 1212 | - | }, | |
| 1213 | - | "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { | |
| 1214 | - | "version": "1.8.1", | |
| 1215 | - | "dev": true, | |
| 1216 | - | "inBundle": true, | |
| 1217 | - | "license": "MIT", | |
| 1218 | - | "optional": true, | |
| 1219 | - | "dependencies": { | |
| 1220 | - | "@emnapi/wasi-threads": "1.1.0", | |
| 1221 | - | "tslib": "^2.4.0" | |
| 1222 | - | } | |
| 1223 | - | }, | |
| 1224 | - | "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { | |
| 1225 | - | "version": "1.8.1", | |
| 1226 | - | "dev": true, | |
| 1227 | - | "inBundle": true, | |
| 1228 | - | "license": "MIT", | |
| 1229 | - | "optional": true, | |
| 1230 | - | "dependencies": { | |
| 1231 | - | "tslib": "^2.4.0" | |
| 1232 | - | } | |
| 1233 | - | }, | |
| 1234 | - | "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { | |
| 1235 | - | "version": "1.1.0", | |
| 1236 | - | "dev": true, | |
| 1237 | - | "inBundle": true, | |
| 1238 | - | "license": "MIT", | |
| 1239 | - | "optional": true, | |
| 1240 | - | "dependencies": { | |
| 1241 | - | "tslib": "^2.4.0" | |
| 1242 | - | } | |
| 1243 | - | }, | |
| 1244 | - | "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { | |
| 1245 | - | "version": "1.1.1", | |
| 1246 | - | "dev": true, | |
| 1247 | - | "inBundle": true, | |
| 1248 | - | "license": "MIT", | |
| 1249 | - | "optional": true, | |
| 1250 | - | "dependencies": { | |
| 1251 | - | "@emnapi/core": "^1.7.1", | |
| 1252 | - | "@emnapi/runtime": "^1.7.1", | |
| 1253 | - | "@tybys/wasm-util": "^0.10.1" | |
| 1254 | - | }, | |
| 1255 | - | "funding": { | |
| 1256 | - | "type": "github", | |
| 1257 | - | "url": "https://github.com/sponsors/Brooooooklyn" | |
| 1258 | - | } | |
| 1259 | - | }, | |
| 1260 | - | "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { | |
| 1261 | - | "version": "0.10.1", | |
| 1262 | - | "dev": true, | |
| 1263 | - | "inBundle": true, | |
| 1264 | - | "license": "MIT", | |
| 1265 | - | "optional": true, | |
| 1266 | - | "dependencies": { | |
| 1267 | - | "tslib": "^2.4.0" | |
| 1268 | - | } | |
| 1269 | - | }, | |
| 1270 | - | "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { | |
| 1271 | - | "version": "2.8.1", | |
| 1272 | - | "dev": true, | |
| 1273 | - | "inBundle": true, | |
| 1274 | - | "license": "0BSD", | |
| 1275 | - | "optional": true | |
| 1276 | - | }, | |
| 1277 | - | "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { | |
| 1278 | - | "version": "4.2.2", | |
| 1279 | - | "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.2.tgz", | |
| 1280 | - | "integrity": "sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==", | |
| 1281 | - | "cpu": [ | |
| 1282 | - | "arm64" | |
| 1283 | - | ], | |
| 1284 | - | "dev": true, | |
| 1285 | - | "license": "MIT", | |
| 1286 | - | "optional": true, | |
| 1287 | - | "os": [ | |
| 1288 | - | "win32" | |
| 1289 | - | ], | |
| 1290 | - | "engines": { | |
| 1291 | - | "node": ">= 20" | |
| 1292 | - | } | |
| 1293 | - | }, | |
| 1294 | - | "node_modules/@tailwindcss/oxide-win32-x64-msvc": { | |
| 1295 | - | "version": "4.2.2", | |
| 1296 | - | "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.2.tgz", | |
| 1297 | - | "integrity": "sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==", | |
| 1298 | - | "cpu": [ | |
| 1299 | - | "x64" | |
| 1300 | - | ], | |
| 1301 | - | "dev": true, | |
| 1302 | - | "license": "MIT", | |
| 1303 | - | "optional": true, | |
| 1304 | - | "os": [ | |
| 1305 | - | "win32" | |
| 1306 | - | ], | |
| 1307 | - | "engines": { | |
| 1308 | - | "node": ">= 20" | |
| 1309 | - | } | |
| 1310 | - | }, | |
| 1311 | - | "node_modules/@tailwindcss/postcss": { | |
| 1312 | - | "version": "4.2.2", | |
| 1313 | - | "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.2.2.tgz", | |
| 1314 | - | "integrity": "sha512-n4goKQbW8RVXIbNKRB/45LzyUqN451deQK0nzIeauVEqjlI49slUlgKYJM2QyUzap/PcpnS7kzSUmPb1sCRvYQ==", | |
| 1315 | - | "dev": true, | |
| 1316 | - | "license": "MIT", | |
| 1317 | - | "dependencies": { | |
| 1318 | - | "@alloc/quick-lru": "^5.2.0", | |
| 1319 | - | "@tailwindcss/node": "4.2.2", | |
| 1320 | - | "@tailwindcss/oxide": "4.2.2", | |
| 1321 | - | "postcss": "^8.5.6", | |
| 1322 | - | "tailwindcss": "4.2.2" | |
| 1323 | - | } | |
| 1324 | - | }, | |
| 1325 | - | "node_modules/@tybys/wasm-util": { | |
| 1326 | - | "version": "0.10.1", | |
| 1327 | - | "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", | |
| 1328 | - | "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", | |
| 1329 | - | "dev": true, | |
| 1330 | - | "license": "MIT", | |
| 1331 | - | "optional": true, | |
| 1332 | - | "dependencies": { | |
| 1333 | - | "tslib": "^2.4.0" | |
| 1334 | - | } | |
| 1335 | - | }, | |
| 1336 | - | "node_modules/@types/esrecurse": { | |
| 1337 | - | "version": "4.3.1", | |
| 1338 | - | "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", | |
| 1339 | - | "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", | |
| 1340 | - | "dev": true, | |
| 1341 | - | "license": "MIT" | |
| 1342 | - | }, | |
| 1343 | - | "node_modules/@types/estree": { | |
| 1344 | - | "version": "1.0.8", | |
| 1345 | - | "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", | |
| 1346 | - | "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", | |
| 1347 | - | "license": "MIT" | |
| 1348 | - | }, | |
| 1349 | - | "node_modules/@types/json-schema": { | |
| 1350 | - | "version": "7.0.15", | |
| 1351 | - | "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", | |
| 1352 | - | "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", | |
| 1353 | - | "dev": true, | |
| 1354 | - | "license": "MIT" | |
| 1355 | - | }, | |
| 1356 | - | "node_modules/@types/node": { | |
| 1357 | - | "version": "25.5.0", | |
| 1358 | - | "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", | |
| 1359 | - | "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", | |
| 1360 | - | "dev": true, | |
| 1361 | - | "license": "MIT", | |
| 1362 | - | "peer": true, | |
| 1363 | - | "dependencies": { | |
| 1364 | - | "undici-types": "~7.18.0" | |
| 1365 | - | } | |
| 1366 | - | }, | |
| 1367 | - | "node_modules/@types/trusted-types": { | |
| 1368 | - | "version": "2.0.7", | |
| 1369 | - | "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", | |
| 1370 | - | "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", | |
| 1371 | - | "license": "MIT" | |
| 1372 | - | }, | |
| 1373 | - | "node_modules/@typescript-eslint/eslint-plugin": { | |
| 1374 | - | "version": "8.57.1", | |
| 1375 | - | "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.1.tgz", | |
| 1376 | - | "integrity": "sha512-Gn3aqnvNl4NGc6x3/Bqk1AOn0thyTU9bqDRhiRnUWezgvr2OnhYCWCgC8zXXRVqBsIL1pSDt7T9nJUe0oM0kDQ==", | |
| 1377 | - | "dev": true, | |
| 1378 | - | "license": "MIT", | |
| 1379 | - | "dependencies": { | |
| 1380 | - | "@eslint-community/regexpp": "^4.12.2", | |
| 1381 | - | "@typescript-eslint/scope-manager": "8.57.1", | |
| 1382 | - | "@typescript-eslint/type-utils": "8.57.1", | |
| 1383 | - | "@typescript-eslint/utils": "8.57.1", | |
| 1384 | - | "@typescript-eslint/visitor-keys": "8.57.1", | |
| 1385 | - | "ignore": "^7.0.5", | |
| 1386 | - | "natural-compare": "^1.4.0", | |
| 1387 | - | "ts-api-utils": "^2.4.0" | |
| 1388 | - | }, | |
| 1389 | - | "engines": { | |
| 1390 | - | "node": "^18.18.0 || ^20.9.0 || >=21.1.0" | |
| 1391 | - | }, | |
| 1392 | - | "funding": { | |
| 1393 | - | "type": "opencollective", | |
| 1394 | - | "url": "https://opencollective.com/typescript-eslint" | |
| 1395 | - | }, | |
| 1396 | - | "peerDependencies": { | |
| 1397 | - | "@typescript-eslint/parser": "^8.57.1", | |
| 1398 | - | "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", | |
| 1399 | - | "typescript": ">=4.8.4 <6.0.0" | |
| 1400 | - | } | |
| 1401 | - | }, | |
| 1402 | - | "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { | |
| 1403 | - | "version": "7.0.5", | |
| 1404 | - | "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", | |
| 1405 | - | "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", | |
| 1406 | - | "dev": true, | |
| 1407 | - | "license": "MIT", | |
| 1408 | - | "engines": { | |
| 1409 | - | "node": ">= 4" | |
| 1410 | - | } | |
| 1411 | - | }, | |
| 1412 | - | "node_modules/@typescript-eslint/parser": { | |
| 1413 | - | "version": "8.57.1", | |
| 1414 | - | "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.1.tgz", | |
| 1415 | - | "integrity": "sha512-k4eNDan0EIMTT/dUKc/g+rsJ6wcHYhNPdY19VoX/EOtaAG8DLtKCykhrUnuHPYvinn5jhAPgD2Qw9hXBwrahsw==", | |
| 1416 | - | "dev": true, | |
| 1417 | - | "license": "MIT", | |
| 1418 | - | "peer": true, | |
| 1419 | - | "dependencies": { | |
| 1420 | - | "@typescript-eslint/scope-manager": "8.57.1", | |
| 1421 | - | "@typescript-eslint/types": "8.57.1", | |
| 1422 | - | "@typescript-eslint/typescript-estree": "8.57.1", | |
| 1423 | - | "@typescript-eslint/visitor-keys": "8.57.1", | |
| 1424 | - | "debug": "^4.4.3" | |
| 1425 | - | }, | |
| 1426 | - | "engines": { | |
| 1427 | - | "node": "^18.18.0 || ^20.9.0 || >=21.1.0" | |
| 1428 | - | }, | |
| 1429 | - | "funding": { | |
| 1430 | - | "type": "opencollective", | |
| 1431 | - | "url": "https://opencollective.com/typescript-eslint" | |
| 1432 | - | }, | |
| 1433 | - | "peerDependencies": { | |
| 1434 | - | "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", | |
| 1435 | - | "typescript": ">=4.8.4 <6.0.0" | |
| 1436 | - | } | |
| 1437 | - | }, | |
| 1438 | - | "node_modules/@typescript-eslint/project-service": { | |
| 1439 | - | "version": "8.57.1", | |
| 1440 | - | "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.1.tgz", | |
| 1441 | - | "integrity": "sha512-vx1F37BRO1OftsYlmG9xay1TqnjNVlqALymwWVuYTdo18XuKxtBpCj1QlzNIEHlvlB27osvXFWptYiEWsVdYsg==", | |
| 1442 | - | "dev": true, | |
| 1443 | - | "license": "MIT", | |
| 1444 | - | "dependencies": { | |
| 1445 | - | "@typescript-eslint/tsconfig-utils": "^8.57.1", | |
| 1446 | - | "@typescript-eslint/types": "^8.57.1", | |
| 1447 | - | "debug": "^4.4.3" | |
| 1448 | - | }, | |
| 1449 | - | "engines": { | |
| 1450 | - | "node": "^18.18.0 || ^20.9.0 || >=21.1.0" | |
| 1451 | - | }, | |
| 1452 | - | "funding": { | |
| 1453 | - | "type": "opencollective", | |
| 1454 | - | "url": "https://opencollective.com/typescript-eslint" | |
| 1455 | - | }, | |
| 1456 | - | "peerDependencies": { | |
| 1457 | - | "typescript": ">=4.8.4 <6.0.0" | |
| 1458 | - | } | |
| 1459 | - | }, | |
| 1460 | - | "node_modules/@typescript-eslint/scope-manager": { | |
| 1461 | - | "version": "8.57.1", | |
| 1462 | - | "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.1.tgz", | |
| 1463 | - | "integrity": "sha512-hs/QcpCwlwT2L5S+3fT6gp0PabyGk4Q0Rv2doJXA0435/OpnSR3VRgvrp8Xdoc3UAYSg9cyUjTeFXZEPg/3OKg==", | |
| 1464 | - | "dev": true, | |
| 1465 | - | "license": "MIT", | |
| 1466 | - | "dependencies": { | |
| 1467 | - | "@typescript-eslint/types": "8.57.1", | |
| 1468 | - | "@typescript-eslint/visitor-keys": "8.57.1" | |
| 1469 | - | }, | |
| 1470 | - | "engines": { | |
| 1471 | - | "node": "^18.18.0 || ^20.9.0 || >=21.1.0" | |
| 1472 | - | }, | |
| 1473 | - | "funding": { | |
| 1474 | - | "type": "opencollective", | |
| 1475 | - | "url": "https://opencollective.com/typescript-eslint" | |
| 1476 | - | } | |
| 1477 | - | }, | |
| 1478 | - | "node_modules/@typescript-eslint/tsconfig-utils": { | |
| 1479 | - | "version": "8.57.1", | |
| 1480 | - | "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.1.tgz", | |
| 1481 | - | "integrity": "sha512-0lgOZB8cl19fHO4eI46YUx2EceQqhgkPSuCGLlGi79L2jwYY1cxeYc1Nae8Aw1xjgW3PKVDLlr3YJ6Bxx8HkWg==", | |
| 1482 | - | "dev": true, | |
| 1483 | - | "license": "MIT", | |
| 1484 | - | "engines": { | |
| 1485 | - | "node": "^18.18.0 || ^20.9.0 || >=21.1.0" | |
| 1486 | - | }, | |
| 1487 | - | "funding": { | |
| 1488 | - | "type": "opencollective", | |
| 1489 | - | "url": "https://opencollective.com/typescript-eslint" | |
| 1490 | - | }, | |
| 1491 | - | "peerDependencies": { | |
| 1492 | - | "typescript": ">=4.8.4 <6.0.0" | |
| 1493 | - | } | |
| 1494 | - | }, | |
| 1495 | - | "node_modules/@typescript-eslint/type-utils": { | |
| 1496 | - | "version": "8.57.1", | |
| 1497 | - | "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.1.tgz", | |
| 1498 | - | "integrity": "sha512-+Bwwm0ScukFdyoJsh2u6pp4S9ktegF98pYUU0hkphOOqdMB+1sNQhIz8y5E9+4pOioZijrkfNO/HUJVAFFfPKA==", | |
| 1499 | - | "dev": true, | |
| 1500 | - | "license": "MIT", | |
| 1501 | - | "dependencies": { | |
| 1502 | - | "@typescript-eslint/types": "8.57.1", | |
| 1503 | - | "@typescript-eslint/typescript-estree": "8.57.1", | |
| 1504 | - | "@typescript-eslint/utils": "8.57.1", | |
| 1505 | - | "debug": "^4.4.3", | |
| 1506 | - | "ts-api-utils": "^2.4.0" | |
| 1507 | - | }, | |
| 1508 | - | "engines": { | |
| 1509 | - | "node": "^18.18.0 || ^20.9.0 || >=21.1.0" | |
| 1510 | - | }, | |
| 1511 | - | "funding": { | |
| 1512 | - | "type": "opencollective", | |
| 1513 | - | "url": "https://opencollective.com/typescript-eslint" | |
| 1514 | - | }, | |
| 1515 | - | "peerDependencies": { | |
| 1516 | - | "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", | |
| 1517 | - | "typescript": ">=4.8.4 <6.0.0" | |
| 1518 | - | } | |
| 1519 | - | }, | |
| 1520 | - | "node_modules/@typescript-eslint/types": { | |
| 1521 | - | "version": "8.57.1", | |
| 1522 | - | "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.1.tgz", | |
| 1523 | - | "integrity": "sha512-S29BOBPJSFUiblEl6RzPPjJt6w25A6XsBqRVDt53tA/tlL8q7ceQNZHTjPeONt/3S7KRI4quk+yP9jK2WjBiPQ==", | |
| 1524 | - | "dev": true, | |
| 1525 | - | "license": "MIT", | |
| 1526 | - | "engines": { | |
| 1527 | - | "node": "^18.18.0 || ^20.9.0 || >=21.1.0" | |
| 1528 | - | }, | |
| 1529 | - | "funding": { | |
| 1530 | - | "type": "opencollective", | |
| 1531 | - | "url": "https://opencollective.com/typescript-eslint" | |
| 1532 | - | } | |
| 1533 | - | }, | |
| 1534 | - | "node_modules/@typescript-eslint/typescript-estree": { | |
| 1535 | - | "version": "8.57.1", | |
| 1536 | - | "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.1.tgz", | |
| 1537 | - | "integrity": "sha512-ybe2hS9G6pXpqGtPli9Gx9quNV0TWLOmh58ADlmZe9DguLq0tiAKVjirSbtM1szG6+QH6rVXyU6GTLQbWnMY+g==", | |
| 1538 | - | "dev": true, | |
| 1539 | - | "license": "MIT", | |
| 1540 | - | "dependencies": { | |
| 1541 | - | "@typescript-eslint/project-service": "8.57.1", | |
| 1542 | - | "@typescript-eslint/tsconfig-utils": "8.57.1", | |
| 1543 | - | "@typescript-eslint/types": "8.57.1", | |
| 1544 | - | "@typescript-eslint/visitor-keys": "8.57.1", | |
| 1545 | - | "debug": "^4.4.3", | |
| 1546 | - | "minimatch": "^10.2.2", | |
| 1547 | - | "semver": "^7.7.3", | |
| 1548 | - | "tinyglobby": "^0.2.15", | |
| 1549 | - | "ts-api-utils": "^2.4.0" | |
| 1550 | - | }, | |
| 1551 | - | "engines": { | |
| 1552 | - | "node": "^18.18.0 || ^20.9.0 || >=21.1.0" | |
| 1553 | - | }, | |
| 1554 | - | "funding": { | |
| 1555 | - | "type": "opencollective", | |
| 1556 | - | "url": "https://opencollective.com/typescript-eslint" | |
| 1557 | - | }, | |
| 1558 | - | "peerDependencies": { | |
| 1559 | - | "typescript": ">=4.8.4 <6.0.0" | |
| 1560 | - | } | |
| 1561 | - | }, | |
| 1562 | - | "node_modules/@typescript-eslint/utils": { | |
| 1563 | - | "version": "8.57.1", | |
| 1564 | - | "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.1.tgz", | |
| 1565 | - | "integrity": "sha512-XUNSJ/lEVFttPMMoDVA2r2bwrl8/oPx8cURtczkSEswY5T3AeLmCy+EKWQNdL4u0MmAHOjcWrqJp2cdvgjn8dQ==", | |
| 1566 | - | "dev": true, | |
| 1567 | - | "license": "MIT", | |
| 1568 | - | "dependencies": { | |
| 1569 | - | "@eslint-community/eslint-utils": "^4.9.1", | |
| 1570 | - | "@typescript-eslint/scope-manager": "8.57.1", | |
| 1571 | - | "@typescript-eslint/types": "8.57.1", | |
| 1572 | - | "@typescript-eslint/typescript-estree": "8.57.1" | |
| 1573 | - | }, | |
| 1574 | - | "engines": { | |
| 1575 | - | "node": "^18.18.0 || ^20.9.0 || >=21.1.0" | |
| 1576 | - | }, | |
| 1577 | - | "funding": { | |
| 1578 | - | "type": "opencollective", | |
| 1579 | - | "url": "https://opencollective.com/typescript-eslint" | |
| 1580 | - | }, | |
| 1581 | - | "peerDependencies": { | |
| 1582 | - | "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", | |
| 1583 | - | "typescript": ">=4.8.4 <6.0.0" | |
| 1584 | - | } | |
| 1585 | - | }, | |
| 1586 | - | "node_modules/@typescript-eslint/visitor-keys": { | |
| 1587 | - | "version": "8.57.1", | |
| 1588 | - | "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.1.tgz", | |
| 1589 | - | "integrity": "sha512-YWnmJkXbofiz9KbnbbwuA2rpGkFPLbAIetcCNO6mJ8gdhdZ/v7WDXsoGFAJuM6ikUFKTlSQnjWnVO4ux+UzS6A==", | |
| 1590 | - | "dev": true, | |
| 1591 | - | "license": "MIT", | |
| 1592 | - | "dependencies": { | |
| 1593 | - | "@typescript-eslint/types": "8.57.1", | |
| 1594 | - | "eslint-visitor-keys": "^5.0.0" | |
| 1595 | - | }, | |
| 1596 | - | "engines": { | |
| 1597 | - | "node": "^18.18.0 || ^20.9.0 || >=21.1.0" | |
| 1598 | - | }, | |
| 1599 | - | "funding": { | |
| 1600 | - | "type": "opencollective", | |
| 1601 | - | "url": "https://opencollective.com/typescript-eslint" | |
| 1602 | - | } | |
| 1603 | - | }, | |
| 1604 | - | "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { | |
| 1605 | - | "version": "5.0.1", | |
| 1606 | - | "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", | |
| 1607 | - | "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", | |
| 1608 | - | "dev": true, | |
| 1609 | - | "license": "Apache-2.0", | |
| 1610 | - | "engines": { | |
| 1611 | - | "node": "^20.19.0 || ^22.13.0 || >=24" | |
| 1612 | - | }, | |
| 1613 | - | "funding": { | |
| 1614 | - | "url": "https://opencollective.com/eslint" | |
| 1615 | - | } | |
| 1616 | - | }, | |
| 1617 | - | "node_modules/acorn": { | |
| 1618 | - | "version": "8.16.0", | |
| 1619 | - | "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", | |
| 1620 | - | "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", | |
| 1621 | - | "license": "MIT", | |
| 1622 | - | "peer": true, | |
| 1623 | - | "bin": { | |
| 1624 | - | "acorn": "bin/acorn" | |
| 1625 | - | }, | |
| 1626 | - | "engines": { | |
| 1627 | - | "node": ">=0.4.0" | |
| 1628 | - | } | |
| 1629 | - | }, | |
| 1630 | - | "node_modules/acorn-jsx": { | |
| 1631 | - | "version": "5.3.2", | |
| 1632 | - | "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", | |
| 1633 | - | "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", | |
| 1634 | - | "dev": true, | |
| 1635 | - | "license": "MIT", | |
| 1636 | - | "peerDependencies": { | |
| 1637 | - | "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" | |
| 1638 | - | } | |
| 1639 | - | }, | |
| 1640 | - | "node_modules/ajv": { | |
| 1641 | - | "version": "6.14.0", | |
| 1642 | - | "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", | |
| 1643 | - | "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", | |
| 1644 | - | "dev": true, | |
| 1645 | - | "license": "MIT", | |
| 1646 | - | "dependencies": { | |
| 1647 | - | "fast-deep-equal": "^3.1.1", | |
| 1648 | - | "fast-json-stable-stringify": "^2.0.0", | |
| 1649 | - | "json-schema-traverse": "^0.4.1", | |
| 1650 | - | "uri-js": "^4.2.2" | |
| 1651 | - | }, | |
| 1652 | - | "funding": { | |
| 1653 | - | "type": "github", | |
| 1654 | - | "url": "https://github.com/sponsors/epoberezkin" | |
| 1655 | - | } | |
| 1656 | - | }, | |
| 1657 | - | "node_modules/aria-query": { | |
| 1658 | - | "version": "5.3.1", | |
| 1659 | - | "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz", | |
| 1660 | - | "integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==", | |
| 1661 | - | "license": "Apache-2.0", | |
| 1662 | - | "engines": { | |
| 1663 | - | "node": ">= 0.4" | |
| 1664 | - | } | |
| 1665 | - | }, | |
| 1666 | - | "node_modules/autoprefixer": { | |
| 1667 | - | "version": "10.4.27", | |
| 1668 | - | "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.27.tgz", | |
| 1669 | - | "integrity": "sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA==", | |
| 1670 | - | "dev": true, | |
| 1671 | - | "funding": [ | |
| 1672 | - | { | |
| 1673 | - | "type": "opencollective", | |
| 1674 | - | "url": "https://opencollective.com/postcss/" | |
| 1675 | - | }, | |
| 1676 | - | { | |
| 1677 | - | "type": "tidelift", | |
| 1678 | - | "url": "https://tidelift.com/funding/github/npm/autoprefixer" | |
| 1679 | - | }, | |
| 1680 | - | { | |
| 1681 | - | "type": "github", | |
| 1682 | - | "url": "https://github.com/sponsors/ai" | |
| 1683 | - | } | |
| 1684 | - | ], | |
| 1685 | - | "license": "MIT", | |
| 1686 | - | "dependencies": { | |
| 1687 | - | "browserslist": "^4.28.1", | |
| 1688 | - | "caniuse-lite": "^1.0.30001774", | |
| 1689 | - | "fraction.js": "^5.3.4", | |
| 1690 | - | "picocolors": "^1.1.1", | |
| 1691 | - | "postcss-value-parser": "^4.2.0" | |
| 1692 | - | }, | |
| 1693 | - | "bin": { | |
| 1694 | - | "autoprefixer": "bin/autoprefixer" | |
| 1695 | - | }, | |
| 1696 | - | "engines": { | |
| 1697 | - | "node": "^10 || ^12 || >=14" | |
| 1698 | - | }, | |
| 1699 | - | "peerDependencies": { | |
| 1700 | - | "postcss": "^8.1.0" | |
| 1701 | - | } | |
| 1702 | - | }, | |
| 1703 | - | "node_modules/axobject-query": { | |
| 1704 | - | "version": "4.1.0", | |
| 1705 | - | "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", | |
| 1706 | - | "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", | |
| 1707 | - | "license": "Apache-2.0", | |
| 1708 | - | "engines": { | |
| 1709 | - | "node": ">= 0.4" | |
| 1710 | - | } | |
| 1711 | - | }, | |
| 1712 | - | "node_modules/balanced-match": { | |
| 1713 | - | "version": "4.0.4", | |
| 1714 | - | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", | |
| 1715 | - | "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", | |
| 1716 | - | "dev": true, | |
| 1717 | - | "license": "MIT", | |
| 1718 | - | "engines": { | |
| 1719 | - | "node": "18 || 20 || >=22" | |
| 1720 | - | } | |
| 1721 | - | }, | |
| 1722 | - | "node_modules/baseline-browser-mapping": { | |
| 1723 | - | "version": "2.9.17", | |
| 1724 | - | "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.17.tgz", | |
| 1725 | - | "integrity": "sha512-agD0MgJFUP/4nvjqzIB29zRPUuCF7Ge6mEv9s8dHrtYD7QWXRcx75rOADE/d5ah1NI+0vkDl0yorDd5U852IQQ==", | |
| 1726 | - | "dev": true, | |
| 1727 | - | "license": "Apache-2.0", | |
| 1728 | - | "bin": { | |
| 1729 | - | "baseline-browser-mapping": "dist/cli.js" | |
| 1730 | - | } | |
| 1731 | - | }, | |
| 1732 | - | "node_modules/bits-ui": { | |
| 1733 | - | "version": "2.16.3", | |
| 1734 | - | "resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-2.16.3.tgz", | |
| 1735 | - | "integrity": "sha512-5hJ5dEhf5yPzkRFcxzgQHScGodeo0gK0MUUXrdLlRHWaBOBGZiacWLG96j/wwFatKwZvouw7q+sn14i0fx3RIg==", | |
| 1736 | - | "license": "MIT", | |
| 1737 | - | "dependencies": { | |
| 1738 | - | "@floating-ui/core": "^1.7.1", | |
| 1739 | - | "@floating-ui/dom": "^1.7.1", | |
| 1740 | - | "esm-env": "^1.1.2", | |
| 1741 | - | "runed": "^0.35.1", | |
| 1742 | - | "svelte-toolbelt": "^0.10.6", | |
| 1743 | - | "tabbable": "^6.2.0" | |
| 1744 | - | }, | |
| 1745 | - | "engines": { | |
| 1746 | - | "node": ">=20" | |
| 1747 | - | }, | |
| 1748 | - | "funding": { | |
| 1749 | - | "url": "https://github.com/sponsors/huntabyte" | |
| 1750 | - | }, | |
| 1751 | - | "peerDependencies": { | |
| 1752 | - | "@internationalized/date": "^3.8.1", | |
| 1753 | - | "svelte": "^5.33.0" | |
| 1754 | - | } | |
| 1755 | - | }, | |
| 1756 | - | "node_modules/brace-expansion": { | |
| 1757 | - | "version": "5.0.3", | |
| 1758 | - | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz", | |
| 1759 | - | "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==", | |
| 1760 | - | "dev": true, | |
| 1761 | - | "license": "MIT", | |
| 1762 | - | "dependencies": { | |
| 1763 | - | "balanced-match": "^4.0.2" | |
| 1764 | - | }, | |
| 1765 | - | "engines": { | |
| 1766 | - | "node": "18 || 20 || >=22" | |
| 1767 | - | } | |
| 1768 | - | }, | |
| 1769 | - | "node_modules/browserslist": { | |
| 1770 | - | "version": "4.28.1", | |
| 1771 | - | "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", | |
| 1772 | - | "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", | |
| 1773 | - | "dev": true, | |
| 1774 | - | "funding": [ | |
| 1775 | - | { | |
| 1776 | - | "type": "opencollective", | |
| 1777 | - | "url": "https://opencollective.com/browserslist" | |
| 1778 | - | }, | |
| 1779 | - | { | |
| 1780 | - | "type": "tidelift", | |
| 1781 | - | "url": "https://tidelift.com/funding/github/npm/browserslist" | |
| 1782 | - | }, | |
| 1783 | - | { | |
| 1784 | - | "type": "github", | |
| 1785 | - | "url": "https://github.com/sponsors/ai" | |
| 1786 | - | } | |
| 1787 | - | ], | |
| 1788 | - | "license": "MIT", | |
| 1789 | - | "peer": true, | |
| 1790 | - | "dependencies": { | |
| 1791 | - | "baseline-browser-mapping": "^2.9.0", | |
| 1792 | - | "caniuse-lite": "^1.0.30001759", | |
| 1793 | - | "electron-to-chromium": "^1.5.263", | |
| 1794 | - | "node-releases": "^2.0.27", | |
| 1795 | - | "update-browserslist-db": "^1.2.0" | |
| 1796 | - | }, | |
| 1797 | - | "bin": { | |
| 1798 | - | "browserslist": "cli.js" | |
| 1799 | - | }, | |
| 1800 | - | "engines": { | |
| 1801 | - | "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" | |
| 1802 | - | } | |
| 1803 | - | }, | |
| 1804 | - | "node_modules/caniuse-lite": { | |
| 1805 | - | "version": "1.0.30001776", | |
| 1806 | - | "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001776.tgz", | |
| 1807 | - | "integrity": "sha512-sg01JDPzZ9jGshqKSckOQthXnYwOEP50jeVFhaSFbZcOy05TiuuaffDOfcwtCisJ9kNQuLBFibYywv2Bgm9osw==", | |
| 1808 | - | "dev": true, | |
| 1809 | - | "funding": [ | |
| 1810 | - | { | |
| 1811 | - | "type": "opencollective", | |
| 1812 | - | "url": "https://opencollective.com/browserslist" | |
| 1813 | - | }, | |
| 1814 | - | { | |
| 1815 | - | "type": "tidelift", | |
| 1816 | - | "url": "https://tidelift.com/funding/github/npm/caniuse-lite" | |
| 1817 | - | }, | |
| 1818 | - | { | |
| 1819 | - | "type": "github", | |
| 1820 | - | "url": "https://github.com/sponsors/ai" | |
| 1821 | - | } | |
| 1822 | - | ], | |
| 1823 | - | "license": "CC-BY-4.0" | |
| 1824 | - | }, | |
| 1825 | - | "node_modules/chokidar": { | |
| 1826 | - | "version": "4.0.3", | |
| 1827 | - | "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", | |
| 1828 | - | "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", | |
| 1829 | - | "dev": true, | |
| 1830 | - | "license": "MIT", | |
| 1831 | - | "dependencies": { | |
| 1832 | - | "readdirp": "^4.0.1" | |
| 1833 | - | }, | |
| 1834 | - | "engines": { | |
| 1835 | - | "node": ">= 14.16.0" | |
| 1836 | - | }, | |
| 1837 | - | "funding": { | |
| 1838 | - | "url": "https://paulmillr.com/funding/" | |
| 1839 | - | } | |
| 1840 | - | }, | |
| 1841 | - | "node_modules/clsx": { | |
| 1842 | - | "version": "2.1.1", | |
| 1843 | - | "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", | |
| 1844 | - | "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", | |
| 1845 | - | "license": "MIT", | |
| 1846 | - | "engines": { | |
| 1847 | - | "node": ">=6" | |
| 1848 | - | } | |
| 1849 | - | }, | |
| 1850 | - | "node_modules/colord": { | |
| 1851 | - | "version": "2.9.3", | |
| 1852 | - | "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", | |
| 1853 | - | "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", | |
| 1854 | - | "dev": true, | |
| 1855 | - | "license": "MIT" | |
| 1856 | - | }, | |
| 1857 | - | "node_modules/cross-spawn": { | |
| 1858 | - | "version": "7.0.6", | |
| 1859 | - | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", | |
| 1860 | - | "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", | |
| 1861 | - | "dev": true, | |
| 1862 | - | "license": "MIT", | |
| 1863 | - | "dependencies": { | |
| 1864 | - | "path-key": "^3.1.0", | |
| 1865 | - | "shebang-command": "^2.0.0", | |
| 1866 | - | "which": "^2.0.1" | |
| 1867 | - | }, | |
| 1868 | - | "engines": { | |
| 1869 | - | "node": ">= 8" | |
| 1870 | - | } | |
| 1871 | - | }, | |
| 1872 | - | "node_modules/cssesc": { | |
| 1873 | - | "version": "3.0.0", | |
| 1874 | - | "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", | |
| 1875 | - | "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", | |
| 1876 | - | "dev": true, | |
| 1877 | - | "license": "MIT", | |
| 1878 | - | "bin": { | |
| 1879 | - | "cssesc": "bin/cssesc" | |
| 1880 | - | }, | |
| 1881 | - | "engines": { | |
| 1882 | - | "node": ">=4" | |
| 1883 | - | } | |
| 1884 | - | }, | |
| 1885 | - | "node_modules/debug": { | |
| 1886 | - | "version": "4.4.3", | |
| 1887 | - | "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", | |
| 1888 | - | "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", | |
| 1889 | - | "dev": true, | |
| 1890 | - | "license": "MIT", | |
| 1891 | - | "dependencies": { | |
| 1892 | - | "ms": "^2.1.3" | |
| 1893 | - | }, | |
| 1894 | - | "engines": { | |
| 1895 | - | "node": ">=6.0" | |
| 1896 | - | }, | |
| 1897 | - | "peerDependenciesMeta": { | |
| 1898 | - | "supports-color": { | |
| 1899 | - | "optional": true | |
| 1900 | - | } | |
| 1901 | - | } | |
| 1902 | - | }, | |
| 1903 | - | "node_modules/deep-is": { | |
| 1904 | - | "version": "0.1.4", | |
| 1905 | - | "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", | |
| 1906 | - | "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", | |
| 1907 | - | "dev": true, | |
| 1908 | - | "license": "MIT" | |
| 1909 | - | }, | |
| 1910 | - | "node_modules/deepmerge": { | |
| 1911 | - | "version": "4.3.1", | |
| 1912 | - | "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", | |
| 1913 | - | "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", | |
| 1914 | - | "dev": true, | |
| 1915 | - | "license": "MIT", | |
| 1916 | - | "engines": { | |
| 1917 | - | "node": ">=0.10.0" | |
| 1918 | - | } | |
| 1919 | - | }, | |
| 1920 | - | "node_modules/dequal": { | |
| 1921 | - | "version": "2.0.3", | |
| 1922 | - | "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", | |
| 1923 | - | "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", | |
| 1924 | - | "license": "MIT", | |
| 1925 | - | "engines": { | |
| 1926 | - | "node": ">=6" | |
| 1927 | - | } | |
| 1928 | - | }, | |
| 1929 | - | "node_modules/detect-libc": { | |
| 1930 | - | "version": "2.1.2", | |
| 1931 | - | "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", | |
| 1932 | - | "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", | |
| 1933 | - | "dev": true, | |
| 1934 | - | "license": "Apache-2.0", | |
| 1935 | - | "engines": { | |
| 1936 | - | "node": ">=8" | |
| 1937 | - | } | |
| 1938 | - | }, | |
| 1939 | - | "node_modules/devalue": { | |
| 1940 | - | "version": "5.6.4", | |
| 1941 | - | "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.4.tgz", | |
| 1942 | - | "integrity": "sha512-Gp6rDldRsFh/7XuouDbxMH3Mx8GMCcgzIb1pDTvNyn8pZGQ22u+Wa+lGV9dQCltFQ7uVw0MhRyb8XDskNFOReA==", | |
| 1943 | - | "license": "MIT" | |
| 1944 | - | }, | |
| 1945 | - | "node_modules/electron-to-chromium": { | |
| 1946 | - | "version": "1.5.277", | |
| 1947 | - | "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.277.tgz", | |
| 1948 | - | "integrity": "sha512-wKXFZw4erWmmOz5N/grBoJ2XrNJGDFMu2+W5ACHza5rHtvsqrK4gb6rnLC7XxKB9WlJ+RmyQatuEXmtm86xbnw==", | |
| 1949 | - | "dev": true, | |
| 1950 | - | "license": "ISC" | |
| 1951 | - | }, | |
| 1952 | - | "node_modules/enhanced-resolve": { | |
| 1953 | - | "version": "5.19.0", | |
| 1954 | - | "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz", | |
| 1955 | - | "integrity": "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==", | |
| 1956 | - | "dev": true, | |
| 1957 | - | "license": "MIT", | |
| 1958 | - | "dependencies": { | |
| 1959 | - | "graceful-fs": "^4.2.4", | |
| 1960 | - | "tapable": "^2.3.0" | |
| 1961 | - | }, | |
| 1962 | - | "engines": { | |
| 1963 | - | "node": ">=10.13.0" | |
| 1964 | - | } | |
| 1965 | - | }, | |
| 1966 | - | "node_modules/escalade": { | |
| 1967 | - | "version": "3.2.0", | |
| 1968 | - | "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", | |
| 1969 | - | "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", | |
| 1970 | - | "dev": true, | |
| 1971 | - | "license": "MIT", | |
| 1972 | - | "engines": { | |
| 1973 | - | "node": ">=6" | |
| 1974 | - | } | |
| 1975 | - | }, | |
| 1976 | - | "node_modules/escape-string-regexp": { | |
| 1977 | - | "version": "4.0.0", | |
| 1978 | - | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", | |
| 1979 | - | "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", | |
| 1980 | - | "dev": true, | |
| 1981 | - | "license": "MIT", | |
| 1982 | - | "engines": { | |
| 1983 | - | "node": ">=10" | |
| 1984 | - | }, | |
| 1985 | - | "funding": { | |
| 1986 | - | "url": "https://github.com/sponsors/sindresorhus" | |
| 1987 | - | } | |
| 1988 | - | }, | |
| 1989 | - | "node_modules/eslint": { | |
| 1990 | - | "version": "10.0.3", | |
| 1991 | - | "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.0.3.tgz", | |
| 1992 | - | "integrity": "sha512-COV33RzXZkqhG9P2rZCFl9ZmJ7WL+gQSCRzE7RhkbclbQPtLAWReL7ysA0Sh4c8Im2U9ynybdR56PV0XcKvqaQ==", | |
| 1993 | - | "dev": true, | |
| 1994 | - | "license": "MIT", | |
| 1995 | - | "peer": true, | |
| 1996 | - | "dependencies": { | |
| 1997 | - | "@eslint-community/eslint-utils": "^4.8.0", | |
| 1998 | - | "@eslint-community/regexpp": "^4.12.2", | |
| 1999 | - | "@eslint/config-array": "^0.23.3", | |
| 2000 | - | "@eslint/config-helpers": "^0.5.2", | |
| 2001 | - | "@eslint/core": "^1.1.1", | |
| 2002 | - | "@eslint/plugin-kit": "^0.6.1", | |
| 2003 | - | "@humanfs/node": "^0.16.6", | |
| 2004 | - | "@humanwhocodes/module-importer": "^1.0.1", | |
| 2005 | - | "@humanwhocodes/retry": "^0.4.2", | |
| 2006 | - | "@types/estree": "^1.0.6", | |
| 2007 | - | "ajv": "^6.14.0", | |
| 2008 | - | "cross-spawn": "^7.0.6", | |
| 2009 | - | "debug": "^4.3.2", | |
| 2010 | - | "escape-string-regexp": "^4.0.0", | |
| 2011 | - | "eslint-scope": "^9.1.2", | |
| 2012 | - | "eslint-visitor-keys": "^5.0.1", | |
| 2013 | - | "espree": "^11.1.1", | |
| 2014 | - | "esquery": "^1.7.0", | |
| 2015 | - | "esutils": "^2.0.2", | |
| 2016 | - | "fast-deep-equal": "^3.1.3", | |
| 2017 | - | "file-entry-cache": "^8.0.0", | |
| 2018 | - | "find-up": "^5.0.0", | |
| 2019 | - | "glob-parent": "^6.0.2", | |
| 2020 | - | "ignore": "^5.2.0", | |
| 2021 | - | "imurmurhash": "^0.1.4", | |
| 2022 | - | "is-glob": "^4.0.0", | |
| 2023 | - | "json-stable-stringify-without-jsonify": "^1.0.1", | |
| 2024 | - | "minimatch": "^10.2.4", | |
| 2025 | - | "natural-compare": "^1.4.0", | |
| 2026 | - | "optionator": "^0.9.3" | |
| 2027 | - | }, | |
| 2028 | - | "bin": { | |
| 2029 | - | "eslint": "bin/eslint.js" | |
| 2030 | - | }, | |
| 2031 | - | "engines": { | |
| 2032 | - | "node": "^20.19.0 || ^22.13.0 || >=24" | |
| 2033 | - | }, | |
| 2034 | - | "funding": { | |
| 2035 | - | "url": "https://eslint.org/donate" | |
| 2036 | - | }, | |
| 2037 | - | "peerDependencies": { | |
| 2038 | - | "jiti": "*" | |
| 2039 | - | }, | |
| 2040 | - | "peerDependenciesMeta": { | |
| 2041 | - | "jiti": { | |
| 2042 | - | "optional": true | |
| 2043 | - | } | |
| 2044 | - | } | |
| 2045 | - | }, | |
| 2046 | - | "node_modules/eslint-plugin-svelte": { | |
| 2047 | - | "version": "3.15.2", | |
| 2048 | - | "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-3.15.2.tgz", | |
| 2049 | - | "integrity": "sha512-k4Nsjs3bHujeEnnckoTM4mFYR1e8Mb9l2rTwNdmYiamA+Tjzn8X+2F+fuSP2w4VbXYhn2bmySyACQYdmUDW2Cg==", | |
| 2050 | - | "dev": true, | |
| 2051 | - | "license": "MIT", | |
| 2052 | - | "dependencies": { | |
| 2053 | - | "@eslint-community/eslint-utils": "^4.6.1", | |
| 2054 | - | "@jridgewell/sourcemap-codec": "^1.5.0", | |
| 2055 | - | "esutils": "^2.0.3", | |
| 2056 | - | "globals": "^16.0.0", | |
| 2057 | - | "known-css-properties": "^0.37.0", | |
| 2058 | - | "postcss": "^8.4.49", | |
| 2059 | - | "postcss-load-config": "^3.1.4", | |
| 2060 | - | "postcss-safe-parser": "^7.0.0", | |
| 2061 | - | "semver": "^7.6.3", | |
| 2062 | - | "svelte-eslint-parser": "^1.4.0" | |
| 2063 | - | }, | |
| 2064 | - | "engines": { | |
| 2065 | - | "node": "^18.18.0 || ^20.9.0 || >=21.1.0" | |
| 2066 | - | }, | |
| 2067 | - | "funding": { | |
| 2068 | - | "url": "https://github.com/sponsors/ota-meshi" | |
| 2069 | - | }, | |
| 2070 | - | "peerDependencies": { | |
| 2071 | - | "eslint": "^8.57.1 || ^9.0.0 || ^10.0.0", | |
| 2072 | - | "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" | |
| 2073 | - | }, | |
| 2074 | - | "peerDependenciesMeta": { | |
| 2075 | - | "svelte": { | |
| 2076 | - | "optional": true | |
| 2077 | - | } | |
| 2078 | - | } | |
| 2079 | - | }, | |
| 2080 | - | "node_modules/eslint-plugin-svelte/node_modules/globals": { | |
| 2081 | - | "version": "16.5.0", | |
| 2082 | - | "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", | |
| 2083 | - | "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", | |
| 2084 | - | "dev": true, | |
| 2085 | - | "license": "MIT", | |
| 2086 | - | "engines": { | |
| 2087 | - | "node": ">=18" | |
| 2088 | - | }, | |
| 2089 | - | "funding": { | |
| 2090 | - | "url": "https://github.com/sponsors/sindresorhus" | |
| 2091 | - | } | |
| 2092 | - | }, | |
| 2093 | - | "node_modules/eslint-scope": { | |
| 2094 | - | "version": "8.4.0", | |
| 2095 | - | "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", | |
| 2096 | - | "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", | |
| 2097 | - | "dev": true, | |
| 2098 | - | "license": "BSD-2-Clause", | |
| 2099 | - | "dependencies": { | |
| 2100 | - | "esrecurse": "^4.3.0", | |
| 2101 | - | "estraverse": "^5.2.0" | |
| 2102 | - | }, | |
| 2103 | - | "engines": { | |
| 2104 | - | "node": "^18.18.0 || ^20.9.0 || >=21.1.0" | |
| 2105 | - | }, | |
| 2106 | - | "funding": { | |
| 2107 | - | "url": "https://opencollective.com/eslint" | |
| 2108 | - | } | |
| 2109 | - | }, | |
| 2110 | - | "node_modules/eslint-visitor-keys": { | |
| 2111 | - | "version": "3.4.3", | |
| 2112 | - | "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", | |
| 2113 | - | "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", | |
| 2114 | - | "dev": true, | |
| 2115 | - | "license": "Apache-2.0", | |
| 2116 | - | "engines": { | |
| 2117 | - | "node": "^12.22.0 || ^14.17.0 || >=16.0.0" | |
| 2118 | - | }, | |
| 2119 | - | "funding": { | |
| 2120 | - | "url": "https://opencollective.com/eslint" | |
| 2121 | - | } | |
| 2122 | - | }, | |
| 2123 | - | "node_modules/eslint/node_modules/eslint-scope": { | |
| 2124 | - | "version": "9.1.2", | |
| 2125 | - | "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", | |
| 2126 | - | "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", | |
| 2127 | - | "dev": true, | |
| 2128 | - | "license": "BSD-2-Clause", | |
| 2129 | - | "dependencies": { | |
| 2130 | - | "@types/esrecurse": "^4.3.1", | |
| 2131 | - | "@types/estree": "^1.0.8", | |
| 2132 | - | "esrecurse": "^4.3.0", | |
| 2133 | - | "estraverse": "^5.2.0" | |
| 2134 | - | }, | |
| 2135 | - | "engines": { | |
| 2136 | - | "node": "^20.19.0 || ^22.13.0 || >=24" | |
| 2137 | - | }, | |
| 2138 | - | "funding": { | |
| 2139 | - | "url": "https://opencollective.com/eslint" | |
| 2140 | - | } | |
| 2141 | - | }, | |
| 2142 | - | "node_modules/eslint/node_modules/eslint-visitor-keys": { | |
| 2143 | - | "version": "5.0.1", | |
| 2144 | - | "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", | |
| 2145 | - | "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", | |
| 2146 | - | "dev": true, | |
| 2147 | - | "license": "Apache-2.0", | |
| 2148 | - | "engines": { | |
| 2149 | - | "node": "^20.19.0 || ^22.13.0 || >=24" | |
| 2150 | - | }, | |
| 2151 | - | "funding": { | |
| 2152 | - | "url": "https://opencollective.com/eslint" | |
| 2153 | - | } | |
| 2154 | - | }, | |
| 2155 | - | "node_modules/eslint/node_modules/espree": { | |
| 2156 | - | "version": "11.1.1", | |
| 2157 | - | "resolved": "https://registry.npmjs.org/espree/-/espree-11.1.1.tgz", | |
| 2158 | - | "integrity": "sha512-AVHPqQoZYc+RUM4/3Ly5udlZY/U4LS8pIG05jEjWM2lQMU/oaZ7qshzAl2YP1tfNmXfftH3ohurfwNAug+MnsQ==", | |
| 2159 | - | "dev": true, | |
| 2160 | - | "license": "BSD-2-Clause", | |
| 2161 | - | "dependencies": { | |
| 2162 | - | "acorn": "^8.16.0", | |
| 2163 | - | "acorn-jsx": "^5.3.2", | |
| 2164 | - | "eslint-visitor-keys": "^5.0.1" | |
| 2165 | - | }, | |
| 2166 | - | "engines": { | |
| 2167 | - | "node": "^20.19.0 || ^22.13.0 || >=24" | |
| 2168 | - | }, | |
| 2169 | - | "funding": { | |
| 2170 | - | "url": "https://opencollective.com/eslint" | |
| 2171 | - | } | |
| 2172 | - | }, | |
| 2173 | - | "node_modules/esm-env": { | |
| 2174 | - | "version": "1.2.2", | |
| 2175 | - | "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", | |
| 2176 | - | "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", | |
| 2177 | - | "license": "MIT" | |
| 2178 | - | }, | |
| 2179 | - | "node_modules/espree": { | |
| 2180 | - | "version": "10.4.0", | |
| 2181 | - | "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", | |
| 2182 | - | "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", | |
| 2183 | - | "dev": true, | |
| 2184 | - | "license": "BSD-2-Clause", | |
| 2185 | - | "dependencies": { | |
| 2186 | - | "acorn": "^8.15.0", | |
| 2187 | - | "acorn-jsx": "^5.3.2", | |
| 2188 | - | "eslint-visitor-keys": "^4.2.1" | |
| 2189 | - | }, | |
| 2190 | - | "engines": { | |
| 2191 | - | "node": "^18.18.0 || ^20.9.0 || >=21.1.0" | |
| 2192 | - | }, | |
| 2193 | - | "funding": { | |
| 2194 | - | "url": "https://opencollective.com/eslint" | |
| 2195 | - | } | |
| 2196 | - | }, | |
| 2197 | - | "node_modules/espree/node_modules/eslint-visitor-keys": { | |
| 2198 | - | "version": "4.2.1", | |
| 2199 | - | "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", | |
| 2200 | - | "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", | |
| 2201 | - | "dev": true, | |
| 2202 | - | "license": "Apache-2.0", | |
| 2203 | - | "engines": { | |
| 2204 | - | "node": "^18.18.0 || ^20.9.0 || >=21.1.0" | |
| 2205 | - | }, | |
| 2206 | - | "funding": { | |
| 2207 | - | "url": "https://opencollective.com/eslint" | |
| 2208 | - | } | |
| 2209 | - | }, | |
| 2210 | - | "node_modules/esquery": { | |
| 2211 | - | "version": "1.7.0", | |
| 2212 | - | "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", | |
| 2213 | - | "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", | |
| 2214 | - | "dev": true, | |
| 2215 | - | "license": "BSD-3-Clause", | |
| 2216 | - | "dependencies": { | |
| 2217 | - | "estraverse": "^5.1.0" | |
| 2218 | - | }, | |
| 2219 | - | "engines": { | |
| 2220 | - | "node": ">=0.10" | |
| 2221 | - | } | |
| 2222 | - | }, | |
| 2223 | - | "node_modules/esrap": { | |
| 2224 | - | "version": "2.2.2", | |
| 2225 | - | "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.2.tgz", | |
| 2226 | - | "integrity": "sha512-zA6497ha+qKvoWIK+WM9NAh5ni17sKZKhbS5B3PoYbBvaYHZWoS33zmFybmyqpn07RLUxSmn+RCls2/XF+d0oQ==", | |
| 2227 | - | "license": "MIT", | |
| 2228 | - | "dependencies": { | |
| 2229 | - | "@jridgewell/sourcemap-codec": "^1.4.15" | |
| 2230 | - | } | |
| 2231 | - | }, | |
| 2232 | - | "node_modules/esrecurse": { | |
| 2233 | - | "version": "4.3.0", | |
| 2234 | - | "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", | |
| 2235 | - | "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", | |
| 2236 | - | "dev": true, | |
| 2237 | - | "license": "BSD-2-Clause", | |
| 2238 | - | "dependencies": { | |
| 2239 | - | "estraverse": "^5.2.0" | |
| 2240 | - | }, | |
| 2241 | - | "engines": { | |
| 2242 | - | "node": ">=4.0" | |
| 2243 | - | } | |
| 2244 | - | }, | |
| 2245 | - | "node_modules/estraverse": { | |
| 2246 | - | "version": "5.3.0", | |
| 2247 | - | "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", | |
| 2248 | - | "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", | |
| 2249 | - | "dev": true, | |
| 2250 | - | "license": "BSD-2-Clause", | |
| 2251 | - | "engines": { | |
| 2252 | - | "node": ">=4.0" | |
| 2253 | - | } | |
| 2254 | - | }, | |
| 2255 | - | "node_modules/esutils": { | |
| 2256 | - | "version": "2.0.3", | |
| 2257 | - | "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", | |
| 2258 | - | "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", | |
| 2259 | - | "dev": true, | |
| 2260 | - | "license": "BSD-2-Clause", | |
| 2261 | - | "engines": { | |
| 2262 | - | "node": ">=0.10.0" | |
| 2263 | - | } | |
| 2264 | - | }, | |
| 2265 | - | "node_modules/fast-deep-equal": { | |
| 2266 | - | "version": "3.1.3", | |
| 2267 | - | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", | |
| 2268 | - | "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", | |
| 2269 | - | "dev": true, | |
| 2270 | - | "license": "MIT" | |
| 2271 | - | }, | |
| 2272 | - | "node_modules/fast-json-stable-stringify": { | |
| 2273 | - | "version": "2.1.0", | |
| 2274 | - | "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", | |
| 2275 | - | "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", | |
| 2276 | - | "dev": true, | |
| 2277 | - | "license": "MIT" | |
| 2278 | - | }, | |
| 2279 | - | "node_modules/fast-levenshtein": { | |
| 2280 | - | "version": "2.0.6", | |
| 2281 | - | "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", | |
| 2282 | - | "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", | |
| 2283 | - | "dev": true, | |
| 2284 | - | "license": "MIT" | |
| 2285 | - | }, | |
| 2286 | - | "node_modules/fdir": { | |
| 2287 | - | "version": "6.5.0", | |
| 2288 | - | "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", | |
| 2289 | - | "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", | |
| 2290 | - | "dev": true, | |
| 2291 | - | "license": "MIT", | |
| 2292 | - | "engines": { | |
| 2293 | - | "node": ">=12.0.0" | |
| 2294 | - | }, | |
| 2295 | - | "peerDependencies": { | |
| 2296 | - | "picomatch": "^3 || ^4" | |
| 2297 | - | }, | |
| 2298 | - | "peerDependenciesMeta": { | |
| 2299 | - | "picomatch": { | |
| 2300 | - | "optional": true | |
| 2301 | - | } | |
| 2302 | - | } | |
| 2303 | - | }, | |
| 2304 | - | "node_modules/file-entry-cache": { | |
| 2305 | - | "version": "8.0.0", | |
| 2306 | - | "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", | |
| 2307 | - | "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", | |
| 2308 | - | "dev": true, | |
| 2309 | - | "license": "MIT", | |
| 2310 | - | "dependencies": { | |
| 2311 | - | "flat-cache": "^4.0.0" | |
| 2312 | - | }, | |
| 2313 | - | "engines": { | |
| 2314 | - | "node": ">=16.0.0" | |
| 2315 | - | } | |
| 2316 | - | }, | |
| 2317 | - | "node_modules/find-up": { | |
| 2318 | - | "version": "5.0.0", | |
| 2319 | - | "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", | |
| 2320 | - | "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", | |
| 2321 | - | "dev": true, | |
| 2322 | - | "license": "MIT", | |
| 2323 | - | "dependencies": { | |
| 2324 | - | "locate-path": "^6.0.0", | |
| 2325 | - | "path-exists": "^4.0.0" | |
| 2326 | - | }, | |
| 2327 | - | "engines": { | |
| 2328 | - | "node": ">=10" | |
| 2329 | - | }, | |
| 2330 | - | "funding": { | |
| 2331 | - | "url": "https://github.com/sponsors/sindresorhus" | |
| 2332 | - | } | |
| 2333 | - | }, | |
| 2334 | - | "node_modules/flat-cache": { | |
| 2335 | - | "version": "4.0.1", | |
| 2336 | - | "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", | |
| 2337 | - | "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", | |
| 2338 | - | "dev": true, | |
| 2339 | - | "license": "MIT", | |
| 2340 | - | "dependencies": { | |
| 2341 | - | "flatted": "^3.2.9", | |
| 2342 | - | "keyv": "^4.5.4" | |
| 2343 | - | }, | |
| 2344 | - | "engines": { | |
| 2345 | - | "node": ">=16" | |
| 2346 | - | } | |
| 2347 | - | }, | |
| 2348 | - | "node_modules/flatted": { | |
| 2349 | - | "version": "3.3.3", | |
| 2350 | - | "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", | |
| 2351 | - | "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", | |
| 2352 | - | "dev": true, | |
| 2353 | - | "license": "ISC" | |
| 2354 | - | }, | |
| 2355 | - | "node_modules/fraction.js": { | |
| 2356 | - | "version": "5.3.4", | |
| 2357 | - | "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", | |
| 2358 | - | "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", | |
| 2359 | - | "dev": true, | |
| 2360 | - | "license": "MIT", | |
| 2361 | - | "engines": { | |
| 2362 | - | "node": "*" | |
| 2363 | - | }, | |
| 2364 | - | "funding": { | |
| 2365 | - | "type": "github", | |
| 2366 | - | "url": "https://github.com/sponsors/rawify" | |
| 2367 | - | } | |
| 2368 | - | }, | |
| 2369 | - | "node_modules/frappe-charts": { | |
| 2370 | - | "version": "1.6.2", | |
| 2371 | - | "resolved": "https://registry.npmjs.org/frappe-charts/-/frappe-charts-1.6.2.tgz", | |
| 2372 | - | "integrity": "sha512-9TC3/+YVUi84yYoEbxFiSqu+1FQ5If/ydUNj6i8FRpwynd08t6a7RkS+IRJozAk6NfdL8/LVTTE1DUOjjKZZxg==", | |
| 2373 | - | "license": "MIT" | |
| 2374 | - | }, | |
| 2375 | - | "node_modules/fsevents": { | |
| 2376 | - | "version": "2.3.3", | |
| 2377 | - | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", | |
| 2378 | - | "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", | |
| 2379 | - | "dev": true, | |
| 2380 | - | "hasInstallScript": true, | |
| 2381 | - | "license": "MIT", | |
| 2382 | - | "optional": true, | |
| 2383 | - | "os": [ | |
| 2384 | - | "darwin" | |
| 2385 | - | ], | |
| 2386 | - | "engines": { | |
| 2387 | - | "node": "^8.16.0 || ^10.6.0 || >=11.0.0" | |
| 2388 | - | } | |
| 2389 | - | }, | |
| 2390 | - | "node_modules/glob-parent": { | |
| 2391 | - | "version": "6.0.2", | |
| 2392 | - | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", | |
| 2393 | - | "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", | |
| 2394 | - | "dev": true, | |
| 2395 | - | "license": "ISC", | |
| 2396 | - | "dependencies": { | |
| 2397 | - | "is-glob": "^4.0.3" | |
| 2398 | - | }, | |
| 2399 | - | "engines": { | |
| 2400 | - | "node": ">=10.13.0" | |
| 2401 | - | } | |
| 2402 | - | }, | |
| 2403 | - | "node_modules/globals": { | |
| 2404 | - | "version": "17.4.0", | |
| 2405 | - | "resolved": "https://registry.npmjs.org/globals/-/globals-17.4.0.tgz", | |
| 2406 | - | "integrity": "sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==", | |
| 2407 | - | "dev": true, | |
| 2408 | - | "license": "MIT", | |
| 2409 | - | "engines": { | |
| 2410 | - | "node": ">=18" | |
| 2411 | - | }, | |
| 2412 | - | "funding": { | |
| 2413 | - | "url": "https://github.com/sponsors/sindresorhus" | |
| 2414 | - | } | |
| 2415 | - | }, | |
| 2416 | - | "node_modules/graceful-fs": { | |
| 2417 | - | "version": "4.2.11", | |
| 2418 | - | "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", | |
| 2419 | - | "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", | |
| 2420 | - | "dev": true, | |
| 2421 | - | "license": "ISC" | |
| 2422 | - | }, | |
| 2423 | - | "node_modules/ignore": { | |
| 2424 | - | "version": "5.3.2", | |
| 2425 | - | "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", | |
| 2426 | - | "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", | |
| 2427 | - | "dev": true, | |
| 2428 | - | "license": "MIT", | |
| 2429 | - | "engines": { | |
| 2430 | - | "node": ">= 4" | |
| 2431 | - | } | |
| 2432 | - | }, | |
| 2433 | - | "node_modules/imurmurhash": { | |
| 2434 | - | "version": "0.1.4", | |
| 2435 | - | "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", | |
| 2436 | - | "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", | |
| 2437 | - | "dev": true, | |
| 2438 | - | "license": "MIT", | |
| 2439 | - | "engines": { | |
| 2440 | - | "node": ">=0.8.19" | |
| 2441 | - | } | |
| 2442 | - | }, | |
| 2443 | - | "node_modules/inline-style-parser": { | |
| 2444 | - | "version": "0.2.7", | |
| 2445 | - | "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", | |
| 2446 | - | "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", | |
| 2447 | - | "license": "MIT" | |
| 2448 | - | }, | |
| 2449 | - | "node_modules/is-extglob": { | |
| 2450 | - | "version": "2.1.1", | |
| 2451 | - | "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", | |
| 2452 | - | "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", | |
| 2453 | - | "dev": true, | |
| 2454 | - | "license": "MIT", | |
| 2455 | - | "engines": { | |
| 2456 | - | "node": ">=0.10.0" | |
| 2457 | - | } | |
| 2458 | - | }, | |
| 2459 | - | "node_modules/is-glob": { | |
| 2460 | - | "version": "4.0.3", | |
| 2461 | - | "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", | |
| 2462 | - | "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", | |
| 2463 | - | "dev": true, | |
| 2464 | - | "license": "MIT", | |
| 2465 | - | "dependencies": { | |
| 2466 | - | "is-extglob": "^2.1.1" | |
| 2467 | - | }, | |
| 2468 | - | "engines": { | |
| 2469 | - | "node": ">=0.10.0" | |
| 2470 | - | } | |
| 2471 | - | }, | |
| 2472 | - | "node_modules/is-reference": { | |
| 2473 | - | "version": "3.0.3", | |
| 2474 | - | "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", | |
| 2475 | - | "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", | |
| 2476 | - | "license": "MIT", | |
| 2477 | - | "dependencies": { | |
| 2478 | - | "@types/estree": "^1.0.6" | |
| 2479 | - | } | |
| 2480 | - | }, | |
| 2481 | - | "node_modules/isexe": { | |
| 2482 | - | "version": "2.0.0", | |
| 2483 | - | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", | |
| 2484 | - | "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", | |
| 2485 | - | "dev": true, | |
| 2486 | - | "license": "ISC" | |
| 2487 | - | }, | |
| 2488 | - | "node_modules/jiti": { | |
| 2489 | - | "version": "2.6.1", | |
| 2490 | - | "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", | |
| 2491 | - | "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", | |
| 2492 | - | "dev": true, | |
| 2493 | - | "license": "MIT", | |
| 2494 | - | "bin": { | |
| 2495 | - | "jiti": "lib/jiti-cli.mjs" | |
| 2496 | - | } | |
| 2497 | - | }, | |
| 2498 | - | "node_modules/json-buffer": { | |
| 2499 | - | "version": "3.0.1", | |
| 2500 | - | "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", | |
| 2501 | - | "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", | |
| 2502 | - | "dev": true, | |
| 2503 | - | "license": "MIT" | |
| 2504 | - | }, | |
| 2505 | - | "node_modules/json-schema-traverse": { | |
| 2506 | - | "version": "0.4.1", | |
| 2507 | - | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", | |
| 2508 | - | "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", | |
| 2509 | - | "dev": true, | |
| 2510 | - | "license": "MIT" | |
| 2511 | - | }, | |
| 2512 | - | "node_modules/json-stable-stringify-without-jsonify": { | |
| 2513 | - | "version": "1.0.1", | |
| 2514 | - | "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", | |
| 2515 | - | "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", | |
| 2516 | - | "dev": true, | |
| 2517 | - | "license": "MIT" | |
| 2518 | - | }, | |
| 2519 | - | "node_modules/keyv": { | |
| 2520 | - | "version": "4.5.4", | |
| 2521 | - | "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", | |
| 2522 | - | "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", | |
| 2523 | - | "dev": true, | |
| 2524 | - | "license": "MIT", | |
| 2525 | - | "dependencies": { | |
| 2526 | - | "json-buffer": "3.0.1" | |
| 2527 | - | } | |
| 2528 | - | }, | |
| 2529 | - | "node_modules/known-css-properties": { | |
| 2530 | - | "version": "0.37.0", | |
| 2531 | - | "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.37.0.tgz", | |
| 2532 | - | "integrity": "sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ==", | |
| 2533 | - | "dev": true, | |
| 2534 | - | "license": "MIT" | |
| 2535 | - | }, | |
| 2536 | - | "node_modules/levn": { | |
| 2537 | - | "version": "0.4.1", | |
| 2538 | - | "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", | |
| 2539 | - | "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", | |
| 2540 | - | "dev": true, | |
| 2541 | - | "license": "MIT", | |
| 2542 | - | "dependencies": { | |
| 2543 | - | "prelude-ls": "^1.2.1", | |
| 2544 | - | "type-check": "~0.4.0" | |
| 2545 | - | }, | |
| 2546 | - | "engines": { | |
| 2547 | - | "node": ">= 0.8.0" | |
| 2548 | - | } | |
| 2549 | - | }, | |
| 2550 | - | "node_modules/lightningcss": { | |
| 2551 | - | "version": "1.32.0", | |
| 2552 | - | "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", | |
| 2553 | - | "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", | |
| 2554 | - | "dev": true, | |
| 2555 | - | "license": "MPL-2.0", | |
| 2556 | - | "dependencies": { | |
| 2557 | - | "detect-libc": "^2.0.3" | |
| 2558 | - | }, | |
| 2559 | - | "engines": { | |
| 2560 | - | "node": ">= 12.0.0" | |
| 2561 | - | }, | |
| 2562 | - | "funding": { | |
| 2563 | - | "type": "opencollective", | |
| 2564 | - | "url": "https://opencollective.com/parcel" | |
| 2565 | - | }, | |
| 2566 | - | "optionalDependencies": { | |
| 2567 | - | "lightningcss-android-arm64": "1.32.0", | |
| 2568 | - | "lightningcss-darwin-arm64": "1.32.0", | |
| 2569 | - | "lightningcss-darwin-x64": "1.32.0", | |
| 2570 | - | "lightningcss-freebsd-x64": "1.32.0", | |
| 2571 | - | "lightningcss-linux-arm-gnueabihf": "1.32.0", | |
| 2572 | - | "lightningcss-linux-arm64-gnu": "1.32.0", | |
| 2573 | - | "lightningcss-linux-arm64-musl": "1.32.0", | |
| 2574 | - | "lightningcss-linux-x64-gnu": "1.32.0", | |
| 2575 | - | "lightningcss-linux-x64-musl": "1.32.0", | |
| 2576 | - | "lightningcss-win32-arm64-msvc": "1.32.0", | |
| 2577 | - | "lightningcss-win32-x64-msvc": "1.32.0" | |
| 2578 | - | } | |
| 2579 | - | }, | |
| 2580 | - | "node_modules/lightningcss-android-arm64": { | |
| 2581 | - | "version": "1.32.0", | |
| 2582 | - | "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", | |
| 2583 | - | "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", | |
| 2584 | - | "cpu": [ | |
| 2585 | - | "arm64" | |
| 2586 | - | ], | |
| 2587 | - | "dev": true, | |
| 2588 | - | "license": "MPL-2.0", | |
| 2589 | - | "optional": true, | |
| 2590 | - | "os": [ | |
| 2591 | - | "android" | |
| 2592 | - | ], | |
| 2593 | - | "engines": { | |
| 2594 | - | "node": ">= 12.0.0" | |
| 2595 | - | }, | |
| 2596 | - | "funding": { | |
| 2597 | - | "type": "opencollective", | |
| 2598 | - | "url": "https://opencollective.com/parcel" | |
| 2599 | - | } | |
| 2600 | - | }, | |
| 2601 | - | "node_modules/lightningcss-darwin-arm64": { | |
| 2602 | - | "version": "1.32.0", | |
| 2603 | - | "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", | |
| 2604 | - | "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", | |
| 2605 | - | "cpu": [ | |
| 2606 | - | "arm64" | |
| 2607 | - | ], | |
| 2608 | - | "dev": true, | |
| 2609 | - | "license": "MPL-2.0", | |
| 2610 | - | "optional": true, | |
| 2611 | - | "os": [ | |
| 2612 | - | "darwin" | |
| 2613 | - | ], | |
| 2614 | - | "engines": { | |
| 2615 | - | "node": ">= 12.0.0" | |
| 2616 | - | }, | |
| 2617 | - | "funding": { | |
| 2618 | - | "type": "opencollective", | |
| 2619 | - | "url": "https://opencollective.com/parcel" | |
| 2620 | - | } | |
| 2621 | - | }, | |
| 2622 | - | "node_modules/lightningcss-darwin-x64": { | |
| 2623 | - | "version": "1.32.0", | |
| 2624 | - | "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", | |
| 2625 | - | "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", | |
| 2626 | - | "cpu": [ | |
| 2627 | - | "x64" | |
| 2628 | - | ], | |
| 2629 | - | "dev": true, | |
| 2630 | - | "license": "MPL-2.0", | |
| 2631 | - | "optional": true, | |
| 2632 | - | "os": [ | |
| 2633 | - | "darwin" | |
| 2634 | - | ], | |
| 2635 | - | "engines": { | |
| 2636 | - | "node": ">= 12.0.0" | |
| 2637 | - | }, | |
| 2638 | - | "funding": { | |
| 2639 | - | "type": "opencollective", | |
| 2640 | - | "url": "https://opencollective.com/parcel" | |
| 2641 | - | } | |
| 2642 | - | }, | |
| 2643 | - | "node_modules/lightningcss-freebsd-x64": { | |
| 2644 | - | "version": "1.32.0", | |
| 2645 | - | "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", | |
| 2646 | - | "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", | |
| 2647 | - | "cpu": [ | |
| 2648 | - | "x64" | |
| 2649 | - | ], | |
| 2650 | - | "dev": true, | |
| 2651 | - | "license": "MPL-2.0", | |
| 2652 | - | "optional": true, | |
| 2653 | - | "os": [ | |
| 2654 | - | "freebsd" | |
| 2655 | - | ], | |
| 2656 | - | "engines": { | |
| 2657 | - | "node": ">= 12.0.0" | |
| 2658 | - | }, | |
| 2659 | - | "funding": { | |
| 2660 | - | "type": "opencollective", | |
| 2661 | - | "url": "https://opencollective.com/parcel" | |
| 2662 | - | } | |
| 2663 | - | }, | |
| 2664 | - | "node_modules/lightningcss-linux-arm-gnueabihf": { | |
| 2665 | - | "version": "1.32.0", | |
| 2666 | - | "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", | |
| 2667 | - | "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", | |
| 2668 | - | "cpu": [ | |
| 2669 | - | "arm" | |
| 2670 | - | ], | |
| 2671 | - | "dev": true, | |
| 2672 | - | "license": "MPL-2.0", | |
| 2673 | - | "optional": true, | |
| 2674 | - | "os": [ | |
| 2675 | - | "linux" | |
| 2676 | - | ], | |
| 2677 | - | "engines": { | |
| 2678 | - | "node": ">= 12.0.0" | |
| 2679 | - | }, | |
| 2680 | - | "funding": { | |
| 2681 | - | "type": "opencollective", | |
| 2682 | - | "url": "https://opencollective.com/parcel" | |
| 2683 | - | } | |
| 2684 | - | }, | |
| 2685 | - | "node_modules/lightningcss-linux-arm64-gnu": { | |
| 2686 | - | "version": "1.32.0", | |
| 2687 | - | "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", | |
| 2688 | - | "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", | |
| 2689 | - | "cpu": [ | |
| 2690 | - | "arm64" | |
| 2691 | - | ], | |
| 2692 | - | "dev": true, | |
| 2693 | - | "license": "MPL-2.0", | |
| 2694 | - | "optional": true, | |
| 2695 | - | "os": [ | |
| 2696 | - | "linux" | |
| 2697 | - | ], | |
| 2698 | - | "engines": { | |
| 2699 | - | "node": ">= 12.0.0" | |
| 2700 | - | }, | |
| 2701 | - | "funding": { | |
| 2702 | - | "type": "opencollective", | |
| 2703 | - | "url": "https://opencollective.com/parcel" | |
| 2704 | - | } | |
| 2705 | - | }, | |
| 2706 | - | "node_modules/lightningcss-linux-arm64-musl": { | |
| 2707 | - | "version": "1.32.0", | |
| 2708 | - | "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", | |
| 2709 | - | "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", | |
| 2710 | - | "cpu": [ | |
| 2711 | - | "arm64" | |
| 2712 | - | ], | |
| 2713 | - | "dev": true, | |
| 2714 | - | "license": "MPL-2.0", | |
| 2715 | - | "optional": true, | |
| 2716 | - | "os": [ | |
| 2717 | - | "linux" | |
| 2718 | - | ], | |
| 2719 | - | "engines": { | |
| 2720 | - | "node": ">= 12.0.0" | |
| 2721 | - | }, | |
| 2722 | - | "funding": { | |
| 2723 | - | "type": "opencollective", | |
| 2724 | - | "url": "https://opencollective.com/parcel" | |
| 2725 | - | } | |
| 2726 | - | }, | |
| 2727 | - | "node_modules/lightningcss-linux-x64-gnu": { | |
| 2728 | - | "version": "1.32.0", | |
| 2729 | - | "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", | |
| 2730 | - | "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", | |
| 2731 | - | "cpu": [ | |
| 2732 | - | "x64" | |
| 2733 | - | ], | |
| 2734 | - | "dev": true, | |
| 2735 | - | "license": "MPL-2.0", | |
| 2736 | - | "optional": true, | |
| 2737 | - | "os": [ | |
| 2738 | - | "linux" | |
| 2739 | - | ], | |
| 2740 | - | "engines": { | |
| 2741 | - | "node": ">= 12.0.0" | |
| 2742 | - | }, | |
| 2743 | - | "funding": { | |
| 2744 | - | "type": "opencollective", | |
| 2745 | - | "url": "https://opencollective.com/parcel" | |
| 2746 | - | } | |
| 2747 | - | }, | |
| 2748 | - | "node_modules/lightningcss-linux-x64-musl": { | |
| 2749 | - | "version": "1.32.0", | |
| 2750 | - | "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", | |
| 2751 | - | "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", | |
| 2752 | - | "cpu": [ | |
| 2753 | - | "x64" | |
| 2754 | - | ], | |
| 2755 | - | "dev": true, | |
| 2756 | - | "license": "MPL-2.0", | |
| 2757 | - | "optional": true, | |
| 2758 | - | "os": [ | |
| 2759 | - | "linux" | |
| 2760 | - | ], | |
| 2761 | - | "engines": { | |
| 2762 | - | "node": ">= 12.0.0" | |
| 2763 | - | }, | |
| 2764 | - | "funding": { | |
| 2765 | - | "type": "opencollective", | |
| 2766 | - | "url": "https://opencollective.com/parcel" | |
| 2767 | - | } | |
| 2768 | - | }, | |
| 2769 | - | "node_modules/lightningcss-win32-arm64-msvc": { | |
| 2770 | - | "version": "1.32.0", | |
| 2771 | - | "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", | |
| 2772 | - | "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", | |
| 2773 | - | "cpu": [ | |
| 2774 | - | "arm64" | |
| 2775 | - | ], | |
| 2776 | - | "dev": true, | |
| 2777 | - | "license": "MPL-2.0", | |
| 2778 | - | "optional": true, | |
| 2779 | - | "os": [ | |
| 2780 | - | "win32" | |
| 2781 | - | ], | |
| 2782 | - | "engines": { | |
| 2783 | - | "node": ">= 12.0.0" | |
| 2784 | - | }, | |
| 2785 | - | "funding": { | |
| 2786 | - | "type": "opencollective", | |
| 2787 | - | "url": "https://opencollective.com/parcel" | |
| 2788 | - | } | |
| 2789 | - | }, | |
| 2790 | - | "node_modules/lightningcss-win32-x64-msvc": { | |
| 2791 | - | "version": "1.32.0", | |
| 2792 | - | "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", | |
| 2793 | - | "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", | |
| 2794 | - | "cpu": [ | |
| 2795 | - | "x64" | |
| 2796 | - | ], | |
| 2797 | - | "dev": true, | |
| 2798 | - | "license": "MPL-2.0", | |
| 2799 | - | "optional": true, | |
| 2800 | - | "os": [ | |
| 2801 | - | "win32" | |
| 2802 | - | ], | |
| 2803 | - | "engines": { | |
| 2804 | - | "node": ">= 12.0.0" | |
| 2805 | - | }, | |
| 2806 | - | "funding": { | |
| 2807 | - | "type": "opencollective", | |
| 2808 | - | "url": "https://opencollective.com/parcel" | |
| 2809 | - | } | |
| 2810 | - | }, | |
| 2811 | - | "node_modules/lilconfig": { | |
| 2812 | - | "version": "2.1.0", | |
| 2813 | - | "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", | |
| 2814 | - | "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", | |
| 2815 | - | "dev": true, | |
| 2816 | - | "license": "MIT", | |
| 2817 | - | "engines": { | |
| 2818 | - | "node": ">=10" | |
| 2819 | - | } | |
| 2820 | - | }, | |
| 2821 | - | "node_modules/locate-character": { | |
| 2822 | - | "version": "3.0.0", | |
| 2823 | - | "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", | |
| 2824 | - | "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", | |
| 2825 | - | "license": "MIT" | |
| 2826 | - | }, | |
| 2827 | - | "node_modules/locate-path": { | |
| 2828 | - | "version": "6.0.0", | |
| 2829 | - | "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", | |
| 2830 | - | "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", | |
| 2831 | - | "dev": true, | |
| 2832 | - | "license": "MIT", | |
| 2833 | - | "dependencies": { | |
| 2834 | - | "p-locate": "^5.0.0" | |
| 2835 | - | }, | |
| 2836 | - | "engines": { | |
| 2837 | - | "node": ">=10" | |
| 2838 | - | }, | |
| 2839 | - | "funding": { | |
| 2840 | - | "url": "https://github.com/sponsors/sindresorhus" | |
| 2841 | - | } | |
| 2842 | - | }, | |
| 2843 | - | "node_modules/lz-string": { | |
| 2844 | - | "version": "1.5.0", | |
| 2845 | - | "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", | |
| 2846 | - | "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", | |
| 2847 | - | "license": "MIT", | |
| 2848 | - | "bin": { | |
| 2849 | - | "lz-string": "bin/bin.js" | |
| 2850 | - | } | |
| 2851 | - | }, | |
| 2852 | - | "node_modules/magic-string": { | |
| 2853 | - | "version": "0.30.21", | |
| 2854 | - | "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", | |
| 2855 | - | "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", | |
| 2856 | - | "license": "MIT", | |
| 2857 | - | "dependencies": { | |
| 2858 | - | "@jridgewell/sourcemap-codec": "^1.5.5" | |
| 2859 | - | } | |
| 2860 | - | }, | |
| 2861 | - | "node_modules/minimatch": { | |
| 2862 | - | "version": "10.2.4", | |
| 2863 | - | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", | |
| 2864 | - | "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", | |
| 2865 | - | "dev": true, | |
| 2866 | - | "license": "BlueOak-1.0.0", | |
| 2867 | - | "dependencies": { | |
| 2868 | - | "brace-expansion": "^5.0.2" | |
| 2869 | - | }, | |
| 2870 | - | "engines": { | |
| 2871 | - | "node": "18 || 20 || >=22" | |
| 2872 | - | }, | |
| 2873 | - | "funding": { | |
| 2874 | - | "url": "https://github.com/sponsors/isaacs" | |
| 2875 | - | } | |
| 2876 | - | }, | |
| 2877 | - | "node_modules/mode-watcher": { | |
| 2878 | - | "version": "1.1.0", | |
| 2879 | - | "resolved": "https://registry.npmjs.org/mode-watcher/-/mode-watcher-1.1.0.tgz", | |
| 2880 | - | "integrity": "sha512-mUT9RRGPDYenk59qJauN1rhsIMKBmWA3xMF+uRwE8MW/tjhaDSCCARqkSuDTq8vr4/2KcAxIGVjACxTjdk5C3g==", | |
| 2881 | - | "license": "MIT", | |
| 2882 | - | "dependencies": { | |
| 2883 | - | "runed": "^0.25.0", | |
| 2884 | - | "svelte-toolbelt": "^0.7.1" | |
| 2885 | - | }, | |
| 2886 | - | "peerDependencies": { | |
| 2887 | - | "svelte": "^5.27.0" | |
| 2888 | - | } | |
| 2889 | - | }, | |
| 2890 | - | "node_modules/mode-watcher/node_modules/runed": { | |
| 2891 | - | "version": "0.25.0", | |
| 2892 | - | "resolved": "https://registry.npmjs.org/runed/-/runed-0.25.0.tgz", | |
| 2893 | - | "integrity": "sha512-7+ma4AG9FT2sWQEA0Egf6mb7PBT2vHyuHail1ie8ropfSjvZGtEAx8YTmUjv/APCsdRRxEVvArNjALk9zFSOrg==", | |
| 2894 | - | "funding": [ | |
| 2895 | - | "https://github.com/sponsors/huntabyte", | |
| 2896 | - | "https://github.com/sponsors/tglide" | |
| 2897 | - | ], | |
| 2898 | - | "dependencies": { | |
| 2899 | - | "esm-env": "^1.0.0" | |
| 2900 | - | }, | |
| 2901 | - | "peerDependencies": { | |
| 2902 | - | "svelte": "^5.7.0" | |
| 2903 | - | } | |
| 2904 | - | }, | |
| 2905 | - | "node_modules/mode-watcher/node_modules/svelte-toolbelt": { | |
| 2906 | - | "version": "0.7.1", | |
| 2907 | - | "resolved": "https://registry.npmjs.org/svelte-toolbelt/-/svelte-toolbelt-0.7.1.tgz", | |
| 2908 | - | "integrity": "sha512-HcBOcR17Vx9bjaOceUvxkY3nGmbBmCBBbuWLLEWO6jtmWH8f/QoWmbyUfQZrpDINH39en1b8mptfPQT9VKQ1xQ==", | |
| 2909 | - | "funding": [ | |
| 2910 | - | "https://github.com/sponsors/huntabyte" | |
| 2911 | - | ], | |
| 2912 | - | "dependencies": { | |
| 2913 | - | "clsx": "^2.1.1", | |
| 2914 | - | "runed": "^0.23.2", | |
| 2915 | - | "style-to-object": "^1.0.8" | |
| 2916 | - | }, | |
| 2917 | - | "engines": { | |
| 2918 | - | "node": ">=18", | |
| 2919 | - | "pnpm": ">=8.7.0" | |
| 2920 | - | }, | |
| 2921 | - | "peerDependencies": { | |
| 2922 | - | "svelte": "^5.0.0" | |
| 2923 | - | } | |
| 2924 | - | }, | |
| 2925 | - | "node_modules/mode-watcher/node_modules/svelte-toolbelt/node_modules/runed": { | |
| 2926 | - | "version": "0.23.4", | |
| 2927 | - | "resolved": "https://registry.npmjs.org/runed/-/runed-0.23.4.tgz", | |
| 2928 | - | "integrity": "sha512-9q8oUiBYeXIDLWNK5DfCWlkL0EW3oGbk845VdKlPeia28l751VpfesaB/+7pI6rnbx1I6rqoZ2fZxptOJLxILA==", | |
| 2929 | - | "funding": [ | |
| 2930 | - | "https://github.com/sponsors/huntabyte", | |
| 2931 | - | "https://github.com/sponsors/tglide" | |
| 2932 | - | ], | |
| 2933 | - | "dependencies": { | |
| 2934 | - | "esm-env": "^1.0.0" | |
| 2935 | - | }, | |
| 2936 | - | "peerDependencies": { | |
| 2937 | - | "svelte": "^5.7.0" | |
| 2938 | - | } | |
| 2939 | - | }, | |
| 2940 | - | "node_modules/mri": { | |
| 2941 | - | "version": "1.2.0", | |
| 2942 | - | "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", | |
| 2943 | - | "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", | |
| 2944 | - | "dev": true, | |
| 2945 | - | "license": "MIT", | |
| 2946 | - | "engines": { | |
| 2947 | - | "node": ">=4" | |
| 2948 | - | } | |
| 2949 | - | }, | |
| 2950 | - | "node_modules/ms": { | |
| 2951 | - | "version": "2.1.3", | |
| 2952 | - | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", | |
| 2953 | - | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", | |
| 2954 | - | "dev": true, | |
| 2955 | - | "license": "MIT" | |
| 2956 | - | }, | |
| 2957 | - | "node_modules/nanoid": { | |
| 2958 | - | "version": "3.3.11", | |
| 2959 | - | "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", | |
| 2960 | - | "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", | |
| 2961 | - | "dev": true, | |
| 2962 | - | "funding": [ | |
| 2963 | - | { | |
| 2964 | - | "type": "github", | |
| 2965 | - | "url": "https://github.com/sponsors/ai" | |
| 2966 | - | } | |
| 2967 | - | ], | |
| 2968 | - | "license": "MIT", | |
| 2969 | - | "bin": { | |
| 2970 | - | "nanoid": "bin/nanoid.cjs" | |
| 2971 | - | }, | |
| 2972 | - | "engines": { | |
| 2973 | - | "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" | |
| 2974 | - | } | |
| 2975 | - | }, | |
| 2976 | - | "node_modules/natural-compare": { | |
| 2977 | - | "version": "1.4.0", | |
| 2978 | - | "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", | |
| 2979 | - | "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", | |
| 2980 | - | "dev": true, | |
| 2981 | - | "license": "MIT" | |
| 2982 | - | }, | |
| 2983 | - | "node_modules/node-addon-api": { | |
| 2984 | - | "version": "7.1.1", | |
| 2985 | - | "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", | |
| 2986 | - | "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", | |
| 2987 | - | "dev": true, | |
| 2988 | - | "license": "MIT" | |
| 2989 | - | }, | |
| 2990 | - | "node_modules/node-releases": { | |
| 2991 | - | "version": "2.0.27", | |
| 2992 | - | "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", | |
| 2993 | - | "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", | |
| 2994 | - | "dev": true, | |
| 2995 | - | "license": "MIT" | |
| 2996 | - | }, | |
| 2997 | - | "node_modules/obug": { | |
| 2998 | - | "version": "2.1.1", | |
| 2999 | - | "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", | |
| 3000 | - | "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", | |
| 3001 | - | "dev": true, | |
| 3002 | - | "funding": [ | |
| 3003 | - | "https://github.com/sponsors/sxzz", | |
| 3004 | - | "https://opencollective.com/debug" | |
| 3005 | - | ], | |
| 3006 | - | "license": "MIT" | |
| 3007 | - | }, | |
| 3008 | - | "node_modules/optionator": { | |
| 3009 | - | "version": "0.9.4", | |
| 3010 | - | "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", | |
| 3011 | - | "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", | |
| 3012 | - | "dev": true, | |
| 3013 | - | "license": "MIT", | |
| 3014 | - | "dependencies": { | |
| 3015 | - | "deep-is": "^0.1.3", | |
| 3016 | - | "fast-levenshtein": "^2.0.6", | |
| 3017 | - | "levn": "^0.4.1", | |
| 3018 | - | "prelude-ls": "^1.2.1", | |
| 3019 | - | "type-check": "^0.4.0", | |
| 3020 | - | "word-wrap": "^1.2.5" | |
| 3021 | - | }, | |
| 3022 | - | "engines": { | |
| 3023 | - | "node": ">= 0.8.0" | |
| 3024 | - | } | |
| 3025 | - | }, | |
| 3026 | - | "node_modules/p-limit": { | |
| 3027 | - | "version": "3.1.0", | |
| 3028 | - | "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", | |
| 3029 | - | "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", | |
| 3030 | - | "dev": true, | |
| 3031 | - | "license": "MIT", | |
| 3032 | - | "dependencies": { | |
| 3033 | - | "yocto-queue": "^0.1.0" | |
| 3034 | - | }, | |
| 3035 | - | "engines": { | |
| 3036 | - | "node": ">=10" | |
| 3037 | - | }, | |
| 3038 | - | "funding": { | |
| 3039 | - | "url": "https://github.com/sponsors/sindresorhus" | |
| 3040 | - | } | |
| 3041 | - | }, | |
| 3042 | - | "node_modules/p-locate": { | |
| 3043 | - | "version": "5.0.0", | |
| 3044 | - | "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", | |
| 3045 | - | "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", | |
| 3046 | - | "dev": true, | |
| 3047 | - | "license": "MIT", | |
| 3048 | - | "dependencies": { | |
| 3049 | - | "p-limit": "^3.0.2" | |
| 3050 | - | }, | |
| 3051 | - | "engines": { | |
| 3052 | - | "node": ">=10" | |
| 3053 | - | }, | |
| 3054 | - | "funding": { | |
| 3055 | - | "url": "https://github.com/sponsors/sindresorhus" | |
| 3056 | - | } | |
| 3057 | - | }, | |
| 3058 | - | "node_modules/path-exists": { | |
| 3059 | - | "version": "4.0.0", | |
| 3060 | - | "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", | |
| 3061 | - | "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", | |
| 3062 | - | "dev": true, | |
| 3063 | - | "license": "MIT", | |
| 3064 | - | "engines": { | |
| 3065 | - | "node": ">=8" | |
| 3066 | - | } | |
| 3067 | - | }, | |
| 3068 | - | "node_modules/path-key": { | |
| 3069 | - | "version": "3.1.1", | |
| 3070 | - | "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", | |
| 3071 | - | "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", | |
| 3072 | - | "dev": true, | |
| 3073 | - | "license": "MIT", | |
| 3074 | - | "engines": { | |
| 3075 | - | "node": ">=8" | |
| 3076 | - | } | |
| 3077 | - | }, | |
| 3078 | - | "node_modules/picocolors": { | |
| 3079 | - | "version": "1.1.1", | |
| 3080 | - | "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", | |
| 3081 | - | "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", | |
| 3082 | - | "dev": true, | |
| 3083 | - | "license": "ISC" | |
| 3084 | - | }, | |
| 3085 | - | "node_modules/picomatch": { | |
| 3086 | - | "version": "4.0.3", | |
| 3087 | - | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", | |
| 3088 | - | "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", | |
| 3089 | - | "dev": true, | |
| 3090 | - | "license": "MIT", | |
| 3091 | - | "engines": { | |
| 3092 | - | "node": ">=12" | |
| 3093 | - | }, | |
| 3094 | - | "funding": { | |
| 3095 | - | "url": "https://github.com/sponsors/jonschlinkert" | |
| 3096 | - | } | |
| 3097 | - | }, | |
| 3098 | - | "node_modules/postcss": { | |
| 3099 | - | "version": "8.5.8", | |
| 3100 | - | "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", | |
| 3101 | - | "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", | |
| 3102 | - | "dev": true, | |
| 3103 | - | "funding": [ | |
| 3104 | - | { | |
| 3105 | - | "type": "opencollective", | |
| 3106 | - | "url": "https://opencollective.com/postcss/" | |
| 3107 | - | }, | |
| 3108 | - | { | |
| 3109 | - | "type": "tidelift", | |
| 3110 | - | "url": "https://tidelift.com/funding/github/npm/postcss" | |
| 3111 | - | }, | |
| 3112 | - | { | |
| 3113 | - | "type": "github", | |
| 3114 | - | "url": "https://github.com/sponsors/ai" | |
| 3115 | - | } | |
| 3116 | - | ], | |
| 3117 | - | "license": "MIT", | |
| 3118 | - | "peer": true, | |
| 3119 | - | "dependencies": { | |
| 3120 | - | "nanoid": "^3.3.11", | |
| 3121 | - | "picocolors": "^1.1.1", | |
| 3122 | - | "source-map-js": "^1.2.1" | |
| 3123 | - | }, | |
| 3124 | - | "engines": { | |
| 3125 | - | "node": "^10 || ^12 || >=14" | |
| 3126 | - | } | |
| 3127 | - | }, | |
| 3128 | - | "node_modules/postcss-load-config": { | |
| 3129 | - | "version": "3.1.4", | |
| 3130 | - | "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", | |
| 3131 | - | "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", | |
| 3132 | - | "dev": true, | |
| 3133 | - | "license": "MIT", | |
| 3134 | - | "dependencies": { | |
| 3135 | - | "lilconfig": "^2.0.5", | |
| 3136 | - | "yaml": "^1.10.2" | |
| 3137 | - | }, | |
| 3138 | - | "engines": { | |
| 3139 | - | "node": ">= 10" | |
| 3140 | - | }, | |
| 3141 | - | "funding": { | |
| 3142 | - | "type": "opencollective", | |
| 3143 | - | "url": "https://opencollective.com/postcss/" | |
| 3144 | - | }, | |
| 3145 | - | "peerDependencies": { | |
| 3146 | - | "postcss": ">=8.0.9", | |
| 3147 | - | "ts-node": ">=9.0.0" | |
| 3148 | - | }, | |
| 3149 | - | "peerDependenciesMeta": { | |
| 3150 | - | "postcss": { | |
| 3151 | - | "optional": true | |
| 3152 | - | }, | |
| 3153 | - | "ts-node": { | |
| 3154 | - | "optional": true | |
| 3155 | - | } | |
| 3156 | - | } | |
| 3157 | - | }, | |
| 3158 | - | "node_modules/postcss-load-config/node_modules/yaml": { | |
| 3159 | - | "version": "1.10.2", | |
| 3160 | - | "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", | |
| 3161 | - | "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", | |
| 3162 | - | "dev": true, | |
| 3163 | - | "license": "ISC", | |
| 3164 | - | "engines": { | |
| 3165 | - | "node": ">= 6" | |
| 3166 | - | } | |
| 3167 | - | }, | |
| 3168 | - | "node_modules/postcss-safe-parser": { | |
| 3169 | - | "version": "7.0.1", | |
| 3170 | - | "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-7.0.1.tgz", | |
| 3171 | - | "integrity": "sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==", | |
| 3172 | - | "dev": true, | |
| 3173 | - | "funding": [ | |
| 3174 | - | { | |
| 3175 | - | "type": "opencollective", | |
| 3176 | - | "url": "https://opencollective.com/postcss/" | |
| 3177 | - | }, | |
| 3178 | - | { | |
| 3179 | - | "type": "tidelift", | |
| 3180 | - | "url": "https://tidelift.com/funding/github/npm/postcss-safe-parser" | |
| 3181 | - | }, | |
| 3182 | - | { | |
| 3183 | - | "type": "github", | |
| 3184 | - | "url": "https://github.com/sponsors/ai" | |
| 3185 | - | } | |
| 3186 | - | ], | |
| 3187 | - | "license": "MIT", | |
| 3188 | - | "engines": { | |
| 3189 | - | "node": ">=18.0" | |
| 3190 | - | }, | |
| 3191 | - | "peerDependencies": { | |
| 3192 | - | "postcss": "^8.4.31" | |
| 3193 | - | } | |
| 3194 | - | }, | |
| 3195 | - | "node_modules/postcss-scss": { | |
| 3196 | - | "version": "4.0.9", | |
| 3197 | - | "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.9.tgz", | |
| 3198 | - | "integrity": "sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==", | |
| 3199 | - | "dev": true, | |
| 3200 | - | "funding": [ | |
| 3201 | - | { | |
| 3202 | - | "type": "opencollective", | |
| 3203 | - | "url": "https://opencollective.com/postcss/" | |
| 3204 | - | }, | |
| 3205 | - | { | |
| 3206 | - | "type": "tidelift", | |
| 3207 | - | "url": "https://tidelift.com/funding/github/npm/postcss-scss" | |
| 3208 | - | }, | |
| 3209 | - | { | |
| 3210 | - | "type": "github", | |
| 3211 | - | "url": "https://github.com/sponsors/ai" | |
| 3212 | - | } | |
| 3213 | - | ], | |
| 3214 | - | "license": "MIT", | |
| 3215 | - | "engines": { | |
| 3216 | - | "node": ">=12.0" | |
| 3217 | - | }, | |
| 3218 | - | "peerDependencies": { | |
| 3219 | - | "postcss": "^8.4.29" | |
| 3220 | - | } | |
| 3221 | - | }, | |
| 3222 | - | "node_modules/postcss-selector-parser": { | |
| 3223 | - | "version": "7.1.1", | |
| 3224 | - | "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", | |
| 3225 | - | "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", | |
| 3226 | - | "dev": true, | |
| 3227 | - | "license": "MIT", | |
| 3228 | - | "dependencies": { | |
| 3229 | - | "cssesc": "^3.0.0", | |
| 3230 | - | "util-deprecate": "^1.0.2" | |
| 3231 | - | }, | |
| 3232 | - | "engines": { | |
| 3233 | - | "node": ">=4" | |
| 3234 | - | } | |
| 3235 | - | }, | |
| 3236 | - | "node_modules/postcss-value-parser": { | |
| 3237 | - | "version": "4.2.0", | |
| 3238 | - | "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", | |
| 3239 | - | "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", | |
| 3240 | - | "dev": true, | |
| 3241 | - | "license": "MIT" | |
| 3242 | - | }, | |
| 3243 | - | "node_modules/prelude-ls": { | |
| 3244 | - | "version": "1.2.1", | |
| 3245 | - | "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", | |
| 3246 | - | "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", | |
| 3247 | - | "dev": true, | |
| 3248 | - | "license": "MIT", | |
| 3249 | - | "engines": { | |
| 3250 | - | "node": ">= 0.8.0" | |
| 3251 | - | } | |
| 3252 | - | }, | |
| 3253 | - | "node_modules/punycode": { | |
| 3254 | - | "version": "2.3.1", | |
| 3255 | - | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", | |
| 3256 | - | "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", | |
| 3257 | - | "dev": true, | |
| 3258 | - | "license": "MIT", | |
| 3259 | - | "engines": { | |
| 3260 | - | "node": ">=6" | |
| 3261 | - | } | |
| 3262 | - | }, | |
| 3263 | - | "node_modules/readdirp": { | |
| 3264 | - | "version": "4.1.2", | |
| 3265 | - | "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", | |
| 3266 | - | "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", | |
| 3267 | - | "dev": true, | |
| 3268 | - | "license": "MIT", | |
| 3269 | - | "engines": { | |
| 3270 | - | "node": ">= 14.18.0" | |
| 3271 | - | }, | |
| 3272 | - | "funding": { | |
| 3273 | - | "type": "individual", | |
| 3274 | - | "url": "https://paulmillr.com/funding/" | |
| 3275 | - | } | |
| 3276 | - | }, | |
| 3277 | - | "node_modules/rolldown": { | |
| 3278 | - | "version": "1.0.0-rc.9", | |
| 3279 | - | "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.9.tgz", | |
| 3280 | - | "integrity": "sha512-9EbgWge7ZH+yqb4d2EnELAntgPTWbfL8ajiTW+SyhJEC4qhBbkCKbqFV4Ge4zmu5ziQuVbWxb/XwLZ+RIO7E8Q==", | |
| 3281 | - | "dev": true, | |
| 3282 | - | "license": "MIT", | |
| 3283 | - | "dependencies": { | |
| 3284 | - | "@oxc-project/types": "=0.115.0", | |
| 3285 | - | "@rolldown/pluginutils": "1.0.0-rc.9" | |
| 3286 | - | }, | |
| 3287 | - | "bin": { | |
| 3288 | - | "rolldown": "bin/cli.mjs" | |
| 3289 | - | }, | |
| 3290 | - | "engines": { | |
| 3291 | - | "node": "^20.19.0 || >=22.12.0" | |
| 3292 | - | }, | |
| 3293 | - | "optionalDependencies": { | |
| 3294 | - | "@rolldown/binding-android-arm64": "1.0.0-rc.9", | |
| 3295 | - | "@rolldown/binding-darwin-arm64": "1.0.0-rc.9", | |
| 3296 | - | "@rolldown/binding-darwin-x64": "1.0.0-rc.9", | |
| 3297 | - | "@rolldown/binding-freebsd-x64": "1.0.0-rc.9", | |
| 3298 | - | "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.9", | |
| 3299 | - | "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.9", | |
| 3300 | - | "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.9", | |
| 3301 | - | "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.9", | |
| 3302 | - | "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.9", | |
| 3303 | - | "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.9", | |
| 3304 | - | "@rolldown/binding-linux-x64-musl": "1.0.0-rc.9", | |
| 3305 | - | "@rolldown/binding-openharmony-arm64": "1.0.0-rc.9", | |
| 3306 | - | "@rolldown/binding-wasm32-wasi": "1.0.0-rc.9", | |
| 3307 | - | "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.9", | |
| 3308 | - | "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.9" | |
| 3309 | - | } | |
| 3310 | - | }, | |
| 3311 | - | "node_modules/runed": { | |
| 3312 | - | "version": "0.35.1", | |
| 3313 | - | "resolved": "https://registry.npmjs.org/runed/-/runed-0.35.1.tgz", | |
| 3314 | - | "integrity": "sha512-2F4Q/FZzbeJTFdIS/PuOoPRSm92sA2LhzTnv6FXhCoENb3huf5+fDuNOg1LNvGOouy3u/225qxmuJvcV3IZK5Q==", | |
| 3315 | - | "funding": [ | |
| 3316 | - | "https://github.com/sponsors/huntabyte", | |
| 3317 | - | "https://github.com/sponsors/tglide" | |
| 3318 | - | ], | |
| 3319 | - | "license": "MIT", | |
| 3320 | - | "dependencies": { | |
| 3321 | - | "dequal": "^2.0.3", | |
| 3322 | - | "esm-env": "^1.0.0", | |
| 3323 | - | "lz-string": "^1.5.0" | |
| 3324 | - | }, | |
| 3325 | - | "peerDependencies": { | |
| 3326 | - | "@sveltejs/kit": "^2.21.0", | |
| 3327 | - | "svelte": "^5.7.0" | |
| 3328 | - | }, | |
| 3329 | - | "peerDependenciesMeta": { | |
| 3330 | - | "@sveltejs/kit": { | |
| 3331 | - | "optional": true | |
| 3332 | - | } | |
| 3333 | - | } | |
| 3334 | - | }, | |
| 3335 | - | "node_modules/sade": { | |
| 3336 | - | "version": "1.8.1", | |
| 3337 | - | "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", | |
| 3338 | - | "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", | |
| 3339 | - | "dev": true, | |
| 3340 | - | "license": "MIT", | |
| 3341 | - | "dependencies": { | |
| 3342 | - | "mri": "^1.1.0" | |
| 3343 | - | }, | |
| 3344 | - | "engines": { | |
| 3345 | - | "node": ">=6" | |
| 3346 | - | } | |
| 3347 | - | }, | |
| 3348 | - | "node_modules/semver": { | |
| 3349 | - | "version": "7.7.3", | |
| 3350 | - | "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", | |
| 3351 | - | "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", | |
| 3352 | - | "dev": true, | |
| 3353 | - | "license": "ISC", | |
| 3354 | - | "bin": { | |
| 3355 | - | "semver": "bin/semver.js" | |
| 3356 | - | }, | |
| 3357 | - | "engines": { | |
| 3358 | - | "node": ">=10" | |
| 3359 | - | } | |
| 3360 | - | }, | |
| 3361 | - | "node_modules/shebang-command": { | |
| 3362 | - | "version": "2.0.0", | |
| 3363 | - | "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", | |
| 3364 | - | "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", | |
| 3365 | - | "dev": true, | |
| 3366 | - | "license": "MIT", | |
| 3367 | - | "dependencies": { | |
| 3368 | - | "shebang-regex": "^3.0.0" | |
| 3369 | - | }, | |
| 3370 | - | "engines": { | |
| 3371 | - | "node": ">=8" | |
| 3372 | - | } | |
| 3373 | - | }, | |
| 3374 | - | "node_modules/shebang-regex": { | |
| 3375 | - | "version": "3.0.0", | |
| 3376 | - | "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", | |
| 3377 | - | "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", | |
| 3378 | - | "dev": true, | |
| 3379 | - | "license": "MIT", | |
| 3380 | - | "engines": { | |
| 3381 | - | "node": ">=8" | |
| 3382 | - | } | |
| 3383 | - | }, | |
| 3384 | - | "node_modules/source-map-js": { | |
| 3385 | - | "version": "1.2.1", | |
| 3386 | - | "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", | |
| 3387 | - | "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", | |
| 3388 | - | "dev": true, | |
| 3389 | - | "license": "BSD-3-Clause", | |
| 3390 | - | "engines": { | |
| 3391 | - | "node": ">=0.10.0" | |
| 3392 | - | } | |
| 3393 | - | }, | |
| 3394 | - | "node_modules/style-to-object": { | |
| 3395 | - | "version": "1.0.14", | |
| 3396 | - | "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", | |
| 3397 | - | "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", | |
| 3398 | - | "license": "MIT", | |
| 3399 | - | "dependencies": { | |
| 3400 | - | "inline-style-parser": "0.2.7" | |
| 3401 | - | } | |
| 3402 | - | }, | |
| 3403 | - | "node_modules/svelte": { | |
| 3404 | - | "version": "5.54.0", | |
| 3405 | - | "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.54.0.tgz", | |
| 3406 | - | "integrity": "sha512-TTDxwYnHkova6Wsyj1PGt9TByuWqvMoeY1bQiuAf2DM/JeDSMw7FjRKzk8K/5mJ99vGOKhbCqTDpyAKwjp4igg==", | |
| 3407 | - | "license": "MIT", | |
| 3408 | - | "peer": true, | |
| 3409 | - | "dependencies": { | |
| 3410 | - | "@jridgewell/remapping": "^2.3.4", | |
| 3411 | - | "@jridgewell/sourcemap-codec": "^1.5.0", | |
| 3412 | - | "@sveltejs/acorn-typescript": "^1.0.5", | |
| 3413 | - | "@types/estree": "^1.0.5", | |
| 3414 | - | "@types/trusted-types": "^2.0.7", | |
| 3415 | - | "acorn": "^8.12.1", | |
| 3416 | - | "aria-query": "5.3.1", | |
| 3417 | - | "axobject-query": "^4.1.0", | |
| 3418 | - | "clsx": "^2.1.1", | |
| 3419 | - | "devalue": "^5.6.4", | |
| 3420 | - | "esm-env": "^1.2.1", | |
| 3421 | - | "esrap": "^2.2.2", | |
| 3422 | - | "is-reference": "^3.0.3", | |
| 3423 | - | "locate-character": "^3.0.0", | |
| 3424 | - | "magic-string": "^0.30.11", | |
| 3425 | - | "zimmerframe": "^1.1.2" | |
| 3426 | - | }, | |
| 3427 | - | "engines": { | |
| 3428 | - | "node": ">=18" | |
| 3429 | - | } | |
| 3430 | - | }, | |
| 3431 | - | "node_modules/svelte-awesome-color-picker": { | |
| 3432 | - | "version": "4.1.1", | |
| 3433 | - | "resolved": "https://registry.npmjs.org/svelte-awesome-color-picker/-/svelte-awesome-color-picker-4.1.1.tgz", | |
| 3434 | - | "integrity": "sha512-k4APFGBFN8EpvUS0UJfdlqj8xDekMbqGBwnDYBK1QCY8kcTXSO8c7SitEk70MkiYOIByOgXlHcjaipy6H7dpAw==", | |
| 3435 | - | "dev": true, | |
| 3436 | - | "license": "MIT", | |
| 3437 | - | "dependencies": { | |
| 3438 | - | "colord": "^2.9.3", | |
| 3439 | - | "svelte-awesome-slider": "2.0.0" | |
| 3440 | - | }, | |
| 3441 | - | "engines": { | |
| 3442 | - | "node": ">=24" | |
| 3443 | - | }, | |
| 3444 | - | "peerDependencies": { | |
| 3445 | - | "svelte": "^5.0.0" | |
| 3446 | - | } | |
| 3447 | - | }, | |
| 3448 | - | "node_modules/svelte-awesome-slider": { | |
| 3449 | - | "version": "2.0.0", | |
| 3450 | - | "resolved": "https://registry.npmjs.org/svelte-awesome-slider/-/svelte-awesome-slider-2.0.0.tgz", | |
| 3451 | - | "integrity": "sha512-YBkOdYm1Feaqsn2JkJBRs+Kc/X3Qy/3GuVmI7GmoYDjBaHkjx9uH4khTuED22z57Hg3gGWeDhp/clIjWDdLNaw==", | |
| 3452 | - | "dev": true, | |
| 3453 | - | "license": "MIT", | |
| 3454 | - | "peerDependencies": { | |
| 3455 | - | "svelte": "^5.0.0" | |
| 3456 | - | } | |
| 3457 | - | }, | |
| 3458 | - | "node_modules/svelte-check": { | |
| 3459 | - | "version": "4.4.5", | |
| 3460 | - | "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.4.5.tgz", | |
| 3461 | - | "integrity": "sha512-1bSwIRCvvmSHrlK52fOlZmVtUZgil43jNL/2H18pRpa+eQjzGt6e3zayxhp1S7GajPFKNM/2PMCG+DZFHlG9fw==", | |
| 3462 | - | "dev": true, | |
| 3463 | - | "license": "MIT", | |
| 3464 | - | "dependencies": { | |
| 3465 | - | "@jridgewell/trace-mapping": "^0.3.25", | |
| 3466 | - | "chokidar": "^4.0.1", | |
| 3467 | - | "fdir": "^6.2.0", | |
| 3468 | - | "picocolors": "^1.0.0", | |
| 3469 | - | "sade": "^1.7.4" | |
| 3470 | - | }, | |
| 3471 | - | "bin": { | |
| 3472 | - | "svelte-check": "bin/svelte-check" | |
| 3473 | - | }, | |
| 3474 | - | "engines": { | |
| 3475 | - | "node": ">= 18.0.0" | |
| 3476 | - | }, | |
| 3477 | - | "peerDependencies": { | |
| 3478 | - | "svelte": "^4.0.0 || ^5.0.0-next.0", | |
| 3479 | - | "typescript": ">=5.0.0" | |
| 3480 | - | } | |
| 3481 | - | }, | |
| 3482 | - | "node_modules/svelte-eslint-parser": { | |
| 3483 | - | "version": "1.4.1", | |
| 3484 | - | "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-1.4.1.tgz", | |
| 3485 | - | "integrity": "sha512-1eqkfQ93goAhjAXxZiu1SaKI9+0/sxp4JIWQwUpsz7ybehRE5L8dNuz7Iry7K22R47p5/+s9EM+38nHV2OlgXA==", | |
| 3486 | - | "dev": true, | |
| 3487 | - | "license": "MIT", | |
| 3488 | - | "dependencies": { | |
| 3489 | - | "eslint-scope": "^8.2.0", | |
| 3490 | - | "eslint-visitor-keys": "^4.0.0", | |
| 3491 | - | "espree": "^10.0.0", | |
| 3492 | - | "postcss": "^8.4.49", | |
| 3493 | - | "postcss-scss": "^4.0.9", | |
| 3494 | - | "postcss-selector-parser": "^7.0.0" | |
| 3495 | - | }, | |
| 3496 | - | "engines": { | |
| 3497 | - | "node": "^18.18.0 || ^20.9.0 || >=21.1.0", | |
| 3498 | - | "pnpm": "10.24.0" | |
| 3499 | - | }, | |
| 3500 | - | "funding": { | |
| 3501 | - | "url": "https://github.com/sponsors/ota-meshi" | |
| 3502 | - | }, | |
| 3503 | - | "peerDependencies": { | |
| 3504 | - | "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" | |
| 3505 | - | }, | |
| 3506 | - | "peerDependenciesMeta": { | |
| 3507 | - | "svelte": { | |
| 3508 | - | "optional": true | |
| 3509 | - | } | |
| 3510 | - | } | |
| 3511 | - | }, | |
| 3512 | - | "node_modules/svelte-eslint-parser/node_modules/eslint-visitor-keys": { | |
| 3513 | - | "version": "4.2.1", | |
| 3514 | - | "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", | |
| 3515 | - | "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", | |
| 3516 | - | "dev": true, | |
| 3517 | - | "license": "Apache-2.0", | |
| 3518 | - | "engines": { | |
| 3519 | - | "node": "^18.18.0 || ^20.9.0 || >=21.1.0" | |
| 3520 | - | }, | |
| 3521 | - | "funding": { | |
| 3522 | - | "url": "https://opencollective.com/eslint" | |
| 3523 | - | } | |
| 3524 | - | }, | |
| 3525 | - | "node_modules/svelte-toolbelt": { | |
| 3526 | - | "version": "0.10.6", | |
| 3527 | - | "resolved": "https://registry.npmjs.org/svelte-toolbelt/-/svelte-toolbelt-0.10.6.tgz", | |
| 3528 | - | "integrity": "sha512-YWuX+RE+CnWYx09yseAe4ZVMM7e7GRFZM6OYWpBKOb++s+SQ8RBIMMe+Bs/CznBMc0QPLjr+vDBxTAkozXsFXQ==", | |
| 3529 | - | "funding": [ | |
| 3530 | - | "https://github.com/sponsors/huntabyte" | |
| 3531 | - | ], | |
| 3532 | - | "dependencies": { | |
| 3533 | - | "clsx": "^2.1.1", | |
| 3534 | - | "runed": "^0.35.1", | |
| 3535 | - | "style-to-object": "^1.0.8" | |
| 3536 | - | }, | |
| 3537 | - | "engines": { | |
| 3538 | - | "node": ">=18", | |
| 3539 | - | "pnpm": ">=8.7.0" | |
| 3540 | - | }, | |
| 3541 | - | "peerDependencies": { | |
| 3542 | - | "svelte": "^5.30.2" | |
| 3543 | - | } | |
| 3544 | - | }, | |
| 3545 | - | "node_modules/tabbable": { | |
| 3546 | - | "version": "6.4.0", | |
| 3547 | - | "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz", | |
| 3548 | - | "integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==", | |
| 3549 | - | "license": "MIT" | |
| 3550 | - | }, | |
| 3551 | - | "node_modules/tailwind-merge": { | |
| 3552 | - | "version": "3.5.0", | |
| 3553 | - | "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz", | |
| 3554 | - | "integrity": "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==", | |
| 3555 | - | "license": "MIT", | |
| 3556 | - | "peer": true, | |
| 3557 | - | "funding": { | |
| 3558 | - | "type": "github", | |
| 3559 | - | "url": "https://github.com/sponsors/dcastil" | |
| 3560 | - | } | |
| 3561 | - | }, | |
| 3562 | - | "node_modules/tailwind-variants": { | |
| 3563 | - | "version": "3.2.2", | |
| 3564 | - | "resolved": "https://registry.npmjs.org/tailwind-variants/-/tailwind-variants-3.2.2.tgz", | |
| 3565 | - | "integrity": "sha512-Mi4kHeMTLvKlM98XPnK+7HoBPmf4gygdFmqQPaDivc3DpYS6aIY6KiG/PgThrGvii5YZJqRsPz0aPyhoFzmZgg==", | |
| 3566 | - | "dev": true, | |
| 3567 | - | "license": "MIT", | |
| 3568 | - | "engines": { | |
| 3569 | - | "node": ">=16.x", | |
| 3570 | - | "pnpm": ">=7.x" | |
| 3571 | - | }, | |
| 3572 | - | "peerDependencies": { | |
| 3573 | - | "tailwind-merge": ">=3.0.0", | |
| 3574 | - | "tailwindcss": "*" | |
| 3575 | - | }, | |
| 3576 | - | "peerDependenciesMeta": { | |
| 3577 | - | "tailwind-merge": { | |
| 3578 | - | "optional": true | |
| 3579 | - | } | |
| 3580 | - | } | |
| 3581 | - | }, | |
| 3582 | - | "node_modules/tailwindcss": { | |
| 3583 | - | "version": "4.2.2", | |
| 3584 | - | "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz", | |
| 3585 | - | "integrity": "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==", | |
| 3586 | - | "dev": true, | |
| 3587 | - | "license": "MIT", | |
| 3588 | - | "peer": true | |
| 3589 | - | }, | |
| 3590 | - | "node_modules/tapable": { | |
| 3591 | - | "version": "2.3.0", | |
| 3592 | - | "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", | |
| 3593 | - | "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", | |
| 3594 | - | "dev": true, | |
| 3595 | - | "license": "MIT", | |
| 3596 | - | "engines": { | |
| 3597 | - | "node": ">=6" | |
| 3598 | - | }, | |
| 3599 | - | "funding": { | |
| 3600 | - | "type": "opencollective", | |
| 3601 | - | "url": "https://opencollective.com/webpack" | |
| 3602 | - | } | |
| 3603 | - | }, | |
| 3604 | - | "node_modules/tinyglobby": { | |
| 3605 | - | "version": "0.2.15", | |
| 3606 | - | "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", | |
| 3607 | - | "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", | |
| 3608 | - | "dev": true, | |
| 3609 | - | "license": "MIT", | |
| 3610 | - | "dependencies": { | |
| 3611 | - | "fdir": "^6.5.0", | |
| 3612 | - | "picomatch": "^4.0.3" | |
| 3613 | - | }, | |
| 3614 | - | "engines": { | |
| 3615 | - | "node": ">=12.0.0" | |
| 3616 | - | }, | |
| 3617 | - | "funding": { | |
| 3618 | - | "url": "https://github.com/sponsors/SuperchupuDev" | |
| 3619 | - | } | |
| 3620 | - | }, | |
| 3621 | - | "node_modules/ts-api-utils": { | |
| 3622 | - | "version": "2.4.0", | |
| 3623 | - | "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", | |
| 3624 | - | "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", | |
| 3625 | - | "dev": true, | |
| 3626 | - | "license": "MIT", | |
| 3627 | - | "engines": { | |
| 3628 | - | "node": ">=18.12" | |
| 3629 | - | }, | |
| 3630 | - | "peerDependencies": { | |
| 3631 | - | "typescript": ">=4.8.4" | |
| 3632 | - | } | |
| 3633 | - | }, | |
| 3634 | - | "node_modules/tslib": { | |
| 3635 | - | "version": "2.8.1", | |
| 3636 | - | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", | |
| 3637 | - | "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", | |
| 3638 | - | "license": "0BSD" | |
| 3639 | - | }, | |
| 3640 | - | "node_modules/tw-animate-css": { | |
| 3641 | - | "version": "1.4.0", | |
| 3642 | - | "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.4.0.tgz", | |
| 3643 | - | "integrity": "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==", | |
| 3644 | - | "dev": true, | |
| 3645 | - | "license": "MIT", | |
| 3646 | - | "funding": { | |
| 3647 | - | "url": "https://github.com/sponsors/Wombosvideo" | |
| 3648 | - | } | |
| 3649 | - | }, | |
| 3650 | - | "node_modules/type-check": { | |
| 3651 | - | "version": "0.4.0", | |
| 3652 | - | "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", | |
| 3653 | - | "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", | |
| 3654 | - | "dev": true, | |
| 3655 | - | "license": "MIT", | |
| 3656 | - | "dependencies": { | |
| 3657 | - | "prelude-ls": "^1.2.1" | |
| 3658 | - | }, | |
| 3659 | - | "engines": { | |
| 3660 | - | "node": ">= 0.8.0" | |
| 3661 | - | } | |
| 3662 | - | }, | |
| 3663 | - | "node_modules/typescript": { | |
| 3664 | - | "version": "5.9.3", | |
| 3665 | - | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", | |
| 3666 | - | "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", | |
| 3667 | - | "dev": true, | |
| 3668 | - | "license": "Apache-2.0", | |
| 3669 | - | "peer": true, | |
| 3670 | - | "bin": { | |
| 3671 | - | "tsc": "bin/tsc", | |
| 3672 | - | "tsserver": "bin/tsserver" | |
| 3673 | - | }, | |
| 3674 | - | "engines": { | |
| 3675 | - | "node": ">=14.17" | |
| 3676 | - | } | |
| 3677 | - | }, | |
| 3678 | - | "node_modules/typescript-eslint": { | |
| 3679 | - | "version": "8.57.1", | |
| 3680 | - | "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.57.1.tgz", | |
| 3681 | - | "integrity": "sha512-fLvZWf+cAGw3tqMCYzGIU6yR8K+Y9NT2z23RwOjlNFF2HwSB3KhdEFI5lSBv8tNmFkkBShSjsCjzx1vahZfISA==", | |
| 3682 | - | "dev": true, | |
| 3683 | - | "license": "MIT", | |
| 3684 | - | "dependencies": { | |
| 3685 | - | "@typescript-eslint/eslint-plugin": "8.57.1", | |
| 3686 | - | "@typescript-eslint/parser": "8.57.1", | |
| 3687 | - | "@typescript-eslint/typescript-estree": "8.57.1", | |
| 3688 | - | "@typescript-eslint/utils": "8.57.1" | |
| 3689 | - | }, | |
| 3690 | - | "engines": { | |
| 3691 | - | "node": "^18.18.0 || ^20.9.0 || >=21.1.0" | |
| 3692 | - | }, | |
| 3693 | - | "funding": { | |
| 3694 | - | "type": "opencollective", | |
| 3695 | - | "url": "https://opencollective.com/typescript-eslint" | |
| 3696 | - | }, | |
| 3697 | - | "peerDependencies": { | |
| 3698 | - | "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", | |
| 3699 | - | "typescript": ">=4.8.4 <6.0.0" | |
| 3700 | - | } | |
| 3701 | - | }, | |
| 3702 | - | "node_modules/undici-types": { | |
| 3703 | - | "version": "7.18.2", | |
| 3704 | - | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", | |
| 3705 | - | "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", | |
| 3706 | - | "dev": true, | |
| 3707 | - | "license": "MIT" | |
| 3708 | - | }, | |
| 3709 | - | "node_modules/update-browserslist-db": { | |
| 3710 | - | "version": "1.2.3", | |
| 3711 | - | "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", | |
| 3712 | - | "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", | |
| 3713 | - | "dev": true, | |
| 3714 | - | "funding": [ | |
| 3715 | - | { | |
| 3716 | - | "type": "opencollective", | |
| 3717 | - | "url": "https://opencollective.com/browserslist" | |
| 3718 | - | }, | |
| 3719 | - | { | |
| 3720 | - | "type": "tidelift", | |
| 3721 | - | "url": "https://tidelift.com/funding/github/npm/browserslist" | |
| 3722 | - | }, | |
| 3723 | - | { | |
| 3724 | - | "type": "github", | |
| 3725 | - | "url": "https://github.com/sponsors/ai" | |
| 3726 | - | } | |
| 3727 | - | ], | |
| 3728 | - | "license": "MIT", | |
| 3729 | - | "dependencies": { | |
| 3730 | - | "escalade": "^3.2.0", | |
| 3731 | - | "picocolors": "^1.1.1" | |
| 3732 | - | }, | |
| 3733 | - | "bin": { | |
| 3734 | - | "update-browserslist-db": "cli.js" | |
| 3735 | - | }, | |
| 3736 | - | "peerDependencies": { | |
| 3737 | - | "browserslist": ">= 4.21.0" | |
| 3738 | - | } | |
| 3739 | - | }, | |
| 3740 | - | "node_modules/uri-js": { | |
| 3741 | - | "version": "4.4.1", | |
| 3742 | - | "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", | |
| 3743 | - | "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", | |
| 3744 | - | "dev": true, | |
| 3745 | - | "license": "BSD-2-Clause", | |
| 3746 | - | "dependencies": { | |
| 3747 | - | "punycode": "^2.1.0" | |
| 3748 | - | } | |
| 3749 | - | }, | |
| 3750 | - | "node_modules/util-deprecate": { | |
| 3751 | - | "version": "1.0.2", | |
| 3752 | - | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", | |
| 3753 | - | "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", | |
| 3754 | - | "dev": true, | |
| 3755 | - | "license": "MIT" | |
| 3756 | - | }, | |
| 3757 | - | "node_modules/vite": { | |
| 3758 | - | "version": "8.0.0", | |
| 3759 | - | "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.0.tgz", | |
| 3760 | - | "integrity": "sha512-fPGaRNj9Zytaf8LEiBhY7Z6ijnFKdzU/+mL8EFBaKr7Vw1/FWcTBAMW0wLPJAGMPX38ZPVCVgLceWiEqeoqL2Q==", | |
| 3761 | - | "dev": true, | |
| 3762 | - | "license": "MIT", | |
| 3763 | - | "peer": true, | |
| 3764 | - | "dependencies": { | |
| 3765 | - | "@oxc-project/runtime": "0.115.0", | |
| 3766 | - | "lightningcss": "^1.32.0", | |
| 3767 | - | "picomatch": "^4.0.3", | |
| 3768 | - | "postcss": "^8.5.8", | |
| 3769 | - | "rolldown": "1.0.0-rc.9", | |
| 3770 | - | "tinyglobby": "^0.2.15" | |
| 3771 | - | }, | |
| 3772 | - | "bin": { | |
| 3773 | - | "vite": "bin/vite.js" | |
| 3774 | - | }, | |
| 3775 | - | "engines": { | |
| 3776 | - | "node": "^20.19.0 || >=22.12.0" | |
| 3777 | - | }, | |
| 3778 | - | "funding": { | |
| 3779 | - | "url": "https://github.com/vitejs/vite?sponsor=1" | |
| 3780 | - | }, | |
| 3781 | - | "optionalDependencies": { | |
| 3782 | - | "fsevents": "~2.3.3" | |
| 3783 | - | }, | |
| 3784 | - | "peerDependencies": { | |
| 3785 | - | "@types/node": "^20.19.0 || >=22.12.0", | |
| 3786 | - | "@vitejs/devtools": "^0.0.0-alpha.31", | |
| 3787 | - | "esbuild": "^0.27.0", | |
| 3788 | - | "jiti": ">=1.21.0", | |
| 3789 | - | "less": "^4.0.0", | |
| 3790 | - | "sass": "^1.70.0", | |
| 3791 | - | "sass-embedded": "^1.70.0", | |
| 3792 | - | "stylus": ">=0.54.8", | |
| 3793 | - | "sugarss": "^5.0.0", | |
| 3794 | - | "terser": "^5.16.0", | |
| 3795 | - | "tsx": "^4.8.1", | |
| 3796 | - | "yaml": "^2.4.2" | |
| 3797 | - | }, | |
| 3798 | - | "peerDependenciesMeta": { | |
| 3799 | - | "@types/node": { | |
| 3800 | - | "optional": true | |
| 3801 | - | }, | |
| 3802 | - | "@vitejs/devtools": { | |
| 3803 | - | "optional": true | |
| 3804 | - | }, | |
| 3805 | - | "esbuild": { | |
| 3806 | - | "optional": true | |
| 3807 | - | }, | |
| 3808 | - | "jiti": { | |
| 3809 | - | "optional": true | |
| 3810 | - | }, | |
| 3811 | - | "less": { | |
| 3812 | - | "optional": true | |
| 3813 | - | }, | |
| 3814 | - | "sass": { | |
| 3815 | - | "optional": true | |
| 3816 | - | }, | |
| 3817 | - | "sass-embedded": { | |
| 3818 | - | "optional": true | |
| 3819 | - | }, | |
| 3820 | - | "stylus": { | |
| 3821 | - | "optional": true | |
| 3822 | - | }, | |
| 3823 | - | "sugarss": { | |
| 3824 | - | "optional": true | |
| 3825 | - | }, | |
| 3826 | - | "terser": { | |
| 3827 | - | "optional": true | |
| 3828 | - | }, | |
| 3829 | - | "tsx": { | |
| 3830 | - | "optional": true | |
| 3831 | - | }, | |
| 3832 | - | "yaml": { | |
| 3833 | - | "optional": true | |
| 3834 | - | } | |
| 3835 | - | } | |
| 3836 | - | }, | |
| 3837 | - | "node_modules/vitefu": { | |
| 3838 | - | "version": "1.1.2", | |
| 3839 | - | "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.2.tgz", | |
| 3840 | - | "integrity": "sha512-zpKATdUbzbsycPFBN71nS2uzBUQiVnFoOrr2rvqv34S1lcAgMKKkjWleLGeiJlZ8lwCXvtWaRn7R3ZC16SYRuw==", | |
| 3841 | - | "dev": true, | |
| 3842 | - | "license": "MIT", | |
| 3843 | - | "workspaces": [ | |
| 3844 | - | "tests/deps/*", | |
| 3845 | - | "tests/projects/*", | |
| 3846 | - | "tests/projects/workspace/packages/*" | |
| 3847 | - | ], | |
| 3848 | - | "peerDependencies": { | |
| 3849 | - | "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-beta.0" | |
| 3850 | - | }, | |
| 3851 | - | "peerDependenciesMeta": { | |
| 3852 | - | "vite": { | |
| 3853 | - | "optional": true | |
| 3854 | - | } | |
| 3855 | - | } | |
| 3856 | - | }, | |
| 3857 | - | "node_modules/which": { | |
| 3858 | - | "version": "2.0.2", | |
| 3859 | - | "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", | |
| 3860 | - | "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", | |
| 3861 | - | "dev": true, | |
| 3862 | - | "license": "ISC", | |
| 3863 | - | "dependencies": { | |
| 3864 | - | "isexe": "^2.0.0" | |
| 3865 | - | }, | |
| 3866 | - | "bin": { | |
| 3867 | - | "node-which": "bin/node-which" | |
| 3868 | - | }, | |
| 3869 | - | "engines": { | |
| 3870 | - | "node": ">= 8" | |
| 3871 | - | } | |
| 3872 | - | }, | |
| 3873 | - | "node_modules/word-wrap": { | |
| 3874 | - | "version": "1.2.5", | |
| 3875 | - | "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", | |
| 3876 | - | "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", | |
| 3877 | - | "dev": true, | |
| 3878 | - | "license": "MIT", | |
| 3879 | - | "engines": { | |
| 3880 | - | "node": ">=0.10.0" | |
| 3881 | - | } | |
| 3882 | - | }, | |
| 3883 | - | "node_modules/yocto-queue": { | |
| 3884 | - | "version": "0.1.0", | |
| 3885 | - | "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", | |
| 3886 | - | "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", | |
| 3887 | - | "dev": true, | |
| 3888 | - | "license": "MIT", | |
| 3889 | - | "engines": { | |
| 3890 | - | "node": ">=10" | |
| 3891 | - | }, | |
| 3892 | - | "funding": { | |
| 3893 | - | "url": "https://github.com/sponsors/sindresorhus" | |
| 3894 | - | } | |
| 3895 | - | }, | |
| 3896 | - | "node_modules/zimmerframe": { | |
| 3897 | - | "version": "1.1.4", | |
| 3898 | - | "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", | |
| 3899 | - | "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", | |
| 3900 | - | "license": "MIT" | |
| 3901 | - | } | |
| 3902 | - | } | |
| 3903 | - | } | |
D webui/package.json +0 -52
| @@ -1,52 +0,0 @@ | |||
| 1 | - | { | |
| 2 | - | "name": "@dumber/gui", | |
| 3 | - | "version": "0.1.0", | |
| 4 | - | "private": true, | |
| 5 | - | "type": "module", | |
| 6 | - | "scripts": { | |
| 7 | - | "build": "VITE_PAGE=homepage vite build && VITE_PAGE=error vite build && VITE_PAGE=config vite build && VITE_PAGE=webrtc vite build", | |
| 8 | - | "build:homepage": "VITE_PAGE=homepage vite build", | |
| 9 | - | "build:error": "VITE_PAGE=error vite build", | |
| 10 | - | "build:config": "VITE_PAGE=config vite build", | |
| 11 | - | "build:webrtc": "VITE_PAGE=webrtc vite build", | |
| 12 | - | "dev": "VITE_PAGE=homepage vite build --watch", | |
| 13 | - | "dev:homepage": "VITE_PAGE=homepage vite build --watch", | |
| 14 | - | "dev:error": "VITE_PAGE=error vite build --watch", | |
| 15 | - | "dev:config": "VITE_PAGE=config vite build --watch", | |
| 16 | - | "dev:webrtc": "VITE_PAGE=webrtc vite build --watch", | |
| 17 | - | "check": "svelte-check", | |
| 18 | - | "lint": "eslint src --fix", | |
| 19 | - | "fmt": "prettier --write ." | |
| 20 | - | }, | |
| 21 | - | "devDependencies": { | |
| 22 | - | "@internationalized/date": "^3.12.0", | |
| 23 | - | "@lucide/svelte": "^0.577.0", | |
| 24 | - | "@sveltejs/vite-plugin-svelte": "^7.0.0", | |
| 25 | - | "@tailwindcss/cli": "^4.2.2", | |
| 26 | - | "@tailwindcss/postcss": "^4.2.2", | |
| 27 | - | "@types/node": "^25.5.0", | |
| 28 | - | "@typescript-eslint/eslint-plugin": "^8.57.1", | |
| 29 | - | "@typescript-eslint/parser": "^8.57.1", | |
| 30 | - | "autoprefixer": "^10.4.27", | |
| 31 | - | "eslint": "^10.0.3", | |
| 32 | - | "eslint-plugin-svelte": "^3.15.2", | |
| 33 | - | "globals": "^17.4.0", | |
| 34 | - | "postcss": "^8.5.8", | |
| 35 | - | "svelte": "^5.54.0", | |
| 36 | - | "svelte-awesome-color-picker": "^4.1.0", | |
| 37 | - | "svelte-check": "^4.4.5", | |
| 38 | - | "tailwind-variants": "^3.2.2", | |
| 39 | - | "tailwindcss": "^4.0.0", | |
| 40 | - | "tw-animate-css": "^1.4.0", | |
| 41 | - | "typescript": "^5.3.0", | |
| 42 | - | "typescript-eslint": "^8.57.1", | |
| 43 | - | "vite": "^8.0.0" | |
| 44 | - | }, | |
| 45 | - | "dependencies": { | |
| 46 | - | "bits-ui": "^2.16.3", | |
| 47 | - | "clsx": "^2.1.1", | |
| 48 | - | "frappe-charts": "^1.6.2", | |
| 49 | - | "mode-watcher": "^1.1.0", | |
| 50 | - | "tailwind-merge": "^3.5.0" | |
| 51 | - | } | |
| 52 | - | } | |
D webui/postcss.config.js +0 -6
| @@ -1,6 +0,0 @@ | |||
| 1 | - | export default { | |
| 2 | - | plugins: { | |
| 3 | - | "@tailwindcss/postcss": {}, | |
| 4 | - | autoprefixer: {}, | |
| 5 | - | }, | |
| 6 | - | }; | |
D webui/public/favicon.png +0 -0 binary file: no text diff
binary file: no text diff
D webui/src/lib/bridge.ts +0 -165
| @@ -1,165 +0,0 @@ | |||
| 1 | - | /** | |
| 2 | - | * WebKit bridge utilities for communicating with the Go backend. | |
| 3 | - | * | |
| 4 | - | * IMPORTANT: Svelte 5 uses Proxy objects for reactive state ($state, $derived). | |
| 5 | - | * WebKit's messageHandlers.postMessage() cannot serialize Proxy objects - it | |
| 6 | - | * fails silently, dropping the message entirely. This module provides safe | |
| 7 | - | * serialization that converts Proxies to plain objects before sending. | |
| 8 | - | */ | |
| 9 | - | ||
| 10 | - | export interface BridgeMessage { | |
| 11 | - | type: string; | |
| 12 | - | webviewId: number; | |
| 13 | - | payload: Record<string, unknown>; | |
| 14 | - | } | |
| 15 | - | ||
| 16 | - | interface WebKitBridge { | |
| 17 | - | postMessage: (msg: unknown) => void; | |
| 18 | - | } | |
| 19 | - | ||
| 20 | - | /** | |
| 21 | - | * Get the native bridge, if available. | |
| 22 | - | * WebKit exposes `window.webkit.messageHandlers.dumber` while CEF injects | |
| 23 | - | * `window.dumber.postMessage`. | |
| 24 | - | */ | |
| 25 | - | export function getWebKitBridge(): WebKitBridge | null { | |
| 26 | - | // eslint-disable-next-line @typescript-eslint/no-explicit-any | |
| 27 | - | const bridge = (window as any).webkit?.messageHandlers?.dumber ?? (window as any).dumber; | |
| 28 | - | return bridge && typeof bridge.postMessage === "function" ? bridge : null; | |
| 29 | - | } | |
| 30 | - | ||
| 31 | - | /** | |
| 32 | - | * Get the current webview ID assigned by the Go backend. | |
| 33 | - | */ | |
| 34 | - | export function getWebViewId(): number { | |
| 35 | - | // eslint-disable-next-line @typescript-eslint/no-explicit-any | |
| 36 | - | return (window as any).__dumber_webview_id || 0; | |
| 37 | - | } | |
| 38 | - | ||
| 39 | - | /** | |
| 40 | - | * Deep-clone an object, converting all Svelte 5 Proxy objects to plain objects. | |
| 41 | - | * Uses JSON serialization which strips Proxy wrappers. | |
| 42 | - | * | |
| 43 | - | * @throws Error if the object contains non-serializable values (functions, circular refs, etc.) | |
| 44 | - | */ | |
| 45 | - | export function toPlainObject<T>(obj: T): T { | |
| 46 | - | try { | |
| 47 | - | return JSON.parse(JSON.stringify(obj)); | |
| 48 | - | } catch (err) { | |
| 49 | - | const message = err instanceof Error ? err.message : String(err); | |
| 50 | - | throw new Error(`Failed to serialize object for bridge: ${message}`); | |
| 51 | - | } | |
| 52 | - | } | |
| 53 | - | ||
| 54 | - | /** | |
| 55 | - | * Post a message to the Go backend via the native bridge. | |
| 56 | - | * | |
| 57 | - | * Automatically: | |
| 58 | - | * - Converts Svelte 5 Proxy objects to plain objects | |
| 59 | - | * - Adds webviewId if not provided | |
| 60 | - | * - Logs errors if serialization fails | |
| 61 | - | * | |
| 62 | - | * @throws Error if bridge is not available or serialization fails | |
| 63 | - | */ | |
| 64 | - | export function postMessage(msg: BridgeMessage): void { | |
| 65 | - | const bridge = getWebKitBridge(); | |
| 66 | - | if (!bridge) { | |
| 67 | - | throw new Error("Dumber bridge not available"); | |
| 68 | - | } | |
| 69 | - | ||
| 70 | - | // Ensure webviewId is set | |
| 71 | - | if (!msg.webviewId) { | |
| 72 | - | msg.webviewId = getWebViewId(); | |
| 73 | - | } | |
| 74 | - | ||
| 75 | - | // Convert to plain object to strip Svelte 5 Proxies | |
| 76 | - | // This is critical - WebKit silently drops messages with Proxy objects | |
| 77 | - | const plainMsg = toPlainObject(msg); | |
| 78 | - | bridge.postMessage(plainMsg); | |
| 79 | - | } | |
| 80 | - | ||
| 81 | - | /** | |
| 82 | - | * Internal callback registry using Map for tracking pending requests. | |
| 83 | - | * Callbacks must also be exposed on window for Go backend to call, | |
| 84 | - | * but we track them here for proper cleanup and to prevent leaks. | |
| 85 | - | */ | |
| 86 | - | const pendingCallbacks = new Map<string, { | |
| 87 | - | cleanup: () => void; | |
| 88 | - | timeoutId: ReturnType<typeof setTimeout>; | |
| 89 | - | }>(); | |
| 90 | - | ||
| 91 | - | let callbackCounter = 0; | |
| 92 | - | const CALLBACK_TIMEOUT_MS = 30000; | |
| 93 | - | ||
| 94 | - | /** | |
| 95 | - | * Post a message and set up success/error callbacks with automatic cleanup. | |
| 96 | - | * Uses unique IDs to prevent naming conflicts between concurrent requests. | |
| 97 | - | * | |
| 98 | - | * @param type - Message type (e.g., "get_keybindings", "set_keybinding") | |
| 99 | - | * @param payload - Message payload (will be serialized to strip Proxies) | |
| 100 | - | * @param successCallback - Base name for success callback (will be made unique) | |
| 101 | - | * @param errorCallback - Base name for error callback (will be made unique) | |
| 102 | - | * @param onSuccess - Handler for success response | |
| 103 | - | * @param onError - Handler for error response | |
| 104 | - | */ | |
| 105 | - | export function postMessageWithCallbacks<TResponse, TPayload extends Record<string, unknown>>( | |
| 106 | - | type: string, | |
| 107 | - | payload: TPayload, | |
| 108 | - | successCallback: string, | |
| 109 | - | errorCallback: string, | |
| 110 | - | onSuccess: (response: TResponse) => void, | |
| 111 | - | onError: (error: string) => void, | |
| 112 | - | ): void { | |
| 113 | - | const id = ++callbackCounter; | |
| 114 | - | const successKey = `${successCallback}_${id}`; | |
| 115 | - | const errorKey = `${errorCallback}_${id}`; | |
| 116 | - | ||
| 117 | - | // Cleanup removes from both window and our tracking Map | |
| 118 | - | const cleanup = () => { | |
| 119 | - | const entry = pendingCallbacks.get(successKey); | |
| 120 | - | if (entry) { | |
| 121 | - | clearTimeout(entry.timeoutId); | |
| 122 | - | pendingCallbacks.delete(successKey); | |
| 123 | - | } | |
| 124 | - | // eslint-disable-next-line @typescript-eslint/no-explicit-any | |
| 125 | - | delete (window as any)[successKey]; | |
| 126 | - | // eslint-disable-next-line @typescript-eslint/no-explicit-any | |
| 127 | - | delete (window as any)[errorKey]; | |
| 128 | - | }; | |
| 129 | - | ||
| 130 | - | // Timeout prevents memory leaks if backend never responds | |
| 131 | - | const timeoutId = setTimeout(() => { | |
| 132 | - | cleanup(); | |
| 133 | - | onError("Request timed out"); | |
| 134 | - | }, CALLBACK_TIMEOUT_MS); | |
| 135 | - | ||
| 136 | - | // Track in our Map for proper lifecycle management | |
| 137 | - | pendingCallbacks.set(successKey, { cleanup, timeoutId }); | |
| 138 | - | ||
| 139 | - | // Expose on window for Go backend (it calls window[callbackName](response)) | |
| 140 | - | // eslint-disable-next-line @typescript-eslint/no-explicit-any | |
| 141 | - | (window as any)[successKey] = (response: TResponse) => { | |
| 142 | - | cleanup(); | |
| 143 | - | onSuccess(response); | |
| 144 | - | }; | |
| 145 | - | // eslint-disable-next-line @typescript-eslint/no-explicit-any | |
| 146 | - | (window as any)[errorKey] = (msg: unknown) => { | |
| 147 | - | cleanup(); | |
| 148 | - | onError(typeof msg === "string" ? msg : "Unknown error"); | |
| 149 | - | }; | |
| 150 | - | ||
| 151 | - | try { | |
| 152 | - | postMessage({ | |
| 153 | - | type, | |
| 154 | - | webviewId: getWebViewId(), | |
| 155 | - | payload: { | |
| 156 | - | ...payload, | |
| 157 | - | successCallback: successKey, | |
| 158 | - | errorCallback: errorKey, | |
| 159 | - | }, | |
| 160 | - | }); | |
| 161 | - | } catch (err) { | |
| 162 | - | cleanup(); | |
| 163 | - | onError(err instanceof Error ? err.message : String(err)); | |
| 164 | - | } | |
| 165 | - | } | |
D webui/src/lib/components/ui/alert-dialog/alert-dialog-action.svelte +0 -27
| @@ -1,27 +0,0 @@ | |||
| 1 | - | <script lang="ts"> | |
| 2 | - | import { AlertDialog as AlertDialogPrimitive } from "bits-ui"; | |
| 3 | - | import { buttonVariants } from "$lib/components/ui/button/index.js"; | |
| 4 | - | import { cn } from "$lib/utils.js"; | |
| 5 | - | import type { Snippet } from "svelte"; | |
| 6 | - | import type { MouseEventHandler } from "svelte/elements"; | |
| 7 | - | ||
| 8 | - | let { | |
| 9 | - | ref = $bindable(null), | |
| 10 | - | class: className, | |
| 11 | - | children, | |
| 12 | - | onclick, | |
| 13 | - | ...restProps | |
| 14 | - | }: AlertDialogPrimitive.ActionProps & { | |
| 15 | - | children?: Snippet; | |
| 16 | - | onclick?: MouseEventHandler<HTMLButtonElement>; | |
| 17 | - | } = $props(); | |
| 18 | - | </script> | |
| 19 | - | ||
| 20 | - | <AlertDialogPrimitive.Action | |
| 21 | - | bind:ref | |
| 22 | - | class={cn(buttonVariants(), className)} | |
| 23 | - | {onclick} | |
| 24 | - | {...restProps} | |
| 25 | - | > | |
| 26 | - | {@render children?.()} | |
| 27 | - | </AlertDialogPrimitive.Action> | |
D webui/src/lib/components/ui/alert-dialog/alert-dialog-cancel.svelte +0 -23
| @@ -1,23 +0,0 @@ | |||
| 1 | - | <script lang="ts"> | |
| 2 | - | import { AlertDialog as AlertDialogPrimitive } from "bits-ui"; | |
| 3 | - | import { buttonVariants } from "$lib/components/ui/button/index.js"; | |
| 4 | - | import { cn } from "$lib/utils.js"; | |
| 5 | - | import type { Snippet } from "svelte"; | |
| 6 | - | ||
| 7 | - | let { | |
| 8 | - | ref = $bindable(null), | |
| 9 | - | class: className, | |
| 10 | - | children, | |
| 11 | - | ...restProps | |
| 12 | - | }: AlertDialogPrimitive.CancelProps & { | |
| 13 | - | children?: Snippet; | |
| 14 | - | } = $props(); | |
| 15 | - | </script> | |
| 16 | - | ||
| 17 | - | <AlertDialogPrimitive.Cancel | |
| 18 | - | bind:ref | |
| 19 | - | class={cn(buttonVariants({ variant: "outline" }), className)} | |
| 20 | - | {...restProps} | |
| 21 | - | > | |
| 22 | - | {@render children?.()} | |
| 23 | - | </AlertDialogPrimitive.Cancel> | |
D webui/src/lib/components/ui/alert-dialog/alert-dialog-content.svelte +0 -24
| @@ -1,24 +0,0 @@ | |||
| 1 | - | <script lang="ts"> | |
| 2 | - | import { AlertDialog as AlertDialogPrimitive } from "bits-ui"; | |
| 3 | - | import { cn } from "$lib/utils.js"; | |
| 4 | - | import AlertDialogOverlay from "./alert-dialog-overlay.svelte"; | |
| 5 | - | import AlertDialogPortal from "./alert-dialog-portal.svelte"; | |
| 6 | - | ||
| 7 | - | let { | |
| 8 | - | ref = $bindable(null), | |
| 9 | - | class: className, | |
| 10 | - | ...restProps | |
| 11 | - | }: AlertDialogPrimitive.ContentProps = $props(); | |
| 12 | - | </script> | |
| 13 | - | ||
| 14 | - | <AlertDialogPortal> | |
| 15 | - | <AlertDialogOverlay /> | |
| 16 | - | <AlertDialogPrimitive.Content | |
| 17 | - | bind:ref | |
| 18 | - | class={cn( | |
| 19 | - | "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed left-1/2 top-1/2 z-50 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 border border-border p-6 shadow-lg duration-200 sm:rounded-lg", | |
| 20 | - | className | |
| 21 | - | )} | |
| 22 | - | {...restProps} | |
| 23 | - | /> | |
| 24 | - | </AlertDialogPortal> | |
D webui/src/lib/components/ui/alert-dialog/alert-dialog-description.svelte +0 -16
| @@ -1,16 +0,0 @@ | |||
| 1 | - | <script lang="ts"> | |
| 2 | - | import { AlertDialog as AlertDialogPrimitive } from "bits-ui"; | |
| 3 | - | import { cn } from "$lib/utils.js"; | |
| 4 | - | ||
| 5 | - | let { | |
| 6 | - | ref = $bindable(null), | |
| 7 | - | class: className, | |
| 8 | - | ...restProps | |
| 9 | - | }: AlertDialogPrimitive.DescriptionProps = $props(); | |
| 10 | - | </script> | |
| 11 | - | ||
| 12 | - | <AlertDialogPrimitive.Description | |
| 13 | - | bind:ref | |
| 14 | - | class={cn("text-sm text-muted-foreground", className)} | |
| 15 | - | {...restProps} | |
| 16 | - | /> | |
D webui/src/lib/components/ui/alert-dialog/alert-dialog-footer.svelte +0 -19
| @@ -1,19 +0,0 @@ | |||
| 1 | - | <script lang="ts"> | |
| 2 | - | import type { HTMLAttributes } from "svelte/elements"; | |
| 3 | - | import { cn, type WithElementRef } from "$lib/utils.js"; | |
| 4 | - | ||
| 5 | - | let { | |
| 6 | - | ref = $bindable(null), | |
| 7 | - | class: className, | |
| 8 | - | children, | |
| 9 | - | ...restProps | |
| 10 | - | }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props(); | |
| 11 | - | </script> | |
| 12 | - | ||
| 13 | - | <div | |
| 14 | - | bind:this={ref} | |
| 15 | - | class={cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className)} | |
| 16 | - | {...restProps} | |
| 17 | - | > | |
| 18 | - | {@render children?.()} | |
| 19 | - | </div> | |
D webui/src/lib/components/ui/alert-dialog/alert-dialog-header.svelte +0 -19
| @@ -1,19 +0,0 @@ | |||
| 1 | - | <script lang="ts"> | |
| 2 | - | import type { HTMLAttributes } from "svelte/elements"; | |
| 3 | - | import { cn, type WithElementRef } from "$lib/utils.js"; | |
| 4 | - | ||
| 5 | - | let { | |
| 6 | - | ref = $bindable(null), | |
| 7 | - | class: className, | |
| 8 | - | children, | |
| 9 | - | ...restProps | |
| 10 | - | }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props(); | |
| 11 | - | </script> | |
| 12 | - | ||
| 13 | - | <div | |
| 14 | - | bind:this={ref} | |
| 15 | - | class={cn("flex flex-col gap-2 text-center sm:text-left", className)} | |
| 16 | - | {...restProps} | |
| 17 | - | > | |
| 18 | - | {@render children?.()} | |
| 19 | - | </div> | |
D webui/src/lib/components/ui/alert-dialog/alert-dialog-overlay.svelte +0 -19
| @@ -1,19 +0,0 @@ | |||
| 1 | - | <script lang="ts"> | |
| 2 | - | import { AlertDialog as AlertDialogPrimitive } from "bits-ui"; | |
| 3 | - | import { cn } from "$lib/utils.js"; | |
| 4 | - | ||
| 5 | - | let { | |
| 6 | - | ref = $bindable(null), | |
| 7 | - | class: className, | |
| 8 | - | ...restProps | |
| 9 | - | }: AlertDialogPrimitive.OverlayProps = $props(); | |
| 10 | - | </script> | |
| 11 | - | ||
| 12 | - | <AlertDialogPrimitive.Overlay | |
| 13 | - | bind:ref | |
| 14 | - | class={cn( | |
| 15 | - | "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80", | |
| 16 | - | className | |
| 17 | - | )} | |
| 18 | - | {...restProps} | |
| 19 | - | /> | |
D webui/src/lib/components/ui/alert-dialog/alert-dialog-portal.svelte +0 -7
| @@ -1,7 +0,0 @@ | |||
| 1 | - | <script lang="ts"> | |
| 2 | - | import { AlertDialog as AlertDialogPrimitive } from "bits-ui"; | |
| 3 | - | ||
| 4 | - | let { ...restProps }: AlertDialogPrimitive.PortalProps = $props(); | |
| 5 | - | </script> | |
| 6 | - | ||
| 7 | - | <AlertDialogPrimitive.Portal {...restProps} /> | |
D webui/src/lib/components/ui/alert-dialog/alert-dialog-title.svelte +0 -16
| @@ -1,16 +0,0 @@ | |||
| 1 | - | <script lang="ts"> | |
| 2 | - | import { AlertDialog as AlertDialogPrimitive } from "bits-ui"; | |
| 3 | - | import { cn } from "$lib/utils.js"; | |
| 4 | - | ||
| 5 | - | let { | |
| 6 | - | ref = $bindable(null), | |
| 7 | - | class: className, | |
| 8 | - | ...restProps | |
| 9 | - | }: AlertDialogPrimitive.TitleProps = $props(); | |
| 10 | - | </script> | |
| 11 | - | ||
| 12 | - | <AlertDialogPrimitive.Title | |
| 13 | - | bind:ref | |
| 14 | - | class={cn("text-lg font-semibold", className)} | |
| 15 | - | {...restProps} | |
| 16 | - | /> | |
D webui/src/lib/components/ui/alert-dialog/alert-dialog-trigger.svelte +0 -16
| @@ -1,16 +0,0 @@ | |||
| 1 | - | <script lang="ts"> | |
| 2 | - | import { AlertDialog as AlertDialogPrimitive } from "bits-ui"; | |
| 3 | - | import { cn } from "$lib/utils.js"; | |
| 4 | - | ||
| 5 | - | let { | |
| 6 | - | ref = $bindable(null), | |
| 7 | - | class: className, | |
| 8 | - | ...restProps | |
| 9 | - | }: AlertDialogPrimitive.TriggerProps = $props(); | |
| 10 | - | </script> | |
| 11 | - | ||
| 12 | - | <AlertDialogPrimitive.Trigger | |
| 13 | - | bind:ref | |
| 14 | - | class={cn(className)} | |
| 15 | - | {...restProps} | |
| 16 | - | /> | |
D webui/src/lib/components/ui/alert-dialog/alert-dialog.svelte +0 -10
| @@ -1,10 +0,0 @@ | |||
| 1 | - | <script lang="ts"> | |
| 2 | - | import { AlertDialog as AlertDialogPrimitive } from "bits-ui"; | |
| 3 | - | ||
| 4 | - | let { | |
| 5 | - | open = $bindable(false), | |
| 6 | - | ...restProps | |
| 7 | - | }: AlertDialogPrimitive.RootProps = $props(); | |
| 8 | - | </script> | |
| 9 | - | ||
| 10 | - | <AlertDialogPrimitive.Root bind:open {...restProps} /> | |
D webui/src/lib/components/ui/alert-dialog/index.ts +0 -37
| @@ -1,37 +0,0 @@ | |||
| 1 | - | import Root from "./alert-dialog.svelte"; | |
| 2 | - | import Trigger from "./alert-dialog-trigger.svelte"; | |
| 3 | - | import Portal from "./alert-dialog-portal.svelte"; | |
| 4 | - | import Overlay from "./alert-dialog-overlay.svelte"; | |
| 5 | - | import Content from "./alert-dialog-content.svelte"; | |
| 6 | - | import Header from "./alert-dialog-header.svelte"; | |
| 7 | - | import Footer from "./alert-dialog-footer.svelte"; | |
| 8 | - | import Title from "./alert-dialog-title.svelte"; | |
| 9 | - | import Description from "./alert-dialog-description.svelte"; | |
| 10 | - | import Action from "./alert-dialog-action.svelte"; | |
| 11 | - | import Cancel from "./alert-dialog-cancel.svelte"; | |
| 12 | - | ||
| 13 | - | export { | |
| 14 | - | Root, | |
| 15 | - | Trigger, | |
| 16 | - | Portal, | |
| 17 | - | Overlay, | |
| 18 | - | Content, | |
| 19 | - | Header, | |
| 20 | - | Footer, | |
| 21 | - | Title, | |
| 22 | - | Description, | |
| 23 | - | Action, | |
| 24 | - | Cancel, | |
| 25 | - | // | |
| 26 | - | Root as AlertDialog, | |
| 27 | - | Trigger as AlertDialogTrigger, | |
| 28 | - | Portal as AlertDialogPortal, | |
| 29 | - | Overlay as AlertDialogOverlay, | |
| 30 | - | Content as AlertDialogContent, | |
| 31 | - | Header as AlertDialogHeader, | |
| 32 | - | Footer as AlertDialogFooter, | |
| 33 | - | Title as AlertDialogTitle, | |
| 34 | - | Description as AlertDialogDescription, | |
| 35 | - | Action as AlertDialogAction, | |
| 36 | - | Cancel as AlertDialogCancel, | |
| 37 | - | }; | |
D webui/src/lib/components/ui/button/button.svelte +0 -82
| @@ -1,82 +0,0 @@ | |||
| 1 | - | <script lang="ts" module> | |
| 2 | - | import { cn, type WithElementRef } from "$lib/utils.js"; | |
| 3 | - | import type { HTMLAnchorAttributes, HTMLButtonAttributes } from "svelte/elements"; | |
| 4 | - | import { type VariantProps, tv } from "tailwind-variants"; | |
| 5 | - | ||
| 6 | - | export const buttonVariants = tv({ | |
| 7 | - | base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex shrink-0 items-center justify-center gap-2 text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", | |
| 8 | - | variants: { | |
| 9 | - | variant: { | |
| 10 | - | default: "bg-primary text-primary-foreground hover:bg-primary/90 shadow-xs", | |
| 11 | - | destructive: | |
| 12 | - | "bg-destructive hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 text-white shadow-xs", | |
| 13 | - | outline: | |
| 14 | - | "bg-background hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 border border-border shadow-xs", | |
| 15 | - | secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80 shadow-xs", | |
| 16 | - | ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", | |
| 17 | - | link: "text-primary underline-offset-4 hover:underline", | |
| 18 | - | }, | |
| 19 | - | size: { | |
| 20 | - | default: "h-9 px-4 py-2 has-[>svg]:px-3", | |
| 21 | - | sm: "h-8 gap-1.5 px-3 has-[>svg]:px-2.5", | |
| 22 | - | lg: "h-10 px-6 has-[>svg]:px-4", | |
| 23 | - | icon: "size-9", | |
| 24 | - | "icon-sm": "size-8", | |
| 25 | - | "icon-lg": "size-10", | |
| 26 | - | }, | |
| 27 | - | }, | |
| 28 | - | defaultVariants: { | |
| 29 | - | variant: "default", | |
| 30 | - | size: "default", | |
| 31 | - | }, | |
| 32 | - | }); | |
| 33 | - | ||
| 34 | - | export type ButtonVariant = VariantProps<typeof buttonVariants>["variant"]; | |
| 35 | - | export type ButtonSize = VariantProps<typeof buttonVariants>["size"]; | |
| 36 | - | ||
| 37 | - | export type ButtonProps = WithElementRef<HTMLButtonAttributes> & | |
| 38 | - | WithElementRef<HTMLAnchorAttributes> & { | |
| 39 | - | variant?: ButtonVariant; | |
| 40 | - | size?: ButtonSize; | |
| 41 | - | }; | |
| 42 | - | </script> | |
| 43 | - | ||
| 44 | - | <script lang="ts"> | |
| 45 | - | let { | |
| 46 | - | class: className, | |
| 47 | - | variant = "default", | |
| 48 | - | size = "default", | |
| 49 | - | ref = $bindable(null), | |
| 50 | - | href = undefined, | |
| 51 | - | type = "button", | |
| 52 | - | disabled, | |
| 53 | - | children, | |
| 54 | - | ...restProps | |
| 55 | - | }: ButtonProps = $props(); | |
| 56 | - | </script> | |
| 57 | - | ||
| 58 | - | {#if href} | |
| 59 | - | <a | |
| 60 | - | bind:this={ref} | |
| 61 | - | data-slot="button" | |
| 62 | - | class={cn(buttonVariants({ variant, size }), className)} | |
| 63 | - | href={disabled ? undefined : href} | |
| 64 | - | aria-disabled={disabled} | |
| 65 | - | role={disabled ? "link" : undefined} | |
| 66 | - | tabindex={disabled ? -1 : undefined} | |
| 67 | - | {...restProps} | |
| 68 | - | > | |
| 69 | - | {@render children?.()} | |
| 70 | - | </a> | |
| 71 | - | {:else} | |
| 72 | - | <button | |
| 73 | - | bind:this={ref} | |
| 74 | - | data-slot="button" | |
| 75 | - | class={cn(buttonVariants({ variant, size }), className)} | |
| 76 | - | {type} | |
| 77 | - | {disabled} | |
| 78 | - | {...restProps} | |
| 79 | - | > | |
| 80 | - | {@render children?.()} | |
| 81 | - | </button> | |
| 82 | - | {/if} | |
D webui/src/lib/components/ui/button/index.ts +0 -17
| @@ -1,17 +0,0 @@ | |||
| 1 | - | import Root, { | |
| 2 | - | type ButtonProps, | |
| 3 | - | type ButtonSize, | |
| 4 | - | type ButtonVariant, | |
| 5 | - | buttonVariants, | |
| 6 | - | } from "./button.svelte"; | |
| 7 | - | ||
| 8 | - | export { | |
| 9 | - | Root, | |
| 10 | - | type ButtonProps as Props, | |
| 11 | - | // | |
| 12 | - | Root as Button, | |
| 13 | - | buttonVariants, | |
| 14 | - | type ButtonProps, | |
| 15 | - | type ButtonSize, | |
| 16 | - | type ButtonVariant, | |
| 17 | - | }; | |
D webui/src/lib/components/ui/card/card-action.svelte +0 -20
| @@ -1,20 +0,0 @@ | |||
| 1 | - | <script lang="ts"> | |
| 2 | - | import { cn, type WithElementRef } from "$lib/utils.js"; | |
| 3 | - | import type { HTMLAttributes } from "svelte/elements"; | |
| 4 | - | ||
| 5 | - | let { | |
| 6 | - | ref = $bindable(null), | |
| 7 | - | class: className, | |
| 8 | - | children, | |
| 9 | - | ...restProps | |
| 10 | - | }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props(); | |
| 11 | - | </script> | |
| 12 | - | ||
| 13 | - | <div | |
| 14 | - | bind:this={ref} | |
| 15 | - | data-slot="card-action" | |
| 16 | - | class={cn("col-start-2 row-span-2 row-start-1 self-start justify-self-end", className)} | |
| 17 | - | {...restProps} | |
| 18 | - | > | |
| 19 | - | {@render children?.()} | |
| 20 | - | </div> | |
D webui/src/lib/components/ui/card/card-content.svelte +0 -15
| @@ -1,15 +0,0 @@ | |||
| 1 | - | <script lang="ts"> | |
| 2 | - | import type { HTMLAttributes } from "svelte/elements"; | |
| 3 | - | import { cn, type WithElementRef } from "$lib/utils.js"; | |
| 4 | - | ||
| 5 | - | let { | |
| 6 | - | ref = $bindable(null), | |
| 7 | - | class: className, | |
| 8 | - | children, | |
| 9 | - | ...restProps | |
| 10 | - | }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props(); | |
| 11 | - | </script> | |
| 12 | - | ||
| 13 | - | <div bind:this={ref} data-slot="card-content" class={cn("px-6", className)} {...restProps}> | |
| 14 | - | {@render children?.()} | |
| 15 | - | </div> | |
D webui/src/lib/components/ui/card/card-description.svelte +0 -20
| @@ -1,20 +0,0 @@ | |||
| 1 | - | <script lang="ts"> | |
| 2 | - | import type { HTMLAttributes } from "svelte/elements"; | |
| 3 | - | import { cn, type WithElementRef } from "$lib/utils.js"; | |
| 4 | - | ||
| 5 | - | let { | |
| 6 | - | ref = $bindable(null), | |
| 7 | - | class: className, | |
| 8 | - | children, | |
| 9 | - | ...restProps | |
| 10 | - | }: WithElementRef<HTMLAttributes<HTMLParagraphElement>> = $props(); | |
| 11 | - | </script> | |
| 12 | - | ||
| 13 | - | <p | |
| 14 | - | bind:this={ref} | |
| 15 | - | data-slot="card-description" | |
| 16 | - | class={cn("text-muted-foreground text-sm", className)} | |
| 17 | - | {...restProps} | |
| 18 | - | > | |
| 19 | - | {@render children?.()} | |
| 20 | - | </p> | |
D webui/src/lib/components/ui/card/card-footer.svelte +0 -20
| @@ -1,20 +0,0 @@ | |||
| 1 | - | <script lang="ts"> | |
| 2 | - | import { cn, type WithElementRef } from "$lib/utils.js"; | |
| 3 | - | import type { HTMLAttributes } from "svelte/elements"; | |
| 4 | - | ||
| 5 | - | let { | |
| 6 | - | ref = $bindable(null), | |
| 7 | - | class: className, | |
| 8 | - | children, | |
| 9 | - | ...restProps | |
| 10 | - | }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props(); | |
| 11 | - | </script> | |
| 12 | - | ||
| 13 | - | <div | |
| 14 | - | bind:this={ref} | |
| 15 | - | data-slot="card-footer" | |
| 16 | - | class={cn("flex items-center px-6 [.border-t]:pt-6", className)} | |
| 17 | - | {...restProps} | |
| 18 | - | > | |
| 19 | - | {@render children?.()} | |
| 20 | - | </div> | |
D webui/src/lib/components/ui/card/card-header.svelte +0 -23
| @@ -1,23 +0,0 @@ | |||
| 1 | - | <script lang="ts"> | |
| 2 | - | import { cn, type WithElementRef } from "$lib/utils.js"; | |
| 3 | - | import type { HTMLAttributes } from "svelte/elements"; | |
| 4 | - | ||
| 5 | - | let { | |
| 6 | - | ref = $bindable(null), | |
| 7 | - | class: className, | |
| 8 | - | children, | |
| 9 | - | ...restProps | |
| 10 | - | }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props(); | |
| 11 | - | </script> | |
| 12 | - | ||
| 13 | - | <div | |
| 14 | - | bind:this={ref} | |
| 15 | - | data-slot="card-header" | |
| 16 | - | class={cn( | |
| 17 | - | "@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6", | |
| 18 | - | className | |
| 19 | - | )} | |
| 20 | - | {...restProps} | |
| 21 | - | > | |
| 22 | - | {@render children?.()} | |
| 23 | - | </div> | |
D webui/src/lib/components/ui/card/card-title.svelte +0 -20
| @@ -1,20 +0,0 @@ | |||
| 1 | - | <script lang="ts"> | |
| 2 | - | import type { HTMLAttributes } from "svelte/elements"; | |
| 3 | - | import { cn, type WithElementRef } from "$lib/utils.js"; | |
| 4 | - | ||
| 5 | - | let { | |
| 6 | - | ref = $bindable(null), | |
| 7 | - | class: className, | |
| 8 | - | children, | |
| 9 | - | ...restProps | |
| 10 | - | }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props(); | |
| 11 | - | </script> | |
| 12 | - | ||
| 13 | - | <div | |
| 14 | - | bind:this={ref} | |
| 15 | - | data-slot="card-title" | |
| 16 | - | class={cn("leading-none font-semibold", className)} | |
| 17 | - | {...restProps} | |
| 18 | - | > | |
| 19 | - | {@render children?.()} | |
| 20 | - | </div> | |
D webui/src/lib/components/ui/card/card.svelte +0 -23
| @@ -1,23 +0,0 @@ | |||
| 1 | - | <script lang="ts"> | |
| 2 | - | import type { HTMLAttributes } from "svelte/elements"; | |
| 3 | - | import { cn, type WithElementRef } from "$lib/utils.js"; | |
| 4 | - | ||
| 5 | - | let { | |
| 6 | - | ref = $bindable(null), | |
| 7 | - | class: className, | |
| 8 | - | children, | |
| 9 | - | ...restProps | |
| 10 | - | }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props(); | |
| 11 | - | </script> | |
| 12 | - | ||
| 13 | - | <div | |
| 14 | - | bind:this={ref} | |
| 15 | - | data-slot="card" | |
| 16 | - | class={cn( | |
| 17 | - | "bg-card text-card-foreground flex flex-col gap-6 border border-border py-6 shadow-sm", | |
| 18 | - | className | |
| 19 | - | )} | |
| 20 | - | {...restProps} | |
| 21 | - | > | |
| 22 | - | {@render children?.()} | |
| 23 | - | </div> | |
D webui/src/lib/components/ui/card/index.ts +0 -25
| @@ -1,25 +0,0 @@ | |||
| 1 | - | import Root from "./card.svelte"; | |
| 2 | - | import Content from "./card-content.svelte"; | |
| 3 | - | import Description from "./card-description.svelte"; | |
| 4 | - | import Footer from "./card-footer.svelte"; | |
| 5 | - | import Header from "./card-header.svelte"; | |
| 6 | - | import Title from "./card-title.svelte"; | |
| 7 | - | import Action from "./card-action.svelte"; | |
| 8 | - | ||
| 9 | - | export { | |
| 10 | - | Root, | |
| 11 | - | Content, | |
| 12 | - | Description, | |
| 13 | - | Footer, | |
| 14 | - | Header, | |
| 15 | - | Title, | |
| 16 | - | Action, | |
| 17 | - | // | |
| 18 | - | Root as Card, | |
| 19 | - | Content as CardContent, | |
| 20 | - | Description as CardDescription, | |
| 21 | - | Footer as CardFooter, | |
| 22 | - | Header as CardHeader, | |
| 23 | - | Title as CardTitle, | |
| 24 | - | Action as CardAction, | |
| 25 | - | }; | |
D webui/src/lib/components/ui/color-picker/color-picker.svelte +0 -80
| @@ -1,80 +0,0 @@ | |||
| 1 | - | <script lang="ts" module> | |
| 2 | - | export type ColorPickerProps = { | |
| 3 | - | value: string; | |
| 4 | - | onValueChange?: (value: string) => void; | |
| 5 | - | id?: string; | |
| 6 | - | class?: string; | |
| 7 | - | }; | |
| 8 | - | </script> | |
| 9 | - | ||
| 10 | - | <script lang="ts"> | |
| 11 | - | import ColorPicker from "svelte-awesome-color-picker"; | |
| 12 | - | import { Popover } from "bits-ui"; | |
| 13 | - | import { cn } from "$lib/utils.js"; | |
| 14 | - | ||
| 15 | - | let { | |
| 16 | - | class: className, | |
| 17 | - | value = $bindable("#000000"), | |
| 18 | - | onValueChange, | |
| 19 | - | id, | |
| 20 | - | }: ColorPickerProps = $props(); | |
| 21 | - | ||
| 22 | - | let open = $state(false); | |
| 23 | - | ||
| 24 | - | function handleInput(event: { hex: string | null }) { | |
| 25 | - | if (event.hex) { | |
| 26 | - | value = event.hex; | |
| 27 | - | onValueChange?.(event.hex); | |
| 28 | - | } | |
| 29 | - | } | |
| 30 | - | </script> | |
| 31 | - | ||
| 32 | - | <Popover.Root bind:open> | |
| 33 | - | <Popover.Trigger | |
| 34 | - | {id} | |
| 35 | - | class={cn( | |
| 36 | - | "inline-flex h-10 w-12 cursor-pointer items-center justify-center border border-border bg-background transition-colors hover:border-foreground/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", | |
| 37 | - | className | |
| 38 | - | )} | |
| 39 | - | > | |
| 40 | - | <span | |
| 41 | - | class="block h-6 w-8 border border-border/50" | |
| 42 | - | style="background-color: {value};" | |
| 43 | - | aria-hidden="true" | |
| 44 | - | ></span> | |
| 45 | - | <span class="sr-only">Choose color: {value}</span> | |
| 46 | - | </Popover.Trigger> | |
| 47 | - | ||
| 48 | - | <Popover.Portal> | |
| 49 | - | <Popover.Content | |
| 50 | - | class="color-picker-popover z-50 border border-border bg-background p-3 shadow-lg" | |
| 51 | - | sideOffset={4} | |
| 52 | - | > | |
| 53 | - | <ColorPicker | |
| 54 | - | bind:hex={value} | |
| 55 | - | onInput={handleInput} | |
| 56 | - | isDialog={false} | |
| 57 | - | /> | |
| 58 | - | <!-- Hex value display --> | |
| 59 | - | <div class="mt-2 flex items-center gap-2"> | |
| 60 | - | <span | |
| 61 | - | class="block h-6 w-6 shrink-0 border border-border" | |
| 62 | - | style="background-color: {value};" | |
| 63 | - | aria-hidden="true" | |
| 64 | - | ></span> | |
| 65 | - | <span class="font-mono text-sm text-foreground">{value}</span> | |
| 66 | - | </div> | |
| 67 | - | </Popover.Content> | |
| 68 | - | </Popover.Portal> | |
| 69 | - | </Popover.Root> | |
| 70 | - | ||
| 71 | - | <style> | |
| 72 | - | /* Override svelte-awesome-color-picker styles to match our theme */ | |
| 73 | - | :global(.color-picker-popover) { | |
| 74 | - | --cp-bg-color: var(--background); | |
| 75 | - | --cp-border-color: var(--border); | |
| 76 | - | --cp-text-color: var(--foreground); | |
| 77 | - | --cp-input-color: var(--background); | |
| 78 | - | --cp-button-hover-color: var(--accent); | |
| 79 | - | } | |
| 80 | - | </style> | |
D webui/src/lib/components/ui/color-picker/index.ts +0 -8
| @@ -1,8 +0,0 @@ | |||
| 1 | - | import Root, { type ColorPickerProps } from "./color-picker.svelte"; | |
| 2 | - | ||
| 3 | - | export { | |
| 4 | - | Root, | |
| 5 | - | Root as ColorPicker, | |
| 6 | - | type ColorPickerProps, | |
| 7 | - | type ColorPickerProps as Props, | |
| 8 | - | }; | |
D webui/src/lib/components/ui/input/index.ts +0 -7
| @@ -1,7 +0,0 @@ | |||
| 1 | - | import Root from "./input.svelte"; | |
| 2 | - | ||
| 3 | - | export { | |
| 4 | - | Root, | |
| 5 | - | // | |
| 6 | - | Root as Input, | |
| 7 | - | }; | |
D webui/src/lib/components/ui/input/input.svelte +0 -52
| @@ -1,52 +0,0 @@ | |||
| 1 | - | <script lang="ts"> | |
| 2 | - | import type { HTMLInputAttributes, HTMLInputTypeAttribute } from "svelte/elements"; | |
| 3 | - | import { cn, type WithElementRef } from "$lib/utils.js"; | |
| 4 | - | ||
| 5 | - | type InputType = Exclude<HTMLInputTypeAttribute, "file">; | |
| 6 | - | ||
| 7 | - | type Props = WithElementRef< | |
| 8 | - | Omit<HTMLInputAttributes, "type"> & | |
| 9 | - | ({ type: "file"; files?: FileList } | { type?: InputType; files?: undefined }) | |
| 10 | - | >; | |
| 11 | - | ||
| 12 | - | let { | |
| 13 | - | ref = $bindable(null), | |
| 14 | - | value = $bindable(), | |
| 15 | - | type, | |
| 16 | - | files = $bindable(), | |
| 17 | - | class: className, | |
| 18 | - | "data-slot": dataSlot = "input", | |
| 19 | - | ...restProps | |
| 20 | - | }: Props = $props(); | |
| 21 | - | </script> | |
| 22 | - | ||
| 23 | - | {#if type === "file"} | |
| 24 | - | <input | |
| 25 | - | bind:this={ref} | |
| 26 | - | data-slot={dataSlot} | |
| 27 | - | class={cn( | |
| 28 | - | "selection:bg-primary dark:bg-input/30 selection:text-primary-foreground border-border ring-offset-background placeholder:text-muted-foreground flex h-9 w-full min-w-0 border bg-transparent px-3 pt-1.5 text-sm font-medium shadow-xs transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-50", | |
| 29 | - | "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]", | |
| 30 | - | "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", | |
| 31 | - | className | |
| 32 | - | )} | |
| 33 | - | type="file" | |
| 34 | - | bind:files | |
| 35 | - | bind:value | |
| 36 | - | {...restProps} | |
| 37 | - | /> | |
| 38 | - | {:else} | |
| 39 | - | <input | |
| 40 | - | bind:this={ref} | |
| 41 | - | data-slot={dataSlot} | |
| 42 | - | class={cn( | |
| 43 | - | "border-border bg-background selection:bg-primary dark:bg-input/30 selection:text-primary-foreground ring-offset-background placeholder:text-muted-foreground flex h-9 w-full min-w-0 border px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", | |
| 44 | - | "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]", | |
| 45 | - | "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", | |
| 46 | - | className | |
| 47 | - | )} | |
| 48 | - | {type} | |
| 49 | - | bind:value | |
| 50 | - | {...restProps} | |
| 51 | - | /> | |
| 52 | - | {/if} | |
D webui/src/lib/components/ui/label/index.ts +0 -7
| @@ -1,7 +0,0 @@ | |||
| 1 | - | import Root from "./label.svelte"; | |
| 2 | - | ||
| 3 | - | export { | |
| 4 | - | Root, | |
| 5 | - | // | |
| 6 | - | Root as Label, | |
| 7 | - | }; | |
D webui/src/lib/components/ui/label/label.svelte +0 -20
| @@ -1,20 +0,0 @@ | |||
| 1 | - | <script lang="ts"> | |
| 2 | - | import { Label as LabelPrimitive } from "bits-ui"; | |
| 3 | - | import { cn } from "$lib/utils.js"; | |
| 4 | - | ||
| 5 | - | let { | |
| 6 | - | ref = $bindable(null), | |
| 7 | - | class: className, | |
| 8 | - | ...restProps | |
| 9 | - | }: LabelPrimitive.RootProps = $props(); | |
| 10 | - | </script> | |
| 11 | - | ||
| 12 | - | <LabelPrimitive.Root | |
| 13 | - | bind:ref | |
| 14 | - | data-slot="label" | |
| 15 | - | class={cn( | |
| 16 | - | "flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50", | |
| 17 | - | className | |
| 18 | - | )} | |
| 19 | - | {...restProps} | |
| 20 | - | /> | |
D webui/src/lib/components/ui/skeleton/index.ts +0 -7
| @@ -1,7 +0,0 @@ | |||
| 1 | - | import Root from "./skeleton.svelte"; | |
| 2 | - | ||
| 3 | - | export { | |
| 4 | - | Root, | |
| 5 | - | // | |
| 6 | - | Root as Skeleton, | |
| 7 | - | }; | |
D webui/src/lib/components/ui/skeleton/skeleton.svelte +0 -17
| @@ -1,17 +0,0 @@ | |||
| 1 | - | <script lang="ts"> | |
| 2 | - | import { cn, type WithElementRef, type WithoutChildren } from "$lib/utils.js"; | |
| 3 | - | import type { HTMLAttributes } from "svelte/elements"; | |
| 4 | - | ||
| 5 | - | let { | |
| 6 | - | ref = $bindable(null), | |
| 7 | - | class: className, | |
| 8 | - | ...restProps | |
| 9 | - | }: WithoutChildren<WithElementRef<HTMLAttributes<HTMLDivElement>>> = $props(); | |
| 10 | - | </script> | |
| 11 | - | ||
| 12 | - | <div | |
| 13 | - | bind:this={ref} | |
| 14 | - | data-slot="skeleton" | |
| 15 | - | class={cn("bg-accent animate-pulse", className)} | |
| 16 | - | {...restProps} | |
| 17 | - | ></div> | |
D webui/src/lib/components/ui/spinner/index.ts +0 -1
| @@ -1,1 +0,0 @@ | |||
| 1 | - | export { default as Spinner } from "./spinner.svelte"; | |
D webui/src/lib/components/ui/spinner/spinner.svelte +0 -14
| @@ -1,14 +0,0 @@ | |||
| 1 | - | <script lang="ts"> | |
| 2 | - | import { cn } from "$lib/utils.js"; | |
| 3 | - | import Loader2Icon from "@lucide/svelte/icons/loader-2"; | |
| 4 | - | import type { ComponentProps } from "svelte"; | |
| 5 | - | ||
| 6 | - | let { class: className, ...restProps }: ComponentProps<typeof Loader2Icon> = $props(); | |
| 7 | - | </script> | |
| 8 | - | ||
| 9 | - | <Loader2Icon | |
| 10 | - | role="status" | |
| 11 | - | aria-label="Loading" | |
| 12 | - | class={cn("size-4 animate-spin", className)} | |
| 13 | - | {...restProps} | |
| 14 | - | /> | |
D webui/src/lib/components/ui/table/index.ts +0 -28
| @@ -1,28 +0,0 @@ | |||
| 1 | - | import Root from "./table.svelte"; | |
| 2 | - | import Body from "./table-body.svelte"; | |
| 3 | - | import Caption from "./table-caption.svelte"; | |
| 4 | - | import Cell from "./table-cell.svelte"; | |
| 5 | - | import Footer from "./table-footer.svelte"; | |
| 6 | - | import Head from "./table-head.svelte"; | |
| 7 | - | import Header from "./table-header.svelte"; | |
| 8 | - | import Row from "./table-row.svelte"; | |
| 9 | - | ||
| 10 | - | export { | |
| 11 | - | Root, | |
| 12 | - | Body, | |
| 13 | - | Caption, | |
| 14 | - | Cell, | |
| 15 | - | Footer, | |
| 16 | - | Head, | |
| 17 | - | Header, | |
| 18 | - | Row, | |
| 19 | - | // | |
| 20 | - | Root as Table, | |
| 21 | - | Body as TableBody, | |
| 22 | - | Caption as TableCaption, | |
| 23 | - | Cell as TableCell, | |
| 24 | - | Footer as TableFooter, | |
| 25 | - | Head as TableHead, | |
| 26 | - | Header as TableHeader, | |
| 27 | - | Row as TableRow, | |
| 28 | - | }; | |
D webui/src/lib/components/ui/table/table-body.svelte +0 -20
| @@ -1,20 +0,0 @@ | |||
| 1 | - | <script lang="ts"> | |
| 2 | - | import { cn, type WithElementRef } from "$lib/utils.js"; | |
| 3 | - | import type { HTMLAttributes } from "svelte/elements"; | |
| 4 | - | ||
| 5 | - | let { | |
| 6 | - | ref = $bindable(null), | |
| 7 | - | class: className, | |
| 8 | - | children, | |
| 9 | - | ...restProps | |
| 10 | - | }: WithElementRef<HTMLAttributes<HTMLTableSectionElement>> = $props(); | |
| 11 | - | </script> | |
| 12 | - | ||
| 13 | - | <tbody | |
| 14 | - | bind:this={ref} | |
| 15 | - | data-slot="table-body" | |
| 16 | - | class={cn("[&_tr:last-child]:border-0", className)} | |
| 17 | - | {...restProps} | |
| 18 | - | > | |
| 19 | - | {@render children?.()} | |
| 20 | - | </tbody> | |
D webui/src/lib/components/ui/table/table-caption.svelte +0 -20
| @@ -1,20 +0,0 @@ | |||
| 1 | - | <script lang="ts"> | |
| 2 | - | import { cn, type WithElementRef } from "$lib/utils.js"; | |
| 3 | - | import type { HTMLAttributes } from "svelte/elements"; | |
| 4 | - | ||
| 5 | - | let { | |
| 6 | - | ref = $bindable(null), | |
| 7 | - | class: className, | |
| 8 | - | children, | |
| 9 | - | ...restProps | |
| 10 | - | }: WithElementRef<HTMLAttributes<HTMLElement>> = $props(); | |
| 11 | - | </script> | |
| 12 | - | ||
| 13 | - | <caption | |
| 14 | - | bind:this={ref} | |
| 15 | - | data-slot="table-caption" | |
| 16 | - | class={cn("text-muted-foreground mt-4 text-sm", className)} | |
| 17 | - | {...restProps} | |
| 18 | - | > | |
| 19 | - | {@render children?.()} | |
| 20 | - | </caption> | |
D webui/src/lib/components/ui/table/table-cell.svelte +0 -23
| @@ -1,23 +0,0 @@ | |||
| 1 | - | <script lang="ts"> | |
| 2 | - | import { cn, type WithElementRef } from "$lib/utils.js"; | |
| 3 | - | import type { HTMLTdAttributes } from "svelte/elements"; | |
| 4 | - | ||
| 5 | - | let { | |
| 6 | - | ref = $bindable(null), | |
| 7 | - | class: className, | |
| 8 | - | children, | |
| 9 | - | ...restProps | |
| 10 | - | }: WithElementRef<HTMLTdAttributes> = $props(); | |
| 11 | - | </script> | |
| 12 | - | ||
| 13 | - | <td | |
| 14 | - | bind:this={ref} | |
| 15 | - | data-slot="table-cell" | |
| 16 | - | class={cn( | |
| 17 | - | "bg-clip-padding p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pe-0", | |
| 18 | - | className | |
| 19 | - | )} | |
| 20 | - | {...restProps} | |
| 21 | - | > | |
| 22 | - | {@render children?.()} | |
| 23 | - | </td> | |
D webui/src/lib/components/ui/table/table-footer.svelte +0 -20
| @@ -1,20 +0,0 @@ | |||
| 1 | - | <script lang="ts"> | |
| 2 | - | import { cn, type WithElementRef } from "$lib/utils.js"; | |
| 3 | - | import type { HTMLAttributes } from "svelte/elements"; | |
| 4 | - | ||
| 5 | - | let { | |
| 6 | - | ref = $bindable(null), | |
| 7 | - | class: className, | |
| 8 | - | children, | |
| 9 | - | ...restProps | |
| 10 | - | }: WithElementRef<HTMLAttributes<HTMLTableSectionElement>> = $props(); | |
| 11 | - | </script> | |
| 12 | - | ||
| 13 | - | <tfoot | |
| 14 | - | bind:this={ref} | |
| 15 | - | data-slot="table-footer" | |
| 16 | - | class={cn("bg-muted/50 border-t font-medium [&>tr]:last:border-b-0", className)} | |
| 17 | - | {...restProps} | |
| 18 | - | > | |
| 19 | - | {@render children?.()} | |
| 20 | - | </tfoot> | |
D webui/src/lib/components/ui/table/table-head.svelte +0 -23
| @@ -1,23 +0,0 @@ | |||
| 1 | - | <script lang="ts"> | |
| 2 | - | import { cn, type WithElementRef } from "$lib/utils.js"; | |
| 3 | - | import type { HTMLThAttributes } from "svelte/elements"; | |
| 4 | - | ||
| 5 | - | let { | |
| 6 | - | ref = $bindable(null), | |
| 7 | - | class: className, | |
| 8 | - | children, | |
| 9 | - | ...restProps | |
| 10 | - | }: WithElementRef<HTMLThAttributes> = $props(); | |
| 11 | - | </script> | |
| 12 | - | ||
| 13 | - | <th | |
| 14 | - | bind:this={ref} | |
| 15 | - | data-slot="table-head" | |
| 16 | - | class={cn( | |
| 17 | - | "text-foreground h-10 bg-clip-padding px-2 text-start align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pe-0", | |
| 18 | - | className | |
| 19 | - | )} | |
| 20 | - | {...restProps} | |
| 21 | - | > | |
| 22 | - | {@render children?.()} | |
| 23 | - | </th> | |
D webui/src/lib/components/ui/table/table-header.svelte +0 -20
| @@ -1,20 +0,0 @@ | |||
| 1 | - | <script lang="ts"> | |
| 2 | - | import { cn, type WithElementRef } from "$lib/utils.js"; | |
| 3 | - | import type { HTMLAttributes } from "svelte/elements"; | |
| 4 | - | ||
| 5 | - | let { | |
| 6 | - | ref = $bindable(null), | |
| 7 | - | class: className, | |
| 8 | - | children, | |
| 9 | - | ...restProps | |
| 10 | - | }: WithElementRef<HTMLAttributes<HTMLTableSectionElement>> = $props(); | |
| 11 | - | </script> | |
| 12 | - | ||
| 13 | - | <thead | |
| 14 | - | bind:this={ref} | |
| 15 | - | data-slot="table-header" | |
| 16 | - | class={cn("[&_tr]:border-b", className)} | |
| 17 | - | {...restProps} | |
| 18 | - | > | |
| 19 | - | {@render children?.()} | |
| 20 | - | </thead> | |
D webui/src/lib/components/ui/table/table-row.svelte +0 -23
| @@ -1,23 +0,0 @@ | |||
| 1 | - | <script lang="ts"> | |
| 2 | - | import { cn, type WithElementRef } from "$lib/utils.js"; | |
| 3 | - | import type { HTMLAttributes } from "svelte/elements"; | |
| 4 | - | ||
| 5 | - | let { | |
| 6 | - | ref = $bindable(null), | |
| 7 | - | class: className, | |
| 8 | - | children, | |
| 9 | - | ...restProps | |
| 10 | - | }: WithElementRef<HTMLAttributes<HTMLTableRowElement>> = $props(); | |
| 11 | - | </script> | |
| 12 | - | ||
| 13 | - | <tr | |
| 14 | - | bind:this={ref} | |
| 15 | - | data-slot="table-row" | |
| 16 | - | class={cn( | |
| 17 | - | "hover:[&,&>svelte-css-wrapper]:[&>th,td]:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors", | |
| 18 | - | className | |
| 19 | - | )} | |
| 20 | - | {...restProps} | |
| 21 | - | > | |
| 22 | - | {@render children?.()} | |
| 23 | - | </tr> | |
D webui/src/lib/components/ui/table/table.svelte +0 -22
| @@ -1,22 +0,0 @@ | |||
| 1 | - | <script lang="ts"> | |
| 2 | - | import type { HTMLTableAttributes } from "svelte/elements"; | |
| 3 | - | import { cn, type WithElementRef } from "$lib/utils.js"; | |
| 4 | - | ||
| 5 | - | let { | |
| 6 | - | ref = $bindable(null), | |
| 7 | - | class: className, | |
| 8 | - | children, | |
| 9 | - | ...restProps | |
| 10 | - | }: WithElementRef<HTMLTableAttributes> = $props(); | |
| 11 | - | </script> | |
| 12 | - | ||
| 13 | - | <div data-slot="table-container" class="relative w-full overflow-x-auto"> | |
| 14 | - | <table | |
| 15 | - | bind:this={ref} | |
| 16 | - | data-slot="table" | |
| 17 | - | class={cn("w-full caption-bottom text-sm", className)} | |
| 18 | - | {...restProps} | |
| 19 | - | > | |
| 20 | - | {@render children?.()} | |
| 21 | - | </table> | |
| 22 | - | </div> | |
D webui/src/lib/components/ui/tabs/index.ts +0 -16
| @@ -1,16 +0,0 @@ | |||
| 1 | - | import Root from "./tabs.svelte"; | |
| 2 | - | import Content from "./tabs-content.svelte"; | |
| 3 | - | import List from "./tabs-list.svelte"; | |
| 4 | - | import Trigger from "./tabs-trigger.svelte"; | |
| 5 | - | ||
| 6 | - | export { | |
| 7 | - | Root, | |
| 8 | - | Content, | |
| 9 | - | List, | |
| 10 | - | Trigger, | |
| 11 | - | // | |
| 12 | - | Root as Tabs, | |
| 13 | - | Content as TabsContent, | |
| 14 | - | List as TabsList, | |
| 15 | - | Trigger as TabsTrigger, | |
| 16 | - | }; | |
D webui/src/lib/components/ui/tabs/tabs-content.svelte +0 -17
| @@ -1,17 +0,0 @@ | |||
| 1 | - | <script lang="ts"> | |
| 2 | - | import { Tabs as TabsPrimitive } from "bits-ui"; | |
| 3 | - | import { cn } from "$lib/utils.js"; | |
| 4 | - | ||
| 5 | - | let { | |
| 6 | - | ref = $bindable(null), | |
| 7 | - | class: className, | |
| 8 | - | ...restProps | |
| 9 | - | }: TabsPrimitive.ContentProps = $props(); | |
| 10 | - | </script> | |
| 11 | - | ||
| 12 | - | <TabsPrimitive.Content | |
| 13 | - | bind:ref | |
| 14 | - | data-slot="tabs-content" | |
| 15 | - | class={cn("flex-1 outline-none", className)} | |
| 16 | - | {...restProps} | |
| 17 | - | /> | |
D webui/src/lib/components/ui/tabs/tabs-list.svelte +0 -20
| @@ -1,20 +0,0 @@ | |||
| 1 | - | <script lang="ts"> | |
| 2 | - | import { Tabs as TabsPrimitive } from "bits-ui"; | |
| 3 | - | import { cn } from "$lib/utils.js"; | |
| 4 | - | ||
| 5 | - | let { | |
| 6 | - | ref = $bindable(null), | |
| 7 | - | class: className, | |
| 8 | - | ...restProps | |
| 9 | - | }: TabsPrimitive.ListProps = $props(); | |
| 10 | - | </script> | |
| 11 | - | ||
| 12 | - | <TabsPrimitive.List | |
| 13 | - | bind:ref | |
| 14 | - | data-slot="tabs-list" | |
| 15 | - | class={cn( | |
| 16 | - | "bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center border border-border p-[3px]", | |
| 17 | - | className | |
| 18 | - | )} | |
| 19 | - | {...restProps} | |
| 20 | - | /> | |
D webui/src/lib/components/ui/tabs/tabs-trigger.svelte +0 -20
| @@ -1,20 +0,0 @@ | |||
| 1 | - | <script lang="ts"> | |
| 2 | - | import { Tabs as TabsPrimitive } from "bits-ui"; | |
| 3 | - | import { cn } from "$lib/utils.js"; | |
| 4 | - | ||
| 5 | - | let { | |
| 6 | - | ref = $bindable(null), | |
| 7 | - | class: className, | |
| 8 | - | ...restProps | |
| 9 | - | }: TabsPrimitive.TriggerProps = $props(); | |
| 10 | - | </script> | |
| 11 | - | ||
| 12 | - | <TabsPrimitive.Trigger | |
| 13 | - | bind:ref | |
| 14 | - | data-slot="tabs-trigger" | |
| 15 | - | class={cn( | |
| 16 | - | "data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", | |
| 17 | - | className | |
| 18 | - | )} | |
| 19 | - | {...restProps} | |
| 20 | - | /> | |
D webui/src/lib/components/ui/tabs/tabs.svelte +0 -19
| @@ -1,19 +0,0 @@ | |||
| 1 | - | <script lang="ts"> | |
| 2 | - | import { Tabs as TabsPrimitive } from "bits-ui"; | |
| 3 | - | import { cn } from "$lib/utils.js"; | |
| 4 | - | ||
| 5 | - | let { | |
| 6 | - | ref = $bindable(null), | |
| 7 | - | value = $bindable(""), | |
| 8 | - | class: className, | |
| 9 | - | ...restProps | |
| 10 | - | }: TabsPrimitive.RootProps = $props(); | |
| 11 | - | </script> | |
| 12 | - | ||
| 13 | - | <TabsPrimitive.Root | |
| 14 | - | bind:ref | |
| 15 | - | bind:value | |
| 16 | - | data-slot="tabs" | |
| 17 | - | class={cn("flex flex-col gap-2", className)} | |
| 18 | - | {...restProps} | |
| 19 | - | /> | |
D webui/src/lib/types.ts +0 -68
| @@ -1,68 +0,0 @@ | |||
| 1 | - | import type { HistoryEntry, SearchShortcut } from "../types/generated.js"; | |
| 2 | - | ||
| 3 | - | type ThemeManagerAPI = { | |
| 4 | - | setUserPreference: (theme: "light" | "dark") => void; | |
| 5 | - | getCurrentTheme: () => "light" | "dark"; | |
| 6 | - | }; | |
| 7 | - | ||
| 8 | - | // Global window API types for browser integration | |
| 9 | - | declare global { | |
| 10 | - | interface Window { | |
| 11 | - | // Toast system API | |
| 12 | - | __dumber_toast_loaded?: boolean; | |
| 13 | - | __dumber_showToast?: ( | |
| 14 | - | message: string, | |
| 15 | - | duration?: number, | |
| 16 | - | type?: "info" | "success" | "error", | |
| 17 | - | ) => number | void; | |
| 18 | - | __dumber_dismissToast?: (id: number) => void; | |
| 19 | - | __dumber_clearToasts?: () => void; | |
| 20 | - | __dumber_showZoomToast?: (zoomLevel: number) => void; | |
| 21 | - | ||
| 22 | - | // Theme integration | |
| 23 | - | __dumber_initial_theme?: string; | |
| 24 | - | __dumber_setTheme?: (theme: "light" | "dark") => void; | |
| 25 | - | __dumber_color_scheme_manager?: ThemeManagerAPI; | |
| 26 | - | ||
| 27 | - | // History callbacks | |
| 28 | - | __dumber_history_recent?: (data: HistoryEntry[]) => void; | |
| 29 | - | __dumber_history_error?: (error: string) => void; | |
| 30 | - | ||
| 31 | - | // Shortcuts callbacks | |
| 32 | - | __dumber_shortcuts_loaded?: (data: Record<string, SearchShortcut>) => void; | |
| 33 | - | __dumber_shortcuts_error?: (error: string) => void; | |
| 34 | - | ||
| 35 | - | // WebKit message handler | |
| 36 | - | webkit?: { | |
| 37 | - | messageHandlers?: { | |
| 38 | - | dumber?: { | |
| 39 | - | postMessage: (message: unknown) => void; | |
| 40 | - | }; | |
| 41 | - | }; | |
| 42 | - | }; | |
| 43 | - | } | |
| 44 | - | } | |
| 45 | - | ||
| 46 | - | export interface ToastAPI { | |
| 47 | - | show: ( | |
| 48 | - | message: string, | |
| 49 | - | duration?: number, | |
| 50 | - | type?: "info" | "success" | "error", | |
| 51 | - | ) => number; | |
| 52 | - | dismiss: (id: number) => void; | |
| 53 | - | clear: () => void; | |
| 54 | - | showZoom: (zoomLevel: number) => void; | |
| 55 | - | } | |
| 56 | - | ||
| 57 | - | export interface ThemeAPI { | |
| 58 | - | setTheme: (theme: "light" | "dark") => void; | |
| 59 | - | getCurrentTheme: () => "light" | "dark"; | |
| 60 | - | } | |
| 61 | - | ||
| 62 | - | // Message types for WebKit communication | |
| 63 | - | export interface BrowserMessage { | |
| 64 | - | action: string; | |
| 65 | - | data?: unknown; | |
| 66 | - | } | |
| 67 | - | ||
| 68 | - | export {}; // Make this file a module | |
D webui/src/lib/utils.ts +0 -13
| @@ -1,13 +0,0 @@ | |||
| 1 | - | import { clsx, type ClassValue } from "clsx"; | |
| 2 | - | import { twMerge } from "tailwind-merge"; | |
| 3 | - | ||
| 4 | - | export function cn(...inputs: ClassValue[]) { | |
| 5 | - | return twMerge(clsx(inputs)); | |
| 6 | - | } | |
| 7 | - | ||
| 8 | - | // eslint-disable-next-line @typescript-eslint/no-explicit-any | |
| 9 | - | export type WithoutChild<T> = T extends { child?: any } ? Omit<T, "child"> : T; | |
| 10 | - | // eslint-disable-next-line @typescript-eslint/no-explicit-any | |
| 11 | - | export type WithoutChildren<T> = T extends { children?: any } ? Omit<T, "children"> : T; | |
| 12 | - | export type WithoutChildrenOrChild<T> = WithoutChildren<WithoutChild<T>>; | |
| 13 | - | export type WithElementRef<T, U extends HTMLElement = HTMLElement> = T & { ref?: U | null }; | |
D webui/src/lib/utils/keys.ts +0 -51
| @@ -1,51 +0,0 @@ | |||
| 1 | - | /** | |
| 2 | - | * Format a keybinding string for display. | |
| 3 | - | * Converts internal key names to user-friendly display format. | |
| 4 | - | * | |
| 5 | - | * Examples: | |
| 6 | - | * "ctrl+shift+a" -> "Ctrl + Shift + A" | |
| 7 | - | * "arrowleft" -> "Left" | |
| 8 | - | * "alt+[" -> "Alt + [" | |
| 9 | - | */ | |
| 10 | - | export function formatKey(key: string): string { | |
| 11 | - | // Split by '+' separator first, then format each part | |
| 12 | - | // This avoids replacing '+' characters that are actual key names | |
| 13 | - | const parts = key.split("+"); | |
| 14 | - | ||
| 15 | - | const formattedParts = parts.map((part) => { | |
| 16 | - | const lower = part.toLowerCase(); | |
| 17 | - | switch (lower) { | |
| 18 | - | case "arrowleft": | |
| 19 | - | return "Left"; | |
| 20 | - | case "arrowright": | |
| 21 | - | return "Right"; | |
| 22 | - | case "arrowup": | |
| 23 | - | return "Up"; | |
| 24 | - | case "arrowdown": | |
| 25 | - | return "Down"; | |
| 26 | - | case "ctrl": | |
| 27 | - | return "Ctrl"; | |
| 28 | - | case "alt": | |
| 29 | - | return "Alt"; | |
| 30 | - | case "shift": | |
| 31 | - | return "Shift"; | |
| 32 | - | case "space": | |
| 33 | - | return "Space"; | |
| 34 | - | case "enter": | |
| 35 | - | return "Enter"; | |
| 36 | - | case "tab": | |
| 37 | - | return "Tab"; | |
| 38 | - | case "backspace": | |
| 39 | - | return "Backspace"; | |
| 40 | - | case "escape": | |
| 41 | - | return "Esc"; | |
| 42 | - | default: | |
| 43 | - | // Capitalize single letters, leave symbols as-is | |
| 44 | - | return part.length === 1 && /[a-z]/i.test(part) | |
| 45 | - | ? part.toUpperCase() | |
| 46 | - | : part; | |
| 47 | - | } | |
| 48 | - | }); | |
| 49 | - | ||
| 50 | - | return formattedParts.join(" + "); | |
| 51 | - | } | |
D webui/src/pages/ConfigPage.svelte +0 -761
| @@ -1,761 +0,0 @@ | |||
| 1 | - | <script lang="ts"> | |
| 2 | - | import { onMount } from "svelte"; | |
| 3 | - | import { ModeWatcher, resetMode, setMode } from "mode-watcher"; | |
| 4 | - | import ConfigShell from "./config/ConfigShell.svelte"; | |
| 5 | - | import ShortcutsTable from "./config/ShortcutsTable.svelte"; | |
| 6 | - | import KeybindingsTab from "./config/KeybindingsTab.svelte"; | |
| 7 | - | import * as AlertDialog from "$lib/components/ui/alert-dialog"; | |
| 8 | - | import { Button } from "$lib/components/ui/button"; | |
| 9 | - | import { ColorPicker } from "$lib/components/ui/color-picker"; | |
| 10 | - | import { Input } from "$lib/components/ui/input"; | |
| 11 | - | import { Label } from "$lib/components/ui/label"; | |
| 12 | - | import { Spinner } from "$lib/components/ui/spinner"; | |
| 13 | - | import * as Card from "$lib/components/ui/card"; | |
| 14 | - | import * as Tabs from "$lib/components/ui/tabs"; | |
| 15 | - | import { FlaskConical, RefreshCw } from "@lucide/svelte"; | |
| 16 | - | ||
| 17 | - | function getErrorMessage(e: unknown): string { | |
| 18 | - | if (e instanceof Error) return e.message; | |
| 19 | - | if (typeof e === "string") return e; | |
| 20 | - | return "Unknown error"; | |
| 21 | - | } | |
| 22 | - | ||
| 23 | - | type ColorPalette = { | |
| 24 | - | background: string; | |
| 25 | - | surface: string; | |
| 26 | - | surface_variant: string; | |
| 27 | - | text: string; | |
| 28 | - | muted: string; | |
| 29 | - | accent: string; | |
| 30 | - | border: string; | |
| 31 | - | }; | |
| 32 | - | ||
| 33 | - | type SearchShortcut = { | |
| 34 | - | url: string; | |
| 35 | - | description: string; | |
| 36 | - | }; | |
| 37 | - | ||
| 38 | - | type ResolvedPerformance = { | |
| 39 | - | skia_cpu_threads: number; | |
| 40 | - | skia_gpu_threads: number; | |
| 41 | - | web_process_memory_mb: number; | |
| 42 | - | network_process_memory_mb: number; | |
| 43 | - | webview_pool_prewarm: number; | |
| 44 | - | conservative_threshold: number; | |
| 45 | - | strict_threshold: number; | |
| 46 | - | }; | |
| 47 | - | ||
| 48 | - | type HardwareInfo = { | |
| 49 | - | cpu_cores: number; | |
| 50 | - | cpu_threads: number; | |
| 51 | - | total_ram_mb: number; | |
| 52 | - | gpu_vendor: string; | |
| 53 | - | gpu_name: string; | |
| 54 | - | vram_mb: number; | |
| 55 | - | }; | |
| 56 | - | ||
| 57 | - | type CustomPerformance = { | |
| 58 | - | skia_cpu_threads: number; | |
| 59 | - | skia_gpu_threads: number; | |
| 60 | - | web_process_memory_mb: number; | |
| 61 | - | network_process_memory_mb: number; | |
| 62 | - | webview_pool_prewarm: number; | |
| 63 | - | }; | |
| 64 | - | ||
| 65 | - | type PerformanceConfig = { | |
| 66 | - | profile: string; | |
| 67 | - | custom: CustomPerformance; | |
| 68 | - | resolved: ResolvedPerformance; | |
| 69 | - | hardware: HardwareInfo; | |
| 70 | - | }; | |
| 71 | - | ||
| 72 | - | type ConfigDTO = { | |
| 73 | - | appearance: { | |
| 74 | - | sans_font: string; | |
| 75 | - | serif_font: string; | |
| 76 | - | monospace_font: string; | |
| 77 | - | default_font_size: number; | |
| 78 | - | color_scheme: string; | |
| 79 | - | light_palette: ColorPalette; | |
| 80 | - | dark_palette: ColorPalette; | |
| 81 | - | }; | |
| 82 | - | performance: PerformanceConfig; | |
| 83 | - | default_ui_scale: number; | |
| 84 | - | default_search_engine: string; | |
| 85 | - | search_shortcuts: Record<string, SearchShortcut>; | |
| 86 | - | engine_type: string; | |
| 87 | - | }; | |
| 88 | - | ||
| 89 | - | const PERFORMANCE_PROFILES = [ | |
| 90 | - | { value: "default", label: "Default", description: "No tuning, uses WebKit defaults. Recommended for most users." }, | |
| 91 | - | { value: "lite", label: "Lite", description: "Reduced resource usage for low-RAM systems (< 4GB) or battery saving." }, | |
| 92 | - | { value: "max", label: "Max", description: "Maximum responsiveness, scales based on detected hardware." }, | |
| 93 | - | { value: "custom", label: "Custom", description: "Manual control via config file. Edit config.toml to set individual values." }, | |
| 94 | - | ]; | |
| 95 | - | ||
| 96 | - | const PALETTE_FIELDS: Array<{ key: keyof ColorPalette; label: string }> = [ | |
| 97 | - | { key: "background", label: "Background" }, | |
| 98 | - | { key: "surface", label: "Surface" }, | |
| 99 | - | { key: "surface_variant", label: "Surface Variant" }, | |
| 100 | - | { key: "text", label: "Text" }, | |
| 101 | - | { key: "muted", label: "Muted" }, | |
| 102 | - | { key: "accent", label: "Accent" }, | |
| 103 | - | { key: "border", label: "Border" }, | |
| 104 | - | ]; | |
| 105 | - | ||
| 106 | - | function isHexColor(value: string): boolean { | |
| 107 | - | return /^#[0-9a-fA-F]{6}$/.test(value); | |
| 108 | - | } | |
| 109 | - | ||
| 110 | - | // UI state | |
| 111 | - | let loading = $state(true); | |
| 112 | - | let loadError = $state<string | null>(null); | |
| 113 | - | let config = $state<ConfigDTO | null>(null); | |
| 114 | - | let saving = $state(false); | |
| 115 | - | let saveSuccess = $state(false); | |
| 116 | - | let saveError = $state<string | null>(null); | |
| 117 | - | let showRestartWarning = $state(false); | |
| 118 | - | let themeEvents = $state(0); | |
| 119 | - | let activeTab = $state("appearance"); | |
| 120 | - | ||
| 121 | - | function getResolvedDarkPreference(fallback?: boolean): boolean { | |
| 122 | - | if (typeof fallback === "boolean") { | |
| 123 | - | return fallback; | |
| 124 | - | } | |
| 125 | - | if (typeof window.__dumber_cef_prefers_dark === "boolean") { | |
| 126 | - | return window.__dumber_cef_prefers_dark; | |
| 127 | - | } | |
| 128 | - | if (typeof window.__dumber_gtk_prefers_dark === "boolean") { | |
| 129 | - | return window.__dumber_gtk_prefers_dark; | |
| 130 | - | } | |
| 131 | - | return document.documentElement.classList.contains("dark"); | |
| 132 | - | } | |
| 133 | - | ||
| 134 | - | function applyResolvedTheme(prefersDark: boolean): void { | |
| 135 | - | const root = document.documentElement; | |
| 136 | - | root.classList.toggle("dark", prefersDark); | |
| 137 | - | root.classList.toggle("light", !prefersDark); | |
| 138 | - | root.style.colorScheme = prefersDark ? "dark" : "light"; | |
| 139 | - | window.__dumber_cef_prefers_dark = prefersDark; | |
| 140 | - | window.__dumber_gtk_prefers_dark = prefersDark; | |
| 141 | - | } | |
| 142 | - | ||
| 143 | - | function syncModeWatcherPreference(colorScheme: string | undefined, prefersDark?: boolean): void { | |
| 144 | - | switch (colorScheme) { | |
| 145 | - | case "prefer-dark": | |
| 146 | - | setMode("dark"); | |
| 147 | - | applyResolvedTheme(true); | |
| 148 | - | break; | |
| 149 | - | case "prefer-light": | |
| 150 | - | setMode("light"); | |
| 151 | - | applyResolvedTheme(false); | |
| 152 | - | break; | |
| 153 | - | default: | |
| 154 | - | resetMode(); | |
| 155 | - | applyResolvedTheme(getResolvedDarkPreference(prefersDark)); | |
| 156 | - | break; | |
| 157 | - | } | |
| 158 | - | } | |
| 159 | - | ||
| 160 | - | function isPerformanceSupported(cfg: ConfigDTO | null): boolean { | |
| 161 | - | return cfg != null && cfg.engine_type !== "cef"; | |
| 162 | - | } | |
| 163 | - | ||
| 164 | - | // Load config from API | |
| 165 | - | async function loadConfig() { | |
| 166 | - | loading = true; | |
| 167 | - | loadError = null; | |
| 168 | - | try { | |
| 169 | - | const response = await fetch("/api/config"); | |
| 170 | - | if (!response.ok) throw new Error("Failed to fetch config"); | |
| 171 | - | config = (await response.json()) as ConfigDTO; | |
| 172 | - | syncModeWatcherPreference(config.appearance.color_scheme); | |
| 173 | - | if (!isPerformanceSupported(config) && activeTab === "performance") { | |
| 174 | - | activeTab = "appearance"; | |
| 175 | - | } | |
| 176 | - | } catch (e: unknown) { | |
| 177 | - | loadError = getErrorMessage(e); | |
| 178 | - | console.error("[config] load failed", e); | |
| 179 | - | } finally { | |
| 180 | - | loading = false; | |
| 181 | - | } | |
| 182 | - | } | |
| 183 | - | ||
| 184 | - | // Reload config (used after save to get normalized values) | |
| 185 | - | function reloadConfig() { | |
| 186 | - | loadConfig(); | |
| 187 | - | } | |
| 188 | - | ||
| 189 | - | let resetDialogOpen = $state(false); | |
| 190 | - | ||
| 191 | - | async function doResetToDefaults() { | |
| 192 | - | saveSuccess = false; | |
| 193 | - | try { | |
| 194 | - | const response = await fetch("/api/config/default"); | |
| 195 | - | if (!response.ok) throw new Error("Failed to fetch default config"); | |
| 196 | - | config = (await response.json()) as ConfigDTO; | |
| 197 | - | syncModeWatcherPreference(config.appearance.color_scheme); | |
| 198 | - | if (!isPerformanceSupported(config) && activeTab === "performance") { | |
| 199 | - | activeTab = "appearance"; | |
| 200 | - | } | |
| 201 | - | } catch (e: unknown) { | |
| 202 | - | console.error("[config] reset defaults failed", e); | |
| 203 | - | } | |
| 204 | - | resetDialogOpen = false; | |
| 205 | - | } | |
| 206 | - | ||
| 207 | - | function getWebKitBridge(): { postMessage: (msg: unknown) => void } | null { | |
| 208 | - | const bridge = window.webkit?.messageHandlers?.dumber ?? window.dumber; | |
| 209 | - | if (bridge && typeof bridge.postMessage === "function") { | |
| 210 | - | return bridge; | |
| 211 | - | } | |
| 212 | - | return null; | |
| 213 | - | } | |
| 214 | - | ||
| 215 | - | function getWebViewId(): number { | |
| 216 | - | return window.__dumber_webview_id || 0; | |
| 217 | - | } | |
| 218 | - | ||
| 219 | - | async function saveConfig() { | |
| 220 | - | saving = true; | |
| 221 | - | saveSuccess = false; | |
| 222 | - | saveError = null; | |
| 223 | - | ||
| 224 | - | try { | |
| 225 | - | const bridge = getWebKitBridge(); | |
| 226 | - | const webviewId = getWebViewId(); | |
| 227 | - | ||
| 228 | - | console.debug("[config] save click", { | |
| 229 | - | hasBridge: Boolean(bridge), | |
| 230 | - | webviewId, | |
| 231 | - | hasConfig: Boolean(config), | |
| 232 | - | }); | |
| 233 | - | ||
| 234 | - | if (!bridge) { | |
| 235 | - | throw new Error("Dumber bridge not available (not running inside Dumber)"); | |
| 236 | - | } | |
| 237 | - | if (!config) { | |
| 238 | - | throw new Error("Config not loaded"); | |
| 239 | - | } | |
| 240 | - | ||
| 241 | - | let timeout: number | null = window.setTimeout(() => { | |
| 242 | - | if (saving) { | |
| 243 | - | console.warn("[config] save timed out (no callback)"); | |
| 244 | - | saveError = "Save timed out (no response from native handler)"; | |
| 245 | - | saving = false; | |
| 246 | - | } | |
| 247 | - | }, 8000); | |
| 248 | - | ||
| 249 | - | const clearSaveTimeout = () => { | |
| 250 | - | if (timeout != null) { | |
| 251 | - | window.clearTimeout(timeout); | |
| 252 | - | timeout = null; | |
| 253 | - | } | |
| 254 | - | }; | |
| 255 | - | ||
| 256 | - | window.__dumber_config_saved = (resp?: unknown) => { | |
| 257 | - | clearSaveTimeout(); | |
| 258 | - | console.debug("[config] save success", resp); | |
| 259 | - | saveSuccess = true; | |
| 260 | - | saving = false; | |
| 261 | - | ||
| 262 | - | // Show restart warning if performance tab was active | |
| 263 | - | if (activeTab === "performance" && isPerformanceSupported(config)) { | |
| 264 | - | showRestartWarning = true; | |
| 265 | - | } | |
| 266 | - | ||
| 267 | - | syncModeWatcherPreference(config?.appearance.color_scheme); | |
| 268 | - | ||
| 269 | - | // Refresh config from backend (in case watcher normalized values) | |
| 270 | - | reloadConfig(); | |
| 271 | - | ||
| 272 | - | setTimeout(() => { | |
| 273 | - | saveSuccess = false; | |
| 274 | - | }, 3000); | |
| 275 | - | }; | |
| 276 | - | window.__dumber_config_error = (msg: unknown) => { | |
| 277 | - | clearSaveTimeout(); | |
| 278 | - | console.error("[config] save error", msg); | |
| 279 | - | saveError = typeof msg === "string" ? msg : "Failed to save config"; | |
| 280 | - | saving = false; | |
| 281 | - | }; | |
| 282 | - | ||
| 283 | - | const payload = $state.snapshot(config); | |
| 284 | - | ||
| 285 | - | try { | |
| 286 | - | bridge.postMessage({ | |
| 287 | - | type: "save_config", | |
| 288 | - | webview_id: webviewId, | |
| 289 | - | payload, | |
| 290 | - | }); | |
| 291 | - | } catch (postErr) { | |
| 292 | - | console.error("[config] postMessage threw", postErr); | |
| 293 | - | throw postErr; | |
| 294 | - | } | |
| 295 | - | } catch (e: unknown) { | |
| 296 | - | console.error("[config] save exception", e); | |
| 297 | - | saveError = getErrorMessage(e); | |
| 298 | - | saving = false; | |
| 299 | - | } | |
| 300 | - | } | |
| 301 | - | ||
| 302 | - | function updateShortcuts(nextShortcuts: Record<string, SearchShortcut>) { | |
| 303 | - | if (!config) return; | |
| 304 | - | config.search_shortcuts = nextShortcuts; | |
| 305 | - | } | |
| 306 | - | ||
| 307 | - | onMount(() => { | |
| 308 | - | console.debug("[config] mount", { | |
| 309 | - | webviewId: window.__dumber_webview_id, | |
| 310 | - | hasBridge: Boolean(getWebKitBridge()), | |
| 311 | - | }); | |
| 312 | - | ||
| 313 | - | const onThemeChanged = (e: Event) => { | |
| 314 | - | themeEvents += 1; | |
| 315 | - | const detail = (e as CustomEvent<{ prefersDark?: boolean }>).detail; | |
| 316 | - | syncModeWatcherPreference(config?.appearance.color_scheme, detail?.prefersDark); | |
| 317 | - | console.debug("[config] theme changed", detail); | |
| 318 | - | }; | |
| 319 | - | window.addEventListener("dumber:theme-changed", onThemeChanged); | |
| 320 | - | ||
| 321 | - | // Load config on mount | |
| 322 | - | loadConfig(); | |
| 323 | - | ||
| 324 | - | return () => { | |
| 325 | - | window.removeEventListener("dumber:theme-changed", onThemeChanged); | |
| 326 | - | }; | |
| 327 | - | }); | |
| 328 | - | </script> | |
| 329 | - | ||
| 330 | - | <ModeWatcher /> | |
| 331 | - | <ConfigShell> | |
| 332 | - | <div class="flex w-full flex-col gap-6 px-6 py-8"> | |
| 333 | - | {#if loading} | |
| 334 | - | <!-- Loading state with spinner --> | |
| 335 | - | <div class="flex flex-col items-center justify-center gap-4 py-16"> | |
| 336 | - | <Spinner class="size-8" /> | |
| 337 | - | <div class="text-xs font-semibold uppercase tracking-[0.4em] text-muted-foreground"> | |
| 338 | - | Loading configuration… | |
| 339 | - | </div> | |
| 340 | - | </div> | |
| 341 | - | {:else if loadError} | |
| 342 | - | <!-- Error state --> | |
| 343 | - | <div class="flex flex-col items-center justify-center gap-4 py-16"> | |
| 344 | - | <Card.Root class="max-w-md"> | |
| 345 | - | <Card.Header> | |
| 346 | - | <Card.Title class="text-destructive">Failed to load config</Card.Title> | |
| 347 | - | <Card.Description>{loadError}</Card.Description> | |
| 348 | - | </Card.Header> | |
| 349 | - | <Card.Content> | |
| 350 | - | <Button variant="outline" onclick={reloadConfig}>Try again</Button> | |
| 351 | - | </Card.Content> | |
| 352 | - | </Card.Root> | |
| 353 | - | </div> | |
| 354 | - | {:else if config} | |
| 355 | - | <Tabs.Root bind:value={activeTab} class="w-full"> | |
| 356 | - | <div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between"> | |
| 357 | - | <div class="text-xs font-semibold uppercase tracking-[0.3em] text-muted-foreground"> | |
| 358 | - | Config Workspace | |
| 359 | - | </div> | |
| 360 | - | <Tabs.List class="bg-muted/60"> | |
| 361 | - | <Tabs.Trigger value="appearance">Appearance</Tabs.Trigger> | |
| 362 | - | <Tabs.Trigger value="search">Search</Tabs.Trigger> | |
| 363 | - | <Tabs.Trigger value="keybindings">Keybindings</Tabs.Trigger> | |
| 364 | - | <Tabs.Trigger | |
| 365 | - | value="performance" | |
| 366 | - | disabled={!isPerformanceSupported(config)} | |
| 367 | - | title={!isPerformanceSupported(config) ? "Performance tuning is only available with the WebKit engine" : undefined} | |
| 368 | - | > | |
| 369 | - | Performance | |
| 370 | - | </Tabs.Trigger> | |
| 371 | - | </Tabs.List> | |
| 372 | - | </div> | |
| 373 | - | ||
| 374 | - | {#if !isPerformanceSupported(config)} | |
| 375 | - | <div class="rounded-md border border-border bg-muted/20 px-4 py-3 text-sm text-muted-foreground"> | |
| 376 | - | Performance settings are disabled while using the CEF engine because these options are specific to WebKitGTK. | |
| 377 | - | </div> | |
| 378 | - | {/if} | |
| 379 | - | ||
| 380 | - | <Tabs.Content value="appearance"> | |
| 381 | - | <Card.Root class="rounded-none border-0 bg-transparent py-0 shadow-none"> | |
| 382 | - | <Card.Header> | |
| 383 | - | <Card.Title>Appearance</Card.Title> | |
| 384 | - | <Card.Description>Fonts, palette, and theme preferences.</Card.Description> | |
| 385 | - | </Card.Header> | |
| 386 | - | <Card.Content class="space-y-8"> | |
| 387 | - | <div class="grid gap-6 md:grid-cols-2"> | |
| 388 | - | <div class="space-y-2"> | |
| 389 | - | <Label for="sans_font">Sans-Serif Font</Label> | |
| 390 | - | <Input id="sans_font" bind:value={config.appearance.sans_font} /> | |
| 391 | - | </div> | |
| 392 | - | <div class="space-y-2"> | |
| 393 | - | <Label for="serif_font">Serif Font</Label> | |
| 394 | - | <Input id="serif_font" bind:value={config.appearance.serif_font} /> | |
| 395 | - | </div> | |
| 396 | - | <div class="space-y-2"> | |
| 397 | - | <Label for="monospace_font">Monospace Font</Label> | |
| 398 | - | <Input id="monospace_font" bind:value={config.appearance.monospace_font} /> | |
| 399 | - | </div> | |
| 400 | - | <div class="space-y-2"> | |
| 401 | - | <Label for="font_size">Default Font Size</Label> | |
| 402 | - | <Input id="font_size" type="number" bind:value={config.appearance.default_font_size} /> | |
| 403 | - | </div> | |
| 404 | - | <div class="space-y-2"> | |
| 405 | - | <Label for="color_scheme">Color Scheme</Label> | |
| 406 | - | <select | |
| 407 | - | id="color_scheme" | |
| 408 | - | bind:value={config.appearance.color_scheme} | |
| 409 | - | class="flex h-10 w-full border border-border bg-background px-3 py-2 text-sm text-foreground ring-offset-background transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2" | |
| 410 | - | > | |
| 411 | - | <option value="default">Follow System</option> | |
| 412 | - | <option value="prefer-dark">Always Dark</option> | |
| 413 | - | <option value="prefer-light">Always Light</option> | |
| 414 | - | </select> | |
| 415 | - | </div> | |
| 416 | - | <div class="space-y-2"> | |
| 417 | - | <Label for="ui_scale">UI Scale</Label> | |
| 418 | - | <Input | |
| 419 | - | id="ui_scale" | |
| 420 | - | type="number" | |
| 421 | - | step="0.1" | |
| 422 | - | bind:value={config.default_ui_scale} | |
| 423 | - | /> | |
| 424 | - | </div> | |
| 425 | - | </div> | |
| 426 | - | ||
| 427 | - | <div class="space-y-6"> | |
| 428 | - | <div class="space-y-3"> | |
| 429 | - | <div class="text-sm font-semibold uppercase tracking-[0.2em] text-muted-foreground"> | |
| 430 | - | Theme: Light | |
| 431 | - | </div> | |
| 432 | - | <div class="grid gap-4 md:grid-cols-2"> | |
| 433 | - | {#each PALETTE_FIELDS as field (field.key)} | |
| 434 | - | <div class="space-y-2"> | |
| 435 | - | <Label for={`light_${field.key}`}>{field.label}</Label> | |
| 436 | - | <div class="flex items-center gap-3"> | |
| 437 | - | <ColorPicker | |
| 438 | - | id={`light_${field.key}`} | |
| 439 | - | value={config.appearance.light_palette[field.key]} | |
| 440 | - | onValueChange={(v) => { | |
| 441 | - | config!.appearance.light_palette[field.key] = v; | |
| 442 | - | }} | |
| 443 | - | /> | |
| 444 | - | <Input bind:value={config.appearance.light_palette[field.key]} /> | |
| 445 | - | </div> | |
| 446 | - | {#if !isHexColor(config.appearance.light_palette[field.key])} | |
| 447 | - | <p class="text-xs text-muted-foreground">Expected hex color like #RRGGBB</p> | |
| 448 | - | {/if} | |
| 449 | - | </div> | |
| 450 | - | {/each} | |
| 451 | - | </div> | |
| 452 | - | </div> | |
| 453 | - | ||
| 454 | - | <div class="space-y-3"> | |
| 455 | - | <div class="text-sm font-semibold uppercase tracking-[0.2em] text-muted-foreground"> | |
| 456 | - | Theme: Dark | |
| 457 | - | </div> | |
| 458 | - | <div class="grid gap-4 md:grid-cols-2"> | |
| 459 | - | {#each PALETTE_FIELDS as field (field.key)} | |
| 460 | - | <div class="space-y-2"> | |
| 461 | - | <Label for={`dark_${field.key}`}>{field.label}</Label> | |
| 462 | - | <div class="flex items-center gap-3"> | |
| 463 | - | <ColorPicker | |
| 464 | - | id={`dark_${field.key}`} | |
| 465 | - | value={config.appearance.dark_palette[field.key]} | |
| 466 | - | onValueChange={(v) => { | |
| 467 | - | config!.appearance.dark_palette[field.key] = v; | |
| 468 | - | }} | |
| 469 | - | /> | |
| 470 | - | <Input bind:value={config.appearance.dark_palette[field.key]} /> | |
| 471 | - | </div> | |
| 472 | - | {#if !isHexColor(config.appearance.dark_palette[field.key])} | |
| 473 | - | <p class="text-xs text-muted-foreground">Expected hex color like #RRGGBB</p> | |
| 474 | - | {/if} | |
| 475 | - | </div> | |
| 476 | - | {/each} | |
| 477 | - | </div> | |
| 478 | - | </div> | |
| 479 | - | </div> | |
| 480 | - | </Card.Content> | |
| 481 | - | </Card.Root> | |
| 482 | - | </Tabs.Content> | |
| 483 | - | ||
| 484 | - | <Tabs.Content value="search"> | |
| 485 | - | <Card.Root class="rounded-none border-0 bg-transparent py-0 shadow-none"> | |
| 486 | - | <Card.Header> | |
| 487 | - | <Card.Title>Search</Card.Title> | |
| 488 | - | <Card.Description>Default engine and shortcuts.</Card.Description> | |
| 489 | - | </Card.Header> | |
| 490 | - | <Card.Content class="space-y-8"> | |
| 491 | - | <div class="space-y-2"> | |
| 492 | - | <Label for="search_engine">Default Search Engine (use %s for query)</Label> | |
| 493 | - | <Input id="search_engine" bind:value={config.default_search_engine} /> | |
| 494 | - | </div> | |
| 495 | - | ||
| 496 | - | <div class="space-y-3"> | |
| 497 | - | <div class="text-sm font-semibold uppercase tracking-[0.2em] text-muted-foreground"> | |
| 498 | - | Search Shortcuts | |
| 499 | - | </div> | |
| 500 | - | <p class="text-sm text-muted-foreground">Map prefixes like g to URL templates.</p> | |
| 501 | - | <ShortcutsTable shortcuts={config.search_shortcuts} onUpdate={updateShortcuts} /> | |
| 502 | - | </div> | |
| 503 | - | </Card.Content> | |
| 504 | - | </Card.Root> | |
| 505 | - | </Tabs.Content> | |
| 506 | - | ||
| 507 | - | <Tabs.Content value="keybindings"> | |
| 508 | - | <KeybindingsTab /> | |
| 509 | - | </Tabs.Content> | |
| 510 | - | ||
| 511 | - | <Tabs.Content value="performance"> | |
| 512 | - | <Card.Root class="rounded-none border-0 bg-transparent py-0 shadow-none"> | |
| 513 | - | <Card.Header> | |
| 514 | - | <Card.Title>Performance Profile</Card.Title> | |
| 515 | - | <Card.Description> | |
| 516 | - | WebKitGTK rendering and memory tuning presets. | |
| 517 | - | </Card.Description> | |
| 518 | - | </Card.Header> | |
| 519 | - | <Card.Content class="space-y-6"> | |
| 520 | - | {#if !isPerformanceSupported(config)} | |
| 521 | - | <div class="rounded-md border border-border bg-muted/20 px-4 py-3 text-sm text-muted-foreground"> | |
| 522 | - | This section is unavailable under the CEF engine. | |
| 523 | - | </div> | |
| 524 | - | {:else} | |
| 525 | - | <!-- Detected Hardware Card --> | |
| 526 | - | {#if config.performance?.hardware} | |
| 527 | - | {@const hw = config.performance.hardware} | |
| 528 | - | <div class="rounded-md border border-border bg-muted/30 px-4 py-3"> | |
| 529 | - | <div class="mb-2 text-xs font-semibold uppercase tracking-[0.15em] text-muted-foreground"> | |
| 530 | - | Detected Hardware | |
| 531 | - | </div> | |
| 532 | - | <div class="flex flex-wrap gap-x-6 gap-y-1 text-sm"> | |
| 533 | - | <div><span class="text-muted-foreground">CPU:</span> <span class="font-mono">{hw.cpu_cores}c/{hw.cpu_threads}t</span></div> | |
| 534 | - | <div><span class="text-muted-foreground">RAM:</span> <span class="font-mono">{hw.total_ram_mb > 0 ? (hw.total_ram_mb / 1024).toFixed(0) + ' GB' : '?'}</span></div> | |
| 535 | - | <div><span class="text-muted-foreground">GPU:</span> <span class="font-mono capitalize">{hw.gpu_vendor || 'Unknown'}</span></div> | |
| 536 | - | <div><span class="text-muted-foreground">VRAM:</span> <span class="font-mono">{hw.vram_mb > 0 ? (hw.vram_mb / 1024).toFixed(0) + ' GB' : (hw.gpu_vendor === 'intel' ? 'Shared' : '?')}</span></div> | |
| 537 | - | </div> | |
| 538 | - | </div> | |
| 539 | - | {/if} | |
| 540 | - | ||
| 541 | - | <!-- Experimental feature warning --> | |
| 542 | - | <div class="flex items-start gap-3 rounded-md border border-info/30 bg-info/10 px-4 py-3"> | |
| 543 | - | <FlaskConical class="mt-0.5 size-4 shrink-0 text-info" /> | |
| 544 | - | <div class="space-y-1"> | |
| 545 | - | <p class="text-sm font-medium text-info">Experimental Feature</p> | |
| 546 | - | <p class="text-xs text-muted-foreground"> | |
| 547 | - | Performance profiles are experimental. Results may vary depending on your hardware and WebKitGTK version. | |
| 548 | - | </p> | |
| 549 | - | </div> | |
| 550 | - | </div> | |
| 551 | - | ||
| 552 | - | <!-- Profile selector --> | |
| 553 | - | {#if config.performance} | |
| 554 | - | <div class="space-y-4"> | |
| 555 | - | {#each PERFORMANCE_PROFILES as profile (profile.value)} | |
| 556 | - | <label | |
| 557 | - | class="flex cursor-pointer items-start gap-4 rounded-md border p-4 transition-colors hover:bg-muted/50 {config.performance.profile === profile.value ? 'border-primary bg-primary/5' : ''}" | |
| 558 | - | > | |
| 559 | - | <input | |
| 560 | - | type="radio" | |
| 561 | - | name="performance_profile" | |
| 562 | - | value={profile.value} | |
| 563 | - | checked={config.performance.profile === profile.value} | |
| 564 | - | onchange={() => config!.performance.profile = profile.value} | |
| 565 | - | class="mt-1 accent-primary" | |
| 566 | - | /> | |
| 567 | - | <div class="space-y-1"> | |
| 568 | - | <div class="font-medium">{profile.label}</div> | |
| 569 | - | <div class="text-sm text-muted-foreground">{profile.description}</div> | |
| 570 | - | </div> | |
| 571 | - | </label> | |
| 572 | - | {/each} | |
| 573 | - | </div> | |
| 574 | - | {:else} | |
| 575 | - | <div class="text-sm text-muted-foreground"> | |
| 576 | - | Performance settings not available. This may be due to a configuration error. | |
| 577 | - | </div> | |
| 578 | - | {/if} | |
| 579 | - | ||
| 580 | - | <!-- Custom profile fields --> | |
| 581 | - | {#if config.performance?.profile === "custom"} | |
| 582 | - | <div class="space-y-4 rounded-md border border-border bg-muted/20 p-4"> | |
| 583 | - | <div class="text-sm font-semibold uppercase tracking-[0.15em] text-muted-foreground"> | |
| 584 | - | Custom Settings | |
| 585 | - | </div> | |
| 586 | - | <div class="grid gap-4 md:grid-cols-2"> | |
| 587 | - | <div class="space-y-2"> | |
| 588 | - | <Label for="skia_cpu_threads">Skia CPU Threads</Label> | |
| 589 | - | <Input | |
| 590 | - | id="skia_cpu_threads" | |
| 591 | - | type="number" | |
| 592 | - | min="0" | |
| 593 | - | max="8" | |
| 594 | - | bind:value={config.performance.custom.skia_cpu_threads} | |
| 595 | - | /> | |
| 596 | - | <p class="text-xs text-muted-foreground">0 = unset (WebKit default). Max 8.</p> | |
| 597 | - | </div> | |
| 598 | - | <div class="space-y-2"> | |
| 599 | - | <Label for="skia_gpu_threads">Skia GPU Threads</Label> | |
| 600 | - | <Input | |
| 601 | - | id="skia_gpu_threads" | |
| 602 | - | type="number" | |
| 603 | - | min="-1" | |
| 604 | - | bind:value={config.performance.custom.skia_gpu_threads} | |
| 605 | - | /> | |
| 606 | - | <p class="text-xs text-muted-foreground">-1 = unset, 0 = disable GPU tiles.</p> | |
| 607 | - | </div> | |
| 608 | - | <div class="space-y-2"> | |
| 609 | - | <Label for="web_process_memory">Web Process Memory (MB)</Label> | |
| 610 | - | <Input | |
| 611 | - | id="web_process_memory" | |
| 612 | - | type="number" | |
| 613 | - | min="0" | |
| 614 | - | step="256" | |
| 615 | - | bind:value={config.performance.custom.web_process_memory_mb} | |
| 616 | - | /> | |
| 617 | - | <p class="text-xs text-muted-foreground">0 = unset (WebKit default).</p> | |
| 618 | - | </div> | |
| 619 | - | <div class="space-y-2"> | |
| 620 | - | <Label for="network_process_memory">Network Process Memory (MB)</Label> | |
| 621 | - | <Input | |
| 622 | - | id="network_process_memory" | |
| 623 | - | type="number" | |
| 624 | - | min="0" | |
| 625 | - | step="128" | |
| 626 | - | bind:value={config.performance.custom.network_process_memory_mb} | |
| 627 | - | /> | |
| 628 | - | <p class="text-xs text-muted-foreground">0 = unset (WebKit default).</p> | |
| 629 | - | </div> | |
| 630 | - | <div class="space-y-2"> | |
| 631 | - | <Label for="webview_pool_prewarm">WebView Pool Prewarm</Label> | |
| 632 | - | <Input | |
| 633 | - | id="webview_pool_prewarm" | |
| 634 | - | type="number" | |
| 635 | - | min="0" | |
| 636 | - | max="20" | |
| 637 | - | bind:value={config.performance.custom.webview_pool_prewarm} | |
| 638 | - | /> | |
| 639 | - | <p class="text-xs text-muted-foreground">Pre-created WebViews at startup.</p> | |
| 640 | - | </div> | |
| 641 | - | </div> | |
| 642 | - | </div> | |
| 643 | - | {/if} | |
| 644 | - | ||
| 645 | - | <!-- Resolved values display --> | |
| 646 | - | {#if config.performance?.resolved} | |
| 647 | - | <div class="space-y-3"> | |
| 648 | - | <div class="text-sm font-semibold uppercase tracking-[0.2em] text-muted-foreground"> | |
| 649 | - | Resolved Settings (applied on restart) | |
| 650 | - | </div> | |
| 651 | - | <div class="space-y-4 rounded-md border border-border bg-muted/20 p-4 text-sm"> | |
| 652 | - | <div class="space-y-1"> | |
| 653 | - | <div class="flex justify-between"> | |
| 654 | - | <span class="font-medium">Skia CPU Threads</span> | |
| 655 | - | <span class="font-mono">{config.performance.resolved.skia_cpu_threads === 0 ? 'unset' : config.performance.resolved.skia_cpu_threads}</span> | |
| 656 | - | </div> | |
| 657 | - | <p class="text-xs text-muted-foreground">Parallel threads for CPU-based 2D rendering. More threads = faster paint operations.</p> | |
| 658 | - | </div> | |
| 659 | - | <div class="space-y-1"> | |
| 660 | - | <div class="flex justify-between"> | |
| 661 | - | <span class="font-medium">Skia GPU Threads</span> | |
| 662 | - | <span class="font-mono">{config.performance.resolved.skia_gpu_threads === -1 ? 'unset' : config.performance.resolved.skia_gpu_threads}</span> | |
| 663 | - | </div> | |
| 664 | - | <p class="text-xs text-muted-foreground">Threads for GPU texture uploads and shader compilation.</p> | |
| 665 | - | </div> | |
| 666 | - | <div class="space-y-1"> | |
| 667 | - | <div class="flex justify-between"> | |
| 668 | - | <span class="font-medium">Web Process Memory</span> | |
| 669 | - | <span class="font-mono">{config.performance.resolved.web_process_memory_mb === 0 ? 'unset' : config.performance.resolved.web_process_memory_mb + ' MB'}</span> | |
| 670 | - | </div> | |
| 671 | - | <p class="text-xs text-muted-foreground">Memory limit for web content processes (JavaScript, DOM, rendering).</p> | |
| 672 | - | </div> | |
| 673 | - | <div class="space-y-1"> | |
| 674 | - | <div class="flex justify-between"> | |
| 675 | - | <span class="font-medium">Network Process Memory</span> | |
| 676 | - | <span class="font-mono">{config.performance.resolved.network_process_memory_mb === 0 ? 'unset' : config.performance.resolved.network_process_memory_mb + ' MB'}</span> | |
| 677 | - | </div> | |
| 678 | - | <p class="text-xs text-muted-foreground">Memory limit for network operations (HTTP cache, connections).</p> | |
| 679 | - | </div> | |
| 680 | - | <div class="space-y-1"> | |
| 681 | - | <div class="flex justify-between"> | |
| 682 | - | <span class="font-medium">WebView Pool Prewarm</span> | |
| 683 | - | <span class="font-mono">{config.performance.resolved.webview_pool_prewarm}</span> | |
| 684 | - | </div> | |
| 685 | - | <p class="text-xs text-muted-foreground">Pre-created WebViews at startup for faster new tab/pane creation.</p> | |
| 686 | - | </div> | |
| 687 | - | </div> | |
| 688 | - | </div> | |
| 689 | - | {/if} | |
| 690 | - | {/if} | |
| 691 | - | </Card.Content> | |
| 692 | - | </Card.Root> | |
| 693 | - | </Tabs.Content> | |
| 694 | - | </Tabs.Root> | |
| 695 | - | ||
| 696 | - | <div class="flex flex-wrap items-center justify-between gap-4 border-t border-border px-6 py-4"> | |
| 697 | - | <div class="flex items-center gap-3 text-xs uppercase tracking-[0.3em] text-muted-foreground"> | |
| 698 | - | {#if saveSuccess} | |
| 699 | - | <span class="text-primary">Saved</span> | |
| 700 | - | {:else} | |
| 701 | - | <span>Ready</span> | |
| 702 | - | {/if} | |
| 703 | - | {#if themeEvents > 0} | |
| 704 | - | <span>Theme x{themeEvents}</span> | |
| 705 | - | {/if} | |
| 706 | - | </div> | |
| 707 | - | {#if activeTab !== "keybindings"} | |
| 708 | - | <div class="flex items-center gap-3"> | |
| 709 | - | <AlertDialog.Root bind:open={resetDialogOpen}> | |
| 710 | - | <AlertDialog.Trigger disabled={saving}> | |
| 711 | - | {#snippet child({ props })} | |
| 712 | - | <Button variant="outline" {...props} type="button"> | |
| 713 | - | Reset Defaults | |
| 714 | - | </Button> | |
| 715 | - | {/snippet} | |
| 716 | - | </AlertDialog.Trigger> | |
| 717 | - | <AlertDialog.Content> | |
| 718 | - | <AlertDialog.Header> | |
| 719 | - | <AlertDialog.Title>Reset to defaults?</AlertDialog.Title> | |
| 720 | - | <AlertDialog.Description> | |
| 721 | - | This will reset all settings on this page to their default values. | |
| 722 | - | You will still need to click Save to persist the changes. | |
| 723 | - | </AlertDialog.Description> | |
| 724 | - | </AlertDialog.Header> | |
| 725 | - | <AlertDialog.Footer> | |
| 726 | - | <AlertDialog.Cancel>Cancel</AlertDialog.Cancel> | |
| 727 | - | <AlertDialog.Action onclick={doResetToDefaults}> | |
| 728 | - | Reset Defaults | |
| 729 | - | </AlertDialog.Action> | |
| 730 | - | </AlertDialog.Footer> | |
| 731 | - | </AlertDialog.Content> | |
| 732 | - | </AlertDialog.Root> | |
| 733 | - | <Button onclick={saveConfig} disabled={saving} type="button"> | |
| 734 | - | {saving ? "Saving…" : "Save"} | |
| 735 | - | </Button> | |
| 736 | - | </div> | |
| 737 | - | {/if} | |
| 738 | - | </div> | |
| 739 | - | ||
| 740 | - | <!-- Save error display --> | |
| 741 | - | {#if saveError} | |
| 742 | - | <div class="border border-destructive/50 bg-destructive/10 px-4 py-3 text-sm text-destructive"> | |
| 743 | - | {saveError} | |
| 744 | - | </div> | |
| 745 | - | {/if} | |
| 746 | - | ||
| 747 | - | <!-- Restart warning after saving performance settings --> | |
| 748 | - | {#if showRestartWarning} | |
| 749 | - | <div class="flex items-start gap-3 border border-warning/30 bg-warning/10 px-4 py-3"> | |
| 750 | - | <RefreshCw class="mt-0.5 size-4 shrink-0 text-warning" /> | |
| 751 | - | <div class="space-y-1"> | |
| 752 | - | <p class="text-sm font-medium text-warning">Restart Required</p> | |
| 753 | - | <p class="text-xs text-muted-foreground"> | |
| 754 | - | Performance settings are applied at browser startup. Restart Dumber for changes to take effect. | |
| 755 | - | </p> | |
| 756 | - | </div> | |
| 757 | - | </div> | |
| 758 | - | {/if} | |
| 759 | - | {/if} | |
| 760 | - | </div> | |
| 761 | - | </ConfigShell> | |
D webui/src/pages/ErrorPage.svelte +0 -213
| @@ -1,213 +0,0 @@ | |||
| 1 | - | <script lang="ts"> | |
| 2 | - | import { onMount } from 'svelte'; | |
| 3 | - | import { ModeWatcher } from 'mode-watcher'; | |
| 4 | - | import { Button } from '$lib/components/ui/button'; | |
| 5 | - | import * as Card from '$lib/components/ui/card'; | |
| 6 | - | ||
| 7 | - | type ErrorConfig = { | |
| 8 | - | code: number; | |
| 9 | - | title: string; | |
| 10 | - | message: string; | |
| 11 | - | icon: string; | |
| 12 | - | actions: string[]; | |
| 13 | - | }; | |
| 14 | - | ||
| 15 | - | const defaultConfig: ErrorConfig = { | |
| 16 | - | code: 404, | |
| 17 | - | title: 'Page Not Found', | |
| 18 | - | message: 'The page you are looking for does not exist.', | |
| 19 | - | icon: 'warning', | |
| 20 | - | actions: ['back'], | |
| 21 | - | }; | |
| 22 | - | ||
| 23 | - | // Error configurations | |
| 24 | - | const errorConfigs: Record<string, ErrorConfig> = { | |
| 25 | - | blocked: { code: 0, title: 'Page Blocked', message: 'Content blocked by filter', icon: 'shield', actions: ['whitelist', 'bypass', 'back'] }, | |
| 26 | - | '404': defaultConfig, | |
| 27 | - | '500': { code: 500, title: 'Server Error', message: 'Something went wrong on the server.', icon: 'error', actions: ['retry', 'back'] }, | |
| 28 | - | '502': { code: 502, title: 'Bad Gateway', message: 'The server received an invalid response.', icon: 'error', actions: ['retry', 'back'] }, | |
| 29 | - | '503': { code: 503, title: 'Service Unavailable', message: 'The server is temporarily unavailable.', icon: 'error', actions: ['retry', 'back'] }, | |
| 30 | - | offline: { code: 0, title: 'You Are Offline', message: 'Check your internet connection.', icon: 'offline', actions: ['retry', 'back'] }, | |
| 31 | - | ssl: { code: 0, title: 'Connection Not Secure', message: 'The security certificate is invalid.', icon: 'lock', actions: ['bypass', 'back'] }, | |
| 32 | - | timeout: { code: 0, title: 'Connection Timed Out', message: 'The server took too long to respond.', icon: 'warning', actions: ['retry', 'back'] }, | |
| 33 | - | }; | |
| 34 | - | ||
| 35 | - | let config = $state<ErrorConfig>(defaultConfig); | |
| 36 | - | let targetUrl = $state(''); | |
| 37 | - | let targetDomain = $state(''); | |
| 38 | - | let customMessage = $state(''); | |
| 39 | - | let isLoading = $state(false); | |
| 40 | - | let actionError = $state(''); | |
| 41 | - | ||
| 42 | - | onMount(() => { | |
| 43 | - | const params = new URLSearchParams(window.location.search); | |
| 44 | - | const typeParam = params.get('type') || params.get('code') || '404'; | |
| 45 | - | ||
| 46 | - | const selectedConfig = errorConfigs[typeParam]; | |
| 47 | - | config = selectedConfig ?? { | |
| 48 | - | code: parseInt(typeParam) || 0, | |
| 49 | - | title: `Error ${typeParam}`, | |
| 50 | - | message: 'An unexpected error occurred.', | |
| 51 | - | icon: 'error', | |
| 52 | - | actions: ['retry', 'back'], | |
| 53 | - | }; | |
| 54 | - | ||
| 55 | - | targetUrl = params.get('url') || ''; | |
| 56 | - | customMessage = params.get('reason') || params.get('message') || ''; | |
| 57 | - | ||
| 58 | - | if (targetUrl) { | |
| 59 | - | try { | |
| 60 | - | targetDomain = new URL(targetUrl).hostname; | |
| 61 | - | } catch { | |
| 62 | - | targetDomain = targetUrl; | |
| 63 | - | } | |
| 64 | - | } | |
| 65 | - | }); | |
| 66 | - | ||
| 67 | - | function sendMessage(type: string, payload: Record<string, unknown>): Promise<unknown> { | |
| 68 | - | return new Promise((resolve, reject) => { | |
| 69 | - | const requestId = `error_${Date.now()}_${Math.random().toString(36).slice(2)}`; | |
| 70 | - | const handler = (event: MessageEvent) => { | |
| 71 | - | if (event.data?.requestId === requestId) { | |
| 72 | - | window.removeEventListener('message', handler); | |
| 73 | - | if (event.data.success) { | |
| 74 | - | resolve(event.data.data); | |
| 75 | - | } else { | |
| 76 | - | reject(new Error(event.data.error || 'Unknown error')); | |
| 77 | - | } | |
| 78 | - | } | |
| 79 | - | }; | |
| 80 | - | window.addEventListener('message', handler); | |
| 81 | - | if (window.webkit?.messageHandlers?.dumber) { | |
| 82 | - | window.webkit.messageHandlers.dumber.postMessage(JSON.stringify({ type, requestId, payload })); | |
| 83 | - | } else { | |
| 84 | - | reject(new Error('WebKit message handler not available')); | |
| 85 | - | } | |
| 86 | - | setTimeout(() => { window.removeEventListener('message', handler); reject(new Error('Timeout')); }, 10000); | |
| 87 | - | }); | |
| 88 | - | } | |
| 89 | - | ||
| 90 | - | async function addToWhitelist() { | |
| 91 | - | if (!targetDomain) return; | |
| 92 | - | isLoading = true; | |
| 93 | - | actionError = ''; | |
| 94 | - | try { | |
| 95 | - | await sendMessage('addToWhitelist', { domain: targetDomain }); | |
| 96 | - | window.location.href = targetUrl; | |
| 97 | - | } catch (e) { | |
| 98 | - | actionError = e instanceof Error ? e.message : 'Failed'; | |
| 99 | - | isLoading = false; | |
| 100 | - | } | |
| 101 | - | } | |
| 102 | - | ||
| 103 | - | async function bypassOnce() { | |
| 104 | - | if (!targetUrl) return; | |
| 105 | - | isLoading = true; | |
| 106 | - | actionError = ''; | |
| 107 | - | try { | |
| 108 | - | await sendMessage('bypassOnce', { url: targetUrl }); | |
| 109 | - | window.location.href = targetUrl; | |
| 110 | - | } catch (e) { | |
| 111 | - | actionError = e instanceof Error ? e.message : 'Failed'; | |
| 112 | - | isLoading = false; | |
| 113 | - | } | |
| 114 | - | } | |
| 115 | - | ||
| 116 | - | function retry() { | |
| 117 | - | if (targetUrl) { | |
| 118 | - | window.location.href = targetUrl; | |
| 119 | - | } else { | |
| 120 | - | window.location.reload(); | |
| 121 | - | } | |
| 122 | - | } | |
| 123 | - | ||
| 124 | - | function goBack() { | |
| 125 | - | window.history.back(); | |
| 126 | - | } | |
| 127 | - | ||
| 128 | - | const icons: Record<string, string> = { | |
| 129 | - | shield: '<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/><path d="M9 12l2 2 4-4"/>', | |
| 130 | - | warning: '<path d="M12 9v4"/><path d="M12 17h.01"/><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/>', | |
| 131 | - | error: '<circle cx="12" cy="12" r="10"/><path d="M15 9l-6 6"/><path d="M9 9l6 6"/>', | |
| 132 | - | offline: '<line x1="1" y1="1" x2="23" y2="23"/><path d="M16.72 11.06A10.94 10.94 0 0 1 19 12.55"/><path d="M5 12.55a10.94 10.94 0 0 1 5.17-2.39"/><path d="M10.71 5.05A16 16 0 0 1 22.58 9"/><path d="M1.42 9a15.91 15.91 0 0 1 4.7-2.88"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><line x1="12" y1="20" x2="12.01" y2="20"/>', | |
| 133 | - | lock: '<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/>', | |
| 134 | - | }; | |
| 135 | - | ||
| 136 | - | const iconColors: Record<string, string> = { | |
| 137 | - | shield: 'text-primary', | |
| 138 | - | warning: 'text-yellow-500', | |
| 139 | - | error: 'text-destructive', | |
| 140 | - | offline: 'text-muted-foreground', | |
| 141 | - | lock: 'text-yellow-500', | |
| 142 | - | }; | |
| 143 | - | </script> | |
| 144 | - | ||
| 145 | - | <svelte:head> | |
| 146 | - | <title>{config.title}</title> | |
| 147 | - | </svelte:head> | |
| 148 | - | ||
| 149 | - | <ModeWatcher /> | |
| 150 | - | <div class="min-h-screen flex items-center justify-center p-8 bg-background"> | |
| 151 | - | <Card.Root class="w-full max-w-md"> | |
| 152 | - | <Card.Header class="text-center"> | |
| 153 | - | <div class="flex justify-center mb-4"> | |
| 154 | - | <svg class="w-16 h-16 {iconColors[config.icon] || 'text-muted-foreground'}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"> | |
| 155 | - | <!-- eslint-disable-next-line svelte/no-at-html-tags -- trusted static SVG paths --> | |
| 156 | - | {@html icons[config.icon] || icons.error} | |
| 157 | - | </svg> | |
| 158 | - | </div> | |
| 159 | - | {#if config.code > 0} | |
| 160 | - | <div class="text-6xl font-bold text-muted-foreground/30 mb-2">{config.code}</div> | |
| 161 | - | {/if} | |
| 162 | - | <Card.Title class="text-xl">{config.title}</Card.Title> | |
| 163 | - | <Card.Description>{customMessage || config.message}</Card.Description> | |
| 164 | - | </Card.Header> | |
| 165 | - | ||
| 166 | - | <Card.Content class="space-y-4"> | |
| 167 | - | {#if targetUrl} | |
| 168 | - | <div class="bg-muted p-3 rounded-md"> | |
| 169 | - | <p class="text-xs text-muted-foreground uppercase tracking-wide mb-1">URL</p> | |
| 170 | - | <code class="text-sm break-all">{targetUrl}</code> | |
| 171 | - | </div> | |
| 172 | - | {/if} | |
| 173 | - | ||
| 174 | - | {#if actionError} | |
| 175 | - | <div class="bg-destructive/10 border border-destructive/30 text-destructive p-3 rounded-md text-sm"> | |
| 176 | - | {actionError} | |
| 177 | - | </div> | |
| 178 | - | {/if} | |
| 179 | - | </Card.Content> | |
| 180 | - | ||
| 181 | - | <Card.Footer class="flex flex-col gap-3"> | |
| 182 | - | {#if config.actions.includes('whitelist') && targetDomain} | |
| 183 | - | <Button class="w-full" onclick={addToWhitelist} disabled={isLoading}> | |
| 184 | - | {isLoading ? 'Adding...' : 'Add to Whitelist'} | |
| 185 | - | </Button> | |
| 186 | - | {/if} | |
| 187 | - | ||
| 188 | - | {#if config.actions.includes('bypass') && targetUrl} | |
| 189 | - | <Button variant="outline" class="w-full" onclick={bypassOnce} disabled={isLoading}> | |
| 190 | - | Proceed Anyway | |
| 191 | - | </Button> | |
| 192 | - | {/if} | |
| 193 | - | ||
| 194 | - | {#if config.actions.includes('retry')} | |
| 195 | - | <Button class="w-full" onclick={retry} disabled={isLoading}> | |
| 196 | - | Try Again | |
| 197 | - | </Button> | |
| 198 | - | {/if} | |
| 199 | - | ||
| 200 | - | {#if config.actions.includes('back')} | |
| 201 | - | <Button variant="ghost" class="w-full" onclick={goBack} disabled={isLoading}> | |
| 202 | - | Go Back | |
| 203 | - | </Button> | |
| 204 | - | {/if} | |
| 205 | - | ||
| 206 | - | {#if config.actions.includes('whitelist') && targetDomain} | |
| 207 | - | <p class="text-xs text-muted-foreground text-center"> | |
| 208 | - | Adding <strong class="text-foreground">{targetDomain}</strong> to whitelist will disable content filtering. | |
| 209 | - | </p> | |
| 210 | - | {/if} | |
| 211 | - | </Card.Footer> | |
| 212 | - | </Card.Root> | |
| 213 | - | </div> | |
D webui/src/pages/Homepage.svelte +0 -17
| @@ -1,17 +0,0 @@ | |||
| 1 | - | <script lang="ts"> | |
| 2 | - | import { ModeWatcher } from 'mode-watcher'; | |
| 3 | - | import { HomepageShell } from './homepage/shell'; | |
| 4 | - | import { HistoryPanel, FavoritesPanel, AnalyticsPanel } from './homepage/panels'; | |
| 5 | - | import { homepageState } from './homepage/state.svelte'; | |
| 6 | - | </script> | |
| 7 | - | ||
| 8 | - | <ModeWatcher /> | |
| 9 | - | <HomepageShell> | |
| 10 | - | {#if homepageState.activePanel === 'history'} | |
| 11 | - | <HistoryPanel /> | |
| 12 | - | {:else if homepageState.activePanel === 'favorites'} | |
| 13 | - | <FavoritesPanel /> | |
| 14 | - | {:else if homepageState.activePanel === 'analytics'} | |
| 15 | - | <AnalyticsPanel /> | |
| 16 | - | {/if} | |
| 17 | - | </HomepageShell> | |
D webui/src/pages/config.ts +0 -10
| @@ -1,10 +0,0 @@ | |||
| 1 | - | import { mount } from "svelte"; | |
| 2 | - | import ConfigPage from "./ConfigPage.svelte"; | |
| 3 | - | import "../styles/app.css"; | |
| 4 | - | ||
| 5 | - | // Mount the config page | |
| 6 | - | const app = mount(ConfigPage, { | |
| 7 | - | target: document.body, | |
| 8 | - | }); | |
| 9 | - | ||
| 10 | - | export default app; | |
D webui/src/pages/config/ConfigShell.svelte +0 -227
| @@ -1,227 +0,0 @@ | |||
| 1 | - | <script lang="ts"> | |
| 2 | - | import { onMount } from 'svelte'; | |
| 3 | - | import { Home } from '@lucide/svelte'; | |
| 4 | - | ||
| 5 | - | interface Props { | |
| 6 | - | children?: import('svelte').Snippet; | |
| 7 | - | } | |
| 8 | - | ||
| 9 | - | let { children }: Props = $props(); | |
| 10 | - | ||
| 11 | - | function navigateWithViewTransition(url: string) { | |
| 12 | - | if (typeof document.startViewTransition !== 'function') { | |
| 13 | - | window.location.href = url; | |
| 14 | - | return; | |
| 15 | - | } | |
| 16 | - | ||
| 17 | - | const transition = document.startViewTransition(() => { | |
| 18 | - | document.documentElement.dataset.vt = 'leaving'; | |
| 19 | - | }); | |
| 20 | - | ||
| 21 | - | transition.finished.finally(() => { | |
| 22 | - | window.location.href = url; | |
| 23 | - | }); | |
| 24 | - | } | |
| 25 | - | ||
| 26 | - | onMount(() => { | |
| 27 | - | // Avoid default WebKit focus rings. | |
| 28 | - | document.documentElement.dataset.vt = 'ready'; | |
| 29 | - | }); | |
| 30 | - | </script> | |
| 31 | - | ||
| 32 | - | <svelte:head> | |
| 33 | - | <title>dumb://config</title> | |
| 34 | - | <meta name="description" content="Dumber Browser - Settings" /> | |
| 35 | - | <!-- eslint-disable-next-line svelte/no-at-html-tags -- trusted static styles for shell initialization --> | |
| 36 | - | {@html `<style> | |
| 37 | - | html, body { margin: 0; padding: 0; } | |
| 38 | - | html { background: var(--background, #0a0a0a); } | |
| 39 | - | body { background: var(--background, #0a0a0a); color: var(--foreground, #e5e5e5); } | |
| 40 | - | *:focus { outline: none !important; } | |
| 41 | - | *:focus-visible { outline: none !important; } | |
| 42 | - | button:focus, input:focus, a:focus { outline: none !important; box-shadow: none !important; } | |
| 43 | - | * { -webkit-overflow-scrolling: touch; } | |
| 44 | - | </style>`} | |
| 45 | - | </svelte:head> | |
| 46 | - | ||
| 47 | - | <div class="config-shell browser-ui"> | |
| 48 | - | <div class="terminal-frame"> | |
| 49 | - | <header class="terminal-header"> | |
| 50 | - | <div class="header-left"> | |
| 51 | - | <span class="terminal-path">dumb://config</span> | |
| 52 | - | </div> | |
| 53 | - | <div class="header-right"> | |
| 54 | - | <button | |
| 55 | - | class="action-button" | |
| 56 | - | type="button" | |
| 57 | - | onclick={() => navigateWithViewTransition('dumb://home')} | |
| 58 | - | title="Back to Home" | |
| 59 | - | > | |
| 60 | - | <Home size={14} strokeWidth={2} /> | |
| 61 | - | <span class="button-text">HOME</span> | |
| 62 | - | </button> | |
| 63 | - | </div> | |
| 64 | - | </header> | |
| 65 | - | ||
| 66 | - | <main class="terminal-body scrollable"> | |
| 67 | - | {#if children} | |
| 68 | - | {@render children()} | |
| 69 | - | {/if} | |
| 70 | - | </main> | |
| 71 | - | ||
| 72 | - | <footer class="terminal-footer"> | |
| 73 | - | <span class="footer-text">Dumber Browser</span> | |
| 74 | - | </footer> | |
| 75 | - | </div> | |
| 76 | - | </div> | |
| 77 | - | ||
| 78 | - | <style> | |
| 79 | - | :global(html), | |
| 80 | - | :global(body) { | |
| 81 | - | height: 100%; | |
| 82 | - | } | |
| 83 | - | ||
| 84 | - | :global(body) { | |
| 85 | - | overflow: hidden; | |
| 86 | - | overscroll-behavior: contain; | |
| 87 | - | } | |
| 88 | - | ||
| 89 | - | .config-shell { | |
| 90 | - | height: 100vh; | |
| 91 | - | min-height: 100vh; | |
| 92 | - | display: flex; | |
| 93 | - | flex-direction: column; | |
| 94 | - | font-family: ui-monospace, 'Fira Code', 'Cascadia Code', Menlo, Monaco, Consolas, monospace; | |
| 95 | - | line-height: 1.5; | |
| 96 | - | color: var(--foreground); | |
| 97 | - | background: var(--background); | |
| 98 | - | overflow: hidden; | |
| 99 | - | } | |
| 100 | - | ||
| 101 | - | .terminal-frame { | |
| 102 | - | flex: 1; | |
| 103 | - | display: grid; | |
| 104 | - | grid-template-rows: auto 1fr auto; | |
| 105 | - | width: 100%; | |
| 106 | - | height: 100%; | |
| 107 | - | background: color-mix(in srgb, var(--card) 60%, var(--background) 40%); | |
| 108 | - | } | |
| 109 | - | ||
| 110 | - | :global(.dark) .terminal-frame { | |
| 111 | - | box-shadow: 0 24px 48px -12px rgb(0 0 0 / 0.5); | |
| 112 | - | } | |
| 113 | - | ||
| 114 | - | .terminal-header { | |
| 115 | - | display: flex; | |
| 116 | - | align-items: center; | |
| 117 | - | justify-content: space-between; | |
| 118 | - | gap: 1rem; | |
| 119 | - | padding: 0.75rem 1rem; | |
| 120 | - | min-height: 3.5rem; | |
| 121 | - | border-bottom: 1px solid var(--border); | |
| 122 | - | background: color-mix(in srgb, var(--background) 90%, var(--card) 10%); | |
| 123 | - | position: sticky; | |
| 124 | - | top: 0; | |
| 125 | - | z-index: 5; | |
| 126 | - | } | |
| 127 | - | ||
| 128 | - | .header-left { | |
| 129 | - | display: flex; | |
| 130 | - | align-items: center; | |
| 131 | - | gap: 1.5rem; | |
| 132 | - | } | |
| 133 | - | ||
| 134 | - | .terminal-path { | |
| 135 | - | font-size: 0.8rem; | |
| 136 | - | font-weight: 600; | |
| 137 | - | color: var(--primary, #4ade80); | |
| 138 | - | letter-spacing: 0.05em; | |
| 139 | - | } | |
| 140 | - | ||
| 141 | - | .header-right { | |
| 142 | - | display: flex; | |
| 143 | - | align-items: center; | |
| 144 | - | gap: 0.5rem; | |
| 145 | - | } | |
| 146 | - | ||
| 147 | - | .action-button { | |
| 148 | - | display: flex; | |
| 149 | - | align-items: center; | |
| 150 | - | gap: 0.5rem; | |
| 151 | - | padding: 0.4rem 0.75rem; | |
| 152 | - | font-size: 0.7rem; | |
| 153 | - | font-weight: 500; | |
| 154 | - | letter-spacing: 0.08em; | |
| 155 | - | color: var(--muted-foreground); | |
| 156 | - | background: transparent; | |
| 157 | - | border-width: 1px; | |
| 158 | - | border-style: solid; | |
| 159 | - | border-color: var(--border); | |
| 160 | - | cursor: pointer; | |
| 161 | - | transition: all 120ms ease; | |
| 162 | - | } | |
| 163 | - | ||
| 164 | - | .action-button:hover, | |
| 165 | - | .action-button:focus-visible { | |
| 166 | - | color: var(--foreground); | |
| 167 | - | border-color: color-mix(in srgb, var(--border) 50%, var(--foreground) 50%); | |
| 168 | - | background: color-mix(in srgb, var(--card) 30%, transparent); | |
| 169 | - | outline: none; | |
| 170 | - | } | |
| 171 | - | ||
| 172 | - | .terminal-body { | |
| 173 | - | display: flex; | |
| 174 | - | flex-direction: column; | |
| 175 | - | min-height: 0; | |
| 176 | - | overflow: hidden; | |
| 177 | - | background: var(--background); | |
| 178 | - | } | |
| 179 | - | ||
| 180 | - | .terminal-body.scrollable { | |
| 181 | - | overflow-y: auto; | |
| 182 | - | -webkit-overflow-scrolling: touch; | |
| 183 | - | touch-action: pan-y; | |
| 184 | - | } | |
| 185 | - | ||
| 186 | - | .terminal-footer { | |
| 187 | - | display: flex; | |
| 188 | - | align-items: center; | |
| 189 | - | justify-content: center; | |
| 190 | - | padding: 0.75rem 1rem; | |
| 191 | - | border-top: 1px solid var(--border); | |
| 192 | - | background: color-mix(in srgb, var(--background) 92%, var(--card) 8%); | |
| 193 | - | } | |
| 194 | - | ||
| 195 | - | .footer-text { | |
| 196 | - | font-size: 0.7rem; | |
| 197 | - | letter-spacing: 0.08em; | |
| 198 | - | color: var(--muted-foreground); | |
| 199 | - | } | |
| 200 | - | ||
| 201 | - | /* View Transition helpers */ | |
| 202 | - | :global(html[data-vt='leaving']) { | |
| 203 | - | view-transition-name: config-root; | |
| 204 | - | } | |
| 205 | - | ||
| 206 | - | :global(::view-transition-old(config-root)) { | |
| 207 | - | animation: vt-fade-out 180ms ease both; | |
| 208 | - | } | |
| 209 | - | ||
| 210 | - | :global(::view-transition-new(config-root)) { | |
| 211 | - | animation: vt-fade-in 220ms ease both; | |
| 212 | - | } | |
| 213 | - | ||
| 214 | - | @keyframes vt-fade-out { | |
| 215 | - | to { | |
| 216 | - | opacity: 0; | |
| 217 | - | transform: translateY(6px); | |
| 218 | - | } | |
| 219 | - | } | |
| 220 | - | ||
| 221 | - | @keyframes vt-fade-in { | |
| 222 | - | from { | |
| 223 | - | opacity: 0; | |
| 224 | - | transform: translateY(-6px); | |
| 225 | - | } | |
| 226 | - | } | |
| 227 | - | </style> | |
D webui/src/pages/config/KeyCaptureModal.svelte +0 -184
| @@ -1,184 +0,0 @@ | |||
| 1 | - | <script lang="ts"> | |
| 2 | - | import { onMount, untrack } from "svelte"; | |
| 3 | - | import { Button } from "$lib/components/ui/button"; | |
| 4 | - | import * as AlertDialog from "$lib/components/ui/alert-dialog"; | |
| 5 | - | import { Keyboard, X, Plus } from "@lucide/svelte"; | |
| 6 | - | import { formatKey } from "$lib/utils/keys"; | |
| 7 | - | ||
| 8 | - | type KeybindingEntry = { | |
| 9 | - | action: string; | |
| 10 | - | description: string; | |
| 11 | - | keys: string[]; | |
| 12 | - | default_keys: string[]; | |
| 13 | - | is_custom: boolean; | |
| 14 | - | }; | |
| 15 | - | ||
| 16 | - | interface Props { | |
| 17 | - | binding: KeybindingEntry; | |
| 18 | - | onSave: (keys: string[]) => void; | |
| 19 | - | onCancel: () => void; | |
| 20 | - | } | |
| 21 | - | ||
| 22 | - | let { binding, onSave, onCancel }: Props = $props(); | |
| 23 | - | ||
| 24 | - | // Initialize with a plain copy of the binding keys using $state.snapshot() | |
| 25 | - | // to strip Svelte 5 Proxy wrappers. untrack() ensures we only read the initial value. | |
| 26 | - | let newKeys = $state<string[]>( | |
| 27 | - | untrack(() => $state.snapshot(binding.keys) as string[]) | |
| 28 | - | ); | |
| 29 | - | let isCapturing = $state(false); | |
| 30 | - | let capturedKey = $state<string | null>(null); | |
| 31 | - | let isSaving = $state(false); | |
| 32 | - | ||
| 33 | - | // Symbol keys that are already shifted (pressing shift produces these characters) | |
| 34 | - | // Don't add 'shift' modifier for these since they're self-describing | |
| 35 | - | const SHIFTED_SYMBOLS = new Set([ | |
| 36 | - | "{", "}", "(", ")", "<", ">", "!", "@", "#", "$", "%", "^", "&", "*", | |
| 37 | - | "+", "_", ":", '"', "?", "|", "~", | |
| 38 | - | ]); | |
| 39 | - | ||
| 40 | - | function handleKeyDown(e: KeyboardEvent) { | |
| 41 | - | if (!isCapturing) return; | |
| 42 | - | e.preventDefault(); | |
| 43 | - | e.stopPropagation(); | |
| 44 | - | ||
| 45 | - | const parts: string[] = []; | |
| 46 | - | if (e.ctrlKey) parts.push("ctrl"); | |
| 47 | - | if (e.altKey) parts.push("alt"); | |
| 48 | - | ||
| 49 | - | // Ignore modifier-only presses | |
| 50 | - | if (["control", "alt", "shift", "meta"].includes(e.key.toLowerCase())) { | |
| 51 | - | return; | |
| 52 | - | } | |
| 53 | - | ||
| 54 | - | // Escape closes the modal | |
| 55 | - | if (e.key === "Escape") { | |
| 56 | - | onCancel(); | |
| 57 | - | return; | |
| 58 | - | } | |
| 59 | - | ||
| 60 | - | // Normalize key name | |
| 61 | - | const key = e.key === " " ? "space" : e.key.toLowerCase(); | |
| 62 | - | ||
| 63 | - | // Only add shift modifier for letter keys and non-symbol keys | |
| 64 | - | // Symbol keys like { } + etc. are already the shifted result | |
| 65 | - | if (e.shiftKey && !SHIFTED_SYMBOLS.has(e.key)) { | |
| 66 | - | parts.push("shift"); | |
| 67 | - | } | |
| 68 | - | ||
| 69 | - | parts.push(key); | |
| 70 | - | capturedKey = parts.join("+"); | |
| 71 | - | isCapturing = false; | |
| 72 | - | } | |
| 73 | - | ||
| 74 | - | function addCapturedKey() { | |
| 75 | - | if (capturedKey && !newKeys.includes(capturedKey)) { | |
| 76 | - | newKeys = [...newKeys, capturedKey]; | |
| 77 | - | } | |
| 78 | - | capturedKey = null; | |
| 79 | - | } | |
| 80 | - | ||
| 81 | - | function removeKey(index: number) { | |
| 82 | - | newKeys = newKeys.filter((_, i) => i !== index); | |
| 83 | - | } | |
| 84 | - | ||
| 85 | - | function handleSave() { | |
| 86 | - | // Use $state.snapshot() to get plain array without Svelte 5 Proxy wrappers | |
| 87 | - | const keysToSave = $state.snapshot(newKeys) as string[]; | |
| 88 | - | // Auto-add any pending captured key before saving | |
| 89 | - | if (capturedKey && !keysToSave.includes(capturedKey)) { | |
| 90 | - | keysToSave.push(capturedKey); | |
| 91 | - | } | |
| 92 | - | isSaving = true; | |
| 93 | - | try { | |
| 94 | - | onSave(keysToSave); | |
| 95 | - | } finally { | |
| 96 | - | // Reset isSaving so modal remains dismissible if save fails | |
| 97 | - | isSaving = false; | |
| 98 | - | } | |
| 99 | - | } | |
| 100 | - | ||
| 101 | - | onMount(() => { | |
| 102 | - | const handler = (e: KeyboardEvent) => handleKeyDown(e); | |
| 103 | - | window.addEventListener("keydown", handler, true); | |
| 104 | - | return () => window.removeEventListener("keydown", handler, true); | |
| 105 | - | }); | |
| 106 | - | </script> | |
| 107 | - | ||
| 108 | - | <AlertDialog.Root open={true} onOpenChange={(open) => !open && !isSaving && onCancel()}> | |
| 109 | - | <AlertDialog.Content class="max-w-md"> | |
| 110 | - | <AlertDialog.Header> | |
| 111 | - | <AlertDialog.Title>Edit Keybinding</AlertDialog.Title> | |
| 112 | - | <AlertDialog.Description> | |
| 113 | - | Configure keybindings for <strong>{binding.action}</strong> | |
| 114 | - | </AlertDialog.Description> | |
| 115 | - | </AlertDialog.Header> | |
| 116 | - | ||
| 117 | - | <div class="space-y-4 py-4"> | |
| 118 | - | <!-- Current bindings --> | |
| 119 | - | <div class="space-y-2"> | |
| 120 | - | <div class="text-xs font-semibold uppercase tracking-[0.15em] text-muted-foreground"> | |
| 121 | - | Current Bindings | |
| 122 | - | </div> | |
| 123 | - | <div class="flex flex-wrap gap-2"> | |
| 124 | - | {#each newKeys as key, i (key)} | |
| 125 | - | <div class="flex items-center gap-1 rounded bg-muted px-2 py-1"> | |
| 126 | - | <kbd class="text-xs font-mono">{formatKey(key)}</kbd> | |
| 127 | - | <button | |
| 128 | - | type="button" | |
| 129 | - | onclick={() => removeKey(i)} | |
| 130 | - | class="text-muted-foreground hover:text-foreground" | |
| 131 | - | > | |
| 132 | - | <X class="size-3" /> | |
| 133 | - | </button> | |
| 134 | - | </div> | |
| 135 | - | {/each} | |
| 136 | - | {#if newKeys.length === 0} | |
| 137 | - | <span class="text-sm text-muted-foreground">(no bindings)</span> | |
| 138 | - | {/if} | |
| 139 | - | </div> | |
| 140 | - | </div> | |
| 141 | - | ||
| 142 | - | <!-- Capture area --> | |
| 143 | - | <div class="space-y-2"> | |
| 144 | - | <div class="text-xs font-semibold uppercase tracking-[0.15em] text-muted-foreground"> | |
| 145 | - | Add New Binding | |
| 146 | - | </div> | |
| 147 | - | <div class="flex gap-2"> | |
| 148 | - | <button | |
| 149 | - | type="button" | |
| 150 | - | onclick={() => { isCapturing = true; capturedKey = null; }} | |
| 151 | - | class="flex flex-1 items-center justify-center gap-2 rounded-md border border-dashed border-border px-4 py-6 text-sm text-muted-foreground transition hover:border-foreground hover:text-foreground {isCapturing ? 'border-primary bg-primary/5 text-primary' : ''}" | |
| 152 | - | > | |
| 153 | - | <Keyboard class="size-5" /> | |
| 154 | - | {#if isCapturing} | |
| 155 | - | Press a key combination... | |
| 156 | - | {:else if capturedKey} | |
| 157 | - | <kbd class="font-mono">{formatKey(capturedKey)}</kbd> | |
| 158 | - | {:else} | |
| 159 | - | Click to capture key | |
| 160 | - | {/if} | |
| 161 | - | </button> | |
| 162 | - | {#if capturedKey} | |
| 163 | - | <Button variant="outline" onclick={addCapturedKey}> | |
| 164 | - | <Plus class="mr-1 size-4" /> | |
| 165 | - | Add | |
| 166 | - | </Button> | |
| 167 | - | {/if} | |
| 168 | - | </div> | |
| 169 | - | </div> | |
| 170 | - | ||
| 171 | - | <!-- Default hint --> | |
| 172 | - | {#if binding.default_keys.length > 0} | |
| 173 | - | <div class="text-xs text-muted-foreground"> | |
| 174 | - | Default: {binding.default_keys.map(formatKey).join(", ")} | |
| 175 | - | </div> | |
| 176 | - | {/if} | |
| 177 | - | </div> | |
| 178 | - | ||
| 179 | - | <AlertDialog.Footer> | |
| 180 | - | <AlertDialog.Cancel>Cancel</AlertDialog.Cancel> | |
| 181 | - | <AlertDialog.Action onclick={handleSave}>Save</AlertDialog.Action> | |
| 182 | - | </AlertDialog.Footer> | |
| 183 | - | </AlertDialog.Content> | |
| 184 | - | </AlertDialog.Root> | |
D webui/src/pages/config/KeybindingGroup.svelte +0 -95
| @@ -1,95 +0,0 @@ | |||
| 1 | - | <script lang="ts"> | |
| 2 | - | import { Button } from "$lib/components/ui/button"; | |
| 3 | - | import * as Table from "$lib/components/ui/table"; | |
| 4 | - | import { RotateCcw, Edit3 } from "@lucide/svelte"; | |
| 5 | - | import { formatKey } from "$lib/utils/keys"; | |
| 6 | - | ||
| 7 | - | type KeybindingEntry = { | |
| 8 | - | action: string; | |
| 9 | - | description: string; | |
| 10 | - | keys: string[]; | |
| 11 | - | default_keys: string[]; | |
| 12 | - | is_custom: boolean; | |
| 13 | - | }; | |
| 14 | - | ||
| 15 | - | type KeybindingGroupData = { | |
| 16 | - | mode: string; | |
| 17 | - | display_name: string; | |
| 18 | - | bindings: KeybindingEntry[]; | |
| 19 | - | activation?: string; | |
| 20 | - | }; | |
| 21 | - | ||
| 22 | - | interface Props { | |
| 23 | - | group: KeybindingGroupData; | |
| 24 | - | onEdit: (action: string, entry: KeybindingEntry) => void; | |
| 25 | - | onReset: (action: string) => void; | |
| 26 | - | } | |
| 27 | - | ||
| 28 | - | let { group, onEdit, onReset }: Props = $props(); | |
| 29 | - | </script> | |
| 30 | - | ||
| 31 | - | <div class="space-y-3"> | |
| 32 | - | <div class="flex items-center gap-2"> | |
| 33 | - | <h3 class="text-sm font-semibold uppercase tracking-[0.15em] text-muted-foreground"> | |
| 34 | - | {group.display_name} | |
| 35 | - | </h3> | |
| 36 | - | {#if group.activation} | |
| 37 | - | <span class="rounded-md bg-muted px-2 py-0.5 text-xs font-mono text-muted-foreground"> | |
| 38 | - | {formatKey(group.activation)} | |
| 39 | - | </span> | |
| 40 | - | {/if} | |
| 41 | - | </div> | |
| 42 | - | ||
| 43 | - | <Table.Root class="rounded-md border border-border"> | |
| 44 | - | <Table.Header> | |
| 45 | - | <Table.Row> | |
| 46 | - | <Table.Head class="w-[180px]">Action</Table.Head> | |
| 47 | - | <Table.Head>Description</Table.Head> | |
| 48 | - | <Table.Head class="w-[200px]">Keybinding</Table.Head> | |
| 49 | - | <Table.Head class="w-[100px] text-right">Actions</Table.Head> | |
| 50 | - | </Table.Row> | |
| 51 | - | </Table.Header> | |
| 52 | - | <Table.Body> | |
| 53 | - | {#each group.bindings as binding (binding.action)} | |
| 54 | - | <Table.Row class={binding.is_custom ? "bg-accent/5" : ""}> | |
| 55 | - | <Table.Cell class="font-mono text-sm">{binding.action}</Table.Cell> | |
| 56 | - | <Table.Cell class="text-muted-foreground">{binding.description}</Table.Cell> | |
| 57 | - | <Table.Cell> | |
| 58 | - | <div class="flex flex-wrap gap-1"> | |
| 59 | - | {#each binding.keys as key (key)} | |
| 60 | - | <kbd class="rounded bg-muted px-1.5 py-0.5 text-xs font-mono"> | |
| 61 | - | {formatKey(key)} | |
| 62 | - | </kbd> | |
| 63 | - | {/each} | |
| 64 | - | {#if binding.keys.length === 0} | |
| 65 | - | <span class="text-xs text-muted-foreground">(unbound)</span> | |
| 66 | - | {/if} | |
| 67 | - | </div> | |
| 68 | - | </Table.Cell> | |
| 69 | - | <Table.Cell class="text-right"> | |
| 70 | - | <div class="flex justify-end gap-1"> | |
| 71 | - | <Button | |
| 72 | - | variant="ghost" | |
| 73 | - | size="icon" | |
| 74 | - | onclick={() => onEdit(binding.action, binding)} | |
| 75 | - | aria-label="Edit binding" | |
| 76 | - | > | |
| 77 | - | <Edit3 class="size-4" /> | |
| 78 | - | </Button> | |
| 79 | - | {#if binding.is_custom} | |
| 80 | - | <Button | |
| 81 | - | variant="ghost" | |
| 82 | - | size="icon" | |
| 83 | - | onclick={() => onReset(binding.action)} | |
| 84 | - | aria-label="Reset to default" | |
| 85 | - | > | |
| 86 | - | <RotateCcw class="size-4" /> | |
| 87 | - | </Button> | |
| 88 | - | {/if} | |
| 89 | - | </div> | |
| 90 | - | </Table.Cell> | |
| 91 | - | </Table.Row> | |
| 92 | - | {/each} | |
| 93 | - | </Table.Body> | |
| 94 | - | </Table.Root> | |
| 95 | - | </div> | |
D webui/src/pages/config/KeybindingsTab.svelte +0 -340
| @@ -1,340 +0,0 @@ | |||
| 1 | - | <script lang="ts"> | |
| 2 | - | import { onMount } from "svelte"; | |
| 3 | - | import { Button } from "$lib/components/ui/button"; | |
| 4 | - | import { Spinner } from "$lib/components/ui/spinner"; | |
| 5 | - | import * as Card from "$lib/components/ui/card"; | |
| 6 | - | import * as AlertDialog from "$lib/components/ui/alert-dialog"; | |
| 7 | - | import { RefreshCw, RotateCcw, AlertTriangle, CheckCircle } from "@lucide/svelte"; | |
| 8 | - | import { postMessage, getWebKitBridge, getWebViewId } from "$lib/bridge"; | |
| 9 | - | import KeybindingGroup from "./KeybindingGroup.svelte"; | |
| 10 | - | import KeyCaptureModal from "./KeyCaptureModal.svelte"; | |
| 11 | - | ||
| 12 | - | type KeybindingEntry = { | |
| 13 | - | action: string; | |
| 14 | - | description: string; | |
| 15 | - | keys: string[]; | |
| 16 | - | default_keys: string[]; | |
| 17 | - | is_custom: boolean; | |
| 18 | - | }; | |
| 19 | - | ||
| 20 | - | type KeybindingGroupData = { | |
| 21 | - | mode: string; | |
| 22 | - | display_name: string; | |
| 23 | - | bindings: KeybindingEntry[]; | |
| 24 | - | activation?: string; | |
| 25 | - | }; | |
| 26 | - | ||
| 27 | - | type KeybindingsConfig = { | |
| 28 | - | groups: KeybindingGroupData[]; | |
| 29 | - | }; | |
| 30 | - | ||
| 31 | - | type KeybindingConflict = { | |
| 32 | - | conflicting_action: string; | |
| 33 | - | conflicting_mode: string; | |
| 34 | - | key: string; | |
| 35 | - | }; | |
| 36 | - | ||
| 37 | - | let loading = $state(true); | |
| 38 | - | let error = $state<string | null>(null); | |
| 39 | - | let keybindings = $state<KeybindingsConfig | null>(null); | |
| 40 | - | let editingBinding = $state<{ mode: string; action: string; entry: KeybindingEntry } | null>(null); | |
| 41 | - | let resetAllDialogOpen = $state(false); | |
| 42 | - | let successMessage = $state<string | null>(null); | |
| 43 | - | let conflicts = $state<KeybindingConflict[]>([]); | |
| 44 | - | let hasPendingReset = $state(false); | |
| 45 | - | let savingReset = $state(false); | |
| 46 | - | ||
| 47 | - | // Type-safe callback setup helper | |
| 48 | - | function setupCallback<T>(name: string, handler: (data: T) => void): void { | |
| 49 | - | (window as unknown as Record<string, (data: T) => void>)[name] = handler; | |
| 50 | - | } | |
| 51 | - | ||
| 52 | - | async function loadKeybindings() { | |
| 53 | - | loading = true; | |
| 54 | - | error = null; | |
| 55 | - | ||
| 56 | - | if (!getWebKitBridge()) { | |
| 57 | - | error = "Dumber bridge not available"; | |
| 58 | - | loading = false; | |
| 59 | - | return; | |
| 60 | - | } | |
| 61 | - | ||
| 62 | - | setupCallback<KeybindingsConfig>("__dumber_keybindings_loaded", (data) => { | |
| 63 | - | keybindings = data; | |
| 64 | - | loading = false; | |
| 65 | - | }); | |
| 66 | - | setupCallback<string>("__dumber_keybindings_error", (msg) => { | |
| 67 | - | error = typeof msg === "string" ? msg : "Failed to load keybindings"; | |
| 68 | - | loading = false; | |
| 69 | - | }); | |
| 70 | - | ||
| 71 | - | try { | |
| 72 | - | postMessage({ | |
| 73 | - | type: "get_keybindings", | |
| 74 | - | webviewId: getWebViewId(), | |
| 75 | - | payload: {}, | |
| 76 | - | }); | |
| 77 | - | } catch (err) { | |
| 78 | - | error = err instanceof Error ? err.message : "Failed to load keybindings"; | |
| 79 | - | loading = false; | |
| 80 | - | } | |
| 81 | - | } | |
| 82 | - | ||
| 83 | - | function startEdit(mode: string, action: string, entry: KeybindingEntry) { | |
| 84 | - | editingBinding = { mode, action, entry }; | |
| 85 | - | } | |
| 86 | - | ||
| 87 | - | function showSuccess(message: string) { | |
| 88 | - | successMessage = message; | |
| 89 | - | setTimeout(() => { successMessage = null; }, 3000); | |
| 90 | - | } | |
| 91 | - | ||
| 92 | - | async function saveBinding(keys: string[]) { | |
| 93 | - | if (!editingBinding) return; | |
| 94 | - | ||
| 95 | - | if (!getWebKitBridge()) { | |
| 96 | - | error = "Dumber bridge not available"; | |
| 97 | - | return; | |
| 98 | - | } | |
| 99 | - | ||
| 100 | - | // Timeout to detect if backend never responds (message silently dropped) | |
| 101 | - | const timeoutId = setTimeout(() => { | |
| 102 | - | error = "Save timed out - backend did not respond"; | |
| 103 | - | }, 5000); | |
| 104 | - | ||
| 105 | - | setupCallback<{ status: string; conflicts?: KeybindingConflict[] }>( | |
| 106 | - | "__dumber_keybinding_set", | |
| 107 | - | (response) => { | |
| 108 | - | clearTimeout(timeoutId); | |
| 109 | - | if (!response || response.status !== "success") { | |
| 110 | - | error = "Failed to save keybinding: invalid response"; | |
| 111 | - | return; | |
| 112 | - | } | |
| 113 | - | conflicts = response.conflicts?.length ? response.conflicts : []; | |
| 114 | - | loadKeybindings(); | |
| 115 | - | editingBinding = null; | |
| 116 | - | showSuccess("Keybinding saved successfully"); | |
| 117 | - | } | |
| 118 | - | ); | |
| 119 | - | setupCallback<string>("__dumber_keybinding_set_error", (msg) => { | |
| 120 | - | clearTimeout(timeoutId); | |
| 121 | - | error = typeof msg === "string" ? msg : "Failed to save keybinding"; | |
| 122 | - | }); | |
| 123 | - | ||
| 124 | - | // Create plain payload - extract primitive values explicitly to avoid any proxy issues | |
| 125 | - | const mode = String(editingBinding.mode); | |
| 126 | - | const action = String(editingBinding.action); | |
| 127 | - | const payload = { mode, action, keys: [...keys] }; | |
| 128 | - | ||
| 129 | - | try { | |
| 130 | - | postMessage({ | |
| 131 | - | type: "set_keybinding", | |
| 132 | - | webviewId: getWebViewId(), | |
| 133 | - | payload, | |
| 134 | - | }); | |
| 135 | - | } catch (err) { | |
| 136 | - | clearTimeout(timeoutId); | |
| 137 | - | error = err instanceof Error ? err.message : "Failed to save keybinding"; | |
| 138 | - | } | |
| 139 | - | } | |
| 140 | - | ||
| 141 | - | async function resetBinding(mode: string, action: string) { | |
| 142 | - | if (!getWebKitBridge()) return; | |
| 143 | - | ||
| 144 | - | setupCallback<void>("__dumber_keybinding_reset", () => { | |
| 145 | - | conflicts = []; | |
| 146 | - | loadKeybindings(); | |
| 147 | - | showSuccess("Keybinding reset to default"); | |
| 148 | - | }); | |
| 149 | - | setupCallback<string>("__dumber_keybinding_reset_error", (msg) => { | |
| 150 | - | error = typeof msg === "string" ? msg : "Failed to reset keybinding"; | |
| 151 | - | }); | |
| 152 | - | ||
| 153 | - | try { | |
| 154 | - | postMessage({ | |
| 155 | - | type: "reset_keybinding", | |
| 156 | - | webviewId: getWebViewId(), | |
| 157 | - | payload: { mode, action }, | |
| 158 | - | }); | |
| 159 | - | } catch (err) { | |
| 160 | - | error = err instanceof Error ? err.message : "Failed to reset keybinding"; | |
| 161 | - | } | |
| 162 | - | } | |
| 163 | - | ||
| 164 | - | // Reset All: update local state to defaults WITHOUT saving | |
| 165 | - | function resetAllBindings() { | |
| 166 | - | if (!keybindings) return; | |
| 167 | - | ||
| 168 | - | // Reset each binding's keys to default_keys in local state | |
| 169 | - | const resetGroups = keybindings.groups.map(group => ({ | |
| 170 | - | ...group, | |
| 171 | - | bindings: group.bindings.map(binding => ({ | |
| 172 | - | ...binding, | |
| 173 | - | keys: [...binding.default_keys], | |
| 174 | - | is_custom: false, | |
| 175 | - | })), | |
| 176 | - | })); | |
| 177 | - | ||
| 178 | - | keybindings = { groups: resetGroups }; | |
| 179 | - | hasPendingReset = true; | |
| 180 | - | conflicts = []; | |
| 181 | - | resetAllDialogOpen = false; | |
| 182 | - | showSuccess("Preview: all keybindings reset to defaults. Click Save to apply."); | |
| 183 | - | } | |
| 184 | - | ||
| 185 | - | // Actually save the reset to backend | |
| 186 | - | async function saveResetAll() { | |
| 187 | - | if (!getWebKitBridge()) return; | |
| 188 | - | ||
| 189 | - | savingReset = true; | |
| 190 | - | ||
| 191 | - | setupCallback<void>("__dumber_keybindings_reset_all", () => { | |
| 192 | - | hasPendingReset = false; | |
| 193 | - | savingReset = false; | |
| 194 | - | loadKeybindings(); | |
| 195 | - | showSuccess("All keybindings saved successfully"); | |
| 196 | - | }); | |
| 197 | - | setupCallback<string>("__dumber_keybindings_reset_all_error", (msg) => { | |
| 198 | - | error = typeof msg === "string" ? msg : "Failed to save keybindings"; | |
| 199 | - | savingReset = false; | |
| 200 | - | }); | |
| 201 | - | ||
| 202 | - | try { | |
| 203 | - | postMessage({ | |
| 204 | - | type: "reset_all_keybindings", | |
| 205 | - | webviewId: getWebViewId(), | |
| 206 | - | payload: {}, | |
| 207 | - | }); | |
| 208 | - | } catch (err) { | |
| 209 | - | error = err instanceof Error ? err.message : "Failed to save keybindings"; | |
| 210 | - | savingReset = false; | |
| 211 | - | } | |
| 212 | - | } | |
| 213 | - | ||
| 214 | - | // Cancel pending reset and reload from backend | |
| 215 | - | function cancelReset() { | |
| 216 | - | hasPendingReset = false; | |
| 217 | - | loadKeybindings(); | |
| 218 | - | } | |
| 219 | - | ||
| 220 | - | onMount(() => { | |
| 221 | - | loadKeybindings(); | |
| 222 | - | }); | |
| 223 | - | </script> | |
| 224 | - | ||
| 225 | - | <Card.Root class="rounded-none border-0 bg-transparent py-0 shadow-none"> | |
| 226 | - | <Card.Header> | |
| 227 | - | <div class="flex items-center justify-between"> | |
| 228 | - | <div> | |
| 229 | - | <Card.Title>Keybindings</Card.Title> | |
| 230 | - | <Card.Description> | |
| 231 | - | Customize keyboard shortcuts for all modes. | |
| 232 | - | </Card.Description> | |
| 233 | - | </div> | |
| 234 | - | <div class="flex gap-2"> | |
| 235 | - | {#if hasPendingReset} | |
| 236 | - | <Button variant="outline" size="sm" onclick={cancelReset} disabled={savingReset}> | |
| 237 | - | Cancel | |
| 238 | - | </Button> | |
| 239 | - | <Button size="sm" onclick={saveResetAll} disabled={savingReset}> | |
| 240 | - | {savingReset ? "Saving…" : "Save"} | |
| 241 | - | </Button> | |
| 242 | - | {:else} | |
| 243 | - | <Button variant="outline" size="sm" onclick={loadKeybindings}> | |
| 244 | - | <RefreshCw class="mr-1 size-4" /> | |
| 245 | - | Refresh | |
| 246 | - | </Button> | |
| 247 | - | <AlertDialog.Root bind:open={resetAllDialogOpen}> | |
| 248 | - | <AlertDialog.Trigger> | |
| 249 | - | {#snippet child({ props })} | |
| 250 | - | <Button variant="outline" size="sm" {...props}> | |
| 251 | - | <RotateCcw class="mr-1 size-4" /> | |
| 252 | - | Reset All | |
| 253 | - | </Button> | |
| 254 | - | {/snippet} | |
| 255 | - | </AlertDialog.Trigger> | |
| 256 | - | <AlertDialog.Content> | |
| 257 | - | <AlertDialog.Header> | |
| 258 | - | <AlertDialog.Title>Reset all keybindings?</AlertDialog.Title> | |
| 259 | - | <AlertDialog.Description> | |
| 260 | - | This will preview all keybindings with their default values. You'll need to click Save to apply. | |
| 261 | - | </AlertDialog.Description> | |
| 262 | - | </AlertDialog.Header> | |
| 263 | - | <AlertDialog.Footer> | |
| 264 | - | <AlertDialog.Cancel>Cancel</AlertDialog.Cancel> | |
| 265 | - | <AlertDialog.Action onclick={resetAllBindings}>Reset All</AlertDialog.Action> | |
| 266 | - | </AlertDialog.Footer> | |
| 267 | - | </AlertDialog.Content> | |
| 268 | - | </AlertDialog.Root> | |
| 269 | - | {/if} | |
| 270 | - | </div> | |
| 271 | - | </div> | |
| 272 | - | </Card.Header> | |
| 273 | - | <Card.Content class="space-y-6"> | |
| 274 | - | {#if hasPendingReset} | |
| 275 | - | <div class="flex items-center gap-2 rounded-md border border-yellow-500/50 bg-yellow-500/10 px-4 py-3 text-sm text-yellow-600 dark:text-yellow-400"> | |
| 276 | - | <AlertTriangle class="size-4" /> | |
| 277 | - | <span>Unsaved changes: all keybindings previewed with defaults. Click <strong>Save</strong> to apply or <strong>Cancel</strong> to discard.</span> | |
| 278 | - | </div> | |
| 279 | - | {/if} | |
| 280 | - | {#if loading} | |
| 281 | - | <div class="flex items-center justify-center py-8"> | |
| 282 | - | <Spinner class="size-6" /> | |
| 283 | - | </div> | |
| 284 | - | {:else if error} | |
| 285 | - | <div class="flex items-center gap-2 rounded-md border border-destructive/50 bg-destructive/10 px-4 py-3 text-sm text-destructive"> | |
| 286 | - | <AlertTriangle class="size-4" /> | |
| 287 | - | {error} | |
| 288 | - | </div> | |
| 289 | - | {/if} | |
| 290 | - | {#if successMessage} | |
| 291 | - | <div class="flex items-center gap-2 rounded-md border border-green-500/50 bg-green-500/10 px-4 py-3 text-sm text-green-600 dark:text-green-400"> | |
| 292 | - | <CheckCircle class="size-4" /> | |
| 293 | - | {successMessage} | |
| 294 | - | </div> | |
| 295 | - | {/if} | |
| 296 | - | {#if conflicts.length > 0} | |
| 297 | - | <div class="rounded-md border border-yellow-500/50 bg-yellow-500/10 px-4 py-3 text-sm"> | |
| 298 | - | <div class="flex items-center gap-2 text-yellow-600 dark:text-yellow-400"> | |
| 299 | - | <AlertTriangle class="size-4" /> | |
| 300 | - | <span class="font-medium">Keybinding conflicts detected</span> | |
| 301 | - | </div> | |
| 302 | - | <ul class="mt-2 list-disc pl-6 text-muted-foreground"> | |
| 303 | - | {#each conflicts as conflict (conflict.key + conflict.conflicting_action)} | |
| 304 | - | <li> | |
| 305 | - | <kbd class="font-mono text-xs">{conflict.key}</kbd> is also bound to | |
| 306 | - | <strong>{conflict.conflicting_action}</strong> in <em>{conflict.conflicting_mode}</em> mode | |
| 307 | - | </li> | |
| 308 | - | {/each} | |
| 309 | - | </ul> | |
| 310 | - | <button | |
| 311 | - | type="button" | |
| 312 | - | class="mt-2 text-xs text-muted-foreground hover:text-foreground" | |
| 313 | - | onclick={() => conflicts = []} | |
| 314 | - | > | |
| 315 | - | Dismiss | |
| 316 | - | </button> | |
| 317 | - | </div> | |
| 318 | - | {/if} | |
| 319 | - | {#if !loading && !error && keybindings} | |
| 320 | - | {#each keybindings.groups as group (group.mode)} | |
| 321 | - | <KeybindingGroup | |
| 322 | - | {group} | |
| 323 | - | onEdit={(action, entry) => startEdit(group.mode, action, entry)} | |
| 324 | - | onReset={(action) => resetBinding(group.mode, action)} | |
| 325 | - | /> | |
| 326 | - | {/each} | |
| 327 | - | {/if} | |
| 328 | - | {#if !loading && !keybindings && !error} | |
| 329 | - | <div class="text-center text-muted-foreground py-8">No keybindings available</div> | |
| 330 | - | {/if} | |
| 331 | - | </Card.Content> | |
| 332 | - | </Card.Root> | |
| 333 | - | ||
| 334 | - | {#if editingBinding} | |
| 335 | - | <KeyCaptureModal | |
| 336 | - | binding={editingBinding.entry} | |
| 337 | - | onSave={saveBinding} | |
| 338 | - | onCancel={() => (editingBinding = null)} | |
| 339 | - | /> | |
| 340 | - | {/if} | |
D webui/src/pages/config/ShortcutsTable.svelte +0 -170
| @@ -1,170 +0,0 @@ | |||
| 1 | - | <script lang="ts"> | |
| 2 | - | import { Check, Plus, Trash2, X } from "@lucide/svelte"; | |
| 3 | - | import { Button } from "$lib/components/ui/button"; | |
| 4 | - | import { Input } from "$lib/components/ui/input"; | |
| 5 | - | import * as Table from "$lib/components/ui/table"; | |
| 6 | - | ||
| 7 | - | type SearchShortcut = { | |
| 8 | - | url: string; | |
| 9 | - | description: string; | |
| 10 | - | }; | |
| 11 | - | ||
| 12 | - | interface Props { | |
| 13 | - | shortcuts: Record<string, SearchShortcut>; | |
| 14 | - | onUpdate: (shortcuts: Record<string, SearchShortcut>) => void; | |
| 15 | - | } | |
| 16 | - | ||
| 17 | - | let { shortcuts, onUpdate }: Props = $props(); | |
| 18 | - | ||
| 19 | - | let editingKey = $state<string | null>(null); | |
| 20 | - | let draftKey = $state(""); | |
| 21 | - | let draftUrl = $state(""); | |
| 22 | - | let draftDescription = $state(""); | |
| 23 | - | ||
| 24 | - | let newKey = $state(""); | |
| 25 | - | let newUrl = $state(""); | |
| 26 | - | let newDescription = $state(""); | |
| 27 | - | ||
| 28 | - | const entries = $derived( | |
| 29 | - | Object.entries(shortcuts ?? {}).sort(([a], [b]) => a.localeCompare(b)), | |
| 30 | - | ); | |
| 31 | - | ||
| 32 | - | function startEdit(key: string, shortcut: SearchShortcut) { | |
| 33 | - | editingKey = key; | |
| 34 | - | draftKey = key; | |
| 35 | - | draftUrl = shortcut.url; | |
| 36 | - | draftDescription = shortcut.description; | |
| 37 | - | } | |
| 38 | - | ||
| 39 | - | function cancelEdit() { | |
| 40 | - | editingKey = null; | |
| 41 | - | draftKey = ""; | |
| 42 | - | draftUrl = ""; | |
| 43 | - | draftDescription = ""; | |
| 44 | - | } | |
| 45 | - | ||
| 46 | - | function commitEdit() { | |
| 47 | - | if (!editingKey) return; | |
| 48 | - | const nextKey = draftKey.trim(); | |
| 49 | - | const nextUrl = draftUrl.trim(); | |
| 50 | - | const nextDescription = draftDescription.trim(); | |
| 51 | - | ||
| 52 | - | const updated: Record<string, SearchShortcut> = { ...shortcuts }; | |
| 53 | - | delete updated[editingKey]; | |
| 54 | - | if (nextKey) { | |
| 55 | - | updated[nextKey] = { url: nextUrl, description: nextDescription }; | |
| 56 | - | } | |
| 57 | - | onUpdate(updated); | |
| 58 | - | cancelEdit(); | |
| 59 | - | } | |
| 60 | - | ||
| 61 | - | function removeShortcut(key: string) { | |
| 62 | - | const updated: Record<string, SearchShortcut> = { ...shortcuts }; | |
| 63 | - | delete updated[key]; | |
| 64 | - | onUpdate(updated); | |
| 65 | - | } | |
| 66 | - | ||
| 67 | - | function addShortcut() { | |
| 68 | - | const key = newKey.trim(); | |
| 69 | - | if (!key) return; | |
| 70 | - | const updated: Record<string, SearchShortcut> = { ...shortcuts }; | |
| 71 | - | updated[key] = { url: newUrl.trim(), description: newDescription.trim() }; | |
| 72 | - | onUpdate(updated); | |
| 73 | - | newKey = ""; | |
| 74 | - | newUrl = ""; | |
| 75 | - | newDescription = ""; | |
| 76 | - | } | |
| 77 | - | </script> | |
| 78 | - | ||
| 79 | - | <div class="space-y-4"> | |
| 80 | - | <Table.Root class="rounded-md border border-border"> | |
| 81 | - | <Table.Header> | |
| 82 | - | <Table.Row> | |
| 83 | - | <Table.Head class="w-[140px]">Prefix</Table.Head> | |
| 84 | - | <Table.Head>URL Template</Table.Head> | |
| 85 | - | <Table.Head>Description</Table.Head> | |
| 86 | - | <Table.Head class="w-[160px] text-right">Actions</Table.Head> | |
| 87 | - | </Table.Row> | |
| 88 | - | </Table.Header> | |
| 89 | - | <Table.Body> | |
| 90 | - | {#if entries.length === 0} | |
| 91 | - | <Table.Row> | |
| 92 | - | <Table.Cell colspan={4} class="py-8 text-center text-sm text-muted-foreground"> | |
| 93 | - | No shortcuts yet. Add one below. | |
| 94 | - | </Table.Cell> | |
| 95 | - | </Table.Row> | |
| 96 | - | {/if} | |
| 97 | - | {#each entries as [key, shortcut] (key)} | |
| 98 | - | <Table.Row> | |
| 99 | - | {#if editingKey === key} | |
| 100 | - | <Table.Cell> | |
| 101 | - | <Input bind:value={draftKey} class="h-8" /> | |
| 102 | - | </Table.Cell> | |
| 103 | - | <Table.Cell> | |
| 104 | - | <Input bind:value={draftUrl} class="h-8" /> | |
| 105 | - | </Table.Cell> | |
| 106 | - | <Table.Cell> | |
| 107 | - | <Input bind:value={draftDescription} class="h-8" /> | |
| 108 | - | </Table.Cell> | |
| 109 | - | <Table.Cell class="text-right"> | |
| 110 | - | <div class="flex justify-end gap-2"> | |
| 111 | - | <Button variant="ghost" size="icon" onclick={commitEdit} type="button" aria-label="Save"> | |
| 112 | - | <Check size={16} /> | |
| 113 | - | </Button> | |
| 114 | - | <Button variant="ghost" size="icon" onclick={cancelEdit} type="button" aria-label="Cancel"> | |
| 115 | - | <X size={16} /> | |
| 116 | - | </Button> | |
| 117 | - | </div> | |
| 118 | - | </Table.Cell> | |
| 119 | - | {:else} | |
| 120 | - | <Table.Cell class="font-mono text-sm">{key}</Table.Cell> | |
| 121 | - | <Table.Cell class="truncate max-w-[360px]">{shortcut.url}</Table.Cell> | |
| 122 | - | <Table.Cell class="text-muted-foreground">{shortcut.description}</Table.Cell> | |
| 123 | - | <Table.Cell class="text-right"> | |
| 124 | - | <div class="flex justify-end gap-2"> | |
| 125 | - | <Button | |
| 126 | - | variant="outline" | |
| 127 | - | size="sm" | |
| 128 | - | onclick={() => startEdit(key, shortcut)} | |
| 129 | - | type="button" | |
| 130 | - | > | |
| 131 | - | Edit | |
| 132 | - | </Button> | |
| 133 | - | <Button | |
| 134 | - | variant="ghost" | |
| 135 | - | size="icon" | |
| 136 | - | onclick={() => removeShortcut(key)} | |
| 137 | - | type="button" | |
| 138 | - | aria-label={`Delete ${key}`} | |
| 139 | - | > | |
| 140 | - | <Trash2 size={16} /> | |
| 141 | - | </Button> | |
| 142 | - | </div> | |
| 143 | - | </Table.Cell> | |
| 144 | - | {/if} | |
| 145 | - | </Table.Row> | |
| 146 | - | {/each} | |
| 147 | - | </Table.Body> | |
| 148 | - | </Table.Root> | |
| 149 | - | ||
| 150 | - | <div class="grid gap-3 md:grid-cols-[140px_1fr_1fr_140px] items-end"> | |
| 151 | - | <div class="space-y-1"> | |
| 152 | - | <span class="text-xs font-semibold uppercase tracking-[0.2em] text-muted-foreground">Prefix</span> | |
| 153 | - | <Input bind:value={newKey} placeholder="g" /> | |
| 154 | - | </div> | |
| 155 | - | <div class="space-y-1"> | |
| 156 | - | <span class="text-xs font-semibold uppercase tracking-[0.2em] text-muted-foreground">URL Template</span> | |
| 157 | - | <Input bind:value={newUrl} placeholder="https://www.google.com/search?q=%s" /> | |
| 158 | - | </div> | |
| 159 | - | <div class="space-y-1"> | |
| 160 | - | <span class="text-xs font-semibold uppercase tracking-[0.2em] text-muted-foreground">Description</span> | |
| 161 | - | <Input bind:value={newDescription} placeholder="Google" /> | |
| 162 | - | </div> | |
| 163 | - | <div class="flex md:justify-end"> | |
| 164 | - | <Button variant="default" onclick={addShortcut} type="button" class="w-full md:w-auto"> | |
| 165 | - | <Plus size={16} /> | |
| 166 | - | Add Shortcut | |
| 167 | - | </Button> | |
| 168 | - | </div> | |
| 169 | - | </div> | |
| 170 | - | </div> | |
D webui/src/pages/error.ts +0 -11
| @@ -1,11 +0,0 @@ | |||
| 1 | - | import { mount } from "svelte"; | |
| 2 | - | import "../styles/app.css"; | |
| 3 | - | import ErrorPage from "./ErrorPage.svelte"; | |
| 4 | - | ||
| 5 | - | console.log("[dumber] Mounting ErrorPage component"); | |
| 6 | - | try { | |
| 7 | - | const app = mount(ErrorPage, { target: document.body }); | |
| 8 | - | console.log("[dumber] ErrorPage mounted successfully", app); | |
| 9 | - | } catch (error) { | |
| 10 | - | console.error("[dumber] Failed to mount ErrorPage:", error); | |
| 11 | - | } | |
D webui/src/pages/homepage.ts +0 -37
| @@ -1,37 +0,0 @@ | |||
| 1 | - | import { mount } from "svelte"; | |
| 2 | - | import "../styles/app.css"; | |
| 3 | - | import Homepage from "./Homepage.svelte"; | |
| 4 | - | ||
| 5 | - | // Proactively unregister any Service Workers and clear caches that may have been | |
| 6 | - | // installed by previous builds (e.g., SvelteKit), which can otherwise attempt to | |
| 7 | - | // request non-existent _app/immutable/* assets under dumb://homepage. | |
| 8 | - | async function cleanupServiceWorkersAndCaches() { | |
| 9 | - | try { | |
| 10 | - | if ("serviceWorker" in navigator) { | |
| 11 | - | const regs = await navigator.serviceWorker.getRegistrations(); | |
| 12 | - | await Promise.all(regs.map((r) => r.unregister().catch(() => {}))); | |
| 13 | - | } | |
| 14 | - | } catch { | |
| 15 | - | /* ignore */ | |
| 16 | - | } | |
| 17 | - | ||
| 18 | - | try { | |
| 19 | - | if ("caches" in window) { | |
| 20 | - | const keys = await caches.keys(); | |
| 21 | - | await Promise.all(keys.map((k) => caches.delete(k).catch(() => false))); | |
| 22 | - | } | |
| 23 | - | } catch { | |
| 24 | - | /* ignore */ | |
| 25 | - | } | |
| 26 | - | } | |
| 27 | - | ||
| 28 | - | // Mount the Homepage component to the DOM | |
| 29 | - | cleanupServiceWorkersAndCaches().finally(() => { | |
| 30 | - | console.log("[dumber] Mounting Homepage component to document.body"); | |
| 31 | - | try { | |
| 32 | - | const app = mount(Homepage, { target: document.body }); | |
| 33 | - | console.log("[dumber] Homepage component mounted successfully", app); | |
| 34 | - | } catch (error) { | |
| 35 | - | console.error("[dumber] Failed to mount Homepage component:", error); | |
| 36 | - | } | |
| 37 | - | }); | |
D webui/src/pages/homepage/analytics/DailyVisitsChart.svelte +0 -124
| @@ -1,124 +0,0 @@ | |||
| 1 | - | <script lang="ts"> | |
| 2 | - | import { Chart } from 'frappe-charts'; | |
| 3 | - | import { onMount } from 'svelte'; | |
| 4 | - | import type { DailyVisitCount } from '../types'; | |
| 5 | - | ||
| 6 | - | interface Props { | |
| 7 | - | dailyVisits: DailyVisitCount[]; | |
| 8 | - | } | |
| 9 | - | ||
| 10 | - | let { dailyVisits }: Props = $props(); | |
| 11 | - | ||
| 12 | - | let container: HTMLDivElement; | |
| 13 | - | let chart: Chart | null = null; | |
| 14 | - | ||
| 15 | - | // Get accent color from CSS variable | |
| 16 | - | function getAccentColor(): string { | |
| 17 | - | return getComputedStyle(document.documentElement).getPropertyValue('--primary').trim() || '#4ade80'; | |
| 18 | - | } | |
| 19 | - | ||
| 20 | - | // Convert data to Frappe Charts format | |
| 21 | - | const chartData = $derived.by(() => { | |
| 22 | - | if (!dailyVisits || dailyVisits.length === 0) { | |
| 23 | - | return { labels: [], datasets: [{ values: [] }] }; | |
| 24 | - | } | |
| 25 | - | ||
| 26 | - | const labels = dailyVisits.map(d => { | |
| 27 | - | const date = new Date(d.day + 'T12:00:00'); | |
| 28 | - | return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); | |
| 29 | - | }); | |
| 30 | - | const values = dailyVisits.map(d => d.visits); | |
| 31 | - | ||
| 32 | - | return { | |
| 33 | - | labels, | |
| 34 | - | datasets: [{ name: 'Visits', values }] | |
| 35 | - | }; | |
| 36 | - | }); | |
| 37 | - | ||
| 38 | - | const totalVisits = $derived(dailyVisits.reduce((sum, d) => sum + d.visits, 0)); | |
| 39 | - | ||
| 40 | - | // Initialize chart on mount | |
| 41 | - | onMount(() => { | |
| 42 | - | if (chartData.labels.length === 0) return; | |
| 43 | - | ||
| 44 | - | chart = new Chart(container, { | |
| 45 | - | data: chartData, | |
| 46 | - | type: 'line', | |
| 47 | - | height: 160, | |
| 48 | - | colors: [getAccentColor()], | |
| 49 | - | axisOptions: { xAxisMode: 'tick', xIsSeries: true }, | |
| 50 | - | lineOptions: { regionFill: 1, hideDots: 0, dotSize: 4 }, | |
| 51 | - | tooltipOptions: { formatTooltipY: (d: number) => `${d} visits` } | |
| 52 | - | }); | |
| 53 | - | ||
| 54 | - | return () => { | |
| 55 | - | chart?.destroy(); | |
| 56 | - | chart = null; | |
| 57 | - | }; | |
| 58 | - | }); | |
| 59 | - | ||
| 60 | - | // Update chart when data changes (after initial mount) | |
| 61 | - | $effect(() => { | |
| 62 | - | // Skip if chart not initialized yet or no data | |
| 63 | - | if (!chart) return; | |
| 64 | - | ||
| 65 | - | // Read chartData to track changes | |
| 66 | - | const data = chartData; | |
| 67 | - | if (data.labels.length === 0) return; | |
| 68 | - | ||
| 69 | - | chart.update(data); | |
| 70 | - | }); | |
| 71 | - | </script> | |
| 72 | - | ||
| 73 | - | <div class="chart-wrapper"> | |
| 74 | - | <div class="chart-header"> | |
| 75 | - | <span class="chart-label">Visits</span> | |
| 76 | - | <span class="chart-value">{totalVisits} total</span> | |
| 77 | - | </div> | |
| 78 | - | <div bind:this={container} class="chart-container"></div> | |
| 79 | - | </div> | |
| 80 | - | ||
| 81 | - | <style> | |
| 82 | - | .chart-wrapper { | |
| 83 | - | width: 100%; | |
| 84 | - | display: flex; | |
| 85 | - | flex-direction: column; | |
| 86 | - | } | |
| 87 | - | ||
| 88 | - | .chart-header { | |
| 89 | - | display: flex; | |
| 90 | - | align-items: center; | |
| 91 | - | gap: 0.5rem; | |
| 92 | - | padding: 0 0.25rem 0.5rem; | |
| 93 | - | font-size: 0.65rem; | |
| 94 | - | font-family: ui-monospace, 'Fira Code', 'Cascadia Code', Menlo, Monaco, Consolas, monospace; | |
| 95 | - | } | |
| 96 | - | ||
| 97 | - | .chart-label { | |
| 98 | - | color: var(--primary, #4ade80); | |
| 99 | - | font-weight: 600; | |
| 100 | - | } | |
| 101 | - | ||
| 102 | - | .chart-value { | |
| 103 | - | color: var(--muted-foreground, #737373); | |
| 104 | - | } | |
| 105 | - | ||
| 106 | - | .chart-container { | |
| 107 | - | width: 100%; | |
| 108 | - | } | |
| 109 | - | ||
| 110 | - | .chart-container :global(.frappe-chart) { | |
| 111 | - | font-family: ui-monospace, 'Fira Code', 'Cascadia Code', Menlo, Monaco, Consolas, monospace; | |
| 112 | - | } | |
| 113 | - | ||
| 114 | - | .chart-container :global(.frappe-chart .axis text), | |
| 115 | - | .chart-container :global(.frappe-chart .chart-label) { | |
| 116 | - | fill: var(--muted-foreground, #737373); | |
| 117 | - | font-size: 10px; | |
| 118 | - | } | |
| 119 | - | ||
| 120 | - | .chart-container :global(.frappe-chart .axis line), | |
| 121 | - | .chart-container :global(.frappe-chart .chart-strokes) { | |
| 122 | - | stroke: var(--border, #333); | |
| 123 | - | } | |
| 124 | - | </style> | |
D webui/src/pages/homepage/analytics/HourlyActivityChart.svelte +0 -130
| @@ -1,130 +0,0 @@ | |||
| 1 | - | <script lang="ts"> | |
| 2 | - | import { Chart } from 'frappe-charts'; | |
| 3 | - | import { onMount } from 'svelte'; | |
| 4 | - | import type { HourlyDistribution } from '../types'; | |
| 5 | - | ||
| 6 | - | interface Props { | |
| 7 | - | hourlyDistribution: HourlyDistribution[]; | |
| 8 | - | } | |
| 9 | - | ||
| 10 | - | let { hourlyDistribution }: Props = $props(); | |
| 11 | - | ||
| 12 | - | let container: HTMLDivElement; | |
| 13 | - | let chart: Chart | null = null; | |
| 14 | - | ||
| 15 | - | // Get accent color from CSS variable | |
| 16 | - | function getAccentColor(): string { | |
| 17 | - | return getComputedStyle(document.documentElement).getPropertyValue('--primary').trim() || '#4ade80'; | |
| 18 | - | } | |
| 19 | - | ||
| 20 | - | // Convert data to Frappe Charts format (ensure all 24 hours) | |
| 21 | - | const chartData = $derived.by(() => { | |
| 22 | - | // eslint-disable-next-line svelte/prefer-svelte-reactivity -- temporary Map for data transformation | |
| 23 | - | const visitsByHour = new Map<number, number>(); | |
| 24 | - | hourlyDistribution.forEach(h => visitsByHour.set(h.hour, h.visit_count)); | |
| 25 | - | ||
| 26 | - | const labels = Array.from({ length: 24 }, (_, i) => | |
| 27 | - | i % 6 === 0 ? `${String(i).padStart(2, '0')}h` : '' | |
| 28 | - | ); | |
| 29 | - | const values = Array.from({ length: 24 }, (_, i) => visitsByHour.get(i) ?? 0); | |
| 30 | - | ||
| 31 | - | return { | |
| 32 | - | labels, | |
| 33 | - | datasets: [{ name: 'Activity', values }] | |
| 34 | - | }; | |
| 35 | - | }); | |
| 36 | - | ||
| 37 | - | // Peak hour for display | |
| 38 | - | const peakHour = $derived.by(() => { | |
| 39 | - | let maxVisits = 0; | |
| 40 | - | let peak = 0; | |
| 41 | - | hourlyDistribution.forEach(h => { | |
| 42 | - | if (h.visit_count > maxVisits) { | |
| 43 | - | maxVisits = h.visit_count; | |
| 44 | - | peak = h.hour; | |
| 45 | - | } | |
| 46 | - | }); | |
| 47 | - | return `${String(peak).padStart(2, '0')}:00`; | |
| 48 | - | }); | |
| 49 | - | ||
| 50 | - | // Initialize chart on mount | |
| 51 | - | onMount(() => { | |
| 52 | - | chart = new Chart(container, { | |
| 53 | - | data: chartData, | |
| 54 | - | type: 'bar', | |
| 55 | - | height: 120, | |
| 56 | - | colors: [getAccentColor()], | |
| 57 | - | axisOptions: { xAxisMode: 'tick' }, | |
| 58 | - | barOptions: { spaceRatio: 0.3 }, | |
| 59 | - | tooltipOptions: { formatTooltipY: (d: number) => `${d} visits` } | |
| 60 | - | }); | |
| 61 | - | ||
| 62 | - | return () => { | |
| 63 | - | chart?.destroy(); | |
| 64 | - | chart = null; | |
| 65 | - | }; | |
| 66 | - | }); | |
| 67 | - | ||
| 68 | - | // Update chart when data changes (after initial mount) | |
| 69 | - | $effect(() => { | |
| 70 | - | if (!chart) return; | |
| 71 | - | ||
| 72 | - | const data = chartData; | |
| 73 | - | if (data.labels.length === 0) return; | |
| 74 | - | ||
| 75 | - | chart.update(data); | |
| 76 | - | }); | |
| 77 | - | </script> | |
| 78 | - | ||
| 79 | - | <div class="chart-wrapper"> | |
| 80 | - | <div class="chart-header"> | |
| 81 | - | <span class="chart-label">Activity</span> | |
| 82 | - | <span class="chart-value">peak at {peakHour}</span> | |
| 83 | - | </div> | |
| 84 | - | <div bind:this={container} class="chart-container"></div> | |
| 85 | - | </div> | |
| 86 | - | ||
| 87 | - | <style> | |
| 88 | - | .chart-wrapper { | |
| 89 | - | width: 100%; | |
| 90 | - | display: flex; | |
| 91 | - | flex-direction: column; | |
| 92 | - | } | |
| 93 | - | ||
| 94 | - | .chart-header { | |
| 95 | - | display: flex; | |
| 96 | - | align-items: center; | |
| 97 | - | gap: 0.5rem; | |
| 98 | - | padding: 0 0.25rem 0.5rem; | |
| 99 | - | font-size: 0.65rem; | |
| 100 | - | font-family: ui-monospace, 'Fira Code', 'Cascadia Code', Menlo, Monaco, Consolas, monospace; | |
| 101 | - | } | |
| 102 | - | ||
| 103 | - | .chart-label { | |
| 104 | - | color: var(--primary, #4ade80); | |
| 105 | - | font-weight: 600; | |
| 106 | - | } | |
| 107 | - | ||
| 108 | - | .chart-value { | |
| 109 | - | color: var(--muted-foreground, #737373); | |
| 110 | - | } | |
| 111 | - | ||
| 112 | - | .chart-container { | |
| 113 | - | width: 100%; | |
| 114 | - | } | |
| 115 | - | ||
| 116 | - | .chart-container :global(.frappe-chart) { | |
| 117 | - | font-family: ui-monospace, 'Fira Code', 'Cascadia Code', Menlo, Monaco, Consolas, monospace; | |
| 118 | - | } | |
| 119 | - | ||
| 120 | - | .chart-container :global(.frappe-chart .axis text), | |
| 121 | - | .chart-container :global(.frappe-chart .chart-label) { | |
| 122 | - | fill: var(--muted-foreground, #737373); | |
| 123 | - | font-size: 10px; | |
| 124 | - | } | |
| 125 | - | ||
| 126 | - | .chart-container :global(.frappe-chart .axis line), | |
| 127 | - | .chart-container :global(.frappe-chart .chart-strokes) { | |
| 128 | - | stroke: var(--border, #333); | |
| 129 | - | } | |
| 130 | - | </style> | |
D webui/src/pages/homepage/analytics/index.ts +0 -2
| @@ -1,2 +0,0 @@ | |||
| 1 | - | export { default as DailyVisitsChart } from './DailyVisitsChart.svelte'; | |
| 2 | - | export { default as HourlyActivityChart } from './HourlyActivityChart.svelte'; | |
D webui/src/pages/homepage/favorites/FavoriteCard.svelte +0 -280
| @@ -1,280 +0,0 @@ | |||
| 1 | - | <script lang="ts"> | |
| 2 | - | import type { Favorite } from '../types'; | |
| 3 | - | ||
| 4 | - | interface Props { | |
| 5 | - | favorite: Favorite; | |
| 6 | - | focused?: boolean; | |
| 7 | - | onSelect?: (fav: Favorite) => void; | |
| 8 | - | onEdit?: (fav: Favorite) => void; | |
| 9 | - | } | |
| 10 | - | ||
| 11 | - | let { favorite, focused = false, onSelect, onEdit }: Props = $props(); | |
| 12 | - | ||
| 13 | - | const getDomain = (url: string): string => { | |
| 14 | - | try { | |
| 15 | - | return new URL(url).hostname.replace('www.', ''); | |
| 16 | - | } catch { | |
| 17 | - | return url; | |
| 18 | - | } | |
| 19 | - | }; | |
| 20 | - | ||
| 21 | - | const getFaviconUrl = (fav: Favorite): string => { | |
| 22 | - | // Use provided favicon_url if available | |
| 23 | - | if (fav.favicon_url) return fav.favicon_url; | |
| 24 | - | // Handle internal dumb:// URLs - use embedded favicon | |
| 25 | - | if (fav.url.startsWith('dumb://')) { | |
| 26 | - | return 'favicon.png'; | |
| 27 | - | } | |
| 28 | - | // Construct DuckDuckGo favicon URL from domain | |
| 29 | - | const domain = getDomain(fav.url); | |
| 30 | - | return `https://icons.duckduckgo.com/ip3/${encodeURIComponent(domain)}.ico`; | |
| 31 | - | }; | |
| 32 | - | ||
| 33 | - | const getDisplayTitle = (fav: Favorite): string => { | |
| 34 | - | const trimmed = fav.title?.trim(); | |
| 35 | - | if (trimmed) return trimmed; | |
| 36 | - | return getDomain(fav.url) || fav.url; | |
| 37 | - | }; | |
| 38 | - | ||
| 39 | - | const handleSelect = () => { | |
| 40 | - | onSelect?.(favorite); | |
| 41 | - | }; | |
| 42 | - | ||
| 43 | - | const handleEdit = (e: Event) => { | |
| 44 | - | e.stopPropagation(); | |
| 45 | - | onEdit?.(favorite); | |
| 46 | - | }; | |
| 47 | - | ||
| 48 | - | const handleKeyDown = (e: KeyboardEvent) => { | |
| 49 | - | switch (e.key) { | |
| 50 | - | case 'Enter': | |
| 51 | - | handleSelect(); | |
| 52 | - | break; | |
| 53 | - | case 'e': | |
| 54 | - | onEdit?.(favorite); | |
| 55 | - | break; | |
| 56 | - | } | |
| 57 | - | }; | |
| 58 | - | </script> | |
| 59 | - | ||
| 60 | - | <div | |
| 61 | - | class="favorite-card" | |
| 62 | - | class:focused | |
| 63 | - | role="button" | |
| 64 | - | tabindex="0" | |
| 65 | - | data-favorite-id={favorite.id} | |
| 66 | - | onclick={handleSelect} | |
| 67 | - | onkeydown={handleKeyDown} | |
| 68 | - | > | |
| 69 | - | <div class="card-favicon"> | |
| 70 | - | <img | |
| 71 | - | src={getFaviconUrl(favorite)} | |
| 72 | - | alt="" | |
| 73 | - | class="favicon-img" | |
| 74 | - | onerror={(e) => { | |
| 75 | - | const target = e.target as HTMLImageElement; | |
| 76 | - | target.style.display = 'none'; | |
| 77 | - | const fallback = target.nextElementSibling as HTMLElement; | |
| 78 | - | if (fallback) fallback.style.display = 'flex'; | |
| 79 | - | }} | |
| 80 | - | /> | |
| 81 | - | <div class="favicon-fallback" style="display: none;"></div> | |
| 82 | - | {#if favorite.shortcut_key} | |
| 83 | - | <div class="shortcut-badge">{favorite.shortcut_key}</div> | |
| 84 | - | {/if} | |
| 85 | - | </div> | |
| 86 | - | ||
| 87 | - | <div class="card-content"> | |
| 88 | - | <span class="card-title">{getDisplayTitle(favorite)}</span> | |
| 89 | - | <span class="card-domain">{getDomain(favorite.url)}</span> | |
| 90 | - | </div> | |
| 91 | - | ||
| 92 | - | {#if favorite.tags && favorite.tags.length > 0} | |
| 93 | - | <div class="card-tags"> | |
| 94 | - | {#each favorite.tags.slice(0, 2) as tag (tag.id)} | |
| 95 | - | <span class="tag-chip" style="--tag-color: {tag.color}"> | |
| 96 | - | {tag.name} | |
| 97 | - | </span> | |
| 98 | - | {/each} | |
| 99 | - | {#if favorite.tags.length > 2} | |
| 100 | - | <span class="tag-more">+{favorite.tags.length - 2}</span> | |
| 101 | - | {/if} | |
| 102 | - | </div> | |
| 103 | - | {/if} | |
| 104 | - | ||
| 105 | - | <div class="card-actions"> | |
| 106 | - | <button | |
| 107 | - | class="action-btn" | |
| 108 | - | type="button" | |
| 109 | - | onclick={handleEdit} | |
| 110 | - | title="Edit (e)" | |
| 111 | - | > | |
| 112 | - | ||
| 113 | - | </button> | |
| 114 | - | </div> | |
| 115 | - | </div> | |
| 116 | - | ||
| 117 | - | <style> | |
| 118 | - | .favorite-card { | |
| 119 | - | display: grid; | |
| 120 | - | grid-template-columns: auto 1fr auto auto; | |
| 121 | - | align-items: center; | |
| 122 | - | gap: 0.75rem; | |
| 123 | - | padding: 0.65rem 0.85rem; | |
| 124 | - | border-width: 1px; | |
| 125 | - | border-style: solid; | |
| 126 | - | border-color: var(--border); | |
| 127 | - | background: color-mix(in srgb, var(--background) 92%, var(--card) 8%); | |
| 128 | - | transition: all 100ms ease; | |
| 129 | - | cursor: pointer; | |
| 130 | - | } | |
| 131 | - | ||
| 132 | - | .favorite-card:hover, | |
| 133 | - | .favorite-card.focused { | |
| 134 | - | background: color-mix(in srgb, var(--card) 40%, var(--background) 60%); | |
| 135 | - | border-color: color-mix(in srgb, var(--border) 50%, var(--foreground) 50%); | |
| 136 | - | } | |
| 137 | - | ||
| 138 | - | .favorite-card.focused { | |
| 139 | - | outline: 1px solid var(--primary, #4ade80); | |
| 140 | - | outline-offset: -1px; | |
| 141 | - | } | |
| 142 | - | ||
| 143 | - | .card-favicon { | |
| 144 | - | position: relative; | |
| 145 | - | width: 32px; | |
| 146 | - | height: 32px; | |
| 147 | - | border-width: 1px; | |
| 148 | - | border-style: solid; | |
| 149 | - | border-color: var(--border); | |
| 150 | - | background: var(--background); | |
| 151 | - | display: flex; | |
| 152 | - | align-items: center; | |
| 153 | - | justify-content: center; | |
| 154 | - | flex-shrink: 0; | |
| 155 | - | } | |
| 156 | - | ||
| 157 | - | .favicon-img { | |
| 158 | - | width: 18px; | |
| 159 | - | height: 18px; | |
| 160 | - | object-fit: contain; | |
| 161 | - | } | |
| 162 | - | ||
| 163 | - | .favicon-fallback { | |
| 164 | - | width: 18px; | |
| 165 | - | height: 18px; | |
| 166 | - | display: flex; | |
| 167 | - | align-items: center; | |
| 168 | - | justify-content: center; | |
| 169 | - | color: var(--muted-foreground); | |
| 170 | - | } | |
| 171 | - | ||
| 172 | - | .favicon-fallback::before { | |
| 173 | - | content: ''; | |
| 174 | - | font-size: 0.9rem; | |
| 175 | - | } | |
| 176 | - | ||
| 177 | - | .shortcut-badge { | |
| 178 | - | position: absolute; | |
| 179 | - | bottom: -4px; | |
| 180 | - | right: -4px; | |
| 181 | - | width: 14px; | |
| 182 | - | height: 14px; | |
| 183 | - | display: flex; | |
| 184 | - | align-items: center; | |
| 185 | - | justify-content: center; | |
| 186 | - | font-size: 0.55rem; | |
| 187 | - | font-weight: 700; | |
| 188 | - | color: var(--background); | |
| 189 | - | background: var(--primary, #4ade80); | |
| 190 | - | border-width: 1px; | |
| 191 | - | border-style: solid; | |
| 192 | - | border-color: var(--background); | |
| 193 | - | } | |
| 194 | - | ||
| 195 | - | .card-content { | |
| 196 | - | display: flex; | |
| 197 | - | flex-direction: column; | |
| 198 | - | gap: 0.15rem; | |
| 199 | - | min-width: 0; | |
| 200 | - | } | |
| 201 | - | ||
| 202 | - | .card-title { | |
| 203 | - | font-size: 0.8rem; | |
| 204 | - | font-weight: 500; | |
| 205 | - | color: var(--foreground); | |
| 206 | - | white-space: nowrap; | |
| 207 | - | overflow: hidden; | |
| 208 | - | text-overflow: ellipsis; | |
| 209 | - | } | |
| 210 | - | ||
| 211 | - | .card-domain { | |
| 212 | - | font-size: 0.65rem; | |
| 213 | - | color: var(--muted-foreground); | |
| 214 | - | letter-spacing: 0.06em; | |
| 215 | - | white-space: nowrap; | |
| 216 | - | overflow: hidden; | |
| 217 | - | text-overflow: ellipsis; | |
| 218 | - | } | |
| 219 | - | ||
| 220 | - | .card-tags { | |
| 221 | - | display: flex; | |
| 222 | - | align-items: center; | |
| 223 | - | gap: 0.3rem; | |
| 224 | - | } | |
| 225 | - | ||
| 226 | - | .tag-chip { | |
| 227 | - | font-size: 0.55rem; | |
| 228 | - | font-weight: 500; | |
| 229 | - | padding: 0.15rem 0.4rem; | |
| 230 | - | background: color-mix(in srgb, var(--tag-color) 20%, transparent); | |
| 231 | - | border-width: 1px; | |
| 232 | - | border-style: solid; | |
| 233 | - | border-color: var(--tag-color); | |
| 234 | - | color: var(--tag-color); | |
| 235 | - | letter-spacing: 0.05em; | |
| 236 | - | } | |
| 237 | - | ||
| 238 | - | .tag-more { | |
| 239 | - | font-size: 0.55rem; | |
| 240 | - | color: var(--muted-foreground); | |
| 241 | - | padding: 0.15rem 0.3rem; | |
| 242 | - | border-width: 1px; | |
| 243 | - | border-style: solid; | |
| 244 | - | border-color: var(--border); | |
| 245 | - | } | |
| 246 | - | ||
| 247 | - | .card-actions { | |
| 248 | - | display: flex; | |
| 249 | - | gap: 0.3rem; | |
| 250 | - | opacity: 0; | |
| 251 | - | transition: opacity 100ms ease; | |
| 252 | - | } | |
| 253 | - | ||
| 254 | - | .favorite-card:hover .card-actions, | |
| 255 | - | .favorite-card.focused .card-actions { | |
| 256 | - | opacity: 1; | |
| 257 | - | } | |
| 258 | - | ||
| 259 | - | .action-btn { | |
| 260 | - | width: 24px; | |
| 261 | - | height: 24px; | |
| 262 | - | display: flex; | |
| 263 | - | align-items: center; | |
| 264 | - | justify-content: center; | |
| 265 | - | font-size: 0.8rem; | |
| 266 | - | border-width: 1px; | |
| 267 | - | border-style: solid; | |
| 268 | - | border-color: var(--border); | |
| 269 | - | background: transparent; | |
| 270 | - | color: var(--muted-foreground); | |
| 271 | - | cursor: pointer; | |
| 272 | - | transition: all 100ms ease; | |
| 273 | - | } | |
| 274 | - | ||
| 275 | - | .action-btn:hover { | |
| 276 | - | color: var(--foreground); | |
| 277 | - | border-color: var(--foreground); | |
| 278 | - | background: color-mix(in srgb, var(--card) 50%, transparent); | |
| 279 | - | } | |
| 280 | - | </style> | |
D webui/src/pages/homepage/favorites/FavoriteEditor.svelte +0 -495
| @@ -1,495 +0,0 @@ | |||
| 1 | - | <script lang="ts"> | |
| 2 | - | import type { Favorite, Folder, Tag } from '../types'; | |
| 3 | - | ||
| 4 | - | interface Props { | |
| 5 | - | favorite: Favorite; | |
| 6 | - | folders: Folder[]; | |
| 7 | - | tags: Tag[]; | |
| 8 | - | onSave: (updates: Partial<Favorite>) => void; | |
| 9 | - | onDelete: (id: number) => void; | |
| 10 | - | onClose: () => void; | |
| 11 | - | onAssignTag: (favoriteId: number, tagId: number) => void; | |
| 12 | - | onRemoveTag: (favoriteId: number, tagId: number) => void; | |
| 13 | - | } | |
| 14 | - | ||
| 15 | - | const props: Props = $props(); | |
| 16 | - | const { folders, tags, onSave, onDelete, onClose, onAssignTag, onRemoveTag } = props; | |
| 17 | - | // Capture initial values for form state (component remounts for different favorites) | |
| 18 | - | const initialFavorite = props.favorite; | |
| 19 | - | ||
| 20 | - | // Form state | |
| 21 | - | let title = $state(initialFavorite.title || ''); | |
| 22 | - | let folderId = $state<number | null>(initialFavorite.folder_id); | |
| 23 | - | let shortcutKey = $state<number | null>(initialFavorite.shortcut_key); | |
| 24 | - | let confirmDelete = $state(false); | |
| 25 | - | ||
| 26 | - | // Track current favorite for reactive operations | |
| 27 | - | const favorite = $derived(props.favorite); | |
| 28 | - | ||
| 29 | - | // Get favorite's current tags | |
| 30 | - | const favoriteTags = $derived(favorite.tags || []); | |
| 31 | - | const hasTag = (tagId: number) => favoriteTags.some(t => t.id === tagId); | |
| 32 | - | ||
| 33 | - | const handleSave = () => { | |
| 34 | - | onSave({ | |
| 35 | - | id: favorite.id, | |
| 36 | - | title: title.trim() || null, | |
| 37 | - | folder_id: folderId, | |
| 38 | - | shortcut_key: shortcutKey, | |
| 39 | - | }); | |
| 40 | - | onClose(); | |
| 41 | - | }; | |
| 42 | - | ||
| 43 | - | const handleDelete = () => { | |
| 44 | - | if (!confirmDelete) { | |
| 45 | - | confirmDelete = true; | |
| 46 | - | return; | |
| 47 | - | } | |
| 48 | - | onDelete(favorite.id); | |
| 49 | - | onClose(); | |
| 50 | - | }; | |
| 51 | - | ||
| 52 | - | const handleTagClick = (tagId: number) => { | |
| 53 | - | if (hasTag(tagId)) { | |
| 54 | - | onRemoveTag(favorite.id, tagId); | |
| 55 | - | } else { | |
| 56 | - | onAssignTag(favorite.id, tagId); | |
| 57 | - | } | |
| 58 | - | }; | |
| 59 | - | ||
| 60 | - | const handleKeyDown = (e: KeyboardEvent) => { | |
| 61 | - | switch (e.key) { | |
| 62 | - | case 'Escape': | |
| 63 | - | e.preventDefault(); | |
| 64 | - | if (confirmDelete) { | |
| 65 | - | confirmDelete = false; | |
| 66 | - | } else { | |
| 67 | - | onClose(); | |
| 68 | - | } | |
| 69 | - | break; | |
| 70 | - | case 'Enter': | |
| 71 | - | if (e.ctrlKey || e.metaKey) { | |
| 72 | - | e.preventDefault(); | |
| 73 | - | handleSave(); | |
| 74 | - | } | |
| 75 | - | break; | |
| 76 | - | } | |
| 77 | - | }; | |
| 78 | - | ||
| 79 | - | const handleOverlayClick = (e: Event) => { | |
| 80 | - | if (e.target === e.currentTarget) { | |
| 81 | - | onClose(); | |
| 82 | - | } | |
| 83 | - | }; | |
| 84 | - | </script> | |
| 85 | - | ||
| 86 | - | <svelte:window onkeydown={handleKeyDown} /> | |
| 87 | - | ||
| 88 | - | <div | |
| 89 | - | class="editor-overlay" | |
| 90 | - | onclick={handleOverlayClick} | |
| 91 | - | onkeydown={(e) => { if (e.key === 'Escape') handleOverlayClick(e); }} | |
| 92 | - | role="button" | |
| 93 | - | tabindex="0" | |
| 94 | - | aria-label="Close editor" | |
| 95 | - | > | |
| 96 | - | <div class="editor-modal"> | |
| 97 | - | <div class="modal-header"> | |
| 98 | - | <span class="modal-icon"></span> | |
| 99 | - | <span class="modal-title">EDIT FAVORITE</span> | |
| 100 | - | <button class="close-btn" type="button" onclick={onClose}> | |
| 101 | - | <kbd>Esc</kbd> | |
| 102 | - | </button> | |
| 103 | - | </div> | |
| 104 | - | ||
| 105 | - | <div class="modal-body"> | |
| 106 | - | <!-- URL (readonly) --> | |
| 107 | - | <div class="form-group"> | |
| 108 | - | <span class="form-label" id="url-label">URL</span> | |
| 109 | - | <div class="url-display" aria-labelledby="url-label">{favorite.url}</div> | |
| 110 | - | </div> | |
| 111 | - | ||
| 112 | - | <!-- Title --> | |
| 113 | - | <div class="form-group"> | |
| 114 | - | <label class="form-label" for="fav-title">TITLE</label> | |
| 115 | - | <input | |
| 116 | - | id="fav-title" | |
| 117 | - | type="text" | |
| 118 | - | class="form-input" | |
| 119 | - | bind:value={title} | |
| 120 | - | placeholder="Custom title (optional)" | |
| 121 | - | /> | |
| 122 | - | </div> | |
| 123 | - | ||
| 124 | - | <!-- Folder --> | |
| 125 | - | <div class="form-group"> | |
| 126 | - | <label class="form-label" for="fav-folder">FOLDER</label> | |
| 127 | - | <select id="fav-folder" class="form-select" bind:value={folderId}> | |
| 128 | - | <option value={null}>— No folder —</option> | |
| 129 | - | {#each folders as folder (folder.id)} | |
| 130 | - | <option value={folder.id}> | |
| 131 | - | {folder.icon || ''} {folder.name} | |
| 132 | - | </option> | |
| 133 | - | {/each} | |
| 134 | - | </select> | |
| 135 | - | </div> | |
| 136 | - | ||
| 137 | - | <!-- Shortcut Key --> | |
| 138 | - | <div class="form-group"> | |
| 139 | - | <label class="form-label" for="fav-shortcut">QUICK ACCESS KEY</label> | |
| 140 | - | <select id="fav-shortcut" class="form-select" bind:value={shortcutKey}> | |
| 141 | - | <option value={null}>— None —</option> | |
| 142 | - | {#each [1, 2, 3, 4, 5, 6, 7, 8, 9] as key (key)} | |
| 143 | - | <option value={key}>{key}</option> | |
| 144 | - | {/each} | |
| 145 | - | </select> | |
| 146 | - | <span class="form-hint">Press 1-9 on homepage to open directly</span> | |
| 147 | - | </div> | |
| 148 | - | ||
| 149 | - | <!-- Tags --> | |
| 150 | - | <div class="form-group"> | |
| 151 | - | <span class="form-label" id="tags-label">TAGS</span> | |
| 152 | - | <div class="tag-selector" role="group" aria-labelledby="tags-label"> | |
| 153 | - | {#if tags.length === 0} | |
| 154 | - | <span class="no-tags">No tags available</span> | |
| 155 | - | {:else} | |
| 156 | - | {#each tags as tag (tag.id)} | |
| 157 | - | <button | |
| 158 | - | class="tag-chip" | |
| 159 | - | class:selected={hasTag(tag.id)} | |
| 160 | - | type="button" | |
| 161 | - | onclick={() => handleTagClick(tag.id)} | |
| 162 | - | style="--tag-color: {tag.color}" | |
| 163 | - | > | |
| 164 | - | <span class="tag-dot"></span> | |
| 165 | - | <span class="tag-name">{tag.name}</span> | |
| 166 | - | </button> | |
| 167 | - | {/each} | |
| 168 | - | {/if} | |
| 169 | - | </div> | |
| 170 | - | </div> | |
| 171 | - | </div> | |
| 172 | - | ||
| 173 | - | <div class="modal-footer"> | |
| 174 | - | {#if confirmDelete} | |
| 175 | - | <div class="confirm-delete"> | |
| 176 | - | <span class="confirm-text">Delete this favorite?</span> | |
| 177 | - | <button class="btn btn-cancel" type="button" onclick={() => confirmDelete = false}> | |
| 178 | - | CANCEL | |
| 179 | - | </button> | |
| 180 | - | <button class="btn btn-danger" type="button" onclick={handleDelete}> | |
| 181 | - | DELETE | |
| 182 | - | </button> | |
| 183 | - | </div> | |
| 184 | - | {:else} | |
| 185 | - | <button class="btn btn-danger-outline" type="button" onclick={handleDelete}> | |
| 186 | - | DELETE | |
| 187 | - | </button> | |
| 188 | - | <div class="footer-right"> | |
| 189 | - | <button class="btn btn-secondary" type="button" onclick={onClose}> | |
| 190 | - | CANCEL | |
| 191 | - | </button> | |
| 192 | - | <button class="btn btn-primary" type="button" onclick={handleSave}> | |
| 193 | - | SAVE | |
| 194 | - | <kbd>Ctrl+Enter</kbd> | |
| 195 | - | </button> | |
| 196 | - | </div> | |
| 197 | - | {/if} | |
| 198 | - | </div> | |
| 199 | - | </div> | |
| 200 | - | </div> | |
| 201 | - | ||
| 202 | - | <style> | |
| 203 | - | .editor-overlay { | |
| 204 | - | position: fixed; | |
| 205 | - | inset: 0; | |
| 206 | - | z-index: 100; | |
| 207 | - | display: flex; | |
| 208 | - | align-items: center; | |
| 209 | - | justify-content: center; | |
| 210 | - | background: rgb(0 0 0 / 0.75); | |
| 211 | - | backdrop-filter: blur(4px); | |
| 212 | - | animation: fade-in 150ms ease; | |
| 213 | - | } | |
| 214 | - | ||
| 215 | - | @keyframes fade-in { | |
| 216 | - | from { opacity: 0; } | |
| 217 | - | } | |
| 218 | - | ||
| 219 | - | .editor-modal { | |
| 220 | - | width: 100%; | |
| 221 | - | max-width: 480px; | |
| 222 | - | margin: 1rem; | |
| 223 | - | background: var(--card); | |
| 224 | - | border-width: 1px; | |
| 225 | - | border-style: solid; | |
| 226 | - | border-color: var(--border); | |
| 227 | - | box-shadow: 0 24px 48px -12px rgb(0 0 0 / 0.6); | |
| 228 | - | animation: modal-in 150ms ease; | |
| 229 | - | } | |
| 230 | - | ||
| 231 | - | @keyframes modal-in { | |
| 232 | - | from { | |
| 233 | - | opacity: 0; | |
| 234 | - | transform: scale(0.96) translateY(-8px); | |
| 235 | - | } | |
| 236 | - | } | |
| 237 | - | ||
| 238 | - | .modal-header { | |
| 239 | - | display: flex; | |
| 240 | - | align-items: center; | |
| 241 | - | gap: 0.6rem; | |
| 242 | - | padding: 0.75rem 1rem; | |
| 243 | - | border-bottom: 1px solid var(--border); | |
| 244 | - | background: color-mix(in srgb, var(--background) 80%, transparent); | |
| 245 | - | } | |
| 246 | - | ||
| 247 | - | .modal-icon::before { | |
| 248 | - | content: ''; | |
| 249 | - | font-size: 0.9rem; | |
| 250 | - | color: var(--primary, #4ade80); | |
| 251 | - | } | |
| 252 | - | ||
| 253 | - | .modal-title { | |
| 254 | - | flex: 1; | |
| 255 | - | font-size: 0.72rem; | |
| 256 | - | font-weight: 600; | |
| 257 | - | letter-spacing: 0.1em; | |
| 258 | - | color: var(--foreground); | |
| 259 | - | } | |
| 260 | - | ||
| 261 | - | .close-btn { | |
| 262 | - | padding: 0.25rem 0.5rem; | |
| 263 | - | background: transparent; | |
| 264 | - | border-width: 1px; | |
| 265 | - | border-style: solid; | |
| 266 | - | border-color: var(--border); | |
| 267 | - | cursor: pointer; | |
| 268 | - | } | |
| 269 | - | ||
| 270 | - | .close-btn kbd { | |
| 271 | - | font-size: 0.6rem; | |
| 272 | - | color: var(--muted-foreground); | |
| 273 | - | } | |
| 274 | - | ||
| 275 | - | .close-btn:hover { | |
| 276 | - | border-color: var(--foreground); | |
| 277 | - | } | |
| 278 | - | ||
| 279 | - | .close-btn:hover kbd { | |
| 280 | - | color: var(--foreground); | |
| 281 | - | } | |
| 282 | - | ||
| 283 | - | .modal-body { | |
| 284 | - | padding: 1rem; | |
| 285 | - | display: flex; | |
| 286 | - | flex-direction: column; | |
| 287 | - | gap: 1rem; | |
| 288 | - | } | |
| 289 | - | ||
| 290 | - | .form-group { | |
| 291 | - | display: flex; | |
| 292 | - | flex-direction: column; | |
| 293 | - | gap: 0.35rem; | |
| 294 | - | } | |
| 295 | - | ||
| 296 | - | .form-label { | |
| 297 | - | font-size: 0.6rem; | |
| 298 | - | font-weight: 600; | |
| 299 | - | letter-spacing: 0.12em; | |
| 300 | - | color: var(--muted-foreground); | |
| 301 | - | } | |
| 302 | - | ||
| 303 | - | .url-display { | |
| 304 | - | font-size: 0.72rem; | |
| 305 | - | color: var(--foreground); | |
| 306 | - | padding: 0.5rem 0.65rem; | |
| 307 | - | border-width: 1px; | |
| 308 | - | border-style: solid; | |
| 309 | - | border-color: var(--border); | |
| 310 | - | background: var(--background); | |
| 311 | - | white-space: nowrap; | |
| 312 | - | overflow: hidden; | |
| 313 | - | text-overflow: ellipsis; | |
| 314 | - | } | |
| 315 | - | ||
| 316 | - | .form-input, | |
| 317 | - | .form-select { | |
| 318 | - | font-family: inherit; | |
| 319 | - | font-size: 0.75rem; | |
| 320 | - | padding: 0.5rem 0.65rem; | |
| 321 | - | border-width: 1px; | |
| 322 | - | border-style: solid; | |
| 323 | - | border-color: var(--border); | |
| 324 | - | background: var(--background); | |
| 325 | - | color: var(--foreground); | |
| 326 | - | outline: none; | |
| 327 | - | transition: border-color 100ms ease; | |
| 328 | - | } | |
| 329 | - | ||
| 330 | - | .form-input:focus, | |
| 331 | - | .form-select:focus { | |
| 332 | - | border-color: var(--primary, #4ade80); | |
| 333 | - | } | |
| 334 | - | ||
| 335 | - | .form-input::placeholder { | |
| 336 | - | color: var(--muted-foreground); | |
| 337 | - | } | |
| 338 | - | ||
| 339 | - | .form-hint { | |
| 340 | - | font-size: 0.6rem; | |
| 341 | - | color: var(--muted-foreground); | |
| 342 | - | letter-spacing: 0.05em; | |
| 343 | - | } | |
| 344 | - | ||
| 345 | - | .tag-selector { | |
| 346 | - | display: flex; | |
| 347 | - | flex-wrap: wrap; | |
| 348 | - | gap: 0.35rem; | |
| 349 | - | } | |
| 350 | - | ||
| 351 | - | .no-tags { | |
| 352 | - | font-size: 0.65rem; | |
| 353 | - | color: var(--muted-foreground); | |
| 354 | - | font-style: italic; | |
| 355 | - | } | |
| 356 | - | ||
| 357 | - | .tag-chip { | |
| 358 | - | display: inline-flex; | |
| 359 | - | align-items: center; | |
| 360 | - | gap: 0.35rem; | |
| 361 | - | padding: 0.25rem 0.5rem; | |
| 362 | - | font-size: 0.62rem; | |
| 363 | - | font-weight: 500; | |
| 364 | - | letter-spacing: 0.05em; | |
| 365 | - | border-width: 1px; | |
| 366 | - | border-style: solid; | |
| 367 | - | border-color: var(--tag-color); | |
| 368 | - | background: transparent; | |
| 369 | - | color: var(--foreground); | |
| 370 | - | cursor: pointer; | |
| 371 | - | transition: all 100ms ease; | |
| 372 | - | } | |
| 373 | - | ||
| 374 | - | .tag-chip:hover { | |
| 375 | - | background: color-mix(in srgb, var(--tag-color) 15%, transparent); | |
| 376 | - | } | |
| 377 | - | ||
| 378 | - | .tag-chip.selected { | |
| 379 | - | background: color-mix(in srgb, var(--tag-color) 25%, transparent); | |
| 380 | - | border-width: 2px; | |
| 381 | - | padding: calc(0.25rem - 1px) calc(0.5rem - 1px); | |
| 382 | - | } | |
| 383 | - | ||
| 384 | - | .tag-dot { | |
| 385 | - | width: 6px; | |
| 386 | - | height: 6px; | |
| 387 | - | background: var(--tag-color); | |
| 388 | - | border-radius: 50%; | |
| 389 | - | } | |
| 390 | - | ||
| 391 | - | .tag-name { | |
| 392 | - | text-transform: lowercase; | |
| 393 | - | } | |
| 394 | - | ||
| 395 | - | .modal-footer { | |
| 396 | - | display: flex; | |
| 397 | - | align-items: center; | |
| 398 | - | justify-content: space-between; | |
| 399 | - | gap: 0.5rem; | |
| 400 | - | padding: 0.75rem 1rem; | |
| 401 | - | border-top: 1px solid var(--border); | |
| 402 | - | background: color-mix(in srgb, var(--background) 60%, transparent); | |
| 403 | - | } | |
| 404 | - | ||
| 405 | - | .footer-right { | |
| 406 | - | display: flex; | |
| 407 | - | gap: 0.5rem; | |
| 408 | - | } | |
| 409 | - | ||
| 410 | - | .btn { | |
| 411 | - | display: inline-flex; | |
| 412 | - | align-items: center; | |
| 413 | - | gap: 0.5rem; | |
| 414 | - | padding: 0.5rem 0.85rem; | |
| 415 | - | font-size: 0.68rem; | |
| 416 | - | font-weight: 500; | |
| 417 | - | letter-spacing: 0.08em; | |
| 418 | - | border-width: 1px; | |
| 419 | - | border-style: solid; | |
| 420 | - | border-color: var(--border); | |
| 421 | - | cursor: pointer; | |
| 422 | - | transition: all 100ms ease; | |
| 423 | - | } | |
| 424 | - | ||
| 425 | - | .btn kbd { | |
| 426 | - | font-size: 0.55rem; | |
| 427 | - | padding: 0.1rem 0.3rem; | |
| 428 | - | border: 1px solid; | |
| 429 | - | opacity: 0.6; | |
| 430 | - | } | |
| 431 | - | ||
| 432 | - | .btn-secondary { | |
| 433 | - | background: transparent; | |
| 434 | - | color: var(--muted-foreground); | |
| 435 | - | } | |
| 436 | - | ||
| 437 | - | .btn-secondary:hover { | |
| 438 | - | color: var(--foreground); | |
| 439 | - | border-color: var(--foreground); | |
| 440 | - | } | |
| 441 | - | ||
| 442 | - | .btn-primary { | |
| 443 | - | background: var(--primary, #4ade80); | |
| 444 | - | color: var(--background); | |
| 445 | - | border-color: var(--primary, #4ade80); | |
| 446 | - | } | |
| 447 | - | ||
| 448 | - | .btn-primary:hover { | |
| 449 | - | filter: brightness(1.1); | |
| 450 | - | } | |
| 451 | - | ||
| 452 | - | .btn-danger-outline { | |
| 453 | - | background: transparent; | |
| 454 | - | color: var(--muted-foreground); | |
| 455 | - | } | |
| 456 | - | ||
| 457 | - | .btn-danger-outline:hover { | |
| 458 | - | color: var(--destructive); | |
| 459 | - | border-color: var(--destructive); | |
| 460 | - | } | |
| 461 | - | ||
| 462 | - | .btn-danger { | |
| 463 | - | background: var(--destructive); | |
| 464 | - | color: var(--destructive-foreground); | |
| 465 | - | border-color: color-mix(in srgb, var(--destructive) 80%, black 20%); | |
| 466 | - | } | |
| 467 | - | ||
| 468 | - | .btn-danger:hover { | |
| 469 | - | background: color-mix(in srgb, var(--destructive) 85%, white 15%); | |
| 470 | - | } | |
| 471 | - | ||
| 472 | - | .btn-cancel { | |
| 473 | - | background: transparent; | |
| 474 | - | color: var(--muted-foreground); | |
| 475 | - | } | |
| 476 | - | ||
| 477 | - | .btn-cancel:hover { | |
| 478 | - | color: var(--foreground); | |
| 479 | - | border-color: var(--foreground); | |
| 480 | - | } | |
| 481 | - | ||
| 482 | - | .confirm-delete { | |
| 483 | - | display: flex; | |
| 484 | - | align-items: center; | |
| 485 | - | gap: 0.75rem; | |
| 486 | - | width: 100%; | |
| 487 | - | } | |
| 488 | - | ||
| 489 | - | .confirm-text { | |
| 490 | - | flex: 1; | |
| 491 | - | font-size: 0.72rem; | |
| 492 | - | color: var(--destructive); | |
| 493 | - | font-weight: 500; | |
| 494 | - | } | |
| 495 | - | </style> | |
D webui/src/pages/homepage/favorites/FavoriteGrid.svelte +0 -108
| @@ -1,108 +0,0 @@ | |||
| 1 | - | <script lang="ts"> | |
| 2 | - | import type { Favorite } from '../types'; | |
| 3 | - | import { homepageState } from '../state.svelte'; | |
| 4 | - | import FavoriteCard from './FavoriteCard.svelte'; | |
| 5 | - | ||
| 6 | - | interface Props { | |
| 7 | - | favorites: Favorite[]; | |
| 8 | - | onSelectFavorite?: (fav: Favorite) => void; | |
| 9 | - | onEditFavorite?: (fav: Favorite) => void; | |
| 10 | - | } | |
| 11 | - | ||
| 12 | - | let { favorites, onSelectFavorite, onEditFavorite }: Props = $props(); | |
| 13 | - | ||
| 14 | - | let containerRef = $state<HTMLElement | null>(null); | |
| 15 | - | ||
| 16 | - | const getFocusedFavorite = () => { | |
| 17 | - | return favorites[homepageState.focusedIndex] ?? null; | |
| 18 | - | }; | |
| 19 | - | ||
| 20 | - | // Track previous focused index to only scroll on actual navigation | |
| 21 | - | let prevFocusedIndex = $state(-1); | |
| 22 | - | ||
| 23 | - | // Scroll focused item into view when focusedIndex changes via keyboard navigation | |
| 24 | - | $effect(() => { | |
| 25 | - | // Skip if panel not active or command palette is open | |
| 26 | - | if (!containerRef || homepageState.activePanel !== 'favorites') return; | |
| 27 | - | if (homepageState.commandPaletteOpen) return; | |
| 28 | - | ||
| 29 | - | const currentIndex = homepageState.focusedIndex; | |
| 30 | - | ||
| 31 | - | // Only scroll if index actually changed (not on initial render or modal close) | |
| 32 | - | if (prevFocusedIndex === currentIndex) return; | |
| 33 | - | if (prevFocusedIndex === -1) { | |
| 34 | - | // First render - just track, don't scroll | |
| 35 | - | prevFocusedIndex = currentIndex; | |
| 36 | - | return; | |
| 37 | - | } | |
| 38 | - | ||
| 39 | - | prevFocusedIndex = currentIndex; | |
| 40 | - | ||
| 41 | - | const focusedFav = getFocusedFavorite(); | |
| 42 | - | if (!focusedFav) return; | |
| 43 | - | ||
| 44 | - | // Find the focused element by favorite ID | |
| 45 | - | const el = containerRef.querySelector(`[data-favorite-id="${focusedFav.id}"]`); | |
| 46 | - | if (el) { | |
| 47 | - | el.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); | |
| 48 | - | } | |
| 49 | - | }); | |
| 50 | - | </script> | |
| 51 | - | ||
| 52 | - | <div class="favorite-grid" bind:this={containerRef}> | |
| 53 | - | {#if favorites.length === 0} | |
| 54 | - | <div class="empty-state"> | |
| 55 | - | <span class="empty-icon"></span> | |
| 56 | - | <span class="empty-text">NO FAVORITES</span> | |
| 57 | - | <span class="empty-hint">Pin sites from history to add them here</span> | |
| 58 | - | </div> | |
| 59 | - | {:else} | |
| 60 | - | {#each favorites as favorite (favorite.id)} | |
| 61 | - | <FavoriteCard | |
| 62 | - | {favorite} | |
| 63 | - | focused={getFocusedFavorite()?.id === favorite.id} | |
| 64 | - | onSelect={onSelectFavorite} | |
| 65 | - | onEdit={onEditFavorite} | |
| 66 | - | /> | |
| 67 | - | {/each} | |
| 68 | - | {/if} | |
| 69 | - | </div> | |
| 70 | - | ||
| 71 | - | <style> | |
| 72 | - | .favorite-grid { | |
| 73 | - | display: flex; | |
| 74 | - | flex-direction: column; | |
| 75 | - | gap: 0.5rem; | |
| 76 | - | } | |
| 77 | - | ||
| 78 | - | .empty-state { | |
| 79 | - | flex: 1; | |
| 80 | - | display: flex; | |
| 81 | - | flex-direction: column; | |
| 82 | - | align-items: center; | |
| 83 | - | justify-content: center; | |
| 84 | - | gap: 0.5rem; | |
| 85 | - | padding: 2rem; | |
| 86 | - | color: var(--muted-foreground); | |
| 87 | - | text-align: center; | |
| 88 | - | border: 1px dashed var(--border); | |
| 89 | - | background: color-mix(in srgb, var(--background) 95%, var(--card) 5%); | |
| 90 | - | } | |
| 91 | - | ||
| 92 | - | .empty-icon { | |
| 93 | - | font-size: 1.5rem; | |
| 94 | - | opacity: 0.5; | |
| 95 | - | } | |
| 96 | - | ||
| 97 | - | .empty-text { | |
| 98 | - | font-size: 0.75rem; | |
| 99 | - | font-weight: 600; | |
| 100 | - | letter-spacing: 0.12em; | |
| 101 | - | } | |
| 102 | - | ||
| 103 | - | .empty-hint { | |
| 104 | - | font-size: 0.68rem; | |
| 105 | - | letter-spacing: 0.06em; | |
| 106 | - | opacity: 0.7; | |
| 107 | - | } | |
| 108 | - | </style> | |
D webui/src/pages/homepage/favorites/FolderList.svelte +0 -312
| @@ -1,312 +0,0 @@ | |||
| 1 | - | <script lang="ts"> | |
| 2 | - | import type { Folder, Favorite } from '../types'; | |
| 3 | - | ||
| 4 | - | interface Props { | |
| 5 | - | folders: Folder[]; | |
| 6 | - | favorites: Favorite[]; | |
| 7 | - | selectedFolderId?: number | null; | |
| 8 | - | onSelectFolder?: (id: number | null) => void; | |
| 9 | - | onCreateFolder?: (name: string) => Promise<void>; | |
| 10 | - | } | |
| 11 | - | ||
| 12 | - | let { | |
| 13 | - | folders, | |
| 14 | - | favorites, | |
| 15 | - | selectedFolderId = null, | |
| 16 | - | onSelectFolder, | |
| 17 | - | onCreateFolder | |
| 18 | - | }: Props = $props(); | |
| 19 | - | ||
| 20 | - | // Inline creation state | |
| 21 | - | let isCreating = $state(false); | |
| 22 | - | let newFolderName = $state(''); | |
| 23 | - | let inputRef = $state<HTMLInputElement | null>(null); | |
| 24 | - | ||
| 25 | - | const startCreating = () => { | |
| 26 | - | isCreating = true; | |
| 27 | - | newFolderName = ''; | |
| 28 | - | // Focus input after it renders | |
| 29 | - | setTimeout(() => inputRef?.focus(), 0); | |
| 30 | - | }; | |
| 31 | - | ||
| 32 | - | const cancelCreating = () => { | |
| 33 | - | isCreating = false; | |
| 34 | - | newFolderName = ''; | |
| 35 | - | }; | |
| 36 | - | ||
| 37 | - | const submitCreate = async () => { | |
| 38 | - | const name = newFolderName.trim(); | |
| 39 | - | if (!name || !onCreateFolder) { | |
| 40 | - | cancelCreating(); | |
| 41 | - | return; | |
| 42 | - | } | |
| 43 | - | try { | |
| 44 | - | await onCreateFolder(name); | |
| 45 | - | cancelCreating(); | |
| 46 | - | } catch (e) { | |
| 47 | - | console.error('Failed to create folder:', e); | |
| 48 | - | } | |
| 49 | - | }; | |
| 50 | - | ||
| 51 | - | const handleKeydown = (e: KeyboardEvent) => { | |
| 52 | - | if (e.key === 'Enter') { | |
| 53 | - | e.preventDefault(); | |
| 54 | - | submitCreate(); | |
| 55 | - | } else if (e.key === 'Escape') { | |
| 56 | - | cancelCreating(); | |
| 57 | - | } | |
| 58 | - | }; | |
| 59 | - | ||
| 60 | - | // Count favorites per folder | |
| 61 | - | const getFolderCount = (folderId: number | null): number => { | |
| 62 | - | if (folderId === null) { | |
| 63 | - | return favorites.length; | |
| 64 | - | } | |
| 65 | - | return favorites.filter(f => f.folder_id === folderId).length; | |
| 66 | - | }; | |
| 67 | - | ||
| 68 | - | // Count unfiled favorites | |
| 69 | - | const unfiledCount = $derived(favorites.filter(f => f.folder_id === null).length); | |
| 70 | - | </script> | |
| 71 | - | ||
| 72 | - | <div class="folder-list"> | |
| 73 | - | <div class="folder-header"> | |
| 74 | - | <span class="header-label">FOLDERS</span> | |
| 75 | - | {#if isCreating} | |
| 76 | - | <button | |
| 77 | - | class="add-folder-btn cancel" | |
| 78 | - | type="button" | |
| 79 | - | onclick={cancelCreating} | |
| 80 | - | title="Cancel" | |
| 81 | - | > | |
| 82 | - | × | |
| 83 | - | </button> | |
| 84 | - | {:else} | |
| 85 | - | <button | |
| 86 | - | class="add-folder-btn" | |
| 87 | - | type="button" | |
| 88 | - | onclick={startCreating} | |
| 89 | - | title="Create folder" | |
| 90 | - | > | |
| 91 | - | + | |
| 92 | - | </button> | |
| 93 | - | {/if} | |
| 94 | - | </div> | |
| 95 | - | ||
| 96 | - | {#if isCreating} | |
| 97 | - | <div class="create-input-row"> | |
| 98 | - | <input | |
| 99 | - | bind:this={inputRef} | |
| 100 | - | bind:value={newFolderName} | |
| 101 | - | onkeydown={handleKeydown} | |
| 102 | - | onblur={cancelCreating} | |
| 103 | - | class="create-input" | |
| 104 | - | type="text" | |
| 105 | - | placeholder="Folder name..." | |
| 106 | - | maxlength="50" | |
| 107 | - | /> | |
| 108 | - | </div> | |
| 109 | - | {/if} | |
| 110 | - | ||
| 111 | - | <div class="folder-items"> | |
| 112 | - | <!-- All favorites --> | |
| 113 | - | <button | |
| 114 | - | class="folder-item" | |
| 115 | - | class:active={selectedFolderId === null} | |
| 116 | - | type="button" | |
| 117 | - | onclick={() => onSelectFolder?.(null)} | |
| 118 | - | > | |
| 119 | - | <span class="folder-icon"></span> | |
| 120 | - | <span class="folder-name">All</span> | |
| 121 | - | <span class="folder-count">{favorites.length}</span> | |
| 122 | - | </button> | |
| 123 | - | ||
| 124 | - | <!-- User folders --> | |
| 125 | - | {#each folders as folder (folder.id)} | |
| 126 | - | <button | |
| 127 | - | class="folder-item" | |
| 128 | - | class:active={selectedFolderId === folder.id} | |
| 129 | - | type="button" | |
| 130 | - | onclick={() => onSelectFolder?.(folder.id)} | |
| 131 | - | > | |
| 132 | - | <span class="folder-icon">{folder.icon || ''}</span> | |
| 133 | - | <span class="folder-name">{folder.name}</span> | |
| 134 | - | <span class="folder-count">{getFolderCount(folder.id)}</span> | |
| 135 | - | </button> | |
| 136 | - | {/each} | |
| 137 | - | ||
| 138 | - | <!-- Unfiled --> | |
| 139 | - | {#if unfiledCount > 0 && folders.length > 0} | |
| 140 | - | <button | |
| 141 | - | class="folder-item unfiled" | |
| 142 | - | class:active={selectedFolderId === -1} | |
| 143 | - | type="button" | |
| 144 | - | onclick={() => onSelectFolder?.(-1)} | |
| 145 | - | > | |
| 146 | - | <span class="folder-icon"></span> | |
| 147 | - | <span class="folder-name">Unfiled</span> | |
| 148 | - | <span class="folder-count">{unfiledCount}</span> | |
| 149 | - | </button> | |
| 150 | - | {/if} | |
| 151 | - | </div> | |
| 152 | - | </div> | |
| 153 | - | ||
| 154 | - | <style> | |
| 155 | - | .folder-list { | |
| 156 | - | display: flex; | |
| 157 | - | flex-direction: column; | |
| 158 | - | border-width: 1px; | |
| 159 | - | border-style: solid; | |
| 160 | - | border-color: var(--border); | |
| 161 | - | background: color-mix(in srgb, var(--background) 92%, var(--card) 8%); | |
| 162 | - | } | |
| 163 | - | ||
| 164 | - | .folder-header { | |
| 165 | - | display: flex; | |
| 166 | - | align-items: center; | |
| 167 | - | justify-content: space-between; | |
| 168 | - | padding: 0.5rem 0.75rem; | |
| 169 | - | border-bottom: 1px solid var(--border); | |
| 170 | - | background: color-mix(in srgb, var(--background) 80%, var(--card) 20%); | |
| 171 | - | } | |
| 172 | - | ||
| 173 | - | .header-label { | |
| 174 | - | font-size: 0.6rem; | |
| 175 | - | font-weight: 600; | |
| 176 | - | color: var(--muted-foreground); | |
| 177 | - | letter-spacing: 0.12em; | |
| 178 | - | } | |
| 179 | - | ||
| 180 | - | .add-folder-btn { | |
| 181 | - | width: 20px; | |
| 182 | - | height: 20px; | |
| 183 | - | display: flex; | |
| 184 | - | align-items: center; | |
| 185 | - | justify-content: center; | |
| 186 | - | font-size: 0.9rem; | |
| 187 | - | font-weight: 500; | |
| 188 | - | border-width: 1px; | |
| 189 | - | border-style: solid; | |
| 190 | - | border-color: var(--border); | |
| 191 | - | background: transparent; | |
| 192 | - | color: var(--muted-foreground); | |
| 193 | - | cursor: pointer; | |
| 194 | - | transition: all 100ms ease; | |
| 195 | - | } | |
| 196 | - | ||
| 197 | - | .add-folder-btn:hover { | |
| 198 | - | color: var(--foreground); | |
| 199 | - | border-color: var(--foreground); | |
| 200 | - | } | |
| 201 | - | ||
| 202 | - | .add-folder-btn.cancel { | |
| 203 | - | color: var(--muted-foreground); | |
| 204 | - | } | |
| 205 | - | ||
| 206 | - | .add-folder-btn.cancel:hover { | |
| 207 | - | color: var(--destructive); | |
| 208 | - | border-color: var(--destructive); | |
| 209 | - | } | |
| 210 | - | ||
| 211 | - | .create-input-row { | |
| 212 | - | padding: 0.4rem 0.5rem; | |
| 213 | - | border-bottom: 1px solid var(--border); | |
| 214 | - | background: color-mix(in srgb, var(--primary, #4ade80) 8%, var(--background) 92%); | |
| 215 | - | } | |
| 216 | - | ||
| 217 | - | .create-input { | |
| 218 | - | width: 100%; | |
| 219 | - | padding: 0.35rem 0.5rem; | |
| 220 | - | font-size: 0.7rem; | |
| 221 | - | font-family: inherit; | |
| 222 | - | color: var(--foreground); | |
| 223 | - | background: var(--background); | |
| 224 | - | border-width: 1px; | |
| 225 | - | border-style: solid; | |
| 226 | - | border-color: var(--primary, #4ade80); | |
| 227 | - | outline: none; | |
| 228 | - | } | |
| 229 | - | ||
| 230 | - | .create-input::placeholder { | |
| 231 | - | color: var(--muted-foreground); | |
| 232 | - | } | |
| 233 | - | ||
| 234 | - | .folder-items { | |
| 235 | - | display: flex; | |
| 236 | - | flex-direction: column; | |
| 237 | - | } | |
| 238 | - | ||
| 239 | - | .folder-item { | |
| 240 | - | display: flex; | |
| 241 | - | align-items: center; | |
| 242 | - | gap: 0.5rem; | |
| 243 | - | padding: 0.5rem 0.75rem; | |
| 244 | - | text-align: left; | |
| 245 | - | background: transparent; | |
| 246 | - | border: none; | |
| 247 | - | border-bottom: 1px solid color-mix(in srgb, var(--border) 40%, transparent); | |
| 248 | - | cursor: pointer; | |
| 249 | - | transition: background-color 100ms ease; | |
| 250 | - | } | |
| 251 | - | ||
| 252 | - | .folder-item:last-child { | |
| 253 | - | border-bottom: none; | |
| 254 | - | } | |
| 255 | - | ||
| 256 | - | .folder-item:hover { | |
| 257 | - | background: color-mix(in srgb, var(--card) 30%, transparent); | |
| 258 | - | } | |
| 259 | - | ||
| 260 | - | .folder-item.active { | |
| 261 | - | background: color-mix(in srgb, var(--card) 50%, transparent); | |
| 262 | - | } | |
| 263 | - | ||
| 264 | - | .folder-item.active::before { | |
| 265 | - | content: ''; | |
| 266 | - | position: absolute; | |
| 267 | - | left: 0; | |
| 268 | - | top: 0; | |
| 269 | - | bottom: 0; | |
| 270 | - | width: 2px; | |
| 271 | - | background: var(--primary, #4ade80); | |
| 272 | - | } | |
| 273 | - | ||
| 274 | - | .folder-item { | |
| 275 | - | position: relative; | |
| 276 | - | } | |
| 277 | - | ||
| 278 | - | .folder-icon { | |
| 279 | - | font-size: 0.8rem; | |
| 280 | - | width: 1.2rem; | |
| 281 | - | text-align: center; | |
| 282 | - | color: var(--muted-foreground); | |
| 283 | - | } | |
| 284 | - | ||
| 285 | - | .folder-item.active .folder-icon { | |
| 286 | - | color: var(--foreground); | |
| 287 | - | } | |
| 288 | - | ||
| 289 | - | .folder-name { | |
| 290 | - | flex: 1; | |
| 291 | - | font-size: 0.72rem; | |
| 292 | - | color: var(--foreground); | |
| 293 | - | white-space: nowrap; | |
| 294 | - | overflow: hidden; | |
| 295 | - | text-overflow: ellipsis; | |
| 296 | - | } | |
| 297 | - | ||
| 298 | - | .folder-item.unfiled .folder-name { | |
| 299 | - | font-style: italic; | |
| 300 | - | color: var(--muted-foreground); | |
| 301 | - | } | |
| 302 | - | ||
| 303 | - | .folder-count { | |
| 304 | - | font-size: 0.6rem; | |
| 305 | - | padding: 0.1rem 0.35rem; | |
| 306 | - | border-width: 1px; | |
| 307 | - | border-style: solid; | |
| 308 | - | border-color: var(--border); | |
| 309 | - | background: var(--background); | |
| 310 | - | color: var(--muted-foreground); | |
| 311 | - | } | |
| 312 | - | </style> | |
D webui/src/pages/homepage/favorites/TagCloud.svelte +0 -292
| @@ -1,292 +0,0 @@ | |||
| 1 | - | <script lang="ts"> | |
| 2 | - | import type { Tag } from '../types'; | |
| 3 | - | ||
| 4 | - | interface Props { | |
| 5 | - | tags: Tag[]; | |
| 6 | - | selectedTagIds?: number[]; | |
| 7 | - | onToggleTag?: (id: number) => void; | |
| 8 | - | onCreateTag?: (name: string, color: string) => Promise<void>; | |
| 9 | - | onClearSelection?: () => void; | |
| 10 | - | } | |
| 11 | - | ||
| 12 | - | let { | |
| 13 | - | tags, | |
| 14 | - | selectedTagIds = [], | |
| 15 | - | onToggleTag, | |
| 16 | - | onCreateTag, | |
| 17 | - | onClearSelection | |
| 18 | - | }: Props = $props(); | |
| 19 | - | ||
| 20 | - | // Inline creation state | |
| 21 | - | let isCreating = $state(false); | |
| 22 | - | let newTagName = $state(''); | |
| 23 | - | let inputRef = $state<HTMLInputElement | null>(null); | |
| 24 | - | ||
| 25 | - | // Predefined tag colors | |
| 26 | - | const tagColors = [ | |
| 27 | - | '#6b7280', '#ef4444', '#f59e0b', '#22c55e', | |
| 28 | - | '#3b82f6', '#8b5cf6', '#ec4899', '#14b8a6' | |
| 29 | - | ]; | |
| 30 | - | ||
| 31 | - | const getRandomColor = () => tagColors[Math.floor(Math.random() * tagColors.length)] ?? '#6b7280'; | |
| 32 | - | ||
| 33 | - | const startCreating = () => { | |
| 34 | - | isCreating = true; | |
| 35 | - | newTagName = ''; | |
| 36 | - | setTimeout(() => inputRef?.focus(), 0); | |
| 37 | - | }; | |
| 38 | - | ||
| 39 | - | const cancelCreating = () => { | |
| 40 | - | isCreating = false; | |
| 41 | - | newTagName = ''; | |
| 42 | - | }; | |
| 43 | - | ||
| 44 | - | const submitCreate = async () => { | |
| 45 | - | const name = newTagName.trim(); | |
| 46 | - | if (!name || !onCreateTag) { | |
| 47 | - | cancelCreating(); | |
| 48 | - | return; | |
| 49 | - | } | |
| 50 | - | try { | |
| 51 | - | await onCreateTag(name, getRandomColor()); | |
| 52 | - | cancelCreating(); | |
| 53 | - | } catch (e) { | |
| 54 | - | console.error('Failed to create tag:', e); | |
| 55 | - | } | |
| 56 | - | }; | |
| 57 | - | ||
| 58 | - | const handleKeydown = (e: KeyboardEvent) => { | |
| 59 | - | if (e.key === 'Enter') { | |
| 60 | - | e.preventDefault(); | |
| 61 | - | submitCreate(); | |
| 62 | - | } else if (e.key === 'Escape') { | |
| 63 | - | cancelCreating(); | |
| 64 | - | } | |
| 65 | - | }; | |
| 66 | - | ||
| 67 | - | const isSelected = (id: number) => selectedTagIds.includes(id); | |
| 68 | - | </script> | |
| 69 | - | ||
| 70 | - | <div class="tag-cloud"> | |
| 71 | - | <div class="tag-header"> | |
| 72 | - | <span class="header-label">TAGS</span> | |
| 73 | - | <div class="header-actions"> | |
| 74 | - | {#if selectedTagIds.length > 0} | |
| 75 | - | <button | |
| 76 | - | class="clear-btn" | |
| 77 | - | type="button" | |
| 78 | - | onclick={() => onClearSelection?.()} | |
| 79 | - | title="Clear selection" | |
| 80 | - | > | |
| 81 | - | CLEAR | |
| 82 | - | </button> | |
| 83 | - | {/if} | |
| 84 | - | {#if isCreating} | |
| 85 | - | <button | |
| 86 | - | class="add-tag-btn cancel" | |
| 87 | - | type="button" | |
| 88 | - | onclick={cancelCreating} | |
| 89 | - | title="Cancel" | |
| 90 | - | > | |
| 91 | - | × | |
| 92 | - | </button> | |
| 93 | - | {:else} | |
| 94 | - | <button | |
| 95 | - | class="add-tag-btn" | |
| 96 | - | type="button" | |
| 97 | - | onclick={startCreating} | |
| 98 | - | title="Create tag" | |
| 99 | - | > | |
| 100 | - | + | |
| 101 | - | </button> | |
| 102 | - | {/if} | |
| 103 | - | </div> | |
| 104 | - | </div> | |
| 105 | - | ||
| 106 | - | {#if isCreating} | |
| 107 | - | <div class="create-input-row"> | |
| 108 | - | <input | |
| 109 | - | bind:this={inputRef} | |
| 110 | - | bind:value={newTagName} | |
| 111 | - | onkeydown={handleKeydown} | |
| 112 | - | onblur={cancelCreating} | |
| 113 | - | class="create-input" | |
| 114 | - | type="text" | |
| 115 | - | placeholder="Tag name..." | |
| 116 | - | maxlength="30" | |
| 117 | - | /> | |
| 118 | - | </div> | |
| 119 | - | {/if} | |
| 120 | - | ||
| 121 | - | <div class="tag-chips"> | |
| 122 | - | {#if tags.length === 0} | |
| 123 | - | <span class="no-tags">No tags yet</span> | |
| 124 | - | {:else} | |
| 125 | - | {#each tags as tag (tag.id)} | |
| 126 | - | <button | |
| 127 | - | class="tag-chip" | |
| 128 | - | class:selected={isSelected(tag.id)} | |
| 129 | - | type="button" | |
| 130 | - | onclick={() => onToggleTag?.(tag.id)} | |
| 131 | - | style="--tag-color: {tag.color}" | |
| 132 | - | > | |
| 133 | - | <span class="tag-dot"></span> | |
| 134 | - | <span class="tag-name">{tag.name}</span> | |
| 135 | - | </button> | |
| 136 | - | {/each} | |
| 137 | - | {/if} | |
| 138 | - | </div> | |
| 139 | - | </div> | |
| 140 | - | ||
| 141 | - | <style> | |
| 142 | - | .tag-cloud { | |
| 143 | - | display: flex; | |
| 144 | - | flex-direction: column; | |
| 145 | - | gap: 0.5rem; | |
| 146 | - | padding: 0.6rem 0.75rem; | |
| 147 | - | border-width: 1px; | |
| 148 | - | border-style: solid; | |
| 149 | - | border-color: var(--border); | |
| 150 | - | background: color-mix(in srgb, var(--background) 92%, var(--card) 8%); | |
| 151 | - | } | |
| 152 | - | ||
| 153 | - | .tag-header { | |
| 154 | - | display: flex; | |
| 155 | - | align-items: center; | |
| 156 | - | justify-content: space-between; | |
| 157 | - | } | |
| 158 | - | ||
| 159 | - | .header-label { | |
| 160 | - | font-size: 0.6rem; | |
| 161 | - | font-weight: 600; | |
| 162 | - | color: var(--muted-foreground); | |
| 163 | - | letter-spacing: 0.12em; | |
| 164 | - | } | |
| 165 | - | ||
| 166 | - | .header-actions { | |
| 167 | - | display: flex; | |
| 168 | - | gap: 0.35rem; | |
| 169 | - | } | |
| 170 | - | ||
| 171 | - | .clear-btn { | |
| 172 | - | padding: 0.2rem 0.4rem; | |
| 173 | - | font-size: 0.55rem; | |
| 174 | - | font-weight: 500; | |
| 175 | - | letter-spacing: 0.08em; | |
| 176 | - | border-width: 1px; | |
| 177 | - | border-style: solid; | |
| 178 | - | border-color: var(--border); | |
| 179 | - | background: transparent; | |
| 180 | - | color: var(--muted-foreground); | |
| 181 | - | cursor: pointer; | |
| 182 | - | transition: all 100ms ease; | |
| 183 | - | } | |
| 184 | - | ||
| 185 | - | .clear-btn:hover { | |
| 186 | - | color: var(--foreground); | |
| 187 | - | border-color: var(--foreground); | |
| 188 | - | } | |
| 189 | - | ||
| 190 | - | .add-tag-btn { | |
| 191 | - | width: 20px; | |
| 192 | - | height: 20px; | |
| 193 | - | display: flex; | |
| 194 | - | align-items: center; | |
| 195 | - | justify-content: center; | |
| 196 | - | font-size: 0.9rem; | |
| 197 | - | font-weight: 500; | |
| 198 | - | border-width: 1px; | |
| 199 | - | border-style: solid; | |
| 200 | - | border-color: var(--border); | |
| 201 | - | background: transparent; | |
| 202 | - | color: var(--muted-foreground); | |
| 203 | - | cursor: pointer; | |
| 204 | - | transition: all 100ms ease; | |
| 205 | - | } | |
| 206 | - | ||
| 207 | - | .add-tag-btn:hover { | |
| 208 | - | color: var(--foreground); | |
| 209 | - | border-color: var(--foreground); | |
| 210 | - | } | |
| 211 | - | ||
| 212 | - | .add-tag-btn.cancel { | |
| 213 | - | color: var(--muted-foreground); | |
| 214 | - | } | |
| 215 | - | ||
| 216 | - | .add-tag-btn.cancel:hover { | |
| 217 | - | color: var(--destructive); | |
| 218 | - | border-color: var(--destructive); | |
| 219 | - | } | |
| 220 | - | ||
| 221 | - | .create-input-row { | |
| 222 | - | margin-bottom: 0.35rem; | |
| 223 | - | } | |
| 224 | - | ||
| 225 | - | .create-input { | |
| 226 | - | width: 100%; | |
| 227 | - | padding: 0.35rem 0.5rem; | |
| 228 | - | font-size: 0.68rem; | |
| 229 | - | font-family: inherit; | |
| 230 | - | color: var(--foreground); | |
| 231 | - | background: var(--background); | |
| 232 | - | border-width: 1px; | |
| 233 | - | border-style: solid; | |
| 234 | - | border-color: var(--primary, #4ade80); | |
| 235 | - | outline: none; | |
| 236 | - | } | |
| 237 | - | ||
| 238 | - | .create-input::placeholder { | |
| 239 | - | color: var(--muted-foreground); | |
| 240 | - | } | |
| 241 | - | ||
| 242 | - | .tag-chips { | |
| 243 | - | display: flex; | |
| 244 | - | flex-wrap: wrap; | |
| 245 | - | gap: 0.35rem; | |
| 246 | - | } | |
| 247 | - | ||
| 248 | - | .no-tags { | |
| 249 | - | font-size: 0.65rem; | |
| 250 | - | color: var(--muted-foreground); | |
| 251 | - | font-style: italic; | |
| 252 | - | padding: 0.25rem 0; | |
| 253 | - | } | |
| 254 | - | ||
| 255 | - | .tag-chip { | |
| 256 | - | display: inline-flex; | |
| 257 | - | align-items: center; | |
| 258 | - | gap: 0.35rem; | |
| 259 | - | padding: 0.25rem 0.5rem; | |
| 260 | - | font-size: 0.62rem; | |
| 261 | - | font-weight: 500; | |
| 262 | - | letter-spacing: 0.05em; | |
| 263 | - | border-width: 1px; | |
| 264 | - | border-style: solid; | |
| 265 | - | border-color: var(--tag-color); | |
| 266 | - | background: transparent; | |
| 267 | - | color: var(--foreground); | |
| 268 | - | cursor: pointer; | |
| 269 | - | transition: all 100ms ease; | |
| 270 | - | } | |
| 271 | - | ||
| 272 | - | .tag-chip:hover { | |
| 273 | - | background: color-mix(in srgb, var(--tag-color) 15%, transparent); | |
| 274 | - | } | |
| 275 | - | ||
| 276 | - | .tag-chip.selected { | |
| 277 | - | background: color-mix(in srgb, var(--tag-color) 25%, transparent); | |
| 278 | - | border-width: 2px; | |
| 279 | - | padding: calc(0.25rem - 1px) calc(0.5rem - 1px); | |
| 280 | - | } | |
| 281 | - | ||
| 282 | - | .tag-dot { | |
| 283 | - | width: 6px; | |
| 284 | - | height: 6px; | |
| 285 | - | background: var(--tag-color); | |
| 286 | - | border-radius: 50%; | |
| 287 | - | } | |
| 288 | - | ||
| 289 | - | .tag-name { | |
| 290 | - | text-transform: lowercase; | |
| 291 | - | } | |
| 292 | - | </style> | |
D webui/src/pages/homepage/favorites/index.ts +0 -6
| @@ -1,6 +0,0 @@ | |||
| 1 | - | // Favorites Components - Folders, Tags, Grid, Cards, Editor | |
| 2 | - | export { default as FavoriteCard } from './FavoriteCard.svelte'; | |
| 3 | - | export { default as FavoriteGrid } from './FavoriteGrid.svelte'; | |
| 4 | - | export { default as FolderList } from './FolderList.svelte'; | |
| 5 | - | export { default as TagCloud } from './TagCloud.svelte'; | |
| 6 | - | export { default as FavoriteEditor } from './FavoriteEditor.svelte'; | |
D webui/src/pages/homepage/history/HistoryCleanup.svelte +0 -396
| @@ -1,396 +0,0 @@ | |||
| 1 | - | <script lang="ts"> | |
| 2 | - | import type { HistoryCleanupRange } from '../types'; | |
| 3 | - | ||
| 4 | - | interface Props { | |
| 5 | - | onCleanup: (range: HistoryCleanupRange) => void; | |
| 6 | - | onClose: () => void; | |
| 7 | - | } | |
| 8 | - | ||
| 9 | - | let { onCleanup, onClose }: Props = $props(); | |
| 10 | - | ||
| 11 | - | type CleanupOption = { | |
| 12 | - | range: HistoryCleanupRange; | |
| 13 | - | label: string; | |
| 14 | - | shortcut: string; | |
| 15 | - | description: string; | |
| 16 | - | destructive?: boolean; | |
| 17 | - | }; | |
| 18 | - | ||
| 19 | - | const options: CleanupOption[] = [ | |
| 20 | - | { range: 'hour', label: 'LAST HOUR', shortcut: 'D h', description: 'Delete entries from the past 60 minutes' }, | |
| 21 | - | { range: 'day', label: 'LAST DAY', shortcut: 'D d', description: 'Delete entries from the past 24 hours' }, | |
| 22 | - | { range: 'week', label: 'LAST WEEK', shortcut: 'D w', description: 'Delete entries from the past 7 days' }, | |
| 23 | - | { range: 'month', label: 'LAST MONTH', shortcut: 'D m', description: 'Delete entries from the past 30 days' }, | |
| 24 | - | { range: 'all', label: 'CLEAR ALL', shortcut: 'D D', description: 'Permanently delete all browsing history', destructive: true }, | |
| 25 | - | ]; | |
| 26 | - | ||
| 27 | - | let selectedIndex = $state(0); | |
| 28 | - | let confirming = $state(false); | |
| 29 | - | ||
| 30 | - | const handleKeyDown = (e: KeyboardEvent) => { | |
| 31 | - | switch (e.key) { | |
| 32 | - | case 'Escape': | |
| 33 | - | e.preventDefault(); | |
| 34 | - | if (confirming) { | |
| 35 | - | confirming = false; | |
| 36 | - | } else { | |
| 37 | - | onClose(); | |
| 38 | - | } | |
| 39 | - | break; | |
| 40 | - | case 'ArrowDown': | |
| 41 | - | case 'j': | |
| 42 | - | e.preventDefault(); | |
| 43 | - | selectedIndex = Math.min(selectedIndex + 1, options.length - 1); | |
| 44 | - | break; | |
| 45 | - | case 'ArrowUp': | |
| 46 | - | case 'k': | |
| 47 | - | e.preventDefault(); | |
| 48 | - | selectedIndex = Math.max(selectedIndex - 1, 0); | |
| 49 | - | break; | |
| 50 | - | case 'Enter': | |
| 51 | - | e.preventDefault(); | |
| 52 | - | handleSelect(options[selectedIndex]!); | |
| 53 | - | break; | |
| 54 | - | } | |
| 55 | - | }; | |
| 56 | - | ||
| 57 | - | const handleSelect = (option: CleanupOption) => { | |
| 58 | - | if (option.destructive && !confirming) { | |
| 59 | - | confirming = true; | |
| 60 | - | return; | |
| 61 | - | } | |
| 62 | - | onCleanup(option.range); | |
| 63 | - | onClose(); | |
| 64 | - | }; | |
| 65 | - | ||
| 66 | - | const handleOverlayClick = (e: Event) => { | |
| 67 | - | if (e.target === e.currentTarget) { | |
| 68 | - | onClose(); | |
| 69 | - | } | |
| 70 | - | }; | |
| 71 | - | </script> | |
| 72 | - | ||
| 73 | - | <svelte:window onkeydown={handleKeyDown} /> | |
| 74 | - | ||
| 75 | - | <div | |
| 76 | - | class="cleanup-overlay" | |
| 77 | - | onclick={handleOverlayClick} | |
| 78 | - | onkeydown={(e) => { if (e.key === 'Escape') onClose(); }} | |
| 79 | - | role="button" | |
| 80 | - | tabindex="0" | |
| 81 | - | aria-label="Close cleanup modal" | |
| 82 | - | > | |
| 83 | - | <div class="cleanup-modal" role="presentation"> | |
| 84 | - | <div class="modal-header"> | |
| 85 | - | <span class="modal-icon"></span> | |
| 86 | - | <span class="modal-title">CLEAR HISTORY</span> | |
| 87 | - | <button class="close-btn" type="button" onclick={onClose}> | |
| 88 | - | <kbd>Esc</kbd> | |
| 89 | - | </button> | |
| 90 | - | </div> | |
| 91 | - | ||
| 92 | - | {#if confirming} | |
| 93 | - | <div class="confirm-panel"> | |
| 94 | - | <div class="confirm-icon"></div> | |
| 95 | - | <p class="confirm-message"> | |
| 96 | - | This will permanently delete <strong>ALL</strong> browsing history. | |
| 97 | - | This action cannot be undone. | |
| 98 | - | </p> | |
| 99 | - | <div class="confirm-actions"> | |
| 100 | - | <button | |
| 101 | - | class="confirm-btn cancel" | |
| 102 | - | type="button" | |
| 103 | - | onclick={() => confirming = false} | |
| 104 | - | > | |
| 105 | - | CANCEL | |
| 106 | - | <kbd>Esc</kbd> | |
| 107 | - | </button> | |
| 108 | - | <button | |
| 109 | - | class="confirm-btn destructive" | |
| 110 | - | type="button" | |
| 111 | - | onclick={() => { onCleanup('all'); onClose(); }} | |
| 112 | - | > | |
| 113 | - | DELETE ALL | |
| 114 | - | <kbd>Enter</kbd> | |
| 115 | - | </button> | |
| 116 | - | </div> | |
| 117 | - | </div> | |
| 118 | - | {:else} | |
| 119 | - | <div class="cleanup-options"> | |
| 120 | - | {#each options as option, i (option.range)} | |
| 121 | - | <button | |
| 122 | - | class="cleanup-option" | |
| 123 | - | class:selected={selectedIndex === i} | |
| 124 | - | class:destructive={option.destructive} | |
| 125 | - | type="button" | |
| 126 | - | onclick={() => handleSelect(option)} | |
| 127 | - | onmouseenter={() => selectedIndex = i} | |
| 128 | - | > | |
| 129 | - | <div class="option-main"> | |
| 130 | - | <span class="option-label">{option.label}</span> | |
| 131 | - | <span class="option-desc">{option.description}</span> | |
| 132 | - | </div> | |
| 133 | - | <kbd class="option-shortcut">{option.shortcut}</kbd> | |
| 134 | - | </button> | |
| 135 | - | {/each} | |
| 136 | - | </div> | |
| 137 | - | {/if} | |
| 138 | - | ||
| 139 | - | <div class="modal-footer"> | |
| 140 | - | <span class="hint"><kbd>j</kbd><kbd>k</kbd> navigate</span> | |
| 141 | - | <span class="hint"><kbd>Enter</kbd> select</span> | |
| 142 | - | </div> | |
| 143 | - | </div> | |
| 144 | - | </div> | |
| 145 | - | ||
| 146 | - | <style> | |
| 147 | - | .cleanup-overlay { | |
| 148 | - | position: fixed; | |
| 149 | - | inset: 0; | |
| 150 | - | z-index: 100; | |
| 151 | - | display: flex; | |
| 152 | - | align-items: center; | |
| 153 | - | justify-content: center; | |
| 154 | - | background: rgb(0 0 0 / 0.75); | |
| 155 | - | backdrop-filter: blur(4px); | |
| 156 | - | animation: fade-in 150ms ease; | |
| 157 | - | } | |
| 158 | - | ||
| 159 | - | @keyframes fade-in { | |
| 160 | - | from { opacity: 0; } | |
| 161 | - | } | |
| 162 | - | ||
| 163 | - | .cleanup-modal { | |
| 164 | - | width: 100%; | |
| 165 | - | max-width: 420px; | |
| 166 | - | margin: 1rem; | |
| 167 | - | background: var(--card); | |
| 168 | - | border-width: 1px; | |
| 169 | - | border-style: solid; | |
| 170 | - | border-color: var(--border); | |
| 171 | - | box-shadow: 0 24px 48px -12px rgb(0 0 0 / 0.6); | |
| 172 | - | animation: modal-in 150ms ease; | |
| 173 | - | } | |
| 174 | - | ||
| 175 | - | @keyframes modal-in { | |
| 176 | - | from { | |
| 177 | - | opacity: 0; | |
| 178 | - | transform: scale(0.96) translateY(-8px); | |
| 179 | - | } | |
| 180 | - | } | |
| 181 | - | ||
| 182 | - | .modal-header { | |
| 183 | - | display: flex; | |
| 184 | - | align-items: center; | |
| 185 | - | gap: 0.6rem; | |
| 186 | - | padding: 0.75rem 1rem; | |
| 187 | - | border-bottom: 1px solid var(--border); | |
| 188 | - | background: color-mix(in srgb, var(--background) 80%, transparent); | |
| 189 | - | } | |
| 190 | - | ||
| 191 | - | .modal-icon::before { | |
| 192 | - | content: ''; | |
| 193 | - | font-size: 0.9rem; | |
| 194 | - | color: var(--warning); | |
| 195 | - | } | |
| 196 | - | ||
| 197 | - | .modal-title { | |
| 198 | - | flex: 1; | |
| 199 | - | font-size: 0.72rem; | |
| 200 | - | font-weight: 600; | |
| 201 | - | letter-spacing: 0.1em; | |
| 202 | - | color: var(--foreground); | |
| 203 | - | } | |
| 204 | - | ||
| 205 | - | .close-btn { | |
| 206 | - | padding: 0.25rem 0.5rem; | |
| 207 | - | background: transparent; | |
| 208 | - | border-width: 1px; | |
| 209 | - | border-style: solid; | |
| 210 | - | border-color: var(--border); | |
| 211 | - | cursor: pointer; | |
| 212 | - | } | |
| 213 | - | ||
| 214 | - | .close-btn kbd { | |
| 215 | - | font-size: 0.6rem; | |
| 216 | - | color: var(--muted-foreground); | |
| 217 | - | } | |
| 218 | - | ||
| 219 | - | .close-btn:hover { | |
| 220 | - | border-color: var(--foreground); | |
| 221 | - | } | |
| 222 | - | ||
| 223 | - | .close-btn:hover kbd { | |
| 224 | - | color: var(--foreground); | |
| 225 | - | } | |
| 226 | - | ||
| 227 | - | .cleanup-options { | |
| 228 | - | display: flex; | |
| 229 | - | flex-direction: column; | |
| 230 | - | } | |
| 231 | - | ||
| 232 | - | .cleanup-option { | |
| 233 | - | display: flex; | |
| 234 | - | align-items: center; | |
| 235 | - | justify-content: space-between; | |
| 236 | - | gap: 1rem; | |
| 237 | - | padding: 0.85rem 1rem; | |
| 238 | - | text-align: left; | |
| 239 | - | background: transparent; | |
| 240 | - | border: none; | |
| 241 | - | border-bottom: 1px solid color-mix(in srgb, var(--border) 50%, transparent); | |
| 242 | - | cursor: pointer; | |
| 243 | - | transition: background-color 100ms ease; | |
| 244 | - | } | |
| 245 | - | ||
| 246 | - | .cleanup-option:last-child { | |
| 247 | - | border-bottom: none; | |
| 248 | - | } | |
| 249 | - | ||
| 250 | - | .cleanup-option:hover, | |
| 251 | - | .cleanup-option.selected { | |
| 252 | - | background: color-mix(in srgb, var(--card) 80%, var(--background) 20%); | |
| 253 | - | } | |
| 254 | - | ||
| 255 | - | .cleanup-option.selected { | |
| 256 | - | outline: 1px solid var(--primary, #4ade80); | |
| 257 | - | outline-offset: -1px; | |
| 258 | - | } | |
| 259 | - | ||
| 260 | - | .cleanup-option.destructive .option-label { | |
| 261 | - | color: var(--destructive); | |
| 262 | - | } | |
| 263 | - | ||
| 264 | - | .option-main { | |
| 265 | - | display: flex; | |
| 266 | - | flex-direction: column; | |
| 267 | - | gap: 0.2rem; | |
| 268 | - | } | |
| 269 | - | ||
| 270 | - | .option-label { | |
| 271 | - | font-size: 0.75rem; | |
| 272 | - | font-weight: 600; | |
| 273 | - | letter-spacing: 0.08em; | |
| 274 | - | color: var(--foreground); | |
| 275 | - | } | |
| 276 | - | ||
| 277 | - | .option-desc { | |
| 278 | - | font-size: 0.68rem; | |
| 279 | - | color: var(--muted-foreground); | |
| 280 | - | letter-spacing: 0.04em; | |
| 281 | - | } | |
| 282 | - | ||
| 283 | - | .option-shortcut { | |
| 284 | - | padding: 0.2rem 0.45rem; | |
| 285 | - | font-size: 0.58rem; | |
| 286 | - | letter-spacing: 0.05em; | |
| 287 | - | border-width: 1px; | |
| 288 | - | border-style: solid; | |
| 289 | - | border-color: var(--border); | |
| 290 | - | background: var(--background); | |
| 291 | - | color: var(--muted-foreground); | |
| 292 | - | } | |
| 293 | - | ||
| 294 | - | .confirm-panel { | |
| 295 | - | padding: 1.5rem 1rem; | |
| 296 | - | display: flex; | |
| 297 | - | flex-direction: column; | |
| 298 | - | align-items: center; | |
| 299 | - | gap: 1rem; | |
| 300 | - | text-align: center; | |
| 301 | - | } | |
| 302 | - | ||
| 303 | - | .confirm-icon::before { | |
| 304 | - | content: ''; | |
| 305 | - | font-size: 2rem; | |
| 306 | - | color: var(--destructive); | |
| 307 | - | } | |
| 308 | - | ||
| 309 | - | .confirm-message { | |
| 310 | - | margin: 0; | |
| 311 | - | font-size: 0.8rem; | |
| 312 | - | line-height: 1.5; | |
| 313 | - | color: var(--foreground); | |
| 314 | - | } | |
| 315 | - | ||
| 316 | - | .confirm-message strong { | |
| 317 | - | color: var(--destructive); | |
| 318 | - | } | |
| 319 | - | ||
| 320 | - | .confirm-actions { | |
| 321 | - | display: flex; | |
| 322 | - | gap: 0.5rem; | |
| 323 | - | width: 100%; | |
| 324 | - | margin-top: 0.5rem; | |
| 325 | - | } | |
| 326 | - | ||
| 327 | - | .confirm-btn { | |
| 328 | - | flex: 1; | |
| 329 | - | display: flex; | |
| 330 | - | align-items: center; | |
| 331 | - | justify-content: center; | |
| 332 | - | gap: 0.5rem; | |
| 333 | - | padding: 0.6rem 1rem; | |
| 334 | - | font-size: 0.7rem; | |
| 335 | - | font-weight: 500; | |
| 336 | - | letter-spacing: 0.08em; | |
| 337 | - | border-width: 1px; | |
| 338 | - | border-style: solid; | |
| 339 | - | border-color: var(--border); | |
| 340 | - | cursor: pointer; | |
| 341 | - | transition: all 100ms ease; | |
| 342 | - | } | |
| 343 | - | ||
| 344 | - | .confirm-btn kbd { | |
| 345 | - | font-size: 0.55rem; | |
| 346 | - | padding: 0.1rem 0.3rem; | |
| 347 | - | border: 1px solid; | |
| 348 | - | opacity: 0.7; | |
| 349 | - | } | |
| 350 | - | ||
| 351 | - | .confirm-btn.cancel { | |
| 352 | - | background: transparent; | |
| 353 | - | color: var(--muted-foreground); | |
| 354 | - | } | |
| 355 | - | ||
| 356 | - | .confirm-btn.cancel:hover { | |
| 357 | - | color: var(--foreground); | |
| 358 | - | border-color: var(--foreground); | |
| 359 | - | } | |
| 360 | - | ||
| 361 | - | .confirm-btn.destructive { | |
| 362 | - | background: var(--destructive); | |
| 363 | - | color: var(--destructive-foreground); | |
| 364 | - | border-color: color-mix(in srgb, var(--destructive) 80%, black 20%); | |
| 365 | - | } | |
| 366 | - | ||
| 367 | - | .confirm-btn.destructive:hover { | |
| 368 | - | background: color-mix(in srgb, var(--destructive) 85%, white 15%); | |
| 369 | - | } | |
| 370 | - | ||
| 371 | - | .modal-footer { | |
| 372 | - | display: flex; | |
| 373 | - | align-items: center; | |
| 374 | - | justify-content: center; | |
| 375 | - | gap: 1rem; | |
| 376 | - | padding: 0.6rem 1rem; | |
| 377 | - | border-top: 1px solid var(--border); | |
| 378 | - | background: color-mix(in srgb, var(--background) 60%, transparent); | |
| 379 | - | } | |
| 380 | - | ||
| 381 | - | .hint { | |
| 382 | - | font-size: 0.6rem; | |
| 383 | - | color: var(--muted-foreground); | |
| 384 | - | letter-spacing: 0.05em; | |
| 385 | - | } | |
| 386 | - | ||
| 387 | - | .hint kbd { | |
| 388 | - | font-size: 0.55rem; | |
| 389 | - | padding: 0.1rem 0.25rem; | |
| 390 | - | margin-right: 0.25rem; | |
| 391 | - | border-width: 1px; | |
| 392 | - | border-style: solid; | |
| 393 | - | border-color: var(--border); | |
| 394 | - | background: var(--background); | |
| 395 | - | } | |
| 396 | - | </style> | |
D webui/src/pages/homepage/history/HistoryFilters.svelte +0 -155
| @@ -1,155 +0,0 @@ | |||
| 1 | - | <script lang="ts"> | |
| 2 | - | import type { DomainStat } from '../types'; | |
| 3 | - | ||
| 4 | - | interface Props { | |
| 5 | - | domains?: DomainStat[]; | |
| 6 | - | selectedDomain?: string | null; | |
| 7 | - | onSelectDomain?: (domain: string | null) => void; | |
| 8 | - | onOpenCleanup?: () => void; | |
| 9 | - | } | |
| 10 | - | ||
| 11 | - | let { | |
| 12 | - | domains = [], | |
| 13 | - | selectedDomain = null, | |
| 14 | - | onSelectDomain, | |
| 15 | - | onOpenCleanup | |
| 16 | - | }: Props = $props(); | |
| 17 | - | ||
| 18 | - | const topDomains = $derived(domains.slice(0, 5)); | |
| 19 | - | </script> | |
| 20 | - | ||
| 21 | - | <div class="history-filters"> | |
| 22 | - | <div class="filter-section"> | |
| 23 | - | <span class="filter-label">FILTER</span> | |
| 24 | - | <div class="filter-chips"> | |
| 25 | - | <button | |
| 26 | - | class="filter-chip" | |
| 27 | - | class:active={selectedDomain === null} | |
| 28 | - | type="button" | |
| 29 | - | onclick={() => onSelectDomain?.(null)} | |
| 30 | - | > | |
| 31 | - | ALL | |
| 32 | - | </button> | |
| 33 | - | {#each topDomains as domain (domain.domain)} | |
| 34 | - | <button | |
| 35 | - | class="filter-chip" | |
| 36 | - | class:active={selectedDomain === domain.domain} | |
| 37 | - | type="button" | |
| 38 | - | onclick={() => onSelectDomain?.(domain.domain)} | |
| 39 | - | title="{domain.page_count} pages, {domain.total_visits} visits" | |
| 40 | - | > | |
| 41 | - | {domain.domain.replace('www.', '').slice(0, 12)} | |
| 42 | - | </button> | |
| 43 | - | {/each} | |
| 44 | - | </div> | |
| 45 | - | </div> | |
| 46 | - | ||
| 47 | - | <div class="filter-actions"> | |
| 48 | - | <button | |
| 49 | - | class="cleanup-btn" | |
| 50 | - | type="button" | |
| 51 | - | onclick={() => onOpenCleanup?.()} | |
| 52 | - | title="Clear history (D)" | |
| 53 | - | > | |
| 54 | - | <span class="cleanup-icon"></span> | |
| 55 | - | <span>CLEAR</span> | |
| 56 | - | </button> | |
| 57 | - | </div> | |
| 58 | - | </div> | |
| 59 | - | ||
| 60 | - | <style> | |
| 61 | - | .history-filters { | |
| 62 | - | display: flex; | |
| 63 | - | align-items: center; | |
| 64 | - | justify-content: space-between; | |
| 65 | - | gap: 1rem; | |
| 66 | - | padding: 0.5rem 1rem; | |
| 67 | - | } | |
| 68 | - | ||
| 69 | - | .filter-section { | |
| 70 | - | display: flex; | |
| 71 | - | align-items: center; | |
| 72 | - | gap: 0.6rem; | |
| 73 | - | flex: 1; | |
| 74 | - | min-width: 0; | |
| 75 | - | } | |
| 76 | - | ||
| 77 | - | .filter-label { | |
| 78 | - | font-size: 0.6rem; | |
| 79 | - | font-weight: 600; | |
| 80 | - | color: var(--muted-foreground); | |
| 81 | - | letter-spacing: 0.12em; | |
| 82 | - | flex-shrink: 0; | |
| 83 | - | } | |
| 84 | - | ||
| 85 | - | .filter-chips { | |
| 86 | - | display: flex; | |
| 87 | - | gap: 0.35rem; | |
| 88 | - | overflow-x: auto; | |
| 89 | - | scrollbar-width: none; | |
| 90 | - | } | |
| 91 | - | ||
| 92 | - | .filter-chips::-webkit-scrollbar { | |
| 93 | - | display: none; | |
| 94 | - | } | |
| 95 | - | ||
| 96 | - | .filter-chip { | |
| 97 | - | padding: 0.3rem 0.55rem; | |
| 98 | - | font-size: 0.6rem; | |
| 99 | - | font-weight: 500; | |
| 100 | - | letter-spacing: 0.08em; | |
| 101 | - | color: var(--muted-foreground); | |
| 102 | - | background: transparent; | |
| 103 | - | border-width: 1px; | |
| 104 | - | border-style: solid; | |
| 105 | - | border-color: var(--border); | |
| 106 | - | cursor: pointer; | |
| 107 | - | transition: all 100ms ease; | |
| 108 | - | white-space: nowrap; | |
| 109 | - | flex-shrink: 0; | |
| 110 | - | } | |
| 111 | - | ||
| 112 | - | .filter-chip:hover { | |
| 113 | - | color: var(--foreground); | |
| 114 | - | border-color: var(--foreground); | |
| 115 | - | } | |
| 116 | - | ||
| 117 | - | .filter-chip.active { | |
| 118 | - | color: var(--primary-foreground, var(--background)); | |
| 119 | - | background: var(--primary); | |
| 120 | - | border-color: var(--primary); | |
| 121 | - | } | |
| 122 | - | ||
| 123 | - | .filter-actions { | |
| 124 | - | display: flex; | |
| 125 | - | gap: 0.35rem; | |
| 126 | - | flex-shrink: 0; | |
| 127 | - | } | |
| 128 | - | ||
| 129 | - | .cleanup-btn { | |
| 130 | - | display: flex; | |
| 131 | - | align-items: center; | |
| 132 | - | gap: 0.4rem; | |
| 133 | - | padding: 0.35rem 0.6rem; | |
| 134 | - | font-size: 0.6rem; | |
| 135 | - | font-weight: 500; | |
| 136 | - | letter-spacing: 0.08em; | |
| 137 | - | color: var(--muted-foreground); | |
| 138 | - | background: transparent; | |
| 139 | - | border-width: 1px; | |
| 140 | - | border-style: solid; | |
| 141 | - | border-color: var(--border); | |
| 142 | - | cursor: pointer; | |
| 143 | - | transition: all 100ms ease; | |
| 144 | - | } | |
| 145 | - | ||
| 146 | - | .cleanup-btn:hover { | |
| 147 | - | color: var(--destructive); | |
| 148 | - | border-color: var(--destructive); | |
| 149 | - | } | |
| 150 | - | ||
| 151 | - | .cleanup-icon::before { | |
| 152 | - | content: ''; | |
| 153 | - | font-size: 0.75rem; | |
| 154 | - | } | |
| 155 | - | </style> | |
D webui/src/pages/homepage/history/HistoryItem.svelte +0 -280
| @@ -1,280 +0,0 @@ | |||
| 1 | - | <script lang="ts"> | |
| 2 | - | import type { HistoryEntry } from '../types'; | |
| 3 | - | ||
| 4 | - | interface Props { | |
| 5 | - | entry: HistoryEntry; | |
| 6 | - | focused?: boolean; | |
| 7 | - | onSelect?: (entry: HistoryEntry) => void; | |
| 8 | - | onDelete?: (entry: HistoryEntry) => void; | |
| 9 | - | } | |
| 10 | - | ||
| 11 | - | let { entry, focused = false, onSelect, onDelete }: Props = $props(); | |
| 12 | - | ||
| 13 | - | let deleting = $state(false); | |
| 14 | - | ||
| 15 | - | const getDomain = (url: string): string => { | |
| 16 | - | try { | |
| 17 | - | return new URL(url).hostname; | |
| 18 | - | } catch { | |
| 19 | - | return url; | |
| 20 | - | } | |
| 21 | - | }; | |
| 22 | - | ||
| 23 | - | const getFaviconUrl = (item: HistoryEntry): string => { | |
| 24 | - | // Use provided favicon_url if available | |
| 25 | - | if (item.favicon_url) return item.favicon_url; | |
| 26 | - | // Handle internal dumb:// URLs - use embedded favicon | |
| 27 | - | if (item.url.startsWith('dumb://')) { | |
| 28 | - | return 'favicon.png'; | |
| 29 | - | } | |
| 30 | - | // Construct DuckDuckGo favicon URL from domain | |
| 31 | - | const domain = getDomain(item.url).replace(/^www\./, ''); | |
| 32 | - | return `https://icons.duckduckgo.com/ip3/${encodeURIComponent(domain)}.ico`; | |
| 33 | - | }; | |
| 34 | - | ||
| 35 | - | const getDisplayTitle = (item: HistoryEntry): string => { | |
| 36 | - | const trimmed = item.title?.trim(); | |
| 37 | - | if (trimmed) return trimmed; | |
| 38 | - | return getDomain(item.url) || item.url; | |
| 39 | - | }; | |
| 40 | - | ||
| 41 | - | const formatTime = (timestamp: string): string => { | |
| 42 | - | try { | |
| 43 | - | const date = new Date(timestamp); | |
| 44 | - | const now = new Date(); | |
| 45 | - | const diff = now.getTime() - date.getTime(); | |
| 46 | - | ||
| 47 | - | const minutes = Math.floor(diff / (1000 * 60)); | |
| 48 | - | const hours = Math.floor(diff / (1000 * 60 * 60)); | |
| 49 | - | ||
| 50 | - | if (minutes < 1) return 'now'; | |
| 51 | - | if (minutes < 60) return `${minutes}m`; | |
| 52 | - | if (hours < 24) return `${hours}h`; | |
| 53 | - | ||
| 54 | - | return date.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' }); | |
| 55 | - | } catch { | |
| 56 | - | return ''; | |
| 57 | - | } | |
| 58 | - | }; | |
| 59 | - | ||
| 60 | - | const handleSelect = () => { | |
| 61 | - | onSelect?.(entry); | |
| 62 | - | }; | |
| 63 | - | ||
| 64 | - | const handleDelete = (e: Event) => { | |
| 65 | - | e.stopPropagation(); | |
| 66 | - | deleting = true; | |
| 67 | - | onDelete?.(entry); | |
| 68 | - | }; | |
| 69 | - | ||
| 70 | - | const handleKeyDown = (e: KeyboardEvent) => { | |
| 71 | - | if (e.key === 'Enter') { | |
| 72 | - | handleSelect(); | |
| 73 | - | } else if (e.key === 'd' || e.key === 'x') { | |
| 74 | - | handleDelete(e); | |
| 75 | - | } | |
| 76 | - | }; | |
| 77 | - | </script> | |
| 78 | - | ||
| 79 | - | <div | |
| 80 | - | class="history-item" | |
| 81 | - | class:focused | |
| 82 | - | class:deleting | |
| 83 | - | role="button" | |
| 84 | - | tabindex="0" | |
| 85 | - | data-entry-id={entry.id} | |
| 86 | - | onclick={handleSelect} | |
| 87 | - | onkeydown={handleKeyDown} | |
| 88 | - | > | |
| 89 | - | <div class="item-favicon"> | |
| 90 | - | <img | |
| 91 | - | src={getFaviconUrl(entry)} | |
| 92 | - | alt="" | |
| 93 | - | class="favicon-img" | |
| 94 | - | onerror={(e) => { | |
| 95 | - | const target = e.target as HTMLImageElement; | |
| 96 | - | target.style.display = 'none'; | |
| 97 | - | const fallback = target.nextElementSibling as HTMLElement; | |
| 98 | - | if (fallback) fallback.style.display = 'flex'; | |
| 99 | - | }} | |
| 100 | - | /> | |
| 101 | - | <div class="favicon-fallback" style="display: none;"></div> | |
| 102 | - | </div> | |
| 103 | - | ||
| 104 | - | <div class="item-content"> | |
| 105 | - | <div class="item-row"> | |
| 106 | - | <span class="item-title">{getDisplayTitle(entry)}</span> | |
| 107 | - | <span class="item-time">{formatTime(entry.last_visited)}</span> | |
| 108 | - | </div> | |
| 109 | - | <div class="item-url">{entry.url}</div> | |
| 110 | - | </div> | |
| 111 | - | ||
| 112 | - | <div class="item-actions"> | |
| 113 | - | {#if entry.visit_count > 1} | |
| 114 | - | <span class="visit-count">{entry.visit_count}x</span> | |
| 115 | - | {/if} | |
| 116 | - | <button | |
| 117 | - | class="action-btn delete-btn" | |
| 118 | - | type="button" | |
| 119 | - | onclick={handleDelete} | |
| 120 | - | title="Delete entry (d)" | |
| 121 | - | > | |
| 122 | - | <span aria-hidden="true">×</span> | |
| 123 | - | </button> | |
| 124 | - | </div> | |
| 125 | - | </div> | |
| 126 | - | ||
| 127 | - | <style> | |
| 128 | - | .history-item { | |
| 129 | - | display: grid; | |
| 130 | - | grid-template-columns: auto 1fr auto; | |
| 131 | - | align-items: center; | |
| 132 | - | gap: 0.75rem; | |
| 133 | - | padding: 0.55rem 0.85rem; | |
| 134 | - | border-bottom-width: 1px; | |
| 135 | - | border-bottom-style: solid; | |
| 136 | - | border-bottom-color: color-mix(in srgb, var(--border) 50%, transparent); | |
| 137 | - | transition: background-color 100ms ease; | |
| 138 | - | cursor: pointer; | |
| 139 | - | } | |
| 140 | - | ||
| 141 | - | .history-item:last-child { | |
| 142 | - | border-bottom: none; | |
| 143 | - | } | |
| 144 | - | ||
| 145 | - | .history-item:hover, | |
| 146 | - | .history-item.focused { | |
| 147 | - | background: color-mix(in srgb, var(--card) 40%, transparent); | |
| 148 | - | } | |
| 149 | - | ||
| 150 | - | .history-item.focused { | |
| 151 | - | outline: 1px solid var(--primary, #4ade80); | |
| 152 | - | outline-offset: -1px; | |
| 153 | - | } | |
| 154 | - | ||
| 155 | - | .history-item.deleting { | |
| 156 | - | opacity: 0.3; | |
| 157 | - | pointer-events: none; | |
| 158 | - | } | |
| 159 | - | ||
| 160 | - | .item-favicon { | |
| 161 | - | width: 28px; | |
| 162 | - | height: 28px; | |
| 163 | - | border-width: 1px; | |
| 164 | - | border-style: solid; | |
| 165 | - | border-color: var(--border); | |
| 166 | - | background: var(--background); | |
| 167 | - | display: flex; | |
| 168 | - | align-items: center; | |
| 169 | - | justify-content: center; | |
| 170 | - | flex-shrink: 0; | |
| 171 | - | } | |
| 172 | - | ||
| 173 | - | .favicon-img { | |
| 174 | - | width: 16px; | |
| 175 | - | height: 16px; | |
| 176 | - | object-fit: contain; | |
| 177 | - | } | |
| 178 | - | ||
| 179 | - | .favicon-fallback { | |
| 180 | - | width: 16px; | |
| 181 | - | height: 16px; | |
| 182 | - | display: flex; | |
| 183 | - | align-items: center; | |
| 184 | - | justify-content: center; | |
| 185 | - | color: var(--muted-foreground); | |
| 186 | - | } | |
| 187 | - | ||
| 188 | - | .favicon-fallback::before { | |
| 189 | - | content: ''; | |
| 190 | - | font-size: 0.75rem; | |
| 191 | - | } | |
| 192 | - | ||
| 193 | - | .item-content { | |
| 194 | - | display: flex; | |
| 195 | - | flex-direction: column; | |
| 196 | - | gap: 0.15rem; | |
| 197 | - | min-width: 0; | |
| 198 | - | } | |
| 199 | - | ||
| 200 | - | .item-row { | |
| 201 | - | display: flex; | |
| 202 | - | align-items: center; | |
| 203 | - | justify-content: space-between; | |
| 204 | - | gap: 0.75rem; | |
| 205 | - | } | |
| 206 | - | ||
| 207 | - | .item-title { | |
| 208 | - | font-size: 0.8rem; | |
| 209 | - | font-weight: 500; | |
| 210 | - | color: var(--foreground); | |
| 211 | - | white-space: nowrap; | |
| 212 | - | overflow: hidden; | |
| 213 | - | text-overflow: ellipsis; | |
| 214 | - | } | |
| 215 | - | ||
| 216 | - | .item-time { | |
| 217 | - | font-size: 0.65rem; | |
| 218 | - | color: var(--muted-foreground); | |
| 219 | - | text-transform: uppercase; | |
| 220 | - | letter-spacing: 0.08em; | |
| 221 | - | white-space: nowrap; | |
| 222 | - | flex-shrink: 0; | |
| 223 | - | } | |
| 224 | - | ||
| 225 | - | .item-url { | |
| 226 | - | font-size: 0.68rem; | |
| 227 | - | color: var(--muted-foreground); | |
| 228 | - | white-space: nowrap; | |
| 229 | - | overflow: hidden; | |
| 230 | - | text-overflow: ellipsis; | |
| 231 | - | } | |
| 232 | - | ||
| 233 | - | .item-actions { | |
| 234 | - | display: flex; | |
| 235 | - | align-items: center; | |
| 236 | - | gap: 0.5rem; | |
| 237 | - | } | |
| 238 | - | ||
| 239 | - | .visit-count { | |
| 240 | - | font-size: 0.6rem; | |
| 241 | - | color: var(--muted-foreground); | |
| 242 | - | padding: 0.15rem 0.35rem; | |
| 243 | - | border-width: 1px; | |
| 244 | - | border-style: solid; | |
| 245 | - | border-color: var(--border); | |
| 246 | - | background: var(--background); | |
| 247 | - | } | |
| 248 | - | ||
| 249 | - | .action-btn { | |
| 250 | - | width: 24px; | |
| 251 | - | height: 24px; | |
| 252 | - | display: flex; | |
| 253 | - | align-items: center; | |
| 254 | - | justify-content: center; | |
| 255 | - | border-width: 1px; | |
| 256 | - | border-style: solid; | |
| 257 | - | border-color: var(--border); | |
| 258 | - | background: transparent; | |
| 259 | - | color: var(--muted-foreground); | |
| 260 | - | cursor: pointer; | |
| 261 | - | transition: all 100ms ease; | |
| 262 | - | opacity: 0; | |
| 263 | - | } | |
| 264 | - | ||
| 265 | - | .history-item:hover .action-btn, | |
| 266 | - | .history-item.focused .action-btn { | |
| 267 | - | opacity: 1; | |
| 268 | - | } | |
| 269 | - | ||
| 270 | - | .action-btn:hover { | |
| 271 | - | color: var(--foreground); | |
| 272 | - | border-color: var(--foreground); | |
| 273 | - | background: color-mix(in srgb, var(--card) 50%, transparent); | |
| 274 | - | } | |
| 275 | - | ||
| 276 | - | .delete-btn:hover { | |
| 277 | - | color: var(--destructive); | |
| 278 | - | border-color: var(--destructive); | |
| 279 | - | } | |
| 280 | - | </style> | |
D webui/src/pages/homepage/history/HistorySearch.svelte +0 -179
| @@ -1,179 +0,0 @@ | |||
| 1 | - | <script lang="ts"> | |
| 2 | - | import { onMount } from 'svelte'; | |
| 3 | - | import { homepageState } from '../state.svelte'; | |
| 4 | - | import { handleSearchInput } from '../keyboard'; | |
| 5 | - | ||
| 6 | - | interface Props { | |
| 7 | - | placeholder?: string; | |
| 8 | - | autofocus?: boolean; | |
| 9 | - | } | |
| 10 | - | ||
| 11 | - | let { placeholder = 'Search history...', autofocus = false }: Props = $props(); | |
| 12 | - | ||
| 13 | - | let inputRef = $state<HTMLInputElement | null>(null); | |
| 14 | - | ||
| 15 | - | // Auto-focus on mount if requested | |
| 16 | - | onMount(() => { | |
| 17 | - | if (autofocus && inputRef) { | |
| 18 | - | inputRef.focus(); | |
| 19 | - | } | |
| 20 | - | }); | |
| 21 | - | ||
| 22 | - | const handleInput = (e: Event) => { | |
| 23 | - | const target = e.target as HTMLInputElement; | |
| 24 | - | handleSearchInput(target.value); | |
| 25 | - | }; | |
| 26 | - | ||
| 27 | - | const handleKeyDown = (e: KeyboardEvent) => { | |
| 28 | - | switch (e.key) { | |
| 29 | - | case 'Escape': | |
| 30 | - | e.preventDefault(); | |
| 31 | - | e.stopPropagation(); | |
| 32 | - | if (homepageState.historySearchQuery) { | |
| 33 | - | // First Esc: clear search | |
| 34 | - | homepageState.setHistorySearchQuery(''); | |
| 35 | - | } else { | |
| 36 | - | // Second Esc (or if no query): blur input | |
| 37 | - | inputRef?.blur(); | |
| 38 | - | } | |
| 39 | - | break; | |
| 40 | - | case 'ArrowDown': | |
| 41 | - | case 'j': | |
| 42 | - | if (e.key === 'j' && !e.ctrlKey) break; // Only Ctrl+j or ArrowDown | |
| 43 | - | e.preventDefault(); | |
| 44 | - | homepageState.focusNext(); | |
| 45 | - | inputRef?.blur(); | |
| 46 | - | break; | |
| 47 | - | case 'ArrowUp': | |
| 48 | - | case 'k': | |
| 49 | - | if (e.key === 'k' && !e.ctrlKey) break; // Only Ctrl+k or ArrowUp | |
| 50 | - | e.preventDefault(); | |
| 51 | - | // Don't move focus - already at top | |
| 52 | - | break; | |
| 53 | - | } | |
| 54 | - | }; | |
| 55 | - | ||
| 56 | - | // Focus input when focusedIndex becomes -1 (user pressed up from first item) | |
| 57 | - | $effect(() => { | |
| 58 | - | if (homepageState.focusedIndex === -1 && homepageState.activePanel === 'history') { | |
| 59 | - | inputRef?.focus(); | |
| 60 | - | } | |
| 61 | - | }); | |
| 62 | - | ||
| 63 | - | const clearSearch = () => { | |
| 64 | - | homepageState.setHistorySearchQuery(''); | |
| 65 | - | inputRef?.focus(); | |
| 66 | - | }; | |
| 67 | - | </script> | |
| 68 | - | ||
| 69 | - | <div class="history-search"> | |
| 70 | - | <div class="search-icon"></div> | |
| 71 | - | <input | |
| 72 | - | bind:this={inputRef} | |
| 73 | - | type="text" | |
| 74 | - | class="search-input" | |
| 75 | - | {placeholder} | |
| 76 | - | value={homepageState.historySearchQuery} | |
| 77 | - | oninput={handleInput} | |
| 78 | - | onkeydown={handleKeyDown} | |
| 79 | - | /> | |
| 80 | - | {#if homepageState.historySearching} | |
| 81 | - | <div class="search-spinner"></div> | |
| 82 | - | {:else if homepageState.historySearchQuery} | |
| 83 | - | <button | |
| 84 | - | class="clear-btn" | |
| 85 | - | type="button" | |
| 86 | - | onclick={clearSearch} | |
| 87 | - | title="Clear search (Esc)" | |
| 88 | - | > | |
| 89 | - | × | |
| 90 | - | </button> | |
| 91 | - | {:else} | |
| 92 | - | <kbd class="search-hint">/</kbd> | |
| 93 | - | {/if} | |
| 94 | - | </div> | |
| 95 | - | ||
| 96 | - | <style> | |
| 97 | - | .history-search { | |
| 98 | - | display: flex; | |
| 99 | - | align-items: center; | |
| 100 | - | gap: 0.6rem; | |
| 101 | - | padding: 0.5rem 0.75rem; | |
| 102 | - | border-width: 1px; | |
| 103 | - | border-style: solid; | |
| 104 | - | border-color: var(--border); | |
| 105 | - | background: var(--background); | |
| 106 | - | transition: border-color 150ms ease; | |
| 107 | - | } | |
| 108 | - | ||
| 109 | - | .history-search:focus-within { | |
| 110 | - | border-color: var(--primary, #4ade80); | |
| 111 | - | } | |
| 112 | - | ||
| 113 | - | .search-icon::before { | |
| 114 | - | content: ''; | |
| 115 | - | font-size: 0.85rem; | |
| 116 | - | color: var(--muted-foreground); | |
| 117 | - | } | |
| 118 | - | ||
| 119 | - | .search-input { | |
| 120 | - | flex: 1; | |
| 121 | - | border: none; | |
| 122 | - | background: transparent; | |
| 123 | - | color: var(--foreground); | |
| 124 | - | font-family: inherit; | |
| 125 | - | font-size: 0.8rem; | |
| 126 | - | outline: none; | |
| 127 | - | } | |
| 128 | - | ||
| 129 | - | .search-input::placeholder { | |
| 130 | - | color: var(--muted-foreground); | |
| 131 | - | letter-spacing: 0.05em; | |
| 132 | - | } | |
| 133 | - | ||
| 134 | - | .search-spinner { | |
| 135 | - | width: 14px; | |
| 136 | - | height: 14px; | |
| 137 | - | border-width: 2px; | |
| 138 | - | border-style: solid; | |
| 139 | - | border-color: var(--border); | |
| 140 | - | border-top-color: var(--primary, #4ade80); | |
| 141 | - | border-radius: 50%; | |
| 142 | - | animation: spin 0.8s linear infinite; | |
| 143 | - | } | |
| 144 | - | ||
| 145 | - | @keyframes spin { | |
| 146 | - | to { transform: rotate(360deg); } | |
| 147 | - | } | |
| 148 | - | ||
| 149 | - | .clear-btn { | |
| 150 | - | width: 20px; | |
| 151 | - | height: 20px; | |
| 152 | - | display: flex; | |
| 153 | - | align-items: center; | |
| 154 | - | justify-content: center; | |
| 155 | - | border-width: 1px; | |
| 156 | - | border-style: solid; | |
| 157 | - | border-color: var(--border); | |
| 158 | - | background: transparent; | |
| 159 | - | color: var(--muted-foreground); | |
| 160 | - | font-size: 0.9rem; | |
| 161 | - | cursor: pointer; | |
| 162 | - | transition: all 100ms ease; | |
| 163 | - | } | |
| 164 | - | ||
| 165 | - | .clear-btn:hover { | |
| 166 | - | color: var(--foreground); | |
| 167 | - | border-color: var(--foreground); | |
| 168 | - | } | |
| 169 | - | ||
| 170 | - | .search-hint { | |
| 171 | - | font-size: 0.6rem; | |
| 172 | - | padding: 0.15rem 0.35rem; | |
| 173 | - | border-width: 1px; | |
| 174 | - | border-style: solid; | |
| 175 | - | border-color: var(--border); | |
| 176 | - | background: color-mix(in srgb, var(--card) 30%, transparent); | |
| 177 | - | color: var(--muted-foreground); | |
| 178 | - | } | |
| 179 | - | </style> | |
D webui/src/pages/homepage/history/HistoryTimeline.svelte +0 -223
| @@ -1,223 +0,0 @@ | |||
| 1 | - | <script lang="ts"> | |
| 2 | - | import type { TimelineGroup, HistoryEntry } from '../types'; | |
| 3 | - | import { homepageState } from '../state.svelte'; | |
| 4 | - | import HistoryItem from './HistoryItem.svelte'; | |
| 5 | - | ||
| 6 | - | interface Props { | |
| 7 | - | groups: TimelineGroup[]; | |
| 8 | - | onSelectEntry?: (entry: HistoryEntry) => void; | |
| 9 | - | onDeleteEntry?: (entry: HistoryEntry) => void; | |
| 10 | - | onLoadMore?: () => void; | |
| 11 | - | } | |
| 12 | - | ||
| 13 | - | let { groups, onSelectEntry, onDeleteEntry, onLoadMore }: Props = $props(); | |
| 14 | - | ||
| 15 | - | let containerRef = $state<HTMLElement | null>(null); | |
| 16 | - | let sentinelRef = $state<HTMLElement | null>(null); | |
| 17 | - | ||
| 18 | - | // Flatten groups to get global index for keyboard navigation | |
| 19 | - | const flatEntries = $derived(() => { | |
| 20 | - | const entries: HistoryEntry[] = []; | |
| 21 | - | for (const group of groups) { | |
| 22 | - | entries.push(...group.entries); | |
| 23 | - | } | |
| 24 | - | return entries; | |
| 25 | - | }); | |
| 26 | - | ||
| 27 | - | // Track which entry is focused based on global focusedIndex | |
| 28 | - | const getFocusedEntry = () => { | |
| 29 | - | const entries = flatEntries(); | |
| 30 | - | return entries[homepageState.focusedIndex] ?? null; | |
| 31 | - | }; | |
| 32 | - | ||
| 33 | - | // Track previous focused index to only scroll on actual navigation | |
| 34 | - | let prevFocusedIndex = $state(-1); | |
| 35 | - | ||
| 36 | - | // Scroll focused item into view when focusedIndex changes via keyboard navigation | |
| 37 | - | $effect(() => { | |
| 38 | - | // Skip if panel not active or command palette is open | |
| 39 | - | if (!containerRef || homepageState.activePanel !== 'history') return; | |
| 40 | - | if (homepageState.commandPaletteOpen) return; | |
| 41 | - | ||
| 42 | - | const currentIndex = homepageState.focusedIndex; | |
| 43 | - | ||
| 44 | - | // Only scroll if index actually changed (not on initial render or modal close) | |
| 45 | - | if (prevFocusedIndex === currentIndex) return; | |
| 46 | - | if (prevFocusedIndex === -1) { | |
| 47 | - | // First render - just track, don't scroll | |
| 48 | - | prevFocusedIndex = currentIndex; | |
| 49 | - | return; | |
| 50 | - | } | |
| 51 | - | ||
| 52 | - | prevFocusedIndex = currentIndex; | |
| 53 | - | ||
| 54 | - | const focusedEntry = getFocusedEntry(); | |
| 55 | - | if (!focusedEntry) return; | |
| 56 | - | ||
| 57 | - | // Find the focused element by entry ID | |
| 58 | - | const el = containerRef.querySelector(`[data-entry-id="${focusedEntry.id}"]`); | |
| 59 | - | if (el) { | |
| 60 | - | el.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); | |
| 61 | - | } | |
| 62 | - | }); | |
| 63 | - | ||
| 64 | - | // Setup infinite scroll observer | |
| 65 | - | $effect(() => { | |
| 66 | - | if (!sentinelRef || !onLoadMore) return; | |
| 67 | - | ||
| 68 | - | const observer = new IntersectionObserver( | |
| 69 | - | (entries) => { | |
| 70 | - | const entry = entries[0]; | |
| 71 | - | if (entry?.isIntersecting && homepageState.hasMoreHistory && !homepageState.historyLoading) { | |
| 72 | - | onLoadMore(); | |
| 73 | - | } | |
| 74 | - | }, | |
| 75 | - | { threshold: 0.1, rootMargin: '100px' } | |
| 76 | - | ); | |
| 77 | - | ||
| 78 | - | observer.observe(sentinelRef); | |
| 79 | - | ||
| 80 | - | return () => observer.disconnect(); | |
| 81 | - | }); | |
| 82 | - | </script> | |
| 83 | - | ||
| 84 | - | <div class="history-timeline" bind:this={containerRef}> | |
| 85 | - | {#if groups.length === 0} | |
| 86 | - | <div class="empty-state"> | |
| 87 | - | <span class="empty-icon"></span> | |
| 88 | - | <span class="empty-text">NO HISTORY RECORDED</span> | |
| 89 | - | <span class="empty-hint">Start browsing to see your history here</span> | |
| 90 | - | </div> | |
| 91 | - | {:else} | |
| 92 | - | {#each groups as group (group.date)} | |
| 93 | - | <div class="timeline-group"> | |
| 94 | - | <div class="group-header"> | |
| 95 | - | <span class="group-label">{group.label}</span> | |
| 96 | - | <span class="group-count">{group.entries.length}</span> | |
| 97 | - | </div> | |
| 98 | - | <div class="group-entries"> | |
| 99 | - | {#each group.entries as entry (entry.id)} | |
| 100 | - | <HistoryItem | |
| 101 | - | {entry} | |
| 102 | - | focused={getFocusedEntry()?.id === entry.id} | |
| 103 | - | onSelect={onSelectEntry} | |
| 104 | - | onDelete={onDeleteEntry} | |
| 105 | - | /> | |
| 106 | - | {/each} | |
| 107 | - | </div> | |
| 108 | - | </div> | |
| 109 | - | {/each} | |
| 110 | - | ||
| 111 | - | {#if homepageState.hasMoreHistory} | |
| 112 | - | <div class="load-more-sentinel" bind:this={sentinelRef}> | |
| 113 | - | {#if homepageState.historyLoading} | |
| 114 | - | <span class="loading-text">LOADING...</span> | |
| 115 | - | {/if} | |
| 116 | - | </div> | |
| 117 | - | {/if} | |
| 118 | - | {/if} | |
| 119 | - | </div> | |
| 120 | - | ||
| 121 | - | <style> | |
| 122 | - | .history-timeline { | |
| 123 | - | display: flex; | |
| 124 | - | flex-direction: column; | |
| 125 | - | } | |
| 126 | - | ||
| 127 | - | .empty-state { | |
| 128 | - | flex: 1; | |
| 129 | - | display: flex; | |
| 130 | - | flex-direction: column; | |
| 131 | - | align-items: center; | |
| 132 | - | justify-content: center; | |
| 133 | - | gap: 0.5rem; | |
| 134 | - | padding: 2rem; | |
| 135 | - | color: var(--muted-foreground); | |
| 136 | - | text-align: center; | |
| 137 | - | } | |
| 138 | - | ||
| 139 | - | .empty-icon { | |
| 140 | - | font-size: 1.5rem; | |
| 141 | - | opacity: 0.5; | |
| 142 | - | } | |
| 143 | - | ||
| 144 | - | .empty-text { | |
| 145 | - | font-size: 0.75rem; | |
| 146 | - | font-weight: 600; | |
| 147 | - | letter-spacing: 0.12em; | |
| 148 | - | } | |
| 149 | - | ||
| 150 | - | .empty-hint { | |
| 151 | - | font-size: 0.7rem; | |
| 152 | - | letter-spacing: 0.08em; | |
| 153 | - | opacity: 0.7; | |
| 154 | - | } | |
| 155 | - | ||
| 156 | - | .timeline-group { | |
| 157 | - | border-bottom-width: 1px; | |
| 158 | - | border-bottom-style: solid; | |
| 159 | - | border-bottom-color: var(--border); | |
| 160 | - | } | |
| 161 | - | ||
| 162 | - | .timeline-group:last-child { | |
| 163 | - | border-bottom: none; | |
| 164 | - | } | |
| 165 | - | ||
| 166 | - | .group-header { | |
| 167 | - | display: flex; | |
| 168 | - | align-items: center; | |
| 169 | - | justify-content: space-between; | |
| 170 | - | gap: 0.75rem; | |
| 171 | - | padding: 0.5rem 0.85rem; | |
| 172 | - | background: color-mix(in srgb, var(--background) 80%, var(--card) 20%); | |
| 173 | - | border-bottom-width: 1px; | |
| 174 | - | border-bottom-style: solid; | |
| 175 | - | border-bottom-color: color-mix(in srgb, var(--border) 50%, transparent); | |
| 176 | - | position: sticky; | |
| 177 | - | top: 0; | |
| 178 | - | z-index: 1; | |
| 179 | - | } | |
| 180 | - | ||
| 181 | - | .group-label { | |
| 182 | - | font-size: 0.68rem; | |
| 183 | - | font-weight: 600; | |
| 184 | - | text-transform: uppercase; | |
| 185 | - | letter-spacing: 0.12em; | |
| 186 | - | color: var(--foreground); | |
| 187 | - | } | |
| 188 | - | ||
| 189 | - | .group-count { | |
| 190 | - | font-size: 0.6rem; | |
| 191 | - | padding: 0.15rem 0.4rem; | |
| 192 | - | border-width: 1px; | |
| 193 | - | border-style: solid; | |
| 194 | - | border-color: var(--border); | |
| 195 | - | background: var(--background); | |
| 196 | - | color: var(--muted-foreground); | |
| 197 | - | } | |
| 198 | - | ||
| 199 | - | .group-entries { | |
| 200 | - | display: flex; | |
| 201 | - | flex-direction: column; | |
| 202 | - | } | |
| 203 | - | ||
| 204 | - | .load-more-sentinel { | |
| 205 | - | padding: 0.75rem; | |
| 206 | - | display: flex; | |
| 207 | - | align-items: center; | |
| 208 | - | justify-content: center; | |
| 209 | - | min-height: 40px; | |
| 210 | - | } | |
| 211 | - | ||
| 212 | - | .loading-text { | |
| 213 | - | font-size: 0.65rem; | |
| 214 | - | color: var(--muted-foreground); | |
| 215 | - | letter-spacing: 0.12em; | |
| 216 | - | animation: pulse 1.5s ease-in-out infinite; | |
| 217 | - | } | |
| 218 | - | ||
| 219 | - | @keyframes pulse { | |
| 220 | - | 0%, 100% { opacity: 0.4; } | |
| 221 | - | 50% { opacity: 1; } | |
| 222 | - | } | |
| 223 | - | </style> | |
D webui/src/pages/homepage/history/index.ts +0 -6
| @@ -1,6 +0,0 @@ | |||
| 1 | - | // History Components - Timeline, Search, Filters, Cleanup | |
| 2 | - | export { default as HistoryItem } from './HistoryItem.svelte'; | |
| 3 | - | export { default as HistoryTimeline } from './HistoryTimeline.svelte'; | |
| 4 | - | export { default as HistorySearch } from './HistorySearch.svelte'; | |
| 5 | - | export { default as HistoryFilters } from './HistoryFilters.svelte'; | |
| 6 | - | export { default as HistoryCleanup } from './HistoryCleanup.svelte'; | |
D webui/src/pages/homepage/keyboard.ts +0 -744
| @@ -1,744 +0,0 @@ | |||
| 1 | - | // ═══════════════════════════════════════════════════════════════════════════════ | |
| 2 | - | // Homepage Keyboard Navigation (Telescope-style) | |
| 3 | - | // Vim-inspired navigation with fuzzy filtering and quick shortcuts | |
| 4 | - | // ═══════════════════════════════════════════════════════════════════════════════ | |
| 5 | - | ||
| 6 | - | import { homepageState } from './state.svelte'; | |
| 7 | - | import { | |
| 8 | - | navigateTo, | |
| 9 | - | deleteHistoryEntry, | |
| 10 | - | deleteHistoryByRange, | |
| 11 | - | clearAllHistory, | |
| 12 | - | deleteHistoryByDomain, | |
| 13 | - | getFavoriteByShortcut, | |
| 14 | - | searchHistoryFTS, | |
| 15 | - | } from './messaging'; | |
| 16 | - | import type { HistoryEntry, Favorite, HistoryCleanupRange } from './types'; | |
| 17 | - | ||
| 18 | - | // ═══════════════════════════════════════════════════════════════════════════════ | |
| 19 | - | // TYPES | |
| 20 | - | // ═══════════════════════════════════════════════════════════════════════════════ | |
| 21 | - | ||
| 22 | - | interface KeyBinding { | |
| 23 | - | key: string; | |
| 24 | - | ctrl?: boolean; | |
| 25 | - | shift?: boolean; | |
| 26 | - | alt?: boolean; | |
| 27 | - | meta?: boolean; | |
| 28 | - | description: string; | |
| 29 | - | action: () => void | Promise<void>; | |
| 30 | - | context?: 'global' | 'history' | 'favorites' | 'command-palette' | 'cleanup'; | |
| 31 | - | } | |
| 32 | - | ||
| 33 | - | // ═══════════════════════════════════════════════════════════════════════════════ | |
| 34 | - | // STATE | |
| 35 | - | // ═══════════════════════════════════════════════════════════════════════════════ | |
| 36 | - | ||
| 37 | - | let pendingPrefix: string | null = null; | |
| 38 | - | let prefixTimeout: ReturnType<typeof setTimeout> | null = null; | |
| 39 | - | const PREFIX_TIMEOUT_MS = 1000; | |
| 40 | - | ||
| 41 | - | // ═══════════════════════════════════════════════════════════════════════════════ | |
| 42 | - | // HELPER FUNCTIONS | |
| 43 | - | // ═══════════════════════════════════════════════════════════════════════════════ | |
| 44 | - | ||
| 45 | - | function getDomain(url: string): string { | |
| 46 | - | try { | |
| 47 | - | return new URL(url).hostname; | |
| 48 | - | } catch { | |
| 49 | - | return url; | |
| 50 | - | } | |
| 51 | - | } | |
| 52 | - | ||
| 53 | - | function getCurrentItem(): HistoryEntry | Favorite | null { | |
| 54 | - | const list = homepageState.currentList; | |
| 55 | - | const idx = homepageState.focusedIndex; | |
| 56 | - | return list[idx] ?? null; | |
| 57 | - | } | |
| 58 | - | ||
| 59 | - | function clearPendingPrefix(): void { | |
| 60 | - | pendingPrefix = null; | |
| 61 | - | homepageState.setPendingKeyPrefix(null); | |
| 62 | - | if (prefixTimeout) { | |
| 63 | - | clearTimeout(prefixTimeout); | |
| 64 | - | prefixTimeout = null; | |
| 65 | - | } | |
| 66 | - | } | |
| 67 | - | ||
| 68 | - | function setPendingPrefix(prefix: string): void { | |
| 69 | - | pendingPrefix = prefix; | |
| 70 | - | homepageState.setPendingKeyPrefix(prefix); | |
| 71 | - | if (prefixTimeout) { | |
| 72 | - | clearTimeout(prefixTimeout); | |
| 73 | - | } | |
| 74 | - | prefixTimeout = setTimeout(clearPendingPrefix, PREFIX_TIMEOUT_MS); | |
| 75 | - | } | |
| 76 | - | ||
| 77 | - | // ═══════════════════════════════════════════════════════════════════════════════ | |
| 78 | - | // ACTIONS | |
| 79 | - | // ═══════════════════════════════════════════════════════════════════════════════ | |
| 80 | - | ||
| 81 | - | async function openCurrentItem(): Promise<void> { | |
| 82 | - | const item = getCurrentItem(); | |
| 83 | - | if (item && 'url' in item) { | |
| 84 | - | navigateTo(item.url); | |
| 85 | - | } | |
| 86 | - | } | |
| 87 | - | ||
| 88 | - | async function deleteCurrentHistoryEntry(): Promise<void> { | |
| 89 | - | if (homepageState.activePanel !== 'history') return; | |
| 90 | - | ||
| 91 | - | const item = getCurrentItem() as HistoryEntry | null; | |
| 92 | - | if (!item) return; | |
| 93 | - | ||
| 94 | - | await deleteHistoryEntry(item.id); | |
| 95 | - | ||
| 96 | - | // Adjust focus if needed | |
| 97 | - | const newLength = homepageState.currentListLength; | |
| 98 | - | if (homepageState.focusedIndex >= newLength) { | |
| 99 | - | homepageState.setFocusedIndex(Math.max(0, newLength - 1)); | |
| 100 | - | } | |
| 101 | - | } | |
| 102 | - | ||
| 103 | - | async function handleCleanupShortcut(range: HistoryCleanupRange): Promise<void> { | |
| 104 | - | if (homepageState.activePanel !== 'history') return; | |
| 105 | - | ||
| 106 | - | const messages: Record<HistoryCleanupRange, string> = { | |
| 107 | - | hour: 'Clear history from the last hour?', | |
| 108 | - | day: 'Clear history from the last day?', | |
| 109 | - | week: 'Clear history from the last week?', | |
| 110 | - | month: 'Clear history from the last month?', | |
| 111 | - | all: 'Clear ALL history? This cannot be undone.', | |
| 112 | - | }; | |
| 113 | - | ||
| 114 | - | homepageState.showConfirm(messages[range], async () => { | |
| 115 | - | if (range === 'all') { | |
| 116 | - | await clearAllHistory(); | |
| 117 | - | } else { | |
| 118 | - | await deleteHistoryByRange(range); | |
| 119 | - | } | |
| 120 | - | }); | |
| 121 | - | } | |
| 122 | - | ||
| 123 | - | async function clearCurrentDomain(): Promise<void> { | |
| 124 | - | if (homepageState.activePanel !== 'history') return; | |
| 125 | - | ||
| 126 | - | const item = getCurrentItem() as HistoryEntry | null; | |
| 127 | - | if (!item) return; | |
| 128 | - | ||
| 129 | - | const domain = getDomain(item.url); | |
| 130 | - | homepageState.showConfirm( | |
| 131 | - | `Clear all history for ${domain}?`, | |
| 132 | - | async () => { | |
| 133 | - | await deleteHistoryByDomain(domain); | |
| 134 | - | } | |
| 135 | - | ); | |
| 136 | - | } | |
| 137 | - | ||
| 138 | - | async function handleQuickShortcut(key: number): Promise<void> { | |
| 139 | - | const favorite = await getFavoriteByShortcut(key); | |
| 140 | - | if (favorite) { | |
| 141 | - | navigateTo(favorite.url); | |
| 142 | - | } | |
| 143 | - | } | |
| 144 | - | ||
| 145 | - | // ═══════════════════════════════════════════════════════════════════════════════ | |
| 146 | - | // KEY BINDINGS | |
| 147 | - | // ═══════════════════════════════════════════════════════════════════════════════ | |
| 148 | - | ||
| 149 | - | const globalBindings: KeyBinding[] = [ | |
| 150 | - | // Command palette | |
| 151 | - | { | |
| 152 | - | key: 'p', | |
| 153 | - | ctrl: true, | |
| 154 | - | description: 'Open command palette', | |
| 155 | - | action: () => homepageState.openCommandPalette(), | |
| 156 | - | }, | |
| 157 | - | // Note: "/" may require Shift on some keyboard layouts (e.g., AZERTY) | |
| 158 | - | // We handle it specially in handleKeyDown to ignore Shift state | |
| 159 | - | { | |
| 160 | - | key: '/', | |
| 161 | - | description: 'Open command palette / search', | |
| 162 | - | action: () => homepageState.openCommandPalette(), | |
| 163 | - | }, | |
| 164 | - | ||
| 165 | - | // Quick navigation (1-9) | |
| 166 | - | ...Array.from({ length: 9 }, (_, i) => ({ | |
| 167 | - | key: String(i + 1), | |
| 168 | - | description: `Navigate to favorite shortcut ${i + 1}`, | |
| 169 | - | action: () => handleQuickShortcut(i + 1), | |
| 170 | - | })), | |
| 171 | - | ||
| 172 | - | // Panel navigation (g prefix) | |
| 173 | - | { | |
| 174 | - | key: 'h', | |
| 175 | - | description: 'Go to History panel', | |
| 176 | - | action: () => { | |
| 177 | - | if (pendingPrefix === 'g') { | |
| 178 | - | homepageState.setActivePanel('history'); | |
| 179 | - | clearPendingPrefix(); | |
| 180 | - | } | |
| 181 | - | }, | |
| 182 | - | context: 'global', | |
| 183 | - | }, | |
| 184 | - | { | |
| 185 | - | key: 'f', | |
| 186 | - | description: 'Go to Favorites panel', | |
| 187 | - | action: () => { | |
| 188 | - | if (pendingPrefix === 'g') { | |
| 189 | - | homepageState.setActivePanel('favorites'); | |
| 190 | - | clearPendingPrefix(); | |
| 191 | - | } | |
| 192 | - | }, | |
| 193 | - | context: 'global', | |
| 194 | - | }, | |
| 195 | - | { | |
| 196 | - | key: 'a', | |
| 197 | - | description: 'Go to Analytics panel', | |
| 198 | - | action: () => { | |
| 199 | - | if (pendingPrefix === 'g') { | |
| 200 | - | homepageState.setActivePanel('analytics'); | |
| 201 | - | clearPendingPrefix(); | |
| 202 | - | } | |
| 203 | - | }, | |
| 204 | - | context: 'global', | |
| 205 | - | }, | |
| 206 | - | ||
| 207 | - | // Escape | |
| 208 | - | { | |
| 209 | - | key: 'Escape', | |
| 210 | - | description: 'Close modals / clear search', | |
| 211 | - | action: () => { | |
| 212 | - | clearPendingPrefix(); | |
| 213 | - | if (homepageState.commandPaletteOpen) { | |
| 214 | - | homepageState.closeCommandPalette(); | |
| 215 | - | } else if (homepageState.confirmModalOpen) { | |
| 216 | - | homepageState.cancelConfirm(); | |
| 217 | - | } else if (homepageState.cleanupModalOpen) { | |
| 218 | - | homepageState.closeCleanupModal(); | |
| 219 | - | } else if (homepageState.historySearchQuery) { | |
| 220 | - | homepageState.setHistorySearchQuery(''); | |
| 221 | - | } | |
| 222 | - | }, | |
| 223 | - | }, | |
| 224 | - | ||
| 225 | - | // Tab - cycle through panels | |
| 226 | - | { | |
| 227 | - | key: 'Tab', | |
| 228 | - | description: 'Next panel', | |
| 229 | - | action: () => { | |
| 230 | - | const panels = ['history', 'favorites', 'analytics'] as const; | |
| 231 | - | const currentIdx = panels.indexOf(homepageState.activePanel); | |
| 232 | - | const nextIdx = (currentIdx + 1) % panels.length; | |
| 233 | - | const nextPanel = panels[nextIdx]; | |
| 234 | - | if (nextPanel) { | |
| 235 | - | homepageState.setActivePanel(nextPanel); | |
| 236 | - | } | |
| 237 | - | }, | |
| 238 | - | }, | |
| 239 | - | { | |
| 240 | - | key: 'Tab', | |
| 241 | - | shift: true, | |
| 242 | - | description: 'Previous panel', | |
| 243 | - | action: () => { | |
| 244 | - | const panels = ['history', 'favorites', 'analytics'] as const; | |
| 245 | - | const currentIdx = panels.indexOf(homepageState.activePanel); | |
| 246 | - | const prevIdx = (currentIdx - 1 + panels.length) % panels.length; | |
| 247 | - | const prevPanel = panels[prevIdx]; | |
| 248 | - | if (prevPanel) { | |
| 249 | - | homepageState.setActivePanel(prevPanel); | |
| 250 | - | } | |
| 251 | - | }, | |
| 252 | - | }, | |
| 253 | - | ]; | |
| 254 | - | ||
| 255 | - | const listBindings: KeyBinding[] = [ | |
| 256 | - | // Navigation | |
| 257 | - | { | |
| 258 | - | key: 'j', | |
| 259 | - | description: 'Move down', | |
| 260 | - | action: () => homepageState.focusNext(), | |
| 261 | - | }, | |
| 262 | - | { | |
| 263 | - | key: 'ArrowDown', | |
| 264 | - | description: 'Move down', | |
| 265 | - | action: () => homepageState.focusNext(), | |
| 266 | - | }, | |
| 267 | - | { | |
| 268 | - | key: 'k', | |
| 269 | - | description: 'Move up', | |
| 270 | - | action: () => homepageState.focusPrev(), | |
| 271 | - | }, | |
| 272 | - | { | |
| 273 | - | key: 'ArrowUp', | |
| 274 | - | description: 'Move up', | |
| 275 | - | action: () => homepageState.focusPrev(), | |
| 276 | - | }, | |
| 277 | - | { | |
| 278 | - | key: 'n', | |
| 279 | - | ctrl: true, | |
| 280 | - | description: 'Move down', | |
| 281 | - | action: () => homepageState.focusNext(), | |
| 282 | - | }, | |
| 283 | - | { | |
| 284 | - | key: 'p', | |
| 285 | - | ctrl: true, | |
| 286 | - | description: 'Move up', | |
| 287 | - | action: () => homepageState.focusPrev(), | |
| 288 | - | }, | |
| 289 | - | { | |
| 290 | - | key: 'g', | |
| 291 | - | description: 'Go to first / prefix for panel nav', | |
| 292 | - | action: () => { | |
| 293 | - | if (pendingPrefix === 'g') { | |
| 294 | - | homepageState.focusFirst(); | |
| 295 | - | clearPendingPrefix(); | |
| 296 | - | } else { | |
| 297 | - | setPendingPrefix('g'); | |
| 298 | - | } | |
| 299 | - | }, | |
| 300 | - | }, | |
| 301 | - | { | |
| 302 | - | key: 'G', | |
| 303 | - | shift: true, | |
| 304 | - | description: 'Go to last', | |
| 305 | - | action: () => homepageState.focusLast(), | |
| 306 | - | }, | |
| 307 | - | ||
| 308 | - | // Selection | |
| 309 | - | { | |
| 310 | - | key: 'Enter', | |
| 311 | - | description: 'Open selected item', | |
| 312 | - | action: openCurrentItem, | |
| 313 | - | }, | |
| 314 | - | { | |
| 315 | - | key: 'o', | |
| 316 | - | description: 'Open selected item', | |
| 317 | - | action: openCurrentItem, | |
| 318 | - | }, | |
| 319 | - | ]; | |
| 320 | - | ||
| 321 | - | const historyBindings: KeyBinding[] = [ | |
| 322 | - | // Delete | |
| 323 | - | { | |
| 324 | - | key: 'd', | |
| 325 | - | description: 'Delete entry / prefix for cleanup', | |
| 326 | - | action: () => { | |
| 327 | - | if (pendingPrefix === 'D') { | |
| 328 | - | // D d - clear last day | |
| 329 | - | handleCleanupShortcut('day'); | |
| 330 | - | clearPendingPrefix(); | |
| 331 | - | } else { | |
| 332 | - | deleteCurrentHistoryEntry(); | |
| 333 | - | } | |
| 334 | - | }, | |
| 335 | - | context: 'history', | |
| 336 | - | }, | |
| 337 | - | { | |
| 338 | - | key: 'x', | |
| 339 | - | description: 'Delete entry', | |
| 340 | - | action: deleteCurrentHistoryEntry, | |
| 341 | - | context: 'history', | |
| 342 | - | }, | |
| 343 | - | ||
| 344 | - | // Cleanup shortcuts (D prefix) | |
| 345 | - | { | |
| 346 | - | key: 'D', | |
| 347 | - | shift: true, | |
| 348 | - | description: 'Cleanup prefix / Clear ALL', | |
| 349 | - | action: () => { | |
| 350 | - | if (pendingPrefix === 'D') { | |
| 351 | - | // D D - clear all | |
| 352 | - | handleCleanupShortcut('all'); | |
| 353 | - | clearPendingPrefix(); | |
| 354 | - | } else { | |
| 355 | - | setPendingPrefix('D'); | |
| 356 | - | } | |
| 357 | - | }, | |
| 358 | - | context: 'history', | |
| 359 | - | }, | |
| 360 | - | { | |
| 361 | - | key: 'h', | |
| 362 | - | description: 'Clear last hour', | |
| 363 | - | action: () => { | |
| 364 | - | if (pendingPrefix === 'D') { | |
| 365 | - | handleCleanupShortcut('hour'); | |
| 366 | - | clearPendingPrefix(); | |
| 367 | - | } | |
| 368 | - | }, | |
| 369 | - | context: 'history', | |
| 370 | - | }, | |
| 371 | - | { | |
| 372 | - | key: 'w', | |
| 373 | - | description: 'Clear last week', | |
| 374 | - | action: () => { | |
| 375 | - | if (pendingPrefix === 'D') { | |
| 376 | - | handleCleanupShortcut('week'); | |
| 377 | - | clearPendingPrefix(); | |
| 378 | - | } | |
| 379 | - | }, | |
| 380 | - | context: 'history', | |
| 381 | - | }, | |
| 382 | - | { | |
| 383 | - | key: 'm', | |
| 384 | - | description: 'Clear last month', | |
| 385 | - | action: () => { | |
| 386 | - | if (pendingPrefix === 'D') { | |
| 387 | - | handleCleanupShortcut('month'); | |
| 388 | - | clearPendingPrefix(); | |
| 389 | - | } | |
| 390 | - | }, | |
| 391 | - | context: 'history', | |
| 392 | - | }, | |
| 393 | - | { | |
| 394 | - | key: '@', | |
| 395 | - | shift: true, | |
| 396 | - | description: 'Clear current domain', | |
| 397 | - | action: () => { | |
| 398 | - | if (pendingPrefix === 'D') { | |
| 399 | - | clearCurrentDomain(); | |
| 400 | - | clearPendingPrefix(); | |
| 401 | - | } | |
| 402 | - | }, | |
| 403 | - | context: 'history', | |
| 404 | - | }, | |
| 405 | - | ]; | |
| 406 | - | ||
| 407 | - | const commandPaletteBindings: KeyBinding[] = [ | |
| 408 | - | { | |
| 409 | - | key: 'j', | |
| 410 | - | ctrl: true, | |
| 411 | - | description: 'Move down', | |
| 412 | - | action: () => homepageState.focusNext(), | |
| 413 | - | context: 'command-palette', | |
| 414 | - | }, | |
| 415 | - | { | |
| 416 | - | key: 'n', | |
| 417 | - | ctrl: true, | |
| 418 | - | description: 'Move down', | |
| 419 | - | action: () => homepageState.focusNext(), | |
| 420 | - | context: 'command-palette', | |
| 421 | - | }, | |
| 422 | - | { | |
| 423 | - | key: 'ArrowDown', | |
| 424 | - | description: 'Move down', | |
| 425 | - | action: () => homepageState.focusNext(), | |
| 426 | - | context: 'command-palette', | |
| 427 | - | }, | |
| 428 | - | { | |
| 429 | - | key: 'k', | |
| 430 | - | ctrl: true, | |
| 431 | - | description: 'Move up', | |
| 432 | - | action: () => homepageState.focusPrev(), | |
| 433 | - | context: 'command-palette', | |
| 434 | - | }, | |
| 435 | - | { | |
| 436 | - | key: 'p', | |
| 437 | - | ctrl: true, | |
| 438 | - | description: 'Move up', | |
| 439 | - | action: () => homepageState.focusPrev(), | |
| 440 | - | context: 'command-palette', | |
| 441 | - | }, | |
| 442 | - | { | |
| 443 | - | key: 'ArrowUp', | |
| 444 | - | description: 'Move up', | |
| 445 | - | action: () => homepageState.focusPrev(), | |
| 446 | - | context: 'command-palette', | |
| 447 | - | }, | |
| 448 | - | { | |
| 449 | - | key: 'Enter', | |
| 450 | - | description: 'Execute selected command', | |
| 451 | - | action: () => { | |
| 452 | - | // This will be handled by the command palette component | |
| 453 | - | }, | |
| 454 | - | context: 'command-palette', | |
| 455 | - | }, | |
| 456 | - | ]; | |
| 457 | - | ||
| 458 | - | // ═══════════════════════════════════════════════════════════════════════════════ | |
| 459 | - | // KEY HANDLER | |
| 460 | - | // ═══════════════════════════════════════════════════════════════════════════════ | |
| 461 | - | ||
| 462 | - | function matchBinding(e: KeyboardEvent, binding: KeyBinding): boolean { | |
| 463 | - | if (binding.key !== e.key) return false; | |
| 464 | - | if (!!binding.ctrl !== e.ctrlKey) return false; | |
| 465 | - | if (!!binding.shift !== e.shiftKey) return false; | |
| 466 | - | if (!!binding.alt !== e.altKey) return false; | |
| 467 | - | if (!!binding.meta !== e.metaKey) return false; | |
| 468 | - | return true; | |
| 469 | - | } | |
| 470 | - | ||
| 471 | - | function getActiveBindings(): KeyBinding[] { | |
| 472 | - | const bindings: KeyBinding[] = [...globalBindings]; | |
| 473 | - | ||
| 474 | - | if (homepageState.commandPaletteOpen) { | |
| 475 | - | bindings.push(...commandPaletteBindings); | |
| 476 | - | } else { | |
| 477 | - | bindings.push(...listBindings); | |
| 478 | - | ||
| 479 | - | switch (homepageState.activePanel) { | |
| 480 | - | case 'history': | |
| 481 | - | bindings.push(...historyBindings); | |
| 482 | - | break; | |
| 483 | - | case 'favorites': | |
| 484 | - | // Add favorites-specific bindings here | |
| 485 | - | break; | |
| 486 | - | } | |
| 487 | - | } | |
| 488 | - | ||
| 489 | - | return bindings; | |
| 490 | - | } | |
| 491 | - | ||
| 492 | - | export function handleKeyDown(e: KeyboardEvent): void { | |
| 493 | - | // Skip if focused on input/textarea (except for navigation keys) | |
| 494 | - | const target = e.target as HTMLElement; | |
| 495 | - | const isInput = target.tagName === 'INPUT' || target.tagName === 'TEXTAREA'; | |
| 496 | - | ||
| 497 | - | // Special handling for "/" - opens command palette from anywhere | |
| 498 | - | // Ignore modifier keys since "/" may require Shift on some layouts (AZERTY) | |
| 499 | - | if (e.key === '/') { | |
| 500 | - | e.preventDefault(); | |
| 501 | - | e.stopPropagation(); | |
| 502 | - | if (isInput) { | |
| 503 | - | (target as HTMLInputElement).blur(); | |
| 504 | - | } | |
| 505 | - | homepageState.openCommandPalette(); | |
| 506 | - | return; | |
| 507 | - | } | |
| 508 | - | ||
| 509 | - | if (isInput) { | |
| 510 | - | // Allow specific keys when in input | |
| 511 | - | const allowedInInput = ['Escape', 'ArrowUp', 'ArrowDown', 'Enter']; | |
| 512 | - | const ctrlAllowed = ['j', 'k', 'n', 'p']; | |
| 513 | - | ||
| 514 | - | if (!allowedInInput.includes(e.key) && !(e.ctrlKey && ctrlAllowed.includes(e.key))) { | |
| 515 | - | return; | |
| 516 | - | } | |
| 517 | - | } | |
| 518 | - | ||
| 519 | - | const bindings = getActiveBindings(); | |
| 520 | - | ||
| 521 | - | for (const binding of bindings) { | |
| 522 | - | if (matchBinding(e, binding)) { | |
| 523 | - | // Check context | |
| 524 | - | if (binding.context) { | |
| 525 | - | switch (binding.context) { | |
| 526 | - | case 'history': | |
| 527 | - | if (homepageState.activePanel !== 'history') continue; | |
| 528 | - | break; | |
| 529 | - | case 'favorites': | |
| 530 | - | if (homepageState.activePanel !== 'favorites') continue; | |
| 531 | - | break; | |
| 532 | - | case 'command-palette': | |
| 533 | - | if (!homepageState.commandPaletteOpen) continue; | |
| 534 | - | break; | |
| 535 | - | } | |
| 536 | - | } | |
| 537 | - | ||
| 538 | - | e.preventDefault(); | |
| 539 | - | e.stopPropagation(); | |
| 540 | - | binding.action(); | |
| 541 | - | return; | |
| 542 | - | } | |
| 543 | - | } | |
| 544 | - | ||
| 545 | - | // Handle 'g' prefix standalone | |
| 546 | - | if (e.key === 'g' && !e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey && !isInput) { | |
| 547 | - | if (!pendingPrefix) { | |
| 548 | - | e.preventDefault(); | |
| 549 | - | setPendingPrefix('g'); | |
| 550 | - | } | |
| 551 | - | } | |
| 552 | - | } | |
| 553 | - | ||
| 554 | - | // ═══════════════════════════════════════════════════════════════════════════════ | |
| 555 | - | // TIERED SEARCH (Client-side first, then FTS) | |
| 556 | - | // ═══════════════════════════════════════════════════════════════════════════════ | |
| 557 | - | ||
| 558 | - | let searchDebounceTimer: ReturnType<typeof setTimeout> | null = null; | |
| 559 | - | const SEARCH_DEBOUNCE_MS = 400; | |
| 560 | - | const MIN_FTS_QUERY_LENGTH = 3; | |
| 561 | - | ||
| 562 | - | /** | |
| 563 | - | * Fuzzy match score calculator. | |
| 564 | - | * Supports words in any order (fzf-style). | |
| 565 | - | * Returns score > 0 if match, 0 if no match. Higher = better. | |
| 566 | - | */ | |
| 567 | - | function fuzzyScore(text: string, query: string): number { | |
| 568 | - | const lowerText = text.toLowerCase(); | |
| 569 | - | const lowerQuery = query.toLowerCase(); | |
| 570 | - | ||
| 571 | - | // Split query into words for "any order" matching | |
| 572 | - | const queryWords = lowerQuery.split(/\s+/).filter(w => w.length > 0); | |
| 573 | - | if (queryWords.length === 0) return 0; | |
| 574 | - | ||
| 575 | - | let totalScore = 0; | |
| 576 | - | ||
| 577 | - | for (const word of queryWords) { | |
| 578 | - | // Exact substring match (highest score) | |
| 579 | - | const exactIdx = lowerText.indexOf(word); | |
| 580 | - | if (exactIdx !== -1) { | |
| 581 | - | // Bonus for word boundary match (start of text or after space/punctuation) | |
| 582 | - | const atBoundary = exactIdx === 0 || /[\s\-_./]/.test(lowerText[exactIdx - 1] || ''); | |
| 583 | - | totalScore += 100 + (atBoundary ? 50 : 0) + (word.length * 10); | |
| 584 | - | continue; | |
| 585 | - | } | |
| 586 | - | ||
| 587 | - | // Fuzzy: chars in sequence (lower score) | |
| 588 | - | let qi = 0; | |
| 589 | - | let lastMatchIdx = -1; | |
| 590 | - | let consecutiveBonus = 0; | |
| 591 | - | ||
| 592 | - | for (let i = 0; i < lowerText.length && qi < word.length; i++) { | |
| 593 | - | if (lowerText[i] === word[qi]) { | |
| 594 | - | // Bonus for consecutive matches | |
| 595 | - | if (lastMatchIdx === i - 1) consecutiveBonus += 5; | |
| 596 | - | lastMatchIdx = i; | |
| 597 | - | qi++; | |
| 598 | - | } | |
| 599 | - | } | |
| 600 | - | ||
| 601 | - | if (qi === word.length) { | |
| 602 | - | // All chars found in order | |
| 603 | - | totalScore += 30 + consecutiveBonus; | |
| 604 | - | } else { | |
| 605 | - | // This query word not found - no match | |
| 606 | - | return 0; | |
| 607 | - | } | |
| 608 | - | } | |
| 609 | - | ||
| 610 | - | return totalScore; | |
| 611 | - | } | |
| 612 | - | ||
| 613 | - | interface ScoredEntry { | |
| 614 | - | entry: HistoryEntry; | |
| 615 | - | score: number; | |
| 616 | - | } | |
| 617 | - | ||
| 618 | - | /** | |
| 619 | - | * Filter history entries client-side using fuzzy matching. | |
| 620 | - | * Combines title + URL so words can match across both fields. | |
| 621 | - | * E.g. "google search" matches title "Google" + URL containing "search" | |
| 622 | - | */ | |
| 623 | - | function filterHistoryLocal(query: string): HistoryEntry[] { | |
| 624 | - | const q = query.trim(); | |
| 625 | - | if (!q) return []; | |
| 626 | - | ||
| 627 | - | const scored: ScoredEntry[] = []; | |
| 628 | - | ||
| 629 | - | for (const entry of homepageState.history) { | |
| 630 | - | const title = entry.title || ''; | |
| 631 | - | const url = entry.url; | |
| 632 | - | ||
| 633 | - | // Combine title and URL for cross-field word matching | |
| 634 | - | // Title gets priority (appears first), URL stripped of protocol | |
| 635 | - | const urlClean = url.replace(/^https?:\/\//, '').replace(/\/$/, ''); | |
| 636 | - | const combined = `${title} ${urlClean}`; | |
| 637 | - | ||
| 638 | - | const score = fuzzyScore(combined, q); | |
| 639 | - | ||
| 640 | - | // Boost score if title matches directly (more relevant) | |
| 641 | - | const titleBonus = fuzzyScore(title, q) > 0 ? 50 : 0; | |
| 642 | - | ||
| 643 | - | if (score > 0) { | |
| 644 | - | scored.push({ entry, score: score + titleBonus }); | |
| 645 | - | } | |
| 646 | - | } | |
| 647 | - | ||
| 648 | - | // Sort by score descending, limit results | |
| 649 | - | scored.sort((a, b) => b.score - a.score); | |
| 650 | - | return scored.slice(0, 50).map(s => s.entry); | |
| 651 | - | } | |
| 652 | - | ||
| 653 | - | export function handleSearchInput(query: string): void { | |
| 654 | - | homepageState.setHistorySearchQuery(query); | |
| 655 | - | ||
| 656 | - | if (searchDebounceTimer) { | |
| 657 | - | clearTimeout(searchDebounceTimer); | |
| 658 | - | searchDebounceTimer = null; | |
| 659 | - | } | |
| 660 | - | ||
| 661 | - | const trimmed = query.trim(); | |
| 662 | - | if (!trimmed) { | |
| 663 | - | homepageState.setHistorySearchResults([]); | |
| 664 | - | return; | |
| 665 | - | } | |
| 666 | - | ||
| 667 | - | // Tier 1: Instant client-side fuzzy filter on cached history | |
| 668 | - | const localResults = filterHistoryLocal(trimmed); | |
| 669 | - | homepageState.setHistorySearchResults(localResults); | |
| 670 | - | ||
| 671 | - | // Tier 2: If local returns 0 results AND query is long enough, try FTS | |
| 672 | - | // Also try FTS after debounce if query is substantial (deeper search) | |
| 673 | - | if (trimmed.length >= MIN_FTS_QUERY_LENGTH) { | |
| 674 | - | searchDebounceTimer = setTimeout(() => { | |
| 675 | - | // Only call FTS if local results are sparse | |
| 676 | - | if (localResults.length < 5) { | |
| 677 | - | searchHistoryFTS(trimmed); | |
| 678 | - | } | |
| 679 | - | }, SEARCH_DEBOUNCE_MS); | |
| 680 | - | } | |
| 681 | - | } | |
| 682 | - | ||
| 683 | - | // ═══════════════════════════════════════════════════════════════════════════════ | |
| 684 | - | // KEYBOARD HINTS FOR UI | |
| 685 | - | // ═══════════════════════════════════════════════════════════════════════════════ | |
| 686 | - | ||
| 687 | - | export interface KeyboardHint { | |
| 688 | - | keys: string[]; | |
| 689 | - | description: string; | |
| 690 | - | } | |
| 691 | - | ||
| 692 | - | export function getContextualHints(): KeyboardHint[] { | |
| 693 | - | const hints: KeyboardHint[] = []; | |
| 694 | - | ||
| 695 | - | if (homepageState.commandPaletteOpen) { | |
| 696 | - | hints.push( | |
| 697 | - | { keys: ['Ctrl+j', '↓'], description: 'Move down' }, | |
| 698 | - | { keys: ['Ctrl+k', '↑'], description: 'Move up' }, | |
| 699 | - | { keys: ['Enter'], description: 'Execute' }, | |
| 700 | - | { keys: ['Escape'], description: 'Close' } | |
| 701 | - | ); | |
| 702 | - | } else { | |
| 703 | - | hints.push( | |
| 704 | - | { keys: ['j', '↓'], description: 'Move down' }, | |
| 705 | - | { keys: ['k', '↑'], description: 'Move up' }, | |
| 706 | - | { keys: ['Enter', 'o'], description: 'Open' }, | |
| 707 | - | { keys: ['/'], description: 'Search' } | |
| 708 | - | ); | |
| 709 | - | ||
| 710 | - | if (homepageState.activePanel === 'history') { | |
| 711 | - | hints.push( | |
| 712 | - | { keys: ['d', 'x'], description: 'Delete' }, | |
| 713 | - | { keys: ['D h'], description: 'Clear hour' }, | |
| 714 | - | { keys: ['D d'], description: 'Clear day' }, | |
| 715 | - | { keys: ['D D'], description: 'Clear all' } | |
| 716 | - | ); | |
| 717 | - | } | |
| 718 | - | ||
| 719 | - | hints.push( | |
| 720 | - | { keys: ['g h'], description: 'History' }, | |
| 721 | - | { keys: ['g f'], description: 'Favorites' }, | |
| 722 | - | { keys: ['g a'], description: 'Analytics' }, | |
| 723 | - | { keys: ['1-9'], description: 'Quick nav' } | |
| 724 | - | ); | |
| 725 | - | } | |
| 726 | - | ||
| 727 | - | return hints; | |
| 728 | - | } | |
| 729 | - | ||
| 730 | - | // ═══════════════════════════════════════════════════════════════════════════════ | |
| 731 | - | // INITIALIZATION | |
| 732 | - | // ═══════════════════════════════════════════════════════════════════════════════ | |
| 733 | - | ||
| 734 | - | export function initializeKeyboard(): () => void { | |
| 735 | - | document.addEventListener('keydown', handleKeyDown); | |
| 736 | - | ||
| 737 | - | return () => { | |
| 738 | - | document.removeEventListener('keydown', handleKeyDown); | |
| 739 | - | clearPendingPrefix(); | |
| 740 | - | if (searchDebounceTimer) { | |
| 741 | - | clearTimeout(searchDebounceTimer); | |
| 742 | - | } | |
| 743 | - | }; | |
| 744 | - | } | |
D webui/src/pages/homepage/messaging.ts +0 -546
| @@ -1,546 +0,0 @@ | |||
| 1 | - | // ═══════════════════════════════════════════════════════════════════════════════ | |
| 2 | - | // Homepage bridge | |
| 3 | - | // Communication layer between frontend and Go backend via WebKit or CEF bridge. | |
| 4 | - | // ═══════════════════════════════════════════════════════════════════════════════ | |
| 5 | - | ||
| 6 | - | import type { | |
| 7 | - | MessageType, | |
| 8 | - | HistoryEntry, | |
| 9 | - | Folder, | |
| 10 | - | Tag, | |
| 11 | - | Favorite, | |
| 12 | - | DomainStat, | |
| 13 | - | Analytics, | |
| 14 | - | HistoryCleanupRange, | |
| 15 | - | FolderCreateRequest, | |
| 16 | - | FolderUpdateRequest, | |
| 17 | - | TagCreateRequest, | |
| 18 | - | TagUpdateRequest, | |
| 19 | - | } from './types'; | |
| 20 | - | import { homepageState } from './state.svelte'; | |
| 21 | - | ||
| 22 | - | // ═══════════════════════════════════════════════════════════════════════════════ | |
| 23 | - | // REQUEST TRACKING | |
| 24 | - | // ═══════════════════════════════════════════════════════════════════════════════ | |
| 25 | - | ||
| 26 | - | // Pending requests store resolvers for different response types. | |
| 27 | - | // The resolve function accepts `unknown` since requests with different | |
| 28 | - | // response types share the same map. Type safety is maintained at the | |
| 29 | - | // sendMessage<T> API level. | |
| 30 | - | interface PendingRequest { | |
| 31 | - | resolve: (value: unknown) => void; | |
| 32 | - | reject: (error: Error) => void; | |
| 33 | - | timeout: ReturnType<typeof setTimeout>; | |
| 34 | - | } | |
| 35 | - | ||
| 36 | - | const pendingRequests = new Map<string, PendingRequest>(); | |
| 37 | - | const REQUEST_TIMEOUT_MS = 10_000; | |
| 38 | - | ||
| 39 | - | let callbacksInitialized = false; | |
| 40 | - | ||
| 41 | - | // ═══════════════════════════════════════════════════════════════════════════════ | |
| 42 | - | // WEBKIT BRIDGE UTILITIES | |
| 43 | - | // ═══════════════════════════════════════════════════════════════════════════════ | |
| 44 | - | ||
| 45 | - | function getWebKitBridge(): DumberBridge | null { | |
| 46 | - | const bridge = window.webkit?.messageHandlers?.dumber ?? window.dumber; | |
| 47 | - | if (bridge && typeof bridge.postMessage === 'function') { | |
| 48 | - | return bridge; | |
| 49 | - | } | |
| 50 | - | return null; | |
| 51 | - | } | |
| 52 | - | ||
| 53 | - | function getWebViewId(): number { | |
| 54 | - | return window.__dumber_webview_id ?? 0; | |
| 55 | - | } | |
| 56 | - | ||
| 57 | - | function generateRequestId(type: string): string { | |
| 58 | - | return `${type}_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`; | |
| 59 | - | } | |
| 60 | - | ||
| 61 | - | // ═══════════════════════════════════════════════════════════════════════════════ | |
| 62 | - | // CALLBACK INITIALIZATION | |
| 63 | - | // ═══════════════════════════════════════════════════════════════════════════════ | |
| 64 | - | ||
| 65 | - | function initializeCallbacks(): void { | |
| 66 | - | if (callbacksInitialized) return; | |
| 67 | - | ||
| 68 | - | // Generic response handler - accepts a single Response object from Go backend | |
| 69 | - | window.__dumber_homepage_response = (response) => { | |
| 70 | - | const request = pendingRequests.get(response.requestId); | |
| 71 | - | if (!request) return; | |
| 72 | - | ||
| 73 | - | clearTimeout(request.timeout); | |
| 74 | - | pendingRequests.delete(response.requestId); | |
| 75 | - | ||
| 76 | - | if (response.success) { | |
| 77 | - | request.resolve(response.data); | |
| 78 | - | } else { | |
| 79 | - | request.reject(new Error(response.error ?? 'Unknown error')); | |
| 80 | - | } | |
| 81 | - | }; | |
| 82 | - | ||
| 83 | - | // Legacy callbacks for backward compatibility | |
| 84 | - | // These can receive either raw data or a Response object | |
| 85 | - | window.__dumber_history_timeline = (dataOrResponse) => { | |
| 86 | - | handleLegacyOrResponseCallback(dataOrResponse); | |
| 87 | - | }; | |
| 88 | - | ||
| 89 | - | window.__dumber_folders = (dataOrResponse) => { | |
| 90 | - | handleLegacyOrResponseCallback(dataOrResponse); | |
| 91 | - | }; | |
| 92 | - | ||
| 93 | - | window.__dumber_tags = (dataOrResponse) => { | |
| 94 | - | handleLegacyOrResponseCallback(dataOrResponse); | |
| 95 | - | }; | |
| 96 | - | ||
| 97 | - | window.__dumber_favorites = (dataOrResponse) => { | |
| 98 | - | handleLegacyOrResponseCallback(dataOrResponse); | |
| 99 | - | }; | |
| 100 | - | ||
| 101 | - | window.__dumber_analytics = (dataOrResponse) => { | |
| 102 | - | handleLegacyOrResponseCallback(dataOrResponse); | |
| 103 | - | }; | |
| 104 | - | ||
| 105 | - | window.__dumber_domain_stats = (dataOrResponse) => { | |
| 106 | - | handleLegacyOrResponseCallback(dataOrResponse); | |
| 107 | - | }; | |
| 108 | - | ||
| 109 | - | window.__dumber_history_search_results = (dataOrResponse) => { | |
| 110 | - | handleLegacyOrResponseCallback(dataOrResponse); | |
| 111 | - | }; | |
| 112 | - | ||
| 113 | - | window.__dumber_history_deleted = (dataOrResponse) => { | |
| 114 | - | handleLegacyOrResponseCallback(dataOrResponse); | |
| 115 | - | }; | |
| 116 | - | ||
| 117 | - | window.__dumber_history_cleared = (dataOrResponse) => { | |
| 118 | - | handleLegacyOrResponseCallback(dataOrResponse); | |
| 119 | - | }; | |
| 120 | - | ||
| 121 | - | window.__dumber_error = (errorOrResponse) => { | |
| 122 | - | // Handle both formats: direct error string or Response object with error | |
| 123 | - | if (typeof errorOrResponse === 'string') { | |
| 124 | - | // Legacy format: direct error string | |
| 125 | - | console.error('[messaging] Legacy error:', errorOrResponse); | |
| 126 | - | } else if (errorOrResponse && typeof errorOrResponse === 'object') { | |
| 127 | - | // Response object format | |
| 128 | - | const request = pendingRequests.get(errorOrResponse.requestId ?? 'default'); | |
| 129 | - | if (request) { | |
| 130 | - | clearTimeout(request.timeout); | |
| 131 | - | pendingRequests.delete(errorOrResponse.requestId ?? 'default'); | |
| 132 | - | request.reject(new Error(errorOrResponse.error ?? 'Unknown error')); | |
| 133 | - | } | |
| 134 | - | } | |
| 135 | - | }; | |
| 136 | - | ||
| 137 | - | // Listen for favorites changes from omnibox (e.g., when user toggles favorite) | |
| 138 | - | document.addEventListener('dumber:favorites-changed', () => { | |
| 139 | - | console.log('[homepage] Favorites changed event received, refreshing...'); | |
| 140 | - | fetchFavorites(); | |
| 141 | - | }); | |
| 142 | - | ||
| 143 | - | callbacksInitialized = true; | |
| 144 | - | } | |
| 145 | - | ||
| 146 | - | // Handle both legacy raw data and new Response object format | |
| 147 | - | function handleLegacyOrResponseCallback(dataOrResponse: unknown): void { | |
| 148 | - | // Check if it's a Response object (has requestId and success fields) | |
| 149 | - | const response = dataOrResponse as { requestId?: string; success?: boolean; data?: unknown; error?: string }; | |
| 150 | - | if (response && typeof response === 'object' && 'requestId' in response && 'success' in response) { | |
| 151 | - | // New Response object format | |
| 152 | - | const request = pendingRequests.get(response.requestId || 'default'); | |
| 153 | - | if (request) { | |
| 154 | - | clearTimeout(request.timeout); | |
| 155 | - | pendingRequests.delete(response.requestId || 'default'); | |
| 156 | - | if (response.success) { | |
| 157 | - | request.resolve(response.data); | |
| 158 | - | } else { | |
| 159 | - | request.reject(new Error(response.error || 'Unknown error')); | |
| 160 | - | } | |
| 161 | - | } | |
| 162 | - | } else { | |
| 163 | - | // Legacy format: raw data (for backward compatibility) | |
| 164 | - | // Try to find any pending request (not ideal but maintains compatibility) | |
| 165 | - | console.warn('[messaging] Received legacy callback format without requestId'); | |
| 166 | - | } | |
| 167 | - | } | |
| 168 | - | ||
| 169 | - | // ═══════════════════════════════════════════════════════════════════════════════ | |
| 170 | - | // CORE SEND FUNCTION | |
| 171 | - | // ═══════════════════════════════════════════════════════════════════════════════ | |
| 172 | - | ||
| 173 | - | async function sendMessage<T = unknown>( | |
| 174 | - | type: MessageType, | |
| 175 | - | payload?: Record<string, unknown> | |
| 176 | - | ): Promise<T> { | |
| 177 | - | initializeCallbacks(); | |
| 178 | - | ||
| 179 | - | const bridge = getWebKitBridge(); | |
| 180 | - | if (!bridge) { | |
| 181 | - | throw new Error('Dumber bridge not available'); | |
| 182 | - | } | |
| 183 | - | ||
| 184 | - | const requestId = generateRequestId(type); | |
| 185 | - | ||
| 186 | - | return new Promise<T>((resolve, reject) => { | |
| 187 | - | const timeout = setTimeout(() => { | |
| 188 | - | pendingRequests.delete(requestId); | |
| 189 | - | reject(new Error(`Request timeout: ${type}`)); | |
| 190 | - | }, REQUEST_TIMEOUT_MS); | |
| 191 | - | ||
| 192 | - | // Cast resolve to accept unknown - type safety is maintained at API level | |
| 193 | - | // since sendMessage<T> returns Promise<T> and callers expect typed responses | |
| 194 | - | pendingRequests.set(requestId, { | |
| 195 | - | resolve: resolve as (value: unknown) => void, | |
| 196 | - | reject, | |
| 197 | - | timeout, | |
| 198 | - | }); | |
| 199 | - | ||
| 200 | - | try { | |
| 201 | - | bridge.postMessage({ | |
| 202 | - | type, | |
| 203 | - | webviewId: getWebViewId(), | |
| 204 | - | payload: { requestId, ...payload }, | |
| 205 | - | }); | |
| 206 | - | } catch (error) { | |
| 207 | - | clearTimeout(timeout); | |
| 208 | - | pendingRequests.delete(requestId); | |
| 209 | - | reject(error); | |
| 210 | - | } | |
| 211 | - | }); | |
| 212 | - | } | |
| 213 | - | ||
| 214 | - | // ═══════════════════════════════════════════════════════════════════════════════ | |
| 215 | - | // HISTORY API | |
| 216 | - | // ═══════════════════════════════════════════════════════════════════════════════ | |
| 217 | - | ||
| 218 | - | export async function fetchHistoryTimeline( | |
| 219 | - | limit: number = 50, | |
| 220 | - | offset: number = 0 | |
| 221 | - | ): Promise<HistoryEntry[]> { | |
| 222 | - | homepageState.setHistoryLoading(true); | |
| 223 | - | try { | |
| 224 | - | const data = await sendMessage<HistoryEntry[]>('history_timeline', { | |
| 225 | - | limit, | |
| 226 | - | offset, | |
| 227 | - | }); | |
| 228 | - | const entries = Array.isArray(data) ? data : []; | |
| 229 | - | ||
| 230 | - | if (offset === 0) { | |
| 231 | - | homepageState.setHistory(entries); | |
| 232 | - | } else { | |
| 233 | - | homepageState.appendHistory(entries); | |
| 234 | - | } | |
| 235 | - | ||
| 236 | - | homepageState.setHistoryOffset(offset + entries.length); | |
| 237 | - | homepageState.setHasMoreHistory(entries.length === limit); | |
| 238 | - | ||
| 239 | - | return entries; | |
| 240 | - | } catch (error) { | |
| 241 | - | console.error('[messaging] fetchHistoryTimeline error:', error); | |
| 242 | - | if (offset === 0) { | |
| 243 | - | homepageState.setHistory([]); | |
| 244 | - | } | |
| 245 | - | homepageState.setHasMoreHistory(false); | |
| 246 | - | return []; | |
| 247 | - | } finally { | |
| 248 | - | homepageState.setHistoryLoading(false); | |
| 249 | - | } | |
| 250 | - | } | |
| 251 | - | ||
| 252 | - | export async function searchHistoryFTS(query: string): Promise<HistoryEntry[]> { | |
| 253 | - | if (!query.trim()) { | |
| 254 | - | homepageState.setHistorySearchResults([]); | |
| 255 | - | return []; | |
| 256 | - | } | |
| 257 | - | ||
| 258 | - | homepageState.setHistorySearching(true); | |
| 259 | - | try { | |
| 260 | - | const data = await sendMessage<HistoryEntry[]>('history_search_fts', { | |
| 261 | - | query, | |
| 262 | - | limit: 100, | |
| 263 | - | }); | |
| 264 | - | const results = Array.isArray(data) ? data : []; | |
| 265 | - | homepageState.setHistorySearchResults(results); | |
| 266 | - | return results; | |
| 267 | - | } catch (error) { | |
| 268 | - | console.error('[messaging] searchHistoryFTS error:', error); | |
| 269 | - | homepageState.setHistorySearchResults([]); | |
| 270 | - | return []; | |
| 271 | - | } finally { | |
| 272 | - | homepageState.setHistorySearching(false); | |
| 273 | - | } | |
| 274 | - | } | |
| 275 | - | ||
| 276 | - | export async function deleteHistoryEntry(id: number): Promise<void> { | |
| 277 | - | try { | |
| 278 | - | await sendMessage('history_delete_entry', { id }); | |
| 279 | - | homepageState.deleteHistoryEntry(id); | |
| 280 | - | } catch (error) { | |
| 281 | - | console.error('[messaging] deleteHistoryEntry error:', error); | |
| 282 | - | throw error; | |
| 283 | - | } | |
| 284 | - | } | |
| 285 | - | ||
| 286 | - | export async function deleteHistoryByRange(range: HistoryCleanupRange): Promise<void> { | |
| 287 | - | try { | |
| 288 | - | await sendMessage('history_delete_range', { range }); | |
| 289 | - | // Refresh history after deletion | |
| 290 | - | await fetchHistoryTimeline(); | |
| 291 | - | } catch (error) { | |
| 292 | - | console.error('[messaging] deleteHistoryByRange error:', error); | |
| 293 | - | throw error; | |
| 294 | - | } | |
| 295 | - | } | |
| 296 | - | ||
| 297 | - | export async function clearAllHistory(): Promise<void> { | |
| 298 | - | try { | |
| 299 | - | await sendMessage('history_clear_all'); | |
| 300 | - | homepageState.clearHistory(); | |
| 301 | - | } catch (error) { | |
| 302 | - | console.error('[messaging] clearAllHistory error:', error); | |
| 303 | - | throw error; | |
| 304 | - | } | |
| 305 | - | } | |
| 306 | - | ||
| 307 | - | export async function deleteHistoryByDomain(domain: string): Promise<void> { | |
| 308 | - | try { | |
| 309 | - | await sendMessage('history_delete_domain', { domain }); | |
| 310 | - | // Refresh history after deletion | |
| 311 | - | await fetchHistoryTimeline(); | |
| 312 | - | } catch (error) { | |
| 313 | - | console.error('[messaging] deleteHistoryByDomain error:', error); | |
| 314 | - | throw error; | |
| 315 | - | } | |
| 316 | - | } | |
| 317 | - | ||
| 318 | - | // ═══════════════════════════════════════════════════════════════════════════════ | |
| 319 | - | // FOLDERS API | |
| 320 | - | // ═══════════════════════════════════════════════════════════════════════════════ | |
| 321 | - | ||
| 322 | - | export async function fetchFolders(): Promise<Folder[]> { | |
| 323 | - | try { | |
| 324 | - | const data = await sendMessage<Folder[]>('folder_list'); | |
| 325 | - | const folders = Array.isArray(data) ? data : []; | |
| 326 | - | homepageState.setFolders(folders); | |
| 327 | - | return folders; | |
| 328 | - | } catch (error) { | |
| 329 | - | console.error('[messaging] fetchFolders error:', error); | |
| 330 | - | homepageState.setFolders([]); | |
| 331 | - | return []; | |
| 332 | - | } | |
| 333 | - | } | |
| 334 | - | ||
| 335 | - | export async function createFolder(req: FolderCreateRequest): Promise<Folder> { | |
| 336 | - | const folder = await sendMessage<Folder>('folder_create', { ...req }); | |
| 337 | - | homepageState.addFolder(folder); | |
| 338 | - | return folder; | |
| 339 | - | } | |
| 340 | - | ||
| 341 | - | export async function updateFolder(req: FolderUpdateRequest): Promise<void> { | |
| 342 | - | await sendMessage('folder_update', { ...req }); | |
| 343 | - | homepageState.updateFolder({ | |
| 344 | - | ...homepageState.folders.find(f => f.id === req.id)!, | |
| 345 | - | name: req.name, | |
| 346 | - | icon: req.icon ?? null, | |
| 347 | - | }); | |
| 348 | - | } | |
| 349 | - | ||
| 350 | - | export async function deleteFolder(id: number): Promise<void> { | |
| 351 | - | await sendMessage('folder_delete', { id }); | |
| 352 | - | homepageState.deleteFolder(id); | |
| 353 | - | } | |
| 354 | - | ||
| 355 | - | // ═══════════════════════════════════════════════════════════════════════════════ | |
| 356 | - | // TAGS API | |
| 357 | - | // ═══════════════════════════════════════════════════════════════════════════════ | |
| 358 | - | ||
| 359 | - | export async function fetchTags(): Promise<Tag[]> { | |
| 360 | - | try { | |
| 361 | - | const data = await sendMessage<Tag[]>('tag_list'); | |
| 362 | - | const tags = Array.isArray(data) ? data : []; | |
| 363 | - | homepageState.setTags(tags); | |
| 364 | - | return tags; | |
| 365 | - | } catch (error) { | |
| 366 | - | console.error('[messaging] fetchTags error:', error); | |
| 367 | - | homepageState.setTags([]); | |
| 368 | - | return []; | |
| 369 | - | } | |
| 370 | - | } | |
| 371 | - | ||
| 372 | - | export async function createTag(req: TagCreateRequest): Promise<Tag> { | |
| 373 | - | const tag = await sendMessage<Tag>('tag_create', { ...req }); | |
| 374 | - | homepageState.addTag(tag); | |
| 375 | - | return tag; | |
| 376 | - | } | |
| 377 | - | ||
| 378 | - | export async function updateTag(req: TagUpdateRequest): Promise<void> { | |
| 379 | - | await sendMessage('tag_update', { ...req }); | |
| 380 | - | const existing = homepageState.tags.find(t => t.id === req.id); | |
| 381 | - | if (existing) { | |
| 382 | - | homepageState.updateTag({ | |
| 383 | - | ...existing, | |
| 384 | - | name: req.name ?? existing.name, | |
| 385 | - | color: req.color ?? existing.color, | |
| 386 | - | }); | |
| 387 | - | } | |
| 388 | - | } | |
| 389 | - | ||
| 390 | - | export async function deleteTag(id: number): Promise<void> { | |
| 391 | - | await sendMessage('tag_delete', { id }); | |
| 392 | - | homepageState.deleteTag(id); | |
| 393 | - | } | |
| 394 | - | ||
| 395 | - | export async function assignTag(favoriteId: number, tagId: number): Promise<void> { | |
| 396 | - | await sendMessage('tag_assign', { favorite_id: favoriteId, tag_id: tagId }); | |
| 397 | - | // Update local state | |
| 398 | - | const favorite = homepageState.favorites.find(f => f.id === favoriteId); | |
| 399 | - | const tag = homepageState.tags.find(t => t.id === tagId); | |
| 400 | - | if (favorite && tag) { | |
| 401 | - | homepageState.updateFavorite({ | |
| 402 | - | ...favorite, | |
| 403 | - | tags: [...(favorite.tags || []), tag], | |
| 404 | - | }); | |
| 405 | - | } | |
| 406 | - | } | |
| 407 | - | ||
| 408 | - | export async function removeTag(favoriteId: number, tagId: number): Promise<void> { | |
| 409 | - | await sendMessage('tag_remove', { favorite_id: favoriteId, tag_id: tagId }); | |
| 410 | - | // Update local state | |
| 411 | - | const favorite = homepageState.favorites.find(f => f.id === favoriteId); | |
| 412 | - | if (favorite) { | |
| 413 | - | homepageState.updateFavorite({ | |
| 414 | - | ...favorite, | |
| 415 | - | tags: favorite.tags?.filter(t => t.id !== tagId), | |
| 416 | - | }); | |
| 417 | - | } | |
| 418 | - | } | |
| 419 | - | ||
| 420 | - | // ═══════════════════════════════════════════════════════════════════════════════ | |
| 421 | - | // FAVORITES API | |
| 422 | - | // ═══════════════════════════════════════════════════════════════════════════════ | |
| 423 | - | ||
| 424 | - | export async function fetchFavorites(): Promise<Favorite[]> { | |
| 425 | - | homepageState.setFavoritesLoading(true); | |
| 426 | - | try { | |
| 427 | - | const data = await sendMessage<Favorite[]>('favorite_list'); | |
| 428 | - | const favorites = Array.isArray(data) ? data : []; | |
| 429 | - | homepageState.setFavorites(favorites); | |
| 430 | - | return favorites; | |
| 431 | - | } catch (error) { | |
| 432 | - | console.error('[messaging] fetchFavorites error:', error); | |
| 433 | - | homepageState.setFavorites([]); | |
| 434 | - | return []; | |
| 435 | - | } finally { | |
| 436 | - | homepageState.setFavoritesLoading(false); | |
| 437 | - | } | |
| 438 | - | } | |
| 439 | - | ||
| 440 | - | export async function setFavoriteShortcut( | |
| 441 | - | favoriteId: number, | |
| 442 | - | shortcutKey: number | null | |
| 443 | - | ): Promise<void> { | |
| 444 | - | await sendMessage('favorite_set_shortcut', { | |
| 445 | - | favorite_id: favoriteId, | |
| 446 | - | shortcut_key: shortcutKey, | |
| 447 | - | }); | |
| 448 | - | // Update local state | |
| 449 | - | const favorite = homepageState.favorites.find(f => f.id === favoriteId); | |
| 450 | - | if (favorite) { | |
| 451 | - | // Clear shortcut from any other favorite that had this key | |
| 452 | - | if (shortcutKey !== null) { | |
| 453 | - | for (const f of homepageState.favorites) { | |
| 454 | - | if (f.shortcut_key === shortcutKey && f.id !== favoriteId) { | |
| 455 | - | homepageState.updateFavorite({ ...f, shortcut_key: null }); | |
| 456 | - | } | |
| 457 | - | } | |
| 458 | - | } | |
| 459 | - | homepageState.updateFavorite({ ...favorite, shortcut_key: shortcutKey }); | |
| 460 | - | } | |
| 461 | - | } | |
| 462 | - | ||
| 463 | - | export async function getFavoriteByShortcut(shortcutKey: number): Promise<Favorite | null> { | |
| 464 | - | try { | |
| 465 | - | const data = await sendMessage<Favorite | null>('favorite_get_by_shortcut', { | |
| 466 | - | shortcut_key: shortcutKey, | |
| 467 | - | }); | |
| 468 | - | return data; | |
| 469 | - | } catch (error) { | |
| 470 | - | console.error('[messaging] getFavoriteByShortcut error:', error); | |
| 471 | - | return null; | |
| 472 | - | } | |
| 473 | - | } | |
| 474 | - | ||
| 475 | - | export async function setFavoriteFolder( | |
| 476 | - | favoriteId: number, | |
| 477 | - | folderId: number | null | |
| 478 | - | ): Promise<void> { | |
| 479 | - | await sendMessage('favorite_set_folder', { | |
| 480 | - | favorite_id: favoriteId, | |
| 481 | - | folder_id: folderId, | |
| 482 | - | }); | |
| 483 | - | // Update local state | |
| 484 | - | const favorite = homepageState.favorites.find(f => f.id === favoriteId); | |
| 485 | - | if (favorite) { | |
| 486 | - | homepageState.updateFavorite({ ...favorite, folder_id: folderId }); | |
| 487 | - | } | |
| 488 | - | } | |
| 489 | - | ||
| 490 | - | // ═══════════════════════════════════════════════════════════════════════════════ | |
| 491 | - | // ANALYTICS API | |
| 492 | - | // ═══════════════════════════════════════════════════════════════════════════════ | |
| 493 | - | ||
| 494 | - | export async function fetchAnalytics(): Promise<Analytics | null> { | |
| 495 | - | homepageState.setAnalyticsLoading(true); | |
| 496 | - | try { | |
| 497 | - | const data = await sendMessage<Analytics>('history_analytics'); | |
| 498 | - | homepageState.setAnalytics(data); | |
| 499 | - | return data; | |
| 500 | - | } catch (error) { | |
| 501 | - | console.error('[messaging] fetchAnalytics error:', error); | |
| 502 | - | homepageState.setAnalytics(null); | |
| 503 | - | return null; | |
| 504 | - | } finally { | |
| 505 | - | homepageState.setAnalyticsLoading(false); | |
| 506 | - | } | |
| 507 | - | } | |
| 508 | - | ||
| 509 | - | export async function fetchDomainStats(limit: number = 20): Promise<DomainStat[]> { | |
| 510 | - | try { | |
| 511 | - | const data = await sendMessage<DomainStat[]>('history_domain_stats', { limit }); | |
| 512 | - | const stats = Array.isArray(data) ? data : []; | |
| 513 | - | homepageState.setDomainStats(stats); | |
| 514 | - | return stats; | |
| 515 | - | } catch (error) { | |
| 516 | - | console.error('[messaging] fetchDomainStats error:', error); | |
| 517 | - | homepageState.setDomainStats([]); | |
| 518 | - | return []; | |
| 519 | - | } | |
| 520 | - | } | |
| 521 | - | ||
| 522 | - | // ═══════════════════════════════════════════════════════════════════════════════ | |
| 523 | - | // INITIALIZATION | |
| 524 | - | // ═══════════════════════════════════════════════════════════════════════════════ | |
| 525 | - | ||
| 526 | - | export async function initializeHomepage(): Promise<void> { | |
| 527 | - | // Initialize callbacks first | |
| 528 | - | initializeCallbacks(); | |
| 529 | - | ||
| 530 | - | // Fetch all initial data in parallel | |
| 531 | - | await Promise.all([ | |
| 532 | - | fetchHistoryTimeline(), | |
| 533 | - | fetchFolders(), | |
| 534 | - | fetchTags(), | |
| 535 | - | fetchFavorites(), | |
| 536 | - | fetchAnalytics(), | |
| 537 | - | ]); | |
| 538 | - | } | |
| 539 | - | ||
| 540 | - | // ═══════════════════════════════════════════════════════════════════════════════ | |
| 541 | - | // NAVIGATION | |
| 542 | - | // ═══════════════════════════════════════════════════════════════════════════════ | |
| 543 | - | ||
| 544 | - | export function navigateTo(url: string): void { | |
| 545 | - | window.location.href = url; | |
| 546 | - | } | |
D webui/src/pages/homepage/panels/AnalyticsPanel.svelte +0 -246
| @@ -1,246 +0,0 @@ | |||
| 1 | - | <script lang="ts"> | |
| 2 | - | import { homepageState } from '../state.svelte'; | |
| 3 | - | import { fetchAnalytics } from '../messaging'; | |
| 4 | - | import { DailyVisitsChart, HourlyActivityChart } from '../analytics'; | |
| 5 | - | ||
| 6 | - | // Fetch analytics on mount | |
| 7 | - | $effect(() => { | |
| 8 | - | if (!homepageState.analytics) { | |
| 9 | - | fetchAnalytics(); | |
| 10 | - | } | |
| 11 | - | }); | |
| 12 | - | ||
| 13 | - | const formatNumber = (n: number): string => { | |
| 14 | - | if (n >= 1000) { | |
| 15 | - | return `${(n / 1000).toFixed(1)}k`; | |
| 16 | - | } | |
| 17 | - | return n.toString(); | |
| 18 | - | }; | |
| 19 | - | </script> | |
| 20 | - | ||
| 21 | - | <div class="analytics-panel"> | |
| 22 | - | {#if homepageState.analyticsLoading} | |
| 23 | - | <div class="loading-state"> | |
| 24 | - | <span class="loading-spinner"></span> | |
| 25 | - | <span class="loading-text">LOADING ANALYTICS...</span> | |
| 26 | - | </div> | |
| 27 | - | {:else if homepageState.analytics} | |
| 28 | - | <div class="stats-grid"> | |
| 29 | - | <div class="stat-card"> | |
| 30 | - | <span class="stat-value">{formatNumber(homepageState.analytics.total_entries)}</span> | |
| 31 | - | <span class="stat-label">TOTAL ENTRIES</span> | |
| 32 | - | </div> | |
| 33 | - | <div class="stat-card"> | |
| 34 | - | <span class="stat-value">{formatNumber(homepageState.analytics.total_visits)}</span> | |
| 35 | - | <span class="stat-label">TOTAL VISITS</span> | |
| 36 | - | </div> | |
| 37 | - | <div class="stat-card"> | |
| 38 | - | <span class="stat-value">{homepageState.analytics.unique_days}</span> | |
| 39 | - | <span class="stat-label">DAYS ACTIVE</span> | |
| 40 | - | </div> | |
| 41 | - | <div class="stat-card"> | |
| 42 | - | <span class="stat-value">{homepageState.domainStats.length}</span> | |
| 43 | - | <span class="stat-label">UNIQUE DOMAINS</span> | |
| 44 | - | </div> | |
| 45 | - | </div> | |
| 46 | - | ||
| 47 | - | <div class="section"> | |
| 48 | - | <div class="section-header"> | |
| 49 | - | <span class="section-title">TOP DOMAINS</span> | |
| 50 | - | </div> | |
| 51 | - | <div class="domain-list"> | |
| 52 | - | {#each homepageState.domainStats.slice(0, 10) as domain, i (domain.domain)} | |
| 53 | - | <div class="domain-item"> | |
| 54 | - | <span class="domain-rank">{i + 1}</span> | |
| 55 | - | <span class="domain-name">{domain.domain}</span> | |
| 56 | - | <span class="domain-stats"> | |
| 57 | - | <span class="domain-pages">{domain.page_count} pages</span> | |
| 58 | - | <span class="domain-visits">{domain.total_visits} visits</span> | |
| 59 | - | </span> | |
| 60 | - | </div> | |
| 61 | - | {/each} | |
| 62 | - | </div> | |
| 63 | - | </div> | |
| 64 | - | ||
| 65 | - | <!-- Daily Visits Chart (30 days) --> | |
| 66 | - | {#if homepageState.dailyVisits.length > 0} | |
| 67 | - | <div class="section"> | |
| 68 | - | <div class="section-header"> | |
| 69 | - | <span class="section-title">VISITS (LAST 30 DAYS)</span> | |
| 70 | - | </div> | |
| 71 | - | <div class="chart-container"> | |
| 72 | - | <DailyVisitsChart dailyVisits={homepageState.dailyVisits} /> | |
| 73 | - | </div> | |
| 74 | - | </div> | |
| 75 | - | {/if} | |
| 76 | - | ||
| 77 | - | <!-- Hourly Activity Chart --> | |
| 78 | - | {#if homepageState.analytics.hourly_distribution.length > 0} | |
| 79 | - | <div class="section"> | |
| 80 | - | <div class="section-header"> | |
| 81 | - | <span class="section-title">ACTIVITY BY HOUR</span> | |
| 82 | - | </div> | |
| 83 | - | <div class="chart-container"> | |
| 84 | - | <HourlyActivityChart hourlyDistribution={homepageState.analytics.hourly_distribution} /> | |
| 85 | - | </div> | |
| 86 | - | </div> | |
| 87 | - | {/if} | |
| 88 | - | {:else} | |
| 89 | - | <div class="empty-state"> | |
| 90 | - | <span class="empty-icon"></span> | |
| 91 | - | <span class="empty-text">NO ANALYTICS DATA</span> | |
| 92 | - | <span class="empty-hint">Browse some sites to generate analytics</span> | |
| 93 | - | </div> | |
| 94 | - | {/if} | |
| 95 | - | </div> | |
| 96 | - | ||
| 97 | - | <style> | |
| 98 | - | .analytics-panel { | |
| 99 | - | display: flex; | |
| 100 | - | flex-direction: column; | |
| 101 | - | gap: 1rem; | |
| 102 | - | padding: 0.5rem 1rem; | |
| 103 | - | } | |
| 104 | - | ||
| 105 | - | .loading-state, | |
| 106 | - | .empty-state { | |
| 107 | - | flex: 1; | |
| 108 | - | display: flex; | |
| 109 | - | flex-direction: column; | |
| 110 | - | align-items: center; | |
| 111 | - | justify-content: center; | |
| 112 | - | gap: 0.5rem; | |
| 113 | - | padding: 2rem; | |
| 114 | - | color: var(--muted-foreground); | |
| 115 | - | text-align: center; | |
| 116 | - | } | |
| 117 | - | ||
| 118 | - | .loading-spinner { | |
| 119 | - | width: 24px; | |
| 120 | - | height: 24px; | |
| 121 | - | border-width: 2px; | |
| 122 | - | border-style: solid; | |
| 123 | - | border-color: var(--border); | |
| 124 | - | border-top-color: var(--primary, #4ade80); | |
| 125 | - | border-radius: 50%; | |
| 126 | - | animation: spin 0.8s linear infinite; | |
| 127 | - | } | |
| 128 | - | ||
| 129 | - | @keyframes spin { | |
| 130 | - | to { transform: rotate(360deg); } | |
| 131 | - | } | |
| 132 | - | ||
| 133 | - | .loading-text, | |
| 134 | - | .empty-text { | |
| 135 | - | font-size: 0.68rem; | |
| 136 | - | font-weight: 600; | |
| 137 | - | letter-spacing: 0.12em; | |
| 138 | - | } | |
| 139 | - | ||
| 140 | - | .empty-icon { | |
| 141 | - | font-size: 1.5rem; | |
| 142 | - | opacity: 0.5; | |
| 143 | - | } | |
| 144 | - | ||
| 145 | - | .empty-hint { | |
| 146 | - | font-size: 0.65rem; | |
| 147 | - | letter-spacing: 0.06em; | |
| 148 | - | opacity: 0.7; | |
| 149 | - | } | |
| 150 | - | ||
| 151 | - | .stats-grid { | |
| 152 | - | display: grid; | |
| 153 | - | grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); | |
| 154 | - | gap: 0.5rem; | |
| 155 | - | } | |
| 156 | - | ||
| 157 | - | .stat-card { | |
| 158 | - | display: flex; | |
| 159 | - | flex-direction: column; | |
| 160 | - | gap: 0.3rem; | |
| 161 | - | padding: 0.85rem; | |
| 162 | - | border-width: 1px; | |
| 163 | - | border-style: solid; | |
| 164 | - | border-color: var(--border); | |
| 165 | - | background: color-mix(in srgb, var(--background) 92%, var(--card) 8%); | |
| 166 | - | } | |
| 167 | - | ||
| 168 | - | .stat-value { | |
| 169 | - | font-size: 1.5rem; | |
| 170 | - | font-weight: 700; | |
| 171 | - | color: var(--foreground); | |
| 172 | - | letter-spacing: -0.02em; | |
| 173 | - | } | |
| 174 | - | ||
| 175 | - | .stat-label { | |
| 176 | - | font-size: 0.6rem; | |
| 177 | - | font-weight: 600; | |
| 178 | - | color: var(--muted-foreground); | |
| 179 | - | letter-spacing: 0.12em; | |
| 180 | - | } | |
| 181 | - | ||
| 182 | - | .section { | |
| 183 | - | border-width: 1px; | |
| 184 | - | border-style: solid; | |
| 185 | - | border-color: var(--border); | |
| 186 | - | background: color-mix(in srgb, var(--background) 92%, var(--card) 8%); | |
| 187 | - | } | |
| 188 | - | ||
| 189 | - | .section-header { | |
| 190 | - | padding: 0.5rem 0.75rem; | |
| 191 | - | border-bottom: 1px solid var(--border); | |
| 192 | - | background: color-mix(in srgb, var(--background) 80%, var(--card) 20%); | |
| 193 | - | } | |
| 194 | - | ||
| 195 | - | .section-title { | |
| 196 | - | font-size: 0.6rem; | |
| 197 | - | font-weight: 600; | |
| 198 | - | color: var(--muted-foreground); | |
| 199 | - | letter-spacing: 0.12em; | |
| 200 | - | } | |
| 201 | - | ||
| 202 | - | .domain-list { | |
| 203 | - | display: flex; | |
| 204 | - | flex-direction: column; | |
| 205 | - | } | |
| 206 | - | ||
| 207 | - | .domain-item { | |
| 208 | - | display: grid; | |
| 209 | - | grid-template-columns: auto 1fr auto; | |
| 210 | - | align-items: center; | |
| 211 | - | gap: 0.75rem; | |
| 212 | - | padding: 0.5rem 0.75rem; | |
| 213 | - | border-bottom: 1px solid color-mix(in srgb, var(--border) 40%, transparent); | |
| 214 | - | } | |
| 215 | - | ||
| 216 | - | .domain-item:last-child { | |
| 217 | - | border-bottom: none; | |
| 218 | - | } | |
| 219 | - | ||
| 220 | - | .domain-rank { | |
| 221 | - | width: 1.5rem; | |
| 222 | - | font-size: 0.65rem; | |
| 223 | - | font-weight: 600; | |
| 224 | - | color: var(--muted-foreground); | |
| 225 | - | text-align: center; | |
| 226 | - | } | |
| 227 | - | ||
| 228 | - | .domain-name { | |
| 229 | - | font-size: 0.75rem; | |
| 230 | - | color: var(--foreground); | |
| 231 | - | white-space: nowrap; | |
| 232 | - | overflow: hidden; | |
| 233 | - | text-overflow: ellipsis; | |
| 234 | - | } | |
| 235 | - | ||
| 236 | - | .domain-stats { | |
| 237 | - | display: flex; | |
| 238 | - | gap: 0.75rem; | |
| 239 | - | font-size: 0.6rem; | |
| 240 | - | color: var(--muted-foreground); | |
| 241 | - | } | |
| 242 | - | ||
| 243 | - | .chart-container { | |
| 244 | - | padding: 0.5rem; | |
| 245 | - | } | |
| 246 | - | </style> | |
D webui/src/pages/homepage/panels/FavoritesPanel.svelte +0 -287
| @@ -1,287 +0,0 @@ | |||
| 1 | - | <script lang="ts"> | |
| 2 | - | import { homepageState } from '../state.svelte'; | |
| 3 | - | import { | |
| 4 | - | navigateTo, | |
| 5 | - | setFavoriteShortcut, | |
| 6 | - | setFavoriteFolder, | |
| 7 | - | assignTag, | |
| 8 | - | removeTag, | |
| 9 | - | createFolder, | |
| 10 | - | createTag | |
| 11 | - | } from '../messaging'; | |
| 12 | - | import type { Favorite } from '../types'; | |
| 13 | - | ||
| 14 | - | import { FolderList, TagCloud, FavoriteGrid, FavoriteEditor } from '../favorites'; | |
| 15 | - | ||
| 16 | - | let editorOpen = $state(false); | |
| 17 | - | let editingFavorite = $state<Favorite | null>(null); | |
| 18 | - | ||
| 19 | - | // Get filtered favorites based on folder and tag selection | |
| 20 | - | const displayedFavorites = $derived(() => { | |
| 21 | - | let result = homepageState.favorites; | |
| 22 | - | ||
| 23 | - | // Filter by folder | |
| 24 | - | const folderId = homepageState.selectedFolderId; | |
| 25 | - | if (folderId !== null) { | |
| 26 | - | if (folderId === -1) { | |
| 27 | - | // Show unfiled | |
| 28 | - | result = result.filter(f => f.folder_id === null); | |
| 29 | - | } else { | |
| 30 | - | result = result.filter(f => f.folder_id === folderId); | |
| 31 | - | } | |
| 32 | - | } | |
| 33 | - | ||
| 34 | - | // Filter by tags | |
| 35 | - | const tagIds = homepageState.selectedTagIds; | |
| 36 | - | if (tagIds.length > 0) { | |
| 37 | - | result = result.filter(f => | |
| 38 | - | f.tags?.some(t => tagIds.includes(t.id)) | |
| 39 | - | ); | |
| 40 | - | } | |
| 41 | - | ||
| 42 | - | return result; | |
| 43 | - | }); | |
| 44 | - | ||
| 45 | - | const handleSelectFavorite = (fav: Favorite) => { | |
| 46 | - | navigateTo(fav.url); | |
| 47 | - | }; | |
| 48 | - | ||
| 49 | - | const handleEditFavorite = (fav: Favorite) => { | |
| 50 | - | editingFavorite = fav; | |
| 51 | - | editorOpen = true; | |
| 52 | - | }; | |
| 53 | - | ||
| 54 | - | const handleSaveFavorite = async (updates: Partial<Favorite>) => { | |
| 55 | - | if (!updates.id) return; | |
| 56 | - | ||
| 57 | - | // Update shortcut key | |
| 58 | - | if (updates.shortcut_key !== undefined) { | |
| 59 | - | await setFavoriteShortcut(updates.id, updates.shortcut_key); | |
| 60 | - | } | |
| 61 | - | ||
| 62 | - | // Update folder | |
| 63 | - | if (updates.folder_id !== undefined) { | |
| 64 | - | await setFavoriteFolder(updates.id, updates.folder_id); | |
| 65 | - | } | |
| 66 | - | ||
| 67 | - | // Title update would need a separate handler - for now just update local state | |
| 68 | - | if (updates.title !== undefined && editingFavorite) { | |
| 69 | - | homepageState.updateFavorite({ | |
| 70 | - | ...editingFavorite, | |
| 71 | - | ...updates | |
| 72 | - | }); | |
| 73 | - | } | |
| 74 | - | }; | |
| 75 | - | ||
| 76 | - | const handleDeleteFavorite = (id: number) => { | |
| 77 | - | // This would call a delete API - for now just update local state | |
| 78 | - | homepageState.deleteFavorite(id); | |
| 79 | - | }; | |
| 80 | - | ||
| 81 | - | const handleAssignTag = async (favoriteId: number, tagId: number) => { | |
| 82 | - | await assignTag(favoriteId, tagId); | |
| 83 | - | }; | |
| 84 | - | ||
| 85 | - | const handleRemoveTag = async (favoriteId: number, tagId: number) => { | |
| 86 | - | await removeTag(favoriteId, tagId); | |
| 87 | - | }; | |
| 88 | - | ||
| 89 | - | const handleSelectFolder = (id: number | null) => { | |
| 90 | - | homepageState.selectFolder(id); | |
| 91 | - | }; | |
| 92 | - | ||
| 93 | - | const handleToggleTag = (id: number) => { | |
| 94 | - | homepageState.toggleTag(id); | |
| 95 | - | }; | |
| 96 | - | ||
| 97 | - | const handleClearTagSelection = () => { | |
| 98 | - | homepageState.clearTagSelection(); | |
| 99 | - | }; | |
| 100 | - | ||
| 101 | - | const handleCreateFolder = async (name: string) => { | |
| 102 | - | await createFolder({ name }); | |
| 103 | - | }; | |
| 104 | - | ||
| 105 | - | const handleCreateTag = async (name: string, color: string) => { | |
| 106 | - | await createTag({ name, color }); | |
| 107 | - | }; | |
| 108 | - | ||
| 109 | - | const closeEditor = () => { | |
| 110 | - | editorOpen = false; | |
| 111 | - | editingFavorite = null; | |
| 112 | - | }; | |
| 113 | - | </script> | |
| 114 | - | ||
| 115 | - | <div class="favorites-panel"> | |
| 116 | - | <div class="panel-sidebar"> | |
| 117 | - | <FolderList | |
| 118 | - | folders={homepageState.folders} | |
| 119 | - | favorites={homepageState.favorites} | |
| 120 | - | selectedFolderId={homepageState.selectedFolderId} | |
| 121 | - | onSelectFolder={handleSelectFolder} | |
| 122 | - | onCreateFolder={handleCreateFolder} | |
| 123 | - | /> | |
| 124 | - | ||
| 125 | - | <TagCloud | |
| 126 | - | tags={homepageState.tags} | |
| 127 | - | selectedTagIds={homepageState.selectedTagIds} | |
| 128 | - | onToggleTag={handleToggleTag} | |
| 129 | - | onClearSelection={handleClearTagSelection} | |
| 130 | - | onCreateTag={handleCreateTag} | |
| 131 | - | /> | |
| 132 | - | </div> | |
| 133 | - | ||
| 134 | - | <div class="panel-main"> | |
| 135 | - | <div class="panel-header"> | |
| 136 | - | <span class="header-title"> | |
| 137 | - | {#if homepageState.selectedFolderId === null} | |
| 138 | - | ALL FAVORITES | |
| 139 | - | {:else if homepageState.selectedFolderId === -1} | |
| 140 | - | UNFILED | |
| 141 | - | {:else} | |
| 142 | - | {homepageState.folders.find(f => f.id === homepageState.selectedFolderId)?.name ?? 'FOLDER'} | |
| 143 | - | {/if} | |
| 144 | - | </span> | |
| 145 | - | <span class="header-count">{displayedFavorites().length}</span> | |
| 146 | - | </div> | |
| 147 | - | ||
| 148 | - | <div class="panel-content"> | |
| 149 | - | {#if homepageState.favoritesLoading} | |
| 150 | - | <div class="loading-state"> | |
| 151 | - | <span class="loading-spinner"></span> | |
| 152 | - | <span class="loading-text">LOADING FAVORITES...</span> | |
| 153 | - | </div> | |
| 154 | - | {:else} | |
| 155 | - | <FavoriteGrid | |
| 156 | - | favorites={displayedFavorites()} | |
| 157 | - | onSelectFavorite={handleSelectFavorite} | |
| 158 | - | onEditFavorite={handleEditFavorite} | |
| 159 | - | /> | |
| 160 | - | {/if} | |
| 161 | - | </div> | |
| 162 | - | </div> | |
| 163 | - | ||
| 164 | - | {#if editorOpen && editingFavorite} | |
| 165 | - | <FavoriteEditor | |
| 166 | - | favorite={editingFavorite} | |
| 167 | - | folders={homepageState.folders} | |
| 168 | - | tags={homepageState.tags} | |
| 169 | - | onSave={handleSaveFavorite} | |
| 170 | - | onDelete={handleDeleteFavorite} | |
| 171 | - | onClose={closeEditor} | |
| 172 | - | onAssignTag={handleAssignTag} | |
| 173 | - | onRemoveTag={handleRemoveTag} | |
| 174 | - | /> | |
| 175 | - | {/if} | |
| 176 | - | </div> | |
| 177 | - | ||
| 178 | - | <style> | |
| 179 | - | .favorites-panel { | |
| 180 | - | display: grid; | |
| 181 | - | grid-template-columns: 200px 1fr; | |
| 182 | - | gap: 0.75rem; | |
| 183 | - | padding: 0.5rem 1rem; | |
| 184 | - | } | |
| 185 | - | ||
| 186 | - | .panel-sidebar { | |
| 187 | - | display: flex; | |
| 188 | - | flex-direction: column; | |
| 189 | - | gap: 0.5rem; | |
| 190 | - | } | |
| 191 | - | ||
| 192 | - | .panel-main { | |
| 193 | - | display: flex; | |
| 194 | - | flex-direction: column; | |
| 195 | - | min-height: 0; | |
| 196 | - | border-width: 1px; | |
| 197 | - | border-style: solid; | |
| 198 | - | border-color: var(--border); | |
| 199 | - | background: color-mix(in srgb, var(--background) 95%, var(--card) 5%); | |
| 200 | - | } | |
| 201 | - | ||
| 202 | - | .panel-header { | |
| 203 | - | display: flex; | |
| 204 | - | align-items: center; | |
| 205 | - | justify-content: space-between; | |
| 206 | - | padding: 0.6rem 0.85rem; | |
| 207 | - | border-bottom: 1px solid var(--border); | |
| 208 | - | background: color-mix(in srgb, var(--background) 80%, var(--card) 20%); | |
| 209 | - | } | |
| 210 | - | ||
| 211 | - | .header-title { | |
| 212 | - | font-size: 0.68rem; | |
| 213 | - | font-weight: 600; | |
| 214 | - | letter-spacing: 0.12em; | |
| 215 | - | color: var(--foreground); | |
| 216 | - | } | |
| 217 | - | ||
| 218 | - | .header-count { | |
| 219 | - | font-size: 0.6rem; | |
| 220 | - | padding: 0.15rem 0.4rem; | |
| 221 | - | border-width: 1px; | |
| 222 | - | border-style: solid; | |
| 223 | - | border-color: var(--border); | |
| 224 | - | background: var(--background); | |
| 225 | - | color: var(--muted-foreground); | |
| 226 | - | } | |
| 227 | - | ||
| 228 | - | .panel-content { | |
| 229 | - | padding: 0.75rem; | |
| 230 | - | } | |
| 231 | - | ||
| 232 | - | .loading-state { | |
| 233 | - | flex: 1; | |
| 234 | - | display: flex; | |
| 235 | - | flex-direction: column; | |
| 236 | - | align-items: center; | |
| 237 | - | justify-content: center; | |
| 238 | - | gap: 0.75rem; | |
| 239 | - | padding: 2rem; | |
| 240 | - | color: var(--muted-foreground); | |
| 241 | - | } | |
| 242 | - | ||
| 243 | - | .loading-spinner { | |
| 244 | - | width: 24px; | |
| 245 | - | height: 24px; | |
| 246 | - | border-width: 2px; | |
| 247 | - | border-style: solid; | |
| 248 | - | border-color: var(--border); | |
| 249 | - | border-top-color: var(--primary, #4ade80); | |
| 250 | - | border-radius: 50%; | |
| 251 | - | animation: spin 0.8s linear infinite; | |
| 252 | - | } | |
| 253 | - | ||
| 254 | - | @keyframes spin { | |
| 255 | - | to { transform: rotate(360deg); } | |
| 256 | - | } | |
| 257 | - | ||
| 258 | - | .loading-text { | |
| 259 | - | font-size: 0.68rem; | |
| 260 | - | letter-spacing: 0.12em; | |
| 261 | - | animation: pulse 1.5s ease-in-out infinite; | |
| 262 | - | } | |
| 263 | - | ||
| 264 | - | @keyframes pulse { | |
| 265 | - | 0%, 100% { opacity: 0.5; } | |
| 266 | - | 50% { opacity: 1; } | |
| 267 | - | } | |
| 268 | - | ||
| 269 | - | /* Responsive: stack on smaller screens */ | |
| 270 | - | @media (max-width: 768px) { | |
| 271 | - | .favorites-panel { | |
| 272 | - | grid-template-columns: 1fr; | |
| 273 | - | grid-template-rows: auto 1fr; | |
| 274 | - | } | |
| 275 | - | ||
| 276 | - | .panel-sidebar { | |
| 277 | - | flex-direction: row; | |
| 278 | - | gap: 0.5rem; | |
| 279 | - | overflow-x: auto; | |
| 280 | - | } | |
| 281 | - | ||
| 282 | - | .panel-sidebar > :global(*) { | |
| 283 | - | flex-shrink: 0; | |
| 284 | - | min-width: 180px; | |
| 285 | - | } | |
| 286 | - | } | |
| 287 | - | </style> | |
D webui/src/pages/homepage/panels/HistoryPanel.svelte +0 -210
| @@ -1,210 +0,0 @@ | |||
| 1 | - | <script lang="ts"> | |
| 2 | - | import { homepageState } from '../state.svelte'; | |
| 3 | - | import { | |
| 4 | - | fetchHistoryTimeline, | |
| 5 | - | deleteHistoryEntry, | |
| 6 | - | deleteHistoryByRange, | |
| 7 | - | clearAllHistory, | |
| 8 | - | fetchDomainStats, | |
| 9 | - | navigateTo | |
| 10 | - | } from '../messaging'; | |
| 11 | - | import type { HistoryEntry, HistoryCleanupRange } from '../types'; | |
| 12 | - | ||
| 13 | - | import { HistoryTimeline, HistorySearch, HistoryFilters, HistoryCleanup } from '../history'; | |
| 14 | - | ||
| 15 | - | let cleanupOpen = $state(false); | |
| 16 | - | let selectedDomain = $state<string | null>(null); | |
| 17 | - | ||
| 18 | - | // Filter entries by domain if selected | |
| 19 | - | const displayedEntries = $derived(() => { | |
| 20 | - | if (!selectedDomain) { | |
| 21 | - | return homepageState.historySearchQuery | |
| 22 | - | ? homepageState.historySearchResults | |
| 23 | - | : homepageState.history; | |
| 24 | - | } | |
| 25 | - | ||
| 26 | - | const entries = homepageState.historySearchQuery | |
| 27 | - | ? homepageState.historySearchResults | |
| 28 | - | : homepageState.history; | |
| 29 | - | ||
| 30 | - | return entries.filter(entry => { | |
| 31 | - | try { | |
| 32 | - | return new URL(entry.url).hostname === selectedDomain; | |
| 33 | - | } catch { | |
| 34 | - | return false; | |
| 35 | - | } | |
| 36 | - | }); | |
| 37 | - | }); | |
| 38 | - | ||
| 39 | - | // Recompute timeline from filtered entries | |
| 40 | - | /* eslint-disable svelte/prefer-svelte-reactivity -- temporary Map/Date for data grouping, not reactive state */ | |
| 41 | - | const displayedTimeline = $derived(() => { | |
| 42 | - | const entries = displayedEntries(); | |
| 43 | - | if (entries.length === 0) return []; | |
| 44 | - | ||
| 45 | - | const groups = new Map<string, HistoryEntry[]>(); | |
| 46 | - | const today = new Date(); | |
| 47 | - | today.setHours(0, 0, 0, 0); | |
| 48 | - | const yesterday = new Date(today); | |
| 49 | - | yesterday.setDate(yesterday.getDate() - 1); | |
| 50 | - | ||
| 51 | - | for (const entry of entries) { | |
| 52 | - | const date = new Date(entry.last_visited); | |
| 53 | - | const dateKey = date.toISOString().split('T')[0] ?? ''; | |
| 54 | - | if (!groups.has(dateKey)) { | |
| 55 | - | groups.set(dateKey, []); | |
| 56 | - | } | |
| 57 | - | groups.get(dateKey)!.push(entry); | |
| 58 | - | } | |
| 59 | - | ||
| 60 | - | const result: { date: string; label: string; entries: HistoryEntry[] }[] = []; | |
| 61 | - | for (const [dateKey, items] of groups) { | |
| 62 | - | const date = new Date(dateKey); | |
| 63 | - | let label: string; | |
| 64 | - | ||
| 65 | - | if (date >= today) { | |
| 66 | - | label = 'Today'; | |
| 67 | - | } else if (date >= yesterday) { | |
| 68 | - | label = 'Yesterday'; | |
| 69 | - | } else { | |
| 70 | - | label = date.toLocaleDateString(undefined, { | |
| 71 | - | weekday: 'short', | |
| 72 | - | month: 'short', | |
| 73 | - | day: 'numeric', | |
| 74 | - | }); | |
| 75 | - | } | |
| 76 | - | ||
| 77 | - | result.push({ date: dateKey, label, entries: items }); | |
| 78 | - | } | |
| 79 | - | ||
| 80 | - | return result.sort((a, b) => b.date.localeCompare(a.date)); | |
| 81 | - | }); | |
| 82 | - | /* eslint-enable svelte/prefer-svelte-reactivity */ | |
| 83 | - | ||
| 84 | - | const handleSelectEntry = (entry: HistoryEntry) => { | |
| 85 | - | navigateTo(entry.url); | |
| 86 | - | }; | |
| 87 | - | ||
| 88 | - | const handleDeleteEntry = async (entry: HistoryEntry) => { | |
| 89 | - | await deleteHistoryEntry(entry.id); | |
| 90 | - | }; | |
| 91 | - | ||
| 92 | - | const handleLoadMore = async () => { | |
| 93 | - | if (homepageState.hasMoreHistory && !homepageState.historyLoading) { | |
| 94 | - | await fetchHistoryTimeline(50, homepageState.historyOffset); | |
| 95 | - | } | |
| 96 | - | }; | |
| 97 | - | ||
| 98 | - | const handleCleanup = async (range: HistoryCleanupRange) => { | |
| 99 | - | if (range === 'all') { | |
| 100 | - | await clearAllHistory(); | |
| 101 | - | } else { | |
| 102 | - | await deleteHistoryByRange(range); | |
| 103 | - | } | |
| 104 | - | cleanupOpen = false; | |
| 105 | - | }; | |
| 106 | - | ||
| 107 | - | const handleSelectDomain = (domain: string | null) => { | |
| 108 | - | selectedDomain = domain; | |
| 109 | - | homepageState.setFocusedIndex(0); | |
| 110 | - | }; | |
| 111 | - | ||
| 112 | - | // Fetch domain stats on mount | |
| 113 | - | $effect(() => { | |
| 114 | - | fetchDomainStats(10); | |
| 115 | - | }); | |
| 116 | - | </script> | |
| 117 | - | ||
| 118 | - | <div class="history-panel"> | |
| 119 | - | <div class="panel-toolbar"> | |
| 120 | - | <HistorySearch placeholder="Search history (FTS)..." autofocus={true} /> | |
| 121 | - | </div> | |
| 122 | - | ||
| 123 | - | <HistoryFilters | |
| 124 | - | domains={homepageState.domainStats} | |
| 125 | - | selectedDomain={selectedDomain} | |
| 126 | - | onSelectDomain={handleSelectDomain} | |
| 127 | - | onOpenCleanup={() => cleanupOpen = true} | |
| 128 | - | /> | |
| 129 | - | ||
| 130 | - | <div class="panel-content"> | |
| 131 | - | {#if homepageState.historyLoading && homepageState.history.length === 0} | |
| 132 | - | <div class="loading-state"> | |
| 133 | - | <span class="loading-spinner"></span> | |
| 134 | - | <span class="loading-text">LOADING HISTORY...</span> | |
| 135 | - | </div> | |
| 136 | - | {:else} | |
| 137 | - | <HistoryTimeline | |
| 138 | - | groups={displayedTimeline()} | |
| 139 | - | onSelectEntry={handleSelectEntry} | |
| 140 | - | onDeleteEntry={handleDeleteEntry} | |
| 141 | - | onLoadMore={handleLoadMore} | |
| 142 | - | /> | |
| 143 | - | {/if} | |
| 144 | - | </div> | |
| 145 | - | ||
| 146 | - | {#if cleanupOpen} | |
| 147 | - | <HistoryCleanup | |
| 148 | - | onCleanup={handleCleanup} | |
| 149 | - | onClose={() => cleanupOpen = false} | |
| 150 | - | /> | |
| 151 | - | {/if} | |
| 152 | - | </div> | |
| 153 | - | ||
| 154 | - | <style> | |
| 155 | - | .history-panel { | |
| 156 | - | display: flex; | |
| 157 | - | flex-direction: column; | |
| 158 | - | gap: 0.5rem; | |
| 159 | - | padding: 0.5rem 1rem; | |
| 160 | - | } | |
| 161 | - | ||
| 162 | - | .panel-toolbar { | |
| 163 | - | flex-shrink: 0; | |
| 164 | - | } | |
| 165 | - | ||
| 166 | - | .panel-content { | |
| 167 | - | display: flex; | |
| 168 | - | flex-direction: column; | |
| 169 | - | border-width: 1px; | |
| 170 | - | border-style: solid; | |
| 171 | - | border-color: var(--border); | |
| 172 | - | background: color-mix(in srgb, var(--background) 95%, var(--card) 5%); | |
| 173 | - | } | |
| 174 | - | ||
| 175 | - | .loading-state { | |
| 176 | - | flex: 1; | |
| 177 | - | display: flex; | |
| 178 | - | flex-direction: column; | |
| 179 | - | align-items: center; | |
| 180 | - | justify-content: center; | |
| 181 | - | gap: 0.75rem; | |
| 182 | - | color: var(--muted-foreground); | |
| 183 | - | } | |
| 184 | - | ||
| 185 | - | .loading-spinner { | |
| 186 | - | width: 24px; | |
| 187 | - | height: 24px; | |
| 188 | - | border-width: 2px; | |
| 189 | - | border-style: solid; | |
| 190 | - | border-color: var(--border); | |
| 191 | - | border-top-color: var(--primary, #4ade80); | |
| 192 | - | border-radius: 50%; | |
| 193 | - | animation: spin 0.8s linear infinite; | |
| 194 | - | } | |
| 195 | - | ||
| 196 | - | @keyframes spin { | |
| 197 | - | to { transform: rotate(360deg); } | |
| 198 | - | } | |
| 199 | - | ||
| 200 | - | .loading-text { | |
| 201 | - | font-size: 0.68rem; | |
| 202 | - | letter-spacing: 0.12em; | |
| 203 | - | animation: pulse 1.5s ease-in-out infinite; | |
| 204 | - | } | |
| 205 | - | ||
| 206 | - | @keyframes pulse { | |
| 207 | - | 0%, 100% { opacity: 0.5; } | |
| 208 | - | 50% { opacity: 1; } | |
| 209 | - | } | |
| 210 | - | </style> | |
D webui/src/pages/homepage/panels/index.ts +0 -4
| @@ -1,4 +0,0 @@ | |||
| 1 | - | // Panel Components - Main content panels for homepage | |
| 2 | - | export { default as HistoryPanel } from './HistoryPanel.svelte'; | |
| 3 | - | export { default as FavoritesPanel } from './FavoritesPanel.svelte'; | |
| 4 | - | export { default as AnalyticsPanel } from './AnalyticsPanel.svelte'; | |
D webui/src/pages/homepage/shell/CommandPalette.svelte +0 -613
| @@ -1,613 +0,0 @@ | |||
| 1 | - | <script lang="ts"> | |
| 2 | - | import { onMount, tick } from 'svelte'; | |
| 3 | - | import { homepageState } from '../state.svelte'; | |
| 4 | - | import { navigateTo } from '../messaging'; | |
| 5 | - | import type { CommandPaletteItem } from '../types'; | |
| 6 | - | import { Search, SearchX, History, Star, BarChart3, Trash2, Globe } from '@lucide/svelte'; | |
| 7 | - | ||
| 8 | - | // Input element reference | |
| 9 | - | let inputRef: HTMLInputElement | null = $state(null); | |
| 10 | - | let listRef: HTMLDivElement | null = $state(null); | |
| 11 | - | ||
| 12 | - | // Local state for command palette | |
| 13 | - | let query = $state(''); | |
| 14 | - | let selectedIndex = $state(0); | |
| 15 | - | ||
| 16 | - | // Build command items from available actions | |
| 17 | - | const staticCommands: CommandPaletteItem[] = [ | |
| 18 | - | { | |
| 19 | - | id: 'nav:history', | |
| 20 | - | label: 'Go to History', | |
| 21 | - | description: 'View browsing history timeline', | |
| 22 | - | icon: History, | |
| 23 | - | shortcut: 'g h', | |
| 24 | - | action: () => { | |
| 25 | - | homepageState.setActivePanel('history'); | |
| 26 | - | homepageState.closeCommandPalette(); | |
| 27 | - | }, | |
| 28 | - | }, | |
| 29 | - | { | |
| 30 | - | id: 'nav:favorites', | |
| 31 | - | label: 'Go to Favorites', | |
| 32 | - | description: 'View bookmarks and folders', | |
| 33 | - | icon: Star, | |
| 34 | - | shortcut: 'g f', | |
| 35 | - | action: () => { | |
| 36 | - | homepageState.setActivePanel('favorites'); | |
| 37 | - | homepageState.closeCommandPalette(); | |
| 38 | - | }, | |
| 39 | - | }, | |
| 40 | - | { | |
| 41 | - | id: 'nav:analytics', | |
| 42 | - | label: 'Go to Analytics', | |
| 43 | - | description: 'View browsing statistics', | |
| 44 | - | icon: BarChart3, | |
| 45 | - | shortcut: 'g a', | |
| 46 | - | action: () => { | |
| 47 | - | homepageState.setActivePanel('analytics'); | |
| 48 | - | homepageState.closeCommandPalette(); | |
| 49 | - | }, | |
| 50 | - | }, | |
| 51 | - | { | |
| 52 | - | id: 'action:cleanup', | |
| 53 | - | label: 'Clean History', | |
| 54 | - | description: 'Remove history entries by time range', | |
| 55 | - | icon: Trash2, | |
| 56 | - | action: () => { | |
| 57 | - | homepageState.closeCommandPalette(); | |
| 58 | - | homepageState.openCleanupModal(); | |
| 59 | - | }, | |
| 60 | - | }, | |
| 61 | - | ]; | |
| 62 | - | ||
| 63 | - | // Fuzzy match function | |
| 64 | - | function fuzzyMatch(text: string, pattern: string): { match: boolean; score: number } { | |
| 65 | - | if (!pattern) return { match: true, score: 0 }; | |
| 66 | - | ||
| 67 | - | const lower = text.toLowerCase(); | |
| 68 | - | const pLower = pattern.toLowerCase(); | |
| 69 | - | ||
| 70 | - | // Exact substring match scores highest | |
| 71 | - | if (lower.includes(pLower)) { | |
| 72 | - | return { match: true, score: 100 - lower.indexOf(pLower) }; | |
| 73 | - | } | |
| 74 | - | ||
| 75 | - | // Fuzzy character matching | |
| 76 | - | let pIdx = 0; | |
| 77 | - | let score = 0; | |
| 78 | - | let consecutive = 0; | |
| 79 | - | ||
| 80 | - | for (let i = 0; i < lower.length && pIdx < pLower.length; i++) { | |
| 81 | - | if (lower[i] === pLower[pIdx]) { | |
| 82 | - | pIdx++; | |
| 83 | - | consecutive++; | |
| 84 | - | score += consecutive * 2; | |
| 85 | - | } else { | |
| 86 | - | consecutive = 0; | |
| 87 | - | } | |
| 88 | - | } | |
| 89 | - | ||
| 90 | - | return { match: pIdx === pLower.length, score }; | |
| 91 | - | } | |
| 92 | - | ||
| 93 | - | // Build filtered items based on query | |
| 94 | - | const filteredItems = $derived.by(() => { | |
| 95 | - | const results: Array<{ item: CommandPaletteItem; score: number }> = []; | |
| 96 | - | ||
| 97 | - | // Add static commands | |
| 98 | - | for (const cmd of staticCommands) { | |
| 99 | - | const labelMatch = fuzzyMatch(cmd.label, query); | |
| 100 | - | const descMatch = cmd.description ? fuzzyMatch(cmd.description, query) : { match: false, score: 0 }; | |
| 101 | - | ||
| 102 | - | if (labelMatch.match || descMatch.match) { | |
| 103 | - | results.push({ | |
| 104 | - | item: cmd, | |
| 105 | - | score: Math.max(labelMatch.score, descMatch.score * 0.8), | |
| 106 | - | }); | |
| 107 | - | } | |
| 108 | - | } | |
| 109 | - | ||
| 110 | - | // Add history items as navigable commands | |
| 111 | - | for (const entry of homepageState.history.slice(0, 50)) { | |
| 112 | - | const titleMatch = fuzzyMatch(entry.title || '', query); | |
| 113 | - | const urlMatch = fuzzyMatch(entry.url, query); | |
| 114 | - | ||
| 115 | - | if (titleMatch.match || urlMatch.match) { | |
| 116 | - | results.push({ | |
| 117 | - | item: { | |
| 118 | - | id: `history:${entry.id}`, | |
| 119 | - | label: entry.title || getDomain(entry.url), | |
| 120 | - | description: entry.url, | |
| 121 | - | icon: Globe, | |
| 122 | - | action: () => { | |
| 123 | - | navigateTo(entry.url); | |
| 124 | - | }, | |
| 125 | - | }, | |
| 126 | - | score: Math.max(titleMatch.score, urlMatch.score * 0.7), | |
| 127 | - | }); | |
| 128 | - | } | |
| 129 | - | } | |
| 130 | - | ||
| 131 | - | // Add favorites as navigable commands | |
| 132 | - | for (const fav of homepageState.favorites) { | |
| 133 | - | const titleMatch = fuzzyMatch(fav.title || '', query); | |
| 134 | - | const urlMatch = fuzzyMatch(fav.url, query); | |
| 135 | - | ||
| 136 | - | if (titleMatch.match || urlMatch.match) { | |
| 137 | - | results.push({ | |
| 138 | - | item: { | |
| 139 | - | id: `fav:${fav.id}`, | |
| 140 | - | label: fav.title || getDomain(fav.url), | |
| 141 | - | description: fav.url, | |
| 142 | - | icon: Star, | |
| 143 | - | shortcut: fav.shortcut_key ? `${fav.shortcut_key}` : undefined, | |
| 144 | - | action: () => { | |
| 145 | - | navigateTo(fav.url); | |
| 146 | - | }, | |
| 147 | - | }, | |
| 148 | - | score: Math.max(titleMatch.score, urlMatch.score * 0.7) + 10, // Boost favorites | |
| 149 | - | }); | |
| 150 | - | } | |
| 151 | - | } | |
| 152 | - | ||
| 153 | - | // Sort by score and return items | |
| 154 | - | results.sort((a, b) => b.score - a.score); | |
| 155 | - | return results.map(r => r.item).slice(0, 20); | |
| 156 | - | }); | |
| 157 | - | ||
| 158 | - | function getDomain(url: string): string { | |
| 159 | - | try { | |
| 160 | - | return new URL(url).hostname; | |
| 161 | - | } catch { | |
| 162 | - | return url; | |
| 163 | - | } | |
| 164 | - | } | |
| 165 | - | ||
| 166 | - | // Reset selection when query changes | |
| 167 | - | $effect(() => { | |
| 168 | - | void query; // Track query changes | |
| 169 | - | selectedIndex = 0; | |
| 170 | - | }); | |
| 171 | - | ||
| 172 | - | // Scroll selected item into view | |
| 173 | - | $effect(() => { | |
| 174 | - | if (listRef) { | |
| 175 | - | const selected = listRef.querySelector(`[data-index="${selectedIndex}"]`); | |
| 176 | - | selected?.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); | |
| 177 | - | } | |
| 178 | - | }); | |
| 179 | - | ||
| 180 | - | function handleKeydown(e: KeyboardEvent) { | |
| 181 | - | switch (e.key) { | |
| 182 | - | case 'ArrowDown': | |
| 183 | - | case 'j': | |
| 184 | - | if (e.key === 'j' && !e.ctrlKey) break; | |
| 185 | - | e.preventDefault(); | |
| 186 | - | selectedIndex = Math.min(selectedIndex + 1, filteredItems.length - 1); | |
| 187 | - | break; | |
| 188 | - | ||
| 189 | - | case 'ArrowUp': | |
| 190 | - | case 'k': | |
| 191 | - | if (e.key === 'k' && !e.ctrlKey) break; | |
| 192 | - | e.preventDefault(); | |
| 193 | - | selectedIndex = Math.max(selectedIndex - 1, 0); | |
| 194 | - | break; | |
| 195 | - | ||
| 196 | - | case 'n': | |
| 197 | - | if (e.ctrlKey) { | |
| 198 | - | e.preventDefault(); | |
| 199 | - | selectedIndex = Math.min(selectedIndex + 1, filteredItems.length - 1); | |
| 200 | - | } | |
| 201 | - | break; | |
| 202 | - | ||
| 203 | - | case 'p': | |
| 204 | - | if (e.ctrlKey) { | |
| 205 | - | e.preventDefault(); | |
| 206 | - | selectedIndex = Math.max(selectedIndex - 1, 0); | |
| 207 | - | } | |
| 208 | - | break; | |
| 209 | - | ||
| 210 | - | case 'Enter': | |
| 211 | - | e.preventDefault(); | |
| 212 | - | executeSelected(); | |
| 213 | - | break; | |
| 214 | - | ||
| 215 | - | case 'Escape': | |
| 216 | - | e.preventDefault(); | |
| 217 | - | homepageState.closeCommandPalette(); | |
| 218 | - | break; | |
| 219 | - | } | |
| 220 | - | } | |
| 221 | - | ||
| 222 | - | function executeSelected() { | |
| 223 | - | const item = filteredItems[selectedIndex]; | |
| 224 | - | if (item) { | |
| 225 | - | item.action(); | |
| 226 | - | } | |
| 227 | - | } | |
| 228 | - | ||
| 229 | - | function handleItemClick(index: number) { | |
| 230 | - | selectedIndex = index; | |
| 231 | - | executeSelected(); | |
| 232 | - | } | |
| 233 | - | ||
| 234 | - | function handleOverlayInteraction(event: MouseEvent | KeyboardEvent) { | |
| 235 | - | if (event instanceof KeyboardEvent) { | |
| 236 | - | if (event.key === 'Escape' || event.key === 'Enter' || event.key === ' ') { | |
| 237 | - | event.preventDefault(); | |
| 238 | - | homepageState.closeCommandPalette(); | |
| 239 | - | } | |
| 240 | - | return; | |
| 241 | - | } | |
| 242 | - | ||
| 243 | - | if (event.currentTarget === event.target) { | |
| 244 | - | homepageState.closeCommandPalette(); | |
| 245 | - | } | |
| 246 | - | } | |
| 247 | - | ||
| 248 | - | onMount(async () => { | |
| 249 | - | await tick(); | |
| 250 | - | inputRef?.focus(); | |
| 251 | - | }); | |
| 252 | - | </script> | |
| 253 | - | ||
| 254 | - | <div | |
| 255 | - | class="palette-overlay" | |
| 256 | - | role="button" | |
| 257 | - | tabindex="0" | |
| 258 | - | aria-label="Close command palette" | |
| 259 | - | onclick={handleOverlayInteraction} | |
| 260 | - | onkeydown={handleOverlayInteraction} | |
| 261 | - | > | |
| 262 | - | <div class="palette-container" onclick={(e) => e.stopPropagation()} role="presentation"> | |
| 263 | - | <!-- Search Header --> | |
| 264 | - | <div class="palette-header"> | |
| 265 | - | <div class="search-row"> | |
| 266 | - | <span class="search-icon"> | |
| 267 | - | <Search size={16} strokeWidth={2} /> | |
| 268 | - | </span> | |
| 269 | - | <input | |
| 270 | - | bind:this={inputRef} | |
| 271 | - | bind:value={query} | |
| 272 | - | type="text" | |
| 273 | - | class="search-input" | |
| 274 | - | placeholder="Search commands, history, favorites..." | |
| 275 | - | spellcheck="false" | |
| 276 | - | autocomplete="off" | |
| 277 | - | onkeydown={handleKeydown} | |
| 278 | - | /> | |
| 279 | - | <div class="search-meta"> | |
| 280 | - | <span class="result-count">{filteredItems.length}</span> | |
| 281 | - | <kbd class="esc-hint">esc</kbd> | |
| 282 | - | </div> | |
| 283 | - | </div> | |
| 284 | - | </div> | |
| 285 | - | ||
| 286 | - | <!-- Results List --> | |
| 287 | - | <div class="palette-results" bind:this={listRef}> | |
| 288 | - | {#if filteredItems.length === 0} | |
| 289 | - | <div class="empty-results"> | |
| 290 | - | <span class="empty-icon"> | |
| 291 | - | <SearchX size={24} strokeWidth={1.5} /> | |
| 292 | - | </span> | |
| 293 | - | <span class="empty-text">No matches found</span> | |
| 294 | - | </div> | |
| 295 | - | {:else} | |
| 296 | - | {#each filteredItems as item, index (item.id)} | |
| 297 | - | <button | |
| 298 | - | class="result-item" | |
| 299 | - | class:selected={index === selectedIndex} | |
| 300 | - | data-index={index} | |
| 301 | - | type="button" | |
| 302 | - | onclick={() => handleItemClick(index)} | |
| 303 | - | onmouseenter={() => selectedIndex = index} | |
| 304 | - | > | |
| 305 | - | <span class="item-icon"> | |
| 306 | - | {#if typeof item.icon === 'function'} | |
| 307 | - | <item.icon size={16} strokeWidth={2} /> | |
| 308 | - | {:else if item.icon} | |
| 309 | - | {item.icon} | |
| 310 | - | {/if} | |
| 311 | - | </span> | |
| 312 | - | <div class="item-content"> | |
| 313 | - | <span class="item-label">{item.label}</span> | |
| 314 | - | {#if item.description} | |
| 315 | - | <span class="item-description">{item.description}</span> | |
| 316 | - | {/if} | |
| 317 | - | </div> | |
| 318 | - | {#if item.shortcut} | |
| 319 | - | <kbd class="item-shortcut">{item.shortcut}</kbd> | |
| 320 | - | {/if} | |
| 321 | - | </button> | |
| 322 | - | {/each} | |
| 323 | - | {/if} | |
| 324 | - | </div> | |
| 325 | - | ||
| 326 | - | <!-- Footer Hints --> | |
| 327 | - | <div class="palette-footer"> | |
| 328 | - | <div class="hint-group"> | |
| 329 | - | <kbd></kbd><kbd></kbd> | |
| 330 | - | <span>navigate</span> | |
| 331 | - | </div> | |
| 332 | - | <div class="hint-group"> | |
| 333 | - | <kbd></kbd> | |
| 334 | - | <span>select</span> | |
| 335 | - | </div> | |
| 336 | - | <div class="hint-group"> | |
| 337 | - | <kbd>esc</kbd> | |
| 338 | - | <span>close</span> | |
| 339 | - | </div> | |
| 340 | - | </div> | |
| 341 | - | </div> | |
| 342 | - | </div> | |
| 343 | - | ||
| 344 | - | <style> | |
| 345 | - | .palette-overlay { | |
| 346 | - | position: fixed; | |
| 347 | - | inset: 0; | |
| 348 | - | z-index: 200; | |
| 349 | - | display: flex; | |
| 350 | - | align-items: flex-start; | |
| 351 | - | justify-content: center; | |
| 352 | - | padding-top: 12vh; | |
| 353 | - | background: rgb(0 0 0 / 0.65); | |
| 354 | - | backdrop-filter: blur(8px); | |
| 355 | - | animation: overlay-in 150ms ease; | |
| 356 | - | } | |
| 357 | - | ||
| 358 | - | @keyframes overlay-in { | |
| 359 | - | from { | |
| 360 | - | opacity: 0; | |
| 361 | - | backdrop-filter: blur(0); | |
| 362 | - | } | |
| 363 | - | } | |
| 364 | - | ||
| 365 | - | .palette-container { | |
| 366 | - | width: 100%; | |
| 367 | - | max-width: 580px; | |
| 368 | - | margin: 0 1rem; | |
| 369 | - | display: flex; | |
| 370 | - | flex-direction: column; | |
| 371 | - | background: var(--card); | |
| 372 | - | border-width: 1px; | |
| 373 | - | border-style: solid; | |
| 374 | - | border-color: var(--border); | |
| 375 | - | box-shadow: | |
| 376 | - | 0 0 0 1px color-mix(in srgb, var(--border) 30%, transparent), | |
| 377 | - | 0 32px 64px -16px rgb(0 0 0 / 0.7); | |
| 378 | - | animation: palette-in 200ms cubic-bezier(0.16, 1, 0.3, 1); | |
| 379 | - | } | |
| 380 | - | ||
| 381 | - | @keyframes palette-in { | |
| 382 | - | from { | |
| 383 | - | opacity: 0; | |
| 384 | - | transform: scale(0.96) translateY(-12px); | |
| 385 | - | } | |
| 386 | - | } | |
| 387 | - | ||
| 388 | - | /* Header */ | |
| 389 | - | .palette-header { | |
| 390 | - | padding: 0; | |
| 391 | - | border-bottom: 1px solid var(--border); | |
| 392 | - | background: color-mix(in srgb, var(--background) 70%, transparent); | |
| 393 | - | } | |
| 394 | - | ||
| 395 | - | .search-row { | |
| 396 | - | display: flex; | |
| 397 | - | align-items: center; | |
| 398 | - | gap: 0.75rem; | |
| 399 | - | padding: 0.85rem 1rem; | |
| 400 | - | } | |
| 401 | - | ||
| 402 | - | .search-icon { | |
| 403 | - | font-size: 1rem; | |
| 404 | - | color: var(--primary, #4ade80); | |
| 405 | - | flex-shrink: 0; | |
| 406 | - | } | |
| 407 | - | ||
| 408 | - | .search-input { | |
| 409 | - | flex: 1; | |
| 410 | - | background: transparent; | |
| 411 | - | border: none; | |
| 412 | - | outline: none; | |
| 413 | - | font-size: 0.95rem; | |
| 414 | - | font-family: inherit; | |
| 415 | - | color: var(--foreground); | |
| 416 | - | caret-color: var(--primary, #4ade80); | |
| 417 | - | } | |
| 418 | - | ||
| 419 | - | .search-input::placeholder { | |
| 420 | - | color: var(--muted-foreground); | |
| 421 | - | opacity: 0.7; | |
| 422 | - | } | |
| 423 | - | ||
| 424 | - | .search-meta { | |
| 425 | - | display: flex; | |
| 426 | - | align-items: center; | |
| 427 | - | gap: 0.5rem; | |
| 428 | - | flex-shrink: 0; | |
| 429 | - | } | |
| 430 | - | ||
| 431 | - | .result-count { | |
| 432 | - | font-size: 0.7rem; | |
| 433 | - | color: var(--muted-foreground); | |
| 434 | - | letter-spacing: 0.05em; | |
| 435 | - | } | |
| 436 | - | ||
| 437 | - | .esc-hint { | |
| 438 | - | padding: 0.2rem 0.4rem; | |
| 439 | - | font-size: 0.65rem; | |
| 440 | - | color: var(--muted-foreground); | |
| 441 | - | background: var(--background); | |
| 442 | - | border-width: 1px; | |
| 443 | - | border-style: solid; | |
| 444 | - | border-color: var(--border); | |
| 445 | - | } | |
| 446 | - | ||
| 447 | - | /* Results */ | |
| 448 | - | .palette-results { | |
| 449 | - | max-height: 360px; | |
| 450 | - | overflow-y: auto; | |
| 451 | - | overscroll-behavior: contain; | |
| 452 | - | } | |
| 453 | - | ||
| 454 | - | .empty-results { | |
| 455 | - | display: flex; | |
| 456 | - | flex-direction: column; | |
| 457 | - | align-items: center; | |
| 458 | - | justify-content: center; | |
| 459 | - | gap: 0.5rem; | |
| 460 | - | padding: 2.5rem 1rem; | |
| 461 | - | color: var(--muted-foreground); | |
| 462 | - | } | |
| 463 | - | ||
| 464 | - | .empty-icon { | |
| 465 | - | font-size: 1.5rem; | |
| 466 | - | opacity: 0.5; | |
| 467 | - | } | |
| 468 | - | ||
| 469 | - | .empty-text { | |
| 470 | - | font-size: 0.8rem; | |
| 471 | - | letter-spacing: 0.05em; | |
| 472 | - | } | |
| 473 | - | ||
| 474 | - | .result-item { | |
| 475 | - | width: 100%; | |
| 476 | - | display: flex; | |
| 477 | - | align-items: center; | |
| 478 | - | gap: 0.75rem; | |
| 479 | - | padding: 0.7rem 1rem; | |
| 480 | - | background: transparent; | |
| 481 | - | border: none; | |
| 482 | - | border-bottom: 1px solid color-mix(in srgb, var(--border) 40%, transparent); | |
| 483 | - | cursor: pointer; | |
| 484 | - | text-align: left; | |
| 485 | - | transition: background 80ms ease; | |
| 486 | - | } | |
| 487 | - | ||
| 488 | - | .result-item:last-child { | |
| 489 | - | border-bottom: none; | |
| 490 | - | } | |
| 491 | - | ||
| 492 | - | .result-item:hover { | |
| 493 | - | background: color-mix(in srgb, var(--card) 80%, var(--background) 20%); | |
| 494 | - | } | |
| 495 | - | ||
| 496 | - | .result-item.selected { | |
| 497 | - | background: color-mix(in srgb, var(--primary, #4ade80) 12%, transparent); | |
| 498 | - | box-shadow: inset 2px 0 0 var(--primary, #4ade80); | |
| 499 | - | } | |
| 500 | - | ||
| 501 | - | .item-icon { | |
| 502 | - | font-size: 1rem; | |
| 503 | - | color: var(--muted-foreground); | |
| 504 | - | flex-shrink: 0; | |
| 505 | - | width: 1.5rem; | |
| 506 | - | text-align: center; | |
| 507 | - | } | |
| 508 | - | ||
| 509 | - | .result-item.selected .item-icon { | |
| 510 | - | color: var(--primary, #4ade80); | |
| 511 | - | } | |
| 512 | - | ||
| 513 | - | .item-content { | |
| 514 | - | flex: 1; | |
| 515 | - | min-width: 0; | |
| 516 | - | display: flex; | |
| 517 | - | flex-direction: column; | |
| 518 | - | gap: 0.15rem; | |
| 519 | - | } | |
| 520 | - | ||
| 521 | - | .item-label { | |
| 522 | - | font-size: 0.85rem; | |
| 523 | - | color: var(--foreground); | |
| 524 | - | white-space: nowrap; | |
| 525 | - | overflow: hidden; | |
| 526 | - | text-overflow: ellipsis; | |
| 527 | - | } | |
| 528 | - | ||
| 529 | - | .item-description { | |
| 530 | - | font-size: 0.72rem; | |
| 531 | - | color: var(--muted-foreground); | |
| 532 | - | white-space: nowrap; | |
| 533 | - | overflow: hidden; | |
| 534 | - | text-overflow: ellipsis; | |
| 535 | - | } | |
| 536 | - | ||
| 537 | - | .item-shortcut { | |
| 538 | - | padding: 0.2rem 0.45rem; | |
| 539 | - | font-size: 0.65rem; | |
| 540 | - | color: var(--muted-foreground); | |
| 541 | - | background: var(--background); | |
| 542 | - | border-width: 1px; | |
| 543 | - | border-style: solid; | |
| 544 | - | border-color: var(--border); | |
| 545 | - | flex-shrink: 0; | |
| 546 | - | } | |
| 547 | - | ||
| 548 | - | .result-item.selected .item-shortcut { | |
| 549 | - | border-color: color-mix(in srgb, var(--primary, #4ade80) 40%, var(--border) 60%); | |
| 550 | - | color: var(--foreground); | |
| 551 | - | } | |
| 552 | - | ||
| 553 | - | /* Footer */ | |
| 554 | - | .palette-footer { | |
| 555 | - | display: flex; | |
| 556 | - | align-items: center; | |
| 557 | - | justify-content: center; | |
| 558 | - | gap: 1.5rem; | |
| 559 | - | padding: 0.65rem 1rem; | |
| 560 | - | border-top: 1px solid var(--border); | |
| 561 | - | background: color-mix(in srgb, var(--background) 70%, transparent); | |
| 562 | - | } | |
| 563 | - | ||
| 564 | - | .hint-group { | |
| 565 | - | display: flex; | |
| 566 | - | align-items: center; | |
| 567 | - | gap: 0.35rem; | |
| 568 | - | font-size: 0.68rem; | |
| 569 | - | color: var(--muted-foreground); | |
| 570 | - | } | |
| 571 | - | ||
| 572 | - | .hint-group kbd { | |
| 573 | - | padding: 0.15rem 0.35rem; | |
| 574 | - | font-size: 0.62rem; | |
| 575 | - | background: var(--background); | |
| 576 | - | border-width: 1px; | |
| 577 | - | border-style: solid; | |
| 578 | - | border-color: var(--border); | |
| 579 | - | } | |
| 580 | - | ||
| 581 | - | /* Scrollbar */ | |
| 582 | - | .palette-results::-webkit-scrollbar { | |
| 583 | - | width: 6px; | |
| 584 | - | } | |
| 585 | - | ||
| 586 | - | .palette-results::-webkit-scrollbar-track { | |
| 587 | - | background: transparent; | |
| 588 | - | } | |
| 589 | - | ||
| 590 | - | .palette-results::-webkit-scrollbar-thumb { | |
| 591 | - | background: var(--border); | |
| 592 | - | } | |
| 593 | - | ||
| 594 | - | .palette-results::-webkit-scrollbar-thumb:hover { | |
| 595 | - | background: var(--muted-foreground); | |
| 596 | - | } | |
| 597 | - | ||
| 598 | - | @media (max-width: 480px) { | |
| 599 | - | .palette-overlay { | |
| 600 | - | padding-top: 2rem; | |
| 601 | - | align-items: flex-start; | |
| 602 | - | } | |
| 603 | - | ||
| 604 | - | .palette-container { | |
| 605 | - | max-height: calc(100vh - 4rem); | |
| 606 | - | } | |
| 607 | - | ||
| 608 | - | .palette-footer { | |
| 609 | - | flex-wrap: wrap; | |
| 610 | - | gap: 0.75rem; | |
| 611 | - | } | |
| 612 | - | } | |
| 613 | - | </style> | |
D webui/src/pages/homepage/shell/HomepageShell.svelte +0 -634
| @@ -1,634 +0,0 @@ | |||
| 1 | - | <script lang="ts"> | |
| 2 | - | import { onMount, onDestroy } from 'svelte'; | |
| 3 | - | import { homepageState } from '../state.svelte'; | |
| 4 | - | import { initializeHomepage } from '../messaging'; | |
| 5 | - | import { initializeKeyboard } from '../keyboard'; | |
| 6 | - | import CommandPalette from './CommandPalette.svelte'; | |
| 7 | - | import StatusBar from './StatusBar.svelte'; | |
| 8 | - | import KeyboardHints from './KeyboardHints.svelte'; | |
| 9 | - | import { History, Star, BarChart3, Search, Moon, Sun, Settings } from '@lucide/svelte'; | |
| 10 | - | ||
| 11 | - | // Props for slot content | |
| 12 | - | interface Props { | |
| 13 | - | children?: import('svelte').Snippet; | |
| 14 | - | } | |
| 15 | - | let { children }: Props = $props(); | |
| 16 | - | ||
| 17 | - | // Theme state | |
| 18 | - | type ThemeMode = 'light' | 'dark'; | |
| 19 | - | let themeMode = $state<ThemeMode>('dark'); | |
| 20 | - | let themeObserver: MutationObserver | null = null; | |
| 21 | - | ||
| 22 | - | // Keyboard cleanup | |
| 23 | - | let cleanupKeyboard: (() => void) | null = null; | |
| 24 | - | ||
| 25 | - | // Panel tabs config | |
| 26 | - | const panelTabs = [ | |
| 27 | - | { id: 'history' as const, label: 'HIST', icon: History }, | |
| 28 | - | { id: 'favorites' as const, label: 'FAV', icon: Star }, | |
| 29 | - | { id: 'analytics' as const, label: 'STAT', icon: BarChart3 }, | |
| 30 | - | ]; | |
| 31 | - | ||
| 32 | - | // Theme management | |
| 33 | - | const syncThemeState = () => { | |
| 34 | - | themeMode = document.documentElement.classList.contains('dark') ? 'dark' : 'light'; | |
| 35 | - | }; | |
| 36 | - | ||
| 37 | - | function navigateWithViewTransition(url: string) { | |
| 38 | - | if (typeof document.startViewTransition !== 'function') { | |
| 39 | - | window.location.href = url; | |
| 40 | - | return; | |
| 41 | - | } | |
| 42 | - | ||
| 43 | - | const transition = document.startViewTransition(() => { | |
| 44 | - | document.documentElement.dataset.vt = 'leaving'; | |
| 45 | - | }); | |
| 46 | - | ||
| 47 | - | transition.finished.finally(() => { | |
| 48 | - | window.location.href = url; | |
| 49 | - | }); | |
| 50 | - | } | |
| 51 | - | ||
| 52 | - | const toggleTheme = () => { | |
| 53 | - | const nextMode = themeMode === 'dark' ? 'light' : 'dark'; | |
| 54 | - | const manager = window.__dumber_color_scheme_manager; | |
| 55 | - | ||
| 56 | - | if (manager?.setUserPreference) { | |
| 57 | - | manager.setUserPreference(nextMode); | |
| 58 | - | return; | |
| 59 | - | } | |
| 60 | - | ||
| 61 | - | if (window.__dumber_setTheme) { | |
| 62 | - | window.__dumber_setTheme(nextMode); | |
| 63 | - | localStorage.setItem('dumber.theme', nextMode); | |
| 64 | - | return; | |
| 65 | - | } | |
| 66 | - | ||
| 67 | - | switch (nextMode) { | |
| 68 | - | case 'light': | |
| 69 | - | document.documentElement.classList.add('light'); | |
| 70 | - | document.documentElement.classList.remove('dark'); | |
| 71 | - | break; | |
| 72 | - | case 'dark': | |
| 73 | - | document.documentElement.classList.remove('light'); | |
| 74 | - | document.documentElement.classList.add('dark'); | |
| 75 | - | break; | |
| 76 | - | } | |
| 77 | - | localStorage.setItem('dumber.theme', nextMode); | |
| 78 | - | themeMode = nextMode; | |
| 79 | - | }; | |
| 80 | - | ||
| 81 | - | onMount(async () => { | |
| 82 | - | // Initialize keyboard navigation | |
| 83 | - | cleanupKeyboard = initializeKeyboard(); | |
| 84 | - | ||
| 85 | - | // Initialize data fetching | |
| 86 | - | await initializeHomepage(); | |
| 87 | - | ||
| 88 | - | // Theme synchronization | |
| 89 | - | syncThemeState(); | |
| 90 | - | themeObserver = new MutationObserver(syncThemeState); | |
| 91 | - | themeObserver.observe(document.documentElement, { | |
| 92 | - | attributes: true, | |
| 93 | - | attributeFilter: ['class'], | |
| 94 | - | }); | |
| 95 | - | }); | |
| 96 | - | ||
| 97 | - | onDestroy(() => { | |
| 98 | - | cleanupKeyboard?.(); | |
| 99 | - | themeObserver?.disconnect(); | |
| 100 | - | themeObserver = null; | |
| 101 | - | homepageState.reset(); | |
| 102 | - | }); | |
| 103 | - | </script> | |
| 104 | - | ||
| 105 | - | <svelte:head> | |
| 106 | - | <title>dumb://home</title> | |
| 107 | - | <meta name="description" content="Dumber Browser - Homepage" /> | |
| 108 | - | <!-- eslint-disable-next-line svelte/no-at-html-tags -- trusted static styles for shell initialization --> | |
| 109 | - | {@html `<style> | |
| 110 | - | html, body { margin: 0; padding: 0; } | |
| 111 | - | html { background: var(--background, #0a0a0a); } | |
| 112 | - | body { background: var(--background, #0a0a0a); color: var(--foreground, #e5e5e5); } | |
| 113 | - | /* Disable GTK/WebKit default focus rings - we handle focus styling ourselves */ | |
| 114 | - | *:focus { outline: none !important; } | |
| 115 | - | *:focus-visible { outline: none !important; } | |
| 116 | - | button:focus, input:focus, a:focus { outline: none !important; box-shadow: none !important; } | |
| 117 | - | /* Enable touch scrolling for WebKit */ | |
| 118 | - | * { -webkit-overflow-scrolling: touch; } | |
| 119 | - | </style>`} | |
| 120 | - | </svelte:head> | |
| 121 | - | ||
| 122 | - | <div class="homepage-shell"> | |
| 123 | - | <!-- Terminal Frame --> | |
| 124 | - | <div class="terminal-frame"> | |
| 125 | - | <!-- Header --> | |
| 126 | - | <header class="terminal-header"> | |
| 127 | - | <div class="header-left"> | |
| 128 | - | <span class="terminal-path">dumb://home</span> | |
| 129 | - | <div class="header-tabs" role="tablist" aria-label="Homepage panels"> | |
| 130 | - | {#each panelTabs as tab, index (tab.id)} | |
| 131 | - | <button | |
| 132 | - | class="tab-button" | |
| 133 | - | class:active={homepageState.activePanel === tab.id} | |
| 134 | - | onclick={() => homepageState.setActivePanel(tab.id)} | |
| 135 | - | onkeydown={(e) => { | |
| 136 | - | switch (e.key) { | |
| 137 | - | case 'Enter': | |
| 138 | - | case ' ': | |
| 139 | - | e.preventDefault(); | |
| 140 | - | homepageState.setActivePanel(tab.id); | |
| 141 | - | break; | |
| 142 | - | case 'ArrowLeft': | |
| 143 | - | case 'ArrowUp': | |
| 144 | - | e.preventDefault(); | |
| 145 | - | { | |
| 146 | - | const prevIndex = index === 0 ? panelTabs.length - 1 : index - 1; | |
| 147 | - | const prevTab = panelTabs[prevIndex]; | |
| 148 | - | if (prevTab) { | |
| 149 | - | homepageState.setActivePanel(prevTab.id); | |
| 150 | - | (e.currentTarget.parentElement?.children[prevIndex] as HTMLElement)?.focus(); | |
| 151 | - | } | |
| 152 | - | } | |
| 153 | - | break; | |
| 154 | - | case 'ArrowRight': | |
| 155 | - | case 'ArrowDown': | |
| 156 | - | e.preventDefault(); | |
| 157 | - | { | |
| 158 | - | const nextIndex = index === panelTabs.length - 1 ? 0 : index + 1; | |
| 159 | - | const nextTab = panelTabs[nextIndex]; | |
| 160 | - | if (nextTab) { | |
| 161 | - | homepageState.setActivePanel(nextTab.id); | |
| 162 | - | (e.currentTarget.parentElement?.children[nextIndex] as HTMLElement)?.focus(); | |
| 163 | - | } | |
| 164 | - | } | |
| 165 | - | break; | |
| 166 | - | } | |
| 167 | - | }} | |
| 168 | - | type="button" | |
| 169 | - | role="tab" | |
| 170 | - | aria-selected={homepageState.activePanel === tab.id} | |
| 171 | - | tabindex={homepageState.activePanel === tab.id ? 0 : -1} | |
| 172 | - | > | |
| 173 | - | <tab.icon size={14} strokeWidth={2} class="tab-icon" /> | |
| 174 | - | <span class="tab-label">{tab.label}</span> | |
| 175 | - | </button> | |
| 176 | - | {/each} | |
| 177 | - | </div> | |
| 178 | - | </div> | |
| 179 | - | <div class="header-right"> | |
| 180 | - | <button | |
| 181 | - | class="action-button" | |
| 182 | - | type="button" | |
| 183 | - | onclick={() => homepageState.openCommandPalette()} | |
| 184 | - | title="Command Palette (Ctrl+P or /)" | |
| 185 | - | > | |
| 186 | - | <Search size={14} strokeWidth={2} /> | |
| 187 | - | <span class="button-text">CMD</span> | |
| 188 | - | <kbd class="kbd-hint">/</kbd> | |
| 189 | - | </button> | |
| 190 | - | <button | |
| 191 | - | class="action-button" | |
| 192 | - | type="button" | |
| 193 | - | onclick={() => navigateWithViewTransition('dumb://config')} | |
| 194 | - | title="Settings" | |
| 195 | - | > | |
| 196 | - | <Settings size={14} strokeWidth={2} /> | |
| 197 | - | <span class="button-text">CFG</span> | |
| 198 | - | </button> | |
| 199 | - | <button | |
| 200 | - | class="theme-toggle" | |
| 201 | - | type="button" | |
| 202 | - | onclick={toggleTheme} | |
| 203 | - | title={themeMode === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'} | |
| 204 | - | aria-pressed={themeMode === 'dark'} | |
| 205 | - | > | |
| 206 | - | {#if themeMode === 'dark'} | |
| 207 | - | <Moon size={18} strokeWidth={2} /> | |
| 208 | - | {:else} | |
| 209 | - | <Sun size={18} strokeWidth={2} /> | |
| 210 | - | {/if} | |
| 211 | - | </button> | |
| 212 | - | </div> | |
| 213 | - | </header> | |
| 214 | - | ||
| 215 | - | <!-- Main Content Area --> | |
| 216 | - | <main class="terminal-body scrollable"> | |
| 217 | - | {#if children} | |
| 218 | - | {@render children()} | |
| 219 | - | {:else} | |
| 220 | - | <div class="empty-shell"> | |
| 221 | - | <span class="shell-prompt">$</span> | |
| 222 | - | <span class="shell-cursor">_</span> | |
| 223 | - | </div> | |
| 224 | - | {/if} | |
| 225 | - | </main> | |
| 226 | - | ||
| 227 | - | <!-- Status Bar --> | |
| 228 | - | <StatusBar /> | |
| 229 | - | </div> | |
| 230 | - | ||
| 231 | - | <!-- Keyboard Hints Overlay --> | |
| 232 | - | <KeyboardHints /> | |
| 233 | - | ||
| 234 | - | <!-- Command Palette Modal --> | |
| 235 | - | {#if homepageState.commandPaletteOpen} | |
| 236 | - | <CommandPalette /> | |
| 237 | - | {/if} | |
| 238 | - | ||
| 239 | - | <!-- Confirmation Modal --> | |
| 240 | - | {#if homepageState.confirmModalOpen} | |
| 241 | - | <div | |
| 242 | - | class="modal-overlay" | |
| 243 | - | onclick={() => homepageState.cancelConfirm()} | |
| 244 | - | onkeydown={(e) => { if (e.key === 'Escape') homepageState.cancelConfirm(); }} | |
| 245 | - | role="button" | |
| 246 | - | tabindex="0" | |
| 247 | - | aria-label="Close confirmation modal" | |
| 248 | - | > | |
| 249 | - | <div class="confirm-modal" onclick={(e) => e.stopPropagation()} role="presentation"> | |
| 250 | - | <div class="modal-header"> | |
| 251 | - | <span class="modal-icon"></span> | |
| 252 | - | <span class="modal-title">CONFIRM</span> | |
| 253 | - | </div> | |
| 254 | - | <p class="modal-message">{homepageState.confirmModalMessage}</p> | |
| 255 | - | <div class="modal-actions"> | |
| 256 | - | <button | |
| 257 | - | class="modal-btn modal-btn-cancel" | |
| 258 | - | type="button" | |
| 259 | - | onclick={() => homepageState.cancelConfirm()} | |
| 260 | - | > | |
| 261 | - | CANCEL | |
| 262 | - | <kbd>Esc</kbd> | |
| 263 | - | </button> | |
| 264 | - | <button | |
| 265 | - | class="modal-btn modal-btn-confirm" | |
| 266 | - | type="button" | |
| 267 | - | onclick={() => homepageState.confirmAction()} | |
| 268 | - | > | |
| 269 | - | CONFIRM | |
| 270 | - | <kbd>Enter</kbd> | |
| 271 | - | </button> | |
| 272 | - | </div> | |
| 273 | - | </div> | |
| 274 | - | </div> | |
| 275 | - | {/if} | |
| 276 | - | </div> | |
| 277 | - | ||
| 278 | - | <style> | |
| 279 | - | :global(html), | |
| 280 | - | :global(body) { | |
| 281 | - | height: 100%; | |
| 282 | - | } | |
| 283 | - | ||
| 284 | - | :global(body) { | |
| 285 | - | overflow: hidden; | |
| 286 | - | overscroll-behavior: contain; | |
| 287 | - | } | |
| 288 | - | ||
| 289 | - | .homepage-shell { | |
| 290 | - | height: 100vh; | |
| 291 | - | min-height: 100vh; | |
| 292 | - | display: flex; | |
| 293 | - | flex-direction: column; | |
| 294 | - | font-family: ui-monospace, 'Fira Code', 'Cascadia Code', Menlo, Monaco, Consolas, monospace; | |
| 295 | - | line-height: 1.5; | |
| 296 | - | color: var(--foreground); | |
| 297 | - | background: var(--background); | |
| 298 | - | overflow: hidden; | |
| 299 | - | } | |
| 300 | - | ||
| 301 | - | .terminal-frame { | |
| 302 | - | flex: 1; | |
| 303 | - | display: grid; | |
| 304 | - | grid-template-rows: auto 1fr auto; | |
| 305 | - | width: 100%; | |
| 306 | - | height: 100%; | |
| 307 | - | background: color-mix(in srgb, var(--card) 60%, var(--background) 40%); | |
| 308 | - | } | |
| 309 | - | ||
| 310 | - | /* Dark mode: subtle shadow */ | |
| 311 | - | :global(.dark) .terminal-frame { | |
| 312 | - | box-shadow: 0 24px 48px -12px rgb(0 0 0 / 0.5); | |
| 313 | - | } | |
| 314 | - | ||
| 315 | - | /* Header */ | |
| 316 | - | .terminal-header { | |
| 317 | - | display: flex; | |
| 318 | - | align-items: center; | |
| 319 | - | justify-content: space-between; | |
| 320 | - | gap: 1rem; | |
| 321 | - | padding: 0.75rem 1rem; | |
| 322 | - | border-bottom: 1px solid var(--border); | |
| 323 | - | background: color-mix(in srgb, var(--background) 90%, var(--card) 10%); | |
| 324 | - | position: sticky; | |
| 325 | - | top: 0; | |
| 326 | - | z-index: 5; | |
| 327 | - | } | |
| 328 | - | ||
| 329 | - | .header-left { | |
| 330 | - | display: flex; | |
| 331 | - | align-items: center; | |
| 332 | - | gap: 1.5rem; | |
| 333 | - | } | |
| 334 | - | ||
| 335 | - | .terminal-path { | |
| 336 | - | font-size: 0.8rem; | |
| 337 | - | font-weight: 600; | |
| 338 | - | color: var(--primary, #4ade80); | |
| 339 | - | letter-spacing: 0.05em; | |
| 340 | - | } | |
| 341 | - | ||
| 342 | - | .header-tabs { | |
| 343 | - | display: flex; | |
| 344 | - | gap: 0.25rem; | |
| 345 | - | } | |
| 346 | - | ||
| 347 | - | .tab-button { | |
| 348 | - | display: flex; | |
| 349 | - | align-items: center; | |
| 350 | - | gap: 0.4rem; | |
| 351 | - | padding: 0.4rem 0.75rem; | |
| 352 | - | font-size: 0.7rem; | |
| 353 | - | font-weight: 500; | |
| 354 | - | letter-spacing: 0.1em; | |
| 355 | - | color: var(--muted-foreground); | |
| 356 | - | background: transparent; | |
| 357 | - | border: 1px solid transparent; | |
| 358 | - | cursor: pointer; | |
| 359 | - | transition: all 120ms ease; | |
| 360 | - | } | |
| 361 | - | ||
| 362 | - | .tab-button:hover { | |
| 363 | - | color: var(--foreground); | |
| 364 | - | background: color-mix(in srgb, var(--card) 50%, transparent); | |
| 365 | - | } | |
| 366 | - | ||
| 367 | - | .tab-button:focus-visible { | |
| 368 | - | color: var(--foreground); | |
| 369 | - | border-color: var(--primary, #4ade80); | |
| 370 | - | background: color-mix(in srgb, var(--primary, #4ade80) 15%, transparent); | |
| 371 | - | } | |
| 372 | - | ||
| 373 | - | .tab-button.active { | |
| 374 | - | color: var(--foreground); | |
| 375 | - | background: var(--card); | |
| 376 | - | border-color: var(--border); | |
| 377 | - | } | |
| 378 | - | ||
| 379 | - | .header-right { | |
| 380 | - | display: flex; | |
| 381 | - | align-items: center; | |
| 382 | - | gap: 0.5rem; | |
| 383 | - | } | |
| 384 | - | ||
| 385 | - | .action-button { | |
| 386 | - | display: flex; | |
| 387 | - | align-items: center; | |
| 388 | - | gap: 0.5rem; | |
| 389 | - | padding: 0.4rem 0.75rem; | |
| 390 | - | font-size: 0.7rem; | |
| 391 | - | font-weight: 500; | |
| 392 | - | letter-spacing: 0.08em; | |
| 393 | - | color: var(--muted-foreground); | |
| 394 | - | background: transparent; | |
| 395 | - | border-width: 1px; | |
| 396 | - | border-style: solid; | |
| 397 | - | border-color: var(--border); | |
| 398 | - | cursor: pointer; | |
| 399 | - | transition: all 120ms ease; | |
| 400 | - | } | |
| 401 | - | ||
| 402 | - | .action-button:hover, | |
| 403 | - | .action-button:focus-visible { | |
| 404 | - | color: var(--foreground); | |
| 405 | - | border-color: color-mix(in srgb, var(--border) 50%, var(--foreground) 50%); | |
| 406 | - | background: color-mix(in srgb, var(--card) 30%, transparent); | |
| 407 | - | outline: none; | |
| 408 | - | } | |
| 409 | - | ||
| 410 | - | .kbd-hint { | |
| 411 | - | padding: 0.15rem 0.35rem; | |
| 412 | - | font-size: 0.65rem; | |
| 413 | - | color: var(--muted-foreground); | |
| 414 | - | background: var(--background); | |
| 415 | - | border-width: 1px; | |
| 416 | - | border-style: solid; | |
| 417 | - | border-color: var(--border); | |
| 418 | - | margin-left: 0.25rem; | |
| 419 | - | } | |
| 420 | - | ||
| 421 | - | .theme-toggle { | |
| 422 | - | display: flex; | |
| 423 | - | align-items: center; | |
| 424 | - | justify-content: center; | |
| 425 | - | width: 2.25rem; | |
| 426 | - | height: 2.25rem; | |
| 427 | - | color: var(--muted-foreground); | |
| 428 | - | background: transparent; | |
| 429 | - | border-width: 1px; | |
| 430 | - | border-style: solid; | |
| 431 | - | border-color: var(--border); | |
| 432 | - | cursor: pointer; | |
| 433 | - | transition: all 120ms ease; | |
| 434 | - | } | |
| 435 | - | ||
| 436 | - | .theme-toggle:hover, | |
| 437 | - | .theme-toggle:focus-visible { | |
| 438 | - | color: var(--foreground); | |
| 439 | - | border-color: color-mix(in srgb, var(--border) 50%, var(--foreground) 50%); | |
| 440 | - | outline: none; | |
| 441 | - | } | |
| 442 | - | ||
| 443 | - | .theme-toggle :global(svg) { | |
| 444 | - | flex-shrink: 0; | |
| 445 | - | } | |
| 446 | - | ||
| 447 | - | /* Main Content */ | |
| 448 | - | .terminal-body { | |
| 449 | - | display: flex; | |
| 450 | - | flex-direction: column; | |
| 451 | - | min-height: 0; | |
| 452 | - | overflow: hidden; | |
| 453 | - | background: var(--background); | |
| 454 | - | } | |
| 455 | - | ||
| 456 | - | .terminal-body.scrollable { | |
| 457 | - | overflow-y: auto; | |
| 458 | - | -webkit-overflow-scrolling: touch; | |
| 459 | - | touch-action: pan-y; | |
| 460 | - | } | |
| 461 | - | ||
| 462 | - | .empty-shell { | |
| 463 | - | flex: 1; | |
| 464 | - | display: flex; | |
| 465 | - | align-items: center; | |
| 466 | - | justify-content: center; | |
| 467 | - | gap: 0.5rem; | |
| 468 | - | color: var(--muted-foreground); | |
| 469 | - | font-size: 1.25rem; | |
| 470 | - | } | |
| 471 | - | ||
| 472 | - | .shell-prompt { | |
| 473 | - | color: var(--primary, #4ade80); | |
| 474 | - | } | |
| 475 | - | ||
| 476 | - | .shell-cursor { | |
| 477 | - | animation: blink 1s step-end infinite; | |
| 478 | - | } | |
| 479 | - | ||
| 480 | - | @keyframes blink { | |
| 481 | - | 50% { opacity: 0; } | |
| 482 | - | } | |
| 483 | - | ||
| 484 | - | /* Modal Overlay */ | |
| 485 | - | .modal-overlay { | |
| 486 | - | position: fixed; | |
| 487 | - | inset: 0; | |
| 488 | - | z-index: 100; | |
| 489 | - | display: flex; | |
| 490 | - | align-items: center; | |
| 491 | - | justify-content: center; | |
| 492 | - | background: rgb(0 0 0 / 0.7); | |
| 493 | - | backdrop-filter: blur(4px); | |
| 494 | - | animation: fade-in 150ms ease; | |
| 495 | - | } | |
| 496 | - | ||
| 497 | - | @keyframes fade-in { | |
| 498 | - | from { opacity: 0; } | |
| 499 | - | } | |
| 500 | - | ||
| 501 | - | .confirm-modal { | |
| 502 | - | width: 100%; | |
| 503 | - | max-width: 420px; | |
| 504 | - | margin: 1rem; | |
| 505 | - | background: var(--card); | |
| 506 | - | border-width: 1px; | |
| 507 | - | border-style: solid; | |
| 508 | - | border-color: var(--border); | |
| 509 | - | box-shadow: 0 24px 48px -12px rgb(0 0 0 / 0.6); | |
| 510 | - | animation: modal-in 200ms ease; | |
| 511 | - | } | |
| 512 | - | ||
| 513 | - | @keyframes modal-in { | |
| 514 | - | from { | |
| 515 | - | opacity: 0; | |
| 516 | - | transform: scale(0.95) translateY(-8px); | |
| 517 | - | } | |
| 518 | - | } | |
| 519 | - | ||
| 520 | - | .modal-header { | |
| 521 | - | display: flex; | |
| 522 | - | align-items: center; | |
| 523 | - | gap: 0.6rem; | |
| 524 | - | padding: 0.85rem 1rem; | |
| 525 | - | border-bottom-width: 1px; | |
| 526 | - | border-bottom-style: solid; | |
| 527 | - | border-bottom-color: var(--border); | |
| 528 | - | background: color-mix(in srgb, var(--background) 80%, transparent); | |
| 529 | - | } | |
| 530 | - | ||
| 531 | - | .modal-icon { | |
| 532 | - | font-size: 1rem; | |
| 533 | - | color: var(--warning); | |
| 534 | - | } | |
| 535 | - | ||
| 536 | - | .modal-title { | |
| 537 | - | font-size: 0.75rem; | |
| 538 | - | font-weight: 600; | |
| 539 | - | letter-spacing: 0.1em; | |
| 540 | - | color: var(--foreground); | |
| 541 | - | } | |
| 542 | - | ||
| 543 | - | .modal-message { | |
| 544 | - | margin: 0; | |
| 545 | - | padding: 1.25rem 1rem; | |
| 546 | - | font-size: 0.85rem; | |
| 547 | - | color: var(--foreground); | |
| 548 | - | line-height: 1.6; | |
| 549 | - | } | |
| 550 | - | ||
| 551 | - | .modal-actions { | |
| 552 | - | display: flex; | |
| 553 | - | gap: 0.5rem; | |
| 554 | - | padding: 0.85rem 1rem; | |
| 555 | - | border-top-width: 1px; | |
| 556 | - | border-top-style: solid; | |
| 557 | - | border-top-color: var(--border); | |
| 558 | - | background: color-mix(in srgb, var(--background) 50%, transparent); | |
| 559 | - | } | |
| 560 | - | ||
| 561 | - | .modal-btn { | |
| 562 | - | flex: 1; | |
| 563 | - | display: flex; | |
| 564 | - | align-items: center; | |
| 565 | - | justify-content: center; | |
| 566 | - | gap: 0.6rem; | |
| 567 | - | padding: 0.6rem 1rem; | |
| 568 | - | font-size: 0.72rem; | |
| 569 | - | font-weight: 500; | |
| 570 | - | letter-spacing: 0.08em; | |
| 571 | - | border-width: 1px; | |
| 572 | - | border-style: solid; | |
| 573 | - | border-color: var(--border); | |
| 574 | - | cursor: pointer; | |
| 575 | - | transition: all 120ms ease; | |
| 576 | - | } | |
| 577 | - | ||
| 578 | - | .modal-btn kbd { | |
| 579 | - | padding: 0.1rem 0.3rem; | |
| 580 | - | font-size: 0.6rem; | |
| 581 | - | background: var(--background); | |
| 582 | - | border-width: 1px; | |
| 583 | - | border-style: solid; | |
| 584 | - | border-color: var(--border); | |
| 585 | - | opacity: 0.7; | |
| 586 | - | } | |
| 587 | - | ||
| 588 | - | .modal-btn-cancel { | |
| 589 | - | color: var(--muted-foreground); | |
| 590 | - | background: transparent; | |
| 591 | - | } | |
| 592 | - | ||
| 593 | - | .modal-btn-cancel:hover { | |
| 594 | - | color: var(--foreground); | |
| 595 | - | background: color-mix(in srgb, var(--card) 40%, transparent); | |
| 596 | - | } | |
| 597 | - | ||
| 598 | - | .modal-btn-confirm { | |
| 599 | - | color: var(--destructive-foreground); | |
| 600 | - | background: var(--destructive); | |
| 601 | - | border-color: color-mix(in srgb, var(--destructive) 80%, black 20%); | |
| 602 | - | } | |
| 603 | - | ||
| 604 | - | .modal-btn-confirm:hover { | |
| 605 | - | background: color-mix(in srgb, var(--destructive) 85%, white 15%); | |
| 606 | - | } | |
| 607 | - | ||
| 608 | - | /* Responsive */ | |
| 609 | - | @media (max-width: 768px) { | |
| 610 | - | .terminal-header { | |
| 611 | - | flex-wrap: wrap; | |
| 612 | - | gap: 0.75rem; | |
| 613 | - | } | |
| 614 | - | ||
| 615 | - | .header-left { | |
| 616 | - | flex-wrap: wrap; | |
| 617 | - | gap: 0.75rem; | |
| 618 | - | } | |
| 619 | - | ||
| 620 | - | .header-tabs { | |
| 621 | - | order: 10; | |
| 622 | - | width: 100%; | |
| 623 | - | } | |
| 624 | - | ||
| 625 | - | .tab-button { | |
| 626 | - | flex: 1; | |
| 627 | - | justify-content: center; | |
| 628 | - | } | |
| 629 | - | ||
| 630 | - | .action-button .button-text { | |
| 631 | - | display: none; | |
| 632 | - | } | |
| 633 | - | } | |
| 634 | - | </style> | |
D webui/src/pages/homepage/shell/KeyboardHints.svelte +0 -447
| @@ -1,447 +0,0 @@ | |||
| 1 | - | <script lang="ts"> | |
| 2 | - | import { homepageState } from '../state.svelte'; | |
| 3 | - | import type { KeyboardHint } from '../keyboard'; | |
| 4 | - | ||
| 5 | - | // Toggle visibility with ? | |
| 6 | - | let showHints = $state(false); | |
| 7 | - | let hintTimeout: ReturnType<typeof setTimeout> | null = null; | |
| 8 | - | ||
| 9 | - | // Panel-specific hints | |
| 10 | - | const panelHints = $derived.by((): KeyboardHint[] => { | |
| 11 | - | switch (homepageState.activePanel) { | |
| 12 | - | case 'history': | |
| 13 | - | return [ | |
| 14 | - | { keys: ['d', 'x'], description: 'Delete entry' }, | |
| 15 | - | { keys: ['D h'], description: 'Clear last hour' }, | |
| 16 | - | { keys: ['D d'], description: 'Clear last day' }, | |
| 17 | - | { keys: ['D w'], description: 'Clear last week' }, | |
| 18 | - | { keys: ['D D'], description: 'Clear all history' }, | |
| 19 | - | { keys: ['D @'], description: 'Clear domain' }, | |
| 20 | - | { keys: ['p'], description: 'Pin to favorites' }, | |
| 21 | - | ]; | |
| 22 | - | case 'favorites': | |
| 23 | - | return [ | |
| 24 | - | { keys: ['e'], description: 'Edit favorite' }, | |
| 25 | - | { keys: ['t'], description: 'Manage tags' }, | |
| 26 | - | { keys: ['m'], description: 'Move to folder' }, | |
| 27 | - | { keys: ['1-9'], description: 'Set shortcut key' }, | |
| 28 | - | { keys: ['d'], description: 'Delete favorite' }, | |
| 29 | - | ]; | |
| 30 | - | case 'analytics': | |
| 31 | - | return [ | |
| 32 | - | { keys: ['r'], description: 'Refresh stats' }, | |
| 33 | - | { keys: ['D @'], description: 'Clear domain' }, | |
| 34 | - | ]; | |
| 35 | - | default: | |
| 36 | - | return []; | |
| 37 | - | } | |
| 38 | - | }); | |
| 39 | - | ||
| 40 | - | // Navigation hints (always shown) | |
| 41 | - | const navHints: KeyboardHint[] = [ | |
| 42 | - | { keys: ['j', '↓'], description: 'Move down' }, | |
| 43 | - | { keys: ['k', '↑'], description: 'Move up' }, | |
| 44 | - | { keys: ['g g'], description: 'Go to first' }, | |
| 45 | - | { keys: ['G'], description: 'Go to last' }, | |
| 46 | - | { keys: ['Enter', 'o'], description: 'Open item' }, | |
| 47 | - | { keys: ['/'], description: 'Search / Command' }, | |
| 48 | - | { keys: ['Esc'], description: 'Close / Cancel' }, | |
| 49 | - | ]; | |
| 50 | - | ||
| 51 | - | // Panel switch hints | |
| 52 | - | const panelSwitchHints: KeyboardHint[] = [ | |
| 53 | - | { keys: ['g h'], description: 'History panel' }, | |
| 54 | - | { keys: ['g f'], description: 'Favorites panel' }, | |
| 55 | - | { keys: ['g a'], description: 'Analytics panel' }, | |
| 56 | - | ]; | |
| 57 | - | ||
| 58 | - | // Quick access hints | |
| 59 | - | const quickAccessHints: KeyboardHint[] = [ | |
| 60 | - | { keys: ['1-9'], description: 'Quick navigate to shortcut' }, | |
| 61 | - | ]; | |
| 62 | - | ||
| 63 | - | // Handle keyboard toggle | |
| 64 | - | function handleKeydown(e: KeyboardEvent) { | |
| 65 | - | if (e.key === '?' && !e.ctrlKey && !e.altKey && !e.metaKey) { | |
| 66 | - | // Don't trigger if in input | |
| 67 | - | if ((e.target as HTMLElement).tagName === 'INPUT') return; | |
| 68 | - | // Don't trigger if command palette is open | |
| 69 | - | if (homepageState.commandPaletteOpen) return; | |
| 70 | - | ||
| 71 | - | e.preventDefault(); | |
| 72 | - | toggleHints(); | |
| 73 | - | } | |
| 74 | - | } | |
| 75 | - | ||
| 76 | - | function toggleHints() { | |
| 77 | - | showHints = !showHints; | |
| 78 | - | ||
| 79 | - | // Auto-hide after 10 seconds | |
| 80 | - | if (showHints) { | |
| 81 | - | if (hintTimeout) clearTimeout(hintTimeout); | |
| 82 | - | hintTimeout = setTimeout(() => { | |
| 83 | - | showHints = false; | |
| 84 | - | }, 10000); | |
| 85 | - | } else { | |
| 86 | - | if (hintTimeout) { | |
| 87 | - | clearTimeout(hintTimeout); | |
| 88 | - | hintTimeout = null; | |
| 89 | - | } | |
| 90 | - | } | |
| 91 | - | } | |
| 92 | - | ||
| 93 | - | function closeHints() { | |
| 94 | - | showHints = false; | |
| 95 | - | if (hintTimeout) { | |
| 96 | - | clearTimeout(hintTimeout); | |
| 97 | - | hintTimeout = null; | |
| 98 | - | } | |
| 99 | - | } | |
| 100 | - | ||
| 101 | - | // Cleanup on destroy | |
| 102 | - | import { onDestroy } from 'svelte'; | |
| 103 | - | onDestroy(() => { | |
| 104 | - | if (hintTimeout) clearTimeout(hintTimeout); | |
| 105 | - | }); | |
| 106 | - | </script> | |
| 107 | - | ||
| 108 | - | <svelte:document onkeydown={handleKeydown} /> | |
| 109 | - | ||
| 110 | - | {#if showHints} | |
| 111 | - | <div | |
| 112 | - | class="hints-overlay" | |
| 113 | - | onclick={closeHints} | |
| 114 | - | onkeydown={(e) => { if (e.key === 'Escape' || e.key === 'Enter') closeHints(); }} | |
| 115 | - | role="button" | |
| 116 | - | tabindex="0" | |
| 117 | - | aria-label="Close keyboard hints" | |
| 118 | - | > | |
| 119 | - | <div class="hints-panel" onclick={(e) => e.stopPropagation()} role="presentation"> | |
| 120 | - | <div class="hints-header"> | |
| 121 | - | <span class="hints-icon"></span> | |
| 122 | - | <span class="hints-title">KEYBOARD SHORTCUTS</span> | |
| 123 | - | <button class="hints-close" onclick={closeHints} type="button"> | |
| 124 | - | <span>×</span> | |
| 125 | - | </button> | |
| 126 | - | </div> | |
| 127 | - | ||
| 128 | - | <div class="hints-body"> | |
| 129 | - | <!-- Navigation Section --> | |
| 130 | - | <section class="hints-section"> | |
| 131 | - | <h3 class="section-title"> | |
| 132 | - | <span class="section-icon"></span> | |
| 133 | - | Navigation | |
| 134 | - | </h3> | |
| 135 | - | <div class="hints-grid"> | |
| 136 | - | {#each navHints as hint (hint.description)} | |
| 137 | - | <div class="hint-row"> | |
| 138 | - | <div class="hint-keys"> | |
| 139 | - | {#each hint.keys as key, i (key)} | |
| 140 | - | {#if i > 0}<span class="key-sep">/</span>{/if} | |
| 141 | - | <kbd>{key}</kbd> | |
| 142 | - | {/each} | |
| 143 | - | </div> | |
| 144 | - | <span class="hint-desc">{hint.description}</span> | |
| 145 | - | </div> | |
| 146 | - | {/each} | |
| 147 | - | </div> | |
| 148 | - | </section> | |
| 149 | - | ||
| 150 | - | <!-- Panel Switch Section --> | |
| 151 | - | <section class="hints-section"> | |
| 152 | - | <h3 class="section-title"> | |
| 153 | - | <span class="section-icon"></span> | |
| 154 | - | Panels | |
| 155 | - | </h3> | |
| 156 | - | <div class="hints-grid"> | |
| 157 | - | {#each panelSwitchHints as hint (hint.description)} | |
| 158 | - | <div class="hint-row"> | |
| 159 | - | <div class="hint-keys"> | |
| 160 | - | {#each hint.keys as key, i (key)} | |
| 161 | - | {#if i > 0}<span class="key-sep">/</span>{/if} | |
| 162 | - | <kbd>{key}</kbd> | |
| 163 | - | {/each} | |
| 164 | - | </div> | |
| 165 | - | <span class="hint-desc">{hint.description}</span> | |
| 166 | - | </div> | |
| 167 | - | {/each} | |
| 168 | - | </div> | |
| 169 | - | </section> | |
| 170 | - | ||
| 171 | - | <!-- Current Panel Section --> | |
| 172 | - | {#if panelHints.length > 0} | |
| 173 | - | <section class="hints-section panel-specific"> | |
| 174 | - | <h3 class="section-title"> | |
| 175 | - | <span class="section-icon"></span> | |
| 176 | - | {homepageState.activePanel.charAt(0).toUpperCase() + homepageState.activePanel.slice(1)} Actions | |
| 177 | - | </h3> | |
| 178 | - | <div class="hints-grid"> | |
| 179 | - | {#each panelHints as hint (hint.description)} | |
| 180 | - | <div class="hint-row"> | |
| 181 | - | <div class="hint-keys"> | |
| 182 | - | {#each hint.keys as key, i (key)} | |
| 183 | - | {#if i > 0}<span class="key-sep">/</span>{/if} | |
| 184 | - | <kbd>{key}</kbd> | |
| 185 | - | {/each} | |
| 186 | - | </div> | |
| 187 | - | <span class="hint-desc">{hint.description}</span> | |
| 188 | - | </div> | |
| 189 | - | {/each} | |
| 190 | - | </div> | |
| 191 | - | </section> | |
| 192 | - | {/if} | |
| 193 | - | ||
| 194 | - | <!-- Quick Access Section --> | |
| 195 | - | <section class="hints-section"> | |
| 196 | - | <h3 class="section-title"> | |
| 197 | - | <span class="section-icon"></span> | |
| 198 | - | Quick Access | |
| 199 | - | </h3> | |
| 200 | - | <div class="hints-grid"> | |
| 201 | - | {#each quickAccessHints as hint (hint.description)} | |
| 202 | - | <div class="hint-row"> | |
| 203 | - | <div class="hint-keys"> | |
| 204 | - | {#each hint.keys as key, i (key)} | |
| 205 | - | {#if i > 0}<span class="key-sep">/</span>{/if} | |
| 206 | - | <kbd>{key}</kbd> | |
| 207 | - | {/each} | |
| 208 | - | </div> | |
| 209 | - | <span class="hint-desc">{hint.description}</span> | |
| 210 | - | </div> | |
| 211 | - | {/each} | |
| 212 | - | </div> | |
| 213 | - | </section> | |
| 214 | - | </div> | |
| 215 | - | ||
| 216 | - | <div class="hints-footer"> | |
| 217 | - | <span class="footer-hint">Press <kbd>?</kbd> to toggle hints</span> | |
| 218 | - | </div> | |
| 219 | - | </div> | |
| 220 | - | </div> | |
| 221 | - | {/if} | |
| 222 | - | ||
| 223 | - | <style> | |
| 224 | - | .hints-overlay { | |
| 225 | - | position: fixed; | |
| 226 | - | inset: 0; | |
| 227 | - | z-index: 150; | |
| 228 | - | display: flex; | |
| 229 | - | align-items: center; | |
| 230 | - | justify-content: center; | |
| 231 | - | background: rgb(0 0 0 / 0.6); | |
| 232 | - | backdrop-filter: blur(4px); | |
| 233 | - | animation: fade-in 150ms ease; | |
| 234 | - | } | |
| 235 | - | ||
| 236 | - | @keyframes fade-in { | |
| 237 | - | from { opacity: 0; } | |
| 238 | - | } | |
| 239 | - | ||
| 240 | - | .hints-panel { | |
| 241 | - | width: 100%; | |
| 242 | - | max-width: 540px; | |
| 243 | - | max-height: 85vh; | |
| 244 | - | margin: 1rem; | |
| 245 | - | display: flex; | |
| 246 | - | flex-direction: column; | |
| 247 | - | background: var(--card); | |
| 248 | - | border-width: 1px; | |
| 249 | - | border-style: solid; | |
| 250 | - | border-color: var(--border); | |
| 251 | - | box-shadow: 0 24px 48px -12px rgb(0 0 0 / 0.6); | |
| 252 | - | animation: panel-in 200ms cubic-bezier(0.16, 1, 0.3, 1); | |
| 253 | - | overflow: hidden; | |
| 254 | - | } | |
| 255 | - | ||
| 256 | - | @keyframes panel-in { | |
| 257 | - | from { | |
| 258 | - | opacity: 0; | |
| 259 | - | transform: scale(0.95); | |
| 260 | - | } | |
| 261 | - | } | |
| 262 | - | ||
| 263 | - | /* Header */ | |
| 264 | - | .hints-header { | |
| 265 | - | display: flex; | |
| 266 | - | align-items: center; | |
| 267 | - | gap: 0.6rem; | |
| 268 | - | padding: 0.85rem 1rem; | |
| 269 | - | border-bottom: 1px solid var(--border); | |
| 270 | - | background: color-mix(in srgb, var(--background) 80%, transparent); | |
| 271 | - | } | |
| 272 | - | ||
| 273 | - | .hints-icon { | |
| 274 | - | font-size: 1rem; | |
| 275 | - | color: var(--primary, #4ade80); | |
| 276 | - | } | |
| 277 | - | ||
| 278 | - | .hints-title { | |
| 279 | - | flex: 1; | |
| 280 | - | font-size: 0.75rem; | |
| 281 | - | font-weight: 600; | |
| 282 | - | letter-spacing: 0.1em; | |
| 283 | - | color: var(--foreground); | |
| 284 | - | } | |
| 285 | - | ||
| 286 | - | .hints-close { | |
| 287 | - | display: flex; | |
| 288 | - | align-items: center; | |
| 289 | - | justify-content: center; | |
| 290 | - | width: 1.75rem; | |
| 291 | - | height: 1.75rem; | |
| 292 | - | font-size: 1.25rem; | |
| 293 | - | color: var(--muted-foreground); | |
| 294 | - | background: transparent; | |
| 295 | - | border-width: 1px; | |
| 296 | - | border-style: solid; | |
| 297 | - | border-color: var(--border); | |
| 298 | - | cursor: pointer; | |
| 299 | - | transition: all 120ms ease; | |
| 300 | - | } | |
| 301 | - | ||
| 302 | - | .hints-close:hover { | |
| 303 | - | color: var(--foreground); | |
| 304 | - | border-color: color-mix(in srgb, var(--border) 50%, var(--foreground) 50%); | |
| 305 | - | } | |
| 306 | - | ||
| 307 | - | /* Body */ | |
| 308 | - | .hints-body { | |
| 309 | - | flex: 1; | |
| 310 | - | overflow-y: auto; | |
| 311 | - | padding: 1rem; | |
| 312 | - | display: flex; | |
| 313 | - | flex-direction: column; | |
| 314 | - | gap: 1.25rem; | |
| 315 | - | } | |
| 316 | - | ||
| 317 | - | .hints-section { | |
| 318 | - | display: flex; | |
| 319 | - | flex-direction: column; | |
| 320 | - | gap: 0.6rem; | |
| 321 | - | } | |
| 322 | - | ||
| 323 | - | .hints-section.panel-specific { | |
| 324 | - | padding: 0.75rem; | |
| 325 | - | background: color-mix(in srgb, var(--primary, #4ade80) 8%, transparent); | |
| 326 | - | border: 1px solid color-mix(in srgb, var(--primary, #4ade80) 25%, var(--border) 75%); | |
| 327 | - | } | |
| 328 | - | ||
| 329 | - | .section-title { | |
| 330 | - | display: flex; | |
| 331 | - | align-items: center; | |
| 332 | - | gap: 0.5rem; | |
| 333 | - | margin: 0; | |
| 334 | - | font-size: 0.72rem; | |
| 335 | - | font-weight: 600; | |
| 336 | - | text-transform: uppercase; | |
| 337 | - | letter-spacing: 0.1em; | |
| 338 | - | color: var(--foreground); | |
| 339 | - | } | |
| 340 | - | ||
| 341 | - | .section-icon { | |
| 342 | - | font-size: 0.85rem; | |
| 343 | - | color: var(--muted-foreground); | |
| 344 | - | } | |
| 345 | - | ||
| 346 | - | .panel-specific .section-icon { | |
| 347 | - | color: var(--primary, #4ade80); | |
| 348 | - | } | |
| 349 | - | ||
| 350 | - | .hints-grid { | |
| 351 | - | display: grid; | |
| 352 | - | grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); | |
| 353 | - | gap: 0.4rem; | |
| 354 | - | } | |
| 355 | - | ||
| 356 | - | .hint-row { | |
| 357 | - | display: flex; | |
| 358 | - | align-items: center; | |
| 359 | - | gap: 0.6rem; | |
| 360 | - | padding: 0.4rem 0.6rem; | |
| 361 | - | background: color-mix(in srgb, var(--background) 60%, transparent); | |
| 362 | - | border: 1px solid color-mix(in srgb, var(--border) 50%, transparent); | |
| 363 | - | } | |
| 364 | - | ||
| 365 | - | .hint-keys { | |
| 366 | - | display: flex; | |
| 367 | - | align-items: center; | |
| 368 | - | gap: 0.2rem; | |
| 369 | - | flex-shrink: 0; | |
| 370 | - | } | |
| 371 | - | ||
| 372 | - | .hint-keys kbd { | |
| 373 | - | padding: 0.2rem 0.4rem; | |
| 374 | - | font-size: 0.68rem; | |
| 375 | - | font-family: inherit; | |
| 376 | - | color: var(--foreground); | |
| 377 | - | background: var(--background); | |
| 378 | - | border-width: 1px; | |
| 379 | - | border-style: solid; | |
| 380 | - | border-color: var(--border); | |
| 381 | - | min-width: 1.4rem; | |
| 382 | - | text-align: center; | |
| 383 | - | } | |
| 384 | - | ||
| 385 | - | .key-sep { | |
| 386 | - | font-size: 0.6rem; | |
| 387 | - | color: var(--muted-foreground); | |
| 388 | - | opacity: 0.6; | |
| 389 | - | } | |
| 390 | - | ||
| 391 | - | .hint-desc { | |
| 392 | - | font-size: 0.72rem; | |
| 393 | - | color: var(--muted-foreground); | |
| 394 | - | white-space: nowrap; | |
| 395 | - | overflow: hidden; | |
| 396 | - | text-overflow: ellipsis; | |
| 397 | - | } | |
| 398 | - | ||
| 399 | - | /* Footer */ | |
| 400 | - | .hints-footer { | |
| 401 | - | display: flex; | |
| 402 | - | justify-content: center; | |
| 403 | - | padding: 0.65rem 1rem; | |
| 404 | - | border-top: 1px solid var(--border); | |
| 405 | - | background: color-mix(in srgb, var(--background) 70%, transparent); | |
| 406 | - | } | |
| 407 | - | ||
| 408 | - | .footer-hint { | |
| 409 | - | font-size: 0.68rem; | |
| 410 | - | color: var(--muted-foreground); | |
| 411 | - | letter-spacing: 0.05em; | |
| 412 | - | } | |
| 413 | - | ||
| 414 | - | .footer-hint kbd { | |
| 415 | - | padding: 0.15rem 0.35rem; | |
| 416 | - | font-size: 0.62rem; | |
| 417 | - | background: var(--background); | |
| 418 | - | border-width: 1px; | |
| 419 | - | border-style: solid; | |
| 420 | - | border-color: var(--border); | |
| 421 | - | margin: 0 0.25rem; | |
| 422 | - | } | |
| 423 | - | ||
| 424 | - | /* Scrollbar */ | |
| 425 | - | .hints-body::-webkit-scrollbar { | |
| 426 | - | width: 6px; | |
| 427 | - | } | |
| 428 | - | ||
| 429 | - | .hints-body::-webkit-scrollbar-track { | |
| 430 | - | background: transparent; | |
| 431 | - | } | |
| 432 | - | ||
| 433 | - | .hints-body::-webkit-scrollbar-thumb { | |
| 434 | - | background: var(--border); | |
| 435 | - | } | |
| 436 | - | ||
| 437 | - | /* Responsive */ | |
| 438 | - | @media (max-width: 480px) { | |
| 439 | - | .hints-grid { | |
| 440 | - | grid-template-columns: 1fr; | |
| 441 | - | } | |
| 442 | - | ||
| 443 | - | .hints-panel { | |
| 444 | - | max-height: 90vh; | |
| 445 | - | } | |
| 446 | - | } | |
| 447 | - | </style> | |
D webui/src/pages/homepage/shell/StatusBar.svelte +0 -416
| @@ -1,416 +0,0 @@ | |||
| 1 | - | <script lang="ts"> | |
| 2 | - | import { homepageState } from '../state.svelte'; | |
| 3 | - | import { History, Star, BarChart3, Terminal, Search, Folder, Tags, Command } from '@lucide/svelte'; | |
| 4 | - | ||
| 5 | - | // Derived status indicators | |
| 6 | - | const panelLabel = $derived({ | |
| 7 | - | history: 'HISTORY', | |
| 8 | - | favorites: 'FAVORITES', | |
| 9 | - | analytics: 'ANALYTICS', | |
| 10 | - | }[homepageState.activePanel]); | |
| 11 | - | ||
| 12 | - | const panelIcon = $derived({ | |
| 13 | - | history: History, | |
| 14 | - | favorites: Star, | |
| 15 | - | analytics: BarChart3, | |
| 16 | - | }[homepageState.activePanel]); | |
| 17 | - | ||
| 18 | - | // Loading states | |
| 19 | - | const isLoading = $derived( | |
| 20 | - | homepageState.historyLoading || | |
| 21 | - | homepageState.favoritesLoading || | |
| 22 | - | homepageState.analyticsLoading | |
| 23 | - | ); | |
| 24 | - | ||
| 25 | - | // Current list stats | |
| 26 | - | const listStats = $derived.by(() => { | |
| 27 | - | switch (homepageState.activePanel) { | |
| 28 | - | case 'history': | |
| 29 | - | if (homepageState.historySearchQuery) { | |
| 30 | - | return { | |
| 31 | - | count: homepageState.historySearchResults.length, | |
| 32 | - | label: 'results', | |
| 33 | - | filtered: true, | |
| 34 | - | }; | |
| 35 | - | } | |
| 36 | - | return { | |
| 37 | - | count: homepageState.history.length, | |
| 38 | - | label: 'entries', | |
| 39 | - | filtered: false, | |
| 40 | - | }; | |
| 41 | - | case 'favorites': { | |
| 42 | - | const filtered = homepageState.filteredFavorites; | |
| 43 | - | const total = homepageState.favorites.length; | |
| 44 | - | return { | |
| 45 | - | count: filtered.length, | |
| 46 | - | label: filtered.length === total ? 'items' : `of ${total}`, | |
| 47 | - | filtered: filtered.length !== total, | |
| 48 | - | }; | |
| 49 | - | } | |
| 50 | - | case 'analytics': | |
| 51 | - | return { | |
| 52 | - | count: homepageState.domainStats.length, | |
| 53 | - | label: 'domains', | |
| 54 | - | filtered: false, | |
| 55 | - | }; | |
| 56 | - | default: | |
| 57 | - | return { count: 0, label: 'items', filtered: false }; | |
| 58 | - | } | |
| 59 | - | }); | |
| 60 | - | ||
| 61 | - | // Position in list | |
| 62 | - | const positionInfo = $derived.by(() => { | |
| 63 | - | const length = homepageState.currentListLength; | |
| 64 | - | if (length === 0) return null; | |
| 65 | - | ||
| 66 | - | const pos = homepageState.focusedIndex + 1; | |
| 67 | - | return { current: pos, total: length }; | |
| 68 | - | }); | |
| 69 | - | ||
| 70 | - | // Mode indicator | |
| 71 | - | const modeIndicator = $derived.by(() => { | |
| 72 | - | if (homepageState.commandPaletteOpen) { | |
| 73 | - | return { icon: Terminal, label: 'COMMAND', color: 'accent' }; | |
| 74 | - | } | |
| 75 | - | if (homepageState.historySearchQuery) { | |
| 76 | - | return { icon: Search, label: 'SEARCH', color: 'warning' }; | |
| 77 | - | } | |
| 78 | - | if (homepageState.selectedFolderId !== null) { | |
| 79 | - | const folder = homepageState.folders.find(f => f.id === homepageState.selectedFolderId); | |
| 80 | - | return { icon: Folder, label: folder?.name || 'FOLDER', color: 'info' }; | |
| 81 | - | } | |
| 82 | - | if (homepageState.selectedTagIds.length > 0) { | |
| 83 | - | return { icon: Tags, label: `${homepageState.selectedTagIds.length} TAGS`, color: 'info' }; | |
| 84 | - | } | |
| 85 | - | return null; | |
| 86 | - | }); | |
| 87 | - | </script> | |
| 88 | - | ||
| 89 | - | <footer class="status-bar"> | |
| 90 | - | <!-- Left Section: Mode & Panel --> | |
| 91 | - | <div class="status-left"> | |
| 92 | - | <!-- Pending Key Prefix Indicator --> | |
| 93 | - | {#if homepageState.pendingKeyPrefix} | |
| 94 | - | <div class="status-chip prefix-chip" data-color="warning"> | |
| 95 | - | <span class="chip-icon"> | |
| 96 | - | <Command size={12} strokeWidth={2} /> | |
| 97 | - | </span> | |
| 98 | - | <kbd class="prefix-key">{homepageState.pendingKeyPrefix}</kbd> | |
| 99 | - | <span class="prefix-waiting">...</span> | |
| 100 | - | </div> | |
| 101 | - | {/if} | |
| 102 | - | ||
| 103 | - | <!-- Mode Indicator --> | |
| 104 | - | {#if modeIndicator} | |
| 105 | - | {@const ModeIcon = modeIndicator.icon} | |
| 106 | - | <div class="status-chip mode-chip" data-color={modeIndicator.color}> | |
| 107 | - | <span class="chip-icon"> | |
| 108 | - | <ModeIcon size={12} strokeWidth={2} /> | |
| 109 | - | </span> | |
| 110 | - | <span class="chip-text">{modeIndicator.label}</span> | |
| 111 | - | </div> | |
| 112 | - | {/if} | |
| 113 | - | ||
| 114 | - | <!-- Panel Indicator --> | |
| 115 | - | <div class="status-chip panel-chip"> | |
| 116 | - | <span class="chip-icon"> | |
| 117 | - | {#if panelIcon} | |
| 118 | - | {@const PanelIcon = panelIcon} | |
| 119 | - | <PanelIcon size={12} strokeWidth={2} /> | |
| 120 | - | {/if} | |
| 121 | - | </span> | |
| 122 | - | <span class="chip-text">{panelLabel}</span> | |
| 123 | - | </div> | |
| 124 | - | ||
| 125 | - | <!-- Loading Indicator --> | |
| 126 | - | {#if isLoading} | |
| 127 | - | <div class="status-chip loading-chip"> | |
| 128 | - | <span class="loading-spinner"></span> | |
| 129 | - | <span class="chip-text">SYNC</span> | |
| 130 | - | </div> | |
| 131 | - | {/if} | |
| 132 | - | </div> | |
| 133 | - | ||
| 134 | - | <!-- Center Section: Stats --> | |
| 135 | - | <div class="status-center"> | |
| 136 | - | <div class="stats-group"> | |
| 137 | - | <span class="stats-count" class:filtered={listStats.filtered}> | |
| 138 | - | {listStats.count} | |
| 139 | - | </span> | |
| 140 | - | <span class="stats-label">{listStats.label}</span> | |
| 141 | - | </div> | |
| 142 | - | ||
| 143 | - | {#if positionInfo} | |
| 144 | - | <div class="position-group"> | |
| 145 | - | <span class="position-current">{positionInfo.current}</span> | |
| 146 | - | <span class="position-sep">/</span> | |
| 147 | - | <span class="position-total">{positionInfo.total}</span> | |
| 148 | - | </div> | |
| 149 | - | {/if} | |
| 150 | - | </div> | |
| 151 | - | ||
| 152 | - | <!-- Right Section: Quick Hints --> | |
| 153 | - | <div class="status-right"> | |
| 154 | - | <div class="hint-item"> | |
| 155 | - | <kbd>/</kbd> | |
| 156 | - | <span>cmd</span> | |
| 157 | - | </div> | |
| 158 | - | <div class="hint-item"> | |
| 159 | - | <kbd>j</kbd><kbd>k</kbd> | |
| 160 | - | <span>nav</span> | |
| 161 | - | </div> | |
| 162 | - | <div class="hint-item"> | |
| 163 | - | <kbd>?</kbd> | |
| 164 | - | <span>help</span> | |
| 165 | - | </div> | |
| 166 | - | </div> | |
| 167 | - | </footer> | |
| 168 | - | ||
| 169 | - | <style> | |
| 170 | - | .status-bar { | |
| 171 | - | display: flex; | |
| 172 | - | align-items: center; | |
| 173 | - | justify-content: space-between; | |
| 174 | - | gap: 1rem; | |
| 175 | - | padding: 0.5rem 0.85rem; | |
| 176 | - | font-size: 0.68rem; | |
| 177 | - | font-family: ui-monospace, 'Fira Code', 'Cascadia Code', Menlo, Monaco, Consolas, monospace; | |
| 178 | - | letter-spacing: 0.05em; | |
| 179 | - | border-top: 1px solid var(--border); | |
| 180 | - | background: color-mix(in srgb, var(--background) 92%, var(--card) 8%); | |
| 181 | - | color: var(--muted-foreground); | |
| 182 | - | } | |
| 183 | - | ||
| 184 | - | /* Sections */ | |
| 185 | - | .status-left, | |
| 186 | - | .status-center, | |
| 187 | - | .status-right { | |
| 188 | - | display: flex; | |
| 189 | - | align-items: center; | |
| 190 | - | gap: 0.6rem; | |
| 191 | - | } | |
| 192 | - | ||
| 193 | - | .status-left { | |
| 194 | - | flex: 1; | |
| 195 | - | justify-content: flex-start; | |
| 196 | - | } | |
| 197 | - | ||
| 198 | - | .status-center { | |
| 199 | - | flex: 0 0 auto; | |
| 200 | - | gap: 1rem; | |
| 201 | - | } | |
| 202 | - | ||
| 203 | - | .status-right { | |
| 204 | - | flex: 1; | |
| 205 | - | justify-content: flex-end; | |
| 206 | - | gap: 0.85rem; | |
| 207 | - | } | |
| 208 | - | ||
| 209 | - | /* Status Chips */ | |
| 210 | - | .status-chip { | |
| 211 | - | display: inline-flex; | |
| 212 | - | align-items: center; | |
| 213 | - | gap: 0.35rem; | |
| 214 | - | padding: 0.25rem 0.5rem; | |
| 215 | - | background: color-mix(in srgb, var(--card) 60%, transparent); | |
| 216 | - | border-width: 1px; | |
| 217 | - | border-style: solid; | |
| 218 | - | border-color: var(--border); | |
| 219 | - | } | |
| 220 | - | ||
| 221 | - | .chip-icon { | |
| 222 | - | display: flex; | |
| 223 | - | align-items: center; | |
| 224 | - | } | |
| 225 | - | ||
| 226 | - | .chip-text { | |
| 227 | - | font-weight: 500; | |
| 228 | - | text-transform: uppercase; | |
| 229 | - | } | |
| 230 | - | ||
| 231 | - | /* Mode chip colors */ | |
| 232 | - | .mode-chip[data-color="accent"] { | |
| 233 | - | background: color-mix(in srgb, var(--primary, #4ade80) 15%, transparent); | |
| 234 | - | border-color: color-mix(in srgb, var(--primary, #4ade80) 40%, var(--border) 60%); | |
| 235 | - | color: var(--primary, #4ade80); | |
| 236 | - | } | |
| 237 | - | ||
| 238 | - | .mode-chip[data-color="warning"] { | |
| 239 | - | background: color-mix(in srgb, var(--warning) 15%, transparent); | |
| 240 | - | border-color: color-mix(in srgb, var(--warning) 40%, var(--border) 60%); | |
| 241 | - | color: var(--warning); | |
| 242 | - | } | |
| 243 | - | ||
| 244 | - | .mode-chip[data-color="info"] { | |
| 245 | - | background: color-mix(in srgb, var(--info) 15%, transparent); | |
| 246 | - | border-color: color-mix(in srgb, var(--info) 40%, var(--border) 60%); | |
| 247 | - | color: var(--info); | |
| 248 | - | } | |
| 249 | - | ||
| 250 | - | /* Pending prefix indicator */ | |
| 251 | - | .prefix-chip { | |
| 252 | - | animation: pulse-border 1s ease-in-out infinite; | |
| 253 | - | } | |
| 254 | - | ||
| 255 | - | .prefix-chip[data-color="warning"] { | |
| 256 | - | background: color-mix(in srgb, var(--warning) 15%, transparent); | |
| 257 | - | border-color: color-mix(in srgb, var(--warning) 50%, var(--border) 50%); | |
| 258 | - | color: var(--warning); | |
| 259 | - | } | |
| 260 | - | ||
| 261 | - | .prefix-key { | |
| 262 | - | font-size: 0.7rem; | |
| 263 | - | font-weight: 700; | |
| 264 | - | padding: 0.1rem 0.25rem; | |
| 265 | - | background: color-mix(in srgb, var(--warning) 25%, transparent); | |
| 266 | - | border: 1px solid color-mix(in srgb, var(--warning) 40%, var(--border) 60%); | |
| 267 | - | font-family: ui-monospace, 'Fira Code', 'Cascadia Code', Menlo, Monaco, Consolas, monospace; | |
| 268 | - | } | |
| 269 | - | ||
| 270 | - | .prefix-waiting { | |
| 271 | - | font-size: 0.65rem; | |
| 272 | - | opacity: 0.8; | |
| 273 | - | animation: blink 0.6s ease-in-out infinite; | |
| 274 | - | } | |
| 275 | - | ||
| 276 | - | @keyframes pulse-border { | |
| 277 | - | 0%, 100% { border-color: color-mix(in srgb, var(--warning) 50%, var(--border) 50%); } | |
| 278 | - | 50% { border-color: var(--warning); } | |
| 279 | - | } | |
| 280 | - | ||
| 281 | - | @keyframes blink { | |
| 282 | - | 0%, 100% { opacity: 0.4; } | |
| 283 | - | 50% { opacity: 1; } | |
| 284 | - | } | |
| 285 | - | ||
| 286 | - | .panel-chip { | |
| 287 | - | color: var(--foreground); | |
| 288 | - | } | |
| 289 | - | ||
| 290 | - | .panel-chip .chip-icon { | |
| 291 | - | color: var(--primary, #4ade80); | |
| 292 | - | } | |
| 293 | - | ||
| 294 | - | /* Loading */ | |
| 295 | - | .loading-chip { | |
| 296 | - | color: var(--muted-foreground); | |
| 297 | - | } | |
| 298 | - | ||
| 299 | - | .loading-spinner { | |
| 300 | - | width: 0.7rem; | |
| 301 | - | height: 0.7rem; | |
| 302 | - | border: 1.5px solid var(--border); | |
| 303 | - | border-top-color: var(--primary, #4ade80); | |
| 304 | - | border-radius: 50%; | |
| 305 | - | animation: spin 0.8s linear infinite; | |
| 306 | - | } | |
| 307 | - | ||
| 308 | - | @keyframes spin { | |
| 309 | - | to { transform: rotate(360deg); } | |
| 310 | - | } | |
| 311 | - | ||
| 312 | - | /* Stats */ | |
| 313 | - | .stats-group { | |
| 314 | - | display: flex; | |
| 315 | - | align-items: baseline; | |
| 316 | - | gap: 0.35rem; | |
| 317 | - | } | |
| 318 | - | ||
| 319 | - | .stats-count { | |
| 320 | - | font-weight: 600; | |
| 321 | - | color: var(--foreground); | |
| 322 | - | font-variant-numeric: tabular-nums; | |
| 323 | - | } | |
| 324 | - | ||
| 325 | - | .stats-count.filtered { | |
| 326 | - | color: var(--primary, #4ade80); | |
| 327 | - | } | |
| 328 | - | ||
| 329 | - | .stats-label { | |
| 330 | - | color: var(--muted-foreground); | |
| 331 | - | } | |
| 332 | - | ||
| 333 | - | .position-group { | |
| 334 | - | display: flex; | |
| 335 | - | align-items: center; | |
| 336 | - | gap: 0.15rem; | |
| 337 | - | padding: 0.2rem 0.45rem; | |
| 338 | - | background: var(--background); | |
| 339 | - | border-width: 1px; | |
| 340 | - | border-style: solid; | |
| 341 | - | border-color: var(--border); | |
| 342 | - | font-variant-numeric: tabular-nums; | |
| 343 | - | } | |
| 344 | - | ||
| 345 | - | .position-current { | |
| 346 | - | font-weight: 600; | |
| 347 | - | color: var(--foreground); | |
| 348 | - | } | |
| 349 | - | ||
| 350 | - | .position-sep { | |
| 351 | - | color: var(--border); | |
| 352 | - | } | |
| 353 | - | ||
| 354 | - | .position-total { | |
| 355 | - | color: var(--muted-foreground); | |
| 356 | - | } | |
| 357 | - | ||
| 358 | - | /* Hints */ | |
| 359 | - | .hint-item { | |
| 360 | - | display: flex; | |
| 361 | - | align-items: center; | |
| 362 | - | gap: 0.25rem; | |
| 363 | - | color: var(--muted-foreground); | |
| 364 | - | } | |
| 365 | - | ||
| 366 | - | .hint-item kbd { | |
| 367 | - | padding: 0.1rem 0.3rem; | |
| 368 | - | font-size: 0.6rem; | |
| 369 | - | background: var(--background); | |
| 370 | - | border-width: 1px; | |
| 371 | - | border-style: solid; | |
| 372 | - | border-color: var(--border); | |
| 373 | - | color: var(--foreground); | |
| 374 | - | } | |
| 375 | - | ||
| 376 | - | .hint-item span { | |
| 377 | - | font-size: 0.62rem; | |
| 378 | - | text-transform: uppercase; | |
| 379 | - | letter-spacing: 0.08em; | |
| 380 | - | } | |
| 381 | - | ||
| 382 | - | /* Responsive */ | |
| 383 | - | @media (max-width: 640px) { | |
| 384 | - | .status-bar { | |
| 385 | - | flex-wrap: wrap; | |
| 386 | - | gap: 0.5rem; | |
| 387 | - | } | |
| 388 | - | ||
| 389 | - | .status-left, | |
| 390 | - | .status-right { | |
| 391 | - | flex: 0 0 auto; | |
| 392 | - | } | |
| 393 | - | ||
| 394 | - | .status-center { | |
| 395 | - | order: -1; | |
| 396 | - | width: 100%; | |
| 397 | - | justify-content: center; | |
| 398 | - | padding-bottom: 0.35rem; | |
| 399 | - | border-bottom: 1px solid color-mix(in srgb, var(--border) 50%, transparent); | |
| 400 | - | } | |
| 401 | - | ||
| 402 | - | .hint-item span { | |
| 403 | - | display: none; | |
| 404 | - | } | |
| 405 | - | } | |
| 406 | - | ||
| 407 | - | @media (max-width: 480px) { | |
| 408 | - | .status-right { | |
| 409 | - | display: none; | |
| 410 | - | } | |
| 411 | - | ||
| 412 | - | .status-left { | |
| 413 | - | flex: 1; | |
| 414 | - | } | |
| 415 | - | } | |
| 416 | - | </style> | |
D webui/src/pages/homepage/shell/index.ts +0 -5
| @@ -1,5 +0,0 @@ | |||
| 1 | - | // Shell Components - Industrial Terminal Layout | |
| 2 | - | export { default as HomepageShell } from './HomepageShell.svelte'; | |
| 3 | - | export { default as CommandPalette } from './CommandPalette.svelte'; | |
| 4 | - | export { default as StatusBar } from './StatusBar.svelte'; | |
| 5 | - | export { default as KeyboardHints } from './KeyboardHints.svelte'; | |
D webui/src/pages/homepage/state.svelte.ts +0 -504
| @@ -1,504 +0,0 @@ | |||
| 1 | - | // ═══════════════════════════════════════════════════════════════════════════════ | |
| 2 | - | // Homepage Global State (Svelte 5 Runes) | |
| 3 | - | // Single module-level $state with exported store object | |
| 4 | - | // ═══════════════════════════════════════════════════════════════════════════════ | |
| 5 | - | ||
| 6 | - | import type { | |
| 7 | - | HistoryEntry, | |
| 8 | - | TimelineGroup, | |
| 9 | - | DomainStat, | |
| 10 | - | DailyVisitCount, | |
| 11 | - | HourlyDistribution, | |
| 12 | - | Favorite, | |
| 13 | - | Folder, | |
| 14 | - | Tag, | |
| 15 | - | Analytics, | |
| 16 | - | PanelType, | |
| 17 | - | HistoryCleanupRange, | |
| 18 | - | } from './types'; | |
| 19 | - | ||
| 20 | - | // ═══════════════════════════════════════════════════════════════════════════════ | |
| 21 | - | // HISTORY STATE | |
| 22 | - | // ═══════════════════════════════════════════════════════════════════════════════ | |
| 23 | - | ||
| 24 | - | let history = $state<HistoryEntry[]>([]); | |
| 25 | - | let timeline = $state<TimelineGroup[]>([]); | |
| 26 | - | let historyLoading = $state(true); | |
| 27 | - | let historyOffset = $state(0); | |
| 28 | - | let hasMoreHistory = $state(true); | |
| 29 | - | let historySearchQuery = $state(''); | |
| 30 | - | let historySearchResults = $state<HistoryEntry[]>([]); | |
| 31 | - | let historySearching = $state(false); | |
| 32 | - | ||
| 33 | - | // ═══════════════════════════════════════════════════════════════════════════════ | |
| 34 | - | // FAVORITES STATE | |
| 35 | - | // ═══════════════════════════════════════════════════════════════════════════════ | |
| 36 | - | ||
| 37 | - | let favorites = $state<Favorite[]>([]); | |
| 38 | - | let folders = $state<Folder[]>([]); | |
| 39 | - | let tags = $state<Tag[]>([]); | |
| 40 | - | let favoritesLoading = $state(true); | |
| 41 | - | let selectedFolderId = $state<number | null>(null); | |
| 42 | - | let selectedTagIds = $state<number[]>([]); | |
| 43 | - | let editingFavorite = $state<Favorite | null>(null); | |
| 44 | - | ||
| 45 | - | // ═══════════════════════════════════════════════════════════════════════════════ | |
| 46 | - | // ANALYTICS STATE | |
| 47 | - | // ═══════════════════════════════════════════════════════════════════════════════ | |
| 48 | - | ||
| 49 | - | let analytics = $state<Analytics | null>(null); | |
| 50 | - | let analyticsLoading = $state(true); | |
| 51 | - | let domainStats = $state<DomainStat[]>([]); | |
| 52 | - | let dailyVisits = $state<DailyVisitCount[]>([]); | |
| 53 | - | let hourlyDistribution = $state<HourlyDistribution[]>([]); | |
| 54 | - | ||
| 55 | - | // ═══════════════════════════════════════════════════════════════════════════════ | |
| 56 | - | // UI STATE | |
| 57 | - | // ═══════════════════════════════════════════════════════════════════════════════ | |
| 58 | - | ||
| 59 | - | let activePanel = $state<PanelType>('history'); | |
| 60 | - | let commandPaletteOpen = $state(false); | |
| 61 | - | let commandPaletteQuery = $state(''); | |
| 62 | - | let focusedIndex = $state(-1); // -1 means no item focused (focus on search input) | |
| 63 | - | let confirmModalOpen = $state(false); | |
| 64 | - | let confirmModalMessage = $state(''); | |
| 65 | - | let confirmModalAction = $state<(() => void) | null>(null); | |
| 66 | - | let cleanupModalOpen = $state(false); | |
| 67 | - | let cleanupRange = $state<HistoryCleanupRange>('hour'); | |
| 68 | - | let pendingKeyPrefix = $state<string | null>(null); | |
| 69 | - | ||
| 70 | - | // ═══════════════════════════════════════════════════════════════════════════════ | |
| 71 | - | // HELPER FUNCTIONS | |
| 72 | - | // ═══════════════════════════════════════════════════════════════════════════════ | |
| 73 | - | ||
| 74 | - | /* eslint-disable svelte/prefer-svelte-reactivity -- helper function uses temporary Map/Date, not reactive state */ | |
| 75 | - | function groupHistoryByDate(entries: HistoryEntry[]): TimelineGroup[] { | |
| 76 | - | const groups = new Map<string, HistoryEntry[]>(); | |
| 77 | - | const today = new Date(); | |
| 78 | - | today.setHours(0, 0, 0, 0); | |
| 79 | - | const yesterday = new Date(today); | |
| 80 | - | yesterday.setDate(yesterday.getDate() - 1); | |
| 81 | - | ||
| 82 | - | for (const entry of entries) { | |
| 83 | - | const date = new Date(entry.last_visited); | |
| 84 | - | const dateKey = date.toISOString().split('T')[0] ?? ''; | |
| 85 | - | if (!groups.has(dateKey)) { | |
| 86 | - | groups.set(dateKey, []); | |
| 87 | - | } | |
| 88 | - | groups.get(dateKey)!.push(entry); | |
| 89 | - | } | |
| 90 | - | ||
| 91 | - | const result: TimelineGroup[] = []; | |
| 92 | - | for (const [dateKey, items] of groups) { | |
| 93 | - | const date = new Date(dateKey); | |
| 94 | - | let label: string; | |
| 95 | - | ||
| 96 | - | if (date >= today) { | |
| 97 | - | label = 'Today'; | |
| 98 | - | } else if (date >= yesterday) { | |
| 99 | - | label = 'Yesterday'; | |
| 100 | - | } else { | |
| 101 | - | label = date.toLocaleDateString(undefined, { | |
| 102 | - | weekday: 'short', | |
| 103 | - | month: 'short', | |
| 104 | - | day: 'numeric', | |
| 105 | - | }); | |
| 106 | - | } | |
| 107 | - | ||
| 108 | - | result.push({ date: dateKey, label, entries: items }); | |
| 109 | - | } | |
| 110 | - | ||
| 111 | - | return result.sort((a, b) => b.date.localeCompare(a.date)); | |
| 112 | - | } | |
| 113 | - | /* eslint-enable svelte/prefer-svelte-reactivity */ | |
| 114 | - | ||
| 115 | - | // ═══════════════════════════════════════════════════════════════════════════════ | |
| 116 | - | // EXPORTED STORE | |
| 117 | - | // ═══════════════════════════════════════════════════════════════════════════════ | |
| 118 | - | ||
| 119 | - | export const homepageState = { | |
| 120 | - | // ───────────────────────────────────────────────────────────────────────────── | |
| 121 | - | // History Getters | |
| 122 | - | // ───────────────────────────────────────────────────────────────────────────── | |
| 123 | - | get history() { return history; }, | |
| 124 | - | get timeline() { return timeline; }, | |
| 125 | - | get historyLoading() { return historyLoading; }, | |
| 126 | - | get historyOffset() { return historyOffset; }, | |
| 127 | - | get hasMoreHistory() { return hasMoreHistory; }, | |
| 128 | - | get historySearchQuery() { return historySearchQuery; }, | |
| 129 | - | get historySearchResults() { return historySearchResults; }, | |
| 130 | - | get historySearching() { return historySearching; }, | |
| 131 | - | ||
| 132 | - | // ───────────────────────────────────────────────────────────────────────────── | |
| 133 | - | // Favorites Getters | |
| 134 | - | // ───────────────────────────────────────────────────────────────────────────── | |
| 135 | - | get favorites() { return favorites; }, | |
| 136 | - | get folders() { return folders; }, | |
| 137 | - | get tags() { return tags; }, | |
| 138 | - | get favoritesLoading() { return favoritesLoading; }, | |
| 139 | - | get selectedFolderId() { return selectedFolderId; }, | |
| 140 | - | get selectedTagIds() { return selectedTagIds; }, | |
| 141 | - | get editingFavorite() { return editingFavorite; }, | |
| 142 | - | ||
| 143 | - | // ───────────────────────────────────────────────────────────────────────────── | |
| 144 | - | // Analytics Getters | |
| 145 | - | // ───────────────────────────────────────────────────────────────────────────── | |
| 146 | - | get analytics() { return analytics; }, | |
| 147 | - | get analyticsLoading() { return analyticsLoading; }, | |
| 148 | - | get domainStats() { return domainStats; }, | |
| 149 | - | get dailyVisits() { return dailyVisits; }, | |
| 150 | - | get hourlyDistribution() { return hourlyDistribution; }, | |
| 151 | - | ||
| 152 | - | // ───────────────────────────────────────────────────────────────────────────── | |
| 153 | - | // UI Getters | |
| 154 | - | // ───────────────────────────────────────────────────────────────────────────── | |
| 155 | - | get activePanel() { return activePanel; }, | |
| 156 | - | get commandPaletteOpen() { return commandPaletteOpen; }, | |
| 157 | - | get commandPaletteQuery() { return commandPaletteQuery; }, | |
| 158 | - | get focusedIndex() { return focusedIndex; }, | |
| 159 | - | get confirmModalOpen() { return confirmModalOpen; }, | |
| 160 | - | get confirmModalMessage() { return confirmModalMessage; }, | |
| 161 | - | get cleanupModalOpen() { return cleanupModalOpen; }, | |
| 162 | - | get cleanupRange() { return cleanupRange; }, | |
| 163 | - | get pendingKeyPrefix() { return pendingKeyPrefix; }, | |
| 164 | - | ||
| 165 | - | // ───────────────────────────────────────────────────────────────────────────── | |
| 166 | - | // Computed Properties | |
| 167 | - | // ───────────────────────────────────────────────────────────────────────────── | |
| 168 | - | get filteredFavorites(): Favorite[] { | |
| 169 | - | let result = favorites; | |
| 170 | - | ||
| 171 | - | // Filter by folder | |
| 172 | - | if (selectedFolderId !== null) { | |
| 173 | - | result = result.filter(f => f.folder_id === selectedFolderId); | |
| 174 | - | } | |
| 175 | - | ||
| 176 | - | // Filter by tags | |
| 177 | - | if (selectedTagIds.length > 0) { | |
| 178 | - | result = result.filter(f => | |
| 179 | - | f.tags?.some(t => selectedTagIds.includes(t.id)) | |
| 180 | - | ); | |
| 181 | - | } | |
| 182 | - | ||
| 183 | - | return result; | |
| 184 | - | }, | |
| 185 | - | ||
| 186 | - | get currentList(): HistoryEntry[] | Favorite[] { | |
| 187 | - | switch (activePanel) { | |
| 188 | - | case 'history': | |
| 189 | - | return historySearchQuery ? historySearchResults : history; | |
| 190 | - | case 'favorites': | |
| 191 | - | return this.filteredFavorites; | |
| 192 | - | default: | |
| 193 | - | return []; | |
| 194 | - | } | |
| 195 | - | }, | |
| 196 | - | ||
| 197 | - | get currentListLength(): number { | |
| 198 | - | return this.currentList.length; | |
| 199 | - | }, | |
| 200 | - | ||
| 201 | - | // ───────────────────────────────────────────────────────────────────────────── | |
| 202 | - | // History Actions | |
| 203 | - | // ───────────────────────────────────────────────────────────────────────────── | |
| 204 | - | setHistory(items: HistoryEntry[]) { | |
| 205 | - | history = items; | |
| 206 | - | timeline = groupHistoryByDate(items); | |
| 207 | - | }, | |
| 208 | - | ||
| 209 | - | appendHistory(items: HistoryEntry[]) { | |
| 210 | - | history = [...history, ...items]; | |
| 211 | - | timeline = groupHistoryByDate(history); | |
| 212 | - | }, | |
| 213 | - | ||
| 214 | - | setHistoryLoading(loading: boolean) { | |
| 215 | - | historyLoading = loading; | |
| 216 | - | }, | |
| 217 | - | ||
| 218 | - | setHistoryOffset(offset: number) { | |
| 219 | - | historyOffset = offset; | |
| 220 | - | }, | |
| 221 | - | ||
| 222 | - | setHasMoreHistory(hasMore: boolean) { | |
| 223 | - | hasMoreHistory = hasMore; | |
| 224 | - | }, | |
| 225 | - | ||
| 226 | - | deleteHistoryEntry(id: number) { | |
| 227 | - | history = history.filter(h => h.id !== id); | |
| 228 | - | timeline = groupHistoryByDate(history); | |
| 229 | - | historySearchResults = historySearchResults.filter(h => h.id !== id); | |
| 230 | - | }, | |
| 231 | - | ||
| 232 | - | clearHistory() { | |
| 233 | - | history = []; | |
| 234 | - | timeline = []; | |
| 235 | - | historySearchResults = []; | |
| 236 | - | historyOffset = 0; | |
| 237 | - | hasMoreHistory = true; | |
| 238 | - | }, | |
| 239 | - | ||
| 240 | - | setHistorySearchQuery(q: string) { | |
| 241 | - | historySearchQuery = q; | |
| 242 | - | if (!q) { | |
| 243 | - | historySearchResults = []; | |
| 244 | - | } | |
| 245 | - | }, | |
| 246 | - | ||
| 247 | - | setHistorySearchResults(results: HistoryEntry[]) { | |
| 248 | - | historySearchResults = results; | |
| 249 | - | }, | |
| 250 | - | ||
| 251 | - | setHistorySearching(searching: boolean) { | |
| 252 | - | historySearching = searching; | |
| 253 | - | }, | |
| 254 | - | ||
| 255 | - | // ───────────────────────────────────────────────────────────────────────────── | |
| 256 | - | // Favorites Actions | |
| 257 | - | // ───────────────────────────────────────────────────────────────────────────── | |
| 258 | - | setFavorites(items: Favorite[]) { | |
| 259 | - | favorites = items; | |
| 260 | - | }, | |
| 261 | - | ||
| 262 | - | setFolders(items: Folder[]) { | |
| 263 | - | folders = items; | |
| 264 | - | }, | |
| 265 | - | ||
| 266 | - | setTags(items: Tag[]) { | |
| 267 | - | tags = items; | |
| 268 | - | }, | |
| 269 | - | ||
| 270 | - | setFavoritesLoading(loading: boolean) { | |
| 271 | - | favoritesLoading = loading; | |
| 272 | - | }, | |
| 273 | - | ||
| 274 | - | selectFolder(id: number | null) { | |
| 275 | - | selectedFolderId = id; | |
| 276 | - | focusedIndex = 0; | |
| 277 | - | }, | |
| 278 | - | ||
| 279 | - | toggleTag(id: number) { | |
| 280 | - | if (selectedTagIds.includes(id)) { | |
| 281 | - | selectedTagIds = selectedTagIds.filter(t => t !== id); | |
| 282 | - | } else { | |
| 283 | - | selectedTagIds = [...selectedTagIds, id]; | |
| 284 | - | } | |
| 285 | - | focusedIndex = 0; | |
| 286 | - | }, | |
| 287 | - | ||
| 288 | - | clearTagSelection() { | |
| 289 | - | selectedTagIds = []; | |
| 290 | - | focusedIndex = 0; | |
| 291 | - | }, | |
| 292 | - | ||
| 293 | - | setEditingFavorite(fav: Favorite | null) { | |
| 294 | - | editingFavorite = fav; | |
| 295 | - | }, | |
| 296 | - | ||
| 297 | - | updateFavorite(updated: Favorite) { | |
| 298 | - | favorites = favorites.map(f => f.id === updated.id ? updated : f); | |
| 299 | - | }, | |
| 300 | - | ||
| 301 | - | deleteFavorite(id: number) { | |
| 302 | - | favorites = favorites.filter(f => f.id !== id); | |
| 303 | - | }, | |
| 304 | - | ||
| 305 | - | addFolder(folder: Folder) { | |
| 306 | - | folders = [...folders, folder]; | |
| 307 | - | }, | |
| 308 | - | ||
| 309 | - | updateFolder(updated: Folder) { | |
| 310 | - | folders = folders.map(f => f.id === updated.id ? updated : f); | |
| 311 | - | }, | |
| 312 | - | ||
| 313 | - | deleteFolder(id: number) { | |
| 314 | - | folders = folders.filter(f => f.id !== id); | |
| 315 | - | // Clear folder selection if deleted folder was selected | |
| 316 | - | if (selectedFolderId === id) { | |
| 317 | - | selectedFolderId = null; | |
| 318 | - | } | |
| 319 | - | // Update favorites that were in this folder | |
| 320 | - | favorites = favorites.map(f => | |
| 321 | - | f.folder_id === id ? { ...f, folder_id: null } : f | |
| 322 | - | ); | |
| 323 | - | }, | |
| 324 | - | ||
| 325 | - | addTag(tag: Tag) { | |
| 326 | - | tags = [...tags, tag]; | |
| 327 | - | }, | |
| 328 | - | ||
| 329 | - | updateTag(updated: Tag) { | |
| 330 | - | tags = tags.map(t => t.id === updated.id ? updated : t); | |
| 331 | - | }, | |
| 332 | - | ||
| 333 | - | deleteTag(id: number) { | |
| 334 | - | tags = tags.filter(t => t.id !== id); | |
| 335 | - | selectedTagIds = selectedTagIds.filter(t => t !== id); | |
| 336 | - | // Remove tag from all favorites | |
| 337 | - | favorites = favorites.map(f => ({ | |
| 338 | - | ...f, | |
| 339 | - | tags: f.tags?.filter(t => t.id !== id), | |
| 340 | - | })); | |
| 341 | - | }, | |
| 342 | - | ||
| 343 | - | // ───────────────────────────────────────────────────────────────────────────── | |
| 344 | - | // Analytics Actions | |
| 345 | - | // ───────────────────────────────────────────────────────────────────────────── | |
| 346 | - | setAnalytics(data: Analytics | null) { | |
| 347 | - | analytics = data; | |
| 348 | - | if (data) { | |
| 349 | - | domainStats = data.top_domains ?? []; | |
| 350 | - | dailyVisits = data.daily_visits ?? []; | |
| 351 | - | hourlyDistribution = data.hourly_distribution ?? []; | |
| 352 | - | } | |
| 353 | - | }, | |
| 354 | - | ||
| 355 | - | setAnalyticsLoading(loading: boolean) { | |
| 356 | - | analyticsLoading = loading; | |
| 357 | - | }, | |
| 358 | - | ||
| 359 | - | setDomainStats(stats: DomainStat[]) { | |
| 360 | - | domainStats = stats; | |
| 361 | - | }, | |
| 362 | - | ||
| 363 | - | // ───────────────────────────────────────────────────────────────────────────── | |
| 364 | - | // UI Actions | |
| 365 | - | // ───────────────────────────────────────────────────────────────────────────── | |
| 366 | - | setActivePanel(panel: PanelType) { | |
| 367 | - | activePanel = panel; | |
| 368 | - | focusedIndex = -1; // Clear focus, let search input be focused | |
| 369 | - | // Clear search when switching panels | |
| 370 | - | historySearchQuery = ''; | |
| 371 | - | historySearchResults = []; | |
| 372 | - | }, | |
| 373 | - | ||
| 374 | - | openCommandPalette() { | |
| 375 | - | commandPaletteOpen = true; | |
| 376 | - | commandPaletteQuery = ''; | |
| 377 | - | }, | |
| 378 | - | ||
| 379 | - | closeCommandPalette() { | |
| 380 | - | commandPaletteOpen = false; | |
| 381 | - | commandPaletteQuery = ''; | |
| 382 | - | }, | |
| 383 | - | ||
| 384 | - | setCommandPaletteQuery(q: string) { | |
| 385 | - | commandPaletteQuery = q; | |
| 386 | - | }, | |
| 387 | - | ||
| 388 | - | setFocusedIndex(i: number) { | |
| 389 | - | focusedIndex = Math.max(-1, i); | |
| 390 | - | }, | |
| 391 | - | ||
| 392 | - | focusNext() { | |
| 393 | - | const maxIndex = this.currentListLength - 1; | |
| 394 | - | if (maxIndex < 0) return; | |
| 395 | - | // If unfocused (-1), go to first item | |
| 396 | - | if (focusedIndex === -1) { | |
| 397 | - | focusedIndex = 0; | |
| 398 | - | } else { | |
| 399 | - | focusedIndex = Math.min(focusedIndex + 1, maxIndex); | |
| 400 | - | } | |
| 401 | - | }, | |
| 402 | - | ||
| 403 | - | focusPrev() { | |
| 404 | - | // If at first item or unfocused, stay at -1 (return to search) | |
| 405 | - | if (focusedIndex <= 0) { | |
| 406 | - | focusedIndex = -1; | |
| 407 | - | } else { | |
| 408 | - | focusedIndex = focusedIndex - 1; | |
| 409 | - | } | |
| 410 | - | }, | |
| 411 | - | ||
| 412 | - | focusFirst() { | |
| 413 | - | focusedIndex = 0; | |
| 414 | - | }, | |
| 415 | - | ||
| 416 | - | focusLast() { | |
| 417 | - | focusedIndex = Math.max(0, this.currentListLength - 1); | |
| 418 | - | }, | |
| 419 | - | ||
| 420 | - | clearFocus() { | |
| 421 | - | focusedIndex = -1; | |
| 422 | - | }, | |
| 423 | - | ||
| 424 | - | // Confirmation modal | |
| 425 | - | showConfirm(message: string, action: () => void) { | |
| 426 | - | confirmModalMessage = message; | |
| 427 | - | confirmModalAction = action; | |
| 428 | - | confirmModalOpen = true; | |
| 429 | - | }, | |
| 430 | - | ||
| 431 | - | confirmAction() { | |
| 432 | - | confirmModalAction?.(); | |
| 433 | - | confirmModalOpen = false; | |
| 434 | - | confirmModalAction = null; | |
| 435 | - | confirmModalMessage = ''; | |
| 436 | - | }, | |
| 437 | - | ||
| 438 | - | cancelConfirm() { | |
| 439 | - | confirmModalOpen = false; | |
| 440 | - | confirmModalAction = null; | |
| 441 | - | confirmModalMessage = ''; | |
| 442 | - | }, | |
| 443 | - | ||
| 444 | - | // Cleanup modal | |
| 445 | - | openCleanupModal() { | |
| 446 | - | cleanupModalOpen = true; | |
| 447 | - | cleanupRange = 'hour'; | |
| 448 | - | }, | |
| 449 | - | ||
| 450 | - | closeCleanupModal() { | |
| 451 | - | cleanupModalOpen = false; | |
| 452 | - | }, | |
| 453 | - | ||
| 454 | - | setCleanupRange(range: HistoryCleanupRange) { | |
| 455 | - | cleanupRange = range; | |
| 456 | - | }, | |
| 457 | - | ||
| 458 | - | // Keyboard prefix state (for 'g' and 'D' prefix indicators) | |
| 459 | - | setPendingKeyPrefix(prefix: string | null) { | |
| 460 | - | pendingKeyPrefix = prefix; | |
| 461 | - | }, | |
| 462 | - | ||
| 463 | - | // ───────────────────────────────────────────────────────────────────────────── | |
| 464 | - | // Reset | |
| 465 | - | // ───────────────────────────────────────────────────────────────────────────── | |
| 466 | - | reset() { | |
| 467 | - | // History | |
| 468 | - | history = []; | |
| 469 | - | timeline = []; | |
| 470 | - | historyLoading = true; | |
| 471 | - | historyOffset = 0; | |
| 472 | - | hasMoreHistory = true; | |
| 473 | - | historySearchQuery = ''; | |
| 474 | - | historySearchResults = []; | |
| 475 | - | historySearching = false; | |
| 476 | - | ||
| 477 | - | // Favorites | |
| 478 | - | favorites = []; | |
| 479 | - | folders = []; | |
| 480 | - | tags = []; | |
| 481 | - | favoritesLoading = true; | |
| 482 | - | selectedFolderId = null; | |
| 483 | - | selectedTagIds = []; | |
| 484 | - | editingFavorite = null; | |
| 485 | - | ||
| 486 | - | // Analytics | |
| 487 | - | analytics = null; | |
| 488 | - | analyticsLoading = true; | |
| 489 | - | domainStats = []; | |
| 490 | - | dailyVisits = []; | |
| 491 | - | hourlyDistribution = []; | |
| 492 | - | ||
| 493 | - | // UI | |
| 494 | - | activePanel = 'history'; | |
| 495 | - | commandPaletteOpen = false; | |
| 496 | - | commandPaletteQuery = ''; | |
| 497 | - | focusedIndex = -1; | |
| 498 | - | confirmModalOpen = false; | |
| 499 | - | confirmModalMessage = ''; | |
| 500 | - | confirmModalAction = null; | |
| 501 | - | cleanupModalOpen = false; | |
| 502 | - | cleanupRange = 'hour'; | |
| 503 | - | }, | |
| 504 | - | }; | |
D webui/src/pages/homepage/types.ts +0 -232
| @@ -1,232 +0,0 @@ | |||
| 1 | - | // ═══════════════════════════════════════════════════════════════════════════════ | |
| 2 | - | // Homepage TypeScript Types | |
| 3 | - | // Mirrors Go models from internal/db/models.go and related query files | |
| 4 | - | // ═══════════════════════════════════════════════════════════════════════════════ | |
| 5 | - | ||
| 6 | - | import type { Component } from 'svelte'; | |
| 7 | - | ||
| 8 | - | // ───────────────────────────────────────────────────────────────────────────── | |
| 9 | - | // History Types | |
| 10 | - | // ───────────────────────────────────────────────────────────────────────────── | |
| 11 | - | ||
| 12 | - | export interface HistoryEntry { | |
| 13 | - | id: number; | |
| 14 | - | url: string; | |
| 15 | - | title: string | null; | |
| 16 | - | favicon_url: string | null; | |
| 17 | - | visit_count: number; | |
| 18 | - | last_visited: string; | |
| 19 | - | created_at: string; | |
| 20 | - | } | |
| 21 | - | ||
| 22 | - | export interface TimelineGroup { | |
| 23 | - | date: string; // YYYY-MM-DD | |
| 24 | - | label: string; // "Today", "Yesterday", "Dec 5", etc. | |
| 25 | - | entries: HistoryEntry[]; | |
| 26 | - | } | |
| 27 | - | ||
| 28 | - | export interface HistoryStats { | |
| 29 | - | total_entries: number; | |
| 30 | - | total_visits: number; | |
| 31 | - | unique_days: number; | |
| 32 | - | } | |
| 33 | - | ||
| 34 | - | export interface DomainStat { | |
| 35 | - | domain: string; | |
| 36 | - | page_count: number; | |
| 37 | - | total_visits: number; | |
| 38 | - | last_visit: string; | |
| 39 | - | } | |
| 40 | - | ||
| 41 | - | export interface DailyVisitCount { | |
| 42 | - | day: string; | |
| 43 | - | entries: number; | |
| 44 | - | visits: number; | |
| 45 | - | } | |
| 46 | - | ||
| 47 | - | export interface HourlyDistribution { | |
| 48 | - | hour: number; | |
| 49 | - | visit_count: number; | |
| 50 | - | } | |
| 51 | - | ||
| 52 | - | // ───────────────────────────────────────────────────────────────────────────── | |
| 53 | - | // Favorites Types | |
| 54 | - | // ───────────────────────────────────────────────────────────────────────────── | |
| 55 | - | ||
| 56 | - | export interface Favorite { | |
| 57 | - | id: number; | |
| 58 | - | url: string; | |
| 59 | - | title: string | null; | |
| 60 | - | favicon_url: string | null; | |
| 61 | - | position: number; | |
| 62 | - | created_at: string; | |
| 63 | - | updated_at: string; | |
| 64 | - | folder_id: number | null; | |
| 65 | - | shortcut_key: number | null; // 1-9 | |
| 66 | - | tags?: Tag[]; // Populated via join | |
| 67 | - | } | |
| 68 | - | ||
| 69 | - | export interface Folder { | |
| 70 | - | id: number; | |
| 71 | - | name: string; | |
| 72 | - | icon: string | null; | |
| 73 | - | position: number; | |
| 74 | - | created_at: string; | |
| 75 | - | } | |
| 76 | - | ||
| 77 | - | export interface Tag { | |
| 78 | - | id: number; | |
| 79 | - | name: string; | |
| 80 | - | color: string; // Hex color like #6b7280 | |
| 81 | - | created_at: string; | |
| 82 | - | } | |
| 83 | - | ||
| 84 | - | export interface TagAssignment { | |
| 85 | - | favorite_id: number; | |
| 86 | - | tag_id: number; | |
| 87 | - | } | |
| 88 | - | ||
| 89 | - | // ───────────────────────────────────────────────────────────────────────────── | |
| 90 | - | // Analytics Types | |
| 91 | - | // ───────────────────────────────────────────────────────────────────────────── | |
| 92 | - | ||
| 93 | - | // Matches Go HistoryAnalytics struct (flat structure) | |
| 94 | - | export interface Analytics { | |
| 95 | - | total_entries: number; | |
| 96 | - | total_visits: number; | |
| 97 | - | unique_days: number; | |
| 98 | - | top_domains: DomainStat[]; | |
| 99 | - | daily_visits: DailyVisitCount[]; | |
| 100 | - | hourly_distribution: HourlyDistribution[]; | |
| 101 | - | } | |
| 102 | - | ||
| 103 | - | // ───────────────────────────────────────────────────────────────────────────── | |
| 104 | - | // UI State Types | |
| 105 | - | // ───────────────────────────────────────────────────────────────────────────── | |
| 106 | - | ||
| 107 | - | export type PanelType = 'history' | 'favorites' | 'analytics'; | |
| 108 | - | ||
| 109 | - | export type HistoryCleanupRange = 'hour' | 'day' | 'week' | 'month' | 'all'; | |
| 110 | - | ||
| 111 | - | export interface CommandPaletteItem { | |
| 112 | - | id: string; | |
| 113 | - | label: string; | |
| 114 | - | description?: string; | |
| 115 | - | icon?: Component | string; | |
| 116 | - | action: () => void; | |
| 117 | - | shortcut?: string; | |
| 118 | - | } | |
| 119 | - | ||
| 120 | - | export interface KeyboardShortcut { | |
| 121 | - | key: string; | |
| 122 | - | description: string; | |
| 123 | - | action: () => void; | |
| 124 | - | context?: 'global' | 'history' | 'favorites' | 'command-palette'; | |
| 125 | - | } | |
| 126 | - | ||
| 127 | - | // ───────────────────────────────────────────────────────────────────────────── | |
| 128 | - | // Message Types (WebKit Bridge) | |
| 129 | - | // ───────────────────────────────────────────────────────────────────────────── | |
| 130 | - | ||
| 131 | - | export type MessageType = | |
| 132 | - | // Folders | |
| 133 | - | | 'folder_list' | |
| 134 | - | | 'folder_create' | |
| 135 | - | | 'folder_update' | |
| 136 | - | | 'folder_delete' | |
| 137 | - | // Tags | |
| 138 | - | | 'tag_list' | |
| 139 | - | | 'tag_create' | |
| 140 | - | | 'tag_update' | |
| 141 | - | | 'tag_delete' | |
| 142 | - | | 'tag_assign' | |
| 143 | - | | 'tag_remove' | |
| 144 | - | // History | |
| 145 | - | | 'history_timeline' | |
| 146 | - | | 'history_search_fts' | |
| 147 | - | | 'history_analytics' | |
| 148 | - | | 'history_domain_stats' | |
| 149 | - | | 'history_delete_entry' | |
| 150 | - | | 'history_delete_range' | |
| 151 | - | | 'history_clear_all' | |
| 152 | - | | 'history_delete_domain' | |
| 153 | - | // Favorites | |
| 154 | - | | 'favorite_list' | |
| 155 | - | | 'favorite_set_shortcut' | |
| 156 | - | | 'favorite_get_by_shortcut' | |
| 157 | - | | 'favorite_set_folder'; | |
| 158 | - | ||
| 159 | - | export interface Message<T = unknown> { | |
| 160 | - | type: MessageType; | |
| 161 | - | requestId?: string; | |
| 162 | - | payload?: T; | |
| 163 | - | } | |
| 164 | - | ||
| 165 | - | export interface MessageResponse<T = unknown> { | |
| 166 | - | success: boolean; | |
| 167 | - | data?: T; | |
| 168 | - | error?: string; | |
| 169 | - | } | |
| 170 | - | ||
| 171 | - | // ───────────────────────────────────────────────────────────────────────────── | |
| 172 | - | // Request/Response Payloads | |
| 173 | - | // ───────────────────────────────────────────────────────────────────────────── | |
| 174 | - | ||
| 175 | - | // History | |
| 176 | - | export interface HistoryTimelineRequest { | |
| 177 | - | limit?: number; | |
| 178 | - | offset?: number; | |
| 179 | - | } | |
| 180 | - | ||
| 181 | - | export interface HistorySearchRequest { | |
| 182 | - | query: string; | |
| 183 | - | limit?: number; | |
| 184 | - | } | |
| 185 | - | ||
| 186 | - | export interface HistoryDeleteRangeRequest { | |
| 187 | - | range: HistoryCleanupRange; | |
| 188 | - | } | |
| 189 | - | ||
| 190 | - | export interface HistoryDeleteDomainRequest { | |
| 191 | - | domain: string; | |
| 192 | - | } | |
| 193 | - | ||
| 194 | - | // Folders | |
| 195 | - | export interface FolderCreateRequest { | |
| 196 | - | name: string; | |
| 197 | - | icon?: string; | |
| 198 | - | } | |
| 199 | - | ||
| 200 | - | export interface FolderUpdateRequest { | |
| 201 | - | id: number; | |
| 202 | - | name: string; | |
| 203 | - | icon?: string; | |
| 204 | - | } | |
| 205 | - | ||
| 206 | - | // Tags | |
| 207 | - | export interface TagCreateRequest { | |
| 208 | - | name: string; | |
| 209 | - | color?: string; | |
| 210 | - | } | |
| 211 | - | ||
| 212 | - | export interface TagUpdateRequest { | |
| 213 | - | id: number; | |
| 214 | - | name?: string; | |
| 215 | - | color?: string; | |
| 216 | - | } | |
| 217 | - | ||
| 218 | - | export interface TagAssignRequest { | |
| 219 | - | favorite_id: number; | |
| 220 | - | tag_id: number; | |
| 221 | - | } | |
| 222 | - | ||
| 223 | - | // Favorites | |
| 224 | - | export interface FavoriteShortcutRequest { | |
| 225 | - | favorite_id: number; | |
| 226 | - | shortcut_key: number; // 1-9 | |
| 227 | - | } | |
| 228 | - | ||
| 229 | - | export interface FavoriteFolderRequest { | |
| 230 | - | favorite_id: number; | |
| 231 | - | folder_id: number | null; | |
| 232 | - | } | |
D webui/src/pages/webrtc.ts +0 -165
| @@ -1,165 +0,0 @@ | |||
| 1 | - | import "../styles/app.css"; | |
| 2 | - | ||
| 3 | - | const activeStreams: MediaStream[] = []; | |
| 4 | - | ||
| 5 | - | function now(): string { | |
| 6 | - | return new Date().toLocaleTimeString(); | |
| 7 | - | } | |
| 8 | - | ||
| 9 | - | function appendLog(output: HTMLPreElement, message: string): void { | |
| 10 | - | output.textContent = `${output.textContent}[${now()}] ${message}\n`; | |
| 11 | - | output.scrollTop = output.scrollHeight; | |
| 12 | - | } | |
| 13 | - | ||
| 14 | - | function setStatus(status: HTMLElement, value: string): void { | |
| 15 | - | status.textContent = value; | |
| 16 | - | } | |
| 17 | - | ||
| 18 | - | function stopAllStreams(video: HTMLVideoElement, output: HTMLPreElement, status: HTMLElement): void { | |
| 19 | - | for (const stream of activeStreams.splice(0)) { | |
| 20 | - | for (const track of stream.getTracks()) { | |
| 21 | - | track.stop(); | |
| 22 | - | } | |
| 23 | - | } | |
| 24 | - | ||
| 25 | - | video.pause(); | |
| 26 | - | video.srcObject = null; | |
| 27 | - | setStatus(status, "idle"); | |
| 28 | - | appendLog(output, "Stopped all active tracks"); | |
| 29 | - | } | |
| 30 | - | ||
| 31 | - | async function probeDevices(output: HTMLPreElement): Promise<void> { | |
| 32 | - | if (!("mediaDevices" in navigator) || typeof navigator.mediaDevices.enumerateDevices !== "function") { | |
| 33 | - | appendLog(output, "enumerateDevices is unavailable"); | |
| 34 | - | return; | |
| 35 | - | } | |
| 36 | - | ||
| 37 | - | try { | |
| 38 | - | const devices = await navigator.mediaDevices.enumerateDevices(); | |
| 39 | - | const summary = devices | |
| 40 | - | .map((device) => `${device.kind} ${device.label || "(label hidden until permission)"}`) | |
| 41 | - | .join(" | "); | |
| 42 | - | ||
| 43 | - | appendLog(output, `Devices: ${summary || "none"}`); | |
| 44 | - | } catch (err) { | |
| 45 | - | appendLog(output, `enumerateDevices failed: ${String(err)}`); | |
| 46 | - | appendLog(output, "Devices: none"); | |
| 47 | - | } | |
| 48 | - | } | |
| 49 | - | ||
| 50 | - | async function requestUserMedia(video: HTMLVideoElement, output: HTMLPreElement, status: HTMLElement): Promise<void> { | |
| 51 | - | if (!("mediaDevices" in navigator) || typeof navigator.mediaDevices.getUserMedia !== "function") { | |
| 52 | - | appendLog(output, "getUserMedia is unavailable"); | |
| 53 | - | return; | |
| 54 | - | } | |
| 55 | - | ||
| 56 | - | try { | |
| 57 | - | setStatus(status, "requesting camera + microphone"); | |
| 58 | - | const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: true }); | |
| 59 | - | activeStreams.push(stream); | |
| 60 | - | video.srcObject = stream; | |
| 61 | - | await video.play(); | |
| 62 | - | setStatus(status, "camera + microphone granted"); | |
| 63 | - | appendLog(output, "camera + microphone permission granted"); | |
| 64 | - | await probeDevices(output); | |
| 65 | - | } catch (err) { | |
| 66 | - | setStatus(status, "camera + microphone blocked"); | |
| 67 | - | appendLog(output, `camera + microphone request failed: ${String(err)}`); | |
| 68 | - | } | |
| 69 | - | } | |
| 70 | - | ||
| 71 | - | async function requestDisplayMedia(video: HTMLVideoElement, output: HTMLPreElement, status: HTMLElement): Promise<void> { | |
| 72 | - | if (!("mediaDevices" in navigator) || typeof navigator.mediaDevices.getDisplayMedia !== "function") { | |
| 73 | - | appendLog(output, "getDisplayMedia is unavailable"); | |
| 74 | - | return; | |
| 75 | - | } | |
| 76 | - | ||
| 77 | - | try { | |
| 78 | - | setStatus(status, "requesting screen capture"); | |
| 79 | - | const stream = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: true }); | |
| 80 | - | activeStreams.push(stream); | |
| 81 | - | video.srcObject = stream; | |
| 82 | - | await video.play(); | |
| 83 | - | setStatus(status, "screen capture granted"); | |
| 84 | - | appendLog(output, "screen capture permission granted"); | |
| 85 | - | } catch (err) { | |
| 86 | - | setStatus(status, "screen capture blocked"); | |
| 87 | - | appendLog(output, `screen capture request failed: ${String(err)}`); | |
| 88 | - | } | |
| 89 | - | } | |
| 90 | - | ||
| 91 | - | function mount(): void { | |
| 92 | - | document.title = "WebRTC Diagnostics"; | |
| 93 | - | document.body.innerHTML = ` | |
| 94 | - | <main class="page-shell" style="padding: 24px; max-width: 920px; margin: 0 auto; display: grid; gap: 16px;"> | |
| 95 | - | <header style="display: grid; gap: 8px;"> | |
| 96 | - | <h1 style="margin: 0; font-size: 1.8rem;">WebRTC Diagnostics</h1> | |
| 97 | - | <p style="margin: 0; opacity: 0.85;">Use this page to validate camera, microphone, and screen permission behavior.</p> | |
| 98 | - | </header> | |
| 99 | - | ||
| 100 | - | <section style="display: flex; flex-wrap: wrap; gap: 10px;"> | |
| 101 | - | <button id="btn-av" type="button">Request camera + microphone</button> | |
| 102 | - | <button id="btn-screen" type="button">Request screen capture</button> | |
| 103 | - | <button id="btn-stop" type="button">Stop all streams</button> | |
| 104 | - | </section> | |
| 105 | - | ||
| 106 | - | <section style="display: grid; gap: 8px;"> | |
| 107 | - | <strong>Status: <span id="status">idle</span></strong> | |
| 108 | - | <video id="preview" autoplay playsinline muted style="width: 100%; max-height: 320px; border-radius: 10px; background: #0c1522;"></video> | |
| 109 | - | </section> | |
| 110 | - | ||
| 111 | - | <section style="display: grid; gap: 8px;"> | |
| 112 | - | <strong>Event Log</strong> | |
| 113 | - | <pre id="output" style="margin: 0; padding: 12px; border-radius: 10px; background: #0d1727; color: #dbe8f4; min-height: 180px; max-height: 280px; overflow: auto;">[${now()}] Ready\n</pre> | |
| 114 | - | </section> | |
| 115 | - | </main> | |
| 116 | - | `; | |
| 117 | - | ||
| 118 | - | const status = document.getElementById("status"); | |
| 119 | - | const preview = document.getElementById("preview"); | |
| 120 | - | const output = document.getElementById("output"); | |
| 121 | - | const btnAV = document.getElementById("btn-av"); | |
| 122 | - | const btnScreen = document.getElementById("btn-screen"); | |
| 123 | - | const btnStop = document.getElementById("btn-stop"); | |
| 124 | - | ||
| 125 | - | if (!(status instanceof HTMLElement)) { | |
| 126 | - | return; | |
| 127 | - | } | |
| 128 | - | if (!(preview instanceof HTMLVideoElement)) { | |
| 129 | - | return; | |
| 130 | - | } | |
| 131 | - | if (!(output instanceof HTMLPreElement)) { | |
| 132 | - | return; | |
| 133 | - | } | |
| 134 | - | if (!(btnAV instanceof HTMLButtonElement)) { | |
| 135 | - | return; | |
| 136 | - | } | |
| 137 | - | if (!(btnScreen instanceof HTMLButtonElement)) { | |
| 138 | - | return; | |
| 139 | - | } | |
| 140 | - | if (!(btnStop instanceof HTMLButtonElement)) { | |
| 141 | - | return; | |
| 142 | - | } | |
| 143 | - | ||
| 144 | - | btnAV.addEventListener("click", () => { | |
| 145 | - | requestUserMedia(preview, output, status).catch((err: unknown) => { | |
| 146 | - | appendLog(output, `unexpected user media error: ${String(err)}`); | |
| 147 | - | }); | |
| 148 | - | }); | |
| 149 | - | ||
| 150 | - | btnScreen.addEventListener("click", () => { | |
| 151 | - | requestDisplayMedia(preview, output, status).catch((err: unknown) => { | |
| 152 | - | appendLog(output, `unexpected display media error: ${String(err)}`); | |
| 153 | - | }); | |
| 154 | - | }); | |
| 155 | - | ||
| 156 | - | btnStop.addEventListener("click", () => { | |
| 157 | - | stopAllStreams(preview, output, status); | |
| 158 | - | }); | |
| 159 | - | ||
| 160 | - | window.addEventListener("beforeunload", () => { | |
| 161 | - | stopAllStreams(preview, output, status); | |
| 162 | - | }); | |
| 163 | - | } | |
| 164 | - | ||
| 165 | - | mount(); | |
D webui/src/styles/app.css +0 -332
| @@ -1,332 +0,0 @@ | |||
| 1 | - | @import "tailwindcss"; | |
| 2 | - | @import "tw-animate-css"; | |
| 3 | - | ||
| 4 | - | /* | |
| 5 | - | * Dumber Browser - Unified Theme CSS | |
| 6 | - | * Following shadcn-svelte conventions | |
| 7 | - | * | |
| 8 | - | * Aesthetic: Industrial Terminal / Brutalist Minimal | |
| 9 | - | * - Sharp corners (--radius: 0) | |
| 10 | - | * - High contrast | |
| 11 | - | * - Vibrant green primary accent | |
| 12 | - | * | |
| 13 | - | * Theme Flow: | |
| 14 | - | * 1. TOML config defines appearance.light_palette / dark_palette | |
| 15 | - | * 2. Go ThemeManager converts to CSS variables via palette.ToWebCSSVars() | |
| 16 | - | * 3. ContentInjector injects <style data-dumber-theme-vars> into webviews | |
| 17 | - | * 4. These CSS values below serve as fallbacks when running outside the browser | |
| 18 | - | */ | |
| 19 | - | ||
| 20 | - | @custom-variant dark (&:is(.dark *)); | |
| 21 | - | ||
| 22 | - | /* ═══════════════════════════════════════════════════════════════ | |
| 23 | - | * LIGHT THEME (fallback values - Go backend overrides at runtime) | |
| 24 | - | * ═══════════════════════════════════════════════════════════════ */ | |
| 25 | - | :root { | |
| 26 | - | --radius: 0; | |
| 27 | - | ||
| 28 | - | /* Core surfaces */ | |
| 29 | - | --background: oklch(0.985 0.002 260); | |
| 30 | - | --foreground: oklch(0.145 0.02 260); | |
| 31 | - | ||
| 32 | - | /* Card/Panel surfaces */ | |
| 33 | - | --card: oklch(0.965 0.004 260); | |
| 34 | - | --card-foreground: oklch(0.145 0.02 260); | |
| 35 | - | ||
| 36 | - | /* Popover/Dropdown */ | |
| 37 | - | --popover: oklch(0.985 0.002 260); | |
| 38 | - | --popover-foreground: oklch(0.145 0.02 260); | |
| 39 | - | ||
| 40 | - | /* Primary - Vibrant Green */ | |
| 41 | - | --primary: oklch(0.55 0.20 145); | |
| 42 | - | --primary-foreground: oklch(0.985 0 0); | |
| 43 | - | ||
| 44 | - | /* Secondary - Muted surfaces */ | |
| 45 | - | --secondary: oklch(0.94 0.008 260); | |
| 46 | - | --secondary-foreground: oklch(0.20 0.02 260); | |
| 47 | - | ||
| 48 | - | /* Muted - Subdued content */ | |
| 49 | - | --muted: oklch(0.94 0.008 260); | |
| 50 | - | --muted-foreground: oklch(0.48 0.02 260); | |
| 51 | - | ||
| 52 | - | /* Accent - Cyan for info/highlights */ | |
| 53 | - | --accent: oklch(0.58 0.14 200); | |
| 54 | - | --accent-foreground: oklch(0.145 0.02 260); | |
| 55 | - | ||
| 56 | - | /* Destructive - Red for danger */ | |
| 57 | - | --destructive: oklch(0.55 0.22 25); | |
| 58 | - | --destructive-foreground: oklch(0.985 0 0); | |
| 59 | - | ||
| 60 | - | /* Borders & Inputs - solid colors for WebKitGTK compatibility */ | |
| 61 | - | --border: oklch(0.88 0.008 260); | |
| 62 | - | --input: oklch(0.88 0.008 260); | |
| 63 | - | --ring: var(--primary); | |
| 64 | - | ||
| 65 | - | /* Semantic colors - for status indicators */ | |
| 66 | - | --success: oklch(0.65 0.20 145); | |
| 67 | - | --success-foreground: oklch(0.985 0 0); | |
| 68 | - | --warning: oklch(0.75 0.18 85); | |
| 69 | - | --warning-foreground: oklch(0.145 0.02 260); | |
| 70 | - | --info: oklch(0.65 0.15 230); | |
| 71 | - | --info-foreground: oklch(0.985 0 0); | |
| 72 | - | ||
| 73 | - | /* Chart colors */ | |
| 74 | - | --chart-1: oklch(0.58 0.18 145); | |
| 75 | - | --chart-2: oklch(0.56 0.14 200); | |
| 76 | - | --chart-3: oklch(0.66 0.15 70); | |
| 77 | - | --chart-4: oklch(0.58 0.18 300); | |
| 78 | - | --chart-5: oklch(0.60 0.18 25); | |
| 79 | - | ||
| 80 | - | /* Sidebar */ | |
| 81 | - | --sidebar: oklch(0.975 0.002 260); | |
| 82 | - | --sidebar-foreground: oklch(0.145 0.02 260); | |
| 83 | - | --sidebar-primary: oklch(0.55 0.20 145); | |
| 84 | - | --sidebar-primary-foreground: oklch(0.985 0 0); | |
| 85 | - | --sidebar-accent: oklch(0.94 0.008 260); | |
| 86 | - | --sidebar-accent-foreground: oklch(0.20 0.02 260); | |
| 87 | - | --sidebar-border: oklch(0.88 0.008 260); | |
| 88 | - | --sidebar-ring: oklch(0.55 0.20 145); | |
| 89 | - | } | |
| 90 | - | ||
| 91 | - | /* ═══════════════════════════════════════════════════════════════ | |
| 92 | - | * DARK THEME (fallback values - Go backend overrides at runtime) | |
| 93 | - | * ═══════════════════════════════════════════════════════════════ */ | |
| 94 | - | .dark { | |
| 95 | - | /* Core surfaces - true black for maximum contrast */ | |
| 96 | - | --background: oklch(0.09 0.005 260); | |
| 97 | - | --foreground: oklch(0.97 0 0); | |
| 98 | - | ||
| 99 | - | /* Card/Panel surfaces - slightly elevated */ | |
| 100 | - | --card: oklch(0.14 0.008 260); | |
| 101 | - | --card-foreground: oklch(0.97 0 0); | |
| 102 | - | ||
| 103 | - | /* Popover/Dropdown */ | |
| 104 | - | --popover: oklch(0.16 0.008 260); | |
| 105 | - | --popover-foreground: oklch(0.97 0 0); | |
| 106 | - | ||
| 107 | - | /* Primary - Bright Green */ | |
| 108 | - | --primary: oklch(0.75 0.19 145); | |
| 109 | - | --primary-foreground: oklch(0.12 0.02 145); | |
| 110 | - | ||
| 111 | - | /* Secondary - Elevated surfaces */ | |
| 112 | - | --secondary: oklch(0.20 0.008 260); | |
| 113 | - | --secondary-foreground: oklch(0.97 0 0); | |
| 114 | - | ||
| 115 | - | /* Muted - More visible subdued content */ | |
| 116 | - | --muted: oklch(0.22 0.008 260); | |
| 117 | - | --muted-foreground: oklch(0.70 0.01 260); | |
| 118 | - | ||
| 119 | - | /* Accent - Cyan for info/highlights */ | |
| 120 | - | --accent: oklch(0.68 0.14 200); | |
| 121 | - | --accent-foreground: oklch(0.97 0 0); | |
| 122 | - | ||
| 123 | - | /* Destructive - Red for danger */ | |
| 124 | - | --destructive: oklch(0.65 0.20 25); | |
| 125 | - | --destructive-foreground: oklch(0.97 0 0); | |
| 126 | - | ||
| 127 | - | /* Borders & Inputs - solid colors for WebKitGTK compatibility */ | |
| 128 | - | --border: oklch(0.35 0.01 260); | |
| 129 | - | --input: oklch(0.35 0.01 260); | |
| 130 | - | --ring: var(--primary); | |
| 131 | - | ||
| 132 | - | /* Semantic colors - brighter for dark bg */ | |
| 133 | - | --success: oklch(0.72 0.18 145); | |
| 134 | - | --success-foreground: oklch(0.12 0.02 145); | |
| 135 | - | --warning: oklch(0.80 0.16 85); | |
| 136 | - | --warning-foreground: oklch(0.12 0.02 85); | |
| 137 | - | --info: oklch(0.70 0.14 230); | |
| 138 | - | --info-foreground: oklch(0.12 0.02 230); | |
| 139 | - | ||
| 140 | - | /* Chart colors - brighter for dark bg */ | |
| 141 | - | --chart-1: oklch(0.72 0.18 145); | |
| 142 | - | --chart-2: oklch(0.68 0.14 200); | |
| 143 | - | --chart-3: oklch(0.76 0.16 70); | |
| 144 | - | --chart-4: oklch(0.66 0.18 300); | |
| 145 | - | --chart-5: oklch(0.68 0.18 25); | |
| 146 | - | ||
| 147 | - | /* Sidebar */ | |
| 148 | - | --sidebar: oklch(0.12 0.008 260); | |
| 149 | - | --sidebar-foreground: oklch(0.97 0 0); | |
| 150 | - | --sidebar-primary: oklch(0.75 0.19 145); | |
| 151 | - | --sidebar-primary-foreground: oklch(0.12 0.02 145); | |
| 152 | - | --sidebar-accent: oklch(0.20 0.008 260); | |
| 153 | - | --sidebar-accent-foreground: oklch(0.97 0 0); | |
| 154 | - | --sidebar-border: oklch(0.35 0.01 260); | |
| 155 | - | --sidebar-ring: oklch(0.75 0.19 145); | |
| 156 | - | } | |
| 157 | - | ||
| 158 | - | /* ═══════════════════════════════════════════════════════════════ | |
| 159 | - | * TAILWIND THEME BRIDGE | |
| 160 | - | * Maps design tokens to Tailwind's color system | |
| 161 | - | * ═══════════════════════════════════════════════════════════════ */ | |
| 162 | - | @theme inline { | |
| 163 | - | --radius-sm: calc(var(--radius) - 4px); | |
| 164 | - | --radius-md: calc(var(--radius) - 2px); | |
| 165 | - | --radius-lg: var(--radius); | |
| 166 | - | --radius-xl: calc(var(--radius) + 4px); | |
| 167 | - | ||
| 168 | - | --color-background: var(--background); | |
| 169 | - | --color-foreground: var(--foreground); | |
| 170 | - | --color-card: var(--card); | |
| 171 | - | --color-card-foreground: var(--card-foreground); | |
| 172 | - | --color-popover: var(--popover); | |
| 173 | - | --color-popover-foreground: var(--popover-foreground); | |
| 174 | - | --color-primary: var(--primary); | |
| 175 | - | --color-primary-foreground: var(--primary-foreground); | |
| 176 | - | --color-secondary: var(--secondary); | |
| 177 | - | --color-secondary-foreground: var(--secondary-foreground); | |
| 178 | - | --color-muted: var(--muted); | |
| 179 | - | --color-muted-foreground: var(--muted-foreground); | |
| 180 | - | --color-accent: var(--accent); | |
| 181 | - | --color-accent-foreground: var(--accent-foreground); | |
| 182 | - | --color-destructive: var(--destructive); | |
| 183 | - | --color-destructive-foreground: var(--destructive-foreground); | |
| 184 | - | --color-success: var(--success); | |
| 185 | - | --color-success-foreground: var(--success-foreground); | |
| 186 | - | --color-warning: var(--warning); | |
| 187 | - | --color-warning-foreground: var(--warning-foreground); | |
| 188 | - | --color-info: var(--info); | |
| 189 | - | --color-info-foreground: var(--info-foreground); | |
| 190 | - | --color-border: var(--border); | |
| 191 | - | --color-input: var(--input); | |
| 192 | - | --color-ring: var(--ring); | |
| 193 | - | --color-chart-1: var(--chart-1); | |
| 194 | - | --color-chart-2: var(--chart-2); | |
| 195 | - | --color-chart-3: var(--chart-3); | |
| 196 | - | --color-chart-4: var(--chart-4); | |
| 197 | - | --color-chart-5: var(--chart-5); | |
| 198 | - | --color-sidebar: var(--sidebar); | |
| 199 | - | --color-sidebar-foreground: var(--sidebar-foreground); | |
| 200 | - | --color-sidebar-primary: var(--sidebar-primary); | |
| 201 | - | --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); | |
| 202 | - | --color-sidebar-accent: var(--sidebar-accent); | |
| 203 | - | --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); | |
| 204 | - | --color-sidebar-border: var(--sidebar-border); | |
| 205 | - | --color-sidebar-ring: var(--sidebar-ring); | |
| 206 | - | } | |
| 207 | - | ||
| 208 | - | /* ═══════════════════════════════════════════════════════════════ | |
| 209 | - | * BASE LAYER | |
| 210 | - | * Default styles applied to all elements | |
| 211 | - | * ═══════════════════════════════════════════════════════════════ */ | |
| 212 | - | @layer base { | |
| 213 | - | * { | |
| 214 | - | @apply border-border outline-ring/50; | |
| 215 | - | } | |
| 216 | - | body { | |
| 217 | - | @apply bg-background text-foreground; | |
| 218 | - | transition: | |
| 219 | - | background-color 200ms ease, | |
| 220 | - | color 200ms ease; | |
| 221 | - | } | |
| 222 | - | } | |
| 223 | - | ||
| 224 | - | /* ═══════════════════════════════════════════════════════════════ | |
| 225 | - | * BROWSER UI BASE | |
| 226 | - | * ═══════════════════════════════════════════════════════════════ */ | |
| 227 | - | ||
| 228 | - | .browser-ui { | |
| 229 | - | background-color: var(--background); | |
| 230 | - | color: var(--foreground); | |
| 231 | - | border-color: var(--border); | |
| 232 | - | transition: colors 250ms; | |
| 233 | - | } | |
| 234 | - | ||
| 235 | - | /* ═══════════════════════════════════════════════════════════════ | |
| 236 | - | * ANIMATIONS | |
| 237 | - | * ═══════════════════════════════════════════════════════════════ */ | |
| 238 | - | ||
| 239 | - | .slide-in-top { | |
| 240 | - | animation: slide-in-top 0.25s ease-out; | |
| 241 | - | } | |
| 242 | - | ||
| 243 | - | .slide-out-top { | |
| 244 | - | animation: slide-out-top 0.25s ease-in; | |
| 245 | - | } | |
| 246 | - | ||
| 247 | - | .fade-in { | |
| 248 | - | animation: fade-in 0.15s ease-out; | |
| 249 | - | } | |
| 250 | - | ||
| 251 | - | .fade-out { | |
| 252 | - | animation: fade-out 0.15s ease-in; | |
| 253 | - | } | |
| 254 | - | ||
| 255 | - | @keyframes slide-in-top { | |
| 256 | - | from { | |
| 257 | - | transform: translateY(-100%); | |
| 258 | - | opacity: 0; | |
| 259 | - | } | |
| 260 | - | to { | |
| 261 | - | transform: translateY(0); | |
| 262 | - | opacity: 1; | |
| 263 | - | } | |
| 264 | - | } | |
| 265 | - | ||
| 266 | - | @keyframes slide-out-top { | |
| 267 | - | from { | |
| 268 | - | transform: translateY(0); | |
| 269 | - | opacity: 1; | |
| 270 | - | } | |
| 271 | - | to { | |
| 272 | - | transform: translateY(-100%); | |
| 273 | - | opacity: 0; | |
| 274 | - | } | |
| 275 | - | } | |
| 276 | - | ||
| 277 | - | @keyframes fade-in { | |
| 278 | - | from { | |
| 279 | - | opacity: 0; | |
| 280 | - | } | |
| 281 | - | to { | |
| 282 | - | opacity: 1; | |
| 283 | - | } | |
| 284 | - | } | |
| 285 | - | ||
| 286 | - | @keyframes fade-out { | |
| 287 | - | from { | |
| 288 | - | opacity: 1; | |
| 289 | - | } | |
| 290 | - | to { | |
| 291 | - | opacity: 0; | |
| 292 | - | } | |
| 293 | - | } | |
| 294 | - | ||
| 295 | - | /* ═══════════════════════════════════════════════════════════════ | |
| 296 | - | * FIND HIGHLIGHTING | |
| 297 | - | * Styles for in-page search highlights | |
| 298 | - | * ═══════════════════════════════════════════════════════════════ */ | |
| 299 | - | ||
| 300 | - | .find-highlight { | |
| 301 | - | background-color: rgb(255 235 59); /* yellow-400 */ | |
| 302 | - | color: rgb(0 0 0); /* black */ | |
| 303 | - | padding: 0 0.125rem; /* px-0.5 */ | |
| 304 | - | border-radius: 0.125rem; /* rounded-sm */ | |
| 305 | - | box-shadow: 0 0 0 1px rgb(200 185 0) inset; | |
| 306 | - | } | |
| 307 | - | ||
| 308 | - | .find-active { | |
| 309 | - | background-color: rgb(255 152 0) !important; /* orange-500 */ | |
| 310 | - | color: rgb(0 0 0); /* black */ | |
| 311 | - | box-shadow: 0 0 0 1px rgb(179 107 0) inset !important; | |
| 312 | - | } | |
| 313 | - | ||
| 314 | - | /* ═══════════════════════════════════════════════════════════════ | |
| 315 | - | * COMPONENT ISOLATION | |
| 316 | - | * For injecting browser UI into external pages | |
| 317 | - | * ═══════════════════════════════════════════════════════════════ */ | |
| 318 | - | ||
| 319 | - | .browser-component-root { | |
| 320 | - | /* Reset any inherited styles from the website */ | |
| 321 | - | all: initial; | |
| 322 | - | /* Apply our design system */ | |
| 323 | - | font-family: | |
| 324 | - | Fira Sans, | |
| 325 | - | system-ui, | |
| 326 | - | -apple-system, | |
| 327 | - | sans-serif; | |
| 328 | - | box-sizing: border-box; | |
| 329 | - | * { | |
| 330 | - | box-sizing: border-box; | |
| 331 | - | } | |
| 332 | - | } | |
D webui/src/types/dumber.d.ts +0 -156
| @@ -1,156 +0,0 @@ | |||
| 1 | - | /** | |
| 2 | - | * Global type declarations for Dumber browser WebKit bridge | |
| 3 | - | */ | |
| 4 | - | ||
| 5 | - | // ═══════════════════════════════════════════════════════════════════════════════ | |
| 6 | - | // WEBKIT BRIDGE TYPES | |
| 7 | - | // ═══════════════════════════════════════════════════════════════════════════════ | |
| 8 | - | ||
| 9 | - | interface DumberBridge { | |
| 10 | - | postMessage: (message: unknown) => void; | |
| 11 | - | } | |
| 12 | - | ||
| 13 | - | interface DumberWebKit { | |
| 14 | - | messageHandlers?: { | |
| 15 | - | dumber?: DumberBridge; | |
| 16 | - | }; | |
| 17 | - | } | |
| 18 | - | ||
| 19 | - | // ═══════════════════════════════════════════════════════════════════════════════ | |
| 20 | - | // VIEW TRANSITION API (not yet in TypeScript DOM lib) | |
| 21 | - | // ═══════════════════════════════════════════════════════════════════════════════ | |
| 22 | - | ||
| 23 | - | interface ViewTransition { | |
| 24 | - | finished: Promise<void>; | |
| 25 | - | ready: Promise<void>; | |
| 26 | - | updateCallbackDone: Promise<void>; | |
| 27 | - | } | |
| 28 | - | ||
| 29 | - | // ═══════════════════════════════════════════════════════════════════════════════ | |
| 30 | - | // THEME TYPES | |
| 31 | - | // ═══════════════════════════════════════════════════════════════════════════════ | |
| 32 | - | ||
| 33 | - | type ThemeMode = 'light' | 'dark'; | |
| 34 | - | ||
| 35 | - | interface ColorSchemeManager { | |
| 36 | - | setUserPreference?: (theme: ThemeMode) => void; | |
| 37 | - | } | |
| 38 | - | ||
| 39 | - | // ═══════════════════════════════════════════════════════════════════════════════ | |
| 40 | - | // HOMEPAGE MESSAGING TYPES | |
| 41 | - | // ═══════════════════════════════════════════════════════════════════════════════ | |
| 42 | - | ||
| 43 | - | /** Response from Go backend for homepage requests */ | |
| 44 | - | interface HomepageResponse { | |
| 45 | - | requestId: string; | |
| 46 | - | success: boolean; | |
| 47 | - | data?: unknown; | |
| 48 | - | error?: string; | |
| 49 | - | } | |
| 50 | - | ||
| 51 | - | /** Error response format (can be string for legacy or object) */ | |
| 52 | - | type HomepageErrorResponse = string | { requestId?: string; error?: string }; | |
| 53 | - | ||
| 54 | - | /** Callback type for homepage responses */ | |
| 55 | - | type HomepageResponseCallback = (response: HomepageResponse) => void; | |
| 56 | - | ||
| 57 | - | /** Callback type for legacy data responses (can receive raw data or Response object) */ | |
| 58 | - | type HomepageLegacyCallback = (dataOrResponse: unknown) => void; | |
| 59 | - | ||
| 60 | - | /** Callback type for error responses */ | |
| 61 | - | type HomepageErrorCallback = (errorOrResponse: HomepageErrorResponse) => void; | |
| 62 | - | ||
| 63 | - | // ═══════════════════════════════════════════════════════════════════════════════ | |
| 64 | - | // GLOBAL DECLARATIONS | |
| 65 | - | // ═══════════════════════════════════════════════════════════════════════════════ | |
| 66 | - | ||
| 67 | - | declare global { | |
| 68 | - | // Re-export bridge type for use in other modules | |
| 69 | - | type DumberBridge = { | |
| 70 | - | postMessage: (message: unknown) => void; | |
| 71 | - | }; | |
| 72 | - | interface Window { | |
| 73 | - | // ───────────────────────────────────────────────────────────────────────── | |
| 74 | - | // Core WebKit Bridge | |
| 75 | - | // ───────────────────────────────────────────────────────────────────────── | |
| 76 | - | /** WebView ID assigned by the browser backend */ | |
| 77 | - | __dumber_webview_id?: number; | |
| 78 | - | /** WebKit message handlers for native bridge */ | |
| 79 | - | webkit?: DumberWebKit; | |
| 80 | - | /** CEF bridge shim injected on internal pages */ | |
| 81 | - | dumber?: DumberBridge; | |
| 82 | - | ||
| 83 | - | // ───────────────────────────────────────────────────────────────────────── | |
| 84 | - | // Config Page Callbacks | |
| 85 | - | // ───────────────────────────────────────────────────────────────────────── | |
| 86 | - | /** Callback invoked when config save succeeds */ | |
| 87 | - | __dumber_config_saved?: (resp?: unknown) => void; | |
| 88 | - | /** Callback invoked when config save fails */ | |
| 89 | - | __dumber_config_error?: (msg: unknown) => void; | |
| 90 | - | ||
| 91 | - | // ───────────────────────────────────────────────────────────────────────── | |
| 92 | - | // Keybindings Callbacks | |
| 93 | - | // ───────────────────────────────────────────────────────────────────────── | |
| 94 | - | /** Callback invoked when keybindings are loaded */ | |
| 95 | - | __dumber_keybindings_loaded?: (resp?: unknown) => void; | |
| 96 | - | /** Callback invoked when keybindings load fails */ | |
| 97 | - | __dumber_keybindings_error?: (msg: unknown) => void; | |
| 98 | - | /** Callback invoked when a keybinding is set */ | |
| 99 | - | __dumber_keybinding_set?: (resp?: unknown) => void; | |
| 100 | - | /** Callback invoked when keybinding set fails */ | |
| 101 | - | __dumber_keybinding_set_error?: (msg: unknown) => void; | |
| 102 | - | /** Callback invoked when a keybinding is reset */ | |
| 103 | - | __dumber_keybinding_reset?: (resp?: unknown) => void; | |
| 104 | - | /** Callback invoked when keybinding reset fails */ | |
| 105 | - | __dumber_keybinding_reset_error?: (msg: unknown) => void; | |
| 106 | - | /** Callback invoked when all keybindings are reset */ | |
| 107 | - | __dumber_keybindings_reset_all?: (resp?: unknown) => void; | |
| 108 | - | /** Callback invoked when reset all keybindings fails */ | |
| 109 | - | __dumber_keybindings_reset_all_error?: (msg: unknown) => void; | |
| 110 | - | ||
| 111 | - | // ───────────────────────────────────────────────────────────────────────── | |
| 112 | - | // Homepage Callbacks | |
| 113 | - | // ───────────────────────────────────────────────────────────────────────── | |
| 114 | - | /** Generic response handler for homepage requests */ | |
| 115 | - | __dumber_homepage_response?: HomepageResponseCallback; | |
| 116 | - | /** History timeline data callback */ | |
| 117 | - | __dumber_history_timeline?: HomepageLegacyCallback; | |
| 118 | - | /** Folders data callback */ | |
| 119 | - | __dumber_folders?: HomepageLegacyCallback; | |
| 120 | - | /** Tags data callback */ | |
| 121 | - | __dumber_tags?: HomepageLegacyCallback; | |
| 122 | - | /** Favorites data callback */ | |
| 123 | - | __dumber_favorites?: HomepageLegacyCallback; | |
| 124 | - | /** Analytics data callback */ | |
| 125 | - | __dumber_analytics?: HomepageLegacyCallback; | |
| 126 | - | /** Domain stats data callback */ | |
| 127 | - | __dumber_domain_stats?: HomepageLegacyCallback; | |
| 128 | - | /** History search results callback */ | |
| 129 | - | __dumber_history_search_results?: HomepageLegacyCallback; | |
| 130 | - | /** History deleted callback */ | |
| 131 | - | __dumber_history_deleted?: HomepageLegacyCallback; | |
| 132 | - | /** History cleared callback */ | |
| 133 | - | __dumber_history_cleared?: HomepageLegacyCallback; | |
| 134 | - | /** Error callback for homepage requests */ | |
| 135 | - | __dumber_error?: HomepageErrorCallback; | |
| 136 | - | ||
| 137 | - | // ───────────────────────────────────────────────────────────────────────── | |
| 138 | - | // Theme Management | |
| 139 | - | // ───────────────────────────────────────────────────────────────────────── | |
| 140 | - | /** Color scheme manager for theme switching */ | |
| 141 | - | __dumber_color_scheme_manager?: ColorSchemeManager; | |
| 142 | - | /** Legacy theme setter */ | |
| 143 | - | __dumber_setTheme?: (theme: ThemeMode) => void; | |
| 144 | - | /** Resolved prefers-dark state injected by the backend */ | |
| 145 | - | __dumber_cef_prefers_dark?: boolean; | |
| 146 | - | /** Resolved prefers-dark state injected by the backend */ | |
| 147 | - | __dumber_gtk_prefers_dark?: boolean; | |
| 148 | - | } | |
| 149 | - | ||
| 150 | - | interface Document { | |
| 151 | - | /** View Transition API */ | |
| 152 | - | startViewTransition?: (callback: () => void | Promise<void>) => ViewTransition; | |
| 153 | - | } | |
| 154 | - | } | |
| 155 | - | ||
| 156 | - | export {}; | |
D webui/src/types/frappe-charts.d.ts +0 -47
| @@ -1,47 +0,0 @@ | |||
| 1 | - | declare module 'frappe-charts' { | |
| 2 | - | export interface ChartData { | |
| 3 | - | labels: string[]; | |
| 4 | - | datasets: Array<{ | |
| 5 | - | name?: string; | |
| 6 | - | values: number[]; | |
| 7 | - | chartType?: string; | |
| 8 | - | }>; | |
| 9 | - | } | |
| 10 | - | ||
| 11 | - | export interface ChartOptions { | |
| 12 | - | data: ChartData; | |
| 13 | - | type: 'line' | 'bar' | 'axis-mixed' | 'pie' | 'percentage' | 'heatmap'; | |
| 14 | - | height?: number; | |
| 15 | - | colors?: string[]; | |
| 16 | - | axisOptions?: { | |
| 17 | - | xAxisMode?: 'tick' | 'span'; | |
| 18 | - | xIsSeries?: boolean; | |
| 19 | - | yAxisMode?: 'tick' | 'span'; | |
| 20 | - | }; | |
| 21 | - | barOptions?: { | |
| 22 | - | spaceRatio?: number; | |
| 23 | - | stacked?: boolean; | |
| 24 | - | }; | |
| 25 | - | lineOptions?: { | |
| 26 | - | regionFill?: number; | |
| 27 | - | hideDots?: number; | |
| 28 | - | hideLine?: number; | |
| 29 | - | heatline?: number; | |
| 30 | - | dotSize?: number; | |
| 31 | - | }; | |
| 32 | - | tooltipOptions?: { | |
| 33 | - | formatTooltipX?: (d: string) => string; | |
| 34 | - | formatTooltipY?: (d: number) => string; | |
| 35 | - | }; | |
| 36 | - | isNavigable?: boolean; | |
| 37 | - | valuesOverPoints?: boolean; | |
| 38 | - | maxSlices?: number; | |
| 39 | - | } | |
| 40 | - | ||
| 41 | - | export class Chart { | |
| 42 | - | constructor(container: HTMLElement, options: ChartOptions); | |
| 43 | - | update(data: ChartData): void; | |
| 44 | - | destroy(): void; | |
| 45 | - | export(): void; | |
| 46 | - | } | |
| 47 | - | } | |
D webui/src/types/generated.ts +0 -21
| @@ -1,21 +0,0 @@ | |||
| 1 | - | export interface HistoryEntry { | |
| 2 | - | id: number; | |
| 3 | - | url: string; | |
| 4 | - | title: string | null; | |
| 5 | - | favicon_url: string | null; | |
| 6 | - | visit_count: number; | |
| 7 | - | last_visited: string; | |
| 8 | - | created_at: string; | |
| 9 | - | } | |
| 10 | - | ||
| 11 | - | export type History = HistoryEntry; | |
| 12 | - | ||
| 13 | - | export interface SearchShortcut { | |
| 14 | - | url: string; | |
| 15 | - | description: string; | |
| 16 | - | } | |
| 17 | - | ||
| 18 | - | export interface Shortcut { | |
| 19 | - | url_template: string; | |
| 20 | - | description?: string; | |
| 21 | - | } | |
D webui/src/utils/dom.ts +0 -203
| @@ -1,203 +0,0 @@ | |||
| 1 | - | import type { History, Shortcut } from "../types/generated.js"; | |
| 2 | - | ||
| 3 | - | export class DOMRenderer { | |
| 4 | - | private historyListElement: HTMLElement | null = null; | |
| 5 | - | private shortcutsElement: HTMLElement | null = null; | |
| 6 | - | ||
| 7 | - | constructor() { | |
| 8 | - | this.historyListElement = document.getElementById("historyList"); | |
| 9 | - | this.shortcutsElement = document.getElementById("shortcuts"); | |
| 10 | - | } | |
| 11 | - | ||
| 12 | - | displayHistory(historyEntries: History[]): void { | |
| 13 | - | if (!this.historyListElement) return; | |
| 14 | - | ||
| 15 | - | if (!historyEntries || historyEntries.length === 0) { | |
| 16 | - | this.historyListElement.innerHTML = ` | |
| 17 | - | <div class="empty-state"> | |
| 18 | - | <h3>No history yet</h3> | |
| 19 | - | <p>Use CLI commands to browse websites</p> | |
| 20 | - | </div> | |
| 21 | - | `; | |
| 22 | - | return; | |
| 23 | - | } | |
| 24 | - | ||
| 25 | - | this.historyListElement.innerHTML = ""; | |
| 26 | - | historyEntries.forEach((item) => { | |
| 27 | - | const historyItem = this.createHistoryItem(item); | |
| 28 | - | this.historyListElement!.appendChild(historyItem); | |
| 29 | - | }); | |
| 30 | - | } | |
| 31 | - | ||
| 32 | - | displayShortcuts(shortcutsData: Record<string, Shortcut>): void { | |
| 33 | - | if (!this.shortcutsElement) return; | |
| 34 | - | ||
| 35 | - | if (!shortcutsData || Object.keys(shortcutsData).length === 0) { | |
| 36 | - | this.shortcutsElement.innerHTML = ` | |
| 37 | - | <div class="empty-state"> | |
| 38 | - | <h3>No shortcuts configured</h3> | |
| 39 | - | <p>Add shortcuts to your config file</p> | |
| 40 | - | </div> | |
| 41 | - | `; | |
| 42 | - | return; | |
| 43 | - | } | |
| 44 | - | ||
| 45 | - | this.shortcutsElement.innerHTML = ""; | |
| 46 | - | Object.entries(shortcutsData).forEach(([key, shortcut]) => { | |
| 47 | - | const shortcutEl = this.createShortcutElement(key, shortcut); | |
| 48 | - | this.shortcutsElement!.appendChild(shortcutEl); | |
| 49 | - | }); | |
| 50 | - | } | |
| 51 | - | ||
| 52 | - | showLoading(container: "history" | "shortcuts"): void { | |
| 53 | - | const element = | |
| 54 | - | container === "history" ? this.historyListElement : this.shortcutsElement; | |
| 55 | - | if (element) { | |
| 56 | - | element.innerHTML = '<div class="loading">Loading...</div>'; | |
| 57 | - | } | |
| 58 | - | } | |
| 59 | - | ||
| 60 | - | showError(container: "history" | "shortcuts", message: string): void { | |
| 61 | - | const element = | |
| 62 | - | container === "history" ? this.historyListElement : this.shortcutsElement; | |
| 63 | - | if (element) { | |
| 64 | - | element.innerHTML = ` | |
| 65 | - | <div class="empty-state"> | |
| 66 | - | <h3>Error</h3> | |
| 67 | - | <p>${message}</p> | |
| 68 | - | </div> | |
| 69 | - | `; | |
| 70 | - | } | |
| 71 | - | } | |
| 72 | - | ||
| 73 | - | private createHistoryItem(item: History): HTMLElement { | |
| 74 | - | const historyItem = document.createElement("div"); | |
| 75 | - | historyItem.className = "history-item"; | |
| 76 | - | const parsed = this.safeParseURL(item.url); | |
| 77 | - | const title = | |
| 78 | - | item.title && item.title.trim() !== "" | |
| 79 | - | ? item.title | |
| 80 | - | : parsed.host || "Untitled"; | |
| 81 | - | ||
| 82 | - | const line = document.createElement("div"); | |
| 83 | - | line.className = "history-line"; | |
| 84 | - | ||
| 85 | - | // Favicon chip (wrapper ensures perfect circle sizing) | |
| 86 | - | const iconWrap = document.createElement("div"); | |
| 87 | - | iconWrap.className = "history-favicon-chip"; | |
| 88 | - | const icon = document.createElement("img"); | |
| 89 | - | icon.className = "history-favicon-img"; | |
| 90 | - | icon.width = 16; | |
| 91 | - | icon.height = 16; | |
| 92 | - | icon.loading = "lazy"; | |
| 93 | - | icon.referrerPolicy = "no-referrer"; | |
| 94 | - | icon.src = this.buildFaviconURL(item.url); | |
| 95 | - | icon.addEventListener("error", () => { | |
| 96 | - | iconWrap.style.display = "none"; | |
| 97 | - | }); | |
| 98 | - | iconWrap.appendChild(icon); | |
| 99 | - | ||
| 100 | - | const titleEl = document.createElement("span"); | |
| 101 | - | titleEl.className = "history-title"; | |
| 102 | - | titleEl.textContent = title; | |
| 103 | - | ||
| 104 | - | const sep1 = document.createElement("span"); | |
| 105 | - | sep1.className = "history-sep"; | |
| 106 | - | sep1.textContent = " - "; | |
| 107 | - | ||
| 108 | - | const domainEl = document.createElement("span"); | |
| 109 | - | domainEl.className = "history-domain"; | |
| 110 | - | domainEl.textContent = parsed.host || ""; | |
| 111 | - | ||
| 112 | - | const sep2 = document.createElement("span"); | |
| 113 | - | sep2.className = "history-sep"; | |
| 114 | - | sep2.textContent = " - "; | |
| 115 | - | ||
| 116 | - | const urlEl = document.createElement("span"); | |
| 117 | - | urlEl.className = "history-url"; | |
| 118 | - | urlEl.textContent = item.url; | |
| 119 | - | ||
| 120 | - | line.appendChild(iconWrap); | |
| 121 | - | line.appendChild(titleEl); | |
| 122 | - | line.appendChild(sep1); | |
| 123 | - | line.appendChild(domainEl); | |
| 124 | - | line.appendChild(sep2); | |
| 125 | - | line.appendChild(urlEl); | |
| 126 | - | ||
| 127 | - | historyItem.appendChild(line); | |
| 128 | - | ||
| 129 | - | historyItem.addEventListener("click", () => { | |
| 130 | - | this.navigateToUrl(item.url); | |
| 131 | - | }); | |
| 132 | - | ||
| 133 | - | return historyItem; | |
| 134 | - | } | |
| 135 | - | ||
| 136 | - | private createShortcutElement(key: string, shortcut: Shortcut): HTMLElement { | |
| 137 | - | const shortcutEl = document.createElement("div"); | |
| 138 | - | shortcutEl.className = "shortcut"; | |
| 139 | - | shortcutEl.innerHTML = ` | |
| 140 | - | <div class="shortcut-key">${this.escapeHtml(key)}:</div> | |
| 141 | - | <div class="shortcut-desc">${this.escapeHtml(shortcut.description || "No description")}</div> | |
| 142 | - | `; | |
| 143 | - | ||
| 144 | - | shortcutEl.addEventListener("click", () => { | |
| 145 | - | // Navigate to the base URL of the shortcut (remove %s template parameter) | |
| 146 | - | const baseUrl = this.extractBaseUrl(shortcut.url_template); | |
| 147 | - | this.navigateToUrl(baseUrl); | |
| 148 | - | }); | |
| 149 | - | ||
| 150 | - | return shortcutEl; | |
| 151 | - | } | |
| 152 | - | ||
| 153 | - | private async navigateToUrl(url: string): Promise<void> { | |
| 154 | - | console.log("Navigating to:", url); | |
| 155 | - | window.location.href = url; | |
| 156 | - | } | |
| 157 | - | ||
| 158 | - | private extractBaseUrl(templateUrl: string): string { | |
| 159 | - | try { | |
| 160 | - | // Support both %s and {query} placeholders | |
| 161 | - | const candidate = templateUrl.replace(/%s|\{query\}/g, ""); | |
| 162 | - | const url = new URL(candidate); | |
| 163 | - | // Remove empty query params left after placeholder removal | |
| 164 | - | const params = new URLSearchParams(url.search); | |
| 165 | - | for (const key of Array.from(params.keys())) { | |
| 166 | - | const v = params.get(key); | |
| 167 | - | if (v === null || v === "") { | |
| 168 | - | params.delete(key); | |
| 169 | - | } | |
| 170 | - | } | |
| 171 | - | url.search = params.toString(); | |
| 172 | - | return url.toString(); | |
| 173 | - | } catch (error) { | |
| 174 | - | console.error("Failed to parse shortcut URL:", templateUrl, error); | |
| 175 | - | throw error; | |
| 176 | - | } | |
| 177 | - | } | |
| 178 | - | ||
| 179 | - | private escapeHtml(text: string): string { | |
| 180 | - | const div = document.createElement("div"); | |
| 181 | - | div.textContent = text; | |
| 182 | - | return div.innerHTML; | |
| 183 | - | } | |
| 184 | - | ||
| 185 | - | private safeParseURL(raw: string): { host: string } { | |
| 186 | - | try { | |
| 187 | - | const u = new URL(raw); | |
| 188 | - | return { host: u.host }; | |
| 189 | - | } catch { | |
| 190 | - | return { host: "" }; | |
| 191 | - | } | |
| 192 | - | } | |
| 193 | - | ||
| 194 | - | private buildFaviconURL(raw: string): string { | |
| 195 | - | try { | |
| 196 | - | const u = new URL(raw); | |
| 197 | - | const scheme = u.protocol && u.protocol !== ":" ? u.protocol : "https:"; | |
| 198 | - | return `${scheme}//${u.host}/favicon.ico`; | |
| 199 | - | } catch { | |
| 200 | - | return ""; | |
| 201 | - | } | |
| 202 | - | } | |
| 203 | - | } | |
D webui/svelte.config.js +0 -10
| @@ -1,10 +0,0 @@ | |||
| 1 | - | import { vitePreprocess } from "@sveltejs/vite-plugin-svelte"; | |
| 2 | - | ||
| 3 | - | export default { | |
| 4 | - | // Enable rune mode for Svelte 5 | |
| 5 | - | compilerOptions: { | |
| 6 | - | runes: true, | |
| 7 | - | }, | |
| 8 | - | // Use Vite's preprocessor for TypeScript and PostCSS | |
| 9 | - | preprocess: vitePreprocess(), | |
| 10 | - | }; | |
D webui/tailwind.config.js +0 -66
| @@ -1,66 +0,0 @@ | |||
| 1 | - | /** @type {import('tailwindcss').Config} */ | |
| 2 | - | export default { | |
| 3 | - | content: ["./src/**/*.{html,js,ts,svelte}"], | |
| 4 | - | theme: { | |
| 5 | - | extend: { | |
| 6 | - | // Font families for Industrial Terminal aesthetic | |
| 7 | - | fontFamily: { | |
| 8 | - | mono: ['"JetBrains Mono NF"', "ui-monospace", "SFMono-Regular", "monospace"], | |
| 9 | - | sans: ["Inter", "system-ui", "-apple-system", "sans-serif"], | |
| 10 | - | }, | |
| 11 | - | // Custom design system colors | |
| 12 | - | colors: { | |
| 13 | - | border: "var(--border)", | |
| 14 | - | input: "var(--input)", | |
| 15 | - | ring: "var(--ring)", | |
| 16 | - | background: "var(--background)", | |
| 17 | - | foreground: "var(--foreground)", | |
| 18 | - | primary: "var(--primary)", | |
| 19 | - | "primary-foreground": "var(--primary-foreground)", | |
| 20 | - | secondary: "var(--secondary)", | |
| 21 | - | "secondary-foreground": "var(--secondary-foreground)", | |
| 22 | - | destructive: "var(--destructive)", | |
| 23 | - | "destructive-foreground": "var(--destructive-foreground)", | |
| 24 | - | muted: "var(--muted)", | |
| 25 | - | "muted-foreground": "var(--muted-foreground)", | |
| 26 | - | accent: "var(--accent)", | |
| 27 | - | "accent-foreground": "var(--accent-foreground)", | |
| 28 | - | popover: "var(--popover)", | |
| 29 | - | "popover-foreground": "var(--popover-foreground)", | |
| 30 | - | card: "var(--card)", | |
| 31 | - | "card-foreground": "var(--card-foreground)", | |
| 32 | - | // Browser-specific colors that work well in both themes | |
| 33 | - | browser: { | |
| 34 | - | bg: "rgb(var(--browser-bg) / <alpha-value>)", | |
| 35 | - | surface: "rgb(var(--browser-surface) / <alpha-value>)", | |
| 36 | - | text: "rgb(var(--browser-text) / <alpha-value>)", | |
| 37 | - | muted: "rgb(var(--browser-muted) / <alpha-value>)", | |
| 38 | - | accent: "rgb(var(--browser-accent) / <alpha-value>)", | |
| 39 | - | border: "rgb(var(--browser-border) / <alpha-value>)", | |
| 40 | - | }, | |
| 41 | - | }, | |
| 42 | - | // Animation speeds optimized for browser UI | |
| 43 | - | transitionDuration: { | |
| 44 | - | 150: "150ms", // Fast interactions | |
| 45 | - | 250: "250ms", // UI feedback | |
| 46 | - | 400: "400ms", // Smooth transitions | |
| 47 | - | }, | |
| 48 | - | // Typography scale for browser UI | |
| 49 | - | fontSize: { | |
| 50 | - | xs: ["0.75rem", "1rem"], | |
| 51 | - | sm: ["0.875rem", "1.25rem"], | |
| 52 | - | base: ["1rem", "1.5rem"], | |
| 53 | - | lg: ["1.125rem", "1.75rem"], | |
| 54 | - | }, | |
| 55 | - | // Spacing for compact UI elements | |
| 56 | - | spacing: { | |
| 57 | - | 18: "4.5rem", | |
| 58 | - | 22: "5.5rem", | |
| 59 | - | }, | |
| 60 | - | }, | |
| 61 | - | }, | |
| 62 | - | plugins: [], | |
| 63 | - | // Manual dark mode control (not system preference) | |
| 64 | - | // This allows GTK to control theme via CSS class | |
| 65 | - | darkMode: "class", | |
| 66 | - | }; | |
D webui/tsconfig.json +0 -27
| @@ -1,27 +0,0 @@ | |||
| 1 | - | { | |
| 2 | - | "compilerOptions": { | |
| 3 | - | "target": "ES2022", | |
| 4 | - | "module": "ESNext", | |
| 5 | - | "moduleResolution": "bundler", | |
| 6 | - | "lib": ["ES2022", "DOM", "DOM.Iterable"], | |
| 7 | - | "allowImportingTsExtensions": true, | |
| 8 | - | "moduleDetection": "force", | |
| 9 | - | "noEmit": true, | |
| 10 | - | "strict": true, | |
| 11 | - | "noUnusedLocals": true, | |
| 12 | - | "noUnusedParameters": true, | |
| 13 | - | "noFallthroughCasesInSwitch": true, | |
| 14 | - | "noUncheckedIndexedAccess": true, | |
| 15 | - | "skipLibCheck": true, | |
| 16 | - | "esModuleInterop": true, | |
| 17 | - | "allowSyntheticDefaultImports": true, | |
| 18 | - | "forceConsistentCasingInFileNames": true, | |
| 19 | - | "baseUrl": ".", | |
| 20 | - | "paths": { | |
| 21 | - | "$lib/*": ["src/lib/*"], | |
| 22 | - | "$components/*": ["src/components/*"] | |
| 23 | - | } | |
| 24 | - | }, | |
| 25 | - | "include": ["src/**/*", "vite.config.ts"], | |
| 26 | - | "exclude": ["node_modules", "dist"] | |
| 27 | - | } | |
D webui/vite-plugin-pages.ts +0 -153
| @@ -1,153 +0,0 @@ | |||
| 1 | - | import type { Plugin } from "vite"; | |
| 2 | - | import { writeFileSync, readFileSync, existsSync, copyFileSync } from "fs"; | |
| 3 | - | import { resolve } from "path"; | |
| 4 | - | ||
| 5 | - | interface PageConfig { | |
| 6 | - | /** Page name (used for filename and script name) */ | |
| 7 | - | name: string; | |
| 8 | - | /** Page title */ | |
| 9 | - | title: string; | |
| 10 | - | /** Script filename (e.g., 'homepage.min.js') */ | |
| 11 | - | script: string; | |
| 12 | - | /** Optional CSS filename */ | |
| 13 | - | css?: string; | |
| 14 | - | /** Target filename (defaults to name + '.html') */ | |
| 15 | - | filename?: string; | |
| 16 | - | } | |
| 17 | - | ||
| 18 | - | // Bootstrap a unified window.dumber bridge before the page bundle runs. | |
| 19 | - | // On WebKit internal pages we forward to the native message handler. | |
| 20 | - | // On CEF internal pages we fall back to the fetch-based /api/message bridge. | |
| 21 | - | function getBridgeBootstrap(): string { | |
| 22 | - | return `<script> | |
| 23 | - | (function() { | |
| 24 | - | if (window.dumber && typeof window.dumber.postMessage === "function") { | |
| 25 | - | return; | |
| 26 | - | } | |
| 27 | - | ||
| 28 | - | window.dumber = { | |
| 29 | - | postMessage: async function(msg) { | |
| 30 | - | const nativeBridge = window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.dumber; | |
| 31 | - | if (nativeBridge && typeof nativeBridge.postMessage === "function") { | |
| 32 | - | return nativeBridge.postMessage(msg); | |
| 33 | - | } | |
| 34 | - | ||
| 35 | - | try { | |
| 36 | - | const encoded = btoa(unescape(encodeURIComponent(JSON.stringify(msg)))); | |
| 37 | - | const response = await fetch("/api/message", { | |
| 38 | - | method: "POST", | |
| 39 | - | headers: { | |
| 40 | - | "Content-Type": "application/json", | |
| 41 | - | "X-Dumber-Body": encoded | |
| 42 | - | } | |
| 43 | - | }); | |
| 44 | - | const payload = await response.json(); | |
| 45 | - | if (payload && payload._callback && typeof window[payload._callback] === "function") { | |
| 46 | - | window[payload._callback](payload.data); | |
| 47 | - | } | |
| 48 | - | return payload; | |
| 49 | - | } catch (err) { | |
| 50 | - | console.error("[dumber:bridge] fetch bridge error:", err); | |
| 51 | - | return { error: err instanceof Error ? err.message : String(err) }; | |
| 52 | - | } | |
| 53 | - | } | |
| 54 | - | }; | |
| 55 | - | ||
| 56 | - | window.dispatchEvent(new CustomEvent("dumber:bridge-ready")); | |
| 57 | - | })(); | |
| 58 | - | </script>`; | |
| 59 | - | } | |
| 60 | - | ||
| 61 | - | // Load the favicon SVG from assets | |
| 62 | - | function getFaviconSVG(): string { | |
| 63 | - | const logoPath = resolve(__dirname, "..", "assets", "logo.svg"); | |
| 64 | - | return readFileSync(logoPath, "utf-8"); | |
| 65 | - | } | |
| 66 | - | ||
| 67 | - | // Generate HTML content for a page | |
| 68 | - | function generatePageHTML(config: PageConfig): string { | |
| 69 | - | const cssLink = config.css | |
| 70 | - | ? `<link rel="stylesheet" href="./${config.css}">` | |
| 71 | - | : ""; | |
| 72 | - | const bridgeBootstrap = getBridgeBootstrap(); | |
| 73 | - | ||
| 74 | - | return `<!DOCTYPE html> | |
| 75 | - | <html lang="en"> | |
| 76 | - | <head> | |
| 77 | - | <meta charset="UTF-8"> | |
| 78 | - | <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| 79 | - | <title>${config.title}</title> | |
| 80 | - | <link rel="icon" type="image/png" href="/favicon.png"> | |
| 81 | - | ${bridgeBootstrap} | |
| 82 | - | ${cssLink} | |
| 83 | - | </head> | |
| 84 | - | <body> | |
| 85 | - | <script src="./${config.script}"></script> | |
| 86 | - | </body> | |
| 87 | - | </html>`; | |
| 88 | - | } | |
| 89 | - | ||
| 90 | - | /** | |
| 91 | - | * Vite plugin to generate dumb:// protocol pages | |
| 92 | - | * Supports multiple pages like homepage, config, about, etc. | |
| 93 | - | */ | |
| 94 | - | export function pageGenerator(pages: PageConfig[]): Plugin { | |
| 95 | - | return { | |
| 96 | - | name: "dumb-page-generator", | |
| 97 | - | writeBundle() { | |
| 98 | - | const assetsDir = resolve(__dirname, "..", "assets", "webui"); | |
| 99 | - | ||
| 100 | - | try { | |
| 101 | - | // Generate HTML files for each page | |
| 102 | - | for (const page of pages) { | |
| 103 | - | const htmlContent = generatePageHTML(page); | |
| 104 | - | const filename = page.filename || `${page.name}.html`; | |
| 105 | - | writeFileSync(resolve(assetsDir, filename), htmlContent); | |
| 106 | - | console.log(`✓ Generated ${filename} for dumb://${page.name}`); | |
| 107 | - | } | |
| 108 | - | ||
| 109 | - | // Generate favicon files for scheme handler lookups | |
| 110 | - | const faviconContent = getFaviconSVG(); | |
| 111 | - | writeFileSync(resolve(assetsDir, "favicon.svg"), faviconContent); | |
| 112 | - | console.log("✓ Generated favicon.svg"); | |
| 113 | - | ||
| 114 | - | // Copy pre-generated PNG and ICO favicons if they exist | |
| 115 | - | const sourceDir = resolve(__dirname, "..", "assets", "webui"); | |
| 116 | - | try { | |
| 117 | - | const pngPath = resolve(sourceDir, "favicon.png"); | |
| 118 | - | const icoPath = resolve(sourceDir, "favicon.ico"); | |
| 119 | - | ||
| 120 | - | if (existsSync(pngPath)) { | |
| 121 | - | copyFileSync(pngPath, resolve(assetsDir, "favicon.png")); | |
| 122 | - | console.log("✓ Copied favicon.png"); | |
| 123 | - | } | |
| 124 | - | ||
| 125 | - | if (existsSync(icoPath)) { | |
| 126 | - | copyFileSync(icoPath, resolve(assetsDir, "favicon.ico")); | |
| 127 | - | console.log("✓ Copied favicon.ico"); | |
| 128 | - | } | |
| 129 | - | } catch (err) { | |
| 130 | - | console.warn("⚠ Could not copy favicon PNG/ICO files:", err); | |
| 131 | - | } | |
| 132 | - | } catch (error) { | |
| 133 | - | console.error("✗ Failed to generate page files:", error); | |
| 134 | - | throw error; | |
| 135 | - | } | |
| 136 | - | }, | |
| 137 | - | }; | |
| 138 | - | } | |
| 139 | - | ||
| 140 | - | /** | |
| 141 | - | * Convenience function for homepage generation | |
| 142 | - | * @deprecated Use pageGenerator with homepage config instead | |
| 143 | - | */ | |
| 144 | - | export function homepageGenerator(): Plugin { | |
| 145 | - | return pageGenerator([ | |
| 146 | - | { | |
| 147 | - | name: "homepage", | |
| 148 | - | title: "Dumber Browser", | |
| 149 | - | script: "homepage.min.js", | |
| 150 | - | filename: "index.html", // Special case: homepage uses index.html | |
| 151 | - | }, | |
| 152 | - | ]); | |
| 153 | - | } | |
D webui/vite.config.ts +0 -123
| @@ -1,123 +0,0 @@ | |||
| 1 | - | import { defineConfig } from "vite"; | |
| 2 | - | import { svelte } from "@sveltejs/vite-plugin-svelte"; | |
| 3 | - | import { resolve } from "path"; | |
| 4 | - | import { pageGenerator } from "./vite-plugin-pages"; | |
| 5 | - | import { copyFileSync, existsSync, mkdirSync } from "fs"; | |
| 6 | - | ||
| 7 | - | // Page configurations for dumb:// protocol pages | |
| 8 | - | const pages = { | |
| 9 | - | homepage: { | |
| 10 | - | entry: "src/pages/homepage.ts", | |
| 11 | - | output: "homepage.min.js", | |
| 12 | - | global: "DumberHomepage", | |
| 13 | - | html: { | |
| 14 | - | name: "homepage", | |
| 15 | - | title: "Dumber Browser", | |
| 16 | - | script: "homepage.min.js", | |
| 17 | - | css: "style.css", | |
| 18 | - | filename: "index.html", | |
| 19 | - | }, | |
| 20 | - | }, | |
| 21 | - | error: { | |
| 22 | - | entry: "src/pages/error.ts", | |
| 23 | - | output: "error.min.js", | |
| 24 | - | global: "DumberError", | |
| 25 | - | html: { | |
| 26 | - | name: "error", | |
| 27 | - | title: "Error", | |
| 28 | - | script: "error.min.js", | |
| 29 | - | css: "style.css", | |
| 30 | - | filename: "error.html", | |
| 31 | - | }, | |
| 32 | - | }, | |
| 33 | - | config: { | |
| 34 | - | entry: "src/pages/config.ts", | |
| 35 | - | output: "config.min.js", | |
| 36 | - | global: "DumberConfig", | |
| 37 | - | html: { | |
| 38 | - | name: "config", | |
| 39 | - | title: "Settings", | |
| 40 | - | script: "config.min.js", | |
| 41 | - | css: "style.css", | |
| 42 | - | filename: "config.html", | |
| 43 | - | }, | |
| 44 | - | }, | |
| 45 | - | webrtc: { | |
| 46 | - | entry: "src/pages/webrtc.ts", | |
| 47 | - | output: "webrtc.min.js", | |
| 48 | - | global: "DumberWebRTC", | |
| 49 | - | html: { | |
| 50 | - | name: "webrtc", | |
| 51 | - | title: "WebRTC Diagnostics", | |
| 52 | - | script: "webrtc.min.js", | |
| 53 | - | css: "style.css", | |
| 54 | - | filename: "webrtc.html", | |
| 55 | - | }, | |
| 56 | - | }, | |
| 57 | - | } as const; | |
| 58 | - | ||
| 59 | - | // Get page from VITE_PAGE env var, default to building all | |
| 60 | - | const targetPage = process.env.VITE_PAGE as keyof typeof pages | undefined; | |
| 61 | - | ||
| 62 | - | // Copy public assets to output directory | |
| 63 | - | function copyPublicAssets() { | |
| 64 | - | const outDir = resolve(__dirname, "../assets/webui"); | |
| 65 | - | const publicDir = resolve(__dirname, "public"); | |
| 66 | - | if (!existsSync(outDir)) { | |
| 67 | - | mkdirSync(outDir, { recursive: true }); | |
| 68 | - | } | |
| 69 | - | // Copy favicon.png for dumb:// URL favicons | |
| 70 | - | const faviconSrc = resolve(publicDir, "favicon.png"); | |
| 71 | - | if (existsSync(faviconSrc)) { | |
| 72 | - | copyFileSync(faviconSrc, resolve(outDir, "favicon.png")); | |
| 73 | - | } | |
| 74 | - | } | |
| 75 | - | ||
| 76 | - | // Build single page config | |
| 77 | - | function buildPageConfig(pageName: keyof typeof pages) { | |
| 78 | - | const page = pages[pageName]; | |
| 79 | - | return defineConfig({ | |
| 80 | - | plugins: [ | |
| 81 | - | svelte({ | |
| 82 | - | emitCss: false, // Inline component CSS for shadow DOM compatibility | |
| 83 | - | }), | |
| 84 | - | pageGenerator([page.html]), | |
| 85 | - | { | |
| 86 | - | name: "copy-public-assets", | |
| 87 | - | closeBundle() { | |
| 88 | - | copyPublicAssets(); | |
| 89 | - | }, | |
| 90 | - | }, | |
| 91 | - | ], | |
| 92 | - | build: { | |
| 93 | - | rollupOptions: { | |
| 94 | - | input: resolve(__dirname, page.entry), | |
| 95 | - | output: { | |
| 96 | - | dir: "../assets/webui", | |
| 97 | - | entryFileNames: page.output, | |
| 98 | - | chunkFileNames: "[name].js", | |
| 99 | - | assetFileNames: "[name].[ext]", | |
| 100 | - | manualChunks: undefined, | |
| 101 | - | format: "iife", | |
| 102 | - | name: page.global, | |
| 103 | - | }, | |
| 104 | - | }, | |
| 105 | - | emptyOutDir: false, | |
| 106 | - | target: ["es2020", "chrome91", "firefox90"], | |
| 107 | - | minify: true, | |
| 108 | - | sourcemap: false, | |
| 109 | - | cssCodeSplit: false, | |
| 110 | - | assetsInlineLimit: 4096, | |
| 111 | - | }, | |
| 112 | - | resolve: { | |
| 113 | - | alias: { | |
| 114 | - | $lib: resolve(__dirname, "src/lib"), | |
| 115 | - | $components: resolve(__dirname, "src/components"), | |
| 116 | - | }, | |
| 117 | - | }, | |
| 118 | - | }); | |
| 119 | - | } | |
| 120 | - | ||
| 121 | - | // Export config for specified page or default to homepage | |
| 122 | - | // Use VITE_PAGE=homepage or VITE_PAGE=blocked to select | |
| 123 | - | export default buildPageConfig(targetPage || "homepage"); | |