@tuturuuu/ui 0.30.1 → 0.31.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.
Files changed (99) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/biome.json +1 -1
  3. package/package.json +103 -57
  4. package/src/components/ui/calendar-app/calendar-client-page.test.tsx +57 -0
  5. package/src/components/ui/calendar-app/calendar-client-page.tsx +13 -4
  6. package/src/components/ui/calendar-app/calendar-page-shell.tsx +7 -1
  7. package/src/components/ui/calendar-app/components/calendar-connections-compact.tsx +47 -39
  8. package/src/components/ui/calendar-app/components/calendar-connections-settings-content.tsx +14 -0
  9. package/src/components/ui/calendar-app/components/calendar-connections-unified.tsx +4 -0
  10. package/src/components/ui/calendar-app/components/calendar-sync-attention-button.tsx +47 -0
  11. package/src/components/ui/calendar-app/components/calendar-sync-recovery.test.tsx +112 -0
  12. package/src/components/ui/calendar-app/components/calendar-sync-recovery.tsx +206 -0
  13. package/src/components/ui/calendar-app/components/calendar-types.ts +1 -1
  14. package/src/components/ui/calendar-app/components/use-calendar-connections-manager.ts +86 -90
  15. package/src/components/ui/custom/language-dropdown-item.tsx +6 -14
  16. package/src/components/ui/custom/nav-link.tsx +7 -7
  17. package/src/components/ui/custom/nav.tsx +15 -63
  18. package/src/components/ui/custom/notification-card.tsx +361 -0
  19. package/src/components/ui/custom/notification-list.tsx +178 -0
  20. package/src/components/ui/custom/notification-popover-client.tsx +12 -780
  21. package/src/components/ui/custom/notification-runtime.tsx +23 -0
  22. package/src/components/ui/custom/onboarding-settings-panel.tsx +8 -55
  23. package/src/components/ui/custom/satellite-account-switcher-menu.tsx +212 -0
  24. package/src/components/ui/custom/satellite-brand.tsx +122 -0
  25. package/src/components/ui/custom/satellite-content.tsx +148 -0
  26. package/src/components/ui/custom/satellite-footer-actions.tsx +350 -0
  27. package/src/components/ui/custom/satellite-language-item.tsx +26 -0
  28. package/src/components/ui/custom/satellite-navigation.tsx +86 -0
  29. package/src/components/ui/custom/satellite-notification-popover.tsx +272 -0
  30. package/src/components/ui/custom/satellite-onboarding-settings-panel.tsx +80 -0
  31. package/src/components/ui/custom/satellite-settings-dialog-shell.tsx +621 -0
  32. package/src/components/ui/custom/satellite-shell-utils.ts +163 -0
  33. package/src/components/ui/custom/satellite-shell.test.tsx +169 -0
  34. package/src/components/ui/custom/satellite-shell.tsx +50 -0
  35. package/src/components/ui/custom/satellite-theme-dropdown-items.tsx +92 -0
  36. package/src/components/ui/custom/satellite-user-menu-items.tsx +190 -0
  37. package/src/components/ui/custom/satellite-user-menu.tsx +93 -0
  38. package/src/components/ui/custom/satellite-workspace-section.tsx +46 -0
  39. package/src/components/ui/custom/settings/keyboard-shortcuts-settings.tsx +16 -0
  40. package/src/components/ui/custom/settings-dialog-shell.tsx +21 -600
  41. package/src/components/ui/custom/sidebar-context.tsx +5 -14
  42. package/src/components/ui/custom/sidebar-footer-actions.tsx +19 -322
  43. package/src/components/ui/custom/structure.tsx +14 -0
  44. package/src/components/ui/custom/system-language-dropdown-item.tsx +7 -14
  45. package/src/components/ui/custom/theme-dropdown-items.tsx +2 -85
  46. package/src/components/ui/custom/use-satellite-shell.tsx +165 -0
  47. package/src/components/ui/finance/invoices/subscription-invoice.test.tsx +49 -13
  48. package/src/components/ui/finance/invoices/subscription-invoice.tsx +4 -12
  49. package/src/components/ui/finance/invoices/utils.test.ts +6 -5
  50. package/src/components/ui/finance/invoices/utils.ts +3 -1
  51. package/src/components/ui/finance/transactions/columns.test.tsx +25 -1
  52. package/src/components/ui/finance/transactions/columns.tsx +45 -16
  53. package/src/components/ui/legacy/calendar/agenda-view.tsx +139 -47
  54. package/src/components/ui/legacy/calendar/calendar-content.test.tsx +116 -0
  55. package/src/components/ui/legacy/calendar/calendar-content.tsx +38 -74
  56. package/src/components/ui/legacy/calendar/calendar-event-tone.ts +20 -0
  57. package/src/components/ui/legacy/calendar/calendar-meeting-link.test.ts +27 -0
  58. package/src/components/ui/legacy/calendar/calendar-meeting-link.ts +34 -0
  59. package/src/components/ui/legacy/calendar/calendar-period.ts +28 -0
  60. package/src/components/ui/legacy/calendar/calendar-shortcuts.test.tsx +107 -0
  61. package/src/components/ui/legacy/calendar/calendar-views.test.tsx +169 -0
  62. package/src/components/ui/legacy/calendar/event-modal-header.tsx +52 -0
  63. package/src/components/ui/legacy/calendar/event-modal.tsx +107 -156
  64. package/src/components/ui/legacy/calendar/month-calendar.tsx +242 -933
  65. package/src/components/ui/legacy/calendar/save-calendar-event-drafts.test.ts +25 -0
  66. package/src/components/ui/legacy/calendar/save-calendar-event-drafts.ts +32 -0
  67. package/src/components/ui/legacy/calendar/smart-calendar.tsx +3 -0
  68. package/src/components/ui/legacy/calendar/use-calendar-view-shortcuts.ts +54 -0
  69. package/src/components/ui/legacy/calendar/use-event-draft-session.test.tsx +59 -0
  70. package/src/components/ui/legacy/calendar/use-event-draft-session.ts +20 -0
  71. package/src/components/ui/legacy/calendar/year-calendar.tsx +34 -16
  72. package/src/components/ui/pwa-status.test.tsx +53 -0
  73. package/src/components/ui/pwa-status.tsx +113 -0
  74. package/src/components/ui/report-problem-dialog-content.tsx +350 -0
  75. package/src/components/ui/report-problem-dialog.tsx +6 -975
  76. package/src/components/ui/report-problem-model.ts +133 -0
  77. package/src/components/ui/report-problem-mutation.ts +122 -0
  78. package/src/components/ui/save-notification.test.tsx +36 -0
  79. package/src/components/ui/save-notification.tsx +17 -0
  80. package/src/components/ui/text-editor/task-item-checkbox-view.test.tsx +85 -0
  81. package/src/components/ui/text-editor/task-item-checkbox-view.tsx +28 -18
  82. package/src/components/ui/use-report-problem.ts +488 -0
  83. package/src/declarations.d.ts +2 -0
  84. package/src/hooks/__tests__/use-calendar-readonly.test.tsx +46 -4
  85. package/src/hooks/__tests__/use-calendar-sync.test.tsx +24 -0
  86. package/src/hooks/calendar-date-utils.ts +11 -0
  87. package/src/hooks/calendar-event-lookup.test.ts +77 -0
  88. package/src/hooks/calendar-event-lookup.ts +47 -0
  89. package/src/hooks/calendar-range-cache.ts +52 -0
  90. package/src/hooks/pwa-install-prompt.ts +44 -0
  91. package/src/hooks/use-calendar-sync.tsx +23 -100
  92. package/src/hooks/use-calendar.tsx +21 -77
  93. package/src/hooks/use-notifications.ts +9 -24
  94. package/src/hooks/use-open-initial-calendar-event.ts +27 -0
  95. package/src/hooks/use-settings-dialog-shortcut.test.tsx +41 -0
  96. package/src/hooks/use-settings-dialog-shortcut.ts +12 -14
  97. package/src/hooks/use-workspace-presence.ts +0 -18
  98. package/src/lib/tasks-app-url.test.ts +25 -0
  99. package/src/lib/tasks-app-url.ts +8 -0
