@place-framework/place-block-image 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +117 -0
  2. package/dist/constants/index.d.ts +9 -0
  3. package/dist/constants/index.d.ts.map +1 -0
  4. package/dist/constants/index.js +17 -0
  5. package/dist/constants/index.js.map +1 -0
  6. package/dist/index.d.ts +3 -0
  7. package/dist/index.d.ts.map +1 -0
  8. package/dist/index.js +7 -0
  9. package/dist/index.js.map +1 -0
  10. package/dist/templates/react-jsx.d.ts +2 -0
  11. package/dist/templates/react-jsx.d.ts.map +1 -0
  12. package/dist/templates/react-jsx.js +29 -0
  13. package/dist/templates/react-jsx.js.map +1 -0
  14. package/dist/templates/react-tsx.d.ts +2 -0
  15. package/dist/templates/react-tsx.d.ts.map +1 -0
  16. package/dist/templates/react-tsx.js +35 -0
  17. package/dist/templates/react-tsx.js.map +1 -0
  18. package/dist/templates/shared/index.d.ts +6 -0
  19. package/dist/templates/shared/index.d.ts.map +1 -0
  20. package/dist/templates/shared/index.js +49 -0
  21. package/dist/templates/shared/index.js.map +1 -0
  22. package/dist/templates/shared/react.d.ts +10 -0
  23. package/dist/templates/shared/react.d.ts.map +1 -0
  24. package/dist/templates/shared/react.js +48 -0
  25. package/dist/templates/shared/react.js.map +1 -0
  26. package/dist/templates/vue.d.ts +2 -0
  27. package/dist/templates/vue.d.ts.map +1 -0
  28. package/dist/templates/vue.js +100 -0
  29. package/dist/templates/vue.js.map +1 -0
  30. package/dist/templates.d.ts +5 -0
  31. package/dist/templates.d.ts.map +1 -0
  32. package/dist/templates.js +32 -0
  33. package/dist/templates.js.map +1 -0
  34. package/dist/utils/index.d.ts +8 -0
  35. package/dist/utils/index.d.ts.map +1 -0
  36. package/dist/utils/index.js +32 -0
  37. package/dist/utils/index.js.map +1 -0
  38. package/dist/webpack-plugin.d.ts +49 -0
  39. package/dist/webpack-plugin.d.ts.map +1 -0
  40. package/dist/webpack-plugin.js +259 -0
  41. package/dist/webpack-plugin.js.map +1 -0
  42. package/package.json +49 -0
  43. package/src/constants/index.ts +14 -0
  44. package/src/index.ts +4 -0
  45. package/src/templates/react-jsx.ts +27 -0
  46. package/src/templates/react-tsx.ts +33 -0
  47. package/src/templates/shared/index.ts +47 -0
  48. package/src/templates/shared/react.ts +51 -0
  49. package/src/templates/vue.ts +98 -0
  50. package/src/templates.ts +29 -0
  51. package/src/utils/index.ts +35 -0
  52. package/src/webpack-plugin.ts +273 -0
  53. package/tsconfig.json +20 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webpack-plugin.d.ts","sourceRoot":"","sources":["../src/webpack-plugin.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAGnC,MAAM,WAAW,4BAA4B;IAC3C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IAEjB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,KAAK,GAAG,KAAK,GAAG,KAAK,CAAC;IACtC,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,qBAAa,qBAAqB;IAChC,OAAO,CAAC,OAAO,CAAyD;IACxE,OAAO,CAAC,YAAY,CAAkB;IACtC,OAAO,CAAC,kBAAkB,CAAa;gBAE3B,OAAO,EAAE,4BAA4B;IASjD;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAYzB;;OAEG;YACW,UAAU;IA8BxB;;OAEG;IACH,OAAO,CAAC,YAAY;IAmCpB;;OAEG;YACW,aAAa;IAY3B;;OAEG;YACW,kBAAkB;IA0BhC;;OAEG;YACW,gBAAgB;IAyB9B,KAAK,CAAC,QAAQ,EAAE,QAAQ,GAAG,IAAI;YA4BjB,mBAAmB;CA8ClC;AAED,eAAe,qBAAqB,CAAC"}
