@ytspar/devbar 1.3.1 → 1.4.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/README.md +43 -0
- package/dist/accessibility.d.ts +4 -0
- package/dist/accessibility.d.ts.map +1 -1
- package/dist/accessibility.js +57 -0
- package/dist/accessibility.js.map +1 -1
- package/dist/constants.d.ts +0 -23
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +0 -3
- package/dist/constants.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/modules/index.d.ts +1 -1
- package/dist/modules/index.d.ts.map +1 -1
- package/dist/modules/index.js +1 -0
- package/dist/modules/index.js.map +1 -1
- package/dist/modules/keyboard.d.ts +1 -1
- package/dist/modules/keyboard.d.ts.map +1 -1
- package/dist/modules/keyboard.js +4 -11
- package/dist/modules/keyboard.js.map +1 -1
- package/dist/modules/rendering.d.ts +1 -1
- package/dist/modules/rendering.d.ts.map +1 -1
- package/dist/modules/rendering.js +448 -466
- package/dist/modules/rendering.js.map +1 -1
- package/dist/modules/screenshot.d.ts +11 -2
- package/dist/modules/screenshot.d.ts.map +1 -1
- package/dist/modules/screenshot.js +32 -29
- package/dist/modules/screenshot.js.map +1 -1
- package/dist/modules/tooltips.d.ts +1 -1
- package/dist/modules/tooltips.d.ts.map +1 -1
- package/dist/modules/tooltips.js +13 -13
- package/dist/modules/tooltips.js.map +1 -1
- package/dist/modules/types.d.ts +7 -0
- package/dist/modules/types.d.ts.map +1 -1
- package/dist/modules/types.js +14 -1
- package/dist/modules/types.js.map +1 -1
- package/dist/modules/websocket.d.ts.map +1 -1
- package/dist/modules/websocket.js +334 -264
- package/dist/modules/websocket.js.map +1 -1
- package/dist/ui/buttons.d.ts.map +1 -1
- package/dist/ui/buttons.js +3 -1
- package/dist/ui/buttons.js.map +1 -1
- package/dist/ui/icons.d.ts +13 -0
- package/dist/ui/icons.d.ts.map +1 -1
- package/dist/ui/icons.js +24 -3
- package/dist/ui/icons.js.map +1 -1
- package/dist/ui/index.d.ts +1 -1
- package/dist/ui/index.d.ts.map +1 -1
- package/dist/ui/index.js.map +1 -1
- package/dist/ui/modals.d.ts +3 -2
- package/dist/ui/modals.d.ts.map +1 -1
- package/dist/ui/modals.js +22 -20
- package/dist/ui/modals.js.map +1 -1
- package/package.json +3 -4
|
@@ -10,10 +10,11 @@ import { checkMissingTags, extractFavicons, extractPageSchema, isImageKey, schem
|
|
|
10
10
|
import { ACCENT_COLOR_PRESETS, DEFAULT_SETTINGS, resolveSaveLocation } from '../settings.js';
|
|
11
11
|
import { createEmptyMessage, createInfoBox, createModalBox, createModalContent, createModalHeader, createModalOverlay, createCloseButton, createStyledButton, createSvgIcon, getButtonStyles, } from '../ui/index.js';
|
|
12
12
|
import { getResponsiveMetricVisibility } from './performance.js';
|
|
13
|
-
import { runA11yAudit, groupViolationsByImpact, getImpactColor, getViolationCounts, preloadAxe, } from '../accessibility.js';
|
|
14
|
-
import { calculateCostEstimate, closeDesignReviewConfirm, copyPathToClipboard, handleA11yAudit, handleDocumentOutline, handlePageSchema, handleSaveA11yAudit, handleSaveConsoleLogs, handleSaveOutline, handleSaveSchema, proceedWithDesignReview, showDesignReviewConfirmation, } from './screenshot.js';
|
|
13
|
+
import { a11yToMarkdown, runA11yAudit, groupViolationsByImpact, getImpactColor, getViolationCounts, preloadAxe, } from '../accessibility.js';
|
|
14
|
+
import { calculateCostEstimate, closeDesignReviewConfirm, consoleLogsToMarkdown, copyPathToClipboard, handleA11yAudit, handleDocumentOutline, handlePageSchema, handleSaveA11yAudit, handleSaveConsoleLogs, handleSaveOutline, handleSaveSchema, proceedWithDesignReview, showDesignReviewConfirmation, } from './screenshot.js';
|
|
15
15
|
import { setThemeMode } from './theme.js';
|
|
16
16
|
import { addTooltipTitle, attachBreakpointTooltip, attachButtonTooltip, attachClickToggleTooltip, attachInfoTooltip, attachMetricTooltip, attachTextTooltip, clearAllTooltips, } from './tooltips.js';
|
|
17
|
+
import { closeAllModals } from './types.js';
|
|
17
18
|
/**
|
|
18
19
|
* Capture the center of an element's bounding rect as a dot position.
|
|
19
20
|
* Used to animate the collapsed circle to the same spot as the connection dot.
|
|
@@ -120,36 +121,34 @@ function renderOverlays(state, consoleCaptureSingleton) {
|
|
|
120
121
|
// Safety: only one overlay at a time. First match wins; close the rest.
|
|
121
122
|
// (Overlay cleanup already performed by render() before calling this.)
|
|
122
123
|
if (state.consoleFilter) {
|
|
123
|
-
|
|
124
|
-
state
|
|
125
|
-
state.
|
|
126
|
-
state.showDesignReviewConfirm = false;
|
|
127
|
-
state.showSettingsPopover = false;
|
|
124
|
+
const filter = state.consoleFilter;
|
|
125
|
+
closeAllModals(state);
|
|
126
|
+
state.consoleFilter = filter;
|
|
128
127
|
renderConsolePopup(state, consoleCaptureSingleton);
|
|
129
128
|
}
|
|
130
129
|
else if (state.showOutlineModal) {
|
|
131
|
-
state
|
|
132
|
-
state.
|
|
133
|
-
state.showDesignReviewConfirm = false;
|
|
134
|
-
state.showSettingsPopover = false;
|
|
130
|
+
closeAllModals(state);
|
|
131
|
+
state.showOutlineModal = true;
|
|
135
132
|
renderOutlineModal(state);
|
|
136
133
|
}
|
|
137
134
|
else if (state.showSchemaModal) {
|
|
138
|
-
state
|
|
139
|
-
state.
|
|
140
|
-
state.showSettingsPopover = false;
|
|
135
|
+
closeAllModals(state);
|
|
136
|
+
state.showSchemaModal = true;
|
|
141
137
|
renderSchemaModal(state);
|
|
142
138
|
}
|
|
143
139
|
else if (state.showA11yModal) {
|
|
144
|
-
state
|
|
145
|
-
state.
|
|
140
|
+
closeAllModals(state);
|
|
141
|
+
state.showA11yModal = true;
|
|
146
142
|
renderA11yModal(state);
|
|
147
143
|
}
|
|
148
144
|
else if (state.showDesignReviewConfirm) {
|
|
149
|
-
state
|
|
145
|
+
closeAllModals(state);
|
|
146
|
+
state.showDesignReviewConfirm = true;
|
|
150
147
|
renderDesignReviewConfirmModal(state);
|
|
151
148
|
}
|
|
152
149
|
else if (state.showSettingsPopover) {
|
|
150
|
+
closeAllModals(state);
|
|
151
|
+
state.showSettingsPopover = true;
|
|
153
152
|
renderSettingsPopover(state);
|
|
154
153
|
}
|
|
155
154
|
}
|
|
@@ -377,24 +376,23 @@ function renderCompact(state) {
|
|
|
377
376
|
wrapper.appendChild(expandBtn);
|
|
378
377
|
}
|
|
379
378
|
// ============================================================================
|
|
380
|
-
// Expanded State
|
|
379
|
+
// Expanded State — Helper Functions
|
|
381
380
|
// ============================================================================
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
381
|
+
/**
|
|
382
|
+
* Compute the CSS position for the expanded devbar wrapper.
|
|
383
|
+
* Uses the captured dot position when available for smooth collapse/expand transitions.
|
|
384
|
+
*/
|
|
385
|
+
function computeExpandedPosition(state, position, isCentered) {
|
|
387
386
|
// Dot offset from container edge in expanded mode:
|
|
388
387
|
// border (1px) + padding (12px) + half indicator (6px) = 19px from left
|
|
389
388
|
// border (1px) + padding (8px) + half indicator (6px) = 15px from top
|
|
390
389
|
const DOT_OFFSET_LEFT = 19;
|
|
391
390
|
const DOT_OFFSET_TOP = 15;
|
|
392
|
-
const isCentered = position === 'bottom-center';
|
|
393
|
-
let posStyle;
|
|
394
391
|
// Use captured dot position to align the expanded bar's dot with where it was
|
|
395
392
|
// Always use top/left positioning for precise alignment
|
|
396
393
|
if (state.lastDotPosition && !isCentered) {
|
|
397
394
|
const isRight = position.endsWith('right');
|
|
395
|
+
let posStyle;
|
|
398
396
|
if (isRight) {
|
|
399
397
|
// For right-aligned, fall back to default
|
|
400
398
|
const isTop = position.startsWith('top');
|
|
@@ -409,20 +407,23 @@ function renderExpanded(state, customControls) {
|
|
|
409
407
|
}
|
|
410
408
|
// Clear the position after using it
|
|
411
409
|
state.lastDotPosition = null;
|
|
410
|
+
return posStyle;
|
|
412
411
|
}
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
412
|
+
const positionStyles = {
|
|
413
|
+
'bottom-left': { bottom: '20px', left: '80px' },
|
|
414
|
+
'bottom-right': { bottom: '20px', right: '16px' },
|
|
415
|
+
'top-left': { top: '20px', left: '80px' },
|
|
416
|
+
'top-right': { top: '20px', right: '16px' },
|
|
417
|
+
'bottom-center': { bottom: '12px', left: '50%', transform: 'translateX(-50%)' },
|
|
418
|
+
};
|
|
419
|
+
return positionStyles[position] ?? positionStyles['bottom-left'];
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Style the expanded wrapper container and attach the double-click-to-collapse handler.
|
|
423
|
+
*/
|
|
424
|
+
function styleExpandedWrapper(state, wrapper, posStyle, accentColor, isCentered) {
|
|
425
425
|
state.resetPositionStyles(wrapper);
|
|
426
|
+
const sizeOverrides = state.options.sizeOverrides;
|
|
426
427
|
// Calculate size values with overrides or defaults
|
|
427
428
|
// Use fit-content so DevBar only takes space it needs, but allow expansion up to max
|
|
428
429
|
// Centered: 16px margin each side. Left/right: 80px for Next.js bar + 16px margin
|
|
@@ -460,7 +461,11 @@ function renderExpanded(state, customControls) {
|
|
|
460
461
|
state.debug.state('Collapsed DevBar (double-click)');
|
|
461
462
|
state.render();
|
|
462
463
|
};
|
|
463
|
-
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Create the main row flex container used in expanded mode.
|
|
467
|
+
*/
|
|
468
|
+
function createExpandedMainRow() {
|
|
464
469
|
const mainRow = document.createElement('div');
|
|
465
470
|
mainRow.className = 'devbar-main';
|
|
466
471
|
Object.assign(mainRow.style, {
|
|
@@ -476,7 +481,12 @@ function renderExpanded(state, customControls) {
|
|
|
476
481
|
fontSize: '0.6875rem',
|
|
477
482
|
lineHeight: '1rem',
|
|
478
483
|
});
|
|
479
|
-
|
|
484
|
+
return mainRow;
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Create the connection indicator configured to collapse the devbar on click.
|
|
488
|
+
*/
|
|
489
|
+
function createExpandedConnectionIndicator(state) {
|
|
480
490
|
const connIndicator = createConnectionIndicator(state);
|
|
481
491
|
attachTextTooltip(state, connIndicator, () => state.sweetlinkConnected
|
|
482
492
|
? 'Sweetlink connected (click to minimize)'
|
|
@@ -488,18 +498,12 @@ function renderExpanded(state, customControls) {
|
|
|
488
498
|
state.debug.state('Collapsed DevBar (connection dot click)');
|
|
489
499
|
state.render();
|
|
490
500
|
};
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
gap: '0.5rem',
|
|
498
|
-
flexWrap: 'nowrap',
|
|
499
|
-
flexShrink: '0',
|
|
500
|
-
});
|
|
501
|
-
statusRow.appendChild(connIndicator);
|
|
502
|
-
// Info section
|
|
501
|
+
return connIndicator;
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Create the info section containing breakpoint display and performance metrics.
|
|
505
|
+
*/
|
|
506
|
+
function createInfoSection(state, showMetrics) {
|
|
503
507
|
const infoSection = document.createElement('div');
|
|
504
508
|
infoSection.className = 'devbar-info';
|
|
505
509
|
Object.assign(infoSection.style, {
|
|
@@ -514,135 +518,180 @@ function renderExpanded(state, customControls) {
|
|
|
514
518
|
});
|
|
515
519
|
// Breakpoint info
|
|
516
520
|
if (showMetrics.breakpoint && state.breakpointInfo) {
|
|
517
|
-
|
|
518
|
-
const breakpointData = TAILWIND_BREAKPOINTS[bp];
|
|
519
|
-
const bpSpan = document.createElement('span');
|
|
520
|
-
bpSpan.className = 'devbar-item';
|
|
521
|
-
Object.assign(bpSpan.style, { opacity: '0.9', cursor: 'default' });
|
|
522
|
-
// Use HTML tooltip for breakpoint info
|
|
523
|
-
attachBreakpointTooltip(state, bpSpan, bp, state.breakpointInfo.dimensions, breakpointData?.label || '');
|
|
524
|
-
let bpText = bp;
|
|
525
|
-
if (bp !== 'base') {
|
|
526
|
-
bpText =
|
|
527
|
-
bp === 'sm'
|
|
528
|
-
? `${bp} - ${state.breakpointInfo.dimensions.split('x')[0]}`
|
|
529
|
-
: `${bp} - ${state.breakpointInfo.dimensions}`;
|
|
530
|
-
}
|
|
531
|
-
bpSpan.textContent = bpText;
|
|
532
|
-
infoSection.appendChild(bpSpan);
|
|
521
|
+
appendBreakpointInfo(state, infoSection);
|
|
533
522
|
}
|
|
534
523
|
// Performance stats with responsive visibility
|
|
535
524
|
if (state.perfStats) {
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
525
|
+
appendPerformanceMetrics(state, infoSection, showMetrics);
|
|
526
|
+
}
|
|
527
|
+
return infoSection;
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Append the Tailwind breakpoint indicator to the info section.
|
|
531
|
+
*/
|
|
532
|
+
function appendBreakpointInfo(state, infoSection) {
|
|
533
|
+
if (!state.breakpointInfo)
|
|
534
|
+
return;
|
|
535
|
+
const bp = state.breakpointInfo.tailwindBreakpoint;
|
|
536
|
+
const breakpointData = TAILWIND_BREAKPOINTS[bp];
|
|
537
|
+
const bpSpan = document.createElement('span');
|
|
538
|
+
bpSpan.className = 'devbar-item';
|
|
539
|
+
Object.assign(bpSpan.style, { opacity: '0.9', cursor: 'default' });
|
|
540
|
+
// Use HTML tooltip for breakpoint info
|
|
541
|
+
attachBreakpointTooltip(state, bpSpan, bp, state.breakpointInfo.dimensions, breakpointData?.label || '');
|
|
542
|
+
let bpText = bp;
|
|
543
|
+
if (bp !== 'base') {
|
|
544
|
+
bpText =
|
|
545
|
+
bp === 'sm'
|
|
546
|
+
? `${bp} - ${state.breakpointInfo.dimensions.split('x')[0]}`
|
|
547
|
+
: `${bp} - ${state.breakpointInfo.dimensions}`;
|
|
548
|
+
}
|
|
549
|
+
bpSpan.textContent = bpText;
|
|
550
|
+
infoSection.appendChild(bpSpan);
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* Build the metric configuration map from current perf stats.
|
|
554
|
+
*/
|
|
555
|
+
function buildMetricConfigs(perfStats) {
|
|
556
|
+
return {
|
|
557
|
+
fcp: {
|
|
558
|
+
label: 'FCP',
|
|
559
|
+
value: perfStats.fcp,
|
|
560
|
+
title: 'First Contentful Paint (FCP)',
|
|
561
|
+
description: 'Time until the first text or image renders on screen.',
|
|
562
|
+
thresholds: { good: '<1.8s', needsWork: '1.8-3s', poor: '>3s' },
|
|
563
|
+
},
|
|
564
|
+
lcp: {
|
|
565
|
+
label: 'LCP',
|
|
566
|
+
value: perfStats.lcp,
|
|
567
|
+
title: 'Largest Contentful Paint (LCP)',
|
|
568
|
+
description: 'Time until the largest visible element renders on screen.',
|
|
569
|
+
thresholds: { good: '<2.5s', needsWork: '2.5-4s', poor: '>4s' },
|
|
570
|
+
},
|
|
571
|
+
cls: {
|
|
572
|
+
label: 'CLS',
|
|
573
|
+
value: perfStats.cls,
|
|
574
|
+
title: 'Cumulative Layout Shift (CLS)',
|
|
575
|
+
description: 'Visual stability score. Higher values mean more unexpected layout shifts.',
|
|
576
|
+
thresholds: { good: '<0.1', needsWork: '0.1-0.25', poor: '>0.25' },
|
|
577
|
+
},
|
|
578
|
+
inp: {
|
|
579
|
+
label: 'INP',
|
|
580
|
+
value: perfStats.inp,
|
|
581
|
+
title: 'Interaction to Next Paint (INP)',
|
|
582
|
+
description: 'Responsiveness to user input. Measures the longest interaction delay.',
|
|
583
|
+
thresholds: { good: '<200ms', needsWork: '200-500ms', poor: '>500ms' },
|
|
584
|
+
},
|
|
585
|
+
pageSize: {
|
|
586
|
+
label: '',
|
|
587
|
+
value: perfStats.totalSize,
|
|
588
|
+
title: 'Total Page Size',
|
|
589
|
+
description: 'Compressed/transferred size including HTML, CSS, JS, images, and other resources.',
|
|
590
|
+
},
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* Append performance metric spans (visible metrics + hidden-metrics ellipsis) to the info section.
|
|
595
|
+
*/
|
|
596
|
+
function appendPerformanceMetrics(state, infoSection, showMetrics) {
|
|
597
|
+
if (!state.perfStats)
|
|
598
|
+
return;
|
|
599
|
+
const { visible, hidden } = getResponsiveMetricVisibility(state);
|
|
600
|
+
const metricConfigs = buildMetricConfigs(state.perfStats);
|
|
601
|
+
const addSeparator = () => {
|
|
602
|
+
const sep = document.createElement('span');
|
|
603
|
+
sep.style.opacity = '0.4';
|
|
604
|
+
sep.textContent = '|';
|
|
605
|
+
infoSection.appendChild(sep);
|
|
606
|
+
};
|
|
607
|
+
// Render visible metrics
|
|
608
|
+
for (const metric of visible) {
|
|
609
|
+
if (!showMetrics[metric])
|
|
610
|
+
continue;
|
|
611
|
+
const config = metricConfigs[metric];
|
|
612
|
+
addSeparator();
|
|
613
|
+
const span = document.createElement('span');
|
|
614
|
+
span.className = 'devbar-item';
|
|
615
|
+
Object.assign(span.style, {
|
|
616
|
+
opacity: metric === 'pageSize' ? '0.7' : '0.85',
|
|
617
|
+
cursor: 'default',
|
|
618
|
+
});
|
|
619
|
+
span.textContent = config.label ? `${config.label} ${config.value}` : config.value;
|
|
620
|
+
if (config.thresholds) {
|
|
621
|
+
attachMetricTooltip(state, span, config.title, config.description, config.thresholds);
|
|
600
622
|
}
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
if (hiddenMetricsEnabled.length > 0) {
|
|
604
|
-
addSeparator();
|
|
605
|
-
const ellipsisBtn = document.createElement('span');
|
|
606
|
-
ellipsisBtn.className = 'devbar-item devbar-clickable';
|
|
607
|
-
Object.assign(ellipsisBtn.style, {
|
|
608
|
-
opacity: '0.7',
|
|
609
|
-
cursor: 'pointer',
|
|
610
|
-
padding: '0 2px',
|
|
611
|
-
});
|
|
612
|
-
ellipsisBtn.textContent = '\u00B7\u00B7\u00B7';
|
|
613
|
-
// Attach click-toggle tooltip showing hidden metrics (for mobile support)
|
|
614
|
-
attachClickToggleTooltip(state, ellipsisBtn, (tooltip) => {
|
|
615
|
-
addTooltipTitle(state, tooltip, 'More Metrics');
|
|
616
|
-
const metricsContainer = document.createElement('div');
|
|
617
|
-
Object.assign(metricsContainer.style, {
|
|
618
|
-
display: 'flex',
|
|
619
|
-
flexDirection: 'column',
|
|
620
|
-
gap: '6px',
|
|
621
|
-
marginTop: '8px',
|
|
622
|
-
});
|
|
623
|
-
for (const metric of hiddenMetricsEnabled) {
|
|
624
|
-
const config = metricConfigs[metric];
|
|
625
|
-
const row = document.createElement('div');
|
|
626
|
-
Object.assign(row.style, {
|
|
627
|
-
display: 'flex',
|
|
628
|
-
justifyContent: 'space-between',
|
|
629
|
-
gap: '12px',
|
|
630
|
-
});
|
|
631
|
-
const labelSpan = document.createElement('span');
|
|
632
|
-
Object.assign(labelSpan.style, { color: CSS_COLORS.textMuted });
|
|
633
|
-
labelSpan.textContent = config.title.split('(')[0].trim();
|
|
634
|
-
const valueSpan = document.createElement('span');
|
|
635
|
-
Object.assign(valueSpan.style, { color: CSS_COLORS.text, fontWeight: '500' });
|
|
636
|
-
valueSpan.textContent = config.value;
|
|
637
|
-
row.appendChild(labelSpan);
|
|
638
|
-
row.appendChild(valueSpan);
|
|
639
|
-
metricsContainer.appendChild(row);
|
|
640
|
-
}
|
|
641
|
-
tooltip.appendChild(metricsContainer);
|
|
642
|
-
});
|
|
643
|
-
infoSection.appendChild(ellipsisBtn);
|
|
623
|
+
else {
|
|
624
|
+
attachInfoTooltip(state, span, config.title, config.description);
|
|
644
625
|
}
|
|
626
|
+
infoSection.appendChild(span);
|
|
645
627
|
}
|
|
628
|
+
// Render ellipsis button for hidden metrics
|
|
629
|
+
const hiddenMetricsEnabled = hidden.filter((m) => showMetrics[m]);
|
|
630
|
+
if (hiddenMetricsEnabled.length > 0) {
|
|
631
|
+
addSeparator();
|
|
632
|
+
appendHiddenMetricsEllipsis(state, infoSection, hiddenMetricsEnabled, metricConfigs);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
/**
|
|
636
|
+
* Append the ellipsis button that reveals hidden metrics in a click-toggle tooltip.
|
|
637
|
+
*/
|
|
638
|
+
function appendHiddenMetricsEllipsis(state, infoSection, hiddenMetricsEnabled, metricConfigs) {
|
|
639
|
+
const ellipsisBtn = document.createElement('span');
|
|
640
|
+
ellipsisBtn.className = 'devbar-item devbar-clickable';
|
|
641
|
+
Object.assign(ellipsisBtn.style, {
|
|
642
|
+
opacity: '0.7',
|
|
643
|
+
cursor: 'pointer',
|
|
644
|
+
padding: '0 2px',
|
|
645
|
+
});
|
|
646
|
+
ellipsisBtn.textContent = '\u00B7\u00B7\u00B7';
|
|
647
|
+
// Attach click-toggle tooltip showing hidden metrics (for mobile support)
|
|
648
|
+
attachClickToggleTooltip(state, ellipsisBtn, (tooltip) => {
|
|
649
|
+
addTooltipTitle(state, tooltip, 'More Metrics');
|
|
650
|
+
const metricsContainer = document.createElement('div');
|
|
651
|
+
Object.assign(metricsContainer.style, {
|
|
652
|
+
display: 'flex',
|
|
653
|
+
flexDirection: 'column',
|
|
654
|
+
gap: '6px',
|
|
655
|
+
marginTop: '8px',
|
|
656
|
+
});
|
|
657
|
+
for (const metric of hiddenMetricsEnabled) {
|
|
658
|
+
const config = metricConfigs[metric];
|
|
659
|
+
const row = document.createElement('div');
|
|
660
|
+
Object.assign(row.style, {
|
|
661
|
+
display: 'flex',
|
|
662
|
+
justifyContent: 'space-between',
|
|
663
|
+
gap: '12px',
|
|
664
|
+
});
|
|
665
|
+
const labelSpan = document.createElement('span');
|
|
666
|
+
Object.assign(labelSpan.style, { color: CSS_COLORS.textMuted });
|
|
667
|
+
labelSpan.textContent = config.title.split('(')[0].trim();
|
|
668
|
+
const valueSpan = document.createElement('span');
|
|
669
|
+
Object.assign(valueSpan.style, { color: CSS_COLORS.text, fontWeight: '500' });
|
|
670
|
+
valueSpan.textContent = config.value;
|
|
671
|
+
row.appendChild(labelSpan);
|
|
672
|
+
row.appendChild(valueSpan);
|
|
673
|
+
metricsContainer.appendChild(row);
|
|
674
|
+
}
|
|
675
|
+
tooltip.appendChild(metricsContainer);
|
|
676
|
+
});
|
|
677
|
+
infoSection.appendChild(ellipsisBtn);
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* Create the status row containing the connection indicator, info section, and console badges.
|
|
681
|
+
*/
|
|
682
|
+
function createStatusRow(state, showMetrics, showConsoleBadges, errorCount, warningCount, infoCount) {
|
|
683
|
+
const connIndicator = createExpandedConnectionIndicator(state);
|
|
684
|
+
const statusRow = document.createElement('div');
|
|
685
|
+
statusRow.className = 'devbar-status';
|
|
686
|
+
Object.assign(statusRow.style, {
|
|
687
|
+
display: 'flex',
|
|
688
|
+
alignItems: 'center',
|
|
689
|
+
gap: '0.5rem',
|
|
690
|
+
flexWrap: 'nowrap',
|
|
691
|
+
flexShrink: '0',
|
|
692
|
+
});
|
|
693
|
+
statusRow.appendChild(connIndicator);
|
|
694
|
+
const infoSection = createInfoSection(state, showMetrics);
|
|
646
695
|
statusRow.appendChild(infoSection);
|
|
647
696
|
// Console badges - add to status row so they stay with info
|
|
648
697
|
if (showConsoleBadges) {
|
|
@@ -656,8 +705,12 @@ function renderExpanded(state, customControls) {
|
|
|
656
705
|
statusRow.appendChild(createConsoleBadge(state, 'info', infoCount, BUTTON_COLORS.info));
|
|
657
706
|
}
|
|
658
707
|
}
|
|
659
|
-
|
|
660
|
-
|
|
708
|
+
return statusRow;
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* Create the action buttons container (screenshot, AI review, outline, schema, a11y, settings, compact).
|
|
712
|
+
*/
|
|
713
|
+
function createActionButtonsContainer(state, showScreenshot, accentColor) {
|
|
661
714
|
const actionsContainer = document.createElement('div');
|
|
662
715
|
actionsContainer.className = 'devbar-actions';
|
|
663
716
|
if (showScreenshot) {
|
|
@@ -669,57 +722,89 @@ function renderExpanded(state, customControls) {
|
|
|
669
722
|
actionsContainer.appendChild(createA11yButton(state));
|
|
670
723
|
actionsContainer.appendChild(createSettingsButton(state));
|
|
671
724
|
actionsContainer.appendChild(createCompactToggleButton(state));
|
|
725
|
+
return actionsContainer;
|
|
726
|
+
}
|
|
727
|
+
/**
|
|
728
|
+
* Create the custom controls row for user-defined buttons.
|
|
729
|
+
* Returns null if there are no custom controls.
|
|
730
|
+
*/
|
|
731
|
+
function createCustomControlsRow(customControls, accentColor) {
|
|
732
|
+
if (customControls.length === 0)
|
|
733
|
+
return null;
|
|
734
|
+
const customRow = document.createElement('div');
|
|
735
|
+
Object.assign(customRow.style, {
|
|
736
|
+
display: 'flex',
|
|
737
|
+
flexWrap: 'wrap',
|
|
738
|
+
alignItems: 'center',
|
|
739
|
+
gap: '0.5rem',
|
|
740
|
+
padding: '0 0.75rem 0.5rem 0.75rem',
|
|
741
|
+
borderTop: `1px solid ${accentColor}30`,
|
|
742
|
+
marginTop: '0',
|
|
743
|
+
paddingTop: '0.5rem',
|
|
744
|
+
fontFamily: FONT_MONO,
|
|
745
|
+
fontSize: '0.6875rem',
|
|
746
|
+
});
|
|
747
|
+
customControls.forEach((control) => {
|
|
748
|
+
const btn = document.createElement('button');
|
|
749
|
+
btn.type = 'button';
|
|
750
|
+
const color = control.variant === 'warning' ? BUTTON_COLORS.warning : accentColor;
|
|
751
|
+
const isActive = control.active ?? false;
|
|
752
|
+
const isDisabled = control.disabled ?? false;
|
|
753
|
+
Object.assign(btn.style, {
|
|
754
|
+
padding: '4px 10px',
|
|
755
|
+
backgroundColor: isActive ? `${color}33` : 'transparent',
|
|
756
|
+
border: `1px solid ${isActive ? color : `${color}60`}`,
|
|
757
|
+
borderRadius: '6px',
|
|
758
|
+
color: isActive ? color : `${color}99`,
|
|
759
|
+
fontSize: '0.625rem',
|
|
760
|
+
cursor: isDisabled ? 'not-allowed' : 'pointer',
|
|
761
|
+
opacity: isDisabled ? '0.5' : '1',
|
|
762
|
+
transition: 'all 150ms',
|
|
763
|
+
});
|
|
764
|
+
btn.textContent = control.label;
|
|
765
|
+
btn.disabled = isDisabled;
|
|
766
|
+
if (!isDisabled) {
|
|
767
|
+
btn.onmouseenter = () => {
|
|
768
|
+
btn.style.backgroundColor = `${color}20`;
|
|
769
|
+
btn.style.borderColor = color;
|
|
770
|
+
btn.style.color = color;
|
|
771
|
+
};
|
|
772
|
+
btn.onmouseleave = () => {
|
|
773
|
+
btn.style.backgroundColor = isActive ? `${color}33` : 'transparent';
|
|
774
|
+
btn.style.borderColor = isActive ? color : `${color}60`;
|
|
775
|
+
btn.style.color = isActive ? color : `${color}99`;
|
|
776
|
+
};
|
|
777
|
+
btn.onclick = () => control.onClick();
|
|
778
|
+
}
|
|
779
|
+
customRow.appendChild(btn);
|
|
780
|
+
});
|
|
781
|
+
return customRow;
|
|
782
|
+
}
|
|
783
|
+
// ============================================================================
|
|
784
|
+
// Expanded State — Orchestrator
|
|
785
|
+
// ============================================================================
|
|
786
|
+
function renderExpanded(state, customControls) {
|
|
787
|
+
if (!state.container)
|
|
788
|
+
return;
|
|
789
|
+
const { position, accentColor, showMetrics, showScreenshot, showConsoleBadges } = state.options;
|
|
790
|
+
const { errorCount, warningCount, infoCount } = state.getLogCounts();
|
|
791
|
+
const isCentered = position === 'bottom-center';
|
|
792
|
+
const wrapper = state.container;
|
|
793
|
+
// 1. Position and style the wrapper
|
|
794
|
+
const posStyle = computeExpandedPosition(state, position, isCentered);
|
|
795
|
+
styleExpandedWrapper(state, wrapper, posStyle, accentColor, isCentered);
|
|
796
|
+
// 2. Build the main row
|
|
797
|
+
const mainRow = createExpandedMainRow();
|
|
798
|
+
// 3. Status row (connection dot + info metrics + console badges)
|
|
799
|
+
const statusRow = createStatusRow(state, showMetrics, showConsoleBadges, errorCount, warningCount, infoCount);
|
|
800
|
+
mainRow.appendChild(statusRow);
|
|
801
|
+
// 4. Action buttons
|
|
802
|
+
const actionsContainer = createActionButtonsContainer(state, showScreenshot, accentColor);
|
|
672
803
|
mainRow.appendChild(actionsContainer);
|
|
673
804
|
wrapper.appendChild(mainRow);
|
|
674
|
-
//
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
Object.assign(customRow.style, {
|
|
678
|
-
display: 'flex',
|
|
679
|
-
flexWrap: 'wrap',
|
|
680
|
-
alignItems: 'center',
|
|
681
|
-
gap: '0.5rem',
|
|
682
|
-
padding: '0 0.75rem 0.5rem 0.75rem',
|
|
683
|
-
borderTop: `1px solid ${accentColor}30`,
|
|
684
|
-
marginTop: '0',
|
|
685
|
-
paddingTop: '0.5rem',
|
|
686
|
-
fontFamily: FONT_MONO,
|
|
687
|
-
fontSize: '0.6875rem',
|
|
688
|
-
});
|
|
689
|
-
customControls.forEach((control) => {
|
|
690
|
-
const btn = document.createElement('button');
|
|
691
|
-
btn.type = 'button';
|
|
692
|
-
const color = control.variant === 'warning' ? BUTTON_COLORS.warning : accentColor;
|
|
693
|
-
const isActive = control.active ?? false;
|
|
694
|
-
const isDisabled = control.disabled ?? false;
|
|
695
|
-
Object.assign(btn.style, {
|
|
696
|
-
padding: '4px 10px',
|
|
697
|
-
backgroundColor: isActive ? `${color}33` : 'transparent',
|
|
698
|
-
border: `1px solid ${isActive ? color : `${color}60`}`,
|
|
699
|
-
borderRadius: '6px',
|
|
700
|
-
color: isActive ? color : `${color}99`,
|
|
701
|
-
fontSize: '0.625rem',
|
|
702
|
-
cursor: isDisabled ? 'not-allowed' : 'pointer',
|
|
703
|
-
opacity: isDisabled ? '0.5' : '1',
|
|
704
|
-
transition: 'all 150ms',
|
|
705
|
-
});
|
|
706
|
-
btn.textContent = control.label;
|
|
707
|
-
btn.disabled = isDisabled;
|
|
708
|
-
if (!isDisabled) {
|
|
709
|
-
btn.onmouseenter = () => {
|
|
710
|
-
btn.style.backgroundColor = `${color}20`;
|
|
711
|
-
btn.style.borderColor = color;
|
|
712
|
-
btn.style.color = color;
|
|
713
|
-
};
|
|
714
|
-
btn.onmouseleave = () => {
|
|
715
|
-
btn.style.backgroundColor = isActive ? `${color}33` : 'transparent';
|
|
716
|
-
btn.style.borderColor = isActive ? color : `${color}60`;
|
|
717
|
-
btn.style.color = isActive ? color : `${color}99`;
|
|
718
|
-
};
|
|
719
|
-
btn.onclick = () => control.onClick();
|
|
720
|
-
}
|
|
721
|
-
customRow.appendChild(btn);
|
|
722
|
-
});
|
|
805
|
+
// 5. Custom controls row (if any)
|
|
806
|
+
const customRow = createCustomControlsRow(customControls, accentColor);
|
|
807
|
+
if (customRow) {
|
|
723
808
|
wrapper.appendChild(customRow);
|
|
724
809
|
}
|
|
725
810
|
}
|
|
@@ -750,10 +835,9 @@ function createConsoleBadge(state, type, count, color) {
|
|
|
750
835
|
badge.textContent = count > 99 ? '99+' : String(count);
|
|
751
836
|
attachTextTooltip(state, badge, () => `${count} console ${label}${count === 1 ? '' : 's'} (click to view)`);
|
|
752
837
|
badge.onclick = () => {
|
|
753
|
-
|
|
754
|
-
state
|
|
755
|
-
state.
|
|
756
|
-
state.showSettingsPopover = false;
|
|
838
|
+
const newFilter = state.consoleFilter === type ? null : type;
|
|
839
|
+
closeAllModals(state);
|
|
840
|
+
state.consoleFilter = newFilter;
|
|
757
841
|
state.render();
|
|
758
842
|
};
|
|
759
843
|
return badge;
|
|
@@ -872,26 +956,12 @@ function createScreenshotButton(state, accentColor) {
|
|
|
872
956
|
}
|
|
873
957
|
else {
|
|
874
958
|
// Camera icon SVG
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
|
882
|
-
g.setAttribute('stroke-linecap', 'round');
|
|
883
|
-
g.setAttribute('stroke-linejoin', 'round');
|
|
884
|
-
g.setAttribute('stroke-width', '4');
|
|
885
|
-
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
886
|
-
path.setAttribute('d', 'M19.844 7.938H7.938v11.905m0 11.113v11.906h11.905m23.019-11.906v11.906H30.956m11.906-23.018V7.938H30.956');
|
|
887
|
-
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
|
888
|
-
circle.setAttribute('cx', '25.4');
|
|
889
|
-
circle.setAttribute('cy', '25.4');
|
|
890
|
-
circle.setAttribute('r', '8.731');
|
|
891
|
-
g.appendChild(path);
|
|
892
|
-
g.appendChild(circle);
|
|
893
|
-
svg.appendChild(g);
|
|
894
|
-
btn.appendChild(svg);
|
|
959
|
+
btn.appendChild(createSvgIcon('M19.844 7.938H7.938v11.905m0 11.113v11.906h11.905m23.019-11.906v11.906H30.956m11.906-23.018V7.938H30.956', {
|
|
960
|
+
viewBox: '0 0 50.8 50.8',
|
|
961
|
+
stroke: true,
|
|
962
|
+
strokeWidth: '4',
|
|
963
|
+
children: [{ type: 'circle', cx: '25.4', cy: '25.4', r: '8.731' }],
|
|
964
|
+
}));
|
|
895
965
|
}
|
|
896
966
|
return btn;
|
|
897
967
|
}
|
|
@@ -1056,49 +1126,15 @@ function createSettingsButton(state) {
|
|
|
1056
1126
|
});
|
|
1057
1127
|
const isActive = state.showSettingsPopover;
|
|
1058
1128
|
const color = CSS_COLORS.textSecondary;
|
|
1059
|
-
Object.assign(btn.style,
|
|
1060
|
-
display: 'flex',
|
|
1061
|
-
alignItems: 'center',
|
|
1062
|
-
justifyContent: 'center',
|
|
1063
|
-
width: '22px',
|
|
1064
|
-
height: '22px',
|
|
1065
|
-
minWidth: '22px',
|
|
1066
|
-
minHeight: '22px',
|
|
1067
|
-
flexShrink: '0',
|
|
1068
|
-
borderRadius: '50%',
|
|
1069
|
-
border: `1px solid ${isActive ? color : `${color}60`}`,
|
|
1070
|
-
backgroundColor: isActive ? `${color}20` : 'transparent',
|
|
1071
|
-
color: isActive ? color : `${color}99`,
|
|
1072
|
-
cursor: 'pointer',
|
|
1073
|
-
transition: 'all 150ms',
|
|
1074
|
-
});
|
|
1129
|
+
Object.assign(btn.style, getButtonStyles(color, isActive, false));
|
|
1075
1130
|
btn.onclick = () => {
|
|
1076
|
-
|
|
1077
|
-
state
|
|
1078
|
-
state.
|
|
1079
|
-
state.showSchemaModal = false;
|
|
1080
|
-
state.showDesignReviewConfirm = false;
|
|
1131
|
+
const wasOpen = state.showSettingsPopover;
|
|
1132
|
+
closeAllModals(state);
|
|
1133
|
+
state.showSettingsPopover = !wasOpen;
|
|
1081
1134
|
state.render();
|
|
1082
1135
|
};
|
|
1083
1136
|
// Gear icon SVG
|
|
1084
|
-
|
|
1085
|
-
svg.setAttribute('width', '12');
|
|
1086
|
-
svg.setAttribute('height', '12');
|
|
1087
|
-
svg.setAttribute('viewBox', '0 0 24 24');
|
|
1088
|
-
svg.setAttribute('fill', 'none');
|
|
1089
|
-
svg.setAttribute('stroke', 'currentColor');
|
|
1090
|
-
svg.setAttribute('stroke-width', '2');
|
|
1091
|
-
svg.setAttribute('stroke-linecap', 'round');
|
|
1092
|
-
svg.setAttribute('stroke-linejoin', 'round');
|
|
1093
|
-
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
1094
|
-
path.setAttribute('d', 'M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z');
|
|
1095
|
-
svg.appendChild(path);
|
|
1096
|
-
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
|
1097
|
-
circle.setAttribute('cx', '12');
|
|
1098
|
-
circle.setAttribute('cy', '12');
|
|
1099
|
-
circle.setAttribute('r', '3');
|
|
1100
|
-
svg.appendChild(circle);
|
|
1101
|
-
btn.appendChild(svg);
|
|
1137
|
+
btn.appendChild(createSvgIcon('M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z', { stroke: true, children: [{ type: 'circle', cx: '12', cy: '12', r: '3' }] }));
|
|
1102
1138
|
return btn;
|
|
1103
1139
|
}
|
|
1104
1140
|
/**
|
|
@@ -1111,22 +1147,8 @@ function createCompactToggleButton(state) {
|
|
|
1111
1147
|
const isCompact = state.compactMode;
|
|
1112
1148
|
const { accentColor } = state.options;
|
|
1113
1149
|
const iconColor = CSS_COLORS.textSecondary;
|
|
1114
|
-
Object.assign(btn.style,
|
|
1115
|
-
|
|
1116
|
-
alignItems: 'center',
|
|
1117
|
-
justifyContent: 'center',
|
|
1118
|
-
width: '22px',
|
|
1119
|
-
height: '22px',
|
|
1120
|
-
minWidth: '22px',
|
|
1121
|
-
minHeight: '22px',
|
|
1122
|
-
flexShrink: '0',
|
|
1123
|
-
borderRadius: '50%',
|
|
1124
|
-
border: `1px solid ${accentColor}60`,
|
|
1125
|
-
backgroundColor: 'transparent',
|
|
1126
|
-
color: `${iconColor}99`,
|
|
1127
|
-
cursor: 'pointer',
|
|
1128
|
-
transition: 'all 150ms',
|
|
1129
|
-
});
|
|
1150
|
+
Object.assign(btn.style, getButtonStyles(iconColor, false, false));
|
|
1151
|
+
btn.style.borderColor = `${accentColor}60`;
|
|
1130
1152
|
attachTextTooltip(state, btn, () => (isCompact ? 'Expand (Cmd or Ctrl+Shift+M)' : 'Compact (Cmd or Ctrl+Shift+M)'), {
|
|
1131
1153
|
onEnter: () => {
|
|
1132
1154
|
btn.style.borderColor = accentColor;
|
|
@@ -1143,20 +1165,8 @@ function createCompactToggleButton(state) {
|
|
|
1143
1165
|
state.toggleCompactMode();
|
|
1144
1166
|
};
|
|
1145
1167
|
// Chevron icon SVG - points right when expanded, left when compact
|
|
1146
|
-
const
|
|
1147
|
-
|
|
1148
|
-
svg.setAttribute('height', '12');
|
|
1149
|
-
svg.setAttribute('viewBox', '0 0 24 24');
|
|
1150
|
-
svg.setAttribute('fill', 'none');
|
|
1151
|
-
svg.setAttribute('stroke', 'currentColor');
|
|
1152
|
-
svg.setAttribute('stroke-width', '2');
|
|
1153
|
-
svg.setAttribute('stroke-linecap', 'round');
|
|
1154
|
-
svg.setAttribute('stroke-linejoin', 'round');
|
|
1155
|
-
const path = document.createElementNS('http://www.w3.org/2000/svg', 'polyline');
|
|
1156
|
-
// Left chevron (<) when expanded to shrink, right chevron (>) when compact to expand
|
|
1157
|
-
path.setAttribute('points', isCompact ? '9 18 15 12 9 6' : '15 18 9 12 15 6');
|
|
1158
|
-
svg.appendChild(path);
|
|
1159
|
-
btn.appendChild(svg);
|
|
1168
|
+
const chevronPoints = isCompact ? '9 18 15 12 9 6' : '15 18 9 12 15 6';
|
|
1169
|
+
btn.appendChild(createSvgIcon('', { stroke: true, children: [{ type: 'polyline', points: chevronPoints }] }));
|
|
1160
1170
|
return btn;
|
|
1161
1171
|
}
|
|
1162
1172
|
// ============================================================================
|
|
@@ -1184,11 +1194,7 @@ function renderConsolePopup(state, consoleCaptureSingleton) {
|
|
|
1184
1194
|
title: `Console ${label} (${logs.length})`,
|
|
1185
1195
|
onClose: closeModal,
|
|
1186
1196
|
onCopyMd: async () => {
|
|
1187
|
-
|
|
1188
|
-
const time = new Date(log.timestamp).toLocaleTimeString();
|
|
1189
|
-
return `[${time}] ${log.level}: ${log.message}`;
|
|
1190
|
-
});
|
|
1191
|
-
await navigator.clipboard.writeText(lines.join('\n'));
|
|
1197
|
+
await navigator.clipboard.writeText(consoleLogsToMarkdown(logs));
|
|
1192
1198
|
},
|
|
1193
1199
|
onSave: () => handleSaveConsoleLogs(state, logs),
|
|
1194
1200
|
onClear: () => state.clearConsoleLogs(),
|
|
@@ -1269,23 +1275,40 @@ function renderOutlineModal(state) {
|
|
|
1269
1275
|
content.appendChild(createEmptyMessage('No semantic elements found in this document'));
|
|
1270
1276
|
}
|
|
1271
1277
|
else {
|
|
1272
|
-
renderOutlineNodes(outline, content, 0);
|
|
1278
|
+
renderOutlineNodes(outline, content, 0, { lastHeadingLevel: 0 });
|
|
1273
1279
|
}
|
|
1274
1280
|
modal.appendChild(content);
|
|
1275
1281
|
overlay.appendChild(modal);
|
|
1276
1282
|
state.overlayElement = overlay;
|
|
1277
1283
|
document.body.appendChild(overlay);
|
|
1278
1284
|
}
|
|
1279
|
-
function renderOutlineNodes(nodes, parentEl, depth) {
|
|
1285
|
+
function renderOutlineNodes(nodes, parentEl, depth, headingTracker) {
|
|
1280
1286
|
for (const node of nodes) {
|
|
1287
|
+
const isHeading = node.category === 'heading' && node.level > 0;
|
|
1288
|
+
const skippedLevel = isHeading && node.level > headingTracker.lastHeadingLevel + 1;
|
|
1289
|
+
if (isHeading) {
|
|
1290
|
+
headingTracker.lastHeadingLevel = node.level;
|
|
1291
|
+
}
|
|
1281
1292
|
const nodeEl = document.createElement('div');
|
|
1282
1293
|
Object.assign(nodeEl.style, {
|
|
1283
1294
|
padding: `4px 0 4px ${depth * 16}px`,
|
|
1284
1295
|
});
|
|
1296
|
+
// Warning icon for heading hierarchy breaks
|
|
1297
|
+
if (skippedLevel) {
|
|
1298
|
+
const warn = document.createElement('span');
|
|
1299
|
+
Object.assign(warn.style, {
|
|
1300
|
+
color: CSS_COLORS.error,
|
|
1301
|
+
fontSize: '0.625rem',
|
|
1302
|
+
marginRight: '4px',
|
|
1303
|
+
});
|
|
1304
|
+
warn.textContent = '\u26A0';
|
|
1305
|
+
warn.title = `Heading level skipped (expected h${node.level - 1} or higher before h${node.level})`;
|
|
1306
|
+
nodeEl.appendChild(warn);
|
|
1307
|
+
}
|
|
1285
1308
|
const tagSpan = document.createElement('span');
|
|
1286
1309
|
const categoryColor = CATEGORY_COLORS[node.category || 'other'] || CATEGORY_COLORS.other;
|
|
1287
1310
|
Object.assign(tagSpan.style, {
|
|
1288
|
-
color: categoryColor,
|
|
1311
|
+
color: skippedLevel ? CSS_COLORS.error : categoryColor,
|
|
1289
1312
|
fontSize: '0.6875rem',
|
|
1290
1313
|
fontWeight: '500',
|
|
1291
1314
|
});
|
|
@@ -1313,7 +1336,7 @@ function renderOutlineNodes(nodes, parentEl, depth) {
|
|
|
1313
1336
|
if (node.id) {
|
|
1314
1337
|
const idSpan = document.createElement('span');
|
|
1315
1338
|
Object.assign(idSpan.style, {
|
|
1316
|
-
color:
|
|
1339
|
+
color: CSS_COLORS.textSecondary,
|
|
1317
1340
|
fontSize: '0.625rem',
|
|
1318
1341
|
marginLeft: '6px',
|
|
1319
1342
|
});
|
|
@@ -1322,7 +1345,7 @@ function renderOutlineNodes(nodes, parentEl, depth) {
|
|
|
1322
1345
|
}
|
|
1323
1346
|
parentEl.appendChild(nodeEl);
|
|
1324
1347
|
if (node.children.length > 0) {
|
|
1325
|
-
renderOutlineNodes(node.children, parentEl, depth + 1);
|
|
1348
|
+
renderOutlineNodes(node.children, parentEl, depth + 1, headingTracker);
|
|
1326
1349
|
}
|
|
1327
1350
|
}
|
|
1328
1351
|
}
|
|
@@ -1335,12 +1358,14 @@ function renderSchemaModal(state) {
|
|
|
1335
1358
|
};
|
|
1336
1359
|
const overlay = createModalOverlay(closeModal);
|
|
1337
1360
|
const modal = createModalBox(color);
|
|
1361
|
+
const missingTags = checkMissingTags(schema);
|
|
1362
|
+
const favicons = extractFavicons();
|
|
1338
1363
|
const header = createModalHeader({
|
|
1339
1364
|
color,
|
|
1340
1365
|
title: 'Page Schema',
|
|
1341
1366
|
onClose: closeModal,
|
|
1342
1367
|
onCopyMd: async () => {
|
|
1343
|
-
const markdown = schemaToMarkdown(schema);
|
|
1368
|
+
const markdown = schemaToMarkdown(schema, { missingTags, favicons });
|
|
1344
1369
|
await navigator.clipboard.writeText(markdown);
|
|
1345
1370
|
},
|
|
1346
1371
|
onSave: () => handleSaveSchema(state),
|
|
@@ -1351,8 +1376,6 @@ function renderSchemaModal(state) {
|
|
|
1351
1376
|
});
|
|
1352
1377
|
modal.appendChild(header);
|
|
1353
1378
|
const content = createModalContent();
|
|
1354
|
-
const missingTags = checkMissingTags(schema);
|
|
1355
|
-
const favicons = extractFavicons();
|
|
1356
1379
|
const hasContent = schema.jsonLd.length > 0 ||
|
|
1357
1380
|
Object.keys(schema.openGraph).length > 0 ||
|
|
1358
1381
|
Object.keys(schema.twitter).length > 0 ||
|
|
@@ -1440,7 +1463,7 @@ function renderJsonLdItems(container, items, color) {
|
|
|
1440
1463
|
});
|
|
1441
1464
|
const itemTitle = document.createElement('span');
|
|
1442
1465
|
Object.assign(itemTitle.style, {
|
|
1443
|
-
color:
|
|
1466
|
+
color: CSS_COLORS.textSecondary,
|
|
1444
1467
|
fontSize: '0.6875rem',
|
|
1445
1468
|
});
|
|
1446
1469
|
itemTitle.textContent = `Schema ${i + 1}`;
|
|
@@ -1465,10 +1488,10 @@ function renderJsonLdItems(container, items, color) {
|
|
|
1465
1488
|
borderRadius: '4px',
|
|
1466
1489
|
borderLeft: `2px solid ${color}50`,
|
|
1467
1490
|
padding: '10px 10px 10px 12px',
|
|
1468
|
-
overflow: 'auto',
|
|
1469
1491
|
fontSize: '0.625rem',
|
|
1470
1492
|
margin: '0',
|
|
1471
|
-
|
|
1493
|
+
whiteSpace: 'pre-wrap',
|
|
1494
|
+
wordBreak: 'break-word',
|
|
1472
1495
|
});
|
|
1473
1496
|
appendHighlightedJson(codeEl, JSON.stringify(item, null, 2));
|
|
1474
1497
|
itemEl.appendChild(codeEl);
|
|
@@ -1870,60 +1893,6 @@ function renderMissingTagsSection(container, tags) {
|
|
|
1870
1893
|
// ============================================================================
|
|
1871
1894
|
// Accessibility Audit Modal
|
|
1872
1895
|
// ============================================================================
|
|
1873
|
-
function formatA11yMarkdown(result) {
|
|
1874
|
-
const counts = getViolationCounts(result.violations);
|
|
1875
|
-
const lines = [
|
|
1876
|
-
'# Accessibility Audit Report',
|
|
1877
|
-
'',
|
|
1878
|
-
`**URL:** ${result.url}`,
|
|
1879
|
-
`**Timestamp:** ${result.timestamp}`,
|
|
1880
|
-
'',
|
|
1881
|
-
'## Summary',
|
|
1882
|
-
'',
|
|
1883
|
-
`- **Total violations:** ${counts.total}`,
|
|
1884
|
-
`- Critical: ${counts.critical}`,
|
|
1885
|
-
`- Serious: ${counts.serious}`,
|
|
1886
|
-
`- Moderate: ${counts.moderate}`,
|
|
1887
|
-
`- Minor: ${counts.minor}`,
|
|
1888
|
-
`- Passes: ${result.passes.length}`,
|
|
1889
|
-
`- Incomplete: ${result.incomplete.length}`,
|
|
1890
|
-
'',
|
|
1891
|
-
];
|
|
1892
|
-
if (result.violations.length === 0) {
|
|
1893
|
-
lines.push('No accessibility violations found.');
|
|
1894
|
-
return lines.join('\n');
|
|
1895
|
-
}
|
|
1896
|
-
const grouped = groupViolationsByImpact(result.violations);
|
|
1897
|
-
for (const [impact, violations] of grouped) {
|
|
1898
|
-
if (violations.length === 0)
|
|
1899
|
-
continue;
|
|
1900
|
-
lines.push(`## ${impact.charAt(0).toUpperCase() + impact.slice(1)} (${violations.length})`);
|
|
1901
|
-
lines.push('');
|
|
1902
|
-
for (const v of violations) {
|
|
1903
|
-
lines.push(`### ${v.id}`);
|
|
1904
|
-
lines.push('');
|
|
1905
|
-
lines.push(`**${v.help}**`);
|
|
1906
|
-
lines.push('');
|
|
1907
|
-
lines.push(v.description);
|
|
1908
|
-
lines.push('');
|
|
1909
|
-
lines.push(`- Help: ${v.helpUrl}`);
|
|
1910
|
-
lines.push(`- Elements affected: ${v.nodes.length}`);
|
|
1911
|
-
lines.push('');
|
|
1912
|
-
for (const node of v.nodes.slice(0, 10)) {
|
|
1913
|
-
const html = node.html.length > 120 ? `${node.html.slice(0, 120)}...` : node.html;
|
|
1914
|
-
lines.push(` - \`${html}\``);
|
|
1915
|
-
if (node.target.length > 0) {
|
|
1916
|
-
lines.push(` Selector: \`${node.target.join(', ')}\``);
|
|
1917
|
-
}
|
|
1918
|
-
}
|
|
1919
|
-
if (v.nodes.length > 10) {
|
|
1920
|
-
lines.push(` - ... and ${v.nodes.length - 10} more`);
|
|
1921
|
-
}
|
|
1922
|
-
lines.push('');
|
|
1923
|
-
}
|
|
1924
|
-
}
|
|
1925
|
-
return lines.join('\n');
|
|
1926
|
-
}
|
|
1927
1896
|
function clearChildren(el) {
|
|
1928
1897
|
while (el.firstChild) {
|
|
1929
1898
|
el.removeChild(el.firstChild);
|
|
@@ -1968,7 +1937,7 @@ function renderA11yModal(state) {
|
|
|
1968
1937
|
// Check modal is still open
|
|
1969
1938
|
if (!state.showA11yModal)
|
|
1970
1939
|
return;
|
|
1971
|
-
const markdown =
|
|
1940
|
+
const markdown = a11yToMarkdown(result);
|
|
1972
1941
|
// Replace modal content
|
|
1973
1942
|
clearChildren(modal);
|
|
1974
1943
|
const violationCount = result.violations.length;
|
|
@@ -1982,7 +1951,7 @@ function renderA11yModal(state) {
|
|
|
1982
1951
|
onCopyMd: async () => {
|
|
1983
1952
|
await navigator.clipboard.writeText(markdown);
|
|
1984
1953
|
},
|
|
1985
|
-
onSave: () => handleSaveA11yAudit(state,
|
|
1954
|
+
onSave: () => handleSaveA11yAudit(state, result),
|
|
1986
1955
|
sweetlinkConnected: state.sweetlinkConnected,
|
|
1987
1956
|
saveLocation: state.options.saveLocation,
|
|
1988
1957
|
isSaving: state.savingA11yAudit,
|
|
@@ -1995,7 +1964,7 @@ function renderA11yModal(state) {
|
|
|
1995
1964
|
Object.assign(successMsg.style, {
|
|
1996
1965
|
textAlign: 'center',
|
|
1997
1966
|
padding: '40px',
|
|
1998
|
-
color:
|
|
1967
|
+
color: CSS_COLORS.primary,
|
|
1999
1968
|
fontSize: '0.875rem',
|
|
2000
1969
|
});
|
|
2001
1970
|
successMsg.textContent = 'No accessibility violations found!';
|
|
@@ -2214,30 +2183,13 @@ function renderDesignReviewConfirmModal(state) {
|
|
|
2214
2183
|
const color = BUTTON_COLORS.review;
|
|
2215
2184
|
const closeModal = () => closeDesignReviewConfirm(state);
|
|
2216
2185
|
const overlay = createModalOverlay(closeModal);
|
|
2217
|
-
// Override z-index for this modal to be above others
|
|
2218
|
-
overlay.style.zIndex = '10003';
|
|
2219
2186
|
const modal = createModalBox(color);
|
|
2220
2187
|
modal.style.maxWidth = '450px';
|
|
2221
|
-
//
|
|
2222
|
-
|
|
2223
|
-
Object.assign(header.style, {
|
|
2224
|
-
display: 'flex',
|
|
2225
|
-
alignItems: 'center',
|
|
2226
|
-
justifyContent: 'space-between',
|
|
2227
|
-
padding: '14px 18px',
|
|
2228
|
-
borderBottom: `1px solid ${color}40`,
|
|
2229
|
-
backgroundColor: `${color}15`,
|
|
2230
|
-
});
|
|
2231
|
-
const title = document.createElement('span');
|
|
2232
|
-
Object.assign(title.style, { color, fontSize: '0.875rem', fontWeight: '600' });
|
|
2233
|
-
title.textContent = 'AI Design Review';
|
|
2234
|
-
header.appendChild(title);
|
|
2235
|
-
header.appendChild(createCloseButton(closeModal));
|
|
2236
|
-
modal.appendChild(header);
|
|
2188
|
+
// Minimal header (title + close only, no Copy MD / Save)
|
|
2189
|
+
modal.appendChild(createModalHeader({ color, title: 'AI Design Review', onClose: closeModal }));
|
|
2237
2190
|
// Content
|
|
2238
|
-
const content =
|
|
2191
|
+
const content = createModalContent();
|
|
2239
2192
|
Object.assign(content.style, {
|
|
2240
|
-
padding: '18px',
|
|
2241
2193
|
color: CSS_COLORS.text,
|
|
2242
2194
|
fontSize: '0.8125rem',
|
|
2243
2195
|
lineHeight: '1.6',
|
|
@@ -2252,23 +2204,22 @@ function renderDesignReviewConfirmModal(state) {
|
|
|
2252
2204
|
content.appendChild(renderApiKeyConfiguredContent(state));
|
|
2253
2205
|
}
|
|
2254
2206
|
modal.appendChild(content);
|
|
2255
|
-
// Footer with
|
|
2256
|
-
const footer = document.createElement('div');
|
|
2257
|
-
Object.assign(footer.style, {
|
|
2258
|
-
display: 'flex',
|
|
2259
|
-
justifyContent: 'flex-end',
|
|
2260
|
-
gap: '10px',
|
|
2261
|
-
padding: '14px 18px',
|
|
2262
|
-
borderTop: `1px solid ${CSS_COLORS.border}`,
|
|
2263
|
-
});
|
|
2264
|
-
footer.appendChild(createCloseButton(closeModal, 'Cancel'));
|
|
2207
|
+
// Footer with action button
|
|
2265
2208
|
if (state.apiKeyStatus?.configured) {
|
|
2209
|
+
const footer = document.createElement('div');
|
|
2210
|
+
Object.assign(footer.style, {
|
|
2211
|
+
display: 'flex',
|
|
2212
|
+
justifyContent: 'flex-end',
|
|
2213
|
+
gap: '10px',
|
|
2214
|
+
padding: '14px 18px',
|
|
2215
|
+
borderTop: `1px solid ${CSS_COLORS.border}`,
|
|
2216
|
+
});
|
|
2266
2217
|
const proceedBtn = createStyledButton({ color, text: 'Run Review', padding: '8px 16px' });
|
|
2267
2218
|
proceedBtn.style.backgroundColor = `${color}20`;
|
|
2268
2219
|
proceedBtn.onclick = () => proceedWithDesignReview(state);
|
|
2269
2220
|
footer.appendChild(proceedBtn);
|
|
2221
|
+
modal.appendChild(footer);
|
|
2270
2222
|
}
|
|
2271
|
-
modal.appendChild(footer);
|
|
2272
2223
|
overlay.appendChild(modal);
|
|
2273
2224
|
state.overlayElement = overlay;
|
|
2274
2225
|
document.body.appendChild(overlay);
|
|
@@ -2356,9 +2307,26 @@ function renderApiKeyConfiguredContent(state) {
|
|
|
2356
2307
|
// ============================================================================
|
|
2357
2308
|
function renderSettingsPopover(state) {
|
|
2358
2309
|
const { position, accentColor } = state.options;
|
|
2310
|
+
// Transparent overlay for click-outside-to-close (consistent with other modals)
|
|
2311
|
+
const overlay = document.createElement('div');
|
|
2312
|
+
overlay.setAttribute('data-devbar', 'true');
|
|
2313
|
+
overlay.setAttribute('data-devbar-overlay', 'true');
|
|
2314
|
+
Object.assign(overlay.style, {
|
|
2315
|
+
position: 'fixed',
|
|
2316
|
+
top: '0',
|
|
2317
|
+
left: '0',
|
|
2318
|
+
right: '0',
|
|
2319
|
+
bottom: '0',
|
|
2320
|
+
zIndex: '10003',
|
|
2321
|
+
});
|
|
2322
|
+
overlay.onclick = (e) => {
|
|
2323
|
+
if (e.target === overlay) {
|
|
2324
|
+
state.showSettingsPopover = false;
|
|
2325
|
+
state.render();
|
|
2326
|
+
}
|
|
2327
|
+
};
|
|
2359
2328
|
const popover = document.createElement('div');
|
|
2360
2329
|
popover.setAttribute('data-devbar', 'true');
|
|
2361
|
-
popover.setAttribute('data-devbar-overlay', 'true');
|
|
2362
2330
|
// Position: centered over the devbar on desktop, centered on screen on mobile
|
|
2363
2331
|
const isTop = position.startsWith('top');
|
|
2364
2332
|
const popoverWidth = 480;
|
|
@@ -2411,8 +2379,9 @@ function renderSettingsPopover(state) {
|
|
|
2411
2379
|
grid.appendChild(rightCol);
|
|
2412
2380
|
popover.appendChild(grid);
|
|
2413
2381
|
popover.appendChild(createResetSection(state));
|
|
2414
|
-
|
|
2415
|
-
|
|
2382
|
+
overlay.appendChild(popover);
|
|
2383
|
+
state.overlayElement = overlay;
|
|
2384
|
+
document.body.appendChild(overlay);
|
|
2416
2385
|
}
|
|
2417
2386
|
// ============================================================================
|
|
2418
2387
|
// Settings Popover Section Builders
|
|
@@ -2443,29 +2412,18 @@ function createSettingsHeader(state) {
|
|
|
2443
2412
|
}
|
|
2444
2413
|
function createThemeSection(state) {
|
|
2445
2414
|
const { accentColor } = state.options;
|
|
2446
|
-
const color = CSS_COLORS.textSecondary;
|
|
2447
2415
|
const themeSection = createSettingsSection('Theme');
|
|
2448
2416
|
const themeOptions = document.createElement('div');
|
|
2449
2417
|
Object.assign(themeOptions.style, { display: 'flex', gap: '6px' });
|
|
2450
2418
|
const themeModes = ['system', 'dark', 'light'];
|
|
2451
2419
|
themeModes.forEach((mode) => {
|
|
2452
|
-
const btn =
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
border: `1px solid ${isActive ? accentColor : `${color}40`}`,
|
|
2458
|
-
borderRadius: '4px',
|
|
2459
|
-
color: isActive ? accentColor : color,
|
|
2460
|
-
fontSize: '0.625rem',
|
|
2461
|
-
cursor: 'pointer',
|
|
2462
|
-
textTransform: 'capitalize',
|
|
2463
|
-
transition: 'all 150ms',
|
|
2420
|
+
const btn = createSettingsRadioButton({
|
|
2421
|
+
label: mode,
|
|
2422
|
+
isActive: state.themeMode === mode,
|
|
2423
|
+
accentColor,
|
|
2424
|
+
onClick: () => setThemeMode(state, mode),
|
|
2464
2425
|
});
|
|
2465
|
-
btn.
|
|
2466
|
-
btn.onclick = () => {
|
|
2467
|
-
setThemeMode(state, mode);
|
|
2468
|
-
};
|
|
2426
|
+
btn.style.textTransform = 'capitalize';
|
|
2469
2427
|
themeOptions.appendChild(btn);
|
|
2470
2428
|
});
|
|
2471
2429
|
themeSection.appendChild(themeOptions);
|
|
@@ -2724,7 +2682,6 @@ function createDisplaySection(state) {
|
|
|
2724
2682
|
}
|
|
2725
2683
|
function createFeaturesSection(state) {
|
|
2726
2684
|
const { accentColor } = state.options;
|
|
2727
|
-
const color = CSS_COLORS.textSecondary;
|
|
2728
2685
|
const featuresSection = createSettingsSection('Features');
|
|
2729
2686
|
featuresSection.appendChild(createToggleRow('Screenshot Button', state.options.showScreenshot, accentColor, () => {
|
|
2730
2687
|
state.options.showScreenshot = !state.options.showScreenshot;
|
|
@@ -2760,31 +2717,19 @@ function createFeaturesSection(state) {
|
|
|
2760
2717
|
{ value: 'local', label: 'Local' },
|
|
2761
2718
|
];
|
|
2762
2719
|
saveLocChoices.forEach(({ value, label }) => {
|
|
2763
|
-
const btn = document.createElement('button');
|
|
2764
|
-
const isActive = state.options.saveLocation === value;
|
|
2765
2720
|
const isLocalDisabled = value === 'local' && !state.sweetlinkConnected;
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2721
|
+
const btn = createSettingsRadioButton({
|
|
2722
|
+
label,
|
|
2723
|
+
isActive: state.options.saveLocation === value,
|
|
2724
|
+
accentColor,
|
|
2725
|
+
disabled: isLocalDisabled,
|
|
2726
|
+
disabledTitle: 'Sweetlink not connected',
|
|
2727
|
+
onClick: () => {
|
|
2728
|
+
state.options.saveLocation = value;
|
|
2729
|
+
state.settingsManager.saveSettings({ saveLocation: value });
|
|
2730
|
+
state.render();
|
|
2731
|
+
},
|
|
2776
2732
|
});
|
|
2777
|
-
btn.textContent = label;
|
|
2778
|
-
if (isLocalDisabled) {
|
|
2779
|
-
btn.title = 'Sweetlink not connected';
|
|
2780
|
-
}
|
|
2781
|
-
btn.onclick = () => {
|
|
2782
|
-
if (isLocalDisabled)
|
|
2783
|
-
return;
|
|
2784
|
-
state.options.saveLocation = value;
|
|
2785
|
-
state.settingsManager.saveSettings({ saveLocation: value });
|
|
2786
|
-
state.render();
|
|
2787
|
-
};
|
|
2788
2733
|
saveLocOptions.appendChild(btn);
|
|
2789
2734
|
});
|
|
2790
2735
|
saveLocRow.appendChild(saveLocOptions);
|
|
@@ -2878,6 +2823,43 @@ function createSettingsSection(title, hasBorder = true) {
|
|
|
2878
2823
|
section.appendChild(sectionTitle);
|
|
2879
2824
|
return section;
|
|
2880
2825
|
}
|
|
2826
|
+
function createSettingsRadioButton(options) {
|
|
2827
|
+
const { label, isActive, accentColor, disabled, disabledTitle, onClick } = options;
|
|
2828
|
+
const color = CSS_COLORS.textSecondary;
|
|
2829
|
+
const btn = document.createElement('button');
|
|
2830
|
+
Object.assign(btn.style, {
|
|
2831
|
+
padding: '4px 10px',
|
|
2832
|
+
backgroundColor: isActive ? `${accentColor}20` : 'transparent',
|
|
2833
|
+
border: `1px solid ${isActive ? accentColor : 'transparent'}`,
|
|
2834
|
+
borderRadius: '4px',
|
|
2835
|
+
color: isActive ? accentColor : color,
|
|
2836
|
+
fontFamily: FONT_MONO,
|
|
2837
|
+
fontSize: '0.625rem',
|
|
2838
|
+
cursor: disabled ? 'not-allowed' : 'pointer',
|
|
2839
|
+
transition: 'all 150ms',
|
|
2840
|
+
opacity: disabled ? '0.5' : '1',
|
|
2841
|
+
});
|
|
2842
|
+
btn.textContent = label;
|
|
2843
|
+
if (disabled) {
|
|
2844
|
+
if (disabledTitle)
|
|
2845
|
+
btn.title = disabledTitle;
|
|
2846
|
+
}
|
|
2847
|
+
else if (!isActive) {
|
|
2848
|
+
btn.onmouseenter = () => {
|
|
2849
|
+
btn.style.borderColor = `${color}`;
|
|
2850
|
+
btn.style.backgroundColor = `${color}10`;
|
|
2851
|
+
};
|
|
2852
|
+
btn.onmouseleave = () => {
|
|
2853
|
+
btn.style.borderColor = 'transparent';
|
|
2854
|
+
btn.style.backgroundColor = 'transparent';
|
|
2855
|
+
};
|
|
2856
|
+
}
|
|
2857
|
+
btn.onclick = () => {
|
|
2858
|
+
if (!disabled)
|
|
2859
|
+
onClick();
|
|
2860
|
+
};
|
|
2861
|
+
return btn;
|
|
2862
|
+
}
|
|
2881
2863
|
function createToggleRow(label, checked, accentColor, onChange) {
|
|
2882
2864
|
const row = document.createElement('div');
|
|
2883
2865
|
Object.assign(row.style, {
|