@tuturuuu/ui 0.25.3 → 0.26.1

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 CHANGED
@@ -1,5 +1,104 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.26.1](https://github.com/tutur3u/platform/compare/ui-v0.26.0...ui-v0.26.1) (2026-08-11)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **vercel:** harden navigation prefetch policy ([b8cf61c](https://github.com/tutur3u/platform/commit/b8cf61c86727817faff1f28da768fee4b3e52e68))
9
+
10
+
11
+ ### Performance Improvements
12
+
13
+ * **vercel:** reduce speculative app requests ([423e240](https://github.com/tutur3u/platform/commit/423e2401c16b2cad4785ba46a08bf5b3d1c8a749))
14
+ * **vercel:** reduce speculative app requests ([#5116](https://github.com/tutur3u/platform/issues/5116)) ([9e2b59b](https://github.com/tutur3u/platform/commit/9e2b59bcaa0bbc7a593ff91e2d2331aca4ab6595))
15
+
16
+ ## [0.26.0](https://github.com/tutur3u/platform/compare/ui-v0.25.3...ui-v0.26.0) (2026-08-09)
17
+
18
+
19
+ ### Features
20
+
21
+ * **apps:** add resilient error recovery shells ([f0f514d](https://github.com/tutur3u/platform/commit/f0f514d2b1712ea76c6845801fa2803369418a63))
22
+ * **chat:** add connected site sync bridge ([05c27f5](https://github.com/tutur3u/platform/commit/05c27f5ac4a545af366097ee2a919ec755970b9b))
23
+ * **chat:** add connected site sync bridge ([#5078](https://github.com/tutur3u/platform/issues/5078)) ([3de1a07](https://github.com/tutur3u/platform/commit/3de1a07dfa2fec96982aa0d116e003d0aa3fe47c))
24
+ * **chat:** add external parity reconciliation ([953b9d6](https://github.com/tutur3u/platform/commit/953b9d6315611ef86ec2c213bbbbc5e62d5f4ad4))
25
+ * **chat:** add external parity reconciliation ([#5086](https://github.com/tutur3u/platform/issues/5086)) ([5ef796f](https://github.com/tutur3u/platform/commit/5ef796f7812ec6a9f9a62193ba21633cd2503001))
26
+ * **chat:** complete connected-site operational parity ([#5093](https://github.com/tutur3u/platform/issues/5093)) ([397e11b](https://github.com/tutur3u/platform/commit/397e11bd87d583fe1c65f83a2b8019c287650e19))
27
+ * **chat:** complete connected-site parity ([fd4061d](https://github.com/tutur3u/platform/commit/fd4061d8b2f654e521c40ea9819a348ae81575c9))
28
+ * **finance:** reconcile inventory provider sales ([56650e0](https://github.com/tutur3u/platform/commit/56650e066c473337700998d8af607380e8ea6202))
29
+ * **inventory:** allocate bundle revenue and add cash checkout ([bd465f9](https://github.com/tutur3u/platform/commit/bd465f9bb35fad16d259378fa49611a8eb5ba95b))
30
+ * **inventory:** secure Square POS event checkout ([532b463](https://github.com/tutur3u/platform/commit/532b46372116ee0bebfd83ba2af762cc9f668c3c))
31
+ * **landing:** retile the app bento and rebuild the problem section ([c56f42a](https://github.com/tutur3u/platform/commit/c56f42adf754362a269ff08b380db1ee0cf8c6ca))
32
+ * **meet:** revamp collaborative scheduling ([9124a5d](https://github.com/tutur3u/platform/commit/9124a5d559e071c7e6c1c713cfbca4d9f5205611))
33
+ * **navigation:** open workspace selector on reveal ([c760d42](https://github.com/tutur3u/platform/commit/c760d42b5e0b8f7bd141e26e2ace956b7f39912e))
34
+ * **offline:** own service worker runtime and refresh dependencies ([ae44477](https://github.com/tutur3u/platform/commit/ae44477603c39f0513244514771653287338a89f))
35
+ * **onboarding:** connect product guidance across apps ([68cf626](https://github.com/tutur3u/platform/commit/68cf626c9650e5044b6c123f9423a6cebf1bba9a))
36
+ * **reports:** add periodic reporting automation ([ec7bd5e](https://github.com/tutur3u/platform/commit/ec7bd5e10abb137e217d1dcf143624530276392f))
37
+ * **ui:** align landing and locale experience ([aa6e47f](https://github.com/tutur3u/platform/commit/aa6e47f17356ce74111ccf130e8b17071cc7aadf))
38
+ * **web:** improve workspace navigation context ([73959e3](https://github.com/tutur3u/platform/commit/73959e3c6745f60513d23740f30024c5d24e7ee4))
39
+ * **web:** redesign marketing landing page ([13ede78](https://github.com/tutur3u/platform/commit/13ede784ac03af9f25bbb7df61095826c335e002))
40
+ * **web:** refine marketing experience ([c7f1cec](https://github.com/tutur3u/platform/commit/c7f1cec0dd667e6d5f59aaf0bab82069b79c7376))
41
+
42
+
43
+ ### Bug Fixes
44
+
45
+ * **auth:** isolate invitation caches by user ([c720f15](https://github.com/tutur3u/platform/commit/c720f1551b1276bedf393bd4fc8975e33f64aa83))
46
+ * **auth:** resume a pending sign-in after switching accounts ([5403718](https://github.com/tutur3u/platform/commit/54037189ee895863eac621239eb54b8ea94c6ed0))
47
+ * **chat:** address external bridge review findings ([30533fc](https://github.com/tutur3u/platform/commit/30533fc64093fed4b25279bc0e45d43bdb7a6cde))
48
+ * **chat:** close bridge edge cases ([db585c8](https://github.com/tutur3u/platform/commit/db585c8486064837dcbce615a30f66c38c0a3ec0))
49
+ * **chat:** decode external visitor labels ([1ca3e75](https://github.com/tutur3u/platform/commit/1ca3e75a939c06222c842da9fe5892a1bf9ce729))
50
+ * **chat:** decode external visitor labels ([#5112](https://github.com/tutur3u/platform/issues/5112)) ([cb7af54](https://github.com/tutur3u/platform/commit/cb7af54264344961234da681c877c5677bf950fa))
51
+ * **chat:** harden bridge review edge cases ([0ecfad7](https://github.com/tutur3u/platform/commit/0ecfad7c66454fdb3bcd971f1362d3789287f086))
52
+ * **chat:** harden connected site migration ([73cccd0](https://github.com/tutur3u/platform/commit/73cccd04e9bd85a443891f3e52b443ef755570cb))
53
+ * **chat:** harden connected site sync ([70e8785](https://github.com/tutur3u/platform/commit/70e8785967e145a77330272e671ca58dfc38fbee))
54
+ * **chat:** harden connected-site parity ([a0d3148](https://github.com/tutur3u/platform/commit/a0d31483a87ed0ebd59f2be5c3bdbb342bd67855))
55
+ * **chat:** harden external bridge boundaries ([2d25202](https://github.com/tutur3u/platform/commit/2d252023e5cfb1c7414ee2ed2d161e7f5de613f8))
56
+ * **chat:** harden external bridge synchronization ([5b7c709](https://github.com/tutur3u/platform/commit/5b7c7092260b05c8de86c2383291433f81c9eead))
57
+ * **chat:** harden external bridge synchronization ([63ccf91](https://github.com/tutur3u/platform/commit/63ccf9129ff8e7cf255bef8c74a3e096ab45cb7d))
58
+ * **chat:** harden external parity replay ([896923b](https://github.com/tutur3u/platform/commit/896923b3c498a09fe0974de1aa0f8d291f8b2c84))
59
+ * **chat:** harden external parity replay ([89c5498](https://github.com/tutur3u/platform/commit/89c5498aef4ad5ebf05ef93bfb05168417a5dfe2))
60
+ * **chat:** mount query state adapter ([#5084](https://github.com/tutur3u/platform/issues/5084)) ([60253f8](https://github.com/tutur3u/platform/commit/60253f82477fc54923556706d12e4dc525a7a170))
61
+ * **chat:** preserve native message behavior ([fc09530](https://github.com/tutur3u/platform/commit/fc095301f2d26ca2bc39de02ce17b1f341327de9))
62
+ * **chat:** restore external inbox display parity ([#5108](https://github.com/tutur3u/platform/issues/5108)) ([b34ea6d](https://github.com/tutur3u/platform/commit/b34ea6d7d8ffda344053ed1f580128446d0d0a81))
63
+ * **ci:** keep task helper collection source-only ([7355a05](https://github.com/tutur3u/platform/commit/7355a05385ccd8d1e2d1bcf6ac15ae5297e057ad))
64
+ * **ci:** resolve shared task helper during e2e collection ([0319a3b](https://github.com/tutur3u/platform/commit/0319a3bd0df12592b823b7cebdff38cea3e50836))
65
+ * **deps:** align Streamdown with Shiki 4 ([0d89f55](https://github.com/tutur3u/platform/commit/0d89f555b3228307e893a5d7723aaf8db8505d63))
66
+ * **inventory:** correct storefront bundle checkout ([cb9c247](https://github.com/tutur3u/platform/commit/cb9c24740ceb77f9426ba640383a68bd9ed95d2c))
67
+ * **platform:** improve satellite workspace routing ([0c49c48](https://github.com/tutur3u/platform/commit/0c49c4882d26704fb16ba94ee289ab0af7deb4de))
68
+ * **platform:** merge notification and group visibility fixes ([4fe9e97](https://github.com/tutur3u/platform/commit/4fe9e970bf61bffaee4353b1ebb83ce2f880a4c8))
69
+ * **platform:** persist settings dialog state ([04d2128](https://github.com/tutur3u/platform/commit/04d212807b6fabf33f43743c0b779ba9499334ba))
70
+ * **platform:** restore notifications and group visibility ([eb570a4](https://github.com/tutur3u/platform/commit/eb570a47e7a3d38fc855fbf3e887ecbde853ece0))
71
+ * **platform:** stabilize offline and calendar initialization ([8ef8fbc](https://github.com/tutur3u/platform/commit/8ef8fbc1274e108930adf38bb8e998384551058b))
72
+ * resolve code quality findings ([63f10b5](https://github.com/tutur3u/platform/commit/63f10b5ec22a4194f48f448ee2b1b088b5da8f08))
73
+ * **security:** remediate code scanning findings ([023db2e](https://github.com/tutur3u/platform/commit/023db2edf4b0557be108a9d772cbc7e2223af947))
74
+ * **settings:** harden workspace role management ([0570687](https://github.com/tutur3u/platform/commit/0570687d255698e438d1eb02262ec0fa7c56240f))
75
+ * **settings:** repair satellite workspace management ([63614cd](https://github.com/tutur3u/platform/commit/63614cdd1550cbf7084724dbed728e798b6f979c))
76
+ * **tasks:** quiet the description toolbar and name the compact action honestly ([51838a0](https://github.com/tutur3u/platform/commit/51838a08a1875a44cd4c7f539b35203f890e1a39))
77
+ * **tasks:** repair board share access and harden AI media attachments ([65b8092](https://github.com/tutur3u/platform/commit/65b809245cc01fb7a5f034f703083f329c20f1c1))
78
+ * **tasks:** repair onboarding and external metadata ([e0b62eb](https://github.com/tutur3u/platform/commit/e0b62eb7119155f6e4cad3dc4fb4d0f9820c98e8))
79
+ * **tasks:** restore task description on deep-link opens ([56d4efa](https://github.com/tutur3u/platform/commit/56d4efabd2f036e7dae39588d8aed7b129fa9a11))
80
+ * **tasks:** show one skeleton per task dialog open ([944288b](https://github.com/tutur3u/platform/commit/944288bdc869ee80b3cdf537ee10feb17fdada22))
81
+ * **tasks:** stop the task dialog remounting after it opens ([9a423da](https://github.com/tutur3u/platform/commit/9a423daf7e7c1e901a63ddcc1a8a94ddff9192c9))
82
+ * **ui:** make member management mobile responsive ([104c812](https://github.com/tutur3u/platform/commit/104c8120949ef34bfdc48da74fdc0e2d35eb4ce7))
83
+ * **ui:** preserve hosted workspace logos ([1f8e8b2](https://github.com/tutur3u/platform/commit/1f8e8b28c23f659952ad58f00a2a371acd8fcef9))
84
+ * **ui:** publish portable vendored dependencies ([99ad359](https://github.com/tutur3u/platform/commit/99ad35903a27b8f9df26f22fc5f1003aed363d6f))
85
+ * **ui:** refine member access settings ([b5b1ebf](https://github.com/tutur3u/platform/commit/b5b1ebf49a4a3b93532da74357899927f6e5d988))
86
+ * **ui:** unify workspace access and sidebar controls ([450183e](https://github.com/tutur3u/platform/commit/450183ef78af8e09a386bebb93be7018379c5152))
87
+ * **web:** harden prerender client boundaries ([ab1596a](https://github.com/tutur3u/platform/commit/ab1596a769e5a6ae43b0247c651ecbecea52d20d))
88
+ * **workspaces:** harden invitation interactions ([b7f8f6c](https://github.com/tutur3u/platform/commit/b7f8f6cf52ceec1b67d36b11513eae7806284f5d))
89
+ * **workspaces:** propagate a workspace rename without a page reload ([cca0b2d](https://github.com/tutur3u/platform/commit/cca0b2d7e14f6d72f1fdda1deaac18aaf07e9eee))
90
+ * **workspaces:** repair shared member administration ([8a1515b](https://github.com/tutur3u/platform/commit/8a1515ba970adbe690b9d92f77a447adb93c339f))
91
+ * **workspaces:** restore invitation access across apps ([3592699](https://github.com/tutur3u/platform/commit/35926990fc28e44996fd330c6403496d78d96872))
92
+ * **workspaces:** restore invitation access across apps ([#5099](https://github.com/tutur3u/platform/issues/5099)) ([c7032c3](https://github.com/tutur3u/platform/commit/c7032c310639c2783b60ac560e83a84d65a5c7f5))
93
+ * **workspaces:** route satellite creation through setup ([754bf1b](https://github.com/tutur3u/platform/commit/754bf1b81360e4755a171819f3a8e6a7c102f351))
94
+
95
+
96
+ ### Performance Improvements
97
+
98
+ * **inventory:** add event-driven storefront caching ([2f571f4](https://github.com/tutur3u/platform/commit/2f571f4fc534bf5dee0ca36c4310d87b44e6e613))
99
+ * **storefront:** share customer navigation shell ([21cb78d](https://github.com/tutur3u/platform/commit/21cb78db484d1e1fdb9f2469fcac4e3a948bd176))
100
+ * **storefront:** stabilize navigation and cart rendering ([471c6b7](https://github.com/tutur3u/platform/commit/471c6b75abc6a4d69692e868cc1b220785e03262))
101
+
3
102
  ## [0.25.3](https://github.com/tutur3u/platform/compare/ui-v0.25.2...ui-v0.25.3) (2026-08-09)
4
103
 
5
104
 
package/README.md CHANGED
@@ -6,12 +6,11 @@ Shared UI components and design system for Tuturuuu Platform.
6
6
 
7
7
  ```bash
8
8
  npm install @tuturuuu/ui
9
- # or
10
- yarn add @tuturuuu/ui
11
- # or
12
- bun add @tuturuuu/ui
13
9
  ```
14
10
 
11
+ Using another package manager? Run `pnpm add @tuturuuu/ui`,
12
+ `yarn add @tuturuuu/ui`, or `bun add @tuturuuu/ui` instead.
13
+
15
14
  The public package depends on other published Tuturuuu packages, including
16
15
  `@tuturuuu/apis` and `@tuturuuu/ai`. Release those dependencies first, then
17
16
  publish UI from the same production commit so standalone apps can install
@@ -19,18 +18,41 @@ publish UI from the same production commit so standalone apps can install
19
18
 
20
19
  ## Usage
21
20
 
21
+ Import the shared stylesheet once from your app's root layout or entry point:
22
+
22
23
  ```typescript
23
- import { Button, Card } from '@tuturuuu/ui'
24
+ import '@tuturuuu/ui/globals.css';
25
+ ```
26
+
27
+ Then import each component from its public subpath:
28
+
29
+ ```tsx
30
+ import { Button } from '@tuturuuu/ui/button';
31
+ import {
32
+ Card,
33
+ CardContent,
34
+ CardHeader,
35
+ CardTitle,
36
+ } from '@tuturuuu/ui/card';
24
37
 
25
38
  export default function MyComponent() {
26
39
  return (
27
40
  <Card>
28
- <Button variant="primary">Click me</Button>
41
+ <CardHeader>
42
+ <CardTitle>Welcome to Tuturuuu</CardTitle>
43
+ </CardHeader>
44
+ <CardContent>
45
+ <Button>Get started</Button>
46
+ </CardContent>
29
47
  </Card>
30
- )
48
+ );
31
49
  }
32
50
  ```
33
51
 
52
+ The package exports TypeScript and TSX source files. Configure your framework
53
+ or bundler to transpile `@tuturuuu/ui` and its published Tuturuuu dependencies
54
+ when it does not transpile package source automatically.
55
+
34
56
  ## Features
35
57
 
36
58
  - Fully accessible components
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tuturuuu/ui",
3
- "version": "0.25.3",
3
+ "version": "0.26.1",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -92,7 +92,7 @@
92
92
  "@tuturuuu/icons": "0.1.0",
93
93
  "@tuturuuu/internal-api": "0.29.0",
94
94
  "@tuturuuu/supabase": "0.5.0",
95
- "@tuturuuu/utils": "0.23.1",
95
+ "@tuturuuu/utils": "0.24.1",
96
96
  "@types/debug": "^4.1.13",
97
97
  "browser-image-compression": "^2.0.2",
98
98
  "class-variance-authority": "^0.7.1",
@@ -396,12 +396,6 @@
396
396
  },
397
397
  "./calendar-app/hooks/*": "./src/components/ui/calendar-app/hooks/*.ts",
398
398
  "./calendar-app/components/*": "./src/components/ui/calendar-app/components/*.tsx",
399
- "./calendar-app/components/sidebar/*": "./src/components/ui/calendar-app/components/sidebar/*.tsx",
400
- "./calendar-app/components/time-tracker": {
401
- "types": "./src/components/ui/calendar-app/components/time-tracker/index.tsx",
402
- "import": "./src/components/ui/calendar-app/components/time-tracker/index.tsx"
403
- },
404
- "./calendar-app/components/time-tracker/*": "./src/components/ui/calendar-app/components/time-tracker/*.tsx",
405
399
  "./custom/icon-picker": {
406
400
  "types": "./src/components/ui/custom/icon-picker/index.ts",
407
401
  "import": "./src/components/ui/custom/icon-picker/index.ts"
@@ -0,0 +1,32 @@
1
+ import {
2
+ resolveFirstDayOfWeek as resolveCanonicalFirstDayOfWeek,
3
+ resolveTimezone as resolveCanonicalTimezone,
4
+ } from '@tuturuuu/utils/calendar-settings-resolver';
5
+ import { describe, expect, it } from 'vitest';
6
+ import {
7
+ resolveFirstDayOfWeek,
8
+ resolveTimezone,
9
+ } from '../../../../lib/calendar-settings-resolver';
10
+
11
+ describe('shared calendar settings resolver integration', () => {
12
+ it('keeps the UI compatibility path on the canonical resolver', () => {
13
+ expect(resolveTimezone).toBe(resolveCanonicalTimezone);
14
+ expect(resolveFirstDayOfWeek).toBe(resolveCanonicalFirstDayOfWeek);
15
+ });
16
+
17
+ it('prefers saved user values and preserves locale fallback', () => {
18
+ expect(
19
+ resolveTimezone(
20
+ { timezone: 'Asia/Ho_Chi_Minh' },
21
+ { timezone: 'Europe/London' }
22
+ )
23
+ ).toBe('Asia/Ho_Chi_Minh');
24
+ expect(
25
+ resolveFirstDayOfWeek(
26
+ { first_day_of_week: 'auto' },
27
+ { first_day_of_week: 'auto' },
28
+ 'vi-VN'
29
+ )
30
+ ).toBe('monday');
31
+ });
32
+ });
@@ -7,10 +7,12 @@ import { NavLink } from './nav-link';
7
7
 
8
8
  const navigationState = vi.hoisted(() => ({
9
9
  pathname: '/personal/tasks',
10
+ prefetch: vi.fn(),
10
11
  }));
11
12
 
12
13
  vi.mock('next/navigation', () => ({
13
14
  usePathname: () => navigationState.pathname,
15
+ useRouter: () => ({ prefetch: navigationState.prefetch }),
14
16
  }));
15
17
 
16
18
  vi.mock('next-intl', () => ({
@@ -51,9 +53,54 @@ function renderNavLink(
51
53
  describe('NavLink', () => {
52
54
  beforeEach(() => {
53
55
  navigationState.pathname = '/personal/tasks';
56
+ navigationState.prefetch.mockReset();
54
57
  vi.restoreAllMocks();
55
58
  });
56
59
 
60
+ it('prefetches internal routes only after navigation intent', () => {
61
+ renderNavLink({
62
+ href: '/personal/tasks/boards',
63
+ title: 'Boards',
64
+ });
65
+
66
+ const link = screen.getByRole('link', { name: 'Boards' });
67
+ expect(navigationState.prefetch).not.toHaveBeenCalled();
68
+
69
+ fireEvent.mouseEnter(link);
70
+ fireEvent.focus(link);
71
+
72
+ expect(navigationState.prefetch).toHaveBeenCalledTimes(1);
73
+ expect(navigationState.prefetch).toHaveBeenCalledWith(
74
+ '/personal/tasks/boards'
75
+ );
76
+ });
77
+
78
+ it('does not prefetch external routes', () => {
79
+ renderNavLink({
80
+ external: true,
81
+ href: 'https://docs.tuturuuu.com',
82
+ title: 'Docs',
83
+ });
84
+
85
+ fireEvent.mouseEnter(screen.getByRole('link', { name: 'Docs' }));
86
+
87
+ expect(navigationState.prefetch).not.toHaveBeenCalled();
88
+ });
89
+
90
+ it.each([
91
+ ['protocol-relative', '//docs.tuturuuu.com/guide'],
92
+ ['email', 'mailto:support@tuturuuu.com'],
93
+ ['telephone', 'tel:+84123456789'],
94
+ ])('does not prefetch %s URLs', (_label, href) => {
95
+ renderNavLink({ href, title: 'External destination' });
96
+
97
+ fireEvent.mouseEnter(
98
+ screen.getByRole('link', { name: 'External destination' })
99
+ );
100
+
101
+ expect(navigationState.prefetch).not.toHaveBeenCalled();
102
+ });
103
+
57
104
  it('matches wildcard aliases even when the primary route is exact', () => {
58
105
  navigationState.pathname = '/personal/tasks/boards/board-1';
59
106
 
@@ -12,9 +12,9 @@ import {
12
12
  import { getWorkspaceConfigIdList } from '@tuturuuu/internal-api';
13
13
  import { cn } from '@tuturuuu/utils/format';
14
14
  import Link from 'next/link';
15
- import { usePathname } from 'next/navigation';
15
+ import { usePathname, useRouter } from 'next/navigation';
16
16
  import { useTranslations } from 'next-intl';
17
- import { useState } from 'react';
17
+ import { useRef, useState } from 'react';
18
18
  import { Badge } from '../badge';
19
19
  import { Button } from '../button';
20
20
  import {
@@ -58,7 +58,7 @@ function getComparablePath(
58
58
  ? new URL(target)
59
59
  : new URL(target, base);
60
60
 
61
- if (isAbsoluteHttpUrl(target) && url.origin !== base) return null;
61
+ if (url.origin !== base) return null;
62
62
 
63
63
  return url.pathname;
64
64
  } catch {
@@ -103,6 +103,7 @@ export function NavLink({
103
103
  }: NavLinkProps) {
104
104
  const t = useTranslations();
105
105
  const pathname = usePathname();
106
+ const router = useRouter();
106
107
  const {
107
108
  title,
108
109
  icon,
@@ -338,6 +339,27 @@ export function NavLink({
338
339
  })()
339
340
  : href;
340
341
 
342
+ const prefetchedHrefRef = useRef<string | null>(null);
343
+ const prefetchOnIntent = () => {
344
+ if (
345
+ !effectiveHref ||
346
+ newTab ||
347
+ link.external ||
348
+ isDisabled ||
349
+ isResolvingHref ||
350
+ hasSubMenu ||
351
+ archivedItems.length > 0 ||
352
+ !getComparablePath(effectiveHref)
353
+ ) {
354
+ return;
355
+ }
356
+
357
+ if (prefetchedHrefRef.current === effectiveHref) return;
358
+
359
+ prefetchedHrefRef.current = effectiveHref;
360
+ router.prefetch(effectiveHref);
361
+ };
362
+
341
363
  const commonProps = {
342
364
  className: cn(
343
365
  'group/navlink flex w-full cursor-pointer items-center justify-between rounded-md p-2 font-medium text-sm',
@@ -388,6 +410,8 @@ export function NavLink({
388
410
  onClick();
389
411
  }
390
412
  },
413
+ onFocus: prefetchOnIntent,
414
+ onMouseEnter: prefetchOnIntent,
391
415
  };
392
416
 
393
417
  const linkElement =
@@ -425,6 +449,7 @@ export function NavLink({
425
449
  ) : effectiveHref && !isDisabled ? (
426
450
  <Link
427
451
  href={effectiveHref}
452
+ prefetch={false}
428
453
  {...commonProps}
429
454
  target={newTab ? '_blank' : '_self'}
430
455
  >
@@ -0,0 +1,118 @@
1
+ import { fireEvent, render, screen, waitFor } from '@testing-library/react';
2
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
3
+ import { PlanDetailsPollContent } from './poll-display';
4
+
5
+ const mocks = vi.hoisted(() => ({
6
+ addPollOption: vi.fn(),
7
+ createPoll: vi.fn(),
8
+ deletePoll: vi.fn(),
9
+ deletePollOption: vi.fn(),
10
+ refresh: vi.fn(),
11
+ submitVote: vi.fn(),
12
+ toast: vi.fn(),
13
+ toggleWherePoll: vi.fn(),
14
+ useTimeBlocking: vi.fn(),
15
+ }));
16
+
17
+ vi.mock('@tuturuuu/apis/meet/actions', () => ({
18
+ addPollOption: (...args: unknown[]) => mocks.addPollOption(...args),
19
+ createPoll: (...args: unknown[]) => mocks.createPoll(...args),
20
+ deletePoll: (...args: unknown[]) => mocks.deletePoll(...args),
21
+ deletePollOption: (...args: unknown[]) => mocks.deletePollOption(...args),
22
+ submitVote: (...args: unknown[]) => mocks.submitVote(...args),
23
+ toggleWherePoll: (...args: unknown[]) => mocks.toggleWherePoll(...args),
24
+ }));
25
+
26
+ vi.mock('@tuturuuu/ui/hooks/time-blocking-provider', () => ({
27
+ useTimeBlocking: () => mocks.useTimeBlocking(),
28
+ }));
29
+
30
+ vi.mock('@tuturuuu/ui/hooks/use-toast', () => ({
31
+ toast: (...args: unknown[]) => mocks.toast(...args),
32
+ }));
33
+
34
+ vi.mock('next/navigation', () => ({
35
+ useRouter: () => ({ refresh: mocks.refresh }),
36
+ }));
37
+
38
+ vi.mock('next-intl', () => ({
39
+ useTranslations: () => (key: string) => key,
40
+ }));
41
+
42
+ vi.mock('./where-tu-meet', () => ({
43
+ DefaultWherePollContent: ({
44
+ onAddOption,
45
+ onVote,
46
+ }: {
47
+ onAddOption: (pollId: string, value: string) => Promise<unknown>;
48
+ onVote: (pollId: string, optionIds: string[]) => Promise<void>;
49
+ }) => (
50
+ <div>
51
+ <button type="button" onClick={() => onAddOption('poll-1', 'Cafe')}>
52
+ add guest option
53
+ </button>
54
+ <button type="button" onClick={() => onVote('poll-1', ['option-1'])}>
55
+ submit guest vote
56
+ </button>
57
+ </div>
58
+ ),
59
+ }));
60
+
61
+ describe('PlanDetailsPollContent guest authorization', () => {
62
+ beforeEach(() => {
63
+ vi.clearAllMocks();
64
+ mocks.useTimeBlocking.mockReturnValue({
65
+ user: {
66
+ display_name: 'Guest',
67
+ id: 'guest-1',
68
+ is_guest: true,
69
+ password_hash: 'plan-bound-credential',
70
+ },
71
+ });
72
+ mocks.addPollOption.mockResolvedValue({
73
+ data: {
74
+ option: {
75
+ created_at: '2026-08-10T00:00:00.000Z',
76
+ guestVotes: [],
77
+ id: 'option-1',
78
+ poll_id: 'poll-1',
79
+ totalVotes: 0,
80
+ userVotes: [],
81
+ value: 'Cafe',
82
+ },
83
+ },
84
+ });
85
+ mocks.submitVote.mockResolvedValue({ data: { success: true } });
86
+ });
87
+
88
+ it('forwards the selected guest credential to option and vote actions', async () => {
89
+ render(
90
+ <PlanDetailsPollContent
91
+ plan={{ id: 'plan-1', is_confirmed: false, where_to_meet: true }}
92
+ isCreator={false}
93
+ platformUser={null}
94
+ polls={null}
95
+ />
96
+ );
97
+
98
+ fireEvent.click(screen.getByRole('button', { name: 'add guest option' }));
99
+ fireEvent.click(screen.getByRole('button', { name: 'submit guest vote' }));
100
+
101
+ await waitFor(() => {
102
+ expect(mocks.addPollOption).toHaveBeenCalledWith('plan-1', {
103
+ guestId: 'guest-1',
104
+ guestPasswordHash: 'plan-bound-credential',
105
+ pollId: 'poll-1',
106
+ userType: 'GUEST',
107
+ value: 'Cafe',
108
+ });
109
+ expect(mocks.submitVote).toHaveBeenCalledWith('plan-1', {
110
+ guestId: 'guest-1',
111
+ guestPasswordHash: 'plan-bound-credential',
112
+ optionIds: ['option-1'],
113
+ pollId: 'poll-1',
114
+ userType: 'GUEST',
115
+ });
116
+ });
117
+ });
118
+ });
@@ -94,6 +94,10 @@ export function PlanDetailsPollContent({
94
94
  optionIds,
95
95
  userType: userType as 'PLATFORM' | 'GUEST',
96
96
  guestId: userType === 'GUEST' ? (user?.id ?? undefined) : undefined,
97
+ guestPasswordHash:
98
+ userType === 'GUEST' && guestUser?.is_guest
99
+ ? guestUser.password_hash
100
+ : undefined,
97
101
  });
98
102
  if (result.error) {
99
103
  toast({
@@ -111,6 +115,10 @@ export function PlanDetailsPollContent({
111
115
  value,
112
116
  userType: userType as 'PLATFORM' | 'GUEST',
113
117
  guestId: userType === 'GUEST' ? (user?.id ?? undefined) : undefined,
118
+ guestPasswordHash:
119
+ userType === 'GUEST' && guestUser?.is_guest
120
+ ? guestUser.password_hash
121
+ : undefined,
114
122
  });
115
123
  if (result.error) {
116
124
  toast({
@@ -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
+ });