@allbluecn/web-app 0.4.9 → 0.4.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@allbluecn/web-app",
3
- "version": "0.4.9",
3
+ "version": "0.4.11",
4
4
  "license": "AGPL-3.0-or-later",
5
5
  "type": "module",
6
6
  "files": [
@@ -24,12 +24,16 @@
24
24
  "@base-ui/react": "^1.4.1",
25
25
  "@dnd-kit/react": "^0.5.0",
26
26
  "@floating-ui/react": "^0.27.19",
27
+ "@fontsource-variable/geist": "^5.3.0",
28
+ "@fontsource-variable/geist-mono": "^5.3.0",
27
29
  "@iconify/react": "^6.0.2",
28
30
  "@lobehub/icons": "^5.10.0",
29
31
  "@prisma/client": "^7.8.0",
32
+ "@radix-ui/react-dialog": "^1.1.23",
30
33
  "@radix-ui/react-dropdown-menu": "^2.1.16",
31
34
  "@radix-ui/react-popover": "^1.1.15",
32
35
  "@radix-ui/react-slot": "^1.2.4",
36
+ "@radix-ui/react-visually-hidden": "^1.2.11",
33
37
  "@tanstack/ai": "^0.43.0",
34
38
  "@tanstack/ai-anthropic": "^0.16.3",
35
39
  "@tanstack/ai-gemini": "^0.20.1",
@@ -131,14 +135,14 @@
131
135
  "three": "^0.180.0",
132
136
  "tw-animate-css": "^1.4.0",
133
137
  "zod": "^4.4.3",
134
- "@allbluecn/database": "0.4.9",
138
+ "@allbluecn/ai-gateway": "0.4.11",
139
+ "@allbluecn/database": "0.4.11",
135
140
  "@allbluecn/kernel": "0.3.0",
136
- "@allbluecn/ai-gateway": "0.4.9",
137
- "@allbluecn/plugins-core": "0.4.9",
141
+ "@allbluecn/plugins-core": "0.4.11",
138
142
  "@allbluecn/prototype": "0.1.1",
139
- "@allbluecn/tiptap": "0.4.9",
140
- "@allbluecn/shared": "0.4.9",
141
- "@allbluecn/ui": "0.4.9"
143
+ "@allbluecn/tiptap": "0.4.11",
144
+ "@allbluecn/shared": "0.4.11",
145
+ "@allbluecn/ui": "0.4.11"
142
146
  },
