@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.
- package/.turbo/turbo-build.log +33 -0
- package/CHANGELOG.md +55 -0
- package/dist/index.css.d.ts +2 -0
- package/dist/index.d.mts +6127 -0
- package/dist/index.d.ts +6127 -71
- package/dist/index.js +1653 -614
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1566 -547
- package/dist/index.mjs.map +1 -1
- package/dist/material-symbols-cdn.css.d.ts +2 -0
- package/dist/material-symbols-self-hosted.css.d.ts +2 -0
- package/dist/typography.css.d.ts +2 -0
- package/package.json +22 -19
- package/scripts/copy-assets.js +82 -0
- package/src/assets/fonts/GoogleSansFlex-VariableFont.woff2 +0 -0
- package/src/assets/fonts/MaterialSymbolsOutlined-VariableFont_FILL,GRAD,opsz,wght.ttf +0 -0
- package/src/assets/fonts/MaterialSymbolsRounded-VariableFont_FILL,GRAD,opsz,wght.ttf +0 -0
- package/src/assets/fonts/MaterialSymbolsSharp-VariableFont_FILL,GRAD,opsz,wght.ttf +0 -0
- package/src/assets/loading-indicator.svg +19 -0
- package/src/assets/material-symbols-cdn.css +65 -0
- package/src/assets/material-symbols-self-hosted.css +90 -0
- package/src/css.d.ts +20 -0
- package/src/hooks/useClickOutside.ts +37 -0
- package/src/hooks/useMediaQuery.ts +28 -0
- package/src/hooks/useRipple.ts +88 -0
- package/src/index.css +23 -0
- package/src/index.ts +349 -0
- package/src/lib/material-symbols-preconnect.tsx +82 -0
- package/src/lib/theme-utils.ts +180 -0
- package/src/lib/utils.ts +6 -0
- package/src/test/button.test.tsx +59 -0
- package/src/test/icon.test.tsx +91 -0
- package/src/test/loading-indicator.test.tsx +128 -0
- package/src/test/progress-indicator.test.tsx +306 -0
- package/src/test/setup.ts +80 -0
- package/src/test/typography.test.tsx +206 -0
- package/src/types/index.ts +7 -0
- package/src/types/md3.ts +31 -0
- package/src/ui/Text.tsx +60 -0
- package/src/ui/__snapshots__/divider.test.tsx.snap +63 -0
- package/src/ui/app-bar/app-bar-column.tsx +99 -0
- package/src/ui/app-bar/app-bar-item-button.tsx +71 -0
- package/src/ui/app-bar/app-bar-items.test.tsx +89 -0
- package/src/ui/app-bar/app-bar-overflow-indicator.tsx +108 -0
- package/src/ui/app-bar/app-bar-row.tsx +104 -0
- package/src/ui/app-bar/app-bar.test.tsx +87 -0
- package/src/ui/app-bar/app-bar.tokens.ts +223 -0
- package/src/ui/app-bar/app-bar.types.ts +441 -0
- package/src/ui/app-bar/bottom-app-bar.test.tsx +42 -0
- package/src/ui/app-bar/bottom-app-bar.tsx +84 -0
- package/src/ui/app-bar/docked-toolbar.test.tsx +34 -0
- package/src/ui/app-bar/docked-toolbar.tsx +54 -0
- package/src/ui/app-bar/flexible-app-bar.test.tsx +75 -0
- package/src/ui/app-bar/hooks/use-app-bar-scroll.ts +110 -0
- package/src/ui/app-bar/hooks/use-flexible-app-bar.ts +123 -0
- package/{dist/ui/app-bar/index.d.ts → src/ui/app-bar/index.ts} +35 -2
- package/src/ui/app-bar/large-flexible-app-bar.tsx +165 -0
- package/src/ui/app-bar/medium-flexible-app-bar.tsx +167 -0
- package/src/ui/app-bar/search-app-bar.test.tsx +49 -0
- package/src/ui/app-bar/search-app-bar.tsx +176 -0
- package/src/ui/app-bar/search-view.tsx +227 -0
- package/src/ui/app-bar/small-app-bar.test.tsx +48 -0
- package/src/ui/app-bar/small-app-bar.tsx +203 -0
- package/src/ui/badge.test.tsx +345 -0
- package/src/ui/badge.tsx +282 -0
- package/src/ui/button-group.test.tsx +71 -0
- package/src/ui/button-group.tsx +350 -0
- package/src/ui/button.test.tsx +297 -0
- package/src/ui/button.tsx +669 -0
- package/src/ui/card.test.tsx +187 -0
- package/src/ui/card.tsx +259 -0
- package/src/ui/checkbox.test.tsx +423 -0
- package/src/ui/checkbox.tsx +525 -0
- package/src/ui/chip.test.tsx +292 -0
- package/src/ui/chip.tsx +548 -0
- package/src/ui/code-block.tsx +219 -0
- package/src/ui/dialog.test.tsx +300 -0
- package/src/ui/dialog.tsx +384 -0
- package/src/ui/divider.test.tsx +314 -0
- package/src/ui/divider.tsx +412 -0
- package/src/ui/drawer.tsx +240 -0
- package/src/ui/fab-menu.test.tsx +494 -0
- package/src/ui/fab-menu.tsx +739 -0
- package/src/ui/fab.test.tsx +232 -0
- package/src/ui/fab.tsx +505 -0
- package/src/ui/icon-button.test.tsx +515 -0
- package/src/ui/icon-button.tsx +525 -0
- package/src/ui/icon.test.tsx +197 -0
- package/src/ui/icon.tsx +179 -0
- package/src/ui/loading-indicator.test.tsx +73 -0
- package/src/ui/loading-indicator.tsx +312 -0
- package/src/ui/menu/context-menu.tsx +275 -0
- package/src/ui/menu/index.ts +77 -0
- package/src/ui/menu/menu-animations.ts +102 -0
- package/src/ui/menu/menu-context.tsx +99 -0
- package/src/ui/menu/menu-divider.tsx +47 -0
- package/src/ui/menu/menu-group.tsx +200 -0
- package/src/ui/menu/menu-item.tsx +294 -0
- package/src/ui/menu/menu-tokens.ts +208 -0
- package/src/ui/menu/menu-types.ts +313 -0
- package/src/ui/menu/menu.test.tsx +624 -0
- package/src/ui/menu/menu.tsx +289 -0
- package/src/ui/menu/sub-menu.tsx +223 -0
- package/src/ui/menu/vertical-menu.tsx +382 -0
- package/src/ui/navigation-rail.test.tsx +404 -0
- package/src/ui/navigation-rail.tsx +604 -0
- package/src/ui/progress-indicator/circular.tsx +248 -0
- package/src/ui/progress-indicator/hooks.ts +51 -0
- package/{dist/ui/progress-indicator/index.d.ts → src/ui/progress-indicator/index.tsx} +20 -2
- package/src/ui/progress-indicator/linear-flat.tsx +83 -0
- package/src/ui/progress-indicator/linear-wavy.tsx +243 -0
- package/src/ui/progress-indicator/linear.tsx +143 -0
- package/src/ui/progress-indicator/types.ts +158 -0
- package/src/ui/progress-indicator/utils.ts +73 -0
- package/src/ui/radio-button.test.tsx +407 -0
- package/src/ui/radio-button.tsx +551 -0
- package/src/ui/ripple.test.tsx +72 -0
- package/src/ui/ripple.tsx +234 -0
- package/src/ui/scroll-area.test.tsx +58 -0
- package/src/ui/scroll-area.tsx +139 -0
- package/src/ui/search/animated-placeholder.tsx +145 -0
- package/src/ui/search/hooks/use-search-keyboard.test.ts +202 -0
- package/src/ui/search/hooks/use-search-keyboard.ts +104 -0
- package/src/ui/search/hooks/use-search-view-focus.test.ts +96 -0
- package/src/ui/search/hooks/use-search-view-focus.ts +24 -0
- package/src/ui/search/index.ts +44 -0
- package/src/ui/search/search-bar.tsx +220 -0
- package/src/ui/search/search-context.tsx +42 -0
- package/src/ui/search/search-view-docked.tsx +194 -0
- package/src/ui/search/search-view-fullscreen.tsx +247 -0
- package/src/ui/search/search.test.tsx +233 -0
- package/src/ui/search/search.tokens.ts +134 -0
- package/src/ui/search/search.tsx +131 -0
- package/src/ui/search/search.types.ts +154 -0
- package/src/ui/search/trailing-action.tsx +49 -0
- package/src/ui/shared/constants.ts +122 -0
- package/{dist/ui/shared/touch-target.d.ts → src/ui/shared/touch-target.tsx} +13 -1
- package/src/ui/slider/hooks/useSliderMath.ts +195 -0
- package/{dist/ui/slider/index.d.ts → src/ui/slider/index.ts} +12 -1
- package/src/ui/slider/range-slider.tsx +561 -0
- package/src/ui/slider/slider-thumb.tsx +379 -0
- package/src/ui/slider/slider-track.tsx +912 -0
- package/src/ui/slider/slider.tokens.ts +189 -0
- package/src/ui/slider/slider.tsx +259 -0
- package/src/ui/slider/slider.types.ts +288 -0
- package/src/ui/snackbar/index.ts +20 -0
- package/src/ui/snackbar/snackbar.test.tsx +338 -0
- package/src/ui/snackbar/snackbar.tsx +476 -0
- package/{dist/ui/switch/index.d.ts → src/ui/switch/index.ts} +1 -0
- package/src/ui/switch/switch.stories.tsx +309 -0
- package/src/ui/switch/switch.test.tsx +243 -0
- package/src/ui/switch/switch.tokens.ts +89 -0
- package/src/ui/switch/switch.tsx +504 -0
- package/src/ui/switch/switch.types.ts +62 -0
- package/{dist/ui/tabs/index.d.ts → src/ui/tabs/index.ts} +8 -1
- package/src/ui/tabs/tab.tsx +407 -0
- package/src/ui/tabs/tabs-content.tsx +89 -0
- package/src/ui/tabs/tabs-list.tsx +146 -0
- package/src/ui/tabs/tabs.test.tsx +290 -0
- package/src/ui/tabs/tabs.tokens.ts +121 -0
- package/src/ui/tabs/tabs.tsx +229 -0
- package/src/ui/tabs/tabs.types.ts +185 -0
- package/{dist/ui/text-field/index.d.ts → src/ui/text-field/index.ts} +8 -1
- package/src/ui/text-field/subcomponents/active-indicator.tsx +67 -0
- package/src/ui/text-field/subcomponents/floating-label.tsx +161 -0
- package/src/ui/text-field/subcomponents/leading-icon.tsx +46 -0
- package/src/ui/text-field/subcomponents/outline-container.tsx +170 -0
- package/src/ui/text-field/subcomponents/prefix-suffix.tsx +59 -0
- package/src/ui/text-field/subcomponents/supporting-text.tsx +145 -0
- package/src/ui/text-field/subcomponents/trailing-icon.tsx +199 -0
- package/src/ui/text-field/text-field.test.tsx +454 -0
- package/src/ui/text-field/text-field.tokens.ts +104 -0
- package/src/ui/text-field/text-field.tsx +548 -0
- package/src/ui/text-field/text-field.types.ts +180 -0
- package/src/ui/theme-provider/index.tsx +190 -0
- package/src/ui/toc.test.tsx +108 -0
- package/src/ui/toc.tsx +172 -0
- package/src/ui/tooltip/plain-tooltip.tsx +63 -0
- package/src/ui/tooltip/rich-tooltip.tsx +94 -0
- package/src/ui/tooltip/tooltip-box.tsx +266 -0
- package/src/ui/tooltip/tooltip-caret-shape.tsx +68 -0
- package/src/ui/tooltip/tooltip.tokens.ts +26 -0
- package/src/ui/tooltip/tooltip.types.ts +70 -0
- package/src/ui/tooltip/use-tooltip-position.ts +208 -0
- package/src/ui/tooltip/use-tooltip-state.ts +41 -0
- package/src/ui/typography/__tests__/typography.test.tsx +170 -0
- package/{dist/ui/typography/index.d.ts → src/ui/typography/index.ts} +21 -3
- package/src/ui/typography/type-scale-tokens.ts +205 -0
- package/src/ui/typography/typography-key-tokens.ts +43 -0
- package/src/ui/typography/typography-tokens.ts +360 -0
- package/src/ui/typography/typography.css +22 -0
- package/src/ui/typography/typography.tsx +559 -0
- package/test-render.tsx +4 -0
- package/test-shadow.html +26 -0
- package/test_output.txt +164 -0
- package/test_output_v2.txt +5 -0
- package/tsconfig.build.json +10 -0
- package/tsconfig.json +18 -0
- package/tsup.config.ts +20 -0
- package/vitest.config.ts +11 -0
- package/dist/hooks/useClickOutside.d.ts +0 -8
- package/dist/hooks/useMediaQuery.d.ts +0 -11
- package/dist/hooks/useRipple.d.ts +0 -26
- package/dist/lib/material-symbols-preconnect.d.ts +0 -42
- package/dist/lib/theme-utils.d.ts +0 -63
- package/dist/lib/utils.d.ts +0 -2
- package/dist/types/index.d.ts +0 -1
- package/dist/types/md3.d.ts +0 -14
- package/dist/ui/app-bar/app-bar-column.d.ts +0 -28
- package/dist/ui/app-bar/app-bar-item-button.d.ts +0 -16
- package/dist/ui/app-bar/app-bar-overflow-indicator.d.ts +0 -18
- package/dist/ui/app-bar/app-bar-row.d.ts +0 -36
- package/dist/ui/app-bar/app-bar.tokens.d.ts +0 -184
- package/dist/ui/app-bar/app-bar.types.d.ts +0 -392
- package/dist/ui/app-bar/bottom-app-bar.d.ts +0 -31
- package/dist/ui/app-bar/docked-toolbar.d.ts +0 -25
- package/dist/ui/app-bar/hooks/use-app-bar-scroll.d.ts +0 -42
- package/dist/ui/app-bar/hooks/use-flexible-app-bar.d.ts +0 -37
- package/dist/ui/app-bar/large-flexible-app-bar.d.ts +0 -26
- package/dist/ui/app-bar/medium-flexible-app-bar.d.ts +0 -28
- package/dist/ui/app-bar/search-app-bar.d.ts +0 -43
- package/dist/ui/app-bar/search-view.d.ts +0 -54
- package/dist/ui/app-bar/small-app-bar.d.ts +0 -37
- package/dist/ui/badge.d.ts +0 -125
- package/dist/ui/button-group.d.ts +0 -59
- package/dist/ui/button.d.ts +0 -148
- package/dist/ui/card.d.ts +0 -62
- package/dist/ui/checkbox.d.ts +0 -82
- package/dist/ui/chip.d.ts +0 -110
- package/dist/ui/code-block.d.ts +0 -14
- package/dist/ui/dialog.d.ts +0 -111
- package/dist/ui/divider.d.ts +0 -164
- package/dist/ui/drawer.d.ts +0 -39
- package/dist/ui/dropdown.d.ts +0 -29
- package/dist/ui/fab-menu.d.ts +0 -204
- package/dist/ui/fab.d.ts +0 -162
- package/dist/ui/icon-button.d.ts +0 -131
- package/dist/ui/icon.d.ts +0 -88
- package/dist/ui/loading-indicator.d.ts +0 -42
- package/dist/ui/navigation-rail.d.ts +0 -29
- package/dist/ui/progress-indicator/circular.d.ts +0 -3
- package/dist/ui/progress-indicator/hooks.d.ts +0 -3
- package/dist/ui/progress-indicator/linear-flat.d.ts +0 -10
- package/dist/ui/progress-indicator/linear-wavy.d.ts +0 -18
- package/dist/ui/progress-indicator/linear.d.ts +0 -3
- package/dist/ui/progress-indicator/types.d.ts +0 -151
- package/dist/ui/progress-indicator/utils.d.ts +0 -3
- package/dist/ui/radio-button.d.ts +0 -106
- package/dist/ui/ripple.d.ts +0 -126
- package/dist/ui/scroll-area.d.ts +0 -27
- package/dist/ui/search/animated-placeholder.d.ts +0 -54
- package/dist/ui/search/hooks/use-search-keyboard.d.ts +0 -32
- package/dist/ui/search/hooks/use-search-view-focus.d.ts +0 -6
- package/dist/ui/search/index.d.ts +0 -27
- package/dist/ui/search/search-bar.d.ts +0 -32
- package/dist/ui/search/search-context.d.ts +0 -24
- package/dist/ui/search/search-view-docked.d.ts +0 -25
- package/dist/ui/search/search-view-fullscreen.d.ts +0 -36
- package/dist/ui/search/search.d.ts +0 -50
- package/dist/ui/search/search.tokens.d.ts +0 -112
- package/dist/ui/search/search.types.d.ts +0 -131
- package/dist/ui/search/trailing-action.d.ts +0 -9
- package/dist/ui/shared/constants.d.ts +0 -86
- package/dist/ui/slider/hooks/useSliderMath.d.ts +0 -101
- package/dist/ui/slider/range-slider.d.ts +0 -47
- package/dist/ui/slider/slider-thumb.d.ts +0 -33
- package/dist/ui/slider/slider-track.d.ts +0 -25
- package/dist/ui/slider/slider.d.ts +0 -60
- package/dist/ui/slider/slider.tokens.d.ts +0 -151
- package/dist/ui/slider/slider.types.d.ts +0 -259
- package/dist/ui/snackbar/index.d.ts +0 -6
- package/dist/ui/snackbar/snackbar.d.ts +0 -197
- package/dist/ui/switch/switch.d.ts +0 -30
- package/dist/ui/switch/switch.stories.d.ts +0 -48
- package/dist/ui/switch/switch.tokens.d.ts +0 -67
- package/dist/ui/switch/switch.types.d.ts +0 -59
- package/dist/ui/tabs/tab.d.ts +0 -43
- package/dist/ui/tabs/tabs-content.d.ts +0 -36
- package/dist/ui/tabs/tabs-list.d.ts +0 -40
- package/dist/ui/tabs/tabs.d.ts +0 -60
- package/dist/ui/tabs/tabs.tokens.d.ts +0 -94
- package/dist/ui/tabs/tabs.types.d.ts +0 -172
- package/dist/ui/text-field/subcomponents/active-indicator.d.ts +0 -24
- package/dist/ui/text-field/subcomponents/floating-label.d.ts +0 -43
- package/dist/ui/text-field/subcomponents/leading-icon.d.ts +0 -23
- package/dist/ui/text-field/subcomponents/outline-container.d.ts +0 -42
- package/dist/ui/text-field/subcomponents/prefix-suffix.d.ts +0 -24
- package/dist/ui/text-field/subcomponents/supporting-text.d.ts +0 -37
- package/dist/ui/text-field/subcomponents/trailing-icon.d.ts +0 -41
- package/dist/ui/text-field/text-field.d.ts +0 -49
- package/dist/ui/text-field/text-field.tokens.d.ts +0 -76
- package/dist/ui/text-field/text-field.types.d.ts +0 -126
- package/dist/ui/theme-provider/index.d.ts +0 -48
- package/dist/ui/toc.d.ts +0 -80
- package/dist/ui/tooltip/plain-tooltip.d.ts +0 -2
- package/dist/ui/tooltip/rich-tooltip.d.ts +0 -2
- package/dist/ui/tooltip/tooltip-box.d.ts +0 -2
- package/dist/ui/tooltip/tooltip-caret-shape.d.ts +0 -9
- package/dist/ui/tooltip/tooltip.tokens.d.ts +0 -26
- package/dist/ui/tooltip/tooltip.types.d.ts +0 -56
- package/dist/ui/tooltip/use-tooltip-position.d.ts +0 -8
- package/dist/ui/tooltip/use-tooltip-state.d.ts +0 -2
- package/dist/ui/typography/type-scale-tokens.d.ts +0 -162
- package/dist/ui/typography/typography-key-tokens.d.ts +0 -40
- package/dist/ui/typography/typography-tokens.d.ts +0 -220
- package/dist/ui/typography/typography.d.ts +0 -265
- /package/{dist/hooks/index.d.ts → src/hooks/index.ts} +0 -0
- /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
|
+
}
|