@yolk_vat-y/dsh-project-memory 0.4.3 → 0.5.0

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.
@@ -0,0 +1,388 @@
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 TaskInsight, type TaskStep } from './task-data-store.ts'
17
+ import css from './TaskPanel.module.css'
18
+
19
+ function insightKindLabel(kind: string, t: ReturnType<typeof createTranslate>): string {
20
+ const map: Record<string, string> = {
21
+ lesson: t('mem.kind.lesson'),
22
+ decision: t('mem.kind.decision'),
23
+ procedure: t('mem.kind.procedure'),
24
+ experience: t('mem.kind.experience'),
25
+ }
26
+ return map[kind] ?? kind
27
+ }
28
+
29
+ type InsightAction = 'confirm' | 'promote' | 'delete'
30
+
31
+ function timeAgo(iso?: string) {
32
+ if (!iso) return ''
33
+ const diff = Date.now() - new Date(iso).getTime()
34
+ const m = Math.floor(diff / 60000)
35
+ if (m < 1) return '刚刚'
36
+ if (m < 60) return `${m}分钟前`
37
+ const h = Math.floor(m / 60)
38
+ if (h < 24) return `${h}小时前`
39
+ return `${Math.floor(h / 24)}天前`
40
+ }
41
+
42
+ function StepIcon({ status }: { status: TaskStep['status'] }) {
43
+ if (status === 'completed') return <IconCheckOutline16 className={css.stepDone} />
44
+ if (status === 'in_progress') return <IconPlayOutline16 className={css.stepRun} />
45
+ return <span className={css.stepPending} />
46
+ }
47
+
48
+ interface MiniBarProps {
49
+ label: string
50
+ hint: string
51
+ position: { x: number; y: number }
52
+ theme: string
53
+ onMove: (pos: { x: number; y: number }) => void
54
+ open: () => void
55
+ }
56
+
57
+ export function MiniBar({ label, hint, position, theme, onMove, open }: MiniBarProps) {
58
+ const barRef = useRef<HTMLDivElement>(null)
59
+ const down = useRef<{ dx: number; dy: number; sx: number; sy: number; moved: boolean } | null>(null)
60
+
61
+ const onMouseDown = (e: React.MouseEvent) => {
62
+ if (e.button !== 0) return
63
+ const rect = barRef.current?.getBoundingClientRect()
64
+ if (!rect) return
65
+ down.current = { dx: e.clientX - rect.left, dy: e.clientY - rect.top, sx: e.clientX, sy: e.clientY, moved: false }
66
+ const onMoveEv = (ev: MouseEvent) => {
67
+ const d = down.current
68
+ if (!d) return
69
+ if (!d.moved && Math.abs(ev.clientX - d.sx) + Math.abs(ev.clientY - d.sy) > 4) d.moved = true
70
+ if (d.moved) {
71
+ const w = barRef.current?.offsetWidth ?? 260
72
+ const h = barRef.current?.offsetHeight ?? 34
73
+ const x = Math.max(8, Math.min(window.innerWidth - w - 8, ev.clientX - d.dx))
74
+ const y = Math.max(8, Math.min(window.innerHeight - h - 8, ev.clientY - d.dy))
75
+ onMove({ x, y })
76
+ }
77
+ }
78
+ const onUpEv = () => {
79
+ const d = down.current
80
+ down.current = null
81
+ window.removeEventListener('mousemove', onMoveEv)
82
+ window.removeEventListener('mouseup', onUpEv)
83
+ if (d && !d.moved) open()
84
+ }
85
+ window.addEventListener('mousemove', onMoveEv)
86
+ window.addEventListener('mouseup', onUpEv)
87
+ }
88
+
89
+ return (
90
+ <div
91
+ ref={barRef}
92
+ className={css.miniBar}
93
+ style={{ left: position.x, top: position.y }}
94
+ data-theme={theme === 'native' ? undefined : theme}
95
+ onMouseDown={onMouseDown}
96
+ role="button"
97
+ title={hint}
98
+ aria-label={hint}
99
+ >
100
+ <IconFolderOpenOutline16 className={css.miniIcon} />
101
+ <span className={css.miniText}>{label}</span>
102
+ <IconChevronUpOutline14 className={css.miniChevron} />
103
+ </div>
104
+ )
105
+ }
106
+
107
+ interface TaskCardProps {
108
+ task: Task
109
+ isBound: boolean
110
+ expanded: boolean
111
+ onToggleExpand: () => void
112
+ onSwitch: () => void
113
+ onUnbind: () => void
114
+ onArchive: () => void
115
+ onRename: (value: string) => void
116
+ onEditStep: (index: number, value: string) => void
117
+ onCycleStatus: (index: number) => void
118
+ onReorderSteps: (fromIndex: number, toIndex: number) => void
119
+ onInsightAction?: (action: InsightAction, id: string) => void
120
+ showHints: boolean
121
+ syncing: boolean
122
+ t: ReturnType<typeof createTranslate>
123
+ }
124
+
125
+ export function TaskCard({
126
+ task,
127
+ isBound,
128
+ expanded,
129
+ onToggleExpand,
130
+ onSwitch,
131
+ onUnbind,
132
+ onArchive,
133
+ onRename,
134
+ onEditStep,
135
+ onCycleStatus,
136
+ onReorderSteps,
137
+ onInsightAction,
138
+ showHints,
139
+ syncing,
140
+ t,
141
+ }: TaskCardProps) {
142
+ const steps = task.steps || []
143
+ const { dragState, dropTargetIndex, startDrag, cancelDrag } = useTaskDrag(task.id, steps, isBound, onReorderSteps)
144
+
145
+ // 行内编辑状态(本卡片局部):双击进入,Enter/失焦提交,Esc 取消。
146
+ const [stepEdit, setStepEdit] = useState<{ index: number; value: string } | null>(null)
147
+ const [titleEdit, setTitleEdit] = useState<{ value: string } | null>(null)
148
+
149
+ const done = steps.filter((s) => s.status === 'completed').length
150
+ const pct = steps.length > 0 ? Math.round((done / steps.length) * 100) : 0
151
+
152
+ const commitStepEdit = (index: number) => {
153
+ if (!stepEdit || stepEdit.index !== index) return
154
+ const value = stepEdit.value
155
+ setStepEdit(null)
156
+ onEditStep(index, value)
157
+ }
158
+
159
+ const commitTitleEdit = () => {
160
+ if (titleEdit === null) return
161
+ const value = titleEdit.value
162
+ setTitleEdit(null)
163
+ onRename(value)
164
+ }
165
+
166
+ return (
167
+ <article data-task-id={task.id} className={`${css.card}${isBound ? ` ${css.cardBound}` : ''}`}>
168
+ <button
169
+ className={css.cardHead}
170
+ onClick={() => { if (titleEdit !== null) return; onToggleExpand() }}
171
+ aria-expanded={expanded}
172
+ >
173
+ <div className={css.cardTitleRow}>
174
+ {isBound && <span className={css.boundBadge}>{t('task.current')}</span>}
175
+ {isBound && titleEdit !== null ? (
176
+ <input
177
+ className={css.stepInput}
178
+ value={titleEdit.value}
179
+ autoFocus
180
+ onChange={(e) => setTitleEdit({ value: e.target.value })}
181
+ onBlur={commitTitleEdit}
182
+ onKeyDown={(e) => {
183
+ if (e.key === 'Enter') commitTitleEdit()
184
+ else if (e.key === 'Escape') setTitleEdit(null)
185
+ }}
186
+ />
187
+ ) : (
188
+ <span
189
+ className={`${css.cardTitle}${isBound ? ` ${css.cardTitleEditable}` : ''}`}
190
+ title={isBound ? t('task.title-edit') : undefined}
191
+ onDoubleClick={(e) => {
192
+ e.preventDefault()
193
+ e.stopPropagation()
194
+ if (isBound) setTitleEdit({ value: String(task.title ?? '') })
195
+ }}
196
+ >
197
+ {task.title}
198
+ </span>
199
+ )}
200
+ </div>
201
+ <div className={css.cardMeta}>
202
+ {steps.length > 0 && <span className={css.progressText}>{done}/{steps.length}</span>}
203
+ <span className={css.updated}>{timeAgo(task.updatedAt || task.lastActiveAt)}</span>
204
+ <span className={css.chevron}>{expanded ? <IconChevronUpOutline14 /> : <IconChevronDownOutline14 />}</span>
205
+ </div>
206
+ </button>
207
+ {steps.length > 0 && (
208
+ <div className={css.track}>
209
+ <div className={css.trackFill} style={{ width: `${pct}%` }} />
210
+ </div>
211
+ )}
212
+ {expanded && (
213
+ <div className={css.cardBody}>
214
+ <div className={css.section}>
215
+ <div className={css.sectionLabel}>
216
+ {t('task.steps')}
217
+ {steps.length > 0 && <span className={css.sectionCount}>{steps.length}</span>}
218
+ </div>
219
+ {steps.length > 0 ? (
220
+ <>
221
+ <ol className={css.stepsList}>
222
+ {steps.map((step, i) => {
223
+ const stepIsEditing = stepEdit !== null && stepEdit.index === i
224
+ const stepIsDragging = dragState?.dragging && dragState.fromIndex === i
225
+ const isDropTarget = dropTargetIndex === i
226
+ const isDropTargetAfter = dropTargetIndex === i + 1
227
+ return (
228
+ <li
229
+ key={i}
230
+ data-step-row
231
+ className={`${css.stepRow}${stepIsDragging ? ` ${css.stepDragging}` : ''}${isDropTarget ? ` ${css.dropTargetBefore}` : ''}${isDropTargetAfter ? ` ${css.dropTargetAfter}` : ''}`}
232
+ onMouseDown={(e) => {
233
+ if (e.button !== 0) return
234
+ if (!isBound || syncing) return
235
+ // 交互控件(状态图标、编辑输入框等)上不进入拖拽
236
+ const el = e.target as HTMLElement
237
+ if (el.closest('button, input, textarea, a')) return
238
+ startDrag(i, step.content, e)
239
+ }}
240
+ >
241
+ {isBound ? (
242
+ <button
243
+ type="button"
244
+ className={css.stepToggle}
245
+ disabled={syncing}
246
+ onClick={(e) => { e.stopPropagation(); onCycleStatus(i) }}
247
+ title={`${t('step.completed')}/${t('step.in-progress')}/${t('step.pending')}`}
248
+ >
249
+ <StepIcon status={step.status} />
250
+ </button>
251
+ ) : (
252
+ <StepIcon status={step.status} />
253
+ )}
254
+ {stepIsEditing ? (
255
+ <input
256
+ className={css.stepInput}
257
+ value={stepEdit!.value}
258
+ autoFocus
259
+ onChange={(e) => setStepEdit({ index: i, value: e.target.value })}
260
+ onBlur={() => commitStepEdit(i)}
261
+ onKeyDown={(e) => {
262
+ if (e.key === 'Enter') commitStepEdit(i)
263
+ else if (e.key === 'Escape') setStepEdit(null)
264
+ }}
265
+ />
266
+ ) : (
267
+ <span
268
+ className={`${css.stepContent}${step.status === 'completed' ? ` ${css.stepContentDone}` : ''}${step.status === 'in_progress' ? ` ${css.stepContentRun}` : ''}${isBound ? ` ${css.stepEditable}` : ''}`}
269
+ title={isBound && showHints ? t('step.edit-hint') : undefined}
270
+ onDoubleClick={(e) => {
271
+ e.stopPropagation()
272
+ if (isBound) setStepEdit({ index: i, value: String(step.content ?? '') })
273
+ }}
274
+ >
275
+ {step.content}
276
+ </span>
277
+ )}
278
+ </li>
279
+ )
280
+ })}
281
+ </ol>
282
+ {dragState && dragState.dragging && (
283
+ <div
284
+ className={css.dragGhost}
285
+ style={{
286
+ left: dragState.clientX + 12,
287
+ top: dragState.clientY - 20,
288
+ }}
289
+ >
290
+ <div className={css.dragGhostCard}>
291
+ <span className={css.dragGhostContent}>{dragState.stepContent}</span>
292
+ <button
293
+ type="button"
294
+ className={css.dragGhostClose}
295
+ style={{ pointerEvents: 'auto' }}
296
+ onClick={(e) => { e.stopPropagation(); cancelDrag() }}
297
+ title={showHints ? t('drag.cancel') : undefined}
298
+ aria-label={t('drag.cancel')}
299
+ >
300
+ ×
301
+ </button>
302
+ </div>
303
+ </div>
304
+ )}
305
+ </>
306
+ ) : (
307
+ <div className={css.muted}>{t('panel.empty-desc')}</div>
308
+ )}
309
+ </div>
310
+
311
+ {(task.files?.length ?? 0) > 0 && (
312
+ <div className={css.section}>
313
+ <div className={css.sectionLabel}>
314
+ {t('task.files')}
315
+ <span className={css.sectionCount}>{task.files.length}</span>
316
+ </div>
317
+ <ul className={css.filesList}>
318
+ {task.files.slice(0, 12).map((file, i) => (
319
+ <li key={i}>
320
+ <button
321
+ className={css.fileRow}
322
+ onClick={() => navigator.clipboard?.writeText(file.path)}
323
+ title={`${t('file.copy')}: ${file.path}${file.line ? `:${file.line}` : ''}`}
324
+ >
325
+ <span className={css.fileDot} />
326
+ <span className={css.filePath}>{file.path}</span>
327
+ {file.line && <span className={css.fileLine}>:{file.line}</span>}
328
+ </button>
329
+ </li>
330
+ ))}
331
+ {(task.files?.length ?? 0) > 12 && (
332
+ <li className={css.muted}>… 共 {task.files.length} 个</li>
333
+ )}
334
+ </ul>
335
+ </div>
336
+ )}
337
+
338
+ {onInsightAction && Array.isArray(task.insights) && task.insights.length > 0 && (
339
+ <div className={css.section}>
340
+ <div className={css.sectionLabel}>
341
+ {t('mem.section-label')}
342
+ <span className={css.sectionCount}>{task.insights.length}</span>
343
+ </div>
344
+ <ul className={css.memList}>
345
+ {(task.insights as TaskInsight[]).map((ins) => (
346
+ <li key={ins.id} className={css.memRow}>
347
+ <div className={css.memTitle}>
348
+ <span className={css.kindBadge}>{insightKindLabel(ins.kind, t)}</span>
349
+ <span>{ins.title || ins.id}</span>
350
+ {ins.draft && <span className={`${css.badge} ${css.badgeDraft}`}>{t('mem.draft')}</span>}
351
+ </div>
352
+ <div className={css.memActs}>
353
+ {ins.draft && (
354
+ <Button variant="outline" size="sm" disabled={syncing} onClick={() => onInsightAction('confirm', ins.id)}>
355
+ {t('mem.confirm')}
356
+ </Button>
357
+ )}
358
+ <Button variant="outline" size="sm" disabled={syncing} onClick={() => onInsightAction('promote', ins.id)}>
359
+ {t('mem.promote')}
360
+ </Button>
361
+ <Button variant="outline" size="sm" disabled={syncing} onClick={() => onInsightAction('delete', ins.id)}>
362
+ {t('mem.delete')}
363
+ </Button>
364
+ </div>
365
+ </li>
366
+ ))}
367
+ </ul>
368
+ </div>
369
+ )}
370
+
371
+ <div className={css.cardFooter}>
372
+ <Button variant="outline" size="sm" disabled={isBound || syncing} onClick={onSwitch}>
373
+ {t('task.switch')}
374
+ </Button>
375
+ {isBound && (
376
+ <Button variant="outline" size="sm" disabled={syncing} onClick={onUnbind} title={t('task.unbind')}>
377
+ {t('task.unbind')}
378
+ </Button>
379
+ )}
380
+ <Button variant="outline" size="sm" disabled={syncing} onClick={onArchive}>
381
+ {t('task.archive')}
382
+ </Button>
383
+ </div>
384
+ </div>
385
+ )}
386
+ </article>
387
+ )
388
+ }
@@ -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, .pill { transition: none; }
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
- .stepInput {
470
- display: block;
471
- flex: 1 1 auto;
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,214 @@
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 防重叠 —— */
628
+
629
+ /* ---- v0.5 Memory View(Task 草稿 / Project / Global 三作用域列表) ---- */
630
+ .memoryPane {
631
+ padding: 8px 10px 10px;
632
+ display: flex;
633
+ flex-direction: column;
634
+ gap: 6px;
635
+ overflow: auto;
636
+ max-height: 480px;
637
+ }
638
+ .memToolbar {
639
+ display: flex;
640
+ align-items: center;
641
+ justify-content: space-between;
642
+ gap: 8px;
643
+ }
644
+ .memCount {
645
+ font-size: 12px;
646
+ opacity: 0.75;
647
+ font-weight: 600;
648
+ }
649
+ .memList {
650
+ display: flex;
651
+ flex-direction: column;
652
+ gap: 6px;
653
+ }
654
+ .memRow {
655
+ border: 1px solid var(--dsw-border-weak, rgba(128, 128, 128, 0.25));
656
+ border-radius: 6px;
657
+ padding: 6px 8px;
658
+ display: flex;
659
+ flex-direction: column;
660
+ gap: 4px;
661
+ background: color-mix(in srgb, var(--dsw-bg-secondary, #f5f5f5) 45%, transparent);
662
+ }
663
+ .memRowArchived {
664
+ opacity: 0.55;
665
+ }
666
+ .memRowMain {
667
+ display: flex;
668
+ flex-direction: column;
669
+ gap: 2px;
670
+ }
671
+ .memTitle {
672
+ display: flex;
673
+ align-items: flex-start;
674
+ gap: 6px;
675
+ font-size: 13px;
676
+ line-height: 1.35;
677
+ word-break: break-word;
678
+ }
679
+ .kindBadge {
680
+ flex: none;
681
+ font-size: 11px;
682
+ border-radius: 4px;
683
+ padding: 0 5px;
684
+ line-height: 1.5;
685
+ background: var(--dsw-accent-soft, rgba(80, 120, 255, 0.15));
686
+ color: var(--dsw-accent, #4d6bfe);
687
+ }
688
+ .memMeta {
689
+ display: flex;
690
+ flex-wrap: wrap;
691
+ gap: 4px;
692
+ align-items: center;
693
+ }
694
+ .badge {
695
+ font-size: 11px;
696
+ border-radius: 4px;
697
+ padding: 0 5px;
698
+ line-height: 1.5;
699
+ background: color-mix(in srgb, var(--dsw-fg, #222) 8%, transparent);
700
+ }
701
+ .badgeDraft {
702
+ color: #b26a00;
703
+ background: rgba(255, 180, 0, 0.16);
704
+ }
705
+ .badgeArchived {
706
+ opacity: 0.8;
707
+ }
708
+ .memBody {
709
+ font-size: 12px;
710
+ opacity: 0.8;
711
+ word-break: break-word;
712
+ }
713
+ .memActs {
714
+ display: flex;
715
+ flex-wrap: wrap;
716
+ gap: 4px;
717
+ }
718
+
719
+ /* ---- v0.5 Memory View 新建表单 ---- */
720
+ .memToolbarActs {
721
+ display: flex;
722
+ align-items: center;
723
+ gap: 6px;
724
+ }
725
+ .memNew {
726
+ display: flex;
727
+ flex-direction: column;
728
+ gap: 6px;
729
+ border: 1px solid var(--dsw-border-weak, rgba(128, 128, 128, 0.25));
730
+ border-radius: 6px;
731
+ padding: 8px;
732
+ }
733
+ .memNewRow {
734
+ display: flex;
735
+ gap: 8px;
736
+ align-items: center;
737
+ }
738
+ .memLabel {
739
+ display: inline-flex;
740
+ align-items: center;
741
+ gap: 6px;
742
+ font-size: 12px;
743
+ opacity: 0.8;
744
+ }
745
+ .memNew select,
746
+ .memNew textarea {
747
+ width: 100%;
748
+ box-sizing: border-box;
749
+ }