dbxplay 0.1.0__tar.gz

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-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,110 @@
1
+ Metadata-Version: 2.4
2
+ Name: dbxplay
3
+ Version: 0.1.0
4
+ Summary: A Databricks-like interactive display() function for Jupyter Notebooks, AWS Glue, Google Colab, and VS Code.
5
+ Author: Varun
6
+ License-Expression: MIT
7
+ Requires-Python: >=3.8
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: ipython>=7.0
10
+ Provides-Extra: pandas
11
+ Requires-Dist: pandas>=1.0; extra == "pandas"
12
+ Provides-Extra: polars
13
+ Requires-Dist: polars>=0.14; extra == "polars"
14
+ Provides-Extra: pyspark
15
+ Requires-Dist: pyspark>=3.0; extra == "pyspark"
16
+ Provides-Extra: all
17
+ Requires-Dist: pandas>=1.0; extra == "all"
18
+ Requires-Dist: polars>=0.14; extra == "all"
19
+ Requires-Dist: pyspark>=3.0; extra == "all"
20
+ Provides-Extra: dev
21
+ Requires-Dist: pytest>=7.0; extra == "dev"
22
+ Requires-Dist: pandas>=1.0; extra == "dev"
23
+
24
+ # dbxplay
25
+
26
+ A Databricks-like interactive `display()` function for **Jupyter Notebooks**, **AWS Glue**, **Google Colab**, and **VS Code**.
27
+
28
+ Brings the beautiful, interactive table UI from Databricks into any Python notebook environment — with sorting, filtering, searching, customizable X/Y charting, data profiling, context menus, column resizing, CSV/Excel export, and more.
29
+
30
+ ## Features
31
+
32
+ | Feature | Description |
33
+ | :--- | :--- |
34
+ | 🔍 **Global Search** | Instant full-text search across all columns & values |
35
+ | ↕️ **Column Sorting** | Click column headers to sort ascending/descending |
36
+ | 🔽 **Column Filtering** | Per-column value checkbox dropdowns |
37
+ | 📊 **Custom Visualization** | Customizable X/Y axes & aggregations (COUNT, SUM, AVG, MIN, MAX) |
38
+ | 📋 **Data Profile** | Overview stats & pop-out closeable distribution charts |
39
+ | 🎯 **Filter by Value** | Right-click any cell → "Filter by this value" |
40
+ | 🚫 **Exclude Value** | Right-click → "Exclude this value" |
41
+ | 📋 **Copy as CSV/TSV/Markdown** | Right-click → Copy as → choose format |
42
+ | 📥 **Download CSV / Excel** | Top `Table ∨` dropdown → Download |
43
+ | 📐 **Column Resizing** | Drag column borders to resize |
44
+ | 📄 **Side Panel** | Row detail view panel |
45
+ | 🔢 **Data Type Icons** | Automatic type badges (ABC, #, 1.2, T/F, 📅, {}) |
46
+ | ✅ **Zero Dependencies** | Self-contained HTML/CSS/JS — works everywhere |
47
+
48
+ ## Installation
49
+
50
+ ```bash
51
+ pip install dbxplay
52
+ ```
53
+
54
+ Or install locally in editable mode:
55
+
56
+ ```bash
57
+ pip install -e .
58
+ ```
59
+
60
+ ## Quick Start
61
+
62
+ ```python
63
+ from dbxplay import display
64
+ import pandas as pd
65
+
66
+ df = pd.read_csv("your_data.csv")
67
+ display(df)
68
+ ```
69
+
70
+ ## Supported Data Types
71
+
72
+ - **Pandas DataFrames**
73
+ - **Polars DataFrames**
74
+ - **PySpark DataFrames** (auto-limited to prevent OOM)
75
+ - **Lists of dicts** — `[{"a": 1, "b": 2}, ...]`
76
+ - **Dicts of lists** — `{"a": [1, 2], "b": [3, 4]}`
77
+
78
+ ## Usage
79
+
80
+ ```python
81
+ from dbxplay import display
82
+
83
+ # Basic usage
84
+ display(df)
85
+
86
+ # With options
87
+ display(df, limit=500, title="My Data", height=400)
88
+ ```
89
+
90
+ ### Parameters
91
+
92
+ | Parameter | Type | Default | Description |
93
+ | :--- | :--- | :--- | :--- |
94
+ | `data` | DataFrame/list/dict | *(required)* | The data to display |
95
+ | `limit` | int | 1000 | Max rows to render |
96
+ | `title` | str | "Table" | Tab title in the top bar |
97
+ | `height` | int | None | Fixed height in px (auto-sizes to ~520px) |
98
+
99
+ ## Context Menu (Right-Click)
100
+
101
+ Right-click any cell to access:
102
+ - **Copy** (⌘C)
103
+ - **Copy as** → CSV, TSV, Markdown
104
+ - **Filter by this value**
105
+ - **Exclude this value**
106
+ - **Toggle side panel**
107
+
108
+ ## License
109
+
110
+ MIT
@@ -0,0 +1,87 @@
1
+ # dbxplay
2
+
3
+ A Databricks-like interactive `display()` function for **Jupyter Notebooks**, **AWS Glue**, **Google Colab**, and **VS Code**.
4
+
5
+ Brings the beautiful, interactive table UI from Databricks into any Python notebook environment — with sorting, filtering, searching, customizable X/Y charting, data profiling, context menus, column resizing, CSV/Excel export, and more.
6
+
7
+ ## Features
8
+
9
+ | Feature | Description |
10
+ | :--- | :--- |
11
+ | 🔍 **Global Search** | Instant full-text search across all columns & values |
12
+ | ↕️ **Column Sorting** | Click column headers to sort ascending/descending |
13
+ | 🔽 **Column Filtering** | Per-column value checkbox dropdowns |
14
+ | 📊 **Custom Visualization** | Customizable X/Y axes & aggregations (COUNT, SUM, AVG, MIN, MAX) |
15
+ | 📋 **Data Profile** | Overview stats & pop-out closeable distribution charts |
16
+ | 🎯 **Filter by Value** | Right-click any cell → "Filter by this value" |
17
+ | 🚫 **Exclude Value** | Right-click → "Exclude this value" |
18
+ | 📋 **Copy as CSV/TSV/Markdown** | Right-click → Copy as → choose format |
19
+ | 📥 **Download CSV / Excel** | Top `Table ∨` dropdown → Download |
20
+ | 📐 **Column Resizing** | Drag column borders to resize |
21
+ | 📄 **Side Panel** | Row detail view panel |
22
+ | 🔢 **Data Type Icons** | Automatic type badges (ABC, #, 1.2, T/F, 📅, {}) |
23
+ | ✅ **Zero Dependencies** | Self-contained HTML/CSS/JS — works everywhere |
24
+
25
+ ## Installation
26
+
27
+ ```bash
28
+ pip install dbxplay
29
+ ```
30
+
31
+ Or install locally in editable mode:
32
+
33
+ ```bash
34
+ pip install -e .
35
+ ```
36
+
37
+ ## Quick Start
38
+
39
+ ```python
40
+ from dbxplay import display
41
+ import pandas as pd
42
+
43
+ df = pd.read_csv("your_data.csv")
44
+ display(df)
45
+ ```
46
+
47
+ ## Supported Data Types
48
+
49
+ - **Pandas DataFrames**
50
+ - **Polars DataFrames**
51
+ - **PySpark DataFrames** (auto-limited to prevent OOM)
52
+ - **Lists of dicts** — `[{"a": 1, "b": 2}, ...]`
53
+ - **Dicts of lists** — `{"a": [1, 2], "b": [3, 4]}`
54
+
55
+ ## Usage
56
+
57
+ ```python
58
+ from dbxplay import display
59
+
60
+ # Basic usage
61
+ display(df)
62
+
63
+ # With options
64
+ display(df, limit=500, title="My Data", height=400)
65
+ ```
66
+
67
+ ### Parameters
68
+
69
+ | Parameter | Type | Default | Description |
70
+ | :--- | :--- | :--- | :--- |
71
+ | `data` | DataFrame/list/dict | *(required)* | The data to display |
72
+ | `limit` | int | 1000 | Max rows to render |
73
+ | `title` | str | "Table" | Tab title in the top bar |
74
+ | `height` | int | None | Fixed height in px (auto-sizes to ~520px) |
75
+
76
+ ## Context Menu (Right-Click)
77
+
78
+ Right-click any cell to access:
79
+ - **Copy** (⌘C)
80
+ - **Copy as** → CSV, TSV, Markdown
81
+ - **Filter by this value**
82
+ - **Exclude this value**
83
+ - **Toggle side panel**
84
+
85
+ ## License
86
+
87
+ MIT
@@ -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"]
@@ -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))
@@ -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
+ )