mirror of
https://github.com/skysyaz/timesheet-staging.git
synced 2026-07-14 08:40:51 +00:00
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:
parent
8f50b1ccbb
commit
e6bab80020
@ -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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user