Kaynağa Gözat

wip: desktop work

Adam 6 ay önce
ebeveyn
işleme
b1e6b9c7c9

+ 9 - 6
bun.lock

@@ -24,7 +24,8 @@
         "@solidjs/router": "0.15.3",
         "@thisbeyond/solid-dnd": "0.7.5",
         "diff": "8.0.2",
-        "luxon": "3.7.1",
+        "fuzzysort": "catalog:",
+        "luxon": "catalog:",
         "marked": "16.2.0",
         "marked-shiki": "1.2.1",
         "remeda": "catalog:",
@@ -218,11 +219,11 @@
         "diff": "8.0.2",
         "js-base64": "3.7.7",
         "lang-map": "0.4.0",
-        "luxon": "3.6.1",
+        "luxon": "catalog:",
         "marked": "15.0.12",
         "marked-shiki": "1.2.1",
         "rehype-autolink-headings": "7.1.0",
-        "remeda": "2.26.0",
+        "remeda": "catalog:",
         "sharp": "0.32.5",
         "shiki": "3.4.2",
         "solid-js": "catalog:",
@@ -249,7 +250,9 @@
     "@types/bun": "1.2.21",
     "@types/node": "22.13.9",
     "ai": "5.0.8",
+    "fuzzysort": "3.1.0",
     "hono": "4.7.10",
+    "luxon": "3.6.1",
     "remeda": "2.26.0",
     "solid-js": "1.9.9",
     "typescript": "5.8.2",
