uivisor 0.1.0 → 0.1.2

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/README.md CHANGED
@@ -2,69 +2,25 @@
2
2
 
3
3
  <img src="./uivisor.jpg" alt="uivisor" width="100%" />
4
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
5
  <p>
8
- <img alt="dev only" src="https://img.shields.io/badge/dev--only-never%20ships%20to%20prod-22c55e?style=flat-square" />
6
+ <img alt="npm" src="https://img.shields.io/npm/v/uivisor?style=flat-square&color=cb3837&label=npm" />
7
+ <img alt="dev only" src="https://img.shields.io/badge/только%20dev-в%20прод%20не%20попадает-22c55e?style=flat-square" />
9
8
  <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
9
  <img alt="license" src="https://img.shields.io/badge/license-MIT-3f3f46?style=flat-square" />
12
10
  </p>
13
11
 
14
12
  </div>
15
13
 
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]`
14
+ Dev-only инструмент для React / Next.js / Vite. Кликни элемент в работающем приложении, поправь его мышкой — панель показывает текущие стили, твои правки подсвечены зелёным. Нажми **Copy prompt for agent** — получишь промпт с `file:line:col`, механизмом стилей и брейкпоинтом, готовый для Claude Code / Cursor.
44
15
 
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"*.
16
+ В исходники не пишет. Правки — временные inline-оверрайды в браузере, пропадают при reload.
52
17
 
53
18
  ---
54
19
 
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):
20
+ ## Установка
61
21
 
62
22
  ```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
23
+ npm i -D uivisor
68
24
  ```
69
25
 
70
26
  ### Vite + React
@@ -76,98 +32,118 @@ import uivisor from 'uivisor/vite'
76
32
  import react from '@vitejs/plugin-react'
77
33
 
78
34
  export default defineConfig({
79
- plugins: [uivisor(), react()], // ⚠️ uivisor() BEFORE react()
35
+ plugins: [uivisor(), react()], // uivisor() до react()
80
36
  })
81
37
  ```
82
38
 
83
- ### Next.js
39
+ Запусти dev-сервер и нажми **`Alt`+`U`**.
40
+
41
+ ### Next.js (App Router, Next 13–16)
42
+
43
+ **1. Оберни конфиг:**
44
+
45
+ ```ts
46
+ // next.config.ts
47
+ import { withUivisor } from 'uivisor/next'
48
+ export default withUivisor({ reactStrictMode: true })
49
+ ```
84
50
 
85
51
  ```js
86
- // next.config.js
52
+ // next.config.js (CommonJS)
87
53
  const { withUivisor } = require('uivisor/next')
54
+ module.exports = withUivisor({ reactStrictMode: true })
55
+ ```
56
+
57
+ **2. Добавь оверлей в корневой layout:**
88
58
 
89
- /** @type {import('next').NextConfig} */
90
- const nextConfig = { reactStrictMode: true }
59
+ ```tsx
60
+ // app/layout.tsx
61
+ import { UivisorOverlay } from 'uivisor/next/overlay'
91
62
 
92
- module.exports = withUivisor(nextConfig)
63
+ export default function RootLayout({ children }) {
64
+ return <html><body>{children}<UivisorOverlay /></body></html>
65
+ }
93
66
  ```
94
67
 
95
- > Use plain `next dev` (webpack). Turbopack (`--turbo`) ignores `webpack()` config, so source locations
96
- > won't be injected under it yet.
68
+ **3. Запусти `next dev` и нажми `Alt`+`U`.**
97
69
 
98
- Then `npm run dev` and press **`Alt`+`U`** (or click the **◎** button bottom-right).
70
+ Работает под обоими бандлерами:
99
71
 
100
- #### Keeping it updated while we iterate
72
+ | Команда | Оверлей | Точный `file:line` |
73
+ |---|---|---|
74
+ | `next dev` (Turbopack, по умолчанию в Next 15/16) | ✅ | ✅ |
75
+ | `next dev --webpack` | ✅ | ✅ |
101
76
 
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.)
77
+ > Чтобы отключить source-mapping под Turbopack: `withUivisor(config, { turbopack: false })` оверлей продолжит работать, `file:line` доступен через `next dev --webpack`.
78
+ >
79
+ > Под Turbopack ставь uivisor **из npm** (`npm i -D uivisor`). Локальный `file:`-линк (`file:../uivisor`) Turbopack не резолвит для сабпасов вроде `uivisor/next/overlay` — будет `Module not found`. С webpack `file:`-линк работает.
104
80
 
105
81
  ---
