pasito 0.1.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.
@@ -0,0 +1,99 @@
1
+ "use client";
2
+
3
+ // src/core/AutoPlayController.ts
4
+ var AutoPlayController = class {
5
+ constructor({ stepDuration = 3e3, loop = true } = {}) {
6
+ this.stepDuration = stepDuration;
7
+ this.loop = loop;
8
+ }
9
+ computeNext(active, count) {
10
+ return active < count - 1 ? active + 1 : this.loop ? 0 : null;
11
+ }
12
+ };
13
+
14
+ // src/core/computeStepWindow.ts
15
+ var DOT_SIZE = 8;
16
+ var ACTIVE_WIDTH = 24;
17
+ var GAP = 6;
18
+ var SLOT_SIZE = DOT_SIZE + GAP;
19
+ function computeStepWindow(count, active, maxVisible, orientation) {
20
+ if (maxVisible == null || count <= maxVisible) {
21
+ return {
22
+ windowStart: 0,
23
+ transformValue: "none",
24
+ containerSize: void 0
25
+ };
26
+ }
27
+ const half = Math.floor(maxVisible / 2);
28
+ const windowStart = Math.max(0, Math.min(active - half, count - maxVisible));
29
+ const offset = windowStart * SLOT_SIZE;
30
+ const axis = orientation === "vertical" ? "Y" : "X";
31
+ const transformValue = `translate${axis}(-${offset}px)`;
32
+ const size = (maxVisible - 1) * DOT_SIZE + ACTIVE_WIDTH + (maxVisible - 1) * GAP;
33
+ return { windowStart, transformValue, containerSize: size };
34
+ }
35
+
36
+ // src/core/StepAnimator.ts
37
+ var StepAnimator = class {
38
+ constructor(count) {
39
+ this.keyGen = count;
40
+ this.steps = Array.from({ length: count }, (_, i) => ({
41
+ key: i,
42
+ index: i,
43
+ phase: "stable"
44
+ }));
45
+ }
46
+ reconcile(newCount) {
47
+ const liveCount = this.steps.filter((s) => s.phase !== "exiting").length;
48
+ if (liveCount === newCount) {
49
+ return {
50
+ hasEntering: this.steps.some((s) => s.phase === "entering"),
51
+ exitingCount: this.steps.filter((s) => s.phase === "exiting").length
52
+ };
53
+ }
54
+ if (newCount < liveCount) {
55
+ let seen = 0;
56
+ this.steps = this.steps.map((s) => {
57
+ if (s.phase === "exiting") return s;
58
+ seen++;
59
+ if (seen > newCount) return { ...s, phase: "exiting" };
60
+ return s;
61
+ });
62
+ }
63
+ if (newCount > liveCount) {
64
+ for (let i = liveCount; i < newCount; i++) {
65
+ this.keyGen++;
66
+ this.steps.push({
67
+ key: this.keyGen,
68
+ index: i,
69
+ phase: "entering"
70
+ });
71
+ }
72
+ }
73
+ let idx = 0;
74
+ this.steps = this.steps.map(
75
+ (s) => s.phase === "exiting" ? s : { ...s, index: idx++ }
76
+ );
77
+ return {
78
+ hasEntering: this.steps.some((s) => s.phase === "entering"),
79
+ exitingCount: this.steps.filter((s) => s.phase === "exiting").length
80
+ };
81
+ }
82
+ promoteEntering() {
83
+ this.steps = this.steps.map(
84
+ (s) => s.phase === "entering" ? { ...s, phase: "stable" } : s
85
+ );
86
+ }
87
+ removeExiting() {
88
+ this.steps = this.steps.filter((s) => s.phase !== "exiting");
89
+ }
90
+ getSteps() {
91
+ return [...this.steps];
92
+ }
93
+ };
94
+
95
+ export {
96
+ computeStepWindow,
97
+ StepAnimator,
98
+ AutoPlayController
99
+ };
@@ -0,0 +1,223 @@
1
+ "use client";
2
+ import {
3
+ AutoPlayController,
4
+ StepAnimator,
5
+ computeStepWindow
6
+ } from "./chunk-4JH2WF3D.mjs";
7
+
8
+ // src/react/hooks/useStepWindow.ts
9
+ import { useMemo } from "react";
10
+ function useStepWindow(count, active, maxVisible, orientation) {
11
+ return useMemo(
12
+ () => computeStepWindow(count, active, maxVisible, orientation),
13
+ [count, active, maxVisible, orientation]
14
+ );
15
+ }
16
+
17
+ // src/react/hooks/useAnimatingSteps.ts
18
+ import { useState, useLayoutEffect, useEffect, useRef } from "react";
19
+ function useAnimatingSteps(count, _duration) {
20
+ const animatorRef = useRef(null);
21
+ if (animatorRef.current === null) {
22
+ animatorRef.current = new StepAnimator(count);
23
+ }
24
+ const animator = animatorRef.current;
25
+ const [steps, setSteps] = useState(() => animator.getSteps());
26
+ useLayoutEffect(() => {
27
+ animator.reconcile(count);
28
+ setSteps(animator.getSteps());
29
+ }, [count, animator]);
30
+ const hasEntering = steps.some((s) => s.phase === "entering");
31
+ useEffect(() => {
32
+ if (!hasEntering) return;
33
+ let raf1;
34
+ let raf2;
35
+ raf1 = requestAnimationFrame(() => {
36
+ raf2 = requestAnimationFrame(() => {
37
+ animator.promoteEntering();
38
+ setSteps(animator.getSteps());
39
+ });
40
+ });
41
+ return () => {
42
+ cancelAnimationFrame(raf1);
43
+ cancelAnimationFrame(raf2);
44
+ };
45
+ }, [hasEntering, animator]);
46
+ const exitingCount = steps.filter((s) => s.phase === "exiting").length;
47
+ useEffect(() => {
48
+ if (exitingCount === 0) return;
49
+ const timer = setTimeout(() => {
50
+ animator.removeExiting();
51
+ setSteps(animator.getSteps());
52
+ }, 300);
53
+ return () => clearTimeout(timer);
54
+ }, [exitingCount, animator]);
55
+ return steps;
56
+ }
57
+
58
+ // src/react/PillStep.tsx
59
+ import { jsx } from "react/jsx-runtime";
60
+ function PillStep({
61
+ index,
62
+ isActive,
63
+ phase,
64
+ transitionDuration,
65
+ filling,
66
+ fillDuration,
67
+ onClick
68
+ }) {
69
+ const classNames = [
70
+ "pasito-step",
71
+ isActive && "pasito-step-active",
72
+ isActive && filling && "pasito-step-filling",
73
+ phase === "entering" && "pasito-entering",
74
+ phase === "exiting" && "pasito-exiting"
75
+ ].filter(Boolean).join(" ");
76
+ const style = {
77
+ "--pill-duration": `${transitionDuration}ms`
78
+ };
79
+ if (isActive && filling && fillDuration) {
80
+ style["--pill-fill-duration"] = `${fillDuration}ms`;
81
+ }
82
+ return /* @__PURE__ */ jsx(
83
+ "button",
84
+ {
85
+ className: classNames,
86
+ style,
87
+ onClick,
88
+ role: "tab",
89
+ "aria-selected": isActive,
90
+ "aria-label": `Step ${index + 1}`,
91
+ tabIndex: isActive ? 0 : -1
92
+ }
93
+ );
94
+ }
95
+
96
+ // src/react/PillStepper.tsx
97
+ import { jsx as jsx2 } from "react/jsx-runtime";
98
+ function PillStepper({
99
+ count,
100
+ active,
101
+ onStepClick,
102
+ orientation = "horizontal",
103
+ maxVisible,
104
+ transitionDuration = 500,
105
+ easing,
106
+ className,
107
+ filling,
108
+ fillDuration
109
+ }) {
110
+ const { transformValue, containerSize } = useStepWindow(
111
+ count,
112
+ active,
113
+ maxVisible,
114
+ orientation
115
+ );
116
+ const animatingSteps = useAnimatingSteps(count, transitionDuration);
117
+ const containerClass = [
118
+ "pasito-container",
119
+ orientation === "vertical" && "pasito-vertical",
120
+ className
121
+ ].filter(Boolean).join(" ");
122
+ const sizeStyle = {};
123
+ if (containerSize != null) {
124
+ if (orientation === "vertical") {
125
+ sizeStyle.height = containerSize;
126
+ } else {
127
+ sizeStyle.width = containerSize;
128
+ }
129
+ }
130
+ return /* @__PURE__ */ jsx2(
131
+ "div",
132
+ {
133
+ className: containerClass,
134
+ role: "tablist",
135
+ "aria-label": "Progress steps",
136
+ style: {
137
+ "--pill-duration": `${transitionDuration}ms`,
138
+ ...easing && { "--pill-easing": easing },
139
+ ...sizeStyle
140
+ },
141
+ children: /* @__PURE__ */ jsx2(
142
+ "div",
143
+ {
144
+ className: "pasito-track",
145
+ style: { transform: transformValue },
146
+ children: animatingSteps.map((step) => /* @__PURE__ */ jsx2(
147
+ PillStep,
148
+ {
149
+ index: step.index,
150
+ isActive: step.index === active,
151
+ phase: step.phase,
152
+ transitionDuration,
153
+ filling: step.index === active && filling,
154
+ fillDuration,
155
+ onClick: onStepClick ? () => onStepClick(step.index) : void 0
156
+ },
157
+ step.key
158
+ ))
159
+ }
160
+ )
161
+ }
162
+ );
163
+ }
164
+
165
+ // src/react/hooks/useAutoPlay.ts
166
+ import { useState as useState2, useEffect as useEffect2, useCallback, useRef as useRef2 } from "react";
167
+ function useAutoPlay({
168
+ count,
169
+ active,
170
+ onStepChange,
171
+ stepDuration = 3e3,
172
+ loop = true,
173
+ enabled = true
174
+ }) {
175
+ const [playing, setPlaying] = useState2(false);
176
+ const timerRef = useRef2(null);
177
+ const controllerRef = useRef2(null);
178
+ if (!controllerRef.current) {
179
+ controllerRef.current = new AutoPlayController({ stepDuration, loop });
180
+ }
181
+ controllerRef.current.stepDuration = stepDuration;
182
+ controllerRef.current.loop = loop;
183
+ const clearTimer = useCallback(() => {
184
+ if (timerRef.current) {
185
+ clearTimeout(timerRef.current);
186
+ timerRef.current = null;
187
+ }
188
+ }, []);
189
+ const toggle = useCallback(() => setPlaying((p) => !p), []);
190
+ useEffect2(() => {
191
+ if (!enabled) {
192
+ setPlaying(false);
193
+ clearTimer();
194
+ }
195
+ }, [enabled, clearTimer]);
196
+ useEffect2(() => {
197
+ if (!playing || !enabled) {
198
+ clearTimer();
199
+ return;
200
+ }
201
+ timerRef.current = setTimeout(() => {
202
+ const nextStep = controllerRef.current.computeNext(active, count);
203
+ if (nextStep !== null) {
204
+ onStepChange(nextStep);
205
+ } else {
206
+ setPlaying(false);
207
+ }
208
+ }, stepDuration);
209
+ return clearTimer;
210
+ }, [playing, enabled, active, count, stepDuration, loop, onStepChange, clearTimer]);
211
+ const isActive = playing && enabled;
212
+ return {
213
+ playing: isActive,
214
+ toggle,
215
+ filling: isActive,
216
+ fillDuration: stepDuration
217
+ };
218
+ }
219
+
220
+ export {
221
+ PillStepper,
222
+ useAutoPlay
223
+ };
package/dist/index.css ADDED
@@ -0,0 +1,125 @@
1
+ /* src/styles/PillStepper.css */
2
+ .pasito-container {
3
+ --pill-dot-size: 8px;
4
+ --pill-active-width: 24px;
5
+ --pill-gap: 6px;
6
+ --pill-duration: 500ms;
7
+ --pill-easing: cubic-bezier(0.215, 0.61, 0.355, 1);
8
+ --pill-bg: rgba(0, 0, 0, 0.12);
9
+ --pill-active-bg: rgba(0, 0, 0, 0.8);
10
+ --pill-fill-bg: rgba(255, 255, 255, 0.45);
11
+ --pill-container-bg: rgba(0, 0, 0, 0.04);
12
+ --pill-container-radius: 999px;
13
+ --pill-container-border: rgba(0, 0, 0, 0.06);
14
+ display: inline-flex;
15
+ padding: 6px 10px;
16
+ background: var(--pill-container-bg);
17
+ border-radius: var(--pill-container-radius);
18
+ border: 1px solid var(--pill-container-border);
19
+ overflow: hidden;
20
+ }
21
+ .pasito-track {
22
+ display: flex;
23
+ align-items: center;
24
+ margin-left: calc(-1 * var(--pill-gap));
25
+ transition: transform var(--pill-duration) var(--pill-easing);
26
+ }
27
+ .pasito-vertical .pasito-track {
28
+ flex-direction: column;
29
+ margin-left: 0;
30
+ margin-top: calc(-1 * var(--pill-gap));
31
+ }
32
+ .pasito-step {
33
+ position: relative;
34
+ width: var(--pill-dot-size);
35
+ height: var(--pill-dot-size);
36
+ border-radius: 999px;
37
+ border: none;
38
+ padding: 0;
39
+ cursor: pointer;
40
+ background: var(--pill-bg);
41
+ flex-shrink: 0;
42
+ overflow: hidden;
43
+ margin-left: var(--pill-gap);
44
+ margin-top: 0;
45
+ transform-origin: center center;
46
+ transition:
47
+ width var(--pill-duration) var(--pill-easing),
48
+ height var(--pill-duration) var(--pill-easing),
49
+ background var(--pill-duration) var(--pill-easing),
50
+ opacity var(--pill-duration) var(--pill-easing),
51
+ transform var(--pill-duration) var(--pill-easing),
52
+ margin-left var(--pill-duration) var(--pill-easing),
53
+ margin-top var(--pill-duration) var(--pill-easing);
54
+ }
55
+ .pasito-vertical .pasito-step {
56
+ margin-left: 0;
57
+ margin-top: var(--pill-gap);
58
+ }
59
+ .pasito-step:focus-visible {
60
+ outline: 2px solid rgba(0, 0, 0, 0.3);
61
+ outline-offset: 2px;
62
+ }
63
+ .pasito-step-active {
64
+ width: var(--pill-active-width);
65
+ background: var(--pill-active-bg);
66
+ }
67
+ .pasito-vertical .pasito-step-active {
68
+ width: var(--pill-dot-size);
69
+ height: var(--pill-active-width);
70
+ }
71
+ .pasito-step::after {
72
+ content: "";
73
+ position: absolute;
74
+ inset: 0;
75
+ border-radius: inherit;
76
+ background: var(--pill-fill-bg);
77
+ transform: scaleX(0);
78
+ transform-origin: left center;
79
+ pointer-events: none;
80
+ }
81
+ .pasito-vertical .pasito-step::after {
82
+ transform: scaleY(0);
83
+ transform-origin: center top;
84
+ }
85
+ .pasito-step-filling::after {
86
+ transform: scaleX(1);
87
+ transition: transform var(--pill-fill-duration, 3000ms) linear;
88
+ }
89
+ .pasito-vertical .pasito-step-filling::after {
90
+ transform: scaleY(1);
91
+ transition: transform var(--pill-fill-duration, 3000ms) linear;
92
+ }
93
+ .pasito-entering {
94
+ width: 0;
95
+ margin-left: 0;
96
+ margin-top: 0;
97
+ opacity: 0;
98
+ transform: scale(0);
99
+ transition-duration: 250ms;
100
+ }
101
+ .pasito-vertical .pasito-entering {
102
+ width: var(--pill-dot-size);
103
+ height: 0;
104
+ }
105
+ .pasito-exiting {
106
+ width: 0;
107
+ margin-left: 0;
108
+ margin-top: 0;
109
+ opacity: 0;
110
+ transform: scale(0);
111
+ transition-duration: 250ms;
112
+ }
113
+ .pasito-vertical .pasito-exiting {
114
+ width: var(--pill-dot-size);
115
+ height: 0;
116
+ }
117
+ @media (prefers-reduced-motion: reduce) {
118
+ .pasito-step,
119
+ .pasito-track {
120
+ transition-duration: 0ms !important;
121
+ }
122
+ .pasito-step-filling::after {
123
+ transition-duration: 0ms !important;
124
+ }
125
+ }
@@ -0,0 +1,7 @@
1
+ import { P as PillStepperProps, U as UseAutoPlayOptions, a as UseAutoPlayReturn } from './types-C_ERj5iI.mjs';
2
+
3
+ declare function PillStepper({ count, active, onStepClick, orientation, maxVisible, transitionDuration, easing, className, filling, fillDuration, }: PillStepperProps): React.ReactElement;
4
+
5
+ declare function useAutoPlay({ count, active, onStepChange, stepDuration, loop, enabled, }: UseAutoPlayOptions): UseAutoPlayReturn;
6
+
7
+ export { PillStepper, PillStepperProps, UseAutoPlayOptions, UseAutoPlayReturn, useAutoPlay };
@@ -0,0 +1,7 @@
1
+ import { P as PillStepperProps, U as UseAutoPlayOptions, a as UseAutoPlayReturn } from './types-C_ERj5iI.js';
2
+
3
+ declare function PillStepper({ count, active, onStepClick, orientation, maxVisible, transitionDuration, easing, className, filling, fillDuration, }: PillStepperProps): React.ReactElement;
4
+
5
+ declare function useAutoPlay({ count, active, onStepChange, stepDuration, loop, enabled, }: UseAutoPlayOptions): UseAutoPlayReturn;
6
+
7
+ export { PillStepper, PillStepperProps, UseAutoPlayOptions, UseAutoPlayReturn, useAutoPlay };