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.
Files changed (208) hide show
  1. package/README.md +1 -0
  2. package/bin/cli.js +15 -5
  3. package/dist/components/macro/accordion-preset.d.ts.map +1 -1
  4. package/dist/components/macro/alert-dialog-preset.d.ts.map +1 -1
  5. package/dist/components/macro/alert-preset.d.ts.map +1 -1
  6. package/dist/components/macro/avatar-preset.d.ts.map +1 -1
  7. package/dist/components/macro/breadcrumb-preset.d.ts.map +1 -1
  8. package/dist/components/macro/card-preset.d.ts.map +1 -1
  9. package/dist/components/macro/carousel-preset.d.ts.map +1 -1
  10. package/dist/components/macro/checkbox-preset.d.ts.map +1 -1
  11. package/dist/components/macro/combobox-preset.d.ts.map +1 -1
  12. package/dist/components/macro/command-preset.d.ts +1 -1
  13. package/dist/components/macro/command-preset.d.ts.map +1 -1
  14. package/dist/components/macro/date-picker.d.ts.map +1 -1
  15. package/dist/components/macro/dialog-preset.d.ts.map +1 -1
  16. package/dist/components/macro/empty-preset.d.ts +1 -1
  17. package/dist/components/macro/empty-preset.d.ts.map +1 -1
  18. package/dist/components/macro/field-preset.d.ts.map +1 -1
  19. package/dist/components/macro/input-otp-preset.d.ts.map +1 -1
  20. package/dist/components/macro/input-preset.d.ts.map +1 -1
  21. package/dist/components/macro/pagination-preset.d.ts.map +1 -1
  22. package/dist/components/macro/progress-preset.d.ts.map +1 -1
  23. package/dist/components/macro/radio-group-preset.d.ts.map +1 -1
  24. package/dist/components/macro/select-preset.d.ts.map +1 -1
  25. package/dist/components/macro/slider-preset.d.ts.map +1 -1
  26. package/dist/components/macro/switch-preset.d.ts.map +1 -1
  27. package/dist/components/macro/tabs-preset.d.ts.map +1 -1
  28. package/dist/components/micro/alert-dialog.d.ts +4 -4
  29. package/dist/components/micro/alert-dialog.d.ts.map +1 -1
  30. package/dist/components/micro/combobox.d.ts +4 -3
  31. package/dist/components/micro/combobox.d.ts.map +1 -1
  32. package/dist/components/micro/context-menu.d.ts +4 -6
  33. package/dist/components/micro/context-menu.d.ts.map +1 -1
  34. package/dist/components/micro/dialog.d.ts +4 -3
  35. package/dist/components/micro/dialog.d.ts.map +1 -1
  36. package/dist/components/micro/drawer.d.ts +4 -3
  37. package/dist/components/micro/drawer.d.ts.map +1 -1
  38. package/dist/components/micro/dropdown-menu.d.ts +7 -4
  39. package/dist/components/micro/dropdown-menu.d.ts.map +1 -1
  40. package/dist/components/micro/hover-card.d.ts +4 -3
  41. package/dist/components/micro/hover-card.d.ts.map +1 -1
  42. package/dist/components/micro/menubar.d.ts +2 -3
  43. package/dist/components/micro/menubar.d.ts.map +1 -1
  44. package/dist/components/micro/navigation-menu.d.ts +4 -3
  45. package/dist/components/micro/navigation-menu.d.ts.map +1 -1
  46. package/dist/components/micro/popover.d.ts +4 -3
  47. package/dist/components/micro/popover.d.ts.map +1 -1
  48. package/dist/components/micro/select.d.ts +4 -3
  49. package/dist/components/micro/select.d.ts.map +1 -1
  50. package/dist/components/micro/sheet.d.ts +3 -3
  51. package/dist/components/micro/sheet.d.ts.map +1 -1
  52. package/dist/components/micro/tooltip.d.ts +4 -3
  53. package/dist/components/micro/tooltip.d.ts.map +1 -1
  54. package/dist/dev/App.d.ts.map +1 -1
  55. package/dist/dev/data.d.ts.map +1 -1
  56. package/dist/dev/showcase/accordion.d.ts.map +1 -1
  57. package/dist/dev/showcase/alert-dialog.d.ts.map +1 -1
  58. package/dist/dev/showcase/alert.d.ts.map +1 -1
  59. package/dist/dev/showcase/aspect-ratio.d.ts.map +1 -1
  60. package/dist/dev/showcase/avatar.d.ts.map +1 -1
  61. package/dist/dev/showcase/badge.d.ts.map +1 -1
  62. package/dist/dev/showcase/breadcrumb.d.ts.map +1 -1
  63. package/dist/dev/showcase/button.d.ts.map +1 -1
  64. package/dist/dev/showcase/calendar.d.ts.map +1 -1
  65. package/dist/dev/showcase/card.d.ts.map +1 -1
  66. package/dist/dev/showcase/carousel.d.ts.map +1 -1
  67. package/dist/dev/showcase/chart.d.ts.map +1 -1
  68. package/dist/dev/showcase/checkbox.d.ts.map +1 -1
  69. package/dist/dev/showcase/collapsible.d.ts.map +1 -1
  70. package/dist/dev/showcase/combobox.d.ts.map +1 -1
  71. package/dist/dev/showcase/command.d.ts.map +1 -1
  72. package/dist/dev/showcase/context-menu.d.ts.map +1 -1
  73. package/dist/dev/showcase/dialog.d.ts.map +1 -1
  74. package/dist/dev/showcase/drawer.d.ts.map +1 -1
  75. package/dist/dev/showcase/dropdown-menu.d.ts.map +1 -1
  76. package/dist/dev/showcase/empty.d.ts.map +1 -1
  77. package/dist/dev/showcase/hover-card.d.ts.map +1 -1
  78. package/dist/dev/showcase/input-group.d.ts.map +1 -1
  79. package/dist/dev/showcase/input-otp.d.ts.map +1 -1
  80. package/dist/dev/showcase/input.d.ts.map +1 -1
  81. package/dist/dev/showcase/item.d.ts.map +1 -1
  82. package/dist/dev/showcase/kbd.d.ts.map +1 -1
  83. package/dist/dev/showcase/label.d.ts.map +1 -1
  84. package/dist/dev/showcase/menubar.d.ts.map +1 -1
  85. package/dist/dev/showcase/navigation-menu.d.ts.map +1 -1
  86. package/dist/dev/showcase/overview.d.ts.map +1 -1
  87. package/dist/dev/showcase/pagination.d.ts.map +1 -1
  88. package/dist/dev/showcase/popover.d.ts.map +1 -1
  89. package/dist/dev/showcase/progress.d.ts.map +1 -1
  90. package/dist/dev/showcase/radio-group.d.ts.map +1 -1
  91. package/dist/dev/showcase/resizable.d.ts.map +1 -1
  92. package/dist/dev/showcase/scroll-area.d.ts.map +1 -1
  93. package/dist/dev/showcase/select.d.ts.map +1 -1
  94. package/dist/dev/showcase/separator.d.ts.map +1 -1
  95. package/dist/dev/showcase/sheet.d.ts.map +1 -1
  96. package/dist/dev/showcase/sidebar.d.ts.map +1 -1
  97. package/dist/dev/showcase/skeleton.d.ts.map +1 -1
  98. package/dist/dev/showcase/slider.d.ts.map +1 -1
  99. package/dist/dev/showcase/sonner.d.ts.map +1 -1
  100. package/dist/dev/showcase/spinner.d.ts.map +1 -1
  101. package/dist/dev/showcase/switch.d.ts.map +1 -1
  102. package/dist/dev/showcase/table.d.ts.map +1 -1
  103. package/dist/dev/showcase/tabs.d.ts.map +1 -1
  104. package/dist/dev/showcase/textarea.d.ts.map +1 -1
  105. package/dist/dev/showcase/toast.d.ts.map +1 -1
  106. package/dist/dev/showcase/toggle-group.d.ts.map +1 -1
  107. package/dist/dev/showcase/toggle.d.ts.map +1 -1
  108. package/dist/dev/showcase/tooltip.d.ts.map +1 -1
  109. package/dist/index.cjs +256 -256
  110. package/dist/index.cjs.map +1 -1
  111. package/dist/index.js +257 -244
  112. package/dist/index.js.map +1 -1
  113. package/package.json +2 -2
  114. package/src/components/macro/accordion-preset.test.tsx +1 -2
  115. package/src/components/macro/accordion-preset.tsx +1 -2
  116. package/src/components/macro/alert-dialog-preset.tsx +7 -12
  117. package/src/components/macro/alert-preset.tsx +1 -2
  118. package/src/components/macro/avatar-preset.tsx +2 -4
  119. package/src/components/macro/breadcrumb-preset.tsx +1 -2
  120. package/src/components/macro/card-preset.tsx +1 -2
  121. package/src/components/macro/carousel-preset.tsx +1 -2
  122. package/src/components/macro/checkbox-preset.tsx +1 -2
  123. package/src/components/macro/combobox-preset.tsx +8 -14
  124. package/src/components/macro/command-preset.tsx +18 -24
  125. package/src/components/macro/date-picker.tsx +14 -23
  126. package/src/components/macro/dialog-preset.tsx +45 -49
  127. package/src/components/macro/empty-preset.tsx +2 -4
  128. package/src/components/macro/field-preset.tsx +1 -2
  129. package/src/components/macro/input-otp-preset.tsx +1 -2
  130. package/src/components/macro/input-preset.tsx +1 -2
  131. package/src/components/macro/pagination-preset.tsx +3 -6
  132. package/src/components/macro/progress-preset.tsx +1 -2
  133. package/src/components/macro/radio-group-preset.tsx +2 -4
  134. package/src/components/macro/select-preset.tsx +7 -12
  135. package/src/components/macro/slider-preset.tsx +1 -2
  136. package/src/components/macro/switch-preset.tsx +1 -2
  137. package/src/components/macro/tabs-preset.tsx +1 -2
  138. package/src/components/micro/alert-dialog.tsx +16 -14
  139. package/src/components/micro/combobox.tsx +28 -22
  140. package/src/components/micro/context-menu.tsx +52 -36
  141. package/src/components/micro/dialog.tsx +23 -19
  142. package/src/components/micro/drawer.tsx +23 -19
  143. package/src/components/micro/dropdown-menu.tsx +23 -18
  144. package/src/components/micro/hover-card.tsx +31 -23
  145. package/src/components/micro/menubar.tsx +0 -8
  146. package/src/components/micro/navigation-menu.tsx +26 -19
  147. package/src/components/micro/popover.tsx +31 -25
  148. package/src/components/micro/select.tsx +36 -29
  149. package/src/components/micro/sheet.tsx +36 -31
  150. package/src/components/micro/tooltip.tsx +28 -23
  151. package/src/dev/App.tsx +36 -10
  152. package/src/dev/data.ts +0 -8
  153. package/src/dev/showcase/accordion.tsx +47 -94
  154. package/src/dev/showcase/alert-dialog.tsx +27 -39
  155. package/src/dev/showcase/alert.tsx +5 -10
  156. package/src/dev/showcase/aspect-ratio.tsx +1 -2
  157. package/src/dev/showcase/avatar.tsx +5 -10
  158. package/src/dev/showcase/badge.tsx +2 -4
  159. package/src/dev/showcase/breadcrumb.tsx +8 -11
  160. package/src/dev/showcase/button.tsx +2 -4
  161. package/src/dev/showcase/calendar.tsx +5 -10
  162. package/src/dev/showcase/card.tsx +4 -8
  163. package/src/dev/showcase/carousel.tsx +40 -80
  164. package/src/dev/showcase/chart.tsx +7 -16
  165. package/src/dev/showcase/checkbox.tsx +7 -14
  166. package/src/dev/showcase/collapsible.tsx +3 -6
  167. package/src/dev/showcase/combobox.tsx +27 -43
  168. package/src/dev/showcase/command.tsx +4 -8
  169. package/src/dev/showcase/context-menu.tsx +13 -19
  170. package/src/dev/showcase/dialog.tsx +19 -33
  171. package/src/dev/showcase/drawer.tsx +19 -27
  172. package/src/dev/showcase/dropdown-menu.tsx +19 -29
  173. package/src/dev/showcase/empty.tsx +3 -6
  174. package/src/dev/showcase/hover-card.tsx +24 -36
  175. package/src/dev/showcase/input-group.tsx +4 -8
  176. package/src/dev/showcase/input-otp.tsx +5 -10
  177. package/src/dev/showcase/input.tsx +5 -10
  178. package/src/dev/showcase/item.tsx +3 -6
  179. package/src/dev/showcase/kbd.tsx +12 -16
  180. package/src/dev/showcase/label.tsx +2 -4
  181. package/src/dev/showcase/menubar.tsx +17 -19
  182. package/src/dev/showcase/navigation-menu.tsx +12 -15
  183. package/src/dev/showcase/overview.tsx +17 -30
  184. package/src/dev/showcase/pagination.tsx +4 -8
  185. package/src/dev/showcase/popover.tsx +21 -27
  186. package/src/dev/showcase/progress.tsx +4 -8
  187. package/src/dev/showcase/radio-group.tsx +8 -16
  188. package/src/dev/showcase/resizable.tsx +2 -4
  189. package/src/dev/showcase/scroll-area.tsx +1 -2
  190. package/src/dev/showcase/select.tsx +27 -39
  191. package/src/dev/showcase/separator.tsx +1 -2
  192. package/src/dev/showcase/sheet.tsx +22 -33
  193. package/src/dev/showcase/sidebar.tsx +2 -4
  194. package/src/dev/showcase/skeleton.tsx +1 -2
  195. package/src/dev/showcase/slider.tsx +5 -10
  196. package/src/dev/showcase/sonner.tsx +18 -38
  197. package/src/dev/showcase/spinner.tsx +2 -4
  198. package/src/dev/showcase/switch.tsx +6 -12
  199. package/src/dev/showcase/table.tsx +3 -6
  200. package/src/dev/showcase/tabs.tsx +20 -40
  201. package/src/dev/showcase/textarea.tsx +5 -10
  202. package/src/dev/showcase/toast.tsx +4 -10
  203. package/src/dev/showcase/toggle-group.tsx +4 -8
  204. package/src/dev/showcase/toggle.tsx +2 -4
  205. package/src/dev/showcase/tooltip.tsx +34 -43
  206. package/dist/dev/showcase/theme-provider.d.ts +0 -2
  207. package/dist/dev/showcase/theme-provider.d.ts.map +0 -1
  208. 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 &amp; 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
- }