106
82
 
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.
83
+ ## Работа с панелью
84
+
85
+ **`Alt`+`U`** включить/выключить · **`Esc`** — снять выделение · **◎** — кнопка справа снизу.
86
+
87
+ 1. **Кликни любой элемент.** Панель подтягивает его текущие стили: значения из браузера горят белым, правки в uivisor — зелёным.
88
+ 2. **Правь:**
89
+ - Padding / Margin / Radius одно поле на все стороны; кнопка **▦** раскрывает по отдельности.
90
+ - Иконка слева от числа (↔) — тяни, значение меняется вживую.
91
+ - Line-height и letter-spacingпереключатель единиц px / % / em.
92
+ 3. **Screen / breakpoint** — клик на `md` / `lg` / загружает приложение в виртуальный экран этой ширины (реальные медиа-запросы перестраиваются). В баре — только брейкпоинты твоего проекта. `Live` — реальное окно.
93
+ 4. **Apply changes to** — куда применить правку:
94
+ - **All N like this** если компонент повторяется N раз, превью и промпт применяются ко всем; агент правит общий компонент, а не `nth-child`.
95
+ - **Only this one** / существующий `.class` / новый класс с именем.
96
+ 5. **Copy prompt for agent** — промпт в буфер. **Copy JSON** — машиночитаемый спек.
127
97
 
128
98
  ---
129
99
 
