dbxplay 0.1.0__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.
- dbxplay/__init__.py +13 -0
- dbxplay/core.py +288 -0
- dbxplay/icons.py +171 -0
- dbxplay/templates.py +1568 -0
- dbxplay-0.1.0.dist-info/METADATA +110 -0
- dbxplay-0.1.0.dist-info/RECORD +8 -0
- dbxplay-0.1.0.dist-info/WHEEL +5 -0
- dbxplay-0.1.0.dist-info/top_level.txt +1 -0
dbxplay/__init__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""
|
|
2
|
+
dbxplay — A Databricks-like interactive display() function
|
|
3
|
+
for Jupyter Notebooks, AWS Glue, Google Colab, and VS Code.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
from dbxplay import display
|
|
7
|
+
display(df)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from dbxplay.core import display
|
|
11
|
+
|
|
12
|
+
__version__ = "0.1.0"
|
|
13
|
+
__all__ = ["display"]
|
dbxplay/core.py
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
"""
|
|
2
|
+
core.py — Main display() function and DataFrame type detection/conversion.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import uuid
|
|
7
|
+
import html as html_module
|
|
8
|
+
import math
|
|
9
|
+
from typing import Any, Dict, List, Optional, Union
|
|
10
|
+
|
|
11
|
+
from dbxplay.templates import render_table_html
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _is_pyspark_dataframe(obj: Any) -> bool:
|
|
15
|
+
"""Check if the object is a PySpark DataFrame without importing PySpark."""
|
|
16
|
+
cls_name = type(obj).__name__
|
|
17
|
+
module = type(obj).__module__ or ""
|
|
18
|
+
return cls_name == "DataFrame" and "pyspark" in module
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _is_pandas_dataframe(obj: Any) -> bool:
|
|
22
|
+
"""Check if the object is a Pandas DataFrame."""
|
|
23
|
+
cls_name = type(obj).__name__
|
|
24
|
+
module = type(obj).__module__ or ""
|
|
25
|
+
return cls_name == "DataFrame" and "pandas" in module
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _is_polars_dataframe(obj: Any) -> bool:
|
|
29
|
+
"""Check if the object is a Polars DataFrame."""
|
|
30
|
+
cls_name = type(obj).__name__
|
|
31
|
+
module = type(obj).__module__ or ""
|
|
32
|
+
return cls_name == "DataFrame" and "polars" in module
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _infer_dtype_category(values: list, col_name: str = "") -> str:
|
|
36
|
+
"""Infer a human-readable dtype category from a sample of Python values.
|
|
37
|
+
|
|
38
|
+
Returns one of: 'string', 'integer', 'float', 'boolean', 'datetime', 'complex'
|
|
39
|
+
"""
|
|
40
|
+
import datetime as dt
|
|
41
|
+
|
|
42
|
+
non_none = [v for v in values if v is not None]
|
|
43
|
+
if not non_none:
|
|
44
|
+
return "string"
|
|
45
|
+
|
|
46
|
+
sample = non_none[:50]
|
|
47
|
+
|
|
48
|
+
# Check boolean first (bool is subclass of int in Python)
|
|
49
|
+
if all(isinstance(v, bool) for v in sample):
|
|
50
|
+
return "boolean"
|
|
51
|
+
if all(isinstance(v, int) and not isinstance(v, bool) for v in sample):
|
|
52
|
+
return "integer"
|
|
53
|
+
if all(isinstance(v, (int, float)) and not isinstance(v, bool) for v in sample):
|
|
54
|
+
return "float"
|
|
55
|
+
if all(isinstance(v, (dt.datetime, dt.date)) for v in sample):
|
|
56
|
+
return "datetime"
|
|
57
|
+
if all(isinstance(v, (dict, list)) for v in sample):
|
|
58
|
+
return "complex"
|
|
59
|
+
|
|
60
|
+
# Try to detect string-encoded timestamps
|
|
61
|
+
str_vals = [v for v in sample if isinstance(v, str)]
|
|
62
|
+
if str_vals and len(str_vals) == len(sample):
|
|
63
|
+
ts_count = sum(1 for s in str_vals[:10] if len(s) >= 10 and s[4:5] == "-")
|
|
64
|
+
if ts_count >= len(str_vals[:10]) * 0.8:
|
|
65
|
+
return "datetime"
|
|
66
|
+
|
|
67
|
+
return "string"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _pandas_dtype_to_category(dtype) -> str:
|
|
71
|
+
"""Convert a pandas dtype to our category string."""
|
|
72
|
+
dtype_str = str(dtype).lower()
|
|
73
|
+
if "bool" in dtype_str:
|
|
74
|
+
return "boolean"
|
|
75
|
+
if "int" in dtype_str:
|
|
76
|
+
return "integer"
|
|
77
|
+
if "float" in dtype_str or "double" in dtype_str:
|
|
78
|
+
return "float"
|
|
79
|
+
if "datetime" in dtype_str or "timestamp" in dtype_str:
|
|
80
|
+
return "datetime"
|
|
81
|
+
if "object" in dtype_str or "string" in dtype_str or "str" in dtype_str:
|
|
82
|
+
return "string"
|
|
83
|
+
if "category" in dtype_str:
|
|
84
|
+
return "string"
|
|
85
|
+
return "string"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _spark_dtype_to_category(dtype_str: str) -> str:
|
|
89
|
+
"""Convert a PySpark dtype string to our category string."""
|
|
90
|
+
dtype_str = dtype_str.lower()
|
|
91
|
+
if "bool" in dtype_str:
|
|
92
|
+
return "boolean"
|
|
93
|
+
if "int" in dtype_str or "long" in dtype_str or "short" in dtype_str or "byte" in dtype_str:
|
|
94
|
+
return "integer"
|
|
95
|
+
if "float" in dtype_str or "double" in dtype_str or "decimal" in dtype_str:
|
|
96
|
+
return "float"
|
|
97
|
+
if "timestamp" in dtype_str or "date" in dtype_str:
|
|
98
|
+
return "datetime"
|
|
99
|
+
if "struct" in dtype_str or "array" in dtype_str or "map" in dtype_str:
|
|
100
|
+
return "complex"
|
|
101
|
+
return "string"
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _polars_dtype_to_category(dtype) -> str:
|
|
105
|
+
"""Convert a Polars dtype to our category string."""
|
|
106
|
+
dtype_str = str(dtype).lower()
|
|
107
|
+
if "bool" in dtype_str:
|
|
108
|
+
return "boolean"
|
|
109
|
+
if "int" in dtype_str or "uint" in dtype_str:
|
|
110
|
+
return "integer"
|
|
111
|
+
if "float" in dtype_str or "decimal" in dtype_str:
|
|
112
|
+
return "float"
|
|
113
|
+
if "date" in dtype_str or "time" in dtype_str or "duration" in dtype_str:
|
|
114
|
+
return "datetime"
|
|
115
|
+
if "struct" in dtype_str or "list" in dtype_str:
|
|
116
|
+
return "complex"
|
|
117
|
+
return "string"
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _safe_str(val: Any) -> str:
|
|
121
|
+
"""Convert a value to a display-safe string."""
|
|
122
|
+
if val is None:
|
|
123
|
+
return "null"
|
|
124
|
+
if isinstance(val, bool):
|
|
125
|
+
return str(val).lower()
|
|
126
|
+
if isinstance(val, float):
|
|
127
|
+
if val != val: # NaN check
|
|
128
|
+
return "NaN"
|
|
129
|
+
if val == float("inf"):
|
|
130
|
+
return "Infinity"
|
|
131
|
+
if val == float("-inf"):
|
|
132
|
+
return "-Infinity"
|
|
133
|
+
return str(val)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _convert_to_records(
|
|
137
|
+
data: Any,
|
|
138
|
+
limit: int = 1000,
|
|
139
|
+
) -> tuple:
|
|
140
|
+
"""Convert various DataFrame types to a list of dicts and column metadata.
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
(records: List[Dict], columns: List[Dict], total_rows: int, truncated: bool)
|
|
144
|
+
Each column dict has keys: 'name', 'dtype_category'
|
|
145
|
+
"""
|
|
146
|
+
|
|
147
|
+
if _is_pyspark_dataframe(data):
|
|
148
|
+
total_rows = None
|
|
149
|
+
limited = data.limit(limit)
|
|
150
|
+
pdf = limited.toPandas()
|
|
151
|
+
records = pdf.to_dict("records")
|
|
152
|
+
columns = []
|
|
153
|
+
for field in data.schema.fields:
|
|
154
|
+
columns.append({
|
|
155
|
+
"name": field.name,
|
|
156
|
+
"dtype_category": _spark_dtype_to_category(str(field.dataType)),
|
|
157
|
+
})
|
|
158
|
+
truncated = len(records) >= limit
|
|
159
|
+
return records, columns, total_rows, truncated
|
|
160
|
+
|
|
161
|
+
if _is_pandas_dataframe(data):
|
|
162
|
+
total_rows = len(data)
|
|
163
|
+
truncated = total_rows > limit
|
|
164
|
+
df_sample = data.head(limit) if truncated else data
|
|
165
|
+
records = json.loads(df_sample.to_json(orient="records", date_format="iso", default_handler=str))
|
|
166
|
+
columns = []
|
|
167
|
+
for col_name in data.columns:
|
|
168
|
+
columns.append({
|
|
169
|
+
"name": str(col_name),
|
|
170
|
+
"dtype_category": _pandas_dtype_to_category(data[col_name].dtype),
|
|
171
|
+
})
|
|
172
|
+
return records, columns, total_rows, truncated
|
|
173
|
+
|
|
174
|
+
if _is_polars_dataframe(data):
|
|
175
|
+
total_rows = data.height
|
|
176
|
+
truncated = total_rows > limit
|
|
177
|
+
df_sample = data.head(limit) if truncated else data
|
|
178
|
+
records = df_sample.to_dicts()
|
|
179
|
+
columns = []
|
|
180
|
+
for col_name in data.columns:
|
|
181
|
+
columns.append({
|
|
182
|
+
"name": col_name,
|
|
183
|
+
"dtype_category": _polars_dtype_to_category(data[col_name].dtype),
|
|
184
|
+
})
|
|
185
|
+
return records, columns, total_rows, truncated
|
|
186
|
+
|
|
187
|
+
if isinstance(data, list):
|
|
188
|
+
if not data:
|
|
189
|
+
return [], [], 0, False
|
|
190
|
+
total_rows = len(data)
|
|
191
|
+
truncated = total_rows > limit
|
|
192
|
+
sample = data[:limit]
|
|
193
|
+
|
|
194
|
+
if isinstance(sample[0], dict):
|
|
195
|
+
records = sample
|
|
196
|
+
all_keys = []
|
|
197
|
+
seen = set()
|
|
198
|
+
for row in sample:
|
|
199
|
+
for k in row:
|
|
200
|
+
if k not in seen:
|
|
201
|
+
all_keys.append(k)
|
|
202
|
+
seen.add(k)
|
|
203
|
+
columns = []
|
|
204
|
+
for k in all_keys:
|
|
205
|
+
vals = [row.get(k) for row in sample]
|
|
206
|
+
columns.append({
|
|
207
|
+
"name": str(k),
|
|
208
|
+
"dtype_category": _infer_dtype_category(vals, str(k)),
|
|
209
|
+
})
|
|
210
|
+
return records, columns, total_rows, truncated
|
|
211
|
+
else:
|
|
212
|
+
records = [{"value": v} for v in sample]
|
|
213
|
+
columns = [{"name": "value", "dtype_category": _infer_dtype_category(sample)}]
|
|
214
|
+
return records, columns, total_rows, truncated
|
|
215
|
+
|
|
216
|
+
if isinstance(data, dict):
|
|
217
|
+
col_names = list(data.keys())
|
|
218
|
+
max_len = max((len(v) if isinstance(v, list) else 1) for v in data.values()) if data else 0
|
|
219
|
+
total_rows = max_len
|
|
220
|
+
truncated = total_rows > limit
|
|
221
|
+
actual_len = min(max_len, limit)
|
|
222
|
+
records = []
|
|
223
|
+
for i in range(actual_len):
|
|
224
|
+
row = {}
|
|
225
|
+
for c in col_names:
|
|
226
|
+
vals = data[c]
|
|
227
|
+
if isinstance(vals, list):
|
|
228
|
+
row[c] = vals[i] if i < len(vals) else None
|
|
229
|
+
else:
|
|
230
|
+
row[c] = vals
|
|
231
|
+
records.append(row)
|
|
232
|
+
columns = []
|
|
233
|
+
for c in col_names:
|
|
234
|
+
vals = data[c] if isinstance(data[c], list) else [data[c]]
|
|
235
|
+
columns.append({
|
|
236
|
+
"name": str(c),
|
|
237
|
+
"dtype_category": _infer_dtype_category(vals[:50], str(c)),
|
|
238
|
+
})
|
|
239
|
+
return records, columns, total_rows, truncated
|
|
240
|
+
|
|
241
|
+
raise TypeError(
|
|
242
|
+
f"display() does not support type '{type(data).__name__}'. "
|
|
243
|
+
"Supported types: PySpark DataFrame, Pandas DataFrame, Polars DataFrame, list, dict."
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def display(
|
|
248
|
+
data: Any,
|
|
249
|
+
limit: int = 1000,
|
|
250
|
+
title: str = "Table",
|
|
251
|
+
height: Optional[int] = None,
|
|
252
|
+
) -> None:
|
|
253
|
+
"""Display a DataFrame or data structure in an interactive Databricks-style table.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
data: A PySpark, Pandas, or Polars DataFrame, or a list of dicts / dict of lists.
|
|
257
|
+
limit: Maximum number of rows to display (default 1000). PySpark DataFrames
|
|
258
|
+
are automatically limited to avoid OOM.
|
|
259
|
+
title: The tab title shown in the top bar (default "Table").
|
|
260
|
+
height: Optional fixed height in pixels for the table container. If None,
|
|
261
|
+
the table auto-sizes up to ~500px then scrolls.
|
|
262
|
+
"""
|
|
263
|
+
from IPython.display import display as ipy_display, HTML
|
|
264
|
+
|
|
265
|
+
records, columns, total_rows, truncated = _convert_to_records(data, limit=limit)
|
|
266
|
+
|
|
267
|
+
# Sanitize all record values for safe JSON embedding
|
|
268
|
+
safe_records = []
|
|
269
|
+
for row in records:
|
|
270
|
+
safe_row = {}
|
|
271
|
+
for k, v in row.items():
|
|
272
|
+
safe_row[k] = _safe_str(v)
|
|
273
|
+
safe_records.append(safe_row)
|
|
274
|
+
|
|
275
|
+
table_id = "db_table_" + uuid.uuid4().hex[:12]
|
|
276
|
+
|
|
277
|
+
html_str = render_table_html(
|
|
278
|
+
table_id=table_id,
|
|
279
|
+
records=safe_records,
|
|
280
|
+
columns=columns,
|
|
281
|
+
total_rows=total_rows,
|
|
282
|
+
truncated=truncated,
|
|
283
|
+
limit=limit,
|
|
284
|
+
title=title,
|
|
285
|
+
height=height,
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
ipy_display(HTML(html_str))
|
dbxplay/icons.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SVG icons used in the Databricks-like table UI.
|
|
3
|
+
These are inline SVG strings matching the Databricks visual style.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def get_type_icon_svg(dtype_category: str) -> str:
|
|
8
|
+
"""Return an inline SVG icon for the given data type category.
|
|
9
|
+
|
|
10
|
+
Categories: 'string', 'integer', 'float', 'boolean', 'datetime', 'complex'
|
|
11
|
+
"""
|
|
12
|
+
icons = {
|
|
13
|
+
# ABC icon for string columns
|
|
14
|
+
"string": (
|
|
15
|
+
'<svg class="db-type-icon" viewBox="0 0 16 16" width="14" height="14" fill="none">'
|
|
16
|
+
'<text x="1" y="12" font-size="7.5" font-weight="700" fill="#5f6368" '
|
|
17
|
+
'font-family="Arial,sans-serif" letter-spacing="0.5">'
|
|
18
|
+
'<tspan fill="#1a73e8">A</tspan>'
|
|
19
|
+
'<tspan fill="#5f6368">B</tspan>'
|
|
20
|
+
'</text>'
|
|
21
|
+
'<text x="10.5" y="8" font-size="5.5" font-weight="600" fill="#5f6368" '
|
|
22
|
+
'font-family="Arial,sans-serif">C</text>'
|
|
23
|
+
'</svg>'
|
|
24
|
+
),
|
|
25
|
+
# 123 icon for integer columns
|
|
26
|
+
"integer": (
|
|
27
|
+
'<svg class="db-type-icon" viewBox="0 0 16 16" width="14" height="14" fill="none">'
|
|
28
|
+
'<text x="0" y="12" font-size="10" font-weight="700" fill="#5f6368" '
|
|
29
|
+
'font-family="Arial,sans-serif" letter-spacing="-0.5">#</text>'
|
|
30
|
+
'</svg>'
|
|
31
|
+
),
|
|
32
|
+
# Decimal icon for float columns
|
|
33
|
+
"float": (
|
|
34
|
+
'<svg class="db-type-icon" viewBox="0 0 16 16" width="14" height="14" fill="none">'
|
|
35
|
+
'<text x="0" y="12" font-size="7" font-weight="700" fill="#5f6368" '
|
|
36
|
+
'font-family="Arial,sans-serif" letter-spacing="-0.2">1.2</text>'
|
|
37
|
+
'</svg>'
|
|
38
|
+
),
|
|
39
|
+
# T/F icon for boolean columns
|
|
40
|
+
"boolean": (
|
|
41
|
+
'<svg class="db-type-icon" viewBox="0 0 16 16" width="14" height="14" fill="none">'
|
|
42
|
+
'<text x="0" y="12" font-size="7.5" font-weight="700" fill="#5f6368" '
|
|
43
|
+
'font-family="Arial,sans-serif">'
|
|
44
|
+
'<tspan fill="#1a73e8">T</tspan>'
|
|
45
|
+
'<tspan fill="#5f6368">/F</tspan>'
|
|
46
|
+
'</text>'
|
|
47
|
+
'</svg>'
|
|
48
|
+
),
|
|
49
|
+
# Calendar icon for datetime columns
|
|
50
|
+
"datetime": (
|
|
51
|
+
'<svg class="db-type-icon" viewBox="0 0 16 16" width="14" height="14" fill="none">'
|
|
52
|
+
'<rect x="1" y="3" width="14" height="12" rx="2" stroke="#5f6368" stroke-width="1.3" fill="none"/>'
|
|
53
|
+
'<line x1="1" y1="7" x2="15" y2="7" stroke="#5f6368" stroke-width="1.3"/>'
|
|
54
|
+
'<line x1="5" y1="1" x2="5" y2="5" stroke="#5f6368" stroke-width="1.3" stroke-linecap="round"/>'
|
|
55
|
+
'<line x1="11" y1="1" x2="11" y2="5" stroke="#5f6368" stroke-width="1.3" stroke-linecap="round"/>'
|
|
56
|
+
'<rect x="4" y="9.5" width="2.5" height="2" rx="0.5" fill="#1a73e8"/>'
|
|
57
|
+
'</svg>'
|
|
58
|
+
),
|
|
59
|
+
# Braces icon for complex/json columns
|
|
60
|
+
"complex": (
|
|
61
|
+
'<svg class="db-type-icon" viewBox="0 0 16 16" width="14" height="14" fill="none">'
|
|
62
|
+
'<text x="1" y="13" font-size="12" font-weight="400" fill="#5f6368" '
|
|
63
|
+
'font-family="Arial,sans-serif">{}</text>'
|
|
64
|
+
'</svg>'
|
|
65
|
+
),
|
|
66
|
+
}
|
|
67
|
+
return icons.get(dtype_category, icons["string"])
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def get_sort_icon_svg() -> str:
|
|
71
|
+
"""Return the sort arrow SVG for column headers."""
|
|
72
|
+
return (
|
|
73
|
+
'<svg class="db-sort-icon" viewBox="0 0 10 14" width="8" height="12" fill="none">'
|
|
74
|
+
'<path class="db-sort-asc" d="M5 0L9 5H1L5 0Z" fill="#bcc0c4"/>'
|
|
75
|
+
'<path class="db-sort-desc" d="M5 14L1 9H9L5 14Z" fill="#bcc0c4"/>'
|
|
76
|
+
'</svg>'
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def get_chevron_down_svg(size: int = 12) -> str:
|
|
81
|
+
"""Return a small chevron-down SVG for dropdown indicators."""
|
|
82
|
+
return (
|
|
83
|
+
f'<svg viewBox="0 0 10 6" width="{size}" height="{size}" fill="none" '
|
|
84
|
+
f'style="vertical-align:middle;">'
|
|
85
|
+
f'<path d="M1 1L5 5L9 1" stroke="#444" stroke-width="1.5" '
|
|
86
|
+
f'stroke-linecap="round" stroke-linejoin="round"/>'
|
|
87
|
+
f'</svg>'
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def get_plus_svg(size: int = 14) -> str:
|
|
92
|
+
"""Return a plus icon SVG."""
|
|
93
|
+
return (
|
|
94
|
+
f'<svg viewBox="0 0 12 12" width="{size}" height="{size}" fill="none">'
|
|
95
|
+
f'<path d="M6 1V11M1 6H11" stroke="#666" stroke-width="1.5" '
|
|
96
|
+
f'stroke-linecap="round"/>'
|
|
97
|
+
f'</svg>'
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def get_download_svg() -> str:
|
|
102
|
+
"""Return a download icon SVG."""
|
|
103
|
+
return (
|
|
104
|
+
'<svg viewBox="0 0 16 16" width="14" height="14" fill="none">'
|
|
105
|
+
'<path d="M8 2V10M8 10L5 7M8 10L11 7" stroke="#333" stroke-width="1.4" '
|
|
106
|
+
'stroke-linecap="round" stroke-linejoin="round"/>'
|
|
107
|
+
'<path d="M3 13H13" stroke="#333" stroke-width="1.4" stroke-linecap="round"/>'
|
|
108
|
+
'</svg>'
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def get_copy_svg() -> str:
|
|
113
|
+
"""Return a copy icon SVG."""
|
|
114
|
+
return (
|
|
115
|
+
'<svg viewBox="0 0 16 16" width="14" height="14" fill="none">'
|
|
116
|
+
'<rect x="5" y="5" width="8" height="9" rx="1.5" stroke="#333" stroke-width="1.2"/>'
|
|
117
|
+
'<path d="M11 5V3.5A1.5 1.5 0 009.5 2H3.5A1.5 1.5 0 002 3.5V10.5A1.5 1.5 0 003.5 12H5" '
|
|
118
|
+
'stroke="#333" stroke-width="1.2"/>'
|
|
119
|
+
'</svg>'
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def get_filter_svg() -> str:
|
|
124
|
+
"""Return a filter funnel icon SVG."""
|
|
125
|
+
return (
|
|
126
|
+
'<svg viewBox="0 0 16 16" width="14" height="14" fill="none">'
|
|
127
|
+
'<path d="M2 3H14L10 8.5V12L6 14V8.5L2 3Z" stroke="#333" stroke-width="1.2" '
|
|
128
|
+
'stroke-linejoin="round"/>'
|
|
129
|
+
'</svg>'
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def get_exclude_svg() -> str:
|
|
134
|
+
"""Return a circle-slash (exclude) icon SVG."""
|
|
135
|
+
return (
|
|
136
|
+
'<svg viewBox="0 0 16 16" width="14" height="14" fill="none">'
|
|
137
|
+
'<circle cx="8" cy="8" r="6" stroke="#333" stroke-width="1.2"/>'
|
|
138
|
+
'<line x1="3.5" y1="12.5" x2="12.5" y2="3.5" stroke="#333" stroke-width="1.2"/>'
|
|
139
|
+
'</svg>'
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def get_panel_svg() -> str:
|
|
144
|
+
"""Return a side-panel toggle icon SVG."""
|
|
145
|
+
return (
|
|
146
|
+
'<svg viewBox="0 0 16 16" width="14" height="14" fill="none">'
|
|
147
|
+
'<rect x="1" y="2" width="14" height="12" rx="2" stroke="#333" stroke-width="1.2"/>'
|
|
148
|
+
'<line x1="10" y1="2" x2="10" y2="14" stroke="#333" stroke-width="1.2"/>'
|
|
149
|
+
'</svg>'
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def get_search_svg() -> str:
|
|
154
|
+
"""Return a search/magnifying glass icon SVG."""
|
|
155
|
+
return (
|
|
156
|
+
'<svg viewBox="0 0 16 16" width="14" height="14" fill="none">'
|
|
157
|
+
'<circle cx="7" cy="7" r="4.5" stroke="#888" stroke-width="1.3"/>'
|
|
158
|
+
'<line x1="10.5" y1="10.5" x2="14" y2="14" stroke="#888" stroke-width="1.3" '
|
|
159
|
+
'stroke-linecap="round"/>'
|
|
160
|
+
'</svg>'
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def get_close_svg(size: int = 12) -> str:
|
|
165
|
+
"""Return a small X close icon SVG."""
|
|
166
|
+
return (
|
|
167
|
+
f'<svg viewBox="0 0 10 10" width="{size}" height="{size}" fill="none">'
|
|
168
|
+
f'<path d="M2 2L8 8M8 2L2 8" stroke="#666" stroke-width="1.5" '
|
|
169
|
+
f'stroke-linecap="round"/>'
|
|
170
|
+
f'</svg>'
|
|
171
|
+
)
|