@capsuletech/web-inspector 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,12 @@
1
+ import { ICategory, OnChangeFn, ValuesMap } from './types';
2
+ interface ICategoryProps {
3
+ category: ICategory;
4
+ values: ValuesMap;
5
+ onChange: OnChangeFn;
6
+ }
7
+ /**
8
+ * Одна секция Inspector'а. Collapsible header — клик переключает развёрнутый
9
+ * вид. Начальное состояние — `category.defaultCollapsed`.
10
+ */
11
+ export declare const Category: (props: ICategoryProps) => import("solid-js").JSX.Element;
12
+ export {};
@@ -0,0 +1,10 @@
1
+ import { IInspectorProps } from './types';
2
+ /**
3
+ * Универсальный редактор пропсов. Принимает список категорий (например
4
+ * «Основное» / «Расширенное»), у каждой — набор типизированных полей.
5
+ *
6
+ * Inspector сам по себе ничего не «знает» о компонентах редактора —
7
+ * это чистая render-функция от описаний полей и текущих значений.
8
+ * Маппинг манифеста компонента → категорий выполняется в host'е.
9
+ */
10
+ export declare const Inspector: (props: IInspectorProps) => import("solid-js").JSX.Element;
@@ -0,0 +1,12 @@
1
+ import { IBooleanField } from '../types';
2
+ interface IProps {
3
+ field: IBooleanField;
4
+ value: boolean | undefined;
5
+ onChange: (v: boolean) => void;
6
+ }
7
+ /**
8
+ * Boolean — inline-layout: label слева, toggle справа. Так компактнее в
9
+ * списке полей и читабельнее («Отключено: ON»).
10
+ */
11
+ export declare const BooleanField: (props: IProps) => import("solid-js").JSX.Element;
12
+ export {};
@@ -0,0 +1,15 @@
1
+ import { JSX } from 'solid-js';
2
+ interface IFieldShellProps {
3
+ label: string;
4
+ hint?: string;
5
+ /** Если true, label рендерится inline (для toggle с подписью справа). */
6
+ inline?: boolean;
7
+ children: JSX.Element;
8
+ }
9
+ /**
10
+ * Общий обёрточный layout для одного поля: label сверху, content под ним,
11
+ * опциональный hint мелким шрифтом внизу. Все Field-компоненты используют
12
+ * эту обёртку, чтобы вид был согласованным.
13
+ */
14
+ export declare const FieldShell: (props: IFieldShellProps) => JSX.Element;
15
+ export {};
@@ -0,0 +1,8 @@
1
+ import { INumberField } from '../types';
2
+ interface IProps {
3
+ field: INumberField;
4
+ value: number | undefined;
5
+ onChange: (v: number) => void;
6
+ }
7
+ export declare const NumberField: (props: IProps) => import("solid-js").JSX.Element;
8
+ export {};
@@ -0,0 +1,12 @@
1
+ import { INumberUnitField } from '../types';
2
+ interface IProps {
3
+ field: INumberUnitField;
4
+ value: string | undefined;
5
+ onChange: (v: string) => void;
6
+ }
7
+ /**
8
+ * Число + единица измерения в одной строке. При выборе `auto` числовое поле
9
+ * блокируется, и в onChange улетает строка `'auto'`.
10
+ */
11
+ export declare const NumberUnitField: (props: IProps) => import("solid-js").JSX.Element;
12
+ export {};
@@ -0,0 +1,12 @@
1
+ import { ISelectField } from '../types';
2
+ interface IProps {
3
+ field: ISelectField;
4
+ value: string | undefined;
5
+ onChange: (v: string) => void;
6
+ }
7
+ /**
8
+ * Простой native select на v1. Когда в @capsuletech/ui появится полноценный
9
+ * Select (Kobalte popover + Listbox) — заменим, API наружу не изменится.
10
+ */
11
+ export declare const SelectField: (props: IProps) => import("solid-js").JSX.Element;
12
+ export {};
@@ -0,0 +1,8 @@
1
+ import { ITextField } from '../types';
2
+ interface IProps {
3
+ field: ITextField;
4
+ value: string | undefined;
5
+ onChange: (v: string) => void;
6
+ }
7
+ export declare const TextField: (props: IProps) => import("solid-js").JSX.Element;
8
+ export {};
@@ -0,0 +1,8 @@
1
+ import { ITextareaField } from '../types';
2
+ interface IProps {
3
+ field: ITextareaField;
4
+ value: string | undefined;
5
+ onChange: (v: string) => void;
6
+ }
7
+ export declare const TextareaField: (props: IProps) => import("solid-js").JSX.Element;
8
+ export {};
@@ -0,0 +1,14 @@
1
+ import { JSX } from 'solid-js';
2
+ import { IFieldDef, OnChangeFn, ValuesMap } from '../types';
3
+ import { BooleanField } from './BooleanField';
4
+ import { NumberField } from './NumberField';
5
+ import { NumberUnitField } from './NumberUnitField';
6
+ import { SelectField } from './SelectField';
7
+ import { TextField } from './TextField';
8
+ import { TextareaField } from './TextareaField';
9
+ /**
10
+ * Диспатчер по `field.type`. Каждый case передаёт уже-типизированный field
11
+ * и пробрасывает изменения через единый `onChange(key, value)`.
12
+ */
13
+ export declare const renderField: (field: IFieldDef, values: ValuesMap, onChange: OnChangeFn) => JSX.Element;
14
+ export { BooleanField, NumberField, NumberUnitField, SelectField, TextField, TextareaField };
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Парсит CSS-подобную строку с единицей измерения. `'100px'` → `{value: 100, unit: 'px'}`,
3
+ * `'auto'` → `{value: null, unit: 'auto'}`.
4
+ *
5
+ * Если строка пустая/невалидная — возвращается `fallbackUnit` и `value: null`.
6
+ * `auto` обрабатывается как «keyword» — не число, юнит сам по себе.
7
+ */
8
+ export interface IParsedUnit {
9
+ value: number | null;
10
+ unit: string;
11
+ }
12
+ export declare const parseUnit: (raw: unknown, fallbackUnit: string) => IParsedUnit;
13
+ export declare const formatUnit: (value: number | null, unit: string) => string;
@@ -0,0 +1,6 @@
1
+ export { Inspector } from './Inspector';
2
+ export { Category } from './Category';
3
+ export { renderField } from './fields';
4
+ export { parseUnit, formatUnit } from './fields/parse-unit';
5
+ export type { IParsedUnit } from './fields/parse-unit';
6
+ export type { IBooleanField, ICategory, IFieldDef, IInspectorProps, INumberField, INumberUnitField, ISelectField, ITextField, ITextareaField, OnChangeFn, ValuesMap, } from './types';
package/dist/index.mjs ADDED
@@ -0,0 +1,274 @@
1
+ import { insert as d, createComponent as i, template as o, effect as c, classList as C, delegateEvents as h, setAttribute as g, className as _ } from "solid-js/web";
2
+ import { Show as m, createMemo as N, For as x, createSignal as F } from "solid-js";
3
+ import { Toggle as T } from "@capsuletech/ui/toggle";
4
+ var S = /* @__PURE__ */ o('<span class="text-xs opacity-50">'), U = /* @__PURE__ */ o('<div class="flex flex-col gap-1"><div class="flex items-center justify-between gap-2"><span class="text-xs opacity-70">');
5
+ const E = (e) => (() => {
6
+ var l = U(), t = l.firstChild, n = t.firstChild;
7
+ return d(n, () => e.field.label), d(t, i(T, {
8
+ get checked() {
9
+ return !!e.value;
10
+ },
11
+ get onChange() {
12
+ return e.onChange;
13
+ },
14
+ get disabled() {
15
+ return e.field.disabled;
16
+ }
17
+ }), null), d(l, i(m, {
18
+ get when() {
19
+ return e.field.hint;
20
+ },
21
+ get children() {
22
+ var r = S();
23
+ return d(r, () => e.field.hint), r;
24
+ }
25
+ }), null), l;
26
+ })();
27
+ var L = /* @__PURE__ */ o('<span class="text-xs opacity-50">'), k = /* @__PURE__ */ o('<div><span class="text-xs opacity-70"></span><div>');
28
+ const f = (e) => (() => {
29
+ var l = k(), t = l.firstChild, n = t.nextSibling;
30
+ return d(t, () => e.label), d(n, () => e.children), d(l, i(m, {
31
+ get when() {
32
+ return e.hint;
33
+ },
34
+ get children() {
35
+ var r = L();
36
+ return d(r, () => e.hint), r;
37
+ }
38
+ }), null), c((r) => C(l, {
39
+ "flex flex-col gap-1": !e.inline,
40
+ "flex items-center justify-between gap-2": e.inline
41
+ }, r)), l;
42
+ })();
43
+ var j = /* @__PURE__ */ o('<input type=number class="w-full px-2 py-1 bg-white/5 border border-white/15 rounded text-sm outline-none focus:border-blue-400/60 transition-colors disabled:opacity-40">');
44
+ const A = (e) => i(f, {
45
+ get label() {
46
+ return e.field.label;
47
+ },
48
+ get hint() {
49
+ return e.field.hint;
50
+ },
51
+ get children() {
52
+ var l = j();
53
+ return l.$$input = (t) => {
54
+ const n = t.currentTarget.valueAsNumber;
55
+ Number.isNaN(n) || e.onChange(n);
56
+ }, c((t) => {
57
+ var n = e.field.min, r = e.field.max, a = e.field.step, s = e.field.disabled;
58
+ return n !== t.e && g(l, "min", t.e = n), r !== t.t && g(l, "max", t.t = r), a !== t.a && g(l, "step", t.a = a), s !== t.o && (l.disabled = t.o = s), t;
59
+ }, {
60
+ e: void 0,
61
+ t: void 0,
62
+ a: void 0,
63
+ o: void 0
64
+ }), c(() => l.value = e.value ?? ""), l;
65
+ }
66
+ });
67
+ h(["input"]);
68
+ const I = /^\s*(-?\d+\.?\d*)\s*(.*)$/, B = (e, l) => {
69
+ if (e == null || e === "")
70
+ return { value: null, unit: l };
71
+ const t = String(e).trim();
72
+ if (t === "auto") return { value: null, unit: "auto" };
73
+ const n = t.match(I);
74
+ return n ? { value: Number.parseFloat(n[1]), unit: n[2] || l } : { value: null, unit: l };
75
+ }, w = (e, l) => l === "auto" ? "auto" : e === null || Number.isNaN(e) ? "" : `${e}${l}`;
76
+ var M = /* @__PURE__ */ o('<div class="flex gap-1"><input type=number class="flex-1 min-w-0 px-2 py-1 bg-white/5 border border-white/15 rounded text-sm outline-none focus:border-blue-400/60 transition-colors disabled:opacity-40"><select class="px-2 py-1 bg-white/5 border border-white/15 rounded text-sm outline-none focus:border-blue-400/60 transition-colors disabled:opacity-40">'), R = /* @__PURE__ */ o("<option>");
77
+ const z = (e) => {
78
+ const l = () => e.field.defaultUnit ?? e.field.units[0] ?? "px", t = N(() => B(e.value, l())), n = (a) => {
79
+ Number.isNaN(a) || e.onChange(w(a, t().unit));
80
+ }, r = (a) => {
81
+ e.onChange(w(t().value, a));
82
+ };
83
+ return i(f, {
84
+ get label() {
85
+ return e.field.label;
86
+ },
87
+ get hint() {
88
+ return e.field.hint;
89
+ },
90
+ get children() {
91
+ var a = M(), s = a.firstChild, v = s.nextSibling;
92
+ return s.$$input = (u) => n(u.currentTarget.valueAsNumber), v.addEventListener("change", (u) => r(u.currentTarget.value)), d(v, i(x, {
93
+ get each() {
94
+ return e.field.units;
95
+ },
96
+ children: (u) => (() => {
97
+ var b = R();
98
+ return b.value = u, d(b, u), b;
99
+ })()
100
+ })), c((u) => {
101
+ var b = e.field.step, $ = e.field.disabled || t().unit === "auto", y = e.field.disabled;
102
+ return b !== u.e && g(s, "step", u.e = b), $ !== u.t && (s.disabled = u.t = $), y !== u.a && (v.disabled = u.a = y), u;
103
+ }, {
104
+ e: void 0,
105
+ t: void 0,
106
+ a: void 0
107
+ }), c(() => s.value = t().value ?? ""), c(() => v.value = t().unit), a;
108
+ }
109
+ });
110
+ };
111
+ h(["input"]);
112
+ var q = /* @__PURE__ */ o('<select class="w-full px-2 py-1 bg-white/5 border border-white/15 rounded text-sm outline-none focus:border-blue-400/60 transition-colors disabled:opacity-40">'), D = /* @__PURE__ */ o("<option>");
113
+ const G = (e) => i(f, {
114
+ get label() {
115
+ return e.field.label;
116
+ },
117
+ get hint() {
118
+ return e.field.hint;
119
+ },
120
+ get children() {
121
+ var l = q();
122
+ return l.addEventListener("change", (t) => e.onChange(t.currentTarget.value)), d(l, i(x, {
123
+ get each() {
124
+ return e.field.options;
125
+ },
126
+ children: (t) => (() => {
127
+ var n = D();
128
+ return d(n, () => t.label ?? t.value), c(() => n.value = t.value), n;
129
+ })()
130
+ })), c(() => l.disabled = e.field.disabled), c(() => l.value = e.value ?? ""), l;
131
+ }
132
+ });
133
+ var H = /* @__PURE__ */ o('<input type=text class="w-full px-2 py-1 bg-white/5 border border-white/15 rounded text-sm outline-none focus:border-blue-400/60 transition-colors disabled:opacity-40">');
134
+ const J = (e) => i(f, {
135
+ get label() {
136
+ return e.field.label;
137
+ },
138
+ get hint() {
139
+ return e.field.hint;
140
+ },
141
+ get children() {
142
+ var l = H();
143
+ return l.$$input = (t) => e.onChange(t.currentTarget.value), c((t) => {
144
+ var n = !!e.field.mono, r = e.field.placeholder, a = e.field.disabled;
145
+ return n !== t.e && l.classList.toggle("font-mono", t.e = n), r !== t.t && g(l, "placeholder", t.t = r), a !== t.a && (l.disabled = t.a = a), t;
146
+ }, {
147
+ e: void 0,
148
+ t: void 0,
149
+ a: void 0
150
+ }), c(() => l.value = e.value ?? ""), l;
151
+ }
152
+ });
153
+ h(["input"]);
154
+ var K = /* @__PURE__ */ o('<textarea class="w-full px-2 py-1 bg-white/5 border border-white/15 rounded text-sm outline-none focus:border-blue-400/60 transition-colors disabled:opacity-40 resize-y">');
155
+ const O = (e) => i(f, {
156
+ get label() {
157
+ return e.field.label;
158
+ },
159
+ get hint() {
160
+ return e.field.hint;
161
+ },
162
+ get children() {
163
+ var l = K();
164
+ return l.$$input = (t) => e.onChange(t.currentTarget.value), c((t) => {
165
+ var n = e.field.rows ?? 3, r = !!e.field.mono, a = e.field.placeholder, s = e.field.disabled;
166
+ return n !== t.e && g(l, "rows", t.e = n), r !== t.t && l.classList.toggle("font-mono", t.t = r), a !== t.a && g(l, "placeholder", t.a = a), s !== t.o && (l.disabled = t.o = s), t;
167
+ }, {
168
+ e: void 0,
169
+ t: void 0,
170
+ a: void 0,
171
+ o: void 0
172
+ }), c(() => l.value = e.value ?? ""), l;
173
+ }
174
+ });
175
+ h(["input"]);
176
+ const P = (e, l, t) => {
177
+ const n = (a) => t(e.key, a), r = l[e.key];
178
+ switch (e.type) {
179
+ case "text":
180
+ return i(J, {
181
+ field: e,
182
+ value: r,
183
+ onChange: n
184
+ });
185
+ case "textarea":
186
+ return i(O, {
187
+ field: e,
188
+ value: r,
189
+ onChange: n
190
+ });
191
+ case "number":
192
+ return i(A, {
193
+ field: e,
194
+ value: r,
195
+ onChange: n
196
+ });
197
+ case "number-unit":
198
+ return i(z, {
199
+ field: e,
200
+ value: r,
201
+ onChange: n
202
+ });
203
+ case "boolean":
204
+ return i(E, {
205
+ field: e,
206
+ value: r,
207
+ onChange: n
208
+ });
209
+ case "select":
210
+ return i(G, {
211
+ field: e,
212
+ value: r,
213
+ onChange: n
214
+ });
215
+ default:
216
+ return null;
217
+ }
218
+ };
219
+ var Q = /* @__PURE__ */ o('<p class="text-xs opacity-60">'), V = /* @__PURE__ */ o('<div class="px-3 py-3 flex flex-col gap-3 border-t border-white/10">'), W = /* @__PURE__ */ o('<div class="border border-white/15 rounded overflow-hidden"><button type=button class="w-full flex items-center justify-between px-3 py-2 text-sm font-medium hover:bg-white/5 transition-colors text-left"><span></span><span class="opacity-50 text-xs">');
220
+ const X = (e) => {
221
+ const [l, t] = F(!!e.category.defaultCollapsed);
222
+ return (() => {
223
+ var n = W(), r = n.firstChild, a = r.firstChild, s = a.nextSibling;
224
+ return r.$$click = () => t(!l()), d(a, () => e.category.label), d(s, () => l() ? "▸" : "▾"), d(n, i(m, {
225
+ get when() {
226
+ return !l();
227
+ },
228
+ get children() {
229
+ var v = V();
230
+ return d(v, i(m, {
231
+ get when() {
232
+ return e.category.description;
233
+ },
234
+ get children() {
235
+ var u = Q();
236
+ return d(u, () => e.category.description), u;
237
+ }
238
+ }), null), d(v, i(x, {
239
+ get each() {
240
+ return e.category.fields;
241
+ },
242
+ children: (u) => P(u, e.values, e.onChange)
243
+ }), null), v;
244
+ }
245
+ }), null), n;
246
+ })();
247
+ };
248
+ h(["click"]);
249
+ var Y = /* @__PURE__ */ o("<div>");
250
+ const te = (e) => (() => {
251
+ var l = Y();
252
+ return d(l, i(x, {
253
+ get each() {
254
+ return e.categories;
255
+ },
256
+ children: (t) => i(X, {
257
+ category: t,
258
+ get values() {
259
+ return e.values;
260
+ },
261
+ get onChange() {
262
+ return e.onChange;
263
+ }
264
+ })
265
+ })), c(() => _(l, `flex flex-col gap-3 w-full ${e.class ?? ""}`)), l;
266
+ })();
267
+ export {
268
+ X as Category,
269
+ te as Inspector,
270
+ w as formatUnit,
271
+ B as parseUnit,
272
+ P as renderField
273
+ };
274
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","sources":["../src/fields/BooleanField.tsx","../src/fields/FieldShell.tsx","../src/fields/NumberField.tsx","../src/fields/parse-unit.ts","../src/fields/NumberUnitField.tsx","../src/fields/SelectField.tsx","../src/fields/TextField.tsx","../src/fields/TextareaField.tsx","../src/fields/index.tsx","../src/Category.tsx","../src/Inspector.tsx"],"sourcesContent":["import { Toggle } from '@capsuletech/ui/toggle';\nimport { Show } from 'solid-js';\nimport type { IBooleanField } from '../types';\n\ninterface IProps {\n field: IBooleanField;\n value: boolean | undefined;\n onChange: (v: boolean) => void;\n}\n\n/**\n * Boolean — inline-layout: label слева, toggle справа. Так компактнее в\n * списке полей и читабельнее («Отключено: ON»).\n */\nexport const BooleanField = (props: IProps) => (\n <div class=\"flex flex-col gap-1\">\n <div class=\"flex items-center justify-between gap-2\">\n <span class=\"text-xs opacity-70\">{props.field.label}</span>\n <Toggle checked={!!props.value} onChange={props.onChange} disabled={props.field.disabled} />\n </div>\n <Show when={props.field.hint}>\n <span class=\"text-xs opacity-50\">{props.field.hint}</span>\n </Show>\n </div>\n);\n","import type { JSX } from 'solid-js';\nimport { Show } from 'solid-js';\n\ninterface IFieldShellProps {\n label: string;\n hint?: string;\n /** Если true, label рендерится inline (для toggle с подписью справа). */\n inline?: boolean;\n children: JSX.Element;\n}\n\n/**\n * Общий обёрточный layout для одного поля: label сверху, content под ним,\n * опциональный hint мелким шрифтом внизу. Все Field-компоненты используют\n * эту обёртку, чтобы вид был согласованным.\n */\nexport const FieldShell = (props: IFieldShellProps) => (\n <div\n classList={{\n 'flex flex-col gap-1': !props.inline,\n 'flex items-center justify-between gap-2': props.inline,\n }}\n >\n <span class=\"text-xs opacity-70\">{props.label}</span>\n <div>{props.children}</div>\n <Show when={props.hint}>\n <span class=\"text-xs opacity-50\">{props.hint}</span>\n </Show>\n </div>\n);\n","import type { INumberField } from '../types';\nimport { FieldShell } from './FieldShell';\n\ninterface IProps {\n field: INumberField;\n value: number | undefined;\n onChange: (v: number) => void;\n}\n\nexport const NumberField = (props: IProps) => (\n <FieldShell label={props.field.label} hint={props.field.hint}>\n <input\n type=\"number\"\n class=\"w-full px-2 py-1 bg-white/5 border border-white/15 rounded text-sm outline-none focus:border-blue-400/60 transition-colors disabled:opacity-40\"\n value={props.value ?? ''}\n min={props.field.min}\n max={props.field.max}\n step={props.field.step}\n disabled={props.field.disabled}\n onInput={(e) => {\n const v = e.currentTarget.valueAsNumber;\n if (!Number.isNaN(v)) props.onChange(v);\n }}\n />\n </FieldShell>\n);\n","/**\n * Парсит CSS-подобную строку с единицей измерения. `'100px'` → `{value: 100, unit: 'px'}`,\n * `'auto'` → `{value: null, unit: 'auto'}`.\n *\n * Если строка пустая/невалидная — возвращается `fallbackUnit` и `value: null`.\n * `auto` обрабатывается как «keyword» — не число, юнит сам по себе.\n */\nexport interface IParsedUnit {\n value: number | null;\n unit: string;\n}\n\nconst NUMBER_UNIT_RE = /^\\s*(-?\\d+\\.?\\d*)\\s*(.*)$/;\n\nexport const parseUnit = (raw: unknown, fallbackUnit: string): IParsedUnit => {\n if (raw === null || raw === undefined || raw === '') {\n return { value: null, unit: fallbackUnit };\n }\n const s = String(raw).trim();\n if (s === 'auto') return { value: null, unit: 'auto' };\n const m = s.match(NUMBER_UNIT_RE);\n if (!m) return { value: null, unit: fallbackUnit };\n return { value: Number.parseFloat(m[1]), unit: m[2] || fallbackUnit };\n};\n\nexport const formatUnit = (value: number | null, unit: string): string => {\n if (unit === 'auto') return 'auto';\n if (value === null || Number.isNaN(value)) return '';\n return `${value}${unit}`;\n};\n","import { For, createMemo } from 'solid-js';\nimport type { INumberUnitField } from '../types';\nimport { FieldShell } from './FieldShell';\nimport { formatUnit, parseUnit } from './parse-unit';\n\ninterface IProps {\n field: INumberUnitField;\n value: string | undefined;\n onChange: (v: string) => void;\n}\n\n/**\n * Число + единица измерения в одной строке. При выборе `auto` числовое поле\n * блокируется, и в onChange улетает строка `'auto'`.\n */\nexport const NumberUnitField = (props: IProps) => {\n const fallbackUnit = () => props.field.defaultUnit ?? props.field.units[0] ?? 'px';\n const parsed = createMemo(() => parseUnit(props.value, fallbackUnit()));\n\n const onNumberInput = (raw: number) => {\n if (Number.isNaN(raw)) return;\n props.onChange(formatUnit(raw, parsed().unit));\n };\n const onUnitChange = (nextUnit: string) => {\n props.onChange(formatUnit(parsed().value, nextUnit));\n };\n\n return (\n <FieldShell label={props.field.label} hint={props.field.hint}>\n <div class=\"flex gap-1\">\n <input\n type=\"number\"\n step={props.field.step}\n class=\"flex-1 min-w-0 px-2 py-1 bg-white/5 border border-white/15 rounded text-sm outline-none focus:border-blue-400/60 transition-colors disabled:opacity-40\"\n value={parsed().value ?? ''}\n disabled={props.field.disabled || parsed().unit === 'auto'}\n onInput={(e) => onNumberInput(e.currentTarget.valueAsNumber)}\n />\n <select\n class=\"px-2 py-1 bg-white/5 border border-white/15 rounded text-sm outline-none focus:border-blue-400/60 transition-colors disabled:opacity-40\"\n value={parsed().unit}\n disabled={props.field.disabled}\n onChange={(e) => onUnitChange(e.currentTarget.value)}\n >\n <For each={props.field.units}>{(u) => <option value={u}>{u}</option>}</For>\n </select>\n </div>\n </FieldShell>\n );\n};\n","import { For } from 'solid-js';\nimport type { ISelectField } from '../types';\nimport { FieldShell } from './FieldShell';\n\ninterface IProps {\n field: ISelectField;\n value: string | undefined;\n onChange: (v: string) => void;\n}\n\n/**\n * Простой native select на v1. Когда в @capsuletech/ui появится полноценный\n * Select (Kobalte popover + Listbox) — заменим, API наружу не изменится.\n */\nexport const SelectField = (props: IProps) => (\n <FieldShell label={props.field.label} hint={props.field.hint}>\n <select\n class=\"w-full px-2 py-1 bg-white/5 border border-white/15 rounded text-sm outline-none focus:border-blue-400/60 transition-colors disabled:opacity-40\"\n value={props.value ?? ''}\n disabled={props.field.disabled}\n onChange={(e) => props.onChange(e.currentTarget.value)}\n >\n <For each={props.field.options}>\n {(opt) => <option value={opt.value}>{opt.label ?? opt.value}</option>}\n </For>\n </select>\n </FieldShell>\n);\n","import type { ITextField } from '../types';\nimport { FieldShell } from './FieldShell';\n\ninterface IProps {\n field: ITextField;\n value: string | undefined;\n onChange: (v: string) => void;\n}\n\nexport const TextField = (props: IProps) => (\n <FieldShell label={props.field.label} hint={props.field.hint}>\n <input\n type=\"text\"\n class=\"w-full px-2 py-1 bg-white/5 border border-white/15 rounded text-sm outline-none focus:border-blue-400/60 transition-colors disabled:opacity-40\"\n classList={{ 'font-mono': props.field.mono }}\n value={props.value ?? ''}\n placeholder={props.field.placeholder}\n disabled={props.field.disabled}\n onInput={(e) => props.onChange(e.currentTarget.value)}\n />\n </FieldShell>\n);\n","import type { ITextareaField } from '../types';\nimport { FieldShell } from './FieldShell';\n\ninterface IProps {\n field: ITextareaField;\n value: string | undefined;\n onChange: (v: string) => void;\n}\n\nexport const TextareaField = (props: IProps) => (\n <FieldShell label={props.field.label} hint={props.field.hint}>\n <textarea\n rows={props.field.rows ?? 3}\n class=\"w-full px-2 py-1 bg-white/5 border border-white/15 rounded text-sm outline-none focus:border-blue-400/60 transition-colors disabled:opacity-40 resize-y\"\n classList={{ 'font-mono': props.field.mono }}\n value={props.value ?? ''}\n placeholder={props.field.placeholder}\n disabled={props.field.disabled}\n onInput={(e) => props.onChange(e.currentTarget.value)}\n />\n </FieldShell>\n);\n","import type { JSX } from 'solid-js';\nimport type { IFieldDef, OnChangeFn, ValuesMap } from '../types';\nimport { BooleanField } from './BooleanField';\nimport { NumberField } from './NumberField';\nimport { NumberUnitField } from './NumberUnitField';\nimport { SelectField } from './SelectField';\nimport { TextField } from './TextField';\nimport { TextareaField } from './TextareaField';\n\n/**\n * Диспатчер по `field.type`. Каждый case передаёт уже-типизированный field\n * и пробрасывает изменения через единый `onChange(key, value)`.\n */\nexport const renderField = (\n field: IFieldDef,\n values: ValuesMap,\n onChange: OnChangeFn,\n): JSX.Element => {\n const emit = (v: unknown) => onChange(field.key, v);\n const raw = values[field.key];\n switch (field.type) {\n case 'text':\n return <TextField field={field} value={raw as string | undefined} onChange={emit} />;\n case 'textarea':\n return <TextareaField field={field} value={raw as string | undefined} onChange={emit} />;\n case 'number':\n return <NumberField field={field} value={raw as number | undefined} onChange={emit} />;\n case 'number-unit':\n return <NumberUnitField field={field} value={raw as string | undefined} onChange={emit} />;\n case 'boolean':\n return <BooleanField field={field} value={raw as boolean | undefined} onChange={emit} />;\n case 'select':\n return <SelectField field={field} value={raw as string | undefined} onChange={emit} />;\n default:\n // exhaustive — TS подсветит если добавили новый тип и забыли тут\n return null;\n }\n};\n\nexport { BooleanField, NumberField, NumberUnitField, SelectField, TextField, TextareaField };\n","import { For, Show, createSignal } from 'solid-js';\nimport { renderField } from './fields';\nimport type { ICategory, OnChangeFn, ValuesMap } from './types';\n\ninterface ICategoryProps {\n category: ICategory;\n values: ValuesMap;\n onChange: OnChangeFn;\n}\n\n/**\n * Одна секция Inspector'а. Collapsible header — клик переключает развёрнутый\n * вид. Начальное состояние — `category.defaultCollapsed`.\n */\nexport const Category = (props: ICategoryProps) => {\n const [collapsed, setCollapsed] = createSignal(!!props.category.defaultCollapsed);\n\n return (\n <div class=\"border border-white/15 rounded overflow-hidden\">\n <button\n type=\"button\"\n class=\"w-full flex items-center justify-between px-3 py-2 text-sm font-medium hover:bg-white/5 transition-colors text-left\"\n onClick={() => setCollapsed(!collapsed())}\n >\n <span>{props.category.label}</span>\n <span class=\"opacity-50 text-xs\">{collapsed() ? '▸' : '▾'}</span>\n </button>\n <Show when={!collapsed()}>\n <div class=\"px-3 py-3 flex flex-col gap-3 border-t border-white/10\">\n <Show when={props.category.description}>\n <p class=\"text-xs opacity-60\">{props.category.description}</p>\n </Show>\n <For each={props.category.fields}>\n {(field) => renderField(field, props.values, props.onChange)}\n </For>\n </div>\n </Show>\n </div>\n );\n};\n","import { For } from 'solid-js';\nimport { Category } from './Category';\nimport type { IInspectorProps } from './types';\n\n/**\n * Универсальный редактор пропсов. Принимает список категорий (например\n * «Основное» / «Расширенное»), у каждой — набор типизированных полей.\n *\n * Inspector сам по себе ничего не «знает» о компонентах редактора —\n * это чистая render-функция от описаний полей и текущих значений.\n * Маппинг манифеста компонента → категорий выполняется в host'е.\n */\nexport const Inspector = (props: IInspectorProps) => (\n <div class={`flex flex-col gap-3 w-full ${props.class ?? ''}`}>\n <For each={props.categories}>\n {(cat) => <Category category={cat} values={props.values} onChange={props.onChange} />}\n </For>\n </div>\n);\n"],"names":["BooleanField","props","_el$","_tmpl$2","_el$2","firstChild","_el$3","_$insert","field","label","_$createComponent","Toggle","checked","value","onChange","disabled","Show","when","hint","children","_el$4","_tmpl$","FieldShell","nextSibling","_$effect","_$p","_$classList","inline","NumberField","$$input","e","v","currentTarget","valueAsNumber","Number","isNaN","_p$","_v$","min","_v$2","max","_v$3","step","_v$4","_$setAttribute","t","a","o","undefined","_$delegateEvents","NUMBER_UNIT_RE","parseUnit","raw","fallbackUnit","s","m","formatUnit","unit","NumberUnitField","defaultUnit","units","parsed","createMemo","onNumberInput","onUnitChange","nextUnit","addEventListener","For","each","u","SelectField","options","opt","TextField","mono","placeholder","classList","toggle","TextareaField","rows","renderField","values","emit","key","type","Category","collapsed","setCollapsed","createSignal","category","defaultCollapsed","_tmpl$3","$$click","_el$5","description","_el$6","fields","Inspector","categories","cat","_$className","class"],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "@capsuletech/web-inspector",
3
+ "version": "0.0.17",
4
+ "type": "module",
5
+ "main": "./index.mjs",
6
+ "types": "./index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./index.d.ts",
10
+ "import": "./index.mjs",
11
+ "default": "./index.mjs"
12
+ }
13
+ },
14
+ "dependencies": {},
15
+ "peerDependencies": {
16
+ "solid-js": "^1.9.0"
17
+ }
18
+ }
@@ -0,0 +1,73 @@
1
+ export type ValuesMap = Record<string, unknown>;
2
+ export type OnChangeFn = (key: string, value: unknown) => void;
3
+ interface IBaseField {
4
+ /** Ключ в `values` map'е. Уникальный в пределах Inspector'а. */
5
+ key: string;
6
+ /** Подпись над полем. Пиши по-человечески — Inspector могут пользовать не-разработчики. */
7
+ label: string;
8
+ /** Длинное пояснение под полем (для не-очевидных пропсов). */
9
+ hint?: string;
10
+ disabled?: boolean;
11
+ }
12
+ export interface ITextField extends IBaseField {
13
+ type: 'text';
14
+ placeholder?: string;
15
+ /** Моноширинный шрифт — для class, id, ARIA и подобного «технического». */
16
+ mono?: boolean;
17
+ }
18
+ export interface ITextareaField extends IBaseField {
19
+ type: 'textarea';
20
+ rows?: number;
21
+ placeholder?: string;
22
+ mono?: boolean;
23
+ }
24
+ export interface INumberField extends IBaseField {
25
+ type: 'number';
26
+ min?: number;
27
+ max?: number;
28
+ step?: number;
29
+ }
30
+ /**
31
+ * Численное значение с единицей измерения (`'100px'`, `'50%'`, `'auto'`).
32
+ * Если выбран unit `'auto'` — input блокируется (нет смысла редактировать
33
+ * числовую часть).
34
+ */
35
+ export interface INumberUnitField extends IBaseField {
36
+ type: 'number-unit';
37
+ units: string[];
38
+ /** По умолчанию первый из `units`. */
39
+ defaultUnit?: string;
40
+ step?: number;
41
+ }
42
+ export interface IBooleanField extends IBaseField {
43
+ type: 'boolean';
44
+ }
45
+ export interface ISelectField extends IBaseField {
46
+ type: 'select';
47
+ options: {
48
+ value: string;
49
+ label?: string;
50
+ }[];
51
+ }
52
+ /** Дискриминированный union — `field.type` сужает остальные поля. */
53
+ export type IFieldDef = ITextField | ITextareaField | INumberField | INumberUnitField | IBooleanField | ISelectField;
54
+ export interface ICategory {
55
+ id: string;
56
+ /** Заголовок секции — «Основное», «Расширенное» и т.д. */
57
+ label: string;
58
+ /** Описание секции под заголовком (опционально). */
59
+ description?: string;
60
+ fields: IFieldDef[];
61
+ /** Сворачивать секцию по умолчанию. Удобно для «advanced» блока. */
62
+ defaultCollapsed?: boolean;
63
+ }
64
+ export interface IInspectorProps {
65
+ categories: ICategory[];
66
+ /** Текущее значение каждого `field.key`. */
67
+ values: ValuesMap;
68
+ /** Колбэк на одно изменение поля — потребитель сам мерджит в values. */
69
+ onChange: OnChangeFn;
70
+ /** Доп. класс на корневой div. */
71
+ class?: string;
72
+ }
73
+ export {};
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@capsuletech/web-inspector",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "./dist/index.mjs",
6
+ "types": "./dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.mjs",
11
+ "default": "./dist/index.mjs"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "src",
17
+ "!**/*.tsbuildinfo"
18
+ ],
19
+ "dependencies": {},
20
+ "peerDependencies": {
21
+ "solid-js": "^1.9.0"
22
+ },
23
+ "devDependencies": {
24
+ "@capsuletech/shared-vite": "0.1.0"
25
+ },
26
+ "scripts": {
27
+ "build": "vite build"
28
+ }
29
+ }
@@ -0,0 +1,40 @@
1
+ import { For, Show, createSignal } from 'solid-js';
2
+ import { renderField } from './fields';
3
+ import type { ICategory, OnChangeFn, ValuesMap } from './types';
4
+
5
+ interface ICategoryProps {
6
+ category: ICategory;
7
+ values: ValuesMap;
8
+ onChange: OnChangeFn;
9
+ }
10
+
11
+ /**
12
+ * Одна секция Inspector'а. Collapsible header — клик переключает развёрнутый
13
+ * вид. Начальное состояние — `category.defaultCollapsed`.
14
+ */
15
+ export const Category = (props: ICategoryProps) => {
16
+ const [collapsed, setCollapsed] = createSignal(!!props.category.defaultCollapsed);
17
+
18
+ return (
19
+ <div class="border border-white/15 rounded overflow-hidden">
20
+ <button
21
+ type="button"
22
+ class="w-full flex items-center justify-between px-3 py-2 text-sm font-medium hover:bg-white/5 transition-colors text-left"
23
+ onClick={() => setCollapsed(!collapsed())}
24
+ >
25
+ <span>{props.category.label}</span>
26
+ <span class="opacity-50 text-xs">{collapsed() ? '▸' : '▾'}</span>
27
+ </button>
28
+ <Show when={!collapsed()}>
29
+ <div class="px-3 py-3 flex flex-col gap-3 border-t border-white/10">
30
+ <Show when={props.category.description}>
31
+ <p class="text-xs opacity-60">{props.category.description}</p>
32
+ </Show>
33
+ <For each={props.category.fields}>
34
+ {(field) => renderField(field, props.values, props.onChange)}
35
+ </For>
36
+ </div>
37
+ </Show>
38
+ </div>
39
+ );
40
+ };
@@ -0,0 +1,19 @@
1
+ import { For } from 'solid-js';
2
+ import { Category } from './Category';
3
+ import type { IInspectorProps } from './types';
4
+
5
+ /**
6
+ * Универсальный редактор пропсов. Принимает список категорий (например
7
+ * «Основное» / «Расширенное»), у каждой — набор типизированных полей.
8
+ *
9
+ * Inspector сам по себе ничего не «знает» о компонентах редактора —
10
+ * это чистая render-функция от описаний полей и текущих значений.
11
+ * Маппинг манифеста компонента → категорий выполняется в host'е.
12
+ */
13
+ export const Inspector = (props: IInspectorProps) => (
14
+ <div class={`flex flex-col gap-3 w-full ${props.class ?? ''}`}>
15
+ <For each={props.categories}>
16
+ {(cat) => <Category category={cat} values={props.values} onChange={props.onChange} />}
17
+ </For>
18
+ </div>
19
+ );
@@ -0,0 +1,25 @@
1
+ import { Toggle } from '@capsuletech/ui/toggle';
2
+ import { Show } from 'solid-js';
3
+ import type { IBooleanField } from '../types';
4
+
5
+ interface IProps {
6
+ field: IBooleanField;
7
+ value: boolean | undefined;
8
+ onChange: (v: boolean) => void;
9
+ }
10
+
11
+ /**
12
+ * Boolean — inline-layout: label слева, toggle справа. Так компактнее в
13
+ * списке полей и читабельнее («Отключено: ON»).
14
+ */
15
+ export const BooleanField = (props: IProps) => (
16
+ <div class="flex flex-col gap-1">
17
+ <div class="flex items-center justify-between gap-2">
18
+ <span class="text-xs opacity-70">{props.field.label}</span>
19
+ <Toggle checked={!!props.value} onChange={props.onChange} disabled={props.field.disabled} />
20
+ </div>
21
+ <Show when={props.field.hint}>
22
+ <span class="text-xs opacity-50">{props.field.hint}</span>
23
+ </Show>
24
+ </div>
25
+ );
@@ -0,0 +1,30 @@
1
+ import type { JSX } from 'solid-js';
2
+ import { Show } from 'solid-js';
3
+
4
+ interface IFieldShellProps {
5
+ label: string;
6
+ hint?: string;
7
+ /** Если true, label рендерится inline (для toggle с подписью справа). */
8
+ inline?: boolean;
9
+ children: JSX.Element;
10
+ }
11
+
12
+ /**
13
+ * Общий обёрточный layout для одного поля: label сверху, content под ним,
14
+ * опциональный hint мелким шрифтом внизу. Все Field-компоненты используют
15
+ * эту обёртку, чтобы вид был согласованным.
16
+ */
17
+ export const FieldShell = (props: IFieldShellProps) => (
18
+ <div
19
+ classList={{
20
+ 'flex flex-col gap-1': !props.inline,
21
+ 'flex items-center justify-between gap-2': props.inline,
22
+ }}
23
+ >
24
+ <span class="text-xs opacity-70">{props.label}</span>
25
+ <div>{props.children}</div>
26
+ <Show when={props.hint}>
27
+ <span class="text-xs opacity-50">{props.hint}</span>
28
+ </Show>
29
+ </div>
30
+ );
@@ -0,0 +1,26 @@
1
+ import type { INumberField } from '../types';
2
+ import { FieldShell } from './FieldShell';
3
+
4
+ interface IProps {
5
+ field: INumberField;
6
+ value: number | undefined;
7
+ onChange: (v: number) => void;
8
+ }
9
+
10
+ export const NumberField = (props: IProps) => (
11
+ <FieldShell label={props.field.label} hint={props.field.hint}>
12
+ <input
13
+ type="number"
14
+ class="w-full px-2 py-1 bg-white/5 border border-white/15 rounded text-sm outline-none focus:border-blue-400/60 transition-colors disabled:opacity-40"
15
+ value={props.value ?? ''}
16
+ min={props.field.min}
17
+ max={props.field.max}
18
+ step={props.field.step}
19
+ disabled={props.field.disabled}
20
+ onInput={(e) => {
21
+ const v = e.currentTarget.valueAsNumber;
22
+ if (!Number.isNaN(v)) props.onChange(v);
23
+ }}
24
+ />
25
+ </FieldShell>
26
+ );
@@ -0,0 +1,50 @@
1
+ import { For, createMemo } from 'solid-js';
2
+ import type { INumberUnitField } from '../types';
3
+ import { FieldShell } from './FieldShell';
4
+ import { formatUnit, parseUnit } from './parse-unit';
5
+
6
+ interface IProps {
7
+ field: INumberUnitField;
8
+ value: string | undefined;
9
+ onChange: (v: string) => void;
10
+ }
11
+
12
+ /**
13
+ * Число + единица измерения в одной строке. При выборе `auto` числовое поле
14
+ * блокируется, и в onChange улетает строка `'auto'`.
15
+ */
16
+ export const NumberUnitField = (props: IProps) => {
17
+ const fallbackUnit = () => props.field.defaultUnit ?? props.field.units[0] ?? 'px';
18
+ const parsed = createMemo(() => parseUnit(props.value, fallbackUnit()));
19
+
20
+ const onNumberInput = (raw: number) => {
21
+ if (Number.isNaN(raw)) return;
22
+ props.onChange(formatUnit(raw, parsed().unit));
23
+ };
24
+ const onUnitChange = (nextUnit: string) => {
25
+ props.onChange(formatUnit(parsed().value, nextUnit));
26
+ };
27
+
28
+ return (
29
+ <FieldShell label={props.field.label} hint={props.field.hint}>
30
+ <div class="flex gap-1">
31
+ <input
32
+ type="number"
33
+ step={props.field.step}
34
+ class="flex-1 min-w-0 px-2 py-1 bg-white/5 border border-white/15 rounded text-sm outline-none focus:border-blue-400/60 transition-colors disabled:opacity-40"
35
+ value={parsed().value ?? ''}
36
+ disabled={props.field.disabled || parsed().unit === 'auto'}
37
+ onInput={(e) => onNumberInput(e.currentTarget.valueAsNumber)}
38
+ />
39
+ <select
40
+ class="px-2 py-1 bg-white/5 border border-white/15 rounded text-sm outline-none focus:border-blue-400/60 transition-colors disabled:opacity-40"
41
+ value={parsed().unit}
42
+ disabled={props.field.disabled}
43
+ onChange={(e) => onUnitChange(e.currentTarget.value)}
44
+ >
45
+ <For each={props.field.units}>{(u) => <option value={u}>{u}</option>}</For>
46
+ </select>
47
+ </div>
48
+ </FieldShell>
49
+ );
50
+ };
@@ -0,0 +1,28 @@
1
+ import { For } from 'solid-js';
2
+ import type { ISelectField } from '../types';
3
+ import { FieldShell } from './FieldShell';
4
+
5
+ interface IProps {
6
+ field: ISelectField;
7
+ value: string | undefined;
8
+ onChange: (v: string) => void;
9
+ }
10
+
11
+ /**
12
+ * Простой native select на v1. Когда в @capsuletech/ui появится полноценный
13
+ * Select (Kobalte popover + Listbox) — заменим, API наружу не изменится.
14
+ */
15
+ export const SelectField = (props: IProps) => (
16
+ <FieldShell label={props.field.label} hint={props.field.hint}>
17
+ <select
18
+ class="w-full px-2 py-1 bg-white/5 border border-white/15 rounded text-sm outline-none focus:border-blue-400/60 transition-colors disabled:opacity-40"
19
+ value={props.value ?? ''}
20
+ disabled={props.field.disabled}
21
+ onChange={(e) => props.onChange(e.currentTarget.value)}
22
+ >
23
+ <For each={props.field.options}>
24
+ {(opt) => <option value={opt.value}>{opt.label ?? opt.value}</option>}
25
+ </For>
26
+ </select>
27
+ </FieldShell>
28
+ );
@@ -0,0 +1,22 @@
1
+ import type { ITextField } from '../types';
2
+ import { FieldShell } from './FieldShell';
3
+
4
+ interface IProps {
5
+ field: ITextField;
6
+ value: string | undefined;
7
+ onChange: (v: string) => void;
8
+ }
9
+
10
+ export const TextField = (props: IProps) => (
11
+ <FieldShell label={props.field.label} hint={props.field.hint}>
12
+ <input
13
+ type="text"
14
+ class="w-full px-2 py-1 bg-white/5 border border-white/15 rounded text-sm outline-none focus:border-blue-400/60 transition-colors disabled:opacity-40"
15
+ classList={{ 'font-mono': props.field.mono }}
16
+ value={props.value ?? ''}
17
+ placeholder={props.field.placeholder}
18
+ disabled={props.field.disabled}
19
+ onInput={(e) => props.onChange(e.currentTarget.value)}
20
+ />
21
+ </FieldShell>
22
+ );
@@ -0,0 +1,22 @@
1
+ import type { ITextareaField } from '../types';
2
+ import { FieldShell } from './FieldShell';
3
+
4
+ interface IProps {
5
+ field: ITextareaField;
6
+ value: string | undefined;
7
+ onChange: (v: string) => void;
8
+ }
9
+
10
+ export const TextareaField = (props: IProps) => (
11
+ <FieldShell label={props.field.label} hint={props.field.hint}>
12
+ <textarea
13
+ rows={props.field.rows ?? 3}
14
+ class="w-full px-2 py-1 bg-white/5 border border-white/15 rounded text-sm outline-none focus:border-blue-400/60 transition-colors disabled:opacity-40 resize-y"
15
+ classList={{ 'font-mono': props.field.mono }}
16
+ value={props.value ?? ''}
17
+ placeholder={props.field.placeholder}
18
+ disabled={props.field.disabled}
19
+ onInput={(e) => props.onChange(e.currentTarget.value)}
20
+ />
21
+ </FieldShell>
22
+ );
@@ -0,0 +1,40 @@
1
+ import type { JSX } from 'solid-js';
2
+ import type { IFieldDef, OnChangeFn, ValuesMap } from '../types';
3
+ import { BooleanField } from './BooleanField';
4
+ import { NumberField } from './NumberField';
5
+ import { NumberUnitField } from './NumberUnitField';
6
+ import { SelectField } from './SelectField';
7
+ import { TextField } from './TextField';
8
+ import { TextareaField } from './TextareaField';
9
+
10
+ /**
11
+ * Диспатчер по `field.type`. Каждый case передаёт уже-типизированный field
12
+ * и пробрасывает изменения через единый `onChange(key, value)`.
13
+ */
14
+ export const renderField = (
15
+ field: IFieldDef,
16
+ values: ValuesMap,
17
+ onChange: OnChangeFn,
18
+ ): JSX.Element => {
19
+ const emit = (v: unknown) => onChange(field.key, v);
20
+ const raw = values[field.key];
21
+ switch (field.type) {
22
+ case 'text':
23
+ return <TextField field={field} value={raw as string | undefined} onChange={emit} />;
24
+ case 'textarea':
25
+ return <TextareaField field={field} value={raw as string | undefined} onChange={emit} />;
26
+ case 'number':
27
+ return <NumberField field={field} value={raw as number | undefined} onChange={emit} />;
28
+ case 'number-unit':
29
+ return <NumberUnitField field={field} value={raw as string | undefined} onChange={emit} />;
30
+ case 'boolean':
31
+ return <BooleanField field={field} value={raw as boolean | undefined} onChange={emit} />;
32
+ case 'select':
33
+ return <SelectField field={field} value={raw as string | undefined} onChange={emit} />;
34
+ default:
35
+ // exhaustive — TS подсветит если добавили новый тип и забыли тут
36
+ return null;
37
+ }
38
+ };
39
+
40
+ export { BooleanField, NumberField, NumberUnitField, SelectField, TextField, TextareaField };
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Парсит CSS-подобную строку с единицей измерения. `'100px'` → `{value: 100, unit: 'px'}`,
3
+ * `'auto'` → `{value: null, unit: 'auto'}`.
4
+ *
5
+ * Если строка пустая/невалидная — возвращается `fallbackUnit` и `value: null`.
6
+ * `auto` обрабатывается как «keyword» — не число, юнит сам по себе.
7
+ */
8
+ export interface IParsedUnit {
9
+ value: number | null;
10
+ unit: string;
11
+ }
12
+
13
+ const NUMBER_UNIT_RE = /^\s*(-?\d+\.?\d*)\s*(.*)$/;
14
+
15
+ export const parseUnit = (raw: unknown, fallbackUnit: string): IParsedUnit => {
16
+ if (raw === null || raw === undefined || raw === '') {
17
+ return { value: null, unit: fallbackUnit };
18
+ }
19
+ const s = String(raw).trim();
20
+ if (s === 'auto') return { value: null, unit: 'auto' };
21
+ const m = s.match(NUMBER_UNIT_RE);
22
+ if (!m) return { value: null, unit: fallbackUnit };
23
+ return { value: Number.parseFloat(m[1]), unit: m[2] || fallbackUnit };
24
+ };
25
+
26
+ export const formatUnit = (value: number | null, unit: string): string => {
27
+ if (unit === 'auto') return 'auto';
28
+ if (value === null || Number.isNaN(value)) return '';
29
+ return `${value}${unit}`;
30
+ };
package/src/index.ts ADDED
@@ -0,0 +1,18 @@
1
+ export { Inspector } from './Inspector';
2
+ export { Category } from './Category';
3
+ export { renderField } from './fields';
4
+ export { parseUnit, formatUnit } from './fields/parse-unit';
5
+ export type { IParsedUnit } from './fields/parse-unit';
6
+ export type {
7
+ IBooleanField,
8
+ ICategory,
9
+ IFieldDef,
10
+ IInspectorProps,
11
+ INumberField,
12
+ INumberUnitField,
13
+ ISelectField,
14
+ ITextField,
15
+ ITextareaField,
16
+ OnChangeFn,
17
+ ValuesMap,
18
+ } from './types';
package/src/types.ts ADDED
@@ -0,0 +1,85 @@
1
+ export type ValuesMap = Record<string, unknown>;
2
+ export type OnChangeFn = (key: string, value: unknown) => void;
3
+
4
+ interface IBaseField {
5
+ /** Ключ в `values` map'е. Уникальный в пределах Inspector'а. */
6
+ key: string;
7
+ /** Подпись над полем. Пиши по-человечески — Inspector могут пользовать не-разработчики. */
8
+ label: string;
9
+ /** Длинное пояснение под полем (для не-очевидных пропсов). */
10
+ hint?: string;
11
+ disabled?: boolean;
12
+ }
13
+
14
+ export interface ITextField extends IBaseField {
15
+ type: 'text';
16
+ placeholder?: string;
17
+ /** Моноширинный шрифт — для class, id, ARIA и подобного «технического». */
18
+ mono?: boolean;
19
+ }
20
+
21
+ export interface ITextareaField extends IBaseField {
22
+ type: 'textarea';
23
+ rows?: number;
24
+ placeholder?: string;
25
+ mono?: boolean;
26
+ }
27
+
28
+ export interface INumberField extends IBaseField {
29
+ type: 'number';
30
+ min?: number;
31
+ max?: number;
32
+ step?: number;
33
+ }
34
+
35
+ /**
36
+ * Численное значение с единицей измерения (`'100px'`, `'50%'`, `'auto'`).
37
+ * Если выбран unit `'auto'` — input блокируется (нет смысла редактировать
38
+ * числовую часть).
39
+ */
40
+ export interface INumberUnitField extends IBaseField {
41
+ type: 'number-unit';
42
+ units: string[];
43
+ /** По умолчанию первый из `units`. */
44
+ defaultUnit?: string;
45
+ step?: number;
46
+ }
47
+
48
+ export interface IBooleanField extends IBaseField {
49
+ type: 'boolean';
50
+ }
51
+
52
+ export interface ISelectField extends IBaseField {
53
+ type: 'select';
54
+ options: { value: string; label?: string }[];
55
+ }
56
+
57
+ /** Дискриминированный union — `field.type` сужает остальные поля. */
58
+ export type IFieldDef =
59
+ | ITextField
60
+ | ITextareaField
61
+ | INumberField
62
+ | INumberUnitField
63
+ | IBooleanField
64
+ | ISelectField;
65
+
66
+ export interface ICategory {
67
+ id: string;
68
+ /** Заголовок секции — «Основное», «Расширенное» и т.д. */
69
+ label: string;
70
+ /** Описание секции под заголовком (опционально). */
71
+ description?: string;
72
+ fields: IFieldDef[];
73
+ /** Сворачивать секцию по умолчанию. Удобно для «advanced» блока. */
74
+ defaultCollapsed?: boolean;
75
+ }
76
+
77
+ export interface IInspectorProps {
78
+ categories: ICategory[];
79
+ /** Текущее значение каждого `field.key`. */
80
+ values: ValuesMap;
81
+ /** Колбэк на одно изменение поля — потребитель сам мерджит в values. */
82
+ onChange: OnChangeFn;
83
+ /** Доп. класс на корневой div. */
84
+ class?: string;
85
+ }