@bug-on/md3-react 3.0.1 → 3.0.3

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.
Files changed (86) hide show
  1. package/.turbo/turbo-build.log +42 -42
  2. package/CHANGELOG.md +10 -0
  3. package/dist/index.css +107 -0
  4. package/dist/index.d.mts +1491 -1053
  5. package/dist/index.d.ts +1491 -1053
  6. package/dist/index.js +4457 -3156
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.mjs +4394 -3109
  9. package/dist/index.mjs.map +1 -1
  10. package/package.json +11 -6
  11. package/scripts/copy-assets.js +113 -8
  12. package/src/index.ts +66 -18
  13. package/src/test/button.test.tsx +1 -1
  14. package/src/ui/app-bar/app-bar.tokens.ts +5 -24
  15. package/src/ui/badge.tsx +2 -1
  16. package/src/ui/buttons/button/button-tokens.ts +118 -0
  17. package/src/ui/{button.test.tsx → buttons/button/button.test.tsx} +0 -21
  18. package/src/ui/buttons/button/button.tsx +381 -0
  19. package/src/ui/buttons/button/index.ts +3 -0
  20. package/src/ui/buttons/button/types.ts +90 -0
  21. package/src/ui/buttons/button-group/button-group-defaults.ts +95 -0
  22. package/src/ui/buttons/button-group/button-group-tokens.ts +20 -0
  23. package/src/ui/{button-group.test.tsx → buttons/button-group/button-group.test.tsx} +9 -10
  24. package/src/ui/buttons/button-group/button-group.tsx +699 -0
  25. package/src/ui/buttons/button-group/index.ts +8 -0
  26. package/src/ui/buttons/button-group/types.ts +77 -0
  27. package/src/ui/{fab.tsx → buttons/fabs/fab/fab.tsx} +6 -6
  28. package/src/ui/buttons/fabs/fab/index.ts +1 -0
  29. package/src/ui/{fab-menu.tsx → buttons/fabs/fab-menu/fab-menu.tsx} +7 -4
  30. package/src/ui/buttons/fabs/fab-menu/index.ts +1 -0
  31. package/src/ui/buttons/fabs/index.ts +2 -0
  32. package/src/ui/{icon-button.tsx → buttons/icon-button/icon-button.tsx} +6 -6
  33. package/src/ui/buttons/icon-button/index.ts +1 -0
  34. package/src/ui/buttons/index.ts +4 -0
  35. package/src/ui/code-block.tsx +1 -1
  36. package/src/ui/dialog.tsx +4 -7
  37. package/src/ui/drawer.tsx +4 -7
  38. package/src/ui/menu/menu-animations.ts +14 -20
  39. package/src/ui/menu/menu-tokens.ts +7 -5
  40. package/src/ui/menu/menu.test.tsx +9 -4
  41. package/src/ui/navigation-bar.test.tsx +111 -0
  42. package/src/ui/navigation-bar.tsx +464 -0
  43. package/src/ui/navigation-rail.test.tsx +5 -4
  44. package/src/ui/navigation-rail.tsx +32 -23
  45. package/src/ui/scroll-area.tsx +4 -0
  46. package/src/ui/search/search-view-fullscreen.tsx +1 -1
  47. package/src/ui/search/search.tokens.ts +9 -43
  48. package/src/ui/search/trailing-action.tsx +1 -1
  49. package/src/ui/shared/constants.ts +25 -27
  50. package/src/ui/shared/motion-tokens.ts +238 -0
  51. package/src/ui/snackbar/snackbar.tsx +4 -6
  52. package/src/ui/switch/switch.tsx +12 -18
  53. package/src/ui/text-field/text-field.tokens.ts +12 -12
  54. package/src/ui/text-field/text-field.tsx +31 -19
  55. package/src/ui/theme-provider/index.tsx +1 -5
  56. package/src/ui/toc.tsx +1 -1
  57. package/src/ui/toolbar/__snapshots__/bottom-docked-toolbar.test.tsx.snap +51 -0
  58. package/src/ui/toolbar/__snapshots__/floating-toolbar-with-fab.test.tsx.snap +113 -0
  59. package/src/ui/toolbar/__snapshots__/floating-toolbar.test.tsx.snap +169 -0
  60. package/src/ui/toolbar/bottom-docked-toolbar.test.tsx +114 -0
  61. package/src/ui/toolbar/docked-toolbar.tsx +186 -0
  62. package/src/ui/toolbar/floating-toolbar-with-fab.test.tsx +139 -0
  63. package/src/ui/toolbar/floating-toolbar-with-fab.tsx +199 -0
  64. package/src/ui/toolbar/floating-toolbar.test.tsx +230 -0
  65. package/src/ui/toolbar/floating-toolbar.tsx +344 -0
  66. package/src/ui/toolbar/index.ts +35 -0
  67. package/src/ui/toolbar/toolbar-colors.ts +37 -0
  68. package/src/ui/toolbar/toolbar-context.tsx +13 -0
  69. package/src/ui/toolbar/toolbar-divider.test.tsx +54 -0
  70. package/src/ui/toolbar/toolbar-divider.tsx +73 -0
  71. package/src/ui/toolbar/toolbar-icon-button.test.tsx +68 -0
  72. package/src/ui/toolbar/toolbar-icon-button.tsx +136 -0
  73. package/src/ui/toolbar/toolbar-scroll-behavior.ts +140 -0
  74. package/src/ui/toolbar/toolbar-tokens.ts +51 -0
  75. package/test-clip.html +31 -0
  76. package/test-shadow.html +5 -1
  77. package/test-width.html +34 -0
  78. package/src/ui/button-group.tsx +0 -350
  79. package/src/ui/button.tsx +0 -665
  80. package/test-render.tsx +0 -4
  81. package/test_output.txt +0 -164
  82. package/test_output_v2.txt +0 -5
  83. /package/src/ui/{fab.test.tsx → buttons/fabs/fab/fab.test.tsx} +0 -0
  84. /package/src/ui/{fab-menu.test.tsx → buttons/fabs/fab-menu/fab-menu.test.tsx} +0 -0
  85. /package/src/ui/{icon-button.test.tsx → buttons/icon-button/icon-button.test.tsx} +0 -0
  86. /package/src/ui/{Text.tsx → text.tsx} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bug-on/md3-react",
