zone5 0.0.0 → 1.0.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/LICENSE +21 -0
- package/README.md +258 -0
- package/dist/cli/index.js +33 -0
- package/dist/cli/index2.js +238 -0
- package/dist/cli/index3.js +53 -0
- package/dist/cli/templates/.zone5.toml +4 -0
- package/dist/cli/templates/app.css +3 -0
- package/dist/cli/templates/layout.svelte +15 -0
- package/dist/cli/templates/layout.ts +1 -0
- package/dist/components/Zone5.svelte +153 -0
- package/dist/components/Zone5.svelte.d.ts +12 -0
- package/dist/components/Zone5Img.svelte +103 -0
- package/dist/components/Zone5Img.svelte.d.ts +10 -0
- package/dist/components/Zone5Lightbox.svelte +131 -0
- package/dist/components/Zone5Lightbox.svelte.d.ts +12 -0
- package/dist/components/Zone5Provider.svelte +68 -0
- package/dist/components/Zone5Provider.svelte.d.ts +9 -0
- package/dist/components/atoms/Button.svelte +40 -0
- package/dist/components/atoms/Button.svelte.d.ts +13 -0
- package/dist/components/atoms/CloseButton.svelte +18 -0
- package/dist/components/atoms/CloseButton.svelte.d.ts +9 -0
- package/dist/components/atoms/NextButton.svelte +19 -0
- package/dist/components/atoms/NextButton.svelte.d.ts +10 -0
- package/dist/components/atoms/PrevButton.svelte +19 -0
- package/dist/components/atoms/PrevButton.svelte.d.ts +10 -0
- package/dist/components/atoms/index.d.ts +4 -0
- package/dist/components/atoms/index.js +5 -0
- package/dist/components/constants.d.ts +17 -0
- package/dist/components/constants.js +17 -0
- package/dist/components/index.d.ts +6 -0
- package/dist/components/index.js +7 -0
- package/dist/components/portal.d.ts +4 -0
- package/dist/components/portal.js +26 -0
- package/dist/components/types.d.ts +7 -0
- package/dist/components/types.js +1 -0
- package/dist/config.d.ts +51 -0
- package/dist/config.js +56 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +4 -0
- package/dist/module.d.ts +19 -0
- package/dist/processor/blurhash.d.ts +7 -0
- package/dist/processor/blurhash.js +37 -0
- package/dist/processor/color.d.ts +5 -0
- package/dist/processor/color.js +32 -0
- package/dist/processor/config.d.ts +12 -0
- package/dist/processor/config.js +9 -0
- package/dist/processor/exif/converters.d.ts +7 -0
- package/dist/processor/exif/converters.js +38 -0
- package/dist/processor/exif/defaults.d.ts +17 -0
- package/dist/processor/exif/defaults.js +17 -0
- package/dist/processor/exif/exif.d.ts +34 -0
- package/dist/processor/exif/exif.js +43 -0
- package/dist/processor/exif/index.d.ts +1 -0
- package/dist/processor/exif/index.js +1 -0
- package/dist/processor/exif/types.d.ts +4 -0
- package/dist/processor/exif/types.js +1 -0
- package/dist/processor/file.d.ts +3 -0
- package/dist/processor/file.js +28 -0
- package/dist/processor/index.d.ts +27 -0
- package/dist/processor/index.js +70 -0
- package/dist/processor/variants.d.ts +13 -0
- package/dist/processor/variants.js +83 -0
- package/dist/remark.d.ts +5 -0
- package/dist/remark.js +268 -0
- package/dist/stores/index.d.ts +1 -0
- package/dist/stores/index.js +1 -0
- package/dist/stores/registry.svelte.d.ts +22 -0
- package/dist/stores/registry.svelte.js +100 -0
- package/dist/test-setup.d.ts +9 -0
- package/dist/test-setup.js +25 -0
- package/dist/vite.d.ts +2 -0
- package/dist/vite.js +158 -0
- package/package.json +144 -5
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount } from 'svelte';
|
|
3
|
+
|
|
4
|
+
import Img from './Zone5Img.svelte';
|
|
5
|
+
import { useImageRegistry } from './Zone5Provider.svelte';
|
|
6
|
+
import {
|
|
7
|
+
DEFAULT_COLUMN_BREAKPOINTS,
|
|
8
|
+
SINGLE_IMAGE_HEIGHT_CLASS,
|
|
9
|
+
WALL_IMAGE_HEIGHT_CLASS,
|
|
10
|
+
} from './constants';
|
|
11
|
+
import type { ImageData } from './types';
|
|
12
|
+
|
|
13
|
+
interface Props {
|
|
14
|
+
columnBreakpoints?: { [key: number]: number };
|
|
15
|
+
images: ImageData[];
|
|
16
|
+
mode?: 'wall' | 'waterfall';
|
|
17
|
+
nocaption?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let {
|
|
21
|
+
columnBreakpoints = DEFAULT_COLUMN_BREAKPOINTS,
|
|
22
|
+
images,
|
|
23
|
+
mode = 'wall',
|
|
24
|
+
nocaption = false,
|
|
25
|
+
}: Props = $props();
|
|
26
|
+
|
|
27
|
+
const imageStore = useImageRegistry();
|
|
28
|
+
const componentId: symbol = Symbol('Zone5Component');
|
|
29
|
+
|
|
30
|
+
// Register images with the global image registry
|
|
31
|
+
$effect(() => {
|
|
32
|
+
if (imageStore) {
|
|
33
|
+
imageStore.register(componentId, images);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Cleanup on component unmount
|
|
38
|
+
onMount(() => {
|
|
39
|
+
return () => {
|
|
40
|
+
if (imageStore) {
|
|
41
|
+
imageStore.remove(componentId);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Opens the lightbox for the image at the specified index
|
|
48
|
+
*/
|
|
49
|
+
function handleImageClick(index: number): void {
|
|
50
|
+
if (imageStore) {
|
|
51
|
+
imageStore.setCurrent(componentId, index);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// WATERFALL MODE CALCULATIONS
|
|
56
|
+
let containerWidth = $state(0);
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Calculate number of columns based on container width and breakpoints
|
|
60
|
+
*/
|
|
61
|
+
let nColumns = $derived.by(() => {
|
|
62
|
+
if (mode !== 'waterfall') return 1;
|
|
63
|
+
|
|
64
|
+
let columns = 1;
|
|
65
|
+
const sortedBreakpoints = Object.entries(columnBreakpoints).sort(
|
|
66
|
+
([a], [b]) => Number(a) - Number(b),
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
for (const [breakpoint, cols] of sortedBreakpoints) {
|
|
70
|
+
if (containerWidth >= Number(breakpoint)) {
|
|
71
|
+
columns = cols;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return columns;
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Distribute images across columns in round-robin fashion
|
|
80
|
+
*/
|
|
81
|
+
let colPhotos = $derived.by(() => {
|
|
82
|
+
if (mode !== 'waterfall') return [];
|
|
83
|
+
|
|
84
|
+
const cols: { image: ImageData; idx: number }[][] = Array.from({ length: nColumns }, () => []);
|
|
85
|
+
images.forEach((image, idx) => {
|
|
86
|
+
cols[idx % nColumns].push({ image, idx });
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return cols;
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Calculate filler heights to equalize column heights in waterfall mode
|
|
94
|
+
*/
|
|
95
|
+
let colFillers = $derived.by(() => {
|
|
96
|
+
if (mode !== 'waterfall') return [];
|
|
97
|
+
|
|
98
|
+
const totalHeights = colPhotos.map((col) =>
|
|
99
|
+
col.reduce((sum, img) => sum + 1 / img.image.properties.aspectRatio, 0),
|
|
100
|
+
);
|
|
101
|
+
const maxHeight = Math.max(...totalHeights);
|
|
102
|
+
|
|
103
|
+
return totalHeights.map((height) => maxHeight - height);
|
|
104
|
+
});
|
|
105
|
+
</script>
|
|
106
|
+
|
|
107
|
+
{#if mode === 'wall' && images.length === 1}
|
|
108
|
+
{@const image = images[0]}
|
|
109
|
+
<figure class="flex justify-center" aria-label={image.properties.title || 'Image'}>
|
|
110
|
+
<Img
|
|
111
|
+
{image}
|
|
112
|
+
class={SINGLE_IMAGE_HEIGHT_CLASS}
|
|
113
|
+
onclick={imageStore ? () => handleImageClick(0) : undefined}
|
|
114
|
+
/>
|
|
115
|
+
{#if image.properties.title && !nocaption}
|
|
116
|
+
<figcaption class="mt-2 text-sm text-gray-600">
|
|
117
|
+
{image.properties.title}
|
|
118
|
+
</figcaption>
|
|
119
|
+
{/if}
|
|
120
|
+
</figure>
|
|
121
|
+
{:else if mode === 'wall'}
|
|
122
|
+
<div class="flex gap-2 flex-col md:flex-row md:flex-wrap zone5-wall" role="list">
|
|
123
|
+
{#each images as image, idx (idx)}
|
|
124
|
+
<div class="grow {WALL_IMAGE_HEIGHT_CLASS} flex" role="listitem">
|
|
125
|
+
<Img
|
|
126
|
+
{image}
|
|
127
|
+
cover
|
|
128
|
+
class="grow"
|
|
129
|
+
onclick={imageStore ? () => handleImageClick(idx) : undefined}
|
|
130
|
+
/>
|
|
131
|
+
</div>
|
|
132
|
+
{/each}
|
|
133
|
+
</div>
|
|
134
|
+
{:else if mode === 'waterfall'}
|
|
135
|
+
<div class="flex gap-2" bind:clientWidth={containerWidth} role="list">
|
|
136
|
+
{#each Array.from({ length: nColumns }, (_, i) => i) as columnId (columnId)}
|
|
137
|
+
<div class="flex flex-col gap-2" role="listitem">
|
|
138
|
+
{#each colPhotos[columnId] as { image, idx } (idx)}
|
|
139
|
+
<div>
|
|
140
|
+
<Img {image} onclick={imageStore ? () => handleImageClick(idx) : undefined} />
|
|
141
|
+
</div>
|
|
142
|
+
{/each}
|
|
143
|
+
{#if colFillers[columnId] > 0}
|
|
144
|
+
<div
|
|
145
|
+
class="bg-slate-200 rounded"
|
|
146
|
+
style:height={colFillers[columnId] * 100 + '%'}
|
|
147
|
+
aria-hidden="true"
|
|
148
|
+
></div>
|
|
149
|
+
{/if}
|
|
150
|
+
</div>
|
|
151
|
+
{/each}
|
|
152
|
+
</div>
|
|
153
|
+
{/if}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ImageData } from './types';
|
|
2
|
+
interface Props {
|
|
3
|
+
columnBreakpoints?: {
|
|
4
|
+
[key: number]: number;
|
|
5
|
+
};
|
|
6
|
+
images: ImageData[];
|
|
7
|
+
mode?: 'wall' | 'waterfall';
|
|
8
|
+
nocaption?: boolean;
|
|
9
|
+
}
|
|
10
|
+
declare const Zone5: import("svelte").Component<Props, {}, "">;
|
|
11
|
+
type Zone5 = ReturnType<typeof Zone5>;
|
|
12
|
+
export default Zone5;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Action } from 'svelte/action';
|
|
3
|
+
|
|
4
|
+
import type { ImageData } from './types';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
image: ImageData;
|
|
8
|
+
class?: string;
|
|
9
|
+
cover?: boolean;
|
|
10
|
+
onclick?: () => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let { image, class: _class, cover, onclick }: Props = $props();
|
|
14
|
+
|
|
15
|
+
const sizes = $derived.by(() => {
|
|
16
|
+
const widths = image.assets.map((a) => a.width).sort((a, b) => a - b);
|
|
17
|
+
const maxWidth = Math.max(...widths);
|
|
18
|
+
|
|
19
|
+
// Generate breakpoints based on actual asset widths
|
|
20
|
+
const breakpoints = [];
|
|
21
|
+
if (maxWidth >= 1200) breakpoints.push('(min-width: 1200px) 1200px');
|
|
22
|
+
if (maxWidth >= 768) breakpoints.push('(min-width: 768px) 768px');
|
|
23
|
+
breakpoints.push(`${Math.min(maxWidth, 640)}px`);
|
|
24
|
+
|
|
25
|
+
return breakpoints.join(', ');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
let img: HTMLImageElement;
|
|
29
|
+
let loaded = $state(false);
|
|
30
|
+
$effect(() => {
|
|
31
|
+
loaded = img.complete;
|
|
32
|
+
const handleLoad = () => (loaded = true);
|
|
33
|
+
img.addEventListener('load', handleLoad);
|
|
34
|
+
return () => {
|
|
35
|
+
img.removeEventListener('load', handleLoad);
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const useOnclick: Action<HTMLDivElement, (() => void) | undefined> = (node, handler) => {
|
|
40
|
+
const setHandler = (handler?: () => void | undefined) => {
|
|
41
|
+
const keyHandler = (event: KeyboardEvent) => {
|
|
42
|
+
if (!handler) return;
|
|
43
|
+
if (event.key === ' ' || event.key === 'Enter') {
|
|
44
|
+
event.preventDefault();
|
|
45
|
+
handler();
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
if (handler) {
|
|
49
|
+
node.onclick = handler;
|
|
50
|
+
node.onkeydown = keyHandler;
|
|
51
|
+
node.role = 'button';
|
|
52
|
+
node.tabIndex = 0;
|
|
53
|
+
node.classList.add('cursor-pointer');
|
|
54
|
+
} else {
|
|
55
|
+
node.onclick = null;
|
|
56
|
+
node.onkeydown = null;
|
|
57
|
+
node.role = null;
|
|
58
|
+
node.tabIndex = -1;
|
|
59
|
+
node.classList.remove('cursor-pointer');
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
setHandler(handler);
|
|
63
|
+
return {
|
|
64
|
+
update: (handler) => setHandler(handler),
|
|
65
|
+
destroy: () => setHandler(),
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
</script>
|
|
69
|
+
|
|
70
|
+
<div
|
|
71
|
+
use:useOnclick={onclick}
|
|
72
|
+
style="background-color:{image.properties.averageColor.hex}; --aspect-ratio: {image.properties
|
|
73
|
+
.aspectRatio}"
|
|
74
|
+
class={['aspect-ratio', 'relative', _class]}
|
|
75
|
+
data-zone5-img="true"
|
|
76
|
+
>
|
|
77
|
+
<picture class="w-full h-full flex justify-center relative mb-0 mt-0">
|
|
78
|
+
<img
|
|
79
|
+
src={image.assets[0].href}
|
|
80
|
+
alt={image.properties.alt}
|
|
81
|
+
title={image.properties.title}
|
|
82
|
+
srcset={image.assets.map((asset) => `${asset.href} ${asset.width}w`).join(', ')}
|
|
83
|
+
{sizes}
|
|
84
|
+
loading="lazy"
|
|
85
|
+
decoding="async"
|
|
86
|
+
class={[
|
|
87
|
+
'transition-opacity duration-100',
|
|
88
|
+
{
|
|
89
|
+
'object-cover h-full w-full': cover,
|
|
90
|
+
'opacity-0': !loaded,
|
|
91
|
+
'opacity-100': loaded,
|
|
92
|
+
},
|
|
93
|
+
]}
|
|
94
|
+
bind:this={img}
|
|
95
|
+
/>
|
|
96
|
+
</picture>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
<style>
|
|
100
|
+
.aspect-ratio {
|
|
101
|
+
aspect-ratio: var(--aspect-ratio) auto;
|
|
102
|
+
}
|
|
103
|
+
</style>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ImageData } from './types';
|
|
2
|
+
interface Props {
|
|
3
|
+
image: ImageData;
|
|
4
|
+
class?: string;
|
|
5
|
+
cover?: boolean;
|
|
6
|
+
onclick?: () => void;
|
|
7
|
+
}
|
|
8
|
+
declare const Zone5Img: import("svelte").Component<Props, {}, "">;
|
|
9
|
+
type Zone5Img = ReturnType<typeof Zone5Img>;
|
|
10
|
+
export default Zone5Img;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { fade } from 'svelte/transition';
|
|
3
|
+
|
|
4
|
+
import Img from './Zone5Img.svelte';
|
|
5
|
+
import CloseButton from './atoms/CloseButton.svelte';
|
|
6
|
+
import NextButton from './atoms/NextButton.svelte';
|
|
7
|
+
import PrevButton from './atoms/PrevButton.svelte';
|
|
8
|
+
import portal from './portal';
|
|
9
|
+
import type { ImageData } from './types';
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
force?: boolean;
|
|
13
|
+
image?: ImageData;
|
|
14
|
+
onclose: () => void;
|
|
15
|
+
onnext?: () => void;
|
|
16
|
+
onprevious?: () => void;
|
|
17
|
+
transitionDuration?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let { force, image, onclose, onnext, onprevious, transitionDuration = 300 }: Props = $props();
|
|
21
|
+
|
|
22
|
+
let transitioning = $state(false);
|
|
23
|
+
let visible = $derived(image && !transitioning);
|
|
24
|
+
let onFigureOutroEnd: (() => void) | undefined = undefined;
|
|
25
|
+
const nextHandler = () => {
|
|
26
|
+
transitioning = true;
|
|
27
|
+
onFigureOutroEnd = onnext;
|
|
28
|
+
};
|
|
29
|
+
const prevHandler = () => {
|
|
30
|
+
transitioning = true;
|
|
31
|
+
onFigureOutroEnd = onprevious;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const keyHandler = (evt: KeyboardEvent) => {
|
|
35
|
+
if (!image) return;
|
|
36
|
+
switch (evt.key) {
|
|
37
|
+
case 'Escape':
|
|
38
|
+
evt.preventDefault();
|
|
39
|
+
onclose();
|
|
40
|
+
break;
|
|
41
|
+
case ' ':
|
|
42
|
+
// Only prevent default for spacebar when dialog is active
|
|
43
|
+
evt.preventDefault();
|
|
44
|
+
if (visible) {
|
|
45
|
+
nextHandler();
|
|
46
|
+
}
|
|
47
|
+
break;
|
|
48
|
+
case 'ArrowRight':
|
|
49
|
+
if (visible) {
|
|
50
|
+
evt.preventDefault();
|
|
51
|
+
nextHandler();
|
|
52
|
+
}
|
|
53
|
+
break;
|
|
54
|
+
case 'ArrowLeft':
|
|
55
|
+
if (visible) {
|
|
56
|
+
evt.preventDefault();
|
|
57
|
+
prevHandler();
|
|
58
|
+
}
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
</script>
|
|
63
|
+
|
|
64
|
+
<svelte:window onkeydown={keyHandler} />
|
|
65
|
+
{#if image || force}
|
|
66
|
+
<section
|
|
67
|
+
use:portal
|
|
68
|
+
role="dialog"
|
|
69
|
+
transition:fade={{ duration: force ? 0 : transitionDuration }}
|
|
70
|
+
class="fixed z-50 inset-0 bg-zinc-50 text-zinc-950"
|
|
71
|
+
data-zone5-lightbox="true"
|
|
72
|
+
>
|
|
73
|
+
<nav>
|
|
74
|
+
<CloseButton
|
|
75
|
+
class={[
|
|
76
|
+
'absolute right-0 top-0 z-30 cursor-pointer p-4',
|
|
77
|
+
'transition delay-150 duration-300 hover:bg-zinc-100/10',
|
|
78
|
+
]}
|
|
79
|
+
{onclose}
|
|
80
|
+
data-zone5-close
|
|
81
|
+
/>
|
|
82
|
+
{#if onprevious}
|
|
83
|
+
<PrevButton
|
|
84
|
+
class={[
|
|
85
|
+
'absolute left-0 top-1/2 z-30 -translate-y-1/2 cursor-pointer p-2',
|
|
86
|
+
'transition delay-150 duration-300 hover:bg-zinc-100/10',
|
|
87
|
+
]}
|
|
88
|
+
disabled={!visible}
|
|
89
|
+
onprevious={prevHandler}
|
|
90
|
+
data-zone5-prev
|
|
91
|
+
/>
|
|
92
|
+
{/if}
|
|
93
|
+
{#if onnext}
|
|
94
|
+
<NextButton
|
|
95
|
+
class={[
|
|
96
|
+
'absolute right-0 top-1/2 z-30 -translate-y-1/2 cursor-pointer p-2',
|
|
97
|
+
'transition delay-150 duration-300 hover:bg-zinc-100/10',
|
|
98
|
+
]}
|
|
99
|
+
disabled={!visible}
|
|
100
|
+
onnext={nextHandler}
|
|
101
|
+
data-zone5-next
|
|
102
|
+
/>
|
|
103
|
+
{/if}
|
|
104
|
+
</nav>
|
|
105
|
+
{#if image && visible}
|
|
106
|
+
<figure
|
|
107
|
+
transition:fade={{ duration: transitionDuration }}
|
|
108
|
+
onoutroend={() => {
|
|
109
|
+
onFigureOutroEnd?.();
|
|
110
|
+
transitioning = false;
|
|
111
|
+
}}
|
|
112
|
+
class="h-full w-full flex flex-col justify-between items-center p-8"
|
|
113
|
+
>
|
|
114
|
+
<div class="flex-1 flex items-center justify-center min-h-0">
|
|
115
|
+
<Img {image} class="min-h-0 max-h-full" />
|
|
116
|
+
</div>
|
|
117
|
+
{#if image.properties.title}
|
|
118
|
+
<figcaption class="pt-2 text-center">
|
|
119
|
+
{image.properties.title}
|
|
120
|
+
</figcaption>
|
|
121
|
+
{/if}
|
|
122
|
+
</figure>
|
|
123
|
+
{/if}
|
|
124
|
+
</section>
|
|
125
|
+
{/if}
|
|
126
|
+
|
|
127
|
+
<style>
|
|
128
|
+
:global(html:has([role='dialog'])) {
|
|
129
|
+
overflow: hidden;
|
|
130
|
+
}
|
|
131
|
+
</style>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ImageData } from './types';
|
|
2
|
+
interface Props {
|
|
3
|
+
force?: boolean;
|
|
4
|
+
image?: ImageData;
|
|
5
|
+
onclose: () => void;
|
|
6
|
+
onnext?: () => void;
|
|
7
|
+
onprevious?: () => void;
|
|
8
|
+
transitionDuration?: number;
|
|
9
|
+
}
|
|
10
|
+
declare const Zone5Lightbox: import("svelte").Component<Props, {}, "">;
|
|
11
|
+
type Zone5Lightbox = ReturnType<typeof Zone5Lightbox>;
|
|
12
|
+
export default Zone5Lightbox;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import { getContext, setContext } from 'svelte';
|
|
3
|
+
|
|
4
|
+
const key = Symbol('Zone5 provider');
|
|
5
|
+
export const useImageRegistry = () => getContext<Registry>(key);
|
|
6
|
+
</script>
|
|
7
|
+
|
|
8
|
+
<script lang="ts">
|
|
9
|
+
import type { Snippet } from 'svelte';
|
|
10
|
+
import { queryParameters } from 'sveltekit-search-params';
|
|
11
|
+
|
|
12
|
+
import { beforeNavigate } from '$app/navigation';
|
|
13
|
+
|
|
14
|
+
import { type Registry, registry } from '../stores';
|
|
15
|
+
import Zone5Lightbox from './Zone5Lightbox.svelte';
|
|
16
|
+
|
|
17
|
+
let { children }: { children: Snippet } = $props();
|
|
18
|
+
|
|
19
|
+
setContext<Registry>(key, registry);
|
|
20
|
+
|
|
21
|
+
// URL state store
|
|
22
|
+
const params = queryParameters({
|
|
23
|
+
z5: true,
|
|
24
|
+
});
|
|
25
|
+
let optimisticForce = $state($params.z5 !== null);
|
|
26
|
+
|
|
27
|
+
// clear registry when navigating away
|
|
28
|
+
beforeNavigate((navigation) => {
|
|
29
|
+
if (navigation.from?.route.id == navigation.to?.route.id) return;
|
|
30
|
+
registry.clear();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// update URL state, once Svelte's router has settled
|
|
34
|
+
let routerSettled = false;
|
|
35
|
+
$effect(function updateZ5UrlState() {
|
|
36
|
+
const current = $registry.current?.id ?? null;
|
|
37
|
+
if (routerSettled || current) {
|
|
38
|
+
routerSettled = true;
|
|
39
|
+
if (current !== $params.z5) {
|
|
40
|
+
$params.z5 = current;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// update from URL state
|
|
46
|
+
$effect(function updateZ5fromUrl() {
|
|
47
|
+
const z5 = $params.z5;
|
|
48
|
+
const current = $registry.current?.id ?? null;
|
|
49
|
+
if (z5 && z5 !== current) {
|
|
50
|
+
if (!registry.findCurrent(z5)) {
|
|
51
|
+
optimisticForce = false;
|
|
52
|
+
$params.z5 = null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
</script>
|
|
57
|
+
|
|
58
|
+
<Zone5Lightbox
|
|
59
|
+
force={optimisticForce}
|
|
60
|
+
image={$registry.current ?? undefined}
|
|
61
|
+
onclose={() => {
|
|
62
|
+
optimisticForce = false;
|
|
63
|
+
registry.clearCurrent();
|
|
64
|
+
}}
|
|
65
|
+
onprevious={registry.prev}
|
|
66
|
+
onnext={registry.next}
|
|
67
|
+
/>
|
|
68
|
+
{@render children()}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare const useImageRegistry: () => Registry;
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import { type Registry } from '../stores';
|
|
4
|
+
type $$ComponentProps = {
|
|
5
|
+
children: Snippet;
|
|
6
|
+
};
|
|
7
|
+
declare const Zone5Provider: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
8
|
+
type Zone5Provider = ReturnType<typeof Zone5Provider>;
|
|
9
|
+
export default Zone5Provider;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
'aria-label'?: string;
|
|
6
|
+
children: Snippet;
|
|
7
|
+
disabled?: boolean;
|
|
8
|
+
class?: string | string[];
|
|
9
|
+
onaction: () => void;
|
|
10
|
+
tabindex?: number;
|
|
11
|
+
[key: string]: unknown;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let {
|
|
15
|
+
'aria-label': aria_label,
|
|
16
|
+
children,
|
|
17
|
+
class: class_,
|
|
18
|
+
disabled = false,
|
|
19
|
+
onaction,
|
|
20
|
+
tabindex = 0,
|
|
21
|
+
...rest
|
|
22
|
+
}: Props = $props();
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<button
|
|
26
|
+
class={class_}
|
|
27
|
+
{disabled}
|
|
28
|
+
onclick={onaction}
|
|
29
|
+
onkeydown={(evt) => {
|
|
30
|
+
if (evt.key == ' ' || evt.key == 'Enter') {
|
|
31
|
+
evt.preventDefault();
|
|
32
|
+
onaction();
|
|
33
|
+
}
|
|
34
|
+
}}
|
|
35
|
+
{tabindex}
|
|
36
|
+
aria-label={aria_label}
|
|
37
|
+
{...rest}
|
|
38
|
+
>
|
|
39
|
+
{@render children()}
|
|
40
|
+
</button>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
interface Props {
|
|
3
|
+
'aria-label'?: string;
|
|
4
|
+
children: Snippet;
|
|
5
|
+
disabled?: boolean;
|
|
6
|
+
class?: string | string[];
|
|
7
|
+
onaction: () => void;
|
|
8
|
+
tabindex?: number;
|
|
9
|
+
[key: string]: unknown;
|
|
10
|
+
}
|
|
11
|
+
declare const Button: import("svelte").Component<Props, {}, "">;
|
|
12
|
+
type Button = ReturnType<typeof Button>;
|
|
13
|
+
export default Button;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { X } from '@lucide/svelte';
|
|
3
|
+
|
|
4
|
+
import Button from './Button.svelte';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
class?: string | string[];
|
|
8
|
+
onclose: () => void;
|
|
9
|
+
tabindex?: number;
|
|
10
|
+
[key: string]: unknown;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let { class: class_, onclose, tabindex = 0, ...rest }: Props = $props();
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<Button class={class_} onaction={onclose} {tabindex} aria-label="close" {...rest}>
|
|
17
|
+
<X />
|
|
18
|
+
</Button>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
class?: string | string[];
|
|
3
|
+
onclose: () => void;
|
|
4
|
+
tabindex?: number;
|
|
5
|
+
[key: string]: unknown;
|
|
6
|
+
}
|
|
7
|
+
declare const CloseButton: import("svelte").Component<Props, {}, "">;
|
|
8
|
+
type CloseButton = ReturnType<typeof CloseButton>;
|
|
9
|
+
export default CloseButton;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { ChevronRight } from '@lucide/svelte';
|
|
3
|
+
|
|
4
|
+
import Button from './Button.svelte';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
class?: string | string[];
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
onnext: () => void;
|
|
10
|
+
tabindex?: number;
|
|
11
|
+
[key: string]: unknown;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let { class: class_, disabled, onnext, tabindex = 0, ...rest }: Props = $props();
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<Button class={class_} {disabled} onaction={onnext} {tabindex} aria-label="next" {...rest}>
|
|
18
|
+
<ChevronRight />
|
|
19
|
+
</Button>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
class?: string | string[];
|
|
3
|
+
disabled?: boolean;
|
|
4
|
+
onnext: () => void;
|
|
5
|
+
tabindex?: number;
|
|
6
|
+
[key: string]: unknown;
|
|
7
|
+
}
|
|
8
|
+
declare const NextButton: import("svelte").Component<Props, {}, "">;
|
|
9
|
+
type NextButton = ReturnType<typeof NextButton>;
|
|
10
|
+
export default NextButton;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { ChevronLeft } from '@lucide/svelte';
|
|
3
|
+
|
|
4
|
+
import Button from './Button.svelte';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
class?: string | string[];
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
onprevious: () => void;
|
|
10
|
+
tabindex?: number;
|
|
11
|
+
[key: string]: unknown;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let { class: class_, disabled, onprevious, tabindex = 0, ...rest }: Props = $props();
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<Button class={class_} {disabled} onaction={onprevious} {tabindex} aria-label="previous" {...rest}>
|
|
18
|
+
<ChevronLeft />
|
|
19
|
+
</Button>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
class?: string | string[];
|
|
3
|
+
disabled?: boolean;
|
|
4
|
+
onprevious: () => void;
|
|
5
|
+
tabindex?: number;
|
|
6
|
+
[key: string]: unknown;
|
|
7
|
+
}
|
|
8
|
+
declare const PrevButton: import("svelte").Component<Props, {}, "">;
|
|
9
|
+
type PrevButton = ReturnType<typeof PrevButton>;
|
|
10
|
+
export default PrevButton;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default column breakpoints for waterfall mode.
|
|
3
|
+
* Maps viewport width (in pixels) to number of columns.
|
|
4
|
+
*/
|
|
5
|
+
export declare const DEFAULT_COLUMN_BREAKPOINTS: {
|
|
6
|
+
readonly 640: 2;
|
|
7
|
+
readonly 768: 3;
|
|
8
|
+
readonly 1024: 4;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Height for single image in wall mode (Tailwind class)
|
|
12
|
+
*/
|
|
13
|
+
export declare const SINGLE_IMAGE_HEIGHT_CLASS = "h-96";
|
|
14
|
+
/**
|
|
15
|
+
* Height for multiple images in wall mode (Tailwind class)
|
|
16
|
+
*/
|
|
17
|
+
export declare const WALL_IMAGE_HEIGHT_CLASS = "h-96";
|