dev-inspector 1.0.5 → 1.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.
Files changed (64) hide show
  1. package/README.md +22 -1
  2. package/lib/init.d.ts +5 -2
  3. package/lib/init.js +5 -2
  4. package/lib/integrations/index.d.ts +2 -0
  5. package/lib/integrations/index.js +7 -0
  6. package/lib/integrations/jira/adf.d.ts +11 -0
  7. package/lib/integrations/jira/adf.js +17 -0
  8. package/lib/integrations/jira/client.d.ts +4 -0
  9. package/lib/integrations/jira/client.js +113 -0
  10. package/lib/integrations/jira/errors.d.ts +10 -0
  11. package/lib/integrations/jira/errors.js +13 -0
  12. package/lib/integrations/jira/index.d.ts +2 -0
  13. package/lib/integrations/jira/index.js +8 -0
  14. package/lib/integrations/types.d.ts +54 -0
  15. package/lib/integrations/types.js +2 -0
  16. package/lib/storage/localStorage.d.ts +30 -0
  17. package/lib/storage/localStorage.js +166 -0
  18. package/lib/ui/panel/behavior/setupPanelBehavior.d.ts +4 -1
  19. package/lib/ui/panel/behavior/setupPanelBehavior.js +48 -1
  20. package/lib/ui/panel/bindings/bindStorageToPanelView.d.ts +2 -0
  21. package/lib/ui/panel/bindings/bindStorageToPanelView.js +50 -6
  22. package/lib/ui/panel/bindings/matchesQuery.d.ts +2 -0
  23. package/lib/ui/panel/bindings/matchesQuery.js +54 -0
  24. package/lib/ui/panel/bindings/types.d.ts +2 -0
  25. package/lib/ui/panel/dom/buildPanelDOM.js +65 -6
  26. package/lib/ui/panel/dom/constants.d.ts +5 -1
  27. package/lib/ui/panel/dom/constants.js +35 -20
  28. package/lib/ui/panel/dom/types.d.ts +6 -0
  29. package/lib/ui/panel/index.d.ts +4 -0
  30. package/lib/ui/panel/index.js +8 -3
  31. package/lib/ui/panel/integrations/icons.d.ts +1 -0
  32. package/lib/ui/panel/integrations/icons.js +7 -0
  33. package/lib/ui/panel/integrations/index.d.ts +1 -0
  34. package/lib/ui/panel/integrations/index.js +5 -0
  35. package/lib/ui/panel/integrations/jiraDialog.d.ts +6 -0
  36. package/lib/ui/panel/integrations/jiraDialog.js +234 -0
  37. package/lib/ui/panel/integrations/screenshot.d.ts +4 -0
  38. package/lib/ui/panel/integrations/screenshot.js +51 -0
  39. package/lib/ui/panel/localStorage/createLocalStorageView.d.ts +4 -0
  40. package/lib/ui/panel/localStorage/createLocalStorageView.js +108 -0
  41. package/lib/ui/panel/localStorage/index.d.ts +3 -0
  42. package/lib/ui/panel/localStorage/index.js +7 -0
  43. package/lib/ui/panel/localStorage/setupLocalStorageSync.d.ts +9 -0
  44. package/lib/ui/panel/localStorage/setupLocalStorageSync.js +30 -0
  45. package/lib/ui/panel/localStorage/types.d.ts +6 -0
  46. package/lib/ui/panel/localStorage/types.js +2 -0
  47. package/lib/ui/panel/state/createPanelState.d.ts +2 -1
  48. package/lib/ui/panel/state/createPanelState.js +5 -1
  49. package/lib/ui/panel/state/types.d.ts +5 -0
  50. package/lib/ui/panel/theme/applyTheme.d.ts +2 -0
  51. package/lib/ui/panel/theme/applyTheme.js +6 -0
  52. package/lib/ui/panel/theme/index.d.ts +3 -0
  53. package/lib/ui/panel/theme/index.js +9 -0
  54. package/lib/ui/panel/theme/storage.d.ts +4 -0
  55. package/lib/ui/panel/theme/storage.js +45 -0
  56. package/lib/ui/panel/theme/types.d.ts +6 -0
  57. package/lib/ui/panel/theme/types.js +2 -0
  58. package/lib/ui/panelStyles.d.ts +1 -1
  59. package/lib/ui/panelStyles.js +514 -125
  60. package/lib/ui/shared/copy.d.ts +2 -0
  61. package/lib/ui/shared/copy.js +101 -0
  62. package/package.json +1 -1
  63. package/lib/ui/logList/networkDetails.d.ts +0 -2
  64. package/lib/ui/logList/networkDetails.js +0 -227
