@larose-ui/devtools 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 laRose contributors
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.
@@ -0,0 +1,46 @@
1
+ import * as react from 'react';
2
+ import { ReactNode } from 'react';
3
+ import { LaRoseRuntimeContext } from '@larose-ui/core';
4
+ import { UIEvent } from '@larose-ui/observability';
5
+
6
+ interface DevToolsPanelProps {
7
+ defaultOpen?: boolean;
8
+ }
9
+ declare function DevToolsPanel({ defaultOpen }: DevToolsPanelProps): react.JSX.Element | null;
10
+ declare function DevToolsProvider({ children }: {
11
+ children: ReactNode;
12
+ }): react.JSX.Element;
13
+
14
+ interface ComponentPerformanceSummary {
15
+ renderCount: number;
16
+ lastRenderMs: number | null;
17
+ avgRenderMs: number | null;
18
+ threshold: string | null;
19
+ }
20
+ declare function getComponentPerformance(events: UIEvent[]): ComponentPerformanceSummary;
21
+
22
+ interface ReactComponentInfo {
23
+ displayName: string;
24
+ props: Record<string, string>;
25
+ }
26
+ declare function resolveReactComponentInfo(domNode: Element): ReactComponentInfo | null;
27
+
28
+ interface InspectedElement {
29
+ name: string;
30
+ source: 'observed' | 'observed-form' | 'provider' | 'component';
31
+ dataset: Record<string, string>;
32
+ tagName: string;
33
+ domNode?: Element;
34
+ react?: ReactComponentInfo | null;
35
+ }
36
+ declare function useComponentInspector(active: boolean): {
37
+ hovered: InspectedElement | null;
38
+ selected: InspectedElement | null;
39
+ clearSelection: () => void;
40
+ };
41
+ declare function InspectorOverlay({ target, }: {
42
+ target: InspectedElement | null;
43
+ }): react.JSX.Element | null;
44
+ declare function buildInspectorReadout(element: InspectedElement, runtime: LaRoseRuntimeContext | null, performance?: ComponentPerformanceSummary | null): string[];
45
+
46
+ export { type ComponentPerformanceSummary, DevToolsPanel, type DevToolsPanelProps, DevToolsProvider, type InspectedElement, InspectorOverlay, type ReactComponentInfo, buildInspectorReadout, getComponentPerformance, resolveReactComponentInfo, useComponentInspector };
package/dist/index.js ADDED
@@ -0,0 +1,568 @@
1
+ // src/DevToolsPanel.tsx
2
+ import { useEffect as useEffect2, useMemo, useState as useState2 } from "react";
3
+ import {
4
+ useOptionalRuntime,
5
+ useOptionalRuntimeEvents,
6
+ useTheme,
7
+ useNetwork,
8
+ useBreakpoint,
9
+ useI18n,
10
+ useEnvironment
11
+ } from "@larose-ui/runtime";
12
+ import { usePermissions } from "@larose-ui/permissions";
13
+ import { useOptionalObservability } from "@larose-ui/observability";
14
+
15
+ // src/ComponentInspector.tsx
16
+ import { useCallback, useEffect, useState } from "react";
17
+
18
+ // src/reactFiber.ts
19
+ var PROP_SKIP = /* @__PURE__ */ new Set(["children", "ref", "key", "dangerouslySetInnerHTML"]);
20
+ var MAX_PROP_LEN = 80;
21
+ function getFiberKey(node) {
22
+ return Object.keys(node).find(
23
+ (key) => key.startsWith("__reactFiber$") || key.startsWith("__reactInternalInstance$")
24
+ );
25
+ }
26
+ function getFiberFromDOM(node) {
27
+ const key = getFiberKey(node);
28
+ if (!key) return null;
29
+ return node[key] ?? null;
30
+ }
31
+ function getComponentName(type) {
32
+ if (!type || typeof type === "string") return null;
33
+ if (typeof type === "function") {
34
+ const fn = type;
35
+ return fn.displayName || fn.name || null;
36
+ }
37
+ if (typeof type === "object") {
38
+ const obj = type;
39
+ if (obj.render) {
40
+ return obj.render.displayName || obj.render.name || null;
41
+ }
42
+ if (obj.displayName) return obj.displayName;
43
+ }
44
+ return null;
45
+ }
46
+ function findNearestComponentFiber(fiber) {
47
+ let current = fiber;
48
+ while (current) {
49
+ const name = getComponentName(current.type);
50
+ if (name) return current;
51
+ current = current.return ?? null;
52
+ }
53
+ return null;
54
+ }
55
+ function sanitizeFiberProps(props) {
56
+ if (!props) return {};
57
+ const out = {};
58
+ for (const [key, value] of Object.entries(props)) {
59
+ if (PROP_SKIP.has(key) || key.startsWith("__")) continue;
60
+ out[key] = formatPropValue(value);
61
+ }
62
+ return out;
63
+ }
64
+ function formatPropValue(value) {
65
+ if (value === null) return "null";
66
+ if (value === void 0) return "undefined";
67
+ if (typeof value === "string") return truncate(value);
68
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
69
+ if (typeof value === "function") return "[Function]";
70
+ try {
71
+ return truncate(JSON.stringify(value));
72
+ } catch {
73
+ return "[Object]";
74
+ }
75
+ }
76
+ function truncate(value) {
77
+ if (value.length <= MAX_PROP_LEN) return value;
78
+ return `${value.slice(0, MAX_PROP_LEN - 3)}...`;
79
+ }
80
+ function resolveReactComponentInfo(domNode) {
81
+ const hostFiber = getFiberFromDOM(domNode);
82
+ if (!hostFiber) return null;
83
+ const componentFiber = findNearestComponentFiber(hostFiber);
84
+ if (!componentFiber) return null;
85
+ const displayName = getComponentName(componentFiber.type);
86
+ if (!displayName) return null;
87
+ return {
88
+ displayName,
89
+ props: sanitizeFiberProps(componentFiber.memoizedProps)
90
+ };
91
+ }
92
+
93
+ // src/ComponentInspector.tsx
94
+ import { jsx } from "react/jsx-runtime";
95
+ var INSPECTABLE_SELECTOR = "[data-lr-observed], [data-lr-observed-form], [data-lr-provider], [data-lr-component]";
96
+ function useComponentInspector(active) {
97
+ const [hovered, setHovered] = useState(null);
98
+ const [selected, setSelected] = useState(null);
99
+ const resolveElement = useCallback((target) => {
100
+ const el = target.closest(INSPECTABLE_SELECTOR);
101
+ if (!el) return null;
102
+ const observed = el.getAttribute("data-lr-observed");
103
+ const observedForm = el.getAttribute("data-lr-observed-form");
104
+ const component = el.getAttribute("data-lr-component");
105
+ const name = observed ?? observedForm ?? component ?? (el.hasAttribute("data-lr-provider") ? "LaRoseProvider" : "unknown");
106
+ const source = observed ? "observed" : observedForm ? "observed-form" : el.hasAttribute("data-lr-provider") ? "provider" : "component";
107
+ const dataset = {};
108
+ for (const attr of el.attributes) {
109
+ if (attr.name.startsWith("data-lr-")) {
110
+ dataset[attr.name] = attr.value;
111
+ }
112
+ }
113
+ return {
114
+ name,
115
+ source,
116
+ dataset,
117
+ tagName: el.tagName.toLowerCase(),
118
+ domNode: el,
119
+ react: resolveReactComponentInfo(el)
120
+ };
121
+ }, []);
122
+ useEffect(() => {
123
+ if (!active) {
124
+ setHovered(null);
125
+ return;
126
+ }
127
+ const onMove = (event) => {
128
+ const target = event.target;
129
+ if (target.closest("[data-lr-devtools]")) {
130
+ setHovered(null);
131
+ return;
132
+ }
133
+ const info = resolveElement(target);
134
+ setHovered(info);
135
+ };
136
+ const onClick = (event) => {
137
+ const target = event.target;
138
+ if (target.closest("[data-lr-devtools]")) return;
139
+ const info = resolveElement(target);
140
+ if (info) {
141
+ event.preventDefault();
142
+ event.stopPropagation();
143
+ setSelected(info);
144
+ }
145
+ };
146
+ document.addEventListener("mousemove", onMove, true);
147
+ document.addEventListener("click", onClick, true);
148
+ return () => {
149
+ document.removeEventListener("mousemove", onMove, true);
150
+ document.removeEventListener("click", onClick, true);
151
+ };
152
+ }, [active, resolveElement]);
153
+ return { hovered, selected, clearSelection: () => setSelected(null) };
154
+ }
155
+ function InspectorOverlay({
156
+ target
157
+ }) {
158
+ if (!target || typeof document === "undefined") return null;
159
+ const el = target.domNode ?? document.querySelector(
160
+ `[data-lr-observed="${target.name}"], [data-lr-observed-form="${target.name}"], [data-lr-component="${target.name}"], [data-lr-provider]`
161
+ );
162
+ if (!el || !(el instanceof HTMLElement)) return null;
163
+ const rect = el.getBoundingClientRect();
164
+ const style = {
165
+ position: "fixed",
166
+ top: rect.top - 2,
167
+ left: rect.left - 2,
168
+ width: rect.width + 4,
169
+ height: rect.height + 4,
170
+ border: "2px solid var(--lr-color-primary, #2563eb)",
171
+ borderRadius: 4,
172
+ pointerEvents: "none",
173
+ zIndex: 9998,
174
+ boxShadow: "0 0 0 1px rgba(37, 99, 235, 0.3)"
175
+ };
176
+ return /* @__PURE__ */ jsx("div", { "data-lr-inspector-overlay": true, style });
177
+ }
178
+ function buildInspectorReadout(element, runtime, performance) {
179
+ const lines = [
180
+ `${element.name}`,
181
+ `\u251C\u2500\u2500 Source: ${element.source}`,
182
+ `\u251C\u2500\u2500 Tag: ${element.tagName}`
183
+ ];
184
+ if (element.react) {
185
+ lines.push(`\u251C\u2500\u2500 React: ${element.react.displayName}`);
186
+ const propEntries = Object.entries(element.react.props);
187
+ if (propEntries.length > 0) {
188
+ lines.push("\u251C\u2500\u2500 Props:");
189
+ for (const [key, value] of propEntries.slice(0, 12)) {
190
+ lines.push(`\u2502 ${key}=${value}`);
191
+ }
192
+ if (propEntries.length > 12) {
193
+ lines.push(`\u2502 \u2026 +${propEntries.length - 12} more`);
194
+ }
195
+ }
196
+ }
197
+ if (performance && performance.renderCount > 0) {
198
+ lines.push(`\u251C\u2500\u2500 Renders: ${performance.renderCount}`);
199
+ if (performance.lastRenderMs !== null) {
200
+ lines.push(
201
+ `\u251C\u2500\u2500 Last render: ${performance.lastRenderMs.toFixed(1)}ms (${performance.threshold ?? "unknown"})`
202
+ );
203
+ }
204
+ if (performance.avgRenderMs !== null) {
205
+ lines.push(`\u251C\u2500\u2500 Avg render: ${performance.avgRenderMs.toFixed(1)}ms`);
206
+ }
207
+ }
208
+ if (runtime) {
209
+ lines.push(`\u251C\u2500\u2500 Session: ${runtime.session}`);
210
+ lines.push(`\u251C\u2500\u2500 Environment: ${runtime.environment}`);
211
+ lines.push(`\u251C\u2500\u2500 Network: ${runtime.network.condition}${runtime.network.rtt ? ` (${runtime.network.rtt}ms)` : ""}`);
212
+ lines.push(`\u251C\u2500\u2500 Tenant: ${runtime.tenant?.id ?? "none"}`);
213
+ lines.push(`\u251C\u2500\u2500 Locale: ${runtime.locale.locale} (${runtime.locale.dir})`);
214
+ lines.push(`\u251C\u2500\u2500 Theme: ${runtime.theme.mode} / ${runtime.theme.density}`);
215
+ lines.push(`\u251C\u2500\u2500 Permissions: ${runtime.permissions.granted.length} granted`);
216
+ lines.push(`\u251C\u2500\u2500 Features: ${Object.keys(runtime.features.flags).length} active`);
217
+ lines.push(`\u2514\u2500\u2500 API version: ${runtime.version.api ?? "unknown"}`);
218
+ }
219
+ const dataAttrs = Object.entries(element.dataset);
220
+ if (dataAttrs.length > 0) {
221
+ lines.push("Dataset:");
222
+ for (const [key, value] of dataAttrs) {
223
+ lines.push(` ${key}=${value}`);
224
+ }
225
+ }
226
+ return lines;
227
+ }
228
+
229
+ // src/componentPerformance.ts
230
+ function getComponentPerformance(events) {
231
+ const perfEvents = events.filter((event) => event.type === "performance");
232
+ if (perfEvents.length === 0) {
233
+ return {
234
+ renderCount: 0,
235
+ lastRenderMs: null,
236
+ avgRenderMs: null,
237
+ threshold: null
238
+ };
239
+ }
240
+ const renderTimes = perfEvents.map((event) => event.metadata?.renderTimeMs).filter((value) => typeof value === "number");
241
+ const last = perfEvents.at(-1);
242
+ if (!last) {
243
+ return {
244
+ renderCount: 0,
245
+ lastRenderMs: null,
246
+ avgRenderMs: null,
247
+ threshold: null
248
+ };
249
+ }
250
+ const lastRenderMs = typeof last.metadata?.renderTimeMs === "number" ? last.metadata.renderTimeMs : null;
251
+ const threshold = typeof last.metadata?.threshold === "string" ? last.metadata.threshold : null;
252
+ const avgRenderMs = renderTimes.length > 0 ? renderTimes.reduce((sum, ms) => sum + ms, 0) / renderTimes.length : null;
253
+ return {
254
+ renderCount: perfEvents.length,
255
+ lastRenderMs,
256
+ avgRenderMs,
257
+ threshold
258
+ };
259
+ }
260
+
261
+ // src/DevToolsPanel.tsx
262
+ import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
263
+ function DevToolsPanel({ defaultOpen = false }) {
264
+ const [open, setOpen] = useState2(defaultOpen);
265
+ const [tab, setTab] = useState2("context");
266
+ const [inspectMode, setInspectMode] = useState2(false);
267
+ const runtime = useOptionalRuntime();
268
+ const runtimeEvents = useOptionalRuntimeEvents();
269
+ const [timeline, setTimeline] = useState2([]);
270
+ const { hovered, selected, clearSelection } = useComponentInspector(inspectMode);
271
+ const theme = useTheme();
272
+ const { locale, dir } = useI18n();
273
+ const network = useNetwork();
274
+ const { breakpoint, width } = useBreakpoint();
275
+ const environment = useEnvironment();
276
+ const { permissions } = usePermissions();
277
+ const observability = useOptionalObservability();
278
+ const journey = observability?.getJourney(20) ?? [];
279
+ const rageAnalyses = observability?.getRageClickAnalyses() ?? [];
280
+ const performanceSummary = useMemo(() => {
281
+ if (!selected || !observability) return null;
282
+ const events = observability.collector.getEvents({ component: selected.name });
283
+ return getComponentPerformance(events);
284
+ }, [selected, observability]);
285
+ useEffect2(() => {
286
+ if (!runtimeEvents) return;
287
+ setTimeline(runtimeEvents.getTimeline(25));
288
+ return runtimeEvents.subscribe(() => setTimeline(runtimeEvents.getTimeline(25)));
289
+ }, [runtimeEvents]);
290
+ if (process.env.NODE_ENV === "production") return null;
291
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
292
+ /* @__PURE__ */ jsx2(InspectorOverlay, { target: inspectMode ? hovered ?? selected : null }),
293
+ /* @__PURE__ */ jsx2(
294
+ "button",
295
+ {
296
+ type: "button",
297
+ onClick: () => setOpen((v) => !v),
298
+ "aria-label": "Toggle laRose DevTools",
299
+ style: toggleStyle,
300
+ children: "laRose"
301
+ }
302
+ ),
303
+ open && /* @__PURE__ */ jsxs("aside", { "data-lr-devtools": true, style: panelStyle, children: [
304
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 4, marginBottom: 8 }, children: [
305
+ /* @__PURE__ */ jsx2(TabButton, { active: tab === "context", onClick: () => {
306
+ setTab("context");
307
+ setInspectMode(false);
308
+ }, children: "Context" }),
309
+ /* @__PURE__ */ jsxs(TabButton, { active: tab === "timeline", onClick: () => {
310
+ setTab("timeline");
311
+ setInspectMode(false);
312
+ }, children: [
313
+ "Timeline (",
314
+ timeline.length,
315
+ ")"
316
+ ] }),
317
+ /* @__PURE__ */ jsx2(
318
+ TabButton,
319
+ {
320
+ active: tab === "inspector",
321
+ onClick: () => setTab("inspector"),
322
+ children: "Inspector"
323
+ }
324
+ ),
325
+ /* @__PURE__ */ jsxs(TabButton, { active: tab === "journey", onClick: () => {
326
+ setTab("journey");
327
+ setInspectMode(false);
328
+ }, children: [
329
+ "Journey (",
330
+ journey.length,
331
+ ")"
332
+ ] })
333
+ ] }),
334
+ tab === "inspector" && /* @__PURE__ */ jsxs("div", { children: [
335
+ /* @__PURE__ */ jsxs("label", { style: { display: "flex", alignItems: "center", gap: 6, marginBottom: 8 }, children: [
336
+ /* @__PURE__ */ jsx2(
337
+ "input",
338
+ {
339
+ type: "checkbox",
340
+ checked: inspectMode,
341
+ onChange: (e) => setInspectMode(e.target.checked)
342
+ }
343
+ ),
344
+ "Select mode"
345
+ ] }),
346
+ !selected && /* @__PURE__ */ jsx2("p", { style: { color: "var(--lr-color-text-muted, #666)", margin: "0 0 8px" }, children: "Click a laRose component on the page to inspect it." }),
347
+ selected && buildInspectorReadout(selected, runtime, performanceSummary).map((line) => /* @__PURE__ */ jsx2("div", { style: { lineHeight: 1.5 }, children: line }, line)),
348
+ selected && /* @__PURE__ */ jsx2(
349
+ "button",
350
+ {
351
+ type: "button",
352
+ onClick: clearSelection,
353
+ style: { marginTop: 8, fontSize: 11, cursor: "pointer" },
354
+ children: "Clear selection"
355
+ }
356
+ )
357
+ ] }),
358
+ tab === "context" && runtime && /* @__PURE__ */ jsxs(Fragment, { children: [
359
+ /* @__PURE__ */ jsx2(Section, { title: "Session", children: runtime.session }),
360
+ /* @__PURE__ */ jsx2(Section, { title: "Tenant", children: runtime.tenant?.name ?? runtime.tenant?.id ?? "none" }),
361
+ /* @__PURE__ */ jsx2(Section, { title: "User", children: runtime.user?.name ?? runtime.user?.id ?? "none" }),
362
+ /* @__PURE__ */ jsx2(Section, { title: "Environment", children: runtime.environment }),
363
+ /* @__PURE__ */ jsxs(Section, { title: "Theme", children: [
364
+ runtime.theme.mode,
365
+ " / ",
366
+ runtime.theme.density,
367
+ runtime.theme.tenantId ? ` \xB7 ${runtime.theme.tenantId}` : ""
368
+ ] }),
369
+ /* @__PURE__ */ jsxs(Section, { title: "Locale", children: [
370
+ runtime.locale.locale,
371
+ " (",
372
+ runtime.locale.dir,
373
+ ") \xB7 ",
374
+ runtime.timezone
375
+ ] }),
376
+ /* @__PURE__ */ jsxs(Section, { title: "Network", children: [
377
+ runtime.network.condition,
378
+ " \xB7 online=",
379
+ String(runtime.network.online),
380
+ runtime.network.rtt !== void 0 ? ` \xB7 ${runtime.network.rtt}ms` : ""
381
+ ] }),
382
+ /* @__PURE__ */ jsxs(Section, { title: "Offline", children: [
383
+ runtime.offline.status,
384
+ " \xB7 queue=",
385
+ runtime.offline.queueLength
386
+ ] }),
387
+ /* @__PURE__ */ jsx2(Section, { title: "Permissions", children: runtime.permissions.granted.length ? runtime.permissions.granted.join(", ") : "none" }),
388
+ /* @__PURE__ */ jsx2(Section, { title: "Features", children: Object.keys(runtime.features.flags).length ? Object.entries(runtime.features.flags).map(([k, v]) => `${k}:${v.enabled ? "on" : "off"}`).join(", ") : "none" }),
389
+ /* @__PURE__ */ jsxs(Section, { title: "Version", children: [
390
+ "fe=",
391
+ runtime.version.frontend,
392
+ runtime.version.api ? ` \xB7 api=${runtime.version.api}` : ""
393
+ ] }),
394
+ /* @__PURE__ */ jsxs(Section, { title: "A11y", children: [
395
+ "reducedMotion=",
396
+ String(runtime.accessibility.reducedMotion),
397
+ " \xB7 highContrast=",
398
+ String(runtime.accessibility.highContrast)
399
+ ] })
400
+ ] }),
401
+ tab === "context" && !runtime && /* @__PURE__ */ jsxs(Fragment, { children: [
402
+ /* @__PURE__ */ jsxs(Section, { title: "Theme", children: [
403
+ "mode=",
404
+ theme.theme,
405
+ " density=",
406
+ theme.density,
407
+ theme.tenantId ? ` tenant=${theme.tenantId}` : ""
408
+ ] }),
409
+ /* @__PURE__ */ jsxs(Section, { title: "Locale", children: [
410
+ locale,
411
+ " (",
412
+ dir,
413
+ ")"
414
+ ] }),
415
+ /* @__PURE__ */ jsx2(Section, { title: "Environment", children: environment }),
416
+ /* @__PURE__ */ jsxs(Section, { title: "Network", children: [
417
+ network.condition,
418
+ " (online=",
419
+ String(network.online),
420
+ ")"
421
+ ] }),
422
+ /* @__PURE__ */ jsxs(Section, { title: "Responsive", children: [
423
+ breakpoint,
424
+ " (",
425
+ width,
426
+ "px)"
427
+ ] }),
428
+ /* @__PURE__ */ jsx2(Section, { title: "Permissions", children: permissions.length ? permissions.join(", ") : "none" })
429
+ ] }),
430
+ tab === "timeline" && /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 4 }, children: [
431
+ timeline.length === 0 && /* @__PURE__ */ jsx2("span", { style: { color: "var(--lr-color-text-muted, #666)" }, children: "No events yet" }),
432
+ timeline.slice().reverse().map((event, index) => /* @__PURE__ */ jsxs("div", { style: { lineHeight: 1.4 }, children: [
433
+ /* @__PURE__ */ jsx2("span", { style: { color: "var(--lr-color-text-muted, #888)" }, children: formatTime(event.timestamp) }),
434
+ " ",
435
+ event.type,
436
+ event.metadata && Object.keys(event.metadata).length > 0 && /* @__PURE__ */ jsxs("span", { style: { color: "var(--lr-color-text-muted, #666)" }, children: [
437
+ " ",
438
+ JSON.stringify(event.metadata)
439
+ ] })
440
+ ] }, `${event.timestamp}-${index}`))
441
+ ] }),
442
+ tab === "journey" && /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 6 }, children: [
443
+ !observability && /* @__PURE__ */ jsx2("span", { style: { color: "var(--lr-color-text-muted, #666)" }, children: "ObservabilityProvider required" }),
444
+ observability && journey.length === 0 && /* @__PURE__ */ jsx2("span", { style: { color: "var(--lr-color-text-muted, #666)" }, children: "No journey steps yet" }),
445
+ journey.slice().reverse().map((step) => /* @__PURE__ */ jsxs("div", { style: { lineHeight: 1.4 }, children: [
446
+ /* @__PURE__ */ jsx2("span", { style: { color: "var(--lr-color-text-muted, #888)" }, children: formatTime(step.timestamp) }),
447
+ " ",
448
+ "[",
449
+ step.kind,
450
+ "] ",
451
+ step.label,
452
+ step.context?.network && /* @__PURE__ */ jsxs("span", { style: { color: "var(--lr-color-text-muted, #666)" }, children: [
453
+ " ",
454
+ "\xB7 net=",
455
+ step.context.network
456
+ ] })
457
+ ] }, step.id)),
458
+ rageAnalyses.length > 0 && /* @__PURE__ */ jsxs("div", { style: { marginTop: 8 }, children: [
459
+ /* @__PURE__ */ jsx2("strong", { children: "Rage click analysis" }),
460
+ rageAnalyses.slice().reverse().slice(0, 3).map((analysis) => /* @__PURE__ */ jsxs("div", { style: { marginTop: 4 }, children: [
461
+ analysis.component,
462
+ " (",
463
+ analysis.clickCount,
464
+ "x)",
465
+ analysis.likelyCauses.map((cause) => /* @__PURE__ */ jsxs(
466
+ "div",
467
+ {
468
+ style: { color: "var(--lr-color-text-muted, #666)", paddingLeft: 8 },
469
+ children: [
470
+ "\u2192 ",
471
+ cause.type,
472
+ ": ",
473
+ cause.label,
474
+ " (",
475
+ cause.confidence,
476
+ ")"
477
+ ]
478
+ },
479
+ `${cause.type}-${cause.label}`
480
+ ))
481
+ ] }, `${analysis.component}-${analysis.timestamp}`))
482
+ ] })
483
+ ] }),
484
+ observability && tab === "context" && /* @__PURE__ */ jsxs(Section, { title: "Observability", children: [
485
+ observability.collector.getEvents().length,
486
+ " UX events tracked"
487
+ ] })
488
+ ] })
489
+ ] });
490
+ }
491
+ function formatTime(timestamp) {
492
+ return new Date(timestamp).toLocaleTimeString(void 0, { hour12: false });
493
+ }
494
+ function TabButton({
495
+ active,
496
+ onClick,
497
+ children
498
+ }) {
499
+ return /* @__PURE__ */ jsx2(
500
+ "button",
501
+ {
502
+ type: "button",
503
+ onClick,
504
+ style: {
505
+ flex: 1,
506
+ padding: "4px 6px",
507
+ fontSize: 11,
508
+ fontWeight: 600,
509
+ border: "1px solid var(--lr-color-border, #ccc)",
510
+ borderRadius: 4,
511
+ background: active ? "var(--lr-color-primary, #2563eb)" : "var(--lr-color-surface, #fff)",
512
+ color: active ? "var(--lr-color-text-inverse, #fff)" : "inherit",
513
+ cursor: "pointer"
514
+ },
515
+ children
516
+ }
517
+ );
518
+ }
519
+ function Section({ title, children }) {
520
+ return /* @__PURE__ */ jsxs("div", { style: { marginBottom: 8 }, children: [
521
+ /* @__PURE__ */ jsx2("strong", { style: { display: "block", marginBottom: 2 }, children: title }),
522
+ /* @__PURE__ */ jsx2("div", { style: { color: "var(--lr-color-text-muted, #666)" }, children })
523
+ ] });
524
+ }
525
+ var toggleStyle = {
526
+ position: "fixed",
527
+ bottom: 16,
528
+ left: 16,
529
+ zIndex: 9999,
530
+ padding: "8px 12px",
531
+ borderRadius: 8,
532
+ border: "1px solid var(--lr-color-border, #ccc)",
533
+ background: "var(--lr-color-surface-elevated, #fff)",
534
+ cursor: "pointer",
535
+ fontSize: 12,
536
+ fontWeight: 600
537
+ };
538
+ var panelStyle = {
539
+ position: "fixed",
540
+ bottom: 56,
541
+ left: 16,
542
+ zIndex: 9999,
543
+ width: 360,
544
+ maxHeight: "60vh",
545
+ overflow: "auto",
546
+ padding: 12,
547
+ borderRadius: 8,
548
+ border: "1px solid var(--lr-color-border, #ccc)",
549
+ background: "var(--lr-color-surface-elevated, #fff)",
550
+ fontSize: 12,
551
+ fontFamily: "monospace",
552
+ boxShadow: "0 4px 12px rgba(0,0,0,0.15)"
553
+ };
554
+ function DevToolsProvider({ children }) {
555
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
556
+ children,
557
+ /* @__PURE__ */ jsx2(DevToolsPanel, {})
558
+ ] });
559
+ }
560
+ export {
561
+ DevToolsPanel,
562
+ DevToolsProvider,
563
+ InspectorOverlay,
564
+ buildInspectorReadout,
565
+ getComponentPerformance,
566
+ resolveReactComponentInfo,
567
+ useComponentInspector
568
+ };
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@larose-ui/devtools",
3
+ "version": "0.1.0",
4
+ "description": "In-app DevTools inspector for laRose UI platform",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "dependencies": {
19
+ "@larose-ui/core": "0.1.0",
20
+ "@larose-ui/observability": "0.1.0",
21
+ "@larose-ui/runtime": "0.1.0"
22
+ },
23
+ "peerDependencies": {
24
+ "react": ">=18",
25
+ "@larose-ui/permissions": "0.1.0"
26
+ },
27
+ "devDependencies": {
28
+ "@testing-library/jest-dom": "^6.6.3",
29
+ "@testing-library/react": "^16.1.0",
30
+ "@testing-library/user-event": "^14.5.2",
31
+ "@vitejs/plugin-react": "^4.3.4",
32
+ "jsdom": "^25.0.1",
33
+ "react": "^19.0.0",
34
+ "react-dom": "^19.0.0",
35
+ "tsup": "^8.3.5",
36
+ "typescript": "^5.7.2",
37
+ "vitest": "^2.1.8",
38
+ "@larose-ui/permissions": "0.1.0",
39
+ "@larose-ui/testing": "0.1.0"
40
+ },
41
+ "license": "MIT",
42
+ "publishConfig": {
43
+ "access": "public"
44
+ },
45
+ "repository": {
46
+ "type": "git",
47
+ "url": "https://github.com/larose-ui/larose.git",
48
+ "directory": "packages/devtools"
49
+ },
50
+ "keywords": [
51
+ "larose",
52
+ "react",
53
+ "ui-platform",
54
+ "design-system",
55
+ "saas"
56
+ ],
57
+ "scripts": {
58
+ "build": "tsup",
59
+ "test": "vitest run",
60
+ "typecheck": "tsc --noEmit",
61
+ "clean": "rm -rf dist"
62
+ }
63
+ }