@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
@@ -9,7 +9,11 @@
9
9
  * @see docs/m3/search/SearchBarTokens.kt
10
10
  * @see docs/m3/search/SearchViewTokens.kt
11
11
  */
12
-
12
+ import {
13
+ DEFAULT_SPATIAL_SPRING,
14
+ FAST_SPATIAL_SPRING,
15
+ SLOW_SPATIAL_SPRING,
16
+ } from "../shared/motion-tokens";
13
17
  // ─── Dimensional Tokens ───────────────────────────────────────────────────────
14
18
 
15
19
  /**
@@ -87,48 +91,10 @@ export const SEARCH_TYPOGRAPHY = {
87
91
  bodyLarge: "text-[16px] leading-6 font-normal tracking-[0.5px]",
88
92
  } as const;
89
93
 
90
- // ─── Animation Constants ──────────────────────────────────────────────────────
94
+ export const SEARCH_BAR_EXPAND_SPRING = FAST_SPATIAL_SPRING;
91
95
 
92
- /**
93
- * Spring animation for SearchBar width expand (inactive → active).
94
- * Matches MD3 FastSpatial motion scheme.
95
- */
96
- export const SEARCH_BAR_EXPAND_SPRING = {
97
- type: "spring" as const,
98
- stiffness: 380,
99
- damping: 38,
100
- mass: 1,
101
- };
102
-
103
- /**
104
- * Spring animation for Docked SearchView dropdown reveal (slide + fade).
105
- * Offset Y: -8px on enter, opacity 0→1.
106
- */
107
- export const SEARCH_DOCKED_REVEAL_SPRING = {
108
- type: "spring" as const,
109
- stiffness: 400,
110
- damping: 35,
111
- mass: 0.8,
112
- };
96
+ export const SEARCH_DOCKED_REVEAL_SPRING = DEFAULT_SPATIAL_SPRING;
113
97
 
114
- /**
115
- * Spring animation for FullScreen SearchView shape morphing.
116
- * Lower stiffness + mass gives a smoother pill→fullscreen morph.
117
- */
118
- export const SEARCH_FULLSCREEN_SPRING = {
119
- type: "spring" as const,
120
- stiffness: 300,
121
- damping: 30,
122
- mass: 0.9,
123
- };
98
+ export const SEARCH_FULLSCREEN_SPRING = SLOW_SPATIAL_SPRING;
124
99
 
125
- /**
126
- * Exit transition for SearchBar when mode="popLayout" is used.
127
- * Fast fade-out so SearchView can claim the layoutId quickly.
128
- */
129
- export const SEARCH_BAR_EXIT_SPRING = {
130
- type: "spring" as const,
131
- stiffness: 500,
132
- damping: 40,
133
- mass: 0.6,
134
- };
100
+ export const SEARCH_BAR_EXIT_SPRING = FAST_SPATIAL_SPRING;
@@ -1,5 +1,5 @@
1
1
  import type * as React from "react";
2
- import { IconButton } from "../icon-button";
2
+ import { IconButton } from "../buttons/icon-button";
3
3
  import { SEARCH_COLORS } from "./search.tokens";
4
4
 