@@ -0,0 +1,259 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.PlaceBlockImagePlugin = void 0;
40
+ const fs = __importStar(require("fs"));
41
+ const path = __importStar(require("path"));
42
+ const glob_1 = require("glob");
43
+ const image_size_1 = __importDefault(require("image-size"));
44
+ const templates_1 = require("./templates");
45
+ class PlaceBlockImagePlugin {
46
+ constructor(options) {
47
+ this.isGenerating = false;
48
+ this.lastGenerationTime = 0;
49
+ this.options = {
50
+ imagePrefix: 'image-',
51
+ generateComponent: true,
52
+ componentType: 'tsx',
53
+ ...options
54
+ };
55
+ }
56
+ /**
57
+ * Generate CSS class name from filename
58
+ */
59
+ generateClassName(filename) {
60
+ const name = path.basename(filename, path.extname(filename));
61
+ // Convert to kebab-case and remove special characters
62
+ const cleanName = name
63
+ .toLowerCase()
64
+ .replace(/[^a-z0-9-]/g, '-')
65
+ .replace(/-+/g, '-')
66
+ .replace(/^-|-$/g, '');
67
+ return `${this.options.imagePrefix}${cleanName}`;
68
+ }
69
+ /**
70
+ * Scan directory for images and get their dimensions
71
+ */
72
+ async scanImages() {
73
+ const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'svg', 'webp', 'bmp', 'ico', 'avif'];
74
+ const pattern = `${this.options.imageDir}/**/*.{${imageExtensions.join(',')}}`;
75
+ const files = await (0, glob_1.glob)(pattern, { nodir: true });
76
+ const results = [];
77
+ for (const file of files) {
78
+ try {
79
+ const dimensions = (0, image_size_1.default)(file);
80
+ if (dimensions.width && dimensions.height) {
81
+ const relativePath = path.relative(this.options.imageDir, file);
82
+ const className = this.generateClassName(relativePath);
83
+ results.push({
84
+ width: dimensions.width,
85
+ height: dimensions.height,
86
+ filename: relativePath,
87
+ className
88
+ });
89
+ }
90
+ }
91
+ catch (error) {
92
+ console.warn(`⚠️ Could not read dimensions for ${file}:`, error);
93
+ }
94
+ }
95
+ return results;
96
+ }
97
+ /**
98
+ * Generate SCSS with CSS custom properties and base class
99
+ */
100
+ generateScss(images) {
101
+ const wrapperClassName = `${this.options.imagePrefix}wrapper`;
102
+ const lines = [
103
+ '// Auto-generated image dimensions',
104
+ '// This file is generated by place-block-image webpack plugin',
105
+ '// Do not edit manually - changes will be overwritten',
106
+ '',
107
+ '// Base wrapper and image styles',
108
+ `.${wrapperClassName} {`,
109
+ ' display: block;',
110
+ ' aspect-ratio: calc(var(--p-width) / var(--p-height));',
111
+ '',
112
+ ' &,',
113
+ ' img {',
114
+ ' display: block;',
115
+ ' }',
116
+ '',
117
+ '// Ensure images fill the wrapper and maintain aspect ratio',
118
+ ' img {',
119
+ ' width: 100%;',
120
+ ' height: 100%;',
121
+ ' object-fit: cover;',
122
+ ' }',
123
+ '}',
124
+ '',
125
+ '// Image-specific dimensions',
126
+ ...images.map(img => `.${wrapperClassName}.${img.className} {\n --p-width: ${img.width}px;\n --p-height: ${img.height}px;\n}`),
127
+ ''
128
+ ];
129
+ return lines.join('\n');
130
+ }
131
+ /**
132
+ * Write SCSS file
133
+ */
134
+ async writeScssFile(images) {
135
+ const scssContent = this.generateScss(images);
136
+ const outputDir = path.dirname(this.options.scssPath);
137
+ // Ensure output directory exists
138
+ if (!fs.existsSync(outputDir)) {
139
+ fs.mkdirSync(outputDir, { recursive: true });
140
+ }
141
+ fs.writeFileSync(this.options.scssPath, scssContent);
142
+ }
143
+ /**
144
+ * Generate and write component file
145
+ */
146
+ async writeComponentFile() {
147
+ if (!this.options.generateComponent || !this.options.componentPath || !this.options.componentType) {
148
+ return;
149
+ }
150
+ const componentFileName = `PlaceBlockImage.${this.options.componentType}`;
151
+ const componentFilePath = path.resolve(this.options.componentPath, componentFileName);
152
+ // Check if component already exists
153
+ if (fs.existsSync(componentFilePath)) {
154
+ console.log(`📦 Component already exists, skipping: ${componentFilePath}`);
155
+ return;
156
+ }
157
+ const componentContent = (0, templates_1.getTemplate)(this.options.componentType, this.options.imagePrefix);
158
+ const outputDir = path.dirname(componentFilePath);
159
+ // Ensure output directory exists
160
+ if (!fs.existsSync(outputDir)) {
161
+ fs.mkdirSync(outputDir, { recursive: true });
162
+ }
163
+ fs.writeFileSync(componentFilePath, componentContent);
164
+ console.log(`📦 Generated component: ${componentFilePath}`);
165
+ }
166
+ /**
167
+ * Check if images have changed since last generation
168
+ */
169
+ async shouldRegenerate() {
170
+ if (!fs.existsSync(this.options.scssPath)) {
171
+ return true;
172
+ }
173
+ const scssStats = fs.statSync(this.options.scssPath);
174
+ const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'svg', 'webp', 'bmp', 'ico', 'avif'];
175
+ const pattern = `${this.options.imageDir}/**/*.{${imageExtensions.join(',')}}`;
176
+ try {
177
+ const files = await (0, glob_1.glob)(pattern, { nodir: true });
178
+ for (const file of files) {
179
+ const fileStats = fs.statSync(file);
180
+ if (fileStats.mtime > scssStats.mtime) {
181
+ return true;
182
+ }
183
+ }
184
+ return false;
185
+ }
186
+ catch {
187
+ return true;
188
+ }
189
+ }
190
+ apply(compiler) {
191
+ // Use environment hook to run only once at startup
192
+ compiler.hooks.environment.tap('PlaceBlockImagePlugin', () => {
193
+ this.generateImageStyles();
194
+ });
195
+ // Also run on watchRun for file changes during development
196
+ compiler.hooks.watchRun.tapAsync('PlaceBlockImagePlugin', async (compiler, callback) => {
197
+ // Prevent running too frequently
198
+ const now = Date.now();
199
+ if (this.isGenerating || (now - this.lastGenerationTime) < 1000) {
200
+ callback();
201
+ return;
202
+ }
203
+ try {
204
+ const shouldRegenerate = await this.shouldRegenerate();
205
+ if (shouldRegenerate) {
206
+ await this.generateImageStyles();
207
+ }
208
+ callback();
209
+ }
210
+ catch (error) {
211
+ console.error('❌ PlaceBlockImagePlugin error:', error);
212
+ callback();
213
+ }
214
+ });
215
+ }
216
+ async generateImageStyles() {
217
+ if (this.isGenerating) {
218
+ return;
219
+ }
220
+ this.isGenerating = true;
221
+ this.lastGenerationTime = Date.now();
222
+ try {
223
+ console.log('🖼️ PlaceBlockImagePlugin: Generating image styles...');
224
+ if (!fs.existsSync(this.options.imageDir)) {
225
+ console.warn(`⚠️ Image directory does not exist: ${this.options.imageDir}`);
226
+ return;
227
+ }
228
+ console.log(`🔍 Scanning images in: ${this.options.imageDir}`);
229
+ const images = await this.scanImages();
230
+ if (images.length === 0) {
231
+ console.log('⚠️ No images found');
232
+ return;
233
+ }
234
+ await this.writeScssFile(images);
235
+ await this.writeComponentFile();
236
+ console.log(`✅ Generated CSS custom properties for ${images.length} images`);
237
+ console.log(`📝 Output: ${this.options.scssPath}`);
238
+ // Log some examples
239
+ if (images.length > 0) {
240
+ console.log('📋 Generated classes:');
241
+ images.slice(0, 3).forEach(img => {
242
+ console.log(` .${img.className} { --p-width: ${img.width}; --p-height: ${img.height}; }`);
243
+ });
244
+ if (images.length > 3) {
245
+ console.log(` ... and ${images.length - 3} more`);
246
+ }
247
+ }
248
+ }
249
+ catch (error) {
250
+ console.error('❌ PlaceBlockImagePlugin error:', error);
251
+ }
252
+ finally {
253
+ this.isGenerating = false;
254
+ }
255
+ }
256
+ }
257
+ exports.PlaceBlockImagePlugin = PlaceBlockImagePlugin;
258
+ exports.default = PlaceBlockImagePlugin;
259
+ //# sourceMappingURL=webpack-plugin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webpack-plugin.js","sourceRoot":"","sources":["../src/webpack-plugin.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@place-framework/place-block-image",
3
+ "version": "1.0.0",
4
+ "description": "A utility package for generating CSS custom properties from image dimensions to prevent layout shift",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "dev": "tsc --watch",
10
+ "generate": "node dist/generate.js",
11
+ "test": "echo \"Error: no test specified\" && exit 1"
12
+ },
13
+ "keywords": [
14
+ "image",
15
+ "layout-shift",
16
+ "css",
17
+ "custom-properties",
18
+ "react",
19
+ "vue",
20
+ "typescript"
21
+ ],
22
+ "author": "Brian Kelley",
23
+ "license": "MIT",
24
+ "devDependencies": {
25
+ "@types/node": "^20.0.0",
26
+ "@types/glob": "^8.1.0",
27
+ "typescript": "^5.0.0"
28
+ },
29
+ "dependencies": {
30
+ "image-size": "^1.0.2",
31
+ "glob": "^10.0.0"
32
+ },
33
+ "peerDependencies": {
34
+ "webpack": ">=5.0.0",
35
+ "react": ">=16.8.0",
36
+ "vue": ">=3.0.0"
37
+ },
38
+ "peerDependenciesMeta": {
39
+ "react": {
40
+ "optional": true
41
+ },
42
+ "vue": {
43
+ "optional": true
44
+ },
45
+ "webpack": {
46
+ "optional": false
47
+ }
48
+ }
49
+ }
@@ -0,0 +1,14 @@
1
+ export const CLASS_NAMES = {
2
+ // Lazy loading states
3
+ LAZY: 'lazy',
4
+ LOADED: 'loaded',
5
+
6
+ // Wrapper classes
7
+ IMAGE_WRAPPER: 'image-wrapper',
8
+
9
+ // Base image class
10
+ IMAGE_BLOCK: 'image-block'
11
+ } as const;
12
+
13
+ export const getWrapperClassName = (imagePrefix: string) => `${imagePrefix}wrapper`;
14
+ export const getImageClassName = (imagePrefix: string, filename: string) => `${imagePrefix}${filename}`;
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ // Main exports for place-block-image package
2
+
3
+ export { PlaceBlockImagePlugin } from './webpack-plugin';
4
+ export type { PlaceBlockImagePluginOptions, ImageDimensions } from './webpack-plugin';
@@ -0,0 +1,27 @@
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
+ }
@@ -0,0 +1,33 @@
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
+ }
@@ -0,0 +1,47 @@
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
+ });
@@ -0,0 +1,51 @@
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
+ };
@@ -0,0 +1,98 @@
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
+ }
@@ -0,0 +1,29 @@
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
+ }
@@ -0,0 +1,35 @@
1
+ // Shared template utilities
2
+
3
+ export interface TemplateData {
4
+ imagePrefix: string;
5
+ baseClassName: string;
6
+ wrapperClassName: string;
7
+ }
8
+
9
+ export function getTemplateData(imagePrefix: string): TemplateData {
10
+ return {
11
+ imagePrefix,
12
+ baseClassName: `${imagePrefix}block`,
13
+ wrapperClassName: `${imagePrefix}wrapper`
14
+ };
15
+ }
16
+
17
+ export function getSharedLogic(imagePrefix: string): string {
18
+ return `
19
+ // Extract filename from src to generate class name
20
+ const getImageClassName = (imageSrc) => {
21
+ // Remove /images/ prefix and file extension, convert to kebab-case
22
+ const filename = imageSrc
23
+ .replace(/^.*\\/images\\//, '') // Remove path up to /images/
24
+ .replace(/\\.[^/.]+$/, '') // Remove file extension
25
+ .toLowerCase()
26
+ .replace(/[^a-z0-9-]/g, '-') // Convert special chars to hyphens
27
+ .replace(/-+/g, '-') // Remove duplicate hyphens
28
+ .replace(/^-|-$/g, ''); // Remove leading/trailing hyphens
29
+
30
+ return \`${imagePrefix}\${filename}\`;
31
+ };
32
+
33
+ const imageClassName = getImageClassName(src);
34
+ const wrapperClassName = \`${imagePrefix}wrapper \${imageClassName}\`;`;
35
+ }