@aprovan/bobbin 0.1.0-dev.6bd527d
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,146 @@
|
|
|
1
|
+
import { useState, useCallback, useEffect, useRef } from 'react';
|
|
2
|
+
import type { SelectedElement } from '../types';
|
|
3
|
+
import { getElementPath, getElementXPath } from '../utils/selectors';
|
|
4
|
+
|
|
5
|
+
export interface UseElementSelectionOptions {
|
|
6
|
+
container?: HTMLElement | null;
|
|
7
|
+
exclude?: string[];
|
|
8
|
+
enabled?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function useElementSelection(options: UseElementSelectionOptions) {
|
|
12
|
+
const { container, exclude = [], enabled = true } = options;
|
|
13
|
+
|
|
14
|
+
const [hoveredElement, setHoveredElement] = useState<SelectedElement | null>(
|
|
15
|
+
null,
|
|
16
|
+
);
|
|
17
|
+
const [selectedElement, setSelectedElement] =
|
|
18
|
+
useState<SelectedElement | null>(null);
|
|
19
|
+
const lastRectRef = useRef<DOMRect | null>(null);
|
|
20
|
+
|
|
21
|
+
const isExcluded = useCallback(
|
|
22
|
+
(el: HTMLElement): boolean => {
|
|
23
|
+
// Exclude bobbin elements themselves
|
|
24
|
+
if (el.closest('[data-bobbin]')) return true;
|
|
25
|
+
// Exclude user-specified selectors
|
|
26
|
+
return exclude.some(
|
|
27
|
+
(selector) => el.matches(selector) || el.closest(selector),
|
|
28
|
+
);
|
|
29
|
+
},
|
|
30
|
+
[exclude],
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const createSelectedElement = useCallback(
|
|
34
|
+
(el: HTMLElement): SelectedElement => {
|
|
35
|
+
return {
|
|
36
|
+
element: el,
|
|
37
|
+
rect: el.getBoundingClientRect(),
|
|
38
|
+
path: getElementPath(el),
|
|
39
|
+
xpath: getElementXPath(el),
|
|
40
|
+
tagName: el.tagName.toLowerCase(),
|
|
41
|
+
id: el.id || undefined,
|
|
42
|
+
classList: Array.from(el.classList),
|
|
43
|
+
};
|
|
44
|
+
},
|
|
45
|
+
[],
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const handleMouseMove = useCallback(
|
|
49
|
+
(e: MouseEvent) => {
|
|
50
|
+
if (!enabled) return;
|
|
51
|
+
|
|
52
|
+
const target = document.elementFromPoint(
|
|
53
|
+
e.clientX,
|
|
54
|
+
e.clientY,
|
|
55
|
+
) as HTMLElement | null;
|
|
56
|
+
if (!target || isExcluded(target)) {
|
|
57
|
+
setHoveredElement(null);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Check if within container bounds
|
|
62
|
+
if (container && !container.contains(target)) {
|
|
63
|
+
setHoveredElement(null);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
setHoveredElement(createSelectedElement(target));
|
|
68
|
+
},
|
|
69
|
+
[enabled, container, isExcluded, createSelectedElement],
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const handleClick = useCallback(
|
|
73
|
+
(e: MouseEvent) => {
|
|
74
|
+
if (!enabled) return;
|
|
75
|
+
|
|
76
|
+
// Don't intercept clicks on bobbin UI elements
|
|
77
|
+
const target = e.target as HTMLElement;
|
|
78
|
+
if (target.closest('[data-bobbin]')) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!hoveredElement) return;
|
|
83
|
+
|
|
84
|
+
e.preventDefault();
|
|
85
|
+
e.stopPropagation();
|
|
86
|
+
|
|
87
|
+
setSelectedElement(hoveredElement);
|
|
88
|
+
lastRectRef.current = hoveredElement.rect;
|
|
89
|
+
},
|
|
90
|
+
[enabled, hoveredElement],
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
const clearSelection = useCallback(() => {
|
|
94
|
+
setSelectedElement(null);
|
|
95
|
+
setHoveredElement(null);
|
|
96
|
+
}, []);
|
|
97
|
+
|
|
98
|
+
const selectElement = useCallback(
|
|
99
|
+
(el: HTMLElement | null) => {
|
|
100
|
+
if (!el) {
|
|
101
|
+
clearSelection();
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
setSelectedElement(createSelectedElement(el));
|
|
105
|
+
},
|
|
106
|
+
[createSelectedElement, clearSelection],
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
if (!enabled) return;
|
|
111
|
+
|
|
112
|
+
document.addEventListener('mousemove', handleMouseMove, { passive: true });
|
|
113
|
+
document.addEventListener('click', handleClick, { capture: true });
|
|
114
|
+
|
|
115
|
+
return () => {
|
|
116
|
+
document.removeEventListener('mousemove', handleMouseMove);
|
|
117
|
+
document.removeEventListener('click', handleClick, { capture: true });
|
|
118
|
+
};
|
|
119
|
+
}, [enabled, handleMouseMove, handleClick]);
|
|
120
|
+
|
|
121
|
+
// Update rect on scroll/resize
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
if (!selectedElement) return;
|
|
124
|
+
|
|
125
|
+
const updateRect = () => {
|
|
126
|
+
const newRect = selectedElement.element.getBoundingClientRect();
|
|
127
|
+
setSelectedElement((prev) => (prev ? { ...prev, rect: newRect } : null));
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
window.addEventListener('scroll', updateRect, { passive: true });
|
|
131
|
+
window.addEventListener('resize', updateRect, { passive: true });
|
|
132
|
+
|
|
133
|
+
return () => {
|
|
134
|
+
window.removeEventListener('scroll', updateRect);
|
|
135
|
+
window.removeEventListener('resize', updateRect);
|
|
136
|
+
};
|
|
137
|
+
}, [selectedElement?.element]);
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
hoveredElement,
|
|
141
|
+
selectedElement,
|
|
142
|
+
selectElement,
|
|
143
|
+
clearSelection,
|
|
144
|
+
lastRect: lastRectRef.current,
|
|
145
|
+
};
|
|
146
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Main component
|
|
2
|
+
export { Bobbin } from './Bobbin';
|
|
3
|
+
export type { BobbinComponentProps } from './Bobbin';
|
|
4
|
+
|
|
5
|
+
// Hooks
|
|
6
|
+
export { useBobbin } from './core/useBobbin';
|
|
7
|
+
export { useElementSelection } from './core/useElementSelection';
|
|
8
|
+
export { useChangeTracker } from './core/useChangeTracker';
|
|
9
|
+
export { useClipboard } from './core/useClipboard';
|
|
10
|
+
|
|
11
|
+
// Utilities
|
|
12
|
+
export {
|
|
13
|
+
serializeChangesToYAML,
|
|
14
|
+
parseYAMLChangeset,
|
|
15
|
+
} from './core/changeSerializer';
|
|
16
|
+
export { getElementPath, getElementXPath, generateId } from './utils/selectors';
|
|
17
|
+
|
|
18
|
+
// Types
|
|
19
|
+
export type {
|
|
20
|
+
BobbinProps,
|
|
21
|
+
BobbinState,
|
|
22
|
+
BobbinActions,
|
|
23
|
+
SelectedElement,
|
|
24
|
+
Change,
|
|
25
|
+
ChangeType,
|
|
26
|
+
StyleChange,
|
|
27
|
+
TextChange,
|
|
28
|
+
MoveChange,
|
|
29
|
+
Annotation,
|
|
30
|
+
DesignTokens,
|
|
31
|
+
BobbinChangeset,
|
|
32
|
+
} from './types';
|
|
33
|
+
|
|
34
|
+
// Tokens
|
|
35
|
+
export { defaultTokens } from './tokens';
|
|
36
|
+
export { colors } from './tokens/colors';
|
|
37
|
+
export { spacing } from './tokens/spacing';
|
|
38
|
+
export {
|
|
39
|
+
fontSize,
|
|
40
|
+
fontWeight,
|
|
41
|
+
fontFamily,
|
|
42
|
+
lineHeight,
|
|
43
|
+
letterSpacing,
|
|
44
|
+
} from './tokens/typography';
|
|
45
|
+
export { borderRadius, borderWidth } from './tokens/borders';
|
|
46
|
+
export { boxShadow } from './tokens/shadows';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export const borderRadius: Record<string, string> = {
|
|
2
|
+
none: '0px',
|
|
3
|
+
sm: '0.125rem',
|
|
4
|
+
DEFAULT: '0.25rem',
|
|
5
|
+
md: '0.375rem',
|
|
6
|
+
lg: '0.5rem',
|
|
7
|
+
xl: '0.75rem',
|
|
8
|
+
'2xl': '1rem',
|
|
9
|
+
'3xl': '1.5rem',
|
|
10
|
+
full: '9999px',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const borderWidth: Record<string, string> = {
|
|
14
|
+
'0': '0px',
|
|
15
|
+
DEFAULT: '1px',
|
|
16
|
+
'2': '2px',
|
|
17
|
+
'4': '4px',
|
|
18
|
+
'8': '8px',
|
|
19
|
+
};
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
export const colors = {
|
|
2
|
+
slate: {
|
|
3
|
+
50: '#f8fafc',
|
|
4
|
+
100: '#f1f5f9',
|
|
5
|
+
200: '#e2e8f0',
|
|
6
|
+
300: '#cbd5e1',
|
|
7
|
+
400: '#94a3b8',
|
|
8
|
+
500: '#64748b',
|
|
9
|
+
600: '#475569',
|
|
10
|
+
700: '#334155',
|
|
11
|
+
800: '#1e293b',
|
|
12
|
+
900: '#0f172a',
|
|
13
|
+
950: '#020617',
|
|
14
|
+
},
|
|
15
|
+
gray: {
|
|
16
|
+
50: '#f9fafb',
|
|
17
|
+
100: '#f3f4f6',
|
|
18
|
+
200: '#e5e7eb',
|
|
19
|
+
300: '#d1d5db',
|
|
20
|
+
400: '#9ca3af',
|
|
21
|
+
500: '#6b7280',
|
|
22
|
+
600: '#4b5563',
|
|
23
|
+
700: '#374151',
|
|
24
|
+
800: '#1f2937',
|
|
25
|
+
900: '#111827',
|
|
26
|
+
950: '#030712',
|
|
27
|
+
},
|
|
28
|
+
zinc: {
|
|
29
|
+
50: '#fafafa',
|
|
30
|
+
100: '#f4f4f5',
|
|
31
|
+
200: '#e4e4e7',
|
|
32
|
+
300: '#d4d4d8',
|
|
33
|
+
400: '#a1a1aa',
|
|
34
|
+
500: '#71717a',
|
|
35
|
+
600: '#52525b',
|
|
36
|
+
700: '#3f3f46',
|
|
37
|
+
800: '#27272a',
|
|
38
|
+
900: '#18181b',
|
|
39
|
+
950: '#09090b',
|
|
40
|
+
},
|
|
41
|
+
red: {
|
|
42
|
+
50: '#fef2f2',
|
|
43
|
+
100: '#fee2e2',
|
|
44
|
+
200: '#fecaca',
|
|
45
|
+
300: '#fca5a5',
|
|
46
|
+
400: '#f87171',
|
|
47
|
+
500: '#ef4444',
|
|
48
|
+
600: '#dc2626',
|
|
49
|
+
700: '#b91c1c',
|
|
50
|
+
800: '#991b1b',
|
|
51
|
+
900: '#7f1d1d',
|
|
52
|
+
950: '#450a0a',
|
|
53
|
+
},
|
|
54
|
+
orange: {
|
|
55
|
+
50: '#fff7ed',
|
|
56
|
+
100: '#ffedd5',
|
|
57
|
+
200: '#fed7aa',
|
|
58
|
+
300: '#fdba74',
|
|
59
|
+
400: '#fb923c',
|
|
60
|
+
500: '#f97316',
|
|
61
|
+
600: '#ea580c',
|
|
62
|
+
700: '#c2410c',
|
|
63
|
+
800: '#9a3412',
|
|
64
|
+
900: '#7c2d12',
|
|
65
|
+
950: '#431407',
|
|
66
|
+
},
|
|
67
|
+
yellow: {
|
|
68
|
+
50: '#fefce8',
|
|
69
|
+
100: '#fef9c3',
|
|
70
|
+
200: '#fef08a',
|
|
71
|
+
300: '#fde047',
|
|
72
|
+
400: '#facc15',
|
|
73
|
+
500: '#eab308',
|
|
74
|
+
600: '#ca8a04',
|
|
75
|
+
700: '#a16207',
|
|
76
|
+
800: '#854d0e',
|
|
77
|
+
900: '#713f12',
|
|
78
|
+
950: '#422006',
|
|
79
|
+
},
|
|
80
|
+
green: {
|
|
81
|
+
50: '#f0fdf4',
|
|
82
|
+
100: '#dcfce7',
|
|
83
|
+
200: '#bbf7d0',
|
|
84
|
+
300: '#86efac',
|
|
85
|
+
400: '#4ade80',
|
|
86
|
+
500: '#22c55e',
|
|
87
|
+
600: '#16a34a',
|
|
88
|
+
700: '#15803d',
|
|
89
|
+
800: '#166534',
|
|
90
|
+
900: '#14532d',
|
|
91
|
+
950: '#052e16',
|
|
92
|
+
},
|
|
93
|
+
blue: {
|
|
94
|
+
50: '#eff6ff',
|
|
95
|
+
100: '#dbeafe',
|
|
96
|
+
200: '#bfdbfe',
|
|
97
|
+
300: '#93c5fd',
|
|
98
|
+
400: '#60a5fa',
|
|
99
|
+
500: '#3b82f6',
|
|
100
|
+
600: '#2563eb',
|
|
101
|
+
700: '#1d4ed8',
|
|
102
|
+
800: '#1e40af',
|
|
103
|
+
900: '#1e3a8a',
|
|
104
|
+
950: '#172554',
|
|
105
|
+
},
|
|
106
|
+
indigo: {
|
|
107
|
+
50: '#eef2ff',
|
|
108
|
+
100: '#e0e7ff',
|
|
109
|
+
200: '#c7d2fe',
|
|
110
|
+
300: '#a5b4fc',
|
|
111
|
+
400: '#818cf8',
|
|
112
|
+
500: '#6366f1',
|
|
113
|
+
600: '#4f46e5',
|
|
114
|
+
700: '#4338ca',
|
|
115
|
+
800: '#3730a3',
|
|
116
|
+
900: '#312e81',
|
|
117
|
+
950: '#1e1b4b',
|
|
118
|
+
},
|
|
119
|
+
purple: {
|
|
120
|
+
50: '#faf5ff',
|
|
121
|
+
100: '#f3e8ff',
|
|
122
|
+
200: '#e9d5ff',
|
|
123
|
+
300: '#d8b4fe',
|
|
124
|
+
400: '#c084fc',
|
|
125
|
+
500: '#a855f7',
|
|
126
|
+
600: '#9333ea',
|
|
127
|
+
700: '#7e22ce',
|
|
128
|
+
800: '#6b21a8',
|
|
129
|
+
900: '#581c87',
|
|
130
|
+
950: '#3b0764',
|
|
131
|
+
},
|
|
132
|
+
pink: {
|
|
133
|
+
50: '#fdf2f8',
|
|
134
|
+
100: '#fce7f3',
|
|
135
|
+
200: '#fbcfe8',
|
|
136
|
+
300: '#f9a8d4',
|
|
137
|
+
400: '#f472b6',
|
|
138
|
+
500: '#ec4899',
|
|
139
|
+
600: '#db2777',
|
|
140
|
+
700: '#be185d',
|
|
141
|
+
800: '#9d174d',
|
|
142
|
+
900: '#831843',
|
|
143
|
+
950: '#500724',
|
|
144
|
+
},
|
|
145
|
+
// Semantic
|
|
146
|
+
white: { DEFAULT: '#ffffff' },
|
|
147
|
+
black: { DEFAULT: '#000000' },
|
|
148
|
+
transparent: { DEFAULT: 'transparent' },
|
|
149
|
+
current: { DEFAULT: 'currentColor' },
|
|
150
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { DesignTokens } from '../types';
|
|
2
|
+
import { colors } from './colors';
|
|
3
|
+
import { spacing } from './spacing';
|
|
4
|
+
import {
|
|
5
|
+
fontSize,
|
|
6
|
+
fontWeight,
|
|
7
|
+
fontFamily,
|
|
8
|
+
lineHeight,
|
|
9
|
+
letterSpacing,
|
|
10
|
+
} from './typography';
|
|
11
|
+
import { borderRadius, borderWidth } from './borders';
|
|
12
|
+
import { boxShadow } from './shadows';
|
|
13
|
+
|
|
14
|
+
export const defaultTokens: DesignTokens = {
|
|
15
|
+
colors,
|
|
16
|
+
spacing,
|
|
17
|
+
fontSize,
|
|
18
|
+
fontWeight,
|
|
19
|
+
fontFamily,
|
|
20
|
+
borderRadius,
|
|
21
|
+
borderWidth,
|
|
22
|
+
boxShadow,
|
|
23
|
+
lineHeight,
|
|
24
|
+
letterSpacing,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export { colors } from './colors';
|
|
28
|
+
export { spacing } from './spacing';
|
|
29
|
+
export {
|
|
30
|
+
fontSize,
|
|
31
|
+
fontWeight,
|
|
32
|
+
fontFamily,
|
|
33
|
+
lineHeight,
|
|
34
|
+
letterSpacing,
|
|
35
|
+
} from './typography';
|
|
36
|
+
export { borderRadius, borderWidth } from './borders';
|
|
37
|
+
export { boxShadow } from './shadows';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export const boxShadow: Record<string, string> = {
|
|
2
|
+
sm: '0 1px 2px 0 rgb(0 0 0 / 0.05)',
|
|
3
|
+
DEFAULT: '0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)',
|
|
4
|
+
md: '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)',
|
|
5
|
+
lg: '0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)',
|
|
6
|
+
xl: '0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)',
|
|
7
|
+
'2xl': '0 25px 50px -12px rgb(0 0 0 / 0.25)',
|
|
8
|
+
inner: 'inset 0 2px 4px 0 rgb(0 0 0 / 0.05)',
|
|
9
|
+
none: 'none',
|
|
10
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export const spacing: Record<string, string> = {
|
|
2
|
+
px: '1px',
|
|
3
|
+
'0': '0px',
|
|
4
|
+
'0.5': '0.125rem',
|
|
5
|
+
'1': '0.25rem',
|
|
6
|
+
'1.5': '0.375rem',
|
|
7
|
+
'2': '0.5rem',
|
|
8
|
+
'2.5': '0.625rem',
|
|
9
|
+
'3': '0.75rem',
|
|
10
|
+
'3.5': '0.875rem',
|
|
11
|
+
'4': '1rem',
|
|
12
|
+
'5': '1.25rem',
|
|
13
|
+
'6': '1.5rem',
|
|
14
|
+
'7': '1.75rem',
|
|
15
|
+
'8': '2rem',
|
|
16
|
+
'9': '2.25rem',
|
|
17
|
+
'10': '2.5rem',
|
|
18
|
+
'11': '2.75rem',
|
|
19
|
+
'12': '3rem',
|
|
20
|
+
'14': '3.5rem',
|
|
21
|
+
'16': '4rem',
|
|
22
|
+
'20': '5rem',
|
|
23
|
+
'24': '6rem',
|
|
24
|
+
'28': '7rem',
|
|
25
|
+
'32': '8rem',
|
|
26
|
+
'36': '9rem',
|
|
27
|
+
'40': '10rem',
|
|
28
|
+
'44': '11rem',
|
|
29
|
+
'48': '12rem',
|
|
30
|
+
'52': '13rem',
|
|
31
|
+
'56': '14rem',
|
|
32
|
+
'60': '15rem',
|
|
33
|
+
'64': '16rem',
|
|
34
|
+
'72': '18rem',
|
|
35
|
+
'80': '20rem',
|
|
36
|
+
'96': '24rem',
|
|
37
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export const fontSize: Record<string, string> = {
|
|
2
|
+
xs: '0.75rem',
|
|
3
|
+
sm: '0.875rem',
|
|
4
|
+
base: '1rem',
|
|
5
|
+
lg: '1.125rem',
|
|
6
|
+
xl: '1.25rem',
|
|
7
|
+
'2xl': '1.5rem',
|
|
8
|
+
'3xl': '1.875rem',
|
|
9
|
+
'4xl': '2.25rem',
|
|
10
|
+
'5xl': '3rem',
|
|
11
|
+
'6xl': '3.75rem',
|
|
12
|
+
'7xl': '4.5rem',
|
|
13
|
+
'8xl': '6rem',
|
|
14
|
+
'9xl': '8rem',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const fontWeight: Record<string, string> = {
|
|
18
|
+
thin: '100',
|
|
19
|
+
extralight: '200',
|
|
20
|
+
light: '300',
|
|
21
|
+
normal: '400',
|
|
22
|
+
medium: '500',
|
|
23
|
+
semibold: '600',
|
|
24
|
+
bold: '700',
|
|
25
|
+
extrabold: '800',
|
|
26
|
+
black: '900',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const fontFamily: Record<string, string> = {
|
|
30
|
+
sans: 'ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"',
|
|
31
|
+
serif: 'ui-serif, Georgia, Cambria, "Times New Roman", Times, serif',
|
|
32
|
+
mono: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const lineHeight: Record<string, string> = {
|
|
36
|
+
none: '1',
|
|
37
|
+
tight: '1.25',
|
|
38
|
+
snug: '1.375',
|
|
39
|
+
normal: '1.5',
|
|
40
|
+
relaxed: '1.625',
|
|
41
|
+
loose: '2',
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const letterSpacing: Record<string, string> = {
|
|
45
|
+
tighter: '-0.05em',
|
|
46
|
+
tight: '-0.025em',
|
|
47
|
+
normal: '0em',
|
|
48
|
+
wide: '0.025em',
|
|
49
|
+
wider: '0.05em',
|
|
50
|
+
widest: '0.1em',
|
|
51
|
+
};
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// === Element Selection ===
|
|
2
|
+
export interface SelectedElement {
|
|
3
|
+
element: HTMLElement;
|
|
4
|
+
rect: DOMRect;
|
|
5
|
+
path: string; // CSS selector path
|
|
6
|
+
xpath: string; // XPath selector
|
|
7
|
+
tagName: string;
|
|
8
|
+
id?: string;
|
|
9
|
+
classList: string[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// === Change Tracking ===
|
|
13
|
+
export type ChangeType =
|
|
14
|
+
| 'style' // CSS property change
|
|
15
|
+
| 'text' // Text content change
|
|
16
|
+
| 'delete' // Element removed
|
|
17
|
+
| 'move' // Element repositioned
|
|
18
|
+
| 'duplicate' // Element duplicated
|
|
19
|
+
| 'insert' // New element inserted
|
|
20
|
+
| 'attribute'; // Attribute modified
|
|
21
|
+
|
|
22
|
+
export interface Change {
|
|
23
|
+
id: string;
|
|
24
|
+
type: ChangeType;
|
|
25
|
+
timestamp: number;
|
|
26
|
+
target: {
|
|
27
|
+
path: string; // CSS selector path to element
|
|
28
|
+
xpath: string; // XPath selector to element
|
|
29
|
+
tagName: string;
|
|
30
|
+
};
|
|
31
|
+
before: unknown;
|
|
32
|
+
after: unknown;
|
|
33
|
+
metadata?: Record<string, unknown>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface StyleChange extends Change {
|
|
37
|
+
type: 'style';
|
|
38
|
+
before: { property: string; value: string };
|
|
39
|
+
after: { property: string; value: string };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface TextChange extends Change {
|
|
43
|
+
type: 'text';
|
|
44
|
+
before: string;
|
|
45
|
+
after: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface MoveChange extends Change {
|
|
49
|
+
type: 'move';
|
|
50
|
+
before: { parent: string; index: number };
|
|
51
|
+
after: { parent: string; index: number };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// === Annotations ===
|
|
55
|
+
export interface Annotation {
|
|
56
|
+
id: string;
|
|
57
|
+
elementPath: string; // CSS selector
|
|
58
|
+
elementXpath: string; // XPath selector
|
|
59
|
+
content: string;
|
|
60
|
+
createdAt: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// === Design Tokens ===
|
|
64
|
+
export interface DesignTokens {
|
|
65
|
+
colors: Record<string, Record<string, string>>;
|
|
66
|
+
spacing: Record<string, string>;
|
|
67
|
+
fontSize: Record<string, string>;
|
|
68
|
+
fontWeight: Record<string, string>;
|
|
69
|
+
fontFamily: Record<string, string>;
|
|
70
|
+
borderRadius: Record<string, string>;
|
|
71
|
+
borderWidth: Record<string, string>;
|
|
72
|
+
boxShadow: Record<string, string>;
|
|
73
|
+
lineHeight: Record<string, string>;
|
|
74
|
+
letterSpacing: Record<string, string>;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// === Bobbin State ===
|
|
78
|
+
export interface BobbinState {
|
|
79
|
+
isActive: boolean;
|
|
80
|
+
isPillExpanded: boolean;
|
|
81
|
+
hoveredElement: SelectedElement | null;
|
|
82
|
+
selectedElement: SelectedElement | null;
|
|
83
|
+
changes: Change[];
|
|
84
|
+
annotations: Annotation[];
|
|
85
|
+
clipboard: SelectedElement | null;
|
|
86
|
+
showMarginPadding: boolean;
|
|
87
|
+
activePanel: 'style' | 'inspector' | null;
|
|
88
|
+
theme: 'light' | 'dark' | 'system';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface BobbinActions {
|
|
92
|
+
activate: () => void;
|
|
93
|
+
deactivate: () => void;
|
|
94
|
+
selectElement: (el: HTMLElement | null) => void;
|
|
95
|
+
clearSelection: () => void;
|
|
96
|
+
applyStyle: (property: string, value: string) => void;
|
|
97
|
+
deleteElement: () => void;
|
|
98
|
+
moveElement: (targetParent: HTMLElement, index: number) => void;
|
|
99
|
+
duplicateElement: () => void;
|
|
100
|
+
insertElement: (
|
|
101
|
+
direction: 'before' | 'after' | 'child',
|
|
102
|
+
content?: string,
|
|
103
|
+
) => void;
|
|
104
|
+
copyElement: () => void;
|
|
105
|
+
pasteElement: (direction: 'before' | 'after' | 'child') => void;
|
|
106
|
+
annotate: (content: string) => void;
|
|
107
|
+
toggleMarginPadding: () => void;
|
|
108
|
+
toggleTheme: () => void;
|
|
109
|
+
undo: () => void;
|
|
110
|
+
exportChanges: () => string; // Returns YAML
|
|
111
|
+
getChanges: () => Change[];
|
|
112
|
+
resetChanges: () => void; // Reset all style changes
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// === YAML Export Format ===
|
|
116
|
+
export interface BobbinChangeset {
|
|
117
|
+
version: '1.0';
|
|
118
|
+
timestamp: string;
|
|
119
|
+
changeCount: number;
|
|
120
|
+
changes: Array<{
|
|
121
|
+
type: ChangeType;
|
|
122
|
+
target: string; // CSS selector
|
|
123
|
+
xpath: string; // XPath selector
|
|
124
|
+
property?: string;
|
|
125
|
+
before?: string;
|
|
126
|
+
after?: string;
|
|
127
|
+
note?: string;
|
|
128
|
+
}>;
|
|
129
|
+
annotations: Array<{
|
|
130
|
+
type: 'annotation';
|
|
131
|
+
target: string;
|
|
132
|
+
xpath: string;
|
|
133
|
+
note: string;
|
|
134
|
+
}>;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// === Component Props ===
|
|
138
|
+
export interface BobbinProps {
|
|
139
|
+
/** Custom design tokens to merge with defaults */
|
|
140
|
+
tokens?: Partial<DesignTokens>;
|
|
141
|
+
/** Container to scope element selection (default: document.body) */
|
|
142
|
+
container?: HTMLElement | null;
|
|
143
|
+
/** Container for pill positioning (if different from container) */
|
|
144
|
+
pillContainer?: HTMLElement | null;
|
|
145
|
+
/** Initial active state */
|
|
146
|
+
defaultActive?: boolean;
|
|
147
|
+
/** Callback when changes occur */
|
|
148
|
+
onChanges?: (changes: Change[]) => void;
|
|
149
|
+
/** Callback when selection changes */
|
|
150
|
+
onSelect?: (element: SelectedElement | null) => void;
|
|
151
|
+
/** Custom pill position offset from bottom-right of container */
|
|
152
|
+
position?: { bottom: number; right: number };
|
|
153
|
+
/** Z-index for overlay elements */
|
|
154
|
+
zIndex?: number;
|
|
155
|
+
/** Elements to exclude from selection (CSS selectors) */
|
|
156
|
+
exclude?: string[];
|
|
157
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export interface FLIPState {
|
|
2
|
+
rect: DOMRect;
|
|
3
|
+
opacity: number;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function measureElement(el: HTMLElement): FLIPState {
|
|
7
|
+
return {
|
|
8
|
+
rect: el.getBoundingClientRect(),
|
|
9
|
+
opacity: parseFloat(getComputedStyle(el).opacity),
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function animateFLIP(
|
|
14
|
+
el: HTMLElement,
|
|
15
|
+
from: FLIPState,
|
|
16
|
+
to: FLIPState,
|
|
17
|
+
duration = 150,
|
|
18
|
+
): void {
|
|
19
|
+
const deltaX = from.rect.left - to.rect.left;
|
|
20
|
+
const deltaY = from.rect.top - to.rect.top;
|
|
21
|
+
const deltaW = from.rect.width / to.rect.width;
|
|
22
|
+
const deltaH = from.rect.height / to.rect.height;
|
|
23
|
+
|
|
24
|
+
el.animate(
|
|
25
|
+
[
|
|
26
|
+
{
|
|
27
|
+
transform: `translate(${deltaX}px, ${deltaY}px) scale(${deltaW}, ${deltaH})`,
|
|
28
|
+
opacity: from.opacity,
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
transform: 'translate(0, 0) scale(1, 1)',
|
|
32
|
+
opacity: to.opacity,
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
{
|
|
36
|
+
duration,
|
|
37
|
+
easing: 'cubic-bezier(0.4, 0, 0.2, 1)',
|
|
38
|
+
},
|
|
39
|
+
);
|
|
40
|
+
}
|