gladvn 0.2.20 → 0.2.22
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 +4 -27
- package/dist/dev/App.d.ts.map +1 -1
- package/dist/dev/data.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/dev/App.tsx +36 -10
- package/src/dev/data.ts +0 -8
- package/src/dev/showcase/overview.tsx +3 -3
- 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 -795
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.
|
|
@@ -22,11 +23,11 @@ Designed with strict architectural principles, `gladvn` is optimized not just fo
|
|
|
22
23
|
## 🚀 Installation
|
|
23
24
|
|
|
24
25
|
> [!WARNING]
|
|
25
|
-
> **Please use `npx` to clone the components into your project
|
|
26
|
+
> **Please use `npx` to clone the components into your project.** Installing via `npm` is no longer supported. Using `npx` gives you full control over the source code and styling, which is the core philosophy and intended way to use this library.
|
|
26
27
|
|
|
27
|
-
|
|
28
|
+
We highly recommend using the CLI to initialize components for maximum flexibility.
|
|
28
29
|
|
|
29
|
-
###
|
|
30
|
+
### Installation
|
|
30
31
|
|
|
31
32
|
This approach automatically copies all component source code, hooks, and styles directly into your project. You own the code, you can freely modify it, and the CLI will automatically install necessary peer dependencies for you!
|
|
32
33
|
|
|
@@ -45,31 +46,7 @@ npx gladvn init components/ui
|
|
|
45
46
|
import { Button } from "@/gladvn/components/micro/button";
|
|
46
47
|
```
|
|
47
48
|
|
|
48
|
-
### Option 2: Install as an npm package
|
|
49
49
|
|
|
50
|
-
If you prefer to use the pre-built components without cluttering your repository with source code, install it via npm:
|
|
51
|
-
|
|
52
|
-
```bash
|
|
53
|
-
npm install gladvn
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
**Setup Tailwind CSS v4:**
|
|
57
|
-
Update your main CSS file to scan the library for Tailwind utility classes and import its CSS variables:
|
|
58
|
-
|
|
59
|
-
```css
|
|
60
|
-
@import "tailwindcss";
|
|
61
|
-
|
|
62
|
-
/* 1. Tell Tailwind to scan gladvn for utility classes */
|
|
63
|
-
@source "../node_modules/gladvn/dist";
|
|
64
|
-
|
|
65
|
-
/* 2. Import gladvn global CSS variables */
|
|
66
|
-
@import "gladvn/gladvn.css";
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
**Usage:**
|
|
70
|
-
```tsx
|
|
71
|
-
import { Button, Card, CardHeader, CardTitle, CardContent } from "gladvn";
|
|
72
|
-
```
|
|
73
50
|
|
|
74
51
|
---
|
|
75
52
|
|
package/dist/dev/App.d.ts.map
CHANGED
|
@@ -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,
|
|
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"}
|
package/dist/dev/data.d.ts.map
CHANGED
|
@@ -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;;;;;;;;;;;;;;;;;;;;;;;;
|
|
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.
|
|
3
|
+
"version": "0.2.22",
|
|
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/
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
|
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="
|
|
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/
|
|
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/
|
|
1125
|
+
href="https://github.com/duongacy/gladvn"
|
|
1126
1126
|
target="_blank"
|
|
1127
1127
|
rel="noreferrer"
|
|
1128
1128
|
/>
|
|
@@ -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 & 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
|
-
}
|