@@ -1660,6 +1663,8 @@
 
     "function-bind": ["[email protected]", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
 
+    "fuzzysort": ["[email protected]", "", {}, "sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ=="],
+
     "gel": ["[email protected]", "", { "dependencies": { "@petamoriken/float16": "^3.8.7", "debug": "^4.3.4", "env-paths": "^3.0.0", "semver": "^7.6.2", "shell-quote": "^1.8.1", "which": "^4.0.0" }, "bin": { "gel": "dist/cli.mjs" } }, "sha512-Newg9X7mRYskoBjSw70l1YnJ/ZGbq64VPyR821H5WVkTGpHG2O0mQILxCeUhxdYERLFY9B4tUyKLyf3uMTjtKw=="],
 
     "generate-function": ["[email protected]", "", { "dependencies": { "is-property": "^1.0.2" } }, "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ=="],
@@ -1988,7 +1993,7 @@
 
     "lru.min": ["[email protected]", "", {}, "sha512-Nv9KddBcQSlQopmBHXSsZVY5xsdlZkdH/Iey0BlcBYggMd4two7cZnKOK9vmy3nY0O5RGH99z1PCeTpPqszUYg=="],
 
-    "luxon": ["luxon@3.7.1", "", {}, "sha512-RkRWjA926cTvz5rAb1BqyWkKbbjzCGchDUIKMCUvNi17j6f6j8uHGDV82Aqcqtzd+icoYpELmG3ksgGiFNNcNg=="],
+    "luxon": ["luxon@3.6.1", "", {}, "sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ=="],
 
     "magic-string": ["[email protected]", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw=="],
 
@@ -3034,8 +3039,6 @@
 
     "@opencode/web/@types/luxon": ["@types/[email protected]", "", {}, "sha512-R/BdP7OxEMc44l2Ex5lSXHoIXTB2JLNa3y2QISIbr58U/YcsffyQrYW//hZSdrfxrjRZj3GcUoxMPGdO8gSYuw=="],
 
-    "@opencode/web/luxon": ["[email protected]", "", {}, "sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ=="],
-
     "@opencode/web/marked": ["[email protected]", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA=="],
 
     "@opencode/web/shiki": ["[email protected]", "", { "dependencies": { "@shikijs/core": "3.4.2", "@shikijs/engine-javascript": "3.4.2", "@shikijs/engine-oniguruma": "3.4.2", "@shikijs/langs": "3.4.2", "@shikijs/themes": "3.4.2", "@shikijs/types": "3.4.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-wuxzZzQG8kvZndD7nustrNFIKYJ1jJoWIPaBpVe2+KHSvtzMi4SBjOxrigs8qeqce/l3U0cwiC+VAkLKSunHQQ=="],

+ 2 - 0
package.json

@@ -23,6 +23,8 @@
       "@tsconfig/node22": "22.0.2",
       "ai": "5.0.8",
       "hono": "4.7.10",
+      "fuzzysort": "3.1.0",
+      "luxon": "3.6.1",
       "typescript": "5.8.2",
       "zod": "4.1.8",
       "remeda": "2.26.0",

+ 2 - 1
packages/app/package.json

@@ -29,7 +29,8 @@
     "@solidjs/router": "0.15.3",
     "@thisbeyond/solid-dnd": "0.7.5",
     "diff": "8.0.2",
-    "luxon": "3.7.1",
+    "fuzzysort": "catalog:",
+    "luxon": "catalog:",
     "marked": "16.2.0",
     "marked-shiki": "1.2.1",
     "remeda": "catalog:",

+ 357 - 356
packages/app/src/assets/theme.css

@@ -159,109 +159,109 @@
 }
 
 [data-theme="ayu"][data-dark="false"] {
-  --theme-primary: #59c2ff;
-  --theme-secondary: #d2a6ff;
-  --theme-accent: #e6b450;
-  --theme-error: #d95757;
-  --theme-warning: #e6b673;
-  --theme-success: #7fd962;
-  --theme-info: #39bae6;
-  --theme-text: #bfbdb6;
-  --theme-text-muted: #565b66;
-  --theme-background: #0b0e14;
-  --theme-background-panel: #0f131a;
-  --theme-background-element: #0d1017;
-  --theme-border: #6c7380;
-  --theme-border-active: #6c7380;
-  --theme-border-subtle: #11151c;
-  --theme-diff-added: #7fd962;
-  --theme-diff-removed: #f26d78;
-  --theme-diff-context: #acb6bf;
-  --theme-diff-hunk-header: #acb6bf;
-  --theme-diff-highlight-added: #aad94c;
-  --theme-diff-highlight-removed: #f07178;
+  --theme-primary: #59C2FF;
+  --theme-secondary: #D2A6FF;
+  --theme-accent: #E6B450;
+  --theme-error: #D95757;
+  --theme-warning: #E6B673;
+  --theme-success: #7FD962;
+  --theme-info: #39BAE6;
+  --theme-text: #BFBDB6;
+  --theme-text-muted: #565B66;
+  --theme-background: #0B0E14;
+  --theme-background-panel: #0F131A;
+  --theme-background-element: #0D1017;
+  --theme-border: #6C7380;
+  --theme-border-active: #6C7380;
+  --theme-border-subtle: #11151C;
+  --theme-diff-added: #7FD962;
+  --theme-diff-removed: #F26D78;
+  --theme-diff-context: #ACB6BF;
+  --theme-diff-hunk-header: #ACB6BF;
+  --theme-diff-highlight-added: #AAD94C;
+  --theme-diff-highlight-removed: #F07178;
   --theme-diff-added-bg: #20303b;
   --theme-diff-removed-bg: #37222c;
-  --theme-diff-context-bg: #0f131a;
-  --theme-diff-line-number: #6c7380;
+  --theme-diff-context-bg: #0F131A;
+  --theme-diff-line-number: #6C7380;
   --theme-diff-added-line-number-bg: #1b2b34;
   --theme-diff-removed-line-number-bg: #2d1f26;
-  --theme-markdown-text: #bfbdb6;
-  --theme-markdown-heading: #d2a6ff;
-  --theme-markdown-link: #59c2ff;
-  --theme-markdown-link-text: #39bae6;
-  --theme-markdown-code: #aad94c;
-  --theme-markdown-block-quote: #e6b673;
-  --theme-markdown-emph: #e6b673;
-  --theme-markdown-strong: #ffb454;
-  --theme-markdown-horizontal-rule: #565b66;
-  --theme-markdown-list-item: #59c2ff;
-  --theme-markdown-list-enumeration: #39bae6;
-  --theme-markdown-image: #59c2ff;
-  --theme-markdown-image-text: #39bae6;
-  --theme-markdown-code-block: #bfbdb6;
-  --theme-syntax-comment: #acb6bf;
-  --theme-syntax-keyword: #ff8f40;
-  --theme-syntax-function: #ffb454;
-  --theme-syntax-variable: #59c2ff;
-  --theme-syntax-string: #aad94c;
-  --theme-syntax-number: #d2a6ff;
-  --theme-syntax-type: #e6b673;
-  --theme-syntax-operator: #f29668;
-  --theme-syntax-punctuation: #bfbdb6;
+  --theme-markdown-text: #BFBDB6;
+  --theme-markdown-heading: #D2A6FF;
+  --theme-markdown-link: #59C2FF;
+  --theme-markdown-link-text: #39BAE6;
+  --theme-markdown-code: #AAD94C;
+  --theme-markdown-block-quote: #E6B673;
+  --theme-markdown-emph: #E6B673;
+  --theme-markdown-strong: #FFB454;
+  --theme-markdown-horizontal-rule: #565B66;
+  --theme-markdown-list-item: #59C2FF;
+  --theme-markdown-list-enumeration: #39BAE6;
+  --theme-markdown-image: #59C2FF;
+  --theme-markdown-image-text: #39BAE6;
+  --theme-markdown-code-block: #BFBDB6;
+  --theme-syntax-comment: #ACB6BF;
+  --theme-syntax-keyword: #FF8F40;
+  --theme-syntax-function: #FFB454;
+  --theme-syntax-variable: #59C2FF;
+  --theme-syntax-string: #AAD94C;
+  --theme-syntax-number: #D2A6FF;
+  --theme-syntax-type: #E6B673;
+  --theme-syntax-operator: #F29668;
+  --theme-syntax-punctuation: #BFBDB6;
 }
 
 [data-theme="ayu"][data-dark="true"] {
-  --theme-primary: #59c2ff;
-  --theme-secondary: #d2a6ff;
-  --theme-accent: #e6b450;
-  --theme-error: #d95757;
-  --theme-warning: #e6b673;
-  --theme-success: #7fd962;
-  --theme-info: #39bae6;
-  --theme-text: #bfbdb6;
-  --theme-text-muted: #565b66;
-  --theme-background: #0b0e14;
-  --theme-background-panel: #0f131a;
-  --theme-background-element: #0d1017;
-  --theme-border: #6c7380;
-  --theme-border-active: #6c7380;
-  --theme-border-subtle: #11151c;
-  --theme-diff-added: #7fd962;
-  --theme-diff-removed: #f26d78;
-  --theme-diff-context: #acb6bf;
-  --theme-diff-hunk-header: #acb6bf;
-  --theme-diff-highlight-added: #aad94c;
-  --theme-diff-highlight-removed: #f07178;
+  --theme-primary: #59C2FF;
+  --theme-secondary: #D2A6FF;
+  --theme-accent: #E6B450;
+  --theme-error: #D95757;
+  --theme-warning: #E6B673;
+  --theme-success: #7FD962;
+  --theme-info: #39BAE6;
+  --theme-text: #BFBDB6;
+  --theme-text-muted: #565B66;
+  --theme-background: #0B0E14;
+  --theme-background-panel: #0F131A;
+  --theme-background-element: #0D1017;
+  --theme-border: #6C7380;
+  --theme-border-active: #6C7380;
+  --theme-border-subtle: #11151C;
+  --theme-diff-added: #7FD962;
+  --theme-diff-removed: #F26D78;
+  --theme-diff-context: #ACB6BF;
+  --theme-diff-hunk-header: #ACB6BF;
+  --theme-diff-highlight-added: #AAD94C;
+  --theme-diff-highlight-removed: #F07178;
   --theme-diff-added-bg: #20303b;
   --theme-diff-removed-bg: #37222c;
-  --theme-diff-context-bg: #0f131a;
-  --theme-diff-line-number: #6c7380;
+  --theme-diff-context-bg: #0F131A;
+  --theme-diff-line-number: #6C7380;
   --theme-diff-added-line-number-bg: #1b2b34;
   --theme-diff-removed-line-number-bg: #2d1f26;
-  --theme-markdown-text: #bfbdb6;
-  --theme-markdown-heading: #d2a6ff;
-  --theme-markdown-link: #59c2ff;
-  --theme-markdown-link-text: #39bae6;
-  --theme-markdown-code: #aad94c;
-  --theme-markdown-block-quote: #e6b673;
-  --theme-markdown-emph: #e6b673;
-  --theme-markdown-strong: #ffb454;
-  --theme-markdown-horizontal-rule: #565b66;
-  --theme-markdown-list-item: #59c2ff;
-  --theme-markdown-list-enumeration: #39bae6;
-  --theme-markdown-image: #59c2ff;
-  --theme-markdown-image-text: #39bae6;
-  --theme-markdown-code-block: #bfbdb6;
-  --theme-syntax-comment: #acb6bf;
-  --theme-syntax-keyword: #ff8f40;
-  --theme-syntax-function: #ffb454;
-  --theme-syntax-variable: #59c2ff;
-  --theme-syntax-string: #aad94c;
-  --theme-syntax-number: #d2a6ff;
-  --theme-syntax-type: #e6b673;
-  --theme-syntax-operator: #f29668;
-  --theme-syntax-punctuation: #bfbdb6;
+  --theme-markdown-text: #BFBDB6;
+  --theme-markdown-heading: #D2A6FF;
+  --theme-markdown-link: #59C2FF;
+  --theme-markdown-link-text: #39BAE6;
+  --theme-markdown-code: #AAD94C;
+  --theme-markdown-block-quote: #E6B673;
+  --theme-markdown-emph: #E6B673;
+  --theme-markdown-strong: #FFB454;
+  --theme-markdown-horizontal-rule: #565B66;
+  --theme-markdown-list-item: #59C2FF;
+  --theme-markdown-list-enumeration: #39BAE6;
+  --theme-markdown-image: #59C2FF;
+  --theme-markdown-image-text: #39BAE6;
+  --theme-markdown-code-block: #BFBDB6;
+  --theme-syntax-comment: #ACB6BF;
+  --theme-syntax-keyword: #FF8F40;
+  --theme-syntax-function: #FFB454;
+  --theme-syntax-variable: #59C2FF;
+  --theme-syntax-string: #AAD94C;
+  --theme-syntax-number: #D2A6FF;
+  --theme-syntax-type: #E6B673;
+  --theme-syntax-operator: #F29668;
+  --theme-syntax-punctuation: #BFBDB6;
 }
 
 [data-theme="catppuccin"][data-dark="false"] {
@@ -901,109 +901,109 @@
 }
 
 [data-theme="kanagawa"][data-dark="false"] {
-  --theme-primary: #2d4f67;
-  --theme-secondary: #957fb8;
-  --theme-accent: #d27e99;
-  --theme-error: #e82424;
-  --theme-warning: #d7a657;
-  --theme-success: #98bb6c;
-  --theme-info: #76946a;
-  --theme-text: #54433a;
-  --theme-text-muted: #9e9389;
-  --theme-background: #f2e9de;
-  --theme-background-panel: #eae4d7;
-  --theme-background-element: #e3dcd2;
-  --theme-border: #d4cbbf;
-  --theme-border-active: #c38d9d;
-  --theme-border-subtle: #dcd4c9;
-  --theme-diff-added: #98bb6c;
-  --theme-diff-removed: #e82424;
-  --theme-diff-context: #9e9389;
-  --theme-diff-hunk-header: #2d4f67;
-  --theme-diff-highlight-added: #89af5b;
-  --theme-diff-highlight-removed: #d61f1f;
-  --theme-diff-added-bg: #eaf3e4;
-  --theme-diff-removed-bg: #fbe6e6;
-  --theme-diff-context-bg: #eae4d7;
-  --theme-diff-line-number: #c7beb4;
-  --theme-diff-added-line-number-bg: #dde8d6;
-  --theme-diff-removed-line-number-bg: #f2dada;
-  --theme-markdown-text: #54433a;
-  --theme-markdown-heading: #957fb8;
-  --theme-markdown-link: #2d4f67;
-  --theme-markdown-link-text: #76946a;
-  --theme-markdown-code: #98bb6c;
-  --theme-markdown-block-quote: #9e9389;
-  --theme-markdown-emph: #c38d9d;
-  --theme-markdown-strong: #d7a657;
-  --theme-markdown-horizontal-rule: #9e9389;
-  --theme-markdown-list-item: #2d4f67;
-  --theme-markdown-list-enumeration: #76946a;
-  --theme-markdown-image: #2d4f67;
-  --theme-markdown-image-text: #76946a;
-  --theme-markdown-code-block: #54433a;
-  --theme-syntax-comment: #9e9389;
-  --theme-syntax-keyword: #957fb8;
-  --theme-syntax-function: #2d4f67;
-  --theme-syntax-variable: #54433a;
-  --theme-syntax-string: #98bb6c;
-  --theme-syntax-number: #d7a657;
-  --theme-syntax-type: #c38d9d;
-  --theme-syntax-operator: #d27e99;
-  --theme-syntax-punctuation: #54433a;
+  --theme-primary: #2D4F67;
+  --theme-secondary: #957FB8;
+  --theme-accent: #D27E99;
+  --theme-error: #E82424;
+  --theme-warning: #D7A657;
+  --theme-success: #98BB6C;
+  --theme-info: #76946A;
+  --theme-text: #54433A;
+  --theme-text-muted: #9E9389;
+  --theme-background: #F2E9DE;
+  --theme-background-panel: #EAE4D7;
+  --theme-background-element: #E3DCD2;
+  --theme-border: #D4CBBF;
+  --theme-border-active: #C38D9D;
+  --theme-border-subtle: #DCD4C9;
+  --theme-diff-added: #98BB6C;
+  --theme-diff-removed: #E82424;
+  --theme-diff-context: #9E9389;
+  --theme-diff-hunk-header: #2D4F67;
+  --theme-diff-highlight-added: #89AF5B;
+  --theme-diff-highlight-removed: #D61F1F;
+  --theme-diff-added-bg: #EAF3E4;
+  --theme-diff-removed-bg: #FBE6E6;
+  --theme-diff-context-bg: #EAE4D7;
+  --theme-diff-line-number: #C7BEB4;
+  --theme-diff-added-line-number-bg: #DDE8D6;
+  --theme-diff-removed-line-number-bg: #F2DADA;
+  --theme-markdown-text: #54433A;
+  --theme-markdown-heading: #957FB8;
+  --theme-markdown-link: #2D4F67;
+  --theme-markdown-link-text: #76946A;
+  --theme-markdown-code: #98BB6C;
+  --theme-markdown-block-quote: #9E9389;
+  --theme-markdown-emph: #C38D9D;
+  --theme-markdown-strong: #D7A657;
+  --theme-markdown-horizontal-rule: #9E9389;
+  --theme-markdown-list-item: #2D4F67;
+  --theme-markdown-list-enumeration: #76946A;
+  --theme-markdown-image: #2D4F67;
+  --theme-markdown-image-text: #76946A;
+  --theme-markdown-code-block: #54433A;
+  --theme-syntax-comment: #9E9389;
+  --theme-syntax-keyword: #957FB8;
+  --theme-syntax-function: #2D4F67;
+  --theme-syntax-variable: #54433A;
+  --theme-syntax-string: #98BB6C;
+  --theme-syntax-number: #D7A657;
+  --theme-syntax-type: #C38D9D;
+  --theme-syntax-operator: #D27E99;
+  --theme-syntax-punctuation: #54433A;
 }
 
 [data-theme="kanagawa"][data-dark="true"] {
-  --theme-primary: #7e9cd8;
-  --theme-secondary: #957fb8;
-  --theme-accent: #d27e99;
-  --theme-error: #e82424;
-  --theme-warning: #d7a657;
-  --theme-success: #98bb6c;
-  --theme-info: #76946a;
-  --theme-text: #dcd7ba;
+  --theme-primary: #7E9CD8;
+  --theme-secondary: #957FB8;
+  --theme-accent: #D27E99;
+  --theme-error: #E82424;
+  --theme-warning: #D7A657;
+  --theme-success: #98BB6C;
+  --theme-info: #76946A;
+  --theme-text: #DCD7BA;
   --theme-text-muted: #727169;
-  --theme-background: #1f1f28;
-  --theme-background-panel: #2a2a37;
+  --theme-background: #1F1F28;
+  --theme-background-panel: #2A2A37;
   --theme-background-element: #363646;
-  --theme-border: #54546d;
-  --theme-border-active: #c38d9d;
+  --theme-border: #54546D;
+  --theme-border-active: #C38D9D;
   --theme-border-subtle: #363646;
-  --theme-diff-added: #98bb6c;
-  --theme-diff-removed: #e82424;
+  --theme-diff-added: #98BB6C;
+  --theme-diff-removed: #E82424;
   --theme-diff-context: #727169;
-  --theme-diff-hunk-header: #2d4f67;
-  --theme-diff-highlight-added: #a9d977;
-  --theme-diff-highlight-removed: #f24a4a;
-  --theme-diff-added-bg: #252e25;
+  --theme-diff-hunk-header: #2D4F67;
+  --theme-diff-highlight-added: #A9D977;
+  --theme-diff-highlight-removed: #F24A4A;
+  --theme-diff-added-bg: #252E25;
   --theme-diff-removed-bg: #362020;
-  --theme-diff-context-bg: #2a2a37;
-  --theme-diff-line-number: #54546d;
+  --theme-diff-context-bg: #2A2A37;
+  --theme-diff-line-number: #54546D;
   --theme-diff-added-line-number-bg: #202820;
-  --theme-diff-removed-line-number-bg: #2d1c1c;
-  --theme-markdown-text: #dcd7ba;
-  --theme-markdown-heading: #957fb8;
-  --theme-markdown-link: #7e9cd8;
-  --theme-markdown-link-text: #76946a;
-  --theme-markdown-code: #98bb6c;
+  --theme-diff-removed-line-number-bg: #2D1C1C;
+  --theme-markdown-text: #DCD7BA;
+  --theme-markdown-heading: #957FB8;
+  --theme-markdown-link: #7E9CD8;
+  --theme-markdown-link-text: #76946A;
+  --theme-markdown-code: #98BB6C;
   --theme-markdown-block-quote: #727169;
-  --theme-markdown-emph: #c38d9d;
-  --theme-markdown-strong: #d7a657;
+  --theme-markdown-emph: #C38D9D;
+  --theme-markdown-strong: #D7A657;
   --theme-markdown-horizontal-rule: #727169;
-  --theme-markdown-list-item: #7e9cd8;
-  --theme-markdown-list-enumeration: #76946a;
-  --theme-markdown-image: #7e9cd8;
-  --theme-markdown-image-text: #76946a;
-  --theme-markdown-code-block: #dcd7ba;
+  --theme-markdown-list-item: #7E9CD8;
+  --theme-markdown-list-enumeration: #76946A;
+  --theme-markdown-image: #7E9CD8;
+  --theme-markdown-image-text: #76946A;
+  --theme-markdown-code-block: #DCD7BA;
   --theme-syntax-comment: #727169;
-  --theme-syntax-keyword: #957fb8;
-  --theme-syntax-function: #7e9cd8;
-  --theme-syntax-variable: #dcd7ba;
-  --theme-syntax-string: #98bb6c;
-  --theme-syntax-number: #d7a657;
-  --theme-syntax-type: #c38d9d;
-  --theme-syntax-operator: #d27e99;
-  --theme-syntax-punctuation: #dcd7ba;
+  --theme-syntax-keyword: #957FB8;
+  --theme-syntax-function: #7E9CD8;
+  --theme-syntax-variable: #DCD7BA;
+  --theme-syntax-string: #98BB6C;
+  --theme-syntax-number: #D7A657;
+  --theme-syntax-type: #C38D9D;
+  --theme-syntax-operator: #D27E99;
+  --theme-syntax-punctuation: #DCD7BA;
 }
 
 [data-theme="material"][data-dark="false"] {
@@ -1325,109 +1325,109 @@
 }
 
 [data-theme="nord"][data-dark="false"] {
-  --theme-primary: #5e81ac;
-  --theme-secondary: #81a1c1;
-  --theme-accent: #8fbcbb;
-  --theme-error: #bf616a;
-  --theme-warning: #d08770;
-  --theme-success: #a3be8c;
-  --theme-info: #5e81ac;
-  --theme-text: #2e3440;
-  --theme-text-muted: #3b4252;
-  --theme-background: #eceff4;
-  --theme-background-panel: #e5e9f0;
-  --theme-background-element: #d8dee9;
-  --theme-border: #4c566a;
-  --theme-border-active: #434c5e;
-  --theme-border-subtle: #4c566a;
-  --theme-diff-added: #a3be8c;
-  --theme-diff-removed: #bf616a;
-  --theme-diff-context: #4c566a;
-  --theme-diff-hunk-header: #4c566a;
-  --theme-diff-highlight-added: #a3be8c;
-  --theme-diff-highlight-removed: #bf616a;
-  --theme-diff-added-bg: #e5e9f0;
-  --theme-diff-removed-bg: #e5e9f0;
-  --theme-diff-context-bg: #e5e9f0;
-  --theme-diff-line-number: #d8dee9;
-  --theme-diff-added-line-number-bg: #e5e9f0;
-  --theme-diff-removed-line-number-bg: #e5e9f0;
-  --theme-markdown-text: #2e3440;
-  --theme-markdown-heading: #5e81ac;
-  --theme-markdown-link: #81a1c1;
-  --theme-markdown-link-text: #8fbcbb;
-  --theme-markdown-code: #a3be8c;
-  --theme-markdown-block-quote: #4c566a;
-  --theme-markdown-emph: #d08770;
-  --theme-markdown-strong: #ebcb8b;
-  --theme-markdown-horizontal-rule: #4c566a;
-  --theme-markdown-list-item: #5e81ac;
-  --theme-markdown-list-enumeration: #8fbcbb;
-  --theme-markdown-image: #81a1c1;
-  --theme-markdown-image-text: #8fbcbb;
-  --theme-markdown-code-block: #2e3440;
-  --theme-syntax-comment: #4c566a;
-  --theme-syntax-keyword: #81a1c1;
-  --theme-syntax-function: #88c0d0;
-  --theme-syntax-variable: #8fbcbb;
-  --theme-syntax-string: #a3be8c;
-  --theme-syntax-number: #b48ead;
-  --theme-syntax-type: #8fbcbb;
-  --theme-syntax-operator: #81a1c1;
-  --theme-syntax-punctuation: #2e3440;
+  --theme-primary: #5E81AC;
+  --theme-secondary: #81A1C1;
+  --theme-accent: #8FBCBB;
+  --theme-error: #BF616A;
+  --theme-warning: #D08770;
+  --theme-success: #A3BE8C;
+  --theme-info: #5E81AC;
+  --theme-text: #2E3440;
+  --theme-text-muted: #3B4252;
+  --theme-background: #ECEFF4;
+  --theme-background-panel: #E5E9F0;
+  --theme-background-element: #D8DEE9;
+  --theme-border: #4C566A;
+  --theme-border-active: #434C5E;
+  --theme-border-subtle: #4C566A;
+  --theme-diff-added: #A3BE8C;
+  --theme-diff-removed: #BF616A;
+  --theme-diff-context: #4C566A;
+  --theme-diff-hunk-header: #4C566A;
+  --theme-diff-highlight-added: #A3BE8C;
+  --theme-diff-highlight-removed: #BF616A;
+  --theme-diff-added-bg: #E5E9F0;
+  --theme-diff-removed-bg: #E5E9F0;
+  --theme-diff-context-bg: #E5E9F0;
+  --theme-diff-line-number: #D8DEE9;
+  --theme-diff-added-line-number-bg: #E5E9F0;
+  --theme-diff-removed-line-number-bg: #E5E9F0;
+  --theme-markdown-text: #2E3440;
+  --theme-markdown-heading: #5E81AC;
+  --theme-markdown-link: #81A1C1;
+  --theme-markdown-link-text: #8FBCBB;
+  --theme-markdown-code: #A3BE8C;
+  --theme-markdown-block-quote: #4C566A;
+  --theme-markdown-emph: #D08770;
+  --theme-markdown-strong: #EBCB8B;
+  --theme-markdown-horizontal-rule: #4C566A;
+  --theme-markdown-list-item: #5E81AC;
+  --theme-markdown-list-enumeration: #8FBCBB;
+  --theme-markdown-image: #81A1C1;
+  --theme-markdown-image-text: #8FBCBB;
+  --theme-markdown-code-block: #2E3440;
+  --theme-syntax-comment: #4C566A;
+  --theme-syntax-keyword: #81A1C1;
+  --theme-syntax-function: #88C0D0;
+  --theme-syntax-variable: #8FBCBB;
+  --theme-syntax-string: #A3BE8C;
+  --theme-syntax-number: #B48EAD;
+  --theme-syntax-type: #8FBCBB;
+  --theme-syntax-operator: #81A1C1;
+  --theme-syntax-punctuation: #2E3440;
 }
 
 [data-theme="nord"][data-dark="true"] {
-  --theme-primary: #88c0d0;
-  --theme-secondary: #81a1c1;
-  --theme-accent: #8fbcbb;
-  --theme-error: #bf616a;
-  --theme-warning: #d08770;
-  --theme-success: #a3be8c;
-  --theme-info: #88c0d0;
-  --theme-text: #eceff4;
-  --theme-text-muted: #8b95a7;
-  --theme-background: #2e3440;
-  --theme-background-panel: #3b4252;
-  --theme-background-element: #434c5e;
-  --theme-border: #434c5e;
-  --theme-border-active: #4c566a;
-  --theme-border-subtle: #434c5e;
-  --theme-diff-added: #a3be8c;
-  --theme-diff-removed: #bf616a;
-  --theme-diff-context: #8b95a7;
-  --theme-diff-hunk-header: #8b95a7;
-  --theme-diff-highlight-added: #a3be8c;
-  --theme-diff-highlight-removed: #bf616a;
-  --theme-diff-added-bg: #3b4252;
-  --theme-diff-removed-bg: #3b4252;
-  --theme-diff-context-bg: #3b4252;
-  --theme-diff-line-number: #434c5e;
-  --theme-diff-added-line-number-bg: #3b4252;
-  --theme-diff-removed-line-number-bg: #3b4252;
-  --theme-markdown-text: #d8dee9;
-  --theme-markdown-heading: #88c0d0;
-  --theme-markdown-link: #81a1c1;
-  --theme-markdown-link-text: #8fbcbb;
-  --theme-markdown-code: #a3be8c;
-  --theme-markdown-block-quote: #8b95a7;
-  --theme-markdown-emph: #d08770;
-  --theme-markdown-strong: #ebcb8b;
-  --theme-markdown-horizontal-rule: #8b95a7;
-  --theme-markdown-list-item: #88c0d0;
-  --theme-markdown-list-enumeration: #8fbcbb;
-  --theme-markdown-image: #81a1c1;
-  --theme-markdown-image-text: #8fbcbb;
-  --theme-markdown-code-block: #d8dee9;
-  --theme-syntax-comment: #8b95a7;
-  --theme-syntax-keyword: #81a1c1;
-  --theme-syntax-function: #88c0d0;
-  --theme-syntax-variable: #8fbcbb;
-  --theme-syntax-string: #a3be8c;
-  --theme-syntax-number: #b48ead;
-  --theme-syntax-type: #8fbcbb;
-  --theme-syntax-operator: #81a1c1;
-  --theme-syntax-punctuation: #d8dee9;
+  --theme-primary: #88C0D0;
+  --theme-secondary: #81A1C1;
+  --theme-accent: #8FBCBB;
+  --theme-error: #BF616A;
+  --theme-warning: #D08770;
+  --theme-success: #A3BE8C;
+  --theme-info: #88C0D0;
+  --theme-text: #ECEFF4;
+  --theme-text-muted: #8B95A7;
+  --theme-background: #2E3440;
+  --theme-background-panel: #3B4252;
+  --theme-background-element: #434C5E;
+  --theme-border: #434C5E;
+  --theme-border-active: #4C566A;
+  --theme-border-subtle: #434C5E;
+  --theme-diff-added: #A3BE8C;
+  --theme-diff-removed: #BF616A;
+  --theme-diff-context: #8B95A7;
+  --theme-diff-hunk-header: #8B95A7;
+  --theme-diff-highlight-added: #A3BE8C;
+  --theme-diff-highlight-removed: #BF616A;
+  --theme-diff-added-bg: #3B4252;
+  --theme-diff-removed-bg: #3B4252;
+  --theme-diff-context-bg: #3B4252;
+  --theme-diff-line-number: #434C5E;
+  --theme-diff-added-line-number-bg: #3B4252;
+  --theme-diff-removed-line-number-bg: #3B4252;
+  --theme-markdown-text: #D8DEE9;
+  --theme-markdown-heading: #88C0D0;
+  --theme-markdown-link: #81A1C1;
+  --theme-markdown-link-text: #8FBCBB;
+  --theme-markdown-code: #A3BE8C;
+  --theme-markdown-block-quote: #8B95A7;
+  --theme-markdown-emph: #D08770;
+  --theme-markdown-strong: #EBCB8B;
+  --theme-markdown-horizontal-rule: #8B95A7;
+  --theme-markdown-list-item: #88C0D0;
+  --theme-markdown-list-enumeration: #8FBCBB;
+  --theme-markdown-image: #81A1C1;
+  --theme-markdown-image-text: #8FBCBB;
+  --theme-markdown-code-block: #D8DEE9;
+  --theme-syntax-comment: #8B95A7;
+  --theme-syntax-keyword: #81A1C1;
+  --theme-syntax-function: #88C0D0;
+  --theme-syntax-variable: #8FBCBB;
+  --theme-syntax-string: #A3BE8C;
+  --theme-syntax-number: #B48EAD;
+  --theme-syntax-type: #8FBCBB;
+  --theme-syntax-operator: #81A1C1;
+  --theme-syntax-punctuation: #D8DEE9;
 }
 
 [data-theme="one-dark"][data-dark="false"] {
@@ -2173,109 +2173,109 @@
 }
 
 [data-theme="vesper"][data-dark="false"] {
-  --theme-primary: #ffc799;
-  --theme-secondary: #99ffe4;
-  --theme-accent: #ffc799;
-  --theme-error: #ff8080;
-  --theme-warning: #ffc799;
-  --theme-success: #99ffe4;
-  --theme-info: #ffc799;
+  --theme-primary: #FFC799;
+  --theme-secondary: #99FFE4;
+  --theme-accent: #FFC799;
+  --theme-error: #FF8080;
+  --theme-warning: #FFC799;
+  --theme-success: #99FFE4;
+  --theme-info: #FFC799;
   --theme-text: #101010;
-  --theme-text-muted: #a0a0a0;
-  --theme-background: #fff;
-  --theme-background-panel: #f0f0f0;
-  --theme-background-element: #e0e0e0;
-  --theme-border: #d0d0d0;
-  --theme-border-active: #ffc799;
-  --theme-border-subtle: #e8e8e8;
-  --theme-diff-added: #99ffe4;
-  --theme-diff-removed: #ff8080;
-  --theme-diff-context: #a0a0a0;
-  --theme-diff-hunk-header: #a0a0a0;
-  --theme-diff-highlight-added: #99ffe4;
-  --theme-diff-highlight-removed: #ff8080;
+  --theme-text-muted: #A0A0A0;
+  --theme-background: #FFF;
+  --theme-background-panel: #F0F0F0;
+  --theme-background-element: #E0E0E0;
+  --theme-border: #D0D0D0;
+  --theme-border-active: #FFC799;
+  --theme-border-subtle: #E8E8E8;
+  --theme-diff-added: #99FFE4;
+  --theme-diff-removed: #FF8080;
+  --theme-diff-context: #A0A0A0;
+  --theme-diff-hunk-header: #A0A0A0;
+  --theme-diff-highlight-added: #99FFE4;
+  --theme-diff-highlight-removed: #FF8080;
   --theme-diff-added-bg: #e8f5e8;
   --theme-diff-removed-bg: #f5e8e8;
-  --theme-diff-context-bg: #f8f8f8;
+  --theme-diff-context-bg: #F8F8F8;
   --theme-diff-line-number: #808080;
   --theme-diff-added-line-number-bg: #e8f5e8;
   --theme-diff-removed-line-number-bg: #f5e8e8;
   --theme-markdown-text: #101010;
-  --theme-markdown-heading: #ffc799;
-  --theme-markdown-link: #ffc799;
-  --theme-markdown-link-text: #a0a0a0;
-  --theme-markdown-code: #a0a0a0;
+  --theme-markdown-heading: #FFC799;
+  --theme-markdown-link: #FFC799;
+  --theme-markdown-link-text: #A0A0A0;
+  --theme-markdown-code: #A0A0A0;
   --theme-markdown-block-quote: #101010;
   --theme-markdown-emph: #101010;
   --theme-markdown-strong: #101010;
-  --theme-markdown-horizontal-rule: #65737e;
+  --theme-markdown-horizontal-rule: #65737E;
   --theme-markdown-list-item: #101010;
   --theme-markdown-list-enumeration: #101010;
-  --theme-markdown-image: #ffc799;
-  --theme-markdown-image-text: #a0a0a0;
+  --theme-markdown-image: #FFC799;
+  --theme-markdown-image-text: #A0A0A0;
   --theme-markdown-code-block: #101010;
   --theme-syntax-comment: #8b8b8b94;
-  --theme-syntax-keyword: #a0a0a0;
-  --theme-syntax-function: #ffc799;
+  --theme-syntax-keyword: #A0A0A0;
+  --theme-syntax-function: #FFC799;
   --theme-syntax-variable: #101010;
-  --theme-syntax-string: #99ffe4;
-  --theme-syntax-number: #ffc799;
-  --theme-syntax-type: #ffc799;
-  --theme-syntax-operator: #a0a0a0;
+  --theme-syntax-string: #99FFE4;
+  --theme-syntax-number: #FFC799;
+  --theme-syntax-type: #FFC799;
+  --theme-syntax-operator: #A0A0A0;
   --theme-syntax-punctuation: #101010;
 }
 
 [data-theme="vesper"][data-dark="true"] {
-  --theme-primary: #ffc799;
-  --theme-secondary: #99ffe4;
-  --theme-accent: #ffc799;
-  --theme-error: #ff8080;
-  --theme-warning: #ffc799;
-  --theme-success: #99ffe4;
-  --theme-info: #ffc799;
-  --theme-text: #fff;
-  --theme-text-muted: #a0a0a0;
+  --theme-primary: #FFC799;
+  --theme-secondary: #99FFE4;
+  --theme-accent: #FFC799;
+  --theme-error: #FF8080;
+  --theme-warning: #FFC799;
+  --theme-success: #99FFE4;
+  --theme-info: #FFC799;
+  --theme-text: #FFF;
+  --theme-text-muted: #A0A0A0;
   --theme-background: #101010;
   --theme-background-panel: #101010;
   --theme-background-element: #101010;
   --theme-border: #282828;
-  --theme-border-active: #ffc799;
-  --theme-border-subtle: #1c1c1c;
-  --theme-diff-added: #99ffe4;
-  --theme-diff-removed: #ff8080;
-  --theme-diff-context: #a0a0a0;
-  --theme-diff-hunk-header: #a0a0a0;
-  --theme-diff-highlight-added: #99ffe4;
-  --theme-diff-highlight-removed: #ff8080;
+  --theme-border-active: #FFC799;
+  --theme-border-subtle: #1C1C1C;
+  --theme-diff-added: #99FFE4;
+  --theme-diff-removed: #FF8080;
+  --theme-diff-context: #A0A0A0;
+  --theme-diff-hunk-header: #A0A0A0;
+  --theme-diff-highlight-added: #99FFE4;
+  --theme-diff-highlight-removed: #FF8080;
   --theme-diff-added-bg: #0d2818;
   --theme-diff-removed-bg: #281a1a;
   --theme-diff-context-bg: #101010;
   --theme-diff-line-number: #505050;
   --theme-diff-added-line-number-bg: #0d2818;
   --theme-diff-removed-line-number-bg: #281a1a;
-  --theme-markdown-text: #fff;
-  --theme-markdown-heading: #ffc799;
-  --theme-markdown-link: #ffc799;
-  --theme-markdown-link-text: #a0a0a0;
-  --theme-markdown-code: #a0a0a0;
-  --theme-markdown-block-quote: #fff;
-  --theme-markdown-emph: #fff;
-  --theme-markdown-strong: #fff;
-  --theme-markdown-horizontal-rule: #65737e;
-  --theme-markdown-list-item: #fff;
-  --theme-markdown-list-enumeration: #fff;
-  --theme-markdown-image: #ffc799;
-  --theme-markdown-image-text: #a0a0a0;
-  --theme-markdown-code-block: #fff;
+  --theme-markdown-text: #FFF;
+  --theme-markdown-heading: #FFC799;
+  --theme-markdown-link: #FFC799;
+  --theme-markdown-link-text: #A0A0A0;
+  --theme-markdown-code: #A0A0A0;
+  --theme-markdown-block-quote: #FFF;
+  --theme-markdown-emph: #FFF;
+  --theme-markdown-strong: #FFF;
+  --theme-markdown-horizontal-rule: #65737E;
+  --theme-markdown-list-item: #FFF;
+  --theme-markdown-list-enumeration: #FFF;
+  --theme-markdown-image: #FFC799;
+  --theme-markdown-image-text: #A0A0A0;
+  --theme-markdown-code-block: #FFF;
   --theme-syntax-comment: #8b8b8b94;
-  --theme-syntax-keyword: #a0a0a0;
-  --theme-syntax-function: #ffc799;
-  --theme-syntax-variable: #fff;
-  --theme-syntax-string: #99ffe4;
-  --theme-syntax-number: #ffc799;
-  --theme-syntax-type: #ffc799;
-  --theme-syntax-operator: #a0a0a0;
-  --theme-syntax-punctuation: #fff;
+  --theme-syntax-keyword: #A0A0A0;
+  --theme-syntax-function: #FFC799;
+  --theme-syntax-variable: #FFF;
+  --theme-syntax-string: #99FFE4;
+  --theme-syntax-number: #FFC799;
+  --theme-syntax-type: #FFC799;
+  --theme-syntax-operator: #A0A0A0;
+  --theme-syntax-punctuation: #FFF;
 }
 
 [data-theme="zenburn"][data-dark="false"] {
@@ -2383,3 +2383,4 @@
   --theme-syntax-operator: #f0dfaf;
   --theme-syntax-punctuation: #dcdccc;
 }
+

+ 184 - 0
packages/app/src/components/select.tsx

@@ -0,0 +1,184 @@
+import { Select as KobalteSelect } from "@kobalte/core/select"
+import { createEffect, createMemo } from "solid-js"
+import type { ComponentProps } from "solid-js"
+import { Icon } from "@/ui/icon"
+import fuzzysort from "fuzzysort"
+import { pipe, groupBy, entries, map } from "remeda"
+import { createStore } from "solid-js/store"
+
+export interface SelectProps<T> {
+  variant?: "default" | "outline"
+  size?: "sm" | "md" | "lg"
+  placeholder?: string
+  options: T[]
+  current?: T
+  value?: (x: T) => string
+  label?: (x: T) => string
+  groupBy?: (x: T) => string
+  filterKeys: string[]
+  onFilter?: (query: string) => void
+  onSelect?: (value: T | undefined) => void
+  class?: ComponentProps<"div">["class"]
+  classList?: ComponentProps<"div">["classList"]
+}
+
+export function Select<T>(props: SelectProps<T>) {
+  let inputRef: HTMLInputElement | undefined = undefined
+  let listboxRef: HTMLUListElement | undefined = undefined
+  let contentRef: HTMLDivElement | undefined = undefined
+  const [store, setStore] = createStore({
+    filter: "",
+  })
+  const grouped = createMemo(() => {
+    const needle = store.filter.toLowerCase()
+    const result = pipe(
+      props.options,
+      (x) => (!needle ? x : fuzzysort.go(needle, x, { keys: props.filterKeys }).map((x) => x.obj)),
+      groupBy((x) => (props.groupBy ? props.groupBy(x) : "")),
+      // mapValues((x) => x.sort((a, b) => a.title.localeCompare(b.title))),
+      entries(),
+      map(([k, v]) => ({ category: k, options: v })),
+    )
+    return result
+  })
+  // const flat = createMemo(() => {
+  //   return pipe(
+  //     grouped(),
+  //     flatMap(({ options }) => options),
+  //   )
+  // })
+
+  createEffect(() => {
+    store.filter
+    listboxRef?.scrollTo(0, 0)
+    // setStore("selected", 0)
+    // scroll.scrollTo(0)
+  })
+
+  return (
+    <KobalteSelect<T, { category: string; options: T[] }>
+      allowDuplicateSelectionEvents={false}
+      disallowEmptySelection={true}
+      closeOnSelection={false}
+      value={props.current}
+      options={grouped()}
+      optionValue={(x) => (props.value ? props.value(x) : (x as string))}
+      optionTextValue={(x) => (props.label ? props.label(x) : (x as string))}
+      optionGroupChildren="options"
+      placeholder={props.placeholder}
+      sectionComponent={(props) => (
+        <KobalteSelect.Section class="text-xs uppercase text-text-muted/60 font-light mt-3 first:mt-0 ml-2">
+          {props.section.rawValue.category}
+        </KobalteSelect.Section>
+      )}
+      itemComponent={(itemProps) => (
+        <KobalteSelect.Item
+          classList={{
+            "relative flex cursor-pointer select-none items-center": true,
+            "rounded-sm px-2 py-0.5 text-xs outline-none text-text": true,
+            "transition-colors data-[disabled]:pointer-events-none": true,
+            "data-[highlighted]:bg-background-element data-[disabled]:opacity-50": true,
+            [props.class ?? ""]: !!props.class,
+          }}
+          {...itemProps}
+        >
+          <KobalteSelect.ItemLabel>
+            {props.label ? props.label(itemProps.item.rawValue) : (itemProps.item.rawValue as string)}
+          </KobalteSelect.ItemLabel>
+          <KobalteSelect.ItemIndicator
+            classList={{
+              "ml-auto": true,
+            }}
+          >
+            <Icon name="checkmark" size={16} />
+          </KobalteSelect.ItemIndicator>
+        </KobalteSelect.Item>
+      )}
+      onChange={(v) => {
+        if (props.onSelect) props.onSelect(v ?? undefined)
+        if (v !== null) {
+          // close the select
+        }
+      }}
+      onOpenChange={(v) => v || setStore("filter", "")}
+    >
+      <KobalteSelect.Trigger
+        classList={{
+          ...(props.classList ?? {}),
+          "flex w-full items-center justify-between rounded-md transition-colors": true,
+          "focus-visible:outline-none focus-visible:ring focus-visible:ring-border-active/30": true,
+          "disabled:cursor-not-allowed disabled:opacity-50": true,
+          "data-[placeholder-shown]:text-text-muted cursor-pointer": true,
+          "hover:bg-background-element focus-visible:ring-border-active": true,
+          "bg-background-element text-text": props.variant === "default" || !props.variant,
+          "border-2 border-border bg-transparent text-text": props.variant === "outline",
+          "h-6 pl-2 text-xs": props.size === "sm",
+          "h-8 pl-3 text-sm": props.size === "md" || !props.size,
+          "h-10 pl-4 text-base": props.size === "lg",
+          [props.class ?? ""]: !!props.class,
+        }}
+      >
+        <KobalteSelect.Value<T>>
+          {(state) => {
+            const selected = state.selectedOption() ?? props.current
+            if (!selected) return props.placeholder || ""
+            if (props.label) return props.label(selected)
+            return selected as string
+          }}
+        </KobalteSelect.Value>
+        <KobalteSelect.Icon
+          classList={{
+            "size-fit shrink-0 text-text-muted transition-transform duration-100 data-[expanded]:rotate-180": true,
+          }}
+        >
+          <Icon name="chevron-down" size={24} />
+        </KobalteSelect.Icon>
+      </KobalteSelect.Trigger>
+      <KobalteSelect.Portal>
+        <KobalteSelect.Content
+          ref={(el) => (contentRef = el)}
+          onKeyDown={(e) => {
+            if (e.key === "ArrowUp" || e.key === "ArrowDown" || e.key === "Escape") {
+              return
+            }
+            inputRef?.focus()
+          }}
+          classList={{
+            "min-w-32 overflow-hidden rounded-md border border-border-subtle/40": true,
+            "bg-background-panel p-1 shadow-md z-50": true,
+            "data-[closed]:animate-out data-[closed]:fade-out-0 data-[closed]:zoom-out-95": true,
+            "data-[expanded]:animate-in data-[expanded]:fade-in-0 data-[expanded]:zoom-in-95": true,
+          }}
+        >
+          <form>
+            <input
+              ref={(el) => (inputRef = el)}
+              id="select-filter"
+              type="text"
+              placeholder="Filter models"
+              value={store.filter}
+              onInput={(e) => setStore("filter", e.currentTarget.value)}
+              onKeyDown={(e) => {
+                if (e.key === "ArrowUp" || e.key === "ArrowDown" || e.key === "Escape") {
+                  e.preventDefault()
+                  e.stopPropagation()
+                  listboxRef?.focus()
+                }
+              }}
+              classList={{
+                "w-full": true,
+                "px-2 pb-2 text-text font-light placeholder-text-muted/70 text-xs focus:outline-none": true,
+              }}
+            />
+          </form>
+          <KobalteSelect.Listbox
+            ref={(el) => (listboxRef = el)}
+            classList={{
+              "overflow-y-auto max-h-48 no-scrollbar": true,
+            }}
+          />
+        </KobalteSelect.Content>
+      </KobalteSelect.Portal>
+    </KobalteSelect>
+  )
+}

+ 19 - 10
packages/app/src/context/local.tsx

@@ -24,22 +24,26 @@ function init() {
   const sdk = useSDK()
   const sync = useSync()
 
-  const agents = createMemo(() => sync.data.agent.filter((x) => x.mode !== "subagent"))
+  const list = createMemo(() => sync.data.agent.filter((x) => x.mode !== "subagent"))
   const agent = (() => {
     const [store, setStore] = createStore<{
       current: string
     }>({
-      current: agents()[0].name,
+      current: list()[0].name,
     })
     return {
+      list,
       current() {
-        return agents().find((x) => x.name === store.current)!
+        return list().find((x) => x.name === store.current)!
+      },
+      set(name: string | undefined) {
+        setStore("current", name ?? list()[0].name)
       },
       move(direction: 1 | -1) {
-        let next = agents().findIndex((x) => x.name === store.current) + direction
-        if (next < 0) next = agents().length - 1
-        if (next >= agents().length) next = 0
-        const value = agents()[next]
+        let next = list().findIndex((x) => x.name === store.current) + direction
+        if (next < 0) next = list().length - 1
+        if (next >= list().length) next = 0
+        const value = list()[next]
         setStore("current", value.name)
         if (value.model)
           model.set({
@@ -89,7 +93,12 @@ function init() {
       return store.model[agent.current().name] ?? (a.model ? a.model : fallback())
     })
 
+    const list = createMemo(() =>
+      sync.data.provider.flatMap((x) => Object.values(x.models).map((m) => ({ providerID: x.id, modelID: m.id }))),
+    )
+
     return {
+      list,
       current,
       recent() {
         return store.recent
@@ -103,10 +112,10 @@ function init() {
           model: model.name ?? value.modelID,
         }
       }),
-      set(model: { providerID: string; modelID: string }, options?: { recent?: boolean }) {
+      set(model: { providerID: string; modelID: string } | undefined, options?: { recent?: boolean }) {
         batch(() => {
-          setStore("model", agent.current().name, model)
-          if (options?.recent) {
+          setStore("model", agent.current().name, model ?? fallback())
+          if (options?.recent && model) {
             const uniq = uniqueBy([model, ...store.recent], (x) => x.providerID + x.modelID)
             if (uniq.length > 5) uniq.pop()
             setStore("recent", uniq)

+ 30 - 8
packages/app/src/pages/index.tsx

@@ -1,5 +1,6 @@
 import { FileIcon, Icon, IconButton, Logo, Tooltip } from "@/ui"
 import { Tabs } from "@/ui/tabs"
+import { Select } from "@/components/select"
 import FileTree from "@/components/file-tree"
 import { createSignal, For, Match, onCleanup, onMount, Show, Switch } from "solid-js"
 import { useLocal, useSDK } from "@/context"
@@ -51,6 +52,10 @@ export default function Page() {
       return
     }
 
+    if (document.activeElement?.id === "select-filter") {
+      return
+    }
+
     if (local.file.active()) {
       if (e.getModifierState(MOD)) {
         if (e.key.toLowerCase() === "a") {
@@ -228,10 +233,10 @@ export default function Page() {
         <Tabs class="relative flex flex-col h-full" defaultValue="files">
           <div class="sticky top-0 shrink-0 flex">
             <Tabs.List class="grow w-full after:hidden">
-              <Tabs.Trigger value="files" class="flex-1 justify-center">
+              <Tabs.Trigger value="files" class="flex-1 justify-center text-xs">
                 Files
               </Tabs.Trigger>
-              <Tabs.Trigger value="changes" class="flex-1 justify-center">
+              <Tabs.Trigger value="changes" class="flex-1 justify-center text-xs">
                 Changes
               </Tabs.Trigger>
             </Tabs.List>
@@ -256,7 +261,7 @@ export default function Page() {
             />
           </Tabs.Content>
           <Tabs.Content value="changes" class="grow min-h-0 py-2 bg-background">
-            <div class="px-2 text-sm text-text-muted">No changes yet</div>
+            <div class="px-2 text-xs text-text-muted">No changes yet</div>
           </Tabs.Content>
         </Tabs>
       </div>
@@ -493,11 +498,28 @@ export default function Page() {
               placeholder="It all starts with a prompt..."
               class="w-full p-1 pb-4 text-text font-light placeholder-text-muted/70 text-sm focus:outline-none"
             />
-            <div class="px-1 flex justify-between items-center text-xs text-text-muted">
-              <span>
-                <span class="text-primary uppercase">{local.agent.current()?.name ?? "unknown"}</span> /{" "}
-                {local.model.parsed().provider} / {local.model.parsed().model}
-              </span>
+            <div class="flex justify-between items-center text-xs text-text-muted">
+              <div class="flex gap-2 items-center">
+                <Select
+                  options={local.agent.list().map((a) => a.name)}
+                  current={local.agent.current().name}
+                  onSelect={local.agent.set}
+                  size="sm"
+                  class="uppercase"
+                />
+                <Select
+                  options={local.model.list()}
+                  current={local.model.current()}
+                  onSelect={local.model.set}
+                  label={(x) => x.modelID}
+                  value={(x) => `${x.providerID}.${x.modelID}`}
+                  filterKeys={["providerID", "modelID"]}
+                  groupBy={(x) => x.providerID}
+                  size="sm"
+                  class="uppercase"
+                />
+                <span class="text-text-muted/70">{local.model.parsed().provider}</span>
+              </div>
               <div class="flex gap-1 items-center">
                 <IconButton class="text-text-muted" size="xs" variant="ghost">
                   <Icon name="photo" size={16} />

+ 2 - 2
packages/web/package.json

@@ -23,11 +23,11 @@
     "diff": "8.0.2",
     "js-base64": "3.7.7",
     "lang-map": "0.4.0",
-    "luxon": "3.6.1",
+    "luxon": "catalog:",
     "marked": "15.0.12",
     "marked-shiki": "1.2.1",
     "rehype-autolink-headings": "7.1.0",
-    "remeda": "2.26.0",
+    "remeda": "catalog:",
     "sharp": "0.32.5",
     "shiki": "3.4.2",
     "solid-js": "catalog:",