gladvn 0.2.35 → 0.2.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +2 -1
- package/scripts/strip-comments.ts +79 -0
- package/src/blocks/auth-form.tsx +34 -30
- package/src/blocks/auth-recovery.tsx +9 -13
- package/src/blocks/auth-split.tsx +46 -36
- package/src/blocks/settings.tsx +84 -117
- package/src/components/macro/alert-preset.tsx +3 -1
- package/src/components/macro/carousel-preset.tsx +0 -1
- package/src/components/macro/checkbox-preset.test.tsx +2 -2
- package/src/components/macro/checkbox-preset.tsx +1 -2
- package/src/components/macro/command-preset.tsx +1 -1
- package/src/components/macro/date-picker.tsx +5 -18
- package/src/components/macro/dialog-preset.tsx +5 -3
- package/src/components/macro/input-preset.tsx +0 -1
- package/src/components/macro/select-preset.tsx +1 -1
- package/src/components/macro/switch-preset.test.tsx +1 -1
- package/src/components/micro/accordion.test.tsx +6 -12
- package/src/components/micro/alert-dialog.tsx +0 -2
- package/src/components/micro/alert.test.tsx +5 -8
- package/src/components/micro/alert.tsx +0 -2
- package/src/components/micro/badge.test.tsx +5 -7
- package/src/components/micro/badge.tsx +1 -3
- package/src/components/micro/button.test.tsx +19 -26
- package/src/components/micro/button.tsx +3 -11
- package/src/components/micro/calendar.tsx +12 -13
- package/src/components/micro/card.test.tsx +3 -5
- package/src/components/micro/carousel.tsx +0 -1
- package/src/components/micro/chart.tsx +3 -2
- package/src/components/micro/checkbox.test.tsx +9 -18
- package/src/components/micro/combobox.tsx +1 -3
- package/src/components/micro/command.tsx +23 -11
- package/src/components/micro/context-menu.tsx +0 -2
- package/src/components/micro/dialog.test.tsx +6 -14
- package/src/components/micro/dialog.tsx +0 -2
- package/src/components/micro/drawer.tsx +0 -3
- package/src/components/micro/dropdown-menu.test.tsx +8 -14
- package/src/components/micro/dropdown-menu.tsx +0 -2
- package/src/components/micro/field.tsx +3 -3
- package/src/components/micro/hover-card.tsx +0 -2
- package/src/components/micro/input-group.tsx +9 -42
- package/src/components/micro/input.test.tsx +15 -24
- package/src/components/micro/navigation-menu.tsx +0 -2
- package/src/components/micro/popover.test.tsx +5 -9
- package/src/components/micro/progress.tsx +5 -1
- package/src/components/micro/select.test.tsx +5 -18
- package/src/components/micro/select.tsx +3 -5
- package/src/components/micro/separator.tsx +1 -1
- package/src/components/micro/sheet.test.tsx +6 -14
- package/src/components/micro/sheet.tsx +0 -2
- package/src/components/micro/sidebar.tsx +5 -6
- package/src/components/micro/sonner.tsx +0 -1
- package/src/components/micro/switch.test.tsx +9 -18
- package/src/components/micro/switch.tsx +1 -1
- package/src/components/micro/tabs.test.tsx +5 -11
- package/src/components/micro/theme-provider.tsx +0 -12
- package/src/components/micro/toggle-group.tsx +1 -12
- package/src/components/micro/tooltip.test.tsx +7 -12
- package/src/dev/App.tsx +6 -6
- package/src/dev/components/BlockViewer.tsx +15 -4
- package/src/dev/components/Container.tsx +20 -0
- package/src/dev/components/GladvnLogo.tsx +3 -9
- package/src/dev/components/code-highlighter.tsx +5 -3
- package/src/dev/components/showcase.tsx +116 -168
- package/src/dev/sections/buttons.tsx +4 -9
- package/src/dev/sections/display.tsx +68 -72
- package/src/dev/sections/feedback.tsx +304 -314
- package/src/dev/sections/forms.tsx +10 -14
- package/src/dev/sections/interactive.tsx +32 -32
- package/src/dev/sections/overview.tsx +2 -10
- package/src/dev/showcase/accordion.tsx +512 -554
- package/src/dev/showcase/alert-dialog.tsx +525 -565
- package/src/dev/showcase/alert.tsx +352 -402
- package/src/dev/showcase/aspect-ratio.tsx +167 -193
- package/src/dev/showcase/avatar.tsx +231 -269
- package/src/dev/showcase/badge.tsx +92 -140
- package/src/dev/showcase/breadcrumb.tsx +113 -135
- package/src/dev/showcase/button.tsx +140 -173
- package/src/dev/showcase/calendar.tsx +187 -237
- package/src/dev/showcase/card.tsx +229 -261
- package/src/dev/showcase/carousel.tsx +288 -315
- package/src/dev/showcase/chart.tsx +44 -50
- package/src/dev/showcase/checkbox.tsx +286 -330
- package/src/dev/showcase/collapsible.tsx +143 -160
- package/src/dev/showcase/combobox.tsx +327 -369
- package/src/dev/showcase/command.tsx +106 -120
- package/src/dev/showcase/context-menu.tsx +93 -107
- package/src/dev/showcase/dialog.tsx +585 -630
- package/src/dev/showcase/drawer.tsx +176 -196
- package/src/dev/showcase/dropdown-menu.tsx +183 -201
- package/src/dev/showcase/empty.tsx +232 -274
- package/src/dev/showcase/hover-card.tsx +153 -180
- package/src/dev/showcase/input-group.tsx +221 -253
- package/src/dev/showcase/input-otp.tsx +204 -250
- package/src/dev/showcase/input.tsx +246 -325
- package/src/dev/showcase/item.tsx +134 -152
- package/src/dev/showcase/kbd.tsx +149 -172
- package/src/dev/showcase/label.tsx +50 -68
- package/src/dev/showcase/menubar.tsx +194 -213
- package/src/dev/showcase/navigation-menu.tsx +119 -133
- package/src/dev/showcase/overview-components/ArchitectureSection.tsx +126 -0
- package/src/dev/showcase/overview-components/CodeBlock.tsx +50 -0
- package/src/dev/showcase/overview-components/ColorPaletteSection.tsx +44 -0
- package/src/dev/showcase/overview-components/DashboardPreview.tsx +159 -0
- package/src/dev/showcase/overview-components/FeatureGridSection.tsx +535 -0
- package/src/dev/showcase/overview-components/FooterCTA.tsx +137 -0
- package/src/dev/showcase/overview-components/HeroSection.tsx +65 -0
- package/src/dev/showcase/overview-components/PrinciplesSection.tsx +128 -0
- package/src/dev/showcase/overview.tsx +21 -1163
- package/src/dev/showcase/pagination.tsx +144 -190
- package/src/dev/showcase/popover.tsx +181 -196
- package/src/dev/showcase/progress.tsx +141 -189
- package/src/dev/showcase/radio-group.tsx +272 -332
- package/src/dev/showcase/resizable.tsx +119 -141
- package/src/dev/showcase/scroll-area.tsx +21 -36
- package/src/dev/showcase/select.tsx +262 -322
- package/src/dev/showcase/separator.tsx +75 -73
- package/src/dev/showcase/sheet.tsx +333 -361
- package/src/dev/showcase/sidebar.tsx +53 -59
- package/src/dev/showcase/skeleton.tsx +93 -112
- package/src/dev/showcase/slider.tsx +244 -296
- package/src/dev/showcase/sonner.tsx +251 -258
- package/src/dev/showcase/spinner.tsx +46 -67
- package/src/dev/showcase/switch.tsx +298 -362
- package/src/dev/showcase/table.tsx +76 -80
- package/src/dev/showcase/tabs.tsx +302 -355
- package/src/dev/showcase/textarea.tsx +240 -293
- package/src/dev/showcase/toast.tsx +48 -54
- package/src/dev/showcase/toggle-group.tsx +104 -129
- package/src/dev/showcase/toggle.tsx +82 -102
- package/src/dev/showcase/tooltip.tsx +173 -192
- package/src/dev/showcase/why.tsx +6 -12
- package/src/hooks/use-mobile.test.ts +3 -2
- package/src/lib/utils.test.ts +5 -4
|
@@ -4,9 +4,6 @@ import * as React from "react";
|
|
|
4
4
|
|
|
5
5
|
import { cn } from "../../lib/utils";
|
|
6
6
|
|
|
7
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
8
|
-
// 1. Sidebar (Wrapper)
|
|
9
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
10
7
|
const Sidebar = React.forwardRef<
|
|
11
8
|
HTMLDivElement,
|
|
12
9
|
React.ComponentProps<"div"> & {
|
|
@@ -27,7 +24,7 @@ const Sidebar = React.forwardRef<
|
|
|
27
24
|
},
|
|
28
25
|
ref,
|
|
29
26
|
) => {
|
|
30
|
-
|
|
27
|
+
|
|
31
28
|
const handleTransitionStart = (
|
|
32
29
|
e: React.TransitionEvent<HTMLDivElement>,
|
|
33
30
|
) => {
|
|
@@ -39,7 +36,7 @@ const Sidebar = React.forwardRef<
|
|
|
39
36
|
};
|
|
40
37
|
|
|
41
38
|
return (
|
|
42
|
-
|
|
39
|
+
|
|
43
40
|
<div
|
|
44
41
|
ref={ref}
|
|
45
42
|
role="navigation"
|
|
@@ -55,7 +52,9 @@ const Sidebar = React.forwardRef<
|
|
|
55
52
|
"transition-[width,border-width] duration-[400ms]",
|
|
56
53
|
"data-[state=expanded]:w-[var(--sb-expanded)]",
|
|
57
54
|
"data-[state=collapsed]:w-[var(--sb-collapsed)]",
|
|
58
|
-
|
|
55
|
+
{
|
|
56
|
+
"data-[state=collapsed]:border-r-0": collapsedWidth === 0
|
|
57
|
+
},
|
|
59
58
|
className,
|
|
60
59
|
)}
|
|
61
60
|
data-state={defaultState}
|
|
@@ -43,7 +43,6 @@ function Toaster({ ...props }: ToasterProps) {
|
|
|
43
43
|
"--normal-border": "var(--border)",
|
|
44
44
|
"--border-radius": "var(--radius)",
|
|
45
45
|
|
|
46
|
-
/* Map Sonner's internal variables to our design system variables */
|
|
47
46
|
"--success-bg":
|
|
48
47
|
"color-mix(in srgb, var(--success) 10%, var(--popover))",
|
|
49
48
|
"--success-text": "var(--success)",
|
|
@@ -8,20 +8,20 @@ import { Switch, SwitchThumb } from "./switch";
|
|
|
8
8
|
|
|
9
9
|
describe("Switch", () => {
|
|
10
10
|
it("[3C.7-01] [P1] renders correctly", () => {
|
|
11
|
-
|
|
11
|
+
|
|
12
12
|
render(
|
|
13
13
|
<Switch aria-label="Toggle setting">
|
|
14
14
|
<SwitchThumb />
|
|
15
15
|
</Switch>,
|
|
16
16
|
);
|
|
17
17
|
const switchEl = screen.getByRole("switch", { name: "Toggle setting" });
|
|
18
|
-
|
|
18
|
+
|
|
19
19
|
expect(switchEl).toBeInTheDocument();
|
|
20
20
|
expect(switchEl).toHaveAttribute("data-slot", "switch");
|
|
21
21
|
});
|
|
22
22
|
|
|
23
23
|
it("[3C.7-02] [P1] handles user click to toggle state", async () => {
|
|
24
|
-
|
|
24
|
+
|
|
25
25
|
const user = userEvent.setup();
|
|
26
26
|
const handleCheckedChange = vi.fn();
|
|
27
27
|
render(
|
|
@@ -32,24 +32,19 @@ describe("Switch", () => {
|
|
|
32
32
|
|
|
33
33
|
const switchEl = screen.getByRole("switch", { name: "Airplane mode" });
|
|
34
34
|
|
|
35
|
-
// Initially unchecked (base-ui uses aria-checked)
|
|
36
|
-
// Then
|
|
37
35
|
expect(switchEl).toHaveAttribute("aria-checked", "false");
|
|
38
36
|
|
|
39
|
-
// Click to check
|
|
40
|
-
// When
|
|
41
37
|
await user.click(switchEl);
|
|
42
38
|
expect(switchEl).toHaveAttribute("aria-checked", "true");
|
|
43
39
|
expect(handleCheckedChange).toHaveBeenCalledWith(true, expect.anything());
|
|
44
40
|
|
|
45
|
-
// Click to uncheck
|
|
46
41
|
await user.click(switchEl);
|
|
47
42
|
expect(switchEl).toHaveAttribute("aria-checked", "false");
|
|
48
43
|
expect(handleCheckedChange).toHaveBeenCalledWith(false, expect.anything());
|
|
49
44
|
});
|
|
50
45
|
|
|
51
46
|
it("[3C.7-03] [P1] can be focused and toggled via keyboard (Space)", async () => {
|
|
52
|
-
|
|
47
|
+
|
|
53
48
|
const user = userEvent.setup();
|
|
54
49
|
render(
|
|
55
50
|
<Switch aria-label="Keyboard toggle">
|
|
@@ -59,19 +54,16 @@ describe("Switch", () => {
|
|
|
59
54
|
|
|
60
55
|
const switchEl = screen.getByRole("switch", { name: "Keyboard toggle" });
|
|
61
56
|
|
|
62
|
-
// Test focus
|
|
63
|
-
// When
|
|
64
57
|
await user.tab();
|
|
65
|
-
|
|
58
|
+
|
|
66
59
|
expect(switchEl).toHaveFocus();
|
|
67
60
|
|
|
68
|
-
// Test keyboard activation (Space for switch/checkbox)
|
|
69
61
|
await user.keyboard("[Space]");
|
|
70
62
|
expect(switchEl).toHaveAttribute("aria-checked", "true");
|
|
71
63
|
});
|
|
72
64
|
|
|
73
65
|
it("[3C.7-04] [P1] handles disabled state correctly and blocks clicks", async () => {
|
|
74
|
-
|
|
66
|
+
|
|
75
67
|
const user = userEvent.setup();
|
|
76
68
|
const handleCheckedChange = vi.fn();
|
|
77
69
|
render(
|
|
@@ -85,24 +77,23 @@ describe("Switch", () => {
|
|
|
85
77
|
);
|
|
86
78
|
|
|
87
79
|
const switchEl = screen.getByRole("switch", { name: "Disabled switch" });
|
|
88
|
-
|
|
80
|
+
|
|
89
81
|
expect(switchEl).toHaveAttribute("aria-disabled", "true");
|
|
90
82
|
|
|
91
|
-
// When
|
|
92
83
|
await user.click(switchEl);
|
|
93
84
|
expect(handleCheckedChange).not.toHaveBeenCalled();
|
|
94
85
|
expect(switchEl).toHaveAttribute("aria-checked", "false");
|
|
95
86
|
});
|
|
96
87
|
|
|
97
88
|
it("[3C.7-05] [P1] forwards the ref to the underlying DOM element", () => {
|
|
98
|
-
|
|
89
|
+
|
|
99
90
|
const ref = React.createRef<HTMLSpanElement>();
|
|
100
91
|
render(
|
|
101
92
|
<Switch ref={ref} aria-label="Ref switch">
|
|
102
93
|
<SwitchThumb />
|
|
103
94
|
</Switch>,
|
|
104
95
|
);
|
|
105
|
-
|
|
96
|
+
|
|
106
97
|
expect(ref.current).toBeInstanceOf(HTMLSpanElement);
|
|
107
98
|
});
|
|
108
99
|
});
|
|
@@ -41,7 +41,7 @@ const Switch = React.forwardRef<
|
|
|
41
41
|
<SwitchPrimitive.Root
|
|
42
42
|
ref={ref}
|
|
43
43
|
data-slot="switch"
|
|
44
|
-
|
|
44
|
+
|
|
45
45
|
className={cn(switchVariants({ size, className }))}
|
|
46
46
|
{...props}
|
|
47
47
|
/>
|
|
@@ -7,7 +7,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "./tabs";
|
|
|
7
7
|
|
|
8
8
|
describe("Tabs", () => {
|
|
9
9
|
it("[3C.8-01] [P1] renders correctly and shows default content", () => {
|
|
10
|
-
|
|
10
|
+
|
|
11
11
|
render(
|
|
12
12
|
<Tabs defaultValue="tab1">
|
|
13
13
|
<TabsList>
|
|
@@ -19,19 +19,16 @@ describe("Tabs", () => {
|
|
|
19
19
|
</Tabs>,
|
|
20
20
|
);
|
|
21
21
|
|
|
22
|
-
// Then
|
|
23
22
|
expect(screen.getByRole("tab", { name: "Tab 1" })).toBeInTheDocument();
|
|
24
23
|
expect(screen.getByRole("tab", { name: "Tab 2" })).toBeInTheDocument();
|
|
25
24
|
|
|
26
|
-
// Tab 1 content should be visible
|
|
27
25
|
expect(screen.getByText("Content 1")).toBeInTheDocument();
|
|
28
26
|
|
|
29
|
-
// Tab 2 content should NOT be visible
|
|
30
27
|
expect(screen.queryByText("Content 2")).not.toBeInTheDocument();
|
|
31
28
|
});
|
|
32
29
|
|
|
33
30
|
it("[3C.8-02] [P1] switches content when another tab is clicked", async () => {
|
|
34
|
-
|
|
31
|
+
|
|
35
32
|
const user = userEvent.setup();
|
|
36
33
|
render(
|
|
37
34
|
<Tabs defaultValue="tab1">
|
|
@@ -45,16 +42,15 @@ describe("Tabs", () => {
|
|
|
45
42
|
);
|
|
46
43
|
|
|
47
44
|
const tab2 = screen.getByRole("tab", { name: "Tab 2" });
|
|
48
|
-
|
|
45
|
+
|
|
49
46
|
await user.click(tab2);
|
|
50
47
|
|
|
51
|
-
// Then
|
|
52
48
|
expect(screen.queryByText("Content 1")).not.toBeInTheDocument();
|
|
53
49
|
expect(screen.getByText("Content 2")).toBeInTheDocument();
|
|
54
50
|
});
|
|
55
51
|
|
|
56
52
|
it("[3C.8-03] [P1] supports keyboard navigation", async () => {
|
|
57
|
-
|
|
53
|
+
|
|
58
54
|
const user = userEvent.setup();
|
|
59
55
|
render(
|
|
60
56
|
<Tabs defaultValue="tab1">
|
|
@@ -70,12 +66,10 @@ describe("Tabs", () => {
|
|
|
70
66
|
const tab1 = screen.getByRole("tab", { name: "Tab 1" });
|
|
71
67
|
const tab2 = screen.getByRole("tab", { name: "Tab 2" });
|
|
72
68
|
|
|
73
|
-
// When
|
|
74
69
|
await user.tab();
|
|
75
|
-
|
|
70
|
+
|
|
76
71
|
expect(tab1).toHaveFocus();
|
|
77
72
|
|
|
78
|
-
// ArrowRight should move focus and potentially activate the next tab
|
|
79
73
|
await user.keyboard("[ArrowRight]");
|
|
80
74
|
await waitFor(() => {
|
|
81
75
|
expect(tab2).toHaveFocus();
|
|
@@ -15,8 +15,6 @@
|
|
|
15
15
|
|
|
16
16
|
import * as React from "react";
|
|
17
17
|
|
|
18
|
-
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
19
|
-
|
|
20
18
|
type ThemeMode = "light" | "dark";
|
|
21
19
|
|
|
22
20
|
type ThemeContextValue = {
|
|
@@ -26,14 +24,10 @@ type ThemeContextValue = {
|
|
|
26
24
|
setMode: (mode: ThemeMode) => void;
|
|
27
25
|
};
|
|
28
26
|
|
|
29
|
-
// ─── Context ──────────────────────────────────────────────────────────────────
|
|
30
|
-
|
|
31
27
|
const ThemeContext = React.createContext<ThemeContextValue | undefined>(
|
|
32
28
|
undefined,
|
|
33
29
|
);
|
|
34
30
|
|
|
35
|
-
// ─── useTheme ─────────────────────────────────────────────────────────────────
|
|
36
|
-
|
|
37
31
|
/**
|
|
38
32
|
* Read and control the nearest ThemeProvider.
|
|
39
33
|
* Returns `undefined` when no provider is found in the tree.
|
|
@@ -49,8 +43,6 @@ function useTheme(): ThemeContextValue | undefined {
|
|
|
49
43
|
return React.useContext(ThemeContext);
|
|
50
44
|
}
|
|
51
45
|
|
|
52
|
-
// ─── ThemeProvider ────────────────────────────────────────────────────────────
|
|
53
|
-
|
|
54
46
|
type ThemeProviderProps = {
|
|
55
47
|
children: React.ReactNode;
|
|
56
48
|
} & (
|
|
@@ -131,8 +123,6 @@ function ThemeProvider({
|
|
|
131
123
|
|
|
132
124
|
ThemeProvider.displayName = "ThemeProvider";
|
|
133
125
|
|
|
134
|
-
// ─── ThemeWrapper ─────────────────────────────────────────────────────────────
|
|
135
|
-
|
|
136
126
|
/**
|
|
137
127
|
* ThemeWrapper — re-applies the current theme class inside a Portal.
|
|
138
128
|
* Used internally by Dialog, Tooltip, Popover, etc. to tunnel the theme
|
|
@@ -157,7 +147,5 @@ function ThemeWrapper({ children }: { children: React.ReactNode }) {
|
|
|
157
147
|
|
|
158
148
|
ThemeWrapper.displayName = "ThemeWrapper";
|
|
159
149
|
|
|
160
|
-
// ─── Exports ──────────────────────────────────────────────────────────────────
|
|
161
|
-
|
|
162
150
|
export { ThemeProvider, ThemeWrapper, useTheme };
|
|
163
151
|
export type { ThemeMode };
|
|
@@ -16,9 +16,6 @@ import { type VariantProps } from "class-variance-authority";
|
|
|
16
16
|
import { toggleVariants } from "../../components/micro/toggle";
|
|
17
17
|
import { cn } from "../../lib/utils";
|
|
18
18
|
|
|
19
|
-
// ──────────────────────────────────────────────────────────
|
|
20
|
-
// Context
|
|
21
|
-
// ──────────────────────────────────────────────────────────
|
|
22
19
|
type ToggleGroupContextValue = VariantProps<typeof toggleVariants> & {
|
|
23
20
|
spacing?: number;
|
|
24
21
|
orientation?: "horizontal" | "vertical";
|
|
@@ -31,9 +28,6 @@ const ToggleGroupContext = React.createContext<ToggleGroupContextValue>({
|
|
|
31
28
|
orientation: "horizontal",
|
|
32
29
|
});
|
|
33
30
|
|
|
34
|
-
// ──────────────────────────────────────────────────────────
|
|
35
|
-
// Types
|
|
36
|
-
// ──────────────────────────────────────────────────────────
|
|
37
31
|
type ToggleGroupProps = ToggleGroupPrimitive.Props &
|
|
38
32
|
VariantProps<typeof toggleVariants> & {
|
|
39
33
|
spacing?: number;
|
|
@@ -43,10 +37,6 @@ type ToggleGroupProps = ToggleGroupPrimitive.Props &
|
|
|
43
37
|
type ToggleGroupItemProps = TogglePrimitive.Props &
|
|
44
38
|
VariantProps<typeof toggleVariants>;
|
|
45
39
|
|
|
46
|
-
// ──────────────────────────────────────────────────────────
|
|
47
|
-
// Components
|
|
48
|
-
// ──────────────────────────────────────────────────────────
|
|
49
|
-
|
|
50
40
|
/**
|
|
51
41
|
* @description A set of two-state buttons that can be toggled on or off.
|
|
52
42
|
* @requires ToggleGroupItem
|
|
@@ -59,8 +49,7 @@ function ToggleGroup({
|
|
|
59
49
|
className,
|
|
60
50
|
variant,
|
|
61
51
|
size,
|
|
62
|
-
|
|
63
|
-
// since Tailwind cannot generate arbitrary gap-{n} utilities at runtime.
|
|
52
|
+
|
|
64
53
|
spacing = 2,
|
|
65
54
|
orientation = "horizontal",
|
|
66
55
|
children,
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
|
|
13
13
|
describe("Tooltip", () => {
|
|
14
14
|
it("[3C.9-01] [P1] renders trigger and hides content initially", () => {
|
|
15
|
-
|
|
15
|
+
|
|
16
16
|
render(
|
|
17
17
|
<TooltipProvider>
|
|
18
18
|
<Tooltip>
|
|
@@ -22,17 +22,15 @@ describe("Tooltip", () => {
|
|
|
22
22
|
</TooltipProvider>,
|
|
23
23
|
);
|
|
24
24
|
|
|
25
|
-
// Then
|
|
26
25
|
expect(
|
|
27
26
|
screen.getByRole("button", { name: "Hover me" }),
|
|
28
27
|
).toBeInTheDocument();
|
|
29
28
|
|
|
30
|
-
// Content should not be visible initially
|
|
31
29
|
expect(screen.queryByText("Tooltip info")).not.toBeInTheDocument();
|
|
32
30
|
});
|
|
33
31
|
|
|
34
32
|
it("[3C.9-02] [P1] shows tooltip content on hover", async () => {
|
|
35
|
-
|
|
33
|
+
|
|
36
34
|
const user = userEvent.setup();
|
|
37
35
|
render(
|
|
38
36
|
<TooltipProvider delay={0}>
|
|
@@ -44,17 +42,16 @@ describe("Tooltip", () => {
|
|
|
44
42
|
);
|
|
45
43
|
|
|
46
44
|
const trigger = screen.getByRole("button", { name: "Hover me" });
|
|
47
|
-
|
|
45
|
+
|
|
48
46
|
await user.hover(trigger);
|
|
49
47
|
|
|
50
|
-
// Then
|
|
51
48
|
await waitFor(() => {
|
|
52
49
|
expect(screen.getByText("Tooltip info")).toBeVisible();
|
|
53
50
|
});
|
|
54
51
|
});
|
|
55
52
|
|
|
56
53
|
it("[3C.9-03] [P1] shows tooltip content on focus", async () => {
|
|
57
|
-
|
|
54
|
+
|
|
58
55
|
const user = userEvent.setup();
|
|
59
56
|
render(
|
|
60
57
|
<TooltipProvider delay={0}>
|
|
@@ -67,9 +64,8 @@ describe("Tooltip", () => {
|
|
|
67
64
|
|
|
68
65
|
const trigger = screen.getByRole("button", { name: "Hover me" });
|
|
69
66
|
|
|
70
|
-
// When
|
|
71
67
|
await user.tab();
|
|
72
|
-
|
|
68
|
+
|
|
73
69
|
expect(trigger).toHaveFocus();
|
|
74
70
|
|
|
75
71
|
await waitFor(() => {
|
|
@@ -78,7 +74,7 @@ describe("Tooltip", () => {
|
|
|
78
74
|
});
|
|
79
75
|
|
|
80
76
|
it("[3C.9-04] [P1] hides tooltip when Escape is pressed", async () => {
|
|
81
|
-
|
|
77
|
+
|
|
82
78
|
const user = userEvent.setup();
|
|
83
79
|
render(
|
|
84
80
|
<TooltipProvider delay={0}>
|
|
@@ -90,10 +86,9 @@ describe("Tooltip", () => {
|
|
|
90
86
|
);
|
|
91
87
|
|
|
92
88
|
const trigger = screen.getByRole("button", { name: "Hover me" });
|
|
93
|
-
|
|
89
|
+
|
|
94
90
|
await user.hover(trigger);
|
|
95
91
|
|
|
96
|
-
// Then
|
|
97
92
|
await waitFor(() => {
|
|
98
93
|
expect(screen.getByText("Tooltip info")).toBeVisible();
|
|
99
94
|
});
|
package/src/dev/App.tsx
CHANGED
|
@@ -199,7 +199,7 @@ export default function App() {
|
|
|
199
199
|
<button
|
|
200
200
|
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
|
|
201
201
|
className="md:hidden -ml-2 p-1.5 rounded-md hover:bg-muted text-muted-foreground transition-colors"
|
|
202
|
-
aria-label="
|
|
202
|
+
aria-label="Bật/tắt menu"
|
|
203
203
|
>
|
|
204
204
|
{isMobileMenuOpen ? (
|
|
205
205
|
<XIcon className="size-5" />
|
|
@@ -227,7 +227,7 @@ export default function App() {
|
|
|
227
227
|
}
|
|
228
228
|
)}
|
|
229
229
|
>
|
|
230
|
-
|
|
230
|
+
Tổng quan
|
|
231
231
|
</button>
|
|
232
232
|
<button
|
|
233
233
|
onClick={() => {
|
|
@@ -272,7 +272,7 @@ export default function App() {
|
|
|
272
272
|
>
|
|
273
273
|
<SearchIcon className="size-3.5 shrink-0" />
|
|
274
274
|
<span className="flex-1 text-left">
|
|
275
|
-
|
|
275
|
+
Tìm kiếm...
|
|
276
276
|
</span>
|
|
277
277
|
<kbd className="text-[10px] bg-background border border-border/80 rounded px-1.5 py-0.5 font-sans leading-none">
|
|
278
278
|
⌘K
|
|
@@ -293,7 +293,7 @@ export default function App() {
|
|
|
293
293
|
/>
|
|
294
294
|
}
|
|
295
295
|
nativeButton={false}
|
|
296
|
-
className="text-muted-foreground
|
|
296
|
+
className="text-muted-foreground hover:text-foreground hidden sm:inline-flex"
|
|
297
297
|
>
|
|
298
298
|
{/* GitHub SVG mark */}
|
|
299
299
|
<svg
|
|
@@ -322,7 +322,7 @@ export default function App() {
|
|
|
322
322
|
>
|
|
323
323
|
<SunIcon className="size-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
|
324
324
|
<MoonIcon className="absolute size-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
|
325
|
-
<span className="sr-only"
|
|
325
|
+
<span className="sr-only">Đổi giao diện</span>
|
|
326
326
|
</Button>
|
|
327
327
|
</div>
|
|
328
328
|
</div>
|
|
@@ -362,7 +362,7 @@ export default function App() {
|
|
|
362
362
|
)}
|
|
363
363
|
>
|
|
364
364
|
<LayersIcon className="size-3.5" />
|
|
365
|
-
|
|
365
|
+
Tổng quan
|
|
366
366
|
</button>
|
|
367
367
|
<div className="h-px bg-border/50 my-2 mx-1"></div>
|
|
368
368
|
<button
|
|
@@ -12,16 +12,18 @@ export function BlockViewer({
|
|
|
12
12
|
title,
|
|
13
13
|
description,
|
|
14
14
|
codeString,
|
|
15
|
+
className,
|
|
15
16
|
}: {
|
|
16
17
|
blockId: string;
|
|
17
18
|
title: string;
|
|
18
19
|
description?: string;
|
|
19
20
|
codeString: string;
|
|
21
|
+
className?: string;
|
|
20
22
|
}) {
|
|
21
23
|
const [width, setWidth] = useState<"100%" | "768px" | "375px">("100%");
|
|
22
24
|
|
|
23
25
|
return (
|
|
24
|
-
<div className="
|
|
26
|
+
<div className={cn("flex flex-col gap-6", className)}>
|
|
25
27
|
<SectionHeader title={title} description={description} />
|
|
26
28
|
|
|
27
29
|
<Tabs defaultValue="preview" className="w-full relative group">
|
|
@@ -37,7 +39,10 @@ export function BlockViewer({
|
|
|
37
39
|
size="sm"
|
|
38
40
|
iconOnly
|
|
39
41
|
onClick={() => setWidth("100%")}
|
|
40
|
-
className={cn("size-7",
|
|
42
|
+
className={cn("size-7", {
|
|
43
|
+
"bg-muted text-foreground": width === "100%",
|
|
44
|
+
"text-muted-foreground": width !== "100%",
|
|
45
|
+
})}
|
|
41
46
|
aria-label="Desktop view"
|
|
42
47
|
>
|
|
43
48
|
<MonitorIcon className="size-3.5" />
|
|
@@ -47,7 +52,10 @@ export function BlockViewer({
|
|
|
47
52
|
size="sm"
|
|
48
53
|
iconOnly
|
|
49
54
|
onClick={() => setWidth("768px")}
|
|
50
|
-
className={cn("size-7 hidden sm:flex",
|
|
55
|
+
className={cn("size-7 hidden sm:flex", {
|
|
56
|
+
"bg-muted text-foreground": width === "768px",
|
|
57
|
+
"text-muted-foreground": width !== "768px",
|
|
58
|
+
})}
|
|
51
59
|
aria-label="Tablet view"
|
|
52
60
|
>
|
|
53
61
|
<TabletIcon className="size-3.5" />
|
|
@@ -57,7 +65,10 @@ export function BlockViewer({
|
|
|
57
65
|
size="sm"
|
|
58
66
|
iconOnly
|
|
59
67
|
onClick={() => setWidth("375px")}
|
|
60
|
-
className={cn("size-7",
|
|
68
|
+
className={cn("size-7", {
|
|
69
|
+
"bg-muted text-foreground": width === "375px",
|
|
70
|
+
"text-muted-foreground": width !== "375px",
|
|
71
|
+
})}
|
|
61
72
|
aria-label="Mobile view"
|
|
62
73
|
>
|
|
63
74
|
<SmartphoneIcon className="size-3.5" />
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { cn } from "../../lib/utils";
|
|
3
|
+
|
|
4
|
+
interface ContainerProps extends React.ComponentProps<"div"> {
|
|
5
|
+
as?: React.ElementType;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const Container = React.forwardRef<HTMLElement, ContainerProps>(
|
|
9
|
+
({ className, as: Component = "div", ...props }, ref) => {
|
|
10
|
+
return (
|
|
11
|
+
<Component
|
|
12
|
+
ref={ref}
|
|
13
|
+
className={cn("container max-w-6xl mx-auto px-4", className)}
|
|
14
|
+
{...props}
|
|
15
|
+
/>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
Container.displayName = "Container";
|
|
@@ -2,15 +2,12 @@ import { cn } from "../../lib/utils";
|
|
|
2
2
|
|
|
3
3
|
interface GladvnLogoProps {
|
|
4
4
|
className?: string;
|
|
5
|
-
/** Size of the icon SVG */
|
|
6
|
-
iconSize?: string;
|
|
7
5
|
/** Show icon only (default) or icon + wordmark */
|
|
8
6
|
variant?: "icon" | "wordmark";
|
|
9
7
|
}
|
|
10
8
|
|
|
11
9
|
export function GladvnLogo({
|
|
12
10
|
className,
|
|
13
|
-
iconSize = "size-7",
|
|
14
11
|
variant = "icon",
|
|
15
12
|
}: GladvnLogoProps) {
|
|
16
13
|
const icon = (
|
|
@@ -18,10 +15,11 @@ export function GladvnLogo({
|
|
|
18
15
|
viewBox="0 0 32 32"
|
|
19
16
|
fill="none"
|
|
20
17
|
xmlns="http://www.w3.org/2000/svg"
|
|
21
|
-
className={cn(
|
|
18
|
+
className={cn("size-7", {
|
|
19
|
+
[`${className}`]: variant === "icon" && !!className
|
|
20
|
+
})}
|
|
22
21
|
aria-hidden="true"
|
|
23
22
|
>
|
|
24
|
-
{/* Macro layer — outer rounded square, primary fill */}
|
|
25
23
|
<rect
|
|
26
24
|
x="1"
|
|
27
25
|
y="1"
|
|
@@ -30,7 +28,6 @@ export function GladvnLogo({
|
|
|
30
28
|
rx="7"
|
|
31
29
|
className="fill-primary"
|
|
32
30
|
/>
|
|
33
|
-
{/* Micro layer — inner square, offset top-right, semi-transparent white */}
|
|
34
31
|
<rect
|
|
35
32
|
x="11"
|
|
36
33
|
y="5"
|
|
@@ -40,7 +37,6 @@ export function GladvnLogo({
|
|
|
40
37
|
fill="white"
|
|
41
38
|
fillOpacity="0.25"
|
|
42
39
|
/>
|
|
43
|
-
{/* Micro inner highlight — brighter corner */}
|
|
44
40
|
<rect
|
|
45
41
|
x="15"
|
|
46
42
|
y="5"
|
|
@@ -50,9 +46,7 @@ export function GladvnLogo({
|
|
|
50
46
|
fill="white"
|
|
51
47
|
fillOpacity="0.2"
|
|
52
48
|
/>
|
|
53
|
-
{/* Token dot — bottom left, CSS token representation */}
|
|
54
49
|
<circle cx="8" cy="24" r="3" fill="white" fillOpacity="0.7" />
|
|
55
|
-
{/* Macro bottom bar — connects Macro to token */}
|
|
56
50
|
<rect
|
|
57
51
|
x="6"
|
|
58
52
|
y="17"
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
import { cn } from "../../lib/utils";
|
|
2
3
|
|
|
3
4
|
import { type Highlighter, createHighlighter } from "shiki";
|
|
4
5
|
|
|
5
|
-
// Singleton highlighter — loaded once, reused across all CodeHighlighter instances
|
|
6
6
|
let highlighterPromise: Promise<Highlighter> | null = null;
|
|
7
7
|
|
|
8
8
|
function getHighlighter() {
|
|
@@ -18,9 +18,11 @@ function getHighlighter() {
|
|
|
18
18
|
export function CodeHighlighter({
|
|
19
19
|
code,
|
|
20
20
|
language = "tsx",
|
|
21
|
+
className,
|
|
21
22
|
}: {
|
|
22
23
|
code: string;
|
|
23
24
|
language?: "tsx" | "ts" | "css";
|
|
25
|
+
className?: string;
|
|
24
26
|
}) {
|
|
25
27
|
const [html, setHtml] = useState<string | null>(null);
|
|
26
28
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
@@ -50,7 +52,7 @@ export function CodeHighlighter({
|
|
|
50
52
|
|
|
51
53
|
if (!html) {
|
|
52
54
|
return (
|
|
53
|
-
<div className="overflow-x-auto text-[13px] leading-relaxed font-mono">
|
|
55
|
+
<div className={cn("overflow-x-auto text-[13px] leading-relaxed font-mono", className)}>
|
|
54
56
|
<pre className="!bg-transparent !p-0 !m-0 whitespace-pre-wrap break-words">
|
|
55
57
|
<code className="text-muted-foreground">Loading...</code>
|
|
56
58
|
</pre>
|
|
@@ -72,7 +74,7 @@ export function CodeHighlighter({
|
|
|
72
74
|
`}</style>
|
|
73
75
|
<div
|
|
74
76
|
ref={containerRef}
|
|
75
|
-
className="code-highlighter overflow-x-auto text-[13px] leading-relaxed"
|
|
77
|
+
className={cn("code-highlighter overflow-x-auto text-[13px] leading-relaxed", className)}
|
|
76
78
|
dangerouslySetInnerHTML={{ __html: html }}
|
|
77
79
|
/>
|
|
78
80
|
</>
|