@@ -0,0 +1,133 @@
1
+ import type { InternalApiClientOptions } from '@tuturuuu/internal-api/client';
2
+ import type { Product, SupportType } from '@tuturuuu/types';
3
+ import type React from 'react';
4
+ import { z } from 'zod';
5
+ export interface ReportProblemFormData {
6
+ product: Product | '';
7
+ type: SupportType | '';
8
+ suggestion: string;
9
+ media: File[];
10
+ }
11
+
12
+ export const MAX_MEDIA_SIZE = 5 * 1024 * 1024; // 5MB
13
+ export const MAX_FILES = 5;
14
+
15
+ // Allowed media types
16
+ export const ALLOWED_IMAGE_TYPES = [
17
+ 'image/png',
18
+ 'image/jpeg',
19
+ 'image/webp',
20
+ 'image/gif',
21
+ ];
22
+ export const ALLOWED_VIDEO_TYPES = [
23
+ 'video/mp4',
24
+ 'video/webm',
25
+ 'video/quicktime',
26
+ ];
27
+
28
+ // Zod schema for form validation
29
+ export const reportProblemSchema = z.object({
30
+ product: z.enum(
31
+ [
32
+ 'web',
33
+ 'nova',
34
+ 'rewise',
35
+ 'calendar',
36
+ 'finance',
37
+ 'tudo',
38
+ 'tumeet',
39
+ 'shortener',
40
+ 'qr',
41
+ 'drive',
42
+ 'mail',
43
+ 'other',
44
+ ],
45
+ {
46
+ message: 'Please select a product',
47
+ }
48
+ ),
49
+ type: z.enum(['bug', 'feature-request'], {
50
+ message: 'Please select a support type',
51
+ }),
52
+ suggestion: z
53
+ .string()
54
+ .trim()
55
+ .min(1, 'Please describe the issue or suggestion')
56
+ .max(1000, 'Suggestion must be at most 1000 characters'),
57
+ media: z
58
+ .array(z.instanceof(File))
59
+ .max(MAX_FILES, `You can upload up to ${MAX_FILES} files`)
60
+ .refine(
61
+ (files) =>
62
+ files.every(
63
+ (f) =>
64
+ ALLOWED_IMAGE_TYPES.includes(f.type) ||
65
+ ALLOWED_VIDEO_TYPES.includes(f.type)
66
+ ),
67
+ {
68
+ message:
69
+ 'Only image (PNG, JPEG, WebP, GIF) and video (MP4, WebM, MOV) files are allowed',
70
+ }
71
+ )
72
+ .refine((files) => files.every((f) => f.size <= MAX_MEDIA_SIZE), {
73
+ message: 'Each file must be 5MB or less',
74
+ }),
75
+ });
76
+
77
+ export interface ReportProblemDialogProps {
78
+ ImageComponent?: React.ComponentType<{
79
+ src: string;
80
+ alt: string;
81
+ className: string;
82
+ width: number;
83
+ height: number;
84
+ }>;
85
+ t: (
86
+ key:
87
+ | 'affected-product-required'
88
+ | 'cancel'
89
+ | 'media-alt'
90
+ | 'media-optional'
91
+ | 'report-problem'
92
+ | 'report-problem-description'
93
+ | 'report-submitted-success'
94
+ | 'select-product-placeholder'
95
+ | 'submit-report'
96
+ | 'submitting'
97
+ | 'suggestion-improve'
98
+ | 'suggestion-placeholder',
99
+ values?: Record<string, string | number>
100
+ ) => string;
101
+ apiOptions?: InternalApiClientOptions;
102
+ open?: boolean;
103
+ onOpenChange?: (open: boolean) => void;
104
+ products?: Array<{ value: Product; label: string }>;
105
+ className?: string;
106
+ trigger?: React.ReactNode;
107
+ showTrigger?: boolean;
108
+ }
109
+
110
+ export const DEFAULT_PRODUCTS: Array<{ value: Product; label: string }> = [
111
+ { value: 'web', label: 'Web Dashboard' },
112
+ { value: 'nova', label: 'Nova' },
113
+ { value: 'rewise', label: 'Rewise' },
114
+ { value: 'calendar', label: 'Calendar' },
115
+ { value: 'finance', label: 'Finance' },
116
+ { value: 'tudo', label: 'Tuturuuu Tasks' },
117
+ { value: 'tumeet', label: 'Tuturuuu Meet' },
118
+ { value: 'shortener', label: 'URL Shortener' },
119
+ { value: 'qr', label: 'QR Code' },
120
+ { value: 'drive', label: 'Drive' },
121
+ { value: 'mail', label: 'Mail' },
122
+ { value: 'other', label: 'Other' },
123
+ ];
124
+
125
+ export const DEFAULT_SUPPORT_TYPES: Array<{
126
+ value: SupportType;
127
+ label: string;
128
+ }> = [
129
+ { value: 'bug', label: 'Bug Report' },
130
+ { value: 'feature-request', label: 'Feature Request' },
131
+ ];
132
+
133
+ export const LOCAL_STORAGE_KEY = 'report-problem-form-data';
@@ -0,0 +1,122 @@
1
+ import { useMutation } from '@tanstack/react-query';
2
+ import type { InternalApiClientOptions } from '@tuturuuu/internal-api/client';
3
+ import {
4
+ createReportUploadUrls,
5
+ deleteReportUploadPaths,
6
+ submitReport,
7
+ } from '@tuturuuu/internal-api/reports';
8
+ import type { Product, SupportType } from '@tuturuuu/types';
9
+
10
+ interface SubmitReportMutationInput {
11
+ product: Product;
12
+ type: SupportType;
13
+ suggestion: string;
14
+ subject: string;
15
+ media: File[];
16
+ }
17
+
18
+ export function useSubmitReportMutation(apiOptions?: InternalApiClientOptions) {
19
+ return useMutation({
20
+ mutationFn: async ({
21
+ product,
22
+ type,
23
+ suggestion,
24
+ subject,
25
+ media,
26
+ }: SubmitReportMutationInput) => {
27
+ const uploadedPaths: string[] = [];
28
+
29
+ const cleanupUploadedMedia = async () => {
30
+ if (uploadedPaths.length === 0) {
31
+ return;
32
+ }
33
+
34
+ try {
35
+ await deleteReportUploadPaths({ paths: uploadedPaths }, apiOptions);
36
+ } catch (cleanupError) {
37
+ console.error(
38
+ 'Failed to clean up uploaded support report media:',
39
+ cleanupError
40
+ );
41
+ }
42
+ };
43
+
44
+ try {
45
+ if (media.length > 0) {
46
+ const { uploads } = await createReportUploadUrls(
47
+ {
48
+ files: media.map((file) => ({
49
+ filename: file.name,
50
+ contentType: file.type,
51
+ size: file.size,
52
+ })),
53
+ },
54
+ apiOptions
55
+ );
56
+
57
+ if (uploads.length !== media.length) {
58
+ throw new Error('Failed to prepare media uploads');
59
+ }
60
+
61
+ for (const [index, upload] of uploads.entries()) {
62
+ const file = media[index];
63
+
64
+ if (!file) {
65
+ throw new Error('Missing media file during upload');
66
+ }
67
+
68
+ let uploadResponse = await fetch(upload.signedUrl, {
69
+ method: 'PUT',
70
+ cache: 'no-store',
71
+ headers: {
72
+ Authorization: `Bearer ${upload.token}`,
73
+ 'Content-Type': file.type || 'application/octet-stream',
74
+ },
75
+ body: file,
76
+ });
77
+
78
+ if (!uploadResponse.ok) {
79
+ uploadResponse = await fetch(upload.signedUrl, {
80
+ method: 'PUT',
81
+ cache: 'no-store',
82
+ headers: {
83
+ Authorization: `Bearer ${upload.token}`,
84
+ },
85
+ body: file,
86
+ });
87
+ }
88
+
89
+ if (!uploadResponse.ok) {
90
+ const text = await uploadResponse.text().catch(() => '');
91
+ throw new Error(
92
+ `Failed to upload media (${uploadResponse.status})${text ? `: ${text}` : ''}`
93
+ );
94
+ }
95
+
96
+ uploadedPaths.push(upload.path);
97
+ }
98
+ }
99
+
100
+ const result = await submitReport(
101
+ {
102
+ product,
103
+ type,
104
+ suggestion,
105
+ subject,
106
+ imagePaths: uploadedPaths,
107
+ },
108
+ apiOptions
109
+ );
110
+
111
+ if (!result.success) {
112
+ throw new Error(result.message || 'Failed to submit report');
113
+ }
114
+
115
+ return result;
116
+ } catch (error) {
117
+ await cleanupUploadedMedia();
118
+ throw error;
119
+ }
120
+ },
121
+ });
122
+ }
@@ -0,0 +1,36 @@
1
+ import { act, render, screen } from '@testing-library/react';
2
+ import { afterEach, expect, it, vi } from 'vitest';
3
+ import { notifySave } from './save-notification';
4
+ import { Toaster, toast } from './sonner';
5
+
6
+ afterEach(() => {
7
+ toast.dismiss();
8
+ vi.unstubAllGlobals();
9
+ });
10
+
11
+ it('renders persistent save failures in the toaster used by app layouts', async () => {
12
+ vi.stubGlobal(
13
+ 'matchMedia',
14
+ vi.fn(() => ({
15
+ matches: false,
16
+ addEventListener: vi.fn(),
17
+ removeEventListener: vi.fn(),
18
+ }))
19
+ );
20
+ render(<Toaster />);
21
+ act(() => {
22
+ notifySave({
23
+ title: 'Save failed',
24
+ description: 'Your draft is preserved. Try again.',
25
+ variant: 'destructive',
26
+ duration: Number.POSITIVE_INFINITY,
27
+ });
28
+ });
29
+ expect(await screen.findByText('Save failed')).toBeInTheDocument();
30
+ expect(
31
+ screen.getByText('Your draft is preserved. Try again.')
32
+ ).toBeInTheDocument();
33
+ expect(
34
+ screen.getByRole('button', { name: 'Close toast' })
35
+ ).toBeInTheDocument();
36
+ });
@@ -0,0 +1,17 @@
1
+ 'use client';
2
+
3
+ import type { ReactNode } from 'react';
4
+ import { toast } from './sonner';
5
+
6
+ interface SaveNotification {
7
+ title: ReactNode;
8
+ description?: ReactNode;
9
+ variant?: 'default' | 'destructive';
10
+ duration?: number;
11
+ }
12
+
13
+ /** Save feedback must use the Sonner toaster mounted by the application shells. */
14
+ export function notifySave({ title, variant, ...options }: SaveNotification) {
15
+ const notify = variant === 'destructive' ? toast.error : toast.success;
16
+ return notify(title, { ...options, closeButton: true });
17
+ }
@@ -0,0 +1,85 @@
1
+ import { fireEvent, render, screen } from '@testing-library/react';
2
+ import type { NodeViewProps } from '@tiptap/react';
3
+ import { describe, expect, it, vi } from 'vitest';
4
+ import { TaskItemCheckboxContent } from './task-item-checkbox-view';
5
+
6
+ vi.mock('@tuturuuu/supabase/next/client', () => ({
7
+ createClient: vi.fn(),
8
+ }));
9
+
10
+ vi.mock('./task-item-checkbox', async (importOriginal) => {
11
+ const actual = await importOriginal<typeof import('./task-item-checkbox')>();
12
+
13
+ return {
14
+ ...actual,
15
+ useMentionedTaskStatuses: () => ({ data: [] }),
16
+ };
17
+ });
18
+
19
+ function createNodeViewProps() {
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 };
42
+ }
43
+
44
+ describe('TaskItemCheckboxContent', () => {
45
+ it('cycles pointer interactions before the editor can replace the node view', () => {
46
+ const { props, setNodeMarkup } = createNodeViewProps();
47
+ render(<TaskItemCheckboxContent {...props} />);
48
+
49
+ const checkbox = screen.getByRole('checkbox', {
50
+ name: 'Task item status',
51
+ });
52
+ expect(checkbox).toHaveAttribute('aria-checked', 'false');
53
+
54
+ fireEvent.pointerDown(checkbox, { button: 0, isPrimary: true });
55
+
56
+ expect(setNodeMarkup).toHaveBeenLastCalledWith(4, undefined, {
57
+ checked: 'indeterminate',
58
+ });
59
+ expect(checkbox).toHaveAttribute('aria-checked', 'mixed');
60
+ });
61
+
62
+ it('cycles all three states from the keyboard', () => {
63
+ const { props, setNodeMarkup } = createNodeViewProps();
64
+ render(<TaskItemCheckboxContent {...props} />);
65
+
66
+ const checkbox = screen.getByRole('checkbox', {
67
+ name: 'Task item status',
68
+ });
69
+
70
+ fireEvent.keyDown(checkbox, { key: ' ' });
71
+ fireEvent.keyDown(checkbox, { key: 'Enter' });
72
+ fireEvent.keyDown(checkbox, { key: ' ' });
73
+
74
+ expect(setNodeMarkup).toHaveBeenNthCalledWith(1, 4, undefined, {
75
+ checked: 'indeterminate',
76
+ });
77
+ expect(setNodeMarkup).toHaveBeenNthCalledWith(2, 4, undefined, {
78
+ checked: true,
79
+ });
80
+ expect(setNodeMarkup).toHaveBeenNthCalledWith(3, 4, undefined, {
81
+ checked: false,
82
+ });
83
+ expect(checkbox).toHaveAttribute('aria-checked', 'false');
84
+ });
85
+ });
@@ -68,14 +68,24 @@ export function TaskItemCheckboxContent({
68
68
  });
