Commit 6acc6e51 authored by edy's avatar edy

feat(ui): add artifact menu for task panel

parent 0a63c6b5
import { useEffect, useMemo, useState } from "react" import { useEffect, useMemo, useRef, useState } from "react"
import type { TaskPanelItem, TaskPanelStatus } from "@qjclaw/shared-types" import type { TaskPanelArtifact, TaskPanelItem, TaskPanelStatus } from "@qjclaw/shared-types"
import { renderExpertIcon } from "../../components/icons/AppIcons"
import { Panel } from "../../components/ui/Panel" import { Panel } from "../../components/ui/Panel"
import { ScrollArea } from "../../components/ui/ScrollArea" import { ScrollArea } from "../../components/ui/ScrollArea"
import { StatusChip, type StatusChipTone } from "../../components/ui/StatusChip" import type { ExpertVisualKey } from "../shell/ExpertTree"
import { getDefaultTaskPanelDate, loadTaskPanelItems } from "./taskPanelData" import { getDefaultTaskPanelDate, loadTaskPanelItems } from "./taskPanelData"
const statusLabels: Record<TaskPanelStatus, string> = { const statusLabels: Record<TaskPanelStatus, string> = {
...@@ -12,47 +13,218 @@ const statusLabels: Record<TaskPanelStatus, string> = { ...@@ -12,47 +13,218 @@ const statusLabels: Record<TaskPanelStatus, string> = {
failed: "失败" failed: "失败"
} }
const statusTones: Record<TaskPanelStatus, StatusChipTone> = { function resolveTaskExpertIconKey(expertName: string): ExpertVisualKey {
pending: "warning", const seed = expertName.toLowerCase()
running: "info", if (/xiaohongshu|xhs|rednote|小红书/.test(seed)) {
completed: "positive", return "xiaohongshu"
failed: "warning" }
if (/douyin|抖音/.test(seed)) {
return "douyin"
}
if (/tiktok/.test(seed)) {
return "tiktok"
}
if (/wechat|weixin|公众号|微信/.test(seed)) {
return "wechat"
}
if (/zhihu|知乎/.test(seed)) {
return "zhihu"
}
if (/content-account|planner|账号规划|内容账号规划/.test(seed)) {
return "planner"
}
if (/precision-leads|线索|lead/.test(seed)) {
return "leads"
}
if (/sales-champion|销售冠军|销冠|sales champion/.test(seed)) {
return "sales"
}
if (/poster|海报/.test(seed)) {
return "poster"
}
if (/(^|[\s-])x($|[\s-])|twitter/.test(seed)) {
return "x"
}
if (/geo/.test(seed)) {
return "geo"
}
if (/browser|automation|chrome|playwright|web|浏览器|自动化/.test(seed)) {
return "browser"
}
return "general"
} }
function TaskStatus({ item }: { item: TaskPanelItem }) { function TaskStatus({ item }: { item: TaskPanelItem }) {
return ( return (
<div className="task-panel-status"> <div className={"task-panel-status task-panel-status-" + item.status}>
<StatusChip tone={statusTones[item.status]}> <span className="task-panel-status-icon" title={item.statusDetail} aria-hidden="true">
<span className={"task-panel-status-dot task-panel-status-dot-" + item.status} aria-hidden="true" /> {item.status === "completed" ? <span></span> : null}
{statusLabels[item.status]} {item.status === "running" ? (
</StatusChip> <span className="task-panel-running-dots" aria-hidden="true">
<span>{item.statusDetail}</span> <span />
<span />
<span />
</span>
) : null}
{item.status === "pending" ? <span></span> : null}
{item.status === "failed" ? <span>!</span> : null}
</span>
<strong className="task-panel-status-text" title={item.statusDetail}>{statusLabels[item.status]}</strong>
</div> </div>
) )
} }
function TaskArtifactList({
artifacts,
copiedArtifactId,
onCopy
}: {
artifacts: TaskPanelArtifact[]
copiedArtifactId: string
onCopy: (artifactId: string, artifactUrl: string) => void
}) {
return (
<ul className="task-panel-artifact-list" aria-label="产物清单">
{artifacts.map((artifact) => (
<li
key={artifact.id}
className={copiedArtifactId === artifact.id ? "task-panel-artifact-item-copied" : undefined}
title={artifact.url ?? artifact.name}
>
<span className="task-panel-artifact-name" title={artifact.name}>{artifact.name}</span>
{artifact.url ? (
<button
type="button"
className="task-panel-artifact-url"
title={artifact.url}
onClick={() => onCopy(artifact.id, artifact.url ?? "")}
>
{artifact.url}
</button>
) : null}
{copiedArtifactId === artifact.id ? (
<span className="task-panel-artifact-copied" aria-live="polite">已复制</span>
) : null}
</li>
))}
</ul>
)
}
function TaskArtifacts({ item }: { item: TaskPanelItem }) { function TaskArtifacts({ item }: { item: TaskPanelItem }) {
const [copiedArtifactId, setCopiedArtifactId] = useState("")
const [isMenuOpen, setIsMenuOpen] = useState(false)
const menuRef = useRef<HTMLDivElement | null>(null)
const copiedTimerRef = useRef<number | null>(null)
useEffect(() => {
return () => {
if (copiedTimerRef.current !== null) {
window.clearTimeout(copiedTimerRef.current)
}
}
}, [])
useEffect(() => {
setIsMenuOpen(false)
setCopiedArtifactId("")
}, [item.id])
useEffect(() => {
if (!isMenuOpen) {
return
}
const handlePointerDown = (event: PointerEvent) => {
const menuElement = menuRef.current
if (menuElement && event.target instanceof Node && !menuElement.contains(event.target)) {
setIsMenuOpen(false)
}
}
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") {
setIsMenuOpen(false)
}
}
document.addEventListener("pointerdown", handlePointerDown, true)
document.addEventListener("keydown", handleKeyDown)
return () => {
document.removeEventListener("pointerdown", handlePointerDown, true)
document.removeEventListener("keydown", handleKeyDown)
}
}, [isMenuOpen])
const copyArtifactUrl = async (artifactId: string, artifactUrl: string) => {
try {
await navigator.clipboard.writeText(artifactUrl)
setCopiedArtifactId(artifactId)
if (copiedTimerRef.current !== null) {
window.clearTimeout(copiedTimerRef.current)
}
copiedTimerRef.current = window.setTimeout(() => {
setCopiedArtifactId("")
copiedTimerRef.current = null
}, 1400)
} catch {
// Copy feedback is best-effort and should not affect task data.
}
}
if (!item.artifacts.length) { if (!item.artifacts.length) {
return <span className="task-panel-muted">暂无产物</span> return (
<div className="task-panel-artifact-center">
<span className="task-panel-muted">暂无产物</span>
</div>
)
}
if (item.artifacts.length <= 2) {
return (
<div className="task-panel-artifact-center">
<TaskArtifactList
artifacts={item.artifacts}
copiedArtifactId={copiedArtifactId}
onCopy={(artifactId, artifactUrl) => void copyArtifactUrl(artifactId, artifactUrl)}
/>
</div>
)
} }
return ( return (
<ul className="task-panel-artifact-list" aria-label={item.taskTitle + "产物清单"}> <div className="task-panel-artifact-menu" ref={menuRef}>
{item.artifacts.map((artifact) => ( <button
<li key={artifact.id}> type="button"
<span className="task-panel-artifact-name">{artifact.name}</span> className="task-panel-artifact-trigger"
{artifact.kind ? <span className="task-panel-artifact-kind">{artifact.kind}</span> : null} aria-haspopup="menu"
</li> aria-expanded={isMenuOpen}
))} aria-controls={item.id + "-artifact-menu"}
</ul> onClick={() => setIsMenuOpen((current) => !current)}
>
{item.artifacts.length} 个产物
</button>
{isMenuOpen ? (
<div className="task-panel-artifact-popover" id={item.id + "-artifact-menu"} role="menu">
<TaskArtifactList
artifacts={item.artifacts}
copiedArtifactId={copiedArtifactId}
onCopy={(artifactId, artifactUrl) => void copyArtifactUrl(artifactId, artifactUrl)}
/>
</div>
) : null}
</div>
) )
} }
export function TaskPanelView() { export function TaskPanelView() {
const [selectedDate, setSelectedDate] = useState(getDefaultTaskPanelDate) const [selectedDate, setSelectedDate] = useState(getDefaultTaskPanelDate)
const [items, setItems] = useState<TaskPanelItem[]>([]) const [items, setItems] = useState<TaskPanelItem[]>([])
const [selectedTaskIds, setSelectedTaskIds] = useState<Record<string, string>>({})
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [errorText, setErrorText] = useState("") const [errorText, setErrorText] = useState("")
const dateInputRef = useRef<HTMLInputElement | null>(null)
useEffect(() => { useEffect(() => {
let active = true let active = true
...@@ -82,24 +254,64 @@ export function TaskPanelView() { ...@@ -82,24 +254,64 @@ export function TaskPanelView() {
} }
}, [selectedDate]) }, [selectedDate])
const completedCount = useMemo(() => items.filter((item) => item.status === "completed").length, [items]) const displayDate = useMemo(() => selectedDate.replaceAll("-", "/"), [selectedDate])
const displayMonth = useMemo(() => `${Number(selectedDate.slice(5, 7))}月`, [selectedDate])
const expertRows = useMemo(() => {
const groups = new Map<string, TaskPanelItem[]>()
for (const item of items) {
const groupItems = groups.get(item.expertName)
if (groupItems) {
groupItems.push(item)
} else {
groups.set(item.expertName, [item])
}
}
return Array.from(groups, ([expertName, tasks]) => {
const selectedTask = tasks.find((task) => task.id === selectedTaskIds[expertName]) ?? tasks[0]
return { expertName, tasks, selectedTask }
})
}, [items, selectedTaskIds])
return ( return (
<div className="page-stack task-panel-page-stack"> <div className="page-stack task-panel-page-stack">
<Panel className="task-panel-page" bodyClassName="task-panel-body"> <Panel className="task-panel-page" bodyClassName="task-panel-body">
<div className="task-panel-header"> <div className="task-panel-header">
<div className="task-panel-title-group"> <div className="task-panel-title-group">
<h1>任务面板</h1> <h1><span>任务面板</span></h1>
<p>{items.length ? `${items.length} 个任务,${completedCount} 个已完成` : "按日期查看专家任务与产物"}</p>
</div> </div>
<label className="task-panel-date-field"> <div className="task-panel-date-field">
<span>日期</span> <button
type="button"
className="task-panel-date-pill"
aria-label={"选择日期,当前为 " + displayDate}
onClick={() => {
const dateInput = dateInputRef.current
if (!dateInput) {
return
}
if (typeof dateInput.showPicker === "function") {
dateInput.showPicker()
} else {
dateInput.click()
}
}}
>
<span className="task-panel-date-calendar" aria-hidden="true">
<span className="task-panel-date-calendar-month">{displayMonth}</span>
<span className="task-panel-date-calendar-day">{selectedDate.slice(-2)}</span>
</span>
<span className="task-panel-date-value">{displayDate}</span>
</button>
<input <input
ref={dateInputRef}
type="date" type="date"
aria-hidden="true"
tabIndex={-1}
value={selectedDate} value={selectedDate}
onChange={(event) => setSelectedDate(event.currentTarget.value)} onChange={(event) => setSelectedDate(event.currentTarget.value)}
/> />
</label> </div>
</div> </div>
<ScrollArea className="scroll-panel task-panel-scroll" aria-busy={loading}> <ScrollArea className="scroll-panel task-panel-scroll" aria-busy={loading}>
...@@ -109,21 +321,42 @@ export function TaskPanelView() { ...@@ -109,21 +321,42 @@ export function TaskPanelView() {
{!loading && !errorText && items.length ? ( {!loading && !errorText && items.length ? (
<div className="task-panel-table" role="table" aria-label="任务面板"> <div className="task-panel-table" role="table" aria-label="任务面板">
<div className="task-panel-row task-panel-row-head" role="row"> <div className="task-panel-row task-panel-row-head" role="row">
<div role="columnheader">专家</div> <div role="columnheader">数字员工</div>
<div role="columnheader">任务列表</div>
<div role="columnheader">执行状态</div> <div role="columnheader">执行状态</div>
<div role="columnheader">产物清单</div> <div role="columnheader">产物清单</div>
</div> </div>
{items.map((item) => ( {expertRows.map((row) => (
<article key={item.id} className="task-panel-row" role="row"> <article key={row.expertName} className="task-panel-row" role="row">
<div className="task-panel-expert-cell" role="cell"> <div className="task-panel-expert-cell" role="cell">
<strong>{item.expertName}</strong> <span className={"task-panel-expert-icon task-panel-expert-icon-" + resolveTaskExpertIconKey(row.expertName)} aria-hidden="true">
<span>{item.taskTitle}</span> {renderExpertIcon(resolveTaskExpertIconKey(row.expertName))}
</span>
<strong title={row.expertName}>{row.expertName}</strong>
</div>
<div className="task-panel-task-cell" role="cell">
<select
aria-label={row.expertName + "任务列表"}
title={row.selectedTask.taskTitle}
value={row.selectedTask.id}
onChange={(event) => {
const selectedTaskId = event.currentTarget.value
setSelectedTaskIds((current) => ({
...current,
[row.expertName]: selectedTaskId
}))
}}
>
{row.tasks.map((task) => (
<option key={task.id} value={task.id}>{task.taskTitle}</option>
))}
</select>
</div> </div>
<div role="cell"> <div role="cell">
<TaskStatus item={item} /> <TaskStatus item={row.selectedTask} />
</div> </div>
<div role="cell"> <div role="cell">
<TaskArtifacts item={item} /> <TaskArtifacts item={row.selectedTask} />
</div> </div>
</article> </article>
))} ))}
......
...@@ -26,8 +26,22 @@ export const mockTaskPanelItems: TaskPanelItem[] = [ ...@@ -26,8 +26,22 @@ export const mockTaskPanelItems: TaskPanelItem[] = [
status: "running", status: "running",
statusDetail: "正在汇总账号定位、目标人群和栏目节奏", statusDetail: "正在汇总账号定位、目标人群和栏目节奏",
artifacts: [ artifacts: [
{ id: "artifact-content-outline", name: "选题规划草稿.md", kind: "文档" }, { id: "artifact-content-outline", name: "选题规划草稿.md", kind: "文档", url: "/Users/edy/Documents/qianjiangclaw/tasks/content/选题规划草稿.md" },
{ id: "artifact-content-calendar", name: "发布日历.xlsx", kind: "表格" } { id: "artifact-content-calendar", name: "发布日历.xlsx", kind: "表格", url: "/Users/edy/Documents/qianjiangclaw/tasks/content/发布日历.xlsx" },
{ id: "artifact-content-persona", name: "目标人群画像.md", kind: "文档", url: "/Users/edy/Documents/qianjiangclaw/tasks/content/目标人群画像.md" },
{ id: "artifact-content-topics", name: "栏目选题池.csv", kind: "表格", url: "/Users/edy/Documents/qianjiangclaw/tasks/content/栏目选题池.csv" },
{ id: "artifact-content-brief", name: "账号定位简报.pdf", kind: "文档", url: "/Users/edy/Documents/qianjiangclaw/tasks/content/账号定位简报.pdf" }
]
},
{
id: "mock-task-content-review",
date: getDefaultTaskPanelDate(),
expertName: "内容账号规划专家",
taskTitle: "复盘昨日内容表现",
status: "completed",
statusDetail: "已完成互动数据摘要与优化建议",
artifacts: [
{ id: "artifact-content-review", name: "昨日内容复盘与下轮优化建议.md", kind: "文档", url: "/Users/edy/Documents/qianjiangclaw/tasks/content/昨日内容复盘与下轮优化建议.md" }
] ]
}, },
{ {
...@@ -38,7 +52,18 @@ export const mockTaskPanelItems: TaskPanelItem[] = [ ...@@ -38,7 +52,18 @@ export const mockTaskPanelItems: TaskPanelItem[] = [
status: "completed", status: "completed",
statusDetail: "已完成回答大纲与首版正文", statusDetail: "已完成回答大纲与首版正文",
artifacts: [ artifacts: [
{ id: "artifact-zhihu-answer", name: "知乎回答初稿.md", kind: "文档" } { id: "artifact-zhihu-answer", name: "知乎回答初稿.md", kind: "文档", url: "/Users/edy/Documents/qianjiangclaw/tasks/zhihu/知乎回答初稿.md" }
]
},
{
id: "mock-task-zhihu-research",
date: getDefaultTaskPanelDate(),
expertName: "知乎专家",
taskTitle: "整理竞品问答素材",
status: "running",
statusDetail: "正在提取高赞回答结构和关键词",
artifacts: [
{ id: "artifact-zhihu-research", name: "竞品问答素材汇总-长文件名用于验证省略显示效果.xlsx", kind: "表格", url: "/Users/edy/Documents/qianjiangclaw/tasks/zhihu/竞品问答素材汇总-长文件名用于验证省略显示效果.xlsx" }
] ]
}, },
{ {
...@@ -57,7 +82,9 @@ export const mockTaskPanelItems: TaskPanelItem[] = [ ...@@ -57,7 +82,9 @@ export const mockTaskPanelItems: TaskPanelItem[] = [
taskTitle: "生成活动海报文案", taskTitle: "生成活动海报文案",
status: "failed", status: "failed",
statusDetail: "素材包缺少主视觉图片", statusDetail: "素材包缺少主视觉图片",
artifacts: [] artifacts: [
{ id: "artifact-poster-brief", name: "活动海报文案草稿.txt", kind: "文档", url: "/Users/edy/Documents/qianjiangclaw/tasks/poster/活动海报文案草稿.txt" }
]
} }
] ]
......
...@@ -34,42 +34,130 @@ ...@@ -34,42 +34,130 @@
.task-panel-title-group { .task-panel-title-group {
display: grid; display: grid;
gap: 6px;
min-width: 0; min-width: 0;
} }
.task-panel-title-group h1 { .task-panel-title-group h1 {
display: inline-flex;
width: fit-content;
margin: 0; margin: 0;
padding: 4px;
border-radius: 999px;
border: 1px solid rgba(191, 219, 254, 0.92);
background: linear-gradient(180deg, #ffffff 0%, #eef7ff 100%);
box-shadow: 0 10px 22px rgba(37, 99, 235, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.96);
color: #1d344f; color: #1d344f;
font-size: 28px; font-size: 18px;
line-height: 1.2; line-height: 1.2;
} }
.task-panel-title-group p { .task-panel-title-group h1 span {
margin: 0; display: inline-flex;
color: #61758f; min-height: 36px;
font-size: 14px; align-items: center;
line-height: 1.6; padding: 0 18px;
border-radius: 999px;
background: #ffffff;
box-shadow: inset 0 0 0 1px rgba(226, 232, 240, 0.9);
font-weight: 800;
white-space: nowrap;
} }
.task-panel-date-field { .task-panel-date-field {
display: grid; position: relative;
gap: 6px; display: inline-grid;
flex: 0 0 auto; flex: 0 0 auto;
color: #53637f;
font-size: 12px;
font-weight: 700;
} }
.task-panel-date-field input { .task-panel-date-field input {
min-height: 38px; position: absolute;
padding: 0 12px; right: 0;
border-radius: 12px; bottom: 0;
border: 1px solid #d8e1ef; width: 1px;
background: #ffffff; height: 1px;
opacity: 0;
pointer-events: none;
}
.task-panel-header .task-panel-date-pill {
display: inline-flex;
align-items: center;
gap: 8px;
min-height: 42px;
padding: 0;
border: 0;
border-radius: 0;
background: transparent;
box-shadow: none;
color: #1f2f49; color: #1f2f49;
cursor: pointer;
font: inherit; font: inherit;
font-size: 13px; transition: color 180ms ease;
}
.task-panel-header .task-panel-date-pill:hover,
.task-panel-header .task-panel-date-pill:focus-visible {
background: transparent;
box-shadow: none;
color: #1d4ed8;
}
.task-panel-header .task-panel-date-pill:focus-visible {
outline: 2px solid rgba(37, 99, 235, 0.28);
outline-offset: 2px;
}
.task-panel-date-calendar {
display: grid;
grid-template-rows: 10px 1fr;
width: 32px;
height: 32px;
overflow: hidden;
place-items: center;
border-radius: 9px;
background: linear-gradient(180deg, #ffffff 0%, #eef4ff 100%);
border: 1px solid #d7e1ef;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.94), 0 5px 10px rgba(15, 23, 42, 0.08);
}
.task-panel-date-calendar-month {
display: grid;
width: 100%;
height: 100%;
place-items: center;
background: #ff5f57;
color: #ffffff;
font-size: 7px;
font-weight: 800;
line-height: 1;
}
.task-panel-date-calendar-day {
color: #1f2f49;
font-size: 12px;
font-weight: 800;
line-height: 1;
}
.task-panel-date-value {
display: inline-flex;
min-height: 42px;
align-items: center;
padding: 0 16px;
border: 1px solid rgba(203, 213, 225, 0.92);
border-radius: 999px;
background: linear-gradient(180deg, #ffffff 0%, #f8fbff 100%);
box-shadow: none;
font-size: 14px;
font-weight: 800;
line-height: 1;
white-space: nowrap;
}
.task-panel-header .task-panel-date-pill:hover .task-panel-date-value,
.task-panel-header .task-panel-date-pill:focus-visible .task-panel-date-value {
border-color: #93c5fd;
color: #1d4ed8;
} }
.task-panel-scroll { .task-panel-scroll {
...@@ -83,16 +171,20 @@ ...@@ -83,16 +171,20 @@
.task-panel-table { .task-panel-table {
display: grid; display: grid;
gap: 10px; gap: 8px;
min-width: 920px;
} }
.task-panel-row { .task-panel-row {
display: grid; display: grid;
grid-template-columns: minmax(190px, 1fr) minmax(190px, 1fr) minmax(220px, 1.15fr); grid-template-columns: 165px 225px 170px minmax(250px, 1fr);
gap: 16px; gap: 14px;
align-items: start; align-items: center;
padding: 16px; min-height: 104px;
border-radius: 18px; max-height: 104px;
padding: 14px 16px;
overflow: visible;
border-radius: 16px;
border: 1px solid rgba(215, 216, 229, 0.96); border: 1px solid rgba(215, 216, 229, 0.96);
background: rgba(255, 255, 255, 0.92); background: rgba(255, 255, 255, 0.92);
box-shadow: 0 12px 28px rgba(17, 24, 39, 0.04); box-shadow: 0 12px 28px rgba(17, 24, 39, 0.04);
...@@ -100,8 +192,11 @@ ...@@ -100,8 +192,11 @@
.task-panel-row-head { .task-panel-row-head {
min-height: 44px; min-height: 44px;
max-height: 44px;
align-items: center; align-items: center;
text-align: center;
padding: 10px 16px; padding: 10px 16px;
overflow: hidden;
border-radius: 14px; border-radius: 14px;
background: rgba(239, 246, 255, 0.78); background: rgba(239, 246, 255, 0.78);
color: #53637f; color: #53637f;
...@@ -109,70 +204,287 @@ ...@@ -109,70 +204,287 @@
font-weight: 800; font-weight: 800;
} }
.task-panel-expert-cell, .task-panel-row > div {
.task-panel-status { min-width: 0;
}
.task-panel-row:not(.task-panel-row-head) > div:nth-child(3) {
padding-left: 54px;
}
.task-panel-row:not(.task-panel-row-head) > div:nth-child(4) {
display: flex;
position: relative;
align-items: center;
justify-content: center;
padding-left: 0;
}
.task-panel-expert-cell {
display: grid; display: grid;
gap: 7px; grid-template-columns: 36px minmax(0, 1fr);
align-items: center;
gap: 10px;
min-width: 0; min-width: 0;
} }
.task-panel-expert-icon {
display: grid;
width: 36px;
height: 36px;
place-items: center;
border-radius: 12px;
color: #2563eb;
background: #eff6ff;
border: 1px solid #dbeafe;
}
.task-panel-expert-icon svg {
width: 21px;
height: 21px;
}
.task-panel-expert-icon-planner {
color: #0f766e;
background: #ecfdf5;
border-color: #bbf7d0;
}
.task-panel-expert-icon-zhihu {
color: #1d4ed8;
background: #eff6ff;
border-color: #bfdbfe;
}
.task-panel-expert-icon-leads {
color: #0f766e;
background: #ecfeff;
border-color: #a5f3fc;
}
.task-panel-expert-icon-poster {
color: #be123c;
background: #fff1f2;
border-color: #fecdd3;
}
.task-panel-expert-cell strong { .task-panel-expert-cell strong {
min-width: 0;
color: #1c324d; color: #1c324d;
font-size: 15px; font-size: 13px;
font-weight: 500;
line-height: 1.4; line-height: 1.4;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.task-panel-task-cell {
min-width: 0;
}
.task-panel-task-cell select {
width: 100%;
min-width: 0;
min-height: 38px;
padding: 0 34px 0 12px;
border-radius: 12px;
border: 1px solid #d8e1ef;
background: #ffffff;
color: #1f2f49;
font: inherit;
font-size: 13px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.task-panel-status {
display: inline-flex;
align-items: center;
gap: 8px;
min-width: 0;
}
.task-panel-status-icon {
display: grid;
flex: 0 0 auto;
width: 34px;
height: 28px;
place-items: center;
border-radius: 999px;
font-size: 13px;
font-weight: 800;
line-height: 1;
}
.task-panel-status-text {
min-width: 0;
color: #1f2f49;
font-size: 13px;
font-weight: 600;
line-height: 1.5;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.task-panel-running-dots {
display: inline-flex;
align-items: center;
gap: 3px;
}
.task-panel-running-dots span {
width: 5px;
height: 5px;
border-radius: 999px;
background: currentColor;
animation: task-status-dot-bounce 1s ease-in-out infinite;
}
.task-panel-running-dots span:nth-child(2) {
animation-delay: 0.14s;
}
.task-panel-running-dots span:nth-child(3) {
animation-delay: 0.28s;
} }
.task-panel-expert-cell span,
.task-panel-status > span:not(.status-chip),
.task-panel-muted { .task-panel-muted {
color: #61758f; color: #61758f;
font-size: 13px; font-size: 13px;
line-height: 1.6; line-height: 1.6;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
} }
.task-panel-status .status-chip { .task-panel-status-completed .task-panel-status-icon {
width: fit-content; color: #047857;
gap: 6px; background: #ecfdf5;
border: 1px solid #a7f3d0;
} }
.task-panel-status-dot { .task-panel-status-pending .task-panel-status-icon {
width: 7px; color: #92400e;
height: 7px; background: #fffbeb;
border-radius: 999px; border: 1px solid #fde68a;
background: currentColor;
} }
.task-panel-status-dot-running { .task-panel-status-running .task-panel-status-icon {
animation: task-status-pulse 1.2s ease-in-out infinite; color: #1d4ed8;
background: #eff6ff;
border: 1px solid #bfdbfe;
} }
.task-panel-status-dot-failed { .task-panel-status-failed .task-panel-status-icon {
color: #b42318; color: #b42318;
background: #fef2f2;
border: 1px solid #fecaca;
} }
.task-panel-artifact-list { .task-panel-artifact-list {
display: grid; display: grid;
gap: 8px; gap: 6px;
width: min(100%, 420px);
margin: 0; margin: 0;
padding: 0; padding: 0;
list-style: none; list-style: none;
min-width: 0;
overflow: visible;
} }
.task-panel-artifact-list li { .task-panel-artifact-center {
display: flex; display: flex;
width: 100%;
min-width: 0;
align-items: center; align-items: center;
justify-content: space-between; justify-content: center;
gap: 10px; }
.task-panel-artifact-menu {
position: relative;
display: flex;
width: 100%;
min-width: 0; min-width: 0;
padding: 8px 10px; align-items: center;
justify-content: center;
z-index: 4;
}
.task-panel-artifact-trigger {
display: inline-flex;
min-height: 34px;
align-items: center;
justify-content: center;
padding: 0 14px;
border-radius: 999px;
border: 1px solid #cfe0f5;
background: #ffffff;
color: #1f2f49;
box-shadow: 0 8px 18px rgba(15, 23, 42, 0.06);
font: inherit;
font-size: 13px;
font-weight: 800;
line-height: 1;
white-space: nowrap;
cursor: pointer;
}
.task-panel-artifact-trigger:hover,
.task-panel-artifact-trigger:focus-visible,
.task-panel-artifact-trigger[aria-expanded="true"] {
border-color: #93c5fd;
color: #1d4ed8;
}
.task-panel-artifact-trigger:focus-visible {
outline: 2px solid rgba(37, 99, 235, 0.28);
outline-offset: 2px;
}
.task-panel-artifact-popover {
position: absolute;
top: calc(100% + 8px);
left: 50%;
width: min(520px, calc(100vw - 64px));
max-height: 220px;
overflow-y: auto;
padding: 8px;
border-radius: 14px;
border: 1px solid rgba(203, 213, 225, 0.96);
background: #ffffff;
box-shadow: 0 18px 42px rgba(15, 23, 42, 0.14);
transform: translateX(-50%);
z-index: 20;
}
.task-panel-artifact-popover .task-panel-artifact-list {
width: 100%;
}
.task-panel-artifact-list li {
display: grid;
position: relative;
grid-template-columns: minmax(110px, 0.86fr) minmax(0, 1.34fr);
align-items: center;
gap: 4px;
min-width: 0;
min-height: 34px;
padding: 6px 10px;
border-radius: 12px; border-radius: 12px;
background: #f8fbff; background: #f8fbff;
border: 1px solid #e3ebf5; border: 1px solid #e3ebf5;
} }
.task-panel-artifact-name { .task-panel-artifact-list li.task-panel-artifact-item-copied {
padding-right: 58px;
}
.task-panel-artifact-name,
.task-panel-artifact-url {
min-width: 0; min-width: 0;
color: #1f2f49;
font-size: 13px; font-size: 13px;
line-height: 1.5; line-height: 1.5;
overflow: hidden; overflow: hidden;
...@@ -180,21 +492,64 @@ ...@@ -180,21 +492,64 @@
white-space: nowrap; white-space: nowrap;
} }
.task-panel-artifact-kind { .task-panel-artifact-name {
flex: 0 0 auto; color: #1f2f49;
font-weight: 700;
}
.task-panel-artifact-url {
color: #60728c; color: #60728c;
justify-self: end;
width: 100%;
min-height: 24px;
padding: 0;
border: 0;
background: transparent;
font: inherit;
text-align: right;
cursor: pointer;
}
.task-panel-artifact-url:hover,
.task-panel-artifact-url:focus-visible {
color: #1d4ed8;
text-decoration: underline;
text-underline-offset: 3px;
}
.task-panel-artifact-url:focus-visible {
outline: 2px solid rgba(37, 99, 235, 0.34);
outline-offset: 2px;
border-radius: 6px;
}
.task-panel-artifact-copied {
position: absolute;
right: 10px;
top: 50%;
color: #047857;
font-size: 12px; font-size: 12px;
font-weight: 700; font-weight: 700;
line-height: 1.5;
text-align: right;
transform: translateY(-50%);
white-space: nowrap;
} }
@keyframes task-status-pulse { @keyframes task-status-dot-bounce {
0%, 100% { opacity: 0.42; } 0%, 80%, 100% {
50% { opacity: 1; } opacity: 0.42;
transform: translateY(0);
}
40% {
opacity: 1;
transform: translateY(-3px);
}
} }
@media (max-width: 980px) { @media (max-width: 980px) {
.task-panel-row { .task-panel-scroll {
grid-template-columns: 1fr; overflow-x: auto;
} }
} }
...@@ -205,6 +560,6 @@ ...@@ -205,6 +560,6 @@
} }
.task-panel-date-field { .task-panel-date-field {
width: 100%; width: fit-content;
} }
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment