@bug-on/md3-react 2.0.3 → 3.0.1

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 (316) hide show
  1. package/.turbo/turbo-build.log +42 -0
  2. package/CHANGELOG.md +69 -0
  3. package/dist/index.css +178 -0
  4. package/dist/index.css.d.ts +2 -0
  5. package/dist/index.d.mts +6135 -0
  6. package/dist/index.d.ts +6135 -71
  7. package/dist/index.js +1688 -631
  8. package/dist/index.js.map +1 -1
  9. package/dist/index.mjs +1600 -564
  10. package/dist/index.mjs.map +1 -1
  11. package/dist/material-symbols-cdn.css.d.ts +2 -0
  12. package/dist/material-symbols-self-hosted.css.d.ts +2 -0
  13. package/dist/plugin.d.mts +1 -0
  14. package/dist/plugin.d.ts +1 -0
  15. package/dist/plugin.js +13 -0
  16. package/dist/plugin.js.map +1 -0
  17. package/dist/plugin.mjs +3 -0
  18. package/dist/plugin.mjs.map +1 -0
  19. package/dist/typography.css.d.ts +2 -0
  20. package/package.json +28 -19
  21. package/scripts/copy-assets.js +115 -0
  22. package/src/assets/fonts/GoogleSansFlex-VariableFont.woff2 +0 -0
  23. package/src/assets/fonts/MaterialSymbolsOutlined-VariableFont_FILL,GRAD,opsz,wght.ttf +0 -0
  24. package/src/assets/fonts/MaterialSymbolsRounded-VariableFont_FILL,GRAD,opsz,wght.ttf +0 -0
  25. package/src/assets/fonts/MaterialSymbolsSharp-VariableFont_FILL,GRAD,opsz,wght.ttf +0 -0
  26. package/src/assets/loading-indicator.svg +19 -0
  27. package/src/assets/material-symbols-cdn.css +65 -0
  28. package/src/assets/material-symbols-self-hosted.css +90 -0
  29. package/src/css.d.ts +20 -0
  30. package/src/hooks/useClickOutside.ts +37 -0
  31. package/src/hooks/useMediaQuery.ts +28 -0
  32. package/src/hooks/useRipple.ts +88 -0
  33. package/src/index.css +23 -0
  34. package/src/index.ts +349 -0
  35. package/src/lib/material-symbols-preconnect.tsx +82 -0
  36. package/src/lib/theme-utils.ts +195 -0
  37. package/src/lib/utils.ts +6 -0
  38. package/src/plugin.ts +12 -0
  39. package/src/test/button.test.tsx +59 -0
  40. package/src/test/icon.test.tsx +91 -0
  41. package/src/test/loading-indicator.test.tsx +128 -0
  42. package/src/test/progress-indicator.test.tsx +306 -0
  43. package/src/test/setup.ts +80 -0
  44. package/src/test/typography.test.tsx +206 -0
  45. package/src/types/index.ts +7 -0
  46. package/src/types/md3.ts +31 -0
  47. package/src/ui/Text.tsx +60 -0
  48. package/src/ui/__snapshots__/divider.test.tsx.snap +63 -0
  49. package/src/ui/app-bar/app-bar-column.tsx +99 -0
  50. package/src/ui/app-bar/app-bar-item-button.tsx +71 -0
  51. package/src/ui/app-bar/app-bar-items.test.tsx +89 -0
  52. package/src/ui/app-bar/app-bar-overflow-indicator.tsx +108 -0
  53. package/src/ui/app-bar/app-bar-row.tsx +104 -0
  54. package/src/ui/app-bar/app-bar.test.tsx +87 -0
  55. package/src/ui/app-bar/app-bar.tokens.ts +223 -0
  56. package/src/ui/app-bar/app-bar.types.ts +441 -0
  57. package/src/ui/app-bar/bottom-app-bar.test.tsx +42 -0
  58. package/src/ui/app-bar/bottom-app-bar.tsx +84 -0
  59. package/src/ui/app-bar/docked-toolbar.test.tsx +34 -0
  60. package/src/ui/app-bar/docked-toolbar.tsx +54 -0
  61. package/src/ui/app-bar/flexible-app-bar.test.tsx +75 -0
  62. package/src/ui/app-bar/hooks/use-app-bar-scroll.ts +110 -0
  63. package/src/ui/app-bar/hooks/use-flexible-app-bar.ts +123 -0
  64. package/{dist/ui/app-bar/index.d.ts → src/ui/app-bar/index.ts} +35 -2
  65. package/src/ui/app-bar/large-flexible-app-bar.tsx +165 -0
  66. package/src/ui/app-bar/medium-flexible-app-bar.tsx +167 -0
  67. package/src/ui/app-bar/search-app-bar.test.tsx +49 -0
  68. package/src/ui/app-bar/search-app-bar.tsx +176 -0
  69. package/src/ui/app-bar/search-view.tsx +227 -0
  70. package/src/ui/app-bar/small-app-bar.test.tsx +48 -0
  71. package/src/ui/app-bar/small-app-bar.tsx +203 -0
  72. package/src/ui/badge.test.tsx +345 -0
  73. package/src/ui/badge.tsx +282 -0
  74. package/src/ui/button-group.test.tsx +71 -0
  75. package/src/ui/button-group.tsx +350 -0
  76. package/src/ui/button.test.tsx +306 -0
  77. package/src/ui/button.tsx +665 -0
  78. package/src/ui/card.test.tsx +187 -0
  79. package/src/ui/card.tsx +259 -0
  80. package/src/ui/checkbox.test.tsx +423 -0
  81. package/src/ui/checkbox.tsx +525 -0
  82. package/src/ui/chip.test.tsx +292 -0
  83. package/src/ui/chip.tsx +548 -0
  84. package/src/ui/code-block.tsx +219 -0
  85. package/src/ui/dialog.test.tsx +300 -0
  86. package/src/ui/dialog.tsx +384 -0
  87. package/src/ui/divider.test.tsx +314 -0
  88. package/src/ui/divider.tsx +412 -0
  89. package/src/ui/drawer.tsx +240 -0
  90. package/src/ui/fab-menu.test.tsx +494 -0
  91. package/src/ui/fab-menu.tsx +739 -0
  92. package/src/ui/fab.test.tsx +232 -0
  93. package/src/ui/fab.tsx +505 -0
  94. package/src/ui/icon-button.test.tsx +515 -0
  95. package/src/ui/icon-button.tsx +525 -0
  96. package/src/ui/icon.test.tsx +197 -0
  97. package/src/ui/icon.tsx +179 -0
  98. package/src/ui/loading-indicator.test.tsx +73 -0
  99. package/src/ui/loading-indicator.tsx +312 -0
  100. package/src/ui/menu/context-menu.tsx +275 -0
  101. package/src/ui/menu/index.ts +77 -0
  102. package/src/ui/menu/menu-animations.ts +102 -0
  103. package/src/ui/menu/menu-context.tsx +99 -0
  104. package/src/ui/menu/menu-divider.tsx +47 -0
  105. package/src/ui/menu/menu-group.tsx +200 -0
  106. package/src/ui/menu/menu-item.tsx +294 -0
  107. package/src/ui/menu/menu-tokens.ts +208 -0
  108. package/src/ui/menu/menu-types.ts +313 -0
  109. package/src/ui/menu/menu.test.tsx +624 -0
  110. package/src/ui/menu/menu.tsx +289 -0
  111. package/src/ui/menu/sub-menu.tsx +223 -0
  112. package/src/ui/menu/vertical-menu.tsx +382 -0
  113. package/src/ui/navigation-rail.test.tsx +404 -0
  114. package/src/ui/navigation-rail.tsx +607 -0
  115. package/src/ui/progress-indicator/circular.tsx +248 -0
  116. package/src/ui/progress-indicator/hooks.ts +51 -0
  117. package/{dist/ui/progress-indicator/index.d.ts → src/ui/progress-indicator/index.tsx} +20 -2
  118. package/src/ui/progress-indicator/linear-flat.tsx +83 -0
  119. package/src/ui/progress-indicator/linear-wavy.tsx +243 -0
  120. package/src/ui/progress-indicator/linear.tsx +143 -0
  121. package/src/ui/progress-indicator/types.ts +158 -0
  122. package/src/ui/progress-indicator/utils.ts +73 -0
  123. package/src/ui/radio-button.test.tsx +407 -0
  124. package/src/ui/radio-button.tsx +551 -0
  125. package/src/ui/ripple.test.tsx +72 -0
  126. package/src/ui/ripple.tsx +234 -0
  127. package/src/ui/scroll-area.test.tsx +58 -0
  128. package/src/ui/scroll-area.tsx +139 -0
  129. package/src/ui/search/animated-placeholder.tsx +145 -0
  130. package/src/ui/search/hooks/use-search-keyboard.test.ts +202 -0
  131. package/src/ui/search/hooks/use-search-keyboard.ts +104 -0
  132. package/src/ui/search/hooks/use-search-view-focus.test.ts +96 -0
  133. package/src/ui/search/hooks/use-search-view-focus.ts +24 -0
  134. package/src/ui/search/index.ts +44 -0
  135. package/src/ui/search/search-bar.tsx +220 -0
  136. package/src/ui/search/search-context.tsx +42 -0
  137. package/src/ui/search/search-view-docked.tsx +194 -0
  138. package/src/ui/search/search-view-fullscreen.tsx +247 -0
  139. package/src/ui/search/search.test.tsx +233 -0
  140. package/src/ui/search/search.tokens.ts +134 -0
  141. package/src/ui/search/search.tsx +131 -0
  142. package/src/ui/search/search.types.ts +154 -0
  143. package/src/ui/search/trailing-action.tsx +49 -0
  144. package/src/ui/shared/constants.ts +135 -0
  145. package/{dist/ui/shared/touch-target.d.ts → src/ui/shared/touch-target.tsx} +13 -1
  146. package/src/ui/slider/hooks/useSliderMath.ts +195 -0
  147. package/{dist/ui/slider/index.d.ts → src/ui/slider/index.ts} +12 -1
  148. package/src/ui/slider/range-slider.tsx +561 -0
  149. package/src/ui/slider/slider-thumb.tsx +379 -0
  150. package/src/ui/slider/slider-track.tsx +912 -0
  151. package/src/ui/slider/slider.tokens.ts +189 -0
  152. package/src/ui/slider/slider.tsx +259 -0
  153. package/src/ui/slider/slider.types.ts +288 -0
  154. package/src/ui/snackbar/index.ts +20 -0
  155. package/src/ui/snackbar/snackbar.test.tsx +338 -0
  156. package/src/ui/snackbar/snackbar.tsx +476 -0
  157. package/{dist/ui/switch/index.d.ts → src/ui/switch/index.ts} +1 -0
  158. package/src/ui/switch/switch.stories.tsx +309 -0
  159. package/src/ui/switch/switch.test.tsx +243 -0
  160. package/src/ui/switch/switch.tokens.ts +89 -0
  161. package/src/ui/switch/switch.tsx +504 -0
  162. package/src/ui/switch/switch.types.ts +62 -0
  163. package/{dist/ui/tabs/index.d.ts → src/ui/tabs/index.ts} +8 -1
  164. package/src/ui/tabs/tab.tsx +407 -0
  165. package/src/ui/tabs/tabs-content.tsx +89 -0
  166. package/src/ui/tabs/tabs-list.tsx +146 -0
  167. package/src/ui/tabs/tabs.test.tsx +290 -0
  168. package/src/ui/tabs/tabs.tokens.ts +121 -0
  169. package/src/ui/tabs/tabs.tsx +229 -0
  170. package/src/ui/tabs/tabs.types.ts +185 -0
  171. package/{dist/ui/text-field/index.d.ts → src/ui/text-field/index.ts} +8 -1
  172. package/src/ui/text-field/subcomponents/active-indicator.tsx +67 -0
  173. package/src/ui/text-field/subcomponents/floating-label.tsx +161 -0
  174. package/src/ui/text-field/subcomponents/leading-icon.tsx +46 -0
  175. package/src/ui/text-field/subcomponents/outline-container.tsx +170 -0
  176. package/src/ui/text-field/subcomponents/prefix-suffix.tsx +59 -0
  177. package/src/ui/text-field/subcomponents/supporting-text.tsx +145 -0
  178. package/src/ui/text-field/subcomponents/trailing-icon.tsx +199 -0
  179. package/src/ui/text-field/text-field.test.tsx +454 -0
  180. package/src/ui/text-field/text-field.tokens.ts +104 -0
  181. package/src/ui/text-field/text-field.tsx +548 -0
  182. package/src/ui/text-field/text-field.types.ts +180 -0
  183. package/src/ui/theme-provider/index.tsx +215 -0
  184. package/src/ui/toc.test.tsx +108 -0
  185. package/src/ui/toc.tsx +172 -0
  186. package/src/ui/tooltip/plain-tooltip.tsx +63 -0
  187. package/src/ui/tooltip/rich-tooltip.tsx +94 -0
  188. package/src/ui/tooltip/tooltip-box.tsx +266 -0
  189. package/src/ui/tooltip/tooltip-caret-shape.tsx +68 -0
  190. package/src/ui/tooltip/tooltip.tokens.ts +26 -0
  191. package/src/ui/tooltip/tooltip.types.ts +70 -0
  192. package/src/ui/tooltip/use-tooltip-position.ts +208 -0
  193. package/src/ui/tooltip/use-tooltip-state.ts +41 -0
  194. package/src/ui/typography/__tests__/typography.test.tsx +170 -0
  195. package/{dist/ui/typography/index.d.ts → src/ui/typography/index.ts} +21 -3
  196. package/src/ui/typography/type-scale-tokens.ts +205 -0
  197. package/src/ui/typography/typography-key-tokens.ts +43 -0
  198. package/src/ui/typography/typography-tokens.ts +360 -0
  199. package/src/ui/typography/typography.css +22 -0
  200. package/src/ui/typography/typography.tsx +559 -0
  201. package/test-render.tsx +4 -0
  202. package/test-shadow.html +26 -0
  203. package/test_output.txt +164 -0
  204. package/test_output_v2.txt +5 -0
  205. package/tsconfig.build.json +10 -0
  206. package/tsconfig.json +18 -0
  207. package/tsup.config.ts +20 -0
  208. package/vitest.config.ts +11 -0
  209. package/dist/hooks/useClickOutside.d.ts +0 -8
  210. package/dist/hooks/useMediaQuery.d.ts +0 -11
  211. package/dist/hooks/useRipple.d.ts +0 -26
  212. package/dist/lib/material-symbols-preconnect.d.ts +0 -42
  213. package/dist/lib/theme-utils.d.ts +0 -63
  214. package/dist/lib/utils.d.ts +0 -2
  215. package/dist/types/index.d.ts +0 -1
  216. package/dist/types/md3.d.ts +0 -14
  217. package/dist/ui/app-bar/app-bar-column.d.ts +0 -28
  218. package/dist/ui/app-bar/app-bar-item-button.d.ts +0 -16
  219. package/dist/ui/app-bar/app-bar-overflow-indicator.d.ts +0 -18
  220. package/dist/ui/app-bar/app-bar-row.d.ts +0 -36
  221. package/dist/ui/app-bar/app-bar.tokens.d.ts +0 -184
  222. package/dist/ui/app-bar/app-bar.types.d.ts +0 -392
  223. package/dist/ui/app-bar/bottom-app-bar.d.ts +0 -31
  224. package/dist/ui/app-bar/docked-toolbar.d.ts +0 -25
  225. package/dist/ui/app-bar/hooks/use-app-bar-scroll.d.ts +0 -42
  226. package/dist/ui/app-bar/hooks/use-flexible-app-bar.d.ts +0 -37
  227. package/dist/ui/app-bar/large-flexible-app-bar.d.ts +0 -26
  228. package/dist/ui/app-bar/medium-flexible-app-bar.d.ts +0 -28
  229. package/dist/ui/app-bar/search-app-bar.d.ts +0 -43
  230. package/dist/ui/app-bar/search-view.d.ts +0 -54
  231. package/dist/ui/app-bar/small-app-bar.d.ts +0 -37
  232. package/dist/ui/badge.d.ts +0 -125
  233. package/dist/ui/button-group.d.ts +0 -59
  234. package/dist/ui/button.d.ts +0 -148
  235. package/dist/ui/card.d.ts +0 -62
  236. package/dist/ui/checkbox.d.ts +0 -82
  237. package/dist/ui/chip.d.ts +0 -110
  238. package/dist/ui/code-block.d.ts +0 -14
  239. package/dist/ui/dialog.d.ts +0 -111
  240. package/dist/ui/divider.d.ts +0 -164
  241. package/dist/ui/drawer.d.ts +0 -39
  242. package/dist/ui/dropdown.d.ts +0 -29
  243. package/dist/ui/fab-menu.d.ts +0 -204
  244. package/dist/ui/fab.d.ts +0 -162
  245. package/dist/ui/icon-button.d.ts +0 -131
  246. package/dist/ui/icon.d.ts +0 -88
  247. package/dist/ui/loading-indicator.d.ts +0 -42
  248. package/dist/ui/navigation-rail.d.ts +0 -29
  249. package/dist/ui/progress-indicator/circular.d.ts +0 -3
  250. package/dist/ui/progress-indicator/hooks.d.ts +0 -3
  251. package/dist/ui/progress-indicator/linear-flat.d.ts +0 -10
  252. package/dist/ui/progress-indicator/linear-wavy.d.ts +0 -18
  253. package/dist/ui/progress-indicator/linear.d.ts +0 -3
  254. package/dist/ui/progress-indicator/types.d.ts +0 -151
  255. package/dist/ui/progress-indicator/utils.d.ts +0 -3
  256. package/dist/ui/radio-button.d.ts +0 -106
  257. package/dist/ui/ripple.d.ts +0 -126
  258. package/dist/ui/scroll-area.d.ts +0 -27
  259. package/dist/ui/search/animated-placeholder.d.ts +0 -54
  260. package/dist/ui/search/hooks/use-search-keyboard.d.ts +0 -32
  261. package/dist/ui/search/hooks/use-search-view-focus.d.ts +0 -6
  262. package/dist/ui/search/index.d.ts +0 -27
  263. package/dist/ui/search/search-bar.d.ts +0 -32
  264. package/dist/ui/search/search-context.d.ts +0 -24
  265. package/dist/ui/search/search-view-docked.d.ts +0 -25
  266. package/dist/ui/search/search-view-fullscreen.d.ts +0 -36
  267. package/dist/ui/search/search.d.ts +0 -50
  268. package/dist/ui/search/search.tokens.d.ts +0 -112
  269. package/dist/ui/search/search.types.d.ts +0 -131
  270. package/dist/ui/search/trailing-action.d.ts +0 -9
  271. package/dist/ui/shared/constants.d.ts +0 -86
  272. package/dist/ui/slider/hooks/useSliderMath.d.ts +0 -101
  273. package/dist/ui/slider/range-slider.d.ts +0 -47
  274. package/dist/ui/slider/slider-thumb.d.ts +0 -33
  275. package/dist/ui/slider/slider-track.d.ts +0 -25
  276. package/dist/ui/slider/slider.d.ts +0 -60
  277. package/dist/ui/slider/slider.tokens.d.ts +0 -151
  278. package/dist/ui/slider/slider.types.d.ts +0 -259
  279. package/dist/ui/snackbar/index.d.ts +0 -6
  280. package/dist/ui/snackbar/snackbar.d.ts +0 -197
  281. package/dist/ui/switch/switch.d.ts +0 -30
  282. package/dist/ui/switch/switch.stories.d.ts +0 -48
  283. package/dist/ui/switch/switch.tokens.d.ts +0 -67
  284. package/dist/ui/switch/switch.types.d.ts +0 -59
  285. package/dist/ui/tabs/tab.d.ts +0 -43
  286. package/dist/ui/tabs/tabs-content.d.ts +0 -36
  287. package/dist/ui/tabs/tabs-list.d.ts +0 -40
  288. package/dist/ui/tabs/tabs.d.ts +0 -60
  289. package/dist/ui/tabs/tabs.tokens.d.ts +0 -94
  290. package/dist/ui/tabs/tabs.types.d.ts +0 -172
  291. package/dist/ui/text-field/subcomponents/active-indicator.d.ts +0 -24
  292. package/dist/ui/text-field/subcomponents/floating-label.d.ts +0 -43
  293. package/dist/ui/text-field/subcomponents/leading-icon.d.ts +0 -23
  294. package/dist/ui/text-field/subcomponents/outline-container.d.ts +0 -42
  295. package/dist/ui/text-field/subcomponents/prefix-suffix.d.ts +0 -24
  296. package/dist/ui/text-field/subcomponents/supporting-text.d.ts +0 -37
  297. package/dist/ui/text-field/subcomponents/trailing-icon.d.ts +0 -41
  298. package/dist/ui/text-field/text-field.d.ts +0 -49
  299. package/dist/ui/text-field/text-field.tokens.d.ts +0 -76
  300. package/dist/ui/text-field/text-field.types.d.ts +0 -126
  301. package/dist/ui/theme-provider/index.d.ts +0 -48
  302. package/dist/ui/toc.d.ts +0 -80
  303. package/dist/ui/tooltip/plain-tooltip.d.ts +0 -2
  304. package/dist/ui/tooltip/rich-tooltip.d.ts +0 -2
  305. package/dist/ui/tooltip/tooltip-box.d.ts +0 -2
  306. package/dist/ui/tooltip/tooltip-caret-shape.d.ts +0 -9
  307. package/dist/ui/tooltip/tooltip.tokens.d.ts +0 -26
  308. package/dist/ui/tooltip/tooltip.types.d.ts +0 -56
  309. package/dist/ui/tooltip/use-tooltip-position.d.ts +0 -8
  310. package/dist/ui/tooltip/use-tooltip-state.d.ts +0 -2
  311. package/dist/ui/typography/type-scale-tokens.d.ts +0 -162
  312. package/dist/ui/typography/typography-key-tokens.d.ts +0 -40
  313. package/dist/ui/typography/typography-tokens.d.ts +0 -220
  314. package/dist/ui/typography/typography.d.ts +0 -265
  315. /package/{dist/hooks/index.d.ts → src/hooks/index.ts} +0 -0
  316. /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
+ }