opentwig 1.1.0 → 1.1.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.
Files changed (56) hide show
  1. package/AGENTS.md +49 -200
  2. package/package.json +2 -3
  3. package/src/live-ui/editor.js +1 -0
  4. package/src/live-ui/index.html +94 -0
  5. package/src/live-ui/sidebar.js +2 -0
  6. package/src/live-ui/styles.css +578 -0
  7. package/src/utils/favicon.js +20 -0
  8. package/src/utils/loadConfig.js +3 -1
  9. package/src/utils/setupWatcher.js +2 -2
  10. package/src/utils/startLiveServer.js +11 -8
  11. package/theme/default/index.js +2 -0
  12. package/vitest.config.js +19 -10
  13. package/website/README.md +42 -0
  14. package/website/components.json +16 -0
  15. package/website/eslint.config.js +36 -0
  16. package/website/package-lock.json +4136 -0
  17. package/website/package.json +41 -0
  18. package/website/shadcn-svelte.md +118 -0
  19. package/website/src/app.d.ts +13 -0
  20. package/website/src/app.html +11 -0
  21. package/website/src/lib/assets/favicon.svg +4 -0
  22. package/website/src/lib/components/ui/badge/badge.svelte +50 -0
  23. package/website/src/lib/components/ui/badge/index.ts +2 -0
  24. package/website/src/lib/components/ui/button/button.svelte +82 -0
  25. package/website/src/lib/components/ui/button/index.ts +17 -0
  26. package/website/src/lib/components/ui/card/card-action.svelte +20 -0
  27. package/website/src/lib/components/ui/card/card-content.svelte +15 -0
  28. package/website/src/lib/components/ui/card/card-description.svelte +20 -0
  29. package/website/src/lib/components/ui/card/card-footer.svelte +20 -0
  30. package/website/src/lib/components/ui/card/card-header.svelte +23 -0
  31. package/website/src/lib/components/ui/card/card-title.svelte +20 -0
  32. package/website/src/lib/components/ui/card/card.svelte +23 -0
  33. package/website/src/lib/components/ui/card/index.ts +25 -0
  34. package/website/src/lib/components/ui/separator/index.ts +7 -0
  35. package/website/src/lib/components/ui/separator/separator.svelte +21 -0
  36. package/website/src/lib/components/ui/tooltip/index.ts +19 -0
  37. package/website/src/lib/components/ui/tooltip/tooltip-content.svelte +52 -0
  38. package/website/src/lib/components/ui/tooltip/tooltip-portal.svelte +7 -0
  39. package/website/src/lib/components/ui/tooltip/tooltip-provider.svelte +7 -0
  40. package/website/src/lib/components/ui/tooltip/tooltip-trigger.svelte +7 -0
  41. package/website/src/lib/components/ui/tooltip/tooltip.svelte +7 -0
  42. package/website/src/lib/index.ts +1 -0
  43. package/website/src/lib/utils.ts +13 -0
  44. package/website/src/routes/+layout.svelte +23 -0
  45. package/website/src/routes/+page.server.ts +82 -0
  46. package/website/src/routes/+page.svelte +892 -0
  47. package/website/src/routes/layout.css +199 -0
  48. package/website/static/live-editor.png +0 -0
  49. package/website/static/robots.txt +3 -0
  50. package/website/static/theme-colorful.png +0 -0
  51. package/website/static/theme-dark.png +0 -0
  52. package/website/static/theme-default.png +0 -0
  53. package/website/static/theme-minimal.png +0 -0
  54. package/website/svelte.config.js +31 -0
  55. package/website/vite.config.ts +5 -0
  56. package/test-og.js +0 -40