130
- ## For your AI agent
100
+ ## Промпт для агента
101
+
102
+ ```md
103
+ # uivisor — apply these UI tweaks (2 changes across 1 element)
104
+
105
+ ## 1. <button> "Get started" — src/components/PricingCard.tsx:26:7
106
+ - Identify by: component <PricingCard>, data-testid="buy-pro"
107
+ - Styling: tailwind (current classes: `mt-6 w-full rounded-md px-4 py-2 bg-indigo-600 text-white`)
108
+ - At lg breakpoint (≥1024px):
109
+ - padding: 16px → 24px → `lg:p-6`
110
+ - background-color: #4f46e5 → #16a34a → `lg:bg-[#16a34a]`
111
+
112
+ ### Rules
113
+ - Edit the EXISTING className/styles. Do NOT add inline styles or duplicate the component.
114
+ - Scope each change to its breakpoint with a responsive variant (e.g. `lg:`).
115
+ ```
131
116
 
132
- uivisor's output is a **self-contained instruction**, designed to be acted on without extra context. When you
133
- receive a uivisor prompt:
117
+ Инструкции агенту:
134
118
 
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.
119
+ - Иди по `file:line:col`главный якорь.
120
+ - Правь существующий механизм стилей (`tailwind` / `css-modules` / `styled-components` / `inline`) — не добавляй inline и не дублируй компонент.
121
+ - Отдавай responsive-вариант под нужный брейкпоинт.
122
+ - `All N like this`правь общий компонент/класс, не `nth-child`.
142
123
 
143
124
  ---
144
125
 
145
- ## How it works
126
+ ## Как это устроено
146
127
 
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.
128
+ - **Source mapping** — dev-only Babel/loader вешает `data-uiv-src="file:line:col"` на host-JSX. Vite — плагином `enforce: 'pre'`, Next — webpack pre-loader и `turbopack.rules` (SWC сохраняется).
129
+ - **Идентичность** file:line имя компонента `data-testid` / id / селектор / текст.
130
+ - **Токены**маппит px в Tailwind-токены (`24px p-6`, `leading-normal`); arbitrary-значения как фолбэк.
131
+ - **Брейкпоинты**детектятся из `@media`-правил твоего CSS.
132
+ - **Responsive-превью** приложение в ресайзимом iframe, медиа-запросы работают по-настоящему.
155
133
 
156
- ## Limitations
134
+ ## Ограничения
157
135
 
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).
136
+ - Только dev-сборкипрод вырезает source-инфо.
137
+ - Source mapping React. DOM/CSS/брейкпоинты фреймворк-агностично.
138
+ - Tailwind — px→токены. Другие стеки сырые px + указание на CSS-правило.
162
139
 
163
- ## Develop
140
+ ## Разработка
164
141
 
165
142
  ```bash
166
143
  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
144
+ npm run build # tsup → dist/{vite,babel,overlay,next}
145
+ npm test # vitest
146
+ npm run demo # Vite + React на :5180
171
147
  ```
172
148
 
173
149
  <div align="center"><sub>MIT · dev-only · prompt-only</sub></div>
@@ -35,13 +35,34 @@ __export(next_exports, {
35
35
  });
36
36
  module.exports = __toCommonJS(next_exports);
37
37
  var path = __toESM(require("path"), 1);
38
+ var import_node_module = require("module");
39
+ var isProd = () => process.env.NODE_ENV === "production";
40
+ var isTurbopack = () => !!process.env.TURBOPACK || !!process.env.TURBOPACK_DEV;
41
+ function nextMajor() {
42
+ try {
43
+ const req = (0, import_node_module.createRequire)(path.join(process.cwd(), "package.json"));
44
+ return parseInt(req("next/package.json").version, 10) || 0;
45
+ } catch {
46
+ return 0;
47
+ }
48
+ }
49
+ var hintShown = false;
50
+ function turbopackHint(canSourceMap) {
51
+ if (hintShown || !isTurbopack() || isProd()) return;
52
+ hintShown = true;
53
+ console.log(
54
+ '\n[uivisor] Turbopack detected.\n \u2022 Overlay: render <UivisorOverlay/> from "uivisor/next/overlay" in your root layout.\n' + (canSourceMap ? " \u2022 Source locations: ON via turbopack.rules.\n" : " \u2022 Exact file:line: use `next dev --webpack` (or remove `{ turbopack: false }`).\n")
55
+ );
56
+ }
38
57
  function withUivisor(nextConfig = {}, options = {}) {
39
58
  const attr = options.attr || "data-uiv-src";
40
59
  const loaderPath = path.join(__dirname, "loader.cjs");
41
60
  const overlayPath = path.join(__dirname, "..", "overlay", "index.js");
42
- return {
43
- ...nextConfig,
44
- webpack(config, ctx) {
61
+ const useTurbopack = options.turbopack !== false && isTurbopack() && !isProd();
62
+ turbopackHint(useTurbopack);
63
+ const out = { ...nextConfig };
64
+ if (!isTurbopack()) {
65
+ out.webpack = (config, ctx) => {
45
66
  if (typeof nextConfig.webpack === "function") {
46
67
  config = nextConfig.webpack(config, ctx);
47
68
  }
@@ -71,8 +92,29 @@ function withUivisor(nextConfig = {}, options = {}) {
71
92
  };
72
93
  }
73
94
  return config;
95
+ };
96
+ }
97
+ if (useTurbopack) {
98
+ const ld = [{ loader: "uivisor/next/loader", options: { attr } }];
99
+ const rules = {
100
+ "*.tsx": { loaders: ld, as: "*.tsx" },
101
+ "*.jsx": { loaders: ld, as: "*.jsx" }
102
+ };
103
+ const major = nextMajor();
104
+ if (major === 0 || major >= 15) {
105
+ out.turbopack = {
106
+ ...nextConfig.turbopack,
107
+ rules: { ...nextConfig.turbopack?.rules ?? {}, ...rules }
108
+ };
109
+ } else {
110
+ const exp = nextConfig.experimental ?? {};
111
+ out.experimental = {
112
+ ...exp,
113
+ turbo: { ...exp.turbo ?? {}, rules: { ...exp.turbo?.rules ?? {}, ...rules } }
114
+ };
74
115
  }
75
- };
116
+ }
117
+ return out;
76
118
  }
77
119
  var next_default = withUivisor;
78
120
  // Annotate the CommonJS export names for ESM import in node:
@@ -0,0 +1,41 @@
1
+ interface WebpackContext {
2
+ dev: boolean;
3
+ isServer: boolean;
4
+ }
5
+ type NextConfig = {
6
+ webpack?: ((config: any, ctx: WebpackContext) => any) | null;
7
+ turbopack?: any;
8
+ [key: string]: unknown;
9
+ };
10
+ interface UivisorNextOptions {
11
+ /** data attribute name for source locations. Default: "data-uiv-src" */
12
+ attr?: string;
13
+ /**
14
+ * Register the source-location loader for Turbopack (`turbopack.rules`) so
15
+ * `data-uiv-src` is injected under plain `next dev` too. Default: true.
16
+ * Set false to disable if it ever conflicts with your setup (the overlay still
17
+ * works via <UivisorOverlay/>; use `next dev --webpack` for source attrs then).
18
+ */
19
+ turbopack?: boolean;
20
+ }
21
+ /**
22
+ * Wrap your Next.js config to enable uivisor in dev.
23
+ *
24
+ * // next.config.ts / .mjs
25
+ * import { withUivisor } from 'uivisor/next'
26
+ * export default withUivisor({ ...yourConfig })
27
+ *
28
+ * // next.config.js (CommonJS)
29
+ * const { withUivisor } = require('uivisor/next')
30
+ * module.exports = withUivisor({ ...yourConfig })
31
+ *
32
+ * Then render <UivisorOverlay/> (from "uivisor/next/overlay") in your root layout.
33
+ *
34
+ * - webpack (`next dev --webpack`): injects `data-uiv-src` + the overlay automatically.
35
+ * - Turbopack (`next dev`): the overlay works via <UivisorOverlay/>; source locations
36
+ * need either `--webpack` or `{ turbopack: true }` (experimental).
37
+ * Dev-only — nothing is added to production builds.
38
+ */
39
+ declare function withUivisor(nextConfig?: NextConfig, options?: UivisorNextOptions): NextConfig;
40
+
41
+ export { type UivisorNextOptions, withUivisor as default, withUivisor };
@@ -0,0 +1,41 @@
1
+ interface WebpackContext {
2
+ dev: boolean;
3
+ isServer: boolean;
4
+ }
5
+ type NextConfig = {
6
+ webpack?: ((config: any, ctx: WebpackContext) => any) | null;
7
+ turbopack?: any;
8
+ [key: string]: unknown;
9
+ };
10
+ interface UivisorNextOptions {
11
+ /** data attribute name for source locations. Default: "data-uiv-src" */
12
+ attr?: string;
13
+ /**
14
+ * Register the source-location loader for Turbopack (`turbopack.rules`) so
15
+ * `data-uiv-src` is injected under plain `next dev` too. Default: true.
16
+ * Set false to disable if it ever conflicts with your setup (the overlay still
17
+ * works via <UivisorOverlay/>; use `next dev --webpack` for source attrs then).
18
+ */
19
+ turbopack?: boolean;
20
+ }
21
+ /**
22
+ * Wrap your Next.js config to enable uivisor in dev.
23
+ *
24
+ * // next.config.ts / .mjs
25
+ * import { withUivisor } from 'uivisor/next'
26
+ * export default withUivisor({ ...yourConfig })
27
+ *
28
+ * // next.config.js (CommonJS)
29
+ * const { withUivisor } = require('uivisor/next')
30
+ * module.exports = withUivisor({ ...yourConfig })
31
+ *
32
+ * Then render <UivisorOverlay/> (from "uivisor/next/overlay") in your root layout.
33
+ *
34
+ * - webpack (`next dev --webpack`): injects `data-uiv-src` + the overlay automatically.
35
+ * - Turbopack (`next dev`): the overlay works via <UivisorOverlay/>; source locations
36
+ * need either `--webpack` or `{ turbopack: true }` (experimental).
37
+ * Dev-only — nothing is added to production builds.
38
+ */
39
+ declare function withUivisor(nextConfig?: NextConfig, options?: UivisorNextOptions): NextConfig;
40
+
41
+ export { type UivisorNextOptions, withUivisor as default, withUivisor };
@@ -0,0 +1,95 @@
1
+ // node_modules/tsup/assets/esm_shims.js
2
+ import path from "path";
3
+ import { fileURLToPath } from "url";
4
+ var getFilename = () => fileURLToPath(import.meta.url);
5
+ var getDirname = () => path.dirname(getFilename());
6
+ var __dirname = /* @__PURE__ */ getDirname();
7
+
8
+ // src/next/index.ts
9
+ import * as path2 from "path";
10
+ import { createRequire } from "module";
11
+ var isProd = () => process.env.NODE_ENV === "production";
12
+ var isTurbopack = () => !!process.env.TURBOPACK || !!process.env.TURBOPACK_DEV;
13
+ function nextMajor() {
14
+ try {
15
+ const req = createRequire(path2.join(process.cwd(), "package.json"));
16
+ return parseInt(req("next/package.json").version, 10) || 0;
17
+ } catch {
18
+ return 0;
19
+ }
20
+ }
21
+ var hintShown = false;
22
+ function turbopackHint(canSourceMap) {
23
+ if (hintShown || !isTurbopack() || isProd()) return;
24
+ hintShown = true;
25
+ console.log(
26
+ '\n[uivisor] Turbopack detected.\n \u2022 Overlay: render <UivisorOverlay/> from "uivisor/next/overlay" in your root layout.\n' + (canSourceMap ? " \u2022 Source locations: ON via turbopack.rules.\n" : " \u2022 Exact file:line: use `next dev --webpack` (or remove `{ turbopack: false }`).\n")
27
+ );
28
+ }
29
+ function withUivisor(nextConfig = {}, options = {}) {
30
+ const attr = options.attr || "data-uiv-src";
31
+ const loaderPath = path2.join(__dirname, "loader.cjs");
32
+ const overlayPath = path2.join(__dirname, "..", "overlay", "index.js");
33
+ const useTurbopack = options.turbopack !== false && isTurbopack() && !isProd();
34
+ turbopackHint(useTurbopack);
35
+ const out = { ...nextConfig };
36
+ if (!isTurbopack()) {
37
+ out.webpack = (config, ctx) => {
38
+ if (typeof nextConfig.webpack === "function") {
39
+ config = nextConfig.webpack(config, ctx);
40
+ }
41
+ if (!ctx.dev) return config;
42
+ config.module = config.module || {};
43
+ config.module.rules = config.module.rules || [];
44
+ config.module.rules.push({
45
+ test: /\.(jsx|tsx)$/,
46
+ exclude: /node_modules/,
47
+ enforce: "pre",
48
+ use: [{ loader: loaderPath, options: { attr } }]
49
+ });
50
+ if (!ctx.isServer) {
51
+ const prevEntry = config.entry;
52
+ config.entry = async () => {
53
+ const entries = typeof prevEntry === "function" ? await prevEntry() : prevEntry;
54
+ for (const key of ["main-app", "main.js", "main"]) {
55
+ const e = entries[key];
56
+ if (!e) continue;
57
+ if (Array.isArray(e)) {
58
+ if (!e.includes(overlayPath)) e.unshift(overlayPath);
59
+ } else if (e && Array.isArray(e.import)) {
60
+ if (!e.import.includes(overlayPath)) e.import.unshift(overlayPath);
61
+ }
62
+ }
63
+ return entries;
64
+ };
65
+ }
66
+ return config;
67
+ };
68
+ }
69
+ if (useTurbopack) {
70
+ const ld = [{ loader: "uivisor/next/loader", options: { attr } }];
71
+ const rules = {
72
+ "*.tsx": { loaders: ld, as: "*.tsx" },
73
+ "*.jsx": { loaders: ld, as: "*.jsx" }
74
+ };
75
+ const major = nextMajor();
76
+ if (major === 0 || major >= 15) {
77
+ out.turbopack = {
78
+ ...nextConfig.turbopack,
79
+ rules: { ...nextConfig.turbopack?.rules ?? {}, ...rules }
80
+ };
81
+ } else {
82
+ const exp = nextConfig.experimental ?? {};
83
+ out.experimental = {
84
+ ...exp,
85
+ turbo: { ...exp.turbo ?? {}, rules: { ...exp.turbo?.rules ?? {}, ...rules } }
86
+ };
87
+ }
88
+ }
89
+ return out;
90
+ }
91
+ var next_default = withUivisor;
92
+ export {
93
+ next_default as default,
94
+ withUivisor
95
+ };
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Dev-only overlay mount for Next.js. Render once near the end of your root
3
+ * layout's <body>. Works under BOTH Turbopack and webpack (no bundler magic) —
4
+ * it just dynamically imports the overlay on the client in development.
5
+ *
6
+ * // app/layout.tsx
7
+ * import { UivisorOverlay } from 'uivisor/next/overlay'
8
+ * ...
9
+ * <body>{children}<UivisorOverlay /></body>
10
+ */
11
+ declare function UivisorOverlay(): null;
12
+
13
+ export { UivisorOverlay, UivisorOverlay as default };
@@ -0,0 +1,17 @@
1
+ "use client";
2
+
3
+ // src/next/overlay.ts
4
+ import { useEffect } from "react";
5
+ function UivisorOverlay() {
6
+ useEffect(() => {
7
+ if (process.env.NODE_ENV !== "production") {
8
+ void import("uivisor/overlay");
9
+ }
10
+ }, []);
11
+ return null;
12
+ }
13
+ var overlay_default = UivisorOverlay;
14
+ export {
15
+ UivisorOverlay,
16
+ overlay_default as default
17
+ };
@@ -894,6 +894,16 @@ var CSS = (
894
894
  .uiv-newclass:focus { border-style: solid; border-color: #6366f1; color: #fff; }
895
895
  .uiv-newclass.on { border-style: solid; border-color: #22d3ee; color: #fff; }
896
896
 
897
+ /* ---- current-styles readout ---- */
898
+ .uiv-readout { display: flex; flex-direction: column; gap: 3px; }
899
+ .uiv-rrow { display: grid; grid-template-columns: 70px 1fr; gap: 8px; align-items: center;
900
+ font-size: 11px; font-family: ui-monospace, monospace; }
901
+ .uiv-rk { color: #71717a; }
902
+ .uiv-rv { color: #fff; word-break: break-all; display: flex; align-items: center; gap: 6px; }
903
+ .uiv-rv.changed { color: #4ade80; } /* edited in uivisor \u2192 green */
904
+ .uiv-sw { display: inline-block; width: 11px; height: 11px; border-radius: 3px;
905
+ border: 1px solid rgba(255,255,255,0.2); flex: 0 0 auto; }
906
+
897
907
  /* ---- Figma-like sectioned controls ---- */
898
908
  .uiv-sectitle { margin: 0 0 8px; font-size: 10px; text-transform: uppercase;
899
909
  letter-spacing: .5px; color: #8b8b94; font-weight: 600; }
@@ -908,7 +918,9 @@ var CSS = (
908
918
  /* numeric field with a scrub handle on the left */
909
919
  .uiv-num { display: flex; align-items: stretch; background: #27272a;
910
920
  border: 1px solid #3f3f46; border-radius: 7px; overflow: hidden; }
911
- .uiv-num.changed { border-color: #22d3ee; }
921
+ .uiv-num.changed { border-color: #4ade80; }
922
+ .uiv-num.changed input { color: #4ade80; } /* uivisor-edited value \u2192 green */
923
+ .uiv-sel.changed, .uiv-color.changed { border-color: #4ade80; }
912
924
  .uiv-num:focus-within { border-color: #6366f1; }
913
925
  .uiv-scrub { display: flex; align-items: center; justify-content: center;
914
926
  width: 24px; color: #8b8b94; cursor: ew-resize; user-select: none;
@@ -948,7 +960,7 @@ var CSS = (
948
960
  .uiv-ctl input.uiv-text { width: 100%; background: #27272a; border: 1px solid #3f3f46;
949
961
  color: #fff; border-radius: 7px; padding: 6px 7px; font-size: 12px; outline: none; }
950
962
  .uiv-ctl input.uiv-text:focus { border-color: #6366f1; }
951
- .uiv-ctl input.uiv-text.changed { border-color: #22d3ee; }
963
+ .uiv-ctl input.uiv-text.changed { border-color: #4ade80; color: #4ade80; }
952
964
 
953
965
  .uiv-journal { display: flex; flex-direction: column; gap: 8px; }
954
966
  .uiv-jitem { background: #1f1f23; border: 1px solid #27272a; border-radius: 8px; padding: 8px; }
@@ -1109,6 +1121,25 @@ var Uivisor = class {
1109
1121
  window.addEventListener("resize", this.onResize, true);
1110
1122
  this.renderBody();
1111
1123
  this.updateBp();
1124
+ window.setTimeout(() => this.reportStatus(), 1500);
1125
+ }
1126
+ /** Tell the dev, in the console, whether uivisor is alive and source-mapped. */
1127
+ reportStatus() {
1128
+ try {
1129
+ const n = document.querySelectorAll("[data-uiv-src]").length;
1130
+ if (n > 0) {
1131
+ console.info(
1132
+ `%cuivisor%c active \u2014 Alt+U (or \u25CE) \xB7 ${n} source-mapped elements`,
1133
+ "color:#818cf8;font-weight:700",
1134
+ "color:inherit"
1135
+ );
1136
+ } else {
1137
+ console.warn(
1138
+ "[uivisor] active, but NO source attributes were found \u2014 exact file:line is OFF.\n\u2022 Next + Turbopack: run `next dev --webpack`, or enable the uivisor Turbopack loader.\n\u2022 Vite: make sure uivisor() is listed BEFORE react() in your plugins.\nTweaking still works (it falls back to component name + selector + text)."
1139
+ );
1140
+ }
1141
+ } catch {
1142
+ }
1112
1143
  }
1113
1144
  q(sel) {
1114
1145
  return this.root.querySelector(sel);
@@ -1476,6 +1507,7 @@ var Uivisor = class {
1476
1507
  <div class="uiv-src">${escapeHtml(src)}</div>
1477
1508
  <span class="uiv-mech">${st.record.styling.primaryMechanism}</span>
1478
1509
  </div>
1510
+ ${this.currentStylesHtml()}
1479
1511
  ${this.breakpointBarHtml()}
1480
1512
  ${this.targetHtml(st)}
1481
1513
  ${this.controlsHtml(this.context(this.selected))}
@@ -1483,20 +1515,93 @@ var Uivisor = class {
1483
1515
  `;
1484
1516
  this.bindControls();
1485
1517
  }
