Skip to content

Commit e52e6d8

Browse files
Improve edit UI and priority colors (#1177)
* feat: flag every selectable priority with its heat-ramp color on desktop and mobile and add a feature-gated Priority… action to the task quick menu, centralizing the fixed ramp in core so row strips, editor pills, slash commands, filters, inbox processing and quick capture share one affordance * feat(desktop): icon every task-editor field heading and choice pill, turn Preview/Edit, change-time, clear and attachment row actions into labelled icon buttons, and align attachment and checklist button styling * feat(mobile): icon every task-editor field heading and choice chip, turn change-time, clear, preview, edit and attachment actions into labelled icon buttons, make None controls icon-only, and align attachment and Add Item button styling * feat(mobile): relabel the quick-capture priority picker Clear row to None and swap the change-time chip for a labelled clock icon button * refactor(mobile): refine task editor controls * fix(mobile): keep the Save word on the task editor header, read priority colours from core everywhere, swap the last text glyphs for lucide icons and stub every lucide icon in tests (#1172) --------- Co-authored-by: dongdongbh <18310682633@163.com>
1 parent 87b2989 commit e52e6d8

60 files changed

Lines changed: 1637 additions & 417 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/desktop/src/components/ExpandedMarkdownEditor.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useCallback, useEffect, useId, useRef, useState, type ClipboardEvent, type KeyboardEvent } from 'react';
2-
import { X } from 'lucide-react';
2+
import { Eye, Pencil, X } from 'lucide-react';
33

44
import { cn } from '../lib/utils';
55
import { MarkdownFormatToolbar } from './MarkdownFormatToolbar';
@@ -112,9 +112,13 @@ export function ExpandedMarkdownEditor({
112112
<button
113113
type="button"
114114
onClick={() => setMode((prev) => (prev === 'edit' ? 'preview' : 'edit'))}
115-
className="rounded-md border border-border bg-background px-2.5 py-1.5 text-xs font-medium text-muted-foreground transition-colors hover:bg-muted/40"
115+
aria-label={mode === 'edit' ? t('markdown.preview') : t('markdown.edit')}
116+
title={mode === 'edit' ? t('markdown.preview') : t('markdown.edit')}
117+
className="rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-muted/50 hover:text-foreground"
116118
>
117-
{mode === 'edit' ? t('markdown.preview') : t('markdown.edit')}
119+
{mode === 'edit'
120+
? <Eye className="h-4 w-4" aria-hidden="true" />
121+
: <Pencil className="h-4 w-4" aria-hidden="true" />}
118122
</button>
119123
<button
120124
ref={closeButtonRef}

apps/desktop/src/components/InboxProcessingPanels.test.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,8 @@ describe('InboxProcessingQuickPanel draft editing', () => {
262262

263263
fireEvent.click(getByRole('button', { name: 'priority.high' }));
264264
expect(getByRole('button', { name: 'priority.high' })).toHaveClass('bg-primary');
265+
expect(getByRole('button', { name: 'priority.high' }).querySelector('[data-priority-flag="high"]'))
266+
.toHaveAttribute('stroke', '#f97316');
265267

266268
fireEvent.click(getByRole('button', { name: 'priority.high' }));
267269
expect(getByRole('button', { name: 'priority.high' })).not.toHaveClass('bg-primary');

apps/desktop/src/components/InboxProcessingQuickPanel.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { ProjectSelector } from './ui/ProjectSelector';
2626
import { DateField } from './ui/DateField';
2727
import { QuickDateChips } from './QuickDateChips';
2828
import { SomedaySectionSelector } from './ui/SomedaySectionSelector';
29+
import { PriorityFlag } from './Task/PriorityFlag';
2930

3031
type QuickActionabilityChoice = 'actionable' | 'later' | 'trash' | 'someday' | 'reference' | 'incubate';
3132
type QuickTwoMinuteChoice = 'yes' | 'no';
@@ -829,6 +830,7 @@ export function InboxProcessingQuickPanel({
829830
: 'bg-muted/40 border-border hover:bg-muted/70'
830831
)}
831832
>
833+
<PriorityFlag priority={priority} />
832834
{t(`priority.${priority}`)}
833835
</button>
834836
);

apps/desktop/src/components/InboxProcessingWizard.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { ProjectSelector } from './ui/ProjectSelector';
2828
import { DateField } from './ui/DateField';
2929
import { QuickDateChips } from './QuickDateChips';
3030
import { SomedaySectionSelector } from './ui/SomedaySectionSelector';
31+
import { PriorityFlag } from './Task/PriorityFlag';
3132

3233
export type ProcessingStep = 'refine' | 'actionable' | 'projectcheck' | 'twomin' | 'decide' | 'context' | 'reference' | 'someday' | 'project' | 'delegate';
3334

@@ -996,6 +997,7 @@ export const InboxProcessingWizard = memo(function InboxProcessingWizard({
996997
: 'bg-muted hover:bg-muted/80'
997998
)}
998999
>
1000+
<PriorityFlag priority={priority} />
9991001
{t(`priority.${priority}`)}
10001002
</button>
10011003
);
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { Flag } from 'lucide-react';
2+
import type { TaskPriority } from '@mindwtr/core';
3+
import { TASK_PRIORITY_COLORS } from '@mindwtr/core';
4+
5+
import { cn } from '../../lib/utils';
6+
7+
/**
8+
* Decorative priority flag, tinted with the fixed priority heat ramp. Text
9+
* labels remain the accessible priority identifier, so this never reaches the
10+
* accessibility tree — color is never the sole signal.
11+
*/
12+
export function PriorityFlag({ priority, className }: { priority: TaskPriority; className?: string }) {
13+
return (
14+
<Flag
15+
aria-hidden
16+
data-priority-flag={priority}
17+
className={cn('h-3 w-3 shrink-0', className)}
18+
color={TASK_PRIORITY_COLORS[priority]}
19+
/>
20+
);
21+
}

apps/desktop/src/components/Task/TaskForm/AttachmentsField.tsx

Lines changed: 32 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1-
import { BookOpen, Edit3, Link2, Paperclip } from 'lucide-react';
1+
import { BookOpen, Link2, Paperclip, Pencil, Trash2 } from 'lucide-react';
22
import { tFallback, type Attachment } from '@mindwtr/core';
33
import { useBareFileReferenceCheck, useExternalFileReferenceCheck } from '../../../lib/attachment-reference';
44
import { getAttachmentDisplayTitle } from '../../../lib/attachment-utils';
55
import { isImageAttachment } from '../task-item-attachment-utils';
66
import { AttachmentImage } from '../AttachmentImage';
7-
import { QUICK_ADD_FIELD_TOKENS, QuickAddTokenBadge, taskEditorLabelClassName } from '../task-editor-label';
7+
import { QUICK_ADD_FIELD_TOKENS, QuickAddTokenBadge, TaskEditorFieldLabel } from '../task-editor-label';
8+
9+
// Secondary add actions share one bordered blue shape with the checklist's
10+
// "Add item" control so every way to grow a task reads the same.
11+
const taskEditorAddButtonClassName = 'inline-flex h-7 items-center gap-1.5 rounded-md border border-primary/30 px-2.5 text-xs font-medium text-primary transition-colors hover:bg-primary/10';
812

913
type AttachmentsFieldProps = {
1014
t: (key: string) => string;
@@ -53,34 +57,34 @@ export function AttachmentsField({
5357
return (
5458
<div className="flex flex-col gap-2">
5559
<div className="flex items-center justify-between">
56-
<label className={`${taskEditorLabelClassName} inline-flex items-center gap-1.5`}>
60+
<TaskEditorFieldLabel icon={Paperclip}>
5761
{t('attachments.title')}
5862
<QuickAddTokenBadge t={t} token={QUICK_ADD_FIELD_TOKENS.link} />
59-
</label>
63+
</TaskEditorFieldLabel>
6064
<div className="flex items-center gap-2">
6165
<button
6266
type="button"
6367
onClick={addFileAttachment}
64-
className="text-xs px-2 py-1 rounded bg-muted/50 hover:bg-muted transition-colors flex items-center gap-1"
68+
className={taskEditorAddButtonClassName}
6569
>
66-
<Paperclip className="w-3 h-3" />
70+
<Paperclip className="w-3.5 h-3.5" aria-hidden="true" />
6771
{t('attachments.addFile')}
6872
</button>
6973
<button
7074
type="button"
7175
onClick={addLinkAttachment}
72-
className="text-xs px-2 py-1 rounded bg-muted/50 hover:bg-muted transition-colors flex items-center gap-1"
76+
className={taskEditorAddButtonClassName}
7377
>
74-
<Link2 className="w-3 h-3" />
78+
<Link2 className="w-3.5 h-3.5" aria-hidden="true" />
7579
{t('attachments.addLink')}
7680
</button>
7781
{showObsidianNoteAttachment && (
7882
<button
7983
type="button"
8084
onClick={addObsidianNoteAttachment}
81-
className="text-xs px-2 py-1 rounded bg-muted/50 hover:bg-muted transition-colors flex items-center gap-1"
85+
className={taskEditorAddButtonClassName}
8286
>
83-
<BookOpen className="w-3 h-3" />
87+
<BookOpen className="w-3.5 h-3.5" aria-hidden="true" />
8488
{t('attachments.attachObsidianNote')}
8589
</button>
8690
)}
@@ -130,23 +134,26 @@ export function AttachmentsField({
130134
>
131135
{displayTitle}
132136
</button>
133-
<div className="flex shrink-0 items-center gap-2">
137+
<div className="flex shrink-0 items-center gap-1">
134138
{canEditAsLink(attachment) && (
135139
<button
136140
type="button"
137141
onClick={() => editLinkAttachment(attachment)}
138-
className="inline-flex items-center gap-1 text-muted-foreground hover:text-foreground"
142+
aria-label={t('common.edit')}
143+
title={t('common.edit')}
144+
className="rounded p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
139145
>
140-
<Edit3 className="h-3 w-3" />
141-
{t('common.edit')}
146+
<Pencil className="h-3.5 w-3.5" aria-hidden="true" />
142147
</button>
143148
)}
144149
<button
145150
type="button"
146151
onClick={() => removeAttachment(attachment.id)}
147-
className="text-muted-foreground hover:text-foreground"
152+
aria-label={t('attachments.remove')}
153+
title={t('attachments.remove')}
154+
className="rounded p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
148155
>
149-
{t('attachments.remove')}
156+
<Trash2 className="h-3.5 w-3.5" aria-hidden="true" />
150157
</button>
151158
</div>
152159
</div>
@@ -178,23 +185,26 @@ export function AttachmentsField({
178185
{displayTitle}
179186
</button>
180187
</div>
181-
<div className="flex shrink-0 items-center gap-2">
188+
<div className="flex shrink-0 items-center gap-1">
182189
{canEditAsLink(attachment) && (
183190
<button
184191
type="button"
185192
onClick={() => editLinkAttachment(attachment)}
186-
className="inline-flex items-center gap-1 text-muted-foreground hover:text-foreground"
193+
aria-label={t('common.edit')}
194+
title={t('common.edit')}
195+
className="rounded p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
187196
>
188-
<Edit3 className="h-3 w-3" />
189-
{t('common.edit')}
197+
<Pencil className="h-3.5 w-3.5" aria-hidden="true" />
190198
</button>
191199
)}
192200
<button
193201
type="button"
194202
onClick={() => removeAttachment(attachment.id)}
195-
className="text-muted-foreground hover:text-foreground"
203+
aria-label={t('attachments.remove')}
204+
title={t('attachments.remove')}
205+
className="rounded p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
196206
>
197-
{t('attachments.remove')}
207+
<Trash2 className="h-3.5 w-3.5" aria-hidden="true" />
198208
</button>
199209
</div>
200210
</div>

apps/desktop/src/components/Task/TaskForm/ChecklistField.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
verticalListSortingStrategy,
1515
} from '@dnd-kit/sortable';
1616
import { CSS } from '@dnd-kit/utilities';
17-
import { Check, GripVertical, Plus, Trash2 } from 'lucide-react';
17+
import { Check, GripVertical, ListChecks, Plus, Trash2 } from 'lucide-react';
1818
import { useCallback, useEffect, useRef, useState, type CSSProperties, type ReactNode } from 'react';
1919
import {
2020
applyMarkdownKeyboardShortcut,
@@ -28,7 +28,7 @@ import {
2828
type Task,
2929
} from '@mindwtr/core';
3030
import { cn } from '../../../lib/utils';
31-
import { taskEditorLabelClassName } from '../task-editor-label';
31+
import { TaskEditorFieldLabel } from '../task-editor-label';
3232
import {
3333
captureScrollSnapshot,
3434
focusElementWithoutScroll,
@@ -365,7 +365,7 @@ export function ChecklistField({
365365
}
366366
}}
367367
>
368-
<label className={taskEditorLabelClassName}>{t('taskEdit.checklist')}</label>
368+
<TaskEditorFieldLabel icon={ListChecks}>{t('taskEdit.checklist')}</TaskEditorFieldLabel>
369369
<div className="space-y-2 pr-3">
370370
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleChecklistDragEnd}>
371371
<SortableContext items={checklistItems.map((item) => item.id)} strategy={verticalListSortingStrategy}>
@@ -562,9 +562,9 @@ export function ChecklistField({
562562
focusChecklistIndex(nextList.length - 1, event.currentTarget);
563563
}
564564
}}
565-
className="flex items-center gap-1 text-xs font-medium text-info hover:text-info/80"
565+
className="inline-flex h-7 items-center gap-1.5 rounded-md border border-primary/30 px-2.5 text-xs font-medium text-primary transition-colors hover:bg-primary/10"
566566
>
567-
<Plus className="w-3 h-3" />
567+
<Plus className="w-3.5 h-3.5" aria-hidden="true" />
568568
{t('taskEdit.addItem')}
569569
</button>
570570
{(checklistDraft || []).length > 0 && (

apps/desktop/src/components/Task/TaskInput.test.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,8 @@ describe('TaskInput autocomplete', () => {
213213
for (const name of ['/link:<url>', '/energy:<level>', '/priority:<level>', '/area:<name>', '/reference', '/archived', '/*']) {
214214
expect(getByRole('option', { name })).toBeInTheDocument();
215215
}
216+
// Only /priority:<level> suggestions carry the decorative flag.
217+
expect(document.querySelectorAll('[data-priority-flag]')).toHaveLength(0);
216218
});
217219

218220
it('suggests the valid energy levels after /energy: and inserts the canonical token', async () => {
@@ -250,6 +252,19 @@ describe('TaskInput autocomplete', () => {
250252
'/priority:urgent',
251253
]);
252254

255+
// Each level suggestion carries the canonical decorative flag; other
256+
// command suggestions keep their plain label markup.
257+
const priorityFlagColors = {
258+
low: '#3b82f6',
259+
medium: '#ca8a04',
260+
high: '#f97316',
261+
urgent: '#dc2626',
262+
} as const;
263+
for (const [level, color] of Object.entries(priorityFlagColors)) {
264+
expect(getByRole('option', { name: `/priority:${level}` }).querySelector(`[data-priority-flag="${level}"]`))
265+
.toHaveAttribute('stroke', color);
266+
}
267+
253268
fireEvent.keyDown(input, { key: 'ArrowDown' });
254269
fireEvent.keyDown(input, { key: 'Enter' });
255270

@@ -366,7 +381,7 @@ describe('TaskInput autocomplete', () => {
366381

367382
expect(getAllByRole('option').map((option) => option.textContent)).toEqual([
368383
'Launch',
369-
'Create Project "La"',
384+
'Create Project "La"',
370385
]);
371386
fireEvent.keyDown(input, { key: 'Tab' });
372387

apps/desktop/src/components/Task/TaskInput.tsx

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import { useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from 'react';
22
import type { ClipboardEventHandler, KeyboardEventHandler, RefObject } from 'react';
3-
import { resolveFeatureFlags, useTaskStore, type Area, type Project } from '@mindwtr/core';
3+
import { resolveFeatureFlags, useTaskStore, type Area, type Project, type TaskPriority } from '@mindwtr/core';
44
import { cn } from '../../lib/utils';
55
import {
66
compareAutocompleteLabels,
77
matchesAutocompleteQuery,
88
normalizeAutocompleteTokens,
99
} from './token-autocomplete';
10+
import { PriorityFlag } from './PriorityFlag';
11+
import { Sparkles } from 'lucide-react';
1012

1113
type TriggerType = 'project' | 'context' | 'tag' | 'area' | 'person' | 'command';
1214
type SlashCommand =
@@ -43,7 +45,7 @@ type Option =
4345
| { kind: 'tag'; label: string; value: string }
4446
| { kind: 'area'; label: string; value: string; id: string }
4547
| { kind: 'person'; label: string; value: string }
46-
| { kind: 'command'; label: string; value: string; command: SlashCommand; requiresArgument: boolean };
48+
| { kind: 'command'; label: string; value: string; command: SlashCommand; requiresArgument: boolean; priority?: TaskPriority };
4749

4850
// WebKit does not scroll an input to a caret set via setSelectionRange, so we
4951
// measure the caret's pixel offset and adjust scrollLeft ourselves (LTR only).
@@ -127,7 +129,7 @@ const SLASH_COMMANDS: Array<{
127129
// The parser only accepts these exact tokens for /energy: — suggest them so
128130
// a partial token like "l" never ends up in the task title.
129131
const ENERGY_LEVEL_VALUES = ['low', 'medium', 'high'];
130-
const PRIORITY_VALUES = ['low', 'medium', 'high', 'urgent'];
132+
const PRIORITY_VALUES: TaskPriority[] = ['low', 'medium', 'high', 'urgent'];
131133

132134
function getSlashCommandOptions(query: string, prioritiesEnabled: boolean): Option[] {
133135
const separatorIndex = query.indexOf(':');
@@ -155,6 +157,7 @@ function getSlashCommandOptions(query: string, prioritiesEnabled: boolean): Opti
155157
value: level,
156158
command: 'priority' as const,
157159
requiresArgument: true,
160+
priority: level,
158161
}));
159162
}
160163

@@ -671,7 +674,10 @@ export function TaskInput({
671674
: 'hover:bg-muted/50'
672675
)}
673676
>
674-
{option.kind === 'create' ? `✨ ${option.label}` : option.label}
677+
{option.kind === 'create' ? <span className="inline-flex items-center gap-1.5"><Sparkles className="h-3.5 w-3.5" aria-hidden="true" />{option.label}</span>
678+
: option.kind === 'command' && option.priority
679+
? <span className="inline-flex items-center gap-1.5"><PriorityFlag priority={option.priority} />{option.label}</span>
680+
: option.label}
675681
</button>
676682
))}
677683
</div>

0 commit comments

Comments
 (0)