3
- "version": "3.0.1",
3
+ "version": "3.0.3",
4
4
  "description": "Material Design 3 Expressive React components",
5
5
  "author": "Bug Ổn",
6
6
  "license": "MIT",
@@ -23,9 +23,14 @@
23
23
  "types": "./dist/index.d.ts",
24
24
  "exports": {
25
25
  ".": {
26
- "types": "./dist/index.d.ts",
27
- "import": "./dist/index.mjs",
28
- "require": "./dist/index.js"
26
+ "import": {
27
+ "types": "./dist/index.d.mts",
28
+ "default": "./dist/index.mjs"
29
+ },
30
+ "require": {
31
+ "types": "./dist/index.d.ts",
32
+ "default": "./dist/index.js"
33
+ }
29
34
  },
30
35
  "./typography.css": {
31
36
  "types": "./dist/typography.css.d.ts",
@@ -69,8 +74,8 @@
69
74
  "class-variance-authority": "^0.7.1",
70
75
  "clsx": "^2.1.1",
71
76
  "tailwind-merge": "^3.3.1",
72
- "@bug-on/md3-tokens": "3.0.1",
73
- "@bug-on/md3-tailwind": "3.0.1"
77
+ "@bug-on/md3-tokens": "3.0.3",
78
+ "@bug-on/md3-tailwind": "3.0.3"
74
79
  },
75
80
  "devDependencies": {
76
81
  "@testing-library/jest-dom": "^6.9.1",
@@ -26,16 +26,13 @@ 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.
29
+ // Bundle index.css = md3-tokens CSS + react component base styles + @theme block.
30
+ // Users only need: @import "@bug-on/md3-react/index.css" — zero manual config.
32
31
  const tokensCssDir = path.join(__dirname, "../../tokens/dist");
33
32
  const tokensColorsCss = path.join(tokensCssDir, "colors.css");
34
33
  const tokensShapeCss = path.join(tokensCssDir, "shape.css");
35
34
 
36
- const bundleParts = [
37
- "/* @bug-on/md3-tokens — MD3 System Color Tokens */",
38
- ];
35
+ const bundleParts = ["/* @bug-on/md3-tokens — MD3 System Color Tokens */"];
39
36
 
40
37
  if (fs.existsSync(tokensColorsCss)) {
41
38
  bundleParts.push(fs.readFileSync(tokensColorsCss, "utf-8"));
@@ -58,9 +55,117 @@ if (fs.existsSync(srcIndexCss)) {
58
55
  );
59
56
  }
60
57
 
61
- fs.writeFileSync(distIndexCss, bundleParts.join("\n"));
62
- console.log("✅ Built bundled index.css (tokens + component base) to dist/index.css");
58
+ // ── Tailwind v4 @theme block ─────────────────────────────────────────────────
59
+ // Registers MD3 system tokens as Tailwind design tokens so utility classes
60
+ // like bg-m3-primary, text-m3-on-surface, rounded-m3-xl work automatically.
61
+ // Tailwind v4 processes @theme blocks inside @imported CSS files.
62
+ const tailwindThemeBlock = `
63
+ /* @bug-on/md3-react — Tailwind v4 Theme Registration */
64
+ /* Registers MD3 tokens as Tailwind utilities. No manual @theme mapping needed. */
65
+ @theme {
66
+ /* ── Colors: Primary ── */
67
+ --color-m3-primary: var(--md-sys-color-primary);
68
+ --color-m3-on-primary: var(--md-sys-color-on-primary);
69
+ --color-m3-primary-container: var(--md-sys-color-primary-container);
70
+ --color-m3-on-primary-container: var(--md-sys-color-on-primary-container);
71
+ --color-m3-primary-fixed: var(--md-sys-color-primary-fixed);
72
+ --color-m3-primary-fixed-dim: var(--md-sys-color-primary-fixed-dim);
73
+ --color-m3-on-primary-fixed: var(--md-sys-color-on-primary-fixed);
74
+ --color-m3-on-primary-fixed-variant: var(--md-sys-color-on-primary-fixed-variant);
75
+
76
+ /* ── Colors: Secondary ── */
77
+ --color-m3-secondary: var(--md-sys-color-secondary);
78
+ --color-m3-on-secondary: var(--md-sys-color-on-secondary);
79
+ --color-m3-secondary-container: var(--md-sys-color-secondary-container);
80
+ --color-m3-on-secondary-container: var(--md-sys-color-on-secondary-container);
81
+ --color-m3-secondary-fixed: var(--md-sys-color-secondary-fixed);
82
+ --color-m3-secondary-fixed-dim: var(--md-sys-color-secondary-fixed-dim);
83
+ --color-m3-on-secondary-fixed: var(--md-sys-color-on-secondary-fixed);
84
+ --color-m3-on-secondary-fixed-variant: var(--md-sys-color-on-secondary-fixed-variant);
85
+
86
+ /* ── Colors: Tertiary ── */
87
+ --color-m3-tertiary: var(--md-sys-color-tertiary);
88
+ --color-m3-on-tertiary: var(--md-sys-color-on-tertiary);
89
+ --color-m3-tertiary-container: var(--md-sys-color-tertiary-container);
90
+ --color-m3-on-tertiary-container: var(--md-sys-color-on-tertiary-container);
91
+ --color-m3-tertiary-fixed: var(--md-sys-color-tertiary-fixed);
92
+ --color-m3-tertiary-fixed-dim: var(--md-sys-color-tertiary-fixed-dim);
93
+ --color-m3-on-tertiary-fixed: var(--md-sys-color-on-tertiary-fixed);
94
+ --color-m3-on-tertiary-fixed-variant: var(--md-sys-color-on-tertiary-fixed-variant);
95
+
96
+ /* ── Colors: Error ── */
97
+ --color-m3-error: var(--md-sys-color-error);
98
+ --color-m3-on-error: var(--md-sys-color-on-error);
99
+ --color-m3-error-container: var(--md-sys-color-error-container);
100
+ --color-m3-on-error-container: var(--md-sys-color-on-error-container);
101
+
102
+ /* ── Colors: Surface ── */
103
+ --color-m3-surface: var(--md-sys-color-surface);
104
+ --color-m3-surface-dim: var(--md-sys-color-surface-dim);
105
+ --color-m3-surface-bright: var(--md-sys-color-surface-bright);
106
+ --color-m3-on-surface: var(--md-sys-color-on-surface);
107
+ --color-m3-surface-variant: var(--md-sys-color-surface-variant);
108
+ --color-m3-on-surface-variant: var(--md-sys-color-on-surface-variant);
109
+ --color-m3-surface-container-lowest: var(--md-sys-color-surface-container-lowest);
110
+ --color-m3-surface-container-low: var(--md-sys-color-surface-container-low);
111
+ --color-m3-surface-container: var(--md-sys-color-surface-container);
112
+ --color-m3-surface-container-high: var(--md-sys-color-surface-container-high);
113
+ --color-m3-surface-container-highest: var(--md-sys-color-surface-container-highest);
114
+
115
+ /* ── Colors: Inverse ── */
116
+ --color-m3-inverse-surface: var(--md-sys-color-inverse-surface);
117
+ --color-m3-inverse-on-surface: var(--md-sys-color-inverse-on-surface);
118
+ --color-m3-inverse-primary: var(--md-sys-color-inverse-primary);
119
+
120
+ /* ── Colors: Outline / Shadow / Scrim ── */
121
+ --color-m3-outline: var(--md-sys-color-outline);
122
+ --color-m3-outline-variant: var(--md-sys-color-outline-variant);
123
+ --color-m3-shadow: var(--md-sys-color-shadow);
124
+ --color-m3-scrim: var(--md-sys-color-scrim);
125
+ --color-m3-surface-tint: var(--md-sys-color-surface-tint);
126
+
127
+ /* ── Colors: Background ── */
128
+ --color-m3-background: var(--md-sys-color-background);
129
+ --color-m3-on-background: var(--md-sys-color-on-background);
130
+
131
+ /* ── Colors: Navigation Indicator ── */
132
+ --color-m3-indicator-active: var(--md-sys-color-indicator-active);
133
+ --color-m3-indicator-container: var(--md-sys-color-indicator-container);
134
+ --color-m3-indicator-contained-active: var(--md-sys-color-indicator-contained-active);
135
+ --color-m3-indicator-contained-container: var(--md-sys-color-indicator-contained-container);
136
+ --color-m3-indicator-track: var(--md-sys-color-indicator-track);
137
+ --color-m3-indicator-stop: var(--md-sys-color-indicator-stop);
138
+
139
+ /* ── Shape: Corner Radius Scale (10 levels, M3 Expressive) ── */
140
+ --radius-m3-none: var(--md-sys-shape-corner-none, 0px);
141
+ --radius-m3-xs: var(--md-sys-shape-corner-extra-small, 4px);
142
+ --radius-m3-sm: var(--md-sys-shape-corner-small, 8px);
143
+ --radius-m3-md: var(--md-sys-shape-corner-medium, 12px);
144
+ --radius-m3-lg: var(--md-sys-shape-corner-large, 16px);
145
+ --radius-m3-lg-inc: var(--md-sys-shape-corner-large-increased, 20px);
146
+ --radius-m3-xl: var(--md-sys-shape-corner-extra-large, 28px);
147
+ --radius-m3-xl-inc: var(--md-sys-shape-corner-extra-large-increased, 32px);
148
+ --radius-m3-xxl: var(--md-sys-shape-corner-extra-extra-large, 48px);
149
+ --radius-m3-full: var(--md-sys-shape-corner-full, 9999px);
150
+
151
+ /* ── Motion Easing ── */
152
+ --ease-m3-emphasized: cubic-bezier(0.2, 0, 0, 1);
153
+ --ease-m3-emphasized-accelerate: cubic-bezier(0.3, 0, 0.8, 0.15);
154
+ --ease-m3-emphasized-decelerate: cubic-bezier(0.05, 0.7, 0.1, 1);
155
+ --ease-m3-standard: cubic-bezier(0.2, 0, 0, 1);
156
+ --ease-m3-standard-accelerate: cubic-bezier(0.3, 0, 1, 1);
157
+ --ease-m3-standard-decelerate: cubic-bezier(0, 0, 0, 1);
158
+ }
159
+ `;
160
+ bundleParts.push(tailwindThemeBlock);
63
161
 
162
+ fs.writeFileSync(distIndexCss, bundleParts.join("\n"));
163
+ console.log(
164
+ "✅ Built bundled index.css (tokens + @theme + component base) to dist/index.css",
165
+ );
166
+ console.log(
167
+ "✅ Tailwind v4 @theme block injected — bg-m3-primary etc. work without manual mapping",
168
+ );
64
169
 
65
170
  // Copy Material Symbols CSS variants
66
171
  const cssVariants = [
package/src/index.ts CHANGED
@@ -62,10 +62,34 @@ export {
62
62
  export type { BadgedBoxProps, BadgeProps } from "./ui/badge";
63
63
  // Badge — MD3 Expressive status indicator
64
64
  export { Badge, BadgedBox } from "./ui/badge";
65
- export type { BaseButtonProps, ButtonProps } from "./ui/button";
66
- export { Button } from "./ui/button";
67
- export type { ButtonGroupProps } from "./ui/button-group";
68
- export { ButtonGroup } from "./ui/button-group";
65
+ export type { BaseButtonProps, ButtonProps } from "./ui/buttons/button";
66
+ export {
67
+ BUTTON_COLOR_TOKENS,
68
+ BUTTON_SIZE_TOKENS,
69
+ Button,
70
+ } from "./ui/buttons/button";
71
+ export type {
72
+ ButtonGroupOrientation,
73
+ ButtonGroupProps,
74
+ ButtonGroupVariant,
75
+ } from "./ui/buttons/button-group";
76
+ export { ButtonGroup } from "./ui/buttons/button-group";
77
+ export type { FABPositionProps, FABProps } from "./ui/buttons/fabs/fab";
78
+ // Floating Action Button
79
+ export { FAB, FABPosition } from "./ui/buttons/fabs/fab";
80
+ export type {
81
+ FABMenuItemData,
82
+ FABMenuItemProps,
83
+ FABMenuProps,
84
+ ToggleFABProps,
85
+ } from "./ui/buttons/fabs/fab-menu";
86
+ // FAB Menu
87
+ export { FABMenu, FABMenuItem, ToggleFAB } from "./ui/buttons/fabs/fab-menu";
88
+ export type {
89
+ BaseIconButtonProps,
90
+ IconButtonProps,
91
+ } from "./ui/buttons/icon-button";
92
+ export { IconButton } from "./ui/buttons/icon-button";
69
93
  export type { CardProps } from "./ui/card";
70
94
  export { Card } from "./ui/card";
71
95
  // Checkbox — MD3 Expressive tri-state checkbox
@@ -117,22 +141,9 @@ export {
117
141
  DrawerTitle,
118
142
  DrawerTrigger,
119
143
  } from "./ui/drawer";
120
- export type { FABPositionProps, FABProps } from "./ui/fab";
121
- // Floating Action Button
122
- export { FAB, FABPosition } from "./ui/fab";
123
- export type {
124
- FABMenuItemData,
125
- FABMenuItemProps,
126
- FABMenuProps,
127
- ToggleFABProps,
128
- } from "./ui/fab-menu";
129
- // FAB Menu
130
- export { FABMenu, FABMenuItem, ToggleFAB } from "./ui/fab-menu";
131
144
  // Icon — Material Symbols variable font
132
145
  export type { IconProps } from "./ui/icon";
133
146
  export { Icon } from "./ui/icon";
134
- export type { BaseIconButtonProps, IconButtonProps } from "./ui/icon-button";
135
- export { IconButton } from "./ui/icon-button";
136
147
  export type { LoadingIndicatorProps } from "./ui/loading-indicator";
137
148
  export { LoadingIndicator } from "./ui/loading-indicator";
138
149
  // MD3 Expressive Menu — Standard + Vibrant + shape morphing
@@ -196,6 +207,14 @@ export {
196
207
  VerticalMenuGroup,
197
208
  VIBRANT_COLORS,
198
209
  } from "./ui/menu";
210
+ // Navigation Bar
211
+ export type {
212
+ NavigationBarItemLayout,
213
+ NavigationBarItemProps,
214
+ NavigationBarProps,
215
+ NavigationBarVariant,
216
+ } from "./ui/navigation-bar";
217
+ export { NavigationBar, NavigationBarItem } from "./ui/navigation-bar";
199
218
  // Navigation Rail
200
219
  export type {
201
220
  NavigationRailItemProps,
@@ -281,7 +300,6 @@ export {
281
300
  // Switch — MD3 Expressive toggle
282
301
  export type { SwitchProps } from "./ui/switch";
283
302
  export { Switch, SwitchColors, SwitchTokens } from "./ui/switch";
284
- export { Text, type TextProps } from "./ui/Text";
285
303
  // Tabs — MD3 Expressive navigation tabs
286
304
  export type {
287
305
  TabProps,
@@ -298,6 +316,9 @@ export {
298
316
  TabsList,
299
317
  TabsTokens,
300
318
  } from "./ui/tabs";
319
+ // Text — MD3 Expressive Typography component
320
+ export type { TextProps } from "./ui/text";
321
+ export { Text } from "./ui/text";
301
322
  // TextField — MD3 Expressive
302
323
  export type {
303
324
  TextFieldHandle,
@@ -311,6 +332,33 @@ export type { MD3ThemeProviderProps } from "./ui/theme-provider";
311
332
  export { MD3ThemeProvider, useTheme, useThemeMode } from "./ui/theme-provider";
312
333
  export type { TableOfContentsProps, ToCItem } from "./ui/toc";
313
334
  export { TableOfContents } from "./ui/toc";
335
+ // Toolbar — MD3 Expressive
336
+ export type {
337
+ BottomDockedToolbarProps,
338
+ FloatingToolbarColors,
339
+ FloatingToolbarProps,
340
+ FloatingToolbarScrollBehavior,
341
+ FloatingToolbarWithFabProps,
342
+ ToolbarDividerProps,
343
+ ToolbarIconButtonProps,
344
+ ToolbarIconButtonSize,
345
+ ToolbarIconButtonVariant,
346
+ UseFloatingToolbarScrollBehaviorOptions,
347
+ } from "./ui/toolbar";
348
+ export {
349
+ BottomDockedToolbar,
350
+ HorizontalFloatingToolbar,
351
+ HorizontalFloatingToolbarWithFab,
352
+ standardFloatingToolbarColors,
353
+ ToolbarDivider,
354
+ ToolbarDividerTokens,
355
+ ToolbarIconButton,
356
+ ToolbarIconButtonTokens,
357
+ useFloatingToolbarScrollBehavior,
358
+ VerticalFloatingToolbar,
359
+ VerticalFloatingToolbarWithFab,
360
+ vibrantFloatingToolbarColors,
361
+ } from "./ui/toolbar";
314
362
  // Tooltip — MD3 Expressive
315
363
  export type {
316
364
  CaretConfig,
@@ -1,7 +1,7 @@
1
1
  import { render, screen } from "@testing-library/react";
2
2
  import userEvent from "@testing-library/user-event";
3
3
  import { describe, expect, it, vi } from "vitest";
4
- import { Button } from "../ui/button";
4
+ import { Button } from "../ui/buttons/button";
5
5
 
6
6
  describe("Button Loading State", () => {
7
7
  it("does not render LoadingIndicator by default", () => {
@@ -14,6 +14,8 @@
14
14
  * @see docs/m3/app-bars/
15
15
  */
16
16
 
17
+ import { DEFAULT_SPATIAL_SPRING } from "../shared/motion-tokens";
18
+
17
19
  // ─── Dimensional Tokens ───────────────────────────────────────────────────────
18
20
 
19
21
  /**
@@ -186,32 +188,11 @@ export const APP_BAR_COLOR_TRANSITION = {
186
188
  * Spring animation for enterAlways behavior (hide/show on scroll direction).
187
189
  * Equivalent to MD3 FastSpatial motion scheme.
188
190
  */
189
- export const APP_BAR_ENTER_ALWAYS_SPRING = {
190
- type: "spring",
191
- stiffness: 380,
192
- damping: 40,
193
- mass: 1,
194
- } as const;
191
+ export const APP_BAR_ENTER_ALWAYS_SPRING = DEFAULT_SPATIAL_SPRING;
195
192
 
196
- /**
197
- * Spring animation for Bottom App Bar hide/show.
198
- * Slightly looser feel for bottom navigation.
199
- */
200
- export const APP_BAR_BOTTOM_SPRING = {
201
- type: "spring",
202
- stiffness: 300,
203
- damping: 30,
204
- } as const;
193
+ export const APP_BAR_BOTTOM_SPRING = DEFAULT_SPATIAL_SPRING;
205
194
 
206
- /**
207
- * SearchView appearance/disappearance transition.
208
- * Uses spring for natural feel of expanding overlay.
209
- */
210
- export const SEARCH_VIEW_SPRING = {
211
- type: "spring",
212
- stiffness: 400,
213
- damping: 35,
214
- } as const;
195
+ export const SEARCH_VIEW_SPRING = DEFAULT_SPATIAL_SPRING;
215
196
 
216
197
  /**
217
198
  * Title crossfade transition for flexible App Bars.
package/src/ui/badge.tsx CHANGED
@@ -32,6 +32,7 @@ import {
32
32
  } from "motion/react";
33
33
  import * as React from "react";
34
34
  import { cn } from "../lib/utils";
35
+ import { FAST_SPATIAL_SPRING } from "./shared/motion-tokens";
35
36
 
36
37
  // ─────────────────────────────────────────────────────────────────────────────
37
38
  // Types
@@ -159,7 +160,7 @@ const BadgeImpl = React.forwardRef<HTMLSpanElement, BadgeProps>(
159
160
 
160
161
  const springTransition = reducedMotion
161
162
  ? { duration: 0 }
162
- : { type: "spring" as const, stiffness: 500, damping: 30, mass: 0.8 };
163
+ : FAST_SPATIAL_SPRING;
163
164
 
164
165
  const colorStyle: React.CSSProperties = {
165
166
  ...(containerColor && { backgroundColor: containerColor }),
@@ -0,0 +1,118 @@
1
+ import { cornerRadius } from "@bug-on/md3-tokens";
2
+ import type { Transition } from "motion";
3
+ import {
4
+ FAST_EFFECTS_SPRING,
5
+ FAST_SPATIAL_SPRING,
6
+ } from "../../shared/motion-tokens";
7
+
8
+ /**
9
+ * Shape morphing spring animation for layout/width morphing.
10
+ * MD3: `fast.spatial` — stiffness 800, dampingRatio 0.6 (has bounce).
11
+ */
12
+ export const BUTTON_SHAPE_MORPH_SPRING: Transition = FAST_SPATIAL_SPRING;
13
+
14
+ /**
15
+ * Spring for border-radius changes on press/release and shape toggle.
16
+ * MD3: `fast.effects` — stiffness 3800, critically damped (dampingRatio 1.0).
17
+ * No overshoot; border-radius never goes negative.
18
+ */
19
+ export const BUTTON_RADIUS_SPRING: Transition = FAST_EFFECTS_SPRING;
20
+
21
+ export const ROUND_SHAPE_RADIUS = 9999;
22
+
23
+ export const BUTTON_SIZE_TOKENS = {
24
+ xs: {
25
+ height: "2rem", // 32dp
26
+ leadingSpace: "1rem", // 16dp
27
+ trailingSpace: "1rem", // 16dp
28
+ iconSize: "1.25rem", // 20dp
29
+ iconLabelSpace: "0.5rem", // 8dp
30
+ outlineWidth: 1, // 1dp
31
+ squareShapeRadius: cornerRadius.medium, // 12dp
32
+ pressedShapeRadius: cornerRadius.small, // 8dp
33
+ roundShapeRadius: 16,
34
+ },
35
+ sm: {
36
+ height: "2.5rem", // 40dp
37
+ leadingSpace: "1rem", // 16dp
38
+ trailingSpace: "1rem", // 16dp
39
+ iconSize: "1.25rem", // 20dp
40
+ iconLabelSpace: "0.5rem", // 8dp
41
+ outlineWidth: 1, // 1dp
42
+ squareShapeRadius: cornerRadius.medium, // 12dp
43
+ pressedShapeRadius: cornerRadius.small, // 8dp
44
+ roundShapeRadius: 20,
45
+ },
46
+ md: {
47
+ height: "3.5rem", // 56dp
48
+ leadingSpace: "1.5rem", // 24dp
49
+ trailingSpace: "1.5rem", // 24dp
50
+ iconSize: "1.5rem", // 24dp
51
+ iconLabelSpace: "0.5rem", // 8dp
52
+ outlineWidth: 1, // 1dp
53
+ squareShapeRadius: cornerRadius.large, // 16dp
54
+ pressedShapeRadius: cornerRadius.medium, // 12dp
55
+ roundShapeRadius: 28,
56
+ },
57
+ lg: {
58
+ height: "6rem", // 96dp
59
+ leadingSpace: "3rem", // 48dp
60
+ trailingSpace: "3rem", // 48dp
61
+ iconSize: "2rem", // 32dp
62
+ iconLabelSpace: "0.75rem", // 12dp
63
+ outlineWidth: 2, // 2dp
64
+ squareShapeRadius: cornerRadius.extraLarge, // 28dp
65
+ pressedShapeRadius: cornerRadius.large, // 16dp
66
+ roundShapeRadius: 48,
67
+ },
68
+ xl: {
69
+ height: "8.5rem", // 136dp
70
+ leadingSpace: "4rem", // 64dp
71
+ trailingSpace: "4rem", // 64dp
72
+ iconSize: "2.5rem", // 40dp
73
+ iconLabelSpace: "1rem", // 16dp
74
+ outlineWidth: 3, // 3dp
75
+ squareShapeRadius: cornerRadius.extraLarge, // 28dp
76
+ pressedShapeRadius: cornerRadius.large, // 16dp
77
+ roundShapeRadius: 68,
78
+ },
79
+ } as const;
80
+
81
+ export const BUTTON_TYPOGRAPHY_TOKENS = {
82
+ xs: "text-sm font-medium tracking-wide", // labelLarge
83
+ sm: "text-sm font-medium tracking-wide", // labelLarge
84
+ md: "text-base font-medium", // titleMedium
85
+ lg: "text-xl font-normal", // headlineSmall
86
+ xl: "text-[2rem] font-normal leading-none", // headlineLarge
87
+ } as const;
88
+
89
+ export const BUTTON_COLOR_TOKENS = {
90
+ elevated: {
91
+ default: "bg-m3-surface-container-low text-m3-primary shadow-md",
92
+ selected: "bg-m3-primary text-m3-on-primary shadow-md",
93
+ unselected: "bg-m3-surface-container-low text-m3-primary shadow-md",
94
+ },
95
+ filled: {
96
+ default: "bg-m3-primary text-m3-on-primary",
97
+ selected: "bg-m3-primary text-m3-on-primary",
98
+ unselected: "bg-m3-surface-container text-m3-on-surface-variant",
99
+ },
100
+ tonal: {
101
+ default: "bg-m3-secondary-container text-m3-on-secondary-container",
102
+ selected: "bg-m3-secondary text-m3-on-secondary",
103
+ unselected: "bg-m3-secondary-container text-m3-on-secondary-container",
104
+ },
105
+ outlined: {
106
+ default:
107
+ "bg-transparent text-m3-on-surface-variant border-m3-outline-variant",
108
+ selected:
109
+ "bg-m3-inverse-surface text-m3-inverse-on-surface border-transparent",
110
+ unselected:
111
+ "bg-transparent text-m3-on-surface-variant border-m3-outline-variant",
112
+ },
113
+ text: {
114
+ default: "bg-transparent text-m3-primary",
115
+ selected: "bg-transparent text-m3-primary",
116
+ unselected: "bg-transparent text-m3-primary",
117
+ },
118
+ } as const;
@@ -4,14 +4,11 @@ import { fireEvent, render, screen } from "@testing-library/react";
4
4
  import { describe, expect, it, vi } from "vitest";
5
5
  import { Button } from "./button";
6
6
 
7
- // ── Inline SVG test icon (no external deps) ───────────────────────────────────
8
7
  const TestIcon = () => (
9
8
  <svg data-testid="test-icon" aria-hidden="true" viewBox="0 0 24 24" />
10
9
  );
11
10
 
12
11
  describe("Button Component", () => {
13
- // ── Existing tests (preserved) ─────────────────────────────────────────────
14
-
15
12
  it("renders correctly with default props", () => {
16
13
  render(<Button>Click me</Button>);
17
14
  const button = screen.getByRole("button");
@@ -56,8 +53,6 @@ describe("Button Component", () => {
56
53
  fireEvent.pointerDown(button, { clientX: 10, clientY: 10 });
57
54
  });
58
55
 
59
- // ── New: Disabled state ────────────────────────────────────────────────────
60
-
61
56
  describe("disabled state", () => {
62
57
  it("has disabled attribute when disabled prop is passed", () => {
63
58
  render(<Button disabled>Disabled</Button>);
@@ -76,8 +71,6 @@ describe("Button Component", () => {
76
71
  });
77
72
  });
78
73
 
79
- // ── New: Keyboard navigation ───────────────────────────────────────────────
80
-
81
74
  describe("keyboard navigation", () => {
82
75
  it("triggers onClick when Enter is pressed", () => {
83
76
  const handleClick = vi.fn();
@@ -105,8 +98,6 @@ describe("Button Component", () => {
105
98
  });
106
99
  });
107
100
 
108
- // ── New: Icon position ─────────────────────────────────────────────────────
109
-
110
101
  describe("icon position", () => {
111
102
  it("renders leading icon before label in DOM order", () => {
112
103
  render(
@@ -153,7 +144,6 @@ describe("Button Component", () => {
153
144
  </Button>,
154
145
  );
155
146
  const button = screen.getByRole("button");
156
- // Icon wrapper span should be aria-hidden
157
147
  const iconWrapper = button.querySelector(
158
148
  "span[aria-hidden='true']:has([data-testid='test-icon'])",
159
149
  );
@@ -178,8 +168,6 @@ describe("Button Component", () => {
178
168
  });
179
169
  });
180
170
 
181
- // ── New: RTL layout ────────────────────────────────────────────────────────
182
-
183
171
  describe("RTL layout", () => {
184
172
  it("renders without crash inside dir=rtl container", () => {
185
173
  const { container } = render(
@@ -215,13 +203,10 @@ describe("Button Component", () => {
215
203
  const labelIndex = spans.findIndex((s) =>
216
204
  s.textContent?.includes("RTL Trailing"),
217
205
  );
218
- // DOM order is still trailing (CSS handles visual flip in RTL via flex)
219
206
  expect(iconIndex).toBeGreaterThan(labelIndex);
220
207
  });
221
208
  });
222
209
 
223
- // ── New: aria-label ────────────────────────────────────────────────────────
224
-
225
210
  describe("aria-label", () => {
226
211
  it("sets aria-label to children string as fallback", () => {
227
212
  render(<Button>Submit</Button>);
@@ -245,14 +230,10 @@ describe("Button Component", () => {
245
230
  <span>Node child</span>
246
231
  </Button>,
247
232
  );
248
- // aria-label should be undefined (not set) for non-string children
249
- // unless explicitly provided
250
233
  expect(screen.getByRole("button")).not.toHaveAttribute("aria-label");
251
234
  });
252
235
  });
253
236
 
254
- // ── New: asChild prop ──────────────────────────────────────────────────────
255
-
256
237
  describe("asChild prop", () => {
257
238
  it("renders children as the root element", () => {
258
239
  render(
@@ -281,8 +262,6 @@ describe("Button Component", () => {
281
262
  });
282
263
  });
283
264
 
284
- // ── New: loading prop ──────────────────────────────────────────────────────
285
-
286
265
  describe("loading prop", () => {
287
266
  it("disables interaction and sets aria-busy", () => {
288
267
  const handleClick = vi.fn();