elixir-data-viewer 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,33 @@
1
+ import type { Tree } from "@lezer/common";
2
+ /**
3
+ * The semantic type of an inspected node.
4
+ */
5
+ export type InspectType = "String" | "Atom" | "Alias" | "Integer" | "Float" | "Boolean" | "Nil" | "Char" | "Charlist" | "Sigil" | "Keyword" | "Pair" | "Map" | "List" | "Tuple" | "Bitstring" | "Range" | "InspectLiteral";
6
+ /**
7
+ * Represents a resolved inspect target — the range to highlight and copy.
8
+ */
9
+ export interface InspectTarget {
10
+ /** Absolute character offset — start in source code */
11
+ from: number;
12
+ /** Absolute character offset — end in source code */
13
+ to: number;
14
+ /** The text to copy when clicked */
15
+ copyText: string;
16
+ /** Whether this is a structural node that may span multiple lines */
17
+ isStructure: boolean;
18
+ /** The semantic type of the inspected node */
19
+ type: InspectType;
20
+ }
21
+ /**
22
+ * Resolve what should be inspected (highlighted + copyable) at a given offset.
23
+ *
24
+ * Logic:
25
+ * 1. Bracket tokens (`{`, `}`, `[`, `]`, `<<`, `>>`) whose parent is a
26
+ * structural node → inspect the whole structure
27
+ * 2. Direct structural nodes (e.g. `%` resolves to `Map`) → inspect whole structure
28
+ * 3. Leaf values (`String`, `Atom`, `Integer`, etc.) → inspect the leaf
29
+ * 4. Inner content nodes (`QuotedContent`) → bubble up to parent leaf
30
+ * 5. `Keyword` nodes (pair keys like `name:`) → inspect the keyword
31
+ * 6. Otherwise → null (no inspection target)
32
+ */
33
+ export declare function resolveInspectTarget(tree: Tree, code: string, offset: number): InspectTarget | null;
@@ -0,0 +1,5 @@
1
+ import type { Tree } from "@lezer/common";
2
+ /**
3
+ * Parse an Elixir data string into a Lezer syntax tree.
4
+ */
5
+ export declare function parseElixir(code: string): Tree;
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Pre-processing for Elixir inspect literals that would be misinterpreted
3
+ * by the lezer-elixir parser as comments.
4
+ *
5
+ * Elixir's `Kernel.inspect/2` renders opaque data types as `#Type<...>`,
6
+ * e.g. `#Reference<0.123.456.789>`, `#PID<0.100.0>`, `#Port<0.6>`.
7
+ * Since `#` starts a line comment in Elixir, the parser consumes
8
+ * everything from `#` to end-of-line as a Comment node, breaking
9
+ * highlighting, structure detection, and inspect.
10
+ *
11
+ * Solution: replace each inspect literal with a same-length Elixir atom
12
+ * (`:___...`) before parsing, so offsets remain 1:1 with the original.
13
+ */
14
+ /**
15
+ * A detected Elixir inspect literal in the original source.
16
+ */
17
+ export interface InspectLiteral {
18
+ /** Absolute character offset — start */
19
+ from: number;
20
+ /** Absolute character offset — end */
21
+ to: number;
22
+ /** The original text, e.g. `#Reference<0.2930644178.2773483521.184681>` */
23
+ originalText: string;
24
+ }
25
+ /**
26
+ * Result of pre-processing: the modified code and the list of literals found.
27
+ */
28
+ export interface PreprocessResult {
29
+ /** Code with inspect literals replaced by same-length atoms */
30
+ modifiedCode: string;
31
+ /** All inspect literals detected, with their original positions and text */
32
+ inspectLiterals: InspectLiteral[];
33
+ }
34
+ /**
35
+ * Pre-process Elixir data code to replace inspect literals with same-length
36
+ * atom placeholders. This ensures the lezer-elixir parser doesn't treat
37
+ * `#Type<...>` patterns as line comments.
38
+ *
39
+ * The replacement atom is `:` followed by underscores to match the original
40
+ * length exactly, preserving all character offsets for highlighting and
41
+ * inspect resolution.
42
+ */
43
+ export declare function preprocessInspectLiterals(code: string): PreprocessResult;
@@ -0,0 +1,408 @@
1
+ import { SearchState } from "./search";
2
+ import { FilterState } from "./filter";
3
+ import { type InspectTarget, type InspectType } from "./inspect";
4
+ /**
5
+ * Toolbar button visibility options.
6
+ * All buttons default to `true` (visible).
7
+ */
8
+ export interface ToolbarOptions {
9
+ /** Show "Fold All" button. Default: true */
10
+ foldAll?: boolean;
11
+ /** Show "Unfold All" button. Default: true */
12
+ unfoldAll?: boolean;
13
+ /** Show "Word Wrap" toggle button. Default: true */
14
+ wordWrap?: boolean;
15
+ /** Show "Copy" button. Default: true */
16
+ copy?: boolean;
17
+ /** Show "Search" button. Default: true */
18
+ search?: boolean;
19
+ /** Show "Filter" button. Default: true */
20
+ filter?: boolean;
21
+ }
22
+ /**
23
+ * Event object passed to the onInspect callback when the user clicks
24
+ * an inspectable value (string, atom, number, structure, etc.).
25
+ */
26
+ export interface InspectEvent {
27
+ /** The semantic type of the inspected node (e.g. "String", "Atom", "Map") */
28
+ type: InspectType;
29
+ /** The text that would be copied to clipboard */
30
+ copyText: string;
31
+ /** The full InspectTarget with offset information */
32
+ target: InspectTarget;
33
+ /** The DOM element that was clicked */
34
+ element: HTMLElement;
35
+ /** The original MouseEvent from the click */
36
+ mouseEvent: MouseEvent;
37
+ /** Call this to prevent the default copy-to-clipboard + toast behavior */
38
+ preventDefault(): void;
39
+ }
40
+ /**
41
+ * Configuration options for ElixirDataViewer.
42
+ */
43
+ export interface ElixirDataViewerOptions {
44
+ /** Toolbar button visibility. All default to true. */
45
+ toolbar?: ToolbarOptions;
46
+ /**
47
+ * Default fold level. Regions deeper than this level are automatically
48
+ * folded when setContent() is called.
49
+ * E.g. 3 = show first 3 levels expanded, fold level 4+.
50
+ * 0 or undefined = no auto-folding (all expanded).
51
+ */
52
+ defaultFoldLevel?: number;
53
+ /** Whether word wrap is enabled by default. Default: false */
54
+ defaultWordWrap?: boolean;
55
+ /** Keys to filter out by default when setContent() is called */
56
+ defaultFilterKeys?: string[];
57
+ }
58
+ /**
59
+ * ElixirDataViewer — A read-only Elixir data viewer with syntax highlighting,
60
+ * line numbers, code folding, search, and a floating toolbar.
61
+ */
62
+ export declare class ElixirDataViewer {
63
+ private container;
64
+ private innerEl;
65
+ private scrollEl;
66
+ private toolbarEl;
67
+ private wrapBtn;
68
+ private copyBtn;
69
+ private searchBtn;
70
+ private filterBtn;
71
+ private copyResetTimer;
72
+ private code;
73
+ private lines;
74
+ private lineOffsets;
75
+ private tokens;
76
+ private tree;
77
+ private foldState;
78
+ private filterState;
79
+ private defaultFoldLevel;
80
+ private defaultFilterKeys;
81
+ private searchState;
82
+ private onRenderCallback;
83
+ private wordWrap;
84
+ private toolbarConfig;
85
+ private searchBarEl;
86
+ private searchInputEl;
87
+ private searchInfoEl;
88
+ private searchCaseBtn;
89
+ private searchVisible;
90
+ private filterBarEl;
91
+ private filterInputEl;
92
+ private filterTagsEl;
93
+ private filterInfoEl;
94
+ private filterDropdownEl;
95
+ private filterCopyBtn;
96
+ private filterCopyResetTimer;
97
+ private filterVisible;
98
+ private filterDropdownIndex;
99
+ private filterDropdownItems;
100
+ private currentInspect;
101
+ private inspectCallback;
102
+ private inspectLiterals;
103
+ constructor(container: HTMLElement, options?: ElixirDataViewerOptions);
104
+ /**
105
+ * Build the floating toolbar DOM and append to the container.
106
+ */
107
+ private buildToolbar;
108
+ /**
109
+ * Build the search bar DOM (hidden by default).
110
+ */
111
+ private buildSearchBar;
112
+ /**
113
+ * Handle keyboard shortcuts on the container.
114
+ */
115
+ private handleKeyDown;
116
+ /**
117
+ * Handle keyboard events inside the search input.
118
+ */
119
+ private handleSearchKeyDown;
120
+ /**
121
+ * Create a toolbar button element.
122
+ */
123
+ private createToolbarButton;
124
+ /**
125
+ * Toggle word wrap mode for long lines.
126
+ */
127
+ toggleWordWrap(): void;
128
+ /**
129
+ * Get current word wrap state.
130
+ */
131
+ isWordWrap(): boolean;
132
+ /**
133
+ * Fold all foldable regions.
134
+ */
135
+ foldAll(): void;
136
+ /**
137
+ * Unfold all folded regions.
138
+ */
139
+ unfoldAll(): void;
140
+ /**
141
+ * Fold all regions deeper than the given level.
142
+ * Level 1 = top-level structures. foldToLevel(3) expands levels 1–3, folds 4+.
143
+ * foldToLevel(0) unfolds all.
144
+ */
145
+ foldToLevel(level: number): void;
146
+ /**
147
+ * Get the raw Elixir data content.
148
+ */
149
+ getContent(): string;
150
+ /**
151
+ * Copy the raw Elixir data content to the clipboard.
152
+ * Shows a "✓" feedback on the copy button for 2 seconds.
153
+ * Returns a promise that resolves when copying is complete.
154
+ */
155
+ copyContent(): Promise<void>;
156
+ /**
157
+ * Show a brief "copied" feedback on the copy button.
158
+ */
159
+ private showCopyFeedback;
160
+ /**
161
+ * Open the search bar and focus the input.
162
+ */
163
+ openSearch(): void;
164
+ /**
165
+ * Close the search bar and clear highlights.
166
+ */
167
+ closeSearch(): void;
168
+ /**
169
+ * Toggle search bar visibility.
170
+ */
171
+ toggleSearch(): void;
172
+ /**
173
+ * Navigate to the next search match.
174
+ */
175
+ searchNext(): void;
176
+ /**
177
+ * Navigate to the previous search match.
178
+ */
179
+ searchPrev(): void;
180
+ /**
181
+ * Get the search state (for programmatic access / testing).
182
+ */
183
+ getSearchState(): SearchState;
184
+ /**
185
+ * Programmatically search for a keyword.
186
+ * Highlights all matches and scrolls to the first one, but does NOT
187
+ * open/show the search bar UI. The search input is always kept in sync.
188
+ *
189
+ * @param query - The keyword to search for. Pass an empty string to clear.
190
+ * @param options - Optional settings. `caseSensitive` defaults to the
191
+ * current case-sensitivity state.
192
+ */
193
+ search(query: string, options?: {
194
+ caseSensitive?: boolean;
195
+ }): void;
196
+ /**
197
+ * Clear the current search state and remove all highlights.
198
+ * The search input is cleared but the search bar visibility is unchanged.
199
+ */
200
+ clearSearch(): void;
201
+ /**
202
+ * Handle input in the search field.
203
+ */
204
+ private onSearchInput;
205
+ /**
206
+ * Toggle case sensitivity and re-search.
207
+ */
208
+ private toggleCaseSensitive;
209
+ /**
210
+ * Update the search info label (e.g. "3 of 12" or "No results").
211
+ */
212
+ private updateSearchInfo;
213
+ /**
214
+ * Ensure the line containing a match is visible (unfold if needed)
215
+ * and scroll to it.
216
+ */
217
+ private revealAndScrollToMatch;
218
+ /**
219
+ * After render, scroll to the current search match element.
220
+ */
221
+ private scrollToCurrentMatch;
222
+ /**
223
+ * Open the filter bar and focus the input.
224
+ */
225
+ openFilter(): void;
226
+ /**
227
+ * Close the filter bar (filters remain active).
228
+ */
229
+ closeFilter(): void;
230
+ /**
231
+ * Toggle filter bar visibility.
232
+ */
233
+ toggleFilter(): void;
234
+ /**
235
+ * Set keys to filter out (replaces existing filters). Re-renders.
236
+ */
237
+ setFilterKeys(keys: string[]): void;
238
+ /**
239
+ * Add a single key to the filter. Re-renders.
240
+ */
241
+ addFilterKey(key: string): void;
242
+ /**
243
+ * Remove a single key from the filter. Re-renders.
244
+ */
245
+ removeFilterKey(key: string): void;
246
+ /**
247
+ * Get currently filtered keys.
248
+ */
249
+ getFilterKeys(): string[];
250
+ /**
251
+ * Get all keys detected in the current content.
252
+ */
253
+ getAvailableKeys(): string[];
254
+ /**
255
+ * Clear all key filters. Re-renders.
256
+ */
257
+ clearFilter(): void;
258
+ /**
259
+ * Get the filter state (for programmatic access / testing).
260
+ */
261
+ getFilterState(): FilterState;
262
+ /**
263
+ * Get the content with filtered lines removed.
264
+ * Returns lines that are not hidden by the current key filter.
265
+ */
266
+ getFilteredContent(): string;
267
+ /**
268
+ * Copy the filtered content (lines with filtered keys removed) to clipboard.
269
+ * Shows "✓" feedback on the filter copy button for 2 seconds.
270
+ */
271
+ copyFilteredContent(): Promise<void>;
272
+ /**
273
+ * Show a brief "copied" feedback on the filter copy button.
274
+ */
275
+ private showFilterCopyFeedback;
276
+ /**
277
+ * Build the filter bar DOM (hidden by default).
278
+ */
279
+ private buildFilterBar;
280
+ /**
281
+ * Handle input in the filter field — show autocomplete dropdown.
282
+ */
283
+ private onFilterInput;
284
+ /**
285
+ * Handle keyboard events in the filter input.
286
+ */
287
+ private handleFilterKeyDown;
288
+ /**
289
+ * Update the visual highlight on the dropdown items.
290
+ */
291
+ private updateFilterDropdownHighlight;
292
+ /**
293
+ * Show the autocomplete dropdown with suggestions.
294
+ */
295
+ private showFilterDropdown;
296
+ /**
297
+ * Hide the autocomplete dropdown.
298
+ */
299
+ private hideFilterDropdown;
300
+ /**
301
+ * Rebuild the filter tag chips in the filter bar.
302
+ */
303
+ private updateFilterTags;
304
+ /**
305
+ * Update the filter info label.
306
+ */
307
+ private updateFilterInfo;
308
+ /**
309
+ * Update filter toolbar button active state.
310
+ */
311
+ private updateFilterBtnState;
312
+ /**
313
+ * Set the Elixir data content and render it.
314
+ */
315
+ setContent(code: string): void;
316
+ /**
317
+ * Set a callback to be called after each render (for testing/animation).
318
+ */
319
+ onRender(callback: () => void): void;
320
+ /**
321
+ * Post-process highlight tokens: replace CSS classes for spans that fall
322
+ * within pre-processed inspect literal ranges (e.g. #Reference<...>)
323
+ * so they render with the inspect-literal style instead of atom style.
324
+ */
325
+ private fixInspectLiteralTokenClasses;
326
+ /**
327
+ * Check if an offset falls within a pre-processed inspect literal and
328
+ * return the literal if so, or null.
329
+ */
330
+ private findInspectLiteral;
331
+ /**
332
+ * Build line offset table for mapping offsets to line-relative positions.
333
+ */
334
+ private buildLineOffsets;
335
+ /**
336
+ * Full re-render of the viewer.
337
+ */
338
+ private render;
339
+ /**
340
+ * Create a single line element with gutter and code content.
341
+ */
342
+ private createLineElement;
343
+ /**
344
+ * Render a normal highlighted line, with search match highlighting.
345
+ */
346
+ private renderHighlightedLine;
347
+ /**
348
+ * Render tokenized text without search highlights.
349
+ */
350
+ private renderTokenizedText;
351
+ /**
352
+ * Render a line with both syntax tokens and search highlight overlays.
353
+ * Search highlights take precedence visually (wrapping around tokens).
354
+ */
355
+ private renderWithSearchHighlights;
356
+ /**
357
+ * Render a folded line: shows the opening line content followed by … and closing bracket.
358
+ */
359
+ private renderFoldedLine;
360
+ /**
361
+ * Handle mouseover on spans to resolve inspect target and apply highlight.
362
+ */
363
+ private handleInspectHover;
364
+ /**
365
+ * Handle mouseout — clear highlight when leaving the scroll area.
366
+ */
367
+ private handleInspectOut;
368
+ /**
369
+ * Register a callback invoked when the user clicks an inspectable value.
370
+ * The callback receives an InspectEvent with type, copyText, DOM element,
371
+ * and a preventDefault() method to suppress the default copy behavior.
372
+ *
373
+ * Pass `null` to unregister the callback and restore default behavior.
374
+ */
375
+ onInspect(callback: ((event: InspectEvent) => void) | null): void;
376
+ /**
377
+ * Handle click on an inspected token — copy to clipboard (unless prevented by callback).
378
+ */
379
+ private handleInspectClick;
380
+ /**
381
+ * Apply highlight CSS classes to all spans within the inspect target range.
382
+ */
383
+ private applyInspectHighlight;
384
+ /**
385
+ * Add a CSS class to all spans whose data-from/data-to overlap the given range.
386
+ */
387
+ private highlightSpansInRange;
388
+ /**
389
+ * Clear all inspect highlight classes.
390
+ */
391
+ private clearInspectHighlight;
392
+ /**
393
+ * Flash animation on currently highlighted elements.
394
+ */
395
+ private flashInspectHighlight;
396
+ /**
397
+ * Show a small floating "Copied!" toast near the mouse click position.
398
+ */
399
+ private showInspectToast;
400
+ /**
401
+ * Copy text to clipboard with fallback.
402
+ */
403
+ private copyToClipboard;
404
+ /**
405
+ * Convert an absolute character offset to a 0-indexed line number.
406
+ */
407
+ private offsetToLine;
408
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Search functionality for ElixirDataViewer.
3
+ * Manages text search, match tracking, and navigation.
4
+ */
5
+ /**
6
+ * Represents a single search match in the code.
7
+ */
8
+ export interface SearchMatch {
9
+ /** 0-indexed line number */
10
+ line: number;
11
+ /** Column offset within the line (0-based) */
12
+ from: number;
13
+ /** Column end offset within the line (0-based, exclusive) */
14
+ to: number;
15
+ }
16
+ /**
17
+ * Manages search state: query, matches, current index.
18
+ */
19
+ export declare class SearchState {
20
+ private query;
21
+ private caseSensitive;
22
+ private matches;
23
+ private currentIndex;
24
+ /**
25
+ * Perform a search across all lines.
26
+ * Returns true if matches changed.
27
+ */
28
+ search(lines: string[], query: string, caseSensitive: boolean): boolean;
29
+ /**
30
+ * Clear search state.
31
+ */
32
+ clear(): void;
33
+ /**
34
+ * Get the current search query.
35
+ */
36
+ getQuery(): string;
37
+ /**
38
+ * Get whether case-sensitive mode is active.
39
+ */
40
+ isCaseSensitive(): boolean;
41
+ /**
42
+ * Get all matches.
43
+ */
44
+ getMatches(): readonly SearchMatch[];
45
+ /**
46
+ * Get matches for a specific line.
47
+ */
48
+ getLineMatches(lineIdx: number): SearchMatch[];
49
+ /**
50
+ * Get the current match index (0-based), or -1 if no matches.
51
+ */
52
+ getCurrentIndex(): number;
53
+ /**
54
+ * Get the current match, or undefined if none.
55
+ */
56
+ getCurrentMatch(): SearchMatch | undefined;
57
+ /**
58
+ * Get total number of matches.
59
+ */
60
+ getMatchCount(): number;
61
+ /**
62
+ * Move to the next match. Wraps around.
63
+ */
64
+ next(): SearchMatch | undefined;
65
+ /**
66
+ * Move to the previous match. Wraps around.
67
+ */
68
+ prev(): SearchMatch | undefined;
69
+ /**
70
+ * Set the current match to the nearest one at or after the given line.
71
+ * Used when opening search to start from the visible area.
72
+ */
73
+ setCurrentToLine(lineIdx: number): void;
74
+ /**
75
+ * Check if a match at the given line/from is the current match.
76
+ */
77
+ isCurrentMatch(lineIdx: number, from: number): boolean;
78
+ /**
79
+ * Has active search query.
80
+ */
81
+ isActive(): boolean;
82
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Standalone entry point for Phoenix / vendor usage.
3
+ *
4
+ * Produces an ESM module with a default export of ElixirDataViewer,
5
+ * plus all named exports from the library. CSS is injected
6
+ * automatically via the vite-plugin-css-injected-by-js plugin.
7
+ *
8
+ * Usage in Phoenix app.js:
9
+ * import ElixirDataViewer from "../vendor/elixir-data-viewer"
10
+ */
11
+ import "./styles/theme.css";
12
+ export * from "./index";
13
+ export { ElixirDataViewer as default } from "./renderer";
@@ -0,0 +1,61 @@
1
+ import type { FoldRegion } from "./fold";
2
+ /**
3
+ * Manages the fold state for the viewer — which lines are folded.
4
+ */
5
+ export declare class FoldState {
6
+ /** Set of startLine indices that are currently folded */
7
+ private foldedLines;
8
+ /** Map from startLine to FoldRegion */
9
+ private regionMap;
10
+ /** All fold regions sorted by startLine */
11
+ private regions;
12
+ /**
13
+ * Update the fold regions (called when content changes).
14
+ * Clears all fold state.
15
+ */
16
+ setRegions(regions: FoldRegion[], regionMap: Map<number, FoldRegion>): void;
17
+ /**
18
+ * Toggle a fold at the given startLine.
19
+ */
20
+ toggle(startLine: number): void;
21
+ /**
22
+ * Check if a given startLine is folded.
23
+ */
24
+ isFolded(startLine: number): boolean;
25
+ /**
26
+ * Get the FoldRegion for a startLine, if any.
27
+ */
28
+ getRegion(startLine: number): FoldRegion | undefined;
29
+ /**
30
+ * Check if a given line is hidden due to being inside a folded region.
31
+ * Returns the folded parent region if hidden, or undefined.
32
+ */
33
+ isLineHidden(line: number): FoldRegion | undefined;
34
+ /**
35
+ * Check if a line has a fold indicator (is the start of a foldable region).
36
+ */
37
+ isFoldable(line: number): boolean;
38
+ /**
39
+ * Get all regions.
40
+ */
41
+ getRegions(): FoldRegion[];
42
+ /**
43
+ * Fold all regions.
44
+ */
45
+ foldAll(): void;
46
+ /**
47
+ * Unfold all regions.
48
+ */
49
+ unfoldAll(): void;
50
+ /**
51
+ * Fold all regions whose nesting depth exceeds maxLevel.
52
+ * Level 1 = outermost structures. foldToLevel(3) shows levels 1–3 expanded,
53
+ * level 4+ folded. foldToLevel(0) or negative values unfold all.
54
+ */
55
+ foldToLevel(maxLevel: number): void;
56
+ /**
57
+ * Reveal a specific line by unfolding any region that hides it.
58
+ * This ensures the line becomes visible in the rendered output.
59
+ */
60
+ revealLine(line: number): void;
61
+ }
package/dist/style.css ADDED
@@ -0,0 +1 @@
1
+ .edv-container{position:relative;background:#1a1b26;color:#c0caf5;font-family:Cascadia Code,Fira Code,Consolas,Courier New,monospace;font-size:14px;line-height:20px;height:100%;overflow:hidden;display:flex;flex-direction:column;tab-size:2;-moz-tab-size:2}.edv-inner{flex:1;min-height:0;overflow:auto}.edv-scroll{min-width:fit-content;padding-bottom:20px}.edv-line{display:flex;min-height:20px}.edv-line:hover{background:#292e42;outline:1px dashed #3b4261}.edv-gutter{display:flex;align-items:center;flex-shrink:0;padding-right:8px;padding-left:4px;user-select:none;-webkit-user-select:none;gap:2px}.edv-line-number{color:#3b4261;text-align:right;min-width:2.5em;padding-right:4px;font-variant-numeric:tabular-nums}.edv-line:hover .edv-line-number{color:#737aa2}.edv-fold-indicator{width:16px;height:20px;display:inline-flex;align-items:center;justify-content:center;font-size:10px;color:transparent;flex-shrink:0}.edv-fold-indicator.edv-foldable{cursor:pointer;color:#565f89;opacity:0;transition:opacity .15s ease}.edv-line:hover .edv-fold-indicator.edv-foldable,.edv-fold-indicator.edv-foldable[data-folded=true]{opacity:1}.edv-code{flex:1;white-space:pre;padding-left:8px;min-height:20px;border-left:1px solid #292e42}.edv-word-wrap .edv-scroll{min-width:unset}.edv-word-wrap .edv-code{white-space:pre-wrap;word-break:break-all;overflow-wrap:anywhere}.edv-word-wrap .edv-line{align-items:flex-start}.edv-fold-ellipsis{display:inline-block;background:#292e42;color:#7aa2f7;padding:0 5px;margin:0 2px;border-radius:3px;font-size:11px;cursor:pointer;border:1px solid #3b4261;line-height:16px;vertical-align:middle;white-space:nowrap}.edv-fold-ellipsis:hover{background:#33394e;color:#7aa2f7;border-color:#7aa2f7}.tok-atom{color:#2ac3de}.tok-namespace{color:#7dcfff}.tok-bool{color:#ff9e64}.tok-null{color:#2ac3de}.tok-number{color:#ff9e64}.tok-character{color:#9ece6a}.tok-variableName{color:#c0caf5}.tok-function,.tok-definition{color:#7aa2f7}.tok-special{color:#f7768e}.tok-string{color:#9ece6a}.tok-escape,.tok-keyword{color:#bb9af7}.tok-operator{color:#89ddff}.tok-comment{color:#565f89;font-style:italic}.tok-inspect-literal{color:#bb9af7}.tok-underscore{color:#565f89;font-style:italic}.tok-punctuation{color:#a9b1d6}.tok-separator,.tok-angleBracket{color:#89ddff}.tok-attributeName{color:#7dcfff}.tok-docString{color:#e0af68}.edv-inner::-webkit-scrollbar{width:10px;height:10px}.edv-inner::-webkit-scrollbar-track{background:#1a1b26}.edv-inner::-webkit-scrollbar-thumb{background:#3b4261;border-radius:5px}.edv-inner::-webkit-scrollbar-thumb:hover{background:#565f89}.edv-inner::-webkit-scrollbar-corner{background:#1a1b26}.edv-toolbar{position:absolute;top:4px;right:14px;z-index:10;display:flex;gap:2px;background:#1a1b26;border:1px solid #3b4261;border-radius:4px;padding:2px;opacity:0;transition:opacity .2s ease;pointer-events:none}.edv-container:hover .edv-toolbar{opacity:1;pointer-events:auto}.edv-toolbar-btn{background:transparent;border:1px solid transparent;color:#a9b1d6;width:26px;height:24px;border-radius:3px;cursor:pointer;font-size:16px;display:flex;align-items:center;justify-content:center;font-family:inherit;padding:0;line-height:1}.edv-toolbar-btn:hover{background:#292e42;border-color:#565f89;color:#c0caf5}.edv-toolbar-btn.edv-toolbar-btn--active{background:#292e42;border-color:#7aa2f7;color:#7aa2f7}.edv-search-bar{flex-shrink:0;z-index:20;display:none;align-items:center;gap:6px;padding:4px 8px;background:#1a1b26;border-bottom:1px solid #3b4261;font-size:13px}.edv-search-bar--visible{display:flex}.edv-search-input-wrapper{display:flex;align-items:center;background:#1a1b26;border:1px solid #3b4261;border-radius:3px;overflow:hidden}.edv-search-input-wrapper:focus-within{border-color:#7aa2f7}.edv-search-input{background:transparent;border:none;outline:none;color:#c0caf5;font-family:inherit;font-size:13px;padding:3px 6px;width:180px}.edv-search-input::placeholder{color:#565f89}.edv-search-case-btn{background:transparent;border:1px solid transparent;color:#565f89;cursor:pointer;font-size:12px;font-family:inherit;padding:2px 6px;border-radius:2px;margin-right:2px;line-height:1}.edv-search-case-btn:hover{color:#c0caf5;background:#292e42;border-color:#565f89}.edv-search-case-btn--active{color:#7aa2f7;background:#292e42;border-color:#7aa2f7}.edv-search-case-btn--active:hover{border-color:#c0caf5}.edv-search-info{color:#c0caf5;font-size:12px;min-width:70px;text-align:center;white-space:nowrap}.edv-search-info--no-results{color:#f7768e}.edv-search-nav-btn{background:transparent;border:1px solid transparent;color:#a9b1d6;cursor:pointer;width:24px;height:24px;border-radius:3px;font-size:14px;display:flex;align-items:center;justify-content:center;font-family:inherit;padding:0;line-height:1}.edv-search-nav-btn:hover{background:#292e42;border-color:#565f89}.edv-search-match{background:#e0af6866;border-radius:2px;color:inherit}.edv-search-current{background:#e0af68cc;border-radius:2px;color:#1a1b26}.edv-line--has-match .edv-line-number{color:#e0af68}.edv-code [data-from]{cursor:pointer}.edv-inspect-token{background:#7aa2f733;border-radius:2px;outline:1px solid rgba(122,162,247,.4)}.edv-line.edv-inspect-line{background:#7aa2f70f}.edv-inspect-bracket{background:#7aa2f733;border-radius:2px;outline:1px solid rgba(122,162,247,.35)}@keyframes edv-inspect-flash{0%{background-color:#7aa2f773}to{background-color:#7aa2f733}}.edv-inspect-copied{animation:edv-inspect-flash .4s ease-out}.edv-copied-toast{position:fixed;background:#292e42;color:#7aa2f7;border:1px solid #7aa2f7;border-radius:4px;padding:2px 8px;font-size:12px;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;pointer-events:none;z-index:100;animation:edv-toast-fade 1.2s ease-out forwards}@keyframes edv-toast-fade{0%{opacity:1;transform:translateY(0)}70%{opacity:1}to{opacity:0;transform:translateY(-10px)}}.edv-filter-bar{flex-shrink:0;z-index:20;display:none;align-items:center;gap:6px;padding:4px 8px;background:#1a1b26;border-bottom:1px solid #3b4261;font-size:13px;flex-wrap:wrap}.edv-filter-bar--visible{display:flex}.edv-filter-input-wrapper{position:relative;display:flex;align-items:center;background:#1a1b26;border:1px solid #3b4261;border-radius:3px;overflow:visible}.edv-filter-input-wrapper:focus-within{border-color:#7aa2f7}.edv-filter-input{background:transparent;border:none;outline:none;color:#c0caf5;font-family:inherit;font-size:13px;padding:3px 6px;width:140px}.edv-filter-input::placeholder{color:#565f89}.edv-filter-tags{display:flex;gap:4px;flex-wrap:wrap;align-items:center}.edv-filter-tag{display:inline-flex;align-items:center;gap:2px;background:#292e42;border:1px solid #3b4261;border-radius:3px;padding:1px 4px 1px 6px;font-size:12px;color:#7aa2f7;white-space:nowrap;line-height:18px}.edv-filter-tag-label{color:#7aa2f7}.edv-filter-tag-remove{background:transparent;border:none;color:#565f89;cursor:pointer;font-size:10px;padding:0 2px;line-height:1;display:flex;align-items:center;justify-content:center;border-radius:2px}.edv-filter-tag-remove:hover{color:#f7768e;background:#f7768e26}.edv-filter-info{color:#c0caf5;font-size:12px;white-space:nowrap;margin-left:auto}.edv-filter-dropdown{display:none;position:absolute;top:100%;left:-1px;right:-1px;background:#1a1b26;border:1px solid #3b4261;border-top:none;border-radius:0 0 3px 3px;max-height:160px;overflow-y:auto;z-index:30}.edv-filter-dropdown--visible{display:block}.edv-filter-dropdown-item{padding:4px 8px;cursor:pointer;font-size:13px;color:#c0caf5;white-space:nowrap}.edv-filter-dropdown-item:hover,.edv-filter-dropdown-item--active{background:#292e42;color:#7aa2f7}.edv-filter-dropdown::-webkit-scrollbar{width:6px}.edv-filter-dropdown::-webkit-scrollbar-track{background:#1a1b26}.edv-filter-dropdown::-webkit-scrollbar-thumb{background:#3b4261;border-radius:3px}