@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.
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,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
+ }