gladvn 0.2.20 → 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 CHANGED
@@ -11,6 +11,7 @@ Designed with strict architectural principles, `gladvn` is optimized not just fo
11
11
  ## ✨ Key Features
12
12
 
13
13
  - **Micro & Macro Architecture**: Cleanly separates primitive UI elements (Micro) from complex, stateful, composite components (Macro).
14
+ - **Zero-Portal API**: Say goodbye to manual `ThemeWrapper` and `*Portal` imports. Overlays (Dialog, Tooltip, Select) automatically tunnel the theme and manage portals out of the box.
14
15
  - **Zero "Magic CSS"**: No arbitrary deep descendant overrides (`[&_p]`, `has-[>div]`). Styling is predictable, slot-based, and relies on strict data-attributes (e.g., `data-active`, `data-disabled`).
15
16
  - **Tailwind CSS v4 Ready**: Fully compatible with the modern `@tailwindcss/postcss` and `@tailwindcss/vite` ecosystem.
16
17
  - **Copy-Paste or Install**: Choose between cloning the source code directly into your repo (the shadcn way) or installing it as a standard npm dependency.
@@ -1 +1 @@
1
- {"version":3,"file":"App.d.ts","sourceRoot":"","sources":["../../src/dev/App.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA2D,MAAM,OAAO,CAAC;AAwEhF,MAAM,CAAC,OAAO,UAAU,GAAG,sBAkR1B"}
1
+ {"version":3,"file":"App.d.ts","sourceRoot":"","sources":["../../src/dev/App.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA2D,MAAM,OAAO,CAAC;AAwEhF,MAAM,CAAC,OAAO,UAAU,GAAG,sBA4S1B"}
@@ -1 +1 @@
1
- {"version":3,"file":"data.d.ts","sourceRoot":"","sources":["../../src/dev/data.ts"],"names":[],"mappings":"AASA,eAAO,MAAM,QAAQ,wDAAyD,CAAC;AAC/E,eAAO,MAAM,MAAM,4FAQT,CAAC;AACX,eAAO,MAAM,KAAK,6BAA8B,CAAC;AAEjD,eAAO,MAAM,GAAG;;;;GAOf,CAAC;AAEF,eAAO,MAAM,KAAK;;;GAKjB,CAAC;AAEF,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAQtB,CAAC;AAEF,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;IA+btB,CAAC"}
1
+ {"version":3,"file":"data.d.ts","sourceRoot":"","sources":["../../src/dev/data.ts"],"names":[],"mappings":"AASA,eAAO,MAAM,QAAQ,wDAAyD,CAAC;AAC/E,eAAO,MAAM,MAAM,4FAQT,CAAC;AACX,eAAO,MAAM,KAAK,6BAA8B,CAAC;AAEjD,eAAO,MAAM,GAAG;;;;GAOf,CAAC;AAEF,eAAO,MAAM,KAAK;;;GAKjB,CAAC;AAEF,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAQtB,CAAC;AAEF,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;IAubtB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gladvn",
3
- "version": "0.2.20",
3
+ "version": "0.2.21",
4
4
  "type": "module",
5
5
  "description": "DO NOT 'npm install'. Use 'npx gladvn init' instead! A highly composable React component library built with Tailwind CSS.",
6
6
  "keywords": [
@@ -15,7 +15,7 @@
15
15
  "homepage": "https://gladvn.vercel.app",
16
16
  "repository": {
17
17
  "type": "git",
18
- "url": "https://github.com/duongacy/gladcn"
18
+ "url": "https://github.com/duongacy/gladvn"
19
19
  },
20
20
  "main": "./dist/index.cjs",
21
21
  "module": "./dist/index.js",
package/src/dev/App.tsx CHANGED
@@ -78,7 +78,14 @@ export default function App() {
78
78
  const [active, setActiveState] = useState(() => {
79
79
  if (typeof window !== "undefined") {
80
80
  const params = new URLSearchParams(window.location.search);
81
- return params.get("component") || "overview";
81
+ const queryComp = params.get("component");
82
+ if (queryComp) {
83
+ // Redirect legacy search params to new path structure automatically
84
+ window.history.replaceState({}, "", `/${queryComp}`);
85
+ return queryComp;
86
+ }
87
+ const pathComp = window.location.pathname.replace(/^\/+/, "");
88
+ return pathComp || "overview";
82
89
  }
83
90
  return "overview";
84
91
  });
@@ -89,7 +96,8 @@ export default function App() {
89
96
  setCmdOpen(false);
90
97
  if (typeof window !== "undefined") {
91
98
  const url = new URL(window.location.href);
92
- url.searchParams.set("component", id);
99
+ url.searchParams.delete("component");
100
+ url.pathname = id === "overview" ? "/" : `/${id}`;
93
101
  window.history.pushState({}, "", url);
94
102
 
95
103
  setTimeout(() => {
@@ -102,20 +110,25 @@ export default function App() {
102
110
  }, []);
103
111
 
104
112
  useEffect(() => {
105
- const params = new URLSearchParams(window.location.search);
106
- const comp = params.get("component");
107
- if (comp) {
108
- setTimeout(() => {
109
- const el = document.getElementById(comp);
110
- if (el) el.scrollIntoView({ behavior: "smooth" });
111
- }, 100);
113
+ if (typeof window !== "undefined") {
114
+ const params = new URLSearchParams(window.location.search);
115
+ const queryComp = params.get("component");
116
+ const comp = queryComp || window.location.pathname.replace(/^\/+/, "");
117
+
118
+ if (comp && comp !== "overview") {
119
+ setTimeout(() => {
120
+ const el = document.getElementById(comp);
121
+ if (el) el.scrollIntoView({ behavior: "smooth" });
122
+ }, 100);
123
+ }
112
124
  }
113
125
  }, []);
114
126
 
115
127
  useEffect(() => {
116
128
  const handlePopState = () => {
117
129
  const params = new URLSearchParams(window.location.search);
118
- const comp = params.get("component") || "overview";
130
+ const queryComp = params.get("component");
131
+ const comp = queryComp || window.location.pathname.replace(/^\/+/, "") || "overview";
119
132
  setActiveState(comp);
120
133
  const el = document.getElementById(comp);
121
134
  if (el) el.scrollIntoView({ behavior: "smooth" });
@@ -148,6 +161,19 @@ export default function App() {
148
161
  return () => document.removeEventListener("keydown", down);
149
162
  }, []);
150
163
 
164
+ useEffect(() => {
165
+ if (typeof window !== "undefined") {
166
+ if (active === "overview") {
167
+ document.title = "gladvn — Tailwind CSS React Components";
168
+ } else {
169
+ const compDef = COMPONENTS.find((c) => c.id === active);
170
+ if (compDef) {
171
+ document.title = `${compDef.label} — gladvn Components`;
172
+ }
173
+ }
174
+ }
175
+ }, [active]);
176
+
151
177
  return (
152
178
  <div className="min-h-screen bg-background text-foreground">
153
179
  {/* Top nav */}
package/src/dev/data.ts CHANGED
@@ -475,14 +475,6 @@ export const COMPONENTS = [
475
475
  status: "stable",
476
476
  hasSize: true,
477
477
  },
478
- {
479
- id: "theme-provider",
480
- category: "Data Display",
481
- label: "Theme Provider",
482
- hasMicro: true,
483
- hasMacro: false,
484
- status: "stable",
485
- },
486
478
  {
487
479
  id: "tooltip",
488
480
  category: "Feedback & Overlays",
@@ -985,7 +985,7 @@ export default function OverviewSection() {
985
985
 
986
986
  <div className="relative z-10 mt-10">
987
987
  <Button
988
- render={<a href="?component=theme-provider" />}
988
+ render={<a href="/dialog" />}
989
989
  variant="outline"
990
990
  color="warning"
991
991
  className="gap-2 font-bold px-6 py-5 rounded-xl border-warning/30 bg-warning/10 hover:bg-warning/20 shadow-sm"
@@ -1078,7 +1078,7 @@ export default function OverviewSection() {
1078
1078
  className="mt-4 rounded-full"
1079
1079
  render={
1080
1080
  <a
1081
- href="https://github.com/duongacy/gladcn"
1081
+ href="https://github.com/duongacy/gladvn"
1082
1082
  target="_blank"
1083
1083
  rel="noreferrer"
1084
1084
  />
@@ -1122,7 +1122,7 @@ export default function OverviewSection() {
1122
1122
  <Button
1123
1123
  render={
1124
1124
  <a
1125
- href="https://github.com/duongacy/gladcn"
1125
+ href="https://github.com/duongacy/gladvn"
1126
1126
  target="_blank"
1127
1127
  rel="noreferrer"
1128
1128
  />
@@ -1,2 +0,0 @@
1
- export default function ThemeProviderShowcase(): import("react").JSX.Element;
2
- //# sourceMappingURL=theme-provider.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"theme-provider.d.ts","sourceRoot":"","sources":["../../../src/dev/showcase/theme-provider.tsx"],"names":[],"mappings":"AAquBA,MAAM,CAAC,OAAO,UAAU,qBAAqB,gCAqD5C"}
@@ -1,795 +0,0 @@
1
- import { useState } from "react";
2
-
3
- import { MoonIcon, SunIcon } from "lucide-react";
4
-
5
- import { Dialog as DialogPrimitive } from "@base-ui/react/dialog";
6
- import { Menu as MenuPrimitive } from "@base-ui/react/menu";
7
- import { Popover as PopoverPrimitive } from "@base-ui/react/popover";
8
- import { Select as SelectPrimitive } from "@base-ui/react/select";
9
- import { Dialog as SheetPrimitive } from "@base-ui/react/dialog";
10
- import { Tooltip as TooltipPrimitive } from "@base-ui/react/tooltip";
11
-
12
- import { Button } from "../../components/micro/button";
13
- import {
14
- Dialog,
15
- DialogContent,
16
- DialogDescription,
17
- DialogHeader,
18
-
19
- DialogTitle,
20
- DialogTrigger } from "../../components/micro/dialog";
21
- import {
22
- DropdownMenu,
23
- DropdownMenuContent,
24
- DropdownMenuGroup,
25
- DropdownMenuItem,
26
- DropdownMenuLabel,
27
-
28
- DropdownMenuSeparator,
29
- DropdownMenuTrigger } from "../../components/micro/dropdown-menu";
30
- import {
31
- Popover,
32
- PopoverContent,
33
-
34
- PopoverTrigger } from "../../components/micro/popover";
35
- import {
36
- Select,
37
- SelectContent,
38
- SelectItem,
39
-
40
- SelectTrigger,
41
- SelectValue } from "../../components/micro/select";
42
- import {
43
- Sheet,
44
- SheetContent,
45
- SheetHeader,
46
-
47
- SheetTitle,
48
- SheetTrigger } from "../../components/micro/sheet";
49
- import {
50
- type ThemeMode,
51
- ThemeProvider,
52
- useTheme } from "../../components/micro/theme-provider";
53
- import {
54
- Tooltip,
55
- TooltipContent,
56
-
57
- TooltipProvider,
58
- TooltipTrigger } from "../../components/micro/tooltip";
59
- import {
60
- DocsCode,
61
- DocsH3,
62
- DocsP,
63
- ExampleSection,
64
- SectionHeader,
65
- Showcase,
66
- ShowcaseDocs } from "../../dev/components/showcase";
67
-
68
- // ──────────────────────────────────────────────────────────
69
- // Demo helpers
70
- // ──────────────────────────────────────────────────────────
71
-
72
- /** Demo card that reads theme from context and shows current mode */
73
- function ThemeAwareCard({ readOnly }: { readOnly?: boolean } = {}) {
74
- const theme = useTheme();
75
-
76
- return (
77
- <div className="rounded-xl border border-border bg-card text-card-foreground p-4 space-y-3">
78
- <div className="flex items-center justify-between">
79
- <span className="text-sm font-medium">Thẻ nội dung</span>
80
- <span className="text-xs px-2 py-0.5 rounded-full bg-muted text-muted-foreground">
81
- {theme?.mode ?? "unknown"}
82
- </span>
83
- </div>
84
- <p className="text-xs text-muted-foreground">
85
- Nền, chữ và viền được điều khiển bởi CSS variable từ ThemeProvider cha.
86
- </p>
87
- {!readOnly && (
88
- <Button
89
- variant="ghost"
90
- size="sm"
91
- iconOnly
92
- aria-label={
93
- theme?.mode === "dark" ? "Chuyển sang Light" : "Chuyển sang Dark"
94
- }
95
- onClick={() =>
96
- theme?.setMode(theme.mode === "dark" ? "light" : "dark")
97
- }
98
- >
99
- {theme?.mode === "dark" ? (
100
- <SunIcon aria-hidden="true" />
101
- ) : (
102
- <MoonIcon aria-hidden="true" />
103
- )}
104
- </Button>
105
- )}
106
- </div>
107
- );
108
- }
109
-
110
- // ──────────────────────────────────────────────────────────
111
- // SECTION 1: Micro Showcase
112
- // ──────────────────────────────────────────────────────────
113
- function ThemeProviderMicroShowcase() {
114
- // Controlled demo state (owned by this parent)
115
- const [controlledMode, setControlledMode] = useState<ThemeMode>("light");
116
-
117
- return (
118
- <div className="space-y-10">
119
- <SectionHeader
120
- title="ThemeProvider"
121
- 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 Library *Portal để tunnel theme qua Portal boundary tự động."
122
- />
123
-
124
- {/* ── Uncontrolled ── */}
125
- <SectionHeader
126
- title="Uncontrolled"
127
- 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."
128
- />
129
- <ExampleSection
130
- label="defaultMode"
131
- 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."
132
- codeString={`function ThemeAwareCard() {
133
- const theme = useTheme();
134
-
135
- return (
136
- <div className="rounded-xl border border-border bg-card text-card-foreground p-4 space-y-3">
137
- <div className="flex items-center justify-between">
138
- <span className="text-sm font-medium">Thẻ nội dung</span>
139
- <span className="text-xs px-2 py-0.5 rounded-full bg-muted text-muted-foreground">
140
- {theme?.mode}
141
- </span>
142
- </div>
143
- <button onClick={() => theme?.setMode(theme.mode === "dark" ? "light" : "dark")}>
144
- Chuyển sang {theme?.mode === "dark" ? "Light" : "Dark"}
145
- </button>
146
- </div>
147
- );
148
- }
149
-
150
- <ThemeProvider defaultMode="light">
151
- <ThemeAwareCard />
152
- </ThemeProvider>`}
153
- >
154
- <ThemeProvider defaultMode="light">
155
- <ThemeAwareCard />
156
- </ThemeProvider>
157
- </ExampleSection>
158
-
159
- {/* ── Controlled ── */}
160
- <SectionHeader
161
- title="Controlled"
162
- 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)."
163
- />
164
- <ExampleSection
165
- label="mode + onModeChange"
166
- 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."
167
- codeString={`const [mode, setMode] = useState<ThemeMode>("light");
168
-
169
- <div className="space-y-3 w-full">
170
- {/* Toggle nằm ở component cha — simulates Macro owning state */}
171
- <div className="flex items-center gap-3 p-3 rounded-lg bg-muted/50 border border-border">
172
- <span className="text-xs text-muted-foreground flex-1">
173
- State ở component cha: <strong>{mode}</strong>
174
- </span>
175
- <Button
176
- size="sm"
177
- variant="outline"
178
- onClick={() => setMode(mode === "dark" ? "light" : "dark")}
179
- >
180
- Toggle từ ngoài
181
- </Button>
182
- </div>
183
-
184
- <ThemeProvider mode={mode} onModeChange={setMode}>
185
- {/* ThemeAwareCard là component đọc useTheme() để hiển thị mode hiện tại */}
186
- <ThemeAwareCard readOnly />
187
- </ThemeProvider>
188
- </div>`}
189
- >
190
- <div className="space-y-3 w-full">
191
- {/* External toggle (simulates Macro owning state) */}
192
- <div className="flex items-center gap-3 p-3 rounded-lg bg-muted/50 border border-border">
193
- <span className="text-xs text-muted-foreground flex-1">
194
- State ở component cha: <DocsCode>{controlledMode}</DocsCode>
195
- </span>
196
- <Button
197
- size="sm"
198
- variant="outline"
199
- onClick={() =>
200
- setControlledMode(controlledMode === "dark" ? "light" : "dark")
201
- }
202
- >
203
- {controlledMode === "dark" ? (
204
- <SunIcon aria-hidden="true" />
205
- ) : (
206
- <MoonIcon aria-hidden="true" />
207
- )}
208
- {controlledMode === "dark" ? "Light" : "Dark"}
209
- </Button>
210
- </div>
211
-
212
- <ThemeProvider mode={controlledMode} onModeChange={setControlledMode}>
213
- <ThemeAwareCard readOnly />
214
- </ThemeProvider>
215
- </div>
216
- </ExampleSection>
217
-
218
- {/* ── ThemeWrapper & Portal Tunnels ── */}
219
- <div className="pt-16 pb-6">
220
- <SectionHeader
221
- title="ThemeWrapper & Portal Tunnels"
222
- description="So sánh trực tiếp: cột trái dùng Base UI primitive Portal trực tiếp (không có ThemeWrapper → mất dark mode), cột phải dùng Library Content (tích hợp sẵn ThemeWrapper từ v0.2.20 → đúng màu tự động)."
223
- />
224
- </div>
225
-
226
- {/* Tooltip */}
227
- <ExampleSection
228
- fullWidth
229
- label="Tooltip"
230
- description="Cột trái dùng TooltipPrimitive.Portal trực tiếp từ @base-ui/react — không có ThemeWrapper nên tooltip trắng toát giữa dark section. Cột phải dùng của thư viện — tự động đúng màu."
231
- codeString={`// ❌ Base UI primitive Portal — không có ThemeWrapper → mất dark mode
232
- import { Tooltip as TooltipPrimitive } from "@base-ui/react/tooltip";
233
-
234
- <ThemeProvider defaultMode="dark">
235
- <TooltipPrimitive.Provider>
236
- <TooltipPrimitive.Root>
237
- <TooltipPrimitive.Trigger>Hover tôi</TooltipPrimitive.Trigger>
238
- <TooltipPrimitive.Portal>
239
- {/* Thoát khỏi DOM tree → mất .dark class → CSS vars sai */}
240
- <TooltipPrimitive.Positioner>
241
- <TooltipPrimitive.Popup>Trắng toát!</TooltipPrimitive.Popup>
242
- </TooltipPrimitive.Positioner>
243
- </TooltipPrimitive.Portal>
244
- </TooltipPrimitive.Root>
245
- </TooltipPrimitive.Provider>
246
- </ThemeProvider>
247
-
248
- // ✅ Library Content — ThemeWrapper tích hợp sẵn từ v0.2.20
249
- <ThemeProvider defaultMode="dark">
250
- <TooltipProvider>
251
- <Tooltip>
252
- <TooltipTrigger render={<Button>Hover tôi</Button>} />
253
-
254
- {/* ← ThemeWrapper đã được bọc bên trong */}
255
- <TooltipContent>Màu tối đồng bộ!</TooltipContent>
256
-
257
- </Tooltip>
258
- </TooltipProvider>
259
- </ThemeProvider>`}
260
- >
261
- <ThemeProvider defaultMode="dark">
262
- <div className="w-full flex gap-10 rounded-xl border border-border bg-background p-10">
263
- <div className="flex-1 flex flex-col items-center gap-4">
264
- <span className="text-sm font-medium text-muted-foreground">
265
- ❌ Base UI Primitive Portal
266
- </span>
267
- <TooltipPrimitive.Provider>
268
- <TooltipPrimitive.Root>
269
- <TooltipPrimitive.Trigger
270
- render={<Button variant="solid">Hover tôi</Button>}
271
- />
272
- <TooltipPrimitive.Portal>
273
- <TooltipPrimitive.Positioner sideOffset={8}>
274
- <TooltipPrimitive.Popup className="bg-card text-card-foreground border border-border shadow-md rounded px-2 py-1 text-xs">
275
- Trắng toát! Lạc quẻ với nền đen.
276
- </TooltipPrimitive.Popup>
277
- </TooltipPrimitive.Positioner>
278
- </TooltipPrimitive.Portal>
279
- </TooltipPrimitive.Root>
280
- </TooltipPrimitive.Provider>
281
- </div>
282
-
283
- <div className="flex-1 flex flex-col items-center gap-4">
284
- <span className="text-sm font-medium text-muted-foreground">
285
- ✅ Library Content (auto ThemeWrapper)
286
- </span>
287
- <TooltipProvider>
288
- <Tooltip>
289
- <TooltipTrigger
290
- render={<Button variant="solid">Hover tôi</Button>}
291
- />
292
-
293
- <TooltipContent
294
- sideOffset={8}
295
- className="bg-card text-card-foreground border border-border shadow-md"
296
- >
297
- Màu Tối! Đồng bộ với provider cha.
298
- </TooltipContent>
299
-
300
- </Tooltip>
301
- </TooltipProvider>
302
- </div>
303
- </div>
304
- </ThemeProvider>
305
- </ExampleSection>
306
-
307
- {/* Popover */}
308
- <ExampleSection
309
- fullWidth
310
- label="Popover"
311
- description="Cột trái dùng PopoverPrimitive.Portal trực tiếp — nền popover trắng dù trigger đang ở dark section. Cột phải chỉ cần của thư viện là xong."
312
- codeString={`// ❌ Base UI primitive Portal
313
- import { Popover as PopoverPrimitive } from "@base-ui/react/popover";
314
-
315
- <ThemeProvider defaultMode="dark">
316
- <PopoverPrimitive.Root>
317
- <PopoverPrimitive.Trigger>Click Popover</PopoverPrimitive.Trigger>
318
- <PopoverPrimitive.Portal>
319
- {/* Không có ThemeWrapper → popover trắng */}
320
- <PopoverPrimitive.Positioner>
321
- <PopoverPrimitive.Popup>...</PopoverPrimitive.Popup>
322
- </PopoverPrimitive.Positioner>
323
- </PopoverPrimitive.Portal>
324
- </PopoverPrimitive.Root>
325
- </ThemeProvider>
326
-
327
- // ✅ Library Content
328
- <ThemeProvider defaultMode="dark">
329
- <Popover>
330
- <PopoverTrigger render={<Button>Click Popover</Button>} />
331
-
332
- {/* ← ThemeWrapper đã được bọc bên trong */}
333
- <PopoverContent>...</PopoverContent>
334
-
335
- </Popover>
336
- </ThemeProvider>`}
337
- >
338
- <ThemeProvider defaultMode="dark">
339
- <div className="w-full flex gap-10 rounded-xl border border-border bg-background p-10">
340
- <div className="flex-1 flex flex-col items-center gap-4">
341
- <span className="text-sm font-medium text-muted-foreground">
342
- ❌ Base UI Primitive Portal
343
- </span>
344
- <PopoverPrimitive.Root>
345
- <PopoverPrimitive.Trigger
346
- render={<Button variant="outline">Click Popover</Button>}
347
- />
348
- <PopoverPrimitive.Portal>
349
- <PopoverPrimitive.Positioner>
350
- <PopoverPrimitive.Popup className="z-50 rounded-lg border border-border bg-card text-card-foreground p-4 shadow-md w-64">
351
- <div className="space-y-2">
352
- <h4 className="font-medium leading-none">
353
- Nội dung Popover
354
- </h4>
355
- <p className="text-sm text-muted-foreground">
356
- Lỗi: Hiển thị giao diện sáng.
357
- </p>
358
- </div>
359
- </PopoverPrimitive.Popup>
360
- </PopoverPrimitive.Positioner>
361
- </PopoverPrimitive.Portal>
362
- </PopoverPrimitive.Root>
363
- </div>
364
-
365
- <div className="flex-1 flex flex-col items-center gap-4">
366
- <span className="text-sm font-medium text-muted-foreground">
367
- ✅ Library Content (auto ThemeWrapper)
368
- </span>
369
- <Popover>
370
- <PopoverTrigger
371
- render={<Button variant="outline">Click Popover</Button>}
372
- />
373
-
374
- <PopoverContent className="w-64">
375
- <div className="space-y-2">
376
- <h4 className="font-medium leading-none">
377
- Nội dung Popover
378
- </h4>
379
- <p className="text-sm text-muted-foreground">
380
- Lớp Portal Tunnel đã hoạt động chính xác.
381
- </p>
382
- </div>
383
- </PopoverContent>
384
-
385
- </Popover>
386
- </div>
387
- </div>
388
- </ThemeProvider>
389
- </ExampleSection>
390
-
391
- {/* Select */}
392
- <ExampleSection
393
- fullWidth
394
- label="Select"
395
- description="Cột trái dùng SelectPrimitive.Portal trực tiếp — dropdown option trắng xuộa trong khi trigger vẫn tối. Cột phải dùng của thư viện là đúng màu ngay."
396
- codeString={`// ❌ Base UI primitive Portal
397
- import { Select as SelectPrimitive } from "@base-ui/react/select";
398
-
399
- <ThemeProvider defaultMode="dark">
400
- <SelectPrimitive.Root items={{ next: "Next.js", vite: "Vite" }}>
401
- <SelectPrimitive.Trigger>...</SelectPrimitive.Trigger>
402
- <SelectPrimitive.Portal>
403
- {/* Không có ThemeWrapper → dropdown trắng */}
404
- <SelectPrimitive.Positioner>
405
- <SelectPrimitive.Popup>...</SelectPrimitive.Popup>
406
- </SelectPrimitive.Positioner>
407
- </SelectPrimitive.Portal>
408
- </SelectPrimitive.Root>
409
- </ThemeProvider>
410
-
411
- // ✅ Library Content
412
- <ThemeProvider defaultMode="dark">
413
- <Select items={{ next: "Next.js", vite: "Vite" }}>
414
- <SelectTrigger><SelectValue placeholder="Chọn..." /></SelectTrigger>
415
-
416
- {/* ← ThemeWrapper đã được bọc bên trong */}
417
- <SelectContent>...</SelectContent>
418
-
419
- </Select>
420
- </ThemeProvider>`}
421
- >
422
- <ThemeProvider defaultMode="dark">
423
- <div className="w-full flex gap-10 rounded-xl border border-border bg-background p-10">
424
- <div className="flex-1 flex flex-col items-center gap-4">
425
- <span className="text-sm font-medium text-muted-foreground">
426
- ❌ Base UI Primitive Portal
427
- </span>
428
- <SelectPrimitive.Root
429
- items={{ next: "Next.js", vite: "Vite", remix: "Remix" }}
430
- >
431
- <SelectPrimitive.Trigger className="inline-flex h-8 items-center justify-between gap-1.5 rounded-lg border border-input bg-transparent px-2.5 py-1 text-sm text-foreground outline-none w-[140px]">
432
- <SelectPrimitive.Value placeholder="Chọn framework..." />
433
- </SelectPrimitive.Trigger>
434
- <SelectPrimitive.Portal>
435
- <SelectPrimitive.Positioner>
436
- <SelectPrimitive.Popup className="z-50 rounded-lg border border-border bg-card text-card-foreground p-1 shadow-md">
437
- <SelectPrimitive.Item
438
- value="next"
439
- className="rounded px-2 py-1.5 text-sm cursor-default"
440
- >
441
- <SelectPrimitive.ItemText>Next.js</SelectPrimitive.ItemText>
442
- </SelectPrimitive.Item>
443
- <SelectPrimitive.Item
444
- value="vite"
445
- className="rounded px-2 py-1.5 text-sm cursor-default"
446
- >
447
- <SelectPrimitive.ItemText>Vite</SelectPrimitive.ItemText>
448
- </SelectPrimitive.Item>
449
- <SelectPrimitive.Item
450
- value="remix"
451
- className="rounded px-2 py-1.5 text-sm cursor-default"
452
- >
453
- <SelectPrimitive.ItemText>Remix</SelectPrimitive.ItemText>
454
- </SelectPrimitive.Item>
455
- </SelectPrimitive.Popup>
456
- </SelectPrimitive.Positioner>
457
- </SelectPrimitive.Portal>
458
- </SelectPrimitive.Root>
459
- </div>
460
-
461
- <div className="flex-1 flex flex-col items-center gap-4">
462
- <span className="text-sm font-medium text-muted-foreground">
463
- ✅ Library Content (auto ThemeWrapper)
464
- </span>
465
- <Select items={{ next: "Next.js", vite: "Vite", remix: "Remix" }}>
466
- <SelectTrigger className="w-[140px]">
467
- <SelectValue placeholder="Chọn framework..." />
468
- </SelectTrigger>
469
-
470
- <SelectContent>
471
- <SelectItem value="next">Next.js</SelectItem>
472
- <SelectItem value="vite">Vite</SelectItem>
473
- <SelectItem value="remix">Remix</SelectItem>
474
- </SelectContent>
475
-
476
- </Select>
477
- </div>
478
- </div>
479
- </ThemeProvider>
480
- </ExampleSection>
481
-
482
- {/* DropdownMenu */}
483
- <ExampleSection
484
- fullWidth
485
- label="DropdownMenu"
486
- description="Cột trái dùng MenuPrimitive.Portal trực tiếp — cả nền menu lẫn hover state đều sai màu. Cột phải dùng thư viện là đúng ngay."
487
- codeString={`// ❌ Base UI primitive Portal
488
- import { Menu as MenuPrimitive } from "@base-ui/react/menu";
489
-
490
- <ThemeProvider defaultMode="dark">
491
- <MenuPrimitive.Root>
492
- <MenuPrimitive.Trigger>Open Dropdown</MenuPrimitive.Trigger>
493
- <MenuPrimitive.Portal>
494
- {/* Không có ThemeWrapper → menu trắng, hover sai màu */}
495
- <MenuPrimitive.Positioner>
496
- <MenuPrimitive.Popup>...</MenuPrimitive.Popup>
497
- </MenuPrimitive.Positioner>
498
- </MenuPrimitive.Portal>
499
- </MenuPrimitive.Root>
500
- </ThemeProvider>
501
-
502
- // ✅ Library Content
503
- <ThemeProvider defaultMode="dark">
504
- <DropdownMenu>
505
- <DropdownMenuTrigger render={<Button>Open Dropdown</Button>} />
506
-
507
- {/* ← ThemeWrapper đã được bọc bên trong */}
508
- <DropdownMenuContent>...</DropdownMenuContent>
509
-
510
- </DropdownMenu>
511
- </ThemeProvider>`}
512
- >
513
- <ThemeProvider defaultMode="dark">
514
- <div className="w-full flex gap-10 rounded-xl border border-border bg-background p-10">
515
- <div className="flex-1 flex flex-col items-center gap-4">
516
- <span className="text-sm font-medium text-muted-foreground">
517
- ❌ Base UI Primitive Portal
518
- </span>
519
- <MenuPrimitive.Root>
520
- <MenuPrimitive.Trigger
521
- render={<Button variant="outline">Open Dropdown</Button>}
522
- />
523
- <MenuPrimitive.Portal>
524
- <MenuPrimitive.Positioner>
525
- <MenuPrimitive.Popup className="z-50 min-w-[8rem] rounded-lg border border-border bg-card text-card-foreground p-1 shadow-md">
526
- <MenuPrimitive.Group>
527
- <MenuPrimitive.GroupLabel className="px-2 py-1.5 text-xs font-medium text-muted-foreground">
528
- Tài khoản
529
- </MenuPrimitive.GroupLabel>
530
- <MenuPrimitive.Item className="rounded px-2 py-1.5 text-sm cursor-default">
531
- Trang cá nhân
532
- </MenuPrimitive.Item>
533
- <MenuPrimitive.Item className="rounded px-2 py-1.5 text-sm cursor-default">
534
- Cài đặt
535
- </MenuPrimitive.Item>
536
- </MenuPrimitive.Group>
537
- <MenuPrimitive.Separator className="my-1 h-px bg-border" />
538
- <MenuPrimitive.Item className="rounded px-2 py-1.5 text-sm cursor-default">
539
- Đăng xuất
540
- </MenuPrimitive.Item>
541
- </MenuPrimitive.Popup>
542
- </MenuPrimitive.Positioner>
543
- </MenuPrimitive.Portal>
544
- </MenuPrimitive.Root>
545
- </div>
546
-
547
- <div className="flex-1 flex flex-col items-center gap-4">
548
- <span className="text-sm font-medium text-muted-foreground">
549
- ✅ Library Content (auto ThemeWrapper)
550
- </span>
551
- <DropdownMenu>
552
- <DropdownMenuTrigger
553
- render={<Button variant="outline">Open Dropdown</Button>}
554
- />
555
-
556
- <DropdownMenuContent className="w-48">
557
- <DropdownMenuGroup>
558
- <DropdownMenuLabel>Tài khoản</DropdownMenuLabel>
559
- <DropdownMenuItem>Trang cá nhân</DropdownMenuItem>
560
- <DropdownMenuItem>Cài đặt</DropdownMenuItem>
561
- </DropdownMenuGroup>
562
- <DropdownMenuSeparator />
563
- <DropdownMenuItem>Đăng xuất</DropdownMenuItem>
564
- </DropdownMenuContent>
565
-
566
- </DropdownMenu>
567
- </div>
568
- </div>
569
- </ThemeProvider>
570
- </ExampleSection>
571
-
572
- {/* Dialog */}
573
- <ExampleSection
574
- fullWidth
575
- label="Dialog"
576
- description="Cột trái dùng DialogPrimitive.Portal trực tiếp — cả Overlay backdrop lẫn Content panel đều hiển thị sai màu. Cột phải dùng thư viện — đồng bộ hoàn hảo."
577
- codeString={`// ❌ Base UI primitive Portal
578
- import { Dialog as DialogPrimitive } from "@base-ui/react/dialog";
579
-
580
- <ThemeProvider defaultMode="dark">
581
- <DialogPrimitive.Root>
582
- <DialogPrimitive.Trigger>Open Dialog</DialogPrimitive.Trigger>
583
- <DialogPrimitive.Portal>
584
- {/* Không có ThemeWrapper → dialog trắng */}
585
- <DialogPrimitive.Backdrop />
586
- <DialogPrimitive.Popup>...</DialogPrimitive.Popup>
587
- </DialogPrimitive.Portal>
588
- </DialogPrimitive.Root>
589
- </ThemeProvider>
590
-
591
- // ✅ Library Content
592
- <ThemeProvider defaultMode="dark">
593
- <Dialog>
594
- <DialogTrigger render={<Button>Open Dialog</Button>} />
595
-
596
- {/* ← ThemeWrapper đã được bọc bên trong */}
597
- <DialogContent>...</DialogContent>
598
-
599
- </Dialog>
600
- </ThemeProvider>`}
601
- >
602
- <ThemeProvider defaultMode="dark">
603
- <div className="w-full flex gap-10 rounded-xl border border-border bg-background p-10">
604
- <div className="flex-1 flex flex-col items-center gap-4">
605
- <span className="text-sm font-medium text-muted-foreground">
606
- ❌ Base UI Primitive Portal
607
- </span>
608
- <DialogPrimitive.Root>
609
- <DialogPrimitive.Trigger
610
- render={<Button variant="outline">Open Dialog</Button>}
611
- />
612
- <DialogPrimitive.Portal>
613
- <DialogPrimitive.Backdrop className="fixed inset-0 bg-black/20 z-40" />
614
- <DialogPrimitive.Popup className="fixed left-1/2 top-1/2 z-50 -translate-x-1/2 -translate-y-1/2 rounded-xl border border-border bg-card text-card-foreground p-6 shadow-lg sm:max-w-md w-full">
615
- <DialogPrimitive.Title className="text-lg font-semibold">
616
- Mất Dark Mode!
617
- </DialogPrimitive.Title>
618
- <DialogPrimitive.Description className="mt-2 text-sm text-muted-foreground">
619
- Lỗi: Hiển thị giao diện sáng mặc định.
620
- </DialogPrimitive.Description>
621
- <DialogPrimitive.Close
622
- render={
623
- <Button variant="outline" className="mt-4">
624
- Đóng
625
- </Button>
626
- }
627
- />
628
- </DialogPrimitive.Popup>
629
- </DialogPrimitive.Portal>
630
- </DialogPrimitive.Root>
631
- </div>
632
-
633
- <div className="flex-1 flex flex-col items-center gap-4">
634
- <span className="text-sm font-medium text-muted-foreground">
635
- ✅ Library Content (auto ThemeWrapper)
636
- </span>
637
- <Dialog>
638
- <DialogTrigger
639
- render={<Button variant="outline">Open Dialog</Button>}
640
- />
641
-
642
- <DialogContent className="sm:max-w-md">
643
- <DialogHeader>
644
- <DialogTitle>Màu tối toàn vẹn</DialogTitle>
645
- <DialogDescription>
646
- Portal Tunnel tự động bảo vệ scope Dark Mode.
647
- </DialogDescription>
648
- </DialogHeader>
649
- </DialogContent>
650
-
651
- </Dialog>
652
- </div>
653
- </div>
654
- </ThemeProvider>
655
- </ExampleSection>
656
-
657
- {/* Sheet */}
658
- <ExampleSection
659
- fullWidth
660
- label="Sheet"
661
- description="Cột trái dùng SheetPrimitive.Portal (Dialog primitive) trực tiếp — panel trắng toát. Cột phải dùng của thư viện là đúng màu ngay."
662
- codeString={`// ❌ Base UI primitive Portal (Sheet dùng Dialog primitive của Base UI)
663
- import { Dialog as SheetPrimitive } from "@base-ui/react/dialog";
664
-
665
- <ThemeProvider defaultMode="dark">
666
- <SheetPrimitive.Root>
667
- <SheetPrimitive.Trigger>Open Sheet</SheetPrimitive.Trigger>
668
- <SheetPrimitive.Portal>
669
- {/* Không có ThemeWrapper → sheet trắng */}
670
- <SheetPrimitive.Popup>...</SheetPrimitive.Popup>
671
- </SheetPrimitive.Portal>
672
- </SheetPrimitive.Root>
673
- </ThemeProvider>
674
-
675
- // ✅ Library Content
676
- <ThemeProvider defaultMode="dark">
677
- <Sheet>
678
- <SheetTrigger render={<Button>Open Sheet</Button>} />
679
-
680
- {/* ← ThemeWrapper đã được bọc bên trong */}
681
- <SheetContent side="right">...</SheetContent>
682
-
683
- </Sheet>
684
- </ThemeProvider>`}
685
- >
686
- <ThemeProvider defaultMode="dark">
687
- <div className="w-full flex gap-10 rounded-xl border border-border bg-background p-10">
688
- <div className="flex-1 flex flex-col items-center gap-4">
689
- <span className="text-sm font-medium text-muted-foreground">
690
- ❌ Base UI Primitive Portal
691
- </span>
692
- <SheetPrimitive.Root>
693
- <SheetPrimitive.Trigger
694
- render={<Button variant="outline">Open Sheet</Button>}
695
- />
696
- <SheetPrimitive.Portal>
697
- <SheetPrimitive.Backdrop className="fixed inset-0 bg-black/20 z-40" />
698
- <SheetPrimitive.Popup className="fixed right-0 top-0 z-50 h-full w-3/4 max-w-sm border-l border-border bg-card text-card-foreground p-6 shadow-lg">
699
- <SheetPrimitive.Title className="text-lg font-semibold">
700
- Mất Dark Mode!
701
- </SheetPrimitive.Title>
702
- <SheetPrimitive.Close
703
- render={
704
- <Button variant="ghost" size="sm" className="mt-4">
705
- Đóng
706
- </Button>
707
- }
708
- />
709
- </SheetPrimitive.Popup>
710
- </SheetPrimitive.Portal>
711
- </SheetPrimitive.Root>
712
- </div>
713
-
714
- <div className="flex-1 flex flex-col items-center gap-4">
715
- <span className="text-sm font-medium text-muted-foreground">
716
- ✅ Library Content (auto ThemeWrapper)
717
- </span>
718
- <Sheet>
719
- <SheetTrigger
720
- render={<Button variant="outline">Open Sheet</Button>}
721
- />
722
-
723
- <SheetContent side="right">
724
- <SheetHeader>
725
- <SheetTitle>Sheet Demo</SheetTitle>
726
- </SheetHeader>
727
- <div className="py-4">Màu tối đồng bộ hoàn hảo!</div>
728
- </SheetContent>
729
-
730
- </Sheet>
731
- </div>
732
- </div>
733
- </ThemeProvider>
734
- </ExampleSection>
735
- </div>
736
- );
737
- }
738
-
739
- // ──────────────────────────────────────────────────────────
740
- // Entry point
741
- // ──────────────────────────────────────────────────────────
742
- export default function ThemeProviderShowcase() {
743
- return (
744
- <Showcase
745
- title="Theme Provider"
746
- description="Context provider cho light/dark mode — uncontrolled (defaultMode) hoặc controlled (mode + onModeChange). Tunnel-safe cho mọi nổi dung nổi (floating) qua ThemeWrapper tích hợp tự động trong các *Content components (từ v0.2.20)."
747
- generalConcept={
748
- <ShowcaseDocs>
749
- <DocsH3>ThemeProvider</DocsH3>
750
- <DocsP>
751
- <DocsCode>ThemeProvider</DocsCode> bọc children trong một{" "}
752
- <DocsCode>{"div[display:contents]"}</DocsCode> và gắn class{" "}
753
- <DocsCode>light</DocsCode> hoặc <DocsCode>dark</DocsCode> lên đó,
754
- kích hoạt CSS variable cascade mà không ảnh hưởng layout. Hỗ trợ
755
- hai chế độ: <strong>Uncontrolled</strong> (truyền{" "}
756
- <DocsCode>defaultMode</DocsCode> — tự quản lý state) và{" "}
757
- <strong>Controlled</strong> (truyền <DocsCode>mode</DocsCode> +{" "}
758
- <DocsCode>onModeChange</DocsCode> — state do component cha sở hữu).
759
- Việc đọc localStorage hoặc system preference thuộc về tầng Macro.
760
- </DocsP>
761
-
762
- <DocsH3>Zero-Portal API &amp; Tích hợp Theme tự động (v0.2.20)</DocsH3>
763
- <DocsP>
764
- CSS variable cascade theo <strong>DOM tree</strong>, không theo
765
- React component tree. Khi một component nổi (như Popover, Dialog) render nội dung thông qua cơ chế Portaling (ra{" "}
766
- <DocsCode>document.body</DocsCode>), nó thoát khỏi hoàn toàn chuỗi
767
- DOM ancestry — không còn tổ tiên nào mang class{" "}
768
- <DocsCode>.dark</DocsCode>, và mọi CSS variable rơi thẳng về giá
769
- trị <DocsCode>:root</DocsCode> (thường là light).
770
- </DocsP>
771
- <DocsP>
772
- <DocsCode>ThemeWrapper</DocsCode> giải quyết vấn đề này bằng cách
773
- đọc <DocsCode>useTheme()</DocsCode> và re-apply class theme vào một
774
- container mới ngay bên trong cây portaling — tái thiết lập CSS variable
775
- cascade cho toàn bộ nội dung bên trong.
776
- </DocsP>
777
- <DocsP>
778
- <strong>Kể từ v0.2.20</strong>, thư viện áp dụng <strong>Zero-Portal API</strong>. Tất cả{" "}
779
- <DocsCode>*Content</DocsCode> components của thư viện (DialogContent,
780
-
781
- TooltipContent, v.v...) đã bọc sẵn cơ chế Portaling và <DocsCode>ThemeWrapper</DocsCode> bên
782
- trong chúng. <strong>Bạn không cần tự dùng <DocsCode>*Portal</DocsCode> hay <DocsCode>ThemeWrapper</DocsCode> nữa.</strong> Nếu bạn bypass
783
- thư viện và dùng Base UI primitive Portal trực tiếp, vấn đề mất giao diện dark mode vẫn xảy
784
- ra — demo bên dưới minh hoạ chính xác điều này.
785
- </DocsP>
786
- </ShowcaseDocs>
787
- }
788
- tabs={[
789
- {
790
- label: "Micro (Primitive)",
791
- content: <ThemeProviderMicroShowcase /> },
792
- ]}
793
- />
794
- );
795
- }