@promakeai/inspector 0.2.1 → 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 -1813
  95. package/package.json +86 -76
  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 -139
  169. package/dist/types.d.ts.map +0 -1
  170. package/dist/types.js +0 -7
@@ -0,0 +1,84 @@
1
+ /**
2
+ * ImageEditor component - Edit/upload images
3
+ */
4
+
5
+ import { useState } from "react";
6
+ import { useMessageBridge } from "../../hooks/useMessageBridge";
7
+ import { UploadBox } from "./UploadBox";
8
+ import { Button } from "../ui/button";
9
+ import type { SelectedElementData } from "@promakeai/inspector-types";
10
+ import { useInspectorStore } from "../../store/useInspectorStore";
11
+ import styles from "./index.module.css";
12
+
13
+ interface ImageEditorProps {
14
+ element: HTMLElement;
15
+ elementData: SelectedElementData;
16
+ }
17
+
18
+ export function ImageEditor({ element, elementData }: ImageEditorProps) {
19
+ const [selectedFile, setSelectedFile] = useState<File | null>(null);
20
+ const [imageData, setImageData] = useState<string | null>(null);
21
+ const { sendToParent } = useMessageBridge();
22
+ const { labels, theme } = useInspectorStore();
23
+
24
+ const handleFileSelect = (file: File) => {
25
+ setSelectedFile(file);
26
+
27
+ // Read file as base64
28
+ const reader = new FileReader();
29
+ reader.onload = (evt) => {
30
+ const data = evt.target?.result as string;
31
+ setImageData(data);
32
+ };
33
+ reader.readAsDataURL(file);
34
+ };
35
+
36
+ const handleUpdate = () => {
37
+ if (!selectedFile || !imageData) return;
38
+
39
+ // Update DOM immediately
40
+ if (element.tagName.toLowerCase() === "img") {
41
+ (element as HTMLImageElement).src = imageData;
42
+ } else if (window.getComputedStyle(element).backgroundImage !== "none") {
43
+ element.style.backgroundImage = `url("${imageData}")`;
44
+ } else {
45
+ // Try to find img child
46
+ const imgChild = element.querySelector("img");
47
+ if (imgChild) {
48
+ imgChild.src = imageData;
49
+ }
50
+ }
51
+
52
+ // Send to parent
53
+ sendToParent("INSPECTOR_IMAGE_UPDATED", {
54
+ imageData,
55
+ imageFile: {
56
+ name: selectedFile.name,
57
+ size: selectedFile.size,
58
+ type: selectedFile.type,
59
+ },
60
+ originalImageUrl: elementData.imageUrl,
61
+ element: elementData,
62
+ });
63
+ };
64
+
65
+ return (
66
+ <div className={styles.container}>
67
+ <UploadBox onFileSelect={handleFileSelect} previewImage={imageData} />
68
+ <div>
69
+ <Button
70
+ onClick={handleUpdate}
71
+ disabled={!selectedFile}
72
+ className={styles.button}
73
+ style={{
74
+ backgroundColor: theme.buttonColor,
75
+ color: theme.buttonTextColor,
76
+ opacity: selectedFile ? 1 : 0.5,
77
+ }}
78
+ >
79
+ {labels.updateImage}
80
+ </Button>
81
+ </div>
82
+ </div>
83
+ );
84
+ }
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Overlay component for highlighting elements
3
+ */
4
+
5
+ import { useEffect, useState } from "react";
6
+
7
+ interface OverlayProps {
8
+ element: HTMLElement | null;
9
+ selectedElement: HTMLElement | null;
10
+ showChildBorders?: boolean;
11
+ }
12
+
13
+ export function Overlay({
14
+ element,
15
+ selectedElement,
16
+ showChildBorders = true,
17
+ }: OverlayProps) {
18
+ const [rect, setRect] = useState<DOMRect | null>(null);
19
+ const [childRects, setChildRects] = useState<DOMRect[]>([]);
20
+ const [isPaused, setIsPaused] = useState(false);
21
+
22
+ const activeElement = selectedElement || element;
23
+
24
+ useEffect(() => {
25
+ if (!activeElement) {
26
+ setRect(null);
27
+ return;
28
+ }
29
+
30
+ setIsPaused(!!selectedElement);
31
+
32
+ // Throttle updates to avoid excessive re-renders
33
+ let updateTimeout: number | null = null;
34
+ let lastUpdate = 0;
35
+ const throttleMs = 16; // ~60fps
36
+
37
+ const updateRect = () => {
38
+ const now = Date.now();
39
+ const timeSinceLastUpdate = now - lastUpdate;
40
+
41
+ if (timeSinceLastUpdate >= throttleMs) {
42
+ const newRect = activeElement.getBoundingClientRect();
43
+ setRect(newRect);
44
+
45
+ // Collect direct children rectangles
46
+ const children = Array.from(activeElement.children) as HTMLElement[];
47
+ const newChildRects = children
48
+ .filter(child => !child.closest("[data-inspector-ignore]"))
49
+ .map(child => child.getBoundingClientRect());
50
+ setChildRects(newChildRects);
51
+
52
+ lastUpdate = now;
53
+ } else if (!updateTimeout) {
54
+ // Schedule update for later
55
+ updateTimeout = window.setTimeout(() => {
56
+ const newRect = activeElement.getBoundingClientRect();
57
+ setRect(newRect);
58
+
59
+ // Collect direct children rectangles
60
+ const children = Array.from(activeElement.children) as HTMLElement[];
61
+ const newChildRects = children
62
+ .filter(child => !child.closest("[data-inspector-ignore]"))
63
+ .map(child => child.getBoundingClientRect());
64
+ setChildRects(newChildRects);
65
+
66
+ lastUpdate = Date.now();
67
+ updateTimeout = null;
68
+ }, throttleMs - timeSinceLastUpdate);
69
+ }
70
+ };
71
+
72
+ updateRect();
73
+
74
+ // Update on scroll and resize
75
+ const handleUpdate = () => {
76
+ updateRect();
77
+ };
78
+
79
+ window.addEventListener("scroll", handleUpdate, true);
80
+ window.addEventListener("resize", handleUpdate);
81
+
82
+ // Use ResizeObserver to track element size changes (for style edits)
83
+ let resizeObserver: ResizeObserver | null = null;
84
+
85
+ if (typeof ResizeObserver !== "undefined") {
86
+ resizeObserver = new ResizeObserver(() => {
87
+ updateRect();
88
+ });
89
+ resizeObserver.observe(activeElement);
90
+ }
91
+
92
+ // Use MutationObserver to track attribute and style changes
93
+ let mutationObserver: MutationObserver | null = null;
94
+
95
+ if (typeof MutationObserver !== "undefined") {
96
+ mutationObserver = new MutationObserver(() => {
97
+ updateRect();
98
+ });
99
+ mutationObserver.observe(activeElement, {
100
+ attributes: true,
101
+ attributeFilter: ["style", "class"],
102
+ childList: false,
103
+ subtree: false,
104
+ });
105
+ }
106
+
107
+ return () => {
108
+ window.removeEventListener("scroll", handleUpdate, true);
109
+ window.removeEventListener("resize", handleUpdate);
110
+
111
+ if (resizeObserver) {
112
+ resizeObserver.disconnect();
113
+ }
114
+
115
+ if (mutationObserver) {
116
+ mutationObserver.disconnect();
117
+ }
118
+
119
+ if (updateTimeout) {
120
+ clearTimeout(updateTimeout);
121
+ }
122
+ };
123
+ }, [activeElement, selectedElement]);
124
+
125
+ if (!rect) return null;
126
+
127
+ return (
128
+ <>
129
+ {/* Main element overlay */}
130
+ <div
131
+ className={`inspector-overlay ${isPaused ? "paused" : ""} active`}
132
+ style={{
133
+ top: `${rect.top}px`,
134
+ left: `${rect.left}px`,
135
+ width: `${rect.width}px`,
136
+ height: `${rect.height}px`,
137
+ }}
138
+ />
139
+
140
+
141
+ {/* Child overlays */}
142
+ {showChildBorders &&
143
+ childRects.map((childRect, index) => (
144
+ <div
145
+ key={index}
146
+ className="inspector-overlay-child active"
147
+ style={{
148
+ top: `${childRect.top}px`,
149
+ left: `${childRect.left}px`,
150
+ width: `${childRect.width}px`,
151
+ height: `${childRect.height}px`,
152
+ }}
153
+ />
154
+ ))}
155
+ </>
156
+ );
157
+ }
@@ -0,0 +1,147 @@
1
+ /**
2
+ * BorderSection - Border Radius, Border Width, Border Color
3
+ */
4
+
5
+ import type { StyleChanges } from "@promakeai/inspector-types";
6
+ import { ColorPicker } from "./ColorPicker";
7
+ import { NumberInput } from "./NumberInput";
8
+ import {
9
+ Select,
10
+ SelectContent,
11
+ SelectItem,
12
+ SelectTrigger,
13
+ SelectValue,
14
+ } from "../ui/select";
15
+ import { RotateCcw } from "lucide-react";
16
+ import { useInspectorStore } from "../../store/useInspectorStore";
17
+ import styles from "./shared.module.css";
18
+
19
+ interface BorderSectionProps {
20
+ styles: StyleChanges;
21
+ onChange: (property: keyof StyleChanges, value: string) => void;
22
+ originalStyles: StyleChanges;
23
+ onResetProperty: (property: keyof StyleChanges) => void;
24
+ }
25
+
26
+ export function BorderSection({
27
+ styles: styleValues,
28
+ onChange,
29
+ originalStyles,
30
+ onResetProperty
31
+ }: BorderSectionProps) {
32
+ const { labels, theme } = useInspectorStore();
33
+
34
+ const BORDER_STYLES = [
35
+ { label: labels.borderStyleSolid || "Solid", value: "solid" },
36
+ { label: labels.borderStyleDashed || "Dashed", value: "dashed" },
37
+ { label: labels.borderStyleDotted || "Dotted", value: "dotted" },
38
+ { label: labels.borderStyleDouble || "Double", value: "double" },
39
+ { label: labels.borderStyleGroove || "Groove", value: "groove" },
40
+ { label: labels.borderStyleRidge || "Ridge", value: "ridge" },
41
+ { label: labels.borderStyleInset || "Inset", value: "inset" },
42
+ { label: labels.borderStyleOutset || "Outset", value: "outset" },
43
+ { label: labels.borderStyleNone || "None", value: "none" },
44
+ ];
45
+
46
+ return (
47
+ <div className={styles.section}>
48
+ <div className={styles.grid2}>
49
+ <NumberInput
50
+ label={labels.borderRadiusLabel || "Border Radius"}
51
+ value={styleValues.borderRadius}
52
+ onChange={(value) => onChange("borderRadius", value)}
53
+ min={0}
54
+ max={400}
55
+ step={1}
56
+ unit="px"
57
+ originalValue={originalStyles.borderRadius}
58
+ onReset={() => onResetProperty("borderRadius")}
59
+ />
60
+
61
+ <NumberInput
62
+ label={labels.borderWidthLabel || "Border Width"}
63
+ value={styleValues.borderWidth}
64
+ onChange={(value) => onChange("borderWidth", value)}
65
+ min={0}
66
+ max={100}
67
+ step={1}
68
+ unit="px"
69
+ originalValue={originalStyles.borderWidth}
70
+ onReset={() => onResetProperty("borderWidth")}
71
+ />
72
+ </div>
73
+
74
+ {/* Border Color and Border Style - Side by side */}
75
+ <div className={styles.grid2}>
76
+ <ColorPicker
77
+ label={labels.borderColorLabel || "Border Color"}
78
+ value={styleValues.borderColor || "#000000"}
79
+ onChange={(value) => onChange("borderColor", value)}
80
+ originalValue={originalStyles.borderColor}
81
+ onReset={() => onResetProperty("borderColor")}
82
+ />
83
+
84
+ {/* Border Style */}
85
+ <div>
86
+ <div className={styles.labelContainer}>
87
+ <label className={styles.label} style={{ color: theme.textColor }}>
88
+ {labels.borderStyleLabel || "Border Style"}
89
+ </label>
90
+ {styleValues.borderStyle !== originalStyles.borderStyle && (
91
+ <button
92
+ type="button"
93
+ onClick={() => onResetProperty("borderStyle")}
94
+ className={styles.resetIcon}
95
+ style={{ color: theme.secondaryTextColor }}
96
+ title="Reset to original"
97
+ >
98
+ <RotateCcw size={12} />
99
+ </button>
100
+ )}
101
+ </div>
102
+ <div
103
+ className={styles.inputContainer}
104
+ style={{
105
+ backgroundColor: theme.inputBackgroundColor,
106
+ borderColor: theme.inputBorderColor,
107
+ }}
108
+ >
109
+ <Select
110
+ value={styleValues.borderStyle || "solid"}
111
+ onValueChange={(value) => onChange("borderStyle", value)}
112
+ >
113
+ <SelectTrigger
114
+ className={styles.input}
115
+ style={{
116
+ backgroundColor: "transparent",
117
+ color: theme.inputTextColor,
118
+ }}
119
+ size="sm"
120
+ >
121
+ <SelectValue />
122
+ </SelectTrigger>
123
+ <SelectContent
124
+ style={{
125
+ backgroundColor: theme.backgroundColor,
126
+ borderColor: theme.borderColor,
127
+ }}
128
+ >
129
+ {BORDER_STYLES.map((borderStyle) => (
130
+ <SelectItem
131
+ key={borderStyle.value}
132
+ value={borderStyle.value}
133
+ style={{
134
+ color: theme.textColor,
135
+ }}
136
+ >
137
+ {borderStyle.label}
138
+ </SelectItem>
139
+ ))}
140
+ </SelectContent>
141
+ </Select>
142
+ </div>
143
+ </div>
144
+ </div>
145
+ </div>
146
+ );
147
+ }
@@ -0,0 +1,182 @@
1
+ /**
2
+ * ColorPicker - Simple color picker with react-colorful
3
+ */
4
+
5
+ import { useMemo, useState } from "react";
6
+ import { Input } from "../ui/input";
7
+ import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
8
+ import { RgbaColorPicker } from "react-colorful";
9
+ import { hexToRgba } from "../../utils/colorUtils";
10
+ import { useInspectorStore } from "../../store/useInspectorStore";
11
+ import { RotateCcw } from "lucide-react";
12
+ import styles from "./shared.module.css";
13
+
14
+ interface ColorPickerProps {
15
+ label: string;
16
+ value: string;
17
+ onChange: (value: string) => void;
18
+ originalValue?: string;
19
+ onReset?: () => void;
20
+ }
21
+
22
+ export function ColorPicker({ label, value, onChange, originalValue, onReset }: ColorPickerProps) {
23
+ const { theme } = useInspectorStore();
24
+ const hasChanged = originalValue !== undefined && value !== originalValue;
25
+ const [hexInput, setHexInput] = useState("");
26
+ const [isEditingHex, setIsEditingHex] = useState(false);
27
+
28
+ // Convert current value to RGBA for react-colorful
29
+ const rgbaValue = useMemo(() => {
30
+ if (value === "transparent") return { r: 255, g: 255, b: 255, a: 0 };
31
+ if (value.startsWith("#")) {
32
+ const rgba = hexToRgba(value);
33
+ return rgba || { r: 0, g: 0, b: 0, a: 1 };
34
+ }
35
+ if (value.startsWith("rgb")) {
36
+ const match = value.match(
37
+ /rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/
38
+ );
39
+ if (match) {
40
+ return {
41
+ r: parseInt(match[1]),
42
+ g: parseInt(match[2]),
43
+ b: parseInt(match[3]),
44
+ a: match[4] ? parseFloat(match[4]) : 1,
45
+ };
46
+ }
47
+ }
48
+ return { r: 0, g: 0, b: 0, a: 1 };
49
+ }, [value]);
50
+
51
+ // Convert to hex for display
52
+ const displayValue = useMemo(() => {
53
+ if (value.startsWith("#")) return value;
54
+ if (value.startsWith("rgb")) {
55
+ const match = value.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
56
+ if (match) {
57
+ const r = parseInt(match[1]);
58
+ const g = parseInt(match[2]);
59
+ const b = parseInt(match[3]);
60
+ return `#${((1 << 24) + (r << 16) + (g << 8) + b)
61
+ .toString(16)
62
+ .slice(1)}`;
63
+ }
64
+ }
65
+ return value || "#000000";
66
+ }, [value]);
67
+
68
+ const handleRgbaChange = (newRgba: {
69
+ r: number;
70
+ g: number;
71
+ b: number;
72
+ a: number;
73
+ }) => {
74
+ if (newRgba.a < 1) {
75
+ onChange(`rgba(${newRgba.r}, ${newRgba.g}, ${newRgba.b}, ${newRgba.a})`);
76
+ } else {
77
+ const hex = `#${(
78
+ (1 << 24) +
79
+ (newRgba.r << 16) +
80
+ (newRgba.g << 8) +
81
+ newRgba.b
82
+ )
83
+ .toString(16)
84
+ .slice(1)}`;
85
+ onChange(hex);
86
+ }
87
+ };
88
+
89
+ const handleHexInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
90
+ const input = e.target.value;
91
+ setHexInput(input);
92
+
93
+ // Validate and update if it's a valid hex color
94
+ if (/^#[0-9A-Fa-f]{6}$/.test(input)) {
95
+ onChange(input);
96
+ }
97
+ };
98
+
99
+ const handleHexInputFocus = () => {
100
+ setIsEditingHex(true);
101
+ setHexInput(displayValue);
102
+ };
103
+
104
+ const handleHexInputBlur = () => {
105
+ setIsEditingHex(false);
106
+ // Ensure valid hex on blur
107
+ if (hexInput && /^#[0-9A-Fa-f]{6}$/.test(hexInput)) {
108
+ onChange(hexInput);
109
+ }
110
+ };
111
+
112
+ return (
113
+ <div className={styles.inputField}>
114
+ <div className={styles.labelContainer}>
115
+ <label className={styles.label} style={{ color: theme.textColor }}>
116
+ {label}
117
+ </label>
118
+ {hasChanged && onReset && (
119
+ <button
120
+ type="button"
121
+ onClick={onReset}
122
+ className={styles.resetIcon}
123
+ style={{ color: theme.secondaryTextColor }}
124
+ title="Reset to original"
125
+ >
126
+ <RotateCcw size={12} />
127
+ </button>
128
+ )}
129
+ </div>
130
+
131
+ <div
132
+ className={styles.inputContainer}
133
+ style={{
134
+ backgroundColor: theme.inputBackgroundColor,
135
+ borderColor: theme.inputBorderColor,
136
+ }}
137
+ >
138
+ {/* Color preview with picker */}
139
+ <Popover>
140
+ <PopoverTrigger asChild>
141
+ <button
142
+ type="button"
143
+ className={styles.colorPreview}
144
+ style={{
145
+ backgroundColor: value,
146
+ borderColor: theme.inputBorderColor,
147
+ }}
148
+ title="Pick color"
149
+ />
150
+ </PopoverTrigger>
151
+ <PopoverContent
152
+ side="top"
153
+ align="center"
154
+ sideOffset={8}
155
+ style={{
156
+ padding: 'var(--spacing-3)',
157
+ width: 'auto',
158
+ }}
159
+ >
160
+ <RgbaColorPicker color={rgbaValue} onChange={handleRgbaChange} />
161
+ </PopoverContent>
162
+ </Popover>
163
+
164
+ {/* Hex input */}
165
+ <Input
166
+ type="text"
167
+ value={isEditingHex ? hexInput : displayValue}
168
+ onChange={handleHexInputChange}
169
+ onFocus={handleHexInputFocus}
170
+ onBlur={handleHexInputBlur}
171
+ className={styles.input}
172
+ style={{
173
+ backgroundColor: "transparent",
174
+ color: theme.inputTextColor,
175
+ fontFamily: "monospace",
176
+ }}
177
+ placeholder="#000000"
178
+ />
179
+ </div>
180
+ </div>
181
+ );
182
+ }