@tuturuuu/ui 0.14.0 → 0.17.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 (101) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/biome.json +1 -1
  3. package/package.json +63 -62
  4. package/src/components/ui/custom/__tests__/settings-dialog-shell.test.tsx +12 -0
  5. package/src/components/ui/custom/__tests__/sidebar-context.test.tsx +93 -6
  6. package/src/components/ui/custom/loading-statistic-card.tsx +18 -0
  7. package/src/components/ui/custom/notification-popover-client.tsx +5 -1
  8. package/src/components/ui/custom/settings/keyboard-shortcuts-settings.test.tsx +34 -0
  9. package/src/components/ui/custom/settings/keyboard-shortcuts-settings.tsx +106 -0
  10. package/src/components/ui/custom/settings/sidebar-settings.tsx +18 -6
  11. package/src/components/ui/custom/settings-dialog-shell.tsx +18 -12
  12. package/src/components/ui/custom/sidebar-context.tsx +58 -3
  13. package/src/components/ui/custom/sidebar-remote-behavior-bridge.tsx +5 -3
  14. package/src/components/ui/custom/statistic-card.tsx +104 -0
  15. package/src/components/ui/custom/structure.test.tsx +29 -0
  16. package/src/components/ui/custom/structure.tsx +103 -88
  17. package/src/components/ui/finance/invoices/civil-month.ts +224 -0
  18. package/src/components/ui/finance/invoices/components/subscription-invoice-proposal.test.tsx +49 -0
  19. package/src/components/ui/finance/invoices/components/subscription-invoice-proposal.tsx +81 -0
  20. package/src/components/ui/finance/invoices/hooks/use-subscription-auto-selection.ts +8 -2
  21. package/src/components/ui/finance/invoices/new-invoice-page.test.tsx +90 -2
  22. package/src/components/ui/finance/invoices/new-invoice-page.tsx +40 -4
  23. package/src/components/ui/finance/invoices/pending-columns.test.ts +26 -0
  24. package/src/components/ui/finance/invoices/pending-columns.tsx +45 -14
  25. package/src/components/ui/finance/invoices/standard-invoice.test.ts +31 -0
  26. package/src/components/ui/finance/invoices/standard-invoice.tsx +33 -16
  27. package/src/components/ui/finance/invoices/subscription-invoice.test.tsx +331 -0
  28. package/src/components/ui/finance/invoices/subscription-invoice.tsx +53 -28
  29. package/src/components/ui/finance/invoices/utils.test.ts +84 -0
  30. package/src/components/ui/finance/invoices/utils.ts +56 -134
  31. package/src/components/ui/resizable.test.ts +15 -0
  32. package/src/components/ui/resizable.tsx +28 -6
  33. package/src/components/ui/storefront/browse-panel.tsx +161 -0
  34. package/src/components/ui/storefront/bundle-selection-dialog.tsx +1 -1
  35. package/src/components/ui/storefront/cart-popover.tsx +1 -1
  36. package/src/components/ui/storefront/hero-panel.tsx +35 -30
  37. package/src/components/ui/storefront/listing-card.tsx +10 -5
  38. package/src/components/ui/storefront/merch-sections.tsx +7 -5
  39. package/src/components/ui/storefront/product-detail.tsx +79 -6
  40. package/src/components/ui/storefront/product-dialog.tsx +5 -1
  41. package/src/components/ui/storefront/storefront-surface.test.tsx +72 -0
  42. package/src/components/ui/storefront/storefront-surface.tsx +67 -63
  43. package/src/components/ui/storefront/types.ts +28 -0
  44. package/src/components/ui/storefront/utils.ts +4 -5
  45. package/src/components/ui/tu-do/boards/boardId/board-column-external-retry.test.tsx +59 -1
  46. package/src/components/ui/tu-do/boards/boardId/board-column.tsx +37 -8
  47. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/__tests__/bulk-mutations-move.test.tsx +62 -0
  48. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.test.ts +48 -0
  49. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.ts +32 -2
  50. package/src/components/ui/tu-do/boards/boardId/menus/__tests__/task-menus.test.tsx +41 -3
  51. package/src/components/ui/tu-do/boards/boardId/menus/task-due-date-menu.tsx +31 -10
  52. package/src/components/ui/tu-do/boards/boardId/menus/task-priority-menu.tsx +7 -5
  53. package/src/components/ui/tu-do/boards/boardId/menus/task-scheduling-menu.tsx +7 -5
  54. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-archive-visibility.test.ts +44 -0
  55. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-archive-visibility.ts +17 -0
  56. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.test.ts +2 -0
  57. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.ts +7 -0
  58. package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +44 -30
  59. package/src/components/ui/tu-do/my-tasks/__tests__/use-task-context-actions.test.ts +54 -0
  60. package/src/components/ui/tu-do/my-tasks/task-list-with-completion.tsx +43 -1
  61. package/src/components/ui/tu-do/my-tasks/use-task-context-actions.ts +33 -15
  62. package/src/components/ui/tu-do/progress/task-progress-leaderboards-panel.tsx +3 -3
  63. package/src/components/ui/tu-do/progress/task-progress-page.tsx +60 -26
  64. package/src/components/ui/tu-do/progress/task-progress-panels.tsx +12 -10
  65. package/src/components/ui/tu-do/projects/components/project-grid-card.tsx +22 -6
  66. package/src/components/ui/tu-do/projects/components/project-list-item.tsx +24 -8
  67. package/src/components/ui/tu-do/projects/projectId/components/__tests__/tasks-tab-layout.test.tsx +6 -0
  68. package/src/components/ui/tu-do/projects/projectId/components/tasks-tab.tsx +3 -1
  69. package/src/components/ui/tu-do/projects/projectId/task-project-detail-page-client.tsx +11 -3
  70. package/src/components/ui/tu-do/projects/projectId/task-project-detail.tsx +2 -0
  71. package/src/components/ui/tu-do/projects/projectId/types.ts +1 -0
  72. package/src/components/ui/tu-do/projects/task-projects-client.tsx +45 -1
  73. package/src/components/ui/tu-do/projects/types.ts +4 -1
  74. package/src/components/ui/tu-do/providers/task-dialog-provider.tsx +10 -0
  75. package/src/components/ui/tu-do/shared/__tests__/board-client.test.tsx +7 -1
  76. package/src/components/ui/tu-do/shared/__tests__/board-header.test.tsx +87 -2
  77. package/src/components/ui/tu-do/shared/__tests__/task-board-loading-state.test.tsx +27 -3
  78. package/src/components/ui/tu-do/shared/__tests__/task-dialog-manager.test.tsx +41 -3
  79. package/src/components/ui/tu-do/shared/__tests__/use-progressive-board-loader.test.tsx +54 -0
  80. package/src/components/ui/tu-do/shared/board-header.tsx +20 -0
  81. package/src/components/ui/tu-do/shared/task-board-loading-state.tsx +4 -4
  82. package/src/components/ui/tu-do/shared/task-dialog-manager.tsx +2 -0
  83. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-dialog-header.tsx +7 -0
  84. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/__tests__/use-task-mutations.test.tsx +231 -0
  85. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-mutations.ts +139 -68
  86. package/src/components/ui/tu-do/shared/task-edit-dialog/task-dialog-actions.test.tsx +68 -0
  87. package/src/components/ui/tu-do/shared/task-edit-dialog/task-dialog-actions.tsx +17 -0
  88. package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +45 -23
  89. package/src/components/ui/tu-do/shared/task-row-actions-menu.test.tsx +138 -0
  90. package/src/components/ui/tu-do/shared/task-row-actions-menu.tsx +15 -9
  91. package/src/components/ui/tu-do/templates/client.tsx +97 -78
  92. package/src/components/ui/tu-do/templates/marketplace/client.tsx +171 -135
  93. package/src/components/ui/tu-do/templates/task-templates-hub.tsx +3 -0
  94. package/src/components/ui/tu-do/templates/templateId/client.tsx +24 -9
  95. package/src/components/ui/tu-do/templates/templateId/task-template-detail-page-client.tsx +8 -2
  96. package/src/globals.css +1 -0
  97. package/src/hooks/__tests__/use-notifications-subscription.test.tsx +34 -0
  98. package/src/hooks/__tests__/use-task-actions.test.tsx +14 -39
  99. package/src/hooks/task-actions-personal-external.ts +12 -6
  100. package/src/hooks/use-notifications.ts +14 -2
  101. package/vitest.config.ts +4 -0
