@promakeai/inspector 0.2.2 → 1.0.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 (170) hide show
  1. package/dist/inspector.css +1 -0
  2. package/dist/inspector.js +8740 -0
  3. package/dist/packages/inspector/src/App.d.ts +5 -0
  4. package/dist/packages/inspector/src/App.d.ts.map +1 -0
  5. package/dist/packages/inspector/src/__tests__/App.test.d.ts +5 -0
  6. package/dist/packages/inspector/src/__tests__/App.test.d.ts.map +1 -0
  7. package/dist/packages/inspector/src/components/Badge.d.ts +9 -0
  8. package/dist/packages/inspector/src/components/Badge.d.ts.map +1 -0
  9. package/dist/packages/inspector/src/components/ControlBox/ContentArea.d.ts +10 -0
  10. package/dist/packages/inspector/src/components/ControlBox/ContentArea.d.ts.map +1 -0
  11. package/dist/packages/inspector/src/components/ControlBox/PromptInput.d.ts +12 -0
  12. package/dist/packages/inspector/src/components/ControlBox/PromptInput.d.ts.map +1 -0
  13. package/dist/packages/inspector/src/components/ControlBox/index.d.ts +21 -0
  14. package/dist/packages/inspector/src/components/ControlBox/index.d.ts.map +1 -0
  15. package/dist/packages/inspector/src/components/ImageEditor/UploadBox.d.ts +10 -0
  16. package/dist/packages/inspector/src/components/ImageEditor/UploadBox.d.ts.map +1 -0
  17. package/dist/packages/inspector/src/components/ImageEditor/index.d.ts +11 -0
  18. package/dist/packages/inspector/src/components/ImageEditor/index.d.ts.map +1 -0
  19. package/dist/packages/inspector/src/components/Overlay.d.ts +11 -0
  20. package/dist/packages/inspector/src/components/Overlay.d.ts.map +1 -0
  21. package/dist/packages/inspector/src/components/StyleEditor/BorderSection.d.ts +13 -0
  22. package/dist/packages/inspector/src/components/StyleEditor/BorderSection.d.ts.map +1 -0
  23. package/dist/packages/inspector/src/components/StyleEditor/ColorPicker.d.ts +13 -0
  24. package/dist/packages/inspector/src/components/StyleEditor/ColorPicker.d.ts.map +1 -0
  25. package/dist/packages/inspector/src/components/StyleEditor/DisplaySection.d.ts +13 -0
  26. package/dist/packages/inspector/src/components/StyleEditor/DisplaySection.d.ts.map +1 -0
  27. package/dist/packages/inspector/src/components/StyleEditor/ImageSection.d.ts +13 -0
  28. package/dist/packages/inspector/src/components/StyleEditor/ImageSection.d.ts.map +1 -0
  29. package/dist/packages/inspector/src/components/StyleEditor/LayoutSection.d.ts +13 -0
  30. package/dist/packages/inspector/src/components/StyleEditor/LayoutSection.d.ts.map +1 -0
  31. package/dist/packages/inspector/src/components/StyleEditor/NumberInput.d.ts +17 -0
  32. package/dist/packages/inspector/src/components/StyleEditor/NumberInput.d.ts.map +1 -0
  33. package/dist/packages/inspector/src/components/StyleEditor/SliderInput.d.ts +16 -0
  34. package/dist/packages/inspector/src/components/StyleEditor/SliderInput.d.ts.map +1 -0
  35. package/dist/packages/inspector/src/components/StyleEditor/SpacingSection.d.ts +13 -0
  36. package/dist/packages/inspector/src/components/StyleEditor/SpacingSection.d.ts.map +1 -0
  37. package/dist/packages/inspector/src/components/StyleEditor/TextSection.d.ts +13 -0
  38. package/dist/packages/inspector/src/components/StyleEditor/TextSection.d.ts.map +1 -0
  39. package/dist/packages/inspector/src/components/StyleEditor/index.d.ts +12 -0
  40. package/dist/packages/inspector/src/components/StyleEditor/index.d.ts.map +1 -0
  41. package/dist/packages/inspector/src/components/TextEditor/index.d.ts +11 -0
  42. package/dist/packages/inspector/src/components/TextEditor/index.d.ts.map +1 -0
  43. package/dist/packages/inspector/src/components/ui/CustomCollapsible.d.ts +26 -0
  44. package/dist/packages/inspector/src/components/ui/CustomCollapsible.d.ts.map +1 -0
  45. package/dist/packages/inspector/src/components/ui/button.d.ts +9 -0
  46. package/dist/packages/inspector/src/components/ui/button.d.ts.map +1 -0
  47. package/dist/packages/inspector/src/components/ui/color-picker.d.ts +10 -0
  48. package/dist/packages/inspector/src/components/ui/color-picker.d.ts.map +1 -0
  49. package/dist/packages/inspector/src/components/ui/input.d.ts +6 -0
  50. package/dist/packages/inspector/src/components/ui/input.d.ts.map +1 -0
  51. package/dist/packages/inspector/src/components/ui/popover.d.ts +8 -0
  52. package/dist/packages/inspector/src/components/ui/popover.d.ts.map +1 -0
  53. package/dist/packages/inspector/src/components/ui/select.d.ts +16 -0
  54. package/dist/packages/inspector/src/components/ui/select.d.ts.map +1 -0
  55. package/dist/packages/inspector/src/components/ui/slider.d.ts +5 -0
  56. package/dist/packages/inspector/src/components/ui/slider.d.ts.map +1 -0
  57. package/dist/packages/inspector/src/components/ui/textarea.d.ts +4 -0
  58. package/dist/packages/inspector/src/components/ui/textarea.d.ts.map +1 -0
  59. package/dist/packages/inspector/src/components/ui/tooltip.d.ts +8 -0
  60. package/dist/packages/inspector/src/components/ui/tooltip.d.ts.map +1 -0
  61. package/dist/packages/inspector/src/core/highlighter.d.ts +40 -0
  62. package/dist/packages/inspector/src/core/highlighter.d.ts.map +1 -0
  63. package/dist/packages/inspector/src/hooks/useMessageBridge.d.ts +9 -0
  64. package/dist/packages/inspector/src/hooks/useMessageBridge.d.ts.map +1 -0
  65. package/dist/packages/inspector/src/hooks/useStylePreview.d.ts +11 -0
  66. package/dist/packages/inspector/src/hooks/useStylePreview.d.ts.map +1 -0
  67. package/dist/packages/inspector/src/index.d.ts +16 -0
  68. package/dist/packages/inspector/src/index.d.ts.map +1 -0
  69. package/dist/packages/inspector/src/lib/utils.d.ts +3 -0
  70. package/dist/packages/inspector/src/lib/utils.d.ts.map +1 -0
  71. package/dist/packages/inspector/src/plugin.d.ts +4 -0
  72. package/dist/packages/inspector/src/plugin.d.ts.map +1 -0
  73. package/dist/packages/inspector/src/store/useInspectorStore.d.ts +13 -0
  74. package/dist/packages/inspector/src/store/useInspectorStore.d.ts.map +1 -0
  75. package/dist/packages/inspector/src/styles.d.ts +5 -0
  76. package/dist/packages/inspector/src/styles.d.ts.map +1 -0
  77. package/dist/packages/inspector/src/utils/colorUtils.d.ts +49 -0
  78. package/dist/packages/inspector/src/utils/colorUtils.d.ts.map +1 -0
  79. package/dist/packages/inspector/src/utils/elementNames.d.ts +7 -0
  80. package/dist/packages/inspector/src/utils/elementNames.d.ts.map +1 -0
  81. package/dist/packages/inspector/src/utils/elementUtils.d.ts +28 -0
  82. package/dist/packages/inspector/src/utils/elementUtils.d.ts.map +1 -0
  83. package/dist/packages/inspector/src/utils/errorTracker.d.ts +48 -0
  84. package/dist/packages/inspector/src/utils/errorTracker.d.ts.map +1 -0
  85. package/dist/packages/inspector/src/utils/inputStyles.d.ts +23 -0
  86. package/dist/packages/inspector/src/utils/inputStyles.d.ts.map +1 -0
  87. package/dist/packages/inspector/src/utils/styleUtils.d.ts +27 -0
  88. package/dist/packages/inspector/src/utils/styleUtils.d.ts.map +1 -0
  89. package/dist/packages/inspector/src/utils/tailwindMapper.d.ts +9 -0
  90. package/dist/packages/inspector/src/utils/tailwindMapper.d.ts.map +1 -0
  91. package/dist/packages/inspector/src/utils/urlTracker.d.ts +27 -0
  92. package/dist/packages/inspector/src/utils/urlTracker.d.ts.map +1 -0
  93. package/dist/packages/inspector/tsconfig.tsbuildinfo +1 -0
  94. package/dist/plugin.js +10 -1893
  95. package/package.json +86 -75
  96. package/src/App.tsx +912 -0
  97. package/src/__tests__/App.test.tsx +373 -0
  98. package/src/assets/fonts/Satoshi-Variable.woff +0 -0
  99. package/src/assets/fonts/Satoshi-Variable.woff2 +0 -0
  100. package/src/components/Badge.tsx +118 -0
  101. package/src/components/ControlBox/ContentArea.tsx +13 -0
  102. package/src/components/ControlBox/PromptInput.module.css +66 -0
  103. package/src/components/ControlBox/PromptInput.tsx +104 -0
  104. package/src/components/ControlBox/index.module.css +81 -0
  105. package/src/components/ControlBox/index.tsx +409 -0
  106. package/src/components/ImageEditor/UploadBox.module.css +69 -0
  107. package/src/components/ImageEditor/UploadBox.tsx +113 -0
  108. package/src/components/ImageEditor/index.module.css +11 -0
  109. package/src/components/ImageEditor/index.tsx +84 -0
  110. package/src/components/Overlay.tsx +157 -0
  111. package/src/components/StyleEditor/BorderSection.tsx +147 -0
  112. package/src/components/StyleEditor/ColorPicker.tsx +182 -0
  113. package/src/components/StyleEditor/DisplaySection.tsx +349 -0
  114. package/src/components/StyleEditor/ImageSection.tsx +105 -0
  115. package/src/components/StyleEditor/LayoutSection.tsx +63 -0
  116. package/src/components/StyleEditor/NumberInput.tsx +138 -0
  117. package/src/components/StyleEditor/SliderInput.tsx +121 -0
  118. package/src/components/StyleEditor/SpacingSection.tsx +365 -0
  119. package/src/components/StyleEditor/TextSection.tsx +381 -0
  120. package/src/components/StyleEditor/index.module.css +133 -0
  121. package/src/components/StyleEditor/index.tsx +612 -0
  122. package/src/components/StyleEditor/shared.module.css +193 -0
  123. package/src/components/TextEditor/index.module.css +31 -0
  124. package/src/components/TextEditor/index.tsx +166 -0
  125. package/src/components/ui/CustomCollapsible.tsx +159 -0
  126. package/src/components/ui/button.module.css +141 -0
  127. package/src/components/ui/button.tsx +73 -0
  128. package/src/components/ui/color-picker.module.css +112 -0
  129. package/src/components/ui/color-picker.tsx +146 -0
  130. package/src/components/ui/input.module.css +49 -0
  131. package/src/components/ui/input.tsx +34 -0
  132. package/src/components/ui/popover.module.css +42 -0
  133. package/src/components/ui/popover.tsx +59 -0
  134. package/src/components/ui/select.module.css +160 -0
  135. package/src/components/ui/select.tsx +216 -0
  136. package/src/components/ui/slider.module.css +75 -0
  137. package/src/components/ui/slider.tsx +60 -0
  138. package/src/components/ui/textarea.module.css +30 -0
  139. package/src/components/ui/textarea.tsx +23 -0
  140. package/src/components/ui/tooltip.module.css +11 -0
  141. package/src/components/ui/tooltip.tsx +37 -0
  142. package/src/core/highlighter.ts +197 -0
  143. package/src/hooks/useMessageBridge.ts +49 -0
  144. package/src/hooks/useStylePreview.ts +332 -0
  145. package/src/index.ts +20 -0
  146. package/src/lib/utils.ts +5 -0
  147. package/src/plugin.ts +11 -0
  148. package/src/store/useInspectorStore.ts +235 -0
  149. package/src/styles/fonts.css +15 -0
  150. package/src/styles/global.css +138 -0
  151. package/src/styles/variables.css +151 -0
  152. package/src/styles.ts +5 -0
  153. package/src/utils/colorUtils.ts +133 -0
  154. package/src/utils/elementNames.ts +103 -0
  155. package/src/utils/elementUtils.ts +90 -0
  156. package/src/utils/errorTracker.ts +186 -0
  157. package/src/utils/inputStyles.ts +30 -0
  158. package/src/utils/styleUtils.ts +226 -0
  159. package/src/utils/tailwindMapper.ts +554 -0
  160. package/src/utils/urlTracker.ts +75 -0
  161. package/src/vite-env.d.ts +7 -0
  162. package/README.md +0 -866
  163. package/dist/hook.d.ts +0 -115
  164. package/dist/hook.d.ts.map +0 -1
  165. package/dist/hook.js +0 -288
  166. package/dist/plugin.d.ts +0 -44
  167. package/dist/plugin.d.ts.map +0 -1
  168. package/dist/types.d.ts +0 -141
  169. package/dist/types.d.ts.map +0 -1
  170. package/dist/types.js +0 -7
