@tuturuuu/ui 0.5.0 → 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.
Files changed (88) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/package.json +41 -34
  3. package/src/components/ui/currency-input.tsx +65 -23
  4. package/src/components/ui/custom/__tests__/sidebar-context.test.tsx +64 -0
  5. package/src/components/ui/custom/__tests__/sidebar-remote-behavior-bridge.test.tsx +109 -0
  6. package/src/components/ui/custom/combobox.test.tsx +141 -0
  7. package/src/components/ui/custom/combobox.tsx +105 -36
  8. package/src/components/ui/custom/settings/task-settings.tsx +50 -0
  9. package/src/components/ui/custom/settings/task-sound-settings.test.tsx +21 -1
  10. package/src/components/ui/custom/sidebar-context.tsx +68 -6
  11. package/src/components/ui/custom/sidebar-remote-behavior-bridge.tsx +21 -2
  12. package/src/components/ui/finance/finance-layout.tsx +2 -4
  13. package/src/components/ui/finance/shared/balance-mode-toggle.tsx +35 -0
  14. package/src/components/ui/finance/shared/finance-layout-controls.tsx +43 -0
  15. package/src/components/ui/finance/shared/quick-actions.tsx +14 -6
  16. package/src/components/ui/finance/shared/use-finance-balance-mode.ts +72 -0
  17. package/src/components/ui/finance/shared/wallet-balance-mode.test.ts +66 -0
  18. package/src/components/ui/finance/shared/wallet-balance-mode.ts +42 -0
  19. package/src/components/ui/finance/transactions/form-types.ts +23 -0
  20. package/src/components/ui/finance/transactions/form.tsx +81 -22
  21. package/src/components/ui/finance/transactions/wallet-filter.tsx +21 -2
  22. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-adjustment-dialog.tsx +73 -26
  23. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-history-dialog.tsx +617 -0
  24. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-panel.tsx +2 -1
  25. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-sections.tsx +4 -4
  26. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoints.test.tsx +298 -34
  27. package/src/components/ui/finance/wallets/checkpoints/wallet-total-check-dialog.tsx +219 -46
  28. package/src/components/ui/finance/wallets/columns-rendering.test.tsx +125 -0
  29. package/src/components/ui/finance/wallets/columns.test.ts +56 -0
  30. package/src/components/ui/finance/wallets/columns.tsx +196 -43
  31. package/src/components/ui/finance/wallets/form.test.tsx +79 -14
  32. package/src/components/ui/finance/wallets/form.tsx +41 -197
  33. package/src/components/ui/finance/wallets/query-invalidation.ts +1 -0
  34. package/src/components/ui/finance/wallets/wallet-basics-fields.tsx +141 -0
  35. package/src/components/ui/finance/wallets/wallet-credit-fields.tsx +136 -0
  36. package/src/components/ui/finance/wallets/walletId/credit-wallet-summary.tsx +143 -68
  37. package/src/components/ui/finance/wallets/walletId/wallet-details-actions.test.tsx +105 -0
  38. package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +120 -16
  39. package/src/components/ui/finance/wallets/walletId/wallet-details-amount.test.tsx +64 -0
  40. package/src/components/ui/finance/wallets/walletId/wallet-details-amount.tsx +226 -6
  41. package/src/components/ui/finance/wallets/walletId/wallet-details-page.test.tsx +64 -2
  42. package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +42 -35
  43. package/src/components/ui/finance/wallets/wallets-data-table.test.tsx +171 -0
  44. package/src/components/ui/finance/wallets/wallets-data-table.tsx +132 -29
  45. package/src/components/ui/finance/wallets/wallets-page.test.tsx +111 -37
  46. package/src/components/ui/finance/wallets/wallets-page.tsx +38 -78
  47. package/src/components/ui/storefront/accent-button.tsx +33 -0
  48. package/src/components/ui/storefront/cart-summary.tsx +140 -0
  49. package/src/components/ui/storefront/empty-listings.tsx +32 -0
  50. package/src/components/ui/storefront/hero-panel.tsx +70 -0
  51. package/src/components/ui/storefront/image-panel.tsx +40 -0
  52. package/src/components/ui/storefront/index.ts +12 -0
  53. package/src/components/ui/storefront/listing-card.tsx +129 -0
  54. package/src/components/ui/storefront/storefront-surface.test.tsx +85 -0
  55. package/src/components/ui/storefront/storefront-surface.tsx +235 -0
  56. package/src/components/ui/storefront/types.ts +99 -0
  57. package/src/components/ui/storefront/utils.ts +90 -0
  58. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.test.ts +134 -0
  59. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.ts +127 -0
  60. package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +17 -42
  61. package/src/components/ui/tu-do/boards/boardId/timeline-board-open-task.test.tsx +164 -0
  62. package/src/components/ui/tu-do/boards/boardId/timeline-board.tsx +25 -16
  63. package/src/components/ui/tu-do/hooks/useTaskDialog.ts +15 -1
  64. package/src/components/ui/tu-do/my-tasks/use-my-tasks-state.ts +2 -0
  65. package/src/components/ui/tu-do/my-tasks/use-task-context-actions.ts +114 -7
  66. package/src/components/ui/tu-do/providers/__tests__/task-dialog-provider.test.tsx +217 -5
  67. package/src/components/ui/tu-do/providers/task-dialog-provider.tsx +180 -35
  68. package/src/components/ui/tu-do/shared/__tests__/task-dialog-manager.test.tsx +222 -26
  69. package/src/components/ui/tu-do/shared/board-client.tsx +1 -3
  70. package/src/components/ui/tu-do/shared/list-view-context-menu.test.tsx +55 -2
  71. package/src/components/ui/tu-do/shared/list-view.tsx +23 -16
  72. package/src/components/ui/tu-do/shared/task-dialog-manager.tsx +93 -76
  73. package/src/components/ui/tu-do/shared/task-dialog-presentation.ts +11 -0
  74. package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.test.tsx +128 -1
  75. package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.tsx +104 -69
  76. package/src/components/ui/tu-do/shared/task-edit-dialog/components/smart-task-suggestions-panel.test.tsx +129 -0
  77. package/src/components/ui/tu-do/shared/task-edit-dialog/components/smart-task-suggestions-panel.tsx +358 -0
  78. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-description-editor.tsx +1 -1
  79. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-dialog-header.tsx +6 -2
  80. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-name-input.test.tsx +17 -1
  81. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-name-input.tsx +151 -111
  82. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-form-reset.ts +18 -2
  83. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-realtime-sync.ts +1 -2
  84. package/src/components/ui/tu-do/shared/task-edit-dialog/task-dialog-actions.tsx +5 -3
  85. package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +584 -53
  86. package/src/hooks/useBoardRealtime.ts +54 -1
  87. package/src/hooks/useBoardRealtimeEventHandler.ts +169 -4
  88. package/src/hooks/useTaskUserRealtime.ts +338 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,34 @@
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
+
3
32
  ## [0.5.0](https://github.com/tutur3u/platform/compare/ui-v0.4.1...ui-v0.5.0) (2026-06-11)
