@aprovan/patchwork 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.
Files changed (225) hide show
  1. package/.eslintrc.json +22 -0
  2. package/.github/workflows/publish.yml +41 -0
  3. package/.prettierignore +17 -0
  4. package/LICENSE +373 -0
  5. package/README.md +15 -0
  6. package/apps/chat/.utcp_config.json +14 -0
  7. package/apps/chat/.working/widgets/27060b91-a2a5-4272-b243-6eb904bd4070/main.tsx +107 -0
  8. package/apps/chat/index.html +17 -0
  9. package/apps/chat/node_modules/.bin/autoprefixer +17 -0
  10. package/apps/chat/node_modules/.bin/browserslist +17 -0
  11. package/apps/chat/node_modules/.bin/conc +17 -0
  12. package/apps/chat/node_modules/.bin/concurrently +17 -0
  13. package/apps/chat/node_modules/.bin/copilot-proxy +17 -0
  14. package/apps/chat/node_modules/.bin/jiti +17 -0
  15. package/apps/chat/node_modules/.bin/tailwind +17 -0
  16. package/apps/chat/node_modules/.bin/tailwindcss +17 -0
  17. package/apps/chat/node_modules/.bin/tsc +17 -0
  18. package/apps/chat/node_modules/.bin/tsserver +17 -0
  19. package/apps/chat/node_modules/.bin/tsx +17 -0
  20. package/apps/chat/node_modules/.bin/vite +17 -0
  21. package/apps/chat/package.json +55 -0
  22. package/apps/chat/postcss.config.js +6 -0
  23. package/apps/chat/src/App.tsx +7 -0
  24. package/apps/chat/src/components/ui/avatar.tsx +48 -0
  25. package/apps/chat/src/components/ui/badge.tsx +36 -0
  26. package/apps/chat/src/components/ui/button.tsx +56 -0
  27. package/apps/chat/src/components/ui/card.tsx +86 -0
  28. package/apps/chat/src/components/ui/collapsible.tsx +9 -0
  29. package/apps/chat/src/components/ui/dialog.tsx +60 -0
  30. package/apps/chat/src/components/ui/input.tsx +25 -0
  31. package/apps/chat/src/components/ui/scroll-area.tsx +46 -0
  32. package/apps/chat/src/index.css +190 -0
  33. package/apps/chat/src/lib/utils.ts +6 -0
  34. package/apps/chat/src/main.tsx +10 -0
  35. package/apps/chat/src/pages/ChatPage.tsx +460 -0
  36. package/apps/chat/tailwind.config.js +71 -0
  37. package/apps/chat/tsconfig.json +25 -0
  38. package/apps/chat/vite.config.ts +26 -0
  39. package/package.json +35 -0
  40. package/packages/bobbin/node_modules/.bin/esbuild +14 -0
  41. package/packages/bobbin/node_modules/.bin/jiti +17 -0
  42. package/packages/bobbin/node_modules/.bin/tsc +17 -0
  43. package/packages/bobbin/node_modules/.bin/tsserver +17 -0
  44. package/packages/bobbin/node_modules/.bin/tsup +17 -0
  45. package/packages/bobbin/node_modules/.bin/tsup-node +17 -0
  46. package/packages/bobbin/node_modules/.bin/tsx +17 -0
  47. package/packages/bobbin/package.json +30 -0
  48. package/packages/bobbin/src/Bobbin.tsx +89 -0
  49. package/packages/bobbin/src/components/EditPanel/EditPanel.tsx +376 -0
  50. package/packages/bobbin/src/components/EditPanel/controls/ColorPicker.tsx +138 -0
  51. package/packages/bobbin/src/components/EditPanel/controls/QuickSelectDropdown.tsx +142 -0
  52. package/packages/bobbin/src/components/EditPanel/controls/SliderInput.tsx +94 -0
  53. package/packages/bobbin/src/components/EditPanel/controls/SpacingControl.tsx +285 -0
  54. package/packages/bobbin/src/components/EditPanel/controls/ToggleGroup.tsx +37 -0
  55. package/packages/bobbin/src/components/EditPanel/controls/TokenDropdown.tsx +33 -0
  56. package/packages/bobbin/src/components/EditPanel/sections/AnnotationSection.tsx +136 -0
  57. package/packages/bobbin/src/components/EditPanel/sections/BackgroundSection.tsx +79 -0
  58. package/packages/bobbin/src/components/EditPanel/sections/EffectsSection.tsx +85 -0
  59. package/packages/bobbin/src/components/EditPanel/sections/LayoutSection.tsx +224 -0
  60. package/packages/bobbin/src/components/EditPanel/sections/SectionWrapper.tsx +57 -0
  61. package/packages/bobbin/src/components/EditPanel/sections/SizeSection.tsx +166 -0
  62. package/packages/bobbin/src/components/EditPanel/sections/SpacingSection.tsx +69 -0
  63. package/packages/bobbin/src/components/EditPanel/sections/TypographySection.tsx +148 -0
  64. package/packages/bobbin/src/components/Inspector/Inspector.tsx +221 -0
  65. package/packages/bobbin/src/components/Overlay/ControlHandles.tsx +572 -0
  66. package/packages/bobbin/src/components/Overlay/MarginPaddingOverlay.tsx +229 -0
  67. package/packages/bobbin/src/components/Overlay/SelectionOverlay.tsx +73 -0
  68. package/packages/bobbin/src/components/Pill/Pill.tsx +155 -0
  69. package/packages/bobbin/src/components/ThemeToggle/ThemeToggle.tsx +72 -0
  70. package/packages/bobbin/src/core/changeSerializer.ts +139 -0
  71. package/packages/bobbin/src/core/useBobbin.ts +399 -0
  72. package/packages/bobbin/src/core/useChangeTracker.ts +186 -0
  73. package/packages/bobbin/src/core/useClipboard.ts +21 -0
  74. package/packages/bobbin/src/core/useElementSelection.ts +146 -0
  75. package/packages/bobbin/src/index.ts +46 -0
  76. package/packages/bobbin/src/tokens/borders.ts +19 -0
  77. package/packages/bobbin/src/tokens/colors.ts +150 -0
  78. package/packages/bobbin/src/tokens/index.ts +37 -0
  79. package/packages/bobbin/src/tokens/shadows.ts +10 -0
  80. package/packages/bobbin/src/tokens/spacing.ts +37 -0
  81. package/packages/bobbin/src/tokens/typography.ts +51 -0
  82. package/packages/bobbin/src/types.ts +157 -0
  83. package/packages/bobbin/src/utils/animation.ts +40 -0
  84. package/packages/bobbin/src/utils/dom.ts +36 -0
  85. package/packages/bobbin/src/utils/selectors.ts +76 -0
  86. package/packages/bobbin/tsconfig.json +10 -0
  87. package/packages/bobbin/tsup.config.ts +10 -0
  88. package/packages/compiler/node_modules/.bin/esbuild +17 -0
  89. package/packages/compiler/node_modules/.bin/jiti +17 -0
  90. package/packages/compiler/node_modules/.bin/tsc +17 -0
  91. package/packages/compiler/node_modules/.bin/tsserver +17 -0
  92. package/packages/compiler/node_modules/.bin/tsup +17 -0
  93. package/packages/compiler/node_modules/.bin/tsup-node +17 -0
  94. package/packages/compiler/node_modules/.bin/tsx +17 -0
  95. package/packages/compiler/package.json +38 -0
  96. package/packages/compiler/src/compiler.ts +258 -0
  97. package/packages/compiler/src/images/index.ts +13 -0
  98. package/packages/compiler/src/images/loader.ts +234 -0
  99. package/packages/compiler/src/images/registry.ts +112 -0
  100. package/packages/compiler/src/index.ts +141 -0
  101. package/packages/compiler/src/mount/bridge.ts +399 -0
  102. package/packages/compiler/src/mount/embedded.ts +306 -0
  103. package/packages/compiler/src/mount/iframe.ts +433 -0
  104. package/packages/compiler/src/mount/index.ts +18 -0
  105. package/packages/compiler/src/schemas.ts +169 -0
  106. package/packages/compiler/src/transforms/cdn.ts +411 -0
  107. package/packages/compiler/src/transforms/index.ts +4 -0
  108. package/packages/compiler/src/transforms/vfs.ts +138 -0
  109. package/packages/compiler/src/types.ts +233 -0
  110. package/packages/compiler/src/vfs/backends/indexeddb.ts +66 -0
  111. package/packages/compiler/src/vfs/backends/local-fs.ts +41 -0
  112. package/packages/compiler/src/vfs/backends/s3.ts +60 -0
  113. package/packages/compiler/src/vfs/index.ts +11 -0
  114. package/packages/compiler/src/vfs/project.ts +56 -0
  115. package/packages/compiler/src/vfs/store.ts +53 -0
  116. package/packages/compiler/src/vfs/types.ts +20 -0
  117. package/packages/compiler/tsconfig.json +8 -0
  118. package/packages/compiler/tsup.config.ts +14 -0
  119. package/packages/editor/node_modules/.bin/jiti +17 -0
  120. package/packages/editor/node_modules/.bin/tsc +17 -0
  121. package/packages/editor/node_modules/.bin/tsserver +17 -0
  122. package/packages/editor/node_modules/.bin/tsup +17 -0
  123. package/packages/editor/node_modules/.bin/tsup-node +17 -0
  124. package/packages/editor/node_modules/.bin/tsx +17 -0
  125. package/packages/editor/package.json +45 -0
  126. package/packages/editor/src/components/CodeBlockExtension.tsx +190 -0
  127. package/packages/editor/src/components/CodePreview.tsx +344 -0
  128. package/packages/editor/src/components/MarkdownEditor.tsx +270 -0
  129. package/packages/editor/src/components/ServicesInspector.tsx +118 -0
  130. package/packages/editor/src/components/edit/EditHistory.tsx +89 -0
  131. package/packages/editor/src/components/edit/EditModal.tsx +236 -0
  132. package/packages/editor/src/components/edit/FileTree.tsx +144 -0
  133. package/packages/editor/src/components/edit/api.ts +100 -0
  134. package/packages/editor/src/components/edit/index.ts +6 -0
  135. package/packages/editor/src/components/edit/types.ts +53 -0
  136. package/packages/editor/src/components/edit/useEditSession.ts +164 -0
  137. package/packages/editor/src/components/index.ts +5 -0
  138. package/packages/editor/src/index.ts +72 -0
  139. package/packages/editor/src/lib/code-extractor.ts +210 -0
  140. package/packages/editor/src/lib/diff.ts +308 -0
  141. package/packages/editor/src/lib/index.ts +4 -0
  142. package/packages/editor/src/lib/utils.ts +6 -0
  143. package/packages/editor/src/lib/vfs.ts +106 -0
  144. package/packages/editor/tsconfig.json +10 -0
  145. package/packages/editor/tsup.config.ts +10 -0
  146. package/packages/images/ink/node_modules/.bin/jiti +17 -0
  147. package/packages/images/ink/node_modules/.bin/tsc +17 -0
  148. package/packages/images/ink/node_modules/.bin/tsserver +17 -0
  149. package/packages/images/ink/node_modules/.bin/tsup +17 -0
  150. package/packages/images/ink/node_modules/.bin/tsup-node +17 -0
  151. package/packages/images/ink/node_modules/.bin/tsx +17 -0
  152. package/packages/images/ink/package.json +53 -0
  153. package/packages/images/ink/src/index.ts +48 -0
  154. package/packages/images/ink/src/runner.ts +331 -0
  155. package/packages/images/ink/src/setup.ts +123 -0
  156. package/packages/images/ink/tsconfig.json +10 -0
  157. package/packages/images/ink/tsup.config.ts +11 -0
  158. package/packages/images/shadcn/node_modules/.bin/jiti +17 -0
  159. package/packages/images/shadcn/node_modules/.bin/tsc +17 -0
  160. package/packages/images/shadcn/node_modules/.bin/tsserver +17 -0
  161. package/packages/images/shadcn/node_modules/.bin/tsup +17 -0
  162. package/packages/images/shadcn/node_modules/.bin/tsup-node +17 -0
  163. package/packages/images/shadcn/node_modules/.bin/tsx +17 -0
  164. package/packages/images/shadcn/package.json +82 -0
  165. package/packages/images/shadcn/src/html.ts +341 -0
  166. package/packages/images/shadcn/src/index.ts +37 -0
  167. package/packages/images/shadcn/src/setup.ts +287 -0
  168. package/packages/images/shadcn/tsconfig.json +9 -0
  169. package/packages/images/shadcn/tsup.config.ts +13 -0
  170. package/packages/images/vanilla/node_modules/.bin/jiti +17 -0
  171. package/packages/images/vanilla/node_modules/.bin/tsc +17 -0
  172. package/packages/images/vanilla/node_modules/.bin/tsserver +17 -0
  173. package/packages/images/vanilla/node_modules/.bin/tsup +17 -0
  174. package/packages/images/vanilla/node_modules/.bin/tsup-node +17 -0
  175. package/packages/images/vanilla/node_modules/.bin/tsx +17 -0
  176. package/packages/images/vanilla/package.json +35 -0
  177. package/packages/images/vanilla/src/index.ts +7 -0
  178. package/packages/images/vanilla/src/setup.ts +6 -0
  179. package/packages/images/vanilla/tsconfig.json +9 -0
  180. package/packages/images/vanilla/tsup.config.ts +10 -0
  181. package/packages/patchwork/node_modules/.bin/jiti +17 -0
  182. package/packages/patchwork/node_modules/.bin/tsc +17 -0
  183. package/packages/patchwork/node_modules/.bin/tsserver +17 -0
  184. package/packages/patchwork/node_modules/.bin/tsup +17 -0
  185. package/packages/patchwork/node_modules/.bin/tsup-node +17 -0
  186. package/packages/patchwork/node_modules/.bin/tsx +17 -0
  187. package/packages/patchwork/package.json +27 -0
  188. package/packages/patchwork/src/index.ts +15 -0
  189. package/packages/patchwork/src/services/index.ts +11 -0
  190. package/packages/patchwork/src/services/proxy.ts +213 -0
  191. package/packages/patchwork/src/services/types.ts +28 -0
  192. package/packages/patchwork/src/types.ts +116 -0
  193. package/packages/patchwork/tsconfig.json +8 -0
  194. package/packages/patchwork/tsup.config.ts +14 -0
  195. package/packages/stitchery/node_modules/.bin/jiti +17 -0
  196. package/packages/stitchery/node_modules/.bin/tsc +17 -0
  197. package/packages/stitchery/node_modules/.bin/tsserver +17 -0
  198. package/packages/stitchery/node_modules/.bin/tsup +17 -0
  199. package/packages/stitchery/node_modules/.bin/tsup-node +17 -0
  200. package/packages/stitchery/node_modules/.bin/tsx +17 -0
  201. package/packages/stitchery/package.json +40 -0
  202. package/packages/stitchery/src/cli.ts +116 -0
  203. package/packages/stitchery/src/index.ts +16 -0
  204. package/packages/stitchery/src/prompts.ts +326 -0
  205. package/packages/stitchery/src/server/index.ts +365 -0
  206. package/packages/stitchery/src/server/local-packages.ts +91 -0
  207. package/packages/stitchery/src/server/routes.ts +122 -0
  208. package/packages/stitchery/src/server/services.ts +382 -0
  209. package/packages/stitchery/src/server/vfs-routes.ts +142 -0
  210. package/packages/stitchery/src/types.ts +59 -0
  211. package/packages/stitchery/tsconfig.json +13 -0
  212. package/packages/stitchery/tsup.config.ts +15 -0
  213. package/packages/utcp/node_modules/.bin/jiti +17 -0
  214. package/packages/utcp/node_modules/.bin/tsc +17 -0
  215. package/packages/utcp/node_modules/.bin/tsserver +17 -0
  216. package/packages/utcp/node_modules/.bin/tsup +17 -0
  217. package/packages/utcp/node_modules/.bin/tsup-node +17 -0
  218. package/packages/utcp/node_modules/.bin/tsx +17 -0
  219. package/packages/utcp/package.json +38 -0
  220. package/packages/utcp/src/index.ts +153 -0
  221. package/packages/utcp/tsconfig.json +8 -0
  222. package/packages/utcp/tsup.config.ts +12 -0
  223. package/pnpm-workspace.yaml +3 -0
  224. package/tsconfig.json +18 -0
  225. package/turbo.json +23 -0
