@inizioevoke/astro-core 2.1.1 → 2.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inizioevoke/astro-core",
3
- "version": "2.1.1",
3
+ "version": "2.2.0",
4
4
  "description": "",
5
5
  "license": "ISC",
6
6
  "author": "",
@@ -0,0 +1,103 @@
1
+ ---
2
+ // See /docs/components/CssBackgroundImage.md
3
+ import type { ImageMetadata, ImageOutputFormat } from "astro";
4
+ import { getImage } from "astro:assets";
5
+
6
+ type ImageSize = 'contain' | 'cover';
7
+ type ImagePosition = 'left top' | 'left center' | 'left bottom' | 'center top' | 'center center' | 'center bottom' | 'right top' | 'right center' | 'right bottom';
8
+
9
+ interface ImageSrcProps {
10
+ image: ImageMetadata;
11
+ optimize?: boolean;
12
+ format?: ImageOutputFormat;
13
+ quality?: number;
14
+ width?: number;
15
+ height?: number;
16
+ }
17
+ interface BgImageProps extends ImageSrcProps {
18
+ size?: ImageSize | (string & {});
19
+ position?: ImagePosition | (string & {});
20
+ repeat?: 'no-repeat' | 'repeat-x' | 'repeat-y' | 'repeat';
21
+ important?: true;
22
+ }
23
+ export interface Props extends BgImageProps {
24
+ selector?: string;
25
+
26
+ breakpoints?: ({
27
+ minWidth?: string | number;
28
+ maxWidth?: string | number;
29
+ } & BgImageProps)[];
30
+ }
31
+
32
+ const {
33
+ selector,
34
+ image,
35
+ optimize = true,
36
+ format,
37
+ quality,
38
+ width,
39
+ height,
40
+ size,
41
+ position,
42
+ repeat,
43
+ important,
44
+ breakpoints = []
45
+ } = Astro.props;
46
+
47
+ const styles: string[] = [];
48
+
49
+ async function getImageSrc({image, format, quality, width, height, optimize}: ImageSrcProps) {
50
+ if (optimize !== false) {
51
+ const img = await getImage({
52
+ src: image,
53
+ format: format ?? image.format,
54
+ quality: quality,
55
+ width,
56
+ height
57
+ });
58
+ return img.src;
59
+ } else {
60
+ return image.src;
61
+ }
62
+ }
63
+
64
+ if (image) {
65
+ const src = await getImageSrc({ image, format, quality, width, height, optimize });
66
+ styles.push(`background-image:url("${src}")${important ? '!important' : ''};`);
67
+ }
68
+
69
+ size && styles.push(`background-size:${size}${important ? '!important' : ''};`);
70
+ position && styles.push(`background-position:${position}${important ? '!important' : ''};`);
71
+ repeat && styles.push(`background-repeat:${repeat}${important ? '!important' : ''};`);
72
+
73
+ for (const bp of breakpoints) {
74
+ const widths = [];
75
+ if (bp.minWidth) {
76
+ widths.push(`(min-width:${typeof bp.minWidth == 'number' ? `${bp.minWidth}px` : bp.minWidth})`);
77
+ }
78
+ if (bp.maxWidth) {
79
+ widths.push(`(max-width:${typeof bp.maxWidth == 'number' ? `${bp.maxWidth}px` : bp.maxWidth})`);
80
+ }
81
+ if (widths.length > 0) {
82
+ // const bpImg = await getImage({ src: bp.image });
83
+ styles.push(`@media screen and ${widths.join(' and ')}{`);
84
+ if (bp.image) {
85
+ const src = await getImageSrc(bp);
86
+ styles.push(`background-image:url("${src}")${important ? '!important' : ''};`);
87
+ }
88
+ bp.size && styles.push(`background-size:${bp.size}${important ? '!important' : ''};`);
89
+ bp.position && styles.push(`background-position:${bp.position}${important ? '!important' : ''};`);
90
+ bp.repeat && styles.push(`background-repeat:${bp.repeat}${important ? '!important' : ''};`);
91
+ styles.push('}');
92
+ }
93
+ }
94
+
95
+ const html = [`${import.meta.env.PROD ? '' : '\n'}${selector ?? 'body'}{`]
96
+ .concat(styles.map((s) => {
97
+ return `${s}`;
98
+ }))
99
+ .concat(['}'])
100
+ .join(import.meta.env.PROD ? '' : '\n');
101
+ ---
102
+
103
+ <style set:html={html} />
@@ -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">{frontTriggerSlot ?? <Fragment set:html={'&#8635;'} />}</button>
40
+ <button class="evo-flip-card-trigger"><Fragment set:html={frontTriggerSlot ?? '&#8635;'} /></button>
41
41
  <slot name="front" />
