@inizioevoke/astro-core 2.1.2 → 2.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/docs/components/CssBackgroundImage.md +59 -0
- package/docs/components/CssVariables.md +64 -0
- package/docs/components/FlipCard.md +113 -0
- package/docs/components/Modal.md +211 -0
- package/docs/components/ScrollContainer.md +160 -0
- package/docs/components/Tabs.md +102 -0
- package/docs/integrations/prune-build.md +75 -0
- package/docs/integrations/relative-links.md +53 -0
- package/docs/lib/pinch.md +98 -0
- package/docs/lib/swipe.md +147 -0
- package/package.json +2 -1
- package/src/components/CssBackgroundImage.astro +103 -0
- package/src/components/CssVariables.astro +129 -0
- package/src/components/FlipCard/FlipCard.astro +2 -2
- package/src/components/Modal/Modal.ts +4 -4
- package/src/components/ScrollContainer/ScrollContainer.astro +5 -1
- package/src/components/ScrollContainer/ScrollContainer.ts +13 -22
- package/src/components/Tabs/TabItem.astro +1 -2
- package/src/components/Tabs/Tabs.astro +2 -3
- package/src/components/index.ts +2 -0
- package/src/integrations/prune-build.ts +33 -25
- package/src/lib/gestures/pinch.ts +62 -15
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
---
|
|
2
|
+
// See /docs/components/CssVariables.md
|
|
3
|
+
import type { ImageMetadata, ImageOutputFormat } from "astro";
|
|
4
|
+
import { getImage } from "astro:assets";
|
|
5
|
+
|
|
6
|
+
export type CssVariableType = 'image' | 'url';
|
|
7
|
+
|
|
8
|
+
export interface Breakpoint {
|
|
9
|
+
minWidth?: string | number;
|
|
10
|
+
maxWidth?: string | number;
|
|
11
|
+
value: string | number | ImageMetadata;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface CssVariable {
|
|
15
|
+
name: string;
|
|
16
|
+
type?: CssVariableType;
|
|
17
|
+
value: string | number | ImageMetadata;
|
|
18
|
+
breakpoints?: Breakpoint[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ImageCssVariable extends CssVariable {
|
|
22
|
+
type: 'image';
|
|
23
|
+
value: ImageMetadata;
|
|
24
|
+
optimize?: boolean;
|
|
25
|
+
format?: ImageOutputFormat;
|
|
26
|
+
quality?: number;
|
|
27
|
+
width?: number;
|
|
28
|
+
height?: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface Props {
|
|
32
|
+
selector?: string;
|
|
33
|
+
variables: (CssVariable | ImageCssVariable)[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const { selector, variables } = Astro.props;
|
|
37
|
+
|
|
38
|
+
const breakpoints: {
|
|
39
|
+
minWidth?: string | number,
|
|
40
|
+
maxWidth?: string | number,
|
|
41
|
+
values: {
|
|
42
|
+
name: string,
|
|
43
|
+
type?: CssVariableType,
|
|
44
|
+
value: string | number | ImageMetadata
|
|
45
|
+
}[]
|
|
46
|
+
}[] = [];
|
|
47
|
+
|
|
48
|
+
// group all the breakpoints together to minimize the output
|
|
49
|
+
for (const v of variables) {
|
|
50
|
+
if (v.breakpoints) {
|
|
51
|
+
for (const bp of v.breakpoints) {
|
|
52
|
+
const b = breakpoints.find((pt) => {
|
|
53
|
+
return pt.minWidth === bp.minWidth && pt.maxWidth === bp.maxWidth;
|
|
54
|
+
});
|
|
55
|
+
if (b) {
|
|
56
|
+
b.values.push({
|
|
57
|
+
name: v.name,
|
|
58
|
+
type: v.type,
|
|
59
|
+
value: bp.value
|
|
60
|
+
});
|
|
61
|
+
} else {
|
|
62
|
+
breakpoints.push({
|
|
63
|
+
minWidth: bp.minWidth,
|
|
64
|
+
maxWidth: bp.maxWidth,
|
|
65
|
+
values: [{
|
|
66
|
+
name: v.name,
|
|
67
|
+
type: v.type,
|
|
68
|
+
value: bp.value
|
|
69
|
+
}]
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function createVariable(v: ImageCssVariable | CssVariable) {
|
|
77
|
+
switch (v.type) {
|
|
78
|
+
case 'image': {
|
|
79
|
+
const iv = v as ImageCssVariable;
|
|
80
|
+
if (iv.optimize !== false) {
|
|
81
|
+
const img = await getImage({
|
|
82
|
+
src: iv.value,
|
|
83
|
+
format: iv.format ?? iv.value.format,
|
|
84
|
+
quality: iv.quality,
|
|
85
|
+
width: iv.width,
|
|
86
|
+
height: iv.height
|
|
87
|
+
});
|
|
88
|
+
return `--${v.name}:url("${img.src}");`;
|
|
89
|
+
} else {
|
|
90
|
+
return `--${v.name}:url("${iv.value.src}");`;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
case 'url':
|
|
94
|
+
return `--${v.name}:url("${v.value}");`;
|
|
95
|
+
default:
|
|
96
|
+
return `--${v.name}:${v.value};`;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const styles: string[] = [];
|
|
101
|
+
for (const v of variables) {
|
|
102
|
+
styles.push(await createVariable(v));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
for (const bp of breakpoints) {
|
|
106
|
+
const widths = [];
|
|
107
|
+
if (bp.minWidth) {
|
|
108
|
+
widths.push(`(min-width:${typeof bp.minWidth == 'number' ? `${bp.minWidth}px` : bp.minWidth})`);
|
|
109
|
+
}
|
|
110
|
+
if (bp.maxWidth) {
|
|
111
|
+
widths.push(`(max-width:${typeof bp.maxWidth == 'number' ? `${bp.maxWidth}px` : bp.maxWidth})`);
|
|
112
|
+
}
|
|
113
|
+
if (widths.length > 0) {
|
|
114
|
+
styles.push(`@media screen and ${widths.join(' and ')}{`);
|
|
115
|
+
for (const v of bp.values) {
|
|
116
|
+
styles.push(await createVariable(v));
|
|
117
|
+
}
|
|
118
|
+
styles.push('}');
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const html = [`${import.meta.env.PROD ? '' : '\n'}${selector ?? ':root'}{`]
|
|
123
|
+
.concat(styles.map((s) => {
|
|
124
|
+
return `${s}`;
|
|
125
|
+
}))
|
|
126
|
+
.concat(['}'])
|
|
127
|
+
.join(import.meta.env.PROD ? '' : '\n');
|
|
128
|
+
---
|
|
129
|
+
<style set:html={html} />
|
|
@@ -37,11 +37,11 @@ const backTriggerSlot = Astro.slots.has('back-trigger') ? await Astro.slots.rend
|
|
|
37
37
|
<evo-flip-card flipped="false" style={style.join(';')} {...attrs}>
|
|
38
38
|
<div class="evo-flip-card-wrapper">
|
|
39
39
|
<div class="evo-flip-card-front">
|
|
40
|
-
<button class="evo-flip-card-trigger"
|
|
40
|
+
<button class="evo-flip-card-trigger"><Fragment set:html={frontTriggerSlot ?? '↻'} /></button>
|
|
41
41
|
<slot name="front" />
|
|
42
42
|
</div>
|
|
43
43
|
<div class="evo-flip-card-back">
|
|
44
|
-
<button class="evo-flip-card-trigger"
|
|
44
|
+
<button class="evo-flip-card-trigger"><Fragment set:html={backTriggerSlot ?? '↻'} /></button>
|
|
45
45
|
<slot name="back" />
|
|
46
46
|
</div>
|
|
47
47
|
</div>
|
|
@@ -74,14 +74,14 @@ export class EvoModalElement extends HTMLElement implements IEvoModalElement {
|
|
|
74
74
|
|
|
75
75
|
const duration = getAnimationDuration(this);
|
|
76
76
|
|
|
77
|
-
this.classList.add(`${CSS_PREFIX_HIDE}-${
|
|
78
|
-
this.classList.remove(`${CSS_PREFIX_SHOW}-${
|
|
77
|
+
this.classList.add(`${CSS_PREFIX_HIDE}-${animation}`);
|
|
78
|
+
this.classList.remove(`${CSS_PREFIX_SHOW}-${animation}`);
|
|
79
79
|
if (overlay) {
|
|
80
80
|
hideOverlay({ modal: this, animation: this.#animation, duration });
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
setTimeout(() => {
|
|
84
|
-
this.classList.remove('evo-modal-visible', `${CSS_PREFIX_HIDE}-${
|
|
84
|
+
this.classList.remove('evo-modal-visible', `${CSS_PREFIX_HIDE}-${animation}`);
|
|
85
85
|
this.dispatchEvent(createModalEvent('evomodal-hidden', this, { replacedBy }));
|
|
86
86
|
resolve();
|
|
87
87
|
}, animation !== 'none' ? duration : 0);
|
|
@@ -176,7 +176,7 @@ export function hideOverlay({ modal, animation = 'fade', duration }: { modal?: E
|
|
|
176
176
|
/**
|
|
177
177
|
* Call this function when dynamically adding a trigger to the DOM
|
|
178
178
|
*/
|
|
179
|
-
export function bindTriggers(container = document) {
|
|
179
|
+
export function bindTriggers(container: Document | HTMLElement = document) {
|
|
180
180
|
container.querySelectorAll<HTMLElement>('[data-evo-modal-show]').forEach((trigger) => {
|
|
181
181
|
if (!trigger.hasAttribute('data-evo-modal-bound')) {
|
|
182
182
|
trigger.setAttribute('data-evo-modal-bound','true');
|
|
@@ -32,7 +32,11 @@ switch (scrolling) {
|
|
|
32
32
|
break;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
containerAttrs.style = typeof containerAttrs.style == 'string'
|
|
35
|
+
containerAttrs.style = typeof containerAttrs.style == 'string'
|
|
36
|
+
? `${containerAttrs.style}${containerAttrs.style.endsWith(';') ? '' : ';'}`
|
|
37
|
+
: typeof containerAttrs.style == 'object' && containerAttrs.style !== null
|
|
38
|
+
? Object.entries(containerAttrs.style).map(([k, v]) => `${k.replace(/([A-Z])/g, '-$1').toLowerCase()}:${v}`).join(';') + ';'
|
|
39
|
+
: '';
|
|
36
40
|
if (width) {
|
|
37
41
|
containerAttrs.style = `${containerAttrs.style}width:${typeof width == 'number' || /^\d+$/.test(width) ? `${width}px` : width};`;
|
|
38
42
|
}
|
|
@@ -11,18 +11,6 @@ const intersectionObserver = new IntersectionObserver((entries) => {
|
|
|
11
11
|
{ threshold: 0.1 }
|
|
12
12
|
);
|
|
13
13
|
|
|
14
|
-
const mutationObserver = new MutationObserver((mutations) => {
|
|
15
|
-
const containers = new Set<IScrollContainer>();
|
|
16
|
-
mutations.forEach((mutation) => {
|
|
17
|
-
const sc = getScrollContainer(mutation.target as HTMLElement);
|
|
18
|
-
if (sc) {
|
|
19
|
-
containers.add(sc);
|
|
20
|
-
}
|
|
21
|
-
});
|
|
22
|
-
[...containers].forEach((container) => {
|
|
23
|
-
container.refresh();
|
|
24
|
-
});
|
|
25
|
-
});
|
|
26
14
|
|
|
27
15
|
export type Scrolling = 'horizontal' | 'vertical' | 'both';
|
|
28
16
|
|
|
@@ -68,10 +56,12 @@ export class EvoScrollContainerElement extends HTMLElement implements IEvoScroll
|
|
|
68
56
|
#mouseupHandlerHorz: any = undefined;
|
|
69
57
|
#mousemoveHandlerHorz: any = undefined;
|
|
70
58
|
#boundResizeHandler: () => void;
|
|
59
|
+
#mutationObserver: MutationObserver;
|
|
71
60
|
|
|
72
61
|
constructor() {
|
|
73
62
|
super();
|
|
74
63
|
this.#boundResizeHandler = this.refresh.bind(this);
|
|
64
|
+
this.#mutationObserver = new MutationObserver(() => this.refresh());
|
|
75
65
|
}
|
|
76
66
|
|
|
77
67
|
connectedCallback() {
|
|
@@ -90,7 +80,7 @@ export class EvoScrollContainerElement extends HTMLElement implements IEvoScroll
|
|
|
90
80
|
this.#scrolling = (this.dataset.scrolling as Scrolling) ?? 'vertical';
|
|
91
81
|
|
|
92
82
|
intersectionObserver.observe(this);
|
|
93
|
-
mutationObserver.observe(this, { attributes: true, childList: true, subtree: true });
|
|
83
|
+
this.#mutationObserver.observe(this, { attributes: true, childList: true, subtree: true });
|
|
94
84
|
|
|
95
85
|
window.addEventListener('resize', this.#boundResizeHandler, { passive: true });
|
|
96
86
|
if (this.#scrolling === 'horizontal') {
|
|
@@ -100,7 +90,8 @@ export class EvoScrollContainerElement extends HTMLElement implements IEvoScroll
|
|
|
100
90
|
|
|
101
91
|
disconnectedCallback() {
|
|
102
92
|
intersectionObserver.unobserve(this);
|
|
103
|
-
|
|
93
|
+
this.#mutationObserver.disconnect();
|
|
94
|
+
window.removeEventListener('resize', this.#boundResizeHandler);
|
|
104
95
|
this.removeEventListener('wheel', this.#wheelHorz);
|
|
105
96
|
}
|
|
106
97
|
|
|
@@ -182,14 +173,14 @@ export class EvoScrollContainerElement extends HTMLElement implements IEvoScroll
|
|
|
182
173
|
}
|
|
183
174
|
}
|
|
184
175
|
|
|
185
|
-
removeEventListener(type: keyof HTMLElementEventMap, listener: (this: EvoScrollContainerElement, ev: any) => void) {
|
|
176
|
+
removeEventListener(type: keyof HTMLElementEventMap, listener: (this: EvoScrollContainerElement, ev: any) => void, options?: boolean | EventListenerOptions) {
|
|
186
177
|
switch (type) {
|
|
187
178
|
case 'scroll':
|
|
188
179
|
case 'scrollend':
|
|
189
|
-
this.#content.removeEventListener(type, listener);
|
|
180
|
+
this.#content.removeEventListener(type, listener, options);
|
|
190
181
|
break;
|
|
191
182
|
default:
|
|
192
|
-
super.removeEventListener(type, listener);
|
|
183
|
+
super.removeEventListener(type, listener, options);
|
|
193
184
|
break;
|
|
194
185
|
}
|
|
195
186
|
}
|
|
@@ -207,7 +198,7 @@ export class EvoScrollContainerElement extends HTMLElement implements IEvoScroll
|
|
|
207
198
|
const trackOffset = parseInt(trackStyle.getPropertyValue('--evo-scroll-container-height-offset'), 10);
|
|
208
199
|
const trackAdjust = (!isNaN(trackTop) ? trackTop : 0) + (!isNaN(trackBot) ? trackBot : 0) + (!isNaN(trackOffset) ? trackOffset : 0);
|
|
209
200
|
|
|
210
|
-
const shouldScroll = (containerHeight / scrollHeight) < 1;
|
|
201
|
+
// const shouldScroll = (containerHeight / scrollHeight) < 1;
|
|
211
202
|
const visibleRatioVert = (containerHeight - trackAdjust) / scrollHeight;
|
|
212
203
|
const thumbHeight = visibleRatioVert * trackHeight;
|
|
213
204
|
|
|
@@ -261,7 +252,7 @@ export class EvoScrollContainerElement extends HTMLElement implements IEvoScroll
|
|
|
261
252
|
#thumbMouseMoveVert(e: MouseEvent) {
|
|
262
253
|
if (this.#isDraggingVert) {
|
|
263
254
|
const deltaY = e.clientY - this.#startY;
|
|
264
|
-
const thumbTrackHeight = this
|
|
255
|
+
const thumbTrackHeight = this.#trackVert!.clientHeight - this.#thumbVert!.clientHeight;
|
|
265
256
|
if (thumbTrackHeight <= 0) return;
|
|
266
257
|
const scrollRatio = (this.#content.scrollHeight - this.clientHeight) / thumbTrackHeight;
|
|
267
258
|
this.#content.scrollTop = this.#startScrollTop + deltaY * scrollRatio;
|
|
@@ -294,7 +285,7 @@ export class EvoScrollContainerElement extends HTMLElement implements IEvoScroll
|
|
|
294
285
|
#thumbMouseMoveHorz(e: MouseEvent) {
|
|
295
286
|
if (this.#isDraggingHorz) {
|
|
296
287
|
const deltaX = e.clientX - this.#startX;
|
|
297
|
-
const thumbTrackWidth = this
|
|
288
|
+
const thumbTrackWidth = this.#trackHorz!.clientWidth - this.#thumbHorz!.clientWidth;
|
|
298
289
|
if (thumbTrackWidth <= 0) return;
|
|
299
290
|
const scrollRatio = (this.#content.scrollWidth - this.clientWidth) / thumbTrackWidth;
|
|
300
291
|
this.#content.scrollLeft = this.#startScrollLeft + deltaX * scrollRatio;
|
|
@@ -367,8 +358,8 @@ export function refresh(selector?: string | HTMLElement) {
|
|
|
367
358
|
sc.refresh()
|
|
368
359
|
}
|
|
369
360
|
} else {
|
|
370
|
-
[...document.querySelectorAll<EvoScrollContainerElement>(EvoScrollContainerElement.htmlTagName)].forEach((
|
|
371
|
-
|
|
361
|
+
[...document.querySelectorAll<EvoScrollContainerElement>(EvoScrollContainerElement.htmlTagName)].forEach((sc) => {
|
|
362
|
+
sc.refresh();
|
|
372
363
|
});
|
|
373
364
|
}
|
|
374
365
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
---
|
|
2
2
|
import type { HTMLAttributes } from 'astro/types';
|
|
3
|
-
import { undefined } from 'astro:schema';
|
|
4
3
|
interface Props {
|
|
5
4
|
active?: true;
|
|
6
5
|
tabAttrs?: HTMLAttributes<'div'>
|
|
@@ -15,7 +14,7 @@ const tabTitle = await Astro.slots.render('title');
|
|
|
15
14
|
const attrs = { ...tabAttrs, title: tabTitle };
|
|
16
15
|
---
|
|
17
16
|
<div
|
|
18
|
-
data-tab-attrs={
|
|
17
|
+
data-tab-attrs={btoa(JSON.stringify(attrs))}
|
|
19
18
|
data-tab-active={active ?? false}
|
|
20
19
|
{...panelAttrs}
|
|
21
20
|
role="tabpanel"
|
|
@@ -55,8 +55,7 @@ const itemHtml = html.replace(
|
|
|
55
55
|
let updated = match
|
|
56
56
|
.replace(/\s*data-tab-attrs="[^"]*"/, '')
|
|
57
57
|
.replace(/\s*data-tab-active="[^"]*"/, '')
|
|
58
|
-
.replace(/\s*hidden(?:="[^"]*")?/, '')
|
|
59
|
-
+ (active ? '' : '');
|
|
58
|
+
.replace(/\s*hidden(?:="[^"]*")?/, '');
|
|
60
59
|
|
|
61
60
|
// Insert data-tab index
|
|
62
61
|
updated = updated.replace(/^<(\w+)/, `<$1 data-tab="${currentIndex}"`);
|
|
@@ -79,7 +78,7 @@ const tabHtml = tabs.map((tab) => {
|
|
|
79
78
|
Object.keys(tab)
|
|
80
79
|
.filter(key => !['class','active','title','index'].includes(key))
|
|
81
80
|
.forEach((key) => {
|
|
82
|
-
attrs.push(`${key}="${tab[key as keyof typeof tab].replace(/"/g, '')}"`)
|
|
81
|
+
attrs.push(`${key}="${String(tab[key as keyof typeof tab] ?? '').replace(/"/g, '')}"`)
|
|
83
82
|
});
|
|
84
83
|
|
|
85
84
|
if (listType === 'none') {
|
package/src/components/index.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
export { default as CssBackgroundImage } from './CssBackgroundImage.astro';
|
|
2
|
+
export { default as CssVariables } from './CssVariables.astro';
|
|
1
3
|
export { default as FlipCard } from './FlipCard/FlipCard.astro';
|
|
2
4
|
export { default as RightISI } from './ISI/Right/RightISI.astro';
|
|
3
5
|
export { default as Modal } from './Modal/Modal.astro';
|
|
@@ -20,6 +20,7 @@ async function getFiles(dir: string, { includeExts, excludeExts }: { includeExts
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
interface PruneBuildOpts {
|
|
23
|
+
enabled?: boolean;
|
|
23
24
|
keep?: (string | RegExp)[]
|
|
24
25
|
}
|
|
25
26
|
export default function(opts?: PruneBuildOpts): AstroIntegration {
|
|
@@ -27,43 +28,50 @@ export default function(opts?: PruneBuildOpts): AstroIntegration {
|
|
|
27
28
|
name: 'prune-build',
|
|
28
29
|
hooks: {
|
|
29
30
|
'astro:build:done': async ({ dir, logger }) => {
|
|
31
|
+
if (opts?.enabled === false) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
30
34
|
logger.info('Pruning unused assets from build');
|
|
31
35
|
let dirPath = fileURLToPath(dir);
|
|
32
36
|
if (dirPath.endsWith(sep)) {
|
|
33
37
|
dirPath = dirPath.slice(0, -1);
|
|
34
38
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
39
|
+
try {
|
|
40
|
+
const assets = await getFiles(dirPath, { excludeExts: ['.html'] });
|
|
41
|
+
const files = await getFiles(dirPath, { includeExts: ['.css', '.html', '.js'] });
|
|
42
|
+
const keepAssets = new Set<string>();
|
|
43
|
+
|
|
44
|
+
for (const file of files) {
|
|
45
|
+
const content = await readFile(file, 'utf8');
|
|
46
|
+
for (const asset of assets) {
|
|
47
|
+
if (content.includes(basename(asset))) {
|
|
48
|
+
keepAssets.add(asset);
|
|
49
|
+
}
|
|
44
50
|
}
|
|
45
51
|
}
|
|
46
|
-
}
|
|
47
52
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
for (const asset of assets) {
|
|
54
|
+
if (!keepAssets.has(asset)) {
|
|
55
|
+
let prune = true;
|
|
56
|
+
const relPath = asset.slice(dirPath.length).replace(/\\/g, '/');
|
|
57
|
+
if (opts?.keep && opts.keep.length > 0) {
|
|
58
|
+
for (const k of opts.keep) {
|
|
59
|
+
if ((typeof k == 'string' && k === relPath) || (k instanceof RegExp && k.test(relPath))) {
|
|
60
|
+
prune = false;
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
57
63
|
}
|
|
58
64
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
+
if (prune) {
|
|
66
|
+
logger.warn(`Pruning asset: ${relPath}`);
|
|
67
|
+
await rm(asset, { force: true });
|
|
68
|
+
} else {
|
|
69
|
+
logger.info(`Retaining asset: ${relPath}`);
|
|
70
|
+
}
|
|
65
71
|
}
|
|
66
72
|
}
|
|
73
|
+
} catch (error: any) {
|
|
74
|
+
logger.error(error.toString());
|
|
67
75
|
}
|
|
68
76
|
}
|
|
69
77
|
}
|
|
@@ -1,22 +1,47 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/** `'spread'` — fingers moving apart; `'pinch'` — fingers moving together. */
|
|
2
|
+
export type PinchGesture = 'spread' | 'pinch';
|
|
3
3
|
|
|
4
|
+
export interface PinchController {
|
|
5
|
+
enable: () => void;
|
|
6
|
+
disable: () => void;
|
|
7
|
+
dispose: () => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Attaches a two-finger pinch gesture listener to `window`.
|
|
12
|
+
*
|
|
13
|
+
* Tracks the cumulative scale across gestures, snapping to `1` when crossing
|
|
14
|
+
* the natural scale boundary. Fires `onPinch` on every move tick where the
|
|
15
|
+
* finger distance changes.
|
|
16
|
+
*
|
|
17
|
+
* @param onPinch - Called with the current cumulative scale and gesture direction.
|
|
18
|
+
* @returns A `PinchController` with `enable`, `disable`, and `dispose` methods.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* const pinch = createPinchListener((scale, gesture) => {
|
|
22
|
+
* console.log(gesture, scale); // 'spread' 1.23
|
|
23
|
+
* });
|
|
24
|
+
*
|
|
25
|
+
* // Later, to clean up:
|
|
26
|
+
* pinch.dispose();
|
|
27
|
+
*/
|
|
28
|
+
export function createPinchListener(onPinch: (scale: number, gesture: PinchGesture) => void): PinchController {
|
|
4
29
|
let initialDist: number = 0;
|
|
5
30
|
let lastDistance: number = 0;
|
|
6
31
|
let currentScale: number = 1;
|
|
7
32
|
let activeScale: number = 1;
|
|
8
33
|
|
|
9
|
-
|
|
34
|
+
const getTouchDistance = (touches: TouchList) => {
|
|
10
35
|
return Math.hypot(touches[0].clientX - touches[1].clientX, touches[0].clientY - touches[1].clientY);
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const reset = () => {
|
|
14
39
|
window.removeEventListener('touchmove', touchmove);
|
|
15
40
|
window.removeEventListener('touchend', touchend);
|
|
16
41
|
currentScale = activeScale;
|
|
17
42
|
initialDist = 0;
|
|
18
43
|
lastDistance = 0;
|
|
19
|
-
}
|
|
44
|
+
};
|
|
20
45
|
|
|
21
46
|
const touchstart = (e: TouchEvent) => {
|
|
22
47
|
if (e.touches.length === 2) {
|
|
@@ -36,22 +61,22 @@ export function createPinchListener() {
|
|
|
36
61
|
|
|
37
62
|
const rawScale = currentScale * ratio;
|
|
38
63
|
|
|
39
|
-
// Detect if we're crossing 1 from either
|
|
64
|
+
// Detect if we're crossing 1 from either gesture
|
|
40
65
|
const crossingOne =
|
|
41
66
|
(currentScale > 1 && rawScale < 1) ||
|
|
42
67
|
(currentScale < 1 && rawScale > 1);
|
|
43
68
|
|
|
44
69
|
activeScale = crossingOne ? 1 : rawScale; // snap to 1
|
|
45
70
|
|
|
46
|
-
let
|
|
71
|
+
let gesture: PinchGesture | undefined = undefined;
|
|
47
72
|
if (currentDistance > lastDistance) {
|
|
48
|
-
|
|
73
|
+
gesture = 'spread';
|
|
49
74
|
} else if (currentDistance < lastDistance) {
|
|
50
|
-
|
|
75
|
+
gesture = 'pinch';
|
|
51
76
|
}
|
|
52
77
|
|
|
53
|
-
if (
|
|
54
|
-
|
|
78
|
+
if (gesture !== undefined) {
|
|
79
|
+
onPinch(activeScale, gesture);
|
|
55
80
|
lastDistance = currentDistance;
|
|
56
81
|
}
|
|
57
82
|
}
|
|
@@ -62,5 +87,27 @@ export function createPinchListener() {
|
|
|
62
87
|
reset();
|
|
63
88
|
}
|
|
64
89
|
};
|
|
65
|
-
|
|
66
|
-
|
|
90
|
+
|
|
91
|
+
let listening = false;
|
|
92
|
+
|
|
93
|
+
const enable = () => {
|
|
94
|
+
if (!listening) {
|
|
95
|
+
listening = true;
|
|
96
|
+
window.addEventListener('touchstart', touchstart, { passive: true });
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const disable = () => {
|
|
101
|
+
listening = false;
|
|
102
|
+
window.removeEventListener('touchstart', touchstart);
|
|
103
|
+
reset();
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
enable();
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
enable,
|
|
110
|
+
disable,
|
|
111
|
+
dispose: disable
|
|
112
|
+
};
|
|
113
|
+
}
|