astro-magic-move 0.1.2 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astro-magic-move",
3
- "version": "0.1.2",
3
+ "version": "0.2.1",
4
4
  "description": "Animated code morphing for Astro, powered by Shiki Magic Move. Build-time tokenization, zero-framework client JS, CSS-variable theming.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -16,6 +16,7 @@ const {
16
16
  stagger = 0.3,
17
17
  threshold = 0.4,
18
18
  lineNumbers = false,
19
+ minLines,
19
20
  class: className,
20
21
  } = Astro.props;
21
22
 
@@ -66,6 +67,7 @@ const stepsJson = JSON.stringify(compiledSteps);
66
67
  data-threshold={threshold}
67
68
  data-lang={lang}
68
69
  {...(lineNumbers ? { "data-line-numbers": "" } : {})}
70
+ {...(minLines ? { style: `--min-lines: ${minLines}` } : {})}
69
71
  >
70
72
  <pre class="shiki-magic-move-container"><code>{steps[0]}</code></pre>
71
73
  <script is:inline type="application/json" set:html={stepsJson} />
@@ -76,14 +78,30 @@ const stepsJson = JSON.stringify(compiledSteps);
76
78
 
77
79
  class MagicMoveElement extends HTMLElement {
78
80
  private renderer: MagicMoveRenderer | null = null;
79
- private steps: any[] = [];
80
- private currentStep = 0;
81
+ private _steps: any[] = [];
82
+ private _currentStep = 0;
83
+ private _animating = false;
84
+ private _pendingStep: number | null = null;
81
85
  private observer: IntersectionObserver | null = null;
82
86
 
87
+ get step(): number {
88
+ return this._currentStep;
89
+ }
90
+
91
+ set step(value: number) {
92
+ const idx = Number(value);
93
+ if (Number.isNaN(idx)) return;
94
+ this.animateToStep(idx);
95
+ }
96
+
97
+ get totalSteps(): number {
98
+ return this._steps.length;
99
+ }
100
+
83
101
  connectedCallback() {
84
102
  const dataScript = this.querySelector('script[type="application/json"]');
85
103
  if (!dataScript?.textContent) return;
86
- this.steps = JSON.parse(dataScript.textContent);
104
+ this._steps = JSON.parse(dataScript.textContent);
87
105
  dataScript.remove();
88
106
 
89
107
  const container = this.querySelector("pre");
@@ -104,10 +122,10 @@ const stepsJson = JSON.stringify(compiledSteps);
104
122
  });
105
123
 
106
124
  if (trigger === "auto") {
107
- this.currentStep = -1;
125
+ this._currentStep = -1;
108
126
  } else {
109
- this.renderer.replace(this.steps[0]);
110
- this.currentStep = 0;
127
+ this.renderer.replace(this._steps[0]);
128
+ this._currentStep = 0;
111
129
  }
112
130
  this.setupTrigger(trigger, threshold);
113
131
  }
@@ -124,13 +142,15 @@ const stepsJson = JSON.stringify(compiledSteps);
124
142
  case "click":
125
143
  this.style.cursor = "pointer";
126
144
  this.addEventListener("click", () => {
127
- const next = (this.currentStep + 1) % this.steps.length;
145
+ const next = (this._currentStep + 1) % this._steps.length;
128
146
  this.animateToStep(next);
129
147
  });
130
148
  break;
131
149
  case "auto":
132
150
  requestAnimationFrame(() => this.autoPlay());
133
151
  break;
152
+ case "none":
153
+ break;
134
154
  }
135
155
  }
136
156
 
@@ -148,28 +168,43 @@ const stepsJson = JSON.stringify(compiledSteps);
148
168
  }
149
169
 
150
170
  private async autoPlay() {
151
- const duration = Number(this.dataset.duration ?? 800);
152
- for (let i = 0; i < this.steps.length; i++) {
153
- await this.animateToStep(i);
154
- if (i < this.steps.length - 1) {
155
- await new Promise((r) => setTimeout(r, duration + 200));
156
- }
171
+ const duration = Number(this.dataset.duration ?? 800);
172
+ for (let i = 0; i < this._steps.length; i++) {
173
+ await this.animateToStep(i);
174
+ if (i < this._steps.length - 1) {
175
+ await new Promise((r) => setTimeout(r, duration + 200));
157
176
  }
158
177
  }
178
+ }
159
179
 
160
- private async animateToStep(stepIndex: number) {
161
- if (!this.renderer || stepIndex === this.currentStep) return;
162
- if (stepIndex < 0 || stepIndex >= this.steps.length) return;
180
+ private async animateToStep(stepIndex: number) {
181
+ if (!this.renderer || stepIndex === this._currentStep) return;
182
+ if (stepIndex < 0 || stepIndex >= this._steps.length) return;
163
183
 
164
- await this.renderer.render(this.steps[stepIndex]);
165
- this.currentStep = stepIndex;
184
+ if (this._animating) {
185
+ this._pendingStep = stepIndex;
186
+ return;
187
+ }
166
188
 
167
- this.dispatchEvent(
168
- new CustomEvent("magic-move:step", {
169
- detail: { step: stepIndex, total: this.steps.length },
170
- bubbles: true,
171
- }),
172
- );
189
+ this._animating = true;
190
+ try {
191
+ await this.renderer.render(this._steps[stepIndex]);
192
+ this._currentStep = stepIndex;
193
+ this.dispatchEvent(
194
+ new CustomEvent("magic-move:step", {
195
+ detail: { step: stepIndex, total: this._steps.length },
196
+ bubbles: true,
197
+ }),
198
+ );
199
+ } finally {
200
+ this._animating = false;
201
+ }
202
+
203
+ if (this._pendingStep !== null) {
204
+ const next = this._pendingStep;
205
+ this._pendingStep = null;
206
+ this.animateToStep(next);
207
+ }
173
208
  }
174
209
  }
175
210
 
package/src/types.ts CHANGED
@@ -20,8 +20,9 @@ export interface MagicMoveProps {
20
20
  * - `'scroll'`: animate when element enters viewport (default)
21
21
  * - `'click'`: toggle forward on click, wraps around
22
22
  * - `'auto'`: animate immediately when the element mounts
23
+ * - `'none'`: no built-in trigger; control steps externally via the element's `step` property
23
24
  */
24
- trigger?: "scroll" | "click" | "auto";
25
+ trigger?: "scroll" | "click" | "auto" | "none";
25
26
 
26
27
  /** Animation duration in ms. Default: `800` */
27
28
  duration?: number;
@@ -38,6 +39,12 @@ export interface MagicMoveProps {
38
39
  /** Whether to animate line numbers. Default: `false` */
39
40
  lineNumbers?: boolean;
40
41
 
42
+ /**
43
+ * Minimum height expressed as a number of lines.
44
+ * Prevents layout shift when stepping between code blocks of different lengths.
45
+ */
46
+ minLines?: number;
47
+
41
48
  /**
42
49
  * CSS class(es) applied to the outer `<magic-move>` element.
43
50
  * Use this for all visual styling (bg, border, rounded, shadow, padding, width, etc).
@@ -21,4 +21,5 @@ magic-move {
21
21
  background: var(--shiki-background, #1e1e1e);
22
22
  color: var(--shiki-foreground, #d4d4d4);
23
23
  scrollbar-width: thin;
24
+ min-height: calc(1.5rem + var(--min-lines, 0) * 1.7em);
24
25
  }