@@ -0,0 +1,399 @@
1
+ import { useState, useCallback, useMemo, useEffect } from 'react';
2
+ import type {
3
+ BobbinState,
4
+ BobbinActions,
5
+ BobbinProps,
6
+ DesignTokens,
7
+ Annotation,
8
+ } 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
+
17
+ export function useBobbin(props: BobbinProps = {}) {
18
+ const {
19
+ tokens: customTokens,
20
+ container,
21
+ defaultActive = false,
22
+ onChanges,
23
+ onSelect,
24
+ exclude = [],
25
+ } = props;
26
+
27
+ const [isActive, setIsActive] = useState(defaultActive);
28
+ const [isPillExpanded, setIsPillExpanded] = useState(false);
29
+ const [annotations, setAnnotations] = useState<Annotation[]>([]);
30
+ const [showMarginPadding, setShowMarginPadding] = useState(false);
31
+ const [activePanel, setActivePanel] = useState<'style' | 'inspector' | null>(
32
+ 'style',
33
+ );
34
+ const [theme, setTheme] = useState<'light' | 'dark' | 'system'>('system');
35
+
36
+ // Merge tokens
37
+ const tokens = useMemo<DesignTokens>(
38
+ () => ({
39
+ ...defaultTokens,
40
+ ...customTokens,
41
+ }),
42
+ [customTokens],
43
+ );
44
+
45
+ // Element selection
46
+ const { hoveredElement, selectedElement, selectElement, clearSelection } =
47
+ useElementSelection({
48
+ container,
49
+ exclude: [...exclude, '[data-bobbin]'],
50
+ enabled: isActive,
51
+ });
52
+
53
+ // Change tracking
54
+ const changeTracker = useChangeTracker();
55
+
56
+ // Clipboard
57
+ const clipboard = useClipboard();
58
+
59
+ // Enable contenteditable on selection
60
+ useEffect(() => {
61
+ if (selectedElement) {
62
+ enableContentEditable(selectedElement.element, true);
63
+ return () => enableContentEditable(selectedElement.element, false);
64
+ }
65
+ }, [selectedElement]);
66
+
67
+ // Notify on changes
68
+ useEffect(() => {
69
+ onChanges?.(changeTracker.changes);
70
+ }, [changeTracker.changes, onChanges]);
71
+
72
+ // Notify on selection
73
+ useEffect(() => {
74
+ onSelect?.(selectedElement);
75
+ }, [selectedElement, onSelect]);
76
+
77
+ // Theme management
78
+ useEffect(() => {
79
+ const root = document.documentElement;
80
+ if (theme === 'dark') {
81
+ root.classList.add('dark');
82
+ root.style.colorScheme = 'dark';
83
+ } else if (theme === 'light') {
84
+ root.classList.remove('dark');
85
+ root.style.colorScheme = 'light';
86
+ } else {
87
+ // System preference
88
+ const prefersDark = window.matchMedia(
89
+ '(prefers-color-scheme: dark)',
90
+ ).matches;
91
+ root.classList.toggle('dark', prefersDark);
92
+ root.style.colorScheme = prefersDark ? 'dark' : 'light';
93
+ }
94
+ }, [theme]);
95
+
96
+ // Actions
97
+ const activate = useCallback(() => setIsActive(true), []);
98
+ const deactivate = useCallback(() => {
99
+ setIsActive(false);
100
+ clearSelection();
101
+ }, [clearSelection]);
102
+
103
+ const applyStyle = useCallback(
104
+ (property: string, value: string) => {
105
+ if (!selectedElement) return;
106
+
107
+ const el = selectedElement.element;
108
+ const originalValue = getComputedStyle(el).getPropertyValue(property);
109
+
110
+ applyStyleToElement(el, property, value);
111
+ changeTracker.recordStyleChange(
112
+ selectedElement.path,
113
+ selectedElement.xpath,
114
+ selectedElement.tagName,
115
+ property,
116
+ value,
117
+ originalValue,
118
+ );
119
+ },
120
+ [selectedElement, changeTracker],
121
+ );
122
+
123
+ const deleteElement = useCallback(() => {
124
+ if (!selectedElement) return;
125
+
126
+ const el = selectedElement.element;
127
+ const parent = el.parentElement;
128
+ if (!parent) return;
129
+
130
+ changeTracker.recordChange(
131
+ 'delete',
132
+ selectedElement.path,
133
+ selectedElement.xpath,
134
+ selectedElement.tagName,
135
+ el.outerHTML,
136
+ null,
137
+ );
138
+
139
+ el.remove();
140
+ clearSelection();
141
+ }, [selectedElement, changeTracker, clearSelection]);
142
+
143
+ const moveElement = useCallback(
144
+ (targetParent: HTMLElement, index: number) => {
145
+ if (!selectedElement) return;
146
+
147
+ const el = selectedElement.element;
148
+ const fromParent = el.parentElement;
149
+ if (!fromParent) return;
150
+
151
+ const fromIndex = Array.from(fromParent.children).indexOf(el);
152
+ const fromPath = getElementPath(fromParent);
153
+ const toPath = getElementPath(targetParent);
154
+
155
+ if (index >= targetParent.children.length) {
156
+ targetParent.appendChild(el);
157
+ } else {
158
+ const referenceNode = targetParent.children[index] ?? null;
159
+ targetParent.insertBefore(el, referenceNode);
160
+ }
161
+
162
+ changeTracker.recordMoveChange(
163
+ selectedElement.path,
164
+ selectedElement.xpath,
165
+ selectedElement.tagName,
166
+ fromPath,
167
+ fromIndex,
168
+ toPath,
169
+ index,
170
+ );
171
+ },
172
+ [selectedElement, changeTracker],
173
+ );
174
+
175
+ const duplicateElement = useCallback(() => {
176
+ if (!selectedElement) return;
177
+
178
+ const el = selectedElement.element;
179
+ const clone = el.cloneNode(true) as HTMLElement;
180
+ const parent = el.parentElement;
181
+
182
+ if (parent) {
183
+ parent.insertBefore(clone, el.nextSibling);
184
+ }
185
+
186
+ changeTracker.recordChange(
187
+ 'duplicate',
188
+ selectedElement.path,
189
+ selectedElement.xpath,
190
+ selectedElement.tagName,
191
+ null,
192
+ clone.outerHTML,
193
+ );
194
+
195
+ // Select the newly duplicated element
196
+ selectElement(clone);
197
+ }, [selectedElement, changeTracker, selectElement]);
198
+
199
+ const insertElement = useCallback(
200
+ (direction: 'before' | 'after' | 'child', content = '') => {
201
+ if (!selectedElement) return;
202
+
203
+ const el = selectedElement.element;
204
+ const newEl = document.createElement('span');
205
+ newEl.textContent = content || '\u200B'; // Zero-width space if empty
206
+ newEl.contentEditable = 'true';
207
+
208
+ if (direction === 'child') {
209
+ el.appendChild(newEl);
210
+ } else if (direction === 'before') {
211
+ el.parentElement?.insertBefore(newEl, el);
212
+ } else {
213
+ el.parentElement?.insertBefore(newEl, el.nextSibling);
214
+ }
215
+
216
+ changeTracker.recordChange(
217
+ 'insert',
218
+ getElementPath(newEl),
219
+ getElementXPath(newEl),
220
+ 'span',
221
+ null,
222
+ newEl.outerHTML,
223
+ { direction },
224
+ );
225
+
226
+ // Select the newly inserted element
227
+ selectElement(newEl);
228
+ },
229
+ [selectedElement, changeTracker, selectElement],
230
+ );
231
+
232
+ const copyElement = useCallback(() => {
233
+ if (!selectedElement) return;
234
+ clipboard.copy(selectedElement);
235
+ }, [selectedElement, clipboard]);
236
+
237
+ const pasteElement = useCallback(
238
+ (direction: 'before' | 'after' | 'child') => {
239
+ if (!selectedElement || !clipboard.copied) return;
240
+
241
+ const el = selectedElement.element;
242
+ const clone = clipboard.copied.element.cloneNode(true) as HTMLElement;
243
+
244
+ if (direction === 'child') {
245
+ el.appendChild(clone);
246
+ } else if (direction === 'before') {
247
+ el.parentElement?.insertBefore(clone, el);
248
+ } else {
249
+ el.parentElement?.insertBefore(clone, el.nextSibling);
250
+ }
251
+
252
+ changeTracker.recordChange(
253
+ 'insert',
254
+ getElementPath(clone),
255
+ getElementXPath(clone),
256
+ clone.tagName.toLowerCase(),
257
+ null,
258
+ clone.outerHTML,
259
+ { source: 'paste', direction },
260
+ );
261
+
262
+ // Select the newly pasted element
263
+ selectElement(clone);
264
+ },
265
+ [selectedElement, clipboard, changeTracker, selectElement],
266
+ );
267
+
268
+ const annotate = useCallback(
269
+ (content: string) => {
270
+ if (!selectedElement) return;
271
+
272
+ setAnnotations((prev) => {
273
+ // Check if annotation already exists for this element
274
+ const existingIndex = prev.findIndex(
275
+ (a) => a.elementPath === selectedElement.path,
276
+ );
277
+
278
+ if (existingIndex >= 0) {
279
+ const existing = prev[existingIndex];
280
+ if (!existing) return prev;
281
+
282
+ // Update existing annotation
283
+ if (!content.trim()) {
284
+ // Remove annotation if content is empty
285
+ return prev.filter((_, i) => i !== existingIndex);
286
+ }
287
+ const updated = [...prev];
288
+ updated[existingIndex] = {
289
+ id: existing.id,
290
+ elementPath: existing.elementPath,
291
+ elementXpath: existing.elementXpath,
292
+ content,
293
+ createdAt: Date.now(),
294
+ };
295
+ return updated;
296
+ } else if (content.trim()) {
297
+ // Add new annotation only if content is not empty
298
+ const annotation: Annotation = {
299
+ id: crypto.randomUUID(),
300
+ elementPath: selectedElement.path,
301
+ elementXpath: selectedElement.xpath,
302
+ content,
303
+ createdAt: Date.now(),
304
+ };
305
+ return [...prev, annotation];
306
+ }
307
+ return prev;
308
+ });
309
+ },
310
+ [selectedElement],
311
+ );
312
+
313
+ const toggleMarginPadding = useCallback(() => {
314
+ setShowMarginPadding((prev) => !prev);
315
+ }, []);
316
+
317
+ const toggleTheme = useCallback(() => {
318
+ setTheme((prev) => {
319
+ if (prev === 'light') return 'dark';
320
+ if (prev === 'dark') return 'system';
321
+ return 'light';
322
+ });
323
+ }, []);
324
+
325
+ const undo = useCallback(() => {
326
+ const lastChange = changeTracker.undo();
327
+ if (!lastChange) return;
328
+
329
+ // TODO: Implement actual undo logic based on change type
330
+ console.log('Undoing:', lastChange);
331
+ }, [changeTracker]);
332
+
333
+ // Reset all style changes by reverting to original values
334
+ const resetChanges = useCallback(() => {
335
+ const originalStates = changeTracker.originalStates;
336
+
337
+ // Revert all changes by applying original values
338
+ for (const [path, properties] of originalStates.entries()) {
339
+ // Find the element by path
340
+ const el = document.querySelector(path) as HTMLElement;
341
+ if (!el) continue;
342
+
343
+ for (const [property, originalValue] of properties.entries()) {
344
+ applyStyleToElement(el, property, originalValue);
345
+ }
346
+ }
347
+
348
+ changeTracker.clearChanges();
349
+ }, [changeTracker]);
350
+
351
+ const exportChanges = useCallback(() => {
352
+ return serializeChangesToYAML(
353
+ changeTracker.deduplicatedChanges,
354
+ annotations,
355
+ );
356
+ }, [changeTracker.deduplicatedChanges, annotations]);
357
+
358
+ const state: BobbinState = {
359
+ isActive,
360
+ isPillExpanded,
361
+ hoveredElement,
362
+ selectedElement,
363
+ changes: changeTracker.deduplicatedChanges,
364
+ annotations,
365
+ clipboard: clipboard.copied,
366
+ showMarginPadding,
367
+ activePanel,
368
+ theme,
369
+ };
370
+
371
+ const actions: BobbinActions = {
372
+ activate,
373
+ deactivate,
374
+ selectElement,
375
+ clearSelection,
376
+ applyStyle,
377
+ deleteElement,
378
+ moveElement,
379
+ duplicateElement,
380
+ insertElement,
381
+ copyElement,
382
+ pasteElement,
383
+ annotate,
384
+ toggleMarginPadding,
385
+ toggleTheme,
386
+ undo,
387
+ exportChanges,
388
+ getChanges: changeTracker.getChanges,
389
+ resetChanges,
390
+ };
391
+
392
+ return {
393
+ ...state,
394
+ ...actions,
395
+ tokens,
396
+ setActivePanel,
397
+ setIsPillExpanded,
398
+ };
399
+ }
@@ -0,0 +1,186 @@
1
+ import { useState, useCallback, useRef, useMemo } from 'react';
2
+ import type {
3
+ Change,
4
+ StyleChange,
5
+ TextChange,
6
+ MoveChange,
7
+ ChangeType,
8
+ } from '../types';
9
+ import { generateId } from '../utils/selectors';
10
+
11
+ export function useChangeTracker() {
12
+ const [changes, setChanges] = useState<Change[]>([]);
13
+ const historyRef = useRef<Change[]>([]);
14
+ const originalStatesRef = useRef<Map<string, Map<string, string>>>(new Map());
15
+
16
+ const recordOriginalState = useCallback(
17
+ (path: string, property: string, value: string) => {
18
+ if (!originalStatesRef.current.has(path)) {
19
+ originalStatesRef.current.set(path, new Map());
20
+ }
21
+ const elementState = originalStatesRef.current.get(path)!;
22
+ if (!elementState.has(property)) {
23
+ elementState.set(property, value);
24
+ }
25
+ },
26
+ [],
27
+ );
28
+
29
+ const addChange = useCallback(
30
+ <T extends Change>(change: Omit<T, 'id' | 'timestamp'>) => {
31
+ const fullChange = {
32
+ ...change,
33
+ id: generateId(),
34
+ timestamp: Date.now(),
35
+ } as T;
36
+
37
+ setChanges((prev) => [...prev, fullChange]);
38
+ historyRef.current.push(fullChange);
39
+ return fullChange;
40
+ },
41
+ [],
42
+ );
43
+
44
+ const recordStyleChange = useCallback(
45
+ (
46
+ path: string,
47
+ xpath: string,
48
+ tagName: string,
49
+ property: string,
50
+ value: string,
51
+ originalValue: string,
52
+ ): StyleChange => {
53
+ recordOriginalState(path, property, originalValue);
54
+
55
+ return addChange<StyleChange>({
56
+ type: 'style',
57
+ target: { path, xpath, tagName },
58
+ before: { property, value: originalValue },
59
+ after: { property, value },
60
+ });
61
+ },
62
+ [addChange, recordOriginalState],
63
+ );
64
+
65
+ const recordTextChange = useCallback(
66
+ (
67
+ path: string,
68
+ xpath: string,
69
+ tagName: string,
70
+ originalText: string,
71
+ newText: string,
72
+ ): TextChange => {
73
+ return addChange<TextChange>({
74
+ type: 'text',
75
+ target: { path, xpath, tagName },
76
+ before: originalText,
77
+ after: newText,
78
+ });
79
+ },
80
+ [addChange],
81
+ );
82
+
83
+ const recordMoveChange = useCallback(
84
+ (
85
+ path: string,
86
+ xpath: string,
87
+ tagName: string,
88
+ fromParent: string,
89
+ fromIndex: number,
90
+ toParent: string,
91
+ toIndex: number,
92
+ ): MoveChange => {
93
+ return addChange<MoveChange>({
94
+ type: 'move',
95
+ target: { path, xpath, tagName },
96
+ before: { parent: fromParent, index: fromIndex },
97
+ after: { parent: toParent, index: toIndex },
98
+ });
99
+ },
100
+ [addChange],
101
+ );
102
+
103
+ const recordChange = useCallback(
104
+ (
105
+ type: ChangeType,
106
+ path: string,
107
+ xpath: string,
108
+ tagName: string,
109
+ before: unknown,
110
+ after: unknown,
111
+ metadata?: Record<string, unknown>,
112
+ ) => {
113
+ return addChange({
114
+ type,
115
+ target: { path, xpath, tagName },
116
+ before,
117
+ after,
118
+ metadata,
119
+ });
120
+ },
121
+ [addChange],
122
+ );
123
+
124
+ const undo = useCallback(() => {
125
+ if (changes.length === 0) return null;
126
+
127
+ const lastChange = changes[changes.length - 1];
128
+ setChanges((prev) => prev.slice(0, -1));
129
+ return lastChange;
130
+ }, [changes]);
131
+
132
+ const clearChanges = useCallback(() => {
133
+ setChanges([]);
134
+ originalStatesRef.current.clear();
135
+ }, []);
136
+
137
+ const getChanges = useCallback(() => [...changes], [changes]);
138
+
139
+ // Deduplicated changes - only count unique (element + property) combinations
140
+ // 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]);
172
+
173
+ return {
174
+ changes,
175
+ deduplicatedChanges,
176
+ changeCount: deduplicatedChanges.length,
177
+ recordStyleChange,
178
+ recordTextChange,
179
+ recordMoveChange,
180
+ recordChange,
181
+ undo,
182
+ clearChanges,
183
+ getChanges,
184
+ originalStates: originalStatesRef.current,
185
+ };
186
+ }
@@ -0,0 +1,21 @@
1
+ import { useState, useCallback } from 'react';
2
+ import type { SelectedElement } from '../types';
3
+
4
+ export function useClipboard() {
5
+ const [copied, setCopied] = useState<SelectedElement | null>(null);
6
+
7
+ const copy = useCallback((element: SelectedElement) => {
8
+ setCopied(element);
9
+ }, []);
10
+
11
+ const clear = useCallback(() => {
12
+ setCopied(null);
13
+ }, []);
14
+
15
+ return {
16
+ copied,
17
+ copy,
18
+ clear,
19
+ hasCopied: copied !== null,
20
+ };
21
+ }