@aprovan/bobbin 0.1.0-dev.03aaf5b

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/.turbo/turbo-build.log +16 -0
  2. package/LICENSE +373 -0
  3. package/dist/index.d.ts +402 -0
  4. package/dist/index.js +3704 -0
  5. package/package.json +30 -0
  6. package/src/Bobbin.tsx +89 -0
  7. package/src/components/EditPanel/EditPanel.tsx +376 -0
  8. package/src/components/EditPanel/controls/ColorPicker.tsx +138 -0
  9. package/src/components/EditPanel/controls/QuickSelectDropdown.tsx +142 -0
  10. package/src/components/EditPanel/controls/SliderInput.tsx +94 -0
  11. package/src/components/EditPanel/controls/SpacingControl.tsx +285 -0
  12. package/src/components/EditPanel/controls/ToggleGroup.tsx +37 -0
  13. package/src/components/EditPanel/controls/TokenDropdown.tsx +33 -0
  14. package/src/components/EditPanel/sections/AnnotationSection.tsx +136 -0
  15. package/src/components/EditPanel/sections/BackgroundSection.tsx +79 -0
  16. package/src/components/EditPanel/sections/EffectsSection.tsx +85 -0
  17. package/src/components/EditPanel/sections/LayoutSection.tsx +224 -0
  18. package/src/components/EditPanel/sections/SectionWrapper.tsx +57 -0
  19. package/src/components/EditPanel/sections/SizeSection.tsx +166 -0
  20. package/src/components/EditPanel/sections/SpacingSection.tsx +69 -0
  21. package/src/components/EditPanel/sections/TypographySection.tsx +148 -0
  22. package/src/components/Inspector/Inspector.tsx +221 -0
  23. package/src/components/Overlay/ControlHandles.tsx +572 -0
  24. package/src/components/Overlay/MarginPaddingOverlay.tsx +229 -0
  25. package/src/components/Overlay/SelectionOverlay.tsx +73 -0
  26. package/src/components/Pill/Pill.tsx +155 -0
  27. package/src/components/ThemeToggle/ThemeToggle.tsx +72 -0
  28. package/src/core/changeSerializer.ts +139 -0
  29. package/src/core/useBobbin.ts +399 -0
  30. package/src/core/useChangeTracker.ts +186 -0
  31. package/src/core/useClipboard.ts +21 -0
  32. package/src/core/useElementSelection.ts +146 -0
  33. package/src/index.ts +46 -0
  34. package/src/tokens/borders.ts +19 -0
  35. package/src/tokens/colors.ts +150 -0
  36. package/src/tokens/index.ts +37 -0
  37. package/src/tokens/shadows.ts +10 -0
  38. package/src/tokens/spacing.ts +37 -0
  39. package/src/tokens/typography.ts +51 -0
  40. package/src/types.ts +157 -0
  41. package/src/utils/animation.ts +40 -0
  42. package/src/utils/dom.ts +36 -0
  43. package/src/utils/selectors.ts +76 -0
  44. package/tsconfig.json +10 -0
  45. package/tsup.config.ts +10 -0
