@tuturuuu/ui 0.26.0 → 0.27.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 (34) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +29 -7
  3. package/biome.json +1 -1
  4. package/package.json +48 -54
  5. package/src/components/ui/calendar-app/hooks/use-calendar-settings.test.ts +32 -0
  6. package/src/components/ui/custom/combobox.tsx +4 -0
  7. package/src/components/ui/custom/nav-link.test.tsx +47 -0
  8. package/src/components/ui/custom/nav-link.tsx +28 -3
  9. package/src/components/ui/custom/workspace-access/adapters.test.ts +89 -1
  10. package/src/components/ui/custom/workspace-access/adapters.ts +67 -2
  11. package/src/components/ui/custom/workspace-access/types.ts +13 -0
  12. package/src/components/ui/custom/workspace-access/workspace-access-invitation-role-menu.test.tsx +79 -0
  13. package/src/components/ui/custom/workspace-access/workspace-access-invitation-role-menu.tsx +125 -0
  14. package/src/components/ui/custom/workspace-access/workspace-access-invite-access-picker.tsx +108 -0
  15. package/src/components/ui/custom/workspace-access/workspace-access-invite-dialog.test.tsx +134 -0
  16. package/src/components/ui/custom/workspace-access/workspace-access-invite-dialog.tsx +94 -113
  17. package/src/components/ui/custom/workspace-access/workspace-access-invite-pos-panel.tsx +75 -0
  18. package/src/components/ui/custom/workspace-access/workspace-access-invite-role-picker.tsx +151 -0
  19. package/src/components/ui/custom/workspace-access/workspace-access-labels.ts +1 -1
  20. package/src/components/ui/custom/workspace-access/workspace-access-member-row.tsx +27 -2
  21. package/src/components/ui/custom/workspace-access/workspace-access-members.tsx +10 -0
  22. package/src/components/ui/custom/workspace-access/workspace-access-page.tsx +102 -3
  23. package/src/components/ui/custom/workspace-access/workspace-access-role-options.test.ts +35 -0
  24. package/src/components/ui/custom/workspace-access/workspace-access-role-options.ts +21 -0
  25. package/src/components/ui/custom/workspace-select-invitations.tsx +2 -1
  26. package/src/components/ui/legacy/polls/poll-display.test.tsx +118 -0
  27. package/src/components/ui/legacy/polls/poll-display.tsx +8 -0
  28. package/src/hooks/__tests__/use-notifications-subscription.test.tsx +34 -1
  29. package/src/hooks/use-board-actions.test.ts +23 -0
  30. package/src/hooks/use-board-actions.ts +48 -35
  31. package/src/hooks/use-calendar-sync.tsx +12 -12
  32. package/src/hooks/use-notifications.ts +4 -8
  33. package/src/lib/calendar-settings-resolver.ts +1 -200
  34. package/src/readme-contract.test.tsx +57 -0
@@ -240,7 +240,7 @@ export const CalendarSyncProvider = ({
240
240
  queryKey: ['workspace-calendars', wsId],
241
241
  enabled: !hasExternalEvents && !!wsId,
242
242
  queryFn: () => listWorkspaceCalendars(wsId),
243
- staleTime: 30_000,
243
+ staleTime: 5 * 60_000,
244
244
  });
