@bearmenu/ui 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-A5ONAJCE.js +102 -0
- package/dist/chunk-AV777R4G.js +43 -0
- package/dist/{chunk-WKMLZDUT.js → chunk-FRPPIXMY.js} +1 -1
- package/dist/{chunk-PSR7N36A.js → chunk-HCM6LKM5.js} +1 -1
- package/dist/{chunk-YJ6OBKZJ.js → chunk-TL4YU2O5.js} +10 -3
- package/dist/chunk-UX6SWAKH.js +320 -0
- package/dist/{chunk-7PPCH42H.js → chunk-VQNDZVI7.js} +1 -1
- package/dist/chunk-WSRWFA6K.js +26 -0
- package/dist/chunk-YLNYXPIH.js +61 -0
- package/dist/components/accordion.js +1 -1
- package/dist/components/alert.d.ts +2 -2
- package/dist/components/badge.d.ts +3 -2
- package/dist/components/badge.js +1 -1
- package/dist/components/button.d.ts +1 -1
- package/dist/components/card.d.ts +2 -2
- package/dist/components/card.js +127 -4
- package/dist/components/combobox.js +2 -2
- package/dist/components/command.d.ts +15 -15
- package/dist/components/command.js +2 -2
- package/dist/components/currency-input.js +1 -1
- package/dist/components/date-input.d.ts +1 -1
- package/dist/components/date-input.js +1 -1
- package/dist/components/dialog.js +1 -1
- package/dist/components/filter-category-row.d.ts +13 -0
- package/dist/components/filter-category-row.js +44 -0
- package/dist/components/input-group.d.ts +2 -0
- package/dist/components/input-group.js +10 -5
- package/dist/components/input.d.ts +1 -0
- package/dist/components/input.js +1 -1
- package/dist/components/item.d.ts +2 -2
- package/dist/components/multi-select-combobox.js +3 -3
- package/dist/components/phone-frame.js +1 -1
- package/dist/components/place-about-tab.d.ts +2 -2
- package/dist/components/place-about-tab.js +4 -3
- package/dist/components/place-details-mobile.js +5 -3
- package/dist/components/place-schedule-week.d.ts +19 -0
- package/dist/components/place-schedule-week.js +4 -0
- package/dist/components/place-types.d.ts +18 -9
- package/dist/components/scroll-fade.d.ts +15 -0
- package/dist/components/scroll-fade.js +121 -0
- package/dist/components/searchable-select.js +2 -2
- package/dist/components/separator.js +3 -26
- package/dist/components/sheet.js +1 -1
- package/dist/components/sliding-panels.d.ts +1 -0
- package/dist/components/sliding-panels.js +22 -6
- package/dist/components/sort-option-list.d.ts +16 -0
- package/dist/components/sort-option-list.js +46 -0
- package/dist/components/spinner.d.ts +2 -2
- package/dist/components/tabs.js +98 -17
- package/dist/components/tag.d.ts +2 -2
- package/dist/components/tag.js +3 -61
- package/dist/components/text.d.ts +3 -3
- package/dist/components/toast.js +1 -1
- package/dist/components/toaster.js +1 -1
- package/package.json +23 -112
- package/src/components/accordion.tsx +1 -1
- package/src/components/badge.tsx +9 -2
- package/src/components/card.tsx +22 -18
- package/src/components/dialog.tsx +1 -1
- package/src/components/filter-category-row.tsx +52 -0
- package/src/components/input-group.tsx +9 -4
- package/src/components/input.tsx +8 -3
- package/src/components/phone-frame.tsx +1 -1
- package/src/components/place-about-tab.stories.tsx +24 -13
- package/src/components/place-about-tab.test.tsx +92 -38
- package/src/components/place-about-tab.tsx +310 -344
- package/src/components/place-schedule-week.tsx +128 -0
- package/src/components/place-types.ts +27 -8
- package/src/components/scroll-fade.stories.tsx +98 -0
- package/src/components/scroll-fade.tsx +132 -0
- package/src/components/sheet.tsx +1 -1
- package/src/components/sliding-panels.tsx +33 -6
- package/src/components/sort-option-list.tsx +57 -0
- package/src/components/tabs.tsx +76 -11
- package/src/components/toast.tsx +1 -1
- package/src/globals.css +77 -19
- package/src/lib/motion.ts +137 -0
- package/dist/chunk-BAZFVSSG.js +0 -279
- package/dist/chunk-RIDXFY6M.js +0 -38
- package/dist/chunk-XK2ZDQVE.js +0 -122
package/src/components/tabs.tsx
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import * as TabsPrimitive from "@radix-ui/react-tabs";
|
|
5
|
+
import { motion } from "framer-motion";
|
|
6
|
+
import { archetype } from "../lib/motion";
|
|
5
7
|
import { cn } from "../lib/utils";
|
|
6
8
|
|
|
7
9
|
type TabsVariant = "pill" | "underline";
|
|
@@ -10,11 +12,15 @@ type TabsOrientation = "horizontal" | "vertical";
|
|
|
10
12
|
type TabsContextValue = {
|
|
11
13
|
variant: TabsVariant;
|
|
12
14
|
orientation: TabsOrientation;
|
|
15
|
+
/** Unique id used by Framer Motion `layoutId` to morph the active indicator
|
|
16
|
+
* between triggers. Generated once per <TabsList>. */
|
|
17
|
+
indicatorId: string;
|
|
13
18
|
};
|
|
14
19
|
|
|
15
20
|
const TabsContext = React.createContext<TabsContextValue>({
|
|
16
21
|
variant: "pill",
|
|
17
22
|
orientation: "horizontal",
|
|
23
|
+
indicatorId: "tabs-indicator",
|
|
18
24
|
});
|
|
19
25
|
|
|
20
26
|
const Tabs = React.forwardRef<
|
|
@@ -45,9 +51,10 @@ const TabsList = React.forwardRef<
|
|
|
45
51
|
>(({ className, variant = "pill", scrollable, children, ...props }, ref) => {
|
|
46
52
|
const orientation: TabsOrientation = props["aria-orientation"] === "vertical" ? "vertical" : "horizontal";
|
|
47
53
|
const allowScroll = scrollable ?? orientation === "horizontal";
|
|
54
|
+
const indicatorId = React.useId();
|
|
48
55
|
|
|
49
56
|
return (
|
|
50
|
-
<TabsContext.Provider value={{ variant, orientation }}>
|
|
57
|
+
<TabsContext.Provider value={{ variant, orientation, indicatorId }}>
|
|
51
58
|
<TabsPrimitive.List
|
|
52
59
|
ref={ref}
|
|
53
60
|
data-slot="tabs-list"
|
|
@@ -75,27 +82,80 @@ TabsList.displayName = TabsPrimitive.List.displayName;
|
|
|
75
82
|
const TabsTrigger = React.forwardRef<
|
|
76
83
|
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
|
77
84
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
|
78
|
-
>(({ className, ...props }, ref) => {
|
|
79
|
-
const { variant, orientation } = React.useContext(TabsContext);
|
|
85
|
+
>(({ className, children, ...props }, ref) => {
|
|
86
|
+
const { variant, orientation, indicatorId } = React.useContext(TabsContext);
|
|
87
|
+
// Inspect data-state at render to know whether to render the indicator.
|
|
88
|
+
// Radix sets data-state="active" | "inactive" on the trigger via its primitive.
|
|
89
|
+
const triggerRef = React.useRef<HTMLButtonElement | null>(null);
|
|
90
|
+
const [isActive, setIsActive] = React.useState(false);
|
|
91
|
+
|
|
92
|
+
// Sync isActive from Radix's data-state. Tracking via mutation observer
|
|
93
|
+
// keeps the indicator in sync without needing controlled state from consumers.
|
|
94
|
+
React.useEffect(() => {
|
|
95
|
+
const node = triggerRef.current;
|
|
96
|
+
if (!node) return;
|
|
97
|
+
const sync = () => setIsActive(node.getAttribute("data-state") === "active");
|
|
98
|
+
sync();
|
|
99
|
+
const observer = new MutationObserver(sync);
|
|
100
|
+
observer.observe(node, { attributes: true, attributeFilter: ["data-state"] });
|
|
101
|
+
return () => observer.disconnect();
|
|
102
|
+
}, []);
|
|
103
|
+
|
|
104
|
+
const composedRef = React.useCallback(
|
|
105
|
+
(node: HTMLButtonElement | null) => {
|
|
106
|
+
triggerRef.current = node;
|
|
107
|
+
if (typeof ref === "function") ref(node);
|
|
108
|
+
else if (ref) (ref as React.MutableRefObject<HTMLButtonElement | null>).current = node;
|
|
109
|
+
},
|
|
110
|
+
[ref]
|
|
111
|
+
);
|
|
80
112
|
|
|
81
113
|
return (
|
|
82
114
|
<TabsPrimitive.Trigger
|
|
83
|
-
ref={
|
|
115
|
+
ref={composedRef}
|
|
84
116
|
data-slot="tabs-trigger"
|
|
85
117
|
className={cn(
|
|
86
|
-
"inline-flex items-center justify-center whitespace-nowrap text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
|
118
|
+
"relative inline-flex items-center justify-center whitespace-nowrap text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
|
87
119
|
variant === "pill" &&
|
|
88
|
-
"rounded-xl px-4 py-1.5 text-muted-foreground hover:text-foreground data-[state=active]:
|
|
120
|
+
"rounded-xl px-4 py-1.5 text-muted-foreground hover:text-foreground data-[state=active]:text-foreground",
|
|
89
121
|
variant === "underline" &&
|
|
90
122
|
orientation === "horizontal" &&
|
|
91
|
-
"
|
|
123
|
+
"h-11 rounded-none px-4 text-muted-foreground hover:text-foreground data-[state=active]:text-foreground",
|
|
92
124
|
variant === "underline" &&
|
|
93
125
|
orientation === "vertical" &&
|
|
94
|
-
"
|
|
126
|
+
"rounded-none px-4 py-2 -mr-px text-muted-foreground hover:text-foreground data-[state=active]:text-foreground justify-start",
|
|
95
127
|
className
|
|
96
128
|
)}
|
|
97
129
|
{...props}
|
|
98
|
-
|
|
130
|
+
>
|
|
131
|
+
{/* Active indicator: shared-layout element morphs between triggers */}
|
|
132
|
+
{isActive && (
|
|
133
|
+
<motion.span
|
|
134
|
+
aria-hidden
|
|
135
|
+
layoutId={indicatorId}
|
|
136
|
+
transition={archetype.indicatorMorph}
|
|
137
|
+
className={cn(
|
|
138
|
+
"absolute inset-0 -z-0 motion-reduce:hidden",
|
|
139
|
+
variant === "pill" && "rounded-xl bg-background shadow-sm",
|
|
140
|
+
variant === "underline" && orientation === "horizontal" && "border-b-2 border-primary",
|
|
141
|
+
variant === "underline" && orientation === "vertical" && "border-r-2 border-primary"
|
|
142
|
+
)}
|
|
143
|
+
/>
|
|
144
|
+
)}
|
|
145
|
+
{/* Static fallback for reduced-motion: keep the legacy active styling */}
|
|
146
|
+
{isActive && (
|
|
147
|
+
<span
|
|
148
|
+
aria-hidden
|
|
149
|
+
className={cn(
|
|
150
|
+
"absolute inset-0 -z-0 hidden motion-reduce:block",
|
|
151
|
+
variant === "pill" && "rounded-xl bg-background shadow-sm",
|
|
152
|
+
variant === "underline" && orientation === "horizontal" && "border-b-2 border-primary",
|
|
153
|
+
variant === "underline" && orientation === "vertical" && "border-r-2 border-primary"
|
|
154
|
+
)}
|
|
155
|
+
/>
|
|
156
|
+
)}
|
|
157
|
+
<span className="relative z-10 flex items-center gap-2">{children}</span>
|
|
158
|
+
</TabsPrimitive.Trigger>
|
|
99
159
|
);
|
|
100
160
|
});
|
|
101
161
|
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
|
|
@@ -103,16 +163,21 @@ TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
|
|
|
103
163
|
const TabsContent = React.forwardRef<
|
|
104
164
|
React.ElementRef<typeof TabsPrimitive.Content>,
|
|
105
165
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
|
106
|
-
>(({ className, ...props }, ref) => (
|
|
166
|
+
>(({ className, children, ...props }, ref) => (
|
|
107
167
|
<TabsPrimitive.Content
|
|
108
168
|
ref={ref}
|
|
109
169
|
data-slot="tabs-content"
|
|
110
170
|
className={cn(
|
|
111
171
|
"flex-1 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 rounded-md",
|
|
172
|
+
// v1.0 motion: content-swap on activation. CSS animation so this works
|
|
173
|
+
// even outside a Framer-Motion-wrapped layout. Disabled for reduced-motion.
|
|
174
|
+
"data-[state=active]:animate-in data-[state=active]:fade-in-0 data-[state=active]:slide-in-from-bottom-1 data-[state=active]:duration-[var(--duration-quick)] data-[state=active]:ease-[var(--ease-standard)] motion-reduce:data-[state=active]:animate-none",
|
|
112
175
|
className
|
|
113
176
|
)}
|
|
114
177
|
{...props}
|
|
115
|
-
|
|
178
|
+
>
|
|
179
|
+
{children}
|
|
180
|
+
</TabsPrimitive.Content>
|
|
116
181
|
));
|
|
117
182
|
TabsContent.displayName = TabsPrimitive.Content.displayName;
|
|
118
183
|
|
package/src/components/toast.tsx
CHANGED
|
@@ -24,7 +24,7 @@ const ToastViewport = React.forwardRef<
|
|
|
24
24
|
ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
|
|
25
25
|
|
|
26
26
|
const toastVariants = cva(
|
|
27
|
-
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-xl p-6 pr-8 shadow-2xl transition-
|
|
27
|
+
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-xl p-6 pr-8 shadow-2xl transition-[transform,opacity] duration-[var(--duration-standard)] ease-[var(--ease-standard)] data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=open]:duration-[var(--duration-standard)] data-[state=closed]:duration-[var(--duration-exit-standard)] data-[state=open]:ease-[var(--ease-enter)] data-[state=closed]:ease-[var(--ease-exit)] data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
|
|
28
28
|
{
|
|
29
29
|
variants: {
|
|
30
30
|
variant: {
|
package/src/globals.css
CHANGED
|
@@ -89,18 +89,30 @@ h6 {
|
|
|
89
89
|
--shadow-alpha-xl: 0.18;
|
|
90
90
|
--shadow-alpha-2xl: 0.25;
|
|
91
91
|
|
|
92
|
-
/* Motion
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
--
|
|
100
|
-
--
|
|
101
|
-
--
|
|
102
|
-
--
|
|
103
|
-
--
|
|
92
|
+
/* ─── Motion System v1.0 ─────────────────────────────────────────────
|
|
93
|
+
Three layers of tokens. Feature code reads only `--duration-*` and
|
|
94
|
+
`--ease-*` semantic names; raw values live here.
|
|
95
|
+
|
|
96
|
+
JS twin: @bearmenu/ui/lib/motion (duration / ease / archetype). */
|
|
97
|
+
|
|
98
|
+
/* Durations */
|
|
99
|
+
--duration-none: 0ms; /* literal "no animation" */
|
|
100
|
+
--duration-instant: 120ms; /* tap feedback, icon swap */
|
|
101
|
+
--duration-quick: 180ms; /* chip toggle, tab indicator */
|
|
102
|
+
--duration-standard: 260ms; /* component entrance — workhorse */
|
|
103
|
+
--duration-deliberate: 360ms; /* drawer / sheet open */
|
|
104
|
+
--duration-narrative: 500ms; /* editorial hero (sparingly) */
|
|
105
|
+
--duration-exit-quick: 110ms; /* content exit inside a surface */
|
|
106
|
+
--duration-exit-standard: 210ms; /* surface / overlay exit */
|
|
107
|
+
|
|
108
|
+
/* Easings */
|
|
109
|
+
--ease-enter: cubic-bezier(0, 0, 0.2, 1);
|
|
110
|
+
--ease-standard: cubic-bezier(0.4, 0, 0.2, 1);
|
|
111
|
+
--ease-exit: cubic-bezier(0.55, 0, 1, 0.45);
|
|
112
|
+
/* CSS equivalents for the JS spring presets — for keyframe / transition use */
|
|
113
|
+
--ease-spring-snappy: cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
114
|
+
--ease-spring-smooth: cubic-bezier(0.22, 1, 0.36, 1);
|
|
115
|
+
--ease-spring-soft: cubic-bezier(0.16, 1, 0.3, 1);
|
|
104
116
|
|
|
105
117
|
/* Z-index scale */
|
|
106
118
|
--z-base: 0;
|
|
@@ -246,8 +258,8 @@ h6 {
|
|
|
246
258
|
--shadow-2xl: 0 25px 50px -12px hsl(var(--shadow-color) / var(--shadow-alpha-2xl));
|
|
247
259
|
|
|
248
260
|
/* Default transition tokens — consumed by Tailwind's `transition-*` utilities */
|
|
249
|
-
--default-transition-duration: var(--duration-
|
|
250
|
-
--default-transition-timing-function: var(--ease-
|
|
261
|
+
--default-transition-duration: var(--duration-standard);
|
|
262
|
+
--default-transition-timing-function: var(--ease-standard);
|
|
251
263
|
|
|
252
264
|
--animate-highlight-ring: highlight-ring 5s ease-out forwards;
|
|
253
265
|
}
|
|
@@ -296,8 +308,8 @@ h6 {
|
|
|
296
308
|
|
|
297
309
|
.glass-hover {
|
|
298
310
|
transition:
|
|
299
|
-
background-color var(--duration-
|
|
300
|
-
border-color var(--duration-
|
|
311
|
+
background-color var(--duration-standard) var(--ease-standard),
|
|
312
|
+
border-color var(--duration-standard) var(--ease-standard);
|
|
301
313
|
}
|
|
302
314
|
|
|
303
315
|
.glass-hover:hover {
|
|
@@ -407,11 +419,11 @@ h6 {
|
|
|
407
419
|
}
|
|
408
420
|
|
|
409
421
|
.animate-collapsible-down {
|
|
410
|
-
animation: collapsible-down var(--duration-
|
|
422
|
+
animation: collapsible-down var(--duration-deliberate) var(--ease-enter);
|
|
411
423
|
}
|
|
412
424
|
|
|
413
425
|
.animate-collapsible-up {
|
|
414
|
-
animation: collapsible-up var(--duration-
|
|
426
|
+
animation: collapsible-up var(--duration-exit-standard) var(--ease-exit);
|
|
415
427
|
}
|
|
416
428
|
|
|
417
429
|
/* Shimmer — referenced by Skeleton (was undefined → animation was dead) */
|
|
@@ -421,13 +433,59 @@ h6 {
|
|
|
421
433
|
}
|
|
422
434
|
}
|
|
423
435
|
|
|
436
|
+
/* Live-pulse — referenced by Badge `live` variant, EventBanner live indicator,
|
|
437
|
+
and any "this is happening right now" surface. Subtle to avoid distraction. */
|
|
438
|
+
@keyframes live-pulse {
|
|
439
|
+
0%, 100% {
|
|
440
|
+
transform: scale(1);
|
|
441
|
+
opacity: 1;
|
|
442
|
+
}
|
|
443
|
+
50% {
|
|
444
|
+
transform: scale(1.08);
|
|
445
|
+
opacity: 0.7;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
.motion-live-pulse {
|
|
450
|
+
animation: live-pulse 1.8s ease-in-out infinite;
|
|
451
|
+
transform-origin: center;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/* Image-enter — gallery / hero photographs. Slight scale-down + fade so images
|
|
455
|
+
read as "arriving" rather than popping. Standalone CSS so consumers don't
|
|
456
|
+
need framer-motion on every photo. */
|
|
457
|
+
@keyframes image-enter {
|
|
458
|
+
from {
|
|
459
|
+
opacity: 0;
|
|
460
|
+
transform: scale(1.04);
|
|
461
|
+
}
|
|
462
|
+
to {
|
|
463
|
+
opacity: 1;
|
|
464
|
+
transform: scale(1);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
.motion-image-enter {
|
|
469
|
+
animation: image-enter var(--duration-standard) var(--ease-standard) both;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/* Reduced-motion: instant — no scale, no fade. */
|
|
473
|
+
@media (prefers-reduced-motion: reduce) {
|
|
474
|
+
.motion-image-enter {
|
|
475
|
+
animation: none;
|
|
476
|
+
}
|
|
477
|
+
.motion-live-pulse {
|
|
478
|
+
animation: none;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
424
482
|
/* Swiper pagination styling for experience cards */
|
|
425
483
|
.swiper-pagination-bullet {
|
|
426
484
|
background: color-mix(in oklch, var(--foreground) 30%, transparent) !important;
|
|
427
485
|
opacity: 1 !important;
|
|
428
486
|
width: 6px !important;
|
|
429
487
|
height: 6px !important;
|
|
430
|
-
transition:
|
|
488
|
+
transition: width var(--duration-standard) var(--ease-standard), background var(--duration-standard) var(--ease-standard) !important;
|
|
431
489
|
}
|
|
432
490
|
|
|
433
491
|
.swiper-pagination-bullet-active {
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import type { Transition, Variants } from "framer-motion";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* BearMenu Motion System — v1.0
|
|
5
|
+
*
|
|
6
|
+
* Three layers:
|
|
7
|
+
* 1. Primitives — durations + easing curves (raw values)
|
|
8
|
+
* 2. Springs — Framer Motion spring configs (named)
|
|
9
|
+
* 3. Archetypes — compound presets that compose primitives + springs
|
|
10
|
+
*
|
|
11
|
+
* Feature code reaches for archetypes, never primitives.
|
|
12
|
+
* Adding a new archetype is a DS-maintainer task.
|
|
13
|
+
*
|
|
14
|
+
* Companion CSS custom properties live in `./globals.css`:
|
|
15
|
+
* --duration-instant / --duration-quick / --duration-standard /
|
|
16
|
+
* --duration-deliberate / --duration-narrative /
|
|
17
|
+
* --duration-exit-quick / --duration-exit-standard
|
|
18
|
+
* --ease-enter / --ease-standard / --ease-exit
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
// ─── Layer 1: Primitives ─────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
export const duration = {
|
|
24
|
+
instant: 0.12,
|
|
25
|
+
quick: 0.18,
|
|
26
|
+
standard: 0.26,
|
|
27
|
+
deliberate: 0.36,
|
|
28
|
+
narrative: 0.5,
|
|
29
|
+
exitQuick: 0.11,
|
|
30
|
+
exitStandard: 0.21,
|
|
31
|
+
} as const;
|
|
32
|
+
|
|
33
|
+
export const ease = {
|
|
34
|
+
enter: [0.0, 0.0, 0.2, 1.0] as const,
|
|
35
|
+
standard: [0.4, 0.0, 0.2, 1.0] as const,
|
|
36
|
+
exit: [0.55, 0.0, 1.0, 0.45] as const,
|
|
37
|
+
} as const;
|
|
38
|
+
|
|
39
|
+
// ─── Layer 2: Springs ────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
export const springSnappy: Transition = {
|
|
42
|
+
type: "spring",
|
|
43
|
+
damping: 30,
|
|
44
|
+
stiffness: 380,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const springSmooth: Transition = {
|
|
48
|
+
type: "spring",
|
|
49
|
+
damping: 30,
|
|
50
|
+
stiffness: 220,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const springSoft: Transition = {
|
|
54
|
+
type: "spring",
|
|
55
|
+
damping: 35,
|
|
56
|
+
stiffness: 140,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/** @deprecated use `springSoft`. Kept as alias during migration. */
|
|
60
|
+
export const springGentle = springSoft;
|
|
61
|
+
|
|
62
|
+
// ─── Layer 3: Archetypes (composed presets) ──────────────────────────────
|
|
63
|
+
|
|
64
|
+
export const archetype = {
|
|
65
|
+
/** Drawer / Sheet / Dialog entrance. Surface arrives first; content layers after. */
|
|
66
|
+
surfaceOpen: { ...springSmooth } as Transition,
|
|
67
|
+
surfaceClose: { duration: duration.exitStandard, ease: ease.exit } as Transition,
|
|
68
|
+
|
|
69
|
+
/** First-reveal stagger for card grids. Per-item spring. Cap stagger at 8 items. */
|
|
70
|
+
listReveal: { ...springSoft } as Transition,
|
|
71
|
+
listRevealStagger: 0.04,
|
|
72
|
+
listRevealMaxItems: 8,
|
|
73
|
+
|
|
74
|
+
/** Tab content swap, skeleton → data swap. Opacity-only — no transform. */
|
|
75
|
+
contentSwap: { duration: duration.quick, ease: ease.standard } as Transition,
|
|
76
|
+
contentSwapExit: { duration: duration.exitQuick, ease: ease.exit } as Transition,
|
|
77
|
+
|
|
78
|
+
/** Bottom-sheet / side-drawer entry. */
|
|
79
|
+
edgeEnter: { ...springSmooth } as Transition,
|
|
80
|
+
edgeExit: { duration: duration.exitStandard, ease: ease.exit } as Transition,
|
|
81
|
+
|
|
82
|
+
/** Card / button tap feedback. */
|
|
83
|
+
pressResponse: { duration: duration.instant, ease: ease.enter } as Transition,
|
|
84
|
+
|
|
85
|
+
/** Active tab pill, active filter chip — use with Framer Motion `layoutId`. */
|
|
86
|
+
indicatorMorph: { ...springSnappy } as Transition,
|
|
87
|
+
|
|
88
|
+
/** Image enter — slight scale on load. */
|
|
89
|
+
imageEnter: { duration: 0.3, ease: ease.standard } as Transition,
|
|
90
|
+
} as const;
|
|
91
|
+
|
|
92
|
+
// ─── Stagger gaps (seconds, for `staggerChildren`) ───────────────────────
|
|
93
|
+
|
|
94
|
+
export const stagger = {
|
|
95
|
+
tight: 0.03,
|
|
96
|
+
standard: 0.04,
|
|
97
|
+
loose: 0.06,
|
|
98
|
+
} as const;
|
|
99
|
+
|
|
100
|
+
// ─── Variant builders (common patterns) ──────────────────────────────────
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* List-reveal container + item variants. Pair with Framer Motion `motion.ul`/`motion.div`.
|
|
104
|
+
* Container drives stagger; item handles the actual fade + Y-shift.
|
|
105
|
+
*/
|
|
106
|
+
export const listRevealVariants: { container: Variants; item: Variants } = {
|
|
107
|
+
container: {
|
|
108
|
+
hidden: { opacity: 1 },
|
|
109
|
+
show: {
|
|
110
|
+
opacity: 1,
|
|
111
|
+
transition: { staggerChildren: stagger.standard },
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
item: {
|
|
115
|
+
hidden: { opacity: 0, y: 8 },
|
|
116
|
+
show: { opacity: 1, y: 0, transition: archetype.listReveal },
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Content-swap variants for `AnimatePresence mode="wait"`. Opacity + tiny Y shift.
|
|
122
|
+
* Use for tab panels.
|
|
123
|
+
*/
|
|
124
|
+
export const contentSwapVariants: Variants = {
|
|
125
|
+
hidden: { opacity: 0, y: 6 },
|
|
126
|
+
show: { opacity: 1, y: 0, transition: archetype.contentSwap },
|
|
127
|
+
exit: { opacity: 0, y: -4, transition: archetype.contentSwapExit },
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Skeleton → data swap. Opacity-only to prevent layout shift on slow networks.
|
|
132
|
+
*/
|
|
133
|
+
export const skeletonSwapVariants: Variants = {
|
|
134
|
+
hidden: { opacity: 0 },
|
|
135
|
+
show: { opacity: 1, transition: archetype.contentSwap },
|
|
136
|
+
exit: { opacity: 0, transition: archetype.contentSwapExit },
|
|
137
|
+
};
|