beez-ui 0.5.6 → 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.
@@ -0,0 +1,13 @@
1
+ /** Resolves entry and exit playback for floating and modal surfaces from their rendered placement. */
2
+ import type { AnimationOptions, DOMKeyframesDefinition } from "motion/react";
3
+ /** Surface kinds whose presence is animated on mount and before removal. */
4
+ export type SurfaceKind = "tooltip" | "surface" | "dialog" | "sheet" | "overlay";
5
+ /** Keyframes plus the transition that plays them. */
6
+ export interface MotionStep {
7
+ keyframes: DOMKeyframesDefinition;
8
+ transition: AnimationOptions;
9
+ }
10
+ /** Entrance playback matching the beui surface family for each kind. */
11
+ export declare function surfaceEnter(element: Element, kind: SurfaceKind): MotionStep;
12
+ /** Exit playback: brief and eased so dismissal never blocks the next interaction. */
13
+ export declare function surfaceExit(element: Element, kind: SurfaceKind): MotionStep;
@@ -0,0 +1,205 @@
1
+ import { MOTION_DIALOG_DISTANCE, MOTION_EASE, MOTION_SURFACE_CLIP_PERCENT, MOTION_SURFACE_SCALE, MOTION_TIMING, MOTION_TOOLTIP_DISTANCE, SPRING_PANEL, SPRING_TOOLTIP } from "./tokens.js";
2
+ const EASE_OUT = [...MOTION_EASE];
3
+ const SIDES = [
4
+ "top",
5
+ "right",
6
+ "bottom",
7
+ "left"
8
+ ];
9
+ const ALIGNS = [
10
+ "start",
11
+ "center",
12
+ "end"
13
+ ];
14
+ /** Reads Radix placement attributes, tolerating primitives that do not position with a popper. */
15
+ function readPlacement(element) {
16
+ const side = element.getAttribute("data-side");
17
+ const align = element.getAttribute("data-align");
18
+ return {
19
+ side: SIDES.includes(side) ? side : undefined,
20
+ align: ALIGNS.includes(align) ? align : "center"
21
+ };
22
+ }
23
+ /** Builds a transform with explicit identity parts, so Motion never reads a missing scale as zero. */
24
+ function transform(x, y, scale = 1) {
25
+ const unit = (value) => typeof value === "number" ? `${value}px` : value;
26
+ return `translate(${unit(x)}, ${unit(y)}) scale(${scale})`;
27
+ }
28
+ /** Offset pointing away from the trigger, so content appears to emerge from it. */
29
+ function offsetAwayFromTrigger(side, distance) {
30
+ if (side === "top") return {
31
+ x: 0,
32
+ y: distance
33
+ };
34
+ if (side === "left") return {
35
+ x: distance,
36
+ y: 0
37
+ };
38
+ if (side === "right") return {
39
+ x: -distance,
40
+ y: 0
41
+ };
42
+ if (side === "bottom") return {
43
+ x: 0,
44
+ y: -distance
45
+ };
46
+ return {
47
+ x: 0,
48
+ y: 0
49
+ };
50
+ }
51
+ /** Collapsed clip anchored at the corner or edge closest to the trigger. */
52
+ function collapsedClip(side, align) {
53
+ const hidden = `${MOTION_SURFACE_CLIP_PERCENT}%`;
54
+ const half = `${MOTION_SURFACE_CLIP_PERCENT / 2}%`;
55
+ /** Maps an alignment to the inset kept on the leading and trailing edges of the cross axis. */
56
+ const crossAxis = (value) => value === "start" ? ["0%", hidden] : value === "end" ? [hidden, "0%"] : [half, half];
57
+ if (side === "bottom" || side === "top") {
58
+ const [left, right] = crossAxis(align);
59
+ return side === "bottom" ? `inset(0% ${right} ${hidden} ${left})` : `inset(${hidden} ${right} 0% ${left})`;
60
+ }
61
+ const [top, bottom] = crossAxis(align);
62
+ return side === "right" ? `inset(${top} ${hidden} ${bottom} 0%)` : `inset(${top} 0% ${bottom} ${hidden})`;
63
+ }
64
+ /** Edge-panel travel: fully off-screen on the side the sheet is attached to. */
65
+ function sheetOffset(element) {
66
+ const side = readPlacement(element).side ?? "right";
67
+ if (side === "left") return {
68
+ x: "-100%",
69
+ y: "0%"
70
+ };
71
+ if (side === "top") return {
72
+ x: "0%",
73
+ y: "-100%"
74
+ };
75
+ if (side === "bottom") return {
76
+ x: "0%",
77
+ y: "100%"
78
+ };
79
+ return {
80
+ x: "100%",
81
+ y: "0%"
82
+ };
83
+ }
84
+ /** Entrance playback matching the beui surface family for each kind. */
85
+ export function surfaceEnter(element, kind) {
86
+ const { side, align } = readPlacement(element);
87
+ if (kind === "overlay") {
88
+ return {
89
+ keyframes: { opacity: [0, 1] },
90
+ transition: {
91
+ duration: MOTION_TIMING.overlay,
92
+ ease: EASE_OUT
93
+ }
94
+ };
95
+ }
96
+ if (kind === "tooltip") {
97
+ const offset = offsetAwayFromTrigger(side, MOTION_TOOLTIP_DISTANCE);
98
+ return {
99
+ keyframes: {
100
+ opacity: [0, 1],
101
+ transform: [transform(offset.x, offset.y, .9), transform(0, 0)],
102
+ filter: ["blur(5px)", "blur(0px)"]
103
+ },
104
+ transition: {
105
+ ...SPRING_TOOLTIP,
106
+ opacity: {
107
+ duration: MOTION_TIMING.fast,
108
+ ease: EASE_OUT
109
+ },
110
+ filter: {
111
+ duration: MOTION_TIMING.enter,
112
+ ease: EASE_OUT
113
+ }
114
+ }
115
+ };
116
+ }
117
+ if (kind === "dialog") {
118
+ return {
119
+ keyframes: {
120
+ opacity: [0, 1],
121
+ transform: [transform(0, MOTION_DIALOG_DISTANCE, .97), transform(0, 0)]
122
+ },
123
+ transition: SPRING_PANEL
124
+ };
125
+ }
126
+ if (kind === "sheet") {
127
+ const offset = sheetOffset(element);
128
+ return {
129
+ keyframes: { transform: [transform(offset.x, offset.y), transform("0%", "0%")] },
130
+ transition: SPRING_PANEL
131
+ };
132
+ }
133
+ const keyframes = {
134
+ opacity: [0, 1],
135
+ transform: [transform(0, 0, MOTION_SURFACE_SCALE), transform(0, 0)]
136
+ };
137
+ // Primitives aligned over their trigger (Select item-aligned) have no side to reveal from.
138
+ if (side) {
139
+ keyframes.clipPath = [collapsedClip(side, align), "inset(0% 0% 0% 0%)"];
140
+ }
141
+ return {
142
+ keyframes,
143
+ transition: {
144
+ ...SPRING_PANEL,
145
+ opacity: {
146
+ duration: MOTION_TIMING.enter,
147
+ ease: EASE_OUT
148
+ },
149
+ clipPath: {
150
+ duration: MOTION_TIMING.surfaceClip,
151
+ ease: EASE_OUT
152
+ }
153
+ }
154
+ };
155
+ }
156
+ /** Exit playback: brief and eased so dismissal never blocks the next interaction. */
157
+ export function surfaceExit(element, kind) {
158
+ const exitTween = {
159
+ duration: MOTION_TIMING.exit,
160
+ ease: EASE_OUT
161
+ };
162
+ if (kind === "overlay") {
163
+ return {
164
+ keyframes: { opacity: [1, 0] },
165
+ transition: exitTween
166
+ };
167
+ }
168
+ if (kind === "tooltip") {
169
+ const offset = offsetAwayFromTrigger(readPlacement(element).side, MOTION_TOOLTIP_DISTANCE * .6);
170
+ return {
171
+ keyframes: {
172
+ opacity: [1, 0],
173
+ transform: [transform(0, 0), transform(offset.x, offset.y, .94)],
174
+ filter: ["blur(0px)", "blur(3px)"]
175
+ },
176
+ transition: exitTween
177
+ };
178
+ }
179
+ if (kind === "dialog") {
180
+ return {
181
+ keyframes: {
182
+ opacity: [1, 0],
183
+ transform: [transform(0, 0), transform(0, MOTION_DIALOG_DISTANCE, .98)]
184
+ },
185
+ transition: {
186
+ duration: MOTION_TIMING.enter,
187
+ ease: EASE_OUT
188
+ }
189
+ };
190
+ }
191
+ if (kind === "sheet") {
192
+ const offset = sheetOffset(element);
193
+ return {
194
+ keyframes: { transform: [transform("0%", "0%"), transform(offset.x, offset.y)] },
195
+ transition: SPRING_PANEL
196
+ };
197
+ }
198
+ return {
199
+ keyframes: {
200
+ opacity: [1, 0],
201
+ transform: [transform(0, 0), transform(0, 0, MOTION_SURFACE_SCALE)]
202
+ },
203
+ transition: exitTween
204
+ };
205
+ }
@@ -1,13 +1,96 @@
1
- /** Defines the restrained timing and movement shared by Motion animations. */
1
+ /** Defines the timing, curves and physics shared by Motion animations, aligned with beui.dev/components/motion. */
2
2
  export declare const MOTION_TIMING: {
3
3
  readonly fast: 0.14;
4
4
  readonly enter: 0.18;
5
+ readonly message: 0.2;
5
6
  readonly panel: 0.22;
7
+ readonly overlay: 0.2;
6
8
  readonly exit: 0.12;
9
+ readonly surfaceClip: 0.32;
10
+ readonly shake: 0.45;
11
+ readonly checkDraw: 0.3;
12
+ readonly checkDrawDelay: 0.04;
13
+ readonly iconSwap: 0.2;
14
+ readonly listItem: 0.18;
15
+ readonly listDelay: 0.05;
16
+ readonly listStagger: 0.035;
17
+ readonly closeEnter: 0.2;
18
+ readonly closeDelay: 0.16;
7
19
  readonly skeleton: 1.8;
8
20
  };
