zone5 1.2.0 → 1.3.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.
@@ -1,13 +1,11 @@
1
1
  <script lang="ts">
2
2
  import { onMount, untrack } from 'svelte';
3
3
 
4
- import Img from './Zone5Img.svelte';
5
4
  import { useImageRegistry } from './Zone5Provider.svelte';
6
- import {
7
- DEFAULT_COLUMN_BREAKPOINTS,
8
- SINGLE_IMAGE_HEIGHT_CLASS,
9
- WALL_IMAGE_HEIGHT_CLASS,
10
- } from './constants';
5
+ import Zone5Single from './Zone5Single.svelte';
6
+ import Zone5Wall from './Zone5Wall.svelte';
7
+ import Zone5Waterfall from './Zone5Waterfall.svelte';
8
+ import { DEFAULT_COLUMN_BREAKPOINTS } from './constants';
11
9
  import type { ImageData } from './types';
12
10
 
13
11
  interface Props {
@@ -57,103 +55,20 @@
57
55
  imageStore.setCurrent(componentId, index);
58
56
  }
59
57
  }
60
-
61
- // WATERFALL MODE CALCULATIONS
62
- let containerWidth = $state(0);
63
-
64
- /**
65
- * Calculate number of columns based on container width and breakpoints
66
- */
67
- let nColumns = $derived.by(() => {
68
- if (mode !== 'waterfall') return 1;
69
-
70
- let columns = 1;
71
- const sortedBreakpoints = Object.entries(columnBreakpoints).sort(
72
- ([a], [b]) => Number(a) - Number(b),
73
- );
74
-
75
- for (const [breakpoint, cols] of sortedBreakpoints) {
76
- if (containerWidth >= Number(breakpoint)) {
77
- columns = cols;
78
- }
79
- }
80
-
81
- return columns;
82
- });
83
-
84
- /**
85
- * Distribute images across columns in round-robin fashion
86
- */
87
- let colPhotos = $derived.by(() => {
88
- if (mode !== 'waterfall') return [];
89
-
90
- const cols: { image: ImageData; idx: number }[][] = Array.from({ length: nColumns }, () => []);
91
- images.forEach((image, idx) => {
92
- cols[idx % nColumns].push({ image, idx });
93
- });
94
-
95
- return cols;
96
- });
97
-
98
- /**
99
- * Calculate filler heights to equalize column heights in waterfall mode
100
- */
101
- let colFillers = $derived.by(() => {
102
- if (mode !== 'waterfall') return [];
103
-
104
- const totalHeights = colPhotos.map((col) =>
105
- col.reduce((sum, img) => sum + 1 / img.image.properties.aspectRatio, 0),
106
- );
107
- const maxHeight = Math.max(...totalHeights);
108
-
109
- return totalHeights.map((height) => maxHeight - height);
110
- });
111
58
  </script>
112
59
 
