@bearmenu/ui 0.8.12 → 0.8.14
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/dist/{chunk-66MAPLXS.js → chunk-2ADCZXZJ.js} +1 -1
- package/dist/{chunk-2DEIXTBN.js → chunk-2ZXIXQIU.js} +1 -1
- package/dist/{chunk-YOKHBAIH.js → chunk-4OF4T4AI.js} +12 -11
- package/dist/{chunk-MZL7CGCL.js → chunk-4XDJXMLF.js} +2 -2
- package/dist/{chunk-YYGB4G4Q.js → chunk-CZK2MRS2.js} +1 -1
- package/dist/chunk-DJD3EMPK.js +126 -0
- package/dist/chunk-EZG2NELX.js +5 -0
- package/dist/{chunk-VAXOQS7W.js → chunk-F76NCVVR.js} +1 -1
- package/dist/{chunk-VG3PND3D.js → chunk-FMTJGO76.js} +1 -1
- package/dist/{chunk-UXWAOISG.js → chunk-FVSKGCK3.js} +2 -2
- package/dist/{chunk-2XCZJWTV.js → chunk-GNCWJWFE.js} +1 -1
- package/dist/{chunk-X3WMRKOW.js → chunk-HDR7V6IZ.js} +1 -1
- package/dist/{chunk-HSIYYA2E.js → chunk-HQU6QVAN.js} +5 -5
- package/dist/chunk-I5V446BX.js +27 -0
- package/dist/{chunk-WCTHZKU7.js → chunk-ICOU5QHT.js} +1 -1
- package/dist/{chunk-G7IFL4Z4.js → chunk-L3VEMP5A.js} +57 -9
- package/dist/chunk-LQWGZIYA.js +5 -0
- package/dist/{chunk-XNA2INVQ.js → chunk-MCOHLWRS.js} +2 -2
- package/dist/{chunk-3GWWZKX7.js → chunk-MMUBH76A.js} +21 -1
- package/dist/{chunk-ZDAHKI3A.js → chunk-MWB7S54O.js} +1 -1
- package/dist/{chunk-D5QHEHXJ.js → chunk-NMEVJT5Q.js} +1 -1
- package/dist/{chunk-3TNPSN4D.js → chunk-NSNEANOQ.js} +2 -2
- package/dist/{chunk-WSRWFA6K.js → chunk-NWZGSLPU.js} +1 -1
- package/dist/{chunk-LEAYBICI.js → chunk-OIV7B44M.js} +1 -1
- package/dist/{chunk-2CXU5WZO.js → chunk-P43B3ONY.js} +1 -1
- package/dist/{chunk-FRPPIXMY.js → chunk-PDTPO6NG.js} +1 -1
- package/dist/{chunk-6XMYEZVR.js → chunk-PX6RICS5.js} +1 -1
- package/dist/{chunk-6TEEY5YA.js → chunk-S33K4RC5.js} +1 -1
- package/dist/{chunk-3VMDCCCX.js → chunk-SC2VUTUI.js} +1 -1
- package/dist/{chunk-24RFI4RQ.js → chunk-SXQZ7DLH.js} +4 -4
- package/dist/{chunk-UDTG3RSE.js → chunk-T6S2ZBV6.js} +2 -2
- package/dist/{chunk-DTXNDLSE.js → chunk-WYTJR2YU.js} +16 -1
- package/dist/{chunk-QXHMPJ35.js → chunk-XBBEX4SF.js} +1 -1
- package/dist/chunk-XCB3F6SF.js +16 -0
- package/dist/{chunk-7BWTYWM3.js → chunk-ZLMRVBD7.js} +1 -1
- package/dist/{chunk-7A42J73Q.js → chunk-ZLPWEHZO.js} +1 -1
- package/dist/components/accordion.js +1 -1
- package/dist/components/alert-dialog.js +2 -2
- package/dist/components/alert.d.ts +2 -2
- package/dist/components/alert.js +2 -2
- package/dist/components/aspect-ratio.js +2 -2
- package/dist/components/avatar.js +1 -1
- package/dist/components/badge.d.ts +1 -1
- package/dist/components/badge.js +2 -2
- package/dist/components/breadcrumb.js +1 -1
- package/dist/components/button-group.js +1 -1
- package/dist/components/button.d.ts +1 -1
- package/dist/components/button.js +2 -2
- package/dist/components/calendar.js +2 -2
- package/dist/components/card.d.ts +2 -2
- package/dist/components/card.js +3 -3
- package/dist/components/carousel.js +2 -2
- package/dist/components/category-deck.js +2 -2
- package/dist/components/checkbox.js +2 -2
- package/dist/components/circular-progress.js +1 -1
- package/dist/components/collapsible.js +1 -1
- package/dist/components/combobox.js +5 -5
- package/dist/components/command.d.ts +15 -15
- package/dist/components/command.js +3 -3
- package/dist/components/concept-card.js +2 -2
- package/dist/components/concept-compare-rail.js +3 -3
- package/dist/components/concept-compare.d.ts +2 -1
- package/dist/components/concept-compare.js +5 -5
- package/dist/components/concept-convergence.js +4 -4
- package/dist/components/convergence.js +3 -3
- package/dist/components/coverflow.js +1 -1
- package/dist/components/currency-input.js +2 -2
- package/dist/components/date-input.d.ts +1 -1
- package/dist/components/date-input.js +2 -2
- package/dist/components/day-rail-chip.js +2 -2
- package/dist/components/description-list.js +1 -1
- package/dist/components/dialog.js +2 -2
- package/dist/components/doorway-tiles.d.ts +57 -0
- package/dist/components/doorway-tiles.js +86 -0
- package/dist/components/drawer.js +1 -1
- package/dist/components/drift-rail.js +1 -1
- package/dist/components/dropdown-menu.js +1 -1
- package/dist/components/empty-state.js +3 -3
- package/dist/components/event-card.js +4 -4
- package/dist/components/event-compact-row.js +4 -4
- package/dist/components/event-day-header.js +2 -2
- package/dist/components/event-day-separator.js +3 -3
- package/dist/components/event-detail-panel.js +4 -4
- package/dist/components/event-hub-hero.js +2 -2
- package/dist/components/event-index-row.js +4 -4
- package/dist/components/event-list-row-wide.js +5 -5
- package/dist/components/event-list-row.js +5 -5
- package/dist/components/event-poster-fallback.js +1 -1
- package/dist/components/event-types.js +1 -1
- package/dist/components/events-empty-state.js +1 -1
- package/dist/components/explainer-steps.js +1 -1
- package/dist/components/facet-group.js +2 -2
- package/dist/components/featured-rail-item.js +2 -2
- package/dist/components/field.js +2 -2
- package/dist/components/filter-category-row.js +3 -3
- package/dist/components/form.js +2 -2
- package/dist/components/glass-ground.js +2 -2
- package/dist/components/grade-atom.d.ts +3 -0
- package/dist/components/grade-atom.js +4 -0
- package/dist/components/haptic-button.js +2 -2
- package/dist/components/hour-band.js +4 -4
- package/dist/components/hour-group.js +4 -4
- package/dist/components/hub-surface.js +1 -1
- package/dist/components/hue-chip.js +1 -1
- package/dist/components/icon.js +1 -1
- package/dist/components/input-group.js +1 -1
- package/dist/components/input-otp.js +1 -1
- package/dist/components/input.js +2 -2
- package/dist/components/item.d.ts +2 -2
- package/dist/components/item.js +1 -1
- package/dist/components/kbd.js +1 -1
- package/dist/components/kitchen-dossier.d.ts +2 -1
- package/dist/components/kitchen-dossier.js +3 -3
- package/dist/components/label.js +2 -2
- package/dist/components/lazy-markdown.js +1 -1
- package/dist/components/link-button.js +2 -2
- package/dist/components/markdown-renderer.js +1 -1
- package/dist/components/menu-dead-end.d.ts +2 -1
- package/dist/components/menu-dead-end.js +1 -1
- package/dist/components/menu-family-block.d.ts +2 -1
- package/dist/components/menu-family-block.js +14 -8
- package/dist/components/menu-family-card.d.ts +2 -1
- package/dist/components/menu-family-card.js +1 -1
- package/dist/components/menu-field.js +1 -1
- package/dist/components/menu-filter-chips.d.ts +2 -1
- package/dist/components/menu-filter-chips.js +3 -3
- package/dist/components/menu-group-rows.d.ts +2 -1
- package/dist/components/menu-group-rows.js +2 -2
- package/dist/components/menu-hub-hero.js +1 -1
- package/dist/components/menu-index-header.d.ts +2 -1
- package/dist/components/menu-index-header.js +2 -2
- package/dist/components/menu-index-list.d.ts +9 -8
- package/dist/components/menu-index-list.js +4 -4
- package/dist/components/menu-intent-hero.d.ts +3 -2
- package/dist/components/menu-intent-hero.js +1 -1
- package/dist/components/menu-item.d.ts +2 -1
- package/dist/components/menu-item.js +4 -4
- package/dist/components/menu-kitchen-list.d.ts +2 -1
- package/dist/components/menu-kitchen-list.js +4 -4
- package/dist/components/menu-mocks.d.ts +16 -5
- package/dist/components/menu-mocks.js +23 -2
- package/dist/components/menu-types.d.ts +21 -34
- package/dist/components/motion-icon-actions.d.ts +27 -0
- package/dist/components/motion-icon-actions.js +336 -0
- package/dist/components/motion-icon-controls.js +3 -3
- package/dist/components/motion-icon-set.d.ts +13 -1
- package/dist/components/motion-icon-set.js +197 -6
- package/dist/components/motion-icon.d.ts +15 -1
- package/dist/components/motion-icon.js +2 -2
- package/dist/components/multi-select-combobox.js +7 -7
- package/dist/components/multi-select-menu.js +2 -2
- package/dist/components/native-select.js +1 -1
- package/dist/components/open-now-list.d.ts +2 -1
- package/dist/components/open-now-list.js +2 -2
- package/dist/components/pagination.js +2 -2
- package/dist/components/phone-frame.js +1 -1
- package/dist/components/photo-ground.js +1 -1
- package/dist/components/place-about-tab.d.ts +2 -1
- package/dist/components/place-about-tab.js +6 -6
- package/dist/components/place-card.d.ts +94 -12
- package/dist/components/place-card.js +203 -128
- package/dist/components/place-details-mobile.d.ts +2 -1
- package/dist/components/place-details-mobile.js +7 -7
- package/dist/components/place-schedule-week.d.ts +2 -1
- package/dist/components/place-schedule-week.js +3 -3
- package/dist/components/place-types.d.ts +3 -265
- package/dist/components/popover.js +2 -2
- package/dist/components/poster-stack.js +1 -1
- package/dist/components/poster-tape.js +1 -1
- package/dist/components/poster-wall.d.ts +22 -6
- package/dist/components/poster-wall.js +197 -12
- package/dist/components/progress.js +1 -1
- package/dist/components/qa-block.d.ts +2 -1
- package/dist/components/qa-block.js +1 -1
- package/dist/components/quiet-stretch.js +1 -1
- package/dist/components/radio-group.js +1 -1
- package/dist/components/rating.js +1 -1
- package/dist/components/scroll-area.js +1 -1
- package/dist/components/scroll-fade.js +2 -2
- package/dist/components/search-bar.js +1 -1
- package/dist/components/searchable-select.js +5 -5
- package/dist/components/section-header.js +2 -2
- package/dist/components/segmented-nav.d.ts +24 -4
- package/dist/components/segmented-nav.js +11 -2
- package/dist/components/select.js +1 -1
- package/dist/components/selection-bar.js +1 -1
- package/dist/components/separator.js +2 -2
- package/dist/components/sheet.js +1 -1
- package/dist/components/signature-chip.js +3 -3
- package/dist/components/skeleton-card.js +3 -3
- package/dist/components/skeleton.js +2 -2
- package/dist/components/slider.js +1 -1
- package/dist/components/sliding-panels.js +2 -2
- package/dist/components/snap-rail.d.ts +0 -5
- package/dist/components/snap-rail.js +2 -2
- package/dist/components/sonner.js +1 -1
- package/dist/components/sort-option-list.js +1 -1
- package/dist/components/spinner.d.ts +2 -2
- package/dist/components/spinner.js +1 -1
- package/dist/components/stat-banner.js +1 -1
- package/dist/components/stepper.js +1 -1
- package/dist/components/sticky-container.js +1 -1
- package/dist/components/story.js +1 -1
- package/dist/components/switch.js +1 -1
- package/dist/components/table.js +1 -1
- package/dist/components/tabs.js +2 -2
- package/dist/components/tag-group.js +2 -2
- package/dist/components/tag.d.ts +2 -2
- package/dist/components/tag.js +2 -2
- package/dist/components/text.d.ts +3 -3
- package/dist/components/text.js +2 -2
- package/dist/components/textarea.js +1 -1
- package/dist/components/timeline-band.js +4 -4
- package/dist/components/timeline-playhead.js +4 -4
- package/dist/components/toggle-group.js +2 -2
- package/dist/components/toggle.js +2 -2
- package/dist/components/tooltip.js +1 -1
- package/dist/components/top-comparison.d.ts +2 -1
- package/dist/components/top-comparison.js +1 -1
- package/dist/components/top-cross-link.d.ts +2 -1
- package/dist/components/top-cross-link.js +1 -1
- package/dist/components/top-ranking-entry.d.ts +2 -1
- package/dist/components/top-ranking-entry.js +6 -6
- package/dist/components/top-ranking-hero.d.ts +2 -1
- package/dist/components/top-ranking-hero.js +1 -1
- package/dist/components/top-ranking-legend.js +1 -1
- package/dist/components/top-types.d.ts +3 -1
- package/dist/components/use-mobile.js +1 -1
- package/dist/components/venue-events-block.js +5 -5
- package/dist/components/venue-grade-dimensions.d.ts +2 -1
- package/dist/components/venue-grade-dimensions.js +1 -1
- package/dist/components/venue-quotes.d.ts +2 -1
- package/dist/components/venue-quotes.js +1 -1
- package/dist/components/venue-wall.d.ts +2 -1
- package/dist/components/venue-wall.js +1 -1
- package/dist/components/weekday-theme-banner.js +1 -1
- package/dist/components/weekend-panel.js +4 -4
- package/dist/hooks/use-drag-swipe.js +1 -1
- package/dist/hooks/use-haptic.js +1 -1
- package/dist/hooks/use-media-query.js +1 -1
- package/dist/hooks/use-mobile.js +1 -1
- package/dist/hooks/use-mounted-theme.js +1 -1
- package/dist/hooks/use-sticky.js +1 -1
- package/dist/lib/card-hover.js +2 -18
- package/dist/lib/grade.d.ts +54 -0
- package/dist/lib/grade.js +2 -0
- package/dist/lib/icon-depth.d.ts +35 -0
- package/dist/lib/icon-depth.js +2 -0
- package/dist/lib/icon-registry.js +1 -1
- package/dist/lib/motion.js +2 -2
- package/dist/lib/overlay-pill.d.ts +30 -0
- package/dist/lib/overlay-pill.js +2 -0
- package/dist/lib/utils.js +1 -1
- package/dist/place-types-BHdeMMiG.d.ts +317 -0
- package/package.json +1 -1
- package/src/components/colour-dictionary.stories.tsx +268 -82
- package/src/components/doorway-tiles.stories.tsx +83 -0
- package/src/components/doorway-tiles.test.tsx +51 -0
- package/src/components/doorway-tiles.tsx +225 -0
- package/src/components/grade-atom.stories.tsx +81 -0
- package/src/components/grade-atom.tsx +256 -0
- package/src/components/hue-chip.stories.tsx +2 -2
- package/src/components/menu-family-block.stories.tsx +29 -0
- package/src/components/menu-family-block.test.tsx +51 -0
- package/src/components/menu-family-block.tsx +20 -4
- package/src/components/menu-index-list.tsx +7 -7
- package/src/components/menu-intent-hero.tsx +1 -1
- package/src/components/menu-kitchen-list.tsx +4 -1
- package/src/components/menu-mocks.ts +39 -3
- package/src/components/menu-types.ts +17 -33
- package/src/components/motion-icon-actions.test.tsx +115 -0
- package/src/components/motion-icon-actions.tsx +580 -0
- package/src/components/motion-icon-controls.test.tsx +1 -0
- package/src/components/motion-icon-controls.tsx +18 -13
- package/src/components/motion-icon-set.stories.tsx +62 -3
- package/src/components/motion-icon-set.tsx +349 -2
- package/src/components/motion-icon.test.tsx +180 -1
- package/src/components/motion-icon.tsx +97 -8
- package/src/components/place-card.stories.tsx +66 -76
- package/src/components/place-card.test.tsx +188 -100
- package/src/components/place-card.tsx +356 -148
- package/src/components/place-types.ts +17 -4
- package/src/components/poster-wall.stories.tsx +84 -0
- package/src/components/poster-wall.test.tsx +94 -3
- package/src/components/poster-wall.tsx +257 -9
- package/src/components/segmented-nav.stories.tsx +72 -1
- package/src/components/segmented-nav.test.tsx +96 -0
- package/src/components/segmented-nav.tsx +36 -4
- package/src/components/snap-rail.tsx +19 -0
- package/src/globals.css +26 -0
- package/src/lib/grade.test.ts +82 -0
- package/src/lib/grade.ts +76 -0
- package/src/lib/icon-depth.ts +35 -0
- package/src/lib/overlay-pill.ts +29 -0
- package/dist/components/menu-family-tiles.d.ts +0 -19
- package/dist/components/menu-family-tiles.js +0 -57
- package/src/components/menu-family-tiles.stories.tsx +0 -54
- package/src/components/menu-family-tiles.test.tsx +0 -30
- package/src/components/menu-family-tiles.tsx +0 -132
|
@@ -1,7 +1,35 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import type { Meta, StoryObj } from "@storybook/react";
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
MotionHouse,
|
|
6
|
+
MotionUtensils,
|
|
7
|
+
MotionMapPin,
|
|
8
|
+
MotionCalendar,
|
|
9
|
+
MotionLayoutGrid,
|
|
10
|
+
MotionWine,
|
|
11
|
+
MotionMap,
|
|
12
|
+
MotionFlame,
|
|
13
|
+
MotionCalendarClock,
|
|
14
|
+
MotionList,
|
|
15
|
+
} from "./motion-icon-set";
|
|
16
|
+
import {
|
|
17
|
+
MotionArrow,
|
|
18
|
+
MotionCaret,
|
|
19
|
+
MotionX,
|
|
20
|
+
MotionCircleX,
|
|
21
|
+
MotionExternalLink,
|
|
22
|
+
MotionSend,
|
|
23
|
+
MotionNavigation,
|
|
24
|
+
MotionLocate,
|
|
25
|
+
MotionMaximize,
|
|
26
|
+
MotionPhone,
|
|
27
|
+
MotionMail,
|
|
28
|
+
MotionFlag,
|
|
29
|
+
MotionLogIn,
|
|
30
|
+
MotionLogOut,
|
|
31
|
+
MotionSettings,
|
|
32
|
+
} from "./motion-icon-actions";
|
|
5
33
|
import {
|
|
6
34
|
MotionHeart,
|
|
7
35
|
MotionBookmark,
|
|
@@ -36,6 +64,35 @@ const OBJECTS: Glyph[] = [
|
|
|
36
64
|
{ name: "Utensils", Icon: MotionUtensils, gesture: "uncrosses into a place setting · holds" },
|
|
37
65
|
{ name: "MapPin", Icon: MotionMapPin, gesture: "jumps, fills on the descent · holds" },
|
|
38
66
|
{ name: "Calendar", Icon: MotionCalendar, gesture: "rolls four pages" },
|
|
67
|
+
{ name: "LayoutGrid", Icon: MotionLayoutGrid, gesture: "tiles shuffle one slot round the board" },
|
|
68
|
+
{ name: "Wine", Icon: MotionWine, gesture: "pours, sloshes past the line · holds" },
|
|
69
|
+
{ name: "Map", Icon: MotionMap, gesture: "folds shut, snaps open" },
|
|
70
|
+
{ name: "Flame", Icon: MotionFlame, gesture: "gathers, flares, fills at the peak" },
|
|
71
|
+
{ name: "CalendarClock", Icon: MotionCalendarClock, gesture: "hand whips a turn, a wedge sweeps · holds" },
|
|
72
|
+
{ name: "List", Icon: MotionList, gesture: "bottom row lifts to the top" },
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
const ArrowLeft = (p: MotionIconProps) => <MotionArrow direction="left" {...p} />;
|
|
76
|
+
const CaretLeft = (p: MotionIconProps) => <MotionCaret direction="left" {...p} />;
|
|
77
|
+
|
|
78
|
+
const ACTIONS: Glyph[] = [
|
|
79
|
+
{ name: "Arrow", Icon: MotionArrow, gesture: "head fires ahead, shaft chases" },
|
|
80
|
+
{ name: "Arrow ←", Icon: ArrowLeft, gesture: "the mirror, for back" },
|
|
81
|
+
{ name: "Caret", Icon: MotionCaret, gesture: "coils, darts folded, springs open" },
|
|
82
|
+
{ name: "Caret ←", Icon: CaretLeft, gesture: "the mirror, for carousel prev" },
|
|
83
|
+
{ name: "X", Icon: MotionX, gesture: "blades shut to a bar, reopen" },
|
|
84
|
+
{ name: "CircleX", Icon: MotionCircleX, gesture: "pressed, ejects its X, reloads" },
|
|
85
|
+
{ name: "ExternalLink", Icon: MotionExternalLink, gesture: "arrow leaves, the next one loads" },
|
|
86
|
+
{ name: "Send", Icon: MotionSend, gesture: "arrow fires out, stop block drops in · holds" },
|
|
87
|
+
{ name: "Navigation", Icon: MotionNavigation, gesture: "needle hunts for its bearing" },
|
|
88
|
+
{ name: "Locate", Icon: MotionLocate, gesture: "reticle skids off, locks on the dot" },
|
|
89
|
+
{ name: "Maximize", Icon: MotionMaximize, gesture: "corners shove apart, snap back" },
|
|
90
|
+
{ name: "Phone", Icon: MotionPhone, gesture: "lifted to the ear, dropped on the hook" },
|
|
91
|
+
{ name: "Mail", Icon: MotionMail, gesture: "flap flips open, slaps shut" },
|
|
92
|
+
{ name: "Flag", Icon: MotionFlag, gesture: "raised, planted, cloth whips" },
|
|
93
|
+
{ name: "LogIn", Icon: MotionLogIn, gesture: "arrow goes through the door" },
|
|
94
|
+
{ name: "LogOut", Icon: MotionLogOut, gesture: "arrow leaves, the next comes out" },
|
|
95
|
+
{ name: "Settings", Icon: MotionSettings, gesture: "gear turns two teeth, hub seats" },
|
|
39
96
|
];
|
|
40
97
|
|
|
41
98
|
const CONTROLS: Glyph[] = [
|
|
@@ -49,13 +106,13 @@ const CONTROLS: Glyph[] = [
|
|
|
49
106
|
{ name: "Theme", Icon: MotionTheme, gesture: "rays retract, crescent swings in · holds" },
|
|
50
107
|
{ name: "Chevron", Icon: MotionChevron, gesture: "turns over, tips past, settles · holds" },
|
|
51
108
|
{ name: "PlayPause", Icon: MotionPlayPause, gesture: "triangle folds into two bars · holds" },
|
|
52
|
-
{ name: "Eye", Icon: MotionEye, gesture: "
|
|
109
|
+
{ name: "Eye", Icon: MotionEye, gesture: "blinks, reopens onto the slash · holds" },
|
|
53
110
|
{ name: "Star", Icon: MotionStar, gesture: "pulls in, springs its points out · holds" },
|
|
54
111
|
{ name: "Copy", Icon: MotionCopy, gesture: "sheet peels off, check lands · holds" },
|
|
55
112
|
{ name: "Refresh", Icon: MotionRefresh, gesture: "one full turn, overshoots, settles" },
|
|
56
113
|
];
|
|
57
114
|
|
|
58
|
-
const ALL = [...OBJECTS, ...CONTROLS];
|
|
115
|
+
const ALL = [...OBJECTS, ...ACTIONS, ...CONTROLS];
|
|
59
116
|
|
|
60
117
|
const meta = {
|
|
61
118
|
title: "Motion/Animated icons",
|
|
@@ -160,6 +217,8 @@ export const Family: Story = {
|
|
|
160
217
|
|
|
161
218
|
/** The objects and the controls separately, when tuning one group at a time. */
|
|
162
219
|
export const Objects: Story = { render: () => <Gallery glyphs={OBJECTS} size={28} /> };
|
|
220
|
+
/** The action vocabulary (`lib/icon-registry`) — most of these render at 16px in a `Button`, so check them at that size too. */
|
|
221
|
+
export const Actions: Story = { render: () => <Gallery glyphs={ACTIONS} size={28} /> };
|
|
163
222
|
export const Controls: Story = { render: () => <Gallery glyphs={CONTROLS} size={28} /> };
|
|
164
223
|
|
|
165
224
|
/**
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
beats,
|
|
21
21
|
pivot,
|
|
22
22
|
returnTransition,
|
|
23
|
+
useSvgId,
|
|
23
24
|
type MotionIconProps,
|
|
24
25
|
} from "./motion-icon";
|
|
25
26
|
|
|
@@ -221,8 +222,7 @@ const reel: Variants = {
|
|
|
221
222
|
* still, which is what sells the contents moving.
|
|
222
223
|
*/
|
|
223
224
|
export function MotionCalendar(props: MotionIconProps) {
|
|
224
|
-
|
|
225
|
-
const clipId = `cal-body-${React.useId().replace(/:/g, "")}`;
|
|
225
|
+
const clipId = useSvgId("cal-body");
|
|
226
226
|
|
|
227
227
|
return (
|
|
228
228
|
<MotionIcon {...props}>
|
|
@@ -245,3 +245,350 @@ export function MotionCalendar(props: MotionIconProps) {
|
|
|
245
245
|
</MotionIcon>
|
|
246
246
|
);
|
|
247
247
|
}
|
|
248
|
+
|
|
249
|
+
// ─── Layout grid — the tiles shuffle one slot around the board (cycle content) ─
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Lucide's `layout-grid`: four 7-unit tiles on an 11-unit pitch. Each tile
|
|
253
|
+
* slides along the outer edge to the next corner clockwise, in a chase — the
|
|
254
|
+
* top-left tile leads and each following one starts a beat behind. Mid-flight
|
|
255
|
+
* the four sit on the edge midpoints, so the outline goes square → plus →
|
|
256
|
+
* square; that is the silhouette change. Nothing rotates: they slide, which is
|
|
257
|
+
* what makes it a shuffle rather than "turn the icon 90°".
|
|
258
|
+
*
|
|
259
|
+
* Travel is the full 11-unit pitch (46% of the box), above the 25–40% band.
|
|
260
|
+
* That is the geometry's own step — a half-slot shuffle ends on the edge
|
|
261
|
+
* midpoints, which is not a rest pose — and the band exists to guarantee
|
|
262
|
+
* visibility, not to cap it. Nothing leaves the box: the overshoot puts a
|
|
263
|
+
* tile's far edge at 22.5 + stroke.
|
|
264
|
+
*
|
|
265
|
+
* `play` ends with every tile one slot from its `rest` values although the
|
|
266
|
+
* picture is identical; `useIconGesture` snaps the values back once the
|
|
267
|
+
* gesture resolves, so leaving never slides them counter-clockwise.
|
|
268
|
+
*/
|
|
269
|
+
const GRID_TIMES = [0, 0.14, 0.6, 0.8, 1];
|
|
270
|
+
const GRID_PITCH = 11;
|
|
271
|
+
/** Per-tile delay in the chase, seconds. */
|
|
272
|
+
const GRID_CHASE = 0.03;
|
|
273
|
+
|
|
274
|
+
/** Anticipation against the travel, the edge, a 1.5 overshoot, a 0.4 rebound, the corner. */
|
|
275
|
+
const slide = (direction: 1 | -1) =>
|
|
276
|
+
[0, -1.5, GRID_PITCH + 1.5, GRID_PITCH - 0.4, GRID_PITCH].map((v) => v * direction);
|
|
277
|
+
|
|
278
|
+
const chase = (order: number) => ({ ...beats(GRID_TIMES, 0.58), delay: order * GRID_CHASE });
|
|
279
|
+
|
|
280
|
+
function tileAlongX(direction: 1 | -1, order: number): Variants {
|
|
281
|
+
return {
|
|
282
|
+
rest: { x: 0, transition: returnTransition },
|
|
283
|
+
active: { x: 0, transition: returnTransition },
|
|
284
|
+
play: { x: slide(direction), transition: chase(order) },
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function tileAlongY(direction: 1 | -1, order: number): Variants {
|
|
289
|
+
return {
|
|
290
|
+
rest: { y: 0, transition: returnTransition },
|
|
291
|
+
active: { y: 0, transition: returnTransition },
|
|
292
|
+
play: { y: slide(direction), transition: chase(order) },
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Clockwise from the top-left: TL → TR (x+), TR → BR (y+), BR → BL (x−), BL → TL (y−).
|
|
297
|
+
const tileTopLeft = tileAlongX(1, 0);
|
|
298
|
+
const tileTopRight = tileAlongY(1, 1);
|
|
299
|
+
const tileBottomRight = tileAlongX(-1, 2);
|
|
300
|
+
const tileBottomLeft = tileAlongY(-1, 3);
|
|
301
|
+
|
|
302
|
+
export function MotionLayoutGrid(props: MotionIconProps) {
|
|
303
|
+
return (
|
|
304
|
+
<MotionIcon {...props}>
|
|
305
|
+
<motion.rect variants={tileTopLeft} width="7" height="7" x="3" y="3" rx="1" />
|
|
306
|
+
<motion.rect variants={tileTopRight} width="7" height="7" x="14" y="3" rx="1" />
|
|
307
|
+
<motion.rect variants={tileBottomRight} width="7" height="7" x="14" y="14" rx="1" />
|
|
308
|
+
<motion.rect variants={tileBottomLeft} width="7" height="7" x="3" y="14" rx="1" />
|
|
309
|
+
</MotionIcon>
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// ─── Wine — pours, sloshes past the line, holds full (fall and land) ──────────
|
|
314
|
+
|
|
315
|
+
const WINE_TIMES = [0, 0.16, 0.6, 0.78, 1];
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* The bowl's bottom (empty), the line lucide already draws at the widest
|
|
319
|
+
* chord (`M7 10h10`, the "pour to here" mark), and the slosh two units above it.
|
|
320
|
+
*/
|
|
321
|
+
const WINE_EMPTY = 15;
|
|
322
|
+
const WINE_LINE = 10;
|
|
323
|
+
const WINE_SLOSH = 8;
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* The liquid is a filled rect under a clip of the bowl, so its top edge IS the
|
|
327
|
+
* level. `attrY` moves the attribute rather than a transform so the rect's
|
|
328
|
+
* geometry itself changes under the `scaleY` the whole glass carries — the
|
|
329
|
+
* level then scales with the glass instead of fighting it. And the level has
|
|
330
|
+
* to rise, not fade: a rising solid is what sells a pour, which is why this
|
|
331
|
+
* does not reuse the pin's `fillOpacity` trick.
|
|
332
|
+
*/
|
|
333
|
+
const liquid: Variants = {
|
|
334
|
+
rest: { attrY: WINE_EMPTY, transition: returnTransition },
|
|
335
|
+
active: { attrY: WINE_LINE, transition: returnTransition },
|
|
336
|
+
play: {
|
|
337
|
+
// Sits empty through the squat, rises past the line at the apex, drops
|
|
338
|
+
// back, settles on the line.
|
|
339
|
+
attrY: [WINE_EMPTY, WINE_EMPTY, WINE_SLOSH, WINE_LINE + 0.5, WINE_LINE],
|
|
340
|
+
transition: beats(WINE_TIMES),
|
|
341
|
+
},
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
/** The glass squats on its foot as the pour lands, stretches with the rise, settles. */
|
|
345
|
+
const glass: Variants = {
|
|
346
|
+
rest: { scaleY: 1, transition: returnTransition },
|
|
347
|
+
active: { scaleY: 1, transition: returnTransition },
|
|
348
|
+
play: { scaleY: [1, 0.9, 1.03, 0.98, 1], transition: beats(WINE_TIMES) },
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
const BOWL = "M12 15a5 5 0 0 0 5-5c0-2-.5-4-2-8H9c-1.5 4-2 6-2 8a5 5 0 0 0 5 5Z";
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* The one gesture that HOLDS on the drinks side: rest is the exact lucide
|
|
355
|
+
* glyph (empty, line visible), active is the bowl filled to the line. Outline
|
|
356
|
+
* → solid mass is the largest silhouette change in the set, and at the
|
|
357
|
+
* segmented control's 16px that is still ~4.7px of rising liquid.
|
|
358
|
+
*/
|
|
359
|
+
export function MotionWine(props: MotionIconProps) {
|
|
360
|
+
const clipId = useSvgId("wine-bowl");
|
|
361
|
+
|
|
362
|
+
return (
|
|
363
|
+
<MotionIcon {...props}>
|
|
364
|
+
<defs>
|
|
365
|
+
<clipPath id={clipId}>
|
|
366
|
+
<path d={BOWL} />
|
|
367
|
+
</clipPath>
|
|
368
|
+
</defs>
|
|
369
|
+
|
|
370
|
+
<motion.g variants={glass} style={pivot(12, 22)}>
|
|
371
|
+
<g clipPath={`url(#${clipId})`}>
|
|
372
|
+
<motion.rect
|
|
373
|
+
variants={liquid}
|
|
374
|
+
fill="currentColor"
|
|
375
|
+
stroke="none"
|
|
376
|
+
x="6"
|
|
377
|
+
y={WINE_EMPTY}
|
|
378
|
+
width="12"
|
|
379
|
+
height="12"
|
|
380
|
+
/>
|
|
381
|
+
</g>
|
|
382
|
+
<path d="M8 22h8" />
|
|
383
|
+
<path d="M7 10h10" />
|
|
384
|
+
<path d="M12 15v7" />
|
|
385
|
+
<path d={BOWL} />
|
|
386
|
+
</motion.g>
|
|
387
|
+
</MotionIcon>
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// ─── Map — a tri-fold map is folded shut and snapped open (articulate) ────────
|
|
392
|
+
|
|
393
|
+
const MAP_TIMES = [0, 0.2, 0.5, 0.66, 0.84, 1];
|
|
394
|
+
const FOLD = [1, 0.2, 1.18, 0.94, 1.03, 1];
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* The two outer panels fold in against the middle one (right first, left a
|
|
398
|
+
* beat behind), then flap open past flat and settle — what you do with a
|
|
399
|
+
* paper map before reading it. Width 18 → ~8.4 → ~20 → 18.
|
|
400
|
+
*
|
|
401
|
+
* `scaleX` around the fold is a 2D stand-in for a hinge (SVG child 3D
|
|
402
|
+
* transforms are unreliable on iOS Safari); at 22px it reads as "closes, pops
|
|
403
|
+
* open", which is the intent, not as a true fold.
|
|
404
|
+
*
|
|
405
|
+
* Lucide's outline is one closed path; it is cut at the two fold lines,
|
|
406
|
+
* exactly where the glyph already draws `M9 3.236v15` and `M15 5.764v15`.
|
|
407
|
+
* Each fold stroke is drawn on the panel that folds around it, so the seams
|
|
408
|
+
* sit under strokes that are already there at rest.
|
|
409
|
+
*/
|
|
410
|
+
const panelRight: Variants = {
|
|
411
|
+
rest: { scaleX: 1, transition: returnTransition },
|
|
412
|
+
active: { scaleX: 1, transition: returnTransition },
|
|
413
|
+
play: { scaleX: FOLD, transition: beats(MAP_TIMES, 0.62) },
|
|
414
|
+
};
|
|
415
|
+
const panelLeft: Variants = {
|
|
416
|
+
rest: { scaleX: 1, transition: returnTransition },
|
|
417
|
+
active: { scaleX: 1, transition: returnTransition },
|
|
418
|
+
play: { scaleX: FOLD, transition: { ...beats(MAP_TIMES, 0.62), delay: 0.06 } },
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
export function MotionMap(props: MotionIconProps) {
|
|
422
|
+
return (
|
|
423
|
+
<MotionIcon {...props}>
|
|
424
|
+
{/* Middle panel — the one in your hand — holds still: the two top/bottom runs and the bumps at the x=15 fold. */}
|
|
425
|
+
<path d="M14.106 5.553a2 2 0 0 0 1.788 0" />
|
|
426
|
+
<path d="M15.894 20.554a2 2 0 0 1-1.788 0l-4.212-2.106" />
|
|
427
|
+
<path d="M9.894 3.447 14.106 5.553" />
|
|
428
|
+
<motion.g variants={panelRight} style={pivot(15, 12)}>
|
|
429
|
+
<path d="M15.894 5.553l3.659-1.83A1 1 0 0 1 21 4.619v12.764a1 1 0 0 1-.553.894l-4.553 2.277" />
|
|
430
|
+
<path d="M15 5.764v15" />
|
|
431
|
+
</motion.g>
|
|
432
|
+
<motion.g variants={panelLeft} style={pivot(9, 12)}>
|
|
433
|
+
<path d="M9.894 18.448a2 2 0 0 0-1.788 0l-3.659 1.83A1 1 0 0 1 3 19.381V6.618a1 1 0 0 1 .553-.894l4.553-2.277a2 2 0 0 1 1.788 0" />
|
|
434
|
+
<path d="M9 3.236v15" />
|
|
435
|
+
</motion.g>
|
|
436
|
+
</MotionIcon>
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// ─── Flame — gathers low, flares tall, fills at the peak (deform) ─────────────
|
|
441
|
+
|
|
442
|
+
const FLAME_TIMES = [0, 0.16, 0.42, 0.58, 0.78, 1];
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* The trending badge's tap — a gesture, not the loop the badge already runs.
|
|
446
|
+
* The flame crouches low and wide, bursts up tall and thin with its tip
|
|
447
|
+
* whipping sideways, fills solid at the peak of the flare, and settles back
|
|
448
|
+
* to an outline. `skewX` rather than `rotate`: a flame bends in a draft, it
|
|
449
|
+
* does not tilt rigidly. Pivot at the base (lucide's bottom arc is radius 7,
|
|
450
|
+
* so the base is y 22, not 15). The inner tongue shares both endpoints with
|
|
451
|
+
* the outer edge — splitting it would break the closed outline.
|
|
452
|
+
*/
|
|
453
|
+
const flame: Variants = {
|
|
454
|
+
rest: { scaleY: 1, scaleX: 1, skewX: 0, fillOpacity: 0, transition: returnTransition },
|
|
455
|
+
active: { scaleY: 1, scaleX: 1, skewX: 0, fillOpacity: 0, transition: returnTransition },
|
|
456
|
+
play: {
|
|
457
|
+
scaleY: [1, 0.8, 1.38, 1.18, 0.96, 1],
|
|
458
|
+
scaleX: [1, 1.15, 0.82, 0.95, 1.03, 1],
|
|
459
|
+
skewX: [0, 0, -8, 7, -2, 0],
|
|
460
|
+
fillOpacity: [0, 0, 1, 1, 0.35, 0],
|
|
461
|
+
transition: beats(FLAME_TIMES),
|
|
462
|
+
},
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
export function MotionFlame(props: MotionIconProps) {
|
|
466
|
+
return (
|
|
467
|
+
<MotionIcon {...props}>
|
|
468
|
+
<motion.path
|
|
469
|
+
variants={flame}
|
|
470
|
+
style={pivot(12, 22)}
|
|
471
|
+
fill="currentColor"
|
|
472
|
+
fillOpacity={0}
|
|
473
|
+
d="M12 3q1 4 4 6.5t3 5.5a1 1 0 0 1-14 0 5 5 0 0 1 1-3 1 1 0 0 0 5 0c0-2-1.5-3-1.5-5q0-2 2.5-4"
|
|
474
|
+
/>
|
|
475
|
+
</MotionIcon>
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// ─── Calendar-clock — time runs on the face, and a wedge of it holds ──────────
|
|
480
|
+
|
|
481
|
+
const CLOCK_TIMES = [0, 0.14, 0.62, 0.8, 1];
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* The events timeline segment — sibling of `MotionCalendar` (same frame, same
|
|
485
|
+
* posts) with its own behaviour on the face. The minute hand winds back and
|
|
486
|
+
* whips a full turn, and behind it a solid wedge of elapsed time sweeps
|
|
487
|
+
* clockwise from 12, overshoots the hour hand, and settles on it (~4 o'clock,
|
|
488
|
+
* the 122° the lucide hour hand already points at). HOLDS: the wedge stays
|
|
489
|
+
* while the timeline is the active view.
|
|
490
|
+
*
|
|
491
|
+
* Lucide's hands are 2.2 and 1.9 units long — 1.5px at 16px — so the hand's
|
|
492
|
+
* whip is the cause and near-invisible on its own; the wedge is load-bearing.
|
|
493
|
+
* It is a circle of radius 2 stroked 4 wide with butt caps (a stroke as wide
|
|
494
|
+
* as its radius is a pie), rotated so `pathLength` starts at 12 o'clock — the
|
|
495
|
+
* set's way of drawing progress without a `d` morph. Outer radius 4 leaves a
|
|
496
|
+
* ring between wedge and rim so the face still reads as a face.
|
|
497
|
+
*/
|
|
498
|
+
const minuteHand: Variants = {
|
|
499
|
+
rest: { rotate: 0, transition: returnTransition },
|
|
500
|
+
active: { rotate: 0, transition: returnTransition },
|
|
501
|
+
play: { rotate: [0, -14, 372, 360, 360], transition: beats(CLOCK_TIMES, 0.62) },
|
|
502
|
+
};
|
|
503
|
+
const elapsed: Variants = {
|
|
504
|
+
rest: { pathLength: 0, transition: returnTransition },
|
|
505
|
+
active: { pathLength: 0.34, transition: returnTransition },
|
|
506
|
+
play: { pathLength: [0, 0, 0.42, 0.31, 0.34], transition: beats(CLOCK_TIMES, 0.62) },
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
export function MotionCalendarClock(props: MotionIconProps) {
|
|
510
|
+
return (
|
|
511
|
+
<MotionIcon {...props}>
|
|
512
|
+
<path d="M21 7.5V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h3.5" />
|
|
513
|
+
<path d="M16 2v4" />
|
|
514
|
+
<path d="M8 2v4" />
|
|
515
|
+
<path d="M3 10h5" />
|
|
516
|
+
<circle cx="16" cy="16" r="6" />
|
|
517
|
+
{/* `pathLength={1}` + `strokeDasharray="0 1"` is exactly what framer
|
|
518
|
+
writes for `pathLength: 0`; without them the first paint — before the
|
|
519
|
+
pose effect runs — is an un-dashed circle, i.e. a solid disc on the
|
|
520
|
+
face for one frame on every mount. Same trap `MotionCheck` guards. */}
|
|
521
|
+
<g transform="rotate(-90 16 16.2)">
|
|
522
|
+
<motion.circle
|
|
523
|
+
variants={elapsed}
|
|
524
|
+
cx="16"
|
|
525
|
+
cy="16.2"
|
|
526
|
+
r="2"
|
|
527
|
+
strokeWidth={4}
|
|
528
|
+
strokeLinecap="butt"
|
|
529
|
+
pathLength={1}
|
|
530
|
+
strokeDasharray="0 1"
|
|
531
|
+
/>
|
|
532
|
+
</g>
|
|
533
|
+
<motion.path variants={minuteHand} style={pivot(16, 16.2)} d="M16 14v2.2" />
|
|
534
|
+
<path d="M16 16.2l1.6 1" />
|
|
535
|
+
</MotionIcon>
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// ─── List — a row is picked up and reinserted at the top (articulate) ─────────
|
|
540
|
+
|
|
541
|
+
const LIST_TIMES = [0, 0.14, 0.62, 0.8, 1];
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* The events list segment. The bottom row is pressed, lifts off the page
|
|
545
|
+
* toward the reader, travels up past the other two — which drop a slot each
|
|
546
|
+
* to make way, row 2 first so it clears the mover's path — and lands in the
|
|
547
|
+
* top slot. The end picture is the same three-row list; the values are one
|
|
548
|
+
* slot on, and `useIconGesture` snaps them. Active = rest, like the grid:
|
|
549
|
+
* no held pose survives 16px on this geometry (ragged line lengths, bigger
|
|
550
|
+
* bullets, a shifted row all fall under the floor and read as a defect), so
|
|
551
|
+
* colour and stroke weight carry selection.
|
|
552
|
+
*/
|
|
553
|
+
const mover: Variants = {
|
|
554
|
+
rest: { y: 0, x: 0, scale: 1, transition: returnTransition },
|
|
555
|
+
active: { y: 0, x: 0, scale: 1, transition: returnTransition },
|
|
556
|
+
play: {
|
|
557
|
+
y: [0, 1.5, -15.2, -13.6, -14],
|
|
558
|
+
x: [0, 0, 2.5, 0.6, 0],
|
|
559
|
+
scale: [1, 1, 1.06, 1.02, 1],
|
|
560
|
+
transition: beats(LIST_TIMES, 0.6),
|
|
561
|
+
},
|
|
562
|
+
};
|
|
563
|
+
const dropSlot = (delay: number): Variants => ({
|
|
564
|
+
rest: { y: 0, transition: returnTransition },
|
|
565
|
+
active: { y: 0, transition: returnTransition },
|
|
566
|
+
play: { y: [0, 0, 7.6, 6.7, 7], transition: { ...beats([0, 0.22, 0.62, 0.8, 1], 0.6), delay } },
|
|
567
|
+
});
|
|
568
|
+
const rowFirst = dropSlot(0.07);
|
|
569
|
+
const rowSecond = dropSlot(0);
|
|
570
|
+
|
|
571
|
+
function ListRow({ y }: { y: number }) {
|
|
572
|
+
return (
|
|
573
|
+
<>
|
|
574
|
+
<path d={`M3 ${y}h.01`} />
|
|
575
|
+
<path d={`M8 ${y}h13`} />
|
|
576
|
+
</>
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
export function MotionList(props: MotionIconProps) {
|
|
581
|
+
return (
|
|
582
|
+
<MotionIcon {...props}>
|
|
583
|
+
<motion.g variants={rowFirst}>
|
|
584
|
+
<ListRow y={5} />
|
|
585
|
+
</motion.g>
|
|
586
|
+
<motion.g variants={rowSecond}>
|
|
587
|
+
<ListRow y={12} />
|
|
588
|
+
</motion.g>
|
|
589
|
+
<motion.g variants={mover} style={pivot(3, 19)}>
|
|
590
|
+
<ListRow y={19} />
|
|
591
|
+
</motion.g>
|
|
592
|
+
</MotionIcon>
|
|
593
|
+
);
|
|
594
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
-
import { render } from "@testing-library/react";
|
|
2
|
+
import { act, render } from "@testing-library/react";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
|
|
5
5
|
import { MotionIcon, pivot } from "./motion-icon";
|
|
@@ -13,8 +13,10 @@ import { MotionIcon, pivot } from "./motion-icon";
|
|
|
13
13
|
*/
|
|
14
14
|
const variantsSet: string[] = [];
|
|
15
15
|
const variantsStarted: string[] = [];
|
|
16
|
+
let prefersReducedMotion = false;
|
|
16
17
|
|
|
17
18
|
vi.mock("framer-motion", () => ({
|
|
19
|
+
useReducedMotion: () => prefersReducedMotion,
|
|
18
20
|
useAnimationControls: () => ({
|
|
19
21
|
set: (v: string) => variantsSet.push(v),
|
|
20
22
|
start: (v: string) => {
|
|
@@ -33,6 +35,7 @@ vi.mock("framer-motion", () => ({
|
|
|
33
35
|
beforeEach(() => {
|
|
34
36
|
variantsSet.length = 0;
|
|
35
37
|
variantsStarted.length = 0;
|
|
38
|
+
prefersReducedMotion = false;
|
|
36
39
|
});
|
|
37
40
|
|
|
38
41
|
describe("MotionIcon", () => {
|
|
@@ -156,6 +159,182 @@ describe("MotionIcon", () => {
|
|
|
156
159
|
|
|
157
160
|
expect(variantsStarted).toEqual(["play"]);
|
|
158
161
|
});
|
|
162
|
+
|
|
163
|
+
it("snaps to the pose the gesture depicts once the performance resolves", async () => {
|
|
164
|
+
const { rerender } = render(
|
|
165
|
+
<MotionIcon active={false} replayToken={0}>
|
|
166
|
+
<path />
|
|
167
|
+
</MotionIcon>
|
|
168
|
+
);
|
|
169
|
+
variantsSet.length = 0;
|
|
170
|
+
|
|
171
|
+
// Pointer down, and the navigation it causes flips `active` before the
|
|
172
|
+
// ~600ms gesture finishes.
|
|
173
|
+
rerender(
|
|
174
|
+
<MotionIcon active={false} replayToken={1}>
|
|
175
|
+
<path />
|
|
176
|
+
</MotionIcon>
|
|
177
|
+
);
|
|
178
|
+
rerender(
|
|
179
|
+
<MotionIcon active replayToken={1}>
|
|
180
|
+
<path />
|
|
181
|
+
</MotionIcon>
|
|
182
|
+
);
|
|
183
|
+
await act(async () => {});
|
|
184
|
+
|
|
185
|
+
// The grid's tiles end one slot around the board, the calendar's reel four
|
|
186
|
+
// rows up: same picture, different values. Without this snap the next
|
|
187
|
+
// `rest` would tween them back and perform the gesture in reverse.
|
|
188
|
+
expect(variantsStarted).toEqual(["play"]);
|
|
189
|
+
expect(variantsSet).toEqual(["active"]);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("holds the end pose for a grace period, then snaps to rest when the state never arrives", async () => {
|
|
193
|
+
vi.useFakeTimers();
|
|
194
|
+
try {
|
|
195
|
+
const { rerender } = render(
|
|
196
|
+
<MotionIcon active={false} replayToken={0}>
|
|
197
|
+
<path />
|
|
198
|
+
</MotionIcon>
|
|
199
|
+
);
|
|
200
|
+
variantsSet.length = 0;
|
|
201
|
+
|
|
202
|
+
rerender(
|
|
203
|
+
<MotionIcon active={false} replayToken={1}>
|
|
204
|
+
<path />
|
|
205
|
+
</MotionIcon>
|
|
206
|
+
);
|
|
207
|
+
await act(async () => {});
|
|
208
|
+
// Resolved, but a route may still be confirming: nothing yet.
|
|
209
|
+
expect(variantsSet).toEqual([]);
|
|
210
|
+
|
|
211
|
+
await act(async () => {
|
|
212
|
+
vi.advanceTimersByTime(900);
|
|
213
|
+
});
|
|
214
|
+
expect(variantsSet).toEqual(["rest"]);
|
|
215
|
+
} finally {
|
|
216
|
+
vi.useRealTimers();
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("takes the held pose without a second performance when `active` arrives late", async () => {
|
|
221
|
+
vi.useFakeTimers();
|
|
222
|
+
try {
|
|
223
|
+
const { rerender } = render(
|
|
224
|
+
<MotionIcon active={false} replayToken={0}>
|
|
225
|
+
<path />
|
|
226
|
+
</MotionIcon>
|
|
227
|
+
);
|
|
228
|
+
variantsSet.length = 0;
|
|
229
|
+
variantsStarted.length = 0;
|
|
230
|
+
|
|
231
|
+
rerender(
|
|
232
|
+
<MotionIcon active={false} replayToken={1}>
|
|
233
|
+
<path />
|
|
234
|
+
</MotionIcon>
|
|
235
|
+
);
|
|
236
|
+
await act(async () => {});
|
|
237
|
+
// A slow route: the gesture has landed, the state lands 850ms after the tap.
|
|
238
|
+
await act(async () => {
|
|
239
|
+
vi.advanceTimersByTime(850);
|
|
240
|
+
});
|
|
241
|
+
rerender(
|
|
242
|
+
<MotionIcon active replayToken={1}>
|
|
243
|
+
<path />
|
|
244
|
+
</MotionIcon>
|
|
245
|
+
);
|
|
246
|
+
await act(async () => {
|
|
247
|
+
vi.advanceTimersByTime(2000);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// One play, one snap to active — no cut to rest in between, no replay.
|
|
251
|
+
expect(variantsStarted).toEqual(["play"]);
|
|
252
|
+
expect(variantsSet).toEqual(["active"]);
|
|
253
|
+
} finally {
|
|
254
|
+
vi.useRealTimers();
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it("lets an in-flight gesture land before returning, instead of springing it back mid-slide", async () => {
|
|
259
|
+
vi.useFakeTimers();
|
|
260
|
+
try {
|
|
261
|
+
const { rerender } = render(
|
|
262
|
+
<MotionIcon active replayToken={0}>
|
|
263
|
+
<path />
|
|
264
|
+
</MotionIcon>
|
|
265
|
+
);
|
|
266
|
+
variantsSet.length = 0;
|
|
267
|
+
variantsStarted.length = 0;
|
|
268
|
+
|
|
269
|
+
// Tap the current tab (replay), then leave for another within 600ms.
|
|
270
|
+
rerender(
|
|
271
|
+
<MotionIcon active replayToken={1}>
|
|
272
|
+
<path />
|
|
273
|
+
</MotionIcon>
|
|
274
|
+
);
|
|
275
|
+
rerender(
|
|
276
|
+
<MotionIcon active={false} replayToken={1}>
|
|
277
|
+
<path />
|
|
278
|
+
</MotionIcon>
|
|
279
|
+
);
|
|
280
|
+
await act(async () => {});
|
|
281
|
+
await act(async () => {
|
|
282
|
+
vi.advanceTimersByTime(900);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// No `rest` tween was STARTED (that would reverse the grid's tiles);
|
|
286
|
+
// the settle snapped the values instead.
|
|
287
|
+
expect(variantsStarted).toEqual(["play"]);
|
|
288
|
+
expect(variantsSet).toEqual(["rest"]);
|
|
289
|
+
} finally {
|
|
290
|
+
vi.useRealTimers();
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it("under reduced motion skips the performance but still changes state", async () => {
|
|
295
|
+
prefersReducedMotion = true;
|
|
296
|
+
const { rerender } = render(
|
|
297
|
+
<MotionIcon active={false} replayToken={0}>
|
|
298
|
+
<path />
|
|
299
|
+
</MotionIcon>
|
|
300
|
+
);
|
|
301
|
+
variantsSet.length = 0;
|
|
302
|
+
|
|
303
|
+
rerender(
|
|
304
|
+
<MotionIcon active replayToken={1}>
|
|
305
|
+
<path />
|
|
306
|
+
</MotionIcon>
|
|
307
|
+
);
|
|
308
|
+
await act(async () => {});
|
|
309
|
+
|
|
310
|
+
// Less movement, not less feedback: no `play`, straight to the held pose.
|
|
311
|
+
expect(variantsStarted).toEqual([]);
|
|
312
|
+
expect(variantsSet).toEqual(["active"]);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it("only the latest of two overlapping performances gets to settle", async () => {
|
|
316
|
+
const { rerender } = render(
|
|
317
|
+
<MotionIcon active={false} replayToken={0}>
|
|
318
|
+
<path />
|
|
319
|
+
</MotionIcon>
|
|
320
|
+
);
|
|
321
|
+
variantsSet.length = 0;
|
|
322
|
+
|
|
323
|
+
rerender(
|
|
324
|
+
<MotionIcon active={false} replayToken={1}>
|
|
325
|
+
<path />
|
|
326
|
+
</MotionIcon>
|
|
327
|
+
);
|
|
328
|
+
rerender(
|
|
329
|
+
<MotionIcon active replayToken={2}>
|
|
330
|
+
<path />
|
|
331
|
+
</MotionIcon>
|
|
332
|
+
);
|
|
333
|
+
await act(async () => {});
|
|
334
|
+
|
|
335
|
+
expect(variantsStarted).toEqual(["play", "play"]);
|
|
336
|
+
expect(variantsSet).toEqual(["active"]);
|
|
337
|
+
});
|
|
159
338
|
});
|
|
160
339
|
|
|
161
340
|
/**
|