draply-dev 1.1.1 → 1.2.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/bin/cli.js +344 -6
- package/package.json +1 -1
- package/src/draply-features.js +10 -0
- package/src/overlay.js +16 -3
package/bin/cli.js
CHANGED
|
@@ -63,13 +63,28 @@ const server = http.createServer((req, res) => {
|
|
|
63
63
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
64
64
|
if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; }
|
|
65
65
|
|
|
66
|
-
// ── Draply: Save endpoint
|
|
66
|
+
// ── Draply: Save endpoint — AUTO-APPLY to source files ─────────────────────
|
|
67
67
|
if (req.url === '/draply-save' && req.method === 'POST') {
|
|
68
68
|
let body = '';
|
|
69
69
|
req.on('data', c => body += c);
|
|
70
70
|
req.on('end', () => {
|
|
71
71
|
try {
|
|
72
72
|
const { changes } = JSON.parse(body);
|
|
73
|
+
const projectInfo = detectProject(projectRoot);
|
|
74
|
+
const results = [];
|
|
75
|
+
|
|
76
|
+
for (const ch of (changes || [])) {
|
|
77
|
+
if (!ch.selector || !ch.props) continue;
|
|
78
|
+
const result = autoApplyChange(projectRoot, ch.selector, ch.props, projectInfo);
|
|
79
|
+
results.push(result);
|
|
80
|
+
if (result.applied) {
|
|
81
|
+
console.log(` \x1b[32m✓\x1b[0m Applied: ${result.file} (${result.strategy})`);
|
|
82
|
+
} else {
|
|
83
|
+
console.log(` \x1b[33m⚠\x1b[0m Fallback CSS: ${ch.selector.split('>').pop().trim()}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Also save CSS override as backup
|
|
73
88
|
const lines = [];
|
|
74
89
|
for (const ch of (changes || [])) {
|
|
75
90
|
if (!ch.selector) continue;
|
|
@@ -77,15 +92,15 @@ const server = http.createServer((req, res) => {
|
|
|
77
92
|
.map(([k, v]) => ` ${k}: ${v};`)
|
|
78
93
|
.join('\n');
|
|
79
94
|
const label = ch.selector.split('>').pop().trim();
|
|
80
|
-
lines.push(`/* ${label}
|
|
81
|
-
${ch.selector} {
|
|
82
|
-
${props}
|
|
83
|
-
}`);
|
|
95
|
+
lines.push(`/* ${label} */\n${ch.selector} {\n${props}\n}`);
|
|
84
96
|
}
|
|
85
97
|
const css = '/* draply — ' + new Date().toLocaleString('ru-RU') + ' */\n\n' + lines.join('\n\n') + '\n';
|
|
86
98
|
fs.writeFileSync(overridesPath, css, 'utf8');
|
|
99
|
+
|
|
100
|
+
const applied = results.filter(r => r.applied).length;
|
|
101
|
+
const total = results.length;
|
|
87
102
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
88
|
-
res.end(JSON.stringify({ ok: true }));
|
|
103
|
+
res.end(JSON.stringify({ ok: true, applied, total, results }));
|
|
89
104
|
} catch (e) {
|
|
90
105
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
91
106
|
res.end(JSON.stringify({ ok: false, error: e.message }));
|
|
@@ -300,3 +315,326 @@ function walkDir(dir, extensions, results = []) {
|
|
|
300
315
|
} catch { /* ignore */ }
|
|
301
316
|
return results;
|
|
302
317
|
}
|
|
318
|
+
|
|
319
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
320
|
+
// AUTO-APPLY CHANGES TO SOURCE FILES
|
|
321
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
322
|
+
|
|
323
|
+
function autoApplyChange(root, selector, props, projectInfo) {
|
|
324
|
+
const result = { selector, applied: false, file: null, strategy: null, reason: '' };
|
|
325
|
+
|
|
326
|
+
// 1. Extract class name from selector
|
|
327
|
+
const className = extractClassName(selector);
|
|
328
|
+
if (!className) {
|
|
329
|
+
result.reason = 'No class name in selector';
|
|
330
|
+
return result;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// 2. Try: find existing CSS rule and modify it
|
|
334
|
+
const cssResult = findAndModifyCSSRule(root, className, props);
|
|
335
|
+
if (cssResult.applied) {
|
|
336
|
+
result.applied = true;
|
|
337
|
+
result.file = path.relative(root, cssResult.file);
|
|
338
|
+
result.strategy = 'css-modify';
|
|
339
|
+
return result;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// 3. Try: find JSX component using this class → find its CSS import → add rule there
|
|
343
|
+
const importResult = findCSSImportAndAppend(root, className, props);
|
|
344
|
+
if (importResult.applied) {
|
|
345
|
+
result.applied = true;
|
|
346
|
+
result.file = path.relative(root, importResult.file);
|
|
347
|
+
result.strategy = 'css-append';
|
|
348
|
+
return result;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// 4. Try: modify inline style in JSX
|
|
352
|
+
const inlineResult = modifyInlineStyle(root, className, props);
|
|
353
|
+
if (inlineResult.applied) {
|
|
354
|
+
result.applied = true;
|
|
355
|
+
result.file = path.relative(root, inlineResult.file);
|
|
356
|
+
result.strategy = 'inline-style';
|
|
357
|
+
return result;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// 5. Fallback: find any CSS file in project and append
|
|
361
|
+
const fallbackResult = appendToAnyCSSFile(root, className, props);
|
|
362
|
+
if (fallbackResult.applied) {
|
|
363
|
+
result.applied = true;
|
|
364
|
+
result.file = path.relative(root, fallbackResult.file);
|
|
365
|
+
result.strategy = 'css-fallback';
|
|
366
|
+
return result;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
result.reason = 'Could not find target file for ' + className;
|
|
370
|
+
return result;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Extract the most specific class name from a CSS selector
|
|
374
|
+
function extractClassName(selector) {
|
|
375
|
+
// Selector like: #root > div > section.nexus-hero > h1.title
|
|
376
|
+
const parts = selector.split('>').map(s => s.trim());
|
|
377
|
+
// Try from the end — most specific first
|
|
378
|
+
for (let i = parts.length - 1; i >= 0; i--) {
|
|
379
|
+
const classMatch = parts[i].match(/\.([a-zA-Z][a-zA-Z0-9_-]*)/);
|
|
380
|
+
if (classMatch) return classMatch[1];
|
|
381
|
+
}
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Strategy 1: Find existing CSS rule in .css/.scss files and modify it
|
|
386
|
+
function findAndModifyCSSRule(root, className, props) {
|
|
387
|
+
const cssFiles = walkDir(root, ['.css', '.scss', '.sass', '.less']);
|
|
388
|
+
const ruleRegex = new RegExp(
|
|
389
|
+
`(\\.${escapeRegex(className)}\\s*\\{)([^}]*)(\\})`, 's'
|
|
390
|
+
);
|
|
391
|
+
|
|
392
|
+
for (const file of cssFiles) {
|
|
393
|
+
// Skip node_modules, .draply, etc
|
|
394
|
+
const rel = path.relative(root, file);
|
|
395
|
+
if (rel.includes('node_modules') || rel.includes('.draply')) continue;
|
|
396
|
+
|
|
397
|
+
try {
|
|
398
|
+
let content = fs.readFileSync(file, 'utf8');
|
|
399
|
+
const match = content.match(ruleRegex);
|
|
400
|
+
if (!match) continue;
|
|
401
|
+
|
|
402
|
+
// Found the rule — modify it
|
|
403
|
+
const existingBlock = match[2];
|
|
404
|
+
let newBlock = existingBlock;
|
|
405
|
+
|
|
406
|
+
for (const [prop, val] of Object.entries(props)) {
|
|
407
|
+
const propRegex = new RegExp(`(${escapeRegex(prop)}\\s*:\\s*)([^;]+)(;)`, 'g');
|
|
408
|
+
if (propRegex.test(newBlock)) {
|
|
409
|
+
// Property exists — update value
|
|
410
|
+
newBlock = newBlock.replace(
|
|
411
|
+
new RegExp(`(${escapeRegex(prop)}\\s*:\\s*)([^;]+)(;)`, 'g'),
|
|
412
|
+
`$1${val}$3`
|
|
413
|
+
);
|
|
414
|
+
} else {
|
|
415
|
+
// Property doesn't exist — add it
|
|
416
|
+
newBlock = newBlock.trimEnd() + `\n ${prop}: ${val};\n`;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
content = content.replace(ruleRegex, `$1${newBlock}$3`);
|
|
421
|
+
fs.writeFileSync(file, content, 'utf8');
|
|
422
|
+
|
|
423
|
+
return { applied: true, file };
|
|
424
|
+
} catch { /* skip */ }
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
return { applied: false };
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Strategy 2: Find the JSX component using className, find its CSS import, append rule
|
|
431
|
+
function findCSSImportAndAppend(root, className, props) {
|
|
432
|
+
const jsxFiles = walkDir(root, ['.jsx', '.tsx', '.js', '.ts']);
|
|
433
|
+
|
|
434
|
+
for (const jsxFile of jsxFiles) {
|
|
435
|
+
const rel = path.relative(root, jsxFile);
|
|
436
|
+
if (rel.includes('node_modules')) continue;
|
|
437
|
+
|
|
438
|
+
try {
|
|
439
|
+
const content = fs.readFileSync(jsxFile, 'utf8');
|
|
440
|
+
|
|
441
|
+
// Check if this component uses the className
|
|
442
|
+
if (!content.includes(className)) continue;
|
|
443
|
+
|
|
444
|
+
// Find CSS imports in this file
|
|
445
|
+
// Matches: import './styles.css' or import styles from './styles.module.css' or require('./styles.css')
|
|
446
|
+
const importRegex = /(?:import\s+(?:\w+\s+from\s+)?['"]([^'"]+\.(?:css|scss|sass|less))['"]|require\(['"]([^'"]+\.(?:css|scss|sass|less))['"]\))/g;
|
|
447
|
+
let importMatch;
|
|
448
|
+
const cssImports = [];
|
|
449
|
+
|
|
450
|
+
while ((importMatch = importRegex.exec(content)) !== null) {
|
|
451
|
+
const importPath = importMatch[1] || importMatch[2];
|
|
452
|
+
const fullCSSPath = path.resolve(path.dirname(jsxFile), importPath);
|
|
453
|
+
if (fs.existsSync(fullCSSPath)) {
|
|
454
|
+
cssImports.push(fullCSSPath);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (cssImports.length === 0) continue;
|
|
459
|
+
|
|
460
|
+
// Append rule to the first CSS import
|
|
461
|
+
const targetCSS = cssImports[0];
|
|
462
|
+
const newRule = `\n/* Draply: .${className} */\n.${className} {\n${formatProps(props)}\n}\n`;
|
|
463
|
+
|
|
464
|
+
let cssContent = fs.readFileSync(targetCSS, 'utf8');
|
|
465
|
+
|
|
466
|
+
// Check if rule already exists (might have been added before)
|
|
467
|
+
if (cssContent.includes(`.${className}`)) {
|
|
468
|
+
// Modify existing rule instead
|
|
469
|
+
return findAndModifyCSSRuleSingle(targetCSS, className, props);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
cssContent += newRule;
|
|
473
|
+
fs.writeFileSync(targetCSS, cssContent, 'utf8');
|
|
474
|
+
|
|
475
|
+
return { applied: true, file: targetCSS };
|
|
476
|
+
} catch { /* skip */ }
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
return { applied: false };
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Helper: modify rule in a specific file
|
|
483
|
+
function findAndModifyCSSRuleSingle(file, className, props) {
|
|
484
|
+
const ruleRegex = new RegExp(
|
|
485
|
+
`(\\.${escapeRegex(className)}\\s*\\{)([^}]*)(\\})`, 's'
|
|
486
|
+
);
|
|
487
|
+
try {
|
|
488
|
+
let content = fs.readFileSync(file, 'utf8');
|
|
489
|
+
const match = content.match(ruleRegex);
|
|
490
|
+
if (!match) return { applied: false };
|
|
491
|
+
|
|
492
|
+
let newBlock = match[2];
|
|
493
|
+
for (const [prop, val] of Object.entries(props)) {
|
|
494
|
+
const propRegex = new RegExp(`(${escapeRegex(prop)}\\s*:\\s*)([^;]+)(;)`, 'g');
|
|
495
|
+
if (propRegex.test(newBlock)) {
|
|
496
|
+
newBlock = newBlock.replace(
|
|
497
|
+
new RegExp(`(${escapeRegex(prop)}\\s*:\\s*)([^;]+)(;)`, 'g'),
|
|
498
|
+
`$1${val}$3`
|
|
499
|
+
);
|
|
500
|
+
} else {
|
|
501
|
+
newBlock = newBlock.trimEnd() + `\n ${prop}: ${val};\n`;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
content = content.replace(ruleRegex, `$1${newBlock}$3`);
|
|
505
|
+
fs.writeFileSync(file, content, 'utf8');
|
|
506
|
+
return { applied: true, file };
|
|
507
|
+
} catch { return { applied: false }; }
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Strategy 3: Add inline style to JSX element
|
|
511
|
+
function modifyInlineStyle(root, className, props) {
|
|
512
|
+
const jsxFiles = walkDir(root, ['.jsx', '.tsx']);
|
|
513
|
+
|
|
514
|
+
for (const file of jsxFiles) {
|
|
515
|
+
const rel = path.relative(root, file);
|
|
516
|
+
if (rel.includes('node_modules')) continue;
|
|
517
|
+
|
|
518
|
+
try {
|
|
519
|
+
let content = fs.readFileSync(file, 'utf8');
|
|
520
|
+
if (!content.includes(className)) continue;
|
|
521
|
+
|
|
522
|
+
// Find element with this className
|
|
523
|
+
// Pattern: className="...nexus-hero..." or className={'...nexus-hero...'}
|
|
524
|
+
const elementRegex = new RegExp(
|
|
525
|
+
`(<[a-zA-Z][a-zA-Z0-9]*[^>]*className=["'{][^"'}]*${escapeRegex(className)}[^"'}]*["'}])([^>]*>|\\s*/>)`,
|
|
526
|
+
's'
|
|
527
|
+
);
|
|
528
|
+
|
|
529
|
+
const match = content.match(elementRegex);
|
|
530
|
+
if (!match) continue;
|
|
531
|
+
|
|
532
|
+
const fullTag = match[0];
|
|
533
|
+
const jsxProps = propsToJSXStyle(props);
|
|
534
|
+
|
|
535
|
+
// Check if element already has a style prop
|
|
536
|
+
if (fullTag.includes('style=')) {
|
|
537
|
+
// Modify existing style prop — add/update properties
|
|
538
|
+
const styleRegex = /style=\{\{([^}]*)\}\}/;
|
|
539
|
+
const styleMatch = fullTag.match(styleRegex);
|
|
540
|
+
if (styleMatch) {
|
|
541
|
+
let existingStyles = styleMatch[1];
|
|
542
|
+
for (const [camelProp, val] of Object.entries(jsxProps)) {
|
|
543
|
+
const propRegex = new RegExp(`${camelProp}\\s*:\\s*['"][^'"]*['"]`, 'g');
|
|
544
|
+
if (propRegex.test(existingStyles)) {
|
|
545
|
+
existingStyles = existingStyles.replace(propRegex, `${camelProp}: '${val}'`);
|
|
546
|
+
} else {
|
|
547
|
+
existingStyles = existingStyles.trim();
|
|
548
|
+
if (existingStyles && !existingStyles.endsWith(',')) existingStyles += ',';
|
|
549
|
+
existingStyles += ` ${camelProp}: '${val}'`;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
const newTag = fullTag.replace(styleRegex, `style={{${existingStyles}}}`);
|
|
553
|
+
content = content.replace(fullTag, newTag);
|
|
554
|
+
}
|
|
555
|
+
} else {
|
|
556
|
+
// Add new style prop
|
|
557
|
+
const styleStr = Object.entries(jsxProps)
|
|
558
|
+
.map(([k, v]) => `${k}: '${v}'`)
|
|
559
|
+
.join(', ');
|
|
560
|
+
const insertion = ` style={{${styleStr}}}`;
|
|
561
|
+
// Insert before the closing > or />
|
|
562
|
+
const newTag = fullTag.replace(match[2], insertion + match[2]);
|
|
563
|
+
content = content.replace(fullTag, newTag);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
fs.writeFileSync(file, content, 'utf8');
|
|
567
|
+
return { applied: true, file };
|
|
568
|
+
} catch { /* skip */ }
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
return { applied: false };
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// Strategy 4: Find any CSS file in the project and append the rule
|
|
575
|
+
function appendToAnyCSSFile(root, className, props) {
|
|
576
|
+
// Priority: index.css, App.css, globals.css, main.css, styles.css
|
|
577
|
+
const candidates = [
|
|
578
|
+
'src/index.css', 'src/App.css', 'src/app/globals.css', 'src/globals.css',
|
|
579
|
+
'src/main.css', 'src/styles.css', 'styles/globals.css', 'app/globals.css',
|
|
580
|
+
'src/styles/globals.css'
|
|
581
|
+
];
|
|
582
|
+
|
|
583
|
+
for (const candidate of candidates) {
|
|
584
|
+
const fullPath = path.join(root, candidate);
|
|
585
|
+
if (fs.existsSync(fullPath)) {
|
|
586
|
+
try {
|
|
587
|
+
let content = fs.readFileSync(fullPath, 'utf8');
|
|
588
|
+
// Check if rule already exists
|
|
589
|
+
if (content.includes(`.${className}`)) {
|
|
590
|
+
return findAndModifyCSSRuleSingle(fullPath, className, props);
|
|
591
|
+
}
|
|
592
|
+
const newRule = `\n/* Draply: .${className} */\n.${className} {\n${formatProps(props)}\n}\n`;
|
|
593
|
+
content += newRule;
|
|
594
|
+
fs.writeFileSync(fullPath, content, 'utf8');
|
|
595
|
+
return { applied: true, file: fullPath };
|
|
596
|
+
} catch { /* skip */ }
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// Last resort: find ANY css file
|
|
601
|
+
const allCSS = walkDir(root, ['.css']);
|
|
602
|
+
for (const file of allCSS) {
|
|
603
|
+
const rel = path.relative(root, file);
|
|
604
|
+
if (rel.includes('node_modules') || rel.includes('.draply') || rel.includes('.next')) continue;
|
|
605
|
+
try {
|
|
606
|
+
let content = fs.readFileSync(file, 'utf8');
|
|
607
|
+
if (content.includes(`.${className}`)) {
|
|
608
|
+
return findAndModifyCSSRuleSingle(file, className, props);
|
|
609
|
+
}
|
|
610
|
+
const newRule = `\n/* Draply: .${className} */\n.${className} {\n${formatProps(props)}\n}\n`;
|
|
611
|
+
content += newRule;
|
|
612
|
+
fs.writeFileSync(file, content, 'utf8');
|
|
613
|
+
return { applied: true, file };
|
|
614
|
+
} catch { /* skip */ }
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
return { applied: false };
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
621
|
+
|
|
622
|
+
function escapeRegex(str) {
|
|
623
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function formatProps(props) {
|
|
627
|
+
return Object.entries(props).map(([k, v]) => ` ${k}: ${v};`).join('\n');
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function camelCase(str) {
|
|
631
|
+
return str.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function propsToJSXStyle(props) {
|
|
635
|
+
const result = {};
|
|
636
|
+
for (const [prop, val] of Object.entries(props)) {
|
|
637
|
+
result[camelCase(prop)] = val;
|
|
638
|
+
}
|
|
639
|
+
return result;
|
|
640
|
+
}
|
package/package.json
CHANGED
package/src/draply-features.js
CHANGED
|
@@ -56,7 +56,12 @@
|
|
|
56
56
|
flex-direction: column;
|
|
57
57
|
gap: 8px;
|
|
58
58
|
padding: 8px 0;
|
|
59
|
+
max-height: 250px;
|
|
60
|
+
overflow-y: auto;
|
|
59
61
|
}
|
|
62
|
+
#__ps_diff_panel__::-webkit-scrollbar { width: 4px; }
|
|
63
|
+
#__ps_diff_panel__::-webkit-scrollbar-track { background: transparent; }
|
|
64
|
+
#__ps_diff_panel__::-webkit-scrollbar-thumb { background: #2a2a44; border-radius: 2px; }
|
|
60
65
|
#__ps_diff_panel__.v { display: flex; }
|
|
61
66
|
|
|
62
67
|
.ps-diff-empty {
|
|
@@ -184,7 +189,12 @@
|
|
|
184
189
|
flex-direction: column;
|
|
185
190
|
gap: 8px;
|
|
186
191
|
padding: 8px 0;
|
|
192
|
+
max-height: 250px;
|
|
193
|
+
overflow-y: auto;
|
|
187
194
|
}
|
|
195
|
+
#__ps_export_panel__::-webkit-scrollbar { width: 4px; }
|
|
196
|
+
#__ps_export_panel__::-webkit-scrollbar-track { background: transparent; }
|
|
197
|
+
#__ps_export_panel__::-webkit-scrollbar-thumb { background: #2a2a44; border-radius: 2px; }
|
|
188
198
|
#__ps_export_panel__.v { display: flex; }
|
|
189
199
|
|
|
190
200
|
.ps-export-format-tabs {
|
package/src/overlay.js
CHANGED
|
@@ -1531,14 +1531,27 @@
|
|
|
1531
1531
|
}
|
|
1532
1532
|
|
|
1533
1533
|
sv.addEventListener('click', () => {
|
|
1534
|
+
toast('⏳ Applying changes to source...');
|
|
1534
1535
|
fetch('/draply-save', {
|
|
1535
1536
|
method: 'POST',
|
|
1536
1537
|
headers: { 'Content-Type': 'application/json' },
|
|
1537
1538
|
body: JSON.stringify({ changes: state.changes })
|
|
1538
1539
|
}).then(r => r.json()).then(d => {
|
|
1539
|
-
if (d.ok)
|
|
1540
|
-
|
|
1541
|
-
|
|
1540
|
+
if (d.ok) {
|
|
1541
|
+
if (d.applied > 0) {
|
|
1542
|
+
toast(`✅ Applied to ${d.applied}/${d.total} source files!`);
|
|
1543
|
+
// Log details to console
|
|
1544
|
+
(d.results || []).forEach(r => {
|
|
1545
|
+
if (r.applied) console.log(`[Draply] ✓ ${r.file} (${r.strategy})`);
|
|
1546
|
+
else console.log(`[Draply] ⚠ ${r.selector}: ${r.reason}`);
|
|
1547
|
+
});
|
|
1548
|
+
} else {
|
|
1549
|
+
toast('💾 Saved to CSS override (no source files found)');
|
|
1550
|
+
}
|
|
1551
|
+
} else {
|
|
1552
|
+
toast('⚠ Error: ' + (d.error || 'unknown'));
|
|
1553
|
+
}
|
|
1554
|
+
}).catch(() => toast('⚠ Server unavailable'));
|
|
1542
1555
|
state.changes = []; history.length = 0; sv.disabled = true; updateUnsUI();
|
|
1543
1556
|
});
|
|
1544
1557
|
|