@aprovan/bobbin 0.1.0-dev.9d1cd22 → 0.1.0-dev.abe9883

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aprovan/bobbin",
3
- "version": "0.1.0-dev.9d1cd22",
3
+ "version": "0.1.0-dev.abe9883",
4
4
  "description": "Visual DOM editor",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -25,6 +25,7 @@
25
25
  "scripts": {
26
26
  "build": "tsup",
27
27
  "dev": "tsup --watch",
28
- "typecheck": "tsc --noEmit"
28
+ "typecheck": "tsc --noEmit",
29
+ "test": "vitest run"
29
30
  }
30
31
  }
package/src/Bobbin.tsx CHANGED
@@ -1,12 +1,12 @@
1
1
  import { createPortal } from 'react-dom';
2
- import type { BobbinProps } from './types';
3
- import { useBobbin } from './core/useBobbin';
4
- import { Pill } from './components/Pill/Pill';
5
- import { SelectionOverlay } from './components/Overlay/SelectionOverlay';
6
- import { ControlHandles } from './components/Overlay/ControlHandles';
7
- import { MarginPaddingOverlay } from './components/Overlay/MarginPaddingOverlay';
8
2
  import { EditPanel } from './components/EditPanel/EditPanel';
9
3
  import { Inspector } from './components/Inspector/Inspector';
4
+ import { ControlHandles } from './components/Overlay/ControlHandles';
5
+ import { MarginPaddingOverlay } from './components/Overlay/MarginPaddingOverlay';
6
+ import { SelectionOverlay } from './components/Overlay/SelectionOverlay';
7
+ import { Pill } from './components/Pill/Pill';
8
+ import { useBobbin } from './core/useBobbin';
9
+ import type { BobbinProps } from './types';
10
10
 