113
60
  {#if mode === 'wall' && images.length === 1}
114
- {@const image = images[0]}
115
- <figure class="flex justify-center" aria-label={image.properties.title || 'Image'}>
116
- <Img
117
- {image}
118
- class={SINGLE_IMAGE_HEIGHT_CLASS}
119
- onclick={imageStore ? () => handleImageClick(0) : undefined}
120
- />
121
- {#if image.properties.title && !nocaption}
122
- <figcaption class="mt-2 text-sm text-gray-600">
123
- {image.properties.title}
124
- </figcaption>
125
- {/if}
126
- </figure>
61
+ <Zone5Single
62
+ image={images[0]}
63
+ {nocaption}
64
+ onclick={imageStore ? () => handleImageClick(0) : undefined}
65
+ />
127
66
  {:else if mode === 'wall'}
128
- <div class="flex gap-2 flex-col md:flex-row md:flex-wrap zone5-wall" role="list">
129
- {#each images as image, idx (idx)}
130
- <div class="grow {WALL_IMAGE_HEIGHT_CLASS} flex" role="listitem">
131
- <Img
132
- {image}
133
- cover
134
- class="grow"
135
- onclick={imageStore ? () => handleImageClick(idx) : undefined}
136
- />
137
- </div>
138
- {/each}
139
- </div>
67
+ <Zone5Wall {images} onImageClick={imageStore ? handleImageClick : undefined} />
140
68
  {:else if mode === 'waterfall'}
141
- <div class="flex gap-2" bind:clientWidth={containerWidth} role="list">
142
- {#each Array.from({ length: nColumns }, (_, i) => i) as columnId (columnId)}
143
- <div class="flex flex-col gap-2" role="listitem">
144
- {#each colPhotos[columnId] as { image, idx } (idx)}
145
- <div>
146
- <Img {image} onclick={imageStore ? () => handleImageClick(idx) : undefined} />
147
- </div>
148
- {/each}
149
- {#if colFillers[columnId] > 0}
150
- <div
151
- class="bg-slate-200 rounded"
152
- style:height={colFillers[columnId] * 100 + '%'}
153
- aria-hidden="true"
154
- ></div>
155
- {/if}
156
- </div>
157
- {/each}
158
- </div>
69
+ <Zone5Waterfall
70
+ {columnBreakpoints}
71
+ {images}
72
+ onImageClick={imageStore ? handleImageClick : undefined}
73
+ />
159
74
  {/if}
@@ -1,5 +1,4 @@
1
1
  <script lang="ts">
2
- import { untrack } from 'svelte';
3
2
  import type { Action } from 'svelte/action';
4
3
 
5
4
  import type { ImageData } from './types';
@@ -26,33 +25,37 @@
26
25
  return breakpoints.join(', ');
27
26
  });
28
27
 
29
- let img: HTMLImageElement;
30
- let loaded = $state(true);
28
+ let loaded = $state(false);
31
29
 
32
- $effect(() => {
33
- // Track `sizes` to rerun on change of `image` prop
34
- void sizes;
35
- // Track `img` element, in case effect runs before img is set
36
- const imgEl = img;
30
+ // Action to handle image load state - runs when element is mounted
31
+ const useImageLoad: Action<HTMLImageElement> = (node) => {
32
+ const handleLoad = () => {
33
+ loaded = true;
34
+ };
37
35
 
38
- if (!imgEl) return;
39
- if (imgEl.complete) {
40
- untrack(() => {
41
- loaded = true;
42
- });
36
+ // Check if already loaded (e.g., from cache)
37
+ if (node.complete && node.naturalWidth > 0) {
38
+ loaded = true;
43
39
  return;
44
40
  }
45
41
 
46
- untrack(() => {
47
- loaded = false;
48
- });
49
- const handleLoad = () => (loaded = true);
50
- imgEl.addEventListener('load', handleLoad);
42
+ // Not loaded yet - add listener first, then recheck to avoid race condition
43
+ loaded = false;
44
+ node.addEventListener('load', handleLoad);
45
+
46
+ // Recheck complete after adding listener in case image loaded during setup
47
+ if (node.complete && node.naturalWidth > 0) {
48
+ loaded = true;
49
+ node.removeEventListener('load', handleLoad);
50
+ return;
51
+ }
51
52
 
52
- return () => {
53
- imgEl.removeEventListener('load', handleLoad);
53
+ return {
54
+ destroy: () => {
55
+ node.removeEventListener('load', handleLoad);
56
+ }
54
57
  };
55
- });
58
+ };
56
59
 
57
60
  const useOnclick: Action<HTMLDivElement, (() => void) | undefined> = (node, handler) => {
58
61
  const setHandler = (handler?: () => void | undefined) => {
@@ -109,7 +112,7 @@
109
112
  'opacity-100': loaded,
110
113
  },
111
114
  ]}
112
- bind:this={img}
115
+ use:useImageLoad
113
116
  />
114
117
  </picture>
115
118
  </div>
@@ -0,0 +1,22 @@
1
+ <script lang="ts">
2
+ import Img from './Zone5Img.svelte';
3
+ import { SINGLE_IMAGE_HEIGHT_CLASS } from './constants';
4
+ import type { ImageData } from './types';
5
+
6
+ interface Props {
7
+ image: ImageData;
8
+ nocaption?: boolean;
9
+ onclick?: () => void;
10
+ }
11
+
12
+ let { image, nocaption = false, onclick }: Props = $props();
13
+ </script>
14
+
15
+ <figure class="flex justify-center" aria-label={image.properties.title || 'Image'}>
16
+ <Img {image} class={SINGLE_IMAGE_HEIGHT_CLASS} {onclick} />
17
+ {#if image.properties.title && !nocaption}
18
+ <figcaption class="mt-2 text-sm text-gray-600">
19
+ {image.properties.title}
20
+ </figcaption>
21
+ {/if}
22
+ </figure>
@@ -0,0 +1,9 @@
1
+ import type { ImageData } from './types';
2
+ interface Props {
3
+ image: ImageData;
4
+ nocaption?: boolean;
5
+ onclick?: () => void;
6
+ }
7
+ declare const Zone5Single: import("svelte").Component<Props, {}, "">;
8
+ type Zone5Single = ReturnType<typeof Zone5Single>;
9
+ export default Zone5Single;
@@ -0,0 +1,25 @@
1
+ <script lang="ts">
2
+ import Img from './Zone5Img.svelte';
3
+ import { WALL_IMAGE_HEIGHT_CLASS } from './constants';
4
+ import type { ImageData } from './types';
5
+
6
+ interface Props {
7
+ images: ImageData[];
8
+ onImageClick?: (index: number) => void;
9
+ }
10
+
11
+ let { images, onImageClick }: Props = $props();
12
+ </script>
13
+
14
+ <div class="flex gap-2 flex-col md:flex-row md:flex-wrap zone5-wall" role="list">
15
+ {#each images as image, idx (idx)}
16
+ <div class="grow {WALL_IMAGE_HEIGHT_CLASS} flex" role="listitem">
17
+ <Img
18
+ {image}
19
+ cover
20
+ class="grow"
21
+ onclick={onImageClick ? () => onImageClick(idx) : undefined}
22
+ />
23
+ </div>
24
+ {/each}
25
+ </div>
@@ -0,0 +1,8 @@
1
+ import type { ImageData } from './types';
2
+ interface Props {
3
+ images: ImageData[];
4
+ onImageClick?: (index: number) => void;
5
+ }
6
+ declare const Zone5Wall: import("svelte").Component<Props, {}, "">;
7
+ type Zone5Wall = ReturnType<typeof Zone5Wall>;
8
+ export default Zone5Wall;
@@ -0,0 +1,76 @@
1
+ <script lang="ts">
2
+ import Img from './Zone5Img.svelte';
3
+ import { DEFAULT_COLUMN_BREAKPOINTS } from './constants';
4
+ import type { ImageData } from './types';
5
+
6
+ interface Props {
7
+ columnBreakpoints?: { [key: number]: number };
8
+ images: ImageData[];
9
+ onImageClick?: (index: number) => void;
10
+ }
11
+
12
+ let { columnBreakpoints = DEFAULT_COLUMN_BREAKPOINTS, images, onImageClick }: Props = $props();
13
+
14
+ let containerWidth = $state(0);
15
+
16
+ /**
17
+ * Calculate number of columns based on container width and breakpoints
18
+ */
19
+ let nColumns = $derived.by(() => {
20
+ let columns = 1;
21
+ const sortedBreakpoints = Object.entries(columnBreakpoints).sort(
22
+ ([a], [b]) => Number(a) - Number(b),
23
+ );
24
+
25
+ for (const [breakpoint, cols] of sortedBreakpoints) {
26
+ if (containerWidth >= Number(breakpoint)) {
27
+ columns = cols;
28
+ }
29
+ }
30
+
31
+ return columns;
32
+ });
33
+
34
+ /**
35
+ * Distribute images across columns in round-robin fashion
36
+ */
37
+ let colPhotos = $derived.by(() => {
38
+ const cols: { image: ImageData; idx: number }[][] = Array.from({ length: nColumns }, () => []);
39
+ images.forEach((image, idx) => {
40
+ cols[idx % nColumns].push({ image, idx });
41
+ });
42
+
43
+ return cols;
44
+ });
45
+
46
+ /**
47
+ * Calculate filler heights to equalize column heights in waterfall mode
48
+ */
49
+ let colFillers = $derived.by(() => {
50
+ const totalHeights = colPhotos.map((col) =>
51
+ col.reduce((sum, img) => sum + 1 / img.image.properties.aspectRatio, 0),
52
+ );
53
+ const maxHeight = Math.max(...totalHeights);
54
+
55
+ return totalHeights.map((height) => maxHeight - height);
56
+ });
57
+ </script>
58
+
59
+ <div class="flex gap-2" bind:clientWidth={containerWidth} role="list">
60
+ {#each Array.from({ length: nColumns }, (_, i) => i) as columnId (columnId)}
61
+ <div class="flex flex-col gap-2" role="listitem">
62
+ {#each colPhotos[columnId] as { image, idx } (idx)}
63
+ <div>
64
+ <Img {image} onclick={onImageClick ? () => onImageClick(idx) : undefined} />
65
+ </div>
66
+ {/each}
67
+ {#if colFillers[columnId] > 0}
68
+ <div
69
+ class="bg-slate-200 rounded"
70
+ style:height={colFillers[columnId] * 100 + '%'}
71
+ aria-hidden="true"
72
+ ></div>
73
+ {/if}
74
+ </div>
75
+ {/each}
76
+ </div>
@@ -0,0 +1,11 @@
1
+ import type { ImageData } from './types';
2
+ interface Props {
3
+ columnBreakpoints?: {
4
+ [key: number]: number;
5
+ };
6
+ images: ImageData[];
7
+ onImageClick?: (index: number) => void;
8
+ }
9
+ declare const Zone5Waterfall: import("svelte").Component<Props, {}, "">;
10
+ type Zone5Waterfall = ReturnType<typeof Zone5Waterfall>;
11
+ export default Zone5Waterfall;
package/dist/module.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- // Augment mdast types
1
+ // Augment mdast types for remark plugin
2
2
  import type { SvelteComponent } from 'svast';
3
3
 
4
4
  declare module 'mdast' {
@@ -10,10 +10,3 @@ declare module 'mdast' {
10
10
  svelteComponent: SvelteComponent;
11
11
  }
12
12
  }
13
-
14
- declare module '*?z5' {
15
- import type { ItemFeature } from './processor';
16
-
17
- const data: ItemFeature;
18
- export default data;
19
- }
package/dist/vite.d.ts CHANGED
@@ -1,2 +1,6 @@
1
1
  import type { Plugin } from 'vite';
2
- export declare function zone5(cwd?: string): Plugin;
2
+ export interface Zone5PluginOptions {
3
+ cwd?: string;
4
+ basePath?: string;
5
+ }
6
+ export declare function zone5(options?: Zone5PluginOptions): Plugin;
package/dist/vite.js CHANGED
@@ -38,7 +38,7 @@ const serve = (basePath, cacheDir) => async (req, res, next) => {
38
38
  }
39
39
  next();
40
40
  };
41
- export function zone5(cwd) {
41
+ export function zone5(options = {}) {
42
42
  let viteConfig;
43
43
  let basePath;
44
44
  let zone5Config;
@@ -47,8 +47,11 @@ export function zone5(cwd) {
47
47
  enforce: 'pre',
48
48
  async configResolved(cfg) {
49
49
  viteConfig = cfg;
50
- zone5Config = await load(cwd);
51
- basePath = createBasePath(zone5Config.base.namespace, viteConfig.base);
50
+ zone5Config = await load(options.cwd);
51
+ const base = options.basePath
52
+ ? `${options.basePath.replace(/\/$/, '')}/${viteConfig.base.replace(/^\//, '')}`
53
+ : viteConfig.base;
54
+ basePath = createBasePath(zone5Config.base.namespace, base);
52
55
  },
53
56
  async resolveId(id, importer) {
54
57
  const srcURL = parseURL(id);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zone5",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "repository": {
5
5
  "url": "https://github.com/cwygoda/zone5"
6
6
  },
@@ -13,6 +13,8 @@
13
13
  "build:cli": "vite build --config vite.cli.config.ts",
14
14
  "build:watch": "vite build --watch",
15
15
  "build": "svelte-kit sync && svelte-package && pnpm build:cli && publint",
16
+ "build:site": "vite build",
17
+ "build:all": "pnpm build && pnpm build:site",
16
18
  "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
17
19
  "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
18
20
  "dev": "vite dev",
@@ -29,6 +31,7 @@
29
31
  "dagger:test:all": "dagger -m ./dagger call test-all --source=.",
30
32
  "dagger:check": "dagger -m ./dagger call check --source=.",
31
33
  "dagger:lint": "dagger -m ./dagger call lint --source=.",
34
+ "dagger:build:site": "dagger -m ./dagger call build-site --source=.",
32
35
  "dagger:ci": "dagger -m ./dagger call ci --source=.",
33
36
  "prepare": "husky"
34
37
  },
@@ -85,6 +88,7 @@
85
88
  "@dagger.io/dagger": "^0.19.6",
86
89
  "@lucide/svelte": "0.553.0",
87
90
  "@playwright/test": "^1.56.1",
91
+ "@sveltejs/adapter-static": "^3.0.10",
88
92
  "@sveltejs/kit": "^2.48.4",
89
93
  "@sveltejs/package": "^2.3.7",
90
94
  "@sveltejs/vite-plugin-svelte": "^6.2.1",
@@ -126,6 +130,7 @@
126
130
  "fast-average-color-node": "^3.1.0",
127
131
  "fs-extra": "^11.2.0",
128
132
  "globals": "^16.5.0",
133
+ "mdast-util-to-string": "^4.0.0",
129
134
  "mime": "^4.1.0",
130
135
  "ora": "^9.0.0",
131
136
  "picocolors": "^1.1.1",