1518
+ /** Read-only readout of the element's actual current styles — so you don't guess. */
1519
+ currentStylesHtml() {
1520
+ const el = this.selected;
1521
+ if (!el) return "";
1522
+ let cs;
1523
+ try {
1524
+ cs = getComputedStyle(el);
1525
+ } catch {
1526
+ return "";
1527
+ }
1528
+ const g = (p) => cs.getPropertyValue(p).trim();
1529
+ const hex = (v) => rgbToHex(v) || v;
1530
+ const swatch = (v) => {
1531
+ const h = hex(v);
1532
+ return /^#|rgb/.test(h) ? `<span class="uiv-sw" style="background:${h}"></span>${h}` : h;
1533
+ };
1534
+ const box = (prefix) => {
1535
+ const t = g(`${prefix}-top`);
1536
+ const r = g(`${prefix}-right`);
1537
+ const b = g(`${prefix}-bottom`);
1538
+ const l = g(`${prefix}-left`);
1539
+ if (t === r && r === b && b === l) return t;
1540
+ if (t === b && r === l) return `${t} ${r}`;
1541
+ return `${t} ${r} ${b} ${l}`;
1542
+ };
1543
+ const changedSet = new Set(this.st()?.record.changes.map((c) => c.property) ?? []);
1544
+ const changed = (props) => props.some((p) => changedSet.has(p));
1545
+ const px4 = (pre) => [`${pre}-top`, `${pre}-right`, `${pre}-bottom`, `${pre}-left`];
1546
+ const rows = [];
1547
+ const add = (k, v, props = []) => {
1548
+ if (v) rows.push({ k, v, edited: changed(props) });
1549
+ };
1550
+ add("display", g("display"));
1551
+ add("size", `${Math.round(parseFloat(g("width")) || 0)} \xD7 ${Math.round(parseFloat(g("height")) || 0)}`);
1552
+ const pad = box("padding");
1553
+ if (pad && pad !== "0px") add("padding", pad, px4("padding"));
1554
+ const mar = box("margin");
1555
+ if (mar && mar !== "0px") add("margin", mar, px4("margin"));
1556
+ if (/flex|grid/.test(g("display"))) {
1557
+ const gap = g("gap");
1558
+ if (gap && gap !== "normal" && gap !== "0px") add("gap", gap, ["gap"]);
1559
+ }
1560
+ add("font", `${g("font-size")} \xB7 ${g("font-weight")} \xB7 lh ${g("line-height")}`, [
1561
+ "font-size",
1562
+ "font-weight",
1563
+ "line-height"
1564
+ ]);
1565
+ const ls = g("letter-spacing");
1566
+ if (ls && ls !== "normal") add("tracking", ls, ["letter-spacing"]);
1567
+ add("color", swatch(g("color")), ["color"]);
1568
+ const bg = g("background-color");
1569
+ if (bg && bg !== "rgba(0, 0, 0, 0)" && bg !== "transparent")
1570
+ add("background", swatch(bg), ["background-color"]);
1571
+ const bw = g("border-top-width");
1572
+ if (bw && parseFloat(bw) > 0)
1573
+ add("border", `${bw} ${g("border-top-style")} ${hex(g("border-top-color"))}`, px4("border").map((p) => `${p}-width`));
1574
+ const br = g("border-radius");
1575
+ if (br && br !== "0px")
1576
+ add("radius", br, [
1577
+ "border-radius",
1578
+ "border-top-left-radius",
1579
+ "border-top-right-radius",
1580
+ "border-bottom-right-radius",
1581
+ "border-bottom-left-radius"
1582
+ ]);
1583
+ if (g("box-shadow") !== "none" && g("box-shadow")) add("shadow", "yes");
1584
+ const op = g("opacity");
1585
+ if (op && parseFloat(op) < 1) add("opacity", op);
1586
+ const items = rows.map((r) => `<div class="uiv-rrow"><span class="uiv-rk">${r.k}</span><span class="uiv-rv${r.edited ? " changed" : ""}">${r.v}</span></div>`).join("");
1587
+ return `<div class="uiv-sec"><div class="uiv-sectitle">Current styles</div><div class="uiv-readout">${items}</div></div>`;
1588
+ }
1486
1589
  /** Breakpoint scope switcher: shows the PROJECT's breakpoints + the live window one. */