9
- export declare const MOTION_EASE: readonly [0.22, 1, 0.36, 1];
10
- export declare const MOTION_PRESS_SCALE = 0.98;
11
- export declare const MOTION_SURFACE_DISTANCE = 4;
12
- export declare const MOTION_PANEL_DISTANCE = 12;
13
- export type MotionKind = "press" | "fade" | "card" | "row" | "field" | "thumb" | "selection" | "skeleton" | "link" | "icon" | "surface" | "dialog" | "sheet" | "overlay";
21
+ /** Items beyond this position share the last stagger delay, so long lists never lag behind. */
22
+ export declare const MOTION_LIST_STAGGER_LIMIT = 10;
23
+ /** Strong ease-out used for entrances; weak defaults such as `ease-out` feel sluggish. */
24
+ export declare const MOTION_EASE: readonly [0.16, 1, 0.3, 1];
25
+ /** Symmetric curve for looping or reversible effects. */
26
+ export declare const MOTION_EASE_IN_OUT: readonly [0.77, 0, 0.175, 1];
27
+ /** Drawer curve for edge panels leaving the viewport. */
28
+ export declare const MOTION_EASE_DRAWER: readonly [0.32, 0.72, 0, 1];
29
+ /** Press feedback on buttons and other tappable surfaces. */
30
+ export declare const SPRING_PRESS: {
31
+ readonly type: "spring";
32
+ readonly stiffness: 500;
33
+ readonly damping: 30;
34
+ readonly mass: 0.6;
35
+ };
36
+ /** Overlay panel entrances — dialogs, sheets and floating surfaces. */
37
+ export declare const SPRING_PANEL: {
38
+ readonly type: "spring";
39
+ readonly stiffness: 420;
40
+ readonly damping: 40;
41
+ readonly mass: 0.5;
42
+ };
43
+ /** Shared-layout glides — indicators and highlights moving between items. */
44
+ export declare const SPRING_LAYOUT: {
45
+ readonly type: "spring";
46
+ readonly stiffness: 360;
47
+ readonly damping: 32;
48
+ readonly mass: 0.6;
49
+ };
50
+ /** Tab indicator glide, tuned to settle without overshooting the list edges. */
51
+ export declare const SPRING_TABS: {
52
+ readonly type: "spring";
53
+ readonly stiffness: 245;
54
+ readonly damping: 36;
55
+ readonly mass: 1.2;
56
+ };
57
+ /** Lighter spawn for small tooltip surfaces. */
58
+ export declare const SPRING_TOOLTIP: {
59
+ readonly type: "spring";
60
+ readonly stiffness: 380;
61
+ readonly damping: 30;
62
+ readonly mass: 0.7;
63
+ };
64
+ /** Disclosure chevrons rotate with a little bounce, as if flicked. */
65
+ export declare const SPRING_CHEVRON: {
66
+ readonly type: "spring";
67
+ readonly duration: 0.4;
68
+ readonly bounce: 0.3;
69
+ };
70
+ /** Heavy, deliberate switch thumb — high mass keeps the travel weighty without wobble. */
71
+ export declare const SPRING_THUMB: {
72
+ readonly type: "spring";
73
+ readonly stiffness: 800;
74
+ readonly damping: 80;
75
+ readonly mass: 4;
76
+ };
77
+ /** Primary actions compress noticeably; dense navigation items stay subtle. */
78
+ export declare const MOTION_PRESS_SCALE = 0.93;
79
+ export declare const MOTION_SUBTLE_PRESS_SCALE = 0.98;
80
+ export declare const MOTION_TOGGLE_PRESS_SCALE = 0.92;
81
+ export declare const MOTION_HOVER_SCALE = 1.02;
82
+ export declare const MOTION_THUMB_SQUISH_SCALE = 0.9;
83
+ export declare const MOTION_ITEM_CHECK_SCALE = 0.75;
84
+ export declare const MOTION_ICON_SWAP_SCALE = 0.25;
85
+ export declare const MOTION_CLOSE_BUTTON_SCALE = 0.8;
86
+ export declare const MOTION_LIST_ITEM_DISTANCE = 6;
87
+ export declare const MOTION_SURFACE_SCALE = 0.96;
88
+ /** Fraction of the surface kept hidden by the clip at the start of its reveal, in percent. */
89
+ export declare const MOTION_SURFACE_CLIP_PERCENT = 92;
90
+ export declare const MOTION_TOOLTIP_DISTANCE = 8;
91
+ export declare const MOTION_DIALOG_DISTANCE = 20;
92
+ export declare const MOTION_CONTENT_DISTANCE = 4;
93
+ export declare const MOTION_INVALID_SHAKE_PX: readonly [0, -6, 6, -4, 4, -2, 0];
94
+ /** Time after an item loses its active state during which a new active item still glides from it. */
95
+ export declare const GLIDE_CONTINUITY_MS = 300;
96
+ export type MotionKind = "press" | "subtle-press" | "toggle" | "fade" | "content" | "message" | "card" | "row" | "field" | "thumb" | "check" | "selection" | "item-check" | "rotate" | "list-item" | "icon-swap" | "delayed-pop" | "skeleton" | "link" | "icon" | "tooltip" | "surface" | "dialog" | "sheet" | "overlay";
@@ -1,17 +1,118 @@
1
- /** Defines the restrained timing and movement shared by Motion animations. */
1
+ /** Defines the timing, curves and physics shared by Motion animations, aligned with beui.dev/components/motion. */
2
2
  export const MOTION_TIMING = {
3
3
  fast: .14,
4
4
  enter: .18,
5
+ message: .2,
5
6
  panel: .22,
7
+ overlay: .2,
6
8
  exit: .12,
9
+ surfaceClip: .32,
10
+ shake: .45,
11
+ checkDraw: .3,
12
+ checkDrawDelay: .04,
13
+ iconSwap: .2,
14
+ listItem: .18,
15
+ listDelay: .05,
16
+ listStagger: .035,
17
+ closeEnter: .2,
18
+ closeDelay: .16,
7
19
  skeleton: 1.8
8
20
  };
