@tuturuuu/utils 0.0.2 → 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 (186) hide show
  1. package/CHANGELOG.md +305 -0
  2. package/biome.json +5 -0
  3. package/jsr.json +8 -8
  4. package/package.json +63 -32
  5. package/src/__tests__/ai-temp-auth.test.ts +309 -0
  6. package/src/__tests__/api-proxy-guard.test.ts +1451 -0
  7. package/src/__tests__/app-url.test.ts +270 -0
  8. package/src/__tests__/avatar-url.test.ts +97 -0
  9. package/src/__tests__/color-helper.test.ts +179 -0
  10. package/src/__tests__/constants.test.ts +351 -0
  11. package/src/__tests__/crypto.test.ts +107 -0
  12. package/src/__tests__/date-helper.test.ts +408 -0
  13. package/src/__tests__/fixtures/task-description-full-featured.json +456 -0
  14. package/src/__tests__/format.test.ts +317 -0
  15. package/src/__tests__/html-sanitizer.test.ts +360 -0
  16. package/src/__tests__/interest-calculator.test.ts +336 -0
  17. package/src/__tests__/interest-detector.test.ts +222 -0
  18. package/src/__tests__/label-colors.test.ts +241 -0
  19. package/src/__tests__/name-helper.test.ts +158 -0
  20. package/src/__tests__/node-diff.test.ts +576 -0
  21. package/src/__tests__/notification-service.test.ts +210 -0
  22. package/src/__tests__/onboarding-helper.test.ts +331 -0
  23. package/src/__tests__/path-helper.test.ts +152 -0
  24. package/src/__tests__/permissions.test.tsx +81 -0
  25. package/src/__tests__/request-emoji-limit.test.ts +172 -0
  26. package/src/__tests__/search-helper.test.ts +51 -0
  27. package/src/__tests__/storage-display-name.test.ts +37 -0
  28. package/src/__tests__/storage-path.test.ts +238 -0
  29. package/src/__tests__/tag-utils.test.ts +205 -0
  30. package/src/__tests__/task-description-yjs-state.test.ts +581 -0
  31. package/src/__tests__/task-helper-board-api-routing.test.ts +94 -0
  32. package/src/__tests__/task-helper-create-task.test.ts +129 -0
  33. package/src/__tests__/task-helpers.test.ts +464 -0
  34. package/src/__tests__/task-overrides.test.ts +305 -0
  35. package/src/__tests__/task-reorder-cache.test.ts +74 -0
  36. package/src/__tests__/task-sort-keys.test.ts +36 -0
  37. package/src/__tests__/task-transformers.test.ts +62 -0
  38. package/src/__tests__/text-helper.test.ts +776 -0
  39. package/src/__tests__/time-helper.test.ts +70 -0
  40. package/src/__tests__/time-tracker-period.test.ts +55 -0
  41. package/src/__tests__/timezone.test.ts +117 -0
  42. package/src/__tests__/upstash-rest.test.ts +77 -0
  43. package/src/__tests__/uuid-helper.test.ts +133 -0
  44. package/src/__tests__/workspace-helper.test.ts +859 -0
  45. package/src/__tests__/workspace-limits.test.ts +255 -0
  46. package/src/__tests__/yjs-helper.test.ts +581 -0
  47. package/src/abuse-protection/__tests__/backend-rate-limit.test.ts +113 -0
  48. package/src/abuse-protection/__tests__/edge.test.ts +136 -0
  49. package/src/abuse-protection/__tests__/index.test.ts +562 -0
  50. package/src/abuse-protection/__tests__/reputation.test.ts +192 -0
  51. package/src/abuse-protection/backend-rate-limit.ts +44 -0
  52. package/src/abuse-protection/constants.ts +117 -0
  53. package/src/abuse-protection/edge.ts +223 -0
  54. package/src/abuse-protection/index.ts +1545 -0
  55. package/src/abuse-protection/reputation.ts +587 -0
  56. package/src/abuse-protection/types.ts +97 -0
  57. package/src/abuse-protection/user-agent.ts +124 -0
  58. package/src/abuse-protection/user-suspension.ts +231 -0
  59. package/src/ai-temp-auth.ts +315 -0
  60. package/src/api-proxy-guard.ts +965 -0
  61. package/src/app-url.ts +96 -0
  62. package/src/avatar-url.ts +64 -0
  63. package/src/break-duration.ts +84 -0
  64. package/src/calendar-auth-token.test.ts +37 -0
  65. package/src/calendar-auth-token.ts +19 -0
  66. package/src/calendar-sync-coordination.md +197 -0
  67. package/src/calendar-utils.test.ts +169 -0
  68. package/src/calendar-utils.ts +91 -0
  69. package/src/color-helper.ts +110 -0
  70. package/src/common/nextjs.tsx +99 -0
  71. package/src/common/scan.tsx +15 -0
  72. package/src/configs/reports.ts +160 -0
  73. package/src/constants.ts +85 -0
  74. package/src/crypto.ts +21 -0
  75. package/src/currencies.ts +97 -0
  76. package/src/date-helper.ts +313 -0
  77. package/src/editor/convert-to-task.ts +264 -0
  78. package/src/editor/index.ts +5 -0
  79. package/src/email/__tests__/client.test.ts +141 -0
  80. package/src/email/__tests__/validation.test.ts +46 -0
  81. package/src/email/client.ts +92 -0
  82. package/src/email/server.ts +128 -0
  83. package/src/email/validation.ts +11 -0
  84. package/src/encryption/__tests__/calendar-events.test.ts +411 -0
  85. package/src/encryption/__tests__/configuration.test.ts +114 -0
  86. package/src/encryption/__tests__/field-encryption.test.ts +232 -0
  87. package/src/encryption/__tests__/key-generation.test.ts +30 -0
  88. package/src/encryption/__tests__/performance-edge-cases.test.ts +187 -0
  89. package/src/encryption/__tests__/test-helpers.ts +22 -0
  90. package/src/encryption/__tests__/workspace-key-encryption.test.ts +129 -0
  91. package/src/encryption/encryption-service.ts +343 -0
  92. package/src/encryption/index.ts +25 -0
  93. package/src/encryption/types.ts +57 -0
  94. package/src/exchange-rates.ts +49 -0
  95. package/src/feature-flags/__tests__/feature-flags.test.ts +302 -0
  96. package/src/feature-flags/core.ts +322 -0
  97. package/src/feature-flags/data.ts +16 -0
  98. package/src/feature-flags/default.ts +18 -0
  99. package/src/feature-flags/index.ts +7 -0
  100. package/src/feature-flags/requestable-features.ts +79 -0
  101. package/src/feature-flags/types.ts +4 -0
  102. package/src/fetcher.ts +2 -0
  103. package/src/finance/index.ts +4 -0
  104. package/src/finance/interest-calculator.ts +456 -0
  105. package/src/finance/interest-detector.ts +141 -0
  106. package/src/finance/transform-invoice-results.ts +219 -0
  107. package/src/finance/wallet-permissions.test.ts +169 -0
  108. package/src/finance/wallet-permissions.ts +82 -0
  109. package/src/format.ts +122 -3
  110. package/src/generated/platform-build-metadata.ts +11 -0
  111. package/src/hooks/use-platform.ts +64 -0
  112. package/src/html-sanitizer.ts +155 -0
  113. package/src/internal-domains.ts +497 -0
  114. package/src/keyboard-preset.ts +109 -0
  115. package/src/label-colors.ts +213 -0
  116. package/src/launchable-apps.test.ts +126 -0
  117. package/src/launchable-apps.ts +490 -0
  118. package/src/name-helper.ts +269 -0
  119. package/src/next-config.test.ts +234 -0
  120. package/src/next-config.ts +203 -0
  121. package/src/node-diff.ts +375 -0
  122. package/src/notification-service.ts +379 -0
  123. package/src/nova/scores/__tests__/calculate.test.ts +254 -0
  124. package/src/nova/scores/calculate.ts +132 -0
  125. package/src/nova/submissions/check-permission.ts +132 -0
  126. package/src/onboarding-helper.ts +213 -0
  127. package/src/path-helper.ts +93 -0
  128. package/src/permissions.tsx +1170 -0
  129. package/src/plan-helpers.test.ts +188 -0
  130. package/src/plan-helpers.ts +80 -0
  131. package/src/platform-release.test.ts +74 -0
  132. package/src/platform-release.ts +155 -0
  133. package/src/portless.ts +124 -0
  134. package/src/priority-styles.ts +42 -0
  135. package/src/request-emoji-limit.ts +335 -0
  136. package/src/search-helper.ts +18 -0
  137. package/src/search.test.ts +89 -0
  138. package/src/search.ts +355 -0
  139. package/src/storage-display-name.ts +30 -0
  140. package/src/storage-path.ts +147 -0
  141. package/src/tag-utils.ts +159 -0
  142. package/src/task/reorder.ts +245 -0
  143. package/src/task/transformers.ts +149 -0
  144. package/src/task-date-timezone.ts +133 -0
  145. package/src/task-description-content.ts +240 -0
  146. package/src/task-helper/board.ts +193 -0
  147. package/src/task-helper/bulk-actions.ts +564 -0
  148. package/src/task-helper/personal-external-staging.ts +21 -0
  149. package/src/task-helper/recycle-bin.ts +202 -0
  150. package/src/task-helper/relationships.ts +346 -0
  151. package/src/task-helper/shared.ts +109 -0
  152. package/src/task-helper/sort-keys.ts +337 -0
  153. package/src/task-helper/task-hooks-basic.ts +342 -0
  154. package/src/task-helper/task-hooks-move.ts +264 -0
  155. package/src/task-helper/task-operations.ts +278 -0
  156. package/src/task-helper.ts +12 -0
  157. package/src/task-helpers.ts +241 -0
  158. package/src/task-list-status.ts +62 -0
  159. package/src/task-overrides.ts +82 -0
  160. package/src/task-snapshot.ts +374 -0
  161. package/src/text-diff.ts +81 -0
  162. package/src/text-helper.ts +537 -0
  163. package/src/time-helper.ts +63 -0
  164. package/src/time-tracker-period.ts +73 -0
  165. package/src/timeblock-helper.ts +418 -0
  166. package/src/timezone.ts +190 -0
  167. package/src/timezones.json +1271 -0
  168. package/src/upstash-rest.ts +56 -0
  169. package/src/user-helper.ts +296 -0
  170. package/src/uuid-helper.ts +11 -0
  171. package/src/workspace-handle.ts +10 -0
  172. package/src/workspace-helper.ts +1408 -0
  173. package/src/workspace-limits.ts +68 -0
  174. package/src/yjs-helper.ts +217 -0
  175. package/src/yjs-task-description.ts +81 -0
  176. package/tsconfig.json +3 -5
  177. package/tsconfig.typecheck.json +33 -0
  178. package/vitest.config.ts +36 -0
  179. package/dist/index.d.ts +0 -8
  180. package/dist/index.js +0 -2
  181. package/dist/index.js.map +0 -1
  182. package/dist/index.mjs +0 -2
  183. package/dist/index.mjs.map +0 -1
  184. package/eslint.config.mjs +0 -20
  185. package/rollup.config.js +0 -41
  186. package/src/index.ts +0 -1