245
245
  const enabledWorkspaceCalendarIds = useMemo(
246
246
  () =>
@@ -438,12 +438,11 @@ export const CalendarSyncProvider = ({
438
438
  [isVisibleInCurrentRange]
439
439
  );
440
440
 
441
- // Fetch database events with caching
442
441
  const { data: fetchedData, isLoading: isDatabaseLoading } = useQuery({
443
442
  queryKey: ['databaseCalendarEvents', wsId, activeCacheKey],
444
443
  enabled: !hasExternalEvents && !!wsId && dates.length > 0,
445
- staleTime: 30000, // Consider data fresh for 30 seconds
446
- gcTime: 5 * 60 * 1000, // Keep in cache for 5 minutes
444
+ staleTime: 2 * 60_000,
445
+ gcTime: 30 * 60_000,
447
446
  queryFn: async () => {
448
447
  if (!activeCacheKey) return null;
449
448
 
@@ -512,10 +511,11 @@ export const CalendarSyncProvider = ({
512
511
  lastSyncTime: new Date(),
513
512
  });
514
513
 
515
- return cachedData?.dbEvents ?? [];
514
+ throw err instanceof Error ? err : new Error(errorMessage);
516
515
  }
517
516
  },
518
- refetchInterval: 60000, // Reduced from 30s to 60s to lower load
517
+ refetchInterval: 5 * 60_000,
518
+ refetchIntervalInBackground: false,
519
519
  });
520
520
 
521
521
  // Legacy direct Google fetch/reconcile is disabled. Provider inbound sync is
@@ -523,18 +523,17 @@ export const CalendarSyncProvider = ({
523
523
  const { isLoading: isGoogleLoading } = useQuery({
524
524
  queryKey: ['googleCalendarEvents', wsId, activeCacheKey],
525
525
  enabled: false,
526
- staleTime: 30000, // Consider data fresh for 30 seconds
527
- gcTime: 5 * 60 * 1000, // Keep in cache for 5 minutes
526
+ staleTime: 2 * 60_000,
527
+ gcTime: 30 * 60_000,
528
528
  queryFn: async () => null,
529
- refetchInterval: 60000, // Reduced from 30s to 60s to lower load
530
529
  });
531
530
 
532
531
  // Fetch habit calendar events to identify which events are habits
533
532
  const { data: habitEventData } = useQuery({
534
533
  queryKey: ['habitCalendarEvents', wsId, activeCacheKey],
535
534
  enabled: !hasExternalEvents && !!wsId && dates.length > 0,
536
- staleTime: 60000, // Consider data fresh for 1 minute
537
- gcTime: 5 * 60 * 1000, // Keep in cache for 5 minutes
535
+ staleTime: 2 * 60_000,
536
+ gcTime: 30 * 60_000,
538
537
  queryFn: async () => {
539
538
  const startDate = dayjs(dates[0]).startOf('day');
540
539
  const endDate = dayjs(dates[dates.length - 1])
@@ -571,7 +570,8 @@ export const CalendarSyncProvider = ({
571
570
  };
572
571
  }
573
572
  },
574
- refetchInterval: 60000, // Refetch every minute
573
+ refetchInterval: 5 * 60_000,
574
+ refetchIntervalInBackground: false,
575
575
  });
576
576
 
577
577
  // Helper to check if dates have actually changed
@@ -91,7 +91,8 @@ interface NotificationSubscriptionEntry {
91
91
  supabase: SupabaseClient;
92
92
  }
93
93
 
94
- export const UNREAD_COUNT_FALLBACK_INTERVAL_MS = 5 * 60 * 1000;
94
+ export const UNREAD_COUNT_STALE_TIME_MS = 5 * 60 * 1000;
95
+ export const UNREAD_COUNT_FALLBACK_INTERVAL_MS = 15 * 60 * 1000;
95
96
 
96
97
  const notificationSubscriptionRegistry = new Map<
97
98
  string,
@@ -164,12 +165,7 @@ function createNotificationSubscriptionEntry(
164
165
  table: 'notifications',
165
166
  filter: `user_id=eq.${userId}`,
166
167
  },
167
- (payload) => {
168
- const newRecord = payload.new as Notification;
169
- if (newRecord?.data?.action_taken) {
170
- invalidateQueries();
171
- }
172
- }
168
+ invalidateQueries
173
169
  )
174
170
  .on(
175
171
  'postgres_changes',
@@ -348,7 +344,7 @@ export function useUnreadCount(
348
344
  return data.count as number;
349
345
  },
350
346
  enabled: options?.enabled ?? true,
351
- staleTime: 60_000,
347
+ staleTime: UNREAD_COUNT_STALE_TIME_MS,
352
348
  // Realtime invalidation is the primary update path. Keep a low-frequency
353
349
  // refresh as a safety net for disconnected or suspended browser sessions.
354
350
  refetchInterval: UNREAD_COUNT_FALLBACK_INTERVAL_MS,
@@ -1,200 +1 @@
1
- /**
2
- * Calendar Settings Resolver
3
- *
4
- * Implements priority system for calendar preferences:
5
- * User settings > Workspace settings > Auto-detection
6
- */
7
-
8
- type FirstDayOfWeek = 'auto' | 'sunday' | 'monday' | 'saturday';
9
-
10
- interface CalendarSettings {
11
- timezone: string;
12
- firstDayOfWeek: FirstDayOfWeek;
13
- timeFormat: '12h' | '24h';
14
- }
15
-
16
- export interface CalendarUserSettings {
17
- timezone?: string | null;
18
- first_day_of_week?: string | null;
19
- time_format?: string | null;
20
- }
21
-
22
- export interface CalendarWorkspaceSettings {
23
- timezone?: string | null;
24
- first_day_of_week?: string | null;
25
- }
26
-
27
- /**
28
- * Detects the system timezone using Intl API
29
- */
30
- export function detectSystemTimezone(): string {
31
- try {
32
- return Intl.DateTimeFormat().resolvedOptions().timeZone;
33
- } catch {
34
- // Fallback to UTC if detection fails
35
- return 'UTC';
36
- }
37
- }
38
-
39
- /**
40
- * Detects the first day of week based on locale
41
- * Vietnamese: Monday
42
- * US/Canada: Sunday
43
- * Middle East: Saturday
44
- */
45
- export function detectLocaleFirstDay(locale?: string): FirstDayOfWeek {
46
- const userLocale =
47
- locale || (typeof navigator !== 'undefined' ? navigator.language : 'en-US');
48
-
49
- // Vietnamese locale uses Monday as first day
50
- if (userLocale.startsWith('vi')) {
51
- return 'monday';
52
- }
53
-
54
- // US, Canada, and some other countries use Sunday
55
- if (userLocale.startsWith('en-US') || userLocale.startsWith('en-CA')) {
56
- return 'sunday';
57
- }
58
-
59
- // Middle Eastern countries typically use Saturday
60
- if (userLocale.startsWith('ar') || userLocale.startsWith('he')) {
61
- return 'saturday';
62
- }
63
-
64
- // Most European and other countries use Monday
65
- return 'monday';
66
- }
67
-
68
- /**
69
- * Detects the preferred time format based on locale
70
- * Uses Intl.DateTimeFormat to determine if the locale uses 12h or 24h format
71
- */
72
- export function detectLocaleTimeFormat(locale?: string): '12h' | '24h' {
73
- const userLocale =
74
- locale || (typeof navigator !== 'undefined' ? navigator.language : 'en-US');
75
-
76
- try {
77
- // Use Intl.DateTimeFormat to detect the locale's preferred format
78
- const formatter = new Intl.DateTimeFormat(userLocale, { hour: 'numeric' });
79
- const parts = formatter.formatToParts(new Date(2000, 0, 1, 13, 0));
80
- const hourPart = parts.find((p) => p.type === 'hour');
81
-
82
- // If hour is "1" instead of "13", it's 12-hour format
83
- return hourPart?.value === '1' ? '12h' : '24h';
84
- } catch {
85
- // Fallback to 12h for English locales, 24h for others
86
- return userLocale.startsWith('en') ? '12h' : '24h';
87
- }
88
- }
89
-
90
- /**
91
- * Resolves the effective time format based on user setting
92
- */
93
- export function resolveTimeFormat(
94
- user?: Pick<CalendarUserSettings, 'time_format'> | null,
95
- locale?: string
96
- ): '12h' | '24h' {
97
- // User setting takes priority
98
- if (user?.time_format && user.time_format !== 'auto') {
99
- return user.time_format as '12h' | '24h';
100
- }
101
-
102
- // Auto-detection based on locale
103
- return detectLocaleTimeFormat(locale);
104
- }
105
-
106
- /**
107
- * Resolves the effective timezone based on priority system
108
- */
109
- export function resolveTimezone(
110
- user?: Pick<CalendarUserSettings, 'timezone'> | null,
111
- workspace?: Pick<CalendarWorkspaceSettings, 'timezone'> | null
112
- ): string {
113
- // Priority 1: User setting
114
- if (user?.timezone && user.timezone !== 'auto') {
115
- return user.timezone;
116
- }
117
-
118
- // Priority 2: Workspace setting
119
- if (workspace?.timezone && workspace.timezone !== 'auto') {
120
- return workspace.timezone;
121
- }
122
-
123
- // Priority 3: Auto-detection
124
- return detectSystemTimezone();
125
- }
126
-
127
- /**
128
- * Resolves the effective first day of week based on priority system
129
- */
130
- export function resolveFirstDayOfWeek(
131
- user?: Pick<CalendarUserSettings, 'first_day_of_week'> | null,
132
- workspace?: Pick<CalendarWorkspaceSettings, 'first_day_of_week'> | null,
133
- locale?: string
134
- ): FirstDayOfWeek {
135
- // Priority 1: User setting
136
- if (user?.first_day_of_week && user.first_day_of_week !== 'auto') {
137
- return user.first_day_of_week as FirstDayOfWeek;
138
- }
139
-
140
- // Priority 2: Workspace setting
141
- if (workspace?.first_day_of_week && workspace.first_day_of_week !== 'auto') {
142
- return workspace.first_day_of_week as FirstDayOfWeek;
143
- }
144
-
145
- // Priority 3: Auto-detection based on locale
146
- return detectLocaleFirstDay(locale);
147
- }
148
-
149
- /**
150
- * Resolves all calendar settings at once
151
- */
152
- export function resolveCalendarSettings(
153
- user?: CalendarUserSettings | null,
154
- workspace?: CalendarWorkspaceSettings | null,
155
- locale?: string
156
- ): CalendarSettings {
157
- return {
158
- timezone: resolveTimezone(user, workspace),
159
- firstDayOfWeek: resolveFirstDayOfWeek(user, workspace, locale),
160
- timeFormat: resolveTimeFormat(user, locale),
161
- };
162
- }
163
-
164
- /**
165
- * Converts first day of week string to number (for Date compatibility)
166
- * 0 = Sunday, 1 = Monday, 6 = Saturday
167
- */
168
- export function firstDayToNumber(day: FirstDayOfWeek, locale?: string): number {
169
- if (day === 'auto') {
170
- day = detectLocaleFirstDay(locale);
171
- }
172
-
173
- switch (day) {
174
- case 'sunday':
175
- return 0;
176
- case 'monday':
177
- return 1;
178
- case 'saturday':
179
- return 6;
180
- default:
181
- return 1; // Default to Monday
182
- }
183
- }
184
-
185
- /**
186
- * Converts number to first day of week string
187
- * 0 = Sunday, 1 = Monday, 6 = Saturday
188
- */
189
- export function numberToFirstDay(num: number): FirstDayOfWeek {
190
- switch (num) {
191
- case 0:
192
- return 'sunday';
193
- case 1:
194
- return 'monday';
195
- case 6:
196
- return 'saturday';
197
- default:
198
- return 'monday'; // Default to Monday
199
- }
200
- }
1
+ export * from '@tuturuuu/utils/calendar-settings-resolver';
@@ -0,0 +1,57 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import { render, screen } from '@testing-library/react';
4
+ import { Button } from '@tuturuuu/ui/button';
5
+ import { Card, CardContent, CardHeader, CardTitle } from '@tuturuuu/ui/card';
6
+ import { describe, expect, it } from 'vitest';
7
+
8
+ const repoRoot = resolve(
9
+ process.cwd(),
10
+ process.cwd().endsWith('/packages/ui') ? '../..' : '.'
11
+ );
12
+
13
+ function readRepoFile(path: string) {
14
+ return readFileSync(resolve(repoRoot, path), 'utf8');
15
+ }
16
+
17
+ describe('@tuturuuu/ui public quickstart', () => {
18
+ it('documents only executable public package entry points', () => {
19
+ const readme = readRepoFile('packages/ui/README.md');
20
+ const packageJson = JSON.parse(
21
+ readRepoFile('packages/ui/package.json')
22
+ ) as {
23
+ exports: Record<string, unknown>;
24
+ };
25
+
26
+ expect(
27
+ readme.match(/import '@tuturuuu\/ui\/globals\.css';/gu) ?? []
28
+ ).toHaveLength(1);
29
+ expect(readme).toContain("import { Button } from '@tuturuuu/ui/button';");
30
+ expect(readme).toMatch(
31
+ /import\s*\{[\s\S]*?Card[\s\S]*?CardContent[\s\S]*?CardHeader[\s\S]*?CardTitle[\s\S]*?\}\s*from '@tuturuuu\/ui\/card';/u
32
+ );
33
+ expect(readme).not.toMatch(/from ['"]@tuturuuu\/ui['"]/u);
34
+ expect(readme).not.toContain('variant="primary"');
35
+ expect(packageJson.exports['.']).toBeUndefined();
36
+ expect(packageJson.exports['./button']).toBeDefined();
37
+ expect(packageJson.exports['./*']).toBe('./src/components/ui/*.tsx');
38
+ });
39
+
40
+ it('renders the documented component tree through public subpaths', () => {
41
+ render(
42
+ <Card>
43
+ <CardHeader>
44
+ <CardTitle>Welcome to Tuturuuu</CardTitle>
45
+ </CardHeader>
46
+ <CardContent>
47
+ <Button>Get started</Button>
48
+ </CardContent>
49
+ </Card>
50
+ );
51
+
52
+ expect(
53
+ screen.getByRole('button', { name: 'Get started' })
54
+ ).toBeInTheDocument();
55
+ expect(screen.getByText('Welcome to Tuturuuu')).toBeInTheDocument();
56
+ });
57
+ });