@aprovan/bobbin 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +16 -0
- package/LICENSE +373 -0
- package/dist/index.d.ts +402 -0
- package/dist/index.js +3704 -0
- package/package.json +30 -0
- package/src/Bobbin.tsx +89 -0
- package/src/components/EditPanel/EditPanel.tsx +376 -0
- package/src/components/EditPanel/controls/ColorPicker.tsx +138 -0
- package/src/components/EditPanel/controls/QuickSelectDropdown.tsx +142 -0
- package/src/components/EditPanel/controls/SliderInput.tsx +94 -0
- package/src/components/EditPanel/controls/SpacingControl.tsx +285 -0
- package/src/components/EditPanel/controls/ToggleGroup.tsx +37 -0
- package/src/components/EditPanel/controls/TokenDropdown.tsx +33 -0
- package/src/components/EditPanel/sections/AnnotationSection.tsx +136 -0
- package/src/components/EditPanel/sections/BackgroundSection.tsx +79 -0
- package/src/components/EditPanel/sections/EffectsSection.tsx +85 -0
- package/src/components/EditPanel/sections/LayoutSection.tsx +224 -0
- package/src/components/EditPanel/sections/SectionWrapper.tsx +57 -0
- package/src/components/EditPanel/sections/SizeSection.tsx +166 -0
- package/src/components/EditPanel/sections/SpacingSection.tsx +69 -0
- package/src/components/EditPanel/sections/TypographySection.tsx +148 -0
- package/src/components/Inspector/Inspector.tsx +221 -0
- package/src/components/Overlay/ControlHandles.tsx +572 -0
- package/src/components/Overlay/MarginPaddingOverlay.tsx +229 -0
- package/src/components/Overlay/SelectionOverlay.tsx +73 -0
- package/src/components/Pill/Pill.tsx +155 -0
- package/src/components/ThemeToggle/ThemeToggle.tsx +72 -0
- package/src/core/changeSerializer.ts +139 -0
- package/src/core/useBobbin.ts +399 -0
- package/src/core/useChangeTracker.ts +186 -0
- package/src/core/useClipboard.ts +21 -0
- package/src/core/useElementSelection.ts +146 -0
- package/src/index.ts +46 -0
- package/src/tokens/borders.ts +19 -0
- package/src/tokens/colors.ts +150 -0
- package/src/tokens/index.ts +37 -0
- package/src/tokens/shadows.ts +10 -0
- package/src/tokens/spacing.ts +37 -0
- package/src/tokens/typography.ts +51 -0
- package/src/types.ts +157 -0
- package/src/utils/animation.ts +40 -0
- package/src/utils/dom.ts +36 -0
- package/src/utils/selectors.ts +76 -0
- package/tsconfig.json +10 -0
- package/tsup.config.ts +10 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { useState, useEffect, useRef, useCallback } from 'react';
|
|
2
|
+
import { SectionWrapper } from './SectionWrapper';
|
|
3
|
+
|
|
4
|
+
interface AnnotationSectionProps {
|
|
5
|
+
expanded: boolean;
|
|
6
|
+
onToggle: () => void;
|
|
7
|
+
onAnnotate: (content: string) => void;
|
|
8
|
+
existingAnnotation?: string;
|
|
9
|
+
hasChanges?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function AnnotationSection({
|
|
13
|
+
expanded,
|
|
14
|
+
onToggle,
|
|
15
|
+
onAnnotate,
|
|
16
|
+
existingAnnotation = '',
|
|
17
|
+
hasChanges = false,
|
|
18
|
+
}: AnnotationSectionProps) {
|
|
19
|
+
const [note, setNote] = useState(existingAnnotation);
|
|
20
|
+
const [isSaved, setIsSaved] = useState(!!existingAnnotation);
|
|
21
|
+
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
22
|
+
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
23
|
+
|
|
24
|
+
// Initialize with existing annotation
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
setNote(existingAnnotation);
|
|
27
|
+
setIsSaved(!!existingAnnotation);
|
|
28
|
+
}, [existingAnnotation]);
|
|
29
|
+
|
|
30
|
+
// Auto-save with debounce
|
|
31
|
+
const handleChange = useCallback((value: string) => {
|
|
32
|
+
setNote(value);
|
|
33
|
+
setIsSaved(false);
|
|
34
|
+
|
|
35
|
+
// Clear previous timeout
|
|
36
|
+
if (debounceRef.current) {
|
|
37
|
+
clearTimeout(debounceRef.current);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Auto-save after 500ms of no typing
|
|
41
|
+
if (value.trim()) {
|
|
42
|
+
debounceRef.current = setTimeout(() => {
|
|
43
|
+
onAnnotate(value.trim());
|
|
44
|
+
setIsSaved(true);
|
|
45
|
+
}, 500);
|
|
46
|
+
}
|
|
47
|
+
}, [onAnnotate]);
|
|
48
|
+
|
|
49
|
+
// Cleanup timeout on unmount
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
return () => {
|
|
52
|
+
if (debounceRef.current) {
|
|
53
|
+
clearTimeout(debounceRef.current);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}, []);
|
|
57
|
+
|
|
58
|
+
// Save immediately on blur if there's unsaved content
|
|
59
|
+
const handleBlur = () => {
|
|
60
|
+
if (debounceRef.current) {
|
|
61
|
+
clearTimeout(debounceRef.current);
|
|
62
|
+
}
|
|
63
|
+
if (note.trim() && !isSaved) {
|
|
64
|
+
onAnnotate(note.trim());
|
|
65
|
+
setIsSaved(true);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const showActiveState = !!(note.trim() && isSaved);
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<SectionWrapper
|
|
73
|
+
title="Annotation"
|
|
74
|
+
expanded={expanded}
|
|
75
|
+
onToggle={onToggle}
|
|
76
|
+
hasChanges={hasChanges || showActiveState}
|
|
77
|
+
>
|
|
78
|
+
<div
|
|
79
|
+
style={{
|
|
80
|
+
position: 'relative',
|
|
81
|
+
borderRadius: '4px',
|
|
82
|
+
border: `1px solid ${showActiveState ? '#3b82f6' : '#e4e4e7'}`,
|
|
83
|
+
backgroundColor: showActiveState ? '#eff6ff' : '#ffffff',
|
|
84
|
+
transition: 'all 0.15s ease',
|
|
85
|
+
}}
|
|
86
|
+
>
|
|
87
|
+
<textarea
|
|
88
|
+
ref={textareaRef}
|
|
89
|
+
value={note}
|
|
90
|
+
onChange={(e) => handleChange(e.target.value)}
|
|
91
|
+
onBlur={handleBlur}
|
|
92
|
+
placeholder="Add a note about this element..."
|
|
93
|
+
style={{
|
|
94
|
+
width: '100%',
|
|
95
|
+
minHeight: '60px',
|
|
96
|
+
padding: '8px',
|
|
97
|
+
borderRadius: '4px',
|
|
98
|
+
border: 'none',
|
|
99
|
+
backgroundColor: 'transparent',
|
|
100
|
+
color: '#18181b',
|
|
101
|
+
fontSize: '11px',
|
|
102
|
+
resize: 'vertical',
|
|
103
|
+
fontFamily: 'system-ui, -apple-system, sans-serif',
|
|
104
|
+
outline: 'none',
|
|
105
|
+
}}
|
|
106
|
+
/>
|
|
107
|
+
{/* Save indicator */}
|
|
108
|
+
{note.trim() && (
|
|
109
|
+
<div
|
|
110
|
+
style={{
|
|
111
|
+
position: 'absolute',
|
|
112
|
+
bottom: '4px',
|
|
113
|
+
right: '4px',
|
|
114
|
+
fontSize: '9px',
|
|
115
|
+
color: isSaved ? '#3b82f6' : '#a1a1aa',
|
|
116
|
+
display: 'flex',
|
|
117
|
+
alignItems: 'center',
|
|
118
|
+
gap: '2px',
|
|
119
|
+
}}
|
|
120
|
+
>
|
|
121
|
+
{isSaved ? (
|
|
122
|
+
<>
|
|
123
|
+
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
124
|
+
<polyline points="20 6 9 17 4 12" />
|
|
125
|
+
</svg>
|
|
126
|
+
Saved
|
|
127
|
+
</>
|
|
128
|
+
) : (
|
|
129
|
+
'Saving...'
|
|
130
|
+
)}
|
|
131
|
+
</div>
|
|
132
|
+
)}
|
|
133
|
+
</div>
|
|
134
|
+
</SectionWrapper>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { DesignTokens } from '../../../types';
|
|
2
|
+
import { SectionWrapper } from './SectionWrapper';
|
|
3
|
+
import { ColorPicker } from '../controls/ColorPicker';
|
|
4
|
+
import { TokenDropdown } from '../controls/TokenDropdown';
|
|
5
|
+
|
|
6
|
+
interface BackgroundSectionProps {
|
|
7
|
+
expanded: boolean;
|
|
8
|
+
onToggle: () => void;
|
|
9
|
+
computedStyle: CSSStyleDeclaration;
|
|
10
|
+
onApplyStyle: (property: string, value: string) => void;
|
|
11
|
+
tokens: DesignTokens;
|
|
12
|
+
hasChanges?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function BackgroundSection({
|
|
16
|
+
expanded,
|
|
17
|
+
onToggle,
|
|
18
|
+
computedStyle,
|
|
19
|
+
onApplyStyle,
|
|
20
|
+
tokens,
|
|
21
|
+
hasChanges = false,
|
|
22
|
+
}: BackgroundSectionProps) {
|
|
23
|
+
const backgroundColor = computedStyle.backgroundColor;
|
|
24
|
+
const borderColor = computedStyle.borderColor;
|
|
25
|
+
const borderWidth = computedStyle.borderWidth;
|
|
26
|
+
const borderRadius = computedStyle.borderRadius;
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<SectionWrapper title="Background & Border" expanded={expanded} onToggle={onToggle} hasChanges={hasChanges}>
|
|
30
|
+
{/* Background Color */}
|
|
31
|
+
<div style={{ marginBottom: '12px' }}>
|
|
32
|
+
<label style={{ fontSize: '10px', color: '#71717a', marginBottom: '4px', display: 'block' }}>
|
|
33
|
+
Background
|
|
34
|
+
</label>
|
|
35
|
+
<ColorPicker
|
|
36
|
+
value={backgroundColor}
|
|
37
|
+
colors={tokens.colors}
|
|
38
|
+
onChange={(value) => onApplyStyle('background-color', value)}
|
|
39
|
+
/>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
{/* Border Color */}
|
|
43
|
+
<div style={{ marginBottom: '12px' }}>
|
|
44
|
+
<label style={{ fontSize: '10px', color: '#71717a', marginBottom: '4px', display: 'block' }}>
|
|
45
|
+
Border Color
|
|
46
|
+
</label>
|
|
47
|
+
<ColorPicker
|
|
48
|
+
value={borderColor}
|
|
49
|
+
colors={tokens.colors}
|
|
50
|
+
onChange={(value) => onApplyStyle('border-color', value)}
|
|
51
|
+
/>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
{/* Border Width */}
|
|
55
|
+
<div style={{ marginBottom: '12px' }}>
|
|
56
|
+
<label style={{ fontSize: '10px', color: '#71717a', marginBottom: '4px', display: 'block' }}>
|
|
57
|
+
Border Width
|
|
58
|
+
</label>
|
|
59
|
+
<TokenDropdown
|
|
60
|
+
value={borderWidth}
|
|
61
|
+
tokens={tokens.borderWidth}
|
|
62
|
+
onChange={(value) => onApplyStyle('border-width', value)}
|
|
63
|
+
/>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
{/* Border Radius */}
|
|
67
|
+
<div>
|
|
68
|
+
<label style={{ fontSize: '10px', color: '#71717a', marginBottom: '4px', display: 'block' }}>
|
|
69
|
+
Border Radius
|
|
70
|
+
</label>
|
|
71
|
+
<TokenDropdown
|
|
72
|
+
value={borderRadius}
|
|
73
|
+
tokens={tokens.borderRadius}
|
|
74
|
+
onChange={(value) => onApplyStyle('border-radius', value)}
|
|
75
|
+
/>
|
|
76
|
+
</div>
|
|
77
|
+
</SectionWrapper>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { DesignTokens } from '../../../types';
|
|
2
|
+
import { SectionWrapper } from './SectionWrapper';
|
|
3
|
+
import { QuickSelectDropdown } from '../controls/QuickSelectDropdown';
|
|
4
|
+
import { SliderInput } from '../controls/SliderInput';
|
|
5
|
+
|
|
6
|
+
interface EffectsSectionProps {
|
|
7
|
+
expanded: boolean;
|
|
8
|
+
onToggle: () => void;
|
|
9
|
+
computedStyle: CSSStyleDeclaration;
|
|
10
|
+
onApplyStyle: (property: string, value: string) => void;
|
|
11
|
+
tokens: DesignTokens;
|
|
12
|
+
hasChanges?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function EffectsSection({
|
|
16
|
+
expanded,
|
|
17
|
+
onToggle,
|
|
18
|
+
computedStyle,
|
|
19
|
+
onApplyStyle,
|
|
20
|
+
tokens,
|
|
21
|
+
hasChanges = false,
|
|
22
|
+
}: EffectsSectionProps) {
|
|
23
|
+
const boxShadow = computedStyle.boxShadow;
|
|
24
|
+
const borderRadius = computedStyle.borderRadius;
|
|
25
|
+
const borderWidth = computedStyle.borderWidth;
|
|
26
|
+
const opacity = parseFloat(computedStyle.opacity) * 100;
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<SectionWrapper title="Effects" expanded={expanded} onToggle={onToggle} hasChanges={hasChanges}>
|
|
30
|
+
{/* Border Radius */}
|
|
31
|
+
<div style={{ marginBottom: '12px' }}>
|
|
32
|
+
<label style={{ fontSize: '10px', color: '#71717a', marginBottom: '4px', display: 'block' }}>
|
|
33
|
+
Border Radius
|
|
34
|
+
</label>
|
|
35
|
+
<QuickSelectDropdown
|
|
36
|
+
value={borderRadius}
|
|
37
|
+
tokens={tokens.borderRadius}
|
|
38
|
+
quickKeys={['none', 'sm', 'md', 'lg', 'full']}
|
|
39
|
+
onChange={(value) => onApplyStyle('border-radius', value)}
|
|
40
|
+
/>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
{/* Border Width */}
|
|
44
|
+
<div style={{ marginBottom: '12px' }}>
|
|
45
|
+
<label style={{ fontSize: '10px', color: '#71717a', marginBottom: '4px', display: 'block' }}>
|
|
46
|
+
Border Width
|
|
47
|
+
</label>
|
|
48
|
+
<QuickSelectDropdown
|
|
49
|
+
value={borderWidth}
|
|
50
|
+
tokens={tokens.borderWidth}
|
|
51
|
+
quickKeys={['0', 'DEFAULT', '2', '4']}
|
|
52
|
+
onChange={(value) => onApplyStyle('border-width', value)}
|
|
53
|
+
/>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
{/* Box Shadow */}
|
|
57
|
+
<div style={{ marginBottom: '12px' }}>
|
|
58
|
+
<label style={{ fontSize: '10px', color: '#71717a', marginBottom: '4px', display: 'block' }}>
|
|
59
|
+
Shadow
|
|
60
|
+
</label>
|
|
61
|
+
<QuickSelectDropdown
|
|
62
|
+
value={boxShadow}
|
|
63
|
+
tokens={tokens.boxShadow}
|
|
64
|
+
quickKeys={['none', 'sm', 'md', 'lg']}
|
|
65
|
+
onChange={(value) => onApplyStyle('box-shadow', value)}
|
|
66
|
+
/>
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
{/* Opacity */}
|
|
70
|
+
<div>
|
|
71
|
+
<label style={{ fontSize: '10px', color: '#71717a', marginBottom: '4px', display: 'block' }}>
|
|
72
|
+
Opacity
|
|
73
|
+
</label>
|
|
74
|
+
<SliderInput
|
|
75
|
+
value={opacity}
|
|
76
|
+
min={0}
|
|
77
|
+
max={100}
|
|
78
|
+
step={1}
|
|
79
|
+
unit="%"
|
|
80
|
+
onChange={(value) => onApplyStyle('opacity', String(value / 100))}
|
|
81
|
+
/>
|
|
82
|
+
</div>
|
|
83
|
+
</SectionWrapper>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import type { DesignTokens } from '../../../types';
|
|
2
|
+
import { SectionWrapper } from './SectionWrapper';
|
|
3
|
+
import { ToggleGroup } from '../controls/ToggleGroup';
|
|
4
|
+
import { TokenDropdown } from '../controls/TokenDropdown';
|
|
5
|
+
|
|
6
|
+
// Direction icons
|
|
7
|
+
const ArrowRightIcon = () => (
|
|
8
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
9
|
+
<line x1="5" y1="12" x2="19" y2="12" />
|
|
10
|
+
<polyline points="12 5 19 12 12 19" />
|
|
11
|
+
</svg>
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
const ArrowLeftIcon = () => (
|
|
15
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
16
|
+
<line x1="19" y1="12" x2="5" y2="12" />
|
|
17
|
+
<polyline points="12 19 5 12 12 5" />
|
|
18
|
+
</svg>
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
const ArrowDownIcon = () => (
|
|
22
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
23
|
+
<line x1="12" y1="5" x2="12" y2="19" />
|
|
24
|
+
<polyline points="19 12 12 19 5 12" />
|
|
25
|
+
</svg>
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
const ArrowUpIcon = () => (
|
|
29
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
30
|
+
<line x1="12" y1="19" x2="12" y2="5" />
|
|
31
|
+
<polyline points="5 12 12 5 19 12" />
|
|
32
|
+
</svg>
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
// Justify icons
|
|
36
|
+
const JustifyStartIcon = () => (
|
|
37
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
38
|
+
<line x1="3" y1="4" x2="3" y2="20" />
|
|
39
|
+
<rect x="7" y="8" width="4" height="8" rx="1" />
|
|
40
|
+
<rect x="13" y="8" width="4" height="8" rx="1" />
|
|
41
|
+
</svg>
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const JustifyCenterIcon = () => (
|
|
45
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
46
|
+
<rect x="6" y="8" width="4" height="8" rx="1" />
|
|
47
|
+
<rect x="14" y="8" width="4" height="8" rx="1" />
|
|
48
|
+
</svg>
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const JustifyEndIcon = () => (
|
|
52
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
53
|
+
<line x1="21" y1="4" x2="21" y2="20" />
|
|
54
|
+
<rect x="7" y="8" width="4" height="8" rx="1" />
|
|
55
|
+
<rect x="13" y="8" width="4" height="8" rx="1" />
|
|
56
|
+
</svg>
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const JustifyBetweenIcon = () => (
|
|
60
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
61
|
+
<line x1="3" y1="4" x2="3" y2="20" />
|
|
62
|
+
<line x1="21" y1="4" x2="21" y2="20" />
|
|
63
|
+
<rect x="6" y="8" width="4" height="8" rx="1" />
|
|
64
|
+
<rect x="14" y="8" width="4" height="8" rx="1" />
|
|
65
|
+
</svg>
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const JustifyAroundIcon = () => (
|
|
69
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
70
|
+
<rect x="4" y="8" width="4" height="8" rx="1" />
|
|
71
|
+
<rect x="10" y="8" width="4" height="8" rx="1" />
|
|
72
|
+
<rect x="16" y="8" width="4" height="8" rx="1" />
|
|
73
|
+
</svg>
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
// Align icons
|
|
77
|
+
const AlignStartIcon = () => (
|
|
78
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
79
|
+
<line x1="4" y1="3" x2="20" y2="3" />
|
|
80
|
+
<rect x="6" y="6" width="4" height="10" rx="1" />
|
|
81
|
+
<rect x="14" y="6" width="4" height="6" rx="1" />
|
|
82
|
+
</svg>
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const AlignCenterVIcon = () => (
|
|
86
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
87
|
+
<rect x="6" y="5" width="4" height="14" rx="1" />
|
|
88
|
+
<rect x="14" y="7" width="4" height="10" rx="1" />
|
|
89
|
+
</svg>
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
const AlignEndIcon = () => (
|
|
93
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
94
|
+
<line x1="4" y1="21" x2="20" y2="21" />
|
|
95
|
+
<rect x="6" y="8" width="4" height="10" rx="1" />
|
|
96
|
+
<rect x="14" y="12" width="4" height="6" rx="1" />
|
|
97
|
+
</svg>
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const AlignStretchIcon = () => (
|
|
101
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
102
|
+
<line x1="4" y1="3" x2="20" y2="3" />
|
|
103
|
+
<line x1="4" y1="21" x2="20" y2="21" />
|
|
104
|
+
<rect x="6" y="6" width="4" height="12" rx="1" />
|
|
105
|
+
<rect x="14" y="6" width="4" height="12" rx="1" />
|
|
106
|
+
</svg>
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
interface LayoutSectionProps {
|
|
110
|
+
expanded: boolean;
|
|
111
|
+
onToggle: () => void;
|
|
112
|
+
computedStyle: CSSStyleDeclaration;
|
|
113
|
+
onApplyStyle: (property: string, value: string) => void;
|
|
114
|
+
tokens: DesignTokens;
|
|
115
|
+
hasChanges?: boolean;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function LayoutSection({
|
|
119
|
+
expanded,
|
|
120
|
+
onToggle,
|
|
121
|
+
computedStyle,
|
|
122
|
+
onApplyStyle,
|
|
123
|
+
tokens,
|
|
124
|
+
hasChanges = false,
|
|
125
|
+
}: LayoutSectionProps) {
|
|
126
|
+
const display = computedStyle.display;
|
|
127
|
+
const flexDirection = computedStyle.flexDirection;
|
|
128
|
+
const justifyContent = computedStyle.justifyContent;
|
|
129
|
+
const alignItems = computedStyle.alignItems;
|
|
130
|
+
const gap = computedStyle.gap;
|
|
131
|
+
|
|
132
|
+
const isFlex = display === 'flex' || display === 'inline-flex';
|
|
133
|
+
const isGrid = display === 'grid' || display === 'inline-grid';
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<SectionWrapper title="Layout" expanded={expanded} onToggle={onToggle} hasChanges={hasChanges}>
|
|
137
|
+
{/* Display */}
|
|
138
|
+
<div style={{ marginBottom: '12px' }}>
|
|
139
|
+
<label style={{ fontSize: '10px', color: '#71717a', marginBottom: '4px', display: 'block' }}>
|
|
140
|
+
Display
|
|
141
|
+
</label>
|
|
142
|
+
<ToggleGroup
|
|
143
|
+
value={display}
|
|
144
|
+
options={[
|
|
145
|
+
{ value: 'block', label: 'Block' },
|
|
146
|
+
{ value: 'flex', label: 'Flex' },
|
|
147
|
+
{ value: 'grid', label: 'Grid' },
|
|
148
|
+
{ value: 'inline', label: 'Inline' },
|
|
149
|
+
{ value: 'none', label: 'None' },
|
|
150
|
+
]}
|
|
151
|
+
onChange={(value) => onApplyStyle('display', value)}
|
|
152
|
+
/>
|
|
153
|
+
</div>
|
|
154
|
+
|
|
155
|
+
{/* Flex-specific controls */}
|
|
156
|
+
{isFlex && (
|
|
157
|
+
<>
|
|
158
|
+
<div style={{ marginBottom: '12px' }}>
|
|
159
|
+
<label style={{ fontSize: '10px', color: '#71717a', marginBottom: '4px', display: 'block' }}>
|
|
160
|
+
Direction
|
|
161
|
+
</label>
|
|
162
|
+
<ToggleGroup
|
|
163
|
+
value={flexDirection}
|
|
164
|
+
options={[
|
|
165
|
+
{ value: 'row', label: <ArrowRightIcon /> },
|
|
166
|
+
{ value: 'row-reverse', label: <ArrowLeftIcon /> },
|
|
167
|
+
{ value: 'column', label: <ArrowDownIcon /> },
|
|
168
|
+
{ value: 'column-reverse', label: <ArrowUpIcon /> },
|
|
169
|
+
]}
|
|
170
|
+
onChange={(value) => onApplyStyle('flex-direction', value)}
|
|
171
|
+
/>
|
|
172
|
+
</div>
|
|
173
|
+
|
|
174
|
+
<div style={{ marginBottom: '12px' }}>
|
|
175
|
+
<label style={{ fontSize: '10px', color: '#71717a', marginBottom: '4px', display: 'block' }}>
|
|
176
|
+
Justify
|
|
177
|
+
</label>
|
|
178
|
+
<ToggleGroup
|
|
179
|
+
value={justifyContent}
|
|
180
|
+
options={[
|
|
181
|
+
{ value: 'flex-start', label: <JustifyStartIcon /> },
|
|
182
|
+
{ value: 'center', label: <JustifyCenterIcon /> },
|
|
183
|
+
{ value: 'flex-end', label: <JustifyEndIcon /> },
|
|
184
|
+
{ value: 'space-between', label: <JustifyBetweenIcon /> },
|
|
185
|
+
{ value: 'space-around', label: <JustifyAroundIcon /> },
|
|
186
|
+
]}
|
|
187
|
+
onChange={(value) => onApplyStyle('justify-content', value)}
|
|
188
|
+
/>
|
|
189
|
+
</div>
|
|
190
|
+
|
|
191
|
+
<div style={{ marginBottom: '12px' }}>
|
|
192
|
+
<label style={{ fontSize: '10px', color: '#71717a', marginBottom: '4px', display: 'block' }}>
|
|
193
|
+
Align
|
|
194
|
+
</label>
|
|
195
|
+
<ToggleGroup
|
|
196
|
+
value={alignItems}
|
|
197
|
+
options={[
|
|
198
|
+
{ value: 'flex-start', label: <AlignStartIcon /> },
|
|
199
|
+
{ value: 'center', label: <AlignCenterVIcon /> },
|
|
200
|
+
{ value: 'flex-end', label: <AlignEndIcon /> },
|
|
201
|
+
{ value: 'stretch', label: <AlignStretchIcon /> },
|
|
202
|
+
]}
|
|
203
|
+
onChange={(value) => onApplyStyle('align-items', value)}
|
|
204
|
+
/>
|
|
205
|
+
</div>
|
|
206
|
+
</>
|
|
207
|
+
)}
|
|
208
|
+
|
|
209
|
+
{/* Gap (for flex and grid) */}
|
|
210
|
+
{(isFlex || isGrid) && (
|
|
211
|
+
<div style={{ marginBottom: '12px' }}>
|
|
212
|
+
<label style={{ fontSize: '10px', color: '#71717a', marginBottom: '4px', display: 'block' }}>
|
|
213
|
+
Gap
|
|
214
|
+
</label>
|
|
215
|
+
<TokenDropdown
|
|
216
|
+
value={gap}
|
|
217
|
+
tokens={tokens.spacing}
|
|
218
|
+
onChange={(value) => onApplyStyle('gap', value)}
|
|
219
|
+
/>
|
|
220
|
+
</div>
|
|
221
|
+
)}
|
|
222
|
+
</SectionWrapper>
|
|
223
|
+
);
|
|
224
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
interface SectionWrapperProps {
|
|
4
|
+
title: string;
|
|
5
|
+
expanded: boolean;
|
|
6
|
+
onToggle: () => void;
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
hasChanges?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function SectionWrapper({ title, expanded, onToggle, children, hasChanges = false }: SectionWrapperProps) {
|
|
12
|
+
return (
|
|
13
|
+
<div style={{ borderBottom: '1px solid #f4f4f5' }}>
|
|
14
|
+
<button
|
|
15
|
+
onClick={onToggle}
|
|
16
|
+
style={{
|
|
17
|
+
width: '100%',
|
|
18
|
+
padding: '8px 12px',
|
|
19
|
+
display: 'flex',
|
|
20
|
+
alignItems: 'center',
|
|
21
|
+
justifyContent: 'space-between',
|
|
22
|
+
backgroundColor: hasChanges ? '#eff6ff' : 'transparent',
|
|
23
|
+
border: 'none',
|
|
24
|
+
cursor: 'pointer',
|
|
25
|
+
color: '#18181b',
|
|
26
|
+
fontSize: '11px',
|
|
27
|
+
fontWeight: 500,
|
|
28
|
+
textTransform: 'uppercase',
|
|
29
|
+
letterSpacing: '0.05em',
|
|
30
|
+
}}
|
|
31
|
+
>
|
|
32
|
+
<span style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
|
33
|
+
{title}
|
|
34
|
+
{hasChanges && (
|
|
35
|
+
<span
|
|
36
|
+
style={{
|
|
37
|
+
width: '6px',
|
|
38
|
+
height: '6px',
|
|
39
|
+
borderRadius: '50%',
|
|
40
|
+
backgroundColor: '#3b82f6',
|
|
41
|
+
}}
|
|
42
|
+
title="Has unsaved changes"
|
|
43
|
+
/>
|
|
44
|
+
)}
|
|
45
|
+
</span>
|
|
46
|
+
<span style={{ fontSize: '10px', color: '#a1a1aa' }}>
|
|
47
|
+
{expanded ? '▲' : '▼'}
|
|
48
|
+
</span>
|
|
49
|
+
</button>
|
|
50
|
+
{expanded && (
|
|
51
|
+
<div style={{ padding: '0 12px 12px' }}>
|
|
52
|
+
{children}
|
|
53
|
+
</div>
|
|
54
|
+
)}
|
|
55
|
+
</div>
|
|
56
|
+
);
|
|
57
|
+
}
|