@noxlovette/material 0.5.1 → 0.6.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/bin/material-claude-skill.js +31 -0
- package/claude-skill/material-design/SKILL.md +68 -0
- package/claude-skill/material-design/references/accessibility-checklist.md +29 -0
- package/claude-skill/material-design/references/component-patterns.md +55 -0
- package/claude-skill/material-design/references/motion-guide.md +163 -0
- package/claude-skill/material-design/references/tokens-and-styles.md +68 -0
- package/dist/animation/Shapes.stories.svelte +10 -6
- package/dist/animation/TransitionPatterns.stories.svelte +30 -20
- package/dist/animation/containerTransform.d.ts +4 -2
- package/dist/animation/containerTransform.js +4 -2
- package/dist/animation/enterExit.js +5 -2
- package/dist/animation/skeleton.d.ts +1 -1
- package/dist/animation/skeleton.js +1 -1
- package/dist/components/badge/Badge.mdx +70 -0
- package/dist/components/badge/Badge.stories.svelte +38 -20
- package/dist/components/badge/Badge.svelte +12 -5
- package/dist/components/badge/theme.d.ts +30 -7
- package/dist/components/badge/theme.js +25 -5
- package/dist/components/badge/types.d.ts +18 -4
- package/dist/components/buttons/Button.mdx +85 -0
- package/dist/components/buttons/Button.stories.svelte +62 -27
- package/dist/components/buttons/Button.svelte +29 -25
- package/dist/components/buttons/Button.svelte.d.ts +11 -9
- package/dist/components/buttons/ButtonIcon.mdx +52 -0
- package/dist/components/buttons/ButtonIcon.stories.svelte +72 -48
- package/dist/components/buttons/ButtonIcon.svelte +58 -36
- package/dist/components/buttons/ButtonIcon.svelte.d.ts +9 -7
- package/dist/components/buttons/FAB.mdx +84 -0
- package/dist/components/buttons/FAB.stories.svelte +103 -24
- package/dist/components/buttons/FAB.stories.svelte.d.ts +2 -17
- package/dist/components/buttons/FAB.svelte +353 -29
- package/dist/components/buttons/FAB.svelte.d.ts +11 -6
- package/dist/components/buttons/FABMenu.svelte +27 -8
- package/dist/components/buttons/FABMenu.svelte.d.ts +7 -3
- package/dist/components/buttons/FABMenuItem.svelte +20 -16
- package/dist/components/buttons/FABMenuItem.svelte.d.ts +3 -3
- package/dist/components/buttons/Toggle.stories.svelte +31 -23
- package/dist/components/buttons/Toggle.stories.svelte.d.ts +2 -17
- package/dist/components/buttons/Toggle.svelte +28 -25
- package/dist/components/buttons/Toggle.svelte.d.ts +5 -5
- package/dist/components/buttons/button-group/ButtonGroup.mdx +39 -0
- package/dist/components/buttons/button-group/ButtonGroup.stories.svelte +62 -43
- package/dist/components/buttons/button-group/ButtonGroup.stories.svelte.d.ts +2 -17
- package/dist/components/buttons/button-group/ButtonGroup.svelte +102 -8
- package/dist/components/buttons/button-group/ButtonGroup.svelte.d.ts +6 -7
- package/dist/components/buttons/button-group/theme.d.ts +46 -16
- package/dist/components/buttons/button-group/theme.js +14 -9
- package/dist/components/buttons/button-group/types.d.ts +13 -7
- package/dist/components/buttons/connected/ConnectedButtonGroup.mdx +43 -0
- package/dist/components/buttons/connected/ConnectedButtonGroup.stories.svelte +56 -74
- package/dist/components/buttons/connected/ConnectedButtonGroup.stories.svelte.d.ts +2 -17
- package/dist/components/buttons/connected/ConnectedButtonGroup.svelte +16 -4
- package/dist/components/buttons/connected/ConnectedButtonGroup.svelte.d.ts +4 -4
- package/dist/components/buttons/connected/ConnectedButtonGroupItem.svelte +35 -8
- package/dist/components/buttons/connected/ConnectedButtonGroupItem.svelte.d.ts +2 -3
- package/dist/components/buttons/connected/theme.d.ts +34 -19
- package/dist/components/buttons/connected/theme.js +33 -10
- package/dist/components/buttons/connected/types.d.ts +36 -9
- package/dist/components/buttons/shapeMorph.d.ts +26 -0
- package/dist/components/buttons/shapeMorph.js +142 -0
- package/dist/components/buttons/split-button/SplitButton.mdx +48 -0
- package/dist/components/buttons/split-button/SplitButton.stories.svelte +9 -14
- package/dist/components/buttons/split-button/SplitButton.svelte +66 -49
- package/dist/components/buttons/split-button/SplitButton.svelte.d.ts +6 -11
- package/dist/components/buttons/split-button/theme.d.ts +111 -18
- package/dist/components/buttons/split-button/theme.js +75 -17
- package/dist/components/buttons/split-button/types.d.ts +24 -18
- package/dist/components/buttons/theme.d.ts +423 -231
- package/dist/components/buttons/theme.js +287 -381
- package/dist/components/buttons/types.d.ts +130 -125
- package/dist/components/cards/Card.mdx +44 -0
- package/dist/components/cards/Card.stories.svelte +11 -11
- package/dist/components/cards/theme.js +6 -6
- package/dist/components/chips/Chip.mdx +68 -0
- package/dist/components/chips/Chip.stories.svelte +4 -4
- package/dist/components/chips/theme.d.ts +1 -1
- package/dist/components/chips/theme.js +4 -4
- package/dist/components/containers/action-rail/ActionRail.stories.svelte +1 -0
- package/dist/components/containers/action-rail/theme.d.ts +1 -1
- package/dist/components/containers/action-rail/theme.js +2 -2
- package/dist/components/containers/app/theme.d.ts +1 -1
- package/dist/components/containers/app/theme.js +1 -1
- package/dist/components/containers/bottom-sheet/BottomSheet.mdx +64 -0
- package/dist/components/containers/bottom-sheet/BottomSheet.stories.svelte +108 -12
- package/dist/components/containers/bottom-sheet/BottomSheet.svelte +179 -109
- package/dist/components/containers/bottom-sheet/BottomSheet.svelte.d.ts +10 -5
- package/dist/components/containers/bottom-sheet/index.d.ts +1 -0
- package/dist/components/containers/bottom-sheet/index.js +1 -0
- package/dist/components/containers/bottom-sheet/theme.d.ts +45 -0
- package/dist/components/containers/bottom-sheet/theme.js +42 -0
- package/dist/components/containers/bottom-sheet/types.d.ts +37 -5
- package/dist/components/containers/carousel/Carousel.mdx +80 -0
- package/dist/components/containers/carousel/Carousel.stories.svelte +131 -0
- package/dist/components/containers/carousel/Carousel.stories.svelte.d.ts +4 -0
- package/dist/components/containers/carousel/Carousel.svelte +194 -0
- package/dist/components/containers/carousel/Carousel.svelte.d.ts +25 -0
- package/dist/components/containers/carousel/index.d.ts +3 -0
- package/dist/components/containers/{stack → carousel}/index.js +1 -2
- package/dist/components/containers/carousel/keylines.d.ts +32 -0
- package/dist/components/containers/carousel/keylines.js +88 -0
- package/dist/components/containers/carousel/theme.d.ts +162 -0
- package/dist/components/containers/carousel/theme.js +67 -0
- package/dist/components/containers/carousel/types.d.ts +52 -0
- package/dist/components/containers/context-menu/ContextMenu.stories.svelte +1 -0
- package/dist/components/containers/context-menu/ContextMenu.svelte +2 -2
- package/dist/components/containers/context-menu/ContextMenu.svelte.d.ts +1 -1
- package/dist/components/containers/context-menu/theme.d.ts +1 -1
- package/dist/components/containers/context-menu/theme.js +11 -11
- package/dist/components/containers/dialogue/Dialogue.mdx +65 -0
- package/dist/components/containers/dialogue/Dialogue.stories.svelte +4 -4
- package/dist/components/containers/dialogue/Dialogue.svelte +2 -2
- package/dist/components/containers/dialogue/theme.d.ts +1 -1
- package/dist/components/containers/dialogue/theme.js +4 -4
- package/dist/components/containers/dialogue/types.d.ts +1 -1
- package/dist/components/containers/divider/Divider.stories.svelte +2 -1
- package/dist/components/containers/divider/Divider.svelte +2 -2
- package/dist/components/containers/divider/theme.d.ts +7 -7
- package/dist/components/containers/divider/theme.js +3 -3
- package/dist/components/containers/index.d.ts +1 -1
- package/dist/components/containers/index.js +1 -1
- package/dist/components/containers/link-preview/theme.d.ts +1 -1
- package/dist/components/containers/link-preview/theme.js +3 -3
- package/dist/components/containers/list/List.svelte +18 -0
- package/dist/components/containers/list/List.svelte.d.ts +4 -0
- package/dist/components/containers/list/ListItem.mdx +100 -0
- package/dist/components/containers/list/ListItem.stories.svelte +177 -42
- package/dist/components/containers/list/ListItem.stories.svelte.d.ts +2 -17
- package/dist/components/containers/list/ListItem.svelte +92 -37
- package/dist/components/containers/list/ListItem.svelte.d.ts +1 -1
- package/dist/components/containers/list/context.d.ts +6 -0
- package/dist/components/containers/list/context.js +4 -0
- package/dist/components/containers/list/index.d.ts +1 -0
- package/dist/components/containers/list/index.js +1 -0
- package/dist/components/containers/list/theme.d.ts +487 -12
- package/dist/components/containers/list/theme.js +141 -14
- package/dist/components/containers/list/types.d.ts +39 -5
- package/dist/components/containers/menu/Menu.mdx +52 -0
- package/dist/components/containers/menu/Menu.stories.svelte +14 -14
- package/dist/components/containers/menu/Menu.svelte +2 -2
- package/dist/components/containers/menu/MenuGroup.svelte +1 -1
- package/dist/components/containers/menu/MenuGroup.svelte.d.ts +1 -1
- package/dist/components/containers/menu/MenuSub.svelte +1 -1
- package/dist/components/containers/menu/menu-item/MenuItem.svelte +1 -1
- package/dist/components/containers/menu/theme.d.ts +1 -1
- package/dist/components/containers/menu/theme.js +9 -9
- package/dist/components/containers/pane/DraggablePane.stories.svelte +4 -3
- package/dist/components/containers/pane/DraggablePane.svelte +2 -2
- package/dist/components/containers/pane/Pane.stories.svelte +11 -10
- package/dist/components/containers/pane/Pane.svelte +24 -2
- package/dist/components/containers/pane/PaneGrid.stories.svelte +19 -18
- package/dist/components/containers/pane/PaneGrid.svelte +3 -2
- package/dist/components/containers/pane/PaneGrid.svelte.d.ts +2 -1
- package/dist/components/containers/pane/PaneHandle.svelte +48 -2
- package/dist/components/containers/pane/PaneHandle.svelte.d.ts +4 -0
- package/dist/components/containers/pane/resizeStore.svelte.d.ts +10 -1
- package/dist/components/containers/pane/resizeStore.svelte.js +13 -2
- package/dist/components/containers/pane/theme.d.ts +1 -1
- package/dist/components/containers/pane/theme.js +131 -47
- package/dist/components/containers/pane/types.d.ts +3 -2
- package/dist/components/containers/popover/Popover.mdx +45 -0
- package/dist/components/containers/popover/Popover.stories.svelte +1 -1
- package/dist/components/containers/popover/Popover.svelte +1 -1
- package/dist/components/containers/popover/theme.d.ts +1 -1
- package/dist/components/containers/popover/theme.js +4 -4
- package/dist/components/containers/scroll-area/theme.js +3 -3
- package/dist/components/containers/side-sheet/SideSheet.mdx +71 -0
- package/dist/components/containers/side-sheet/SideSheet.stories.svelte +113 -17
- package/dist/components/containers/side-sheet/SideSheet.svelte +113 -66
- package/dist/components/containers/side-sheet/SideSheet.svelte.d.ts +8 -7
- package/dist/components/containers/side-sheet/index.d.ts +1 -0
- package/dist/components/containers/side-sheet/index.js +1 -0
- package/dist/components/containers/side-sheet/theme.d.ts +108 -0
- package/dist/components/containers/side-sheet/theme.js +72 -0
- package/dist/components/containers/side-sheet/types.d.ts +35 -7
- package/dist/components/date/DateField.stories.svelte +4 -3
- package/dist/components/date/DateField.svelte +11 -2
- package/dist/components/date/DateRangeField.stories.svelte +4 -3
- package/dist/components/date/DateRangeField.svelte +12 -3
- package/dist/components/date/theme.d.ts +8 -8
- package/dist/components/date/theme.js +32 -32
- package/dist/components/forms/checkbox/Checkbox.mdx +35 -0
- package/dist/components/forms/checkbox/Checkbox.stories.svelte +2 -2
- package/dist/components/forms/checkbox/Checkbox.svelte +1 -1
- package/dist/components/forms/checkbox/theme.d.ts +1 -1
- package/dist/components/forms/checkbox/theme.js +4 -4
- package/dist/components/forms/command/Command.stories.svelte +1 -0
- package/dist/components/forms/command/theme.d.ts +1 -1
- package/dist/components/forms/command/theme.js +10 -10
- package/dist/components/forms/pin/theme.d.ts +1 -1
- package/dist/components/forms/pin/theme.js +5 -5
- package/dist/components/forms/radio-group/RadioGroup.svelte +1 -1
- package/dist/components/forms/radio-group/theme.d.ts +1 -1
- package/dist/components/forms/radio-group/theme.js +6 -6
- package/dist/components/forms/search/Search.mdx +38 -0
- package/dist/components/forms/search/Search.stories.svelte +19 -9
- package/dist/components/forms/search/Search.svelte +40 -17
- package/dist/components/forms/search/Search.svelte.d.ts +2 -2
- package/dist/components/forms/search/theme.d.ts +46 -13
- package/dist/components/forms/search/theme.js +14 -9
- package/dist/components/forms/search/types.d.ts +18 -12
- package/dist/components/forms/select/Select.mdx +53 -0
- package/dist/components/forms/select/Select.stories.svelte +4 -4
- package/dist/components/forms/select/Select.svelte +12 -4
- package/dist/components/forms/select/SelectItem.svelte +1 -1
- package/dist/components/forms/select/theme.d.ts +1 -1
- package/dist/components/forms/select/theme.js +14 -14
- package/dist/components/forms/slider/Slider.mdx +85 -0
- package/dist/components/forms/slider/Slider.stories.svelte +7 -7
- package/dist/components/forms/slider/Slider.svelte +46 -28
- package/dist/components/forms/slider/theme.d.ts +15 -1
- package/dist/components/forms/slider/theme.js +29 -18
- package/dist/components/forms/slider/types.d.ts +3 -2
- package/dist/components/forms/switch/Switch.mdx +38 -0
- package/dist/components/forms/switch/Switch.stories.svelte +3 -3
- package/dist/components/forms/switch/theme.d.ts +1 -1
- package/dist/components/forms/switch/theme.js +7 -7
- package/dist/components/forms/textfield/Textfield.mdx +53 -0
- package/dist/components/forms/textfield/Textfield.stories.svelte +6 -6
- package/dist/components/forms/textfield/Textfield.svelte +1 -0
- package/dist/components/forms/textfield/theme.d.ts +1 -1
- package/dist/components/forms/textfield/theme.js +20 -20
- package/dist/components/forms/toggle-group/theme.d.ts +1 -1
- package/dist/components/forms/toggle-group/theme.js +3 -3
- package/dist/components/forms/tooltip/Tooltip.stories.svelte +1 -0
- package/dist/components/forms/tooltip/theme.d.ts +1 -1
- package/dist/components/forms/tooltip/theme.js +4 -4
- package/dist/components/misc/Avatar.stories.svelte +5 -4
- package/dist/components/misc/ThemeSettings.svelte +9 -9
- package/dist/components/misc/ThemeSwitcher.svelte +3 -3
- package/dist/components/misc/theme.js +5 -5
- package/dist/components/nav/appbar/AppBar.mdx +71 -0
- package/dist/components/nav/appbar/AppBar.stories.svelte +93 -3
- package/dist/components/nav/appbar/AppBar.stories.svelte.d.ts +2 -17
- package/dist/components/nav/appbar/AppBar.svelte +90 -37
- package/dist/components/nav/appbar/AppBar.svelte.d.ts +9 -2
- package/dist/components/nav/appbar/theme.d.ts +86 -19
- package/dist/components/nav/appbar/theme.js +160 -24
- package/dist/components/nav/appbar/types.d.ts +46 -9
- package/dist/components/nav/breadcrumb/Breadcrumb.stories.svelte +1 -0
- package/dist/components/nav/breadcrumb/theme.d.ts +1 -1
- package/dist/components/nav/breadcrumb/theme.js +6 -6
- package/dist/components/nav/navbar/Navbar.mdx +38 -0
- package/dist/components/nav/navbar/Navbar.svelte +1 -1
- package/dist/components/nav/navbar/NavbarItem.svelte +12 -9
- package/dist/components/nav/navbar/theme.d.ts +3 -3
- package/dist/components/nav/navbar/theme.js +7 -7
- package/dist/components/nav/rail/Rail.mdx +73 -0
- package/dist/components/nav/rail/Rail.stories.svelte +3 -7
- package/dist/components/nav/rail/Rail.svelte +5 -2
- package/dist/components/nav/rail/RailItem.svelte +12 -9
- package/dist/components/nav/rail/theme.d.ts +1 -1
- package/dist/components/nav/rail/theme.js +14 -14
- package/dist/components/nav/rail/types.d.ts +8 -1
- package/dist/components/nav/tabs/Tabs.mdx +62 -0
- package/dist/components/nav/tabs/Tabs.stories.svelte +1 -1
- package/dist/components/nav/tabs/theme.d.ts +1 -1
- package/dist/components/nav/tabs/theme.js +5 -5
- package/dist/components/pill/Pill.mdx +32 -0
- package/dist/components/pill/Pill.stories.svelte +1 -1
- package/dist/components/pill/theme.d.ts +2 -2
- package/dist/components/pill/theme.js +2 -2
- package/dist/components/progress/LinearProgress.svelte +2 -2
- package/dist/components/progress/Progress.mdx +50 -0
- package/dist/components/progress/Progress.stories.svelte +6 -6
- package/dist/components/snackbar/Snackbar.mdx +52 -0
- package/dist/components/snackbar/Snackbar.stories.svelte +5 -5
- package/dist/components/snackbar/Snackbar.svelte +1 -1
- package/dist/components/snackbar/theme.d.ts +4 -4
- package/dist/components/snackbar/theme.js +5 -5
- package/dist/components/table/Table.stories.svelte +1 -0
- package/dist/components/table/TableHeader.svelte +1 -1
- package/dist/components/table/theme.d.ts +1 -1
- package/dist/components/table/theme.js +4 -4
- package/dist/components/time/TimeField.stories.svelte +3 -2
- package/dist/components/time/TimeField.svelte +2 -2
- package/dist/components/time/TimepickerInput.svelte +1 -1
- package/dist/components/time/theme.d.ts +1 -1
- package/dist/components/time/theme.js +6 -6
- package/dist/components/toolbar/Toolbar.stories.svelte +1 -0
- package/dist/components/toolbar/Toolbar.svelte +1 -1
- package/dist/components/toolbar/Toolbar.svelte.d.ts +1 -1
- package/dist/components/toolbar/theme.d.ts +1 -1
- package/dist/components/toolbar/theme.js +15 -15
- package/dist/components/typography/Typography.mdx +67 -0
- package/dist/components/typography/body/Body.stories.svelte +2 -1
- package/dist/components/typography/body/theme.d.ts +16 -13
- package/dist/components/typography/body/theme.js +20 -5
- package/dist/components/typography/body/types.d.ts +2 -1
- package/dist/components/typography/display/Display.stories.svelte +2 -1
- package/dist/components/typography/display/theme.d.ts +16 -13
- package/dist/components/typography/display/theme.js +20 -5
- package/dist/components/typography/display/types.d.ts +2 -1
- package/dist/components/typography/headline/Headline.stories.svelte +2 -1
- package/dist/components/typography/headline/theme.d.ts +16 -13
- package/dist/components/typography/headline/theme.js +20 -5
- package/dist/components/typography/headline/types.d.ts +2 -1
- package/dist/components/typography/kbd/Kbd.stories.svelte +2 -1
- package/dist/components/typography/kbd/theme.d.ts +5 -5
- package/dist/components/typography/kbd/theme.js +3 -3
- package/dist/components/typography/label/Label.stories.svelte +2 -1
- package/dist/components/typography/label/theme.d.ts +15 -12
- package/dist/components/typography/label/theme.js +20 -5
- package/dist/components/typography/label/types.d.ts +2 -1
- package/dist/components/typography/title/Title.stories.svelte +2 -1
- package/dist/components/typography/title/theme.d.ts +16 -13
- package/dist/components/typography/title/theme.js +20 -5
- package/dist/components/typography/title/types.d.ts +2 -1
- package/dist/index.css +2 -0
- package/dist/styles/component.css +78 -175
- package/dist/styles/elevation.css +9 -0
- package/dist/styles/layers.css +33 -0
- package/dist/styles/spacing.css +29 -0
- package/dist/styles/typescale.css +635 -20
- package/dist/utils/Layer.svelte +1 -1
- package/dist/utils/icon/Icon.svelte +8 -6
- package/dist/utils/icon/theme.d.ts +1 -1
- package/dist/utils/icon/theme.js +1 -1
- package/dist/utils/tv.d.ts +6 -0
- package/dist/utils/tv.js +12 -0
- package/package.json +19 -15
- package/dist/components/containers/stack/HStack.svelte +0 -23
- package/dist/components/containers/stack/HStack.svelte.d.ts +0 -5
- package/dist/components/containers/stack/VStack.svelte +0 -23
- package/dist/components/containers/stack/VStack.svelte.d.ts +0 -5
- package/dist/components/containers/stack/index.d.ts +0 -4
- package/dist/components/containers/stack/theme.d.ts +0 -66
- package/dist/components/containers/stack/theme.js +0 -26
- package/dist/components/containers/stack/types.d.ts +0 -12
- /package/dist/components/containers/{stack → carousel}/types.js +0 -0
|
@@ -2,16 +2,27 @@
|
|
|
2
2
|
@component
|
|
3
3
|
Floating action buttons (FABs) help people take primary actions.
|
|
4
4
|
|
|
5
|
-
-
|
|
6
|
-
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
- Sizes: regular (56dp, default), medium (80dp), large (96dp); small (40dp) is baseline only
|
|
6
|
+
- `expanded` + `label`: the extended FAB, whose label slides open beside the icon. Responsive:
|
|
7
|
+
a FAB on compact windows and an extended FAB from medium up by default, and inside a `Rail`
|
|
8
|
+
it follows the rail's collapsed state
|
|
9
|
+
- `withMenu`: opens a FAB menu of `FABMenuItem`s; the FAB becomes its 56dp close button
|
|
10
|
+
- `surface`: opens into a surface with a container transform (FAB → sheet); the FAB's
|
|
11
|
+
container grows into it and changes colour on the way
|
|
9
12
|
|
|
10
|
-
@see https://m3.material.io/components/floating-action-button/
|
|
13
|
+
@see https://m3.material.io/components/floating-action-button/specs
|
|
14
|
+
@see https://m3.material.io/components/extended-fab/specs
|
|
15
|
+
@see https://m3.material.io/components/fab-menu/specs
|
|
11
16
|
-->
|
|
12
17
|
<script lang="ts">
|
|
13
18
|
import clsx from 'clsx';
|
|
14
|
-
import {
|
|
19
|
+
import { getContext, setContext, tick, untrack } from 'svelte';
|
|
20
|
+
import type { Attachment } from 'svelte/attachments';
|
|
21
|
+
import { animate, type AnimationPlaybackControls } from 'motion';
|
|
22
|
+
import { springTokens, springTransition } from '../../animation/spring.js';
|
|
23
|
+
import { clickOutside } from '../../attachments/index.js';
|
|
24
|
+
import { fab, fabExpandedClasses, fabMenuSet, fabSurface } from './theme.js';
|
|
25
|
+
import type { Breakpoint } from '../containers/pane/theme.js';
|
|
15
26
|
import type { FABProps } from './types.js';
|
|
16
27
|
import FABMenu from './FABMenu.svelte';
|
|
17
28
|
import { Layer, Icon, LoadingIndicator } from '../../utils/index.js';
|
|
@@ -22,30 +33,286 @@ Floating action buttons (FABs) help people take primary actions.
|
|
|
22
33
|
disabled,
|
|
23
34
|
formaction,
|
|
24
35
|
size = 'regular',
|
|
25
|
-
|
|
36
|
+
color = 'primary-container',
|
|
26
37
|
iconProps,
|
|
27
38
|
label,
|
|
28
39
|
loading,
|
|
29
|
-
expanded
|
|
40
|
+
expanded,
|
|
30
41
|
class: className,
|
|
31
42
|
withMenu,
|
|
43
|
+
surface,
|
|
44
|
+
surfaceOpen = $bindable(false),
|
|
32
45
|
onclick,
|
|
33
46
|
...restProps
|
|
34
47
|
}: FABProps = $props();
|
|
35
48
|
|
|
36
|
-
|
|
49
|
+
let menuOpen = $state(false);
|
|
50
|
+
const menuSet = $derived(fabMenuSet(color));
|
|
51
|
+
setContext('fabMenuSet', () => menuSet);
|
|
37
52
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
className,
|
|
44
|
-
'transition-[border-radius] md-sys-motion-fast-spatial',
|
|
45
|
-
withMenu && showMenu && '!rounded-full'
|
|
46
|
-
)
|
|
47
|
-
})
|
|
53
|
+
// M3: in a collapsed rail use a FAB, in an expanded one an extended FAB; elsewhere a FAB when
|
|
54
|
+
// space is limited (compact windows) and an extended FAB from medium windows up.
|
|
55
|
+
const rail = getContext<{ collapsed: boolean } | undefined>('rail');
|
|
56
|
+
const resolvedExpanded = $derived(
|
|
57
|
+
expanded ?? (rail ? !rail.collapsed : { small: false, medium: true })
|
|
48
58
|
);
|
|
59
|
+
const hasLabel = $derived(!!label && size !== 'small');
|
|
60
|
+
// Always a FAB: the menu keeps its footprint only then (an extended FAB's width is its label's).
|
|
61
|
+
const neverExpanded = $derived(
|
|
62
|
+
!hasLabel ||
|
|
63
|
+
(typeof resolvedExpanded === 'object'
|
|
64
|
+
? Object.values(resolvedExpanded).every((v) => !v)
|
|
65
|
+
: !resolvedExpanded)
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const open = $derived(!!withMenu && menuOpen);
|
|
69
|
+
const cls = $derived(fab({ size, color, menuOpen: open, menuSet }));
|
|
70
|
+
const expandedCls = $derived.by(() => {
|
|
71
|
+
const tiers: [Breakpoint, boolean][] =
|
|
72
|
+
typeof resolvedExpanded === 'object'
|
|
73
|
+
? (Object.entries(resolvedExpanded) as [Breakpoint, boolean][])
|
|
74
|
+
: [['small', resolvedExpanded]];
|
|
75
|
+
const entries = tiers.map(([bp, v]) => fabExpandedClasses[bp][v ? 'true' : 'false']);
|
|
76
|
+
return {
|
|
77
|
+
track: entries.map((e) => e.track).join(' '),
|
|
78
|
+
base: size === 'large' && !open ? entries.map((e) => e.large).join(' ') : ''
|
|
79
|
+
};
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// The close button shares the FAB's top trailing corner, so the FAB keeps its footprint while
|
|
83
|
+
// it shrinks into it.
|
|
84
|
+
const footprint: Record<NonNullable<FABProps['size']>, string> = {
|
|
85
|
+
small: 'size-spacing-500',
|
|
86
|
+
regular: 'size-spacing-700',
|
|
87
|
+
medium: 'size-20',
|
|
88
|
+
large: 'size-24'
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const reducedMotion = () =>
|
|
92
|
+
typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
93
|
+
const spatial = springTransition(springTokens.spatial);
|
|
94
|
+
const colorAndShape = { ...spatial, backgroundColor: springTransition(springTokens.effects) };
|
|
95
|
+
|
|
96
|
+
let fabEl = $state<HTMLElement | null>(null);
|
|
97
|
+
|
|
98
|
+
/*
|
|
99
|
+
* FAB menu: the FAB springs into the 56dp round close button (M3: always 56dp, whatever the
|
|
100
|
+
* FAB's size) and back. Measured before and after the DOM update, then sprung between the two
|
|
101
|
+
* in px, so the corners stay a real radius instead of racing a 9999px `rounded-full`.
|
|
102
|
+
*/
|
|
103
|
+
const CLOSE_BUTTON = { w: 56, h: 56, r: 28 };
|
|
104
|
+
type Box = { w: number; h: number; r: number };
|
|
105
|
+
const measure = (el: HTMLElement): Box => ({
|
|
106
|
+
w: el.offsetWidth,
|
|
107
|
+
h: el.offsetHeight,
|
|
108
|
+
r: parseFloat(getComputedStyle(el).borderTopLeftRadius)
|
|
109
|
+
});
|
|
110
|
+
const clearBox = (el: HTMLElement) => {
|
|
111
|
+
el.style.width = '';
|
|
112
|
+
el.style.height = '';
|
|
113
|
+
el.style.borderRadius = '';
|
|
114
|
+
};
|
|
115
|
+
let menuMorph: AnimationPlaybackControls | undefined;
|
|
116
|
+
let beforeMenu: Box | null = null;
|
|
117
|
+
$effect.pre(() => {
|
|
118
|
+
void open;
|
|
119
|
+
untrack(() => (beforeMenu = fabEl ? measure(fabEl) : null));
|
|
120
|
+
});
|
|
121
|
+
$effect(() => {
|
|
122
|
+
const isOpen = open;
|
|
123
|
+
untrack(() => {
|
|
124
|
+
const el = fabEl;
|
|
125
|
+
const from = beforeMenu;
|
|
126
|
+
beforeMenu = null;
|
|
127
|
+
if (!el || !from || !withMenu) return;
|
|
128
|
+
menuMorph?.stop();
|
|
129
|
+
clearBox(el);
|
|
130
|
+
const to = isOpen ? CLOSE_BUTTON : measure(el);
|
|
131
|
+
const keyframes = {
|
|
132
|
+
width: [`${from.w}px`, `${to.w}px`],
|
|
133
|
+
height: [`${from.h}px`, `${to.h}px`],
|
|
134
|
+
borderRadius: [`${from.r}px`, `${to.r}px`]
|
|
135
|
+
};
|
|
136
|
+
if (reducedMotion()) {
|
|
137
|
+
if (isOpen)
|
|
138
|
+
Object.assign(el.style, { width: '56px', height: '56px', borderRadius: '28px' });
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const controls = animate(el, keyframes, spatial);
|
|
142
|
+
menuMorph = controls;
|
|
143
|
+
// Closed: hand size and shape back to the classes once the spring settles.
|
|
144
|
+
if (!isOpen) controls.then(() => !open && clearBox(el));
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
/*
|
|
149
|
+
* FAB ↔ extended FAB. The responsive classes decide which one shows; when that flips (the
|
|
150
|
+
* prop, the rail, or the window crossing a tier), the FAB's width and padding spring from what
|
|
151
|
+
* was on screen to the new layout, with the label kept laid out and clipped by the moving edge.
|
|
152
|
+
*/
|
|
153
|
+
const tierOrder: Breakpoint[] = ['small', 'medium', 'large', 'extraLarge'];
|
|
154
|
+
let tier = $state<Breakpoint>('small');
|
|
155
|
+
$effect(() => {
|
|
156
|
+
const root = getComputedStyle(document.documentElement);
|
|
157
|
+
const min = (name: string, fallback: string) =>
|
|
158
|
+
`(min-width: ${root.getPropertyValue(`--breakpoint-${name}`).trim() || fallback})`;
|
|
159
|
+
const queries: [Breakpoint, MediaQueryList][] = [
|
|
160
|
+
['medium', matchMedia(min('md', '48rem'))],
|
|
161
|
+
['large', matchMedia(min('lg', '64rem'))],
|
|
162
|
+
['extraLarge', matchMedia(min('xl', '80rem'))]
|
|
163
|
+
];
|
|
164
|
+
const update = () =>
|
|
165
|
+
(tier = queries.reduce<Breakpoint>((t, [bp, q]) => (q.matches ? bp : t), 'small'));
|
|
166
|
+
update();
|
|
167
|
+
queries.forEach(([, q]) => q.addEventListener('change', update));
|
|
168
|
+
return () => queries.forEach(([, q]) => q.removeEventListener('change', update));
|
|
169
|
+
});
|
|
170
|
+
// Missing tiers inherit upward, as in the classes.
|
|
171
|
+
const expandedNow = $derived.by(() => {
|
|
172
|
+
if (!hasLabel) return false;
|
|
173
|
+
if (typeof resolvedExpanded !== 'object') return resolvedExpanded;
|
|
174
|
+
let value = false;
|
|
175
|
+
for (const bp of tierOrder.slice(0, tierOrder.indexOf(tier) + 1))
|
|
176
|
+
value = resolvedExpanded[bp] ?? value;
|
|
177
|
+
return value;
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
let trackEl = $state<HTMLElement | null>(null);
|
|
181
|
+
// What was last on screen, kept by a ResizeObserver so a flip can start from it.
|
|
182
|
+
let shown = { width: 0, padding: 0 };
|
|
183
|
+
$effect(() => {
|
|
184
|
+
const el = fabEl;
|
|
185
|
+
if (!el) return;
|
|
186
|
+
const observer = new ResizeObserver(() => {
|
|
187
|
+
shown = { width: el.offsetWidth, padding: parseFloat(getComputedStyle(el).paddingLeft) };
|
|
188
|
+
});
|
|
189
|
+
observer.observe(el);
|
|
190
|
+
return () => observer.disconnect();
|
|
191
|
+
});
|
|
192
|
+
let labelMorph: AnimationPlaybackControls | undefined;
|
|
193
|
+
let labelMorphStarted = false;
|
|
194
|
+
$effect(() => {
|
|
195
|
+
void expandedNow;
|
|
196
|
+
untrack(() => {
|
|
197
|
+
const el = fabEl;
|
|
198
|
+
const track = trackEl;
|
|
199
|
+
if (!labelMorphStarted) {
|
|
200
|
+
labelMorphStarted = true;
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (!el || !track || open || !shown.width) return;
|
|
204
|
+
labelMorph?.stop();
|
|
205
|
+
// The new layout, with nothing inline.
|
|
206
|
+
el.style.width = el.style.paddingInline = el.style.overflow = '';
|
|
207
|
+
track.style.gridTemplateColumns = '';
|
|
208
|
+
const to = { width: el.offsetWidth, padding: parseFloat(getComputedStyle(el).paddingLeft) };
|
|
209
|
+
if (reducedMotion() || (to.width === shown.width && to.padding === shown.padding)) return;
|
|
210
|
+
Object.assign(el.style, { overflow: 'hidden', width: `${shown.width}px` });
|
|
211
|
+
track.style.gridTemplateColumns = '1fr';
|
|
212
|
+
const controls = animate(
|
|
213
|
+
el,
|
|
214
|
+
{
|
|
215
|
+
width: [`${shown.width}px`, `${to.width}px`],
|
|
216
|
+
paddingInline: [`${shown.padding}px`, `${to.padding}px`]
|
|
217
|
+
},
|
|
218
|
+
spatial
|
|
219
|
+
);
|
|
220
|
+
labelMorph = controls;
|
|
221
|
+
controls.then(() => {
|
|
222
|
+
if (labelMorph !== controls) return;
|
|
223
|
+
el.style.width = el.style.paddingInline = el.style.overflow = '';
|
|
224
|
+
track.style.gridTemplateColumns = '';
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
/*
|
|
230
|
+
* FAB → surface, M3's container transform with a persistent container: the FAB's container
|
|
231
|
+
* grows from its corner into the surface (a clip-path), turning from the FAB's colour into the
|
|
232
|
+
* surface's, and the content fades in once it has room. Closing plays it backwards and hands
|
|
233
|
+
* focus back to the FAB, which stays in the layout, invisible, while the surface is open.
|
|
234
|
+
*/
|
|
235
|
+
const surfaceCls = fabSurface();
|
|
236
|
+
const SURFACE_RADIUS = 24;
|
|
237
|
+
let surfaceEl: HTMLElement | null = null;
|
|
238
|
+
let closing = false;
|
|
239
|
+
const fabClip = (surfaceBox: DOMRect) => {
|
|
240
|
+
const f = fabEl!.getBoundingClientRect();
|
|
241
|
+
const r = parseFloat(getComputedStyle(fabEl!).borderTopLeftRadius);
|
|
242
|
+
return `inset(${f.top - surfaceBox.top}px ${surfaceBox.right - f.right}px ${surfaceBox.bottom - f.bottom}px ${f.left - surfaceBox.left}px round ${r}px)`;
|
|
243
|
+
};
|
|
244
|
+
const focusFirst = (node: HTMLElement) => {
|
|
245
|
+
const target = node.querySelector<HTMLElement>(
|
|
246
|
+
'a[href], button:not([disabled]), input:not([disabled]), select, textarea, [tabindex]:not([tabindex="-1"])'
|
|
247
|
+
);
|
|
248
|
+
(target ?? node).focus();
|
|
249
|
+
};
|
|
250
|
+
// The content grows out of the FAB with the container: scaled from the FAB's centre.
|
|
251
|
+
const CONTENT_START_SCALE = 0.85;
|
|
252
|
+
const fromFabCentre = (content: HTMLElement) => {
|
|
253
|
+
const f = fabEl!.getBoundingClientRect();
|
|
254
|
+
const c = content.getBoundingClientRect();
|
|
255
|
+
content.style.transformOrigin = `${f.left + f.width / 2 - c.left}px ${f.top + f.height / 2 - c.top}px`;
|
|
256
|
+
};
|
|
257
|
+
const growFromFab: Attachment<HTMLElement> = (node) => {
|
|
258
|
+
surfaceEl = node;
|
|
259
|
+
const content = node.querySelector<HTMLElement>('[data-fab-surface-content]');
|
|
260
|
+
if (fabEl && !reducedMotion()) {
|
|
261
|
+
animate(
|
|
262
|
+
node,
|
|
263
|
+
{
|
|
264
|
+
clipPath: [
|
|
265
|
+
fabClip(node.getBoundingClientRect()),
|
|
266
|
+
`inset(0px 0px 0px 0px round ${SURFACE_RADIUS}px)`
|
|
267
|
+
],
|
|
268
|
+
backgroundColor: [
|
|
269
|
+
getComputedStyle(fabEl).backgroundColor,
|
|
270
|
+
getComputedStyle(node).backgroundColor
|
|
271
|
+
]
|
|
272
|
+
},
|
|
273
|
+
colorAndShape
|
|
274
|
+
);
|
|
275
|
+
if (content) {
|
|
276
|
+
fromFabCentre(content);
|
|
277
|
+
animate(
|
|
278
|
+
content,
|
|
279
|
+
{ scale: [CONTENT_START_SCALE, 1], opacity: [0, 1] },
|
|
280
|
+
{ ...spatial, opacity: { ...springTransition(springTokens.effects), delay: 0.06 } }
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
focusFirst(node);
|
|
285
|
+
return () => (surfaceEl = null);
|
|
286
|
+
};
|
|
287
|
+
const closeSurface = async () => {
|
|
288
|
+
if (!surfaceOpen || closing) return;
|
|
289
|
+
closing = true;
|
|
290
|
+
const node = surfaceEl;
|
|
291
|
+
if (node && fabEl && !reducedMotion()) {
|
|
292
|
+
const content = node.querySelector<HTMLElement>('[data-fab-surface-content]');
|
|
293
|
+
if (content) fromFabCentre(content);
|
|
294
|
+
await Promise.all([
|
|
295
|
+
content &&
|
|
296
|
+
animate(
|
|
297
|
+
content,
|
|
298
|
+
{ scale: CONTENT_START_SCALE, opacity: 0 },
|
|
299
|
+
{ ...spatial, opacity: springTransition(springTokens.fastEffects) }
|
|
300
|
+
),
|
|
301
|
+
animate(
|
|
302
|
+
node,
|
|
303
|
+
{
|
|
304
|
+
clipPath: fabClip(node.getBoundingClientRect()),
|
|
305
|
+
backgroundColor: getComputedStyle(fabEl).backgroundColor
|
|
306
|
+
},
|
|
307
|
+
colorAndShape
|
|
308
|
+
)
|
|
309
|
+
]);
|
|
310
|
+
}
|
|
311
|
+
surfaceOpen = false;
|
|
312
|
+
closing = false;
|
|
313
|
+
await tick();
|
|
314
|
+
fabEl?.focus();
|
|
315
|
+
};
|
|
49
316
|
|
|
50
317
|
function handleClick(e: MouseEvent, triggerClick?: unknown) {
|
|
51
318
|
if (disabled || loading) {
|
|
@@ -54,6 +321,7 @@ Floating action buttons (FABs) help people take primary actions.
|
|
|
54
321
|
}
|
|
55
322
|
(onclick as ((e: MouseEvent) => void) | null | undefined)?.(e);
|
|
56
323
|
if (typeof triggerClick === 'function') triggerClick(e);
|
|
324
|
+
if (surface && !e.defaultPrevented) surfaceOpen = true;
|
|
57
325
|
}
|
|
58
326
|
</script>
|
|
59
327
|
|
|
@@ -61,37 +329,93 @@ Floating action buttons (FABs) help people take primary actions.
|
|
|
61
329
|
<Button.Root
|
|
62
330
|
{disabled}
|
|
63
331
|
{formaction}
|
|
332
|
+
aria-label={label}
|
|
333
|
+
aria-busy={loading || undefined}
|
|
64
334
|
data-cy="m3-fab"
|
|
335
|
+
aria-haspopup={surface ? 'dialog' : undefined}
|
|
336
|
+
aria-expanded={surface ? surfaceOpen : undefined}
|
|
337
|
+
bind:ref={fabEl}
|
|
65
338
|
{...restProps as ButtonRootProps}
|
|
66
339
|
{...triggerProps}
|
|
67
|
-
class={
|
|
340
|
+
class={cls.base({
|
|
341
|
+
class: clsx(
|
|
342
|
+
expandedCls.base,
|
|
343
|
+
surfaceOpen && 'invisible',
|
|
344
|
+
// In its footprint, pinned to the top trailing corner the close button shares.
|
|
345
|
+
withMenu &&
|
|
346
|
+
neverExpanded && ['col-start-1 row-start-1 self-start justify-self-end', footprint[size]],
|
|
347
|
+
className
|
|
348
|
+
)
|
|
349
|
+
})}
|
|
68
350
|
onclick={(e: MouseEvent) => handleClick(e, triggerProps.onclick)}
|
|
69
351
|
>
|
|
70
352
|
{#if loading}
|
|
71
|
-
<LoadingIndicator />
|
|
353
|
+
<LoadingIndicator center={false} class={cls.icon({ class: 'text-current' })} />
|
|
72
354
|
{:else}
|
|
73
|
-
<Icon
|
|
74
|
-
|
|
355
|
+
<Icon
|
|
356
|
+
{...open ? { name: 'close' } : iconProps}
|
|
357
|
+
class={cls.icon({ class: clsx(!open && iconProps.class) })}
|
|
358
|
+
/>
|
|
75
359
|
{/if}
|
|
76
|
-
{#if
|
|
77
|
-
<
|
|
78
|
-
{
|
|
79
|
-
|
|
360
|
+
{#if hasLabel}
|
|
361
|
+
<span
|
|
362
|
+
bind:this={trackEl}
|
|
363
|
+
class={cls.labelTrack({ class: expandedCls.track })}
|
|
364
|
+
aria-hidden="true"
|
|
365
|
+
>
|
|
366
|
+
<span class={cls.labelClip()}>
|
|
367
|
+
<span class={cls.label()}>{label}</span>
|
|
368
|
+
</span>
|
|
369
|
+
</span>
|
|
80
370
|
{/if}
|
|
371
|
+
<Layer />
|
|
81
372
|
</Button.Root>
|
|
82
373
|
{/snippet}
|
|
83
374
|
|
|
84
375
|
{#if withMenu}
|
|
85
|
-
<DropdownMenu.Root bind:open={
|
|
376
|
+
<DropdownMenu.Root bind:open={menuOpen}>
|
|
86
377
|
<DropdownMenu.Trigger disabled={disabled || loading}>
|
|
87
378
|
{#snippet child({ props })}
|
|
88
|
-
{
|
|
379
|
+
{#if !neverExpanded}
|
|
380
|
+
{@render fabButton(props)}
|
|
381
|
+
{:else}
|
|
382
|
+
<span class="inline-grid">
|
|
383
|
+
<span class="invisible col-start-1 row-start-1 {footprint[size]}"></span>
|
|
384
|
+
{@render fabButton(props)}
|
|
385
|
+
</span>
|
|
386
|
+
{/if}
|
|
89
387
|
{/snippet}
|
|
90
388
|
</DropdownMenu.Trigger>
|
|
91
389
|
<FABMenu>
|
|
92
390
|
{@render children?.()}
|
|
93
391
|
</FABMenu>
|
|
94
392
|
</DropdownMenu.Root>
|
|
393
|
+
{:else if surface}
|
|
394
|
+
<span class={surfaceCls.anchor()}>
|
|
395
|
+
{@render fabButton({})}
|
|
396
|
+
{#if surfaceOpen}
|
|
397
|
+
<div class={surfaceCls.wrapper()}>
|
|
398
|
+
<div
|
|
399
|
+
role="dialog"
|
|
400
|
+
aria-label={label ?? restProps['aria-label']}
|
|
401
|
+
tabindex="-1"
|
|
402
|
+
class={surfaceCls.surface()}
|
|
403
|
+
onkeydown={(e) => {
|
|
404
|
+
if (e.key === 'Escape') {
|
|
405
|
+
e.stopPropagation();
|
|
406
|
+
closeSurface();
|
|
407
|
+
}
|
|
408
|
+
}}
|
|
409
|
+
{@attach growFromFab}
|
|
410
|
+
{@attach clickOutside(closeSurface)}
|
|
411
|
+
>
|
|
412
|
+
<div data-fab-surface-content>
|
|
413
|
+
{@render surface({ close: closeSurface })}
|
|
414
|
+
</div>
|
|
415
|
+
</div>
|
|
416
|
+
</div>
|
|
417
|
+
{/if}
|
|
418
|
+
</span>
|
|
95
419
|
{:else}
|
|
96
420
|
{@render fabButton({})}
|
|
97
421
|
{/if}
|
|
@@ -2,13 +2,18 @@ import type { FABProps } from './types.js';
|
|
|
2
2
|
/**
|
|
3
3
|
* Floating action buttons (FABs) help people take primary actions.
|
|
4
4
|
*
|
|
5
|
-
* -
|
|
6
|
-
* -
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* - Sizes: regular (56dp, default), medium (80dp), large (96dp); small (40dp) is baseline only
|
|
6
|
+
* - `expanded` + `label`: the extended FAB, whose label slides open beside the icon. Responsive:
|
|
7
|
+
* a FAB on compact windows and an extended FAB from medium up by default, and inside a `Rail`
|
|
8
|
+
* it follows the rail's collapsed state
|
|
9
|
+
* - `withMenu`: opens a FAB menu of `FABMenuItem`s; the FAB becomes its 56dp close button
|
|
10
|
+
* - `surface`: opens into a surface with a container transform (FAB → sheet); the FAB's
|
|
11
|
+
* container grows into it and changes colour on the way
|
|
9
12
|
*
|
|
10
|
-
* @see https://m3.material.io/components/floating-action-button/
|
|
13
|
+
* @see https://m3.material.io/components/floating-action-button/specs
|
|
14
|
+
* @see https://m3.material.io/components/extended-fab/specs
|
|
15
|
+
* @see https://m3.material.io/components/fab-menu/specs
|
|
11
16
|
*/
|
|
12
|
-
declare const FAB: import("svelte").Component<FABProps, {}, "">;
|
|
17
|
+
declare const FAB: import("svelte").Component<FABProps, {}, "surfaceOpen">;
|
|
13
18
|
type FAB = ReturnType<typeof FAB>;
|
|
14
19
|
export default FAB;
|
|
@@ -1,33 +1,52 @@
|
|
|
1
1
|
<!--
|
|
2
2
|
@component
|
|
3
|
-
The
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
The item stack of a FAB menu, rendered by `FAB` when `withMenu` is set. It lives inside the
|
|
4
|
+
FAB's `DropdownMenu.Root`, which owns open state, positioning, and outside-click/Escape
|
|
5
|
+
dismissal. Items are end-aligned, 4dp apart and 8dp above the close button. Use up to six
|
|
6
|
+
`FABMenuItem`s.
|
|
7
|
+
|
|
8
|
+
Motion: the items grow out of the close button one after another, nearest first, on the fast
|
|
9
|
+
spatial spring; the stack fades out on close.
|
|
6
10
|
-->
|
|
7
11
|
<script lang="ts">
|
|
8
12
|
import clsx from 'clsx';
|
|
9
13
|
import { DropdownMenu } from 'bits-ui';
|
|
14
|
+
import { animate, stagger } from 'motion';
|
|
15
|
+
import type { Attachment } from 'svelte/attachments';
|
|
10
16
|
import { fabMenu } from './theme.js';
|
|
11
17
|
import type { FABMenuProps } from './types.js';
|
|
12
18
|
import { enterExit, presence } from '../../animation/index.js';
|
|
19
|
+
import { springTokens, springTransition } from '../../animation/spring.js';
|
|
13
20
|
|
|
14
21
|
let { children, class: className, ...restProps }: FABMenuProps = $props();
|
|
15
22
|
|
|
16
23
|
const { base } = fabMenu();
|
|
24
|
+
|
|
25
|
+
const STAGGER_S = 0.03;
|
|
26
|
+
const itemsIn: Attachment<HTMLElement> = (node) => {
|
|
27
|
+
if (matchMedia('(prefers-reduced-motion: reduce)').matches) return;
|
|
28
|
+
const items = [...node.children] as HTMLElement[];
|
|
29
|
+
for (const item of items) item.style.transformOrigin = 'right center';
|
|
30
|
+
const controls = animate(
|
|
31
|
+
items,
|
|
32
|
+
{ y: [8, 0], scale: [0.6, 1] },
|
|
33
|
+
{ ...springTransition(springTokens.fastSpatial), delay: stagger(STAGGER_S, { from: 'last' }) }
|
|
34
|
+
);
|
|
35
|
+
return () => controls.stop();
|
|
36
|
+
};
|
|
17
37
|
</script>
|
|
18
38
|
|
|
19
39
|
<DropdownMenu.Portal>
|
|
20
|
-
<DropdownMenu.Content side="top" align="end" sideOffset={
|
|
40
|
+
<DropdownMenu.Content side="top" align="end" sideOffset={3} collisionPadding={8}>
|
|
21
41
|
{#snippet child({ wrapperProps, props, open })}
|
|
22
42
|
<div {...wrapperProps}>
|
|
23
43
|
<div
|
|
24
44
|
data-cy="m3-fab-menu"
|
|
25
45
|
{...restProps}
|
|
26
46
|
{...props}
|
|
27
|
-
class={base({
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
{@attach presence(() => open, enterExit.scale)}
|
|
47
|
+
class={base({ class: clsx(className) })}
|
|
48
|
+
{@attach presence(() => open, enterExit.fade)}
|
|
49
|
+
{@attach itemsIn}
|
|
31
50
|
>
|
|
32
51
|
{@render children()}
|
|
33
52
|
</div>
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import type { FABMenuProps } from './types.js';
|
|
2
2
|
/**
|
|
3
|
-
* The
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* The item stack of a FAB menu, rendered by `FAB` when `withMenu` is set. It lives inside the
|
|
4
|
+
* FAB's `DropdownMenu.Root`, which owns open state, positioning, and outside-click/Escape
|
|
5
|
+
* dismissal. Items are end-aligned, 4dp apart and 8dp above the close button. Use up to six
|
|
6
|
+
* `FABMenuItem`s.
|
|
7
|
+
*
|
|
8
|
+
* Motion: the items grow out of the close button one after another, nearest first, on the fast
|
|
9
|
+
* spatial spring; the stack fades out on close.
|
|
6
10
|
*/
|
|
7
11
|
declare const FABMenu: import("svelte").Component<FABMenuProps, {}, "">;
|
|
8
12
|
type FABMenu = ReturnType<typeof FABMenu>;
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
<!--
|
|
2
2
|
@component
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
inside a `FAB` with `withMenu`.
|
|
3
|
+
One action in a FAB menu: a 56dp pill in the FAB's colour set (primary, secondary or tertiary
|
|
4
|
+
container). Wraps bits-ui's `DropdownMenu.Item`, so it gets the `menuitem` role, arrow-key
|
|
5
|
+
navigation, and closes the menu when chosen. Render it inside a `FAB` with `withMenu`.
|
|
6
6
|
-->
|
|
7
7
|
<script lang="ts">
|
|
8
8
|
import clsx from 'clsx';
|
|
9
|
+
import { getContext } from 'svelte';
|
|
9
10
|
import { fabMenuItem } from './theme.js';
|
|
10
11
|
import type { FABMenuItemProps } from './types.js';
|
|
11
12
|
import { Icon, LoadingIndicator, Layer } from '../../utils/index.js';
|
|
@@ -13,7 +14,6 @@ inside a `FAB` with `withMenu`.
|
|
|
13
14
|
|
|
14
15
|
let {
|
|
15
16
|
class: className,
|
|
16
|
-
variant = 'secondary',
|
|
17
17
|
loading,
|
|
18
18
|
children,
|
|
19
19
|
iconProps,
|
|
@@ -22,24 +22,28 @@ inside a `FAB` with `withMenu`.
|
|
|
22
22
|
...restProps
|
|
23
23
|
}: FABMenuItemProps = $props();
|
|
24
24
|
|
|
25
|
-
const
|
|
26
|
-
|
|
25
|
+
const menuSet = getContext<(() => 'primary' | 'secondary' | 'tertiary') | undefined>(
|
|
26
|
+
'fabMenuSet'
|
|
27
|
+
);
|
|
28
|
+
const cls = $derived(fabMenuItem({ set: menuSet?.() ?? 'primary' }));
|
|
27
29
|
</script>
|
|
28
30
|
|
|
29
31
|
<!-- Spread onto the Item, not the button: bits-ui merges (and chains handlers of) these into `props`. -->
|
|
30
32
|
<DropdownMenu.Item {disabled} {...restProps as DropdownMenuItemProps}>
|
|
31
33
|
{#snippet child({ props })}
|
|
32
|
-
<Button.Root
|
|
33
|
-
{
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
{
|
|
40
|
-
<LoadingIndicator />
|
|
34
|
+
<Button.Root
|
|
35
|
+
{formaction}
|
|
36
|
+
{disabled}
|
|
37
|
+
data-cy="m3-fab-menu-item"
|
|
38
|
+
{...props}
|
|
39
|
+
class={cls.base({ class: clsx(className) })}
|
|
40
|
+
>
|
|
41
|
+
{#if loading}
|
|
42
|
+
<LoadingIndicator center={false} class={cls.icon({ class: 'text-current' })} />
|
|
43
|
+
{:else}
|
|
44
|
+
<Icon {...iconProps} class={cls.icon({ class: clsx(iconProps.class) })} />
|
|
41
45
|
{/if}
|
|
42
|
-
{@render children
|
|
46
|
+
{@render children()}
|
|
43
47
|
<Layer />
|
|
44
48
|
</Button.Root>
|
|
45
49
|
{/snippet}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { FABMenuItemProps } from './types.js';
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* inside a `FAB` with `withMenu`.
|
|
3
|
+
* One action in a FAB menu: a 56dp pill in the FAB's colour set (primary, secondary or tertiary
|
|
4
|
+
* container). Wraps bits-ui's `DropdownMenu.Item`, so it gets the `menuitem` role, arrow-key
|
|
5
|
+
* navigation, and closes the menu when chosen. Render it inside a `FAB` with `withMenu`.
|
|
6
6
|
*/
|
|
7
7
|
declare const FABMenuItem: import("svelte").Component<FABMenuItemProps, {}, "">;
|
|
8
8
|
type FABMenuItem = ReturnType<typeof FABMenuItem>;
|