1487
1590
  breakpointBarHtml() {
1488
1591
  const sys = this.bpSystem();
1489
1592
  const bps = sys.breakpoints;
1490
1593
  const names = ["base", ...bps.map((b) => b.name)];
1491
1594
  const frameBp = this.responsive ? activeBreakpoint(this.frameWidth, sys).name : null;
1492
- const liveChip = `<button class="uiv-chip${!this.responsive ? " on" : ""}" data-bp="live" title="Your real browser window">Live</button>`;
1595
+ const winBp = currentBreakpoint(sys).name;
1596
+ const liveW = typeof window !== "undefined" ? window.innerWidth : 0;
1597
+ const liveChip = `<button class="uiv-chip${!this.responsive ? " on" : ""}" data-bp="live" title="Follow your real browser window">Live</button>`;
1493
1598
  const chips = names.map((n) => {
1494
- const active = this.responsive && n === frameBp;
1599
+ const active = this.responsive ? n === frameBp : n === winBp;
1495
1600
  const px2 = n === "base" ? 0 : bps.find((b) => b.name === n).minWidth;
1496
1601
  return `<button class="uiv-chip${active ? " on" : ""}" data-bp="${n}" title="Preview at \u2265${px2}px">${n}</button>`;
1497
1602
  }).join("");
1498
1603
  const detected = sys.name === "detected" ? "" : " (defaults)";
1499
- const hint = this.responsive ? `Virtual screen at <b>${this.frameWidth}px</b> (${frameBp}). Edits scoped to <b>${frameBp}:</b>. Drag the frame edge to fine-tune.` : `Click a size to shrink the screen to it & design for that breakpoint. Live = your real window.`;
1604
+ const hint = this.responsive ? `Virtual screen at <b>${this.frameWidth}px</b> (${frameBp}). Edits scoped to <b>${frameBp}:</b>. Drag the frame edge to fine-tune.` : `Live \u2014 your window is <b>${liveW}px</b> = <b>${winBp}</b> range, edits scoped to <b>${winBp}:</b>. Click another size to shrink the screen to it.`;
1500
1605
  return `<div class="uiv-sec"><div class="uiv-sectitle">Screen / breakpoint${detected}</div><div class="uiv-chips">${liveChip}${chips}</div><div class="uiv-bphint">${hint}</div></div>`;
1501
1606
  }
