@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.
- package/.eslintrc.json +22 -0
- package/.github/workflows/publish.yml +41 -0
- package/.prettierignore +17 -0
- package/LICENSE +373 -0
- package/README.md +15 -0
- package/apps/chat/.utcp_config.json +14 -0
- package/apps/chat/.working/widgets/27060b91-a2a5-4272-b243-6eb904bd4070/main.tsx +107 -0
- package/apps/chat/index.html +17 -0
- package/apps/chat/node_modules/.bin/autoprefixer +17 -0
- package/apps/chat/node_modules/.bin/browserslist +17 -0
- package/apps/chat/node_modules/.bin/conc +17 -0
- package/apps/chat/node_modules/.bin/concurrently +17 -0
- package/apps/chat/node_modules/.bin/copilot-proxy +17 -0
- package/apps/chat/node_modules/.bin/jiti +17 -0
- package/apps/chat/node_modules/.bin/tailwind +17 -0
- package/apps/chat/node_modules/.bin/tailwindcss +17 -0
- package/apps/chat/node_modules/.bin/tsc +17 -0
- package/apps/chat/node_modules/.bin/tsserver +17 -0
- package/apps/chat/node_modules/.bin/tsx +17 -0
- package/apps/chat/node_modules/.bin/vite +17 -0
- package/apps/chat/package.json +55 -0
- package/apps/chat/postcss.config.js +6 -0
- package/apps/chat/src/App.tsx +7 -0
- package/apps/chat/src/components/ui/avatar.tsx +48 -0
- package/apps/chat/src/components/ui/badge.tsx +36 -0
- package/apps/chat/src/components/ui/button.tsx +56 -0
- package/apps/chat/src/components/ui/card.tsx +86 -0
- package/apps/chat/src/components/ui/collapsible.tsx +9 -0
- package/apps/chat/src/components/ui/dialog.tsx +60 -0
- package/apps/chat/src/components/ui/input.tsx +25 -0
- package/apps/chat/src/components/ui/scroll-area.tsx +46 -0
- package/apps/chat/src/index.css +190 -0
- package/apps/chat/src/lib/utils.ts +6 -0
- package/apps/chat/src/main.tsx +10 -0
- package/apps/chat/src/pages/ChatPage.tsx +460 -0
- package/apps/chat/tailwind.config.js +71 -0
- package/apps/chat/tsconfig.json +25 -0
- package/apps/chat/vite.config.ts +26 -0
- package/package.json +35 -0
- package/packages/bobbin/node_modules/.bin/esbuild +14 -0
- package/packages/bobbin/node_modules/.bin/jiti +17 -0
- package/packages/bobbin/node_modules/.bin/tsc +17 -0
- package/packages/bobbin/node_modules/.bin/tsserver +17 -0
- package/packages/bobbin/node_modules/.bin/tsup +17 -0
- package/packages/bobbin/node_modules/.bin/tsup-node +17 -0
- package/packages/bobbin/node_modules/.bin/tsx +17 -0
- package/packages/bobbin/package.json +30 -0
- package/packages/bobbin/src/Bobbin.tsx +89 -0
- package/packages/bobbin/src/components/EditPanel/EditPanel.tsx +376 -0
- package/packages/bobbin/src/components/EditPanel/controls/ColorPicker.tsx +138 -0
- package/packages/bobbin/src/components/EditPanel/controls/QuickSelectDropdown.tsx +142 -0
- package/packages/bobbin/src/components/EditPanel/controls/SliderInput.tsx +94 -0
- package/packages/bobbin/src/components/EditPanel/controls/SpacingControl.tsx +285 -0
- package/packages/bobbin/src/components/EditPanel/controls/ToggleGroup.tsx +37 -0
- package/packages/bobbin/src/components/EditPanel/controls/TokenDropdown.tsx +33 -0
- package/packages/bobbin/src/components/EditPanel/sections/AnnotationSection.tsx +136 -0
- package/packages/bobbin/src/components/EditPanel/sections/BackgroundSection.tsx +79 -0
- package/packages/bobbin/src/components/EditPanel/sections/EffectsSection.tsx +85 -0
- package/packages/bobbin/src/components/EditPanel/sections/LayoutSection.tsx +224 -0
- package/packages/bobbin/src/components/EditPanel/sections/SectionWrapper.tsx +57 -0
- package/packages/bobbin/src/components/EditPanel/sections/SizeSection.tsx +166 -0
- package/packages/bobbin/src/components/EditPanel/sections/SpacingSection.tsx +69 -0
- package/packages/bobbin/src/components/EditPanel/sections/TypographySection.tsx +148 -0
- package/packages/bobbin/src/components/Inspector/Inspector.tsx +221 -0
- package/packages/bobbin/src/components/Overlay/ControlHandles.tsx +572 -0
- package/packages/bobbin/src/components/Overlay/MarginPaddingOverlay.tsx +229 -0
- package/packages/bobbin/src/components/Overlay/SelectionOverlay.tsx +73 -0
- package/packages/bobbin/src/components/Pill/Pill.tsx +155 -0
- package/packages/bobbin/src/components/ThemeToggle/ThemeToggle.tsx +72 -0
- package/packages/bobbin/src/core/changeSerializer.ts +139 -0
- package/packages/bobbin/src/core/useBobbin.ts +399 -0
- package/packages/bobbin/src/core/useChangeTracker.ts +186 -0
- package/packages/bobbin/src/core/useClipboard.ts +21 -0
- package/packages/bobbin/src/core/useElementSelection.ts +146 -0
- package/packages/bobbin/src/index.ts +46 -0
- package/packages/bobbin/src/tokens/borders.ts +19 -0
- package/packages/bobbin/src/tokens/colors.ts +150 -0
- package/packages/bobbin/src/tokens/index.ts +37 -0
- package/packages/bobbin/src/tokens/shadows.ts +10 -0
- package/packages/bobbin/src/tokens/spacing.ts +37 -0
- package/packages/bobbin/src/tokens/typography.ts +51 -0
- package/packages/bobbin/src/types.ts +157 -0
- package/packages/bobbin/src/utils/animation.ts +40 -0
- package/packages/bobbin/src/utils/dom.ts +36 -0
- package/packages/bobbin/src/utils/selectors.ts +76 -0
- package/packages/bobbin/tsconfig.json +10 -0
- package/packages/bobbin/tsup.config.ts +10 -0
- package/packages/compiler/node_modules/.bin/esbuild +17 -0
- package/packages/compiler/node_modules/.bin/jiti +17 -0
- package/packages/compiler/node_modules/.bin/tsc +17 -0
- package/packages/compiler/node_modules/.bin/tsserver +17 -0
- package/packages/compiler/node_modules/.bin/tsup +17 -0
- package/packages/compiler/node_modules/.bin/tsup-node +17 -0
- package/packages/compiler/node_modules/.bin/tsx +17 -0
- package/packages/compiler/package.json +38 -0
- package/packages/compiler/src/compiler.ts +258 -0
- package/packages/compiler/src/images/index.ts +13 -0
- package/packages/compiler/src/images/loader.ts +234 -0
- package/packages/compiler/src/images/registry.ts +112 -0
- package/packages/compiler/src/index.ts +141 -0
- package/packages/compiler/src/mount/bridge.ts +399 -0
- package/packages/compiler/src/mount/embedded.ts +306 -0
- package/packages/compiler/src/mount/iframe.ts +433 -0
- package/packages/compiler/src/mount/index.ts +18 -0
- package/packages/compiler/src/schemas.ts +169 -0
- package/packages/compiler/src/transforms/cdn.ts +411 -0
- package/packages/compiler/src/transforms/index.ts +4 -0
- package/packages/compiler/src/transforms/vfs.ts +138 -0
- package/packages/compiler/src/types.ts +233 -0
- package/packages/compiler/src/vfs/backends/indexeddb.ts +66 -0
- package/packages/compiler/src/vfs/backends/local-fs.ts +41 -0
- package/packages/compiler/src/vfs/backends/s3.ts +60 -0
- package/packages/compiler/src/vfs/index.ts +11 -0
- package/packages/compiler/src/vfs/project.ts +56 -0
- package/packages/compiler/src/vfs/store.ts +53 -0
- package/packages/compiler/src/vfs/types.ts +20 -0
- package/packages/compiler/tsconfig.json +8 -0
- package/packages/compiler/tsup.config.ts +14 -0
- package/packages/editor/node_modules/.bin/jiti +17 -0
- package/packages/editor/node_modules/.bin/tsc +17 -0
- package/packages/editor/node_modules/.bin/tsserver +17 -0
- package/packages/editor/node_modules/.bin/tsup +17 -0
- package/packages/editor/node_modules/.bin/tsup-node +17 -0
- package/packages/editor/node_modules/.bin/tsx +17 -0
- package/packages/editor/package.json +45 -0
- package/packages/editor/src/components/CodeBlockExtension.tsx +190 -0
- package/packages/editor/src/components/CodePreview.tsx +344 -0
- package/packages/editor/src/components/MarkdownEditor.tsx +270 -0
- package/packages/editor/src/components/ServicesInspector.tsx +118 -0
- package/packages/editor/src/components/edit/EditHistory.tsx +89 -0
- package/packages/editor/src/components/edit/EditModal.tsx +236 -0
- package/packages/editor/src/components/edit/FileTree.tsx +144 -0
- package/packages/editor/src/components/edit/api.ts +100 -0
- package/packages/editor/src/components/edit/index.ts +6 -0
- package/packages/editor/src/components/edit/types.ts +53 -0
- package/packages/editor/src/components/edit/useEditSession.ts +164 -0
- package/packages/editor/src/components/index.ts +5 -0
- package/packages/editor/src/index.ts +72 -0
- package/packages/editor/src/lib/code-extractor.ts +210 -0
- package/packages/editor/src/lib/diff.ts +308 -0
- package/packages/editor/src/lib/index.ts +4 -0
- package/packages/editor/src/lib/utils.ts +6 -0
- package/packages/editor/src/lib/vfs.ts +106 -0
- package/packages/editor/tsconfig.json +10 -0
- package/packages/editor/tsup.config.ts +10 -0
- package/packages/images/ink/node_modules/.bin/jiti +17 -0
- package/packages/images/ink/node_modules/.bin/tsc +17 -0
- package/packages/images/ink/node_modules/.bin/tsserver +17 -0
- package/packages/images/ink/node_modules/.bin/tsup +17 -0
- package/packages/images/ink/node_modules/.bin/tsup-node +17 -0
- package/packages/images/ink/node_modules/.bin/tsx +17 -0
- package/packages/images/ink/package.json +53 -0
- package/packages/images/ink/src/index.ts +48 -0
- package/packages/images/ink/src/runner.ts +331 -0
- package/packages/images/ink/src/setup.ts +123 -0
- package/packages/images/ink/tsconfig.json +10 -0
- package/packages/images/ink/tsup.config.ts +11 -0
- package/packages/images/shadcn/node_modules/.bin/jiti +17 -0
- package/packages/images/shadcn/node_modules/.bin/tsc +17 -0
- package/packages/images/shadcn/node_modules/.bin/tsserver +17 -0
- package/packages/images/shadcn/node_modules/.bin/tsup +17 -0
- package/packages/images/shadcn/node_modules/.bin/tsup-node +17 -0
- package/packages/images/shadcn/node_modules/.bin/tsx +17 -0
- package/packages/images/shadcn/package.json +82 -0
- package/packages/images/shadcn/src/html.ts +341 -0
- package/packages/images/shadcn/src/index.ts +37 -0
- package/packages/images/shadcn/src/setup.ts +287 -0
- package/packages/images/shadcn/tsconfig.json +9 -0
- package/packages/images/shadcn/tsup.config.ts +13 -0
- package/packages/images/vanilla/node_modules/.bin/jiti +17 -0
- package/packages/images/vanilla/node_modules/.bin/tsc +17 -0
- package/packages/images/vanilla/node_modules/.bin/tsserver +17 -0
- package/packages/images/vanilla/node_modules/.bin/tsup +17 -0
- package/packages/images/vanilla/node_modules/.bin/tsup-node +17 -0
- package/packages/images/vanilla/node_modules/.bin/tsx +17 -0
- package/packages/images/vanilla/package.json +35 -0
- package/packages/images/vanilla/src/index.ts +7 -0
- package/packages/images/vanilla/src/setup.ts +6 -0
- package/packages/images/vanilla/tsconfig.json +9 -0
- package/packages/images/vanilla/tsup.config.ts +10 -0
- package/packages/patchwork/node_modules/.bin/jiti +17 -0
- package/packages/patchwork/node_modules/.bin/tsc +17 -0
- package/packages/patchwork/node_modules/.bin/tsserver +17 -0
- package/packages/patchwork/node_modules/.bin/tsup +17 -0
- package/packages/patchwork/node_modules/.bin/tsup-node +17 -0
- package/packages/patchwork/node_modules/.bin/tsx +17 -0
- package/packages/patchwork/package.json +27 -0
- package/packages/patchwork/src/index.ts +15 -0
- package/packages/patchwork/src/services/index.ts +11 -0
- package/packages/patchwork/src/services/proxy.ts +213 -0
- package/packages/patchwork/src/services/types.ts +28 -0
- package/packages/patchwork/src/types.ts +116 -0
- package/packages/patchwork/tsconfig.json +8 -0
- package/packages/patchwork/tsup.config.ts +14 -0
- package/packages/stitchery/node_modules/.bin/jiti +17 -0
- package/packages/stitchery/node_modules/.bin/tsc +17 -0
- package/packages/stitchery/node_modules/.bin/tsserver +17 -0
- package/packages/stitchery/node_modules/.bin/tsup +17 -0
- package/packages/stitchery/node_modules/.bin/tsup-node +17 -0
- package/packages/stitchery/node_modules/.bin/tsx +17 -0
- package/packages/stitchery/package.json +40 -0
- package/packages/stitchery/src/cli.ts +116 -0
- package/packages/stitchery/src/index.ts +16 -0
- package/packages/stitchery/src/prompts.ts +326 -0
- package/packages/stitchery/src/server/index.ts +365 -0
- package/packages/stitchery/src/server/local-packages.ts +91 -0
- package/packages/stitchery/src/server/routes.ts +122 -0
- package/packages/stitchery/src/server/services.ts +382 -0
- package/packages/stitchery/src/server/vfs-routes.ts +142 -0
- package/packages/stitchery/src/types.ts +59 -0
- package/packages/stitchery/tsconfig.json +13 -0
- package/packages/stitchery/tsup.config.ts +15 -0
- package/packages/utcp/node_modules/.bin/jiti +17 -0
- package/packages/utcp/node_modules/.bin/tsc +17 -0
- package/packages/utcp/node_modules/.bin/tsserver +17 -0
- package/packages/utcp/node_modules/.bin/tsup +17 -0
- package/packages/utcp/node_modules/.bin/tsup-node +17 -0
- package/packages/utcp/node_modules/.bin/tsx +17 -0
- package/packages/utcp/package.json +38 -0
- package/packages/utcp/src/index.ts +153 -0
- package/packages/utcp/tsconfig.json +8 -0
- package/packages/utcp/tsup.config.ts +12 -0
- package/pnpm-workspace.yaml +3 -0
- package/tsconfig.json +18 -0
- 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
|
+
}
|