gladvn 0.2.19 → 0.2.21
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/README.md +1 -0
- package/bin/cli.js +15 -5
- package/dist/components/macro/accordion-preset.d.ts.map +1 -1
- package/dist/components/macro/alert-dialog-preset.d.ts.map +1 -1
- package/dist/components/macro/alert-preset.d.ts.map +1 -1
- package/dist/components/macro/avatar-preset.d.ts.map +1 -1
- package/dist/components/macro/breadcrumb-preset.d.ts.map +1 -1
- package/dist/components/macro/card-preset.d.ts.map +1 -1
- package/dist/components/macro/carousel-preset.d.ts.map +1 -1
- package/dist/components/macro/checkbox-preset.d.ts.map +1 -1
- package/dist/components/macro/combobox-preset.d.ts.map +1 -1
- package/dist/components/macro/command-preset.d.ts +1 -1
- package/dist/components/macro/command-preset.d.ts.map +1 -1
- package/dist/components/macro/date-picker.d.ts.map +1 -1
- package/dist/components/macro/dialog-preset.d.ts.map +1 -1
- package/dist/components/macro/empty-preset.d.ts +1 -1
- package/dist/components/macro/empty-preset.d.ts.map +1 -1
- package/dist/components/macro/field-preset.d.ts.map +1 -1
- package/dist/components/macro/input-otp-preset.d.ts.map +1 -1
- package/dist/components/macro/input-preset.d.ts.map +1 -1
- package/dist/components/macro/pagination-preset.d.ts.map +1 -1
- package/dist/components/macro/progress-preset.d.ts.map +1 -1
- package/dist/components/macro/radio-group-preset.d.ts.map +1 -1
- package/dist/components/macro/select-preset.d.ts.map +1 -1
- package/dist/components/macro/slider-preset.d.ts.map +1 -1
- package/dist/components/macro/switch-preset.d.ts.map +1 -1
- package/dist/components/macro/tabs-preset.d.ts.map +1 -1
- package/dist/components/micro/alert-dialog.d.ts +4 -4
- package/dist/components/micro/alert-dialog.d.ts.map +1 -1
- package/dist/components/micro/combobox.d.ts +4 -3
- package/dist/components/micro/combobox.d.ts.map +1 -1
- package/dist/components/micro/context-menu.d.ts +4 -6
- package/dist/components/micro/context-menu.d.ts.map +1 -1
- package/dist/components/micro/dialog.d.ts +4 -3
- package/dist/components/micro/dialog.d.ts.map +1 -1
- package/dist/components/micro/drawer.d.ts +4 -3
- package/dist/components/micro/drawer.d.ts.map +1 -1
- package/dist/components/micro/dropdown-menu.d.ts +7 -4
- package/dist/components/micro/dropdown-menu.d.ts.map +1 -1
- package/dist/components/micro/hover-card.d.ts +4 -3
- package/dist/components/micro/hover-card.d.ts.map +1 -1
- package/dist/components/micro/menubar.d.ts +2 -3
- package/dist/components/micro/menubar.d.ts.map +1 -1
- package/dist/components/micro/navigation-menu.d.ts +4 -3
- package/dist/components/micro/navigation-menu.d.ts.map +1 -1
- package/dist/components/micro/popover.d.ts +4 -3
- package/dist/components/micro/popover.d.ts.map +1 -1
- package/dist/components/micro/select.d.ts +4 -3
- package/dist/components/micro/select.d.ts.map +1 -1
- package/dist/components/micro/sheet.d.ts +3 -3
- package/dist/components/micro/sheet.d.ts.map +1 -1
- package/dist/components/micro/tooltip.d.ts +4 -3
- package/dist/components/micro/tooltip.d.ts.map +1 -1
- package/dist/dev/App.d.ts.map +1 -1
- package/dist/dev/data.d.ts.map +1 -1
- package/dist/dev/showcase/accordion.d.ts.map +1 -1
- package/dist/dev/showcase/alert-dialog.d.ts.map +1 -1
- package/dist/dev/showcase/alert.d.ts.map +1 -1
- package/dist/dev/showcase/aspect-ratio.d.ts.map +1 -1
- package/dist/dev/showcase/avatar.d.ts.map +1 -1
- package/dist/dev/showcase/badge.d.ts.map +1 -1
- package/dist/dev/showcase/breadcrumb.d.ts.map +1 -1
- package/dist/dev/showcase/button.d.ts.map +1 -1
- package/dist/dev/showcase/calendar.d.ts.map +1 -1
- package/dist/dev/showcase/card.d.ts.map +1 -1
- package/dist/dev/showcase/carousel.d.ts.map +1 -1
- package/dist/dev/showcase/chart.d.ts.map +1 -1
- package/dist/dev/showcase/checkbox.d.ts.map +1 -1
- package/dist/dev/showcase/collapsible.d.ts.map +1 -1
- package/dist/dev/showcase/combobox.d.ts.map +1 -1
- package/dist/dev/showcase/command.d.ts.map +1 -1
- package/dist/dev/showcase/context-menu.d.ts.map +1 -1
- package/dist/dev/showcase/dialog.d.ts.map +1 -1
- package/dist/dev/showcase/drawer.d.ts.map +1 -1
- package/dist/dev/showcase/dropdown-menu.d.ts.map +1 -1
- package/dist/dev/showcase/empty.d.ts.map +1 -1
- package/dist/dev/showcase/hover-card.d.ts.map +1 -1
- package/dist/dev/showcase/input-group.d.ts.map +1 -1
- package/dist/dev/showcase/input-otp.d.ts.map +1 -1
- package/dist/dev/showcase/input.d.ts.map +1 -1
- package/dist/dev/showcase/item.d.ts.map +1 -1
- package/dist/dev/showcase/kbd.d.ts.map +1 -1
- package/dist/dev/showcase/label.d.ts.map +1 -1
- package/dist/dev/showcase/menubar.d.ts.map +1 -1
- package/dist/dev/showcase/navigation-menu.d.ts.map +1 -1
- package/dist/dev/showcase/overview.d.ts.map +1 -1
- package/dist/dev/showcase/pagination.d.ts.map +1 -1
- package/dist/dev/showcase/popover.d.ts.map +1 -1
- package/dist/dev/showcase/progress.d.ts.map +1 -1
- package/dist/dev/showcase/radio-group.d.ts.map +1 -1
- package/dist/dev/showcase/resizable.d.ts.map +1 -1
- package/dist/dev/showcase/scroll-area.d.ts.map +1 -1
- package/dist/dev/showcase/select.d.ts.map +1 -1
- package/dist/dev/showcase/separator.d.ts.map +1 -1
- package/dist/dev/showcase/sheet.d.ts.map +1 -1
- package/dist/dev/showcase/sidebar.d.ts.map +1 -1
- package/dist/dev/showcase/skeleton.d.ts.map +1 -1
- package/dist/dev/showcase/slider.d.ts.map +1 -1
- package/dist/dev/showcase/sonner.d.ts.map +1 -1
- package/dist/dev/showcase/spinner.d.ts.map +1 -1
- package/dist/dev/showcase/switch.d.ts.map +1 -1
- package/dist/dev/showcase/table.d.ts.map +1 -1
- package/dist/dev/showcase/tabs.d.ts.map +1 -1
- package/dist/dev/showcase/textarea.d.ts.map +1 -1
- package/dist/dev/showcase/toast.d.ts.map +1 -1
- package/dist/dev/showcase/toggle-group.d.ts.map +1 -1
- package/dist/dev/showcase/toggle.d.ts.map +1 -1
- package/dist/dev/showcase/tooltip.d.ts.map +1 -1
- package/dist/index.cjs +256 -256
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +257 -244
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/components/macro/accordion-preset.test.tsx +1 -2
- package/src/components/macro/accordion-preset.tsx +1 -2
- package/src/components/macro/alert-dialog-preset.tsx +7 -12
- package/src/components/macro/alert-preset.tsx +1 -2
- package/src/components/macro/avatar-preset.tsx +2 -4
- package/src/components/macro/breadcrumb-preset.tsx +1 -2
- package/src/components/macro/card-preset.tsx +1 -2
- package/src/components/macro/carousel-preset.tsx +1 -2
- package/src/components/macro/checkbox-preset.tsx +1 -2
- package/src/components/macro/combobox-preset.tsx +8 -14
- package/src/components/macro/command-preset.tsx +18 -24
- package/src/components/macro/date-picker.tsx +14 -23
- package/src/components/macro/dialog-preset.tsx +45 -49
- package/src/components/macro/empty-preset.tsx +2 -4
- package/src/components/macro/field-preset.tsx +1 -2
- package/src/components/macro/input-otp-preset.tsx +1 -2
- package/src/components/macro/input-preset.tsx +1 -2
- package/src/components/macro/pagination-preset.tsx +3 -6
- package/src/components/macro/progress-preset.tsx +1 -2
- package/src/components/macro/radio-group-preset.tsx +2 -4
- package/src/components/macro/select-preset.tsx +7 -12
- package/src/components/macro/slider-preset.tsx +1 -2
- package/src/components/macro/switch-preset.tsx +1 -2
- package/src/components/macro/tabs-preset.tsx +1 -2
- package/src/components/micro/alert-dialog.tsx +16 -14
- package/src/components/micro/combobox.tsx +28 -22
- package/src/components/micro/context-menu.tsx +52 -36
- package/src/components/micro/dialog.tsx +23 -19
- package/src/components/micro/drawer.tsx +23 -19
- package/src/components/micro/dropdown-menu.tsx +23 -18
- package/src/components/micro/hover-card.tsx +31 -23
- package/src/components/micro/menubar.tsx +0 -8
- package/src/components/micro/navigation-menu.tsx +26 -19
- package/src/components/micro/popover.tsx +31 -25
- package/src/components/micro/select.tsx +36 -29
- package/src/components/micro/sheet.tsx +36 -31
- package/src/components/micro/tooltip.tsx +28 -23
- package/src/dev/App.tsx +36 -10
- package/src/dev/data.ts +0 -8
- package/src/dev/showcase/accordion.tsx +47 -94
- package/src/dev/showcase/alert-dialog.tsx +27 -39
- package/src/dev/showcase/alert.tsx +5 -10
- package/src/dev/showcase/aspect-ratio.tsx +1 -2
- package/src/dev/showcase/avatar.tsx +5 -10
- package/src/dev/showcase/badge.tsx +2 -4
- package/src/dev/showcase/breadcrumb.tsx +8 -11
- package/src/dev/showcase/button.tsx +2 -4
- package/src/dev/showcase/calendar.tsx +5 -10
- package/src/dev/showcase/card.tsx +4 -8
- package/src/dev/showcase/carousel.tsx +40 -80
- package/src/dev/showcase/chart.tsx +7 -16
- package/src/dev/showcase/checkbox.tsx +7 -14
- package/src/dev/showcase/collapsible.tsx +3 -6
- package/src/dev/showcase/combobox.tsx +27 -43
- package/src/dev/showcase/command.tsx +4 -8
- package/src/dev/showcase/context-menu.tsx +13 -19
- package/src/dev/showcase/dialog.tsx +19 -33
- package/src/dev/showcase/drawer.tsx +19 -27
- package/src/dev/showcase/dropdown-menu.tsx +19 -29
- package/src/dev/showcase/empty.tsx +3 -6
- package/src/dev/showcase/hover-card.tsx +24 -36
- package/src/dev/showcase/input-group.tsx +4 -8
- package/src/dev/showcase/input-otp.tsx +5 -10
- package/src/dev/showcase/input.tsx +5 -10
- package/src/dev/showcase/item.tsx +3 -6
- package/src/dev/showcase/kbd.tsx +12 -16
- package/src/dev/showcase/label.tsx +2 -4
- package/src/dev/showcase/menubar.tsx +17 -19
- package/src/dev/showcase/navigation-menu.tsx +12 -15
- package/src/dev/showcase/overview.tsx +17 -30
- package/src/dev/showcase/pagination.tsx +4 -8
- package/src/dev/showcase/popover.tsx +21 -27
- package/src/dev/showcase/progress.tsx +4 -8
- package/src/dev/showcase/radio-group.tsx +8 -16
- package/src/dev/showcase/resizable.tsx +2 -4
- package/src/dev/showcase/scroll-area.tsx +1 -2
- package/src/dev/showcase/select.tsx +27 -39
- package/src/dev/showcase/separator.tsx +1 -2
- package/src/dev/showcase/sheet.tsx +22 -33
- package/src/dev/showcase/sidebar.tsx +2 -4
- package/src/dev/showcase/skeleton.tsx +1 -2
- package/src/dev/showcase/slider.tsx +5 -10
- package/src/dev/showcase/sonner.tsx +18 -38
- package/src/dev/showcase/spinner.tsx +2 -4
- package/src/dev/showcase/switch.tsx +6 -12
- package/src/dev/showcase/table.tsx +3 -6
- package/src/dev/showcase/tabs.tsx +20 -40
- package/src/dev/showcase/textarea.tsx +5 -10
- package/src/dev/showcase/toast.tsx +4 -10
- package/src/dev/showcase/toggle-group.tsx +4 -8
- package/src/dev/showcase/toggle.tsx +2 -4
- package/src/dev/showcase/tooltip.tsx +34 -43
- package/dist/dev/showcase/theme-provider.d.ts +0 -2
- package/dist/dev/showcase/theme-provider.d.ts.map +0 -1
- package/src/dev/showcase/theme-provider.tsx +0 -956
|
@@ -1,956 +0,0 @@
|
|
|
1
|
-
import { useState } from "react";
|
|
2
|
-
|
|
3
|
-
import { MoonIcon, SunIcon } from "lucide-react";
|
|
4
|
-
|
|
5
|
-
import { Button } from "../../components/micro/button";
|
|
6
|
-
import {
|
|
7
|
-
Dialog,
|
|
8
|
-
DialogContent,
|
|
9
|
-
DialogDescription,
|
|
10
|
-
DialogHeader,
|
|
11
|
-
DialogPortal,
|
|
12
|
-
DialogTitle,
|
|
13
|
-
DialogTrigger,
|
|
14
|
-
} from "../../components/micro/dialog";
|
|
15
|
-
import {
|
|
16
|
-
DropdownMenu,
|
|
17
|
-
DropdownMenuContent,
|
|
18
|
-
DropdownMenuGroup,
|
|
19
|
-
DropdownMenuItem,
|
|
20
|
-
DropdownMenuLabel,
|
|
21
|
-
DropdownMenuPortal,
|
|
22
|
-
DropdownMenuSeparator,
|
|
23
|
-
DropdownMenuTrigger,
|
|
24
|
-
} from "../../components/micro/dropdown-menu";
|
|
25
|
-
import {
|
|
26
|
-
Popover,
|
|
27
|
-
PopoverContent,
|
|
28
|
-
PopoverPortal,
|
|
29
|
-
PopoverTrigger,
|
|
30
|
-
} from "../../components/micro/popover";
|
|
31
|
-
import {
|
|
32
|
-
Select,
|
|
33
|
-
SelectContent,
|
|
34
|
-
SelectItem,
|
|
35
|
-
SelectPortal,
|
|
36
|
-
SelectTrigger,
|
|
37
|
-
SelectValue,
|
|
38
|
-
} from "../../components/micro/select";
|
|
39
|
-
import {
|
|
40
|
-
Sheet,
|
|
41
|
-
SheetContent,
|
|
42
|
-
SheetHeader,
|
|
43
|
-
SheetPortal,
|
|
44
|
-
SheetTitle,
|
|
45
|
-
SheetTrigger,
|
|
46
|
-
} from "../../components/micro/sheet";
|
|
47
|
-
import {
|
|
48
|
-
type ThemeMode,
|
|
49
|
-
ThemeProvider,
|
|
50
|
-
ThemeWrapper,
|
|
51
|
-
useTheme,
|
|
52
|
-
} from "../../components/micro/theme-provider";
|
|
53
|
-
import {
|
|
54
|
-
Tooltip,
|
|
55
|
-
TooltipContent,
|
|
56
|
-
TooltipPortal,
|
|
57
|
-
TooltipProvider,
|
|
58
|
-
TooltipTrigger,
|
|
59
|
-
} from "../../components/micro/tooltip";
|
|
60
|
-
import {
|
|
61
|
-
DocsCode,
|
|
62
|
-
DocsH3,
|
|
63
|
-
DocsP,
|
|
64
|
-
ExampleSection,
|
|
65
|
-
SectionHeader,
|
|
66
|
-
Showcase,
|
|
67
|
-
ShowcaseDocs,
|
|
68
|
-
} from "../../dev/components/showcase";
|
|
69
|
-
|
|
70
|
-
// ──────────────────────────────────────────────────────────
|
|
71
|
-
// Demo helpers
|
|
72
|
-
// ──────────────────────────────────────────────────────────
|
|
73
|
-
|
|
74
|
-
/** Demo card that reads theme from context and shows current mode */
|
|
75
|
-
function ThemeAwareCard({ readOnly }: { readOnly?: boolean } = {}) {
|
|
76
|
-
const theme = useTheme();
|
|
77
|
-
|
|
78
|
-
return (
|
|
79
|
-
<div className="rounded-xl border border-border bg-card text-card-foreground p-4 space-y-3">
|
|
80
|
-
<div className="flex items-center justify-between">
|
|
81
|
-
<span className="text-sm font-medium">Thẻ nội dung</span>
|
|
82
|
-
<span className="text-xs px-2 py-0.5 rounded-full bg-muted text-muted-foreground">
|
|
83
|
-
{theme?.mode ?? "unknown"}
|
|
84
|
-
</span>
|
|
85
|
-
</div>
|
|
86
|
-
<p className="text-xs text-muted-foreground">
|
|
87
|
-
Nền, chữ và viền được điều khiển bởi CSS variable từ ThemeProvider cha.
|
|
88
|
-
</p>
|
|
89
|
-
{!readOnly && (
|
|
90
|
-
<Button
|
|
91
|
-
variant="ghost"
|
|
92
|
-
size="sm"
|
|
93
|
-
iconOnly
|
|
94
|
-
aria-label={
|
|
95
|
-
theme?.mode === "dark" ? "Chuyển sang Light" : "Chuyển sang Dark"
|
|
96
|
-
}
|
|
97
|
-
onClick={() =>
|
|
98
|
-
theme?.setMode(theme.mode === "dark" ? "light" : "dark")
|
|
99
|
-
}
|
|
100
|
-
>
|
|
101
|
-
{theme?.mode === "dark" ? (
|
|
102
|
-
<SunIcon aria-hidden="true" />
|
|
103
|
-
) : (
|
|
104
|
-
<MoonIcon aria-hidden="true" />
|
|
105
|
-
)}
|
|
106
|
-
</Button>
|
|
107
|
-
)}
|
|
108
|
-
</div>
|
|
109
|
-
);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// ──────────────────────────────────────────────────────────
|
|
113
|
-
// SECTION 1: Micro Showcase
|
|
114
|
-
// ──────────────────────────────────────────────────────────
|
|
115
|
-
function ThemeProviderMicroShowcase() {
|
|
116
|
-
// Controlled demo state (owned by this parent)
|
|
117
|
-
const [controlledMode, setControlledMode] = useState<ThemeMode>("light");
|
|
118
|
-
|
|
119
|
-
return (
|
|
120
|
-
<div className="space-y-10">
|
|
121
|
-
<SectionHeader
|
|
122
|
-
title="ThemeProvider"
|
|
123
|
-
description="Context provider điều phối dark/light mode cho toàn bộ component tree. Hỗ trợ hai chế độ: Uncontrolled (tự quản lý state) và Controlled (state do component cha sở hữu). Dùng ThemeWrapper để tunnel theme qua Portal boundary."
|
|
124
|
-
/>
|
|
125
|
-
|
|
126
|
-
{/* ── Uncontrolled ── */}
|
|
127
|
-
<SectionHeader
|
|
128
|
-
title="Uncontrolled"
|
|
129
|
-
description="Dùng khi component tự quản lý theme — không cần chia sẻ state với component khác. Thích hợp cho: scoped dark sections, isolated previews, hoặc khi không cần sync với localStorage/system."
|
|
130
|
-
/>
|
|
131
|
-
<ExampleSection
|
|
132
|
-
label="defaultMode"
|
|
133
|
-
description="ThemeProvider giữ state nội bộ. Bất kỳ component con nào đều có thể sử dụng hook `useTheme()` để đọc hoặc cập nhật mode."
|
|
134
|
-
codeString={`function ThemeAwareCard() {
|
|
135
|
-
const theme = useTheme();
|
|
136
|
-
|
|
137
|
-
return (
|
|
138
|
-
<div className="rounded-xl border border-border bg-card text-card-foreground p-4 space-y-3">
|
|
139
|
-
<div className="flex items-center justify-between">
|
|
140
|
-
<span className="text-sm font-medium">Thẻ nội dung</span>
|
|
141
|
-
<span className="text-xs px-2 py-0.5 rounded-full bg-muted text-muted-foreground">
|
|
142
|
-
{theme?.mode}
|
|
143
|
-
</span>
|
|
144
|
-
</div>
|
|
145
|
-
<button onClick={() => theme?.setMode(theme.mode === "dark" ? "light" : "dark")}>
|
|
146
|
-
Chuyển sang {theme?.mode === "dark" ? "Light" : "Dark"}
|
|
147
|
-
</button>
|
|
148
|
-
</div>
|
|
149
|
-
);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
<ThemeProvider defaultMode="light">
|
|
153
|
-
<ThemeAwareCard />
|
|
154
|
-
</ThemeProvider>`}
|
|
155
|
-
>
|
|
156
|
-
<ThemeProvider defaultMode="light">
|
|
157
|
-
<ThemeAwareCard />
|
|
158
|
-
</ThemeProvider>
|
|
159
|
-
</ExampleSection>
|
|
160
|
-
|
|
161
|
-
{/* ── Controlled ── */}
|
|
162
|
-
<SectionHeader
|
|
163
|
-
title="Controlled"
|
|
164
|
-
description="Dùng khi theme cần được đồng bộ với state bên ngoài — ví dụ: Macro component đọc từ localStorage, system settings, hoặc API. Component cha hoàn toàn sở hữu state, ThemeProvider chỉ đóng vai trò hiển thị (dumb component)."
|
|
165
|
-
/>
|
|
166
|
-
<ExampleSection
|
|
167
|
-
label="mode + onModeChange"
|
|
168
|
-
description="State hoàn toàn được quản lý bởi component cha. ThemeProvider chỉ nhận prop mode và cập nhật UI khi cha thay đổi state."
|
|
169
|
-
codeString={`const [mode, setMode] = useState<ThemeMode>("light");
|
|
170
|
-
|
|
171
|
-
<div className="space-y-3 w-full">
|
|
172
|
-
{/* Toggle nằm ở component cha — simulates Macro owning state */}
|
|
173
|
-
<div className="flex items-center gap-3 p-3 rounded-lg bg-muted/50 border border-border">
|
|
174
|
-
<span className="text-xs text-muted-foreground flex-1">
|
|
175
|
-
State ở component cha: <strong>{mode}</strong>
|
|
176
|
-
</span>
|
|
177
|
-
<Button
|
|
178
|
-
size="sm"
|
|
179
|
-
variant="outline"
|
|
180
|
-
onClick={() => setMode(mode === "dark" ? "light" : "dark")}
|
|
181
|
-
>
|
|
182
|
-
Toggle từ ngoài
|
|
183
|
-
</Button>
|
|
184
|
-
</div>
|
|
185
|
-
|
|
186
|
-
<ThemeProvider mode={mode} onModeChange={setMode}>
|
|
187
|
-
{/* ThemeAwareCard là component đọc useTheme() để hiển thị mode hiện tại */}
|
|
188
|
-
<ThemeAwareCard readOnly />
|
|
189
|
-
</ThemeProvider>
|
|
190
|
-
</div>`}
|
|
191
|
-
>
|
|
192
|
-
<div className="space-y-3 w-full">
|
|
193
|
-
{/* External toggle (simulates Macro owning state) */}
|
|
194
|
-
<div className="flex items-center gap-3 p-3 rounded-lg bg-muted/50 border border-border">
|
|
195
|
-
<span className="text-xs text-muted-foreground flex-1">
|
|
196
|
-
State ở component cha: <DocsCode>{controlledMode}</DocsCode>
|
|
197
|
-
</span>
|
|
198
|
-
<Button
|
|
199
|
-
size="sm"
|
|
200
|
-
variant="outline"
|
|
201
|
-
onClick={() =>
|
|
202
|
-
setControlledMode(controlledMode === "dark" ? "light" : "dark")
|
|
203
|
-
}
|
|
204
|
-
>
|
|
205
|
-
{controlledMode === "dark" ? (
|
|
206
|
-
<SunIcon aria-hidden="true" />
|
|
207
|
-
) : (
|
|
208
|
-
<MoonIcon aria-hidden="true" />
|
|
209
|
-
)}
|
|
210
|
-
{controlledMode === "dark" ? "Light" : "Dark"}
|
|
211
|
-
</Button>
|
|
212
|
-
</div>
|
|
213
|
-
|
|
214
|
-
<ThemeProvider mode={controlledMode} onModeChange={setControlledMode}>
|
|
215
|
-
<ThemeAwareCard readOnly />
|
|
216
|
-
</ThemeProvider>
|
|
217
|
-
</div>
|
|
218
|
-
</ExampleSection>
|
|
219
|
-
|
|
220
|
-
{/* ── ThemeWrapper & Portal Tunnels ── */}
|
|
221
|
-
<div className="pt-16 pb-6">
|
|
222
|
-
<SectionHeader
|
|
223
|
-
title="ThemeWrapper & Portal Tunnels"
|
|
224
|
-
description="ThemeWrapper tunnel theme class qua Portal boundary (Dialog, Tooltip, Popover...) bằng cách re-apply CSS variable vào container mới. Dưới đây là so sánh trực tiếp giữa portal không có và có ThemeWrapper khi theme cục bộ (ThemeProvider cha) đang ở Dark Mode."
|
|
225
|
-
/>
|
|
226
|
-
</div>
|
|
227
|
-
|
|
228
|
-
{/* Tooltip */}
|
|
229
|
-
<ExampleSection
|
|
230
|
-
fullWidth
|
|
231
|
-
label="Tooltip"
|
|
232
|
-
description="Tooltip được định vị gần trigger và hiển thị khi hover. Không có ThemeWrapper, tooltip của bạn sẽ trắng toát giữa một dark section — dười đây là bằng chứng trực quan."
|
|
233
|
-
codeString={`<ThemeProvider defaultMode="dark">
|
|
234
|
-
<div className="w-full flex gap-10 rounded-xl border border-border bg-background p-10">
|
|
235
|
-
<div className="flex-1 flex flex-col items-center gap-4">
|
|
236
|
-
<span className="text-sm font-medium text-muted-foreground">
|
|
237
|
-
Không dùng ThemeWrapper
|
|
238
|
-
</span>
|
|
239
|
-
<TooltipProvider>
|
|
240
|
-
<Tooltip>
|
|
241
|
-
<TooltipTrigger
|
|
242
|
-
render={
|
|
243
|
-
<Button variant="solid">Hover tôi</Button>
|
|
244
|
-
}
|
|
245
|
-
/>
|
|
246
|
-
<TooltipPortal>
|
|
247
|
-
<TooltipContent
|
|
248
|
-
sideOffset={8}
|
|
249
|
-
className="bg-card text-card-foreground border border-border shadow-md"
|
|
250
|
-
>
|
|
251
|
-
Trắng toát! Lạc quẻ với nền đen.
|
|
252
|
-
</TooltipContent>
|
|
253
|
-
</TooltipPortal>
|
|
254
|
-
</Tooltip>
|
|
255
|
-
</TooltipProvider>
|
|
256
|
-
</div>
|
|
257
|
-
|
|
258
|
-
<div className="flex-1 flex flex-col items-center gap-4">
|
|
259
|
-
<span className="text-sm font-medium text-muted-foreground">
|
|
260
|
-
Có ThemeWrapper
|
|
261
|
-
</span>
|
|
262
|
-
<TooltipProvider>
|
|
263
|
-
<Tooltip>
|
|
264
|
-
<TooltipTrigger
|
|
265
|
-
render={
|
|
266
|
-
<Button variant="solid">Hover tôi</Button>
|
|
267
|
-
}
|
|
268
|
-
/>
|
|
269
|
-
<TooltipPortal>
|
|
270
|
-
<ThemeWrapper>
|
|
271
|
-
<TooltipContent
|
|
272
|
-
sideOffset={8}
|
|
273
|
-
className="bg-card text-card-foreground border border-border shadow-md"
|
|
274
|
-
>
|
|
275
|
-
Màu Tối! Đồng bộ với provider cha.
|
|
276
|
-
</TooltipContent>
|
|
277
|
-
</ThemeWrapper>
|
|
278
|
-
</TooltipPortal>
|
|
279
|
-
</Tooltip>
|
|
280
|
-
</TooltipProvider>
|
|
281
|
-
</div>
|
|
282
|
-
</div>
|
|
283
|
-
</ThemeProvider>`}
|
|
284
|
-
>
|
|
285
|
-
<ThemeProvider defaultMode="dark">
|
|
286
|
-
<div className="w-full flex gap-10 rounded-xl border border-border bg-background p-10">
|
|
287
|
-
<div className="flex-1 flex flex-col items-center gap-4">
|
|
288
|
-
<span className="text-sm font-medium text-muted-foreground">
|
|
289
|
-
Không dùng ThemeWrapper
|
|
290
|
-
</span>
|
|
291
|
-
<TooltipProvider>
|
|
292
|
-
<Tooltip>
|
|
293
|
-
<TooltipTrigger
|
|
294
|
-
render={<Button variant="solid">Hover tôi</Button>}
|
|
295
|
-
/>
|
|
296
|
-
<TooltipPortal>
|
|
297
|
-
<TooltipContent
|
|
298
|
-
sideOffset={8}
|
|
299
|
-
className="bg-card text-card-foreground border border-border shadow-md"
|
|
300
|
-
>
|
|
301
|
-
Trắng toát! Lạc quẻ với nền đen.
|
|
302
|
-
</TooltipContent>
|
|
303
|
-
</TooltipPortal>
|
|
304
|
-
</Tooltip>
|
|
305
|
-
</TooltipProvider>
|
|
306
|
-
</div>
|
|
307
|
-
|
|
308
|
-
<div className="flex-1 flex flex-col items-center gap-4">
|
|
309
|
-
<span className="text-sm font-medium text-muted-foreground">
|
|
310
|
-
Có ThemeWrapper
|
|
311
|
-
</span>
|
|
312
|
-
<TooltipProvider>
|
|
313
|
-
<Tooltip>
|
|
314
|
-
<TooltipTrigger
|
|
315
|
-
render={<Button variant="solid">Hover tôi</Button>}
|
|
316
|
-
/>
|
|
317
|
-
<TooltipPortal>
|
|
318
|
-
<ThemeWrapper>
|
|
319
|
-
<TooltipContent
|
|
320
|
-
sideOffset={8}
|
|
321
|
-
className="bg-card text-card-foreground border border-border shadow-md"
|
|
322
|
-
>
|
|
323
|
-
Màu Tối! Đồng bộ với provider cha.
|
|
324
|
-
</TooltipContent>
|
|
325
|
-
</ThemeWrapper>
|
|
326
|
-
</TooltipPortal>
|
|
327
|
-
</Tooltip>
|
|
328
|
-
</TooltipProvider>
|
|
329
|
-
</div>
|
|
330
|
-
</div>
|
|
331
|
-
</ThemeProvider>
|
|
332
|
-
</ExampleSection>
|
|
333
|
-
|
|
334
|
-
{/* Popover */}
|
|
335
|
-
<ExampleSection
|
|
336
|
-
fullWidth
|
|
337
|
-
label="Popover"
|
|
338
|
-
description="Popover chứa content phức tạp và mở khi click. Vì portal hướng tới `document.body`, toàn bộ nền popover lấy CSS variable từ root — sẽ trắng hoàn toàn dù trigger đang nằm trong dark section."
|
|
339
|
-
codeString={`<ThemeProvider defaultMode="dark">
|
|
340
|
-
<div className="w-full flex gap-10 rounded-xl border border-border bg-background p-10">
|
|
341
|
-
<div className="flex-1 flex flex-col items-center gap-4">
|
|
342
|
-
<span className="text-sm font-medium text-muted-foreground">
|
|
343
|
-
Không dùng ThemeWrapper
|
|
344
|
-
</span>
|
|
345
|
-
<Popover>
|
|
346
|
-
<PopoverTrigger
|
|
347
|
-
render={
|
|
348
|
-
<Button variant="outline">
|
|
349
|
-
Click Popover
|
|
350
|
-
</Button>
|
|
351
|
-
}
|
|
352
|
-
/>
|
|
353
|
-
<PopoverPortal>
|
|
354
|
-
<PopoverContent className="w-64">
|
|
355
|
-
<div className="space-y-2">
|
|
356
|
-
<h4 className="font-medium leading-none">
|
|
357
|
-
Nội dung Popover
|
|
358
|
-
</h4>
|
|
359
|
-
<p className="text-sm text-muted-foreground">
|
|
360
|
-
Lỗi: Hiển thị giao diện sáng.
|
|
361
|
-
</p>
|
|
362
|
-
</div>
|
|
363
|
-
</PopoverContent>
|
|
364
|
-
</PopoverPortal>
|
|
365
|
-
</Popover>
|
|
366
|
-
</div>
|
|
367
|
-
|
|
368
|
-
<div className="flex-1 flex flex-col items-center gap-4">
|
|
369
|
-
<span className="text-sm font-medium text-muted-foreground">
|
|
370
|
-
Có ThemeWrapper
|
|
371
|
-
</span>
|
|
372
|
-
<Popover>
|
|
373
|
-
<PopoverTrigger
|
|
374
|
-
render={
|
|
375
|
-
<Button variant="outline">
|
|
376
|
-
Click Popover
|
|
377
|
-
</Button>
|
|
378
|
-
}
|
|
379
|
-
/>
|
|
380
|
-
<PopoverPortal>
|
|
381
|
-
<ThemeWrapper>
|
|
382
|
-
<PopoverContent className="w-64">
|
|
383
|
-
<div className="space-y-2">
|
|
384
|
-
<h4 className="font-medium leading-none">
|
|
385
|
-
Nội dung Popover
|
|
386
|
-
</h4>
|
|
387
|
-
<p className="text-sm text-muted-foreground">
|
|
388
|
-
Lớp Portal Tunnel đã hoạt động chính
|
|
389
|
-
xác.
|
|
390
|
-
</p>
|
|
391
|
-
</div>
|
|
392
|
-
</PopoverContent>
|
|
393
|
-
</ThemeWrapper>
|
|
394
|
-
</PopoverPortal>
|
|
395
|
-
</Popover>
|
|
396
|
-
</div>
|
|
397
|
-
</div>
|
|
398
|
-
</ThemeProvider>`}
|
|
399
|
-
>
|
|
400
|
-
<ThemeProvider defaultMode="dark">
|
|
401
|
-
<div className="w-full flex gap-10 rounded-xl border border-border bg-background p-10">
|
|
402
|
-
<div className="flex-1 flex flex-col items-center gap-4">
|
|
403
|
-
<span className="text-sm font-medium text-muted-foreground">
|
|
404
|
-
Không dùng ThemeWrapper
|
|
405
|
-
</span>
|
|
406
|
-
<Popover>
|
|
407
|
-
<PopoverTrigger
|
|
408
|
-
render={<Button variant="outline">Click Popover</Button>}
|
|
409
|
-
/>
|
|
410
|
-
<PopoverPortal>
|
|
411
|
-
<PopoverContent className="w-64">
|
|
412
|
-
<div className="space-y-2">
|
|
413
|
-
<h4 className="font-medium leading-none">
|
|
414
|
-
Nội dung Popover
|
|
415
|
-
</h4>
|
|
416
|
-
<p className="text-sm text-muted-foreground">
|
|
417
|
-
Lỗi: Hiển thị giao diện sáng.
|
|
418
|
-
</p>
|
|
419
|
-
</div>
|
|
420
|
-
</PopoverContent>
|
|
421
|
-
</PopoverPortal>
|
|
422
|
-
</Popover>
|
|
423
|
-
</div>
|
|
424
|
-
|
|
425
|
-
<div className="flex-1 flex flex-col items-center gap-4">
|
|
426
|
-
<span className="text-sm font-medium text-muted-foreground">
|
|
427
|
-
Có ThemeWrapper
|
|
428
|
-
</span>
|
|
429
|
-
<Popover>
|
|
430
|
-
<PopoverTrigger
|
|
431
|
-
render={<Button variant="outline">Click Popover</Button>}
|
|
432
|
-
/>
|
|
433
|
-
<PopoverPortal>
|
|
434
|
-
<ThemeWrapper>
|
|
435
|
-
<PopoverContent className="w-64">
|
|
436
|
-
<div className="space-y-2">
|
|
437
|
-
<h4 className="font-medium leading-none">
|
|
438
|
-
Nội dung Popover
|
|
439
|
-
</h4>
|
|
440
|
-
<p className="text-sm text-muted-foreground">
|
|
441
|
-
Lớp Portal Tunnel đã hoạt động chính xác.
|
|
442
|
-
</p>
|
|
443
|
-
</div>
|
|
444
|
-
</PopoverContent>
|
|
445
|
-
</ThemeWrapper>
|
|
446
|
-
</PopoverPortal>
|
|
447
|
-
</Popover>
|
|
448
|
-
</div>
|
|
449
|
-
</div>
|
|
450
|
-
</ThemeProvider>
|
|
451
|
-
</ExampleSection>
|
|
452
|
-
|
|
453
|
-
{/* Select */}
|
|
454
|
-
<ExampleSection
|
|
455
|
-
fullWidth
|
|
456
|
-
label="Select"
|
|
457
|
-
description="SelectContent được render tách biệt hoàn toàn khỏi trigger. Thiếu ThemeWrapper, dropdown option sẽ trắng xuộa trong khi trigger vẫn tối — mù quáng và khó đọc."
|
|
458
|
-
codeString={`<ThemeProvider defaultMode="dark">
|
|
459
|
-
<div className="w-full flex gap-10 rounded-xl border border-border bg-background p-10">
|
|
460
|
-
<div className="flex-1 flex flex-col items-center gap-4">
|
|
461
|
-
<span className="text-sm font-medium text-muted-foreground">
|
|
462
|
-
Không dùng ThemeWrapper
|
|
463
|
-
</span>
|
|
464
|
-
<Select
|
|
465
|
-
items={{
|
|
466
|
-
next: "Next.js",
|
|
467
|
-
vite: "Vite",
|
|
468
|
-
remix: "Remix",
|
|
469
|
-
}}
|
|
470
|
-
>
|
|
471
|
-
<SelectTrigger className="w-[140px]">
|
|
472
|
-
<SelectValue placeholder="Chọn framework..." />
|
|
473
|
-
</SelectTrigger>
|
|
474
|
-
<SelectPortal>
|
|
475
|
-
<SelectContent>
|
|
476
|
-
<SelectItem value="next">Next.js</SelectItem>
|
|
477
|
-
<SelectItem value="vite">Vite</SelectItem>
|
|
478
|
-
<SelectItem value="remix">Remix</SelectItem>
|
|
479
|
-
</SelectContent>
|
|
480
|
-
</SelectPortal>
|
|
481
|
-
</Select>
|
|
482
|
-
</div>
|
|
483
|
-
|
|
484
|
-
<div className="flex-1 flex flex-col items-center gap-4">
|
|
485
|
-
<span className="text-sm font-medium text-muted-foreground">
|
|
486
|
-
Có ThemeWrapper
|
|
487
|
-
</span>
|
|
488
|
-
<Select
|
|
489
|
-
items={{
|
|
490
|
-
next: "Next.js",
|
|
491
|
-
vite: "Vite",
|
|
492
|
-
remix: "Remix",
|
|
493
|
-
}}
|
|
494
|
-
>
|
|
495
|
-
<SelectTrigger className="w-[140px]">
|
|
496
|
-
<SelectValue placeholder="Chọn framework..." />
|
|
497
|
-
</SelectTrigger>
|
|
498
|
-
<SelectPortal>
|
|
499
|
-
<ThemeWrapper>
|
|
500
|
-
<SelectContent>
|
|
501
|
-
<SelectItem value="next">
|
|
502
|
-
Next.js
|
|
503
|
-
</SelectItem>
|
|
504
|
-
<SelectItem value="vite">Vite</SelectItem>
|
|
505
|
-
<SelectItem value="remix">Remix</SelectItem>
|
|
506
|
-
</SelectContent>
|
|
507
|
-
</ThemeWrapper>
|
|
508
|
-
</SelectPortal>
|
|
509
|
-
</Select>
|
|
510
|
-
</div>
|
|
511
|
-
</div>
|
|
512
|
-
</ThemeProvider>`}
|
|
513
|
-
>
|
|
514
|
-
<ThemeProvider defaultMode="dark">
|
|
515
|
-
<div className="w-full flex gap-10 rounded-xl border border-border bg-background p-10">
|
|
516
|
-
<div className="flex-1 flex flex-col items-center gap-4">
|
|
517
|
-
<span className="text-sm font-medium text-muted-foreground">
|
|
518
|
-
Không dùng ThemeWrapper
|
|
519
|
-
</span>
|
|
520
|
-
<Select items={{ next: "Next.js", vite: "Vite", remix: "Remix" }}>
|
|
521
|
-
<SelectTrigger className="w-[140px]">
|
|
522
|
-
<SelectValue placeholder="Chọn framework..." />
|
|
523
|
-
</SelectTrigger>
|
|
524
|
-
<SelectPortal>
|
|
525
|
-
<SelectContent>
|
|
526
|
-
<SelectItem value="next">Next.js</SelectItem>
|
|
527
|
-
<SelectItem value="vite">Vite</SelectItem>
|
|
528
|
-
<SelectItem value="remix">Remix</SelectItem>
|
|
529
|
-
</SelectContent>
|
|
530
|
-
</SelectPortal>
|
|
531
|
-
</Select>
|
|
532
|
-
</div>
|
|
533
|
-
|
|
534
|
-
<div className="flex-1 flex flex-col items-center gap-4">
|
|
535
|
-
<span className="text-sm font-medium text-muted-foreground">
|
|
536
|
-
Có ThemeWrapper
|
|
537
|
-
</span>
|
|
538
|
-
<Select items={{ next: "Next.js", vite: "Vite", remix: "Remix" }}>
|
|
539
|
-
<SelectTrigger className="w-[140px]">
|
|
540
|
-
<SelectValue placeholder="Chọn framework..." />
|
|
541
|
-
</SelectTrigger>
|
|
542
|
-
<SelectPortal>
|
|
543
|
-
<ThemeWrapper>
|
|
544
|
-
<SelectContent>
|
|
545
|
-
<SelectItem value="next">Next.js</SelectItem>
|
|
546
|
-
<SelectItem value="vite">Vite</SelectItem>
|
|
547
|
-
<SelectItem value="remix">Remix</SelectItem>
|
|
548
|
-
</SelectContent>
|
|
549
|
-
</ThemeWrapper>
|
|
550
|
-
</SelectPortal>
|
|
551
|
-
</Select>
|
|
552
|
-
</div>
|
|
553
|
-
</div>
|
|
554
|
-
</ThemeProvider>
|
|
555
|
-
</ExampleSection>
|
|
556
|
-
|
|
557
|
-
{/* DropdownMenu */}
|
|
558
|
-
<ExampleSection
|
|
559
|
-
fullWidth
|
|
560
|
-
label="DropdownMenu"
|
|
561
|
-
description="DropdownMenu thường dùng cho navigation actions (profile, settings, logout). Khi hover qua từng item, màu highlight cũng lấy từ CSS variable — thiếu ThemeWrapper, cả nền menu lẫn trạng thái hover cùng lúc đều hiển thị sai màu."
|
|
562
|
-
codeString={`<ThemeProvider defaultMode="dark">
|
|
563
|
-
<div className="w-full flex gap-10 rounded-xl border border-border bg-background p-10">
|
|
564
|
-
<div className="flex-1 flex flex-col items-center gap-4">
|
|
565
|
-
<span className="text-sm font-medium text-muted-foreground">
|
|
566
|
-
Không dùng ThemeWrapper
|
|
567
|
-
</span>
|
|
568
|
-
<DropdownMenu>
|
|
569
|
-
<DropdownMenuTrigger
|
|
570
|
-
render={
|
|
571
|
-
<Button variant="outline">
|
|
572
|
-
Open Dropdown
|
|
573
|
-
</Button>
|
|
574
|
-
}
|
|
575
|
-
/>
|
|
576
|
-
<DropdownMenuPortal>
|
|
577
|
-
<DropdownMenuContent className="w-48">
|
|
578
|
-
<DropdownMenuGroup>
|
|
579
|
-
<DropdownMenuLabel>
|
|
580
|
-
Tài khoản
|
|
581
|
-
</DropdownMenuLabel>
|
|
582
|
-
<DropdownMenuItem>
|
|
583
|
-
Trang cá nhân
|
|
584
|
-
</DropdownMenuItem>
|
|
585
|
-
<DropdownMenuItem>Cài đặt</DropdownMenuItem>
|
|
586
|
-
</DropdownMenuGroup>
|
|
587
|
-
<DropdownMenuSeparator />
|
|
588
|
-
<DropdownMenuItem>Đăng xuất</DropdownMenuItem>
|
|
589
|
-
</DropdownMenuContent>
|
|
590
|
-
</DropdownMenuPortal>
|
|
591
|
-
</DropdownMenu>
|
|
592
|
-
</div>
|
|
593
|
-
|
|
594
|
-
<div className="flex-1 flex flex-col items-center gap-4">
|
|
595
|
-
<span className="text-sm font-medium text-muted-foreground">
|
|
596
|
-
Có ThemeWrapper
|
|
597
|
-
</span>
|
|
598
|
-
<DropdownMenu>
|
|
599
|
-
<DropdownMenuTrigger
|
|
600
|
-
render={
|
|
601
|
-
<Button variant="outline">
|
|
602
|
-
Open Dropdown
|
|
603
|
-
</Button>
|
|
604
|
-
}
|
|
605
|
-
/>
|
|
606
|
-
<DropdownMenuPortal>
|
|
607
|
-
<ThemeWrapper>
|
|
608
|
-
<DropdownMenuContent className="w-48">
|
|
609
|
-
<DropdownMenuGroup>
|
|
610
|
-
<DropdownMenuLabel>
|
|
611
|
-
Tài khoản
|
|
612
|
-
</DropdownMenuLabel>
|
|
613
|
-
<DropdownMenuItem>
|
|
614
|
-
Trang cá nhân
|
|
615
|
-
</DropdownMenuItem>
|
|
616
|
-
<DropdownMenuItem>
|
|
617
|
-
Cài đặt
|
|
618
|
-
</DropdownMenuItem>
|
|
619
|
-
</DropdownMenuGroup>
|
|
620
|
-
<DropdownMenuSeparator />
|
|
621
|
-
<DropdownMenuItem>
|
|
622
|
-
Đăng xuất
|
|
623
|
-
</DropdownMenuItem>
|
|
624
|
-
</DropdownMenuContent>
|
|
625
|
-
</ThemeWrapper>
|
|
626
|
-
</DropdownMenuPortal>
|
|
627
|
-
</DropdownMenu>
|
|
628
|
-
</div>
|
|
629
|
-
</div>
|
|
630
|
-
</ThemeProvider>`}
|
|
631
|
-
>
|
|
632
|
-
<ThemeProvider defaultMode="dark">
|
|
633
|
-
<div className="w-full flex gap-10 rounded-xl border border-border bg-background p-10">
|
|
634
|
-
<div className="flex-1 flex flex-col items-center gap-4">
|
|
635
|
-
<span className="text-sm font-medium text-muted-foreground">
|
|
636
|
-
Không dùng ThemeWrapper
|
|
637
|
-
</span>
|
|
638
|
-
<DropdownMenu>
|
|
639
|
-
<DropdownMenuTrigger
|
|
640
|
-
render={<Button variant="outline">Open Dropdown</Button>}
|
|
641
|
-
/>
|
|
642
|
-
<DropdownMenuPortal>
|
|
643
|
-
<DropdownMenuContent className="w-48">
|
|
644
|
-
<DropdownMenuGroup>
|
|
645
|
-
<DropdownMenuLabel>Tài khoản</DropdownMenuLabel>
|
|
646
|
-
<DropdownMenuItem>Trang cá nhân</DropdownMenuItem>
|
|
647
|
-
<DropdownMenuItem>Cài đặt</DropdownMenuItem>
|
|
648
|
-
</DropdownMenuGroup>
|
|
649
|
-
<DropdownMenuSeparator />
|
|
650
|
-
<DropdownMenuItem>Đăng xuất</DropdownMenuItem>
|
|
651
|
-
</DropdownMenuContent>
|
|
652
|
-
</DropdownMenuPortal>
|
|
653
|
-
</DropdownMenu>
|
|
654
|
-
</div>
|
|
655
|
-
|
|
656
|
-
<div className="flex-1 flex flex-col items-center gap-4">
|
|
657
|
-
<span className="text-sm font-medium text-muted-foreground">
|
|
658
|
-
Có ThemeWrapper
|
|
659
|
-
</span>
|
|
660
|
-
<DropdownMenu>
|
|
661
|
-
<DropdownMenuTrigger
|
|
662
|
-
render={<Button variant="outline">Open Dropdown</Button>}
|
|
663
|
-
/>
|
|
664
|
-
<DropdownMenuPortal>
|
|
665
|
-
<ThemeWrapper>
|
|
666
|
-
<DropdownMenuContent className="w-48">
|
|
667
|
-
<DropdownMenuGroup>
|
|
668
|
-
<DropdownMenuLabel>Tài khoản</DropdownMenuLabel>
|
|
669
|
-
<DropdownMenuItem>Trang cá nhân</DropdownMenuItem>
|
|
670
|
-
<DropdownMenuItem>Cài đặt</DropdownMenuItem>
|
|
671
|
-
</DropdownMenuGroup>
|
|
672
|
-
<DropdownMenuSeparator />
|
|
673
|
-
<DropdownMenuItem>Đăng xuất</DropdownMenuItem>
|
|
674
|
-
</DropdownMenuContent>
|
|
675
|
-
</ThemeWrapper>
|
|
676
|
-
</DropdownMenuPortal>
|
|
677
|
-
</DropdownMenu>
|
|
678
|
-
</div>
|
|
679
|
-
</div>
|
|
680
|
-
</ThemeProvider>
|
|
681
|
-
</ExampleSection>
|
|
682
|
-
|
|
683
|
-
{/* Dialog */}
|
|
684
|
-
<ExampleSection
|
|
685
|
-
fullWidth
|
|
686
|
-
label="Dialog"
|
|
687
|
-
description="Dialog là trường hợp phức tạp nhất: portal chứa cả Overlay backdrop lẫn Content panel. Cả hai layer đều cần được bọc trong ThemeWrapper để đảm bảo đồng bộ hoàn toàn."
|
|
688
|
-
codeString={`<ThemeProvider defaultMode="dark">
|
|
689
|
-
<div className="w-full flex gap-10 rounded-xl border border-border bg-background p-10">
|
|
690
|
-
<div className="flex-1 flex flex-col items-center gap-4">
|
|
691
|
-
<span className="text-sm font-medium text-muted-foreground">
|
|
692
|
-
Không dùng ThemeWrapper
|
|
693
|
-
</span>
|
|
694
|
-
<Dialog>
|
|
695
|
-
<DialogTrigger
|
|
696
|
-
render={
|
|
697
|
-
<Button variant="outline">Open Dialog</Button>
|
|
698
|
-
}
|
|
699
|
-
/>
|
|
700
|
-
<DialogPortal>
|
|
701
|
-
<DialogContent className="sm:max-w-md">
|
|
702
|
-
<DialogHeader>
|
|
703
|
-
<DialogTitle>Mất Dark Mode!</DialogTitle>
|
|
704
|
-
<DialogDescription>
|
|
705
|
-
Lỗi: Hiển thị giao diện sáng mặc định.
|
|
706
|
-
</DialogDescription>
|
|
707
|
-
</DialogHeader>
|
|
708
|
-
</DialogContent>
|
|
709
|
-
</DialogPortal>
|
|
710
|
-
</Dialog>
|
|
711
|
-
</div>
|
|
712
|
-
|
|
713
|
-
<div className="flex-1 flex flex-col items-center gap-4">
|
|
714
|
-
<span className="text-sm font-medium text-muted-foreground">
|
|
715
|
-
Có ThemeWrapper
|
|
716
|
-
</span>
|
|
717
|
-
<Dialog>
|
|
718
|
-
<DialogTrigger
|
|
719
|
-
render={
|
|
720
|
-
<Button variant="outline">Open Dialog</Button>
|
|
721
|
-
}
|
|
722
|
-
/>
|
|
723
|
-
<DialogPortal>
|
|
724
|
-
<ThemeWrapper>
|
|
725
|
-
<DialogContent className="sm:max-w-md">
|
|
726
|
-
<DialogHeader>
|
|
727
|
-
<DialogTitle>
|
|
728
|
-
Màu tối toàn vẹn
|
|
729
|
-
</DialogTitle>
|
|
730
|
-
<DialogDescription>
|
|
731
|
-
Ngay cả khi mở Dialog bọc ngoài toàn bộ
|
|
732
|
-
màn hình, Portal Tunnel vẫn bảo vệ được
|
|
733
|
-
scope Dark Mode.
|
|
734
|
-
</DialogDescription>
|
|
735
|
-
</DialogHeader>
|
|
736
|
-
</DialogContent>
|
|
737
|
-
</ThemeWrapper>
|
|
738
|
-
</DialogPortal>
|
|
739
|
-
</Dialog>
|
|
740
|
-
</div>
|
|
741
|
-
</div>
|
|
742
|
-
</ThemeProvider>`}
|
|
743
|
-
>
|
|
744
|
-
<ThemeProvider defaultMode="dark">
|
|
745
|
-
<div className="w-full flex gap-10 rounded-xl border border-border bg-background p-10">
|
|
746
|
-
<div className="flex-1 flex flex-col items-center gap-4">
|
|
747
|
-
<span className="text-sm font-medium text-muted-foreground">
|
|
748
|
-
Không dùng ThemeWrapper
|
|
749
|
-
</span>
|
|
750
|
-
<Dialog>
|
|
751
|
-
<DialogTrigger
|
|
752
|
-
render={<Button variant="outline">Open Dialog</Button>}
|
|
753
|
-
/>
|
|
754
|
-
<DialogPortal>
|
|
755
|
-
<DialogContent className="sm:max-w-md">
|
|
756
|
-
<DialogHeader>
|
|
757
|
-
<DialogTitle>Mất Dark Mode!</DialogTitle>
|
|
758
|
-
<DialogDescription>
|
|
759
|
-
Lỗi: Hiển thị giao diện sáng mặc định.
|
|
760
|
-
</DialogDescription>
|
|
761
|
-
</DialogHeader>
|
|
762
|
-
</DialogContent>
|
|
763
|
-
</DialogPortal>
|
|
764
|
-
</Dialog>
|
|
765
|
-
</div>
|
|
766
|
-
|
|
767
|
-
<div className="flex-1 flex flex-col items-center gap-4">
|
|
768
|
-
<span className="text-sm font-medium text-muted-foreground">
|
|
769
|
-
Có ThemeWrapper
|
|
770
|
-
</span>
|
|
771
|
-
<Dialog>
|
|
772
|
-
<DialogTrigger
|
|
773
|
-
render={<Button variant="outline">Open Dialog</Button>}
|
|
774
|
-
/>
|
|
775
|
-
<DialogPortal>
|
|
776
|
-
<ThemeWrapper>
|
|
777
|
-
<DialogContent className="sm:max-w-md">
|
|
778
|
-
<DialogHeader>
|
|
779
|
-
<DialogTitle>Màu tối toàn vẹn</DialogTitle>
|
|
780
|
-
<DialogDescription>
|
|
781
|
-
Ngay cả khi mở Dialog bọc ngoài toàn bộ màn hình,
|
|
782
|
-
Portal Tunnel vẫn bảo vệ được scope Dark Mode.
|
|
783
|
-
</DialogDescription>
|
|
784
|
-
</DialogHeader>
|
|
785
|
-
</DialogContent>
|
|
786
|
-
</ThemeWrapper>
|
|
787
|
-
</DialogPortal>
|
|
788
|
-
</Dialog>
|
|
789
|
-
</div>
|
|
790
|
-
</div>
|
|
791
|
-
</ThemeProvider>
|
|
792
|
-
</ExampleSection>
|
|
793
|
-
|
|
794
|
-
{/* Sheet */}
|
|
795
|
-
<ExampleSection
|
|
796
|
-
fullWidth
|
|
797
|
-
label="Sheet"
|
|
798
|
-
description="Sheet trượt từ cạnh màn hình và phủ một vùng diện tích lớn — lỗi theme dễ nhận biết nhất vì panel thông thường chứa layout phức tạp hơn Popover hay Tooltip."
|
|
799
|
-
codeString={`<ThemeProvider defaultMode="dark">
|
|
800
|
-
<div className="w-full flex gap-10 rounded-xl border border-border bg-background p-10">
|
|
801
|
-
<div className="flex-1 flex flex-col items-center gap-4">
|
|
802
|
-
<span className="text-sm font-medium text-muted-foreground">
|
|
803
|
-
Không dùng ThemeWrapper
|
|
804
|
-
</span>
|
|
805
|
-
<Sheet>
|
|
806
|
-
<SheetTrigger
|
|
807
|
-
render={
|
|
808
|
-
<Button variant="outline">Open Sheet</Button>
|
|
809
|
-
}
|
|
810
|
-
/>
|
|
811
|
-
<SheetPortal>
|
|
812
|
-
<SheetContent side="right">
|
|
813
|
-
<SheetHeader>
|
|
814
|
-
<SheetTitle>Mất Dark Mode!</SheetTitle>
|
|
815
|
-
</SheetHeader>
|
|
816
|
-
</SheetContent>
|
|
817
|
-
</SheetPortal>
|
|
818
|
-
</Sheet>
|
|
819
|
-
</div>
|
|
820
|
-
|
|
821
|
-
<div className="flex-1 flex flex-col items-center gap-4">
|
|
822
|
-
<span className="text-sm font-medium text-muted-foreground">
|
|
823
|
-
Có ThemeWrapper
|
|
824
|
-
</span>
|
|
825
|
-
<Sheet>
|
|
826
|
-
<SheetTrigger
|
|
827
|
-
render={
|
|
828
|
-
<Button variant="outline">Open Sheet</Button>
|
|
829
|
-
}
|
|
830
|
-
/>
|
|
831
|
-
<SheetPortal>
|
|
832
|
-
<ThemeWrapper>
|
|
833
|
-
<SheetContent side="right">
|
|
834
|
-
<SheetHeader>
|
|
835
|
-
<SheetTitle>Sheet Demo</SheetTitle>
|
|
836
|
-
</SheetHeader>
|
|
837
|
-
<div className="py-4">
|
|
838
|
-
Màu tối đồng bộ hoàn hảo!
|
|
839
|
-
</div>
|
|
840
|
-
</SheetContent>
|
|
841
|
-
</ThemeWrapper>
|
|
842
|
-
</SheetPortal>
|
|
843
|
-
</Sheet>
|
|
844
|
-
</div>
|
|
845
|
-
</div>
|
|
846
|
-
</ThemeProvider>`}
|
|
847
|
-
>
|
|
848
|
-
<ThemeProvider defaultMode="dark">
|
|
849
|
-
<div className="w-full flex gap-10 rounded-xl border border-border bg-background p-10">
|
|
850
|
-
<div className="flex-1 flex flex-col items-center gap-4">
|
|
851
|
-
<span className="text-sm font-medium text-muted-foreground">
|
|
852
|
-
Không dùng ThemeWrapper
|
|
853
|
-
</span>
|
|
854
|
-
<Sheet>
|
|
855
|
-
<SheetTrigger
|
|
856
|
-
render={<Button variant="outline">Open Sheet</Button>}
|
|
857
|
-
/>
|
|
858
|
-
<SheetPortal>
|
|
859
|
-
<SheetContent side="right">
|
|
860
|
-
<SheetHeader>
|
|
861
|
-
<SheetTitle>Mất Dark Mode!</SheetTitle>
|
|
862
|
-
</SheetHeader>
|
|
863
|
-
</SheetContent>
|
|
864
|
-
</SheetPortal>
|
|
865
|
-
</Sheet>
|
|
866
|
-
</div>
|
|
867
|
-
|
|
868
|
-
<div className="flex-1 flex flex-col items-center gap-4">
|
|
869
|
-
<span className="text-sm font-medium text-muted-foreground">
|
|
870
|
-
Có ThemeWrapper
|
|
871
|
-
</span>
|
|
872
|
-
<Sheet>
|
|
873
|
-
<SheetTrigger
|
|
874
|
-
render={<Button variant="outline">Open Sheet</Button>}
|
|
875
|
-
/>
|
|
876
|
-
<SheetPortal>
|
|
877
|
-
<ThemeWrapper>
|
|
878
|
-
<SheetContent side="right">
|
|
879
|
-
<SheetHeader>
|
|
880
|
-
<SheetTitle>Sheet Demo</SheetTitle>
|
|
881
|
-
</SheetHeader>
|
|
882
|
-
<div className="py-4">Màu tối đồng bộ hoàn hảo!</div>
|
|
883
|
-
</SheetContent>
|
|
884
|
-
</ThemeWrapper>
|
|
885
|
-
</SheetPortal>
|
|
886
|
-
</Sheet>
|
|
887
|
-
</div>
|
|
888
|
-
</div>
|
|
889
|
-
</ThemeProvider>
|
|
890
|
-
</ExampleSection>
|
|
891
|
-
</div>
|
|
892
|
-
);
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
// ──────────────────────────────────────────────────────────
|
|
896
|
-
// Entry point
|
|
897
|
-
// ──────────────────────────────────────────────────────────
|
|
898
|
-
export default function ThemeProviderShowcase() {
|
|
899
|
-
return (
|
|
900
|
-
<Showcase
|
|
901
|
-
title="Theme Provider"
|
|
902
|
-
description="Context provider cho light/dark mode — uncontrolled (defaultMode) hoặc controlled (mode + onModeChange). Portal-safe qua ThemeWrapper."
|
|
903
|
-
generalConcept={
|
|
904
|
-
<ShowcaseDocs>
|
|
905
|
-
<DocsH3>ThemeProvider</DocsH3>
|
|
906
|
-
<DocsP>
|
|
907
|
-
<DocsCode>ThemeProvider</DocsCode> bọc children trong một{" "}
|
|
908
|
-
<DocsCode>{"div[display:contents]"}</DocsCode> và gắn class{" "}
|
|
909
|
-
<DocsCode>light</DocsCode> hoặc <DocsCode>dark</DocsCode> lên đó,
|
|
910
|
-
kích hoạt CSS variable cascade mà không ảnh hưởng layout. Hỗ trợ hai
|
|
911
|
-
chế độ: <strong>Uncontrolled</strong> (truyền{" "}
|
|
912
|
-
<DocsCode>defaultMode</DocsCode> — tự quản lý state) và{" "}
|
|
913
|
-
<strong>Controlled</strong> (truyền <DocsCode>mode</DocsCode> +{" "}
|
|
914
|
-
<DocsCode>onModeChange</DocsCode> — state do component cha sở hữu).
|
|
915
|
-
Việc đọc localStorage hoặc system preference thuộc về tầng Macro.
|
|
916
|
-
</DocsP>
|
|
917
|
-
|
|
918
|
-
<DocsH3>ThemeWrapper & Portal — Hard Requirement</DocsH3>
|
|
919
|
-
<DocsP>
|
|
920
|
-
CSS variable cascade theo <strong>DOM tree</strong>, không theo
|
|
921
|
-
React component tree. Khi một component render qua Portal (ra{" "}
|
|
922
|
-
<DocsCode>document.body</DocsCode>), nó thoát khỏi hoàn toàn chuỗi
|
|
923
|
-
DOM ancestry — không còn tổ tiên nào mang class{" "}
|
|
924
|
-
<DocsCode>.dark</DocsCode>, và mọi CSS variable rơi thẳng về giá trị{" "}
|
|
925
|
-
<DocsCode>:root</DocsCode> (thường là light).
|
|
926
|
-
</DocsP>
|
|
927
|
-
<DocsP>
|
|
928
|
-
<DocsCode>ThemeWrapper</DocsCode> giải quyết vấn đề này bằng cách
|
|
929
|
-
đọc <DocsCode>useTheme()</DocsCode> và re-apply class theme vào một
|
|
930
|
-
container mới ngay bên trong Portal — tái thiết lập CSS variable
|
|
931
|
-
cascade cho toàn bộ nội dung bên trong.{" "}
|
|
932
|
-
<strong>
|
|
933
|
-
Đây không phải best practice tuỳ chọn — đây là yêu cầu bắt buộc
|
|
934
|
-
</strong>{" "}
|
|
935
|
-
khi dùng Portal trong bất kỳ scoped theme section nào. Bỏ qua{" "}
|
|
936
|
-
<DocsCode>ThemeWrapper</DocsCode> sẽ không gây lỗi compile hay
|
|
937
|
-
runtime warning, nhưng UI sẽ hiển thị sai màu âm thầm.
|
|
938
|
-
</DocsP>
|
|
939
|
-
<DocsP>
|
|
940
|
-
Trách nhiệm đặt <DocsCode>ThemeWrapper</DocsCode> không thể delegate
|
|
941
|
-
tuỳ tiện: Micro component không được tự ôm nó bên trong (vi phạm
|
|
942
|
-
nguyên tắc pure composition). Tầng nào kiểm soát Portal — Macro
|
|
943
|
-
component hoặc consumer trực tiếp — tầng đó phải tự đặt{" "}
|
|
944
|
-
<DocsCode>ThemeWrapper</DocsCode> ngay sau Portal boundary.
|
|
945
|
-
</DocsP>
|
|
946
|
-
</ShowcaseDocs>
|
|
947
|
-
}
|
|
948
|
-
tabs={[
|
|
949
|
-
{
|
|
950
|
-
label: "Micro (Primitive)",
|
|
951
|
-
content: <ThemeProviderMicroShowcase />,
|
|
952
|
-
},
|
|
953
|
-
]}
|
|
954
|
-
/>
|
|
955
|
-
);
|
|
956
|
-
}
|