@@ -0,0 +1,81 @@
1
+ .controlBox {
2
+ position: fixed;
3
+ display: flex;
4
+ flex-direction: column;
5
+ padding: var(--spacing-3);
6
+ gap: var(--spacing-2);
7
+ border-radius: var(--radius-lg);
8
+ box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05);
9
+ width: 100%;
10
+ max-width: 480px;
11
+ min-width: 320px;
12
+ box-sizing: border-box;
13
+ overflow: hidden;
14
+ backdrop-filter: blur(8px);
15
+ z-index: var(--z-inspector-controlbox);
16
+ }
17
+
18
+ .tabsContainer {
19
+ flex: 1;
20
+ display: flex;
21
+ flex-direction: column;
22
+ min-height: 0;
23
+ /* width: 100%; */
24
+ gap: var(--spacing-2);
25
+ overflow: hidden;
26
+ box-sizing: border-box;
27
+ }
28
+
29
+ .tabsList {
30
+ display: flex;
31
+ align-items: center;
32
+ height: var(--spacing-9);
33
+ /* width: 100%; */
34
+ border-radius: var(--radius-md);
35
+ flex-shrink: 0;
36
+ box-sizing: border-box;
37
+ border: 1px solid rgba(0, 0, 0, 0.05);
38
+ }
39
+
40
+ .tabTrigger {
41
+ display: inline-flex;
42
+ align-items: center;
43
+ justify-content: center;
44
+ gap: var(--spacing-1-5);
45
+ height: calc(100% - 2 * var(--spacing-1));
46
+ margin: var(--spacing-1);
47
+ border-radius: var(--radius-md);
48
+ border: 1px solid transparent;
49
+ padding: var(--spacing-1) var(--spacing-3);
50
+ font-family: 'Satoshi', var(--font-family-sans);
51
+ font-size: var(--text-sm);
52
+ font-weight: var(--font-medium);
53
+ white-space: nowrap;
54
+ cursor: pointer;
55
+ transition: all var(--transition-fast);
56
+ outline: none;
57
+ background: none;
58
+ box-sizing: border-box;
59
+ box-shadow: none;
60
+ }
61
+
62
+ .tabTrigger:hover {
63
+ opacity: 0.85;
64
+ }
65
+
66
+ /* Active tab */
67
+ .tabTrigger[data-active="true"] {
68
+ box-shadow: none;
69
+ }
70
+
71
+ .tabContent {
72
+ flex: 1;
73
+ display: flex;
74
+ flex-direction: column;
75
+ min-height: 0;
76
+ width: 100%;
77
+ max-width: 100%;
78
+ overflow-y: auto;
79
+ overflow-x: hidden;
80
+ box-sizing: border-box;
81
+ }
@@ -0,0 +1,409 @@
1
+ /**
2
+ * ControlBox - Main control interface for inspector
3
+ * Layout: TOP (tabs) | CONTENT (editors) | BOTTOM (prompt input)
4
+ */
5
+
6
+ import { useEffect, useRef, useState } from "react";
7
+ import { PromptInput } from "./PromptInput";
8
+ import { TextEditor } from "../TextEditor";
9
+ import { ImageEditor } from "../ImageEditor";
10
+ import { StyleEditor } from "../StyleEditor";
11
+ import { useInspectorStore } from "../../store/useInspectorStore";
12
+ import type { SelectedElementData } from "@promakeai/inspector-types";
13
+ import { getElementLabel } from "../../utils/elementNames";
14
+ import clsx from "clsx";
15
+ import styles from "./index.module.css";
16
+
17
+ interface ControlBoxProps {
18
+ className?: string;
19
+ element: HTMLElement;
20
+ elementData: SelectedElementData;
21
+ activeTab: "text" | "image" | "style" | null;
22
+ availableTabs: ("text" | "image" | "style")[];
23
+ onTabChange: (tab: "text" | "image" | "style" | null) => void;
24
+ onClose: () => void;
25
+ onPromptSubmit: (prompt: string) => void;
26
+ elementStack?: HTMLElement[];
27
+ onElementSelect?: (element: HTMLElement) => void;
28
+ labels?: Record<string, string>;
29
+ }
30
+
31
+ // Custom Tab Components
32
+ const TabButton = ({
33
+ active,
34
+ onClick,
35
+ children,
36
+ theme,
37
+ }: {
38
+ active: boolean;
39
+ onClick: () => void;
40
+ children: React.ReactNode;
41
+ theme: any;
42
+ }) => {
43
+ return (
44
+ <button
45
+ onClick={onClick}
46
+ className={styles.tabTrigger}
47
+ data-active={active}
48
+ style={{
49
+ backgroundColor: active ? theme.tabActiveBg : theme.tabInactiveBg,
50
+ color: active ? theme.tabActiveColor : theme.tabInactiveColor,
51
+ }}
52
+ >
53
+ {children}
54
+ </button>
55
+ );
56
+ };
57
+
58
+ export function ControlBox({
59
+ className,
60
+ element,
61
+ elementData,
62
+ activeTab,
63
+ availableTabs,
64
+ onTabChange,
65
+ onClose,
66
+ onPromptSubmit,
67
+ elementStack = [],
68
+ onElementSelect,
69
+ labels: customLabels,
70
+ }: ControlBoxProps) {
71
+ const { theme, labels } = useInspectorStore();
72
+ const boxRef = useRef<HTMLDivElement>(null);
73
+ const [position, setPosition] = useState<{
74
+ top?: number;
75
+ bottom?: number;
76
+ left: number;
77
+ }>({ left: 0 });
78
+ const [maxHeight, setMaxHeight] = useState<number>(0);
79
+ const [anchorPosition, setAnchorPosition] = useState<
80
+ "above" | "below" | "inside"
81
+ >("below");
82
+ const [isPositioned, setIsPositioned] = useState(false);
83
+
84
+ const TAB_LABELS = {
85
+ text: labels.textTabLabel || "Content",
86
+ image: labels.imageTabLabel || "Image",
87
+ style: labels.styleTabLabel || "Design",
88
+ };
89
+
90
+ // Reset positioned state and anchor when element changes
91
+ useEffect(() => {
92
+ setIsPositioned(false);
93
+ // Don't reset anchor here - let calculatePosition do it
94
+ }, [element]);
95
+
96
+ // Calculate position based on element
97
+ useEffect(() => {
98
+ if (!boxRef.current || !element) return;
99
+
100
+ let initialAnchor: "above" | "below" | "inside" | null = null;
101
+
102
+ const calculatePosition = (isInitial = false) => {
103
+ const rect = element.getBoundingClientRect();
104
+ const viewportWidth = window.innerWidth;
105
+ const viewportHeight = window.innerHeight;
106
+ const padding = 10;
107
+ const gap = 10;
108
+ const insidePadding = 16;
109
+ const minRequiredHeight = 250;
110
+ const largeElementThreshold = 0.7;
111
+
112
+ // Calculate box dimensions
113
+ const maxWidth = Math.min(600, viewportWidth - padding * 2);
114
+ const minWidth = Math.min(320, maxWidth);
115
+ const boxWidth = Math.max(minWidth, Math.min(rect.width, maxWidth));
116
+
117
+ // Horizontal position (always calculated)
118
+ let centerLeft = rect.left + rect.width / 2 - boxWidth / 2;
119
+ centerLeft = Math.max(
120
+ padding,
121
+ Math.min(centerLeft, viewportWidth - boxWidth - padding)
122
+ );
123
+
124
+ // Determine anchor position only on initial calculation
125
+ if (isInitial || initialAnchor === null) {
126
+ // Calculate actual visible space
127
+ const spaceBelow = Math.max(0, viewportHeight - rect.bottom);
128
+ const spaceAbove = Math.max(0, rect.top);
129
+
130
+ // Check element size ratios
131
+ const elementHeightRatio = rect.height / viewportHeight;
132
+ const elementWidthRatio = rect.width / viewportWidth;
133
+
134
+ // Element is large in both dimensions (e.g., full page, large section)
135
+ const isLargeInBothDimensions =
136
+ elementHeightRatio > largeElementThreshold &&
137
+ elementWidthRatio > largeElementThreshold;
138
+
139
+ // Element is wide but short (e.g., navbar, header) - should use outside positioning
140
+ const isWideButShort =
141
+ elementWidthRatio > largeElementThreshold &&
142
+ elementHeightRatio <= largeElementThreshold;
143
+
144
+ // Check if element extends beyond viewport (partially visible)
145
+ const isPartiallyVisible = rect.top < 0 || rect.bottom > viewportHeight;
146
+
147
+ // Check if there's insufficient space outside
148
+ const hasInsufficientSpace =
149
+ spaceAbove < minRequiredHeight && spaceBelow < minRequiredHeight;
150
+
151
+ // Check if there's sufficient space outside (prefer outside for wide but short elements)
152
+ const hasSufficientSpaceOutside =
153
+ spaceAbove >= minRequiredHeight || spaceBelow >= minRequiredHeight;
154
+
155
+ // Decision logic:
156
+ // 1. Wide but short elements with space outside -> use outside positioning
157
+ // 2. Large in both dimensions -> use inside positioning
158
+ // 3. Tall elements OR partially visible large elements -> use inside if insufficient space
159
+ if (isWideButShort && hasSufficientSpaceOutside) {
160
+ // Wide but short (navbar/header): use outside positioning
161
+ initialAnchor = spaceAbove > spaceBelow ? "above" : "below";
162
+ } else if (
163
+ isLargeInBothDimensions ||
164
+ hasInsufficientSpace ||
165
+ (isPartiallyVisible && elementHeightRatio > 0.5)
166
+ ) {
167
+ // Large element or insufficient space: use inside positioning
168
+ initialAnchor = "inside";
169
+ } else {
170
+ // Normal elements: choose based on available space
171
+ initialAnchor = spaceAbove > spaceBelow ? "above" : "below";
172
+ }
173
+ setAnchorPosition(initialAnchor);
174
+ }
175
+
176
+ // Calculate dynamic max-height based on anchor
177
+ let calculatedMaxHeight: number;
178
+ let newPosition: { top?: number; bottom?: number; left: number };
179
+
180
+ if (initialAnchor === "inside") {
181
+ // Position inside the element, but respect viewport boundaries
182
+ // If element is scrolled partially off screen, start from viewport edge
183
+ const topPos = Math.max(rect.top + insidePadding, insidePadding);
184
+
185
+ // Calculate available height: from topPos to element bottom or viewport bottom
186
+ const availableBottom = Math.min(
187
+ rect.bottom - insidePadding,
188
+ viewportHeight - insidePadding
189
+ );
190
+ calculatedMaxHeight = availableBottom - topPos;
191
+
192
+ // Horizontal positioning
193
+ let insideLeft = rect.left + insidePadding;
194
+
195
+ // If element is scrolled partially off screen horizontally, start from viewport edge
196
+ if (rect.left < 0) {
197
+ insideLeft = insidePadding;
198
+ }
199
+
200
+ // Ensure the box fits within element width and viewport
201
+ const elementRightEdge = Math.min(rect.right, viewportWidth);
202
+ const maxBoxWidth = elementRightEdge - insideLeft - insidePadding;
203
+ const actualBoxWidth = Math.min(boxWidth, maxBoxWidth);
204
+
205
+ // Center horizontally within element if there's room
206
+ const elementVisibleWidth =
207
+ Math.min(rect.right, viewportWidth) - Math.max(rect.left, 0);
208
+ if (actualBoxWidth < elementVisibleWidth - insidePadding * 2) {
209
+ const elementVisibleLeft = Math.max(rect.left, 0);
210
+ insideLeft =
211
+ elementVisibleLeft + (elementVisibleWidth - actualBoxWidth) / 2;
212
+ }
213
+
214
+ newPosition = { top: topPos, left: insideLeft };
215
+
216
+ // Update box width for inside positioning
217
+ if (boxRef.current && actualBoxWidth !== boxWidth) {
218
+ boxRef.current.style.width = `${actualBoxWidth}px`;
219
+ }
220
+ } else if (initialAnchor === "above") {
221
+ // Anchored above: grow upward from element
222
+ calculatedMaxHeight = rect.top - padding - gap;
223
+ const bottomPos = viewportHeight - rect.top + gap;
224
+ newPosition = { bottom: bottomPos, left: centerLeft };
225
+ } else {
226
+ // Anchored below: grow downward from element
227
+ calculatedMaxHeight = viewportHeight - rect.bottom - padding - gap;
228
+ const topPos = rect.bottom + gap;
229
+ newPosition = { top: topPos, left: centerLeft };
230
+ }
231
+
232
+ // Ensure minimum height
233
+ calculatedMaxHeight = Math.max(200, calculatedMaxHeight);
234
+
235
+ setMaxHeight(calculatedMaxHeight);
236
+ setPosition(newPosition);
237
+
238
+ // Update box width (for non-inside positioning)
239
+ if (boxRef.current && initialAnchor !== "inside") {
240
+ boxRef.current.style.width = `${boxWidth}px`;
241
+ }
242
+
243
+ // Mark as positioned after first calculation
244
+ if (isInitial) {
245
+ setIsPositioned(true);
246
+ }
247
+ };
248
+
249
+ // Initial calculation
250
+ calculatePosition(true);
251
+
252
+ // Update position on scroll/resize (but keep anchor)
253
+ const handleUpdate = () => calculatePosition(false);
254
+ window.addEventListener("scroll", handleUpdate, true);
255
+ window.addEventListener("resize", handleUpdate);
256
+
257
+ // Watch for content height changes (e.g., tab switches)
258
+ const resizeObserver = new ResizeObserver(() => {
259
+ // Only update if already positioned (avoid flash on initial render)
260
+ if (initialAnchor !== null) {
261
+ calculatePosition(false);
262
+ }
263
+ });
264
+ resizeObserver.observe(boxRef.current);
265
+
266
+ return () => {
267
+ window.removeEventListener("scroll", handleUpdate, true);
268
+ window.removeEventListener("resize", handleUpdate);
269
+ resizeObserver.disconnect();
270
+ initialAnchor = null;
271
+ };
272
+ }, [element]);
273
+
274
+ return (
275
+ <div
276
+ ref={boxRef}
277
+ className={clsx(styles.controlBox, className)}
278
+ style={{
279
+ top: position.top !== undefined ? `${position.top}px` : undefined,
280
+ bottom:
281
+ position.bottom !== undefined ? `${position.bottom}px` : undefined,
282
+ left: `${position.left}px`,
283
+ maxHeight: maxHeight > 0 ? `${maxHeight}px` : undefined,
284
+ backgroundColor: theme.backgroundColor,
285
+ opacity: isPositioned ? 1 : 0,
286
+ transform: isPositioned ? "scale(1)" : "scale(0.95)",
287
+ transition: "opacity 0.2s ease, transform 0.2s ease",
288
+ }}
289
+ >
290
+ {/* Element name tags - at top of control box */}
291
+ {(() => {
292
+ const shouldShow = elementStack.length > 1;
293
+ console.log("🔘 Element Tags Debug:", {
294
+ shouldShow,
295
+ stackLength: elementStack.length,
296
+ stack: elementStack.map((el) => ({
297
+ tag: el.tagName,
298
+ id: el.id,
299
+ className: el.className,
300
+ devId: el.getAttribute("data-dev-id"),
301
+ })),
302
+ isPositioned,
303
+ });
304
+
305
+ return (
306
+ shouldShow && (
307
+ <div
308
+ className="inspector-element-tags"
309
+ data-inspector-ignore
310
+ style={{
311
+ display: "flex",
312
+ gap: "4px",
313
+ overflowX: "auto",
314
+ overflowY: "hidden",
315
+ width: "calc(100% + 24px)",
316
+ marginLeft: "-12px",
317
+ marginRight: "-12px",
318
+ marginTop: "-12px",
319
+ paddingLeft: "12px",
320
+ paddingRight: "12px",
321
+ paddingTop: "12px",
322
+ paddingBottom: "10px",
323
+ borderBottom: "1px solid rgba(0, 0, 0, 0.05)",
324
+ }}
325
+ >
326
+ {elementStack.map((el, index) => {
327
+ const labelText = getElementLabel(el, customLabels || labels);
328
+ const isSelected = el === element; // Check if this element is the currently selected one
329
+ console.log(`🏷️ Button ${index}:`, labelText, el, {
330
+ isSelected,
331
+ });
332
+ return (
333
+ <button
334
+ key={index}
335
+ className="inspector-element-tag"
336
+ onClick={() => onElementSelect?.(el)}
337
+ style={{
338
+ background: isSelected ? "rgb(58, 18, 189)" : "#ffffff",
339
+ color: isSelected ? "#ffffff" : "#475569",
340
+ border: isSelected ? "none" : "1px solid #e2e8f0",
341
+ padding: "4px 8px",
342
+ borderRadius: "4px",
343
+ fontSize: "11px",
344
+ fontWeight: "500",
345
+ cursor: "pointer",
346
+ whiteSpace: "nowrap",
347
+ flexShrink: 0,
348
+ height: "24px",
349
+ display: "flex",
350
+ alignItems: "center",
351
+ boxShadow: isSelected
352
+ ? "0 1px 3px rgba(58, 18, 189, 0.2)"
353
+ : "0 1px 2px rgba(0, 0, 0, 0.05)",
354
+ }}
355
+ >
356
+ {String(labelText)}
357
+ </button>
358
+ );
359
+ })}
360
+ </div>
361
+ )
362
+ );
363
+ })()}
364
+
365
+ <PromptInput
366
+ placeholder={labels.promptPlaceholder || "Ask AI..."}
367
+ onSubmit={onPromptSubmit}
368
+ onClose={onClose}
369
+ autoFocus={!activeTab}
370
+ />
371
+ {/* Custom Tabs */}
372
+ {availableTabs.length > 0 && activeTab ? (
373
+ <div className={styles.tabsContainer}>
374
+ {/* Tab Buttons */}
375
+ <div
376
+ className={styles.tabsList}
377
+ style={{
378
+ backgroundColor: theme.tabContainerBg,
379
+ }}
380
+ >
381
+ {availableTabs.map((tab) => (
382
+ <TabButton
383
+ key={tab}
384
+ active={activeTab === tab}
385
+ onClick={() => onTabChange(tab)}
386
+ theme={theme}
387
+ >
388
+ {TAB_LABELS[tab]}
389
+ </TabButton>
390
+ ))}
391
+ </div>
392
+
393
+ {/* Tab Content */}
394
+ <div className={styles.tabContent}>
395
+ {activeTab === "text" && (
396
+ <TextEditor element={element} elementData={elementData} />
397
+ )}
398
+ {activeTab === "image" && (
399
+ <ImageEditor element={element} elementData={elementData} />
400
+ )}
401
+ {activeTab === "style" && (
402
+ <StyleEditor element={element} elementData={elementData} />
403
+ )}
404
+ </div>
405
+ </div>
406
+ ) : null}
407
+ </div>
408
+ );
409
+ }
@@ -0,0 +1,69 @@
1
+ .hiddenInput {
2
+ position: absolute;
3
+ width: 1px;
4
+ height: 1px;
5
+ padding: 0;
6
+ margin: -1px;
7
+ overflow: hidden;
8
+ clip: rect(0, 0, 0, 0);
9
+ white-space: nowrap;
10
+ border-width: 0;
11
+ opacity: 0;
12
+ pointer-events: none;
13
+ }
14
+
15
+ .uploadBox {
16
+ min-height: 100px;
17
+ border: 2px dashed;
18
+ border-radius: var(--radius-lg);
19
+ cursor: pointer;
20
+ transition: all var(--transition-fast);
21
+ display: flex;
22
+ align-items: center;
23
+ justify-content: center;
24
+ flex-direction: column;
25
+ gap: var(--spacing-2);
26
+ padding: var(--spacing-4);
27
+ position: relative;
28
+ overflow: hidden;
29
+ }
30
+
31
+ .uploadBox:hover {
32
+ opacity: 0.9;
33
+ }
34
+
35
+ .uploadContent {
36
+ text-align: center;
37
+ pointer-events: none;
38
+ display: flex;
39
+ flex-direction: column;
40
+ align-items: center;
41
+ gap: var(--spacing-2);
42
+ }
43
+
44
+ .uploadIcon {
45
+ width: 32px;
46
+ height: 32px;
47
+ margin-bottom: var(--spacing-2);
48
+ opacity: 0.5;
49
+ }
50
+
51
+ .uploadTitle {
52
+ font-family: "Satoshi", var(--font-family-sans);
53
+ font-size: var(--text-sm);
54
+ font-weight: var(--font-medium);
55
+ }
56
+
57
+ .uploadHint {
58
+ font-family: "Satoshi", var(--font-family-sans);
59
+ font-size: var(--text-xs);
60
+ margin-top: var(--spacing-0_5);
61
+ }
62
+
63
+ .previewImage {
64
+ width: 100%;
65
+ height: auto;
66
+ max-height: 100px;
67
+ object-fit: contain;
68
+ border-radius: var(--radius-lg);
69
+ }
@@ -0,0 +1,113 @@
1
+ /**
2
+ * UploadBox component - Drag & drop image upload
3
+ */
4
+
5
+ import { useRef, useState } from "react";
6
+ import { useInspectorStore } from "../../store/useInspectorStore";
7
+ import styles from "./UploadBox.module.css";
8
+
9
+ interface UploadBoxProps {
10
+ onFileSelect: (file: File) => void;
11
+ previewImage: string | null;
12
+ }
13
+
14
+ export function UploadBox({ onFileSelect, previewImage }: UploadBoxProps) {
15
+ const { theme, labels } = useInspectorStore();
16
+ const inputRef = useRef<HTMLInputElement>(null);
17
+ const [isDragging, setIsDragging] = useState(false);
18
+
19
+ const handleClick = () => {
20
+ inputRef.current?.click();
21
+ };
22
+
23
+ const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
24
+ const file = e.target.files?.[0];
25
+ if (file && file.type.startsWith("image/")) {
26
+ onFileSelect(file);
27
+ }
28
+ };
29
+
30
+ const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
31
+ e.preventDefault();
32
+ setIsDragging(true);
33
+ };
34
+
35
+ const handleDragLeave = (e: React.DragEvent<HTMLDivElement>) => {
36
+ e.preventDefault();
37
+ setIsDragging(false);
38
+ };
39
+
40
+ const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
41
+ e.preventDefault();
42
+ setIsDragging(false);
43
+
44
+ const file = e.dataTransfer?.files[0];
45
+ if (file && file.type.startsWith("image/")) {
46
+ onFileSelect(file);
47
+ }
48
+ };
49
+
50
+ return (
51
+ <>
52
+ <input
53
+ ref={inputRef}
54
+ type="file"
55
+ accept="image/*"
56
+ onChange={handleFileChange}
57
+ className={styles.hiddenInput}
58
+ />
59
+ <div
60
+ onClick={handleClick}
61
+ onDragOver={handleDragOver}
62
+ onDragLeave={handleDragLeave}
63
+ onDrop={handleDrop}
64
+ className={styles.uploadBox}
65
+ style={{
66
+ borderColor: isDragging ? theme.buttonColor : theme.inputBorderColor,
67
+ backgroundColor: isDragging
68
+ ? `${theme.buttonColor}10`
69
+ : theme.inputBackgroundColor,
70
+ }}
71
+ >
72
+ {!previewImage ? (
73
+ <div className={styles.uploadContent}>
74
+ <svg
75
+ width="32"
76
+ height="32"
77
+ viewBox="0 0 24 24"
78
+ fill="none"
79
+ className={styles.uploadIcon}
80
+ style={{ color: theme.secondaryTextColor }}
81
+ >
82
+ <path
83
+ d="M21 15V19C21 19.5304 20.7893 20.0391 20.4142 20.4142C20.0391 20.7893 19.5304 21 19 21H5C4.46957 21 3.96086 20.7893 3.58579 20.4142C3.21071 20.0391 3 19.5304 3 19V15M17 8L12 3M12 3L7 8M12 3V15"
84
+ stroke="currentColor"
85
+ strokeWidth="2"
86
+ strokeLinecap="round"
87
+ strokeLinejoin="round"
88
+ />
89
+ </svg>
90
+ <div
91
+ className={styles.uploadTitle}
92
+ style={{ color: theme.textColor }}
93
+ >
94
+ {labels.imageUploadTitle || "Click or drag image"}
95
+ </div>
96
+ <div
97
+ className={styles.uploadHint}
98
+ style={{ color: theme.secondaryTextColor }}
99
+ >
100
+ {labels.imageUploadHint || "Supports: JPG, PNG, GIF"}
101
+ </div>
102
+ </div>
103
+ ) : (
104
+ <img
105
+ src={previewImage}
106
+ alt="Preview"
107
+ className={styles.previewImage}
108
+ />
109
+ )}
110
+ </div>
111
+ </>
112
+ );
113
+ }
@@ -0,0 +1,11 @@
1
+ .container {
2
+ display: flex;
3
+ flex-direction: column;
4
+ gap: var(--spacing-2);
5
+ flex: 1;
6
+ box-sizing: border-box;
7
+ }
8
+
9
+ .button {
10
+ width: 100%;
11
+ }