Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f7d9197011 | ||
|
|
5ab81432d1 | ||
|
|
9b9f24b7be | ||
|
|
8dc68fd1e3 | ||
|
|
ddf3135c3f | ||
|
|
b04f4ded95 | ||
|
|
710ef836b5 |
@@ -22,7 +22,7 @@ const confirmation = work('confirm1', 'action_required', { kind: 'confirm', titl
|
||||
})
|
||||
|
||||
async function fixture(page: Page, initialCards: ActivityCard[] = [progress], fontSize = 14,
|
||||
history?: { events: StreamEvent[]; cardRef?: string; savedParts?: Part[] }) {
|
||||
history?: { events: StreamEvent[]; cardRef?: string; savedParts?: Part[] }, theme: 'light' | 'dark' | 'molv' = 'light') {
|
||||
let cards = [...initialCards]
|
||||
let actionCount = 0
|
||||
let releaseAction: (() => void) | undefined
|
||||
@@ -31,9 +31,9 @@ async function fixture(page: Page, initialCards: ActivityCard[] = [progress], fo
|
||||
let uploadNumber = 0
|
||||
const errors: string[] = []
|
||||
page.on('pageerror', error => errors.push(error.message))
|
||||
await page.addInitScript(({ fontSize }) => {
|
||||
await page.addInitScript(({ fontSize, theme }) => {
|
||||
localStorage.setItem('as.conv', 'c1')
|
||||
localStorage.setItem('as.ui', JSON.stringify({ theme: 'light', fontSize, rightCollapsed: true, leftWidth: 260, rightWidth: 360 }))
|
||||
localStorage.setItem('as.ui', JSON.stringify({ theme, fontSize, rightCollapsed: true, leftWidth: 260, rightWidth: 360 }))
|
||||
const streams: { url: string; emit: (event: unknown) => void }[] = []
|
||||
class TestEventSource {
|
||||
onopen: (() => void) | null = null
|
||||
@@ -49,7 +49,7 @@ async function fixture(page: Page, initialCards: ActivityCard[] = [progress], fo
|
||||
close() {}
|
||||
}
|
||||
Object.assign(window, { EventSource: TestEventSource, __workStreams: streams })
|
||||
}, { fontSize })
|
||||
}, { fontSize, theme })
|
||||
await page.route('**/api/**', async route => {
|
||||
const request = route.request()
|
||||
const url = new URL(request.url())
|
||||
@@ -141,6 +141,92 @@ test('places the full work card only in the desktop work pane', async ({ page })
|
||||
expect(state.errors).toEqual([])
|
||||
})
|
||||
|
||||
test('uses a focused reading column and a compact authored-message shape', async ({ page }) => {
|
||||
await page.setViewportSize({ width: 1920, height: 1080 })
|
||||
const state = await fixture(page)
|
||||
await page.getByRole('button', { name: '打开工作', exact: true }).click()
|
||||
|
||||
const conversation = page.locator('.chat-body .msg').first()
|
||||
const authored = page.locator('.chat-body .msg.user').first()
|
||||
const answer = page.locator('.chat-body .msg.assistant').first()
|
||||
const [conversationBox, authoredBox, answerBox] = await Promise.all([
|
||||
conversation.boundingBox(), authored.boundingBox(), answer.boundingBox(),
|
||||
])
|
||||
expect(conversationBox).not.toBeNull()
|
||||
expect(authoredBox).not.toBeNull()
|
||||
expect(answerBox).not.toBeNull()
|
||||
expect(conversationBox!.width).toBeLessThanOrEqual(840)
|
||||
expect(authoredBox!.width).toBeLessThanOrEqual(680)
|
||||
expect(authoredBox!.x + authoredBox!.width).toBeCloseTo(answerBox!.x + answerBox!.width, 0)
|
||||
|
||||
const surfaces = await page.evaluate(() => {
|
||||
const pick = (selector: string) => {
|
||||
const element = document.querySelector(selector)
|
||||
if (!element) throw new Error(`missing ${selector}`)
|
||||
const style = getComputedStyle(element)
|
||||
return {
|
||||
background: style.backgroundColor,
|
||||
color: style.color,
|
||||
borderRadius: Number.parseFloat(style.borderRadius),
|
||||
paddingLeft: Number.parseFloat(style.paddingLeft),
|
||||
}
|
||||
}
|
||||
return {
|
||||
chrome: pick('.companion-topbar'),
|
||||
context: pick('.left-nav'),
|
||||
conversation: pick('.main'),
|
||||
answer: pick('.msg.assistant'),
|
||||
authored: pick('.msg.user .bubble'),
|
||||
}
|
||||
})
|
||||
const rgb = (value: string) => value.match(/[\d.]+/g)?.slice(0, 3).map(Number) ?? [0, 0, 0]
|
||||
const distance = (a: string, b: string) => rgb(a).reduce((sum, value, index) => sum + Math.abs(value - rgb(b)[index]), 0)
|
||||
expect(distance(surfaces.chrome.background, surfaces.conversation.background)).toBeGreaterThan(300)
|
||||
expect(distance(surfaces.context.background, surfaces.conversation.background)).toBeGreaterThan(300)
|
||||
expect(distance(surfaces.authored.background, surfaces.answer.background)).toBeGreaterThan(300)
|
||||
expect(surfaces.answer.borderRadius).toBeGreaterThanOrEqual(12)
|
||||
expect(surfaces.answer.paddingLeft).toBeGreaterThanOrEqual(20)
|
||||
|
||||
const activeCard = page.locator('.right-panel .acard.status-active')
|
||||
await expect(activeCard).toBeVisible()
|
||||
const cardStyle = await activeCard.evaluate(element => {
|
||||
const style = getComputedStyle(element)
|
||||
return { background: style.backgroundColor, borderLeftWidth: style.borderLeftWidth }
|
||||
})
|
||||
const panelBackground = await page.locator('.right-panel').evaluate(element => getComputedStyle(element).backgroundColor)
|
||||
expect(cardStyle.background).not.toBe(panelBackground)
|
||||
expect(Number.parseFloat(cardStyle.borderLeftWidth)).toBeGreaterThanOrEqual(3)
|
||||
expect(state.errors).toEqual([])
|
||||
})
|
||||
|
||||
for (const theme of ['light', 'dark', 'molv'] as const) {
|
||||
test(`keeps the workbench frame legible in ${theme}`, async ({ page }) => {
|
||||
await page.setViewportSize({ width: 1440, height: 960 })
|
||||
const state = await fixture(page, [progress], 14, undefined, theme)
|
||||
await page.getByRole('button', { name: '打开工作', exact: true }).click()
|
||||
const surfaces = await page.evaluate(() => ['.companion-topbar', '.left-nav', '.msg.user .bubble', '.msg.assistant'].map(selector => {
|
||||
const element = document.querySelector(selector)
|
||||
if (!element) throw new Error(`missing ${selector}`)
|
||||
const style = getComputedStyle(element)
|
||||
return { selector, foreground: style.color, background: style.backgroundColor }
|
||||
}))
|
||||
const channels = (value: string) => value.match(/[\d.]+/g)?.slice(0, 3).map(Number) ?? [0, 0, 0]
|
||||
const luminance = (value: string) => {
|
||||
const linear = channels(value).map(channel => {
|
||||
const normalized = channel / 255
|
||||
return normalized <= 0.04045 ? normalized / 12.92 : ((normalized + 0.055) / 1.055) ** 2.4
|
||||
})
|
||||
return 0.2126 * linear[0] + 0.7152 * linear[1] + 0.0722 * linear[2]
|
||||
}
|
||||
for (const surface of surfaces) {
|
||||
const lighter = Math.max(luminance(surface.foreground), luminance(surface.background))
|
||||
const darker = Math.min(luminance(surface.foreground), luminance(surface.background))
|
||||
expect((lighter + 0.05) / (darker + 0.05), `${surface.selector} contrast in ${theme}`).toBeGreaterThanOrEqual(4.5)
|
||||
}
|
||||
expect(state.errors).toEqual([])
|
||||
})
|
||||
}
|
||||
|
||||
test('preserves the selected material, physical scroll and focus when background work finishes', async ({ page }) => {
|
||||
await page.setViewportSize({ width: 1440, height: 960 })
|
||||
const tabbed = work('work1', 'active', { body: [...progress.body!, {
|
||||
|
||||
@@ -3,9 +3,11 @@
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto auto;
|
||||
gap: 12px;
|
||||
padding: 16px 24px;
|
||||
min-height: 64px;
|
||||
padding: 12px 24px;
|
||||
background: var(--bg-panel);
|
||||
}
|
||||
.work-companion-chat .chat-title { font-size: var(--fs-ui-xl); font-weight: 650; letter-spacing: -0.02em; }
|
||||
.work-companion-chat .chat-title { font-size: var(--fs-ui-xl); font-weight: 650; letter-spacing: -0.025em; }
|
||||
.work-companion-chat .chat-title { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.work-companion-chat .chat-header > .mk-col-root { position: relative; }
|
||||
.work-companion-chat .chat-header > .mk-col-root > .mk-col {
|
||||
@@ -37,9 +39,58 @@
|
||||
.companion-setting select { max-width: 100%; padding: 4px 8px; background: var(--bg-panel); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius-sm); font-size: var(--fs-ui-caption); }
|
||||
.companion-setting .session-note { flex: 1 1 100%; min-width: 0; width: 100%; }
|
||||
.companion-setting-note { flex: 1 1 100%; margin: 0; color: var(--text-dim); font-size: var(--fs-ui-sm); line-height: 1.6; }
|
||||
.work-companion-chat .chat-body { padding: 24px clamp(16px, 5vw, 64px); }
|
||||
.work-companion-chat .input-bar { padding: 8px clamp(16px, 5vw, 64px) 16px; }
|
||||
.work-companion-chat .input-row { flex-direction: column; align-items: stretch; padding: 12px; box-shadow: none; }
|
||||
.work-companion-chat .chat-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 36px clamp(20px, 6vw, 96px) 48px;
|
||||
scroll-padding-block: 36px;
|
||||
}
|
||||
.work-companion-chat .chat-body > * {
|
||||
width: min(100%, 840px);
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
.work-companion-chat .chat-body > .msg { margin-bottom: 32px; }
|
||||
.work-companion-chat .chat-body > .msg.user {
|
||||
width: min(80%, 680px);
|
||||
margin-right: max(0px, calc((100% - 840px) / 2));
|
||||
margin-left: auto;
|
||||
}
|
||||
.work-companion-chat .chat-body > .msg.assistant {
|
||||
padding: 20px 24px 16px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-xl);
|
||||
background: var(--bg-raised);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
.work-companion-chat .msg-role {
|
||||
margin-bottom: 10px;
|
||||
color: var(--text-dim);
|
||||
font-size: var(--fs-ui-caption);
|
||||
font-weight: 600;
|
||||
}
|
||||
.work-companion-chat .msg.user .msg-role { justify-content: flex-end; }
|
||||
.work-companion-chat .msg.assistant .role-mark {
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
color: var(--primary-foreground);
|
||||
}
|
||||
.work-companion-chat .msg.assistant .part-text { color: var(--text); font-size: var(--fs-ui-base); line-height: 1.8; }
|
||||
.work-companion-chat .input-bar {
|
||||
padding: 12px clamp(20px, 6vw, 96px) 20px;
|
||||
border-top: 1px solid var(--border-soft);
|
||||
background: var(--bg-panel);
|
||||
}
|
||||
.work-companion-chat .input-bar > * { width: min(100%, 840px); margin-left: auto; margin-right: auto; }
|
||||
.work-companion-chat .input-row {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
padding: 14px;
|
||||
border-color: var(--border-strong);
|
||||
border-radius: var(--radius-xl);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
.work-companion-chat .input-row textarea { box-sizing: border-box; width: 100%; min-width: 0; padding: 4px; line-height: 1.6; font-size: var(--fs-ui-base); }
|
||||
.companion-compose-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; min-width: 0; }
|
||||
.companion-compose-spacer { flex: 1; }
|
||||
@@ -48,7 +99,7 @@
|
||||
.companion-add-material:focus-within { outline: 2px solid var(--accent); outline-offset: 2px; }
|
||||
.companion-file-input { position: absolute; width: 1px; height: 1px; padding: 0; margin: 0; overflow: hidden; clip-path: inset(50%); }
|
||||
.companion-input-hint { font-size: var(--fs-ui-sm); color: var(--text-dim); }
|
||||
.companion-operation-trigger { margin-bottom: 8px; }
|
||||
.companion-operation-trigger { margin-bottom: 8px; color: var(--text-dim); }
|
||||
.companion-operation-body { padding: 12px; margin-bottom: 12px; border: 1px solid var(--border-soft); border-radius: var(--radius); background: var(--bg-panel); }
|
||||
.companion-operation-body .companion-setting { margin-bottom: 8px; }
|
||||
.work-companion-chat .input-status { flex-wrap: wrap; gap: 4px 8px; padding: 8px 0; line-height: 1.5; }
|
||||
@@ -58,31 +109,63 @@
|
||||
.companion-remove-material { display: inline-flex; align-items: center; justify-content: center; padding: 4px; color: var(--text-dim); background: transparent; border: 0; border-radius: var(--radius-xs); cursor: pointer; }
|
||||
.companion-remove-material:hover { background: var(--bg-hover); color: var(--text); }
|
||||
.work-companion-chat .attach-chip { max-width: 100%; overflow-wrap: anywhere; }
|
||||
.companion-empty { min-height: 240px; height: 100%; text-align: center; line-height: 1.7; }
|
||||
.companion-empty-title { color: var(--text); font-size: var(--fs-ui-xl); }
|
||||
.companion-empty { min-height: 320px; height: 100%; text-align: center; line-height: 1.7; }
|
||||
.companion-empty .es-mark { width: 52px; height: 52px; margin-bottom: 8px; background: var(--bg-panel); box-shadow: var(--shadow-md); }
|
||||
.companion-empty-title { color: var(--text); font-size: var(--fs-ui-xl); font-weight: 650; letter-spacing: -0.02em; }
|
||||
.companion-examples { display: grid; gap: 8px; width: min(100%, 32rem); margin-top: 12px; }
|
||||
.companion-example { padding: 12px 16px; background: var(--bg-panel); border: 1px solid var(--border-soft); border-radius: var(--radius); color: var(--text-dim); font: inherit; text-align: left; cursor: pointer; }
|
||||
.companion-example:hover { color: var(--text); background: var(--bg-hover); }
|
||||
.companion-example { padding: 14px 16px; background: var(--bg-panel); border: 1px solid var(--border); border-radius: var(--radius-lg); color: var(--text-dim); font: inherit; text-align: left; cursor: pointer; }
|
||||
.companion-example:hover { color: var(--text); background: var(--bg-raised); border-color: var(--border-strong); }
|
||||
.conversation-work { margin: 24px 0 16px; min-width: 0; }
|
||||
.conversation-work-heading { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 8px; margin-bottom: 12px; color: var(--text-dim); font-size: var(--fs-ui-caption); }
|
||||
.conversation-work-heading > span:first-child { color: var(--text); font-size: var(--fs-ui-lg); font-weight: 650; }
|
||||
.conversation-work-current, .conversation-work-history { display: grid; gap: 12px; min-width: 0; }
|
||||
.conversation-work-more { padding: 12px 0; color: var(--text-dim); font-size: var(--fs-ui-caption); }
|
||||
.conversation-work .acard { margin: 0; }
|
||||
.conversation-action-anchor { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 4px 12px; width: 100%; margin-top: 8px; padding: 8px 12px; border: 1px solid var(--border-soft); border-radius: var(--radius); background: var(--bg-panel); color: var(--text); text-align: left; font-size: var(--fs-ui-caption); cursor: pointer; }
|
||||
.conversation-action-anchor { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 4px 12px; width: 100%; margin-top: 12px; padding: 10px 14px; border: 1px solid var(--border); border-radius: var(--radius); background: var(--bg-panel); color: var(--text); text-align: left; font-size: var(--fs-ui-caption); cursor: pointer; }
|
||||
.conversation-action-anchor > span:last-child { color: var(--accent-text); }
|
||||
.conversation-action-anchor:hover { background: var(--bg-hover); }
|
||||
.work-companion-chat .live-bar { flex-wrap: wrap; }
|
||||
.work-companion-chat .part-error-detail, .work-companion-chat .hitl-detail { overflow-wrap: anywhere; white-space: pre-wrap; }
|
||||
@media (max-width: 540px) {
|
||||
.work-companion-chat .chat-header { padding: 12px 16px; gap: 8px; }
|
||||
.work-companion-chat .chat-body { padding: 24px 16px 32px; }
|
||||
.work-companion-chat .chat-body > .msg.user { width: 92%; margin-right: 0; }
|
||||
.work-companion-chat .input-bar { padding: 10px 12px 14px; }
|
||||
.companion-input-hint { display: none; }
|
||||
.companion-empty { min-height: 200px; }
|
||||
.work-companion-chat .hitl-choices { grid-template-columns: minmax(0, 1fr); }
|
||||
}
|
||||
|
||||
.work-companion-chat .msg.user .bubble { background: var(--bg-active); }
|
||||
.work-companion-chat .msg.user .bubble {
|
||||
padding: 14px 18px;
|
||||
background: var(--primary);
|
||||
border: 1px solid var(--primary);
|
||||
border-radius: var(--radius-lg) var(--radius-lg) var(--radius-xs) var(--radius-lg);
|
||||
color: var(--primary-foreground);
|
||||
line-height: 1.7;
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
.work-companion-chat .process-collapse {
|
||||
margin: 12px 0 16px;
|
||||
border-color: var(--border);
|
||||
background: var(--bg-panel);
|
||||
}
|
||||
.work-companion-chat .process-collapse .process-collapse-summary { padding: 9px 14px; color: var(--text-dim); }
|
||||
.work-companion-chat .process-collapse .process-collapse-summary:hover { background: var(--bg-hover); color: var(--text); }
|
||||
.work-companion-chat .process-collapse .body { padding: 8px 14px 14px; border-top: 1px solid var(--border-soft); }
|
||||
.companion-message-copy { padding: 4px 8px; border: 0; background: transparent; color: var(--text-dim); font-size: var(--fs-ui-caption); border-radius: var(--radius-sm); cursor: pointer; }
|
||||
.companion-message-copy:hover { background: var(--bg-hover); color: var(--text); }
|
||||
.companion-message-actions { display: flex; flex-wrap: wrap; gap: 8px; padding: 8px 0; }
|
||||
.companion-message-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
padding: 8px 0;
|
||||
opacity: 0;
|
||||
}
|
||||
.work-companion-chat .msg:hover .companion-message-actions,
|
||||
.work-companion-chat .msg:focus-within .companion-message-actions { opacity: 1; }
|
||||
.work-companion-chat .msg-tools { gap: 4px; }
|
||||
|
||||
@media (hover: none) {
|
||||
.companion-message-actions { opacity: 1; }
|
||||
}
|
||||
|
||||
+115
-28
@@ -9,6 +9,20 @@
|
||||
.companion-app .left-nav,
|
||||
.companion-app .main,
|
||||
.companion-app .right-panel { min-height: 0; max-width: 100%; overflow: hidden; }
|
||||
.companion-app .left-nav,
|
||||
.companion-app .companion-topbar {
|
||||
--text: var(--workbench-chrome-text);
|
||||
--text-dim: var(--workbench-chrome-dim);
|
||||
--text-faint: var(--workbench-chrome-faint);
|
||||
--bg-hover: var(--workbench-chrome-hover);
|
||||
--bg-active: var(--workbench-chrome-active);
|
||||
--border: var(--workbench-chrome-active);
|
||||
--border-soft: var(--workbench-chrome-hover);
|
||||
--border-strong: var(--workbench-chrome-dim);
|
||||
background: var(--workbench-chrome);
|
||||
color: var(--workbench-chrome-text);
|
||||
border-color: var(--workbench-chrome-hover);
|
||||
}
|
||||
.companion-app .left-nav { transition: none; }
|
||||
.companion-app .left-nav[hidden],
|
||||
.companion-app .right-panel[hidden],
|
||||
@@ -16,20 +30,58 @@
|
||||
.companion-app .icon-only[hidden] { display: none !important; }
|
||||
|
||||
.shell-context-content { display: flex; flex-direction: column; flex: 1; min-height: 0; min-width: 0; }
|
||||
.companion-app .companion-topbar { height: auto; min-height: 44px; gap: 8px; padding: 4px 12px; }
|
||||
.companion-app .companion-topbar {
|
||||
height: auto;
|
||||
min-height: 52px;
|
||||
gap: 12px;
|
||||
padding: 8px 16px;
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
.topbar-identity { display: flex; align-items: center; gap: 8px; min-width: 0; }
|
||||
.companion-topbar .topbar-name { font-size: var(--fs-ui-base); font-weight: 650; white-space: nowrap; }
|
||||
.companion-app .right-panel .tabs { padding: 4px 12px 0; }
|
||||
.companion-app .right-panel .tab { font-size: var(--fs-ui-base); font-weight: 600; }
|
||||
.companion-app .right-panel .tab-body { padding: 16px; }
|
||||
.companion-app .right-panel .inbox-zone-label { color: var(--text); font-size: var(--fs-ui-caption); font-weight: 600; margin-top: 16px; }
|
||||
.companion-app .right-panel .acard.status-active { border-left: 3px solid var(--work-focus-border); background: var(--work-focus-surface); }
|
||||
.companion-app .right-panel .acard-title { color: var(--text); font-size: var(--fs-ui-lg); }
|
||||
.companion-topbar .topbar-name { font-size: var(--fs-ui-lg); font-weight: 650; letter-spacing: -0.02em; white-space: nowrap; }
|
||||
.companion-app .right-panel .tabs {
|
||||
min-height: 52px;
|
||||
align-items: flex-end;
|
||||
gap: 16px;
|
||||
padding: 0 20px;
|
||||
background: var(--bg-panel);
|
||||
}
|
||||
.companion-app .right-panel .tab {
|
||||
min-height: 52px;
|
||||
padding: 0 2px;
|
||||
color: var(--text-dim);
|
||||
font-size: var(--fs-ui-base);
|
||||
font-weight: 600;
|
||||
}
|
||||
.companion-app .right-panel .tab:hover { color: var(--text); }
|
||||
.companion-app .right-panel .tab-body { padding: 20px; }
|
||||
.companion-app .right-panel .inbox-zone-label { margin: 20px 0 8px; color: var(--text); font-size: var(--fs-ui-caption); font-weight: 650; }
|
||||
.companion-app .right-panel .acard { margin: 12px 0; border-color: var(--border); padding: 18px; }
|
||||
.companion-app .right-panel .acard.status-active {
|
||||
border-left: 3px solid var(--work-focus-border);
|
||||
background: var(--work-focus-surface);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
.companion-app .right-panel .acard-title { color: var(--text); font-size: var(--fs-ui-lg); line-height: 1.4; }
|
||||
.companion-app .right-panel .acard-status {
|
||||
display: inline-flex;
|
||||
width: fit-content;
|
||||
margin-top: 8px;
|
||||
padding: 2px 8px;
|
||||
border-radius: var(--radius-full);
|
||||
background: var(--bg-active);
|
||||
color: var(--text);
|
||||
font-weight: 600;
|
||||
}
|
||||
.companion-app .right-panel .acard-body { margin-top: 16px; }
|
||||
.companion-app .right-panel .acard-summary-row { grid-template-columns: minmax(64px, auto) minmax(0, 1fr); gap: 12px; margin: 12px 0; }
|
||||
.companion-app .right-panel .acard-summary-row > span,
|
||||
.companion-app .right-panel .acard-summary-label { color: var(--text-dim); font-weight: 600; }
|
||||
.companion-app .right-panel .acard-summary-row p,
|
||||
.companion-app .right-panel .acard-findings li { color: var(--text); font-size: var(--fs-ui-base); }
|
||||
.companion-topbar .brand-mark {
|
||||
background: var(--primary-sidebar);
|
||||
color: var(--primary-sidebar-foreground);
|
||||
background: var(--workbench-chrome-text);
|
||||
color: var(--workbench-chrome);
|
||||
box-shadow: none;
|
||||
border-radius: var(--radius-sm) !important;
|
||||
}
|
||||
@@ -45,39 +97,52 @@
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
background: none;
|
||||
color: var(--text-sidebar-dim);
|
||||
color: var(--workbench-chrome-dim);
|
||||
font-size: var(--fs-ui-caption);
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.shell-nav-button:hover,
|
||||
.shell-return:hover { background: var(--bg-hover); color: var(--text-sidebar); }
|
||||
.shell-nav-button[aria-pressed='true'] { background: var(--bg-active); color: var(--text-sidebar); }
|
||||
.shell-return:hover { background: var(--workbench-chrome-hover); color: var(--workbench-chrome-text); }
|
||||
.shell-nav-button[aria-pressed='true'] { background: var(--workbench-chrome-active); color: var(--workbench-chrome-text); }
|
||||
.shell-nav-button svg,
|
||||
.shell-return svg { flex: none; }
|
||||
.shell-back-icon { transform: rotate(180deg); }
|
||||
.shell-pending {
|
||||
padding: 0 4px;
|
||||
border-radius: var(--radius-xs);
|
||||
background: var(--bg-active);
|
||||
color: var(--text-sidebar);
|
||||
background: var(--workbench-chrome-active);
|
||||
color: var(--workbench-chrome-text);
|
||||
font-size: var(--fs-ui-xs);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.companion-app .tsb-brand { gap: 4px; padding: 16px 12px 8px; }
|
||||
.companion-app .tsb-projrow { min-height: 36px; height: auto; border-color: transparent; background: none; padding: 4px 8px; }
|
||||
.companion-app .tsb-brand { gap: 8px; padding: 16px 14px 12px; border-bottom: 1px solid var(--border-soft); }
|
||||
.companion-app .tsb-projrow { min-height: 40px; height: auto; border-color: transparent; background: none; padding: 6px 10px; }
|
||||
.companion-app .tsb-projrow:hover { background: var(--bg-hover); }
|
||||
.companion-app .tsb-projrow-name { flex: 1; }
|
||||
.companion-app .tsb-projrow-name { flex: 1; font-size: var(--fs-ui-base); font-weight: 650; }
|
||||
.companion-app .tsb-projrow-stage { max-width: 40%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text-dim); border-radius: var(--radius-sm); }
|
||||
.companion-app .tsb-iconbtn { width: 32px; height: 32px; flex: none; }
|
||||
.companion-app .tsb-actions { padding: 8px 12px 12px; gap: 12px; }
|
||||
.companion-app .tsb-newtask { min-height: 40px; height: auto; box-shadow: none; transition: none; }
|
||||
.companion-app .tsb-actions { padding: 12px 14px; gap: 10px; }
|
||||
.companion-app .tsb-newtask {
|
||||
min-height: 40px;
|
||||
height: auto;
|
||||
background: var(--workbench-chrome-text);
|
||||
color: var(--workbench-chrome);
|
||||
box-shadow: none;
|
||||
transition: none;
|
||||
}
|
||||
.companion-app .tsb-newtask:hover { background: var(--workbench-chrome-text); }
|
||||
.companion-app .tsb-newtask-main { justify-content: center; padding: 8px 12px; }
|
||||
.companion-app .tsb-newtask-caret { width: 36px; flex: none; }
|
||||
.companion-app .tsb-search { min-height: 36px; height: auto; padding: 8px 12px; }
|
||||
.companion-app .tsb-orgrow { padding-bottom: 4px; }
|
||||
.companion-app .tsb-task { min-height: 40px; height: auto; gap: 4px; transition: none; }
|
||||
.companion-app .tsb-newtask-caret { width: 36px; flex: none; border-left-color: var(--workbench-chrome-inverse-border); }
|
||||
.companion-app .tsb-newtask-caret:hover { background: var(--workbench-chrome-inverse-hover); }
|
||||
.companion-app .tsb-search { min-height: 38px; height: auto; padding: 8px 12px; background: var(--workbench-chrome-hover); border-color: var(--workbench-chrome-hover); }
|
||||
.companion-app .tsb-search:focus-within { background: var(--workbench-chrome-active); border-color: var(--workbench-chrome-dim); }
|
||||
.companion-app .tsb-orgrow { padding: 4px 16px 8px; }
|
||||
.companion-app .tsb-seclabel { color: var(--text-dim); font-size: var(--fs-ui-sm); font-weight: 600; }
|
||||
.companion-app .tsb-scroll { padding: 0 8px 12px; }
|
||||
.companion-app .tsb-task { min-height: 42px; height: auto; gap: 6px; padding: 0 10px; transition: none; }
|
||||
.companion-app .tsb-task.active { background: var(--bg-active); color: var(--text); }
|
||||
.companion-app .tsb-task-open {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -93,6 +158,7 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
.companion-app .tsb-task-open > svg { flex: none; }
|
||||
.companion-app .tsb-task-t { font-size: var(--fs-ui-base); }
|
||||
.companion-app .tsb-task-tm { color: var(--text-dim); }
|
||||
.companion-app .tsb-task:focus-within .tsb-row-acts { display: inline-flex; }
|
||||
.companion-app .tsb-task:focus-within .tsb-task-tm { display: none; }
|
||||
@@ -111,11 +177,32 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
.companion-app .tsb-row-menu:hover { background: var(--bg-hover); color: var(--text); }
|
||||
.companion-app .tsb-bottom { padding: 8px 12px; gap: 4px; }
|
||||
.companion-app .tsb-tool { min-height: 36px; }
|
||||
.companion-app .tsb-bottom { padding: 10px 12px; gap: 4px; background: var(--workbench-chrome-hover); }
|
||||
.companion-app .tsb-tool { min-height: 38px; padding: 7px 10px; }
|
||||
.companion-app .tsb-avatar { background: var(--bg-active); color: var(--text); box-shadow: none; border-radius: var(--radius-full); }
|
||||
.companion-app .tsb-user-sub { color: var(--text-dim); }
|
||||
.companion-app .tsb-user { min-height: 44px; height: auto; }
|
||||
.companion-app .tsb-footer { padding: 10px 12px; }
|
||||
.companion-app .tsb-user { min-height: 46px; height: auto; }
|
||||
.companion-app .right-panel .inbox-scope {
|
||||
gap: 4px;
|
||||
margin: 14px 20px 0;
|
||||
padding: 4px;
|
||||
border: 1px solid var(--border-soft);
|
||||
border-radius: var(--radius);
|
||||
background: var(--bg);
|
||||
}
|
||||
.companion-app .right-panel .inbox-scope button {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 5px 10px;
|
||||
opacity: 1;
|
||||
color: var(--text-dim);
|
||||
font-size: var(--fs-ui-sm);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.companion-app .right-panel .inbox-scope button.active { background: var(--bg-raised); box-shadow: var(--shadow-md); color: var(--text); }
|
||||
.companion-app .tsb-menu {
|
||||
min-width: min(216px, calc(100vw - 16px));
|
||||
max-width: min(360px, calc(100vw - 16px));
|
||||
@@ -124,7 +211,7 @@
|
||||
}
|
||||
.companion-app .tsb-mi { overflow-wrap: anywhere; }
|
||||
|
||||
.companion-app.compact .companion-topbar { flex-wrap: wrap; padding: 8px; }
|
||||
.companion-app.compact .companion-topbar { flex-wrap: wrap; padding: 8px 12px; }
|
||||
.companion-app.compact .shell-navigation { width: 100%; display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
||||
.companion-app.compact .shell-nav-button { min-width: 0; min-height: 40px; flex-wrap: wrap; gap: 0 4px; padding: 4px; }
|
||||
.companion-app.compact .shell-nav-button > svg { display: none; }
|
||||
|
||||
@@ -278,6 +278,18 @@ interface DutyCatalogDoc {
|
||||
assigned: boolean; author?: string; reviewers?: string[]; approver?: string; activeRound?: number
|
||||
}
|
||||
|
||||
interface DocsTemplateRow {
|
||||
templateId: string; version: string; title: string; docTypes: string[]; stage?: string
|
||||
}
|
||||
|
||||
/** rdm.docs.instantiate 参数:docType 取首个适用类型;空标题交由后端取模板标题。 */
|
||||
export function docsInstantiateParams(tpl: DocsTemplateRow, title: string): Record<string, unknown> {
|
||||
return {
|
||||
docType: tpl.docTypes[0], templateId: tpl.templateId, version: tpl.version,
|
||||
...(title.trim() ? { title: title.trim() } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
interface DutyGetOut {
|
||||
docId: string; title: string; assigned: boolean
|
||||
roles: { author: string; reviewers: string[]; approver: string }
|
||||
@@ -320,6 +332,9 @@ export function RdmDutiesSection({ project }: { project: Project }) {
|
||||
const [err, setErr] = useState('')
|
||||
const [notice, setNotice] = useState('')
|
||||
const [busy, setBusy] = useState(false)
|
||||
const [tpls, setTpls] = useState<DocsTemplateRow[] | null>(null)
|
||||
const [tplSel, setTplSel] = useState('')
|
||||
const [newTitle, setNewTitle] = useState('')
|
||||
|
||||
const cmd = <T,>(id: string, params: Record<string, unknown>) => api.rdmProjectCommand<T>(project, id, params)
|
||||
const memberPage = useMemberPickerPage(members)
|
||||
@@ -388,6 +403,9 @@ export function RdmDutiesSection({ project }: { project: Project }) {
|
||||
cmd<{ members: { owner: string; role: string }[] }>('rdm.project.member.list', { projectId })
|
||||
.then(out => { if (currentProject(epoch)) setMembers(out.members ?? []) })
|
||||
.catch(() => { if (currentProject(epoch)) setMembers([]) })
|
||||
api.rdmCommand<DocsTemplateRow[]>('rdm.docs.templates')
|
||||
.then(rows => { if (currentProject(epoch)) setTpls(rows ?? []) })
|
||||
.catch(() => { if (currentProject(epoch)) setTpls([]) })
|
||||
return () => {
|
||||
requests.current.epoch++; requests.current.selection++; requests.current.status++
|
||||
requests.current.catalog++; requests.current.action++; requests.current.selected = ''
|
||||
@@ -431,8 +449,39 @@ export function RdmDutiesSection({ project }: { project: Project }) {
|
||||
cmd('rdm.review.assign.doc.batch', { projectId, docs: [...batchSel].map(d => ({
|
||||
docId: d, author: batchAuthor[0], reviewers: batchReviewers, approver: batchApprover[0] })) }))
|
||||
|
||||
/** U4/UI:按已发布 DHF 模板整份生成文档(章节骨架自动播种),随后选中新文档。 */
|
||||
const instantiate = () => {
|
||||
const tpl = (tpls ?? []).find(r => `${r.templateId}@${r.version}` === tplSel)
|
||||
if (!tpl) return
|
||||
const title = newTitle.trim()
|
||||
return run(`已按模板生成「${title || tpl.title}」`, async () => {
|
||||
const res = await cmd<{ docId: string }>('rdm.docs.instantiate', docsInstantiateParams(tpl, title))
|
||||
await refreshCatalog()
|
||||
await selectDoc(res.docId, true)
|
||||
})
|
||||
}
|
||||
const tpl = (tpls ?? []).find(r => `${r.templateId}@${r.version}` === tplSel)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h4 className="settings-group-title">按模板生成文档</h4>
|
||||
<div className="ad-create">
|
||||
<div className="ad-row">
|
||||
<select className="lg-input" aria-label="文档模板" value={tplSel}
|
||||
onChange={e => { setTplSel(e.target.value); setNewTitle('') }}>
|
||||
<option value="">{tpls === null ? '模板加载中…' : '选择模板…'}</option>
|
||||
{(tpls ?? []).map(r => (
|
||||
<option key={`${r.templateId}@${r.version}`} value={`${r.templateId}@${r.version}`}>
|
||||
{r.title}({r.templateId}@{r.version})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<input className="lg-input" aria-label="文档标题" placeholder={`文档标题(默认:${tpl?.title ?? '模板标题'})`}
|
||||
value={newTitle} onChange={e => setNewTitle(e.target.value)} />
|
||||
<button className="lg-submit ad-create-btn" disabled={busy || !tpl} onClick={instantiate}>生成文档</button>
|
||||
</div>
|
||||
<p className="ad-hint">按已发布 DHF 模板整份生成:章节骨架自动播种;生成后在下方清单选中文档分配三权并走整文审签。</p>
|
||||
</div>
|
||||
<h4 className="settings-group-title">文档清单(职责分配对象为整份文档;搜索/浏览同一目录)</h4>
|
||||
<table className="ad-table">
|
||||
<thead><tr><th></th><th>文档</th><th>类型 / 阶段</th><th>当前职责</th><th>状态</th><th></th></tr></thead>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// 设计系统守护测试(父仓 DESIGN.md §11 禁止清单 · 宿主侧,2026-09-30)。
|
||||
// 棘轮基线:现存豁免登记在下方;新增同类违规直接失败。收紧 = 修掉违规后删基线条目。
|
||||
// 范围:src/styles.css 与 src/**/*.tsx。@ui 组件库侧由 Agent-System-UI 的守护测试覆盖。
|
||||
// 范围:src/**/*.css 与 src/**/*.tsx。@ui 组件库侧由 Agent-System-UI 的守护测试覆盖。
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs'
|
||||
import { join, relative, dirname } from 'node:path'
|
||||
@@ -36,8 +36,14 @@ describe('DESIGN.md 宪法在位', () => {
|
||||
|
||||
// ---- 2. 动效唯一所有者 ----
|
||||
describe('动效唯一所有者', () => {
|
||||
it('styles.css 无 @keyframes(全部走 @ui/motion.css 的 mk-* 类)', () => {
|
||||
expect(stripCssComments(read(join(SRC, 'styles.css')))).not.toContain('@keyframes')
|
||||
it('宿主 CSS 无 @keyframes 或私有 transition(全部走 @ui/motion.css 的 mk-* 类)', () => {
|
||||
for (const path of walk(SRC).filter(p => p.endsWith('.css'))) {
|
||||
const css = stripCssComments(read(path))
|
||||
expect(css, `${relative(SRC, path)} 不得定义 @keyframes`).not.toContain('@keyframes')
|
||||
const transitions = css.split('\n').filter(line => /transition\s*:/.test(line) && !/transition\s*:\s*none\b/.test(line))
|
||||
if (path.endsWith('styles.css')) continue // 历史 transition 由现有棘轮逐步收紧;新分层 CSS 不再扩散。
|
||||
expect(transitions, `${relative(SRC, path)} 动效必须消费 mk-*`).toEqual([])
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -53,6 +59,16 @@ describe('裸色值棘轮', () => {
|
||||
const extra = found.filter(h => !allow.has(h))
|
||||
expect(extra, `新增裸色值 ${[...new Set(extra)].join(', ')}——改用 tokens 令牌;确需豁免更新 HEX_BASELINE_STYLES 并写明理由`).toEqual([])
|
||||
})
|
||||
it('分层组件 CSS 只消费令牌,不派生私有颜色', () => {
|
||||
const componentCss = walk(SRC).filter(p => p.endsWith('.css') && !p.endsWith('styles.css'))
|
||||
const extra: string[] = []
|
||||
for (const path of componentCss) {
|
||||
const css = stripCssComments(read(path))
|
||||
for (const hex of collectHex(css)) extra.push(`${relative(SRC, path)}: ${hex}`)
|
||||
if (/color-mix\s*\(/.test(css)) extra.push(`${relative(SRC, path)}: color-mix`)
|
||||
}
|
||||
expect(extra, '分层组件 CSS 的颜色须由 @ui/tokens.css 提供').toEqual([])
|
||||
})
|
||||
it('组件 TSX 无裸色值', () => {
|
||||
const extra: string[] = []
|
||||
for (const p of walk(SRC).filter(p => /\.(tsx|ts)$/.test(p) && !p.endsWith('.d.ts'))) {
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { docsInstantiateParams } from '../src/components/adminRdmSections'
|
||||
|
||||
const tpl = {
|
||||
templateId: 'dhf.2750fbaf1f67d10c', version: '1.0.0', title: '项目建议书',
|
||||
docTypes: ['rd_proposal'], stage: '0. 商务阶段',
|
||||
}
|
||||
|
||||
describe('docsInstantiateParams', () => {
|
||||
it('carries docType/templateId/version and trims the title', () => {
|
||||
expect(docsInstantiateParams(tpl, ' U5 试点项目建议书 ')).toEqual({
|
||||
docType: 'rd_proposal', templateId: 'dhf.2750fbaf1f67d10c',
|
||||
version: '1.0.0', title: 'U5 试点项目建议书',
|
||||
})
|
||||
})
|
||||
it('omits blank titles so the backend falls back to the template title', () => {
|
||||
const p = docsInstantiateParams(tpl, ' ')
|
||||
expect(Object.hasOwn(p, 'title')).toBe(false)
|
||||
expect(p).toEqual({ docType: 'rd_proposal', templateId: 'dhf.2750fbaf1f67d10c', version: '1.0.0' })
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user