21
+ /** Items beyond this position share the last stagger delay, so long lists never lag behind. */
22
+ export const MOTION_LIST_STAGGER_LIMIT = 10;
23
+ /** Strong ease-out used for entrances; weak defaults such as `ease-out` feel sluggish. */
9
24
  export const MOTION_EASE = [
10
- .22,
25
+ .16,
11
26
  1,
12
- .36,
27
+ .3,
13
28
  1
14
29
  ];
15
- export const MOTION_PRESS_SCALE = .98;
16
- export const MOTION_SURFACE_DISTANCE = 4;
17
- export const MOTION_PANEL_DISTANCE = 12;
30
+ /** Symmetric curve for looping or reversible effects. */
31
+ export const MOTION_EASE_IN_OUT = [
32
+ .77,
33
+ 0,
34
+ .175,
35
+ 1
36
+ ];
37
+ /** Drawer curve for edge panels leaving the viewport. */
38
+ export const MOTION_EASE_DRAWER = [
39
+ .32,
40
+ .72,
41
+ 0,
42
+ 1
43
+ ];
44
+ /** Press feedback on buttons and other tappable surfaces. */
45
+ export const SPRING_PRESS = {
46
+ type: "spring",
47
+ stiffness: 500,
48
+ damping: 30,
49
+ mass: .6
50
+ };
51
+ /** Overlay panel entrances — dialogs, sheets and floating surfaces. */
52
+ export const SPRING_PANEL = {
53
+ type: "spring",
54
+ stiffness: 420,
55
+ damping: 40,
56
+ mass: .5
57
+ };
58
+ /** Shared-layout glides — indicators and highlights moving between items. */
59
+ export const SPRING_LAYOUT = {
60
+ type: "spring",
61
+ stiffness: 360,
62
+ damping: 32,
63
+ mass: .6
64
+ };
65
+ /** Tab indicator glide, tuned to settle without overshooting the list edges. */
66
+ export const SPRING_TABS = {
67
+ type: "spring",
68
+ stiffness: 245,
69
+ damping: 36,
70
+ mass: 1.2
71
+ };
72
+ /** Lighter spawn for small tooltip surfaces. */
73
+ export const SPRING_TOOLTIP = {
74
+ type: "spring",
75
+ stiffness: 380,
76
+ damping: 30,
77
+ mass: .7
78
+ };
79
+ /** Disclosure chevrons rotate with a little bounce, as if flicked. */
80
+ export const SPRING_CHEVRON = {
81
+ type: "spring",
82
+ duration: .4,
83
+ bounce: .3
84
+ };
85
+ /** Heavy, deliberate switch thumb — high mass keeps the travel weighty without wobble. */
86
+ export const SPRING_THUMB = {
87
+ type: "spring",
88
+ stiffness: 800,
89
+ damping: 80,
90
+ mass: 4
91
+ };
92
+ /** Primary actions compress noticeably; dense navigation items stay subtle. */
93
+ export const MOTION_PRESS_SCALE = .93;
94
+ export const MOTION_SUBTLE_PRESS_SCALE = .98;
95
+ export const MOTION_TOGGLE_PRESS_SCALE = .92;
96
+ export const MOTION_HOVER_SCALE = 1.02;
97
+ export const MOTION_THUMB_SQUISH_SCALE = .9;
98
+ export const MOTION_ITEM_CHECK_SCALE = .75;
99
+ export const MOTION_ICON_SWAP_SCALE = .25;
100
+ export const MOTION_CLOSE_BUTTON_SCALE = .8;
101
+ export const MOTION_LIST_ITEM_DISTANCE = 6;
102
+ export const MOTION_SURFACE_SCALE = .96;
103
+ /** Fraction of the surface kept hidden by the clip at the start of its reveal, in percent. */
104
+ export const MOTION_SURFACE_CLIP_PERCENT = 92;
105
+ export const MOTION_TOOLTIP_DISTANCE = 8;
106
+ export const MOTION_DIALOG_DISTANCE = 20;
107
+ export const MOTION_CONTENT_DISTANCE = 4;
108
+ export const MOTION_INVALID_SHAKE_PX = [
109
+ 0,
110
+ -6,
111
+ 6,
112
+ -4,
113
+ 4,
114
+ -2,
115
+ 0
116
+ ];
117
+ /** Time after an item loses its active state during which a new active item still glides from it. */
118
+ export const GLIDE_CONTINUITY_MS = 300;
@@ -0,0 +1,7 @@
1
+ /** Tracks the DOM node rendered through a Slot while forwarding it to the caller's ref. */
2
+ import { type Ref } from "react";
3
+ /**
4
+ * Returns the attached element and a callback ref that also feeds the caller's ref,
5
+ * honoring React 19 ref cleanups.
6
+ */
7
+ export declare function useAttachedElement<T extends HTMLElement>(ref: Ref<T> | undefined): readonly [T | null, (node: T | null) => () => void];
@@ -0,0 +1,22 @@
1
+ "use client";
2
+ /** Tracks the DOM node rendered through a Slot while forwarding it to the caller's ref. */
3
+ import { useCallback, useState } from "react";
4
+ /**
5
+ * Returns the attached element and a callback ref that also feeds the caller's ref,
6
+ * honoring React 19 ref cleanups.
7
+ */
8
+ export function useAttachedElement(ref) {
9
+ const [element, setElement] = useState(null);
10
+ const attach = useCallback((node) => {
11
+ setElement(node);
12
+ const cleanup = typeof ref === "function" ? ref(node) : undefined;
13
+ if (ref && typeof ref !== "function") ref.current = node;
14
+ return () => {
15
+ setElement(null);
16
+ if (typeof cleanup === "function") cleanup();
17
+ else if (typeof ref === "function") ref(null);
18
+ else if (ref) ref.current = null;
19
+ };
20
+ }, [ref]);
21
+ return [element, attach];
22
+ }