1502
1607
  /** "Apply changes to": this element, an existing shared class, or a NEW class. */
@@ -1641,10 +1746,10 @@ var Uivisor = class {
1641
1746
  if (c.kind === "select") {
1642
1747
  const cur = this.selectCurrent(c.css);
1643
1748
  const opts = c.options.map((o) => `<option value="${o}"${o === cur ? " selected" : ""}>${o}</option>`).join("");
1644
- return `<div class="uiv-ctl"><span class="clabel">${c.label}</span><div class="cfield"><select class="uiv-sel" data-css="${c.css}">${opts}</select></div><span></span></div>`;
1749
+ return `<div class="uiv-ctl"><span class="clabel">${c.label}</span><div class="cfield"><select class="uiv-sel${this.isChanged([c.css]) ? " changed" : ""}" data-css="${c.css}">${opts}</select></div><span></span></div>`;
1645
1750
  }
1646
1751
  const val = toHexInput(this.liveVal(c.css));
1647
- return `<div class="uiv-ctl"><span class="clabel">${c.label}</span><div class="cfield"><input type="color" class="uiv-color" data-css="${c.css}" value="${val}"></div><span></span></div>`;
1752
+ return `<div class="uiv-ctl"><span class="clabel">${c.label}</span><div class="cfield"><input type="color" class="uiv-color${this.isChanged([c.css]) ? " changed" : ""}" data-css="${c.css}" value="${val}"></div><span></span></div>`;
1648
1753
  }
