Spaces:
Running
Running
Home: POV tab + DuckDB advanced filter
Browse files- New POV tab listing one row per (round, player). SQL filter results
drop into the same table so the user goes from query to clip.
- DuckDB-wasm singleton, hf:// → https URL rewrite, 1000-row cap.
- CodeMirror SQL editor with light/dark theme via a Compartment;
Cmd/Ctrl+Enter runs the query.
- Six preset recipes adapted from the dataset card (AWP 1v1, smoke
kill, noscope/wallbang, knife, 5-kill burst, long-distance).
- Kill/duel-derived seek times land 2.5s before the event so the
lead-up is visible; tick `t` and explicit start_t stay exact.
- Match page reads ?t= on first land (one-shot, stripped on internal
nav) so deep links from the filter open at the right moment.
- bun.lock +120 -0
- package.json +8 -0
- src/lib/components/advanced-filter/advanced-filter.svelte +182 -0
- src/lib/components/advanced-filter/map-results.ts +111 -0
- src/lib/components/advanced-filter/recipes.ts +154 -0
- src/lib/components/advanced-filter/sql-editor.svelte +141 -0
- src/lib/components/match-table/cells/player-cell.svelte +16 -0
- src/lib/components/match-table/cells/start-time-cell.svelte +18 -0
- src/lib/components/match-table/columns.ts +76 -1
- src/lib/components/match-table/rows.ts +60 -0
- src/lib/duckdb.ts +94 -0
- src/routes/+page.svelte +100 -11
- src/routes/match/[matchId]/[mapName]/+page.svelte +22 -5
bun.lock
CHANGED
|
@@ -5,6 +5,14 @@
|
|
| 5 |
"": {
|
| 6 |
"name": "app",
|
| 7 |
"dependencies": {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
"hyparquet": "^1.25.6",
|
| 9 |
"hyparquet-compressors": "^1.1.1",
|
| 10 |
"mediabunny": "^1.42.0",
|
|
@@ -44,10 +52,30 @@
|
|
| 44 |
},
|
| 45 |
},
|
| 46 |
"packages": {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
"@dagrejs/dagre": ["@dagrejs/dagre@2.0.4", "", { "dependencies": { "@dagrejs/graphlib": "3.0.4" } }, "sha512-J6vCWTNpicHF4zFlZG1cS5DkGzMr9941gddYkakjrg3ZNev4bbqEgLHFTWiFrcJm7UCRu7olO3K6IRDd9gSGhA=="],
|
| 48 |
|
| 49 |
"@dagrejs/graphlib": ["@dagrejs/graphlib@3.0.4", "", {}, "sha512-HxZ7fCvAwTLCWCO0WjDkzAFQze8LdC6iOpKbetDKHIuDfIgMlIzYzqZ4nxwLlclQX+3ZVeZ1K2OuaOE2WWcyOg=="],
|
| 50 |
|
|
|
|
|
|
|
| 51 |
"@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "2.8.1" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="],
|
| 52 |
|
| 53 |
"@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "2.8.1" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
|
|
@@ -110,6 +138,14 @@
|
|
| 110 |
|
| 111 |
"@layerstack/utils": ["@layerstack/utils@2.0.0-next.18", "", { "dependencies": { "d3-array": "^3.2.4", "d3-time": "^3.1.0", "d3-time-format": "^4.1.0" } }, "sha512-EYILHpfBRYMMEahajInu9C2AXQom5IcAEdtCeucD3QIl/fdDgRbtzn6/8QW9ewumfyNZetdUvitOksmI1+gZYQ=="],
|
| 112 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "0.10.1" }, "peerDependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0" } }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="],
|
| 114 |
|
| 115 |
"@oxc-project/types": ["@oxc-project/types@0.127.0", "", {}, "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ=="],
|
|
@@ -196,6 +232,10 @@
|
|
| 196 |
|
| 197 |
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "2.8.1" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
|
| 198 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 199 |
"@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="],
|
| 200 |
|
| 201 |
"@types/d3-array": ["@types/d3-array@3.2.2", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="],
|
|
@@ -248,8 +288,12 @@
|
|
| 248 |
|
| 249 |
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
| 250 |
|
|
|
|
|
|
|
| 251 |
"aria-query": ["aria-query@5.3.1", "", {}, "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g=="],
|
| 252 |
|
|
|
|
|
|
|
| 253 |
"axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
|
| 254 |
|
| 255 |
"balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
|
|
@@ -258,20 +302,34 @@
|
|
| 258 |
|
| 259 |
"brace-expansion": ["brace-expansion@5.0.5", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="],
|
| 260 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 261 |
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
| 262 |
|
|
|
|
|
|
|
| 263 |
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "4.1.2" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
|
| 264 |
|
| 265 |
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
| 266 |
|
|
|
|
|
|
|
| 267 |
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
| 268 |
|
| 269 |
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
| 270 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 271 |
"commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="],
|
| 272 |
|
| 273 |
"cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="],
|
| 274 |
|
|
|
|
|
|
|
| 275 |
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
| 276 |
|
| 277 |
"cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
|
|
@@ -342,8 +400,16 @@
|
|
| 342 |
|
| 343 |
"devalue": ["devalue@5.7.1", "", {}, "sha512-MUbZ586EgQqdRnC4yDrlod3BEdyvE4TapGYHMW2CiaW+KkkFmWEFqBUaLltEZCGi0iFXCEjRF0OjF0DV2QHjOA=="],
|
| 344 |
|
|
|
|
|
|
|
| 345 |
"enhanced-resolve": ["enhanced-resolve@5.21.0", "", { "dependencies": { "graceful-fs": "4.2.11", "tapable": "2.3.3" } }, "sha512-otxSQPw4lkOZWkHpB3zaEQs6gWYEsmX4xQF68ElXC/TWvGxGMSGOvoNbaLXm6/cS/fSfHtsEdw90y20PCd+sCA=="],
|
| 346 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 347 |
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
| 348 |
|
| 349 |
"eslint": ["eslint@10.2.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", "@eslint/config-helpers": "^0.5.5", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.4", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-wiyGaKsDgqXvF40P8mDwiUp/KQjE1FdrIEJsM8PZ3XCiniTMXS3OHWWUe5FI5agoCnr8x4xPrTDZuxsBlNHl+Q=="],
|
|
@@ -380,22 +446,38 @@
|
|
| 380 |
|
| 381 |
"file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
|
| 382 |
|
|
|
|
|
|
|
| 383 |
"find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
|
| 384 |
|
| 385 |
"flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="],
|
| 386 |
|
|
|
|
|
|
|
| 387 |
"flatted": ["flatted@3.4.2", "", {}, "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA=="],
|
| 388 |
|
| 389 |
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
| 390 |
|
|
|
|
|
|
|
| 391 |
"fzstd": ["fzstd@0.1.1", "", {}, "sha512-dkuVSOKKwh3eas5VkJy1AW1vFpet8TA/fGmVA5krThl8YcOVE/8ZIoEA1+U1vEn5ckxxhLirSdY837azmbaNHA=="],
|
| 392 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 393 |
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
|
| 394 |
|
|
|
|
|
|
|
| 395 |
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
| 396 |
|
| 397 |
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
|
| 398 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 399 |
"hyparquet": ["hyparquet@1.25.6", "", {}, "sha512-Q9W5IjkVch3ZMnYd4qFv2q8suu5Jc36yt7J+zUNM9grwnP1S189icp0jdEQKM5HJvQkTVy8NMiQ8n/dM5QAt1A=="],
|
| 400 |
|
| 401 |
"hyparquet-compressors": ["hyparquet-compressors@1.1.1", "", { "dependencies": { "fzstd": "0.1.1", "hysnappy": "1.0.0" } }, "sha512-yx7aA3Rhj0YycbdV71+XznQSLAefa4cT0urpgNXy4aM6eSeCknaVDNne8y45Uz74Fb15yyXUzOStlceOJBan7A=="],
|
|
@@ -422,6 +504,8 @@
|
|
| 422 |
|
| 423 |
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
|
| 424 |
|
|
|
|
|
|
|
| 425 |
"json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
|
| 426 |
|
| 427 |
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
|
@@ -466,10 +550,14 @@
|
|
| 466 |
|
| 467 |
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
|
| 468 |
|
|
|
|
|
|
|
| 469 |
"lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="],
|
| 470 |
|
| 471 |
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
| 472 |
|
|
|
|
|
|
|
| 473 |
"mdn-data": ["mdn-data@2.27.1", "", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="],
|
| 474 |
|
| 475 |
"mediabunny": ["mediabunny@1.42.0", "", { "dependencies": { "@types/dom-mediacapture-transform": "0.1.11", "@types/dom-webcodecs": "0.1.13" } }, "sha512-s9ypTqLi6kbh95gC+YaJlG0PkLvMxu37Q/wO/pFZx0fUCA5Ym5mp+2dWoa83mKQ3Uo18aNlgev5iJ5ESZqWwgQ=="],
|
|
@@ -496,6 +584,8 @@
|
|
| 496 |
|
| 497 |
"node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="],
|
| 498 |
|
|
|
|
|
|
|
| 499 |
"obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="],
|
| 500 |
|
| 501 |
"optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
|
|
@@ -528,6 +618,8 @@
|
|
| 528 |
|
| 529 |
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
| 530 |
|
|
|
|
|
|
|
| 531 |
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
| 532 |
|
| 533 |
"robust-predicates": ["robust-predicates@3.0.3", "", {}, "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA=="],
|
|
@@ -552,12 +644,22 @@
|
|
| 552 |
|
| 553 |
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
| 554 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 555 |
"sirv": ["sirv@3.0.2", "", { "dependencies": { "@polka/url": "1.0.0-next.29", "mrmime": "2.0.1", "totalist": "3.0.1" } }, "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g=="],
|
| 556 |
|
| 557 |
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
| 558 |
|
| 559 |
"strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="],
|
| 560 |
|
|
|
|
|
|
|
| 561 |
"style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="],
|
| 562 |
|
| 563 |
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
|
@@ -576,6 +678,8 @@
|
|
| 576 |
|
| 577 |
"tabbable": ["tabbable@6.4.0", "", {}, "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg=="],
|
| 578 |
|
|
|
|
|
|
|
| 579 |
"tailwind-csstree": ["tailwind-csstree@0.3.1", "", { "peerDependencies": { "@eslint/css": ">=1.0.0" }, "optionalPeers": ["@eslint/css"] }, "sha512-v147gLOR+E+9H4dNaP9rBeS/S/CTQJMRItlX9jLOXjdBGfSRauLwiz7LBCViaQmn6URXIlOdN6iMzSzOaeoUUw=="],
|
| 580 |
|
| 581 |
"tailwind-merge": ["tailwind-merge@3.5.0", "", {}, "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A=="],
|
|
@@ -606,6 +710,8 @@
|
|
| 606 |
|
| 607 |
"typescript-eslint": ["typescript-eslint@8.59.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.59.1", "@typescript-eslint/parser": "8.59.1", "@typescript-eslint/typescript-estree": "8.59.1", "@typescript-eslint/utils": "8.59.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-xqDcFVBmlrltH64lklOVp1wYxgJr6LVdg3NamBgH2OOQDLFdTKfIZXF5PfghrnXQKXZGTQs8tr1vL7fJvq8CTQ=="],
|
| 608 |
|
|
|
|
|
|
|
| 609 |
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
| 610 |
|
| 611 |
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
|
|
@@ -618,10 +724,14 @@
|
|
| 618 |
|
| 619 |
"vitefu": ["vitefu@1.1.3", "", { "optionalDependencies": { "vite": "8.0.10" } }, "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg=="],
|
| 620 |
|
|
|
|
|
|
|
| 621 |
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
| 622 |
|
| 623 |
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
|
| 624 |
|
|
|
|
|
|
|
| 625 |
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
| 626 |
|
| 627 |
"zimmerframe": ["zimmerframe@1.1.4", "", {}, "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ=="],
|
|
@@ -630,6 +740,12 @@
|
|
| 630 |
|
| 631 |
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
|
| 632 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 633 |
"d3-dsv/commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="],
|
| 634 |
|
| 635 |
"d3-sankey/d3-array": ["d3-array@2.12.1", "", { "dependencies": { "internmap": "^1.0.0" } }, "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ=="],
|
|
@@ -650,6 +766,10 @@
|
|
| 650 |
|
| 651 |
"svelte-sonner/runed": ["runed@0.28.0", "", { "dependencies": { "esm-env": "1.2.2" }, "peerDependencies": { "svelte": "5.55.5" } }, "sha512-k2xx7RuO9hWcdd9f+8JoBeqWtYrm5CALfgpkg2YDB80ds/QE4w0qqu34A7fqiAwiBBSBQOid7TLxwxVC27ymWQ=="],
|
| 652 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 653 |
"d3-sankey/d3-array/internmap": ["internmap@1.0.1", "", {}, "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw=="],
|
| 654 |
|
| 655 |
"d3-sankey/d3-shape/d3-path": ["d3-path@1.0.9", "", {}, "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="],
|
|
|
|
| 5 |
"": {
|
| 6 |
"name": "app",
|
| 7 |
"dependencies": {
|
| 8 |
+
"@codemirror/commands": "^6.10.3",
|
| 9 |
+
"@codemirror/lang-sql": "^6.10.0",
|
| 10 |
+
"@codemirror/language": "^6.12.3",
|
| 11 |
+
"@codemirror/state": "^6.6.0",
|
| 12 |
+
"@codemirror/theme-one-dark": "^6.1.3",
|
| 13 |
+
"@codemirror/view": "^6.42.1",
|
| 14 |
+
"@duckdb/duckdb-wasm": "^1.33.1-dev45.0",
|
| 15 |
+
"codemirror": "^6.0.2",
|
| 16 |
"hyparquet": "^1.25.6",
|
| 17 |
"hyparquet-compressors": "^1.1.1",
|
| 18 |
"mediabunny": "^1.42.0",
|
|
|
|
| 52 |
},
|
| 53 |
},
|
| 54 |
"packages": {
|
| 55 |
+
"@codemirror/autocomplete": ["@codemirror/autocomplete@6.20.2", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0" } }, "sha512-G5FPkgIiLjOgZMjqVjvuKQ1rGPtHogLldJr33eFJdVLtmwY+giGrlv/ewljLz6b9BSQLkjxuwBc6g6omDM+YxQ=="],
|
| 56 |
+
|
| 57 |
+
"@codemirror/commands": ["@codemirror/commands@6.10.3", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.6.0", "@codemirror/view": "^6.27.0", "@lezer/common": "^1.1.0" } }, "sha512-JFRiqhKu+bvSkDLI+rUhJwSxQxYb759W5GBezE8Uc8mHLqC9aV/9aTC7yJSqCtB3F00pylrLCwnyS91Ap5ej4Q=="],
|
| 58 |
+
|
| 59 |
+
"@codemirror/lang-sql": ["@codemirror/lang-sql@6.10.0", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-6ayPkEd/yRw0XKBx5uAiToSgGECo/GY2NoJIHXIIQh1EVwLuKoU8BP/qK0qH5NLXAbtJRLuT73hx7P9X34iO4w=="],
|
| 60 |
+
|
| 61 |
+
"@codemirror/language": ["@codemirror/language@6.12.3", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.23.0", "@lezer/common": "^1.5.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0", "style-mod": "^4.0.0" } }, "sha512-QwCZW6Tt1siP37Jet9Tb02Zs81TQt6qQrZR2H+eGMcFsL1zMrk2/b9CLC7/9ieP1fjIUMgviLWMmgiHoJrj+ZA=="],
|
| 62 |
+
|
| 63 |
+
"@codemirror/lint": ["@codemirror/lint@6.9.6", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.42.0", "crelt": "^1.0.5" } }, "sha512-6Kp7r6XfCi/D/5sdXieMfg9pJU1bUEx96WITuLU6ESaKizCz0QHFMjY/TaFSbigDdEAIgi93itLBIUETP4oK+A=="],
|
| 64 |
+
|
| 65 |
+
"@codemirror/search": ["@codemirror/search@6.7.0", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.37.0", "crelt": "^1.0.5" } }, "sha512-ZvGm99wc/s2cITtMT15LFdn8aH/aS+V+DqyGq/N5ZlV5vWtH+nILvC2nw0zX7ByNoHHDZ2IxxdW38O0tc5nVHg=="],
|
| 66 |
+
|
| 67 |
+
"@codemirror/state": ["@codemirror/state@6.6.0", "", { "dependencies": { "@marijn/find-cluster-break": "^1.0.0" } }, "sha512-4nbvra5R5EtiCzr9BTHiTLc+MLXK2QGiAVYMyi8PkQd3SR+6ixar/Q/01Fa21TBIDOZXgeWV4WppsQolSreAPQ=="],
|
| 68 |
+
|
| 69 |
+
"@codemirror/theme-one-dark": ["@codemirror/theme-one-dark@6.1.3", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0", "@lezer/highlight": "^1.0.0" } }, "sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA=="],
|
| 70 |
+
|
| 71 |
+
"@codemirror/view": ["@codemirror/view@6.42.1", "", { "dependencies": { "@codemirror/state": "^6.6.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-ToN3oFc0nsxNUYVF5P0ztLgbC4UPPjPtA9aKYhkOKQaZASpOUo6ISXyQLP66ctVwlDc+j6Jv0uK5IFALkiXztg=="],
|
| 72 |
+
|
| 73 |
"@dagrejs/dagre": ["@dagrejs/dagre@2.0.4", "", { "dependencies": { "@dagrejs/graphlib": "3.0.4" } }, "sha512-J6vCWTNpicHF4zFlZG1cS5DkGzMr9941gddYkakjrg3ZNev4bbqEgLHFTWiFrcJm7UCRu7olO3K6IRDd9gSGhA=="],
|
| 74 |
|
| 75 |
"@dagrejs/graphlib": ["@dagrejs/graphlib@3.0.4", "", {}, "sha512-HxZ7fCvAwTLCWCO0WjDkzAFQze8LdC6iOpKbetDKHIuDfIgMlIzYzqZ4nxwLlclQX+3ZVeZ1K2OuaOE2WWcyOg=="],
|
| 76 |
|
| 77 |
+
"@duckdb/duckdb-wasm": ["@duckdb/duckdb-wasm@1.33.1-dev45.0", "", { "dependencies": { "apache-arrow": "^17.0.0", "qs": "^6.14.1" } }, "sha512-ETlrjhiGQzNdaOhpro/Y9u/RCcK+iyuczLy7uOn0kG5Mqlj8C+gTuhBXjs4JpK9ocdUgr3oT8zYYIbUnFD9AYA=="],
|
| 78 |
+
|
| 79 |
"@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "2.8.1" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="],
|
| 80 |
|
| 81 |
"@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "2.8.1" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
|
|
|
|
| 138 |
|
| 139 |
"@layerstack/utils": ["@layerstack/utils@2.0.0-next.18", "", { "dependencies": { "d3-array": "^3.2.4", "d3-time": "^3.1.0", "d3-time-format": "^4.1.0" } }, "sha512-EYILHpfBRYMMEahajInu9C2AXQom5IcAEdtCeucD3QIl/fdDgRbtzn6/8QW9ewumfyNZetdUvitOksmI1+gZYQ=="],
|
| 140 |
|
| 141 |
+
"@lezer/common": ["@lezer/common@1.5.2", "", {}, "sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ=="],
|
| 142 |
+
|
| 143 |
+
"@lezer/highlight": ["@lezer/highlight@1.2.3", "", { "dependencies": { "@lezer/common": "^1.3.0" } }, "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g=="],
|
| 144 |
+
|
| 145 |
+
"@lezer/lr": ["@lezer/lr@1.4.10", "", { "dependencies": { "@lezer/common": "^1.0.0" } }, "sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A=="],
|
| 146 |
+
|
| 147 |
+
"@marijn/find-cluster-break": ["@marijn/find-cluster-break@1.0.2", "", {}, "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g=="],
|
| 148 |
+
|
| 149 |
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "0.10.1" }, "peerDependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0" } }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="],
|
| 150 |
|
| 151 |
"@oxc-project/types": ["@oxc-project/types@0.127.0", "", {}, "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ=="],
|
|
|
|
| 232 |
|
| 233 |
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "2.8.1" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
|
| 234 |
|
| 235 |
+
"@types/command-line-args": ["@types/command-line-args@5.2.3", "", {}, "sha512-uv0aG6R0Y8WHZLTamZwtfsDLVRnOa+n+n5rEvFWL5Na5gZ8V2Teab/duDPFzIIIhs9qizDpcavCusCLJZu62Kw=="],
|
| 236 |
+
|
| 237 |
+
"@types/command-line-usage": ["@types/command-line-usage@5.0.4", "", {}, "sha512-BwR5KP3Es/CSht0xqBcUXS3qCAUVXwpRKsV2+arxeb65atasuXG9LykC9Ab10Cw3s2raH92ZqOeILaQbsB2ACg=="],
|
| 238 |
+
|
| 239 |
"@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="],
|
| 240 |
|
| 241 |
"@types/d3-array": ["@types/d3-array@3.2.2", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="],
|
|
|
|
| 288 |
|
| 289 |
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
| 290 |
|
| 291 |
+
"apache-arrow": ["apache-arrow@17.0.0", "", { "dependencies": { "@swc/helpers": "^0.5.11", "@types/command-line-args": "^5.2.3", "@types/command-line-usage": "^5.0.4", "@types/node": "^20.13.0", "command-line-args": "^5.2.1", "command-line-usage": "^7.0.1", "flatbuffers": "^24.3.25", "json-bignum": "^0.0.3", "tslib": "^2.6.2" }, "bin": { "arrow2csv": "bin/arrow2csv.cjs" } }, "sha512-X0p7auzdnGuhYMVKYINdQssS4EcKec9TCXyez/qtJt32DrIMGbzqiaMiQ0X6fQlQpw8Fl0Qygcv4dfRAr5Gu9Q=="],
|
| 292 |
+
|
| 293 |
"aria-query": ["aria-query@5.3.1", "", {}, "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g=="],
|
| 294 |
|
| 295 |
+
"array-back": ["array-back@3.1.0", "", {}, "sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q=="],
|
| 296 |
+
|
| 297 |
"axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
|
| 298 |
|
| 299 |
"balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
|
|
|
|
| 302 |
|
| 303 |
"brace-expansion": ["brace-expansion@5.0.5", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="],
|
| 304 |
|
| 305 |
+
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
| 306 |
+
|
| 307 |
+
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
|
| 308 |
+
|
| 309 |
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
| 310 |
|
| 311 |
+
"chalk-template": ["chalk-template@0.4.0", "", { "dependencies": { "chalk": "^4.1.2" } }, "sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg=="],
|
| 312 |
+
|
| 313 |
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "4.1.2" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
|
| 314 |
|
| 315 |
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
| 316 |
|
| 317 |
+
"codemirror": ["codemirror@6.0.2", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/commands": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/lint": "^6.0.0", "@codemirror/search": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0" } }, "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw=="],
|
| 318 |
+
|
| 319 |
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
| 320 |
|
| 321 |
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
| 322 |
|
| 323 |
+
"command-line-args": ["command-line-args@5.2.1", "", { "dependencies": { "array-back": "^3.1.0", "find-replace": "^3.0.0", "lodash.camelcase": "^4.3.0", "typical": "^4.0.0" } }, "sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg=="],
|
| 324 |
+
|
| 325 |
+
"command-line-usage": ["command-line-usage@7.0.4", "", { "dependencies": { "array-back": "^6.2.2", "chalk-template": "^0.4.0", "table-layout": "^4.1.1", "typical": "^7.3.0" } }, "sha512-85UdvzTNx/+s5CkSgBm/0hzP80RFHAa7PsfeADE5ezZF3uHz3/Tqj9gIKGT9PTtpycc3Ua64T0oVulGfKxzfqg=="],
|
| 326 |
+
|
| 327 |
"commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="],
|
| 328 |
|
| 329 |
"cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="],
|
| 330 |
|
| 331 |
+
"crelt": ["crelt@1.0.6", "", {}, "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g=="],
|
| 332 |
+
|
| 333 |
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
| 334 |
|
| 335 |
"cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
|
|
|
|
| 400 |
|
| 401 |
"devalue": ["devalue@5.7.1", "", {}, "sha512-MUbZ586EgQqdRnC4yDrlod3BEdyvE4TapGYHMW2CiaW+KkkFmWEFqBUaLltEZCGi0iFXCEjRF0OjF0DV2QHjOA=="],
|
| 402 |
|
| 403 |
+
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
| 404 |
+
|
| 405 |
"enhanced-resolve": ["enhanced-resolve@5.21.0", "", { "dependencies": { "graceful-fs": "4.2.11", "tapable": "2.3.3" } }, "sha512-otxSQPw4lkOZWkHpB3zaEQs6gWYEsmX4xQF68ElXC/TWvGxGMSGOvoNbaLXm6/cS/fSfHtsEdw90y20PCd+sCA=="],
|
| 406 |
|
| 407 |
+
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
|
| 408 |
+
|
| 409 |
+
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
| 410 |
+
|
| 411 |
+
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
|
| 412 |
+
|
| 413 |
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
| 414 |
|
| 415 |
"eslint": ["eslint@10.2.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", "@eslint/config-helpers": "^0.5.5", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.4", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-wiyGaKsDgqXvF40P8mDwiUp/KQjE1FdrIEJsM8PZ3XCiniTMXS3OHWWUe5FI5agoCnr8x4xPrTDZuxsBlNHl+Q=="],
|
|
|
|
| 446 |
|
| 447 |
"file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
|
| 448 |
|
| 449 |
+
"find-replace": ["find-replace@3.0.0", "", { "dependencies": { "array-back": "^3.0.1" } }, "sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ=="],
|
| 450 |
+
|
| 451 |
"find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
|
| 452 |
|
| 453 |
"flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="],
|
| 454 |
|
| 455 |
+
"flatbuffers": ["flatbuffers@24.12.23", "", {}, "sha512-dLVCAISd5mhls514keQzmEG6QHmUUsNuWsb4tFafIUwvvgDjXhtfAYSKOzt5SWOy+qByV5pbsDZ+Vb7HUOBEdA=="],
|
| 456 |
+
|
| 457 |
"flatted": ["flatted@3.4.2", "", {}, "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA=="],
|
| 458 |
|
| 459 |
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
| 460 |
|
| 461 |
+
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
| 462 |
+
|
| 463 |
"fzstd": ["fzstd@0.1.1", "", {}, "sha512-dkuVSOKKwh3eas5VkJy1AW1vFpet8TA/fGmVA5krThl8YcOVE/8ZIoEA1+U1vEn5ckxxhLirSdY837azmbaNHA=="],
|
| 464 |
|
| 465 |
+
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
| 466 |
+
|
| 467 |
+
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
|
| 468 |
+
|
| 469 |
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
|
| 470 |
|
| 471 |
+
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
| 472 |
+
|
| 473 |
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
| 474 |
|
| 475 |
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
|
| 476 |
|
| 477 |
+
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
|
| 478 |
+
|
| 479 |
+
"hasown": ["hasown@2.0.3", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg=="],
|
| 480 |
+
|
| 481 |
"hyparquet": ["hyparquet@1.25.6", "", {}, "sha512-Q9W5IjkVch3ZMnYd4qFv2q8suu5Jc36yt7J+zUNM9grwnP1S189icp0jdEQKM5HJvQkTVy8NMiQ8n/dM5QAt1A=="],
|
| 482 |
|
| 483 |
"hyparquet-compressors": ["hyparquet-compressors@1.1.1", "", { "dependencies": { "fzstd": "0.1.1", "hysnappy": "1.0.0" } }, "sha512-yx7aA3Rhj0YycbdV71+XznQSLAefa4cT0urpgNXy4aM6eSeCknaVDNne8y45Uz74Fb15yyXUzOStlceOJBan7A=="],
|
|
|
|
| 504 |
|
| 505 |
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
|
| 506 |
|
| 507 |
+
"json-bignum": ["json-bignum@0.0.3", "", {}, "sha512-2WHyXj3OfHSgNyuzDbSxI1w2jgw5gkWSWhS7Qg4bWXx1nLk3jnbwfUeS0PSba3IzpTUWdHxBieELUzXRjQB2zg=="],
|
| 508 |
+
|
| 509 |
"json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
|
| 510 |
|
| 511 |
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
|
|
|
| 550 |
|
| 551 |
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
|
| 552 |
|
| 553 |
+
"lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="],
|
| 554 |
+
|
| 555 |
"lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="],
|
| 556 |
|
| 557 |
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
| 558 |
|
| 559 |
+
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
| 560 |
+
|
| 561 |
"mdn-data": ["mdn-data@2.27.1", "", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="],
|
| 562 |
|
| 563 |
"mediabunny": ["mediabunny@1.42.0", "", { "dependencies": { "@types/dom-mediacapture-transform": "0.1.11", "@types/dom-webcodecs": "0.1.13" } }, "sha512-s9ypTqLi6kbh95gC+YaJlG0PkLvMxu37Q/wO/pFZx0fUCA5Ym5mp+2dWoa83mKQ3Uo18aNlgev5iJ5ESZqWwgQ=="],
|
|
|
|
| 584 |
|
| 585 |
"node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="],
|
| 586 |
|
| 587 |
+
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
|
| 588 |
+
|
| 589 |
"obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="],
|
| 590 |
|
| 591 |
"optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
|
|
|
|
| 618 |
|
| 619 |
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
| 620 |
|
| 621 |
+
"qs": ["qs@6.15.1", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg=="],
|
| 622 |
+
|
| 623 |
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
| 624 |
|
| 625 |
"robust-predicates": ["robust-predicates@3.0.3", "", {}, "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA=="],
|
|
|
|
| 644 |
|
| 645 |
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
| 646 |
|
| 647 |
+
"side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
|
| 648 |
+
|
| 649 |
+
"side-channel-list": ["side-channel-list@1.0.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4" } }, "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w=="],
|
| 650 |
+
|
| 651 |
+
"side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
|
| 652 |
+
|
| 653 |
+
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
|
| 654 |
+
|
| 655 |
"sirv": ["sirv@3.0.2", "", { "dependencies": { "@polka/url": "1.0.0-next.29", "mrmime": "2.0.1", "totalist": "3.0.1" } }, "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g=="],
|
| 656 |
|
| 657 |
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
| 658 |
|
| 659 |
"strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="],
|
| 660 |
|
| 661 |
+
"style-mod": ["style-mod@4.1.3", "", {}, "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ=="],
|
| 662 |
+
|
| 663 |
"style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="],
|
| 664 |
|
| 665 |
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
|
|
|
| 678 |
|
| 679 |
"tabbable": ["tabbable@6.4.0", "", {}, "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg=="],
|
| 680 |
|
| 681 |
+
"table-layout": ["table-layout@4.1.1", "", { "dependencies": { "array-back": "^6.2.2", "wordwrapjs": "^5.1.0" } }, "sha512-iK5/YhZxq5GO5z8wb0bY1317uDF3Zjpha0QFFLA8/trAoiLbQD0HUbMesEaxyzUgDxi2QlcbM8IvqOlEjgoXBA=="],
|
| 682 |
+
|
| 683 |
"tailwind-csstree": ["tailwind-csstree@0.3.1", "", { "peerDependencies": { "@eslint/css": ">=1.0.0" }, "optionalPeers": ["@eslint/css"] }, "sha512-v147gLOR+E+9H4dNaP9rBeS/S/CTQJMRItlX9jLOXjdBGfSRauLwiz7LBCViaQmn6URXIlOdN6iMzSzOaeoUUw=="],
|
| 684 |
|
| 685 |
"tailwind-merge": ["tailwind-merge@3.5.0", "", {}, "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A=="],
|
|
|
|
| 710 |
|
| 711 |
"typescript-eslint": ["typescript-eslint@8.59.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.59.1", "@typescript-eslint/parser": "8.59.1", "@typescript-eslint/typescript-estree": "8.59.1", "@typescript-eslint/utils": "8.59.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-xqDcFVBmlrltH64lklOVp1wYxgJr6LVdg3NamBgH2OOQDLFdTKfIZXF5PfghrnXQKXZGTQs8tr1vL7fJvq8CTQ=="],
|
| 712 |
|
| 713 |
+
"typical": ["typical@4.0.0", "", {}, "sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw=="],
|
| 714 |
+
|
| 715 |
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
| 716 |
|
| 717 |
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
|
|
|
|
| 724 |
|
| 725 |
"vitefu": ["vitefu@1.1.3", "", { "optionalDependencies": { "vite": "8.0.10" } }, "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg=="],
|
| 726 |
|
| 727 |
+
"w3c-keyname": ["w3c-keyname@2.2.8", "", {}, "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="],
|
| 728 |
+
|
| 729 |
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
| 730 |
|
| 731 |
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
|
| 732 |
|
| 733 |
+
"wordwrapjs": ["wordwrapjs@5.1.1", "", {}, "sha512-0yweIbkINJodk27gX9LBGMzyQdBDan3s/dEAiwBOj+Mf0PPyWL6/rikalkv8EeD0E8jm4o5RXEOrFTP3NXbhJg=="],
|
| 734 |
+
|
| 735 |
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
| 736 |
|
| 737 |
"zimmerframe": ["zimmerframe@1.1.4", "", {}, "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ=="],
|
|
|
|
| 740 |
|
| 741 |
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
|
| 742 |
|
| 743 |
+
"apache-arrow/@types/node": ["@types/node@20.19.41", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ=="],
|
| 744 |
+
|
| 745 |
+
"command-line-usage/array-back": ["array-back@6.2.3", "", {}, "sha512-SGDvmg6QTYiTxCBkYVmThcoa67uLl35pyzRHdpCGBOcqFy6BtwnphoFPk7LhJshD+Yk1Kt35WGWeZPTgwR4Fhw=="],
|
| 746 |
+
|
| 747 |
+
"command-line-usage/typical": ["typical@7.3.0", "", {}, "sha512-ya4mg/30vm+DOWfBg4YK3j2WD6TWtRkCbasOJr40CseYENzCUby/7rIvXA99JGsQHeNxLbnXdyLLxKSv3tauFw=="],
|
| 748 |
+
|
| 749 |
"d3-dsv/commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="],
|
| 750 |
|
| 751 |
"d3-sankey/d3-array": ["d3-array@2.12.1", "", { "dependencies": { "internmap": "^1.0.0" } }, "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ=="],
|
|
|
|
| 766 |
|
| 767 |
"svelte-sonner/runed": ["runed@0.28.0", "", { "dependencies": { "esm-env": "1.2.2" }, "peerDependencies": { "svelte": "5.55.5" } }, "sha512-k2xx7RuO9hWcdd9f+8JoBeqWtYrm5CALfgpkg2YDB80ds/QE4w0qqu34A7fqiAwiBBSBQOid7TLxwxVC27ymWQ=="],
|
| 768 |
|
| 769 |
+
"table-layout/array-back": ["array-back@6.2.3", "", {}, "sha512-SGDvmg6QTYiTxCBkYVmThcoa67uLl35pyzRHdpCGBOcqFy6BtwnphoFPk7LhJshD+Yk1Kt35WGWeZPTgwR4Fhw=="],
|
| 770 |
+
|
| 771 |
+
"apache-arrow/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
| 772 |
+
|
| 773 |
"d3-sankey/d3-array/internmap": ["internmap@1.0.1", "", {}, "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw=="],
|
| 774 |
|
| 775 |
"d3-sankey/d3-shape/d3-path": ["d3-path@1.0.9", "", {}, "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="],
|
package.json
CHANGED
|
@@ -51,6 +51,14 @@
|
|
| 51 |
"vite": "^8.0.7"
|
| 52 |
},
|
| 53 |
"dependencies": {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
"hyparquet": "^1.25.6",
|
| 55 |
"hyparquet-compressors": "^1.1.1",
|
| 56 |
"mediabunny": "^1.42.0"
|
|
|
|
| 51 |
"vite": "^8.0.7"
|
| 52 |
},
|
| 53 |
"dependencies": {
|
| 54 |
+
"@codemirror/commands": "^6.10.3",
|
| 55 |
+
"@codemirror/lang-sql": "^6.10.0",
|
| 56 |
+
"@codemirror/language": "^6.12.3",
|
| 57 |
+
"@codemirror/state": "^6.6.0",
|
| 58 |
+
"@codemirror/theme-one-dark": "^6.1.3",
|
| 59 |
+
"@codemirror/view": "^6.42.1",
|
| 60 |
+
"@duckdb/duckdb-wasm": "^1.33.1-dev45.0",
|
| 61 |
+
"codemirror": "^6.0.2",
|
| 62 |
"hyparquet": "^1.25.6",
|
| 63 |
"hyparquet-compressors": "^1.1.1",
|
| 64 |
"mediabunny": "^1.42.0"
|
src/lib/components/advanced-filter/advanced-filter.svelte
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script lang="ts">
|
| 2 |
+
import { Button } from '$lib/components/ui/button';
|
| 3 |
+
import * as Select from '$lib/components/ui/select';
|
| 4 |
+
import CaretRightIcon from 'phosphor-svelte/lib/CaretRightIcon';
|
| 5 |
+
import CaretDownIcon from 'phosphor-svelte/lib/CaretDownIcon';
|
| 6 |
+
import PlayIcon from 'phosphor-svelte/lib/PlayIcon';
|
| 7 |
+
import CircleNotchIcon from 'phosphor-svelte/lib/CircleNotchIcon';
|
| 8 |
+
import { toast } from 'svelte-sonner';
|
| 9 |
+
import { runQuery } from '$lib/duckdb';
|
| 10 |
+
import { rowsToPov } from './map-results';
|
| 11 |
+
import { RECIPES, DEFAULT_QUERY, type Recipe } from './recipes';
|
| 12 |
+
import SqlEditor from './sql-editor.svelte';
|
| 13 |
+
import type { Match, Round } from '$lib/types';
|
| 14 |
+
import type { PovRow } from '$lib/components/match-table/rows';
|
| 15 |
+
|
| 16 |
+
interface Props {
|
| 17 |
+
matches: Match[];
|
| 18 |
+
rounds: Round[];
|
| 19 |
+
activeFilterCount: number | null;
|
| 20 |
+
onResults: (rows: PovRow[]) => void;
|
| 21 |
+
onClear: () => void;
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
let { matches, rounds, activeFilterCount, onResults, onClear }: Props = $props();
|
| 25 |
+
|
| 26 |
+
let open = $state(false);
|
| 27 |
+
let query = $state(DEFAULT_QUERY);
|
| 28 |
+
let recipeId = $state<string>(RECIPES[0].id);
|
| 29 |
+
let running = $state(false);
|
| 30 |
+
let lastError = $state<string | null>(null);
|
| 31 |
+
let lastRunMs = $state<number | null>(null);
|
| 32 |
+
let lastSkipped = $state(0);
|
| 33 |
+
|
| 34 |
+
function applyRecipe(id: string) {
|
| 35 |
+
const r = RECIPES.find((x) => x.id === id);
|
| 36 |
+
if (!r) return;
|
| 37 |
+
recipeId = id;
|
| 38 |
+
query = r.sql;
|
| 39 |
+
lastError = null;
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
async function handleRun() {
|
| 43 |
+
if (running) return;
|
| 44 |
+
running = true;
|
| 45 |
+
lastError = null;
|
| 46 |
+
lastSkipped = 0;
|
| 47 |
+
const start = performance.now();
|
| 48 |
+
try {
|
| 49 |
+
const res = await runQuery(query, { limit: 1000 });
|
| 50 |
+
const mapped = rowsToPov(res.rows, matches, rounds);
|
| 51 |
+
lastRunMs = performance.now() - start;
|
| 52 |
+
lastSkipped = mapped.skipped;
|
| 53 |
+
if (!mapped.rows.length) {
|
| 54 |
+
toast.info('Query ran, but produced 0 POV rows', {
|
| 55 |
+
description: mapped.skipped
|
| 56 |
+
? `Skipped ${mapped.skipped} rows missing match_id / map_name / round / player_slot.`
|
| 57 |
+
: 'No matching POVs in the dataset.'
|
| 58 |
+
});
|
| 59 |
+
}
|
| 60 |
+
onResults(mapped.rows);
|
| 61 |
+
} catch (err) {
|
| 62 |
+
const msg = (err as Error)?.message ?? String(err);
|
| 63 |
+
lastError = msg;
|
| 64 |
+
toast.error('Query failed', { description: msg.slice(0, 240) });
|
| 65 |
+
} finally {
|
| 66 |
+
running = false;
|
| 67 |
+
}
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
function handleClear() {
|
| 71 |
+
onClear();
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
const activeRecipe = $derived(RECIPES.find((r) => r.id === recipeId) as Recipe | undefined);
|
| 75 |
+
</script>
|
| 76 |
+
|
| 77 |
+
<section class="rounded-md border bg-card/40">
|
| 78 |
+
<button
|
| 79 |
+
type="button"
|
| 80 |
+
onclick={() => (open = !open)}
|
| 81 |
+
class="flex w-full items-center justify-between gap-2 px-3 py-2 text-left"
|
| 82 |
+
aria-expanded={open}
|
| 83 |
+
>
|
| 84 |
+
<span class="flex items-center gap-2 text-sm font-medium">
|
| 85 |
+
{#if open}
|
| 86 |
+
<CaretDownIcon size={14} weight="bold" />
|
| 87 |
+
{:else}
|
| 88 |
+
<CaretRightIcon size={14} weight="bold" />
|
| 89 |
+
{/if}
|
| 90 |
+
Advanced filter
|
| 91 |
+
<span class="font-mono text-[10px] tracking-wider text-muted-foreground uppercase"
|
| 92 |
+
>DuckDB · SQL</span
|
| 93 |
+
>
|
| 94 |
+
</span>
|
| 95 |
+
<span class="flex items-center gap-2 text-xs text-muted-foreground">
|
| 96 |
+
{#if activeFilterCount !== null}
|
| 97 |
+
<span class="rounded bg-primary/15 px-1.5 py-0.5 font-medium text-primary">
|
| 98 |
+
{activeFilterCount} active
|
| 99 |
+
</span>
|
| 100 |
+
{/if}
|
| 101 |
+
</span>
|
| 102 |
+
</button>
|
| 103 |
+
|
| 104 |
+
{#if open}
|
| 105 |
+
<div class="space-y-3 border-t px-3 py-3">
|
| 106 |
+
<div class="flex flex-wrap items-center gap-2">
|
| 107 |
+
<div class="flex items-center gap-2">
|
| 108 |
+
<span class="text-xs text-muted-foreground">Recipe</span>
|
| 109 |
+
<Select.Root type="single" value={recipeId} onValueChange={(v) => v && applyRecipe(v)}>
|
| 110 |
+
<Select.Trigger class="h-8 w-[14rem] text-xs">
|
| 111 |
+
{activeRecipe?.label ?? 'Pick a recipe…'}
|
| 112 |
+
</Select.Trigger>
|
| 113 |
+
<Select.Content>
|
| 114 |
+
{#each RECIPES as r (r.id)}
|
| 115 |
+
<Select.Item value={r.id}>
|
| 116 |
+
<div class="flex flex-col">
|
| 117 |
+
<span class="text-sm">{r.label}</span>
|
| 118 |
+
<span class="text-[11px] text-muted-foreground">{r.description}</span>
|
| 119 |
+
</div>
|
| 120 |
+
</Select.Item>
|
| 121 |
+
{/each}
|
| 122 |
+
</Select.Content>
|
| 123 |
+
</Select.Root>
|
| 124 |
+
</div>
|
| 125 |
+
<div class="grow"></div>
|
| 126 |
+
<Button
|
| 127 |
+
size="sm"
|
| 128 |
+
variant="default"
|
| 129 |
+
onclick={handleRun}
|
| 130 |
+
disabled={running}
|
| 131 |
+
class="h-8 gap-1.5"
|
| 132 |
+
>
|
| 133 |
+
{#if running}
|
| 134 |
+
<CircleNotchIcon size={14} class="animate-spin" />
|
| 135 |
+
Running
|
| 136 |
+
{:else}
|
| 137 |
+
<PlayIcon size={14} weight="fill" />
|
| 138 |
+
Run query
|
| 139 |
+
{/if}
|
| 140 |
+
</Button>
|
| 141 |
+
<Button
|
| 142 |
+
size="sm"
|
| 143 |
+
variant="outline"
|
| 144 |
+
onclick={handleClear}
|
| 145 |
+
disabled={activeFilterCount === null || running}
|
| 146 |
+
class="h-8"
|
| 147 |
+
>
|
| 148 |
+
Clear
|
| 149 |
+
</Button>
|
| 150 |
+
</div>
|
| 151 |
+
|
| 152 |
+
<SqlEditor bind:value={query} onSubmit={handleRun} />
|
| 153 |
+
|
| 154 |
+
<div class="flex flex-wrap items-center gap-x-3 gap-y-1 text-[11px] text-muted-foreground">
|
| 155 |
+
<span>
|
| 156 |
+
⌘/Ctrl + Enter runs the query. Use <code class="font-mono">hf://datasets/…</code> URLs
|
| 157 |
+
— they're rewritten to public HTTPS automatically.
|
| 158 |
+
</span>
|
| 159 |
+
{#if lastRunMs !== null}
|
| 160 |
+
<span class="font-mono">
|
| 161 |
+
{Math.round(lastRunMs)} ms{lastSkipped ? ` · skipped ${lastSkipped}` : ''}
|
| 162 |
+
</span>
|
| 163 |
+
{/if}
|
| 164 |
+
</div>
|
| 165 |
+
|
| 166 |
+
{#if lastError}
|
| 167 |
+
<pre
|
| 168 |
+
class="overflow-x-auto rounded-md border border-destructive/40 bg-destructive/5 p-2 text-[11px] text-destructive whitespace-pre-wrap">{lastError}</pre>
|
| 169 |
+
{/if}
|
| 170 |
+
|
| 171 |
+
<p class="text-[11px] leading-relaxed text-muted-foreground">
|
| 172 |
+
Your SELECT must expose <code class="font-mono">match_id</code>,
|
| 173 |
+
<code class="font-mono">map_name</code>, <code class="font-mono">round</code>, a
|
| 174 |
+
<code class="font-mono">player_slot</code> (or any of <code class="font-mono"
|
| 175 |
+
>winner_player_slot / attacker_player_slot / victim_player_slot / target_player_slot / other_player_slot</code
|
| 176 |
+
>), and a time column (<code class="font-mono">event_video_seconds</code>,
|
| 177 |
+
<code class="font-mono">event_seconds</code>, or <code class="font-mono">t</code>). Rows
|
| 178 |
+
missing these are dropped from the POV list.
|
| 179 |
+
</p>
|
| 180 |
+
</div>
|
| 181 |
+
{/if}
|
| 182 |
+
</section>
|
src/lib/components/advanced-filter/map-results.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { Match, Round } from '$lib/types';
|
| 2 |
+
import type { PovRow } from '$lib/components/match-table/rows';
|
| 3 |
+
|
| 4 |
+
const TICK_RATE = 64;
|
| 5 |
+
|
| 6 |
+
// Column-name aliases we'll accept from the user's SELECT. Order matters:
|
| 7 |
+
// the first match wins.
|
| 8 |
+
const PLAYER_KEYS = [
|
| 9 |
+
'player_slot',
|
| 10 |
+
'winner_player_slot',
|
| 11 |
+
'attacker_player_slot',
|
| 12 |
+
'victim_player_slot',
|
| 13 |
+
'target_player_slot',
|
| 14 |
+
'other_player_slot',
|
| 15 |
+
'player'
|
| 16 |
+
] as const;
|
| 17 |
+
|
| 18 |
+
// Timestamp candidates, ordered by preference. `multiplier` translates from
|
| 19 |
+
// the source's time base into POV video seconds — the events tables store
|
| 20 |
+
// `event_seconds` on a half-rate clock per the dataset README, so they need
|
| 21 |
+
// ×2 to seek correctly. `lead` rolls the seek back so kills/duels open with
|
| 22 |
+
// a couple seconds of context before the actual moment. `start_t` is a
|
| 23 |
+
// user-controlled escape hatch with no lead.
|
| 24 |
+
const LEAD_S = 2.5;
|
| 25 |
+
const TIME_SOURCES = [
|
| 26 |
+
{ key: 'event_video_seconds', multiplier: 1, lead: LEAD_S },
|
| 27 |
+
{ key: 'first_kill_video_seconds', multiplier: 1, lead: LEAD_S },
|
| 28 |
+
{ key: 'last_kill_video_seconds', multiplier: 1, lead: LEAD_S },
|
| 29 |
+
{ key: 'start_t', multiplier: 1, lead: 0 },
|
| 30 |
+
{ key: 'event_seconds', multiplier: 2, lead: LEAD_S },
|
| 31 |
+
{ key: 't', multiplier: 1, lead: 0 }
|
| 32 |
+
] as const;
|
| 33 |
+
|
| 34 |
+
function pickPlayer(row: Record<string, unknown>): number | null {
|
| 35 |
+
for (const k of PLAYER_KEYS) {
|
| 36 |
+
const v = row[k];
|
| 37 |
+
if (typeof v === 'number' && Number.isFinite(v)) return v;
|
| 38 |
+
}
|
| 39 |
+
return null;
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
function pickStartT(row: Record<string, unknown>): number | undefined {
|
| 43 |
+
for (const src of TIME_SOURCES) {
|
| 44 |
+
const v = row[src.key];
|
| 45 |
+
if (typeof v !== 'number' || !Number.isFinite(v)) continue;
|
| 46 |
+
return Math.max(0, v * src.multiplier - src.lead);
|
| 47 |
+
}
|
| 48 |
+
return undefined;
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
function pickString(row: Record<string, unknown>, key: string): string | null {
|
| 52 |
+
const v = row[key];
|
| 53 |
+
return typeof v === 'string' ? v : null;
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
function pickNumber(row: Record<string, unknown>, key: string): number | null {
|
| 57 |
+
const v = row[key];
|
| 58 |
+
return typeof v === 'number' && Number.isFinite(v) ? v : null;
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
/**
|
| 62 |
+
* Map raw DuckDB result rows back to POV rows the existing table can render.
|
| 63 |
+
* Required columns: `match_id`, `map_name`, `round`, and one of the player
|
| 64 |
+
* keys above. Rows missing any of these are dropped (and counted).
|
| 65 |
+
*/
|
| 66 |
+
export function rowsToPov(
|
| 67 |
+
rawRows: Record<string, unknown>[],
|
| 68 |
+
matches: Match[],
|
| 69 |
+
rounds: Round[]
|
| 70 |
+
): { rows: PovRow[]; skipped: number } {
|
| 71 |
+
const matchByMap = new Map<string, Match>();
|
| 72 |
+
for (const m of matches) matchByMap.set(`${m.match_id}|${m.map_name}`, m);
|
| 73 |
+
|
| 74 |
+
const roundByKey = new Map<string, Round>();
|
| 75 |
+
for (const r of rounds) roundByKey.set(`${r.match_id}|${r.map_name}|${r.round}`, r);
|
| 76 |
+
|
| 77 |
+
let skipped = 0;
|
| 78 |
+
const out: PovRow[] = [];
|
| 79 |
+
for (const r of rawRows) {
|
| 80 |
+
const match_id = pickNumber(r, 'match_id');
|
| 81 |
+
const map_name = pickString(r, 'map_name');
|
| 82 |
+
const round = pickNumber(r, 'round');
|
| 83 |
+
const player = pickPlayer(r);
|
| 84 |
+
if (match_id === null || map_name === null || round === null || player === null) {
|
| 85 |
+
skipped++;
|
| 86 |
+
continue;
|
| 87 |
+
}
|
| 88 |
+
const m = matchByMap.get(`${match_id}|${map_name}`);
|
| 89 |
+
const rd = roundByKey.get(`${match_id}|${map_name}|${round}`);
|
| 90 |
+
const duration_s = rd ? (rd.round_duration_ticks || 0) / TICK_RATE : 0;
|
| 91 |
+
out.push({
|
| 92 |
+
match_id,
|
| 93 |
+
map_name,
|
| 94 |
+
round,
|
| 95 |
+
player,
|
| 96 |
+
duration_s,
|
| 97 |
+
event: m?.event ?? '',
|
| 98 |
+
team1: m?.team1 ?? '',
|
| 99 |
+
team2: m?.team2 ?? '',
|
| 100 |
+
score1: m?.score1 ?? 0,
|
| 101 |
+
score2: m?.score2 ?? 0,
|
| 102 |
+
winner: (m?.winner ?? 'team1') as Match['winner'],
|
| 103 |
+
format: m?.format ?? '',
|
| 104 |
+
match_date: m?.match_date ?? '',
|
| 105 |
+
uploaded_at: rd?.uploaded_at ?? m?.uploaded_at ?? '',
|
| 106 |
+
rounds_played: m?.rounds_played ?? 0,
|
| 107 |
+
start_t: pickStartT(r)
|
| 108 |
+
});
|
| 109 |
+
}
|
| 110 |
+
return { rows: out, skipped };
|
| 111 |
+
}
|
src/lib/components/advanced-filter/recipes.ts
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Curated SQL recipes adapted from the OpenCS2 dataset card. Each query must
|
| 2 |
+
// SELECT match_id / map_name / round / a *player_slot column and a timestamp
|
| 3 |
+
// column the result mapper understands (see ./map-results.ts).
|
| 4 |
+
|
| 5 |
+
export type Recipe = {
|
| 6 |
+
id: string;
|
| 7 |
+
label: string;
|
| 8 |
+
description: string;
|
| 9 |
+
sql: string;
|
| 10 |
+
};
|
| 11 |
+
|
| 12 |
+
const POV = `'hf://datasets/blanchon/opencs2_dataset/index/pov_rounds.parquet'`;
|
| 13 |
+
const KILLS = `'hf://datasets/blanchon/opencs2_dataset/events/kills.parquet'`;
|
| 14 |
+
const DUELS = `'hf://datasets/blanchon/opencs2_dataset/events/duels.parquet'`;
|
| 15 |
+
|
| 16 |
+
export const RECIPES: Recipe[] = [
|
| 17 |
+
{
|
| 18 |
+
id: 'awp_1v1_duel',
|
| 19 |
+
label: 'AWP 1v1 duel',
|
| 20 |
+
description: 'Winner POV for AWP kills when the duel started as a 1v1.',
|
| 21 |
+
sql: `SELECT
|
| 22 |
+
d.match_id,
|
| 23 |
+
d.map_name,
|
| 24 |
+
d.round,
|
| 25 |
+
d.winner_player_slot AS player_slot,
|
| 26 |
+
d.event_seconds * 2.0 AS event_video_seconds,
|
| 27 |
+
d.weapon,
|
| 28 |
+
d.distance,
|
| 29 |
+
d.headshot
|
| 30 |
+
FROM ${DUELS} d
|
| 31 |
+
JOIN ${POV} p
|
| 32 |
+
ON d.match_id = p.match_id
|
| 33 |
+
AND d.map_name = p.map_name
|
| 34 |
+
AND d.round = p.round
|
| 35 |
+
AND d.winner_player_slot = p.player_slot
|
| 36 |
+
WHERE d.weapon = 'awp'
|
| 37 |
+
AND d.is_1v1_before
|
| 38 |
+
ORDER BY d.event_seconds
|
| 39 |
+
LIMIT 50`
|
| 40 |
+
},
|
| 41 |
+
{
|
| 42 |
+
id: 'kill_through_smoke',
|
| 43 |
+
label: 'Kill through smoke',
|
| 44 |
+
description: 'Attacker POV when the kill went through a smoke grenade.',
|
| 45 |
+
sql: `SELECT
|
| 46 |
+
k.match_id,
|
| 47 |
+
k.map_name,
|
| 48 |
+
k.round,
|
| 49 |
+
k.attacker_player_slot AS player_slot,
|
| 50 |
+
k.event_seconds * 2.0 AS event_video_seconds,
|
| 51 |
+
k.weapon,
|
| 52 |
+
k.distance,
|
| 53 |
+
k.headshot
|
| 54 |
+
FROM ${KILLS} k
|
| 55 |
+
JOIN ${POV} p
|
| 56 |
+
ON k.match_id = p.match_id
|
| 57 |
+
AND k.map_name = p.map_name
|
| 58 |
+
AND k.round = p.round
|
| 59 |
+
AND k.attacker_player_slot = p.player_slot
|
| 60 |
+
WHERE k.through_smoke
|
| 61 |
+
LIMIT 50`
|
| 62 |
+
},
|
| 63 |
+
{
|
| 64 |
+
id: 'noscope_or_wallbang',
|
| 65 |
+
label: 'Noscope / wallbang',
|
| 66 |
+
description: 'Attacker POV for noscope, wallbang or penetration kills.',
|
| 67 |
+
sql: `SELECT
|
| 68 |
+
k.match_id,
|
| 69 |
+
k.map_name,
|
| 70 |
+
k.round,
|
| 71 |
+
k.attacker_player_slot AS player_slot,
|
| 72 |
+
k.event_seconds * 2.0 AS event_video_seconds,
|
| 73 |
+
k.weapon,
|
| 74 |
+
k.noscope,
|
| 75 |
+
k.wallbang,
|
| 76 |
+
k.penetrated
|
| 77 |
+
FROM ${KILLS} k
|
| 78 |
+
JOIN ${POV} p
|
| 79 |
+
ON k.match_id = p.match_id
|
| 80 |
+
AND k.map_name = p.map_name
|
| 81 |
+
AND k.round = p.round
|
| 82 |
+
AND k.attacker_player_slot = p.player_slot
|
| 83 |
+
WHERE (k.noscope OR k.wallbang OR k.penetrated > 0)
|
| 84 |
+
ORDER BY k.noscope DESC, k.wallbang DESC, k.penetrated DESC
|
| 85 |
+
LIMIT 50`
|
| 86 |
+
},
|
| 87 |
+
{
|
| 88 |
+
id: 'knife_kill',
|
| 89 |
+
label: 'Knife kill',
|
| 90 |
+
description: 'Attacker POV for actual knife kills (not just rounds spent holding a knife).',
|
| 91 |
+
sql: `SELECT
|
| 92 |
+
k.match_id,
|
| 93 |
+
k.map_name,
|
| 94 |
+
k.round,
|
| 95 |
+
k.attacker_player_slot AS player_slot,
|
| 96 |
+
k.event_seconds * 2.0 AS event_video_seconds,
|
| 97 |
+
k.weapon
|
| 98 |
+
FROM ${KILLS} k
|
| 99 |
+
JOIN ${POV} p
|
| 100 |
+
ON k.match_id = p.match_id
|
| 101 |
+
AND k.map_name = p.map_name
|
| 102 |
+
AND k.round = p.round
|
| 103 |
+
AND k.attacker_player_slot = p.player_slot
|
| 104 |
+
WHERE lower(k.weapon_class) = 'knife'
|
| 105 |
+
OR lower(k.weapon) LIKE '%knife%'
|
| 106 |
+
OR lower(k.weapon) LIKE '%bayonet%'
|
| 107 |
+
OR lower(k.weapon) LIKE '%karambit%'
|
| 108 |
+
LIMIT 50`
|
| 109 |
+
},
|
| 110 |
+
{
|
| 111 |
+
id: 'five_kills_under_10s',
|
| 112 |
+
label: '5 kills in < 10s',
|
| 113 |
+
description: 'Player POV when the same player got ≥5 kills within a 10-second window.',
|
| 114 |
+
sql: `WITH streaks AS (
|
| 115 |
+
SELECT match_id, map_name, round, attacker_player_slot AS player_slot,
|
| 116 |
+
COUNT(*) AS n_kills,
|
| 117 |
+
MIN(event_seconds) * 2.0 AS first_kill_video_seconds,
|
| 118 |
+
MAX(event_seconds) - MIN(event_seconds) AS window_s
|
| 119 |
+
FROM ${KILLS}
|
| 120 |
+
GROUP BY match_id, map_name, round, attacker_player_slot
|
| 121 |
+
HAVING COUNT(*) >= 5 AND MAX(event_seconds) - MIN(event_seconds) < 10.0
|
| 122 |
+
)
|
| 123 |
+
SELECT s.match_id, s.map_name, s.round, s.player_slot,
|
| 124 |
+
s.first_kill_video_seconds AS event_video_seconds,
|
| 125 |
+
s.n_kills, s.window_s
|
| 126 |
+
FROM streaks s
|
| 127 |
+
ORDER BY s.window_s
|
| 128 |
+
LIMIT 50`
|
| 129 |
+
},
|
| 130 |
+
{
|
| 131 |
+
id: 'long_distance_kill',
|
| 132 |
+
label: 'Long-distance kill',
|
| 133 |
+
description: 'Attacker POV for the longest-recorded kills by in-engine distance.',
|
| 134 |
+
sql: `SELECT
|
| 135 |
+
k.match_id,
|
| 136 |
+
k.map_name,
|
| 137 |
+
k.round,
|
| 138 |
+
k.attacker_player_slot AS player_slot,
|
| 139 |
+
k.event_seconds * 2.0 AS event_video_seconds,
|
| 140 |
+
k.weapon,
|
| 141 |
+
k.distance
|
| 142 |
+
FROM ${KILLS} k
|
| 143 |
+
JOIN ${POV} p
|
| 144 |
+
ON k.match_id = p.match_id
|
| 145 |
+
AND k.map_name = p.map_name
|
| 146 |
+
AND k.round = p.round
|
| 147 |
+
AND k.attacker_player_slot = p.player_slot
|
| 148 |
+
WHERE k.distance IS NOT NULL
|
| 149 |
+
ORDER BY k.distance DESC
|
| 150 |
+
LIMIT 50`
|
| 151 |
+
}
|
| 152 |
+
];
|
| 153 |
+
|
| 154 |
+
export const DEFAULT_QUERY = RECIPES[0].sql;
|
src/lib/components/advanced-filter/sql-editor.svelte
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script lang="ts">
|
| 2 |
+
import { onMount, onDestroy, untrack } from 'svelte';
|
| 3 |
+
import { mode } from 'mode-watcher';
|
| 4 |
+
import { Compartment, EditorState, type Extension } from '@codemirror/state';
|
| 5 |
+
import { EditorView, keymap, lineNumbers, highlightActiveLine } from '@codemirror/view';
|
| 6 |
+
import { defaultKeymap, history, historyKeymap, indentWithTab } from '@codemirror/commands';
|
| 7 |
+
import {
|
| 8 |
+
bracketMatching,
|
| 9 |
+
indentOnInput,
|
| 10 |
+
syntaxHighlighting,
|
| 11 |
+
defaultHighlightStyle
|
| 12 |
+
} from '@codemirror/language';
|
| 13 |
+
import { sql, PostgreSQL } from '@codemirror/lang-sql';
|
| 14 |
+
import { oneDark } from '@codemirror/theme-one-dark';
|
| 15 |
+
|
| 16 |
+
interface Props {
|
| 17 |
+
value: string;
|
| 18 |
+
onChange?: (v: string) => void;
|
| 19 |
+
onSubmit?: () => void;
|
| 20 |
+
placeholder?: string;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
let { value = $bindable(), onChange, onSubmit, placeholder = '' }: Props = $props();
|
| 24 |
+
|
| 25 |
+
let hostEl: HTMLDivElement | undefined = $state();
|
| 26 |
+
let view: EditorView | null = null;
|
| 27 |
+
const themeCompartment = new Compartment();
|
| 28 |
+
// Tracks whether the latest change came from the editor itself; suppresses
|
| 29 |
+
// the round-trip `value` → `view.dispatch(value)` echo that would otherwise
|
| 30 |
+
// move the cursor every keystroke.
|
| 31 |
+
let internalUpdate = false;
|
| 32 |
+
|
| 33 |
+
function themeExtension(dark: boolean): Extension {
|
| 34 |
+
return dark ? oneDark : [];
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
function buildExtensions(): Extension[] {
|
| 38 |
+
return [
|
| 39 |
+
lineNumbers(),
|
| 40 |
+
history(),
|
| 41 |
+
indentOnInput(),
|
| 42 |
+
bracketMatching(),
|
| 43 |
+
highlightActiveLine(),
|
| 44 |
+
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
|
| 45 |
+
sql({ dialect: PostgreSQL, upperCaseKeywords: true }),
|
| 46 |
+
keymap.of([
|
| 47 |
+
...defaultKeymap,
|
| 48 |
+
...historyKeymap,
|
| 49 |
+
indentWithTab,
|
| 50 |
+
{
|
| 51 |
+
key: 'Mod-Enter',
|
| 52 |
+
preventDefault: true,
|
| 53 |
+
run: () => {
|
| 54 |
+
onSubmit?.();
|
| 55 |
+
return true;
|
| 56 |
+
}
|
| 57 |
+
}
|
| 58 |
+
]),
|
| 59 |
+
EditorView.lineWrapping,
|
| 60 |
+
EditorView.theme({
|
| 61 |
+
'&': {
|
| 62 |
+
fontSize: '12.5px',
|
| 63 |
+
backgroundColor: 'transparent'
|
| 64 |
+
},
|
| 65 |
+
'.cm-scroller': {
|
| 66 |
+
fontFamily:
|
| 67 |
+
'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
|
| 68 |
+
maxHeight: '320px'
|
| 69 |
+
},
|
| 70 |
+
'.cm-gutters': {
|
| 71 |
+
backgroundColor: 'transparent',
|
| 72 |
+
borderRight: '1px solid hsl(var(--border) / 0.6)'
|
| 73 |
+
},
|
| 74 |
+
'.cm-activeLine': {
|
| 75 |
+
backgroundColor: 'transparent'
|
| 76 |
+
},
|
| 77 |
+
'.cm-activeLineGutter': {
|
| 78 |
+
backgroundColor: 'transparent'
|
| 79 |
+
}
|
| 80 |
+
}),
|
| 81 |
+
EditorView.updateListener.of((u) => {
|
| 82 |
+
if (!u.docChanged) return;
|
| 83 |
+
internalUpdate = true;
|
| 84 |
+
const next = u.state.doc.toString();
|
| 85 |
+
value = next;
|
| 86 |
+
onChange?.(next);
|
| 87 |
+
internalUpdate = false;
|
| 88 |
+
}),
|
| 89 |
+
themeCompartment.of(themeExtension(false))
|
| 90 |
+
];
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
onMount(() => {
|
| 94 |
+
if (!hostEl) return;
|
| 95 |
+
const dark = mode.current === 'dark';
|
| 96 |
+
view = new EditorView({
|
| 97 |
+
state: EditorState.create({
|
| 98 |
+
doc: value,
|
| 99 |
+
extensions: buildExtensions()
|
| 100 |
+
}),
|
| 101 |
+
parent: hostEl
|
| 102 |
+
});
|
| 103 |
+
// Apply initial theme via the compartment so we can swap it later
|
| 104 |
+
// without rebuilding the whole extension array.
|
| 105 |
+
view.dispatch({
|
| 106 |
+
effects: themeCompartment.reconfigure(themeExtension(dark))
|
| 107 |
+
});
|
| 108 |
+
});
|
| 109 |
+
|
| 110 |
+
onDestroy(() => {
|
| 111 |
+
view?.destroy();
|
| 112 |
+
view = null;
|
| 113 |
+
});
|
| 114 |
+
|
| 115 |
+
// Sync external `value` changes (recipe selection etc.) into the editor.
|
| 116 |
+
$effect(() => {
|
| 117 |
+
const v = value;
|
| 118 |
+
if (!view) return;
|
| 119 |
+
if (untrack(() => internalUpdate)) return;
|
| 120 |
+
const cur = view.state.doc.toString();
|
| 121 |
+
if (cur === v) return;
|
| 122 |
+
view.dispatch({
|
| 123 |
+
changes: { from: 0, to: cur.length, insert: v }
|
| 124 |
+
});
|
| 125 |
+
});
|
| 126 |
+
|
| 127 |
+
// Swap theme on color-scheme toggle via the compartment.
|
| 128 |
+
$effect(() => {
|
| 129 |
+
const dark = mode.current === 'dark';
|
| 130 |
+
if (!view) return;
|
| 131 |
+
view.dispatch({
|
| 132 |
+
effects: themeCompartment.reconfigure(themeExtension(dark))
|
| 133 |
+
});
|
| 134 |
+
});
|
| 135 |
+
</script>
|
| 136 |
+
|
| 137 |
+
<div
|
| 138 |
+
bind:this={hostEl}
|
| 139 |
+
class="overflow-hidden rounded-md border bg-background text-foreground focus-within:ring-2 focus-within:ring-ring/40"
|
| 140 |
+
data-placeholder={placeholder || undefined}
|
| 141 |
+
></div>
|
src/lib/components/match-table/cells/player-cell.svelte
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script lang="ts">
|
| 2 |
+
import { playerColor } from '$lib/utils/player-colors';
|
| 3 |
+
|
| 4 |
+
interface Props {
|
| 5 |
+
player: number;
|
| 6 |
+
}
|
| 7 |
+
let { player }: Props = $props();
|
| 8 |
+
const color = $derived(playerColor(player));
|
| 9 |
+
</script>
|
| 10 |
+
|
| 11 |
+
<div
|
| 12 |
+
class="inline-flex size-6 items-center justify-center rounded-sm font-mono text-[11px] font-bold text-white/95 shadow-sm ring-1 ring-black/20"
|
| 13 |
+
style="background-color: {color};"
|
| 14 |
+
>
|
| 15 |
+
{player}
|
| 16 |
+
</div>
|
src/lib/components/match-table/cells/start-time-cell.svelte
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script lang="ts">
|
| 2 |
+
interface Props {
|
| 3 |
+
seconds?: number;
|
| 4 |
+
}
|
| 5 |
+
let { seconds }: Props = $props();
|
| 6 |
+
|
| 7 |
+
function fmt(s: number): string {
|
| 8 |
+
const m = Math.floor(s / 60);
|
| 9 |
+
const r = s - m * 60;
|
| 10 |
+
return `${m}:${r.toFixed(1).padStart(4, '0')}`;
|
| 11 |
+
}
|
| 12 |
+
</script>
|
| 13 |
+
|
| 14 |
+
{#if seconds === undefined || !Number.isFinite(seconds)}
|
| 15 |
+
<span class="font-mono text-xs text-muted-foreground/40">—</span>
|
| 16 |
+
{:else}
|
| 17 |
+
<span class="font-mono text-xs text-foreground tabular-nums">{fmt(seconds)}</span>
|
| 18 |
+
{/if}
|
src/lib/components/match-table/columns.ts
CHANGED
|
@@ -13,7 +13,9 @@ import DurationCell from './cells/duration-cell.svelte';
|
|
| 13 |
import DateCell from './cells/date-cell.svelte';
|
| 14 |
import RoundCell from './cells/round-cell.svelte';
|
| 15 |
import RoundsPlayedCell from './cells/rounds-played-cell.svelte';
|
| 16 |
-
import
|
|
|
|
|
|
|
| 17 |
import type { Match } from '$lib/types';
|
| 18 |
|
| 19 |
declare module '@tanstack/table-core' {
|
|
@@ -203,6 +205,79 @@ export const mapColumns: ColumnDef<MapRow>[] = [
|
|
| 203 |
uploadedColumn<MapRow>()
|
| 204 |
];
|
| 205 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 206 |
export const matchColumns: ColumnDef<MatchRow>[] = [
|
| 207 |
matchIdColumn<MatchRow>(),
|
| 208 |
eventColumn<MatchRow>(),
|
|
|
|
| 13 |
import DateCell from './cells/date-cell.svelte';
|
| 14 |
import RoundCell from './cells/round-cell.svelte';
|
| 15 |
import RoundsPlayedCell from './cells/rounds-played-cell.svelte';
|
| 16 |
+
import PlayerCell from './cells/player-cell.svelte';
|
| 17 |
+
import StartTimeCell from './cells/start-time-cell.svelte';
|
| 18 |
+
import type { MapRow, MatchRow, PovRow, RoundRow } from './rows';
|
| 19 |
import type { Match } from '$lib/types';
|
| 20 |
|
| 21 |
declare module '@tanstack/table-core' {
|
|
|
|
| 205 |
uploadedColumn<MapRow>()
|
| 206 |
];
|
| 207 |
|
| 208 |
+
export const povColumns: ColumnDef<PovRow>[] = [
|
| 209 |
+
matchIdColumn<PovRow>(),
|
| 210 |
+
eventColumn<PovRow>(),
|
| 211 |
+
{
|
| 212 |
+
id: 'map_name',
|
| 213 |
+
accessorKey: 'map_name',
|
| 214 |
+
header: () => renderComponent(PlainHeader, { label: 'Map' }),
|
| 215 |
+
cell: ({ row }) => renderComponent(MapCell, { map: row.original.map_name }),
|
| 216 |
+
enableSorting: false,
|
| 217 |
+
filterFn: (row, id, value) => {
|
| 218 |
+
if (!value) return true;
|
| 219 |
+
return row.getValue<string>(id) === value;
|
| 220 |
+
},
|
| 221 |
+
meta: { label: 'Map' }
|
| 222 |
+
},
|
| 223 |
+
{
|
| 224 |
+
id: 'round',
|
| 225 |
+
accessorKey: 'round',
|
| 226 |
+
header: ({ column }) => renderComponent(SortHeader, { column, label: 'Round' }),
|
| 227 |
+
cell: ({ row }) =>
|
| 228 |
+
renderComponent(RoundCell, { round: row.original.round, total: row.original.rounds_played }),
|
| 229 |
+
enableGlobalFilter: false,
|
| 230 |
+
meta: { label: 'Round' }
|
| 231 |
+
},
|
| 232 |
+
{
|
| 233 |
+
id: 'player',
|
| 234 |
+
accessorKey: 'player',
|
| 235 |
+
header: ({ column }) => renderComponent(SortHeader, { column, label: 'POV' }),
|
| 236 |
+
cell: ({ row }) => renderComponent(PlayerCell, { player: row.original.player }),
|
| 237 |
+
enableGlobalFilter: false,
|
| 238 |
+
meta: { label: 'POV' }
|
| 239 |
+
},
|
| 240 |
+
{
|
| 241 |
+
id: 'start_t',
|
| 242 |
+
accessorKey: 'start_t',
|
| 243 |
+
header: ({ column }) => renderComponent(SortHeader, { column, label: 'Start' }),
|
| 244 |
+
cell: ({ row }) => renderComponent(StartTimeCell, { seconds: row.original.start_t }),
|
| 245 |
+
enableGlobalFilter: false,
|
| 246 |
+
meta: { label: 'Start' }
|
| 247 |
+
},
|
| 248 |
+
{
|
| 249 |
+
id: 'teams',
|
| 250 |
+
accessorFn: (r) => `${r.team1} ${r.team2}`,
|
| 251 |
+
header: () => renderComponent(PlainHeader, { label: 'Teams' }),
|
| 252 |
+
cell: ({ row }) =>
|
| 253 |
+
renderComponent(TeamsCell, {
|
| 254 |
+
team1: row.original.team1,
|
| 255 |
+
team2: row.original.team2,
|
| 256 |
+
winner: row.original.winner
|
| 257 |
+
}),
|
| 258 |
+
enableSorting: false,
|
| 259 |
+
meta: { label: 'Teams' }
|
| 260 |
+
},
|
| 261 |
+
{
|
| 262 |
+
id: 'duration_s',
|
| 263 |
+
accessorKey: 'duration_s',
|
| 264 |
+
header: ({ column }) => renderComponent(SortHeader, { column, label: 'Duration' }),
|
| 265 |
+
cell: ({ row }) => renderComponent(DurationCell, { seconds: row.original.duration_s }),
|
| 266 |
+
enableGlobalFilter: false,
|
| 267 |
+
meta: { label: 'Duration' }
|
| 268 |
+
},
|
| 269 |
+
{
|
| 270 |
+
id: 'match_date',
|
| 271 |
+
accessorFn: (r) => new Date(r.match_date).getTime(),
|
| 272 |
+
sortingFn: (a, b) => dateSort(a.original, b.original),
|
| 273 |
+
header: ({ column }) => renderComponent(SortHeader, { column, label: 'Date' }),
|
| 274 |
+
cell: ({ row }) => renderComponent(DateCell, { iso: row.original.match_date }),
|
| 275 |
+
enableGlobalFilter: false,
|
| 276 |
+
meta: { label: 'Date' }
|
| 277 |
+
},
|
| 278 |
+
uploadedColumn<PovRow>()
|
| 279 |
+
];
|
| 280 |
+
|
| 281 |
export const matchColumns: ColumnDef<MatchRow>[] = [
|
| 282 |
matchIdColumn<MatchRow>(),
|
| 283 |
eventColumn<MatchRow>(),
|
src/lib/components/match-table/rows.ts
CHANGED
|
@@ -1,11 +1,33 @@
|
|
| 1 |
import type { Match, Round } from '$lib/types';
|
| 2 |
|
| 3 |
const TICK_RATE = 64;
|
|
|
|
| 4 |
|
| 5 |
export type MapRow = Match & {
|
| 6 |
duration_s: number;
|
| 7 |
};
|
| 8 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
export type RoundRow = {
|
| 10 |
match_id: number;
|
| 11 |
map_name: string;
|
|
@@ -83,6 +105,44 @@ export function buildRoundRows(matches: Match[], rounds: Round[]): RoundRow[] {
|
|
| 83 |
return out;
|
| 84 |
}
|
| 85 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
export function buildMatchRows(matches: Match[], rounds: Round[]): MatchRow[] {
|
| 87 |
const durationByMap = new Map<string, number>();
|
| 88 |
for (const r of rounds) {
|
|
|
|
| 1 |
import type { Match, Round } from '$lib/types';
|
| 2 |
|
| 3 |
const TICK_RATE = 64;
|
| 4 |
+
const PLAYERS_PER_ROUND = 10;
|
| 5 |
|
| 6 |
export type MapRow = Match & {
|
| 7 |
duration_s: number;
|
| 8 |
};
|
| 9 |
|
| 10 |
+
export type PovRow = {
|
| 11 |
+
match_id: number;
|
| 12 |
+
map_name: string;
|
| 13 |
+
round: number;
|
| 14 |
+
player: number;
|
| 15 |
+
duration_s: number;
|
| 16 |
+
event: string;
|
| 17 |
+
team1: string;
|
| 18 |
+
team2: string;
|
| 19 |
+
score1: number;
|
| 20 |
+
score2: number;
|
| 21 |
+
winner: Match['winner'];
|
| 22 |
+
format: string;
|
| 23 |
+
match_date: string;
|
| 24 |
+
uploaded_at: string;
|
| 25 |
+
rounds_played: number;
|
| 26 |
+
// When this row is materialized from an SQL filter, the timestamp inside
|
| 27 |
+
// the POV's video to seek to on open. Otherwise undefined → opens at 0.
|
| 28 |
+
start_t?: number;
|
| 29 |
+
};
|
| 30 |
+
|
| 31 |
export type RoundRow = {
|
| 32 |
match_id: number;
|
| 33 |
map_name: string;
|
|
|
|
| 105 |
return out;
|
| 106 |
}
|
| 107 |
|
| 108 |
+
/**
|
| 109 |
+
* Synthesize 10 rows per round (one per player slot). No per-POV metadata
|
| 110 |
+
* is loaded eagerly — side/weapon would require fetching per-(match, map)
|
| 111 |
+
* chunks-preview.parquet for every visible match, which doesn't pay back on
|
| 112 |
+
* the home page. Per-POV detail surfaces inside the match viewer.
|
| 113 |
+
*/
|
| 114 |
+
export function buildPovRows(matches: Match[], rounds: Round[]): PovRow[] {
|
| 115 |
+
const matchByMap = new Map<string, Match>();
|
| 116 |
+
for (const m of matches) matchByMap.set(`${m.match_id}|${m.map_name}`, m);
|
| 117 |
+
|
| 118 |
+
const out: PovRow[] = [];
|
| 119 |
+
for (const r of rounds) {
|
| 120 |
+
const m = matchByMap.get(`${r.match_id}|${r.map_name}`);
|
| 121 |
+
if (!m) continue;
|
| 122 |
+
const duration_s = (r.round_duration_ticks || 0) / TICK_RATE;
|
| 123 |
+
for (let player = 0; player < PLAYERS_PER_ROUND; player++) {
|
| 124 |
+
out.push({
|
| 125 |
+
match_id: r.match_id,
|
| 126 |
+
map_name: r.map_name,
|
| 127 |
+
round: r.round,
|
| 128 |
+
player,
|
| 129 |
+
duration_s,
|
| 130 |
+
event: m.event,
|
| 131 |
+
team1: m.team1,
|
| 132 |
+
team2: m.team2,
|
| 133 |
+
score1: m.score1,
|
| 134 |
+
score2: m.score2,
|
| 135 |
+
winner: m.winner,
|
| 136 |
+
format: m.format,
|
| 137 |
+
match_date: m.match_date,
|
| 138 |
+
uploaded_at: r.uploaded_at,
|
| 139 |
+
rounds_played: m.rounds_played
|
| 140 |
+
});
|
| 141 |
+
}
|
| 142 |
+
}
|
| 143 |
+
return out;
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
export function buildMatchRows(matches: Match[], rounds: Round[]): MatchRow[] {
|
| 147 |
const durationByMap = new Map<string, number>();
|
| 148 |
for (const r of rounds) {
|
src/lib/duckdb.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// DuckDB-WASM singleton wrapper. Lazy-instantiated on first query so the
|
| 2 |
+
// ~5 MB worker + WASM bundles don't show up in the home-page critical path.
|
| 3 |
+
|
| 4 |
+
import * as duckdb from '@duckdb/duckdb-wasm';
|
| 5 |
+
import type { AsyncDuckDB, AsyncDuckDBConnection } from '@duckdb/duckdb-wasm';
|
| 6 |
+
|
| 7 |
+
let dbPromise: Promise<AsyncDuckDB> | null = null;
|
| 8 |
+
|
| 9 |
+
async function getDb(): Promise<AsyncDuckDB> {
|
| 10 |
+
if (dbPromise) return dbPromise;
|
| 11 |
+
dbPromise = (async () => {
|
| 12 |
+
const bundles = duckdb.getJsDelivrBundles();
|
| 13 |
+
const bundle = await duckdb.selectBundle(bundles);
|
| 14 |
+
// Worker URL needs to be served from the same origin to satisfy the
|
| 15 |
+
// browser's Worker constructor; wrap the jsdelivr URL in a blob to do so.
|
| 16 |
+
const workerUrl = URL.createObjectURL(
|
| 17 |
+
new Blob([`importScripts("${bundle.mainWorker!}");`], { type: 'text/javascript' })
|
| 18 |
+
);
|
| 19 |
+
const worker = new Worker(workerUrl);
|
| 20 |
+
const logger = new duckdb.ConsoleLogger(duckdb.LogLevel.WARNING);
|
| 21 |
+
const db = new duckdb.AsyncDuckDB(logger, worker);
|
| 22 |
+
await db.instantiate(bundle.mainModule, bundle.pthreadWorker);
|
| 23 |
+
URL.revokeObjectURL(workerUrl);
|
| 24 |
+
return db;
|
| 25 |
+
})().catch((err) => {
|
| 26 |
+
dbPromise = null;
|
| 27 |
+
throw err;
|
| 28 |
+
});
|
| 29 |
+
return dbPromise;
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
// Rewrite the dataset README's `hf://datasets/org/repo/path` URLs to the
|
| 33 |
+
// public HTTPS form duckdb-wasm can actually fetch.
|
| 34 |
+
export function rewriteHfUrls(sql: string): string {
|
| 35 |
+
return sql.replace(
|
| 36 |
+
/hf:\/\/datasets\/([^/\s'"]+)\/([^/\s'"]+)\/([^'"\s]+)/g,
|
| 37 |
+
'https://huggingface.co/datasets/$1/$2/resolve/main/$3'
|
| 38 |
+
);
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
export type QueryResult = {
|
| 42 |
+
columns: string[];
|
| 43 |
+
rows: Record<string, unknown>[];
|
| 44 |
+
rewrittenSql: string;
|
| 45 |
+
};
|
| 46 |
+
|
| 47 |
+
/**
|
| 48 |
+
* Run a DuckDB SQL query against HTTPS-served parquet files. Returns plain
|
| 49 |
+
* JS objects with BigInts downcast to numbers and Dates ISO-stringified, so
|
| 50 |
+
* the caller doesn't have to think about Arrow types.
|
| 51 |
+
*/
|
| 52 |
+
export async function runQuery(sql: string, opts: { limit?: number } = {}): Promise<QueryResult> {
|
| 53 |
+
const limit = opts.limit ?? 1000;
|
| 54 |
+
const db = await getDb();
|
| 55 |
+
const conn: AsyncDuckDBConnection = await db.connect();
|
| 56 |
+
try {
|
| 57 |
+
const rewrittenSql = rewriteHfUrls(sql);
|
| 58 |
+
// Wrap in a CTE to enforce a row cap without rewriting user SQL further.
|
| 59 |
+
const limited = /\blimit\b/i.test(rewrittenSql)
|
| 60 |
+
? rewrittenSql
|
| 61 |
+
: `WITH _q AS (${stripTrailingSemicolon(rewrittenSql)}) SELECT * FROM _q LIMIT ${limit}`;
|
| 62 |
+
const result = await conn.query(limited);
|
| 63 |
+
const columns = result.schema.fields.map((f) => f.name);
|
| 64 |
+
const rows: Record<string, unknown>[] = [];
|
| 65 |
+
for (const row of result.toArray()) {
|
| 66 |
+
const obj: Record<string, unknown> = {};
|
| 67 |
+
const r = row.toJSON() as Record<string, unknown>;
|
| 68 |
+
for (const [k, v] of Object.entries(r)) {
|
| 69 |
+
obj[k] = normalize(v);
|
| 70 |
+
}
|
| 71 |
+
rows.push(obj);
|
| 72 |
+
}
|
| 73 |
+
return { columns, rows, rewrittenSql };
|
| 74 |
+
} finally {
|
| 75 |
+
await conn.close();
|
| 76 |
+
}
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
function stripTrailingSemicolon(s: string): string {
|
| 80 |
+
return s.trim().replace(/;+\s*$/, '');
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
function normalize(v: unknown): unknown {
|
| 84 |
+
if (v == null) return v;
|
| 85 |
+
if (typeof v === 'bigint') return Number(v);
|
| 86 |
+
if (v instanceof Date) return v.toISOString();
|
| 87 |
+
if (Array.isArray(v)) return v.map(normalize);
|
| 88 |
+
if (typeof v === 'object') {
|
| 89 |
+
const out: Record<string, unknown> = {};
|
| 90 |
+
for (const [k, vv] of Object.entries(v as Record<string, unknown>)) out[k] = normalize(vv);
|
| 91 |
+
return out;
|
| 92 |
+
}
|
| 93 |
+
return v;
|
| 94 |
+
}
|
src/routes/+page.svelte
CHANGED
|
@@ -19,8 +19,20 @@
|
|
| 19 |
import MoonIcon from 'phosphor-svelte/lib/MoonIcon';
|
| 20 |
import MatchTable from '$lib/components/match-table/match-table.svelte';
|
| 21 |
import StatsSection from '$lib/components/stats-section.svelte';
|
| 22 |
-
import
|
| 23 |
-
import {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
import type {
|
| 25 |
ColumnFiltersState,
|
| 26 |
PaginationState,
|
|
@@ -48,16 +60,32 @@
|
|
| 48 |
const mapRows = $derived(buildMapRows(data.matches, data.rounds));
|
| 49 |
const matchRows = $derived(buildMatchRows(data.matches, data.rounds));
|
| 50 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
const mapOptions = $derived(Array.from(new Set(data.matches.map((m) => m.map_name))).sort());
|
| 52 |
|
| 53 |
// Always emit explicit ?round=&player=&view= so the destination URL is
|
| 54 |
-
// self-contained — no implicit defaults for back/share to lose.
|
| 55 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
const params = new URLSearchParams({
|
| 57 |
round: String(round),
|
| 58 |
-
player:
|
| 59 |
-
view: 'grid'
|
| 60 |
});
|
|
|
|
|
|
|
|
|
|
| 61 |
return `/match/${encodeURIComponent(matchId)}/${encodeURIComponent(mapName)}?${params}`;
|
| 62 |
}
|
| 63 |
|
|
@@ -70,7 +98,14 @@
|
|
| 70 |
const linkToFirstMap = (m: { match_id: number; first_map: string }) =>
|
| 71 |
matchUrl(m.match_id, m.first_map, 1);
|
| 72 |
|
| 73 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
|
| 75 |
type TableState = {
|
| 76 |
pagination: PaginationState;
|
|
@@ -92,9 +127,21 @@
|
|
| 92 |
const tables = $state<Record<TabKey, TableState>>({
|
| 93 |
rounds: newTableState(),
|
| 94 |
maps: newTableState(),
|
| 95 |
-
matches: newTableState()
|
|
|
|
| 96 |
});
|
| 97 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
// Persist tab + table state across navigations (back from a match page,
|
| 99 |
// preload, etc.) using SvelteKit's snapshot API.
|
| 100 |
export const snapshot: Snapshot<{ view: TabKey; tables: Record<TabKey, TableState> }> = {
|
|
@@ -344,13 +391,22 @@
|
|
| 344 |
|
| 345 |
<!-- Browser -->
|
| 346 |
<section class="mx-auto mt-8 max-w-5xl">
|
| 347 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 348 |
<div class="mb-1 flex items-center justify-between gap-2">
|
| 349 |
<h2 class="font-heading text-lg font-semibold tracking-tight">Browse the dataset</h2>
|
| 350 |
<Tabs.List>
|
| 351 |
<Tabs.Trigger value="rounds">Rounds</Tabs.Trigger>
|
| 352 |
<Tabs.Trigger value="maps">Maps</Tabs.Trigger>
|
| 353 |
<Tabs.Trigger value="matches">Matches</Tabs.Trigger>
|
|
|
|
| 354 |
</Tabs.List>
|
| 355 |
</div>
|
| 356 |
<p class="mb-3 text-xs text-muted-foreground">
|
|
@@ -358,8 +414,8 @@
|
|
| 358 |
(usually BO3) of
|
| 359 |
<strong class="font-semibold text-foreground">maps</strong>; each map plays up to 24
|
| 360 |
<strong class="font-semibold text-foreground">rounds</strong> (MR12 regulation + MR3
|
| 361 |
-
overtime)
|
| 362 |
-
<strong class="font-semibold text-foreground">
|
| 363 |
</p>
|
| 364 |
|
| 365 |
{#if data.matches.length === 0}
|
|
@@ -413,6 +469,39 @@
|
|
| 413 |
bind:globalFilter={tables.matches.globalFilter}
|
| 414 |
/>
|
| 415 |
</Tabs.Content>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 416 |
{/if}
|
| 417 |
</Tabs.Root>
|
| 418 |
</section>
|
|
|
|
| 19 |
import MoonIcon from 'phosphor-svelte/lib/MoonIcon';
|
| 20 |
import MatchTable from '$lib/components/match-table/match-table.svelte';
|
| 21 |
import StatsSection from '$lib/components/stats-section.svelte';
|
| 22 |
+
import AdvancedFilter from '$lib/components/advanced-filter/advanced-filter.svelte';
|
| 23 |
+
import {
|
| 24 |
+
buildMapRows,
|
| 25 |
+
buildMatchRows,
|
| 26 |
+
buildPovRows,
|
| 27 |
+
buildRoundRows,
|
| 28 |
+
type PovRow
|
| 29 |
+
} from '$lib/components/match-table/rows';
|
| 30 |
+
import {
|
| 31 |
+
mapColumns,
|
| 32 |
+
matchColumns,
|
| 33 |
+
povColumns,
|
| 34 |
+
roundColumns
|
| 35 |
+
} from '$lib/components/match-table/columns';
|
| 36 |
import type {
|
| 37 |
ColumnFiltersState,
|
| 38 |
PaginationState,
|
|
|
|
| 60 |
const mapRows = $derived(buildMapRows(data.matches, data.rounds));
|
| 61 |
const matchRows = $derived(buildMatchRows(data.matches, data.rounds));
|
| 62 |
|
| 63 |
+
// SQL-filter results, when present, replace the POV tab's row source.
|
| 64 |
+
// Null = no filter applied → fall back to the 10×rounds synthesis.
|
| 65 |
+
let sqlPovRows = $state<PovRow[] | null>(null);
|
| 66 |
+
const defaultPovRows = $derived(buildPovRows(data.matches, data.rounds));
|
| 67 |
+
const povRows = $derived(sqlPovRows ?? defaultPovRows);
|
| 68 |
+
|
| 69 |
const mapOptions = $derived(Array.from(new Set(data.matches.map((m) => m.map_name))).sort());
|
| 70 |
|
| 71 |
// Always emit explicit ?round=&player=&view= so the destination URL is
|
| 72 |
+
// self-contained — no implicit defaults for back/share to lose. `view`
|
| 73 |
+
// defaults to 'grid' from the home tabs; POV rows force 'single' so the
|
| 74 |
+
// user lands directly on their chosen perspective.
|
| 75 |
+
function matchUrl(
|
| 76 |
+
matchId: number,
|
| 77 |
+
mapName: string,
|
| 78 |
+
round: number,
|
| 79 |
+
opts: { player?: number; view?: 'single' | 'grid'; t?: number } = {}
|
| 80 |
+
) {
|
| 81 |
const params = new URLSearchParams({
|
| 82 |
round: String(round),
|
| 83 |
+
player: String(opts.player ?? 0),
|
| 84 |
+
view: opts.view ?? 'grid'
|
| 85 |
});
|
| 86 |
+
if (opts.t !== undefined && Number.isFinite(opts.t) && opts.t > 0) {
|
| 87 |
+
params.set('t', opts.t.toFixed(2));
|
| 88 |
+
}
|
| 89 |
return `/match/${encodeURIComponent(matchId)}/${encodeURIComponent(mapName)}?${params}`;
|
| 90 |
}
|
| 91 |
|
|
|
|
| 98 |
const linkToFirstMap = (m: { match_id: number; first_map: string }) =>
|
| 99 |
matchUrl(m.match_id, m.first_map, 1);
|
| 100 |
|
| 101 |
+
const linkToPov = (r: PovRow) =>
|
| 102 |
+
matchUrl(r.match_id, r.map_name, r.round, {
|
| 103 |
+
player: r.player,
|
| 104 |
+
view: 'single',
|
| 105 |
+
t: r.start_t
|
| 106 |
+
});
|
| 107 |
+
|
| 108 |
+
type TabKey = 'rounds' | 'maps' | 'matches' | 'pov';
|
| 109 |
|
| 110 |
type TableState = {
|
| 111 |
pagination: PaginationState;
|
|
|
|
| 127 |
const tables = $state<Record<TabKey, TableState>>({
|
| 128 |
rounds: newTableState(),
|
| 129 |
maps: newTableState(),
|
| 130 |
+
matches: newTableState(),
|
| 131 |
+
pov: newTableState()
|
| 132 |
});
|
| 133 |
|
| 134 |
+
function handleSqlResults(rows: PovRow[]) {
|
| 135 |
+
sqlPovRows = rows;
|
| 136 |
+
view = 'pov';
|
| 137 |
+
// Reset POV table state so the user sees the filtered results from page 1.
|
| 138 |
+
tables.pov = newTableState();
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
function clearSqlFilter() {
|
| 142 |
+
sqlPovRows = null;
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
// Persist tab + table state across navigations (back from a match page,
|
| 146 |
// preload, etc.) using SvelteKit's snapshot API.
|
| 147 |
export const snapshot: Snapshot<{ view: TabKey; tables: Record<TabKey, TableState> }> = {
|
|
|
|
| 391 |
|
| 392 |
<!-- Browser -->
|
| 393 |
<section class="mx-auto mt-8 max-w-5xl">
|
| 394 |
+
<AdvancedFilter
|
| 395 |
+
matches={data.matches}
|
| 396 |
+
rounds={data.rounds}
|
| 397 |
+
activeFilterCount={sqlPovRows?.length ?? null}
|
| 398 |
+
onResults={handleSqlResults}
|
| 399 |
+
onClear={clearSqlFilter}
|
| 400 |
+
/>
|
| 401 |
+
|
| 402 |
+
<Tabs.Root bind:value={view} class="mt-3 w-full">
|
| 403 |
<div class="mb-1 flex items-center justify-between gap-2">
|
| 404 |
<h2 class="font-heading text-lg font-semibold tracking-tight">Browse the dataset</h2>
|
| 405 |
<Tabs.List>
|
| 406 |
<Tabs.Trigger value="rounds">Rounds</Tabs.Trigger>
|
| 407 |
<Tabs.Trigger value="maps">Maps</Tabs.Trigger>
|
| 408 |
<Tabs.Trigger value="matches">Matches</Tabs.Trigger>
|
| 409 |
+
<Tabs.Trigger value="pov">POV</Tabs.Trigger>
|
| 410 |
</Tabs.List>
|
| 411 |
</div>
|
| 412 |
<p class="mb-3 text-xs text-muted-foreground">
|
|
|
|
| 414 |
(usually BO3) of
|
| 415 |
<strong class="font-semibold text-foreground">maps</strong>; each map plays up to 24
|
| 416 |
<strong class="font-semibold text-foreground">rounds</strong> (MR12 regulation + MR3
|
| 417 |
+
overtime), each with 10 tick-aligned
|
| 418 |
+
<strong class="font-semibold text-foreground">POVs</strong>.
|
| 419 |
</p>
|
| 420 |
|
| 421 |
{#if data.matches.length === 0}
|
|
|
|
| 469 |
bind:globalFilter={tables.matches.globalFilter}
|
| 470 |
/>
|
| 471 |
</Tabs.Content>
|
| 472 |
+
|
| 473 |
+
<Tabs.Content value="pov">
|
| 474 |
+
{#if sqlPovRows !== null}
|
| 475 |
+
<div
|
| 476 |
+
class="mb-2 flex items-center justify-between gap-2 rounded-md border bg-accent/40 px-3 py-2 text-xs"
|
| 477 |
+
>
|
| 478 |
+
<span class="text-muted-foreground">
|
| 479 |
+
Showing <strong class="font-semibold text-foreground">{sqlPovRows.length}</strong>
|
| 480 |
+
POV{sqlPovRows.length === 1 ? '' : 's'} from advanced filter.
|
| 481 |
+
</span>
|
| 482 |
+
<button
|
| 483 |
+
type="button"
|
| 484 |
+
onclick={clearSqlFilter}
|
| 485 |
+
class="font-medium text-foreground hover:underline"
|
| 486 |
+
>
|
| 487 |
+
Clear filter
|
| 488 |
+
</button>
|
| 489 |
+
</div>
|
| 490 |
+
{/if}
|
| 491 |
+
<MatchTable
|
| 492 |
+
data={povRows}
|
| 493 |
+
columns={povColumns}
|
| 494 |
+
{mapOptions}
|
| 495 |
+
searchPlaceholder="Search event, team, map…"
|
| 496 |
+
getRowHref={linkToPov}
|
| 497 |
+
emptyMessage="No POVs match the current filter."
|
| 498 |
+
bind:pagination={tables.pov.pagination}
|
| 499 |
+
bind:sorting={tables.pov.sorting}
|
| 500 |
+
bind:columnFilters={tables.pov.columnFilters}
|
| 501 |
+
bind:columnVisibility={tables.pov.columnVisibility}
|
| 502 |
+
bind:globalFilter={tables.pov.globalFilter}
|
| 503 |
+
/>
|
| 504 |
+
</Tabs.Content>
|
| 505 |
{/if}
|
| 506 |
</Tabs.Root>
|
| 507 |
</section>
|
src/routes/match/[matchId]/[mapName]/+page.svelte
CHANGED
|
@@ -40,6 +40,11 @@
|
|
| 40 |
function readViewFromUrl(): 'single' | 'grid' {
|
| 41 |
return page.url.searchParams.get('view') === 'single' ? 'single' : 'grid';
|
| 42 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
|
| 44 |
// svelte-ignore state_referenced_locally
|
| 45 |
let currentRoundNum = $state<number>(readRoundFromUrl(data.rounds));
|
|
@@ -51,8 +56,13 @@
|
|
| 51 |
|
| 52 |
// Virtual time across all chunks of the current player. Captured before
|
| 53 |
// player switches so the new player picks up at the same timestamp.
|
| 54 |
-
|
| 55 |
-
let
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
|
| 57 |
// Lifted so player/mode/round changes don't reset the user's intent.
|
| 58 |
let masterPaused = $state(false);
|
|
@@ -148,9 +158,13 @@
|
|
| 148 |
activeFetch = ctrl;
|
| 149 |
previewsLoading = true;
|
| 150 |
previewsError = null;
|
| 151 |
-
// Reset virtual-time and preload state on round change.
|
| 152 |
-
|
| 153 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 154 |
activeVideoEnded = false;
|
| 155 |
bufferedRanges = [];
|
| 156 |
preloadedPlayers = new Set();
|
|
@@ -366,6 +380,9 @@
|
|
| 366 |
url.searchParams.set('player', String(currentPlayer));
|
| 367 |
if (viewMode === 'single') url.searchParams.set('view', 'single');
|
| 368 |
else url.searchParams.delete('view');
|
|
|
|
|
|
|
|
|
|
| 369 |
goto(url.pathname + '?' + url.searchParams.toString(), {
|
| 370 |
replaceState: true,
|
| 371 |
keepFocus: true,
|
|
|
|
| 40 |
function readViewFromUrl(): 'single' | 'grid' {
|
| 41 |
return page.url.searchParams.get('view') === 'single' ? 'single' : 'grid';
|
| 42 |
}
|
| 43 |
+
function readStartTimeFromUrl(): number {
|
| 44 |
+
const param = page.url.searchParams.get('t');
|
| 45 |
+
const parsed = param ? Number(param) : NaN;
|
| 46 |
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;
|
| 47 |
+
}
|
| 48 |
|
| 49 |
// svelte-ignore state_referenced_locally
|
| 50 |
let currentRoundNum = $state<number>(readRoundFromUrl(data.rounds));
|
|
|
|
| 56 |
|
| 57 |
// Virtual time across all chunks of the current player. Captured before
|
| 58 |
// player switches so the new player picks up at the same timestamp.
|
| 59 |
+
// svelte-ignore state_referenced_locally
|
| 60 |
+
let virtualTime = $state(readStartTimeFromUrl());
|
| 61 |
+
// svelte-ignore state_referenced_locally
|
| 62 |
+
let initialVirtualTime = $state(readStartTimeFromUrl());
|
| 63 |
+
// Consumed once per round-load so that subsequent round switches don't
|
| 64 |
+
// keep seeking to the URL `t`.
|
| 65 |
+
let pendingStartT: number | null = readStartTimeFromUrl() > 0 ? readStartTimeFromUrl() : null;
|
| 66 |
|
| 67 |
// Lifted so player/mode/round changes don't reset the user's intent.
|
| 68 |
let masterPaused = $state(false);
|
|
|
|
| 158 |
activeFetch = ctrl;
|
| 159 |
previewsLoading = true;
|
| 160 |
previewsError = null;
|
| 161 |
+
// Reset virtual-time and preload state on round change. If the URL
|
| 162 |
+
// landed us at a specific timestamp (?t=…), honor it once on first
|
| 163 |
+
// load and then forget it so subsequent round changes start at 0.
|
| 164 |
+
const startAt = pendingStartT ?? 0;
|
| 165 |
+
pendingStartT = null;
|
| 166 |
+
virtualTime = startAt;
|
| 167 |
+
initialVirtualTime = startAt;
|
| 168 |
activeVideoEnded = false;
|
| 169 |
bufferedRanges = [];
|
| 170 |
preloadedPlayers = new Set();
|
|
|
|
| 380 |
url.searchParams.set('player', String(currentPlayer));
|
| 381 |
if (viewMode === 'single') url.searchParams.set('view', 'single');
|
| 382 |
else url.searchParams.delete('view');
|
| 383 |
+
// `?t=` is one-shot: honored on first land, stripped on any in-app
|
| 384 |
+
// navigation so reloads don't keep seeking back to the seed time.
|
| 385 |
+
url.searchParams.delete('t');
|
| 386 |
goto(url.pathname + '?' + url.searchParams.toString(), {
|
| 387 |
replaceState: true,
|
| 388 |
keepFocus: true,
|