@bug-on/md3-react 3.0.0 → 3.0.2
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/.turbo/turbo-build.log +20 -12
- package/CHANGELOG.md +24 -0
- package/dist/index.css +178 -0
- package/dist/index.d.mts +65 -6
- package/dist/index.d.ts +65 -6
- package/dist/index.js +796 -487
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +742 -436
- package/dist/index.mjs.map +1 -1
- package/dist/plugin.d.mts +1 -0
- package/dist/plugin.d.ts +1 -0
- package/dist/plugin.js +13 -0
- package/dist/plugin.js.map +1 -0
- package/dist/plugin.mjs +3 -0
- package/dist/plugin.mjs.map +1 -0
- package/package.json +8 -2
- package/scripts/copy-assets.js +36 -3
- package/src/index.ts +9 -1
- package/src/lib/theme-utils.ts +19 -4
- package/src/plugin.ts +12 -0
- package/src/ui/button.test.tsx +19 -10
- package/src/ui/button.tsx +2 -6
- package/src/ui/navigation-bar.test.tsx +111 -0
- package/src/ui/navigation-bar.tsx +448 -0
- package/src/ui/navigation-rail.test.tsx +5 -4
- package/src/ui/navigation-rail.tsx +28 -26
- package/src/ui/scroll-area.tsx +4 -0
- package/src/ui/shared/constants.ts +13 -0
- package/src/ui/theme-provider/index.tsx +32 -7
- package/tsup.config.ts +3 -3
- package/test_output.txt +0 -164
- package/test_output_v2.txt +0 -5
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from '@bug-on/md3-tailwind';
|
package/dist/plugin.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from '@bug-on/md3-tailwind';
|
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var md3Tailwind = require('@bug-on/md3-tailwind');
|
|
4
|
+
|
|
5
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
6
|
+
|
|
7
|
+
var md3Tailwind__default = /*#__PURE__*/_interopDefault(md3Tailwind);
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
module.exports = md3Tailwind__default.default;
|
|
12
|
+
//# sourceMappingURL=plugin.js.map
|
|
13
|
+
//# sourceMappingURL=plugin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"plugin.js","sourcesContent":[]}
|
package/dist/plugin.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"plugin.mjs","sourcesContent":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bug-on/md3-react",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.2",
|
|
4
4
|
"description": "Material Design 3 Expressive React components",
|
|
5
5
|
"author": "Bug Ổn",
|
|
6
6
|
"license": "MIT",
|
|
@@ -42,6 +42,11 @@
|
|
|
42
42
|
"./material-symbols-self-hosted.css": {
|
|
43
43
|
"types": "./dist/material-symbols-self-hosted.css.d.ts",
|
|
44
44
|
"default": "./dist/material-symbols-self-hosted.css"
|
|
45
|
+
},
|
|
46
|
+
"./plugin": {
|
|
47
|
+
"types": "./dist/plugin.d.ts",
|
|
48
|
+
"import": "./dist/plugin.mjs",
|
|
49
|
+
"require": "./dist/plugin.js"
|
|
45
50
|
}
|
|
46
51
|
},
|
|
47
52
|
"peerDependencies": {
|
|
@@ -64,7 +69,8 @@
|
|
|
64
69
|
"class-variance-authority": "^0.7.1",
|
|
65
70
|
"clsx": "^2.1.1",
|
|
66
71
|
"tailwind-merge": "^3.3.1",
|
|
67
|
-
"@bug-on/md3-tokens": "3.0.
|
|
72
|
+
"@bug-on/md3-tokens": "3.0.1",
|
|
73
|
+
"@bug-on/md3-tailwind": "3.0.1"
|
|
68
74
|
},
|
|
69
75
|
"devDependencies": {
|
|
70
76
|
"@testing-library/jest-dom": "^6.9.1",
|
package/scripts/copy-assets.js
CHANGED
|
@@ -26,11 +26,42 @@ if (fs.existsSync(srcCss)) {
|
|
|
26
26
|
console.log("✅ Copied typography.css to dist/typography.css");
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
// Bundle index.css = md3-tokens CSS + react component base styles.
|
|
30
|
+
// This ensures users only need to import @bug-on/md3-react/index.css
|
|
31
|
+
// and get all required design tokens automatically.
|
|
32
|
+
const tokensCssDir = path.join(__dirname, "../../tokens/dist");
|
|
33
|
+
const tokensColorsCss = path.join(tokensCssDir, "colors.css");
|
|
34
|
+
const tokensShapeCss = path.join(tokensCssDir, "shape.css");
|
|
35
|
+
|
|
36
|
+
const bundleParts = [
|
|
37
|
+
"/* @bug-on/md3-tokens — MD3 System Color Tokens */",
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
if (fs.existsSync(tokensColorsCss)) {
|
|
41
|
+
bundleParts.push(fs.readFileSync(tokensColorsCss, "utf-8"));
|
|
42
|
+
console.log("✅ Bundled colors.css from md3-tokens into index.css");
|
|
43
|
+
} else {
|
|
44
|
+
console.warn("⚠️ md3-tokens colors.css not found — build tokens first");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (fs.existsSync(tokensShapeCss)) {
|
|
48
|
+
bundleParts.push(fs.readFileSync(tokensShapeCss, "utf-8"));
|
|
49
|
+
console.log("✅ Bundled shape.css from md3-tokens into index.css");
|
|
50
|
+
} else {
|
|
51
|
+
console.warn("⚠️ md3-tokens shape.css not found — build tokens first");
|
|
52
|
+
}
|
|
53
|
+
|
|
29
54
|
if (fs.existsSync(srcIndexCss)) {
|
|
30
|
-
|
|
31
|
-
|
|
55
|
+
bundleParts.push(
|
|
56
|
+
"\n/* @bug-on/md3-react — Component Base Reset */",
|
|
57
|
+
fs.readFileSync(srcIndexCss, "utf-8"),
|
|
58
|
+
);
|
|
32
59
|
}
|
|
33
60
|
|
|
61
|
+
fs.writeFileSync(distIndexCss, bundleParts.join("\n"));
|
|
62
|
+
console.log("✅ Built bundled index.css (tokens + component base) to dist/index.css");
|
|
63
|
+
|
|
64
|
+
|
|
34
65
|
// Copy Material Symbols CSS variants
|
|
35
66
|
const cssVariants = [
|
|
36
67
|
"material-symbols-cdn.css",
|
|
@@ -62,7 +93,9 @@ for (const stub of cssStubs) {
|
|
|
62
93
|
console.log(`✅ Generated ${stub}`);
|
|
63
94
|
}
|
|
64
95
|
|
|
65
|
-
// Prepend "use client" to JS/MJS output files to support React Server Components
|
|
96
|
+
// Prepend "use client" to JS/MJS output files to support React Server Components.
|
|
97
|
+
// NOTE: Using post-build script instead of tsup banner — banner causes esbuild
|
|
98
|
+
// "Module level directives" warnings in library bundling context.
|
|
66
99
|
const distFiles = [
|
|
67
100
|
path.join(__dirname, "../dist/index.js"),
|
|
68
101
|
path.join(__dirname, "../dist/index.mjs"),
|
package/src/index.ts
CHANGED
|
@@ -5,7 +5,7 @@ export { useRipple as useDOMRipple } from "./hooks/useRipple";
|
|
|
5
5
|
export { MaterialSymbolsPreconnect } from "./lib/material-symbols-preconnect";
|
|
6
6
|
// Theme — MD3 Dynamic Color
|
|
7
7
|
export type { MD3ColorScheme, ThemeMode } from "./lib/theme-utils";
|
|
8
|
-
export { applyTheme, generateM3Theme } from "./lib/theme-utils";
|
|
8
|
+
export { applyTheme, generateM3Theme, resolveMode } from "./lib/theme-utils";
|
|
9
9
|
// Utils
|
|
10
10
|
export { cn } from "./lib/utils";
|
|
11
11
|
// Types
|
|
@@ -196,6 +196,14 @@ export {
|
|
|
196
196
|
VerticalMenuGroup,
|
|
197
197
|
VIBRANT_COLORS,
|
|
198
198
|
} from "./ui/menu";
|
|
199
|
+
// Navigation Bar
|
|
200
|
+
export type {
|
|
201
|
+
NavigationBarItemProps,
|
|
202
|
+
NavigationBarItemLayout,
|
|
203
|
+
NavigationBarProps,
|
|
204
|
+
NavigationBarVariant,
|
|
205
|
+
} from "./ui/navigation-bar";
|
|
206
|
+
export { NavigationBar, NavigationBarItem } from "./ui/navigation-bar";
|
|
199
207
|
// Navigation Rail
|
|
200
208
|
export type {
|
|
201
209
|
NavigationRailItemProps,
|
package/src/lib/theme-utils.ts
CHANGED
|
@@ -4,7 +4,20 @@ import {
|
|
|
4
4
|
themeFromSourceColor,
|
|
5
5
|
} from "@material/material-color-utilities";
|
|
6
6
|
|
|
7
|
-
export type ThemeMode = "light" | "dark";
|
|
7
|
+
export type ThemeMode = "light" | "dark" | "system";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Resolves the effective color scheme from a ThemeMode.
|
|
11
|
+
* When mode is "system", reads the OS preference via matchMedia.
|
|
12
|
+
* Returns "light" as the safe default in SSR environments.
|
|
13
|
+
*/
|
|
14
|
+
export function resolveMode(mode: ThemeMode): "light" | "dark" {
|
|
15
|
+
if (mode !== "system") return mode;
|
|
16
|
+
if (typeof window === "undefined") return "light";
|
|
17
|
+
return window.matchMedia("(prefers-color-scheme: dark)").matches
|
|
18
|
+
? "dark"
|
|
19
|
+
: "light";
|
|
20
|
+
}
|
|
8
21
|
|
|
9
22
|
export interface MD3ColorScheme {
|
|
10
23
|
primary: string;
|
|
@@ -70,7 +83,7 @@ export interface MD3ColorScheme {
|
|
|
70
83
|
*/
|
|
71
84
|
export function generateM3Theme(
|
|
72
85
|
sourceColorHex: string,
|
|
73
|
-
mode:
|
|
86
|
+
mode: "light" | "dark" = "light",
|
|
74
87
|
): MD3ColorScheme {
|
|
75
88
|
const sourceColor = argbFromHex(sourceColorHex);
|
|
76
89
|
const theme = themeFromSourceColor(sourceColor);
|
|
@@ -168,7 +181,8 @@ export function applyTheme(
|
|
|
168
181
|
mode: ThemeMode = "light",
|
|
169
182
|
root: HTMLElement = document.documentElement,
|
|
170
183
|
): void {
|
|
171
|
-
const
|
|
184
|
+
const resolved = resolveMode(mode);
|
|
185
|
+
const colors = generateM3Theme(sourceColorHex, resolved);
|
|
172
186
|
|
|
173
187
|
for (const [key, value] of Object.entries(colors)) {
|
|
174
188
|
const kebabKey = key.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);
|
|
@@ -176,5 +190,6 @@ export function applyTheme(
|
|
|
176
190
|
root.style.setProperty(`--color-m3-${kebabKey}`, value);
|
|
177
191
|
}
|
|
178
192
|
|
|
179
|
-
|
|
193
|
+
// data-theme is always "light" or "dark" — never "system"
|
|
194
|
+
root.setAttribute("data-theme", resolved);
|
|
180
195
|
}
|
package/src/plugin.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Re-export the MD3 Tailwind plugin from @bug-on/md3-tailwind.
|
|
2
|
+
// This allows users to reference the plugin directly from @bug-on/md3-react
|
|
3
|
+
// without installing @bug-on/md3-tailwind separately.
|
|
4
|
+
//
|
|
5
|
+
// Usage in CSS (Tailwind v4):
|
|
6
|
+
// @import "@bug-on/md3-react/index.css";
|
|
7
|
+
// @plugin "@bug-on/md3-react/plugin";
|
|
8
|
+
//
|
|
9
|
+
// Usage in tailwind.config.ts (Tailwind v3):
|
|
10
|
+
// import md3Plugin from "@bug-on/md3-react/plugin";
|
|
11
|
+
// export default { plugins: [md3Plugin] };
|
|
12
|
+
export { default } from "@bug-on/md3-tailwind";
|
package/src/ui/button.test.tsx
CHANGED
|
@@ -161,22 +161,20 @@ describe("Button Component", () => {
|
|
|
161
161
|
});
|
|
162
162
|
});
|
|
163
163
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
describe("sentence-case label", () => {
|
|
167
|
-
it("converts ALL CAPS to sentence-case", () => {
|
|
164
|
+
describe("label case", () => {
|
|
165
|
+
it("preserves ALL CAPS", () => {
|
|
168
166
|
render(<Button>HELLO WORLD</Button>);
|
|
169
|
-
expect(screen.getByRole("button")).toHaveTextContent("
|
|
167
|
+
expect(screen.getByRole("button")).toHaveTextContent("HELLO WORLD");
|
|
170
168
|
});
|
|
171
169
|
|
|
172
|
-
it("
|
|
170
|
+
it("preserves all-lowercase", () => {
|
|
173
171
|
render(<Button>hello world</Button>);
|
|
174
|
-
expect(screen.getByRole("button")).toHaveTextContent("
|
|
172
|
+
expect(screen.getByRole("button")).toHaveTextContent("hello world");
|
|
175
173
|
});
|
|
176
174
|
|
|
177
|
-
it("
|
|
175
|
+
it("preserves mixed case", () => {
|
|
178
176
|
render(<Button>hElLo WoRlD</Button>);
|
|
179
|
-
expect(screen.getByRole("button")).toHaveTextContent("
|
|
177
|
+
expect(screen.getByRole("button")).toHaveTextContent("hElLo WoRlD");
|
|
180
178
|
});
|
|
181
179
|
});
|
|
182
180
|
|
|
@@ -215,7 +213,7 @@ describe("Button Component", () => {
|
|
|
215
213
|
s.querySelector("[data-testid='test-icon']"),
|
|
216
214
|
);
|
|
217
215
|
const labelIndex = spans.findIndex((s) =>
|
|
218
|
-
s.textContent?.includes("
|
|
216
|
+
s.textContent?.includes("RTL Trailing"),
|
|
219
217
|
);
|
|
220
218
|
// DOM order is still trailing (CSS handles visual flip in RTL via flex)
|
|
221
219
|
expect(iconIndex).toBeGreaterThan(labelIndex);
|
|
@@ -270,6 +268,17 @@ describe("Button Component", () => {
|
|
|
270
268
|
"transition-[background-color,color,border-color,box-shadow,opacity,filter]",
|
|
271
269
|
);
|
|
272
270
|
});
|
|
271
|
+
|
|
272
|
+
it("applies h-full to the label wrapper for vertical alignment", () => {
|
|
273
|
+
render(
|
|
274
|
+
<Button asChild>
|
|
275
|
+
<a href="/test">Aligned Link</a>
|
|
276
|
+
</Button>,
|
|
277
|
+
);
|
|
278
|
+
const link = screen.getByRole("link");
|
|
279
|
+
const labelWrapper = link.querySelector("span.inline-flex.items-center");
|
|
280
|
+
expect(labelWrapper).toHaveClass("h-full");
|
|
281
|
+
});
|
|
273
282
|
});
|
|
274
283
|
|
|
275
284
|
// ── New: loading prop ──────────────────────────────────────────────────────
|
package/src/ui/button.tsx
CHANGED
|
@@ -294,10 +294,6 @@ export type ButtonProps = BaseButtonProps &
|
|
|
294
294
|
|
|
295
295
|
// ─── Helpers ───────────────────────────────────────────────────────────────────
|
|
296
296
|
|
|
297
|
-
function toSentenceCase(text: string): string {
|
|
298
|
-
return text.charAt(0).toUpperCase() + text.slice(1).toLowerCase();
|
|
299
|
-
}
|
|
300
|
-
|
|
301
297
|
function resolveLabel(
|
|
302
298
|
children: React.ReactNode,
|
|
303
299
|
asChild: boolean,
|
|
@@ -308,7 +304,7 @@ function resolveLabel(
|
|
|
308
304
|
}>;
|
|
309
305
|
return child.props.children;
|
|
310
306
|
}
|
|
311
|
-
return
|
|
307
|
+
return children;
|
|
312
308
|
}
|
|
313
309
|
|
|
314
310
|
/** Framer Motion-specific props to strip before forwarding to a plain DOM element. */
|
|
@@ -550,7 +546,7 @@ const ButtonComponent = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
|
550
546
|
|
|
551
547
|
<m.span
|
|
552
548
|
layout="size"
|
|
553
|
-
className="inline-flex items-center gap-[inherit]"
|
|
549
|
+
className="inline-flex items-center h-full gap-[inherit]"
|
|
554
550
|
transition={SPRING_TRANSITION}
|
|
555
551
|
>
|
|
556
552
|
{labelText}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { fireEvent, render, screen } from "@testing-library/react";
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { NavigationBar, NavigationBarItem } from "./navigation-bar";
|
|
4
|
+
|
|
5
|
+
describe("NavigationBar", () => {
|
|
6
|
+
it("renders with correct role and aria-label", () => {
|
|
7
|
+
render(
|
|
8
|
+
<NavigationBar>
|
|
9
|
+
<NavigationBarItem selected icon={<span />} label="Home" />
|
|
10
|
+
</NavigationBar>,
|
|
11
|
+
);
|
|
12
|
+
const nav = screen.getByRole("navigation", { name: "Main navigation" });
|
|
13
|
+
expect(nav).toBeInTheDocument();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("renders correct number of items", () => {
|
|
17
|
+
render(
|
|
18
|
+
<NavigationBar>
|
|
19
|
+
<NavigationBarItem selected icon={<span />} label="Home" />
|
|
20
|
+
<NavigationBarItem selected={false} icon={<span />} label="Search" />
|
|
21
|
+
</NavigationBar>,
|
|
22
|
+
);
|
|
23
|
+
const items = screen.getAllByRole("menuitem");
|
|
24
|
+
expect(items).toHaveLength(2);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('marks selected item with aria-current="page"', () => {
|
|
28
|
+
render(
|
|
29
|
+
<NavigationBar>
|
|
30
|
+
<NavigationBarItem selected icon={<span />} label="Home" />
|
|
31
|
+
<NavigationBarItem selected={false} icon={<span />} label="Search" />
|
|
32
|
+
</NavigationBar>,
|
|
33
|
+
);
|
|
34
|
+
const selectedItem = screen.getByRole("menuitem", { current: "page" });
|
|
35
|
+
expect(selectedItem).toHaveTextContent("Home");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("calls onClick when item clicked", () => {
|
|
39
|
+
const onClick = vi.fn();
|
|
40
|
+
render(
|
|
41
|
+
<NavigationBar>
|
|
42
|
+
<NavigationBarItem
|
|
43
|
+
selected={false}
|
|
44
|
+
icon={<span />}
|
|
45
|
+
label="Home"
|
|
46
|
+
onClick={onClick}
|
|
47
|
+
/>
|
|
48
|
+
</NavigationBar>,
|
|
49
|
+
);
|
|
50
|
+
const item = screen.getByRole("menuitem");
|
|
51
|
+
fireEvent.click(item);
|
|
52
|
+
expect(onClick).toHaveBeenCalledTimes(1);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("does not call onClick when disabled", () => {
|
|
56
|
+
const onClick = vi.fn();
|
|
57
|
+
render(
|
|
58
|
+
<NavigationBar>
|
|
59
|
+
<NavigationBarItem
|
|
60
|
+
selected={false}
|
|
61
|
+
disabled
|
|
62
|
+
icon={<span />}
|
|
63
|
+
label="Home"
|
|
64
|
+
onClick={onClick}
|
|
65
|
+
/>
|
|
66
|
+
</NavigationBar>,
|
|
67
|
+
);
|
|
68
|
+
const item = screen.getByRole("menuitem");
|
|
69
|
+
fireEvent.click(item);
|
|
70
|
+
expect(onClick).not.toHaveBeenCalled();
|
|
71
|
+
expect(item).toHaveAttribute("aria-disabled", "true");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("renders badge when provided", () => {
|
|
75
|
+
render(
|
|
76
|
+
<NavigationBar>
|
|
77
|
+
<NavigationBarItem selected icon={<span />} label="Home" badge="1" />
|
|
78
|
+
</NavigationBar>,
|
|
79
|
+
);
|
|
80
|
+
const badge = screen.getByText("1");
|
|
81
|
+
expect(badge).toBeInTheDocument();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("uses aria-label prop when provided", () => {
|
|
85
|
+
render(
|
|
86
|
+
<NavigationBar>
|
|
87
|
+
<NavigationBarItem
|
|
88
|
+
selected
|
|
89
|
+
icon={<span />}
|
|
90
|
+
label="Home"
|
|
91
|
+
aria-label="Go to homepage"
|
|
92
|
+
/>
|
|
93
|
+
</NavigationBar>,
|
|
94
|
+
);
|
|
95
|
+
const item = screen.getByRole("menuitem");
|
|
96
|
+
expect(item).toHaveAttribute("aria-label", "Go to homepage");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("renders xr variant with correct classes", () => {
|
|
100
|
+
render(
|
|
101
|
+
<NavigationBar variant="xr">
|
|
102
|
+
<NavigationBarItem selected icon={<span />} label="Home" />
|
|
103
|
+
</NavigationBar>,
|
|
104
|
+
);
|
|
105
|
+
const nav = screen.getByRole("navigation", { name: "Main navigation" });
|
|
106
|
+
expect(nav).toHaveClass("bottom-6");
|
|
107
|
+
expect(nav).toHaveClass("left-1/2");
|
|
108
|
+
expect(nav).toHaveClass("-translate-x-1/2");
|
|
109
|
+
expect(nav).toHaveClass("rounded-[48px]");
|
|
110
|
+
});
|
|
111
|
+
});
|