1649
1754
  bindControls() {
1650
1755
  const root = this.root;
package/package.json CHANGED
@@ -1,10 +1,28 @@
1
1
  {
2
2
  "name": "uivisor",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "type": "module",
5
5
  "description": "Dev-only visual UI tweaker that turns mouse edits into a precise, breakpoint-aware prompt for your AI coding agent — without touching your source.",
6
6
  "license": "MIT",
7
- "keywords": ["vite-plugin", "react", "devtools", "ai", "visual-editing", "tailwind"],
7
+ "author": "Kazbek Kabdulov",
8
+ "homepage": "https://github.com/kabdulov/uivisor#readme",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/kabdulov/uivisor.git"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/kabdulov/uivisor/issues"
15
+ },
16
+ "keywords": [
17
+ "vite-plugin",
18
+ "next",
19
+ "turbopack",
20
+ "react",
21
+ "devtools",
22
+ "ai",
23
+ "visual-editing",
24
+ "tailwind"
25
+ ],
8
26
  "exports": {
9
27
  "./vite": {
10
28
  "types": "./dist/vite/index.d.ts",
@@ -22,26 +40,37 @@
22
40
  "default": "./dist/overlay/index.js"
23
41
  },
24
42
  "./next": {
25
- "types": "./dist/next/index.d.cts",
26
- "import": "./dist/next/index.cjs",
43
+ "types": "./dist/next/index.d.ts",
44
+ "import": "./dist/next/index.js",
27
45
  "require": "./dist/next/index.cjs",
28
46
  "default": "./dist/next/index.cjs"
29
47
  },
48
+ "./next/overlay": {
49
+ "types": "./dist/next/overlay.d.ts",
50
+ "import": "./dist/next/overlay.js",
51
+ "default": "./dist/next/overlay.js"
52
+ },
30
53
  "./next/loader": {
31
54
  "require": "./dist/next/loader.cjs",
32
55
  "default": "./dist/next/loader.cjs"
33
- }
56
+ },
57
+ "./package.json": "./package.json"
34
58
  },
