embedgl 0.0.1__py3-none-any.whl

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,157 @@
1
+ Third-party licenses for the bundled embedgl frontend.
2
+
3
+ ========================================================================
4
+ kdbush@4.1.0
5
+ License: ISC
6
+
7
+ LICENSE
8
+ ISC License
9
+
10
+ Copyright (c) 2026, Vladimir Agafonkin
11
+
12
+ Permission to use, copy, modify, and/or distribute this software for any purpose
13
+ with or without fee is hereby granted, provided that the above copyright notice
14
+ and this permission notice appear in all copies.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
17
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
18
+ FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
19
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
20
+ OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
21
+ TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
22
+ THIS SOFTWARE.
23
+
24
+
25
+ ========================================================================
26
+ lucide-react@0.468.0
27
+ License: ISC
28
+
29
+ LICENSE
30
+ ISC License
31
+
32
+ Copyright (c) for portions of Lucide are held by Cole Bemis 2013-2022 as part of Feather (MIT). All other copyright (c) for Lucide are held by Lucide Contributors 2022.
33
+
34
+ Permission to use, copy, modify, and/or distribute this software for any
35
+ purpose with or without fee is hereby granted, provided that the above
36
+ copyright notice and this permission notice appear in all copies.
37
+
38
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
39
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
40
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
41
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
42
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
43
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
44
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
45
+
46
+
47
+ ========================================================================
48
+ react@19.3.1
49
+ License: MIT
50
+
51
+ LICENSE
52
+ MIT License
53
+
54
+ Copyright (c) Meta Platforms, Inc. and affiliates.
55
+
56
+ Permission is hereby granted, free of charge, to any person obtaining a copy
57
+ of this software and associated documentation files (the "Software"), to deal
58
+ in the Software without restriction, including without limitation the rights
59
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
60
+ copies of the Software, and to permit persons to whom the Software is
61
+ furnished to do so, subject to the following conditions:
62
+
63
+ The above copyright notice and this permission notice shall be included in all
64
+ copies or substantial portions of the Software.
65
+
66
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
67
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
68
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
69
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
70
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
71
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
72
+ SOFTWARE.
73
+
74
+
75
+ ========================================================================
76
+ react-dom@19.3.1
77
+ License: MIT
78
+
79
+ LICENSE
80
+ MIT License
81
+
82
+ Copyright (c) Meta Platforms, Inc. and affiliates.
83
+
84
+ Permission is hereby granted, free of charge, to any person obtaining a copy
85
+ of this software and associated documentation files (the "Software"), to deal
86
+ in the Software without restriction, including without limitation the rights
87
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
88
+ copies of the Software, and to permit persons to whom the Software is
89
+ furnished to do so, subject to the following conditions:
90
+
91
+ The above copyright notice and this permission notice shall be included in all
92
+ copies or substantial portions of the Software.
93
+
94
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
95
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
96
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
97
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
98
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
99
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
100
+ SOFTWARE.
101
+
102
+
103
+ ========================================================================
104
+ scheduler@0.28.0
105
+ License: MIT
106
+
107
+ LICENSE
108
+ MIT License
109
+
110
+ Copyright (c) Meta Platforms, Inc. and affiliates.
111
+
112
+ Permission is hereby granted, free of charge, to any person obtaining a copy
113
+ of this software and associated documentation files (the "Software"), to deal
114
+ in the Software without restriction, including without limitation the rights
115
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
116
+ copies of the Software, and to permit persons to whom the Software is
117
+ furnished to do so, subject to the following conditions:
118
+
119
+ The above copyright notice and this permission notice shall be included in all
120
+ copies or substantial portions of the Software.
121
+
122
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
123
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
124
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
125
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
126
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
127
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
128
+ SOFTWARE.
129
+
130
+
131
+ ========================================================================
132
+ three@0.174.0
133
+ License: MIT
134
+
135
+ LICENSE
136
+ The MIT License
137
+
138
+ Copyright © 2010-2025 three.js authors
139
+
140
+ Permission is hereby granted, free of charge, to any person obtaining a copy
141
+ of this software and associated documentation files (the "Software"), to deal
142
+ in the Software without restriction, including without limitation the rights
143
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
144
+ copies of the Software, and to permit persons to whom the Software is
145
+ furnished to do so, subject to the following conditions:
146
+
147
+ The above copyright notice and this permission notice shall be included in
148
+ all copies or substantial portions of the Software.
149
+
150
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
151
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
152
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
153
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
154
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
155
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
156
+ THE SOFTWARE.
157
+
embedgl/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """A Polars-backed notebook point-cloud explorer."""
2
+
3
+ from .widget import EmbeddingViewer
4
+
5
+ __all__ = ["EmbeddingViewer"]
embedgl/demo.py ADDED
@@ -0,0 +1,72 @@
1
+ """Small projected document dataset for trying the widget without loading receipt models."""
2
+
3
+ import io
4
+ from datetime import date, timedelta
5
+
6
+ import numpy as np
7
+ import polars as pl
8
+ from PIL import Image, ImageDraw
9
+
10
+ from .widget import EmbeddingViewer
11
+
12
+
13
+ def make_demo_frame(count: int = 2000, seed: int = 42) -> pl.DataFrame:
14
+ if count < 1:
15
+ raise ValueError("count must be positive")
16
+ generator = np.random.default_rng(seed)
17
+ merchants = ["Northline Market", "Corner Coffee", "Field Supply", "Paper House"]
18
+ categories = ["Groceries", "Dining", "Supplies", "Stationery"]
19
+ samples: list[bytes] = []
20
+ for template in range(24):
21
+ image = Image.new("RGB", (192, 256), "#fafaf5")
22
+ draw = ImageDraw.Draw(image)
23
+ draw.rectangle((8, 8, 183, 247), outline="#d9ddd5")
24
+ draw.text((20, 23), merchants[template % 4], fill="#253a3a")
25
+ draw.text((20, 45), f"RECEIPT #{template + 1001}", fill="#57605b")
26
+ draw.line((20, 66, 172, 66), fill="#bbc6bd")
27
+ for line in range(5):
28
+ draw.text(
29
+ (20, 80 + line * 20), f"Item {line + 1} {(template + line + 2) * 1.25:6.2f}", fill="#3e4640"
30
+ )
31
+ draw.line((20, 187, 172, 187), fill="#bbc6bd")
32
+ draw.text((20, 201), f"TOTAL {(template + 5) * 6.25:6.2f}", fill="#253a3a")
33
+ draw.text((20, 225), "Thank you", fill="#6b7c73")
34
+ output = io.BytesIO()
35
+ image.save(output, format="JPEG", quality=80)
36
+ samples.append(output.getvalue())
37
+ labels = generator.integers(0, 4, size=count)
38
+ centers = np.array([[-2, -1], [1.5, 1], [-1, 2], [2, -2]])
39
+ coordinates = centers[labels] + generator.normal(0, 0.55, (count, 2))
40
+ templates = generator.integers(0, len(samples), size=count)
41
+ amounts = generator.uniform(3, 250, size=count).round(2)
42
+ return pl.DataFrame(
43
+ {
44
+ "id": [f"demo-{index:06d}" for index in range(count)],
45
+ "file_name": [f"receipt_{index:06d}.jpg" for index in range(count)],
46
+ "merchant": [merchants[label] for label in labels],
47
+ "category": [categories[label] for label in labels],
48
+ "amount": amounts,
49
+ "date": [date(2026, 1, 1) + timedelta(days=int(day)) for day in generator.integers(0, 300, size=count)],
50
+ "reviewed": generator.random(count) > 0.5,
51
+ "ocr_text": [
52
+ f"{merchants[label]} receipt total {amount:.2f}" for label, amount in zip(labels, amounts, strict=True)
53
+ ],
54
+ "thumbnail": [samples[template] for template in templates],
55
+ "x": coordinates[:, 0],
56
+ "y": coordinates[:, 1],
57
+ "z": generator.normal(0, 0.6, count) + labels * 0.6,
58
+ }
59
+ )
60
+
61
+
62
+ def make_demo(count: int = 2000) -> EmbeddingViewer:
63
+ return EmbeddingViewer(
64
+ make_demo_frame(count),
65
+ id_column="id",
66
+ coordinates_2d=("x", "y"),
67
+ coordinates_3d=("x", "y", "z"),
68
+ cluster_column="category",
69
+ searchable_columns=("file_name", "merchant", "ocr_text"),
70
+ filterable_columns=("merchant", "category", "amount", "date", "reviewed"),
71
+ metadata_columns=("file_name", "merchant", "category", "amount", "date", "reviewed", "ocr_text"),
72
+ )
embedgl/query.py ADDED
@@ -0,0 +1,116 @@
1
+ import math
2
+ from datetime import date, datetime, time
3
+ from decimal import Decimal
4
+ from typing import Any
5
+
6
+ import polars as pl
7
+
8
+
9
+ def scalar(value: object) -> str | int | float | bool | None:
10
+ if isinstance(value, (datetime, date, time)):
11
+ return value.isoformat()
12
+ if isinstance(value, (Decimal, int)) and not isinstance(value, bool):
13
+ return str(value) if isinstance(value, Decimal) or abs(value) > 2**53 - 1 else value
14
+ if isinstance(value, float):
15
+ return value if math.isfinite(value) else None
16
+ if value is None or isinstance(value, (str, bool)):
17
+ return value
18
+ return str(value)
19
+
20
+
21
+ def field_kind(dtype: pl.DataType) -> str:
22
+ if dtype.is_numeric():
23
+ return "number"
24
+ if dtype == pl.Boolean:
25
+ return "boolean"
26
+ if dtype == pl.Date or isinstance(dtype, pl.Datetime):
27
+ return "date"
28
+ if dtype == pl.String or dtype == pl.Categorical or isinstance(dtype, pl.Enum):
29
+ return "string"
30
+ raise ValueError(f"Unsupported filter dtype: {dtype}. Flatten nested fields before using them as filters.")
31
+
32
+
33
+ def typed_value(value: object, dtype: pl.DataType) -> str | int | float | bool | date | datetime:
34
+ if value is None:
35
+ raise ValueError("Use is_null or not_null to filter null values")
36
+ if dtype == pl.Date:
37
+ return date.fromisoformat(str(value))
38
+ if isinstance(dtype, pl.Datetime):
39
+ parsed = datetime.fromisoformat(str(value))
40
+ if bool(parsed.tzinfo) != bool(dtype.time_zone):
41
+ raise ValueError("Datetime filter timezone must match the column timezone")
42
+ return parsed
43
+ if dtype == pl.Boolean and not isinstance(value, bool):
44
+ raise ValueError("Boolean filters require true or false")
45
+ if dtype.is_numeric():
46
+ return pl.Series([value]).cast(dtype, strict=True).item()
47
+ if isinstance(value, (str, int, float, bool, date, datetime)):
48
+ return value
49
+ raise ValueError("Filter values must be scalar")
50
+
51
+
52
+ def evaluate(
53
+ frame: pl.DataFrame,
54
+ index_column: str,
55
+ text: str,
56
+ filters: list[dict[str, Any]],
57
+ searchable: list[str],
58
+ filterable: list[str],
59
+ ) -> pl.Series:
60
+ if len(text) > 4096 or len(filters) > 32:
61
+ raise ValueError("Search is limited to 4096 characters and 32 filters")
62
+ predicates: list[pl.Expr] = []
63
+ if text:
64
+ if not searchable:
65
+ raise ValueError("No searchable columns are configured")
66
+ predicates.append(
67
+ pl.any_horizontal(
68
+ [
69
+ pl.col(column)
70
+ .cast(pl.String)
71
+ .str.to_lowercase()
72
+ .str.contains(text.lower(), literal=True)
73
+ .fill_null(False)
74
+ for column in searchable
75
+ ]
76
+ )
77
+ )
78
+ for spec in filters:
79
+ column, operation = spec.get("column"), spec.get("op")
80
+ if column not in filterable:
81
+ raise ValueError(f"Column is not filterable: {column}")
82
+ dtype = frame.schema[column]
83
+ expression = pl.col(column)
84
+ kind = field_kind(dtype)
85
+ if operation == "is_null":
86
+ predicate = expression.is_null()
87
+ elif operation == "not_null":
88
+ predicate = expression.is_not_null()
89
+ elif operation == "contains" and kind == "string":
90
+ predicate = (
91
+ expression.cast(pl.String)
92
+ .str.to_lowercase()
93
+ .str.contains(
94
+ str(spec.get("value", "")).lower(),
95
+ literal=True,
96
+ )
97
+ )
98
+ elif operation == "in":
99
+ values = spec.get("value")
100
+ if not isinstance(values, list) or len(values) > 1000:
101
+ raise ValueError("Membership filters require at most 1000 values")
102
+ predicate = expression.is_in([typed_value(value, dtype) for value in values])
103
+ elif operation in {"eq", "gte", "lte"}:
104
+ value = pl.lit(typed_value(spec.get("value"), dtype))
105
+ if operation == "eq":
106
+ predicate = expression == value
107
+ elif operation == "gte" and kind in {"number", "date"}:
108
+ predicate = expression >= value
109
+ elif operation == "lte" and kind in {"number", "date"}:
110
+ predicate = expression <= value
111
+ else:
112
+ raise ValueError(f"Unsupported operation for {column}: {operation}")
113
+ else:
114
+ raise ValueError(f"Unsupported operation for {column}: {operation}")
115
+ predicates.append(predicate.fill_null(False))
116
+ return frame.filter(pl.all_horizontal(predicates) if predicates else pl.lit(True)).get_column(index_column)
@@ -0,0 +1 @@
1
+ .ev-root{--ev-ink: #253a3a;--ev-muted: #6a7b7b;--ev-line: #dce5e4;--ev-accent: #147d6c;--ev-soft: #eaf5f1;--ev-gold: #efb146;font:13px/1.45 IBM Plex Sans,Segoe UI,sans-serif;letter-spacing:0;color:var(--ev-ink);background:#fff;border:1px solid var(--ev-line);display:flex;flex-direction:column;min-width:0;width:100%;overflow:hidden;isolation:isolate;text-align:left;color-scheme:light}.ev-root *,.ev-root *:before,.ev-root *:after{box-sizing:border-box}.ev-root button,.ev-root input,.ev-root select{font:inherit;letter-spacing:0;color:inherit}.ev-root button{cursor:pointer}.ev-root button:disabled{opacity:.4;cursor:default}.ev-root button:focus-visible,.ev-root input:focus-visible,.ev-root select:focus-visible{outline:2px solid var(--ev-accent);outline-offset:2px}.ev-root button{border:0;background:transparent}.ev-root h3{font-size:12px;line-height:1.4;font-weight:650;margin:0}.ev-header{min-height:48px;display:flex;align-items:center;gap:16px;padding:8px 14px;border-bottom:1px solid var(--ev-line)}.ev-brand{display:flex;align-items:center;gap:8px;min-width:0}.ev-brand svg{color:var(--ev-accent);flex-shrink:0}.ev-brand strong{font-size:15px;font-weight:600}.ev-dataset-count{color:var(--ev-muted);font-size:12px}.ev-live{font-size:11px;margin-left:auto;display:flex;align-items:center;gap:6px;white-space:nowrap}.ev-live:before{content:"";width:6px;height:6px;border-radius:50%;background:#279a74}.ev-live.is-error:before{background:#ca5656}.ev-toolbar{display:flex;align-items:center;flex-wrap:wrap;gap:4px;padding:8px 10px;border-bottom:1px solid var(--ev-line)}.ev-icon{width:30px;height:30px;flex:0 0 30px;display:inline-flex;align-items:center;justify-content:center;border-radius:4px;padding:0}.ev-icon:hover:not(:disabled){background:#edf2f1}.ev-icon.is-active{color:var(--ev-accent);background:var(--ev-soft)}.ev-search{height:32px;display:flex;align-items:center;gap:7px;padding:0 9px;border:1px solid var(--ev-line);border-radius:4px;flex:1 1 150px;min-width:100px;max-width:400px;margin-right:auto}.ev-search svg{flex-shrink:0;color:var(--ev-muted)}.ev-search input{min-width:0;width:100%;border:0;background:transparent;height:28px;outline:none}.ev-segment{display:flex;border:1px solid var(--ev-line);border-radius:4px;padding:2px;gap:2px}.ev-segment button{height:26px;min-width:34px;font-size:11px;border-radius:2px}.ev-segment button[aria-pressed=true]{background:var(--ev-ink);color:#fff}.ev-toolbar-divider{width:1px;height:20px;background:var(--ev-line);margin:0 3px}.ev-size{display:flex;align-items:center;gap:5px;font-size:11px;margin-left:4px;color:var(--ev-muted)}.ev-size input{width:65px;accent-color:var(--ev-accent)}.ev-workspace{display:grid;grid-template-columns:minmax(0,1fr) 240px;flex:1;min-height:0}.ev-workspace.with-filters{grid-template-columns:196px minmax(0,1fr) 240px}.ev-sidebar{border-right:1px solid var(--ev-line);overflow-y:auto;min-width:0}.ev-section-heading{display:flex;align-items:center;gap:7px;min-height:40px;padding:7px 12px}.ev-section-heading>.ev-icon:last-child,.ev-section-heading .ev-count{margin-left:auto}.ev-count{font-size:11px;color:var(--ev-muted);font-variant-numeric:tabular-nums}.ev-filter-editor form{display:grid;gap:9px;padding:0 12px 14px}.ev-filter-editor label{display:grid;gap:4px;font-size:11px;color:var(--ev-muted)}.ev-filter-editor select,.ev-filter-editor input{min-width:0;width:100%;background:#fff;border:1px solid var(--ev-line);border-radius:4px;padding:6px;font-size:12px;color:var(--ev-ink)}.ev-command{display:inline-flex;gap:6px;align-items:center;justify-content:center;min-height:30px;padding:5px 8px!important;border:1px solid var(--ev-line)!important;border-radius:4px;font-size:11px!important}.ev-command:hover:not(:disabled){background:var(--ev-soft);border-color:#9dc9bc!important}.ev-active-filters{display:grid;gap:5px;padding:0 8px}.ev-filter-chip{display:flex;gap:2px;align-items:center;background:var(--ev-soft);padding:5px 3px 5px 7px;border-radius:4px;margin-bottom:6px}.ev-filter-chip>span{flex:1;min-width:0;overflow-wrap:anywhere}.ev-filter-chip strong{display:block;font-size:11px;font-weight:600}.ev-filter-chip small{display:block;font-size:10px;color:var(--ev-muted)}.ev-field-error{font-size:11px;color:#ad3b46;padding:8px 12px;overflow-wrap:anywhere}.ev-legend{border-top:1px solid var(--ev-line);padding-bottom:12px}.ev-legend-item{display:flex;align-items:center;gap:7px;padding:3px 12px;font-size:11px;overflow-wrap:anywhere}.ev-legend-item>span{width:8px;height:8px;border-radius:2px;flex-shrink:0}.ev-muted{color:var(--ev-muted);font-size:11px;padding:6px 12px}.ev-stage{position:relative;overflow:hidden;min-width:0;min-height:160px;background:#f1f5f5}.ev-canvas{position:absolute;top:0;right:0;bottom:0;left:0}.ev-canvas canvas{display:block;width:100%;height:100%;touch-action:none}.ev-stage-status{position:absolute;bottom:8px;left:10px;right:10px;display:flex;justify-content:space-between;pointer-events:none;gap:8px;color:#5c7370;font-size:10px}.ev-stage-status>span{display:inline-flex;align-items:center;gap:4px;background:#f1f5f5df;padding:2px 4px;border-radius:2px}.ev-loading,.ev-stage-empty{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);display:flex;gap:8px;align-items:center;padding:10px 12px;background:#fffefb;font-size:12px;white-space:normal;max-width:90%;pointer-events:none}.ev-selection-box{position:absolute;background:#147d6c19;border:1px solid var(--ev-accent);pointer-events:none}.ev-hover{position:absolute;left:10px;bottom:35px;width:min(328px,calc(100% - 20px));max-height:calc(100% - 45px);overflow-y:auto;background:#fff;border:1px solid var(--ev-line);border-radius:6px;box-shadow:0 3px 16px #163c3020}.ev-hover .ev-section-heading{min-height:34px}.ev-hover-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:4px;padding:0 8px 8px}.ev-hover-grid button{padding:0;border-radius:3px;overflow:hidden}.ev-hover-grid button:hover{outline:2px solid var(--ev-accent)}.ev-hover-grid .ev-preview{aspect-ratio:1;width:100%;height:auto}.ev-preview{background:#f1f4f3;display:flex;align-items:center;justify-content:center;overflow:hidden;color:#a4b7b0;flex-shrink:0}.ev-preview img{width:100%;height:100%;object-fit:contain;display:block}.ev-results{border-left:1px solid var(--ev-line);display:flex;flex-direction:column;min-width:0;min-height:0;background:#fff}.ev-result-actions{padding:0 10px 9px;border-bottom:1px solid var(--ev-line)}.ev-result-scope{margin:0 10px 8px;flex-shrink:0}.ev-result-scope button{flex:1;min-width:0;overflow-wrap:anywhere;height:auto;min-height:26px}.ev-result-actions .ev-command{width:100%}.ev-row-list{flex:1;overflow-y:auto;min-height:0}.ev-row{display:flex;align-items:center;gap:7px;padding:6px 9px;border-bottom:1px solid #eef2f1}.ev-row.is-selected{background:#fff8e9}.ev-row>input{width:13px;height:13px;flex-shrink:0;accent-color:var(--ev-accent);margin:0}.ev-result-open{display:flex;align-items:center;gap:8px;padding:0;text-align:left;min-width:0;flex:1}.ev-result-open>span{min-width:0;flex:1}.ev-result-open .ev-preview{width:38px;height:44px;border-radius:3px}.ev-row-title,.ev-row-subtitle{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ev-row-title{font-size:11px;font-weight:500}.ev-row-subtitle{font-size:10px;color:var(--ev-muted);margin-top:3px}.ev-pagination{display:flex;align-items:center;justify-content:space-between;min-height:38px;padding:3px 8px;border-top:1px solid var(--ev-line);font-size:11px}.ev-detail{overflow-y:auto;padding:0 12px 12px}.ev-detail>.ev-preview{width:100%;height:240px;max-height:40vh;margin-bottom:10px}.ev-detail dl{font-size:11px;margin:12px 0 0}.ev-detail dt{color:var(--ev-muted);margin-top:10px}.ev-detail dd{margin:2px 0 0;white-space:pre-wrap;overflow-wrap:anywhere;max-height:180px;overflow:auto}.ev-empty{padding:30px 12px;text-align:center;color:var(--ev-muted);font-size:12px}.ev-footer{display:flex;gap:8px;align-items:center;min-height:34px;padding:2px 12px;border-top:1px solid var(--ev-line);font-size:11px;color:var(--ev-muted)}.ev-footer>span:first-child{display:flex;align-items:center;gap:6px}.ev-selected-dot{width:7px;height:7px;border-radius:50%;background:var(--ev-gold)}.ev-footer-total{margin-left:auto}.ev-error{display:flex;align-items:center;gap:6px;padding:6px 12px;background:#fff0ef;color:#a13942;font-size:12px;max-height:90px;overflow:auto}.ev-error>span{flex:1;overflow-wrap:anywhere}.ev-spin{animation:ev-spin 1.2s linear infinite;flex-shrink:0}@keyframes ev-spin{to{transform:rotate(360deg)}}@media(prefers-reduced-motion:reduce){.ev-spin{animation:none}}@container (max-width: 850px){.ev-workspace{grid-template-columns:minmax(0,1fr) 200px}.ev-workspace.with-filters{grid-template-columns:170px minmax(0,1fr) 200px}.ev-size{display:none}}.ev-root{container-type:inline-size}@container (max-width: 640px){.ev-dataset-count,.ev-live{display:none}.ev-workspace,.ev-workspace.with-filters{grid-template-columns:minmax(0,1fr);grid-template-rows:minmax(160px,1fr) 160px;position:relative}.ev-stage{grid-column:1;grid-row:1}.ev-results{grid-column:1;grid-row:2;border-left:0;border-top:1px solid var(--ev-line)}.ev-results>.ev-section-heading{min-height:28px;padding:1px 8px}.ev-result-actions{padding:0 8px 3px}.ev-result-actions .ev-command{min-height:22px;padding:1px 5px!important}.ev-sidebar{position:absolute;left:0;top:0;bottom:160px;width:min(196px,70%);z-index:3;background:#fff;box-shadow:4px 0 10px #143b2920}.ev-search{max-width:none}.ev-detail{display:flex;align-items:flex-start;gap:10px}.ev-detail>.ev-preview{width:90px;height:120px;flex-shrink:0}.ev-detail dl{margin:0}.ev-footer{gap:3px;padding:2px 8px;font-size:10px}}