blanchon commited on
Commit
5a67aa1
·
1 Parent(s): 16c253d

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 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 type { MapRow, MatchRow, RoundRow } from './rows';
 
 
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 { buildMapRows, buildMatchRows, buildRoundRows } from '$lib/components/match-table/rows';
23
- import { mapColumns, matchColumns, roundColumns } from '$lib/components/match-table/columns';
 
 
 
 
 
 
 
 
 
 
 
 
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
- function matchUrl(matchId: number, mapName: string, round: number) {
 
 
 
 
 
 
 
56
  const params = new URLSearchParams({
57
  round: String(round),
58
- player: '0',
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
- type TabKey = 'rounds' | 'maps' | 'matches';
 
 
 
 
 
 
 
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
- <Tabs.Root bind:value={view} class="w-full">
 
 
 
 
 
 
 
 
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); per player POV every round is sliced into ≤ 1-minute
362
- <strong class="font-semibold text-foreground">chunks</strong>.
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
- let virtualTime = $state(0);
55
- let initialVirtualTime = $state(0);
 
 
 
 
 
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
- virtualTime = 0;
153
- initialVirtualTime = 0;
 
 
 
 
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,