@@ -0,0 +1,109 @@
1
+ import {
2
+ closestCorners,
3
+ type DroppableContainer,
4
+ getFirstCollision,
5
+ KeyboardCode,
6
+ type KeyboardCoordinateGetter,
7
+ } from '@dnd-kit/core';
8
+
9
+ const directions: string[] = [
10
+ KeyboardCode.Down,
11
+ KeyboardCode.Right,
12
+ KeyboardCode.Up,
13
+ KeyboardCode.Left,
14
+ ];
15
+
16
+ export const coordinateGetter: KeyboardCoordinateGetter = (
17
+ event,
18
+ { context: { active, droppableRects, droppableContainers, collisionRect } }
19
+ ) => {
20
+ if (directions.includes(event.code)) {
21
+ event.preventDefault();
22
+
23
+ if (!active || !collisionRect) {
24
+ return;
25
+ }
26
+
27
+ const filteredContainers: DroppableContainer[] = [];
28
+
29
+ droppableContainers.getEnabled().forEach((entry) => {
30
+ if (!entry || entry?.disabled) {
31
+ return;
32
+ }
33
+
34
+ const rect = droppableRects.get(entry.id);
35
+
36
+ if (!rect) {
37
+ return;
38
+ }
39
+
40
+ const data = entry.data.current;
41
+
42
+ if (data) {
43
+ const { type, children } = data;
44
+
45
+ if (type === 'Column' && children?.length > 0) {
46
+ if (active.data.current?.type !== 'Column') {
47
+ return;
48
+ }
49
+ }
50
+ }
51
+
52
+ switch (event.code) {
53
+ case KeyboardCode.Down:
54
+ if (active.data.current?.type === 'Column') {
55
+ return;
56
+ }
57
+ if (collisionRect.top < rect.top) {
58
+ // find all droppable areas below
59
+ filteredContainers.push(entry);
60
+ }
61
+ break;
62
+ case KeyboardCode.Up:
63
+ if (active.data.current?.type === 'Column') {
64
+ return;
65
+ }
66
+ if (collisionRect.top > rect.top) {
67
+ // find all droppable areas above
68
+ filteredContainers.push(entry);
69
+ }
70
+ break;
71
+ case KeyboardCode.Left:
72
+ if (collisionRect.left >= rect.left + rect.width) {
73
+ // find all droppable areas to left
74
+ filteredContainers.push(entry);
75
+ }
76
+ break;
77
+ case KeyboardCode.Right:
78
+ // find all droppable areas to right
79
+ if (collisionRect.left + collisionRect.width <= rect.left) {
80
+ filteredContainers.push(entry);
81
+ }
82
+ break;
83
+ }
84
+ });
85
+ const collisions = closestCorners({
86
+ active,
87
+ collisionRect: collisionRect,
88
+ droppableRects,
89
+ droppableContainers: filteredContainers,
90
+ pointerCoordinates: null,
91
+ });
92
+ const closestId = getFirstCollision(collisions, 'id');
93
+
94
+ if (closestId != null) {
95
+ const newDroppable = droppableContainers.get(closestId);
96
+ const newNode = newDroppable?.node.current;
97
+ const newRect = newDroppable?.rect.current;
98
+
99
+ if (newNode && newRect) {
100
+ return {
101
+ x: newRect.left,
102
+ y: newRect.top,
103
+ };
104
+ }
105
+ }
106
+ }
107
+
108
+ return undefined;
109
+ };
@@ -0,0 +1,213 @@
1
+ /**
2
+ * Label color utilities for computing accessible label styles with proper contrast.
3
+ * These functions handle color normalization, luminance calculation, and automatic
4
+ * color adjustments to ensure accessibility standards are met.
5
+ */
6
+
7
+ /**
8
+ * Normalize a hex color string to a standard 6-character format.
9
+ * Handles both 3-character and 6-character hex codes, with or without # prefix.
10
+ *
11
+ * @param input - Raw color string (e.g., "#f00", "ff0000", "#ff0000")
12
+ * @returns Normalized hex string with # prefix, or null if invalid
13
+ */
14
+ function normalizeHex(input: string): string | null {
15
+ if (!input) return null;
16
+ let c = input.trim();
17
+ if (c.startsWith('#')) c = c.slice(1);
18
+ if (c.length === 3) {
19
+ c = c
20
+ .split('')
21
+ .map((ch) => ch + ch)
22
+ .join('');
23
+ }
24
+ if (c.length !== 6) return null;
25
+ if (!/^[0-9a-fA-F]{6}$/.test(c)) return null;
26
+ return `#${c.toLowerCase()}`;
27
+ }
28
+
29
+ /**
30
+ * Convert a hex color to RGB values.
31
+ *
32
+ * @param hex - Hex color string
33
+ * @returns RGB object with r, g, b values (0-255), or null if invalid
34
+ */
35
+ function hexToRgb(hex: string): { r: number; g: number; b: number } | null {
36
+ const n = normalizeHex(hex);
37
+ if (!n) return null;
38
+ const r = parseInt(n.substring(1, 3), 16);
39
+ const g = parseInt(n.substring(3, 5), 16);
40
+ const b = parseInt(n.substring(5, 7), 16);
41
+ return { r, g, b };
42
+ }
43
+
44
+ /**
45
+ * Calculate the relative luminance of an RGB color.
46
+ * Uses the WCAG formula for luminance calculation.
47
+ *
48
+ * @param rgb - RGB color object
49
+ * @returns Relative luminance value (0-1)
50
+ */
51
+ function luminance({ r, g, b }: { r: number; g: number; b: number }): number {
52
+ const channel = (v: number) => {
53
+ const s = v / 255;
54
+ return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
55
+ };
56
+ return 0.2126 * channel(r) + 0.7152 * channel(g) + 0.0722 * channel(b);
57
+ }
58
+
59
+ /**
60
+ * Adjust the brightness of a hex color by modifying its lightness.
61
+ * Converts to HSL, adjusts lightness and saturation, then converts back to hex.
62
+ *
63
+ * @param hex - Hex color string
64
+ * @param factor - Brightness factor (< 1 darkens, > 1 lightens)
65
+ * @returns Adjusted hex color string
66
+ */
67
+ function adjust(hex: string, factor: number): string {
68
+ const rgb = hexToRgb(hex);
69
+ if (!rgb) return hex;
70
+ const rN = rgb.r / 255;
71
+ const gN = rgb.g / 255;
72
+ const bN = rgb.b / 255;
73
+ const max = Math.max(rN, gN, bN);
74
+ const min = Math.min(rN, gN, bN);
75
+ let h = 0;
76
+ const l = (max + min) / 2;
77
+ const d = max - min;
78
+ let s = 0;
79
+ if (d !== 0) {
80
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
81
+ switch (max) {
82
+ case rN:
83
+ h = (gN - bN) / d + (gN < bN ? 6 : 0);
84
+ break;
85
+ case gN:
86
+ h = (bN - rN) / d + 2;
87
+ break;
88
+ default:
89
+ h = (rN - gN) / d + 4;
90
+ }
91
+ h /= 6;
92
+ }
93
+ const targetL = Math.min(
94
+ 1,
95
+ Math.max(0, l * (factor >= 1 ? 1 + (factor - 1) * 0.75 : factor))
96
+ );
97
+ const targetS = factor > 1 && targetL > 0.7 ? s * 0.85 : s;
98
+ const hue2rgb = (p: number, q: number, t: number) => {
99
+ if (t < 0) t += 1;
100
+ if (t > 1) t -= 1;
101
+ if (t < 1 / 6) return p + (q - p) * 6 * t;
102
+ if (t < 1 / 2) return q;
103
+ if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
104
+ return p;
105
+ };
106
+ const q =
107
+ targetL < 0.5
108
+ ? targetL * (1 + targetS)
109
+ : targetL + targetS - targetL * targetS;
110
+ const p = 2 * targetL - q;
111
+ const r = Math.round(hue2rgb(p, q, h + 1 / 3) * 255);
112
+ const g = Math.round(hue2rgb(p, q, h) * 255);
113
+ const b = Math.round(hue2rgb(p, q, h - 1 / 3) * 255);
114
+ return `#${[r, g, b].map((v) => v.toString(16).padStart(2, '0')).join('')}`;
115
+ }
116
+
117
+ /**
118
+ * Color name to hex mapping for common color names.
119
+ * Supports Tailwind-style color names.
120
+ */
121
+ const COLOR_NAME_MAP: Record<string, string> = {
122
+ red: '#ef4444',
123
+ orange: '#f97316',
124
+ amber: '#f59e0b',
125
+ yellow: '#eab308',
126
+ lime: '#84cc16',
127
+ green: '#22c55e',
128
+ emerald: '#10b981',
129
+ teal: '#14b8a6',
130
+ cyan: '#06b6d4',
131
+ sky: '#0ea5e9',
132
+ blue: '#3b82f6',
133
+ indigo: '#6366f1',
134
+ violet: '#8b5cf6',
135
+ purple: '#a855f7',
136
+ fuchsia: '#d946ef',
137
+ pink: '#ec4899',
138
+ rose: '#f43f5e',
139
+ gray: '#6b7280',
140
+ slate: '#64748b',
141
+ zinc: '#71717a',
142
+ };
143
+
144
+ export interface AccessibleLabelStyles {
145
+ bg: string;
146
+ border: string;
147
+ text: string;
148
+ }
149
+
150
+ /**
151
+ * Compute accessible label styles based on theme.
152
+ *
153
+ * @param raw - Raw color input (hex code or color name)
154
+ * @param isDark - Whether current theme is dark mode
155
+ */
156
+ export function computeAccessibleLabelStyles(
157
+ raw: string | null | undefined,
158
+ isDark?: boolean
159
+ ): AccessibleLabelStyles | null {
160
+ if (typeof raw !== 'string') return null;
161
+ const normalizedRaw = raw.trim();
162
+ if (!normalizedRaw) return null;
163
+
164
+ const baseHex =
165
+ normalizeHex(normalizedRaw) ||
166
+ COLOR_NAME_MAP[normalizedRaw.toLowerCase()] ||
167
+ null;
168
+ if (!baseHex) return null;
169
+
170
+ const rgb = hexToRgb(baseHex);
171
+ if (!rgb) return null;
172
+ const lum = luminance(rgb);
173
+
174
+ const bg = `${baseHex}1a`;
175
+ let border: string;
176
+ let text = baseHex;
177
+
178
+ // Backward-compatible default behavior used across existing packages.
179
+ if (typeof isDark !== 'boolean') {
180
+ border = `${baseHex}4d`;
181
+ if (lum < 0.22) {
182
+ text = adjust(baseHex, 1.25);
183
+ } else if (lum > 0.82) {
184
+ text = adjust(baseHex, 0.65);
185
+ }
186
+ return { bg, border, text };
187
+ }
188
+
189
+ if (isDark) {
190
+ border = `${baseHex}4d`;
191
+ if (lum < 0.35) {
192
+ text = adjust(baseHex, 1.5);
193
+ } else if (lum < 0.6) {
194
+ text = adjust(baseHex, 1.25);
195
+ } else if (lum > 0.85) {
196
+ text = adjust(baseHex, 0.9);
197
+ }
198
+ } else {
199
+ border = `${baseHex}66`;
200
+ if (lum < 0.18) {
201
+ text = adjust(baseHex, 1.2);
202
+ border = `${baseHex}4d`;
203
+ } else if (lum > 0.82) {
204
+ text = adjust(baseHex, 0.45);
205
+ border = `${adjust(baseHex, 0.45)}99`;
206
+ } else if (lum > 0.5) {
207
+ text = adjust(baseHex, 0.65);
208
+ border = `${adjust(baseHex, 0.65)}99`;
209
+ }
210
+ }
211
+
212
+ return { bg, border, text };
213
+ }
@@ -0,0 +1,126 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ getLaunchableApp,
4
+ getLaunchableAppByTitle,
5
+ getLaunchableAppOrigin,
6
+ type LaunchableWorkspace,
7
+ resolveLaunchableAppPath,
8
+ resolveLaunchableAppUrl,
9
+ } from './launchable-apps';
10
+
11
+ const personalWorkspace: LaunchableWorkspace = {
12
+ id: 'workspace-id',
13
+ name: 'Personal',
14
+ personal: true,
15
+ };
16
+
17
+ const teamWorkspace: LaunchableWorkspace = {
18
+ id: 'team-workspace',
19
+ name: 'Team Workspace',
20
+ personal: false,
21
+ };
22
+
23
+ describe('launchable apps', () => {
24
+ it('resolves apps by slug, title, and aliases', () => {
25
+ expect(getLaunchableApp('calendar')?.title).toBe('Calendar');
26
+ expect(getLaunchableAppByTitle('Tuturuuu')?.slug).toBe('platform');
27
+ expect(getLaunchableAppByTitle('apps gateway')?.slug).toBe('apps');
28
+ });
29
+
30
+ it('resolves production, Portless, and local origins', () => {
31
+ const app = getLaunchableApp('calendar');
32
+
33
+ expect(app).not.toBeNull();
34
+ expect(getLaunchableAppOrigin(app!, { environment: 'production' })).toBe(
35
+ 'https://calendar.tuturuuu.com'
36
+ );
37
+ expect(getLaunchableAppOrigin(app!, { environment: 'portless' })).toBe(
38
+ 'https://calendar.tuturuuu.localhost'
39
+ );
40
+ expect(getLaunchableAppOrigin(app!, { environment: 'localhost' })).toBe(
41
+ 'http://localhost:7806'
42
+ );
43
+
44
+ const meet = getLaunchableApp('meet');
45
+ expect(meet).not.toBeNull();
46
+ expect(getLaunchableAppOrigin(meet!, { environment: 'production' })).toBe(
47
+ 'https://meet.tuturuuu.com'
48
+ );
49
+ });
50
+
51
+ it('detects Portless dev from the current origin', () => {
52
+ const app = getLaunchableApp('tasks');
53
+
54
+ expect(app).not.toBeNull();
55
+ expect(
56
+ getLaunchableAppOrigin(app!, {
57
+ currentOrigin: 'https://calendar.tuturuuu.localhost',
58
+ })
59
+ ).toBe('https://tasks.tuturuuu.localhost');
60
+ });
61
+
62
+ it('uses default paths when no workspace is provided', () => {
63
+ const app = getLaunchableApp('learn');
64
+
65
+ expect(app).not.toBeNull();
66
+ expect(resolveLaunchableAppPath({ app: app! })).toBe('/dashboard');
67
+ });
68
+
69
+ it('uses workspace-aware paths and personal slugs', () => {
70
+ const platform = getLaunchableApp('platform');
71
+ const tasks = getLaunchableApp('tasks');
72
+ const meet = getLaunchableApp('meet');
73
+
74
+ expect(platform).not.toBeNull();
75
+ expect(tasks).not.toBeNull();
76
+ expect(meet).not.toBeNull();
77
+ expect(
78
+ resolveLaunchableAppPath({
79
+ app: platform!,
80
+ workspace: personalWorkspace,
81
+ })
82
+ ).toBe('/personal');
83
+ expect(
84
+ resolveLaunchableAppPath({
85
+ app: tasks!,
86
+ workspace: teamWorkspace,
87
+ })
88
+ ).toBe('/team-workspace/tasks');
89
+ expect(
90
+ resolveLaunchableAppPath({
91
+ app: meet!,
92
+ workspace: teamWorkspace,
93
+ })
94
+ ).toBe('/workspace/team-workspace');
95
+ });
96
+
97
+ it('lets callers override workspace path resolution', () => {
98
+ const app = getLaunchableApp('calendar');
99
+
100
+ expect(app).not.toBeNull();
101
+ expect(
102
+ resolveLaunchableAppPath({
103
+ app: app!,
104
+ workspace: teamWorkspace,
105
+ workspacePathResolver: (workspace) => `/custom/${workspace.id}`,
106
+ })
107
+ ).toBe('/custom/team-workspace');
108
+ });
109
+
110
+ it('resolves full URLs with paths and search params', () => {
111
+ const app = getLaunchableApp('finance');
112
+
113
+ expect(app).not.toBeNull();
114
+ expect(
115
+ resolveLaunchableAppUrl({
116
+ app: app!,
117
+ environment: 'production',
118
+ searchParams: {
119
+ source: 'command',
120
+ tag: ['a', 'b'],
121
+ },
122
+ workspace: personalWorkspace,
123
+ })
124
+ ).toBe('https://finance.tuturuuu.com/personal?source=command&tag=a&tag=b');
125
+ });
126
+ });