@tuturuuu/ui 0.4.1 → 0.6.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 +43 -0
- package/package.json +41 -34
- package/src/components/ui/currency-input.tsx +65 -23
- package/src/components/ui/custom/__tests__/sidebar-context.test.tsx +64 -0
- package/src/components/ui/custom/__tests__/sidebar-remote-behavior-bridge.test.tsx +109 -0
- package/src/components/ui/custom/combobox.test.tsx +141 -0
- package/src/components/ui/custom/combobox.tsx +105 -36
- package/src/components/ui/custom/settings/task-settings.tsx +126 -0
- package/src/components/ui/custom/settings/task-sound-settings.test.tsx +146 -0
- package/src/components/ui/custom/sidebar-context.tsx +68 -6
- package/src/components/ui/custom/sidebar-remote-behavior-bridge.tsx +21 -2
- package/src/components/ui/finance/finance-layout.tsx +2 -4
- package/src/components/ui/finance/shared/balance-mode-toggle.tsx +35 -0
- package/src/components/ui/finance/shared/finance-layout-controls.tsx +43 -0
- package/src/components/ui/finance/shared/quick-actions.tsx +14 -6
- package/src/components/ui/finance/shared/use-finance-balance-mode.ts +72 -0
- package/src/components/ui/finance/shared/wallet-balance-mode.test.ts +66 -0
- package/src/components/ui/finance/shared/wallet-balance-mode.ts +42 -0
- package/src/components/ui/finance/transactions/form-types.ts +23 -0
- package/src/components/ui/finance/transactions/form.tsx +81 -22
- package/src/components/ui/finance/transactions/infinite-transactions-list.tsx +29 -18
- package/src/components/ui/finance/transactions/transaction-card.tsx +75 -43
- package/src/components/ui/finance/transactions/transfer-merge.test.ts +90 -0
- package/src/components/ui/finance/transactions/transfer-merge.ts +52 -0
- package/src/components/ui/finance/transactions/wallet-filter.tsx +21 -2
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-adjustment-dialog.tsx +219 -0
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-amount.tsx +32 -0
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-delete-dialog.tsx +50 -0
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-dialog.tsx +138 -0
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-history-dialog.tsx +617 -0
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-panel.tsx +197 -0
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-sections.tsx +201 -0
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoints.test.tsx +541 -0
- package/src/components/ui/finance/wallets/checkpoints/wallet-total-check-dialog.tsx +362 -0
- package/src/components/ui/finance/wallets/columns-rendering.test.tsx +125 -0
- package/src/components/ui/finance/wallets/columns.test.ts +56 -0
- package/src/components/ui/finance/wallets/columns.tsx +196 -43
- package/src/components/ui/finance/wallets/form.test.tsx +79 -14
- package/src/components/ui/finance/wallets/form.tsx +41 -197
- package/src/components/ui/finance/wallets/query-invalidation.ts +3 -0
- package/src/components/ui/finance/wallets/wallet-basics-fields.tsx +141 -0
- package/src/components/ui/finance/wallets/wallet-credit-fields.tsx +136 -0
- package/src/components/ui/finance/wallets/walletId/credit-wallet-summary.tsx +143 -68
- package/src/components/ui/finance/wallets/walletId/wallet-details-actions.test.tsx +105 -0
- package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +120 -16
- package/src/components/ui/finance/wallets/walletId/wallet-details-amount.test.tsx +64 -0
- package/src/components/ui/finance/wallets/walletId/wallet-details-amount.tsx +226 -6
- package/src/components/ui/finance/wallets/walletId/wallet-details-page.test.tsx +71 -5
- package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +52 -35
- package/src/components/ui/finance/wallets/wallets-data-table.test.tsx +171 -0
- package/src/components/ui/finance/wallets/wallets-data-table.tsx +132 -29
- package/src/components/ui/finance/wallets/wallets-page.test.tsx +117 -36
- package/src/components/ui/finance/wallets/wallets-page.tsx +40 -64
- package/src/components/ui/storefront/accent-button.tsx +33 -0
- package/src/components/ui/storefront/cart-summary.tsx +140 -0
- package/src/components/ui/storefront/empty-listings.tsx +32 -0
- package/src/components/ui/storefront/hero-panel.tsx +70 -0
- package/src/components/ui/storefront/image-panel.tsx +40 -0
- package/src/components/ui/storefront/index.ts +12 -0
- package/src/components/ui/storefront/listing-card.tsx +129 -0
- package/src/components/ui/storefront/storefront-surface.test.tsx +85 -0
- package/src/components/ui/storefront/storefront-surface.tsx +235 -0
- package/src/components/ui/storefront/types.ts +99 -0
- package/src/components/ui/storefront/utils.ts +90 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/__tests__/bulk-mutations-move.test.tsx +14 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-operations.ts +29 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.test.ts +134 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.ts +127 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +17 -42
- package/src/components/ui/tu-do/boards/boardId/timeline-board-open-task.test.tsx +164 -0
- package/src/components/ui/tu-do/boards/boardId/timeline-board.tsx +25 -16
- package/src/components/ui/tu-do/hooks/useTaskDialog.ts +15 -1
- package/src/components/ui/tu-do/my-tasks/__tests__/use-task-context-actions.test.ts +11 -0
- package/src/components/ui/tu-do/my-tasks/use-my-tasks-state.ts +2 -0
- package/src/components/ui/tu-do/my-tasks/use-task-context-actions.ts +124 -7
- package/src/components/ui/tu-do/providers/__tests__/task-dialog-provider.test.tsx +217 -5
- package/src/components/ui/tu-do/providers/task-dialog-provider.tsx +180 -35
- package/src/components/ui/tu-do/shared/__tests__/task-dialog-manager.test.tsx +222 -26
- package/src/components/ui/tu-do/shared/board-client.tsx +1 -3
- package/src/components/ui/tu-do/shared/list-view-context-menu.test.tsx +55 -2
- package/src/components/ui/tu-do/shared/list-view.tsx +23 -16
- package/src/components/ui/tu-do/shared/task-dialog-manager.tsx +93 -76
- package/src/components/ui/tu-do/shared/task-dialog-presentation.ts +11 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.test.tsx +268 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.tsx +243 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/quick-settings-popover.tsx +26 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/smart-task-suggestions-panel.test.tsx +129 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/smart-task-suggestions-panel.tsx +358 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-description-editor.tsx +1 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-dialog-header.tsx +6 -2
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-list-selector.tsx +36 -20
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-name-input.test.tsx +41 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-name-input.tsx +157 -102
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-form-reset.ts +18 -2
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-realtime-sync.ts +1 -2
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-save.test.ts +84 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-save.ts +5 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-dialog-actions.tsx +5 -3
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-properties-section.tsx +300 -172
- package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +959 -340
- package/src/components/ui/tu-do/shared/task-sound-effects.test.ts +189 -0
- package/src/components/ui/tu-do/shared/task-sound-effects.tsx +468 -0
- package/src/hooks/__tests__/use-task-actions.test.tsx +61 -0
- package/src/hooks/use-task-actions.ts +45 -0
- package/src/hooks/useBoardRealtime.ts +54 -1
- package/src/hooks/useBoardRealtimeEventHandler.ts +169 -4
- package/src/hooks/useTaskUserRealtime.ts +338 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,48 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.6.0](https://github.com/tutur3u/platform/compare/ui-v0.5.0...ui-v0.6.0) (2026-06-13)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **finance:** add infinite wallet loading ([76eba7a](https://github.com/tutur3u/platform/commit/76eba7a849c3a6b948e231a1e83e1e2faa10bb16))
|
|
9
|
+
* **finance:** add reconciliation defaults and audited balances ([206f941](https://github.com/tutur3u/platform/commit/206f9416351ade0cfbd1ed822595d44843efbaeb))
|
|
10
|
+
* **finance:** add wallet checkpoint audit history ([11139c7](https://github.com/tutur3u/platform/commit/11139c7e354a8f29e83187748711f6ae39c48e70))
|
|
11
|
+
* **finance:** improve credit wallet support ([3a737fe](https://github.com/tutur3u/platform/commit/3a737fe1f1daf2294ca79a8f0f08f85c69697057))
|
|
12
|
+
* **inventory:** add costing and simulated storefront checkout ([7fcdabb](https://github.com/tutur3u/platform/commit/7fcdabb145e6fa9cc899b563b117e65f7772643a))
|
|
13
|
+
* **inventory:** revamp storefront commerce experience ([72a2bde](https://github.com/tutur3u/platform/commit/72a2bde46a1e6c2815d0b2111fc743373c7bec9b))
|
|
14
|
+
* **tasks:** add compact task dialog AI suggestions ([99058e9](https://github.com/tutur3u/platform/commit/99058e90a4f81153f664eb92fdbacade1e2188c6))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Bug Fixes
|
|
18
|
+
|
|
19
|
+
* **finance:** improve wallet credit limit and balance ux ([6295d5c](https://github.com/tutur3u/platform/commit/6295d5c0013b2bbe064855712a984d17225d3271))
|
|
20
|
+
* **finance:** reveal wallet audit context on hover ([8b78454](https://github.com/tutur3u/platform/commit/8b784543093243cd44bacec0f0ab59cb4cac7ec7))
|
|
21
|
+
* **inventory:** improve combobox creation and overflow ([a1e1a78](https://github.com/tutur3u/platform/commit/a1e1a78d66384d54b59460f42d04349c4e358cba))
|
|
22
|
+
* **sidebar:** persist collapsed state across refresh ([cb0eb6d](https://github.com/tutur3u/platform/commit/cb0eb6d0d30ecc8b3f3231255f9906e60a895f04))
|
|
23
|
+
* **tasks:** hydrate external dialogs from source workspace ([95a7a23](https://github.com/tutur3u/platform/commit/95a7a23ec8957918cffc81698e8fdc8951adf400))
|
|
24
|
+
* **tasks:** open external task dialogs immediately ([d1535f3](https://github.com/tutur3u/platform/commit/d1535f377cf0d39c5f73d28566322d8dbbdd8331))
|
|
25
|
+
* **tasks:** open task dialogs immediately ([7980e66](https://github.com/tutur3u/platform/commit/7980e66b6a11cd62b9e04ad6d421deee915b2dea))
|
|
26
|
+
* **tasks:** place task title caret at end ([27af729](https://github.com/tutur3u/platform/commit/27af729a673c2bb712819a6d2d95ba0198a558ce))
|
|
27
|
+
* **tasks:** prevent external dialog hydration flash ([8aa7765](https://github.com/tutur3u/platform/commit/8aa7765dfa235c240e2156e404c6255909ec7aee))
|
|
28
|
+
* **tasks:** refine compact task dialog actions ([dcf6b03](https://github.com/tutur3u/platform/commit/dcf6b033f2c134d115d74b53d4b875c16cd7070c))
|
|
29
|
+
* **tasks:** sync task realtime with broadcasts ([8c56154](https://github.com/tutur3u/platform/commit/8c56154e517797dcac0ec0971d8a474b50292706))
|
|
30
|
+
* **ui:** make package graph installable ([f3eb0ff](https://github.com/tutur3u/platform/commit/f3eb0ff3cbed2e43fd77dfb8164e60c5d195a36b))
|
|
31
|
+
|
|
32
|
+
## [0.5.0](https://github.com/tutur3u/platform/compare/ui-v0.4.1...ui-v0.5.0) (2026-06-11)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
### Features
|
|
36
|
+
|
|
37
|
+
* **finance:** add wallet checkpoints ([54f9f29](https://github.com/tutur3u/platform/commit/54f9f29446ff9991e09a68abb258ce66c640b086))
|
|
38
|
+
* **tasks:** add compact task create popover ([6c4b957](https://github.com/tutur3u/platform/commit/6c4b957634136a57e3ceb4ba1fc2f151c8a04314))
|
|
39
|
+
* **tasks:** add task sound effects ([7c4cb06](https://github.com/tutur3u/platform/commit/7c4cb06f8f134db201f54294c3c2641ae9ae5d07))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
### Bug Fixes
|
|
43
|
+
|
|
44
|
+
* **finance:** merge transfer rows and sync wallet icons ([084e1ac](https://github.com/tutur3u/platform/commit/084e1ac662a3f41c59cfc54d58fa5897293697d2))
|
|
45
|
+
|
|
3
46
|
## [0.4.1](https://github.com/tutur3u/platform/compare/ui-v0.4.0...ui-v0.4.1) (2026-06-11)
|
|
4
47
|
|
|
5
48
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tuturuuu/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -60,36 +60,35 @@
|
|
|
60
60
|
"@tanstack/react-query": "^5.101.0",
|
|
61
61
|
"@tanstack/react-table": "^8.21.3",
|
|
62
62
|
"@tanstack/react-virtual": "^3.14.2",
|
|
63
|
-
"@tiptap/core": "3.26.
|
|
64
|
-
"@tiptap/extension-collaboration": "3.26.
|
|
65
|
-
"@tiptap/extension-collaboration-caret": "3.26.
|
|
66
|
-
"@tiptap/extension-drag-handle-react": "3.26.
|
|
67
|
-
"@tiptap/extension-highlight": "3.26.
|
|
68
|
-
"@tiptap/extension-horizontal-rule": "3.26.
|
|
69
|
-
"@tiptap/extension-image": "3.26.
|
|
70
|
-
"@tiptap/extension-link": "3.26.
|
|
71
|
-
"@tiptap/extension-list": "3.26.
|
|
72
|
-
"@tiptap/extension-placeholder": "3.26.
|
|
73
|
-
"@tiptap/extension-strike": "3.26.
|
|
74
|
-
"@tiptap/extension-subscript": "3.26.
|
|
75
|
-
"@tiptap/extension-superscript": "3.26.
|
|
76
|
-
"@tiptap/extension-table": "3.26.
|
|
77
|
-
"@tiptap/extension-table-cell": "3.26.
|
|
78
|
-
"@tiptap/extension-table-header": "3.26.
|
|
79
|
-
"@tiptap/extension-table-row": "3.26.
|
|
80
|
-
"@tiptap/extension-text-align": "3.26.
|
|
81
|
-
"@tiptap/extension-youtube": "3.26.
|
|
82
|
-
"@tiptap/pm": "3.26.
|
|
83
|
-
"@tiptap/react": "3.26.
|
|
84
|
-
"@tiptap/starter-kit": "3.26.
|
|
63
|
+
"@tiptap/core": "3.26.1",
|
|
64
|
+
"@tiptap/extension-collaboration": "3.26.1",
|
|
65
|
+
"@tiptap/extension-collaboration-caret": "3.26.1",
|
|
66
|
+
"@tiptap/extension-drag-handle-react": "3.26.1",
|
|
67
|
+
"@tiptap/extension-highlight": "3.26.1",
|
|
68
|
+
"@tiptap/extension-horizontal-rule": "3.26.1",
|
|
69
|
+
"@tiptap/extension-image": "3.26.1",
|
|
70
|
+
"@tiptap/extension-link": "3.26.1",
|
|
71
|
+
"@tiptap/extension-list": "3.26.1",
|
|
72
|
+
"@tiptap/extension-placeholder": "3.26.1",
|
|
73
|
+
"@tiptap/extension-strike": "3.26.1",
|
|
74
|
+
"@tiptap/extension-subscript": "3.26.1",
|
|
75
|
+
"@tiptap/extension-superscript": "3.26.1",
|
|
76
|
+
"@tiptap/extension-table": "3.26.1",
|
|
77
|
+
"@tiptap/extension-table-cell": "3.26.1",
|
|
78
|
+
"@tiptap/extension-table-header": "3.26.1",
|
|
79
|
+
"@tiptap/extension-table-row": "3.26.1",
|
|
80
|
+
"@tiptap/extension-text-align": "3.26.1",
|
|
81
|
+
"@tiptap/extension-youtube": "3.26.1",
|
|
82
|
+
"@tiptap/pm": "3.26.1",
|
|
83
|
+
"@tiptap/react": "3.26.1",
|
|
84
|
+
"@tiptap/starter-kit": "3.26.1",
|
|
85
85
|
"@tuturuuu/ai": "0.2.1",
|
|
86
|
-
"@tuturuuu/apis": "0.
|
|
87
|
-
"@tuturuuu/hooks": "0.0.
|
|
88
|
-
"@tuturuuu/icons": "0.0.
|
|
89
|
-
"@tuturuuu/internal-api": "0.
|
|
86
|
+
"@tuturuuu/apis": "0.4.0",
|
|
87
|
+
"@tuturuuu/hooks": "0.0.2",
|
|
88
|
+
"@tuturuuu/icons": "0.0.6",
|
|
89
|
+
"@tuturuuu/internal-api": "0.7.0",
|
|
90
90
|
"@tuturuuu/supabase": "0.3.3",
|
|
91
|
-
"@tuturuuu/
|
|
92
|
-
"@tuturuuu/utils": "0.5.1",
|
|
91
|
+
"@tuturuuu/utils": "0.6.1",
|
|
93
92
|
"@types/debug": "^4.1.13",
|
|
94
93
|
"browser-image-compression": "^2.0.2",
|
|
95
94
|
"class-variance-authority": "^0.7.1",
|
|
@@ -109,7 +108,7 @@
|
|
|
109
108
|
"lodash": "^4.18.1",
|
|
110
109
|
"moment": "^2.30.1",
|
|
111
110
|
"motion": "^12.40.0",
|
|
112
|
-
"next": "^16.2.
|
|
111
|
+
"next": "^16.2.9",
|
|
113
112
|
"next-intl": "^4.13.0",
|
|
114
113
|
"next-themes": "^0.4.6",
|
|
115
114
|
"nuqs": "^2.8.9",
|
|
@@ -136,23 +135,23 @@
|
|
|
136
135
|
"tiptap-extension-resize-image": "^1.4.3",
|
|
137
136
|
"use-debounce": "^10.1.1",
|
|
138
137
|
"vaul": "^1.1.2",
|
|
139
|
-
"xlsx": "
|
|
138
|
+
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
|
|
140
139
|
"y-protocols": "^1.0.7",
|
|
141
140
|
"yjs": "^13.6.31",
|
|
142
141
|
"zod": "^4.4.3"
|
|
143
142
|
},
|
|
144
143
|
"devDependencies": {
|
|
145
144
|
"@tailwindcss/postcss": "^4.3.0",
|
|
146
|
-
"@tailwindcss/typography": "^0.5.
|
|
145
|
+
"@tailwindcss/typography": "^0.5.20",
|
|
147
146
|
"@tanstack/react-query": "^5.101.0",
|
|
148
147
|
"@tanstack/react-table": "^8.21.3",
|
|
149
148
|
"@testing-library/jest-dom": "^6.9.1",
|
|
150
149
|
"@testing-library/react": "^16.3.2",
|
|
151
|
-
"@tuturuuu/types": "0.
|
|
150
|
+
"@tuturuuu/types": "0.8.0",
|
|
152
151
|
"@tuturuuu/typescript-config": "0.1.1",
|
|
153
152
|
"@types/html2canvas": "^1.0.0",
|
|
154
153
|
"@types/lodash": "^4.17.24",
|
|
155
|
-
"@types/node": "^25.9.
|
|
154
|
+
"@types/node": "^25.9.3",
|
|
156
155
|
"@types/react": "^19.2.17",
|
|
157
156
|
"@types/react-dom": "^19.2.3",
|
|
158
157
|
"@types/react-resizable": "^4.0.0",
|
|
@@ -183,6 +182,10 @@
|
|
|
183
182
|
"types": "./src/components/ui/button.tsx",
|
|
184
183
|
"import": "./src/components/ui/button.tsx"
|
|
185
184
|
},
|
|
185
|
+
"./chart": {
|
|
186
|
+
"types": "./src/components/ui/chart.tsx",
|
|
187
|
+
"import": "./src/components/ui/chart.tsx"
|
|
188
|
+
},
|
|
186
189
|
"./command": {
|
|
187
190
|
"types": "./src/components/ui/command.tsx",
|
|
188
191
|
"import": "./src/components/ui/command.tsx"
|
|
@@ -207,6 +210,10 @@
|
|
|
207
210
|
"types": "./src/components/ui/sonner.tsx",
|
|
208
211
|
"import": "./src/components/ui/sonner.tsx"
|
|
209
212
|
},
|
|
213
|
+
"./storefront": {
|
|
214
|
+
"types": "./src/components/ui/storefront/index.ts",
|
|
215
|
+
"import": "./src/components/ui/storefront/index.ts"
|
|
216
|
+
},
|
|
210
217
|
"./hooks/use-board-actions": "./src/hooks/use-board-actions.ts",
|
|
211
218
|
"./hooks/*": "./src/hooks/*.ts",
|
|
212
219
|
"./hooks/time-blocking-provider": "./src/hooks/time-blocking-provider.tsx",
|
|
@@ -50,6 +50,47 @@ function getRoundingUnit(v: number): number {
|
|
|
50
50
|
return 1;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
function escapeRegExp(value: string): string {
|
|
54
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function getLocaleNumberSeparators(locale: string) {
|
|
58
|
+
const parts = new Intl.NumberFormat(locale).formatToParts(1000.1);
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
decimal: parts.find((part) => part.type === 'decimal')?.value ?? '.',
|
|
62
|
+
group: parts.find((part) => part.type === 'group')?.value ?? ',',
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function normalizeCurrencyInputText(
|
|
67
|
+
value: string,
|
|
68
|
+
separators: ReturnType<typeof getLocaleNumberSeparators>
|
|
69
|
+
) {
|
|
70
|
+
let normalized = value;
|
|
71
|
+
|
|
72
|
+
if (separators.group) {
|
|
73
|
+
normalized = normalized.replace(
|
|
74
|
+
new RegExp(escapeRegExp(separators.group), 'g'),
|
|
75
|
+
''
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (separators.decimal && separators.decimal !== '.') {
|
|
80
|
+
normalized = normalized.replace(
|
|
81
|
+
new RegExp(escapeRegExp(separators.decimal), 'g'),
|
|
82
|
+
'.'
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const numericText = normalized.replace(/[^\d.]/g, '');
|
|
87
|
+
const [integerPart = '', ...fractionParts] = numericText.split('.');
|
|
88
|
+
|
|
89
|
+
if (fractionParts.length === 0) return integerPart;
|
|
90
|
+
|
|
91
|
+
return `${integerPart}.${fractionParts.join('')}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
53
94
|
/**
|
|
54
95
|
* A currency input component that formats numbers while preserving cursor position.
|
|
55
96
|
*
|
|
@@ -87,6 +128,10 @@ export const CurrencyInput = forwardRef<HTMLInputElement, CurrencyInputProps>(
|
|
|
87
128
|
|
|
88
129
|
// Expose the input ref
|
|
89
130
|
useImperativeHandle(ref, () => inputRef.current as HTMLInputElement);
|
|
131
|
+
const localeSeparators = useMemo(
|
|
132
|
+
() => getLocaleNumberSeparators(locale),
|
|
133
|
+
[locale]
|
|
134
|
+
);
|
|
90
135
|
|
|
91
136
|
// Format number for display (with thousand separators)
|
|
92
137
|
const formatForDisplay = useCallback(
|
|
@@ -101,20 +146,17 @@ export const CurrencyInput = forwardRef<HTMLInputElement, CurrencyInputProps>(
|
|
|
101
146
|
);
|
|
102
147
|
|
|
103
148
|
// Parse display string back to number
|
|
104
|
-
const parseValue = useCallback(
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
.
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const parsed = parseFloat(normalized);
|
|
116
|
-
return Number.isNaN(parsed) ? 0 : parsed;
|
|
117
|
-
}, []);
|
|
149
|
+
const parseValue = useCallback(
|
|
150
|
+
(str: string): number => {
|
|
151
|
+
if (!str) return 0;
|
|
152
|
+
|
|
153
|
+
const parsed = parseFloat(
|
|
154
|
+
normalizeCurrencyInputText(str, localeSeparators)
|
|
155
|
+
);
|
|
156
|
+
return Number.isNaN(parsed) ? 0 : parsed;
|
|
157
|
+
},
|
|
158
|
+
[localeSeparators]
|
|
159
|
+
);
|
|
118
160
|
|
|
119
161
|
// Format the raw input while preserving cursor position
|
|
120
162
|
const formatWithCursor = useCallback(
|
|
@@ -122,10 +164,10 @@ export const CurrencyInput = forwardRef<HTMLInputElement, CurrencyInputProps>(
|
|
|
122
164
|
rawValue: string,
|
|
123
165
|
cursorPos: number
|
|
124
166
|
): { formatted: string; newCursor: number } => {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
167
|
+
const cleanValue = normalizeCurrencyInputText(
|
|
168
|
+
rawValue,
|
|
169
|
+
localeSeparators
|
|
170
|
+
);
|
|
129
171
|
const parts = cleanValue.split('.');
|
|
130
172
|
let normalized = parts[0] || '';
|
|
131
173
|
if (parts.length > 1) {
|
|
@@ -162,7 +204,7 @@ export const CurrencyInput = forwardRef<HTMLInputElement, CurrencyInputProps>(
|
|
|
162
204
|
// Count how many digits are before the cursor in the original
|
|
163
205
|
let digitsBeforeCursor = 0;
|
|
164
206
|
for (let i = 0; i < cursorPos && i < rawValue.length; i++) {
|
|
165
|
-
if (/[\d
|
|
207
|
+
if (/[\d.,]/.test(rawValue[i]!)) {
|
|
166
208
|
digitsBeforeCursor++;
|
|
167
209
|
}
|
|
168
210
|
}
|
|
@@ -172,7 +214,7 @@ export const CurrencyInput = forwardRef<HTMLInputElement, CurrencyInputProps>(
|
|
|
172
214
|
let digitCount = 0;
|
|
173
215
|
for (let i = 0; i < formatted.length; i++) {
|
|
174
216
|
if (digitCount >= digitsBeforeCursor) break;
|
|
175
|
-
if (/[\d
|
|
217
|
+
if (/[\d.,]/.test(formatted[i]!)) {
|
|
176
218
|
digitCount++;
|
|
177
219
|
}
|
|
178
220
|
newCursor = i + 1;
|
|
@@ -180,7 +222,7 @@ export const CurrencyInput = forwardRef<HTMLInputElement, CurrencyInputProps>(
|
|
|
180
222
|
|
|
181
223
|
return { formatted, newCursor };
|
|
182
224
|
},
|
|
183
|
-
[locale, maximumFractionDigits]
|
|
225
|
+
[locale, localeSeparators, maximumFractionDigits]
|
|
184
226
|
);
|
|
185
227
|
|
|
186
228
|
// Sync external value to display
|
|
@@ -261,8 +303,8 @@ export const CurrencyInput = forwardRef<HTMLInputElement, CurrencyInputProps>(
|
|
|
261
303
|
return;
|
|
262
304
|
}
|
|
263
305
|
|
|
264
|
-
// Allow numbers and decimal
|
|
265
|
-
if (/^[\d
|
|
306
|
+
// Allow numbers and common decimal separators
|
|
307
|
+
if (/^[\d.,]$/.test(e.key)) return;
|
|
266
308
|
|
|
267
309
|
// Block all other keys
|
|
268
310
|
e.preventDefault();
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import {
|
|
4
|
+
SIDEBAR_BEHAVIOR_COOKIE_NAME,
|
|
5
|
+
SIDEBAR_BEHAVIOR_UPDATED_AT_COOKIE_NAME,
|
|
6
|
+
SIDEBAR_COOKIE_OPTIONS,
|
|
7
|
+
SidebarProvider,
|
|
8
|
+
useSidebar,
|
|
9
|
+
} from '../sidebar-context';
|
|
10
|
+
|
|
11
|
+
const { mockSetCookie } = vi.hoisted(() => ({
|
|
12
|
+
mockSetCookie: vi.fn(),
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
vi.mock('cookies-next', () => ({
|
|
16
|
+
setCookie: mockSetCookie,
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
vi.mock('../sidebar-remote-behavior-bridge', () => ({
|
|
20
|
+
SidebarRemoteBehaviorBridge: () => null,
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
function SidebarHarness() {
|
|
24
|
+
const { behavior, handleBehaviorChange } = useSidebar();
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<button type="button" onClick={() => handleBehaviorChange('collapsed')}>
|
|
28
|
+
{behavior}
|
|
29
|
+
</button>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe('SidebarProvider', () => {
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
vi.restoreAllMocks();
|
|
36
|
+
mockSetCookie.mockClear();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('writes durable behavior and timestamp cookies for user behavior changes', async () => {
|
|
40
|
+
vi.spyOn(Date, 'now').mockReturnValue(1_234_567_890);
|
|
41
|
+
|
|
42
|
+
render(
|
|
43
|
+
<SidebarProvider initialBehavior="expanded">
|
|
44
|
+
<SidebarHarness />
|
|
45
|
+
</SidebarProvider>
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
fireEvent.click(screen.getByRole('button', { name: 'expanded' }));
|
|
49
|
+
|
|
50
|
+
await waitFor(() =>
|
|
51
|
+
expect(screen.getByRole('button', { name: 'collapsed' })).toBeVisible()
|
|
52
|
+
);
|
|
53
|
+
expect(mockSetCookie).toHaveBeenCalledWith(
|
|
54
|
+
SIDEBAR_BEHAVIOR_COOKIE_NAME,
|
|
55
|
+
'collapsed',
|
|
56
|
+
SIDEBAR_COOKIE_OPTIONS
|
|
57
|
+
);
|
|
58
|
+
expect(mockSetCookie).toHaveBeenCalledWith(
|
|
59
|
+
SIDEBAR_BEHAVIOR_UPDATED_AT_COOKIE_NAME,
|
|
60
|
+
'1234567890',
|
|
61
|
+
SIDEBAR_COOKIE_OPTIONS
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { render, waitFor } from '@testing-library/react';
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { SidebarRemoteBehaviorBridge } from '../sidebar-remote-behavior-bridge';
|
|
4
|
+
|
|
5
|
+
const { mockConfigState, mockUpdateConfigMutate } = vi.hoisted(() => ({
|
|
6
|
+
mockConfigState: {
|
|
7
|
+
remoteBehavior: 'expanded' as string | undefined,
|
|
8
|
+
remoteLoaded: true,
|
|
9
|
+
},
|
|
10
|
+
mockUpdateConfigMutate: vi.fn(),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
vi.mock('@tuturuuu/ui/hooks/use-user-config', () => ({
|
|
14
|
+
useUpdateUserConfig: () => ({
|
|
15
|
+
mutate: mockUpdateConfigMutate,
|
|
16
|
+
}),
|
|
17
|
+
useUserConfig: () => ({
|
|
18
|
+
data: mockConfigState.remoteBehavior,
|
|
19
|
+
isSuccess: mockConfigState.remoteLoaded,
|
|
20
|
+
}),
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
function renderBridge({
|
|
24
|
+
behavior = 'collapsed',
|
|
25
|
+
behaviorUpdatedAt = null,
|
|
26
|
+
localOverride = false,
|
|
27
|
+
localOverrideVersion = 0,
|
|
28
|
+
onApplyRemoteBehavior = vi.fn(),
|
|
29
|
+
onRemoteBehaviorAvailable = vi.fn(),
|
|
30
|
+
userChangeVersion = 0,
|
|
31
|
+
}: Partial<Parameters<typeof SidebarRemoteBehaviorBridge>[0]> = {}) {
|
|
32
|
+
render(
|
|
33
|
+
<SidebarRemoteBehaviorBridge
|
|
34
|
+
behavior={behavior}
|
|
35
|
+
behaviorUpdatedAt={behaviorUpdatedAt}
|
|
36
|
+
localOverride={localOverride}
|
|
37
|
+
localOverrideVersion={localOverrideVersion}
|
|
38
|
+
onApplyRemoteBehavior={onApplyRemoteBehavior}
|
|
39
|
+
onRemoteBehaviorAvailable={onRemoteBehaviorAvailable}
|
|
40
|
+
userChangeVersion={userChangeVersion}
|
|
41
|
+
/>
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
return { onApplyRemoteBehavior, onRemoteBehaviorAvailable };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
describe('SidebarRemoteBehaviorBridge', () => {
|
|
48
|
+
beforeEach(() => {
|
|
49
|
+
mockConfigState.remoteBehavior = 'expanded';
|
|
50
|
+
mockConfigState.remoteLoaded = true;
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
afterEach(() => {
|
|
54
|
+
vi.clearAllMocks();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('keeps a recent local behavior and persists it over stale remote behavior', async () => {
|
|
58
|
+
const { onApplyRemoteBehavior } = renderBridge({
|
|
59
|
+
behavior: 'collapsed',
|
|
60
|
+
behaviorUpdatedAt: Date.now(),
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
await waitFor(() =>
|
|
64
|
+
expect(mockUpdateConfigMutate).toHaveBeenCalledWith({
|
|
65
|
+
configId: 'SIDEBAR_BEHAVIOR',
|
|
66
|
+
value: 'collapsed',
|
|
67
|
+
})
|
|
68
|
+
);
|
|
69
|
+
expect(onApplyRemoteBehavior).not.toHaveBeenCalled();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('applies stale remote behavior when the local timestamp is stale', async () => {
|
|
73
|
+
const { onApplyRemoteBehavior } = renderBridge({
|
|
74
|
+
behavior: 'collapsed',
|
|
75
|
+
behaviorUpdatedAt: Date.now() - 6 * 60 * 1000,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
await waitFor(() =>
|
|
79
|
+
expect(onApplyRemoteBehavior).toHaveBeenCalledWith('expanded')
|
|
80
|
+
);
|
|
81
|
+
expect(mockUpdateConfigMutate).not.toHaveBeenCalled();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('applies stale remote behavior when there is no local timestamp', async () => {
|
|
85
|
+
const { onApplyRemoteBehavior } = renderBridge({
|
|
86
|
+
behavior: 'collapsed',
|
|
87
|
+
behaviorUpdatedAt: null,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
await waitFor(() =>
|
|
91
|
+
expect(onApplyRemoteBehavior).toHaveBeenCalledWith('expanded')
|
|
92
|
+
);
|
|
93
|
+
expect(mockUpdateConfigMutate).not.toHaveBeenCalled();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('does not apply remote behavior while local override is enabled', async () => {
|
|
97
|
+
const { onApplyRemoteBehavior, onRemoteBehaviorAvailable } = renderBridge({
|
|
98
|
+
behavior: 'collapsed',
|
|
99
|
+
behaviorUpdatedAt: null,
|
|
100
|
+
localOverride: true,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
await waitFor(() =>
|
|
104
|
+
expect(onRemoteBehaviorAvailable).toHaveBeenCalledWith('expanded')
|
|
105
|
+
);
|
|
106
|
+
expect(onApplyRemoteBehavior).not.toHaveBeenCalled();
|
|
107
|
+
expect(mockUpdateConfigMutate).not.toHaveBeenCalled();
|
|
108
|
+
});
|
|
109
|
+
});
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
2
|
+
import { beforeAll, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { Combobox } from './combobox';
|
|
4
|
+
|
|
5
|
+
beforeAll(() => {
|
|
6
|
+
class ResizeObserverMock {
|
|
7
|
+
observe() {}
|
|
8
|
+
unobserve() {}
|
|
9
|
+
disconnect() {}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
vi.stubGlobal('ResizeObserver', ResizeObserverMock);
|
|
13
|
+
window.HTMLElement.prototype.scrollIntoView = vi.fn();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const options = [
|
|
17
|
+
{ label: 'Alpha', value: 'alpha' },
|
|
18
|
+
{ label: 'Beta', value: 'beta' },
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
function openCombobox() {
|
|
22
|
+
fireEvent.click(screen.getByRole('combobox'));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe('Combobox', () => {
|
|
26
|
+
it('shows create row when the query has no exact normalized match', () => {
|
|
27
|
+
render(
|
|
28
|
+
<Combobox
|
|
29
|
+
createText="Create"
|
|
30
|
+
onCreate={vi.fn()}
|
|
31
|
+
options={options}
|
|
32
|
+
placeholder="Pick item"
|
|
33
|
+
searchPlaceholder="Search items"
|
|
34
|
+
selected=""
|
|
35
|
+
/>
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
openCombobox();
|
|
39
|
+
fireEvent.change(screen.getByPlaceholderText('Search items'), {
|
|
40
|
+
target: { value: 'Alph' },
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
expect(screen.getByText('Alph')).toBeInTheDocument();
|
|
44
|
+
expect(screen.getByText('Create')).toBeInTheDocument();
|
|
45
|
+
|
|
46
|
+
fireEvent.change(screen.getByPlaceholderText('Search items'), {
|
|
47
|
+
target: { value: 'Alpha' },
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
expect(screen.queryByText('Create')).not.toBeInTheDocument();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('prevents duplicate async creates and selects returned options', async () => {
|
|
54
|
+
const onChange = vi.fn();
|
|
55
|
+
let resolveCreate:
|
|
56
|
+
| ((value: { label: string; value: string }) => void)
|
|
57
|
+
| undefined;
|
|
58
|
+
const onCreate = vi.fn(
|
|
59
|
+
() =>
|
|
60
|
+
new Promise<{ label: string; value: string }>((resolve) => {
|
|
61
|
+
resolveCreate = resolve;
|
|
62
|
+
})
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
render(
|
|
66
|
+
<Combobox
|
|
67
|
+
creatingText="Creating"
|
|
68
|
+
createText="Create"
|
|
69
|
+
onChange={onChange}
|
|
70
|
+
onCreate={onCreate}
|
|
71
|
+
options={options}
|
|
72
|
+
placeholder="Pick item"
|
|
73
|
+
searchPlaceholder="Search items"
|
|
74
|
+
selected=""
|
|
75
|
+
/>
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
openCombobox();
|
|
79
|
+
fireEvent.change(screen.getByPlaceholderText('Search items'), {
|
|
80
|
+
target: { value: 'Gamma' },
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
fireEvent.click(screen.getByText('Gamma'));
|
|
84
|
+
fireEvent.click(screen.getByText('Gamma'));
|
|
85
|
+
|
|
86
|
+
expect(onCreate).toHaveBeenCalledTimes(1);
|
|
87
|
+
expect(screen.getByText('Creating')).toBeInTheDocument();
|
|
88
|
+
|
|
89
|
+
resolveCreate?.({ label: 'Gamma', value: 'gamma' });
|
|
90
|
+
|
|
91
|
+
await waitFor(() => expect(onChange).toHaveBeenCalledWith('gamma'));
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('selects returned string values from create callbacks', async () => {
|
|
95
|
+
const onChange = vi.fn();
|
|
96
|
+
|
|
97
|
+
render(
|
|
98
|
+
<Combobox
|
|
99
|
+
createText="Create"
|
|
100
|
+
onChange={onChange}
|
|
101
|
+
onCreate={() => 'delta'}
|
|
102
|
+
options={options}
|
|
103
|
+
placeholder="Pick item"
|
|
104
|
+
searchPlaceholder="Search items"
|
|
105
|
+
selected=""
|
|
106
|
+
/>
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
openCombobox();
|
|
110
|
+
fireEvent.change(screen.getByPlaceholderText('Search items'), {
|
|
111
|
+
target: { value: 'Delta' },
|
|
112
|
+
});
|
|
113
|
+
fireEvent.click(screen.getByText('Delta'));
|
|
114
|
+
|
|
115
|
+
await waitFor(() => expect(onChange).toHaveBeenCalledWith('delta'));
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('keeps action rendering backward compatible', () => {
|
|
119
|
+
const onSelect = vi.fn();
|
|
120
|
+
|
|
121
|
+
render(
|
|
122
|
+
<Combobox
|
|
123
|
+
actions={[
|
|
124
|
+
{
|
|
125
|
+
key: 'manage',
|
|
126
|
+
label: 'Manage options',
|
|
127
|
+
onSelect,
|
|
128
|
+
},
|
|
129
|
+
]}
|
|
130
|
+
options={options}
|
|
131
|
+
placeholder="Pick item"
|
|
132
|
+
selected=""
|
|
133
|
+
/>
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
openCombobox();
|
|
137
|
+
fireEvent.click(screen.getByText('Manage options'));
|
|
138
|
+
|
|
139
|
+
expect(onSelect).toHaveBeenCalledTimes(1);
|
|
140
|
+
});
|
|
141
|
+
});
|