@bug-on/md3-react 2.0.3 → 3.0.0

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 (308) hide show
  1. package/.turbo/turbo-build.log +33 -0
  2. package/CHANGELOG.md +55 -0
  3. package/dist/index.css.d.ts +2 -0
  4. package/dist/index.d.mts +6127 -0
  5. package/dist/index.d.ts +6127 -71
  6. package/dist/index.js +1653 -614
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.mjs +1566 -547
  9. package/dist/index.mjs.map +1 -1
  10. package/dist/material-symbols-cdn.css.d.ts +2 -0
  11. package/dist/material-symbols-self-hosted.css.d.ts +2 -0
  12. package/dist/typography.css.d.ts +2 -0
  13. package/package.json +22 -19
  14. package/scripts/copy-assets.js +82 -0
  15. package/src/assets/fonts/GoogleSansFlex-VariableFont.woff2 +0 -0
  16. package/src/assets/fonts/MaterialSymbolsOutlined-VariableFont_FILL,GRAD,opsz,wght.ttf +0 -0
  17. package/src/assets/fonts/MaterialSymbolsRounded-VariableFont_FILL,GRAD,opsz,wght.ttf +0 -0
  18. package/src/assets/fonts/MaterialSymbolsSharp-VariableFont_FILL,GRAD,opsz,wght.ttf +0 -0
  19. package/src/assets/loading-indicator.svg +19 -0
  20. package/src/assets/material-symbols-cdn.css +65 -0
  21. package/src/assets/material-symbols-self-hosted.css +90 -0
  22. package/src/css.d.ts +20 -0
  23. package/src/hooks/useClickOutside.ts +37 -0
  24. package/src/hooks/useMediaQuery.ts +28 -0
  25. package/src/hooks/useRipple.ts +88 -0
  26. package/src/index.css +23 -0
  27. package/src/index.ts +349 -0
  28. package/src/lib/material-symbols-preconnect.tsx +82 -0
  29. package/src/lib/theme-utils.ts +180 -0
  30. package/src/lib/utils.ts +6 -0
  31. package/src/test/button.test.tsx +59 -0
  32. package/src/test/icon.test.tsx +91 -0
  33. package/src/test/loading-indicator.test.tsx +128 -0
  34. package/src/test/progress-indicator.test.tsx +306 -0
  35. package/src/test/setup.ts +80 -0
  36. package/src/test/typography.test.tsx +206 -0
  37. package/src/types/index.ts +7 -0
  38. package/src/types/md3.ts +31 -0
  39. package/src/ui/Text.tsx +60 -0
  40. package/src/ui/__snapshots__/divider.test.tsx.snap +63 -0
  41. package/src/ui/app-bar/app-bar-column.tsx +99 -0
  42. package/src/ui/app-bar/app-bar-item-button.tsx +71 -0
  43. package/src/ui/app-bar/app-bar-items.test.tsx +89 -0
  44. package/src/ui/app-bar/app-bar-overflow-indicator.tsx +108 -0
  45. package/src/ui/app-bar/app-bar-row.tsx +104 -0
  46. package/src/ui/app-bar/app-bar.test.tsx +87 -0
  47. package/src/ui/app-bar/app-bar.tokens.ts +223 -0
  48. package/src/ui/app-bar/app-bar.types.ts +441 -0
  49. package/src/ui/app-bar/bottom-app-bar.test.tsx +42 -0
  50. package/src/ui/app-bar/bottom-app-bar.tsx +84 -0
  51. package/src/ui/app-bar/docked-toolbar.test.tsx +34 -0
  52. package/src/ui/app-bar/docked-toolbar.tsx +54 -0
  53. package/src/ui/app-bar/flexible-app-bar.test.tsx +75 -0
  54. package/src/ui/app-bar/hooks/use-app-bar-scroll.ts +110 -0
  55. package/src/ui/app-bar/hooks/use-flexible-app-bar.ts +123 -0
  56. package/{dist/ui/app-bar/index.d.ts → src/ui/app-bar/index.ts} +35 -2
  57. package/src/ui/app-bar/large-flexible-app-bar.tsx +165 -0
  58. package/src/ui/app-bar/medium-flexible-app-bar.tsx +167 -0
  59. package/src/ui/app-bar/search-app-bar.test.tsx +49 -0
  60. package/src/ui/app-bar/search-app-bar.tsx +176 -0
  61. package/src/ui/app-bar/search-view.tsx +227 -0
  62. package/src/ui/app-bar/small-app-bar.test.tsx +48 -0
  63. package/src/ui/app-bar/small-app-bar.tsx +203 -0
  64. package/src/ui/badge.test.tsx +345 -0
  65. package/src/ui/badge.tsx +282 -0
  66. package/src/ui/button-group.test.tsx +71 -0
  67. package/src/ui/button-group.tsx +350 -0
  68. package/src/ui/button.test.tsx +297 -0
  69. package/src/ui/button.tsx +669 -0
  70. package/src/ui/card.test.tsx +187 -0
  71. package/src/ui/card.tsx +259 -0
  72. package/src/ui/checkbox.test.tsx +423 -0
  73. package/src/ui/checkbox.tsx +525 -0
  74. package/src/ui/chip.test.tsx +292 -0
  75. package/src/ui/chip.tsx +548 -0
  76. package/src/ui/code-block.tsx +219 -0
  77. package/src/ui/dialog.test.tsx +300 -0
  78. package/src/ui/dialog.tsx +384 -0
  79. package/src/ui/divider.test.tsx +314 -0
  80. package/src/ui/divider.tsx +412 -0
  81. package/src/ui/drawer.tsx +240 -0
  82. package/src/ui/fab-menu.test.tsx +494 -0
  83. package/src/ui/fab-menu.tsx +739 -0
  84. package/src/ui/fab.test.tsx +232 -0
  85. package/src/ui/fab.tsx +505 -0
  86. package/src/ui/icon-button.test.tsx +515 -0
  87. package/src/ui/icon-button.tsx +525 -0
  88. package/src/ui/icon.test.tsx +197 -0
  89. package/src/ui/icon.tsx +179 -0
  90. package/src/ui/loading-indicator.test.tsx +73 -0
  91. package/src/ui/loading-indicator.tsx +312 -0
  92. package/src/ui/menu/context-menu.tsx +275 -0
  93. package/src/ui/menu/index.ts +77 -0
  94. package/src/ui/menu/menu-animations.ts +102 -0
  95. package/src/ui/menu/menu-context.tsx +99 -0
  96. package/src/ui/menu/menu-divider.tsx +47 -0
  97. package/src/ui/menu/menu-group.tsx +200 -0
  98. package/src/ui/menu/menu-item.tsx +294 -0
  99. package/src/ui/menu/menu-tokens.ts +208 -0
  100. package/src/ui/menu/menu-types.ts +313 -0
  101. package/src/ui/menu/menu.test.tsx +624 -0
  102. package/src/ui/menu/menu.tsx +289 -0
  103. package/src/ui/menu/sub-menu.tsx +223 -0
  104. package/src/ui/menu/vertical-menu.tsx +382 -0
  105. package/src/ui/navigation-rail.test.tsx +404 -0
  106. package/src/ui/navigation-rail.tsx +604 -0
  107. package/src/ui/progress-indicator/circular.tsx +248 -0
  108. package/src/ui/progress-indicator/hooks.ts +51 -0
  109. package/{dist/ui/progress-indicator/index.d.ts → src/ui/progress-indicator/index.tsx} +20 -2
  110. package/src/ui/progress-indicator/linear-flat.tsx +83 -0
  111. package/src/ui/progress-indicator/linear-wavy.tsx +243 -0
  112. package/src/ui/progress-indicator/linear.tsx +143 -0
  113. package/src/ui/progress-indicator/types.ts +158 -0
  114. package/src/ui/progress-indicator/utils.ts +73 -0
  115. package/src/ui/radio-button.test.tsx +407 -0
  116. package/src/ui/radio-button.tsx +551 -0
  117. package/src/ui/ripple.test.tsx +72 -0
  118. package/src/ui/ripple.tsx +234 -0
  119. package/src/ui/scroll-area.test.tsx +58 -0
  120. package/src/ui/scroll-area.tsx +139 -0
  121. package/src/ui/search/animated-placeholder.tsx +145 -0
  122. package/src/ui/search/hooks/use-search-keyboard.test.ts +202 -0
  123. package/src/ui/search/hooks/use-search-keyboard.ts +104 -0
  124. package/src/ui/search/hooks/use-search-view-focus.test.ts +96 -0
  125. package/src/ui/search/hooks/use-search-view-focus.ts +24 -0
  126. package/src/ui/search/index.ts +44 -0
  127. package/src/ui/search/search-bar.tsx +220 -0
  128. package/src/ui/search/search-context.tsx +42 -0
  129. package/src/ui/search/search-view-docked.tsx +194 -0
  130. package/src/ui/search/search-view-fullscreen.tsx +247 -0
  131. package/src/ui/search/search.test.tsx +233 -0
  132. package/src/ui/search/search.tokens.ts +134 -0
  133. package/src/ui/search/search.tsx +131 -0
  134. package/src/ui/search/search.types.ts +154 -0
  135. package/src/ui/search/trailing-action.tsx +49 -0
  136. package/src/ui/shared/constants.ts +122 -0
  137. package/{dist/ui/shared/touch-target.d.ts → src/ui/shared/touch-target.tsx} +13 -1
  138. package/src/ui/slider/hooks/useSliderMath.ts +195 -0
  139. package/{dist/ui/slider/index.d.ts → src/ui/slider/index.ts} +12 -1
  140. package/src/ui/slider/range-slider.tsx +561 -0
  141. package/src/ui/slider/slider-thumb.tsx +379 -0
  142. package/src/ui/slider/slider-track.tsx +912 -0
  143. package/src/ui/slider/slider.tokens.ts +189 -0
  144. package/src/ui/slider/slider.tsx +259 -0
  145. package/src/ui/slider/slider.types.ts +288 -0
  146. package/src/ui/snackbar/index.ts +20 -0
  147. package/src/ui/snackbar/snackbar.test.tsx +338 -0
  148. package/src/ui/snackbar/snackbar.tsx +476 -0
  149. package/{dist/ui/switch/index.d.ts → src/ui/switch/index.ts} +1 -0
  150. package/src/ui/switch/switch.stories.tsx +309 -0
  151. package/src/ui/switch/switch.test.tsx +243 -0
  152. package/src/ui/switch/switch.tokens.ts +89 -0
  153. package/src/ui/switch/switch.tsx +504 -0
  154. package/src/ui/switch/switch.types.ts +62 -0
  155. package/{dist/ui/tabs/index.d.ts → src/ui/tabs/index.ts} +8 -1
  156. package/src/ui/tabs/tab.tsx +407 -0
  157. package/src/ui/tabs/tabs-content.tsx +89 -0
  158. package/src/ui/tabs/tabs-list.tsx +146 -0
  159. package/src/ui/tabs/tabs.test.tsx +290 -0
  160. package/src/ui/tabs/tabs.tokens.ts +121 -0
  161. package/src/ui/tabs/tabs.tsx +229 -0
  162. package/src/ui/tabs/tabs.types.ts +185 -0
  163. package/{dist/ui/text-field/index.d.ts → src/ui/text-field/index.ts} +8 -1
  164. package/src/ui/text-field/subcomponents/active-indicator.tsx +67 -0
  165. package/src/ui/text-field/subcomponents/floating-label.tsx +161 -0
  166. package/src/ui/text-field/subcomponents/leading-icon.tsx +46 -0
  167. package/src/ui/text-field/subcomponents/outline-container.tsx +170 -0
  168. package/src/ui/text-field/subcomponents/prefix-suffix.tsx +59 -0
  169. package/src/ui/text-field/subcomponents/supporting-text.tsx +145 -0
  170. package/src/ui/text-field/subcomponents/trailing-icon.tsx +199 -0
  171. package/src/ui/text-field/text-field.test.tsx +454 -0
  172. package/src/ui/text-field/text-field.tokens.ts +104 -0
  173. package/src/ui/text-field/text-field.tsx +548 -0
  174. package/src/ui/text-field/text-field.types.ts +180 -0
  175. package/src/ui/theme-provider/index.tsx +190 -0
  176. package/src/ui/toc.test.tsx +108 -0
  177. package/src/ui/toc.tsx +172 -0
  178. package/src/ui/tooltip/plain-tooltip.tsx +63 -0
  179. package/src/ui/tooltip/rich-tooltip.tsx +94 -0
  180. package/src/ui/tooltip/tooltip-box.tsx +266 -0
  181. package/src/ui/tooltip/tooltip-caret-shape.tsx +68 -0
  182. package/src/ui/tooltip/tooltip.tokens.ts +26 -0
  183. package/src/ui/tooltip/tooltip.types.ts +70 -0
  184. package/src/ui/tooltip/use-tooltip-position.ts +208 -0
  185. package/src/ui/tooltip/use-tooltip-state.ts +41 -0
  186. package/src/ui/typography/__tests__/typography.test.tsx +170 -0
  187. package/{dist/ui/typography/index.d.ts → src/ui/typography/index.ts} +21 -3
  188. package/src/ui/typography/type-scale-tokens.ts +205 -0
  189. package/src/ui/typography/typography-key-tokens.ts +43 -0
  190. package/src/ui/typography/typography-tokens.ts +360 -0
  191. package/src/ui/typography/typography.css +22 -0
  192. package/src/ui/typography/typography.tsx +559 -0
  193. package/test-render.tsx +4 -0
  194. package/test-shadow.html +26 -0
  195. package/test_output.txt +164 -0
  196. package/test_output_v2.txt +5 -0
  197. package/tsconfig.build.json +10 -0
  198. package/tsconfig.json +18 -0
  199. package/tsup.config.ts +20 -0
  200. package/vitest.config.ts +11 -0
  201. package/dist/hooks/useClickOutside.d.ts +0 -8
  202. package/dist/hooks/useMediaQuery.d.ts +0 -11
  203. package/dist/hooks/useRipple.d.ts +0 -26
  204. package/dist/lib/material-symbols-preconnect.d.ts +0 -42
  205. package/dist/lib/theme-utils.d.ts +0 -63
  206. package/dist/lib/utils.d.ts +0 -2
  207. package/dist/types/index.d.ts +0 -1
  208. package/dist/types/md3.d.ts +0 -14
  209. package/dist/ui/app-bar/app-bar-column.d.ts +0 -28
  210. package/dist/ui/app-bar/app-bar-item-button.d.ts +0 -16
  211. package/dist/ui/app-bar/app-bar-overflow-indicator.d.ts +0 -18
  212. package/dist/ui/app-bar/app-bar-row.d.ts +0 -36
  213. package/dist/ui/app-bar/app-bar.tokens.d.ts +0 -184
  214. package/dist/ui/app-bar/app-bar.types.d.ts +0 -392
  215. package/dist/ui/app-bar/bottom-app-bar.d.ts +0 -31
  216. package/dist/ui/app-bar/docked-toolbar.d.ts +0 -25
  217. package/dist/ui/app-bar/hooks/use-app-bar-scroll.d.ts +0 -42
  218. package/dist/ui/app-bar/hooks/use-flexible-app-bar.d.ts +0 -37
  219. package/dist/ui/app-bar/large-flexible-app-bar.d.ts +0 -26
  220. package/dist/ui/app-bar/medium-flexible-app-bar.d.ts +0 -28
  221. package/dist/ui/app-bar/search-app-bar.d.ts +0 -43
  222. package/dist/ui/app-bar/search-view.d.ts +0 -54
  223. package/dist/ui/app-bar/small-app-bar.d.ts +0 -37
  224. package/dist/ui/badge.d.ts +0 -125
  225. package/dist/ui/button-group.d.ts +0 -59
  226. package/dist/ui/button.d.ts +0 -148
  227. package/dist/ui/card.d.ts +0 -62
  228. package/dist/ui/checkbox.d.ts +0 -82
  229. package/dist/ui/chip.d.ts +0 -110
  230. package/dist/ui/code-block.d.ts +0 -14
  231. package/dist/ui/dialog.d.ts +0 -111
  232. package/dist/ui/divider.d.ts +0 -164
  233. package/dist/ui/drawer.d.ts +0 -39
  234. package/dist/ui/dropdown.d.ts +0 -29
  235. package/dist/ui/fab-menu.d.ts +0 -204
  236. package/dist/ui/fab.d.ts +0 -162
  237. package/dist/ui/icon-button.d.ts +0 -131
  238. package/dist/ui/icon.d.ts +0 -88
  239. package/dist/ui/loading-indicator.d.ts +0 -42
  240. package/dist/ui/navigation-rail.d.ts +0 -29
  241. package/dist/ui/progress-indicator/circular.d.ts +0 -3
  242. package/dist/ui/progress-indicator/hooks.d.ts +0 -3
  243. package/dist/ui/progress-indicator/linear-flat.d.ts +0 -10
  244. package/dist/ui/progress-indicator/linear-wavy.d.ts +0 -18
  245. package/dist/ui/progress-indicator/linear.d.ts +0 -3
  246. package/dist/ui/progress-indicator/types.d.ts +0 -151
  247. package/dist/ui/progress-indicator/utils.d.ts +0 -3
  248. package/dist/ui/radio-button.d.ts +0 -106
  249. package/dist/ui/ripple.d.ts +0 -126
  250. package/dist/ui/scroll-area.d.ts +0 -27
  251. package/dist/ui/search/animated-placeholder.d.ts +0 -54
  252. package/dist/ui/search/hooks/use-search-keyboard.d.ts +0 -32
  253. package/dist/ui/search/hooks/use-search-view-focus.d.ts +0 -6
  254. package/dist/ui/search/index.d.ts +0 -27
  255. package/dist/ui/search/search-bar.d.ts +0 -32
  256. package/dist/ui/search/search-context.d.ts +0 -24
  257. package/dist/ui/search/search-view-docked.d.ts +0 -25
  258. package/dist/ui/search/search-view-fullscreen.d.ts +0 -36
  259. package/dist/ui/search/search.d.ts +0 -50
  260. package/dist/ui/search/search.tokens.d.ts +0 -112
  261. package/dist/ui/search/search.types.d.ts +0 -131
  262. package/dist/ui/search/trailing-action.d.ts +0 -9
  263. package/dist/ui/shared/constants.d.ts +0 -86
  264. package/dist/ui/slider/hooks/useSliderMath.d.ts +0 -101
  265. package/dist/ui/slider/range-slider.d.ts +0 -47
  266. package/dist/ui/slider/slider-thumb.d.ts +0 -33
  267. package/dist/ui/slider/slider-track.d.ts +0 -25
  268. package/dist/ui/slider/slider.d.ts +0 -60
  269. package/dist/ui/slider/slider.tokens.d.ts +0 -151
  270. package/dist/ui/slider/slider.types.d.ts +0 -259
  271. package/dist/ui/snackbar/index.d.ts +0 -6
  272. package/dist/ui/snackbar/snackbar.d.ts +0 -197
  273. package/dist/ui/switch/switch.d.ts +0 -30
  274. package/dist/ui/switch/switch.stories.d.ts +0 -48
  275. package/dist/ui/switch/switch.tokens.d.ts +0 -67
  276. package/dist/ui/switch/switch.types.d.ts +0 -59
  277. package/dist/ui/tabs/tab.d.ts +0 -43
  278. package/dist/ui/tabs/tabs-content.d.ts +0 -36
  279. package/dist/ui/tabs/tabs-list.d.ts +0 -40
  280. package/dist/ui/tabs/tabs.d.ts +0 -60
  281. package/dist/ui/tabs/tabs.tokens.d.ts +0 -94
  282. package/dist/ui/tabs/tabs.types.d.ts +0 -172
  283. package/dist/ui/text-field/subcomponents/active-indicator.d.ts +0 -24
  284. package/dist/ui/text-field/subcomponents/floating-label.d.ts +0 -43
  285. package/dist/ui/text-field/subcomponents/leading-icon.d.ts +0 -23
  286. package/dist/ui/text-field/subcomponents/outline-container.d.ts +0 -42
  287. package/dist/ui/text-field/subcomponents/prefix-suffix.d.ts +0 -24
  288. package/dist/ui/text-field/subcomponents/supporting-text.d.ts +0 -37
  289. package/dist/ui/text-field/subcomponents/trailing-icon.d.ts +0 -41
  290. package/dist/ui/text-field/text-field.d.ts +0 -49
  291. package/dist/ui/text-field/text-field.tokens.d.ts +0 -76
  292. package/dist/ui/text-field/text-field.types.d.ts +0 -126
  293. package/dist/ui/theme-provider/index.d.ts +0 -48
  294. package/dist/ui/toc.d.ts +0 -80
  295. package/dist/ui/tooltip/plain-tooltip.d.ts +0 -2
  296. package/dist/ui/tooltip/rich-tooltip.d.ts +0 -2
  297. package/dist/ui/tooltip/tooltip-box.d.ts +0 -2
  298. package/dist/ui/tooltip/tooltip-caret-shape.d.ts +0 -9
  299. package/dist/ui/tooltip/tooltip.tokens.d.ts +0 -26
  300. package/dist/ui/tooltip/tooltip.types.d.ts +0 -56
  301. package/dist/ui/tooltip/use-tooltip-position.d.ts +0 -8
  302. package/dist/ui/tooltip/use-tooltip-state.d.ts +0 -2
  303. package/dist/ui/typography/type-scale-tokens.d.ts +0 -162
  304. package/dist/ui/typography/typography-key-tokens.d.ts +0 -40
  305. package/dist/ui/typography/typography-tokens.d.ts +0 -220
  306. package/dist/ui/typography/typography.d.ts +0 -265
  307. /package/{dist/hooks/index.d.ts → src/hooks/index.ts} +0 -0
  308. /package/{dist/ui/tooltip/index.d.ts → src/ui/tooltip/index.ts} +0 -0
