@nextclaw/ui 0.14.1 → 0.14.3
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/CHANGELOG.md +41 -0
- package/dist/assets/{api-DPcvCLfa.js → api-BP_8IaxB.js} +7 -7
- package/dist/assets/channels-list-page-DEh5Jjqx.js +8 -0
- package/dist/assets/chat-page-BRB0N5WW.js +105 -0
- package/dist/assets/config-split-page-CRhlZ_pH.js +1 -0
- package/dist/assets/{confirm-dialog-CWh5FfK2.js → confirm-dialog-DsivFJ4r.js} +2 -2
- package/dist/assets/{desktop-update-config-DsRhBizi.js → desktop-update-config-B0w9p2ep.js} +1 -1
- package/dist/assets/{dist-DPrgcEC0.js → dist-CikkhOlq.js} +1 -1
- package/dist/assets/{dist-DsE4OsSS.js → dist-PVA_xL1D.js} +1 -1
- package/dist/assets/doc-browser-BoNJ7-tE.js +1 -0
- package/dist/assets/doc-browser-CdJ_d4ik.js +1 -0
- package/dist/assets/doc-browser-context-MTw4EvKd.js +1 -0
- package/dist/assets/{ellipsis-B81FStLm.js → ellipsis-y6vDtq4M.js} +1 -1
- package/dist/assets/{external-link-C_dmG-WJ.js → external-link-DVJIQf_G.js} +1 -1
- package/dist/assets/index-BiE-K8-q.css +1 -0
- package/dist/assets/index-D9BAE_L_.js +105 -0
- package/dist/assets/mcp-marketplace-page-B1SZNY5m.js +40 -0
- package/dist/assets/mcp-marketplace-page-Cq_Tw42T.js +1 -0
- package/dist/assets/middleware-CkFT3R4v.js +1 -0
- package/dist/assets/model-config-page-DotaTUHb.js +1 -0
- package/dist/assets/plus-BbZqquJe.js +1 -0
- package/dist/assets/provider-scoped-model-input-DsFRuWJH.js +1 -0
- package/dist/assets/providers-config-page-DW4tQSLN.js +1 -0
- package/dist/assets/{react-sZz5HSCm.js → react-VNqT5Zp7.js} +1 -1
- package/dist/assets/remote-B8TiUfwW.js +1 -0
- package/dist/assets/rotate-cw-DjHDTkCv.js +1 -0
- package/dist/assets/runtime-config-page-S6ErRHR8.js +1 -0
- package/dist/assets/{save-MzjoiHri.js → save-BOJ477H-.js} +1 -1
- package/dist/assets/{search-B29dE4GN.js → search-BEti5beA.js} +1 -1
- package/dist/assets/{search-config-page-Dl5sZzob.js → search-config-page-DGj5am03.js} +1 -1
- package/dist/assets/{secrets-config-page-C_l-IFCK.js → secrets-config-page--IzrIOid.js} +2 -2
- package/dist/assets/{select-BKmh05hd.js → select-hoj8dK4K.js} +2 -2
- package/dist/assets/{tag-chip-BS_7Ueom.js → tag-chip-T8-h-die.js} +1 -1
- package/dist/assets/use-config-CjRV2hwL.js +1 -0
- package/dist/assets/x-D9PXs_xP.js +1 -0
- package/dist/index.html +70 -19
- package/index.html +55 -5
- package/package.json +9 -9
- package/src/app/components/app-presenter-provider.tsx +11 -2
- package/src/app/components/layout/__tests__/app-layout.test.tsx +11 -2
- package/src/app/components/layout/__tests__/sidebar.layout.test.tsx +110 -79
- package/src/app/components/layout/app-layout.tsx +18 -4
- package/src/app/components/layout/header.tsx +3 -3
- package/src/app/components/layout/page-layout.tsx +2 -2
- package/src/app/components/layout/runtime-status-entry.tsx +7 -7
- package/src/app/components/layout/sidebar-items.tsx +194 -71
- package/src/app/components/layout/sidebar-rail.styles.ts +15 -0
- package/src/app/components/layout/sidebar.tsx +340 -83
- package/src/app/managers/viewport-layout.manager.ts +10 -0
- package/src/app/stores/viewport-layout.store.ts +39 -3
- package/src/app/styles/design-system.css +388 -26
- package/src/app/test/vitest-setup.ts +31 -0
- package/src/features/agents/components/__tests__/agents-page.test.tsx +62 -39
- package/src/features/agents/components/agents-page.tsx +10 -6
- package/src/features/chat/components/chat-sidebar-list-mode-switch.tsx +3 -3
- package/src/features/chat/components/conversation/__tests__/chat-conversation-header-section.test.tsx +145 -0
- package/src/features/chat/components/conversation/__tests__/chat-conversation-header.test.tsx +4 -2
- package/src/features/chat/components/conversation/__tests__/chat-conversation-panel.test.tsx +69 -762
- package/src/features/chat/components/conversation/__tests__/chat-conversation-workspace-section.test.tsx +132 -0
- package/src/features/chat/components/conversation/chat-conversation-content.tsx +22 -15
- package/src/features/chat/components/conversation/chat-conversation-header-section.tsx +15 -16
- package/src/features/chat/components/conversation/chat-conversation-header.tsx +7 -6
- package/src/features/chat/components/conversation/chat-conversation-panel.tsx +19 -19
- package/src/features/chat/components/conversation/chat-conversation-skeleton.tsx +13 -13
- package/src/features/chat/components/conversation/chat-conversation-workspace-section.tsx +10 -4
- package/src/features/chat/components/layout/__tests__/chat-sidebar-read-state.test.tsx +52 -11
- package/src/features/chat/components/layout/__tests__/chat-sidebar.test.tsx +18 -16
- package/src/features/chat/components/layout/chat-page-shell.tsx +1 -1
- package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +247 -0
- package/src/features/chat/components/layout/chat-sidebar-toolbar.tsx +93 -20
- package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +93 -36
- package/src/features/chat/components/layout/chat-sidebar.tsx +60 -93
- package/src/features/chat/components/providers/chat-presenter.provider.tsx +0 -6
- package/src/features/chat/features/conversation/components/__tests__/session-conversation-area.test.tsx +225 -0
- package/src/features/chat/features/conversation/components/__tests__/session-conversation-input.streaming.test.tsx +199 -0
- package/src/features/chat/features/conversation/components/session-conversation-area.tsx +308 -0
- package/src/features/chat/features/conversation/components/session-conversation-input.tsx +439 -0
- package/src/features/chat/features/conversation/hooks/use-session-conversation-controller.ts +194 -0
- package/src/features/chat/features/conversation/hooks/use-session-conversation-input-attachments.ts +66 -0
- package/src/features/chat/features/{input/hooks/use-chat-input-bar-query-state.ts → conversation/hooks/use-session-conversation-input-query.ts} +51 -20
- package/src/features/chat/features/conversation/hooks/use-session-conversation-input-state.ts +281 -0
- package/src/features/chat/features/conversation/utils/session-conversation-input-toolbar.utils.ts +158 -0
- package/src/features/chat/features/input/hooks/__tests__/use-chat-input-surface-state.test.tsx +56 -0
- package/src/features/chat/features/input/hooks/use-chat-input-surface-state.ts +117 -0
- package/src/features/chat/features/input/input-surface-plugins/__tests__/panel-app-reference-plugin.test.ts +77 -0
- package/src/features/chat/features/input/input-surface-plugins/__tests__/slash-command-plugin.test.ts +108 -0
- package/src/features/chat/features/input/input-surface-plugins/chat-input-product-plugin-adapters.types.ts +10 -0
- package/src/features/chat/features/input/input-surface-plugins/input-surface-search.utils.ts +108 -0
- package/src/features/chat/features/input/input-surface-plugins/panel-app-reference-plugin.utils.ts +108 -0
- package/src/features/chat/features/input/input-surface-plugins/slash-command-plugin.utils.ts +161 -0
- package/src/features/chat/features/input/utils/__tests__/chat-composer-state.utils.test.ts +22 -1
- package/src/features/chat/features/input/utils/__tests__/chat-inline-token.utils.test.ts +26 -13
- package/src/features/chat/features/input/utils/__tests__/ncp-chat-input-availability.utils.test.ts +7 -28
- package/src/features/chat/features/input/utils/chat-composer-state.utils.ts +6 -0
- package/src/features/chat/features/input/utils/chat-inline-token.utils.ts +26 -51
- package/src/features/chat/features/input/utils/chat-input-bar.utils.ts +2 -0
- package/src/features/chat/features/input/utils/ncp-chat-input-availability.utils.ts +5 -6
- package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +47 -12
- package/src/features/chat/features/message/components/chat-inline-panel-app-card.tsx +7 -7
- package/src/features/chat/features/message/components/chat-message-list.container.tsx +99 -18
- package/src/features/chat/features/message/utils/__tests__/chat-message.utils.test.ts +31 -12
- package/src/features/chat/features/message/utils/chat-message-markdown-part.utils.ts +38 -0
- package/src/features/chat/features/message/utils/chat-message-part.utils.ts +1 -1
- package/src/features/chat/features/session/components/chat-sidebar-project-groups.tsx +7 -7
- package/src/features/chat/features/session/components/chat-sidebar-session-list.tsx +6 -6
- package/src/features/chat/features/session/components/session-context-icon.tsx +2 -2
- package/src/features/chat/features/session/components/session-header/__tests__/chat-session-project-badge.test.tsx +2 -2
- package/src/features/chat/features/session/components/session-header/chat-session-header-actions.tsx +1 -1
- package/src/features/chat/features/session/components/session-header/chat-session-header-menu-item.tsx +1 -1
- package/src/features/chat/features/session/components/session-header/chat-session-metadata-dialog.tsx +1 -1
- package/src/features/chat/features/session/components/session-header/chat-session-project-badge.tsx +3 -3
- package/src/features/chat/features/session/components/session-run-badge.tsx +1 -1
- package/src/features/chat/features/session/hooks/__tests__/use-chat-session-project.test.tsx +3 -30
- package/src/features/chat/features/session/hooks/use-chat-session-project.ts +0 -5
- package/src/features/chat/features/session/utils/chat-run-metadata.utils.ts +8 -0
- package/src/features/chat/features/session-type/components/__tests__/chat-session-type-menu.test.tsx +2 -2
- package/src/features/chat/features/session-type/components/__tests__/chat-session-type-option-item.test.tsx +2 -0
- package/src/features/chat/features/session-type/components/chat-session-type-menu.tsx +2 -2
- package/src/features/chat/features/session-type/components/chat-session-type-option-item.tsx +2 -2
- package/src/features/chat/features/session-type/hooks/use-chat-session-type-state.ts +10 -2
- package/src/features/chat/features/welcome/components/__tests__/chat-conversation-welcome.test.tsx +49 -31
- package/src/features/chat/features/welcome/components/__tests__/chat-welcome.test.tsx +2 -9
- package/src/features/chat/features/welcome/components/chat-conversation-welcome.tsx +21 -10
- package/src/features/chat/features/welcome/components/chat-welcome-agent-picker.tsx +3 -3
- package/src/features/chat/features/welcome/components/chat-welcome-capability-grid.tsx +4 -4
- package/src/features/chat/features/welcome/components/chat-welcome-project-picker.tsx +14 -19
- package/src/features/chat/features/welcome/components/chat-welcome-session-type-picker.tsx +5 -5
- package/src/features/chat/features/welcome/components/chat-welcome.tsx +6 -6
- package/src/features/chat/features/welcome/utils/__tests__/chat-welcome-project-options.utils.test.ts +6 -8
- package/src/features/chat/features/welcome/utils/chat-welcome-project-options.utils.ts +1 -16
- package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel-content.test.tsx +54 -0
- package/src/features/chat/features/workspace/components/chat-session-workspace-panel-content.tsx +44 -76
- package/src/features/chat/features/workspace/components/chat-session-workspace-panel-nav.tsx +5 -1
- package/src/features/chat/features/workspace/components/chat-session-workspace-panel.tsx +7 -1
- package/src/features/chat/features/workspace/hooks/use-chat-conversation-workspace-state.ts +40 -31
- package/src/features/chat/features/workspace/utils/__tests__/chat-workspace-panel-view-model.utils.test.ts +59 -1
- package/src/features/chat/features/workspace/utils/chat-thread-workspace-session.utils.ts +109 -0
- package/src/features/chat/features/workspace/utils/chat-workspace-panel-view-model.utils.ts +44 -4
- package/src/features/chat/index.ts +0 -1
- package/src/features/chat/managers/__tests__/chat-session-list.manager.test.ts +51 -99
- package/src/features/chat/managers/__tests__/chat-thread.manager.test.ts +135 -21
- package/src/features/chat/managers/chat-session-list.manager.ts +12 -43
- package/src/features/chat/managers/chat-thread.manager.ts +108 -34
- package/src/features/chat/pages/__tests__/ncp-chat-page.test.ts +48 -1
- package/src/features/chat/pages/__tests__/ncp-chat-page.test.tsx +72 -0
- package/src/features/chat/pages/ncp-chat-page.tsx +0 -122
- package/src/features/chat/presenters/chat.presenter.ts +2 -19
- package/src/features/chat/stores/__tests__/chat-thread.store.test.ts +31 -0
- package/src/features/chat/stores/chat-thread.store.ts +42 -26
- package/src/features/panel-apps/components/panel-app-list-item.tsx +8 -8
- package/src/features/panel-apps/components/panel-app-toolbar.tsx +4 -4
- package/src/features/panel-apps/components/panel-apps-list.tsx +20 -20
- package/src/features/panel-apps/hooks/use-panel-apps.ts +2 -1
- package/src/features/pwa/managers/__tests__/pwa-shell-theme.manager.test.ts +70 -6
- package/src/features/pwa/managers/pwa-shell-theme.manager.ts +10 -5
- package/src/features/service-apps/components/service-action-authorization-dialog.tsx +8 -8
- package/src/features/service-apps/components/service-apps-panel.tsx +24 -24
- package/src/features/settings/components/config/provider-advanced-settings-section.tsx +5 -5
- package/src/features/settings/components/config/provider-auth-section.tsx +7 -7
- package/src/features/settings/components/config/provider-form.tsx +10 -10
- package/src/features/settings/components/config/provider-models-section.tsx +16 -16
- package/src/features/settings/components/config/provider-pill-selector.tsx +1 -1
- package/src/features/settings/pages/providers-config-page.tsx +17 -17
- package/src/platforms/desktop/components/__tests__/desktop-app-shell.test.tsx +50 -10
- package/src/platforms/desktop/components/desktop-app-shell.tsx +34 -10
- package/src/platforms/desktop/components/desktop-window-chrome.tsx +41 -16
- package/src/shared/components/config-split-page.tsx +11 -11
- package/src/shared/components/doc-browser/doc-browser-home-page.tsx +2 -2
- package/src/shared/components/doc-browser/doc-browser-panel-parts.tsx +4 -4
- package/src/shared/components/doc-browser/doc-browser-tab-strip.tsx +4 -4
- package/src/shared/components/doc-browser/doc-browser.tsx +3 -3
- package/src/shared/components/resizable-right-panel/resizable-right-panel.tsx +1 -1
- package/src/shared/components/ui/actions/icon-action-button.tsx +1 -1
- package/src/shared/components/ui/button.tsx +4 -4
- package/src/shared/components/ui/card.tsx +4 -4
- package/src/shared/components/ui/dialog.tsx +5 -5
- package/src/shared/components/ui/input.tsx +1 -1
- package/src/shared/components/ui/label.tsx +1 -1
- package/src/shared/components/ui/popover.tsx +1 -1
- package/src/shared/components/ui/select.tsx +3 -3
- package/src/shared/components/ui/switch.tsx +3 -3
- package/src/shared/components/ui/tab-strip/__tests__/compact-tab-strip.test.tsx +24 -1
- package/src/shared/components/ui/tab-strip/compact-tab-strip.tsx +8 -4
- package/src/shared/components/ui/tabs.tsx +4 -4
- package/src/shared/components/ui/textarea.tsx +1 -1
- package/src/shared/lib/i18n/locales/en-US/chat.json +20 -2
- package/src/shared/lib/i18n/locales/en-US/core.json +9 -2
- package/src/shared/lib/i18n/locales/zh-CN/chat.json +20 -2
- package/src/shared/lib/i18n/locales/zh-CN/core.json +9 -2
- package/src/shared/lib/theme/index.ts +94 -52
- package/src/shared/lib/ui-document-title/__tests__/ui-document-title.test.ts +49 -0
- package/src/shared/lib/ui-document-title/index.ts +50 -21
- package/dist/assets/channels-list-page-D6jtziu4.js +0 -8
- package/dist/assets/chat-page-BBnNJoKG.js +0 -105
- package/dist/assets/config-split-page-BCJhqdK9.js +0 -1
- package/dist/assets/doc-browser-C8xOF_9F.js +0 -1
- package/dist/assets/doc-browser-DgQedeAn.js +0 -1
- package/dist/assets/doc-browser-context-B46tQGmO.js +0 -1
- package/dist/assets/index-7o1WdUbU.js +0 -103
- package/dist/assets/index-DQWmEWzk.css +0 -1
- package/dist/assets/mcp-marketplace-page-CWZN7ohK.js +0 -1
- package/dist/assets/mcp-marketplace-page-Dzull_4-.js +0 -40
- package/dist/assets/model-config-page-Cv0O3BIj.js +0 -1
- package/dist/assets/plus-Bnt56oqG.js +0 -1
- package/dist/assets/provider-scoped-model-input-Duuh7i3h.js +0 -1
- package/dist/assets/providers-config-page-CPDz0Lxc.js +0 -1
- package/dist/assets/remote-B0LMs8u7.js +0 -1
- package/dist/assets/rotate-cw-BfyG_KEF.js +0 -1
- package/dist/assets/runtime-config-page-q0fvv8ME.js +0 -1
- package/dist/assets/use-config-DZkBWIdL.js +0 -1
- package/dist/assets/x-CW2KO-Hz.js +0 -1
- package/src/features/chat/components/conversation/chat-conversation-alerts.tsx +0 -37
- package/src/features/chat/features/input/components/__tests__/chat-attachment-upload-limit.test.ts +0 -38
- package/src/features/chat/features/input/components/chat-input-bar.container.tsx +0 -496
- package/src/features/chat/features/message/utils/chat-message-inline-content.utils.ts +0 -95
- package/src/features/chat/managers/__tests__/chat-input.manager.test.ts +0 -448
- package/src/features/chat/managers/__tests__/chat-run-snapshot.manager.test.ts +0 -129
- package/src/features/chat/managers/__tests__/chat-run.manager.test.ts +0 -236
- package/src/features/chat/managers/__tests__/chat-session-preference-sync.manager.test.ts +0 -219
- package/src/features/chat/managers/chat-input.manager.ts +0 -430
- package/src/features/chat/managers/chat-run.manager.ts +0 -110
- package/src/features/chat/managers/chat-session-preference-sync.manager.ts +0 -142
- package/src/features/chat/stores/chat-input.store.ts +0 -104
- /package/dist/assets/{config-hints-CTqBnCDp.js → config-hints-BkYBc7z0.js} +0 -0
|
@@ -36,14 +36,14 @@ const DialogContent = React.forwardRef<
|
|
|
36
36
|
<DialogPrimitive.Content
|
|
37
37
|
ref={ref}
|
|
38
38
|
className={cn(
|
|
39
|
-
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border border-
|
|
39
|
+
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 rounded-2xl border border-border bg-popover p-6 text-popover-foreground shadow-xl duration-base data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%]",
|
|
40
40
|
className
|
|
41
41
|
)}
|
|
42
42
|
{...props}
|
|
43
43
|
>
|
|
44
44
|
{children}
|
|
45
|
-
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-lg p-1 opacity-70 ring-offset-
|
|
46
|
-
<X className="h-4 w-4 text-
|
|
45
|
+
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-lg p-1 opacity-70 ring-offset-background transition-all duration-fast hover:bg-accent hover:text-accent-foreground hover:opacity-100 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 disabled:pointer-events-none">
|
|
46
|
+
<X className="h-4 w-4 text-muted-foreground" />
|
|
47
47
|
<span className="sr-only">Close</span>
|
|
48
48
|
</DialogPrimitive.Close>
|
|
49
49
|
</DialogPrimitive.Content>
|
|
@@ -86,7 +86,7 @@ const DialogTitle = React.forwardRef<
|
|
|
86
86
|
<DialogPrimitive.Title
|
|
87
87
|
ref={ref}
|
|
88
88
|
className={cn(
|
|
89
|
-
"text-lg font-semibold leading-tight tracking-tight text-
|
|
89
|
+
"text-lg font-semibold leading-tight tracking-tight text-foreground",
|
|
90
90
|
className
|
|
91
91
|
)}
|
|
92
92
|
{...props}
|
|
@@ -100,7 +100,7 @@ const DialogDescription = React.forwardRef<
|
|
|
100
100
|
>(({ className, ...props }, ref) => (
|
|
101
101
|
<DialogPrimitive.Description
|
|
102
102
|
ref={ref}
|
|
103
|
-
className={cn("text-sm text-
|
|
103
|
+
className={cn("text-sm text-muted-foreground leading-relaxed", className)}
|
|
104
104
|
{...props}
|
|
105
105
|
/>
|
|
106
106
|
))
|
|
@@ -9,7 +9,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
|
9
9
|
<input
|
|
10
10
|
type={type}
|
|
11
11
|
className={cn(
|
|
12
|
-
'flex h-9 w-full rounded-xl border border-
|
|
12
|
+
'flex h-9 w-full rounded-xl border border-border bg-card px-3.5 py-2 text-sm text-foreground file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground/55 placeholder:font-normal focus:outline-none focus:ring-1 focus:ring-primary/40 focus:border-primary/50 transition-colors disabled:cursor-not-allowed disabled:opacity-50',
|
|
13
13
|
className
|
|
14
14
|
)}
|
|
15
15
|
ref={ref}
|
|
@@ -8,7 +8,7 @@ const Label = React.forwardRef<HTMLLabelElement, LabelProps>(
|
|
|
8
8
|
<label
|
|
9
9
|
ref={ref}
|
|
10
10
|
className={cn(
|
|
11
|
-
'text-sm font-medium leading-none text-
|
|
11
|
+
'text-sm font-medium leading-none text-foreground peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
|
|
12
12
|
className
|
|
13
13
|
)}
|
|
14
14
|
{...props}
|
|
@@ -28,7 +28,7 @@ const PopoverContent = React.forwardRef<
|
|
|
28
28
|
align={align}
|
|
29
29
|
collisionPadding={collisionPadding}
|
|
30
30
|
className={cn(
|
|
31
|
-
'z-[var(--z-popover,50)] w-72 overflow-x-hidden overflow-y-auto rounded-2xl border border-
|
|
31
|
+
'z-[var(--z-popover,50)] w-72 overflow-x-hidden overflow-y-auto rounded-2xl border border-border bg-popover p-4 text-popover-foreground shadow-lg animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
|
32
32
|
className
|
|
33
33
|
)}
|
|
34
34
|
style={{ maxHeight: POPOVER_CONTENT_MAX_HEIGHT, ...style }}
|
|
@@ -25,7 +25,7 @@ const SelectTrigger = React.forwardRef<
|
|
|
25
25
|
<SelectPrimitive.Trigger
|
|
26
26
|
ref={ref}
|
|
27
27
|
className={cn(
|
|
28
|
-
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-xl border border-
|
|
28
|
+
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-xl border border-border bg-card px-3 py-2 text-sm text-foreground shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-primary/40 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
|
29
29
|
className
|
|
30
30
|
)}
|
|
31
31
|
{...props}
|
|
@@ -78,7 +78,7 @@ const SelectContent = React.forwardRef<
|
|
|
78
78
|
<SelectPrimitive.Content
|
|
79
79
|
ref={ref}
|
|
80
80
|
className={cn(
|
|
81
|
-
"relative z-[var(--z-popover,50)] flex max-h-96 min-w-[8rem] flex-col overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2
|
|
81
|
+
"relative z-[var(--z-popover,50)] flex max-h-96 min-w-[8rem] flex-col overflow-hidden rounded-md border border-border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
|
82
82
|
position === "popper" &&
|
|
83
83
|
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
|
84
84
|
className
|
|
@@ -119,7 +119,7 @@ const SelectItem = React.forwardRef<
|
|
|
119
119
|
<SelectPrimitive.Item
|
|
120
120
|
ref={ref}
|
|
121
121
|
className={cn(
|
|
122
|
-
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-
|
|
122
|
+
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 hover:bg-accent",
|
|
123
123
|
className
|
|
124
124
|
)}
|
|
125
125
|
{...props}
|
|
@@ -16,8 +16,8 @@ const Switch = React.forwardRef<HTMLButtonElement, SwitchProps>(
|
|
|
16
16
|
aria-checked={checked}
|
|
17
17
|
ref={ref}
|
|
18
18
|
className={cn(
|
|
19
|
-
'peer inline-flex h-[22px] w-10 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors duration-fast focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-
|
|
20
|
-
checked ? 'bg-primary' : 'bg-
|
|
19
|
+
'peer inline-flex h-[22px] w-10 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors duration-fast focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50',
|
|
20
|
+
checked ? 'bg-primary' : 'bg-muted hover:bg-accent',
|
|
21
21
|
className
|
|
22
22
|
)}
|
|
23
23
|
onClick={() => onCheckedChange?.(!checked)}
|
|
@@ -26,7 +26,7 @@ const Switch = React.forwardRef<HTMLButtonElement, SwitchProps>(
|
|
|
26
26
|
<span
|
|
27
27
|
data-state={checked ? 'checked' : 'unchecked'}
|
|
28
28
|
className={cn(
|
|
29
|
-
'pointer-events-none block h-5 w-5 rounded-full bg-
|
|
29
|
+
'pointer-events-none block h-5 w-5 rounded-full bg-card shadow-md ring-0 transition-transform duration-fast',
|
|
30
30
|
checked ? 'translate-x-5' : 'translate-x-0',
|
|
31
31
|
thumbClassName
|
|
32
32
|
)}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { render, screen } from "@testing-library/react";
|
|
1
|
+
import { fireEvent, render, screen } from "@testing-library/react";
|
|
2
2
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
3
3
|
import {
|
|
4
4
|
CompactTabStrip,
|
|
@@ -49,4 +49,27 @@ describe("CompactTabStrip", () => {
|
|
|
49
49
|
inline: "nearest",
|
|
50
50
|
});
|
|
51
51
|
});
|
|
52
|
+
|
|
53
|
+
it("selects a tab when clicking its leading icon", () => {
|
|
54
|
+
const onSelect = vi.fn();
|
|
55
|
+
|
|
56
|
+
render(
|
|
57
|
+
<CompactTabStrip
|
|
58
|
+
tabs={[
|
|
59
|
+
{
|
|
60
|
+
key: "child-session",
|
|
61
|
+
label: "Child session",
|
|
62
|
+
active: false,
|
|
63
|
+
leadingIcon: <span data-testid="child-tab-icon" />,
|
|
64
|
+
onSelect,
|
|
65
|
+
},
|
|
66
|
+
]}
|
|
67
|
+
actions={[]}
|
|
68
|
+
/>,
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
fireEvent.click(screen.getByTestId("child-tab-icon"));
|
|
72
|
+
|
|
73
|
+
expect(onSelect).toHaveBeenCalledTimes(1);
|
|
74
|
+
});
|
|
52
75
|
});
|
|
@@ -65,6 +65,9 @@ function CompactTabLabelButton({
|
|
|
65
65
|
labelClassName?: string;
|
|
66
66
|
tab: CompactTabStripTab;
|
|
67
67
|
}) {
|
|
68
|
+
const leadingIcon = tab.onClose && tab.closePlacement === "leading-hover"
|
|
69
|
+
? null
|
|
70
|
+
: tab.leadingIcon;
|
|
68
71
|
const button = (
|
|
69
72
|
<button
|
|
70
73
|
type="button"
|
|
@@ -72,6 +75,11 @@ function CompactTabLabelButton({
|
|
|
72
75
|
aria-label={tab.label}
|
|
73
76
|
className="flex min-w-0 flex-1 items-center gap-1.5 text-left"
|
|
74
77
|
>
|
|
78
|
+
{leadingIcon ? (
|
|
79
|
+
<span className="inline-flex shrink-0 items-center justify-center">
|
|
80
|
+
{leadingIcon}
|
|
81
|
+
</span>
|
|
82
|
+
) : null}
|
|
75
83
|
<span className={cn("min-w-0 truncate text-[12px] font-medium", labelClassName)}>
|
|
76
84
|
{tab.label}
|
|
77
85
|
</span>
|
|
@@ -136,10 +144,6 @@ function CompactTabItem({
|
|
|
136
144
|
</span>
|
|
137
145
|
<X className="hidden h-3.5 w-3.5 group-hover:block" />
|
|
138
146
|
</button>
|
|
139
|
-
) : tab.leadingIcon ? (
|
|
140
|
-
<span className="inline-flex shrink-0 items-center justify-center">
|
|
141
|
-
{tab.leadingIcon}
|
|
142
|
-
</span>
|
|
143
147
|
) : null}
|
|
144
148
|
<CompactTabLabelButton tab={tab} labelClassName={labelClassName} />
|
|
145
149
|
{tab.onClose && tab.closePlacement !== "leading-hover" ? (
|
|
@@ -42,7 +42,7 @@ export function TabsList({ children, className }: TabsListProps) {
|
|
|
42
42
|
return (
|
|
43
43
|
<div
|
|
44
44
|
className={cn(
|
|
45
|
-
'inline-flex h-9 items-center justify-center rounded-xl bg-
|
|
45
|
+
'inline-flex h-9 items-center justify-center rounded-xl bg-muted p-1 text-muted-foreground',
|
|
46
46
|
className
|
|
47
47
|
)}
|
|
48
48
|
>
|
|
@@ -64,10 +64,10 @@ export function TabsTrigger({ value, children, className }: TabsTriggerProps) {
|
|
|
64
64
|
aria-pressed={isActive}
|
|
65
65
|
data-state={isActive ? 'active' : 'inactive'}
|
|
66
66
|
className={cn(
|
|
67
|
-
'inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-[13px] font-medium ring-offset-
|
|
67
|
+
'inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-[13px] font-medium ring-offset-background transition-all duration-fast focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
|
|
68
68
|
isActive
|
|
69
|
-
? 'bg-
|
|
70
|
-
: 'hover:bg-
|
|
69
|
+
? 'bg-card text-foreground shadow-sm'
|
|
70
|
+
: 'text-muted-foreground hover:bg-accent hover:text-accent-foreground',
|
|
71
71
|
className
|
|
72
72
|
)}
|
|
73
73
|
>
|
|
@@ -7,7 +7,7 @@ export const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
|
|
7
7
|
({ className, ...props }, ref) => (
|
|
8
8
|
<textarea
|
|
9
9
|
className={cn(
|
|
10
|
-
"flex min-h-28 w-full rounded-xl border border-
|
|
10
|
+
"flex min-h-28 w-full rounded-xl border border-border bg-card px-3.5 py-2.5 text-sm text-foreground placeholder:text-muted-foreground/55 placeholder:font-normal focus:outline-none focus:ring-1 focus:ring-primary/40 focus:border-primary/50 transition-colors disabled:cursor-not-allowed disabled:opacity-50",
|
|
11
11
|
className,
|
|
12
12
|
)}
|
|
13
13
|
ref={ref}
|
|
@@ -39,6 +39,8 @@
|
|
|
39
39
|
"chatWorkspaceBack": "Back in workspace",
|
|
40
40
|
"chatWorkspaceForward": "Forward in workspace",
|
|
41
41
|
"chatWorkspaceClosePanel": "Close workspace panel",
|
|
42
|
+
"chatWorkspaceSideChatDraftTitle": "Side chat",
|
|
43
|
+
"chatWorkspaceSideChatDraftSubtitle": "This draft will inherit the parent session context when you send the first message.",
|
|
42
44
|
"chatWorkspaceChildSessions": "Child sessions",
|
|
43
45
|
"chatWorkspaceOpenFiles": "Open files",
|
|
44
46
|
"chatWorkspacePreview": "Preview",
|
|
@@ -55,9 +57,10 @@
|
|
|
55
57
|
"chatTyping": "Agent is thinking...",
|
|
56
58
|
"chatRuntimeInitializing": "Chat is still initializing. You can keep typing and send once startup finishes.",
|
|
57
59
|
"chatRuntimeInitializationFailed": "Chat startup failed. Please retry in a moment or inspect the service logs.",
|
|
58
|
-
"chatInputPlaceholder": "Type a message,
|
|
60
|
+
"chatInputPlaceholder": "Type a message, / for skills, @ for panel apps, Enter to send, Shift + Enter for newline",
|
|
59
61
|
"chatInputPlaceholderCompact": "Message NextClaw...",
|
|
60
62
|
"chatInputHint": "Multi-turn context is preserved in the current session.",
|
|
63
|
+
"chatSlashSection": "Slash menu",
|
|
61
64
|
"chatSlashSectionCommands": "Commands",
|
|
62
65
|
"chatSlashSectionSkills": "Skills",
|
|
63
66
|
"chatSlashTypeCommand": "Command",
|
|
@@ -67,8 +70,20 @@
|
|
|
67
70
|
"chatSlashLoading": "Loading commands and skills…",
|
|
68
71
|
"chatSlashNoResult": "No matches",
|
|
69
72
|
"chatSlashHint": "Type / to access commands and skills",
|
|
70
|
-
"chatSlashCommandHint": "Press Enter to
|
|
73
|
+
"chatSlashCommandHint": "Press Enter to run this command.",
|
|
74
|
+
"chatSlashCommandSideChatTitle": "Side chat",
|
|
75
|
+
"chatSlashCommandSideChatDescription": "Open a draft side conversation that inherits this session context.",
|
|
76
|
+
"chatSlashCommandSideChatDetail": "Creates the child session only after your first message.",
|
|
71
77
|
"chatSlashSkillHint": "Press Enter to add this skill for the next turn.",
|
|
78
|
+
"chatPanelAppReferenceLoading": "Loading panel apps...",
|
|
79
|
+
"chatPanelAppReferenceSection": "Panel Apps",
|
|
80
|
+
"chatPanelAppReferenceNoResult": "No matching panel apps",
|
|
81
|
+
"chatPanelAppReferenceHint": "Type @ to reference a panel app",
|
|
82
|
+
"chatPanelAppReferenceItemHint": "Press Enter to reference this panel app.",
|
|
83
|
+
"chatPanelAppReferenceType": "Panel App",
|
|
84
|
+
"chatPanelAppReferenceAppId": "App ID",
|
|
85
|
+
"chatPanelAppReferenceFile": "File",
|
|
86
|
+
"chatPanelAppReferenceNoDescription": "No panel app description",
|
|
72
87
|
"chatSkillScopeBuiltin": "Built-in",
|
|
73
88
|
"chatSkillScopeProject": "Project",
|
|
74
89
|
"chatSkillScopeWorkspace": "Workspace",
|
|
@@ -98,6 +113,9 @@
|
|
|
98
113
|
"chatContextCompactionCoveredMessages": "Covered messages",
|
|
99
114
|
"chatContextCompactionOriginalTokens": "Estimated before compaction",
|
|
100
115
|
"chatContextCompactionProjectedTokens": "Estimated after compaction",
|
|
116
|
+
"chatContextInheritanceInherited": "Inherited parent session context",
|
|
117
|
+
"chatContextInheritanceSourceSession": "Source session",
|
|
118
|
+
"chatContextInheritanceMessages": "Inherited messages",
|
|
101
119
|
"chatSending": "Sending...",
|
|
102
120
|
"chatQueueSend": "Queue",
|
|
103
121
|
"chatQueuedHintPrefix": "Queued",
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
"advanced": "Advanced",
|
|
13
13
|
"settings": "Settings",
|
|
14
14
|
"settingsMenu": "Settings menu",
|
|
15
|
+
"sidebarCollapse": "Collapse sidebar",
|
|
16
|
+
"sidebarExpand": "Expand sidebar",
|
|
15
17
|
"security": "Security",
|
|
16
18
|
"backToMain": "Back to Main",
|
|
17
19
|
"enabled": "Enabled",
|
|
@@ -37,8 +39,13 @@
|
|
|
37
39
|
"noneOption": "None",
|
|
38
40
|
"language": "Language",
|
|
39
41
|
"theme": "Theme",
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
+
"themeNatural": "Natural",
|
|
43
|
+
"themeMinimal": "Minimal",
|
|
44
|
+
"themeWarm": "Paper",
|
|
45
|
+
"themeCool": "Mist",
|
|
46
|
+
"themeDawn": "Dawn",
|
|
47
|
+
"themeGraphite": "Graphite",
|
|
48
|
+
"themeProbe": "Probe",
|
|
42
49
|
"isRequired": "is required",
|
|
43
50
|
"duplicate": "duplicate",
|
|
44
51
|
"notFound": "not found",
|
|
@@ -39,6 +39,8 @@
|
|
|
39
39
|
"chatWorkspaceBack": "工作区后退",
|
|
40
40
|
"chatWorkspaceForward": "工作区前进",
|
|
41
41
|
"chatWorkspaceClosePanel": "关闭工作区侧栏",
|
|
42
|
+
"chatWorkspaceSideChatDraftTitle": "Side chat",
|
|
43
|
+
"chatWorkspaceSideChatDraftSubtitle": "发送第一条消息后,会继承父会话上下文并创建真实 child 会话。",
|
|
42
44
|
"chatWorkspaceChildSessions": "子会话",
|
|
43
45
|
"chatWorkspaceOpenFiles": "打开的文件",
|
|
44
46
|
"chatWorkspacePreview": "预览",
|
|
@@ -55,9 +57,10 @@
|
|
|
55
57
|
"chatTyping": "Agent 正在思考...",
|
|
56
58
|
"chatRuntimeInitializing": "聊天能力正在初始化。你可以先输入内容,完成后即可发送。",
|
|
57
59
|
"chatRuntimeInitializationFailed": "聊天能力启动失败,请稍后重试或检查服务日志。",
|
|
58
|
-
"chatInputPlaceholder": "
|
|
60
|
+
"chatInputPlaceholder": "输入消息,/ 选技能,@ 引用面板应用,Enter 发送,Shift + Enter 换行",
|
|
59
61
|
"chatInputPlaceholderCompact": "发消息...",
|
|
60
62
|
"chatInputHint": "支持多轮上下文,默认走当前会话。",
|
|
63
|
+
"chatSlashSection": "斜杠菜单",
|
|
61
64
|
"chatSlashSectionCommands": "命令",
|
|
62
65
|
"chatSlashSectionSkills": "技能",
|
|
63
66
|
"chatSlashTypeCommand": "命令",
|
|
@@ -67,8 +70,20 @@
|
|
|
67
70
|
"chatSlashLoading": "加载命令与技能中…",
|
|
68
71
|
"chatSlashNoResult": "无匹配项",
|
|
69
72
|
"chatSlashHint": "输入 / 触发命令或技能选择",
|
|
70
|
-
"chatSlashCommandHint": "
|
|
73
|
+
"chatSlashCommandHint": "回车执行这个命令。",
|
|
74
|
+
"chatSlashCommandSideChatTitle": "Side chat",
|
|
75
|
+
"chatSlashCommandSideChatDescription": "打开一个继承当前会话上下文的右侧草稿会话。",
|
|
76
|
+
"chatSlashCommandSideChatDetail": "只有发送第一条消息后才会创建真实 child 会话。",
|
|
71
77
|
"chatSlashSkillHint": "回车把该技能加入本轮请求。",
|
|
78
|
+
"chatPanelAppReferenceLoading": "加载面板应用中…",
|
|
79
|
+
"chatPanelAppReferenceSection": "面板应用",
|
|
80
|
+
"chatPanelAppReferenceNoResult": "没有匹配的面板应用",
|
|
81
|
+
"chatPanelAppReferenceHint": "输入 @ 引用面板应用",
|
|
82
|
+
"chatPanelAppReferenceItemHint": "回车引用这个面板应用。",
|
|
83
|
+
"chatPanelAppReferenceType": "面板应用",
|
|
84
|
+
"chatPanelAppReferenceAppId": "应用 ID",
|
|
85
|
+
"chatPanelAppReferenceFile": "文件",
|
|
86
|
+
"chatPanelAppReferenceNoDescription": "暂无面板应用描述",
|
|
72
87
|
"chatSkillScopeBuiltin": "内建",
|
|
73
88
|
"chatSkillScopeProject": "项目",
|
|
74
89
|
"chatSkillScopeWorkspace": "工作区",
|
|
@@ -98,6 +113,9 @@
|
|
|
98
113
|
"chatContextCompactionCoveredMessages": "覆盖消息数",
|
|
99
114
|
"chatContextCompactionOriginalTokens": "压缩前估算",
|
|
100
115
|
"chatContextCompactionProjectedTokens": "压缩后估算",
|
|
116
|
+
"chatContextInheritanceInherited": "已继承父会话上下文",
|
|
117
|
+
"chatContextInheritanceSourceSession": "来源会话",
|
|
118
|
+
"chatContextInheritanceMessages": "继承消息数",
|
|
101
119
|
"chatSending": "发送中...",
|
|
102
120
|
"chatQueueSend": "排队发送",
|
|
103
121
|
"chatQueuedHintPrefix": "当前有",
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
"advanced": "高级",
|
|
13
13
|
"settings": "设置",
|
|
14
14
|
"settingsMenu": "设置菜单",
|
|
15
|
+
"sidebarCollapse": "收起侧边栏",
|
|
16
|
+
"sidebarExpand": "展开侧边栏",
|
|
15
17
|
"security": "安全",
|
|
16
18
|
"backToMain": "返回主界面",
|
|
17
19
|
"enabled": "启用",
|
|
@@ -37,8 +39,13 @@
|
|
|
37
39
|
"noneOption": "无",
|
|
38
40
|
"language": "语言",
|
|
39
41
|
"theme": "主题",
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
+
"themeNatural": "自然",
|
|
43
|
+
"themeMinimal": "简白",
|
|
44
|
+
"themeWarm": "纸暖",
|
|
45
|
+
"themeCool": "雾蓝",
|
|
46
|
+
"themeDawn": "晨砂",
|
|
47
|
+
"themeGraphite": "石墨",
|
|
48
|
+
"themeProbe": "探针",
|
|
42
49
|
"isRequired": "必填",
|
|
43
50
|
"duplicate": "重复",
|
|
44
51
|
"notFound": "未找到",
|
|
@@ -1,80 +1,122 @@
|
|
|
1
|
-
export type UiTheme = 'warm' | 'cool';
|
|
1
|
+
export type UiTheme = 'natural' | 'minimal' | 'warm' | 'cool' | 'dawn' | 'graphite' | 'probe';
|
|
2
2
|
|
|
3
3
|
const THEME_STORAGE_KEY = 'nextclaw.ui.theme';
|
|
4
|
+
const DEFAULT_THEME: UiTheme = 'natural';
|
|
5
|
+
const THEME_VALUES: readonly UiTheme[] = ['natural', 'minimal', 'warm', 'cool', 'dawn', 'graphite', 'probe'];
|
|
4
6
|
|
|
5
7
|
export const THEME_OPTIONS: Array<{ value: UiTheme; labelKey: string }> = [
|
|
8
|
+
{ value: 'natural', labelKey: 'themeNatural' },
|
|
9
|
+
{ value: 'minimal', labelKey: 'themeMinimal' },
|
|
6
10
|
{ value: 'warm', labelKey: 'themeWarm' },
|
|
7
|
-
{ value: 'cool', labelKey: 'themeCool' }
|
|
11
|
+
{ value: 'cool', labelKey: 'themeCool' },
|
|
12
|
+
{ value: 'dawn', labelKey: 'themeDawn' },
|
|
13
|
+
{ value: 'graphite', labelKey: 'themeGraphite' },
|
|
14
|
+
{ value: 'probe', labelKey: 'themeProbe' }
|
|
8
15
|
];
|
|
9
16
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
function isTheme(value: unknown): value is UiTheme {
|
|
15
|
-
return value === 'warm' || value === 'cool';
|
|
16
|
-
}
|
|
17
|
+
export function normalizeTheme(value: unknown): UiTheme | null {
|
|
18
|
+
if (typeof value !== 'string') {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
17
21
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
return;
|
|
22
|
+
if ((THEME_VALUES as readonly string[]).includes(value)) {
|
|
23
|
+
return value as UiTheme;
|
|
21
24
|
}
|
|
22
|
-
|
|
25
|
+
|
|
26
|
+
return value === 'leaf' ? 'warm' : null;
|
|
23
27
|
}
|
|
24
28
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
+
class UiThemeOwner {
|
|
30
|
+
private activeTheme: UiTheme = DEFAULT_THEME;
|
|
31
|
+
private initialized = false;
|
|
32
|
+
private readonly listeners = new Set<(theme: UiTheme) => void>();
|
|
29
33
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
return saved;
|
|
34
|
+
resolveInitialTheme = (): UiTheme => {
|
|
35
|
+
if (typeof window === 'undefined') {
|
|
36
|
+
return DEFAULT_THEME;
|
|
34
37
|
}
|
|
35
|
-
} catch {
|
|
36
|
-
// ignore storage failures
|
|
37
|
-
}
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
try {
|
|
40
|
+
const saved = window.localStorage.getItem(THEME_STORAGE_KEY);
|
|
41
|
+
const theme = normalizeTheme(saved);
|
|
42
|
+
if (theme) {
|
|
43
|
+
return theme;
|
|
44
|
+
}
|
|
45
|
+
} catch {
|
|
46
|
+
// ignore storage failures
|
|
47
|
+
}
|
|
41
48
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
activeTheme = resolveInitialTheme();
|
|
45
|
-
applyThemeAttribute(activeTheme);
|
|
46
|
-
initialized = true;
|
|
47
|
-
}
|
|
48
|
-
return activeTheme;
|
|
49
|
-
}
|
|
49
|
+
return DEFAULT_THEME;
|
|
50
|
+
};
|
|
50
51
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
initializeTheme = (): UiTheme => {
|
|
53
|
+
if (!this.initialized) {
|
|
54
|
+
this.activeTheme = this.resolveInitialTheme();
|
|
55
|
+
this.applyThemeAttribute(this.activeTheme);
|
|
56
|
+
this.initialized = true;
|
|
57
|
+
}
|
|
58
|
+
return this.activeTheme;
|
|
59
|
+
};
|
|
54
60
|
|
|
55
|
-
|
|
56
|
-
initializeTheme();
|
|
57
|
-
if (theme === activeTheme) {
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
61
|
+
getTheme = (): UiTheme => this.initialized ? this.activeTheme : this.initializeTheme();
|
|
60
62
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
+
setTheme = (theme: UiTheme): void => {
|
|
64
|
+
this.initializeTheme();
|
|
65
|
+
if (theme === this.activeTheme) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
this.activeTheme = theme;
|
|
70
|
+
this.applyThemeAttribute(this.activeTheme);
|
|
71
|
+
this.saveTheme(this.activeTheme);
|
|
72
|
+
this.listeners.forEach((listener) => listener(this.activeTheme));
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
subscribeThemeChange = (listener: (theme: UiTheme) => void): (() => void) => {
|
|
76
|
+
this.listeners.add(listener);
|
|
77
|
+
return () => {
|
|
78
|
+
this.listeners.delete(listener);
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
private applyThemeAttribute = (theme: UiTheme): void => {
|
|
83
|
+
if (typeof document === 'undefined') {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
document.documentElement.setAttribute('data-theme', theme);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
private saveTheme = (theme: UiTheme): void => {
|
|
90
|
+
if (typeof window === 'undefined') {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
63
93
|
|
|
64
|
-
if (typeof window !== 'undefined') {
|
|
65
94
|
try {
|
|
66
|
-
window.localStorage.setItem(THEME_STORAGE_KEY,
|
|
95
|
+
window.localStorage.setItem(THEME_STORAGE_KEY, theme);
|
|
67
96
|
} catch {
|
|
68
97
|
// ignore storage failures
|
|
69
98
|
}
|
|
70
|
-
}
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const uiThemeOwner = new UiThemeOwner();
|
|
71
103
|
|
|
72
|
-
|
|
104
|
+
export function resolveInitialTheme(): UiTheme {
|
|
105
|
+
return uiThemeOwner.resolveInitialTheme();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function initializeTheme(): UiTheme {
|
|
109
|
+
return uiThemeOwner.initializeTheme();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function getTheme(): UiTheme {
|
|
113
|
+
return uiThemeOwner.getTheme();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function setTheme(theme: UiTheme): void {
|
|
117
|
+
uiThemeOwner.setTheme(theme);
|
|
73
118
|
}
|
|
74
119
|
|
|
75
120
|
export function subscribeThemeChange(listener: (theme: UiTheme) => void): () => void {
|
|
76
|
-
|
|
77
|
-
return () => {
|
|
78
|
-
listeners.delete(listener);
|
|
79
|
-
};
|
|
121
|
+
return uiThemeOwner.subscribeThemeChange(listener);
|
|
80
122
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { setLanguage } from "@/shared/lib/i18n";
|
|
3
|
+
import { resolveUiDocumentTitle } from "@/shared/lib/ui-document-title";
|
|
4
|
+
|
|
5
|
+
type TestLocation = Pick<Location, "host" | "hostname" | "port">;
|
|
6
|
+
|
|
7
|
+
function testLocation(host: string, hostname: string, port = ""): TestLocation {
|
|
8
|
+
return { host, hostname, port };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
describe("resolveUiDocumentTitle", () => {
|
|
12
|
+
it("keeps the previous title shape when the browser location is unavailable", () => {
|
|
13
|
+
setLanguage("en");
|
|
14
|
+
|
|
15
|
+
expect(resolveUiDocumentTitle("/chat")).toBe("NextClaw - Chat");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("uses only the port for localhost instances", () => {
|
|
19
|
+
setLanguage("en");
|
|
20
|
+
|
|
21
|
+
expect(
|
|
22
|
+
resolveUiDocumentTitle(
|
|
23
|
+
"/chat",
|
|
24
|
+
testLocation("localhost:5173", "localhost", "5173"),
|
|
25
|
+
),
|
|
26
|
+
).toBe("NextClaw 5173 - Chat");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("uses only the port for loopback IP instances", () => {
|
|
30
|
+
setLanguage("en");
|
|
31
|
+
|
|
32
|
+
expect(
|
|
33
|
+
resolveUiDocumentTitle(
|
|
34
|
+
"/model",
|
|
35
|
+
testLocation("127.0.0.1:3000", "127.0.0.1", "3000"),
|
|
36
|
+
),
|
|
37
|
+
).toBe("NextClaw 3000 - Model Configuration");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("uses the full host for non-local instances", () => {
|
|
41
|
+
setLanguage("en");
|
|
42
|
+
const title = resolveUiDocumentTitle(
|
|
43
|
+
"/providers",
|
|
44
|
+
testLocation("nextclaw.example.com:8443", "nextclaw.example.com", "8443"),
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
expect(title).toBe("NextClaw nextclaw.example.com:8443 - AI Providers");
|
|
48
|
+
});
|
|
49
|
+
});
|