35
- "files": ["dist"],
59
+ "files": [
60
+ "dist"
61
+ ],
36
62
  "publishConfig": {
37
63
  "access": "public"
38
64
  },
39
65
  "engines": {
40
66
  "node": ">=18"
41
67
  },
42
- "workspaces": ["demo", "demo-next"],
68
+ "workspaces": [
69
+ "demo",
70
+ "demo-next"
71
+ ],
43
72
  "scripts": {
44
- "build": "tsup",
73
+ "build": "rm -rf dist && tsup",
45
74
  "dev": "tsup --watch",
46
75
  "test": "vitest run",
47
76
  "typecheck": "tsc --noEmit",
@@ -49,10 +78,16 @@
49
78
  "prepublishOnly": "npm run build && vitest run"
50
79
  },
51
80
  "peerDependencies": {
52
- "vite": ">=4"
81
+ "vite": ">=4",
82
+ "react": ">=18"
53
83
  },
54
84
  "peerDependenciesMeta": {
55
- "vite": { "optional": true }
85
+ "vite": {
86
+ "optional": true
87
+ },
88
+ "react": {
89
+ "optional": true
90
+ }
56
91
  },
57
92
  "dependencies": {
58
93
  "@babel/core": "^7.24.0"
@@ -61,7 +96,9 @@
61
96
  "@babel/types": "^7.24.0",
62
97
  "@types/babel__core": "^7.20.5",
63
98
  "@types/node": "^20.11.0",
99
+ "@types/react": "^18.3.0",
64
100
  "jsdom": "^24.0.0",
101
+ "react": "^18.3.1",
65
102
  "tsup": "^8.0.0",
66
103
  "typescript": "^5.4.0",
67
104
  "vitest": "^1.6.0"