@@ -0,0 +1,42 @@
1
+ import * as React from "react";
2
+
3
+ /**
4
+ * MD3 Expressive Search Context
5
+ * Shared state for the Search orchestrator and its children.
6
+ */
7
+ interface SearchContextValue {
8
+ /** Unique ID for the results listbox, used for aria-controls. */
9
+ listboxId: string;
10
+ /** Currently highlighted suggestion index. -1 = none. */
11
+ activeIndex: number;
12
+ }
13
+
14
+ const SearchContext = React.createContext<SearchContextValue | null>(null);
15
+
16
+ /**
17
+ * Provider for Search component state.
18
+ * Internal use only within the library.
19
+ */
20
+ export function SearchProvider({
21
+ children,
22
+ value,
23
+ }: {
24
+ children: React.ReactNode;
25
+ value: SearchContextValue;
26
+ }) {
27
+ return (
28
+ <SearchContext.Provider value={value}>{children}</SearchContext.Provider>
29
+ );
30
+ }
31
+
32
+ /**
33
+ * Hook to access Search state from children (e.g., search items).
34
+ */
35
+ export function useSearch() {
36
+ const context = React.useContext(SearchContext);
37
+ if (!context) {
38
+ // If used outside Search, return defaults instead of throwing to avoid crashing simple use cases
39
+ return { listboxId: "", activeIndex: -1 };
40
+ }
41
+ return context;
42
+ }
@@ -0,0 +1,194 @@
1
+ /**
2
+ * @file search-view-docked.tsx
3
+ * MD3 Expressive SearchView — Docked variant.
4
+ *
5
+ * Displays as a popup dropdown below the SearchBar.
6
+ * Recommended for medium and large screens (tablets, desktops).
7
+ *
8
+ * Shape: CornerExtraLarge (rounded-[28px]) per SearchViewTokens.DockedContainerShape.
9
+ * Header height: 56dp per SearchViewTokens.DockedHeaderContainerHeight.
10
+ *
11
+ * Animation (Option B — MD3 morphing):
12
+ * - Shares `layoutId` with SearchBar. After SearchBar exits via its own
13
+ * AnimatePresence (mode="popLayout"), this view claims the layoutId and
14
+ * Framer Motion morphs the pill shape → rounded-[28px] container.
15
+ * - Uses mode="popLayout" so SearchBar can re-enter after this exits.
16
+ * - Focus: double-rAF pattern syncs focus with layout animation frame.
17
+ */
18
+
19
+ import { AnimatePresence, m, useReducedMotion } from "motion/react";
20
+ import * as React from "react";
21
+ import { useClickOutside } from "../../hooks/useClickOutside";
22
+ import { cn } from "../../lib/utils";
23
+ import { AnimatedPlaceholder } from "./animated-placeholder";
24
+ import { useSearchViewFocus } from "./hooks/use-search-view-focus";
25
+ import {
26
+ SEARCH_COLORS,
27
+ SEARCH_DOCKED_REVEAL_SPRING,
28
+ SearchTokens,
29
+ } from "./search.tokens";
30
+ import type { SearchInternalProps, SearchProps } from "./search.types";
31
+ import { TrailingAction } from "./trailing-action";
32
+
33
+ type SearchViewDockedProps = Pick<
34
+ SearchProps,
35
+ | "query"
36
+ | "onQueryChange"
37
+ | "onSearch"
38
+ | "active"
39
+ | "onActiveChange"
40
+ | "leadingIcon"
41
+ | "trailingIcon"
42
+ | "placeholder"
43
+ | "textAlign"
44
+ | "styleType"
45
+ | "hasGap"
46
+ | "children"
47
+ | "viewClassName"
48
+ | "aria-label"
49
+ > &
50
+ SearchInternalProps & {
51
+ onKeyDown: (e: React.KeyboardEvent) => void;
52
+ activeIndex: number;
53
+ };
54
+
55
+ export function SearchViewDocked({
56
+ query,
57
+ onQueryChange,
58
+ onSearch,
59
+ active,
60
+ onActiveChange,
61
+ leadingIcon,
62
+ trailingIcon,
63
+ placeholder = "Search",
64
+ textAlign = "left",
65
+ styleType = "contained",
66
+ hasGap = false,
67
+ children,
68
+ viewClassName,
69
+ "aria-label": ariaLabel = "Search",
70
+ searchId,
71
+ listboxId,
72
+ onKeyDown,
73
+ activeIndex,
74
+ }: SearchViewDockedProps) {
75
+ const shouldReduceMotion = useReducedMotion();
76
+ const inputRef = React.useRef<HTMLInputElement>(null);
77
+
78
+ useSearchViewFocus(inputRef, active);
79
+
80
+ const dropdownRef = useClickOutside<HTMLDivElement>(() => {
81
+ onActiveChange(false);
82
+ }, active);
83
+
84
+ const handleFormSubmit = (e: React.FormEvent) => {
85
+ e.preventDefault();
86
+ onSearch(query);
87
+ };
88
+
89
+ const activeDescendant =
90
+ activeIndex >= 0 ? `${listboxId}-option-${activeIndex}` : undefined;
91
+
92
+ return (
93
+ /*
94
+ * mode="popLayout": when SearchView exits, it plays exit animation first,
95
+ * then unmounts — freeing the layoutId for SearchBar to re-enter and morph back.
96
+ */
97
+ <AnimatePresence mode="popLayout">
98
+ {active && (
99
+ <m.div
100
+ ref={dropdownRef}
101
+ key={`${searchId}-view`}
102
+ layoutId={shouldReduceMotion ? undefined : searchId}
103
+ className={cn(
104
+ // DockedContainerShape = CornerExtraLarge = 28dp radius
105
+ "rounded-[28px] overflow-hidden",
106
+ viewClassName,
107
+ )}
108
+ style={{ backgroundColor: SEARCH_COLORS.container }}
109
+ initial={shouldReduceMotion ? {} : { opacity: 0, y: -8 }}
110
+ animate={{ opacity: 1, y: 0 }}
111
+ exit={shouldReduceMotion ? {} : { opacity: 0, y: -8 }}
112
+ transition={
113
+ shouldReduceMotion ? { duration: 0 } : SEARCH_DOCKED_REVEAL_SPRING
114
+ }
115
+ >
116
+ {/* Header row — h-56px per DockedHeaderContainerHeight */}
117
+ <search
118
+ aria-label={ariaLabel}
119
+ className="flex items-center gap-2 px-4"
120
+ style={{ height: SearchTokens.heights.dockedHeader }}
121
+ >
122
+ <form className="contents" onSubmit={handleFormSubmit}>
123
+ <span
124
+ className="flex shrink-0 items-center justify-center"
125
+ style={{ color: SEARCH_COLORS.leadingIcon }}
126
+ aria-hidden="true"
127
+ >
128
+ {leadingIcon}
129
+ </span>
130
+
131
+ <AnimatedPlaceholder
132
+ text={placeholder}
133
+ textAlign={textAlign}
134
+ visible={!query}
135
+ focused={active}
136
+ >
137
+ <input
138
+ ref={inputRef}
139
+ id={`${searchId}-view`}
140
+ type="search"
141
+ role="combobox"
142
+ aria-expanded={true}
143
+ aria-controls={listboxId}
144
+ aria-autocomplete="list"
145
+ aria-activedescendant={activeDescendant}
146
+ aria-label={placeholder}
147
+ value={query}
148
+ placeholder={placeholder}
149
+ className={cn(
150
+ "w-full bg-transparent border-none outline-none",
151
+ "text-[16px] leading-6 font-normal tracking-[0.5px]",
152
+ "placeholder:text-transparent",
153
+ )}
154
+ style={{ color: SEARCH_COLORS.inputText }}
155
+ onChange={(e) => onQueryChange(e.target.value)}
156
+ onKeyDown={onKeyDown}
157
+ />
158
+ </AnimatedPlaceholder>
159
+
160
+ <TrailingAction
161
+ query={query}
162
+ trailingIcon={trailingIcon}
163
+ onClear={() => onQueryChange("")}
164
+ />
165
+ </form>
166
+ </search>
167
+
168
+ {/* hasGap: 2dp gap between header and results. */}
169
+ {hasGap && children && <div className="h-0.5" aria-hidden="true" />}
170
+
171
+ {/* Divider for "divided" styleType */}
172
+ {styleType === "divided" && children && (
173
+ <hr
174
+ className="border-0 border-t"
175
+ style={{ borderColor: SEARCH_COLORS.divider }}
176
+ />
177
+ )}
178
+
179
+ {/* Results / Suggestions listbox */}
180
+ {children && (
181
+ <div
182
+ id={listboxId}
183
+ role="listbox"
184
+ aria-label={`${ariaLabel} results`}
185
+ className="min-h-30"
186
+ >
187
+ {children}
188
+ </div>
189
+ )}
190
+ </m.div>
191
+ )}
192
+ </AnimatePresence>
193
+ );
194
+ }
@@ -0,0 +1,247 @@
1
+ /**
2
+ * @file search-view-fullscreen.tsx
3
+ * MD3 Expressive SearchView — FullScreen variant.
4
+ *
5
+ * Renders a full-screen overlay via React Portal.
6
+ * Using Portal avoids z-index stacking context issues and ensures
7
+ * the overlay always covers the entire viewport correctly.
8
+ *
9
+ * Animation (Option B — MD3 morphing):
10
+ * - Shares `layoutId` with SearchBar. After SearchBar exits via its own
11
+ * AnimatePresence (mode="popLayout"), this view claims the layoutId and
12
+ * Framer Motion morphs the pill shape → full-screen rect (CornerFull → CornerNone).
13
+ * - mode="popLayout" on this AnimatePresence enables SearchBar to re-enter
14
+ * after this view exits.
15
+ * - Focus: double-rAF pattern syncs focus with layout animation frame.
16
+ *
17
+ * Header height: 72dp per SearchViewTokens.FullScreenHeaderContainerHeight.
18
+ * ESC key closes the view (handled by useSearchKeyboard).
19
+ */
20
+
21
+ import { AnimatePresence, m, useReducedMotion } from "motion/react";
22
+ import * as React from "react";
23
+ import { createPortal } from "react-dom";
24
+ import { cn } from "../../lib/utils";
25
+ import { IconButton } from "../icon-button";
26
+ import { AnimatedPlaceholder } from "./animated-placeholder";
27
+ import { useSearchViewFocus } from "./hooks/use-search-view-focus";
28
+ import {
29
+ SEARCH_COLORS,
30
+ SEARCH_FULLSCREEN_SPRING,
31
+ SearchTokens,
32
+ } from "./search.tokens";
33
+ import type { SearchInternalProps, SearchProps } from "./search.types";
34
+ import { TrailingAction } from "./trailing-action";
35
+
36
+ /** Back arrow icon for FullScreen header. */
37
+ function ArrowBackIcon() {
38
+ return (
39
+ <span
40
+ className="material-symbols-rounded select-none leading-none"
41
+ style={{ fontSize: 24 }}
42
+ aria-hidden="true"
43
+ >
44
+ arrow_back
45
+ </span>
46
+ );
47
+ }
48
+
49
+ type SearchViewFullScreenProps = Pick<
50
+ SearchProps,
51
+ | "query"
52
+ | "onQueryChange"
53
+ | "onSearch"
54
+ | "active"
55
+ | "onActiveChange"
56
+ | "leadingIcon"
57
+ | "trailingIcon"
58
+ | "placeholder"
59
+ | "textAlign"
60
+ | "styleType"
61
+ | "children"
62
+ | "viewClassName"
63
+ | "aria-label"
64
+ > &
65
+ SearchInternalProps & {
66
+ onKeyDown: (e: React.KeyboardEvent) => void;
67
+ activeIndex: number;
68
+ };
69
+
70
+ /**
71
+ * SearchView FullScreen — full-screen overlay via React Portal.
72
+ *
73
+ * The `layoutId` shared with SearchBar enables Framer Motion to animate
74
+ * the shape morphing from the pill (rounded-full) to a full-screen rect.
75
+ *
76
+ * Contained style: no divider, background preserved.
77
+ * Divided style: HorizontalDivider between header and results.
78
+ */
79
+ export function SearchViewFullScreen({
80
+ query,
81
+ onQueryChange,
82
+ onSearch,
83
+ active,
84
+ onActiveChange,
85
+ leadingIcon,
86
+ trailingIcon,
87
+ placeholder = "Search",
88
+ textAlign = "left",
89
+ styleType = "contained",
90
+ children,
91
+ viewClassName,
92
+ "aria-label": ariaLabel = "Search",
93
+ searchId,
94
+ listboxId,
95
+ onKeyDown,
96
+ activeIndex,
97
+ }: SearchViewFullScreenProps) {
98
+ const shouldReduceMotion = useReducedMotion();
99
+ const inputRef = React.useRef<HTMLInputElement>(null);
100
+
101
+ useSearchViewFocus(inputRef, active);
102
+
103
+ // Avoid SSR mismatch — only portal on client.
104
+ const [mounted, setMounted] = React.useState(false);
105
+ React.useEffect(() => {
106
+ setMounted(true);
107
+ }, []);
108
+
109
+ const handleFormSubmit = (e: React.FormEvent) => {
110
+ e.preventDefault();
111
+ onSearch(query);
112
+ };
113
+
114
+ const handleEscape = (e: React.KeyboardEvent) => {
115
+ if (e.key === "Escape") {
116
+ e.stopPropagation();
117
+ onActiveChange(false);
118
+ }
119
+ };
120
+
121
+ const activeDescendant =
122
+ activeIndex >= 0 ? `${listboxId}-option-${activeIndex}` : undefined;
123
+
124
+ if (!mounted) return null;
125
+
126
+ const content = (
127
+ /*
128
+ * mode="popLayout": when FullScreen exits, it plays exit animation first,
129
+ * then unmounts — freeing the layoutId for SearchBar to re-enter and morph back
130
+ * to the pill shape.
131
+ */
132
+ <AnimatePresence mode="popLayout">
133
+ {active && (
134
+ <m.div
135
+ key={`${searchId}-fs`}
136
+ layoutId={shouldReduceMotion ? undefined : searchId}
137
+ role="dialog"
138
+ aria-modal="true"
139
+ aria-label={ariaLabel}
140
+ className={cn(
141
+ "fixed inset-0 z-50 flex flex-col overflow-hidden",
142
+ // CornerNone — shape is resolved by Framer Motion layout animation
143
+ "rounded-none",
144
+ viewClassName,
145
+ )}
146
+ style={{ backgroundColor: SEARCH_COLORS.container }}
147
+ initial={shouldReduceMotion ? {} : { opacity: 0 }}
148
+ animate={{ opacity: 1 }}
149
+ exit={shouldReduceMotion ? {} : { opacity: 0 }}
150
+ transition={
151
+ shouldReduceMotion ? { duration: 0 } : SEARCH_FULLSCREEN_SPRING
152
+ }
153
+ onKeyDown={handleEscape}
154
+ >
155
+ {/* Header — h-72px per FullScreenHeaderContainerHeight */}
156
+ <search
157
+ aria-label={ariaLabel}
158
+ className="flex shrink-0 items-center gap-2 px-4"
159
+ style={{ height: SearchTokens.heights.fullScreenHeader }}
160
+ >
161
+ <form className="contents" onSubmit={handleFormSubmit}>
162
+ <IconButton
163
+ size="sm"
164
+ style={{ color: SEARCH_COLORS.leadingIcon }}
165
+ aria-label="Close search"
166
+ onClick={(e) => {
167
+ e.stopPropagation();
168
+ onActiveChange(false);
169
+ }}
170
+ >
171
+ {leadingIcon ?? <ArrowBackIcon />}
172
+ </IconButton>
173
+
174
+ <AnimatedPlaceholder
175
+ text={placeholder}
176
+ textAlign={textAlign}
177
+ visible={!query}
178
+ focused={active}
179
+ >
180
+ <input
181
+ ref={inputRef}
182
+ id={`${searchId}-fs`}
183
+ type="search"
184
+ role="combobox"
185
+ aria-expanded={true}
186
+ aria-controls={listboxId}
187
+ aria-autocomplete="list"
188
+ aria-activedescendant={activeDescendant}
189
+ aria-label={placeholder}
190
+ value={query}
191
+ placeholder={placeholder}
192
+ className={cn(
193
+ "w-full bg-transparent border-none outline-none",
194
+ "text-[16px] leading-6 font-normal tracking-[0.5px]",
195
+ "placeholder:text-transparent",
196
+ )}
197
+ style={{ color: SEARCH_COLORS.inputText }}
198
+ onChange={(e) => onQueryChange(e.target.value)}
199
+ onKeyDown={onKeyDown}
200
+ />
201
+ </AnimatedPlaceholder>
202
+
203
+ <TrailingAction
204
+ query={query}
205
+ trailingIcon={trailingIcon}
206
+ onClear={() => onQueryChange("")}
207
+ />
208
+ </form>
209
+ </search>
210
+
211
+ {/* Divider for "divided" styleType */}
212
+ {styleType === "divided" && (
213
+ <hr
214
+ className="border-0 border-t shrink-0"
215
+ style={{ borderColor: SEARCH_COLORS.divider }}
216
+ />
217
+ )}
218
+
219
+ {/* Results / Suggestions listbox — scrollable area */}
220
+ <div
221
+ id={listboxId}
222
+ role="listbox"
223
+ tabIndex={0}
224
+ aria-label={`${ariaLabel} results`}
225
+ className="flex-1 overflow-y-auto outline-none"
226
+ onClick={(e) => {
227
+ if (e.target === e.currentTarget) onActiveChange(false);
228
+ }}
229
+ onKeyDown={(e) => {
230
+ if (
231
+ e.target === e.currentTarget &&
232
+ (e.key === "Enter" || e.key === " ")
233
+ ) {
234
+ e.preventDefault();
235
+ onActiveChange(false);
236
+ }
237
+ }}
238
+ >
239
+ {children}
240
+ </div>
241
+ </m.div>
242
+ )}
243
+ </AnimatePresence>
244
+ );
245
+
246
+ return createPortal(content, document.body);
247
+ }