@place-framework/place-block-image 1.0.0 → 1.0.2
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/dist/constants/index.d.ts +2 -2
- package/dist/constants/index.d.ts.map +1 -1
- package/dist/constants/index.js +4 -5
- package/dist/constants/index.js.map +1 -1
- package/dist/generate-components.d.ts +16 -0
- package/dist/generate-components.d.ts.map +1 -0
- package/dist/generate-components.js +93 -0
- package/dist/generate-components.js.map +1 -0
- package/dist/react/PlaceBlockImage.cjs +1 -0
- package/dist/react/PlaceBlockImage.js +67 -0
- package/dist/templates/shared/index.d.ts.map +1 -1
- package/dist/templates/shared/index.js +8 -16
- package/dist/templates/shared/index.js.map +1 -1
- package/dist/templates/shared/vue.d.ts +6 -0
- package/dist/templates/shared/vue.d.ts.map +1 -0
- package/dist/templates/shared/vue.js +26 -0
- package/dist/templates/shared/vue.js.map +1 -0
- package/dist/templates/vue.d.ts.map +1 -1
- package/dist/templates/vue.js +14 -19
- package/dist/templates/vue.js.map +1 -1
- package/dist/utils/index.d.ts +61 -7
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +112 -27
- package/dist/utils/index.js.map +1 -1
- package/dist/vue/PlaceBlockImage.cjs +1 -0
- package/dist/vue/PlaceBlockImage.js +83 -0
- package/dist/webpack-plugin.d.ts +21 -8
- package/dist/webpack-plugin.d.ts.map +1 -1
- package/dist/webpack-plugin.js +24 -36
- package/dist/webpack-plugin.js.map +1 -1
- package/package.json +36 -10
- package/src/constants/index.ts +5 -6
- package/src/react/PlaceBlockImage.tsx +63 -0
- package/src/react/index.ts +1 -0
- package/src/utils/index.ts +125 -30
- package/src/vue/PlaceBlockImage.vue +98 -0
- package/src/vue/index.ts +1 -0
- package/src/webpack-plugin.ts +47 -49
- package/tsconfig.json +8 -3
- package/vite.config.mts +27 -0
- package/vite.react.config.mts +25 -0
- package/vite.vue.config.mts +26 -0
- package/src/templates/react-jsx.ts +0 -27
- package/src/templates/react-tsx.ts +0 -33
- package/src/templates/shared/index.ts +0 -47
- package/src/templates/shared/react.ts +0 -51
- package/src/templates/vue.ts +0 -98
- package/src/templates.ts +0 -29
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<picture :class="[wrapperClassName, attrs.class]">
|
|
3
|
+
<img
|
|
4
|
+
ref="imgRef"
|
|
5
|
+
:src="imageSrc"
|
|
6
|
+
:alt="alt"
|
|
7
|
+
:class="imgClassName"
|
|
8
|
+
v-bind="imgAttrs"
|
|
9
|
+
/>
|
|
10
|
+
</picture>
|
|
11
|
+
</template>
|
|
12
|
+
|
|
13
|
+
<script setup lang="ts">
|
|
14
|
+
import { computed, ref, onMounted, onUnmounted, watch, useAttrs } from 'vue';
|
|
15
|
+
import { getWrapperClass, getLazyClass, createLazyObserver } from '../utils/index';
|
|
16
|
+
import { IMAGE_PREFIX } from '../constants/index';
|
|
17
|
+
|
|
18
|
+
declare const __PLUGIN_ASSET_PATH__: string;
|
|
19
|
+
|
|
20
|
+
defineOptions({ inheritAttrs: false });
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* PlaceBlockImage — prevents layout shift using CSS custom properties.
|
|
24
|
+
* Import: import PlaceBlockImage from '@place-framework/place-block-image/vue'
|
|
25
|
+
*
|
|
26
|
+
* Usage:
|
|
27
|
+
* <PlaceBlockImage src="/assets/logo.png" alt="Logo" />
|
|
28
|
+
* <PlaceBlockImage src="/assets/hero.jpg" alt="Hero" :lazy="true" />
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
const attrs = useAttrs();
|
|
32
|
+
|
|
33
|
+
const props = defineProps({
|
|
34
|
+
src: { type: String, required: true },
|
|
35
|
+
alt: { type: String, required: true },
|
|
36
|
+
imagePrefix: { type: String, default: () => IMAGE_PREFIX },
|
|
37
|
+
lazy: { type: Boolean, default: false }
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// All attrs except class go to the <img>
|
|
41
|
+
const imgAttrs = computed(() => {
|
|
42
|
+
const { class: _, ...rest } = attrs;
|
|
43
|
+
return rest;
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const imgRef = ref(null);
|
|
47
|
+
const imageSrc = ref(props.lazy ? '' : props.src);
|
|
48
|
+
const isLoaded = ref(!props.lazy);
|
|
49
|
+
let observer: IntersectionObserver | null = null;
|
|
50
|
+
|
|
51
|
+
const assetPath: string = typeof __PLUGIN_ASSET_PATH__ !== 'undefined' ? __PLUGIN_ASSET_PATH__ : '';
|
|
52
|
+
|
|
53
|
+
const wrapperClassName = computed(() =>
|
|
54
|
+
getWrapperClass(props.src, props.imagePrefix, assetPath)
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const imgClassName = computed(() => getLazyClass(props.lazy, isLoaded.value));
|
|
58
|
+
|
|
59
|
+
const setupLazyLoading = () => {
|
|
60
|
+
if (!props.lazy || isLoaded.value || !imgRef.value) return;
|
|
61
|
+
|
|
62
|
+
observer = createLazyObserver(imgRef.value, () => {
|
|
63
|
+
imageSrc.value = props.src;
|
|
64
|
+
isLoaded.value = true;
|
|
65
|
+
});
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const cleanupObserver = () => {
|
|
69
|
+
if (observer) {
|
|
70
|
+
observer.disconnect();
|
|
71
|
+
observer = null;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
onMounted(() => {
|
|
76
|
+
setupLazyLoading();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
onUnmounted(() => {
|
|
80
|
+
cleanupObserver();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
watch(() => props.src, (newSrc) => {
|
|
84
|
+
if (!props.lazy) {
|
|
85
|
+
imageSrc.value = newSrc;
|
|
86
|
+
} else if (!isLoaded.value) {
|
|
87
|
+
cleanupObserver();
|
|
88
|
+
setupLazyLoading();
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
watch(isLoaded, (loaded) => {
|
|
93
|
+
if (loaded) {
|
|
94
|
+
imageSrc.value = props.src;
|
|
95
|
+
cleanupObserver();
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
</script>
|
package/src/vue/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as PlaceBlockImage } from './PlaceBlockImage.vue';
|
package/src/webpack-plugin.ts
CHANGED
|
@@ -2,17 +2,33 @@ import * as fs from 'fs';
|
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import { glob } from 'glob';
|
|
4
4
|
import sizeOf from 'image-size';
|
|
5
|
-
import { Compiler } from 'webpack';
|
|
6
|
-
import {
|
|
5
|
+
import { Compiler, DefinePlugin } from 'webpack';
|
|
6
|
+
import { generateImageClassName } from './utils';
|
|
7
7
|
|
|
8
8
|
export interface PlaceBlockImagePluginOptions {
|
|
9
9
|
imagePrefix?: string;
|
|
10
10
|
imageDir: string;
|
|
11
11
|
scssPath: string;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
/**
|
|
13
|
+
* The URL path at which images are served (e.g. '/images').
|
|
14
|
+
* Used by the webpack plugin to inject a __PLACE_ASSET_PATH__ constant
|
|
15
|
+
* that the React/Vue components use to strip the prefix from src before
|
|
16
|
+
* generating the CSS class name — ensuring it matches what the plugin
|
|
17
|
+
* generates from the file path relative to imageDir.
|
|
18
|
+
*
|
|
19
|
+
* Defaults to the basename of imageDir prefixed with '/' (e.g. '/images').
|
|
20
|
+
*/
|
|
21
|
+
assetPath?: string;
|
|
22
|
+
/**
|
|
23
|
+
* Output path prefix prepended to the relative image path when generating
|
|
24
|
+
* CSS class names — must match the prefix used in assetModuleFilename so
|
|
25
|
+
* the class name the plugin writes into the SCSS matches what the component
|
|
26
|
+
* derives from the served URL at runtime.
|
|
27
|
+
*
|
|
28
|
+
* e.g. if assetModuleFilename outputs "images/story/foo.png" (served as
|
|
29
|
+
* "/images/story/foo.png"), set outputPathPrefix: 'images'
|
|
30
|
+
*/
|
|
31
|
+
outputPathPrefix?: string;
|
|
16
32
|
}
|
|
17
33
|
|
|
18
34
|
export interface ImageDimensions {
|
|
@@ -30,25 +46,22 @@ export class PlaceBlockImagePlugin {
|
|
|
30
46
|
constructor(options: PlaceBlockImagePluginOptions) {
|
|
31
47
|
this.options = {
|
|
32
48
|
imagePrefix: 'image-',
|
|
33
|
-
|
|
34
|
-
|
|
49
|
+
outputPathPrefix: '',
|
|
50
|
+
assetPath: `/${path.basename(options.imageDir)}`,
|
|
35
51
|
...options
|
|
36
52
|
};
|
|
37
53
|
}
|
|
38
54
|
|
|
39
55
|
/**
|
|
40
|
-
* Generate CSS class name from filename
|
|
56
|
+
* Generate CSS class name from filename - uses shared logic
|
|
41
57
|
*/
|
|
42
58
|
private generateClassName(filename: string): string {
|
|
43
|
-
|
|
44
|
-
//
|
|
45
|
-
const
|
|
46
|
-
.
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
.replace(/^-|-$/g, '');
|
|
50
|
-
|
|
51
|
-
return `${this.options.imagePrefix}${cleanName}`;
|
|
59
|
+
// When outputPathPrefix is set the asset output is flat (just basename),
|
|
60
|
+
// so use only the basename to mirror the served URL: /assets/accent-orchid.png
|
|
61
|
+
const name = this.options.outputPathPrefix
|
|
62
|
+
? path.basename(filename)
|
|
63
|
+
: filename;
|
|
64
|
+
return generateImageClassName(name, this.options.imagePrefix, this.options.outputPathPrefix);
|
|
52
65
|
}
|
|
53
66
|
|
|
54
67
|
/**
|
|
@@ -137,35 +150,6 @@ export class PlaceBlockImagePlugin {
|
|
|
137
150
|
fs.writeFileSync(this.options.scssPath, scssContent);
|
|
138
151
|
}
|
|
139
152
|
|
|
140
|
-
/**
|
|
141
|
-
* Generate and write component file
|
|
142
|
-
*/
|
|
143
|
-
private async writeComponentFile(): Promise<void> {
|
|
144
|
-
if (!this.options.generateComponent || !this.options.componentPath || !this.options.componentType) {
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const componentFileName = `PlaceBlockImage.${this.options.componentType}`;
|
|
149
|
-
const componentFilePath = path.resolve(this.options.componentPath, componentFileName);
|
|
150
|
-
|
|
151
|
-
// Check if component already exists
|
|
152
|
-
if (fs.existsSync(componentFilePath)) {
|
|
153
|
-
console.log(`📦 Component already exists, skipping: ${componentFilePath}`);
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
const componentContent = getTemplate(this.options.componentType, this.options.imagePrefix);
|
|
158
|
-
const outputDir = path.dirname(componentFilePath);
|
|
159
|
-
|
|
160
|
-
// Ensure output directory exists
|
|
161
|
-
if (!fs.existsSync(outputDir)) {
|
|
162
|
-
fs.mkdirSync(outputDir, { recursive: true });
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
fs.writeFileSync(componentFilePath, componentContent);
|
|
166
|
-
console.log(`📦 Generated component: ${componentFilePath}`);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
153
|
/**
|
|
170
154
|
* Check if images have changed since last generation
|
|
171
155
|
*/
|
|
@@ -195,6 +179,21 @@ export class PlaceBlockImagePlugin {
|
|
|
195
179
|
}
|
|
196
180
|
|
|
197
181
|
apply(compiler: Compiler): void {
|
|
182
|
+
// Inject @place-block-image alias so the Vue/React source components can
|
|
183
|
+
// resolve their shared utils/constants imports without requiring the
|
|
184
|
+
// consuming app to configure anything manually.
|
|
185
|
+
const pkgSrc = path.resolve(__dirname, '../src');
|
|
186
|
+
compiler.options.resolve.alias = {
|
|
187
|
+
...((compiler.options.resolve.alias as Record<string, string>) || {}),
|
|
188
|
+
'@place-block-image': pkgSrc,
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
// Inject __PLUGIN_ASSET_PATH__ so the React/Vue components can strip the
|
|
192
|
+
// served URL prefix from src before generating the CSS class name.
|
|
193
|
+
new DefinePlugin({
|
|
194
|
+
__PLUGIN_ASSET_PATH__: JSON.stringify(this.options.assetPath),
|
|
195
|
+
}).apply(compiler);
|
|
196
|
+
|
|
198
197
|
// Use environment hook to run only once at startup
|
|
199
198
|
compiler.hooks.environment.tap('PlaceBlockImagePlugin', () => {
|
|
200
199
|
this.generateImageStyles();
|
|
@@ -247,8 +246,7 @@ export class PlaceBlockImagePlugin {
|
|
|
247
246
|
}
|
|
248
247
|
|
|
249
248
|
await this.writeScssFile(images);
|
|
250
|
-
|
|
251
|
-
|
|
249
|
+
|
|
252
250
|
console.log(`✅ Generated CSS custom properties for ${images.length} images`);
|
|
253
251
|
console.log(`📝 Output: ${this.options.scssPath}`);
|
|
254
252
|
|
package/tsconfig.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"compilerOptions": {
|
|
3
3
|
"target": "ES2020",
|
|
4
4
|
"module": "commonjs",
|
|
5
|
-
"lib": ["ES2020"],
|
|
5
|
+
"lib": ["ES2020", "DOM"],
|
|
6
6
|
"outDir": "./dist",
|
|
7
7
|
"rootDir": "./src",
|
|
8
8
|
"strict": true,
|
|
@@ -13,8 +13,13 @@
|
|
|
13
13
|
"declarationMap": true,
|
|
14
14
|
"sourceMap": true,
|
|
15
15
|
"moduleResolution": "node",
|
|
16
|
-
"resolveJsonModule": true
|
|
16
|
+
"resolveJsonModule": true,
|
|
17
|
+
"jsx": "react",
|
|
18
|
+
"baseUrl": ".",
|
|
19
|
+
"paths": {
|
|
20
|
+
"@place-block-image/*": ["./src/*"]
|
|
21
|
+
}
|
|
17
22
|
},
|
|
18
23
|
"include": ["src/**/*"],
|
|
19
|
-
"exclude": ["node_modules", "dist"]
|
|
24
|
+
"exclude": ["node_modules", "dist", "src/vue"]
|
|
20
25
|
}
|
package/vite.config.mts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { defineConfig, build, Plugin } from 'vite';
|
|
2
|
+
import { vueConfig } from './vite.vue.config.mts';
|
|
3
|
+
import { reactConfig } from './vite.react.config.mts';
|
|
4
|
+
|
|
5
|
+
// Runs the React build once after the Vue build completes.
|
|
6
|
+
// The instance-level flag prevents closeBundle from re-firing
|
|
7
|
+
// when the spawned React build itself reaches closeBundle.
|
|
8
|
+
function buildReact(): Plugin {
|
|
9
|
+
let done = false;
|
|
10
|
+
return {
|
|
11
|
+
name: 'build-react',
|
|
12
|
+
apply: 'build',
|
|
13
|
+
async closeBundle() {
|
|
14
|
+
if (done) return;
|
|
15
|
+
done = true;
|
|
16
|
+
await build({ configFile: false, ...reactConfig });
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Builds:
|
|
22
|
+
// src/vue/PlaceBlockImage.vue → dist/vue/PlaceBlockImage.{js,cjs} (via vite.vue.config.mts)
|
|
23
|
+
// src/react/PlaceBlockImage.tsx → dist/react/PlaceBlockImage.{js,cjs} (via vite.react.config.mts)
|
|
24
|
+
export default defineConfig({
|
|
25
|
+
...vueConfig,
|
|
26
|
+
plugins: [...(vueConfig.plugins as Plugin[]), buildReact()],
|
|
27
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { resolve } from 'path';
|
|
2
|
+
import type { UserConfig } from 'vite';
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
// Builds src/react/PlaceBlockImage.tsx → dist/react/PlaceBlockImage.{js,cjs}
|
|
6
|
+
// React and its runtime are kept external — consumers supply their own.
|
|
7
|
+
export const reactConfig: UserConfig = {
|
|
8
|
+
build: {
|
|
9
|
+
outDir: 'dist/react',
|
|
10
|
+
emptyOutDir: true,
|
|
11
|
+
lib: {
|
|
12
|
+
entry: resolve(__dirname, 'src/react/PlaceBlockImage.tsx'),
|
|
13
|
+
name: 'PlaceBlockImage',
|
|
14
|
+
fileName: (format: string, name: string) => (format === 'es' ? `${name}.js` : `${name}.cjs`),
|
|
15
|
+
formats: ['es', 'cjs'],
|
|
16
|
+
},
|
|
17
|
+
rollupOptions: {
|
|
18
|
+
external: ['react', 'react/jsx-runtime'],
|
|
19
|
+
output: {
|
|
20
|
+
globals: { react: 'React' },
|
|
21
|
+
exports: 'named',
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import vue from '@vitejs/plugin-vue';
|
|
2
|
+
import { resolve } from 'path';
|
|
3
|
+
import type { UserConfig } from 'vite';
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
// Builds src/vue/PlaceBlockImage.vue → dist/vue/PlaceBlockImage.{js,cjs}
|
|
7
|
+
// Vue and its runtime are kept external — consumers supply their own.
|
|
8
|
+
export const vueConfig: UserConfig = {
|
|
9
|
+
plugins: [vue()],
|
|
10
|
+
build: {
|
|
11
|
+
outDir: 'dist/vue',
|
|
12
|
+
emptyOutDir: true,
|
|
13
|
+
lib: {
|
|
14
|
+
entry: resolve(__dirname, 'src/vue/PlaceBlockImage.vue'),
|
|
15
|
+
name: 'PlaceBlockImage',
|
|
16
|
+
fileName: (format: string, name: string) => (format === 'es' ? `${name}.js` : `${name}.cjs`),
|
|
17
|
+
formats: ['es', 'cjs'],
|
|
18
|
+
},
|
|
19
|
+
rollupOptions: {
|
|
20
|
+
external: ['vue'],
|
|
21
|
+
output: {
|
|
22
|
+
globals: { vue: 'Vue' },
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
};
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { getSharedReactTemplate } from './shared/react';
|
|
2
|
-
|
|
3
|
-
export function getReactJsxTemplate(imagePrefix: string): string {
|
|
4
|
-
const shared = getSharedReactTemplate(imagePrefix);
|
|
5
|
-
|
|
6
|
-
return `${shared.imports}
|
|
7
|
-
|
|
8
|
-
${shared.comment}
|
|
9
|
-
export const PlaceBlockImage = ({
|
|
10
|
-
src,
|
|
11
|
-
alt,
|
|
12
|
-
lazy = false,
|
|
13
|
-
className = '',
|
|
14
|
-
...props
|
|
15
|
-
}) => {
|
|
16
|
-
${shared.hooks}
|
|
17
|
-
|
|
18
|
-
${shared.getImageClassName}
|
|
19
|
-
|
|
20
|
-
${shared.classNames}
|
|
21
|
-
|
|
22
|
-
${shared.jsx}
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
${shared.export}
|
|
26
|
-
`;
|
|
27
|
-
}
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { getSharedReactTemplate } from './shared/react';
|
|
2
|
-
|
|
3
|
-
export function getReactTsxTemplate(imagePrefix: string): string {
|
|
4
|
-
const shared = getSharedReactTemplate(imagePrefix);
|
|
5
|
-
|
|
6
|
-
return `${shared.imports}
|
|
7
|
-
|
|
8
|
-
interface PlaceBlockImageProps extends React.ImgHTMLAttributes<HTMLImageElement> {
|
|
9
|
-
src: string;
|
|
10
|
-
alt: string;
|
|
11
|
-
lazy?: boolean;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
${shared.comment}
|
|
15
|
-
export const PlaceBlockImage: React.FC<PlaceBlockImageProps> = ({
|
|
16
|
-
src,
|
|
17
|
-
alt,
|
|
18
|
-
lazy = false,
|
|
19
|
-
className = '',
|
|
20
|
-
...props
|
|
21
|
-
}) => {
|
|
22
|
-
${shared.hooks}
|
|
23
|
-
|
|
24
|
-
${shared.getImageClassName}
|
|
25
|
-
|
|
26
|
-
${shared.classNames}
|
|
27
|
-
|
|
28
|
-
${shared.jsx}
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
${shared.export}
|
|
32
|
-
`;
|
|
33
|
-
}
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import { CLASS_NAMES } from '../../constants';
|
|
2
|
-
|
|
3
|
-
export const getSharedTemplate = (imagePrefix: string) => ({
|
|
4
|
-
// Common comment block
|
|
5
|
-
comment: `/**
|
|
6
|
-
* PlaceBlockImage component that prevents layout shift using CSS custom properties
|
|
7
|
-
* Generated by place-block-image webpack plugin
|
|
8
|
-
*
|
|
9
|
-
* Usage:
|
|
10
|
-
* <PlaceBlockImage src="/images/logo.svg" alt="Logo" />
|
|
11
|
-
* <PlaceBlockImage src="/images/hero.jpg" alt="Hero" lazy={true} />
|
|
12
|
-
*
|
|
13
|
-
* This will automatically apply:
|
|
14
|
-
* - .${imagePrefix}wrapper class on picture (for dimensions)
|
|
15
|
-
* - .${imagePrefix}logo class on picture (specific dimensions via CSS custom properties)
|
|
16
|
-
* - ${CLASS_NAMES.LAZY}/${CLASS_NAMES.LAZY}.${CLASS_NAMES.LOADED} classes for lazy loading states
|
|
17
|
-
*/`,
|
|
18
|
-
|
|
19
|
-
// Common filename extraction logic (as string for interpolation)
|
|
20
|
-
getImageClassNameTemplate: `// Extract filename from src to generate class name
|
|
21
|
-
const getImageClassName = (imageSrc: string): string => {
|
|
22
|
-
// Remove /images/ prefix and file extension, convert to kebab-case
|
|
23
|
-
const filename = imageSrc
|
|
24
|
-
.replace(/^.*\\/images\\//, '') // Remove path up to /images/
|
|
25
|
-
.replace(/\\.[^/.]+$/, '') // Remove file extension
|
|
26
|
-
.toLowerCase()
|
|
27
|
-
.replace(/[^a-z0-9-]/g, '-') // Convert special chars to hyphens
|
|
28
|
-
.replace(/-+/g, '-') // Remove duplicate hyphens
|
|
29
|
-
.replace(/^-|-$/g, ''); // Remove leading/trailing hyphens
|
|
30
|
-
|
|
31
|
-
return \`${imagePrefix}\${filename}\`;
|
|
32
|
-
};`,
|
|
33
|
-
|
|
34
|
-
// Common intersection observer logic
|
|
35
|
-
intersectionObserverTemplate: `const observer = new IntersectionObserver(
|
|
36
|
-
(entries) => {
|
|
37
|
-
entries.forEach((entry) => {
|
|
38
|
-
if (entry.isIntersecting) {
|
|
39
|
-
setImageSrc(src);
|
|
40
|
-
setIsLoaded(true);
|
|
41
|
-
observer.unobserve(entry.target);
|
|
42
|
-
}
|
|
43
|
-
});
|
|
44
|
-
},
|
|
45
|
-
{ threshold: 0.1 }
|
|
46
|
-
);`
|
|
47
|
-
});
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { getSharedTemplate } from './index';
|
|
2
|
-
import { CLASS_NAMES } from '../../constants';
|
|
3
|
-
|
|
4
|
-
export const getSharedReactTemplate = (imagePrefix: string) => {
|
|
5
|
-
const shared = getSharedTemplate(imagePrefix);
|
|
6
|
-
|
|
7
|
-
return {
|
|
8
|
-
imports: `import React, { useRef, useEffect, useState } from 'react';`,
|
|
9
|
-
|
|
10
|
-
comment: shared.comment,
|
|
11
|
-
|
|
12
|
-
hooks: ` const imgRef = useRef(null);
|
|
13
|
-
const [imageSrc, setImageSrc] = useState(lazy ? '' : src);
|
|
14
|
-
const [isLoaded, setIsLoaded] = useState(!lazy);
|
|
15
|
-
|
|
16
|
-
useEffect(() => {
|
|
17
|
-
if (!lazy || isLoaded) return;
|
|
18
|
-
|
|
19
|
-
${shared.intersectionObserverTemplate}
|
|
20
|
-
|
|
21
|
-
if (imgRef.current) {
|
|
22
|
-
observer.observe(imgRef.current);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
return () => observer.disconnect();
|
|
26
|
-
}, [src, lazy, isLoaded]);`,
|
|
27
|
-
|
|
28
|
-
getImageClassName: shared.getImageClassNameTemplate,
|
|
29
|
-
|
|
30
|
-
classNames: ` const imageClassName = getImageClassName(src);
|
|
31
|
-
const wrapperClassName = \`${imagePrefix}wrapper \${imageClassName} \${className || ''}\`.trim();
|
|
32
|
-
|
|
33
|
-
// Build img className with lazy states
|
|
34
|
-
const lazyClass = lazy ? (isLoaded ? '${CLASS_NAMES.LAZY} ${CLASS_NAMES.LOADED}' : '${CLASS_NAMES.LAZY}') : '';
|
|
35
|
-
const imgClassName = \`${CLASS_NAMES.IMAGE_BLOCK} \${lazyClass}\`.trim();`,
|
|
36
|
-
|
|
37
|
-
jsx: ` return (
|
|
38
|
-
<picture className={wrapperClassName}>
|
|
39
|
-
<img
|
|
40
|
-
ref={imgRef}
|
|
41
|
-
src={imageSrc}
|
|
42
|
-
alt={alt}
|
|
43
|
-
className={imgClassName}
|
|
44
|
-
{...props}
|
|
45
|
-
/>
|
|
46
|
-
</picture>
|
|
47
|
-
);`,
|
|
48
|
-
|
|
49
|
-
export: `export default PlaceBlockImage;`
|
|
50
|
-
};
|
|
51
|
-
};
|
package/src/templates/vue.ts
DELETED
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
import { getSharedTemplate } from './shared/';
|
|
2
|
-
import { CLASS_NAMES } from '../constants';
|
|
3
|
-
|
|
4
|
-
export function getVueTemplate(imagePrefix: string): string {
|
|
5
|
-
const shared = getSharedTemplate(imagePrefix);
|
|
6
|
-
|
|
7
|
-
return `<template>
|
|
8
|
-
<picture :class="wrapperClassName">
|
|
9
|
-
<img
|
|
10
|
-
ref="imgRef"
|
|
11
|
-
:src="imageSrc"
|
|
12
|
-
:alt="alt"
|
|
13
|
-
:class="imgClassName"
|
|
14
|
-
v-bind="$attrs"
|
|
15
|
-
/>
|
|
16
|
-
</picture>
|
|
17
|
-
</template>
|
|
18
|
-
|
|
19
|
-
<script setup lang="ts">
|
|
20
|
-
import { computed, ref, onMounted, onUnmounted, watch } from 'vue';
|
|
21
|
-
|
|
22
|
-
interface Props {
|
|
23
|
-
src: string;
|
|
24
|
-
alt: string;
|
|
25
|
-
lazy?: boolean;
|
|
26
|
-
class?: string;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const props = withDefaults(defineProps<Props>(), {
|
|
30
|
-
lazy: false,
|
|
31
|
-
class: ''
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
${shared.comment}
|
|
35
|
-
|
|
36
|
-
const imgRef = ref<HTMLImageElement | null>(null);
|
|
37
|
-
const imageSrc = ref(props.lazy ? '' : props.src);
|
|
38
|
-
const isLoaded = ref(!props.lazy);
|
|
39
|
-
let observer: IntersectionObserver | null = null;
|
|
40
|
-
|
|
41
|
-
${shared.getImageClassNameTemplate}
|
|
42
|
-
|
|
43
|
-
const imageClassName = computed(() => getImageClassName(props.src));
|
|
44
|
-
const wrapperClassName = computed(() =>
|
|
45
|
-
\`${imagePrefix}wrapper \${imageClassName.value}\`
|
|
46
|
-
);
|
|
47
|
-
|
|
48
|
-
const lazyClass = computed(() =>
|
|
49
|
-
props.lazy ? (isLoaded.value ? '${CLASS_NAMES.LAZY} ${CLASS_NAMES.LOADED}' : '${CLASS_NAMES.LAZY}') : ''
|
|
50
|
-
);
|
|
51
|
-
|
|
52
|
-
const imgClassName = computed(() =>
|
|
53
|
-
\`\${props.class} \${lazyClass.value}\`.trim()
|
|
54
|
-
);
|
|
55
|
-
|
|
56
|
-
const setupLazyLoading = () => {
|
|
57
|
-
if (!props.lazy || isLoaded.value) return;
|
|
58
|
-
|
|
59
|
-
${shared.intersectionObserverTemplate}
|
|
60
|
-
|
|
61
|
-
if (imgRef.value) {
|
|
62
|
-
observer.observe(imgRef.value);
|
|
63
|
-
}
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
const cleanupObserver = () => {
|
|
67
|
-
if (observer) {
|
|
68
|
-
observer.disconnect();
|
|
69
|
-
observer = null;
|
|
70
|
-
}
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
onMounted(() => {
|
|
74
|
-
setupLazyLoading();
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
onUnmounted(() => {
|
|
78
|
-
cleanupObserver();
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
watch(() => props.src, (newSrc) => {
|
|
82
|
-
if (!props.lazy) {
|
|
83
|
-
imageSrc.value = newSrc;
|
|
84
|
-
} else if (!isLoaded.value) {
|
|
85
|
-
cleanupObserver();
|
|
86
|
-
setupLazyLoading();
|
|
87
|
-
}
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
watch(isLoaded, (loaded) => {
|
|
91
|
-
if (loaded) {
|
|
92
|
-
imageSrc.value = props.src;
|
|
93
|
-
cleanupObserver();
|
|
94
|
-
}
|
|
95
|
-
});
|
|
96
|
-
</script>
|
|
97
|
-
`;
|
|
98
|
-
}
|
package/src/templates.ts
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
// Main template exports
|
|
2
|
-
import { getReactTsxTemplate as getReactTsx } from './templates/react-tsx';
|
|
3
|
-
import { getReactJsxTemplate as getReactJsx } from './templates/react-jsx';
|
|
4
|
-
import { getVueTemplate as getVue } from './templates/vue';
|
|
5
|
-
|
|
6
|
-
export function getReactTsxTemplate(imagePrefix: string): string {
|
|
7
|
-
return getReactTsx(imagePrefix);
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function getReactJsxTemplate(imagePrefix: string): string {
|
|
11
|
-
return getReactJsx(imagePrefix);
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function getVueTemplate(imagePrefix: string): string {
|
|
15
|
-
return getVue(imagePrefix);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function getTemplate(type: 'tsx' | 'jsx' | 'vue', imagePrefix: string): string {
|
|
19
|
-
switch (type) {
|
|
20
|
-
case 'tsx':
|
|
21
|
-
return getReactTsxTemplate(imagePrefix);
|
|
22
|
-
case 'jsx':
|
|
23
|
-
return getReactJsxTemplate(imagePrefix);
|
|
24
|
-
case 'vue':
|
|
25
|
-
return getVueTemplate(imagePrefix);
|
|
26
|
-
default:
|
|
27
|
-
throw new Error(`Unsupported component type: ${type}`);
|
|
28
|
-
}
|
|
29
|
-
}
|