11
11
  export interface BobbinComponentProps extends BobbinProps {
12
12
  /** Show inspector panel */
@@ -0,0 +1,178 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { deduplicateChanges } from '../utils/deduplicateChanges';
3
+ import type { Change, StyleChange, TextChange, MoveChange } from '../types';
4
+
5
+ function makeStyleChange(
6
+ path: string,
7
+ property: string,
8
+ value: string,
9
+ id = 'id-1',
10
+ ): StyleChange {
11
+ return {
12
+ id,
13
+ type: 'style',
14
+ timestamp: 1000,
15
+ target: { path, xpath: `//${path}`, tagName: path },
16
+ before: { property, value: 'original' },
17
+ after: { property, value },
18
+ };
19
+ }
20
+
21
+ function makeTextChange(
22
+ path: string,
23
+ before: string,
24
+ after: string,
25
+ id = 'tid-1',
26
+ ): TextChange {
27
+ return {
28
+ id,
29
+ type: 'text',
30
+ timestamp: 1000,
31
+ target: { path, xpath: `//${path}`, tagName: path },
32
+ before,
33
+ after,
34
+ };
35
+ }
36
+
37
+ function makeMoveChange(
38
+ path: string,
39
+ fromParent: string,
40
+ fromIndex: number,
41
+ toParent: string,
42
+ toIndex: number,
43
+ id = 'mid-1',
44
+ ): MoveChange {
45
+ return {
46
+ id,
47
+ type: 'move',
48
+ timestamp: 1000,
49
+ target: { path, xpath: `//${path}`, tagName: path },
50
+ before: { parent: fromParent, index: fromIndex },
51
+ after: { parent: toParent, index: toIndex },
52
+ };
53
+ }
54
+
55
+ describe('deduplicateChanges', () => {
56
+ it('returns empty array for no changes', () => {
57
+ const result = deduplicateChanges([], new Map());
58
+ expect(result).toEqual([]);
59
+ });
60
+
61
+ it('returns unique style changes for different properties', () => {
62
+ const changes: Change[] = [
63
+ makeStyleChange('div', 'color', 'blue', 'id-1'),
64
+ makeStyleChange('div', 'font-size', '16px', 'id-2'),
65
+ ];
66
+ const result = deduplicateChanges(changes, new Map());
67
+ expect(result).toHaveLength(2);
68
+ });
69
+
70
+ it('keeps only the latest style change for same property', () => {
71
+ const changes: Change[] = [
72
+ makeStyleChange('div', 'color', 'blue', 'id-1'),
73
+ makeStyleChange('div', 'color', 'green', 'id-2'),
74
+ ];
75
+ const result = deduplicateChanges(changes, new Map());
76
+ expect(result).toHaveLength(1);
77
+ const styleResult = result[0] as StyleChange;
78
+ expect(styleResult.after.value).toBe('green');
79
+ });
80
+
81
+ it('removes style change when value reverts to original', () => {
82
+ const originalStates = new Map<string, Map<string, string>>();
83
+ originalStates.set('div', new Map([['color', 'blue']]));
84
+
85
+ const changes: Change[] = [
86
+ makeStyleChange('div', 'color', 'blue', 'id-1'),
87
+ ];
88
+ const result = deduplicateChanges(changes, originalStates);
89
+ expect(result).toHaveLength(0);
90
+ });
91
+
92
+ it('keeps style change when value differs from original', () => {
93
+ const originalStates = new Map<string, Map<string, string>>();
94
+ originalStates.set('div', new Map([['color', 'red']]));
95
+
96
+ const changes: Change[] = [
97
+ makeStyleChange('div', 'color', 'blue', 'id-1'),
98
+ ];
99
+ const result = deduplicateChanges(changes, originalStates);
100
+ expect(result).toHaveLength(1);
101
+ });
102
+
103
+ it('does not deduplicate text changes by content', () => {
104
+ const changes: Change[] = [
105
+ makeTextChange('p', 'a', 'b', 'tid-1'),
106
+ makeTextChange('p', 'c', 'd', 'tid-2'),
107
+ ];
108
+ const result = deduplicateChanges(changes, new Map());
109
+ expect(result).toHaveLength(2);
110
+ });
111
+
112
+ it('does not deduplicate move changes', () => {
113
+ const changes: Change[] = [
114
+ makeMoveChange('div', 'section', 0, 'main', 1, 'mid-1'),
115
+ makeMoveChange('div', 'main', 1, 'section', 2, 'mid-2'),
116
+ ];
117
+ const result = deduplicateChanges(changes, new Map());
118
+ expect(result).toHaveLength(2);
119
+ });
120
+
121
+ it('handles mixed change types', () => {
122
+ const originalStates = new Map<string, Map<string, string>>();
123
+ originalStates.set('div', new Map([['color', 'red']]));
124
+
125
+ const changes: Change[] = [
126
+ makeStyleChange('div', 'color', 'blue', 'id-1'),
127
+ makeTextChange('div', 'a', 'b', 'tid-1'),
128
+ makeMoveChange('div', 'section', 0, 'main', 1, 'mid-1'),
129
+ ];
130
+ const result = deduplicateChanges(changes, originalStates);
131
+ expect(result).toHaveLength(3);
132
+ });
133
+
134
+ it('handles reverts in a sequence of style changes', () => {
135
+ const originalStates = new Map<string, Map<string, string>>();
136
+ originalStates.set('div', new Map([['color', 'red']]));
137
+
138
+ const changes: Change[] = [
139
+ makeStyleChange('div', 'color', 'blue', 'id-1'),
140
+ makeStyleChange('div', 'color', 'green', 'id-2'),
141
+ makeStyleChange('div', 'color', 'red', 'id-3'),
142
+ ];
143
+ const result = deduplicateChanges(changes, originalStates);
144
+ expect(result).toHaveLength(0);
145
+ });
146
+
147
+ it('handles style changes for different elements independently', () => {
148
+ const changes: Change[] = [
149
+ makeStyleChange('div', 'color', 'blue', 'id-1'),
150
+ makeStyleChange('span', 'color', 'green', 'id-2'),
151
+ ];
152
+ const result = deduplicateChanges(changes, new Map());
153
+ expect(result).toHaveLength(2);
154
+ });
155
+
156
+ it('handles delete/insert/attribute changes without dedup by content', () => {
157
+ const changes: Change[] = [
158
+ {
159
+ id: 'del-1',
160
+ type: 'delete',
161
+ timestamp: 1000,
162
+ target: { path: 'div', xpath: '//div', tagName: 'div' },
163
+ before: '<div/>',
164
+ after: null,
165
+ },
166
+ {
167
+ id: 'ins-1',
168
+ type: 'insert',
169
+ timestamp: 1000,
170
+ target: { path: 'div', xpath: '//div', tagName: 'div' },
171
+ before: null,
172
+ after: '<span/>',
173
+ },
174
+ ];
175
+ const result = deduplicateChanges(changes, new Map());
176
+ expect(result).toHaveLength(2);
177
+ });
178
+ });
@@ -0,0 +1,416 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import {
3
+ isChangeType,
4
+ isChange,
5
+ isStyleChange,
6
+ isTextChange,
7
+ isMoveChange,
8
+ isSelectedElement,
9
+ isAnnotation,
10
+ isDesignTokens,
11
+ isBobbinState,
12
+ isBobbinChangeset,
13
+ } from '../typeGuards';
14
+ import type {
15
+ Change,
16
+ StyleChange,
17
+ TextChange,
18
+ MoveChange,
19
+ SelectedElement,
20
+ Annotation,
21
+ DesignTokens,
22
+ BobbinState,
23
+ BobbinChangeset,
24
+ } from '../types';
25
+
26
+ function makeChange(overrides: Partial<Change> = {}): Change {
27
+ return {
28
+ id: 'test-id',
29
+ type: 'style',
30
+ timestamp: 1000,
31
+ target: { path: 'div', xpath: '//div', tagName: 'div' },
32
+ before: { property: 'color', value: 'red' },
33
+ after: { property: 'color', value: 'blue' },
34
+ ...overrides,
35
+ };
36
+ }
37
+
38
+ function makeStyleChange(overrides: Partial<StyleChange> = {}): StyleChange {
39
+ return {
40
+ id: 'style-id',
41
+ type: 'style',
42
+ timestamp: 1000,
43
+ target: { path: 'div', xpath: '//div', tagName: 'div' },
44
+ before: { property: 'color', value: 'red' },
45
+ after: { property: 'color', value: 'blue' },
46
+ ...overrides,
47
+ };
48
+ }
49
+
50
+ function makeTextChange(overrides: Partial<TextChange> = {}): TextChange {
51
+ return {
52
+ id: 'text-id',
53
+ type: 'text',
54
+ timestamp: 1000,
55
+ target: { path: 'p', xpath: '//p', tagName: 'p' },
56
+ before: 'hello',
57
+ after: 'world',
58
+ ...overrides,
59
+ };
60
+ }
61
+
62
+ function makeMoveChange(overrides: Partial<MoveChange> = {}): MoveChange {
63
+ return {
64
+ id: 'move-id',
65
+ type: 'move',
66
+ timestamp: 1000,
67
+ target: { path: 'div', xpath: '//div', tagName: 'div' },
68
+ before: { parent: 'section', index: 0 },
69
+ after: { parent: 'main', index: 1 },
70
+ ...overrides,
71
+ };
72
+ }
73
+
74
+ describe('isChangeType', () => {
75
+ it('accepts valid change types', () => {
76
+ expect(isChangeType('style')).toBe(true);
77
+ expect(isChangeType('text')).toBe(true);
78
+ expect(isChangeType('delete')).toBe(true);
79
+ expect(isChangeType('move')).toBe(true);
80
+ expect(isChangeType('duplicate')).toBe(true);
81
+ expect(isChangeType('insert')).toBe(true);
82
+ expect(isChangeType('attribute')).toBe(true);
83
+ });
84
+
85
+ it('rejects invalid change types', () => {
86
+ expect(isChangeType('unknown')).toBe(false);
87
+ expect(isChangeType('')).toBe(false);
88
+ expect(isChangeType(123)).toBe(false);
89
+ expect(isChangeType(null)).toBe(false);
90
+ expect(isChangeType(undefined)).toBe(false);
91
+ });
92
+ });
93
+
94
+ describe('isChange', () => {
95
+ it('accepts a valid Change', () => {
96
+ expect(isChange(makeChange())).toBe(true);
97
+ });
98
+
99
+ it('accepts different change types', () => {
100
+ expect(isChange(makeChange({ type: 'text', before: 'a', after: 'b' }))).toBe(true);
101
+ expect(isChange(makeChange({ type: 'delete', before: '<div>', after: null }))).toBe(true);
102
+ expect(isChange(makeChange({ type: 'insert', before: null, after: '<div>' }))).toBe(true);
103
+ });
104
+
105
+ it('rejects non-objects', () => {
106
+ expect(isChange(null)).toBe(false);
107
+ expect(isChange('string')).toBe(false);
108
+ expect(isChange(42)).toBe(false);
109
+ });
110
+
111
+ it('rejects objects with missing fields', () => {
112
+ expect(isChange({})).toBe(false);
113
+ expect(isChange({ id: 'x', type: 'style' })).toBe(false);
114
+ expect(isChange({ id: 'x', type: 'style', timestamp: 1 })).toBe(false);
115
+ });
116
+
117
+ it('rejects objects with invalid type', () => {
118
+ expect(isChange({ ...makeChange(), type: 'bogus' })).toBe(false);
119
+ });
120
+
121
+ it('rejects objects with invalid target', () => {
122
+ expect(isChange({ ...makeChange(), target: { path: 1 } })).toBe(false);
123
+ });
124
+ });
125
+
126
+ describe('isStyleChange', () => {
127
+ it('accepts a valid StyleChange', () => {
128
+ expect(isStyleChange(makeStyleChange())).toBe(true);
129
+ });
130
+
131
+ it('rejects a Change with wrong type', () => {
132
+ expect(isStyleChange(makeTextChange())).toBe(false);
133
+ });
134
+
135
+ it('rejects non-Change objects', () => {
136
+ expect(isStyleChange(null)).toBe(false);
137
+ expect(isStyleChange({})).toBe(false);
138
+ });
139
+
140
+ it('rejects if before/after lack property/value strings', () => {
141
+ const bad = {
142
+ ...makeStyleChange(),
143
+ before: { property: 1, value: 'red' },
144
+ };
145
+ expect(isStyleChange(bad)).toBe(false);
146
+ });
147
+ });
148
+
149
+ describe('isTextChange', () => {
150
+ it('accepts a valid TextChange', () => {
151
+ expect(isTextChange(makeTextChange())).toBe(true);
152
+ });
153
+
154
+ it('rejects a Change with wrong type', () => {
155
+ expect(isTextChange(makeStyleChange())).toBe(false);
156
+ });
157
+
158
+ it('rejects if before/after are not strings', () => {
159
+ const bad = { ...makeTextChange(), before: 42 };
160
+ expect(isTextChange(bad)).toBe(false);
161
+ });
162
+ });
163
+
164
+ describe('isMoveChange', () => {
165
+ it('accepts a valid MoveChange', () => {
166
+ expect(isMoveChange(makeMoveChange())).toBe(true);
167
+ });
168
+
169
+ it('rejects a Change with wrong type', () => {
170
+ expect(isMoveChange(makeStyleChange())).toBe(false);
171
+ });
172
+
173
+ it('rejects if before/after lack parent/index', () => {
174
+ const bad = { ...makeMoveChange(), before: { parent: 1, index: 0 } };
175
+ expect(isMoveChange(bad)).toBe(false);
176
+ });
177
+ });
178
+
179
+ describe('isSelectedElement', () => {
180
+ it('accepts a valid SelectedElement', () => {
181
+ const el: SelectedElement = {
182
+ element: {} as HTMLElement,
183
+ rect: {} as DOMRect,
184
+ path: 'div',
185
+ xpath: '//div',
186
+ tagName: 'div',
187
+ classList: ['foo'],
188
+ };
189
+ expect(isSelectedElement(el)).toBe(true);
190
+ });
191
+
192
+ it('accepts with optional id', () => {
193
+ const el: SelectedElement = {
194
+ element: {} as HTMLElement,
195
+ rect: {} as DOMRect,
196
+ path: 'div',
197
+ xpath: '//div',
198
+ tagName: 'div',
199
+ classList: [],
200
+ id: 'my-id',
201
+ };
202
+ expect(isSelectedElement(el)).toBe(true);
203
+ });
204
+
205
+ it('rejects null', () => {
206
+ expect(isSelectedElement(null)).toBe(false);
207
+ });
208
+
209
+ it('rejects missing required fields', () => {
210
+ expect(isSelectedElement({ path: 'div' })).toBe(false);
211
+ });
212
+
213
+ it('rejects non-array classList', () => {
214
+ const el = {
215
+ element: {},
216
+ rect: {},
217
+ path: 'div',
218
+ xpath: '//div',
219
+ tagName: 'div',
220
+ classList: 'not-array',
221
+ };
222
+ expect(isSelectedElement(el)).toBe(false);
223
+ });
224
+
225
+ it('rejects non-string id', () => {
226
+ const el = {
227
+ element: {},
228
+ rect: {},
229
+ path: 'div',
230
+ xpath: '//div',
231
+ tagName: 'div',
232
+ classList: [],
233
+ id: 123,
234
+ };
235
+ expect(isSelectedElement(el)).toBe(false);
236
+ });
237
+ });
238
+
239
+ describe('isAnnotation', () => {
240
+ it('accepts a valid Annotation', () => {
241
+ const a: Annotation = {
242
+ id: 'ann-1',
243
+ elementPath: 'div',
244
+ elementXpath: '//div',
245
+ content: 'note',
246
+ createdAt: 1000,
247
+ };
248
+ expect(isAnnotation(a)).toBe(true);
249
+ });
250
+
251
+ it('rejects missing fields', () => {
252
+ expect(isAnnotation({})).toBe(false);
253
+ expect(isAnnotation({ id: 'x', elementPath: 'd' })).toBe(false);
254
+ });
255
+
256
+ it('rejects wrong types', () => {
257
+ expect(isAnnotation({ id: 1, elementPath: 'd', elementXpath: 'x', content: 'c', createdAt: 1 })).toBe(false);
258
+ });
259
+ });
260
+
261
+ describe('isDesignTokens', () => {
262
+ it('accepts a valid DesignTokens', () => {
263
+ const tokens: DesignTokens = {
264
+ colors: { primary: { base: '#000' } },
265
+ spacing: { sm: '8px' },
266
+ fontSize: { base: '16px' },
267
+ fontWeight: { normal: '400' },
268
+ fontFamily: { sans: 'Inter' },
269
+ borderRadius: { sm: '4px' },
270
+ borderWidth: { thin: '1px' },
271
+ boxShadow: { sm: '0 1px' },
272
+ lineHeight: { base: '1.5' },
273
+ letterSpacing: { normal: '0' },
274
+ };
275
+ expect(isDesignTokens(tokens)).toBe(true);
276
+ });
277
+
278
+ it('rejects missing token categories', () => {
279
+ const partial = { colors: {}, spacing: {} };
280
+ expect(isDesignTokens(partial)).toBe(false);
281
+ });
282
+
283
+ it('rejects null', () => {
284
+ expect(isDesignTokens(null)).toBe(false);
285
+ });
286
+
287
+ it('rejects non-object values for token categories', () => {
288
+ const bad = {
289
+ colors: 'not-an-object',
290
+ spacing: {},
291
+ fontSize: {},
292
+ fontWeight: {},
293
+ fontFamily: {},
294
+ borderRadius: {},
295
+ borderWidth: {},
296
+ boxShadow: {},
297
+ lineHeight: {},
298
+ letterSpacing: {},
299
+ };
300
+ expect(isDesignTokens(bad)).toBe(false);
301
+ });
302
+ });
303
+
304
+ describe('isBobbinState', () => {
305
+ it('accepts a valid BobbinState', () => {
306
+ const state: BobbinState = {
307
+ isActive: true,
308
+ isPillExpanded: false,
309
+ hoveredElement: null,
310
+ selectedElement: null,
311
+ changes: [],
312
+ annotations: [],
313
+ clipboard: null,
314
+ showMarginPadding: false,
315
+ activePanel: 'style',
316
+ theme: 'system',
317
+ };
318
+ expect(isBobbinState(state)).toBe(true);
319
+ });
320
+
321
+ it('accepts with non-null selected elements', () => {
322
+ const state: BobbinState = {
323
+ isActive: true,
324
+ isPillExpanded: false,
325
+ hoveredElement: null,
326
+ selectedElement: {
327
+ element: {} as HTMLElement,
328
+ rect: {} as DOMRect,
329
+ path: 'div',
330
+ xpath: '//div',
331
+ tagName: 'div',
332
+ classList: [],
333
+ },
334
+ changes: [],
335
+ annotations: [],
336
+ clipboard: null,
337
+ showMarginPadding: false,
338
+ activePanel: null,
339
+ theme: 'dark',
340
+ };
341
+ expect(isBobbinState(state)).toBe(true);
342
+ });
343
+
344
+ it('rejects missing boolean fields', () => {
345
+ expect(isBobbinState({ isActive: true })).toBe(false);
346
+ });
347
+
348
+ it('rejects invalid theme', () => {
349
+ const bad = {
350
+ isActive: true,
351
+ isPillExpanded: false,
352
+ hoveredElement: null,
353
+ selectedElement: null,
354
+ changes: [],
355
+ annotations: [],
356
+ clipboard: null,
357
+ showMarginPadding: false,
358
+ activePanel: null,
359
+ theme: 'invalid',
360
+ };
361
+ expect(isBobbinState(bad)).toBe(false);
362
+ });
363
+
364
+ it('rejects invalid changes array', () => {
365
+ const bad = {
366
+ isActive: true,
367
+ isPillExpanded: false,
368
+ hoveredElement: null,
369
+ selectedElement: null,
370
+ changes: [{}],
371
+ annotations: [],
372
+ clipboard: null,
373
+ showMarginPadding: false,
374
+ activePanel: null,
375
+ theme: 'system',
376
+ };
377
+ expect(isBobbinState(bad)).toBe(false);
378
+ });
379
+ });
380
+
381
+ describe('isBobbinChangeset', () => {
382
+ it('accepts a valid BobbinChangeset', () => {
383
+ const cs: BobbinChangeset = {
384
+ version: '1.0',
385
+ timestamp: '2024-01-01T00:00:00Z',
386
+ changeCount: 2,
387
+ changes: [],
388
+ annotations: [],
389
+ };
390
+ expect(isBobbinChangeset(cs)).toBe(true);
391
+ });
392
+
393
+ it('rejects wrong version', () => {
394
+ const bad = {
395
+ version: '2.0',
396
+ timestamp: '2024-01-01T00:00:00Z',
397
+ changeCount: 0,
398
+ changes: [],
399
+ annotations: [],
400
+ };
401
+ expect(isBobbinChangeset(bad)).toBe(false);
402
+ });
403
+
404
+ it('rejects missing arrays', () => {
405
+ const bad = {
406
+ version: '1.0',
407
+ timestamp: '2024-01-01',
408
+ changeCount: 0,
409
+ };
410
+ expect(isBobbinChangeset(bad)).toBe(false);
411
+ });
412
+
413
+ it('rejects null', () => {
414
+ expect(isBobbinChangeset(null)).toBe(false);
415
+ });
416
+ });
@@ -1,12 +1,12 @@
1
1
  import { useState, useMemo } from 'react';
2
- import type { SelectedElement, BobbinActions, DesignTokens, Change, StyleChange, Annotation } from '../../types';
2
+ import { AnnotationSection } from './sections/AnnotationSection';
3
+ import { BackgroundSection } from './sections/BackgroundSection';
4
+ import { EffectsSection } from './sections/EffectsSection';
3
5
  import { LayoutSection } from './sections/LayoutSection';
4
- import { SpacingSection } from './sections/SpacingSection';
5
6
  import { SizeSection } from './sections/SizeSection';
7
+ import { SpacingSection } from './sections/SpacingSection';
6
8
  import { TypographySection } from './sections/TypographySection';
7
- import { BackgroundSection } from './sections/BackgroundSection';
8
- import { EffectsSection } from './sections/EffectsSection';
9
- import { AnnotationSection } from './sections/AnnotationSection';
9
+ import type { SelectedElement, BobbinActions, DesignTokens, Change, StyleChange, Annotation } from '../../types';
10
10
 
11
11
  // Theme icons
12
12
  const SunIcon = () => (
@@ -13,7 +13,7 @@ export function QuickSelectDropdown({
13
13
  tokens,
14
14
  quickKeys,
15
15
  onChange,
16
- placeholder = 'More...',
16
+ placeholder: _placeholder = 'More...',
17
17
  }: QuickSelectDropdownProps) {
18
18
  const [showDropdown, setShowDropdown] = useState(false);
19
19
 
@@ -1,7 +1,7 @@
1
- import type { DesignTokens } from '../../../types';
2
- import { SectionWrapper } from './SectionWrapper';
3
1
  import { ColorPicker } from '../controls/ColorPicker';
4
2
  import { TokenDropdown } from '../controls/TokenDropdown';
3
+ import { SectionWrapper } from './SectionWrapper';
4
+ import type { DesignTokens } from '../../../types';
5
5
 
6
6
  interface BackgroundSectionProps {
7
7
  expanded: boolean;
@@ -1,7 +1,7 @@
1
- import type { DesignTokens } from '../../../types';
2
- import { SectionWrapper } from './SectionWrapper';
3
1
  import { QuickSelectDropdown } from '../controls/QuickSelectDropdown';
4
2
  import { SliderInput } from '../controls/SliderInput';
3
+ import { SectionWrapper } from './SectionWrapper';
4
+ import type { DesignTokens } from '../../../types';
5
5
 
6
6
  interface EffectsSectionProps {
7
7
  expanded: boolean;
@@ -1,7 +1,7 @@
1
- import type { DesignTokens } from '../../../types';
2
- import { SectionWrapper } from './SectionWrapper';
3
1
  import { ToggleGroup } from '../controls/ToggleGroup';
4
2
  import { TokenDropdown } from '../controls/TokenDropdown';
3
+ import { SectionWrapper } from './SectionWrapper';
4
+ import type { DesignTokens } from '../../../types';
5
5
 
6
6
  // Direction icons
7
7
  const ArrowRightIcon = () => (
@@ -1,4 +1,4 @@
1
- import { ReactNode } from 'react';
1
+ import { type ReactNode } from 'react';
2
2
 
3
3
  interface SectionWrapperProps {
4
4
  title: string;