From 3a865038dcc5bd2d0a530cd27b859028d814d084 Mon Sep 17 00:00:00 2001 From: Hany Renando Date: Thu, 17 Sep 2026 17:53:56 +0700 Subject: [PATCH] feat: find/replace in JSON result and document views MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reading a large result set or a deeply nested document as pretty JSON had no way to locate a value except scrolling and eyeballing it. Backs the JSON result view and the row inspector's "View document JSON" modal with CodeMirror instead of a plain
, adding a find
bar with next/previous cycling, a match-case toggle, and replace/
replace-all. Edits stay local to the viewer's buffer only — these are
read-only surfaces, so nothing is written back to the collection.

CodeMirror only highlights matches while its own search state is
"open", so the component opens that panel and hides it with CSS in
favour of its own bar, refocusing the visible find field.

Pin the project to Node 22.12.0 via Volta so `npm install` resolves
the correct native optional-dependency binaries on the first pass.

Co-Authored-By: Claude Sonnet 5 
---
 package-lock.json                 | 281 +++++++++++++++---------------
 package.json                      |   5 +
 scripts/ui-check.ts               |  53 ++++++
 src/components/JsonSearchView.tsx | 216 +++++++++++++++++++++++
 src/components/ResultView.tsx     |   7 +-
 src/styles.css                    |  70 ++++++++
 6 files changed, 485 insertions(+), 147 deletions(-)
 create mode 100644 src/components/JsonSearchView.tsx

diff --git a/package-lock.json b/package-lock.json
index 3291f76..b4f05a1 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -13,6 +13,8 @@
       },
       "devDependencies": {
         "@codemirror/lang-javascript": "^6.2.5",
+        "@codemirror/lang-json": "^6.0.2",
+        "@codemirror/search": "^6.7.2",
         "@codemirror/theme-one-dark": "^6.1.3",
         "@types/node": "^24.13.4",
         "@types/react": "^19.3.0",
@@ -52,9 +54,9 @@
       }
     },
     "node_modules/@codemirror/commands": {
-      "version": "6.11.0",
-      "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.11.0.tgz",
-      "integrity": "sha512-/K4Rl5BN0OtTiPWmJCdqODu38XnDMsDxKY5rgrPnCkutPTJf2wVbkoixLfealF5Kwse/s8P8M5jAiURiwSwnFA==",
+      "version": "6.11.1",
+      "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.11.1.tgz",
+      "integrity": "sha512-O/4hG3SC1YwcmQ0d2UVNDs+AsaNWd1iHVxbTeEBuqH+6bExAiPK3iS/BvpY6rZGURALv4ZD3sIgcCmRvw3ehBg==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
@@ -80,6 +82,17 @@
         "@lezer/javascript": "^1.0.0"
       }
     },
+    "node_modules/@codemirror/lang-json": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.2.tgz",
+      "integrity": "sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@codemirror/language": "^6.0.0",
+        "@lezer/json": "^1.0.0"
+      }
+    },
     "node_modules/@codemirror/language": {
       "version": "6.12.4",
       "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.4.tgz",
@@ -120,9 +133,9 @@
       }
     },
     "node_modules/@codemirror/state": {
-      "version": "6.7.4",
-      "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.7.4.tgz",
-      "integrity": "sha512-QhQIVRY+xHZDxwOSFrJ1eUMapJBUID3IdeAjf7dHO7zBUzSkyooHiodnalz5MG3iHzwixKMlAAyn7244y537EA==",
+      "version": "6.7.5",
+      "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.7.5.tgz",
+      "integrity": "sha512-QjLbZmY1Au3JiRrDVYFLRD0BZ3SOKS9pR3yjIkd7u27YY8TFD9/Q9fhPnLV5l1mHFSo3hHU/N31vpwEJOx4owQ==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
@@ -143,9 +156,9 @@
       }
     },
     "node_modules/@codemirror/view": {
-      "version": "6.43.11",
-      "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.43.11.tgz",
-      "integrity": "sha512-2+esucbQX6wB2JYi1eDvdCPFTA31BN8oSy6xCmk3G6CloV11yOvEjYk+gH7kLrP0MuHG94E8WDhjs5oMiu3+Wg==",
+      "version": "6.43.12",
+      "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.43.12.tgz",
+      "integrity": "sha512-Nv0vxQ19NAqvB/c2pFzjIzFlzzJl7jmdtNkwOwGbn0Ks9mFAzibvumz7cQem5cRsFA2cEw2fg+uHZGbcHupLQQ==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
@@ -191,9 +204,9 @@
       "license": "MIT"
     },
     "node_modules/@electron/asar/node_modules/brace-expansion": {
-      "version": "1.1.18",
-      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz",
-      "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==",
+      "version": "1.1.21",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz",
+      "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
@@ -380,9 +393,9 @@
       "license": "MIT"
     },
     "node_modules/@electron/universal/node_modules/brace-expansion": {
-      "version": "2.1.4",
-      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz",
-      "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==",
+      "version": "2.1.7",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz",
+      "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
@@ -943,6 +956,18 @@
         "@lezer/lr": "^1.3.0"
       }
     },
