uivisor 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kazbek Kabdulov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,173 @@
1
+ <div align="center">
2
+
3
+ <img src="./uivisor.jpg" alt="uivisor" width="100%" />
4
+
5
+ <h3>Point at any element in your running app, tweak it by hand, and copy a precise,<br/>breakpoint-aware prompt for your AI agent — without ever touching your code.</h3>
6
+
7
+ <p>
8
+ <img alt="dev only" src="https://img.shields.io/badge/dev--only-never%20ships%20to%20prod-22c55e?style=flat-square" />
9
+ <img alt="stack" src="https://img.shields.io/badge/React%20·%20Next.js%20·%20Vite-4f46e5?style=flat-square" />
10
+ <img alt="prompt only" src="https://img.shields.io/badge/prompt--only-no%20code%20mutation-06b6d4?style=flat-square" />
11
+ <img alt="license" src="https://img.shields.io/badge/license-MIT-3f3f46?style=flat-square" />
12
+ </p>
13
+
14
+ </div>
15
+
16
+ ---
17
+
18
+ ## The problem
19
+
20
+ You spot a small UI nit in your running app — this padding's too tight, that heading wants a heavier
21
+ weight, those cards need more radius at `lg`. Two bad options:
22
+
23
+ 1. **Dig into the code** yourself for a one-line change, or
24
+ 2. **Burn agent tokens** describing it in prose — *"on the pricing page, the middle card's button…"* — and hope the agent finds the right element.
25
+
26
+ **uivisor** is a third option. Turn it on, **click the element, nudge it with your mouse**, and it hands you a
27
+ copy-paste instruction that pins the **exact file & line**, the **styling mechanism**, the **breakpoint**, and
28
+ **what to change** — so your agent makes the edit in one shot. uivisor itself **never writes to your source**;
29
+ your tweaks are throwaway inline overrides in the browser.
30
+
31
+ ## What you get
32
+
33
+ Click a button, bump its padding and color, hit **Copy prompt for agent**, and you get:
34
+
35
+ ```md
36
+ # uivisor — apply these UI tweaks (2 changes across 1 element)
37
+
38
+ ## 1. <button> "Get started" — src/components/PricingCard.tsx:26:7
39
+ - Identify by: component <PricingCard>, data-testid="buy-pro", selector `button[data-testid="buy-pro"]`
40
+ - Styling: tailwind (current classes: `mt-6 w-full rounded-md px-4 py-2 bg-indigo-600 text-white`)
41
+ - At lg breakpoint (≥1024px):
42
+ - padding: 16px → 24px → `lg:p-6`
43
+ - background-color: rgb(79,70,229) → #16a34a → `lg:bg-[#16a34a]`
44
+
45
+ ### Rules
46
+ - Edit the EXISTING className/styles. Do NOT add inline styles or duplicate the component.
47
+ - Scope each change to its breakpoint with a responsive variant (e.g. `lg:`).
48
+ - Confirm the element by its source location, text and data-testid before editing.
49
+ ```
50
+
51
+ Paste that into Claude Code / Cursor / whatever — done. No more *"page 55, that thing"*.
52
+
53
+ ---
54
+
55
+ ## Quick start
56
+
57
+ > uivisor runs **only in dev** (`apply: 'serve'` / gated to `next dev`). It is **physically absent from your
58
+ > production build.**
59
+
60
+ It isn't on npm yet, so for a real project link it locally (rebuild + restart picks up changes — no reinstall):
61
+
62
+ ```bash
63
+ # in the uivisor folder
64
+ npm install && npm run build
65
+
66
+ # in YOUR project
67
+ npm i -D file:/absolute/path/to/uivisor
68
+ ```
69
+
70
+ ### Vite + React
71
+
72
+ ```ts
73
+ // vite.config.ts
74
+ import { defineConfig } from 'vite'
75
+ import uivisor from 'uivisor/vite'
76
+ import react from '@vitejs/plugin-react'
77
+
78
+ export default defineConfig({
79
+ plugins: [uivisor(), react()], // ⚠️ uivisor() BEFORE react()
80
+ })
81
+ ```
82
+
83
+ ### Next.js
84
+
85
+ ```js
86
+ // next.config.js
87
+ const { withUivisor } = require('uivisor/next')
88
+
89
+ /** @type {import('next').NextConfig} */
90
+ const nextConfig = { reactStrictMode: true }
91
+
92
+ module.exports = withUivisor(nextConfig)
93
+ ```
94
+
95
+ > Use plain `next dev` (webpack). Turbopack (`--turbo`) ignores `webpack()` config, so source locations
96
+ > won't be injected under it yet.
97
+
98
+ Then `npm run dev` and press **`Alt`+`U`** (or click the **◎** button bottom-right).
99
+
100
+ #### Keeping it updated while we iterate
101
+
102
+ Linked with `file:` → after any change to uivisor, just `npm run build` in the uivisor folder and **restart your
103
+ dev server**. No reinstall. (For Next, clear `.next/` if HMR doesn't pick it up.)
104
+
105
+ ---
106
+
107
+ ## Using the panel
108
+
109
+ 1. **`Alt`+`U`** toggles uivisor · **Esc** deselects.
110
+ 2. **Click any element.** A Figma-like inspector fills with its spacing / border / typography / fill —
111
+ only the controls that are relevant (Typography shows on text elements, Gap on flex/grid containers).
112
+ 3. **Tweak:**
113
+ - **Combined-by-default** — Padding / Margin / Radius show one "all sides" input; click **▦** to split into
114
+ individual sides/corners.
115
+ - **Drag-to-scrub** — drag the icon on the left of any number field (cursor → ↔) to change it live.
116
+ - **Units** — line-height & letter-spacing have a px / % / em / × selector and always show the current number.
117
+ 4. **Screen / breakpoint** — click `md` / `lg` / … and the app loads in a **virtual screen at that width**
118
+ (real CSS media queries reflow); drag the frame edge to fine-tune. Only your project's **real breakpoints**
119
+ are shown. Edits are scoped to the chosen breakpoint (`lg:p-6`). `Live` = your real window.
120
+ 5. **Apply changes to** — pick the edit target:
121
+ - **All N like this** — when the element is a repeated sibling (same component/source, e.g. 3 cards), the
122
+ change previews on *all* of them and the prompt tells the agent to edit the shared component/source.
123
+ - **Only this one** · an existing **`.class`** · or **a new class you name** (agent creates it, leaves the rest).
124
+ 6. **Copy prompt for agent** (or **Copy JSON** for the machine-readable spec).
125
+
126
+ Nothing is written to disk — tweaks live in the browser and vanish on reload.
127
+
128
+ ---
129
+
130
+ ## For your AI agent
131
+
132
+ uivisor's output is a **self-contained instruction**, designed to be acted on without extra context. When you
133
+ receive a uivisor prompt:
134
+
135
+ - **Go to the `file:line:col`** it names — that's the authoritative anchor (injected in dev, React-19 safe).
136
+ - **Edit the existing styling mechanism** it identifies (`tailwind` / `css-modules` / `styled-components` /
137
+ `inline` / `plain-css`) — not inline styles, and don't duplicate the component.
138
+ - **Respect the breakpoint scope** — emit the responsive variant (`lg:…`), don't make it global.
139
+ - **Respect the target** — `All N like this` means edit the shared component/class so every instance updates;
140
+ `new class` means create it and leave existing classes untouched; a positional `nth-of-type` selector is a
141
+ last-resort locator, never the thing to hard-code against.
142
+
143
+ ---
144
+
145
+ ## How it works
146
+
147
+ - **Source mapping** — a tiny dev-only Babel pass tags host JSX with `data-uiv-src="file:line:col"`
148
+ (Vite plugin runs it `enforce: 'pre'`; Next runs it as a webpack pre-loader, keeping SWC).
149
+ - **Identity, layered** — file:line → component name (from the file) → `data-testid` / id / stable selector / text.
150
+ - **Mechanism + tokens** — detects how the element is styled and reverse-maps px to Tailwind tokens
151
+ (`24px → p-6`, `leading-normal`, `tracking-tight`), with arbitrary-value fallback.
152
+ - **Breakpoints** — detected from the `@media` rules in your CSS, so the bar shows *your* breakpoints.
153
+ - **Responsive preview** — the app is loaded in a resizable iframe so real media queries reflow; the inspector
154
+ operates inside it.
155
+
156
+ ## Limitations
157
+
158
+ - **Dev builds only** — production strips source info and mangles classes.
159
+ - **React-first** — the DOM/CSS/breakpoint core is framework-agnostic; only source mapping is React-specific today.
160
+ - **Tailwind-tuned tokens** — non-Tailwind stacks get raw px + selector guidance (the prompt says which
161
+ CSS-module / styled rule to edit).
162
+
163
+ ## Develop
164
+
165
+ ```bash
166
+ npm install
167
+ npm run build # tsup → dist/{vite,babel,overlay,next}
168
+ npm test # vitest (pure logic + babel transform)
169
+ npm run demo # Vite + React playground on :5180
170
+ # demo-next/ # Next.js (app router) playground on :5181
171
+ ```
172
+
173
+ <div align="center"><sub>MIT · dev-only · prompt-only</sub></div>
@@ -0,0 +1,21 @@
1
+ import { types, PluginObj } from '@babel/core';
2
+
3
+ interface BabelAPI {
4
+ types: typeof types;
5
+ }
6
+ interface PluginState {
7
+ filename?: string;
8
+ cwd?: string;
9
+ opts: {
10
+ attr?: string;
11
+ };
12
+ }
13
+ /**
14
+ * Babel plugin: tag every intrinsic (host) JSX element with
15
+ * `data-uiv-src="relative/path.tsx:line:col"` so the overlay can resolve a
16
+ * clicked DOM node back to its exact source location. Dev-only; React-19 safe
17
+ * (does not rely on the removed fiber `_debugSource`).
18
+ */
19
+ declare function uivisorBabel({ types: t }: BabelAPI): PluginObj<PluginState>;
20
+
21
+ export { uivisorBabel as default };
@@ -0,0 +1,6 @@
1
+ import {
2
+ uivisorBabel
3
+ } from "../chunk-4XKGGP26.js";
4
+ export {
5
+ uivisorBabel as default
6
+ };
@@ -0,0 +1,32 @@
1
+ // src/babel/index.ts
2
+ import * as path from "path";
3
+ function uivisorBabel({ types: t }) {
4
+ return {
5
+ name: "uivisor-source",
6
+ visitor: {
7
+ JSXOpeningElement(nodePath, state) {
8
+ const node = nodePath.node;
9
+ const name = node.name;
10
+ if (!t.isJSXIdentifier(name)) return;
11
+ if (!/^[a-z]/.test(name.name)) return;
12
+ if (!node.loc) return;
13
+ const attrName = state.opts.attr || "data-uiv-src";
14
+ const already = node.attributes.some(
15
+ (a) => t.isJSXAttribute(a) && t.isJSXIdentifier(a.name) && a.name.name === attrName
16
+ );
17
+ if (already) return;
18
+ const filename = state.filename || "unknown";
19
+ const cwd = state.cwd || process.cwd();
20
+ const rel = filename === "unknown" ? filename : path.relative(cwd, filename);
21
+ const value = `${rel}:${node.loc.start.line}:${node.loc.start.column + 1}`;
22
+ node.attributes.push(
23
+ t.jsxAttribute(t.jsxIdentifier(attrName), t.stringLiteral(value))
24
+ );
25
+ }
26
+ }
27
+ };
28
+ }
29
+
30
+ export {
31
+ uivisorBabel
32
+ };
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/next/index.ts
31
+ var next_exports = {};
32
+ __export(next_exports, {
33
+ default: () => next_default,
34
+ withUivisor: () => withUivisor
35
+ });
36
+ module.exports = __toCommonJS(next_exports);
37
+ var path = __toESM(require("path"), 1);
38
+ function withUivisor(nextConfig = {}, options = {}) {
39
+ const attr = options.attr || "data-uiv-src";
40
+ const loaderPath = path.join(__dirname, "loader.cjs");
41
+ const overlayPath = path.join(__dirname, "..", "overlay", "index.js");
42
+ return {
43
+ ...nextConfig,
44
+ webpack(config, ctx) {
45
+ if (typeof nextConfig.webpack === "function") {
46
+ config = nextConfig.webpack(config, ctx);
47
+ }
48
+ if (!ctx.dev) return config;
49
+ config.module = config.module || {};
50
+ config.module.rules = config.module.rules || [];
51
+ config.module.rules.push({
52
+ test: /\.(jsx|tsx)$/,
53
+ exclude: /node_modules/,
54
+ enforce: "pre",
55
+ use: [{ loader: loaderPath, options: { attr } }]
56
+ });
57
+ if (!ctx.isServer) {
58
+ const prevEntry = config.entry;
59
+ config.entry = async () => {
60
+ const entries = typeof prevEntry === "function" ? await prevEntry() : prevEntry;
61
+ for (const key of ["main-app", "main.js", "main"]) {
62
+ const e = entries[key];
63
+ if (!e) continue;
64
+ if (Array.isArray(e)) {
65
+ if (!e.includes(overlayPath)) e.unshift(overlayPath);
66
+ } else if (e && Array.isArray(e.import)) {
67
+ if (!e.import.includes(overlayPath)) e.import.unshift(overlayPath);
68
+ }
69
+ }
70
+ return entries;
71
+ };
72
+ }
73
+ return config;
74
+ }
75
+ };
76
+ }
77
+ var next_default = withUivisor;
78
+ // Annotate the CommonJS export names for ESM import in node:
79
+ 0 && (module.exports = {
80
+ withUivisor
81
+ });
@@ -0,0 +1,96 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/next/loader.ts
31
+ var loader_exports = {};
32
+ __export(loader_exports, {
33
+ default: () => uivisorNextLoader
34
+ });
35
+ module.exports = __toCommonJS(loader_exports);
36
+ var import_core = require("@babel/core");
37
+
38
+ // src/babel/index.ts
39
+ var path = __toESM(require("path"), 1);
40
+ function uivisorBabel({ types: t }) {
41
+ return {
42
+ name: "uivisor-source",
43
+ visitor: {
44
+ JSXOpeningElement(nodePath, state) {
45
+ const node = nodePath.node;
46
+ const name = node.name;
47
+ if (!t.isJSXIdentifier(name)) return;
48
+ if (!/^[a-z]/.test(name.name)) return;
49
+ if (!node.loc) return;
50
+ const attrName = state.opts.attr || "data-uiv-src";
51
+ const already = node.attributes.some(
52
+ (a) => t.isJSXAttribute(a) && t.isJSXIdentifier(a.name) && a.name.name === attrName
53
+ );
54
+ if (already) return;
55
+ const filename = state.filename || "unknown";
56
+ const cwd = state.cwd || process.cwd();
57
+ const rel = filename === "unknown" ? filename : path.relative(cwd, filename);
58
+ const value = `${rel}:${node.loc.start.line}:${node.loc.start.column + 1}`;
59
+ node.attributes.push(
60
+ t.jsxAttribute(t.jsxIdentifier(attrName), t.stringLiteral(value))
61
+ );
62
+ }
63
+ }
64
+ };
65
+ }
66
+
67
+ // src/next/loader.ts
68
+ function uivisorNextLoader(source, inMap) {
69
+ const callback = this.async();
70
+ const resourcePath = this.resourcePath || "";
71
+ if (!/\.[jt]sx$/.test(resourcePath) || !source.includes("<")) {
72
+ callback(null, source, inMap);
73
+ return;
74
+ }
75
+ const attr = this.getOptions?.().attr || "data-uiv-src";
76
+ try {
77
+ const result = (0, import_core.transformSync)(source, {
78
+ filename: resourcePath,
79
+ cwd: this.rootContext || process.cwd(),
80
+ babelrc: false,
81
+ configFile: false,
82
+ sourceMaps: true,
83
+ parserOpts: { plugins: ["jsx", "typescript"], sourceType: "module" },
84
+ generatorOpts: { retainLines: true },
85
+ plugins: [[uivisorBabel, { attr }]]
86
+ });
87
+ if (!result?.code) {
88
+ callback(null, source, inMap);
89
+ return;
90
+ }
91
+ callback(null, result.code, result.map ?? inMap);
92
+ } catch {
93
+ callback(null, source, inMap);
94
+ }
95
+ }
96
+ if (module.exports && module.exports.default) module.exports = module.exports.default;
@@ -0,0 +1,3 @@
1
+ declare function start(): void;
2
+
3
+ export { start };