42
42
  </div>
43
43
  <div class="evo-flip-card-back">
44
- <button class="evo-flip-card-trigger">{backTriggerSlot ?? <Fragment set:html={'&#8635;'} />}</button>
44
+ <button class="evo-flip-card-trigger"><Fragment set:html={backTriggerSlot ?? '&#8635;'} /></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}-${this.#animation}`);
78
- this.classList.remove(`${CSS_PREFIX_SHOW}-${this.#animation}`);
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}-${this.#animation}`);
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' ? `${containerAttrs.style}${containerAttrs.style.endsWith(';') ? '' : ';'}` : '';
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
- window.removeEventListener('resize', this.#boundResizeHandler);
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
  }
@@ -198,45 +189,49 @@ export class EvoScrollContainerElement extends HTMLElement implements IEvoScroll
198
189
  if (this.#thumbVert && this.#trackVert) {
199
190
  const scrollHeight = this.#content.scrollHeight - getPaddingY(this.#content);
200
191
  const containerHeight = this.clientHeight - getPaddingY(this);
201
- const trackHeight = this.#trackVert.clientHeight;
202
- const trackStyle = getComputedStyle(this.#trackVert);
203
- const trackTop = parseInt(trackStyle.getPropertyValue('--evo-scroll-container-track-vert-top'), 10);
204
- const trackBot = parseInt(trackStyle.getPropertyValue('--evo-scroll-container-track-vert-bottom'), 10);
205
- const trackOffset = parseInt(trackStyle.getPropertyValue('--evo-scroll-container-height-offset'), 10);
206
- const trackAdjust = (!isNaN(trackTop) ? trackTop : 0) + (!isNaN(trackBot) ? trackBot : 0) + (!isNaN(trackOffset) ? trackOffset : 0);
207
-
208
- const visibleRatioVert = (containerHeight - trackAdjust) / scrollHeight;
209
- const thumbHeight = visibleRatioVert * trackHeight;
210
-
211
- if (this.#scrollbarSettings.thumbHeight != undefined) {
212
- this.#thumbVert.style.height = `${Math.max(0, Math.min(this.#scrollbarSettings.thumbHeight, trackHeight))}px`;
213
- } else {
214
- this.#thumbVert.style.height = `${Math.min(thumbHeight > 20 ? thumbHeight : 20, trackHeight)}px`;
215
- }
216
- if (this.#scrollbarSettings.trackVisible !== undefined) {
217
- this.#trackVert.style.display = this.#scrollbarSettings.trackVisible === true ? 'block' : 'none';
192
+
193
+ if ((containerHeight / scrollHeight) < 1 || this.#scrollbarSettings.trackVisible === true) { // should scroll
194
+ const trackHeight = this.#trackVert.clientHeight;
195
+ const trackStyle = getComputedStyle(this.#trackVert);
196
+ const trackTop = parseInt(trackStyle.getPropertyValue('--evo-scroll-container-track-vert-top'), 10);
197
+ const trackBot = parseInt(trackStyle.getPropertyValue('--evo-scroll-container-track-vert-bottom'), 10);
198
+ const trackOffset = parseInt(trackStyle.getPropertyValue('--evo-scroll-container-height-offset'), 10);
199
+ const trackAdjust = (!isNaN(trackTop) ? trackTop : 0) + (!isNaN(trackBot) ? trackBot : 0) + (!isNaN(trackOffset) ? trackOffset : 0);
200
+
201
+ // const shouldScroll = (containerHeight / scrollHeight) < 1;
202
+ const visibleRatioVert = (containerHeight - trackAdjust) / scrollHeight;
203
+ const thumbHeight = visibleRatioVert * trackHeight;
204
+
205
+ if (this.#scrollbarSettings.thumbHeight != undefined) {
206
+ this.#thumbVert.style.height = `${Math.max(0, Math.min(this.#scrollbarSettings.thumbHeight, trackHeight))}px`;
207
+ } else {
208
+ this.#thumbVert.style.height = `${Math.min(thumbHeight > 20 ? thumbHeight : 20, trackHeight)}px`;
209
+ }
210
+
211
+ this.#trackVert.style.display = 'block';
218
212
  } else {
219
- this.#trackVert.style.display = visibleRatioVert < 1 ? 'block' : 'none';
213
+ this.#trackVert.style.display = 'none';
220
214
  }
221
215
  }
222
216
 
223
217
  if (this.#thumbHorz && this.#trackHorz) {
224
218
  const scrollWidth = this.#content.scrollWidth - getPaddingX(this.#content);
225
219
  const containerWidth = this.clientWidth - getPaddingX(this);
226
- const trackWidth = this.#trackHorz.clientWidth;
227
- const trackStyle = getComputedStyle(this.#trackHorz);
228
- const trackLeft = parseInt(trackStyle.getPropertyValue('--evo-scroll-container-track-horz-left'), 10);
229
- const trackRight = parseInt(trackStyle.getPropertyValue('--evo-scroll-container-track-horz-right'), 10);
230
- const trackOffset = parseInt(trackStyle.getPropertyValue('--evo-scroll-container-width-offset'), 10);
231
- const trackAdjust = (!isNaN(trackLeft) ? trackLeft : 0) + (!isNaN(trackRight) ? trackRight : 0) + (!isNaN(trackOffset) ? trackOffset : 0);
232
-
233
- const visibleRatioHorz = (containerWidth - trackAdjust) / scrollWidth;
234
- const thumbWidth = visibleRatioHorz * trackWidth;
235
- this.#thumbHorz.style.width = `${Math.min(thumbWidth > 20 ? thumbWidth : 20, trackWidth)}px`;
236
- if (this.#scrollbarSettings.trackVisible !== undefined) {
237
- this.#trackHorz.style.display = this.#scrollbarSettings.trackVisible === true ? 'block' : 'none';
220
+
221
+ if ((containerWidth / scrollWidth) < 1 || this.#scrollbarSettings.trackVisible === true) {
222
+ const trackWidth = this.#trackHorz.clientWidth;
223
+ const trackStyle = getComputedStyle(this.#trackHorz);
224
+ const trackLeft = parseInt(trackStyle.getPropertyValue('--evo-scroll-container-track-horz-left'), 10);
225
+ const trackRight = parseInt(trackStyle.getPropertyValue('--evo-scroll-container-track-horz-right'), 10);
226
+ const trackOffset = parseInt(trackStyle.getPropertyValue('--evo-scroll-container-width-offset'), 10);
227
+ const trackAdjust = (!isNaN(trackLeft) ? trackLeft : 0) + (!isNaN(trackRight) ? trackRight : 0) + (!isNaN(trackOffset) ? trackOffset : 0);
228
+
229
+ const visibleRatioHorz = (containerWidth - trackAdjust) / scrollWidth;
230
+ const thumbWidth = visibleRatioHorz * trackWidth;
231
+ this.#thumbHorz.style.width = `${Math.min(thumbWidth > 20 ? thumbWidth : 20, trackWidth)}px`;
232
+ this.#trackHorz.style.display = 'block';
238
233
  } else {
239
- this.#trackHorz.style.display = visibleRatioHorz < 1 ? 'block' : 'none';
234
+ this.#trackHorz.style.display = 'none';
240
235
  }
241
236
  }
242
237
  }
@@ -257,7 +252,7 @@ export class EvoScrollContainerElement extends HTMLElement implements IEvoScroll
257
252
  #thumbMouseMoveVert(e: MouseEvent) {
258
253
  if (this.#isDraggingVert) {
259
254
  const deltaY = e.clientY - this.#startY;
260
- const thumbTrackHeight = this.clientHeight - this.#thumbVert!.clientHeight;
255
+ const thumbTrackHeight = this.#trackVert!.clientHeight - this.#thumbVert!.clientHeight;
261
256
  if (thumbTrackHeight <= 0) return;
262
257
  const scrollRatio = (this.#content.scrollHeight - this.clientHeight) / thumbTrackHeight;
263
258
  this.#content.scrollTop = this.#startScrollTop + deltaY * scrollRatio;
@@ -290,7 +285,7 @@ export class EvoScrollContainerElement extends HTMLElement implements IEvoScroll
290
285
  #thumbMouseMoveHorz(e: MouseEvent) {
291
286
  if (this.#isDraggingHorz) {
292
287
  const deltaX = e.clientX - this.#startX;
293
- const thumbTrackWidth = this.clientWidth - this.#thumbHorz!.clientWidth;
288
+ const thumbTrackWidth = this.#trackHorz!.clientWidth - this.#thumbHorz!.clientWidth;
294
289
  if (thumbTrackWidth <= 0) return;
295
290
  const scrollRatio = (this.#content.scrollWidth - this.clientWidth) / thumbTrackWidth;
296
291
  this.#content.scrollLeft = this.#startScrollLeft + deltaX * scrollRatio;
@@ -363,8 +358,8 @@ export function refresh(selector?: string | HTMLElement) {
363
358
  sc.refresh()
364
359
  }
365
360
  } else {
366
- [...document.querySelectorAll<EvoScrollContainerElement>(EvoScrollContainerElement.htmlTagName)].forEach((selector) => {
367
- selector.refresh();
361
+ [...document.querySelectorAll<EvoScrollContainerElement>(EvoScrollContainerElement.htmlTagName)].forEach((sc) => {
362
+ sc.refresh();
368
363
  });
369
364
  }
370
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={attrs ? btoa(JSON.stringify(attrs)) : undefined}
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') {
@@ -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
- const assets = await getFiles(dirPath, { excludeExts: ['.html'] });
36
- const files = await getFiles(dirPath, { includeExts: ['.css', '.html', '.js'] });
37
- const keep = new Set<string>();
38
-
39
- for (const file of files) {
40
- const content = await readFile(file, 'utf8');
41
- for (const asset of assets) {
42
- if (content.includes(basename(asset))) {
43
- keep.add(asset);
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
- for (const asset of assets) {
49
- if (!keep.has(asset)) {
50
- let prune = true;
51
- const relPath = asset.replace(dirPath, '').replace(/\\/g, '/');
52
- if (opts?.keep && opts.keep.length > 0) {
53
- for (const k of opts.keep) {
54
- if ((typeof k == 'string' && k === relPath) || (k instanceof RegExp && k.test(relPath))) {
55
- prune = false;
56
- break;
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
- if (prune) {
61
- logger.warn(`Pruning asset: ${relPath}`);
62
- await rm(asset, { force: true });
63
- } else {
64
- logger.info(`Retaining asset: ${relPath}`);
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
- export function createPinchListener() {
2
- type PinchDirection = 'in' | 'out';
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
- function getTouchDistance(touches: TouchList) {
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
- function reset() {
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 direction
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 direction: PinchDirection | undefined = undefined;
71
+ let gesture: PinchGesture | undefined = undefined;
47
72
  if (currentDistance > lastDistance) {
48
- direction = 'in';
73
+ gesture = 'spread';
49
74
  } else if (currentDistance < lastDistance) {
50
- direction = 'out';
75
+ gesture = 'pinch';
51
76
  }
52
77
 
53
- if (direction !== undefined) {
54
- console.log(activeScale, direction);
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
- window.addEventListener('touchstart', touchstart, { passive: true });
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
+ }