livestylesync-overlay 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.
@@ -0,0 +1,6 @@
1
+ interface MountOptions {
2
+ port?: number;
3
+ }
4
+ declare function mount(options?: MountOptions): void;
5
+
6
+ export { mount };
package/dist/index.js ADDED
@@ -0,0 +1,411 @@
1
+ // src/index.ts
2
+ import { createRoot } from "react-dom/client";
3
+ import { createElement } from "react";
4
+
5
+ // src/Overlay.tsx
6
+ import { useState, useEffect as useEffect2, useRef as useRef2 } from "react";
7
+
8
+ // src/useWebSocket.ts
9
+ import { useEffect, useRef } from "react";
10
+ function useWebSocket(url) {
11
+ const ws = useRef(null);
12
+ useEffect(() => {
13
+ ws.current = new WebSocket(url);
14
+ ws.current.onopen = () => {
15
+ console.log("[LSS] connected");
16
+ };
17
+ ws.current.onclose = () => {
18
+ console.log("[LSS] disconnected");
19
+ };
20
+ return () => {
21
+ ws.current?.close();
22
+ };
23
+ }, [url]);
24
+ const send = (data) => {
25
+ if (ws.current?.readyState === WebSocket.OPEN) {
26
+ ws.current.send(JSON.stringify(data));
27
+ }
28
+ };
29
+ return { send };
30
+ }
31
+
32
+ // src/Overlay.tsx
33
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
34
+ function rgbToHex(rgb) {
35
+ if (!rgb || rgb === "transparent") return "#ffffff";
36
+ const match = rgb.match(/\d+/g);
37
+ if (!match || match.length < 3) return "#000000";
38
+ return "#" + match.slice(0, 3).map((n) => parseInt(n).toString(16).padStart(2, "0")).join("");
39
+ }
40
+ function getComputed(el, prop) {
41
+ return window.getComputedStyle(el).getPropertyValue(prop).trim();
42
+ }
43
+ function findSourceStyle(el) {
44
+ for (const sheet of Array.from(document.styleSheets)) {
45
+ let fileUrl = sheet.href;
46
+ if (!fileUrl && sheet.ownerNode instanceof HTMLElement) {
47
+ fileUrl = sheet.ownerNode.getAttribute("data-vite-dev-id");
48
+ }
49
+ if (!fileUrl) continue;
50
+ let rules;
51
+ try {
52
+ rules = sheet.cssRules;
53
+ } catch {
54
+ continue;
55
+ }
56
+ for (const rule of Array.from(rules)) {
57
+ if (!(rule instanceof CSSStyleRule)) continue;
58
+ try {
59
+ if (el.matches(rule.selectorText)) {
60
+ return { fileUrl, selector: rule.selectorText };
61
+ }
62
+ } catch {
63
+ continue;
64
+ }
65
+ }
66
+ }
67
+ return null;
68
+ }
69
+ function Overlay({ port = 3100 }) {
70
+ const [open, setOpen] = useState(false);
71
+ const [picking, setPicking] = useState(false);
72
+ const [selected, setSelected] = useState(null);
73
+ const highlightRef = useRef2(null);
74
+ const overlayRootRef = useRef2(null);
75
+ const [padding, setPadding] = useState("0");
76
+ const [margin, setMargin] = useState("0");
77
+ const [color, setColor] = useState("#000000");
78
+ const [bg, setBg] = useState("#ffffff");
79
+ const [source, setSource] = useState(null);
80
+ const [origValues, setOrigValues] = useState({});
81
+ const [pendingChanges, setPendingChanges] = useState({});
82
+ const { send } = useWebSocket(`ws://localhost:${port}`);
83
+ useEffect2(() => {
84
+ if (!selected) return;
85
+ const p = getComputed(selected, "padding-top").replace("px", "");
86
+ const m = getComputed(selected, "margin-top").replace("px", "");
87
+ const c = rgbToHex(getComputed(selected, "color"));
88
+ const b = rgbToHex(getComputed(selected, "background-color"));
89
+ setPadding(p);
90
+ setMargin(m);
91
+ setColor(c);
92
+ setBg(b);
93
+ setSource(findSourceStyle(selected));
94
+ setOrigValues({ padding: p + "px", margin: m + "px", color: c, "background-color": b });
95
+ setPendingChanges({});
96
+ }, [selected]);
97
+ const apply = (prop, value) => {
98
+ if (!selected) return;
99
+ selected.style[prop] = value;
100
+ };
101
+ const applyToFile = () => {
102
+ if (!source) return;
103
+ Object.entries(pendingChanges).forEach(([prop, value]) => {
104
+ send({ ...source, prop, value });
105
+ });
106
+ setOrigValues((prev) => ({ ...prev, ...pendingChanges }));
107
+ setPendingChanges({});
108
+ };
109
+ const row = {
110
+ display: "flex",
111
+ justifyContent: "space-between",
112
+ alignItems: "center",
113
+ marginBottom: 8,
114
+ fontSize: 11,
115
+ color: "#aaa"
116
+ };
117
+ const numInput = {
118
+ background: "#2d2d4e",
119
+ border: "1px solid #555",
120
+ borderRadius: 4,
121
+ color: "#fff",
122
+ fontFamily: "monospace",
123
+ fontSize: 11,
124
+ padding: "2px 6px",
125
+ width: 60,
126
+ textAlign: "right"
127
+ };
128
+ const sectionLabel = {
129
+ margin: "12px 0 6px",
130
+ color: "#555",
131
+ fontSize: 10,
132
+ textTransform: "uppercase",
133
+ letterSpacing: 1
134
+ };
135
+ useEffect2(() => {
136
+ if (!picking) {
137
+ if (highlightRef.current) highlightRef.current.style.display = "none";
138
+ return;
139
+ }
140
+ document.body.style.cursor = "crosshair";
141
+ const handleMouseOver = (e) => {
142
+ const target = e.target;
143
+ if (overlayRootRef.current?.contains(target)) return;
144
+ const rect = target.getBoundingClientRect();
145
+ const h = highlightRef.current;
146
+ if (!h) return;
147
+ h.style.display = "block";
148
+ h.style.top = rect.top + window.scrollY + "px";
149
+ h.style.left = rect.left + window.scrollX + "px";
150
+ h.style.width = rect.width + "px";
151
+ h.style.height = rect.height + "px";
152
+ };
153
+ const handleClick = (e) => {
154
+ const target = e.target;
155
+ if (overlayRootRef.current?.contains(target)) return;
156
+ e.preventDefault();
157
+ e.stopPropagation();
158
+ setSelected(target);
159
+ setPicking(false);
160
+ };
161
+ document.addEventListener("mouseover", handleMouseOver);
162
+ document.addEventListener("click", handleClick, true);
163
+ return () => {
164
+ document.body.style.cursor = "";
165
+ document.removeEventListener("mouseover", handleMouseOver);
166
+ document.removeEventListener("click", handleClick, true);
167
+ };
168
+ }, [picking]);
169
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
170
+ /* @__PURE__ */ jsx(
171
+ "div",
172
+ {
173
+ ref: highlightRef,
174
+ style: {
175
+ display: "none",
176
+ position: "absolute",
177
+ pointerEvents: "none",
178
+ outline: "2px solid #3B82F6",
179
+ background: "rgba(59,130,246,0.08)",
180
+ zIndex: 9998
181
+ }
182
+ }
183
+ ),
184
+ /* @__PURE__ */ jsx(
185
+ "button",
186
+ {
187
+ onClick: () => setOpen((v) => !v),
188
+ style: {
189
+ position: "fixed",
190
+ bottom: 20,
191
+ right: 20,
192
+ width: 12,
193
+ height: 12,
194
+ borderRadius: "50%",
195
+ background: open ? "#5B21B6" : "#7C3AED",
196
+ border: "none",
197
+ cursor: "pointer",
198
+ zIndex: 9999,
199
+ padding: 0
200
+ }
201
+ }
202
+ ),
203
+ open && /* @__PURE__ */ jsxs(
204
+ "div",
205
+ {
206
+ ref: overlayRootRef,
207
+ style: {
208
+ position: "fixed",
209
+ bottom: 44,
210
+ right: 20,
211
+ width: 280,
212
+ background: "#1a1a2e",
213
+ border: "1px solid #7C3AED",
214
+ borderRadius: 8,
215
+ padding: 16,
216
+ zIndex: 9999,
217
+ color: "#fff",
218
+ fontFamily: "monospace",
219
+ fontSize: 13
220
+ },
221
+ children: [
222
+ /* @__PURE__ */ jsx("p", { style: { margin: "0 0 12px" }, children: "LiveStyleSync" }),
223
+ /* @__PURE__ */ jsx(
224
+ "button",
225
+ {
226
+ onClick: () => setPicking((v) => !v),
227
+ style: {
228
+ width: "100%",
229
+ padding: "8px 0",
230
+ background: picking ? "#3B82F6" : "#2d2d4e",
231
+ color: "#fff",
232
+ border: "1px solid " + (picking ? "#3B82F6" : "#555"),
233
+ borderRadius: 6,
234
+ cursor: "pointer",
235
+ fontFamily: "monospace",
236
+ fontSize: 12
237
+ },
238
+ children: picking ? "\u2299 \u041A\u043B\u0438\u043A\u043D\u0438 \u043D\u0430 \u044D\u043B\u0435\u043C\u0435\u043D\u0442..." : "\u2196 \u0412\u044B\u0431\u0440\u0430\u0442\u044C \u044D\u043B\u0435\u043C\u0435\u043D\u0442"
239
+ }
240
+ ),
241
+ selected && /* @__PURE__ */ jsxs(Fragment, { children: [
242
+ /* @__PURE__ */ jsxs("p", { style: { margin: "8px 0 4px", color: "#7C3AED", fontSize: 11 }, children: [
243
+ selected.tagName.toLowerCase(),
244
+ selected.className ? "." + String(selected.className).split(" ")[0] : ""
245
+ ] }),
246
+ /* @__PURE__ */ jsx("p", { style: sectionLabel, children: "Spacing" }),
247
+ /* @__PURE__ */ jsxs("div", { style: row, children: [
248
+ /* @__PURE__ */ jsx("span", { children: "Padding" }),
249
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: 4 }, children: [
250
+ /* @__PURE__ */ jsx(
251
+ "input",
252
+ {
253
+ type: "number",
254
+ value: padding,
255
+ style: numInput,
256
+ onChange: (e) => {
257
+ setPadding(e.target.value);
258
+ apply("padding", e.target.value + "px");
259
+ setPendingChanges((prev) => ({ ...prev, padding: e.target.value + "px" }));
260
+ }
261
+ }
262
+ ),
263
+ /* @__PURE__ */ jsx("span", { style: { fontSize: 10, color: "#555" }, children: "px" })
264
+ ] })
265
+ ] }),
266
+ /* @__PURE__ */ jsxs("div", { style: row, children: [
267
+ /* @__PURE__ */ jsx("span", { children: "Margin" }),
268
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: 4 }, children: [
269
+ /* @__PURE__ */ jsx(
270
+ "input",
271
+ {
272
+ type: "number",
273
+ value: margin,
274
+ style: numInput,
275
+ onChange: (e) => {
276
+ setMargin(e.target.value);
277
+ apply("margin", e.target.value + "px");
278
+ setPendingChanges((prev) => ({ ...prev, margin: e.target.value + "px" }));
279
+ }
280
+ }
281
+ ),
282
+ /* @__PURE__ */ jsx("span", { style: { fontSize: 10, color: "#555" }, children: "px" })
283
+ ] })
284
+ ] }),
285
+ /* @__PURE__ */ jsx("p", { style: sectionLabel, children: "Colors" }),
286
+ /* @__PURE__ */ jsxs("div", { style: row, children: [
287
+ /* @__PURE__ */ jsx("span", { children: "Color" }),
288
+ /* @__PURE__ */ jsx(
289
+ "input",
290
+ {
291
+ type: "color",
292
+ value: color,
293
+ style: { ...numInput, width: 40, padding: 2, cursor: "pointer" },
294
+ onChange: (e) => {
295
+ setColor(e.target.value);
296
+ apply("color", e.target.value);
297
+ setPendingChanges((prev) => ({ ...prev, color: e.target.value }));
298
+ }
299
+ }
300
+ )
301
+ ] }),
302
+ /* @__PURE__ */ jsxs("div", { style: row, children: [
303
+ /* @__PURE__ */ jsx("span", { children: "Background" }),
304
+ /* @__PURE__ */ jsx(
305
+ "input",
306
+ {
307
+ type: "color",
308
+ value: bg,
309
+ style: { ...numInput, width: 40, padding: 2, cursor: "pointer" },
310
+ onChange: (e) => {
311
+ setBg(e.target.value);
312
+ apply("backgroundColor", e.target.value);
313
+ setPendingChanges((prev) => ({ ...prev, "background-color": e.target.value }));
314
+ }
315
+ }
316
+ )
317
+ ] }),
318
+ Object.keys(pendingChanges).length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
319
+ /* @__PURE__ */ jsx("p", { style: { ...sectionLabel, color: "#f59e0b" }, children: "Pending" }),
320
+ Object.entries(pendingChanges).map(([prop, value]) => /* @__PURE__ */ jsxs("div", { style: { ...row, color: "#f59e0b", marginBottom: 4 }, children: [
321
+ /* @__PURE__ */ jsx("span", { children: prop }),
322
+ /* @__PURE__ */ jsxs("span", { style: { fontSize: 10 }, children: [
323
+ origValues[prop],
324
+ " \u2192 ",
325
+ value
326
+ ] })
327
+ ] }, prop)),
328
+ /* @__PURE__ */ jsx(
329
+ "button",
330
+ {
331
+ onClick: applyToFile,
332
+ style: {
333
+ width: "100%",
334
+ padding: "6px 0",
335
+ marginTop: 8,
336
+ background: source ? "#065f46" : "#2d2d4e",
337
+ color: source ? "#6ee7b7" : "#555",
338
+ border: "1px solid " + (source ? "#059669" : "#444"),
339
+ borderRadius: 6,
340
+ cursor: source ? "pointer" : "not-allowed",
341
+ fontFamily: "monospace",
342
+ fontSize: 11
343
+ },
344
+ children: source ? "\u2713 Apply to file" : "\u2717 No CSS source found"
345
+ }
346
+ )
347
+ ] })
348
+ ] })
349
+ ]
350
+ }
351
+ )
352
+ ] });
353
+ }
354
+
355
+ // src/ErrorBoundary.tsx
356
+ import { Component } from "react";
357
+ import { jsxs as jsxs2 } from "react/jsx-runtime";
358
+ var ErrorBoundary = class extends Component {
359
+ constructor() {
360
+ super(...arguments);
361
+ this.state = { error: null };
362
+ }
363
+ static getDerivedStateFromError(error) {
364
+ return { error };
365
+ }
366
+ render() {
367
+ if (this.state.error) {
368
+ return /* @__PURE__ */ jsxs2(
369
+ "div",
370
+ {
371
+ style: {
372
+ position: "fixed",
373
+ bottom: 20,
374
+ right: 20,
375
+ background: "#7f1d1d",
376
+ border: "1px solid #ef4444",
377
+ borderRadius: 8,
378
+ padding: "8px 12px",
379
+ color: "#fca5a5",
380
+ fontFamily: "monospace",
381
+ fontSize: 11,
382
+ zIndex: 9999,
383
+ maxWidth: 280
384
+ },
385
+ children: [
386
+ "LSS error: ",
387
+ this.state.error.message
388
+ ]
389
+ }
390
+ );
391
+ }
392
+ return this.props.children;
393
+ }
394
+ };
395
+
396
+ // src/index.ts
397
+ function mount(options = {}) {
398
+ const container = document.createElement("div");
399
+ container.id = "livestylesync-root";
400
+ document.body.appendChild(container);
401
+ createRoot(container).render(
402
+ createElement(
403
+ ErrorBoundary,
404
+ null,
405
+ createElement(Overlay, { port: options.port ?? 3100 })
406
+ )
407
+ );
408
+ }
409
+ export {
410
+ mount
411
+ };
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "livestylesync-overlay",
3
+ "version": "0.1.0",
4
+ "description": "Browser overlay for LiveStyleSync",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": "./dist/index.js",
12
+ "types": "./dist/index.d.ts"
13
+ }
14
+ },
15
+ "files": ["dist"],
16
+ "scripts": {
17
+ "build": "tsup src/index.ts --format esm --dts --external react --external react-dom"
18
+ },
19
+ "peerDependencies": {
20
+ "react": ">=18",
21
+ "react-dom": ">=18"
22
+ },
23
+ "devDependencies": {
24
+ "@types/react": "^19.2.14",
25
+ "@types/react-dom": "^19.2.3",
26
+ "@vitejs/plugin-react": "^6.0.1",
27
+ "tsup": "^8.5.1",
28
+ "typescript": "^6.0.3",
29
+ "vite": "^8.0.11"
30
+ }
31
+ }