Skip to content

Commit bc27c55

Browse files
vpeterssonclaude
andauthored
fix(server): polish home bulk-action & upload UI/UX (#3066)
* fix(server): polish home bulk-action & upload UI/UX Follow-up UI/UX pass over the recently shipped bulk asset management (#3048), multi-file upload (#3049), and ffmpeg/HandBrake rejection hints (#3040). - Reserve bottom space (.has-bulk-selection) while a selection is active so the fixed bulk-action bar never floats over the last rows or their action buttons — exactly the assets a bulk selection targets. - Cap .modal-card to the viewport and scroll inside it, with sticky header/footer, so a tall bulk-edit form (or the Edit modal with the failure alert + Advanced open) keeps its title and Apply/Save buttons reachable instead of pushing them below the fold. - Wire real drag-and-drop on the upload dropzone (dropFiles() feeds the same sequential uploadFiles() batch path); the dashed zone already read as a drop target but silently ignored drops. - Lift the selection checkbox contrast on the dark Enabled surface so the select-all (checked/indeterminate) and per-row boxes are legible against the purple gradient; scoped to .asset-select so the activity switch keeps its track styling. - Anchor the bulk bar's clear (x) to the top-right on phones so it no longer wraps beside the destructive Delete (mis-tap risk). - Stack the ffmpeg recipe's copy button under the command at narrow widths; make the empty-state CTA a <button> (action, not nav); drop the bulk duration field's placeholder that collided with its floating label. Presentational only — no API/model changes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(server): stop dropzone highlight flicker on child drag-over Copilot review on #3066: dragleave bubbles from the dropzone's child icon/paragraphs, toggling dragActive off while the cursor is still over the label and flickering the highlight. Add the .self modifier so the handler only runs when leaving the label itself. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(server): clear dropzone drag highlight when the modal closes Second Copilot pass on #3066: dragActive could stay true if the user drags into the dropzone then closes the Add modal (Esc/backdrop/Cancel) before dragleave fires, so the dropzone re-opened still highlighted. Reset dragActive in closeModal() alongside the other modal state. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 8a4db73 commit bc27c55

6 files changed

Lines changed: 120 additions & 8 deletions

File tree

src/anthias_server/app/static/sass/_styles.scss

Lines changed: 84 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -908,10 +908,23 @@ label { text-align: left; }
908908
font-size: 0.9rem;
909909
margin: 0 0 var(--space-3);
910910
}
911-
a {
911+
// The call-to-action is an action (opens the Add modal), not
912+
// navigation, so it's a <button> styled as a link.
913+
a,
914+
.empty-state__action {
915+
font: inherit;
912916
font-weight: 600;
917+
padding: 0;
918+
border: none;
919+
background: none;
920+
cursor: pointer;
913921
color: var(--surface-anchor);
914922
&:hover { color: var(--surface-anchor-hover); }
923+
&:focus-visible {
924+
outline: none;
925+
border-radius: var(--radius-sm);
926+
box-shadow: 0 0 0 var(--ring-width) var(--color-focus-ring);
927+
}
915928
}
916929
}
917930

@@ -939,7 +952,15 @@ label { text-align: left; }
939952
width: 100%;
940953
max-width: 720px;
941954
margin: auto;
942-
overflow: hidden;
955+
// Cap the card to the viewport (minus the overlay's space-5 padding
956+
// top + bottom) and scroll inside it, so a tall modal — the
957+
// bulk-edit form with every group expanded, or the Edit modal with
958+
// the failure alert + Advanced open — never pushes its footer below
959+
// the fold. The header/footer pin via position: sticky so the title
960+
// and the Save/Apply buttons stay reachable while the body scrolls.
961+
// (overflow-y: auto still clips children to the rounded corners.)
962+
max-height: calc(100vh - var(--space-5) * 2);
963+
overflow-y: auto;
943964
color: var(--color-text);
944965
animation: modal-in 220ms var(--ease-out) both;
945966
}
@@ -954,6 +975,13 @@ label { text-align: left; }
954975
.modal-card__header {
955976
padding: var(--space-5) var(--space-6);
956977
border-bottom: 1px solid var(--color-border);
978+
// Pin to the top of the scrolling card so the title + close button
979+
// stay put while a tall body scrolls underneath. Opaque background
980+
// so scrolled content doesn't bleed through.
981+
position: sticky;
982+
top: 0;
983+
z-index: 2;
984+
background: var(--color-surface);
957985
display: flex;
958986
// Top-aligned across every modal: only the preview modal can carry
959987
// a long title (the asset name), but `flex-start` is also fine for
@@ -1030,6 +1058,11 @@ label { text-align: left; }
10301058
padding: var(--space-4) var(--space-6);
10311059
border-top: 1px solid var(--color-border);
10321060
background: var(--color-surface-soft);
1061+
// Pin to the bottom of the scrolling card so Save / Apply / Cancel
1062+
// stay reachable no matter how tall the body grows.
1063+
position: sticky;
1064+
bottom: 0;
1065+
z-index: 2;
10331066
display: flex;
10341067
align-items: center;
10351068
justify-content: flex-end;
@@ -2187,7 +2220,11 @@ body.auth-body {
21872220
background: var(--color-surface-tint);
21882221
transition: border-color var(--t-fast), background var(--t-fast);
21892222

2190-
&:hover {
2223+
// The dashed border + cloud icon read as "drop files here", so the
2224+
// zone now accepts drops (dropFiles() in home.ts). Mirror the hover
2225+
// treatment while a drag is over it for feedback.
2226+
&:hover,
2227+
&.is-dragover {
21912228
border-color: var(--color-link);
21922229
background: #ece4f5;
21932230
}
@@ -2444,15 +2481,27 @@ body.auth-body {
24442481
color: var(--color-text-on-dark);
24452482
background: transparent;
24462483
border-color: var(--color-border-on-dark);
2484+
// Set the utility "clear" apart from the destructive Delete it sits
2485+
// beside in the action row.
2486+
margin-left: var(--space-2);
24472487
}
24482488

24492489
@media (max-width: 640px) {
24502490
.bulk-bar__inner {
24512491
flex-direction: column;
24522492
align-items: stretch;
24532493
border-radius: var(--radius-lg);
2494+
// Anchor the clear (×) to the top-right corner so it doesn't wrap
2495+
// onto a second row right next to the red Delete (mis-tap risk).
2496+
position: relative;
24542497
}
24552498
.bulk-bar__actions { justify-content: center; }
2499+
.bulk-bar__clear {
2500+
position: absolute;
2501+
top: var(--space-3);
2502+
right: var(--space-3);
2503+
margin-left: 0;
2504+
}
24562505
}
24572506

24582507
// Bulk-edit modal: each schedule field group is a card whose body is
@@ -2471,3 +2520,35 @@ body.auth-body {
24712520
}
24722521
}
24732522
.bulk-field__toggle { cursor: pointer; }
2523+
2524+
// Reserve room at the bottom of the page while a selection is active so
2525+
// the fixed bulk bar never covers the last rows or their action
2526+
// buttons (it floats over them otherwise, blocking clicks on the very
2527+
// assets a bulk selection targets). Bound to `selectedIds.length` in
2528+
// home.html. The bar is taller when stacked on phones, hence the bump.
2529+
.app-container.has-bulk-selection {
2530+
padding-bottom: 7rem;
2531+
@media (max-width: 640px) { padding-bottom: 11rem; }
2532+
}
2533+
2534+
// Selection checkboxes on the dark Enabled surface. The default
2535+
// white-fill, faint-border checkbox and its purple checked/indeterminate
2536+
// fill nearly vanish against the purple gradient — lift the border and
2537+
// give the box a translucent-white ground so it reads as a control and
2538+
// its state is legible. Scoped to .asset-select so the per-row activity
2539+
// switch (also an .app-check-input) keeps its own track styling.
2540+
.surface--active .asset-select .app-check-input {
2541+
border-color: rgba(255, 255, 255, 0.5);
2542+
background-color: rgba(255, 255, 255, 0.1);
2543+
2544+
&:checked,
2545+
&:indeterminate { border-color: rgba(255, 255, 255, 0.75); }
2546+
}
2547+
2548+
// Stack the ffmpeg recipe's copy button under the (horizontally
2549+
// scrolling) command on narrow widths, instead of squeezing the code to
2550+
// a thin strip beside it.
2551+
@media (max-width: 480px) {
2552+
.modal-alert__recipe { flex-direction: column; }
2553+
.modal-alert__copy { align-self: stretch; }
2554+
}

src/anthias_server/app/static/src/home.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ interface HomeAppData {
5151
uploadFileName: string
5252
uploadIndex: number
5353
uploadTotal: number
54+
dragActive: boolean
5455
// Bulk selection / actions (#3046)
5556
selectedIds: string[]
5657
visibleIds: Record<SectionKey, string[]>
@@ -65,6 +66,7 @@ interface HomeAppData {
6566
closePreview(): void
6667
bindFlatpickr(): void
6768
uploadFiles(input: HTMLInputElement): Promise<void>
69+
dropFiles(event: DragEvent): void
6870
uploadOne(url: string, csrf: string, file: File): Promise<UploadResult>
6971
// Bulk selection helpers
7072
isSelected(id: string): boolean
@@ -126,6 +128,7 @@ function homeApp(): HomeAppData {
126128
uploadFileName: '',
127129
uploadIndex: 0,
128130
uploadTotal: 0,
131+
dragActive: false,
129132
selectedIds: [],
130133
visibleIds: { active: [], inactive: [] },
131134
bulkEditOpen: false,
@@ -239,6 +242,11 @@ function homeApp(): HomeAppData {
239242
// anyway.
240243
this.mode = null
241244
this.editAsset = null
245+
// Clear any leftover drag highlight: dragging into the dropzone and
246+
// then closing the modal (Esc/backdrop/Cancel) before dragleave
247+
// fires would otherwise leave dragActive true, so the dropzone
248+
// re-opens still highlighted.
249+
this.dragActive = false
242250
if (!this.uploadState) {
243251
this.uploadProgress = 0
244252
this.uploadFileName = ''
@@ -398,6 +406,21 @@ function homeApp(): HomeAppData {
398406
}
399407
},
400408

409+
// Drag-and-drop entry point for the dropzone. Assigns the dropped
410+
// FileList to the hidden <input> (so input.form / re-select still
411+
// behave) and runs it through the same sequential uploadFiles()
412+
// batch path the input's change event uses.
413+
dropFiles(event: DragEvent) {
414+
const dropped = event.dataTransfer?.files
415+
if (!dropped || !dropped.length || this.uploadState) return
416+
const input = document.getElementById(
417+
'add-file',
418+
) as HTMLInputElement | null
419+
if (!input) return
420+
input.files = dropped
421+
void this.uploadFiles(input)
422+
},
423+
401424
// POST a single file and resolve an UploadResult: 'ok' on a 2xx
402425
// with no error toast, 'rejected' on a 2xx carrying an error toast
403426
// (server refused the file), 'error' on a non-2xx or transport

src/anthias_server/app/templates/_asset_modal.html

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,11 +84,19 @@ <h2 x-text="mode === 'edit' ? 'Edit asset' : 'Add asset'"></h2>
8484
>
8585
{% csrf_token %}
8686
<div class="modal-card__body" style="padding-top: 0">
87+
{% comment %} The dashed dropzone reads as a drop target, so it
88+
accepts drops as well as clicks. dropFiles() hands the
89+
dropped FileList to the same uploadFiles() batch path the
90+
<input>'s change event uses. {% endcomment %}
8791
<label
8892
for="add-file"
8993
class="upload-dropzone"
90-
:class="{ 'is-disabled': uploadState }"
94+
:class="{ 'is-disabled': uploadState, 'is-dragover': dragActive }"
9195
x-show="!uploadState"
96+
@dragover.prevent
97+
@dragenter.prevent="dragActive = true"
98+
@dragleave.self.prevent="dragActive = false"
99+
@drop.prevent="dragActive = false; dropFiles($event)"
92100
>
93101
<i class="ti ti-cloud-upload" style="font-size: 2.25rem; opacity: 0.55"></i>
94102
<p class="mb-1 font-semibold mt-2">Click to choose files</p>

src/anthias_server/app/templates/_bulk_edit_modal.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ <h2>Edit
7272
</label>
7373
<div class="mt-3" x-show="applyDuration" x-cloak>
7474
<div class="app-floating">
75-
<input type="number" class="app-input" id="bulk-duration" name="duration" min="0" step="1" placeholder="e.g. 10" required :disabled="!applyDuration">
75+
<input type="number" class="app-input" id="bulk-duration" name="duration" min="0" step="1" required :disabled="!applyDuration">
7676
<label for="bulk-duration">Duration (seconds)</label>
7777
</div>
7878
<p class="modal-section__hint mt-2 mb-0">

src/anthias_server/app/templates/_empty_assets.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
{% else %}Assets you toggle off — or that haven't been enabled yet — appear here.
1111
{% endif %}
1212
</p>
13-
<a href="#" @click.prevent="openAdd()">
13+
<button type="button" class="empty-state__action" @click="openAdd()">
1414
<i class="ti ti-plus mr-1"></i>Add an asset
15-
</a>
15+
</button>
1616
</div>

src/anthias_server/app/templates/home.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
{% endblock %}
99

1010
{% block main %}
11-
<div class="app-container" x-data="homeApp()" @asset-saved.window="closeModal()" @bulk-done.window="onBulkDone($event)">
11+
<div class="app-container" :class="{ 'has-bulk-selection': selectedIds.length }" x-data="homeApp()" @asset-saved.window="closeModal()" @bulk-done.window="onBulkDone($event)">
1212
<div class="page-header-bar">
1313
<div class="page-header-bar__title">
1414
<h1 class="page-header">Schedule Overview</h1>

0 commit comments

Comments
 (0)