@yolk_vat-y/dsh-project-memory 0.4.2 → 0.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -0
- package/README.md +33 -4
- package/README.zh-CN.md +34 -6
- package/client/client.js +709 -451
- package/client/client.js.map +1 -1
- package/package.json +9 -9
- package/src/client/TaskCommandNode.tsx +14 -22
- package/src/client/TaskComponents.tsx +341 -0
- package/src/client/TaskPanel.module.css +103 -40
- package/src/client/TaskPanel.tsx +104 -381
- package/src/client/locales.ts +6 -0
- package/src/client/task-data-store.ts +136 -0
- package/src/client/task-hooks.ts +169 -0
- package/src/client/task-ui-store.ts +118 -0
- package/src/commands/task-actions.js +9 -7
- package/src/commands/tasks.js +5 -3
- package/src/index.js +4 -3
- package/src/tools/task-tools.js +19 -0
- package/src/client/task-store.ts +0 -192
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task Panel Presentational 组件 — MiniBar(折叠迷你条)与 TaskCard(任务卡)。
|
|
3
|
+
* 纯渲染 + 卡片局部行内编辑/拖拽状态,动作经 props 回调交给容器(TaskPanel.tsx)。
|
|
4
|
+
*/
|
|
5
|
+
import { useRef, useState } from 'react'
|
|
6
|
+
import {
|
|
7
|
+
Button,
|
|
8
|
+
IconChevronDownOutline14,
|
|
9
|
+
IconChevronUpOutline14,
|
|
10
|
+
IconCheckOutline16,
|
|
11
|
+
IconPlayOutline16,
|
|
12
|
+
IconFolderOpenOutline16,
|
|
13
|
+
} from '@deepseek-ai/dsh-client-ui-primitives'
|
|
14
|
+
import { createTranslate } from './locales.ts'
|
|
15
|
+
import { useTaskDrag } from './task-hooks.ts'
|
|
16
|
+
import { type Task, type TaskStep } from './task-data-store.ts'
|
|
17
|
+
import css from './TaskPanel.module.css'
|
|
18
|
+
|
|
19
|
+
function timeAgo(iso?: string) {
|
|
20
|
+
if (!iso) return ''
|
|
21
|
+
const diff = Date.now() - new Date(iso).getTime()
|
|
22
|
+
const m = Math.floor(diff / 60000)
|
|
23
|
+
if (m < 1) return '刚刚'
|
|
24
|
+
if (m < 60) return `${m}分钟前`
|
|
25
|
+
const h = Math.floor(m / 60)
|
|
26
|
+
if (h < 24) return `${h}小时前`
|
|
27
|
+
return `${Math.floor(h / 24)}天前`
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function StepIcon({ status }: { status: TaskStep['status'] }) {
|
|
31
|
+
if (status === 'completed') return <IconCheckOutline16 className={css.stepDone} />
|
|
32
|
+
if (status === 'in_progress') return <IconPlayOutline16 className={css.stepRun} />
|
|
33
|
+
return <span className={css.stepPending} />
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface MiniBarProps {
|
|
37
|
+
label: string
|
|
38
|
+
hint: string
|
|
39
|
+
position: { x: number; y: number }
|
|
40
|
+
theme: string
|
|
41
|
+
onMove: (pos: { x: number; y: number }) => void
|
|
42
|
+
open: () => void
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function MiniBar({ label, hint, position, theme, onMove, open }: MiniBarProps) {
|
|
46
|
+
const barRef = useRef<HTMLDivElement>(null)
|
|
47
|
+
const down = useRef<{ dx: number; dy: number; sx: number; sy: number; moved: boolean } | null>(null)
|
|
48
|
+
|
|
49
|
+
const onMouseDown = (e: React.MouseEvent) => {
|
|
50
|
+
if (e.button !== 0) return
|
|
51
|
+
const rect = barRef.current?.getBoundingClientRect()
|
|
52
|
+
if (!rect) return
|
|
53
|
+
down.current = { dx: e.clientX - rect.left, dy: e.clientY - rect.top, sx: e.clientX, sy: e.clientY, moved: false }
|
|
54
|
+
const onMoveEv = (ev: MouseEvent) => {
|
|
55
|
+
const d = down.current
|
|
56
|
+
if (!d) return
|
|
57
|
+
if (!d.moved && Math.abs(ev.clientX - d.sx) + Math.abs(ev.clientY - d.sy) > 4) d.moved = true
|
|
58
|
+
if (d.moved) {
|
|
59
|
+
const w = barRef.current?.offsetWidth ?? 260
|
|
60
|
+
const h = barRef.current?.offsetHeight ?? 34
|
|
61
|
+
const x = Math.max(8, Math.min(window.innerWidth - w - 8, ev.clientX - d.dx))
|
|
62
|
+
const y = Math.max(8, Math.min(window.innerHeight - h - 8, ev.clientY - d.dy))
|
|
63
|
+
onMove({ x, y })
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const onUpEv = () => {
|
|
67
|
+
const d = down.current
|
|
68
|
+
down.current = null
|
|
69
|
+
window.removeEventListener('mousemove', onMoveEv)
|
|
70
|
+
window.removeEventListener('mouseup', onUpEv)
|
|
71
|
+
if (d && !d.moved) open()
|
|
72
|
+
}
|
|
73
|
+
window.addEventListener('mousemove', onMoveEv)
|
|
74
|
+
window.addEventListener('mouseup', onUpEv)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<div
|
|
79
|
+
ref={barRef}
|
|
80
|
+
className={css.miniBar}
|
|
81
|
+
style={{ left: position.x, top: position.y }}
|
|
82
|
+
data-theme={theme === 'native' ? undefined : theme}
|
|
83
|
+
onMouseDown={onMouseDown}
|
|
84
|
+
role="button"
|
|
85
|
+
title={hint}
|
|
86
|
+
aria-label={hint}
|
|
87
|
+
>
|
|
88
|
+
<IconFolderOpenOutline16 className={css.miniIcon} />
|
|
89
|
+
<span className={css.miniText}>{label}</span>
|
|
90
|
+
<IconChevronUpOutline14 className={css.miniChevron} />
|
|
91
|
+
</div>
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
interface TaskCardProps {
|
|
96
|
+
task: Task
|
|
97
|
+
isBound: boolean
|
|
98
|
+
expanded: boolean
|
|
99
|
+
onToggleExpand: () => void
|
|
100
|
+
onSwitch: () => void
|
|
101
|
+
onUnbind: () => void
|
|
102
|
+
onArchive: () => void
|
|
103
|
+
onRename: (value: string) => void
|
|
104
|
+
onEditStep: (index: number, value: string) => void
|
|
105
|
+
onCycleStatus: (index: number) => void
|
|
106
|
+
onReorderSteps: (fromIndex: number, toIndex: number) => void
|
|
107
|
+
showHints: boolean
|
|
108
|
+
syncing: boolean
|
|
109
|
+
t: ReturnType<typeof createTranslate>
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function TaskCard({
|
|
113
|
+
task,
|
|
114
|
+
isBound,
|
|
115
|
+
expanded,
|
|
116
|
+
onToggleExpand,
|
|
117
|
+
onSwitch,
|
|
118
|
+
onUnbind,
|
|
119
|
+
onArchive,
|
|
120
|
+
onRename,
|
|
121
|
+
onEditStep,
|
|
122
|
+
onCycleStatus,
|
|
123
|
+
onReorderSteps,
|
|
124
|
+
showHints,
|
|
125
|
+
syncing,
|
|
126
|
+
t,
|
|
127
|
+
}: TaskCardProps) {
|
|
128
|
+
const steps = task.steps || []
|
|
129
|
+
const { dragState, dropTargetIndex, startDrag, cancelDrag } = useTaskDrag(task.id, steps, isBound, onReorderSteps)
|
|
130
|
+
|
|
131
|
+
// 行内编辑状态(本卡片局部):双击进入,Enter/失焦提交,Esc 取消。
|
|
132
|
+
const [stepEdit, setStepEdit] = useState<{ index: number; value: string } | null>(null)
|
|
133
|
+
const [titleEdit, setTitleEdit] = useState<{ value: string } | null>(null)
|
|
134
|
+
|
|
135
|
+
const done = steps.filter((s) => s.status === 'completed').length
|
|
136
|
+
const pct = steps.length > 0 ? Math.round((done / steps.length) * 100) : 0
|
|
137
|
+
|
|
138
|
+
const commitStepEdit = (index: number) => {
|
|
139
|
+
if (!stepEdit || stepEdit.index !== index) return
|
|
140
|
+
const value = stepEdit.value
|
|
141
|
+
setStepEdit(null)
|
|
142
|
+
onEditStep(index, value)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const commitTitleEdit = () => {
|
|
146
|
+
if (titleEdit === null) return
|
|
147
|
+
const value = titleEdit.value
|
|
148
|
+
setTitleEdit(null)
|
|
149
|
+
onRename(value)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<article data-task-id={task.id} className={`${css.card}${isBound ? ` ${css.cardBound}` : ''}`}>
|
|
154
|
+
<button
|
|
155
|
+
className={css.cardHead}
|
|
156
|
+
onClick={() => { if (titleEdit !== null) return; onToggleExpand() }}
|
|
157
|
+
aria-expanded={expanded}
|
|
158
|
+
>
|
|
159
|
+
<div className={css.cardTitleRow}>
|
|
160
|
+
{isBound && <span className={css.boundBadge}>{t('task.current')}</span>}
|
|
161
|
+
{isBound && titleEdit !== null ? (
|
|
162
|
+
<input
|
|
163
|
+
className={css.stepInput}
|
|
164
|
+
value={titleEdit.value}
|
|
165
|
+
autoFocus
|
|
166
|
+
onChange={(e) => setTitleEdit({ value: e.target.value })}
|
|
167
|
+
onBlur={commitTitleEdit}
|
|
168
|
+
onKeyDown={(e) => {
|
|
169
|
+
if (e.key === 'Enter') commitTitleEdit()
|
|
170
|
+
else if (e.key === 'Escape') setTitleEdit(null)
|
|
171
|
+
}}
|
|
172
|
+
/>
|
|
173
|
+
) : (
|
|
174
|
+
<span
|
|
175
|
+
className={`${css.cardTitle}${isBound ? ` ${css.cardTitleEditable}` : ''}`}
|
|
176
|
+
title={isBound ? t('task.title-edit') : undefined}
|
|
177
|
+
onDoubleClick={(e) => {
|
|
178
|
+
e.preventDefault()
|
|
179
|
+
e.stopPropagation()
|
|
180
|
+
if (isBound) setTitleEdit({ value: String(task.title ?? '') })
|
|
181
|
+
}}
|
|
182
|
+
>
|
|
183
|
+
{task.title}
|
|
184
|
+
</span>
|
|
185
|
+
)}
|
|
186
|
+
</div>
|
|
187
|
+
<div className={css.cardMeta}>
|
|
188
|
+
{steps.length > 0 && <span className={css.progressText}>{done}/{steps.length}</span>}
|
|
189
|
+
<span className={css.updated}>{timeAgo(task.updatedAt || task.lastActiveAt)}</span>
|
|
190
|
+
<span className={css.chevron}>{expanded ? <IconChevronUpOutline14 /> : <IconChevronDownOutline14 />}</span>
|
|
191
|
+
</div>
|
|
192
|
+
</button>
|
|
193
|
+
{steps.length > 0 && (
|
|
194
|
+
<div className={css.track}>
|
|
195
|
+
<div className={css.trackFill} style={{ width: `${pct}%` }} />
|
|
196
|
+
</div>
|
|
197
|
+
)}
|
|
198
|
+
{expanded && (
|
|
199
|
+
<div className={css.cardBody}>
|
|
200
|
+
<div className={css.section}>
|
|
201
|
+
<div className={css.sectionLabel}>
|
|
202
|
+
{t('task.steps')}
|
|
203
|
+
{steps.length > 0 && <span className={css.sectionCount}>{steps.length}</span>}
|
|
204
|
+
</div>
|
|
205
|
+
{steps.length > 0 ? (
|
|
206
|
+
<>
|
|
207
|
+
<ol className={css.stepsList}>
|
|
208
|
+
{steps.map((step, i) => {
|
|
209
|
+
const stepIsEditing = stepEdit !== null && stepEdit.index === i
|
|
210
|
+
const stepIsDragging = dragState?.dragging && dragState.fromIndex === i
|
|
211
|
+
const isDropTarget = dropTargetIndex === i
|
|
212
|
+
const isDropTargetAfter = dropTargetIndex === i + 1
|
|
213
|
+
return (
|
|
214
|
+
<li
|
|
215
|
+
key={i}
|
|
216
|
+
data-step-row
|
|
217
|
+
className={`${css.stepRow}${stepIsDragging ? ` ${css.stepDragging}` : ''}${isDropTarget ? ` ${css.dropTargetBefore}` : ''}${isDropTargetAfter ? ` ${css.dropTargetAfter}` : ''}`}
|
|
218
|
+
onMouseDown={(e) => {
|
|
219
|
+
if (e.button !== 0) return
|
|
220
|
+
if (!isBound || syncing) return
|
|
221
|
+
// 交互控件(状态图标、编辑输入框等)上不进入拖拽
|
|
222
|
+
const el = e.target as HTMLElement
|
|
223
|
+
if (el.closest('button, input, textarea, a')) return
|
|
224
|
+
startDrag(i, step.content, e)
|
|
225
|
+
}}
|
|
226
|
+
>
|
|
227
|
+
{isBound ? (
|
|
228
|
+
<button
|
|
229
|
+
type="button"
|
|
230
|
+
className={css.stepToggle}
|
|
231
|
+
disabled={syncing}
|
|
232
|
+
onClick={(e) => { e.stopPropagation(); onCycleStatus(i) }}
|
|
233
|
+
title={`${t('step.completed')}/${t('step.in-progress')}/${t('step.pending')}`}
|
|
234
|
+
>
|
|
235
|
+
<StepIcon status={step.status} />
|
|
236
|
+
</button>
|
|
237
|
+
) : (
|
|
238
|
+
<StepIcon status={step.status} />
|
|
239
|
+
)}
|
|
240
|
+
{stepIsEditing ? (
|
|
241
|
+
<input
|
|
242
|
+
className={css.stepInput}
|
|
243
|
+
value={stepEdit!.value}
|
|
244
|
+
autoFocus
|
|
245
|
+
onChange={(e) => setStepEdit({ index: i, value: e.target.value })}
|
|
246
|
+
onBlur={() => commitStepEdit(i)}
|
|
247
|
+
onKeyDown={(e) => {
|
|
248
|
+
if (e.key === 'Enter') commitStepEdit(i)
|
|
249
|
+
else if (e.key === 'Escape') setStepEdit(null)
|
|
250
|
+
}}
|
|
251
|
+
/>
|
|
252
|
+
) : (
|
|
253
|
+
<span
|
|
254
|
+
className={`${css.stepContent}${step.status === 'completed' ? ` ${css.stepContentDone}` : ''}${step.status === 'in_progress' ? ` ${css.stepContentRun}` : ''}${isBound ? ` ${css.stepEditable}` : ''}`}
|
|
255
|
+
title={isBound && showHints ? t('step.edit-hint') : undefined}
|
|
256
|
+
onDoubleClick={(e) => {
|
|
257
|
+
e.stopPropagation()
|
|
258
|
+
if (isBound) setStepEdit({ index: i, value: String(step.content ?? '') })
|
|
259
|
+
}}
|
|
260
|
+
>
|
|
261
|
+
{step.content}
|
|
262
|
+
</span>
|
|
263
|
+
)}
|
|
264
|
+
</li>
|
|
265
|
+
)
|
|
266
|
+
})}
|
|
267
|
+
</ol>
|
|
268
|
+
{dragState && dragState.dragging && (
|
|
269
|
+
<div
|
|
270
|
+
className={css.dragGhost}
|
|
271
|
+
style={{
|
|
272
|
+
left: dragState.clientX + 12,
|
|
273
|
+
top: dragState.clientY - 20,
|
|
274
|
+
}}
|
|
275
|
+
>
|
|
276
|
+
<div className={css.dragGhostCard}>
|
|
277
|
+
<span className={css.dragGhostContent}>{dragState.stepContent}</span>
|
|
278
|
+
<button
|
|
279
|
+
type="button"
|
|
280
|
+
className={css.dragGhostClose}
|
|
281
|
+
style={{ pointerEvents: 'auto' }}
|
|
282
|
+
onClick={(e) => { e.stopPropagation(); cancelDrag() }}
|
|
283
|
+
title={showHints ? t('drag.cancel') : undefined}
|
|
284
|
+
aria-label={t('drag.cancel')}
|
|
285
|
+
>
|
|
286
|
+
×
|
|
287
|
+
</button>
|
|
288
|
+
</div>
|
|
289
|
+
</div>
|
|
290
|
+
)}
|
|
291
|
+
</>
|
|
292
|
+
) : (
|
|
293
|
+
<div className={css.muted}>{t('panel.empty-desc')}</div>
|
|
294
|
+
)}
|
|
295
|
+
</div>
|
|
296
|
+
|
|
297
|
+
{(task.files?.length ?? 0) > 0 && (
|
|
298
|
+
<div className={css.section}>
|
|
299
|
+
<div className={css.sectionLabel}>
|
|
300
|
+
{t('task.files')}
|
|
301
|
+
<span className={css.sectionCount}>{task.files.length}</span>
|
|
302
|
+
</div>
|
|
303
|
+
<ul className={css.filesList}>
|
|
304
|
+
{task.files.slice(0, 12).map((file, i) => (
|
|
305
|
+
<li key={i}>
|
|
306
|
+
<button
|
|
307
|
+
className={css.fileRow}
|
|
308
|
+
onClick={() => navigator.clipboard?.writeText(file.path)}
|
|
309
|
+
title={`${t('file.copy')}: ${file.path}${file.line ? `:${file.line}` : ''}`}
|
|
310
|
+
>
|
|
311
|
+
<span className={css.fileDot} />
|
|
312
|
+
<span className={css.filePath}>{file.path}</span>
|
|
313
|
+
{file.line && <span className={css.fileLine}>:{file.line}</span>}
|
|
314
|
+
</button>
|
|
315
|
+
</li>
|
|
316
|
+
))}
|
|
317
|
+
{(task.files?.length ?? 0) > 12 && (
|
|
318
|
+
<li className={css.muted}>… 共 {task.files.length} 个</li>
|
|
319
|
+
)}
|
|
320
|
+
</ul>
|
|
321
|
+
</div>
|
|
322
|
+
)}
|
|
323
|
+
|
|
324
|
+
<div className={css.cardFooter}>
|
|
325
|
+
<Button variant="outline" size="sm" disabled={isBound || syncing} onClick={onSwitch}>
|
|
326
|
+
{t('task.switch')}
|
|
327
|
+
</Button>
|
|
328
|
+
{isBound && (
|
|
329
|
+
<Button variant="outline" size="sm" disabled={syncing} onClick={onUnbind} title={t('task.unbind')}>
|
|
330
|
+
{t('task.unbind')}
|
|
331
|
+
</Button>
|
|
332
|
+
)}
|
|
333
|
+
<Button variant="outline" size="sm" disabled={syncing} onClick={onArchive}>
|
|
334
|
+
{t('task.archive')}
|
|
335
|
+
</Button>
|
|
336
|
+
</div>
|
|
337
|
+
</div>
|
|
338
|
+
)}
|
|
339
|
+
</article>
|
|
340
|
+
)
|
|
341
|
+
}
|
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
全部用 dsh 别名 token(--dsw-alias-*),亮暗随宿主主题自动切换;
|
|
3
3
|
轻边框 + 轻阴影,无玻璃拟态、无默认 AI 渐变,与 dsh 控件同语言。 */
|
|
4
4
|
|
|
5
|
-
.panel
|
|
6
|
-
.pill {
|
|
5
|
+
.panel {
|
|
7
6
|
/* shell.overlay 默认 click-through,条目自身开启 pointer-events */
|
|
8
7
|
pointer-events: auto;
|
|
9
8
|
font-family: inherit;
|
|
@@ -102,17 +101,6 @@
|
|
|
102
101
|
background: var(--dsw-alias-button-primary-fill, #3a6ef5);
|
|
103
102
|
white-space: nowrap;
|
|
104
103
|
}
|
|
105
|
-
.softBadge {
|
|
106
|
-
flex: none;
|
|
107
|
-
font-size: 10px;
|
|
108
|
-
line-height: 14px;
|
|
109
|
-
padding: 0 6px;
|
|
110
|
-
border-radius: 7px;
|
|
111
|
-
color: var(--dsw-alias-label-tertiary);
|
|
112
|
-
background: var(--dsw-alias-interactive-bg-hover, rgba(127, 127, 127, 0.12));
|
|
113
|
-
white-space: nowrap;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
104
|
.notice {
|
|
117
105
|
margin: 8px 12px 0;
|
|
118
106
|
padding: 6px 10px;
|
|
@@ -238,6 +226,9 @@
|
|
|
238
226
|
.cardBody {
|
|
239
227
|
border-top: 1px solid var(--dsw-alias-border-l1, rgba(127, 127, 127, 0.14));
|
|
240
228
|
padding: 8px 10px 10px;
|
|
229
|
+
max-height: 300px;
|
|
230
|
+
overflow-y: auto;
|
|
231
|
+
overscroll-behavior: contain;
|
|
241
232
|
}
|
|
242
233
|
.section { margin-bottom: 8px; }
|
|
243
234
|
.section:last-child { margin-bottom: 0; }
|
|
@@ -355,17 +346,9 @@
|
|
|
355
346
|
margin-top: 8px;
|
|
356
347
|
}
|
|
357
348
|
|
|
358
|
-
.spinning svg {
|
|
359
|
-
animation: spin 0.9s linear infinite;
|
|
360
|
-
}
|
|
361
|
-
@keyframes spin {
|
|
362
|
-
to { transform: rotate(360deg); }
|
|
363
|
-
}
|
|
364
|
-
|
|
365
349
|
@media (prefers-reduced-motion: reduce) {
|
|
366
350
|
.panel { animation: none; }
|
|
367
|
-
.trackFill, .card
|
|
368
|
-
.spinning svg { animation-duration: 2.4s; }
|
|
351
|
+
.trackFill, .card { transition: none; }
|
|
369
352
|
}
|
|
370
353
|
|
|
371
354
|
/* —— 折叠迷你条(顶部可拖拽) —— */
|
|
@@ -404,11 +387,6 @@
|
|
|
404
387
|
flex: 1 1 auto;
|
|
405
388
|
min-height: 0;
|
|
406
389
|
}
|
|
407
|
-
.cardBody {
|
|
408
|
-
max-height: 300px;
|
|
409
|
-
overflow-y: auto;
|
|
410
|
-
overscroll-behavior: contain;
|
|
411
|
-
}
|
|
412
390
|
.card {
|
|
413
391
|
display: flex;
|
|
414
392
|
flex-direction: column;
|
|
@@ -452,9 +430,12 @@
|
|
|
452
430
|
outline: 1px dashed var(--dsw-alias-border-l3, rgba(127,127,127,.4));
|
|
453
431
|
outline-offset: 1px;
|
|
454
432
|
}
|
|
433
|
+
/* 行内编辑框(步骤/标题共用,单行 input) */
|
|
455
434
|
.stepInput {
|
|
456
|
-
flex: 1;
|
|
435
|
+
flex: 1 1 auto;
|
|
457
436
|
min-width: 0;
|
|
437
|
+
width: auto;
|
|
438
|
+
box-sizing: border-box;
|
|
458
439
|
font: inherit;
|
|
459
440
|
font-size: 12px;
|
|
460
441
|
line-height: 20px;
|
|
@@ -465,19 +446,12 @@
|
|
|
465
446
|
padding: 0 5px;
|
|
466
447
|
}
|
|
467
448
|
|
|
468
|
-
/* ——
|
|
469
|
-
.
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
min-width: 0;
|
|
473
|
-
width: auto;
|
|
474
|
-
resize: none;
|
|
475
|
-
overflow: hidden;
|
|
476
|
-
box-sizing: border-box;
|
|
477
|
-
white-space: pre-wrap;
|
|
478
|
-
word-break: break-word;
|
|
479
|
-
line-height: 20px;
|
|
449
|
+
/* —— 步骤拖拽重排:源行半透明 —— */
|
|
450
|
+
.stepDragging {
|
|
451
|
+
opacity: 0.3;
|
|
452
|
+
background: var(--dsw-alias-interactive-bg-hover, rgba(127,127,127,.08));
|
|
480
453
|
}
|
|
454
|
+
|
|
481
455
|
.cardTitleEditable {
|
|
482
456
|
cursor: text;
|
|
483
457
|
border-radius: 3px;
|
|
@@ -562,3 +536,92 @@
|
|
|
562
536
|
.headerTitle { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
|
|
563
537
|
.headerRight { min-width: 0; }
|
|
564
538
|
.counts { flex: none; max-width: 45%; overflow: hidden; text-overflow: ellipsis; margin-left: auto; }
|
|
539
|
+
|
|
540
|
+
/* —— 长按拖拽浮动卡片 —— */
|
|
541
|
+
.dragGhost {
|
|
542
|
+
position: fixed;
|
|
543
|
+
pointer-events: none;
|
|
544
|
+
z-index: 9999;
|
|
545
|
+
transform: translate(-50%, -50%);
|
|
546
|
+
transition: left 0.05s ease, top 0.05s ease;
|
|
547
|
+
}
|
|
548
|
+
.dragGhostCard {
|
|
549
|
+
display: inline-flex;
|
|
550
|
+
align-items: center;
|
|
551
|
+
gap: 8px;
|
|
552
|
+
padding: 8px 12px;
|
|
553
|
+
border-radius: 8px;
|
|
554
|
+
background: var(--dsw-alias-bg-layer-2, var(--dsw-alias-bg-base, #fff));
|
|
555
|
+
border: 1px solid var(--dsw-alias-border-l3, rgba(127,127,127,.35));
|
|
556
|
+
box-shadow: 0 8px 24px rgba(0,0,0,.18), 0 2px 8px rgba(0,0,0,.1);
|
|
557
|
+
font-size: 12px;
|
|
558
|
+
line-height: 20px;
|
|
559
|
+
color: var(--dsw-alias-label-primary);
|
|
560
|
+
white-space: nowrap;
|
|
561
|
+
max-width: 280px;
|
|
562
|
+
transform: scale(0.9);
|
|
563
|
+
}
|
|
564
|
+
.dragGhostContent {
|
|
565
|
+
flex: 1;
|
|
566
|
+
min-width: 0;
|
|
567
|
+
overflow: hidden;
|
|
568
|
+
text-overflow: ellipsis;
|
|
569
|
+
word-break: break-word;
|
|
570
|
+
}
|
|
571
|
+
.dragGhostClose {
|
|
572
|
+
flex: none;
|
|
573
|
+
display: inline-flex;
|
|
574
|
+
align-items: center;
|
|
575
|
+
justify-content: center;
|
|
576
|
+
width: 20px;
|
|
577
|
+
height: 20px;
|
|
578
|
+
margin-left: 8px;
|
|
579
|
+
border: none;
|
|
580
|
+
border-radius: 4px;
|
|
581
|
+
background: transparent;
|
|
582
|
+
color: var(--dsw-alias-label-tertiary);
|
|
583
|
+
font-size: 16px;
|
|
584
|
+
line-height: 1;
|
|
585
|
+
cursor: pointer;
|
|
586
|
+
opacity: 0.6;
|
|
587
|
+
transition: opacity 0.15s ease, background 0.15s ease, color 0.15s ease;
|
|
588
|
+
}
|
|
589
|
+
.dragGhostClose:hover {
|
|
590
|
+
opacity: 1;
|
|
591
|
+
color: var(--dsw-alias-label-secondary);
|
|
592
|
+
background: var(--dsw-alias-interactive-bg-hover, rgba(127,127,127,.12));
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/* —— 步骤列表插入指示线 —— */
|
|
596
|
+
.dropTargetBefore { position: relative; }
|
|
597
|
+
.dropTargetBefore::before {
|
|
598
|
+
content: '';
|
|
599
|
+
position: absolute;
|
|
600
|
+
left: 0;
|
|
601
|
+
right: 0;
|
|
602
|
+
top: -1px;
|
|
603
|
+
height: 2px;
|
|
604
|
+
background: var(--dsw-alias-button-primary-fill, #3a6ef5);
|
|
605
|
+
border-radius: 1px;
|
|
606
|
+
box-shadow: 0 0 4px var(--dsw-alias-button-primary-fill, #3a6ef5);
|
|
607
|
+
animation: dropPulse 0.8s ease-in-out infinite;
|
|
608
|
+
}
|
|
609
|
+
.dropTargetAfter { position: relative; }
|
|
610
|
+
.dropTargetAfter::after {
|
|
611
|
+
content: '';
|
|
612
|
+
position: absolute;
|
|
613
|
+
left: 0;
|
|
614
|
+
right: 0;
|
|
615
|
+
bottom: -1px;
|
|
616
|
+
height: 2px;
|
|
617
|
+
background: var(--dsw-alias-button-primary-fill, #3a6ef5);
|
|
618
|
+
border-radius: 1px;
|
|
619
|
+
box-shadow: 0 0 4px var(--dsw-alias-button-primary-fill, #3a6ef5);
|
|
620
|
+
animation: dropPulse 0.8s ease-in-out infinite;
|
|
621
|
+
}
|
|
622
|
+
@keyframes dropPulse {
|
|
623
|
+
0%, 100% { opacity: 0.6; }
|
|
624
|
+
50% { opacity: 1; }
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/* —— header 防重叠 —— */
|