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.
- embedgl/THIRD_PARTY_LICENSES.txt +157 -0
- embedgl/__init__.py +5 -0
- embedgl/demo.py +72 -0
- embedgl/query.py +116 -0
- embedgl/static/widget.css +1 -0
- embedgl/static/widget.js +32121 -0
- embedgl/widget.py +378 -0
- embedgl-0.0.1.dist-info/METADATA +212 -0
- embedgl-0.0.1.dist-info/RECORD +12 -0
- embedgl-0.0.1.dist-info/WHEEL +5 -0
- embedgl-0.0.1.dist-info/licenses/LICENSE +201 -0
- embedgl-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -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
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}}
|