feat: enrich project selector details
The project selector previously used a native list that showed only project names and inherited platform-specific styling. It now presents an accessible rich menu grouped into local projects and remote channels, with folder or channel context for each option. Release note: 项目选择器现在按本地项目和远程通道分组,并显示目录或通道来源,切换项目时更容易辨认目标。
This commit is contained in:
+1
-1
@@ -12,7 +12,7 @@
|
||||
### 桌面基础、工作空间与上下文
|
||||
|
||||
- [x] **跨平台桌面应用**:支持 Windows、macOS、Linux,以及 `x64`、`arm64` 发布目标。
|
||||
- [x] **Projects 与独立对话**:按项目隔离上下文,管理会话、附件和 Git 工作区变更。
|
||||
- [x] **Projects 与独立对话**:按项目隔离上下文,管理会话、附件和 Git 工作区变更;项目选择器区分本地项目与远程通道,并在展开后显示本地目录或通道来源等辨认信息。
|
||||
- [x] **文件、截图、窗口、剪贴板上下文**:用户明确选择后才加入模型上下文。
|
||||
- [x] **富文本回答**:支持 GitHub Flavored Markdown、LaTeX 数学公式和受控 Mermaid 图表;大图可缩放、拖动或查看源码,失败时保留原始图表代码。
|
||||
- [ ] **项目 Agent Space**(规划中):在 Project 中统一角色、知识、Skills/MCP、模型、审批策略、预算和超时,并支持模板复用。
|
||||
|
||||
+5
-2
@@ -324,16 +324,19 @@
|
||||
- 窄窗口下优先压缩状态标签并保留图标按钮,不隐藏窗口控制、当前范围或进行中的风险状态。
|
||||
- 使用全局菜单时,菜单项使用 `--font-body`、`14px` 图标和约 `32px` 单项高度;标签使用短名称。菜单保留 `menu`、`menuitem` 语义,支持上下方向键、Home、End 和 Escape,关闭后焦点返回触发按钮。
|
||||
|
||||
### 6.11 上下文单选菜单
|
||||
### 6.11 单选选择器
|
||||
|
||||
模型、专家角色、工作模式、Runtime Agent、Runtime 预设和 Runtime 快捷操作属于同一输入上下文,其选择器必须共享结构、尺寸和菜单视觉,不能出现一个精细菜单与多个风格不一致的原生下拉框。
|
||||
|
||||
- 只有选项能由单行短标签充分区分、不需要补充来源、位置、状态或说明时,才使用原生 `select`。
|
||||
- 当用户必须在选择前比较来源、范围、位置、连接状态或其他辅助信息时,使用与输入区选择器同类的自定义富信息单选菜单,不用 CSS 修饰原生 `select` 冒充富信息菜单。
|
||||
- 触发按钮复用统一的模型选择按钮样式,保持相同高度、圆角、边框、展开指示和焦点状态。
|
||||
- 菜单使用 `menu` 与 `menuitemradio` 语义,当前项同时显示选中标记和 `aria-checked`。选项可以包含一行简短说明,但标签和说明不得被截断到无法区分。
|
||||
- 支持上、下方向键、Home、End、Enter 或 Space、Escape;打开后焦点进入当前项,关闭后返回触发按钮。
|
||||
- 点击或聚焦菜单外部时关闭;同一输入区内的模型、专家和模式菜单互斥展开。
|
||||
- 不可用选项保持可读并说明原因,键盘导航不得停留在不可选择项上。
|
||||
- 仅在选项简单且不需要说明、禁用原因或一致菜单行为时使用原生 `select`。
|
||||
- 项目选择器属于富信息单选菜单。收起时保持当前项目名称紧凑可见;展开后每项至少显示项目名称和项目类型,并按数据可用性补充本地目录、远程来源、平台、连接状态或其他能帮助辨认项目的信息。
|
||||
- 项目菜单当前区分“本地项目 / 远程通道”。未来出现真正的远程项目时新增独立分组,不把消息通道命名为远程项目。分组、项目类型和连接状态均须显示文字,不只依赖图标或颜色;新增项目来源时不得退回仅列名称的原生 `select`。
|
||||
|
||||
### 6.12 应用通知与就地反馈
|
||||
|
||||
|
||||
@@ -79,10 +79,10 @@ GoodBuddy 已将企业微信、钉钉和微信 ClawBot 远程消息通道纳入
|
||||
|
||||
### 5.1 项目分组
|
||||
|
||||
项目选择器增加“远程通道”分组:
|
||||
项目选择器使用富信息单选菜单,不使用只显示项目名称的原生 `select`。当前区分“本地项目 / 远程通道”;未来出现真正的远程项目时,应新增独立分组,不把通道提前命名为远程项目:
|
||||
|
||||
```text
|
||||
普通项目
|
||||
本地项目
|
||||
├─ 默认项目
|
||||
└─ 用户创建的其他项目
|
||||
|
||||
@@ -92,6 +92,8 @@ GoodBuddy 已将企业微信、钉钉和微信 ClawBot 远程消息通道纳入
|
||||
└─ 钉钉
|
||||
```
|
||||
|
||||
选择器收起时显示当前项目名称;展开后每个选项至少显示项目名称和项目类型。本地项目补充目录摘要,远程通道项目补充平台来源和默认目录;接入可复用的通道状态数据后继续补充文字连接状态。未来接入其他远程项目时可以补充远程主机、工作区或服务来源,而不改变菜单的基本结构。
|
||||
|
||||
每个通道项目持续显示连接状态:
|
||||
|
||||
- 未配置
|
||||
@@ -645,6 +647,8 @@ Renderer 快照只返回是否已配置和脱敏标识。
|
||||
- [ ] 同名普通项目不会被占用或修改。
|
||||
- [ ] 通道项目默认根目录为当前用户目录,默认模式为 Ask。
|
||||
- [ ] 通道项目在项目选择器的“远程通道”分组中显示。
|
||||
- [ ] 项目选择器使用富信息单选菜单;每项显示项目名称和类型,本地项目显示目录摘要,远程通道显示平台来源与默认目录,并在可复用状态数据接入后显示文字连接状态。
|
||||
- [ ] 项目选择器的信息结构可扩展为“本地 / 远程”分组,不以图标或颜色作为项目来源和状态的唯一表达。
|
||||
- [ ] 停用或断开通道不会删除项目和历史。
|
||||
- [ ] 普通项目删除流程不能永久删除通道项目。
|
||||
|
||||
|
||||
@@ -4706,6 +4706,52 @@ describe('App', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('shows grouped project details and keeps the rich menu keyboard accessible', async () => {
|
||||
const channelProject = {
|
||||
...project,
|
||||
id: '00000000-0000-4000-8000-000000000201',
|
||||
name: '微信 ClawBot',
|
||||
kind: 'channel' as const,
|
||||
channel: 'weixin' as const
|
||||
}
|
||||
vi.mocked(api.projects.list).mockResolvedValueOnce([
|
||||
project,
|
||||
channelProject
|
||||
])
|
||||
render(<App />)
|
||||
|
||||
const trigger = await screen.findByRole('button', {
|
||||
name: '当前项目'
|
||||
})
|
||||
fireEvent.keyDown(trigger, { key: 'ArrowDown' })
|
||||
|
||||
const menu = screen.getByRole('menu', { name: '当前项目' })
|
||||
expect(
|
||||
within(menu).getByRole('group', { name: '本地项目' })
|
||||
).toHaveTextContent('本地目录 · C:\\Users\\test')
|
||||
expect(
|
||||
within(menu).getByRole('group', { name: '远程通道' })
|
||||
).toHaveTextContent(
|
||||
'微信 ClawBot · 远程通道 · C:\\Users\\test'
|
||||
)
|
||||
const selectedProject = within(menu)
|
||||
.getByText(project.name, { selector: 'b' })
|
||||
.closest<HTMLButtonElement>('[role="menuitemradio"]')
|
||||
const remoteProject = within(menu)
|
||||
.getByText(channelProject.name, { selector: 'b' })
|
||||
.closest<HTMLButtonElement>('[role="menuitemradio"]')
|
||||
expect(selectedProject).toHaveAttribute('aria-checked', 'true')
|
||||
await waitFor(() => expect(selectedProject).toHaveFocus())
|
||||
|
||||
fireEvent.keyDown(selectedProject!, { key: 'End' })
|
||||
expect(remoteProject).toHaveFocus()
|
||||
fireEvent.keyDown(remoteProject!, { key: 'Escape' })
|
||||
expect(trigger).toHaveFocus()
|
||||
expect(
|
||||
screen.queryByRole('menu', { name: '当前项目' })
|
||||
).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('keeps channel projects empty until a client message creates a remote conversation', async () => {
|
||||
const channelProject = {
|
||||
...project,
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import {
|
||||
Archive,
|
||||
Check,
|
||||
ChevronDown,
|
||||
Folder,
|
||||
FolderOpen,
|
||||
Plus,
|
||||
RadioTower,
|
||||
Settings,
|
||||
Trash2,
|
||||
X
|
||||
@@ -15,7 +19,8 @@ import type {
|
||||
} from '../../shared/assistant-contracts'
|
||||
import {
|
||||
interactiveWorkModes,
|
||||
normalizeInteractiveWorkMode
|
||||
normalizeInteractiveWorkMode,
|
||||
projectChannelLabels
|
||||
} from '../../shared/assistant-contracts'
|
||||
import type { RuntimeSettings } from '../../shared/contracts'
|
||||
import { trapTabFocus } from './dialog-focus'
|
||||
@@ -66,10 +71,14 @@ export function ProjectSwitcher({
|
||||
const [error, setError] = useState<string>()
|
||||
const createButtonRef = useRef<HTMLButtonElement>(null)
|
||||
const settingsButtonRef = useRef<HTMLButtonElement>(null)
|
||||
const projectPickerRef = useRef<HTMLDivElement>(null)
|
||||
const projectPickerButtonRef = useRef<HTMLButtonElement>(null)
|
||||
const projectPickerMenuRef = useRef<HTMLDivElement>(null)
|
||||
const dialogRef = useRef<HTMLDivElement>(null)
|
||||
const restoreFocusTarget = useRef<
|
||||
'create' | 'settings' | undefined
|
||||
>(undefined)
|
||||
const [projectMenuOpen, setProjectMenuOpen] = useState(false)
|
||||
const [draft, setDraft] = useState<ProjectCreateInput>({
|
||||
name: '',
|
||||
description: '',
|
||||
@@ -87,6 +96,38 @@ export function ProjectSwitcher({
|
||||
)
|
||||
const busy = saving || archiving || deleting
|
||||
|
||||
useEffect(() => {
|
||||
if (!projectMenuOpen) {
|
||||
return
|
||||
}
|
||||
const focusFrame = requestAnimationFrame(() => {
|
||||
const items = Array.from(
|
||||
projectPickerMenuRef.current?.querySelectorAll<HTMLButtonElement>(
|
||||
'[role="menuitemradio"]'
|
||||
) ?? []
|
||||
)
|
||||
const initialItem =
|
||||
items.find((item) => item.getAttribute('aria-checked') === 'true') ??
|
||||
items[0]
|
||||
initialItem?.focus()
|
||||
})
|
||||
const closeOutside = (event: Event): void => {
|
||||
if (
|
||||
event.target instanceof Node &&
|
||||
!projectPickerRef.current?.contains(event.target)
|
||||
) {
|
||||
setProjectMenuOpen(false)
|
||||
}
|
||||
}
|
||||
document.addEventListener('pointerdown', closeOutside)
|
||||
document.addEventListener('focusin', closeOutside)
|
||||
return () => {
|
||||
cancelAnimationFrame(focusFrame)
|
||||
document.removeEventListener('pointerdown', closeOutside)
|
||||
document.removeEventListener('focusin', closeOutside)
|
||||
}
|
||||
}, [projectMenuOpen])
|
||||
|
||||
useEffect(() => {
|
||||
if (!dialogMode) {
|
||||
if (restoreFocusTarget.current === 'create') {
|
||||
@@ -207,36 +248,150 @@ export function ProjectSwitcher({
|
||||
return (
|
||||
<div className="project-switcher">
|
||||
<div className="project-switcher__row">
|
||||
<select
|
||||
aria-label={t('projectSwitcher.selector.ariaLabel')}
|
||||
onChange={(event) => onSelect(event.target.value)}
|
||||
value={activeProjectId}
|
||||
<div
|
||||
className="project-switcher__picker"
|
||||
ref={projectPickerRef}
|
||||
>
|
||||
{userProjects.length > 0 && (
|
||||
<optgroup label={t('projectSwitcher.selector.userProjects')}>
|
||||
{userProjects.map((project) => (
|
||||
<option key={project.id} value={project.id}>
|
||||
{project.name}
|
||||
</option>
|
||||
))}
|
||||
</optgroup>
|
||||
)}
|
||||
{channelProjects.length > 0 && (
|
||||
<optgroup
|
||||
label={t('projectSwitcher.selector.channelProjects')}
|
||||
<button
|
||||
aria-expanded={projectMenuOpen}
|
||||
aria-haspopup="menu"
|
||||
aria-label={t('projectSwitcher.selector.ariaLabel')}
|
||||
className="project-switcher__trigger"
|
||||
onClick={() => setProjectMenuOpen((open) => !open)}
|
||||
onKeyDown={(event) => {
|
||||
if (
|
||||
!projectMenuOpen &&
|
||||
(event.key === 'ArrowDown' ||
|
||||
event.key === 'Enter' ||
|
||||
event.key === ' ')
|
||||
) {
|
||||
event.preventDefault()
|
||||
setProjectMenuOpen(true)
|
||||
}
|
||||
}}
|
||||
ref={projectPickerButtonRef}
|
||||
title={activeProject?.name}
|
||||
type="button"
|
||||
>
|
||||
<span>{activeProject?.name ?? t('projectSwitcher.selector.empty')}</span>
|
||||
<ChevronDown aria-hidden="true" size={14} />
|
||||
</button>
|
||||
{projectMenuOpen && (
|
||||
<div
|
||||
aria-label={t('projectSwitcher.selector.ariaLabel')}
|
||||
className="project-switcher__menu"
|
||||
onKeyDown={(event) => {
|
||||
const items = Array.from(
|
||||
event.currentTarget.querySelectorAll<HTMLButtonElement>(
|
||||
'[role="menuitemradio"]'
|
||||
)
|
||||
)
|
||||
const currentIndex = items.indexOf(
|
||||
document.activeElement as HTMLButtonElement
|
||||
)
|
||||
let nextIndex: number | undefined
|
||||
if (event.key === 'ArrowDown') {
|
||||
nextIndex = (currentIndex + 1) % items.length
|
||||
} else if (event.key === 'ArrowUp') {
|
||||
nextIndex =
|
||||
(currentIndex - 1 + items.length) % items.length
|
||||
} else if (event.key === 'Home') {
|
||||
nextIndex = 0
|
||||
} else if (event.key === 'End') {
|
||||
nextIndex = items.length - 1
|
||||
} else if (event.key === 'Escape') {
|
||||
event.preventDefault()
|
||||
setProjectMenuOpen(false)
|
||||
projectPickerButtonRef.current?.focus()
|
||||
}
|
||||
const nextItem =
|
||||
nextIndex === undefined ? undefined : items.at(nextIndex)
|
||||
if (nextItem) {
|
||||
event.preventDefault()
|
||||
items.forEach((item) => {
|
||||
item.tabIndex = item === nextItem ? 0 : -1
|
||||
})
|
||||
nextItem.focus()
|
||||
}
|
||||
}}
|
||||
ref={projectPickerMenuRef}
|
||||
role="menu"
|
||||
>
|
||||
{channelProjects.map((project) => (
|
||||
<option key={project.id} value={project.id}>
|
||||
{project.name}
|
||||
</option>
|
||||
))}
|
||||
</optgroup>
|
||||
{[
|
||||
{
|
||||
projects: userProjects,
|
||||
label: t('projectSwitcher.selector.userProjects'),
|
||||
icon: Folder
|
||||
},
|
||||
{
|
||||
projects: channelProjects,
|
||||
label: t('projectSwitcher.selector.channelProjects'),
|
||||
icon: RadioTower
|
||||
}
|
||||
].map((group) =>
|
||||
group.projects.length > 0 ? (
|
||||
<div
|
||||
aria-label={group.label}
|
||||
className="project-switcher__group"
|
||||
key={group.label}
|
||||
role="group"
|
||||
>
|
||||
<strong>{group.label}</strong>
|
||||
{group.projects.map((project) => {
|
||||
const selected = project.id === activeProjectId
|
||||
const ProjectIcon = group.icon
|
||||
const detail =
|
||||
project.kind === 'channel'
|
||||
? t('projectSwitcher.selector.remoteDetail', {
|
||||
channel: project.channel
|
||||
? projectChannelLabels[project.channel]
|
||||
: t(
|
||||
'projectSwitcher.selector.remoteChannel'
|
||||
),
|
||||
path: project.rootPath
|
||||
})
|
||||
: t('projectSwitcher.selector.localDetail', {
|
||||
path: project.rootPath
|
||||
})
|
||||
return (
|
||||
<button
|
||||
aria-checked={selected}
|
||||
key={project.id}
|
||||
onClick={() => {
|
||||
if (!selected) {
|
||||
onSelect(project.id)
|
||||
}
|
||||
setProjectMenuOpen(false)
|
||||
requestAnimationFrame(() =>
|
||||
projectPickerButtonRef.current?.focus()
|
||||
)
|
||||
}}
|
||||
role="menuitemradio"
|
||||
tabIndex={selected ? 0 : -1}
|
||||
type="button"
|
||||
>
|
||||
<ProjectIcon aria-hidden="true" size={16} />
|
||||
<span>
|
||||
<b>{project.name}</b>
|
||||
<small>{detail}</small>
|
||||
</span>
|
||||
{selected && (
|
||||
<Check aria-hidden="true" size={14} />
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
) : null
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</select>
|
||||
</div>
|
||||
<button
|
||||
aria-label={t('projectSwitcher.selector.create')}
|
||||
className="icon-button"
|
||||
onClick={() => {
|
||||
setProjectMenuOpen(false)
|
||||
setError(undefined)
|
||||
setConfirmingDelete(false)
|
||||
setDeleteConfirmation('')
|
||||
@@ -265,6 +420,7 @@ export function ProjectSwitcher({
|
||||
if (!activeProject) {
|
||||
return
|
||||
}
|
||||
setProjectMenuOpen(false)
|
||||
setError(undefined)
|
||||
setConfirmingDelete(false)
|
||||
setDeleteConfirmation('')
|
||||
|
||||
@@ -150,6 +150,15 @@ describe('WorkspacePrimitives', () => {
|
||||
expect(stylesheet).not.toContain('.conversation-delete')
|
||||
})
|
||||
|
||||
it('uses the shared control and menu surfaces for the rich project picker', () => {
|
||||
expect(stylesheet).toMatch(
|
||||
/\.project-switcher__trigger\s*\{[^}]*height:\s*var\(--control-height\);[^}]*border-radius:\s*var\(--radius-control\);[^}]*background:\s*var\(--surface-raised\);/u
|
||||
)
|
||||
expect(stylesheet).toMatch(
|
||||
/\.project-switcher__menu\s*\{[^}]*border-radius:\s*var\(--radius-card\);[^}]*background:\s*var\(--surface-raised\);[^}]*box-shadow:\s*var\(--shadow-dialog\);/u
|
||||
)
|
||||
})
|
||||
|
||||
it('separates Runtime-specific controls from the main composer toolbar', () => {
|
||||
expect(stylesheet).toMatch(
|
||||
/\.composer__toolbar--with-runtime-controls\s*\{[^}]*border-radius:\s*0;/u
|
||||
|
||||
@@ -9,8 +9,12 @@ export const workspace = {
|
||||
},
|
||||
selector: {
|
||||
ariaLabel: 'Current project',
|
||||
userProjects: 'Projects',
|
||||
userProjects: 'Local projects',
|
||||
channelProjects: 'Remote channels',
|
||||
empty: 'No project selected',
|
||||
localDetail: 'Local folder · {{path}}',
|
||||
remoteDetail: '{{channel}} · Remote channel · {{path}}',
|
||||
remoteChannel: 'Remote channel',
|
||||
create: 'New project',
|
||||
settings: 'Project settings'
|
||||
},
|
||||
|
||||
@@ -6,8 +6,12 @@ export const workspace = {
|
||||
},
|
||||
selector: {
|
||||
ariaLabel: '当前项目',
|
||||
userProjects: '普通项目',
|
||||
userProjects: '本地项目',
|
||||
channelProjects: '远程通道',
|
||||
empty: '未选择项目',
|
||||
localDetail: '本地目录 · {{path}}',
|
||||
remoteDetail: '{{channel}} · 远程通道 · {{path}}',
|
||||
remoteChannel: '远程通道',
|
||||
create: '新建项目',
|
||||
settings: '项目设置'
|
||||
},
|
||||
|
||||
+108
-3
@@ -2317,15 +2317,120 @@ textarea:focus-visible {
|
||||
grid-template-columns: minmax(0, 1fr) auto auto;
|
||||
}
|
||||
|
||||
.project-switcher select {
|
||||
width: 100%;
|
||||
.project-switcher__picker {
|
||||
min-width: 0;
|
||||
padding: 8px 9px;
|
||||
}
|
||||
|
||||
.project-switcher__trigger {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: var(--control-height);
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
padding: 0 var(--space-3);
|
||||
border: 1px solid var(--border-default);
|
||||
border-radius: var(--radius-control);
|
||||
background: var(--surface-raised);
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
font-size: var(--font-body);
|
||||
font-weight: 600;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.project-switcher__trigger:hover,
|
||||
.project-switcher__trigger[aria-expanded="true"] {
|
||||
border-color: var(--accent);
|
||||
background: var(--accent-subtle);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.project-switcher__trigger > span {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
text-align: left;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.project-switcher__menu {
|
||||
position: absolute;
|
||||
z-index: 30;
|
||||
top: calc(var(--control-height) + var(--space-2));
|
||||
right: 0;
|
||||
left: 0;
|
||||
display: grid;
|
||||
max-height: min(420px, calc(100vh - 160px));
|
||||
padding: var(--space-2);
|
||||
border: 1px solid var(--border-default);
|
||||
border-radius: var(--radius-card);
|
||||
overflow-y: auto;
|
||||
background: var(--surface-raised);
|
||||
box-shadow: var(--shadow-dialog);
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.project-switcher__group {
|
||||
display: grid;
|
||||
gap: var(--space-1);
|
||||
}
|
||||
|
||||
.project-switcher__group + .project-switcher__group {
|
||||
padding-top: var(--space-2);
|
||||
border-top: 1px solid var(--border-subtle);
|
||||
}
|
||||
|
||||
.project-switcher__group > strong {
|
||||
padding: var(--space-1) var(--space-2);
|
||||
color: var(--text-muted);
|
||||
font-size: var(--font-caption);
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.project-switcher__group > button {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
min-height: 52px;
|
||||
align-items: center;
|
||||
padding: var(--space-2);
|
||||
border-radius: var(--radius-control);
|
||||
background: transparent;
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
gap: var(--space-2);
|
||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.project-switcher__group > button:hover,
|
||||
.project-switcher__group > button[aria-checked="true"] {
|
||||
background: var(--accent-subtle);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.project-switcher__group > button > span {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.project-switcher__group > button b,
|
||||
.project-switcher__group > button small {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.project-switcher__group > button b {
|
||||
font-size: var(--font-body);
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.project-switcher__group > button small {
|
||||
color: var(--text-muted);
|
||||
font-size: var(--font-caption);
|
||||
}
|
||||
|
||||
.project-create-backdrop {
|
||||
|
||||
Reference in New Issue
Block a user