+    "node_modules/@lezer/json": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.3.tgz",
+      "integrity": "sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@lezer/common": "^1.2.0",
+        "@lezer/highlight": "^1.0.0",
+        "@lezer/lr": "^1.0.0"
+      }
+    },
     "node_modules/@lezer/lr": {
       "version": "1.4.10",
       "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.10.tgz",
@@ -1038,9 +1063,9 @@
       }
     },
     "node_modules/@oxc-project/types": {
-      "version": "0.149.0",
-      "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.149.0.tgz",
-      "integrity": "sha512-Efcc+iF0j3Bf67YjEqIqWXbX5XddXoK/Mw4K1/JuXwRCZ8N16VR7iT23nlCc9XrveFVh/E5Rqs2StT0V8v9LdA==",
+      "version": "0.150.0",
+      "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.150.0.tgz",
+      "integrity": "sha512-rDS5/31E9HfPl/CIzGrn0DOlvBbXFseQ5URJ9sYMfstbKLD/c6Gm9vmRzRGDdAXyOIL4zmO37lc9RIwYqVruZw==",
       "dev": true,
       "license": "MIT",
       "funding": {
@@ -1103,9 +1128,9 @@
       }
     },
     "node_modules/@rolldown/binding-android-arm-eabi": {
-      "version": "1.2.8",
-      "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.8.tgz",
-      "integrity": "sha512-tN5aztYkKCte4i5SIrrz5yK/HMjEuCqCSCJa418jOV8tZ1cBY3YF2otxB1ktPxzsLA1BeTqwapK0bfjxNvHJVw==",
+      "version": "1.2.9",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.9.tgz",
+      "integrity": "sha512-tNISae1QEf/vkb3xkRcjV5SEdzPE97We5IVaa2Z8jSszQPZ8U60B/YCYpw4QI7VidYsBtKavczXf+DyDs9WGxw==",
       "cpu": [
         "arm"
       ],
@@ -1120,9 +1145,9 @@
       }
     },
     "node_modules/@rolldown/binding-android-arm64": {
-      "version": "1.2.8",
-      "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.8.tgz",
-      "integrity": "sha512-dIYTWl9XprMUiQFoc55KUyk/oS8SKYH3zFl0LTR7RT0Xj4hgSVyuJcroH8JUu8RcpF8fTB6E0aOwCkZoYPcDSQ==",
+      "version": "1.2.9",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.9.tgz",
+      "integrity": "sha512-YC8YsI30o606GTZi0VyzYlsDKFP8W61i/QzayHDkLbNEz/IShqAmTa+hsJRj13xTHA0H+6fk4b2UmGn+Q/cMlg==",
       "cpu": [
         "arm64"
       ],
@@ -1137,9 +1162,9 @@
       }
     },
     "node_modules/@rolldown/binding-darwin-arm64": {
-      "version": "1.2.8",
-      "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.8.tgz",
-      "integrity": "sha512-PCSDQGXD2IyTEFrcgPyBM8jJuGmrbCMuoIOXdbEGVemruKACXoLQJrb+A45Z0L5t1RQkdfJprAYPkikbh7dzdA==",
+      "version": "1.2.9",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.9.tgz",
+      "integrity": "sha512-IwhlH3qK5urrY8hZiEgGkHKEFN901p/p2bjxCxJlr4GyNnF7wYpUvK+Y43uaRYuC4hpfjzbR3SJC3arX1jGvmw==",
       "cpu": [
         "arm64"
       ],
@@ -1154,9 +1179,9 @@
       }
     },
     "node_modules/@rolldown/binding-darwin-x64": {
-      "version": "1.2.8",
-      "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.8.tgz",
-      "integrity": "sha512-Uk7lRsGhPFHVX/sAUC6D5H9Ol30dFHd6iquokll2th3LpdJ3F5CzQB+7DHn0Ri2mG+U7k2zXiPHDrwZenXhwSA==",
+      "version": "1.2.9",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.9.tgz",
+      "integrity": "sha512-XxpJfVzFh+jilRxIXUqcfYAYcunIc/XEzIizsOL1fcJee5Sf7H3mH8WlLmfHfluz5amqR88QQo9izKtmMlavAw==",
       "cpu": [
         "x64"
       ],
@@ -1171,9 +1196,9 @@
       }
     },
     "node_modules/@rolldown/binding-freebsd-x64": {
-      "version": "1.2.8",
-      "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.8.tgz",
-      "integrity": "sha512-DjszaTEVogPqA5bYzsEeqDCQxbcp2fexQwKcRspYji2yzR68fCf+e4fx6kBSRDwX5/brZaHw/hWS9+A/+/w9sQ==",
+      "version": "1.2.9",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.9.tgz",
+      "integrity": "sha512-kSfvhmgeWyfkbT3p/1s5vSgboogoah2zkm9fX2zjg2hHxSV7T4KhMWRUUaRk4OXNqoD3QAUeRqLcs1aZOK4U1g==",
       "cpu": [
         "x64"
       ],
@@ -1188,9 +1213,9 @@
       }
     },
     "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