@@ -8,13 +8,12 @@ import { RadioGroup, RadioGroupItem } from '@tuturuuu/ui/radio-group';
8
8
  import { Separator } from '@tuturuuu/ui/separator';
9
9
  import { Switch } from '@tuturuuu/ui/switch';
10
10
  import { useTranslations } from 'next-intl';
11
+ import type { SidebarBehavior } from '../sidebar-context';
11
12
 
12
13
  interface SidebarSettingsProps {
13
14
  useSidebar: () => {
14
- behavior: 'expanded' | 'collapsed' | 'hover';
15
- handleBehaviorChange: (
16
- behavior: 'expanded' | 'collapsed' | 'hover'
17
- ) => void;
15
+ behavior: SidebarBehavior;
16
+ handleBehaviorChange: (behavior: SidebarBehavior) => void;
18
17
  localOverride: boolean;
19
18
  setLocalOverride: (value: boolean) => void;
20
19
  };
@@ -45,9 +44,9 @@ export default function SidebarSettings({ useSidebar }: SidebarSettingsProps) {
45
44
  <RadioGroup
46
45
  value={behavior}
47
46
  onValueChange={(value) =>
48
- handleBehaviorChange(value as 'expanded' | 'collapsed' | 'hover')
47
+ handleBehaviorChange(value as SidebarBehavior)
49
48
  }
50
- className="grid grid-cols-3 gap-4"
49
+ className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4"
51
50
  >
52
51
  <div>
53
52
  <RadioGroupItem
@@ -84,6 +83,19 @@ export default function SidebarSettings({ useSidebar }: SidebarSettingsProps) {
84
83
  {t('settings.preferences.sidebar_hover')}
85
84
  </Label>
86
85
  </div>
86
+ <div>
87
+ <RadioGroupItem
88
+ value="hidden"
89
+ id="hidden"
90
+ className="peer sr-only"
91
+ />
92
+ <Label
93
+ htmlFor="hidden"
94
+ className="flex cursor-pointer flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary"
95
+ >
96
+ {t('settings.preferences.sidebar_hidden')}
97
+ </Label>
98
+ </div>
87
99
  </RadioGroup>
88
100
  </SettingItemTab>
89
101
 
@@ -64,6 +64,7 @@ export interface SettingsNavItem {
64
64
  label: string;
65
65
  icon: ComponentType<{ className?: string }>;
66
66
  description?: string;
67
+ hideContentHeader?: boolean;
67
68
  disabled?: boolean;
68
69
  keywords?: string[];
69
70
  aliases?: string[];
@@ -157,6 +158,7 @@ export function SettingsDialogShell({
157
158
  allNavItems.find((item) => item.name === activeTab) ||
158
159
  allNavItems.find((item) => !item.disabled) ||
159
160
  allNavItems[0];
161
+ const showContentHeader = !activeItem?.hideContentHeader;
160
162
 
161
163
  const ensureSearchEngine = useCallback(() => {
162
164
  if (searchEngineFactory || searchEngineLoadRef.current) return;
@@ -511,18 +513,22 @@ export function SettingsDialogShell({
511
513
  </header>
512
514
  <div className="flex flex-1 flex-col gap-4 overflow-y-auto p-6">
513
515
  <div className="mx-auto w-full max-w-4xl space-y-6">
514
- <div className="space-y-1">
515
- <h2 className="font-semibold text-lg tracking-tight">
516
- {activeItem?.label}
517
- </h2>
518
- <p className="text-muted-foreground text-sm">
519
- {activeItem?.description ||
520
- t('settings.manage_settings', {
521
- label: activeItem?.label?.toLowerCase() ?? '',
522
- })}
523
- </p>
524
- </div>
525
- <Separator />
516
+ {showContentHeader && (
517
+ <>
518
+ <div className="space-y-1">
519
+ <h2 className="font-semibold text-lg tracking-tight">
520
+ {activeItem?.label}
521
+ </h2>
522
+ <p className="text-muted-foreground text-sm">
523
+ {activeItem?.description ||
524
+ t('settings.manage_settings', {
525
+ label: activeItem?.label?.toLowerCase() ?? '',
526
+ })}
527
+ </p>
528
+ </div>
529
+ <Separator />
530
+ </>
531
+ )}
526
532
  {children}
527
533
  </div>
528
534
  </div>
@@ -1,6 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import { useLocalStorage } from '@tuturuuu/ui/hooks/use-local-storage';
4
+ import { getTuturuuuBrowserSharedCookieOptions } from '@tuturuuu/utils/shared-cookie';
4
5
  import { setCookie } from 'cookies-next';
5
6
  import {
6
7
  type ComponentType,
@@ -19,7 +20,7 @@ export const SIDEBAR_BEHAVIOR_UPDATED_AT_COOKIE_NAME =
19
20
  'sidebar-behavior-updated-at';
20
21
  export const SIDEBAR_BEHAVIOR_CONFIG_KEY = 'SIDEBAR_BEHAVIOR';
21
22
 
22
- export type SidebarBehavior = 'expanded' | 'collapsed' | 'hover';
23
+ export type SidebarBehavior = 'expanded' | 'collapsed' | 'hover' | 'hidden';
23
24
 
24
25
  interface SidebarContextProps {
25
26
  behavior: SidebarBehavior;
@@ -40,6 +41,10 @@ export const SIDEBAR_COOKIE_OPTIONS = {
40
41
  path: '/',
41
42
  } as const;
42
43
 
44
+ export function getSidebarCookieOptions() {
45
+ return getTuturuuuBrowserSharedCookieOptions(SIDEBAR_COOKIE_OPTIONS);
46
+ }
47
+
43
48
  function parseSidebarBehaviorUpdatedAt(value: string | undefined | null) {
44
49
  if (!value) return null;
45
50
 
@@ -73,6 +78,19 @@ type SidebarRemoteBehaviorBridgeComponent = ComponentType<{
73
78
  userChangeVersion: number;
74
79
  }>;
75
80
 
81
+ function isEditableShortcutTarget(target: EventTarget | null) {
82
+ if (!(target instanceof HTMLElement)) return false;
83
+
84
+ if (target.isContentEditable) return true;
85
+
86
+ const tagName = target.tagName.toLowerCase();
87
+ if (tagName === 'input' || tagName === 'textarea' || tagName === 'select') {
88
+ return true;
89
+ }
90
+
91
+ return Boolean(target.closest('[contenteditable="true"], [role="textbox"]'));
92
+ }
93
+
76
94
  function useSidebarRemoteBehaviorBridge() {
77
95
  const [RemoteBehaviorBridge, setRemoteBehaviorBridge] =
78
96
  useState<SidebarRemoteBehaviorBridgeComponent | null>(null);
@@ -136,7 +154,7 @@ export const SidebarProvider = ({
136
154
  setCookie(
137
155
  SIDEBAR_BEHAVIOR_COOKIE_NAME,
138
156
  newBehavior,
139
- SIDEBAR_COOKIE_OPTIONS
157
+ getSidebarCookieOptions()
140
158
  );
141
159
 
142
160
  if (!options.markLocalChange) return;
@@ -146,7 +164,7 @@ export const SidebarProvider = ({
146
164
  setCookie(
147
165
  SIDEBAR_BEHAVIOR_UPDATED_AT_COOKIE_NAME,
148
166
  String(updatedAt),
149
- SIDEBAR_COOKIE_OPTIONS
167
+ getSidebarCookieOptions()
150
168
  );
151
169
  },
152
170
  []
@@ -175,6 +193,41 @@ export const SidebarProvider = ({
175
193
  [applyBehavior, remoteBehavior, setLocalOverrideRaw]
176
194
  );
177
195
 
196
+ useEffect(() => {
197
+ const handleKeyDown = (event: KeyboardEvent) => {
198
+ if (
199
+ event.defaultPrevented ||
200
+ event.key.toLowerCase() !== 'b' ||
201
+ !(event.metaKey || event.ctrlKey) ||
202
+ event.shiftKey ||
203
+ isEditableShortcutTarget(event.target)
204
+ ) {
205
+ return;
206
+ }
207
+
208
+ if (event.altKey) {
209
+ event.preventDefault();
210
+ handleBehaviorChange('hidden');
211
+ return;
212
+ }
213
+
214
+ event.preventDefault();
215
+ handleBehaviorChange(
216
+ behavior === 'expanded'
217
+ ? 'collapsed'
218
+ : behavior === 'hidden'
219
+ ? 'collapsed'
220
+ : 'expanded'
221
+ );
222
+ };
223
+
224
+ window.addEventListener('keydown', handleKeyDown);
225
+
226
+ return () => {
227
+ window.removeEventListener('keydown', handleKeyDown);
228
+ };
229
+ }, [behavior, handleBehaviorChange]);
230
+
178
231
  return (
179
232
  <SidebarContext.Provider
180
233
  value={{
@@ -208,3 +261,5 @@ export const useSidebar = () => {
208
261
  }
209
262
  return context;
210
263
  };
264
+
265
+ export const useOptionalSidebar = () => useContext(SidebarContext);
@@ -5,14 +5,16 @@ import {
5
5
  useUserConfig,
6
6
  } from '@tuturuuu/ui/hooks/use-user-config';
7
7
  import { useEffect, useRef } from 'react';
8
-
9
- type SidebarBehavior = 'expanded' | 'collapsed' | 'hover';
8
+ import type { SidebarBehavior } from './sidebar-context';
10
9
 
11
10
  const SIDEBAR_BEHAVIOR_CONFIG_KEY = 'SIDEBAR_BEHAVIOR';
12
11
  const RECENT_LOCAL_BEHAVIOR_GRACE_MS = 5 * 60 * 1000;
13
12
 
14
13
  const isValidBehavior = (value: string | undefined): value is SidebarBehavior =>
15
- value === 'expanded' || value === 'collapsed' || value === 'hover';
14
+ value === 'expanded' ||
15
+ value === 'collapsed' ||
16
+ value === 'hover' ||
17
+ value === 'hidden';
16
18
 
17
19
  interface SidebarRemoteBehaviorBridgeProps {
18
20
  behavior: SidebarBehavior;
@@ -0,0 +1,104 @@
1
+ import { Progress } from '@tuturuuu/ui/progress';
2
+ import Link from 'next/link';
3
+
4
+ interface Props {
5
+ title?: string;
6
+ value?: string | number | null;
7
+ limit?: number;
8
+ displayValue?: React.ReactNode;
9
+ displayLimit?: React.ReactNode;
10
+ progress?: number;
11
+ href?: string;
12
+ className?: string;
13
+ onClick?: () => void;
14
+ }
15
+
16
+ const StatisticCard = ({
17
+ title,
18
+ value,
19
+ limit,
20
+ displayValue,
21
+ displayLimit,
22
+ progress,
23
+ href,
24
+ className,
25
+ onClick,
26
+ }: Props) => {
27
+ const generateOuterColor = (enableHoverEffect: boolean) =>
28
+ `${
29
+ enableHoverEffect
30
+ ? 'border-border hover:bg-foreground/[0.025] dark:hover:bg-foreground/5'
31
+ : 'border-border/50'
32
+ }`;
33
+
34
+ const progressValue =
35
+ progress !== undefined
36
+ ? progress
37
+ : typeof value === 'number' && limit
38
+ ? (value / limit) * 100
39
+ : 0;
40
+
41
+ const progressColor =
42
+ progressValue >= 100
43
+ ? 'bg-dynamic-red'
44
+ : progressValue >= 90
45
+ ? 'bg-dynamic-yellow'
46
+ : '';
47
+
48
+ const cardContent = (
49
+ <div className="flex h-full flex-col justify-between">
50
+ <div>
51
+ <div className="line-clamp-1 p-1 text-center font-semibold text-lg">
52
+ {title}
53
+ </div>
54
+ <div
55
+ className={`m-2 mt-0 line-clamp-1 rounded border border-border/30 bg-foreground/5 p-4 text-center font-bold text-2xl text-foreground ${
56
+ onClick || href
57
+ ? 'transition-all duration-300 group-hover:rounded-lg'
58
+ : ''
59
+ }`}
60
+ >
61
+ {displayValue ?? (value != null ? value : 'N/A')}
62
+ {(displayLimit || limit) && (
63
+ <span className="font-normal text-base text-muted-foreground">
64
+ {' '}
65
+ / {displayLimit ?? limit}
66
+ </span>
67
+ )}
68
+ </div>
69
+ </div>
70
+ {(limit || displayLimit || progress !== undefined) && (
71
+ <div className="px-4 pb-4">
72
+ <Progress value={progressValue} indicatorClassName={progressColor} />
73
+ </div>
74
+ )}
75
+ </div>
76
+ );
77
+
78
+ if (href)
79
+ return (
80
+ <Link
81
+ href={href}
82
+ onClick={onClick}
83
+ className={`group flex flex-col rounded-lg border transition-all duration-300 ${
84
+ onClick || href ? 'hover:rounded-xl' : 'cursor-default'
85
+ } ${generateOuterColor(!!onClick || !!href)} ${className || ''}`}
86
+ >
87
+ {cardContent}
88
+ </Link>
89
+ );
90
+
91
+ return (
92
+ <button
93
+ type="button"
94
+ onClick={onClick}
95
+ className={`group flex flex-col rounded-lg border transition duration-300 ${
96
+ onClick || href ? 'hover:rounded-xl' : 'cursor-default'
97
+ } ${generateOuterColor(!!onClick || !!href)} ${className || ''}`}
98
+ >
99
+ {cardContent}
100
+ </button>
101
+ );
102
+ };
103
+
104
+ export default StatisticCard;
@@ -0,0 +1,29 @@
1
+ import '@testing-library/jest-dom';
2
+ import { render, screen } from '@testing-library/react';
3
+ import { describe, expect, it, vi } from 'vitest';
4
+ import { Structure } from './structure';
5
+
6
+ describe('Structure', () => {
7
+ it('removes sidebar chrome and desktop padding when the sidebar is hidden', () => {
8
+ const { container } = render(
9
+ <Structure
10
+ isCollapsed
11
+ setIsCollapsed={vi.fn()}
12
+ mobileHeader={<span>Mobile header</span>}
13
+ sidebarContent={<span>Sidebar content</span>}
14
+ sidebarHeader={<span>Sidebar header</span>}
15
+ sidebarHidden
16
+ >
17
+ <span>Page content</span>
18
+ </Structure>
19
+ );
20
+
21
+ expect(screen.getByText('Page content')).toBeInTheDocument();
22
+ expect(screen.queryByText('Mobile header')).not.toBeInTheDocument();
23
+ expect(screen.queryByText('Sidebar header')).not.toBeInTheDocument();
24
+ expect(screen.queryByText('Sidebar content')).not.toBeInTheDocument();
25
+ expect(container.querySelector('aside')).not.toBeInTheDocument();
26
+ expect(container.querySelector('nav')).not.toBeInTheDocument();
27
+ expect(container.querySelector('main')).toHaveClass('md:pl-0');
28
+ });
29
+ });
@@ -21,6 +21,7 @@ interface StructureProps {
21
21
  children: ReactNode;
22
22
  onMouseEnter?: () => void;
23
23
  onMouseLeave?: () => void;
24
+ sidebarHidden?: boolean;
24
25
  sidebarCollapsedWidth?: string;
25
26
  sidebarExpandedWidth?: string;
26
27
  sidebarHeaderClassName?: string;
@@ -42,6 +43,7 @@ export function Structure({
42
43
  children,
43
44
  onMouseEnter,
44
45
  onMouseLeave,
46
+ sidebarHidden = false,
45
47
  sidebarCollapsedWidth = '4rem',
46
48
  sidebarExpandedWidth = '16rem',
47
49
  sidebarHeaderClassName,
@@ -60,7 +62,7 @@ export function Structure({
60
62
  if (typeof window === 'undefined') return;
61
63
 
62
64
  const isMobile = window.innerWidth < 768; // md breakpoint
63
- if (isMobile && !isCollapsed) {
65
+ if (isMobile && !sidebarHidden && !isCollapsed) {
64
66
  const scrollY = window.scrollY;
65
67
  document.body.style.position = 'fixed';
66
68
  document.body.style.top = `-${scrollY}px`;
@@ -73,31 +75,35 @@ export function Structure({
73
75
  window.scrollTo(0, scrollY);
74
76
  };
75
77
  }
76
- }, [isCollapsed]);
78
+ }, [isCollapsed, sidebarHidden]);
77
79
 
78
80
  return (
79
81
  <>
80
- <nav className="safe-top safe-x fixed inset-x-0 top-0 z-30 max-sm:border-b md:hidden">
81
- <div className="bg-background/50 p-2 font-semibold backdrop-blur-md md:px-8 lg:px-16 xl:px-32">
82
- <div className="relative flex items-center justify-between gap-2 md:gap-4">
83
- <div className="flex w-full items-center gap-2">{mobileHeader}</div>
84
- <div className="flex w-fit items-center gap-2">
85
- {userPopover}
86
- <Button
87
- size="icon"
88
- variant="outline"
89
- className="h-auto w-auto flex-none rounded-lg p-2 md:hidden"
90
- onClick={() => setIsCollapsed(!isCollapsed)}
91
- >
92
- <Menu className="h-5 w-5" />
93
- </Button>
82
+ {!sidebarHidden && (
83
+ <nav className="safe-top safe-x fixed inset-x-0 top-0 z-30 max-sm:border-b md:hidden">
84
+ <div className="bg-background/50 p-2 font-semibold backdrop-blur-md md:px-8 lg:px-16 xl:px-32">
85
+ <div className="relative flex items-center justify-between gap-2 md:gap-4">
86
+ <div className="flex w-full items-center gap-2">
87
+ {mobileHeader}
88
+ </div>
89
+ <div className="flex w-fit items-center gap-2">
90
+ {userPopover}
91
+ <Button
92
+ size="icon"
93
+ variant="outline"
94
+ className="h-auto w-auto flex-none rounded-lg p-2 md:hidden"
95
+ onClick={() => setIsCollapsed(!isCollapsed)}
96
+ >
97
+ <Menu className="h-5 w-5" />
98
+ </Button>
99
+ </div>
94
100
  </div>
95
101
  </div>
96
- </div>
97
- </nav>
102
+ </nav>
103
+ )}
98
104
 
99
105
  <TooltipProvider delayDuration={0}>
100
- {!isCollapsed && (
106
+ {!sidebarHidden && !isCollapsed && (
101
107
  <button
102
108
  type="button"
103
109
  className="fixed inset-0 z-40 bg-black/20 backdrop-blur-lg md:hidden"
@@ -105,104 +111,113 @@ export function Structure({
105
111
  />
106
112
  )}
107
113
  <div className="relative w-full">
108
- <aside
109
- className={cn(
110
- 'group fixed top-0 right-0 left-auto z-50 flex h-dvh flex-col overflow-hidden border-l backdrop-blur-lg transition-all duration-300 ease-in-out md:left-0 md:z-20 md:border-r md:border-l-0',
111
- isCollapsed
112
- ? 'w-[var(--sidebar-collapsed-width)] bg-background/50 max-md:w-0'
113
- : 'w-[var(--sidebar-expanded-width)] bg-background max-sm:w-full',
114
- isCollapsed && 'max-md:translate-x-full',
115
- overlayOnExpand && !isCollapsed && 'md:z-40'
116
- )}
117
- onMouseEnter={onMouseEnter}
118
- onMouseLeave={onMouseLeave}
119
- style={sidebarStyle}
120
- >
121
- <div
114
+ {!sidebarHidden && (
115
+ <aside
122
116
  className={cn(
123
- sidebarHeaderHeight
124
- ? 'flex h-[var(--sidebar-header-height)] shrink-0 items-center p-2 md:px-0'
125
- : 'flex min-h-12 items-center p-2 md:px-0',
126
- isCollapsed ? 'justify-center' : 'py-1',
127
- sidebarHeaderClassName
117
+ 'group fixed top-0 right-0 left-auto z-50 flex h-dvh flex-col overflow-hidden border-l backdrop-blur-lg transition-all duration-300 ease-in-out md:left-0 md:z-20 md:border-r md:border-l-0',
118
+ isCollapsed
119
+ ? 'w-[var(--sidebar-collapsed-width)] bg-background/50 max-md:w-0'
120
+ : 'w-[var(--sidebar-expanded-width)] bg-background max-sm:w-full',
121
+ isCollapsed && 'max-md:translate-x-full',
122
+ overlayOnExpand && !isCollapsed && 'md:z-40'
128
123
  )}
124
+ onMouseEnter={onMouseEnter}
125
+ onMouseLeave={onMouseLeave}
126
+ style={sidebarStyle}
129
127
  >
130
128
  <div
131
129
  className={cn(
132
- 'flex min-h-13 w-full items-center md:px-2',
133
- isCollapsed ? 'justify-center' : ''
130
+ sidebarHeaderHeight
131
+ ? 'flex h-[var(--sidebar-header-height)] shrink-0 items-center p-2 md:px-0'
132
+ : 'flex min-h-12 items-center p-2 md:px-0',
133
+ isCollapsed ? 'justify-center' : 'py-1',
134
+ sidebarHeaderClassName
134
135
  )}
135
136
  >
136
137
  <div
137
138
  className={cn(
138
- 'flex w-full items-center gap-2',
139
- isCollapsed ? 'justify-center' : 'justify-between'
139
+ 'flex min-h-13 w-full items-center md:px-2',
140
+ isCollapsed ? 'justify-center' : ''
140
141
  )}
141
142
  >
142
- {sidebarHeader}
143
- {isCollapsed || (
144
- <Button
145
- size="icon"
146
- variant="outline"
147
- className="h-auto w-auto flex-none rounded-lg p-2 md:hidden"
148
- onClick={() => setIsCollapsed(!isCollapsed)}
149
- >
150
- <X className="h-5 w-5" />
151
- </Button>
152
- )}
143
+ <div
144
+ className={cn(
145
+ 'flex w-full items-center gap-2',
146
+ isCollapsed ? 'justify-center' : 'justify-between'
147
+ )}
148
+ >
149
+ {sidebarHeader}
150
+ {isCollapsed || (
151
+ <Button
152
+ size="icon"
153
+ variant="outline"
154
+ className="h-auto w-auto flex-none rounded-lg p-2 md:hidden"
155
+ onClick={() => setIsCollapsed(!isCollapsed)}
156
+ >
157
+ <X className="h-5 w-5" />
158
+ </Button>
159
+ )}
160
+ </div>
153
161
  </div>
154
162
  </div>
155
- </div>
156
- <div className="scrollbar-none flex flex-1 flex-col gap-y-1 overflow-y-auto overflow-x-hidden overscroll-contain">
157
- {sidebarContent}
158
- </div>
159
- {feedbackButton && (
163
+ <div className="scrollbar-none flex flex-1 flex-col gap-y-1 overflow-y-auto overflow-x-hidden overscroll-contain">
164
+ {sidebarContent}
165
+ </div>
166
+ {feedbackButton && (
167
+ <div
168
+ className={cn(
169
+ 'flex border-foreground/10 border-t p-2',
170
+ isCollapsed ? 'justify-center' : ''
171
+ )}
172
+ >
173
+ {feedbackButton}
174
+ </div>
175
+ )}
160
176
  <div
161
177
  className={cn(
162
- 'flex border-foreground/10 border-t p-2',
178
+ 'mt-auto flex border-foreground/10 border-t p-2',
163
179
  isCollapsed ? 'justify-center' : ''
164
180
  )}
165
181
  >
166
- {feedbackButton}
182
+ {isCollapsed ? userPopover : actions}
167
183
  </div>
168
- )}
169
- <div
170
- className={cn(
171
- 'mt-auto flex border-foreground/10 border-t p-2',
172
- isCollapsed ? 'justify-center' : ''
173
- )}
174
- >
175
- {isCollapsed ? userPopover : actions}
176
- </div>
177
184
 
178
- {!hideSizeToggle && (
179
- <Button
180
- size="icon"
181
- variant="outline"
182
- className="absolute top-1/2 -right-4 z-10 hidden h-auto w-auto -translate-y-1/2 rounded-full border-2 bg-background p-1.5 pl-1.5 opacity-0 transition duration-500 hover:bg-accent group-hover:opacity-100 md:block"
183
- onClick={() => setIsCollapsed(!isCollapsed)}
184
- >
185
- {isCollapsed ? (
186
- <ChevronRight className="mr-2 h-4 w-4" />
187
- ) : (
188
- <ChevronLeft className="mr-2 h-4 w-4" />
189
- )}
190
- </Button>
191
- )}
192
- </aside>
185
+ {!hideSizeToggle && (
186
+ <Button
187
+ size="icon"
188
+ variant="outline"
189
+ className="absolute top-1/2 -right-4 z-10 hidden h-auto w-auto -translate-y-1/2 rounded-full border-2 bg-background p-1.5 pl-1.5 opacity-0 transition duration-500 hover:bg-accent group-hover:opacity-100 md:block"
190
+ onClick={() => setIsCollapsed(!isCollapsed)}
191
+ >
192
+ {isCollapsed ? (
193
+ <ChevronRight className="mr-2 h-4 w-4" />
194
+ ) : (
195
+ <ChevronLeft className="mr-2 h-4 w-4" />
196
+ )}
197
+ </Button>
198
+ )}
199
+ </aside>
200
+ )}
193
201
  {/* Main content area - overflow-y-auto removed to prevent double scrollbars */}
194
202
  {/* Body element now handles page-level scrolling */}
195
203
  <main
196
204
  className={cn(
197
205
  'relative flex h-full min-h-screen flex-col overflow-x-clip overflow-y-visible transition-all duration-300 ease-in-out',
198
- isCollapsed || overlayOnExpand
199
- ? 'md:pl-[var(--sidebar-collapsed-width)]'
200
- : 'md:pl-[var(--sidebar-expanded-width)]'
206
+ sidebarHidden
207
+ ? 'md:pl-0'
208
+ : isCollapsed || overlayOnExpand
209
+ ? 'md:pl-[var(--sidebar-collapsed-width)]'
210
+ : 'md:pl-[var(--sidebar-expanded-width)]'
201
211
  )}
202
212
  style={sidebarStyle}
203
213
  >
204
214
  {header && <div className="mb-4 hidden md:block">{header}</div>}
205
- <div className="safe-bottom relative h-full w-full p-2 pt-17 pl-2 md:p-4 md:pt-4">
215
+ <div
216
+ className={cn(
217
+ 'safe-bottom relative h-full w-full p-2 md:p-4 md:pt-4',
218
+ sidebarHidden ? 'pt-2' : 'pt-17 pl-2'
219
+ )}
220
+ >
206
221
  {children}
207
222
  </div>
208
223
  </main>