feat: 项目卡片统一加框+小字精简 + 图标点击可选(18emoji) + 新建布局与travelatlas唯一互斥
This commit is contained in:
@@ -74,6 +74,9 @@ const PRESET_DEFS = [
|
||||
{ id: 't3', leftCount: 0, topCount: 1, contentCount: 2, chatFull: true },
|
||||
] as const
|
||||
|
||||
/** 侧栏图标备选集(emoji):布局/快捷方式的图标,点击可换(首项 🧱 为布局默认) */
|
||||
const EMOJI_SET = ['🧱', '🏠', '🎓', '🚗', '✈️', '🌍', '🏥', '📚', '✏️', '⚙️', '🎨', '🎮', '🌏', '📐', '🧪', '🤖', '📦', '💬']
|
||||
|
||||
function buildLayout(presetId: string, name: string): LayoutSpec {
|
||||
const def = PRESET_DEFS.find((d) => d.id === presetId) ?? PRESET_DEFS[0]
|
||||
const mk = (i: number): SplitPane => ({
|
||||
@@ -283,6 +286,8 @@ function WorktableSection(props: any) {
|
||||
const [wsPreset, setWsPreset] = useState<string>('2h')
|
||||
const [wsName, setWsName] = useState('')
|
||||
const [wsError, setWsError] = useState(false)
|
||||
/** 图标选择器:kind + 目标 id + 弹窗锚点坐标(fixed 定位) */
|
||||
const [iconPick, setIconPick] = useState<{ kind: 'layout' | 'shortcut'; id: string; x: number; y: number } | null>(null)
|
||||
const [float, setFloat] = useState<FloatRect | null>(() =>
|
||||
view.dock === 'float' && view.floatTop != null ? { top: view.floatTop } : null,
|
||||
)
|
||||
@@ -622,6 +627,28 @@ function WorktableSection(props: any) {
|
||||
persistProjects((prev) => ({ ...prev, layouts: prev.layouts.filter((l) => l.id !== id) }))
|
||||
}
|
||||
|
||||
// ── 图标选择器(布局 / 快捷方式的侧栏 emoji 点击可换) ──
|
||||
const openIconPick = (kind: 'layout' | 'shortcut', id: string, e: any) => {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
const r = (e.currentTarget as HTMLElement).getBoundingClientRect()
|
||||
const x = Math.min(r.right + 8, Math.max(16, window.innerWidth - 284))
|
||||
const y = Math.max(MIN_TOP, Math.min(r.top - 4, window.innerHeight - 316))
|
||||
setIconPick({ kind, id, x, y })
|
||||
}
|
||||
const setLayoutIcon = (id: string, icon: string) => {
|
||||
persistProjects((prev) => ({
|
||||
...prev,
|
||||
layouts: prev.layouts.map((l) => (l.id === id ? { ...l, icon } : l)),
|
||||
}))
|
||||
}
|
||||
const setShortcutIcon = (id: string, icon: string) => {
|
||||
persistProjects((prev) => ({
|
||||
...prev,
|
||||
shortcuts: prev.shortcuts.map((s) => (s.id === id ? { ...s, icon } : s)),
|
||||
}))
|
||||
}
|
||||
|
||||
const query = view.query.trim()
|
||||
const queryLower = query.toLowerCase()
|
||||
const visibleShortcuts = projects.shortcuts.filter((s) =>
|
||||
@@ -705,7 +732,7 @@ function WorktableSection(props: any) {
|
||||
if (!wide) {
|
||||
const projectIcons = registeredIds.map((id) => metas[id]?.icon ?? '📦')
|
||||
const shortcutIcons = projects.shortcuts.map((s) => s.icon)
|
||||
const layoutIcons = projects.layouts.map(() => '🧱')
|
||||
const layoutIcons = projects.layouts.map((l) => l.icon ?? '🧱')
|
||||
const icons = [...projectIcons, ...shortcutIcons, ...layoutIcons]
|
||||
const railNames = [
|
||||
...registeredIds.map((id) => projects.nameOverrides[id] ?? metas[id]?.name ?? id),
|
||||
@@ -819,6 +846,35 @@ function WorktableSection(props: any) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{iconPick && (
|
||||
<>
|
||||
<div className="dsh-wt_popBackdrop" onClick={() => setIconPick(null)} />
|
||||
<div className="dsh-wt_iconPop" style={{ left: iconPick.x, top: iconPick.y }}>
|
||||
<div className="dsh-wt_iconPopTitle">{t('icons.title')}</div>
|
||||
<div className="dsh-wt_iconGrid">
|
||||
{EMOJI_SET.map((em) => {
|
||||
const cur = iconPick.kind === 'layout'
|
||||
? (projects.layouts.find((l) => l.id === iconPick.id)?.icon ?? '🧱')
|
||||
: (projects.shortcuts.find((s) => s.id === iconPick.id)?.icon ?? '🔗')
|
||||
return (
|
||||
<button
|
||||
key={em}
|
||||
type="button"
|
||||
className="dsh-wt_iconCell"
|
||||
data-on={cur === em ? 'true' : 'false'}
|
||||
onClick={() => {
|
||||
if (iconPick.kind === 'layout') setLayoutIcon(iconPick.id, em)
|
||||
else setShortcutIcon(iconPick.id, em)
|
||||
setIconPick(null)
|
||||
}}
|
||||
>{em}</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{viewOptionsOpen && (
|
||||
<div className="dsh-wt_menu">
|
||||
<span className="dsh-wt_menuLabel">{t('sort.label')}</span>
|
||||
@@ -858,7 +914,15 @@ function WorktableSection(props: any) {
|
||||
onDragEnd={() => { dragIdRef.current = null }}
|
||||
>
|
||||
<span className="dsh-wt_manageGrip" aria-hidden>≡</span>
|
||||
<span className="dsh-wt_manageIcon" aria-hidden>{layout ? '🧱' : (meta?.icon ?? '📦')}</span>
|
||||
{layout
|
||||
? <span
|
||||
className="dsh-wt_manageIcon dsh-wt_iconPick"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
title={t('icons.change')}
|
||||
onClick={(e) => openIconPick('layout', id, e)}
|
||||
>{layout.icon ?? '🧱'}</span>
|
||||
: <span className="dsh-wt_manageIcon" aria-hidden>{meta?.icon ?? '📦'}</span>}
|
||||
<input
|
||||
className="dsh-wt_manageInput"
|
||||
value={display}
|
||||
@@ -879,7 +943,13 @@ function WorktableSection(props: any) {
|
||||
{projects.shortcuts.map((s) => (
|
||||
<div key={s.id} className="dsh-wt_manageRow dsh-wt_manageRowSc">
|
||||
<span className="dsh-wt_manageGrip" aria-hidden>🔗</span>
|
||||
<span className="dsh-wt_manageIcon" aria-hidden>{s.icon}</span>
|
||||
<span
|
||||
className="dsh-wt_manageIcon dsh-wt_iconPick"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
title={t('icons.change')}
|
||||
onClick={(e) => openIconPick('shortcut', s.id, e)}
|
||||
>{s.icon}</span>
|
||||
<span className="dsh-wt_manageScName">{s.name}</span>
|
||||
<button type="button" className="dsh-wt_manageBtn" title={t('manage.deleteShortcut')} onClick={() => removeShortcut(s.id)}>✕</button>
|
||||
</div>
|
||||
@@ -903,7 +973,13 @@ function WorktableSection(props: any) {
|
||||
style={{ order: effectiveOrder.indexOf(l.id) + 1000 }}
|
||||
onClick={() => { openSplit(l); reportUsed(l.id) }}
|
||||
>
|
||||
<span className="dsh-wt_layoutIcon" aria-hidden>🧱</span>
|
||||
<span
|
||||
className="dsh-wt_layoutIcon dsh-wt_iconPick"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
title={t('icons.change')}
|
||||
onClick={(e) => openIconPick('layout', l.id, e)}
|
||||
>{l.icon ?? '🧱'}</span>
|
||||
<span className="dsh-wt_layoutText">
|
||||
<span className="dsh-wt_layoutName">{projects.nameOverrides[l.id] ?? l.title}</span>
|
||||
<span className="dsh-wt_layoutDesc">{t('layout.desc', { n: String(paneCount) })}</span>
|
||||
@@ -926,7 +1002,13 @@ function WorktableSection(props: any) {
|
||||
rel="noreferrer noopener"
|
||||
title={s.href}
|
||||
>
|
||||
<span className="dsh-wt_shortcutIcon" aria-hidden>{s.icon}</span>
|
||||
<span
|
||||
className="dsh-wt_shortcutIcon dsh-wt_iconPick"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
title={t('icons.change')}
|
||||
onClick={(e) => openIconPick('shortcut', s.id, e)}
|
||||
>{s.icon}</span>
|
||||
<span className="dsh-wt_shortcutName">{s.name}</span>
|
||||
<span className="dsh-wt_shortcutBadge">{t('shortcut.badge')}</span>
|
||||
</a>
|
||||
|
||||
@@ -67,6 +67,8 @@ export const zh = {
|
||||
'pane.subagentsEmpty': '当前会话暂无子代理',
|
||||
'split.flip': '切换对话窗位置(左⇄右)',
|
||||
'split.dragSwap': '拖动标题栏与其他窗交换位置',
|
||||
'icons.title': '选择图标',
|
||||
'icons.change': '更换图标',
|
||||
'empty': '暂无项目',
|
||||
} satisfies Record<string, string>
|
||||
|
||||
@@ -136,6 +138,8 @@ export const en = {
|
||||
'pane.subagentsEmpty': 'No subagents in the current session',
|
||||
'split.flip': 'Flip chat side (left ⇄ right)',
|
||||
'split.dragSwap': 'Drag the title bar to swap panes',
|
||||
'icons.title': 'Pick an icon',
|
||||
'icons.change': 'Change icon',
|
||||
'empty': 'No projects yet',
|
||||
} satisfies Record<WorktableKey, string>
|
||||
|
||||
|
||||
@@ -45,6 +45,8 @@ export type LayoutSpec = {
|
||||
topHeight?: { default: number; min: number; max: number }
|
||||
/** 聊天窗贴边位置:'right'(右列/右下,默认)| 'left'(左列/左下) */
|
||||
chatSide?: 'left' | 'right'
|
||||
/** 布局条目图标(emoji;工作台侧栏展示,点击可换) */
|
||||
icon?: string
|
||||
/** 聊天窗通高(整列):为 true 时聊天占整条右/左列,内容区(含 top 行)全部排在其另一侧 */
|
||||
chatFullHeight?: boolean
|
||||
}
|
||||
@@ -729,6 +731,16 @@ export const splitStore: SplitState = {
|
||||
},
|
||||
}
|
||||
|
||||
/** 兼容桥:travelatlas 分栏浮层(.ta_split)出现 = 其引擎打开 → 本引擎让位。
|
||||
* 不改动 travelatlas 代码,仅在 DOM 层观察其浮层挂载。 */
|
||||
if (typeof document !== 'undefined' && document.body) {
|
||||
const taObserver = new MutationObserver(() => {
|
||||
if (!splitStore.active) return
|
||||
if (document.querySelector('.ta_split')) splitStore.close()
|
||||
})
|
||||
taObserver.observe(document.body, { childList: true, subtree: true })
|
||||
}
|
||||
|
||||
/** 分配各窗宽度(最后一个拿余量) */
|
||||
function allocate(panes: SplitPane[], ws: number[], total: number) {
|
||||
const out: { pane: SplitPane; left: number; width: number }[] = []
|
||||
|
||||
@@ -107,6 +107,22 @@ export const css = [
|
||||
'.dsh-wt_layoutDesc{font-size:10px;line-height:14px;color:var(--dsw-alias-label-tertiary,#6e7683);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
|
||||
'.dsh-wt_layoutBadge{flex:none;font-size:9px;line-height:14px;padding:0 5px;border-radius:8px;background:var(--dsw-alias-fill-l1,rgba(255,255,255,.06));color:var(--dsw-alias-label-tertiary,#6e7683)}',
|
||||
'.dsh-wt_layoutArrow{flex:none;color:var(--dsw-alias-label-tertiary,#6e7683);font-size:14px;line-height:20px}',
|
||||
// 项目卡片统一:入驻插件卡片(travelatlas/planreview)与布局卡片同款常显框(静止态可见边框+底)
|
||||
'.dsh-wt_projects .ta_card,.dsh-wt_projects .pr_card{border-color:var(--dsw-alias-border-l1,#262b36);background:var(--dsw-alias-fill-l1,rgba(255,255,255,.02))}',
|
||||
'.dsh-wt_projects .ta_card:hover,.dsh-wt_projects .pr_card:hover{border-color:var(--dsw-alias-state-accent-primary,#4f8ef7);background:var(--dsw-alias-fill-l1,rgba(255,255,255,.05))}',
|
||||
'.dsh-wt_projects .ta_card[data-on=true],.dsh-wt_projects .pr_card[data-on=true]{border-color:var(--dsw-alias-state-accent-primary,#4f8ef7);background:var(--dsw-alias-fill-l1,rgba(255,255,255,.05))}',
|
||||
// 标签精简:项目卡片只留名称一行,小字描述行去掉
|
||||
'.dsh-wt_projects .ta_cardDesc,.dsh-wt_projects .pr_cardDesc{display:none}',
|
||||
// 图标选择弹窗(固定定位,点击项目/布局/快捷方式的图标唤出)
|
||||
'.dsh-wt_iconPop{position:fixed;z-index:81;width:256px;padding:8px;border:1px solid var(--dsw-alias-border-l2,#3a4150);border-radius:10px;background:var(--dsw-alias-bg-base,#0b0e14);box-shadow:var(--dsw-shadow-lv2,0 8px 24px rgba(0,0,0,.4))}',
|
||||
'.dsh-wt_iconPopTitle{font-size:10px;letter-spacing:.05em;color:var(--dsw-alias-label-tertiary,#6e7683);padding:1px 4px 6px}',
|
||||
'.dsh-wt_iconGrid{display:grid;grid-template-columns:repeat(6,1fr);gap:4px}',
|
||||
'.dsh-wt_iconCell{display:flex;align-items:center;justify-content:center;aspect-ratio:1;border:1px solid transparent;border-radius:6px;background:transparent;font-size:17px;line-height:1;cursor:pointer;padding:0}',
|
||||
'.dsh-wt_iconCell:hover{background:var(--dsw-alias-fill-l1,rgba(255,255,255,.05));border-color:var(--dsw-alias-border-l2,#3a4150)}',
|
||||
'.dsh-wt_iconCell[data-on=true]{background:var(--dsw-alias-fill-l1,rgba(255,255,255,.08));border-color:var(--dsw-alias-state-accent-primary,#4f8ef7)}',
|
||||
// 可点图标(布局卡 / 管理行 / 快捷方式上的图标,点击换 emoji)
|
||||
'.dsh-wt_iconPick{flex:none;display:inline-flex;align-items:center;justify-content:center;padding:1px;border:none;background:transparent;font:inherit;cursor:pointer;border-radius:4px}',
|
||||
'.dsh-wt_iconPick:hover{background:var(--dsw-alias-fill-l1,rgba(255,255,255,.08))}',
|
||||
// 窗内容:选择器 / 自定义 / 浏览器 / 占位
|
||||
'.dsh-wt_panePicker{flex:1;min-height:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px;padding:12px;overflow:auto}',
|
||||
'.dsh-wt_panePicker-row{flex-direction:row;flex-wrap:wrap}',
|
||||
|
||||
Reference in New Issue
Block a user