@ekkolyth/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.
@@ -0,0 +1,4 @@
1
+ //#region src/shell/devtools.d.ts
2
+ declare function EkkoDevtools(): import("react").JSX.Element;
3
+ //#endregion
4
+ export { EkkoDevtools };
package/dist/index.js ADDED
@@ -0,0 +1,879 @@
1
+ import { Button, Input, Label, RadioGroup, RadioGroupItem } from "@ekkolyth/ui";
2
+ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
3
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
4
+ import { useThemeContext } from "@ekkolyth/ui/themes/provider";
5
+ //#region src/inspector/pick-lock.ts
6
+ let pickerArmed = false;
7
+ function isPickerArmed() {
8
+ return pickerArmed;
9
+ }
10
+ function setPickerArmed(value) {
11
+ pickerArmed = value;
12
+ }
13
+ //#endregion
14
+ //#region src/inspector/source-attr.ts
15
+ const SOURCE_ATTRIBUTE = "data-ekko-source";
16
+ function parseSourceAttribute(value) {
17
+ const lastColon = value.lastIndexOf(":");
18
+ if (lastColon === -1) return null;
19
+ const secondColon = value.lastIndexOf(":", lastColon - 1);
20
+ if (secondColon === -1) return null;
21
+ const columnPart = value.slice(lastColon + 1);
22
+ const linePart = value.slice(secondColon + 1, lastColon);
23
+ if (columnPart === "" || linePart === "") return null;
24
+ const column = Number(columnPart);
25
+ const line = Number(linePart);
26
+ const file = value.slice(0, secondColon);
27
+ if (!Number.isFinite(column) || !Number.isFinite(line)) return null;
28
+ if (file === "") return null;
29
+ return {
30
+ column,
31
+ file,
32
+ line
33
+ };
34
+ }
35
+ function findSourceElement(start) {
36
+ const value = (start?.closest(`[data-ekko-source]`) ?? null)?.getAttribute(SOURCE_ATTRIBUTE);
37
+ return value === null || value === void 0 ? null : parseSourceAttribute(value);
38
+ }
39
+ function launchEditorUrl(location) {
40
+ return `/__nextjs_launch-editor?file=${encodeURIComponent(location.file)}&line1=${location.line}&column1=${location.column}`;
41
+ }
42
+ //#endregion
43
+ //#region src/inspector/overlay.tsx
44
+ function isChord(event) {
45
+ return event.metaKey && event.shiftKey;
46
+ }
47
+ function InspectorOverlay() {
48
+ const [armed, setArmed] = useState(false);
49
+ const [hover, setHover] = useState(null);
50
+ const [failed, setFailed] = useState(false);
51
+ useEffect(() => {
52
+ const disarm = () => {
53
+ setArmed(false);
54
+ setHover(null);
55
+ setFailed(false);
56
+ };
57
+ const onKeyDown = (event) => {
58
+ if (isChord(event)) setArmed(true);
59
+ if (event.key === "Escape") disarm();
60
+ };
61
+ const onKeyUp = (event) => {
62
+ if (!isChord(event)) disarm();
63
+ };
64
+ window.addEventListener("keydown", onKeyDown);
65
+ window.addEventListener("keyup", onKeyUp);
66
+ window.addEventListener("blur", disarm);
67
+ return () => {
68
+ window.removeEventListener("keydown", onKeyDown);
69
+ window.removeEventListener("keyup", onKeyUp);
70
+ window.removeEventListener("blur", disarm);
71
+ };
72
+ }, []);
73
+ useEffect(() => {
74
+ if (!armed) return;
75
+ const openInEditor = (location) => {
76
+ Promise.resolve(fetch(launchEditorUrl(location))).then((response) => setFailed(!response.ok)).catch(() => setFailed(true));
77
+ };
78
+ const onMouseMove = (event) => {
79
+ if (!isChord(event)) {
80
+ setArmed(false);
81
+ setHover(null);
82
+ setFailed(false);
83
+ return;
84
+ }
85
+ const target = event.target;
86
+ if (!(target instanceof Element)) return;
87
+ const box = target.getBoundingClientRect();
88
+ setHover({
89
+ location: findSourceElement(target),
90
+ rect: {
91
+ height: box.height,
92
+ left: box.left,
93
+ top: box.top,
94
+ width: box.width
95
+ }
96
+ });
97
+ setFailed(false);
98
+ };
99
+ const onClick = (event) => {
100
+ if (isPickerArmed()) return;
101
+ if (!isChord(event)) {
102
+ setArmed(false);
103
+ setHover(null);
104
+ setFailed(false);
105
+ return;
106
+ }
107
+ const target = event.target;
108
+ if (!(target instanceof Element)) return;
109
+ const location = findSourceElement(target);
110
+ if (location === null) return;
111
+ event.preventDefault();
112
+ event.stopPropagation();
113
+ openInEditor(location);
114
+ };
115
+ document.addEventListener("mousemove", onMouseMove, true);
116
+ document.addEventListener("click", onClick, true);
117
+ return () => {
118
+ document.removeEventListener("mousemove", onMouseMove, true);
119
+ document.removeEventListener("click", onClick, true);
120
+ };
121
+ }, [armed]);
122
+ if (!armed || hover === null) return null;
123
+ const label = hover.location === null ? "no source" : failed ? `could not open ${hover.location.file}` : `${hover.location.file}:${hover.location.line}`;
124
+ return /* @__PURE__ */ jsx("div", {
125
+ className: "pointer-events-none fixed z-[10000] border-2 border-primary bg-primary/10",
126
+ "data-testid": "ekko-inspector",
127
+ style: {
128
+ height: hover.rect.height,
129
+ left: hover.rect.left,
130
+ top: hover.rect.top,
131
+ width: hover.rect.width
132
+ },
133
+ children: /* @__PURE__ */ jsx("span", {
134
+ className: "absolute top-0 left-0 -translate-y-full bg-primary px-1 text-primary-foreground text-xs",
135
+ children: label
136
+ })
137
+ });
138
+ }
139
+ //#endregion
140
+ //#region src/probes/result.ts
141
+ function ok(data) {
142
+ return {
143
+ data,
144
+ status: "ok"
145
+ };
146
+ }
147
+ function probeError(message) {
148
+ return {
149
+ message,
150
+ status: "error"
151
+ };
152
+ }
153
+ function unavailable(reason) {
154
+ return {
155
+ reason,
156
+ status: "unavailable"
157
+ };
158
+ }
159
+ function message(error) {
160
+ return error instanceof Error ? error.message : String(error);
161
+ }
162
+ //#endregion
163
+ //#region src/probes/network.ts
164
+ function isCssResource(entry) {
165
+ if (entry.initiatorType === "css") return true;
166
+ return (entry.name.split("?")[0] ?? entry.name).endsWith(".css");
167
+ }
168
+ function readCssTimings() {
169
+ if (typeof performance?.getEntriesByType !== "function") return unavailable("PerformanceResourceTiming unsupported");
170
+ try {
171
+ return ok(performance.getEntriesByType("resource").filter(isCssResource).map((entry) => ({
172
+ fromCache: entry.transferSize === 0,
173
+ name: entry.name,
174
+ transferSize: entry.transferSize
175
+ })));
176
+ } catch (error) {
177
+ return probeError(message(error));
178
+ }
179
+ }
180
+ async function readCacheStatus(url) {
181
+ try {
182
+ const response = await fetch(url, {
183
+ cache: "no-store",
184
+ method: "HEAD"
185
+ });
186
+ return ok({
187
+ cfCacheStatus: response.headers.get("cf-cache-status"),
188
+ status: response.status
189
+ });
190
+ } catch (error) {
191
+ return probeError(message(error));
192
+ }
193
+ }
194
+ async function readServiceWorkers() {
195
+ if (typeof navigator === "undefined" || !navigator.serviceWorker) return unavailable("serviceWorker unsupported");
196
+ try {
197
+ return ok((await navigator.serviceWorker.getRegistrations()).map((registration) => ({
198
+ active: registration.active !== null,
199
+ scope: registration.scope
200
+ })));
201
+ } catch (error) {
202
+ return probeError(message(error));
203
+ }
204
+ }
205
+ async function readCacheKeys() {
206
+ if (typeof caches === "undefined") return unavailable("CacheStorage unsupported");
207
+ try {
208
+ return ok(await caches.keys());
209
+ } catch (error) {
210
+ return probeError(message(error));
211
+ }
212
+ }
213
+ //#endregion
214
+ //#region src/probes/stylesheets.ts
215
+ function inspectSheet(sheet) {
216
+ try {
217
+ return {
218
+ href: sheet.href,
219
+ readable: true,
220
+ ruleCount: sheet.cssRules.length
221
+ };
222
+ } catch {
223
+ return {
224
+ href: sheet.href,
225
+ readable: false,
226
+ ruleCount: null
227
+ };
228
+ }
229
+ }
230
+ function readStylesheets() {
231
+ try {
232
+ return ok(Array.from(document.styleSheets).map(inspectSheet));
233
+ } catch (error) {
234
+ return probeError(message(error));
235
+ }
236
+ }
237
+ //#endregion
238
+ //#region src/panels/message.tsx
239
+ function Message({ result }) {
240
+ if (result.status === "ok") return null;
241
+ return /* @__PURE__ */ jsx("p", {
242
+ className: result.status === "error" ? "text-destructive" : "text-muted-foreground",
243
+ children: result.status === "error" ? result.message : result.reason
244
+ });
245
+ }
246
+ function Row({ label, value }) {
247
+ return /* @__PURE__ */ jsxs("div", {
248
+ className: "flex justify-between gap-4 border-t py-1 first:border-t-0",
249
+ children: [/* @__PURE__ */ jsx("span", {
250
+ className: "text-muted-foreground",
251
+ children: label
252
+ }), /* @__PURE__ */ jsx("span", {
253
+ className: "font-mono",
254
+ children: value
255
+ })]
256
+ });
257
+ }
258
+ //#endregion
259
+ //#region src/panels/cache/panel.tsx
260
+ function withOccurrenceKeys(list, baseKey) {
261
+ const seen = /* @__PURE__ */ new Map();
262
+ return list.map((item) => {
263
+ const base = baseKey(item);
264
+ const count = seen.get(base) ?? 0;
265
+ seen.set(base, count + 1);
266
+ return {
267
+ ...item,
268
+ key: count === 0 ? base : `${base}#${count}`
269
+ };
270
+ });
271
+ }
272
+ function keyedStylesheets(list) {
273
+ return withOccurrenceKeys(list, (sheet) => sheet.href ?? "inline");
274
+ }
275
+ function CachePanel() {
276
+ const [cacheKeys, setCacheKeys] = useState(null);
277
+ const [workers, setWorkers] = useState(null);
278
+ const [pageStatus, setPageStatus] = useState(null);
279
+ const [assetStatuses, setAssetStatuses] = useState([]);
280
+ const mountedRef = useRef(true);
281
+ const sheets = readStylesheets();
282
+ const timings = readCssTimings();
283
+ const probe = useCallback(async () => {
284
+ const currentSheets = readStylesheets();
285
+ const hrefs = currentSheets.status === "ok" ? currentSheets.data.map((sheet) => sheet.href).filter((href) => href !== null) : [];
286
+ const [keys, registrations, status, assets] = await Promise.all([
287
+ readCacheKeys(),
288
+ readServiceWorkers(),
289
+ readCacheStatus(window.location.href),
290
+ Promise.all(hrefs.map((href) => readCacheStatus(href)))
291
+ ]);
292
+ if (!mountedRef.current) return;
293
+ setCacheKeys(keys);
294
+ setWorkers(registrations);
295
+ setPageStatus(status);
296
+ setAssetStatuses(hrefs.map((href, index) => ({
297
+ href,
298
+ result: assets[index]
299
+ })));
300
+ }, []);
301
+ useEffect(() => {
302
+ mountedRef.current = true;
303
+ probe();
304
+ return () => {
305
+ mountedRef.current = false;
306
+ };
307
+ }, [probe]);
308
+ const reprobe = useCallback(() => {
309
+ probe();
310
+ }, [probe]);
311
+ return /* @__PURE__ */ jsxs("div", {
312
+ className: "flex flex-col gap-4",
313
+ children: [
314
+ /* @__PURE__ */ jsx(Button, {
315
+ className: "self-start",
316
+ onClick: reprobe,
317
+ size: "sm",
318
+ type: "button",
319
+ variant: "outline",
320
+ children: "Re-probe"
321
+ }),
322
+ /* @__PURE__ */ jsxs("section", { children: [/* @__PURE__ */ jsx("h3", {
323
+ className: "pb-1 font-medium",
324
+ children: "CSS transfer"
325
+ }), timings.status === "ok" ? /* @__PURE__ */ jsx("ul", { children: withOccurrenceKeys(timings.data, (timing) => timing.name).map((timing) => /* @__PURE__ */ jsxs("li", {
326
+ className: "flex justify-between gap-4 border-t py-1 first:border-t-0",
327
+ children: [/* @__PURE__ */ jsx("span", {
328
+ className: "truncate font-mono",
329
+ children: timing.name
330
+ }), /* @__PURE__ */ jsx("span", {
331
+ className: "shrink-0 text-muted-foreground",
332
+ children: timing.fromCache ? "from cache" : `${timing.transferSize} B`
333
+ })]
334
+ }, timing.key)) }) : /* @__PURE__ */ jsx(Message, { result: timings })] }),
335
+ /* @__PURE__ */ jsxs("section", { children: [/* @__PURE__ */ jsx("h3", {
336
+ className: "pb-1 font-medium",
337
+ children: "Stylesheets"
338
+ }), sheets.status === "ok" ? /* @__PURE__ */ jsx("ul", { children: keyedStylesheets(sheets.data).map((sheet) => /* @__PURE__ */ jsxs("li", {
339
+ className: "flex justify-between gap-4 border-t py-1 first:border-t-0",
340
+ children: [/* @__PURE__ */ jsx("span", {
341
+ className: "truncate font-mono",
342
+ children: sheet.href ?? "inline"
343
+ }), /* @__PURE__ */ jsx("span", {
344
+ className: "shrink-0 text-muted-foreground",
345
+ children: sheet.readable ? `${sheet.ruleCount} rules` : "unreadable"
346
+ })]
347
+ }, sheet.key)) }) : /* @__PURE__ */ jsx(Message, { result: sheets })] }),
348
+ /* @__PURE__ */ jsxs("section", { children: [
349
+ /* @__PURE__ */ jsx("h3", {
350
+ className: "pb-1 font-medium",
351
+ children: "Edge (cf-cache-status)"
352
+ }),
353
+ pageStatus === null ? null : pageStatus.status === "ok" ? /* @__PURE__ */ jsx(Row, {
354
+ label: "page",
355
+ value: pageStatus.data.cfCacheStatus ?? "—"
356
+ }) : /* @__PURE__ */ jsx(Message, { result: pageStatus }),
357
+ assetStatuses.map(({ href, result }) => result.status === "ok" ? /* @__PURE__ */ jsx(Row, {
358
+ label: href,
359
+ value: result.data.cfCacheStatus ?? "—"
360
+ }, href) : /* @__PURE__ */ jsxs("div", {
361
+ className: "flex flex-col gap-1 border-t py-1 first:border-t-0",
362
+ children: [/* @__PURE__ */ jsx("span", {
363
+ className: "truncate font-mono text-muted-foreground",
364
+ children: href
365
+ }), /* @__PURE__ */ jsx(Message, { result })]
366
+ }, href))
367
+ ] }),
368
+ /* @__PURE__ */ jsxs("section", { children: [/* @__PURE__ */ jsx("h3", {
369
+ className: "pb-1 font-medium",
370
+ children: "Service workers"
371
+ }), workers === null ? null : workers.status === "ok" ? workers.data.length === 0 ? /* @__PURE__ */ jsx("p", {
372
+ className: "text-muted-foreground",
373
+ children: "none registered"
374
+ }) : /* @__PURE__ */ jsx("ul", { children: workers.data.map((worker) => /* @__PURE__ */ jsxs("li", {
375
+ className: "font-mono",
376
+ children: [worker.scope, worker.active ? " (active)" : ""]
377
+ }, worker.scope)) }) : /* @__PURE__ */ jsx(Message, { result: workers })] }),
378
+ /* @__PURE__ */ jsxs("section", { children: [/* @__PURE__ */ jsx("h3", {
379
+ className: "pb-1 font-medium",
380
+ children: "Cache storage"
381
+ }), cacheKeys === null ? null : cacheKeys.status === "ok" ? /* @__PURE__ */ jsx("ul", { children: cacheKeys.data.map((key) => /* @__PURE__ */ jsx("li", {
382
+ className: "font-mono",
383
+ children: key
384
+ }, key)) }) : /* @__PURE__ */ jsx(Message, { result: cacheKeys })] })
385
+ ]
386
+ });
387
+ }
388
+ //#endregion
389
+ //#region src/shell/corner-context.tsx
390
+ const CornerContext = createContext(null);
391
+ function useCorner() {
392
+ const value = useContext(CornerContext);
393
+ if (value === null) throw new Error("useCorner must be used within CornerContext.Provider");
394
+ return value;
395
+ }
396
+ //#endregion
397
+ //#region src/shell/storage.ts
398
+ const STORAGE_PREFIX = "ekko-devtools";
399
+ const CORNERS = [
400
+ "bottom-right",
401
+ "bottom-left",
402
+ "top-right",
403
+ "top-left"
404
+ ];
405
+ const DEFAULT_STATE = {
406
+ activeTab: "theme",
407
+ corner: "bottom-right",
408
+ open: false
409
+ };
410
+ function isCorner(value) {
411
+ return CORNERS.includes(value);
412
+ }
413
+ function readItem(key) {
414
+ if (typeof window === "undefined") return null;
415
+ try {
416
+ return window.localStorage.getItem(`${STORAGE_PREFIX}.${key}`);
417
+ } catch {
418
+ return null;
419
+ }
420
+ }
421
+ function writeItem(key, value) {
422
+ if (typeof window === "undefined") return;
423
+ try {
424
+ window.localStorage.setItem(`${STORAGE_PREFIX}.${key}`, value);
425
+ } catch {
426
+ return;
427
+ }
428
+ }
429
+ function readShellState() {
430
+ const storedCorner = readItem("corner");
431
+ return {
432
+ activeTab: readItem("tab") ?? DEFAULT_STATE.activeTab,
433
+ corner: storedCorner !== null && isCorner(storedCorner) ? storedCorner : DEFAULT_STATE.corner,
434
+ open: readItem("open") === "true"
435
+ };
436
+ }
437
+ function writeShellState(patch) {
438
+ if (patch.activeTab !== void 0) writeItem("tab", patch.activeTab);
439
+ if (patch.corner !== void 0) writeItem("corner", patch.corner);
440
+ if (patch.open !== void 0) writeItem("open", String(patch.open));
441
+ }
442
+ //#endregion
443
+ //#region src/panels/preferences/panel.tsx
444
+ const CORNER_LABELS = {
445
+ "bottom-right": "Bottom right",
446
+ "bottom-left": "Bottom left",
447
+ "top-right": "Top right",
448
+ "top-left": "Top left"
449
+ };
450
+ function PreferencesPanel() {
451
+ const { corner, setCorner } = useCorner();
452
+ return /* @__PURE__ */ jsx("div", {
453
+ className: "flex flex-col gap-4",
454
+ children: /* @__PURE__ */ jsxs("section", { children: [/* @__PURE__ */ jsx("h3", {
455
+ className: "pb-1 font-medium",
456
+ children: "Position"
457
+ }), /* @__PURE__ */ jsx(RadioGroup, {
458
+ onValueChange: (value) => setCorner(value),
459
+ value: corner,
460
+ children: CORNERS.map((option) => /* @__PURE__ */ jsxs("div", {
461
+ className: "flex items-center gap-2",
462
+ children: [
463
+ /* @__PURE__ */ jsx(RadioGroupItem, {
464
+ id: `devtools-corner-${option}`,
465
+ value: option
466
+ }),
467
+ /* @__PURE__ */ jsx(Label, {
468
+ htmlFor: `devtools-corner-${option}`,
469
+ children: CORNER_LABELS[option]
470
+ }),
471
+ option === "bottom-left" && /* @__PURE__ */ jsx("span", {
472
+ className: "text-muted-foreground text-sm",
473
+ children: "overlaps Next's dev indicator"
474
+ })
475
+ ]
476
+ }, option))
477
+ })] })
478
+ });
479
+ }
480
+ //#endregion
481
+ //#region src/probes/theme-dom.ts
482
+ function readThemeDom() {
483
+ if (typeof document === "undefined" || typeof window === "undefined") return unavailable("no document");
484
+ try {
485
+ const root = document.documentElement;
486
+ return ok({
487
+ bodyBackgroundColor: window.getComputedStyle(document.body).backgroundColor,
488
+ dataTheme: root.getAttribute("data-theme"),
489
+ htmlClasses: Array.from(root.classList)
490
+ });
491
+ } catch (error) {
492
+ return probeError(message(error));
493
+ }
494
+ }
495
+ //#endregion
496
+ //#region src/probes/ui-storage.ts
497
+ function readUiThemeStorage(storageKey = "ekko-ui") {
498
+ if (typeof window === "undefined" || !window.localStorage) return unavailable("no window");
499
+ try {
500
+ const read = (key) => window.localStorage.getItem(`${storageKey}.${key}`);
501
+ return ok({
502
+ themeDark: read("theme-dark"),
503
+ themeLight: read("theme-light"),
504
+ themeMode: read("theme-mode")
505
+ });
506
+ } catch (error) {
507
+ return probeError(message(error));
508
+ }
509
+ }
510
+ //#endregion
511
+ //#region src/panels/theme/panel.tsx
512
+ function ThemePanel() {
513
+ const theme = useThemeContext();
514
+ const [, forceUpdate] = useState(0);
515
+ useEffect(() => {
516
+ const bump = () => forceUpdate((n) => n + 1);
517
+ window.addEventListener("storage", bump);
518
+ const observer = new MutationObserver(bump);
519
+ observer.observe(document.documentElement, { attributeFilter: ["class", "data-theme"] });
520
+ return () => {
521
+ window.removeEventListener("storage", bump);
522
+ observer.disconnect();
523
+ };
524
+ }, []);
525
+ const dom = readThemeDom();
526
+ const storage = readUiThemeStorage();
527
+ return /* @__PURE__ */ jsxs("div", {
528
+ className: "flex flex-col gap-4",
529
+ children: [
530
+ /* @__PURE__ */ jsxs("section", { children: [/* @__PURE__ */ jsx("h3", {
531
+ className: "pb-1 font-medium",
532
+ children: "Context"
533
+ }), theme === null ? /* @__PURE__ */ jsx("p", {
534
+ className: "text-muted-foreground",
535
+ children: "no theme provider above this panel"
536
+ }) : /* @__PURE__ */ jsxs(Fragment, { children: [
537
+ /* @__PURE__ */ jsx(Row, {
538
+ label: "appearance",
539
+ value: theme.appearance
540
+ }),
541
+ /* @__PURE__ */ jsx(Row, {
542
+ label: "resolvedMode",
543
+ value: theme.resolvedMode
544
+ }),
545
+ /* @__PURE__ */ jsx(Row, {
546
+ label: "resolvedTheme",
547
+ value: theme.resolvedTheme
548
+ })
549
+ ] })] }),
550
+ /* @__PURE__ */ jsxs("section", { children: [/* @__PURE__ */ jsx("h3", {
551
+ className: "pb-1 font-medium",
552
+ children: "DOM"
553
+ }), dom.status === "ok" ? /* @__PURE__ */ jsxs(Fragment, { children: [
554
+ /* @__PURE__ */ jsx(Row, {
555
+ label: "html[data-theme]",
556
+ value: dom.data.dataTheme ?? "—"
557
+ }),
558
+ /* @__PURE__ */ jsx(Row, {
559
+ label: "html class",
560
+ value: dom.data.htmlClasses.join(" ") || "—"
561
+ }),
562
+ /* @__PURE__ */ jsx(Row, {
563
+ label: "body background",
564
+ value: dom.data.bodyBackgroundColor
565
+ })
566
+ ] }) : /* @__PURE__ */ jsx(Message, { result: dom })] }),
567
+ /* @__PURE__ */ jsxs("section", { children: [/* @__PURE__ */ jsx("h3", {
568
+ className: "pb-1 font-medium",
569
+ children: "Storage"
570
+ }), storage.status === "ok" ? /* @__PURE__ */ jsxs(Fragment, { children: [
571
+ /* @__PURE__ */ jsx(Row, {
572
+ label: "ekko-ui.theme-mode",
573
+ value: storage.data.themeMode ?? "—"
574
+ }),
575
+ /* @__PURE__ */ jsx(Row, {
576
+ label: "ekko-ui.theme-light",
577
+ value: storage.data.themeLight ?? "—"
578
+ }),
579
+ /* @__PURE__ */ jsx(Row, {
580
+ label: "ekko-ui.theme-dark",
581
+ value: storage.data.themeDark ?? "—"
582
+ })
583
+ ] }) : /* @__PURE__ */ jsx(Message, { result: storage })] })
584
+ ]
585
+ });
586
+ }
587
+ //#endregion
588
+ //#region src/probes/cascade.ts
589
+ function isStyleRule(rule) {
590
+ return "selectorText" in rule && "style" in rule;
591
+ }
592
+ function isGroupingRule(rule) {
593
+ return "cssRules" in rule;
594
+ }
595
+ function collectMatches(rules, property, target, href, out) {
596
+ for (const rule of Array.from(rules)) if (isStyleRule(rule)) {
597
+ const value = rule.style.getPropertyValue(property).trim();
598
+ if (value === "") continue;
599
+ try {
600
+ if (!target.matches(rule.selectorText)) continue;
601
+ } catch {
602
+ continue;
603
+ }
604
+ out.push({
605
+ href,
606
+ selector: rule.selectorText,
607
+ value,
608
+ wins: false
609
+ });
610
+ } else if (isGroupingRule(rule)) collectMatches(rule.cssRules, property, target, href, out);
611
+ }
612
+ function resolveCustomProperty(property, target) {
613
+ try {
614
+ let computed = "";
615
+ try {
616
+ computed = window.getComputedStyle(target).getPropertyValue(property).trim();
617
+ } catch {}
618
+ const matches = [];
619
+ let unreadableSheets = 0;
620
+ for (const sheet of Array.from(document.styleSheets)) try {
621
+ collectMatches(sheet.cssRules, property, target, sheet.href, matches);
622
+ } catch {
623
+ unreadableSheets += 1;
624
+ }
625
+ const winnerIndex = matches.findLastIndex((match) => match.value === computed);
626
+ if (winnerIndex !== -1) matches[winnerIndex].wins = true;
627
+ return ok({
628
+ computed,
629
+ matches,
630
+ unreadableSheets,
631
+ winnerFound: winnerIndex !== -1
632
+ });
633
+ } catch (error) {
634
+ return probeError(message(error));
635
+ }
636
+ }
637
+ //#endregion
638
+ //#region src/panels/tokens/panel.tsx
639
+ function targetLabel(element) {
640
+ const tag = element.tagName.toLowerCase();
641
+ const slot = element.getAttribute("data-slot");
642
+ return slot === null ? tag : `${tag}[data-slot=${slot}]`;
643
+ }
644
+ function TokensPanel() {
645
+ const [property, setProperty] = useState("");
646
+ const [target, setTarget] = useState(document.documentElement);
647
+ const [picking, setPicking] = useState(false);
648
+ useEffect(() => {
649
+ if (!picking) return;
650
+ setPickerArmed(true);
651
+ const onClick = (event) => {
652
+ const clicked = event.target;
653
+ if (!(clicked instanceof Element)) return;
654
+ event.preventDefault();
655
+ event.stopPropagation();
656
+ setTarget(clicked);
657
+ setPicking(false);
658
+ };
659
+ document.addEventListener("click", onClick, true);
660
+ return () => {
661
+ document.removeEventListener("click", onClick, true);
662
+ setPickerArmed(false);
663
+ };
664
+ }, [picking]);
665
+ const trimmed = property.trim();
666
+ const report = trimmed === "" ? null : resolveCustomProperty(trimmed, target);
667
+ return /* @__PURE__ */ jsxs("div", {
668
+ className: "flex flex-col gap-3",
669
+ children: [
670
+ /* @__PURE__ */ jsx(Row, {
671
+ label: "target",
672
+ value: targetLabel(target)
673
+ }),
674
+ /* @__PURE__ */ jsxs("div", {
675
+ className: "flex gap-2",
676
+ children: [/* @__PURE__ */ jsx(Button, {
677
+ className: "self-start",
678
+ onClick: () => setPicking(true),
679
+ size: "sm",
680
+ type: "button",
681
+ variant: "outline",
682
+ children: picking ? "click an element…" : "Pick element"
683
+ }), target !== document.documentElement && /* @__PURE__ */ jsx(Button, {
684
+ className: "self-start",
685
+ onClick: () => setTarget(document.documentElement),
686
+ size: "sm",
687
+ type: "button",
688
+ variant: "outline",
689
+ children: "Reset to html"
690
+ })]
691
+ }),
692
+ /* @__PURE__ */ jsxs("div", {
693
+ className: "flex flex-col gap-1",
694
+ children: [/* @__PURE__ */ jsx(Label, {
695
+ className: "text-muted-foreground",
696
+ htmlFor: "devtools-custom-property",
697
+ children: "Custom property"
698
+ }), /* @__PURE__ */ jsx(Input, {
699
+ className: "font-mono",
700
+ id: "devtools-custom-property",
701
+ onChange: (event) => setProperty(event.target.value),
702
+ placeholder: "--sidebar-accent",
703
+ value: property
704
+ })]
705
+ }),
706
+ report === null ? null : report.status !== "ok" ? /* @__PURE__ */ jsx(Message, { result: report }) : /* @__PURE__ */ jsxs(Fragment, { children: [
707
+ /* @__PURE__ */ jsx(Row, {
708
+ label: "computed",
709
+ value: report.data.computed || "—"
710
+ }),
711
+ report.data.winnerFound ? null : /* @__PURE__ */ jsx("p", {
712
+ className: "text-muted-foreground",
713
+ children: "no declaration matches the computed value — an inline style or an unreadable sheet won"
714
+ }),
715
+ /* @__PURE__ */ jsx("ul", {
716
+ className: "flex flex-col",
717
+ children: report.data.matches.map((match) => /* @__PURE__ */ jsxs("li", {
718
+ className: "flex flex-col border-t py-1 first:border-t-0",
719
+ children: [/* @__PURE__ */ jsxs("span", {
720
+ className: "font-mono",
721
+ children: [/* @__PURE__ */ jsx("span", { children: match.selector }), match.wins ? /* @__PURE__ */ jsx("span", { children: " ← wins" }) : null]
722
+ }), /* @__PURE__ */ jsxs("span", {
723
+ className: "text-muted-foreground",
724
+ children: [
725
+ match.value,
726
+ " · ",
727
+ match.href ?? "inline"
728
+ ]
729
+ })]
730
+ }, `${match.href ?? "inline"}-${match.selector}-${match.value}`))
731
+ }),
732
+ report.data.unreadableSheets === 0 ? null : /* @__PURE__ */ jsxs("p", {
733
+ className: "text-muted-foreground",
734
+ children: [
735
+ report.data.unreadableSheets,
736
+ " unreadable sheet",
737
+ report.data.unreadableSheets === 1 ? "" : "s"
738
+ ]
739
+ })
740
+ ] })
741
+ ]
742
+ });
743
+ }
744
+ //#endregion
745
+ //#region src/panels/registry.tsx
746
+ const PANELS = [
747
+ {
748
+ id: "theme",
749
+ render: () => /* @__PURE__ */ jsx(ThemePanel, {}),
750
+ title: "Theme"
751
+ },
752
+ {
753
+ id: "tokens",
754
+ render: () => /* @__PURE__ */ jsx(TokensPanel, {}),
755
+ title: "Tokens"
756
+ },
757
+ {
758
+ id: "cache",
759
+ render: () => /* @__PURE__ */ jsx(CachePanel, {}),
760
+ title: "Cache"
761
+ },
762
+ {
763
+ id: "preferences",
764
+ render: () => /* @__PURE__ */ jsx(PreferencesPanel, {}),
765
+ title: "Preferences"
766
+ }
767
+ ];
768
+ //#endregion
769
+ //#region src/shell/corner.ts
770
+ const CORNER_CONTAINER_CLASSES = {
771
+ "bottom-right": "fixed z-[9999] flex flex-col-reverse items-end gap-2 right-4 bottom-4",
772
+ "bottom-left": "fixed z-[9999] flex flex-col-reverse items-start gap-2 left-4 bottom-4",
773
+ "top-right": "fixed z-[9999] flex flex-col items-end gap-2 right-4 top-4",
774
+ "top-left": "fixed z-[9999] flex flex-col items-start gap-2 left-4 top-4"
775
+ };
776
+ //#endregion
777
+ //#region src/shell/heart-icon.tsx
778
+ const HEART_ICON_DATA_URI = "data:image/png;base64,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";
779
+ function HeartIcon() {
780
+ return /* @__PURE__ */ jsx("img", {
781
+ alt: "",
782
+ className: "h-5 w-5",
783
+ src: HEART_ICON_DATA_URI
784
+ });
785
+ }
786
+ //#endregion
787
+ //#region src/shell/devtools.tsx
788
+ function panelFor(id) {
789
+ return PANELS.find((panel) => panel.id === id) ?? PANELS[0];
790
+ }
791
+ function EkkoDevtools() {
792
+ const [open, setOpen] = useState(DEFAULT_STATE.open);
793
+ const [activeTab, setActiveTab] = useState(DEFAULT_STATE.activeTab);
794
+ const [corner, setCornerState] = useState(DEFAULT_STATE.corner);
795
+ useEffect(() => {
796
+ const stored = readShellState();
797
+ setOpen(stored.open);
798
+ setActiveTab(stored.activeTab);
799
+ setCornerState(stored.corner);
800
+ }, []);
801
+ const toggle = useCallback(() => {
802
+ setOpen((previous) => {
803
+ writeShellState({ open: !previous });
804
+ return !previous;
805
+ });
806
+ }, []);
807
+ const setCorner = useCallback((next) => {
808
+ setCornerState(next);
809
+ writeShellState({ corner: next });
810
+ }, []);
811
+ useEffect(() => {
812
+ const onKeyDown = (event) => {
813
+ if (event.metaKey && event.shiftKey && !event.ctrlKey && event.code === "KeyU") {
814
+ event.preventDefault();
815
+ toggle();
816
+ return;
817
+ }
818
+ if (event.key === "Escape") setOpen((previous) => {
819
+ if (previous) writeShellState({ open: false });
820
+ return false;
821
+ });
822
+ };
823
+ window.addEventListener("keydown", onKeyDown);
824
+ return () => window.removeEventListener("keydown", onKeyDown);
825
+ }, [toggle]);
826
+ const selectTab = (id) => {
827
+ setActiveTab(id);
828
+ writeShellState({ activeTab: id });
829
+ };
830
+ const cornerContextValue = useMemo(() => ({
831
+ corner,
832
+ setCorner
833
+ }), [corner, setCorner]);
834
+ const containerClasses = CORNER_CONTAINER_CLASSES[corner];
835
+ const active = panelFor(activeTab);
836
+ return /* @__PURE__ */ jsxs(CornerContext.Provider, {
837
+ value: cornerContextValue,
838
+ children: [/* @__PURE__ */ jsx(InspectorOverlay, {}), /* @__PURE__ */ jsxs("div", {
839
+ className: containerClasses,
840
+ children: [/* @__PURE__ */ jsx("button", {
841
+ "aria-expanded": open,
842
+ "aria-label": "Open Ekko devtools",
843
+ className: "flex h-9 w-9 shrink-0 items-center justify-center rounded-full border bg-card shadow-lg",
844
+ onClick: toggle,
845
+ type: "button",
846
+ children: /* @__PURE__ */ jsx(HeartIcon, {})
847
+ }), open && /* @__PURE__ */ jsxs("section", {
848
+ "aria-label": "Ekko devtools",
849
+ className: "flex h-[28rem] w-[26rem] flex-col overflow-hidden rounded-xl border bg-card shadow-2xl",
850
+ role: "dialog",
851
+ children: [/* @__PURE__ */ jsxs("header", {
852
+ className: "flex items-center gap-1 border-b px-2 py-1",
853
+ children: [PANELS.map((panel) => /* @__PURE__ */ jsx(Button, {
854
+ "aria-pressed": panel.id === active?.id,
855
+ className: "aria-pressed:bg-accent aria-pressed:text-accent-foreground",
856
+ onClick: () => selectTab(panel.id),
857
+ size: "sm",
858
+ type: "button",
859
+ variant: "ghost",
860
+ children: panel.title
861
+ }, panel.id)), /* @__PURE__ */ jsx(Button, {
862
+ "aria-label": "Close Ekko devtools",
863
+ className: "ml-auto text-muted-foreground",
864
+ onClick: toggle,
865
+ size: "icon-sm",
866
+ type: "button",
867
+ variant: "ghost",
868
+ children: "×"
869
+ })]
870
+ }), /* @__PURE__ */ jsx("div", {
871
+ className: "flex-1 overflow-y-auto p-3 text-sm",
872
+ children: active === void 0 ? null : active.render()
873
+ })]
874
+ })]
875
+ })]
876
+ });
877
+ }
878
+ //#endregion
879
+ export { EkkoDevtools };
@@ -0,0 +1,12 @@
1
+ //#region loader/index.d.ts
2
+ /**
3
+ * @this {{ getOptions?: () => unknown; resourcePath: string }}
4
+ * @param {string} source
5
+ * @returns {string}
6
+ */
7
+ declare function ekkoDevtoolsLoader(this: {
8
+ getOptions?: () => unknown;
9
+ resourcePath: string;
10
+ }, source: string): string;
11
+ //#endregion
12
+ export { ekkoDevtoolsLoader as default };
@@ -0,0 +1,133 @@
1
+ import path from "node:path";
2
+ import MagicString from "magic-string";
3
+ import { parseSync, visitorKeys } from "oxc-parser";
4
+ //#region loader/inject-source.js
5
+ const SOURCE_ATTRIBUTE = "data-ekko-source";
6
+ /**
7
+ * @param {any} node
8
+ * @param {(child: any) => void} visit
9
+ */
10
+ function forEachChild(node, visit) {
11
+ for (const key of visitorKeys[node.type] ?? []) {
12
+ const value = node[key];
13
+ if (Array.isArray(value)) {
14
+ for (const child of value) if (child && typeof child.type === "string") visit(child);
15
+ } else if (value && typeof value.type === "string") visit(value);
16
+ }
17
+ }
18
+ /** @param {any} name */
19
+ function nameOf(name) {
20
+ if (name.type === "JSXIdentifier") return name.name;
21
+ if (name.type === "JSXMemberExpression") return `${nameOf(name.object)}.${name.property.name}`;
22
+ return `${name.namespace.name}:${name.name.name}`;
23
+ }
24
+ /** @param {any[]} params */
25
+ function propsNameOf(params) {
26
+ const first = params[0];
27
+ if (!first) return null;
28
+ if (first.type === "Identifier") return first.name;
29
+ if (first.type === "ObjectPattern") {
30
+ for (const property of first.properties) if (property.type === "RestElement" && property.argument.type === "Identifier") return property.argument.name;
31
+ }
32
+ return null;
33
+ }
34
+ /**
35
+ * @param {any} node
36
+ * @param {string | null} propsName
37
+ */
38
+ function shouldStamp(node, propsName) {
39
+ const name = nameOf(node.name);
40
+ if (name === "Fragment" || name === "React.Fragment") return false;
41
+ for (const attribute of node.attributes) {
42
+ if (attribute.type === "JSXAttribute" && attribute.name.type === "JSXIdentifier" && attribute.name.name === "data-ekko-source") return false;
43
+ if (attribute.type === "JSXSpreadAttribute" && attribute.argument.type === "Identifier" && attribute.argument.name === propsName) return false;
44
+ }
45
+ return true;
46
+ }
47
+ /** @param {string} code */
48
+ function lineStarts(code) {
49
+ const starts = [0];
50
+ for (let i = 0; i < code.length; i += 1) if (code[i] === "\n") starts.push(i + 1);
51
+ return starts;
52
+ }
53
+ /**
54
+ * @param {number[]} starts
55
+ * @param {number} offset
56
+ */
57
+ function locOf(starts, offset) {
58
+ let low = 0;
59
+ let high = starts.length - 1;
60
+ while (low < high) {
61
+ const mid = Math.ceil((low + high) / 2);
62
+ if (starts[mid] <= offset) low = mid;
63
+ else high = mid - 1;
64
+ }
65
+ return {
66
+ column: offset - starts[low] + 1,
67
+ line: low + 1
68
+ };
69
+ }
70
+ /**
71
+ * @param {string} filePath
72
+ * @returns {string}
73
+ */
74
+ function stampedPathFor(filePath) {
75
+ if (!path.isAbsolute(filePath)) return filePath;
76
+ const relative = path.relative(process.cwd(), filePath);
77
+ if (relative === "") return filePath;
78
+ return relative.split(path.sep).join("/");
79
+ }
80
+ /**
81
+ * @param {string} code
82
+ * @param {string} filePath
83
+ * @returns {{ code: string } | undefined}
84
+ */
85
+ function addSourceToJsx(code, filePath) {
86
+ let parsed;
87
+ try {
88
+ parsed = parseSync(filePath, code, {
89
+ lang: "tsx",
90
+ sourceType: "module"
91
+ });
92
+ } catch {
93
+ return;
94
+ }
95
+ if (parsed.errors.length > 0) return void 0;
96
+ const stampedPath = stampedPathFor(filePath);
97
+ const magic = new MagicString(code);
98
+ const starts = lineStarts(code);
99
+ let stamped = false;
100
+ /**
101
+ * @param {any} node
102
+ * @param {string | null} propsName
103
+ */
104
+ const walk = (node, propsName) => {
105
+ let scopeProps = propsName;
106
+ if (node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression") scopeProps = propsNameOf(node.params);
107
+ if (node.type === "JSXOpeningElement" && shouldStamp(node, scopeProps)) {
108
+ const loc = locOf(starts, node.start);
109
+ const insertAt = node.end - (node.selfClosing ? 2 : 1);
110
+ const attribute = `${!/\s/.test(code[insertAt - 1]) ? " " : ""}${SOURCE_ATTRIBUTE}="${stampedPath}:${loc.line}:${loc.column}"`;
111
+ magic.appendLeft(insertAt, attribute);
112
+ stamped = true;
113
+ }
114
+ forEachChild(node, (child) => walk(child, scopeProps));
115
+ };
116
+ walk(parsed.program, null);
117
+ return stamped ? { code: magic.toString() } : void 0;
118
+ }
119
+ //#endregion
120
+ //#region loader/index.js
121
+ const EXCLUDE = /node_modules/;
122
+ /**
123
+ * @this {{ getOptions?: () => unknown; resourcePath: string }}
124
+ * @param {string} source
125
+ * @returns {string}
126
+ */
127
+ function ekkoDevtoolsLoader(source) {
128
+ const filePath = this.resourcePath;
129
+ if (EXCLUDE.test(filePath) || !/\.[jt]sx$/.test(filePath)) return source;
130
+ return addSourceToJsx(source, filePath)?.code ?? source;
131
+ }
132
+ //#endregion
133
+ export { ekkoDevtoolsLoader as default };
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@ekkolyth/devtools",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "sideEffects": false,
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "files": [
9
+ "dist"
10
+ ],
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js"
15
+ },
16
+ "./loader": {
17
+ "import": "./dist/loader/index.js",
18
+ "default": "./dist/loader/index.js"
19
+ }
20
+ },
21
+ "dependencies": {
22
+ "magic-string": "^0.30.21",
23
+ "oxc-parser": "^0.127.0"
24
+ },
25
+ "peerDependencies": {
26
+ "@ekkolyth/ui": "0.1.18",
27
+ "react": "^19.2.0",
28
+ "react-dom": "^19.2.0"
29
+ },
30
+ "publishConfig": {
31
+ "access": "public"
32
+ }
33
+ }