143
147
  "devDependencies": {
144
148
  "@babel/parser": "^8.0.4",
@@ -0,0 +1,23 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 321.33 50">
3
+ <defs>
4
+ <style>
5
+ .cls-1 {
6
+ fill-rule: evenodd;
7
+ }
8
+
9
+ .cls-1, .cls-2 {
10
+ fill: #121212;
11
+ }
12
+ </style>
13
+ </defs>
14
+ <path class="cls-1" d="M19.03,0c13.76,0,19.03,8.54,19.03,25s-5.27,25-19.03,25S0,41.46,0,25C0,8.54,5.27,0,19.03,0ZM25.32,36.42c-1.79,0-2.76.49-3.55.88-.68.34-1.21.61-2.38.61-1.17,0-1.71-.27-2.38-.61-.78-.39-1.76-.88-3.55-.88-1.79,0-2.76.49-3.55.88-.68.34-1.21.61-2.38.61v2.6c1.79,0,2.76-.49,3.55-.88.68-.34,1.21-.61,2.38-.61s1.71.27,2.38.61c.78.39,1.76.88,3.55.88s2.76-.49,3.55-.88c.68-.34,1.21-.61,2.38-.61s1.71.27,2.38.61c.78.39,1.76.88,3.55.88v-2.6c-1.17,0-1.71-.27-2.38-.61-.78-.39-1.76-.88-3.55-.88ZM26.17,10.31c1.35-1.22,0-3.43-1.69-2.76-.93.37-1.81.91-2.59,1.62-1.23,1.11-2.05,2.52-2.5,4.04-.44-1.53-1.27-2.93-2.5-4.04-.78-.7-1.66-1.25-2.6-1.62-1.69-.67-3.04,1.54-1.69,2.76l4.95,4.49c-1.62-1.1-3.56-1.75-5.67-1.75-1.15,0-2.25.19-3.28.54-1.73.59-1.25,3.19.57,3.19h6.91c-1.52.29-2.97.97-4.18,2.06-.78.7-1.4,1.53-1.87,2.43-.84,1.61,1.22,3.18,2.57,1.97l5.57-5.04-.42,13.87c0,.17-.1.33-.22.46-.18-.08-.35-.16-.53-.26-.78-.39-1.76-.88-3.55-.88s-2.76.49-3.55.88c-.68.34-1.21.61-2.38.61v2.6c1.79,0,2.76-.49,3.55-.88.68-.34,1.21-.61,2.38-.61s1.71.27,2.38.61c.78.39,1.76.88,3.55.88s2.76-.49,3.55-.88c.68-.34,1.21-.61,2.38-.61s1.71.27,2.38.61c.78.39,1.76.88,3.55.88v-2.6c-1.17,0-1.71-.27-2.38-.61h0c-.78-.4-1.76-.89-3.55-.89s-2.76.49-3.55.88c-.2.1-.39.2-.59.28-.15-.13-.24-.3-.25-.48l-.42-13.94,5.65,5.12c1.35,1.22,3.4-.35,2.57-1.97-.46-.89-1.08-1.72-1.87-2.43-1.21-1.1-2.65-1.77-4.18-2.06h6.91c1.83,0,2.3-2.6.57-3.19-1.03-.35-2.13-.54-3.28-.54-2.1,0-4.04.65-5.67,1.75l4.95-4.49Z"/>
15
+ <path class="cls-2" d="M151.71,43.25h-12.45c-.67,0-1.28-.42-1.53-1.04l-2.55-6.55c-.65-1.7-3.15-1.22-3.17.58-.02,1.8-.02,3.53-.02,5.37,0,.9-.74,1.64-1.64,1.64h-10.47c-.92,0-1.64-.72-1.64-1.64V8.34c0-.9.72-1.64,1.64-1.64h12.43c.69-.02,1.28.41,1.53,1.04l2.61,6.84c.65,1.7,3.15,1.25,3.17-.57l.07-5.69c0-.9.74-1.63,1.64-1.63h10.4c.9,0,1.64.74,1.64,1.66v33.26c0,.92-.74,1.64-1.64,1.64l-.02-.02Z"/>
16
+ <path class="cls-2" d="M98.25,37.35l-3.85-.04c-.81,0-1.5.58-1.64,1.38l-.53,3.18c-.12.79-.81,1.38-1.62,1.38h-11.07c-1.07,0-1.85-.99-1.6-2.03l8.38-33.28c.18-.72.85-1.24,1.6-1.24h16.76c.76.02,1.41.53,1.58,1.27l8.4,33.25c.26,1.04-.51,2.05-1.58,2.05l-11.07.04c-.81,0-1.5-.58-1.62-1.38l-.53-3.16c-.12-.79-.81-1.38-1.6-1.38v-.04ZM97.94,27.4l-.85-4.19c-.18-.88-1.44-.88-1.62,0l-.76,4.2c-.19,1.02.62,1.98,1.65,1.94,1.02-.04,1.76-.97,1.57-1.98v.02Z"/>
17
+ <path class="cls-2" d="M69.77,43.23l-10.49.04c-.92,0-1.65-.74-1.65-1.64v-21.76c0-.9-.72-1.64-1.64-1.64h-5.39c-.9,0-1.64-.76-1.64-1.66v-8.2c0-.9.74-1.64,1.65-1.64h27.8c.9,0,1.64.74,1.64,1.64v8.2c0,.9-.72,1.64-1.64,1.64h-5.39c-.9,0-1.64.74-1.64,1.66v21.75c0,.9-.74,1.64-1.64,1.64l.02-.02Z"/>
18
+ <path class="cls-2" d="M311.9,24.92l9.21,15.85c.63,1.1-.14,2.47-1.41,2.47l-10.72.05c-.6,0-1.14-.32-1.44-.83l-4.07-7.21c-.6-1.08-2.24-.65-2.25.58l-.1,5.81c0,.9-.74,1.63-1.64,1.63h-10.47c-.92,0-1.64-.72-1.64-1.64V8.37c0-.9.72-1.64,1.64-1.64h10.49c.9,0,1.62.71,1.64,1.59l.11,4.26c.04,1.27,1.74,1.64,2.29.49l2.59-5.39c.28-.57.85-.94,1.48-.94h10.88c1.27-.02,2.06,1.34,1.43,2.44l-7.97,14.1c-.28.51-.28,1.13,0,1.64h-.02Z"/>
19
+ <path class="cls-2" d="M238.4,37.38l-3.85-.04c-.81,0-1.5.58-1.64,1.38l-.53,3.18c-.12.79-.81,1.38-1.62,1.38h-11.09c-1.07,0-1.85-.99-1.6-2.03l8.38-33.26c.18-.72.85-1.24,1.58-1.24h16.78c.76-.02,1.41.49,1.6,1.22l8.4,33.26c.26,1.04-.51,2.05-1.58,2.05h-11.07c-.81.04-1.5-.55-1.62-1.34l-.53-3.16c-.12-.79-.81-1.38-1.6-1.38v-.02ZM238.07,27.43l-.74-3.67c-.19-.99-1.62-.99-1.81.02l-.67,3.69c-.19,1.02.62,1.98,1.65,1.94,1.02-.04,1.76-.97,1.57-1.98Z"/>
20
+ <path class="cls-2" d="M189.42,33.35c-.99,11.71-17.78,12.35-29.17,8.2-.67-.25-1.11-.9-1.11-1.61v-9.57c0-1.31,1.37-2.14,2.52-1.5,3.68,2.01,7.39,3.44,11.6,4.29,0,0,1.39.11,1.9-.79.51-.9-.46-1.25-.88-1.61-8.01-3.48-17.32-5.53-16.49-14.59.97-10.48,16.83-11.75,27.62-8.39.7.21,1.18.9,1.18,1.64v9.38c0,1.24-1.27,2.07-2.41,1.57-5.16-2.28-12.16-4.29-11.81-2.95.9,3.39,18.06,3.76,17.04,15.9l.02.04Z"/>
21
+ <path class="cls-2" d="M271.32,29.38c2.8,2.33,6.25,2.44,9.22.94,1.41-.71,3.06.25,3.06,1.82l.05,8.37c0,.87-.51,1.68-1.34,1.96-8.08,2.86-17.55,1.24-23.25-5.81-5.7-7.05-5.42-16.91.23-23.62,5.65-6.71,15.03-8.27,23.01-5.55.83.28,1.36,1.1,1.36,1.98l-.05,8.37c0,1.57-1.65,2.53-3.06,1.84-2.94-1.47-6.28-1.47-9.12.79-2.48,1.98-2.75,6.69-.11,8.9Z"/>
22
+ <path class="cls-2" d="M210.7,43.24l-10.49.04c-.92,0-1.65-.74-1.65-1.64v-21.76c0-.9-.72-1.64-1.64-1.64h-5.39c-.9,0-1.64-.76-1.64-1.66v-8.2c0-.9.74-1.64,1.65-1.64h27.79c.9,0,1.64.74,1.64,1.64v8.2c0,.9-.72,1.64-1.64,1.64h-5.39c-.9,0-1.64.74-1.64,1.66v21.75c0,.9-.72,1.64-1.64,1.64l.02-.02Z"/>
23
+ </svg>
@@ -0,0 +1,23 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 321.33 50">
3
+ <defs>
4
+ <style>
5
+ .cls-1 {
6
+ fill-rule: evenodd;
7
+ }
8
+
9
+ .cls-1, .cls-2 {
10
+ fill: #fff;
11
+ }
12
+ </style>
13
+ </defs>
14
+ <path class="cls-1" d="M19.03,0c13.76,0,19.03,8.54,19.03,25s-5.27,25-19.03,25S0,41.46,0,25C0,8.54,5.27,0,19.03,0ZM25.32,36.42c-1.79,0-2.76.49-3.55.88-.68.34-1.21.61-2.38.61-1.17,0-1.71-.27-2.38-.61-.78-.39-1.76-.88-3.55-.88-1.79,0-2.76.49-3.55.88-.68.34-1.21.61-2.38.61v2.6c1.79,0,2.76-.49,3.55-.88.68-.34,1.21-.61,2.38-.61s1.71.27,2.38.61c.78.39,1.76.88,3.55.88s2.76-.49,3.55-.88c.68-.34,1.21-.61,2.38-.61s1.71.27,2.38.61c.78.39,1.76.88,3.55.88v-2.6c-1.17,0-1.71-.27-2.38-.61-.78-.39-1.76-.88-3.55-.88ZM26.17,10.31c1.35-1.22,0-3.43-1.69-2.76-.93.37-1.81.91-2.59,1.62-1.23,1.11-2.05,2.52-2.5,4.04-.44-1.53-1.27-2.93-2.5-4.04-.78-.7-1.66-1.25-2.6-1.62-1.69-.67-3.04,1.54-1.69,2.76l4.95,4.49c-1.62-1.1-3.56-1.75-5.67-1.75-1.15,0-2.25.19-3.28.54-1.73.59-1.25,3.19.57,3.19h6.91c-1.52.29-2.97.97-4.18,2.06-.78.7-1.4,1.53-1.87,2.43-.84,1.61,1.22,3.18,2.57,1.97l5.57-5.04-.42,13.87c0,.17-.1.33-.22.46-.18-.08-.35-.16-.53-.26-.78-.39-1.76-.88-3.55-.88s-2.76.49-3.55.88c-.68.34-1.21.61-2.38.61v2.6c1.79,0,2.76-.49,3.55-.88.68-.34,1.21-.61,2.38-.61s1.71.27,2.38.61c.78.39,1.76.88,3.55.88s2.76-.49,3.55-.88c.68-.34,1.21-.61,2.38-.61s1.71.27,2.38.61c.78.39,1.76.88,3.55.88v-2.6c-1.17,0-1.71-.27-2.38-.61h0c-.78-.4-1.76-.89-3.55-.89s-2.76.49-3.55.88c-.2.1-.39.2-.59.28-.15-.13-.24-.3-.25-.48l-.42-13.94,5.65,5.12c1.35,1.22,3.4-.35,2.57-1.97-.46-.89-1.08-1.72-1.87-2.43-1.21-1.1-2.65-1.77-4.18-2.06h6.91c1.83,0,2.3-2.6.57-3.19-1.03-.35-2.13-.54-3.28-.54-2.1,0-4.04.65-5.67,1.75l4.95-4.49Z"/>
15
+ <path class="cls-2" d="M151.71,43.25h-12.45c-.67,0-1.28-.42-1.53-1.04l-2.55-6.55c-.65-1.7-3.15-1.22-3.17.58-.02,1.8-.02,3.53-.02,5.37,0,.9-.74,1.64-1.64,1.64h-10.47c-.92,0-1.64-.72-1.64-1.64V8.34c0-.9.72-1.64,1.64-1.64h12.43c.69-.02,1.28.41,1.53,1.04l2.61,6.84c.65,1.7,3.15,1.25,3.17-.57l.07-5.69c0-.9.74-1.63,1.64-1.63h10.4c.9,0,1.64.74,1.64,1.66v33.26c0,.92-.74,1.64-1.64,1.64l-.02-.02Z"/>
16
+ <path class="cls-2" d="M98.25,37.35l-3.85-.04c-.81,0-1.5.58-1.64,1.38l-.53,3.18c-.12.79-.81,1.38-1.62,1.38h-11.07c-1.07,0-1.85-.99-1.6-2.03l8.38-33.28c.18-.72.85-1.24,1.6-1.24h16.76c.76.02,1.41.53,1.58,1.27l8.4,33.25c.26,1.04-.51,2.05-1.58,2.05l-11.07.04c-.81,0-1.5-.58-1.62-1.38l-.53-3.16c-.12-.79-.81-1.38-1.6-1.38v-.04ZM97.94,27.4l-.85-4.19c-.18-.88-1.44-.88-1.62,0l-.76,4.2c-.19,1.02.62,1.98,1.65,1.94,1.02-.04,1.76-.97,1.57-1.98v.02Z"/>
17
+ <path class="cls-2" d="M69.77,43.23l-10.49.04c-.92,0-1.65-.74-1.65-1.64v-21.76c0-.9-.72-1.64-1.64-1.64h-5.39c-.9,0-1.64-.76-1.64-1.66v-8.2c0-.9.74-1.64,1.65-1.64h27.8c.9,0,1.64.74,1.64,1.64v8.2c0,.9-.72,1.64-1.64,1.64h-5.39c-.9,0-1.64.74-1.64,1.66v21.75c0,.9-.74,1.64-1.64,1.64l.02-.02Z"/>
18
+ <path class="cls-2" d="M311.9,24.92l9.21,15.85c.63,1.1-.14,2.47-1.41,2.47l-10.72.05c-.6,0-1.14-.32-1.44-.83l-4.07-7.21c-.6-1.08-2.24-.65-2.25.58l-.1,5.81c0,.9-.74,1.63-1.64,1.63h-10.47c-.92,0-1.64-.72-1.64-1.64V8.37c0-.9.72-1.64,1.64-1.64h10.49c.9,0,1.62.71,1.64,1.59l.11,4.26c.04,1.27,1.74,1.64,2.29.49l2.59-5.39c.28-.57.85-.94,1.48-.94h10.88c1.27-.02,2.06,1.34,1.43,2.44l-7.97,14.1c-.28.51-.28,1.13,0,1.64h-.02Z"/>
19
+ <path class="cls-2" d="M238.4,37.38l-3.85-.04c-.81,0-1.5.58-1.64,1.38l-.53,3.18c-.12.79-.81,1.38-1.62,1.38h-11.09c-1.07,0-1.85-.99-1.6-2.03l8.38-33.26c.18-.72.85-1.24,1.58-1.24h16.78c.76-.02,1.41.49,1.6,1.22l8.4,33.26c.26,1.04-.51,2.05-1.58,2.05h-11.07c-.81.04-1.5-.55-1.62-1.34l-.53-3.16c-.12-.79-.81-1.38-1.6-1.38v-.02ZM238.07,27.43l-.74-3.67c-.19-.99-1.62-.99-1.81.02l-.67,3.69c-.19,1.02.62,1.98,1.65,1.94,1.02-.04,1.76-.97,1.57-1.98Z"/>
20
+ <path class="cls-2" d="M189.42,33.35c-.99,11.71-17.78,12.35-29.17,8.2-.67-.25-1.11-.9-1.11-1.61v-9.57c0-1.31,1.37-2.14,2.52-1.5,3.68,2.01,7.39,3.44,11.6,4.29,0,0,1.39.11,1.9-.79.51-.9-.46-1.25-.88-1.61-8.01-3.48-17.32-5.53-16.49-14.59.97-10.48,16.83-11.75,27.62-8.39.7.21,1.18.9,1.18,1.64v9.38c0,1.24-1.27,2.07-2.41,1.57-5.16-2.28-12.16-4.29-11.81-2.95.9,3.39,18.06,3.76,17.04,15.9l.02.04Z"/>
21
+ <path class="cls-2" d="M271.32,29.38c2.8,2.33,6.25,2.44,9.22.94,1.41-.71,3.06.25,3.06,1.82l.05,8.37c0,.87-.51,1.68-1.34,1.96-8.08,2.86-17.55,1.24-23.25-5.81-5.7-7.05-5.42-16.91.23-23.62,5.65-6.71,15.03-8.27,23.01-5.55.83.28,1.36,1.1,1.36,1.98l-.05,8.37c0,1.57-1.65,2.53-3.06,1.84-2.94-1.47-6.28-1.47-9.12.79-2.48,1.98-2.75,6.69-.11,8.9Z"/>
22
+ <path class="cls-2" d="M210.7,43.24l-10.49.04c-.92,0-1.65-.74-1.65-1.64v-21.76c0-.9-.72-1.64-1.64-1.64h-5.39c-.9,0-1.64-.76-1.64-1.66v-8.2c0-.9.74-1.64,1.65-1.64h27.79c.9,0,1.64.74,1.64,1.64v8.2c0,.9-.72,1.64-1.64,1.64h-5.39c-.9,0-1.64.74-1.64,1.66v21.75c0,.9-.72,1.64-1.64,1.64l.02-.02Z"/>
23
+ </svg>
@@ -0,0 +1,382 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // AllBlue - 理想之海
3
+ // Copyright (C) 2026 AllBlue Contributors
4
+ //
5
+ // This program is free software: you can redistribute it and/or modify
6
+ // it under the terms of the GNU Affero General Public License as published by
7
+ // the Free Software Foundation, either version 3 of the License, or
8
+ // (at your option) any later version.
9
+ //
10
+ // This program is distributed in the hope that it will be useful,
11
+ // but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ // GNU Affero General Public License for more details.
14
+ //
15
+ // You should have received a copy of the GNU Affero General Public License
16
+ // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+
18
+ import { cn } from '@/lib/utils'
19
+ import { ScrollText, AlertTriangle, ArrowLeft } from 'lucide-react'
20
+ import ReactMarkdown from 'react-markdown'
21
+ import { useTranslation } from 'react-i18next'
22
+ import { useMemo, useState } from 'react'
23
+ import { Route } from '@/routes/_nav/changelog/route'
24
+ import type { DeveloperLogEntry } from '@allbluecn/shared'
25
+ import { SDK_GROUPS } from '@/lib/changelog/sdk-versions'
26
+ import { APP_FULL_VERSION } from '@/lib/app-version'
27
+
28
+ const TAG_KEYS = ['all', 'feat', 'fix', 'improvement', 'design', 'refactor', 'breaking', 'docs', 'security', 'perf']
29
+
30
+ const REQUIRES_ACTION_TAGS = new Set(['breaking'])
31
+
32
+ function ChangelogSidebar() {
33
+ const { t } = useTranslation('common')
34
+
35
+ return (
36
+ <aside className="shrink-0 w-150 h-full flex flex-col py-6 pr-12 pl-10 border-r border-border bg-muted/50">
37
+ <div className="flex-1 flex flex-col gap-8 justify-center">
38
+ <div>
39
+ <h1 className="text-4xl font-medium tracking-tight">
40
+ {t('changelog.title')}
41
+ </h1>
42
+ <p className="text-sm text-muted-foreground mt-3 leading-relaxed">
43
+ {t('changelog.description')}
44
+ </p>
45
+ </div>
46
+
47
+ <div className="flex flex-col gap-4">
48
+ <h2 className="text-sm font-semibold tracking-tight">
49
+ {t('changelog.sdkVersions')}
50
+ </h2>
51
+ {SDK_GROUPS.map((group) => (
52
+ <SdkGroupSection key={group.id} group={group} t={t} />
53
+ ))}
54
+ </div>
55
+
56
+ <div>
57
+ <h3 className="text-sm font-semibold tracking-tight">
58
+ {t('changelog.subscribe.title')}
59
+ </h3>
60
+ <p className="text-xs text-muted-foreground mt-1 leading-relaxed">
61
+ {t('changelog.subscribe.subtitle')}
62
+ </p>
63
+ <div className="mt-3 flex gap-1.5">
64
+ <input
65
+ type="email"
66
+ placeholder={t('changelog.subscribe.placeholder')}
67
+ className="flex-1 min-w-0 h-8 rounded-md border border-border bg-background px-3 text-xs placeholder:text-muted-foreground/70 focus:outline-none focus:ring-2 focus:ring-primary/20 transition-shadow"
68
+ />
69
+ <button className="h-8 px-3 rounded-md bg-primary text-primary-foreground text-xs font-medium hover:bg-primary/90 transition-colors shrink-0">
70
+ {t('changelog.subscribe.submit')}
71
+ </button>
72
+ </div>
73
+ </div>
74
+ </div>
75
+
76
+ <div className="pt-4 border-border/50">
77
+ <code className="text-[10px] font-mono text-primary/80 bg-primary/5 px-2 py-0.5 rounded-full">
78
+ {APP_FULL_VERSION}
79
+ </code>
80
+ </div>
81
+ </aside>
82
+ )
83
+ }
84
+
85
+ function TagFilter({
86
+ activeTag,
87
+ onTagChange,
88
+ }: {
89
+ activeTag: string
90
+ onTagChange: (tag: string) => void
91
+ }) {
92
+ const { t } = useTranslation('common')
93
+
94
+ return (
95
+ <div className="flex flex-wrap gap-1.5">
96
+ {TAG_KEYS.map((tag) => {
97
+ const isActive = activeTag === tag
98
+ const label = t(`changelog.tags.${tag}`)
99
+ return (
100
+ <button
101
+ key={tag}
102
+ onClick={() => onTagChange(tag)}
103
+ className={cn(
104
+ 'inline-flex items-center gap-1 px-2.5 py-1 rounded-md text-xs font-medium transition-colors border',
105
+ isActive
106
+ ? 'bg-primary/10 text-primary border-primary/20'
107
+ : 'bg-transparent text-muted-foreground border-border/50 hover:text-foreground hover:border-border'
108
+ )}
109
+ >
110
+ {label}
111
+ {REQUIRES_ACTION_TAGS.has(tag) && (
112
+ <AlertTriangle className="size-3" />
113
+ )}
114
+ </button>
115
+ )
116
+ })}
117
+ </div>
118
+ )
119
+ }
120
+
121
+ function YearHeader({ year, count }: { year: string; count: number }) {
122
+ const { t } = useTranslation('common')
123
+
124
+ return (
125
+ <div className="flex items-center gap-3 py-4 mb-1">
126
+ <h3 className="text-xl font-semibold tracking-tight">{year}</h3>
127
+ <span className="text-xs text-muted-foreground font-mono">
128
+ {count} {t('changelog.entries')}
129
+ </span>
130
+ <div className="flex-1 h-px bg-border/50" />
131
+ </div>
132
+ )
133
+ }
134
+
135
+ function ChangelogEntry({
136
+ entry,
137
+ onClick,
138
+ }: {
139
+ entry: { id: string; date: string; title: string; desc: string; tags: string[] }
140
+ onClick: (id: string) => void
141
+ }) {
142
+ const { t } = useTranslation('common')
143
+
144
+ return (
145
+ <div
146
+ className="group py-3 hover:bg-muted/30 -mx-3 px-3 rounded-lg transition-colors cursor-pointer"
147
+ onClick={() => onClick(entry.id)}
148
+ >
149
+ <div className="flex items-start gap-3">
150
+ <div className="shrink-0 pt-0.5">
151
+ <span className="text-xs text-muted-foreground tabular-nums">
152
+ {entry.date}
153
+ </span>
154
+ </div>
155
+ <div className="flex-1 min-w-0">
156
+ <div className="flex items-start gap-2">
157
+ <h4 className="text-sm font-semibold leading-snug group-hover:text-primary transition-colors">
158
+ {entry.title}
159
+ </h4>
160
+ {entry.tags.includes('breaking') && (
161
+ <span className="inline-flex items-center gap-0.5 px-1.5 py-0.5 rounded text-[10px] font-medium bg-destructive/10 text-destructive shrink-0 mt-0.5">
162
+ <AlertTriangle className="size-2.5" />
163
+ {t('changelog.requiresAction')}
164
+ </span>
165
+ )}
166
+ </div>
167
+ <p className="text-xs text-muted-foreground leading-relaxed mt-1 line-clamp-2">
168
+ {entry.desc}
169
+ </p>
170
+ {entry.tags.length > 0 && (
171
+ <div className="flex flex-wrap gap-1 mt-1.5">
172
+ {entry.tags.map((tag) => (
173
+ <span
174
+ key={tag}
175
+ className="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] text-muted-foreground bg-muted/50"
176
+ >
177
+ {t(`changelog.tags.${tag}`)}
178
+ </span>
179
+ ))}
180
+ </div>
181
+ )}
182
+ </div>
183
+ </div>
184
+ </div>
185
+ )
186
+ }
187
+
188
+ function ChangelogDetail({
189
+ entry,
190
+ onBack,
191
+ }: {
192
+ entry: DeveloperLogEntry
193
+ onBack: () => void
194
+ }) {
195
+ const { t } = useTranslation('common')
196
+
197
+ return (
198
+ <div className="h-full flex justify-center">
199
+ <div className="max-w-4xl flex flex-col py-5">
200
+ <button
201
+ onClick={onBack}
202
+ className="flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors mb-6 shrink-0"
203
+ >
204
+ <ArrowLeft className="size-3.5" />
205
+ {t('changelog.backToList')}
206
+ </button>
207
+
208
+ <div className="flex items-start gap-3 mb-4">
209
+ <span className="text-xs text-muted-foreground font-mono tabular-nums pt-0.5 shrink-0">
210
+ {entry.date}
211
+ </span>
212
+ </div>
213
+
214
+ <h2 className="text-2xl font-semibold tracking-tight mb-4">
215
+ {entry.title}
216
+ </h2>
217
+
218
+ {entry.tags.length > 0 && (
219
+ <div className="flex flex-wrap gap-1.5 mb-6">
220
+ {entry.tags.map((tag) => (
221
+ <span
222
+ key={tag}
223
+ className={cn(
224
+ 'inline-flex items-center gap-1 px-2 py-0.5 rounded text-[10px] font-medium border',
225
+ tag === 'breaking'
226
+ ? 'bg-destructive/10 text-destructive border-destructive/20'
227
+ : 'text-muted-foreground bg-muted/50 border-border/50'
228
+ )}
229
+ >
230
+ {t(`changelog.tags.${tag}`)}
231
+ </span>
232
+ ))}
233
+ </div>
234
+ )}
235
+
236
+ <div className="typeset typeset-article max-w-[37em]">
237
+ <ReactMarkdown>{entry.desc}</ReactMarkdown>
238
+ </div>
239
+
240
+ {entry.commitSha && (
241
+ <div className="mt-6 pt-4 border-t border-border/50">
242
+ <code className="text-[10px] font-mono text-muted-foreground">
243
+ commit: {entry.commitSha}
244
+ </code>
245
+ </div>
246
+ )}
247
+ </div>
248
+ </div>
249
+ )
250
+ }
251
+
252
+ function SdkGroupSection({ group, t }: { group: (typeof SDK_GROUPS)[0]; t: (key: string) => string }) {
253
+ const isTanstack = group.id === 'tanstack'
254
+
255
+ return (
256
+ <div>
257
+ <div
258
+ className="flex items-center gap-1.5 w-full py-1"
259
+ >
260
+ {isTanstack ? (
261
+ <span className="shrink-0">
262
+ <img
263
+ src="/tanstack-landscape-black.svg"
264
+ alt="TanStack"
265
+ className="h-4 dark:hidden"
266
+ />
267
+ <img
268
+ src="/tanstack-landscape-white.svg"
269
+ alt="TanStack"
270
+ className="hidden h-4 dark:block"
271
+ />
272
+ </span>
273
+ ) : (
274
+ <span className="text-base">{group.icon}</span>
275
+ )}
276
+ <span className="text-xs mt-1 font-semibold text-foreground/80">
277
+ {t(`changelog.sdkGroups.${group.label}`)}
278
+ </span>
279
+ </div>
280
+ <ul className="mt-1 flex flex-col">
281
+ {group.items.map((sdk) => (
282
+ <li key={sdk.name}>
283
+ <a
284
+ href={sdk.docsUrl || undefined}
285
+ target={sdk.docsUrl ? '_blank' : undefined}
286
+ rel={sdk.docsUrl ? 'noopener noreferrer' : undefined}
287
+ className="flex items-center justify-between gap-2 py-1.5 group"
288
+ >
289
+ <span className="text-xs font-medium text-muted-foreground group-hover:text-primary transition-colors truncate">
290
+ {sdk.name}
291
+ </span>
292
+ <code className="text-[10px] font-mono text-muted-foreground bg-muted/50 px-1.5 py-0.5 rounded shrink-0 group-hover:text-primary transition-colors">
293
+ {sdk.version}
294
+ </code>
295
+ </a>
296
+ </li>
297
+ ))}
298
+ </ul>
299
+ </div>
300
+ )
301
+ }
302
+
303
+ function ChangelogPage() {
304
+ const { t } = useTranslation('common')
305
+ const [activeTag, setActiveTag] = useState<string>('all')
306
+ const [selectedEntryId, setSelectedEntryId] = useState<string | null>(null)
307
+ const { entries } = Route.useLoaderData()
308
+
309
+ const filteredEntries = useMemo(() => {
310
+ if (activeTag === 'all') return entries
311
+ return entries.filter((entry) => entry.tags.includes(activeTag))
312
+ }, [activeTag, entries])
313
+
314
+ const grouped = useMemo(() => {
315
+ const groups: Record<string, DeveloperLogEntry[]> = {}
316
+ filteredEntries.forEach((entry) => {
317
+ if (!groups[entry.year]) groups[entry.year] = []
318
+ groups[entry.year].push(entry)
319
+ })
320
+ return Object.entries(groups).sort(([a], [b]) => Number(b) - Number(a))
321
+ }, [filteredEntries])
322
+
323
+ const selectedEntry = useMemo(
324
+ () => entries.find((e) => e.id === selectedEntryId) ?? null,
325
+ [entries, selectedEntryId]
326
+ )
327
+
328
+ const handleEntryClick = (id: string) => {
329
+ setSelectedEntryId(id)
330
+ }
331
+
332
+ const handleBack = () => {
333
+ setSelectedEntryId(null)
334
+ }
335
+
336
+ return (
337
+ <div className="h-full flex">
338
+ <ChangelogSidebar />
339
+
340
+ <div className="flex-1 min-w-0 flex flex-col h-full pl-8 pr-6 py-5">
341
+ {selectedEntry ? (
342
+ <ChangelogDetail entry={selectedEntry} onBack={handleBack} />
343
+ ) : (
344
+ <>
345
+ <div className="shrink-0 pb-3">
346
+ <TagFilter activeTag={activeTag} onTagChange={setActiveTag} />
347
+ </div>
348
+
349
+ <div className="flex-1 min-h-0 overflow-y-auto">
350
+ {grouped.length === 0 ? (
351
+ <div className="flex flex-col items-center justify-center h-40 text-center">
352
+ <ScrollText className="size-8 text-muted-foreground/40 mb-2" />
353
+ <p className="text-sm text-muted-foreground">{t('changelog.noMatch')}</p>
354
+ <p className="text-xs text-muted-foreground mt-0.5">{t('changelog.noMatchHint')}</p>
355
+ </div>
356
+ ) : (
357
+ <div className="space-y-4">
358
+ {grouped.map(([year, yearEntries]) => (
359
+ <div key={year}>
360
+ <YearHeader year={year} count={yearEntries.length} />
361
+ <div className="space-y-0">
362
+ {yearEntries.map((entry) => (
363
+ <ChangelogEntry
364
+ key={entry.id}
365
+ entry={entry}
366
+ onClick={handleEntryClick}
367
+ />
368
+ ))}
369
+ </div>
370
+ </div>
371
+ ))}
372
+ </div>
373
+ )}
374
+ </div>
375
+ </>
376
+ )}
377
+ </div>
378
+ </div>
379
+ )
380
+ }
381
+
382
+ export { ChangelogPage }
@@ -21,15 +21,15 @@ import { useHotkey, useHotkeySequences } from "@tanstack/react-hotkeys"
21
21
  import type { HotkeySequence } from "@tanstack/hotkeys"
22
22
  import { useTranslation } from "react-i18next"
23
23
  import {
24
- Command,
25
- CommandDialog,
26
- CommandEmpty,
27
- CommandGroup,
28
- CommandInput,
29
- CommandItem,
30
- CommandList,
31
- CommandShortcut,
32
- } from "@/components/ui/command"
24
+ CommandMenu,
25
+ CommandMenuContent,
26
+ CommandMenuInput,
27
+ CommandMenuList,
28
+ CommandMenuEmpty,
29
+ CommandMenuGroup,
30
+ CommandMenuItem,
31
+ useCommandMenu,
32
+ } from "@/components/hextaui/command-menu"
33
33
  import { CreatePRDDialog } from "@allbluecn/plugins-core/prd/components/dialog/create-prd-dialog"
