From cf7d874092249829c47a459c2c2e51705d053454 Mon Sep 17 00:00:00 2001
From: kev1n77
Date: Fri, 28 Aug 2026 20:28:24 +0800
Subject: [PATCH 1/9] feat(web-ui): adopt SegmentedControl for exclusive
view-mode switchers
Replace the custom two-option diff type switcher (GitDiffView), the markdown editor view-mode button pairs, and the font size preset button group (FontPreferencePanel) with the design-system SegmentedControl. Retire the corresponding appearance parts and migrate persisted appearance packages that still reference them.
---
docs/development/ui-testids-CN.md | 3 +-
docs/development/ui-testids.md | 3 +-
.../compiler/AppearanceCompiler.test.ts | 4 +-
.../schema/migrateAppearancePackage.ts | 4 +-
.../font-preference/appearance.ts | 6 -
.../components/FontPreferencePanel.scss | 50 -----
.../components/FontPreferencePanel.tsx | 188 ++++++++----------
.../components/MarkdownEditor.appearance.ts | 2 +-
.../editor/components/MarkdownEditor.scss | 4 +-
.../editor/components/MarkdownEditor.tsx | 62 ++----
.../GitDiffView/GitDiffView.appearance.ts | 3 +-
.../components/GitDiffView/GitDiffView.scss | 37 ----
.../components/GitDiffView/GitDiffView.tsx | 31 +--
13 files changed, 128 insertions(+), 269 deletions(-)
diff --git a/docs/development/ui-testids-CN.md b/docs/development/ui-testids-CN.md
index fb7209f07e..f5e88175ab 100644
--- a/docs/development/ui-testids-CN.md
+++ b/docs/development/ui-testids-CN.md
@@ -270,8 +270,7 @@
| Appearance 语言选项 | `appearance-language-option` | 重复的语言下拉选项。包含 `data-locale-id`,并带有 Select 组件提供的 `data-selected`。 |
| Appearance 主题选择器 | `appearance-theme-select` | Appearance 中 theme Select 的真实触发节点。 |
| Appearance 外观选项 | `appearance-palette-option` | 重复的外观下拉选项。包含 `data-appearance-id`,并带有 Select 组件提供的 `data-selected`。 |
-| Appearance UI 字号分组 | `appearance-ui-font-level-group` | UI font size 预置级别按钮组根节点。 |
-| Appearance UI 字号按钮 | `appearance-ui-font-level-btn` | 重复的 UI font size 预置级别按钮。包含 `data-font-level` 和 `data-selected`。 |
+| Appearance UI 字号分组 | `appearance-ui-font-level-group` | UI font size 预置级别控件根节点。预置级别渲染为设计系统 SegmentedControl 分段,可通过 `[data-bf-part="segment"][data-bf-value=""]` 定位,选中分段带 `aria-checked="true"`。 |
| Appearance UI 自定义字号控制区 | `appearance-ui-font-custom-controls` | custom UI 字号控制区根节点,仅在 custom 激活时渲染。 |
| Appearance UI 自定义字号输入框 | `appearance-ui-font-custom-input` | custom UI 字号 px 输入框。包含 `data-font-level="custom"`。 |
| Appearance UI 自定义字号减一按钮 | `appearance-ui-font-custom-step-minus` | custom UI 字号减一按钮。 |
diff --git a/docs/development/ui-testids.md b/docs/development/ui-testids.md
index 80689b39fa..343a6ff5a3 100644
--- a/docs/development/ui-testids.md
+++ b/docs/development/ui-testids.md
@@ -270,8 +270,7 @@ Avoid adding IDs to these surfaces unless there is a clear automated workflow.
| Appearance language option | `appearance-language-option` | Repeated language dropdown option. Includes `data-locale-id` and Select-provided `data-selected`. |
| Appearance theme select | `appearance-theme-select` | Theme select trigger in Appearance settings. |
| Appearance palette option | `appearance-palette-option` | Repeated appearance dropdown option. Includes `data-appearance-id` and Select-provided `data-selected`. |
-| Appearance UI font level group | `appearance-ui-font-level-group` | UI font preset button group root. |
-| Appearance UI font level button | `appearance-ui-font-level-btn` | Repeated preset button. Includes `data-font-level` and `data-selected`. |
+| Appearance UI font level group | `appearance-ui-font-level-group` | UI font preset control root. Presets render as design-system segmented control segments; target one via `[data-bf-part="segment"][data-bf-value=""]`, selected segment has `aria-checked="true"`. |
| Appearance UI font custom controls | `appearance-ui-font-custom-controls` | Custom UI font px controls root, rendered when custom is active. |
| Appearance UI font custom input | `appearance-ui-font-custom-input` | Custom UI font px number input. Includes `data-font-level="custom"`. |
| Appearance UI font custom step minus | `appearance-ui-font-custom-step-minus` | Custom UI font px decrement button. |
diff --git a/src/web-ui/src/infrastructure/appearance/compiler/AppearanceCompiler.test.ts b/src/web-ui/src/infrastructure/appearance/compiler/AppearanceCompiler.test.ts
index 372c5e9ff9..d08adf052a 100644
--- a/src/web-ui/src/infrastructure/appearance/compiler/AppearanceCompiler.test.ts
+++ b/src/web-ui/src/infrastructure/appearance/compiler/AppearanceCompiler.test.ts
@@ -900,7 +900,7 @@ describe('AppearanceCompiler', () => {
parts: { commit: { states: { expanded: { borderColor: accent } } } },
},
'git-diff-view': {
- parts: { typeOption: { states: { active: { backgroundColor: accent } } } },
+ parts: { file: { states: { expanded: { backgroundColor: accent } } } },
},
'git-settings-view': {
parts: { status: { states: { error: { borderColor: accent } } } },
@@ -980,7 +980,7 @@ describe('AppearanceCompiler', () => {
expect(snapshot.cssText).toContain('[data-bf-component="session-file-modifications-bar"][data-bf-part="file"][data-bf-operation="edit"]');
expect(snapshot.cssText).toContain('[data-bf-component="editor-breadcrumb"][data-bf-part="item"][data-bf-state~="active"]');
expect(snapshot.cssText).toContain('[data-bf-component="git-branch-history"][data-bf-part="commit"][data-bf-state~="expanded"]');
- expect(snapshot.cssText).toContain('[data-bf-component="git-diff-view"][data-bf-part="typeOption"][data-bf-state~="active"]');
+ expect(snapshot.cssText).toContain('[data-bf-component="git-diff-view"][data-bf-part="file"][data-bf-state~="expanded"]');
expect(snapshot.cssText).toContain('[data-bf-component="git-settings-view"][data-bf-part="status"][data-bf-state~="error"]');
});
});
diff --git a/src/web-ui/src/infrastructure/appearance/schema/migrateAppearancePackage.ts b/src/web-ui/src/infrastructure/appearance/schema/migrateAppearancePackage.ts
index 60054b8b42..618afbfa74 100644
--- a/src/web-ui/src/infrastructure/appearance/schema/migrateAppearancePackage.ts
+++ b/src/web-ui/src/infrastructure/appearance/schema/migrateAppearancePackage.ts
@@ -31,8 +31,10 @@ const RETIRED_COMPONENT_PARTS: Readonly>> = {
'context-list': new Set(['clear']),
'copy-output-button': new Set(['action', 'icon', 'text']),
'create-agent-page': new Set(['back']),
- 'font-preference': new Set(['resetButton']),
+ 'font-preference': new Set(['resetButton', 'levelGroup', 'levelButton']),
+ 'git-diff-view': new Set(['typeSwitcher', 'typeOption']),
'image-analysis-card': new Set(['expand']),
+ 'markdown-editor': new Set(['modeToggle']),
'mini-app-tool-display': new Set(['open']),
'peer-device': new Set(['switcherDisconnect']),
'review-session-summary-card': new Set(['open']),
diff --git a/src/web-ui/src/infrastructure/font-preference/appearance.ts b/src/web-ui/src/infrastructure/font-preference/appearance.ts
index b096d7a7fd..ba07d1b79e 100644
--- a/src/web-ui/src/infrastructure/font-preference/appearance.ts
+++ b/src/web-ui/src/infrastructure/font-preference/appearance.ts
@@ -4,19 +4,13 @@ export const fontPreferenceAppearanceDescriptor: AppearanceSurfaceDescriptor = {
id: 'font-preference',
parts: [
{ id: 'root' },
- { id: 'levelGroup' },
- { id: 'levelButton' },
{ id: 'customControls' },
{ id: 'numberInput' },
{ id: 'error' },
{ id: 'preview' },
{ id: 'flowChatControls' },
],
- facets: [
- { id: 'level', attribute: 'data-bf-level', values: ['compact', 'small', 'default', 'medium', 'large', 'custom'] },
- ],
states: [
- { id: 'selected', selector: { kind: 'self', suffix: '[data-bf-state~="selected"]' } },
{ id: 'error', selector: { kind: 'self', suffix: '[data-bf-state~="error"]' } },
],
};
diff --git a/src/web-ui/src/infrastructure/font-preference/components/FontPreferencePanel.scss b/src/web-ui/src/infrastructure/font-preference/components/FontPreferencePanel.scss
index 136afe1050..e9295f5b41 100644
--- a/src/web-ui/src/infrastructure/font-preference/components/FontPreferencePanel.scss
+++ b/src/web-ui/src/infrastructure/font-preference/components/FontPreferencePanel.scss
@@ -1,5 +1,4 @@
@use '../../../component-library/styles/tokens.scss' as *;
-@use '../../../component-library/styles/btn-primary-tokens.scss' as btn-primary;
.font-pref-panel {
/* 界面字体:多行区块,标题与说明之间略松一点 */
@@ -40,15 +39,6 @@
min-width: 0;
}
- /* 第六档「自定义」与右侧步进器保持同一行,整组可随前置按钮换行 */
- &__custom-segment-inline {
- display: inline-flex;
- flex-wrap: nowrap;
- align-items: center;
- gap: $size-gap-2;
- min-width: 0;
- }
-
&__custom-controls {
display: inline-flex;
flex-wrap: nowrap;
@@ -98,46 +88,6 @@
}
}
- &__level-btn {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- min-height: 30px;
- min-width: 0;
- padding: $size-gap-1 $size-gap-3;
- font-size: var(--bf-appearance-token-font-size-sm);
- font-family: var(--bf-appearance-token-font-family-sans);
- background: transparent;
- border: 1px solid var(--bf-appearance-token-border-base);
- border-radius: $size-radius-sm;
- color: var(--bf-appearance-token-color-text-secondary);
- cursor: pointer;
- transition: background $motion-fast $easing-standard,
- border-color $motion-fast $easing-standard,
- color $motion-fast $easing-standard,
- box-shadow $motion-fast $easing-standard;
- white-space: nowrap;
-
- &:hover:not(:disabled):not(&--active) {
- background: var(--bf-appearance-token-element-bg-subtle);
- border-color: var(--bf-appearance-token-border-strong);
- color: var(--bf-appearance-token-color-text-primary);
- }
-
- &--active {
- @include btn-primary.btn-primary-surface-default;
- border-color: var(--bf-appearance-token-btn-primary-border);
- font-weight: $font-weight-semibold;
- box-shadow: var(--bf-appearance-token-btn-primary-shadow);
-
- &:hover:not(:disabled) {
- @include btn-primary.btn-primary-surface-hover;
- border-color: var(--bf-appearance-token-btn-primary-hover-border);
- box-shadow: var(--bf-appearance-token-btn-primary-hover-shadow);
- }
- }
- }
-
&__level-label {
display: block;
line-height: 1.15;
diff --git a/src/web-ui/src/infrastructure/font-preference/components/FontPreferencePanel.tsx b/src/web-ui/src/infrastructure/font-preference/components/FontPreferencePanel.tsx
index c47bc8a5c3..193a4fc750 100644
--- a/src/web-ui/src/infrastructure/font-preference/components/FontPreferencePanel.tsx
+++ b/src/web-ui/src/infrastructure/font-preference/components/FontPreferencePanel.tsx
@@ -1,4 +1,11 @@
-import { Button, Select, Switch, type SelectOption } from '@bitfun/ui';
+import {
+ Button,
+ SegmentedControl,
+ Select,
+ Switch,
+ type SegmentedControlOption,
+ type SelectOption,
+} from '@bitfun/ui';
import React, { useMemo, useState, useCallback, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { ConfigPageRow, ConfigPageSection } from '@/infrastructure/config/components/common';
@@ -89,6 +96,34 @@ export function FontPreferencePanel() {
return !isNaN(n) && n >= 12 && n <= 20 ? n : 14;
})();
+ const levelOptions = useMemo(
+ () => [
+ ...UI_LEVELS.map((l) => ({
+ value: l,
+ label: (
+
+ {t(`appearance.fontSize.levels.${l}`)}
+
+ ),
+ })),
+ {
+ value: 'custom',
+ label: (
+
+ {t('appearance.fontSize.levels.custom')}
+
+ ),
+ },
+ ],
+ [t, customLevelLabelPx]
+ );
+
const fcIndependent = preference.flowChat.mode === 'independent';
const flowChatPxValue = (() => {
const n = parseInt(fcBaseInput, 10);
@@ -152,111 +187,62 @@ export function FontPreferencePanel() {
- {UI_LEVELS.map((l) => (
-
- ))}
-
-
{customError && (
diff --git a/src/web-ui/src/tools/editor/components/MarkdownEditor.appearance.ts b/src/web-ui/src/tools/editor/components/MarkdownEditor.appearance.ts
index 01e21b474f..2da695f3d5 100644
--- a/src/web-ui/src/tools/editor/components/MarkdownEditor.appearance.ts
+++ b/src/web-ui/src/tools/editor/components/MarkdownEditor.appearance.ts
@@ -4,7 +4,7 @@ export const markdownEditorAppearanceDescriptor: AppearanceSurfaceDescriptor = {
id: 'markdown-editor',
parts: [
{ id: 'root' }, { id: 'loading' }, { id: 'error' },
- { id: 'toolbar' }, { id: 'modeToggle' }, { id: 'actions' }, { id: 'body' },
+ { id: 'toolbar' }, { id: 'actions' }, { id: 'body' },
],
facets: [{ id: 'view', attribute: 'data-bf-view', values: ['preview', 'markdown', 'source'] }],
states: [
diff --git a/src/web-ui/src/tools/editor/components/MarkdownEditor.scss b/src/web-ui/src/tools/editor/components/MarkdownEditor.scss
index 69b8a1b055..041d87707e 100644
--- a/src/web-ui/src/tools/editor/components/MarkdownEditor.scss
+++ b/src/web-ui/src/tools/editor/components/MarkdownEditor.scss
@@ -26,9 +26,7 @@
}
.bitfun-markdown-editor__mode-toggle {
- display: inline-flex;
- align-items: center;
- gap: 4px;
+ flex-shrink: 0;
}
.bitfun-markdown-editor__toolbar-actions {
diff --git a/src/web-ui/src/tools/editor/components/MarkdownEditor.tsx b/src/web-ui/src/tools/editor/components/MarkdownEditor.tsx
index d9591690d4..d619510c92 100644
--- a/src/web-ui/src/tools/editor/components/MarkdownEditor.tsx
+++ b/src/web-ui/src/tools/editor/components/MarkdownEditor.tsx
@@ -5,7 +5,7 @@
* @module components/MarkdownEditor
*/
-import { Button, IconButton } from '@bitfun/ui';
+import { Button, IconButton, SegmentedControl } from '@bitfun/ui';
import React, { useEffect, useState, useCallback, useRef } from 'react';
import { MEditor } from '../meditor';
import type { EditorInstance } from '../meditor';
@@ -652,26 +652,16 @@ const MarkdownEditor: React.FC
= ({
return (
-
- setUnsafeViewMode('source')}
- aria-pressed={unsafeViewMode === 'source'}
- >
- {t('editor.markdownEditor.markdown')}
-
- setUnsafeViewMode('preview')}
- aria-pressed={unsafeViewMode === 'preview'}
- >
- {t('editor.markdownEditor.preview')}
-
-
+
setUnsafeViewMode(value as 'source' | 'preview')}
+ />
= ({
return (
-
- setViewMode('preview')}
- aria-pressed={viewMode === 'preview'}
- >
- {t('editor.markdownEditor.preview')}
-
- setViewMode('markdown')}
- aria-pressed={viewMode === 'markdown'}
- >
- {t('editor.markdownEditor.markdown')}
-
-
+
setViewMode(value as 'preview' | 'markdown')}
+ />
= ({
)}
{!sourceCommit && !targetCommit && (
-
- setCurrentShowStaged(false)}
- >
- {t('diffView.workingTree')}
-
- setCurrentShowStaged(true)}
- >
- {t('diffView.staged')}
-
-
+ setCurrentShowStaged(value === 'staged')}
+ />
)}
{loading && (
From 6fd86b4af3cf964eb2dd1bbe92c0a76c96ac933c Mon Sep 17 00:00:00 2001
From: kev1n77
Date: Fri, 28 Aug 2026 20:28:49 +0800
Subject: [PATCH 2/9] feat(web-ui): render market account dropdown with
design-system Menu
Replace the hand-rolled menu surface and menu item in MarketAccountControls with Menu/MenuItem, keep only positioning overrides in SCSS, retire the menu/menuItem appearance parts, and migrate persisted appearance packages.
---
.../market-account/MarketAccountControls.scss | 36 +++----------------
.../MarketAccountControls.test.tsx | 4 +++
.../market-account/MarketAccountControls.tsx | 21 ++++-------
.../src/features/market-account/appearance.ts | 2 --
.../schema/migrateAppearancePackage.ts | 1 +
5 files changed, 17 insertions(+), 47 deletions(-)
diff --git a/src/web-ui/src/features/market-account/MarketAccountControls.scss b/src/web-ui/src/features/market-account/MarketAccountControls.scss
index 2e7d8a35e6..320d13cbd9 100644
--- a/src/web-ui/src/features/market-account/MarketAccountControls.scss
+++ b/src/web-ui/src/features/market-account/MarketAccountControls.scss
@@ -1,5 +1,4 @@
@use '../../component-library/styles/tokens' as *;
-@use '../../component-library/styles/overlay-surfaces' as surfaces;
.market-account-controls {
position: relative;
@@ -48,17 +47,14 @@
}
}
+ /* Positioning-only overrides; the design-system Menu owns the surface look. */
&__menu {
- @include surfaces.floating-surface;
-
position: fixed;
z-index: $z-popover;
- width: max-content;
- min-width: min(190px, calc(100vw - 16px));
- max-width: min(230px, calc(100vw - 16px));
- max-height: calc(100vh - 16px);
- overflow-y: auto;
- padding: $size-gap-2;
+ inline-size: max-content;
+ min-inline-size: min(190px, calc(100vw - 16px));
+ max-inline-size: min(230px, calc(100vw - 16px));
+ max-block-size: calc(100vh - 16px);
}
&__profile {
@@ -94,28 +90,6 @@
}
}
- &__menu-item {
- display: flex;
- align-items: center;
- gap: $size-gap-2;
- width: 100%;
- margin-top: $size-gap-1;
- padding: $size-gap-2;
- border: 0;
- border-radius: $size-radius-sm;
- background: transparent;
- color: var(--bf-appearance-token-color-text-secondary);
- font-size: var(--bf-appearance-token-font-size-sm);
- text-align: left;
- cursor: pointer;
-
- &:hover,
- &:focus-visible {
- background: var(--bf-appearance-token-element-bg-subtle);
- color: var(--bf-appearance-token-color-text-primary);
- }
- }
-
&__spinner {
animation: market-account-spin 0.85s linear infinite;
}
diff --git a/src/web-ui/src/features/market-account/MarketAccountControls.test.tsx b/src/web-ui/src/features/market-account/MarketAccountControls.test.tsx
index 38a9370f82..a789f2925d 100644
--- a/src/web-ui/src/features/market-account/MarketAccountControls.test.tsx
+++ b/src/web-ui/src/features/market-account/MarketAccountControls.test.tsx
@@ -47,6 +47,10 @@ vi.mock('@/shared/notification-system', () => ({
vi.mock('@bitfun/ui', () => ({
Button: ({ children, ...props }: any) => {children},
+ Menu: ({ children, ...props }: any) => {children}
,
+ MenuItem: ({ children, leading, ...props }: any) => (
+ {leading}{children}
+ ),
Modal: ({ isOpen, title, children }: any) => isOpen ? (
) : null,
diff --git a/src/web-ui/src/features/market-account/MarketAccountControls.tsx b/src/web-ui/src/features/market-account/MarketAccountControls.tsx
index 602cb15d2e..6d566b2ec1 100644
--- a/src/web-ui/src/features/market-account/MarketAccountControls.tsx
+++ b/src/web-ui/src/features/market-account/MarketAccountControls.tsx
@@ -1,4 +1,4 @@
-import { Button, Modal } from '@bitfun/ui';
+import { Button, Menu, MenuItem, Modal } from '@bitfun/ui';
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { ChevronDown, Github, Loader2, LogOut } from 'lucide-react';
@@ -181,12 +181,10 @@ export function MarketAccountControls({
{menuOpen && createPortal(
- {t('market.account.githubAccount')}
- }
onClick={() => void signOut()}
>
-
{t('market.signOut')}
-
- ,
+
+ ,
getAppearanceOverlayHost(),
)}
diff --git a/src/web-ui/src/features/market-account/appearance.ts b/src/web-ui/src/features/market-account/appearance.ts
index 146fbab90c..0b208ee115 100644
--- a/src/web-ui/src/features/market-account/appearance.ts
+++ b/src/web-ui/src/features/market-account/appearance.ts
@@ -5,9 +5,7 @@ export const marketAccountControlsAppearanceDescriptor: AppearanceSurfaceDescrip
parts: [
{ id: 'root' },
{ id: 'identityTrigger' },
- { id: 'menu' },
{ id: 'profile' },
- { id: 'menuItem' },
{ id: 'login' },
{ id: 'waiting' },
{ id: 'error' },
diff --git a/src/web-ui/src/infrastructure/appearance/schema/migrateAppearancePackage.ts b/src/web-ui/src/infrastructure/appearance/schema/migrateAppearancePackage.ts
index 618afbfa74..445ebf9909 100644
--- a/src/web-ui/src/infrastructure/appearance/schema/migrateAppearancePackage.ts
+++ b/src/web-ui/src/infrastructure/appearance/schema/migrateAppearancePackage.ts
@@ -35,6 +35,7 @@ const RETIRED_COMPONENT_PARTS: Readonly>> = {
'git-diff-view': new Set(['typeSwitcher', 'typeOption']),
'image-analysis-card': new Set(['expand']),
'markdown-editor': new Set(['modeToggle']),
+ 'market-account-controls': new Set(['menu', 'menuItem']),
'mini-app-tool-display': new Set(['open']),
'peer-device': new Set(['switcherDisconnect']),
'review-session-summary-card': new Set(['open']),
From 08996e59110391835800d3d9ca364227e29bfa17 Mon Sep 17 00:00:00 2001
From: kev1n77
Date: Fri, 28 Aug 2026 20:29:11 +0800
Subject: [PATCH 3/9] feat(web-ui): rebuild image viewer toolbar on
design-system Toolbar
Compose the image viewer header from Toolbar/ToolbarGroup/ToolbarSeparator with IconButton and Button controls, drop the bespoke toolbar button styles, retire the toolbar/controls/action appearance parts, and migrate persisted appearance packages.
---
.../schema/migrateAppearancePackage.ts | 1 +
.../components/ImageViewer.appearance.ts | 4 +-
.../tools/editor/components/ImageViewer.scss | 62 +------
.../tools/editor/components/ImageViewer.tsx | 171 +++++++++---------
4 files changed, 94 insertions(+), 144 deletions(-)
diff --git a/src/web-ui/src/infrastructure/appearance/schema/migrateAppearancePackage.ts b/src/web-ui/src/infrastructure/appearance/schema/migrateAppearancePackage.ts
index 445ebf9909..a59c778c1f 100644
--- a/src/web-ui/src/infrastructure/appearance/schema/migrateAppearancePackage.ts
+++ b/src/web-ui/src/infrastructure/appearance/schema/migrateAppearancePackage.ts
@@ -34,6 +34,7 @@ const RETIRED_COMPONENT_PARTS: Readonly>> = {
'font-preference': new Set(['resetButton', 'levelGroup', 'levelButton']),
'git-diff-view': new Set(['typeSwitcher', 'typeOption']),
'image-analysis-card': new Set(['expand']),
+ 'image-viewer': new Set(['toolbar', 'controls', 'action']),
'markdown-editor': new Set(['modeToggle']),
'market-account-controls': new Set(['menu', 'menuItem']),
'mini-app-tool-display': new Set(['open']),
diff --git a/src/web-ui/src/tools/editor/components/ImageViewer.appearance.ts b/src/web-ui/src/tools/editor/components/ImageViewer.appearance.ts
index 04e4117956..8a5a432da6 100644
--- a/src/web-ui/src/tools/editor/components/ImageViewer.appearance.ts
+++ b/src/web-ui/src/tools/editor/components/ImageViewer.appearance.ts
@@ -2,8 +2,8 @@ import type { AppearanceSurfaceDescriptor } from '@/infrastructure/appearance';
export const imageViewerAppearanceDescriptor: AppearanceSurfaceDescriptor = {
id: 'image-viewer',
parts: [
- { id: 'root' }, { id: 'toolbar' }, { id: 'info' }, { id: 'controls' },
- { id: 'action' }, { id: 'container' }, { id: 'loading' }, { id: 'error' },
+ { id: 'root' }, { id: 'info' },
+ { id: 'container' }, { id: 'loading' }, { id: 'error' },
{ id: 'imageWrapper' }, { id: 'image' },
],
states: [{ id: 'fullscreen', selector: { kind: 'self', suffix: '[data-bf-state~="fullscreen"]' } }],
diff --git a/src/web-ui/src/tools/editor/components/ImageViewer.scss b/src/web-ui/src/tools/editor/components/ImageViewer.scss
index 5f9e70e805..4fd4de2570 100644
--- a/src/web-ui/src/tools/editor/components/ImageViewer.scss
+++ b/src/web-ui/src/tools/editor/components/ImageViewer.scss
@@ -24,15 +24,8 @@
background: var(--bf-appearance-token-color-bg-primary);
}
- // Toolbar
+ // Toolbar layout comes from the design-system Toolbar; keep it pinned to the top.
&__toolbar {
- display: flex;
- align-items: center;
- gap: $size-gap-2;
- padding: $size-gap-2 $size-gap-3;
- background: var(--bf-appearance-token-color-bg-primary);
- border-bottom: 1px solid var(--bf-appearance-token-border-base);
- min-height: 32px;
flex-shrink: 0;
}
@@ -60,57 +53,8 @@
white-space: nowrap;
}
- &__controls {
- display: flex;
- align-items: center;
- gap: $size-gap-2;
- flex-shrink: 0;
- margin-left: auto;
- }
-
- // Button Styles
- &__btn {
- display: flex;
- align-items: center;
- justify-content: center;
- width: 24px;
- height: 24px;
- padding: 0;
- background: transparent;
- border: none;
- color: var(--bf-appearance-token-color-text-secondary);
- cursor: pointer;
- transition: color $motion-base $easing-standard;
-
- &:hover:not(:disabled) {
- color: var(--bf-appearance-token-color-text-primary);
- }
-
- &:disabled {
- opacity: $opacity-disabled;
- cursor: default;
- }
-
- svg {
- width: 14px;
- height: 14px;
- flex-shrink: 0;
- }
-
- &--zoom-display {
- width: auto;
- min-width: 40px;
- padding: 0 $size-gap-1;
- font-size: var(--bf-appearance-token-font-size-xs);
- font-weight: $font-weight-medium;
- }
- }
-
- &__divider {
- width: 1px;
- height: 20px;
- background: var(--bf-appearance-token-border-base);
- margin: 0 $size-gap-1;
+ &__zoom-display {
+ min-width: 40px;
}
// Image Container
diff --git a/src/web-ui/src/tools/editor/components/ImageViewer.tsx b/src/web-ui/src/tools/editor/components/ImageViewer.tsx
index da67e09de3..91a92b8a85 100644
--- a/src/web-ui/src/tools/editor/components/ImageViewer.tsx
+++ b/src/web-ui/src/tools/editor/components/ImageViewer.tsx
@@ -7,6 +7,7 @@
import React, { useState, useEffect, useCallback } from 'react';
import { ZoomIn, ZoomOut, RotateCw, Download, Maximize2 } from 'lucide-react';
+import { Button, IconButton, Toolbar, ToolbarGroup, ToolbarSeparator } from '@bitfun/ui';
import { createLogger } from '@/shared/utils/logger';
import { Tooltip } from '@/component-library';
import { useI18n } from '@/infrastructure/i18n';
@@ -153,89 +154,93 @@ export const ImageViewer: React.FC = ({
data-bf-part="root"
data-bf-state={isFullscreen ? 'fullscreen' : undefined}
>
-
-
- {fileName || filePath.split(/[/\\]/).pop()}
- {imageDimensions && (
-
- {imageDimensions.width} × {imageDimensions.height}
-
- )}
- {fileSize > 0 && (
-
- {formatFileSize(fileSize)}
-
- )}
-
-
-
-
-
-
-
-
-
- {zoom}%
-
-
-
- = 500}
- >
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+ {fileName || filePath.split(/[/\\]/).pop()}
+ {imageDimensions && (
+
+ {imageDimensions.width} × {imageDimensions.height}
+
+ )}
+ {fileSize > 0 && (
+
+ {formatFileSize(fileSize)}
+
+ )}
+
+ }
+ trailing={
+ <>
+
+
+ }
+ onClick={handleZoomOut}
+ disabled={zoom <= 25}
+ />
+
+
+
+ {zoom}%
+
+
+
+ }
+ onClick={handleZoomIn}
+ disabled={zoom >= 500}
+ />
+
+
+
+
+
+ }
+ onClick={handleRotate}
+ />
+
+
+ }
+ onClick={handleDownload}
+ />
+
+
+ }
+ onClick={handleToggleFullscreen}
+ />
+
+
+ >
+ }
+ />
{loading && (
From 4433fb127679e586c2c1d5ca31ca1bdec545e7c5 Mon Sep 17 00:00:00 2001
From: kev1n77
Date: Fri, 28 Aug 2026 21:13:38 +0800
Subject: [PATCH 4/9] feat(design-system): add danger tone to ActionItem for
destructive menu rows
Destructive actions such as close/delete/reset need a semantic danger treatment inside Menu lists. Expose tone=neutral|danger on ActionItem (inherited by MenuItem), style it with the shared status danger tokens, and cover the contract in tests.
---
.../src/components/ActionItem/ActionItem.meta.ts | 5 ++++-
.../components/ActionItem/ActionItem.module.css | 14 ++++++++++++++
.../ui/src/components/ActionItem/ActionItem.tsx | 5 +++++
.../packages/ui/src/components/ActionItem/index.ts | 1 +
design-system/packages/ui/src/index.ts | 1 +
.../packages/ui/tests/action-item.test.mjs | 12 ++++++++++++
6 files changed, 37 insertions(+), 1 deletion(-)
diff --git a/design-system/packages/ui/src/components/ActionItem/ActionItem.meta.ts b/design-system/packages/ui/src/components/ActionItem/ActionItem.meta.ts
index 8c9c90cf13..06b9fe7043 100644
--- a/design-system/packages/ui/src/components/ActionItem/ActionItem.meta.ts
+++ b/design-system/packages/ui/src/components/ActionItem/ActionItem.meta.ts
@@ -13,8 +13,9 @@ export const actionItemMeta = {
{ name: "shortcut", type: "ReactNode" },
{ defaultValue: "[]", name: "actions", type: "readonly ActionItemAction[]" },
{ defaultValue: "false", name: "disabled", type: "boolean" },
+ { defaultValue: "neutral", name: "tone", type: "neutral | danger" },
],
- states: ["default", "hover", "active", "focus-visible", "disabled"],
+ states: ["default", "hover", "active", "focus-visible", "disabled", "danger"],
tokens: [
"color.action.neutral.content",
"color.action.neutral.contentDisabled",
@@ -22,6 +23,8 @@ export const actionItemMeta = {
"color.action.neutral.surfacePressed",
"color.content.muted",
"color.focus.ring",
+ "color.status.danger.content",
+ "color.status.danger.surface",
"control.height.sm",
"font.family.control",
"font.size.small",
diff --git a/design-system/packages/ui/src/components/ActionItem/ActionItem.module.css b/design-system/packages/ui/src/components/ActionItem/ActionItem.module.css
index 71f05faf6c..334adba05d 100644
--- a/design-system/packages/ui/src/components/ActionItem/ActionItem.module.css
+++ b/design-system/packages/ui/src/components/ActionItem/ActionItem.module.css
@@ -38,6 +38,20 @@
cursor: not-allowed;
}
+ .root[data-bf-tone="danger"]:not([data-disabled="true"]) {
+ color: var(--bf-color-status-danger-content);
+ }
+
+ .root[data-bf-tone="danger"]:not([data-disabled="true"]):hover,
+ .root[data-bf-tone="danger"]:not([data-disabled="true"]):has(.trigger[data-bf-preview-state="hover"]) {
+ background: var(--bf-color-status-danger-surface);
+ }
+
+ .root[data-bf-tone="danger"]:not([data-disabled="true"]):has(.trigger:active),
+ .root[data-bf-tone="danger"]:not([data-disabled="true"]):has(.trigger[data-bf-preview-state="active"]) {
+ background: var(--bf-color-status-danger-surface);
+ }
+
.trigger {
display: inline-flex;
flex: 1 1 auto;
diff --git a/design-system/packages/ui/src/components/ActionItem/ActionItem.tsx b/design-system/packages/ui/src/components/ActionItem/ActionItem.tsx
index dbcea44a8b..4489219504 100644
--- a/design-system/packages/ui/src/components/ActionItem/ActionItem.tsx
+++ b/design-system/packages/ui/src/components/ActionItem/ActionItem.tsx
@@ -17,6 +17,8 @@ export interface ActionItemAction {
tone?: IconButtonProps["tone"];
}
+export type ActionItemTone = "neutral" | "danger";
+
export interface ActionItemProps
extends Omit, "children" | "className"> {
actions?: readonly ActionItemAction[];
@@ -26,6 +28,7 @@ export interface ActionItemProps
metadata?: ReactNode;
reserveLeadingSpace?: boolean;
shortcut?: ReactNode;
+ tone?: ActionItemTone;
triggerClassName?: string;
}
@@ -38,6 +41,7 @@ export const ActionItem = forwardRef(functio
metadata,
reserveLeadingSpace = false,
shortcut,
+ tone = "neutral",
triggerClassName,
type = "button",
...props
@@ -48,6 +52,7 @@ export const ActionItem = forwardRef(functio
{
assert.match(markup, /data-bf-part="leading"><\/span>/);
});
+test("ActionItem exposes a danger tone for destructive rows", () => {
+ const neutralMarkup = renderToStaticMarkup(
+ createElement(ActionItem, null, "Rename"),
+ );
+ const dangerMarkup = renderToStaticMarkup(
+ createElement(ActionItem, { tone: "danger" }, "Delete"),
+ );
+
+ assert.match(neutralMarkup, /data-bf-tone="neutral"/);
+ assert.match(dangerMarkup, /data-bf-tone="danger"/);
+});
+
test("ActionItem styles share action state and focus tokens", async () => {
const styles = await readFile(new URL("../dist/styles.css", import.meta.url), "utf8");
From 982e74709bca26fe2a3ebb9d6ce3893009c10c95 Mon Sep 17 00:00:00 2001
From: kev1n77
Date: Fri, 28 Aug 2026 21:13:55 +0800
Subject: [PATCH 5/9] feat(web-ui): render nav and gallery dropdowns with
design-system Menu
Replace the custom popover markup of the workspace item menus, the assistant session create menu, and the miniapp gallery import menu with Menu/MenuItem/MenuSeparator from @bitfun/ui, keeping only positioning overrides in SCSS. Retire the corresponding appearance parts, migrate persisted appearance packages, and teach the overlay-surface portal contract that design-system surface components own their chrome.
---
.../src/app/components/NavPanel/NavPanel.scss | 43 +--
.../src/app/components/NavPanel/appearance.ts | 1 -
.../components/AssistantSessionCreateMenu.tsx | 18 +-
.../workspaces/WorkspaceItem.appearance.ts | 4 +-
.../sections/workspaces/WorkspaceItem.tsx | 329 ++++++------------
.../workspaces/WorkspaceListSection.scss | 91 +----
.../miniapps/views/MiniAppGalleryView.scss | 40 +--
.../miniapps/views/MiniAppGalleryView.tsx | 30 +-
.../styles/overlay-surfaces.contract.test.ts | 14 +-
.../schema/migrateAppearancePackage.ts | 2 +
10 files changed, 149 insertions(+), 423 deletions(-)
diff --git a/src/web-ui/src/app/components/NavPanel/NavPanel.scss b/src/web-ui/src/app/components/NavPanel/NavPanel.scss
index e9a7a4c406..c82861c849 100644
--- a/src/web-ui/src/app/components/NavPanel/NavPanel.scss
+++ b/src/web-ui/src/app/components/NavPanel/NavPanel.scss
@@ -1035,50 +1035,15 @@ $_section-header-height: 22px;
border-left: 1px solid color-mix(in srgb, var(--bf-appearance-token-border-subtle) 72%, transparent);
}
+ /* Positioning-only overrides; the design-system Menu owns the surface look. */
&__assistant-session-menu {
@include nav-font.nav-panel-font-token-scope;
- @include surfaces.floating-surface;
position: fixed;
- min-width: 200px;
- max-width: min(280px, calc(100vw - 24px));
- max-height: calc(100vh - 16px);
- overflow-y: auto;
- padding: 6px;
+ min-inline-size: 200px;
+ max-inline-size: min(280px, calc(100vw - 24px));
+ max-block-size: calc(100vh - 16px);
z-index: $z-popover;
- color: var(--bf-appearance-token-color-text-primary);
- }
-
- &__assistant-session-menu-item {
- display: flex;
- align-items: center;
- gap: 8px;
- width: 100%;
- height: 30px;
- padding: 0 8px;
- border: none;
- border-radius: 8px;
- background: transparent;
- color: var(--bf-appearance-token-color-text-secondary);
- cursor: pointer;
- text-align: left;
- font-size: var(--bf-appearance-token-font-size-xs);
- transition: color $motion-fast $easing-standard,
- background $motion-fast $easing-standard;
-
- > svg {
- flex-shrink: 0;
- }
-
- &:hover {
- color: var(--bf-appearance-token-color-text-primary);
- background: var(--bf-appearance-token-element-bg-soft);
- }
-
- &:focus-visible {
- outline: 1px solid var(--bf-appearance-token-color-accent-500);
- outline-offset: -1px;
- }
}
&__assistant-session-menu-name {
diff --git a/src/web-ui/src/app/components/NavPanel/appearance.ts b/src/web-ui/src/app/components/NavPanel/appearance.ts
index 535afa2ba1..c8d332310c 100644
--- a/src/web-ui/src/app/components/NavPanel/appearance.ts
+++ b/src/web-ui/src/app/components/NavPanel/appearance.ts
@@ -22,7 +22,6 @@ export const navPanelAppearanceDescriptor: AppearanceSurfaceDescriptor = {
{ id: 'stickySectionHeader', visualRole: 'toolbar', continuityGroup: 'nav-panel' },
{ id: 'sectionContent', visualRole: 'content' },
{ id: 'assistantSessionActions', propertyProfile: 'control', visualRole: 'control' },
- { id: 'assistantSessionMenu', propertyProfile: 'overlay', visualRole: 'popup' },
{ id: 'miniAppFooter', visualRole: 'toolbar', continuityGroup: 'nav-panel' },
{ id: 'footer', visualRole: 'toolbar', continuityGroup: 'nav-panel' },
{ id: 'deviceStatus', propertyProfile: 'control', visualRole: 'control' },
diff --git a/src/web-ui/src/app/components/NavPanel/components/AssistantSessionCreateMenu.tsx b/src/web-ui/src/app/components/NavPanel/components/AssistantSessionCreateMenu.tsx
index 09f2444ae8..8a4d7badb9 100644
--- a/src/web-ui/src/app/components/NavPanel/components/AssistantSessionCreateMenu.tsx
+++ b/src/web-ui/src/app/components/NavPanel/components/AssistantSessionCreateMenu.tsx
@@ -1,6 +1,7 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { ChevronDown, Plus } from 'lucide-react';
+import { Menu, MenuItem } from '@bitfun/ui';
import { Tooltip } from '@/component-library';
import { getAppearanceOverlayHost } from '@/infrastructure/appearance/runtime/AppearanceOverlayHost';
import { useI18n } from '@/infrastructure/i18n/hooks/useI18n';
@@ -121,13 +122,9 @@ const AssistantSessionCreateMenu: React.FC = ({
{menuOpen ? createPortal(
- = ({
{orderedAssistants.map(workspace => {
const assistantName = getAssistantDisplayName(workspace);
return (
-
}
aria-label={t('nav.sessions.newAssistantSessionFor', { assistantName })}
onClick={() => {
closeMenu();
@@ -151,12 +146,11 @@ const AssistantSessionCreateMenu: React.FC
= ({
}}
data-testid={`nav-assistant-session-menu-item-${workspace.id}`}
>
-
{assistantName}
-
+
);
})}
- ,
+ ,
getAppearanceOverlayHost(),
) : null}
diff --git a/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceItem.appearance.ts b/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceItem.appearance.ts
index 1fbc2b7d7d..e3925e6242 100644
--- a/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceItem.appearance.ts
+++ b/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceItem.appearance.ts
@@ -5,8 +5,8 @@ export const workspaceItemAppearanceDescriptor: AppearanceSurfaceDescriptor = {
parts: [
{ id: 'root' }, { id: 'card' }, { id: 'collapse' }, { id: 'icon' },
{ id: 'name' }, { id: 'label' }, { id: 'badge' }, { id: 'action' },
- { id: 'menu' }, { id: 'menuTrigger' }, { id: 'menuPopover' }, { id: 'menuItem' },
- { id: 'menuDivider' }, { id: 'sessions' }, { id: 'remoteStatus' },
+ { id: 'menu' }, { id: 'menuTrigger' },
+ { id: 'sessions' }, { id: 'remoteStatus' },
{ id: 'indexIndicator' }, { id: 'indexPanel' },
],
facets: [{ id: 'variant', attribute: 'data-bf-variant', values: ['workspace', 'assistant'] }],
diff --git a/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceItem.tsx b/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceItem.tsx
index 0a774b5d0d..194be2d566 100644
--- a/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceItem.tsx
+++ b/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceItem.tsx
@@ -1,4 +1,4 @@
-import { Button, Modal, ConfirmDialog } from '@bitfun/ui';
+import { Button, Modal, ConfirmDialog, Menu, MenuItem, MenuSeparator } from '@bitfun/ui';
import React, { lazy, Suspense, useCallback, useContext, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';
import { createPortal } from 'react-dom';
import { Folder, FolderOpen, MoreHorizontal, FolderSearch, Plus, ChevronDown, Trash2, RotateCcw, Copy, FileText, Bot, Link2, ListChecks, Loader2, Clock3, ShieldCheck, Pencil, Network } from 'lucide-react';
@@ -903,125 +903,84 @@ const WorkspaceItem: React.FC
= ({
{menuOpen && menuPosition && createPortal(
-
-
{ void handleCreateSession(); }}
- data-testid="nav-workspace-menu-create-session"
- >
-
- {t('nav.workspaces.actions.newSession')}
-
-
-
- {t('nav.scheduledJobs.open')}
-
-
-
-
- {t('nav.workspaces.actions.manageProjectPermissions')}
-
-
- {portForwardConnectionId ? (
-
}
+ onClick={() => { void handleCreateSession(); }}
+ data-testid="nav-workspace-menu-create-session"
+ >
+ {t('nav.workspaces.actions.newSession')}
+
+
} onClick={handleOpenScheduledJobs}>
+ {t('nav.scheduledJobs.open')}
+
+
}
+ onClick={handleOpenProjectPermissions}
+ data-testid="nav-workspace-menu-project-permissions"
+ >
+ {t('nav.workspaces.actions.manageProjectPermissions')}
+
+ {portForwardConnectionId ? (
+
}
onClick={handleOpenPortForward}
data-testid="nav-workspace-menu-port-forward"
>
-
-
- {t('ssh.portForward.menuEntry')}
-
-
+ {t('ssh.portForward.menuEntry')}
+
) : null}
-
-
{ void handleCopyWorkspacePath(); }}
- disabled={!workspace.rootPath}
- data-testid="nav-workspace-menu-copy-path"
- >
-
- {t('nav.workspaces.actions.copyPath')}
-
-
{ void handleReveal(); }}
- disabled={isRemoteWorkspace(workspace)}
- data-testid="nav-workspace-menu-reveal"
- >
-
- {t('nav.workspaces.actions.reveal')}
-
+
+
}
+ onClick={() => { void handleCopyWorkspacePath(); }}
+ disabled={!workspace.rootPath}
+ data-testid="nav-workspace-menu-copy-path"
+ >
+ {t('nav.workspaces.actions.copyPath')}
+
+
}
+ onClick={() => { void handleReveal(); }}
+ disabled={isRemoteWorkspace(workspace)}
+ data-testid="nav-workspace-menu-reveal"
+ >
+ {t('nav.workspaces.actions.reveal')}
+
{(isDefaultAssistantWorkspace || isDeletableAssistantWorkspace) ? (
<>
-
+
{isDefaultAssistantWorkspace ? (
-
}
+ tone="danger"
onClick={handleRequestResetWorkspace}
disabled={isResettingWorkspace}
data-testid="nav-workspace-menu-reset-assistant"
>
-
-
{t('nav.workspaces.actions.resetWorkspace')}
-
+ {t('nav.workspaces.actions.resetWorkspace')}
+
) : null}
{isDeletableAssistantWorkspace ? (
-
}
+ tone="danger"
onClick={handleRequestDeleteAssistant}
disabled={isDeletingAssistant}
data-testid="nav-workspace-menu-delete-assistant"
>
-
-
{t('nav.workspaces.actions.deleteAssistant')}
-
+ {t('nav.workspaces.actions.deleteAssistant')}
+
) : null}
>
) : null}
-
,
+ ,
getAppearanceOverlayHost()
)}
@@ -1386,186 +1345,112 @@ const WorkspaceItem: React.FC = ({
{menuOpen && menuPosition && createPortal(
-
-
}
onClick={handleCreateProjectSession}
data-testid="nav-workspace-menu-create-session"
>
-
-
{t('nav.sessions.newSession')}
-
+ {t('nav.sessions.newSession')}
+
{acpClients.map(client => {
const label = client.name || client.id;
return (
-
}
onClick={() => { void handleCreateAcpSession(client); }}
data-testid="nav-workspace-menu-create-acp-session"
data-acp-client-id={client.id}
>
-
-
- {t('nav.sessions.newExternalAgentSessionShort', { agentName: label })}
-
-
+ {t('nav.sessions.newExternalAgentSessionShort', { agentName: label })}
+
);
})}
{acpClientsLoading ? (
-
-
- {t('app.loading')}
-
+
} disabled>
+ {t('app.loading')}
+
) : null}
-
}
onClick={() => { void handleCreateInitSession(); }}
data-testid="nav-workspace-menu-create-init-session"
>
-
-
{t('nav.workspaces.actions.initAgents')}
-
-
+ }
onClick={() => {
setMenuOpen(false);
setRelatedPathsDialogOpen(true);
}}
data-testid="nav-workspace-menu-related-paths"
>
-
-
- {t('nav.workspaces.actions.manageRelatedPaths')}
-
-
-
+ }
onClick={handleOpenProjectPermissions}
data-testid="nav-workspace-menu-project-permissions"
>
-
-
- {t('nav.workspaces.actions.manageProjectPermissions')}
-
-
-
-
- {t('nav.scheduledJobs.open')}
-
+ {t('nav.workspaces.actions.manageProjectPermissions')}
+
+
} onClick={handleOpenScheduledJobs}>
+ {t('nav.scheduledJobs.open')}
+
{portForwardConnectionId ? (
-
}
onClick={handleOpenPortForward}
data-testid="nav-workspace-menu-port-forward"
>
-
-
- {t('ssh.portForward.menuEntry')}
-
-
+ {t('ssh.portForward.menuEntry')}
+
) : null}
-
-
+
}
onClick={handleRequestRename}
data-testid="nav-workspace-menu-rename"
>
-
-
- {t('nav.workspaces.actions.rename')}
-
-
-
+ }
onClick={() => { void handleCopyWorkspacePath(); }}
disabled={!workspace.rootPath}
data-testid="nav-workspace-menu-copy-path"
>
-
- {t('nav.workspaces.actions.copyPath')}
-
-
+ }
onClick={() => { void handleReveal(); }}
disabled={isRemoteWorkspace(workspace)}
data-testid="nav-workspace-menu-reveal"
>
-
- {t('nav.workspaces.actions.reveal')}
-
-
-
-
- {t('nav.sessions.manage')}
-
-
+
+ } onClick={handleOpenSessionBatchModal}>
+ {t('nav.sessions.manage')}
+
+ }
+ tone="danger"
onClick={() => { void handleCloseWorkspace(); }}
data-testid="nav-workspace-menu-close"
>
-
- {t('nav.workspaces.actions.close')}
-
-
,
+ {t('nav.workspaces.actions.close')}
+
+ ,
getAppearanceOverlayHost()
)}
diff --git a/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceListSection.scss b/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceListSection.scss
index fd0820b57f..f8a6f915cf 100644
--- a/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceListSection.scss
+++ b/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceListSection.scss
@@ -1,5 +1,4 @@
@use '../../../../../component-library/styles/tokens.scss' as *;
-@use '../../../../../component-library/styles/overlay-surfaces' as surfaces;
@use '../../../../styles/nav-panel-font-scope.scss' as nav-font;
@use '../../../../styles/workspace-shell-surfaces' as shell-surfaces;
@@ -957,87 +956,20 @@
}
}
+ /* Positioning-only overrides; the design-system Menu owns the surface look. */
&__workspace-item-menu-popover {
@include nav-font.nav-panel-font-token-scope;
- @include surfaces.floating-surface;
position: fixed;
- width: max-content;
- min-width: 180px;
- max-width: min(320px, calc(100vw - 24px));
- max-height: min(560px, calc(100vh - 16px));
- overflow-x: hidden;
- overflow-y: auto;
- padding: $size-gap-1 0;
+ inline-size: max-content;
+ min-inline-size: 180px;
+ max-inline-size: min(320px, calc(100vw - 24px));
+ max-block-size: min(560px, calc(100vh - 16px));
z-index: 10000;
transform-origin: top left;
animation: bitfun-footer-menu-in $motion-fast $easing-decelerate forwards;
}
- /* Match `.bitfun-nav-panel__footer-menu-divider` (PersistentFooterActions more menu) */
- &__workspace-item-menu-divider {
- height: 1px;
- margin: $size-gap-1 $size-gap-2;
- background: var(--bf-appearance-token-border-subtle);
- }
-
- &__workspace-item-menu-item {
- display: flex;
- align-items: center;
- gap: $size-gap-2;
- width: 100%;
- min-height: 30px;
- padding: 0 $size-gap-2;
- border: none;
- border-radius: $size-radius-sm;
- background: transparent;
- color: var(--bf-appearance-token-color-text-secondary);
- font-size: var(--bf-appearance-token-font-size-sm);
- font-weight: 400;
- cursor: pointer;
- text-align: left;
- white-space: nowrap;
- transition: color $motion-fast $easing-standard,
- background $motion-fast $easing-standard;
-
- svg {
- flex-shrink: 0;
- opacity: 0.7;
- transition: opacity $motion-fast $easing-standard;
- }
-
- &:hover:not(:disabled) {
- color: var(--bf-appearance-token-color-text-primary);
- background: var(--bf-appearance-token-element-bg-soft);
-
- svg {
- opacity: 1;
- }
- }
-
- &:active:not(:disabled) {
- background: var(--bf-appearance-token-element-bg-medium);
- }
-
- &:disabled {
- opacity: 0.45;
- cursor: not-allowed;
- }
-
- &.is-danger:hover:not(:disabled) {
- color: var(--bf-appearance-token-color-error);
- background: color-mix(in srgb, var(--bf-appearance-token-color-error) 10%, transparent);
- }
- }
-
- &__workspace-item-menu-label {
- flex: 1;
- text-align: left;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- }
-
&__workspace-item-sessions {
position: relative;
z-index: 0;
@@ -1530,22 +1462,9 @@
}
&__workspace-item-menu-popover {
- padding: $size-gap-1;
transform-origin: top left;
animation: bitfun-workspace-popover-in 150ms cubic-bezier(0.23, 1, 0.32, 1) both;
}
-
- &__workspace-item-menu-item {
- border-radius: $size-radius-sm;
- transition:
- transform 120ms cubic-bezier(0.23, 1, 0.32, 1),
- background 120ms ease,
- color 120ms ease;
-
- &:active:not(:disabled) {
- transform: scale(0.985);
- }
- }
}
@media (hover: hover) and (pointer: fine) {
diff --git a/src/web-ui/src/app/scenes/miniapps/views/MiniAppGalleryView.scss b/src/web-ui/src/app/scenes/miniapps/views/MiniAppGalleryView.scss
index b15844afff..d41dd2ce8e 100644
--- a/src/web-ui/src/app/scenes/miniapps/views/MiniAppGalleryView.scss
+++ b/src/web-ui/src/app/scenes/miniapps/views/MiniAppGalleryView.scss
@@ -1,5 +1,4 @@
@use '../../../../component-library/styles/tokens' as *;
-@use '../../../../component-library/styles/overlay-surfaces' as surfaces;
.miniapp-gallery {
background: var(--bf-appearance-token-color-bg-scene);
@@ -97,46 +96,11 @@
}
+/* Positioning-only overrides; the design-system Menu owns the surface look. */
.miniapp-gallery__import-menu {
- @include surfaces.floating-surface;
-
position: fixed;
z-index: 10020;
- display: grid;
- gap: 2px;
- min-width: 196px;
- padding: $size-gap-1;
-}
-
-.miniapp-gallery__import-menu-item {
- display: flex;
- align-items: center;
- gap: $size-gap-2;
- width: 100%;
- min-height: 34px;
- padding: 0 $size-gap-3;
- border: 0;
- border-radius: $size-radius-base;
- background: transparent;
- color: var(--bf-appearance-token-color-text-secondary);
- font: inherit;
- font-size: var(--bf-appearance-token-font-size-xs);
- text-align: left;
- cursor: pointer;
- transition:
- background-color $motion-fast $easing-standard,
- color $motion-fast $easing-standard;
-
- &:hover,
- &:focus-visible {
- background: var(--bf-appearance-token-element-bg-soft);
- color: var(--bf-appearance-token-color-text-primary);
- }
-
- &:focus-visible {
- outline: 2px solid var(--bf-appearance-token-color-accent-500);
- outline-offset: -2px;
- }
+ min-inline-size: 196px;
}
@media (max-width: 720px) {
diff --git a/src/web-ui/src/app/scenes/miniapps/views/MiniAppGalleryView.tsx b/src/web-ui/src/app/scenes/miniapps/views/MiniAppGalleryView.tsx
index e9ed4f9e25..41907dfdf7 100644
--- a/src/web-ui/src/app/scenes/miniapps/views/MiniAppGalleryView.tsx
+++ b/src/web-ui/src/app/scenes/miniapps/views/MiniAppGalleryView.tsx
@@ -23,7 +23,7 @@ import {
type MarketPackageInspection,
} from '@/infrastructure/api/service-api/MiniAppMarketAPI';
import { createLogger } from '@/shared/utils/logger';
-import { ConfirmDialog, SearchField, IconButton } from '@bitfun/ui';
+import { ConfirmDialog, SearchField, IconButton, Menu, MenuItem } from '@bitfun/ui';
import {
GalleryEmpty,
@@ -438,46 +438,38 @@ const MiniAppGalleryView: React.FC = () => {
icon={}
/>
{importMenuOpen ? createPortal(
-
-
}
onClick={() => {
closeImportMenu();
void handleAddFromFolder();
}}
data-testid="miniapp-import-folder-action"
>
-
-
{t('importFromFolder')}
-
-
+ }
onClick={() => {
closeImportMenu();
void handleAddPackage();
}}
data-testid="miniapp-import-package-action"
>
-
- {t('market.import.action')}
-
-
,
+ {t('market.import.action')}
+
+ ,
getAppearanceOverlayHost(),
) : null}
diff --git a/src/web-ui/src/component-library/styles/overlay-surfaces.contract.test.ts b/src/web-ui/src/component-library/styles/overlay-surfaces.contract.test.ts
index 98689bd13f..ffad10f3ee 100644
--- a/src/web-ui/src/component-library/styles/overlay-surfaces.contract.test.ts
+++ b/src/web-ui/src/component-library/styles/overlay-surfaces.contract.test.ts
@@ -9,8 +9,6 @@ const readSource = (path: string): string => readFileSync(join(SOURCE_ROOT, path
const readRepositorySource = (path: string): string => readFileSync(join(REPOSITORY_ROOT, path), 'utf8');
const portalStyleOverrides: Record = {
- 'app/components/NavPanel/components/AssistantSessionCreateMenu.tsx':
- 'app/components/NavPanel/NavPanel.scss',
'app/components/NavPanel/components/DeviceStatusControl.tsx':
'app/components/NavPanel/NavPanel.scss',
'app/components/NavPanel/components/PersistentFooterActions.tsx':
@@ -18,8 +16,6 @@ const portalStyleOverrides: Record = {
'app/components/NavPanel/components/WorkspaceSessionFilterMenu.tsx':
'app/components/NavPanel/sections/sessions/SessionsSection.scss',
'app/components/NavPanel/MainNav.tsx': 'app/components/NavPanel/NavPanel.scss',
- 'app/components/NavPanel/sections/workspaces/WorkspaceItem.tsx':
- 'app/components/NavPanel/sections/workspaces/WorkspaceListSection.scss',
'app/components/scheduled-jobs/DateTimePickerPopover.tsx':
'app/components/scheduled-jobs/LocalizedDateTimeField.scss',
'app/scenes/profile/views/AssistantAvatarPicker.tsx':
@@ -29,6 +25,12 @@ const portalStyleOverrides: Record = {
'flow_chat/components/WelcomePanel.tsx': 'flow_chat/components/WelcomePanelSurface.scss',
};
+// Portals whose chrome is owned by a design-system surface component satisfy
+// the contract without a floating-surface/dialog-surface SCSS include.
+const rendersDesignSystemSurface = (source: string): boolean =>
+ /import\s*\{[^}]*\b(?:Menu|Modal|ConfirmDialog)\b[^}]*\}\s*from\s*'@bitfun\/ui'/.test(source)
+ && /createPortal\(\s*<(?:Menu|Modal|ConfirmDialog)\b/.test(source);
+
const portalSurfaceExceptions: Record = {
'app/components/panels/DiffFullscreenViewer.tsx': 'fullscreen viewer',
'component-library/components/Tooltip/Tooltip.tsx': 'tooltip primitive',
@@ -270,6 +272,10 @@ describe('overlay surface contracts', () => {
continue;
}
+ if (rendersDesignSystemSurface(readSource(sourcePath))) {
+ continue;
+ }
+
const stylePath = resolvePortalStylePath(sourcePath);
expect(stylePath, `${sourcePath} must resolve to an overlay stylesheet`).toBeDefined();
const styleSource = readSource(stylePath!);
diff --git a/src/web-ui/src/infrastructure/appearance/schema/migrateAppearancePackage.ts b/src/web-ui/src/infrastructure/appearance/schema/migrateAppearancePackage.ts
index a59c778c1f..8891fc547a 100644
--- a/src/web-ui/src/infrastructure/appearance/schema/migrateAppearancePackage.ts
+++ b/src/web-ui/src/infrastructure/appearance/schema/migrateAppearancePackage.ts
@@ -38,6 +38,8 @@ const RETIRED_COMPONENT_PARTS: Readonly>> = {
'markdown-editor': new Set(['modeToggle']),
'market-account-controls': new Set(['menu', 'menuItem']),
'mini-app-tool-display': new Set(['open']),
+ 'nav-panel': new Set(['assistantSessionMenu']),
+ 'workspace-item': new Set(['menuPopover', 'menuItem', 'menuDivider']),
'peer-device': new Set(['switcherDisconnect']),
'review-session-summary-card': new Set(['open']),
'sessions-section': new Set(['retry', 'aggregateRetry']),
From b19b6d31cc456a4bb3382b2570f5e845d0aa3f43 Mon Sep 17 00:00:00 2001
From: kev1n77
Date: Sat, 29 Aug 2026 09:36:26 +0800
Subject: [PATCH 6/9] feat(web-ui): migrate nav footer and workspace switcher
menus to design-system Menu
---
.../src/app/components/NavPanel/MainNav.tsx | 150 +++++++---------
.../src/app/components/NavPanel/NavPanel.scss | 169 +-----------------
.../src/app/components/NavPanel/appearance.ts | 10 +-
.../components/PersistentFooterActions.tsx | 69 +++----
.../TitleBar/NotificationButton.appearance.ts | 2 +-
.../TitleBar/NotificationButton.tsx | 29 ++-
src/web-ui/src/app/scenes/shell/ShellNav.scss | 32 +---
.../components/ShellNavWorkspaceSwitcher.tsx | 23 +--
.../schema/migrateAppearancePackage.ts | 7 +-
9 files changed, 124 insertions(+), 367 deletions(-)
diff --git a/src/web-ui/src/app/components/NavPanel/MainNav.tsx b/src/web-ui/src/app/components/NavPanel/MainNav.tsx
index be380cfb9e..671a03aef7 100644
--- a/src/web-ui/src/app/components/NavPanel/MainNav.tsx
+++ b/src/web-ui/src/app/components/NavPanel/MainNav.tsx
@@ -12,7 +12,7 @@
import React, { useCallback, useState, useMemo, useEffect, useRef, useSyncExternalStore } from 'react';
import { createPortal } from 'react-dom';
-import { KeyHint } from '@bitfun/ui';
+import { KeyHint, Menu, MenuItem, MenuSection, MenuSeparator } from '@bitfun/ui';
import { getAppearanceOverlayHost } from '@/infrastructure/appearance/runtime/AppearanceOverlayHost';
import { isImeOwnedKeyboardEvent } from '@/shared/utils/ime';
import { FolderOpen, FolderPlus, History, Check, User, Users, Puzzle, ChevronDown, Network, Search, CalendarClock } from 'lucide-react';
@@ -276,105 +276,87 @@ const MainNav: React.FC = ({
}, [isAgentsActive, isEcosystemCompatibilityActive, isSkillsActive]);
const workspaceMenuPortal = workspaceMenuOpen ? createPortal(
-
-
}
onClick={() => { closeWorkspaceMenu(); void handleOpenProject(); }}
>
-
-
{t('header.openProject')}
-
-
+ }
onClick={() => { closeWorkspaceMenu(); handleNewProject(); }}
>
-
- {t('header.newProject')}
-
-
+ }
onClick={handleOpenAssistantManager}
data-testid="nav-session-group-add-assistant"
>
-
- {t('nav.workspaces.actions.newAssistant')}
-
-
+
-
-
-
- {t('header.recentWorkspaces')}
-
- {recentWorkspaces.length === 0 ? (
-
- {t('header.noRecentWorkspaces')}
-
- ) : (
-
- {recentWorkspaces.map((workspace) => {
+ {t('ssh.remote.connect')}
+
+
+
+
+ {t('header.recentWorkspaces')}
+ >
+ )}
+ itemsClassName="bitfun-nav-panel__workspace-menu-workspaces"
+ >
+ {recentWorkspaces.length === 0 ? (
+
+ {t('header.noRecentWorkspaces')}
+
+ ) : (
+ recentWorkspaces.map((workspace) => {
const { hostPrefix, folderLabel, tooltip } = getRecentWorkspaceLineParts(workspace);
+ const isCurrent = workspace.id === currentWorkspace?.id;
return (
- { void handleSwitchWorkspace(workspace.id); }}
- data-testid="nav-workspace-menu-recent-workspace"
- data-workspace-id={workspace.id}
- >
-
-
- {hostPrefix ? (
- <>
- {hostPrefix}
-
- ·
-
- >
- ) : null}
- {folderLabel}
-
- {workspace.id === currentWorkspace?.id ? : null}
-
+ }
+ role="menuitemradio"
+ checked={isCurrent}
+ metadata={isCurrent ? : undefined}
+ title={tooltip}
+ onClick={() => { void handleSwitchWorkspace(workspace.id); }}
+ data-testid="nav-workspace-menu-recent-workspace"
+ data-workspace-id={workspace.id}
+ >
+
+ {hostPrefix ? (
+ <>
+ {hostPrefix}
+
+ ·
+
+ >
+ ) : null}
+ {folderLabel}
+
+
);
- })}
-
- )}
-
,
+ })
+ )}
+
+ ,
getAppearanceOverlayHost()
) : null;
diff --git a/src/web-ui/src/app/components/NavPanel/NavPanel.scss b/src/web-ui/src/app/components/NavPanel/NavPanel.scss
index c82861c849..5b724fb5af 100644
--- a/src/web-ui/src/app/components/NavPanel/NavPanel.scss
+++ b/src/web-ui/src/app/components/NavPanel/NavPanel.scss
@@ -1110,21 +1110,15 @@ $_section-header-height: 22px;
position: relative;
}
+ /* Positioning-only overrides; the design-system Menu owns the surface look. */
&__workspace-menu {
@include nav-font.nav-panel-font-token-scope;
- @include surfaces.floating-surface;
position: fixed;
/* Shrink-wrap to widest row; cap width for long paths + viewport on small screens */
- display: inline-block;
width: fit-content;
max-width: min(440px, calc(100vw - 32px));
max-height: min(560px, calc(100vh - 16px));
- overflow-x: hidden;
- overflow-y: auto;
- vertical-align: top;
- box-sizing: border-box;
- padding: $size-gap-1 0;
z-index: 9999;
transform-origin: top left;
animation: bitfun-footer-menu-in $motion-fast $easing-decelerate forwards;
@@ -1134,59 +1128,13 @@ $_section-header-height: 22px;
}
}
- &__workspace-menu-item {
- display: flex;
- align-items: center;
- gap: $size-gap-2;
- width: 100%;
- padding: $size-gap-2 $size-gap-3;
- border: none;
- background: transparent;
- color: var(--bf-appearance-token-color-text-secondary);
- cursor: pointer;
- font-size: var(--bf-appearance-token-font-size-sm);
- text-align: left;
- transition: color $motion-fast $easing-standard,
- background $motion-fast $easing-standard;
-
- &:hover:not(:disabled):not(.is-disabled) {
- color: var(--bf-appearance-token-color-text-primary);
- background: var(--bf-appearance-token-element-bg-soft);
- }
-
- &.is-disabled,
- &:disabled {
- opacity: 0.45;
- cursor: not-allowed;
- color: var(--bf-appearance-token-color-text-muted);
- }
-
- svg {
- flex-shrink: 0;
- }
-
- /* Only the label row grows; nested spans inside --main must NOT get flex:1 or text splits across columns */
- > span {
- flex: 1;
- min-width: 0;
- }
-
- &--workspace {
- padding-right: $size-gap-2;
- }
- }
-
&__workspace-menu-item-main {
- min-width: 0;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
-
- &__workspace-menu-item--workspace &__workspace-menu-item-main {
display: flex;
align-items: baseline;
gap: 0.3em;
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
white-space: nowrap;
}
@@ -1214,23 +1162,6 @@ $_section-header-height: 22px;
text-overflow: ellipsis;
}
- &__workspace-menu-divider {
- height: 1px;
- margin: $size-gap-1 0;
- background: var(--bf-appearance-token-border-subtle);
- }
-
- &__workspace-menu-section-title {
- display: flex;
- align-items: center;
- gap: $size-gap-1;
- padding: $size-gap-1 $size-gap-3;
- color: var(--bf-appearance-token-color-text-muted);
- font-size: var(--bf-appearance-token-font-size-xs);
- text-transform: uppercase;
- letter-spacing: 0.04em;
- }
-
&__workspace-menu-empty {
padding: $size-gap-2 $size-gap-3;
color: var(--bf-appearance-token-color-text-muted);
@@ -2011,19 +1942,13 @@ $_section-header-height: 22px;
z-index: 9998;
}
+/* Positioning-only overrides; the design-system Menu owns the surface look. */
.bitfun-nav-panel__footer-menu {
- @include surfaces.floating-surface;
-
position: fixed;
min-width: 148px;
max-width: calc(100vw - 16px);
max-height: calc(100vh - 16px);
- overflow-y: auto;
- padding: $size-gap-1;
z-index: 9999;
- color: var(--bf-appearance-token-color-text-primary);
- font-family: var(--bf-appearance-token-font-family-sans);
- font-size: var(--bf-appearance-token-font-size-sm);
transform-origin: bottom right;
animation: bitfun-footer-menu-in $motion-fast $easing-decelerate forwards;
@@ -2364,88 +2289,6 @@ $_section-header-height: 22px;
padding: 0 4px;
}
-.bitfun-nav-panel__footer-menu-divider {
- height: 1px;
- margin: $size-gap-1 $size-gap-2;
- background: var(--bf-appearance-token-border-subtle);
-}
-
-.bitfun-nav-panel__footer-menu-item {
- display: flex;
- align-items: center;
- gap: $size-gap-2;
- width: 100%;
- padding: 0 $size-gap-2;
- height: 30px;
- border: none;
- border-radius: $size-radius-sm;
- background: transparent;
- color: var(--bf-appearance-token-color-text-secondary);
- cursor: pointer;
- font-size: var(--bf-appearance-token-font-size-sm);
- font-weight: 400;
- text-align: left;
- white-space: nowrap;
- transition: color $motion-fast $easing-standard,
- background $motion-fast $easing-standard;
-
- svg {
- flex-shrink: 0;
- opacity: 0.7;
- transition: opacity $motion-fast $easing-standard;
- }
-
- &:hover {
- color: var(--bf-appearance-token-color-text-primary);
- background: var(--bf-appearance-token-element-bg-soft);
-
- svg {
- opacity: 1;
- }
- }
-
- &:active {
- background: var(--bf-appearance-token-element-bg-medium);
- }
-
- &:focus-visible {
- outline: 2px solid var(--bf-appearance-token-color-accent-500);
- outline-offset: -1px;
- }
-
- &.is-disabled,
- &:disabled {
- opacity: 0.4;
- cursor: not-allowed;
- pointer-events: auto;
-
- &:hover {
- color: var(--bf-appearance-token-color-text-secondary);
- background: transparent;
-
- svg {
- opacity: 0.7;
- }
- }
- }
-}
-
-.bitfun-nav-panel__footer-menu-item-label {
- flex: 1;
- min-width: 0;
- max-width: 160px;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-
-.bitfun-nav-panel__footer-menu-item-dot {
- flex-shrink: 0;
- width: 7px;
- height: 7px;
- border-radius: 50%;
- background: var(--bf-appearance-token-color-success);
-}
-
.bitfun-nav-panel__remote-disclaimer {
display: flex;
flex-direction: column;
@@ -3350,9 +3193,7 @@ $_section-header-height: 22px;
animation: bitfun-nav-centered-popover-in 150ms cubic-bezier(0.23, 1, 0.32, 1) both;
}
-.bitfun-nav-panel__workspace-menu-item,
.bitfun-nav-panel__mode-dropdown-item,
-.bitfun-nav-panel__footer-menu-item,
.bitfun-nav-panel__footer-multimodal-item {
border-radius: $size-radius-sm;
transition:
diff --git a/src/web-ui/src/app/components/NavPanel/appearance.ts b/src/web-ui/src/app/components/NavPanel/appearance.ts
index c8d332310c..cb88637afd 100644
--- a/src/web-ui/src/app/components/NavPanel/appearance.ts
+++ b/src/web-ui/src/app/components/NavPanel/appearance.ts
@@ -27,18 +27,10 @@ export const navPanelAppearanceDescriptor: AppearanceSurfaceDescriptor = {
{ id: 'deviceStatus', propertyProfile: 'control', visualRole: 'control' },
{ id: 'settingsEntry', propertyProfile: 'control', visualRole: 'control' },
{ id: 'footerButton', propertyProfile: 'control', visualRole: 'control' },
- { id: 'footerMenu', propertyProfile: 'overlay', visualRole: 'popup' },
- { id: 'footerMenuItem', propertyProfile: 'control', visualRole: 'control' },
- { id: 'footerMenuDivider', propertyProfile: 'paint', visualRole: 'divider' },
- { id: 'workspaceMenu', propertyProfile: 'overlay', visualRole: 'popup' },
- { id: 'workspaceMenuItem', propertyProfile: 'control', visualRole: 'control' },
- { id: 'workspaceMenuDivider', propertyProfile: 'paint', visualRole: 'divider' },
- { id: 'workspaceMenuTitle', propertyProfile: 'paint', visualRole: 'content' },
- { id: 'workspaceMenuEmpty', visualRole: 'content' },
],
facets: [
{ id: 'layer', attribute: 'data-bf-layer', values: ['main', 'scene'] },
- { id: 'action', attribute: 'data-bf-action', values: ['new-session', 'smart-members', 'long-term-tracking', 'todos', 'extensions', 'agents', 'skills', 'ecosystem-compatibility', 'session-filter', 'assistant-manager', 'floating-window', 'appearance-configuration', 'open-settings', 'about'] },
+ { id: 'action', attribute: 'data-bf-action', values: ['new-session', 'smart-members', 'long-term-tracking', 'todos', 'extensions', 'agents', 'skills', 'ecosystem-compatibility', 'session-filter', 'assistant-manager'] },
{ id: 'section', attribute: 'data-bf-section', values: ['smart-members', 'workspace', 'sessions'] },
],
states: [
diff --git a/src/web-ui/src/app/components/NavPanel/components/PersistentFooterActions.tsx b/src/web-ui/src/app/components/NavPanel/components/PersistentFooterActions.tsx
index b407969376..023fe204cf 100644
--- a/src/web-ui/src/app/components/NavPanel/components/PersistentFooterActions.tsx
+++ b/src/web-ui/src/app/components/NavPanel/components/PersistentFooterActions.tsx
@@ -6,7 +6,7 @@ import {
PictureInPicture2,
Palette,
} from 'lucide-react';
-import { Modal } from '@bitfun/ui';
+import { Menu, MenuItem, MenuSeparator, Modal } from '@bitfun/ui';
import { Tooltip, PresenceBoundary } from '@/component-library';
import { useI18n } from '@/infrastructure/i18n/hooks/useI18n';
import { useSceneStore } from '../../../stores/sceneStore';
@@ -212,77 +212,48 @@ const PersistentFooterActions: React.FC = () => {
className="bitfun-nav-panel__footer-backdrop"
onClick={closeMenu}
/>
-
-
}
onClick={handleFloatingMode}
data-testid="nav-settings-floating-item"
- data-bf-component="nav-panel"
- data-bf-part="footerMenuItem"
- data-bf-action="floating-window"
>
-
-
{t('nav.settingsMenu.floatingWindow')}
-
+ {t('nav.settingsMenu.floatingWindow')}
+
-
}
onClick={handleOpenThemeConfiguration}
data-testid="nav-settings-theme-item"
- data-bf-component="nav-panel"
- data-bf-part="footerMenuItem"
- data-bf-action="appearance-configuration"
>
-
-
{t('nav.settingsMenu.themeConfiguration')}
-
-
-
+
+ }
onClick={handleOpenSettings}
data-testid="nav-settings-open-item"
- data-bf-component="nav-panel"
- data-bf-part="footerMenuItem"
- data-bf-action="open-settings"
>
-
- {t('nav.settingsMenu.openSettings')}
-
-
+ }
onClick={handleShowAbout}
data-testid="nav-settings-about-item"
- data-bf-component="nav-panel"
- data-bf-part="footerMenuItem"
- data-bf-action="about"
>
-
- {t('nav.settingsMenu.about')}
-
-
+ {t('nav.settingsMenu.about')}
+
+
>,
getAppearanceOverlayHost(),
)}
diff --git a/src/web-ui/src/app/components/TitleBar/NotificationButton.appearance.ts b/src/web-ui/src/app/components/TitleBar/NotificationButton.appearance.ts
index 4983012a12..b3a6a227c8 100644
--- a/src/web-ui/src/app/components/TitleBar/NotificationButton.appearance.ts
+++ b/src/web-ui/src/app/components/TitleBar/NotificationButton.appearance.ts
@@ -1,2 +1,2 @@
import type { AppearanceSurfaceDescriptor } from '@/infrastructure/appearance';
-export const notificationButtonAppearanceDescriptor: AppearanceSurfaceDescriptor = { id: 'notification-button', parts: [{ id: 'root' }, { id: 'menuItem' }, { id: 'progress' }, { id: 'loadingIcon' }, { id: 'progressIcon' }, { id: 'progressText' }, { id: 'tooltip' }, { id: 'tooltipContent' }] };
+export const notificationButtonAppearanceDescriptor: AppearanceSurfaceDescriptor = { id: 'notification-button', parts: [{ id: 'root' }, { id: 'progress' }, { id: 'loadingIcon' }, { id: 'progressIcon' }, { id: 'progressText' }, { id: 'tooltip' }, { id: 'tooltipContent' }] };
diff --git a/src/web-ui/src/app/components/TitleBar/NotificationButton.tsx b/src/web-ui/src/app/components/TitleBar/NotificationButton.tsx
index 3d74bc3566..fa4930d275 100644
--- a/src/web-ui/src/app/components/TitleBar/NotificationButton.tsx
+++ b/src/web-ui/src/app/components/TitleBar/NotificationButton.tsx
@@ -7,6 +7,7 @@
import React, { useRef, useEffect, useState } from 'react';
import { Bell, BellDot, BellRing } from 'lucide-react';
+import { MenuItem } from '@bitfun/ui';
import { Tooltip } from '@/component-library';
import { useI18n } from '@/infrastructure/i18n/hooks/useI18n';
import {
@@ -64,17 +65,9 @@ const NotificationButton: React.FC = ({
: null;
return (
-
- {activeNotification?.variant === 'loading' ? (
+
+ ) : undefined}
+ onClick={handleActivate}
+ aria-label={t('nav.notifications')}
+ data-testid="notification-button"
+ >
+ {t('nav.notifications')}
+
);
}
diff --git a/src/web-ui/src/app/scenes/shell/ShellNav.scss b/src/web-ui/src/app/scenes/shell/ShellNav.scss
index e9f678ecab..acb7f06aae 100644
--- a/src/web-ui/src/app/scenes/shell/ShellNav.scss
+++ b/src/web-ui/src/app/scenes/shell/ShellNav.scss
@@ -88,44 +88,15 @@
opacity: 0.7;
}
+ /* Positioning-only overrides; the design-system Menu owns the surface look. */
&__workspace-menu {
- @include surfaces.floating-surface;
-
position: fixed;
min-width: 220px;
max-width: min(320px, calc(100vw - 32px));
max-height: min(560px, calc(100vh - 16px));
- overflow-x: hidden;
- overflow-y: auto;
- padding: 6px;
z-index: 8;
}
- &__workspace-menu-item {
- display: flex;
- align-items: center;
- gap: 8px;
- width: 100%;
- min-height: 30px;
- padding: 0 8px;
- border: none;
- border-radius: 8px;
- background: transparent;
- color: var(--bf-appearance-token-color-text-secondary);
- cursor: pointer;
- text-align: left;
- font-size: var(--bf-appearance-token-font-size-xs);
- transition:
- color $motion-fast $easing-standard,
- background $motion-fast $easing-standard;
-
- &:hover,
- &.is-active {
- color: var(--bf-appearance-token-color-text-primary);
- background: var(--bf-appearance-token-element-bg-soft);
- }
- }
-
&__workspace-menu-check {
display: inline-flex;
align-items: center;
@@ -485,7 +456,6 @@
@media (prefers-reduced-motion: reduce) {
.bitfun-shell-nav {
&__workspace-trigger,
- &__workspace-menu-item,
&__split-button,
&__split-button-main,
&__split-button-toggle,
diff --git a/src/web-ui/src/app/scenes/shell/components/ShellNavWorkspaceSwitcher.tsx b/src/web-ui/src/app/scenes/shell/components/ShellNavWorkspaceSwitcher.tsx
index 6c3ea28729..3fe8a72a93 100644
--- a/src/web-ui/src/app/scenes/shell/components/ShellNavWorkspaceSwitcher.tsx
+++ b/src/web-ui/src/app/scenes/shell/components/ShellNavWorkspaceSwitcher.tsx
@@ -2,6 +2,7 @@ import React from 'react';
import { createPortal } from 'react-dom';
import { getAppearanceOverlayHost } from '@/infrastructure/appearance/runtime/AppearanceOverlayHost';
import { Check, ChevronDown } from 'lucide-react';
+import { Menu, MenuItem } from '@bitfun/ui';
import { Tooltip } from '@/component-library/components/Tooltip';
import { WorkspaceKind, type WorkspaceInfo } from '@/shared/types';
@@ -66,10 +67,9 @@ const ShellNavWorkspaceSwitcher: React.FC = ({
{workspaceMenuOpen && hasMultipleWorkspaces && workspaceMenuPosition
? createPortal(
- = ({
placement="right"
disabled={!workspace.rootPath}
>
-
+
+
+ ) : undefined}
onClick={() => { void onSelectWorkspace(workspace.id); }}
>
-
- {isActive ? : null}
-
{label}
-
+
);
})}
-
,
+ ,
getAppearanceOverlayHost(),
)
: null}
diff --git a/src/web-ui/src/infrastructure/appearance/schema/migrateAppearancePackage.ts b/src/web-ui/src/infrastructure/appearance/schema/migrateAppearancePackage.ts
index 8891fc547a..3eae5e02f7 100644
--- a/src/web-ui/src/infrastructure/appearance/schema/migrateAppearancePackage.ts
+++ b/src/web-ui/src/infrastructure/appearance/schema/migrateAppearancePackage.ts
@@ -38,7 +38,12 @@ const RETIRED_COMPONENT_PARTS: Readonly>> = {
'markdown-editor': new Set(['modeToggle']),
'market-account-controls': new Set(['menu', 'menuItem']),
'mini-app-tool-display': new Set(['open']),
- 'nav-panel': new Set(['assistantSessionMenu']),
+ 'nav-panel': new Set([
+ 'assistantSessionMenu',
+ 'footerMenu', 'footerMenuItem', 'footerMenuDivider',
+ 'workspaceMenu', 'workspaceMenuItem', 'workspaceMenuDivider', 'workspaceMenuTitle', 'workspaceMenuEmpty',
+ ]),
+ 'notification-button': new Set(['menuItem']),
'workspace-item': new Set(['menuPopover', 'menuItem', 'menuDivider']),
'peer-device': new Set(['switcherDisconnect']),
'review-session-summary-card': new Set(['open']),
From ac17ef1ccb2d26de40a8800cfad11ca6b4487480 Mon Sep 17 00:00:00 2001
From: kev1n77
Date: Fri, 28 Aug 2026 22:02:48 +0800
Subject: [PATCH 7/9] feat(design-system): add Tooltip component with provider
and Design Lab preview
Introduce @bitfun/ui Tooltip with TooltipProvider, placement flipping, follow-cursor and interactive persistence, backed by new overlay.tooltip dimension tokens. Register it in the component registry and expose placement previews in Design Lab.
---
.../design-lab/src/i18n/componentMetadata.ts | 1 +
.../apps/design-lab/src/i18n/messages.ts | 15 +
.../src/pages/ComponentDetailPage.tsx | 26 +-
.../design-lab/src/pages/ComponentsPage.tsx | 10 +
.../design-tokens/src/system.tokens.json | 14 +
.../ui/src/components/Tooltip/Tooltip.meta.ts | 38 ++
.../src/components/Tooltip/Tooltip.module.css | 166 ++++++
.../ui/src/components/Tooltip/Tooltip.tsx | 472 ++++++++++++++++++
.../ui/src/components/Tooltip/index.ts | 10 +
design-system/packages/ui/src/index.ts | 10 +
design-system/packages/ui/src/registry.ts | 2 +
.../packages/ui/tests/registry.test.mjs | 1 +
.../packages/ui/tests/tooltip.test.mjs | 67 +++
13 files changed, 831 insertions(+), 1 deletion(-)
create mode 100644 design-system/packages/ui/src/components/Tooltip/Tooltip.meta.ts
create mode 100644 design-system/packages/ui/src/components/Tooltip/Tooltip.module.css
create mode 100644 design-system/packages/ui/src/components/Tooltip/Tooltip.tsx
create mode 100644 design-system/packages/ui/src/components/Tooltip/index.ts
create mode 100644 design-system/packages/ui/tests/tooltip.test.mjs
diff --git a/design-system/apps/design-lab/src/i18n/componentMetadata.ts b/design-system/apps/design-lab/src/i18n/componentMetadata.ts
index 834840cb1a..999b9a822e 100644
--- a/design-system/apps/design-lab/src/i18n/componentMetadata.ts
+++ b/design-system/apps/design-lab/src/i18n/componentMetadata.ts
@@ -58,6 +58,7 @@ const descriptionKeys: Readonly> = {
TerminalControlToolCard: "component.TerminalControlToolCard.description",
TodoToolCard: "component.TodoToolCard.description",
Toolbar: "component.Toolbar.description",
+ Tooltip: "component.Tooltip.description",
ViewImageToolCard: "component.ViewImageToolCard.description",
WebFetchToolCard: "component.WebFetchToolCard.description",
WebSearchToolCard: "component.WebSearchToolCard.description",
diff --git a/design-system/apps/design-lab/src/i18n/messages.ts b/design-system/apps/design-lab/src/i18n/messages.ts
index ec18d6eda6..09046d6a76 100644
--- a/design-system/apps/design-lab/src/i18n/messages.ts
+++ b/design-system/apps/design-lab/src/i18n/messages.ts
@@ -99,6 +99,7 @@ export const enUSMessages = {
"component.TerminalControlToolCard.description": "An ambient trace for interrupting or terminating a terminal session.",
"component.TodoToolCard.description": "An ambient task-progress card with compact and expandable list presentations.",
"component.Toolbar.description": "A responsive bar with independent leading, centered, trailing, and horizontally scrollable composition regions.",
+ "component.Tooltip.description": "A portaled, delay-managed hover label that flips placement at viewport edges and supports cursor-following and interactive content.",
"component.ViewImageToolCard.description": "An ambient image-result card with an expandable preview and accessible lightbox.",
"component.WebFetchToolCard.description": "An ambient web-fetch card with source metadata, result actions, and content preview.",
"component.WebSearchToolCard.description": "An ambient web-search card with link, snippet, URL, and summary result presentations.",
@@ -244,6 +245,8 @@ export const enUSMessages = {
"components.preview.scrollAreaLabel": "Scrollable activity",
"components.preview.scrollAreaItem": "Activity item {index}",
"components.preview.menuLabel": "Session menu",
+ "components.preview.tooltipTrigger": "Rename session",
+ "components.preview.tooltipContent": "Rename the current session",
"components.preview.menuSectionTitle": "Sessions",
"components.preview.menuItemOne": "Welcome session",
"components.preview.menuItemTwo": "Design review",
@@ -402,6 +405,8 @@ export const enUSMessages = {
"detail.option.left": "Left",
"detail.option.right": "Right",
"detail.option.confirmation": "Confirmation",
+ "detail.option.top": "Top",
+ "detail.option.bottom": "Bottom",
"detail.option.off": "Off",
"detail.option.on": "On",
"detail.option.selected": "Selected",
@@ -694,6 +699,7 @@ export const zhCNMessages = {
"component.TerminalControlToolCard.description": "低关注终端控制轨迹,用于呈现中断或终止终端会话的结果。",
"component.TodoToolCard.description": "低关注任务进度卡片,支持紧凑摘要与可展开列表两种呈现。",
"component.Toolbar.description": "具备独立前置、居中、尾部与横向滚动区域的响应式工具栏。",
+ "component.Tooltip.description": "具备 Portal、延迟管理、视口边缘翻转,并支持跟随光标与可交互内容的悬停提示。",
"component.ViewImageToolCard.description": "低关注图片结果卡片,包含可展开预览和无障碍灯箱。",
"component.WebFetchToolCard.description": "低关注网页读取卡片,包含来源元数据、结果操作与内容预览。",
"component.WebSearchToolCard.description": "低关注网页搜索卡片,统一呈现链接、摘要、网址与结果列表。",
@@ -839,6 +845,8 @@ export const zhCNMessages = {
"components.preview.scrollAreaLabel": "可滚动动态",
"components.preview.scrollAreaItem": "动态项目 {index}",
"components.preview.menuLabel": "会话菜单",
+ "components.preview.tooltipTrigger": "重命名会话",
+ "components.preview.tooltipContent": "重命名当前会话",
"components.preview.menuSectionTitle": "会话",
"components.preview.menuItemOne": "欢迎会话",
"components.preview.menuItemTwo": "设计审查",
@@ -997,6 +1005,8 @@ export const zhCNMessages = {
"detail.option.left": "左侧",
"detail.option.right": "右侧",
"detail.option.confirmation": "待确认",
+ "detail.option.top": "顶部",
+ "detail.option.bottom": "底部",
"detail.option.off": "关闭",
"detail.option.on": "开启",
"detail.option.selected": "选中",
@@ -1283,6 +1293,7 @@ export const zhTWMessages = {
"component.TerminalControlToolCard.description": "低關注終端控制軌跡,用於呈現中斷或終止終端工作階段的結果。",
"component.TodoToolCard.description": "低關注任務進度卡片,支援緊湊摘要與可展開列表兩種呈現。",
"component.Toolbar.description": "具備獨立前置、置中、尾端與橫向捲動區域的響應式工具列。",
+ "component.Tooltip.description": "具備 Portal、延遲管理、視口邊緣翻轉,並支援跟隨游標與可互動內容的懸停提示。",
"component.ViewImageToolCard.description": "低關注圖片結果卡片,包含可展開預覽和無障礙燈箱。",
"component.WebFetchToolCard.description": "低關注網頁讀取卡片,包含來源中繼資料、結果操作與內容預覽。",
"component.WebSearchToolCard.description": "低關注網頁搜尋卡片,統一呈現連結、摘要、網址與結果列表。",
@@ -1423,6 +1434,8 @@ export const zhTWMessages = {
"components.preview.scrollAreaLabel": "可捲動動態",
"components.preview.scrollAreaItem": "動態項目 {index}",
"components.preview.menuLabel": "工作階段選單",
+ "components.preview.tooltipTrigger": "重新命名工作階段",
+ "components.preview.tooltipContent": "重新命名目前的工作階段",
"components.preview.menuSectionTitle": "工作階段",
"components.preview.menuItemOne": "歡迎工作階段",
"components.preview.menuItemTwo": "設計審查",
@@ -1575,6 +1588,8 @@ export const zhTWMessages = {
"detail.option.left": "左側",
"detail.option.right": "右側",
"detail.option.confirmation": "待確認",
+ "detail.option.top": "頂部",
+ "detail.option.bottom": "底部",
"detail.option.off": "關閉",
"detail.option.on": "開啟",
"detail.option.selected": "已選取",
diff --git a/design-system/apps/design-lab/src/pages/ComponentDetailPage.tsx b/design-system/apps/design-lab/src/pages/ComponentDetailPage.tsx
index 1c9c88d3e6..42f104a4a2 100644
--- a/design-system/apps/design-lab/src/pages/ComponentDetailPage.tsx
+++ b/design-system/apps/design-lab/src/pages/ComponentDetailPage.tsx
@@ -69,6 +69,7 @@ import {
ToolbarBadge,
ToolbarGroup,
ToolbarSeparator,
+ Tooltip,
type ColorScheme,
type ConfirmDialogType,
type ContrastMode,
@@ -282,6 +283,8 @@ export function ComponentDetailPage({
? "selected"
: component.name === "ScrollArea"
? "auto"
+ : component.name === "Tooltip"
+ ? "top"
: "default",
);
const [inspectorDisabled, setInspectorDisabled] = useState(false);
@@ -345,6 +348,8 @@ export function ComponentDetailPage({
return ["selected", "unselected", "hover", "disabled"] as const;
case "Toolbar":
return ["default", "with-center", "overflow"] as const;
+ case "Tooltip":
+ return ["top", "bottom", "left", "right"] as const;
default:
return ["off", "on", "focus-visible", "disabled"] as const;
}
@@ -436,6 +441,9 @@ export function ComponentDetailPage({
if (component.name === "FieldGroup") {
return `import { Field, FieldGroup, FieldRow, FormSection, Input } from "@bitfun/ui";\nimport { Settings } from "lucide-react";\n\n}\n title="${t("components.preview.modalSectionTitle")}"\n>\n \n \n \n \n \n \n \n \n \n \n \n \n`;
}
+ if (component.name === "Tooltip") {
+ return `import { Tooltip } from "@bitfun/ui";\n\n\n ${t("components.preview.tooltipTrigger")}\n`;
+ }
if (component.name === "Menu") {
return `import { Menu, MenuItem, MenuSection, MenuSeparator } from "@bitfun/ui";\nimport { MessageCircle } from "lucide-react";\n\n`;
}
@@ -1040,6 +1048,20 @@ export function ComponentDetailPage({
);
}
+ if (component.name === "Tooltip") {
+ return (
+
+
+ {t("components.preview.tooltipTrigger")}
+
+
+ );
+ }
+
if (component.name === "Menu") {
const itemCount = state === "scrolling" ? 12 : 3;
return (
@@ -1619,7 +1641,7 @@ export function ComponentDetailPage({
))}
- ) : component.name === "ActionCard" || component.name === "ActionItem" || component.name === "Field" || component.name === "FieldGroup" || component.name === "Input" || component.name === "KeyHint" || component.name === "Menu" || component.name === "NavigationPanel" || component.name === "PageHeader" || component.name === "ScrollArea" || component.name === "SearchField" || component.name === "SegmentedControl" || component.name === "Select" || component.name === "StatusPill" ? (
+ ) : component.name === "ActionCard" || component.name === "ActionItem" || component.name === "Field" || component.name === "FieldGroup" || component.name === "Input" || component.name === "KeyHint" || component.name === "Menu" || component.name === "NavigationPanel" || component.name === "PageHeader" || component.name === "ScrollArea" || component.name === "SearchField" || component.name === "SegmentedControl" || component.name === "Select" || component.name === "StatusPill" || component.name === "Tooltip" ? (
diff --git a/design-system/apps/design-lab/src/pages/ComponentsPage.tsx b/design-system/apps/design-lab/src/pages/ComponentsPage.tsx
index 96e0ff5a8e..4a6ff35152 100644
--- a/design-system/apps/design-lab/src/pages/ComponentsPage.tsx
+++ b/design-system/apps/design-lab/src/pages/ComponentsPage.tsx
@@ -57,6 +57,7 @@ import {
ToolbarBadge,
ToolbarGroup,
ToolbarSeparator,
+ Tooltip,
type ColorScheme,
type ContrastMode,
type DensityMode,
@@ -109,6 +110,7 @@ const componentIcons = {
Switch: ToggleLeft,
TabGroup: PanelTop,
Toolbar: PanelTop,
+ Tooltip: MessageCircle,
} as const;
function ComponentCardPreview({ component }: { component: ComponentMeta }) {
@@ -452,6 +454,14 @@ function ComponentCardPreview({ component }: { component: ComponentMeta }) {
)}
/>
);
+ case "Tooltip":
+ return (
+
+
+ {t("components.preview.tooltipTrigger")}
+
+
+ );
default:
return null;
}
diff --git a/design-system/packages/design-tokens/src/system.tokens.json b/design-system/packages/design-tokens/src/system.tokens.json
index ea7a32b1eb..e48b5612c3 100644
--- a/design-system/packages/design-tokens/src/system.tokens.json
+++ b/design-system/packages/design-tokens/src/system.tokens.json
@@ -352,6 +352,20 @@
"itemIconSize": { "$value": "14px" },
"scrollbarGap": { "$value": "2px" }
},
+ "tooltip": {
+ "$type": "dimension",
+ "maxInlineSize": { "$value": "280px" },
+ "maxBlockSize": { "$value": "320px" },
+ "paddingBlock": { "$value": "6px" },
+ "paddingInline": { "$value": "10px" },
+ "surfaceRadius": { "$value": "{radius.sm}" },
+ "arrowSize": { "$value": "8px" },
+ "gap": {
+ "$description": "Distance between the trigger (or cursor) and the tooltip surface.",
+ "$value": "{space.2}"
+ },
+ "fontSize": { "$value": "{font.size.xs}" }
+ },
"modal": {
"$type": "dimension",
"viewportGutter": {
diff --git a/design-system/packages/ui/src/components/Tooltip/Tooltip.meta.ts b/design-system/packages/ui/src/components/Tooltip/Tooltip.meta.ts
new file mode 100644
index 0000000000..62da7be279
--- /dev/null
+++ b/design-system/packages/ui/src/components/Tooltip/Tooltip.meta.ts
@@ -0,0 +1,38 @@
+import type { ComponentMeta } from "../../registry.types";
+
+export const tooltipMeta = {
+ category: "feedback",
+ description: "A portaled, delay-managed hover label that flips placement at viewport edges and supports cursor-following and interactive content.",
+ maturity: "stable",
+ name: "Tooltip",
+ props: [
+ { name: "children", type: "ReactElement" },
+ { name: "content", type: "ReactNode" },
+ { defaultValue: "top", name: "placement", type: "top | bottom | left | right" },
+ { defaultValue: "hover", name: "trigger", type: "hover | click | focus" },
+ { defaultValue: "450", name: "delay", type: "number" },
+ { defaultValue: "false", name: "disabled", type: "boolean" },
+ { defaultValue: "false", name: "followCursor", type: "boolean" },
+ { defaultValue: "false", name: "interactive", type: "boolean" },
+ { name: "portalContainer", type: "TooltipPortalTarget" },
+ ],
+ states: ["default", "visible", "interactive", "instant"],
+ tokens: [
+ "color.surface.raised",
+ "color.content.primary",
+ "color.border.subtle",
+ "color.border.default",
+ "color.border.strong",
+ "font.family.sans",
+ "font.weight.regular",
+ "shadow.sm",
+ "overlay.tooltip.maxInlineSize",
+ "overlay.tooltip.maxBlockSize",
+ "overlay.tooltip.paddingBlock",
+ "overlay.tooltip.paddingInline",
+ "overlay.tooltip.surfaceRadius",
+ "overlay.tooltip.arrowSize",
+ "overlay.tooltip.gap",
+ "overlay.tooltip.fontSize",
+ ],
+} as const satisfies ComponentMeta;
diff --git a/design-system/packages/ui/src/components/Tooltip/Tooltip.module.css b/design-system/packages/ui/src/components/Tooltip/Tooltip.module.css
new file mode 100644
index 0000000000..0a3451f77a
--- /dev/null
+++ b/design-system/packages/ui/src/components/Tooltip/Tooltip.module.css
@@ -0,0 +1,166 @@
+@layer bf.components {
+ .root,
+ .arrow,
+ .content,
+ .body {
+ box-sizing: border-box;
+ }
+
+ .root {
+ --_enter-x: 0px;
+ --_enter-y: 0px;
+
+ position: fixed;
+ z-index: var(--bf-layer-tooltip);
+ opacity: 0;
+ pointer-events: none;
+ transform: translate3d(var(--_enter-x), var(--_enter-y), 0) scale(0.98);
+ transition:
+ opacity var(--bf-motion-duration-fast) ease,
+ transform var(--bf-motion-duration-fast) var(--bf-motion-easing-standard);
+ }
+
+ .root[data-bf-state="visible"] {
+ opacity: 1;
+ transform: translate3d(0, 0, 0) scale(1);
+ }
+
+ .root[data-bf-interactive="true"][data-bf-state="visible"] {
+ pointer-events: auto;
+ }
+
+ .content {
+ position: relative;
+ z-index: 1;
+ max-inline-size: var(--bf-overlay-tooltip-max-inline-size);
+ max-block-size: min(var(--bf-overlay-tooltip-max-block-size), calc(100vh - 24px));
+ overflow-y: auto;
+ overscroll-behavior: contain;
+ padding: var(--bf-overlay-tooltip-padding-block) var(--bf-overlay-tooltip-padding-inline);
+ border: var(--bf-border-width-default) solid var(--bf-color-border-subtle);
+ border-radius: var(--bf-overlay-tooltip-surface-radius);
+ background: color-mix(in srgb, var(--bf-color-surface-raised) 96%, transparent);
+ color: var(--bf-color-content-primary);
+ font-family: var(--bf-font-family-sans);
+ font-size: var(--bf-overlay-tooltip-font-size);
+ font-weight: var(--bf-font-weight-regular);
+ line-height: var(--bf-line-height-base);
+ overflow-wrap: break-word;
+ user-select: text;
+ box-shadow: var(--bf-shadow-sm);
+ backdrop-filter: var(--bf-effect-blur-base);
+ -webkit-backdrop-filter: var(--bf-effect-blur-base);
+ }
+
+ .content::-webkit-scrollbar {
+ inline-size: 4px;
+ }
+
+ .content::-webkit-scrollbar-track {
+ background: transparent;
+ }
+
+ .content::-webkit-scrollbar-thumb {
+ background: var(--bf-color-border-default);
+ border-radius: 2px;
+ }
+
+ .arrow {
+ position: absolute;
+ z-index: 0;
+ inline-size: var(--bf-overlay-tooltip-arrow-size);
+ block-size: var(--bf-overlay-tooltip-arrow-size);
+ border: var(--bf-border-width-default) solid var(--bf-color-border-subtle);
+ background: color-mix(in srgb, var(--bf-color-surface-raised) 96%, transparent);
+ transform: rotate(45deg);
+ }
+
+ .root[data-bf-placement="top"] .arrow,
+ .root[data-bf-placement="bottom"] .arrow {
+ left: calc(50% - var(--bf-overlay-tooltip-arrow-size) / 2);
+ }
+
+ .root[data-bf-placement="left"] .arrow,
+ .root[data-bf-placement="right"] .arrow {
+ top: calc(50% - var(--bf-overlay-tooltip-arrow-size) / 2);
+ }
+
+ .root[data-bf-placement="top"] .arrow {
+ bottom: -3px;
+ border-top: 0;
+ border-left: 0;
+ }
+
+ .root[data-bf-placement="bottom"] .arrow {
+ top: -3px;
+ border-right: 0;
+ border-bottom: 0;
+ }
+
+ .root[data-bf-placement="left"] .arrow {
+ right: -3px;
+ border-bottom: 0;
+ border-left: 0;
+ }
+
+ .root[data-bf-placement="right"] .arrow {
+ left: -3px;
+ border-top: 0;
+ border-right: 0;
+ }
+
+ .root[data-bf-placement="top"] {
+ --_enter-y: 3px;
+ transform-origin: center bottom;
+ }
+
+ .root[data-bf-placement="bottom"] {
+ --_enter-y: -3px;
+ transform-origin: center top;
+ }
+
+ .root[data-bf-placement="left"] {
+ --_enter-x: 3px;
+ transform-origin: right center;
+ }
+
+ .root[data-bf-placement="right"] {
+ --_enter-x: -3px;
+ transform-origin: left center;
+ }
+
+ .root[data-instant="true"] {
+ transition-duration: 0ms;
+ }
+
+ @media (prefers-contrast: more) {
+ .content,
+ .arrow {
+ border-color: var(--bf-color-border-strong);
+ }
+ }
+
+ @media (forced-colors: active) {
+ .content,
+ .arrow {
+ border-color: CanvasText;
+ background: Canvas;
+ box-shadow: none;
+ }
+ }
+
+ @media (prefers-reduced-motion: reduce) {
+ .root {
+ transition: opacity 80ms ease;
+ transform: none;
+ }
+
+ .root[data-bf-state="visible"] {
+ transform: none;
+ }
+
+ .root[data-instant="true"] {
+ transition-duration: 0ms;
+ }
+ }
+}
diff --git a/design-system/packages/ui/src/components/Tooltip/Tooltip.tsx b/design-system/packages/ui/src/components/Tooltip/Tooltip.tsx
new file mode 100644
index 0000000000..1064ddc7a7
--- /dev/null
+++ b/design-system/packages/ui/src/components/Tooltip/Tooltip.tsx
@@ -0,0 +1,472 @@
+import {
+ cloneElement,
+ createContext,
+ useCallback,
+ useContext,
+ useEffect,
+ useId,
+ useMemo,
+ useRef,
+ useState,
+ type FocusEvent as ReactFocusEvent,
+ type MouseEvent as ReactMouseEvent,
+ type ReactElement,
+ type ReactNode,
+ type Ref,
+} from "react";
+import { createPortal } from "react-dom";
+import { classNames } from "../../internal/classNames";
+import styles from "./Tooltip.module.css";
+
+export type TooltipPlacement = "top" | "bottom" | "left" | "right";
+export type TooltipTrigger = "hover" | "click" | "focus";
+export type TooltipPortalContainer = Element | DocumentFragment;
+export type TooltipPortalTarget =
+ | TooltipPortalContainer
+ | (() => TooltipPortalContainer | null)
+ | null;
+
+const DEFAULT_TOOLTIP_DELAY_MS = 450;
+const INTERACTIVE_HIDE_DELAY_MS = 400;
+/**
+ * After a tooltip hides, tooltips shown again within this window skip the
+ * open delay so scanning across adjacent triggers feels instant.
+ */
+const WARM_WINDOW_MS = 300;
+let tooltipWarmUntil = 0;
+
+/** Cursor offset when followCursor: right and down so the tooltip never covers the cursor. */
+const CURSOR_OFFSET_X = 12;
+const CURSOR_OFFSET_Y = 8;
+const GAP = 8;
+const VIEWPORT_PADDING = 8;
+
+interface TooltipContextValue {
+ delay?: number;
+ portalContainer?: TooltipPortalTarget;
+}
+
+const TooltipContext = createContext
({});
+
+export interface TooltipProviderProps {
+ children: ReactNode;
+ /** Default open delay in milliseconds for descendant tooltips. */
+ delay?: number;
+ portalContainer?: TooltipPortalTarget;
+}
+
+export function TooltipProvider({
+ children,
+ delay,
+ portalContainer,
+}: TooltipProviderProps) {
+ const value = useMemo(
+ () => ({ delay, portalContainer }),
+ [delay, portalContainer],
+ );
+
+ return {children};
+}
+
+export interface TooltipProps {
+ /** Single focusable trigger element the tooltip describes. */
+ children: ReactElement;
+ className?: string;
+ content: ReactNode;
+ /** Open delay in milliseconds. Falls back to the provider value, then 450ms. */
+ delay?: number;
+ disabled?: boolean;
+ /** Position near the mouse cursor instead of the trigger element. */
+ followCursor?: boolean;
+ /** Keep the tooltip open while hovered so its content can be selected or clicked. */
+ interactive?: boolean;
+ /** Preferred side of the trigger; flips to the opposite side when space runs out. */
+ placement?: TooltipPlacement;
+ portalContainer?: TooltipPortalTarget;
+ trigger?: TooltipTrigger;
+}
+
+function resolvePortalContainer(
+ target: TooltipPortalTarget | undefined,
+): TooltipPortalContainer | null {
+ if (typeof target === "function") return target();
+ if (target) return target;
+ return typeof document === "undefined" ? null : document.body;
+}
+
+function assignRef(ref: Ref | undefined, value: T | null): void {
+ if (!ref) return;
+ if (typeof ref === "function") {
+ ref(value);
+ return;
+ }
+ (ref as { current: T | null }).current = value;
+}
+
+const OPPOSITE_PLACEMENT: Record = {
+ top: "bottom",
+ bottom: "top",
+ left: "right",
+ right: "left",
+};
+
+function getAvailableSpace(triggerRect: DOMRect, placement: TooltipPlacement): number {
+ switch (placement) {
+ case "top":
+ return triggerRect.top - VIEWPORT_PADDING;
+ case "bottom":
+ return window.innerHeight - triggerRect.bottom - VIEWPORT_PADDING;
+ case "left":
+ return triggerRect.left - VIEWPORT_PADDING;
+ case "right":
+ return window.innerWidth - triggerRect.right - VIEWPORT_PADDING;
+ }
+}
+
+function getPositionForPlacement(
+ triggerRect: DOMRect,
+ tooltipRect: DOMRect,
+ placement: TooltipPlacement,
+): { top: number; left: number } {
+ switch (placement) {
+ case "top":
+ return {
+ top: triggerRect.top - tooltipRect.height - GAP,
+ left: triggerRect.left + (triggerRect.width - tooltipRect.width) / 2,
+ };
+ case "bottom":
+ return {
+ top: triggerRect.bottom + GAP,
+ left: triggerRect.left + (triggerRect.width - tooltipRect.width) / 2,
+ };
+ case "left":
+ return {
+ top: triggerRect.top + (triggerRect.height - tooltipRect.height) / 2,
+ left: triggerRect.left - tooltipRect.width - GAP,
+ };
+ case "right":
+ return {
+ top: triggerRect.top + (triggerRect.height - tooltipRect.height) / 2,
+ left: triggerRect.right + GAP,
+ };
+ }
+}
+
+function determineBestPlacement(
+ triggerRect: DOMRect,
+ tooltipRect: DOMRect,
+ preferredPlacement: TooltipPlacement,
+): TooltipPlacement {
+ const requiredSpace = preferredPlacement === "top" || preferredPlacement === "bottom"
+ ? tooltipRect.height + GAP
+ : tooltipRect.width + GAP;
+
+ const preferredSpace = getAvailableSpace(triggerRect, preferredPlacement);
+ if (preferredSpace >= requiredSpace) return preferredPlacement;
+
+ const oppositePlacement = OPPOSITE_PLACEMENT[preferredPlacement];
+ const oppositeSpace = getAvailableSpace(triggerRect, oppositePlacement);
+ if (oppositeSpace >= requiredSpace) return oppositePlacement;
+
+ return oppositeSpace > preferredSpace ? oppositePlacement : preferredPlacement;
+}
+
+function applyBoundaryConstraints(
+ position: { top: number; left: number },
+ tooltipRect: DOMRect,
+): { top: number; left: number } {
+ let { top, left } = position;
+
+ if (left < VIEWPORT_PADDING) {
+ left = VIEWPORT_PADDING;
+ } else if (left + tooltipRect.width > window.innerWidth - VIEWPORT_PADDING) {
+ left = window.innerWidth - tooltipRect.width - VIEWPORT_PADDING;
+ }
+
+ if (top < VIEWPORT_PADDING) {
+ top = VIEWPORT_PADDING;
+ } else if (top + tooltipRect.height > window.innerHeight - VIEWPORT_PADDING) {
+ top = window.innerHeight - tooltipRect.height - VIEWPORT_PADDING;
+ }
+
+ return { top, left };
+}
+
+interface TooltipLayout {
+ top: number;
+ left: number;
+ placement: TooltipPlacement;
+ ready: boolean;
+}
+
+export function Tooltip({
+ children,
+ className,
+ content,
+ delay,
+ disabled = false,
+ followCursor = false,
+ interactive = false,
+ placement = "top",
+ portalContainer,
+ trigger = "hover",
+}: TooltipProps) {
+ const context = useContext(TooltipContext);
+ const resolvedDelayMs = delay ?? context.delay ?? DEFAULT_TOOLTIP_DELAY_MS;
+ const resolvedPortalContainer = resolvePortalContainer(
+ portalContainer === undefined ? context.portalContainer : portalContainer,
+ );
+
+ const tooltipId = useId();
+ const [visible, setVisible] = useState(false);
+ // Single layout state (position + placement + ready) so one recalculation
+ // commits at most one re-render.
+ const [layout, setLayout] = useState({
+ top: 0,
+ left: 0,
+ placement,
+ ready: false,
+ });
+ const [mousePosition, setMousePosition] = useState<{ x: number; y: number } | null>(null);
+ const triggerRef = useRef(null);
+ const tooltipRef = useRef(null);
+ const showTimeoutRef = useRef | null>(null);
+ const hideTimeoutRef = useRef | null>(null);
+ const latestMousePositionRef = useRef<{ x: number; y: number } | null>(null);
+ const recalcFrameRef = useRef(null);
+ const instantRef = useRef(false);
+
+ const calculatePosition = useCallback(() => {
+ if (!tooltipRef.current) return;
+
+ const tooltipRect = tooltipRef.current.getBoundingClientRect();
+
+ if (followCursor && mousePosition) {
+ const raw = {
+ top: mousePosition.y + CURSOR_OFFSET_Y,
+ left: mousePosition.x + CURSOR_OFFSET_X,
+ };
+ const pos = applyBoundaryConstraints(raw, tooltipRect);
+ setLayout({ top: pos.top, left: pos.left, placement: "bottom", ready: true });
+ return;
+ }
+
+ if (!triggerRef.current) return;
+
+ const triggerRect = triggerRef.current.getBoundingClientRect();
+ const bestPlacement = determineBestPlacement(triggerRect, tooltipRect, placement);
+ const pos = applyBoundaryConstraints(
+ getPositionForPlacement(triggerRect, tooltipRect, bestPlacement),
+ tooltipRect,
+ );
+
+ setLayout({ top: pos.top, left: pos.left, placement: bestPlacement, ready: true });
+ }, [placement, followCursor, mousePosition]);
+
+ // rAF-merged recalculation for scroll/resize storms: at most one
+ // getBoundingClientRect pass per frame.
+ const scheduleCalculatePosition = useCallback(() => {
+ if (recalcFrameRef.current !== null) return;
+ recalcFrameRef.current = requestAnimationFrame(() => {
+ recalcFrameRef.current = null;
+ calculatePosition();
+ });
+ }, [calculatePosition]);
+
+ const showTooltip = useCallback((event?: ReactMouseEvent) => {
+ if (disabled) return;
+ if (showTimeoutRef.current) clearTimeout(showTimeoutRef.current);
+ if (hideTimeoutRef.current) {
+ clearTimeout(hideTimeoutRef.current);
+ hideTimeoutRef.current = null;
+ }
+ if (followCursor && event) {
+ latestMousePositionRef.current = { x: event.clientX, y: event.clientY };
+ }
+ const openDelay = trigger === "hover" && Date.now() < tooltipWarmUntil
+ ? 0
+ : resolvedDelayMs;
+ instantRef.current = openDelay === 0;
+ showTimeoutRef.current = setTimeout(() => {
+ showTimeoutRef.current = null;
+ if (followCursor) {
+ setMousePosition(latestMousePositionRef.current);
+ }
+ setLayout((prev) => (prev.ready ? { ...prev, ready: false } : prev));
+ setVisible(true);
+ }, openDelay);
+ }, [disabled, followCursor, resolvedDelayMs, trigger]);
+
+ const hideTooltip = useCallback(() => {
+ if (showTimeoutRef.current) {
+ clearTimeout(showTimeoutRef.current);
+ showTimeoutRef.current = null;
+ }
+ if (hideTimeoutRef.current) {
+ clearTimeout(hideTimeoutRef.current);
+ hideTimeoutRef.current = null;
+ }
+ if (visible) {
+ tooltipWarmUntil = Date.now() + WARM_WINDOW_MS;
+ }
+ setVisible(false);
+ setLayout((prev) => (prev.ready ? { ...prev, ready: false } : prev));
+ if (followCursor) {
+ latestMousePositionRef.current = null;
+ setMousePosition(null);
+ }
+ }, [followCursor, visible]);
+
+ const scheduleHideTooltip = useCallback(() => {
+ if (!interactive) {
+ hideTooltip();
+ return;
+ }
+
+ if (hideTimeoutRef.current) clearTimeout(hideTimeoutRef.current);
+ hideTimeoutRef.current = setTimeout(() => {
+ hideTimeoutRef.current = null;
+ hideTooltip();
+ }, INTERACTIVE_HIDE_DELAY_MS);
+ }, [hideTooltip, interactive]);
+
+ useEffect(() => {
+ setLayout((prev) => (prev.placement === placement ? prev : { ...prev, placement }));
+ }, [placement]);
+
+ // When the tooltip becomes disabled (e.g. the parent opens a menu that
+ // covers the trigger), cancel any pending show timer and force-hide so a
+ // tooltip cannot appear or linger above the new overlay.
+ useEffect(() => {
+ if (disabled) hideTooltip();
+ }, [disabled, hideTooltip]);
+
+ useEffect(() => {
+ if (!visible) return;
+
+ scheduleCalculatePosition();
+ if (!followCursor) {
+ window.addEventListener("scroll", scheduleCalculatePosition, { capture: true, passive: true });
+ }
+ window.addEventListener("resize", scheduleCalculatePosition, { passive: true });
+ return () => {
+ if (!followCursor) {
+ window.removeEventListener("scroll", scheduleCalculatePosition, { capture: true });
+ }
+ window.removeEventListener("resize", scheduleCalculatePosition);
+ if (recalcFrameRef.current !== null) {
+ cancelAnimationFrame(recalcFrameRef.current);
+ recalcFrameRef.current = null;
+ }
+ };
+ }, [visible, followCursor, scheduleCalculatePosition]);
+
+ useEffect(() => () => {
+ if (showTimeoutRef.current) clearTimeout(showTimeoutRef.current);
+ if (hideTimeoutRef.current) clearTimeout(hideTimeoutRef.current);
+ }, []);
+
+ const childProps = children.props as Record;
+ const childRef = (children as ReactElement & { ref?: Ref }).ref;
+
+ const handleTriggerRef = useCallback((node: HTMLElement | null) => {
+ triggerRef.current = node;
+ assignRef(childRef, node);
+ }, [childRef]);
+
+ const handleMouseEnter = (event: ReactMouseEvent) => {
+ if (trigger === "hover") showTooltip(event);
+ (childProps.onMouseEnter as ((event: ReactMouseEvent) => void) | undefined)?.(event);
+ };
+
+ const handleMouseLeave = (event: ReactMouseEvent) => {
+ if (trigger === "hover") scheduleHideTooltip();
+ (childProps.onMouseLeave as ((event: ReactMouseEvent) => void) | undefined)?.(event);
+ };
+
+ const handleMouseMove = (event: ReactMouseEvent) => {
+ if (followCursor && !visible) {
+ latestMousePositionRef.current = { x: event.clientX, y: event.clientY };
+ }
+ (childProps.onMouseMove as ((event: ReactMouseEvent) => void) | undefined)?.(event);
+ };
+
+ const handleClick = (event: ReactMouseEvent) => {
+ // Always cancel any pending show timer so a click before the tooltip
+ // appears cannot surface a stale tooltip after the trigger is covered
+ // by a menu or backdrop (which prevents the natural mouseleave).
+ if (showTimeoutRef.current) {
+ clearTimeout(showTimeoutRef.current);
+ showTimeoutRef.current = null;
+ }
+ if (visible) {
+ hideTooltip();
+ } else if (trigger === "click") {
+ showTooltip();
+ }
+ (childProps.onClick as ((event: ReactMouseEvent) => void) | undefined)?.(event);
+ };
+
+ const handleFocus = (event: ReactFocusEvent) => {
+ if (trigger === "focus") showTooltip();
+ (childProps.onFocus as ((event: ReactFocusEvent) => void) | undefined)?.(event);
+ };
+
+ const handleBlur = (event: ReactFocusEvent) => {
+ if (trigger === "focus") hideTooltip();
+ (childProps.onBlur as ((event: ReactFocusEvent) => void) | undefined)?.(event);
+ };
+
+ const isShown = visible && layout.ready;
+
+ const triggerElement = cloneElement(children as ReactElement>, {
+ ref: handleTriggerRef,
+ onMouseEnter: handleMouseEnter,
+ onMouseLeave: handleMouseLeave,
+ onMouseMove: followCursor ? handleMouseMove : childProps.onMouseMove,
+ onClick: handleClick,
+ onFocus: handleFocus,
+ onBlur: handleBlur,
+ "aria-describedby": isShown
+ ? [childProps["aria-describedby"], tooltipId].filter(Boolean).join(" ")
+ : childProps["aria-describedby"],
+ } as Record);
+
+ return (
+ <>
+ {triggerElement}
+ {visible && resolvedPortalContainer && createPortal(
+ ,
+ resolvedPortalContainer,
+ )}
+ >
+ );
+}
diff --git a/design-system/packages/ui/src/components/Tooltip/index.ts b/design-system/packages/ui/src/components/Tooltip/index.ts
new file mode 100644
index 0000000000..d6c6ee23b7
--- /dev/null
+++ b/design-system/packages/ui/src/components/Tooltip/index.ts
@@ -0,0 +1,10 @@
+export {
+ Tooltip,
+ TooltipProvider,
+ type TooltipPlacement,
+ type TooltipPortalContainer,
+ type TooltipPortalTarget,
+ type TooltipProps,
+ type TooltipProviderProps,
+ type TooltipTrigger,
+} from "./Tooltip";
diff --git a/design-system/packages/ui/src/index.ts b/design-system/packages/ui/src/index.ts
index 9e25c3abc8..4c26911dc1 100644
--- a/design-system/packages/ui/src/index.ts
+++ b/design-system/packages/ui/src/index.ts
@@ -172,6 +172,16 @@ export {
type ToolbarSeparatorProps,
type ToolbarSize,
} from "./components/Toolbar";
+export {
+ Tooltip,
+ TooltipProvider,
+ type TooltipPlacement,
+ type TooltipPortalContainer,
+ type TooltipPortalTarget,
+ type TooltipProps,
+ type TooltipProviderProps,
+ type TooltipTrigger,
+} from "./components/Tooltip";
export { SessionIcon, type SessionIconProps } from "./icons";
export { Stack, type StackProps } from "./primitives/Stack";
export {
diff --git a/design-system/packages/ui/src/registry.ts b/design-system/packages/ui/src/registry.ts
index 873c660fb1..87235f61f8 100644
--- a/design-system/packages/ui/src/registry.ts
+++ b/design-system/packages/ui/src/registry.ts
@@ -23,6 +23,7 @@ import { statusPillMeta } from "./components/StatusPill/StatusPill.meta";
import { switchMeta } from "./components/Switch/Switch.meta";
import { tabGroupMeta } from "./components/TabGroup/TabGroup.meta";
import { toolbarMeta } from "./components/Toolbar/Toolbar.meta";
+import { tooltipMeta } from "./components/Tooltip/Tooltip.meta";
import { askUserMeta } from "./flow-chat/ask-user/AskUser.meta";
import { chatComposerMeta } from "./flow-chat/composer/ChatComposer.meta";
import { ambientToolCardMeta } from "./flow-chat/tool-cards/AmbientToolCard.meta";
@@ -116,6 +117,7 @@ export const componentRegistry = [
terminalControlToolCardMeta,
todoToolCardMeta,
toolbarMeta,
+ tooltipMeta,
viewImageToolCardMeta,
webFetchToolCardMeta,
webSearchToolCardMeta,
diff --git a/design-system/packages/ui/tests/registry.test.mjs b/design-system/packages/ui/tests/registry.test.mjs
index 6f9b951e16..386ab7cf55 100644
--- a/design-system/packages/ui/tests/registry.test.mjs
+++ b/design-system/packages/ui/tests/registry.test.mjs
@@ -62,6 +62,7 @@ test("registry exposes only the formal stable components", () => {
"TerminalControlToolCard",
"TodoToolCard",
"Toolbar",
+ "Tooltip",
"ViewImageToolCard",
"WebFetchToolCard",
"WebSearchToolCard",
diff --git a/design-system/packages/ui/tests/tooltip.test.mjs b/design-system/packages/ui/tests/tooltip.test.mjs
new file mode 100644
index 0000000000..f4bbf5199b
--- /dev/null
+++ b/design-system/packages/ui/tests/tooltip.test.mjs
@@ -0,0 +1,67 @@
+import assert from "node:assert/strict";
+import { readFile } from "node:fs/promises";
+import test from "node:test";
+import { createElement } from "react";
+import { renderToStaticMarkup } from "react-dom/server";
+import { Tooltip, TooltipProvider } from "../dist/index.js";
+
+test("Tooltip renders only the trigger until it is shown", () => {
+ const markup = renderToStaticMarkup(
+ createElement(
+ Tooltip,
+ { content: "Open settings" },
+ createElement("button", { type: "button" }, "Settings"),
+ ),
+ );
+
+ assert.match(markup, /Settings<\/button>/);
+ assert.doesNotMatch(markup, /role="tooltip"/);
+});
+
+test("TooltipProvider accepts shared delay and portal configuration", () => {
+ const markup = renderToStaticMarkup(
+ createElement(
+ TooltipProvider,
+ { delay: 0, portalContainer: null },
+ createElement(
+ Tooltip,
+ { content: "Rename", placement: "right" },
+ createElement("button", { type: "button" }, "Rename"),
+ ),
+ ),
+ );
+
+ assert.match(markup, /Rename<\/button>/);
+});
+
+test("Tooltip owns delayed opening, viewport flipping, and interactive persistence", async () => {
+ const source = await readFile(
+ new URL("../src/components/Tooltip/Tooltip.tsx", import.meta.url),
+ "utf8",
+ );
+
+ assert.match(source, /DEFAULT_TOOLTIP_DELAY_MS = 450/);
+ assert.match(source, /WARM_WINDOW_MS/);
+ assert.match(source, /determineBestPlacement/);
+ assert.match(source, /applyBoundaryConstraints/);
+ assert.match(source, /INTERACTIVE_HIDE_DELAY_MS/);
+ assert.match(source, /followCursor/);
+ assert.match(source, /aria-describedby/);
+ assert.match(source, /role="tooltip"/);
+ assert.match(source, /requestAnimationFrame/);
+});
+
+test("Tooltip styling uses only public surface, geometry, and elevation tokens", async () => {
+ const styles = await readFile(
+ new URL("../src/components/Tooltip/Tooltip.module.css", import.meta.url),
+ "utf8",
+ );
+
+ assert.match(styles, /--bf-overlay-tooltip-max-inline-size/);
+ assert.match(styles, /--bf-overlay-tooltip-arrow-size/);
+ assert.match(styles, /--bf-color-surface-raised/);
+ assert.match(styles, /--bf-color-border-subtle/);
+ assert.match(styles, /--bf-shadow-sm/);
+ assert.match(styles, /--bf-layer-tooltip/);
+ assert.doesNotMatch(styles, /#[0-9a-f]{3,8}/i);
+});
From 0f94c3a5009730e9e6b5d0c6bf8394be715954f3 Mon Sep 17 00:00:00 2001
From: kev1n77
Date: Fri, 28 Aug 2026 22:29:21 +0800
Subject: [PATCH 8/9] feat(web-ui): adopt design-system Tooltip across product
surfaces
Switch Tooltip imports from the legacy component library to @bitfun/ui outside flow_chat, mount TooltipProvider with the appearance overlay host, retarget custom tooltip width overrides to data-bf parts, and align test mocks. The legacy Tooltip stays for flow_chat and internal legacy components.
---
src/web-ui/src/app/App.tsx | 4 ++-
.../components/AboutDialog/AboutDialog.tsx | 4 +--
.../src/app/components/NavBar/NavBar.tsx | 3 ++-
.../src/app/components/NavPanel/MainNav.tsx | 3 +--
.../components/AssistantSessionCreateMenu.tsx | 4 +--
.../NavPanel/components/GithubStarButton.tsx | 3 ++-
.../NavPanel/components/MiniAppEntry.tsx | 2 +-
.../NavPanel/components/NavItem.tsx | 3 ++-
.../components/PersistentFooterActions.tsx | 4 +--
.../components/WorkspaceSessionFilterMenu.tsx | 3 ++-
.../WorkspaceSessionGroupingToggle.tsx | 2 +-
.../sections/sessions/SessionsSection.tsx | 4 +--
.../sections/workspaces/WorkspaceItem.tsx | 4 +--
.../WorkspaceProjectPermissionsDialog.tsx | 4 +--
.../TitleBar/NotificationButton.tsx | 4 +--
.../components/panels/BranchSelectModal.tsx | 4 +--
.../panels/DiffFullscreenViewer.tsx | 4 +--
.../src/app/components/panels/FilesPanel.tsx | 4 +--
.../components/panels/base/FlexiblePanel.tsx | 4 +--
.../content-canvas/anchor-zone/AnchorZone.tsx | 3 ++-
.../editor-area/SplitHandle.tsx | 3 ++-
.../content-canvas/empty-state/EmptyState.tsx | 3 ++-
.../mission-control/ThumbnailCard.tsx | 3 ++-
.../content-canvas/quick-look/QuickLook.tsx | 3 ++-
.../panels/content-canvas/tab-bar/Tab.tsx | 3 ++-
.../panels/content-canvas/tab-bar/TabBar.tsx | 3 ++-
.../tab-bar/TabOverflowMenu.tsx | 4 +--
.../review-platform/ReviewPlatformPanel.tsx | 4 +--
.../scheduled-jobs/LocalizedDateTimeField.tsx | 4 +--
.../scheduled-jobs/ScheduledJobsView.tsx | 4 +--
.../src/app/layout/FloatingMiniChat.tsx | 4 +--
.../src/app/scenes/agents/AgentsScene.tsx | 4 +--
.../components/AgentCapabilityTooltip.scss | 3 ++-
.../components/AgentCapabilityTooltip.tsx | 3 ++-
.../agents/components/CreateAgentPage.scss | 3 ++-
.../agents/components/CreateAgentPage.tsx | 4 +--
.../agents/components/SkillGroupPicker.tsx | 4 +--
.../agents/components/ToolGroupPicker.tsx | 4 +--
.../app/scenes/file-viewer/FileViewerNav.tsx | 4 +--
src/web-ui/src/app/scenes/git/GitNav.tsx | 4 +--
src/web-ui/src/app/scenes/git/GitScene.tsx | 4 +--
.../src/app/scenes/git/views/BranchesView.tsx | 4 +--
.../app/scenes/git/views/WorkingCopyView.tsx | 4 +--
.../src/app/scenes/miniapps/MiniAppScene.tsx | 4 +--
.../customization/MiniAppCustomizeEntry.tsx | 4 +--
.../customization/MiniAppCustomizePanel.tsx | 4 +--
.../scenes/profile/views/AssistantCard.tsx | 4 +--
.../profile/views/AssistantConfigPage.tsx | 4 +--
.../profile/views/AssistantDefaultsPage.tsx | 4 +--
.../components/KeyboardShortcutsTab.tsx | 4 +--
src/web-ui/src/app/scenes/shell/ShellNav.tsx | 3 +--
.../shell/components/ShellNavEntryItem.tsx | 2 +-
.../components/ShellNavWorkspaceSwitcher.tsx | 3 +--
.../src/app/scenes/todos/TodosScene.tsx | 4 +--
.../scenes/todos/components/TodoItemRow.tsx | 4 +--
.../dispatch/DispatchTargetPicker.tsx | 3 ++-
.../relay-deploy/RelayDeployWizard.tsx | 4 +--
.../ssh-remote/PortForwardDialog.test.tsx | 2 +-
.../features/ssh-remote/PortForwardDialog.tsx | 4 +--
.../ssh-remote/SSHAuthPromptDialog.tsx | 4 +--
.../ssh-remote/SSHConnectionDialog.test.tsx | 16 +++++++++++-
.../ssh-remote/SSHConnectionDialog.tsx | 4 +--
.../config/components/AcpAgentsConfig.tsx | 4 +--
.../AppearanceMarketDialog.test.tsx | 4 +++
.../AppearancePackageConfigSection.tsx | 4 +--
.../components/AppearanceSettingsPage.scss | 2 +-
.../components/ApplicationSettingsPages.tsx | 3 +--
.../config/components/ExternalMcpOverview.tsx | 4 +--
.../components/ExternalSourcesConfig.tsx | 4 +--
.../GlobalPermissionRulesDialog.test.tsx | 5 +---
.../GlobalPermissionRulesDialog.tsx | 4 +--
.../components/LocalVoiceModelsConfig.tsx | 4 +--
.../config/components/McpToolsConfig.tsx | 4 +--
.../config/components/MemorySettingsPage.tsx | 4 +--
.../config/components/ModelSettingsPage.tsx | 4 +--
.../config/components/QuickActionsConfig.tsx | 4 +--
.../components/ReasoningPresetEditor.test.tsx | 2 +-
.../components/ReasoningPresetEditor.tsx | 4 +--
.../components/RuntimeSettingsPages.tsx | 4 +--
.../config/components/SkillsConfig.tsx | 4 +--
.../components/UsageStatisticsConfig.test.tsx | 2 +-
.../components/UsageStatisticsConfig.tsx | 3 +--
.../components/VoiceInputDiagnostics.tsx | 4 +--
.../components/WorktreeSettingsPage.test.tsx | 2 +-
.../components/WorktreeSettingsPage.tsx | 4 +--
.../external-sources/ExternalAppsOverview.tsx | 4 +--
.../bitfun-canvas/runtime/sdk/controls.tsx | 2 +-
.../tools/editor/components/DiffEditor.tsx | 3 ++-
.../editor/components/EditorBreadcrumb.tsx | 3 ++-
.../editor/components/EditorStatusBar.tsx | 3 ++-
.../tools/editor/components/ImageViewer.tsx | 4 +--
.../editor/components/MarkdownEditor.test.tsx | 25 +++++++++++++++++++
.../tools/editor/components/PlanViewer.tsx | 4 +--
.../meditor/components/TiptapEditor.tsx | 4 +--
.../file-system/components/FileExplorer.tsx | 4 +--
.../git/components/PushButton/PushButton.tsx | 4 +--
96 files changed, 216 insertions(+), 161 deletions(-)
diff --git a/src/web-ui/src/app/App.tsx b/src/web-ui/src/app/App.tsx
index 8fc4a9f178..6574daf48b 100644
--- a/src/web-ui/src/app/App.tsx
+++ b/src/web-ui/src/app/App.tsx
@@ -1,5 +1,5 @@
import { lazy, Suspense, useEffect, useCallback, useLayoutEffect, useState, useRef } from 'react';
-import { ConfirmDialogProvider, ModalProvider } from '@bitfun/ui';
+import { ConfirmDialogProvider, ModalProvider, TooltipProvider } from '@bitfun/ui';
import { useShortcut } from '@/infrastructure/hooks/useShortcut';
import { useHasDismissibleLayer } from '@/infrastructure/hooks/useDismissibleLayer';
import { dismissibleLayerManager } from '@/infrastructure/services/DismissibleLayerManager';
@@ -924,6 +924,7 @@ function App() {
closeLabel={tComponents('modal.close')}
portalContainer={getAppearanceOverlayHost}
>
+
+
);
}
diff --git a/src/web-ui/src/app/components/AboutDialog/AboutDialog.tsx b/src/web-ui/src/app/components/AboutDialog/AboutDialog.tsx
index 8c691d9305..d5526bace6 100644
--- a/src/web-ui/src/app/components/AboutDialog/AboutDialog.tsx
+++ b/src/web-ui/src/app/components/AboutDialog/AboutDialog.tsx
@@ -4,10 +4,10 @@
* persistent GitHub repository entry point.
*/
-import { Button, Modal } from '@bitfun/ui';
+import { Button, Modal, Tooltip } from '@bitfun/ui';
import React, { useCallback, useEffect, useState } from 'react';
import { useI18n } from '@/infrastructure/i18n';
-import { Alert, Tooltip } from '@/component-library';
+import { Alert } from '@/component-library';
import {
ArrowRight,
CalendarDays,
diff --git a/src/web-ui/src/app/components/NavBar/NavBar.tsx b/src/web-ui/src/app/components/NavBar/NavBar.tsx
index 0a765e4309..047717a1e0 100644
--- a/src/web-ui/src/app/components/NavBar/NavBar.tsx
+++ b/src/web-ui/src/app/components/NavBar/NavBar.tsx
@@ -11,13 +11,14 @@
import React, { useCallback, useMemo, useRef } from 'react';
import { ArrowLeft, ArrowRight } from 'lucide-react';
-import { Tooltip } from '@/component-library';
+
import { useNavSceneStore } from '../../stores/navSceneStore';
import { useI18n } from '../../../infrastructure/i18n';
import { PanelLeftIcon } from '../TitleBar/PanelIcons';
import { createLogger } from '@/shared/utils/logger';
import { isMacOSDesktopRuntime, supportsNativeWindowDragging } from '@/infrastructure/runtime';
import './NavBar.scss';
+import { Tooltip } from '@bitfun/ui';
const log = createLogger('NavBar');
diff --git a/src/web-ui/src/app/components/NavPanel/MainNav.tsx b/src/web-ui/src/app/components/NavPanel/MainNav.tsx
index 671a03aef7..f3a950a8a7 100644
--- a/src/web-ui/src/app/components/NavPanel/MainNav.tsx
+++ b/src/web-ui/src/app/components/NavPanel/MainNav.tsx
@@ -12,7 +12,7 @@
import React, { useCallback, useState, useMemo, useEffect, useRef, useSyncExternalStore } from 'react';
import { createPortal } from 'react-dom';
-import { KeyHint, Menu, MenuItem, MenuSection, MenuSeparator } from '@bitfun/ui';
+import { KeyHint, Menu, MenuItem, MenuSection, MenuSeparator, Tooltip } from '@bitfun/ui';
import { getAppearanceOverlayHost } from '@/infrastructure/appearance/runtime/AppearanceOverlayHost';
import { isImeOwnedKeyboardEvent } from '@/shared/utils/ime';
import { FolderOpen, FolderPlus, History, Check, User, Users, Puzzle, ChevronDown, Network, Search, CalendarClock } from 'lucide-react';
@@ -21,7 +21,6 @@ import {
BITFUN_ICON_SIZE,
NavigationExtensionsCompatibilityIcon,
NavigationSessionContextAddIcon,
- Tooltip,
} from '@/component-library';
import { useSceneManager } from '../../hooks/useSceneManager';
import { useI18n } from '@/infrastructure/i18n/hooks/useI18n';
diff --git a/src/web-ui/src/app/components/NavPanel/components/AssistantSessionCreateMenu.tsx b/src/web-ui/src/app/components/NavPanel/components/AssistantSessionCreateMenu.tsx
index 8a4d7badb9..b35da39f61 100644
--- a/src/web-ui/src/app/components/NavPanel/components/AssistantSessionCreateMenu.tsx
+++ b/src/web-ui/src/app/components/NavPanel/components/AssistantSessionCreateMenu.tsx
@@ -1,8 +1,8 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { ChevronDown, Plus } from 'lucide-react';
-import { Menu, MenuItem } from '@bitfun/ui';
-import { Tooltip } from '@/component-library';
+import { Menu, MenuItem, Tooltip } from '@bitfun/ui';
+
import { getAppearanceOverlayHost } from '@/infrastructure/appearance/runtime/AppearanceOverlayHost';
import { useI18n } from '@/infrastructure/i18n/hooks/useI18n';
import type { WorkspaceInfo } from '@/shared/types';
diff --git a/src/web-ui/src/app/components/NavPanel/components/GithubStarButton.tsx b/src/web-ui/src/app/components/NavPanel/components/GithubStarButton.tsx
index cf74425099..a8a1718d56 100644
--- a/src/web-ui/src/app/components/NavPanel/components/GithubStarButton.tsx
+++ b/src/web-ui/src/app/components/NavPanel/components/GithubStarButton.tsx
@@ -1,9 +1,10 @@
import React, { useCallback, useState } from 'react';
import { Star } from 'lucide-react';
-import { Tooltip } from '@/component-library';
+
import { useI18n } from '@/infrastructure/i18n/hooks/useI18n';
import { systemAPI } from '@/infrastructure/api/service-api/SystemAPI';
import { createLogger } from '@/shared/utils/logger';
+import { Tooltip } from '@bitfun/ui';
import {
GITHUB_STAR_URL,
isGithubStarCtaDismissed,
diff --git a/src/web-ui/src/app/components/NavPanel/components/MiniAppEntry.tsx b/src/web-ui/src/app/components/NavPanel/components/MiniAppEntry.tsx
index 30337d7d5c..1caf3882d8 100644
--- a/src/web-ui/src/app/components/NavPanel/components/MiniAppEntry.tsx
+++ b/src/web-ui/src/app/components/NavPanel/components/MiniAppEntry.tsx
@@ -2,12 +2,12 @@ import React, { useMemo } from 'react';
import {
BITFUN_ICON_SIZE,
NavigationMiniAppIcon,
- Tooltip,
} from '@/component-library';
import { useI18n } from '@/infrastructure/i18n/hooks/useI18n';
import { useMiniAppStore } from '@/app/scenes/miniapps/miniAppStore';
import { useMiniAppActivity } from '@/app/scenes/miniapps/hooks/useMiniAppActivity';
import { renderMiniAppIcon, getMiniAppIconGradient } from '@/app/scenes/miniapps/utils/miniAppIcons';
+import { Tooltip } from '@bitfun/ui';
const MAX_VISIBLE_ACTIVE_APPS = 3;
diff --git a/src/web-ui/src/app/components/NavPanel/components/NavItem.tsx b/src/web-ui/src/app/components/NavPanel/components/NavItem.tsx
index 8c650ad18b..e6a31f8fad 100644
--- a/src/web-ui/src/app/components/NavPanel/components/NavItem.tsx
+++ b/src/web-ui/src/app/components/NavPanel/components/NavItem.tsx
@@ -8,8 +8,9 @@
import React, { useRef } from 'react';
import type { LucideIcon } from 'lucide-react';
-import { Tooltip } from '@/component-library';
+
import type { NavItem as NavItemConfig } from '../types';
+import { Tooltip } from '@bitfun/ui';
interface NavItemProps {
item: NavItemConfig;
diff --git a/src/web-ui/src/app/components/NavPanel/components/PersistentFooterActions.tsx b/src/web-ui/src/app/components/NavPanel/components/PersistentFooterActions.tsx
index 023fe204cf..ac7001e25c 100644
--- a/src/web-ui/src/app/components/NavPanel/components/PersistentFooterActions.tsx
+++ b/src/web-ui/src/app/components/NavPanel/components/PersistentFooterActions.tsx
@@ -6,8 +6,8 @@ import {
PictureInPicture2,
Palette,
} from 'lucide-react';
-import { Menu, MenuItem, MenuSeparator, Modal } from '@bitfun/ui';
-import { Tooltip, PresenceBoundary } from '@/component-library';
+import { Menu, MenuItem, MenuSeparator, Modal, Tooltip } from '@bitfun/ui';
+import { PresenceBoundary } from '@/component-library';
import { useI18n } from '@/infrastructure/i18n/hooks/useI18n';
import { useSceneStore } from '../../../stores/sceneStore';
import { activateProductAction } from '@/app/global-search/productActionActivator';
diff --git a/src/web-ui/src/app/components/NavPanel/components/WorkspaceSessionFilterMenu.tsx b/src/web-ui/src/app/components/NavPanel/components/WorkspaceSessionFilterMenu.tsx
index fd4deead7c..c988ee1128 100644
--- a/src/web-ui/src/app/components/NavPanel/components/WorkspaceSessionFilterMenu.tsx
+++ b/src/web-ui/src/app/components/NavPanel/components/WorkspaceSessionFilterMenu.tsx
@@ -1,10 +1,11 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { Check, ChevronRight, ListFilter } from 'lucide-react';
-import { Tooltip } from '@/component-library';
+
import { useI18n } from '@/infrastructure/i18n';
import { getAppearanceOverlayHost } from '@/infrastructure/appearance/runtime/AppearanceOverlayHost';
import { flowChatStore } from '@/flow_chat/store/FlowChatStore';
+import { Tooltip } from '@bitfun/ui';
import {
DEFAULT_WORKSPACE_SESSION_VIEW,
hasWorkspaceSessionFilters,
diff --git a/src/web-ui/src/app/components/NavPanel/components/WorkspaceSessionGroupingToggle.tsx b/src/web-ui/src/app/components/NavPanel/components/WorkspaceSessionGroupingToggle.tsx
index a93fc57e0f..88429874ec 100644
--- a/src/web-ui/src/app/components/NavPanel/components/WorkspaceSessionGroupingToggle.tsx
+++ b/src/web-ui/src/app/components/NavPanel/components/WorkspaceSessionGroupingToggle.tsx
@@ -3,9 +3,9 @@ import {
BITFUN_ICON_SIZE,
NavigationSessionViewAllIcon,
NavigationSessionViewGroupedIcon,
- Tooltip,
} from '@/component-library';
import { useI18n } from '@/infrastructure/i18n';
+import { Tooltip } from '@bitfun/ui';
import {
getNextWorkspaceSessionGrouping,
useWorkspaceSessionViewStore,
diff --git a/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.tsx b/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.tsx
index 31b30c6415..dca367836b 100644
--- a/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.tsx
+++ b/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.tsx
@@ -6,10 +6,10 @@
*/
import React, { lazy, Suspense, useCallback, useEffect, useMemo, useRef, useState } from 'react';
-import { Button, IconButton, Input } from '@bitfun/ui';
+import { Button, IconButton, Input, Tooltip } from '@bitfun/ui';
import { createPortal } from 'react-dom';
import { Pencil, Trash2, Check, X, Bot, MoreHorizontal, Loader2, Archive, Clock3, Copy, FileDown, ChevronLeft, ChevronDown, ChevronUp } from 'lucide-react';
-import { PresenceBoundary, Tooltip } from '@/component-library';
+import { PresenceBoundary } from '@/component-library';
import { useI18n } from '@/infrastructure/i18n';
import { flowChatStore } from '../../../../../flow_chat/store/FlowChatStore';
import { flowChatManager } from '../../../../../flow_chat/services/FlowChatManager';
diff --git a/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceItem.tsx b/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceItem.tsx
index 194be2d566..85d8a014c3 100644
--- a/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceItem.tsx
+++ b/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceItem.tsx
@@ -1,9 +1,9 @@
-import { Button, Modal, ConfirmDialog, Menu, MenuItem, MenuSeparator } from '@bitfun/ui';
+import { Button, Modal, ConfirmDialog, Menu, MenuItem, MenuSeparator, Tooltip } from '@bitfun/ui';
import React, { lazy, Suspense, useCallback, useContext, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';
import { createPortal } from 'react-dom';
import { Folder, FolderOpen, MoreHorizontal, FolderSearch, Plus, ChevronDown, Trash2, RotateCcw, Copy, FileText, Bot, Link2, ListChecks, Loader2, Clock3, ShieldCheck, Pencil, Network } from 'lucide-react';
import { useTranslation } from 'react-i18next';
-import { InputDialog, PresenceBoundary, BITFUN_ICON_SIZE, SessionGroupAssistantIcon, SessionGroupAssistantSelectedIcon, SessionGroupRemoteWorkspaceIcon, SessionGroupRemoteWorkspaceSelectedIcon, SessionGroupWorkspaceIcon, SessionGroupWorkspaceSelectedIcon, Tooltip } from '@/component-library';
+import { InputDialog, PresenceBoundary, BITFUN_ICON_SIZE, SessionGroupAssistantIcon, SessionGroupAssistantSelectedIcon, SessionGroupRemoteWorkspaceIcon, SessionGroupRemoteWorkspaceSelectedIcon, SessionGroupWorkspaceIcon, SessionGroupWorkspaceSelectedIcon } from '@/component-library';
import { useI18n } from '@/infrastructure/i18n';
import { getAppearanceOverlayHost } from '@/infrastructure/appearance/runtime/AppearanceOverlayHost';
diff --git a/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceProjectPermissionsDialog.tsx b/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceProjectPermissionsDialog.tsx
index ae4157eab1..ac4041db4a 100644
--- a/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceProjectPermissionsDialog.tsx
+++ b/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceProjectPermissionsDialog.tsx
@@ -1,7 +1,7 @@
-import { Button, IconButton, Input, Modal, Select, type SelectOption } from '@bitfun/ui';
+import { Button, IconButton, Input, Modal, Select, type SelectOption, Tooltip } from '@bitfun/ui';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { ArrowDown, ArrowUp, Plus, Save, ShieldCheck, Trash2 } from 'lucide-react';
-import { Tooltip } from '@/component-library';
+
import { confirmDanger } from '@/infrastructure/confirm-dialog';
import {
permissionAPI,
diff --git a/src/web-ui/src/app/components/TitleBar/NotificationButton.tsx b/src/web-ui/src/app/components/TitleBar/NotificationButton.tsx
index fa4930d275..9f5a7db79c 100644
--- a/src/web-ui/src/app/components/TitleBar/NotificationButton.tsx
+++ b/src/web-ui/src/app/components/TitleBar/NotificationButton.tsx
@@ -7,8 +7,8 @@
import React, { useRef, useEffect, useState } from 'react';
import { Bell, BellDot, BellRing } from 'lucide-react';
-import { MenuItem } from '@bitfun/ui';
-import { Tooltip } from '@/component-library';
+import { MenuItem, Tooltip } from '@bitfun/ui';
+
import { useI18n } from '@/infrastructure/i18n/hooks/useI18n';
import {
useUnreadCount,
diff --git a/src/web-ui/src/app/components/panels/BranchSelectModal.tsx b/src/web-ui/src/app/components/panels/BranchSelectModal.tsx
index 1ac2e5885c..b53b5b36d7 100644
--- a/src/web-ui/src/app/components/panels/BranchSelectModal.tsx
+++ b/src/web-ui/src/app/components/panels/BranchSelectModal.tsx
@@ -3,14 +3,14 @@
* Supports selecting existing branches or creating new branches
*/
-import { Button, IconButton, Input } from '@bitfun/ui';
+import { Button, IconButton, Input, Tooltip } from '@bitfun/ui';
import React, { useState, useEffect, useMemo, useCallback, useRef } from 'react';
import { createPortal } from 'react-dom';
import { getAppearanceOverlayHost } from '@/infrastructure/appearance/runtime/AppearanceOverlayHost';
import { GitBranch, Plus, X } from 'lucide-react';
import { createLogger } from '@/shared/utils/logger';
import { isImeOwnedKeyboardEvent } from '@/shared/utils/ime';
-import { Checkbox, PresenceBoundary, PRESENCE_BOUNDARY_MIN_EXIT_MS, Tooltip } from '@/component-library';
+import { Checkbox, PresenceBoundary, PRESENCE_BOUNDARY_MIN_EXIT_MS } from '@/component-library';
import { useI18n } from '@/infrastructure/i18n';
import { gitAPI, type GitBranch as GitBranchType } from '../../../infrastructure/api/service-api/GitAPI';
import './BranchSelectModal.scss';
diff --git a/src/web-ui/src/app/components/panels/DiffFullscreenViewer.tsx b/src/web-ui/src/app/components/panels/DiffFullscreenViewer.tsx
index e82a132457..0b6d1f732a 100644
--- a/src/web-ui/src/app/components/panels/DiffFullscreenViewer.tsx
+++ b/src/web-ui/src/app/components/panels/DiffFullscreenViewer.tsx
@@ -1,9 +1,9 @@
import React, { useEffect, useCallback, useRef } from 'react';
-import { Button, IconButton } from '@bitfun/ui';
+import { Button, IconButton, Tooltip } from '@bitfun/ui';
import { createPortal } from 'react-dom';
import { getAppearanceOverlayHost } from '@/infrastructure/appearance/runtime/AppearanceOverlayHost';
import { X, CheckCircle, XCircle } from 'lucide-react';
-import { PresenceBoundary, Tooltip } from '@/component-library';
+import { PresenceBoundary } from '@/component-library';
import { useI18n } from '@/infrastructure/i18n';
import { isImeOwnedKeyboardEvent } from '@/shared/utils/ime';
import { DiffEditor } from '../../../tools/editor';
diff --git a/src/web-ui/src/app/components/panels/FilesPanel.tsx b/src/web-ui/src/app/components/panels/FilesPanel.tsx
index 15b24d74b4..810a6f2d2d 100644
--- a/src/web-ui/src/app/components/panels/FilesPanel.tsx
+++ b/src/web-ui/src/app/components/panels/FilesPanel.tsx
@@ -3,7 +3,7 @@
* Displays the file explorer for the current workspace
*/
-import { Button, IconButton, SearchField, StatusPill } from '@bitfun/ui';
+import { Button, IconButton, SearchField, StatusPill, Tooltip } from '@bitfun/ui';
import React, { useState, useCallback, useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { Search as SearchIcon, CaseSensitive, Regex, WholeWord, List, Loader2 } from 'lucide-react';
@@ -14,7 +14,7 @@ import {
type FileExplorerToolbarHandlers,
} from '@/tools/file-system';
import { useExplorerSearch } from '@/tools/file-explorer';
-import { Tooltip } from '@/component-library';
+
import { useI18n } from '@/infrastructure/i18n/hooks/useI18n';
import { confirmWarning } from '@/infrastructure/confirm-dialog';
import { FileSearchResults } from '@/tools/file-system/components/FileSearchResults';
diff --git a/src/web-ui/src/app/components/panels/base/FlexiblePanel.tsx b/src/web-ui/src/app/components/panels/base/FlexiblePanel.tsx
index b8cff0b034..615550f0c1 100644
--- a/src/web-ui/src/app/components/panels/base/FlexiblePanel.tsx
+++ b/src/web-ui/src/app/components/panels/base/FlexiblePanel.tsx
@@ -1,7 +1,7 @@
import React, { useCallback, memo } from 'react';
-import { IconButton } from '@bitfun/ui';
+import { IconButton, Tooltip } from '@bitfun/ui';
import { Download, Copy, X, AlertCircle } from 'lucide-react';
-import { Tooltip } from '@/component-library';
+
import { Markdown as MarkdownRenderer } from '@/component-library/components/Markdown/Markdown';
import { useI18n } from '@/infrastructure/i18n';
import { createLogger } from '@/shared/utils/logger';
diff --git a/src/web-ui/src/app/components/panels/content-canvas/anchor-zone/AnchorZone.tsx b/src/web-ui/src/app/components/panels/content-canvas/anchor-zone/AnchorZone.tsx
index 63d1190bc5..163279839f 100644
--- a/src/web-ui/src/app/components/panels/content-canvas/anchor-zone/AnchorZone.tsx
+++ b/src/web-ui/src/app/components/panels/content-canvas/anchor-zone/AnchorZone.tsx
@@ -6,10 +6,11 @@
import React, { useRef, useState, useCallback, useEffect } from 'react';
import { ChevronDown, ChevronUp, X, Terminal, Maximize2, Minimize2 } from 'lucide-react';
import { useTranslation } from 'react-i18next';
-import { Tooltip } from '@/component-library';
+
import type { AnchorPosition } from '../types';
import { LAYOUT_CONFIG, clampAnchorSize } from '../types';
import './AnchorZone.scss';
+import { Tooltip } from '@bitfun/ui';
export interface AnchorZoneProps {
/** Position */
diff --git a/src/web-ui/src/app/components/panels/content-canvas/editor-area/SplitHandle.tsx b/src/web-ui/src/app/components/panels/content-canvas/editor-area/SplitHandle.tsx
index e67a29ad93..c39a7e49cc 100644
--- a/src/web-ui/src/app/components/panels/content-canvas/editor-area/SplitHandle.tsx
+++ b/src/web-ui/src/app/components/panels/content-canvas/editor-area/SplitHandle.tsx
@@ -5,9 +5,10 @@
import React, { useState, useCallback, useRef, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
-import { Tooltip } from '@/component-library';
+
import { LAYOUT_CONFIG, clampSplitRatio } from '../types';
import './SplitHandle.scss';
+import { Tooltip } from '@bitfun/ui';
export interface SplitHandleProps {
/** Split direction */
diff --git a/src/web-ui/src/app/components/panels/content-canvas/empty-state/EmptyState.tsx b/src/web-ui/src/app/components/panels/content-canvas/empty-state/EmptyState.tsx
index 58767850e4..7afcc1ab6c 100644
--- a/src/web-ui/src/app/components/panels/content-canvas/empty-state/EmptyState.tsx
+++ b/src/web-ui/src/app/components/panels/content-canvas/empty-state/EmptyState.tsx
@@ -6,8 +6,9 @@
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { X } from 'lucide-react';
-import { Tooltip } from '@/component-library';
+
import './EmptyState.scss';
+import { Tooltip } from '@bitfun/ui';
export interface EmptyStateProps {
onClose?: () => void;
diff --git a/src/web-ui/src/app/components/panels/content-canvas/mission-control/ThumbnailCard.tsx b/src/web-ui/src/app/components/panels/content-canvas/mission-control/ThumbnailCard.tsx
index 99d6eae18f..eca155b3b7 100644
--- a/src/web-ui/src/app/components/panels/content-canvas/mission-control/ThumbnailCard.tsx
+++ b/src/web-ui/src/app/components/panels/content-canvas/mission-control/ThumbnailCard.tsx
@@ -6,10 +6,11 @@
import React, { useCallback, useMemo } from 'react';
import { X, Pin, FileCode, FileText, Image, Terminal, GitBranch } from 'lucide-react';
import { useTranslation } from 'react-i18next';
-import { Tooltip } from '@/component-library';
+
import type { CanvasTab, EditorGroupId } from '../types';
import { isFileViewerType } from '../types';
import './ThumbnailCard.scss';
+import { Tooltip } from '@bitfun/ui';
export interface ThumbnailCardProps {
/** Tab data */
diff --git a/src/web-ui/src/app/components/panels/content-canvas/quick-look/QuickLook.tsx b/src/web-ui/src/app/components/panels/content-canvas/quick-look/QuickLook.tsx
index 9619e6a035..a5a51b06ea 100644
--- a/src/web-ui/src/app/components/panels/content-canvas/quick-look/QuickLook.tsx
+++ b/src/web-ui/src/app/components/panels/content-canvas/quick-look/QuickLook.tsx
@@ -18,11 +18,12 @@ import { createPortal } from 'react-dom';
import { getAppearanceOverlayHost } from '@/infrastructure/appearance/runtime/AppearanceOverlayHost';
import { X, Pin, ExternalLink } from 'lucide-react';
import { useTranslation } from 'react-i18next';
-import { Tooltip } from '@/component-library';
+
import { useDismissibleLayer } from '@/infrastructure/hooks/useDismissibleLayer';
import FlexiblePanel from '../../base/FlexiblePanel';
import type { PanelContent } from '../types';
import './QuickLook.scss';
+import { Tooltip } from '@bitfun/ui';
export interface QuickLookProps {
/** Whether visible */
diff --git a/src/web-ui/src/app/components/panels/content-canvas/tab-bar/Tab.tsx b/src/web-ui/src/app/components/panels/content-canvas/tab-bar/Tab.tsx
index 990e18761e..b873fc7541 100644
--- a/src/web-ui/src/app/components/panels/content-canvas/tab-bar/Tab.tsx
+++ b/src/web-ui/src/app/components/panels/content-canvas/tab-bar/Tab.tsx
@@ -6,7 +6,7 @@
import React, { useCallback } from 'react';
import { X, Pin, Split } from 'lucide-react';
import { useTranslation } from 'react-i18next';
-import { Tooltip } from '@/component-library';
+
import { commandExecutor } from '@/shared/context-menu-system/commands/CommandExecutor';
import { useContextMenuStore } from '@/shared/context-menu-system/store/ContextMenuStore';
import { ContextType, type TabContext } from '@/shared/context-menu-system/types/context.types';
@@ -17,6 +17,7 @@ import { hasNonFileUriScheme } from '@/shared/utils/pathUtils';
import { isHtmlFilePath } from '@/shared/utils/htmlFilePreview';
import type { CanvasTab, EditorGroupId, TabState } from '../types';
import './Tab.scss';
+import { Tooltip } from '@bitfun/ui';
export interface TabProps {
/** Tab data */
tab: CanvasTab;
diff --git a/src/web-ui/src/app/components/panels/content-canvas/tab-bar/TabBar.tsx b/src/web-ui/src/app/components/panels/content-canvas/tab-bar/TabBar.tsx
index 359cb5ad89..0dc43000c0 100644
--- a/src/web-ui/src/app/components/panels/content-canvas/tab-bar/TabBar.tsx
+++ b/src/web-ui/src/app/components/panels/content-canvas/tab-bar/TabBar.tsx
@@ -6,12 +6,13 @@
import React, { useState, useRef, useEffect, useCallback, useMemo, useLayoutEffect } from 'react';
import { X } from 'lucide-react';
import { useTranslation } from 'react-i18next';
-import { Tooltip } from '@/component-library';
+
import { Tab } from './Tab';
import { TabOverflowMenu } from './TabOverflowMenu';
import type { CanvasTab, EditorGroupId, TabDragPayload } from '../types';
import { createLogger } from '@/shared/utils/logger';
import './TabBar.scss';
+import { Tooltip } from '@bitfun/ui';
const log = createLogger('TabBar');
const TAB_REORDER_DURATION_MS = 160;
diff --git a/src/web-ui/src/app/components/panels/content-canvas/tab-bar/TabOverflowMenu.tsx b/src/web-ui/src/app/components/panels/content-canvas/tab-bar/TabOverflowMenu.tsx
index b763a2da73..10e0118cc1 100644
--- a/src/web-ui/src/app/components/panels/content-canvas/tab-bar/TabOverflowMenu.tsx
+++ b/src/web-ui/src/app/components/panels/content-canvas/tab-bar/TabOverflowMenu.tsx
@@ -8,11 +8,11 @@
import React, { useState, useRef, useEffect, useCallback } from 'react';
import { createPortal } from 'react-dom';
-import { KeyHint } from '@bitfun/ui';
+import { KeyHint, Tooltip } from '@bitfun/ui';
import { getAppearanceOverlayHost } from '@/infrastructure/appearance/runtime/AppearanceOverlayHost';
import { LayoutGrid, ChevronDown, X } from 'lucide-react';
import { useTranslation } from 'react-i18next';
-import { Tooltip } from '@/component-library';
+
import type { CanvasTab } from '../types';
import './TabOverflowMenu.scss';
export interface TabOverflowMenuProps {
diff --git a/src/web-ui/src/app/components/panels/review-platform/ReviewPlatformPanel.tsx b/src/web-ui/src/app/components/panels/review-platform/ReviewPlatformPanel.tsx
index f6234ea977..e028d04f61 100644
--- a/src/web-ui/src/app/components/panels/review-platform/ReviewPlatformPanel.tsx
+++ b/src/web-ui/src/app/components/panels/review-platform/ReviewPlatformPanel.tsx
@@ -1,4 +1,4 @@
-import { Button, Field, IconButton, Input as DesignInput, Modal, TabGroup } from '@bitfun/ui';
+import { Button, Field, IconButton, Input as DesignInput, Modal, TabGroup, Tooltip } from '@bitfun/ui';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
ChevronDown,
@@ -26,7 +26,7 @@ import {
UserRound,
XCircle,
} from 'lucide-react';
-import { MarkdownRenderer, Select, Tooltip, type SelectOption } from '@/component-library';
+import { MarkdownRenderer, Select, type SelectOption } from '@/component-library';
import { reviewPlatformAPI, systemAPI, type ReviewPlatformAccount, type ReviewPlatformAuthChallenge, type ReviewPlatformCiItem, type ReviewPlatformCiLog, type ReviewPlatformCommit, type ReviewPlatformDetailSection, type ReviewPlatformFile, type ReviewPlatformPagination, type ReviewPlatformPullRequest, type ReviewPlatformPullRequestDetail, type ReviewPlatformPullRequestDetailPage, type ReviewPlatformRemote, type ReviewPlatformRepositoryRef, type ReviewPlatformThread, type ReviewPlatformWorkspaceSnapshot } from '@/infrastructure/api';
import { createLogger } from '@/shared/utils/logger';
import { notificationService } from '@/shared/notification-system';
diff --git a/src/web-ui/src/app/components/scheduled-jobs/LocalizedDateTimeField.tsx b/src/web-ui/src/app/components/scheduled-jobs/LocalizedDateTimeField.tsx
index ff5eeeff91..74156bbba9 100644
--- a/src/web-ui/src/app/components/scheduled-jobs/LocalizedDateTimeField.tsx
+++ b/src/web-ui/src/app/components/scheduled-jobs/LocalizedDateTimeField.tsx
@@ -13,9 +13,9 @@
*/
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
-import { IconButton, Input } from '@bitfun/ui';
+import { IconButton, Input, Tooltip } from '@bitfun/ui';
import { CalendarDays } from 'lucide-react';
-import { Tooltip } from '@/component-library';
+
import { useI18n } from '@/infrastructure/i18n';
import DateTimePickerPopover from './DateTimePickerPopover';
import {
diff --git a/src/web-ui/src/app/components/scheduled-jobs/ScheduledJobsView.tsx b/src/web-ui/src/app/components/scheduled-jobs/ScheduledJobsView.tsx
index b08442fb22..1c1839a040 100644
--- a/src/web-ui/src/app/components/scheduled-jobs/ScheduledJobsView.tsx
+++ b/src/web-ui/src/app/components/scheduled-jobs/ScheduledJobsView.tsx
@@ -5,10 +5,10 @@
* job list at top, inline editor expands below the selected job.
*/
-import { Button, Switch, IconButton, Input, Select } from '@bitfun/ui';
+import { Button, Switch, IconButton, Input, Select, Tooltip } from '@bitfun/ui';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { RefreshCw, Trash2 } from 'lucide-react';
-import { Select as LegacySelect, Textarea, Tooltip } from '@/component-library';
+import { Select as LegacySelect, Textarea } from '@/component-library';
import { confirmDanger } from '@/infrastructure/confirm-dialog';
import {
cronAPI,
diff --git a/src/web-ui/src/app/layout/FloatingMiniChat.tsx b/src/web-ui/src/app/layout/FloatingMiniChat.tsx
index e1e06d2260..ad8a78f201 100644
--- a/src/web-ui/src/app/layout/FloatingMiniChat.tsx
+++ b/src/web-ui/src/app/layout/FloatingMiniChat.tsx
@@ -19,8 +19,8 @@ import type {
ChatInputRegistration,
ChatInputSubmission,
} from '@/flow_chat/components/chatInputRegistration';
-import { IconButton } from '@bitfun/ui';
-import { Tooltip } from '@/component-library';
+import { IconButton, Tooltip } from '@bitfun/ui';
+
import { useCurrentWorkspace } from '@/infrastructure/contexts/WorkspaceContext';
import { SessionMenu, useFlowChatSessions } from '../../flow_chat/components/session-menu';
import { useSceneStore } from '@/app/stores/sceneStore';
diff --git a/src/web-ui/src/app/scenes/agents/AgentsScene.tsx b/src/web-ui/src/app/scenes/agents/AgentsScene.tsx
index cb788db860..fa3636c939 100644
--- a/src/web-ui/src/app/scenes/agents/AgentsScene.tsx
+++ b/src/web-ui/src/app/scenes/agents/AgentsScene.tsx
@@ -1,4 +1,4 @@
-import { Button, IconButton, SearchField, Select, StatusPill } from '@bitfun/ui';
+import { Button, IconButton, SearchField, Select, StatusPill, Tooltip } from '@bitfun/ui';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import type { TFunction } from 'i18next';
import {
@@ -16,7 +16,7 @@ import {
Wrench,
} from 'lucide-react';
import { useTranslation } from 'react-i18next';
-import { Select as LegacySelect, Tooltip } from '@/component-library';
+import { Select as LegacySelect } from '@/component-library';
import { useI18n } from '@/infrastructure/i18n/hooks/useI18n';
import { confirmDanger } from '@/infrastructure/confirm-dialog';
import { HarnessCreativeIcon } from '@/component-library/icons';
diff --git a/src/web-ui/src/app/scenes/agents/components/AgentCapabilityTooltip.scss b/src/web-ui/src/app/scenes/agents/components/AgentCapabilityTooltip.scss
index cf1b98e56c..fc7daad616 100644
--- a/src/web-ui/src/app/scenes/agents/components/AgentCapabilityTooltip.scss
+++ b/src/web-ui/src/app/scenes/agents/components/AgentCapabilityTooltip.scss
@@ -1,9 +1,10 @@
@use '../../../../component-library/styles/tokens' as tokens;
.agent-capability-tooltip {
- .bitfun-tooltip__content {
+ [data-bf-part='content'] {
width: min(360px, calc(100vw - 16px));
max-width: min(360px, calc(100vw - 16px));
+ max-inline-size: none;
padding: tokens.$size-gap-2 tokens.$size-gap-3;
}
diff --git a/src/web-ui/src/app/scenes/agents/components/AgentCapabilityTooltip.tsx b/src/web-ui/src/app/scenes/agents/components/AgentCapabilityTooltip.tsx
index e6f7152570..3600d5e779 100644
--- a/src/web-ui/src/app/scenes/agents/components/AgentCapabilityTooltip.tsx
+++ b/src/web-ui/src/app/scenes/agents/components/AgentCapabilityTooltip.tsx
@@ -1,7 +1,8 @@
import React from 'react';
-import { Tooltip } from '@/component-library';
+
import type { AgentCapabilityTooltipField } from './agentCapabilityTooltipUtils';
import './AgentCapabilityTooltip.scss';
+import { Tooltip } from '@bitfun/ui';
export type { AgentCapabilityTooltipField } from './agentCapabilityTooltipUtils';
diff --git a/src/web-ui/src/app/scenes/agents/components/CreateAgentPage.scss b/src/web-ui/src/app/scenes/agents/components/CreateAgentPage.scss
index 282b8f982b..d2bc78018b 100644
--- a/src/web-ui/src/app/scenes/agents/components/CreateAgentPage.scss
+++ b/src/web-ui/src/app/scenes/agents/components/CreateAgentPage.scss
@@ -515,9 +515,10 @@
.th-create-panel__context-tooltip {
min-width: 0;
- .bitfun-tooltip__content {
+ [data-bf-part='content'] {
width: max-content;
max-width: min(520px, calc(100vw - 32px));
+ max-inline-size: none;
white-space: pre-wrap;
}
}
diff --git a/src/web-ui/src/app/scenes/agents/components/CreateAgentPage.tsx b/src/web-ui/src/app/scenes/agents/components/CreateAgentPage.tsx
index 87ca669d5f..992ae8fba3 100644
--- a/src/web-ui/src/app/scenes/agents/components/CreateAgentPage.tsx
+++ b/src/web-ui/src/app/scenes/agents/components/CreateAgentPage.tsx
@@ -1,4 +1,4 @@
-import { Button, Switch, Input } from '@bitfun/ui';
+import { Button, Switch, Input, Tooltip } from '@bitfun/ui';
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import {
ArrowLeft,
@@ -10,7 +10,7 @@ import {
type LucideIcon,
} from 'lucide-react';
import { useTranslation } from 'react-i18next';
-import { Textarea, Tooltip } from '@/component-library';
+import { Textarea } from '@/component-library';
import {
CustomAgentAPI,
type CustomAgentKind,
diff --git a/src/web-ui/src/app/scenes/agents/components/SkillGroupPicker.tsx b/src/web-ui/src/app/scenes/agents/components/SkillGroupPicker.tsx
index b05318cc8a..270892f4b0 100644
--- a/src/web-ui/src/app/scenes/agents/components/SkillGroupPicker.tsx
+++ b/src/web-ui/src/app/scenes/agents/components/SkillGroupPicker.tsx
@@ -1,4 +1,4 @@
-import { Button, Switch, IconButton, Input, Modal } from '@bitfun/ui';
+import { Button, Switch, IconButton, Input, Modal, Tooltip } from '@bitfun/ui';
import React, { useMemo, useState } from 'react';
import type { TFunction } from 'i18next';
import {
@@ -10,7 +10,7 @@ import {
Trash2,
} from 'lucide-react';
import { useTranslation } from 'react-i18next';
-import { Tooltip } from '@/component-library';
+
import { confirmDanger } from '@/infrastructure/confirm-dialog';
import type { UserSkillGroup } from '@/infrastructure/config/types';
import { useNotification } from '@/shared/notification-system';
diff --git a/src/web-ui/src/app/scenes/agents/components/ToolGroupPicker.tsx b/src/web-ui/src/app/scenes/agents/components/ToolGroupPicker.tsx
index f032bfcd3b..8c6f11be2c 100644
--- a/src/web-ui/src/app/scenes/agents/components/ToolGroupPicker.tsx
+++ b/src/web-ui/src/app/scenes/agents/components/ToolGroupPicker.tsx
@@ -1,4 +1,4 @@
-import { Button, Switch, IconButton, Input, Modal } from '@bitfun/ui';
+import { Button, Switch, IconButton, Input, Modal, Tooltip } from '@bitfun/ui';
import React, { useMemo, useState } from 'react';
import type { TFunction } from 'i18next';
import {
@@ -10,7 +10,7 @@ import {
Trash2,
} from 'lucide-react';
import { useTranslation } from 'react-i18next';
-import { Tooltip } from '@/component-library';
+
import { confirmDanger } from '@/infrastructure/confirm-dialog';
import type { UserToolGroup } from '@/infrastructure/config/types';
import { useNotification } from '@/shared/notification-system';
diff --git a/src/web-ui/src/app/scenes/file-viewer/FileViewerNav.tsx b/src/web-ui/src/app/scenes/file-viewer/FileViewerNav.tsx
index e16661b425..17277c2e67 100644
--- a/src/web-ui/src/app/scenes/file-viewer/FileViewerNav.tsx
+++ b/src/web-ui/src/app/scenes/file-viewer/FileViewerNav.tsx
@@ -7,12 +7,12 @@
*/
import React, { useState, useCallback } from 'react';
-import { IconButton } from '@bitfun/ui';
+import { IconButton, Tooltip } from '@bitfun/ui';
import { Folder, Search as SearchIcon, List, FilePlus, FolderPlus, RefreshCw } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { useCurrentWorkspace } from '../../../infrastructure/contexts/WorkspaceContext';
import { useI18n } from '@/infrastructure/i18n';
-import { Tooltip } from '@/component-library';
+
import type { FileExplorerToolbarHandlers } from '@/tools/file-system';
import FilesPanel from '../../components/panels/FilesPanel';
import './FileViewerNav.scss';
diff --git a/src/web-ui/src/app/scenes/git/GitNav.tsx b/src/web-ui/src/app/scenes/git/GitNav.tsx
index b0e45997ca..e7396bd6a6 100644
--- a/src/web-ui/src/app/scenes/git/GitNav.tsx
+++ b/src/web-ui/src/app/scenes/git/GitNav.tsx
@@ -5,13 +5,13 @@
*/
import React, { useCallback } from 'react';
-import { IconButton } from '@bitfun/ui';
+import { IconButton, Tooltip } from '@bitfun/ui';
import { useTranslation } from 'react-i18next';
import { GitBranch, Layers2, ArrowUp, ArrowDown, RefreshCw } from 'lucide-react';
import { useGitSceneStore, type GitSceneView } from './gitSceneStore';
import { useGitState } from '../../../tools/git/hooks';
import { useCurrentWorkspace } from '@/infrastructure/contexts/WorkspaceContext';
-import { Tooltip } from '@/component-library';
+
import './GitNav.scss';
const NAV_ITEMS: { id: GitSceneView; icon: React.ElementType; labelKey: string }[] = [
diff --git a/src/web-ui/src/app/scenes/git/GitScene.tsx b/src/web-ui/src/app/scenes/git/GitScene.tsx
index 62981d2625..a956c8b724 100644
--- a/src/web-ui/src/app/scenes/git/GitScene.tsx
+++ b/src/web-ui/src/app/scenes/git/GitScene.tsx
@@ -4,14 +4,14 @@
*/
import React, { useState, useRef, useEffect, useCallback } from 'react';
-import { Button, IconButton } from '@bitfun/ui';
+import { Button, IconButton, Tooltip } from '@bitfun/ui';
import { useTranslation } from 'react-i18next';
import { GitBranch, Plus, RefreshCw, ShieldAlert } from 'lucide-react';
import { useGitSceneStore } from './gitSceneStore';
import { WorkingCopyView, BranchesView, GraphView } from './views';
import { useGitState } from '@/tools/git/hooks';
import { useCurrentWorkspace } from '@/infrastructure/contexts/WorkspaceContext';
-import { CubeLoading, Tooltip } from '@/component-library';
+import { CubeLoading } from '@/component-library';
import { globalEventBus } from '@/infrastructure/event-bus';
import { requestGitRepositoryTrust } from '@/shared/services/gitTrustService';
import './GitScene.scss';
diff --git a/src/web-ui/src/app/scenes/git/views/BranchesView.tsx b/src/web-ui/src/app/scenes/git/views/BranchesView.tsx
index 1a61785d6d..39272f9ef1 100644
--- a/src/web-ui/src/app/scenes/git/views/BranchesView.tsx
+++ b/src/web-ui/src/app/scenes/git/views/BranchesView.tsx
@@ -2,7 +2,7 @@
* BranchesView — Left: branch list (switch/create/delete). Right: commit history for selected branch.
*/
-import { Button, IconButton, SearchField } from '@bitfun/ui';
+import { Button, IconButton, SearchField, Tooltip } from '@bitfun/ui';
import React, { useCallback, useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import {
@@ -17,7 +17,7 @@ import {
ChevronRight,
Search as SearchIcon,
} from 'lucide-react';
-import { Tooltip } from '@/component-library';
+
import { useI18n } from '@/infrastructure/i18n/hooks/useI18n';
import { gitService } from '@/tools/git/services';
import { useGitOperations } from '@/tools/git/hooks';
diff --git a/src/web-ui/src/app/scenes/git/views/WorkingCopyView.tsx b/src/web-ui/src/app/scenes/git/views/WorkingCopyView.tsx
index 8b1cc1ab59..62698105fb 100644
--- a/src/web-ui/src/app/scenes/git/views/WorkingCopyView.tsx
+++ b/src/web-ui/src/app/scenes/git/views/WorkingCopyView.tsx
@@ -2,7 +2,7 @@
* WorkingCopyView — Git working copy: commit bar + file list + diff area (ContentCanvas mode=git).
*/
-import { Button, IconButton, SearchField } from '@bitfun/ui';
+import { Button, IconButton, SearchField, Tooltip } from '@bitfun/ui';
import React, { useCallback, useState, useMemo, useEffect, useRef } from 'react';
import { useShortcut } from '@/infrastructure/hooks/useShortcut';
import { useTranslation } from 'react-i18next';
@@ -21,7 +21,7 @@ import {
FileCode2,
Search as SearchIcon,
} from 'lucide-react';
-import { Tooltip, Textarea } from '@/component-library';
+import { Textarea } from '@/component-library';
import { useI18n } from '@/infrastructure/i18n/hooks/useI18n';
import { ContentCanvas } from '@/app/components/panels/content-canvas';
import { CanvasStoreModeContext } from '@/app/components/panels/content-canvas/stores';
diff --git a/src/web-ui/src/app/scenes/miniapps/MiniAppScene.tsx b/src/web-ui/src/app/scenes/miniapps/MiniAppScene.tsx
index 76e7a5808d..e9d3d5cb97 100644
--- a/src/web-ui/src/app/scenes/miniapps/MiniAppScene.tsx
+++ b/src/web-ui/src/app/scenes/miniapps/MiniAppScene.tsx
@@ -2,7 +2,7 @@
* MiniAppScene — standalone scene tab for a single MiniApp.
* Mounts MiniAppRunner; close via SceneBar × (does not stop worker).
*/
-import { Button, IconButton } from '@bitfun/ui';
+import { Button, IconButton, Tooltip } from '@bitfun/ui';
import React, { useCallback, useEffect, useState } from 'react';
import { RefreshCw, Loader2, AlertTriangle, CheckCircle2, X } from 'lucide-react';
import { miniAppAPI } from '@/infrastructure/api/service-api/MiniAppAPI';
@@ -11,7 +11,7 @@ import type { MiniApp, MiniAppDraft } from '@/infrastructure/api/service-api/Min
import { useAppearance } from '@/infrastructure/appearance';
import { useCurrentWorkspace } from '@/infrastructure/contexts/WorkspaceContext';
import { createLogger } from '@/shared/utils/logger';
-import { Tooltip } from '@/component-library';
+
import { useSceneManager } from '@/app/hooks/useSceneManager';
import type { SceneTabId } from '@/app/components/SceneBar/types';
import { useMiniAppStore } from './miniAppStore';
diff --git a/src/web-ui/src/app/scenes/miniapps/customization/MiniAppCustomizeEntry.tsx b/src/web-ui/src/app/scenes/miniapps/customization/MiniAppCustomizeEntry.tsx
index ca116a848a..bfab0820bf 100644
--- a/src/web-ui/src/app/scenes/miniapps/customization/MiniAppCustomizeEntry.tsx
+++ b/src/web-ui/src/app/scenes/miniapps/customization/MiniAppCustomizeEntry.tsx
@@ -1,7 +1,7 @@
import React from 'react';
-import { IconButton } from '@bitfun/ui';
+import { IconButton, Tooltip } from '@bitfun/ui';
import { WandSparkles } from 'lucide-react';
-import { Tooltip } from '@/component-library';
+
import { useI18n } from '@/infrastructure/i18n';
interface MiniAppCustomizeEntryProps {
diff --git a/src/web-ui/src/app/scenes/miniapps/customization/MiniAppCustomizePanel.tsx b/src/web-ui/src/app/scenes/miniapps/customization/MiniAppCustomizePanel.tsx
index 6cf6670de4..9d77525053 100644
--- a/src/web-ui/src/app/scenes/miniapps/customization/MiniAppCustomizePanel.tsx
+++ b/src/web-ui/src/app/scenes/miniapps/customization/MiniAppCustomizePanel.tsx
@@ -1,7 +1,7 @@
-import { Button, IconButton } from '@bitfun/ui';
+import { Button, IconButton, Tooltip } from '@bitfun/ui';
import React, { useCallback, useEffect, useMemo, useState, useSyncExternalStore } from 'react';
import { AlertTriangle, Check, Eye, EyeOff, Loader2, RefreshCw, Send, Trash2, X } from 'lucide-react';
-import { Tooltip } from '@/component-library';
+
import { flowChatStore } from '@/flow_chat/store/FlowChatStore';
import type { MiniApp, MiniAppCustomizationMetadata, MiniAppDraft } from '@/infrastructure/api/service-api/MiniAppAPI';
import { miniAppAPI } from '@/infrastructure/api/service-api/MiniAppAPI';
diff --git a/src/web-ui/src/app/scenes/profile/views/AssistantCard.tsx b/src/web-ui/src/app/scenes/profile/views/AssistantCard.tsx
index c8fb8255d4..0d1a7a0a58 100644
--- a/src/web-ui/src/app/scenes/profile/views/AssistantCard.tsx
+++ b/src/web-ui/src/app/scenes/profile/views/AssistantCard.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { Button, IconButton, StatusPill } from '@bitfun/ui';
+import { Button, IconButton, StatusPill, Tooltip } from '@bitfun/ui';
import {
ChevronRight,
MessageSquarePlus,
@@ -9,7 +9,7 @@ import {
Trash2,
} from 'lucide-react';
import { useTranslation } from 'react-i18next';
-import { Tooltip } from '@/component-library';
+
import { AssistantAvatar } from '@/app/components/AssistantAvatar';
import type { WorkspaceInfo } from '@/shared/types';
diff --git a/src/web-ui/src/app/scenes/profile/views/AssistantConfigPage.tsx b/src/web-ui/src/app/scenes/profile/views/AssistantConfigPage.tsx
index d4a57b4c7c..106b1ec0af 100644
--- a/src/web-ui/src/app/scenes/profile/views/AssistantConfigPage.tsx
+++ b/src/web-ui/src/app/scenes/profile/views/AssistantConfigPage.tsx
@@ -10,7 +10,7 @@ import {
RefreshCw,
X,
} from 'lucide-react';
-import { Tooltip } from '@/component-library';
+
import { workspaceAPI } from '@/infrastructure/api/service-api/WorkspaceAPI';
import { notificationService } from '@/shared/notification-system';
import { createLogger } from '@/shared/utils/logger';
@@ -29,7 +29,7 @@ import AssistantAvatarPicker from './AssistantAvatarPicker';
import AssistantQuickInput from './AssistantQuickInput';
import { useNurseryStore } from '../nurseryStore';
import './NurseryView.scss';
-import { IconButton, Input } from '@bitfun/ui';
+import { IconButton, Input, Tooltip } from '@bitfun/ui';
const log = createLogger('AssistantConfigPage');
diff --git a/src/web-ui/src/app/scenes/profile/views/AssistantDefaultsPage.tsx b/src/web-ui/src/app/scenes/profile/views/AssistantDefaultsPage.tsx
index 70ba7c9e3b..54a9d86b2b 100644
--- a/src/web-ui/src/app/scenes/profile/views/AssistantDefaultsPage.tsx
+++ b/src/web-ui/src/app/scenes/profile/views/AssistantDefaultsPage.tsx
@@ -1,4 +1,4 @@
-import { Button, IconButton, Switch } from '@bitfun/ui';
+import { Button, IconButton, Switch, Tooltip } from '@bitfun/ui';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
ArrowLeft,
@@ -15,7 +15,7 @@ import {
Wrench,
X,
} from 'lucide-react';
-import { Tooltip } from '@/component-library';
+
import { configAPI } from '@/infrastructure/api/service-api/ConfigAPI';
import { MCPAPI, type MCPServerInfo } from '@/infrastructure/api/service-api/MCPAPI';
import { toolAPI } from '@/infrastructure/api/service-api/ToolAPI';
diff --git a/src/web-ui/src/app/scenes/settings/components/KeyboardShortcutsTab.tsx b/src/web-ui/src/app/scenes/settings/components/KeyboardShortcutsTab.tsx
index 8d48a1dede..7c5d2ac70f 100644
--- a/src/web-ui/src/app/scenes/settings/components/KeyboardShortcutsTab.tsx
+++ b/src/web-ui/src/app/scenes/settings/components/KeyboardShortcutsTab.tsx
@@ -10,10 +10,10 @@
* - Reset button restores all defaults
*/
-import { Button, SearchField } from '@bitfun/ui';
+import { Button, SearchField, Tooltip } from '@bitfun/ui';
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react';
import { Search as SearchIcon } from 'lucide-react';
-import { Tooltip } from '@/component-library';
+
import { useI18n } from '@/infrastructure/i18n';
import { ConfigPageLayout, ConfigPageHeader, ConfigPageContent, ConfigPageSection } from '@/infrastructure/config/components/common';
import {
diff --git a/src/web-ui/src/app/scenes/shell/ShellNav.tsx b/src/web-ui/src/app/scenes/shell/ShellNav.tsx
index dca82d6927..cd605b392f 100644
--- a/src/web-ui/src/app/scenes/shell/ShellNav.tsx
+++ b/src/web-ui/src/app/scenes/shell/ShellNav.tsx
@@ -24,8 +24,7 @@ import type { ShellInfo } from '@/tools/terminal/types/session';
import { useShellEntries } from './hooks';
import type { ShellEntry } from './hooks/shellEntryTypes';
import { useShellNavMenuState } from './hooks/useShellNavMenuState';
-import { Button } from '@bitfun/ui';
-import { Tooltip } from '@/component-library/components/Tooltip';
+import { Button, Tooltip } from '@bitfun/ui';
import ShellNavEntryItem from './components/ShellNavEntryItem';
import ShellNavWorkspaceSwitcher from './components/ShellNavWorkspaceSwitcher';
import { getAppearanceOverlayHost } from '@/infrastructure/appearance/runtime/AppearanceOverlayHost';
diff --git a/src/web-ui/src/app/scenes/shell/components/ShellNavEntryItem.tsx b/src/web-ui/src/app/scenes/shell/components/ShellNavEntryItem.tsx
index 32bd902681..298a40a7ae 100644
--- a/src/web-ui/src/app/scenes/shell/components/ShellNavEntryItem.tsx
+++ b/src/web-ui/src/app/scenes/shell/components/ShellNavEntryItem.tsx
@@ -1,6 +1,6 @@
import React from 'react';
import { Bookmark, SquareTerminal } from 'lucide-react';
-import { Tooltip } from '@/component-library/components/Tooltip';
+import { Tooltip } from '@bitfun/ui';
import type { MenuItem } from '@/shared/context-menu-system/types/menu.types';
import type { ShellEntry } from '../hooks/shellEntryTypes';
diff --git a/src/web-ui/src/app/scenes/shell/components/ShellNavWorkspaceSwitcher.tsx b/src/web-ui/src/app/scenes/shell/components/ShellNavWorkspaceSwitcher.tsx
index 3fe8a72a93..a4bf4cd7ed 100644
--- a/src/web-ui/src/app/scenes/shell/components/ShellNavWorkspaceSwitcher.tsx
+++ b/src/web-ui/src/app/scenes/shell/components/ShellNavWorkspaceSwitcher.tsx
@@ -2,8 +2,7 @@ import React from 'react';
import { createPortal } from 'react-dom';
import { getAppearanceOverlayHost } from '@/infrastructure/appearance/runtime/AppearanceOverlayHost';
import { Check, ChevronDown } from 'lucide-react';
-import { Menu, MenuItem } from '@bitfun/ui';
-import { Tooltip } from '@/component-library/components/Tooltip';
+import { Menu, MenuItem, Tooltip } from '@bitfun/ui';
import { WorkspaceKind, type WorkspaceInfo } from '@/shared/types';
interface ShellNavWorkspaceSwitcherProps {
diff --git a/src/web-ui/src/app/scenes/todos/TodosScene.tsx b/src/web-ui/src/app/scenes/todos/TodosScene.tsx
index e07baffa94..d81de0ab6d 100644
--- a/src/web-ui/src/app/scenes/todos/TodosScene.tsx
+++ b/src/web-ui/src/app/scenes/todos/TodosScene.tsx
@@ -11,7 +11,7 @@
* broadcast on the shared change event so those views stay in step.
*/
-import { Button, IconButton, Modal } from '@bitfun/ui';
+import { Button, IconButton, Modal, Tooltip } from '@bitfun/ui';
import React, {
useCallback,
useEffect,
@@ -27,7 +27,7 @@ import {
ChevronRight,
Plus,
} from 'lucide-react';
-import { PresenceBoundary, Tooltip } from '@/component-library';
+import { PresenceBoundary } from '@/component-library';
import { confirmDanger } from '@/infrastructure/confirm-dialog';
import { cronAPI, type CronJob, type CreateCronJobRequest, type UpdateCronJobRequest } from '@/infrastructure/api';
import { useI18n } from '@/infrastructure/i18n';
diff --git a/src/web-ui/src/app/scenes/todos/components/TodoItemRow.tsx b/src/web-ui/src/app/scenes/todos/components/TodoItemRow.tsx
index ebdcb447ca..45407cfc29 100644
--- a/src/web-ui/src/app/scenes/todos/components/TodoItemRow.tsx
+++ b/src/web-ui/src/app/scenes/todos/components/TodoItemRow.tsx
@@ -5,10 +5,10 @@
* several rows with different times.
*/
-import { IconButton, Switch } from '@bitfun/ui';
+import { IconButton, Switch, Tooltip } from '@bitfun/ui';
import React from 'react';
import { CalendarClock, Pencil, Trash2 } from 'lucide-react';
-import { Tooltip } from '@/component-library';
+
import { useI18n } from '@/infrastructure/i18n';
import type { CronJob } from '@/infrastructure/api';
import type { WorkspaceInfo } from '@/shared/types';
diff --git a/src/web-ui/src/features/dispatch/DispatchTargetPicker.tsx b/src/web-ui/src/features/dispatch/DispatchTargetPicker.tsx
index 443f107822..eb3e14f5c1 100644
--- a/src/web-ui/src/features/dispatch/DispatchTargetPicker.tsx
+++ b/src/web-ui/src/features/dispatch/DispatchTargetPicker.tsx
@@ -17,7 +17,7 @@ import {
RefreshCw,
Server,
} from 'lucide-react';
-import { Tooltip } from '@/component-library';
+
import { SSHConnectionDialog } from '@/features/ssh-remote/SSHConnectionDialog';
import { useAccountLoginState } from '@/infrastructure/account/useAccountLoginState';
import { getAppearanceOverlayHost } from '@/infrastructure/appearance/runtime/AppearanceOverlayHost';
@@ -31,6 +31,7 @@ import type {
} from './types';
import { useDispatchTargets } from './useDispatchTargets';
import './DispatchTargetPicker.scss';
+import { Tooltip } from '@bitfun/ui';
interface DispatchTargetPickerProps {
target: DispatchTarget;
diff --git a/src/web-ui/src/features/relay-deploy/RelayDeployWizard.tsx b/src/web-ui/src/features/relay-deploy/RelayDeployWizard.tsx
index 0fa5220c05..c63592917e 100644
--- a/src/web-ui/src/features/relay-deploy/RelayDeployWizard.tsx
+++ b/src/web-ui/src/features/relay-deploy/RelayDeployWizard.tsx
@@ -10,10 +10,10 @@
* Closing the wizard cancels any in-progress remote task.
*/
-import { Button, Field, IconButton, Input as DesignInput, Modal, Select } from '@bitfun/ui';
+import { Button, Field, IconButton, Input as DesignInput, Modal, Select, Tooltip } from '@bitfun/ui';
import React, { useState, useEffect, useCallback, useRef } from 'react';
import { useI18n } from '@/infrastructure/i18n';
-import { Alert, Tooltip } from '@/component-library';
+import { Alert } from '@/component-library';
import {
Server, User, Lock, Key, FolderOpen, Loader2, Play, ArrowDownToLine,
CheckCircle2, XCircle, AlertTriangle, RefreshCw, Eye, EyeOff, Search,
diff --git a/src/web-ui/src/features/ssh-remote/PortForwardDialog.test.tsx b/src/web-ui/src/features/ssh-remote/PortForwardDialog.test.tsx
index 0a90741ad3..30e73c4cc9 100644
--- a/src/web-ui/src/features/ssh-remote/PortForwardDialog.test.tsx
+++ b/src/web-ui/src/features/ssh-remote/PortForwardDialog.test.tsx
@@ -95,10 +95,10 @@ vi.mock('@bitfun/ui', () => ({
data-testid={testId}
/>
)),
+ Tooltip: ({ children }: React.PropsWithChildren) => <>{children}>,
}));
vi.mock('@/component-library', () => ({
- Tooltip: ({ children }: React.PropsWithChildren) => <>{children}>,
Checkbox: ({
checked,
onChange,
diff --git a/src/web-ui/src/features/ssh-remote/PortForwardDialog.tsx b/src/web-ui/src/features/ssh-remote/PortForwardDialog.tsx
index 776e6dbf29..5125b8a6b5 100644
--- a/src/web-ui/src/features/ssh-remote/PortForwardDialog.tsx
+++ b/src/web-ui/src/features/ssh-remote/PortForwardDialog.tsx
@@ -15,9 +15,9 @@
* the local port is an allocation that moves when the number is taken.
*/
-import { Button, IconButton, Input, Modal } from '@bitfun/ui';
+import { Button, IconButton, Input, Modal, Tooltip } from '@bitfun/ui';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
-import { Checkbox, Tooltip } from '@/component-library';
+import { Checkbox } from '@/component-library';
import { useI18n } from '@/infrastructure/i18n';
import { systemAPI } from '@/infrastructure/api/service-api/SystemAPI';
import { createLogger } from '@/shared/utils/logger';
diff --git a/src/web-ui/src/features/ssh-remote/SSHAuthPromptDialog.tsx b/src/web-ui/src/features/ssh-remote/SSHAuthPromptDialog.tsx
index f18709239c..40cdbca657 100644
--- a/src/web-ui/src/features/ssh-remote/SSHAuthPromptDialog.tsx
+++ b/src/web-ui/src/features/ssh-remote/SSHAuthPromptDialog.tsx
@@ -2,10 +2,10 @@
* Unified SSH authentication prompt.
*/
-import { Button, Field, IconButton, Input, Modal, Select } from '@bitfun/ui';
+import { Button, Field, IconButton, Input, Modal, Select, Tooltip } from '@bitfun/ui';
import React, { useState, useEffect, useRef, useCallback } from 'react';
import { useI18n } from '@/infrastructure/i18n';
-import { Tooltip } from '@/component-library';
+
import { FolderOpen, Key, Loader2, Lock, Server, User } from 'lucide-react';
import type { SSHAuthMethod } from './types';
import {
diff --git a/src/web-ui/src/features/ssh-remote/SSHConnectionDialog.test.tsx b/src/web-ui/src/features/ssh-remote/SSHConnectionDialog.test.tsx
index 32bd9aec88..4a7e33d0a6 100644
--- a/src/web-ui/src/features/ssh-remote/SSHConnectionDialog.test.tsx
+++ b/src/web-ui/src/features/ssh-remote/SSHConnectionDialog.test.tsx
@@ -94,10 +94,24 @@ vi.mock('@bitfun/ui', () => ({
))}
),
+ Tooltip: ({ children }: React.PropsWithChildren) => <>{children}>,
+ // The real Field associates its label with the child control; the mock
+ // mirrors that accessible name via aria-label so queries stay realistic.
+ Field: ({
+ label,
+ error,
+ children,
+ }: { label?: React.ReactNode; error?: React.ReactNode; children: React.ReactElement }) => (
+
+ {React.cloneElement(children as React.ReactElement>, {
+ 'aria-label': typeof label === 'string' ? label : undefined,
+ })}
+ {error}
+
+ ),
}));
vi.mock('@/component-library', () => ({
- Tooltip: ({ children }: React.PropsWithChildren) => <>{children}>,
Input: ({
label,
value,
diff --git a/src/web-ui/src/features/ssh-remote/SSHConnectionDialog.tsx b/src/web-ui/src/features/ssh-remote/SSHConnectionDialog.tsx
index 6ca21345bb..41512bac12 100644
--- a/src/web-ui/src/features/ssh-remote/SSHConnectionDialog.tsx
+++ b/src/web-ui/src/features/ssh-remote/SSHConnectionDialog.tsx
@@ -3,12 +3,12 @@
* Professional SSH connection dialog following BitFun design patterns
*/
-import { Button, Field, IconButton, Input as DesignInput, Modal, Select } from '@bitfun/ui';
+import { Button, Field, IconButton, Input as DesignInput, Modal, Select, Tooltip } from '@bitfun/ui';
import React, { useState, useEffect, useCallback, useRef } from 'react';
import { useI18n } from '@/infrastructure/i18n';
import { useSSHRemoteContext } from './SSHRemoteContext';
import { SSHAuthPromptDialog, type SSHAuthPromptSubmitPayload } from './SSHAuthPromptDialog';
-import { Alert, Tooltip } from '@/component-library';
+import { Alert } from '@/component-library';
import {
ArrowDownToLine,
CheckCircle2,
diff --git a/src/web-ui/src/infrastructure/config/components/AcpAgentsConfig.tsx b/src/web-ui/src/infrastructure/config/components/AcpAgentsConfig.tsx
index 351628004b..d2b48244aa 100644
--- a/src/web-ui/src/infrastructure/config/components/AcpAgentsConfig.tsx
+++ b/src/web-ui/src/infrastructure/config/components/AcpAgentsConfig.tsx
@@ -1,4 +1,4 @@
-import { Button, IconButton, Input, Select } from '@bitfun/ui';
+import { Button, IconButton, Input, Select, Tooltip } from '@bitfun/ui';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
@@ -17,7 +17,7 @@ import {
Server,
Terminal,
} from 'lucide-react';
-import { Textarea, Tooltip } from '@/component-library';
+import { Textarea } from '@/component-library';
import {
ConfigPageContent,
ConfigPageHeader,
diff --git a/src/web-ui/src/infrastructure/config/components/AppearanceMarketDialog.test.tsx b/src/web-ui/src/infrastructure/config/components/AppearanceMarketDialog.test.tsx
index 866201b952..5ae9878ce6 100644
--- a/src/web-ui/src/infrastructure/config/components/AppearanceMarketDialog.test.tsx
+++ b/src/web-ui/src/infrastructure/config/components/AppearanceMarketDialog.test.tsx
@@ -62,6 +62,10 @@ vi.mock('@bitfun/ui', () => ({
),
Field: ({ label, children }: any) => ,
+ Input: ({ leading, trailing, ...props }: any) => (
+ {leading}{trailing}
+ ),
+ Tooltip: ({ children }: any) => <>{children}>,
}));
vi.mock('@/component-library', () => ({
diff --git a/src/web-ui/src/infrastructure/config/components/AppearancePackageConfigSection.tsx b/src/web-ui/src/infrastructure/config/components/AppearancePackageConfigSection.tsx
index fd2d4e3726..2d58b285fb 100644
--- a/src/web-ui/src/infrastructure/config/components/AppearancePackageConfigSection.tsx
+++ b/src/web-ui/src/infrastructure/config/components/AppearancePackageConfigSection.tsx
@@ -1,8 +1,8 @@
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { AlertTriangle, Check, Download, Image, Trash2, X } from 'lucide-react';
import { useTranslation } from 'react-i18next';
-import { Button, IconButton } from '@bitfun/ui';
-import { Tooltip } from '@/component-library';
+import { Button, IconButton, Tooltip } from '@bitfun/ui';
+
import { confirmDialog } from '@/infrastructure/confirm-dialog';
import {
SYSTEM_APPEARANCE_ID,
diff --git a/src/web-ui/src/infrastructure/config/components/AppearanceSettingsPage.scss b/src/web-ui/src/infrastructure/config/components/AppearanceSettingsPage.scss
index 00e3ff582b..c1502d7297 100644
--- a/src/web-ui/src/infrastructure/config/components/AppearanceSettingsPage.scss
+++ b/src/web-ui/src/infrastructure/config/components/AppearanceSettingsPage.scss
@@ -1479,7 +1479,7 @@
.appearance-preview-tooltip {
- .bitfun-tooltip__content {
+ [data-bf-part='content'] {
padding: 0 !important;
background: transparent !important;
border: none !important;
diff --git a/src/web-ui/src/infrastructure/config/components/ApplicationSettingsPages.tsx b/src/web-ui/src/infrastructure/config/components/ApplicationSettingsPages.tsx
index 7d43b6fcdf..bbbb4b2218 100644
--- a/src/web-ui/src/infrastructure/config/components/ApplicationSettingsPages.tsx
+++ b/src/web-ui/src/infrastructure/config/components/ApplicationSettingsPages.tsx
@@ -1,11 +1,10 @@
-import { Button, Select, Switch } from '@bitfun/ui';
+import { Button, Select, Switch, Tooltip } from '@bitfun/ui';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Archive, FolderOpen } from 'lucide-react';
import {
Alert,
Select as LegacySelect,
- Tooltip,
type SelectOption,
ConfigPageLoading,
ConfigPageMessage,
diff --git a/src/web-ui/src/infrastructure/config/components/ExternalMcpOverview.tsx b/src/web-ui/src/infrastructure/config/components/ExternalMcpOverview.tsx
index 8e131e6982..0e3ce2f58d 100644
--- a/src/web-ui/src/infrastructure/config/components/ExternalMcpOverview.tsx
+++ b/src/web-ui/src/infrastructure/config/components/ExternalMcpOverview.tsx
@@ -1,8 +1,8 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
-import { Button, IconButton } from '@bitfun/ui';
+import { Button, IconButton, Tooltip } from '@bitfun/ui';
import { Puzzle, RefreshCw } from 'lucide-react';
import { useTranslation } from 'react-i18next';
-import { Tooltip } from '@/component-library';
+
import { openEcosystemCompatibility } from '@/app/scenes/ecosystem-compatibility/ecosystemCompatibilityStore';
import { useCurrentWorkspace } from '@/infrastructure/contexts/WorkspaceContext';
import { usePeerDeviceModeOptional } from '@/infrastructure/peer-device/peerDeviceContextState';
diff --git a/src/web-ui/src/infrastructure/config/components/ExternalSourcesConfig.tsx b/src/web-ui/src/infrastructure/config/components/ExternalSourcesConfig.tsx
index bbc591ff8c..087c1da4bf 100644
--- a/src/web-ui/src/infrastructure/config/components/ExternalSourcesConfig.tsx
+++ b/src/web-ui/src/infrastructure/config/components/ExternalSourcesConfig.tsx
@@ -1,4 +1,4 @@
-import { Button, Switch, ConfirmDialog, Select } from '@bitfun/ui';
+import { Button, Switch, ConfirmDialog, Select, Tooltip } from '@bitfun/ui';
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import type { TFunction } from 'i18next';
import { useTranslation } from 'react-i18next';
@@ -14,7 +14,7 @@ import {
Settings2,
ShieldCheck,
} from 'lucide-react';
-import { ConfigPageLoading, Select as LegacySelect, Tooltip } from '@/component-library';
+import { ConfigPageLoading, Select as LegacySelect } from '@/component-library';
import { useCurrentWorkspace } from '@/infrastructure/contexts/WorkspaceContext';
import { i18nService } from '@/infrastructure/i18n';
diff --git a/src/web-ui/src/infrastructure/config/components/GlobalPermissionRulesDialog.test.tsx b/src/web-ui/src/infrastructure/config/components/GlobalPermissionRulesDialog.test.tsx
index 40197756e2..87af5b578a 100644
--- a/src/web-ui/src/infrastructure/config/components/GlobalPermissionRulesDialog.test.tsx
+++ b/src/web-ui/src/infrastructure/config/components/GlobalPermissionRulesDialog.test.tsx
@@ -33,11 +33,8 @@ vi.mock('react-i18next', () => ({
}),
}));
-vi.mock('@/component-library', () => ({
- Tooltip: ({ children }: React.PropsWithChildren) => <>{children}>,
-}));
-
vi.mock('@bitfun/ui', () => ({
+ Tooltip: ({ children }: React.PropsWithChildren) => <>{children}>,
Modal: ({ isOpen, children }: { isOpen: boolean; children: React.ReactNode }) => (
isOpen ? {children}
: null
),
diff --git a/src/web-ui/src/infrastructure/config/components/GlobalPermissionRulesDialog.tsx b/src/web-ui/src/infrastructure/config/components/GlobalPermissionRulesDialog.tsx
index 78f7536f4c..d0542544dd 100644
--- a/src/web-ui/src/infrastructure/config/components/GlobalPermissionRulesDialog.tsx
+++ b/src/web-ui/src/infrastructure/config/components/GlobalPermissionRulesDialog.tsx
@@ -1,4 +1,4 @@
-import { Button, IconButton, Input, Modal, Select, type SelectOption } from '@bitfun/ui';
+import { Button, IconButton, Input, Modal, Select, type SelectOption, Tooltip } from '@bitfun/ui';
import React, {
useCallback,
useEffect,
@@ -9,7 +9,7 @@ import React, {
} from 'react';
import { ArrowDown, ArrowUp, Plus, Save, ShieldCheck, Trash2 } from 'lucide-react';
import { useTranslation } from 'react-i18next';
-import { Tooltip } from '@/component-library';
+
import type { PermissionEffect, PermissionRule } from '../types';
import './GlobalPermissionRulesDialog.scss';
diff --git a/src/web-ui/src/infrastructure/config/components/LocalVoiceModelsConfig.tsx b/src/web-ui/src/infrastructure/config/components/LocalVoiceModelsConfig.tsx
index be7f0e1ada..8d808a9dc3 100644
--- a/src/web-ui/src/infrastructure/config/components/LocalVoiceModelsConfig.tsx
+++ b/src/web-ui/src/infrastructure/config/components/LocalVoiceModelsConfig.tsx
@@ -1,4 +1,4 @@
-import { Button, IconButton, Modal, StatusPill, type StatusPillTone } from '@bitfun/ui';
+import { Button, IconButton, Modal, StatusPill, type StatusPillTone, Tooltip } from '@bitfun/ui';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
@@ -9,7 +9,7 @@ import {
ShieldCheck,
Trash2,
} from 'lucide-react';
-import { Tooltip } from '@/component-library';
+
import { confirmDanger } from '@/infrastructure/confirm-dialog';
import {
speechAPI,
diff --git a/src/web-ui/src/infrastructure/config/components/McpToolsConfig.tsx b/src/web-ui/src/infrastructure/config/components/McpToolsConfig.tsx
index 134dddbdf2..d1c6e4a74e 100644
--- a/src/web-ui/src/infrastructure/config/components/McpToolsConfig.tsx
+++ b/src/web-ui/src/infrastructure/config/components/McpToolsConfig.tsx
@@ -4,7 +4,7 @@
* Uses settings/mcp-tools for page title/subtitle, settings/mcp for the MCP section.
*/
-import { Button, IconButton, Modal } from '@bitfun/ui';
+import { Button, IconButton, Modal, Tooltip } from '@bitfun/ui';
import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
@@ -19,7 +19,7 @@ import {
KeyRound,
Trash2,
} from 'lucide-react';
-import { Textarea, Tooltip } from '@/component-library';
+import { Textarea } from '@/component-library';
import { confirmDanger } from '@/infrastructure/confirm-dialog';
import { ToolProcessingDots } from '@bitfun/ui/flow-chat';
import {
diff --git a/src/web-ui/src/infrastructure/config/components/MemorySettingsPage.tsx b/src/web-ui/src/infrastructure/config/components/MemorySettingsPage.tsx
index e5f51bbba0..4ae26b6cb0 100644
--- a/src/web-ui/src/infrastructure/config/components/MemorySettingsPage.tsx
+++ b/src/web-ui/src/infrastructure/config/components/MemorySettingsPage.tsx
@@ -1,8 +1,8 @@
-import { Switch, IconButton, ConfirmDialog, Select, type SelectOption } from '@bitfun/ui';
+import { Switch, IconButton, ConfirmDialog, Select, type SelectOption, Tooltip } from '@bitfun/ui';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ChevronDown, ChevronUp, FolderOpen, RotateCcw, Trash2 } from 'lucide-react';
-import { ConfigPageLoading, NumberInput, Tooltip } from '@/component-library';
+import { ConfigPageLoading, NumberInput } from '@/component-library';
import { useNotification } from '@/shared/notification-system';
import { createLogger } from '@/shared/utils/logger';
diff --git a/src/web-ui/src/infrastructure/config/components/ModelSettingsPage.tsx b/src/web-ui/src/infrastructure/config/components/ModelSettingsPage.tsx
index 418bec7b70..87db1368e0 100644
--- a/src/web-ui/src/infrastructure/config/components/ModelSettingsPage.tsx
+++ b/src/web-ui/src/infrastructure/config/components/ModelSettingsPage.tsx
@@ -1,8 +1,8 @@
-import { Button, Card, Switch, IconButton, Input, Modal, SearchField, Select } from '@bitfun/ui';
+import { Button, Card, Switch, IconButton, Input, Modal, SearchField, Select, Tooltip } from '@bitfun/ui';
import React, { useState, useEffect, useMemo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { Plus, SquarePen, Trash2, Wifi, Loader, RefreshCw, AlertTriangle, X, Settings, ExternalLink, Eye, EyeOff, ChevronDown, ChevronRight, ChevronUp, Info, Brain, FolderOpen, Search as SearchIcon } from 'lucide-react';
-import { Select as LegacySelect, NumberInput, Textarea, Tooltip, type SelectOption } from '@/component-library';
+import { Select as LegacySelect, NumberInput, Textarea, type SelectOption } from '@/component-library';
import {
AIModelConfig as AIModelConfigType,
ProxyConfig,
diff --git a/src/web-ui/src/infrastructure/config/components/QuickActionsConfig.tsx b/src/web-ui/src/infrastructure/config/components/QuickActionsConfig.tsx
index 6d13cf37e6..9d80449b67 100644
--- a/src/web-ui/src/infrastructure/config/components/QuickActionsConfig.tsx
+++ b/src/web-ui/src/infrastructure/config/components/QuickActionsConfig.tsx
@@ -1,8 +1,8 @@
-import { Button, Switch, IconButton, Input, Modal } from '@bitfun/ui';
+import { Button, Switch, IconButton, Input, Modal, Tooltip } from '@bitfun/ui';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Zap, GitCommitHorizontal, GitPullRequest, Pencil, Trash2, Plus, Check } from 'lucide-react';
-import { ConfigPageLoading, Textarea, Tooltip } from '@/component-library';
+import { ConfigPageLoading, Textarea } from '@/component-library';
import {
ConfigPageHeader,
ConfigPageLayout,
diff --git a/src/web-ui/src/infrastructure/config/components/ReasoningPresetEditor.test.tsx b/src/web-ui/src/infrastructure/config/components/ReasoningPresetEditor.test.tsx
index ebee2a8f3d..928f4562fe 100644
--- a/src/web-ui/src/infrastructure/config/components/ReasoningPresetEditor.test.tsx
+++ b/src/web-ui/src/infrastructure/config/components/ReasoningPresetEditor.test.tsx
@@ -35,6 +35,7 @@ interface SelectSpyProps {
const selectProps: Record = {};
vi.mock('@bitfun/ui', () => ({
+ Tooltip: ({ children }: { children: React.ReactNode }) => <>{children}>,
Button: ({
children,
...props
@@ -100,7 +101,6 @@ vi.mock('@/component-library', () => ({
Input: (props: React.InputHTMLAttributes) => ,
NumberInput: () => ,
Textarea: () => ,
- Tooltip: ({ children }: { children: React.ReactNode }) => <>{children}>,
}));
const modelsDevReasoningCatalog: ModelsDevReasoningCatalog = {
diff --git a/src/web-ui/src/infrastructure/config/components/ReasoningPresetEditor.tsx b/src/web-ui/src/infrastructure/config/components/ReasoningPresetEditor.tsx
index 39a580111d..58249a60d0 100644
--- a/src/web-ui/src/infrastructure/config/components/ReasoningPresetEditor.tsx
+++ b/src/web-ui/src/infrastructure/config/components/ReasoningPresetEditor.tsx
@@ -1,4 +1,4 @@
-import { Button, Switch, IconButton, Input, Select } from '@bitfun/ui';
+import { Button, Switch, IconButton, Input, Select, Tooltip } from '@bitfun/ui';
import React, { useMemo, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import {
@@ -12,7 +12,7 @@ import {
Trash2,
} from 'lucide-react';
import { useTranslation } from 'react-i18next';
-import { NumberInput, Select as LegacySelect, Textarea, Tooltip, type SelectOption } from '@/component-library';
+import { NumberInput, Select as LegacySelect, Textarea, type SelectOption } from '@/component-library';
import type {
ReasoningCatalogProjection,
ReasoningConfig,
diff --git a/src/web-ui/src/infrastructure/config/components/RuntimeSettingsPages.tsx b/src/web-ui/src/infrastructure/config/components/RuntimeSettingsPages.tsx
index f4ed2edc5f..96afe27a68 100644
--- a/src/web-ui/src/infrastructure/config/components/RuntimeSettingsPages.tsx
+++ b/src/web-ui/src/infrastructure/config/components/RuntimeSettingsPages.tsx
@@ -1,8 +1,8 @@
-import { Button, Switch, IconButton, Modal, Select } from '@bitfun/ui';
+import { Button, Switch, IconButton, Modal, Select, Tooltip } from '@bitfun/ui';
import React, { useState, useEffect, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { RefreshCw, ChevronDown, Plus, Trash2, Check, Info } from 'lucide-react';
-import { NumberInput, ConfigPageLoading, Select as LegacySelect, type SelectOption, Tooltip } from '@/component-library';
+import { NumberInput, ConfigPageLoading, Select as LegacySelect, type SelectOption } from '@/component-library';
import { confirmDanger } from '@/infrastructure/confirm-dialog';
import { ConfigPageHeader, ConfigPageLayout, ConfigPageContent, ConfigPageSection, ConfigPageRow } from './common';
import { aiExperienceConfigService, type AIExperienceSettings } from '../services/AIExperienceConfigService';
diff --git a/src/web-ui/src/infrastructure/config/components/SkillsConfig.tsx b/src/web-ui/src/infrastructure/config/components/SkillsConfig.tsx
index af544e209e..96f0c46bea 100644
--- a/src/web-ui/src/infrastructure/config/components/SkillsConfig.tsx
+++ b/src/web-ui/src/infrastructure/config/components/SkillsConfig.tsx
@@ -1,9 +1,9 @@
/* eslint-disable @typescript-eslint/no-use-before-define */
-import { Button, Card, CardBody, IconButton, ConfirmDialog, Field, Input, SearchField, Select } from '@bitfun/ui';
+import { Button, Card, CardBody, IconButton, ConfirmDialog, Field, Input, SearchField, Select, Tooltip } from '@bitfun/ui';
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { Plus, Trash2, RefreshCw, FolderOpen, X, Download, CheckCircle2, TrendingUp, Search as SearchIcon } from 'lucide-react';
-import { Tooltip } from '@/component-library';
+
import { useI18n } from '@/infrastructure/i18n/hooks/useI18n';
import { ConfigPageHeader, ConfigPageLayout, ConfigPageContent, ConfigPageSection, ConfigCollectionItem } from './common';
diff --git a/src/web-ui/src/infrastructure/config/components/UsageStatisticsConfig.test.tsx b/src/web-ui/src/infrastructure/config/components/UsageStatisticsConfig.test.tsx
index a51c3d762b..28c870e915 100644
--- a/src/web-ui/src/infrastructure/config/components/UsageStatisticsConfig.test.tsx
+++ b/src/web-ui/src/infrastructure/config/components/UsageStatisticsConfig.test.tsx
@@ -34,6 +34,7 @@ vi.mock('@/infrastructure/i18n', () => ({
}));
vi.mock('@bitfun/ui', () => ({
+ Tooltip: ({ children }: React.PropsWithChildren) => <>{children}>,
IconButton: ({
children,
icon,
@@ -85,7 +86,6 @@ vi.mock('@bitfun/ui', () => ({
}));
vi.mock('@/component-library', () => ({
- Tooltip: ({ children }: React.PropsWithChildren) => <>{children}>,
ConfigPageLoading: ({ text }: { text?: string }) => {text}
,
ConfigPageMessage: ({
message,
diff --git a/src/web-ui/src/infrastructure/config/components/UsageStatisticsConfig.tsx b/src/web-ui/src/infrastructure/config/components/UsageStatisticsConfig.tsx
index 3d0cd24ad0..81ac402810 100644
--- a/src/web-ui/src/infrastructure/config/components/UsageStatisticsConfig.tsx
+++ b/src/web-ui/src/infrastructure/config/components/UsageStatisticsConfig.tsx
@@ -4,7 +4,6 @@ import {
ConfigPageLoading,
ConfigPageMessage,
ConfigPageRefreshButton,
- Tooltip,
} from '@/component-library';
import {
TokenUsageStatisticsUnavailableError,
@@ -24,7 +23,7 @@ import {
ConfigPageSectionStack,
} from './common';
import './UsageStatisticsConfig.scss';
-import { IconButton, Input, Select } from '@bitfun/ui';
+import { IconButton, Input, Select, Tooltip } from '@bitfun/ui';
// ---------------------------------------------------------------------------
// Chart palette — appearance tokens only (literal vars so the theme color
diff --git a/src/web-ui/src/infrastructure/config/components/VoiceInputDiagnostics.tsx b/src/web-ui/src/infrastructure/config/components/VoiceInputDiagnostics.tsx
index 401992a0cf..37fb555c99 100644
--- a/src/web-ui/src/infrastructure/config/components/VoiceInputDiagnostics.tsx
+++ b/src/web-ui/src/infrastructure/config/components/VoiceInputDiagnostics.tsx
@@ -1,7 +1,7 @@
-import { Button, IconButton, Select, type SelectOption } from '@bitfun/ui';
+import { Button, IconButton, Select, type SelectOption, Tooltip } from '@bitfun/ui';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Activity, RefreshCw, Square } from 'lucide-react';
-import { Tooltip } from '@/component-library';
+
import {
DEFAULT_SPEECH_SAMPLE_RATE,
speechAPI,
diff --git a/src/web-ui/src/infrastructure/config/components/WorktreeSettingsPage.test.tsx b/src/web-ui/src/infrastructure/config/components/WorktreeSettingsPage.test.tsx
index 81c911eb81..49d8e36a77 100644
--- a/src/web-ui/src/infrastructure/config/components/WorktreeSettingsPage.test.tsx
+++ b/src/web-ui/src/infrastructure/config/components/WorktreeSettingsPage.test.tsx
@@ -70,7 +70,6 @@ vi.mock('@/shared/notification-system', () => ({
}));
vi.mock('@/component-library', () => ({
- Tooltip: ({ children }: React.PropsWithChildren) => <>{children}>,
ConfigPageLoading: ({ text }: { text: string }) => {text}
,
ConfigPageMessage: ({
message,
@@ -104,6 +103,7 @@ vi.mock('@/component-library', () => ({
}));
vi.mock('@bitfun/ui', () => ({
+ Tooltip: ({ children }: React.PropsWithChildren) => <>{children}>,
Button: ({ children, disabled, onClick }: React.ButtonHTMLAttributes) => (
{children}
),
diff --git a/src/web-ui/src/infrastructure/config/components/WorktreeSettingsPage.tsx b/src/web-ui/src/infrastructure/config/components/WorktreeSettingsPage.tsx
index ca9955bd9f..0eb23f6f33 100644
--- a/src/web-ui/src/infrastructure/config/components/WorktreeSettingsPage.tsx
+++ b/src/web-ui/src/infrastructure/config/components/WorktreeSettingsPage.tsx
@@ -1,4 +1,4 @@
-import { Button, Switch, IconButton, Input, ConfirmDialog } from '@bitfun/ui';
+import { Button, Switch, IconButton, Input, ConfirmDialog, Tooltip } from '@bitfun/ui';
import React, {
useCallback,
useEffect,
@@ -16,7 +16,7 @@ import {
Trash2,
} from 'lucide-react';
import { openAgentCompanionSession } from '@/app/services/openAgentCompanionSession';
-import { ConfigPageLoading, ConfigPageMessage, ConfigPageRefreshButton, NumberInput, Tooltip } from '@/component-library';
+import { ConfigPageLoading, ConfigPageMessage, ConfigPageRefreshButton, NumberInput } from '@/component-library';
import { confirmWarning } from '@/infrastructure/confirm-dialog';
import { flowChatManager } from '@/flow_chat/services/FlowChatManager';
diff --git a/src/web-ui/src/infrastructure/config/components/external-sources/ExternalAppsOverview.tsx b/src/web-ui/src/infrastructure/config/components/external-sources/ExternalAppsOverview.tsx
index a8f3fe2bd3..625dd42fbe 100644
--- a/src/web-ui/src/infrastructure/config/components/external-sources/ExternalAppsOverview.tsx
+++ b/src/web-ui/src/infrastructure/config/components/external-sources/ExternalAppsOverview.tsx
@@ -1,8 +1,8 @@
-import { Switch } from '@bitfun/ui';
+import { Switch, Tooltip } from '@bitfun/ui';
import React from 'react';
import { CircleAlert } from 'lucide-react';
import type { TFunction } from 'i18next';
-import { Tooltip } from '@/component-library';
+
import { ConfigPageSection } from '../common';
import type { ExternalApplicationView } from './applicationModel';
diff --git a/src/web-ui/src/tools/bitfun-canvas/runtime/sdk/controls.tsx b/src/web-ui/src/tools/bitfun-canvas/runtime/sdk/controls.tsx
index 88fd43e5af..7c0f467231 100644
--- a/src/web-ui/src/tools/bitfun-canvas/runtime/sdk/controls.tsx
+++ b/src/web-ui/src/tools/bitfun-canvas/runtime/sdk/controls.tsx
@@ -1,10 +1,10 @@
import { Checkbox as BitFunCheckbox } from '@/component-library/components/Checkbox/Checkbox';
-import { Tooltip as BitFunTooltip } from '@/component-library/components/Tooltip/Tooltip';
import {
Field as DesignField,
IconButton as BitFunIconButton,
Input as DesignInput,
Switch as BitFunSwitch,
+ Tooltip as BitFunTooltip,
} from '@bitfun/ui';
import { Textarea as BitFunTextarea } from '@/component-library/components/Textarea/Textarea';
import type {
diff --git a/src/web-ui/src/tools/editor/components/DiffEditor.tsx b/src/web-ui/src/tools/editor/components/DiffEditor.tsx
index c8fad5bca8..635315290f 100644
--- a/src/web-ui/src/tools/editor/components/DiffEditor.tsx
+++ b/src/web-ui/src/tools/editor/components/DiffEditor.tsx
@@ -8,13 +8,14 @@ import { monacoAppearanceAdapter } from '@/infrastructure/appearance/adapters/Mo
import { configManager } from '@/infrastructure/config/services/ConfigManager';
import { EditorConfig as EditorConfigType } from '@/infrastructure/config/types';
import { getMonacoLanguage } from '@/infrastructure/language-detection';
-import { Tooltip, CubeLoading } from '@/component-library';
+import { CubeLoading } from '@/component-library';
import { useNotification } from '@/shared/notification-system';
import { createLogger } from '@/shared/utils/logger';
import { useI18n } from '@/infrastructure/i18n';
import { AlertCircle } from 'lucide-react';
import { activeEditTargetService, createMonacoEditTarget } from '../services/ActiveEditTargetService';
import './DiffEditor.scss';
+import { Tooltip } from '@bitfun/ui';
const log = createLogger('DiffEditor');
diff --git a/src/web-ui/src/tools/editor/components/EditorBreadcrumb.tsx b/src/web-ui/src/tools/editor/components/EditorBreadcrumb.tsx
index 3661340847..07bef2ff6a 100644
--- a/src/web-ui/src/tools/editor/components/EditorBreadcrumb.tsx
+++ b/src/web-ui/src/tools/editor/components/EditorBreadcrumb.tsx
@@ -7,8 +7,9 @@ import { ChevronRight, File, Folder, Code, Loader2, ArrowLeft } from 'lucide-rea
import { getFileIconType } from '@/tools/file-system/utils/fileIcons';
import { workspaceAPI } from '@/infrastructure/api';
import { createLogger } from '@/shared/utils/logger';
-import { Tooltip } from '@/component-library';
+
import './EditorBreadcrumb.scss';
+import { Tooltip } from '@bitfun/ui';
const log = createLogger('EditorBreadcrumb');
diff --git a/src/web-ui/src/tools/editor/components/EditorStatusBar.tsx b/src/web-ui/src/tools/editor/components/EditorStatusBar.tsx
index 5a97996fd9..3dafdc68ab 100644
--- a/src/web-ui/src/tools/editor/components/EditorStatusBar.tsx
+++ b/src/web-ui/src/tools/editor/components/EditorStatusBar.tsx
@@ -1,9 +1,10 @@
/** Status bar for cursor position, language, and encoding. */
import React from 'react';
-import { Tooltip } from '@/component-library';
+
import { useI18n } from '@/infrastructure/i18n';
import './EditorStatusBar.scss';
+import { Tooltip } from '@bitfun/ui';
export interface EditorStatusBarProps {
/** Current line number */
diff --git a/src/web-ui/src/tools/editor/components/ImageViewer.tsx b/src/web-ui/src/tools/editor/components/ImageViewer.tsx
index 91a92b8a85..ed59a1aa2c 100644
--- a/src/web-ui/src/tools/editor/components/ImageViewer.tsx
+++ b/src/web-ui/src/tools/editor/components/ImageViewer.tsx
@@ -7,9 +7,9 @@
import React, { useState, useEffect, useCallback } from 'react';
import { ZoomIn, ZoomOut, RotateCw, Download, Maximize2 } from 'lucide-react';
-import { Button, IconButton, Toolbar, ToolbarGroup, ToolbarSeparator } from '@bitfun/ui';
+import { Button, IconButton, Toolbar, ToolbarGroup, ToolbarSeparator, Tooltip } from '@bitfun/ui';
import { createLogger } from '@/shared/utils/logger';
-import { Tooltip } from '@/component-library';
+
import { useI18n } from '@/infrastructure/i18n';
import './ImageViewer.scss';
diff --git a/src/web-ui/src/tools/editor/components/MarkdownEditor.test.tsx b/src/web-ui/src/tools/editor/components/MarkdownEditor.test.tsx
index e86f90e0ef..9500f305e1 100644
--- a/src/web-ui/src/tools/editor/components/MarkdownEditor.test.tsx
+++ b/src/web-ui/src/tools/editor/components/MarkdownEditor.test.tsx
@@ -39,6 +39,31 @@ vi.mock('@bitfun/ui', () => ({
}) => (
{icon}
),
+ SegmentedControl: ({
+ options,
+ value,
+ onValueChange,
+ 'aria-label': ariaLabel,
+ }: {
+ options: Array<{ value: string; label: React.ReactNode }>;
+ value: string;
+ onValueChange?: (value: string) => void;
+ 'aria-label'?: string;
+ }) => (
+
+ {options.map((option) => (
+ onValueChange?.(option.value)}
+ >
+ {option.label}
+
+ ))}
+
+ ),
}));
vi.mock('../meditor', () => ({
diff --git a/src/web-ui/src/tools/editor/components/PlanViewer.tsx b/src/web-ui/src/tools/editor/components/PlanViewer.tsx
index 49b81584e2..688e6c40a4 100644
--- a/src/web-ui/src/tools/editor/components/PlanViewer.tsx
+++ b/src/web-ui/src/tools/editor/components/PlanViewer.tsx
@@ -1,13 +1,13 @@
/** Optimized viewer/editor for `.plan.md` files (frontmatter + markdown body). */
-import { Button, IconButton } from '@bitfun/ui';
+import { Button, IconButton, Tooltip } from '@bitfun/ui';
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
import { Circle, ArrowRight, Check, XCircle, Loader2, CheckCircle, AlertCircle, FileText, Pencil, X, ChevronDown, Trash2, Plus } from 'lucide-react';
import yaml from 'yaml';
import { MEditor } from '../meditor';
import type { EditorInstance } from '../meditor';
import { createLogger } from '@/shared/utils/logger';
-import { CubeLoading, Tooltip } from '@/component-library';
+import { CubeLoading } from '@/component-library';
import { useI18n } from '@/infrastructure/i18n';
import { workspaceAPI } from '@/infrastructure/api/service-api/WorkspaceAPI';
import { flowChatManager } from '@/flow_chat/services/FlowChatManager';
diff --git a/src/web-ui/src/tools/editor/meditor/components/TiptapEditor.tsx b/src/web-ui/src/tools/editor/meditor/components/TiptapEditor.tsx
index 07d7a3f93f..2b80fe631b 100644
--- a/src/web-ui/src/tools/editor/meditor/components/TiptapEditor.tsx
+++ b/src/web-ui/src/tools/editor/meditor/components/TiptapEditor.tsx
@@ -6,7 +6,7 @@ import React, {
useRef,
useState,
} from 'react';
-import { Button, IconButton, Input } from '@bitfun/ui';
+import { Button, IconButton, Input, Tooltip } from '@bitfun/ui';
import { EditorContent, useEditor } from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit';
import { Details, DetailsContent, DetailsSummary } from '@tiptap/extension-details';
@@ -19,7 +19,7 @@ import type { Editor as TiptapEditorInstance, JSONContent } from '@tiptap/core';
import type { Node as ProseMirrorNode } from '@tiptap/pm/model';
import { Selection } from '@tiptap/pm/state';
import { useI18n } from '@/infrastructure/i18n';
-import { Tooltip } from '@/component-library';
+
import { editorAiAPI } from '@/infrastructure/api/service-api/EditorAiAPI';
import { notificationService } from '@/shared/notification-system';
import { createLogger } from '@/shared/utils/logger';
diff --git a/src/web-ui/src/tools/file-system/components/FileExplorer.tsx b/src/web-ui/src/tools/file-system/components/FileExplorer.tsx
index 9736df0e56..8faa4cbd0b 100644
--- a/src/web-ui/src/tools/file-system/components/FileExplorer.tsx
+++ b/src/web-ui/src/tools/file-system/components/FileExplorer.tsx
@@ -1,5 +1,5 @@
import React, { useState, useCallback, useMemo, useRef, useEffect } from 'react';
-import { IconButton } from '@bitfun/ui';
+import { IconButton, Tooltip } from '@bitfun/ui';
import { useShortcut } from '@/infrastructure/hooks/useShortcut';
import { Folder, FilePlus, FolderPlus, RefreshCw } from 'lucide-react';
import { VirtualFileTree } from './VirtualFileTree';
@@ -8,7 +8,7 @@ import { flattenFileTree } from '../utils/treeFlattening';
import { getNewItemParentPath } from '../utils/getNewItemParentPath';
import { i18nService, useI18n } from '@/infrastructure/i18n';
import { expandedFoldersContains, pathsEquivalentFs } from '@/shared/utils/pathUtils';
-import { Tooltip } from '@/component-library';
+
import { filterTreeByPredicate, filterTreeBySearch } from '@/tools/file-explorer';
import { globalEventBus } from '@/infrastructure/event-bus';
import { commandExecutor } from '@/shared/context-menu-system/commands/CommandExecutor';
diff --git a/src/web-ui/src/tools/git/components/PushButton/PushButton.tsx b/src/web-ui/src/tools/git/components/PushButton/PushButton.tsx
index e25bb5f2a0..70896ac239 100644
--- a/src/web-ui/src/tools/git/components/PushButton/PushButton.tsx
+++ b/src/web-ui/src/tools/git/components/PushButton/PushButton.tsx
@@ -1,9 +1,9 @@
/** Push button with optional force-push dropdown. */
-import { Button, IconButton } from '@bitfun/ui';
+import { Button, IconButton, Tooltip } from '@bitfun/ui';
import React, { useState, useRef, useEffect } from 'react';
import { ChevronDown, ArrowUp, AlertTriangle } from 'lucide-react';
-import { Tooltip } from '@/component-library';
+
import { useI18n } from '@/infrastructure/i18n';
import './PushButton.scss';
From 76fa61a289b9f9277bacb3921cd4d56a379c6873 Mon Sep 17 00:00:00 2001
From: kev1n77
Date: Sat, 29 Aug 2026 09:31:34 +0800
Subject: [PATCH 9/9] feat(web-ui): adopt ScrollArea, FieldGroup,
NavigationPanel, and catalog Icons
Replace remaining product viewports, form groups, scene navigation, and 1:1 lucide glyphs with @bitfun/ui primitives while keeping flow_chat frozen.
---
.../NavigationPanel/NavigationPanel.meta.ts | 3 +
.../NavigationPanel/NavigationPanel.tsx | 5 +
.../components/AboutDialog/AboutDialog.scss | 2 -
.../components/AboutDialog/AboutDialog.tsx | 33 +--
.../AgentCompanionDesktopPet.scss | 1 -
.../AgentCompanionDesktopPet.tsx | 5 +-
.../GalleryLayout/GalleryLayout.scss | 1 -
.../GalleryLayout/GalleryLayout.tsx | 5 +-
.../MCPInteractionDialog.scss | 1 -
.../MCPInteractionDialog.tsx | 6 +-
.../src/app/components/NavBar/NavBar.tsx | 8 +-
.../src/app/components/NavPanel/MainNav.tsx | 39 +--
.../src/app/components/NavPanel/NavPanel.scss | 41 ++-
.../src/app/components/NavPanel/NavPanel.tsx | 4 +-
.../src/app/components/NavPanel/appearance.ts | 1 -
.../components/AssistantSessionCreateMenu.tsx | 10 +-
.../BranchQuickSwitch.appearance.ts | 2 +-
.../components/BranchQuickSwitch.scss | 10 -
.../NavPanel/components/BranchQuickSwitch.tsx | 11 +-
.../components/DeviceStatusControl.tsx | 20 +-
.../NavPanel/components/GithubStarButton.tsx | 8 +-
.../components/PersistentFooterActions.tsx | 13 +-
.../NavPanel/components/SectionHeader.tsx | 5 +-
.../components/WorkspaceSessionFilterMenu.tsx | 10 +-
.../sections/sessions/SessionsSection.scss | 1 -
.../sections/sessions/SessionsSection.tsx | 24 +-
.../sections/workspaces/WorkspaceItem.tsx | 32 +--
.../WorkspaceProjectPermissionsDialog.tsx | 8 +-
.../WorkspaceRelatedPathsDialog.tsx | 8 +-
.../WorkspaceSessionBatchModal.scss | 1 -
.../workspaces/WorkspaceSessionBatchModal.tsx | 14 +-
.../NavPanel/unifiedSessionCreation.test.ts | 2 +-
.../NewProjectDialog/NewProjectDialog.tsx | 14 +-
.../RemoteConnectDialog/AccountPanel.scss | 1 -
.../RemoteConnectDialog/AccountPanel.tsx | 36 +--
.../RemoteConnectDialog.contract.test.ts | 2 +-
.../RemoteConnectDialog.scss | 2 -
.../RemoteConnectDialog.tsx | 42 ++-
.../RemoteConnectDisclaimer.scss | 6 +-
.../RemoteConnectDisclaimer.tsx | 32 +--
.../src/app/components/SceneBar/SceneBar.tsx | 8 +-
.../TitleBar/NotificationButton.tsx | 10 +-
.../components/panels/BranchSelectModal.scss | 1 -
.../components/panels/BranchSelectModal.tsx | 14 +-
.../panels/DiffFullscreenViewer.tsx | 10 +-
.../src/app/components/panels/FilesPanel.scss | 1 -
.../src/app/components/panels/FilesPanel.tsx | 16 +-
.../components/panels/base/FlexiblePanel.tsx | 10 +-
.../src/app/components/panels/base/utils.ts | 33 +--
.../content-canvas/anchor-zone/AnchorZone.tsx | 10 +-
.../content-canvas/empty-state/EmptyState.tsx | 6 +-
.../mission-control/MissionControl.scss | 1 -
.../mission-control/MissionControl.tsx | 10 +-
.../mission-control/SearchFilter.tsx | 6 +-
.../mission-control/ThumbnailCard.tsx | 16 +-
.../content-canvas/quick-look/QuickLook.tsx | 9 +-
.../panels/content-canvas/tab-bar/Tab.tsx | 6 +-
.../panels/content-canvas/tab-bar/TabBar.tsx | 6 +-
.../tab-bar/TabOverflowMenu.appearance.ts | 2 +-
.../tab-bar/TabOverflowMenu.scss | 14 -
.../tab-bar/TabOverflowMenu.tsx | 12 +-
.../review-platform/ReviewPlatformPanel.scss | 2 -
.../review-platform/ReviewPlatformPanel.tsx | 95 +++----
.../scheduled-jobs/DateTimePickerPopover.tsx | 6 +-
.../app/global-search/GlobalSearchRoot.scss | 1 -
.../app/global-search/GlobalSearchRoot.tsx | 58 ++---
.../src/app/layout/FloatingMiniChat.scss | 1 -
.../src/app/layout/FloatingMiniChat.tsx | 8 +-
.../src/app/layout/MiniAppBubbleWelcome.tsx | 9 +-
.../app/scenes/agents/AgentsScene.test.tsx | 2 +-
.../src/app/scenes/agents/AgentsScene.tsx | 51 ++--
.../src/app/scenes/agents/agentsIcons.ts | 32 ++-
.../scenes/agents/components/AgentCard.scss | 1 -
.../scenes/agents/components/AgentCard.tsx | 19 +-
.../agents/components/CoreAgentCard.scss | 4 +-
.../agents/components/CoreAgentCard.tsx | 19 +-
.../agents/components/CreateAgentPage.scss | 18 --
.../agents/components/CreateAgentPage.tsx | 41 +--
.../agents/components/SkillGroupPicker.scss | 2 -
.../agents/components/SkillGroupPicker.tsx | 22 +-
.../agents/components/ToolGroupPicker.scss | 2 -
.../agents/components/ToolGroupPicker.tsx | 22 +-
.../src/app/scenes/browser/BrowserPanel.tsx | 10 +-
.../src/app/scenes/browser/BrowserScene.tsx | 10 +-
.../EcosystemCompatibilityScene.scss | 2 -
.../EcosystemCompatibilityScene.tsx | 54 ++--
.../app/scenes/file-viewer/FileViewerNav.scss | 20 ++
.../app/scenes/file-viewer/FileViewerNav.tsx | 20 +-
.../src/app/scenes/git/GitNav.appearance.ts | 2 +-
src/web-ui/src/app/scenes/git/GitNav.scss | 21 +-
src/web-ui/src/app/scenes/git/GitNav.tsx | 59 +++--
src/web-ui/src/app/scenes/git/GitScene.tsx | 8 +-
.../app/scenes/git/views/BranchesView.scss | 2 -
.../src/app/scenes/git/views/BranchesView.tsx | 35 +--
.../app/scenes/git/views/WorkingCopyView.tsx | 32 +--
.../scenes/miniapps/MiniAppGalleryScene.tsx | 10 +-
.../src/app/scenes/miniapps/MiniAppScene.scss | 6 +-
.../src/app/scenes/miniapps/MiniAppScene.tsx | 8 +-
.../components/MiniAppDetailModal.scss | 1 -
.../components/MiniAppDetailModal.tsx | 35 +--
.../customization/MiniAppCustomizePanel.tsx | 12 +-
.../MiniAppPermissionDiffDialog.tsx | 14 +-
.../scenes/miniapps/utils/miniAppIcons.tsx | 36 ++-
.../miniapps/views/MiniAppGalleryView.tsx | 8 +-
.../miniapps/views/MiniAppMarketView.tsx | 32 +--
.../miniapps/views/MiniAppSubmissionsView.tsx | 9 +-
.../app/scenes/my-agent/InsightsScene.scss | 2 -
.../src/app/scenes/my-agent/InsightsScene.tsx | 48 ++--
.../src/app/scenes/pages/PagesScene.tsx | 42 +--
.../profile/views/AssistantAvatarPicker.tsx | 14 +-
.../scenes/profile/views/AssistantCard.tsx | 21 +-
.../profile/views/AssistantConfigPage.tsx | 18 +-
.../profile/views/AssistantDefaultsPage.scss | 2 -
.../profile/views/AssistantDefaultsPage.tsx | 46 ++--
.../profile/views/AssistantQuickInput.tsx | 6 +-
.../views/NurseryGallery.presentation.test.ts | 4 +-
.../scenes/profile/views/NurseryGallery.tsx | 31 +--
.../app/scenes/profile/views/NurseryView.scss | 11 -
src/web-ui/src/app/scenes/registry.ts | 39 ++-
.../scenes/settings/SettingsNav.appearance.ts | 1 -
.../src/app/scenes/settings/SettingsNav.scss | 21 +-
.../src/app/scenes/settings/SettingsNav.tsx | 239 ++++++++++--------
.../components/ArchivedSessionsConfig.tsx | 8 +-
.../components/KeyboardShortcutsTab.tsx | 6 +-
.../pages/AutomationSettingsPage.scss | 3 +-
.../settings/pages/AutomationSettingsPage.tsx | 5 +-
.../pages/DevelopmentSettingsPage.scss | 1 -
.../pages/DevelopmentSettingsPage.tsx | 5 +-
.../settings/pages/InputSettingsPage.scss | 1 -
.../settings/pages/InputSettingsPage.tsx | 5 +-
.../app/scenes/shell/ShellNav.appearance.ts | 2 +-
src/web-ui/src/app/scenes/shell/ShellNav.scss | 22 +-
src/web-ui/src/app/scenes/shell/ShellNav.tsx | 34 +--
.../components/ShellNavWorkspaceSwitcher.tsx | 8 +-
.../src/app/scenes/skills/SkillsScene.scss | 13 -
.../src/app/scenes/skills/SkillsScene.tsx | 61 ++---
.../src/app/scenes/skills/appearance.ts | 4 +-
.../scenes/skills/components/SkillCard.tsx | 9 +-
.../skills/components/SkillsSuiteView.tsx | 13 +-
.../src/app/scenes/todos/TodosScene.scss | 12 +-
.../src/app/scenes/todos/TodosScene.tsx | 21 +-
.../scenes/todos/components/TodoCalendar.tsx | 5 +-
.../scenes/todos/components/TodoEditor.tsx | 29 +--
.../scenes/todos/components/TodoItemRow.tsx | 6 +-
.../dispatch/DispatchInstallDialog.scss | 1 -
.../dispatch/DispatchInstallDialog.test.tsx | 2 +
.../dispatch/DispatchInstallDialog.tsx | 10 +-
.../dispatch/DispatchResultDialog.scss | 7 +-
.../dispatch/DispatchResultDialog.test.tsx | 1 +
.../dispatch/DispatchResultDialog.tsx | 24 +-
.../dispatch/DispatchTargetPicker.scss | 1 -
.../dispatch/DispatchTargetPicker.tsx | 22 +-
.../MarketAccountControls.test.tsx | 1 +
.../market-account/MarketAccountControls.tsx | 6 +-
.../relay-deploy/RelayDeployWizard.scss | 1 -
.../relay-deploy/RelayDeployWizard.tsx | 78 +++---
.../ssh-remote/PortForwardDialog.test.tsx | 10 +
.../features/ssh-remote/PortForwardDialog.tsx | 49 ++--
.../ssh-remote/RemoteFileBrowser.scss | 1 -
.../features/ssh-remote/RemoteFileBrowser.tsx | 42 ++-
.../ssh-remote/SSHAuthPromptDialog.tsx | 6 +-
.../ssh-remote/SSHConnectionDialog.scss | 7 -
.../ssh-remote/SSHConnectionDialog.test.tsx | 20 ++
.../ssh-remote/SSHConnectionDialog.tsx | 134 +++++-----
.../schema/migrateAppearancePackage.ts | 8 +-
.../config/components/AcpAgentsConfig.tsx | 30 +--
.../AppearanceMarketDialog.test.tsx | 2 +
.../components/AppearanceMarketDialog.tsx | 36 +--
.../components/AppearanceMarketWorkflows.tsx | 26 +-
.../AppearancePackageConfigSection.tsx | 14 +-
.../components/AppearanceSettingsPage.scss | 11 +-
.../AppearanceSettingsPage.test.tsx | 3 +
.../config/components/ExternalMcpOverview.tsx | 10 +-
.../components/ExternalSourcesConfig.tsx | 29 +--
.../GlobalPermissionRulesDialog.test.tsx | 10 +
.../GlobalPermissionRulesDialog.tsx | 37 ++-
.../config/components/HooksConfig.tsx | 6 +-
.../components/LocalVoiceModelsConfig.tsx | 8 +-
.../config/components/MCPResourceBrowser.scss | 2 -
.../config/components/MCPResourceBrowser.tsx | 20 +-
.../config/components/McpToolsConfig.scss | 5 +-
.../config/components/McpToolsConfig.tsx | 8 +-
.../config/components/MemorySettingsPage.tsx | 6 +-
.../config/components/ModelSettingsPage.scss | 11 +-
.../config/components/ModelSettingsPage.tsx | 80 +++---
.../config/components/QuickActionsConfig.tsx | 12 +-
.../components/ReasoningConfigPanel.scss | 1 -
.../components/ReasoningConfigPanel.tsx | 6 +-
.../components/ReasoningPresetEditor.scss | 1 -
.../components/ReasoningPresetEditor.test.tsx | 2 +
.../components/ReasoningPresetEditor.tsx | 25 +-
.../components/RuntimeSettingsPages.scss | 1 -
.../components/RuntimeSettingsPages.tsx | 19 +-
.../config/components/SkillsConfig.tsx | 22 +-
.../components/UsageStatisticsConfig.scss | 2 -
.../components/UsageStatisticsConfig.test.tsx | 10 +
.../components/UsageStatisticsConfig.tsx | 16 +-
.../config/components/VoiceInputConfig.tsx | 10 +-
.../components/WorktreeSettingsPage.test.tsx | 1 +
.../components/WorktreeSettingsPage.tsx | 5 +-
.../common/ConfigCollectionItem.scss | 1 -
.../common/ConfigCollectionItem.tsx | 5 +-
.../components/common/ConfigPageLayout.scss | 2 -
.../components/common/ConfigPageLayout.tsx | 55 ++--
.../components/form-controls/ConfigStatus.tsx | 6 +-
.../peer-device/DeviceSurfaceSwitcher.scss | 1 -
.../peer-device/DeviceSurfaceSwitcher.tsx | 10 +-
.../peer-device/PeerDirectoryBrowser.scss | 1 -
.../peer-device/PeerDirectoryBrowser.tsx | 15 +-
.../update/UpdateAvailableDialog.scss | 1 -
.../update/UpdateAvailableDialog.tsx | 10 +-
.../components/AnnouncementToastItem.tsx | 6 +-
.../components/FeatureModal.tsx | 10 +-
.../styles/FeatureModal.scss | 1 -
.../components/ContextMenuRenderer.tsx | 32 ++-
.../components/ContextCard/ContextCard.tsx | 7 +-
.../components/ContextList/ContextList.scss | 1 -
.../components/ContextList/ContextList.tsx | 10 +-
.../core/types/FileContextImpl.tsx | 5 +-
.../core/types/ImageContextImpl.tsx | 8 +-
.../core/types/MermaidDiagramContextImpl.tsx | 5 +-
.../components/LoadingNotification.tsx | 6 +-
.../components/NotificationCenter.scss | 2 -
.../components/NotificationCenter.tsx | 22 +-
.../components/NotificationContainer.scss | 13 +-
.../components/NotificationContainer.tsx | 12 +-
.../components/NotificationItem.tsx | 12 +-
.../components/ProgressNotification.tsx | 6 +-
.../tools/bitfun-canvas/BitfunCanvasPanel.tsx | 5 +-
.../editor/components/EditorBreadcrumb.scss | 1 -
.../editor/components/EditorBreadcrumb.tsx | 27 +-
.../tools/editor/components/ImageViewer.tsx | 6 +-
.../editor/components/MarkdownEditor.test.tsx | 3 +-
.../editor/components/MarkdownEditor.tsx | 8 +-
.../tools/editor/components/PlanViewer.tsx | 26 +-
.../StatusBarPopovers/StatusBarPopovers.scss | 1 -
.../StatusBarPopovers/StatusBarPopovers.tsx | 14 +-
.../components/InlineAiPreviewBlock.tsx | 8 +-
.../meditor/components/TiptapEditor.tsx | 10 +-
.../file-system/components/FileExplorer.tsx | 6 +-
.../components/FileSearchResults.scss | 1 -
.../components/FileSearchResults.tsx | 15 +-
.../file-system/components/FileTreeItem.tsx | 6 +-
.../src/tools/file-system/utils/fileIcons.ts | 18 +-
.../CreateBranchDialog.scss | 5 +-
.../CreateBranchDialog/CreateBranchDialog.tsx | 78 +++---
.../GitBranchHistoryView.scss | 1 -
.../GitBranchHistoryView.tsx | 16 +-
.../GitDiffEditor/GitDiffEditor.tsx | 5 +-
.../components/GitDiffView/GitDiffView.tsx | 20 +-
.../components/GitGraphView/GitGraphView.scss | 1 -
.../components/GitGraphView/GitGraphView.tsx | 14 +-
.../GitSettingsView/GitSettingsView.css | 1 -
.../GitSettingsView/GitSettingsView.scss | 1 -
.../GitSettingsView/GitSettingsView.tsx | 44 ++--
.../git/components/PushButton/PushButton.tsx | 15 +-
.../terminal/components/ConnectedTerminal.tsx | 6 +-
.../workspace/components/WorkspaceManager.css | 1 -
.../workspace/components/WorkspaceManager.tsx | 16 +-
tests/e2e/specs/l0-navigation.spec.ts | 4 +-
260 files changed, 1856 insertions(+), 2059 deletions(-)
diff --git a/design-system/packages/ui/src/components/NavigationPanel/NavigationPanel.meta.ts b/design-system/packages/ui/src/components/NavigationPanel/NavigationPanel.meta.ts
index ad57c2c9b5..26f3058b3f 100644
--- a/design-system/packages/ui/src/components/NavigationPanel/NavigationPanel.meta.ts
+++ b/design-system/packages/ui/src/components/NavigationPanel/NavigationPanel.meta.ts
@@ -9,6 +9,9 @@ export const navigationPanelMeta = {
{ name: "children", type: "ReactNode" },
{ name: "header", type: "ReactNode" },
{ name: "footer", type: "ReactNode" },
+ { name: "bodyRef", type: "Ref" },
+ { name: "bodyClassName", type: "string" },
+ { name: "contentClassName", type: "string" },
{ defaultValue: "auto", name: "scrollbarVisibility", type: "auto | always | hidden" },
],
states: ["default", "selected-item", "disabled-item", "scrolling"],
diff --git a/design-system/packages/ui/src/components/NavigationPanel/NavigationPanel.tsx b/design-system/packages/ui/src/components/NavigationPanel/NavigationPanel.tsx
index 9f3df6b6e7..cefe3faec2 100644
--- a/design-system/packages/ui/src/components/NavigationPanel/NavigationPanel.tsx
+++ b/design-system/packages/ui/src/components/NavigationPanel/NavigationPanel.tsx
@@ -4,6 +4,7 @@ import {
type HTMLAttributes,
type MouseEventHandler,
type ReactNode,
+ type Ref,
} from "react";
import { ActionItem, type ActionItemProps } from "../ActionItem";
import { IconButton, type IconButtonProps } from "../IconButton";
@@ -14,6 +15,8 @@ import styles from "./NavigationPanel.module.css";
export interface NavigationPanelProps
extends Omit, "children"> {
bodyClassName?: string;
+ /** Ref to the scroll viewport so product sticky headers can track the same root. */
+ bodyRef?: Ref;
children: ReactNode;
contentClassName?: string;
footer?: ReactNode;
@@ -47,6 +50,7 @@ export type NavigationPanelSeparatorProps = HTMLAttributes;
export const NavigationPanel = forwardRef(
function NavigationPanel({
bodyClassName,
+ bodyRef,
children,
className,
contentClassName,
@@ -68,6 +72,7 @@ export const NavigationPanel = forwardRef(
= ({
data-bf-part="root"
>
-
= ({
}
+ leadingIcon={}
onClick={() => void handleCheckForUpdates()}
data-testid="about-check-updates"
>
@@ -329,7 +323,7 @@ export const AboutDialog: React.FC = ({
{updateStatus === 'installed' ? (
-
+
{t('update.installedMessage')}
@@ -359,9 +353,9 @@ export const AboutDialog: React.FC = ({
))}
-
+
-
= ({
onClick={() => void copyToClipboard(version.gitCommit ?? '', 'commit')}
aria-label={t('about.copyCommit')}
>
- {copiedItem === 'commit' ? : }
+ {copiedItem === 'commit' ? : }
) : null}
@@ -424,7 +418,7 @@ export const AboutDialog: React.FC = ({
-
+
{t('about.branch')}
= ({
{licenseName}
-
+
= ({
className="bitfun-about-dialog__star-rule bitfun-about-dialog__star-rule--leading"
aria-hidden="true"
/>
-
+
{t('about.githubStarTitle')}
@@ -485,7 +474,7 @@ export const AboutDialog: React.FC = ({
variant="fill"
size="md"
className="bitfun-about-dialog__star-button"
- trailingIcon={}
+ trailingIcon={}
onClick={handleGithubStar}
data-testid="about-github-star"
>
diff --git a/src/web-ui/src/app/components/AgentCompanionDesktopPet/AgentCompanionDesktopPet.scss b/src/web-ui/src/app/components/AgentCompanionDesktopPet/AgentCompanionDesktopPet.scss
index 486941b3ae..8c9a0d7375 100644
--- a/src/web-ui/src/app/components/AgentCompanionDesktopPet/AgentCompanionDesktopPet.scss
+++ b/src/web-ui/src/app/components/AgentCompanionDesktopPet/AgentCompanionDesktopPet.scss
@@ -273,7 +273,6 @@
align-items: flex-end;
gap: 6px;
overflow-x: hidden;
- overflow-y: auto;
overscroll-behavior: contain;
pointer-events: auto;
scrollbar-width: thin;
diff --git a/src/web-ui/src/app/components/AgentCompanionDesktopPet/AgentCompanionDesktopPet.tsx b/src/web-ui/src/app/components/AgentCompanionDesktopPet/AgentCompanionDesktopPet.tsx
index d7b6c9ea2e..dcf206ab50 100644
--- a/src/web-ui/src/app/components/AgentCompanionDesktopPet/AgentCompanionDesktopPet.tsx
+++ b/src/web-ui/src/app/components/AgentCompanionDesktopPet/AgentCompanionDesktopPet.tsx
@@ -1,3 +1,4 @@
+import { ScrollArea } from '@bitfun/ui';
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { emit, listen } from '@tauri-apps/api/event';
@@ -913,7 +914,7 @@ export const AgentCompanionDesktopPet: React.FC = () => {
className="bitfun-agent-companion-window__dock"
data-bf-component="agent-companion-desktop-pet" data-bf-part="dock">
{visibleTasks.length > 0 && (
- {
);
})}
-
+
)}
{
@@ -13,11 +14,11 @@ const GalleryLayout: React.FC
= ({ children, className, ...r
{...rootProps}
className={['gallery-layout', className].filter(Boolean).join(' ')}
>
-
+
);
diff --git a/src/web-ui/src/app/components/MCPInteractionDialog/MCPInteractionDialog.scss b/src/web-ui/src/app/components/MCPInteractionDialog/MCPInteractionDialog.scss
index d2aa6d667b..f6967b7bc4 100644
--- a/src/web-ui/src/app/components/MCPInteractionDialog/MCPInteractionDialog.scss
+++ b/src/web-ui/src/app/components/MCPInteractionDialog/MCPInteractionDialog.scss
@@ -47,7 +47,6 @@
white-space: pre-wrap;
word-break: break-word;
max-height: 180px;
- overflow: auto;
}
.mcp-interaction-dialog__editor {
diff --git a/src/web-ui/src/app/components/MCPInteractionDialog/MCPInteractionDialog.tsx b/src/web-ui/src/app/components/MCPInteractionDialog/MCPInteractionDialog.tsx
index c3461acccc..e346794cc7 100644
--- a/src/web-ui/src/app/components/MCPInteractionDialog/MCPInteractionDialog.tsx
+++ b/src/web-ui/src/app/components/MCPInteractionDialog/MCPInteractionDialog.tsx
@@ -1,4 +1,4 @@
-import { Button, Modal } from '@bitfun/ui';
+import { Button, Modal, ScrollArea } from '@bitfun/ui';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { globalEventBus } from '@/infrastructure/event-bus';
import { MCPAPI } from '@/infrastructure/api/service-api/MCPAPI';
@@ -153,7 +153,9 @@ export const MCPInteractionDialog: React.FC = () => {
Request Params
-
{paramsPreview}
+
+ {paramsPreview}
+
diff --git a/src/web-ui/src/app/components/NavBar/NavBar.tsx b/src/web-ui/src/app/components/NavBar/NavBar.tsx
index 047717a1e0..cd73285564 100644
--- a/src/web-ui/src/app/components/NavBar/NavBar.tsx
+++ b/src/web-ui/src/app/components/NavBar/NavBar.tsx
@@ -10,7 +10,7 @@
*/
import React, { useCallback, useMemo, useRef } from 'react';
-import { ArrowLeft, ArrowRight } from 'lucide-react';
+;
import { useNavSceneStore } from '../../stores/navSceneStore';
import { useI18n } from '../../../infrastructure/i18n';
@@ -18,7 +18,7 @@ import { PanelLeftIcon } from '../TitleBar/PanelIcons';
import { createLogger } from '@/shared/utils/logger';
import { isMacOSDesktopRuntime, supportsNativeWindowDragging } from '@/infrastructure/runtime';
import './NavBar.scss';
-import { Tooltip } from '@bitfun/ui';
+import { Icon, Tooltip } from '@bitfun/ui';
const log = createLogger('NavBar');
@@ -128,7 +128,7 @@ const NavBar: React.FC
= ({
aria-disabled={!canGoBack}
aria-label={t('nav.back')}
>
-
+
@@ -142,7 +142,7 @@ const NavBar: React.FC = ({
aria-disabled={!canGoForward}
aria-label={t('nav.forward')}
>
-
+
diff --git a/src/web-ui/src/app/components/NavPanel/MainNav.tsx b/src/web-ui/src/app/components/NavPanel/MainNav.tsx
index f3a950a8a7..7cee4b4605 100644
--- a/src/web-ui/src/app/components/NavPanel/MainNav.tsx
+++ b/src/web-ui/src/app/components/NavPanel/MainNav.tsx
@@ -12,10 +12,10 @@
import React, { useCallback, useState, useMemo, useEffect, useRef, useSyncExternalStore } from 'react';
import { createPortal } from 'react-dom';
-import { KeyHint, Menu, MenuItem, MenuSection, MenuSeparator, Tooltip } from '@bitfun/ui';
+import { Icon, KeyHint, Menu, MenuItem, MenuSection, MenuSeparator, NavigationPanel, ScrollArea, Tooltip } from '@bitfun/ui';
import { getAppearanceOverlayHost } from '@/infrastructure/appearance/runtime/AppearanceOverlayHost';
import { isImeOwnedKeyboardEvent } from '@/shared/utils/ime';
-import { FolderOpen, FolderPlus, History, Check, User, Users, Puzzle, ChevronDown, Network, Search, CalendarClock } from 'lucide-react';
+import { FolderOpen, FolderPlus, History, Users, Network, CalendarClock } from 'lucide-react';
// import { PanelsTopLeft } from 'lucide-react'; // temporarily hidden: Pages nav entry
import {
BITFUN_ICON_SIZE,
@@ -293,7 +293,7 @@ const MainNav: React.FC = ({
{t('header.newProject')}
}
+ leading={}
onClick={handleOpenAssistantManager}
data-testid="nav-session-group-add-assistant"
>
@@ -317,8 +317,8 @@ const MainNav: React.FC = ({
{t('header.recentWorkspaces')}
>
)}
- itemsClassName="bitfun-nav-panel__workspace-menu-workspaces"
>
+
{recentWorkspaces.length === 0 ? (
{t('header.noRecentWorkspaces')}
@@ -333,7 +333,7 @@ const MainNav: React.FC
= ({
leading={}
role="menuitemradio"
checked={isCurrent}
- metadata={isCurrent ? : undefined}
+ metadata={isCurrent ? : undefined}
title={tooltip}
onClick={() => { void handleSwitchWorkspace(workspace.id); }}
data-testid="nav-workspace-menu-recent-workspace"
@@ -354,6 +354,7 @@ const MainNav: React.FC = ({
);
})
)}
+
,
getAppearanceOverlayHost()
@@ -372,7 +373,12 @@ const MainNav: React.FC = ({
const isTaskBoardActive = activeTabId === 'todos';
return (
<>
- {/* ── Search and client voice assistant ─────── */}
+
@@ -388,7 +394,7 @@ const MainNav: React.FC = ({
>
-
+
@@ -407,9 +413,9 @@ const MainNav: React.FC = ({
-
- {/* ── Long-lived navigation ──────────────────── */}
-
+ )}
+ >
+
= ({
data-testid="nav-assistant-manager"
>
-
+
{assistantManagerLabel}
@@ -488,8 +494,9 @@ const MainNav: React.FC = ({
size={BITFUN_ICON_SIZE.navigation}
className="bitfun-nav-panel__top-action-expand-icon-default"
/>
- = ({
data-testid="skill-tab"
>
-
+
{t('nav.items.skills')}
@@ -624,8 +631,8 @@ const MainNav: React.FC = ({
-
-
+
+
{workspaceMenuPortal}
diff --git a/src/web-ui/src/app/components/NavPanel/NavPanel.scss b/src/web-ui/src/app/components/NavPanel/NavPanel.scss
index 5b724fb5af..2fe9d7f9ac 100644
--- a/src/web-ui/src/app/components/NavPanel/NavPanel.scss
+++ b/src/web-ui/src/app/components/NavPanel/NavPanel.scss
@@ -554,20 +554,10 @@ $_section-header-height: 22px;
flex: 1 1 auto;
min-width: 0;
max-width: 100%;
- overflow-y: auto;
- overflow-x: hidden;
- overscroll-behavior: contain;
// No padding-top: separation from top-actions is only .bitfun-nav-panel__top-actions padding-bottom.
// Top padding here stacked with that and read as “extra” gap above the first section header.
padding: 0 0 $size-gap-3 0;
- &::-webkit-scrollbar { width: 3px; }
- &::-webkit-scrollbar-track { background: transparent; }
- &::-webkit-scrollbar-thumb {
- background: var(--bf-appearance-token-border-subtle);
- border-radius: 2px;
- }
-
// Synced with button ripple: fade list out, swap group, fade in
&.is-mode-switching {
animation: bitfun-nav-sections-mode-switch 0.48s linear;
@@ -1170,7 +1160,6 @@ $_section-header-height: 22px;
&__workspace-menu-workspaces {
max-height: 240px;
- overflow-y: auto;
overscroll-behavior: contain;
}
@@ -1966,11 +1955,17 @@ $_section-header-height: 22px;
z-index: 9999;
width: min(320px, calc(100vw - 16px));
max-height: calc(100vh - 16px);
- overflow-y: auto;
+ display: flex;
+ flex-direction: column;
box-sizing: border-box;
animation: bitfun-nav-popover-in 150ms cubic-bezier(0.23, 1, 0.32, 1) both;
}
+.bitfun-device-overview__scroll {
+ min-height: 0;
+ flex: 1 1 auto;
+}
+
.bitfun-device-overview__title {
margin: 0;
color: inherit;
@@ -3385,3 +3380,25 @@ $_section-header-height: 22px;
@include nav-font.nav-panel-text-meta;
}
}
+
+.bitfun-nav-panel__main-nav {
+ inline-size: 100%;
+ max-inline-size: none;
+ flex: 1 1 auto;
+ min-block-size: 0;
+ height: 100%;
+ background: transparent;
+
+ > [data-bf-part="header"] {
+ padding: 0;
+ }
+}
+
+.bitfun-nav-panel__main-nav-content {
+ padding: 0;
+ gap: 0;
+}
+
+.bitfun-nav-panel__sections-slot {
+ display: contents;
+}
diff --git a/src/web-ui/src/app/components/NavPanel/NavPanel.tsx b/src/web-ui/src/app/components/NavPanel/NavPanel.tsx
index 43968e7ac2..648e491cd9 100644
--- a/src/web-ui/src/app/components/NavPanel/NavPanel.tsx
+++ b/src/web-ui/src/app/components/NavPanel/NavPanel.tsx
@@ -116,7 +116,7 @@ const NavPanel: React.FC = ({ className = '' }) => {
].filter(Boolean).join(' ');
return (
-
-
+
);
};
diff --git a/src/web-ui/src/app/components/NavPanel/appearance.ts b/src/web-ui/src/app/components/NavPanel/appearance.ts
index cb88637afd..9544fdd786 100644
--- a/src/web-ui/src/app/components/NavPanel/appearance.ts
+++ b/src/web-ui/src/app/components/NavPanel/appearance.ts
@@ -17,7 +17,6 @@ export const navPanelAppearanceDescriptor: AppearanceSurfaceDescriptor = {
{ id: 'topAction', propertyProfile: 'control', visualRole: 'control' },
{ id: 'topActionUnseen', propertyProfile: 'paint', visualRole: 'decoration' },
{ id: 'extensionGroup', visualRole: 'content' },
- { id: 'sections', visualRole: 'content', continuityGroup: 'nav-panel' },
{ id: 'section', visualRole: 'content' },
{ id: 'stickySectionHeader', visualRole: 'toolbar', continuityGroup: 'nav-panel' },
{ id: 'sectionContent', visualRole: 'content' },
diff --git a/src/web-ui/src/app/components/NavPanel/components/AssistantSessionCreateMenu.tsx b/src/web-ui/src/app/components/NavPanel/components/AssistantSessionCreateMenu.tsx
index b35da39f61..a0fc646db7 100644
--- a/src/web-ui/src/app/components/NavPanel/components/AssistantSessionCreateMenu.tsx
+++ b/src/web-ui/src/app/components/NavPanel/components/AssistantSessionCreateMenu.tsx
@@ -1,7 +1,7 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
-import { ChevronDown, Plus } from 'lucide-react';
-import { Menu, MenuItem, Tooltip } from '@bitfun/ui';
+;
+import { Icon, Menu, MenuItem, Tooltip } from '@bitfun/ui';
import { getAppearanceOverlayHost } from '@/infrastructure/appearance/runtime/AppearanceOverlayHost';
import { useI18n } from '@/infrastructure/i18n/hooks/useI18n';
@@ -102,7 +102,7 @@ const AssistantSessionCreateMenu: React.FC = ({
}}
data-testid="nav-primary-assistant-session-add-btn"
>
-
+
@@ -116,7 +116,7 @@ const AssistantSessionCreateMenu: React.FC = ({
onClick={() => setMenuOpen(open => !open)}
data-testid="nav-assistant-session-menu-toggle"
>
-
+
@@ -138,7 +138,7 @@ const AssistantSessionCreateMenu: React.FC = ({
return (
}
+ leading={}
aria-label={t('nav.sessions.newAssistantSessionFor', { assistantName })}
onClick={() => {
closeMenu();
diff --git a/src/web-ui/src/app/components/NavPanel/components/BranchQuickSwitch.appearance.ts b/src/web-ui/src/app/components/NavPanel/components/BranchQuickSwitch.appearance.ts
index 53f5a93eab..a4b2cc5290 100644
--- a/src/web-ui/src/app/components/NavPanel/components/BranchQuickSwitch.appearance.ts
+++ b/src/web-ui/src/app/components/NavPanel/components/BranchQuickSwitch.appearance.ts
@@ -2,7 +2,7 @@ import type { AppearanceSurfaceDescriptor } from '@/infrastructure/appearance';
export const branchQuickSwitchAppearanceDescriptor: AppearanceSurfaceDescriptor = {
id: 'branch-quick-switch',
parts: [
- { id: 'root' }, { id: 'search' }, { id: 'input' }, { id: 'list' },
+ { id: 'root' }, { id: 'search' }, { id: 'input' },
{ id: 'loading' }, { id: 'empty' }, { id: 'item' }, { id: 'itemName' },
],
states: [
diff --git a/src/web-ui/src/app/components/NavPanel/components/BranchQuickSwitch.scss b/src/web-ui/src/app/components/NavPanel/components/BranchQuickSwitch.scss
index 3294ebdc9d..e93b877313 100644
--- a/src/web-ui/src/app/components/NavPanel/components/BranchQuickSwitch.scss
+++ b/src/web-ui/src/app/components/NavPanel/components/BranchQuickSwitch.scss
@@ -49,16 +49,6 @@
.branch-quick-switch__list {
max-height: 280px;
padding: $size-gap-1;
- overflow-y: auto;
- overflow-x: hidden;
-
- &::-webkit-scrollbar { width: 4px; }
- &::-webkit-scrollbar-track { background: transparent; }
- &::-webkit-scrollbar-thumb {
- background: var(--bf-appearance-token-color-text-muted);
- border-radius: 2px;
- opacity: 0.3;
- }
}
.branch-quick-switch__loading,
diff --git a/src/web-ui/src/app/components/NavPanel/components/BranchQuickSwitch.tsx b/src/web-ui/src/app/components/NavPanel/components/BranchQuickSwitch.tsx
index db71500c56..c06cc63b7b 100644
--- a/src/web-ui/src/app/components/NavPanel/components/BranchQuickSwitch.tsx
+++ b/src/web-ui/src/app/components/NavPanel/components/BranchQuickSwitch.tsx
@@ -5,7 +5,8 @@
*/
import React, { useState, useEffect, useMemo, useRef, useCallback } from 'react';
-import { GitBranch, Check, Loader2 } from 'lucide-react';
+import { Icon, ScrollArea } from '@bitfun/ui';
+import { Loader2 } from 'lucide-react';
import { type GitBranch as GitBranchType } from '../../../../infrastructure/api/service-api/GitAPI';
import { useI18n } from '@/infrastructure/i18n';
import { gitService, gitEventService } from '../../../../tools/git/services';
@@ -258,7 +259,7 @@ export const BranchQuickSwitch: React.FC = ({
data-bf-part="input"
/>
-
+
{isLoading ? (
@@ -287,14 +288,14 @@ export const BranchQuickSwitch: React.FC = ({
onClick={() => handleSwitchBranch(branch.name)}
onMouseEnter={() => setSelectedIndex(index)}
>
-
+
{branch.name}
- {branch.current && }
+ {branch.current && }
{switchingBranch === branch.name && }
))
)}
-
+
);
};
diff --git a/src/web-ui/src/app/components/NavPanel/components/DeviceStatusControl.tsx b/src/web-ui/src/app/components/NavPanel/components/DeviceStatusControl.tsx
index 0ce01cb907..b65c53fc6c 100644
--- a/src/web-ui/src/app/components/NavPanel/components/DeviceStatusControl.tsx
+++ b/src/web-ui/src/app/components/NavPanel/components/DeviceStatusControl.tsx
@@ -1,16 +1,7 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
-import { Button, Card, CardBody, CardFooter, CardHeader } from '@bitfun/ui';
+import { Button, Card, CardBody, CardFooter, CardHeader, Icon, ScrollArea } from '@bitfun/ui';
import { createPortal } from 'react-dom';
-import {
- Cloud,
- Link2,
- MessageCircle,
- Monitor,
- RefreshCw,
- Server,
- Smartphone,
- Undo2,
-} from 'lucide-react';
+import { Cloud, Monitor, RefreshCw, Server, Smartphone, Undo2 } from 'lucide-react';
import { useI18n } from '@/infrastructure/i18n/hooks/useI18n';
import { getAppearanceOverlayHost } from '@/infrastructure/appearance/runtime/AppearanceOverlayHost';
import { useAnchoredPopoverPosition } from '@/shared/utils/useAnchoredPopoverPosition';
@@ -33,13 +24,14 @@ interface DeviceStatusControlProps {
}
function DeviceIcon({ kind, size = 17 }: { kind: DeviceOverviewDeviceKind; size?: number }) {
+ const catalogSize = size <= 11 ? '2xs' : size <= 13 ? 'xs' : size <= 15 ? 'sm' : size <= 17 ? 'md' : 'lg';
switch (kind) {
case 'mobile':
return ;
case 'execution-host':
return ;
case 'message-app':
- return ;
+ return ;
default:
return ;
}
@@ -251,6 +243,7 @@ const DeviceStatusControl: React.FC = ({
contentAlign="center"
title={{t('deviceOverview.title')}
}
/>
+
{overview.mode === 'local' ? (
= ({
)}
+
}
+ leadingIcon={}
onClick={handleManageDevices}
data-testid="nav-device-status-manage"
>
diff --git a/src/web-ui/src/app/components/NavPanel/components/GithubStarButton.tsx b/src/web-ui/src/app/components/NavPanel/components/GithubStarButton.tsx
index a8a1718d56..6cb5e01587 100644
--- a/src/web-ui/src/app/components/NavPanel/components/GithubStarButton.tsx
+++ b/src/web-ui/src/app/components/NavPanel/components/GithubStarButton.tsx
@@ -1,10 +1,10 @@
import React, { useCallback, useState } from 'react';
-import { Star } from 'lucide-react';
+;
import { useI18n } from '@/infrastructure/i18n/hooks/useI18n';
import { systemAPI } from '@/infrastructure/api/service-api/SystemAPI';
import { createLogger } from '@/shared/utils/logger';
-import { Tooltip } from '@bitfun/ui';
+import { Icon, Tooltip } from '@bitfun/ui';
import {
GITHUB_STAR_URL,
isGithubStarCtaDismissed,
@@ -47,8 +47,8 @@ const GithubStarButton: React.FC = () => {
data-bf-part="footerButton"
>
-
-
+
+
{t('nav.githubStar.label')}
diff --git a/src/web-ui/src/app/components/NavPanel/components/PersistentFooterActions.tsx b/src/web-ui/src/app/components/NavPanel/components/PersistentFooterActions.tsx
index ac7001e25c..65cc239986 100644
--- a/src/web-ui/src/app/components/NavPanel/components/PersistentFooterActions.tsx
+++ b/src/web-ui/src/app/components/NavPanel/components/PersistentFooterActions.tsx
@@ -1,12 +1,9 @@
import React, { lazy, Suspense, useState, useCallback, useEffect, useRef } from 'react';
import { createPortal } from 'react-dom';
import {
- Settings,
- Info,
PictureInPicture2,
- Palette,
} from 'lucide-react';
-import { Menu, MenuItem, MenuSeparator, Modal, Tooltip } from '@bitfun/ui';
+import { Icon, Menu, MenuItem, MenuSeparator, Modal, Tooltip } from '@bitfun/ui';
import { PresenceBoundary } from '@/component-library';
import { useI18n } from '@/infrastructure/i18n/hooks/useI18n';
import { useSceneStore } from '../../../stores/sceneStore';
@@ -202,7 +199,7 @@ const PersistentFooterActions: React.FC = () => {
data-bf-part="settingsEntry"
data-bf-state={menuOpen ? 'open' : isSettingsActive ? 'active' : undefined}
>
-
+
@@ -232,7 +229,7 @@ const PersistentFooterActions: React.FC = () => {
}
+ leading={}
onClick={handleOpenThemeConfiguration}
data-testid="nav-settings-theme-item"
>
@@ -240,14 +237,14 @@ const PersistentFooterActions: React.FC = () => {
}
+ leading={}
onClick={handleOpenSettings}
data-testid="nav-settings-open-item"
>
{t('nav.settingsMenu.openSettings')}
}
+ leading={}
onClick={handleShowAbout}
data-testid="nav-settings-about-item"
>
diff --git a/src/web-ui/src/app/components/NavPanel/components/SectionHeader.tsx b/src/web-ui/src/app/components/NavPanel/components/SectionHeader.tsx
index 93893d0823..7e891e8c0f 100644
--- a/src/web-ui/src/app/components/NavPanel/components/SectionHeader.tsx
+++ b/src/web-ui/src/app/components/NavPanel/components/SectionHeader.tsx
@@ -3,7 +3,8 @@
*/
import React, { useCallback } from 'react';
-import { ChevronRight } from 'lucide-react';
+import { Icon } from '@bitfun/ui';
+;
interface SectionHeaderProps {
label: string;
@@ -62,7 +63,7 @@ const SectionHeader: React.FC = ({
{label}
{onSceneOpen ? (
-
+
) : null}
{actions ? (
diff --git a/src/web-ui/src/app/components/NavPanel/components/WorkspaceSessionFilterMenu.tsx b/src/web-ui/src/app/components/NavPanel/components/WorkspaceSessionFilterMenu.tsx
index c988ee1128..23ea436fa1 100644
--- a/src/web-ui/src/app/components/NavPanel/components/WorkspaceSessionFilterMenu.tsx
+++ b/src/web-ui/src/app/components/NavPanel/components/WorkspaceSessionFilterMenu.tsx
@@ -1,11 +1,11 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
-import { Check, ChevronRight, ListFilter } from 'lucide-react';
+import { ListFilter } from 'lucide-react';
import { useI18n } from '@/infrastructure/i18n';
import { getAppearanceOverlayHost } from '@/infrastructure/appearance/runtime/AppearanceOverlayHost';
import { flowChatStore } from '@/flow_chat/store/FlowChatStore';
-import { Tooltip } from '@bitfun/ui';
+import { Icon, Tooltip } from '@bitfun/ui';
import {
DEFAULT_WORKSPACE_SESSION_VIEW,
hasWorkspaceSessionFilters,
@@ -138,7 +138,7 @@ const WorkspaceSessionFilterMenu: React.FC = () => {
{active ? : null}
{value ? t(`nav.sessions.viewMenu.${submenu}.${value}`) : null}
-
+
);
@@ -188,7 +188,7 @@ const WorkspaceSessionFilterMenu: React.FC = () => {
onClick={view.toggleArchived}
>
{t('nav.sessions.viewMenu.archived')}
- {!view.filters.hideArchived ? : null}
+ {!view.filters.hideArchived ? : null}
{view.grouping === 'grouped' ? (
@@ -249,7 +249,7 @@ const WorkspaceSessionFilterMenu: React.FC = () => {
}
}}
>
- {selected ? : null}
+ {selected ? : null}
{t(`nav.sessions.viewMenu.${activeSubmenu}.${option}`)}
);
diff --git a/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.scss b/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.scss
index 04ec73c2d1..53f3c7d14b 100644
--- a/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.scss
+++ b/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.scss
@@ -468,7 +468,6 @@
max-width: min(220px, calc(100vw - 24px));
max-height: min(560px, calc(100vh - 16px));
overflow-x: hidden;
- overflow-y: auto;
padding: $size-gap-1 0;
z-index: 10000;
transform-origin: top left;
diff --git a/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.tsx b/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.tsx
index dca367836b..b5164e3bb0 100644
--- a/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.tsx
+++ b/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.tsx
@@ -6,9 +6,9 @@
*/
import React, { lazy, Suspense, useCallback, useEffect, useMemo, useRef, useState } from 'react';
-import { Button, IconButton, Input, Tooltip } from '@bitfun/ui';
+import { Button, Icon, IconButton, Input, ScrollArea, Tooltip } from '@bitfun/ui';
import { createPortal } from 'react-dom';
-import { Pencil, Trash2, Check, X, Bot, MoreHorizontal, Loader2, Archive, Clock3, Copy, FileDown, ChevronLeft, ChevronDown, ChevronUp } from 'lucide-react';
+import { Trash2, Bot, Loader2, Archive, FileDown, ChevronLeft, ChevronUp } from 'lucide-react';
import { PresenceBoundary } from '@/component-library';
import { useI18n } from '@/infrastructure/i18n';
import { flowChatStore } from '../../../../../flow_chat/store/FlowChatStore';
@@ -1761,7 +1761,7 @@ const SessionsSection: React.FC = ({
size="sm"
className="bitfun-nav-panel__inline-item-edit-btn confirm"
onClick={e => { e.stopPropagation(); handleConfirmEdit(); }}
- icon={}
+ icon={}
/>
@@ -1770,7 +1770,7 @@ const SessionsSection: React.FC = ({
size="sm"
className="bitfun-nav-panel__inline-item-edit-btn cancel"
onMouseDown={e => { e.preventDefault(); e.stopPropagation(); handleCancelEdit(); }}
- icon={}
+ icon={}
/>
@@ -1849,11 +1849,11 @@ const SessionsSection: React.FC = ({
data-testid="nav-session-menu-btn"
data-session-id={session.sessionId}
>
-
+
{openMenuSessionId === session.sessionId && sessionMenuPosition && createPortal(
- = ({
data-testid="nav-session-menu-rename"
data-session-id={session.sessionId}
>
-
+
{t('nav.sessions.rename')}
= ({
data-testid="nav-session-menu-copy-id"
data-session-id={session.sessionId}
>
-
+
{t('nav.sessions.copySessionId')}
= ({
data-testid="nav-session-menu-scheduled-jobs"
data-session-id={session.sessionId}
>
-
+
{t('nav.scheduledJobs.open')}
= ({
>
)}
-
,
+ ,
getAppearanceOverlayHost()
)}
>
@@ -2015,7 +2015,7 @@ const SessionsSection: React.FC = ({
+{topLevelSessions.length - sessionDisplayLimit}
-
+
)}
@@ -2045,7 +2045,7 @@ const SessionsSection: React.FC = ({
) : expandToggleLabels.remainingCount === null ? (
) : (
-
+
)}
)}
diff --git a/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceItem.tsx b/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceItem.tsx
index 85d8a014c3..5f5775eb19 100644
--- a/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceItem.tsx
+++ b/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceItem.tsx
@@ -1,7 +1,7 @@
-import { Button, Modal, ConfirmDialog, Menu, MenuItem, MenuSeparator, Tooltip } from '@bitfun/ui';
+import { Button, ConfirmDialog, Icon, Menu, MenuItem, MenuSeparator, Modal, Tooltip } from '@bitfun/ui';
import React, { lazy, Suspense, useCallback, useContext, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';
import { createPortal } from 'react-dom';
-import { Folder, FolderOpen, MoreHorizontal, FolderSearch, Plus, ChevronDown, Trash2, RotateCcw, Copy, FileText, Bot, Link2, ListChecks, Loader2, Clock3, ShieldCheck, Pencil, Network } from 'lucide-react';
+import { FolderOpen, FolderSearch, Trash2, RotateCcw, FileText, Bot, ListChecks, Loader2, ShieldCheck, Network } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { InputDialog, PresenceBoundary, BITFUN_ICON_SIZE, SessionGroupAssistantIcon, SessionGroupAssistantSelectedIcon, SessionGroupRemoteWorkspaceIcon, SessionGroupRemoteWorkspaceSelectedIcon, SessionGroupWorkspaceIcon, SessionGroupWorkspaceSelectedIcon } from '@/component-library';
@@ -856,7 +856,7 @@ const WorkspaceItem: React.FC = ({
)}
-
+
@@ -885,7 +885,7 @@ const WorkspaceItem: React.FC = ({
data-testid="nav-workspace-files-btn"
data-workspace-id={workspace.id}
>
-
+
@@ -898,7 +898,7 @@ const WorkspaceItem: React.FC = ({
data-testid="nav-workspace-menu-btn"
data-workspace-id={workspace.id}
>
-
+
@@ -911,13 +911,13 @@ const WorkspaceItem: React.FC = ({
data-workspace-id={workspace.id}
>
}
+ leading={}
onClick={() => { void handleCreateSession(); }}
data-testid="nav-workspace-menu-create-session"
>
{t('nav.workspaces.actions.newSession')}
- } onClick={handleOpenScheduledJobs}>
+ } onClick={handleOpenScheduledJobs}>
{t('nav.scheduledJobs.open')}
}
+ leading={}
onClick={() => {
setMenuOpen(false);
setRelatedPathsDialogOpen(true);
@@ -1402,7 +1402,7 @@ const WorkspaceItem: React.FC = ({
>
{t('nav.workspaces.actions.manageProjectPermissions')}
- } onClick={handleOpenScheduledJobs}>
+ } onClick={handleOpenScheduledJobs}>
{t('nav.scheduledJobs.open')}
{portForwardConnectionId ? (
@@ -1416,14 +1416,14 @@ const WorkspaceItem: React.FC = ({
) : null}
}
+ leading={}
onClick={handleRequestRename}
data-testid="nav-workspace-menu-rename"
>
{t('nav.workspaces.actions.rename')}
}
+ leading={}
onClick={() => { void handleCopyWorkspacePath(); }}
disabled={!workspace.rootPath}
data-testid="nav-workspace-menu-copy-path"
diff --git a/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceProjectPermissionsDialog.tsx b/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceProjectPermissionsDialog.tsx
index ac4041db4a..3a8e045300 100644
--- a/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceProjectPermissionsDialog.tsx
+++ b/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceProjectPermissionsDialog.tsx
@@ -1,6 +1,6 @@
-import { Button, IconButton, Input, Modal, Select, type SelectOption, Tooltip } from '@bitfun/ui';
+import { Button, Icon, IconButton, Input, Modal, Select, type SelectOption, Tooltip } from '@bitfun/ui';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
-import { ArrowDown, ArrowUp, Plus, Save, ShieldCheck, Trash2 } from 'lucide-react';
+import { ArrowDown, Save, ShieldCheck, Trash2 } from 'lucide-react';
import { confirmDanger } from '@/infrastructure/confirm-dialog';
import {
@@ -300,7 +300,7 @@ export const WorkspaceProjectPermissionsDialog: React.FC setDraftRules((rules) => [...rules, toDraftRule({ action: '', resource: '', effect: 'ask' })])}
- leadingIcon={}
+ leadingIcon={}
>
{t('projectPermissions.addRule')}
@@ -356,7 +356,7 @@ export const WorkspaceProjectPermissionsDialog: React.FC moveDraftRule(index, -1)}
- icon={}
+ icon={}
/>
diff --git a/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceRelatedPathsDialog.tsx b/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceRelatedPathsDialog.tsx
index 45064f7ae3..03d2713700 100644
--- a/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceRelatedPathsDialog.tsx
+++ b/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceRelatedPathsDialog.tsx
@@ -1,4 +1,4 @@
-import { Button, Input, Modal } from '@bitfun/ui';
+import { Button, Icon, Input, Modal } from '@bitfun/ui';
import React, { useEffect, useMemo, useState } from 'react';
import { Textarea } from '@/component-library';
import { useI18n } from '@/infrastructure/i18n';
@@ -12,7 +12,7 @@ import { sshApi } from '@/features/ssh-remote/sshApi';
import RemoteFileBrowser from '@/features/ssh-remote/RemoteFileBrowser';
import { createLogger } from '@/shared/utils/logger';
import { isRemoteWorkspace, type RelatedPath, type WorkspaceInfo } from '@/shared/types';
-import { FolderOpen, Link2, Plus, Trash2 } from 'lucide-react';
+import { FolderOpen, Trash2 } from 'lucide-react';
import './WorkspaceRelatedPathsDialog.scss';
const log = createLogger('WorkspaceRelatedPathsDialog');
@@ -244,7 +244,7 @@ export const WorkspaceRelatedPathsDialog: React.FC
-
+
@@ -413,7 +413,7 @@ export const WorkspaceRelatedPathsDialog: React.FC}
+ leadingIcon={}
onClick={handleAddDraft}
disabled={saving}
>
diff --git a/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceSessionBatchModal.scss b/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceSessionBatchModal.scss
index 49b5cf01d7..88b6507743 100644
--- a/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceSessionBatchModal.scss
+++ b/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceSessionBatchModal.scss
@@ -126,7 +126,6 @@
&__list {
flex: 1 1 auto;
min-height: 0;
- overflow: auto;
padding: 6px;
padding-bottom: 10px;
border: 1px solid color-mix(in srgb, var(--bf-appearance-token-border-subtle) 84%, transparent);
diff --git a/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceSessionBatchModal.tsx b/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceSessionBatchModal.tsx
index 944a42e1b1..25139712fe 100644
--- a/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceSessionBatchModal.tsx
+++ b/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceSessionBatchModal.tsx
@@ -1,6 +1,6 @@
-import { Button, Modal } from '@bitfun/ui';
+import { Button, Icon, Modal, ScrollArea } from '@bitfun/ui';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
-import { Archive, Bot, FolderKanban, Loader2, MessageSquare, Trash2 } from 'lucide-react';
+import { Archive, Bot, FolderKanban, Loader2, Trash2 } from 'lucide-react';
import { Checkbox} from '@/component-library';
import { useI18n } from '@/infrastructure/i18n';
import { sessionAPI } from '@/infrastructure/api/service-api/SessionAPI';
@@ -353,7 +353,7 @@ const WorkspaceSessionBatchModal: React.FC = ({
-
+
{isLoading ? (
@@ -374,7 +374,9 @@ const WorkspaceSessionBatchModal: React.FC = ({
sessions.map(({ metadata, displayAsChild }) => {
const isSelected = selectedSessionIds.has(metadata.sessionId);
const sessionPresentation = resolveSessionPresentation(metadata.agentType);
- const SessionIcon = sessionPresentation === 'assistant' ? Bot : MessageSquare;
+ const sessionGlyph = sessionPresentation === 'assistant'
+ ?
+ : ;
return (
-
+ {sessionGlyph}
@@ -421,7 +423,7 @@ const WorkspaceSessionBatchModal: React.FC = ({
);
})
)}
-
+
diff --git a/src/web-ui/src/app/components/NavPanel/unifiedSessionCreation.test.ts b/src/web-ui/src/app/components/NavPanel/unifiedSessionCreation.test.ts
index ff1b62dd7b..8363788033 100644
--- a/src/web-ui/src/app/components/NavPanel/unifiedSessionCreation.test.ts
+++ b/src/web-ui/src/app/components/NavPanel/unifiedSessionCreation.test.ts
@@ -131,7 +131,7 @@ describe('unified project session creation', () => {
expect(sectionsIndex).toBeGreaterThan(brandHeaderIndex);
expect(contentIndex).toBeGreaterThan(-1);
expect(persistentFooterIndex).toBeGreaterThan(contentIndex);
- expect(mainNav).toContain('ref={sectionsScrollRef}');
+ expect(mainNav).toContain('bodyRef={sectionsScrollRef}');
expect(mainNav).toContain('');
expect(stickyHeader).toContain('new IntersectionObserver');
expect(stickyHeader).toContain('root: scrollRoot');
diff --git a/src/web-ui/src/app/components/NewProjectDialog/NewProjectDialog.tsx b/src/web-ui/src/app/components/NewProjectDialog/NewProjectDialog.tsx
index b0242268c5..f22ce9bce5 100644
--- a/src/web-ui/src/app/components/NewProjectDialog/NewProjectDialog.tsx
+++ b/src/web-ui/src/app/components/NewProjectDialog/NewProjectDialog.tsx
@@ -2,16 +2,14 @@
* New Project Dialog Component
*/
-import { Button, Input, Modal } from '@bitfun/ui';
+import { Button, Icon, Input, Modal } from '@bitfun/ui';
import React, { useState, useCallback, useMemo } from 'react';
-import {
- FolderPlus,
- FolderOpen,
+import {
+ FolderPlus,
+ FolderOpen,
FileText,
FolderTree,
AlertCircle,
- Check,
- X
} from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { createLogger } from '@/shared/utils/logger';
@@ -203,7 +201,7 @@ export const NewProjectDialog: React.FC = ({
size="sm"
onClick={handleCancel}
disabled={isCreating}
- leadingIcon={}
+ leadingIcon={}
>
{t('newProject.cancel')}
@@ -220,7 +218,7 @@ export const NewProjectDialog: React.FC = ({
t('newProject.creating')
) : (
<>
-
+
{t('newProject.create')}
>
)}
diff --git a/src/web-ui/src/app/components/RemoteConnectDialog/AccountPanel.scss b/src/web-ui/src/app/components/RemoteConnectDialog/AccountPanel.scss
index 7e9d40bdd7..469898c1b8 100644
--- a/src/web-ui/src/app/components/RemoteConnectDialog/AccountPanel.scss
+++ b/src/web-ui/src/app/components/RemoteConnectDialog/AccountPanel.scss
@@ -11,7 +11,6 @@
&__scroll {
flex: 1;
min-height: 0;
- overflow-y: auto;
padding: 22px 40px 32px;
}
diff --git a/src/web-ui/src/app/components/RemoteConnectDialog/AccountPanel.tsx b/src/web-ui/src/app/components/RemoteConnectDialog/AccountPanel.tsx
index 6890be7d7b..dedd9ce0a2 100644
--- a/src/web-ui/src/app/components/RemoteConnectDialog/AccountPanel.tsx
+++ b/src/web-ui/src/app/components/RemoteConnectDialog/AccountPanel.tsx
@@ -15,7 +15,7 @@
* group), not an external README. See `src/features/relay-deploy/README.md`.
*/
-import { Button, Field, IconButton, Input } from '@bitfun/ui';
+import { Button, Field, Icon, IconButton, Input, ScrollArea } from '@bitfun/ui';
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
import { useI18n } from '@/infrastructure/i18n';
import { useCurrentWorkspace } from '@/infrastructure/contexts/WorkspaceContext';
@@ -25,8 +25,14 @@ import {
confirmWarning,
} from '@/infrastructure/confirm-dialog';
import {
- User, Lock, Server, LogIn, Monitor, CloudDownload, Upload,
- ChevronRight, RefreshCw, Eye, EyeOff, X, Rocket, Copy, Check,
+ Lock,
+ Server,
+ LogIn,
+ Monitor,
+ CloudDownload,
+ RefreshCw,
+ EyeOff,
+ Rocket,
} from 'lucide-react';
import { remoteConnectAPI } from '@/infrastructure/api/service-api/RemoteConnectAPI';
import type {
@@ -1073,7 +1079,7 @@ export const AccountPanel: React.FC = ({
)}
{view === 'login' && (
-
+
{t('accountLogin.loginValueProp')}
-
+
)}
{view === 'overwrite' && (
-
+
{t('accountLogin.cloudOverwriteWarning')}
@@ -1175,7 +1181,7 @@ export const AccountPanel: React.FC
= ({
onClick={handleUseLocalOverwrite}
disabled={loading}
>
-
+
{t('accountLogin.useLocalTitle')}
{t('accountLogin.useLocalDesc')}
@@ -1200,11 +1206,11 @@ export const AccountPanel: React.FC
= ({
{t('accountLogin.disagree')}
-
+
)}
{view === 'devices' && (
-
+
{accountRelayUrl && (
@@ -1213,7 +1219,7 @@ export const AccountPanel: React.FC
= ({
: }
+ icon={copiedServerUrl ? : }
onClick={handleCopyRelayUrl}
size="sm"
title={t('accountLogin.copyServerUrl')}
@@ -1343,12 +1349,12 @@ export const AccountPanel: React.FC = ({
- {isSelectable && }
+ {isSelectable && }
}
+ icon={}
onClick={(e) => { e.stopPropagation(); handleDeleteDevice(d.device_id, displayName); }}
size="sm"
tone="danger"
@@ -1386,7 +1392,7 @@ export const AccountPanel: React.FC = ({
{t('accountLogin.logout')}
-
+
)}
diff --git a/src/web-ui/src/app/components/RemoteConnectDialog/RemoteConnectDialog.contract.test.ts b/src/web-ui/src/app/components/RemoteConnectDialog/RemoteConnectDialog.contract.test.ts
index bf2175c600..4325df25ea 100644
--- a/src/web-ui/src/app/components/RemoteConnectDialog/RemoteConnectDialog.contract.test.ts
+++ b/src/web-ui/src/app/components/RemoteConnectDialog/RemoteConnectDialog.contract.test.ts
@@ -64,7 +64,7 @@ describe('Remote Connect safety contracts', () => {
expect(desktopGeometry).toContain('min-height: min(620px, calc(100vh - 40px))');
expect(desktopGeometry).toContain('max-height: min(620px, calc(100vh - 40px))');
expect(dialogStyleSource).toContain(".bitfun-remote-connect [data-bf-part='panel']");
- expect(dialogStyleSource).toContain('overflow-y: auto');
+ expect(dialogSource).toContain(' {
diff --git a/src/web-ui/src/app/components/RemoteConnectDialog/RemoteConnectDialog.scss b/src/web-ui/src/app/components/RemoteConnectDialog/RemoteConnectDialog.scss
index 9c0f37c494..d67c45bba2 100644
--- a/src/web-ui/src/app/components/RemoteConnectDialog/RemoteConnectDialog.scss
+++ b/src/web-ui/src/app/components/RemoteConnectDialog/RemoteConnectDialog.scss
@@ -136,7 +136,6 @@
min-height: 0;
flex-direction: column;
gap: 32px;
- overflow-y: auto;
padding: 64px 40px 36px;
}
@@ -373,7 +372,6 @@
.bitfun-remote-connect [data-bf-part='panel'] {
flex: 1;
min-height: 0;
- overflow-y: auto;
}
.bitfun-remote-connect [data-bf-part='panel'] > [role='tabpanel'] {
diff --git a/src/web-ui/src/app/components/RemoteConnectDialog/RemoteConnectDialog.tsx b/src/web-ui/src/app/components/RemoteConnectDialog/RemoteConnectDialog.tsx
index ba6061ea3b..14aac22524 100644
--- a/src/web-ui/src/app/components/RemoteConnectDialog/RemoteConnectDialog.tsx
+++ b/src/web-ui/src/app/components/RemoteConnectDialog/RemoteConnectDialog.tsx
@@ -14,32 +14,20 @@
import {
Button,
Field,
+ Icon,
IconButton,
Input,
+ Modal,
PageHeader,
+ ScrollArea,
StatusPill,
Switch,
TabGroup,
type TabGroupItem,
- Modal,
} from '@bitfun/ui';
import React, { useState, useEffect, useCallback, useRef } from 'react';
import { QRCodeSVG } from 'qrcode.react';
-import {
- ArrowLeft,
- Check,
- ChevronRight,
- Copy,
- Globe2,
- MessageCircle,
- MessageSquareText,
- MessagesSquare,
- Monitor,
- MonitorSmartphone,
- Radar,
- Send,
- Smartphone,
-} from 'lucide-react';
+import { MessageSquareText, MessagesSquare, Monitor, MonitorSmartphone, Radar, Send, Smartphone } from 'lucide-react';
import { useI18n } from '@/infrastructure/i18n';
import { getLocaleFallbackChain, type LocaleId } from '@/infrastructure/i18n/presets';
import { Select } from '@/component-library';
@@ -1035,7 +1023,7 @@ export const RemoteConnectDialog: React.FC = ({
: }
+ icon={qrCopied ? : }
onClick={() => void handleCopyPairingUrl()}
size="md"
variant="quiet"
@@ -1046,7 +1034,7 @@ export const RemoteConnectDialog: React.FC = ({
{t('remoteConnect.scanHint')}
-
+
{t('remoteConnect.mobileBrowserDescription')}
>
@@ -1558,12 +1546,12 @@ export const RemoteConnectDialog: React.FC
= ({
)}
{statusLabel}
-
+
);
const renderOverview = () => (
- = ({
})}
{renderOverviewAction({
view: 'bot',
- icon: ,
+ icon: ,
title: t('remoteConnect.chatAppsTitle'),
description: t('remoteConnect.chatAppsDescription'),
statusLabel: !hasWorkspace
@@ -1656,7 +1644,7 @@ export const RemoteConnectDialog: React.FC = ({
})}
-
+
);
const renderViewHeader = () => {
@@ -1680,7 +1668,7 @@ export const RemoteConnectDialog: React.FC
= ({
>
}
+ leadingIcon={}
onClick={() => handleViewChange('overview')}
size="sm"
variant="text"
@@ -1864,7 +1852,7 @@ export const RemoteConnectDialog: React.FC = ({
) : activeView === 'network' ? (
-
= ({
>
{renderNetworkContent()}
-
+
) : (
- = ({
>
{renderBotContent()}
-
+
)}
>
)}
diff --git a/src/web-ui/src/app/components/RemoteConnectDialog/RemoteConnectDisclaimer.scss b/src/web-ui/src/app/components/RemoteConnectDialog/RemoteConnectDisclaimer.scss
index f205a5f794..69488f20ea 100644
--- a/src/web-ui/src/app/components/RemoteConnectDialog/RemoteConnectDisclaimer.scss
+++ b/src/web-ui/src/app/components/RemoteConnectDialog/RemoteConnectDisclaimer.scss
@@ -62,10 +62,12 @@
border-bottom: 1px solid var(--bf-appearance-token-border-subtle);
}
- .bitfun-remote-disclaimer__list {
+ .bitfun-remote-disclaimer__list-scroll {
max-height: 240px;
+ }
+
+ .bitfun-remote-disclaimer__list {
padding: 10px 14px 12px 32px;
- overflow-y: auto;
}
}
diff --git a/src/web-ui/src/app/components/RemoteConnectDialog/RemoteConnectDisclaimer.tsx b/src/web-ui/src/app/components/RemoteConnectDialog/RemoteConnectDisclaimer.tsx
index 9ed7920333..027801f714 100644
--- a/src/web-ui/src/app/components/RemoteConnectDialog/RemoteConnectDisclaimer.tsx
+++ b/src/web-ui/src/app/components/RemoteConnectDialog/RemoteConnectDisclaimer.tsx
@@ -1,4 +1,4 @@
-import { Button, StatusPill } from '@bitfun/ui';
+import { Button, ScrollArea, StatusPill } from '@bitfun/ui';
import React from 'react';
import { useI18n } from '@/infrastructure/i18n';
import './RemoteConnectDisclaimer.scss';
@@ -39,20 +39,22 @@ export const RemoteConnectDisclaimerContent: React.FC
{t('remoteConnect.disclaimerFullDetails')}
-
- - {t('remoteConnect.disclaimerItemOpenSource')}
- - {t('remoteConnect.disclaimerItemDataUsage')}
- - {t('remoteConnect.disclaimerItemCredentials')}
- - {t('remoteConnect.disclaimerItemQrCode')}
- - {t('remoteConnect.disclaimerItemNgrok')}
- - {t('remoteConnect.disclaimerItemSelfHosted')}
- - {t('remoteConnect.disclaimerItemNetwork')}
- - {t('remoteConnect.disclaimerItemBot')}
- - {t('remoteConnect.disclaimerItemBotPersistence')}
- - {t('remoteConnect.disclaimerItemMobileBrowser')}
- - {t('remoteConnect.disclaimerItemCompliance')}
- - {t('remoteConnect.disclaimerItemLiability')}
-
+
+
+ - {t('remoteConnect.disclaimerItemOpenSource')}
+ - {t('remoteConnect.disclaimerItemDataUsage')}
+ - {t('remoteConnect.disclaimerItemCredentials')}
+ - {t('remoteConnect.disclaimerItemQrCode')}
+ - {t('remoteConnect.disclaimerItemNgrok')}
+ - {t('remoteConnect.disclaimerItemSelfHosted')}
+ - {t('remoteConnect.disclaimerItemNetwork')}
+ - {t('remoteConnect.disclaimerItemBot')}
+ - {t('remoteConnect.disclaimerItemBotPersistence')}
+ - {t('remoteConnect.disclaimerItemMobileBrowser')}
+ - {t('remoteConnect.disclaimerItemCompliance')}
+ - {t('remoteConnect.disclaimerItemLiability')}
+
+
diff --git a/src/web-ui/src/app/components/SceneBar/SceneBar.tsx b/src/web-ui/src/app/components/SceneBar/SceneBar.tsx
index 9f94d58d5b..108ddd2a3a 100644
--- a/src/web-ui/src/app/components/SceneBar/SceneBar.tsx
+++ b/src/web-ui/src/app/components/SceneBar/SceneBar.tsx
@@ -6,8 +6,8 @@
*/
import React, { useCallback } from 'react';
-import { ChevronLeft, ChevronRight, X } from 'lucide-react';
-import { TabGroup, type TabGroupItem } from '@bitfun/ui';
+import { ChevronLeft } from 'lucide-react';
+import { Icon, TabGroup, type TabGroupItem } from '@bitfun/ui';
import { useSceneTabNavigation } from './useSceneTabNavigation';
import { useSceneManager } from '../../hooks/useSceneManager';
import { useCurrentSessionTitle } from '../../hooks/useCurrentSessionTitle';
@@ -114,7 +114,7 @@ const SceneBar: React.FC = ({
}}
tabIndex={-1}
>
-
+
),
});
@@ -172,7 +172,7 @@ const SceneBar: React.FC = ({
data-bf-component="scene-bar"
data-bf-part="scrollNext"
>
-
+
)}
diff --git a/src/web-ui/src/app/components/TitleBar/NotificationButton.tsx b/src/web-ui/src/app/components/TitleBar/NotificationButton.tsx
index 9f5a7db79c..a0c3ac2bf0 100644
--- a/src/web-ui/src/app/components/TitleBar/NotificationButton.tsx
+++ b/src/web-ui/src/app/components/TitleBar/NotificationButton.tsx
@@ -6,8 +6,8 @@
*/
import React, { useRef, useEffect, useState } from 'react';
-import { Bell, BellDot, BellRing } from 'lucide-react';
-import { MenuItem, Tooltip } from '@bitfun/ui';
+import { BellDot, BellRing } from 'lucide-react';
+import { Icon, MenuItem, Tooltip } from '@bitfun/ui';
import { useI18n } from '@/infrastructure/i18n/hooks/useI18n';
import {
@@ -83,7 +83,7 @@ const NotificationButton: React.FC = ({
) : unreadCount > 0 ? (
) : (
-
+
)}
metadata={progressLabel ? (
{progressLabel}
@@ -183,14 +183,14 @@ const NotificationButton: React.FC = ({
) : (
-
+
)
) : (
unreadCount > 0
?
- :
+ :
)}
diff --git a/src/web-ui/src/app/components/panels/BranchSelectModal.scss b/src/web-ui/src/app/components/panels/BranchSelectModal.scss
index 11c96855b9..af7fcc7c63 100644
--- a/src/web-ui/src/app/components/panels/BranchSelectModal.scss
+++ b/src/web-ui/src/app/components/panels/BranchSelectModal.scss
@@ -131,7 +131,6 @@
flex: 1;
min-height: 0;
max-height: 240px;
- overflow-y: auto;
overflow-x: hidden;
border: 1px solid var(--bf-appearance-token-border-subtle);
border-radius: 6px;
diff --git a/src/web-ui/src/app/components/panels/BranchSelectModal.tsx b/src/web-ui/src/app/components/panels/BranchSelectModal.tsx
index b53b5b36d7..d1daad11ba 100644
--- a/src/web-ui/src/app/components/panels/BranchSelectModal.tsx
+++ b/src/web-ui/src/app/components/panels/BranchSelectModal.tsx
@@ -3,11 +3,11 @@
* Supports selecting existing branches or creating new branches
*/
-import { Button, IconButton, Input, Tooltip } from '@bitfun/ui';
+import { Button, Icon, IconButton, Input, ScrollArea, Tooltip } from '@bitfun/ui';
import React, { useState, useEffect, useMemo, useCallback, useRef } from 'react';
import { createPortal } from 'react-dom';
import { getAppearanceOverlayHost } from '@/infrastructure/appearance/runtime/AppearanceOverlayHost';
-import { GitBranch, Plus, X } from 'lucide-react';
+;
import { createLogger } from '@/shared/utils/logger';
import { isImeOwnedKeyboardEvent } from '@/shared/utils/ime';
import { Checkbox, PresenceBoundary, PRESENCE_BOUNDARY_MIN_EXIT_MS } from '@/component-library';
@@ -205,7 +205,7 @@ export const BranchSelectModal: React.FC = ({
className="branch-select-dialog__close"
data-bf-component="branch-select-modal"
data-bf-part="close"
- icon={}
+ icon={}
onClick={onClose}
size="md"
aria-label={tCommon('actions.close')}
@@ -236,7 +236,7 @@ export const BranchSelectModal: React.FC = ({
)}
-
+
{isLoading ? (
@@ -259,7 +259,7 @@ export const BranchSelectModal: React.FC
= ({
onClick={() => handleSelectBranch(searchTerm.trim(), true)}
onDoubleClick={() => handleDoubleClick(searchTerm.trim(), true)}
>
-
+
{t('branchSelect.createNewLabel')} {searchTerm.trim()}
@@ -283,7 +283,7 @@ export const BranchSelectModal: React.FC = ({
onClick={() => !isDisabled && handleSelectBranch(branch.name, false)}
onDoubleClick={() => !isDisabled && handleDoubleClick(branch.name, false)}
>
-
+
{branch.name}
@@ -306,7 +306,7 @@ export const BranchSelectModal: React.FC = ({
)}
>
)}
-
+
diff --git a/src/web-ui/src/app/components/panels/DiffFullscreenViewer.tsx b/src/web-ui/src/app/components/panels/DiffFullscreenViewer.tsx
index 0b6d1f732a..4ea901dd98 100644
--- a/src/web-ui/src/app/components/panels/DiffFullscreenViewer.tsx
+++ b/src/web-ui/src/app/components/panels/DiffFullscreenViewer.tsx
@@ -1,8 +1,8 @@
import React, { useEffect, useCallback, useRef } from 'react';
-import { Button, IconButton, Tooltip } from '@bitfun/ui';
+import { Button, Icon, IconButton, Tooltip } from '@bitfun/ui';
import { createPortal } from 'react-dom';
import { getAppearanceOverlayHost } from '@/infrastructure/appearance/runtime/AppearanceOverlayHost';
-import { X, CheckCircle, XCircle } from 'lucide-react';
+;
import { PresenceBoundary } from '@/component-library';
import { useI18n } from '@/infrastructure/i18n';
import { isImeOwnedKeyboardEvent } from '@/shared/utils/ime';
@@ -111,7 +111,7 @@ export const DiffFullscreenViewer: React.FC
= ({
}
+ leadingIcon={}
onClick={onAcceptFile}
disabled={retainedContent.loading}
>
@@ -123,7 +123,7 @@ export const DiffFullscreenViewer: React.FC = ({
}
+ leadingIcon={}
onClick={onRejectFile}
disabled={retainedContent.loading}
>
@@ -137,7 +137,7 @@ export const DiffFullscreenViewer: React.FC = ({
}
+ icon={}
onClick={onClose}
/>
diff --git a/src/web-ui/src/app/components/panels/FilesPanel.scss b/src/web-ui/src/app/components/panels/FilesPanel.scss
index 725ab8ad4d..1514854641 100644
--- a/src/web-ui/src/app/components/panels/FilesPanel.scss
+++ b/src/web-ui/src/app/components/panels/FilesPanel.scss
@@ -341,7 +341,6 @@
&__transfers {
flex-shrink: 0;
max-height: 200px;
- overflow-y: auto;
border-top: 1px solid var(--bf-appearance-token-border-base);
background: var(--bf-appearance-token-color-bg-secondary);
}
diff --git a/src/web-ui/src/app/components/panels/FilesPanel.tsx b/src/web-ui/src/app/components/panels/FilesPanel.tsx
index 810a6f2d2d..93732f41a6 100644
--- a/src/web-ui/src/app/components/panels/FilesPanel.tsx
+++ b/src/web-ui/src/app/components/panels/FilesPanel.tsx
@@ -3,10 +3,10 @@
* Displays the file explorer for the current workspace
*/
-import { Button, IconButton, SearchField, StatusPill, Tooltip } from '@bitfun/ui';
+import { Button, Icon, IconButton, SearchField, StatusPill, Tooltip, ScrollArea } from '@bitfun/ui';
import React, { useState, useCallback, useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
-import { Search as SearchIcon, CaseSensitive, Regex, WholeWord, List, Loader2 } from 'lucide-react';
+import { CaseSensitive, Regex, WholeWord, List, Loader2 } from 'lucide-react';
import {
FileExplorer,
getNewItemParentPath,
@@ -1056,7 +1056,7 @@ const FilesPanel: React.FC = ({
aria-label={viewMode === 'tree' ? t('actions.switchToSearch') : t('actions.switchToTree')}
size="sm"
onClick={handleToggleViewMode}
- icon={viewMode === 'tree' ? :
}
+ icon={viewMode === 'tree' ? :
}
/>
)
@@ -1077,7 +1077,7 @@ const FilesPanel: React.FC = ({
size="sm"
leadingIcon={isSearching
?
- : }
+ : }
/>
@@ -1218,7 +1218,7 @@ const FilesPanel: React.FC
= ({
!isSearching && !searchError && (
-
+
{t('search.noResults')}
@@ -1228,7 +1228,7 @@ const FilesPanel: React.FC = ({
) : (
-
+
{t('search.enterKeyword')}
@@ -1276,7 +1276,7 @@ const FilesPanel: React.FC = ({
{transfers.size > 0 && (
-
+
{Array.from(transfers.entries()).map(([id, tp]) => (
@@ -1346,7 +1346,7 @@ const FilesPanel: React.FC = ({
))}
-
+
)}
= memo(({
}
+ icon={}
onClick={handleCopy}
/>
@@ -1006,7 +1006,7 @@ const FlexiblePanel: React.FC = memo(({
}
+ icon={}
onClick={handleDownload}
/>
@@ -1018,7 +1018,7 @@ const FlexiblePanel: React.FC = memo(({
size="sm"
tone="danger"
aria-label={t('flexiblePanel.actions.close')}
- icon={}
+ icon={}
onClick={handleClose}
/>
diff --git a/src/web-ui/src/app/components/panels/base/utils.ts b/src/web-ui/src/app/components/panels/base/utils.ts
index 429b26c72b..356694ea95 100644
--- a/src/web-ui/src/app/components/panels/base/utils.ts
+++ b/src/web-ui/src/app/components/panels/base/utils.ts
@@ -4,25 +4,28 @@
*/
import React from 'react';
+import { Icon, type IconName, type IconSize } from '@bitfun/ui';
import {
Code,
FileText,
- GitBranch,
- Eye,
Edit3,
BookOpen,
- Settings,
ClipboardList,
- Image,
Network,
MessageSquareQuote,
- Globe,
Activity,
GitPullRequest,
- Terminal,
} from 'lucide-react';
import { PanelContentType, PanelContentConfig } from './types';
+function catalogPanelIcon(name: IconName): React.ComponentType<{ size?: string | number }> {
+ return function CatalogPanelIcon({ size }) {
+ const n = typeof size === 'number' ? size : 16;
+ const mapped: IconSize = n <= 11 ? '2xs' : n <= 13 ? 'xs' : n <= 15 ? 'sm' : n <= 17 ? 'md' : 'lg';
+ return React.createElement(Icon, { name, size: mapped });
+ };
+}
+
// Configuration mapping for each panel content type
export const PANEL_CONTENT_CONFIGS: Record = {
'empty': {
@@ -76,7 +79,7 @@ export const PANEL_CONTENT_CONFIGS: Record
'text-viewer': {
type: 'text-viewer',
displayName: 'Text Viewer',
- icon: Eye,
+ icon: catalogPanelIcon('eye'),
supportsCopy: true,
supportsDownload: true,
showHeader: true
@@ -92,7 +95,7 @@ export const PANEL_CONTENT_CONFIGS: Record
'image-viewer': {
type: 'image-viewer',
displayName: 'Image Viewer',
- icon: Image,
+ icon: catalogPanelIcon('image'),
supportsCopy: false,
supportsDownload: false,
showHeader: false
@@ -108,7 +111,7 @@ export const PANEL_CONTENT_CONFIGS: Record
'git-diff': {
type: 'git-diff',
displayName: 'Git Diff',
- icon: GitBranch,
+ icon: catalogPanelIcon('git'),
supportsCopy: true,
supportsDownload: true,
showHeader: false
@@ -116,7 +119,7 @@ export const PANEL_CONTENT_CONFIGS: Record
'git-settings': {
type: 'git-settings',
displayName: 'Git Settings',
- icon: GitBranch,
+ icon: catalogPanelIcon('git'),
supportsCopy: false,
supportsDownload: false,
showHeader: false
@@ -124,7 +127,7 @@ export const PANEL_CONTENT_CONFIGS: Record
'git-graph': {
type: 'git-graph',
displayName: 'Git Graph',
- icon: GitBranch,
+ icon: catalogPanelIcon('git'),
supportsCopy: false,
supportsDownload: false,
showHeader: false
@@ -132,7 +135,7 @@ export const PANEL_CONTENT_CONFIGS: Record
'git-branch-history': {
type: 'git-branch-history',
displayName: 'Git Branch History',
- icon: GitBranch,
+ icon: catalogPanelIcon('git'),
supportsCopy: false,
supportsDownload: false,
showHeader: false
@@ -172,7 +175,7 @@ export const PANEL_CONTENT_CONFIGS: Record
'design-tokens': {
type: 'design-tokens',
displayName: 'Design Tokens',
- icon: Settings,
+ icon: catalogPanelIcon('settings'),
supportsCopy: false,
supportsDownload: false,
showHeader: false
@@ -212,7 +215,7 @@ export const PANEL_CONTENT_CONFIGS: Record
'background-command-output': {
type: 'background-command-output',
displayName: 'Command Output',
- icon: Terminal,
+ icon: catalogPanelIcon('terminal'),
supportsCopy: false,
supportsDownload: false,
showHeader: false
@@ -260,7 +263,7 @@ export const PANEL_CONTENT_CONFIGS: Record
'browser': {
type: 'browser',
displayName: 'Browser',
- icon: Globe,
+ icon: catalogPanelIcon('browser'),
supportsCopy: false,
supportsDownload: false,
showHeader: false
diff --git a/src/web-ui/src/app/components/panels/content-canvas/anchor-zone/AnchorZone.tsx b/src/web-ui/src/app/components/panels/content-canvas/anchor-zone/AnchorZone.tsx
index 163279839f..81d635cd16 100644
--- a/src/web-ui/src/app/components/panels/content-canvas/anchor-zone/AnchorZone.tsx
+++ b/src/web-ui/src/app/components/panels/content-canvas/anchor-zone/AnchorZone.tsx
@@ -4,13 +4,13 @@
*/
import React, { useRef, useState, useCallback, useEffect } from 'react';
-import { ChevronDown, ChevronUp, X, Terminal, Maximize2, Minimize2 } from 'lucide-react';
+import { ChevronUp, Maximize2, Minimize2 } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import type { AnchorPosition } from '../types';
import { LAYOUT_CONFIG, clampAnchorSize } from '../types';
import './AnchorZone.scss';
-import { Tooltip } from '@bitfun/ui';
+import { Icon, Tooltip } from '@bitfun/ui';
export interface AnchorZoneProps {
/** Position */
@@ -123,7 +123,7 @@ export const AnchorZone: React.FC = ({
{/* Header */}
-
+
{t('canvas.terminal')}
@@ -137,7 +137,7 @@ export const AnchorZone: React.FC
= ({
{isCollapsed ? (
isBottom ? :
) : (
- isBottom ? :
+ isBottom ? :
)}
@@ -160,7 +160,7 @@ export const AnchorZone: React.FC = ({
className="canvas-anchor-zone__action-btn canvas-anchor-zone__close-btn"
onClick={onClose}
>
-
+
diff --git a/src/web-ui/src/app/components/panels/content-canvas/empty-state/EmptyState.tsx b/src/web-ui/src/app/components/panels/content-canvas/empty-state/EmptyState.tsx
index 7afcc1ab6c..61ca31d616 100644
--- a/src/web-ui/src/app/components/panels/content-canvas/empty-state/EmptyState.tsx
+++ b/src/web-ui/src/app/components/panels/content-canvas/empty-state/EmptyState.tsx
@@ -5,10 +5,10 @@
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
-import { X } from 'lucide-react';
+;
import './EmptyState.scss';
-import { Tooltip } from '@bitfun/ui';
+import { Icon, Tooltip } from '@bitfun/ui';
export interface EmptyStateProps {
onClose?: () => void;
@@ -33,7 +33,7 @@ export const EmptyState: React.FC = ({ onClose, children }) =>
className="canvas-empty-state__close-btn"
onClick={handleClose}
>
-
+
diff --git a/src/web-ui/src/app/components/panels/content-canvas/mission-control/MissionControl.scss b/src/web-ui/src/app/components/panels/content-canvas/mission-control/MissionControl.scss
index 22ef25124e..967cad0a05 100644
--- a/src/web-ui/src/app/components/panels/content-canvas/mission-control/MissionControl.scss
+++ b/src/web-ui/src/app/components/panels/content-canvas/mission-control/MissionControl.scss
@@ -155,7 +155,6 @@
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 10px;
padding: 8px 16px 16px;
- overflow-y: auto;
align-content: start;
&::-webkit-scrollbar {
diff --git a/src/web-ui/src/app/components/panels/content-canvas/mission-control/MissionControl.tsx b/src/web-ui/src/app/components/panels/content-canvas/mission-control/MissionControl.tsx
index c30815f40a..2aa0c9b39d 100644
--- a/src/web-ui/src/app/components/panels/content-canvas/mission-control/MissionControl.tsx
+++ b/src/web-ui/src/app/components/panels/content-canvas/mission-control/MissionControl.tsx
@@ -4,8 +4,8 @@
*/
import React, { useState, useEffect, useMemo, useCallback, useRef } from 'react';
-import { Button, IconButton, KeyHint } from '@bitfun/ui';
-import { X, Merge } from 'lucide-react';
+import { Button, Icon, IconButton, KeyHint, ScrollArea } from '@bitfun/ui';
+import { Merge } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { useDismissibleLayer } from '@/infrastructure/hooks/useDismissibleLayer';
import { ThumbnailCard } from './ThumbnailCard';
@@ -212,7 +212,7 @@ export const MissionControl: React.FC = ({
}
+ icon={}
onClick={onClose}
title={t('tooltip.close')}
/>
@@ -260,7 +260,7 @@ export const MissionControl: React.FC = ({
{/* Thumbnail grid - unified display */}
-
+
{filteredTabs.length > 0 ? (
filteredTabs.map(({ tab, groupId }) => (
= ({
)}
)}
-
+
{/* Footer hint */}
diff --git a/src/web-ui/src/app/components/panels/content-canvas/mission-control/SearchFilter.tsx b/src/web-ui/src/app/components/panels/content-canvas/mission-control/SearchFilter.tsx
index ffd8e32082..1040ebfc95 100644
--- a/src/web-ui/src/app/components/panels/content-canvas/mission-control/SearchFilter.tsx
+++ b/src/web-ui/src/app/components/panels/content-canvas/mission-control/SearchFilter.tsx
@@ -5,8 +5,8 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
-import { Search as SearchIcon } from 'lucide-react';
-import { SearchField } from '@bitfun/ui';
+;
+import { Icon, SearchField } from '@bitfun/ui';
import './SearchFilter.scss';
export interface SearchFilterProps {
@@ -41,7 +41,7 @@ export const SearchFilter: React.FC = ({
onValueChange={onChange}
placeholder={t('canvas.searchPlaceholder')}
aria-label={t('canvas.searchPlaceholder')}
- leadingIcon={}
+ leadingIcon={}
autoFocus={autoFocus}
size="md"
className="canvas-search-filter__search"
diff --git a/src/web-ui/src/app/components/panels/content-canvas/mission-control/ThumbnailCard.tsx b/src/web-ui/src/app/components/panels/content-canvas/mission-control/ThumbnailCard.tsx
index eca155b3b7..12ab27de7c 100644
--- a/src/web-ui/src/app/components/panels/content-canvas/mission-control/ThumbnailCard.tsx
+++ b/src/web-ui/src/app/components/panels/content-canvas/mission-control/ThumbnailCard.tsx
@@ -4,13 +4,13 @@
*/
import React, { useCallback, useMemo } from 'react';
-import { X, Pin, FileCode, FileText, Image, Terminal, GitBranch } from 'lucide-react';
+import { FileCode, FileText } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import type { CanvasTab, EditorGroupId } from '../types';
import { isFileViewerType } from '../types';
import './ThumbnailCard.scss';
-import { Tooltip } from '@bitfun/ui';
+import { Icon, Tooltip } from '@bitfun/ui';
export interface ThumbnailCardProps {
/** Tab data */
@@ -42,13 +42,13 @@ const getContentIcon = (type: string) => {
return ;
}
if (type.includes('image')) {
- return ;
+ return ;
}
if (type === 'terminal') {
- return ;
+ return ;
}
if (type.includes('git')) {
- return ;
+ return ;
}
return ;
};
@@ -161,7 +161,7 @@ export const ThumbnailCard: React.FC = ({
{getContentIcon(tab.content.type)}
- {tab.state === 'pinned' &&
}
+ {tab.state === 'pinned' &&
}
{titleWithDeleted}
@@ -175,7 +175,7 @@ export const ThumbnailCard: React.FC
= ({
className={`canvas-thumbnail-card__action-btn ${tab.state === 'pinned' ? 'is-active' : ''}`}
onClick={handlePin}
>
-
+
@@ -185,7 +185,7 @@ export const ThumbnailCard: React.FC = ({
className="canvas-thumbnail-card__action-btn canvas-thumbnail-card__close-btn"
onClick={handleClose}
>
-
+
diff --git a/src/web-ui/src/app/components/panels/content-canvas/quick-look/QuickLook.tsx b/src/web-ui/src/app/components/panels/content-canvas/quick-look/QuickLook.tsx
index a5a51b06ea..808a0fb7bf 100644
--- a/src/web-ui/src/app/components/panels/content-canvas/quick-look/QuickLook.tsx
+++ b/src/web-ui/src/app/components/panels/content-canvas/quick-look/QuickLook.tsx
@@ -16,14 +16,13 @@
import React, { useEffect, useRef, useCallback, useState } from 'react';
import { createPortal } from 'react-dom';
import { getAppearanceOverlayHost } from '@/infrastructure/appearance/runtime/AppearanceOverlayHost';
-import { X, Pin, ExternalLink } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { useDismissibleLayer } from '@/infrastructure/hooks/useDismissibleLayer';
import FlexiblePanel from '../../base/FlexiblePanel';
import type { PanelContent } from '../types';
import './QuickLook.scss';
-import { Tooltip } from '@bitfun/ui';
+import { Icon, Tooltip } from '@bitfun/ui';
export interface QuickLookProps {
/** Whether visible */
@@ -168,7 +167,7 @@ export const QuickLook: React.FC = ({
{content.data?.filePath && (
-
+
)}
@@ -180,7 +179,7 @@ export const QuickLook: React.FC = ({
className="canvas-quick-look__action-btn canvas-quick-look__pin-btn"
onClick={onPin}
>
-
+
@@ -189,7 +188,7 @@ export const QuickLook: React.FC = ({
className="canvas-quick-look__action-btn canvas-quick-look__close-btn"
onClick={onClose}
>
-
+
diff --git a/src/web-ui/src/app/components/panels/content-canvas/tab-bar/Tab.tsx b/src/web-ui/src/app/components/panels/content-canvas/tab-bar/Tab.tsx
index b873fc7541..a69b3f6fef 100644
--- a/src/web-ui/src/app/components/panels/content-canvas/tab-bar/Tab.tsx
+++ b/src/web-ui/src/app/components/panels/content-canvas/tab-bar/Tab.tsx
@@ -4,7 +4,7 @@
*/
import React, { useCallback } from 'react';
-import { X, Pin, Split } from 'lucide-react';
+import { Split } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { commandExecutor } from '@/shared/context-menu-system/commands/CommandExecutor';
@@ -17,7 +17,7 @@ import { hasNonFileUriScheme } from '@/shared/utils/pathUtils';
import { isHtmlFilePath } from '@/shared/utils/htmlFilePreview';
import type { CanvasTab, EditorGroupId, TabState } from '../types';
import './Tab.scss';
-import { Tooltip } from '@bitfun/ui';
+import { Icon, Tooltip } from '@bitfun/ui';
export interface TabProps {
/** Tab data */
tab: CanvasTab;
@@ -348,7 +348,7 @@ export const Tab: React.FC = ({
onClick={isPinned ? handlePinClick : handleCloseClick}
tabIndex={-1}
>
- {isPinned ? : }
+ {isPinned ? : }
diff --git a/src/web-ui/src/app/components/panels/content-canvas/tab-bar/TabBar.tsx b/src/web-ui/src/app/components/panels/content-canvas/tab-bar/TabBar.tsx
index 0dc43000c0..33ac071d8a 100644
--- a/src/web-ui/src/app/components/panels/content-canvas/tab-bar/TabBar.tsx
+++ b/src/web-ui/src/app/components/panels/content-canvas/tab-bar/TabBar.tsx
@@ -4,7 +4,7 @@
*/
import React, { useState, useRef, useEffect, useCallback, useMemo, useLayoutEffect } from 'react';
-import { X } from 'lucide-react';
+;
import { useTranslation } from 'react-i18next';
import { Tab } from './Tab';
@@ -12,7 +12,7 @@ import { TabOverflowMenu } from './TabOverflowMenu';
import type { CanvasTab, EditorGroupId, TabDragPayload } from '../types';
import { createLogger } from '@/shared/utils/logger';
import './TabBar.scss';
-import { Tooltip } from '@bitfun/ui';
+import { Icon, Tooltip } from '@bitfun/ui';
const log = createLogger('TabBar');
const TAB_REORDER_DURATION_MS = 160;
@@ -445,7 +445,7 @@ export const TabBar: React.FC = ({
await onCloseAllTabs();
}}
>
-
+
)}
diff --git a/src/web-ui/src/app/components/panels/content-canvas/tab-bar/TabOverflowMenu.appearance.ts b/src/web-ui/src/app/components/panels/content-canvas/tab-bar/TabOverflowMenu.appearance.ts
index ea68e98603..5404153a55 100644
--- a/src/web-ui/src/app/components/panels/content-canvas/tab-bar/TabOverflowMenu.appearance.ts
+++ b/src/web-ui/src/app/components/panels/content-canvas/tab-bar/TabOverflowMenu.appearance.ts
@@ -3,7 +3,7 @@ export const canvasTabOverflowAppearanceDescriptor: AppearanceSurfaceDescriptor
id: 'canvas-tab-overflow',
parts: [
{ id: 'root' }, { id: 'trigger' }, { id: 'badge' }, { id: 'menu' },
- { id: 'missionControl' }, { id: 'divider' }, { id: 'list' },
+ { id: 'missionControl' }, { id: 'divider' },
{ id: 'item' }, { id: 'itemTitle' }, { id: 'itemClose' },
],
states: [
diff --git a/src/web-ui/src/app/components/panels/content-canvas/tab-bar/TabOverflowMenu.scss b/src/web-ui/src/app/components/panels/content-canvas/tab-bar/TabOverflowMenu.scss
index 66305e7a69..b0249792ef 100644
--- a/src/web-ui/src/app/components/panels/content-canvas/tab-bar/TabOverflowMenu.scss
+++ b/src/web-ui/src/app/components/panels/content-canvas/tab-bar/TabOverflowMenu.scss
@@ -139,20 +139,6 @@
&__list {
padding: 4px 0;
max-height: 280px;
- overflow-y: auto;
-
- &::-webkit-scrollbar {
- width: 5px;
- }
-
- &::-webkit-scrollbar-track {
- background: transparent;
- }
-
- &::-webkit-scrollbar-thumb {
- background: var(--bf-appearance-token-scrollbar-thumb);
- border-radius: 3px;
- }
}
// Tab item
diff --git a/src/web-ui/src/app/components/panels/content-canvas/tab-bar/TabOverflowMenu.tsx b/src/web-ui/src/app/components/panels/content-canvas/tab-bar/TabOverflowMenu.tsx
index 10e0118cc1..78b6ec7f28 100644
--- a/src/web-ui/src/app/components/panels/content-canvas/tab-bar/TabOverflowMenu.tsx
+++ b/src/web-ui/src/app/components/panels/content-canvas/tab-bar/TabOverflowMenu.tsx
@@ -8,9 +8,9 @@
import React, { useState, useRef, useEffect, useCallback } from 'react';
import { createPortal } from 'react-dom';
-import { KeyHint, Tooltip } from '@bitfun/ui';
+import { Icon, KeyHint, ScrollArea, Tooltip } from '@bitfun/ui';
import { getAppearanceOverlayHost } from '@/infrastructure/appearance/runtime/AppearanceOverlayHost';
-import { LayoutGrid, ChevronDown, X } from 'lucide-react';
+import { LayoutGrid } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import type { CanvasTab } from '../types';
@@ -176,7 +176,7 @@ export const TabOverflowMenu: React.FC = ({
{hasMissionControl ? (
) : (
-
+
)}
{hasOverflow && (
@@ -218,7 +218,7 @@ export const TabOverflowMenu: React.FC = ({
)}
{/* Overflow tab list */}
-
+
{overflowTabs.map((tab) => {
const deletedSuffix = tab.fileDeletedFromDisk ? ` - ${t('tabs.fileDeleted')}` : '';
const titleWithDeleted = `${tab.title}${deletedSuffix}`;
@@ -252,12 +252,12 @@ export const TabOverflowMenu: React.FC = ({
className="canvas-tab-overflow-menu__item-close"
onClick={(e) => handleCloseClick(e, tab.id)}
>
-
+
);
})}
-
+
,
getAppearanceOverlayHost()
)}
diff --git a/src/web-ui/src/app/components/panels/review-platform/ReviewPlatformPanel.scss b/src/web-ui/src/app/components/panels/review-platform/ReviewPlatformPanel.scss
index 7f8901d1b8..37eaa4b113 100644
--- a/src/web-ui/src/app/components/panels/review-platform/ReviewPlatformPanel.scss
+++ b/src/web-ui/src/app/components/panels/review-platform/ReviewPlatformPanel.scss
@@ -367,7 +367,6 @@
&__list-scroll {
min-height: 0;
flex: 1;
- overflow-y: auto;
padding: 8px;
}
@@ -740,7 +739,6 @@
box-sizing: border-box;
flex: 1 1 auto;
min-height: 0;
- overflow-y: auto;
padding: 12px 14px;
}
diff --git a/src/web-ui/src/app/components/panels/review-platform/ReviewPlatformPanel.tsx b/src/web-ui/src/app/components/panels/review-platform/ReviewPlatformPanel.tsx
index e028d04f61..09a61566f6 100644
--- a/src/web-ui/src/app/components/panels/review-platform/ReviewPlatformPanel.tsx
+++ b/src/web-ui/src/app/components/panels/review-platform/ReviewPlatformPanel.tsx
@@ -1,31 +1,6 @@
-import { Button, Field, IconButton, Input as DesignInput, Modal, TabGroup, Tooltip } from '@bitfun/ui';
+import { Button, Field, Icon, IconButton, Input as DesignInput, Modal, ScrollArea, TabGroup, Tooltip } from '@bitfun/ui';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
-import {
- ChevronDown,
- ChevronLeft,
- ChevronRight,
- CheckCircle2,
- CircleDot,
- Clock3,
- Code2,
- Copy,
- ExternalLink,
- GitCommitHorizontal,
- GitPullRequest,
- GitPullRequestClosed,
- KeyRound,
- Link2,
- Loader2,
- MessageSquareText,
- RefreshCw,
- Search,
- ShieldCheck,
- Sparkles,
- Trash2,
- Terminal,
- UserRound,
- XCircle,
-} from 'lucide-react';
+import { ChevronLeft, CircleDot, Code2, GitCommitHorizontal, GitPullRequest, GitPullRequestClosed, KeyRound, Loader2, MessageSquareText, RefreshCw, ShieldCheck, Trash2 } from 'lucide-react';
import { MarkdownRenderer, Select, type SelectOption } from '@/component-library';
import { reviewPlatformAPI, systemAPI, type ReviewPlatformAccount, type ReviewPlatformAuthChallenge, type ReviewPlatformCiItem, type ReviewPlatformCiLog, type ReviewPlatformCommit, type ReviewPlatformDetailSection, type ReviewPlatformFile, type ReviewPlatformPagination, type ReviewPlatformPullRequest, type ReviewPlatformPullRequestDetail, type ReviewPlatformPullRequestDetailPage, type ReviewPlatformRemote, type ReviewPlatformRepositoryRef, type ReviewPlatformThread, type ReviewPlatformWorkspaceSnapshot } from '@/infrastructure/api';
import { createLogger } from '@/shared/utils/logger';
@@ -1311,7 +1286,7 @@ export const ReviewPlatformPanel: React.FC = ({
size="sm"
disabled={!page.hasNext && page.pageIndex >= page.totalPages - 1}
onClick={() => onPageChange(page.pageIndex + 1)}
- icon={}
+ icon={}
/>
@@ -1933,8 +1908,8 @@ export const ReviewPlatformPanel: React.FC = ({
{summary.open} open on page
{!isGithubUserList && {summary.merged} merged on page}
- {summary.reviewRequired} review on page
- {remoteStatus}
+ {summary.reviewRequired} review on page
+ {remoteStatus}
{loadingLabel && (
{loading && }
@@ -1955,13 +1930,13 @@ export const ReviewPlatformPanel: React.FC = ({
value={query}
onChange={event => setQuery(event.target.value)}
placeholder="Search pull requests"
- leading={}
+ leading={}
trailing={query ? setQuery('')}
- icon={}
+ icon={}
/> : undefined}
size="sm"
/>
@@ -1981,13 +1956,13 @@ export const ReviewPlatformPanel: React.FC = ({
)}
-
+
{loading && (
Loading pull requests...
)}
{error && (
-
+
{error}
void loadSnapshot(listRemoteId, { force: true, page: currentPageIndex + 1 })}>
Retry
@@ -2043,7 +2018,7 @@ export const ReviewPlatformPanel: React.FC = ({
);
})()
))}
-
+
{!loading && !error && (totalPages > 1 || pagination.hasNext) && (
@@ -2066,7 +2041,7 @@ export const ReviewPlatformPanel: React.FC = ({
size="sm"
disabled={!pagination.hasNext && currentPageIndex >= totalPages - 1}
onClick={() => handlePageChange(currentPageIndex + 1)}
- icon={}
+ icon={}
/>
@@ -2090,7 +2065,7 @@ export const ReviewPlatformPanel: React.FC
= ({
{!selectedPr && detailOnly && !loading && !detailLoading && !authChallenge && (detailError || error) && (
-
+
{detailError || error}
@@ -2126,7 +2101,7 @@ export const ReviewPlatformPanel: React.FC = ({
size="sm"
variant="outline"
onClick={handleOpenExternal}
- leadingIcon={}
+ leadingIcon={}
>
Open in browser
@@ -2156,7 +2131,7 @@ export const ReviewPlatformPanel: React.FC = ({
#{selectedPr.number}
- {formatAbsoluteTime(selectedPr.updatedAt) || formatRelativeTime(selectedPr.updatedAt)}
+ {formatAbsoluteTime(selectedPr.updatedAt) || formatRelativeTime(selectedPr.updatedAt)}
@@ -2175,14 +2150,14 @@ export const ReviewPlatformPanel: React.FC
= ({
latestCurrentReview?.lifecycle === 'running'
}
loading={reviewLaunching}
- leadingIcon={}
+ leadingIcon={}
>
{latestCurrentReview?.lifecycle === 'running' ? 'Review running' : 'Review'}
- }>
+ }>
Open
@@ -2217,7 +2192,7 @@ export const ReviewPlatformPanel: React.FC = ({
Branches
{displayPr?.sourceBranch ?? selectedPr.sourceBranch}
-
+
{displayPr?.targetBranch ?? selectedPr.targetBranch}
{resolvedChangedFileCount(displayPr, selectedPr) ?? '—'} files
+{displayPr?.additions ?? selectedPr.additions}
@@ -2225,7 +2200,7 @@ export const ReviewPlatformPanel: React.FC = ({
-
Author
+
Author
{displayPr?.author ?? selectedPr.author}
@@ -2235,14 +2210,14 @@ export const ReviewPlatformPanel: React.FC
= ({
{commentsText}
-
Checks
+
Checks
{checksStatusText}
{displayPr && displayPr.checks.total > 0 && {checksText}}
-
BitFun Review
+
BitFun Review
{reviewStatusText}
{(latestCurrentReview || latestStaleReview || latestUnknownReview) && (
@@ -2267,7 +2242,7 @@ export const ReviewPlatformPanel: React.FC = ({
/>
{activeTab === 'overview' && (
-
+
Description
@@ -2278,7 +2253,7 @@ export const ReviewPlatformPanel: React.FC = ({
{detailError ? (
-
+
{detailError}
Retry
@@ -2337,7 +2312,7 @@ export const ReviewPlatformPanel: React.FC = ({
onClick={() => toggleCiExpanded(item)}
disabled={ciLogLoading}
aria-busy={ciLogLoading}
- icon={isCiExpanded ? : }
+ icon={isCiExpanded ? : }
/>
)}
@@ -2358,7 +2333,7 @@ export const ReviewPlatformPanel: React.FC = ({
className="review-platform__icon-button review-platform__ci-action"
size="sm"
onClick={() => void handleOpenCiUrl(item.webUrl)}
- icon={}
+ icon={}
/>
)}
@@ -2374,7 +2349,7 @@ export const ReviewPlatformPanel: React.FC = ({
{ciLogLoading && renderDetailLoading('Loading check details...')}
{!ciLogLoading && ciLogError && logAvailable && (
-
+
{ciLogError}
void loadCiLog(item)}>Retry
@@ -2445,14 +2420,14 @@ export const ReviewPlatformPanel: React.FC = ({
{!detailLoading && detail && reviewThreads.length === 0 && No comments yet.
}
{renderDetailPagination('Comments', reviewPage, reviewThreads.length, setReviewPageIndex)}
-
+
)}
{activeTab === 'changes' && (
-
+
{detailError && (
-
+
{detailError}
Retry
@@ -2475,7 +2450,7 @@ export const ReviewPlatformPanel: React.FC = ({
onClick={() => toggleFileExpanded(key)}
>
- {isExpanded ? : }
+ {isExpanded ? : }
{file.status}
@@ -2515,11 +2490,11 @@ export const ReviewPlatformPanel: React.FC = ({
)}
{renderDetailPagination('Files', changePage, changedFiles.length, setChangePageIndex)}
-
+
)}
{activeTab === 'commits' && (
-
+
Commits
}>
@@ -2529,7 +2504,7 @@ export const ReviewPlatformPanel: React.FC = ({
{detailError && (
-
+
{detailError}
Retry
@@ -2551,7 +2526,7 @@ export const ReviewPlatformPanel: React.FC = ({
No commits were returned by this provider.
)}
{renderDetailPagination('Commits', commitPage, commits.length, setCommitPageIndex)}
-
+
)}
>
@@ -2623,7 +2598,7 @@ export const ReviewPlatformPanel: React.FC = ({
variant="outline"
disabled={authSaving}
onClick={() => void handleCopyGithubAuthCommand()}
- leadingIcon={}
+ leadingIcon={}
>
Copy
@@ -2634,7 +2609,7 @@ export const ReviewPlatformPanel: React.FC = ({
variant="fill"
loading={authSaving}
onClick={() => void handleOpenGithubAuthTerminal()}
- leadingIcon={}
+ leadingIcon={}
>
Open terminal
diff --git a/src/web-ui/src/app/components/scheduled-jobs/DateTimePickerPopover.tsx b/src/web-ui/src/app/components/scheduled-jobs/DateTimePickerPopover.tsx
index cc6385fd15..7f2adfab1c 100644
--- a/src/web-ui/src/app/components/scheduled-jobs/DateTimePickerPopover.tsx
+++ b/src/web-ui/src/app/components/scheduled-jobs/DateTimePickerPopover.tsx
@@ -11,10 +11,10 @@
* and the text field already accepts it.
*/
-import { Button, IconButton } from '@bitfun/ui';
+import { Button, Icon, IconButton } from '@bitfun/ui';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
-import { ChevronLeft, ChevronRight } from 'lucide-react';
+import { ChevronLeft } from 'lucide-react';
import { getAppearanceOverlayHost } from '@/infrastructure/appearance/runtime/AppearanceOverlayHost';
import { useI18n } from '@/infrastructure/i18n';
import { computeFixedPopoverPosition } from '@/shared/utils/fixedPopoverViewport';
@@ -156,7 +156,7 @@ const DateTimePickerPopover: React.FC = ({
type="button"
size="sm"
aria-label={t('dateTimeField.nextMonth')}
- icon={}
+ icon={}
onClick={() => shiftMonth(1)}
/>
diff --git a/src/web-ui/src/app/global-search/GlobalSearchRoot.scss b/src/web-ui/src/app/global-search/GlobalSearchRoot.scss
index fcce0ea51b..1a684e6615 100644
--- a/src/web-ui/src/app/global-search/GlobalSearchRoot.scss
+++ b/src/web-ui/src/app/global-search/GlobalSearchRoot.scss
@@ -175,7 +175,6 @@
&__results {
flex: 1;
min-height: 220px;
- overflow-y: auto;
overscroll-behavior: contain;
padding: 6px 18px 10px;
scrollbar-gutter: stable;
diff --git a/src/web-ui/src/app/global-search/GlobalSearchRoot.tsx b/src/web-ui/src/app/global-search/GlobalSearchRoot.tsx
index 3c48378ada..9bbbe99fa6 100644
--- a/src/web-ui/src/app/global-search/GlobalSearchRoot.tsx
+++ b/src/web-ui/src/app/global-search/GlobalSearchRoot.tsx
@@ -8,33 +8,21 @@ import React, {
useSyncExternalStore,
} from 'react';
import { isImeOwnedKeyboardEvent } from '@/shared/utils/ime';
-import { ActionCard, Button, KeyHint, Modal, SearchField } from '@bitfun/ui';
+import { ActionCard, Button, Icon, KeyHint, Modal, SearchField, type IconName, type IconSize, ScrollArea } from '@bitfun/ui';
import {
BarChart3,
Blocks,
Bot,
CheckSquare2,
ChevronLeft,
- ChevronRight,
FileText,
- Folder,
- Globe,
Keyboard,
- MessageCircle,
MessageSquareText,
MessagesSquare,
- MoreHorizontal,
Network,
- Pin,
- Plus,
- Puzzle,
- Search,
- Settings,
SlidersHorizontal,
SquareTerminal,
- UserRound,
Users,
- X,
type LucideIcon,
} from 'lucide-react';
import { useShortcut } from '@/infrastructure/hooks/useShortcut';
@@ -72,6 +60,14 @@ import {
} from './types';
import './GlobalSearchRoot.scss';
+function catalogLucide(name: IconName): LucideIcon {
+ return function CatalogLucide({ size }: { size?: number | string }) {
+ const n = typeof size === 'number' ? size : 20;
+ const mapped: IconSize = n <= 11 ? '2xs' : n <= 13 ? 'xs' : n <= 15 ? 'sm' : n <= 17 ? 'md' : 'lg';
+ return ;
+ } as LucideIcon;
+}
+
const log = createLogger('GlobalSearch');
const SEARCH_DEBOUNCE_MS = 90;
const SEARCH_LIMIT_PER_GROUP = 20;
@@ -86,18 +82,18 @@ const EMPTY_SNAPSHOT: GlobalSearchSnapshot = {
};
const ACTION_ICONS: Record = {
- 'message-circle': MessageCircle,
- folder: Folder,
- plus: Plus,
- globe: Globe,
+ 'message-circle': catalogLucide('side-chat'),
+ folder: catalogLucide('folder'),
+ plus: catalogLucide('plus'),
+ globe: catalogLucide('browser'),
terminal: SquareTerminal,
files: FileText,
users: Users,
- puzzle: Puzzle,
+ puzzle: catalogLucide('extension'),
blocks: Blocks,
'check-square': CheckSquare2,
chart: BarChart3,
- settings: Settings,
+ settings: catalogLucide('settings'),
keyboard: Keyboard,
network: Network,
};
@@ -121,7 +117,7 @@ const GROUP_ICONS: Record, LucideIcon> =
messages: MessageSquareText,
sessions: MessagesSquare,
files: FileText,
- workspaces: Folder,
+ workspaces: catalogLucide('folder'),
assistants: Bot,
capabilities: Blocks,
settings: SlidersHorizontal,
@@ -131,10 +127,10 @@ function iconForItem(item: GlobalSearchItem): LucideIcon {
if (item.target.kind === 'action') {
const actionId = item.target.actionId;
const action = PRODUCT_ACTION_CATALOG.find((candidate) => candidate.id === actionId);
- return action ? ACTION_ICONS[action.icon] : Search;
+ return action ? ACTION_ICONS[action.icon] : catalogLucide('search');
}
- if (item.target.kind === 'assistant') return UserRound;
- return GROUP_ICONS[item.group as Exclude] ?? Search;
+ if (item.target.kind === 'assistant') return catalogLucide('user');
+ return GROUP_ICONS[item.group as Exclude] ?? catalogLucide('search');
}
function resultVariant(group: GlobalSearchGroupId): 'action' | 'entity' | 'standard' {
@@ -396,7 +392,7 @@ export const GlobalSearchContent: React.FC = ({
inputRef.current?.focus();
} : undefined}
clearLabel={query ? tCommon('nav.search.clear') : undefined}
- leadingIcon={}
+ leadingIcon={}
shortcut={query ? undefined : (
{searchShortcutHint.key}
@@ -416,7 +412,7 @@ export const GlobalSearchContent: React.FC = ({
) : (
-
+
= ({
}}
aria-label={tCommon('nav.search.clear')}
>
-
+
) : null}
@@ -502,7 +498,7 @@ export const GlobalSearchContent: React.FC
= ({
- = ({
data-testid={`${testIdPrefix}-group-drilldown-${groupId}`}
>
{tCommon('nav.search.resultCount', { count: groupView.totalCount })}
-
+
) : (
@@ -687,8 +683,8 @@ export const GlobalSearchContent: React.FC = ({
) : null}
{entity ? (
-
-
+
+
) : null}
@@ -698,7 +694,7 @@ export const GlobalSearchContent: React.FC = ({
);
})}
-
+
{(variant === 'embedded'
|| Boolean(parsedQuery.query)
diff --git a/src/web-ui/src/app/layout/FloatingMiniChat.scss b/src/web-ui/src/app/layout/FloatingMiniChat.scss
index af19a03abb..9926f3c0d0 100644
--- a/src/web-ui/src/app/layout/FloatingMiniChat.scss
+++ b/src/web-ui/src/app/layout/FloatingMiniChat.scss
@@ -428,7 +428,6 @@ $panel-height: 680px;
justify-content: center;
box-sizing: border-box;
padding: 36px 32px;
- overflow-y: auto;
color: var(--bf-appearance-token-color-text-primary);
h2 {
diff --git a/src/web-ui/src/app/layout/FloatingMiniChat.tsx b/src/web-ui/src/app/layout/FloatingMiniChat.tsx
index ad8a78f201..4bc0e12520 100644
--- a/src/web-ui/src/app/layout/FloatingMiniChat.tsx
+++ b/src/web-ui/src/app/layout/FloatingMiniChat.tsx
@@ -11,7 +11,7 @@
import React, { useState, useCallback, useMemo, useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
-import { MessageSquare, X } from 'lucide-react';
+
import { flowChatStore } from '../../flow_chat/store/FlowChatStore';
import { syncSessionToModernStore } from '../../flow_chat/services/storeSync';
import ChatPane from '@/app/scenes/session/ChatPane';
@@ -19,7 +19,7 @@ import type {
ChatInputRegistration,
ChatInputSubmission,
} from '@/flow_chat/components/chatInputRegistration';
-import { IconButton, Tooltip } from '@bitfun/ui';
+import { Icon, IconButton, Tooltip } from '@bitfun/ui';
import { useCurrentWorkspace } from '@/infrastructure/contexts/WorkspaceContext';
import { SessionMenu, useFlowChatSessions } from '../../flow_chat/components/session-menu';
@@ -540,7 +540,7 @@ export const FloatingMiniChat: React.FC = () => {
{renderMiniAppIcon(activeMiniAppIcon, 20)}
) : (
-
+
)}
@@ -584,7 +584,7 @@ export const FloatingMiniChat: React.FC = () => {
data-bf-component="floating-mini-chat"
data-bf-part="headerAction"
className="bitfun-fmc__close"
- icon={}
+ icon={}
onClick={handleClose}
size="md"
aria-label={t('session.close')}
diff --git a/src/web-ui/src/app/layout/MiniAppBubbleWelcome.tsx b/src/web-ui/src/app/layout/MiniAppBubbleWelcome.tsx
index 7988aa389f..7ea2deb47d 100644
--- a/src/web-ui/src/app/layout/MiniAppBubbleWelcome.tsx
+++ b/src/web-ui/src/app/layout/MiniAppBubbleWelcome.tsx
@@ -1,5 +1,6 @@
import React from 'react';
-import { ArrowUpRight, FolderOpen } from 'lucide-react';
+import { Icon, ScrollArea } from '@bitfun/ui';
+import { FolderOpen } from 'lucide-react';
import type { MiniAppBubbleCustomization } from '@/app/scenes/miniapps/miniAppStore';
import { renderMiniAppIcon } from '@/app/scenes/miniapps/utils/miniAppIcons';
@@ -32,7 +33,7 @@ export const MiniAppBubbleWelcome: React.FC = ({
const suggestions = welcome?.suggestions || [];
return (
-
+
= ({
onClick={() => onSuggestion(suggestion.prompt)}
>
{suggestion.label}
-
+
))}
)}
-
+
);
};
diff --git a/src/web-ui/src/app/scenes/agents/AgentsScene.test.tsx b/src/web-ui/src/app/scenes/agents/AgentsScene.test.tsx
index 72cd72bf4d..ff837ef817 100644
--- a/src/web-ui/src/app/scenes/agents/AgentsScene.test.tsx
+++ b/src/web-ui/src/app/scenes/agents/AgentsScene.test.tsx
@@ -322,7 +322,7 @@ describeWithJsdom('AgentsScene', () => {
expect(agentCardSource).not.toContain('--agent-card-gradient');
expect(coreCardSource).not.toContain('getAlphaColor');
expect(coreCardSource).not.toContain('--core-card-gradient');
- expect(coreCardStyles).toMatch(/&__status \{[\s\S]*?color: var\(--bf-appearance-token-color-text-primary\);[\s\S]*?> svg \{[\s\S]*?color: var\(--bf-appearance-token-color-success\);/);
+ expect(coreCardStyles).toMatch(/&__status \{[\s\S]*?color: var\(--bf-appearance-token-color-text-primary\);[\s\S]*?\[data-bf-component="icon"\] \{[\s\S]*?color: var\(--bf-appearance-token-color-success\);/);
expect(coreCardSurfaceStyles).not.toContain('$gradient');
expect(coreCardSurfaceStyles).toContain('@mixin agent-icon-dot-field()');
expect(coreCardSurfaceStyles).toContain('background-size: 7px 7px;');
diff --git a/src/web-ui/src/app/scenes/agents/AgentsScene.tsx b/src/web-ui/src/app/scenes/agents/AgentsScene.tsx
index fa3636c939..de52739e07 100644
--- a/src/web-ui/src/app/scenes/agents/AgentsScene.tsx
+++ b/src/web-ui/src/app/scenes/agents/AgentsScene.tsx
@@ -1,20 +1,7 @@
-import { Button, IconButton, SearchField, Select, StatusPill, Tooltip } from '@bitfun/ui';
+import { Button, Icon, IconButton, SearchField, Select, StatusPill, Tooltip, ScrollArea } from '@bitfun/ui';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import type { TFunction } from 'i18next';
-import {
- Bot,
- Circle,
- Cpu,
- FileText,
- MessageSquareText,
- RotateCcw,
- Pencil,
- Plus,
- Puzzle,
- Search as SearchIcon,
- Trash2,
- Wrench,
-} from 'lucide-react';
+import { Bot, Cpu, FileText, MessageSquareText, RotateCcw, Trash2, Wrench, type LucideIcon } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { Select as LegacySelect } from '@/component-library';
import { useI18n } from '@/infrastructure/i18n/hooks/useI18n';
@@ -493,7 +480,7 @@ const AgentsHomeView: React.FC = () => {
const selectedAgentCapabilityTabs = useMemo(() => {
const tabs: Array<{
key: CapabilityTab;
- icon: typeof Wrench;
+ icon: LucideIcon;
label: string;
count?: string;
}> = [];
@@ -534,7 +521,11 @@ const AgentsHomeView: React.FC = () => {
: selectedAgentSkills.length;
tabs.push({
key: 'skills',
- icon: Puzzle,
+ icon: (({ size = 14 }: { size?: number | string }) => {
+ const n = typeof size === 'number' ? size : 14;
+ const mapped = n <= 11 ? '2xs' : n <= 13 ? 'xs' : n <= 15 ? 'sm' : n <= 17 ? 'md' : 'lg';
+ return ;
+ }) as LucideIcon,
label: t('agentsOverview.skills'),
count: `${currentSkillCount}/${selectedAgentSkillConfigs.length}`,
});
@@ -683,7 +674,7 @@ const AgentsHomeView: React.FC = () => {
className="bitfun-agents-scene__search"
value={searchQuery}
onValueChange={setSearchQuery}
- leadingIcon={}
+ leadingIcon={}
placeholder={t('page.searchPlaceholder')}
aria-label={t('page.searchPlaceholder')}
size="sm"
@@ -694,7 +685,7 @@ const AgentsHomeView: React.FC = () => {
}
+ leadingIcon={}
onClick={openCreateAgent}
data-testid="agents-create-agent-btn"
>
@@ -723,20 +714,10 @@ const AgentsHomeView: React.FC = () => {
{HARNESS_GEAR_PROFILES.map(({ id }) => (
-
+ ].filter(Boolean).join(' ')} />
))}
@@ -926,7 +907,7 @@ const AgentsHomeView: React.FC = () => {
}
+ leadingIcon={
}
onClick={() => {
const id = selectedAgent.id;
closeAgentDetails();
@@ -949,7 +930,7 @@ const AgentsHomeView: React.FC = () => {
}
+ leadingIcon={
}
onClick={() => {
closeAgentDetails();
openEcosystemCompatibility({ ownerSurface: 'external-sources' });
@@ -1002,7 +983,7 @@ const AgentsHomeView: React.FC = () => {
})}
-
+
{activeDetailSection === 'basic' ? (
@@ -1442,7 +1423,7 @@ const AgentsHomeView: React.FC = () => {
) : null}
) : null}
-
+
) : null}
diff --git a/src/web-ui/src/app/scenes/agents/agentsIcons.ts b/src/web-ui/src/app/scenes/agents/agentsIcons.ts
index 3a09372d8a..a1e547d5bf 100644
--- a/src/web-ui/src/app/scenes/agents/agentsIcons.ts
+++ b/src/web-ui/src/app/scenes/agents/agentsIcons.ts
@@ -1,27 +1,39 @@
/**
* Icon and color mapping for the agents scene
- * All visuals use lucide-react icons + CSS custom properties.
+ * Catalog-mapped glyphs render through @bitfun/ui Icon; the rest stay lucide.
*/
+import { Icon, type IconName, type IconSize } from '@bitfun/ui';
import {
Code2,
FlaskConical,
Bug,
FileText,
- Globe,
BarChart2,
- PenLine,
Server,
- Eye,
Layers,
Bot,
Cpu,
- Terminal,
Microscope,
type LucideProps,
} from 'lucide-react';
-import type React from 'react';
+import React from 'react';
export { CAPABILITY_ACCENT } from './agentAppearance';
+function catalogSize(size?: number | string): IconSize {
+ const n = typeof size === 'number' ? size : 21;
+ if (n <= 11) return '2xs';
+ if (n <= 13) return 'xs';
+ if (n <= 15) return 'sm';
+ if (n <= 17) return 'md';
+ return 'lg';
+}
+
+function catalogIcon(name: IconName): React.FC {
+ return function CatalogIcon({ size }) {
+ return React.createElement(Icon, { name, size: catalogSize(size) });
+ };
+}
+
export type AgentIconKey =
| 'code2' | 'eye' | 'flask' | 'bug' | 'filetext'
| 'globe' | 'barchart' | 'layers' | 'penline' | 'server'
@@ -29,17 +41,17 @@ export type AgentIconKey =
export const AGENT_ICON_MAP: Record> = {
code2: Code2,
- eye: Eye,
+ eye: catalogIcon('eye'),
flask: FlaskConical,
bug: Bug,
filetext: FileText,
- globe: Globe,
+ globe: catalogIcon('browser'),
barchart: BarChart2,
layers: Layers,
- penline: PenLine,
+ penline: catalogIcon('edit'),
server: Server,
bot: Bot,
- terminal: Terminal,
+ terminal: catalogIcon('terminal'),
microscope: Microscope,
cpu: Cpu,
};
diff --git a/src/web-ui/src/app/scenes/agents/components/AgentCard.scss b/src/web-ui/src/app/scenes/agents/components/AgentCard.scss
index 02b3116c85..0ab8ad8230 100644
--- a/src/web-ui/src/app/scenes/agents/components/AgentCard.scss
+++ b/src/web-ui/src/app/scenes/agents/components/AgentCard.scss
@@ -365,7 +365,6 @@
min-width: 0;
min-height: 0;
height: 100%;
- overflow-y: auto;
padding-inline-end: 2px;
> .agent-card__config-panel,
diff --git a/src/web-ui/src/app/scenes/agents/components/AgentCard.tsx b/src/web-ui/src/app/scenes/agents/components/AgentCard.tsx
index 77f25766b4..4a0be42a57 100644
--- a/src/web-ui/src/app/scenes/agents/components/AgentCard.tsx
+++ b/src/web-ui/src/app/scenes/agents/components/AgentCard.tsx
@@ -1,14 +1,7 @@
import React from 'react';
-import {
- Bot,
- Wrench,
- Puzzle,
- Cpu,
- Sparkles,
- UsersRound,
-} from 'lucide-react';
+import { Bot, Wrench, Cpu, UsersRound } from 'lucide-react';
import { useTranslation } from 'react-i18next';
-import { StatusPill } from '@bitfun/ui';
+import { Icon, StatusPill } from '@bitfun/ui';
import type { AgentWithCapabilities } from '../agentsStore';
import { AGENT_ICON_MAP } from '../agentsIcons';
import { getAgentBadge, getAgentDescription, getCapabilityLabel } from '../utils';
@@ -33,7 +26,7 @@ const AgentCard: React.FC = ({
}) => {
const { t } = useTranslation('scenes/agents');
const badge = getAgentBadge(t, agent.agentKind, agent.source ?? agent.subagentSource);
- const Icon = AGENT_ICON_MAP[(agent.iconKey ?? 'bot') as keyof typeof AGENT_ICON_MAP] ?? Bot;
+ const AgentGlyph = AGENT_ICON_MAP[(agent.iconKey ?? 'bot') as keyof typeof AGENT_ICON_MAP] ?? Bot;
const totalTools = toolCount ?? agent.toolCount ?? agent.defaultTools?.length ?? 0;
const capabilityCount = agent.capabilities.length;
const openDetails = () => onOpenDetails(agent);
@@ -63,7 +56,7 @@ const AgentCard: React.FC = ({
@@ -109,7 +102,7 @@ const AgentCard: React.FC
= ({
{agent.agentKind === 'mode' ? (
<>
-
+
{t('agentCard.metrics.skills')}
{skillCount}
@@ -121,7 +114,7 @@ const AgentCard: React.FC = ({
>
) : (
-
+
{t('agentCard.metrics.capabilities')}
{capabilityCount}
diff --git a/src/web-ui/src/app/scenes/agents/components/CoreAgentCard.scss b/src/web-ui/src/app/scenes/agents/components/CoreAgentCard.scss
index 520355fd98..c76da83eea 100644
--- a/src/web-ui/src/app/scenes/agents/components/CoreAgentCard.scss
+++ b/src/web-ui/src/app/scenes/agents/components/CoreAgentCard.scss
@@ -111,7 +111,7 @@
line-height: $line-height-base;
white-space: nowrap;
- > svg {
+ [data-bf-component="icon"] {
flex: none;
color: var(--bf-appearance-token-color-success);
}
@@ -124,7 +124,7 @@
&.is-disabled {
color: var(--bf-appearance-token-color-text-secondary);
- > svg {
+ [data-bf-component="icon"] {
color: var(--bf-appearance-token-color-warning);
}
}
diff --git a/src/web-ui/src/app/scenes/agents/components/CoreAgentCard.tsx b/src/web-ui/src/app/scenes/agents/components/CoreAgentCard.tsx
index 7f7725acb2..8e74c07cac 100644
--- a/src/web-ui/src/app/scenes/agents/components/CoreAgentCard.tsx
+++ b/src/web-ui/src/app/scenes/agents/components/CoreAgentCard.tsx
@@ -1,14 +1,7 @@
import React from 'react';
-import {
- Bot,
- Wrench,
- Puzzle,
- Cpu,
- Circle,
- UsersRound,
-} from 'lucide-react';
+import { Bot, Wrench, Cpu, UsersRound } from 'lucide-react';
import { useTranslation } from 'react-i18next';
-import { StatusPill } from '@bitfun/ui';
+import { Icon, StatusPill } from '@bitfun/ui';
import type { AgentWithCapabilities } from '../agentsStore';
import { AGENT_ICON_MAP } from '../agentsIcons';
import { getAgentDescription } from '../utils';
@@ -43,7 +36,7 @@ const CoreAgentCard: React.FC = ({
disabledReason,
}) => {
const { t } = useTranslation('scenes/agents');
- const Icon = AGENT_ICON_MAP[(agent.iconKey ?? 'bot') as keyof typeof AGENT_ICON_MAP] ?? Bot;
+ const AgentGlyph = AGENT_ICON_MAP[(agent.iconKey ?? 'bot') as keyof typeof AGENT_ICON_MAP] ?? Bot;
const totalTools = toolCount ?? agent.toolCount ?? agent.defaultTools?.length ?? 0;
const openDetails = () => onOpenDetails(agent);
const statusLabel = disabledReason ?? t('agentCard.status.connected');
@@ -71,7 +64,7 @@ const CoreAgentCard: React.FC = ({
>
@@ -93,7 +86,7 @@ const CoreAgentCard: React.FC = ({
data-bf-state={disabledReason ? 'disabled' : 'connected'}
title={statusLabel}
>
-
+
{statusLabel}
@@ -114,7 +107,7 @@ const CoreAgentCard: React.FC = ({
{agent.agentKind === 'mode' ? (
<>
-
+
{t('agentCard.metrics.skills')}
{skillCount}
diff --git a/src/web-ui/src/app/scenes/agents/components/CreateAgentPage.scss b/src/web-ui/src/app/scenes/agents/components/CreateAgentPage.scss
index d2bc78018b..c07e9416c1 100644
--- a/src/web-ui/src/app/scenes/agents/components/CreateAgentPage.scss
+++ b/src/web-ui/src/app/scenes/agents/components/CreateAgentPage.scss
@@ -6,16 +6,7 @@
.th__list-body {
flex: 1;
- overflow-y: auto;
- overflow-x: hidden;
padding: $size-gap-5 clamp(16px, 2.2vw, 28px);
-
- &::-webkit-scrollbar { width: 4px; }
- &::-webkit-scrollbar-track { background: transparent; }
- &::-webkit-scrollbar-thumb {
- background: var(--bf-appearance-token-border-subtle);
- border-radius: 2px;
- }
}
.th__list-inner {
@@ -231,17 +222,8 @@
box-sizing: border-box;
min-height: 96px;
max-height: 360px;
- overflow-y: auto;
- overscroll-behavior: contain;
padding-right: 2px;
- &::-webkit-scrollbar { width: 4px; }
- &::-webkit-scrollbar-track { background: transparent; }
- &::-webkit-scrollbar-thumb {
- background: var(--bf-appearance-token-border-subtle);
- border-radius: 2px;
- }
-
.tool-group-summary {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
diff --git a/src/web-ui/src/app/scenes/agents/components/CreateAgentPage.tsx b/src/web-ui/src/app/scenes/agents/components/CreateAgentPage.tsx
index 992ae8fba3..63a26b0ec0 100644
--- a/src/web-ui/src/app/scenes/agents/components/CreateAgentPage.tsx
+++ b/src/web-ui/src/app/scenes/agents/components/CreateAgentPage.tsx
@@ -1,12 +1,9 @@
-import { Button, Switch, Input, Tooltip } from '@bitfun/ui';
+import { Button, Icon, Input, ScrollArea, Switch, Tooltip, type IconName, type IconSize } from '@bitfun/ui';
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import {
- ArrowLeft,
FileText,
FolderTree,
Minus,
- Settings2,
- Terminal,
type LucideIcon,
} from 'lucide-react';
import { useTranslation } from 'react-i18next';
@@ -32,6 +29,14 @@ import { useUserToolGroups } from './useUserToolGroups';
import '../AgentsView.scss';
import './CreateAgentPage.scss';
+function catalogLucide(name: IconName): LucideIcon {
+ return function CatalogLucide({ size }: { size?: number | string }) {
+ const n = typeof size === 'number' ? size : 14;
+ const mapped: IconSize = n <= 11 ? '2xs' : n <= 13 ? 'xs' : n <= 15 ? 'sm' : n <= 17 ? 'md' : 'lg';
+ return ;
+ } as LucideIcon;
+}
+
const ID_REGEX = /^[a-zA-Z][a-zA-Z0-9_-]*$/;
const DEFAULT_MODE_POLICY: UserContextSection[] = [
'workspace_context',
@@ -68,7 +73,7 @@ const VISIBLE_CONTEXT_SECTIONS: UserContextSection[] = [
'project_layout',
];
const CONTEXT_SECTION_ICONS: Record = {
- workspace_context: Terminal,
+ workspace_context: catalogLucide('terminal'),
workspace_instructions: FileText,
project_layout: FolderTree,
};
@@ -488,17 +493,17 @@ const CreateAgentPage: React.FC = () => {
}
+ leadingIcon={}
onClick={openHome}
>
{t('agentsOverview.backToOverview')}
-
-
+
+
{t('agentsOverview.form.loadingDetail')}
-
+
);
}
@@ -510,28 +515,28 @@ const CreateAgentPage: React.FC = () => {
}
+ leadingIcon={}
onClick={openHome}
>
{t('agentsOverview.backToOverview')}
-
+
{detailError}
{t('agentsOverview.form.cancel')}
-
+
);
}
return (
-
-
+
+
{formTitle}
@@ -800,7 +805,7 @@ const CreateAgentPage: React.FC = () => {
setToolsEditing(true);
}}
disabled={submitting}
- leadingIcon={
}
+ leadingIcon={
}
>
{t('agentsOverview.toolsEdit')}
@@ -819,7 +824,7 @@ const CreateAgentPage: React.FC = () => {
testId="custom-agent-tool-groups"
/>
) : (
-
{
selectedToolNames={Array.from(selectedTools)}
userGroups={userToolGroups}
/>
-
+
)}
>
@@ -943,7 +948,7 @@ const CreateAgentPage: React.FC = () => {
-
+
);
};
diff --git a/src/web-ui/src/app/scenes/agents/components/SkillGroupPicker.scss b/src/web-ui/src/app/scenes/agents/components/SkillGroupPicker.scss
index cdfea89114..6d0650f86a 100644
--- a/src/web-ui/src/app/scenes/agents/components/SkillGroupPicker.scss
+++ b/src/web-ui/src/app/scenes/agents/components/SkillGroupPicker.scss
@@ -194,7 +194,6 @@
}
&__list {
- overflow-y: auto;
border: 1px solid var(--bf-appearance-token-border-subtle);
}
@@ -256,7 +255,6 @@
display: flex;
flex-wrap: wrap;
gap: $size-gap-1;
- overflow-y: auto;
border: 1px solid var(--bf-appearance-token-border-subtle);
padding: $size-gap-2;
}
diff --git a/src/web-ui/src/app/scenes/agents/components/SkillGroupPicker.tsx b/src/web-ui/src/app/scenes/agents/components/SkillGroupPicker.tsx
index 270892f4b0..ed3b740e7b 100644
--- a/src/web-ui/src/app/scenes/agents/components/SkillGroupPicker.tsx
+++ b/src/web-ui/src/app/scenes/agents/components/SkillGroupPicker.tsx
@@ -1,12 +1,8 @@
-import { Button, Switch, IconButton, Input, Modal, Tooltip } from '@bitfun/ui';
+import { Button, Icon, IconButton, Input, Modal, ScrollArea, Switch, Tooltip } from '@bitfun/ui';
import React, { useMemo, useState } from 'react';
import type { TFunction } from 'i18next';
import {
ArrowDown,
- ArrowUp,
- Pencil,
- Plus,
- Settings2,
Trash2,
} from 'lucide-react';
import { useTranslation } from 'react-i18next';
@@ -304,7 +300,7 @@ export const SkillGroupManagerModal: React.FC = ({
{t('agentsOverview.skillGroupPicker.groupSkills')}
-
+
{selectableSkills.map((skill) => {
const selected = skillKeys.has(skill.key);
const tooltipFields = skillTooltipFields(skill, t);
@@ -336,7 +332,7 @@ export const SkillGroupManagerModal: React.FC = ({
);
})}
-
+
@@ -353,7 +349,7 @@ export const SkillGroupManagerModal: React.FC = ({
<>
{t('agentsOverview.skillGroupPicker.manageSubtitle')}
-
}>
+
}>
{t('agentsOverview.skillGroupPicker.createGroup')}
@@ -361,7 +357,7 @@ export const SkillGroupManagerModal: React.FC
= ({
{groups.length === 0 ? (
{t('agentsOverview.skillGroupPicker.noUserGroups')}
) : (
-
+
{groups.map((group, index) => {
const unavailable = unavailableUserSkillKeys(group, skills);
return (
@@ -383,7 +379,7 @@ export const SkillGroupManagerModal: React.FC = ({
aria-label={t('agentsOverview.skillGroupPicker.moveUp')}
onClick={() => void moveGroup(index, -1)}
disabled={saving || index === 0}
- icon={}
+ icon={}
/>
@@ -403,7 +399,7 @@ export const SkillGroupManagerModal: React.FC = ({
aria-label={t('agentsOverview.skillGroupPicker.editGroup')}
onClick={() => startEdit(group)}
disabled={saving}
- icon={}
+ icon={}
/>
@@ -420,7 +416,7 @@ export const SkillGroupManagerModal: React.FC = ({
);
})}
-
+
)}
>
)}
@@ -469,7 +465,7 @@ export const SkillGroupPicker: React.FC = ({
size="sm"
onClick={() => setIsManagerOpen(true)}
disabled={disabled}
- leadingIcon={}
+ leadingIcon={}
>
{t('agentsOverview.skillGroupPicker.manageGroups')}
diff --git a/src/web-ui/src/app/scenes/agents/components/ToolGroupPicker.scss b/src/web-ui/src/app/scenes/agents/components/ToolGroupPicker.scss
index 6a804f3d44..6f811040c6 100644
--- a/src/web-ui/src/app/scenes/agents/components/ToolGroupPicker.scss
+++ b/src/web-ui/src/app/scenes/agents/components/ToolGroupPicker.scss
@@ -193,7 +193,6 @@
}
&__list {
- overflow-y: auto;
border: 1px solid var(--bf-appearance-token-border-subtle);
}
@@ -255,7 +254,6 @@
display: flex;
flex-wrap: wrap;
gap: $size-gap-1;
- overflow-y: auto;
border: 1px solid var(--bf-appearance-token-border-subtle);
padding: $size-gap-2;
}
diff --git a/src/web-ui/src/app/scenes/agents/components/ToolGroupPicker.tsx b/src/web-ui/src/app/scenes/agents/components/ToolGroupPicker.tsx
index 8c6f11be2c..348e82b5f1 100644
--- a/src/web-ui/src/app/scenes/agents/components/ToolGroupPicker.tsx
+++ b/src/web-ui/src/app/scenes/agents/components/ToolGroupPicker.tsx
@@ -1,12 +1,8 @@
-import { Button, Switch, IconButton, Input, Modal, Tooltip } from '@bitfun/ui';
+import { Button, Icon, IconButton, Input, Modal, ScrollArea, Switch, Tooltip } from '@bitfun/ui';
import React, { useMemo, useState } from 'react';
import type { TFunction } from 'i18next';
import {
ArrowDown,
- ArrowUp,
- Pencil,
- Plus,
- Settings2,
Trash2,
} from 'lucide-react';
import { useTranslation } from 'react-i18next';
@@ -287,7 +283,7 @@ const GroupManagerModal: React.FC = ({
{t('agentsOverview.toolGroups.groupTools')}
-
+
{selectableTools.map((tool) => {
const selected = toolNames.has(tool.name);
const tooltipFields = toolTooltipFields(tool, t);
@@ -316,7 +312,7 @@ const GroupManagerModal: React.FC = ({
);
})}
-
+
@@ -333,7 +329,7 @@ const GroupManagerModal: React.FC = ({
<>
{t('agentsOverview.toolGroups.manageSubtitle')}
-
}>
+
}>
{t('agentsOverview.toolGroups.createGroup')}
@@ -341,7 +337,7 @@ const GroupManagerModal: React.FC
= ({
{groups.length === 0 ? (
{t('agentsOverview.toolGroups.noUserGroups')}
) : (
-
+
{groups.map((group, index) => {
const unavailable = unavailableUserToolNames(group, tools);
return (
@@ -363,7 +359,7 @@ const GroupManagerModal: React.FC = ({
aria-label={t('agentsOverview.toolGroups.moveUp')}
onClick={() => void moveGroup(index, -1)}
disabled={saving || index === 0}
- icon={}
+ icon={}
/>
@@ -383,7 +379,7 @@ const GroupManagerModal: React.FC = ({
aria-label={t('agentsOverview.toolGroups.editGroup')}
onClick={() => startEdit(group)}
disabled={saving}
- icon={}
+ icon={}
/>
@@ -400,7 +396,7 @@ const GroupManagerModal: React.FC = ({
);
})}
-
+
)}
>
)}
@@ -445,7 +441,7 @@ export const ToolGroupPicker: React.FC = ({
size="sm"
onClick={() => setIsManagerOpen(true)}
disabled={disabled}
- leadingIcon={}
+ leadingIcon={}
>
{t('agentsOverview.toolGroups.manageGroups')}
diff --git a/src/web-ui/src/app/scenes/browser/BrowserPanel.tsx b/src/web-ui/src/app/scenes/browser/BrowserPanel.tsx
index e1632eb889..0947b53972 100644
--- a/src/web-ui/src/app/scenes/browser/BrowserPanel.tsx
+++ b/src/web-ui/src/app/scenes/browser/BrowserPanel.tsx
@@ -7,8 +7,8 @@
*/
import React, { useCallback, useEffect, useRef, useState } from 'react';
-import { IconButton } from '@bitfun/ui';
-import { AlertTriangle, ChevronLeft, ChevronRight, Globe, RefreshCw, MousePointer2 } from 'lucide-react';
+import { Icon, IconButton } from '@bitfun/ui';
+import { AlertTriangle, ChevronLeft, RefreshCw, MousePointer2 } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { createLogger } from '@/shared/utils/logger';
import { useSceneStore } from '@/app/stores/sceneStore';
@@ -176,7 +176,7 @@ const BrowserPanel: React.FC = ({ isActive, initialUrl, openR
size="sm"
onClick={goForward}
aria-label={t('nav.forward')}
- icon={}
+ icon={}
data-testid="browser-forward-button"
/>
= ({ isActive, initialUrl, openR
data-testid="browser-refresh-button"
/>
-
+
= ({ isActive, initialUrl, openR
data-webview-label={webviewLabel}
>
-
+
{currentUrl}
diff --git a/src/web-ui/src/app/scenes/browser/BrowserScene.tsx b/src/web-ui/src/app/scenes/browser/BrowserScene.tsx
index fd4d0264d7..167360c5a7 100644
--- a/src/web-ui/src/app/scenes/browser/BrowserScene.tsx
+++ b/src/web-ui/src/app/scenes/browser/BrowserScene.tsx
@@ -1,6 +1,6 @@
import React, { useCallback } from 'react';
-import { IconButton } from '@bitfun/ui';
-import { AlertTriangle, ChevronLeft, ChevronRight, Globe, RefreshCw } from 'lucide-react';
+import { Icon, IconButton } from '@bitfun/ui';
+import { AlertTriangle, ChevronLeft, RefreshCw } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { createLogger } from '@/shared/utils/logger';
import { useSceneStore } from '@/app/stores/sceneStore';
@@ -54,7 +54,7 @@ const BrowserScene: React.FC = () => {
size="sm"
onClick={browser.goForward}
aria-label={t('nav.forward')}
- icon={}
+ icon={}
data-testid="browser-forward-button"
/>
{
data-testid="browser-refresh-button"
/>
-
+
{
data-webview-label={browser.webviewLabel}
>
-
+
{browser.currentUrl}
diff --git a/src/web-ui/src/app/scenes/ecosystem-compatibility/EcosystemCompatibilityScene.scss b/src/web-ui/src/app/scenes/ecosystem-compatibility/EcosystemCompatibilityScene.scss
index 00440b8413..2d0b8799ba 100644
--- a/src/web-ui/src/app/scenes/ecosystem-compatibility/EcosystemCompatibilityScene.scss
+++ b/src/web-ui/src/app/scenes/ecosystem-compatibility/EcosystemCompatibilityScene.scss
@@ -75,7 +75,6 @@ $compatibility-sidebar-width: 236px;
&__product-groups {
flex: 1;
min-height: 0;
- overflow-y: auto;
padding: $size-gap-3 $size-gap-2;
}
@@ -376,7 +375,6 @@ $compatibility-sidebar-width: 236px;
&__content {
flex: 1;
min-height: 0;
- overflow-y: auto;
padding: $size-gap-5 $size-gap-6 $size-gap-8;
}
diff --git a/src/web-ui/src/app/scenes/ecosystem-compatibility/EcosystemCompatibilityScene.tsx b/src/web-ui/src/app/scenes/ecosystem-compatibility/EcosystemCompatibilityScene.tsx
index fd0bb4e8e2..d0df4353c4 100644
--- a/src/web-ui/src/app/scenes/ecosystem-compatibility/EcosystemCompatibilityScene.tsx
+++ b/src/web-ui/src/app/scenes/ecosystem-compatibility/EcosystemCompatibilityScene.tsx
@@ -1,24 +1,16 @@
-import { Button, SearchField, Switch } from '@bitfun/ui';
+import { Button, Icon, ScrollArea, SearchField, Switch, type IconName, type IconSize } from '@bitfun/ui';
import React, { Suspense, lazy, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import type { LucideIcon } from 'lucide-react';
import {
Bot,
Brain,
- ChevronDown,
- ChevronRight,
CircleUserRound,
- Command,
- Info,
MessageSquarePlus,
Network,
Package,
PawPrint,
- Puzzle,
RefreshCw,
- Search as SearchIcon,
Server,
- Settings,
- Terminal,
Webhook,
Wrench,
} from 'lucide-react';
@@ -49,6 +41,14 @@ import {
import { useEcosystemCompatibilityStore } from './ecosystemCompatibilityStore';
import './EcosystemCompatibilityScene.scss';
+function catalogLucide(name: IconName): LucideIcon {
+ return function CatalogLucide({ size }: { size?: number | string }) {
+ const n = typeof size === 'number' ? size : 15;
+ const mapped: IconSize = n <= 11 ? '2xs' : n <= 13 ? 'xs' : n <= 15 ? 'sm' : n <= 17 ? 'md' : 'lg';
+ return ;
+ } as LucideIcon;
+}
+
const AcpAgentsConfig = lazy(
() => import('@/infrastructure/config/components/AcpAgentsConfig'),
);
@@ -83,15 +83,15 @@ function EcosystemProductIcon({ productId, size }: {
const IMPORT_ITEM_ICONS: Record = {
account: CircleUserRound,
- settings: Settings,
- command: Command,
+ settings: catalogLucide('settings'),
+ command: catalogLucide('command-mac'),
tool: Wrench,
subagent: Bot,
skill: Package,
mcp: Server,
hook: Webhook,
memory: Brain,
- plugin: Puzzle,
+ plugin: catalogLucide('extension'),
pet: PawPrint,
};
@@ -457,7 +457,7 @@ const EcosystemCompatibilityScene: React.FC = () => {
{importItems.map((item) => {
const state = importItemState(item);
- const Icon = IMPORT_ITEM_ICONS[item.kind];
+ const ItemIcon = IMPORT_ITEM_ICONS[item.kind];
const ready = state === 'ready' || state === 'readyRename';
const dimmed = [
'notDetected',
@@ -494,7 +494,7 @@ const EcosystemCompatibilityScene: React.FC = () => {
>
-
+
{itemName}
@@ -579,7 +579,7 @@ const EcosystemCompatibilityScene: React.FC = () => {
-
+
{displayName}
@@ -692,7 +692,7 @@ const EcosystemCompatibilityScene: React.FC = () => {
-
+
{t('run.subagent.profileNote')}
@@ -735,7 +735,7 @@ const EcosystemCompatibilityScene: React.FC = () => {
{ownerSurface === 'acp' ? (
-
+
{t('run.managerScope')}
}>
@@ -761,7 +761,7 @@ const EcosystemCompatibilityScene: React.FC = () => {
>
}
+ leadingIcon={}
size="sm"
value={searchQuery}
onValueChange={setSearchQuery}
@@ -776,11 +776,11 @@ const EcosystemCompatibilityScene: React.FC = () => {
>
{t('host.label')}
{currentHost}
-
+
- {
{t(`status.${runtime.status}`)}
-
+
);
})}
@@ -827,10 +827,10 @@ const EcosystemCompatibilityScene: React.FC = () => {
{t('search.empty')}
) : null}
-
+
-
+
{t('sidebar.hint')}
@@ -892,7 +892,7 @@ const EcosystemCompatibilityScene: React.FC = () => {
-
{
) : null}
{loadIssues.length > 0 ? (
-
+
{t('partialLoad', {
sources: loadIssues.includes('externalSources') ? t('loadAreas.externalSources') : '',
acp: loadIssues.includes('acpClients') ? t('loadAreas.acpClients') : '',
@@ -921,7 +921,7 @@ const EcosystemCompatibilityScene: React.FC = () => {
aria-label={t('governance.managerLabel')}
>
-
+
{t('governance.managerScope')}
}>
@@ -946,7 +946,7 @@ const EcosystemCompatibilityScene: React.FC = () => {
{renderImport()}
)}
-
+
);
diff --git a/src/web-ui/src/app/scenes/file-viewer/FileViewerNav.scss b/src/web-ui/src/app/scenes/file-viewer/FileViewerNav.scss
index 7daba83651..bf33b80440 100644
--- a/src/web-ui/src/app/scenes/file-viewer/FileViewerNav.scss
+++ b/src/web-ui/src/app/scenes/file-viewer/FileViewerNav.scss
@@ -5,6 +5,26 @@
flex-direction: column;
flex: 1;
min-height: 0;
+ inline-size: 100%;
+ max-inline-size: none;
+ background: transparent;
+
+ > [data-bf-part="header"] {
+ padding: 0;
+ }
+
+ &__body {
+ padding: 0;
+ overflow: hidden;
+ }
+
+ &__content {
+ padding: 0;
+ gap: 0;
+ flex: 1 1 auto;
+ min-block-size: 0;
+ height: 100%;
+ }
&__header {
display: flex;
diff --git a/src/web-ui/src/app/scenes/file-viewer/FileViewerNav.tsx b/src/web-ui/src/app/scenes/file-viewer/FileViewerNav.tsx
index 17277c2e67..d2e6b23987 100644
--- a/src/web-ui/src/app/scenes/file-viewer/FileViewerNav.tsx
+++ b/src/web-ui/src/app/scenes/file-viewer/FileViewerNav.tsx
@@ -7,8 +7,8 @@
*/
import React, { useState, useCallback } from 'react';
-import { IconButton, Tooltip } from '@bitfun/ui';
-import { Folder, Search as SearchIcon, List, FilePlus, FolderPlus, RefreshCw } from 'lucide-react';
+import { Icon, IconButton, NavigationPanel, Tooltip } from '@bitfun/ui';
+import { List, FilePlus, FolderPlus, RefreshCw } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { useCurrentWorkspace } from '../../../infrastructure/contexts/WorkspaceContext';
import { useI18n } from '@/infrastructure/i18n';
@@ -30,10 +30,16 @@ const FileViewerNav: React.FC = () => {
}, []);
return (
-
+
-
+
{t('nav.items.project')}
@@ -75,13 +81,15 @@ const FileViewerNav: React.FC = () => {
:
}
+ icon={viewMode === 'tree' ? :
}
onClick={handleToggleViewMode}
/>
)}
+ )}
+ >
{
viewMode={viewMode}
onViewModeChange={setViewMode}
/>
-
+
);
};
diff --git a/src/web-ui/src/app/scenes/git/GitNav.appearance.ts b/src/web-ui/src/app/scenes/git/GitNav.appearance.ts
index 4e421ac3bb..a5f7a1d90b 100644
--- a/src/web-ui/src/app/scenes/git/GitNav.appearance.ts
+++ b/src/web-ui/src/app/scenes/git/GitNav.appearance.ts
@@ -1,2 +1,2 @@
import type { AppearanceSurfaceDescriptor } from '@/infrastructure/appearance';
-export const gitNavAppearanceDescriptor: AppearanceSurfaceDescriptor = { id: 'git-nav', parts: [{ id: 'root' }, { id: 'header' }, { id: 'title' }, { id: 'status' }, { id: 'actions' }, { id: 'sections' }] };
+export const gitNavAppearanceDescriptor: AppearanceSurfaceDescriptor = { id: 'git-nav', parts: [{ id: 'root' }, { id: 'header' }, { id: 'title' }, { id: 'status' }, { id: 'actions' }] };
diff --git a/src/web-ui/src/app/scenes/git/GitNav.scss b/src/web-ui/src/app/scenes/git/GitNav.scss
index 738ca3b7f9..c64611cca2 100644
--- a/src/web-ui/src/app/scenes/git/GitNav.scss
+++ b/src/web-ui/src/app/scenes/git/GitNav.scss
@@ -10,8 +10,20 @@
display: flex;
flex-direction: column;
width: 100%;
+ inline-size: 100%;
+ max-inline-size: none;
height: 100%;
overflow: hidden;
+ background: transparent;
+
+ > [data-bf-part="header"] {
+ padding: 0;
+ }
+
+ [data-bf-part="content"] {
+ padding: 0;
+ gap: 0;
+ }
&__header {
display: flex;
@@ -32,16 +44,7 @@
&__sections {
flex: 1 1 auto;
- overflow-y: auto;
- overflow-x: hidden;
padding: $size-gap-2 0;
-
- &::-webkit-scrollbar { width: 3px; }
- &::-webkit-scrollbar-track { background: transparent; }
- &::-webkit-scrollbar-thumb {
- background: var(--bf-appearance-token-border-subtle);
- border-radius: 2px;
- }
}
&__status {
diff --git a/src/web-ui/src/app/scenes/git/GitNav.tsx b/src/web-ui/src/app/scenes/git/GitNav.tsx
index e7396bd6a6..2b4f81c63b 100644
--- a/src/web-ui/src/app/scenes/git/GitNav.tsx
+++ b/src/web-ui/src/app/scenes/git/GitNav.tsx
@@ -5,17 +5,17 @@
*/
import React, { useCallback } from 'react';
-import { IconButton, Tooltip } from '@bitfun/ui';
+import { Icon, IconButton, NavigationPanel, NavigationPanelItem, Tooltip } from '@bitfun/ui';
import { useTranslation } from 'react-i18next';
-import { GitBranch, Layers2, ArrowUp, ArrowDown, RefreshCw } from 'lucide-react';
+import { Layers2, ArrowDown, RefreshCw } from 'lucide-react';
import { useGitSceneStore, type GitSceneView } from './gitSceneStore';
import { useGitState } from '../../../tools/git/hooks';
import { useCurrentWorkspace } from '@/infrastructure/contexts/WorkspaceContext';
import './GitNav.scss';
-const NAV_ITEMS: { id: GitSceneView; icon: React.ElementType; labelKey: string }[] = [
- { id: 'working-copy', icon: GitBranch, labelKey: 'tabs.changes' },
+const NAV_ITEMS: { id: GitSceneView; icon?: React.ElementType; labelKey: string }[] = [
+ { id: 'working-copy', labelKey: 'tabs.changes' },
{ id: 'branches', icon: Layers2, labelKey: 'tabs.branches' },
{ id: 'graph', icon: Layers2, labelKey: 'tabs.branchGraph' },
];
@@ -54,15 +54,21 @@ const GitNav: React.FC = () => {
);
return (
-
+
{t('title')}
+ )}
+ >
{isRepository && (
-
+
{currentBranch ?? t('common.unknown')}
@@ -71,7 +77,7 @@ const GitNav: React.FC = () => {
{ahead > 0 && (
- {ahead}
+ {ahead}
)}
{behind > 0 && (
@@ -94,28 +100,27 @@ const GitNav: React.FC = () => {
)}
-
- {NAV_ITEMS.map(({ id, icon: Icon, labelKey }) => (
- handleViewClick(id)}
- >
-
-
- {t(labelKey)}
-
- {id === 'working-copy' && changeCount > 0 && (
+ {NAV_ITEMS.map(({ id, icon: ItemIcon, labelKey }) => (
+
+ : ItemIcon ? : undefined}
+ metadata={
+ id === 'working-copy' && changeCount > 0 ? (
({changeCount})
- )}
- {id === 'branches' && branchCount > 0 && (
+ ) : id === 'branches' && branchCount > 0 ? (
({branchCount})
- )}
-
- ))}
-
-
+ ) : undefined
+ }
+ onClick={() => handleViewClick(id)}
+ >
+ {t(labelKey)}
+
+ ))}
+
);
};
diff --git a/src/web-ui/src/app/scenes/git/GitScene.tsx b/src/web-ui/src/app/scenes/git/GitScene.tsx
index a956c8b724..507f0bdb66 100644
--- a/src/web-ui/src/app/scenes/git/GitScene.tsx
+++ b/src/web-ui/src/app/scenes/git/GitScene.tsx
@@ -4,9 +4,9 @@
*/
import React, { useState, useRef, useEffect, useCallback } from 'react';
-import { Button, IconButton, Tooltip } from '@bitfun/ui';
+import { Button, Icon, IconButton, Tooltip } from '@bitfun/ui';
import { useTranslation } from 'react-i18next';
-import { GitBranch, Plus, RefreshCw, ShieldAlert } from 'lucide-react';
+import { RefreshCw, ShieldAlert } from 'lucide-react';
import { useGitSceneStore } from './gitSceneStore';
import { WorkingCopyView, BranchesView, GraphView } from './views';
import { useGitState } from '@/tools/git/hooks';
@@ -152,7 +152,7 @@ const GitScene: React.FC
= ({
-
+
{t('init.title')}
@@ -161,7 +161,7 @@ const GitScene: React.FC
= ({
}
+ leadingIcon={}
onClick={handleInitGitRepository}
>
{t('init.initButton')}
diff --git a/src/web-ui/src/app/scenes/git/views/BranchesView.scss b/src/web-ui/src/app/scenes/git/views/BranchesView.scss
index eee1878892..c54bbd6084 100644
--- a/src/web-ui/src/app/scenes/git/views/BranchesView.scss
+++ b/src/web-ui/src/app/scenes/git/views/BranchesView.scss
@@ -43,7 +43,6 @@
&__list {
flex: 1;
- overflow-y: auto;
padding: 8px;
}
@@ -141,7 +140,6 @@
&__history-list {
flex: 1;
- overflow-y: auto;
padding: 8px;
}
diff --git a/src/web-ui/src/app/scenes/git/views/BranchesView.tsx b/src/web-ui/src/app/scenes/git/views/BranchesView.tsx
index 39272f9ef1..021a1945b6 100644
--- a/src/web-ui/src/app/scenes/git/views/BranchesView.tsx
+++ b/src/web-ui/src/app/scenes/git/views/BranchesView.tsx
@@ -2,20 +2,13 @@
* BranchesView — Left: branch list (switch/create/delete). Right: commit history for selected branch.
*/
-import { Button, IconButton, SearchField, Tooltip } from '@bitfun/ui';
+import { Button, Icon, IconButton, SearchField, Tooltip, ScrollArea } from '@bitfun/ui';
import React, { useCallback, useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import {
- GitBranch,
- Plus,
Trash2,
- GitCommit,
- Copy,
RotateCcw,
FileText,
- ChevronDown,
- ChevronRight,
- Search as SearchIcon,
} from 'lucide-react';
import { useI18n } from '@/infrastructure/i18n/hooks/useI18n';
@@ -208,7 +201,7 @@ const BranchesView: React.FC = ({ workspacePath }) => {
return (
-
+
{t('tabs.branches')}
Open a workspace to see branches.
@@ -223,7 +216,7 @@ const BranchesView: React.FC
= ({ workspacePath }) => {
}
+ leadingIcon={}
placeholder={t('search.branches')}
aria-label={t('search.branches')}
value={branchSearchQuery}
@@ -236,7 +229,7 @@ const BranchesView: React.FC = ({ workspacePath }) => {
}
+ leadingIcon={}
onClick={() => handleCreateFrom(branches.find(b => b.current)?.name ?? selectedBranchName ?? '')}
title={t('dialog.createNewBranch.title')}
>
@@ -244,7 +237,7 @@ const BranchesView: React.FC = ({ workspacePath }) => {
-
+
{branchLoading ? (
{t('common.loading')}
) : filteredBranches.length === 0 ? (
@@ -265,7 +258,7 @@ const BranchesView: React.FC = ({ workspacePath }) => {
onClick={() => handleSelectBranch(branch.name)}
>
-
+
{branch.name}
{branch.current && {t('branch.current')}}
@@ -277,7 +270,7 @@ const BranchesView: React.FC = ({ workspacePath }) => {
size="sm"
onClick={() => handleSwitchBranch(branch.name)}
disabled={isOperating}
- icon={}
+ icon={}
/>
)}
@@ -287,7 +280,7 @@ const BranchesView: React.FC = ({ workspacePath }) => {
size="sm"
onClick={() => handleCreateFrom(branch.name)}
disabled={isOperating}
- icon={}
+ icon={}
/>
{!branch.current && (
@@ -305,7 +298,7 @@ const BranchesView: React.FC = ({ workspacePath }) => {
))
)}
-
+
@@ -315,7 +308,7 @@ const BranchesView: React.FC = ({ workspacePath }) => {
}
+ leadingIcon={}
placeholder={t('search.commits')}
aria-label={t('search.commits')}
value={commitSearchQuery}
@@ -324,7 +317,7 @@ const BranchesView: React.FC = ({ workspacePath }) => {
onClear={commitSearchQuery ? () => setCommitSearchQuery('') : undefined}
/>
-
+
{!selectedBranchName ? (
{t('empty.noCommits')}
) : commitLoading ? (
@@ -348,7 +341,7 @@ const BranchesView: React.FC = ({ workspacePath }) => {
>
toggleCommitExpand(commit.hash)}>
- {isExpanded ? : }
+ {isExpanded ? : }
{summary}
@@ -362,7 +355,7 @@ const BranchesView: React.FC
= ({ workspacePath }) => {
aria-label={t('actions.copyCommitHash')}
size="sm"
onClick={() => handleCopyHash(commit.hash)}
- icon={}
+ icon={}
/>
@@ -397,7 +390,7 @@ const BranchesView: React.FC = ({ workspacePath }) => {
);
})
)}
-
+
= ({
-
+
{status?.current_branch ?? t('common.unknown')}
{(status?.ahead ?? 0) > 0 && (
@@ -431,7 +423,7 @@ const WorkingCopyView: React.FC = ({
size="sm"
onClick={handlePush}
disabled={isOperating}
- icon={}
+ icon={}
/>
@@ -466,7 +458,7 @@ const WorkingCopyView: React.FC
= ({
size="sm"
onClick={handleAIGenerateCommit}
disabled={isOperating}
- icon={}
+ icon={}
/>
)}
@@ -488,7 +480,7 @@ const WorkingCopyView: React.FC = ({
}
+ leadingIcon={}
placeholder={t('search.files')}
aria-label={t('search.files')}
value={searchQuery}
@@ -507,7 +499,7 @@ const WorkingCopyView: React.FC = ({
data-bf-part="groupHeader"
onClick={() => toggleFileGroup('unstaged')}
>
- {expandedFileGroups.has('unstaged') ? : }
+ {expandedFileGroups.has('unstaged') ? : }
{searchQuery
? t('fileGroups.unstagedWithFilter', { filtered: filteredFiles.unstaged.length, total: status.unstaged?.length || 0 })
@@ -521,7 +513,7 @@ const WorkingCopyView: React.FC = ({
e.stopPropagation();
toggleSelectAll();
}}
- icon={isAllSelected ? : isPartialSelected ? : }
+ icon={isAllSelected ? : isPartialSelected ? : }
/>
@@ -533,7 +525,7 @@ const WorkingCopyView: React.FC = ({
handleStageSelectedFiles();
}}
disabled={isOperating || selectedFiles.size === 0}
- icon={}
+ icon={}
/>
@@ -561,7 +553,7 @@ const WorkingCopyView: React.FC = ({
toggleFileSelection(file.path);
}}
>
- {isSelected ? : }
+ {isSelected ? : }
{fileName}
{dirPath && {dirPath}}
@@ -586,7 +578,7 @@ const WorkingCopyView: React.FC = ({
{filteredFiles.untracked.length > 0 && (
<>
toggleFileGroup('untracked')} data-bf-component="working-copy-view" data-bf-part="groupHeader">
- {expandedFileGroups.has('untracked') ?
:
}
+ {expandedFileGroups.has('untracked') ?
:
}
{searchQuery
? t('fileGroups.untrackedWithFilter', { filtered: filteredFiles.untracked.length, total: status.untracked?.length || 0 })
@@ -616,7 +608,7 @@ const WorkingCopyView: React.FC = ({
toggleFileSelection(filePath);
}}
>
- {isSelected ? : }
+ {isSelected ? : }
{fileName}
{dirPath && {dirPath}}
@@ -641,7 +633,7 @@ const WorkingCopyView: React.FC = ({
{filteredFiles.staged.length > 0 && (
<>
toggleFileGroup('staged')} data-bf-component="working-copy-view" data-bf-part="groupHeader">
- {expandedFileGroups.has('staged') ? : }
+ {expandedFileGroups.has('staged') ? : }
{searchQuery
? t('fileGroups.stagedWithFilter', { filtered: filteredFiles.staged.length, total: status.staged?.length || 0 })
diff --git a/src/web-ui/src/app/scenes/miniapps/MiniAppGalleryScene.tsx b/src/web-ui/src/app/scenes/miniapps/MiniAppGalleryScene.tsx
index 380d68efbe..b2cc8deea8 100644
--- a/src/web-ui/src/app/scenes/miniapps/MiniAppGalleryScene.tsx
+++ b/src/web-ui/src/app/scenes/miniapps/MiniAppGalleryScene.tsx
@@ -3,8 +3,8 @@
* Opening an app opens a separate scene tab (miniapp:id).
*/
import React, { Suspense, lazy, useState } from 'react';
-import { Download, Store, UploadCloud } from 'lucide-react';
-import { TabGroup, type TabGroupItem } from '@bitfun/ui';
+
+import { Icon, TabGroup, type TabGroupItem } from '@bitfun/ui';
import { useI18n } from '@/infrastructure/i18n';
import './MiniAppGalleryScene.scss';
@@ -21,17 +21,17 @@ const MiniAppGalleryScene: React.FC = () => {
const tabItems: TabGroupItem[] = [
{
value: 'installed',
- icon: ,
+ icon: ,
label: t('market.tabs.installed'),
},
{
value: 'market',
- icon: ,
+ icon: ,
label: t('market.tabs.market'),
},
{
value: 'submissions',
- icon: ,
+ icon: ,
label: t('market.tabs.submissions'),
},
];
diff --git a/src/web-ui/src/app/scenes/miniapps/MiniAppScene.scss b/src/web-ui/src/app/scenes/miniapps/MiniAppScene.scss
index 27b563ad59..162808da19 100644
--- a/src/web-ui/src/app/scenes/miniapps/MiniAppScene.scss
+++ b/src/web-ui/src/app/scenes/miniapps/MiniAppScene.scss
@@ -489,14 +489,16 @@
letter-spacing: 0;
}
+ &__list {
+ max-height: 140px;
+ }
+
ul {
display: flex;
flex-direction: column;
gap: $size-gap-1;
- max-height: 140px;
margin: 0;
padding: $size-gap-2 $size-gap-3;
- overflow: auto;
border: 1px solid var(--bf-appearance-token-border-subtle);
border-radius: $size-radius-base;
background: var(--bf-appearance-token-element-bg-subtle);
diff --git a/src/web-ui/src/app/scenes/miniapps/MiniAppScene.tsx b/src/web-ui/src/app/scenes/miniapps/MiniAppScene.tsx
index e9d3d5cb97..09ebdd1949 100644
--- a/src/web-ui/src/app/scenes/miniapps/MiniAppScene.tsx
+++ b/src/web-ui/src/app/scenes/miniapps/MiniAppScene.tsx
@@ -2,9 +2,9 @@
* MiniAppScene — standalone scene tab for a single MiniApp.
* Mounts MiniAppRunner; close via SceneBar × (does not stop worker).
*/
-import { Button, IconButton, Tooltip } from '@bitfun/ui';
+import { Button, Icon, IconButton, Tooltip } from '@bitfun/ui';
import React, { useCallback, useEffect, useState } from 'react';
-import { RefreshCw, Loader2, AlertTriangle, CheckCircle2, X } from 'lucide-react';
+import { RefreshCw, Loader2, AlertTriangle } from 'lucide-react';
import { miniAppAPI } from '@/infrastructure/api/service-api/MiniAppAPI';
import { api } from '@/infrastructure/api/service-api/ApiClient';
import type { MiniApp, MiniAppDraft } from '@/infrastructure/api/service-api/MiniAppAPI';
@@ -223,7 +223,7 @@ const MiniAppScene: React.FC = ({ appId }) => {
size="sm"
onClick={() => setCustomizePreview(null)}
aria-label={t('customize.hidePreview')}
- icon={}
+ icon={}
/>
@@ -240,7 +240,7 @@ const MiniAppScene: React.FC = ({ appId }) => {
)}
{customizeNotice && (
-
+
{customizeNotice}
)}
diff --git a/src/web-ui/src/app/scenes/miniapps/components/MiniAppDetailModal.scss b/src/web-ui/src/app/scenes/miniapps/components/MiniAppDetailModal.scss
index 8c48a1af1a..09698f2a1f 100644
--- a/src/web-ui/src/app/scenes/miniapps/components/MiniAppDetailModal.scss
+++ b/src/web-ui/src/app/scenes/miniapps/components/MiniAppDetailModal.scss
@@ -35,7 +35,6 @@
color: var(--bf-appearance-token-color-text-primary);
&__modal-content {
- overflow-y: auto;
}
&__source-badge,
diff --git a/src/web-ui/src/app/scenes/miniapps/components/MiniAppDetailModal.tsx b/src/web-ui/src/app/scenes/miniapps/components/MiniAppDetailModal.tsx
index 94eb457061..0ba151953e 100644
--- a/src/web-ui/src/app/scenes/miniapps/components/MiniAppDetailModal.tsx
+++ b/src/web-ui/src/app/scenes/miniapps/components/MiniAppDetailModal.tsx
@@ -1,21 +1,6 @@
-import { Button, Modal } from '@bitfun/ui';
+import { Button, Icon, Modal, ScrollArea } from '@bitfun/ui';
import React, { useMemo, useRef } from 'react';
-import {
- AppWindow,
- Bot,
- CheckCircle2,
- Cpu,
- Database,
- Download,
- FolderKanban,
- Globe2,
- Play,
- ShieldCheck,
- Sparkles,
- Square,
- Terminal,
- Trash2,
-} from 'lucide-react';
+import { Bot, Cpu, Database, FolderKanban, Play, ShieldCheck, Square, Trash2 } from 'lucide-react';
import { useI18n } from '@/infrastructure/i18n';
import type { MiniAppMeta } from '@/infrastructure/api/service-api/MiniAppAPI';
import { renderMiniAppIcon } from '../utils/miniAppIcons';
@@ -50,11 +35,11 @@ type Translate = (key: string, params?: Record) => string;
function capabilityIcon(capability: MiniAppDetailCapability): React.ReactNode {
switch (capability.kind) {
- case 'ai': return ;
+ case 'ai': return ;
case 'workspace': return ;
- case 'export': return ;
- case 'shell': return ;
- case 'network': return ;
+ case 'export': return ;
+ case 'shell': return ;
+ case 'network': return ;
case 'worker': return ;
case 'storage': return ;
case 'desktop': return ;
@@ -62,7 +47,7 @@ function capabilityIcon(capability: MiniAppDetailCapability): React.ReactNode {
case 'instant': return ;
case 'surface':
default:
- return ;
+ return ;
}
}
@@ -202,7 +187,7 @@ const MiniAppDetailModal: React.FC = ({
titleTestId="miniapp-detail-title"
closeButtonTestId="miniapp-detail-close"
>
- = ({
-
+
);
};
diff --git a/src/web-ui/src/app/scenes/miniapps/customization/MiniAppCustomizePanel.tsx b/src/web-ui/src/app/scenes/miniapps/customization/MiniAppCustomizePanel.tsx
index 9d77525053..6293769d9c 100644
--- a/src/web-ui/src/app/scenes/miniapps/customization/MiniAppCustomizePanel.tsx
+++ b/src/web-ui/src/app/scenes/miniapps/customization/MiniAppCustomizePanel.tsx
@@ -1,6 +1,6 @@
-import { Button, IconButton, Tooltip } from '@bitfun/ui';
+import { Button, Icon, IconButton, Tooltip } from '@bitfun/ui';
import React, { useCallback, useEffect, useMemo, useState, useSyncExternalStore } from 'react';
-import { AlertTriangle, Check, Eye, EyeOff, Loader2, RefreshCw, Send, Trash2, X } from 'lucide-react';
+import { AlertTriangle, EyeOff, Loader2, RefreshCw, Send, Trash2 } from 'lucide-react';
import { flowChatStore } from '@/flow_chat/store/FlowChatStore';
import type { MiniApp, MiniAppCustomizationMetadata, MiniAppDraft } from '@/infrastructure/api/service-api/MiniAppAPI';
@@ -415,7 +415,7 @@ export const MiniAppCustomizePanel: React.FC = ({
onClick={handleClose}
disabled={busy}
aria-label={t('customize.close')}
- icon={}
+ icon={}
/>
@@ -442,7 +442,7 @@ export const MiniAppCustomizePanel: React.FC = ({
onClick={() => void handleDismissBuiltinUpdate()}
disabled={dismissingBuiltinUpdate}
loading={dismissingBuiltinUpdate}
- leadingIcon={}
+ leadingIcon={}
>
{t('customize.dismissBuiltinUpdate')}
@@ -503,7 +503,7 @@ export const MiniAppCustomizePanel: React.FC = ({
onClick={handleTogglePreview}
disabled={busy}
>
- {previewOpen ? : }
+ {previewOpen ? : }
{previewOpen ? t('customize.hidePreview') : t('customize.openPreview')}
)}
@@ -517,7 +517,7 @@ export const MiniAppCustomizePanel: React.FC = ({
{editorStatus && (
-
+
{editorStatus}
)}
diff --git a/src/web-ui/src/app/scenes/miniapps/customization/MiniAppPermissionDiffDialog.tsx b/src/web-ui/src/app/scenes/miniapps/customization/MiniAppPermissionDiffDialog.tsx
index 2102c957ff..8437efc132 100644
--- a/src/web-ui/src/app/scenes/miniapps/customization/MiniAppPermissionDiffDialog.tsx
+++ b/src/web-ui/src/app/scenes/miniapps/customization/MiniAppPermissionDiffDialog.tsx
@@ -1,4 +1,4 @@
-import { Button, Modal } from '@bitfun/ui';
+import { Button, Modal, ScrollArea } from '@bitfun/ui';
import React from 'react';
import { AlertTriangle } from 'lucide-react';
import type { MiniAppPermissionDiff } from '@/infrastructure/api/service-api/MiniAppAPI';
@@ -20,11 +20,13 @@ function PermissionList({ title, items }: { title: string; items: string[] }) {
return (
{title}
-
- {items.map((item) => (
- - {item}
- ))}
-
+
+
+ {items.map((item) => (
+ - {item}
+ ))}
+
+
);
}
diff --git a/src/web-ui/src/app/scenes/miniapps/utils/miniAppIcons.tsx b/src/web-ui/src/app/scenes/miniapps/utils/miniAppIcons.tsx
index 6934152102..ec294eabc5 100644
--- a/src/web-ui/src/app/scenes/miniapps/utils/miniAppIcons.tsx
+++ b/src/web-ui/src/app/scenes/miniapps/utils/miniAppIcons.tsx
@@ -1,4 +1,5 @@
import React from 'react';
+import { Icon, type IconName, type IconSize } from '@bitfun/ui';
import {
Aperture,
AppWindow,
@@ -10,19 +11,32 @@ import {
GitPullRequest,
Globe,
Grid3x3,
- Image,
LayoutGrid,
Presentation,
Regex,
Rocket,
- Settings,
- Sparkles,
- Terminal,
Workflow,
Wrench,
type LucideIcon,
} from 'lucide-react';
+const MINI_APP_CATALOG: Record = {
+ AppWindow: 'floating-window',
+ Globe: 'browser',
+ Image: 'image',
+ Settings: 'settings',
+ Sparkles: 'spark',
+ Terminal: 'terminal',
+};
+
+function catalogSize(size: number): IconSize {
+ if (size <= 11) return '2xs';
+ if (size <= 13) return 'xs';
+ if (size <= 15) return 'sm';
+ if (size <= 17) return 'md';
+ return 'lg';
+}
+
const ICON_GRADIENTS = [
'linear-gradient(135deg, color-mix(in srgb, var(--bf-appearance-token-color-accent-600) 35%, transparent) 0%, color-mix(in srgb, var(--bf-appearance-token-color-purple-500) 25%, transparent) 100%)',
'linear-gradient(135deg, color-mix(in srgb, var(--bf-appearance-token-color-success) 30%, transparent) 0%, color-mix(in srgb, var(--bf-appearance-token-color-accent-600) 25%, transparent) 100%)',
@@ -43,14 +57,10 @@ const MINI_APP_ICONS = {
GitPullRequest,
Globe,
Grid3x3,
- Image,
LayoutGrid,
Presentation,
Regex,
Rocket,
- Settings,
- Sparkles,
- Terminal,
Workflow,
Wrench,
} satisfies Record;
@@ -60,10 +70,14 @@ export function renderMiniAppIcon(name: string, size = 28): React.ReactNode {
.split('-')
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
.join('') as keyof typeof MINI_APP_ICONS;
- const Icon = MINI_APP_ICONS[key];
+ const catalogName = MINI_APP_CATALOG[key];
+ if (catalogName) {
+ return ;
+ }
+ const LucideGlyph = MINI_APP_ICONS[key];
- return Icon
- ?
+ return LucideGlyph
+ ?
: ;
}
diff --git a/src/web-ui/src/app/scenes/miniapps/views/MiniAppGalleryView.tsx b/src/web-ui/src/app/scenes/miniapps/views/MiniAppGalleryView.tsx
index 41907dfdf7..698512e29f 100644
--- a/src/web-ui/src/app/scenes/miniapps/views/MiniAppGalleryView.tsx
+++ b/src/web-ui/src/app/scenes/miniapps/views/MiniAppGalleryView.tsx
@@ -4,8 +4,6 @@ import {
FolderPlus,
LayoutGrid,
PackagePlus,
- Search as SearchIcon,
- Sparkles,
} from 'lucide-react';
import { open } from '@tauri-apps/plugin-dialog';
import { useSceneManager } from '@/app/hooks/useSceneManager';
@@ -23,7 +21,7 @@ import {
type MarketPackageInspection,
} from '@/infrastructure/api/service-api/MiniAppMarketAPI';
import { createLogger } from '@/shared/utils/logger';
-import { ConfirmDialog, SearchField, IconButton, Menu, MenuItem } from '@bitfun/ui';
+import { ConfirmDialog, Icon, IconButton, Menu, MenuItem, SearchField } from '@bitfun/ui';
import {
GalleryEmpty,
@@ -380,7 +378,7 @@ const MiniAppGalleryView: React.FC = () => {
+ ?
:
}
message={apps.length === 0
@@ -418,7 +416,7 @@ const MiniAppGalleryView: React.FC = () => {
actions={(
<>
}
+ leadingIcon={}
onValueChange={setSearch}
placeholder={t('searchPlaceholder')}
size="sm"
diff --git a/src/web-ui/src/app/scenes/miniapps/views/MiniAppMarketView.tsx b/src/web-ui/src/app/scenes/miniapps/views/MiniAppMarketView.tsx
index a459ad214d..9b43e25291 100644
--- a/src/web-ui/src/app/scenes/miniapps/views/MiniAppMarketView.tsx
+++ b/src/web-ui/src/app/scenes/miniapps/views/MiniAppMarketView.tsx
@@ -1,20 +1,13 @@
-import { Button, ConfirmDialog, SearchField, Select, StatusPill, type SelectOption } from '@bitfun/ui';
+import { Button, ConfirmDialog, Icon, SearchField, Select, type SelectOption, StatusPill } from '@bitfun/ui';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import {
AlertTriangle,
- ArrowUpRight,
- Check,
- ChevronDown,
ChevronUp,
- Download,
- ExternalLink,
Heart,
Loader2,
PackageCheck,
RefreshCw,
- Search as SearchIcon,
ShieldCheck,
- Star,
} from 'lucide-react';
import {
@@ -275,7 +268,7 @@ const MiniAppMarketView: React.FC = () => {
data-bf-part="headerActions"
>
}
+ leadingIcon={}
value={query}
onValueChange={setQuery}
placeholder={t('market.search')}
@@ -388,8 +381,8 @@ const MiniAppMarketView: React.FC = () => {
{name}
{description}
- {item.ratingAverage.toFixed(1)}
- {formatNumber(item.downloadCount)}
+ {item.ratingAverage.toFixed(1)}
+ {formatNumber(item.downloadCount)}
{formatNumber(item.favoriteCount)}
@@ -445,7 +438,7 @@ const MiniAppMarketView: React.FC = () => {
{/* Installed and up to date: nothing left to install, so say so and let "Open" lead. */}
{installed && !canUpdate ? (
-
+
{t('market.detail.installed')}
) : null}
@@ -455,7 +448,7 @@ const MiniAppMarketView: React.FC = () => {
variant={canUpdate ? 'outline' : 'fill'}
disabled={actionBusy || workspaceUnsupported}
onClick={() => openInstalledApp(installed.appId)}
- leadingIcon={
}
+ leadingIcon={
}
>
{t('market.detail.open')}
@@ -468,7 +461,7 @@ const MiniAppMarketView: React.FC = () => {
disabled={actionBusy || workspaceUnsupported}
onClick={() => setInstallPrompt(true)}
>
- {actionBusy ?
: canUpdate ?
:
}
+ {actionBusy ?
: canUpdate ?
:
}
{installLabel}
) : null}
@@ -526,10 +519,7 @@ const MiniAppMarketView: React.FC = () => {
{[1, 2, 3, 4, 5].map((value) => (
void rate(value)} disabled={actionBusy}>
- = value ? 'currentColor' : 'none'}
- />
+
))}
{detail.ratingAverage.toFixed(1)} · {formatNumber(detail.ratingCount)}
@@ -546,7 +536,7 @@ const MiniAppMarketView: React.FC = () => {
v{release.releaseNumber}
{release.minBitfunVersion}+
- {release.yanked ? {t('market.detail.yanked')} : }
+ {release.yanked ? {t('market.detail.yanked')} : }
))}
@@ -557,7 +547,7 @@ const MiniAppMarketView: React.FC = () => {
aria-expanded={releasesExpanded}
onClick={() => setReleasesExpanded((current) => !current)}
>
- {releasesExpanded ?
:
}
+ {releasesExpanded ?
:
}
{releasesExpanded
? t('market.detail.releasesCollapse')
: t('market.detail.releasesExpand', { count: releaseHistory.hiddenCount })}
@@ -569,7 +559,7 @@ const MiniAppMarketView: React.FC = () => {
size="sm"
variant="outline"
onClick={() => void systemAPI.openExternal(detail.repositoryUrl!)}
- leadingIcon={
}
+ leadingIcon={
}
>
{t('market.detail.repository')}
diff --git a/src/web-ui/src/app/scenes/miniapps/views/MiniAppSubmissionsView.tsx b/src/web-ui/src/app/scenes/miniapps/views/MiniAppSubmissionsView.tsx
index aef3bb10e6..d57c34e9e8 100644
--- a/src/web-ui/src/app/scenes/miniapps/views/MiniAppSubmissionsView.tsx
+++ b/src/web-ui/src/app/scenes/miniapps/views/MiniAppSubmissionsView.tsx
@@ -1,11 +1,9 @@
-import { Button, Field, Input, Select, StatusPill } from '@bitfun/ui';
+import { Button, Field, Icon, Input, Select, StatusPill } from '@bitfun/ui';
import React, { useEffect, useMemo, useState } from 'react';
import { open } from '@tauri-apps/plugin-dialog';
import {
AlertTriangle,
Camera,
- ChevronDown,
- ChevronRight,
FileImage,
Github,
History,
@@ -13,7 +11,6 @@ import {
PackageOpen,
RefreshCw,
Send,
- X,
} from 'lucide-react';
import { GalleryEmpty, GalleryLayout, GalleryPageHeader } from '@/app/components';
import { useI18n } from '@/infrastructure/i18n';
@@ -382,7 +379,7 @@ const MiniAppSubmissionsView: React.FC = () => {
setScreenshotPaths((current) => current.filter((item) => item !== path))
}
>
-
+
))}
@@ -394,7 +391,7 @@ const MiniAppSubmissionsView: React.FC = () => {
className="miniapp-submissions__advanced-toggle"
onClick={() => setShowAdvanced((current) => !current)}
>
- {showAdvanced ? : }
+ {showAdvanced ? : }
{t('market.submissions.advanced')}
{!showAdvanced ? (
diff --git a/src/web-ui/src/app/scenes/my-agent/InsightsScene.scss b/src/web-ui/src/app/scenes/my-agent/InsightsScene.scss
index ff961b57c0..66f4fd5c8b 100644
--- a/src/web-ui/src/app/scenes/my-agent/InsightsScene.scss
+++ b/src/web-ui/src/app/scenes/my-agent/InsightsScene.scss
@@ -382,7 +382,6 @@ $ins-label: 12px;
.insights-scene__history {
padding: $ins-xs $ins-lg $ins-lg;
flex: 1;
- overflow-y: auto;
min-height: 0;
}
@@ -716,7 +715,6 @@ $ins-label: 12px;
display: flex;
flex-direction: row;
min-height: 0;
- overflow-y: auto;
overflow-x: hidden;
&::-webkit-scrollbar { width: 6px; }
diff --git a/src/web-ui/src/app/scenes/my-agent/InsightsScene.tsx b/src/web-ui/src/app/scenes/my-agent/InsightsScene.tsx
index 3cdc7be743..d0e23d468a 100644
--- a/src/web-ui/src/app/scenes/my-agent/InsightsScene.tsx
+++ b/src/web-ui/src/app/scenes/my-agent/InsightsScene.tsx
@@ -1,12 +1,7 @@
/* eslint-disable @typescript-eslint/no-use-before-define */
import React, { useEffect, useCallback, useMemo, useState, useRef } from 'react';
-import { Button, IconButton } from '@bitfun/ui';
-import {
- ExternalLink, Copy, Check, ArrowLeft, Loader2, AlertTriangle,
- BarChart3, MessageSquare, Calendar, Clock, X, Target, Zap, Trophy,
- AlertCircle, Lightbulb, Rocket, Database, ScanSearch, Layers3,
- FileCheck2, Gauge, Sparkles, Brain,
-} from 'lucide-react';
+import { Button, Icon, IconButton, ScrollArea } from '@bitfun/ui';
+import { Loader2, AlertTriangle, BarChart3, Calendar, Target, Zap, Trophy, AlertCircle, Lightbulb, Rocket, Database, ScanSearch, Layers3, FileCheck2, Gauge, Brain } from 'lucide-react';
import { useI18n } from '@/infrastructure/i18n/hooks/useI18n';
import { insightsApi, type InsightsReport, type InsightsReportMeta, type InsightsStats } from '@/infrastructure/api/insightsApi';
import { Select, type SelectOption } from '@/component-library';
@@ -63,7 +58,10 @@ const GENERATION_STEPS = [
id: 'summary',
titleKey: 'insights.generationStageSummary',
detailKey: 'insights.generationStageSummaryDetail',
- icon: Sparkles,
+ icon: function SparkIcon({ size }: { size?: number }) {
+ const n = typeof size === 'number' ? size : 13;
+ return ;
+ },
stages: ['synthesis'],
},
{
@@ -130,7 +128,7 @@ const GenerationPanel: React.FC<{ progress: GenerationProgress }> = ({ progress
{detail}
-
+
{t('insights.generationElapsed')}
{elapsed}
@@ -147,7 +145,7 @@ const GenerationPanel: React.FC<{ progress: GenerationProgress }> = ({ progress
return (
- {state === 'complete' ? : }
+ {state === 'complete' ? : }
{t(step.titleKey)}
@@ -288,7 +286,7 @@ const InsightsScene: React.FC = () => {
}
+ leadingIcon={}
onClick={cancelGeneration}
>
{t('insights.cancelBtn')}
@@ -314,14 +312,14 @@ const InsightsScene: React.FC = () => {
size="sm"
onClick={clearError}
aria-label={t('insights.dismissError')}
- icon={}
+ icon={}
/>
)}
{generating && }
-
+
{t('insights.history')}
@@ -344,7 +342,7 @@ const InsightsScene: React.FC = () => {
))}
)}
-
+
);
};
@@ -415,13 +413,13 @@ const ReportMetaCard: React.FC<{
-
+
{formatNumber(meta.total_messages)}{t('insights.messages')}
- {meta.total_hours.toFixed(1)} {t('insights.hours')}
+ {meta.total_hours.toFixed(1)} {t('insights.hours')}
{formatNumber(meta.days_covered)} {t('insights.days')}
@@ -449,7 +447,7 @@ const ReportMetaCard: React.FC<{
title={generationTokenTitle}
className={generationUsageComplete ? '' : 'insights-meta-card__generation-meta--partial'}
>
-
+
{t('insights.insightsGenerationTokens')}:
{' '}{hasGenerationUsage
? formatNumber(generationUsage.total_tokens, { notation: 'compact', maximumFractionDigits: 1 })
@@ -541,7 +539,7 @@ const ReportNav: React.FC<{ report: InsightsReport; scrollContainerRef: React.Re
return (
-
+
@@ -733,7 +731,7 @@ const ReportView: React.FC<{ report: InsightsReport; onBack: () => void }> = ({
} />
-
+
);
};
@@ -1185,7 +1183,7 @@ const CopyableCode: React.FC<{ text: string; label?: string }> = ({ text, label
{text}
- {copied ? : }
+ {copied ? : }
diff --git a/src/web-ui/src/app/scenes/pages/PagesScene.tsx b/src/web-ui/src/app/scenes/pages/PagesScene.tsx
index 813297c7f6..1740c37375 100644
--- a/src/web-ui/src/app/scenes/pages/PagesScene.tsx
+++ b/src/web-ui/src/app/scenes/pages/PagesScene.tsx
@@ -1,26 +1,6 @@
-import { Button, Input, Select, type SelectOption } from '@bitfun/ui';
+import { Button, Icon, Input, Select, type SelectOption } from '@bitfun/ui';
import React, { Suspense, lazy, useCallback, useEffect, useMemo, useRef, useState } from 'react';
-import {
- ChevronDown,
- ChevronUp,
- CircleStop,
- Clock3,
- Copy,
- ExternalLink,
- FileClock,
- Files,
- Globe,
- HardDrive,
- Lock,
- PanelsTopLeft,
- RefreshCw,
- Rocket,
- Save,
- Settings2,
- Trash2,
- Users,
- type LucideIcon,
-} from 'lucide-react';
+import { ChevronUp, CircleStop, FileClock, Globe, HardDrive, Lock, PanelsTopLeft, RefreshCw, Rocket, Save, Trash2, Users, type LucideIcon } from 'lucide-react';
import { PresenceBoundary } from '@/component-library';
import { confirmDanger, confirmWarning } from '@/infrastructure/confirm-dialog';
import { GalleryEmpty, GalleryLayout, GalleryPageHeader } from '@/app/components';
@@ -824,7 +804,7 @@ const PagesScene: React.FC = ({ isActive = true }) => {
-
+
{t('meta.updated', { date: formatTimestamp(page.updated_at) })}
@@ -832,7 +812,7 @@ const PagesScene: React.FC = ({ isActive = true }) => {
{t('meta.size', { size: formatBytes(page.total_bytes) })}
-
+
{t('meta.files', { count: page.file_count })}
= ({ isActive = true }) => {
onClick={() => void openPage(page, pageOwnerEpoch)}
disabled={pageBusy}
loading={pendingAction === `open:${page.slug}:production`}
- leadingIcon={}
+ leadingIcon={}
>
{t('actions.openProduction')}
@@ -864,7 +844,7 @@ const PagesScene: React.FC = ({ isActive = true }) => {
onClick={() => void copyPageLink(page, pageOwnerEpoch)}
disabled={pageBusy}
loading={pendingAction === `copy:${page.slug}:production`}
- leadingIcon={}
+ leadingIcon={}
>
{t('actions.copyLink')}
@@ -883,7 +863,7 @@ const PagesScene: React.FC = ({ isActive = true }) => {
{versions.length > 0 && (
{formatNumber(versions.length)}
)}
- {expanded ? : }
+ {expanded ? : }
= ({ isActive = true }) => {
disabled={pageBusy}
aria-expanded={managing}
aria-controls={managementId}
- leadingIcon={}
+ leadingIcon={}
>
{t('actions.manage')}
- {managing ? : }
+ {managing ? : }
@@ -1055,7 +1035,7 @@ const PagesScene: React.FC = ({ isActive = true }) => {
loading={pendingAction === `open:${page.slug}:${version.version_id}`}
aria-label={t('actions.openVersionAria', { version: version.version_id })}
>
-
+
= ({ isActive = true }) => {
loading={pendingAction === `copy:${page.slug}:${version.version_id}`}
aria-label={t('actions.copyVersionAria', { version: version.version_id })}
>
-
+
{!version.deployed && (
= ({
size={58}
/>
-
+
{isOpen ? createPortal(
- = ({
type="button"
size="sm"
aria-label={t('identity.avatarPickerClose')}
- icon={
}
+ icon={
}
onClick={() => {
setIsOpen(false);
triggerRef.current?.focus();
@@ -226,11 +226,11 @@ const AssistantAvatarPicker: React.FC
= ({
title={saveStatus === 'error' && saveError ? saveError : undefined}
aria-live="polite"
>
- {saveStatus === 'saved' ? : null}
+ {saveStatus === 'saved' ? : null}
{statusContent}
- ,
+ ,
getAppearanceOverlayHost(),
) : null}
diff --git a/src/web-ui/src/app/scenes/profile/views/AssistantCard.tsx b/src/web-ui/src/app/scenes/profile/views/AssistantCard.tsx
index 0d1a7a0a58..b812a840b1 100644
--- a/src/web-ui/src/app/scenes/profile/views/AssistantCard.tsx
+++ b/src/web-ui/src/app/scenes/profile/views/AssistantCard.tsx
@@ -1,11 +1,7 @@
import React from 'react';
-import { Button, IconButton, StatusPill, Tooltip } from '@bitfun/ui';
+import { Button, Icon, IconButton, StatusPill, Tooltip } from '@bitfun/ui';
import {
- ChevronRight,
MessageSquarePlus,
- MoreHorizontal,
- Pin,
- Settings2,
Trash2,
} from 'lucide-react';
import { useTranslation } from 'react-i18next';
@@ -98,14 +94,7 @@ const AssistantCard: React.FC = ({
) : null}
-
+
@@ -113,8 +102,8 @@ const AssistantCard: React.FC = ({
}
- trailingIcon={}
+ leadingIcon={}
+ trailingIcon={}
className="assistant-card__configure"
onClick={onClick}
disabled={isDeleting || isSettingPrimary}
@@ -148,7 +137,7 @@ const AssistantCard: React.FC = ({
aria-label={t('nursery.card.setPrimary')}
loading={isSettingPrimary}
disabled={isDeleting || isStartingSession || isSettingPrimary}
- icon={}
+ icon={}
/>
) : null}
diff --git a/src/web-ui/src/app/scenes/profile/views/AssistantConfigPage.tsx b/src/web-ui/src/app/scenes/profile/views/AssistantConfigPage.tsx
index 106b1ec0af..f9fe219968 100644
--- a/src/web-ui/src/app/scenes/profile/views/AssistantConfigPage.tsx
+++ b/src/web-ui/src/app/scenes/profile/views/AssistantConfigPage.tsx
@@ -5,10 +5,8 @@ import React, {
} from 'react';
import { useTranslation } from 'react-i18next';
import {
- ArrowLeft,
FileText,
RefreshCw,
- X,
} from 'lucide-react';
import { workspaceAPI } from '@/infrastructure/api/service-api/WorkspaceAPI';
@@ -29,7 +27,7 @@ import AssistantAvatarPicker from './AssistantAvatarPicker';
import AssistantQuickInput from './AssistantQuickInput';
import { useNurseryStore } from '../nurseryStore';
import './NurseryView.scss';
-import { IconButton, Input, Tooltip } from '@bitfun/ui';
+import { Icon, IconButton, Input, Tooltip, ScrollArea } from '@bitfun/ui';
const log = createLogger('AssistantConfigPage');
@@ -375,7 +373,7 @@ const AssistantConfigPage: React.FC = () => {
{/* Scheduled tasks — title/toolbar live inside ScheduledJobsView */}
-
+
{!workspacePath ? (
{t('nursery.assistant.scheduledSessionsNoWorkspace')}
) : (
@@ -395,7 +393,7 @@ const AssistantConfigPage: React.FC = () => {
/>
)}
-
+
@@ -423,7 +421,7 @@ const AssistantConfigPage: React.FC = () => {
className="acp-persona-editor__back"
onClick={closePersonaDoc}
aria-label={t('nursery.template.closeDetail')}
- icon={}
+ icon={}
/>
{t(`nursery.assistant.personaDocs.${docLabelKey}`)}
@@ -435,7 +433,7 @@ const AssistantConfigPage: React.FC = () => {
className="acp-persona-editor__close"
onClick={closePersonaDoc}
aria-label={t('nursery.template.closeDetail')}
- icon={}
+ icon={}
/>
@@ -515,7 +513,7 @@ const AssistantConfigPage: React.FC = () => {
data-bf-part="back"
onClick={openGallery}
aria-label={t('nursery.backToGallery')}
- icon={}
+ icon={}
/>
@@ -613,7 +611,7 @@ const AssistantConfigPage: React.FC = () => {
workspaceId={workspace?.id}
assistantName={identityName}
/>
-
+
{t('nursery.assistant.sessionsSectionTitle')}
{
}}
isActiveWorkspace
/>
-
+
{/* Right: persona docs + schedule */}
diff --git a/src/web-ui/src/app/scenes/profile/views/AssistantDefaultsPage.scss b/src/web-ui/src/app/scenes/profile/views/AssistantDefaultsPage.scss
index d2ed5609c7..6c9c27476a 100644
--- a/src/web-ui/src/app/scenes/profile/views/AssistantDefaultsPage.scss
+++ b/src/web-ui/src/app/scenes/profile/views/AssistantDefaultsPage.scss
@@ -114,7 +114,6 @@ $assistant-defaults-row-columns:
&__main {
min-width: 0;
min-height: 0;
- overflow: auto;
padding: 0 clamp(20px, 3vw, 32px) $size-gap-4;
scrollbar-width: thin;
scrollbar-color: var(--bf-appearance-token-border-subtle) transparent;
@@ -820,7 +819,6 @@ $assistant-defaults-row-columns:
&__body {
min-height: 0;
flex: 1;
- overflow-y: auto;
padding: 0 clamp(16px, 2vw, 22px) $size-gap-5;
scrollbar-width: thin;
scrollbar-color: var(--bf-appearance-token-border-subtle) transparent;
diff --git a/src/web-ui/src/app/scenes/profile/views/AssistantDefaultsPage.tsx b/src/web-ui/src/app/scenes/profile/views/AssistantDefaultsPage.tsx
index 54a9d86b2b..7928609eb6 100644
--- a/src/web-ui/src/app/scenes/profile/views/AssistantDefaultsPage.tsx
+++ b/src/web-ui/src/app/scenes/profile/views/AssistantDefaultsPage.tsx
@@ -1,19 +1,11 @@
-import { Button, IconButton, Switch, Tooltip } from '@bitfun/ui';
+import { Button, Icon, IconButton, Switch, Tooltip, ScrollArea } from '@bitfun/ui';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
- ArrowLeft,
- CheckCircle2,
- ChevronDown,
- ChevronRight,
CircleAlert,
- Info,
Plug2,
RefreshCw,
RotateCcw,
- Search,
- Sparkles,
Wrench,
- X,
} from 'lucide-react';
import { configAPI } from '@/infrastructure/api/service-api/ConfigAPI';
@@ -590,7 +582,7 @@ const AssistantDefaultsPage: React.FC = () => {
? skillsLoading[row.detail.skill.key]
: row.detail.type === 'tool' && toolsLoading[row.detail.tool.name];
const icon = row.kind === 'skill'
- ?
+ ?
: row.kind === 'mcp'
?
: ;
@@ -649,7 +641,7 @@ const AssistantDefaultsPage: React.FC = () => {
onClick={() => openDetail(row.detail)}
aria-label={t('nursery.template.openCapabilityDetail', { name: row.name })}
>
-
+
>
@@ -698,7 +690,7 @@ const AssistantDefaultsPage: React.FC = () => {
const renderEmptyState = (message: string) => (
-
+
{message}
{(searchQuery || statusFilter !== 'all') ? (
{
aria-expanded={!collapsed}
aria-label={t('nursery.template.toggleServerGroup', { name: serverName })}
>
-
+
@@ -779,7 +771,7 @@ const AssistantDefaultsPage: React.FC = () => {
onClick={() => openDetail({ type: 'mcpServer', serverId: group.id })}
aria-label={t('nursery.template.openServerDetail')}
>
-
+
{allNames.length > 0 ? (
{
aria-label={t('nursery.template.detailPanel')}
>
-
+
{t('nursery.template.detailEmptyTitle')}
{t('nursery.template.detailEmptyHint')}
@@ -847,9 +839,9 @@ const AssistantDefaultsPage: React.FC = () => {
MCP
{title}
-
setDetail(null)} aria-label={t('nursery.template.closeDetail')}>
+
setDetail(null)} aria-label={t('nursery.template.closeDetail')}>
-
+
{server?.statusMessage ? {server.statusMessage}
: null}
{t('nursery.template.detailSections.basic')}
@@ -896,7 +888,7 @@ const AssistantDefaultsPage: React.FC = () => {
) : {t('nursery.template.mcpServerNoTools')}
}
-
+
);
}
@@ -917,9 +909,9 @@ const AssistantDefaultsPage: React.FC = () => {
{kindLabel}
{row.name}
- setDetail(null)} aria-label={t('nursery.template.closeDetail')}>
+ setDetail(null)} aria-label={t('nursery.template.closeDetail')}>
-
+
{row.description}
{t('nursery.template.detailSections.basic')}
@@ -981,7 +973,7 @@ const AssistantDefaultsPage: React.FC = () => {
/>
-
+
);
};
@@ -1007,7 +999,7 @@ const AssistantDefaultsPage: React.FC = () => {
size="sm"
onClick={openGallery}
aria-label={t('nursery.backToGallery')}
- icon={}
+ icon={}
/>
{t('nursery.template.title')}
@@ -1018,7 +1010,7 @@ const AssistantDefaultsPage: React.FC = () => {
?
: saveState === 'error'
?
- : }
+ : }
{saveLabel}
{
) : (
-
+
{loadWarning ? (
@@ -1083,7 +1075,7 @@ const AssistantDefaultsPage: React.FC = () => {
@@ -1124,7 +1116,7 @@ const AssistantDefaultsPage: React.FC = () => {
) : renderEmptyState(t('nursery.template.noFilterResults'))}
-
+
{renderDetailPanel()}
)}
diff --git a/src/web-ui/src/app/scenes/profile/views/AssistantQuickInput.tsx b/src/web-ui/src/app/scenes/profile/views/AssistantQuickInput.tsx
index 02496c38b0..b9d26bed2f 100644
--- a/src/web-ui/src/app/scenes/profile/views/AssistantQuickInput.tsx
+++ b/src/web-ui/src/app/scenes/profile/views/AssistantQuickInput.tsx
@@ -10,8 +10,8 @@
*/
import React, { useCallback, useState } from 'react';
-import { IconButton } from '@bitfun/ui';
-import { ArrowUp } from 'lucide-react';
+import { Icon, IconButton } from '@bitfun/ui';
+;
import { useTranslation } from 'react-i18next';
import { Textarea } from '@/component-library';
import { ModelSelector } from '@/flow_chat/components/ModelSelector';
@@ -124,7 +124,7 @@ const AssistantQuickInput: React.FC = ({
onClick={() => { void handleSend(); }}
aria-label={t('actions.send')}
className="aqi__send"
- icon={}
+ icon={}
/>
diff --git a/src/web-ui/src/app/scenes/profile/views/NurseryGallery.presentation.test.ts b/src/web-ui/src/app/scenes/profile/views/NurseryGallery.presentation.test.ts
index 83eb215307..54ef9002ee 100644
--- a/src/web-ui/src/app/scenes/profile/views/NurseryGallery.presentation.test.ts
+++ b/src/web-ui/src/app/scenes/profile/views/NurseryGallery.presentation.test.ts
@@ -77,8 +77,8 @@ describe('Nursery gallery presentation', () => {
expect(cardSection).not.toContain('&__set-primary-btn {');
expect(cardSection).not.toContain('&__delete-btn {');
expect(source).toMatch(/import \{[^}]*\bButton\b[^}]*} from '@bitfun\/ui';/);
- expect(source).toContain('leadingIcon={}');
- expect(source).toContain('trailingIcon={}');
+ expect(source).toContain('leadingIcon={}');
expect(source).toContain('className="assistant-card__configure"');
expect(source).toContain('className="assistant-card__session-actions"');
diff --git a/src/web-ui/src/app/scenes/profile/views/NurseryGallery.tsx b/src/web-ui/src/app/scenes/profile/views/NurseryGallery.tsx
index d7102d107c..9f3bc8b396 100644
--- a/src/web-ui/src/app/scenes/profile/views/NurseryGallery.tsx
+++ b/src/web-ui/src/app/scenes/profile/views/NurseryGallery.tsx
@@ -1,17 +1,7 @@
-import { Button } from '@bitfun/ui';
+import { Button, Icon } from '@bitfun/ui';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
-import {
- Bot,
- ChevronRight,
- CircleAlert,
- LoaderCircle,
- Plus,
- Puzzle,
- Settings2,
- Sparkles,
- Wrench,
-} from 'lucide-react';
+import { Bot, CircleAlert, LoaderCircle, Wrench } from 'lucide-react';
import {
GalleryEmpty,
GalleryLayout,
@@ -222,7 +212,7 @@ const NurseryGallery: React.FC = () => {
title={(
{t('nursery.gallery.title')}
-
+
)}
subtitle={t('nursery.gallery.subtitle')}
@@ -238,7 +228,7 @@ const NurseryGallery: React.FC = () => {
{creating ? (
) : (
-
+
)}
{t(creating ? 'nursery.gallery.creating' : 'nursery.gallery.newAssistant')}
@@ -284,7 +274,7 @@ const NurseryGallery: React.FC = () => {
{t('nursery.template.stats.tools', { count: templateStats.enabledToolCount })}
-
+
{t('nursery.template.stats.skills', { count: templateStats.enabledSkillCount })}
>
@@ -298,15 +288,10 @@ const NurseryGallery: React.FC = () => {
onClick={openDefaults}
>
-
+
{t('nursery.template.configure')}
-
+
@@ -350,7 +335,7 @@ const NurseryGallery: React.FC = () => {
size="sm"
onClick={handleCreateAssistant}
disabled={creating}
- leadingIcon={}
+ leadingIcon={}
>
{t('nursery.gallery.newAssistant')}
diff --git a/src/web-ui/src/app/scenes/profile/views/NurseryView.scss b/src/web-ui/src/app/scenes/profile/views/NurseryView.scss
index eb4dae6e67..d88dd5b5ed 100644
--- a/src/web-ui/src/app/scenes/profile/views/NurseryView.scss
+++ b/src/web-ui/src/app/scenes/profile/views/NurseryView.scss
@@ -710,7 +710,6 @@ $nursery-gallery-gutter: clamp(20px, 2.5vw, 36px);
&__body {
flex: 1;
- overflow-y: auto;
overflow-x: hidden;
min-height: 0;
@@ -728,7 +727,6 @@ $nursery-gallery-gutter: clamp(20px, 2.5vw, 36px);
&__content {
flex: 1;
- overflow-y: auto;
overflow-x: hidden;
padding: $size-gap-6 $nursery-scene-gutter;
display: flex;
@@ -1258,10 +1256,6 @@ $nursery-gallery-gutter: clamp(20px, 2.5vw, 36px);
grid-template-columns: 1fr;
}
- .nursery-page__content:has(.tc-template-shell--has-detail) {
- overflow-y: auto;
- }
-
.tc-template-shell--has-detail {
display: flex;
flex-direction: column;
@@ -1410,7 +1404,6 @@ $nursery-gallery-gutter: clamp(20px, 2.5vw, 36px);
.tc-template-shell__main {
min-height: 0;
- overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
@@ -1702,7 +1695,6 @@ $nursery-gallery-gutter: clamp(20px, 2.5vw, 36px);
flex: 1;
min-height: 0;
overflow-x: hidden;
- overflow-y: auto;
-webkit-overflow-scrolling: touch;
display: flex;
flex-direction: column;
@@ -3035,7 +3027,6 @@ $acp-content-top: clamp(40px, 4.5vh, 52px);
width: 328px;
max-width: calc(100vw - 16px);
max-height: calc(100vh - 16px);
- overflow-y: auto;
padding: $size-gap-3;
color: var(--bf-appearance-token-color-text-primary);
font-family: var(--bf-appearance-token-font-family-sans);
@@ -3162,7 +3153,6 @@ $acp-content-top: clamp(40px, 4.5vh, 52px);
.acp-sessions-area {
flex: 1;
min-height: 0;
- overflow-y: auto;
overflow-x: hidden;
padding: $size-gap-3 $size-gap-6 $size-gap-5;
@@ -3462,7 +3452,6 @@ $acp-content-top: clamp(40px, 4.5vh, 52px);
&__schedule-body {
flex: 1;
min-height: 0;
- overflow-y: auto;
padding: $size-gap-3 $size-gap-4;
&::-webkit-scrollbar {
diff --git a/src/web-ui/src/app/scenes/registry.ts b/src/web-ui/src/app/scenes/registry.ts
index c68e6dbc6e..abf2e50a3f 100644
--- a/src/web-ui/src/app/scenes/registry.ts
+++ b/src/web-ui/src/app/scenes/registry.ts
@@ -7,33 +7,32 @@
* - Scene ids are unique, while Mini Apps use one id per app instance.
*/
-import { SessionIcon } from '@bitfun/ui';
+import React from 'react';
+import { Icon, SessionIcon, type IconName } from '@bitfun/ui';
import {
- MessageSquare,
- Terminal,
- GitBranch,
- Settings,
FileCode2,
CircleUserRound,
Users,
- Puzzle,
Boxes,
PanelsTopLeft,
- Globe,
- User,
BarChart3,
CalendarClock,
- ExternalLink,
Network,
} from 'lucide-react';
-import type { SceneTabDef, SceneTabId } from '../components/SceneBar/types';
+import type { SceneTabDef, SceneTabIcon, SceneTabId } from '../components/SceneBar/types';
+
+function catalogSceneIcon(name: IconName): SceneTabIcon {
+ return function CatalogSceneIcon() {
+ return React.createElement(Icon, { name, size: 'lg', 'aria-hidden': true });
+ };
+}
export const SCENE_TAB_REGISTRY: SceneTabDef[] = [
{
id: 'welcome' as SceneTabId,
label: 'Welcome',
labelKey: 'welcomeScene.tabLabel',
- Icon: MessageSquare,
+ Icon: catalogSceneIcon('side-chat'),
pinned: false,
singleton: true,
defaultOpen: true,
@@ -51,7 +50,7 @@ export const SCENE_TAB_REGISTRY: SceneTabDef[] = [
{
id: 'terminal' as SceneTabId,
label: 'Terminal',
- Icon: Terminal,
+ Icon: catalogSceneIcon('terminal'),
pinned: false,
singleton: true,
defaultOpen: false,
@@ -59,7 +58,7 @@ export const SCENE_TAB_REGISTRY: SceneTabDef[] = [
{
id: 'git' as SceneTabId,
label: 'Git',
- Icon: GitBranch,
+ Icon: catalogSceneIcon('git'),
pinned: false,
singleton: true,
defaultOpen: false,
@@ -68,7 +67,7 @@ export const SCENE_TAB_REGISTRY: SceneTabDef[] = [
id: 'settings' as SceneTabId,
label: 'Settings',
labelKey: 'shared:features.settings',
- Icon: Settings,
+ Icon: catalogSceneIcon('settings'),
pinned: false,
singleton: true,
defaultOpen: false,
@@ -102,7 +101,7 @@ export const SCENE_TAB_REGISTRY: SceneTabDef[] = [
id: 'skills' as SceneTabId,
label: 'Skills',
labelKey: 'scenes.skills',
- Icon: Puzzle,
+ Icon: catalogSceneIcon('extension'),
pinned: false,
singleton: true,
defaultOpen: false,
@@ -137,7 +136,7 @@ export const SCENE_TAB_REGISTRY: SceneTabDef[] = [
id: 'browser' as SceneTabId,
label: 'Browser',
labelKey: 'scenes.browser',
- Icon: Globe,
+ Icon: catalogSceneIcon('browser'),
pinned: false,
singleton: true,
defaultOpen: false,
@@ -146,7 +145,7 @@ export const SCENE_TAB_REGISTRY: SceneTabDef[] = [
id: 'assistant' as SceneTabId,
label: 'Assistant',
labelKey: 'scenes.assistant',
- Icon: User,
+ Icon: catalogSceneIcon('user'),
pinned: false,
singleton: true,
defaultOpen: false,
@@ -173,7 +172,7 @@ export const SCENE_TAB_REGISTRY: SceneTabDef[] = [
id: 'shell' as SceneTabId,
label: 'Shell',
labelKey: 'scenes.shell',
- Icon: Terminal,
+ Icon: catalogSceneIcon('terminal'),
pinned: false,
singleton: true,
defaultOpen: false,
@@ -182,7 +181,7 @@ export const SCENE_TAB_REGISTRY: SceneTabDef[] = [
id: 'panel-view' as SceneTabId,
label: 'Panel View',
labelKey: 'scenes.panelView',
- Icon: ExternalLink,
+ Icon: catalogSceneIcon('arrow-up-right'),
pinned: false,
closable: true,
singleton: true,
@@ -203,7 +202,7 @@ export function getMiniAppSceneDef(appId: string, appName?: string): SceneTabDef
return {
id,
label: appName ?? appId,
- Icon: Puzzle,
+ Icon: catalogSceneIcon('mini-app'),
pinned: false,
closable: true,
singleton: false,
diff --git a/src/web-ui/src/app/scenes/settings/SettingsNav.appearance.ts b/src/web-ui/src/app/scenes/settings/SettingsNav.appearance.ts
index f7a615b167..e8f4292756 100644
--- a/src/web-ui/src/app/scenes/settings/SettingsNav.appearance.ts
+++ b/src/web-ui/src/app/scenes/settings/SettingsNav.appearance.ts
@@ -6,7 +6,6 @@ export const settingsNavAppearanceDescriptor: AppearanceSurfaceDescriptor = {
{ id: 'root', propertyProfile: 'layout', visualRole: 'panel', continuityGroup: 'settings-workspace' },
{ id: 'header', visualRole: 'toolbar', continuityGroup: 'settings-workspace' },
{ id: 'search', propertyProfile: 'control', visualRole: 'control' },
- { id: 'sections', visualRole: 'content', continuityGroup: 'settings-workspace' },
{ id: 'searchEmpty', visualRole: 'content' },
{ id: 'searchResults', visualRole: 'content' },
{ id: 'searchResult', propertyProfile: 'control', visualRole: 'control' },
diff --git a/src/web-ui/src/app/scenes/settings/SettingsNav.scss b/src/web-ui/src/app/scenes/settings/SettingsNav.scss
index 4d2d1a94ce..0bcf3a6c8b 100644
--- a/src/web-ui/src/app/scenes/settings/SettingsNav.scss
+++ b/src/web-ui/src/app/scenes/settings/SettingsNav.scss
@@ -15,11 +15,23 @@
flex-direction: column;
gap: $size-gap-4;
width: 100%;
+ inline-size: 100%;
+ max-inline-size: none;
height: 100%;
padding: $size-gap-2;
box-sizing: border-box;
overflow: hidden;
line-height: 1.35;
+ background: transparent;
+
+ > [data-bf-part="header"] {
+ padding: 0;
+ }
+
+ [data-bf-part="content"] {
+ padding: 0;
+ gap: 0;
+ }
// ── Header: title (back/forward lives in NavBar) ──────
@@ -146,16 +158,7 @@
display: flex;
flex-direction: column;
gap: $size-gap-4;
- overflow-y: auto;
- overflow-x: hidden;
padding: 0;
-
- &::-webkit-scrollbar { width: 6px; }
- &::-webkit-scrollbar-track { background: transparent; }
- &::-webkit-scrollbar-thumb {
- background: color-mix(in srgb, var(--bf-appearance-token-color-text-primary) 10%, transparent);
- border-radius: $size-radius-full;
- }
}
// ── Category group ─────────────────────────────────────
diff --git a/src/web-ui/src/app/scenes/settings/SettingsNav.tsx b/src/web-ui/src/app/scenes/settings/SettingsNav.tsx
index 2c36e92c8b..b9375aec2d 100644
--- a/src/web-ui/src/app/scenes/settings/SettingsNav.tsx
+++ b/src/web-ui/src/app/scenes/settings/SettingsNav.tsx
@@ -8,8 +8,7 @@ import React, {
} from 'react';
import type { i18n as I18nApi } from 'i18next';
import { useTranslation } from 'react-i18next';
-import { Search as SearchIcon } from 'lucide-react';
-import { SearchField } from '@bitfun/ui';
+import { Icon, NavigationPanel, NavigationPanelItem, NavigationPanelSection, SearchField } from '@bitfun/ui';
import { useI18n } from '@/infrastructure/i18n/hooks/useI18n';
import { getInteractionMotion } from '@/shared/utils/motionPreference';
import {
@@ -205,121 +204,137 @@ const SettingsNav: React.FC = () => {
}, [activate, clearSearch, highlightedIndex, results]);
return (
-
-
- {t('shared:features.settings')}
-
-
-
- {
- clearSearch();
- searchInputRef.current?.focus();
- } : undefined}
- clearLabel={draftQuery ? tComponents('search.clear') : undefined}
- onKeyDown={handleSearchKeyDown}
- leadingIcon={}
- placeholder={t('navigation.search.placeholder')}
- aria-label={t('navigation.search.placeholder')}
- aria-controls="settings-nav-results"
- aria-expanded={isSearchMode}
- />
-
-
-
= 0
- ? `settings-nav-result-${highlightedIndex}`
- : undefined}
- >
- {isSearchMode ? (
- results.length ? (
-
- {results.map((row, index) => {
- const active = activePageId === row.destination.pageId
- && (!row.destination.viewId || row.destination.viewId === activeViewId);
- const selected = index === highlightedIndex;
- const path = [row.categoryLabel, row.pageLabel, row.viewLabel].filter(Boolean).join(' › ');
- return (
- activate(row.destination, true)}
- onMouseEnter={() => {
- setHighlightedIndex(index);
- preload(row.destination.pageId);
- }}
- onFocus={() => preload(row.destination.pageId)}
- >
-
- {highlightFirstMatch(path, searchQuery)}
-
-
- {highlightFirstMatch(row.description, searchQuery)}
-
-
- );
- })}
-
- ) : (
-
- {t('navigation.search.empty')}
-
- )
- ) : SETTINGS_CATEGORIES.map((category) => (
-
-
- {t(category.labelKey)}
-
-
- {category.pages.map((page) => (
-
+
+ {t('shared:features.settings')}
+
+
+ {
+ clearSearch();
+ searchInputRef.current?.focus();
+ } : undefined}
+ clearLabel={draftQuery ? tComponents('search.clear') : undefined}
+ onKeyDown={handleSearchKeyDown}
+ leadingIcon={}
+ placeholder={t('navigation.search.placeholder')}
+ aria-label={t('navigation.search.placeholder')}
+ aria-controls="settings-nav-results"
+ aria-expanded={isSearchMode}
+ />
+
+ >
+ )}
+ >
+ {isSearchMode ? (
+ results.length ? (
+ = 0
+ ? `settings-nav-result-${highlightedIndex}`
+ : undefined}
+ >
+ {results.map((row, index) => {
+ const active = activePageId === row.destination.pageId
+ && (!row.destination.viewId || row.destination.viewId === activeViewId);
+ const selected = index === highlightedIndex;
+ const path = [row.categoryLabel, row.pageLabel, row.viewLabel].filter(Boolean).join(' › ');
+ return (
+ activate({ pageId: page.id })}
- onPointerEnter={() => preload(page.id)}
- onFocus={() => preload(page.id)}
+ onClick={() => activate(row.destination, true)}
+ onMouseEnter={() => {
+ setHighlightedIndex(index);
+ preload(row.destination.pageId);
+ }}
+ onFocus={() => preload(row.destination.pageId)}
>
- {t(page.labelKey)}
-
- ))}
-
+
+ {highlightFirstMatch(path, searchQuery)}
+
+
+ {highlightFirstMatch(row.description, searchQuery)}
+
+
+ );
+ })}
+
+ ) : (
+
+ {t('navigation.search.empty')}
+
+ )
+ ) : SETTINGS_CATEGORIES.map((category) => (
+
+ {t(category.labelKey)}
+
+ )}
+ >
+
+ {category.pages.map((page) => (
+ activate({ pageId: page.id })}
+ onPointerEnter={() => preload(page.id)}
+ onFocus={() => preload(page.id)}
+ >
+ {t(page.labelKey)}
+
+ ))}
- ))}
-
-
+
+ ))}
+
);
};
diff --git a/src/web-ui/src/app/scenes/settings/components/ArchivedSessionsConfig.tsx b/src/web-ui/src/app/scenes/settings/components/ArchivedSessionsConfig.tsx
index d11149b3e7..becbd1cf9f 100644
--- a/src/web-ui/src/app/scenes/settings/components/ArchivedSessionsConfig.tsx
+++ b/src/web-ui/src/app/scenes/settings/components/ArchivedSessionsConfig.tsx
@@ -7,9 +7,9 @@
* confirmation dialog.
*/
-import { Button, IconButton } from '@bitfun/ui';
+import { Button, Icon, IconButton } from '@bitfun/ui';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
-import { Trash2, RotateCcw, Inbox, RefreshCw, ChevronRight, ChevronDown } from 'lucide-react';
+import { Trash2, RotateCcw, Inbox, RefreshCw } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import {
ConfigPageLayout,
@@ -360,9 +360,9 @@ const ArchivedSessionsConfig: React.FC = () => {
onClick={() => toggleWorkspace(workspacePath)}
>
{isCollapsed ? (
-
+
) : (
-
+
)}
{workspacePath}
diff --git a/src/web-ui/src/app/scenes/settings/components/KeyboardShortcutsTab.tsx b/src/web-ui/src/app/scenes/settings/components/KeyboardShortcutsTab.tsx
index 7c5d2ac70f..8f3161b542 100644
--- a/src/web-ui/src/app/scenes/settings/components/KeyboardShortcutsTab.tsx
+++ b/src/web-ui/src/app/scenes/settings/components/KeyboardShortcutsTab.tsx
@@ -10,9 +10,9 @@
* - Reset button restores all defaults
*/
-import { Button, SearchField, Tooltip } from '@bitfun/ui';
+import { Button, Icon, SearchField, Tooltip } from '@bitfun/ui';
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react';
-import { Search as SearchIcon } from 'lucide-react';
+;
import { useI18n } from '@/infrastructure/i18n';
import { ConfigPageLayout, ConfigPageHeader, ConfigPageContent, ConfigPageSection } from '@/infrastructure/config/components/common';
@@ -566,7 +566,7 @@ const KeyboardShortcutsTab: React.FC = () => {
size="sm"
value={searchQuery}
onValueChange={setSearchQuery}
- leadingIcon={}
+ leadingIcon={}
placeholder={t('keyboard.search')}
aria-label={t('keyboard.search')}
clearLabel={searchQuery ? tComponents('search.clear') : undefined}
diff --git a/src/web-ui/src/app/scenes/settings/pages/AutomationSettingsPage.scss b/src/web-ui/src/app/scenes/settings/pages/AutomationSettingsPage.scss
index 776f345f1b..2bc10e55a5 100644
--- a/src/web-ui/src/app/scenes/settings/pages/AutomationSettingsPage.scss
+++ b/src/web-ui/src/app/scenes/settings/pages/AutomationSettingsPage.scss
@@ -2,8 +2,7 @@
flex: 1 1 auto;
min-width: 0;
min-height: 0;
- overflow-x: hidden;
- overflow-y: auto;
+ /* ScrollArea owns overflow. */
background: var(--bf-appearance-token-color-bg-scene);
&__section {
diff --git a/src/web-ui/src/app/scenes/settings/pages/AutomationSettingsPage.tsx b/src/web-ui/src/app/scenes/settings/pages/AutomationSettingsPage.tsx
index 378f35d5db..31b9c79e52 100644
--- a/src/web-ui/src/app/scenes/settings/pages/AutomationSettingsPage.tsx
+++ b/src/web-ui/src/app/scenes/settings/pages/AutomationSettingsPage.tsx
@@ -1,3 +1,4 @@
+import { ScrollArea } from '@bitfun/ui';
import React, { lazy, Suspense, useEffect, useRef } from 'react';
import type { SettingsPageProps } from '../settingsTypes';
import './AutomationSettingsPage.scss';
@@ -25,7 +26,7 @@ const AutomationSettingsPage: React.FC = ({ viewId, navigatio
}, [navigationRequestId, viewId]);
return (
- = ({ viewId, navigatio
-
+
);
};
diff --git a/src/web-ui/src/app/scenes/settings/pages/DevelopmentSettingsPage.scss b/src/web-ui/src/app/scenes/settings/pages/DevelopmentSettingsPage.scss
index 888d4cee92..e388bc28b1 100644
--- a/src/web-ui/src/app/scenes/settings/pages/DevelopmentSettingsPage.scss
+++ b/src/web-ui/src/app/scenes/settings/pages/DevelopmentSettingsPage.scss
@@ -3,7 +3,6 @@
min-width: 0;
min-height: 0;
overflow-x: hidden;
- overflow-y: auto;
background: var(--bf-appearance-token-color-bg-scene);
&__section {
diff --git a/src/web-ui/src/app/scenes/settings/pages/DevelopmentSettingsPage.tsx b/src/web-ui/src/app/scenes/settings/pages/DevelopmentSettingsPage.tsx
index ddf8c3280e..6db527b574 100644
--- a/src/web-ui/src/app/scenes/settings/pages/DevelopmentSettingsPage.tsx
+++ b/src/web-ui/src/app/scenes/settings/pages/DevelopmentSettingsPage.tsx
@@ -1,3 +1,4 @@
+import { ScrollArea } from '@bitfun/ui';
import React, { lazy, Suspense, useEffect, useRef } from 'react';
import type { SettingsPageProps } from '../settingsTypes';
import './DevelopmentSettingsPage.scss';
@@ -27,7 +28,7 @@ const DevelopmentSettingsPage: React.FC = ({ viewId, navigati
}, [navigationRequestId, viewId]);
return (
- = ({ viewId, navigati
-
+
);
};
diff --git a/src/web-ui/src/app/scenes/settings/pages/InputSettingsPage.scss b/src/web-ui/src/app/scenes/settings/pages/InputSettingsPage.scss
index df6c006099..d7316b3f21 100644
--- a/src/web-ui/src/app/scenes/settings/pages/InputSettingsPage.scss
+++ b/src/web-ui/src/app/scenes/settings/pages/InputSettingsPage.scss
@@ -3,7 +3,6 @@
min-width: 0;
min-height: 0;
overflow-x: hidden;
- overflow-y: auto;
background: var(--bf-appearance-token-color-bg-scene);
&__section {
diff --git a/src/web-ui/src/app/scenes/settings/pages/InputSettingsPage.tsx b/src/web-ui/src/app/scenes/settings/pages/InputSettingsPage.tsx
index f9768a4dd3..f336cb30d7 100644
--- a/src/web-ui/src/app/scenes/settings/pages/InputSettingsPage.tsx
+++ b/src/web-ui/src/app/scenes/settings/pages/InputSettingsPage.tsx
@@ -1,3 +1,4 @@
+import { ScrollArea } from '@bitfun/ui';
import React, { lazy, Suspense, useEffect, useRef } from 'react';
import type { SettingsPageProps } from '../settingsTypes';
import './InputSettingsPage.scss';
@@ -25,7 +26,7 @@ const InputSettingsPage: React.FC = ({ viewId, navigationRequ
}, [navigationRequestId, viewId]);
return (
- = ({ viewId, navigationRequ
-
+
);
};
diff --git a/src/web-ui/src/app/scenes/shell/ShellNav.appearance.ts b/src/web-ui/src/app/scenes/shell/ShellNav.appearance.ts
index c6af8053f4..7236ed56cf 100644
--- a/src/web-ui/src/app/scenes/shell/ShellNav.appearance.ts
+++ b/src/web-ui/src/app/scenes/shell/ShellNav.appearance.ts
@@ -4,7 +4,7 @@ export const shellNavAppearanceDescriptor: AppearanceSurfaceDescriptor = {
parts: [
{ id: 'root' }, { id: 'header' }, { id: 'title' }, { id: 'headerActions' },
{ id: 'splitButton' }, { id: 'menu' }, { id: 'menuItem' },
- { id: 'content' }, { id: 'empty' }, { id: 'list' },
+ { id: 'empty' }, { id: 'list' },
],
states: [
{ id: 'active', selector: { kind: 'self', suffix: '[data-bf-state~="active"]' } },
diff --git a/src/web-ui/src/app/scenes/shell/ShellNav.scss b/src/web-ui/src/app/scenes/shell/ShellNav.scss
index acb7f06aae..e6932246b4 100644
--- a/src/web-ui/src/app/scenes/shell/ShellNav.scss
+++ b/src/web-ui/src/app/scenes/shell/ShellNav.scss
@@ -5,8 +5,20 @@
display: flex;
flex-direction: column;
width: 100%;
+ inline-size: 100%;
+ max-inline-size: none;
height: 100%;
overflow: hidden;
+ background: transparent;
+
+ > [data-bf-part="header"] {
+ padding: 0;
+ }
+
+ [data-bf-part="content"] {
+ padding: 0;
+ gap: 0;
+ }
&__header {
display: flex;
@@ -177,7 +189,6 @@
min-width: 200px;
max-width: min(280px, calc(100vw - 24px));
max-height: calc(100vh - 16px);
- overflow-y: auto;
padding: 6px;
z-index: $z-popover;
color: var(--bf-appearance-token-color-text-primary);
@@ -230,17 +241,8 @@
&__sections {
flex: 1 1 auto;
min-height: 0;
- overflow-y: auto;
- overflow-x: hidden;
padding: 0 0 $size-gap-3;
- &::-webkit-scrollbar { width: 3px; }
- &::-webkit-scrollbar-track { background: transparent; }
- &::-webkit-scrollbar-thumb {
- background: var(--bf-appearance-token-border-subtle);
- border-radius: 2px;
- }
-
&--empty {
display: flex;
flex-direction: column;
diff --git a/src/web-ui/src/app/scenes/shell/ShellNav.tsx b/src/web-ui/src/app/scenes/shell/ShellNav.tsx
index cd605b392f..73e0fa2d56 100644
--- a/src/web-ui/src/app/scenes/shell/ShellNav.tsx
+++ b/src/web-ui/src/app/scenes/shell/ShellNav.tsx
@@ -1,11 +1,8 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { createPortal } from 'react-dom';
import {
- Plus,
- ChevronDown,
RefreshCw,
Play,
- Pencil,
Square,
Trash2,
} from 'lucide-react';
@@ -24,7 +21,7 @@ import type { ShellInfo } from '@/tools/terminal/types/session';
import { useShellEntries } from './hooks';
import type { ShellEntry } from './hooks/shellEntryTypes';
import { useShellNavMenuState } from './hooks/useShellNavMenuState';
-import { Button, Tooltip } from '@bitfun/ui';
+import { Button, Icon, NavigationPanel, ScrollArea, Tooltip } from '@bitfun/ui';
import ShellNavEntryItem from './components/ShellNavEntryItem';
import ShellNavWorkspaceSwitcher from './components/ShellNavWorkspaceSwitcher';
import { getAppearanceOverlayHost } from '@/infrastructure/appearance/runtime/AppearanceOverlayHost';
@@ -200,7 +197,7 @@ const ShellNav: React.FC = () => {
{
id: `edit-${entry.sessionId}`,
label: t('nav.shell.context.editConfig'),
- icon: ,
+ icon: ,
onClick: () => {
openEditModal(entry);
},
@@ -223,7 +220,7 @@ const ShellNav: React.FC = () => {
return [{
id: `config-${entry.sessionId}`,
label: t('nav.shell.context.saveConfig'),
- icon: ,
+ icon: ,
onClick: () => {
openEditModal(entry);
},
@@ -255,7 +252,7 @@ const ShellNav: React.FC = () => {
}, [deleteEntry, t]);
return (
-
+
{t('nav.shell.title')}
@@ -281,7 +278,7 @@ const ShellNav: React.FC = () => {
className="bitfun-shell-nav__split-button-main"
onClick={() => { void handleCreateManualTerminal(); }}
>
-
+
@@ -292,13 +289,13 @@ const ShellNav: React.FC = () => {
aria-haspopup="menu"
aria-expanded={menuOpen}
>
-
+
{menuOpen ? createPortal(
- {
role="menuitem"
onClick={() => { void handleCreateManualTerminal(shell.shellType); }}
>
-
+
{shell.label}
))}
@@ -330,17 +327,12 @@ const ShellNav: React.FC = () => {
{t('nav.shell.actions.refresh')}
-
,
+ ,
getAppearanceOverlayHost(),
) : null}
-
-
+ )} bodyClassName={`bitfun-shell-nav__sections${!hasVisibleContent ? ' bitfun-shell-nav__sections--empty' : ''}`}>
{hasVisibleContent ? (
{entries.map((entry) => (
@@ -368,15 +360,13 @@ const ShellNav: React.FC = () => {
variant="outline"
size="sm"
onClick={() => { void handleCreateManualTerminal(); }}
- leadingIcon={
}
+ leadingIcon={
}
>
{t('nav.shell.empty.quickNew')}
)}
-
-
{editingTerminal ? (
{
showStartupCommand
/>
) : null}
-
+
);
};
diff --git a/src/web-ui/src/app/scenes/shell/components/ShellNavWorkspaceSwitcher.tsx b/src/web-ui/src/app/scenes/shell/components/ShellNavWorkspaceSwitcher.tsx
index a4bf4cd7ed..3bc43337dc 100644
--- a/src/web-ui/src/app/scenes/shell/components/ShellNavWorkspaceSwitcher.tsx
+++ b/src/web-ui/src/app/scenes/shell/components/ShellNavWorkspaceSwitcher.tsx
@@ -1,8 +1,8 @@
import React from 'react';
import { createPortal } from 'react-dom';
import { getAppearanceOverlayHost } from '@/infrastructure/appearance/runtime/AppearanceOverlayHost';
-import { Check, ChevronDown } from 'lucide-react';
-import { Menu, MenuItem, Tooltip } from '@bitfun/ui';
+;
+import { Icon, Menu, MenuItem, Tooltip } from '@bitfun/ui';
import { WorkspaceKind, type WorkspaceInfo } from '@/shared/types';
interface ShellNavWorkspaceSwitcherProps {
@@ -59,7 +59,7 @@ const ShellNavWorkspaceSwitcher: React.FC = ({
/
{workspaceName}
{hasMultipleWorkspaces ? (
-
+
) : null}
@@ -92,7 +92,7 @@ const ShellNavWorkspaceSwitcher: React.FC = ({
reserveLeadingSpace
leading={isActive ? (
-
+
) : undefined}
onClick={() => { void onSelectWorkspace(workspace.id); }}
diff --git a/src/web-ui/src/app/scenes/skills/SkillsScene.scss b/src/web-ui/src/app/scenes/skills/SkillsScene.scss
index 9328ef43bc..b5cda19649 100644
--- a/src/web-ui/src/app/scenes/skills/SkillsScene.scss
+++ b/src/web-ui/src/app/scenes/skills/SkillsScene.scss
@@ -116,7 +116,6 @@ $skills-installed-columns: minmax(220px, 1fr) minmax(82px, 0.24fr) minmax(96px,
flex-direction: column;
border-right: 1px solid var(--bf-appearance-token-border-subtle);
background: var(--bf-appearance-token-color-bg-primary);
- overflow-y: auto;
}
.skills-sidebar__header {
@@ -459,14 +458,10 @@ $skills-installed-columns: minmax(220px, 1fr) minmax(82px, 0.24fr) minmax(96px,
.skills-suite__sections {
flex: 1;
min-height: 0;
- overflow-y: auto;
display: flex;
flex-direction: column;
gap: 16px;
padding-right: 4px;
-
- &::-webkit-scrollbar { width: 5px; }
- &::-webkit-scrollbar-thumb { background: var(--bf-appearance-token-border-subtle); border-radius: 3px; }
}
.skills-suite__section {
@@ -699,13 +694,9 @@ $skills-installed-columns: minmax(220px, 1fr) minmax(82px, 0.24fr) minmax(96px,
.skills-main__loading {
flex: 0 1 auto;
min-height: 0;
- overflow-y: auto;
border: 1px solid var(--bf-appearance-token-border-subtle);
border-radius: $size-radius-lg;
background: var(--bf-appearance-token-color-bg-secondary);
-
- &::-webkit-scrollbar { width: 5px; }
- &::-webkit-scrollbar-thumb { background: var(--bf-appearance-token-border-subtle); border-radius: 3px; }
}
.skills-main__grid {
@@ -1026,11 +1017,7 @@ $skills-installed-columns: minmax(220px, 1fr) minmax(82px, 0.24fr) minmax(96px,
.skills-discover__content {
flex: 1;
min-height: 0;
- overflow-y: auto;
padding: 14px 16px;
-
- &::-webkit-scrollbar { width: 6px; }
- &::-webkit-scrollbar-thumb { background: var(--bf-appearance-token-border-subtle); border-radius: 3px; }
}
.skills-discover__results-info {
diff --git a/src/web-ui/src/app/scenes/skills/SkillsScene.tsx b/src/web-ui/src/app/scenes/skills/SkillsScene.tsx
index b7052150f4..03a57c186e 100644
--- a/src/web-ui/src/app/scenes/skills/SkillsScene.tsx
+++ b/src/web-ui/src/app/scenes/skills/SkillsScene.tsx
@@ -1,25 +1,6 @@
-import { Button, Switch, IconButton, Input, Modal, ConfirmDialog, Field, SearchField, Select, StatusPill } from '@bitfun/ui';
+import { Button, ConfirmDialog, Field, Icon, IconButton, Input, Modal, ScrollArea, SearchField, Select, StatusPill, Switch } from '@bitfun/ui';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
-import {
- ArrowRight,
- CheckCircle2,
- ChevronLeft,
- ChevronRight,
- Download,
- Filter,
- FolderOpen,
- Layers,
- Package,
- Plus,
- Puzzle,
- Search as SearchIcon,
- ShieldAlert,
- ShieldCheck,
- Trash2,
- TrendingUp,
- User,
- Zap,
-} from 'lucide-react';
+import { ChevronLeft, FolderOpen, Layers, Package, ShieldAlert, ShieldCheck, Trash2, TrendingUp, Zap } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { useI18n } from '@/infrastructure/i18n/hooks/useI18n';
@@ -76,7 +57,7 @@ const CATEGORIES: CategoryInfo[] = [
},
{
id: 'user',
- icon: ,
+ icon: ,
labelKey: 'filters.user',
titleKey: 'installed.titleUser',
descKey: 'categories.user',
@@ -276,7 +257,7 @@ const SkillsScene: React.FC = () => {
{activeTab === 'installed' && (
- {desktopConfigAvailable &&
- }
+ }
{!desktopConfigAvailable ? (
@@ -327,7 +308,7 @@ const SkillsScene: React.FC = () => {
className="skills-main__toolbar-search"
value={installedSearch}
onValueChange={setInstalledSearch}
- leadingIcon={}
+ leadingIcon={}
placeholder={t('toolbar.searchPlaceholder')}
aria-label={t('toolbar.searchPlaceholder')}
size="sm"
@@ -342,13 +323,13 @@ const SkillsScene: React.FC = () => {
data-bf-part="filterAction"
data-bf-state={hideDuplicates ? 'active' : undefined}
>
-
+
{t('toolbar.hideDuplicates')}
}
+ leadingIcon={}
onClick={toggleAddForm}
data-testid="skills-add-skill-btn"
>
@@ -389,7 +370,7 @@ const SkillsScene: React.FC = () => {
{installed.loading && (
-
+
{Array.from({ length: 8 }).map((_, i) => (
{
data-bf-part="skeleton"
/>
))}
-
+
)}
{!installed.loading && installed.error && (
@@ -471,7 +452,7 @@ const SkillsScene: React.FC = () => {
>
{skill.name}
@@ -526,7 +507,7 @@ const SkillsScene: React.FC = () => {
data-bf-part="installedCardLevel"
>
{skill.level === 'user'
- ?
+ ?
:
}
{market.isRemoteWorkspace
@@ -575,7 +556,7 @@ const SkillsScene: React.FC = () => {
title={t('list.item.detail')}
data-bf-scene="skills"
data-bf-part="installedCardDetails"
- icon={}
+ icon={}
/>
{canDeleteSkill(skill) && (
{
value={searchDraft}
onValueChange={setSearchDraft}
onSearch={submitMarketQuery}
- leadingIcon={}
+ leadingIcon={}
placeholder={t('market.searchPlaceholder')}
aria-label={t('market.searchPlaceholder')}
size="md"
@@ -630,7 +611,7 @@ const SkillsScene: React.FC = () => {
-
+
{market.marketLoading && (
{Array.from({ length: 12 }).map((_, i) => (
@@ -701,7 +682,7 @@ const SkillsScene: React.FC = () => {
accentSeed={skill.installId}
iconKind="market"
badges={isInstalled ? (
- }>
+ }>
{t('market.item.installed')}
) : null}
@@ -714,7 +695,7 @@ const SkillsScene: React.FC = () => {
actions={[
{
id: 'download',
- icon: isInstalled ? : ,
+ icon: isInstalled ? : ,
ariaLabel: isInstalled ? t('market.item.installed') : t('market.item.downloadProject'),
title: isDownloading
? t('market.item.downloading')
@@ -761,13 +742,13 @@ const SkillsScene: React.FC = () => {
data-bf-scene="skills"
data-bf-part="pageButton"
>
-
+
)}
>
)}
-
+
)}
@@ -775,7 +756,7 @@ const SkillsScene: React.FC = () => {
setSelectedDetail(null)}
- icon={selectedMarketSkill ? : }
+ icon={selectedMarketSkill ? : }
iconGradient={getCardGradient(
selectedInstalledSkill?.name
?? selectedMarketSkill?.installId
@@ -812,7 +793,7 @@ const SkillsScene: React.FC = () => {
>
) : selectedMarketSkill && installedSkillNames.has(selectedMarketSkill.name) ? (
- }>
+ }>
{t('market.item.installed')}
) : null}
diff --git a/src/web-ui/src/app/scenes/skills/appearance.ts b/src/web-ui/src/app/scenes/skills/appearance.ts
index 8bc8c0021d..fcf56384f6 100644
--- a/src/web-ui/src/app/scenes/skills/appearance.ts
+++ b/src/web-ui/src/app/scenes/skills/appearance.ts
@@ -19,11 +19,11 @@ export const skillsAppearanceDescriptor: AppearanceSurfaceDescriptor = {
{ id: 'pagination' }, { id: 'pageButton' }, { id: 'pageInfo' },
{ id: 'discover' }, { id: 'discoverHero' }, { id: 'discoverHeroContent' },
{ id: 'discoverTitle' }, { id: 'discoverSubtitle' }, { id: 'discoverSearch' },
- { id: 'discoverContent' }, { id: 'resultsInfo' },
+ { id: 'resultsInfo' },
{ id: 'suite' }, { id: 'suiteHero' }, { id: 'suiteHeroCopy' }, { id: 'suiteHeroActions' },
{ id: 'suiteModeToolbar' }, { id: 'suiteModes' }, { id: 'suiteModeTab' },
{ id: 'suiteModeTabLabel' }, { id: 'suiteLoading' },
- { id: 'suiteSections' }, { id: 'suiteSection' }, { id: 'suiteSectionLabel' },
+ { id: 'suiteSection' }, { id: 'suiteSectionLabel' },
{ id: 'suiteGrid' }, { id: 'suiteGroupCard' }, { id: 'suiteGroupHead' },
{ id: 'suiteGroupTitleWrap' }, { id: 'suiteGroupTitleRow' }, { id: 'suiteGroupTitle' },
{ id: 'suiteGroupCount' }, { id: 'suiteGroupActions' }, { id: 'suiteSkills' },
diff --git a/src/web-ui/src/app/scenes/skills/components/SkillCard.tsx b/src/web-ui/src/app/scenes/skills/components/SkillCard.tsx
index 8ff4c4b8d3..6473472a0b 100644
--- a/src/web-ui/src/app/scenes/skills/components/SkillCard.tsx
+++ b/src/web-ui/src/app/scenes/skills/components/SkillCard.tsx
@@ -1,5 +1,6 @@
import React from 'react';
-import { Package, Puzzle } from 'lucide-react';
+import { Icon } from '@bitfun/ui';
+import { Package } from 'lucide-react';
import { getCardGradient } from '@/shared/utils/cardGradients';
import './SkillCard.scss';
@@ -41,7 +42,9 @@ const SkillCard: React.FC = ({
style,
...rootProps
}) => {
- const Icon = iconKind === 'market' ? Package : Puzzle;
+ const glyph = iconKind === 'market'
+ ?
+ : ;
const openDetails = () => onOpenDetails?.();
return (
@@ -68,7 +71,7 @@ const SkillCard: React.FC = ({
{badges &&
{badges}
}
diff --git a/src/web-ui/src/app/scenes/skills/components/SkillsSuiteView.tsx b/src/web-ui/src/app/scenes/skills/components/SkillsSuiteView.tsx
index 875a8c7ef9..2fa57484f0 100644
--- a/src/web-ui/src/app/scenes/skills/components/SkillsSuiteView.tsx
+++ b/src/web-ui/src/app/scenes/skills/components/SkillsSuiteView.tsx
@@ -1,6 +1,6 @@
-import { Button, IconButton, StatusPill } from '@bitfun/ui';
+import { Button, Icon, IconButton, ScrollArea, StatusPill } from '@bitfun/ui';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
-import { Package, RefreshCw, RotateCcw, Settings2, ShieldAlert, ShieldCheck } from 'lucide-react';
+import { Package, RefreshCw, RotateCcw, ShieldAlert, ShieldCheck } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { confirmDialog } from '@/infrastructure/confirm-dialog';
import { configAPI } from '@/infrastructure/api';
@@ -415,7 +415,7 @@ const SkillsSuiteView: React.FC = () => {
}
+ leadingIcon={
}
onClick={() => setIsGroupManagerOpen(true)}
disabled={isSaving}
>
@@ -492,14 +492,11 @@ const SkillsSuiteView: React.FC = () => {
)}
{!loading && !error && suiteGroups.length > 0 && (
-
{suiteSections.map(([sectionLabel, sectionGroups]) => (
@@ -644,7 +641,7 @@ const SkillsSuiteView: React.FC = () => {
))}
-
+
)}
{
}, [clearSelectedDay, nowMs]);
return (
- {
type="button"
size="sm"
aria-label={t('calendar.nextMonth')}
- icon={
}
+ icon={
}
onClick={() => shiftMonth(1)}
data-testid="todos-calendar-next"
/>
@@ -463,7 +461,7 @@ const TodosScene: React.FC = () => {
size="sm"
variant="fill"
className="bf-todos__new-button"
- leadingIcon={
}
+ leadingIcon={
}
onClick={handleCreateNew}
disabled={workspaceOptions.length === 0}
data-testid="todos-new"
@@ -508,12 +506,13 @@ const TodosScene: React.FC = () => {
{/* ── Tier 1: due within 24 hours ───────────────────── */}
-
) : null}
-
+
{/* ── Tier 2: more than 24 hours out ────────────────── */}
{
exitDurationMs={160}
minimumExitDurationMs={160}
>
-
{
)}
-
+
-
+
);
};
diff --git a/src/web-ui/src/app/scenes/todos/components/TodoCalendar.tsx b/src/web-ui/src/app/scenes/todos/components/TodoCalendar.tsx
index 54d5de7856..2fe0e2a349 100644
--- a/src/web-ui/src/app/scenes/todos/components/TodoCalendar.tsx
+++ b/src/web-ui/src/app/scenes/todos/components/TodoCalendar.tsx
@@ -7,6 +7,7 @@
* Jobs with nothing left to run are the only ones left out.
*/
+import { ScrollArea } from '@bitfun/ui';
import React, { useMemo } from 'react';
import { useI18n } from '@/infrastructure/i18n';
import {
@@ -52,7 +53,7 @@ const TodoCalendar: React.FC = ({
);
return (
-
+
);
};
diff --git a/src/web-ui/src/app/scenes/todos/components/TodoEditor.tsx b/src/web-ui/src/app/scenes/todos/components/TodoEditor.tsx
index d4afdb8474..a54f3b72f6 100644
--- a/src/web-ui/src/app/scenes/todos/components/TodoEditor.tsx
+++ b/src/web-ui/src/app/scenes/todos/components/TodoEditor.tsx
@@ -6,18 +6,9 @@
* scheduled without knowing which workspace it runs in.
*/
-import { Button, Switch, Input, Select } from '@bitfun/ui';
+import { Button, Icon, Input, Select, Switch, ScrollArea } from '@bitfun/ui';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
-import {
- Bot,
- CalendarClock,
- ChevronDown,
- ClipboardList,
- Clock3,
- Folder,
- RefreshCw,
- Sparkles,
-} from 'lucide-react';
+import { Bot, CalendarClock, ClipboardList, RefreshCw } from 'lucide-react';
import { Select as LegacySelect, Textarea } from '@/component-library';
import { agentAPI, type ModeInfo } from '@/infrastructure/api/service-api/AgentAPI';
import { useI18n } from '@/infrastructure/i18n';
@@ -203,7 +194,7 @@ const TodoEditor: React.FC = ({
- = ({
-
+
{t('shared:features.workspace')}
= ({
-
+
{draft.scheduleKind === 'at'
? t('editor.fields.at')
: draft.scheduleKind === 'every'
@@ -416,7 +407,7 @@ const TodoEditor: React.FC = ({
-
+
{t('editor.smartExecution.title')}
@@ -467,7 +458,7 @@ const TodoEditor: React.FC = ({
data-testid="todos-editor-advanced-toggle"
>
{t('editor.advanced.title')}
-
+
{advancedOpen ? (
@@ -475,7 +466,7 @@ const TodoEditor: React.FC = ({
{draft.scheduleKind === 'every' ? (
-
+
{t('editor.fields.anchor')}
= ({
{draft.scheduleKind === 'cron' ? (
-
+
{t('editor.fields.timezone')}
= ({
{t('editor.noWorkspace')}
) : null}
-
+
- = ({
{preparationPhase ? (
) : (
-
+
)}
{preparationPhase === 'installing'
? t('dispatch.installingCli')
@@ -628,7 +628,7 @@ export const DispatchInstallDialog: React.FC = ({
-
+
({
}));
vi.mock('@bitfun/ui', () => ({
+ ScrollArea: ({ children, ...props }: React.HTMLAttributes
) => {children}
,
Button: ({
children,
disabled,
diff --git a/src/web-ui/src/features/dispatch/DispatchResultDialog.tsx b/src/web-ui/src/features/dispatch/DispatchResultDialog.tsx
index 954d707d72..72c05f4eb2 100644
--- a/src/web-ui/src/features/dispatch/DispatchResultDialog.tsx
+++ b/src/web-ui/src/features/dispatch/DispatchResultDialog.tsx
@@ -1,4 +1,4 @@
-import { Button, Modal } from '@bitfun/ui';
+import { Button, Modal, ScrollArea } from '@bitfun/ui';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { Alert} from '@/component-library';
import { useI18n } from '@/infrastructure/i18n';
@@ -99,7 +99,7 @@ export const DispatchResultDialog: React.FC = ({
- = ({
{result.changes.length}
{result.changes.length > 0 ? (
-
- {result.changes.map(change => (
- -
- {change.status}
- {change.path}
-
- ))}
-
+
+
+ {result.changes.map(change => (
+ -
+ {change.status}
+ {change.path}
+
+ ))}
+
+
) : (
{t('dispatch.syncNoFileList')}
@@ -186,7 +188,7 @@ export const DispatchResultDialog: React.FC
= ({
)
) : null}
-
+
= ({
);
const menu = open ? (
-
= ({
{t('chatInput.dispatch.local')}
{t('chatInput.dispatch.localDescription')}
- {target.kind === 'local' ? : null}
+ {target.kind === 'local' ? : null}
@@ -213,7 +211,7 @@ export const DispatchTargetPicker: React.FC
= ({
: t('chatInput.dispatch.deviceOffline')}
- {selected ? : null}
+ {selected ? : null}
);
})}
@@ -265,7 +263,7 @@ export const DispatchTargetPicker: React.FC = ({
{option.displayName}
{option.description || t('chatInput.dispatch.sshDescription')}
- {selected ? : null}
+ {selected ? : null}
);
})}
@@ -281,10 +279,10 @@ export const DispatchTargetPicker: React.FC = ({
setSshDialogOpen(true);
}}
>
-
+
{t('chatInput.dispatch.addSsh')}
-
+
) : null;
return (
@@ -320,11 +318,7 @@ export const DispatchTargetPicker: React.FC = ({
: }
{displayLabel}
{!locked ? (
-
+
) : null}
diff --git a/src/web-ui/src/features/market-account/MarketAccountControls.test.tsx b/src/web-ui/src/features/market-account/MarketAccountControls.test.tsx
index a789f2925d..544b0fd762 100644
--- a/src/web-ui/src/features/market-account/MarketAccountControls.test.tsx
+++ b/src/web-ui/src/features/market-account/MarketAccountControls.test.tsx
@@ -46,6 +46,7 @@ vi.mock('@/shared/notification-system', () => ({
}));
vi.mock('@bitfun/ui', () => ({
+ Icon: ({ name, ...props }: { name: string } & React.HTMLAttributes) => ,
Button: ({ children, ...props }: any) => {children},
Menu: ({ children, ...props }: any) => {children}
,
MenuItem: ({ children, leading, ...props }: any) => (
diff --git a/src/web-ui/src/features/market-account/MarketAccountControls.tsx b/src/web-ui/src/features/market-account/MarketAccountControls.tsx
index 6d566b2ec1..21e7eac661 100644
--- a/src/web-ui/src/features/market-account/MarketAccountControls.tsx
+++ b/src/web-ui/src/features/market-account/MarketAccountControls.tsx
@@ -1,7 +1,7 @@
-import { Button, Menu, MenuItem, Modal } from '@bitfun/ui';
+import { Button, Icon, Menu, MenuItem, Modal } from '@bitfun/ui';
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
-import { ChevronDown, Github, Loader2, LogOut } from 'lucide-react';
+import { Github, Loader2, LogOut } from 'lucide-react';
import { Avatar} from '@/component-library';
import { getAppearanceOverlayHost } from '@/infrastructure/appearance/runtime/AppearanceOverlayHost';
import { useI18n } from '@/infrastructure/i18n';
@@ -178,7 +178,7 @@ export function MarketAccountControls({
>
@{account.me.user.login}
-
+
{menuOpen && createPortal(
@@ -852,7 +862,7 @@ export const RelayDeployWizard: React.FC = ({
setFormData((p) => ({ ...p, username: e.target.value }))}
- leading={} disabled={connecting} />
+ leading={} disabled={connecting} />
@@ -876,7 +886,7 @@ export const RelayDeployWizard: React.FC = ({
aria-label={showPassword ? t('ssh.remote.hidePassword') : t('ssh.remote.showPassword')}
onClick={() => setShowPassword((s) => !s)}
tabIndex={-1}
- icon={showPassword ? : }
+ icon={showPassword ? : }
/>
} />
@@ -918,7 +928,7 @@ export const RelayDeployWizard: React.FC = ({
aria-label={showPassphrase ? t('ssh.remote.hidePassword') : t('ssh.remote.showPassword')}
onClick={() => setShowPassphrase((s) => !s)}
tabIndex={-1}
- icon={showPassphrase ? : }
+ icon={showPassphrase ? : }
/>
} />
@@ -940,7 +950,7 @@ export const RelayDeployWizard: React.FC = ({
)}
-
+
);
const renderCheckRow = (
@@ -949,9 +959,9 @@ export const RelayDeployWizard: React.FC
= ({
detail: string,
) => (
- {ok === true &&
}
+ {ok === true &&
}
{ok === 'warn' &&
}
- {ok === false &&
}
+ {ok === false &&
}
{label}
{detail}
@@ -965,7 +975,7 @@ export const RelayDeployWizard: React.FC = ({
const dockerWarn = !!pf?.dockerInstalled && !dockerOk && pf.dockerAccessMode !== 'missing';
const dockerWillInstall = !!pf && !pf.dockerInstalled && canInstallDocker;
return (
-
+
{serverLabel}
@@ -980,7 +990,7 @@ export const RelayDeployWizard: React.FC
= ({
<>
{alreadyDeployed && (
-
+
{t('relayDeploy.alreadyDeployedTitle')}
@@ -1102,7 +1112,7 @@ export const RelayDeployWizard: React.FC = ({
)}
{taskFailed && (
-
+
{t('relayDeploy.dockerInstallFailed')}
)}
@@ -1126,7 +1136,7 @@ export const RelayDeployWizard: React.FC = ({
setStep('register');
}}
disabled={taskRunning || !pf.relayHealthy}
- leadingIcon={}
+ leadingIcon={}
>
{t('relayDeploy.skipToRegister')}
@@ -1152,12 +1162,12 @@ export const RelayDeployWizard: React.FC = ({
>
)}
-
+
);
};
const renderDeploy = () => (
-
+
{serverLabel}
@@ -1174,7 +1184,7 @@ export const RelayDeployWizard: React.FC
= ({
)}
{taskStatus === 'succeeded' && (
<>
-
+
{t('relayDeploy.deploySucceeded')}
@@ -1182,7 +1192,7 @@ export const RelayDeployWizard: React.FC = ({
)}
{taskStatus === 'failed' && (
<>
-
+
{t('relayDeploy.deployFailed')}
@@ -1219,11 +1229,11 @@ export const RelayDeployWizard: React.FC = ({
)}
-
+
);
const renderRegister = () => (
-
+
{relayUrl}
@@ -1247,7 +1257,7 @@ export const RelayDeployWizard: React.FC = ({
-
+
{regMode === 'create' ? t('relayDeploy.registerTitle') : t('relayDeploy.registerExistingTitle')}
@@ -1262,7 +1272,7 @@ export const RelayDeployWizard: React.FC = ({
setRegUsername(e.target.value)}
- leading={} disabled={regLoading} />
+ leading={} disabled={regLoading} />
@@ -1278,7 +1288,7 @@ export const RelayDeployWizard: React.FC = ({
aria-label={showRegPassword ? t('ssh.remote.hidePassword') : t('ssh.remote.showPassword')}
onClick={() => setShowRegPassword((s) => !s)}
tabIndex={-1}
- icon={showRegPassword ? : }
+ icon={showRegPassword ? : }
/>
} />
@@ -1304,41 +1314,41 @@ export const RelayDeployWizard: React.FC = ({
{regLoading ? (
<>{t('relayDeploy.creatingAccount')}>
) : (
- <>{t('relayDeploy.createAccount')}>
+ <>{t('relayDeploy.createAccount')}>
)}
) : (
}>
+ leadingIcon={}>
{t('relayDeploy.finishAndLogin')}
)}
-
+
);
const renderDone = () => (
-
+
{t('relayDeploy.doneTitle')}
{relayUrl}
{verify && (
- {verify.reachable ?
:
}
+ {verify.reachable ?
:
}
{verify.reachable ? t('relayDeploy.verifyOk') : t('relayDeploy.verifyFailed')}
)}
- }>
+ }>
{t('relayDeploy.finishAndLogin')}
-
+
);
return (
@@ -1363,7 +1373,7 @@ export const RelayDeployWizard: React.FC
= ({
data-bf-state={[i === stepIndex && 'active', i < stepIndex && 'completed'].filter(Boolean).join(' ') || undefined}
>
- {i < stepIndex ? : i + 1}
+ {i < stepIndex ? : i + 1}
{s.label}
diff --git a/src/web-ui/src/features/ssh-remote/PortForwardDialog.test.tsx b/src/web-ui/src/features/ssh-remote/PortForwardDialog.test.tsx
index 30e73c4cc9..83b390f11b 100644
--- a/src/web-ui/src/features/ssh-remote/PortForwardDialog.test.tsx
+++ b/src/web-ui/src/features/ssh-remote/PortForwardDialog.test.tsx
@@ -39,6 +39,7 @@ vi.mock('@/shared/utils/logger', () => ({
}));
vi.mock('@bitfun/ui', () => ({
+ Icon: ({ name, ...props }: { name: string } & React.HTMLAttributes) => ,
Modal: ({ isOpen, children }: React.PropsWithChildren<{ isOpen: boolean }>) => (
isOpen ? {children}
: null
),
@@ -96,6 +97,15 @@ vi.mock('@bitfun/ui', () => ({
/>
)),
Tooltip: ({ children }: React.PropsWithChildren) => <>{children}>,
+ FormSection: ({
+ children,
+ title,
+ actions,
+ ...props
+ }: React.HTMLAttributes & { title?: React.ReactNode; actions?: React.ReactNode }) => (
+ {title}{actions}{children}
+ ),
+ FieldGroup: ({ children, ...props }: React.HTMLAttributes) => {children}
,
}));
vi.mock('@/component-library', () => ({
diff --git a/src/web-ui/src/features/ssh-remote/PortForwardDialog.tsx b/src/web-ui/src/features/ssh-remote/PortForwardDialog.tsx
index 5125b8a6b5..91dd5abfb1 100644
--- a/src/web-ui/src/features/ssh-remote/PortForwardDialog.tsx
+++ b/src/web-ui/src/features/ssh-remote/PortForwardDialog.tsx
@@ -15,13 +15,13 @@
* the local port is an allocation that moves when the number is taken.
*/
-import { Button, IconButton, Input, Modal, Tooltip } from '@bitfun/ui';
+import { Button, FieldGroup, FormSection, Icon, IconButton, Input, Modal, Tooltip } from '@bitfun/ui';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Checkbox } from '@/component-library';
import { useI18n } from '@/infrastructure/i18n';
import { systemAPI } from '@/infrastructure/api/service-api/SystemAPI';
import { createLogger } from '@/shared/utils/logger';
-import { AlertTriangle, Check, Copy, ExternalLink, Network, Plus, RefreshCw, X } from 'lucide-react';
+import { AlertTriangle, Network, RefreshCw } from 'lucide-react';
import { sshApi } from './sshApi';
import type { PortForward, RemoteListeningPort } from './types';
import './PortForwardDialog.scss';
@@ -261,9 +261,10 @@ export const PortForwardDialog: React.FC = ({
)}
{/* Discovery first: it answers "which port?" without making anyone look it up. */}
-
-
- {t('ssh.portForward.detectedTitle')}
+
= ({
/>}
/>
-
-
+ )}
+ >
{isDetecting && detectedPorts === null ? (
{t('ssh.portForward.detecting')}
@@ -305,9 +306,9 @@ export const PortForwardDialog: React.FC
= ({
data-port={port.port}
>
{alreadyForwarded ? (
-
+
) : (
-
+
)}
{port.port}
{port.process && (
@@ -321,16 +322,14 @@ export const PortForwardDialog: React.FC = ({
{t('ssh.portForward.detectedEmpty')}
)}
-
+
-
-
- {t('ssh.portForward.activeTitle')}
-
{forwards.length === 0 ? (
@@ -397,7 +396,7 @@ export const PortForwardDialog: React.FC
= ({
size="sm"
aria-label={t('ssh.portForward.openInBrowser')}
onClick={() => handleOpen(forward)}
- icon={}
+ icon={}
/>
@@ -406,7 +405,7 @@ export const PortForwardDialog: React.FC = ({
size="sm"
aria-label={t('ssh.portForward.copyAddress')}
onClick={() => handleCopy(forward)}
- icon={}
+ icon={}
/>
@@ -416,7 +415,7 @@ export const PortForwardDialog: React.FC = ({
aria-label={t('ssh.portForward.stop')}
onClick={() => void handleStop(forward.id)}
data-testid="ssh-port-forward-stop"
- icon={}
+ icon={}
/>
@@ -424,16 +423,14 @@ export const PortForwardDialog: React.FC = ({
))}
)}
-
+
{/* The escape hatch: a specific local port, or a host detection cannot see. */}
-
-
- {t('ssh.portForward.manualTitle')}
-
-
+
-
= ({
>
{t('ssh.portForward.add')}
-
+
= ({
description={t('ssh.portForward.exposeOnLanHint')}
/>
-
+
);
diff --git a/src/web-ui/src/features/ssh-remote/RemoteFileBrowser.scss b/src/web-ui/src/features/ssh-remote/RemoteFileBrowser.scss
index 7bcadd4652..588168fb5b 100644
--- a/src/web-ui/src/features/ssh-remote/RemoteFileBrowser.scss
+++ b/src/web-ui/src/features/ssh-remote/RemoteFileBrowser.scss
@@ -212,7 +212,6 @@
// Content area
&__content {
flex: 1;
- overflow: auto;
padding: 0;
}
diff --git a/src/web-ui/src/features/ssh-remote/RemoteFileBrowser.tsx b/src/web-ui/src/features/ssh-remote/RemoteFileBrowser.tsx
index 7d8230ab6c..c7169a6140 100644
--- a/src/web-ui/src/features/ssh-remote/RemoteFileBrowser.tsx
+++ b/src/web-ui/src/features/ssh-remote/RemoteFileBrowser.tsx
@@ -3,7 +3,7 @@
* Used to browse and select remote directory as workspace
*/
-import { Button, ConfirmDialog, IconButton } from '@bitfun/ui';
+import { Button, ConfirmDialog, Icon, IconButton, ScrollArea } from '@bitfun/ui';
import React, { useState, useEffect, useRef, useCallback } from 'react';
import { createPortal } from 'react-dom';
import { getAppearanceOverlayHost } from '@/infrastructure/appearance/runtime/AppearanceOverlayHost';
@@ -11,19 +11,7 @@ import { useI18n } from '@/infrastructure/i18n';
import type { RemoteFileEntry } from './types';
import { sshApi } from './sshApi';
import { isImeOwnedKeyboardEvent } from '@/shared/utils/ime';
-import {
- RefreshCw,
- Folder,
- File,
- Link,
- ChevronRight,
- Home,
- ArrowLeft,
- Loader2,
- Upload,
- Download,
- X,
-} from 'lucide-react';
+import { RefreshCw, Home, Loader2 } from 'lucide-react';
import './RemoteFileBrowser.scss';
interface RemoteFileBrowserProps {
@@ -375,9 +363,9 @@ export const RemoteFileBrowser: React.FC = ({
};
const getEntryIcon = (entry: RemoteFileEntry) => {
- if (entry.isDir) return ;
- if (entry.isSymlink) return ;
- return ;
+ if (entry.isDir) return ;
+ if (entry.isSymlink) return ;
+ return ;
};
const pathParts = (() => {
@@ -407,7 +395,7 @@ export const RemoteFileBrowser: React.FC = ({
}
+ icon={}
size="md"
onClick={onCancel}
aria-label={t('actions.close')}
@@ -451,7 +439,7 @@ export const RemoteFileBrowser: React.FC = ({
>
-
+
{pathParts.length === 0 ? (
/
) : (
@@ -466,7 +454,7 @@ export const RemoteFileBrowser: React.FC = ({
>
{part}
- {!isLast && }
+ {!isLast && }
);
})
@@ -494,7 +482,7 @@ export const RemoteFileBrowser: React.FC = ({
title="Go up"
disabled={getRemoteParentPath(currentPath) === null || transferBusy}
>
-
+
= ({
title={t('ssh.remote.upload')}
disabled={transferBusy}
>
-
+
@@ -515,7 +503,7 @@ export const RemoteFileBrowser: React.FC = ({
)}
{/* File List */}
-
+
{error && (
{error}
@@ -562,7 +550,7 @@ export const RemoteFileBrowser: React.FC = ({
className="remote-file-browser__row remote-file-browser__row--parent"
>
|
-
+
..
|
@@ -599,7 +587,7 @@ export const RemoteFileBrowser: React.FC = ({
)}
-
+
{/* Context Menu */}
{contextMenu.show && contextMenu.entry && (
@@ -612,7 +600,7 @@ export const RemoteFileBrowser: React.FC
= ({
className="remote-file-browser__context-menu-item"
onClick={() => handleContextMenuAction('open')}
>
-
+
{t('actions.open') || 'Open'}
{!contextMenu.entry.isDir && (
@@ -621,7 +609,7 @@ export const RemoteFileBrowser: React.FC = ({
className="remote-file-browser__context-menu-item"
onClick={() => handleContextMenuAction('download')}
>
-
+
{t('ssh.remote.download')}
)}
diff --git a/src/web-ui/src/features/ssh-remote/SSHAuthPromptDialog.tsx b/src/web-ui/src/features/ssh-remote/SSHAuthPromptDialog.tsx
index 40cdbca657..9278b3ef85 100644
--- a/src/web-ui/src/features/ssh-remote/SSHAuthPromptDialog.tsx
+++ b/src/web-ui/src/features/ssh-remote/SSHAuthPromptDialog.tsx
@@ -2,11 +2,11 @@
* Unified SSH authentication prompt.
*/
-import { Button, Field, IconButton, Input, Modal, Select, Tooltip } from '@bitfun/ui';
+import { Button, Field, Icon, IconButton, Input, Modal, Select, Tooltip } from '@bitfun/ui';
import React, { useState, useEffect, useRef, useCallback } from 'react';
import { useI18n } from '@/infrastructure/i18n';
-import { FolderOpen, Key, Loader2, Lock, Server, User } from 'lucide-react';
+import { FolderOpen, Key, Loader2, Lock, Server } from 'lucide-react';
import type { SSHAuthMethod } from './types';
import {
pickSshCertificatePath,
@@ -172,7 +172,7 @@ export const SSHAuthPromptDialog: React.FC = ({
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="root"
- leading={}
+ leading={}
disabled={isConnecting}
/>
diff --git a/src/web-ui/src/features/ssh-remote/SSHConnectionDialog.scss b/src/web-ui/src/features/ssh-remote/SSHConnectionDialog.scss
index 7a15024d5d..177bc82022 100644
--- a/src/web-ui/src/features/ssh-remote/SSHConnectionDialog.scss
+++ b/src/web-ui/src/features/ssh-remote/SSHConnectionDialog.scss
@@ -27,7 +27,6 @@
&__scroll {
flex: 1;
min-height: 0;
- overflow-y: auto;
overflow-x: hidden;
padding: 10px 0 6px;
}
@@ -66,12 +65,6 @@
flex-direction: column;
gap: 4px;
max-height: 132px;
- overflow-y: auto;
- scrollbar-width: none;
-
- &::-webkit-scrollbar {
- display: none;
- }
}
&__saved-item {
diff --git a/src/web-ui/src/features/ssh-remote/SSHConnectionDialog.test.tsx b/src/web-ui/src/features/ssh-remote/SSHConnectionDialog.test.tsx
index 4a7e33d0a6..c17960b2d6 100644
--- a/src/web-ui/src/features/ssh-remote/SSHConnectionDialog.test.tsx
+++ b/src/web-ui/src/features/ssh-remote/SSHConnectionDialog.test.tsx
@@ -52,6 +52,7 @@ vi.mock('./SSHAuthPromptDialog', () => ({
}));
vi.mock('@bitfun/ui', () => ({
+ Icon: ({ name, ...props }: { name: string } & React.HTMLAttributes) => ,
Modal: ({
isOpen,
children,
@@ -95,6 +96,25 @@ vi.mock('@bitfun/ui', () => ({
),
Tooltip: ({ children }: React.PropsWithChildren) => <>{children}>,
+ ScrollArea: ({ children, ...props }: React.HTMLAttributes) => {children}
,
+ FormSection: ({
+ children,
+ title,
+ actions,
+ ...props
+ }: React.HTMLAttributes & { title?: React.ReactNode; actions?: React.ReactNode; headingAs?: string }) => (
+
+ {title}
+ {actions}
+ {children}
+
+ ),
+ FieldGroup: React.forwardRef>(
+ function FieldGroup({ children, ...props }, ref) {
+ return {children}
;
+ },
+ ),
+ FieldRow: ({ children, ...props }: React.HTMLAttributes) => {children}
,
// The real Field associates its label with the child control; the mock
// mirrors that accessible name via aria-label so queries stay realistic.
Field: ({
diff --git a/src/web-ui/src/features/ssh-remote/SSHConnectionDialog.tsx b/src/web-ui/src/features/ssh-remote/SSHConnectionDialog.tsx
index 41512bac12..a55b1acce9 100644
--- a/src/web-ui/src/features/ssh-remote/SSHConnectionDialog.tsx
+++ b/src/web-ui/src/features/ssh-remote/SSHConnectionDialog.tsx
@@ -3,7 +3,7 @@
* Professional SSH connection dialog following BitFun design patterns
*/
-import { Button, Field, IconButton, Input as DesignInput, Modal, Select, Tooltip } from '@bitfun/ui';
+import { Button, Field, FieldGroup, FieldRow, FormSection, Icon, IconButton, Input as DesignInput, Modal, ScrollArea, Select, Tooltip } from '@bitfun/ui';
import React, { useState, useEffect, useCallback, useRef } from 'react';
import { useI18n } from '@/infrastructure/i18n';
import { useSSHRemoteContext } from './SSHRemoteContext';
@@ -11,23 +11,15 @@ import { SSHAuthPromptDialog, type SSHAuthPromptSubmitPayload } from './SSHAuthP
import { Alert } from '@/component-library';
import {
ArrowDownToLine,
- CheckCircle2,
- ChevronDown,
- Eye,
EyeOff,
FolderOpen,
Key,
Loader2,
Lock,
- Pencil,
Play,
- Plus,
RefreshCw,
- Search,
Server,
Trash2,
- User,
- XCircle,
} from 'lucide-react';
import type {
ConnectionTestReport,
@@ -735,24 +727,27 @@ export const SSHConnectionDialog: React.FC = ({
)}
-
+
{/* Saved connections section */}
{savedConnections.length > 0 && (
-
-
-
- {t('ssh.remote.savedConnections')}
-
+ {t('ssh.remote.savedConnections')}}
+ actions={(
setSavedSearch(e.target.value)}
placeholder={t('actions.search')}
- leading={}
+ leading={}
size="sm"
/>
-
-
+ )}
+ >
+
{filteredSavedConnections.map((conn) => (
= ({
disabled={isConnecting}
title={t('actions.edit') || 'Edit'}
aria-label={t('actions.edit') || 'Edit'}
- icon={
}
+ icon={
}
/>
= ({
))}
-
-
+
+
)}
{/* SSH Config hosts section */}
{sshConfigHosts.length > 0 && (
-
-
-
- {t('ssh.remote.sshConfigHosts') || 'SSH Config'}
-
+ {t('ssh.remote.sshConfigHosts') || 'SSH Config'}}
+ actions={(
setConfigSearch(e.target.value)}
placeholder={t('actions.search')}
- leading={}
+ leading={}
size="sm"
/>
-
-
+ )}
+ >
+
{filteredSSHConfigHosts.map((configHost) => (
= ({
))}
-
-
+
+
)}
{/* Divider */}
@@ -880,8 +876,10 @@ export const SSHConnectionDialog: React.FC = ({
)}
{/* New connection form */}
- = ({
data-bf-component="ssh-remote"
data-bf-part="connectionForm"
>
-
+
@@ -899,12 +897,12 @@ export const SSHConnectionDialog: React.FC = ({
onValueChange={(value) => handleInputChange('targetType', String(value))}
size="md"
/>
-
+
{!isLocalDockerTarget && (
<>
{/* Host and Port */}
-
+
{/* Username */}
-
+
handleInputChange('username', e.target.value)}
placeholder=""
- leading={}
+ leading={}
/>
-
+
>
)}
@@ -1040,7 +1038,7 @@ export const SSHConnectionDialog: React.FC
= ({
{!isLocalDockerTarget && (
<>
{/* Authentication Method */}
-
+
@@ -1050,11 +1048,11 @@ export const SSHConnectionDialog: React.FC = ({
onValueChange={(value) => handleInputChange('authType', String(value))}
size="md"
/>
-
+
{/* Password */}
{formData.authType === 'password' && (
-
+
= ({
aria-label={showPassword ? t('ssh.remote.hidePassword') : t('ssh.remote.showPassword')}
onClick={() => setShowPassword(s => !s)}
tabIndex={-1}
- icon={showPassword ? : }
+ icon={showPassword ? : }
/>
}
/>
-
+
)}
{/* Private Key */}
{formData.authType === 'privateKey' && (
<>
-
+
= ({
}
/>
-
-
+
+
= ({
aria-label={showPassphrase ? t('ssh.remote.hidePassword') : t('ssh.remote.showPassword')}
onClick={() => setShowPassphrase(s => !s)}
tabIndex={-1}
- icon={showPassphrase ? : }
+ icon={showPassphrase ? : }
/>
}
/>
-
-
+
+
= ({
}
/>
-
+
>
)}
{formData.authType === 'agent' && (
<>
-
+
= ({
placeholder={t('ssh.remote.optional')}
/>
-
-
+
+
= ({
placeholder={t('ssh.remote.optional')}
/>
-
+
>
)}
{formData.authType === 'keyboardInteractive' && (
-
+
)}
>
@@ -1208,20 +1206,16 @@ export const SSHConnectionDialog: React.FC = ({
onClick={() => setShowAdvancedSettings((visible) => !visible)}
>
{t('ssh.remote.advancedSettings')}
-
+ ].filter(Boolean).join(' ')} />
{showAdvancedSettings && (
-
+
)}
@@ -1315,8 +1309,8 @@ export const SSHConnectionDialog: React.FC = ({
{connectionTest.stages.map((stage) => (
{stage.success
- ?
- : }
+ ?
+ : }
{formatTestStageLabel(stage)}
{stage.error && {stage.error}}
@@ -1333,8 +1327,8 @@ export const SSHConnectionDialog: React.FC = ({
)}
)}
-
-
+
+
{/* Actions */}
@@ -1374,7 +1368,7 @@ export const SSHConnectionDialog: React.FC
= ({
>
) : (
<>
-
+
{t('ssh.remote.connect')}
>
)}
diff --git a/src/web-ui/src/infrastructure/appearance/schema/migrateAppearancePackage.ts b/src/web-ui/src/infrastructure/appearance/schema/migrateAppearancePackage.ts
index 3eae5e02f7..44765f8e36 100644
--- a/src/web-ui/src/infrastructure/appearance/schema/migrateAppearancePackage.ts
+++ b/src/web-ui/src/infrastructure/appearance/schema/migrateAppearancePackage.ts
@@ -27,12 +27,15 @@ const RETIRED_COMPONENT_SURFACE_IDS = new Set(['button', 'switch']);
const RETIRED_COMPONENT_PARTS: Readonly>> = {
'assistant-card': new Set(['configure', 'newSession']),
+ 'branch-quick-switch': new Set(['list']),
+ 'canvas-tab-overflow': new Set(['list']),
'computer-use-tool-card': new Set(['settingsButton']),
'context-list': new Set(['clear']),
'copy-output-button': new Set(['action', 'icon', 'text']),
'create-agent-page': new Set(['back']),
'font-preference': new Set(['resetButton', 'levelGroup', 'levelButton']),
'git-diff-view': new Set(['typeSwitcher', 'typeOption']),
+ 'git-nav': new Set(['sections']),
'image-analysis-card': new Set(['expand']),
'image-viewer': new Set(['toolbar', 'controls', 'action']),
'markdown-editor': new Set(['modeToggle']),
@@ -42,8 +45,11 @@ const RETIRED_COMPONENT_PARTS: Readonly>> = {
'assistantSessionMenu',
'footerMenu', 'footerMenuItem', 'footerMenuDivider',
'workspaceMenu', 'workspaceMenuItem', 'workspaceMenuDivider', 'workspaceMenuTitle', 'workspaceMenuEmpty',
+ 'sections',
]),
'notification-button': new Set(['menuItem']),
+ 'settings-nav': new Set(['sections']),
+ 'shell-nav': new Set(['content']),
'workspace-item': new Set(['menuPopover', 'menuItem', 'menuDivider']),
'peer-device': new Set(['switcherDisconnect']),
'review-session-summary-card': new Set(['open']),
@@ -54,7 +60,7 @@ const RETIRED_COMPONENT_PARTS: Readonly>> = {
};
const RETIRED_SCENE_PARTS: Readonly>> = {
- skills: new Set(['addAction']),
+ skills: new Set(['addAction', 'discoverContent', 'suiteSections']),
};
function migrateRuntimePartRule(value: unknown): unknown {
diff --git a/src/web-ui/src/infrastructure/config/components/AcpAgentsConfig.tsx b/src/web-ui/src/infrastructure/config/components/AcpAgentsConfig.tsx
index d2b48244aa..7b0353bfc2 100644
--- a/src/web-ui/src/infrastructure/config/components/AcpAgentsConfig.tsx
+++ b/src/web-ui/src/infrastructure/config/components/AcpAgentsConfig.tsx
@@ -1,21 +1,15 @@
-import { Button, IconButton, Input, Select, Tooltip } from '@bitfun/ui';
+import { Button, Icon, IconButton, Input, Select, Tooltip } from '@bitfun/ui';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
Bot,
CircleAlert,
- Download,
- Eye,
EyeOff,
- ExternalLink,
FileJson,
LoaderCircle,
- Plus,
RefreshCw,
Save,
- Search,
Server,
- Terminal,
} from 'lucide-react';
import { Textarea } from '@/component-library';
import {
@@ -1067,7 +1061,7 @@ const AcpAgentsConfig: React.FC = () => {
}
+ trailingIcon={}
onClick={openLearnMore}
>
{t('actions.learnMore')}
@@ -1091,7 +1085,7 @@ const AcpAgentsConfig: React.FC = () => {
value={registrySearch}
onChange={(event) => setRegistrySearch(event.target.value)}
placeholder={t('registry.searchPlaceholder')}
- leading={}
+ leading={}
size="md"
/>
@@ -1277,7 +1271,7 @@ const AcpAgentsConfig: React.FC = () => {
data-bf-part="capabilities"
>
}
+ icon={}
item={requirementProbe?.tool}
label={t('requirements.tool')}
installedText={t('requirements.installed')}
@@ -1312,7 +1306,7 @@ const AcpAgentsConfig: React.FC = () => {
}
+ leadingIcon={}
onClick={() => { void installPresetClient(preset); }}
loading={installing}
>
@@ -1355,7 +1349,7 @@ const AcpAgentsConfig: React.FC = () => {
}
+ leadingIcon={}
onClick={() => addPresetClient(preset, {
manualCliRequired: selfManagedCliMissing,
})}
@@ -1434,7 +1428,7 @@ const AcpAgentsConfig: React.FC = () => {
data-bf-part="capabilities"
>
}
+ icon={}
item={requirementProbe?.tool}
label={t('requirements.tool')}
installedText={t('requirements.installed')}
@@ -1498,7 +1492,7 @@ const AcpAgentsConfig: React.FC = () => {
onClick={() => setShowHiddenRemoteConnections(visible => !visible)}
aria-expanded={showHiddenRemoteConnections}
>
- {showHiddenRemoteConnections ? : }
+ {showHiddenRemoteConnections ? : }
{t(
showHiddenRemoteConnections
? 'remote.hideHiddenConnections'
@@ -1708,7 +1702,7 @@ const AcpAgentsConfig: React.FC = () => {
data-bf-part="capabilities"
>
}
+ icon={}
item={row.requirementProbe?.tool}
label={t('requirements.tool')}
installedText={t('requirements.installed')}
@@ -1744,7 +1738,7 @@ const AcpAgentsConfig: React.FC = () => {
}
+ leadingIcon={}
onClick={() => {
void installPresetClient(row.preset!, {
remoteConnectionId: connection.id,
@@ -1778,7 +1772,7 @@ const AcpAgentsConfig: React.FC = () => {
}
+ leadingIcon={}
onClick={() => addPresetClient(row.preset!)}
>
{t('actions.add')}
@@ -1850,7 +1844,7 @@ const AcpAgentsConfig: React.FC = () => {
name: connection.name || connection.id,
})}
onClick={() => restoreRemoteConnection(connection)}
- icon={}
+ icon={}
/>
diff --git a/src/web-ui/src/infrastructure/config/components/AppearanceMarketDialog.test.tsx b/src/web-ui/src/infrastructure/config/components/AppearanceMarketDialog.test.tsx
index 5ae9878ce6..dcd3a4c63b 100644
--- a/src/web-ui/src/infrastructure/config/components/AppearanceMarketDialog.test.tsx
+++ b/src/web-ui/src/infrastructure/config/components/AppearanceMarketDialog.test.tsx
@@ -39,6 +39,8 @@ vi.mock('react-i18next', () => ({
}));
vi.mock('@bitfun/ui', () => ({
+ ScrollArea: ({ children, ...props }: React.HTMLAttributes) => {children}
,
+ Icon: ({ name, ...props }: { name: string } & React.HTMLAttributes) => ,
Button: ({ children, isLoading: _isLoading, iconOnly: _iconOnly, ...props }: any) => (
{children}
),
diff --git a/src/web-ui/src/infrastructure/config/components/AppearanceMarketDialog.tsx b/src/web-ui/src/infrastructure/config/components/AppearanceMarketDialog.tsx
index ff0d1674d3..7dd8489e6f 100644
--- a/src/web-ui/src/infrastructure/config/components/AppearanceMarketDialog.tsx
+++ b/src/web-ui/src/infrastructure/config/components/AppearanceMarketDialog.tsx
@@ -1,16 +1,6 @@
-import { Button, Modal, SearchField, Select } from '@bitfun/ui';
+import { Button, Icon, Modal, ScrollArea, SearchField, Select } from '@bitfun/ui';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
-import {
- AlertTriangle,
- ArrowLeft,
- Check,
- Download,
- Image,
- PackageCheck,
- RefreshCw,
- Search as SearchIcon,
- Store,
-} from 'lucide-react';
+import { AlertTriangle, PackageCheck, RefreshCw } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { confirmDialog } from '@/infrastructure/confirm-dialog';
import { MarketAccountControls } from '@/features/market-account';
@@ -290,12 +280,12 @@ export function AppearanceMarketDialog({ isOpen, onClose }: AppearanceMarketDial
|| Boolean(linkedToOtherListing);
return (
-
-
setDetail(null)} leadingIcon={}>
+ setDetail(null)} leadingIcon={}>
{t('package.market.back')}
@@ -315,7 +305,7 @@ export function AppearanceMarketDialog({ isOpen, onClose }: AppearanceMarketDial
onError={(event) => retryOriginalMarketImage(event.currentTarget, detail.previewUrl)}
/>
)
- : }
+ : }
void handleApply()}
>
- {active ?
:
}
+ {active ?
:
}
{active ? t('package.market.applied') : t('package.market.apply')}
) : (
@@ -423,7 +413,7 @@ export function AppearanceMarketDialog({ isOpen, onClose }: AppearanceMarketDial
disabled={installDisabled}
onClick={() => release && void handleInstall(release)}
>
- {updateAvailable ?
:
}
+ {updateAvailable ?
:
}
{installing
? t('package.market.installing')
: updateAvailable
@@ -436,7 +426,7 @@ export function AppearanceMarketDialog({ isOpen, onClose }: AppearanceMarketDial
{t('package.market.noAutoApply')}
-
+
);
};
@@ -510,7 +500,7 @@ export function AppearanceMarketDialog({ isOpen, onClose }: AppearanceMarketDial
data-bf-part="marketToolbar"
>
}
+ leadingIcon={}
value={query}
onValueChange={setQuery}
onSearch={value => setSubmittedQuery(value.trim())}
@@ -556,7 +546,7 @@ export function AppearanceMarketDialog({ isOpen, onClose }: AppearanceMarketDial
)}
- retryOriginalMarketImage(event.currentTarget, item.previewUrl)}
/>
)
- : }
+ : }
{t(`package.market.mode.${item.mode}`)}
-
+
{t('package.market.empty')}
)}
@@ -673,7 +663,7 @@ export function AppearanceMarketDialog({ isOpen, onClose }: AppearanceMarketDial
)}
-
+
{loading && {t('package.market.loading')}}
diff --git a/src/web-ui/src/infrastructure/config/components/AppearanceMarketWorkflows.tsx b/src/web-ui/src/infrastructure/config/components/AppearanceMarketWorkflows.tsx
index 35fc60ddc6..89784a98e3 100644
--- a/src/web-ui/src/infrastructure/config/components/AppearanceMarketWorkflows.tsx
+++ b/src/web-ui/src/infrastructure/config/components/AppearanceMarketWorkflows.tsx
@@ -1,6 +1,6 @@
-import { Button, Field, Input, Select } from '@bitfun/ui';
+import { Button, Field, Icon, Input, Select, ScrollArea } from '@bitfun/ui';
import { type FormEvent, useCallback, useEffect, useState } from 'react';
-import { AlertTriangle, Image, Inbox, RefreshCw, ShieldCheck, Upload } from 'lucide-react';
+import { AlertTriangle, Inbox, RefreshCw, ShieldCheck } from 'lucide-react';
import { Textarea } from '@/component-library';
import { confirmDialog } from '@/infrastructure/confirm-dialog';
import {
@@ -335,7 +335,7 @@ export function AppearanceMarketWorkflows({ workflow }: AppearanceMarketWorkflow
variant="fill"
loading={submitting}
disabled={!manualDraft.packagePath || !manualDraft.licenseValue.trim()}
- leadingIcon={}
+ leadingIcon={}
>
{t('package.market.submissions.manual.submit')}
@@ -396,7 +396,7 @@ export function AppearanceMarketWorkflows({ workflow }: AppearanceMarketWorkflow
size="sm"
onClick={() => setManualSubmitOpen(open => !open)}
disabled={submitting}
- leadingIcon={}
+ leadingIcon={}
>
{t('package.market.submissions.manual.open')}
@@ -409,7 +409,7 @@ export function AppearanceMarketWorkflows({ workflow }: AppearanceMarketWorkflow
{renderError()}
-
+
{renderManualSubmit()}
{loading ? {t('package.market.submissions.loading')}
: submissions.length === 0 ? (
@@ -441,7 +441,7 @@ export function AppearanceMarketWorkflows({ workflow }: AppearanceMarketWorkflow
onError={(event) => retryOriginalMarketImage(event.currentTarget, submission.previewUrl!)}
/>
)
- : }
+ : }
@@ -475,7 +475,7 @@ export function AppearanceMarketWorkflows({ workflow }: AppearanceMarketWorkflow
))}
)}
-
+
);
}
@@ -511,7 +511,7 @@ export function AppearanceMarketWorkflows({ workflow }: AppearanceMarketWorkflow
data-bf-component="appearance-settings"
data-bf-part="marketReviewLayout"
>
- {formattedDate(submission.updatedAt)}
))}
-
-
+
{t('package.market.review.manifest')}
- {JSON.stringify(reviewDetail.manifest, null, 2)}
+
+ {JSON.stringify(reviewDetail.manifest, null, 2)}
+
)}
>
)}
-
+
)}
diff --git a/src/web-ui/src/infrastructure/config/components/AppearancePackageConfigSection.tsx b/src/web-ui/src/infrastructure/config/components/AppearancePackageConfigSection.tsx
index 2d58b285fb..19005607a1 100644
--- a/src/web-ui/src/infrastructure/config/components/AppearancePackageConfigSection.tsx
+++ b/src/web-ui/src/infrastructure/config/components/AppearancePackageConfigSection.tsx
@@ -1,7 +1,7 @@
import React, { useEffect, useMemo, useRef, useState } from 'react';
-import { AlertTriangle, Check, Download, Image, Trash2, X } from 'lucide-react';
+import { AlertTriangle, Trash2 } from 'lucide-react';
import { useTranslation } from 'react-i18next';
-import { Button, IconButton, Tooltip } from '@bitfun/ui';
+import { Button, Icon, IconButton, Tooltip } from '@bitfun/ui';
import { confirmDialog } from '@/infrastructure/confirm-dialog';
import {
@@ -87,7 +87,7 @@ export function AppearancePackageFailurePanel({
title={t('package.diagnostics.dismiss')}
aria-label={t('package.diagnostics.dismiss')}
onClick={onDismiss}
- icon={}
+ icon={}
/>
@@ -188,7 +188,7 @@ function AppearancePackagePreview({
{previewUrl
?

- :
}
+ :
}
{appearanceName}
@@ -205,10 +205,10 @@ function AppearancePackagePreview({
{previewUrl
?

- :
}
+ :
}
{!fallbackSrc && (
-
+
)}
@@ -342,7 +342,7 @@ export function AppearancePackageConfigSection() {
aria-label={t('package.export')}
disabled={busy}
onClick={() => void handleExport(selectedAppearance.id)}
- icon={}
+ icon={}
/>
[data-bf-component='button']:first-child {
diff --git a/src/web-ui/src/infrastructure/config/components/AppearanceSettingsPage.test.tsx b/src/web-ui/src/infrastructure/config/components/AppearanceSettingsPage.test.tsx
index aa44969355..bdb5765658 100644
--- a/src/web-ui/src/infrastructure/config/components/AppearanceSettingsPage.test.tsx
+++ b/src/web-ui/src/infrastructure/config/components/AppearanceSettingsPage.test.tsx
@@ -12,6 +12,9 @@ vi.mock('react-i18next', () => ({
}));
vi.mock('@bitfun/ui', () => ({
+ ScrollArea: ({ children, ...props }: React.HTMLAttributes) => {children}
,
+ FormSection: ({ children, ...props }: React.HTMLAttributes) => ,
+ FieldGroup: ({ children, ...props }: React.HTMLAttributes) => {children}
,
Select: ({ options, ...props }: any) => (
-
-
- {t('permissionPolicy.globalRulesTitle')}
+
+ {t('permissionPolicy.globalRulesTitle')}
+
+ )}
+ actions={(
}
+ leadingIcon={}
>
-
{t('permissionPolicy.addGlobalRule')}
-
+ )}
+ >
{draftRules.length === 0 ? (
{t('permissionPolicy.globalRulesEmpty')}
) : (
-
+
{t('permissionPolicy.globalRulesEffect')}
{t('permissionPolicy.globalRulesAction')}
@@ -437,7 +450,7 @@ export const GlobalPermissionRulesDialog: React.FC
moveDraftRule(rule.localId, -1)}
- icon={}
+ icon={}
/>
@@ -469,7 +482,7 @@ export const GlobalPermissionRulesDialog: React.FC
);
})}
-
+
)}
{rulesDirty ? (
@@ -495,7 +508,7 @@ export const GlobalPermissionRulesDialog: React.FC
) : null}
-
+
);
diff --git a/src/web-ui/src/infrastructure/config/components/HooksConfig.tsx b/src/web-ui/src/infrastructure/config/components/HooksConfig.tsx
index 5bafde13cd..f6d70b78d6 100644
--- a/src/web-ui/src/infrastructure/config/components/HooksConfig.tsx
+++ b/src/web-ui/src/infrastructure/config/components/HooksConfig.tsx
@@ -1,7 +1,7 @@
-import { Button, Switch, Modal, ConfirmDialog } from '@bitfun/ui';
+import { Button, ConfirmDialog, Icon, Modal, Switch } from '@bitfun/ui';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
-import { ExternalLink, RefreshCw } from 'lucide-react';
+import { RefreshCw } from 'lucide-react';
import { ConfigPageLoading } from '@/component-library';
import { useCurrentWorkspace } from '@/infrastructure/contexts/WorkspaceContext';
@@ -495,7 +495,7 @@ const HooksConfig: React.FC = ({ embedded = false }) => {
label={t('compatibility.reference.label')}
align="center"
>
- }>
+ }>
{t('compatibility.reference.open')}
diff --git a/src/web-ui/src/infrastructure/config/components/LocalVoiceModelsConfig.tsx b/src/web-ui/src/infrastructure/config/components/LocalVoiceModelsConfig.tsx
index 8d808a9dc3..274bb6bfc3 100644
--- a/src/web-ui/src/infrastructure/config/components/LocalVoiceModelsConfig.tsx
+++ b/src/web-ui/src/infrastructure/config/components/LocalVoiceModelsConfig.tsx
@@ -1,10 +1,8 @@
-import { Button, IconButton, Modal, StatusPill, type StatusPillTone, Tooltip } from '@bitfun/ui';
+import { Button, Icon, IconButton, Modal, StatusPill, type StatusPillTone, Tooltip } from '@bitfun/ui';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
- Download,
FolderOpen,
- MoreHorizontal,
RefreshCw,
ShieldCheck,
Trash2,
@@ -400,7 +398,7 @@ const LocalVoiceModelsConfig: React.FC = ({
size="sm"
onClick={() => handleDownload(model)}
disabled={busyAction !== null || anyDownloading}
- leadingIcon={}
+ leadingIcon={}
>
{needsRepair ? t('model.repair') : t('model.download')}
@@ -415,7 +413,7 @@ const LocalVoiceModelsConfig: React.FC = ({
data-bf-component="voice-input-config"
data-bf-part="modelMore"
onClick={event => openMaintenanceMenu(event, model)}
- icon={}
+ icon={}
/>
) : null}
diff --git a/src/web-ui/src/infrastructure/config/components/MCPResourceBrowser.scss b/src/web-ui/src/infrastructure/config/components/MCPResourceBrowser.scss
index 35e8d42ed1..f82e5926d8 100644
--- a/src/web-ui/src/infrastructure/config/components/MCPResourceBrowser.scss
+++ b/src/web-ui/src/infrastructure/config/components/MCPResourceBrowser.scss
@@ -63,7 +63,6 @@
.resources-list {
border-right: 1px solid var(--bf-appearance-token-border-base);
- overflow-y: auto;
background: var(--bf-appearance-token-color-bg-secondary);
.loading-state,
@@ -198,7 +197,6 @@
.viewer-content {
flex: 1;
- overflow: auto;
padding: 1.5rem;
.loading-content {
diff --git a/src/web-ui/src/infrastructure/config/components/MCPResourceBrowser.tsx b/src/web-ui/src/infrastructure/config/components/MCPResourceBrowser.tsx
index b3fdef8c4b..b8201205f1 100644
--- a/src/web-ui/src/infrastructure/config/components/MCPResourceBrowser.tsx
+++ b/src/web-ui/src/infrastructure/config/components/MCPResourceBrowser.tsx
@@ -1,9 +1,9 @@
import React, { useState, useEffect, useCallback } from 'react';
-import { Button } from '@bitfun/ui';
+import { Button, Icon, ScrollArea } from '@bitfun/ui';
import { useTranslation } from 'react-i18next';
-import { FileText, FileImage, FileJson, FileCode, File, Search as SearchIcon, ArrowLeft } from 'lucide-react';
+import { FileText, FileImage, FileJson, FileCode } from 'lucide-react';
import MCPAPI, { MCPResource } from '../../api/service-api/MCPAPI';
import { createLogger } from '@/shared/utils/logger';
import './MCPResourceBrowser.scss';
@@ -115,13 +115,13 @@ export const MCPResourceBrowser: React.FC = ({ serverId
};
const getMimeTypeIcon = (mimeType?: string): React.ReactNode => {
- if (!mimeType) return ;
+ if (!mimeType) return ;
if (mimeType.startsWith('text/')) return ;
if (mimeType.startsWith('image/')) return ;
if (mimeType.includes('json')) return ;
if (mimeType.includes('html')) return ;
if (mimeType.includes('pdf')) return ;
- return ;
+ return ;
};
return (
@@ -159,13 +159,13 @@ export const MCPResourceBrowser: React.FC = ({ serverId
-
+
{loading ? (
{t('resourceBrowser.loading.resources')}
) : filteredResources.length === 0 ? (
-
+
{searchQuery ? t('resourceBrowser.empty.noMatch') : t('resourceBrowser.empty.noResources')}
@@ -190,7 +190,7 @@ export const MCPResourceBrowser: React.FC = ({ serverId
))
)}
-
+
{selectedResource ? (
@@ -204,18 +204,18 @@ export const MCPResourceBrowser: React.FC
= ({ serverId
{selectedResource.mimeType}
)}
-
+
{loadingContent ? (
{t('resourceBrowser.loading.content')}
) : resourceContent ? (
{resourceContent}
) : null}
-
+
>
) : (
{t('resourceBrowser.empty.selectToView')}
diff --git a/src/web-ui/src/infrastructure/config/components/McpToolsConfig.scss b/src/web-ui/src/infrastructure/config/components/McpToolsConfig.scss
index a0792dae6e..8dd7915b79 100644
--- a/src/web-ui/src/infrastructure/config/components/McpToolsConfig.scss
+++ b/src/web-ui/src/infrastructure/config/components/McpToolsConfig.scss
@@ -204,10 +204,13 @@
margin: 0;
}
+ .bitfun-mcp-tools__import-list {
+ max-height: 120px;
+ }
+
ul {
max-height: 120px;
margin: 0;
- overflow: auto;
}
}
diff --git a/src/web-ui/src/infrastructure/config/components/McpToolsConfig.tsx b/src/web-ui/src/infrastructure/config/components/McpToolsConfig.tsx
index d1c6e4a74e..477f24b8e2 100644
--- a/src/web-ui/src/infrastructure/config/components/McpToolsConfig.tsx
+++ b/src/web-ui/src/infrastructure/config/components/McpToolsConfig.tsx
@@ -4,16 +4,14 @@
* Uses settings/mcp-tools for page title/subtitle, settings/mcp for the MCP section.
*/
-import { Button, IconButton, Modal, Tooltip } from '@bitfun/ui';
+import { Button, Icon, IconButton, Modal, Tooltip } from '@bitfun/ui';
import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
FileJson,
RefreshCw,
- X,
Play,
Square,
- CheckCircle,
AlertTriangle,
MinusCircle,
KeyRound,
@@ -958,7 +956,7 @@ const McpToolsConfig: React.FC = () => {
const getStatusIcon = (status: string): React.ReactNode => {
const s = status.toLowerCase();
- if (s.includes('healthy') || s.includes('connected')) return ;
+ if (s.includes('healthy') || s.includes('connected')) return ;
if (s.includes('starting') || s.includes('reconnecting')) return ;
if (s.includes('failed') || s.includes('stopped') || s.includes('auth'))
return ;
@@ -1113,7 +1111,7 @@ const McpToolsConfig: React.FC = () => {
size="sm"
onClick={() => setShowJsonEditor(!showJsonEditor)}
aria-label={showJsonEditor ? tMcp('actions.backToList') : tMcp('actions.jsonConfig')}
- icon={showJsonEditor ? : }
+ icon={showJsonEditor ? : }
/>
>
diff --git a/src/web-ui/src/infrastructure/config/components/MemorySettingsPage.tsx b/src/web-ui/src/infrastructure/config/components/MemorySettingsPage.tsx
index 4ae26b6cb0..df8f932b87 100644
--- a/src/web-ui/src/infrastructure/config/components/MemorySettingsPage.tsx
+++ b/src/web-ui/src/infrastructure/config/components/MemorySettingsPage.tsx
@@ -1,7 +1,7 @@
-import { Switch, IconButton, ConfirmDialog, Select, type SelectOption, Tooltip } from '@bitfun/ui';
+import { ConfirmDialog, Icon, IconButton, Select, type SelectOption, Switch, Tooltip } from '@bitfun/ui';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
-import { ChevronDown, ChevronUp, FolderOpen, RotateCcw, Trash2 } from 'lucide-react';
+import { ChevronUp, FolderOpen, RotateCcw, Trash2 } from 'lucide-react';
import { ConfigPageLoading, NumberInput } from '@/component-library';
import { useNotification } from '@/shared/notification-system';
@@ -381,7 +381,7 @@ const MemorySettingsPage: React.FC = () => {
onClick={() => setAdvancedOpen((open) => !open)}
aria-label={t(advancedOpen ? 'actions.collapseAdvanced' : 'actions.expandAdvanced')}
aria-expanded={advancedOpen}
- icon={advancedOpen ? : }
+ icon={advancedOpen ? : }
/>
)}
diff --git a/src/web-ui/src/infrastructure/config/components/ModelSettingsPage.scss b/src/web-ui/src/infrastructure/config/components/ModelSettingsPage.scss
index 1a06368179..e0bbbcc2a5 100644
--- a/src/web-ui/src/infrastructure/config/components/ModelSettingsPage.scss
+++ b/src/web-ui/src/infrastructure/config/components/ModelSettingsPage.scss
@@ -7,7 +7,6 @@
display: flex;
flex-direction: column;
height: 100%;
- overflow-y: auto;
overflow-x: hidden;
color: var(--bf-appearance-token-color-text-primary);
font-family: $font-family-sans;
@@ -134,7 +133,6 @@
&--form {
- overflow-y: auto;
overflow-x: hidden;
padding: 0;
}
@@ -444,7 +442,6 @@
&__tab-content {
flex: 1;
- overflow-y: auto;
overflow-x: hidden;
min-height: 0;
padding: $size-gap-4 0;
@@ -1636,7 +1633,6 @@
&__form-scrollable {
flex: 1 1 auto;
min-height: 0;
- overflow-y: auto;
overflow-x: hidden;
padding-left: $size-gap-6;
padding-right: calc(#{$size-gap-6} + 4px);
@@ -2051,7 +2047,6 @@
&__content--selection {
- overflow-y: auto;
overflow-x: hidden;
padding: 0;
}
@@ -2410,11 +2405,13 @@
line-height: 1.6;
}
- ul {
+ .bitfun-model-settings__subscription-logout-list {
max-height: 160px;
+ }
+
+ ul {
margin: 0;
padding-left: var(--bf-appearance-token-size-gap-5);
- overflow: auto;
color: var(--bf-appearance-token-color-text-primary);
font-size: var(--bf-appearance-token-font-size-sm);
line-height: 1.7;
diff --git a/src/web-ui/src/infrastructure/config/components/ModelSettingsPage.tsx b/src/web-ui/src/infrastructure/config/components/ModelSettingsPage.tsx
index 87db1368e0..a8bba66fb4 100644
--- a/src/web-ui/src/infrastructure/config/components/ModelSettingsPage.tsx
+++ b/src/web-ui/src/infrastructure/config/components/ModelSettingsPage.tsx
@@ -1,7 +1,7 @@
-import { Button, Card, Switch, IconButton, Input, Modal, SearchField, Select, Tooltip } from '@bitfun/ui';
+import { Button, Card, Icon, IconButton, Input, Modal, SearchField, Select, Switch, Tooltip, ScrollArea } from '@bitfun/ui';
import React, { useState, useEffect, useMemo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
-import { Plus, SquarePen, Trash2, Wifi, Loader, RefreshCw, AlertTriangle, X, Settings, ExternalLink, Eye, EyeOff, ChevronDown, ChevronRight, ChevronUp, Info, Brain, FolderOpen, Search as SearchIcon } from 'lucide-react';
+import { Trash2, Wifi, Loader, RefreshCw, AlertTriangle, EyeOff, ChevronUp, Brain, FolderOpen } from 'lucide-react';
import { Select as LegacySelect, NumberInput, Textarea, type SelectOption } from '@/component-library';
import {
AIModelConfig as AIModelConfigType,
@@ -233,7 +233,7 @@ function dedupeSelectedModelDraftsByModelName(drafts: SelectedModelDraft[]): Sel
/**
* Compute the stored request URL from a base URL and provider format.
- * For gemini, stores the bare base (no /v1beta/models/... suffix) —
+ * For gemini, stores the bare base (no /v1beta/models/... suffix)
* the backend dynamically appends /v1beta/models/{model}:streamGenerateContent?alt=sse.
*/
function resolveRequestUrl(baseUrl: string, provider: string, _modelName = ''): string {
@@ -1944,7 +1944,7 @@ const ModelSettingsPage: React.FC = () => {
onClick={handleSelectCustom}
>
-
+
{t('providerSelection.customTitle')}
{t('providerSelection.customDescription')}
@@ -1959,7 +1959,7 @@ const ModelSettingsPage: React.FC = () => {
}
+ leadingIcon={
}
size="sm"
className="bitfun-model-settings__provider-search"
data-testid="settings-model-provider-search"
@@ -2014,11 +2014,11 @@ const ModelSettingsPage: React.FC = () => {
}
}}
>
-
+
{t('providerSelection.getApiKey')}
)}
-
+
))}
@@ -2040,7 +2040,7 @@ const ModelSettingsPage: React.FC = () => {
{isProviderListCollapsed
? t('providerSelection.showAllProviders', { count: matchedProviders.length })
: t('providerSelection.collapseProviders')}
- {isProviderListCollapsed ?
:
}
+ {isProviderListCollapsed ?
:
}
)}
@@ -2173,7 +2173,7 @@ const ModelSettingsPage: React.FC = () => {
aria-label={apiKeyVisibilityLabel}
title={apiKeyVisibilityLabel}
>
- {showApiKey ? : }
+ {showApiKey ? : }
);
@@ -2270,7 +2270,7 @@ const ModelSettingsPage: React.FC = () => {
- {isExpanded ? : }
+ {isExpanded ? : }
{modelDisplayName}
{hasUnsavedChanges && (
@@ -2297,7 +2297,7 @@ const ModelSettingsPage: React.FC = () => {
e.stopPropagation();
removeSelectedModelDraft(draft.modelName);
}}
- icon={
}
+ icon={
}
/>
)}
@@ -2306,9 +2306,9 @@ const ModelSettingsPage: React.FC = () => {
{categoryLabel}
- {' · '}
+ {' '}
{formatTokenCountShort(draft.contextWindow)} ctx
- {' · '}
+ {' '}
{formatReasoningSummary(draft, reasoningProjection)}
@@ -2351,7 +2351,7 @@ const ModelSettingsPage: React.FC = () => {
tabIndex={0}
aria-label={t('form.contextWindowHint')}
>
-
+
@@ -2504,7 +2504,7 @@ const ModelSettingsPage: React.FC = () => {
return (
<>
-
+
{
}));
}}
placeholder={t('form.baseUrl')}
- options={currentTemplate.baseUrlOptions.map(opt => ({ label: opt.url, value: opt.url, description: `${opt.format.toUpperCase()} · ${opt.note}` }))}
+ options={currentTemplate.baseUrlOptions.map(opt => ({ label: opt.url, value: opt.url, description: `${opt.format.toUpperCase()} ${opt.note}` }))}
size="small"
/>
)}
@@ -2828,7 +2828,7 @@ const ModelSettingsPage: React.FC = () => {
tabIndex={0}
aria-label={t('advancedSettings.customHeaders.hint')}
>
-
+
@@ -2884,12 +2884,12 @@ const ModelSettingsPage: React.FC = () => {
aria-label={t('actions.delete')}
size="sm"
onClick={() => { const nh = { ...editingConfig.custom_headers }; delete nh[key]; setEditingConfig(prev => ({ ...prev, custom_headers: Object.keys(nh).length > 0 ? nh : undefined })); }}
- icon={}
+ icon={}
/>
))}
-
setEditingConfig(prev => ({ ...prev, custom_headers: { ...prev?.custom_headers, '': '' } }))} className="bitfun-model-settings__add-header-btn" leadingIcon={}>{t('advancedSettings.customHeaders.addHeader')}
+
setEditingConfig(prev => ({ ...prev, custom_headers: { ...prev?.custom_headers, '': '' } }))} className="bitfun-model-settings__add-header-btn" leadingIcon={}>{t('advancedSettings.customHeaders.addHeader')}
@@ -2913,7 +2913,7 @@ const ModelSettingsPage: React.FC = () => {
tabIndex={0}
aria-label={t('advancedSettings.customRequestBody.hint')}
>
-
+
@@ -2957,7 +2957,7 @@ const ModelSettingsPage: React.FC = () => {
>
)}
-
+
>
@@ -3075,7 +3075,7 @@ const ModelSettingsPage: React.FC = () => {
aria-label={t('actions.edit')}
size="sm"
onClick={() => handleEdit(config)}
- icon={}
+ icon={}
/>
@@ -3121,7 +3121,7 @@ const ModelSettingsPage: React.FC = () => {
tabIndex={0}
aria-label={t('streamTtftTimeout.hint')}
>
-
+
@@ -3137,7 +3137,7 @@ const ModelSettingsPage: React.FC = () => {
tabIndex={0}
aria-label={t('streamIdleTimeout.hint')}
>
-
+
@@ -3201,7 +3201,7 @@ const ModelSettingsPage: React.FC = () => {
{showSubscriptionMigrationNotice && (
-
+
{t('subscriptionAuth.secureStoreMigrationNotice')}
{
size="sm"
variant="outline"
onClick={() => void handleOpenSubscriptionAuthorization(loginPanel.authorizationUrl)}
- leadingIcon={}
+ leadingIcon={}
>
{t('subscriptionAuth.openAuthorization')}
@@ -3452,7 +3452,7 @@ const ModelSettingsPage: React.FC = () => {
aria-label={t('actions.addProvider')}
size="sm"
onClick={handleCreateNew}
- icon={
}
+ icon={
}
/>
)}
@@ -3461,7 +3461,7 @@ const ModelSettingsPage: React.FC = () => {
{t('empty.noModels')}
-
}>
+
}>
{t('actions.createFirst')}
@@ -3484,7 +3484,7 @@ const ModelSettingsPage: React.FC = () => {
aria-label={t('actions.edit')}
size="sm"
onClick={() => handleEditProvider(group.models[0])}
- icon={
}
+ icon={
}
/>
@@ -3509,7 +3509,7 @@ const ModelSettingsPage: React.FC = () => {
aria-label={t('modelsDevCatalog.viewDetails')}
size="sm"
onClick={() => setShowModelsDevDetails(true)}
- icon={
}
+ icon={
}
/>
{
-
{modelsDevStatus?.cache_path || '—'}
+
{modelsDevStatus?.cache_path || ''}
{
- {modelsDevStatus?.revision ? `${modelsDevStatus.revision.slice(0, 12)}…` : '—'}
+ {modelsDevStatus?.revision ? `${modelsDevStatus.revision.slice(0, 12)}` : ''}
-
+
{t('modelsDevCatalog.offlineTitle')}
{t('modelsDevCatalog.offlineDescription')}
@@ -3683,7 +3683,7 @@ const ModelSettingsPage: React.FC = () => {
variant="outline"
size="sm"
onClick={() => void systemAPI.openExternal(MODELS_DEV_DOWNLOAD_URL)}
- leadingIcon={
}
+ leadingIcon={
}
>
{t('modelsDevCatalog.downloadOriginal')}
@@ -3743,11 +3743,13 @@ const ModelSettingsPage: React.FC = () => {
: t('subscriptionAuth.logoutNoAffectedModels')}
{!!subscriptionLogoutRequest?.affectedModels.length && (
-
- {subscriptionLogoutRequest.affectedModels.map((model) => (
- - {model.name} · {model.model_name}
- ))}
-
+
+
+ {subscriptionLogoutRequest.affectedModels.map((model) => (
+ - {model.name} {model.model_name}
+ ))}
+
+
)}
{t('subscriptionAuth.logoutConsequence')}
diff --git a/src/web-ui/src/infrastructure/config/components/QuickActionsConfig.tsx b/src/web-ui/src/infrastructure/config/components/QuickActionsConfig.tsx
index 9d80449b67..b69f89e5fc 100644
--- a/src/web-ui/src/infrastructure/config/components/QuickActionsConfig.tsx
+++ b/src/web-ui/src/infrastructure/config/components/QuickActionsConfig.tsx
@@ -1,7 +1,7 @@
-import { Button, Switch, IconButton, Input, Modal, Tooltip } from '@bitfun/ui';
+import { Button, Icon, IconButton, Input, Modal, Switch, Tooltip } from '@bitfun/ui';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
-import { Zap, GitCommitHorizontal, GitPullRequest, Pencil, Trash2, Plus, Check } from 'lucide-react';
+import { Zap, GitCommitHorizontal, GitPullRequest, Trash2 } from 'lucide-react';
import { ConfigPageLoading, Textarea } from '@/component-library';
import {
ConfigPageHeader,
@@ -127,7 +127,7 @@ const ActionFormModal: React.FC
= ({ isOpen, target, onClo
size="sm"
onClick={() => onSubmit(label.trim(), prompt.trim())}
disabled={!canSubmit}
- leadingIcon={}
+ leadingIcon={}
>
{isEdit ? t('modal.saveEdit') : t('modal.confirmAdd')}
@@ -174,7 +174,7 @@ const ActionRow: React.FC = ({ action, onToggle, onEdit, onDelet
size="sm"
aria-label={t('edit.button')}
onClick={() => onEdit(action)}
- icon={}
+ icon={}
/>
{canDelete && (
@@ -305,7 +305,7 @@ const QuickActionsConfig: React.FC = () => {
size="sm"
variant="outline"
onClick={() => setModalTarget(null)}
- leadingIcon={}
+ leadingIcon={}
>
{t('add.button')}
@@ -321,7 +321,7 @@ const QuickActionsConfig: React.FC = () => {
size="sm"
variant="outline"
onClick={() => setModalTarget(null)}
- leadingIcon={}
+ leadingIcon={}
>
{t('add.button')}
diff --git a/src/web-ui/src/infrastructure/config/components/ReasoningConfigPanel.scss b/src/web-ui/src/infrastructure/config/components/ReasoningConfigPanel.scss
index f7d9829e36..a06d2cbab4 100644
--- a/src/web-ui/src/infrastructure/config/components/ReasoningConfigPanel.scss
+++ b/src/web-ui/src/infrastructure/config/components/ReasoningConfigPanel.scss
@@ -9,7 +9,6 @@
&__body {
flex: 1;
min-height: 0;
- overflow-y: auto;
}
&__footer {
diff --git a/src/web-ui/src/infrastructure/config/components/ReasoningConfigPanel.tsx b/src/web-ui/src/infrastructure/config/components/ReasoningConfigPanel.tsx
index d9d6e4f0e5..0a2226dd3f 100644
--- a/src/web-ui/src/infrastructure/config/components/ReasoningConfigPanel.tsx
+++ b/src/web-ui/src/infrastructure/config/components/ReasoningConfigPanel.tsx
@@ -1,4 +1,4 @@
-import { Button } from '@bitfun/ui';
+import { Button, ScrollArea } from '@bitfun/ui';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { AlertTriangle } from 'lucide-react';
import { useTranslation } from 'react-i18next';
@@ -102,7 +102,7 @@ export const ReasoningConfigPanel: React.FC = ({
data-bf-component="reasoning-config-panel"
data-bf-part="root"
>
- = ({
onChange={setDraft}
onValidationChange={setEditorInvalid}
/>
-
+
= {};
vi.mock('@bitfun/ui', () => ({
+ ScrollArea: ({ children, ...props }: React.HTMLAttributes
) => {children}
,
+ Icon: ({ name, ...props }: { name: string } & React.HTMLAttributes) => ,
Tooltip: ({ children }: { children: React.ReactNode }) => <>{children}>,
Button: ({
children,
diff --git a/src/web-ui/src/infrastructure/config/components/ReasoningPresetEditor.tsx b/src/web-ui/src/infrastructure/config/components/ReasoningPresetEditor.tsx
index 58249a60d0..f94928490f 100644
--- a/src/web-ui/src/infrastructure/config/components/ReasoningPresetEditor.tsx
+++ b/src/web-ui/src/infrastructure/config/components/ReasoningPresetEditor.tsx
@@ -1,14 +1,9 @@
-import { Button, Switch, IconButton, Input, Select, Tooltip } from '@bitfun/ui';
+import { Button, Icon, IconButton, Input, Select, Switch, Tooltip } from '@bitfun/ui';
import React, { useMemo, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import {
AlertTriangle,
ArrowDown,
- ArrowUp,
- ChevronDown,
- ChevronRight,
- Info,
- Plus,
Trash2,
} from 'lucide-react';
import { useTranslation } from 'react-i18next';
@@ -373,11 +368,7 @@ export const ReasoningPresetEditor: React.FC = ({
{option.label}
-
+
)}
@@ -626,11 +617,11 @@ export const ReasoningPresetEditor: React.FC = ({
tabIndex={0}
aria-label={t('reasoningPresets.customTooltip')}
>
-
+
- }>
+ }>
{t('reasoningPresets.add')}
@@ -673,7 +664,7 @@ export const ReasoningPresetEditor: React.FC = ({
aria-expanded={expanded}
aria-label={preset.label?.trim() || preset.id}
>
- {expanded ? : }
+ {expanded ? : }
{expanded ? (
@@ -725,7 +716,7 @@ export const ReasoningPresetEditor: React.FC
= ({
size="sm"
disabled={disabled || presetIndex === 0}
onClick={() => movePreset(presetIndex, -1)}
- icon={}
+ icon={}
/>
@@ -872,7 +863,7 @@ export const ReasoningPresetEditor: React.FC = ({
size="sm"
disabled={disabled || actionIndex === 0}
onClick={() => moveAction(presetIndex, actionIndex, -1)}
- icon={}
+ icon={}
/>
@@ -913,7 +904,7 @@ export const ReasoningPresetEditor: React.FC = ({
),
],
})}
- leadingIcon={}
+ leadingIcon={}
>
{t('reasoningPresets.addAction')}
diff --git a/src/web-ui/src/infrastructure/config/components/RuntimeSettingsPages.scss b/src/web-ui/src/infrastructure/config/components/RuntimeSettingsPages.scss
index dbca634501..41abe86f4d 100644
--- a/src/web-ui/src/infrastructure/config/components/RuntimeSettingsPages.scss
+++ b/src/web-ui/src/infrastructure/config/components/RuntimeSettingsPages.scss
@@ -145,7 +145,6 @@
&__pet-list {
max-height: min(360px, 52vh);
- overflow-y: auto;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: $size-gap-2;
diff --git a/src/web-ui/src/infrastructure/config/components/RuntimeSettingsPages.tsx b/src/web-ui/src/infrastructure/config/components/RuntimeSettingsPages.tsx
index 96afe27a68..2cfefb7fc6 100644
--- a/src/web-ui/src/infrastructure/config/components/RuntimeSettingsPages.tsx
+++ b/src/web-ui/src/infrastructure/config/components/RuntimeSettingsPages.tsx
@@ -1,7 +1,7 @@
-import { Button, Switch, IconButton, Modal, Select, Tooltip } from '@bitfun/ui';
+import { Button, Icon, IconButton, Modal, Select, Switch, Tooltip, ScrollArea } from '@bitfun/ui';
import React, { useState, useEffect, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
-import { RefreshCw, ChevronDown, Plus, Trash2, Check, Info } from 'lucide-react';
+import { RefreshCw, Trash2 } from 'lucide-react';
import { NumberInput, ConfigPageLoading, Select as LegacySelect, type SelectOption } from '@/component-library';
import { confirmDanger } from '@/infrastructure/confirm-dialog';
import { ConfigPageHeader, ConfigPageLayout, ConfigPageContent, ConfigPageSection, ConfigPageRow } from './common';
@@ -941,7 +941,7 @@ const RuntimeSettingsPage: React.FC = ({ page }) => {
onClick={() => void handleImportCompanionPet()}
disabled={!IS_TAURI_DESKTOP || companionPetImporting}
title={t('features.pet.importHint')}
- leadingIcon={}
+ leadingIcon={}
>
{companionPetImporting ? t('features.pet.importing') : t('features.pet.import')}
@@ -980,13 +980,10 @@ const RuntimeSettingsPage: React.FC = ({ page }) => {
{selectedCompanionPetOption?.label ?? t('features.pet.petPlaceholder')}
-
+
{companionPetListExpanded && (
- = ({ page }) => {
{isSelected && (
-
+
)}
{isUserPet && IS_TAURI_DESKTOP && pet && (
@@ -1065,7 +1062,7 @@ const RuntimeSettingsPage: React.FC = ({ page }) => {
);
})}
-
+
)}
@@ -1394,7 +1391,7 @@ const RuntimeSettingsPage: React.FC = ({ page }) => {
padding: '8px 0 4px',
}}
>
-
+
{t('computerUse.platformNote')}:
{computerUsePlatformMessage}
diff --git a/src/web-ui/src/infrastructure/config/components/SkillsConfig.tsx b/src/web-ui/src/infrastructure/config/components/SkillsConfig.tsx
index 96f0c46bea..7084dc3415 100644
--- a/src/web-ui/src/infrastructure/config/components/SkillsConfig.tsx
+++ b/src/web-ui/src/infrastructure/config/components/SkillsConfig.tsx
@@ -1,8 +1,8 @@
/* eslint-disable @typescript-eslint/no-use-before-define */
-import { Button, Card, CardBody, IconButton, ConfirmDialog, Field, Input, SearchField, Select, Tooltip } from '@bitfun/ui';
+import { Button, Card, CardBody, ConfirmDialog, Field, Icon, IconButton, Input, SearchField, Select, Tooltip } from '@bitfun/ui';
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
import { useTranslation } from 'react-i18next';
-import { Plus, Trash2, RefreshCw, FolderOpen, X, Download, CheckCircle2, TrendingUp, Search as SearchIcon } from 'lucide-react';
+import { Trash2, RefreshCw, FolderOpen, TrendingUp } from 'lucide-react';
import { useI18n } from '@/infrastructure/i18n/hooks/useI18n';
@@ -229,7 +229,7 @@ const SkillsConfig: React.FC = () => {
aria-label={t('form.closeTooltip')}
size="sm"
onClick={resetForm}
- icon={}
+ icon={}
/>
@@ -453,7 +453,7 @@ const SkillsConfig: React.FC = () => {
{skill.name}
{isInstalled ? (
-
+
{t('market.item.installed')}
) : null}
@@ -487,7 +487,7 @@ const SkillsConfig: React.FC = () => {
{isInstalled ? (
- }>
+ }>
{t('market.item.installed')}
@@ -503,7 +503,7 @@ const SkillsConfig: React.FC = () => {
size="sm"
onClick={() => handleDownload(skill, 'project')}
disabled={isDownloading || !hasWorkspace}
- leadingIcon={}
+ leadingIcon={}
>
{isDownloading ? t('market.item.downloading') : t('market.item.downloadProject')}
@@ -518,7 +518,7 @@ const SkillsConfig: React.FC = () => {
size="sm"
onClick={() => handleDownload(skill, 'user')}
disabled={isDownloading}
- leadingIcon={}
+ leadingIcon={}
>
{isDownloading ? t('market.item.downloading') : t('market.item.downloadUser')}
@@ -557,7 +557,7 @@ const SkillsConfig: React.FC = () => {
size="sm"
onClick={() => { setFormLevel(level); setShowAddForm(true); }}
disabled={level === 'project' && !hasWorkspace}
- icon={}
+ icon={}
/>
>
@@ -663,7 +663,7 @@ const SkillsConfig: React.FC = () => {
}
+ leadingIcon={}
value={marketKeyword}
onValueChange={(value) => setMarketKeyword(value)}
onSearch={handleMarketSearch}
@@ -686,7 +686,7 @@ const SkillsConfig: React.FC = () => {
{renderAddForm('user')}
{userSkills.length === 0 && !(showAddForm && formLevel === 'user') ? (
-
{ setFormLevel('user'); setShowAddForm(true); }} leadingIcon={}>
+ { setFormLevel('user'); setShowAddForm(true); }} leadingIcon={}>
{t('toolbar.addTooltip')}
@@ -704,7 +704,7 @@ const SkillsConfig: React.FC = () => {
{!hasWorkspace &&
{t('messages.noWorkspace')}
}
{hasWorkspace && (
-
{ setFormLevel('project'); setShowAddForm(true); }} leadingIcon={}>
+ { setFormLevel('project'); setShowAddForm(true); }} leadingIcon={}>
{t('toolbar.addTooltip')}
diff --git a/src/web-ui/src/infrastructure/config/components/UsageStatisticsConfig.scss b/src/web-ui/src/infrastructure/config/components/UsageStatisticsConfig.scss
index 2aed283f34..06722db0ab 100644
--- a/src/web-ui/src/infrastructure/config/components/UsageStatisticsConfig.scss
+++ b/src/web-ui/src/infrastructure/config/components/UsageStatisticsConfig.scss
@@ -114,7 +114,6 @@
display: flex;
flex-direction: column;
max-height: var(--usage-statistics-table-max-height);
- overflow-y: auto;
}
&__hit-rate-row {
@@ -215,7 +214,6 @@
&__table-scroll {
min-width: 0;
max-height: var(--usage-statistics-table-max-height);
- overflow: auto;
}
&__table {
diff --git a/src/web-ui/src/infrastructure/config/components/UsageStatisticsConfig.test.tsx b/src/web-ui/src/infrastructure/config/components/UsageStatisticsConfig.test.tsx
index 28c870e915..0659fae0ef 100644
--- a/src/web-ui/src/infrastructure/config/components/UsageStatisticsConfig.test.tsx
+++ b/src/web-ui/src/infrastructure/config/components/UsageStatisticsConfig.test.tsx
@@ -34,6 +34,16 @@ vi.mock('@/infrastructure/i18n', () => ({
}));
vi.mock('@bitfun/ui', () => ({
+ ScrollArea: ({ children, ...props }: React.HTMLAttributes) => {children}
,
+ FormSection: ({
+ children,
+ title,
+ ...props
+ }: React.HTMLAttributes & { title?: React.ReactNode }) => (
+
+ ),
+ FieldGroup: ({ children, ...props }: React.HTMLAttributes) => {children}
,
+ Icon: ({ name, ...props }: { name: string } & React.HTMLAttributes) => ,
Tooltip: ({ children }: React.PropsWithChildren) => <>{children}>,
IconButton: ({
children,
diff --git a/src/web-ui/src/infrastructure/config/components/UsageStatisticsConfig.tsx b/src/web-ui/src/infrastructure/config/components/UsageStatisticsConfig.tsx
index 81ac402810..0283187d5c 100644
--- a/src/web-ui/src/infrastructure/config/components/UsageStatisticsConfig.tsx
+++ b/src/web-ui/src/infrastructure/config/components/UsageStatisticsConfig.tsx
@@ -1,5 +1,5 @@
import React, { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react';
-import { BarChart3, Search, X } from 'lucide-react';
+import { BarChart3 } from 'lucide-react';
import {
ConfigPageLoading,
ConfigPageMessage,
@@ -23,7 +23,7 @@ import {
ConfigPageSectionStack,
} from './common';
import './UsageStatisticsConfig.scss';
-import { IconButton, Input, Select, Tooltip } from '@bitfun/ui';
+import { Icon, IconButton, Input, Select, Tooltip, ScrollArea } from '@bitfun/ui';
// ---------------------------------------------------------------------------
// Chart palette — appearance tokens only (literal vars so the theme color
@@ -272,7 +272,7 @@ const DistributionPanel: React.FC<{
{title}
-
+
{t('table.caption', { dimension: title })}
@@ -315,7 +315,7 @@ const DistributionPanel: React.FC<{
})}
-
+
);
@@ -329,7 +329,7 @@ const ModelCacheHitRateList: React.FC<{ entries: UsageStatisticsEntry[] }> = ({
const { t, formatNumber } = useI18n('settings/usage');
return (
-
@@ -376,7 +376,7 @@ const ModelCacheHitRateList: React.FC<{ entries: UsageStatisticsEntry[] }> = ({
);
})}
-
+
);
};
@@ -928,7 +928,7 @@ const UsageStatisticsConfig: React.FC = () => {
data-testid="usage-filter-input"
maxLength={100}
disabled={loading}
- leading={}
+ leading={}
trailing={filterInput ? (
{
aria-label={t('filter.clear')}
onClick={clearFilter}
disabled={loading}
- icon={}
+ icon={}
/>
) : undefined}
diff --git a/src/web-ui/src/infrastructure/config/components/VoiceInputConfig.tsx b/src/web-ui/src/infrastructure/config/components/VoiceInputConfig.tsx
index 394f31a763..ce2d346e76 100644
--- a/src/web-ui/src/infrastructure/config/components/VoiceInputConfig.tsx
+++ b/src/web-ui/src/infrastructure/config/components/VoiceInputConfig.tsx
@@ -1,7 +1,7 @@
-import { Button, Input, Select, StatusPill, Switch, type SelectOption, type StatusPillTone } from '@bitfun/ui';
+import { Button, Icon, Input, Select, type SelectOption, StatusPill, type StatusPillTone, Switch } from '@bitfun/ui';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
-import { CheckCircle2, CloudOff, Download, HardDrive, PhoneCall } from 'lucide-react';
+import { CloudOff, HardDrive, PhoneCall } from 'lucide-react';
import {
LOCAL_SENSEVOICE_SMALL_INT8_MODEL_ID,
speechAPI,
@@ -304,11 +304,11 @@ const VoiceInputConfig: React.FC = () => {
const progressPercent = Math.min(100, Math.max(0, selectedModel?.progress?.percent ?? 0));
const statusIcon = status === 'ready'
- ?
+ ?
: status === 'unavailable'
?
: status === 'setup'
- ?
+ ?
: ;
return (
@@ -362,7 +362,7 @@ const VoiceInputConfig: React.FC = () => {
size="sm"
onClick={() => handleDownload(selectedModel)}
loading={busyAction === `download:${selectedModel.modelId}`}
- leadingIcon={}
+ leadingIcon={}
>
{t('status.downloadAndEnable')}
diff --git a/src/web-ui/src/infrastructure/config/components/WorktreeSettingsPage.test.tsx b/src/web-ui/src/infrastructure/config/components/WorktreeSettingsPage.test.tsx
index 49d8e36a77..aba4bd93fa 100644
--- a/src/web-ui/src/infrastructure/config/components/WorktreeSettingsPage.test.tsx
+++ b/src/web-ui/src/infrastructure/config/components/WorktreeSettingsPage.test.tsx
@@ -103,6 +103,7 @@ vi.mock('@/component-library', () => ({
}));
vi.mock('@bitfun/ui', () => ({
+ Icon: ({ name, ...props }: { name: string } & React.HTMLAttributes) => ,
Tooltip: ({ children }: React.PropsWithChildren) => <>{children}>,
Button: ({ children, disabled, onClick }: React.ButtonHTMLAttributes) => (
{children}
diff --git a/src/web-ui/src/infrastructure/config/components/WorktreeSettingsPage.tsx b/src/web-ui/src/infrastructure/config/components/WorktreeSettingsPage.tsx
index 0eb23f6f33..b4c6e29c1f 100644
--- a/src/web-ui/src/infrastructure/config/components/WorktreeSettingsPage.tsx
+++ b/src/web-ui/src/infrastructure/config/components/WorktreeSettingsPage.tsx
@@ -1,4 +1,4 @@
-import { Button, Switch, IconButton, Input, ConfirmDialog, Tooltip } from '@bitfun/ui';
+import { Button, ConfirmDialog, Icon, IconButton, Input, Switch, Tooltip } from '@bitfun/ui';
import React, {
useCallback,
useEffect,
@@ -8,7 +8,6 @@ import React, {
} from 'react';
import {
FolderGit2,
- GitBranch,
LoaderCircle,
MessageSquareText,
RotateCcw,
@@ -730,7 +729,7 @@ const WorktreeSettingsPage: React.FC = () => {
data-bf-part="root"
>
}
+ icon={}
title={t('settings.title')}
subtitle={t('settings.description')}
/>
diff --git a/src/web-ui/src/infrastructure/config/components/common/ConfigCollectionItem.scss b/src/web-ui/src/infrastructure/config/components/common/ConfigCollectionItem.scss
index cdf8e39150..0a78199a4d 100644
--- a/src/web-ui/src/infrastructure/config/components/common/ConfigCollectionItem.scss
+++ b/src/web-ui/src/infrastructure/config/components/common/ConfigCollectionItem.scss
@@ -244,7 +244,6 @@
white-space: pre-wrap;
word-break: break-word;
max-height: 200px;
- overflow-y: auto;
}
.bitfun-collection-details__meta {
diff --git a/src/web-ui/src/infrastructure/config/components/common/ConfigCollectionItem.tsx b/src/web-ui/src/infrastructure/config/components/common/ConfigCollectionItem.tsx
index c66573e373..384815c7e2 100644
--- a/src/web-ui/src/infrastructure/config/components/common/ConfigCollectionItem.tsx
+++ b/src/web-ui/src/infrastructure/config/components/common/ConfigCollectionItem.tsx
@@ -1,5 +1,6 @@
import React, { useId, useState } from 'react';
-import { ChevronDown } from 'lucide-react';
+import { Icon } from '@bitfun/ui';
+;
import { PresenceBoundary } from '@/component-library';
import './ConfigCollectionItem.scss';
@@ -84,7 +85,7 @@ export const ConfigCollectionItem: React.FC = ({
aria-expanded={isExpanded}
aria-controls={detailsId}
>
-
+
) : null}
diff --git a/src/web-ui/src/infrastructure/config/components/common/ConfigPageLayout.scss b/src/web-ui/src/infrastructure/config/components/common/ConfigPageLayout.scss
index 01561474da..94cd53155f 100644
--- a/src/web-ui/src/infrastructure/config/components/common/ConfigPageLayout.scss
+++ b/src/web-ui/src/infrastructure/config/components/common/ConfigPageLayout.scss
@@ -9,8 +9,6 @@
display: flex;
flex-direction: column;
height: 100%;
- overflow-y: auto;
- overflow-x: hidden;
background: var(--bf-appearance-token-color-bg-scene);
color: var(--bf-appearance-token-color-text-primary);
font-family: var(--bf-appearance-token-font-family-sans);
diff --git a/src/web-ui/src/infrastructure/config/components/common/ConfigPageLayout.tsx b/src/web-ui/src/infrastructure/config/components/common/ConfigPageLayout.tsx
index 802e9a5932..6e0e28fd4e 100644
--- a/src/web-ui/src/infrastructure/config/components/common/ConfigPageLayout.tsx
+++ b/src/web-ui/src/infrastructure/config/components/common/ConfigPageLayout.tsx
@@ -1,5 +1,4 @@
-
-
+ import { FieldGroup, FormSection, ScrollArea } from '@bitfun/ui';
import React from 'react';
import './ConfigPageLayout.scss';
@@ -17,11 +16,11 @@ export const ConfigPageLayout: React.FC = ({
...props
}) => {
return (
-
+
{children}
{/* Real DOM spacer: keeps a guaranteed blank tail at the end of the scroll range. */}
-
+
);
};
@@ -104,27 +103,35 @@ export const ConfigPageSection: React.FC = ({
].filter(Boolean).join(' ');
return (
-
-
-
-
-
{title}
- {titleSuffix}
-
- {description && (
-
{description}
- )}
-
- {extra && (
-
- {extra}
-
- )}
-
-
+
+ {title}
+ {titleSuffix}
+
+ )}
+ description={description ? (
+ {description}
+ ) : undefined}
+ actions={extra ? (
+ {extra}
+ ) : undefined}
+ {...props}
+ >
+
{children}
-
-
+
+
);
};
diff --git a/src/web-ui/src/infrastructure/config/components/form-controls/ConfigStatus.tsx b/src/web-ui/src/infrastructure/config/components/form-controls/ConfigStatus.tsx
index 9df799efea..632065c6e3 100644
--- a/src/web-ui/src/infrastructure/config/components/form-controls/ConfigStatus.tsx
+++ b/src/web-ui/src/infrastructure/config/components/form-controls/ConfigStatus.tsx
@@ -1,6 +1,6 @@
import React from 'react';
-import { IconButton } from '@bitfun/ui';
-import { X } from 'lucide-react';
+import { Icon, IconButton } from '@bitfun/ui';
+;
import { useI18n } from '@/infrastructure/i18n';
export interface ConfigStatusProps {
@@ -63,7 +63,7 @@ export const ConfigStatus: React.FC = ({
onClick={onClose}
style={{ marginLeft: '8px' }}
aria-label={t('actions.close')}
- icon={}
+ icon={}
/>
)}
diff --git a/src/web-ui/src/infrastructure/peer-device/DeviceSurfaceSwitcher.scss b/src/web-ui/src/infrastructure/peer-device/DeviceSurfaceSwitcher.scss
index ed960adc96..d53951eeb0 100644
--- a/src/web-ui/src/infrastructure/peer-device/DeviceSurfaceSwitcher.scss
+++ b/src/web-ui/src/infrastructure/peer-device/DeviceSurfaceSwitcher.scss
@@ -93,7 +93,6 @@
min-width: 216px;
max-width: calc(100vw - 16px);
max-height: calc(100vh - 16px);
- overflow-y: auto;
padding: $size-gap-1;
z-index: 9999;
color: var(--bf-appearance-token-color-text-primary);
diff --git a/src/web-ui/src/infrastructure/peer-device/DeviceSurfaceSwitcher.tsx b/src/web-ui/src/infrastructure/peer-device/DeviceSurfaceSwitcher.tsx
index 34c3fd6120..31d44e5429 100644
--- a/src/web-ui/src/infrastructure/peer-device/DeviceSurfaceSwitcher.tsx
+++ b/src/web-ui/src/infrastructure/peer-device/DeviceSurfaceSwitcher.tsx
@@ -8,9 +8,9 @@
*/
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
-import { Button } from '@bitfun/ui';
+import { Button, Icon, ScrollArea } from '@bitfun/ui';
import { createPortal } from 'react-dom';
-import { Check, ChevronUp, Monitor, MonitorSmartphone, Loader2 } from 'lucide-react';
+import { ChevronUp, Monitor, MonitorSmartphone, Loader2 } from 'lucide-react';
import { useI18n } from '@/infrastructure/i18n/hooks/useI18n';
import { useNotification } from '@/shared/notification-system';
import { getAppearanceOverlayHost } from '@/infrastructure/appearance/runtime/AppearanceOverlayHost';
@@ -200,7 +200,7 @@ export const DeviceSurfaceSwitcher: React.FC = () => {
className="bitfun-device-switcher__backdrop"
onClick={() => setOpen(false)}
/>
-
{
{t('accountLogin.offline')}
)}
- {isCurrent &&
}
+ {isCurrent &&
}
{attached && (
{
{t('accountLogin.deviceSwitcher.hint')}
-
+
>,
getAppearanceOverlayHost(),
)}
diff --git a/src/web-ui/src/infrastructure/peer-device/PeerDirectoryBrowser.scss b/src/web-ui/src/infrastructure/peer-device/PeerDirectoryBrowser.scss
index d2b61cdb56..e482adf0dc 100644
--- a/src/web-ui/src/infrastructure/peer-device/PeerDirectoryBrowser.scss
+++ b/src/web-ui/src/infrastructure/peer-device/PeerDirectoryBrowser.scss
@@ -113,7 +113,6 @@
&__body {
flex: 1;
min-height: 0;
- overflow: auto;
padding: 8px;
}
diff --git a/src/web-ui/src/infrastructure/peer-device/PeerDirectoryBrowser.tsx b/src/web-ui/src/infrastructure/peer-device/PeerDirectoryBrowser.tsx
index 378ccea82c..a9c2e41532 100644
--- a/src/web-ui/src/infrastructure/peer-device/PeerDirectoryBrowser.tsx
+++ b/src/web-ui/src/infrastructure/peer-device/PeerDirectoryBrowser.tsx
@@ -3,17 +3,14 @@
* Lists directories on the peer via HostInvoke FS APIs.
*/
-import { Button, IconButton } from '@bitfun/ui';
+import { Button, Icon, IconButton, ScrollArea } from '@bitfun/ui';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { getAppearanceOverlayHost } from '@/infrastructure/appearance/runtime/AppearanceOverlayHost';
import {
- ArrowLeft,
- Folder,
Home,
Loader2,
RefreshCw,
- X,
} from 'lucide-react';
import { useI18n } from '@/infrastructure/i18n';
import { workspaceAPI } from '@/infrastructure/api';
@@ -214,7 +211,7 @@ export const PeerDirectoryBrowser: React.FC
= ({
>{title}
}
+ icon={}
size="md"
aria-label={t('peerDirectoryPicker.cancel')}
onClick={onCancel}
@@ -237,7 +234,7 @@ export const PeerDirectoryBrowser: React.FC = ({
data-bf-component="peer-device"
data-bf-part="toolButton"
>
-
+
= ({
- = ({
data-bf-part="item"
data-bf-state={selectedPath === entry.path ? 'selected' : undefined}
>
-
+
{entry.name}
))}
)}
-
+
= ({
data-bf-component="update"
data-bf-part="leadIcon"
>
-
+
= ({
{notes ? (
{t('update.releaseNotes')}
-
{notes}
+
+ {notes}
+
) : null}
diff --git a/src/web-ui/src/shared/announcement-system/components/AnnouncementToastItem.tsx b/src/web-ui/src/shared/announcement-system/components/AnnouncementToastItem.tsx
index d55113d16e..11efa59956 100644
--- a/src/web-ui/src/shared/announcement-system/components/AnnouncementToastItem.tsx
+++ b/src/web-ui/src/shared/announcement-system/components/AnnouncementToastItem.tsx
@@ -1,6 +1,6 @@
import React, { useEffect, useRef, useState } from 'react';
-import { Button } from '@bitfun/ui';
-import { X } from 'lucide-react';
+import { Button, Icon } from '@bitfun/ui';
+;
import type { AnnouncementCard } from '../types';
import { useAnnouncementStore } from '../store/announcementStore';
import { useAnnouncementI18n } from '../hooks/useAnnouncementI18n';
@@ -94,7 +94,7 @@ const AnnouncementToastItem: React.FC = ({ card }) => {
onClick={handleDismiss}
aria-label={t('announcements.common.close')}
>
-
+
)}
diff --git a/src/web-ui/src/shared/announcement-system/components/FeatureModal.tsx b/src/web-ui/src/shared/announcement-system/components/FeatureModal.tsx
index ecbe2085a2..da79fe93a0 100644
--- a/src/web-ui/src/shared/announcement-system/components/FeatureModal.tsx
+++ b/src/web-ui/src/shared/announcement-system/components/FeatureModal.tsx
@@ -1,6 +1,6 @@
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
-import { Button, IconButton } from '@bitfun/ui';
-import { X } from 'lucide-react';
+import { Button, Icon, IconButton, ScrollArea } from '@bitfun/ui';
+;
import { useAnnouncementStore } from '../store/announcementStore';
import FeatureModalPage from './FeatureModalPage';
import { useAnnouncementI18n } from '../hooks/useAnnouncementI18n';
@@ -41,7 +41,7 @@ const FeatureModalPages: React.FC = ({ pages, currentPag
const page = pages[pageIndex];
if (!page) return null;
return (
- = ({ pages, currentPag
{...(state !== 'active' ? { inert: '' } : {})}
>
-
+
);
};
@@ -136,7 +136,7 @@ const FeatureModal: React.FC = () => {
{modal.closable && (
}
+ icon={}
size="md"
data-bf-component="announcement"
data-bf-part="modalClose"
diff --git a/src/web-ui/src/shared/announcement-system/styles/FeatureModal.scss b/src/web-ui/src/shared/announcement-system/styles/FeatureModal.scss
index 466e585d42..6205c096f0 100644
--- a/src/web-ui/src/shared/announcement-system/styles/FeatureModal.scss
+++ b/src/web-ui/src/shared/announcement-system/styles/FeatureModal.scss
@@ -67,7 +67,6 @@
&__page {
position: absolute;
inset: 0;
- overflow-y: auto;
will-change: opacity, transform;
&[data-state='active'][data-direction='forward'] {
diff --git a/src/web-ui/src/shared/context-menu-system/components/ContextMenuRenderer.tsx b/src/web-ui/src/shared/context-menu-system/components/ContextMenuRenderer.tsx
index 5c78ae5072..02b46893f6 100644
--- a/src/web-ui/src/shared/context-menu-system/components/ContextMenuRenderer.tsx
+++ b/src/web-ui/src/shared/context-menu-system/components/ContextMenuRenderer.tsx
@@ -1,13 +1,11 @@
import React from 'react';
+import { Icon, type IconName } from '@bitfun/ui';
import {
Archive,
ArchiveRestore,
Clipboard,
Code,
- Copy,
- Download,
Edit,
- ExternalLink,
FileInput,
FileOutput,
FilePlus,
@@ -22,15 +20,23 @@ import {
MessageSquarePlus,
Navigation,
PanelRightOpen,
- Pin,
- Plus,
RefreshCw,
Scissors,
- Search,
Trash2,
- X,
type LucideIcon,
} from 'lucide-react';
+
+const CONTEXT_MENU_CATALOG: Record = {
+ Copy: 'duplicate',
+ Download: 'download',
+ Edit: 'edit',
+ ExternalLink: 'arrow-up-right',
+ MessageSquare: 'side-chat',
+ Pin: 'pin',
+ Plus: 'plus',
+ Search: 'search',
+ X: 'xmark',
+};
import { ContextMenu } from './ui/ContextMenu';
import { useContextMenuStore } from '../store/ContextMenuStore';
import { MenuItem as SystemMenuItem } from '../types/menu.types';
@@ -41,10 +47,7 @@ const CONTEXT_MENU_ICONS = {
ArchiveRestore,
Clipboard,
Code,
- Copy,
- Download,
Edit,
- ExternalLink,
FileInput,
FileOutput,
FilePlus,
@@ -59,13 +62,9 @@ const CONTEXT_MENU_ICONS = {
MessageSquarePlus,
Navigation,
PanelRightOpen,
- Pin,
- Plus,
RefreshCw,
Scissors,
- Search,
Trash2,
- X,
} satisfies Record;
function getIconComponent(icon: any): string | React.ReactNode | undefined {
@@ -76,6 +75,11 @@ function getIconComponent(icon: any): string | React.ReactNode | undefined {
}
if (typeof icon === 'string') {
+ const catalogName = CONTEXT_MENU_CATALOG[icon];
+ if (catalogName) {
+ return React.createElement(Icon, { name: catalogName, size: 'md' });
+ }
+
const IconComponent = CONTEXT_MENU_ICONS[icon as keyof typeof CONTEXT_MENU_ICONS];
if (IconComponent) {
return React.createElement(IconComponent, { size: 16 });
diff --git a/src/web-ui/src/shared/context-system/components/ContextCard/ContextCard.tsx b/src/web-ui/src/shared/context-system/components/ContextCard/ContextCard.tsx
index d5bd3e4011..0aebb6e005 100644
--- a/src/web-ui/src/shared/context-system/components/ContextCard/ContextCard.tsx
+++ b/src/web-ui/src/shared/context-system/components/ContextCard/ContextCard.tsx
@@ -1,7 +1,8 @@
import React, { useMemo } from 'react';
-import { X, AlertCircle, CheckCircle, Loader2 } from 'lucide-react';
+import { Icon } from '@bitfun/ui';
+import { AlertCircle, Loader2 } from 'lucide-react';
import { ContextItem } from '../../../types/context';
import { contextRegistry } from '../../../services/ContextRegistry';
import { useContextStore, selectValidationState, selectIsValidating } from '../../../stores/contextStore';
@@ -109,7 +110,7 @@ export const ContextCard: React.FC = ({
) : validationState ? (
validationState.valid ? (
-
+
) : (
= ({
onClick={handleRemove}
title={t('contextSystem.contextCard.removeContext')}
>
-
+
)}
diff --git a/src/web-ui/src/shared/context-system/components/ContextList/ContextList.scss b/src/web-ui/src/shared/context-system/components/ContextList/ContextList.scss
index d905d20ddc..163af8bc95 100644
--- a/src/web-ui/src/shared/context-system/components/ContextList/ContextList.scss
+++ b/src/web-ui/src/shared/context-system/components/ContextList/ContextList.scss
@@ -49,7 +49,6 @@
display: flex;
flex-direction: column;
gap: 0;
- overflow-y: auto;
overflow-x: hidden;
padding: 0 $size-gap-2;
diff --git a/src/web-ui/src/shared/context-system/components/ContextList/ContextList.tsx b/src/web-ui/src/shared/context-system/components/ContextList/ContextList.tsx
index 98da547e0c..c742b70721 100644
--- a/src/web-ui/src/shared/context-system/components/ContextList/ContextList.tsx
+++ b/src/web-ui/src/shared/context-system/components/ContextList/ContextList.tsx
@@ -1,8 +1,8 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
-import { Button } from '@bitfun/ui';
-import { AlertCircle, X } from 'lucide-react';
+import { Button, Icon, ScrollArea } from '@bitfun/ui';
+import { AlertCircle } from 'lucide-react';
import { useContextStore, selectContexts } from '../../../stores/contextStore';
import { ContextCard } from '../ContextCard/ContextCard';
import { useI18n } from '@/infrastructure/i18n';
@@ -126,7 +126,7 @@ export const ContextList: React.FC = ({
type="button"
variant="outline"
size="sm"
- leadingIcon={}
+ leadingIcon={}
onClick={handleClearAll}
disabled={leavingContextIds.size > 0}
title={t('contextSystem.contextList.clearAllTitle')}
@@ -136,7 +136,7 @@ export const ContextList: React.FC = ({
- = ({
))}
-
+
);
};
diff --git a/src/web-ui/src/shared/context-system/core/types/FileContextImpl.tsx b/src/web-ui/src/shared/context-system/core/types/FileContextImpl.tsx
index fa51e4d55d..481ce6bf6e 100644
--- a/src/web-ui/src/shared/context-system/core/types/FileContextImpl.tsx
+++ b/src/web-ui/src/shared/context-system/core/types/FileContextImpl.tsx
@@ -1,8 +1,9 @@
import React from 'react';
+import { Icon } from '@bitfun/ui';
import { api } from '@/infrastructure/api/service-api/ApiClient';
-import { FileIcon, CheckCircle } from 'lucide-react';
+import { FileIcon } from 'lucide-react';
import type { FileContext, ValidationResult, RenderOptions } from '../../../types/context';
import type {
ContextTransformer,
@@ -144,7 +145,7 @@ export class FileCardRenderer implements ContextCardRenderer<'file'> {
return (
-
+
);
}
diff --git a/src/web-ui/src/shared/context-system/core/types/ImageContextImpl.tsx b/src/web-ui/src/shared/context-system/core/types/ImageContextImpl.tsx
index f5ecb75125..78b14605fd 100644
--- a/src/web-ui/src/shared/context-system/core/types/ImageContextImpl.tsx
+++ b/src/web-ui/src/shared/context-system/core/types/ImageContextImpl.tsx
@@ -1,9 +1,9 @@
-import { Button, Modal } from '@bitfun/ui';
+import { Button, Icon, Modal } from '@bitfun/ui';
import React from 'react';
import { api } from '@/infrastructure/api/service-api/ApiClient';
-import { Image as ImageIcon, Eye } from 'lucide-react';
+;
import type { ImageContext, ValidationResult, RenderOptions } from '../../../types/context';
import type {
ContextTransformer,
@@ -153,7 +153,7 @@ export class ImageCardRenderer implements ContextCardRenderer<'image'> {
-
+
{context.imageName}
@@ -196,7 +196,7 @@ export class ImageCardRenderer implements ContextCardRenderer<'image'> {
variant="outline"
size="sm"
onClick={() => setShowFullImage(true)}
- leadingIcon={
}
+ leadingIcon={
}
>
{i18nService.t('components:contextSystem.contextCard.viewLargeImage')}
diff --git a/src/web-ui/src/shared/context-system/core/types/MermaidDiagramContextImpl.tsx b/src/web-ui/src/shared/context-system/core/types/MermaidDiagramContextImpl.tsx
index 780ec1d220..81998bb9a1 100644
--- a/src/web-ui/src/shared/context-system/core/types/MermaidDiagramContextImpl.tsx
+++ b/src/web-ui/src/shared/context-system/core/types/MermaidDiagramContextImpl.tsx
@@ -1,7 +1,8 @@
import React from 'react';
-import { Network, CheckCircle } from 'lucide-react';
+import { Icon } from '@bitfun/ui';
+import { Network } from 'lucide-react';
import type { MermaidDiagramContext, ValidationResult, RenderOptions } from '../../../types/context';
import type {
ContextTransformer,
@@ -159,7 +160,7 @@ export class MermaidDiagramCardRenderer implements ContextCardRenderer<'mermaid-
return (
-
+
);
}
diff --git a/src/web-ui/src/shared/notification-system/components/LoadingNotification.tsx b/src/web-ui/src/shared/notification-system/components/LoadingNotification.tsx
index 365bbd69a3..66b6e259c1 100644
--- a/src/web-ui/src/shared/notification-system/components/LoadingNotification.tsx
+++ b/src/web-ui/src/shared/notification-system/components/LoadingNotification.tsx
@@ -1,8 +1,8 @@
import React from 'react';
-import { IconButton } from '@bitfun/ui';
-import { X, Loader2 } from 'lucide-react';
+import { Icon, IconButton } from '@bitfun/ui';
+import { Loader2 } from 'lucide-react';
import { useI18n } from '@/infrastructure/i18n';
import { Notification } from '../types';
import { notificationService } from '../services/NotificationService';
@@ -61,7 +61,7 @@ export const LoadingNotification: React.FC
= ({ notifi
className="loading-notification__cancel"
onClick={handleCancel}
aria-label={t('actions.cancel')}
- icon={}
+ icon={}
/>
)}
diff --git a/src/web-ui/src/shared/notification-system/components/NotificationCenter.scss b/src/web-ui/src/shared/notification-system/components/NotificationCenter.scss
index a81016a16a..8ca16c490e 100644
--- a/src/web-ui/src/shared/notification-system/components/NotificationCenter.scss
+++ b/src/web-ui/src/shared/notification-system/components/NotificationCenter.scss
@@ -129,7 +129,6 @@
&__content {
flex: 1;
- overflow-y: auto;
min-height: 0;
}
@@ -356,7 +355,6 @@
&__item-technical-body {
margin: 0;
max-height: 160px;
- overflow: auto;
white-space: pre-wrap;
word-break: break-word;
font-family: $font-family-mono;
diff --git a/src/web-ui/src/shared/notification-system/components/NotificationCenter.tsx b/src/web-ui/src/shared/notification-system/components/NotificationCenter.tsx
index 45b641b613..62251370e7 100644
--- a/src/web-ui/src/shared/notification-system/components/NotificationCenter.tsx
+++ b/src/web-ui/src/shared/notification-system/components/NotificationCenter.tsx
@@ -1,8 +1,8 @@
import React, { useState, useMemo } from 'react';
-import { CheckCheck, Trash2, X, XCircle, ChevronDown, ChevronUp, Loader2, Search as SearchIcon } from 'lucide-react';
-import { IconButton, Modal, SearchField } from '@bitfun/ui';
+import { CheckCheck, Trash2, ChevronUp, Loader2 } from 'lucide-react';
+import { Icon, IconButton, Modal, ScrollArea, SearchField } from '@bitfun/ui';
import { useI18n } from '@/infrastructure/i18n';
import { useNotificationHistory, useCenterOpen, useAllProgressNotifications, useAllLoadingNotifications } from '../hooks/useNotificationState';
import { notificationService } from '../services/NotificationService';
@@ -325,9 +325,9 @@ export const NotificationCenter: React.FC = () => {
{t('errors:boundary.technicalDetails')}
-
- {technicalDetails}
-
+
+ {technicalDetails}
+
)}
@@ -341,14 +341,14 @@ export const NotificationCenter: React.FC = () => {
}}
title={isExpanded ? t('common:actions.collapse') : t('common:actions.expand')}
>
- {isExpanded ? : }
+ {isExpanded ? : }
handleDeleteNotification(e, notification.id)}
title={t('common:actions.delete')}
>
-
+
@@ -387,7 +387,7 @@ export const NotificationCenter: React.FC = () => {
}
+ icon={}
onClick={handleClose}
size="md"
title={t('common:actions.close')}
@@ -404,7 +404,7 @@ export const NotificationCenter: React.FC = () => {
}
+ leadingIcon={}
value={searchQuery}
onValueChange={(val) => setSearchQuery(val)}
clearLabel={t('components:search.clear')}
@@ -442,7 +442,7 @@ export const NotificationCenter: React.FC = () => {
-
+
{activeTaskNotifications.length > 0 && (
@@ -489,7 +489,7 @@ export const NotificationCenter: React.FC = () => {
)}
>
)}
-
+
);
diff --git a/src/web-ui/src/shared/notification-system/components/NotificationContainer.scss b/src/web-ui/src/shared/notification-system/components/NotificationContainer.scss
index 0ad2c5f87f..6691692ad5 100644
--- a/src/web-ui/src/shared/notification-system/components/NotificationContainer.scss
+++ b/src/web-ui/src/shared/notification-system/components/NotificationContainer.scss
@@ -34,14 +34,11 @@
}
- max-height: calc(100vh - 120px);
- overflow-y: auto;
-
-
- scrollbar-width: none;
- &::-webkit-scrollbar {
- display: none;
- }
+ max-height: calc(100vh - 120px);
+}
+
+.notification-container__viewport {
+ max-height: inherit;
}
diff --git a/src/web-ui/src/shared/notification-system/components/NotificationContainer.tsx b/src/web-ui/src/shared/notification-system/components/NotificationContainer.tsx
index 96127e8ba6..a11d9ea1d5 100644
--- a/src/web-ui/src/shared/notification-system/components/NotificationContainer.tsx
+++ b/src/web-ui/src/shared/notification-system/components/NotificationContainer.tsx
@@ -1,5 +1,6 @@
+import { ScrollArea } from '@bitfun/ui';
import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import type { Notification } from '../types';
import { useActiveNotifications } from '../hooks/useNotificationState';
@@ -118,7 +119,15 @@ export const NotificationContainer: React.FC = () => {
}
return (
-
+
+
{presentedNotifications.map((notification) => {
const isExiting = !visibleIds.has(notification.id);
@@ -149,6 +158,7 @@ export const NotificationContainer: React.FC = () => {
/>
);
})}
+
);
};
diff --git a/src/web-ui/src/shared/notification-system/components/NotificationItem.tsx b/src/web-ui/src/shared/notification-system/components/NotificationItem.tsx
index f6350d04f9..8dbb944900 100644
--- a/src/web-ui/src/shared/notification-system/components/NotificationItem.tsx
+++ b/src/web-ui/src/shared/notification-system/components/NotificationItem.tsx
@@ -1,8 +1,8 @@
import React from 'react';
-import { Button, IconButton } from '@bitfun/ui';
-import { X, CheckCircle, XCircle, AlertTriangle, Info } from 'lucide-react';
+import { Button, Icon, IconButton } from '@bitfun/ui';
+import { AlertTriangle } from 'lucide-react';
import { useI18n } from '@/infrastructure/i18n';
import { Notification } from '../types';
import { notificationService } from '../services/NotificationService';
@@ -22,14 +22,14 @@ export const NotificationItem: React.FC
= ({ notification
const getIcon = () => {
switch (type) {
case 'success':
- return ;
+ return ;
case 'error':
- return ;
+ return ;
case 'warning':
return ;
case 'info':
default:
- return ;
+ return ;
}
};
@@ -94,7 +94,7 @@ export const NotificationItem: React.FC = ({ notification
size="sm"
onClick={handleClose}
aria-label={t('actions.close')}
- icon={}
+ icon={}
/>
)}
diff --git a/src/web-ui/src/shared/notification-system/components/ProgressNotification.tsx b/src/web-ui/src/shared/notification-system/components/ProgressNotification.tsx
index 96172f0fc0..0924747ff3 100644
--- a/src/web-ui/src/shared/notification-system/components/ProgressNotification.tsx
+++ b/src/web-ui/src/shared/notification-system/components/ProgressNotification.tsx
@@ -1,8 +1,8 @@
import React from 'react';
-import { IconButton } from '@bitfun/ui';
-import { X, Loader2 } from 'lucide-react';
+import { Icon, IconButton } from '@bitfun/ui';
+import { Loader2 } from 'lucide-react';
import { useI18n } from '@/infrastructure/i18n';
import { Notification } from '../types';
import { notificationService } from '../services/NotificationService';
@@ -102,7 +102,7 @@ export const ProgressNotification: React.FC = ({ noti
className="progress-notification__cancel"
onClick={handleCancel}
aria-label={t('actions.cancel')}
- icon={}
+ icon={}
/>
)}
diff --git a/src/web-ui/src/tools/bitfun-canvas/BitfunCanvasPanel.tsx b/src/web-ui/src/tools/bitfun-canvas/BitfunCanvasPanel.tsx
index 7b21ce285d..312beb9b4d 100644
--- a/src/web-ui/src/tools/bitfun-canvas/BitfunCanvasPanel.tsx
+++ b/src/web-ui/src/tools/bitfun-canvas/BitfunCanvasPanel.tsx
@@ -1,5 +1,6 @@
import React, { Suspense, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
-import { AlertTriangle, Code2, Download, Loader2, MousePointer2 } from 'lucide-react';
+import { Icon } from '@bitfun/ui';
+import { AlertTriangle, Code2, Loader2, MousePointer2 } from 'lucide-react';
import path from 'path-browserify';
import { flowChatStore } from '@/flow_chat/store/FlowChatStore';
import { canvasAPI, type CanvasSnapshotValue } from '@/infrastructure/api/service-api/CanvasAPI';
@@ -1017,7 +1018,7 @@ export const BitfunCanvasPanel: React.FC = ({
disabled={exportingHtml}
onClick={handleExportHtml}
>
- {exportingHtml ? : }
+ {exportingHtml ? : }
{isFrameReady && (
diff --git a/src/web-ui/src/tools/editor/components/EditorBreadcrumb.scss b/src/web-ui/src/tools/editor/components/EditorBreadcrumb.scss
index eb2ff4d587..ff682f969f 100644
--- a/src/web-ui/src/tools/editor/components/EditorBreadcrumb.scss
+++ b/src/web-ui/src/tools/editor/components/EditorBreadcrumb.scss
@@ -163,7 +163,6 @@ $_dropdown-width: 220px;
margin: 0;
padding: 4px 0;
max-height: $_dropdown-max-height - 40px;
- overflow-y: auto;
}
diff --git a/src/web-ui/src/tools/editor/components/EditorBreadcrumb.tsx b/src/web-ui/src/tools/editor/components/EditorBreadcrumb.tsx
index 07bef2ff6a..4ce829d2c0 100644
--- a/src/web-ui/src/tools/editor/components/EditorBreadcrumb.tsx
+++ b/src/web-ui/src/tools/editor/components/EditorBreadcrumb.tsx
@@ -3,13 +3,13 @@
import React, { useMemo, useCallback, useState, useRef, useEffect } from 'react';
import { createPortal } from 'react-dom';
import { getAppearanceOverlayHost } from '@/infrastructure/appearance/runtime/AppearanceOverlayHost';
-import { ChevronRight, File, Folder, Code, Loader2, ArrowLeft } from 'lucide-react';
+import { Code, Loader2 } from 'lucide-react';
import { getFileIconType } from '@/tools/file-system/utils/fileIcons';
import { workspaceAPI } from '@/infrastructure/api';
import { createLogger } from '@/shared/utils/logger';
import './EditorBreadcrumb.scss';
-import { Tooltip } from '@bitfun/ui';
+import { Icon, ScrollArea, Tooltip } from '@bitfun/ui';
const log = createLogger('EditorBreadcrumb');
@@ -54,7 +54,7 @@ const getFileIconComponent = (fileName: string, size: number = 12): React.ReactE
case 'code':
return ;
default:
- return ;
+ return ;
}
};
@@ -182,7 +182,7 @@ const DropdownMenu: React.FC = ({
onGoBack();
}}
>
-
+
@@ -203,7 +203,8 @@ const DropdownMenu: React.FC = ({
Empty directory
) : (
-
+
+
{sortedItems.map((item) => {
const isCurrentFile = item.path.replace(/\\/g, '/') === currentFilePath.replace(/\\/g, '/');
return (
@@ -220,7 +221,7 @@ const DropdownMenu: React.FC = ({
>
{item.isDirectory ? (
-
+
) : (
getFileIconComponent(item.name, 14)
)}
@@ -229,12 +230,13 @@ const DropdownMenu: React.FC = ({
{item.name}
{item.isDirectory && (
-
+
)}
);
})}
-
+
+
)}
);
@@ -414,12 +416,7 @@ export const EditorBreadcrumb: React.FC = ({
return (
{index > 0 && (
-
+
)}
{isEllipsis ? (
@@ -444,7 +441,7 @@ export const EditorBreadcrumb: React.FC = ({
{pathSegment.isFile ? (
getFileIconComponent(pathSegment.name)
) : (
-
+
)}
diff --git a/src/web-ui/src/tools/editor/components/ImageViewer.tsx b/src/web-ui/src/tools/editor/components/ImageViewer.tsx
index ed59a1aa2c..9f17796652 100644
--- a/src/web-ui/src/tools/editor/components/ImageViewer.tsx
+++ b/src/web-ui/src/tools/editor/components/ImageViewer.tsx
@@ -6,8 +6,8 @@
*/
import React, { useState, useEffect, useCallback } from 'react';
-import { ZoomIn, ZoomOut, RotateCw, Download, Maximize2 } from 'lucide-react';
-import { Button, IconButton, Toolbar, ToolbarGroup, ToolbarSeparator, Tooltip } from '@bitfun/ui';
+import { ZoomIn, ZoomOut, RotateCw, Maximize2 } from 'lucide-react';
+import { Button, Icon, IconButton, Toolbar, ToolbarGroup, ToolbarSeparator, Tooltip } from '@bitfun/ui';
import { createLogger } from '@/shared/utils/logger';
import { useI18n } from '@/infrastructure/i18n';
@@ -221,7 +221,7 @@ export const ImageViewer: React.FC = ({
aria-label={t('editor.imageViewer.download')}
size="sm"
variant="quiet"
- icon={}
+ icon={}
onClick={handleDownload}
/>
diff --git a/src/web-ui/src/tools/editor/components/MarkdownEditor.test.tsx b/src/web-ui/src/tools/editor/components/MarkdownEditor.test.tsx
index 9500f305e1..b1042fd2f5 100644
--- a/src/web-ui/src/tools/editor/components/MarkdownEditor.test.tsx
+++ b/src/web-ui/src/tools/editor/components/MarkdownEditor.test.tsx
@@ -26,6 +26,7 @@ vi.mock('@/component-library', () => ({
}));
vi.mock('@bitfun/ui', () => ({
+ Icon: ({ name, ...props }: { name: string } & React.HTMLAttributes) => ,
Button: ({ children, ...props }: React.ButtonHTMLAttributes) => (
{children}
),
@@ -135,7 +136,7 @@ describe('MarkdownEditor', () => {
);
expect(html).toContain('aria-label="Copy Markdown"');
- expect(html).toContain('data-icon="copy"');
+ expect(html).toContain('data-icon="duplicate"');
expect(html).toContain('data-component="icon-button"');
});
diff --git a/src/web-ui/src/tools/editor/components/MarkdownEditor.tsx b/src/web-ui/src/tools/editor/components/MarkdownEditor.tsx
index d619510c92..0492130a90 100644
--- a/src/web-ui/src/tools/editor/components/MarkdownEditor.tsx
+++ b/src/web-ui/src/tools/editor/components/MarkdownEditor.tsx
@@ -5,12 +5,12 @@
* @module components/MarkdownEditor
*/
-import { Button, IconButton, SegmentedControl } from '@bitfun/ui';
+import { Button, Icon, IconButton, SegmentedControl } from '@bitfun/ui';
import React, { useEffect, useState, useCallback, useRef } from 'react';
import { MEditor } from '../meditor';
import type { EditorInstance } from '../meditor';
import { analyzeMarkdownEditability, type MarkdownEditabilityAnalysis } from '../meditor/utils/tiptapMarkdown';
-import { AlertCircle, Check, Copy } from 'lucide-react';
+import { AlertCircle } from 'lucide-react';
import { createLogger } from '@/shared/utils/logger';
import { sendDebugProbe } from '@/shared/utils/debugProbe';
import { elapsedMs, nowMs } from '@/shared/utils/timing';
@@ -670,7 +670,7 @@ const MarkdownEditor: React.FC = ({
aria-label={copied
? t('editor.markdownEditor.copiedMarkdown')
: t('editor.markdownEditor.copyMarkdown')}
- icon={copied ? : }
+ icon={copied ? : }
title={copied
? t('editor.markdownEditor.copiedMarkdown')
: t('editor.markdownEditor.copyMarkdown')}
@@ -751,7 +751,7 @@ const MarkdownEditor: React.FC = ({
aria-label={copied
? t('editor.markdownEditor.copiedMarkdown')
: t('editor.markdownEditor.copyMarkdown')}
- icon={copied ? : }
+ icon={copied ? : }
title={copied
? t('editor.markdownEditor.copiedMarkdown')
: t('editor.markdownEditor.copyMarkdown')}
diff --git a/src/web-ui/src/tools/editor/components/PlanViewer.tsx b/src/web-ui/src/tools/editor/components/PlanViewer.tsx
index 688e6c40a4..9b3eb88e1c 100644
--- a/src/web-ui/src/tools/editor/components/PlanViewer.tsx
+++ b/src/web-ui/src/tools/editor/components/PlanViewer.tsx
@@ -1,8 +1,8 @@
/** Optimized viewer/editor for `.plan.md` files (frontmatter + markdown body). */
-import { Button, IconButton, Tooltip } from '@bitfun/ui';
+import { Button, Icon, IconButton, Tooltip } from '@bitfun/ui';
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
-import { Circle, ArrowRight, Check, XCircle, Loader2, CheckCircle, AlertCircle, FileText, Pencil, X, ChevronDown, Trash2, Plus } from 'lucide-react';
+import { Loader2, AlertCircle, FileText, Trash2 } from 'lucide-react';
import yaml from 'yaml';
import { MEditor } from '../meditor';
import type { EditorInstance } from '../meditor';
@@ -567,7 +567,7 @@ const PlanViewer: React.FC = ({
size="sm"
onClick={closeYamlEditor}
aria-label={t('editor.planViewer.toggleYamlEditOff')}
- icon={}
+ icon={}
/>
) : isPanelEditing ? (
@@ -578,7 +578,7 @@ const PlanViewer: React.FC = ({
size="sm"
onClick={addTodo}
aria-label={t('editor.common.add')}
- icon={}
+ icon={}
/>
@@ -588,7 +588,7 @@ const PlanViewer: React.FC = ({
variant="primary"
onClick={saveEdit}
aria-label={t('editor.common.save')}
- icon={}
+ icon={}
/>
@@ -597,7 +597,7 @@ const PlanViewer: React.FC = ({
size="sm"
onClick={cancelEdit}
aria-label={t('editor.common.cancel')}
- icon={}
+ icon={}
/>
>
@@ -618,7 +618,7 @@ const PlanViewer: React.FC = ({
size="sm"
onClick={startEdit}
aria-label={t('editor.common.edit')}
- icon={}
+ icon={}
/>
>
@@ -759,14 +759,14 @@ ${JSON.stringify(simpleTodos, null, 2)}
function getTodoIcon(status?: string) {
switch (status) {
case 'completed':
- return ;
+ return ;
case 'in_progress':
- return ;
+ return ;
case 'cancelled':
- return ;
+ return ;
case 'pending':
default:
- return ;
+ return ;
}
}
@@ -816,7 +816,7 @@ ${JSON.stringify(simpleTodos, null, 2)}
-
+
)}
@@ -836,7 +836,7 @@ ${JSON.stringify(simpleTodos, null, 2)}
buildStatus === 'building'
?
: buildStatus === 'built'
- ?
+ ?
: undefined
}
onClick={handleBuild}
diff --git a/src/web-ui/src/tools/editor/components/StatusBarPopovers/StatusBarPopovers.scss b/src/web-ui/src/tools/editor/components/StatusBarPopovers/StatusBarPopovers.scss
index 84358f6517..43408d1723 100644
--- a/src/web-ui/src/tools/editor/components/StatusBarPopovers/StatusBarPopovers.scss
+++ b/src/web-ui/src/tools/editor/components/StatusBarPopovers/StatusBarPopovers.scss
@@ -39,7 +39,6 @@
&__list {
max-height: 280px;
- overflow-y: auto;
padding: 4px 0;
}
diff --git a/src/web-ui/src/tools/editor/components/StatusBarPopovers/StatusBarPopovers.tsx b/src/web-ui/src/tools/editor/components/StatusBarPopovers/StatusBarPopovers.tsx
index 42bfe6d024..e8ce730a34 100644
--- a/src/web-ui/src/tools/editor/components/StatusBarPopovers/StatusBarPopovers.tsx
+++ b/src/web-ui/src/tools/editor/components/StatusBarPopovers/StatusBarPopovers.tsx
@@ -18,7 +18,7 @@ import {
import { useI18n } from '@/infrastructure/i18n';
import './StatusBarPopovers.scss';
-import { Input } from '@bitfun/ui';
+import { Input, ScrollArea } from '@bitfun/ui';
export type StatusBarPopoverType = 'position' | 'indent' | 'encoding' | 'language';
@@ -166,7 +166,7 @@ export const IndentPopover: React.FC = ({
aria-label={t('editor.statusBar.indentSettings')}
>
{t('editor.statusBar.selectIndent')}
-
+
{INDENT_OPTIONS.map((opt) => {
const label = opt.insertSpaces
? t('editor.statusBar.indentOptionSpaces', { n: opt.tabSize })
@@ -203,7 +203,7 @@ export const IndentPopover: React.FC = ({
);
})}
-
+
,
getAppearanceOverlayHost()
);
@@ -239,7 +239,7 @@ export const EncodingPopover: React.FC = ({
aria-label={t('editor.statusBar.fileEncoding')}
>
{t('editor.statusBar.selectEncoding')}
-
+
{ENCODING_OPTIONS.map((enc) => (
= ({
{enc}
))}
-
+
,
getAppearanceOverlayHost()
);
@@ -354,7 +354,7 @@ export const LanguagePopover: React.FC = ({
aria-label={t('editor.statusBar.selectLanguageMode')}
>
{t('editor.statusBar.selectLanguageModeHint')}
-
+
{languages.map((lang) => {
const Icon = getLanguageIcon(lang.id);
return (
@@ -385,7 +385,7 @@ export const LanguagePopover: React.FC = ({
);
})}
-
+
,
getAppearanceOverlayHost()
);
diff --git a/src/web-ui/src/tools/editor/meditor/components/InlineAiPreviewBlock.tsx b/src/web-ui/src/tools/editor/meditor/components/InlineAiPreviewBlock.tsx
index 9a7fc15073..665bf870ec 100644
--- a/src/web-ui/src/tools/editor/meditor/components/InlineAiPreviewBlock.tsx
+++ b/src/web-ui/src/tools/editor/meditor/components/InlineAiPreviewBlock.tsx
@@ -1,6 +1,6 @@
-import { Button } from '@bitfun/ui';
+import { Button, Icon } from '@bitfun/ui';
import React from 'react';
-import { Check, RotateCcw, X } from 'lucide-react';
+import { RotateCcw } from 'lucide-react';
import { InlineMarkdownPreview } from './InlineMarkdownPreview';
type InlineAiPreviewStatus = 'submitting' | 'streaming' | 'ready' | 'error';
@@ -86,7 +86,7 @@ export const InlineAiPreviewBlock: React.FC = ({
disabled={!canAccept}
data-testid="md-inline-ai-accept"
onClick={onAccept}
- leadingIcon={}
+ leadingIcon={}
>
{labels.accept}
@@ -98,7 +98,7 @@ export const InlineAiPreviewBlock: React.FC = ({
size="sm"
data-testid="md-inline-ai-reject"
onClick={onReject}
- leadingIcon={}
+ leadingIcon={}
>
{labels.reject}
diff --git a/src/web-ui/src/tools/editor/meditor/components/TiptapEditor.tsx b/src/web-ui/src/tools/editor/meditor/components/TiptapEditor.tsx
index 2b80fe631b..c86159d4f7 100644
--- a/src/web-ui/src/tools/editor/meditor/components/TiptapEditor.tsx
+++ b/src/web-ui/src/tools/editor/meditor/components/TiptapEditor.tsx
@@ -6,7 +6,7 @@ import React, {
useRef,
useState,
} from 'react';
-import { Button, IconButton, Input, Tooltip } from '@bitfun/ui';
+import { Button, Icon, IconButton, Input, Tooltip } from '@bitfun/ui';
import { EditorContent, useEditor } from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit';
import { Details, DetailsContent, DetailsSummary } from '@tiptap/extension-details';
@@ -14,7 +14,7 @@ import Placeholder from '@tiptap/extension-placeholder';
import TaskItem from '@tiptap/extension-task-item';
import TaskList from '@tiptap/extension-task-list';
import Link from '@tiptap/extension-link';
-import { ArrowUp, FileText, ListTodo, PenLine } from 'lucide-react';
+import { FileText, ListTodo } from 'lucide-react';
import type { Editor as TiptapEditorInstance, JSONContent } from '@tiptap/core';
import type { Node as ProseMirrorNode } from '@tiptap/pm/model';
import { Selection } from '@tiptap/pm/state';
@@ -1241,7 +1241,7 @@ export const TiptapEditor = React.forwardRef}
+ leading={}
value={inlineAiState.query}
onChange={(event) => {
const nextValue = event.target.value;
@@ -1298,7 +1298,7 @@ export const TiptapEditor = React.forwardRef}
+ icon={}
/>
@@ -1321,7 +1321,7 @@ export const TiptapEditor = React.forwardRef}
+ leadingIcon={}
data-testid="md-inline-ai-continue"
onClick={() => {
handleInlineAiQuickAction('continue', '');
diff --git a/src/web-ui/src/tools/file-system/components/FileExplorer.tsx b/src/web-ui/src/tools/file-system/components/FileExplorer.tsx
index 8faa4cbd0b..fe8de846fa 100644
--- a/src/web-ui/src/tools/file-system/components/FileExplorer.tsx
+++ b/src/web-ui/src/tools/file-system/components/FileExplorer.tsx
@@ -1,7 +1,7 @@
import React, { useState, useCallback, useMemo, useRef, useEffect } from 'react';
-import { IconButton, Tooltip } from '@bitfun/ui';
+import { Icon, IconButton, Tooltip } from '@bitfun/ui';
import { useShortcut } from '@/infrastructure/hooks/useShortcut';
-import { Folder, FilePlus, FolderPlus, RefreshCw } from 'lucide-react';
+import { FilePlus, FolderPlus, RefreshCw } from 'lucide-react';
import { VirtualFileTree } from './VirtualFileTree';
import { FileExplorerProps, FileSystemNode, FlatFileNode } from '../types';
import { flattenFileTree } from '../utils/treeFlattening';
@@ -287,7 +287,7 @@ export const FileExplorer: React.FC = ({
tabIndex={0}
>
-
+
{searchQuery ? t('fileTree.emptyFiltered') : t('fileTree.empty')}
diff --git a/src/web-ui/src/tools/file-system/components/FileSearchResults.scss b/src/web-ui/src/tools/file-system/components/FileSearchResults.scss
index 4794a0270b..6238a21829 100644
--- a/src/web-ui/src/tools/file-system/components/FileSearchResults.scss
+++ b/src/web-ui/src/tools/file-system/components/FileSearchResults.scss
@@ -53,7 +53,6 @@ $_icon-size: 16px;
.bitfun-search-results__list {
flex: 1;
min-height: 0;
- overflow-y: auto;
overflow-x: hidden;
padding: $size-gap-2 0;
-webkit-overflow-scrolling: touch;
diff --git a/src/web-ui/src/tools/file-system/components/FileSearchResults.tsx b/src/web-ui/src/tools/file-system/components/FileSearchResults.tsx
index bca71cf233..efc4687ba7 100644
--- a/src/web-ui/src/tools/file-system/components/FileSearchResults.tsx
+++ b/src/web-ui/src/tools/file-system/components/FileSearchResults.tsx
@@ -1,5 +1,6 @@
import React, { useMemo, useState, useCallback, startTransition, memo, useEffect, useRef } from 'react';
-import { File, FileText, Folder, ChevronRight, ChevronDown } from 'lucide-react';
+import { Icon, ScrollArea } from '@bitfun/ui';
+import { FileText } from 'lucide-react';
import type {
FileSearchResult,
FileSearchResultGroup,
@@ -243,9 +244,9 @@ const FileGroup = memo(({
}`}
>
{group.isDirectory ? (
-
+
) : (
-
+
)}
@@ -269,9 +270,9 @@ const FileGroup = memo(({
title={isExpanded ? t('search.collapse') : t('search.expand')}
>
{isExpanded ? (
-
+
) : (
-
+
)}
{group.contentMatches.length}
@@ -516,7 +517,7 @@ export const FileSearchResults: React.FC = ({
- = ({
onLineClick={handleLineClick}
/>
))}
-
+
);
};
diff --git a/src/web-ui/src/tools/file-system/components/FileTreeItem.tsx b/src/web-ui/src/tools/file-system/components/FileTreeItem.tsx
index 3fca05d7d1..b1a4deef75 100644
--- a/src/web-ui/src/tools/file-system/components/FileTreeItem.tsx
+++ b/src/web-ui/src/tools/file-system/components/FileTreeItem.tsx
@@ -1,6 +1,6 @@
import React, { useEffect, useState } from 'react';
-import { ChevronRight, FolderOpen, FileText, Loader2 } from 'lucide-react';
-import { Input } from '@bitfun/ui';
+import { FolderOpen, FileText, Loader2 } from 'lucide-react';
+import { Icon, Input } from '@bitfun/ui';
import { dragManager } from '../../../shared/services/DragManager';
import { fileTreeDragSource } from '../../../shared/context-system/drag-drop/FileTreeDragSource';
import { useI18n } from '@/infrastructure/i18n';
@@ -202,7 +202,7 @@ export const FileTreeItem: React.FC = ({
{isLoading ? (
) : (
-
+
)}
) : (
diff --git a/src/web-ui/src/tools/file-system/utils/fileIcons.ts b/src/web-ui/src/tools/file-system/utils/fileIcons.ts
index 6fd21f2691..8c59a3e9d3 100644
--- a/src/web-ui/src/tools/file-system/utils/fileIcons.ts
+++ b/src/web-ui/src/tools/file-system/utils/fileIcons.ts
@@ -1,11 +1,9 @@
import React from 'react';
-import {
- Folder,
- FolderOpen,
- File,
- FileText,
- Image,
- Code
+import { Icon } from '@bitfun/ui';
+import {
+ FolderOpen,
+ FileText,
+ Code,
} from 'lucide-react';
import { FileSystemNode, FileIconType } from '../types';
import { getFileIconType as getIconTypeFromDetector } from '@/infrastructure/language-detection';
@@ -60,12 +58,12 @@ export function getFileIcon(node: FileSystemNode, isExpanded?: boolean): React.R
const iconType = getFileIconType(node);
if (node.isDirectory) {
- return isExpanded ? React.createElement(FolderOpen, { size: 16 }) : React.createElement(Folder, { size: 16 });
+ return isExpanded ? React.createElement(FolderOpen, { size: 16 }) : React.createElement(Icon, { name: 'folder', size: 'md' });
}
switch (iconType) {
case 'image':
- return React.createElement(Image, { size: 16 });
+ return React.createElement(Icon, { name: 'image', size: 'md' });
case 'code':
case 'javascript':
case 'typescript':
@@ -84,7 +82,7 @@ export function getFileIcon(node: FileSystemNode, isExpanded?: boolean): React.R
case 'text':
return React.createElement(FileText, { size: 16 });
default:
- return React.createElement(File, { size: 16 });
+ return React.createElement(Icon, { name: 'files', size: 'md' });
}
}
diff --git a/src/web-ui/src/tools/git/components/CreateBranchDialog/CreateBranchDialog.scss b/src/web-ui/src/tools/git/components/CreateBranchDialog/CreateBranchDialog.scss
index 9585c2d713..f028bf0283 100644
--- a/src/web-ui/src/tools/git/components/CreateBranchDialog/CreateBranchDialog.scss
+++ b/src/web-ui/src/tools/git/components/CreateBranchDialog/CreateBranchDialog.scss
@@ -5,7 +5,10 @@
min-width: 520px;
max-width: 600px;
max-height: 90vh;
- overflow-y: auto;
+
+ &__scroll {
+ max-height: inherit;
+ }
&__header {
display: flex;
diff --git a/src/web-ui/src/tools/git/components/CreateBranchDialog/CreateBranchDialog.tsx b/src/web-ui/src/tools/git/components/CreateBranchDialog/CreateBranchDialog.tsx
index eaada1c2ec..361cc50df2 100644
--- a/src/web-ui/src/tools/git/components/CreateBranchDialog/CreateBranchDialog.tsx
+++ b/src/web-ui/src/tools/git/components/CreateBranchDialog/CreateBranchDialog.tsx
@@ -3,10 +3,10 @@
* Provides a consistent UI for creating a new branch from an existing base branch.
*/
-import { Button, Field, Input, Modal } from '@bitfun/ui';
+import { Button, Field, FieldGroup, FieldRow, Icon, Input, Modal, ScrollArea } from '@bitfun/ui';
import React, { useState, useCallback, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
-import { GitBranch } from 'lucide-react';
+;
import './CreateBranchDialog.scss';
export interface CreateBranchDialogProps {
@@ -121,9 +121,10 @@ export const CreateBranchDialog: React.FC = ({
return (
+
-
+
{t('dialog.createNewBranch.baseBranch')}
@@ -131,38 +132,42 @@ export const CreateBranchDialog: React.FC = ({
-
-
-
-
-
-
{t('dialog.createNewBranch.namingHintTitle')}
-
- -
- {t('dialog.createNewBranch.namingHints.featureLabel')}
{t('dialog.createNewBranch.namingHints.featureExample')}
-
- -
- {t('dialog.createNewBranch.namingHints.bugfixLabel')}
{t('dialog.createNewBranch.namingHints.bugfixExample')}
-
- -
- {t('dialog.createNewBranch.namingHints.hotfixLabel')}
{t('dialog.createNewBranch.namingHints.hotfixExample')}
-
- -
- {t('dialog.createNewBranch.namingHints.releaseLabel')}
{t('dialog.createNewBranch.namingHints.releaseExample')}
-
-
-
-
+
+
+
+
+
+
+
+
+
{t('dialog.createNewBranch.namingHintTitle')}
+
+ -
+ {t('dialog.createNewBranch.namingHints.featureLabel')}
{t('dialog.createNewBranch.namingHints.featureExample')}
+
+ -
+ {t('dialog.createNewBranch.namingHints.bugfixLabel')}
{t('dialog.createNewBranch.namingHints.bugfixExample')}
+
+ -
+ {t('dialog.createNewBranch.namingHints.hotfixLabel')}
{t('dialog.createNewBranch.namingHints.hotfixExample')}
+
+ -
+ {t('dialog.createNewBranch.namingHints.releaseLabel')}
{t('dialog.createNewBranch.namingHints.releaseExample')}
+
+
+
+
+
= ({
onClick={handleConfirm}
disabled={!canSubmit}
loading={isCreating}
- leadingIcon={}
+ leadingIcon={}
>
{t('dialog.createNewBranch.confirm')}
@@ -189,6 +194,7 @@ export const CreateBranchDialog: React.FC = ({
Esc {t('dialog.createNewBranch.cancel')} · Enter {t('dialog.createNewBranch.confirm')}
+
);
diff --git a/src/web-ui/src/tools/git/components/GitBranchHistoryView/GitBranchHistoryView.scss b/src/web-ui/src/tools/git/components/GitBranchHistoryView/GitBranchHistoryView.scss
index 3c37c0b155..5f9d09746a 100644
--- a/src/web-ui/src/tools/git/components/GitBranchHistoryView/GitBranchHistoryView.scss
+++ b/src/web-ui/src/tools/git/components/GitBranchHistoryView/GitBranchHistoryView.scss
@@ -96,7 +96,6 @@
&__content {
flex: 1;
- overflow-y: auto;
overflow-x: hidden;
min-height: 0;
diff --git a/src/web-ui/src/tools/git/components/GitBranchHistoryView/GitBranchHistoryView.tsx b/src/web-ui/src/tools/git/components/GitBranchHistoryView/GitBranchHistoryView.tsx
index c070eb6764..deaef0cb83 100644
--- a/src/web-ui/src/tools/git/components/GitBranchHistoryView/GitBranchHistoryView.tsx
+++ b/src/web-ui/src/tools/git/components/GitBranchHistoryView/GitBranchHistoryView.tsx
@@ -3,10 +3,10 @@
* Shows a branch's commits and supports cherry-pick when applicable.
*/
-import { Button, IconButton, SearchField, Select } from '@bitfun/ui';
+import { Button, Icon, IconButton, SearchField, Select, ScrollArea } from '@bitfun/ui';
import React, { useState, useEffect, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
-import { Copy, RefreshCw, ChevronDown, ChevronUp, GitBranch, Square, CheckSquare, Search as SearchIcon } from 'lucide-react';
+import { RefreshCw, ChevronUp, Square, CheckSquare } from 'lucide-react';
import { useI18n } from '@/infrastructure/i18n/hooks/useI18n';
import { gitAPI } from '@/infrastructure/api';
import { useNotification } from '@/shared/notification-system';
@@ -346,7 +346,7 @@ export const GitBranchHistoryView: React.FC = ({
data-bf-part="search"
value={searchQuery}
onValueChange={setSearchQuery}
- leadingIcon={}
+ leadingIcon={}
placeholder={t('search.commits')}
aria-label={t('search.commits')}
size="sm"
@@ -378,7 +378,7 @@ export const GitBranchHistoryView: React.FC = ({
onClick={handleCherryPick}
disabled={isCherryPicking || selectedCommits.size === 0}
loading={isCherryPicking}
- leadingIcon={}
+ leadingIcon={}
>
{isCherryPicking
? t('branchHistory.cherryPickRunning')
@@ -396,7 +396,7 @@ export const GitBranchHistoryView: React.FC = ({
-
+
{filteredCommits.length === 0 ? (
{searchQuery ? t('empty.noMatchingCommits') : t('empty.noCommits')}
@@ -460,7 +460,7 @@ export const GitBranchHistoryView: React.FC
= ({
title={copiedHash === commit.hash ? t('branchHistory.copied') : t('branchHistory.copy')}
>
{commit.hash.substring(0, 7)}
-
+
@@ -469,7 +469,7 @@ export const GitBranchHistoryView: React.FC = ({
aria-label={isExpanded ? t('tooltips.collapseDetails') : t('tooltips.expandDetails')}
className="git-branch-history-view__expand-btn"
size="sm"
- icon={isExpanded ? : }
+ icon={isExpanded ? : }
/>
@@ -498,7 +498,7 @@ export const GitBranchHistoryView: React.FC = ({
})}
)}
-
+
);
};
diff --git a/src/web-ui/src/tools/git/components/GitDiffEditor/GitDiffEditor.tsx b/src/web-ui/src/tools/git/components/GitDiffEditor/GitDiffEditor.tsx
index 0a71e15f41..a7c8d9b2cf 100644
--- a/src/web-ui/src/tools/git/components/GitDiffEditor/GitDiffEditor.tsx
+++ b/src/web-ui/src/tools/git/components/GitDiffEditor/GitDiffEditor.tsx
@@ -4,9 +4,10 @@
*/
import React, { useState, useCallback, useRef, useEffect } from 'react';
+import { Icon } from '@bitfun/ui';
import { useTranslation } from 'react-i18next';
import { DiffEditor } from '@/tools/editor/components/DiffEditor';
-import { X } from 'lucide-react';
+;
import { createLogger } from '@/shared/utils/logger';
import { globalEventBus } from '@/infrastructure/event-bus';
import './GitDiffEditor.scss';
@@ -153,7 +154,7 @@ export const GitDiffEditor: React.FC = ({
>
{error && (
-
+
{error}
)}
diff --git a/src/web-ui/src/tools/git/components/GitDiffView/GitDiffView.tsx b/src/web-ui/src/tools/git/components/GitDiffView/GitDiffView.tsx
index a6df6248c9..bd306b6f14 100644
--- a/src/web-ui/src/tools/git/components/GitDiffView/GitDiffView.tsx
+++ b/src/web-ui/src/tools/git/components/GitDiffView/GitDiffView.tsx
@@ -1,18 +1,14 @@
/** Git diff view. */
-import { Button, IconButton, SegmentedControl } from '@bitfun/ui';
+import { Button, Icon, IconButton, SegmentedControl } from '@bitfun/ui';
import React, { useState, useCallback, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
-import {
- FileText,
+import {
+ FileText,
RefreshCw,
- ChevronDown,
- ChevronRight,
- Plus,
Minus,
- Eye,
EyeOff,
- AlertCircle
+ AlertCircle,
} from 'lucide-react';
import { gitService } from '../../services';
@@ -185,7 +181,7 @@ const GitDiffView: React.FC = ({
const getFileStatusIcon = useCallback((status: DiffFile['status']) => {
switch (status) {
- case 'added': return ;
+ case 'added': return ;
case 'deleted': return ;
default: return ;
}
@@ -302,7 +298,7 @@ const GitDiffView: React.FC = ({
onClick={toggleAllExpansion}
size="sm"
title={allExpanded ? t('diffView.collapseAll') : t('diffView.expandAll')}
- icon={allExpanded ? : }
+ icon={allExpanded ? : }
/>
= ({
>
- {file.expanded ? : }
+ {file.expanded ? : }
@@ -365,7 +361,7 @@ const GitDiffView: React.FC = ({
{file.additions > 0 && (
-
+
{file.additions}
)}
diff --git a/src/web-ui/src/tools/git/components/GitGraphView/GitGraphView.scss b/src/web-ui/src/tools/git/components/GitGraphView/GitGraphView.scss
index d76d79bf65..824c943d1e 100644
--- a/src/web-ui/src/tools/git/components/GitGraphView/GitGraphView.scss
+++ b/src/web-ui/src/tools/git/components/GitGraphView/GitGraphView.scss
@@ -134,7 +134,6 @@
&__content {
flex: 1;
- overflow-y: auto;
overflow-x: auto;
position: relative;
display: flex;
diff --git a/src/web-ui/src/tools/git/components/GitGraphView/GitGraphView.tsx b/src/web-ui/src/tools/git/components/GitGraphView/GitGraphView.tsx
index e2e5abf4b5..0780a919f9 100644
--- a/src/web-ui/src/tools/git/components/GitGraphView/GitGraphView.tsx
+++ b/src/web-ui/src/tools/git/components/GitGraphView/GitGraphView.tsx
@@ -2,9 +2,9 @@
/** Git commit graph view (branch graph). */
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react';
-import { Button, SearchField } from '@bitfun/ui';
+import { Button, Icon, SearchField, ScrollArea } from '@bitfun/ui';
import { useTranslation } from 'react-i18next';
-import { GitBranch, ChevronUp, ChevronDown, Loader2, Search as SearchIcon } from 'lucide-react';
+import { ChevronUp, Loader2 } from 'lucide-react';
import { useI18n } from '@/infrastructure/i18n/hooks/useI18n';
import { gitAPI } from '@/infrastructure/api';
import type { GitGraph, GitGraphNode } from '@/infrastructure/api/service-api/GitAPI';
@@ -291,7 +291,7 @@ export const GitGraphView: React.FC
= ({
{t('graph.title')}
{graphData.currentBranch && (
-
+
{graphData.currentBranch}
)}
@@ -320,7 +320,7 @@ export const GitGraphView: React.FC = ({
onClear={searchQuery ? () => handleSearch('') : undefined}
leadingIcon={searchQuery !== debouncedSearchQuery
?
- : }
+ : }
trailing={
searchFilter && debouncedSearchQuery && searchFilter.totalMatches > 0 ? (
@@ -341,7 +341,7 @@ export const GitGraphView: React.FC = ({
title={t('graph.searchNext')}
disabled={searchFilter.totalMatches === 0}
>
-
+
) : searchFilter && debouncedSearchQuery && searchFilter.totalMatches === 0 ? (
@@ -354,7 +354,7 @@ export const GitGraphView: React.FC = ({
- = ({
);
})}
-
+
);
};
diff --git a/src/web-ui/src/tools/git/components/GitSettingsView/GitSettingsView.css b/src/web-ui/src/tools/git/components/GitSettingsView/GitSettingsView.css
index b2e9e7ffd6..54a330a58e 100644
--- a/src/web-ui/src/tools/git/components/GitSettingsView/GitSettingsView.css
+++ b/src/web-ui/src/tools/git/components/GitSettingsView/GitSettingsView.css
@@ -157,7 +157,6 @@
.settings-content {
flex: 1;
- overflow-y: auto;
padding: 20px;
}
diff --git a/src/web-ui/src/tools/git/components/GitSettingsView/GitSettingsView.scss b/src/web-ui/src/tools/git/components/GitSettingsView/GitSettingsView.scss
index 8a9ccafac3..4f98599e15 100644
--- a/src/web-ui/src/tools/git/components/GitSettingsView/GitSettingsView.scss
+++ b/src/web-ui/src/tools/git/components/GitSettingsView/GitSettingsView.scss
@@ -93,7 +93,6 @@ $opacity-high: 0.8 !default;
&__content {
flex: 1;
- overflow: auto;
padding: $size-gap-4;
}
diff --git a/src/web-ui/src/tools/git/components/GitSettingsView/GitSettingsView.tsx b/src/web-ui/src/tools/git/components/GitSettingsView/GitSettingsView.tsx
index 64cabdefec..ecf7fb7cd1 100644
--- a/src/web-ui/src/tools/git/components/GitSettingsView/GitSettingsView.tsx
+++ b/src/web-ui/src/tools/git/components/GitSettingsView/GitSettingsView.tsx
@@ -1,18 +1,8 @@
/** Git settings view. */
-import { Button, Field, IconButton, Input, Select, TabGroup } from '@bitfun/ui';
+import { Button, Field, Icon, IconButton, Input, Select, TabGroup, ScrollArea } from '@bitfun/ui';
import React, { useState, useCallback, useEffect } from 'react';
-import {
- Settings,
- User,
- Mail,
- Key,
- Globe,
- Save,
- RefreshCw,
- Check,
- X
-} from 'lucide-react';
+import { Mail, Key, Save, RefreshCw } from 'lucide-react';
import { Checkbox } from '@/component-library';
import { useI18n } from '@/infrastructure/i18n';
import './GitSettingsView.scss';
@@ -66,14 +56,14 @@ const GitSettingsView: React.FC = ({
const { t } = useI18n('panels/git');
const tabItems = [
{
- icon: ,
+ icon: ,
id: 'git-settings-user-tab',
label: t('settingsView.tabs.user'),
panelId: 'git-settings-user-panel',
value: 'user',
},
{
- icon: ,
+ icon: ,
id: 'git-settings-repository-tab',
label: t('settingsView.tabs.repository'),
panelId: 'git-settings-repository-panel',
@@ -181,7 +171,7 @@ const GitSettingsView: React.FC = ({
}, []);
const renderUserTab = useCallback(() => (
-
+
), [config.user, updateUserConfig, t]);
const renderRepositoryTab = useCallback(() => (
-
+
{t('settingsView.sections.editor.title')}
@@ -280,11 +270,11 @@ const GitSettingsView: React.FC = ({
))}
-
+
), [config.core, config.remote, updateCoreConfig, updateRemoteConfig, t]);
const renderAdvancedTab = useCallback(() => (
-
+
{t('settingsView.sections.core.title')}
@@ -329,7 +319,7 @@ const GitSettingsView: React.FC = ({
))}
-
+
), [config.core.ignorecase, config.branch, updateCoreConfig, t]);
@@ -354,7 +344,7 @@ const GitSettingsView: React.FC = ({
return (
-
+
{t('settingsView.loadFailedTitle')}
{error}
@@ -369,7 +359,7 @@ const GitSettingsView: React.FC = ({
-
+
{t('settingsView.title')}
@@ -397,28 +387,28 @@ const GitSettingsView: React.FC
= ({
{error && (
-
+
{error}
setError(null)}
className="bitfun-git-settings-view__close-btn"
size="sm"
- icon={}
+ icon={}
/>
)}
{success && (
-
+
{success}
setSuccess(null)}
className="bitfun-git-settings-view__close-btn"
size="sm"
- icon={}
+ icon={}
/>
)}
diff --git a/src/web-ui/src/tools/git/components/PushButton/PushButton.tsx b/src/web-ui/src/tools/git/components/PushButton/PushButton.tsx
index 70896ac239..5738707e48 100644
--- a/src/web-ui/src/tools/git/components/PushButton/PushButton.tsx
+++ b/src/web-ui/src/tools/git/components/PushButton/PushButton.tsx
@@ -1,8 +1,8 @@
/** Push button with optional force-push dropdown. */
-import { Button, IconButton, Tooltip } from '@bitfun/ui';
+import { Button, Icon, IconButton, Tooltip } from '@bitfun/ui';
import React, { useState, useRef, useEffect } from 'react';
-import { ChevronDown, ArrowUp, AlertTriangle } from 'lucide-react';
+import { AlertTriangle } from 'lucide-react';
import { useI18n } from '@/infrastructure/i18n';
import './PushButton.scss';
@@ -109,7 +109,7 @@ export const PushButton: React.FC = ({
disabled={disabled || loading}
loading={loading}
aria-label={t('actions.push')}
- icon={}
+ icon={}
/>
) : (
@@ -119,7 +119,7 @@ export const PushButton: React.FC = ({
onClick={() => handlePush(false)}
disabled={disabled || loading}
loading={loading}
- leadingIcon={}
+ leadingIcon={}
>
{t('actions.push')}
@@ -132,10 +132,7 @@ export const PushButton: React.FC = ({
onClick={handleToggleDropdown}
disabled={disabled || loading}
aria-label={`${t('actions.push')} / ${t('actions.forcePush')}`}
- icon={}
+ icon={}
/>
@@ -152,7 +149,7 @@ export const PushButton: React.FC
= ({
className="bitfun-push-button__menu-item"
onClick={() => handlePush(false)}
>
-
+
{t('actions.push')}
diff --git a/src/web-ui/src/tools/terminal/components/ConnectedTerminal.tsx b/src/web-ui/src/tools/terminal/components/ConnectedTerminal.tsx
index ad91cd152f..5bd33c5a1b 100644
--- a/src/web-ui/src/tools/terminal/components/ConnectedTerminal.tsx
+++ b/src/web-ui/src/tools/terminal/components/ConnectedTerminal.tsx
@@ -4,8 +4,8 @@
*/
import React, { useEffect, useRef, useCallback, useState, memo } from 'react';
-import { Button } from '@bitfun/ui';
-import { AlertCircle, RefreshCw, Terminal as TerminalIcon, Trash2 } from 'lucide-react';
+import { Button, Icon } from '@bitfun/ui';
+import { AlertCircle, RefreshCw, Trash2 } from 'lucide-react';
import Terminal, { TerminalRef, type TerminalOptions } from './Terminal';
import { useTerminal } from '../hooks/useTerminal';
import { registerTerminalActions, unregisterTerminalActions } from '../services/TerminalActionManager';
@@ -490,7 +490,7 @@ const ConnectedTerminal: React.FC = memo(({
{showToolbar && (
-
+
{title}
{session && (
diff --git a/src/web-ui/src/tools/workspace/components/WorkspaceManager.css b/src/web-ui/src/tools/workspace/components/WorkspaceManager.css
index 8810c88494..30a8bbb6a3 100644
--- a/src/web-ui/src/tools/workspace/components/WorkspaceManager.css
+++ b/src/web-ui/src/tools/workspace/components/WorkspaceManager.css
@@ -4,7 +4,6 @@
flex-direction: column;
gap: 24px;
max-height: 70vh;
- overflow-y: auto;
padding-right: 2px;
}
diff --git a/src/web-ui/src/tools/workspace/components/WorkspaceManager.tsx b/src/web-ui/src/tools/workspace/components/WorkspaceManager.tsx
index 7e85cb475b..3fd21113c7 100644
--- a/src/web-ui/src/tools/workspace/components/WorkspaceManager.tsx
+++ b/src/web-ui/src/tools/workspace/components/WorkspaceManager.tsx
@@ -1,6 +1,6 @@
-import { Button, Modal } from '@bitfun/ui';
+import { Button, Icon, Modal, ScrollArea } from '@bitfun/ui';
import React, { useState } from 'react';
-import { FolderOpen, Clock, FileText, Code, Folder } from 'lucide-react';
+import { FolderOpen, FileText, Code } from 'lucide-react';
import { useWorkspaceContext } from '../../../infrastructure/contexts/WorkspaceContext';
import { WorkspaceInfo, WorkspaceKind, WorkspaceType } from '../../../shared/types';
import { AssistantAvatar } from '@/app/components/AssistantAvatar';
@@ -156,7 +156,7 @@ const WorkspaceManager: React.FC = ({
case WorkspaceType.Documentation:
return ;
case WorkspaceType.MultiProject:
- return ;
+ return ;
default:
return ;
}
@@ -187,7 +187,7 @@ const WorkspaceManager: React.FC = ({
title="Workspace Status"
size="medium"
>
-
+
{error && (
Error: {error}
@@ -209,7 +209,7 @@ const WorkspaceManager: React.FC = ({
{currentWorkspace.workspaceType}
{currentWorkspace.lastAccessed && (
-
+
{formatDate(currentWorkspace.lastAccessed)}
)}
@@ -304,7 +304,7 @@ const WorkspaceManager: React.FC = ({
{workspace.workspaceType}
{workspace.lastAccessed && (
-
+
{formatDate(workspace.lastAccessed)}
)}
@@ -348,7 +348,7 @@ const WorkspaceManager: React.FC = ({
assistant
{workspace.lastAccessed && (
-
+
{formatDate(workspace.lastAccessed)}
)}
@@ -366,7 +366,7 @@ const WorkspaceManager: React.FC = ({
)}
-
+
);
};
diff --git a/tests/e2e/specs/l0-navigation.spec.ts b/tests/e2e/specs/l0-navigation.spec.ts
index 70be6cfc0f..a03d066fae 100644
--- a/tests/e2e/specs/l0-navigation.spec.ts
+++ b/tests/e2e/specs/l0-navigation.spec.ts
@@ -363,8 +363,8 @@ describe('L0 Navigation Panel', () => {
const alignment = await browser.execute(() => {
const parent = document.querySelector
('[data-testid="agent-skill-entry"]');
const parentLabel = parent?.querySelector(':scope > span:last-child');
- const childIcons = Array.from(document.querySelectorAll(
- '[data-testid="agent-skill-tabs"] .bitfun-nav-panel__top-action-icon-slot > svg',
+ const childIcons = Array.from(document.querySelectorAll(
+ '[data-testid="agent-skill-tabs"] .bitfun-nav-panel__top-action-icon-slot > svg, [data-testid="agent-skill-tabs"] .bitfun-nav-panel__top-action-icon-slot > [data-bf-component="icon"]',
));
if (!parentLabel || childIcons.length === 0) {
return null;