@tuturuuu/ui 0.13.0 → 0.16.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.
- package/CHANGELOG.md +90 -0
- package/biome.json +1 -1
- package/package.json +62 -62
- package/src/components/ui/custom/__tests__/settings-dialog-shell.test.tsx +12 -0
- package/src/components/ui/custom/__tests__/sidebar-context.test.tsx +93 -6
- package/src/components/ui/custom/loading-statistic-card.tsx +18 -0
- package/src/components/ui/custom/notification-popover-client.tsx +5 -1
- package/src/components/ui/custom/settings/keyboard-shortcuts-settings.test.tsx +34 -0
- package/src/components/ui/custom/settings/keyboard-shortcuts-settings.tsx +106 -0
- package/src/components/ui/custom/settings/sidebar-settings.tsx +18 -6
- package/src/components/ui/custom/settings/task-settings.tsx +55 -0
- package/src/components/ui/custom/settings-dialog-shell.tsx +18 -12
- package/src/components/ui/custom/sidebar-context.tsx +58 -3
- package/src/components/ui/custom/sidebar-remote-behavior-bridge.tsx +5 -3
- package/src/components/ui/custom/statistic-card.tsx +104 -0
- package/src/components/ui/custom/structure.test.tsx +29 -0
- package/src/components/ui/custom/structure.tsx +103 -88
- package/src/components/ui/finance/invoices/civil-month.ts +224 -0
- package/src/components/ui/finance/invoices/components/subscription-invoice-proposal.test.tsx +49 -0
- package/src/components/ui/finance/invoices/components/subscription-invoice-proposal.tsx +81 -0
- package/src/components/ui/finance/invoices/hooks/use-subscription-auto-selection.ts +8 -2
- package/src/components/ui/finance/invoices/new-invoice-page.test.tsx +90 -2
- package/src/components/ui/finance/invoices/new-invoice-page.tsx +40 -4
- package/src/components/ui/finance/invoices/pending-columns.test.ts +26 -0
- package/src/components/ui/finance/invoices/pending-columns.tsx +45 -14
- package/src/components/ui/finance/invoices/standard-invoice.test.ts +31 -0
- package/src/components/ui/finance/invoices/standard-invoice.tsx +33 -16
- package/src/components/ui/finance/invoices/subscription-invoice.test.tsx +331 -0
- package/src/components/ui/finance/invoices/subscription-invoice.tsx +53 -28
- package/src/components/ui/finance/invoices/utils.test.ts +84 -0
- package/src/components/ui/finance/invoices/utils.ts +56 -134
- package/src/components/ui/resizable.test.ts +15 -0
- package/src/components/ui/resizable.tsx +28 -6
- package/src/components/ui/tu-do/boards/boardId/board-column.tsx +4 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/__tests__/bulk-mutations-move.test.tsx +62 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/auto-scroll.test.ts +118 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/auto-scroll.ts +144 -70
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.test.ts +48 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.ts +50 -6
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.tsx +6 -0
- package/src/components/ui/tu-do/boards/boardId/kanban.tsx +4 -2
- package/src/components/ui/tu-do/boards/boardId/menus/__tests__/task-menus.test.tsx +41 -3
- package/src/components/ui/tu-do/boards/boardId/menus/task-due-date-menu.tsx +31 -10
- package/src/components/ui/tu-do/boards/boardId/menus/task-priority-menu.tsx +7 -5
- package/src/components/ui/tu-do/boards/boardId/menus/task-scheduling-menu.tsx +7 -5
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-archive-visibility.test.ts +44 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-archive-visibility.ts +17 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.test.ts +2 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.ts +7 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +44 -30
- package/src/components/ui/tu-do/my-tasks/__tests__/use-task-context-actions.test.ts +54 -0
- package/src/components/ui/tu-do/my-tasks/task-list-with-completion.tsx +43 -1
- package/src/components/ui/tu-do/my-tasks/use-task-context-actions.ts +33 -15
- package/src/components/ui/tu-do/progress/task-progress-leaderboards-panel.tsx +3 -3
- package/src/components/ui/tu-do/progress/task-progress-page.tsx +74 -28
- package/src/components/ui/tu-do/progress/task-progress-panels.tsx +12 -10
- package/src/components/ui/tu-do/projects/components/project-grid-card.tsx +22 -6
- package/src/components/ui/tu-do/projects/components/project-list-item.tsx +24 -8
- package/src/components/ui/tu-do/projects/projectId/components/__tests__/tasks-tab-layout.test.tsx +6 -0
- package/src/components/ui/tu-do/projects/projectId/components/tasks-tab.tsx +3 -1
- package/src/components/ui/tu-do/projects/projectId/task-project-detail-page-client.tsx +11 -3
- package/src/components/ui/tu-do/projects/projectId/task-project-detail.tsx +2 -0
- package/src/components/ui/tu-do/projects/projectId/types.ts +1 -0
- package/src/components/ui/tu-do/projects/task-projects-client.tsx +45 -1
- package/src/components/ui/tu-do/projects/types.ts +4 -1
- package/src/components/ui/tu-do/providers/task-dialog-provider.tsx +10 -0
- package/src/components/ui/tu-do/shared/__tests__/board-client.test.tsx +7 -1
- package/src/components/ui/tu-do/shared/__tests__/board-header.test.tsx +87 -2
- package/src/components/ui/tu-do/shared/__tests__/board-views.test.tsx +70 -0
- package/src/components/ui/tu-do/shared/__tests__/task-board-loading-state.test.tsx +27 -3
- package/src/components/ui/tu-do/shared/__tests__/task-dialog-manager.test.tsx +41 -3
- package/src/components/ui/tu-do/shared/__tests__/use-progressive-board-loader.test.tsx +54 -0
- package/src/components/ui/tu-do/shared/board-header.tsx +20 -0
- package/src/components/ui/tu-do/shared/board-views.tsx +33 -3
- package/src/components/ui/tu-do/shared/task-board-loading-state.tsx +4 -4
- package/src/components/ui/tu-do/shared/task-dialog-manager.tsx +2 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-dialog-header.tsx +7 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/__tests__/use-task-mutations.test.tsx +231 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-mutations.ts +139 -68
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-dialog-actions.test.tsx +68 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-dialog-actions.tsx +17 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +45 -23
- package/src/components/ui/tu-do/shared/task-quick-create-target-list.ts +20 -0
- package/src/components/ui/tu-do/shared/task-row-actions-menu.test.tsx +138 -0
- package/src/components/ui/tu-do/shared/task-row-actions-menu.tsx +15 -9
- package/src/components/ui/tu-do/templates/client.tsx +97 -78
- package/src/components/ui/tu-do/templates/marketplace/client.tsx +171 -135
- package/src/components/ui/tu-do/templates/task-templates-hub.tsx +3 -0
- package/src/components/ui/tu-do/templates/templateId/client.tsx +24 -9
- package/src/components/ui/tu-do/templates/templateId/task-template-detail-page-client.tsx +8 -2
- package/src/hooks/__tests__/use-notifications-subscription.test.tsx +34 -0
- package/src/hooks/__tests__/use-task-actions.test.tsx +14 -39
- package/src/hooks/task-actions-personal-external.ts +12 -6
- package/src/hooks/use-notifications.ts +14 -2
- package/src/hooks/use-user-config.ts +10 -2
- 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:
|
|
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
|
|
47
|
+
handleBehaviorChange(value as SidebarBehavior)
|
|
49
48
|
}
|
|
50
|
-
className="grid grid-cols-
|
|
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
|
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
TASK_NAVIGATION_LEADERBOARDS_CONFIG_ID,
|
|
8
8
|
TASK_NAVIGATION_PROGRESS_CONFIG_ID,
|
|
9
9
|
TASK_NAVIGATION_STATS_CONFIG_ID,
|
|
10
|
+
TASK_QUICK_CREATE_TARGET_LIST_CONFIG_ID,
|
|
10
11
|
} from '@tuturuuu/internal-api/users';
|
|
11
12
|
import type { Workspace } from '@tuturuuu/types';
|
|
12
13
|
import { SettingItemTab } from '@tuturuuu/ui/custom/settings-item-tab';
|
|
@@ -34,6 +35,11 @@ import {
|
|
|
34
35
|
type TaskDialogPresentation,
|
|
35
36
|
} from '../../tu-do/shared/task-dialog-presentation';
|
|
36
37
|
import { TASKS_SHOW_REVIEW_DUE_DATES_CONFIG_ID } from '../../tu-do/shared/task-due-date-visibility';
|
|
38
|
+
import {
|
|
39
|
+
DEFAULT_TASK_QUICK_CREATE_TARGET_LIST,
|
|
40
|
+
normalizeTaskQuickCreateTargetList,
|
|
41
|
+
type TaskQuickCreateTargetList,
|
|
42
|
+
} from '../../tu-do/shared/task-quick-create-target-list';
|
|
37
43
|
import {
|
|
38
44
|
clampTaskSoundEffectsVolume,
|
|
39
45
|
DEFAULT_TASK_SOUND_EFFECTS_VOLUME,
|
|
@@ -142,6 +148,14 @@ export function TaskSettings({ workspace }: TaskSettingsProps) {
|
|
|
142
148
|
const { data: dialogPresentationRaw, isLoading: dialogPresentationLoading } =
|
|
143
149
|
useUserConfig(TASK_DIALOG_DEFAULT_PRESENTATION_CONFIG_ID, 'compact');
|
|
144
150
|
const updateDialogPresentation = useUpdateUserConfig();
|
|
151
|
+
const {
|
|
152
|
+
data: quickCreateTargetListRaw,
|
|
153
|
+
isLoading: quickCreateTargetListLoading,
|
|
154
|
+
} = useUserConfig(
|
|
155
|
+
TASK_QUICK_CREATE_TARGET_LIST_CONFIG_ID,
|
|
156
|
+
DEFAULT_TASK_QUICK_CREATE_TARGET_LIST
|
|
157
|
+
);
|
|
158
|
+
const updateQuickCreateTargetList = useUpdateUserConfig();
|
|
145
159
|
|
|
146
160
|
const { data: settings, isLoading } = useQuery({
|
|
147
161
|
queryKey: ['user-task-settings'],
|
|
@@ -211,6 +225,9 @@ export function TaskSettings({ workspace }: TaskSettingsProps) {
|
|
|
211
225
|
const dialogPresentation = normalizeTaskDialogPresentation(
|
|
212
226
|
dialogPresentationRaw
|
|
213
227
|
);
|
|
228
|
+
const quickCreateTargetList = normalizeTaskQuickCreateTargetList(
|
|
229
|
+
quickCreateTargetListRaw
|
|
230
|
+
);
|
|
214
231
|
|
|
215
232
|
const handleDialogPresentationChange = (value: string) => {
|
|
216
233
|
const nextValue: TaskDialogPresentation = normalizeTaskDialogPresentation(
|
|
@@ -223,6 +240,15 @@ export function TaskSettings({ workspace }: TaskSettingsProps) {
|
|
|
223
240
|
});
|
|
224
241
|
};
|
|
225
242
|
|
|
243
|
+
const handleQuickCreateTargetListChange = (value: string) => {
|
|
244
|
+
const nextValue: TaskQuickCreateTargetList =
|
|
245
|
+
normalizeTaskQuickCreateTargetList(value);
|
|
246
|
+
updateQuickCreateTargetList.mutate({
|
|
247
|
+
configId: TASK_QUICK_CREATE_TARGET_LIST_CONFIG_ID,
|
|
248
|
+
value: nextValue,
|
|
249
|
+
});
|
|
250
|
+
};
|
|
251
|
+
|
|
226
252
|
return (
|
|
227
253
|
<div className="space-y-8">
|
|
228
254
|
<div className="grid gap-6">
|
|
@@ -330,6 +356,35 @@ export function TaskSettings({ workspace }: TaskSettingsProps) {
|
|
|
330
356
|
</Select>
|
|
331
357
|
</SettingItemTab>
|
|
332
358
|
<Separator />
|
|
359
|
+
<SettingItemTab
|
|
360
|
+
title={t('quick_create_target_list')}
|
|
361
|
+
description={t('quick_create_target_list_description')}
|
|
362
|
+
>
|
|
363
|
+
<Select
|
|
364
|
+
value={quickCreateTargetList}
|
|
365
|
+
onValueChange={handleQuickCreateTargetListChange}
|
|
366
|
+
disabled={
|
|
367
|
+
quickCreateTargetListLoading ||
|
|
368
|
+
updateQuickCreateTargetList.isPending
|
|
369
|
+
}
|
|
370
|
+
>
|
|
371
|
+
<SelectTrigger
|
|
372
|
+
aria-label={t('quick_create_target_list')}
|
|
373
|
+
className="w-44"
|
|
374
|
+
>
|
|
375
|
+
<SelectValue />
|
|
376
|
+
</SelectTrigger>
|
|
377
|
+
<SelectContent>
|
|
378
|
+
<SelectItem value="default_list">
|
|
379
|
+
{t('quick_create_target_list_default')}
|
|
380
|
+
</SelectItem>
|
|
381
|
+
<SelectItem value="hovered_list">
|
|
382
|
+
{t('quick_create_target_list_hovered')}
|
|
383
|
+
</SelectItem>
|
|
384
|
+
</SelectContent>
|
|
385
|
+
</Select>
|
|
386
|
+
</SettingItemTab>
|
|
387
|
+
<Separator />
|
|
333
388
|
<SettingItemTab
|
|
334
389
|
title={t('draft_mode')}
|
|
335
390
|
description={t('draft_mode_description')}
|
|
@@ -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
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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' ||
|
|
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
|
+
});
|