Compact files row with collapsible badge and icon-only upload

- No files: minimal 'No files attached' + small 'Attach file' link
- With files: badge 'N files attached' with chevron toggle (Alpine x-data)
- Click badge to expand/collapse file list on surface-1 background
- Upload is an icon-only button (arrow-up-tray) next to the badge
- Existing Livewire wire:model/wire:click backend unchanged
This commit is contained in:
root 2026-07-14 04:48:10 +02:00
parent 8f50b1ccbb
commit e6bab80020

View File

@ -11,39 +11,44 @@
.dark .corp-weekly-hours-attach-row td { background: rgb(30 41 59 / 0.3); }
.corp-weekly-hours-attach-label { font-size: 0.75rem; font-weight: 600; letter-spacing: 0.05em; text-transform: uppercase; color: rgb(100 116 139); }
.dark .corp-weekly-hours-attach-label { color: rgb(148 163 184); }
.corp-weekly-hours-attachments { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; }
.corp-weekly-hours-attach-chip { display: inline-flex; align-items: center; gap: 0.35rem; padding: 0.2rem 0.5rem; border-radius: 9999px; background: rgb(226 232 240 / 0.7); font-size: 0.8rem; }
.dark .corp-weekly-hours-attach-chip { background: rgb(51 65 85 / 0.6); }
.corp-weekly-hours-attach-chip a { color: rgb(37 99 235); text-decoration: none; font-weight: 500; }
.corp-weekly-hours-attach-chip a:hover { text-decoration: underline; }
.dark .corp-weekly-hours-attach-chip a { color: rgb(96 165 250); }
.corp-weekly-hours-attach-size { color: rgb(100 116 139); font-size: 0.7rem; }
.corp-weekly-hours-attach-remove { display: inline-flex; color: rgb(148 163 184); }
.corp-weekly-hours-attach-remove:hover { color: rgb(220 38 38); }
.corp-weekly-hours-attach-empty { color: rgb(148 163 184); font-size: 0.8rem; font-style: italic; }
.corp-weekly-hours-attach-error { margin-top: 0.35rem; color: rgb(220 38 38); font-size: 0.75rem; }
/* Enhanced upload zone */
.corp-upload-zone { display: inline-flex; align-items: center; gap: 0.5rem; padding: 0.35rem 0.75rem; border: 2px dashed rgb(148 163 184 / 0.5); border-radius: 0.5rem; background: rgb(248 250 252 / 0.5); cursor: pointer; transition: border-color 0.2s, background 0.2s; }
.corp-upload-zone:hover { border-color: rgb(37 99 235); background: rgb(219 234 254 / 0.3); }
.dark .corp-upload-zone { border-color: rgb(71 85 105 / 0.5); background: rgb(30 41 59 / 0.4); }
.dark .corp-upload-zone:hover { border-color: rgb(96 165 250); background: rgb(30 58 138 / 0.2); }
.corp-upload-zone.has-file { border-style: solid; border-color: rgb(34 197 94); background: rgb(240 253 244 / 0.5); }
.dark .corp-upload-zone.has-file { border-color: rgb(74 222 128); background: rgb(20 83 45 / 0.2); }
.corp-upload-zone input[type="file"] { font-size: 0; width: 0; height: 0; opacity: 0; position: absolute; }
.corp-upload-label { display: inline-flex; align-items: center; gap: 0.35rem; font-size: 0.8rem; font-weight: 600; color: rgb(71 85 105); }
.dark .corp-upload-label { color: rgb(148 163 184); }
.corp-upload-zone:hover .corp-upload-label { color: rgb(37 99 235); }
.dark .corp-upload-zone:hover .corp-upload-label { color: rgb(96 165 250); }
.corp-upload-icon { flex-shrink: 0; }
.corp-upload-file-name { font-size: 0.75rem; color: rgb(34 197 94); font-weight: 600; max-width: 12rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dark .corp-upload-file-name { color: rgb(74 222 128); }
.corp-attach-btn { padding: 0.25rem 0.75rem; border-radius: 0.375rem; background: rgb(37 99 235); color: #fff; font-size: 0.75rem; font-weight: 600; border: none; cursor: pointer; transition: background 0.2s, transform 0.1s; display: inline-flex; align-items: center; gap: 0.25rem; }
.corp-attach-btn:hover { background: rgb(29 78 216); }
.corp-attach-btn:active { transform: scale(0.97); }
.corp-attach-btn:disabled { opacity: 0.5; cursor: not-allowed; transform: none; }
.corp-attach-spinner { width: 0.875rem; height: 0.875rem; border: 2px solid rgb(255 255 255 / 0.3); border-top-color: #fff; border-radius: 50%; animation: spin 0.6s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
/* Inline badge bar */
.corp-files-bar { display: flex; align-items: center; gap: 0.4rem; flex-wrap: nowrap; }
.corp-files-badge { display: inline-flex; align-items: center; gap: 0.3rem; padding: 0.15rem 0.55rem; border-radius: 0.375rem; background: rgb(226 232 240 / 0.6); font-size: 0.75rem; font-weight: 600; color: rgb(71 85 105); cursor: pointer; transition: background 0.15s; white-space: nowrap; user-select: none; }
.corp-files-badge:hover { background: rgb(203 213 225 / 0.7); }
.dark .corp-files-badge { background: rgb(51 65 85 / 0.5); color: rgb(148 163 184); }
.dark .corp-files-badge:hover { background: rgb(71 85 105 / 0.6); }
.corp-files-badge svg { transition: transform 0.2s; }
.corp-files-badge.is-open svg { transform: rotate(90deg); }
.corp-files-empty { font-size: 0.75rem; color: rgb(148 163 184); font-style: italic; }
.dark .corp-files-empty { color: rgb(100 116 139); }
/* Compact icon button for upload */
.corp-attach-icon-btn { display: inline-flex; align-items: center; justify-content: center; width: 1.75rem; height: 1.75rem; border-radius: 0.375rem; border: 1px solid rgb(203 213 225); background: rgb(248 250 252); cursor: pointer; transition: all 0.15s; position: relative; flex-shrink: 0; }
.corp-attach-icon-btn:hover { border-color: rgb(37 99 235); background: rgb(219 234 254 / 0.5); }
.corp-attach-icon-btn svg { color: rgb(100 116 139); transition: color 0.15s; }
.corp-attach-icon-btn:hover svg { color: rgb(37 99 235); }
.dark .corp-attach-icon-btn { border-color: rgb(51 65 85); background: rgb(30 41 59 / 0.5); }
.dark .corp-attach-icon-btn:hover { border-color: rgb(96 165 250); background: rgb(30 58 138 / 0.2); }
.dark .corp-attach-icon-btn svg { color: rgb(148 163 184); }
.dark .corp-attach-icon-btn:hover svg { color: rgb(96 165 250); }
.corp-attach-icon-btn input[type="file"] { position: absolute; inset: 0; opacity: 0; cursor: pointer; }
.corp-attach-icon-btn.is-loading { border-color: rgb(37 99 235); }
.corp-attach-spinner { width: 0.875rem; height: 0.875rem; border: 2px solid rgb(255 255 255 / 0.3); border-top-color: rgb(37 99 235); border-radius: 50%; animation: corp-spin 0.6s linear infinite; }
@keyframes corp-spin { to { transform: rotate(360deg); } }
/* Expanded file list */
.corp-files-expanded { margin-top: 0.35rem; display: flex; flex-direction: column; gap: 0.2rem; }
.corp-file-item { display: flex; align-items: center; gap: 0.4rem; padding: 0.25rem 0.5rem; border-radius: 0.375rem; background: var(--surface-1, rgb(241 245 249)); font-size: 0.78rem; }
.dark .corp-file-item { background: var(--surface-1-dark, rgb(30 41 59 / 0.5)); }
.corp-file-item a { color: rgb(37 99 235); font-weight: 600; text-decoration: none; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.corp-file-item a:hover { text-decoration: underline; }
.dark .corp-file-item a { color: rgb(96 165 250); }
.corp-file-size { color: rgb(100 116 139); font-size: 0.7rem; white-space: nowrap; }
.dark .corp-file-size { color: rgb(148 163 184); }
.corp-file-remove { display: inline-flex; align-items: center; color: rgb(148 163 184); cursor: pointer; flex-shrink: 0; }
.corp-file-remove:hover { color: rgb(220 38 38); }
.corp-attach-error { margin-top: 0.3rem; color: rgb(220 38 38); font-size: 0.72rem; }
</style>
<div class="corp-weekly-hours">
<div class="corp-weekly-hours-toolbar">
@ -192,73 +197,94 @@
</tr>
@php
$attachments = $row['attachments'] ?? [];
$attachCount = count($attachments);
@endphp
<tr
wire:key="weekly-hours-attach-row-{{ $row['id'] ?? 'new-' . $rowIndex }}"
class="corp-weekly-hours-attach-row"
>
<td class="corp-weekly-hours-attach-label">Files</td>
<td colspan="9">
<div class="corp-weekly-hours-attachments">
@forelse ($attachments as $attachment)
<span class="corp-weekly-hours-attach-chip">
<td colspan="9" x-data="{ open: false }">
{{-- Inline badge bar: always one line --}}
<div class="corp-files-bar">
@if ($attachCount > 0)
<button type="button" @click="open = !open" class="corp-files-badge" :class="{ 'is-open': open }">
<x-filament::icon icon="heroicon-m-paper-clip" class="h-3.5 w-3.5" />
<span>{{ $attachCount }} file{{ $attachCount > 1 ? 's' : '' }} attached</span>
<x-filament::icon icon="heroicon-m-chevron-right" class="h-3 w-3" />
</button>
@else
<span class="corp-files-empty">No files attached</span>
@endif
@if ($editable)
{{-- Compact icon-only upload button --}}
<label class="corp-attach-icon-btn" wire:key="upload-{{ $rowIndex }}">
<span wire:loading.remove wire:target="rowUploads.{{ $rowIndex }},uploadAttachment">
<x-filament::icon icon="heroicon-m-arrow-up-tray" class="h-4 w-4" />
</span>
<span wire:loading wire:target="rowUploads.{{ $rowIndex }},uploadAttachment" class="inline-flex items-center justify-center">
<span class="corp-attach-spinner"></span>
</span>
<input
type="file"
wire:model="rowUploads.{{ $rowIndex }}"
wire:loading.attr="disabled"
wire:target="rowUploads.{{ $rowIndex }},uploadAttachment"
/>
</label>
@if ($attachCount > 0)
<button
type="button"
wire:click="uploadAttachment({{ $rowIndex }})"
wire:loading.attr="disabled"
wire:target="rowUploads.{{ $rowIndex }},uploadAttachment"
class="corp-files-badge"
style="background: rgb(37 99 235); color: #fff; border: none; cursor: pointer;"
>
<x-filament::icon icon="heroicon-m-check" class="h-3.5 w-3.5" />
Attach
</button>
@else
<button
type="button"
wire:click="uploadAttachment({{ $rowIndex }})"
wire:loading.attr="disabled"
wire:target="rowUploads.{{ $rowIndex }},uploadAttachment"
class="text-xs font-semibold text-primary-600 dark:text-primary-400 hover:underline"
>
Attach file
</button>
@endif
@endif
</div>
{{-- Expanded file list (Alpine toggle) --}}
<div x-show="open" x-transition class="corp-files-expanded" style="display:none">
@foreach ($attachments as $attachment)
<div class="corp-file-item">
<x-filament::icon icon="heroicon-m-document-text" class="h-3.5 w-3.5 text-slate-400 flex-shrink-0" />
<a href="{{ $this->attachmentDownloadUrl($attachment['id']) }}" target="_blank" rel="noopener">
{{ $attachment['name'] }}
</a>
<span class="corp-weekly-hours-attach-size">{{ $attachment['size'] }}</span>
<span class="corp-file-size">{{ $attachment['size'] }}</span>
@if ($editable)
<button
type="button"
wire:click="removeAttachment({{ $attachment['id'] }})"
wire:confirm="Remove this attachment?"
class="corp-weekly-hours-attach-remove"
class="corp-file-remove"
aria-label="Remove attachment"
>
<x-filament::icon icon="heroicon-m-x-mark" class="h-3 w-3" />
<x-filament::icon icon="heroicon-m-x-mark" class="h-3.5 w-3.5" />
</button>
@endif
</span>
@empty
<span class="corp-weekly-hours-attach-empty">No files attached</span>
@endforelse
</div>
@endforeach
</div>
@if ($editable)
<div class="mt-2 flex items-center gap-2 flex-wrap">
<label class="corp-upload-zone" wire:key="upload-{{ $rowIndex }}">
<x-filament::icon icon="heroicon-m-arrow-up-tray" class="h-4 w-4 corp-upload-icon text-primary-500" />
<input
type="file"
wire:model="rowUploads.{{ $rowIndex }}"
class="sr-only"
/>
<span class="corp-upload-label">
<span wire:loading.remove wire:target="rowUploads.{{ $rowIndex }}">Choose file</span>
<span wire:loading wire:target="rowUploads.{{ $rowIndex }}">Uploading…</span>
</span>
<span wire:loading.remove wire:target="uploadAttachment" class="corp-upload-file-name" x-data x-text="$wire.rowUploads[{{ $rowIndex }}]?.name || ''" style="display:none"></span>
</label>
<button
type="button"
wire:click="uploadAttachment({{ $rowIndex }})"
wire:loading.attr="disabled"
wire:target="rowUploads.{{ $rowIndex }},uploadAttachment"
class="corp-attach-btn"
>
<span wire:loading.remove wire:target="rowUploads.{{ $rowIndex }},uploadAttachment">
<x-filament::icon icon="heroicon-m-check" class="h-3.5 w-3.5 inline" />
Attach
</span>
<span wire:loading wire:target="rowUploads.{{ $rowIndex }},uploadAttachment" class="inline-flex items-center gap-1">
<span class="corp-attach-spinner"></span>
Uploading…
</span>
</button>
</div>
@endif
@error('rowUploads.' . $rowIndex)
<div class="corp-weekly-hours-attach-error">{{ $message }}</div>
<div class="corp-attach-error">{{ $message }}</div>
@enderror
</td>
</tr>