streamlit-server-file-picker 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.
@@ -0,0 +1,192 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ from pathlib import Path
5
+ from typing import Any, Literal
6
+
7
+ import streamlit as st
8
+
9
+ from .component import server_file_picker_component
10
+
11
+
12
+ def _resolve_path(value: str | Path) -> Path:
13
+ return Path(value).expanduser().resolve(strict=False)
14
+
15
+
16
+ def _within_top_level(path: Path, top_level: Path | None) -> bool:
17
+ if top_level is None:
18
+ return True
19
+
20
+ try:
21
+ return path == top_level or path.is_relative_to(top_level)
22
+ except AttributeError:
23
+ return str(path).startswith(str(top_level))
24
+
25
+
26
+ def _best_existing_directory(path: Path, top_level: Path | None) -> Path:
27
+ candidate = _resolve_path(path)
28
+
29
+ if candidate.is_file():
30
+ candidate = candidate.parent
31
+
32
+ while not candidate.exists():
33
+ parent = candidate.parent
34
+ if parent == candidate:
35
+ break
36
+ candidate = parent
37
+
38
+ if candidate.is_file():
39
+ candidate = candidate.parent
40
+
41
+ if top_level is not None and not _within_top_level(candidate, top_level):
42
+ return top_level
43
+
44
+ return candidate
45
+
46
+
47
+ def _clamp_to_top_level(path: Path, top_level: Path | None) -> Path:
48
+ if top_level is None:
49
+ return _best_existing_directory(path, None)
50
+
51
+ if not _within_top_level(path, top_level):
52
+ return top_level
53
+
54
+ return _best_existing_directory(path, top_level)
55
+
56
+
57
+ def _normalize_extensions(values: list[str] | None) -> set[str] | None:
58
+ if not values:
59
+ return None
60
+
61
+ normalized: set[str] = set()
62
+ for value in values:
63
+ item = value.strip().lower()
64
+ if not item:
65
+ continue
66
+ if not item.startswith("."):
67
+ item = f".{item}"
68
+ normalized.add(item)
69
+
70
+ return normalized or None
71
+
72
+
73
+ def _directory_key(path: Path) -> str:
74
+ return hashlib.sha1(str(path).encode("utf-8")).hexdigest()[:12]
75
+
76
+
77
+ def _build_directory_tree(directory: Path, show_hidden: bool) -> dict[str, Any]:
78
+ node: dict[str, Any] = {
79
+ "name": directory.name or str(directory),
80
+ "path": str(directory),
81
+ "is_dir": True,
82
+ "children": [],
83
+ }
84
+
85
+ try:
86
+ entries = sorted(
87
+ directory.iterdir(),
88
+ key=lambda entry: (not entry.is_dir(), entry.name.lower()),
89
+ )
90
+ except (FileNotFoundError, PermissionError, NotADirectoryError):
91
+ return node
92
+
93
+ children: list[dict[str, Any]] = []
94
+ for entry in entries:
95
+ if not show_hidden and entry.name.startswith("."):
96
+ continue
97
+
98
+ if entry.is_symlink() and entry.is_dir():
99
+ continue
100
+
101
+ if entry.is_dir():
102
+ children.append(_build_directory_tree(entry, show_hidden))
103
+ else:
104
+ children.append(
105
+ {
106
+ "name": entry.name,
107
+ "path": str(entry),
108
+ "is_dir": False,
109
+ }
110
+ )
111
+
112
+ node["children"] = children
113
+ return node
114
+
115
+
116
+ def _tree_root_for(current_directory: Path, top_level_directory: Path | None) -> Path:
117
+ if top_level_directory is not None:
118
+ return top_level_directory
119
+ return _best_existing_directory(current_directory, None).parent
120
+
121
+
122
+ def _picker_state_key(prefix: str, suffix: str) -> str:
123
+ return f"{prefix}_{suffix}"
124
+
125
+
126
+ def server_file_picker_dialog(
127
+ current_directory: str = ".",
128
+ top_level_directory: str | None = None,
129
+ show_files: bool = True,
130
+ show_hidden: bool = False,
131
+ extensions: list[str] | None = None,
132
+ element_type: Literal["file", "directory"] = "file",
133
+ target_input_label: str = "Image Cache Path",
134
+ ) -> str:
135
+ normalized_top_level = (
136
+ _resolve_path(top_level_directory) if top_level_directory is not None else None
137
+ )
138
+ initial_directory = _clamp_to_top_level(
139
+ Path(current_directory), normalized_top_level
140
+ )
141
+ normalized_extensions = _normalize_extensions(extensions)
142
+ root_directory = _tree_root_for(initial_directory, normalized_top_level)
143
+
144
+ state_key = f"server_file_picker_{_directory_key(initial_directory)}"
145
+ selected_path_key = _picker_state_key(state_key, "selected_path")
146
+
147
+ tree = _build_directory_tree(root_directory, show_hidden=show_hidden)
148
+
149
+ result = server_file_picker_component(
150
+ data={
151
+ "current_directory": str(initial_directory),
152
+ "root_directory": str(root_directory),
153
+ "top_level_directory": str(normalized_top_level)
154
+ if normalized_top_level is not None
155
+ else None,
156
+ "show_files": show_files,
157
+ "show_hidden": show_hidden,
158
+ "extensions": sorted(normalized_extensions) if normalized_extensions else None,
159
+ "element_type": element_type,
160
+ "tree": tree,
161
+ "selected_path": str(st.session_state.get(selected_path_key, "") or ""),
162
+ "target_input_label": target_input_label,
163
+ },
164
+ default={
165
+ "current_directory": str(initial_directory),
166
+ "selected_path": str(st.session_state.get(selected_path_key, "") or ""),
167
+ "confirm": None,
168
+ "cancel": None,
169
+ },
170
+ key=state_key,
171
+ on_current_directory_change=lambda: None,
172
+ on_selected_path_change=lambda: None,
173
+ on_confirm_change=lambda: None,
174
+ on_cancel_change=lambda: None,
175
+ )
176
+
177
+ if getattr(result, "selected_path", None):
178
+ st.session_state[selected_path_key] = str(getattr(result, "selected_path"))
179
+
180
+ if getattr(result, "confirm", None):
181
+ return str(getattr(result, "selected_path", "") or "")
182
+
183
+ if getattr(result, "cancel", None):
184
+ return ""
185
+
186
+ return ""
187
+
188
+
189
+ __all__ = [
190
+ "server_file_picker_dialog",
191
+ "server_file_picker_component",
192
+ ]
@@ -0,0 +1,21 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ import streamlit as st
6
+
7
+ _FRONTEND_DIR = Path(__file__).with_name("frontend")
8
+
9
+ _HTML = (_FRONTEND_DIR / "component.html").read_text(encoding="utf-8")
10
+ _CSS = (_FRONTEND_DIR / "component.css").read_text(encoding="utf-8")
11
+ _JS = (_FRONTEND_DIR / "component.js").read_text(encoding="utf-8")
12
+
13
+ server_file_picker_component = st.components.v2.component(
14
+ "server_file_picker_component",
15
+ html=_HTML,
16
+ css=_CSS,
17
+ js=_JS,
18
+ isolate_styles=False,
19
+ )
20
+
21
+ __all__ = ["server_file_picker_component"]
@@ -0,0 +1,216 @@
1
+ #server-file-picker-overlay {
2
+ position: fixed;
3
+ inset: 0;
4
+ z-index: 999995;
5
+ display: grid;
6
+ place-items: center;
7
+ pointer-events: auto;
8
+ font-family: var(--st-font, Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
9
+ "Segoe UI", sans-serif);
10
+ color: var(--st-text-color, #1f1c17);
11
+ }
12
+
13
+ #server-file-picker-overlay * {
14
+ box-sizing: border-box;
15
+ }
16
+
17
+ #server-file-picker-overlay .backdrop {
18
+ position: absolute;
19
+ inset: 0;
20
+ background: color-mix(in srgb, var(--st-background-color, #ffffff) 18%, #000000);
21
+ opacity: 0.72;
22
+ }
23
+
24
+ #server-file-picker-overlay .dialog {
25
+ position: relative;
26
+ width: min(980px, calc(100vw - 32px));
27
+ max-height: calc(100vh - 32px);
28
+ z-index: 1;
29
+ display: flex;
30
+ flex-direction: column;
31
+ border: none;
32
+ border-radius: 18px;
33
+ background: color-mix(in srgb, var(--st-background-color, #ffffff) 92%, var(--st-secondary-background-color, #f2ede3));
34
+ box-shadow: 0 18px 45px rgba(38, 31, 19, 0.18);
35
+ overflow: hidden;
36
+ }
37
+
38
+ #server-file-picker-overlay .header {
39
+ padding: 14px;
40
+ border-bottom: 1px solid var(--st-border-color, rgba(128, 128, 128, 0.24));
41
+ background: linear-gradient(
42
+ 180deg,
43
+ color-mix(in srgb, var(--st-background-color, #ffffff) 86%, transparent),
44
+ color-mix(in srgb, var(--st-secondary-background-color, #f2ede3) 78%, transparent)
45
+ );
46
+ }
47
+
48
+ #server-file-picker-overlay .row {
49
+ display: flex;
50
+ align-items: center;
51
+ gap: 10px;
52
+ }
53
+
54
+ #server-file-picker-overlay .row + .row {
55
+ margin-top: 10px;
56
+ }
57
+
58
+ #server-file-picker-overlay .button,
59
+ #server-file-picker-overlay .input,
60
+ #server-file-picker-overlay .select-button,
61
+ #server-file-picker-overlay .cancel-button {
62
+ border: 1px solid var(--st-border-color, rgba(128, 128, 128, 0.24));
63
+ border-radius: 12px;
64
+ font: inherit;
65
+ }
66
+
67
+ #server-file-picker-overlay .button,
68
+ #server-file-picker-overlay .select-button,
69
+ #server-file-picker-overlay .cancel-button {
70
+ display: inline-flex;
71
+ align-items: center;
72
+ justify-content: center;
73
+ gap: 8px;
74
+ min-height: 40px;
75
+ padding: 0 14px;
76
+ cursor: pointer;
77
+ background: var(--st-background-color, #fffaf3);
78
+ color: var(--st-text-color, #1f1c17);
79
+ transition: transform 120ms ease, background 120ms ease, border-color 120ms ease;
80
+ }
81
+
82
+ #server-file-picker-overlay .button:hover,
83
+ #server-file-picker-overlay .select-button:hover,
84
+ #server-file-picker-overlay .cancel-button:hover,
85
+ #server-file-picker-overlay .entry:hover {
86
+ background: var(--st-secondary-background-color, #ece4d7);
87
+ }
88
+
89
+ #server-file-picker-overlay .button:active,
90
+ #server-file-picker-overlay .select-button:active,
91
+ #server-file-picker-overlay .cancel-button:active,
92
+ #server-file-picker-overlay .entry:active {
93
+ transform: translateY(1px);
94
+ }
95
+
96
+ #server-file-picker-overlay .button:disabled,
97
+ #server-file-picker-overlay .select-button:disabled,
98
+ #server-file-picker-overlay .cancel-button:disabled {
99
+ cursor: not-allowed;
100
+ opacity: 0.45;
101
+ }
102
+
103
+ #server-file-picker-overlay .input {
104
+ width: 100%;
105
+ min-width: 0;
106
+ min-height: 40px;
107
+ padding: 0 12px;
108
+ background: var(--st-background-color, rgba(255, 255, 255, 0.8));
109
+ color: var(--st-text-color, #1f1c17);
110
+ outline: none;
111
+ }
112
+
113
+ #server-file-picker-overlay .input:focus {
114
+ border-color: var(--st-primary-color, #1f6f78);
115
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--st-primary-color, #1f6f78) 18%, transparent);
116
+ }
117
+
118
+ #server-file-picker-overlay .current-group {
119
+ flex: 1 1 auto;
120
+ }
121
+
122
+ #server-file-picker-overlay .select-group {
123
+ width: 140px;
124
+ flex: 0 0 140px;
125
+ display: flex;
126
+ justify-content: flex-end;
127
+ gap: 8px;
128
+ }
129
+
130
+ #server-file-picker-overlay .list-shell {
131
+ padding: 14px;
132
+ display: flex;
133
+ flex-direction: column;
134
+ min-height: 0;
135
+ flex: 1 1 auto;
136
+ }
137
+
138
+ #server-file-picker-overlay .list-meta {
139
+ display: flex;
140
+ justify-content: space-between;
141
+ align-items: center;
142
+ margin-bottom: 10px;
143
+ color: var(--st-secondary-text-color, var(--st-text-color, #6d655a));
144
+ font-size: 0.93rem;
145
+ }
146
+
147
+ #server-file-picker-overlay .list {
148
+ flex: 1 1 auto;
149
+ min-height: 0;
150
+ overflow-y: auto;
151
+ border: 1px solid var(--st-border-color, rgba(128, 128, 128, 0.24));
152
+ border-radius: 14px;
153
+ background: var(--st-background-color, #fffaf3);
154
+ padding: 8px;
155
+ }
156
+
157
+ #server-file-picker-overlay .entry {
158
+ display: flex;
159
+ align-items: center;
160
+ gap: 10px;
161
+ width: 100%;
162
+ min-height: 40px;
163
+ padding: 0 10px;
164
+ border-radius: 10px;
165
+ cursor: pointer;
166
+ user-select: none;
167
+ border: 1px solid transparent;
168
+ background: transparent;
169
+ color: inherit;
170
+ text-align: left;
171
+ }
172
+
173
+ #server-file-picker-overlay .entry + .entry {
174
+ margin-top: 4px;
175
+ }
176
+
177
+ #server-file-picker-overlay .entry.selected {
178
+ background: color-mix(in srgb, var(--st-primary-color, #1f6f78) 12%, var(--st-background-color, #ffffff));
179
+ border-color: color-mix(in srgb, var(--st-primary-color, #1f6f78) 30%, transparent);
180
+ }
181
+
182
+ #server-file-picker-overlay .entry.directory {
183
+ font-weight: 600;
184
+ }
185
+
186
+ #server-file-picker-overlay .entry-name {
187
+ flex: 1 1 auto;
188
+ min-width: 0;
189
+ overflow: hidden;
190
+ text-overflow: ellipsis;
191
+ white-space: nowrap;
192
+ }
193
+
194
+ #server-file-picker-overlay .entry-path {
195
+ color: var(--st-secondary-text-color, var(--st-text-color, #6d655a));
196
+ font-size: 0.82rem;
197
+ }
198
+
199
+ #server-file-picker-overlay .icon {
200
+ font-family: "Material Symbols Outlined";
201
+ font-size: 20px;
202
+ line-height: 1;
203
+ color: var(--st-primary-color, #154b52);
204
+ font-variation-settings: "FILL" 0, "wght" 400, "GRAD" 0, "opsz" 24;
205
+ }
206
+
207
+ #server-file-picker-overlay .hint {
208
+ color: var(--st-secondary-text-color, var(--st-text-color, #6d655a));
209
+ font-size: 0.9rem;
210
+ }
211
+
212
+ #server-file-picker-overlay .empty {
213
+ padding: 18px 12px;
214
+ color: var(--st-secondary-text-color, var(--st-text-color, #6d655a));
215
+ text-align: center;
216
+ }
@@ -0,0 +1 @@
1
+ <div id="server-file-picker-anchor"></div>
@@ -0,0 +1,384 @@
1
+ const OVERLAY_ID = "server-file-picker-overlay";
2
+ const ROOT_ID = "server-file-picker-root";
3
+
4
+ const instances = window.__serverFilePickerInstances || new Map();
5
+ window.__serverFilePickerInstances = instances;
6
+
7
+ function normalizePath(path) {
8
+ return String(path || "").replace(/\\/g, "/");
9
+ }
10
+
11
+ function pathBasename(path) {
12
+ const normalized = normalizePath(path).replace(/\/$/, "");
13
+ if (!normalized) {
14
+ return ".";
15
+ }
16
+
17
+ const parts = normalized.split("/").filter(Boolean);
18
+ return parts.length ? parts[parts.length - 1] : normalized;
19
+ }
20
+
21
+ function parentDirectory(path) {
22
+ const normalized = normalizePath(path).replace(/\/$/, "");
23
+ if (!normalized) {
24
+ return ".";
25
+ }
26
+
27
+ const parts = normalized.split("/").filter(Boolean);
28
+ if (parts.length <= 1) {
29
+ return ".";
30
+ }
31
+
32
+ return normalized.startsWith("/")
33
+ ? `/${parts.slice(0, -1).join("/")}`
34
+ : parts.slice(0, -1).join("/");
35
+ }
36
+
37
+ function escapeHtml(value) {
38
+ return String(value)
39
+ .replaceAll("&", "&amp;")
40
+ .replaceAll("<", "&lt;")
41
+ .replaceAll(">", "&gt;")
42
+ .replaceAll('"', "&quot;")
43
+ .replaceAll("'", "&#39;");
44
+ }
45
+
46
+ function buildDirectoryMap(tree) {
47
+ const map = new Map();
48
+
49
+ function walk(node) {
50
+ if (!node || !node.is_dir) {
51
+ return;
52
+ }
53
+
54
+ map.set(normalizePath(node.path), node);
55
+ const children = Array.isArray(node.children) ? node.children : [];
56
+ for (const child of children) {
57
+ if (child && child.is_dir) {
58
+ walk(child);
59
+ }
60
+ }
61
+ }
62
+
63
+ walk(tree);
64
+ return map;
65
+ }
66
+
67
+ function getCurrentEntries(treeMap, currentDirectory) {
68
+ const node = treeMap.get(normalizePath(currentDirectory));
69
+ if (!node) {
70
+ return [];
71
+ }
72
+
73
+ return Array.isArray(node.children) ? node.children : [];
74
+ }
75
+
76
+ function updateTextInput(targetLabel, value) {
77
+ if (!targetLabel) {
78
+ return;
79
+ }
80
+
81
+ const selector = `input[aria-label="${CSS.escape(targetLabel)}"]`;
82
+ const input = document.querySelector(selector);
83
+ if (!input) {
84
+ return;
85
+ }
86
+
87
+ const prototype = Object.getPrototypeOf(input);
88
+ const descriptor = Object.getOwnPropertyDescriptor(prototype, "value");
89
+ if (descriptor && descriptor.set) {
90
+ descriptor.set.call(input, value);
91
+ } else {
92
+ input.value = value;
93
+ }
94
+
95
+ input.dispatchEvent(new Event("input", { bubbles: true }));
96
+ input.dispatchEvent(new Event("change", { bubbles: true }));
97
+ }
98
+
99
+ function ensureOverlay() {
100
+ let overlay = document.getElementById(OVERLAY_ID);
101
+ if (overlay) {
102
+ return overlay;
103
+ }
104
+
105
+ overlay = document.createElement("div");
106
+ overlay.id = OVERLAY_ID;
107
+ overlay.innerHTML = `
108
+ <div class="backdrop"></div>
109
+ <div class="dialog" role="dialog" aria-modal="true">
110
+ <div class="header">
111
+ <div class="row">
112
+ <button class="button up-button" type="button" aria-label="Up one level" title="Up one level">
113
+ <span class="icon">arrow_upward</span>
114
+ </button>
115
+ <div class="current-group">
116
+ <input
117
+ class="input directory-input"
118
+ type="text"
119
+ spellcheck="false"
120
+ autocomplete="off"
121
+ autocapitalize="off"
122
+ placeholder="Current directory"
123
+ aria-label="Current directory"
124
+ />
125
+ </div>
126
+ <div class="select-group">
127
+ <button class="cancel-button cancel-action" type="button">
128
+ <span class="icon">close</span>
129
+ <span>Cancel</span>
130
+ </button>
131
+ <button class="select-button select-action" type="button">
132
+ <span class="icon">check</span>
133
+ <span>Select</span>
134
+ </button>
135
+ </div>
136
+ </div>
137
+ </div>
138
+ <div class="list-shell">
139
+ <div class="list-meta">
140
+ <span class="subtitle"></span>
141
+ <span class="hint">Single click a directory to enter it. Double click a file to confirm.</span>
142
+ </div>
143
+ <div class="list"></div>
144
+ </div>
145
+ </div>
146
+ `;
147
+
148
+ document.body.appendChild(overlay);
149
+ return overlay;
150
+ }
151
+
152
+ function renderEntry(instance, entry) {
153
+ const isDirectory = Boolean(entry.is_dir);
154
+ const isSelected = normalizePath(instance.state.selectedPath) === normalizePath(entry.path);
155
+ const row = document.createElement("button");
156
+ row.type = "button";
157
+ row.className = `entry ${isDirectory ? "directory" : "file"}${isSelected ? " selected" : ""}`;
158
+ row.dataset.path = entry.path;
159
+
160
+ row.innerHTML = `
161
+ <span class="icon">${isDirectory ? "folder" : "description"}</span>
162
+ <span class="entry-name">${escapeHtml(entry.name)}</span>
163
+ <span class="entry-path">${escapeHtml(entry.path)}</span>
164
+ `;
165
+
166
+ if (isDirectory) {
167
+ row.addEventListener("click", () => {
168
+ instance.state.currentDirectory = normalizePath(entry.path);
169
+ instance.state.selectedPath = "";
170
+ instance.render();
171
+ });
172
+ } else {
173
+ row.addEventListener("click", () => {
174
+ instance.state.selectedPath = normalizePath(entry.path);
175
+ instance.render();
176
+ });
177
+ row.addEventListener("dblclick", () => {
178
+ instance.confirmSelection(normalizePath(entry.path));
179
+ });
180
+ }
181
+
182
+ return row;
183
+ }
184
+
185
+ function createInstance(component) {
186
+ const overlay = ensureOverlay();
187
+ const subtitle = overlay.querySelector(".subtitle");
188
+ const list = overlay.querySelector(".list");
189
+ const upButton = overlay.querySelector(".up-button");
190
+ const directoryInput = overlay.querySelector(".directory-input");
191
+ const selectButton = overlay.querySelector(".select-action");
192
+ const cancelButton = overlay.querySelector(".cancel-action");
193
+ const backdrop = overlay.querySelector(".backdrop");
194
+
195
+ if (!subtitle || !list || !upButton || !directoryInput || !selectButton || !cancelButton || !backdrop) {
196
+ throw new Error("Unexpected: server file picker overlay not found");
197
+ }
198
+
199
+ const instance = {
200
+ overlay,
201
+ subtitle,
202
+ list,
203
+ upButton,
204
+ directoryInput,
205
+ selectButton,
206
+ cancelButton,
207
+ backdrop,
208
+ treeMap: buildDirectoryMap(component.data.tree),
209
+ state: {
210
+ currentDirectory: normalizePath(component.data.current_directory || "."),
211
+ selectedPath: normalizePath(component.data.selected_path || ""),
212
+ elementType: component.data.element_type || "file",
213
+ topLevelDirectory: component.data.top_level_directory || null,
214
+ targetInputLabel: component.data.target_input_label || null,
215
+ showFiles: Boolean(component.data.show_files),
216
+ showHidden: Boolean(component.data.show_hidden),
217
+ extensions: Array.isArray(component.data.extensions) ? component.data.extensions : null,
218
+ tree: component.data.tree,
219
+ },
220
+ confirmSelection: (selectedPath) => {
221
+ const resolvedPath = normalizePath(selectedPath || instance.state.selectedPath);
222
+ if (!resolvedPath) {
223
+ return;
224
+ }
225
+
226
+ instance.state.selectedPath = resolvedPath;
227
+ updateTextInput(instance.state.targetInputLabel, resolvedPath);
228
+ component.setStateValue("selected_path", resolvedPath);
229
+ component.setTriggerValue("confirm", resolvedPath);
230
+ hideOverlay(instance);
231
+ },
232
+ cancelSelection: () => {
233
+ component.setTriggerValue("cancel", true);
234
+ hideOverlay(instance);
235
+ },
236
+ render: () => {
237
+ instance.subtitle.textContent = instance.state.currentDirectory;
238
+ directoryInput.value = instance.state.currentDirectory;
239
+ upButton.disabled = Boolean(
240
+ instance.state.topLevelDirectory &&
241
+ normalizePath(instance.state.currentDirectory) === normalizePath(instance.state.topLevelDirectory),
242
+ );
243
+ selectButton.disabled = instance.state.elementType !== "directory" && !instance.state.selectedPath;
244
+
245
+ list.innerHTML = "";
246
+ const entries = getCurrentEntries(instance.treeMap, instance.state.currentDirectory);
247
+
248
+ const visibleEntries = entries.filter((entry) => {
249
+ if (!entry) {
250
+ return false;
251
+ }
252
+
253
+ if (entry.name.startsWith(".") && !instance.state.showHidden) {
254
+ return false;
255
+ }
256
+
257
+ if (entry.is_dir) {
258
+ return true;
259
+ }
260
+
261
+ if (!instance.state.showFiles) {
262
+ return false;
263
+ }
264
+
265
+ if (instance.state.extensions && instance.state.extensions.length > 0) {
266
+ const suffix = entry.name.toLowerCase().slice(entry.name.lastIndexOf("."));
267
+ return instance.state.extensions.includes(suffix);
268
+ }
269
+
270
+ return true;
271
+ });
272
+
273
+ if (!visibleEntries.length) {
274
+ const empty = document.createElement("div");
275
+ empty.className = "empty";
276
+ empty.textContent = "No matching items in this directory.";
277
+ list.appendChild(empty);
278
+ } else {
279
+ for (const entry of visibleEntries) {
280
+ list.appendChild(renderEntry(instance, entry));
281
+ }
282
+ }
283
+ },
284
+ };
285
+
286
+ upButton.onclick = () => {
287
+ const nextDirectory = parentDirectory(instance.state.currentDirectory);
288
+ if (instance.treeMap.has(normalizePath(nextDirectory))) {
289
+ instance.state.currentDirectory = nextDirectory;
290
+ instance.state.selectedPath = "";
291
+ instance.render();
292
+ }
293
+ };
294
+
295
+ directoryInput.onkeydown = (event) => {
296
+ if (event.key === "Enter") {
297
+ event.preventDefault();
298
+ const nextDirectory = normalizePath(directoryInput.value.trim() || ".");
299
+ if (instance.treeMap.has(nextDirectory)) {
300
+ instance.state.currentDirectory = nextDirectory;
301
+ instance.state.selectedPath = "";
302
+ instance.render();
303
+ }
304
+ }
305
+ if (event.key === "Escape") {
306
+ event.preventDefault();
307
+ instance.cancelSelection();
308
+ }
309
+ };
310
+
311
+ directoryInput.onblur = () => {
312
+ directoryInput.value = instance.state.currentDirectory;
313
+ };
314
+
315
+ selectButton.onclick = () => {
316
+ const finalPath = instance.state.elementType === "directory" ? instance.state.currentDirectory : instance.state.selectedPath;
317
+ if (finalPath) {
318
+ instance.confirmSelection(finalPath);
319
+ }
320
+ };
321
+
322
+ cancelButton.onclick = () => {
323
+ instance.cancelSelection();
324
+ };
325
+
326
+ backdrop.onclick = () => {
327
+ instance.cancelSelection();
328
+ };
329
+
330
+ if (!window.__serverFilePickerEscapeHandler) {
331
+ window.__serverFilePickerEscapeHandler = (event) => {
332
+ if (event.key === "Escape" && overlay.style.display !== "none") {
333
+ event.preventDefault();
334
+ instance.cancelSelection();
335
+ }
336
+ };
337
+ window.addEventListener("keydown", window.__serverFilePickerEscapeHandler);
338
+ }
339
+
340
+ instance.render();
341
+ return instance;
342
+ }
343
+
344
+ function showOverlay(instance) {
345
+ instance.overlay.style.display = "grid";
346
+ instance.overlay.setAttribute("aria-hidden", "false");
347
+ }
348
+
349
+ function hideOverlay(instance) {
350
+ instance.overlay.style.display = "none";
351
+ instance.overlay.setAttribute("aria-hidden", "true");
352
+ }
353
+
354
+ export default function(component) {
355
+ const instanceKey = component.key || "server-file-picker-default";
356
+ let instance = instances.get(instanceKey);
357
+
358
+ if (!instance) {
359
+ instance = createInstance(component);
360
+ instances.set(instanceKey, instance);
361
+ } else {
362
+ instance.treeMap = buildDirectoryMap(component.data.tree);
363
+ instance.state = {
364
+ currentDirectory: normalizePath(component.data.current_directory || "."),
365
+ selectedPath: normalizePath(component.data.selected_path || ""),
366
+ elementType: component.data.element_type || "file",
367
+ topLevelDirectory: component.data.top_level_directory || null,
368
+ targetInputLabel: component.data.target_input_label || null,
369
+ showFiles: Boolean(component.data.show_files),
370
+ showHidden: Boolean(component.data.show_hidden),
371
+ extensions: Array.isArray(component.data.extensions) ? component.data.extensions : null,
372
+ tree: component.data.tree,
373
+ };
374
+ instance.render();
375
+ showOverlay(instance);
376
+ }
377
+
378
+ showOverlay(instance);
379
+
380
+ return () => {
381
+ // Keep the overlay alive across Streamlit reruns; it is explicitly closed
382
+ // by the cancel/select actions.
383
+ };
384
+ }
@@ -0,0 +1,6 @@
1
+ Metadata-Version: 2.4
2
+ Name: streamlit-server-file-picker
3
+ Version: 0.1.0
4
+ Summary: Standalone Streamlit file picker dialog package
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: streamlit
@@ -0,0 +1,9 @@
1
+ streamlit_server_file_picker/__init__.py,sha256=7B_0ea-GqJ2BbN09u5HQeibAvzlON_dkOVL-9dyyDW0,5658
2
+ streamlit_server_file_picker/component/__init__.py,sha256=eZHbQE9rJTUKGEUN1lhNsEHwSfsTruiWh4LPpBCgh6Y,556
3
+ streamlit_server_file_picker/component/frontend/component.css,sha256=LwTzX9yOzrl6sCavDg0BXkN-VqiTt6RdTDeClj9M8R0,5637
4
+ streamlit_server_file_picker/component/frontend/component.html,sha256=Hp97tp7iuMDyZXEJEBTgJxZLgCdIMh5lm07QBhZgd-4,42
5
+ streamlit_server_file_picker/component/frontend/component.js,sha256=8IRvMptC8cLQzR6ZJvlyzgH0XXoXLiy5tbLN0IVNhis,11776
6
+ streamlit_server_file_picker-0.1.0.dist-info/METADATA,sha256=OdnyId4x0Kp5nS64pU3KTXKy8kvEcJlS5kvWJeRjfEE,178
7
+ streamlit_server_file_picker-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
8
+ streamlit_server_file_picker-0.1.0.dist-info/top_level.txt,sha256=szezCJQ4inp3WqG0b7mGqJBQZ1k08_dXT8yoPvKdapo,29
9
+ streamlit_server_file_picker-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ streamlit_server_file_picker