@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.
- package/.turbo/turbo-build.log +42 -0
- package/CHANGELOG.md +69 -0
- package/dist/index.css +178 -0
- package/dist/index.css.d.ts +2 -0
- package/dist/index.d.mts +6135 -0
- package/dist/index.d.ts +6135 -71
- package/dist/index.js +1688 -631
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1600 -564
- 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/plugin.d.mts +1 -0
- package/dist/plugin.d.ts +1 -0
- package/dist/plugin.js +13 -0
- package/dist/plugin.js.map +1 -0
- package/dist/plugin.mjs +3 -0
- package/dist/plugin.mjs.map +1 -0
- package/dist/typography.css.d.ts +2 -0
- package/package.json +28 -19
- package/scripts/copy-assets.js +115 -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 +195 -0
- package/src/lib/utils.ts +6 -0
- package/src/plugin.ts +12 -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 +306 -0
- package/src/ui/button.tsx +665 -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 +607 -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 +135 -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 +215 -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,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file tabs.types.ts
|
|
3
|
+
* MD3 Expressive Tabs — TypeScript prop definitions.
|
|
4
|
+
* Spec: https://m3.material.io/components/tabs/overview
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type * as React from "react";
|
|
8
|
+
|
|
9
|
+
// ─── Variant & Layout ─────────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
/** Visual variant: primary (content-width indicator) or secondary (full-width indicator + divider). */
|
|
12
|
+
export type TabsVariant = "primary" | "secondary";
|
|
13
|
+
|
|
14
|
+
// ─── Internal Context ──────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Internal context shared across all compound components.
|
|
18
|
+
* @internal
|
|
19
|
+
*/
|
|
20
|
+
export interface TabsContextValue {
|
|
21
|
+
/** Currently selected tab value. */
|
|
22
|
+
value: string;
|
|
23
|
+
/** Callback to change the selected tab. */
|
|
24
|
+
onValueChange: (value: string) => void;
|
|
25
|
+
/** Currently keyboard-focused tab value (for roving tabindex). */
|
|
26
|
+
focusedValue: string;
|
|
27
|
+
/** Sets the focused tab value (keyboard nav only — does NOT select). */
|
|
28
|
+
setFocusedValue: (value: string) => void;
|
|
29
|
+
/** Ordered list of all registered tab values (for ArrowKey nav). */
|
|
30
|
+
tabValues: string[];
|
|
31
|
+
/** Register a tab value when a <Tab> mounts. */
|
|
32
|
+
registerTab: (value: string) => void;
|
|
33
|
+
/** Unregister a tab value when a <Tab> unmounts. */
|
|
34
|
+
unregisterTab: (value: string) => void;
|
|
35
|
+
/** Unique layout group ID scoped to this Tabs instance. */
|
|
36
|
+
layoutGroupId: string;
|
|
37
|
+
/**
|
|
38
|
+
* Set of currently disabled tab values.
|
|
39
|
+
* Used by keyboard navigation to skip disabled tabs.
|
|
40
|
+
*/
|
|
41
|
+
disabledValues: Set<string>;
|
|
42
|
+
/**
|
|
43
|
+
* Mark or unmark a tab value as disabled.
|
|
44
|
+
* Called by <Tab> on mount and when `disabled` prop changes.
|
|
45
|
+
*/
|
|
46
|
+
markTabDisabled: (value: string, disabled: boolean) => void;
|
|
47
|
+
/**
|
|
48
|
+
* When true, focus moving via ArrowKey also selects the tab immediately.
|
|
49
|
+
* Mirrors Google's `autoActivate` attribute on <md-tabs>.
|
|
50
|
+
* @default false
|
|
51
|
+
*/
|
|
52
|
+
autoActivate: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ─── Component Props ───────────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Props for the `<Tabs>` root component.
|
|
59
|
+
*
|
|
60
|
+
* Supports both controlled (`value` + `onValueChange`) and
|
|
61
|
+
* uncontrolled (`defaultValue`) usage patterns.
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```tsx
|
|
65
|
+
* // Controlled
|
|
66
|
+
* <Tabs value={tab} onValueChange={setTab}>...</Tabs>
|
|
67
|
+
*
|
|
68
|
+
* // Uncontrolled
|
|
69
|
+
* <Tabs defaultValue="flights">...</Tabs>
|
|
70
|
+
*
|
|
71
|
+
* // Auto-activate (focus = select)
|
|
72
|
+
* <Tabs defaultValue="flights" autoActivate>...</Tabs>
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
export interface TabsProps {
|
|
76
|
+
/** Controlled selected value. Use with `onValueChange`. */
|
|
77
|
+
value?: string;
|
|
78
|
+
/** Initial value for uncontrolled usage. */
|
|
79
|
+
defaultValue?: string;
|
|
80
|
+
/** Called when the selected tab changes. */
|
|
81
|
+
onValueChange?: (value: string) => void;
|
|
82
|
+
/**
|
|
83
|
+
* When true, ArrowKey navigation also selects the focused tab immediately.
|
|
84
|
+
* Mirrors Google's `auto-activate` attribute on `<md-tabs>`.
|
|
85
|
+
* @default false
|
|
86
|
+
*/
|
|
87
|
+
autoActivate?: boolean;
|
|
88
|
+
/** Tab compound components as children. */
|
|
89
|
+
children: React.ReactNode;
|
|
90
|
+
/** Additional CSS class names for the root wrapper. */
|
|
91
|
+
className?: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Props for the `<TabsList>` container component.
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* ```tsx
|
|
99
|
+
* <TabsList variant="primary" scrollable={false}>
|
|
100
|
+
* <Tab value="tab1">Tab 1</Tab>
|
|
101
|
+
* </TabsList>
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
export interface TabsListProps {
|
|
105
|
+
/** Visual style variant. @required */
|
|
106
|
+
variant: TabsVariant;
|
|
107
|
+
/**
|
|
108
|
+
* When true, tabs scroll horizontally with 52px edge padding (MD3 spec).
|
|
109
|
+
* When false, tabs divide the available width equally (flex-1).
|
|
110
|
+
* @default false
|
|
111
|
+
*/
|
|
112
|
+
scrollable?: boolean;
|
|
113
|
+
/**
|
|
114
|
+
* Background color override for the tab bar.
|
|
115
|
+
* @default "var(--md-sys-color-surface)"
|
|
116
|
+
*/
|
|
117
|
+
backgroundColor?: string;
|
|
118
|
+
/** Tab components as children. */
|
|
119
|
+
children: React.ReactNode;
|
|
120
|
+
/** Additional CSS class names. */
|
|
121
|
+
className?: string;
|
|
122
|
+
/** Forwarded aria-label for the tablist. */
|
|
123
|
+
"aria-label"?: string;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Props for an individual `<Tab>` component.
|
|
128
|
+
*
|
|
129
|
+
* @example
|
|
130
|
+
* ```tsx
|
|
131
|
+
* <Tab value="flights" icon={<Icon name="flight" />}>Flights</Tab>
|
|
132
|
+
* <Tab value="trips" disabled>Trips</Tab>
|
|
133
|
+
* <Tab value="hotels" icon={<Icon name="hotel" />} inlineIcon>Hotels</Tab>
|
|
134
|
+
* ```
|
|
135
|
+
*/
|
|
136
|
+
export interface TabProps {
|
|
137
|
+
/** Unique value identifying this tab. Must match a `<TabsContent value>`. */
|
|
138
|
+
value: string;
|
|
139
|
+
/**
|
|
140
|
+
* Optional icon rendered with the label text.
|
|
141
|
+
* - Default (stacked): icon above label, height increases to 64dp.
|
|
142
|
+
* - With `inlineIcon`: icon beside label (same row), height stays 48dp.
|
|
143
|
+
*/
|
|
144
|
+
icon?: React.ReactNode;
|
|
145
|
+
/**
|
|
146
|
+
* When true, icon is placed inline (same row) with the label text.
|
|
147
|
+
* Container height stays at 48dp (does NOT increase to 64dp).
|
|
148
|
+
* Mirrors the `inline-icon` attribute on `<md-primary-tab>`.
|
|
149
|
+
* @default false
|
|
150
|
+
*/
|
|
151
|
+
inlineIcon?: boolean;
|
|
152
|
+
/**
|
|
153
|
+
* When true, disables interaction.
|
|
154
|
+
* Disabled tabs are skipped entirely in keyboard navigation (ArrowKey).
|
|
155
|
+
*/
|
|
156
|
+
disabled?: boolean;
|
|
157
|
+
/** Additional CSS class names. */
|
|
158
|
+
className?: string;
|
|
159
|
+
/**
|
|
160
|
+
* Optional badge element overlaid on the tab content.
|
|
161
|
+
* Handled via `BadgedBox`:
|
|
162
|
+
* - Stacked icon: Overlaps icon's top-trailing corner.
|
|
163
|
+
* - Inline/Text-only: Placed next to the text.
|
|
164
|
+
*/
|
|
165
|
+
badge?: React.ReactNode;
|
|
166
|
+
/** Label text rendered inside the tab. */
|
|
167
|
+
children: React.ReactNode;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Props for the `<TabsContent>` panel component.
|
|
172
|
+
*
|
|
173
|
+
* @example
|
|
174
|
+
* ```tsx
|
|
175
|
+
* <TabsContent value="flights">Flight content here</TabsContent>
|
|
176
|
+
* ```
|
|
177
|
+
*/
|
|
178
|
+
export interface TabsContentProps {
|
|
179
|
+
/** Must match the `value` of a sibling `<Tab>`. */
|
|
180
|
+
value: string;
|
|
181
|
+
/** Additional CSS class names. */
|
|
182
|
+
className?: string;
|
|
183
|
+
/** Panel content. */
|
|
184
|
+
children: React.ReactNode;
|
|
185
|
+
}
|
|
@@ -7,5 +7,12 @@
|
|
|
7
7
|
* import { TextField } from '@bug-on/md3-react';
|
|
8
8
|
* ```
|
|
9
9
|
*/
|
|
10
|
+
|
|
10
11
|
export { TextField } from "./text-field";
|
|
11
|
-
export type {
|
|
12
|
+
export type {
|
|
13
|
+
TextFieldHandle,
|
|
14
|
+
TextFieldInputType,
|
|
15
|
+
TextFieldProps,
|
|
16
|
+
TextFieldTrailingIconMode,
|
|
17
|
+
TextFieldVariant,
|
|
18
|
+
} from "./text-field.types";
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file active-indicator.tsx
|
|
3
|
+
* Animated bottom border line for the MD3 Filled TextField.
|
|
4
|
+
* Expands height from 1px → 2px and color changes on focus.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { m } from "motion/react";
|
|
8
|
+
import * as React from "react";
|
|
9
|
+
import {
|
|
10
|
+
MD3_INDICATOR_DURATION,
|
|
11
|
+
MD3_STANDARD_EASING,
|
|
12
|
+
} from "../../shared/constants";
|
|
13
|
+
import { TF_COLORS, TF_SIZE } from "../text-field.tokens";
|
|
14
|
+
|
|
15
|
+
export interface ActiveIndicatorProps {
|
|
16
|
+
isFocused: boolean;
|
|
17
|
+
isError: boolean;
|
|
18
|
+
isDisabled: boolean;
|
|
19
|
+
isHovered: boolean;
|
|
20
|
+
prefersReduced: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* MD3 Active Indicator — the bottom border line for Filled TextField.
|
|
25
|
+
*
|
|
26
|
+
* Animates:
|
|
27
|
+
* - `height`: 1px (enabled) → 2px (focused)
|
|
28
|
+
* - `backgroundColor`: on-surface-variant → primary (focused) → error
|
|
29
|
+
* - `scaleX`: 0 → 1 expanding from center on focus in
|
|
30
|
+
*
|
|
31
|
+
* @see https://m3.material.io/components/text-fields/specs#filled-text-field
|
|
32
|
+
*/
|
|
33
|
+
export const ActiveIndicator = React.memo(function ActiveIndicator({
|
|
34
|
+
isFocused,
|
|
35
|
+
isError,
|
|
36
|
+
isDisabled,
|
|
37
|
+
isHovered,
|
|
38
|
+
prefersReduced,
|
|
39
|
+
}: ActiveIndicatorProps) {
|
|
40
|
+
const duration = prefersReduced ? 0 : MD3_INDICATOR_DURATION;
|
|
41
|
+
const ease = MD3_STANDARD_EASING;
|
|
42
|
+
|
|
43
|
+
const height =
|
|
44
|
+
isFocused || isError ? TF_SIZE.indicatorThick : TF_SIZE.indicatorThin;
|
|
45
|
+
|
|
46
|
+
let backgroundColor: string;
|
|
47
|
+
if (isError) {
|
|
48
|
+
backgroundColor = TF_COLORS.error;
|
|
49
|
+
} else if (isFocused) {
|
|
50
|
+
backgroundColor = TF_COLORS.primary;
|
|
51
|
+
} else if (isHovered && !isDisabled) {
|
|
52
|
+
backgroundColor = TF_COLORS.inputText;
|
|
53
|
+
} else {
|
|
54
|
+
backgroundColor = TF_COLORS.onSurfaceVariant;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<m.div
|
|
59
|
+
aria-hidden="true"
|
|
60
|
+
className="absolute bottom-0 left-0 right-0 origin-center"
|
|
61
|
+
animate={{ height, backgroundColor }}
|
|
62
|
+
transition={{ duration, ease }}
|
|
63
|
+
/>
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
ActiveIndicator.displayName = "ActiveIndicator";
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file floating-label.tsx
|
|
3
|
+
* Animated floating label for MD3 TextField.
|
|
4
|
+
* Animates between inline (body large) and floated (body small) positions.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { AnimatePresence, m } from "motion/react";
|
|
8
|
+
import * as React from "react";
|
|
9
|
+
import { cn } from "../../../lib/utils";
|
|
10
|
+
import {
|
|
11
|
+
MD3_LABEL_FLOAT_DURATION,
|
|
12
|
+
MD3_STANDARD_EASING,
|
|
13
|
+
} from "../../shared/constants";
|
|
14
|
+
import { TF_COLORS, TF_TYPOGRAPHY } from "../text-field.tokens";
|
|
15
|
+
|
|
16
|
+
export interface FloatingLabelProps {
|
|
17
|
+
/** The label text content. */
|
|
18
|
+
text: string;
|
|
19
|
+
/** Whether the label is in the floated (small) position. */
|
|
20
|
+
isFloated: boolean;
|
|
21
|
+
/** Whether the field is currently focused. */
|
|
22
|
+
isFocused: boolean;
|
|
23
|
+
/** Whether the field is in error state. */
|
|
24
|
+
isError: boolean;
|
|
25
|
+
/** Whether the field is disabled. */
|
|
26
|
+
isDisabled: boolean;
|
|
27
|
+
/** 'filled' or 'outlined' — determines vertical y offset. */
|
|
28
|
+
variant: "filled" | "outlined";
|
|
29
|
+
/** Container height in px (56 normal, 48 dense). */
|
|
30
|
+
containerHeight: number;
|
|
31
|
+
/** Whether to skip animations (prefers-reduced-motion). */
|
|
32
|
+
prefersReduced: boolean;
|
|
33
|
+
/** Whether the required asterisk should be shown. */
|
|
34
|
+
showAsterisk: boolean;
|
|
35
|
+
/** ID of the label element, for associating with a containing element. */
|
|
36
|
+
htmlFor?: string;
|
|
37
|
+
/** Ref callback so the parent can measure label width for the outlined notch. */
|
|
38
|
+
labelRef?: React.Ref<HTMLSpanElement>;
|
|
39
|
+
/** Whether there is a leading icon. */
|
|
40
|
+
hasLeading?: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Calculates the y-translation (in px) for the floated label position.
|
|
45
|
+
*
|
|
46
|
+
* For FILLED: move up from vertical center to the top inline padding area.
|
|
47
|
+
* For OUTLINED: move up so the label sits on the top border line.
|
|
48
|
+
*
|
|
49
|
+
* The label starts at y=0 (vertically centered by flex parent).
|
|
50
|
+
* When floated, it moves upward by `offset` pixels.
|
|
51
|
+
*/
|
|
52
|
+
function getFloatedY(
|
|
53
|
+
variant: "filled" | "outlined",
|
|
54
|
+
containerHeight: number,
|
|
55
|
+
): number {
|
|
56
|
+
// Label body-large line height ≈ 24px (16px * 1.5)
|
|
57
|
+
// Label body-small line height ≈ 16px (12px * 1.33)
|
|
58
|
+
// Floated container top-padding: 8px from top of container
|
|
59
|
+
const labelSmallHeight = 16;
|
|
60
|
+
const paddingTop = 8;
|
|
61
|
+
|
|
62
|
+
if (variant === "filled") {
|
|
63
|
+
// Center of container → top-padding area
|
|
64
|
+
// Center is at containerHeight/2, floated center is at paddingTop + labelSmallHeight/2
|
|
65
|
+
const floatedCenter = paddingTop + labelSmallHeight / 2;
|
|
66
|
+
const unfloatedCenter = containerHeight / 2;
|
|
67
|
+
return -(unfloatedCenter - floatedCenter);
|
|
68
|
+
}
|
|
69
|
+
// Outlined: label sits on the border line (y = 0 relative to container top)
|
|
70
|
+
// We move up by half the container height to reach the border
|
|
71
|
+
return -(containerHeight / 2);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Returns the label color based on current state.
|
|
76
|
+
*/
|
|
77
|
+
function getLabelColor(
|
|
78
|
+
isFloated: boolean,
|
|
79
|
+
isFocused: boolean,
|
|
80
|
+
isError: boolean,
|
|
81
|
+
isDisabled: boolean,
|
|
82
|
+
): string {
|
|
83
|
+
if (isDisabled) return TF_COLORS.onSurfaceVariant;
|
|
84
|
+
if (isError) return TF_COLORS.error;
|
|
85
|
+
if (isFloated && isFocused) return TF_COLORS.primary;
|
|
86
|
+
return TF_COLORS.onSurfaceVariant;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* MD3 Expressive Floating Label.
|
|
91
|
+
*
|
|
92
|
+
* Animates y-position, scale, and color when the label floats.
|
|
93
|
+
* Uses `transformOrigin: 'left center'` so scaling anchors at the start.
|
|
94
|
+
*
|
|
95
|
+
* @accessibility
|
|
96
|
+
* Rendered as a `<label>` with `htmlFor` linking to the `<input>`.
|
|
97
|
+
* When floated, visual size changes but the semantic label is unchanged.
|
|
98
|
+
*/
|
|
99
|
+
export const FloatingLabel = React.memo(function FloatingLabel({
|
|
100
|
+
text,
|
|
101
|
+
isFloated,
|
|
102
|
+
isFocused,
|
|
103
|
+
isError,
|
|
104
|
+
isDisabled,
|
|
105
|
+
variant,
|
|
106
|
+
containerHeight,
|
|
107
|
+
prefersReduced,
|
|
108
|
+
showAsterisk,
|
|
109
|
+
htmlFor,
|
|
110
|
+
labelRef,
|
|
111
|
+
hasLeading = false,
|
|
112
|
+
}: FloatingLabelProps) {
|
|
113
|
+
const duration = prefersReduced ? 0 : MD3_LABEL_FLOAT_DURATION;
|
|
114
|
+
const ease = MD3_STANDARD_EASING;
|
|
115
|
+
|
|
116
|
+
const y = isFloated ? getFloatedY(variant, containerHeight) : 0;
|
|
117
|
+
const x = variant === "outlined" && isFloated && hasLeading ? -36 : 0;
|
|
118
|
+
const scale = isFloated ? TF_TYPOGRAPHY.labelScaleRatio : 1;
|
|
119
|
+
const color = getLabelColor(isFloated, isFocused, isError, isDisabled);
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<m.label
|
|
123
|
+
htmlFor={htmlFor}
|
|
124
|
+
className={cn(
|
|
125
|
+
"absolute pointer-events-none select-none origin-[left_center] leading-6 text-base whitespace-nowrap",
|
|
126
|
+
"px-1 -mx-1", // Clear the notch gap and keep text aligned
|
|
127
|
+
variant === "outlined" && isFloated && "bg-m3-surface",
|
|
128
|
+
"left-4",
|
|
129
|
+
isDisabled && "opacity-[0.38]",
|
|
130
|
+
)}
|
|
131
|
+
animate={{ y, x, scale, color }}
|
|
132
|
+
transition={{ duration, ease }}
|
|
133
|
+
style={{
|
|
134
|
+
top: (containerHeight - 24) / 2, // 24px is the resting line-height
|
|
135
|
+
transformOrigin: "left center",
|
|
136
|
+
zIndex: 1,
|
|
137
|
+
}}
|
|
138
|
+
>
|
|
139
|
+
<span ref={labelRef} className="inline-block relative">
|
|
140
|
+
{text}
|
|
141
|
+
<AnimatePresence>
|
|
142
|
+
{showAsterisk && (
|
|
143
|
+
<m.span
|
|
144
|
+
key="asterisk"
|
|
145
|
+
aria-hidden="true"
|
|
146
|
+
initial={{ opacity: 0 }}
|
|
147
|
+
animate={{ opacity: 1 }}
|
|
148
|
+
exit={{ opacity: 0 }}
|
|
149
|
+
transition={{ duration: prefersReduced ? 0 : 0.1 }}
|
|
150
|
+
className="ml-0.5 text-m3-error"
|
|
151
|
+
>
|
|
152
|
+
*
|
|
153
|
+
</m.span>
|
|
154
|
+
)}
|
|
155
|
+
</AnimatePresence>
|
|
156
|
+
</span>
|
|
157
|
+
</m.label>
|
|
158
|
+
);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
FloatingLabel.displayName = "FloatingLabel";
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file leading-icon.tsx
|
|
3
|
+
* Leading icon slot for MD3 TextField.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as React from "react";
|
|
7
|
+
import { TF_COLORS } from "../text-field.tokens";
|
|
8
|
+
|
|
9
|
+
export interface LeadingIconProps {
|
|
10
|
+
/** Icon node — should be 24×24px. */
|
|
11
|
+
children: React.ReactNode;
|
|
12
|
+
/** Whether the field is in error state (changes icon color). */
|
|
13
|
+
isError: boolean;
|
|
14
|
+
/** Whether the field is disabled. */
|
|
15
|
+
isDisabled: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* MD3 Leading Icon wrapper.
|
|
20
|
+
*
|
|
21
|
+
* Decorative — `aria-hidden="true"`.
|
|
22
|
+
* Color: `on-surface-variant` (default), `error` (error state).
|
|
23
|
+
* Size: 24×24px icon, 48×56px touch target via flex alignment.
|
|
24
|
+
*
|
|
25
|
+
* @see https://m3.material.io/components/text-fields/specs#anatomy
|
|
26
|
+
*/
|
|
27
|
+
export const LeadingIcon = React.memo(function LeadingIcon({
|
|
28
|
+
children,
|
|
29
|
+
isError,
|
|
30
|
+
isDisabled,
|
|
31
|
+
}: LeadingIconProps) {
|
|
32
|
+
const color =
|
|
33
|
+
isError && !isDisabled ? TF_COLORS.error : TF_COLORS.onSurfaceVariant;
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div
|
|
37
|
+
aria-hidden="true"
|
|
38
|
+
className="flex items-center justify-center shrink-0 w-6 h-6 ml-3"
|
|
39
|
+
style={{ color }}
|
|
40
|
+
>
|
|
41
|
+
{children}
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
LeadingIcon.displayName = "LeadingIcon";
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file outline-container.tsx
|
|
3
|
+
* MD3-compliant outlined border with animated notch for the Outlined TextField.
|
|
4
|
+
*
|
|
5
|
+
* Implementation: 3-segment approach inspired by Material Web's fieldset/legend pattern.
|
|
6
|
+
* The top border is split into: [left-segment] [notch-gap] [right-segment].
|
|
7
|
+
* The notch-gap width animates from 0 → (labelWidth × scaleRatio + 8px) when label floats.
|
|
8
|
+
*
|
|
9
|
+
* This mirrors Material Web's implementation without requiring <fieldset> semantics.
|
|
10
|
+
*
|
|
11
|
+
* @see https://github.com/material-components/material-web/tree/main/textfield
|
|
12
|
+
* @see https://m3.material.io/components/text-fields/specs#outlined-text-field
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { m } from "motion/react";
|
|
16
|
+
import * as React from "react";
|
|
17
|
+
import {
|
|
18
|
+
MD3_INDICATOR_DURATION,
|
|
19
|
+
MD3_LABEL_FLOAT_DURATION,
|
|
20
|
+
MD3_STANDARD_EASING,
|
|
21
|
+
} from "../../shared/constants";
|
|
22
|
+
import { TF_COLORS, TF_SIZE, TF_TYPOGRAPHY } from "../text-field.tokens";
|
|
23
|
+
|
|
24
|
+
export interface OutlineContainerProps {
|
|
25
|
+
/** Whether the label is in the floated position. */
|
|
26
|
+
isFloated: boolean;
|
|
27
|
+
/** Whether the field is focused. */
|
|
28
|
+
isFocused: boolean;
|
|
29
|
+
/** Whether the field is in error state. */
|
|
30
|
+
isError: boolean;
|
|
31
|
+
/** Whether the field is disabled. */
|
|
32
|
+
isDisabled: boolean;
|
|
33
|
+
/** Whether the field is hovered. */
|
|
34
|
+
isHovered: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Measured width of the label element in its full (unfloated) size.
|
|
37
|
+
* The notch width = labelWidth × scaleRatio + 2×notchPadding.
|
|
38
|
+
*/
|
|
39
|
+
labelWidth: number;
|
|
40
|
+
/** Whether to disable animations. */
|
|
41
|
+
prefersReduced: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Determines the outline border color based on current state.
|
|
46
|
+
*/
|
|
47
|
+
function getOutlineColor(
|
|
48
|
+
isFocused: boolean,
|
|
49
|
+
isError: boolean,
|
|
50
|
+
isHovered: boolean,
|
|
51
|
+
isDisabled: boolean,
|
|
52
|
+
): string {
|
|
53
|
+
if (isDisabled) return TF_COLORS.onSurfaceVariant;
|
|
54
|
+
if (isError) return TF_COLORS.error;
|
|
55
|
+
if (isFocused) return TF_COLORS.primary;
|
|
56
|
+
if (isHovered) return TF_COLORS.inputText;
|
|
57
|
+
return TF_COLORS.outline;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* MD3 Outlined TextField container with animated notch.
|
|
62
|
+
*
|
|
63
|
+
* The notch gap expands/collapses in sync with the FloatingLabel animation,
|
|
64
|
+
* creating the visual effect of the label breaking through the border.
|
|
65
|
+
*
|
|
66
|
+
* Accessibility: `aria-hidden="true"` — purely decorative border.
|
|
67
|
+
*/
|
|
68
|
+
export const OutlineContainer = React.memo(function OutlineContainer({
|
|
69
|
+
isFloated,
|
|
70
|
+
isFocused,
|
|
71
|
+
isError,
|
|
72
|
+
isDisabled,
|
|
73
|
+
isHovered,
|
|
74
|
+
labelWidth,
|
|
75
|
+
prefersReduced,
|
|
76
|
+
}: OutlineContainerProps) {
|
|
77
|
+
const colorDuration = prefersReduced ? 0 : MD3_INDICATOR_DURATION;
|
|
78
|
+
const notchDuration = prefersReduced ? 0 : MD3_LABEL_FLOAT_DURATION;
|
|
79
|
+
const ease = MD3_STANDARD_EASING;
|
|
80
|
+
|
|
81
|
+
const borderColor = getOutlineColor(
|
|
82
|
+
isFocused,
|
|
83
|
+
isError,
|
|
84
|
+
isHovered,
|
|
85
|
+
isDisabled,
|
|
86
|
+
);
|
|
87
|
+
const borderWidth =
|
|
88
|
+
isFocused || isError ? TF_SIZE.outlineThick : TF_SIZE.outlineThin;
|
|
89
|
+
|
|
90
|
+
// Calculate offset for notch (always 16px from the edge in M3 Outlined)
|
|
91
|
+
const leftSegmentWidth = TF_SIZE.paddingStart;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Notch width calculation:
|
|
95
|
+
* When floated: label is scaled to 0.75, so its displayed width = labelWidth × 0.75
|
|
96
|
+
* Add 2 × notchPadding (4px each side) for spacing inside the notch gap.
|
|
97
|
+
* When unfloated: 0 (no gap in the border).
|
|
98
|
+
*/
|
|
99
|
+
const notchWidth = isFloated
|
|
100
|
+
? labelWidth * TF_TYPOGRAPHY.labelScaleRatio + TF_SIZE.notchPadding * 2
|
|
101
|
+
: 0;
|
|
102
|
+
|
|
103
|
+
const borderTransition = { duration: colorDuration, ease };
|
|
104
|
+
const notchTransition = { duration: notchDuration, ease };
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<div
|
|
108
|
+
aria-hidden="true"
|
|
109
|
+
className="absolute inset-0 pointer-events-none flex rounded-[inherit]"
|
|
110
|
+
>
|
|
111
|
+
{/* Left segment — left + bottom + top borders */}
|
|
112
|
+
<m.div
|
|
113
|
+
className="rounded-tl-[inherit] rounded-bl-[inherit]"
|
|
114
|
+
style={{ width: leftSegmentWidth - TF_SIZE.notchPadding }}
|
|
115
|
+
animate={{
|
|
116
|
+
borderColor,
|
|
117
|
+
borderWidth,
|
|
118
|
+
borderStyle: "solid",
|
|
119
|
+
borderRightWidth: 0,
|
|
120
|
+
}}
|
|
121
|
+
transition={borderTransition}
|
|
122
|
+
/>
|
|
123
|
+
|
|
124
|
+
{/* Center (notch) segment */}
|
|
125
|
+
<m.div
|
|
126
|
+
className="flex flex-col shrink-0"
|
|
127
|
+
style={{ minWidth: 0 }}
|
|
128
|
+
animate={{ width: notchWidth }}
|
|
129
|
+
transition={notchTransition}
|
|
130
|
+
>
|
|
131
|
+
{/* Top border of notch: transparent when notched */}
|
|
132
|
+
<m.div
|
|
133
|
+
className="shrink-0"
|
|
134
|
+
style={{ height: borderWidth }}
|
|
135
|
+
animate={{
|
|
136
|
+
borderTopColor: borderColor,
|
|
137
|
+
opacity: isFloated ? 0 : 1,
|
|
138
|
+
borderTopWidth: borderWidth,
|
|
139
|
+
borderTopStyle: "solid",
|
|
140
|
+
}}
|
|
141
|
+
transition={borderTransition}
|
|
142
|
+
/>
|
|
143
|
+
{/* Bottom border (always present) */}
|
|
144
|
+
<m.div
|
|
145
|
+
className="flex-1"
|
|
146
|
+
animate={{
|
|
147
|
+
borderBottomColor: borderColor,
|
|
148
|
+
borderBottomWidth: borderWidth,
|
|
149
|
+
borderBottomStyle: "solid",
|
|
150
|
+
}}
|
|
151
|
+
transition={borderTransition}
|
|
152
|
+
/>
|
|
153
|
+
</m.div>
|
|
154
|
+
|
|
155
|
+
{/* Right segment — full height, right + bottom + top borders */}
|
|
156
|
+
<m.div
|
|
157
|
+
className="flex-1 rounded-tr-[inherit] rounded-br-[inherit]"
|
|
158
|
+
animate={{
|
|
159
|
+
borderColor,
|
|
160
|
+
borderWidth,
|
|
161
|
+
borderStyle: "solid",
|
|
162
|
+
borderLeftWidth: 0,
|
|
163
|
+
}}
|
|
164
|
+
transition={borderTransition}
|
|
165
|
+
/>
|
|
166
|
+
</div>
|
|
167
|
+
);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
OutlineContainer.displayName = "OutlineContainer";
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file prefix-suffix.tsx
|
|
3
|
+
* Prefix and suffix text for MD3 TextField.
|
|
4
|
+
* Visible only when the label is floated (or when there is no label).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { AnimatePresence, m } from "motion/react";
|
|
8
|
+
import * as React from "react";
|
|
9
|
+
import {
|
|
10
|
+
MD3_LABEL_FLOAT_DURATION,
|
|
11
|
+
MD3_STANDARD_EASING,
|
|
12
|
+
} from "../../shared/constants";
|
|
13
|
+
import { TF_CLASSES } from "../text-field.tokens";
|
|
14
|
+
|
|
15
|
+
export interface PrefixSuffixProps {
|
|
16
|
+
text: string;
|
|
17
|
+
type: "prefix" | "suffix";
|
|
18
|
+
/** Whether the label is floated (controls visibility). */
|
|
19
|
+
visible: boolean;
|
|
20
|
+
/** Disable animations. */
|
|
21
|
+
prefersReduced: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* MD3 Prefix / Suffix Text.
|
|
26
|
+
*
|
|
27
|
+
* Animates in/out in sync with the floating label using AnimatePresence.
|
|
28
|
+
* Hidden when label is in the inline position (would overlap the label).
|
|
29
|
+
*
|
|
30
|
+
* @accessibility
|
|
31
|
+
* `aria-hidden="true"` — decorative. Screen readers read the full value in context.
|
|
32
|
+
*/
|
|
33
|
+
export const PrefixSuffix = React.memo(function PrefixSuffix({
|
|
34
|
+
text,
|
|
35
|
+
type,
|
|
36
|
+
visible,
|
|
37
|
+
prefersReduced,
|
|
38
|
+
}: PrefixSuffixProps) {
|
|
39
|
+
const duration = prefersReduced ? 0 : MD3_LABEL_FLOAT_DURATION;
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<AnimatePresence>
|
|
43
|
+
{visible && (
|
|
44
|
+
<m.span
|
|
45
|
+
aria-hidden="true"
|
|
46
|
+
className={`${TF_CLASSES.prefixSuffix} ${type === "suffix" ? "ml-0.5" : "mr-0.5"}`}
|
|
47
|
+
initial={{ opacity: 0 }}
|
|
48
|
+
animate={{ opacity: 1 }}
|
|
49
|
+
exit={{ opacity: 0 }}
|
|
50
|
+
transition={{ duration, ease: MD3_STANDARD_EASING }}
|
|
51
|
+
>
|
|
52
|
+
{text}
|
|
53
|
+
</m.span>
|
|
54
|
+
)}
|
|
55
|
+
</AnimatePresence>
|
|
56
|
+
);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
PrefixSuffix.displayName = "PrefixSuffix";
|