package/README.md CHANGED
@@ -19,6 +19,10 @@ In-page devtools-style logger panel for web apps. Capture **console** and **netw
19
19
  - Network interception: `fetch` + `XMLHttpRequest`
20
20
  - In-memory storage with events (`EventTarget`)
21
21
  - UI panel with tabs (Console / Network), counters, severity/status colors, and resize
22
+ - Search across console messages and network requests (URL, method, status, body)
23
+ - Auto-scroll lock with a "Latest" jump button that surfaces unread entries
24
+ - Light / dark theme with `localStorage` persistence (defaults to light)
25
+ - Configurable panel title (used in the header and the floating toggle)
22
26
  - Vite demo playground
23
27
 
24
28
  ## Installation
@@ -35,10 +39,27 @@ import { initDevInspector } from "dev-inspector";
35
39
  initDevInspector({
36
40
  maxSize: 500,
37
41
  networkOptions: { includeBodies: false },
38
- panelOptions: { initiallyOpen: true, title: "Dev Inspector" },
42
+ panelOptions: {
43
+ initiallyOpen: true,
44
+ title: "My App Inspector",
45
+ theme: "light",
46
+ persistTheme: true,
47
+ },
39
48
  });
40
49
  ```
41
50
 
51
+ ### Panel options
52
+
53
+ | Option | Type | Default | Description |
54
+ | ----------------- | ------------------- | ----------------------- | ------------------------------------------------------------------------------------ |
55
+ | `title` | `string` | `"Dev Inspector"` | Label shown in the panel header **and** the floating toggle pill in the bottom-right |
56
+ | `initiallyOpen` | `boolean` | `true` | Open the panel on mount |
57
+ | `theme` | `"light" \| "dark"` | `"light"` | Initial theme. Overridden by a stored value when `persistTheme` is `true` |
58
+ | `persistTheme` | `boolean` | `true` | Persist the user's theme choice in `localStorage` |
59
+ | `themeStorageKey` | `string` | `"dev-inspector:theme"` | Key used by `localStorage` to remember the theme |
60
+
61
+ The theme can also be toggled at runtime via the sun/moon button in the panel header. When `persistTheme` is `true`, the choice is stored under `themeStorageKey` and restored on the next load.
62
+
42
63
  ## Important: Browser-only (SSR)
43
64
 
44
65
  Dev Inspector’s UI (`createPanel()` and the default `initDevInspector()` flow) requires a **browser environment** (it needs `document`).
package/lib/init.d.ts CHANGED
@@ -1,8 +1,12 @@
1
- type InitOptions = {
1
+ export type Theme = "light" | "dark";
2
+ export type InitOptions = {
2
3
  maxSize?: number;
3
4
  panelOptions?: {
4
5
  title?: string;
5
6
  initiallyOpen?: boolean;
7
+ theme?: Theme;
8
+ persistTheme?: boolean;
9
+ themeStorageKey?: string;
6
10
  };
7
11
  networkOptions?: {
8
12
  includeBodies?: boolean;
@@ -10,4 +14,3 @@ type InitOptions = {
10
14
  };
11
15
  };
12
16
  export declare function initDevInspector(options?: InitOptions): void;
13
- export {};
package/lib/init.js CHANGED
@@ -12,7 +12,7 @@ function getGlobalBag() {
12
12
  return globalThis;
13
13
  }
14
14
  function initDevInspector(options = {}) {
15
- var _a, _b, _c, _d, _e, _f, _g, _h, _j;
15
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
16
16
  const key = getGlobalInstanceKey();
17
17
  const bag = getGlobalBag();
18
18
  const prev = bag[key];
@@ -20,7 +20,7 @@ function initDevInspector(options = {}) {
20
20
  try {
21
21
  prev.destroy();
22
22
  }
23
- catch (_k) {
23
+ catch (_o) {
24
24
  void 0;
25
25
  }
26
26
  }
@@ -35,6 +35,9 @@ function initDevInspector(options = {}) {
35
35
  storage,
36
36
  initiallyOpen: (_g = (_f = options.panelOptions) === null || _f === void 0 ? void 0 : _f.initiallyOpen) !== null && _g !== void 0 ? _g : true,
37
37
  title: (_j = (_h = options.panelOptions) === null || _h === void 0 ? void 0 : _h.title) !== null && _j !== void 0 ? _j : "Dev Inspector",
38
+ theme: (_k = options.panelOptions) === null || _k === void 0 ? void 0 : _k.theme,
39
+ persistTheme: (_l = options.panelOptions) === null || _l === void 0 ? void 0 : _l.persistTheme,
40
+ themeStorageKey: (_m = options.panelOptions) === null || _m === void 0 ? void 0 : _m.themeStorageKey,
38
41
  });
39
42
  const destroy = () => {
40
43
  try {
@@ -0,0 +1,2 @@
1
+ export type { CreateIssueInput, CreateIssueResult, IntegrationContext, IntegrationId, IntegrationsConfig, JiraIntegrationConfig } from "./types";
2
+ export { createJiraIssue, fetchJiraIssueTypes, JiraIntegrationError } from "./jira";
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.JiraIntegrationError = exports.fetchJiraIssueTypes = exports.createJiraIssue = void 0;
4
+ var jira_1 = require("./jira");
5
+ Object.defineProperty(exports, "createJiraIssue", { enumerable: true, get: function () { return jira_1.createJiraIssue; } });
6
+ Object.defineProperty(exports, "fetchJiraIssueTypes", { enumerable: true, get: function () { return jira_1.fetchJiraIssueTypes; } });
7
+ Object.defineProperty(exports, "JiraIntegrationError", { enumerable: true, get: function () { return jira_1.JiraIntegrationError; } });
@@ -0,0 +1,11 @@
1
+ export declare function textToAdf(text: string): {
2
+ type: string;
3
+ version: number;
4
+ content: {
5
+ type: string;
6
+ content: {
7
+ type: string;
8
+ text: string;
9
+ }[];
10
+ }[];
11
+ };
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.textToAdf = textToAdf;
4
+ function paragraph(text) {
5
+ return {
6
+ type: "paragraph",
7
+ content: text.length > 0 ? [{ type: "text", text }] : [],
8
+ };
9
+ }
10
+ function textToAdf(text) {
11
+ const lines = text.split(/\r?\n/);
12
+ return {
13
+ type: "doc",
14
+ version: 1,
15
+ content: lines.map((l) => paragraph(l)),
16
+ };
17
+ }
@@ -0,0 +1,4 @@
1
+ import type { CreateIssueInput, CreateIssueResult, JiraIntegrationConfig } from "../types";
2
+ import type { JiraIssueTypeOption } from "../types";
3
+ export declare function fetchJiraIssueTypes(config: JiraIntegrationConfig, projectKey?: string): Promise<JiraIssueTypeOption[]>;
4
+ export declare function createJiraIssue(input: CreateIssueInput, config: JiraIntegrationConfig): Promise<CreateIssueResult>;
@@ -0,0 +1,113 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.fetchJiraIssueTypes = fetchJiraIssueTypes;
4
+ exports.createJiraIssue = createJiraIssue;
5
+ const adf_1 = require("./adf");
6
+ const errors_1 = require("./errors");
7
+ async function readJsonSafe(res) {
8
+ try {
9
+ return (await res.json());
10
+ }
11
+ catch (_a) {
12
+ return undefined;
13
+ }
14
+ }
15
+ function mapStatus(status) {
16
+ if (status === 400)
17
+ return { code: "bad_request", message: "Bad request. Check required fields and Jira configuration." };
18
+ if (status === 401)
19
+ return { code: "unauthorized", message: "Unauthorized. Check Jira credentials." };
20
+ if (status === 403)
21
+ return { code: "forbidden", message: "Forbidden. Check Jira permissions." };
22
+ if (status === 429)
23
+ return { code: "rate_limited", message: "Rate limited by Jira. Try again later." };
24
+ if (status >= 500)
25
+ return { code: "server_error", message: "Jira server error. Try again later." };
26
+ return { code: "unknown", message: "Unexpected Jira error." };
27
+ }
28
+ function isRecord(value) {
29
+ return typeof value === "object" && value !== null;
30
+ }
31
+ function toQuery(params) {
32
+ const sp = new URLSearchParams();
33
+ for (const [k, v] of Object.entries(params)) {
34
+ if (typeof v === "string" && v.length > 0)
35
+ sp.set(k, v);
36
+ }
37
+ const s = sp.toString();
38
+ return s.length > 0 ? `?${s}` : "";
39
+ }
40
+ async function fetchJiraIssueTypes(config, projectKey) {
41
+ var _a;
42
+ const proxyUrl = config.proxyUrl;
43
+ if (!proxyUrl)
44
+ throw new errors_1.JiraIntegrationError("Missing Jira proxyUrl.", "not_configured");
45
+ const url = `${proxyUrl}${toQuery({ op: "issueTypes", projectKey: (_a = projectKey !== null && projectKey !== void 0 ? projectKey : config.projectKey) !== null && _a !== void 0 ? _a : "" })}`;
46
+ let res;
47
+ try {
48
+ res = await fetch(url, { method: "GET", headers: { accept: "application/json" } });
49
+ }
50
+ catch (e) {
51
+ throw new errors_1.JiraIntegrationError("Network error while loading Jira issue types.", "network_error", { details: String(e) });
52
+ }
53
+ const data = await readJsonSafe(res);
54
+ if (!res.ok) {
55
+ const mapped = mapStatus(res.status);
56
+ throw new errors_1.JiraIntegrationError(mapped.message, mapped.code, { status: res.status, details: data });
57
+ }
58
+ if (!Array.isArray(data))
59
+ return [];
60
+ const out = [];
61
+ for (const it of data) {
62
+ if (!isRecord(it))
63
+ continue;
64
+ const id = typeof it.id === "string" ? it.id : undefined;
65
+ const name = typeof it.name === "string" ? it.name : undefined;
66
+ if (id && name)
67
+ out.push({ id, name });
68
+ }
69
+ return out;
70
+ }
71
+ async function createJiraIssue(input, config) {
72
+ var _a, _b, _c, _d, _e, _f;
73
+ const title = input.title.trim();
74
+ if (title.length === 0)
75
+ throw new errors_1.JiraIntegrationError("Title is required.", "bad_request");
76
+ const projectKey = (_b = (_a = input.jira) === null || _a === void 0 ? void 0 : _a.projectKey) !== null && _b !== void 0 ? _b : config.projectKey;
77
+ if (!projectKey)
78
+ throw new errors_1.JiraIntegrationError("Missing Jira project key.", "not_configured");
79
+ const issueType = (_e = (_d = (_c = input.jira) === null || _c === void 0 ? void 0 : _c.issueType) !== null && _d !== void 0 ? _d : config.issueType) !== null && _e !== void 0 ? _e : {};
80
+ if (!issueType.name && !issueType.id)
81
+ throw new errors_1.JiraIntegrationError("Missing Jira issue type (name or id).", "not_configured");
82
+ const descriptionText = (_f = input.description) !== null && _f !== void 0 ? _f : "";
83
+ const adf = (0, adf_1.textToAdf)(descriptionText);
84
+ const proxyUrl = config.proxyUrl;
85
+ if (!proxyUrl)
86
+ throw new errors_1.JiraIntegrationError("Missing Jira proxyUrl.", "not_configured");
87
+ const res = await fetch(proxyUrl, {
88
+ method: "POST",
89
+ headers: { "content-type": "application/json" },
90
+ body: JSON.stringify({
91
+ provider: "jira",
92
+ input: {
93
+ title,
94
+ descriptionAdf: adf,
95
+ context: input.context,
96
+ screenshot: input.screenshot,
97
+ projectKey,
98
+ issueType,
99
+ },
100
+ }),
101
+ });
102
+ const data = await readJsonSafe(res);
103
+ if (!res.ok) {
104
+ const mapped = mapStatus(res.status);
105
+ throw new errors_1.JiraIntegrationError(mapped.message, mapped.code, { status: res.status, details: data });
106
+ }
107
+ const rec = isRecord(data) ? data : {};
108
+ return {
109
+ id: typeof rec.id === "string" ? rec.id : undefined,
110
+ key: typeof rec.key === "string" ? rec.key : undefined,
111
+ url: typeof rec.url === "string" ? rec.url : undefined,
112
+ };
113
+ }
@@ -0,0 +1,10 @@
1
+ export type JiraErrorCode = "not_configured" | "bad_request" | "unauthorized" | "forbidden" | "rate_limited" | "server_error" | "network_error" | "unknown";
2
+ export declare class JiraIntegrationError extends Error {
3
+ code: JiraErrorCode;
4
+ status?: number;
5
+ details?: unknown;
6
+ constructor(message: string, code: JiraErrorCode, options?: {
7
+ status?: number;
8
+ details?: unknown;
9
+ });
10
+ }
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.JiraIntegrationError = void 0;
4
+ class JiraIntegrationError extends Error {
5
+ constructor(message, code, options) {
6
+ super(message);
7
+ this.name = "JiraIntegrationError";
8
+ this.code = code;
9
+ this.status = options === null || options === void 0 ? void 0 : options.status;
10
+ this.details = options === null || options === void 0 ? void 0 : options.details;
11
+ }
12
+ }
13
+ exports.JiraIntegrationError = JiraIntegrationError;
@@ -0,0 +1,2 @@
1
+ export { createJiraIssue, fetchJiraIssueTypes } from "./client";
2
+ export { JiraIntegrationError } from "./errors";
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.JiraIntegrationError = exports.fetchJiraIssueTypes = exports.createJiraIssue = void 0;
4
+ var client_1 = require("./client");
5
+ Object.defineProperty(exports, "createJiraIssue", { enumerable: true, get: function () { return client_1.createJiraIssue; } });
6
+ Object.defineProperty(exports, "fetchJiraIssueTypes", { enumerable: true, get: function () { return client_1.fetchJiraIssueTypes; } });
7
+ var errors_1 = require("./errors");
8
+ Object.defineProperty(exports, "JiraIntegrationError", { enumerable: true, get: function () { return errors_1.JiraIntegrationError; } });
@@ -0,0 +1,54 @@
1
+ export type IntegrationId = "jira";
2
+ export type JiraIssueType = {
3
+ name?: string;
4
+ id?: string;
5
+ };
6
+ export type JiraIssueTypeOption = {
7
+ id: string;
8
+ name: string;
9
+ };
10
+ export type JiraIntegrationConfig = {
11
+ proxyUrl?: string;
12
+ projectKey?: string;
13
+ issueType?: JiraIssueType;
14
+ screenshot?: {
15
+ enabled?: boolean;
16
+ mode?: "displayMedia" | "none";
17
+ };
18
+ user?: {
19
+ id?: string;
20
+ email?: string;
21
+ name?: string;
22
+ };
23
+ };
24
+ export type IntegrationsConfig = {
25
+ jira?: JiraIntegrationConfig;
26
+ };
27
+ export type IntegrationContext = {
28
+ url?: string;
29
+ userAgent?: string;
30
+ timestamp?: string;
31
+ user?: {
32
+ id?: string;
33
+ email?: string;
34
+ name?: string;
35
+ };
36
+ };
37
+ export type CreateIssueInput = {
38
+ title: string;
39
+ description?: string;
40
+ context?: IntegrationContext;
41
+ screenshot?: {
42
+ dataUrl: string;
43
+ filename: string;
44
+ };
45
+ jira?: {
46
+ projectKey?: string;
47
+ issueType?: JiraIssueType;
48
+ };
49
+ };
50
+ export type CreateIssueResult = {
51
+ id?: string;
52
+ key?: string;
53
+ url?: string;
54
+ };
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,30 @@
1
+ export type LocalStorageItem = {
2
+ key: string;
3
+ value: string;
4
+ preview: string;
5
+ valueLength: number;
6
+ approxBytes: number;
7
+ };
8
+ export type LocalStorageSnapshot = {
9
+ items: LocalStorageItem[];
10
+ totalApproxBytes: number;
11
+ };
12
+ export type LocalStorageAccess = {
13
+ ok: true;
14
+ storage: Storage;
15
+ } | {
16
+ ok: false;
17
+ message: string;
18
+ };
19
+ export declare function getLocalStorageAccess(): LocalStorageAccess;
20
+ export declare function readLocalStorageSnapshot(storage: Storage, opts: {
21
+ maxPreviewLength: number;
22
+ }): LocalStorageSnapshot;
23
+ export declare function createLocalStorageWatcher(args: {
24
+ storage: Storage;
25
+ pollIntervalMs: number;
26
+ onChange: () => void;
27
+ }): {
28
+ destroy: () => void;
29
+ trigger: () => void;
30
+ };
@@ -0,0 +1,166 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getLocalStorageAccess = getLocalStorageAccess;
4
+ exports.readLocalStorageSnapshot = readLocalStorageSnapshot;
5
+ exports.createLocalStorageWatcher = createLocalStorageWatcher;
6
+ function safeTruncate(value, maxLen) {
7
+ if (maxLen <= 0)
8
+ return "";
9
+ if (value.length <= maxLen)
10
+ return value;
11
+ return `${value.slice(0, maxLen)}…`;
12
+ }
13
+ function getLocalStorageAccess() {
14
+ try {
15
+ const w = globalThis;
16
+ const storage = w.localStorage;
17
+ if (!storage)
18
+ return { ok: false, message: "localStorage is not available in this environment." };
19
+ // Some environments expose the object but throw on access (privacy modes / blocked cookies).
20
+ void storage.length;
21
+ return { ok: true, storage };
22
+ }
23
+ catch (_a) {
24
+ return { ok: false, message: "localStorage access is blocked in this environment (privacy settings / permissions)." };
25
+ }
26
+ }
27
+ function readLocalStorageSnapshot(storage, opts) {
28
+ var _a;
29
+ const items = [];
30
+ let totalApproxBytes = 0;
31
+ const len = storage.length;
32
+ for (let i = 0; i < len; i += 1) {
33
+ const key = storage.key(i);
34
+ if (!key)
35
+ continue;
36
+ const value = (_a = storage.getItem(key)) !== null && _a !== void 0 ? _a : "";
37
+ const approxBytes = (key.length + value.length) * 2; // UTF-16-ish approximation
38
+ totalApproxBytes += approxBytes;
39
+ items.push({
40
+ key,
41
+ value,
42
+ preview: safeTruncate(value, opts.maxPreviewLength),
43
+ valueLength: value.length,
44
+ approxBytes,
45
+ });
46
+ }
47
+ items.sort((a, b) => a.key.localeCompare(b.key));
48
+ return { items, totalApproxBytes };
49
+ }
50
+ function buildSignature(storage) {
51
+ var _a;
52
+ try {
53
+ const parts = [String(storage.length)];
54
+ const len = storage.length;
55
+ for (let i = 0; i < len; i += 1) {
56
+ const k = storage.key(i);
57
+ if (!k)
58
+ continue;
59
+ const v = (_a = storage.getItem(k)) !== null && _a !== void 0 ? _a : "";
60
+ parts.push(`${k}:${v.length}`);
61
+ }
62
+ return parts.join("|");
63
+ }
64
+ catch (_b) {
65
+ return "err";
66
+ }
67
+ }
68
+ function createLocalStorageWatcher(args) {
69
+ var _a, _b, _c, _d;
70
+ const { storage, onChange } = args;
71
+ const pollIntervalMs = Math.max(250, args.pollIntervalMs);
72
+ let destroyed = false;
73
+ let lastSig = buildSignature(storage);
74
+ const trigger = () => {
75
+ if (destroyed)
76
+ return;
77
+ onChange();
78
+ };
79
+ const win = globalThis;
80
+ const onStorageEvent = () => {
81
+ const sig = buildSignature(storage);
82
+ if (sig !== lastSig) {
83
+ lastSig = sig;
84
+ trigger();
85
+ }
86
+ };
87
+ try {
88
+ (_a = win.addEventListener) === null || _a === void 0 ? void 0 : _a.call(win, "storage", onStorageEvent);
89
+ }
90
+ catch (_e) {
91
+ void 0;
92
+ }
93
+ // Same-tab changes don't fire "storage" events. We try to patch methods; if that fails, polling still keeps UI fresh.
94
+ const orig = {
95
+ setItem: (_b = storage.setItem) === null || _b === void 0 ? void 0 : _b.bind(storage),
96
+ removeItem: (_c = storage.removeItem) === null || _c === void 0 ? void 0 : _c.bind(storage),
97
+ clear: (_d = storage.clear) === null || _d === void 0 ? void 0 : _d.bind(storage),
98
+ };
99
+ const patch = () => {
100
+ try {
101
+ storage.setItem = ((key, value) => {
102
+ var _a;
103
+ (_a = orig.setItem) === null || _a === void 0 ? void 0 : _a.call(orig, key, value);
104
+ const sig = buildSignature(storage);
105
+ if (sig !== lastSig) {
106
+ lastSig = sig;
107
+ trigger();
108
+ }
109
+ });
110
+ storage.removeItem = ((key) => {
111
+ var _a;
112
+ (_a = orig.removeItem) === null || _a === void 0 ? void 0 : _a.call(orig, key);
113
+ const sig = buildSignature(storage);
114
+ if (sig !== lastSig) {
115
+ lastSig = sig;
116
+ trigger();
117
+ }
118
+ });
119
+ storage.clear = (() => {
120
+ var _a;
121
+ (_a = orig.clear) === null || _a === void 0 ? void 0 : _a.call(orig);
122
+ const sig = buildSignature(storage);
123
+ if (sig !== lastSig) {
124
+ lastSig = sig;
125
+ trigger();
126
+ }
127
+ });
128
+ return true;
129
+ }
130
+ catch (_a) {
131
+ return false;
132
+ }
133
+ };
134
+ const patched = patch();
135
+ const timer = globalThis.setInterval(() => {
136
+ const sig = buildSignature(storage);
137
+ if (sig !== lastSig) {
138
+ lastSig = sig;
139
+ trigger();
140
+ }
141
+ }, patched ? Math.max(800, pollIntervalMs) : pollIntervalMs);
142
+ const destroy = () => {
143
+ var _a;
144
+ destroyed = true;
145
+ globalThis.clearInterval(timer);
146
+ try {
147
+ (_a = win.removeEventListener) === null || _a === void 0 ? void 0 : _a.call(win, "storage", onStorageEvent);
148
+ }
149
+ catch (_b) {
150
+ void 0;
151
+ }
152
+ // best-effort restore
153
+ try {
154
+ if (orig.setItem)
155
+ storage.setItem = orig.setItem;
156
+ if (orig.removeItem)
157
+ storage.removeItem = orig.removeItem;
158
+ if (orig.clear)
159
+ storage.clear = orig.clear;
160
+ }
161
+ catch (_c) {
162
+ void 0;
163
+ }
164
+ };
165
+ return { destroy, trigger };
166
+ }
@@ -2,4 +2,7 @@ import type { LogStorage } from "../../../storage/logStorage";
2
2
  import type { PanelHandle } from "..";
3
3
  import type { PanelDOM } from "../dom/types";
4
4
  import type { PanelState } from "../state/types";
5
- export declare function setupPanelBehavior(dom: PanelDOM, state: PanelState, storage: LogStorage): PanelHandle;
5
+ export declare function setupPanelBehavior(dom: PanelDOM, state: PanelState, storage: LogStorage, themeConfig: {
6
+ storageKey: string;
7
+ persist: boolean;
8
+ }): PanelHandle;
@@ -3,7 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.setupPanelBehavior = setupPanelBehavior;
4
4
  const bindings_1 = require("../bindings");
5
5
  const resize_1 = require("../resize");
6
- function setupPanelBehavior(dom, state, storage) {
6
+ const theme_1 = require("../theme");
7
+ function setupPanelBehavior(dom, state, storage, themeConfig) {
7
8
  const applyVisibility = () => {
8
9
  if (state.open)
9
10
  dom.panel.classList.remove("di-hidden");
@@ -20,17 +21,36 @@ function setupPanelBehavior(dom, state, storage) {
20
21
  dom.consoleTab.classList.remove("di-tabActive");
21
22
  }
22
23
  };
24
+ const updateThemeButton = () => {
25
+ dom.themeBtn.setAttribute("aria-pressed", state.theme === "dark" ? "true" : "false");
26
+ dom.themeBtn.setAttribute("title", state.theme === "dark" ? "Switch to light theme" : "Switch to dark theme");
27
+ };
28
+ const setTheme = (next) => {
29
+ state.theme = next;
30
+ (0, theme_1.applyTheme)(dom.root, next);
31
+ updateThemeButton();
32
+ if (themeConfig.persist)
33
+ (0, theme_1.saveTheme)(themeConfig.storageKey, next);
34
+ };
35
+ (0, theme_1.applyTheme)(dom.root, state.theme);
36
+ updateThemeButton();
23
37
  const bindings = (0, bindings_1.bindStorageToPanelView)({
24
38
  storage,
25
39
  state,
26
40
  list: dom.list,
27
41
  body: dom.body,
42
+ jumpBtn: dom.jumpBtn,
43
+ jumpBtnLabel: dom.jumpBtnLabel,
28
44
  counters: dom.counters,
29
45
  updateTabStyles,
30
46
  });
31
47
  applyVisibility();
32
48
  updateTabStyles();
33
49
  const resizeControls = (0, resize_1.attachResizeHandling)(dom.panel, dom.resizeHandle);
50
+ const updateSearchClearVisibility = () => {
51
+ dom.searchClearBtn.classList.toggle("di-searchClear--visible", state.query.length > 0);
52
+ };
53
+ updateSearchClearVisibility();
34
54
  const open = () => {
35
55
  state.open = true;
36
56
  applyVisibility();
@@ -56,17 +76,44 @@ function setupPanelBehavior(dom, state, storage) {
56
76
  updateTabStyles();
57
77
  bindings.renderActiveTab();
58
78
  };
79
+ const onThemeClick = () => setTheme(state.theme === "dark" ? "light" : "dark");
80
+ const onSearchInput = () => {
81
+ state.query = dom.searchInput.value;
82
+ updateSearchClearVisibility();
83
+ bindings.renderActiveTab();
84
+ };
85
+ const onSearchClear = () => {
86
+ if (state.query.length === 0)
87
+ return;
88
+ state.query = "";
89
+ dom.searchInput.value = "";
90
+ updateSearchClearVisibility();
91
+ bindings.renderActiveTab();
92
+ dom.searchInput.focus();
93
+ };
94
+ const onBodyScroll = () => bindings.refreshScrollAffordances();
95
+ const onJumpClick = () => bindings.scrollToBottom();
59
96
  dom.toggleBtn.addEventListener("click", onToggleClick);
60
97
  dom.closeBtn.addEventListener("click", onCloseClick);
61
98
  dom.clearBtn.addEventListener("click", onClearClick);
99
+ dom.themeBtn.addEventListener("click", onThemeClick);
62
100
  dom.consoleTab.addEventListener("click", onConsoleTab);
63
101
  dom.networkTab.addEventListener("click", onNetworkTab);
102
+ dom.searchInput.addEventListener("input", onSearchInput);
103
+ dom.searchClearBtn.addEventListener("click", onSearchClear);
104
+ dom.body.addEventListener("scroll", onBodyScroll, { passive: true });
105
+ dom.jumpBtn.addEventListener("click", onJumpClick);
64
106
  const destroy = () => {
65
107
  dom.toggleBtn.removeEventListener("click", onToggleClick);
66
108
  dom.closeBtn.removeEventListener("click", onCloseClick);
67
109
  dom.clearBtn.removeEventListener("click", onClearClick);
110
+ dom.themeBtn.removeEventListener("click", onThemeClick);
68
111
  dom.consoleTab.removeEventListener("click", onConsoleTab);
69
112
  dom.networkTab.removeEventListener("click", onNetworkTab);
113
+ dom.searchInput.removeEventListener("input", onSearchInput);
114
+ dom.searchClearBtn.removeEventListener("click", onSearchClear);
115
+ dom.body.removeEventListener("scroll", onBodyScroll);
116
+ dom.jumpBtn.removeEventListener("click", onJumpClick);
70
117
  resizeControls.destroy();
71
118
  bindings.destroy();
72
119
  dom.root.remove();
@@ -11,6 +11,8 @@ export declare function bindStorageToPanelView(args: {
11
11
  append: (entry: LogEntry) => void;
12
12
  };
13
13
  body: HTMLElement;
14
+ jumpBtn: HTMLElement;
15
+ jumpBtnLabel: HTMLElement;
14
16
  counters: PanelCountersDOM;
15
17
  updateTabStyles: () => void;
16
18
  }): DataBindings;