@tuturuuu/ui 0.30.0 → 0.30.2
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 +16 -0
- package/biome.json +1 -1
- package/package.json +47 -43
- package/src/components/ui/custom/__tests__/tuturuuu-wordmark.test.ts +22 -0
- package/src/components/ui/custom/tuturuuu-wordmark.tsx +72 -0
- package/src/components/ui/finance/invoices/subscription-invoice.test.tsx +49 -13
- package/src/components/ui/finance/invoices/subscription-invoice.tsx +4 -12
- package/src/components/ui/finance/invoices/utils.test.ts +6 -5
- package/src/components/ui/finance/invoices/utils.ts +3 -1
- package/src/components/ui/finance/transactions/columns.test.tsx +25 -1
- package/src/components/ui/finance/transactions/columns.tsx +45 -16
- package/src/components/ui/marketing/marketing-footer.tsx +8 -7
- package/src/components/ui/marketing/marketing-nav.tsx +17 -7
- package/src/components/ui/text-editor/task-item-checkbox-view.test.tsx +85 -0
- package/src/components/ui/text-editor/task-item-checkbox-view.tsx +28 -18
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.30.2](https://github.com/tutur3u/platform/compare/ui-v0.30.1...ui-v0.30.2) (2026-08-31)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* **finance:** allow advance subscription invoices ([d307068](https://github.com/tutur3u/platform/commit/d3070683a7c74b9f18f65dabdf482132fa02cf13))
|
|
9
|
+
* **tasks:** restore description checklist controls ([7976f78](https://github.com/tutur3u/platform/commit/7976f78292237e8047509e4243dafa14acea1b54))
|
|
10
|
+
|
|
11
|
+
## [0.30.1](https://github.com/tutur3u/platform/compare/ui-v0.30.0...ui-v0.30.1) (2026-08-27)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Bug Fixes
|
|
15
|
+
|
|
16
|
+
* **forms:** repair the landing demo, the brand lockup and two analytics labels ([3f16968](https://github.com/tutur3u/platform/commit/3f16968f5802ebbd518ee27025c61a759839fd4e))
|
|
17
|
+
* **forms:** repair the landing demo, the brand lockup and two analytics labels ([#5164](https://github.com/tutur3u/platform/issues/5164)) ([56d7f62](https://github.com/tutur3u/platform/commit/56d7f62325a8ec0caa5bb3ccd76b050dd66b1e53))
|
|
18
|
+
|
|
3
19
|
## [0.30.0](https://github.com/tutur3u/platform/compare/ui-v0.29.2...ui-v0.30.0) (2026-08-26)
|
|
4
20
|
|
|
5
21
|
|
package/biome.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tuturuuu/ui",
|
|
3
|
-
"version": "0.30.
|
|
3
|
+
"version": "0.30.2",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -57,43 +57,43 @@
|
|
|
57
57
|
"@streamdown/mermaid": "^1.0.2",
|
|
58
58
|
"@tanstack/react-hotkeys": "^0.10.0",
|
|
59
59
|
"@tanstack/react-pacer": "^0.23.0",
|
|
60
|
-
"@tanstack/react-query": "^5.
|
|
61
|
-
"@tanstack/react-table": "^9.
|
|
60
|
+
"@tanstack/react-query": "^5.102.8",
|
|
61
|
+
"@tanstack/react-table": "^9.2.3",
|
|
62
62
|
"@tanstack/react-virtual": "^3.14.10",
|
|
63
|
-
"@tiptap/core": "3.30.
|
|
64
|
-
"@tiptap/extension-collaboration": "3.30.
|
|
65
|
-
"@tiptap/extension-collaboration-caret": "3.30.
|
|
66
|
-
"@tiptap/extension-color": "3.30.
|
|
67
|
-
"@tiptap/extension-details": "3.30.
|
|
68
|
-
"@tiptap/extension-drag-handle": "3.30.
|
|
69
|
-
"@tiptap/extension-drag-handle-react": "3.30.
|
|
70
|
-
"@tiptap/extension-highlight": "3.30.
|
|
71
|
-
"@tiptap/extension-horizontal-rule": "3.30.
|
|
72
|
-
"@tiptap/extension-image": "3.30.
|
|
73
|
-
"@tiptap/extension-link": "3.30.
|
|
74
|
-
"@tiptap/extension-list": "3.30.
|
|
75
|
-
"@tiptap/extension-node-range": "3.30.
|
|
76
|
-
"@tiptap/extension-placeholder": "3.30.
|
|
77
|
-
"@tiptap/extension-strike": "3.30.
|
|
78
|
-
"@tiptap/extension-subscript": "3.30.
|
|
79
|
-
"@tiptap/extension-superscript": "3.30.
|
|
80
|
-
"@tiptap/extension-table": "3.30.
|
|
81
|
-
"@tiptap/extension-table-cell": "3.30.
|
|
82
|
-
"@tiptap/extension-table-header": "3.30.
|
|
83
|
-
"@tiptap/extension-table-row": "3.30.
|
|
84
|
-
"@tiptap/extension-text-align": "3.30.
|
|
85
|
-
"@tiptap/extension-text-style": "3.30.
|
|
86
|
-
"@tiptap/extension-youtube": "3.30.
|
|
87
|
-
"@tiptap/pm": "3.30.
|
|
88
|
-
"@tiptap/react": "3.30.
|
|
89
|
-
"@tiptap/starter-kit": "3.30.
|
|
63
|
+
"@tiptap/core": "3.30.5",
|
|
64
|
+
"@tiptap/extension-collaboration": "3.30.5",
|
|
65
|
+
"@tiptap/extension-collaboration-caret": "3.30.5",
|
|
66
|
+
"@tiptap/extension-color": "3.30.5",
|
|
67
|
+
"@tiptap/extension-details": "3.30.5",
|
|
68
|
+
"@tiptap/extension-drag-handle": "3.30.5",
|
|
69
|
+
"@tiptap/extension-drag-handle-react": "3.30.5",
|
|
70
|
+
"@tiptap/extension-highlight": "3.30.5",
|
|
71
|
+
"@tiptap/extension-horizontal-rule": "3.30.5",
|
|
72
|
+
"@tiptap/extension-image": "3.30.5",
|
|
73
|
+
"@tiptap/extension-link": "3.30.5",
|
|
74
|
+
"@tiptap/extension-list": "3.30.5",
|
|
75
|
+
"@tiptap/extension-node-range": "3.30.5",
|
|
76
|
+
"@tiptap/extension-placeholder": "3.30.5",
|
|
77
|
+
"@tiptap/extension-strike": "3.30.5",
|
|
78
|
+
"@tiptap/extension-subscript": "3.30.5",
|
|
79
|
+
"@tiptap/extension-superscript": "3.30.5",
|
|
80
|
+
"@tiptap/extension-table": "3.30.5",
|
|
81
|
+
"@tiptap/extension-table-cell": "3.30.5",
|
|
82
|
+
"@tiptap/extension-table-header": "3.30.5",
|
|
83
|
+
"@tiptap/extension-table-row": "3.30.5",
|
|
84
|
+
"@tiptap/extension-text-align": "3.30.5",
|
|
85
|
+
"@tiptap/extension-text-style": "3.30.5",
|
|
86
|
+
"@tiptap/extension-youtube": "3.30.5",
|
|
87
|
+
"@tiptap/pm": "3.30.5",
|
|
88
|
+
"@tiptap/react": "3.30.5",
|
|
89
|
+
"@tiptap/starter-kit": "3.30.5",
|
|
90
90
|
"@tuturuuu/ai": "0.8.3",
|
|
91
91
|
"@tuturuuu/apis": "0.11.3",
|
|
92
92
|
"@tuturuuu/hooks": "0.1.1",
|
|
93
93
|
"@tuturuuu/icons": "0.1.0",
|
|
94
94
|
"@tuturuuu/internal-api": "0.33.0",
|
|
95
95
|
"@tuturuuu/supabase": "0.5.1",
|
|
96
|
-
"@tuturuuu/utils": "0.
|
|
96
|
+
"@tuturuuu/utils": "0.27.0",
|
|
97
97
|
"@types/debug": "^4.1.13",
|
|
98
98
|
"browser-image-compression": "^2.0.2",
|
|
99
99
|
"class-variance-authority": "^0.7.1",
|
|
@@ -108,18 +108,18 @@
|
|
|
108
108
|
"framer-motion": "^13.1.1",
|
|
109
109
|
"gsap": "^3.15.0",
|
|
110
110
|
"html-entities": "^2.6.0",
|
|
111
|
-
"html2canvas-pro": "^2.
|
|
111
|
+
"html2canvas-pro": "^2.4.0",
|
|
112
112
|
"input-otp": "^1.5.0",
|
|
113
113
|
"katex": "^0.18.4",
|
|
114
114
|
"lodash": "4.18.1",
|
|
115
115
|
"moment": "^2.30.1",
|
|
116
116
|
"motion": "^13.1.1",
|
|
117
|
-
"next": "^16.3.
|
|
118
|
-
"next-intl": "^4.
|
|
117
|
+
"next": "^16.3.3",
|
|
118
|
+
"next-intl": "^4.14.1",
|
|
119
119
|
"next-themes": "^0.4.6",
|
|
120
|
-
"nuqs": "^2.10.
|
|
120
|
+
"nuqs": "^2.10.1",
|
|
121
121
|
"prosemirror-state": "^1.4.4",
|
|
122
|
-
"prosemirror-view": "1.42.
|
|
122
|
+
"prosemirror-view": "1.42.3",
|
|
123
123
|
"qrcode.react": "^4.2.0",
|
|
124
124
|
"react": "^19.2.8",
|
|
125
125
|
"react-colorful": "^5.8.0",
|
|
@@ -136,7 +136,7 @@
|
|
|
136
136
|
"recharts": "3.10.1",
|
|
137
137
|
"slot-text": "^0.3.4",
|
|
138
138
|
"sonner": "^2.0.8",
|
|
139
|
-
"streamdown": "^2.
|
|
139
|
+
"streamdown": "^2.6.0",
|
|
140
140
|
"tailwind-scrollbar": "^4.0.2",
|
|
141
141
|
"tailwind-scrollbar-hide": "^4.0.0",
|
|
142
142
|
"tailwindcss-animate": "^1.0.7",
|
|
@@ -150,21 +150,21 @@
|
|
|
150
150
|
"devDependencies": {
|
|
151
151
|
"@tailwindcss/postcss": "^4.3.3",
|
|
152
152
|
"@tailwindcss/typography": "^0.5.20",
|
|
153
|
-
"@tanstack/react-query": "^5.102.
|
|
154
|
-
"@tanstack/react-table": "^9.
|
|
153
|
+
"@tanstack/react-query": "^5.102.8",
|
|
154
|
+
"@tanstack/react-table": "^9.2.3",
|
|
155
155
|
"@testing-library/jest-dom": "^7.0.1",
|
|
156
|
-
"@testing-library/react": "^16.3.
|
|
156
|
+
"@testing-library/react": "^16.3.3",
|
|
157
157
|
"@tuturuuu/types": "0.28.0",
|
|
158
158
|
"@tuturuuu/typescript-config": "0.1.2",
|
|
159
159
|
"@types/html2canvas": "^1.0.0",
|
|
160
160
|
"@types/lodash": "^4.17.25",
|
|
161
|
-
"@types/node": "^26.
|
|
161
|
+
"@types/node": "^26.4.0",
|
|
162
162
|
"@types/react": "^19.2.18",
|
|
163
|
-
"@types/react-dom": "^19.2.
|
|
163
|
+
"@types/react-dom": "^19.2.5",
|
|
164
164
|
"@types/react-resizable": "^4.0.0",
|
|
165
165
|
"@types/react-syntax-highlighter": "^15.5.13",
|
|
166
166
|
"@types/uuid": "^11.0.0",
|
|
167
|
-
"@vitejs/plugin-react": "^6.1.
|
|
167
|
+
"@vitejs/plugin-react": "^6.1.1",
|
|
168
168
|
"autoprefixer": "^10.5.4",
|
|
169
169
|
"jsdom": "^30.0.1",
|
|
170
170
|
"postcss": "^8.5.26",
|
|
@@ -313,6 +313,10 @@
|
|
|
313
313
|
"types": "./src/components/ui/custom/system-language-wrapper.tsx",
|
|
314
314
|
"import": "./src/components/ui/custom/system-language-wrapper.tsx"
|
|
315
315
|
},
|
|
316
|
+
"./custom/tuturuuu-wordmark": {
|
|
317
|
+
"types": "./src/components/ui/custom/tuturuuu-wordmark.tsx",
|
|
318
|
+
"import": "./src/components/ui/custom/tuturuuu-wordmark.tsx"
|
|
319
|
+
},
|
|
316
320
|
"./custom/tuturuuu-logo": {
|
|
317
321
|
"types": "./src/components/ui/custom/tuturuuu-logo.tsx",
|
|
318
322
|
"import": "./src/components/ui/custom/tuturuuu-logo.tsx"
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { toProductName } from '../tuturuuu-wordmark';
|
|
3
|
+
|
|
4
|
+
describe('toProductName', () => {
|
|
5
|
+
it('strips the company name from a full app name', () => {
|
|
6
|
+
expect(toProductName('Tuturuuu Forms')).toBe('Forms');
|
|
7
|
+
expect(toProductName('Tuturuuu Calendar')).toBe('Calendar');
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('is case-insensitive and tolerates extra spacing', () => {
|
|
11
|
+
expect(toProductName('TUTURUUU Forms')).toBe('Forms');
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('leaves a bare product name alone', () => {
|
|
15
|
+
expect(toProductName('Forms')).toBe('Forms');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('returns the name unchanged when stripping would empty it', () => {
|
|
19
|
+
// Otherwise the lockup renders with a blank second line.
|
|
20
|
+
expect(toProductName('Tuturuuu')).toBe('Tuturuuu');
|
|
21
|
+
});
|
|
22
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { cn } from '@tuturuuu/utils/format';
|
|
2
|
+
import { TuturuuLogo } from './tuturuuu-logo';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The Tuturuuu product lockup: mark, then "Tuturuuu" over the product name.
|
|
6
|
+
*
|
|
7
|
+
* Two lines rather than one string. "Tuturuuu Forms" set on a single line reads
|
|
8
|
+
* as one long product name; stacking it says what it is — a Tuturuuu product
|
|
9
|
+
* called Forms — and keeps the company name consistent across products while
|
|
10
|
+
* only the second line changes.
|
|
11
|
+
*
|
|
12
|
+
* The logo deliberately does not take a local `src`. Satellites are served from
|
|
13
|
+
* their own domains and do not carry `/media`, so a local path 404s there; the
|
|
14
|
+
* component's default is the absolute URL for exactly that reason.
|
|
15
|
+
*/
|
|
16
|
+
export function TuturuuuWordmark({
|
|
17
|
+
className,
|
|
18
|
+
product,
|
|
19
|
+
size = 'md',
|
|
20
|
+
}: {
|
|
21
|
+
className?: string;
|
|
22
|
+
/** The second line — the product name, without "Tuturuuu" in front of it. */
|
|
23
|
+
product: string;
|
|
24
|
+
size?: 'sm' | 'md';
|
|
25
|
+
}) {
|
|
26
|
+
const dimension = size === 'sm' ? 28 : 32;
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<span className={cn('flex items-center gap-2.5', className)}>
|
|
30
|
+
<TuturuuLogo
|
|
31
|
+
alt=""
|
|
32
|
+
aria-hidden
|
|
33
|
+
className={cn('object-contain', size === 'sm' ? 'h-7 w-7' : 'h-8 w-8')}
|
|
34
|
+
height={dimension}
|
|
35
|
+
width={dimension}
|
|
36
|
+
/>
|
|
37
|
+
<span className="flex flex-col text-left leading-none">
|
|
38
|
+
<span
|
|
39
|
+
className={cn(
|
|
40
|
+
'uppercase tracking-[0.24em] opacity-70',
|
|
41
|
+
size === 'sm' ? 'text-[9px]' : 'text-[10px]'
|
|
42
|
+
)}
|
|
43
|
+
>
|
|
44
|
+
Tuturuuu
|
|
45
|
+
</span>
|
|
46
|
+
<span
|
|
47
|
+
className={cn(
|
|
48
|
+
'font-display font-semibold uppercase tracking-[0.08em]',
|
|
49
|
+
size === 'sm' ? 'text-sm' : 'text-lg'
|
|
50
|
+
)}
|
|
51
|
+
>
|
|
52
|
+
{product}
|
|
53
|
+
</span>
|
|
54
|
+
</span>
|
|
55
|
+
</span>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The product line of a full app name: "Tuturuuu Forms" -> "Forms".
|
|
61
|
+
*
|
|
62
|
+
* Exists so the nav and the footer cannot disagree. They derive the same label
|
|
63
|
+
* from the same prop, and two copies of one regex is exactly the kind of
|
|
64
|
+
* duplication that drifts when only one of them is updated.
|
|
65
|
+
*
|
|
66
|
+
* A name that is only "Tuturuuu" is returned unchanged rather than reduced to
|
|
67
|
+
* an empty string, which would render a lockup with a blank second line.
|
|
68
|
+
*/
|
|
69
|
+
export function toProductName(appName: string): string {
|
|
70
|
+
const stripped = appName.replace(/^Tuturuuu\s+/i, '').trim();
|
|
71
|
+
return stripped || appName;
|
|
72
|
+
}
|
|
@@ -30,15 +30,28 @@ const testState = vi.hoisted(() => {
|
|
|
30
30
|
|
|
31
31
|
return {
|
|
32
32
|
InvoicePaymentSettings: vi.fn(),
|
|
33
|
+
SubscriptionAttendanceSummary: vi.fn(),
|
|
33
34
|
categories: [] as Array<{ id: string; name: string }>,
|
|
34
35
|
groupIds: ['group-1'],
|
|
35
36
|
month: '2026-07',
|
|
37
|
+
setMonth: vi.fn(),
|
|
36
38
|
productSelectionInjected: false,
|
|
37
39
|
products: [product],
|
|
38
40
|
selectedProducts: [{ inventory, product, quantity: 1 }],
|
|
39
41
|
useCategories: vi.fn(),
|
|
40
42
|
useWallets: vi.fn(),
|
|
41
43
|
userId: 'user-1',
|
|
44
|
+
userGroups: [
|
|
45
|
+
{
|
|
46
|
+
workspace_user_groups: {
|
|
47
|
+
ending_date: null as string | null,
|
|
48
|
+
id: 'group-1',
|
|
49
|
+
name: 'Group 1',
|
|
50
|
+
sessions: ['2026-07-05'],
|
|
51
|
+
starting_date: '2026-07-01',
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
],
|
|
42
55
|
wallets: [] as Array<{
|
|
43
56
|
currency: string;
|
|
44
57
|
id: string;
|
|
@@ -67,7 +80,7 @@ vi.mock('nuqs', () => ({
|
|
|
67
80
|
useQueryState: (key: string, options?: { defaultValue?: unknown }) => {
|
|
68
81
|
if (key === 'user_id') return [testState.userId, vi.fn()];
|
|
69
82
|
if (key === 'group_ids') return [testState.groupIds, vi.fn()];
|
|
70
|
-
if (key === 'month') return [testState.month,
|
|
83
|
+
if (key === 'month') return [testState.month, testState.setMonth];
|
|
71
84
|
return [options?.defaultValue ?? '', vi.fn()];
|
|
72
85
|
},
|
|
73
86
|
}));
|
|
@@ -113,7 +126,10 @@ vi.mock('./components/invoice-products-permission-warning', () => ({
|
|
|
113
126
|
}));
|
|
114
127
|
|
|
115
128
|
vi.mock('./components/subscription-attendance-summary', () => ({
|
|
116
|
-
SubscriptionAttendanceSummary: () =>
|
|
129
|
+
SubscriptionAttendanceSummary: (props: unknown) => {
|
|
130
|
+
testState.SubscriptionAttendanceSummary(props);
|
|
131
|
+
return null;
|
|
132
|
+
},
|
|
117
133
|
}));
|
|
118
134
|
|
|
119
135
|
vi.mock('./components/subscription-group-selector', () => ({
|
|
@@ -178,17 +194,7 @@ vi.mock('./hooks', () => ({
|
|
|
178
194
|
isLoading: false,
|
|
179
195
|
}),
|
|
180
196
|
useUserGroups: () => ({
|
|
181
|
-
data:
|
|
182
|
-
{
|
|
183
|
-
workspace_user_groups: {
|
|
184
|
-
ending_date: null,
|
|
185
|
-
id: 'group-1',
|
|
186
|
-
name: 'Group 1',
|
|
187
|
-
sessions: ['2026-07-05'],
|
|
188
|
-
starting_date: '2026-07-01',
|
|
189
|
-
},
|
|
190
|
-
},
|
|
191
|
-
],
|
|
197
|
+
data: testState.userGroups,
|
|
192
198
|
isLoading: false,
|
|
193
199
|
}),
|
|
194
200
|
useUserLinkedPromotions: () => ({ data: [] }),
|
|
@@ -262,9 +268,14 @@ function renderSubscriptionInvoice(
|
|
|
262
268
|
describe('SubscriptionInvoice checkout defaults', () => {
|
|
263
269
|
beforeEach(() => {
|
|
264
270
|
testState.InvoicePaymentSettings.mockClear();
|
|
271
|
+
testState.SubscriptionAttendanceSummary.mockClear();
|
|
265
272
|
testState.useCategories.mockClear();
|
|
266
273
|
testState.useWallets.mockClear();
|
|
267
274
|
testState.productSelectionInjected = false;
|
|
275
|
+
testState.month = '2026-07';
|
|
276
|
+
testState.setMonth.mockClear();
|
|
277
|
+
testState.userGroups[0]!.workspace_user_groups.ending_date = null;
|
|
278
|
+
testState.userGroups[0]!.workspace_user_groups.sessions = ['2026-07-05'];
|
|
268
279
|
testState.products[0]!.finance_category_id = null;
|
|
269
280
|
testState.wallets = [];
|
|
270
281
|
testState.categories = [];
|
|
@@ -328,4 +339,29 @@ describe('SubscriptionInvoice checkout defaults', () => {
|
|
|
328
339
|
)
|
|
329
340
|
);
|
|
330
341
|
});
|
|
342
|
+
|
|
343
|
+
it('keeps an advance month selected after the group schedule end', async () => {
|
|
344
|
+
testState.month = '2026-09';
|
|
345
|
+
testState.userGroups[0]!.workspace_user_groups.ending_date = '2026-08-31';
|
|
346
|
+
testState.userGroups[0]!.workspace_user_groups.sessions = [
|
|
347
|
+
'2026-07-05',
|
|
348
|
+
'2026-08-05',
|
|
349
|
+
'2026-09-05',
|
|
350
|
+
];
|
|
351
|
+
|
|
352
|
+
renderSubscriptionInvoice();
|
|
353
|
+
|
|
354
|
+
await waitFor(() =>
|
|
355
|
+
expect(testState.SubscriptionAttendanceSummary).toHaveBeenCalledWith(
|
|
356
|
+
expect.objectContaining({
|
|
357
|
+
availableMonths: expect.arrayContaining([
|
|
358
|
+
expect.objectContaining({ value: '2026-09' }),
|
|
359
|
+
]),
|
|
360
|
+
selectedMonth: '2026-09',
|
|
361
|
+
})
|
|
362
|
+
)
|
|
363
|
+
);
|
|
364
|
+
expect(testState.setMonth).not.toHaveBeenCalledWith('2026-07');
|
|
365
|
+
expect(testState.setMonth).not.toHaveBeenCalledWith('2026-08');
|
|
366
|
+
});
|
|
331
367
|
});
|
|
@@ -728,30 +728,22 @@ export function SubscriptionInvoice({
|
|
|
728
728
|
}
|
|
729
729
|
lastMonthSyncRef.current = null;
|
|
730
730
|
|
|
731
|
-
const { earliestStart
|
|
732
|
-
userGroups,
|
|
733
|
-
selectedGroupIds
|
|
734
|
-
);
|
|
731
|
+
const { earliestStart } = getGroupsDateRange(userGroups, selectedGroupIds);
|
|
735
732
|
|
|
736
|
-
if (!earliestStart
|
|
733
|
+
if (!earliestStart) return;
|
|
737
734
|
|
|
738
735
|
const currentMonth = getMonthStartDate(selectedMonth);
|
|
739
736
|
if (Number.isNaN(currentMonth.getTime())) return;
|
|
740
737
|
|
|
741
738
|
const earliestMonthStart = getMonthStartDate(earliestStart);
|
|
742
|
-
const latestMonthStart = getMonthStartDate(latestEnd);
|
|
743
739
|
|
|
744
|
-
if (currentMonth < earliestMonthStart
|
|
740
|
+
if (currentMonth < earliestMonthStart) {
|
|
745
741
|
// Only correct once per groups-selection to avoid update loops
|
|
746
742
|
if (lastValidatedKeyRef.current === monthValidationKey) return;
|
|
747
743
|
lastValidatedKeyRef.current = monthValidationKey;
|
|
748
744
|
|
|
749
745
|
const now = getCurrentBillingDate(workspaceTimezone);
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
if (now >= earliestStart && now <= latestEnd) defaultMonth = now;
|
|
753
|
-
else if (now > latestEnd) defaultMonth = latestEnd;
|
|
754
|
-
else defaultMonth = earliestStart;
|
|
746
|
+
const defaultMonth = now >= earliestStart ? now : earliestStart;
|
|
755
747
|
|
|
756
748
|
const nextMonth = formatMonthValue(defaultMonth);
|
|
757
749
|
if (nextMonth !== selectedMonth) {
|
|
@@ -233,7 +233,7 @@ describe('subscription invoice coverage', () => {
|
|
|
233
233
|
).toBe(true);
|
|
234
234
|
});
|
|
235
235
|
|
|
236
|
-
it('
|
|
236
|
+
it('keeps future billing months available after a group ending date', () => {
|
|
237
237
|
const groups = [
|
|
238
238
|
{
|
|
239
239
|
workspace_user_groups: {
|
|
@@ -247,10 +247,11 @@ describe('subscription invoice coverage', () => {
|
|
|
247
247
|
] satisfies UserGroup[];
|
|
248
248
|
|
|
249
249
|
expect(
|
|
250
|
-
getAvailableMonths(groups, [groupId], [], 'en-US', '2026-06',
|
|
251
|
-
(
|
|
252
|
-
|
|
253
|
-
|
|
250
|
+
getAvailableMonths(groups, [groupId], [], 'en-US', '2026-06', 3, {
|
|
251
|
+
now: new Date(2026, 5, 15),
|
|
252
|
+
workspaceTimezone: 'UTC',
|
|
253
|
+
}).map((month) => month.value)
|
|
254
|
+
).toEqual(['2026-06', '2026-07', '2026-08', '2026-09']);
|
|
254
255
|
});
|
|
255
256
|
|
|
256
257
|
it('skips paid months and uses scheduled sessions for future prepaid quantities', () => {
|
|
@@ -524,7 +524,9 @@ export const getAvailableMonths = (
|
|
|
524
524
|
? getMonthStartDate(selectedMonthFallback)
|
|
525
525
|
: currentMonthStart;
|
|
526
526
|
const resolvedRangeEnd =
|
|
527
|
-
futureMonthHorizon > 0 &&
|
|
527
|
+
futureMonthHorizon > 0 && futureHorizonEnd > resolvedLatestEnd
|
|
528
|
+
? futureHorizonEnd
|
|
529
|
+
: resolvedLatestEnd;
|
|
528
530
|
|
|
529
531
|
const months: AvailableMonthOption[] = [];
|
|
530
532
|
const currentDate = new Date(earliestStart);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { render, screen, waitFor } from '@testing-library/react';
|
|
2
2
|
import type { ColumnDef } from '@tuturuuu/ui/custom/tables/data-table';
|
|
3
|
-
import {
|
|
3
|
+
import { renderToString } from 'react-dom/server';
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
5
|
import { transactionColumns } from './columns';
|
|
5
6
|
|
|
6
7
|
vi.mock('next-intl', () => ({
|
|
@@ -82,6 +83,10 @@ describe('transactionColumns', () => {
|
|
|
82
83
|
'finance-confidential-mode=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/;Secure';
|
|
83
84
|
});
|
|
84
85
|
|
|
86
|
+
afterEach(() => {
|
|
87
|
+
vi.useRealTimers();
|
|
88
|
+
});
|
|
89
|
+
|
|
85
90
|
it('masks the amount column when finance numbers are globally hidden', () => {
|
|
86
91
|
render(<TransactionAmountCell amount={123} />);
|
|
87
92
|
|
|
@@ -116,4 +121,23 @@ describe('transactionColumns', () => {
|
|
|
116
121
|
expect(screen.getByText('Cash')).toBeVisible();
|
|
117
122
|
expect(screen.getByText('Food & Beverage')).toBeVisible();
|
|
118
123
|
});
|
|
124
|
+
|
|
125
|
+
it('keeps server-rendered transaction dates stable as time advances', () => {
|
|
126
|
+
vi.useFakeTimers();
|
|
127
|
+
vi.setSystemTime('2026-08-31T00:59:00.000Z');
|
|
128
|
+
|
|
129
|
+
const transaction = {
|
|
130
|
+
taken_at: '2026-08-31T00:00:00.000Z',
|
|
131
|
+
};
|
|
132
|
+
const firstRender = renderToString(
|
|
133
|
+
<TransactionColumnCell accessorKey="taken_at" original={transaction} />
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
vi.setSystemTime('2026-08-31T02:01:00.000Z');
|
|
137
|
+
const laterRender = renderToString(
|
|
138
|
+
<TransactionColumnCell accessorKey="taken_at" original={transaction} />
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
expect(laterRender).toBe(firstRender);
|
|
142
|
+
});
|
|
119
143
|
});
|
|
@@ -12,6 +12,7 @@ import { formatCurrency } from '@tuturuuu/utils/format';
|
|
|
12
12
|
import moment from 'moment';
|
|
13
13
|
import 'moment/locale/vi';
|
|
14
14
|
import { useLocale } from 'next-intl';
|
|
15
|
+
import { useSyncExternalStore } from 'react';
|
|
15
16
|
import { DataTableColumnHeader } from '../../custom/tables/data-table-column-header';
|
|
16
17
|
import {
|
|
17
18
|
FINANCE_HIDDEN_AMOUNT,
|
|
@@ -27,6 +28,44 @@ interface TransactionExtraData {
|
|
|
27
28
|
isPersonalWorkspace?: boolean;
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
const subscribeToHydration = () => () => undefined;
|
|
32
|
+
const getHydratedSnapshot = () => true;
|
|
33
|
+
const getServerSnapshot = () => false;
|
|
34
|
+
|
|
35
|
+
function TransactionDateCell({
|
|
36
|
+
includeTime = false,
|
|
37
|
+
locale,
|
|
38
|
+
value,
|
|
39
|
+
}: {
|
|
40
|
+
includeTime?: boolean;
|
|
41
|
+
locale: string;
|
|
42
|
+
value: unknown;
|
|
43
|
+
}) {
|
|
44
|
+
const isHydrated = useSyncExternalStore(
|
|
45
|
+
subscribeToHydration,
|
|
46
|
+
getHydratedSnapshot,
|
|
47
|
+
getServerSnapshot
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
if (!value) return <div className="min-w-32">-</div>;
|
|
51
|
+
|
|
52
|
+
const localDate = moment(value);
|
|
53
|
+
if (!localDate.isValid()) return <div className="min-w-32">-</div>;
|
|
54
|
+
|
|
55
|
+
const format = includeTime ? 'DD/MM/YYYY, HH:mm:ss' : 'DD/MM/YYYY';
|
|
56
|
+
const absoluteDate = isHydrated
|
|
57
|
+
? localDate.locale(locale).format(format)
|
|
58
|
+
: moment.utc(value).locale(locale).format(format);
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div className="min-w-32">
|
|
62
|
+
{isHydrated && !includeTime
|
|
63
|
+
? `${localDate.locale(locale).fromNow()}, ${absoluteDate}`
|
|
64
|
+
: absoluteDate}
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
30
69
|
function TransactionAmountCell({
|
|
31
70
|
amount,
|
|
32
71
|
currency,
|
|
@@ -265,15 +304,7 @@ export const transactionColumns = ({
|
|
|
265
304
|
/>
|
|
266
305
|
),
|
|
267
306
|
cell: ({ row }) => (
|
|
268
|
-
<
|
|
269
|
-
{row.getValue('taken_at')
|
|
270
|
-
? `${moment(row.getValue('taken_at')).locale(locale).fromNow()}, ${moment(
|
|
271
|
-
row.getValue('taken_at')
|
|
272
|
-
)
|
|
273
|
-
.locale(locale)
|
|
274
|
-
.format('DD/MM/YYYY')}`
|
|
275
|
-
: '-'}
|
|
276
|
-
</div>
|
|
307
|
+
<TransactionDateCell locale={locale} value={row.getValue('taken_at')} />
|
|
277
308
|
),
|
|
278
309
|
},
|
|
279
310
|
{
|
|
@@ -286,13 +317,11 @@ export const transactionColumns = ({
|
|
|
286
317
|
/>
|
|
287
318
|
),
|
|
288
319
|
cell: ({ row }) => (
|
|
289
|
-
<
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
: '-'}
|
|
295
|
-
</div>
|
|
320
|
+
<TransactionDateCell
|
|
321
|
+
includeTime
|
|
322
|
+
locale={locale}
|
|
323
|
+
value={row.getValue('created_at')}
|
|
324
|
+
/>
|
|
296
325
|
),
|
|
297
326
|
},
|
|
298
327
|
{
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { cn } from '@tuturuuu/utils/format';
|
|
2
2
|
import type { ReactNode } from 'react';
|
|
3
|
-
import {
|
|
3
|
+
import { TuturuuuWordmark, toProductName } from '../custom/tuturuuu-wordmark';
|
|
4
4
|
|
|
5
5
|
export interface MarketingFooterLink {
|
|
6
6
|
href: string;
|
|
@@ -16,6 +16,8 @@ export interface MarketingFooterColumn {
|
|
|
16
16
|
|
|
17
17
|
interface MarketingFooterProps {
|
|
18
18
|
appName: string;
|
|
19
|
+
/** Product line of the lockup; defaults to `appName` minus "Tuturuuu ". */
|
|
20
|
+
productName?: string;
|
|
19
21
|
/** One-line positioning statement under the wordmark. */
|
|
20
22
|
tagline: string;
|
|
21
23
|
columns: MarketingFooterColumn[];
|
|
@@ -29,6 +31,7 @@ interface MarketingFooterProps {
|
|
|
29
31
|
/** Shared satellite marketing footer: wordmark column plus link columns. */
|
|
30
32
|
export function MarketingFooter({
|
|
31
33
|
appName,
|
|
34
|
+
productName,
|
|
32
35
|
tagline,
|
|
33
36
|
columns,
|
|
34
37
|
legal,
|
|
@@ -44,12 +47,10 @@ export function MarketingFooter({
|
|
|
44
47
|
>
|
|
45
48
|
<div className="mx-auto grid w-full max-w-6xl gap-12 lg:grid-cols-[1.5fr_repeat(3,1fr)]">
|
|
46
49
|
<div className="max-w-sm">
|
|
47
|
-
<
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
</span>
|
|
52
|
-
</div>
|
|
50
|
+
<TuturuuuWordmark
|
|
51
|
+
product={productName ?? toProductName(appName)}
|
|
52
|
+
size="sm"
|
|
53
|
+
/>
|
|
53
54
|
<p className="mt-4 text-foreground/50 text-sm leading-relaxed">
|
|
54
55
|
{tagline}
|
|
55
56
|
</p>
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { Menu, X } from '@tuturuuu/icons';
|
|
4
4
|
import { cn } from '@tuturuuu/utils/format';
|
|
5
5
|
import { type ReactNode, useEffect, useState } from 'react';
|
|
6
|
-
import {
|
|
6
|
+
import { TuturuuuWordmark, toProductName } from '../custom/tuturuuu-wordmark';
|
|
7
7
|
|
|
8
8
|
export interface MarketingNavLink {
|
|
9
9
|
/** In-page anchor (`#pricing`) or absolute URL. */
|
|
@@ -12,8 +12,17 @@ export interface MarketingNavLink {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
interface MarketingNavProps {
|
|
15
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* The full app name, e.g. "Tuturuuu Forms". Used for the accessible landmark
|
|
17
|
+
* label, and as the fallback source for `productName`.
|
|
18
|
+
*/
|
|
16
19
|
appName: string;
|
|
20
|
+
/**
|
|
21
|
+
* The product line of the lockup — "Forms", not "Tuturuuu Forms". Defaults
|
|
22
|
+
* to `appName` with a leading "Tuturuuu " stripped, so callers that already
|
|
23
|
+
* pass the full name keep working.
|
|
24
|
+
*/
|
|
25
|
+
productName?: string;
|
|
17
26
|
/** Where the wordmark links back to. Defaults to the landing root. */
|
|
18
27
|
homeHref?: string;
|
|
19
28
|
links: MarketingNavLink[];
|
|
@@ -38,6 +47,7 @@ interface MarketingNavProps {
|
|
|
38
47
|
*/
|
|
39
48
|
export function MarketingNav({
|
|
40
49
|
appName,
|
|
50
|
+
productName,
|
|
41
51
|
homeHref = '/',
|
|
42
52
|
links,
|
|
43
53
|
action,
|
|
@@ -79,13 +89,13 @@ export function MarketingNav({
|
|
|
79
89
|
className="mx-auto flex h-16 w-full max-w-7xl items-center justify-between gap-4 px-4 sm:px-6 lg:px-8"
|
|
80
90
|
>
|
|
81
91
|
<a
|
|
82
|
-
className="flex shrink-0 items-center
|
|
92
|
+
className="flex shrink-0 items-center rounded-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
83
93
|
href={homeHref}
|
|
84
94
|
>
|
|
85
|
-
<
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
95
|
+
<TuturuuuWordmark
|
|
96
|
+
product={productName ?? toProductName(appName)}
|
|
97
|
+
size="sm"
|
|
98
|
+
/>
|
|
89
99
|
</a>
|
|
90
100
|
|
|
91
101
|
<div className="hidden items-center gap-1 md:flex">
|
|
@@ -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
|
|
72
|
-
(event: React.
|
|
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
|
-
[
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
104
|
+
onPointerDown={handleCheckboxPointerDown}
|
|
105
|
+
onKeyDown={handleCheckboxKeyDown}
|
|
98
106
|
className={cn(
|
|
99
|
-
'task-list-checkbox flex
|
|
100
|
-
'cursor-pointer rounded-
|
|
101
|
-
'transition-
|
|
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
|
|
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',
|