@@ -0,0 +1,166 @@
1
+ import { useState } from 'react';
2
+ import type { DesignTokens } from '../../../types';
3
+ import { SectionWrapper } from './SectionWrapper';
4
+ import { SliderInput } from '../controls/SliderInput';
5
+
6
+ // Common size presets
7
+ const sizePresets: Record<string, string> = {
8
+ 'auto': 'auto',
9
+ 'full': '100%',
10
+ 'fit': 'fit-content',
11
+ 'min': 'min-content',
12
+ 'max': 'max-content',
13
+ 'screen': '100vh',
14
+ };
15
+
16
+ interface SizeSectionProps {
17
+ expanded: boolean;
18
+ onToggle: () => void;
19
+ computedStyle: CSSStyleDeclaration;
20
+ onApplyStyle: (property: string, value: string) => void;
21
+ tokens: DesignTokens;
22
+ hasChanges?: boolean;
23
+ }
24
+
25
+ // Component for size input with quick presets and slider
26
+ function SizeInput({
27
+ label,
28
+ value,
29
+ property,
30
+ onApplyStyle,
31
+ }: {
32
+ label: string;
33
+ value: string;
34
+ property: string;
35
+ onApplyStyle: (property: string, value: string) => void;
36
+ }) {
37
+ const [showSlider, setShowSlider] = useState(false);
38
+ const numericValue = parseFloat(value) || 0;
39
+
40
+ // Check if current value matches a preset
41
+ const isPresetSelected = (presetValue: string) => {
42
+ return value.toLowerCase() === presetValue.toLowerCase();
43
+ };
44
+
45
+ return (
46
+ <div style={{ marginBottom: '10px' }}>
47
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '4px' }}>
48
+ <label style={{ fontSize: '10px', color: '#71717a' }}>{label}</label>
49
+ <button
50
+ onClick={() => setShowSlider(!showSlider)}
51
+ style={{
52
+ padding: '1px 4px',
53
+ borderRadius: '2px',
54
+ border: '1px solid #e4e4e7',
55
+ backgroundColor: showSlider ? '#18181b' : '#ffffff',
56
+ color: showSlider ? '#fafafa' : '#71717a',
57
+ fontSize: '8px',
58
+ cursor: 'pointer',
59
+ }}
60
+ title="Toggle custom size slider"
61
+ >
62
+ px
63
+ </button>
64
+ </div>
65
+
66
+ {/* Quick presets */}
67
+ <div style={{ display: 'flex', gap: '2px', flexWrap: 'wrap', marginBottom: showSlider ? '6px' : 0 }}>
68
+ {Object.entries(sizePresets).slice(0, 5).map(([key, presetValue]) => (
69
+ <button
70
+ key={key}
71
+ onClick={() => onApplyStyle(property, presetValue)}
72
+ style={{
73
+ padding: '2px 5px',
74
+ borderRadius: '3px',
75
+ border: '1px solid',
76
+ borderColor: isPresetSelected(presetValue) ? '#18181b' : '#e4e4e7',
77
+ backgroundColor: isPresetSelected(presetValue) ? '#18181b' : '#ffffff',
78
+ color: isPresetSelected(presetValue) ? '#fafafa' : '#18181b',
79
+ fontSize: '9px',
80
+ fontFamily: 'ui-monospace, monospace',
81
+ cursor: 'pointer',
82
+ transition: 'all 0.1s ease',
83
+ }}
84
+ title={presetValue}
85
+ >
86
+ {key}
87
+ </button>
88
+ ))}
89
+ </div>
90
+
91
+ {/* Slider for custom values */}
92
+ {showSlider && (
93
+ <SliderInput
94
+ value={numericValue}
95
+ min={0}
96
+ max={1000}
97
+ onChange={(v) => onApplyStyle(property, `${v}px`)}
98
+ />
99
+ )}
100
+ </div>
101
+ );
102
+ }
103
+
104
+ export function SizeSection({
105
+ expanded,
106
+ onToggle,
107
+ computedStyle,
108
+ onApplyStyle,
109
+ hasChanges = false,
110
+ }: SizeSectionProps) {
111
+ const width = computedStyle.width;
112
+ const height = computedStyle.height;
113
+ const minWidth = computedStyle.minWidth;
114
+ const maxWidth = computedStyle.maxWidth;
115
+ const minHeight = computedStyle.minHeight;
116
+ const maxHeight = computedStyle.maxHeight;
117
+
118
+ return (
119
+ <SectionWrapper title="Size" expanded={expanded} onToggle={onToggle} hasChanges={hasChanges}>
120
+ <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px' }}>
121
+ <SizeInput
122
+ label="Width"
123
+ value={width}
124
+ property="width"
125
+ onApplyStyle={onApplyStyle}
126
+ />
127
+ <SizeInput
128
+ label="Height"
129
+ value={height}
130
+ property="height"
131
+ onApplyStyle={onApplyStyle}
132
+ />
133
+ </div>
134
+
135
+ <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px' }}>
136
+ <SizeInput
137
+ label="Min W"
138
+ value={minWidth}
139
+ property="min-width"
140
+ onApplyStyle={onApplyStyle}
141
+ />
142
+ <SizeInput
143
+ label="Max W"
144
+ value={maxWidth}
145
+ property="max-width"
146
+ onApplyStyle={onApplyStyle}
147
+ />
148
+ </div>
149
+
150
+ <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px' }}>
151
+ <SizeInput
152
+ label="Min H"
153
+ value={minHeight}
154
+ property="min-height"
155
+ onApplyStyle={onApplyStyle}
156
+ />
157
+ <SizeInput
158
+ label="Max H"
159
+ value={maxHeight}
160
+ property="max-height"
161
+ onApplyStyle={onApplyStyle}
162
+ />
163
+ </div>
164
+ </SectionWrapper>
165
+ );
166
+ }
@@ -0,0 +1,69 @@
1
+ import type { DesignTokens } from '../../../types';
2
+ import { SectionWrapper } from './SectionWrapper';
3
+ import { SpacingControl } from '../controls/SpacingControl';
4
+
5
+ interface SpacingSectionProps {
6
+ expanded: boolean;
7
+ onToggle: () => void;
8
+ computedStyle: CSSStyleDeclaration;
9
+ onApplyStyle: (property: string, value: string) => void;
10
+ tokens: DesignTokens;
11
+ hasChanges?: boolean;
12
+ changedProps?: Record<string, boolean>;
13
+ }
14
+
15
+ export function SpacingSection({
16
+ expanded,
17
+ onToggle,
18
+ computedStyle,
19
+ onApplyStyle,
20
+ hasChanges = false,
21
+ changedProps = {},
22
+ }: SpacingSectionProps) {
23
+ const margin = {
24
+ top: computedStyle.marginTop,
25
+ right: computedStyle.marginRight,
26
+ bottom: computedStyle.marginBottom,
27
+ left: computedStyle.marginLeft,
28
+ };
29
+
30
+ const padding = {
31
+ top: computedStyle.paddingTop,
32
+ right: computedStyle.paddingRight,
33
+ bottom: computedStyle.paddingBottom,
34
+ left: computedStyle.paddingLeft,
35
+ };
36
+
37
+ const marginChanges = {
38
+ top: changedProps['margin-top'],
39
+ right: changedProps['margin-right'],
40
+ bottom: changedProps['margin-bottom'],
41
+ left: changedProps['margin-left'],
42
+ };
43
+
44
+ const paddingChanges = {
45
+ top: changedProps['padding-top'],
46
+ right: changedProps['padding-right'],
47
+ bottom: changedProps['padding-bottom'],
48
+ left: changedProps['padding-left'],
49
+ };
50
+
51
+ return (
52
+ <SectionWrapper title="Spacing" expanded={expanded} onToggle={onToggle} hasChanges={hasChanges}>
53
+ <div style={{ marginBottom: '16px' }}>
54
+ <SpacingControl
55
+ label="Margin"
56
+ values={margin}
57
+ onChange={(side, value) => onApplyStyle(`margin-${side}`, value)}
58
+ hasChanges={marginChanges}
59
+ />
60
+ </div>
61
+ <SpacingControl
62
+ label="Padding"
63
+ values={padding}
64
+ onChange={(side, value) => onApplyStyle(`padding-${side}`, value)}
65
+ hasChanges={paddingChanges}
66
+ />
67
+ </SectionWrapper>
68
+ );
69
+ }
@@ -0,0 +1,148 @@
1
+ import type { DesignTokens } from '../../../types';
2
+ import { SectionWrapper } from './SectionWrapper';
3
+ import { TokenDropdown } from '../controls/TokenDropdown';
4
+ import { QuickSelectDropdown } from '../controls/QuickSelectDropdown';
5
+ import { ColorPicker } from '../controls/ColorPicker';
6
+ import { ToggleGroup } from '../controls/ToggleGroup';
7
+
8
+ // Text alignment icons
9
+ const AlignLeftIcon = () => (
10
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
11
+ <line x1="3" y1="6" x2="21" y2="6" />
12
+ <line x1="3" y1="12" x2="15" y2="12" />
13
+ <line x1="3" y1="18" x2="18" y2="18" />
14
+ </svg>
15
+ );
16
+
17
+ const AlignCenterIcon = () => (
18
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
19
+ <line x1="3" y1="6" x2="21" y2="6" />
20
+ <line x1="6" y1="12" x2="18" y2="12" />
21
+ <line x1="4" y1="18" x2="20" y2="18" />
22
+ </svg>
23
+ );
24
+
25
+ const AlignRightIcon = () => (
26
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
27
+ <line x1="3" y1="6" x2="21" y2="6" />
28
+ <line x1="9" y1="12" x2="21" y2="12" />
29
+ <line x1="6" y1="18" x2="21" y2="18" />
30
+ </svg>
31
+ );
32
+
33
+ const AlignJustifyIcon = () => (
34
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
35
+ <line x1="3" y1="6" x2="21" y2="6" />
36
+ <line x1="3" y1="12" x2="21" y2="12" />
37
+ <line x1="3" y1="18" x2="21" y2="18" />
38
+ </svg>
39
+ );
40
+
41
+ interface TypographySectionProps {
42
+ expanded: boolean;
43
+ onToggle: () => void;
44
+ computedStyle: CSSStyleDeclaration;
45
+ onApplyStyle: (property: string, value: string) => void;
46
+ tokens: DesignTokens;
47
+ hasChanges?: boolean;
48
+ }
49
+
50
+ export function TypographySection({
51
+ expanded,
52
+ onToggle,
53
+ computedStyle,
54
+ onApplyStyle,
55
+ tokens,
56
+ hasChanges = false,
57
+ }: TypographySectionProps) {
58
+ const color = computedStyle.color;
59
+ const fontSize = computedStyle.fontSize;
60
+ const fontWeight = computedStyle.fontWeight;
61
+ const fontFamily = computedStyle.fontFamily;
62
+ const textAlign = computedStyle.textAlign;
63
+ const lineHeight = computedStyle.lineHeight;
64
+
65
+ return (
66
+ <SectionWrapper title="Typography" expanded={expanded} onToggle={onToggle} hasChanges={hasChanges}>
67
+ {/* Color */}
68
+ <div style={{ marginBottom: '12px' }}>
69
+ <label style={{ fontSize: '10px', color: '#71717a', marginBottom: '4px', display: 'block' }}>
70
+ Color
71
+ </label>
72
+ <ColorPicker
73
+ value={color}
74
+ colors={tokens.colors}
75
+ onChange={(value) => onApplyStyle('color', value)}
76
+ />
77
+ </div>
78
+
79
+ {/* Font Size */}
80
+ <div style={{ marginBottom: '12px' }}>
81
+ <label style={{ fontSize: '10px', color: '#71717a', marginBottom: '4px', display: 'block' }}>
82
+ Font Size
83
+ </label>
84
+ <QuickSelectDropdown
85
+ value={fontSize}
86
+ tokens={tokens.fontSize}
87
+ quickKeys={['xs', 'sm', 'base', 'lg', 'xl', '2xl']}
88
+ onChange={(value) => onApplyStyle('font-size', value)}
89
+ />
90
+ </div>
91
+
92
+ {/* Font Weight */}
93
+ <div style={{ marginBottom: '12px' }}>
94
+ <label style={{ fontSize: '10px', color: '#71717a', marginBottom: '4px', display: 'block' }}>
95
+ Font Weight
96
+ </label>
97
+ <QuickSelectDropdown
98
+ value={fontWeight}
99
+ tokens={tokens.fontWeight}
100
+ quickKeys={['light', 'normal', 'medium', 'semibold', 'bold']}
101
+ onChange={(value) => onApplyStyle('font-weight', value)}
102
+ />
103
+ </div>
104
+
105
+ {/* Font Family */}
106
+ <div style={{ marginBottom: '12px' }}>
107
+ <label style={{ fontSize: '10px', color: '#71717a', marginBottom: '4px', display: 'block' }}>
108
+ Font Family
109
+ </label>
110
+ <TokenDropdown
111
+ value={fontFamily}
112
+ tokens={tokens.fontFamily}
113
+ onChange={(value) => onApplyStyle('font-family', value)}
114
+ />
115
+ </div>
116
+
117
+ {/* Text Align */}
118
+ <div style={{ marginBottom: '12px' }}>
119
+ <label style={{ fontSize: '10px', color: '#71717a', marginBottom: '4px', display: 'block' }}>
120
+ Text Align
121
+ </label>
122
+ <ToggleGroup
123
+ value={textAlign}
124
+ options={[
125
+ { value: 'left', label: <AlignLeftIcon /> },
126
+ { value: 'center', label: <AlignCenterIcon /> },
127
+ { value: 'right', label: <AlignRightIcon /> },
128
+ { value: 'justify', label: <AlignJustifyIcon /> },
129
+ ]}
130
+ onChange={(value) => onApplyStyle('text-align', value)}
131
+ />
132
+ </div>
133
+
134
+ {/* Line Height */}
135
+ <div>
136
+ <label style={{ fontSize: '10px', color: '#71717a', marginBottom: '4px', display: 'block' }}>
137
+ Line Height
138
+ </label>
139
+ <QuickSelectDropdown
140
+ value={lineHeight}
141
+ tokens={tokens.lineHeight}
142
+ quickKeys={['tight', 'snug', 'normal', 'relaxed']}
143
+ onChange={(value) => onApplyStyle('line-height', value)}
144
+ />
145
+ </div>
146
+ </SectionWrapper>
147
+ );
148
+ }
@@ -0,0 +1,221 @@
1
+ import { useState, useMemo } from 'react';
2
+ import type { SelectedElement } from '../../types';
3
+
4
+ interface InspectorProps {
5
+ selectedElement: SelectedElement;
6
+ onSelectElement: (el: HTMLElement) => void;
7
+ onClose?: () => void;
8
+ zIndex?: number;
9
+ }
10
+
11
+ export function Inspector({
12
+ selectedElement,
13
+ onSelectElement,
14
+ zIndex = 9999,
15
+ }: InspectorProps) {
16
+ const [activeTab, setActiveTab] = useState<'tree' | 'styles' | 'attributes'>('attributes');
17
+ const [isMinimized, setIsMinimized] = useState(false);
18
+
19
+ const computedStyles = useMemo(() => {
20
+ const computed = window.getComputedStyle(selectedElement.element);
21
+ const styles: Record<string, string> = {};
22
+
23
+ // Get commonly-inspected properties
24
+ const properties = [
25
+ 'display', 'position', 'width', 'height', 'margin', 'padding',
26
+ 'border', 'background', 'color', 'font-family', 'font-size',
27
+ 'font-weight', 'line-height', 'text-align', 'flex', 'grid',
28
+ ];
29
+
30
+ for (const prop of properties) {
31
+ styles[prop] = computed.getPropertyValue(prop);
32
+ }
33
+
34
+ return styles;
35
+ }, [selectedElement.element]);
36
+
37
+ const attributes = useMemo(() => {
38
+ const attrs: Record<string, string> = {};
39
+ const el = selectedElement.element;
40
+ for (const attr of el.attributes) {
41
+ // Skip 'contenteditable'
42
+ if (attr.name.toLowerCase() === 'contenteditable') {
43
+ continue;
44
+ }
45
+ attrs[attr.name] = attr.value;
46
+ }
47
+ return attrs;
48
+ }, [selectedElement.element]);
49
+
50
+ // Build DOM tree path
51
+ const domPath = useMemo(() => {
52
+ const path: HTMLElement[] = [];
53
+ let el: HTMLElement | null = selectedElement.element;
54
+ while (el && el !== document.body) {
55
+ path.unshift(el);
56
+ el = el.parentElement;
57
+ }
58
+ return path;
59
+ }, [selectedElement.element]);
60
+
61
+ return (
62
+ <div
63
+ data-bobbin="inspector"
64
+ style={{
65
+ position: 'fixed',
66
+ bottom: '16px',
67
+ left: '16px',
68
+ width: isMinimized ? 'auto' : '320px',
69
+ maxHeight: isMinimized ? 'auto' : '260px',
70
+ backgroundColor: '#fafafa',
71
+ borderRadius: '8px',
72
+ border: '1px solid #e4e4e7',
73
+ boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1)',
74
+ zIndex,
75
+ overflow: 'hidden',
76
+ display: 'flex',
77
+ flexDirection: 'column',
78
+ color: '#18181b',
79
+ fontFamily: 'ui-monospace, SFMono-Regular, monospace',
80
+ fontSize: '11px',
81
+ }}
82
+ >
83
+ {/* Header with minimize/close controls */}
84
+ {isMinimized ? (
85
+ <div
86
+ style={{
87
+ padding: '6px 10px',
88
+ display: 'flex',
89
+ alignItems: 'center',
90
+ gap: '8px',
91
+ cursor: 'pointer',
92
+ backgroundColor: '#ffffff',
93
+ }}
94
+ onClick={() => setIsMinimized(false)}
95
+ >
96
+ <span style={{ fontSize: '10px', color: '#71717a' }}>Inspector</span>
97
+ </div>
98
+ ) : (
99
+ <>
100
+ {/* Tabs with close button */}
101
+ <div style={{ display: 'flex', borderBottom: '1px solid #e4e4e7', backgroundColor: '#ffffff' }}>
102
+ {(['styles', 'attributes'] as const).map(tab => (
103
+ <button
104
+ key={tab}
105
+ onClick={() => setActiveTab(tab)}
106
+ style={{
107
+ flex: 1,
108
+ padding: '6px 8px',
109
+ border: 'none',
110
+ borderBottom: activeTab === tab ? '2px solid #18181b' : '2px solid transparent',
111
+ backgroundColor: 'transparent',
112
+ color: activeTab === tab ? '#18181b' : '#71717a',
113
+ cursor: 'pointer',
114
+ textTransform: 'capitalize',
115
+ fontSize: '11px',
116
+ fontWeight: activeTab === tab ? 500 : 400,
117
+ }}
118
+ >
119
+ {tab}
120
+ </button>
121
+ ))}
122
+ <div style={{ display: 'flex', alignItems: 'center', gap: '2px', padding: '0 4px' }}>
123
+ <button
124
+ onClick={() => setIsMinimized(true)}
125
+ style={{
126
+ width: '18px',
127
+ height: '18px',
128
+ borderRadius: '3px',
129
+ border: 'none',
130
+ backgroundColor: 'transparent',
131
+ color: '#71717a',
132
+ cursor: 'pointer',
133
+ display: 'flex',
134
+ alignItems: 'center',
135
+ justifyContent: 'center',
136
+ fontSize: '14px',
137
+ }}
138
+ title="Minimize"
139
+ >
140
+
141
+ </button>
142
+ </div>
143
+ </div>
144
+
145
+ {/* Content */}
146
+ <div style={{ flex: 1, overflow: 'auto', padding: '6px' }}>
147
+ {activeTab === 'tree' && (
148
+ <div>
149
+ {domPath.map((el, i) => (
150
+ <div
151
+ key={i}
152
+ onClick={() => onSelectElement(el)}
153
+ style={{
154
+ padding: '3px 6px',
155
+ paddingLeft: `${i * 10 + 6}px`,
156
+ cursor: 'pointer',
157
+ backgroundColor: el === selectedElement.element ? '#18181b' : 'transparent',
158
+ color: el === selectedElement.element ? '#fafafa' : '#18181b',
159
+ borderRadius: '3px',
160
+ marginBottom: '1px',
161
+ }}
162
+ >
163
+ <span style={{ color: el === selectedElement.element ? '#a1a1aa' : '#52525b' }}>
164
+ {el.tagName.toLowerCase()}
165
+ </span>
166
+ {el.id && (
167
+ <span style={{ color: el === selectedElement.element ? '#d4d4d8' : '#71717a' }}>
168
+ #{el.id}
169
+ </span>
170
+ )}
171
+ {el.classList.length > 0 && (
172
+ <span style={{ color: el === selectedElement.element ? '#a1a1aa' : '#a1a1aa' }}>
173
+ .{Array.from(el.classList).slice(0, 2).join('.')}
174
+ </span>
175
+ )}
176
+ </div>
177
+ ))}
178
+ </div>
179
+ )}
180
+
181
+ {activeTab === 'styles' && (
182
+ <div>
183
+ {Object.entries(computedStyles).map(([prop, value]) => (
184
+ <div
185
+ key={prop}
186
+ style={{
187
+ display: 'flex',
188
+ padding: '2px 0',
189
+ borderBottom: '1px solid #f4f4f5',
190
+ }}
191
+ >
192
+ <span style={{ color: '#52525b', width: '100px' }}>{prop}:</span>
193
+ <span style={{ color: '#18181b', flex: 1 }}>{value}</span>
194
+ </div>
195
+ ))}
196
+ </div>
197
+ )}
198
+
199
+ {activeTab === 'attributes' && (
200
+ <div>
201
+ {Object.entries(attributes).map(([name, value]) => (
202
+ <div
203
+ key={name}
204
+ style={{
205
+ display: 'flex',
206
+ padding: '2px 0',
207
+ borderBottom: '1px solid #f4f4f5',
208
+ }}
209
+ >
210
+ <span style={{ color: '#71717a', width: '80px' }}>{name}</span>
211
+ <span style={{ color: '#18181b' }}>"{value}"</span>
212
+ </div>
213
+ ))}
214
+ </div>
215
+ )}
216
+ </div>
217
+ </>
218
+ )}
219
+ </div>
220
+ );
221
+ }