5
5
  interface TrailingActionProps {
@@ -9,57 +9,55 @@
9
9
  */
10
10
 
11
11
  import type { Target, TargetAndTransition, Transition } from "motion/react";
12
+ import {
13
+ DEFAULT_EFFECTS_SPRING,
14
+ DEFAULT_SPATIAL_SPRING,
15
+ FAST_EFFECTS_SPRING,
16
+ } from "./motion-tokens";
17
+
18
+ export {
19
+ DEFAULT_EFFECTS_SPRING,
20
+ DEFAULT_SPATIAL_SPRING,
21
+ FAST_EFFECTS_SPRING,
22
+ FAST_SPATIAL_SPRING,
23
+ getMotionSpring,
24
+ getMotionTransitionCSS,
25
+ SLOW_EFFECTS_SPRING,
26
+ SLOW_SPATIAL_SPRING,
27
+ } from "./motion-tokens";
12
28
 
13
29
  // ─────────────────────────────────────────────────────────────────────────────
14
30
  // Spring Transitions
15
31
  // ─────────────────────────────────────────────────────────────────────────────
16
32
 
17
33
  /**
18
- * Fast critically-damped spring — used for border-radius morphing.
19
- *
20
- * - Duration: 200ms
21
- * - Bounce: 0 (no overshoot → prevents negative radius jitter)
34
+ * Fast spring — used for border-radius morphing, icon swaps.
35
+ * Maps to MD3 `fast.effects` (critically damped, stiffness 3800).
22
36
  *
23
37
  * @example
24
38
  * ```tsx
25
39
  * <m.button transition={{ borderRadius: SPRING_TRANSITION_FAST }}>...</m.button>
26
40
  * ```
27
41
  */
28
- export const SPRING_TRANSITION_FAST: Transition = {
29
- type: "spring",
30
- bounce: 0,
31
- duration: 0.2,
32
- } as const;
42
+ export const SPRING_TRANSITION_FAST: Transition = FAST_EFFECTS_SPRING;
33
43
 
34
44
  /**
35
- * Standard critically-damped spring — used for icon/content scale animations.
36
- *
37
- * - Duration: 300ms
38
- * - Bounce: 0 (no overshoot)
45
+ * Standard spring — used for icon/content scale animations.
46
+ * Maps to MD3 `default.effects` (critically damped, stiffness 1600).
39
47
  *
40
48
  * @example
41
49
  * ```tsx
42
50
  * <m.span transition={SPRING_TRANSITION}>...</m.span>
43
51
  * ```
44
52
  */
45
- export const SPRING_TRANSITION: Transition = {
46
- type: "spring",
47
- bounce: 0,
48
- duration: 0.3,
49
- } as const;
53
+ export const SPRING_TRANSITION: Transition = DEFAULT_EFFECTS_SPRING;
50
54
 
51
55
  /**
52
56
  * MD3 Expressive spring — active indicator expand/collapse.
53
- * Higher bounce for the "pop" effect per MD3 Expressive spec.
54
- *
55
- * - Duration: 400ms
56
- * - Bounce: 0.35 (spring overshoot → lò xo)
57
+ * Maps to MD3 `default.spatial` (dampingRatio: 0.8, stiffness: 380).
58
+ * Gentle bounce for the "pop" effect per MD3 Expressive spec.
57
59
  */
58
- export const SPRING_TRANSITION_EXPRESSIVE: Transition = {
59
- type: "spring",
60
- bounce: 0.35,
61
- duration: 0.4,
62
- } as const;
60
+ export const SPRING_TRANSITION_EXPRESSIVE: Transition = DEFAULT_SPATIAL_SPRING;
63
61
 
64
62
  // ─────────────────────────────────────────────────────────────────────────────
65
63
  // Icon Span Motion Variants
@@ -0,0 +1,238 @@
1
+ /**
2
+ * @file ui/shared/motion-tokens.ts
3
+ *
4
+ * Framer Motion spring presets derived from the official MD3 Expressive
5
+ * motion physics system (May 2025).
6
+ *
7
+ * Usage hierarchy:
8
+ * 1. Import a named preset (e.g. `FAST_SPATIAL_SPRING`) for convenience.
9
+ * 2. Call `getMotionSpring(speed, type)` when you need to pick dynamically.
10
+ * 3. Call `getMotionTransitionCSS(speed, type, scheme?)` for pure-CSS contexts.
11
+ *
12
+ * @see https://m3.material.io/styles/motion/overview/how-it-works
13
+ * @see https://m3.material.io/styles/motion/overview/specs
14
+ */
15
+
16
+ import type { Transition } from "motion/react";
17
+
18
+ // ─────────────────────────────────────────────────────────────────────────────
19
+ // MD3 Spring Token Structure
20
+ //
21
+ // MD3 defines springs as dampingRatio (0–1) + stiffness.
22
+ // Framer Motion uses absolute `damping` coefficient.
23
+ //
24
+ // Conversion (with Framer's default mass = 1):
25
+ // damping = 2 × dampingRatio × √stiffness
26
+ //
27
+ // Computed values (rounded to 2 decimal places):
28
+ //
29
+ // fast spatial: 2 × 0.6 × √800 = 2 × 0.6 × 28.28 ≈ 33.94
30
+ // fast effects: 2 × 1.0 × √3800 = 2 × 61.64 ≈ 123.29
31
+ // default spatial: 2 × 0.8 × √380 = 2 × 0.8 × 19.49 ≈ 31.19
32
+ // default effects: 2 × 1.0 × √1600 = 2 × 40 = 80
33
+ // slow spatial: 2 × 0.8 × √200 = 2 × 0.8 × 14.14 ≈ 22.63
34
+ // slow effects: 2 × 1.0 × √800 = 2 × 28.28 ≈ 56.57
35
+ // ─────────────────────────────────────────────────────────────────────────────
36
+
37
+ // ── Spatial springs — use for position, size, border-radius, rotation, scale ─
38
+
39
+ /**
40
+ * MD3 Expressive — Fast Spatial Spring
41
+ * `md.sys.motion.spring.fast.spatial` (dampingRatio: 0.6, stiffness: 800)
42
+ *
43
+ * Use for: small components (buttons, switches, chips).
44
+ * Has notable bounce/overshoot — feels snappy and expressive.
45
+ */
46
+ export const FAST_SPATIAL_SPRING: Transition = {
47
+ type: "spring",
48
+ stiffness: 800,
49
+ damping: 33.94,
50
+ } as const;
51
+
52
+ /**
53
+ * MD3 Expressive — Default Spatial Spring
54
+ * `md.sys.motion.spring.default.spatial` (dampingRatio: 0.8, stiffness: 380)
55
+ *
56
+ * Use for: medium elements (bottom sheets, menus, navigation rail).
57
+ * Gentle overshoot — the go-to spring for most UI animations.
58
+ */
59
+ export const DEFAULT_SPATIAL_SPRING: Transition = {
60
+ type: "spring",
61
+ stiffness: 380,
62
+ damping: 31.19,
63
+ } as const;
64
+
65
+ /**
66
+ * MD3 Expressive — Slow Spatial Spring
67
+ * `md.sys.motion.spring.slow.spatial` (dampingRatio: 0.8, stiffness: 200)
68
+ *
69
+ * Use for: full-screen transitions and large layout shifts.
70
+ * Subtle, slow overshoot.
71
+ */
72
+ export const SLOW_SPATIAL_SPRING: Transition = {
73
+ type: "spring",
74
+ stiffness: 200,
75
+ damping: 22.63,
76
+ } as const;
77
+
78
+ // ── Effects springs — use for color, opacity (critically damped, no bounce) ──
79
+
80
+ /**
81
+ * MD3 Expressive — Fast Effects Spring
82
+ * `md.sys.motion.spring.fast.effects` (dampingRatio: 1.0, stiffness: 3800)
83
+ *
84
+ * Use for: color changes on switches, icon swaps, badge counts.
85
+ * Critically damped — very fast, no overshoot.
86
+ */
87
+ export const FAST_EFFECTS_SPRING: Transition = {
88
+ type: "spring",
89
+ stiffness: 3800,
90
+ damping: 123.29,
91
+ } as const;
92
+
93
+ /**
94
+ * MD3 Expressive — Default Effects Spring
95
+ * `md.sys.motion.spring.default.effects` (dampingRatio: 1.0, stiffness: 1600)
96
+ *
97
+ * Use for: opacity fades, color transitions for medium-sized elements.
98
+ * Critically damped — smooth with no overshoot.
99
+ */
100
+ export const DEFAULT_EFFECTS_SPRING: Transition = {
101
+ type: "spring",
102
+ stiffness: 1600,
103
+ damping: 80,
104
+ } as const;
105
+
106
+ /**
107
+ * MD3 Expressive — Slow Effects Spring
108
+ * `md.sys.motion.spring.slow.effects` (dampingRatio: 1.0, stiffness: 800)
109
+ *
110
+ * Use for: full-screen opacity/color transitions.
111
+ * Critically damped — slow and smooth.
112
+ */
113
+ export const SLOW_EFFECTS_SPRING: Transition = {
114
+ type: "spring",
115
+ stiffness: 800,
116
+ damping: 56.57,
117
+ } as const;
118
+
119
+ // ─────────────────────────────────────────────────────────────────────────────
120
+ // Dynamic helper
121
+ // ─────────────────────────────────────────────────────────────────────────────
122
+
123
+ export type SpringSpeed = "fast" | "default" | "slow";
124
+ export type SpringAnimationType = "spatial" | "effects";
125
+
126
+ const springMap = {
127
+ fast: { spatial: FAST_SPATIAL_SPRING, effects: FAST_EFFECTS_SPRING },
128
+ default: { spatial: DEFAULT_SPATIAL_SPRING, effects: DEFAULT_EFFECTS_SPRING },
129
+ slow: { spatial: SLOW_SPATIAL_SPRING, effects: SLOW_EFFECTS_SPRING },
130
+ } as const;
131
+
132
+ /**
133
+ * Pick a Framer Motion spring by speed and type.
134
+ *
135
+ * @example
136
+ * ```tsx
137
+ * transition={getMotionSpring("fast", "spatial")}
138
+ * ```
139
+ */
140
+ export function getMotionSpring(
141
+ speed: SpringSpeed,
142
+ type: SpringAnimationType,
143
+ ): Transition {
144
+ return springMap[speed][type];
145
+ }
146
+
147
+ // ─────────────────────────────────────────────────────────────────────────────
148
+ // CSS transition strings
149
+ // For contexts where Framer Motion is not used (plain CSS / Tailwind).
150
+ // Uses official Web spring → CSS cubic-bezier conversion table.
151
+ // ─────────────────────────────────────────────────────────────────────────────
152
+
153
+ const CSS_CURVES = {
154
+ expressive: {
155
+ fast: {
156
+ spatial: {
157
+ curve: "cubic-bezier(0.42, 1.67, 0.21, 0.90)",
158
+ durationMs: 350,
159
+ },
160
+ effects: {
161
+ curve: "cubic-bezier(0.31, 0.94, 0.34, 1.00)",
162
+ durationMs: 150,
163
+ },
164
+ },
165
+ default: {
166
+ spatial: {
167
+ curve: "cubic-bezier(0.38, 1.21, 0.22, 1.00)",
168
+ durationMs: 500,
169
+ },
170
+ effects: {
171
+ curve: "cubic-bezier(0.34, 0.80, 0.34, 1.00)",
172
+ durationMs: 200,
173
+ },
174
+ },
175
+ slow: {
176
+ spatial: {
177
+ curve: "cubic-bezier(0.39, 1.29, 0.35, 0.98)",
178
+ durationMs: 650,
179
+ },
180
+ effects: {
181
+ curve: "cubic-bezier(0.34, 0.88, 0.34, 1.00)",
182
+ durationMs: 300,
183
+ },
184
+ },
185
+ },
186
+ standard: {
187
+ fast: {
188
+ spatial: {
189
+ curve: "cubic-bezier(0.27, 1.06, 0.18, 1.00)",
190
+ durationMs: 350,
191
+ },
192
+ effects: {
193
+ curve: "cubic-bezier(0.31, 0.94, 0.34, 1.00)",
194
+ durationMs: 150,
195
+ },
196
+ },
197
+ default: {
198
+ spatial: {
199
+ curve: "cubic-bezier(0.27, 1.06, 0.18, 1.00)",
200
+ durationMs: 500,
201
+ },
202
+ effects: {
203
+ curve: "cubic-bezier(0.34, 0.80, 0.34, 1.00)",
204
+ durationMs: 200,
205
+ },
206
+ },
207
+ slow: {
208
+ spatial: {
209
+ curve: "cubic-bezier(0.27, 1.06, 0.18, 1.00)",
210
+ durationMs: 750,
211
+ },
212
+ effects: {
213
+ curve: "cubic-bezier(0.34, 0.88, 0.34, 1.00)",
214
+ durationMs: 300,
215
+ },
216
+ },
217
+ },
218
+ } as const;
219
+
220
+ export type MotionScheme = "expressive" | "standard";
221
+
222
+ /**
223
+ * Returns a CSS `transition` value string using the official MD3 web curve
224
+ * conversion, e.g. `"500ms cubic-bezier(0.38, 1.21, 0.22, 1.00)"`.
225
+ *
226
+ * @example
227
+ * ```ts
228
+ * element.style.transition = `border-radius ${getMotionTransitionCSS("default", "spatial")}`;
229
+ * ```
230
+ */
231
+ export function getMotionTransitionCSS(
232
+ speed: SpringSpeed,
233
+ type: SpringAnimationType,
234
+ scheme: MotionScheme = "expressive",
235
+ ): string {
236
+ const { curve, durationMs } = CSS_CURVES[scheme][speed][type];
237
+ return `${durationMs}ms ${curve}`;
238
+ }
@@ -27,8 +27,9 @@ import {
27
27
  } from "motion/react";
28
28
  import * as React from "react";
29
29
  import { cn } from "../../lib/utils";
30
+ import { IconButton } from "../buttons/icon-button";
30
31
  import { Icon } from "../icon";
31
- import { IconButton } from "../icon-button";
32
+ import { DEFAULT_SPATIAL_SPRING } from "../shared/motion-tokens";
32
33
 
33
34
  // ─── Constants ────────────────────────────────────────────────────────────────
34
35
 
@@ -44,11 +45,8 @@ const RESULT = {
44
45
 
45
46
  // ─── Animation Config (Framer Motion — NOT CSS transitions) ──────────────────
46
47
 
47
- const SNACKBAR_SPRING = {
48
- type: "spring" as const,
49
- bounce: 0.15,
50
- duration: 0.4,
51
- };
48
+ // MD3 default.spatial: stiffness=380, dampingRatio=0.8 → Framer damping ≈ 31.19
49
+ const SNACKBAR_SPRING = DEFAULT_SPATIAL_SPRING;
52
50
 
53
51
  const SNACKBAR_ANIM = {
54
52
  initial: { opacity: 0, y: 56, scale: 0.9 },
@@ -20,32 +20,26 @@ import {
20
20
  import * as React from "react";
21
21
  import { cn } from "../../lib/utils";
22
22
  import { Ripple, type RippleOrigin } from "../ripple";
23
+ import {
24
+ FAST_EFFECTS_SPRING,
25
+ FAST_SPATIAL_SPRING,
26
+ } from "../shared/motion-tokens";
23
27
  import { SwitchColors, SwitchTokens } from "./switch.tokens";
24
28
  import type { SwitchProps } from "./switch.types";
25
29
 
26
30
  // ─── Animation constants ───────────────────────────────────────────────────────
27
31
 
28
- /**
29
- * FastSpatial spring equivalent to MotionSchemeKeyTokens.FastSpatial.
30
- * Used for thumb translation and size morph on checked state change.
31
- */
32
- const FAST_SPATIAL_SPRING = {
33
- type: "spring",
34
- stiffness: 500,
35
- damping: 40,
36
- } as const;
32
+ // FAST_SPATIAL_SPRING and FAST_EFFECTS_SPRING imported from motion-tokens:
33
+ // fast.spatial: stiffness=800, dampingRatio=0.6 Framer damping≈33.94 (thumb movement/size)
34
+ // fast.effects: stiffness=3800, dampingRatio=1.0 Framer damping≈123.29 (state layer)
37
35
 
38
36
  /** Instant transition (SnapSpec equivalent) — used when thumb is pressed. */
39
37
  const SNAP_TRANSITION = { duration: 0 } as const;
40
38
 
41
- /** Color transition for track/thumb color changes. */
42
- const COLOR_TRANSITION = { duration: 0.2, ease: "easeInOut" } as const;
43
-
44
- /** State layer spring for hover/focus overlay. */
45
- const STATE_LAYER_SPRING = {
46
- type: "spring",
47
- stiffness: 400,
48
- damping: 30,
39
+ /** Color transition for track/thumb color changes. Uses fast.effects CSS curve. */
40
+ const COLOR_TRANSITION = {
41
+ duration: 0.15,
42
+ ease: [0.31, 0.94, 0.34, 1.0] as [number, number, number, number],
49
43
  } as const;
50
44
 
51
45
  // ─── Helpers ──────────────────────────────────────────────────────────────────
@@ -223,7 +217,7 @@ const SwitchVisual = React.memo(function SwitchVisual({
223
217
 
224
218
  const stateLayerTransition = prefersReduced
225
219
  ? { duration: 0 }
226
- : STATE_LAYER_SPRING;
220
+ : FAST_EFFECTS_SPRING;
227
221
 
228
222
  return (
229
223
  <div
@@ -12,17 +12,17 @@
12
12
 
13
13
  export const TF_COLORS = {
14
14
  /** Filled container background */
15
- filledBg: "var(--color-m3-surface-container-highest)",
15
+ filledBg: "var(--md-sys-color-surface-container-highest)",
16
16
  /** Input text color */
17
- inputText: "var(--color-m3-on-surface)",
17
+ inputText: "var(--md-sys-color-on-surface)",
18
18
  /** Label (unfloated) + icons + prefix/suffix + supporting text */
19
- onSurfaceVariant: "var(--color-m3-on-surface-variant)",
19
+ onSurfaceVariant: "var(--md-sys-color-on-surface-variant)",
20
20
  /** Focused active indicator / outline, floated label */
21
- primary: "var(--color-m3-primary)",
21
+ primary: "var(--md-sys-color-primary)",
22
22
  /** Error indicator / outline / label / icon / supporting text */
23
- error: "var(--color-m3-error)",
23
+ error: "var(--md-sys-color-error)",
24
24
  /** Outlined border (enabled) */
25
- outline: "var(--color-m3-outline)",
25
+ outline: "var(--md-sys-color-outline)",
26
26
  /** Transparent */
27
27
  transparent: "transparent",
28
28
  } as const;
@@ -78,12 +78,12 @@ export const TF_TYPOGRAPHY = {
78
78
  export const TF_CLASSES = {
79
79
  // Container
80
80
  filledContainer:
81
- "bg-[var(--color-m3-surface-container-highest)] rounded-tl-[4px] rounded-tr-[4px] rounded-bl-none rounded-br-none",
81
+ "bg-[var(--md-sys-color-surface-container-highest)] rounded-tl-[4px] rounded-tr-[4px] rounded-bl-none rounded-br-none",
82
82
  outlinedContainer: "bg-transparent rounded-[4px]",
83
83
 
84
84
  // Input
85
85
  input:
86
- "bg-transparent outline-none w-full text-base text-[var(--color-m3-on-surface)] caret-[var(--color-m3-primary)] placeholder:text-[var(--color-m3-on-surface-variant)]",
86
+ "bg-transparent outline-none w-full text-base text-[var(--md-sys-color-on-surface)] caret-[var(--md-sys-color-primary)] placeholder:text-[var(--md-sys-color-on-surface-variant)]",
87
87
  inputNoSpinner:
88
88
  "[appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none",
89
89
 
@@ -91,14 +91,14 @@ export const TF_CLASSES = {
91
91
  disabled: "opacity-[0.38] pointer-events-none cursor-not-allowed",
92
92
 
93
93
  // Supporting text
94
- supportingText: "text-xs text-[var(--color-m3-on-surface-variant)] px-4",
95
- errorText: "text-xs text-[var(--color-m3-error)] px-4",
94
+ supportingText: "text-xs text-[var(--md-sys-color-on-surface-variant)] px-4",
95
+ errorText: "text-xs text-[var(--md-sys-color-error)] px-4",
96
96
 
97
97
  // Prefix / Suffix
98
98
  prefixSuffix:
99
- "text-base text-[var(--color-m3-on-surface-variant)] select-none shrink-0",
99
+ "text-base text-[var(--md-sys-color-on-surface-variant)] select-none shrink-0",
100
100
 
101
101
  // State layer (hover)
102
102
  stateLayer:
103
- "absolute inset-0 bg-[var(--color-m3-on-surface)] opacity-0 transition-opacity duration-150 pointer-events-none rounded-[inherit]",
103
+ "absolute inset-0 bg-[var(--md-sys-color-on-surface)] opacity-0 transition-opacity duration-150 pointer-events-none rounded-[inherit]",
104
104
  } as const;
@@ -18,12 +18,11 @@
18
18
  * @see https://m3.material.io/components/text-fields/overview
19
19
  */
20
20
 
21
- "use client";
22
-
23
21
  import { domMax, LazyMotion, useReducedMotion } from "motion/react";
24
22
  import * as React from "react";
25
23
  import { cn } from "../../lib/utils";
26
24
  import { ScrollArea } from "../scroll-area";
25
+ import { useToolbarContext } from "../toolbar/toolbar-context";
27
26
  import { ActiveIndicator } from "./subcomponents/active-indicator";
28
27
  import { FloatingLabel } from "./subcomponents/floating-label";
29
28
  import { LeadingIcon } from "./subcomponents/leading-icon";
@@ -107,6 +106,12 @@ const TextFieldComponent = React.forwardRef<TextFieldHandle, TextFieldProps>(
107
106
  ref,
108
107
  ) => {
109
108
  const prefersReduced = useReducedMotion() ?? false;
109
+ const toolbarContext = useToolbarContext();
110
+ const isInToolbar = !!toolbarContext;
111
+
112
+ // ── Props Override (Toolbar) ──────────────────────────────────────────
113
+ const resolvedDense = isInToolbar ? true : dense;
114
+ const resolvedLabel = isInToolbar ? undefined : label;
110
115
 
111
116
  // ── IDs ───────────────────────────────────────────────────────────────
112
117
  const generatedId = React.useId();
@@ -141,7 +146,9 @@ const TextFieldComponent = React.forwardRef<TextFieldHandle, TextFieldProps>(
141
146
  errorProp ||
142
147
  !!nativeError ||
143
148
  (maxLength !== undefined && currentValue.length > maxLength);
144
- const containerHeight = dense ? TF_SIZE.denseHeight : TF_SIZE.height;
149
+ const containerHeight = resolvedDense
150
+ ? TF_SIZE.denseHeight
151
+ : TF_SIZE.height;
145
152
  const showAsterisk = required && !noAsterisk;
146
153
 
147
154
  // ── Refs ──────────────────────────────────────────────────────────────
@@ -270,7 +277,8 @@ const TextFieldComponent = React.forwardRef<TextFieldHandle, TextFieldProps>(
270
277
 
271
278
  // ── Input classes ─────────────────────────────────────────────────────
272
279
  const inputClass = cn(
273
- "bg-transparent outline-none w-full self-end",
280
+ "bg-transparent outline-none w-full",
281
+ isInToolbar ? "self-center" : "self-end",
274
282
  "text-base leading-6 text-m3-on-surface",
275
283
  "caret-[var(--color-m3-primary)]",
276
284
  "placeholder:text-m3-on-surface-variant/60",
@@ -315,22 +323,26 @@ const TextFieldComponent = React.forwardRef<TextFieldHandle, TextFieldProps>(
315
323
  const containerClass = cn(
316
324
  "relative flex items-center",
317
325
  variant === "filled"
318
- ? "bg-[var(--color-m3-surface-container-highest)] rounded-tl-sm rounded-tr-sm"
326
+ ? isInToolbar
327
+ ? "bg-transparent"
328
+ : "bg-[var(--color-m3-surface-container-highest)] rounded-tl-sm rounded-tr-sm"
319
329
  : "bg-transparent rounded-sm",
320
330
  fullWidth ? "w-full" : "w-fit",
321
331
  );
322
332
 
323
333
  const innerClass = cn(
324
- "relative flex flex-col flex-1 min-w-0",
334
+ "relative flex flex-col flex-1 min-w-0 h-full",
325
335
  paddingStart,
326
336
  paddingEnd,
327
- variant === "filled"
328
- ? dense
329
- ? "pt-5 pb-2"
330
- : "pt-6 pb-2"
331
- : dense
332
- ? "py-3"
333
- : "py-4",
337
+ isInToolbar
338
+ ? "justify-center"
339
+ : variant === "filled"
340
+ ? resolvedDense
341
+ ? "pt-5 pb-2"
342
+ : "pt-6 pb-2"
343
+ : resolvedDense
344
+ ? "py-3"
345
+ : "py-4",
334
346
  );
335
347
 
336
348
  const wrapperClass = cn(
@@ -340,7 +352,7 @@ const TextFieldComponent = React.forwardRef<TextFieldHandle, TextFieldProps>(
340
352
  className,
341
353
  );
342
354
 
343
- const containerHeightClass = dense ? "h-12" : "h-14";
355
+ const containerHeightClass = resolvedDense ? "h-12" : "h-14";
344
356
 
345
357
  // ── Render ────────────────────────────────────────────────────────────
346
358
  return (
@@ -354,7 +366,7 @@ const TextFieldComponent = React.forwardRef<TextFieldHandle, TextFieldProps>(
354
366
  )}
355
367
  >
356
368
  {/* Outlined border with animated notch */}
357
- {variant === "outlined" && (
369
+ {variant === "outlined" && !isInToolbar && (
358
370
  <OutlineContainer
359
371
  isFloated={isFloated}
360
372
  isFocused={isFocused}
@@ -367,7 +379,7 @@ const TextFieldComponent = React.forwardRef<TextFieldHandle, TextFieldProps>(
367
379
  )}
368
380
 
369
381
  {/* Hover state layer (filled only) */}
370
- {variant === "filled" && (
382
+ {variant === "filled" && !isInToolbar && (
371
383
  <div
372
384
  aria-hidden="true"
373
385
  className={cn(
@@ -388,9 +400,9 @@ const TextFieldComponent = React.forwardRef<TextFieldHandle, TextFieldProps>(
388
400
  {/* Inner column: floating label + input row */}
389
401
  <div className={innerClass}>
390
402
  {/* Floating label — absolutely positioned over the inner region */}
391
- {label && (
403
+ {resolvedLabel && (
392
404
  <FloatingLabel
393
- text={label}
405
+ text={resolvedLabel}
394
406
  isFloated={isFloated}
395
407
  isFocused={isFocused}
396
408
  isError={isError}
@@ -485,7 +497,7 @@ const TextFieldComponent = React.forwardRef<TextFieldHandle, TextFieldProps>(
485
497
  )}
486
498
 
487
499
  {/* Active indicator: bottom border line (filled variant only) */}
488
- {variant === "filled" && (
500
+ {variant === "filled" && !isInToolbar && (
489
501
  <ActiveIndicator
490
502
  isFocused={isFocused}
491
503
  isError={isError}
@@ -106,11 +106,7 @@ export function MD3ThemeProvider({
106
106
  ) as ThemeMode | null;
107
107
 
108
108
  if (savedColor) setSourceColor(savedColor);
109
- if (
110
- savedMode === "light" ||
111
- savedMode === "dark" ||
112
- savedMode === "system"
113
- )
109
+ if (savedMode === "light" || savedMode === "dark" || savedMode === "system")
114
110
  setMode(savedMode);
115
111
 
116
112
  setIsHydrated(true);