@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.
@@ -0,0 +1 @@
1
+ export { default } from '@bug-on/md3-tailwind';
@@ -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":[]}
@@ -0,0 +1,3 @@
1
+ export { default } from '@bug-on/md3-tailwind';
2
+ //# sourceMappingURL=plugin.mjs.map
3
+ //# sourceMappingURL=plugin.mjs.map
@@ -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.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.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",
@@ -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
- fs.copyFileSync(srcIndexCss, distIndexCss);
31
- console.log(" Copied index.css to dist/index.css");
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,
@@ -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: ThemeMode = "light",
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 colors = generateM3Theme(sourceColorHex, mode);
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
- root.setAttribute("data-theme", mode);
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";
@@ -161,22 +161,20 @@ describe("Button Component", () => {
161
161
  });
162
162
  });
163
163
 
164
- // ── New: Sentence-case label ───────────────────────────────────────────────
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("Hello world");
167
+ expect(screen.getByRole("button")).toHaveTextContent("HELLO WORLD");
170
168
  });
171
169
 
172
- it("converts all-lowercase to sentence-case", () => {
170
+ it("preserves all-lowercase", () => {
173
171
  render(<Button>hello world</Button>);
174
- expect(screen.getByRole("button")).toHaveTextContent("Hello world");
172
+ expect(screen.getByRole("button")).toHaveTextContent("hello world");
175
173
  });
176
174
 
177
- it("handles mixed case correctly", () => {
175
+ it("preserves mixed case", () => {
178
176
  render(<Button>hElLo WoRlD</Button>);
179
- expect(screen.getByRole("button")).toHaveTextContent("Hello world");
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("Rtl trailing"),
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 typeof children === "string" ? toSentenceCase(children) : children;
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
+ });