34
34
  import { useCommands, type ResolvedCommand } from "@/hooks/use-commands"
35
35
  import {
@@ -54,8 +54,8 @@ import {
54
54
  ShoppingCart,
55
55
  Building2,
56
56
  Settings,
57
- Users,
58
- Earth,
57
+ SunMoon,
58
+ LogOut,
59
59
  } from "lucide-react"
60
60
  import type { LucideIcon } from "lucide-react"
61
61
 
@@ -76,8 +76,8 @@ const PALETTE_ICON_MAP: Record<string, LucideIcon> = {
76
76
  cart: ShoppingCart,
77
77
  building2: Building2,
78
78
  settings: Settings,
79
- users: Users,
80
- earth: Earth,
79
+ sunMoon: SunMoon,
80
+ logOut: LogOut,
81
81
  }
82
82
 
83
83
  export function CommandPalette() {
@@ -100,15 +100,12 @@ export function CommandPalette() {
100
100
  },
101
101
  })
102
102
 
103
- const handleClose = () => setCommandPaletteOpen(false)
104
-
105
103
  const run = (cmd: ResolvedCommand) => {
106
- handleClose()
104
+ setCommandPaletteOpen(false)
107
105
  execute(cmd)
108
106
  }
109
107
 
110
108
  useHotkey("Mod+K", toggleCommandPalette, { ignoreInputs: false })
