lumosaic 1.1.4
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/README.md +212 -0
- package/dist/components/Lumosaic.vue.d.ts +46 -0
- package/dist/components/lumosaic.d.ts +59 -0
- package/dist/index.d.ts +8 -0
- package/dist/lumosaic.cjs.js +1 -0
- package/dist/lumosaic.es.js +300 -0
- package/dist/nuxt/module.d.ts +2 -0
- package/dist/nuxt-module.cjs.js +1 -0
- package/dist/nuxt-module.d.ts +3 -0
- package/dist/nuxt-module.es.js +17 -0
- package/dist/style.css +1 -0
- package/package.json +60 -0
package/README.md
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# Lumosaic
|
|
2
|
+
|
|
3
|
+
**Adaptive, data-driven image and video mosaic gallery for Vue 3 and Nuxt 4**
|
|
4
|
+
|
|
5
|
+
Lumosaic is a lightweight Vue 3 component that automatically arranges photos of any orientation into perfectly aligned rows spanning the full screen width. It intelligently calculates image dimensions and creates a beautiful, responsive gallery layout without relying on external CSS frameworks.
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
## Live demo
|
|
10
|
+
|
|
11
|
+
See it in action: [lumosaic.syntheticsymbiosis.com](https://lumosaic.syntheticsymbiosis.com)
|
|
12
|
+
|
|
13
|
+
## Features
|
|
14
|
+
|
|
15
|
+
- **Vue 3 & Nuxt 4 Ready** - Native component for modern reactive applications.
|
|
16
|
+
- **Intelligent Layout** - Automatically arranges images into perfectly aligned rows.
|
|
17
|
+
- **Responsive Design** - Adapts to different screen sizes with customizable row heights.
|
|
18
|
+
- **Auto Dimension Detection** - Automatically retrieves image dimensions from image files (PNG, JPEG, WebP) if missing.
|
|
19
|
+
- **Event-Driven Architecture** - Emits clean events for seamless integration with lightboxes.
|
|
20
|
+
- **Highly Configurable** - Extensive props for customization.
|
|
21
|
+
- **Lightweight** - Clean TypeScript core, zero heavy dependencies.
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
Install the package via npm:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install lumosaic
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Quick Start
|
|
32
|
+
|
|
33
|
+
### Vue 3
|
|
34
|
+
|
|
35
|
+
1. Import the global CSS in your main entry file (e.g., `main.ts`):
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import { createApp } from 'vue'
|
|
39
|
+
import App from './App.vue'
|
|
40
|
+
import 'lumosaic/dist/style.css' // Import styles
|
|
41
|
+
|
|
42
|
+
createApp(App).mount('#app')
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
2. Use the component in your templates:
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
<template>
|
|
49
|
+
<Lumosaic :images="images" :gap="8" />
|
|
50
|
+
</template>
|
|
51
|
+
|
|
52
|
+
<script setup lang="ts">
|
|
53
|
+
import { Lumosaic } from 'lumosaic'
|
|
54
|
+
|
|
55
|
+
const images = [
|
|
56
|
+
{ src: "[https://picsum.photos/800/600?random=1](https://picsum.photos/800/600?random=1)", width: 800, height: 600 },
|
|
57
|
+
{ src: "[https://picsum.photos/600/800?random=2](https://picsum.photos/600/800?random=2)", width: 600, height: 800 },
|
|
58
|
+
{ src: "[https://picsum.photos/800/800?random=3](https://picsum.photos/800/800?random=3)", width: 800, height: 800 }
|
|
59
|
+
]
|
|
60
|
+
</script>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Nuxt 4
|
|
64
|
+
|
|
65
|
+
Lumosaic comes with a built-in Nuxt module. Simply add it to your `nuxt.config.ts`. The module automatically registers the component and injects the CSS.
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
export default defineNuxtConfig({
|
|
69
|
+
modules: [
|
|
70
|
+
'lumosaic/nuxt'
|
|
71
|
+
]
|
|
72
|
+
})
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Now you can use `<Lumosaic />` anywhere in your Nuxt app without manually importing it!
|
|
76
|
+
|
|
77
|
+
## Props (Options)
|
|
78
|
+
|
|
79
|
+
Customize the gallery by passing props to the `<Lumosaic>` component.
|
|
80
|
+
|
|
81
|
+
### Row Height Props
|
|
82
|
+
|
|
83
|
+
|Property |Type |Default |Description |
|
|
84
|
+
|---|---|---|---|
|
|
85
|
+
|`row-height-sm` |Number |`0.25` |Height/width desired row ratio for mobile devices (screen width < 768px) |
|
|
86
|
+
|`row-height-md` |Number |`0.2` |Height/width desired row ratio for medium devices (screen width >= 768px and < 1024px) |
|
|
87
|
+
|`row-height-xl` |Number |`0.18` |Height/width desired row ratio for extra large devices (screen width >= 1024px) |
|
|
88
|
+
|`row-height` |Number |`undefined` |Generic ratio for all screen sizes (overwrites SM, MD, and XL if provided) |
|
|
89
|
+
|
|
90
|
+
### Image Configuration
|
|
91
|
+
|
|
92
|
+
|Property |Type |Default |Description |
|
|
93
|
+
|---|---|---|---|
|
|
94
|
+
|`should-retrieve-width-and-height`|Boolean|`false`|If `true`, automatically fetches dimensions from the file when missing|
|
|
95
|
+
|`fallback-image-width`|Number|`1000`|Fallback width in pixels if dimensions cannot be retrieved|
|
|
96
|
+
|`fallback-image-height`|Number|`1000`|Fallback height in pixels if dimensions cannot be retrieved|
|
|
97
|
+
|`max-image-ratio`|Number|`1.6`|Maximum width/height ratio allowed for images|
|
|
98
|
+
|`min-image-ratio`|Number|`0.65`|Minimum width/height ratio allowed for images|
|
|
99
|
+
|
|
100
|
+
### Layout Props
|
|
101
|
+
|
|
102
|
+
|Property |Type |Default |Description |
|
|
103
|
+
|---|---|---|---|
|
|
104
|
+
|`max-rows`|Number|`0`|Maximum number of rows to display. Set to `0` for no limit|
|
|
105
|
+
|`stretch-last-row`|Boolean|`true`|Stretches the last row to fill the container|
|
|
106
|
+
|`shuffle-images`|Boolean|`false`|Shuffles images randomly before rendering|
|
|
107
|
+
|`gap`|Number|`4`|Gap in pixels between images (horizontal and vertical)|
|
|
108
|
+
|`play-button-on-video-cover`|Boolean|`true`|Displays a play icon over video files|
|
|
109
|
+
|`observe-window-width`|Boolean|`true`|Re-renders the gallery layout on window resize|
|
|
110
|
+
|
|
111
|
+
## Events
|
|
112
|
+
|
|
113
|
+
The component emits events that allow you to react to user interactions without tightly coupling logic.
|
|
114
|
+
|
|
115
|
+
`@image-click`
|
|
116
|
+
Fired when an image or video in the gallery is clicked.
|
|
117
|
+
|
|
118
|
+
**Payload:**
|
|
119
|
+
- `index` (Number): The global index of the clicked item in the `images` array.
|
|
120
|
+
- `images` (Array): The complete array of image objects currently rendered.
|
|
121
|
+
|
|
122
|
+
## Exposed Methods
|
|
123
|
+
|
|
124
|
+
You can trigger internal core logic directly by assigning a Template Ref to the component.
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
<template>
|
|
128
|
+
<Lumosaic ref="myGallery" :images="images" />
|
|
129
|
+
<button @click="shuffleGallery">Shuffle!</button>
|
|
130
|
+
</template>
|
|
131
|
+
|
|
132
|
+
<script setup lang="ts">
|
|
133
|
+
import { ref } from 'vue'
|
|
134
|
+
import { Lumosaic } from 'lumosaic'
|
|
135
|
+
|
|
136
|
+
const myGallery = ref<InstanceType<typeof Lumosaic> | null>(null)
|
|
137
|
+
|
|
138
|
+
function shuffleGallery() {
|
|
139
|
+
myGallery.value?.shuffle()
|
|
140
|
+
}
|
|
141
|
+
</script>
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Available exposed methods:
|
|
145
|
+
- `shuffle()`: Instantly shuffles the current grid layout.
|
|
146
|
+
|
|
147
|
+
## Image Object Interface
|
|
148
|
+
|
|
149
|
+
The `images` prop expects an array of objects.
|
|
150
|
+
|
|
151
|
+
### Image Configuration
|
|
152
|
+
|
|
153
|
+
|Prop |Type |Required |Description |
|
|
154
|
+
|---|---|---|---|
|
|
155
|
+
|`src`|String|`Yes`|Full-size image or video URL|
|
|
156
|
+
|`preview`|String|`No`|Preview/thumbnail URL (defaults to `src` if omitted)|
|
|
157
|
+
|`width`||`Recommended`|Image width in pixels|
|
|
158
|
+
|`height`||`Recommended`|Image height in pixels|
|
|
159
|
+
|`alt`|String|`No`|Alt text for accessibility|
|
|
160
|
+
|`title`|String|`No`|Title attribute|
|
|
161
|
+
|`exif`|Object|`No`|EXIF object that can be returned stringified with `@image-click` event|
|
|
162
|
+
|
|
163
|
+
Example:
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
{
|
|
167
|
+
src: "[https://example.com/full-size.jpg](https://example.com/full-size.jpg)",
|
|
168
|
+
preview: "[https://example.com/preview.jpg](https://example.com/preview.jpg)",
|
|
169
|
+
width: 1920,
|
|
170
|
+
height: 1080,
|
|
171
|
+
alt: "Beautiful landscape"
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Integration with Obsidium Lightbox
|
|
176
|
+
|
|
177
|
+
Lumosaic is designed to work perfectly with lightboxes like [Obsidium](https://obsidium.syntheticsymbiosis.com). Since Lumosaic exposes the `@image-click` event, you can keep a single instance of your lightbox at the root of your application and feed data to it from multiple galleries.
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
<template>
|
|
181
|
+
<div>
|
|
182
|
+
<Lumosaic :images="vacationImages" @image-click="openLightbox" />
|
|
183
|
+
|
|
184
|
+
<Lumosaic :images="workImages" @image-click="openLightbox" />
|
|
185
|
+
|
|
186
|
+
<Obsidium ref="lightbox" />
|
|
187
|
+
</div>
|
|
188
|
+
</template>
|
|
189
|
+
|
|
190
|
+
<script setup lang="ts">
|
|
191
|
+
import { ref } from 'vue'
|
|
192
|
+
|
|
193
|
+
const lightbox = ref(null)
|
|
194
|
+
const vacationImages = [ /* ... */ ]
|
|
195
|
+
const workImages = [ /* ... */ ]
|
|
196
|
+
|
|
197
|
+
// Handle the event emitted by Lumosaic
|
|
198
|
+
function openLightbox(index: number, currentImages: any[]) {
|
|
199
|
+
// Pass the images and starting index to your lightbox
|
|
200
|
+
lightbox.value?.open(currentImages, index)
|
|
201
|
+
}
|
|
202
|
+
</script>
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## License
|
|
206
|
+
|
|
207
|
+
Released under the [MIT License](https://www.google.com/search?q=MIT%20LICENSE).
|
|
208
|
+
|
|
209
|
+
## Links
|
|
210
|
+
|
|
211
|
+
- [Documentation & Demo](https://lumosaic.syntheticsymbiosis.com/)
|
|
212
|
+
- [GitHub Repository](https://github.com/volkar/lumosaic)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { LumosaicOptions, LumosaicImage } from './lumosaic';
|
|
2
|
+
|
|
3
|
+
interface Props extends Partial<LumosaicOptions> {
|
|
4
|
+
images: Partial<LumosaicImage>[];
|
|
5
|
+
}
|
|
6
|
+
declare const _default: import('vue').DefineComponent<import('vue').ExtractPropTypes<__VLS_WithDefaults<__VLS_TypePropsToRuntimeProps<Props>, LumosaicOptions>>, {
|
|
7
|
+
shuffle: () => void;
|
|
8
|
+
}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {
|
|
9
|
+
"image-click": (index: number, images: Partial<LumosaicImage>[]) => void;
|
|
10
|
+
}, string, import('vue').PublicProps, Readonly<import('vue').ExtractPropTypes<__VLS_WithDefaults<__VLS_TypePropsToRuntimeProps<Props>, LumosaicOptions>>> & Readonly<{
|
|
11
|
+
"onImage-click"?: ((index: number, images: Partial<LumosaicImage>[]) => any) | undefined;
|
|
12
|
+
}>, {
|
|
13
|
+
rowHeightSm: number;
|
|
14
|
+
rowHeightMd: number;
|
|
15
|
+
rowHeightXl: number;
|
|
16
|
+
shouldRetrieveWidthAndHeight: boolean;
|
|
17
|
+
fallbackImageWidth: number;
|
|
18
|
+
fallbackImageHeight: number;
|
|
19
|
+
maxImageRatio: number;
|
|
20
|
+
minImageRatio: number;
|
|
21
|
+
maxRows: number;
|
|
22
|
+
stretchLastRow: boolean;
|
|
23
|
+
shuffleImages: boolean;
|
|
24
|
+
gap: number;
|
|
25
|
+
playButtonOnVideoCover: boolean;
|
|
26
|
+
observeWindowWidth: boolean;
|
|
27
|
+
rowHeight: number;
|
|
28
|
+
}, {}, {}, {}, string, import('vue').ComponentProvideOptions, true, {}, any>;
|
|
29
|
+
export default _default;
|
|
30
|
+
type __VLS_NonUndefinedable<T> = T extends undefined ? never : T;
|
|
31
|
+
type __VLS_TypePropsToRuntimeProps<T> = {
|
|
32
|
+
[K in keyof T]-?: {} extends Pick<T, K> ? {
|
|
33
|
+
type: import('vue').PropType<__VLS_NonUndefinedable<T[K]>>;
|
|
34
|
+
} : {
|
|
35
|
+
type: import('vue').PropType<T[K]>;
|
|
36
|
+
required: true;
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
type __VLS_WithDefaults<P, D> = {
|
|
40
|
+
[K in keyof Pick<P, keyof P>]: K extends keyof D ? __VLS_Prettify<P[K] & {
|
|
41
|
+
default: D[K];
|
|
42
|
+
}> : P[K];
|
|
43
|
+
};
|
|
44
|
+
type __VLS_Prettify<T> = {
|
|
45
|
+
[K in keyof T]: T[K];
|
|
46
|
+
} & {};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export interface LumosaicOptions {
|
|
2
|
+
rowHeightSm: number;
|
|
3
|
+
rowHeightMd: number;
|
|
4
|
+
rowHeightXl: number;
|
|
5
|
+
shouldRetrieveWidthAndHeight: boolean;
|
|
6
|
+
fallbackImageWidth: number;
|
|
7
|
+
fallbackImageHeight: number;
|
|
8
|
+
maxImageRatio: number;
|
|
9
|
+
minImageRatio: number;
|
|
10
|
+
maxRows: number;
|
|
11
|
+
stretchLastRow: boolean;
|
|
12
|
+
shuffleImages: boolean;
|
|
13
|
+
gap: number;
|
|
14
|
+
playButtonOnVideoCover: boolean;
|
|
15
|
+
observeWindowWidth: boolean;
|
|
16
|
+
rowHeight?: number;
|
|
17
|
+
}
|
|
18
|
+
export interface LumosaicImage {
|
|
19
|
+
src: string;
|
|
20
|
+
preview: string;
|
|
21
|
+
width: number;
|
|
22
|
+
height: number;
|
|
23
|
+
srcWidth?: number;
|
|
24
|
+
srcHeight?: number;
|
|
25
|
+
displayWidth?: number;
|
|
26
|
+
displayHeight?: number;
|
|
27
|
+
alt?: string;
|
|
28
|
+
title?: string;
|
|
29
|
+
exif?: any;
|
|
30
|
+
}
|
|
31
|
+
export interface LumosaicParams {
|
|
32
|
+
imagesSource: Partial<LumosaicImage>[];
|
|
33
|
+
}
|
|
34
|
+
export declare const defaultLumosaicOptions: LumosaicOptions;
|
|
35
|
+
export declare class Lumosaic {
|
|
36
|
+
private options;
|
|
37
|
+
private params;
|
|
38
|
+
private gallery;
|
|
39
|
+
private images;
|
|
40
|
+
private lastRenderedScreenSize;
|
|
41
|
+
private resizeObserver;
|
|
42
|
+
private targetRowHeight;
|
|
43
|
+
constructor(galleryElement: HTMLElement, imagesSource: Partial<LumosaicImage>[]);
|
|
44
|
+
init(userConfig?: Partial<LumosaicOptions>): Promise<this | void>;
|
|
45
|
+
replaceImages(images: Partial<LumosaicImage>[]): void;
|
|
46
|
+
shuffleImages(): void;
|
|
47
|
+
updateOptions(newOptions: Partial<LumosaicOptions>): void;
|
|
48
|
+
destroy(): void;
|
|
49
|
+
private initResizeObserver;
|
|
50
|
+
private getObservedWidth;
|
|
51
|
+
private processParams;
|
|
52
|
+
private normalizeImageData;
|
|
53
|
+
private getImageSizeFromUrl;
|
|
54
|
+
private getUint24;
|
|
55
|
+
private computeRows;
|
|
56
|
+
private calculateRowLayout;
|
|
57
|
+
private renderGallery;
|
|
58
|
+
private mergeOptions;
|
|
59
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";var b=Object.defineProperty;var W=(g,t,e)=>t in g?b(g,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):g[t]=e;var f=(g,t,e)=>W(g,typeof t!="symbol"?t+"":t,e);Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const p=require("vue"),v={rowHeightSm:.25,rowHeightMd:.2,rowHeightXl:.18,shouldRetrieveWidthAndHeight:!1,fallbackImageWidth:1e3,fallbackImageHeight:1e3,maxImageRatio:1.6,minImageRatio:.65,maxRows:0,stretchLastRow:!0,shuffleImages:!1,gap:4,playButtonOnVideoCover:!0,observeWindowWidth:!0};class H{constructor(t,e){f(this,"options");f(this,"params");f(this,"gallery");f(this,"images",[]);f(this,"lastRenderedScreenSize",null);f(this,"resizeObserver",null);f(this,"targetRowHeight",0);this.options={...v},this.params={imagesSource:e},this.gallery=t}async init(t={}){return console.log(t),this.mergeOptions(t),this.gallery.classList.add("lumosaic-loading"),await this.processParams(),this.options.shuffleImages?this.shuffleImages():this.renderGallery(),this.lastRenderedScreenSize=this.getObservedWidth(),this.gallery.classList.remove("lumosaic-loading"),this.initResizeObserver(),this}replaceImages(t){this.params.imagesSource=t,this.processParams().then(()=>this.renderGallery())}shuffleImages(){this.images.sort(()=>Math.random()-.5),this.renderGallery()}updateOptions(t){this.mergeOptions(t),this.renderGallery()}destroy(){this.resizeObserver&&this.resizeObserver.disconnect()}initResizeObserver(){this.resizeObserver=new ResizeObserver(()=>{const t=this.getObservedWidth();t&&this.lastRenderedScreenSize!==t&&(this.renderGallery(),this.lastRenderedScreenSize=t)}),this.options.observeWindowWidth?this.resizeObserver.observe(document.body):this.gallery&&this.resizeObserver.observe(this.gallery)}getObservedWidth(){let t=0;return this.options.observeWindowWidth?t=window.innerWidth:this.gallery&&(t=this.gallery.offsetWidth),t&&t>=1024?"xl":t&&t>=768&&t<1024?"md":t&&t<768?"sm":!1}async processParams(){const t=this.params.imagesSource.map(e=>this.normalizeImageData({...e}));this.images=await Promise.all(t)}async normalizeImageData(t){t.src&&!t.preview&&(t.preview=t.src),t.preview&&!t.src&&(t.src=t.preview);const e=t;if(e.src||(e.src=""),e.preview||(e.preview=""),!e.width||!e.height)if(this.options.shouldRetrieveWidthAndHeight&&e.src)try{const o=await this.getImageSizeFromUrl(e.src);e.width=o.width,e.height=o.height}catch(o){console.warn(`Lumosaic: Could not fetch size for ${e.src}`,o),e.width=0,e.height=0}else e.width=0,e.height=0;return e.srcWidth=e.width,e.srcHeight=e.height,e}async getImageSizeFromUrl(t){const e={PNG:2303741511,PNG_END:218765834,JPEG_START:65496,WEBP_RIFF:1380533830,WEBP_WEBP:1464156752,VP8:1448097824,VP8L:1448097868,VP8X:1448097880},o=await fetch(t,{headers:{Range:"bytes=0-65535"}});if(!o.ok)throw new Error(`Failed to fetch: ${o.status}`);const a=await o.arrayBuffer(),s=new DataView(a);if(s.getUint32(0)===e.PNG&&s.getUint32(4)===e.PNG_END)return{width:s.getUint32(16),height:s.getUint32(20),type:"png"};if(s.getUint16(0)===e.JPEG_START){let i=2;for(;i<s.byteLength&&s.getUint8(i)===255;){const r=s.getUint8(i+1),n=s.getUint16(i+2);if(r>=192&&r<=207&&![196,200,204].includes(r))return{height:s.getUint16(i+5),width:s.getUint16(i+7),type:"jpeg"};i+=2+n}}if(s.getUint32(0,!1)===e.WEBP_RIFF&&s.getUint32(8,!1)===e.WEBP_WEBP){let i=12;for(;i<s.byteLength;){const r=s.getUint32(i,!1),n=s.getUint32(i+4,!0);if(r===e.VP8){const h=i+10;return{width:s.getUint16(h+6,!0)&16383,height:s.getUint16(h+8,!0)&16383,type:"webp"}}else if(r===e.VP8L){const h=s.getUint8(i+8),l=s.getUint8(i+9),c=s.getUint8(i+10),w=s.getUint8(i+11),u=1+((l&63)<<8|h),d=1+((w&15)<<10|c<<2|(l&192)>>6);return{width:u,height:d,type:"webp"}}else if(r===e.VP8X){const h=1+this.getUint24(s,i+12,!0),l=1+this.getUint24(s,i+15,!0);return{width:h,height:l,type:"webp"}}i+=8+n+n%2}}return{width:0,height:0,type:"unknown"}}getUint24(t,e,o){return o?t.getUint8(e)|t.getUint8(e+1)<<8|t.getUint8(e+2)<<16:t.getUint8(e)<<16|t.getUint8(e+1)<<8|t.getUint8(e+2)}computeRows(t,e){const o=[];let a=[],s=0;for(const i of t){const n=(i.height>0?i.width/i.height:1)*this.targetRowHeight,h=s+n+a.length*this.options.gap;if(a.length===0||h<e)a.push(i),s+=n;else if(h>e&&h-e<e-s)a.push(i),s+=n;else{if(a.length>0&&(o.push([...a]),this.options.maxRows&&o.length>=this.options.maxRows)){a=[];break}a=[i],s=n}}if(a.length>0)if(this.options.stretchLastRow===!0)if(a.length===1)if(o.length>0){const i=o[o.length-1],r=a[0];i&&r&&i.push(r)}else o.push(a);else if(a.length===2)if(o.length>1){const i=o[o.length-1],r=o[o.length-2],n=a[0],h=a[1];if(i&&r&&n&&h){const l=i.shift();l&&r.push(l),i.push(n,h)}}else o.push(a);else o.push([...a]);else o.push(a);return o}calculateRowLayout(t,e,o=!1){const a=(t.length-1)*this.options.gap,s=e-a,i=t.reduce((n,h)=>n+h.width/h.height,0);let r=s/i;if(o===!0&&this.options.stretchLastRow===!0){if(r>this.targetRowHeight){const n=r/this.targetRowHeight;t=t.map(h=>({...h,width:h.width*n,height:h.height})),r=this.targetRowHeight}}else o===!0&&this.options.stretchLastRow===!1&&r>this.targetRowHeight&&(r=this.targetRowHeight);return t.map(n=>({...n,displayWidth:n.width/n.height*r,displayHeight:r}))}renderGallery(){if(!this.gallery)return;this.images.forEach(s=>{let i=s.srcWidth||0,r=s.srcHeight||0;i===0&&(i=this.options.fallbackImageWidth),r===0&&(r=this.options.fallbackImageHeight),i/r>this.options.maxImageRatio?i=this.options.maxImageRatio*r:i/r<this.options.minImageRatio&&(i=this.options.minImageRatio*r),s.width=i,s.height=r});const t=this.getObservedWidth(),e=this.gallery.offsetWidth;t==="xl"?this.targetRowHeight=this.options.rowHeightXl*e:t==="md"?this.targetRowHeight=this.options.rowHeightMd*e:this.targetRowHeight=this.options.rowHeightSm*e,this.lastRenderedScreenSize=t;const o=this.computeRows(this.images,e),a=document.createDocumentFragment();o.forEach((s,i)=>{const r=i===o.length-1,n=this.calculateRowLayout(s,e,r),h=document.createElement("div");h.className="lumosaic-row",n[0]&&n[0].displayHeight&&(h.style.aspectRatio=(e/n[0].displayHeight).toString()),n.forEach(l=>{const c=document.createElement("div");c.className="lumosaic-item";const u=(l.displayWidth||0)/e*100;c.style.flexBasis=`${u}%`,c.style.flexGrow="0",c.style.flexShrink="1",n.indexOf(l)<n.length-1&&(c.style.marginRight=`${this.options.gap}px`);const d=document.createElement("img");if(d.src=l.preview,l.alt&&(d.alt=l.alt),d.loading="lazy",l.src&&(d.dataset.src=l.src),l.title&&(d.title=l.title),l.exif&&(d.dataset.exif=JSON.stringify(l.exif)),this.options.playButtonOnVideoCover){const m=l.src.toLowerCase();if(m.endsWith(".mp4")||m.endsWith(".webm")||m.endsWith(".mov")||m.endsWith(".avi")||m.endsWith(".wmv")){const y=document.createElement("div");y.className="lumosaic-play-icon",y.innerHTML='<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="m10.775 15.475l4.6-3.05q.225-.15.225-.425t-.225-.425l-4.6-3.05q-.25-.175-.513-.038T10 8.926v6.15q0 .3.263.438t.512-.038M12 22q-2.075 0-3.9-.788t-3.175-2.137T2.788 15.9T2 12q0-.8.125-1.6T2.5 8.825q.125-.4.513-.537t.737.062q.375.2.538.588t.037.812q-.15.55-.238 1.113T4 12q0 3.35 2.325 5.675T12 20t5.675-2.325T20 12t-2.325-5.675T12 4q-.6 0-1.187.087T9.65 4.35q-.425.125-.8-.025T8.3 3.8t-.013-.762t.563-.513q.75-.275 1.55-.4T12 2q2.075 0 3.9.788t3.175 2.137T21.213 8.1T22 12t-.788 3.9t-2.137 3.175t-3.175 2.138T12 22M5.5 7q-.625 0-1.062-.437T4 5.5t.438-1.062T5.5 4t1.063.438T7 5.5t-.437 1.063T5.5 7m6.5 5"/></svg>',c.appendChild(y)}}c.appendChild(d),h.appendChild(c)}),i<o.length-1&&(h.style.marginBottom=`${this.options.gap}px`),a.appendChild(h)}),this.gallery.innerHTML="",this.gallery.appendChild(a)}mergeOptions(t){const e={...t};e.rowHeight!==void 0&&(e.rowHeightSm=e.rowHeight,e.rowHeightMd=e.rowHeight,e.rowHeightXl=e.rowHeight,delete e.rowHeight),Object.keys(e).forEach(o=>{const a=o;e[a]===void 0&&delete e[a]}),this.options={...this.options,...e}}}const R=p.defineComponent({__name:"Lumosaic",props:p.mergeDefaults({images:{},rowHeightSm:{},rowHeightMd:{},rowHeightXl:{},shouldRetrieveWidthAndHeight:{type:Boolean},fallbackImageWidth:{},fallbackImageHeight:{},maxImageRatio:{},minImageRatio:{},maxRows:{},stretchLastRow:{type:Boolean},shuffleImages:{type:Boolean},gap:{},playButtonOnVideoCover:{type:Boolean},observeWindowWidth:{type:Boolean},rowHeight:{}},v),emits:["image-click"],setup(g,{expose:t,emit:e}){const o=g,a=e,s=p.ref(null);let i=null;return p.onMounted(()=>{if(!s.value)return;const{images:r,...n}=o;i=new H(s.value,r),s.value.addEventListener("click",h=>{const c=h.target.closest(".lumosaic-item");if(c&&s.value){const u=Array.from(s.value.querySelectorAll(".lumosaic-item")).indexOf(c);u!==-1&&a("image-click",u,o.images)}}),i.init(n)}),p.onBeforeUnmount(()=>{i&&(i.destroy(),i=null)}),p.watch(()=>o.images,r=>{i&&i.replaceImages(r)},{deep:!0}),p.watch(()=>{const{images:r,...n}=o;return n},r=>{i&&i.updateOptions(r)},{deep:!0}),t({shuffle:()=>{i&&i.shuffleImages()}}),(r,n)=>(p.openBlock(),p.createElementBlock("div",{ref_key:"galleryContainer",ref:s,class:"lumosaic-wrapper"},null,512))}}),I={install:g=>{g.component("Lumosaic",R)}};exports.Lumosaic=R;exports.default=I;
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
var v = Object.defineProperty;
|
|
2
|
+
var b = (g, e, t) => e in g ? v(g, e, { enumerable: !0, configurable: !0, writable: !0, value: t }) : g[e] = t;
|
|
3
|
+
var p = (g, e, t) => b(g, typeof e != "symbol" ? e + "" : e, t);
|
|
4
|
+
import { defineComponent as W, ref as H, onMounted as I, onBeforeUnmount as U, watch as y, openBlock as O, createElementBlock as x, mergeDefaults as L } from "vue";
|
|
5
|
+
const R = {
|
|
6
|
+
rowHeightSm: 0.25,
|
|
7
|
+
rowHeightMd: 0.2,
|
|
8
|
+
rowHeightXl: 0.18,
|
|
9
|
+
shouldRetrieveWidthAndHeight: !1,
|
|
10
|
+
fallbackImageWidth: 1e3,
|
|
11
|
+
fallbackImageHeight: 1e3,
|
|
12
|
+
maxImageRatio: 1.6,
|
|
13
|
+
minImageRatio: 0.65,
|
|
14
|
+
maxRows: 0,
|
|
15
|
+
stretchLastRow: !0,
|
|
16
|
+
shuffleImages: !1,
|
|
17
|
+
gap: 4,
|
|
18
|
+
playButtonOnVideoCover: !0,
|
|
19
|
+
observeWindowWidth: !0
|
|
20
|
+
};
|
|
21
|
+
class S {
|
|
22
|
+
constructor(e, t) {
|
|
23
|
+
p(this, "options");
|
|
24
|
+
p(this, "params");
|
|
25
|
+
p(this, "gallery");
|
|
26
|
+
p(this, "images", []);
|
|
27
|
+
p(this, "lastRenderedScreenSize", null);
|
|
28
|
+
p(this, "resizeObserver", null);
|
|
29
|
+
p(this, "targetRowHeight", 0);
|
|
30
|
+
this.options = { ...R }, this.params = { imagesSource: t }, this.gallery = e;
|
|
31
|
+
}
|
|
32
|
+
// --- Public functions ---
|
|
33
|
+
async init(e = {}) {
|
|
34
|
+
return console.log(e), this.mergeOptions(e), this.gallery.classList.add("lumosaic-loading"), await this.processParams(), this.options.shuffleImages ? this.shuffleImages() : this.renderGallery(), this.lastRenderedScreenSize = this.getObservedWidth(), this.gallery.classList.remove("lumosaic-loading"), this.initResizeObserver(), this;
|
|
35
|
+
}
|
|
36
|
+
replaceImages(e) {
|
|
37
|
+
this.params.imagesSource = e, this.processParams().then(() => this.renderGallery());
|
|
38
|
+
}
|
|
39
|
+
shuffleImages() {
|
|
40
|
+
this.images.sort(() => Math.random() - 0.5), this.renderGallery();
|
|
41
|
+
}
|
|
42
|
+
updateOptions(e) {
|
|
43
|
+
this.mergeOptions(e), this.renderGallery();
|
|
44
|
+
}
|
|
45
|
+
destroy() {
|
|
46
|
+
this.resizeObserver && this.resizeObserver.disconnect();
|
|
47
|
+
}
|
|
48
|
+
// --- Private functions ---
|
|
49
|
+
initResizeObserver() {
|
|
50
|
+
this.resizeObserver = new ResizeObserver(() => {
|
|
51
|
+
const e = this.getObservedWidth();
|
|
52
|
+
e && this.lastRenderedScreenSize !== e && (this.renderGallery(), this.lastRenderedScreenSize = e);
|
|
53
|
+
}), this.options.observeWindowWidth ? this.resizeObserver.observe(document.body) : this.gallery && this.resizeObserver.observe(this.gallery);
|
|
54
|
+
}
|
|
55
|
+
getObservedWidth() {
|
|
56
|
+
let e = 0;
|
|
57
|
+
return this.options.observeWindowWidth ? e = window.innerWidth : this.gallery && (e = this.gallery.offsetWidth), e && e >= 1024 ? "xl" : e && e >= 768 && e < 1024 ? "md" : e && e < 768 ? "sm" : !1;
|
|
58
|
+
}
|
|
59
|
+
async processParams() {
|
|
60
|
+
const e = this.params.imagesSource.map((t) => this.normalizeImageData({ ...t }));
|
|
61
|
+
this.images = await Promise.all(e);
|
|
62
|
+
}
|
|
63
|
+
async normalizeImageData(e) {
|
|
64
|
+
e.src && !e.preview && (e.preview = e.src), e.preview && !e.src && (e.src = e.preview);
|
|
65
|
+
const t = e;
|
|
66
|
+
if (t.src || (t.src = ""), t.preview || (t.preview = ""), !t.width || !t.height)
|
|
67
|
+
if (this.options.shouldRetrieveWidthAndHeight && t.src)
|
|
68
|
+
try {
|
|
69
|
+
const o = await this.getImageSizeFromUrl(t.src);
|
|
70
|
+
t.width = o.width, t.height = o.height;
|
|
71
|
+
} catch (o) {
|
|
72
|
+
console.warn(`Lumosaic: Could not fetch size for ${t.src}`, o), t.width = 0, t.height = 0;
|
|
73
|
+
}
|
|
74
|
+
else
|
|
75
|
+
t.width = 0, t.height = 0;
|
|
76
|
+
return t.srcWidth = t.width, t.srcHeight = t.height, t;
|
|
77
|
+
}
|
|
78
|
+
async getImageSizeFromUrl(e) {
|
|
79
|
+
const t = {
|
|
80
|
+
PNG: 2303741511,
|
|
81
|
+
PNG_END: 218765834,
|
|
82
|
+
JPEG_START: 65496,
|
|
83
|
+
WEBP_RIFF: 1380533830,
|
|
84
|
+
WEBP_WEBP: 1464156752,
|
|
85
|
+
VP8: 1448097824,
|
|
86
|
+
VP8L: 1448097868,
|
|
87
|
+
VP8X: 1448097880
|
|
88
|
+
}, o = await fetch(e, { headers: { Range: "bytes=0-65535" } });
|
|
89
|
+
if (!o.ok) throw new Error(`Failed to fetch: ${o.status}`);
|
|
90
|
+
const a = await o.arrayBuffer(), s = new DataView(a);
|
|
91
|
+
if (s.getUint32(0) === t.PNG && s.getUint32(4) === t.PNG_END)
|
|
92
|
+
return {
|
|
93
|
+
width: s.getUint32(16),
|
|
94
|
+
height: s.getUint32(20),
|
|
95
|
+
type: "png"
|
|
96
|
+
};
|
|
97
|
+
if (s.getUint16(0) === t.JPEG_START) {
|
|
98
|
+
let i = 2;
|
|
99
|
+
for (; i < s.byteLength && s.getUint8(i) === 255; ) {
|
|
100
|
+
const r = s.getUint8(i + 1), n = s.getUint16(i + 2);
|
|
101
|
+
if (r >= 192 && r <= 207 && ![196, 200, 204].includes(r))
|
|
102
|
+
return {
|
|
103
|
+
height: s.getUint16(i + 5),
|
|
104
|
+
width: s.getUint16(i + 7),
|
|
105
|
+
type: "jpeg"
|
|
106
|
+
};
|
|
107
|
+
i += 2 + n;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (s.getUint32(0, !1) === t.WEBP_RIFF && s.getUint32(8, !1) === t.WEBP_WEBP) {
|
|
111
|
+
let i = 12;
|
|
112
|
+
for (; i < s.byteLength; ) {
|
|
113
|
+
const r = s.getUint32(i, !1), n = s.getUint32(i + 4, !0);
|
|
114
|
+
if (r === t.VP8) {
|
|
115
|
+
const h = i + 10;
|
|
116
|
+
return {
|
|
117
|
+
width: s.getUint16(h + 6, !0) & 16383,
|
|
118
|
+
height: s.getUint16(h + 8, !0) & 16383,
|
|
119
|
+
type: "webp"
|
|
120
|
+
};
|
|
121
|
+
} else if (r === t.VP8L) {
|
|
122
|
+
const h = s.getUint8(i + 8), l = s.getUint8(i + 9), c = s.getUint8(i + 10), u = s.getUint8(i + 11), f = 1 + ((l & 63) << 8 | h), d = 1 + ((u & 15) << 10 | c << 2 | (l & 192) >> 6);
|
|
123
|
+
return { width: f, height: d, type: "webp" };
|
|
124
|
+
} else if (r === t.VP8X) {
|
|
125
|
+
const h = 1 + this.getUint24(s, i + 12, !0), l = 1 + this.getUint24(s, i + 15, !0);
|
|
126
|
+
return { width: h, height: l, type: "webp" };
|
|
127
|
+
}
|
|
128
|
+
i += 8 + n + n % 2;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return { width: 0, height: 0, type: "unknown" };
|
|
132
|
+
}
|
|
133
|
+
getUint24(e, t, o) {
|
|
134
|
+
return o ? e.getUint8(t) | e.getUint8(t + 1) << 8 | e.getUint8(t + 2) << 16 : e.getUint8(t) << 16 | e.getUint8(t + 1) << 8 | e.getUint8(t + 2);
|
|
135
|
+
}
|
|
136
|
+
computeRows(e, t) {
|
|
137
|
+
const o = [];
|
|
138
|
+
let a = [], s = 0;
|
|
139
|
+
for (const i of e) {
|
|
140
|
+
const n = (i.height > 0 ? i.width / i.height : 1) * this.targetRowHeight, h = s + n + a.length * this.options.gap;
|
|
141
|
+
if (a.length === 0 || h < t)
|
|
142
|
+
a.push(i), s += n;
|
|
143
|
+
else if (h > t && h - t < t - s)
|
|
144
|
+
a.push(i), s += n;
|
|
145
|
+
else {
|
|
146
|
+
if (a.length > 0 && (o.push([...a]), this.options.maxRows && o.length >= this.options.maxRows)) {
|
|
147
|
+
a = [];
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
a = [i], s = n;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (a.length > 0)
|
|
154
|
+
if (this.options.stretchLastRow === !0)
|
|
155
|
+
if (a.length === 1)
|
|
156
|
+
if (o.length > 0) {
|
|
157
|
+
const i = o[o.length - 1], r = a[0];
|
|
158
|
+
i && r && i.push(r);
|
|
159
|
+
} else
|
|
160
|
+
o.push(a);
|
|
161
|
+
else if (a.length === 2)
|
|
162
|
+
if (o.length > 1) {
|
|
163
|
+
const i = o[o.length - 1], r = o[o.length - 2], n = a[0], h = a[1];
|
|
164
|
+
if (i && r && n && h) {
|
|
165
|
+
const l = i.shift();
|
|
166
|
+
l && r.push(l), i.push(n, h);
|
|
167
|
+
}
|
|
168
|
+
} else
|
|
169
|
+
o.push(a);
|
|
170
|
+
else
|
|
171
|
+
o.push([...a]);
|
|
172
|
+
else
|
|
173
|
+
o.push(a);
|
|
174
|
+
return o;
|
|
175
|
+
}
|
|
176
|
+
calculateRowLayout(e, t, o = !1) {
|
|
177
|
+
const a = (e.length - 1) * this.options.gap, s = t - a, i = e.reduce((n, h) => n + h.width / h.height, 0);
|
|
178
|
+
let r = s / i;
|
|
179
|
+
if (o === !0 && this.options.stretchLastRow === !0) {
|
|
180
|
+
if (r > this.targetRowHeight) {
|
|
181
|
+
const n = r / this.targetRowHeight;
|
|
182
|
+
e = e.map((h) => ({
|
|
183
|
+
...h,
|
|
184
|
+
width: h.width * n,
|
|
185
|
+
height: h.height
|
|
186
|
+
})), r = this.targetRowHeight;
|
|
187
|
+
}
|
|
188
|
+
} else o === !0 && this.options.stretchLastRow === !1 && r > this.targetRowHeight && (r = this.targetRowHeight);
|
|
189
|
+
return e.map((n) => ({
|
|
190
|
+
...n,
|
|
191
|
+
displayWidth: n.width / n.height * r,
|
|
192
|
+
displayHeight: r
|
|
193
|
+
}));
|
|
194
|
+
}
|
|
195
|
+
renderGallery() {
|
|
196
|
+
if (!this.gallery) return;
|
|
197
|
+
this.images.forEach((s) => {
|
|
198
|
+
let i = s.srcWidth || 0, r = s.srcHeight || 0;
|
|
199
|
+
i === 0 && (i = this.options.fallbackImageWidth), r === 0 && (r = this.options.fallbackImageHeight), i / r > this.options.maxImageRatio ? i = this.options.maxImageRatio * r : i / r < this.options.minImageRatio && (i = this.options.minImageRatio * r), s.width = i, s.height = r;
|
|
200
|
+
});
|
|
201
|
+
const e = this.getObservedWidth(), t = this.gallery.offsetWidth;
|
|
202
|
+
e === "xl" ? this.targetRowHeight = this.options.rowHeightXl * t : e === "md" ? this.targetRowHeight = this.options.rowHeightMd * t : this.targetRowHeight = this.options.rowHeightSm * t, this.lastRenderedScreenSize = e;
|
|
203
|
+
const o = this.computeRows(this.images, t), a = document.createDocumentFragment();
|
|
204
|
+
o.forEach((s, i) => {
|
|
205
|
+
const r = i === o.length - 1, n = this.calculateRowLayout(s, t, r), h = document.createElement("div");
|
|
206
|
+
h.className = "lumosaic-row", n[0] && n[0].displayHeight && (h.style.aspectRatio = (t / n[0].displayHeight).toString()), n.forEach((l) => {
|
|
207
|
+
const c = document.createElement("div");
|
|
208
|
+
c.className = "lumosaic-item";
|
|
209
|
+
const f = (l.displayWidth || 0) / t * 100;
|
|
210
|
+
c.style.flexBasis = `${f}%`, c.style.flexGrow = "0", c.style.flexShrink = "1", n.indexOf(l) < n.length - 1 && (c.style.marginRight = `${this.options.gap}px`);
|
|
211
|
+
const d = document.createElement("img");
|
|
212
|
+
if (d.src = l.preview, l.alt && (d.alt = l.alt), d.loading = "lazy", l.src && (d.dataset.src = l.src), l.title && (d.title = l.title), l.exif && (d.dataset.exif = JSON.stringify(l.exif)), this.options.playButtonOnVideoCover) {
|
|
213
|
+
const m = l.src.toLowerCase();
|
|
214
|
+
if (m.endsWith(".mp4") || m.endsWith(".webm") || m.endsWith(".mov") || m.endsWith(".avi") || m.endsWith(".wmv")) {
|
|
215
|
+
const w = document.createElement("div");
|
|
216
|
+
w.className = "lumosaic-play-icon", w.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="m10.775 15.475l4.6-3.05q.225-.15.225-.425t-.225-.425l-4.6-3.05q-.25-.175-.513-.038T10 8.926v6.15q0 .3.263.438t.512-.038M12 22q-2.075 0-3.9-.788t-3.175-2.137T2.788 15.9T2 12q0-.8.125-1.6T2.5 8.825q.125-.4.513-.537t.737.062q.375.2.538.588t.037.812q-.15.55-.238 1.113T4 12q0 3.35 2.325 5.675T12 20t5.675-2.325T20 12t-2.325-5.675T12 4q-.6 0-1.187.087T9.65 4.35q-.425.125-.8-.025T8.3 3.8t-.013-.762t.563-.513q.75-.275 1.55-.4T12 2q2.075 0 3.9.788t3.175 2.137T21.213 8.1T22 12t-.788 3.9t-2.137 3.175t-3.175 2.138T12 22M5.5 7q-.625 0-1.062-.437T4 5.5t.438-1.062T5.5 4t1.063.438T7 5.5t-.437 1.063T5.5 7m6.5 5"/></svg>', c.appendChild(w);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
c.appendChild(d), h.appendChild(c);
|
|
220
|
+
}), i < o.length - 1 && (h.style.marginBottom = `${this.options.gap}px`), a.appendChild(h);
|
|
221
|
+
}), this.gallery.innerHTML = "", this.gallery.appendChild(a);
|
|
222
|
+
}
|
|
223
|
+
mergeOptions(e) {
|
|
224
|
+
const t = { ...e };
|
|
225
|
+
t.rowHeight !== void 0 && (t.rowHeightSm = t.rowHeight, t.rowHeightMd = t.rowHeight, t.rowHeightXl = t.rowHeight, delete t.rowHeight), Object.keys(t).forEach((o) => {
|
|
226
|
+
const a = o;
|
|
227
|
+
t[a] === void 0 && delete t[a];
|
|
228
|
+
}), this.options = { ...this.options, ...t };
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
const T = /* @__PURE__ */ W({
|
|
232
|
+
__name: "Lumosaic",
|
|
233
|
+
props: /* @__PURE__ */ L({
|
|
234
|
+
images: {},
|
|
235
|
+
rowHeightSm: {},
|
|
236
|
+
rowHeightMd: {},
|
|
237
|
+
rowHeightXl: {},
|
|
238
|
+
shouldRetrieveWidthAndHeight: { type: Boolean },
|
|
239
|
+
fallbackImageWidth: {},
|
|
240
|
+
fallbackImageHeight: {},
|
|
241
|
+
maxImageRatio: {},
|
|
242
|
+
minImageRatio: {},
|
|
243
|
+
maxRows: {},
|
|
244
|
+
stretchLastRow: { type: Boolean },
|
|
245
|
+
shuffleImages: { type: Boolean },
|
|
246
|
+
gap: {},
|
|
247
|
+
playButtonOnVideoCover: { type: Boolean },
|
|
248
|
+
observeWindowWidth: { type: Boolean },
|
|
249
|
+
rowHeight: {}
|
|
250
|
+
}, R),
|
|
251
|
+
emits: ["image-click"],
|
|
252
|
+
setup(g, { expose: e, emit: t }) {
|
|
253
|
+
const o = g, a = t, s = H(null);
|
|
254
|
+
let i = null;
|
|
255
|
+
return I(() => {
|
|
256
|
+
if (!s.value) return;
|
|
257
|
+
const { images: r, ...n } = o;
|
|
258
|
+
i = new S(s.value, r), s.value.addEventListener("click", (h) => {
|
|
259
|
+
const c = h.target.closest(".lumosaic-item");
|
|
260
|
+
if (c && s.value) {
|
|
261
|
+
const f = Array.from(s.value.querySelectorAll(".lumosaic-item")).indexOf(c);
|
|
262
|
+
f !== -1 && a("image-click", f, o.images);
|
|
263
|
+
}
|
|
264
|
+
}), i.init(n);
|
|
265
|
+
}), U(() => {
|
|
266
|
+
i && (i.destroy(), i = null);
|
|
267
|
+
}), y(
|
|
268
|
+
() => o.images,
|
|
269
|
+
(r) => {
|
|
270
|
+
i && i.replaceImages(r);
|
|
271
|
+
},
|
|
272
|
+
{ deep: !0 }
|
|
273
|
+
), y(
|
|
274
|
+
() => {
|
|
275
|
+
const { images: r, ...n } = o;
|
|
276
|
+
return n;
|
|
277
|
+
},
|
|
278
|
+
(r) => {
|
|
279
|
+
i && i.updateOptions(r);
|
|
280
|
+
},
|
|
281
|
+
{ deep: !0 }
|
|
282
|
+
), e({
|
|
283
|
+
shuffle: () => {
|
|
284
|
+
i && i.shuffleImages();
|
|
285
|
+
}
|
|
286
|
+
}), (r, n) => (O(), x("div", {
|
|
287
|
+
ref_key: "galleryContainer",
|
|
288
|
+
ref: s,
|
|
289
|
+
class: "lumosaic-wrapper"
|
|
290
|
+
}, null, 512));
|
|
291
|
+
}
|
|
292
|
+
}), P = {
|
|
293
|
+
install: (g) => {
|
|
294
|
+
g.component("Lumosaic", T);
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
export {
|
|
298
|
+
T as Lumosaic,
|
|
299
|
+
P as default
|
|
300
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";const s=require("@nuxt/kit"),e=s.defineNuxtModule({meta:{name:"lumosaic-nuxt",configKey:"lumosaic"},setup(t,o){s.addComponent({name:"Lumosaic",export:"Lumosaic",filePath:"lumosaic"}),o.options.css.push("lumosaic/style.css")}});module.exports=e;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { defineNuxtModule as s, addComponent as e } from "@nuxt/kit";
|
|
2
|
+
const m = s({
|
|
3
|
+
meta: {
|
|
4
|
+
name: "lumosaic-nuxt",
|
|
5
|
+
configKey: "lumosaic"
|
|
6
|
+
},
|
|
7
|
+
setup(t, o) {
|
|
8
|
+
e({
|
|
9
|
+
name: "Lumosaic",
|
|
10
|
+
export: "Lumosaic",
|
|
11
|
+
filePath: "lumosaic"
|
|
12
|
+
}), o.options.css.push("lumosaic/style.css");
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
export {
|
|
16
|
+
m as default
|
|
17
|
+
};
|
package/dist/style.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.lumosaic-wrapper{width:100%}.lumosaic-row{display:flex;width:100%}.lumosaic-item{flex-grow:1;overflow:hidden;position:relative;background-color:#00000017}.lumosaic-item:last-child{margin-right:0}.lumosaic-item img{width:100%;height:100%;object-fit:cover;display:block;transition:transform .3s ease}@media (hover: hover){.lumosaic-item:hover img{transform:scale(1.1)}}.lumosaic-play-icon{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);z-index:2;pointer-events:none}.lumosaic-play-icon svg{width:10vw;height:auto}@media (min-width: 768px){.lumosaic-play-icon svg{width:7vw}}@media (min-width: 1024px){.lumosaic-play-icon svg{width:5vw}}.lumosaic-loading:before{position:absolute;content:"";z-index:1;opacity:.3;width:3em;height:3em;top:calc(50% - 1.5em);left:calc(50% - 1.5em);border:.3em dotted #8d9bbb;border-radius:50%;display:inline-block;box-sizing:border-box;animation:lumosaic-rotation 2s linear infinite}@keyframes lumosaic-rotation{0%{transform:rotate(0)}to{transform:rotate(360deg)}}
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "lumosaic",
|
|
3
|
+
"version": "1.1.4",
|
|
4
|
+
"description": "Adaptive photo gallery with intelligent row layout for Vue 3 and Nuxt 4",
|
|
5
|
+
"author": "Sergey Volkar <volkar.sergey@gmail.com>",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/volkar/lumosaic.git"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"vue",
|
|
13
|
+
"vue3",
|
|
14
|
+
"nuxt",
|
|
15
|
+
"nuxt4",
|
|
16
|
+
"gallery",
|
|
17
|
+
"mosaic",
|
|
18
|
+
"masonry",
|
|
19
|
+
"images",
|
|
20
|
+
"grid"
|
|
21
|
+
],
|
|
22
|
+
"type": "module",
|
|
23
|
+
"main": "./dist/lumosaic.umd.js",
|
|
24
|
+
"module": "./dist/lumosaic.es.js",
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"files": [
|
|
27
|
+
"dist"
|
|
28
|
+
],
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"import": "./dist/lumosaic.es.js",
|
|
32
|
+
"require": "./dist/lumosaic.umd.js",
|
|
33
|
+
"types": "./dist/index.d.ts"
|
|
34
|
+
},
|
|
35
|
+
"./nuxt": {
|
|
36
|
+
"import": "./dist/nuxt-module.es.js",
|
|
37
|
+
"require": "./dist/nuxt-module.umd.js"
|
|
38
|
+
},
|
|
39
|
+
"./style.css": "./dist/style.css"
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"vue": "^3.0.0"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"dev": "vite",
|
|
46
|
+
"build": "vue-tsc --noEmit && vite build",
|
|
47
|
+
"preview": "vite preview"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@nuxt/kit": "^4.4.8",
|
|
51
|
+
"@nuxt/schema": "^4.4.8",
|
|
52
|
+
"@types/node": "^25.9.3",
|
|
53
|
+
"@vitejs/plugin-vue": "^5.0.4",
|
|
54
|
+
"typescript": "^5.2.2",
|
|
55
|
+
"vite": "^5.2.0",
|
|
56
|
+
"vite-plugin-dts": "^3.8.1",
|
|
57
|
+
"vue": "^3.4.21",
|
|
58
|
+
"vue-tsc": "^2.0.0"
|
|
59
|
+
}
|
|
60
|
+
}
|