@aprovan/bobbin 0.1.0-dev.98f1b7b → 0.1.0-dev.99f9769

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.
@@ -1,7 +1,7 @@
1
1
  import { useState } from 'react';
2
- import type { DesignTokens } from '../../../types';
3
- import { SectionWrapper } from './SectionWrapper';
4
2
  import { SliderInput } from '../controls/SliderInput';
3
+ import { SectionWrapper } from './SectionWrapper';
4
+ import type { DesignTokens } from '../../../types';
5
5
 
6
6
  // Common size presets
7
7
  const sizePresets: Record<string, string> = {
@@ -1,6 +1,6 @@
1
- import type { DesignTokens } from '../../../types';
2
- import { SectionWrapper } from './SectionWrapper';
3
1
  import { SpacingControl } from '../controls/SpacingControl';
2
+ import { SectionWrapper } from './SectionWrapper';
3
+ import type { DesignTokens } from '../../../types';
4
4
 
5
5
  interface SpacingSectionProps {
6
6
  expanded: boolean;
@@ -1,9 +1,9 @@
1
- import type { DesignTokens } from '../../../types';
2
- import { SectionWrapper } from './SectionWrapper';
3
- import { TokenDropdown } from '../controls/TokenDropdown';
4
- import { QuickSelectDropdown } from '../controls/QuickSelectDropdown';
5
1
  import { ColorPicker } from '../controls/ColorPicker';
2
+ import { QuickSelectDropdown } from '../controls/QuickSelectDropdown';
6
3
  import { ToggleGroup } from '../controls/ToggleGroup';
4
+ import { TokenDropdown } from '../controls/TokenDropdown';
5
+ import { SectionWrapper } from './SectionWrapper';
6
+ import type { DesignTokens } from '../../../types';
7
7
 
8
8
  // Text alignment icons
9
9
  const AlignLeftIcon = () => (
@@ -50,7 +50,8 @@ export function ControlHandles({
50
50
  const [isDragging, setIsDragging] = useState(false);
51
51
  const [dropTarget, setDropTarget] = useState<HTMLElement | null>(null);
52
52
  const [cornerToolbarExpanded, setCornerToolbarExpanded] = useState(false);
53
- const toolbarRef = useRef<HTMLDivElement>(null);
53
+ // @ts-expect-error TS6133
54
+ const _toolbarRef = useRef<HTMLDivElement>(null);
54
55
  const { rect } = selectedElement;
55
56
 
56
57
  const layoutDirection = useMemo(
@@ -59,7 +60,8 @@ export function ControlHandles({
59
60
  );
60
61
 
61
62
  // Check if corner toolbar should collapse
62
- const isNarrowElement = rect.width < MIN_WIDTH_FOR_CORNER_TOOLBAR;
63
+ // @ts-expect-error TS6133
64
+ const _isNarrowElement = rect.width < MIN_WIDTH_FOR_CORNER_TOOLBAR;
63
65
 
64
66
  // Check if edge zones need collapsing (based on element dimension perpendicular to edge)
65
67
  const isShortForVerticalEdge = rect.height < MIN_SIZE_FOR_EDGE_ICONS; // for left/right edges
@@ -117,7 +119,8 @@ export function ControlHandles({
117
119
  </svg>
118
120
  );
119
121
 
120
- const MoreIcon = () => (
122
+ // @ts-expect-error TS6133
123
+ const _MoreIcon = () => (
121
124
  <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
122
125
  <circle cx="12" cy="12" r="1" fill="currentColor" />
123
126
  <circle cx="19" cy="12" r="1" fill="currentColor" />
@@ -125,7 +128,8 @@ export function ControlHandles({
125
128
  </svg>
126
129
  );
127
130
 
128
- const MoreIconVertical = () => (
131
+ // @ts-expect-error TS6133
132
+ const _MoreIconVertical = () => (
129
133
  <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
130
134
  <circle cx="12" cy="5" r="1" fill="currentColor" />
131
135
  <circle cx="12" cy="12" r="1" fill="currentColor" />
@@ -270,7 +274,8 @@ export function ControlHandles({
270
274
  };
271
275
 
272
276
  // Check if an edge needs collapse based on position
273
- const edgeNeedsCollapse = (position: HandlePosition): boolean => {
277
+ // @ts-expect-error TS6133
278
+ const _edgeNeedsCollapse = (position: HandlePosition): boolean => {
274
279
  const isHorizontal = position === 'top' || position === 'bottom';
275
280
  return isHorizontal ? isShortForHorizontalEdge : isShortForVerticalEdge;
276
281
  };
@@ -417,7 +422,8 @@ export function ControlHandles({
417
422
  };
418
423
 
419
424
  // Expanded popup for collapsed edge zones
420
- const EdgeExpandedPopup = ({ position }: { position: HandlePosition }) => {
425
+ // @ts-expect-error TS6133
426
+ const _EdgeExpandedPopup = ({ position }: { position: HandlePosition }) => {
421
427
  const isHorizontal = position === 'top' || position === 'bottom';
422
428
 
423
429
  // Position the popup to extend in the direction it needs space
@@ -518,7 +524,8 @@ export function ControlHandles({
518
524
  );
519
525
 
520
526
  // Toolbar buttons (always the same, rendered conditionally based on narrow state)
521
- const ToolbarButtons = () => (
527
+ // @ts-expect-error TS6133
528
+ const _ToolbarButtons = () => (
522
529
  <>
523
530
  <ActionButton
524
531
  icon={<MoveIcon />}
@@ -1,4 +1,11 @@
1
1
  import { useState, useCallback, useMemo, useEffect } from 'react';
2
+ import { defaultTokens } from '../tokens';
3
+ import { applyStyleToElement, enableContentEditable } from '../utils/dom';
4
+ import { getElementPath, getElementXPath } from '../utils/selectors';
5
+ import { serializeChangesToYAML } from './changeSerializer';
6
+ import { useChangeTracker } from './useChangeTracker';
7
+ import { useClipboard } from './useClipboard';
8
+ import { useElementSelection } from './useElementSelection';
2
9
  import type {
3
10
  BobbinState,
4
11
  BobbinActions,
@@ -6,13 +13,6 @@ import type {
6
13
  DesignTokens,
7
14
  Annotation,
8
15
  } from '../types';
9
- import { useElementSelection } from './useElementSelection';
10
- import { useChangeTracker } from './useChangeTracker';
11
- import { useClipboard } from './useClipboard';
12
- import { serializeChangesToYAML } from './changeSerializer';
13
- import { defaultTokens } from '../tokens';
14
- import { getElementPath, getElementXPath } from '../utils/selectors';
15
- import { applyStyleToElement, enableContentEditable } from '../utils/dom';
16
16
 
17
17
  export function useBobbin(props: BobbinProps = {}) {
18
18
  const {
@@ -62,6 +62,7 @@ export function useBobbin(props: BobbinProps = {}) {
62
62
  enableContentEditable(selectedElement.element, true);
63
63
  return () => enableContentEditable(selectedElement.element, false);
64
64
  }
65
+ return undefined;
65
66
  }, [selectedElement]);
66
67
 
67
68
  // Notify on changes
@@ -1,4 +1,6 @@
1
1
  import { useState, useCallback, useRef, useMemo } from 'react';
2
+ import { deduplicateChanges } from '../utils/deduplicateChanges';
3
+ import { generateId } from '../utils/selectors';
2
4
  import type {
3
5
  Change,
4
6
  StyleChange,
@@ -6,7 +8,6 @@ import type {
6
8
  MoveChange,
7
9
  ChangeType,
8
10
  } from '../types';
9
- import { generateId } from '../utils/selectors';
10
11
 
11
12
  export function useChangeTracker() {
12
13
  const [changes, setChanges] = useState<Change[]>([]);
@@ -138,37 +139,10 @@ export function useChangeTracker() {
138
139
 
139
140
  // Deduplicated changes - only count unique (element + property) combinations
140
141
  // This prevents counting every keystroke as a separate change
141
- const deduplicatedChanges = useMemo(() => {
142
- const uniqueChanges = new Map<string, Change>();
143
-
144
- for (const change of changes) {
145
- let key: string;
146
-
147
- if (change.type === 'style') {
148
- const styleChange = change as StyleChange;
149
- key = `${change.target.path}:style:${styleChange.after.property}`;
150
- } else {
151
- key = `${change.target.path}:${change.type}:${change.id}`;
152
- }
153
-
154
- // Check if the value has changed back to original
155
- if (change.type === 'style') {
156
- const styleChange = change as StyleChange;
157
- const originalValue = originalStatesRef.current
158
- .get(change.target.path)
159
- ?.get(styleChange.after.property);
160
- if (originalValue === styleChange.after.value) {
161
- // Value is back to original, remove from unique changes
162
- uniqueChanges.delete(key);
163
- continue;
164
- }
165
- }
166
-
167
- uniqueChanges.set(key, change);
168
- }
169
-
170
- return Array.from(uniqueChanges.values());
171
- }, [changes]);
142
+ const deduplicatedChanges = useMemo(
143
+ () => deduplicateChanges(changes, originalStatesRef.current),
144
+ [changes],
145
+ );
172
146
 
173
147
  return {
174
148
  changes,
@@ -1,6 +1,6 @@
1
1
  import { useState, useCallback, useEffect, useRef } from 'react';
2
- import type { SelectedElement } from '../types';
3
2
  import { getElementPath, getElementXPath } from '../utils/selectors';
3
+ import type { SelectedElement } from '../types';
4
4
 
5
5
  export interface UseElementSelectionOptions {
6
6
  container?: HTMLElement | null;
package/src/index.ts CHANGED
@@ -14,6 +14,21 @@ export {
14
14
  parseYAMLChangeset,
15
15
  } from './core/changeSerializer';
16
16
  export { getElementPath, getElementXPath, generateId } from './utils/selectors';
17
+ export { deduplicateChanges } from './utils/deduplicateChanges';
18
+
19
+ // Type guards
20
+ export {
21
+ isChangeType,
22
+ isChange,
23
+ isStyleChange,
24
+ isTextChange,
25
+ isMoveChange,
26
+ isSelectedElement,
27
+ isAnnotation,
28
+ isDesignTokens,
29
+ isBobbinState,
30
+ isBobbinChangeset,
31
+ } from './typeGuards';
17
32
 
18
33
  // Types
19
34
  export type {
@@ -1,5 +1,6 @@
1
- import type { DesignTokens } from '../types';
1
+ import { borderRadius, borderWidth } from './borders';
2
2
  import { colors } from './colors';
3
+ import { boxShadow } from './shadows';
3
4
  import { spacing } from './spacing';
4
5
  import {
5
6
  fontSize,
@@ -8,8 +9,7 @@ import {
8
9
  lineHeight,
9
10
  letterSpacing,
10
11
  } from './typography';
11
- import { borderRadius, borderWidth } from './borders';
12
- import { boxShadow } from './shadows';
12
+ import type { DesignTokens } from '../types';
13
13
 
14
14
  export const defaultTokens: DesignTokens = {
15
15
  colors,
@@ -0,0 +1,169 @@
1
+ import type {
2
+ ChangeType,
3
+ Change,
4
+ StyleChange,
5
+ TextChange,
6
+ MoveChange,
7
+ SelectedElement,
8
+ Annotation,
9
+ DesignTokens,
10
+ BobbinState,
11
+ BobbinChangeset,
12
+ } from './types';
13
+
14
+ const CHANGE_TYPE_VALUES: readonly string[] = [
15
+ 'style',
16
+ 'text',
17
+ 'delete',
18
+ 'move',
19
+ 'duplicate',
20
+ 'insert',
21
+ 'attribute',
22
+ ];
23
+
24
+ export function isChangeType(value: unknown): value is ChangeType {
25
+ return typeof value === 'string' && CHANGE_TYPE_VALUES.includes(value);
26
+ }
27
+
28
+ export function isChangeTarget(
29
+ value: unknown,
30
+ ): value is Change['target'] & Record<string, unknown> {
31
+ if (typeof value !== 'object' || value === null) return false;
32
+ const obj = value as Record<string, unknown>;
33
+ return (
34
+ typeof obj.path === 'string' &&
35
+ typeof obj.xpath === 'string' &&
36
+ typeof obj.tagName === 'string'
37
+ );
38
+ }
39
+
40
+ export function isChange(value: unknown): value is Change {
41
+ if (typeof value !== 'object' || value === null) return false;
42
+ const obj = value as Record<string, unknown>;
43
+ return (
44
+ typeof obj.id === 'string' &&
45
+ isChangeType(obj.type) &&
46
+ typeof obj.timestamp === 'number' &&
47
+ isChangeTarget(obj.target)
48
+ );
49
+ }
50
+
51
+ export function isStyleChange(value: unknown): value is StyleChange {
52
+ if (!isChange(value)) return false;
53
+ if (value.type !== 'style') return false;
54
+ const obj = value as unknown as Record<string, unknown>;
55
+ const before = obj.before;
56
+ const after = obj.after;
57
+ if (typeof before !== 'object' || before === null) return false;
58
+ if (typeof after !== 'object' || after === null) return false;
59
+ const b = before as Record<string, unknown>;
60
+ const a = after as Record<string, unknown>;
61
+ return (
62
+ typeof b.property === 'string' &&
63
+ typeof b.value === 'string' &&
64
+ typeof a.property === 'string' &&
65
+ typeof a.value === 'string'
66
+ );
67
+ }
68
+
69
+ export function isTextChange(value: unknown): value is TextChange {
70
+ if (!isChange(value)) return false;
71
+ if (value.type !== 'text') return false;
72
+ const obj = value as unknown as Record<string, unknown>;
73
+ return typeof obj.before === 'string' && typeof obj.after === 'string';
74
+ }
75
+
76
+ export function isMoveChange(value: unknown): value is MoveChange {
77
+ if (!isChange(value)) return false;
78
+ if (value.type !== 'move') return false;
79
+ const obj = value as unknown as Record<string, unknown>;
80
+ const before = obj.before;
81
+ const after = obj.after;
82
+ if (typeof before !== 'object' || before === null) return false;
83
+ if (typeof after !== 'object' || after === null) return false;
84
+ const b = before as Record<string, unknown>;
85
+ const a = after as Record<string, unknown>;
86
+ return (
87
+ typeof b.parent === 'string' &&
88
+ typeof b.index === 'number' &&
89
+ typeof a.parent === 'string' &&
90
+ typeof a.index === 'number'
91
+ );
92
+ }
93
+
94
+ export function isSelectedElement(value: unknown): value is SelectedElement {
95
+ if (typeof value !== 'object' || value === null) return false;
96
+ const obj = value as Record<string, unknown>;
97
+ return (
98
+ typeof obj.tagName === 'string' &&
99
+ typeof obj.path === 'string' &&
100
+ typeof obj.xpath === 'string' &&
101
+ Array.isArray(obj.classList) &&
102
+ (obj.id === undefined || typeof obj.id === 'string')
103
+ );
104
+ }
105
+
106
+ export function isAnnotation(value: unknown): value is Annotation {
107
+ if (typeof value !== 'object' || value === null) return false;
108
+ const obj = value as Record<string, unknown>;
109
+ return (
110
+ typeof obj.id === 'string' &&
111
+ typeof obj.elementPath === 'string' &&
112
+ typeof obj.elementXpath === 'string' &&
113
+ typeof obj.content === 'string' &&
114
+ typeof obj.createdAt === 'number'
115
+ );
116
+ }
117
+
118
+ export function isDesignTokens(value: unknown): value is DesignTokens {
119
+ if (typeof value !== 'object' || value === null) return false;
120
+ const obj = value as Record<string, unknown>;
121
+ const keys = [
122
+ 'colors',
123
+ 'spacing',
124
+ 'fontSize',
125
+ 'fontWeight',
126
+ 'fontFamily',
127
+ 'borderRadius',
128
+ 'borderWidth',
129
+ 'boxShadow',
130
+ 'lineHeight',
131
+ 'letterSpacing',
132
+ ];
133
+ return keys.every(
134
+ (k) => k in obj && typeof obj[k] === 'object' && obj[k] !== null,
135
+ );
136
+ }
137
+
138
+ export function isBobbinState(value: unknown): value is BobbinState {
139
+ if (typeof value !== 'object' || value === null) return false;
140
+ const obj = value as Record<string, unknown>;
141
+ return (
142
+ typeof obj.isActive === 'boolean' &&
143
+ typeof obj.isPillExpanded === 'boolean' &&
144
+ typeof obj.showMarginPadding === 'boolean' &&
145
+ (obj.hoveredElement === null || isSelectedElement(obj.hoveredElement)) &&
146
+ (obj.selectedElement === null || isSelectedElement(obj.selectedElement)) &&
147
+ Array.isArray(obj.changes) &&
148
+ obj.changes.every(isChange) &&
149
+ Array.isArray(obj.annotations) &&
150
+ obj.annotations.every(isAnnotation) &&
151
+ (obj.clipboard === null || isSelectedElement(obj.clipboard)) &&
152
+ (obj.activePanel === null ||
153
+ obj.activePanel === 'style' ||
154
+ obj.activePanel === 'inspector') &&
155
+ (obj.theme === 'light' || obj.theme === 'dark' || obj.theme === 'system')
156
+ );
157
+ }
158
+
159
+ export function isBobbinChangeset(value: unknown): value is BobbinChangeset {
160
+ if (typeof value !== 'object' || value === null) return false;
161
+ const obj = value as Record<string, unknown>;
162
+ return (
163
+ obj.version === '1.0' &&
164
+ typeof obj.timestamp === 'string' &&
165
+ typeof obj.changeCount === 'number' &&
166
+ Array.isArray(obj.changes) &&
167
+ Array.isArray(obj.annotations)
168
+ );
169
+ }
@@ -0,0 +1,34 @@
1
+ import type { Change, StyleChange } from '../types';
2
+
3
+ export function deduplicateChanges(
4
+ changes: Change[],
5
+ originalStates: Map<string, Map<string, string>>,
6
+ ): Change[] {
7
+ const uniqueChanges = new Map<string, Change>();
8
+
9
+ for (const change of changes) {
10
+ let key: string;
11
+
12
+ if (change.type === 'style') {
13
+ const styleChange = change as StyleChange;
14
+ key = `${change.target.path}:style:${styleChange.after.property}`;
15
+ } else {
16
+ key = `${change.target.path}:${change.type}:${change.id}`;
17
+ }
18
+
19
+ if (change.type === 'style') {
20
+ const styleChange = change as StyleChange;
21
+ const originalValue = originalStates
22
+ .get(change.target.path)
23
+ ?.get(styleChange.after.property);
24
+ if (originalValue === styleChange.after.value) {
25
+ uniqueChanges.delete(key);
26
+ continue;
27
+ }
28
+ }
29
+
30
+ uniqueChanges.set(key, change);
31
+ }
32
+
33
+ return Array.from(uniqueChanges.values());
34
+ }