iwgt 2.4.11 → 2.4.13

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.
@@ -0,0 +1,484 @@
1
+ /**
2
+ * Генераторы для snippet файлов (liquid, scss, js)
3
+ */
4
+ /**
5
+ * Генерирует snippet.liquid
6
+ * ВАЖНО: Код автоматически оборачивается системой в div.layout с CSS-переменными
7
+ * НЕ добавляйте обёртку layout вручную!
8
+ */
9
+ export function generateSnippetLiquid(config, cleanMode = false) {
10
+ const useBlocks = config.type === 'block_list_widget_type';
11
+ let liquid = '';
12
+ if (!cleanMode) {
13
+ liquid += `{% comment %}
14
+ Виджет: ${generateWidgetName(config.description)}
15
+ Категория: ${config.category}
16
+ Тип: ${config.type}
17
+
18
+ ВАЖНО:
19
+ - Этот код будет автоматически обёрнут в div.layout с классом widget-type_${config.handle}
20
+ - CSS-переменные из настроек будут доступны через var(--handle-настройки)
21
+ - Используйте widget_settings для доступа к настройкам
22
+ ${useBlocks ? '- Блоки доступны через data.blocks (не widget.blocks!)' : ''}
23
+ {% endcomment %}
24
+
25
+ `;
26
+ }
27
+ liquid += `<div class="widget-container">
28
+ `;
29
+ // Заголовок виджета (если есть в настройках)
30
+ liquid += ` {% if widget_settings.show-heading %}
31
+ <h2 class="widget-heading">{{ widget_settings.heading-text }}</h2>
32
+ {% endif %}
33
+
34
+ `;
35
+ if (useBlocks) {
36
+ const blockTemplate = config.blockTemplate;
37
+ const exampleFields = blockTemplate ? Object.keys(blockTemplate.fields).slice(0, 3) : ['name', 'image', 'link'];
38
+ liquid += ` {% comment %} Блоки используют шаблон: ${blockTemplate?.handle || 'не указан'} {% endcomment %}
39
+ {% if data.blocks and data.blocks.size > 0 %}
40
+ <div class="widget-blocks">
41
+ {% for block in data.blocks %}
42
+ <div class="widget-block">
43
+ {% comment %} Доступные поля блока: ${exampleFields.join(', ')} {% endcomment %}
44
+
45
+ {% if block.image %}
46
+ {% assign img_width = widget_settings.layout-content-max-width | default: 1200 %}
47
+ <picture class="block-image">
48
+ <source media="(min-width:769px)" data-srcset="{{ block.image | image_url: img_width, format: 'webp', resizing_type: 'fit_width' }}" type="image/webp" class="lazyload">
49
+ <source media="(max-width:768px)" data-srcset="{{ block.image | image_url: 768, format: 'webp', resizing_type: 'fit_width' }}" type="image/webp" class="lazyload">
50
+ <img data-src="{{ block.image | image_url: 800, resizing_type: 'fit_width' }}" alt="{{ block.name }}" class="lazyload">
51
+ </picture>
52
+ {% endif %}
53
+
54
+ {% if block.name %}
55
+ <h3 class="block-title">{{ block.name }}</h3>
56
+ {% endif %}
57
+
58
+ {% if block.description or block.content %}
59
+ <div class="block-content">
60
+ {{ block.description | default: block.content }}
61
+ </div>
62
+ {% endif %}
63
+
64
+ {% if block.link %}
65
+ <a href="{{ block.link }}" class="block-link">
66
+ {{ block.button_text | default: widget_messages.read_more }}
67
+ </a>
68
+ {% endif %}
69
+ </div>
70
+ {% endfor %}
71
+ </div>
72
+ {% else %}
73
+ {% comment %} Заглушка для редактора {% endcomment %}
74
+ {% if editor_mode? %}
75
+ <div class="widget-empty">
76
+ <p>{{ widget_messages.no_blocks | default: 'Добавьте блоки для отображения контента' }}</p>
77
+ </div>
78
+ {% endif %}
79
+ {% endif %}
80
+ `;
81
+ }
82
+ else {
83
+ liquid += ` <div class="widget-content">
84
+ {% comment %} Здесь размещается контент простого виджета {% endcomment %}
85
+ <p>{{ widget_messages.widget_description | default: 'Контент виджета' }}</p>
86
+ </div>
87
+ `;
88
+ }
89
+ liquid += `</div>
90
+ `;
91
+ return liquid;
92
+ }
93
+ /**
94
+ * Генерирует snippet.scss
95
+ * ВАЖНО: Весь код оборачивается в класс виджета
96
+ * Используйте & для обращения к родительскому элементу (layout + класс виджета)
97
+ */
98
+ export function generateSnippetScss(config, cleanMode = false) {
99
+ let scss = '';
100
+ if (!cleanMode) {
101
+ scss += `// ВАЖНО: Код автоматически оборачивается в .layout.widget-type_${config.handle}
102
+ // Используйте & для обращения к родительскому классу
103
+ // CSS-переменные из настроек доступны через var(--название-настройки)
104
+
105
+ // КРИТИЧЕСКИ ВАЖНО: Миксин background-color устанавливает фон И автоматически
106
+ // определяет контрастный цвет текста (светлый на тёмном фоне и наоборот)
107
+ // Переменная --bg всегда доступна из настроек design
108
+ `;
109
+ }
110
+ scss += `@include background-color(--bg);
111
+
112
+ & {
113
+ // Основные стили родительского элемента
114
+ padding-top: var(--layout-pt, 2rem);
115
+ padding-bottom: var(--layout-pb, 2rem);
116
+ position: relative;
117
+ }
118
+
119
+ // Широкий фон
120
+ &[style*="--layout-wide-bg:true"] {
121
+ width: 100vw;
122
+ margin-left: calc(50% - 50vw);
123
+ }
124
+
125
+ // Скрытие на устройствах
126
+ &[style*="--hide-desktop:true"] {
127
+ @media (min-width: 1024px) {
128
+ display: none;
129
+ }
130
+ }
131
+
132
+ &[style*="--hide-mobile:true"] {
133
+ @media (max-width: 767px) {
134
+ display: none;
135
+ }
136
+ }
137
+
138
+ // Состояния на основе настроек
139
+ &[style*="--heading-hide:true"] {
140
+ .widget-heading {
141
+ display: none;
142
+ }
143
+ }
144
+
145
+ // Контейнер виджета
146
+ .widget-container {
147
+ max-width: var(--layout-content-max-width, 1200px);
148
+ margin: 0 auto;
149
+ padding: 0 15px;
150
+ }
151
+
152
+ // Заголовок виджета
153
+ .widget-heading {
154
+ text-align: var(--align-title, left);
155
+ margin-bottom: 2rem;
156
+ font-size: 1.5rem;
157
+ font-weight: 600;
158
+ }
159
+
160
+ // Блоки (для block_list_widget_type)
161
+ .widget-blocks {
162
+ display: grid;
163
+ gap: var(--slide-gap, 2rem);
164
+
165
+ @media (min-width: 768px) {
166
+ grid-template-columns: repeat(auto-fit, minmax(var(--slide-width, 300px), 1fr));
167
+ }
168
+
169
+ @media (max-width: 767px) {
170
+ grid-template-columns: repeat(auto-fit, minmax(var(--slide-width-mobile, 150px), 1fr));
171
+ }
172
+ }
173
+
174
+ .widget-block {
175
+ position: relative;
176
+
177
+ .block-image {
178
+ width: 100%;
179
+ aspect-ratio: var(--img-ratio, 1);
180
+ object-fit: cover;
181
+ border-radius: var(--banner-border-radius, 0);
182
+ overflow: hidden;
183
+
184
+ @media (max-width: 767px) {
185
+ aspect-ratio: var(--img-ratio-mobile, 1);
186
+ }
187
+
188
+ img {
189
+ width: 100%;
190
+ height: 100%;
191
+ object-fit: cover;
192
+ transition: transform 0.3s ease;
193
+ }
194
+ }
195
+
196
+ &:hover .block-image img {
197
+ transform: scale(1.05);
198
+ }
199
+
200
+ .block-title {
201
+ margin: 1rem 0 0.5rem;
202
+ font-size: 1.25rem;
203
+ font-weight: 600;
204
+ }
205
+
206
+ .block-content {
207
+ margin: 0.5rem 0;
208
+ color: var(--text-color, inherit);
209
+ line-height: 1.6;
210
+ }
211
+
212
+ .block-link {
213
+ display: inline-block;
214
+ margin-top: 1rem;
215
+ padding: 0.5rem 1.5rem;
216
+ background: var(--button-bg, #000);
217
+ color: var(--button-color, #fff);
218
+ text-decoration: none;
219
+ border-radius: var(--button-radius, 4px);
220
+ transition: opacity 0.2s;
221
+
222
+ &:hover {
223
+ opacity: 0.8;
224
+ }
225
+ }
226
+ }
227
+
228
+ // Заглушка для редактора
229
+ .widget-empty {
230
+ padding: 3rem;
231
+ text-align: center;
232
+ background: #f5f5f5;
233
+ border: 2px dashed #ddd;
234
+ border-radius: 8px;
235
+
236
+ p {
237
+ margin: 0;
238
+ color: #999;
239
+ font-size: 0.875rem;
240
+ }
241
+ }
242
+
243
+ // Адаптивность
244
+ @media screen and (max-width: 767px) {
245
+ & {
246
+ // На мобильных используются те же переменные --layout-pt и --layout-pb
247
+ // Значения задаются в настройках в vw
248
+ }
249
+ }
250
+ `;
251
+ return scss;
252
+ }
253
+ /**
254
+ * Генерирует snippet.js
255
+ * ВАЖНО:
256
+ * - Код автоматически оборачивается в try/catch
257
+ * - Доступны переменные: widget (селектор) и $widget (jQuery объект)
258
+ * - НЕ используйте Vue.js или другие фреймворки!
259
+ * - На странице может быть несколько экземпляров виджета - используйте $widget.each()
260
+ */
261
+ export function generateSnippetJs(config, cleanMode = false) {
262
+ const hasJquery = config.libraries.includes('jquery');
263
+ const hasCommonjs = config.libraries.includes('commonjs_v2');
264
+ const hasSplide = config.libraries.includes('splide') || config.libraries.includes('splide3');
265
+ const widgetClass = `widget-type_${config.handle}`;
266
+ let js = '';
267
+ if (!cleanMode) {
268
+ js += `/**
269
+ * Виджет: ${generateWidgetName(config.description)}
270
+ * Handle: ${config.handle}
271
+ *
272
+ * ВАЖНО:
273
+ * - Этот код автоматически оборачивается в try/catch
274
+ * - Доступны переменные: widget = '.${widgetClass}' и $widget = $('.${widgetClass}')
275
+ * - Виджет обёрнут в .layout.${widgetClass}
276
+ * - На странице может быть несколько виджетов - используйте $widget.each()
277
+ */
278
+
279
+ `;
280
+ }
281
+ if (hasCommonjs && !cleanMode) {
282
+ js += `// CommonJS API InSales
283
+ // Документация: https://liquidhub.ru/collection/start
284
+ // Основные модули: EventBus, Cart, Products, Shop
285
+
286
+ `;
287
+ }
288
+ if (hasJquery) {
289
+ if (!cleanMode) {
290
+ js += `// Переменная $widget уже доступна (создана системой)
291
+ // ВАЖНО: На странице может быть несколько виджетов одного типа
292
+ `;
293
+ }
294
+ js += `$widget.each(function(index, el) {
295
+ const $widgetInstance = $(el);
296
+
297
+ // Пример: получение настроек из CSS-переменных для каждого экземпляра
298
+ const settings = {
299
+ slideWidth: getComputedStyle(el).getPropertyValue('--slide-width'),
300
+ slideGap: getComputedStyle(el).getPropertyValue('--slide-gap'),
301
+ };
302
+
303
+ console.log(\`Инициализация виджета #\${index}\`, settings);
304
+
305
+ `;
306
+ if (hasSplide) {
307
+ js += ` // Инициализация Splide слайдера для этого экземпляра
308
+ const splideEl = $widgetInstance.find('.splide');
309
+ if (splideEl.length) {
310
+ new Splide(splideEl[0], {
311
+ type: 'loop',
312
+ perPage: 3,
313
+ perMove: 1,
314
+ gap: '1rem',
315
+ pagination: false,
316
+ breakpoints: {
317
+ 768: {
318
+ perPage: 1,
319
+ },
320
+ 1024: {
321
+ perPage: 2,
322
+ },
323
+ },
324
+ }).mount();
325
+ }
326
+
327
+ `;
328
+ }
329
+ js += ` // Ваш код для каждого экземпляра виджета
330
+ });
331
+
332
+ `;
333
+ if (hasCommonjs && !cleanMode) {
334
+ js += `// Пример работы с EventBus (глобальные события, вне цикла)
335
+ EventBus.subscribe('cart:add', function(cart) {
336
+ console.log('Товар добавлен в корзину', cart);
337
+ });
338
+
339
+ // Реакция на изменение настроек в редакторе
340
+ EventBus.subscribe([
341
+ 'widget:input-setting:insales:system:editor',
342
+ 'widget:change-setting:insales:system:editor'
343
+ ], function(data) {
344
+ console.log('Настройка изменена:', data.setting_name, data.value);
345
+ // Здесь можно обновить виджет при изменении настроек
346
+ });
347
+
348
+ `;
349
+ }
350
+ }
351
+ else {
352
+ if (!cleanMode) {
353
+ js += `// Переменная widget уже доступна (создана системой)
354
+ // ВАЖНО: На странице может быть несколько виджетов одного типа
355
+ `;
356
+ }
357
+ js += `const widgetElements = document.querySelectorAll(widget);
358
+
359
+ if (widgetElements.length === 0) return;
360
+
361
+ console.log('Виджет ${config.handle} инициализирован, найдено экземпляров:', widgetElements.length);
362
+
363
+ // Перебираем каждый экземпляр виджета на странице
364
+ widgetElements.forEach((el, index) => {
365
+ // Пример: получение настроек из CSS-переменных для каждого экземпляра
366
+ const styles = getComputedStyle(el);
367
+ const settings = {
368
+ slideWidth: styles.getPropertyValue('--slide-width'),
369
+ slideGap: styles.getPropertyValue('--slide-gap'),
370
+ };
371
+
372
+ console.log(\`Инициализация виджета #\${index}\`, settings);
373
+
374
+ `;
375
+ if (hasSplide) {
376
+ js += ` // Инициализация Splide слайдера для этого экземпляра
377
+ const splideEl = el.querySelector('.splide');
378
+ if (splideEl) {
379
+ new Splide(splideEl, {
380
+ type: 'loop',
381
+ perPage: 3,
382
+ perMove: 1,
383
+ gap: '1rem',
384
+ pagination: false,
385
+ breakpoints: {
386
+ 768: {
387
+ perPage: 1,
388
+ },
389
+ 1024: {
390
+ perPage: 2,
391
+ },
392
+ },
393
+ }).mount();
394
+ }
395
+
396
+ `;
397
+ }
398
+ js += ` // Ваш код для каждого экземпляра виджета
399
+ });
400
+
401
+ `;
402
+ if (hasCommonjs && !cleanMode) {
403
+ js += `// Пример работы с EventBus (глобальные события, вне цикла)
404
+ if (typeof EventBus !== 'undefined') {
405
+ EventBus.subscribe('cart:add', function(cart) {
406
+ console.log('Товар добавлен в корзину', cart);
407
+ });
408
+
409
+ // Реакция на изменение настроек в редакторе
410
+ EventBus.subscribe([
411
+ 'widget:input-setting:insales:system:editor',
412
+ 'widget:change-setting:insales:system:editor'
413
+ ], function(data) {
414
+ console.log('Настройка изменена:', data.setting_name, data.value);
415
+ // Здесь можно обновить виджет при изменении настроек
416
+ });
417
+ }
418
+
419
+ `;
420
+ }
421
+ }
422
+ return js;
423
+ }
424
+ /**
425
+ * Генерирует setup.json для block_list_widget_type
426
+ */
427
+ export function generateSetupJson(blockTemplate) {
428
+ // Создаем дефолтный блок с полями из block template
429
+ const createDefaultBlock = () => {
430
+ const block = {};
431
+ blockTemplate.fields.forEach(field => {
432
+ // Генерируем дефолтные значения в зависимости от типа поля
433
+ switch (field.kind) {
434
+ case 'text':
435
+ case 'link':
436
+ block[field.handle] = '';
437
+ break;
438
+ case 'textarea':
439
+ case 'html':
440
+ block[field.handle] = '';
441
+ break;
442
+ case 'account_file':
443
+ case 'image':
444
+ // Для изображений оставляем пустую строку или placeholder ID
445
+ block[field.handle] = '';
446
+ break;
447
+ case 'checkbox':
448
+ block[field.handle] = false;
449
+ break;
450
+ case 'number':
451
+ case 'range':
452
+ block[field.handle] = 0;
453
+ break;
454
+ case 'color':
455
+ block[field.handle] = '#000000';
456
+ break;
457
+ case 'collection':
458
+ case 'product':
459
+ case 'blog':
460
+ case 'article':
461
+ case 'page':
462
+ block[field.handle] = null;
463
+ break;
464
+ default:
465
+ block[field.handle] = '';
466
+ }
467
+ });
468
+ return block;
469
+ };
470
+ // Создаем 3-4 дефолтных блока для примера
471
+ const defaultBlocksCount = Math.min(4, Math.max(2, blockTemplate.fields.length > 5 ? 2 : 3));
472
+ const blocks = Array.from({ length: defaultBlocksCount }, () => createDefaultBlock());
473
+ return {
474
+ blocks
475
+ };
476
+ }
477
+ /**
478
+ * Вспомогательная функция для генерации имени виджета
479
+ */
480
+ function generateWidgetName(description) {
481
+ const words = description.split(' ').slice(0, 4);
482
+ return words.map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
483
+ }
484
+ //# sourceMappingURL=snippet-generator.js.map
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Типы и интерфейсы для генераторов виджетов
3
+ */
4
+ import type { BlockTemplate } from '../types/index.js';
5
+ import type { BaseSetting as Setting } from '../types/settings.js';
6
+ export interface WidgetConfig {
7
+ handle: string;
8
+ category: string;
9
+ type: string;
10
+ description: string;
11
+ blockTemplate?: BlockTemplate;
12
+ libraries: string[];
13
+ commonSettings: Setting[];
14
+ }
15
+ export interface ValidationError {
16
+ filter: string;
17
+ line: number;
18
+ column: number;
19
+ suggestion: string;
20
+ }
21
+ export interface ValidationResult {
22
+ isValid: boolean;
23
+ errors: ValidationError[];
24
+ }
25
+ export interface FixResult {
26
+ fixedCode: string;
27
+ fixes: Array<{
28
+ filter: string;
29
+ line: number;
30
+ suggestion: string;
31
+ }>;
32
+ }
33
+ export interface CollectionsMenuResult {
34
+ liquidCode: string;
35
+ requiredSettings: any[];
36
+ cssPrefix: string;
37
+ dataAttributes: any[];
38
+ }
39
+ export interface SettingsFormResult {
40
+ form: Record<string, any[]>;
41
+ count: number;
42
+ }
43
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Типы и интерфейсы для генераторов виджетов
3
+ */
4
+ export {};
5
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Валидация Liquid кода
3
+ */
4
+ import type { ValidationResult, FixResult } from './types.js';
5
+ /**
6
+ * Валидирует Liquid код на наличие несуществующих фильтров
7
+ */
8
+ export declare function validateLiquidFilters(liquidCode: string): ValidationResult;
9
+ /**
10
+ * Проверяет и исправляет несуществующие фильтры в Liquid коде
11
+ */
12
+ export declare function fixInvalidFilters(liquidCode: string): FixResult;
13
+ //# sourceMappingURL=validation.d.ts.map
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Валидация Liquid кода
3
+ */
4
+ /**
5
+ * Валидирует Liquid код на наличие несуществующих фильтров
6
+ */
7
+ export function validateLiquidFilters(liquidCode) {
8
+ const invalidFiltersList = [
9
+ { name: 't', suggestion: 'Используйте messages или widget_messages для переводов' },
10
+ { name: 'translate', suggestion: 'Используйте messages для переводов' },
11
+ { name: 'i18n', suggestion: 'Используйте messages для переводов' },
12
+ { name: 'l', suggestion: 'Используйте messages для переводов' },
13
+ { name: 'localize', suggestion: 'Используйте messages для переводов' },
14
+ { name: 'tr', suggestion: 'Используйте messages для переводов' }
15
+ ];
16
+ const errors = [];
17
+ const lines = liquidCode.split('\n');
18
+ lines.forEach((line, lineIndex) => {
19
+ invalidFiltersList.forEach(invalidFilter => {
20
+ // Ищем паттерн | filter_name
21
+ const regex = new RegExp(`\\|\\s*${invalidFilter.name}\\b`, 'g');
22
+ let match;
23
+ while ((match = regex.exec(line)) !== null) {
24
+ errors.push({
25
+ filter: invalidFilter.name,
26
+ line: lineIndex + 1,
27
+ column: match.index + 1,
28
+ suggestion: invalidFilter.suggestion
29
+ });
30
+ }
31
+ });
32
+ });
33
+ return {
34
+ isValid: errors.length === 0,
35
+ errors
36
+ };
37
+ }
38
+ /**
39
+ * Проверяет и исправляет несуществующие фильтры в Liquid коде
40
+ */
41
+ export function fixInvalidFilters(liquidCode) {
42
+ const fixes = [];
43
+ let fixedCode = liquidCode;
44
+ // Исправляем фильтр 't' на messages
45
+ const tRegex = /\|\s*t\b/g;
46
+ let match;
47
+ while ((match = tRegex.exec(fixedCode)) !== null) {
48
+ const beforeMatch = fixedCode.substring(0, match.index);
49
+ const afterMatch = fixedCode.substring(match.index + match[0].length);
50
+ // Определяем, что это за контекст
51
+ const contextBefore = beforeMatch.substring(Math.max(0, beforeMatch.length - 50));
52
+ if (contextBefore.includes('widget_messages')) {
53
+ // Если это в контексте widget_messages, оставляем как есть
54
+ continue;
55
+ }
56
+ else {
57
+ // Заменяем на messages
58
+ fixedCode = beforeMatch + '| messages' + afterMatch;
59
+ fixes.push({
60
+ filter: 't',
61
+ line: fixedCode.substring(0, match.index).split('\n').length,
62
+ suggestion: 'Заменен на messages'
63
+ });
64
+ }
65
+ }
66
+ return {
67
+ fixedCode,
68
+ fixes
69
+ };
70
+ }
71
+ //# sourceMappingURL=validation.js.map
@@ -57,7 +57,7 @@ const httpServer = createServer(async (req, res) => {
57
57
  res.end(JSON.stringify({
58
58
  status: 'ok',
59
59
  service: 'InSales Widgets MCP Server',
60
- version: '2.4.11',
60
+ version: '2.4.13',
61
61
  transport: 'SSE',
62
62
  endpoints: {
63
63
  sse: '/sse',
package/dist/server.d.ts CHANGED
@@ -3,6 +3,10 @@ export declare class WidgetServer {
3
3
  constructor();
4
4
  private setupHandlers;
5
5
  private createWidget;
6
+ /**
7
+ * Получить справочник типов настроек виджетов
8
+ */
9
+ private getSettingsTypesReference;
6
10
  /**
7
11
  * Подключить внешний транспорт (например, SSE)
8
12
  */