-      "version": "1.2.8",
-      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.8.tgz",
-      "integrity": "sha512-zmwa7FTmdzB6aaEEuuls18H6Ap5JmJPSoPTuXixeJZV6tG40SyLkApQtz1g8ptZtiEKqj9OM0oNLPh1AgvE31Q==",
+      "version": "1.2.9",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.9.tgz",
+      "integrity": "sha512-1RVzG17pxqbTfYLC352JlLt6kKLG+6Hr30n8DlIJqsnV5luUDd2Qdx9Ayw1Cabfyb1K9k0jXEZ7evxkRoT+uiw==",
       "cpu": [
         "arm"
       ],
@@ -1205,16 +1230,13 @@
       }
     },
     "node_modules/@rolldown/binding-linux-arm64-gnu": {
-      "version": "1.2.8",
-      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.8.tgz",
-      "integrity": "sha512-KdYQDPHwJVnbFwdTGMgxsI9SqblBlz6STGM+w1We/d5B8OWWidYH0MwkU/uA1wM5fIpO2MkOVxXrNzzuZhw9ew==",
+      "version": "1.2.9",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.9.tgz",
+      "integrity": "sha512-BXqPvZ2drqVD+/Z8UpKwcs4Mp7grM+eGFku4CAEKrEtcbAsUpzREphK1sogCRZGreVPiMkiiBtw0n3TPteuqvw==",
       "cpu": [
         "arm64"
       ],
       "dev": true,
-      "libc": [
-        "glibc"
-      ],
       "license": "MIT",
       "optional": true,
       "os": [
@@ -1225,16 +1247,13 @@
       }
     },
     "node_modules/@rolldown/binding-linux-arm64-musl": {
-      "version": "1.2.8",
-      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.8.tgz",
-      "integrity": "sha512-jFJTifHnNPY+yzOoNZQfSIysrVyXzEQPhPnOUjmD1bcQGHH6s7c8cViKWar8YplQImE5N9JRqMCLrM2CdxOrZA==",
+      "version": "1.2.9",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.9.tgz",
+      "integrity": "sha512-11vWvo8YDwLzukt27J3aYDWU+gg2P7J+ZOmiJ0hkF5BXZDW7pVya7r40MXDy6ya0i9KamoENSVKIugvJNgFXIA==",
       "cpu": [
         "arm64"
       ],
       "dev": true,
-      "libc": [
-        "musl"
-      ],
       "license": "MIT",
       "optional": true,
       "os": [
@@ -1245,16 +1264,13 @@
       }
     },
     "node_modules/@rolldown/binding-linux-ppc64-gnu": {
-      "version": "1.2.8",
-      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.8.tgz",
-      "integrity": "sha512-FhiOziBDWPBjbcmRzfLyIJnaP7AVMFXT7YCXPjXxj7wKU3vx24RjrCNN/zjvVa+N2vVoHJwCoUBvsrN/DG3zIA==",
+      "version": "1.2.9",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.9.tgz",
+      "integrity": "sha512-a1tijMkdwsIARtc0F39ApURROkf3NwqinI6TOiSSWCTR7dT96dffNvMUtDHnq64wKNTIZOIlzKrFvvFUznJiyw==",
       "cpu": [
         "ppc64"
       ],
       "dev": true,
-      "libc": [
-        "glibc"
-      ],
       "license": "MIT",
       "optional": true,
       "os": [
@@ -1265,16 +1281,13 @@
       }
     },
     "node_modules/@rolldown/binding-linux-s390x-gnu": {
-      "version": "1.2.8",
-      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.8.tgz",
-      "integrity": "sha512-WnHfADMzOV2Y55wlx1hzzQnar/wDt/VdvWSD99r18Mz9ylNieIGOkRx3UV21h7m/eJvjySYJkO26VvGNFkwsIQ==",
+      "version": "1.2.9",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.9.tgz",
+      "integrity": "sha512-x6SQNdAvv4c3hWqTMaWuawzMX9myaCs/yEmlGsxJzkdClnHW7FbrjQuSiRDhuSYzEYoEMhsaJy9qHG/XNemJPQ==",
       "cpu": [
         "s390x"
       ],
       "dev": true,
-      "libc": [
-        "glibc"
-      ],
       "license": "MIT",
       "optional": true,
       "os": [
@@ -1285,16 +1298,13 @@
       }
     },
     "node_modules/@rolldown/binding-linux-x64-gnu": {
-      "version": "1.2.8",
-      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.8.tgz",
-      "integrity": "sha512-H9tRr5ibfXFVLxbPOseVewewFpl28zcEdjRDt2FTUZU7odxP0gEv1ki4/kGmcGOh78oRwZuuQllGLZ9zTJp84g==",
+      "version": "1.2.9",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.9.tgz",
+      "integrity": "sha512-9s0AZ8BFK5/n7B/TBoa2yJE3gI3KURrbXcPBlsAsvjU4VeJKgE90y1YtNxyEUIcHPQkg6/yfF3qihUrcM/Kf0Q==",
       "cpu": [
         "x64"
       ],
       "dev": true,
-      "libc": [
-        "glibc"
-      ],
       "license": "MIT",
       "optional": true,
       "os": [
@@ -1305,16 +1315,13 @@
       }
     },
     "node_modules/@rolldown/binding-linux-x64-musl": {
-      "version": "1.2.8",
-      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.8.tgz",
-      "integrity": "sha512-UefiqfM3D6IVNlZ8tSGs9+Ejjud2T+oxO0IHADU45Y+lyEjD2dVFyZHbkfX0LUb5Zugo/oIv1eCO/KVYhgYJYA==",
+      "version": "1.2.9",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.9.tgz",
+      "integrity": "sha512-P7VWAmV+WdJluH7ovnRGoiv2i8To7GAZ+kGzfGup635cyL7SyYl3lSUaA3Gp5THf0n/Co5EyEqb2zbqq+nMOHQ==",
       "cpu": [
         "x64"
       ],
       "dev": true,
-      "libc": [
-        "musl"
-      ],
       "license": "MIT",
       "optional": true,
       "os": [
@@ -1325,9 +1332,9 @@
       }
     },
     "node_modules/@rolldown/binding-openharmony-arm64": {
-      "version": "1.2.8",
-      "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.8.tgz",
-      "integrity": "sha512-637Ke4kWSy6rp9cxQ9gMOXlxPgIw/c1beASV4M//3+9I4uwBVOOl74G+e3zyU3u19U7RkRl/HuewixZ/Z6+Rjg==",
+      "version": "1.2.9",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.9.tgz",
+      "integrity": "sha512-1qixtsE4BK8h+yS3BfmZ09UhA7O/N4IACva6YBr7EBvCJraByTuRcgOTaiA62Tm0vey3UcKXLOaoGHtYmNGEVg==",
       "cpu": [
         "arm64"
       ],
@@ -1342,9 +1349,9 @@
       }
     },
     "node_modules/@rolldown/binding-win32-arm64-msvc": {
-      "version": "1.2.8",
-      "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.8.tgz",
-      "integrity": "sha512-xWBkPOF1Q9k/Gv1nQXnVdLxKu74jXppuOM4Z3mnypVUJJJwLsMl7hNJGRAUJoG8A5MgOI1ACKM+wBFxSJzKy4A==",
+      "version": "1.2.9",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.9.tgz",
+      "integrity": "sha512-ok8IQjcEPs1AKZfuEUznVBrJw+gK4soq+bx8b1X2XoMqVClarc1q5JDmVtWXY1xfr6ZuHTAsPXHTgTrqKTZeww==",
       "cpu": [
         "arm64"
       ],
@@ -1359,9 +1366,9 @@
       }
     },
     "node_modules/@rolldown/binding-win32-x64-msvc": {
-      "version": "1.2.8",
-      "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.8.tgz",
-      "integrity": "sha512-uz2ZvfgXbxqNwijjjbxrnvALwpyODDcgc1T1N8N3rf/DXKQmaFwmB4LX4yyjggpwN2obdQLb2rgirX5ffCWYng==",
+      "version": "1.2.9",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.9.tgz",
+      "integrity": "sha512-Ip2mXoU0hM0boq3Rf+ekuT653OROSo6aSYcPT1VHE4q52KvyxgFkQgrgb/IEsxOuvQ2fZZbs8khJAyCEPM24/g==",
       "cpu": [
         "x64"
       ],
@@ -1466,9 +1473,9 @@
       "license": "MIT"
     },
     "node_modules/@types/node": {
-      "version": "24.13.4",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.4.tgz",
-      "integrity": "sha512-YJ7EqCstVTzIr0fMr7qul/977en+pQHrfmuKIo6Zr9i75Be21dr3MovcfvGtyvi2HAUrRerWps5sMO9I7WaxDw==",
+      "version": "24.13.5",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.5.tgz",
+      "integrity": "sha512-TXyindR+lBr22aJIdMQzCFHPHR6cR4js838mRDCSz5hOKWZvZwsXSSiXDmjRj4iJmgl+sR9O+1mkoVBSMadNug==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
@@ -2292,9 +2299,9 @@
       "optional": true
     },
     "node_modules/brace-expansion": {
-      "version": "5.0.9",
-      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz",
-      "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==",
+      "version": "5.0.12",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz",
+      "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
@@ -2777,9 +2784,9 @@
       "license": "MIT"
     },
     "node_modules/dir-compare/node_modules/brace-expansion": {
-      "version": "1.1.18",
-      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz",
-      "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==",
+      "version": "1.1.21",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz",
+      "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
@@ -2884,9 +2891,9 @@
       }
     },
     "node_modules/electron": {
-      "version": "44.3.0",
-      "resolved": "https://registry.npmjs.org/electron/-/electron-44.3.0.tgz",
-      "integrity": "sha512-St9EV7F2VtYaYWD2qaAjBwUgKxx39eJOUsUJ5+/1113sqbVfNqv4Dbm/W1rN7qmYSPa+mWwR6yr+b7MfgjgVfQ==",
+      "version": "44.4.1",
+      "resolved": "https://registry.npmjs.org/electron/-/electron-44.4.1.tgz",
+      "integrity": "sha512-gXJarQ2Utfn8G6bp/IqLXvckOSCtrxPWjFqQaqm02Gf4zYn1aP+RN9PKimoLH/DMqjKLfBrNC4C4CviPv0cXnQ==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
@@ -3194,9 +3201,9 @@
       "license": "MIT"
     },
     "node_modules/fast-uri": {
-      "version": "3.1.7",
-      "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.7.tgz",
-      "integrity": "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==",
+      "version": "3.1.8",
+      "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz",
+      "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==",
       "dev": true,
       "funding": [
         {
@@ -3246,9 +3253,9 @@
       "license": "MIT"
     },
     "node_modules/filelist/node_modules/brace-expansion": {
-      "version": "2.1.4",
-      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz",
-      "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==",
+      "version": "2.1.7",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz",
+      "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
@@ -3427,9 +3434,9 @@
       "license": "MIT"
     },
     "node_modules/glob/node_modules/brace-expansion": {
-      "version": "1.1.18",
-      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz",
-      "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==",
+      "version": "1.1.21",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz",
+      "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
@@ -3979,9 +3986,6 @@
         "arm64"
       ],
       "dev": true,
-      "libc": [
-        "glibc"
-      ],
       "license": "MPL-2.0",
       "optional": true,
       "os": [
@@ -4003,9 +4007,6 @@
         "arm64"
       ],
       "dev": true,
-      "libc": [
-        "musl"
-      ],
       "license": "MPL-2.0",
       "optional": true,
       "os": [
@@ -4027,9 +4028,6 @@
         "x64"
       ],
       "dev": true,
-      "libc": [
-        "glibc"
-      ],
       "license": "MPL-2.0",
       "optional": true,
       "os": [
@@ -4051,9 +4049,6 @@
         "x64"
       ],
       "dev": true,
-      "libc": [
-        "musl"
-      ],
       "license": "MPL-2.0",
       "optional": true,
       "os": [
@@ -4342,9 +4337,9 @@
       "license": "MIT"
     },
     "node_modules/nanoid": {
-      "version": "3.3.18",
-      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
-      "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",
+      "version": "3.3.19",
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz",
+      "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==",
       "dev": true,
       "funding": [
         {
@@ -4958,13 +4953,13 @@
       }
     },
     "node_modules/rolldown": {
-      "version": "1.2.8",
-      "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.8.tgz",
-      "integrity": "sha512-Z67nTmhZe7anqnM/EjI392w5i/ANUinjip7QYsOyN37oayduxt3ksdX0hf5OOamkAd53BiIHfbfSzfUmzKFQqQ==",
+      "version": "1.2.9",
+      "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.9.tgz",
+      "integrity": "sha512-hx/Pv0N1haXRb11qkfnK5MXB/iqr7i0yjWQqmO9uHqZpBgQSqzc8UsSnEpalsh+j1I8qQ2CkXAkJC8Br3dKSlg==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "@oxc-project/types": "=0.149.0",
+        "@oxc-project/types": "=0.150.0",
         "@rolldown/pluginutils": "^1.0.0"
       },
       "bin": {
@@ -4974,21 +4969,21 @@
         "node": "^20.19.0 || >=22.12.0"
       },
       "optionalDependencies": {
-        "@rolldown/binding-android-arm-eabi": "1.2.8",
-        "@rolldown/binding-android-arm64": "1.2.8",
-        "@rolldown/binding-darwin-arm64": "1.2.8",
-        "@rolldown/binding-darwin-x64": "1.2.8",
-        "@rolldown/binding-freebsd-x64": "1.2.8",
-        "@rolldown/binding-linux-arm-gnueabihf": "1.2.8",
-        "@rolldown/binding-linux-arm64-gnu": "1.2.8",
-        "@rolldown/binding-linux-arm64-musl": "1.2.8",
-        "@rolldown/binding-linux-ppc64-gnu": "1.2.8",
-        "@rolldown/binding-linux-s390x-gnu": "1.2.8",
-        "@rolldown/binding-linux-x64-gnu": "1.2.8",
-        "@rolldown/binding-linux-x64-musl": "1.2.8",
-        "@rolldown/binding-openharmony-arm64": "1.2.8",
-        "@rolldown/binding-win32-arm64-msvc": "1.2.8",
-        "@rolldown/binding-win32-x64-msvc": "1.2.8"
+        "@rolldown/binding-android-arm-eabi": "1.2.9",
+        "@rolldown/binding-android-arm64": "1.2.9",
+        "@rolldown/binding-darwin-arm64": "1.2.9",
+        "@rolldown/binding-darwin-x64": "1.2.9",
+        "@rolldown/binding-freebsd-x64": "1.2.9",
+        "@rolldown/binding-linux-arm-gnueabihf": "1.2.9",
+        "@rolldown/binding-linux-arm64-gnu": "1.2.9",
+        "@rolldown/binding-linux-arm64-musl": "1.2.9",
+        "@rolldown/binding-linux-ppc64-gnu": "1.2.9",
+        "@rolldown/binding-linux-s390x-gnu": "1.2.9",
+        "@rolldown/binding-linux-x64-gnu": "1.2.9",
+        "@rolldown/binding-linux-x64-musl": "1.2.9",
+        "@rolldown/binding-openharmony-arm64": "1.2.9",
+        "@rolldown/binding-win32-arm64-msvc": "1.2.9",
+        "@rolldown/binding-win32-x64-msvc": "1.2.9"
       }
     },
     "node_modules/safe-buffer": {
@@ -5495,16 +5490,16 @@
       "license": "MIT"
     },
     "node_modules/vite": {
-      "version": "8.2.2",
-      "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.2.tgz",
-      "integrity": "sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==",
+      "version": "8.3.0",
+      "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.0.tgz",
+      "integrity": "sha512-lhZBVvEHefgE+HQZC9O7EBJgCU/nVzFNl7vkS4RE0APtWLP02/8QVIkQtzBxPquh7lq5/78NHipTj7ODQ6XuyQ==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
         "lightningcss": "^1.33.0",
-        "picomatch": "^4.0.5",
-        "postcss": "^8.5.26",
-        "rolldown": "~1.2.4",
+        "picomatch": "^4.0.7",
+        "postcss": "^8.5.28",
+        "rolldown": "~1.2.6",
         "tinyglobby": "^0.2.17"
       },
       "bin": {
@@ -5521,7 +5516,7 @@
       },
       "peerDependencies": {
         "@types/node": "^20.19.0 || >=22.12.0",
-        "@vitejs/devtools": "^0.4.0 || ^0.5.0",
+        "@vitejs/devtools": "^0.7.1",
         "esbuild": "^0.27.0 || ^0.28.0",
         "jiti": ">=1.21.0",
         "less": "^4.0.0",
diff --git a/package.json b/package.json
index 4be4418..c483999 100644
--- a/package.json
+++ b/package.json
@@ -33,6 +33,8 @@
   },
   "devDependencies": {
     "@codemirror/lang-javascript": "^6.2.5",
+    "@codemirror/lang-json": "^6.0.2",
+    "@codemirror/search": "^6.7.2",
     "@codemirror/theme-one-dark": "^6.1.3",
     "@types/node": "^24.13.4",
     "@types/react": "^19.3.0",
@@ -50,5 +52,8 @@
   "allowScripts": {
     "esbuild@0.28.2": true,
     "electron-winstaller@5.4.0": true
+  },
+  "volta": {
+    "node": "22.12.0"
   }
 }
diff --git a/scripts/ui-check.ts b/scripts/ui-check.ts
index d768819..1b81e82 100644
--- a/scripts/ui-check.ts
+++ b/scripts/ui-check.ts
@@ -1019,6 +1019,59 @@ async function main(): Promise {
   await wait(500);
   await shoot(window, '10-query-json');
 
+  console.log('  finding and replacing inside the JSON view…');
+  if (!(await click(window, '.find-trigger'))) {
+    throw new Error('the find trigger should open the find bar');
+  }
+  await wait(200);
+  if (!(await fill(window, '.find-bar input[placeholder="Find"]', 'paid'))) {
+    throw new Error('the find bar should have a find field');
+  }
+  await wait(300);
+  if (!(await textOf(window, '.find-count')).includes('/')) {
+    throw new Error('typing a search term should show a match count');
+  }
+  await shoot(window, '10b-json-find');
+  await click(window, '.find-bar button[title="Next match (Enter)"]');
+  await wait(200);
+  await click(window, '.find-case');
+  await wait(200);
+  await shoot(window, '10c-json-find-case');
+  if (!(await fill(window, '.find-bar input[placeholder="Replace"]', 'PAID-TEST'))) {
+    throw new Error('the find bar should have a replace field');
+  }
+  if (!(await click(window, '.find-bar .btn', 'Replace all'))) {
+    throw new Error('the find bar should have a Replace all button');
+  }
+  await wait(300);
+  if (!(await textOf(window, '.cm-content')).includes('PAID-TEST')) {
+    throw new Error('replace all should update the JSON buffer');
+  }
+  await shoot(window, '10d-json-replace-all');
+  await click(window, '.find-bar .icon-button');
+  await wait(200);
+
+  console.log('  back to table, inspecting a nested document…');
+  await click(window, '.segmented button', 'Table');
+  await wait(500);
+  if (!(await cellEvent(window, 0, 'customer', 'dblclick')).ok) {
+    throw new Error('double-clicking an expandable cell should open the document inspector');
+  }
+  await wait(500);
+  await shoot(window, '10e-inspect-modal');
+  if (!(await click(window, '.find-trigger'))) {
+    throw new Error('the inspector modal should also have a find trigger');
+  }
+  await wait(200);
+  await fill(window, '.find-bar input[placeholder="Find"]', 'Jakarta');
+  await wait(300);
+  if (!(await textOf(window, '.find-count')).includes('/')) {
+    throw new Error('the inspector modal find bar should report a match count');
+  }
+  await shoot(window, '10f-inspect-modal-find');
+  await click(window, '.modal-header .icon-button');
+  await wait(300);
+
   console.log('  opening statistics…');
   await click(window, '.titlebar-actions .btn', 'Statistics');
   await wait(2500);
diff --git a/src/components/JsonSearchView.tsx b/src/components/JsonSearchView.tsx
new file mode 100644
index 0000000..985d0f7
--- /dev/null
+++ b/src/components/JsonSearchView.tsx
@@ -0,0 +1,216 @@
+import { json } from '@codemirror/lang-json';
+import {
+  SearchQuery,
+  closeSearchPanel,
+  findNext,
+  findPrevious,
+  openSearchPanel,
+  replaceAll as replaceAllMatches,
+  replaceNext,
+  search,
+  setSearchQuery
+} from '@codemirror/search';
+import { Prec } from '@codemirror/state';
+import { oneDark } from '@codemirror/theme-one-dark';
+import { EditorView, keymap } from '@codemirror/view';
+import CodeMirror, { type ReactCodeMirrorRef } from '@uiw/react-codemirror';
+import { useCallback, useEffect, useMemo, useRef, useState, type KeyboardEvent } from 'react';
+import { Button, TextInput } from './ui';
+
+/**
+ * A pretty-printed JSON viewer with an in-buffer find/replace bar. Edits here
+ * are local to the CodeMirror instance only — nothing is written back to the
+ * collection, so it is safe to use for read-only result and document views.
+ */
+export function JsonSearchView({ value, minHeight = '200px' }: { value: string; minHeight?: string }) {
+  const editorRef = useRef(null);
+  const findInputRef = useRef(null);
+  const [open, setOpen] = useState(false);
+  const [findText, setFindText] = useState('');
+  const [replaceText, setReplaceText] = useState('');
+  const [matchCase, setMatchCase] = useState(false);
+  const [matches, setMatches] = useState({ current: 0, total: 0 });
+
+  const dark = (document.documentElement.dataset.theme ?? 'dark') === 'dark';
+
+  const buildQuery = useCallback(
+    () => new SearchQuery({ search: findText, caseSensitive: matchCase, literal: true, replace: replaceText }),
+    [findText, matchCase, replaceText]
+  );
+
+  const refreshMatches = useCallback((view: EditorView, query: SearchQuery) => {
+    if (!query.search) {
+      setMatches({ current: 0, total: 0 });
+      return;
+    }
+    const cursor = query.getCursor(view.state);
+    const sel = view.state.selection.main;
+    let total = 0;
+    let current = 0;
+    for (let next = cursor.next(); !next.done; next = cursor.next()) {
+      total += 1;
+      if (next.value.from === sel.from && next.value.to === sel.to) current = total;
+    }
+    setMatches({ current, total });
+  }, []);
+
+  const dispatchQuery = useCallback(() => {
+    const view = editorRef.current?.view;
+    if (!view) return null;
+    const query = buildQuery();
+    view.dispatch({ effects: setSearchQuery.of(query) });
+    return { view, query };
+  }, [buildQuery]);
+
+  // CodeMirror only decorates matches while its own search panel is open (its
+  // "is a search active" flag), so we open it to get highlighting and hide it
+  // with CSS in favour of this component's own bar, then steal focus back.
+  useEffect(() => {
+    const view = editorRef.current?.view;
+    if (!view) return;
+    if (open) {
+      openSearchPanel(view);
+      const frame = requestAnimationFrame(() => findInputRef.current?.focus());
+      return () => cancelAnimationFrame(frame);
+    }
+    closeSearchPanel(view);
+  }, [open]);
+
+  // Keeps every match highlighted as the search text or case toggle changes,
+  // without moving the selection — only Enter/next/previous does that.
+  useEffect(() => {
+    const view = editorRef.current?.view;
+    if (!view) return;
+    const query = new SearchQuery({ search: findText, caseSensitive: matchCase, literal: true });
+    view.dispatch({ effects: setSearchQuery.of(query) });
+    refreshMatches(view, query);
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [findText, matchCase]);
+
+  const runFind = useCallback(
+    (direction: 'next' | 'previous') => {
+      if (!findText) return;
+      const dispatched = dispatchQuery();
+      if (!dispatched) return;
+      (direction === 'next' ? findNext : findPrevious)(dispatched.view);
+      refreshMatches(dispatched.view, dispatched.query);
+    },
+    [dispatchQuery, findText, refreshMatches]
+  );
+
+  const runReplace = useCallback(
+    (all: boolean) => {
+      if (!findText) return;
+      const dispatched = dispatchQuery();
+      if (!dispatched) return;
+      (all ? replaceAllMatches : replaceNext)(dispatched.view);
+      refreshMatches(dispatched.view, buildQuery());
+    },
+    [buildQuery, dispatchQuery, findText, refreshMatches]
+  );
+
+  const extensions = useMemo(
+    () => [
+      json(),
+      search(),
+      EditorView.lineWrapping,
+      Prec.highest(
+        keymap.of([
+          {
+            key: 'Mod-f',
+            run: () => {
+              setOpen(true);
+              return true;
+            }
+          }
+        ])
+      )
+    ],
+    []
+  );
+
+  const onFindKeyDown = (event: KeyboardEvent) => {
+    if (event.key === 'Enter') {
+      event.preventDefault();
+      runFind(event.shiftKey ? 'previous' : 'next');
+    }
+    if (event.key === 'Escape') setOpen(false);
+  };
+
+  return (
+    
+
+ {open ? ( +
+ setFindText(event.target.value)} + onKeyDown={onFindKeyDown} + /> + {findText ? `${matches.current}/${matches.total}` : ''} + + + + setReplaceText(event.target.value)} + onKeyDown={(event) => { + if (event.key === 'Enter') runReplace(false); + }} + /> + + + +
+ ) : ( + + )} +
+ +
+ ); +} diff --git a/src/components/ResultView.tsx b/src/components/ResultView.tsx index 6a28234..a80bcbb 100644 --- a/src/components/ResultView.tsx +++ b/src/components/ResultView.tsx @@ -4,6 +4,7 @@ import { api, unwrap } from '../lib/api'; import { cellEditHint, cellEditor, cellValueJson, type CellEditKind } from '../lib/cellEdit'; import { formatCellValue, plural, prettyJson, shellLiteral, valueType } from '../lib/format'; import { useStore } from '../state/store'; +import { JsonSearchView } from './JsonSearchView'; import { Button, ContextMenu, @@ -818,7 +819,7 @@ export function ResultView({ } if (mode === 'json') { - return
{prettyJson(rows)}
; + return ; } return ( @@ -937,9 +938,7 @@ export function ResultView({ {inspected !== null ? ( setInspected(null)} width={760}> -
-            {prettyJson(inspected)}
-          
+
) : null} diff --git a/src/styles.css b/src/styles.css index f44d2fc..b1ff169 100644 --- a/src/styles.css +++ b/src/styles.css @@ -640,6 +640,76 @@ select.cell-editor { color: var(--text); } +.json-search { + display: flex; + flex-direction: column; + min-height: 0; +} + +.json-search .cm-editor { + height: 100%; +} + +.json-search .cm-scroller { + font-family: var(--font-mono); + font-size: 12px; + line-height: 1.55; +} + +/* CodeMirror's own search panel is only kept open for its match-highlighting + side effect — this component's .find-bar is the actual, visible UI. */ +.json-search .cm-panel.cm-search { + display: none; +} + +.find-bar-row { + border-bottom: 1px solid var(--border); + background: var(--bg-elevated); +} + +.find-trigger { + background: none; + border: none; + color: var(--text-faint); + cursor: pointer; + padding: 6px 12px; + font-size: 12px; +} + +.find-trigger:hover { + color: var(--text); +} + +.find-bar { + display: flex; + align-items: center; + gap: 6px; + padding: 6px 12px; + flex-wrap: wrap; +} + +.find-bar .input { + width: 160px; +} + +.find-count { + min-width: 40px; + font-size: 11px; + color: var(--text-faint); + text-align: center; +} + +.find-case { + color: var(--text-dim); + font-weight: 600; +} + +.find-case.is-active { + background: var(--accent-soft); + border-color: var(--accent); + color: var(--accent); +} + /* ---------- stats ---------- */ .stats-page {