@n8n/design-system 2.12.0 → 2.13.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.
- package/.turbo/turbo-build.log +8 -8
- package/dist/design-system.css +1 -1
- package/dist/n8n-design-system.es.js +686 -548
- package/dist/n8n-design-system.umd.js +37 -37
- package/package.json +1 -1
- package/scripts/migrate-button-v2.mjs +0 -418
package/package.json
CHANGED
|
@@ -1,418 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Button V2 Migration Codemod
|
|
5
|
-
*
|
|
6
|
-
* This script migrates N8nButton components from the legacy V1 API to the V2 API.
|
|
7
|
-
*
|
|
8
|
-
* Transformations:
|
|
9
|
-
* - type="primary" → variant="solid"
|
|
10
|
-
* - type="secondary" → variant="subtle"
|
|
11
|
-
* - type="tertiary" → variant="subtle"
|
|
12
|
-
* - type="danger" → variant="destructive"
|
|
13
|
-
* - type="success" → variant="solid" class="n8n-button--success"
|
|
14
|
-
* - type="warning" → variant="solid" class="n8n-button--warning"
|
|
15
|
-
* - type="highlight" → variant="ghost" class="n8n-button--highlight"
|
|
16
|
-
* - type="highlightFill" → variant="subtle" class="n8n-button--highlightFill"
|
|
17
|
-
* - outline prop → variant="outline"
|
|
18
|
-
* - text prop → variant="ghost"
|
|
19
|
-
* - size="xmini"|"mini" → size="xsmall"
|
|
20
|
-
* - square → iconOnly
|
|
21
|
-
* - nativeType → type attribute
|
|
22
|
-
* - block → style="width: 100%"
|
|
23
|
-
* - element="a" → (removed, href determines element)
|
|
24
|
-
* Usage:
|
|
25
|
-
* node migrate-button-v2.mjs [--dry-run]
|
|
26
|
-
*/
|
|
27
|
-
|
|
28
|
-
import { readFileSync, writeFileSync, readdirSync, statSync } from 'fs';
|
|
29
|
-
import { join, relative } from 'path';
|
|
30
|
-
import { fileURLToPath } from 'url';
|
|
31
|
-
import { dirname } from 'path';
|
|
32
|
-
|
|
33
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
34
|
-
const __dirname = dirname(__filename);
|
|
35
|
-
|
|
36
|
-
// Configuration
|
|
37
|
-
const FRONTEND_ROOT = join(__dirname, '../../..');
|
|
38
|
-
const DRY_RUN = process.argv.includes('--dry-run');
|
|
39
|
-
|
|
40
|
-
// Mapping from legacy type to new variant
|
|
41
|
-
const TYPE_TO_VARIANT = {
|
|
42
|
-
primary: 'solid',
|
|
43
|
-
secondary: 'subtle',
|
|
44
|
-
tertiary: 'subtle',
|
|
45
|
-
danger: 'destructive',
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
// Legacy types that need override classes
|
|
49
|
-
const LEGACY_TYPES_WITH_CLASSES = {
|
|
50
|
-
success: { variant: 'solid', className: 'n8n-button--success' },
|
|
51
|
-
warning: { variant: 'solid', className: 'n8n-button--warning' },
|
|
52
|
-
highlight: { variant: 'ghost', className: 'n8n-button--highlight' },
|
|
53
|
-
highlightFill: { variant: 'subtle', className: 'n8n-button--highlightFill' },
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
// Size normalization
|
|
57
|
-
const SIZE_MAP = {
|
|
58
|
-
xmini: 'xsmall',
|
|
59
|
-
mini: 'xsmall',
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
// Stats
|
|
63
|
-
const stats = {
|
|
64
|
-
filesScanned: 0,
|
|
65
|
-
filesModified: 0,
|
|
66
|
-
transformations: {
|
|
67
|
-
typeToVariant: 0,
|
|
68
|
-
legacyTypeWithClass: 0,
|
|
69
|
-
outlineToVariant: 0,
|
|
70
|
-
textToVariant: 0,
|
|
71
|
-
sizeNormalized: 0,
|
|
72
|
-
squareToIconOnly: 0,
|
|
73
|
-
nativeTypeToType: 0,
|
|
74
|
-
blockToStyle: 0,
|
|
75
|
-
elementRemoved: 0,
|
|
76
|
-
},
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Find all .vue files recursively
|
|
81
|
-
*/
|
|
82
|
-
function findVueFiles(dir, files = []) {
|
|
83
|
-
const entries = readdirSync(dir);
|
|
84
|
-
|
|
85
|
-
for (const entry of entries) {
|
|
86
|
-
const fullPath = join(dir, entry);
|
|
87
|
-
|
|
88
|
-
// Skip node_modules and hidden directories
|
|
89
|
-
if (entry === 'node_modules' || entry.startsWith('.')) continue;
|
|
90
|
-
|
|
91
|
-
const stat = statSync(fullPath);
|
|
92
|
-
if (stat.isDirectory()) {
|
|
93
|
-
findVueFiles(fullPath, files);
|
|
94
|
-
} else if (entry.endsWith('.vue')) {
|
|
95
|
-
files.push(fullPath);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
return files;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Transform a single N8nButton tag
|
|
104
|
-
*/
|
|
105
|
-
function transformButtonTag(fullMatch, tagContent, selfClosing, content, closingTag) {
|
|
106
|
-
let modified = false;
|
|
107
|
-
const changes = [];
|
|
108
|
-
|
|
109
|
-
// Track what we need to add
|
|
110
|
-
let newVariant = null;
|
|
111
|
-
let addClass = null;
|
|
112
|
-
let addStyle = null;
|
|
113
|
-
|
|
114
|
-
// Parse current attributes (handles both static and v-bind shorthand :prop)
|
|
115
|
-
const hasType = /\btype=["']([^"']+)["']/.exec(tagContent);
|
|
116
|
-
const hasVariant = /\bvariant=["']/.test(tagContent);
|
|
117
|
-
const hasOutline = /\b:?outline(?:=["']true["'])?(?=\s|\/?>|\s)/.test(tagContent);
|
|
118
|
-
const hasText = /\b:?text(?:=["']true["'])?(?=\s|\/?>|\s)/.test(tagContent);
|
|
119
|
-
const hasSize = /\b:?size=["']([^"']+)["']/.exec(tagContent);
|
|
120
|
-
const hasSquare = /\b:?square(?:=["']true["'])?(?=\s|\/?>|\s)/.test(tagContent);
|
|
121
|
-
const hasNativeType = /\b:?nativeType=["']([^"']+)["']/.exec(tagContent);
|
|
122
|
-
const hasBlock = /\b:?block(?:=["']true["'])?(?=\s|\/?>|\s)/.test(tagContent);
|
|
123
|
-
const hasElement = /\b:?element=["']([^"']+)["']/.exec(tagContent);
|
|
124
|
-
const hasClass = /\bclass=["']([^"']+)["']/.exec(tagContent);
|
|
125
|
-
const hasStyle = /\bstyle=["']([^"']+)["']/.exec(tagContent);
|
|
126
|
-
|
|
127
|
-
// Skip if already using variant (already migrated)
|
|
128
|
-
if (hasVariant) {
|
|
129
|
-
return fullMatch;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// 1. Handle outline prop → variant="outline"
|
|
133
|
-
if (hasOutline && !hasText) {
|
|
134
|
-
newVariant = 'outline';
|
|
135
|
-
tagContent = tagContent.replace(/\s*\b:?outline(?:=["']true["'])?(?=\s|\/?>)/, '');
|
|
136
|
-
// Also remove type if present since outline takes precedence
|
|
137
|
-
if (hasType) {
|
|
138
|
-
tagContent = tagContent.replace(/\s*\btype=["'][^"']+["']/, '');
|
|
139
|
-
}
|
|
140
|
-
changes.push('outline → variant="outline"');
|
|
141
|
-
stats.transformations.outlineToVariant++;
|
|
142
|
-
modified = true;
|
|
143
|
-
}
|
|
144
|
-
// 2. Handle text prop → variant="ghost"
|
|
145
|
-
else if (hasText) {
|
|
146
|
-
newVariant = 'ghost';
|
|
147
|
-
tagContent = tagContent.replace(/\s*\b:?text(?:=["']true["'])?(?=\s|\/?>)/, '');
|
|
148
|
-
// Also remove type and outline if present
|
|
149
|
-
if (hasType) {
|
|
150
|
-
tagContent = tagContent.replace(/\s*\btype=["'][^"']+["']/, '');
|
|
151
|
-
}
|
|
152
|
-
if (hasOutline) {
|
|
153
|
-
tagContent = tagContent.replace(/\s*\b:?outline(?:=["']true["'])?(?=\s|\/?>)/, '');
|
|
154
|
-
}
|
|
155
|
-
changes.push('text → variant="ghost"');
|
|
156
|
-
stats.transformations.textToVariant++;
|
|
157
|
-
modified = true;
|
|
158
|
-
}
|
|
159
|
-
// 3. Handle type prop
|
|
160
|
-
else if (hasType) {
|
|
161
|
-
const typeValue = hasType[1];
|
|
162
|
-
|
|
163
|
-
if (TYPE_TO_VARIANT[typeValue]) {
|
|
164
|
-
// Direct mapping
|
|
165
|
-
newVariant = TYPE_TO_VARIANT[typeValue];
|
|
166
|
-
tagContent = tagContent.replace(/\s*\btype=["'][^"']+["']/, '');
|
|
167
|
-
changes.push(`type="${typeValue}" → variant="${newVariant}"`);
|
|
168
|
-
stats.transformations.typeToVariant++;
|
|
169
|
-
modified = true;
|
|
170
|
-
} else if (LEGACY_TYPES_WITH_CLASSES[typeValue]) {
|
|
171
|
-
// Legacy type with class override
|
|
172
|
-
const mapping = LEGACY_TYPES_WITH_CLASSES[typeValue];
|
|
173
|
-
newVariant = mapping.variant;
|
|
174
|
-
addClass = mapping.className;
|
|
175
|
-
tagContent = tagContent.replace(/\s*\btype=["'][^"']+["']/, '');
|
|
176
|
-
changes.push(`type="${typeValue}" → variant="${newVariant}" + class="${addClass}"`);
|
|
177
|
-
stats.transformations.legacyTypeWithClass++;
|
|
178
|
-
modified = true;
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// 4. Handle size normalization
|
|
183
|
-
if (hasSize && SIZE_MAP[hasSize[1]]) {
|
|
184
|
-
const oldSize = hasSize[1];
|
|
185
|
-
const newSize = SIZE_MAP[oldSize];
|
|
186
|
-
tagContent = tagContent.replace(/\b:?size=["'][^"']+["']/, `size="${newSize}"`);
|
|
187
|
-
changes.push(`size="${oldSize}" → size="${newSize}"`);
|
|
188
|
-
stats.transformations.sizeNormalized++;
|
|
189
|
-
modified = true;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// 5. Handle square → iconOnly
|
|
193
|
-
if (hasSquare) {
|
|
194
|
-
tagContent = tagContent.replace(/\s*\b:?square(?:=["']true["'])?(?=\s|\/?>)/, '');
|
|
195
|
-
// Add iconOnly attribute
|
|
196
|
-
tagContent = tagContent.replace(
|
|
197
|
-
/(N8nButton|n8n-button|N8nIconButton|n8n-icon-button|IconButton)/,
|
|
198
|
-
'$1 iconOnly',
|
|
199
|
-
);
|
|
200
|
-
changes.push('square → iconOnly');
|
|
201
|
-
stats.transformations.squareToIconOnly++;
|
|
202
|
-
modified = true;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// 6. Handle nativeType → type
|
|
206
|
-
if (hasNativeType) {
|
|
207
|
-
const nativeTypeValue = hasNativeType[1];
|
|
208
|
-
tagContent = tagContent.replace(/\s*\b:?nativeType=["'][^"']+["']/, '');
|
|
209
|
-
tagContent = tagContent.replace(
|
|
210
|
-
/(N8nButton|n8n-button|N8nIconButton|n8n-icon-button|IconButton)/,
|
|
211
|
-
`$1 type="${nativeTypeValue}"`,
|
|
212
|
-
);
|
|
213
|
-
changes.push(`nativeType="${nativeTypeValue}" → type="${nativeTypeValue}"`);
|
|
214
|
-
stats.transformations.nativeTypeToType++;
|
|
215
|
-
modified = true;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// 7. Handle block → style="width: 100%"
|
|
219
|
-
if (hasBlock) {
|
|
220
|
-
tagContent = tagContent.replace(/\s*\b:?block(?:=["']true["'])?(?=\s|\/?>)/, '');
|
|
221
|
-
addStyle = 'width: 100%';
|
|
222
|
-
changes.push('block → style="width: 100%"');
|
|
223
|
-
stats.transformations.blockToStyle++;
|
|
224
|
-
modified = true;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// 8. Handle element="a" → remove (href determines element)
|
|
228
|
-
if (hasElement) {
|
|
229
|
-
tagContent = tagContent.replace(/\s*\b:?element=["'][^"']+["']/, '');
|
|
230
|
-
changes.push('element removed (href determines element)');
|
|
231
|
-
stats.transformations.elementRemoved++;
|
|
232
|
-
modified = true;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
if (!modified) {
|
|
236
|
-
return fullMatch;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
// Now apply the collected changes
|
|
240
|
-
const BUTTON_TAG_PATTERN = /(N8nButton|n8n-button|N8nIconButton|n8n-icon-button|IconButton)/;
|
|
241
|
-
|
|
242
|
-
// Add variant attribute
|
|
243
|
-
if (newVariant) {
|
|
244
|
-
tagContent = tagContent.replace(BUTTON_TAG_PATTERN, `$1 variant="${newVariant}"`);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// Merge class attribute
|
|
248
|
-
if (addClass) {
|
|
249
|
-
if (hasClass) {
|
|
250
|
-
// Merge with existing class
|
|
251
|
-
tagContent = tagContent.replace(/\bclass=["']([^"']+)["']/, `class="$1 ${addClass}"`);
|
|
252
|
-
} else {
|
|
253
|
-
// Add new class attribute
|
|
254
|
-
tagContent = tagContent.replace(BUTTON_TAG_PATTERN, `$1 class="${addClass}"`);
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
// Merge style attribute
|
|
259
|
-
if (addStyle) {
|
|
260
|
-
if (hasStyle) {
|
|
261
|
-
// Merge with existing style
|
|
262
|
-
const existingStyle = hasStyle[1].trim();
|
|
263
|
-
const separator = existingStyle.endsWith(';') ? ' ' : '; ';
|
|
264
|
-
tagContent = tagContent.replace(
|
|
265
|
-
/\bstyle=["']([^"']+)["']/,
|
|
266
|
-
`style="$1${separator}${addStyle}"`,
|
|
267
|
-
);
|
|
268
|
-
} else {
|
|
269
|
-
// Add new style attribute
|
|
270
|
-
tagContent = tagContent.replace(BUTTON_TAG_PATTERN, `$1 style="${addStyle}"`);
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
// Build the new tag
|
|
275
|
-
let result;
|
|
276
|
-
if (selfClosing) {
|
|
277
|
-
result = `<${tagContent.trim()} />`;
|
|
278
|
-
} else {
|
|
279
|
-
result = `<${tagContent.trim()}>${content || ''}${closingTag}`;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
// Log the transformation
|
|
283
|
-
console.log(` ${changes.join(', ')}`);
|
|
284
|
-
|
|
285
|
-
return result;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
/**
|
|
289
|
-
* Transform all N8nButton usages in a file
|
|
290
|
-
*/
|
|
291
|
-
function transformFile(filePath) {
|
|
292
|
-
const content = readFileSync(filePath, 'utf-8');
|
|
293
|
-
stats.filesScanned++;
|
|
294
|
-
|
|
295
|
-
// Find template section - use greedy match ([\s\S]*) to get the outermost </template>
|
|
296
|
-
// Vue SFC files may have nested <template> tags for slots, so we need the last closing tag
|
|
297
|
-
const templateMatch = /<template[^>]*>([\s\S]*)<\/template>/i.exec(content);
|
|
298
|
-
if (!templateMatch) {
|
|
299
|
-
return { modified: false };
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
const templateStart = templateMatch.index;
|
|
303
|
-
const templateContent = templateMatch[1];
|
|
304
|
-
|
|
305
|
-
// Match button components (both self-closing and with content)
|
|
306
|
-
// Includes: N8nButton, n8n-button, N8nIconButton, n8n-icon-button, IconButton
|
|
307
|
-
const BUTTON_TAGS = 'N8nButton|n8n-button|N8nIconButton|n8n-icon-button|IconButton';
|
|
308
|
-
|
|
309
|
-
let newTemplateContent = templateContent;
|
|
310
|
-
let hasChanges = false;
|
|
311
|
-
|
|
312
|
-
// Process self-closing buttons first
|
|
313
|
-
// Pattern matches: <TAG + attributes (no unquoted >) + />
|
|
314
|
-
// This avoids matching non-self-closing tags like <N8nButton ...>content</N8nButton>
|
|
315
|
-
newTemplateContent = newTemplateContent.replace(
|
|
316
|
-
new RegExp(`<(${BUTTON_TAGS})((?:[^>"]|"[^"]*")*)\\s*\\/>`, 'gi'),
|
|
317
|
-
(match, tagName, attrs) => {
|
|
318
|
-
// Skip if no attributes
|
|
319
|
-
if (!attrs || !attrs.trim()) return match;
|
|
320
|
-
const result = transformButtonTag(match, `${tagName} ${attrs.trim()}`, true, null, null);
|
|
321
|
-
if (result !== match) hasChanges = true;
|
|
322
|
-
return result;
|
|
323
|
-
},
|
|
324
|
-
);
|
|
325
|
-
|
|
326
|
-
// Process buttons with content (non-self-closing)
|
|
327
|
-
// Pattern matches: <TAG + attrs (not ending with /) + > + content + </TAG>
|
|
328
|
-
// The attrs pattern uses negative lookahead to ensure / is not followed by >
|
|
329
|
-
newTemplateContent = newTemplateContent.replace(
|
|
330
|
-
new RegExp(
|
|
331
|
-
`<(${BUTTON_TAGS})((?:[^>"/]|"[^"]*"|/(?!>))*)>([\\s\\S]*?)<\\/(${BUTTON_TAGS})>`,
|
|
332
|
-
'gi',
|
|
333
|
-
),
|
|
334
|
-
(match, tagName, attrs, content, closeTag) => {
|
|
335
|
-
const result = transformButtonTag(
|
|
336
|
-
match,
|
|
337
|
-
`${tagName}${attrs || ''}`,
|
|
338
|
-
false,
|
|
339
|
-
content,
|
|
340
|
-
`</${closeTag}>`,
|
|
341
|
-
);
|
|
342
|
-
if (result !== match) hasChanges = true;
|
|
343
|
-
return result;
|
|
344
|
-
},
|
|
345
|
-
);
|
|
346
|
-
|
|
347
|
-
if (!hasChanges) {
|
|
348
|
-
return { modified: false };
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
// Reconstruct the file
|
|
352
|
-
const newContent =
|
|
353
|
-
content.slice(0, templateStart) +
|
|
354
|
-
'<template>' +
|
|
355
|
-
newTemplateContent +
|
|
356
|
-
'</template>' +
|
|
357
|
-
content.slice(templateStart + templateMatch[0].length);
|
|
358
|
-
|
|
359
|
-
return { modified: true, content: newContent };
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
/**
|
|
363
|
-
* Main function
|
|
364
|
-
*/
|
|
365
|
-
async function main() {
|
|
366
|
-
console.log('Button V2 Migration Codemod');
|
|
367
|
-
console.log('===========================');
|
|
368
|
-
console.log(`Mode: ${DRY_RUN ? 'DRY RUN (no files will be modified)' : 'LIVE'}`);
|
|
369
|
-
console.log(`Scanning: ${FRONTEND_ROOT}`);
|
|
370
|
-
console.log('');
|
|
371
|
-
|
|
372
|
-
const vueFiles = findVueFiles(FRONTEND_ROOT);
|
|
373
|
-
console.log(`Found ${vueFiles.length} Vue files\n`);
|
|
374
|
-
|
|
375
|
-
for (const filePath of vueFiles) {
|
|
376
|
-
const relativePath = relative(FRONTEND_ROOT, filePath);
|
|
377
|
-
|
|
378
|
-
try {
|
|
379
|
-
const result = transformFile(filePath);
|
|
380
|
-
|
|
381
|
-
if (result.modified) {
|
|
382
|
-
console.log(`\nModified: ${relativePath}`);
|
|
383
|
-
stats.filesModified++;
|
|
384
|
-
|
|
385
|
-
if (!DRY_RUN) {
|
|
386
|
-
writeFileSync(filePath, result.content, 'utf-8');
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
} catch (error) {
|
|
390
|
-
console.error(`Error processing ${relativePath}:`, error.message);
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
// Print summary
|
|
395
|
-
console.log('\n===========================');
|
|
396
|
-
console.log('Summary');
|
|
397
|
-
console.log('===========================');
|
|
398
|
-
console.log(`Files scanned: ${stats.filesScanned}`);
|
|
399
|
-
console.log(`Files modified: ${stats.filesModified}`);
|
|
400
|
-
console.log('');
|
|
401
|
-
console.log('Transformations:');
|
|
402
|
-
console.log(` type → variant: ${stats.transformations.typeToVariant}`);
|
|
403
|
-
console.log(` legacy type + class: ${stats.transformations.legacyTypeWithClass}`);
|
|
404
|
-
console.log(` outline → variant: ${stats.transformations.outlineToVariant}`);
|
|
405
|
-
console.log(` text → variant: ${stats.transformations.textToVariant}`);
|
|
406
|
-
console.log(` size normalized: ${stats.transformations.sizeNormalized}`);
|
|
407
|
-
console.log(` square → iconOnly: ${stats.transformations.squareToIconOnly}`);
|
|
408
|
-
console.log(` nativeType → type: ${stats.transformations.nativeTypeToType}`);
|
|
409
|
-
console.log(` block → style: ${stats.transformations.blockToStyle}`);
|
|
410
|
-
console.log(` element removed: ${stats.transformations.elementRemoved}`);
|
|
411
|
-
|
|
412
|
-
if (DRY_RUN) {
|
|
413
|
-
console.log('\nDry run complete. No files were modified.');
|
|
414
|
-
console.log('Run without --dry-run to apply changes.');
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
main().catch(console.error);
|