@@ -0,0 +1,578 @@
1
+ * {
2
+ margin: 0;
3
+ padding: 0;
4
+ box-sizing: border-box;
5
+ }
6
+
7
+ body {
8
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
9
+ background: #f5f5f5;
10
+ color: #333;
11
+ overflow: hidden;
12
+ }
13
+
14
+ .app-container {
15
+ display: flex;
16
+ flex-direction: column;
17
+ height: 100vh;
18
+ overflow: hidden;
19
+ }
20
+
21
+ .header {
22
+ display: flex;
23
+ justify-content: space-between;
24
+ align-items: center;
25
+ padding: 12px 24px;
26
+ background: #fff;
27
+ border-bottom: 1px solid #e5e5e5;
28
+ }
29
+
30
+ .app-header {
31
+ display: flex;
32
+ justify-content: space-between;
33
+ align-items: center;
34
+ padding: 12px 24px;
35
+ background: #fff;
36
+ border-bottom: 1px solid #e5e5e5;
37
+ height: 60px;
38
+ flex-shrink: 0;
39
+ }
40
+
41
+ .header-left {
42
+ display: flex;
43
+ align-items: center;
44
+ gap: 16px;
45
+ }
46
+
47
+ .app-header h1 {
48
+ font-size: 18px;
49
+ font-weight: 600;
50
+ color: #2c3e50;
51
+ }
52
+
53
+ .status-indicator {
54
+ display: flex;
55
+ align-items: center;
56
+ gap: 6px;
57
+ font-size: 13px;
58
+ color: #666;
59
+ }
60
+
61
+ .status-dot {
62
+ width: 8px;
63
+ height: 8px;
64
+ border-radius: 50%;
65
+ background: #ccc;
66
+ animation: pulse 2s infinite;
67
+ }
68
+
69
+ .status-dot.connected {
70
+ background: #52c41a;
71
+ }
72
+
73
+ .status-dot.disconnected {
74
+ background: #f5222d;
75
+ }
76
+
77
+ @keyframes pulse {
78
+ 0%, 100% {
79
+ opacity: 1;
80
+ }
81
+ 50% {
82
+ opacity: 0.5;
83
+ }
84
+ }
85
+
86
+ .header-right {
87
+ display: flex;
88
+ gap: 8px;
89
+ }
90
+
91
+ .btn {
92
+ display: inline-flex;
93
+ align-items: center;
94
+ gap: 6px;
95
+ padding: 8px 16px;
96
+ border: none;
97
+ border-radius: 6px;
98
+ font-size: 14px;
99
+ font-weight: 500;
100
+ cursor: pointer;
101
+ transition: all 0.2s;
102
+ }
103
+
104
+ .btn-primary {
105
+ background: #2c3e50;
106
+ color: #fff;
107
+ }
108
+
109
+ .btn-primary:hover {
110
+ background: #1a252f;
111
+ transform: translateY(-1px);
112
+ }
113
+
114
+ .btn-secondary {
115
+ background: #fff;
116
+ color: #2c3e50;
117
+ border: 1px solid #d9d9d9;
118
+ }
119
+
120
+ .btn-secondary:hover {
121
+ background: #f5f5f5;
122
+ border-color: #40a9ff;
123
+ }
124
+
125
+ .main-content {
126
+ display: flex;
127
+ flex: 1;
128
+ overflow: hidden;
129
+ }
130
+
131
+ .preview-container {
132
+ flex: 1;
133
+ display: flex;
134
+ flex-direction: column;
135
+ overflow: hidden;
136
+ }
137
+
138
+ .preview-toolbar {
139
+ display: flex;
140
+ justify-content: space-between;
141
+ align-items: center;
142
+ padding: 8px 16px;
143
+ background: #fff;
144
+ border-bottom: 1px solid #e5e5e5;
145
+ }
146
+
147
+ .preview-title {
148
+ font-size: 13px;
149
+ font-weight: 500;
150
+ color: #666;
151
+ }
152
+
153
+ .refresh-btn {
154
+ display: inline-flex;
155
+ align-items: center;
156
+ justify-content: center;
157
+ padding: 6px;
158
+ border: none;
159
+ background: #f5f5f5;
160
+ border-radius: 4px;
161
+ cursor: pointer;
162
+ transition: all 0.2s;
163
+ }
164
+
165
+ .refresh-btn:hover {
166
+ background: #e8e8e8;
167
+ }
168
+
169
+ .refresh-btn svg {
170
+ stroke: #666;
171
+ }
172
+
173
+ .preview-frame {
174
+ flex: 1;
175
+ background: #f0f2f5;
176
+ overflow: hidden;
177
+ }
178
+
179
+ #previewFrame {
180
+ width: 100%;
181
+ height: 100%;
182
+ border: none;
183
+ background: #fff;
184
+ }
185
+
186
+ .sidebar {
187
+ width: 400px;
188
+ display: flex;
189
+ flex-direction: column;
190
+ background: #fff;
191
+ border-left: 1px solid #e5e5e5;
192
+ overflow: hidden;
193
+ transition: width 0.3s ease;
194
+ }
195
+
196
+ .sidebar.collapsed {
197
+ width: 0;
198
+ }
199
+
200
+ .sidebar-header {
201
+ display: flex;
202
+ justify-content: space-between;
203
+ align-items: center;
204
+ padding: 16px;
205
+ border-bottom: 1px solid #e5e5e5;
206
+ background: #fafafa;
207
+ }
208
+
209
+ .sidebar-header h2 {
210
+ font-size: 14px;
211
+ font-weight: 600;
212
+ color: #2c3e50;
213
+ }
214
+
215
+ .toggle-sidebar {
216
+ display: inline-flex;
217
+ align-items: center;
218
+ justify-content: center;
219
+ padding: 4px;
220
+ border: none;
221
+ background: transparent;
222
+ cursor: pointer;
223
+ border-radius: 4px;
224
+ transition: all 0.2s;
225
+ }
226
+
227
+ .toggle-sidebar:hover {
228
+ background: #e8e8e8;
229
+ }
230
+
231
+ .toggle-sidebar svg {
232
+ stroke: #666;
233
+ }
234
+
235
+ .sidebar-content {
236
+ flex: 1;
237
+ overflow-y: auto;
238
+ padding: 16px;
239
+ }
240
+
241
+ .config-section {
242
+ display: flex;
243
+ flex-direction: column;
244
+ gap: 20px;
245
+ }
246
+
247
+ .section-title {
248
+ font-size: 12px;
249
+ font-weight: 600;
250
+ text-transform: uppercase;
251
+ color: #666;
252
+ margin-bottom: 12px;
253
+ letter-spacing: 0.5px;
254
+ }
255
+
256
+ .form-group {
257
+ margin-bottom: 16px;
258
+ }
259
+
260
+ .form-group label {
261
+ display: block;
262
+ font-size: 13px;
263
+ font-weight: 500;
264
+ color: #333;
265
+ margin-bottom: 6px;
266
+ }
267
+
268
+ .form-group input[type="text"],
269
+ .form-group input[type="url"],
270
+ .form-group textarea,
271
+ .form-group select {
272
+ width: 100%;
273
+ padding: 8px 12px;
274
+ border: 1px solid #d9d9d9;
275
+ border-radius: 6px;
276
+ font-size: 13px;
277
+ transition: all 0.2s;
278
+ }
279
+
280
+ .form-group textarea {
281
+ min-height: 80px;
282
+ resize: vertical;
283
+ font-family: inherit;
284
+ }
285
+
286
+ .form-group input:focus,
287
+ .form-group textarea:focus,
288
+ .form-group select:focus {
289
+ outline: none;
290
+ border-color: #40a9ff;
291
+ box-shadow: 0 0 0 2px rgba(64, 169, 255, 0.1);
292
+ }
293
+
294
+ .link-item {
295
+ background: #f5f5f5;
296
+ border: 1px solid #e5e5e5;
297
+ border-radius: 6px;
298
+ padding: 12px;
299
+ margin-bottom: 8px;
300
+ }
301
+
302
+ .link-item-header {
303
+ display: flex;
304
+ justify-content: space-between;
305
+ align-items: center;
306
+ margin-bottom: 8px;
307
+ }
308
+
309
+ .link-item-title {
310
+ font-size: 12px;
311
+ font-weight: 500;
312
+ color: #666;
313
+ }
314
+
315
+ .link-item-actions {
316
+ display: flex;
317
+ gap: 4px;
318
+ }
319
+
320
+ .link-item-actions button {
321
+ display: inline-flex;
322
+ align-items: center;
323
+ justify-content: center;
324
+ padding: 4px;
325
+ border: none;
326
+ background: transparent;
327
+ cursor: pointer;
328
+ border-radius: 4px;
329
+ transition: all 0.2s;
330
+ }
331
+
332
+ .link-item-actions button:hover {
333
+ background: #e8e8e8;
334
+ }
335
+
336
+ .link-item-actions button svg {
337
+ width: 14px;
338
+ height: 14px;
339
+ stroke: #666;
340
+ }
341
+
342
+ .add-link-btn {
343
+ width: 100%;
344
+ padding: 8px;
345
+ border: 1px dashed #d9d9d9;
346
+ border-radius: 6px;
347
+ background: transparent;
348
+ color: #40a9ff;
349
+ font-size: 13px;
350
+ font-weight: 500;
351
+ cursor: pointer;
352
+ transition: all 0.2s;
353
+ }
354
+
355
+ .add-link-btn:hover {
356
+ background: rgba(64, 169, 255, 0.05);
357
+ border-color: #40a9ff;
358
+ }
359
+
360
+ .theme-grid {
361
+ display: grid;
362
+ grid-template-columns: repeat(2, 1fr);
363
+ gap: 8px;
364
+ }
365
+
366
+ .theme-card {
367
+ padding: 12px;
368
+ border: 1px solid #d9d9d9;
369
+ border-radius: 6px;
370
+ cursor: pointer;
371
+ transition: all 0.2s;
372
+ text-align: center;
373
+ }
374
+
375
+ .theme-card:hover {
376
+ border-color: #40a9ff;
377
+ background: rgba(64, 169, 255, 0.05);
378
+ }
379
+
380
+ .theme-card.active {
381
+ border-color: #40a9ff;
382
+ background: rgba(64, 169, 255, 0.1);
383
+ }
384
+
385
+ .theme-card-name {
386
+ font-size: 12px;
387
+ font-weight: 500;
388
+ color: #333;
389
+ }
390
+
391
+ .avatar-upload {
392
+ display: flex;
393
+ align-items: center;
394
+ gap: 12px;
395
+ padding: 12px;
396
+ border: 2px dashed #d9d9d9;
397
+ border-radius: 6px;
398
+ cursor: pointer;
399
+ transition: all 0.2s;
400
+ }
401
+
402
+ .avatar-upload:hover {
403
+ border-color: #40a9ff;
404
+ background: rgba(64, 169, 255, 0.05);
405
+ }
406
+
407
+ .avatar-preview {
408
+ width: 48px;
409
+ height: 48px;
410
+ border-radius: 50%;
411
+ background: #f5f5f5;
412
+ object-fit: cover;
413
+ }
414
+
415
+ .avatar-upload-text {
416
+ flex: 1;
417
+ font-size: 12px;
418
+ color: #666;
419
+ }
420
+
421
+ .sidebar-footer {
422
+ padding: 16px;
423
+ border-top: 1px solid #e5e5e5;
424
+ background: #fafafa;
425
+ }
426
+
427
+ .auto-save-toggle {
428
+ display: flex;
429
+ align-items: center;
430
+ gap: 8px;
431
+ margin-bottom: 8px;
432
+ }
433
+
434
+ .toggle-switch {
435
+ position: relative;
436
+ display: inline-block;
437
+ width: 36px;
438
+ height: 20px;
439
+ }
440
+
441
+ .toggle-switch input {
442
+ opacity: 0;
443
+ width: 0;
444
+ height: 0;
445
+ }
446
+
447
+ .slider {
448
+ position: absolute;
449
+ cursor: pointer;
450
+ top: 0;
451
+ left: 0;
452
+ right: 0;
453
+ bottom: 0;
454
+ background-color: #ccc;
455
+ transition: .4s;
456
+ border-radius: 20px;
457
+ }
458
+
459
+ .slider:before {
460
+ position: absolute;
461
+ content: "";
462
+ height: 14px;
463
+ width: 14px;
464
+ left: 3px;
465
+ bottom: 3px;
466
+ background-color: white;
467
+ transition: .4s;
468
+ border-radius: 50%;
469
+ }
470
+
471
+ input:checked + .slider {
472
+ background-color: #2c3e50;
473
+ }
474
+
475
+ input:checked + .slider:before {
476
+ transform: translateX(16px);
477
+ }
478
+
479
+ .auto-save-toggle span:last-child {
480
+ font-size: 12px;
481
+ color: #666;
482
+ }
483
+
484
+ .last-saved {
485
+ font-size: 11px;
486
+ color: #999;
487
+ }
488
+
489
+ .notification-container {
490
+ position: fixed;
491
+ top: 20px;
492
+ right: 20px;
493
+ z-index: 9999;
494
+ display: flex;
495
+ flex-direction: column;
496
+ gap: 8px;
497
+ }
498
+
499
+ .notification {
500
+ display: flex;
501
+ align-items: center;
502
+ gap: 8px;
503
+ padding: 12px 16px;
504
+ border-radius: 6px;
505
+ background: #fff;
506
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
507
+ font-size: 13px;
508
+ animation: slideIn 0.3s ease-out;
509
+ max-width: 300px;
510
+ }
511
+
512
+ .notification.success {
513
+ border-left: 4px solid #52c41a;
514
+ }
515
+
516
+ .notification.error {
517
+ border-left: 4px solid #f5222d;
518
+ }
519
+
520
+ .notification.info {
521
+ border-left: 4px solid #40a9ff;
522
+ }
523
+
524
+ @keyframes slideIn {
525
+ from {
526
+ transform: translateX(100%);
527
+ opacity: 0;
528
+ }
529
+ to {
530
+ transform: translateX(0);
531
+ opacity: 1;
532
+ }
533
+ }
534
+
535
+ @media (max-width: 1024px) {
536
+ .sidebar {
537
+ position: fixed;
538
+ right: 0;
539
+ top: 60px;
540
+ bottom: 0;
541
+ z-index: 1000;
542
+ box-shadow: -4px 0 12px rgba(0, 0, 0, 0.15);
543
+ }
544
+
545
+ .sidebar.collapsed {
546
+ right: -400px;
547
+ }
548
+
549
+ .preview-toolbar {
550
+ padding: 8px 12px;
551
+ }
552
+ }
553
+
554
+ .loading {
555
+ display: flex;
556
+ align-items: center;
557
+ justify-content: center;
558
+ padding: 40px;
559
+ font-size: 13px;
560
+ color: #666;
561
+ }
562
+
563
+ .loading::after {
564
+ content: "";
565
+ width: 20px;
566
+ height: 20px;
567
+ margin-left: 12px;
568
+ border: 2px solid #e5e5e5;
569
+ border-top-color: #40a9ff;
570
+ border-radius: 50%;
571
+ animation: spin 0.8s linear infinite;
572
+ }
573
+
574
+ @keyframes spin {
575
+ to {
576
+ transform: rotate(360deg);
577
+ }
578
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Leaf favicon SVG for OpenTwig
3
+ * Used across all themes
4
+ */
5
+
6
+ const LEAF_FAVICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#16a34a" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 20A7 7 0 0 1 9.8 6.1C15.5 5 17 4.48 19 2c1 2 2 4.18 2 8 0 5.5-4.78 10-10 10Z"/><path d="M2 21c0-3 1.85-5.36 5.08-6C9.5 14.52 12 13 13 12"/></svg>`;
7
+
8
+ /**
9
+ * Get favicon as data URI for embedding in HTML
10
+ * @returns {string} Data URI for favicon
11
+ */
12
+ function getFaviconDataURI() {
13
+ const base64 = Buffer.from(LEAF_FAVICON_SVG).toString('base64');
14
+ return `data:image/svg+xml;base64,${base64}`;
15
+ }
16
+
17
+ module.exports = {
18
+ LEAF_FAVICON_SVG,
19
+ getFaviconDataURI
20
+ };
@@ -11,7 +11,9 @@ module.exports = function() {
11
11
  process.exit(1);
12
12
  }
13
13
 
14
- const config = require(configPath);
14
+ // Use fs.readFileSync instead of require to avoid caching issues
15
+ const configContent = fs.readFileSync(configPath, 'utf8');
16
+ const config = JSON.parse(configContent);
15
17
 
16
18
  // Apply default values to the loaded configuration
17
19
  return applyDefaults(config);
@@ -12,10 +12,10 @@ const setupWatcher = (configPath, wss, onConfigChange) => {
12
12
  let watcher = null;
13
13
  let isPaused = false;
14
14
 
15
- const handleConfigChange = debounce(async (event, path) => {
15
+ const handleConfigChange = debounce(async (path) => {
16
16
  if (isPaused) return;
17
17
 
18
- console.log(`Config file changed: ${event} ${path}`);
18
+ console.log(`Config file changed: ${path}`);
19
19
 
20
20
  try {
21
21
  if (onConfigChange) {
@@ -50,22 +50,22 @@ const startLiveServer = async (customPort) => {
50
50
  }
51
51
  });
52
52
 
53
- app.post('/api/config', (req, res) => {
53
+ app.post('/api/config', async (req, res) => {
54
54
  try {
55
55
  const newConfig = req.body;
56
56
  currentConfig = applyDefaults(newConfig);
57
57
 
58
58
  fs.writeFileSync(configPath, JSON.stringify(currentConfig, null, 4));
59
59
 
60
- buildPage(currentConfig).then(({ html, css, ogImage, qrImage }) => {
61
- saveFiles(html, css, currentConfig.avatar, ogImage, qrImage);
62
- wss.broadcastConfigUpdate(currentConfig);
63
- }).catch(error => {
64
- console.error('Error building page:', error);
65
- });
60
+ const { html, css, ogImage, qrImage } = await buildPage(currentConfig);
61
+ saveFiles(html, css, currentConfig.avatar, ogImage, qrImage);
66
62
 
67
63
  res.json({ success: true, config: currentConfig });
64
+
65
+ // Broadcast reload after files are saved
66
+ wss.broadcastReload();
68
67
  } catch (error) {
68
+ console.error('Error building page:', error);
69
69
  res.status(500).json({ error: error.message });
70
70
  }
71
71
  });
@@ -201,7 +201,10 @@ const startLiveServer = async (customPort) => {
201
201
 
202
202
  const watcher = setupWatcher(configPath, wss, async (changedPath) => {
203
203
  try {
204
- currentConfig = loadConfig();
204
+ // Read config directly from file to avoid require cache issues
205
+ const configContent = fs.readFileSync(configPath, 'utf8');
206
+ const fileConfig = JSON.parse(configContent);
207
+ currentConfig = applyDefaults(fileConfig);
205
208
  await buildPage(currentConfig);
206
209
  console.log('Page rebuilt from config change');
207
210
  } catch (error) {
@@ -5,6 +5,7 @@ const shareButtonComponent = require('./components/share-button');
5
5
  const qrComponent = require('./components/qr');
6
6
  const dialogComponent = require('./components/dialog');
7
7
  const escapeHTML = require('../../src/utils/escapeHTML');
8
+ const { getFaviconDataURI } = require('../../src/utils/favicon');
8
9
 
9
10
  module.exports = function({title, url, name, content, avatar, links, footerLinks, share}) {
10
11
 
@@ -20,6 +21,7 @@ module.exports = function({title, url, name, content, avatar, links, footerLinks
20
21
  <meta property="og:description" content="${escapeHTML(content)}"/>
21
22
  <meta property="og:url" content="${escapeHTML(url)}"/>
22
23
  <meta property="og:image" content="${escapeHTML(url)}/og-image.jpg"/>
24
+ <link rel="icon" type="image/svg+xml" href="${getFaviconDataURI()}">
23
25
  </head>
24
26
  <body>
25
27
  <div class="app-bg">
package/vitest.config.js CHANGED
@@ -1,11 +1,20 @@
1
- module.exports = {
2
- test: {
3
- environment: 'node',
4
- include: ['**/*.test.js'],
5
- coverage: {
6
- provider: 'v8',
7
- reporter: ['text', 'json'],
8
- exclude: ['node_modules/', 'dist/', '**/*.test.js', 'test-og.js']
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ globals: true,
6
+ environment: 'node',
7
+ include: ['tests/**/*.test.js'],
8
+ coverage: {
9
+ provider: 'v8',
10
+ reporter: ['text', 'json', 'html'],
11
+ exclude: [
12
+ 'node_modules/',
13
+ 'tests/',
14
+ 'website/',
15
+ 'theme/',
16
+ 'dist/'
17
+ ]
18
+ }
9
19
  }
10
- }
11
- };
20
+ });