@tuturuuu/ui 0.32.0 → 0.33.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.
- package/CHANGELOG.md +49 -0
- package/biome.json +1 -1
- package/package.json +53 -51
- package/src/components/ui/calendar-app/components/calendar-sync-attention-button.tsx +58 -24
- package/src/components/ui/calendar-app/components/calendar-sync-recovery.test.tsx +57 -0
- package/src/components/ui/calendar-app/components/calendar-sync-recovery.tsx +28 -14
- package/src/components/ui/calendar-app/components/refresh-calendar-sync-status.test.tsx +321 -0
- package/src/components/ui/calendar-app/components/refresh-calendar-sync-status.ts +182 -0
- package/src/components/ui/calendar-app/components/use-calendar-connections-manager.ts +12 -14
- package/src/components/ui/chat/ai-message-parts.test.tsx +47 -0
- package/src/components/ui/chat/ai-message-parts.tsx +23 -16
- package/src/components/ui/chat/ai-message-render-utils.ts +19 -1
- package/src/components/ui/commercial-comparison-style.ts +104 -0
- package/src/components/ui/commercial-comparison-table.tsx +159 -0
- package/src/components/ui/commercial-comparison-toolbar.tsx +205 -0
- package/src/components/ui/commercial-comparison.test.tsx +86 -0
- package/src/components/ui/commercial-comparison.tsx +224 -0
- package/src/components/ui/comparison-value.test.tsx +32 -0
- package/src/components/ui/comparison-value.tsx +72 -0
- package/src/components/ui/custom/__tests__/report-preview-pagination-progress.test.ts +129 -0
- package/src/components/ui/custom/__tests__/report-preview-viewport.test.tsx +56 -0
- package/src/components/ui/custom/education/modules/resources/pdf-viewer.tsx +13 -2
- package/src/components/ui/custom/report-preview-pagination.ts +5 -3
- package/src/components/ui/custom/report-preview-viewport.tsx +40 -0
- package/src/components/ui/custom/report-preview.tsx +2 -3
- package/src/components/ui/finance/categories-tags-tabs.tsx +3 -0
- package/src/components/ui/finance/invoices/columns.tsx +2 -0
- package/src/components/ui/finance/invoices/components/invoice-checkout-summary.tsx +6 -3
- package/src/components/ui/finance/invoices/components/invoice-data-state.test.tsx +27 -0
- package/src/components/ui/finance/invoices/components/invoice-data-state.tsx +34 -0
- package/src/components/ui/finance/invoices/hooks/use-invoice-customer-search.test.tsx +89 -0
- package/src/components/ui/finance/invoices/hooks/use-invoice-customer-search.ts +117 -0
- package/src/components/ui/finance/invoices/hooks.ts +5 -110
- package/src/components/ui/finance/invoices/internal-api.test.ts +122 -0
- package/src/components/ui/finance/invoices/internal-api.ts +19 -1
- package/src/components/ui/finance/invoices/invoice-discount.test.tsx +107 -0
- package/src/components/ui/finance/invoices/invoice-discount.ts +12 -0
- package/src/components/ui/finance/invoices/invoice-history-filters.tsx +121 -0
- package/src/components/ui/finance/invoices/invoice-history-pagination.tsx +81 -0
- package/src/components/ui/finance/invoices/invoice-history-row.tsx +159 -0
- package/src/components/ui/finance/invoices/invoice-history.test.tsx +170 -0
- package/src/components/ui/finance/invoices/invoice-history.tsx +300 -0
- package/src/components/ui/finance/invoices/invoice-page.tsx +18 -2
- package/src/components/ui/finance/invoices/invoiceId/full-invoice-template.tsx +3 -7
- package/src/components/ui/finance/invoices/invoiceId/invoice-details-page.test.tsx +16 -0
- package/src/components/ui/finance/invoices/invoiceId/invoice-details-page.tsx +9 -0
- package/src/components/ui/finance/invoices/invoices-table.tsx +3 -0
- package/src/components/ui/finance/invoices/promotion-form.tsx +5 -4
- package/src/components/ui/finance/invoices/query-invalidation.ts +1 -0
- package/src/components/ui/finance/invoices/row-actions.tsx +35 -4
- package/src/components/ui/finance/invoices/standard-invoice.tsx +27 -16
- package/src/components/ui/finance/invoices/subscription-invoice-props.ts +20 -0
- package/src/components/ui/finance/invoices/subscription-invoice.test.tsx +71 -4
- package/src/components/ui/finance/invoices/subscription-invoice.tsx +44 -45
- package/src/components/ui/finance/shared/charts/category-breakdown-chart-body.tsx +73 -80
- package/src/components/ui/finance/shared/charts/category-breakdown-chart-controls.tsx +3 -1
- package/src/components/ui/finance/shared/charts/category-breakdown-chart-legend.test.tsx +30 -0
- package/src/components/ui/finance/shared/charts/category-breakdown-chart-legend.tsx +31 -24
- package/src/components/ui/finance/shared/charts/category-breakdown-chart-tooltip.tsx +9 -6
- package/src/components/ui/finance/shared/charts/category-breakdown-chart-types.ts +2 -0
- package/src/components/ui/finance/shared/charts/category-breakdown-chart-utils.test.ts +59 -0
- package/src/components/ui/finance/shared/charts/category-breakdown-chart-utils.ts +51 -37
- package/src/components/ui/finance/shared/charts/category-breakdown-chart.tsx +37 -7
- package/src/components/ui/public-workspace-prices.tsx +13 -0
- package/src/components/ui/pwa-status.test.tsx +13 -2
- package/src/components/ui/pwa-status.tsx +15 -3
- package/src/components/ui/text-editor/editor-readonly-transition.test.tsx +86 -0
- package/src/components/ui/text-editor/editor.tsx +11 -3
- package/src/components/ui/text-editor/task-item-checkbox-view.test.tsx +188 -65
- package/src/components/ui/text-editor/task-item-checkbox-view.tsx +178 -164
- package/src/components/ui/text-editor/task-item-status-picker.tsx +130 -0
- package/src/hooks/__tests__/use-calendar-sync.test.tsx +20 -1
- package/src/hooks/__tests__/use-notifications-subscription.test.tsx +99 -180
- package/src/hooks/calendar-provider-sync.ts +33 -0
- package/src/hooks/notification-subscription.ts +74 -0
- package/src/hooks/use-calendar-sync.tsx +11 -14
- package/src/hooks/use-calendar.tsx +2 -2
- package/src/hooks/use-notifications.ts +5 -141
|
@@ -1,85 +1,208 @@
|
|
|
1
|
-
import { fireEvent, render, screen } from '@testing-library/react';
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
1
|
+
import { act, fireEvent, render, screen } from '@testing-library/react';
|
|
2
|
+
import { TaskList } from '@tiptap/extension-list';
|
|
3
|
+
import { type Editor, EditorContent, useEditor } from '@tiptap/react';
|
|
4
|
+
import StarterKit from '@tiptap/starter-kit';
|
|
5
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
6
|
+
import { TaskItemCheckbox } from './task-item-checkbox-extension';
|
|
7
|
+
import { TaskItemStatusPicker } from './task-item-status-picker';
|
|
5
8
|
|
|
6
|
-
vi.mock('
|
|
7
|
-
|
|
9
|
+
vi.mock('next-intl', () => ({ useTranslations: () => (key: string) => key }));
|
|
10
|
+
vi.mock('./task-item-checkbox', async (importOriginal) => ({
|
|
11
|
+
...(await importOriginal<typeof import('./task-item-checkbox')>()),
|
|
12
|
+
useMentionedTaskStatuses: () => ({ data: [] }),
|
|
8
13
|
}));
|
|
9
14
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const setNodeMarkup = vi.fn();
|
|
21
|
-
const command = vi.fn(
|
|
22
|
-
(
|
|
23
|
-
callback: (props: {
|
|
24
|
-
tr: { setNodeMarkup: typeof setNodeMarkup };
|
|
25
|
-
}) => boolean
|
|
26
|
-
) => callback({ tr: { setNodeMarkup } })
|
|
27
|
-
);
|
|
28
|
-
|
|
29
|
-
const props = {
|
|
30
|
-
node: {
|
|
31
|
-
attrs: { checked: false },
|
|
32
|
-
descendants: vi.fn(),
|
|
33
|
-
},
|
|
34
|
-
getPos: () => 4,
|
|
35
|
-
editor: {
|
|
36
|
-
isEditable: true,
|
|
37
|
-
commands: { command },
|
|
38
|
-
},
|
|
39
|
-
} as unknown as NodeViewProps;
|
|
40
|
-
|
|
41
|
-
return { command, props, setNodeMarkup };
|
|
15
|
+
let editor: Editor;
|
|
16
|
+
function Harness({ editable = true }: { editable?: boolean }) {
|
|
17
|
+
const instance = useEditor({
|
|
18
|
+
extensions: [StarterKit, TaskList, TaskItemCheckbox],
|
|
19
|
+
content:
|
|
20
|
+
'<ul data-type="taskList"><li data-type="taskItem" data-checked="false"><p>Checklist text</p></li></ul>',
|
|
21
|
+
editable,
|
|
22
|
+
});
|
|
23
|
+
if (instance) editor = instance;
|
|
24
|
+
return <EditorContent editor={instance} />;
|
|
42
25
|
}
|
|
26
|
+
const checkbox = () => screen.getByRole('checkbox', { name: 'status' });
|
|
27
|
+
const checked = () => editor.state.doc.firstChild?.firstChild?.attrs.checked;
|
|
28
|
+
afterEach(() => vi.useRealTimers());
|
|
43
29
|
|
|
44
|
-
describe('
|
|
45
|
-
it('cycles
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const checkbox = screen.getByRole('checkbox', {
|
|
50
|
-
name: 'Task item status',
|
|
30
|
+
describe('checklist editor interactions', () => {
|
|
31
|
+
it('cycles through all three persisted states with normal clicks', async () => {
|
|
32
|
+
await act(async () => {
|
|
33
|
+
render(<Harness />);
|
|
51
34
|
});
|
|
52
|
-
|
|
35
|
+
for (const state of ['indeterminate', true, false]) {
|
|
36
|
+
await act(async () => {
|
|
37
|
+
fireEvent.click(checkbox());
|
|
38
|
+
});
|
|
39
|
+
expect(checked()).toBe(state);
|
|
40
|
+
expect(checkbox()).toHaveAttribute(
|
|
41
|
+
'aria-checked',
|
|
42
|
+
state === 'indeterminate' ? 'mixed' : String(state)
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
53
46
|
|
|
54
|
-
|
|
47
|
+
it('enables the status picker after task collaboration finishes syncing', async () => {
|
|
48
|
+
await act(async () => {
|
|
49
|
+
render(<Harness editable={false} />);
|
|
50
|
+
});
|
|
51
|
+
await act(async () => {
|
|
52
|
+
editor.setEditable(true);
|
|
53
|
+
});
|
|
54
|
+
expect(checkbox()).toHaveAttribute('aria-disabled', 'false');
|
|
55
|
+
fireEvent.keyDown(checkbox(), { key: 'ArrowDown' });
|
|
56
|
+
fireEvent.click(screen.getByRole('button', { name: 'in_progress' }));
|
|
57
|
+
expect(checked()).toBe('indeterminate');
|
|
58
|
+
for (const state of [true, false, 'indeterminate']) {
|
|
59
|
+
await act(async () => {
|
|
60
|
+
fireEvent.click(checkbox());
|
|
61
|
+
});
|
|
62
|
+
expect(checked()).toBe(state);
|
|
63
|
+
}
|
|
64
|
+
await act(async () => {
|
|
65
|
+
editor.setEditable(false);
|
|
66
|
+
});
|
|
67
|
+
expect(checkbox()).toHaveAttribute('aria-disabled', 'true');
|
|
68
|
+
});
|
|
55
69
|
|
|
56
|
-
|
|
57
|
-
|
|
70
|
+
it('follows undo, redo, and external document updates', async () => {
|
|
71
|
+
await act(async () => {
|
|
72
|
+
render(<Harness />);
|
|
73
|
+
});
|
|
74
|
+
await act(async () => {
|
|
75
|
+
fireEvent.click(checkbox());
|
|
76
|
+
});
|
|
77
|
+
await act(async () => {
|
|
78
|
+
editor.commands.undo();
|
|
79
|
+
});
|
|
80
|
+
expect(checkbox()).toHaveAttribute('aria-checked', 'false');
|
|
81
|
+
await act(async () => {
|
|
82
|
+
editor.commands.redo();
|
|
58
83
|
});
|
|
59
|
-
expect(checkbox).toHaveAttribute('aria-checked', 'mixed');
|
|
84
|
+
expect(checkbox()).toHaveAttribute('aria-checked', 'mixed');
|
|
85
|
+
await act(async () => {
|
|
86
|
+
editor.view.dispatch(
|
|
87
|
+
editor.state.tr.setNodeMarkup(1, undefined, { checked: true })
|
|
88
|
+
);
|
|
89
|
+
});
|
|
90
|
+
expect(checkbox()).toHaveAttribute('aria-checked', 'true');
|
|
60
91
|
});
|
|
61
92
|
|
|
62
|
-
it('
|
|
63
|
-
|
|
64
|
-
|
|
93
|
+
it('opens a delayed picker and selects a state without moving text selection', async () => {
|
|
94
|
+
vi.useFakeTimers();
|
|
95
|
+
await act(async () => {
|
|
96
|
+
render(<Harness />);
|
|
97
|
+
});
|
|
98
|
+
await act(async () => {
|
|
99
|
+
editor.commands.setTextSelection(4);
|
|
100
|
+
});
|
|
101
|
+
fireEvent.pointerEnter(checkbox().parentElement!);
|
|
102
|
+
await act(async () => {
|
|
103
|
+
vi.advanceTimersByTime(499);
|
|
104
|
+
});
|
|
105
|
+
expect(
|
|
106
|
+
screen.queryByRole('button', { name: 'completed' })
|
|
107
|
+
).not.toBeInTheDocument();
|
|
108
|
+
await act(async () => {
|
|
109
|
+
vi.advanceTimersByTime(1);
|
|
110
|
+
});
|
|
111
|
+
fireEvent.click(screen.getByRole('button', { name: 'completed' }));
|
|
112
|
+
expect(checked()).toBe(true);
|
|
113
|
+
expect(editor.state.selection.from).toBe(4);
|
|
114
|
+
});
|
|
65
115
|
|
|
66
|
-
|
|
67
|
-
|
|
116
|
+
it('cancels a brief hover and closes the picker with Escape', async () => {
|
|
117
|
+
vi.useFakeTimers();
|
|
118
|
+
await act(async () => {
|
|
119
|
+
render(<Harness />);
|
|
120
|
+
});
|
|
121
|
+
fireEvent.pointerEnter(checkbox().parentElement!);
|
|
122
|
+
await act(async () => {
|
|
123
|
+
vi.advanceTimersByTime(200);
|
|
124
|
+
});
|
|
125
|
+
fireEvent.pointerLeave(checkbox().parentElement!);
|
|
126
|
+
await act(async () => {
|
|
127
|
+
vi.advanceTimersByTime(500);
|
|
68
128
|
});
|
|
129
|
+
expect(
|
|
130
|
+
screen.queryByRole('button', { name: 'completed' })
|
|
131
|
+
).not.toBeInTheDocument();
|
|
132
|
+
fireEvent.keyDown(checkbox(), { key: 'ArrowDown' });
|
|
133
|
+
fireEvent.keyDown(screen.getByRole('button', { name: 'completed' }), {
|
|
134
|
+
key: 'Escape',
|
|
135
|
+
});
|
|
136
|
+
expect(
|
|
137
|
+
screen.queryByRole('button', { name: 'completed' })
|
|
138
|
+
).not.toBeInTheDocument();
|
|
139
|
+
});
|
|
69
140
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
141
|
+
it('keeps indeterminate state when serializing and reloading the checklist', async () => {
|
|
142
|
+
await act(async () => {
|
|
143
|
+
render(<Harness />);
|
|
144
|
+
});
|
|
145
|
+
await act(async () => {
|
|
146
|
+
fireEvent.click(checkbox());
|
|
147
|
+
});
|
|
148
|
+
const html = editor.getHTML();
|
|
149
|
+
await act(async () => {
|
|
150
|
+
editor.commands.setContent(html);
|
|
151
|
+
});
|
|
152
|
+
expect(checked()).toBe('indeterminate');
|
|
153
|
+
expect(checkbox()).toHaveAttribute('aria-checked', 'mixed');
|
|
154
|
+
});
|
|
73
155
|
|
|
74
|
-
|
|
75
|
-
|
|
156
|
+
it('opens the picker from the keyboard and prevents read-only changes', async () => {
|
|
157
|
+
let unmount = () => {};
|
|
158
|
+
await act(async () => {
|
|
159
|
+
({ unmount } = render(<Harness />));
|
|
76
160
|
});
|
|
77
|
-
|
|
78
|
-
|
|
161
|
+
fireEvent.keyDown(checkbox(), { key: 'ArrowDown' });
|
|
162
|
+
expect(
|
|
163
|
+
screen.getByRole('button', { name: 'in_progress' })
|
|
164
|
+
).toBeInTheDocument();
|
|
165
|
+
unmount();
|
|
166
|
+
await act(async () => {
|
|
167
|
+
render(<Harness editable={false} />);
|
|
79
168
|
});
|
|
80
|
-
|
|
81
|
-
|
|
169
|
+
await act(async () => {
|
|
170
|
+
fireEvent.click(checkbox());
|
|
82
171
|
});
|
|
83
|
-
|
|
172
|
+
fireEvent.keyDown(checkbox(), { key: 'ArrowDown' });
|
|
173
|
+
expect(checked()).toBe(false);
|
|
174
|
+
expect(
|
|
175
|
+
screen.queryByRole('button', { name: 'in_progress' })
|
|
176
|
+
).not.toBeInTheDocument();
|
|
84
177
|
});
|
|
178
|
+
it.each([200, 500])(
|
|
179
|
+
'clears a pending or open picker when disabled after %i ms',
|
|
180
|
+
async (hoverMs) => {
|
|
181
|
+
vi.useFakeTimers();
|
|
182
|
+
const picker = (disabled: boolean) => (
|
|
183
|
+
<TaskItemStatusPicker
|
|
184
|
+
state={false}
|
|
185
|
+
disabled={disabled}
|
|
186
|
+
onSelect={vi.fn()}
|
|
187
|
+
>
|
|
188
|
+
<button type="button">Toggle</button>
|
|
189
|
+
</TaskItemStatusPicker>
|
|
190
|
+
);
|
|
191
|
+
const { rerender } = render(picker(false));
|
|
192
|
+
fireEvent.pointerEnter(
|
|
193
|
+
screen.getByRole('button', { name: 'Toggle' }).parentElement!
|
|
194
|
+
);
|
|
195
|
+
await act(async () => {
|
|
196
|
+
vi.advanceTimersByTime(hoverMs);
|
|
197
|
+
});
|
|
198
|
+
rerender(picker(true));
|
|
199
|
+
rerender(picker(false));
|
|
200
|
+
await act(async () => {
|
|
201
|
+
vi.advanceTimersByTime(1000);
|
|
202
|
+
});
|
|
203
|
+
expect(
|
|
204
|
+
screen.queryByRole('button', { name: 'completed' })
|
|
205
|
+
).not.toBeInTheDocument();
|
|
206
|
+
}
|
|
207
|
+
);
|
|
85
208
|
});
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
import type { NodeViewProps } from '@tiptap/react';
|
|
4
4
|
import { NodeViewContent } from '@tiptap/react';
|
|
5
5
|
import { cn } from '@tuturuuu/utils/format';
|
|
6
|
-
import {
|
|
6
|
+
import { useTranslations } from 'next-intl';
|
|
7
|
+
import { useCallback, useMemo, useSyncExternalStore } from 'react';
|
|
7
8
|
import { DraggableNodeContainer } from './draggable-node-container';
|
|
8
9
|
import {
|
|
9
10
|
areAllMentionedTasksCompleted,
|
|
@@ -14,16 +15,31 @@ import {
|
|
|
14
15
|
type TriStateChecked,
|
|
15
16
|
useMentionedTaskStatuses,
|
|
16
17
|
} from './task-item-checkbox';
|
|
18
|
+
import { TaskItemStatusPicker } from './task-item-status-picker';
|
|
17
19
|
|
|
18
20
|
export function TaskItemCheckboxContent({
|
|
19
21
|
node,
|
|
20
22
|
getPos,
|
|
21
23
|
editor,
|
|
22
24
|
}: NodeViewProps) {
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
+
const t = useTranslations('common.checklist');
|
|
26
|
+
const subscribe = useCallback(
|
|
27
|
+
(notify: () => void) => {
|
|
28
|
+
// setEditable emits update without changing the document/node view.
|
|
29
|
+
editor.on('update', notify);
|
|
30
|
+
editor.on('transaction', notify);
|
|
31
|
+
return () => {
|
|
32
|
+
editor.off('update', notify);
|
|
33
|
+
editor.off('transaction', notify);
|
|
34
|
+
};
|
|
35
|
+
},
|
|
36
|
+
[editor]
|
|
37
|
+
);
|
|
38
|
+
const isEditable = useSyncExternalStore(
|
|
39
|
+
subscribe,
|
|
40
|
+
() => editor.isEditable,
|
|
41
|
+
() => false
|
|
25
42
|
);
|
|
26
|
-
|
|
27
43
|
const taskMentionIds = useMemo(() => {
|
|
28
44
|
return extractTaskMentionIds(node);
|
|
29
45
|
}, [node]);
|
|
@@ -40,187 +56,185 @@ export function TaskItemCheckboxContent({
|
|
|
40
56
|
|
|
41
57
|
const checkboxState = useMemo((): TriStateChecked => {
|
|
42
58
|
return resolveCheckboxState({
|
|
43
|
-
manualOverride,
|
|
59
|
+
manualOverride: null,
|
|
44
60
|
nodeChecked: node.attrs.checked,
|
|
45
61
|
allMentionedTasksCompleted,
|
|
46
62
|
});
|
|
47
|
-
}, [
|
|
63
|
+
}, [node.attrs.checked, allMentionedTasksCompleted]);
|
|
48
64
|
|
|
49
65
|
const isChecked = checkboxState === true;
|
|
50
66
|
const isIndeterminate = checkboxState === 'indeterminate';
|
|
51
67
|
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
68
|
+
const setCheckboxState = useCallback(
|
|
69
|
+
(
|
|
70
|
+
checked: TriStateChecked | ((current: TriStateChecked) => TriStateChecked)
|
|
71
|
+
) => {
|
|
72
|
+
if (!editor.isEditable) return;
|
|
73
|
+
const pos = getPos();
|
|
74
|
+
if (typeof pos !== 'number') return;
|
|
75
|
+
editor.commands.command(({ tr }) => {
|
|
76
|
+
const currentNode = tr.doc.nodeAt(pos);
|
|
77
|
+
if (currentNode?.type.name !== 'taskItem') return false;
|
|
78
|
+
// Node view props can lag behind a transaction (e.g. a picker click).
|
|
79
|
+
// Cycle from the document so rapid clicks never repeat a stale state.
|
|
80
|
+
const current = resolveCheckboxState({
|
|
81
|
+
manualOverride: null,
|
|
82
|
+
nodeChecked: currentNode.attrs.checked,
|
|
83
|
+
allMentionedTasksCompleted,
|
|
84
|
+
});
|
|
85
|
+
tr.setNodeMarkup(pos, undefined, {
|
|
86
|
+
...currentNode.attrs,
|
|
87
|
+
checked: typeof checked === 'function' ? checked(current) : checked,
|
|
88
|
+
});
|
|
89
|
+
return true;
|
|
66
90
|
});
|
|
67
|
-
return true;
|
|
68
|
-
});
|
|
69
|
-
}, [editor, getPos, node.attrs, checkboxState]);
|
|
70
|
-
|
|
71
|
-
const handleCheckboxPointerDown = useCallback(
|
|
72
|
-
(event: React.PointerEvent<HTMLButtonElement>) => {
|
|
73
|
-
if (!event.isPrimary || event.button !== 0) return;
|
|
74
|
-
event.preventDefault();
|
|
75
|
-
event.stopPropagation();
|
|
76
|
-
handleCheckboxCycle();
|
|
77
|
-
},
|
|
78
|
-
[handleCheckboxCycle]
|
|
79
|
-
);
|
|
80
|
-
|
|
81
|
-
const handleCheckboxKeyDown = useCallback(
|
|
82
|
-
(event: React.KeyboardEvent<HTMLButtonElement>) => {
|
|
83
|
-
if (event.key !== 'Enter' && event.key !== ' ') return;
|
|
84
|
-
event.preventDefault();
|
|
85
|
-
event.stopPropagation();
|
|
86
|
-
handleCheckboxCycle();
|
|
87
91
|
},
|
|
88
|
-
[
|
|
92
|
+
[editor, getPos, allMentionedTasksCompleted]
|
|
89
93
|
);
|
|
90
94
|
|
|
91
95
|
return (
|
|
92
|
-
<div className="flex items-start gap-2">
|
|
96
|
+
<div className="flex min-w-0 flex-1 items-start gap-2">
|
|
93
97
|
<div
|
|
94
98
|
className="task-list-checkbox-label flex shrink-0 select-none pt-2.5"
|
|
95
99
|
contentEditable={false}
|
|
96
100
|
>
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
aria-label="Task item status"
|
|
102
|
-
aria-checked={isIndeterminate ? 'mixed' : isChecked}
|
|
103
|
-
aria-disabled={!editor.isEditable}
|
|
104
|
-
onPointerDown={handleCheckboxPointerDown}
|
|
105
|
-
onKeyDown={handleCheckboxKeyDown}
|
|
106
|
-
className={cn(
|
|
107
|
-
'task-list-checkbox flex size-5 items-center justify-center',
|
|
108
|
-
'cursor-pointer rounded-md border-2 bg-background',
|
|
109
|
-
'transition-[color,background-color,border-color,box-shadow,transform] duration-150 active:scale-95',
|
|
110
|
-
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/35 focus-visible:ring-offset-2',
|
|
111
|
-
!editor.isEditable && 'cursor-default opacity-70',
|
|
112
|
-
!completedTaskColor && [
|
|
113
|
-
'border-input',
|
|
114
|
-
'hover:scale-105 hover:border-dynamic-gray hover:bg-dynamic-gray/10',
|
|
115
|
-
'focus-visible:border-dynamic-gray',
|
|
116
|
-
isChecked &&
|
|
117
|
-
'border-dynamic-green bg-dynamic-green/20 hover:border-dynamic-green hover:bg-dynamic-green/10 focus-visible:border-dynamic-green focus-visible:ring-dynamic-green/30',
|
|
118
|
-
isIndeterminate &&
|
|
119
|
-
'border-dynamic-orange bg-dynamic-orange/20 hover:border-dynamic-orange hover:bg-dynamic-orange/10 focus-visible:border-dynamic-orange focus-visible:ring-dynamic-orange/30',
|
|
120
|
-
],
|
|
121
|
-
completedTaskColor === 'red' && [
|
|
122
|
-
'border-dynamic-red/50',
|
|
123
|
-
'hover:scale-105 hover:border-dynamic-red hover:bg-dynamic-red/10',
|
|
124
|
-
'focus:border-dynamic-red focus:outline-none focus:ring-2 focus:ring-dynamic-red/30 focus:ring-offset-2',
|
|
125
|
-
(isChecked || isIndeterminate) &&
|
|
126
|
-
'border-dynamic-red bg-dynamic-red/20',
|
|
127
|
-
],
|
|
128
|
-
completedTaskColor === 'orange' && [
|
|
129
|
-
'border-dynamic-orange/50',
|
|
130
|
-
'hover:scale-105 hover:border-dynamic-orange hover:bg-dynamic-orange/10',
|
|
131
|
-
'focus:border-dynamic-orange focus:outline-none focus:ring-2 focus:ring-dynamic-orange/30 focus:ring-offset-2',
|
|
132
|
-
(isChecked || isIndeterminate) &&
|
|
133
|
-
'border-dynamic-orange bg-dynamic-orange/20',
|
|
134
|
-
],
|
|
135
|
-
completedTaskColor === 'yellow' && [
|
|
136
|
-
'border-dynamic-yellow/50',
|
|
137
|
-
'hover:scale-105 hover:border-dynamic-yellow hover:bg-dynamic-yellow/10',
|
|
138
|
-
'focus:border-dynamic-yellow focus:outline-none focus:ring-2 focus:ring-dynamic-yellow/30 focus:ring-offset-2',
|
|
139
|
-
(isChecked || isIndeterminate) &&
|
|
140
|
-
'border-dynamic-yellow bg-dynamic-yellow/20',
|
|
141
|
-
],
|
|
142
|
-
completedTaskColor === 'green' && [
|
|
143
|
-
'border-dynamic-green/50',
|
|
144
|
-
'hover:scale-105 hover:border-dynamic-green hover:bg-dynamic-green/10',
|
|
145
|
-
'focus:border-dynamic-green focus:outline-none focus:ring-2 focus:ring-dynamic-green/30 focus:ring-offset-2',
|
|
146
|
-
(isChecked || isIndeterminate) &&
|
|
147
|
-
'border-dynamic-green bg-dynamic-green/20',
|
|
148
|
-
],
|
|
149
|
-
completedTaskColor === 'cyan' && [
|
|
150
|
-
'border-dynamic-cyan/50',
|
|
151
|
-
'hover:scale-105 hover:border-dynamic-cyan hover:bg-dynamic-cyan/10',
|
|
152
|
-
'focus:border-dynamic-cyan focus:outline-none focus:ring-2 focus:ring-dynamic-cyan/30 focus:ring-offset-2',
|
|
153
|
-
(isChecked || isIndeterminate) &&
|
|
154
|
-
'border-dynamic-cyan bg-dynamic-cyan/20',
|
|
155
|
-
],
|
|
156
|
-
completedTaskColor === 'blue' && [
|
|
157
|
-
'border-dynamic-blue/50',
|
|
158
|
-
'hover:scale-105 hover:border-dynamic-blue hover:bg-dynamic-blue/10',
|
|
159
|
-
'focus:border-dynamic-blue focus:outline-none focus:ring-2 focus:ring-dynamic-blue/30 focus:ring-offset-2',
|
|
160
|
-
(isChecked || isIndeterminate) &&
|
|
161
|
-
'border-dynamic-blue bg-dynamic-blue/20',
|
|
162
|
-
],
|
|
163
|
-
completedTaskColor === 'purple' && [
|
|
164
|
-
'border-dynamic-purple/50',
|
|
165
|
-
'hover:scale-105 hover:border-dynamic-purple hover:bg-dynamic-purple/10',
|
|
166
|
-
'focus:border-dynamic-purple focus:outline-none focus:ring-2 focus:ring-dynamic-purple/30 focus:ring-offset-2',
|
|
167
|
-
(isChecked || isIndeterminate) &&
|
|
168
|
-
'border-dynamic-purple bg-dynamic-purple/20',
|
|
169
|
-
],
|
|
170
|
-
completedTaskColor === 'pink' && [
|
|
171
|
-
'border-dynamic-pink/50',
|
|
172
|
-
'hover:scale-105 hover:border-dynamic-pink hover:bg-dynamic-pink/10',
|
|
173
|
-
'focus:border-dynamic-pink focus:outline-none focus:ring-2 focus:ring-dynamic-pink/30 focus:ring-offset-2',
|
|
174
|
-
(isChecked || isIndeterminate) &&
|
|
175
|
-
'border-dynamic-pink bg-dynamic-pink/20',
|
|
176
|
-
]
|
|
177
|
-
)}
|
|
101
|
+
<TaskItemStatusPicker
|
|
102
|
+
state={checkboxState}
|
|
103
|
+
disabled={!isEditable}
|
|
104
|
+
onSelect={setCheckboxState}
|
|
178
105
|
>
|
|
179
|
-
{
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
106
|
+
{/* biome-ignore lint/a11y/useSemanticElements: a button avoids native checkbox state changes while ProseMirror owns the tri-state document update */}
|
|
107
|
+
<button
|
|
108
|
+
type="button"
|
|
109
|
+
role="checkbox"
|
|
110
|
+
aria-label={t('status')}
|
|
111
|
+
aria-keyshortcuts="ArrowDown"
|
|
112
|
+
aria-checked={isIndeterminate ? 'mixed' : isChecked}
|
|
113
|
+
aria-disabled={!isEditable}
|
|
114
|
+
onMouseDown={(event) => event.preventDefault()}
|
|
115
|
+
onClick={(event) => {
|
|
116
|
+
event.stopPropagation();
|
|
117
|
+
setCheckboxState(getNextTriState);
|
|
118
|
+
}}
|
|
119
|
+
className={cn(
|
|
120
|
+
'task-list-checkbox flex size-5 items-center justify-center',
|
|
121
|
+
'cursor-pointer rounded-md border-2 bg-background',
|
|
122
|
+
'transition-[color,background-color,border-color,box-shadow,transform] duration-150 active:scale-95',
|
|
123
|
+
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/35 focus-visible:ring-offset-2',
|
|
124
|
+
!isEditable && 'cursor-default opacity-70',
|
|
125
|
+
!completedTaskColor && [
|
|
126
|
+
'border-muted-foreground/50',
|
|
127
|
+
'hover:scale-105 hover:border-dynamic-gray hover:bg-dynamic-gray/10',
|
|
128
|
+
'focus-visible:border-dynamic-gray',
|
|
129
|
+
isChecked &&
|
|
130
|
+
'border-dynamic-green bg-dynamic-green/20 hover:border-dynamic-green hover:bg-dynamic-green/10 focus-visible:border-dynamic-green focus-visible:ring-dynamic-green/30',
|
|
131
|
+
isIndeterminate &&
|
|
132
|
+
'border-dynamic-orange bg-dynamic-orange/20 hover:border-dynamic-orange hover:bg-dynamic-orange/10 focus-visible:border-dynamic-orange focus-visible:ring-dynamic-orange/30',
|
|
133
|
+
],
|
|
134
|
+
completedTaskColor === 'red' && [
|
|
135
|
+
'border-dynamic-red/50',
|
|
136
|
+
'hover:scale-105 hover:border-dynamic-red hover:bg-dynamic-red/10',
|
|
137
|
+
'focus:border-dynamic-red focus:outline-none focus:ring-2 focus:ring-dynamic-red/30 focus:ring-offset-2',
|
|
138
|
+
(isChecked || isIndeterminate) &&
|
|
139
|
+
'border-dynamic-red bg-dynamic-red/20',
|
|
140
|
+
],
|
|
141
|
+
completedTaskColor === 'orange' && [
|
|
142
|
+
'border-dynamic-orange/50',
|
|
143
|
+
'hover:scale-105 hover:border-dynamic-orange hover:bg-dynamic-orange/10',
|
|
144
|
+
'focus:border-dynamic-orange focus:outline-none focus:ring-2 focus:ring-dynamic-orange/30 focus:ring-offset-2',
|
|
145
|
+
(isChecked || isIndeterminate) &&
|
|
146
|
+
'border-dynamic-orange bg-dynamic-orange/20',
|
|
147
|
+
],
|
|
148
|
+
completedTaskColor === 'yellow' && [
|
|
149
|
+
'border-dynamic-yellow/50',
|
|
150
|
+
'hover:scale-105 hover:border-dynamic-yellow hover:bg-dynamic-yellow/10',
|
|
151
|
+
'focus:border-dynamic-yellow focus:outline-none focus:ring-2 focus:ring-dynamic-yellow/30 focus:ring-offset-2',
|
|
152
|
+
(isChecked || isIndeterminate) &&
|
|
153
|
+
'border-dynamic-yellow bg-dynamic-yellow/20',
|
|
154
|
+
],
|
|
155
|
+
completedTaskColor === 'green' && [
|
|
156
|
+
'border-dynamic-green/50',
|
|
157
|
+
'hover:scale-105 hover:border-dynamic-green hover:bg-dynamic-green/10',
|
|
158
|
+
'focus:border-dynamic-green focus:outline-none focus:ring-2 focus:ring-dynamic-green/30 focus:ring-offset-2',
|
|
159
|
+
(isChecked || isIndeterminate) &&
|
|
160
|
+
'border-dynamic-green bg-dynamic-green/20',
|
|
161
|
+
],
|
|
162
|
+
completedTaskColor === 'cyan' && [
|
|
163
|
+
'border-dynamic-cyan/50',
|
|
164
|
+
'hover:scale-105 hover:border-dynamic-cyan hover:bg-dynamic-cyan/10',
|
|
165
|
+
'focus:border-dynamic-cyan focus:outline-none focus:ring-2 focus:ring-dynamic-cyan/30 focus:ring-offset-2',
|
|
166
|
+
(isChecked || isIndeterminate) &&
|
|
167
|
+
'border-dynamic-cyan bg-dynamic-cyan/20',
|
|
168
|
+
],
|
|
169
|
+
completedTaskColor === 'blue' && [
|
|
170
|
+
'border-dynamic-blue/50',
|
|
171
|
+
'hover:scale-105 hover:border-dynamic-blue hover:bg-dynamic-blue/10',
|
|
172
|
+
'focus:border-dynamic-blue focus:outline-none focus:ring-2 focus:ring-dynamic-blue/30 focus:ring-offset-2',
|
|
173
|
+
(isChecked || isIndeterminate) &&
|
|
174
|
+
'border-dynamic-blue bg-dynamic-blue/20',
|
|
175
|
+
],
|
|
176
|
+
completedTaskColor === 'purple' && [
|
|
177
|
+
'border-dynamic-purple/50',
|
|
178
|
+
'hover:scale-105 hover:border-dynamic-purple hover:bg-dynamic-purple/10',
|
|
179
|
+
'focus:border-dynamic-purple focus:outline-none focus:ring-2 focus:ring-dynamic-purple/30 focus:ring-offset-2',
|
|
180
|
+
(isChecked || isIndeterminate) &&
|
|
181
|
+
'border-dynamic-purple bg-dynamic-purple/20',
|
|
182
|
+
],
|
|
183
|
+
completedTaskColor === 'pink' && [
|
|
184
|
+
'border-dynamic-pink/50',
|
|
185
|
+
'hover:scale-105 hover:border-dynamic-pink hover:bg-dynamic-pink/10',
|
|
186
|
+
'focus:border-dynamic-pink focus:outline-none focus:ring-2 focus:ring-dynamic-pink/30 focus:ring-offset-2',
|
|
187
|
+
(isChecked || isIndeterminate) &&
|
|
188
|
+
'border-dynamic-pink bg-dynamic-pink/20',
|
|
189
|
+
]
|
|
190
|
+
)}
|
|
191
|
+
>
|
|
192
|
+
{isChecked && (
|
|
193
|
+
<svg
|
|
194
|
+
className={cn(
|
|
195
|
+
'h-3.5 w-3.5',
|
|
196
|
+
completedTaskColor ? 'text-foreground' : 'text-dynamic-green'
|
|
197
|
+
)}
|
|
198
|
+
viewBox="0 0 16 16"
|
|
199
|
+
fill="none"
|
|
200
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
201
|
+
aria-hidden="true"
|
|
202
|
+
>
|
|
203
|
+
<path
|
|
204
|
+
d="M4 8l2.5 2.5L12 5"
|
|
205
|
+
stroke="currentColor"
|
|
206
|
+
strokeWidth="2"
|
|
207
|
+
strokeLinecap="round"
|
|
208
|
+
strokeLinejoin="round"
|
|
209
|
+
/>
|
|
210
|
+
</svg>
|
|
211
|
+
)}
|
|
212
|
+
{isIndeterminate && (
|
|
213
|
+
<svg
|
|
214
|
+
className={cn(
|
|
215
|
+
'h-3.5 w-3.5',
|
|
216
|
+
completedTaskColor ? 'text-foreground' : 'text-dynamic-orange'
|
|
217
|
+
)}
|
|
218
|
+
viewBox="0 0 16 16"
|
|
219
|
+
fill="none"
|
|
220
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
221
|
+
aria-hidden="true"
|
|
222
|
+
>
|
|
223
|
+
<path
|
|
224
|
+
d="M4 8h8"
|
|
225
|
+
stroke="currentColor"
|
|
226
|
+
strokeWidth="2"
|
|
227
|
+
strokeLinecap="round"
|
|
228
|
+
/>
|
|
229
|
+
</svg>
|
|
230
|
+
)}
|
|
231
|
+
</button>
|
|
232
|
+
</TaskItemStatusPicker>
|
|
219
233
|
</div>
|
|
220
234
|
|
|
221
235
|
<NodeViewContent
|
|
222
236
|
className={cn(
|
|
223
|
-
'transition-opacity duration-150',
|
|
237
|
+
'min-w-0 flex-1 cursor-text caret-foreground transition-opacity duration-150',
|
|
224
238
|
isChecked && 'task-content-faded'
|
|
225
239
|
)}
|
|
226
240
|
/>
|