69
69
  }, [editor, getPos, node.attrs, checkboxState]);
70
70
 
71
- const handleCheckboxClick = useCallback(
72
- (event: React.MouseEvent<HTMLButtonElement>) => {
71
+ const handleCheckboxPointerDown = useCallback(
72
+ (event: React.PointerEvent<HTMLButtonElement>) => {
73
+ if (!event.isPrimary || event.button !== 0) return;
73
74
  event.preventDefault();
74
75
  event.stopPropagation();
75
- if (!editor.isEditable) return;
76
76
  handleCheckboxCycle();
77
77
  },
78
- [editor, handleCheckboxCycle]
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
+ },
88
+ [handleCheckboxCycle]
79
89
  );
80
90
 
81
91
  return (
@@ -84,29 +94,29 @@ export function TaskItemCheckboxContent({
84
94
  className="task-list-checkbox-label flex shrink-0 select-none pt-2.5"
85
95
  contentEditable={false}
86
96
  >
97
+ {/* biome-ignore lint/a11y/useSemanticElements: a button avoids native checkbox state changes while ProseMirror owns the tri-state document update */}
87
98
  <button
88
99
  type="button"
89
- aria-label={
90
- isChecked
91
- ? 'Checked'
92
- : isIndeterminate
93
- ? 'Indeterminate'
94
- : 'Unchecked'
95
- }
100
+ role="checkbox"
101
+ aria-label="Task item status"
102
+ aria-checked={isIndeterminate ? 'mixed' : isChecked}
96
103
  aria-disabled={!editor.isEditable}
97
- onClick={handleCheckboxClick}
104
+ onPointerDown={handleCheckboxPointerDown}
105
+ onKeyDown={handleCheckboxKeyDown}
98
106
  className={cn(
99
- 'task-list-checkbox flex h-4.5 w-4.5 items-center justify-center',
100
- 'cursor-pointer rounded-lg border-2 bg-background',
101
- 'transition-all duration-150',
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',
102
112
  !completedTaskColor && [
103
113
  'border-input',
104
114
  'hover:scale-105 hover:border-dynamic-gray hover:bg-dynamic-gray/10',
105
- 'focus:border-dynamic-gray focus:outline-none focus:ring-2 focus:ring-dynamic-gray/30 focus:ring-offset-2',
115
+ 'focus-visible:border-dynamic-gray',
106
116
  isChecked &&
107
- 'border-dynamic-green bg-dynamic-green/20 hover:border-dynamic-green hover:bg-dynamic-green/10 focus:border-dynamic-green focus:ring-dynamic-green/30',
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',
108
118
  isIndeterminate &&
109
- 'border-dynamic-orange bg-dynamic-orange/20 hover:border-dynamic-orange hover:bg-dynamic-orange/10 focus:border-dynamic-orange focus:ring-dynamic-orange/30',
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',
110
120
  ],
111
121
  completedTaskColor === 'red' && [
112
122
  'border-dynamic-red/50',