111
-
112
109
  useHotkey("Mod+Shift+L", () => setTheme(theme === "dark" ? "light" : "dark"), {
113
110
  ignoreInputs: false,
114
111
  })
@@ -123,50 +120,101 @@ export function CommandPalette() {
123
120
  })),
124
121
  )
125
122
 
123
+ return (
124
+ <>
125
+ <CommandMenu
126
+ open={open}
127
+ onOpenChange={(next) => {
128
+ if (next) toggleCommandPalette()
129
+ else setCommandPaletteOpen(false)
130
+ }}
131
+ >
132
+ <CommandMenuContent className="w-205">
133
+ <CommandMenuInput
134
+ placeholder={t("command.searchPlaceholder")}
135
+ />
136
+ <CommandMenuList maxHeight="500px">
137
+ <CommandPaletteList commands={commands} onRun={run} t={t} />
138
+ </CommandMenuList>
139
+ </CommandMenuContent>
140
+ </CommandMenu>
141
+ <CreatePRDDialog open={prdDialogOpen} onOpenChange={setPrdDialogOpen} />
142
+ </>
143
+ )
144
+ }
145
+
146
+ function resolveShortcut(shortcut: string[]): string {
147
+ const isMac = typeof navigator !== "undefined"
148
+ && (navigator.platform.toUpperCase().includes("MAC")
149
+ || navigator.userAgent.toUpperCase().includes("MAC"))
150
+ return shortcut.map((k) => {
151
+ const upper = k.toUpperCase()
152
+ if (upper === "MOD") return isMac ? "CMD" : "CTRL"
153
+ return upper
154
+ }).join("+")
155
+ }
156
+
157
+ function CommandPaletteList({
158
+ commands,
159
+ onRun,
160
+ t,
161
+ }: {
162
+ commands: ResolvedCommand[]
163
+ onRun: (cmd: ResolvedCommand) => void
164
+ t: (key: string) => string
165
+ }) {
166
+ const { value } = useCommandMenu()
167
+
126
168
  const navCommands = commands.filter((c) => c.group === "navigation")
127
169
  const actionCommands = commands.filter((c) => c.group === "action")
128
170
 
171
+ const filterCommands = (cmds: ResolvedCommand[]): ResolvedCommand[] => {
172
+ if (!value) return cmds
173
+ const lower = value.toLowerCase()
174
+ return cmds.filter(
175
+ (c) =>
176
+ c.title.toLowerCase().includes(lower) ||
177
+ (c.keywords ?? []).some((k) => k.toLowerCase().includes(lower)),
178
+ )
179
+ }
180
+
181
+ const filteredNav = filterCommands(navCommands)
182
+ const filteredAction = filterCommands(actionCommands)
183
+
184
+ if (filteredNav.length === 0 && filteredAction.length === 0) {
185
+ return <CommandMenuEmpty>{t("command.empty")}</CommandMenuEmpty>
186
+ }
187
+
188
+ let index = 0
189
+
129
190
  const renderItem = (cmd: ResolvedCommand) => {
130
191
  const Icon = cmd.icon ? PALETTE_ICON_MAP[cmd.icon] : undefined
192
+ const idx = index++
131
193
  return (
132
- <CommandItem
194
+ <CommandMenuItem
133
195
  key={cmd.id}
134
- value={[cmd.title, ...(cmd.keywords ?? [])].join(" ")}
135
- onSelect={() => run(cmd)}
196
+ index={idx}
197
+ onSelect={() => onRun(cmd)}
198
+ shortcut={cmd.shortcut ? resolveShortcut(cmd.shortcut) : undefined}
199
+ icon={Icon && <Icon className="size-4 shrink-0" />}
136
200
  >
137
- {Icon && <Icon className="size-4 shrink-0" />}
138
201
  <span className="truncate">{cmd.title}</span>
139
- {cmd.shortcut && (
140
- <CommandShortcut>{cmd.shortcut.join(" → ")}</CommandShortcut>
141
- )}
142
- </CommandItem>
202
+ </CommandMenuItem>
143
203
  )
144
204
  }
145
205
 
146
206
  return (
147
207
  <>
148
- <CommandDialog
149
- open={open}
150
- onOpenChange={(next) =>
151
- next ? toggleCommandPalette() : handleClose()
152
- }
153
- title={t("command.searchPlaceholder")}
154
- description={t("command.searchPlaceholder")}
155
- >
156
- <Command className="rounded-xl!">
157
- <CommandInput placeholder={t("command.searchPlaceholder")} />
158
- <CommandList className="max-h-100">
159
- <CommandEmpty>{t("command.empty")}</CommandEmpty>
160
- <CommandGroup heading={t("command.groupNavigation")}>
161
- {navCommands.map(renderItem)}
162
- </CommandGroup>
163
- <CommandGroup heading={t("command.groupAction")}>
164
- {actionCommands.map(renderItem)}
165
- </CommandGroup>
166
- </CommandList>
167
- </Command>
168
- </CommandDialog>
169
- <CreatePRDDialog open={prdDialogOpen} onOpenChange={setPrdDialogOpen} />
208
+ {filteredNav.length > 0 && (
209
+ <CommandMenuGroup heading={t("command.groupNavigation")}>
210
+ {filteredNav.map(renderItem)}
211
+ </CommandMenuGroup>
212
+ )}
213
+ {filteredAction.length > 0 && (
214
+ <CommandMenuGroup heading={t("command.groupAction")}>
215
+ {filteredAction.map(renderItem)}
216
+ </CommandMenuGroup>
217
+ )}
170
218
  </>
171
219
  )
172
220
  }