main
html 1,154 lines 38.7 KB
Raw
1 <html
2 class="surface-modal file-browser-modal modal-no-backdrop"
3 data-surface-id="files"
4 data-surface-modal-path="modals/file-browser/file-browser.html"
5 data-surface-dock-title="Open Files in canvas"
6 data-surface-dock-icon="dock_to_right"
7 data-canvas-surface="files"
8 data-canvas-modal-path="modals/file-browser/file-browser.html"
9 data-canvas-dock-title="Open Files in canvas"
10 data-canvas-dock-icon="dock_to_right"
11 >
12 <head>
13 <title>File Browser</title>
14 <script type="module">
15 import { store } from "/components/modals/file-browser/file-browser-store.js";
16 </script>
17 </head>
18 <body class="file-browser-modal-body">
19 <div
20 x-data
21 class="file-browser-shell"
22 :class="{ 'is-surface': xAttrs($el)?.mode === 'canvas' }"
23 x-create="$store.fileBrowser.onMount($el, xAttrs($el) || {})"
24 x-destroy="$store.fileBrowser.onUnmount(xAttrs($el) || {})"
25 >
26 <template x-if="$store.fileBrowser">
27 <div class="file-browser-root" :class="{ 'is-surface': xAttrs($el)?.mode === 'canvas' }">
28
29 <!-- Loading State -->
30 <div x-show="$store.fileBrowser.isLoading" class="loading-state">
31 <div class="loading-spinner"></div>
32 <p>Loading files...</p>
33 </div>
34
35 <!-- File Browser Content -->
36 <div x-show="!$store.fileBrowser.isLoading" class="file-browser-content">
37 <!-- Path navigator -->
38 <div class="path-navigator-wrap">
39 <form class="path-navigator" @submit.prevent="$store.fileBrowser.submitPath()">
40 <button
41 type="button"
42 class="nav-button back-button"
43 @click="$store.fileBrowser.navigateUp()"
44 @dragover="$store.fileBrowser.setDropTarget($store.fileBrowser.browser.parentPath, $event)"
45 @dragleave="$store.fileBrowser.clearDropTarget($store.fileBrowser.browser.parentPath, $event)"
46 @drop="$store.fileBrowser.dropItems($store.fileBrowser.browser.parentPath, 'parent folder', $event)"
47 :class="{ 'is-drop-target': $store.fileBrowser.dragOverPath === $store.fileBrowser.browser.parentPath }"
48 :disabled="!$store.fileBrowser.browser.parentPath || $store.fileBrowser.isLoading"
49 aria-label="Navigate up"
50 title="Navigate up"
51 >
52 <x-icon aria-hidden="true" name="arrow_upward"></x-icon>
53 <span class="nav-button-label">Up</span>
54 </button>
55 <div class="path-input-shell" :class="{ 'has-error': $store.fileBrowser.pathError }">
56 <input
57 id="current-path"
58 class="path-input"
59 type="text"
60 x-model="$store.fileBrowser.pathInput"
61 @focus="$event.target.select()"
62 @keydown.escape.stop.prevent="$store.fileBrowser.resetPathInput()"
63 :aria-invalid="$store.fileBrowser.pathError ? 'true' : 'false'"
64 aria-label="Directory path"
65 spellcheck="false"
66 autocomplete="off"
67 />
68 <button
69 type="submit"
70 class="btn-icon-action path-submit"
71 :disabled="$store.fileBrowser.isPathSubmitting || $store.fileBrowser.isLoading"
72 aria-label="Go to directory"
73 >
74 <x-icon name="arrow_forward"></x-icon>
75 </button>
76 </div>
77 </form>
78 <template x-if="$store.fileBrowser.pathError">
79 <div class="path-error" x-text="$store.fileBrowser.pathError"></div>
80 </template>
81 </div>
82 <div class="file-browser-toolbar">
83 <div class="file-search-shell">
84 <x-icon class="file-search-icon" aria-hidden="true" name="search"></x-icon>
85 <input
86 type="search"
87 class="file-search-input"
88 x-model="$store.fileBrowser.searchQuery"
89 @keydown.escape="$store.fileBrowser.clearSearch()"
90 placeholder="Search files..."
91 aria-label="Search files"
92 />
93 <button
94 type="button"
95 class="btn-icon-action file-search-clear"
96 x-show="$store.fileBrowser.searchQuery"
97 @click="$store.fileBrowser.clearSearch()"
98 aria-label="Clear search"
99 title="Clear search"
100 >
101 <x-icon name="close"></x-icon>
102 </button>
103 </div>
104
105 <div class="new-item-buttons">
106 <button
107 type="button"
108 class="btn btn-ok btn-new-item"
109 x-show="!$store.fileBrowser.isPickerMode()"
110 @click="$store.fileBrowser.openNewFile()"
111 aria-label="New file"
112 title="New file"
113 >
114 <x-icon aria-hidden="true" name="note_add"></x-icon>
115 </button>
116 <button
117 type="button"
118 class="btn btn-ok btn-new-item"
119 x-show="!$store.fileBrowser.isTextOpenPicker()"
120 @click="$store.fileBrowser.openNewFolderModal()"
121 aria-label="New folder"
122 title="New folder"
123 >
124 <x-icon aria-hidden="true" name="create_new_folder"></x-icon>
125 </button>
126 </div>
127 </div>
128
129 <div class="data-status-bar data-status-bar-standalone file-status-bar">
130 <div class="status-info">
131 <span class="status-item">
132 <x-icon name="folder"></x-icon>
133 Total: <strong x-text="$store.fileBrowser.browser.entries.length"></strong>
134 </span>
135 <span class="status-separator"></span>
136 <span class="status-item">
137 Filtered: <strong x-text="$store.fileBrowser.filteredEntries.length"></strong>
138 </span>
139 <template x-if="$store.fileBrowser.selectedCount > 0">
140 <span class="status-separator"></span>
141 </template>
142 <template x-if="$store.fileBrowser.selectedCount > 0">
143 <span class="status-item">
144 Selected: <strong x-text="$store.fileBrowser.selectedCount"></strong>
145 </span>
146 </template>
147 </div>
148 </div>
149
150 <div x-show="$store.fileBrowser.selectedCount > 0 && !$store.fileBrowser.isPickerMode()" class="mass-action-toolbar file-mass-toolbar">
151 <div class="selection-info" x-text="$store.fileBrowser.selectedCountLabel"></div>
152
153 <div class="mass-actions">
154 <button
155 type="button"
156 class="btn btn-mass copy"
157 @click="$store.fileBrowser.copySelectedPaths()"
158 :disabled="$store.fileBrowser.isBulkBusy"
159 title="Copy Selected Paths"
160 >
161 <x-icon name="content_copy"></x-icon>
162 Copy Paths
163 </button>
164
165 <button
166 type="button"
167 class="btn btn-mass export"
168 @click="$store.fileBrowser.bulkDownloadFiles()"
169 :disabled="$store.fileBrowser.isBulkBusy"
170 title="Download Selected Items"
171 >
172 <x-icon name="folder_zip"></x-icon>
173 Download ZIP
174 </button>
175
176 <button
177 type="button"
178 class="btn btn-mass delete"
179 @click="$confirmClick($event, () => $store.fileBrowser.bulkDeleteFiles())"
180 :disabled="$store.fileBrowser.isBulkBusy"
181 title="Delete Selected Items"
182 >
183 <x-icon name="delete"></x-icon>
184 Delete
185 </button>
186
187 <button
188 type="button"
189 class="btn btn-mass clear"
190 @click="$store.fileBrowser.clearSelection()"
191 :disabled="$store.fileBrowser.isBulkBusy"
192 title="Clear Selection"
193 >
194 <x-icon name="close"></x-icon>
195 Clear
196 </button>
197 </div>
198 </div>
199
200 <!-- Files list -->
201 <div class="files-list" @scroll="$store.fileBrowser.closeDropdown()">
202 <div class="file-header">
203 <div class="file-cell-select">
204 <input
205 type="checkbox"
206 :checked="$store.fileBrowser.allVisibleSelected"
207 :indeterminate="$store.fileBrowser.someVisibleSelected && !$store.fileBrowser.allVisibleSelected"
208 @change="$store.fileBrowser.toggleSelectAllVisible()"
209 :disabled="$store.fileBrowser.selectableEntries.length === 0"
210 title="Select visible items"
211 aria-label="Select visible items"
212 />
213 </div>
214 <div class="file-cell" @click="$store.fileBrowser.toggleSort('name')">Name <span x-show="$store.fileBrowser.browser.sortBy === 'name'" x-text="$store.fileBrowser.browser.sortDirection === 'asc' ? '↑' : '↓'"></span></div>
215 <div class="file-cell-size" @click="$store.fileBrowser.toggleSort('size')">Size <span x-show="$store.fileBrowser.browser.sortBy === 'size'" x-text="$store.fileBrowser.browser.sortDirection === 'asc' ? '↑' : '↓'"></span></div>
216 <div class="file-cell-date" @click="$store.fileBrowser.toggleSort('date')">Modified <span x-show="$store.fileBrowser.browser.sortBy === 'date'" x-text="$store.fileBrowser.browser.sortDirection === 'asc' ? '↑' : '↓'"></span></div>
217 <div class="file-cell-actions"></div>
218 </div>
219
220 <!-- File list entries -->
221 <template x-if="$store.fileBrowser.visibleEntries.length">
222 <template x-for="file in $store.fileBrowser.visibleEntries" :key="file.path">
223 <div
224 class="file-item"
225 :data-is-dir="file.is_dir"
226 :draggable="!$store.fileBrowser.isPickerMode() && !$store.fileBrowser.isBulkBusy"
227 :class="{
228 'selected': file.selected,
229 'is-dragging': $store.fileBrowser.isDraggingPath(file.path),
230 'is-drop-target': file.is_dir && $store.fileBrowser.dragOverPath === file.path
231 }"
232 @dragstart="$store.fileBrowser.startDrag(file, $event)"
233 @dragend="$store.fileBrowser.clearDragState()"
234 @dragover="file.is_dir && $store.fileBrowser.setDropTarget(file.path, $event)"
235 @dragleave="file.is_dir && $store.fileBrowser.clearDropTarget(file.path, $event)"
236 @drop="file.is_dir && $store.fileBrowser.dropItems(file.path, file.name, $event)"
237 >
238 <label class="file-select-cell" @click.stop>
239 <input
240 type="checkbox"
241 x-model="file.selected"
242 :disabled="!$store.fileBrowser.isSelectableEntry(file)"
243 :aria-label="`Select ${file.name}`"
244 />
245 </label>
246 <div class="file-name" @click="$store.fileBrowser.handleFileNameClick(file)">
247 <img :src="'/public/' + (file.type === 'unknown' ? 'file' : ($store.fileBrowser.isArchive(file.name) ? 'archive' : file.type)) + '.svg'" class="file-icon" :alt="file.type" draggable="false" />
248 <span x-text="file.name"></span>
249 </div>
250 <div class="file-size" x-text="$store.fileBrowser.formatFileSize(file.size)"></div>
251 <div class="file-date" x-text="$store.fileBrowser.formatDate(file.modified)"></div>
252 <div class="file-actions" x-show="!$store.fileBrowser.isPickerMode()">
253 <button
254 type="button"
255 class="btn-icon-action file-editor-open-action"
256 x-show="$store.fileBrowser.isEditorSurface(file)"
257 @click.stop="$store.fileBrowser.openInSurface(file)"
258 :title="$store.fileBrowser.surfaceActionTitle(file)"
259 aria-label="Open in Editor"
260 >
261 <x-icon aria-hidden="true" :name="$store.fileBrowser.surfaceActionIcon(file)"></x-icon>
262 </button>
263
264 <!-- Secondary single-item actions (Rename/...) are grouped under a dropdown -->
265 <div
266 class="dropdown file-actions-dropdown"
267 @click.outside="$store.fileBrowser.closeDropdown()"
268 @keydown.escape.window="$store.fileBrowser.closeDropdown()"
269 >
270 <button
271 type="button"
272 class="btn-icon-action dropdown-trigger"
273 @click.stop="$store.fileBrowser.toggleDropdown(file.path, $event.currentTarget)"
274 :aria-expanded="$store.fileBrowser.isDropdownOpen(file.path).toString()"
275 aria-label="More actions"
276 title="More actions"
277 >
278 <x-icon name="more_vert"></x-icon>
279 </button>
280
281 <template x-teleport="body">
282 <div
283 class="dropdown-menu file-actions-menu"
284 x-show="$store.fileBrowser.isDropdownOpen(file.path)"
285 x-transition
286 :style="$store.fileBrowser.dropdownStyle"
287 style="display: none;"
288 @click.stop="$store.fileBrowser.closeDropdown()"
289 >
290 <button
291 type="button"
292 class="dropdown-item"
293 x-show="$store.fileBrowser.canOpenInActionMenu(file)"
294 @click="$store.fileBrowser.openInSurface(file)"
295 :title="$store.fileBrowser.surfaceActionTitle(file)"
296 >
297 <x-icon :name="$store.fileBrowser.surfaceActionIcon(file)"></x-icon>
298 <span x-text="$store.fileBrowser.surfaceActionLabel(file)"></span>
299 </button>
300
301 <button
302 type="button"
303 class="dropdown-item"
304 x-show="!file.is_dir && file.size <= 1048576 && !$store.fileBrowser.canOpenInSurface(file)"
305 @click="$store.fileBrowser.openFileEditor(file)"
306 >
307 <x-icon name="file_open"></x-icon>
308 <span>Edit</span>
309 </button>
310
311 <button
312 type="button"
313 class="dropdown-item"
314 x-show="file.is_dir"
315 @click="$store.fileBrowser.downloadFile(file)"
316 >
317 <x-icon name="folder_zip"></x-icon>
318 <span>Download ZIP</span>
319 </button>
320
321 <button
322 type="button"
323 class="dropdown-item"
324 x-show="!file.is_dir && $store.fileBrowser.isArchive(file.name)"
325 @click="$store.fileBrowser.extractArchive(file)"
326 >
327 <x-icon name="unarchive"></x-icon>
328 <span>Extract</span>
329 </button>
330
331 <button
332 type="button"
333 class="dropdown-item"
334 @click="$store.fileBrowser.openRenameModal(file)"
335 >
336 <x-icon name="drive_file_rename_outline"></x-icon>
337 <span>Rename</span>
338 </button>
339 </div>
340 </template>
341 </div>
342 <button class="btn-icon-action" x-show="!file.is_dir" @click.stop="$store.fileBrowser.downloadFile(file)" title="Download file">
343 <x-icon name="download"></x-icon>
344 </button>
345 <button class="btn-icon-action danger" @click.stop="$confirmClick($event, () => $store.fileBrowser.deleteFile(file))" title="Delete item">
346 <x-icon name="delete"></x-icon>
347 </button>
348 </div>
349 </div>
350 </template>
351 </template>
352
353 <!-- Empty state -->
354 <template x-if="!$store.fileBrowser.browser.entries.length">
355 <div class="no-files">No files found</div>
356 </template>
357 <template x-if="$store.fileBrowser.browser.entries.length && !$store.fileBrowser.visibleEntries.length">
358 <div class="no-files">
359 <div>No matching files found</div>
360 <button class="btn btn-cancel btn-clear-search" @click="$store.fileBrowser.clearSearch()">
361 <x-icon name="close"></x-icon>
362 Clear
363 </button>
364 </div>
365 </template>
366 </div>
367 </div>
368 </div>
369
370 </div>
371 </template>
372
373 <!-- Modal Footer (outside template x-if so it exists immediately) -->
374 <template x-if="$store.fileBrowser">
375 <div class="modal-footer file-browser-footer" data-modal-footer :class="{ 'is-surface': xAttrs($el)?.mode === 'canvas' }">
376 <div class="file-browser-picker-actions" x-show="$store.fileBrowser.isPickerMode()" style="display: none;">
377 <template x-if="$store.fileBrowser.isSaveAsPicker()">
378 <div class="picker-filename-field">
379 <x-icon aria-hidden="true" name="article"></x-icon>
380 <input
381 type="text"
382 class="picker-filename-input"
383 x-model="$store.fileBrowser.pickerFilename"
384 @input="$store.fileBrowser.onPickerFilenameInput()"
385 @keydown.enter.prevent="$store.fileBrowser.confirmPicker()"
386 placeholder="File name"
387 aria-label="File name"
388 spellcheck="false"
389 />
390 </div>
391 </template>
392 <span
393 class="picker-selection-label"
394 x-show="$store.fileBrowser.isTextOpenPicker()"
395 x-text="$store.fileBrowser.pickerSelectionLabel()"
396 ></span>
397 <template x-if="$store.fileBrowser.pickerFilenameError">
398 <span class="picker-inline-error" x-text="$store.fileBrowser.pickerFilenameError"></span>
399 </template>
400 <button
401 type="button"
402 class="btn btn-ok picker-confirm-button"
403 :disabled="!$store.fileBrowser.canConfirmPicker() || $store.fileBrowser.isBulkBusy"
404 @click="$store.fileBrowser.confirmPicker()"
405 x-text="$store.fileBrowser.pickerConfirmLabel"
406 ></button>
407 <button type="button" class="btn btn-cancel" :disabled="$store.fileBrowser.isBulkBusy" @click="$store.fileBrowser.cancelPicker()">Cancel</button>
408 </div>
409 <label class="btn btn-upload" x-show="!$store.fileBrowser.isPickerMode()">
410 <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5m-13.5-9L12 3m0 0 4.5 4.5M12 3v13.5"/></svg>
411 Upload Files
412 <input type="file" multiple accept="*" @change="$store.fileBrowser.handleFileUpload" style="display:none;" />
413 </label>
414 <button class="btn btn-cancel" x-show="xAttrs($el)?.mode !== 'canvas' && !$store.fileBrowser.isPickerMode()" @click="$store.fileBrowser.handleClose()">Close Browser</button>
415 </div>
416 </template>
417 </div>
418
419 <style>
420 .file-browser-shell {
421 width: 100%;
422 min-width: 0;
423 }
424
425 .modal-inner.file-browser-modal {
426 box-sizing: border-box;
427 width: min(78vw, 1040px);
428 height: min(82vh, 820px);
429 min-width: min(420px, calc(100vw - 16px));
430 min-height: min(360px, calc(100vh - 16px));
431 max-width: calc(100vw - 16px);
432 max-height: calc(100vh - 16px);
433 resize: both;
434 overflow: hidden;
435 }
436
437 .modal-inner.file-browser-modal.is-focus-mode {
438 resize: none;
439 border-radius: 6px;
440 }
441
442 .modal-inner.file-browser-modal .modal-header {
443 min-height: 34px;
444 padding: 0.35rem 0.75rem 0.35rem 1rem;
445 background: color-mix(in srgb, var(--color-background) 92%, #000 8%);
446 border-bottom: 1px solid color-mix(in srgb, var(--color-border) 70%, transparent);
447 }
448
449 .modal-inner.file-browser-modal .modal-scroll,
450 .modal-inner.file-browser-modal .modal-bd.file-browser-modal-body {
451 display: flex;
452 flex: 1 1 auto;
453 width: 100%;
454 height: 100%;
455 min-width: 0;
456 min-height: 0;
457 max-height: none;
458 overflow: hidden;
459 padding: 0;
460 }
461
462 .modal-inner.file-browser-modal .modal-bd.file-browser-modal-body > div[x-data],
463 .modal-inner.file-browser-modal .file-browser-shell {
464 display: flex;
465 flex: 1 1 auto;
466 flex-direction: column;
467 height: 100%;
468 min-width: 0;
469 min-height: 0;
470 }
471
472 .file-browser-shell.is-surface {
473 display: flex;
474 flex: 1 1 auto;
475 flex-direction: column;
476 height: 100%;
477 min-height: 0;
478 }
479
480 /* File Browser Root */
481 .file-browser-root {
482 container: file-browser / inline-size;
483 display: flex;
484 flex-direction: column;
485 width: 100%;
486 height: 100%;
487 min-height: 400px;
488 }
489
490 .file-browser-root.is-surface {
491 flex: 1 1 auto;
492 min-height: 0;
493 }
494
495 .modal-inner.file-browser-modal .file-browser-root {
496 flex: 1 1 auto;
497 min-height: 0;
498 }
499
500 /* Loading State */
501 .loading-state {
502 display: flex;
503 flex-direction: column;
504 align-items: center;
505 justify-content: center;
506 padding: var(--spacing-lg);
507 min-height: 200px;
508 }
509
510 .loading-spinner {
511 width: 40px;
512 height: 40px;
513 border: 3px solid var(--color-border);
514 border-top: 3px solid var(--color-primary);
515 border-radius: 50%;
516 animation: spin 1s linear infinite;
517 margin-bottom: var(--spacing-md);
518 }
519
520 @keyframes spin {
521 0% { transform: rotate(0deg); }
522 100% { transform: rotate(360deg); }
523 }
524
525 .loading-state p {
526 color: var(--color-text-secondary);
527 margin: 0;
528 }
529
530 /* File Browser Content */
531 .file-browser-content {
532 display: flex;
533 flex-direction: column;
534 padding: var(--spacing-sm) var(--spacing-sm);
535 gap: var(--spacing-sm);
536 min-height: 0;
537 }
538
539 .file-browser-shell.is-surface .file-browser-content {
540 flex: 1 1 auto;
541 overflow: hidden;
542 }
543
544 .modal-inner.file-browser-modal .file-browser-content {
545 flex: 1 1 auto;
546 overflow: hidden;
547 }
548
549 /* File Browser Styles */
550 .files-list {
551 width: 100%;
552 border-radius: 4px;
553 }
554
555 .file-browser-shell.is-surface .files-list {
556 flex: 1 1 auto;
557 min-height: 0;
558 overflow: auto;
559 border: 1px solid var(--color-border);
560 background: color-mix(in srgb, var(--color-panel) 82%, transparent);
561 }
562
563 .modal-inner.file-browser-modal .files-list {
564 flex: 1 1 auto;
565 min-height: 0;
566 overflow: auto;
567 border: 1px solid var(--color-border);
568 background: color-mix(in srgb, var(--color-panel) 82%, transparent);
569 }
570
571 .file-browser-shell.is-surface .file-header {
572 position: sticky;
573 top: 0;
574 z-index: 2;
575 }
576
577 .modal-inner.file-browser-modal .file-header {
578 position: sticky;
579 top: 0;
580 z-index: 2;
581 }
582
583 /* Header Styles */
584 .file-header {
585 width: 100%;
586 border-radius: 4px;
587 overflow: hidden;
588 display: grid;
589 grid-template-columns: 2.5rem minmax(0, 1.5fr) minmax(5.5rem, 0.7fr) minmax(9rem, 1fr) 8.75rem;
590 background: color-mix(in srgb, var(--color-panel) 88%, var(--color-background) 12%);
591 padding: 8px 0;
592 font-weight: bold;
593 border-bottom: 1px solid var(--color-border);
594 color: var(--color-primary);
595 }
596 .file-cell-select,
597 .file-cell-actions {
598 display: flex;
599 align-items: center;
600 justify-content: center;
601 }
602 .file-cell,
603 .file-cell-size,
604 .file-cell-date {
605 color: var(--color-primary);
606 padding: 4px;
607 cursor: pointer;
608 }
609
610 /* File Item Styles */
611 .file-item {
612 display: grid;
613 grid-template-columns: 2.5rem minmax(0, 1.5fr) minmax(5.5rem, 0.7fr) minmax(9rem, 1fr) 8.75rem;
614 align-items: center;
615 padding: 8px 0;
616 font-size: 0.875rem;
617 border-top: 1px solid var(--color-border);
618 transition: background-color 120ms cubic-bezier(0.2, 0, 0, 1), box-shadow 120ms cubic-bezier(0.2, 0, 0, 1), opacity 120ms ease, transform 120ms cubic-bezier(0.2, 0, 0, 1);
619 white-space: nowrap;
620 border-radius: 4px;
621 overflow: visible; /* allow action dropdown menus to overflow the row */
622 color: var(--color-text);
623 }
624 .file-item:hover {
625 background-color: var(--color-secondary);
626 }
627 .file-item.selected {
628 background: color-mix(in srgb, var(--color-primary) 12%, transparent);
629 }
630 .file-item.is-dragging {
631 opacity: 0.45;
632 transform: scale(0.995);
633 }
634 .file-item.is-drop-target,
635 .nav-button.is-drop-target {
636 background: color-mix(in srgb, var(--color-primary) 18%, var(--color-input) 82%);
637 box-shadow: inset 0 0 0 2px color-mix(in srgb, var(--color-primary) 72%, transparent);
638 }
639 .file-item.is-drop-target .file-icon,
640 .nav-button.is-drop-target .material-symbols-outlined {
641 transform: scale(1.12);
642 }
643
644 .file-select-cell {
645 display: flex;
646 align-items: center;
647 justify-content: center;
648 cursor: pointer;
649 min-height: 1.75rem;
650 }
651
652 .file-cell-select input,
653 .file-select-cell input {
654 width: 1rem;
655 height: 1rem;
656 accent-color: var(--color-primary);
657 cursor: pointer;
658 }
659
660 /* File Icon and Name */
661 .file-icon {
662 width: 1.8rem;
663 height: 1.8rem;
664 margin: 0 1rem 0 0.7rem;
665 vertical-align: middle;
666 font-size: var(--font-size-sm);
667 transition: transform 120ms cubic-bezier(0.2, 0, 0, 1);
668 }
669 .file-name {
670 display: flex;
671 align-items: center;
672 font-weight: 500;
673 margin-right: var(--spacing-sm);
674 overflow: hidden;
675 }
676 .file-name > span {
677 white-space: nowrap;
678 overflow: hidden;
679 text-overflow: ellipsis;
680 }
681 .file-size,
682 .file-date {
683 color: var(--color-text-secondary);
684 }
685 /* No Files Message */
686 .no-files {
687 padding: 32px;
688 text-align: center;
689 color: var(--color-text-secondary);
690 }
691 .no-files .btn-clear-search {
692 display: inline-flex;
693 align-items: center;
694 gap: 0.25rem;
695 margin-top: 0.75rem;
696 }
697 /* Light Mode Adjustments */
698 .light-mode .file-item:hover {
699 background-color: var(--color-secondary-light);
700 }
701
702 /* Path Navigator Styles */
703 .path-navigator-wrap {
704 display: flex;
705 flex-direction: column;
706 gap: 0.35rem;
707 }
708
709 .path-navigator {
710 overflow: hidden;
711 display: flex;
712 align-items: center;
713 gap: 0.75rem;
714 background-color: var(--color-message-bg);
715 padding: 0.5rem var(--spacing-sm);
716 margin: 0;
717 border: 1px solid var(--color-border);
718 border-radius: 8px;
719 }
720
721 .path-navigator .back-button {
722 flex: 0 0 auto;
723 }
724
725 .path-input-shell {
726 position: relative;
727 display: flex;
728 align-items: center;
729 flex: 1;
730 min-width: 0;
731 }
732
733 .path-input {
734 width: 100%;
735 height: 2.25rem;
736 min-width: 0;
737 border: 1px solid transparent;
738 border-radius: 6px;
739 background: color-mix(in srgb, var(--color-input) 78%, transparent);
740 color: var(--color-text);
741 padding: 0 2.5rem 0 0.75rem;
742 font: inherit;
743 font-family: 'Roboto Mono', monospace;
744 -webkit-font-optical-sizing: auto;
745 font-optical-sizing: auto;
746 line-height: 1;
747 transition: border-color 0.15s ease, box-shadow 0.15s ease, background-color 0.15s ease;
748 }
749
750 .path-input:focus {
751 outline: none;
752 border-color: var(--color-primary);
753 background: var(--color-input-focus);
754 box-shadow: 0 0 0 1px color-mix(in srgb, var(--color-primary) 20%, transparent);
755 }
756
757 .path-input-shell.has-error .path-input {
758 border-color: var(--color-error);
759 box-shadow: 0 0 0 1px color-mix(in srgb, var(--color-error) 18%, transparent);
760 }
761
762 .path-submit {
763 position: absolute;
764 right: 0.35rem;
765 width: 1.75rem;
766 height: 1.75rem;
767 color: var(--color-text);
768 }
769
770 .path-submit:disabled {
771 opacity: 0.5;
772 cursor: wait;
773 }
774
775 .path-error {
776 color: var(--color-error);
777 font-size: 0.8rem;
778 padding: 0 0.25rem;
779 }
780
781 .file-browser-toolbar {
782 display: flex;
783 align-items: center;
784 justify-content: space-between;
785 gap: var(--spacing-sm);
786 }
787
788 .file-search-shell {
789 position: relative;
790 display: flex;
791 align-items: center;
792 min-width: 16rem;
793 flex: 1;
794 }
795
796 .file-search-icon {
797 position: absolute;
798 left: 0.65rem;
799 font-size: 1.1rem;
800 color: var(--color-primary);
801 opacity: 0.72;
802 pointer-events: none;
803 }
804
805 .file-search-input {
806 width: 100%;
807 height: 2.4rem;
808 border: 1px solid var(--color-border);
809 border-radius: 6px;
810 background: var(--color-input);
811 color: var(--color-text);
812 padding: 0 2.35rem 0 2.2rem;
813 font: inherit;
814 transition: border-color 0.15s ease, box-shadow 0.15s ease, background-color 0.15s ease;
815 }
816
817 .file-search-input:focus {
818 outline: none;
819 border-color: var(--color-primary);
820 background: var(--color-input-focus);
821 box-shadow: 0 0 0 1px color-mix(in srgb, var(--color-primary) 20%, transparent);
822 }
823
824 .file-search-clear {
825 position: absolute;
826 right: 0.35rem;
827 width: 1.65rem;
828 height: 1.65rem;
829 color: var(--color-text);
830 }
831
832 .new-item-buttons {
833 display: flex;
834 justify-content: flex-end;
835 gap: 0.5em;
836 }
837 .new-item-buttons button {
838 margin-left: 0;
839 }
840
841 .file-status-bar {
842 border-radius: 8px 8px 0 0;
843 margin-bottom: calc(-1 * var(--spacing-sm));
844 }
845
846 .file-mass-toolbar {
847 border-left: 1px solid var(--color-border);
848 border-right: 1px solid var(--color-border);
849 border-bottom: 1px solid var(--color-border);
850 }
851
852 .file-mass-toolbar .btn-mass:disabled {
853 opacity: 0.55;
854 cursor: wait;
855 }
856
857 .nav-button {
858 appearance: none;
859 display: inline-flex;
860 align-items: center;
861 justify-content: center;
862 gap: 0.1rem;
863 min-height: 2.35rem;
864 padding: 0.22rem 0.62rem 0.24rem;
865 border: 1px solid var(--color-border);
866 border-radius: 6px;
867 background: color-mix(in srgb, var(--color-input) 86%, var(--color-panel) 14%);
868 color: var(--color-text);
869 font: inherit;
870 line-height: 1;
871 box-shadow: none;
872 cursor: pointer;
873 transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease, opacity 0.15s ease;
874 }
875 .nav-button.back-button {
876 flex: 0 0 auto;
877 flex-direction: column;
878 min-width: 3.1rem;
879 }
880 .nav-button .material-symbols-outlined {
881 font-size: 1.05rem;
882 line-height: 1;
883 transition: transform 120ms cubic-bezier(0.2, 0, 0, 1);
884 }
885 .nav-button-label {
886 font-size: 0.66rem;
887 font-weight: 600;
888 letter-spacing: 0;
889 line-height: 1;
890 }
891 .nav-button:hover:not(:disabled) {
892 border-color: color-mix(in srgb, var(--color-primary) 34%, var(--color-border));
893 background: color-mix(in srgb, var(--color-input-focus) 84%, var(--color-primary) 16%);
894 color: var(--color-text);
895 }
896 .nav-button:focus-visible {
897 outline: none;
898 border-color: var(--color-primary);
899 box-shadow: 0 0 0 2px color-mix(in srgb, var(--color-primary) 22%, transparent);
900 }
901 .nav-button:disabled {
902 cursor: not-allowed;
903 opacity: 0.5;
904 }
905 /* Folder Specific Styles */
906 .file-item[data-is-dir="true"] {
907 cursor: pointer;
908 }
909 .file-item[draggable="true"] {
910 cursor: grab;
911 }
912 .file-item[draggable="true"]:active {
913 cursor: grabbing;
914 }
915 .file-item[data-is-dir="true"]:hover {
916 background-color: var(--color-secondary);
917 }
918 .file-item[data-is-dir="true"].is-drop-target:hover {
919 background: color-mix(in srgb, var(--color-primary) 18%, var(--color-input) 82%);
920 }
921
922 /* Upload Button Styles */
923 .btn-upload {
924 display: inline-flex;
925 align-items: center;
926 padding: 8px 16px;
927 background: #4248f1;
928 gap: 0.5rem;
929 color: white;
930 border-radius: 4px;
931 cursor: pointer;
932 transition: background-color 0.3s ease-in-out;
933 }
934 .btn-upload > svg {
935 width: 20px;
936 }
937 .btn-upload:hover {
938 background-color: #353bc5;
939 }
940 .btn-upload:active {
941 background-color: #2b309c;
942 }
943
944 .file-browser-footer.is-surface {
945 display: flex;
946 flex: 0 0 auto;
947 justify-content: flex-end;
948 gap: var(--spacing-sm);
949 padding: var(--spacing-sm);
950 border-top: 1px solid var(--color-border);
951 background: color-mix(in srgb, var(--color-panel) 90%, var(--color-background) 10%);
952 }
953
954 .file-browser-picker-actions {
955 display: flex;
956 flex: 1 1 auto;
957 align-items: center;
958 justify-content: flex-end;
959 gap: var(--spacing-sm);
960 min-width: 0;
961 }
962
963 .picker-filename-field {
964 display: flex;
965 align-items: center;
966 flex: 1 1 16rem;
967 max-width: 24rem;
968 min-width: 10rem;
969 height: 2.35rem;
970 border: 1px solid var(--color-border);
971 border-radius: 7px;
972 background: var(--color-input);
973 color: var(--color-text);
974 }
975
976 .picker-filename-field .material-symbols-outlined {
977 flex: 0 0 auto;
978 padding-left: 0.65rem;
979 color: var(--color-primary);
980 font-size: 1.1rem;
981 opacity: 0.78;
982 }
983
984 .picker-filename-input {
985 flex: 1 1 auto;
986 width: 100%;
987 min-width: 0;
988 height: 100%;
989 border: 0;
990 outline: 0;
991 background: transparent;
992 color: var(--color-text);
993 padding: 0 0.7rem;
994 font: inherit;
995 }
996
997 .picker-selection-label,
998 .picker-inline-error {
999 overflow: hidden;
1000 text-overflow: ellipsis;
1001 white-space: nowrap;
1002 font-size: 0.82rem;
1003 font-weight: 700;
1004 letter-spacing: 0;
1005 }
1006
1007 .picker-selection-label {
1008 color: var(--color-text-secondary);
1009 }
1010
1011 .picker-inline-error {
1012 color: var(--color-error);
1013 }
1014
1015 .picker-confirm-button {
1016 min-width: 7.6rem;
1017 }
1018
1019 /* File Actions */
1020 .file-actions {
1021 display: flex;
1022 gap: var(--spacing-xs);
1023 justify-content: flex-end;
1024 padding-right: 0.5rem;
1025 }
1026 .file-editor-open-action {
1027 border-color: color-mix(in srgb, var(--color-primary) 52%, var(--color-border));
1028 background: color-mix(in srgb, var(--color-primary) 10%, transparent);
1029 }
1030 .file-actions-menu {
1031 margin: 0;
1032 }
1033 .btn-new-item {
1034 display: inline-flex;
1035 align-items: center;
1036 justify-content: center;
1037 gap: 0.4rem;
1038 width: 2.8rem;
1039 height: 2.8rem;
1040 min-width: 2.8rem;
1041 padding: 0;
1042 border-radius: 8px;
1043 }
1044 .btn-new-item .material-symbols-outlined {
1045 font-size: 1.35rem;
1046 line-height: 1;
1047 }
1048 .btn-secondary {
1049 background: var(--color-secondary);
1050 color: var(--color-text);
1051 border: 1px solid var(--color-border);
1052 }
1053 .btn-secondary:hover {
1054 filter: brightness(1.05);
1055 }
1056 .btn-secondary:active {
1057 filter: brightness(0.95);
1058 }
1059 /* Responsive Design */
1060 @media (max-width: 768px) {
1061 .path-navigator {
1062 align-items: center;
1063 flex-direction: row;
1064 gap: 0.5rem;
1065 }
1066 .path-navigator .back-button {
1067 align-self: stretch;
1068 }
1069 .file-browser-toolbar {
1070 align-items: center;
1071 flex-direction: row;
1072 gap: 0.5rem;
1073 }
1074 .file-search-shell {
1075 flex: 1 1 auto;
1076 min-width: 0;
1077 width: auto;
1078 }
1079 .new-item-buttons {
1080 flex: 0 0 auto;
1081 gap: 0.4rem;
1082 }
1083 .btn-new-item {
1084 width: 2.5rem;
1085 min-width: 2.5rem;
1086 height: 2.5rem;
1087 }
1088 .file-header {
1089 grid-template-columns: 2.25rem minmax(0, 1fr) minmax(5rem, 0.52fr) 8.75rem;
1090 }
1091 .file-item {
1092 grid-template-columns: 2.25rem minmax(0, 1fr) minmax(5rem, 0.5fr) 8.75rem;
1093 }
1094 .file-cell-date,
1095 .file-date {
1096 display: none;
1097 }
1098 .file-actions {
1099 gap: 0.2rem;
1100 padding-right: 0.2rem;
1101 }
1102 }
1103 @media (max-width: 540px) {
1104 .path-navigator .back-button {
1105 min-width: 2.45rem;
1106 padding-inline: 0.5rem;
1107 }
1108 .path-navigator .nav-button-label {
1109 display: none;
1110 }
1111 .file-header,
1112 .file-item {
1113 grid-template-columns: 2.25rem minmax(0, 1fr) minmax(4.25rem, max-content) 8rem;
1114 }
1115 .file-cell-date,
1116 .file-date {
1117 display: none;
1118 }
1119 }
1120 @container file-browser (max-width: 620px) {
1121 .file-header,
1122 .file-item {
1123 grid-template-columns: 2.25rem minmax(0, 1fr) minmax(4.25rem, max-content) 8rem;
1124 }
1125 .file-cell-date,
1126 .file-date {
1127 display: none;
1128 }
1129 .file-actions {
1130 gap: 0.2rem;
1131 padding-right: 0.2rem;
1132 }
1133 .file-browser-picker-actions {
1134 flex-wrap: wrap;
1135 justify-content: stretch;
1136 }
1137 .picker-filename-field {
1138 flex-basis: 100%;
1139 max-width: none;
1140 }
1141 .picker-confirm-button {
1142 flex: 1 1 auto;
1143 }
1144 }
1145 @media (prefers-reduced-motion: reduce) {
1146 .file-item,
1147 .file-icon,
1148 .nav-button .material-symbols-outlined {
1149 transition: none;
1150 }
1151 }
1152 </style>
1153 </body>
1154 </html>