4
33
 
5
34
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tuturuuu/ui",
3
- "version": "0.5.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.0",
64
- "@tiptap/extension-collaboration": "3.26.0",
65
- "@tiptap/extension-collaboration-caret": "3.26.0",
66
- "@tiptap/extension-drag-handle-react": "3.26.0",
67
- "@tiptap/extension-highlight": "3.26.0",
68
- "@tiptap/extension-horizontal-rule": "3.26.0",
69
- "@tiptap/extension-image": "3.26.0",
70
- "@tiptap/extension-link": "3.26.0",
71
- "@tiptap/extension-list": "3.26.0",
72
- "@tiptap/extension-placeholder": "3.26.0",
73
- "@tiptap/extension-strike": "3.26.0",
74
- "@tiptap/extension-subscript": "3.26.0",
75
- "@tiptap/extension-superscript": "3.26.0",
76
- "@tiptap/extension-table": "3.26.0",
77
- "@tiptap/extension-table-cell": "3.26.0",
78
- "@tiptap/extension-table-header": "3.26.0",
79
- "@tiptap/extension-table-row": "3.26.0",
80
- "@tiptap/extension-text-align": "3.26.0",
81
- "@tiptap/extension-youtube": "3.26.0",
82
- "@tiptap/pm": "3.26.0",
83
- "@tiptap/react": "3.26.0",
84
- "@tiptap/starter-kit": "3.26.0",
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.3.0",
87
- "@tuturuuu/hooks": "0.0.1",
88
- "@tuturuuu/icons": "0.0.5",
89
- "@tuturuuu/internal-api": "0.5.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/trigger": "0.2.0",
92
- "@tuturuuu/utils": "0.6.0",
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.7",
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": "file:vendor/xlsx-0.20.3.tgz",
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.19",
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.7.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.2",
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((str: string): number => {
105
- if (!str) return 0;
106
- // Remove all non-numeric characters except decimal point
107
- // Handle both comma and period as decimal separators
108
- const normalized = str
109
- .replace(/[^\d.,]/g, '')
110
- // If there are both commas and periods, assume the last one is decimal
111
- .replace(/,(?=\d{3}(?:[.,]|$))/g, '') // Remove thousand separators (commas followed by 3 digits)
112
- .replace(/\.(?=\d{3}(?:[.,]|$))/g, '') // Remove thousand separators (periods followed by 3 digits)
113
- .replace(/,/g, '.'); // Convert remaining commas to periods for parsing
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
- // Extract just the numeric characters and decimal
126
- const cleanValue = rawValue.replace(/[^\d.]/g, '');
127
-
128
- // Handle multiple decimals - keep only the first
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.]/.test(rawValue[i]!)) {
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.]/.test(formatted[i]!)) {
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 point
265
- if (/^[\d.]$/.test(e.key)) return;
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
+ });