mirror of
https://github.com/Kuingsmile/PicList.git
synced 2026-05-11 18:10:32 +08:00
🚧 WIP(custom): migrate to tailwind, refator gallery page
This commit is contained in:
@@ -1,15 +1,19 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="containerRef" class="virtual-scroller" @scroll="handleScroll">
|
<div
|
||||||
<div class="virtual-scroller-content" :style="contentStyles">
|
ref="containerRef"
|
||||||
|
class="relative overflow-auto will-change-transform contain-[layout_style_paint] [-webkit-overflow-scrolling:touch]"
|
||||||
|
@scroll="handleScroll"
|
||||||
|
>
|
||||||
|
<div class="relative w-full" :style="contentStyles">
|
||||||
<div
|
<div
|
||||||
class="virtual-scroller-viewport"
|
class="group absolute inset-[0_auto_auto_0] w-full will-change-transform backface-hidden [.is-grid]:grid [.is-grid]:auto-rows-(--row-height,1px) [.is-grid]:grid-cols-[repeat(var(--items-per-row,1),minmax(0,1fr))] [.is-grid]:gap-(--item-gap,0)"
|
||||||
:class="{ 'is-grid': isGridMode, 'is-list': !isGridMode }"
|
:class="{ 'is-grid': isGridMode, 'is-list': !isGridMode }"
|
||||||
:style="viewportStyle"
|
:style="viewportStyle"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="realIndex in visibleIndexes"
|
v-for="realIndex in visibleIndexes"
|
||||||
:key="items[realIndex] && items[realIndex][keyField || 'id'] ? items[realIndex][keyField || 'id'] : realIndex"
|
:key="items[realIndex] && items[realIndex][keyField || 'id'] ? items[realIndex][keyField || 'id'] : realIndex"
|
||||||
class="virtual-scroller-item"
|
class="w-full"
|
||||||
:style="itemStyle"
|
:style="itemStyle"
|
||||||
>
|
>
|
||||||
<slot :item="items[realIndex]" :index="realIndex" />
|
<slot :item="items[realIndex]" :index="realIndex" />
|
||||||
|
|||||||
@@ -1,306 +1,367 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="gallery-container">
|
<div class="relative no-scrollbar flex h-full w-full items-center justify-center">
|
||||||
<!-- Header Card -->
|
<!-- Header Card -->
|
||||||
<div class="gallery-card header-card">
|
<div
|
||||||
<div class="card-header">
|
class="relative z-1 no-scrollbar flex h-full w-full flex-col items-center justify-start gap-4 overflow-auto rounded-xl border-none p-4 shadow-sm"
|
||||||
<div class="header-content">
|
>
|
||||||
<div class="header-icon">
|
<div
|
||||||
|
class="flex w-full items-center justify-between gap-4 rounded-2xl border border-border-secondary px-6 py-2 shadow-md max-md:items-stretch max-md:p-5"
|
||||||
|
>
|
||||||
|
<div class="flex flex-1 items-center gap-4">
|
||||||
|
<div class="flex items-center text-accent">
|
||||||
<ImagesIcon :size="24" />
|
<ImagesIcon :size="24" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h1>{{ t('pages.gallery.title') }}</h1>
|
<h1 class="m-0 text-2xl font-semibold tracking-tight text-main">{{ t('pages.gallery.title') }}</h1>
|
||||||
<p v-if="selectedCount > 0">{{ `${selectedCount}/${filterList.length} ${t('pages.gallery.selected')}` }}</p>
|
<p v-if="selectedCount > 0" class="m-0 text-sm text-secondary">
|
||||||
<p v-else>{{ `${filterList.length} ${t('pages.gallery.images')}` }}</p>
|
{{ `${selectedCount}/${filterList.length} ${t('pages.gallery.selected')}` }}
|
||||||
|
</p>
|
||||||
|
<p v-else class="m-0 text-sm text-secondary">{{ `${filterList.length} ${t('pages.gallery.images')}` }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-actions">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<div class="grid-size-control">
|
<div class="flex items-center gap-1.5 rounded-md border border-border-secondary px-2 py-1.5">
|
||||||
<GridIcon :size="14" />
|
<GridIcon :size="14" class="text-main" />
|
||||||
<input
|
<input
|
||||||
v-model.number="userGridColumns"
|
v-model.number="userGridColumns"
|
||||||
type="range"
|
type="range"
|
||||||
min="1"
|
min="1"
|
||||||
max="15"
|
max="15"
|
||||||
step="1"
|
step="1"
|
||||||
class="grid-slider"
|
class="grid-slider h-[4px] w-[70px] cursor-pointer appearance-none rounded-[2px] bg-(--color-background-tertiary) outline-none [&::-moz-range-thumb]:h-[14px] [&::-moz-range-thumb]:w-[14px] [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:border-none [&::-moz-range-thumb]:bg-accent [&::-moz-range-thumb]:transition-all [&::-moz-range-thumb]:duration-200 [&::-webkit-slider-thumb]:h-[15px] [&::-webkit-slider-thumb]:w-[15px] [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-accent [&::-webkit-slider-thumb]:transition-all [&::-webkit-slider-thumb]:duration-200 hover:[&::-webkit-slider-thumb]:scale-110 hover:[&::-webkit-slider-thumb]:shadow-[0_0_0_2px_rgba(var(--color-accent-rgb),0.4)]"
|
||||||
:title="t('pages.gallery.gridSize')"
|
:title="t('pages.gallery.gridSize')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="sync-delete-toggle">
|
<div class="flex items-center gap-2">
|
||||||
<span class="toggle-label">{{ t('pages.gallery.isAlwaysForceReload') }}</span>
|
<span class="text-sm text-secondary">{{ t('pages.gallery.isAlwaysForceReload') }}</span>
|
||||||
<label class="custom-switch">
|
<label class="relative inline-block h-[20px] w-[44px]">
|
||||||
<input v-model="isAlwaysForceReload" type="checkbox" @change="handleIsAlwaysForceReload" />
|
<input
|
||||||
<span class="switch-slider" />
|
v-model="isAlwaysForceReload"
|
||||||
|
class="peer/fir h-0 w-0 opacity-0"
|
||||||
|
type="checkbox"
|
||||||
|
@change="handleIsAlwaysForceReload"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
class="switch-slider peer-checked/fir:bg-accent peer-checked/fir:bg-none! peer-checked/fir:before:translate-x-[22px]"
|
||||||
|
/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="sync-delete-toggle">
|
<div class="flex items-center gap-2">
|
||||||
<span class="toggle-label">{{ t('pages.gallery.syncDelete') }}</span>
|
<span class="text-sm text-secondary">{{ t('pages.gallery.syncDelete') }}</span>
|
||||||
<label class="custom-switch">
|
<label class="relative inline-block h-[20px] w-[44px]">
|
||||||
<input v-model="deleteCloud" type="checkbox" @change="handleDeleteCloudFile" />
|
<input
|
||||||
<span class="switch-slider" />
|
v-model="deleteCloud"
|
||||||
|
class="peer/sec h-0 w-0 opacity-0"
|
||||||
|
type="checkbox"
|
||||||
|
@change="handleDeleteCloudFile"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
class="switch-slider peer-checked/sec:bg-accent peer-checked/sec:bg-none! peer-checked/sec:before:translate-x-[22px]"
|
||||||
|
/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<button class="action-button view-mode-toggle" :title="getViewModeLabel()" @click="toggleViewMode">
|
<button class="head-action-button relative" :title="getViewModeLabel()" @click="toggleViewMode">
|
||||||
<component :is="getViewModeIcon()" :size="16" />
|
<component :is="getViewModeIcon()" :size="16" />
|
||||||
{{ getViewModeLabel() }}
|
<span class="mt-0.5">{{ getViewModeLabel() }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="action-button" @click="toggleHandleBar">
|
<button class="head-action-button" @click="toggleHandleBar">
|
||||||
<ChevronDownIcon v-if="!handleBarActive" :size="16" />
|
<ChevronDownIcon v-if="!handleBarActive" :size="16" />
|
||||||
<ChevronUpIcon v-else :size="16" />
|
<ChevronUpIcon v-else :size="16" />
|
||||||
{{ t('pages.gallery.hideFilters') }}
|
<span class="mt-0.5">{{ t('pages.gallery.hideFilters') }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="action-button" @click="refreshPage">
|
<button class="head-action-button" @click="refreshPage">
|
||||||
<RefreshCwIcon :size="16" />
|
<RefreshCwIcon :size="16" />
|
||||||
{{ t('pages.gallery.refresh') }}
|
<span class="mt-0.5">{{ t('pages.gallery.refresh') }}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Filter Controls Card -->
|
<!-- Filter Controls Card -->
|
||||||
<transition name="filter-slide">
|
<div
|
||||||
<div v-show="handleBarActive" class="gallery-card filter-card">
|
v-show="handleBarActive"
|
||||||
<div class="filter-content">
|
class="flex w-full flex-wrap items-center justify-between gap-2 rounded-2xl border border-border-secondary px-6 py-2 shadow-md max-md:items-stretch max-md:p-5"
|
||||||
<div class="filter-row">
|
|
||||||
<div class="filter-group">
|
|
||||||
<label class="filter-label">{{ t('pages.gallery.picBedType') }}</label>
|
|
||||||
<div class="custom-multiselect">
|
|
||||||
<button
|
|
||||||
class="multiselect-trigger"
|
|
||||||
:class="{ active: picBedDropdownOpen }"
|
|
||||||
@click="togglePicBedDropdown($event)"
|
|
||||||
>
|
|
||||||
<span v-if="choosedPicBed.length === 0">{{ t('pages.gallery.chooseShowedPicBed') }}</span>
|
|
||||||
<span v-else>{{ choosedPicBed.length }} {{ t('pages.gallery.selected') }}</span>
|
|
||||||
<ChevronDownIcon :size="16" />
|
|
||||||
</button>
|
|
||||||
<div v-show="picBedDropdownOpen" class="multiselect-dropdown">
|
|
||||||
<label v-for="item in filteredPicBedG" :key="item.type" class="multiselect-option">
|
|
||||||
<input v-model="choosedPicBed" type="checkbox" :value="item.type" />
|
|
||||||
{{ item.name }}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="filter-group">
|
|
||||||
<label class="filter-label">{{ t('pages.gallery.dateRange') }}</label>
|
|
||||||
<div class="date-range-picker">
|
|
||||||
<input v-model="dateRangeStart" type="date" class="date-input" placeholder="Start date" />
|
|
||||||
<span class="date-separator">-</span>
|
|
||||||
<input v-model="dateRangeEnd" type="date" class="date-input" placeholder="End date" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="filter-group">
|
|
||||||
<label class="filter-label">{{ t('pages.gallery.pasteFormat') }}</label>
|
|
||||||
<select v-model="pasteStyle" class="custom-select" @change="handlePasteStyleChange">
|
|
||||||
<option v-for="(value, key) in pasteStyleMap" :key="key" :value="value" class="select-option">
|
|
||||||
{{ key }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="filter-group">
|
|
||||||
<label class="filter-label">{{ t('pages.gallery.urlType') }}</label>
|
|
||||||
<select v-model="useShortUrl" class="custom-select" @change="handleUseShortUrlChange">
|
|
||||||
<option v-for="(value, key) in shortURLMap" :key="key" :value="value" class="select-option">
|
|
||||||
{{ key }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="filter-group">
|
|
||||||
<label class="filter-label">{{ t('pages.gallery.sort') }}</label>
|
|
||||||
<div class="sort-dropdown">
|
|
||||||
<button class="sort-button" :class="{ active: sortDropdownOpen }" @click="toggleSortDropdown($event)">
|
|
||||||
<SortAscIcon :size="14" />
|
|
||||||
{{ t(`pages.gallery.sortBy.${currentSortField}`) }}
|
|
||||||
<ChevronDownIcon :size="14" />
|
|
||||||
</button>
|
|
||||||
<div v-show="sortDropdownOpen" class="sort-options">
|
|
||||||
<button
|
|
||||||
v-for="key in ['name', 'ext', 'time', 'check']"
|
|
||||||
:key="key"
|
|
||||||
class="sort-option"
|
|
||||||
@click="sortFile(key as any)"
|
|
||||||
>
|
|
||||||
{{ t(`pages.gallery.sortBy.${key}`) }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Second Row - Search and Actions -->
|
|
||||||
<div class="filter-row">
|
|
||||||
<div class="search-group">
|
|
||||||
<div class="search-input-wrapper">
|
|
||||||
<SearchIcon :size="16" class="search-icon" />
|
|
||||||
<input
|
|
||||||
v-model="searchText"
|
|
||||||
type="text"
|
|
||||||
class="search-input"
|
|
||||||
:placeholder="$t('pages.gallery.searchFilename')"
|
|
||||||
/>
|
|
||||||
<button v-if="searchText" class="clear-button" @click="cleanSearch">
|
|
||||||
<XIcon :size="15" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="search-group">
|
|
||||||
<div class="search-input-wrapper">
|
|
||||||
<LinkIcon :size="16" class="search-icon" />
|
|
||||||
<input
|
|
||||||
v-model="searchTextURL"
|
|
||||||
type="text"
|
|
||||||
class="search-input"
|
|
||||||
:placeholder="t('pages.gallery.searchUrl')"
|
|
||||||
/>
|
|
||||||
<button v-if="searchTextURL" class="clear-button" @click="cleanSearchUrl">
|
|
||||||
<XIcon :size="14" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="action-buttons">
|
|
||||||
<button class="action-btn copy-btn" :class="{ active: isMultiple(choosedList) }" @click="multiCopy">
|
|
||||||
<ClipboardIcon :size="16" />
|
|
||||||
{{ t('pages.gallery.copy') }}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="action-btn edit-btn"
|
|
||||||
:class="{ active: filterList.length > 0 }"
|
|
||||||
@click="() => (isShowBatchRenameDialog = true)"
|
|
||||||
>
|
|
||||||
<EditIcon :size="16" />
|
|
||||||
{{ t('pages.gallery.edit') }}
|
|
||||||
</button>
|
|
||||||
<button class="action-btn delete-btn" :class="{ active: isMultiple(choosedList) }" @click="multiRemove">
|
|
||||||
<TrashIcon :size="16" />
|
|
||||||
{{ `${t('pages.gallery.delete')}${selectedCount > 0 ? ` (${selectedCount})` : ''}` }}
|
|
||||||
</button>
|
|
||||||
<button class="action-btn select-btn" :class="{ active: filterList.length > 0 }" @click="toggleSelectAll">
|
|
||||||
<CheckSquareIcon :size="16" />
|
|
||||||
{{ isAllSelected ? t('pages.gallery.cancel') : t('pages.gallery.selectAll') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</transition>
|
|
||||||
|
|
||||||
<!-- Gallery Grid -->
|
|
||||||
<div class="gallery-card gallery-content">
|
|
||||||
<div v-if="filterList.length === 0" class="empty-state">
|
|
||||||
<ImageIcon :size="64" class="empty-icon" />
|
|
||||||
<h3>{{ t('pages.gallery.noImagesFound') }}</h3>
|
|
||||||
<p>{{ t('pages.gallery.tryAdjustingFilters') }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<VirtualScroller
|
|
||||||
v-else
|
|
||||||
:key="componentKey"
|
|
||||||
ref="virtualScrollerRef"
|
|
||||||
:view-mode="viewMode"
|
|
||||||
class="virtual-gallery-scroller"
|
|
||||||
:items="filterList"
|
|
||||||
:item-height="300"
|
|
||||||
:grid-breakpoints="effectiveGridBreakpoints"
|
|
||||||
key-field="key"
|
|
||||||
>
|
>
|
||||||
<template #default="{ item, index }">
|
<div class="mb-1 flex w-full flex-wrap items-start gap-3">
|
||||||
<div
|
<div class="filter-group">
|
||||||
class="gallery-item"
|
<label class="filter-label">{{ t('pages.gallery.picBedType') }}</label>
|
||||||
:class="{ selected: choosedList[item.id || ''] }"
|
<div class="custom-multiselect relative">
|
||||||
@click="handleChooseImage(!choosedList[item.id || ''], index)"
|
<button
|
||||||
>
|
class="flex h-[28px] w-full cursor-pointer items-center justify-between rounded-md border border-border-secondary px-2 py-1.5 text-sm leading-[1.4] text-main transition-all duration-fast ease-apple hover:border-accent-hover focus:[.active]:border-accent-hover focus:[.active]:shadow-sm"
|
||||||
<div class="image-container" @click.stop="zoomImage(index)">
|
:class="{ active: picBedDropdownOpen }"
|
||||||
<img
|
@click="togglePicBedDropdown($event)"
|
||||||
:src="
|
>
|
||||||
imageErrorStates[item.key || '']
|
<span v-if="choosedPicBed.length === 0">{{ t('pages.gallery.chooseShowedPicBed') }}</span>
|
||||||
? './errorLoading.png'
|
<span v-else>{{ choosedPicBed.length }} {{ t('pages.gallery.selected') }}</span>
|
||||||
: isAlwaysForceReload
|
<ChevronDownIcon :size="16" />
|
||||||
? addCacheBustParam(item.src)
|
</button>
|
||||||
: item.src
|
<div
|
||||||
"
|
v-show="picBedDropdownOpen"
|
||||||
class="gallery-image"
|
class="multiselect-dropdown shadow-lg; fixed z-1000 mt-[2px] no-scrollbar max-h-[280px] min-w-[185px] overflow-y-auto rounded-md border border-border-secondary bg-bg-tertiary px-2 py-1.5 text-main"
|
||||||
:class="{ loading: !imageLoadStates[item.key || ''] }"
|
>
|
||||||
@load="onImageLoad(item.key || '')"
|
<label
|
||||||
@error="onImageError(item.key || '')"
|
v-for="item in filteredPicBedG"
|
||||||
/>
|
:key="item.type"
|
||||||
<div v-if="!imageLoadStates[item.key || '']" class="image-loader">
|
class="flex min-h-[unset] cursor-pointer items-center justify-between px-2 py-1 text-sm leading-[1.4] transition-all duration-fast ease-apple hover:bg-accent-hover"
|
||||||
<div class="loader-spinner" />
|
>
|
||||||
</div>
|
<input v-model="choosedPicBed" type="checkbox" :value="item.type" class="m-0" />
|
||||||
</div>
|
{{ item.name }}
|
||||||
|
|
||||||
<div class="image-info">
|
|
||||||
<div class="image-name" :title="item.fileName">
|
|
||||||
{{ formatFileName(item.fileName || '') }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="image-actions">
|
|
||||||
<div class="action-icons">
|
|
||||||
<button :title="t('pages.gallery.copy')" class="icon-button copy-icon" @click.stop="copy(item)">
|
|
||||||
<ClipboardIcon :size="16" />
|
|
||||||
</button>
|
|
||||||
<button :title="t('pages.gallery.edit')" class="icon-button edit-icon" @click.stop="openDialog(item)">
|
|
||||||
<EditIcon :size="16" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
:title="t('pages.gallery.delete')"
|
|
||||||
class="icon-button delete-icon"
|
|
||||||
@click.stop="remove(item, index)"
|
|
||||||
>
|
|
||||||
<TrashIcon :size="16" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<label class="custom-checkbox" @click.stop>
|
|
||||||
<input
|
|
||||||
v-model="choosedList[item.id ? item.id : '']"
|
|
||||||
type="checkbox"
|
|
||||||
@change="e => handleChooseImage((e.target as HTMLInputElement).checked, index)"
|
|
||||||
/>
|
|
||||||
<span class="checkbox-mark" />
|
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
|
||||||
</VirtualScroller>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
<div class="filter-group">
|
||||||
|
<label class="filter-label">{{ t('pages.gallery.dateRange') }}</label>
|
||||||
|
<div class="flex w-full flex-wrap items-center gap-2 max-md:items-start">
|
||||||
|
<input v-model="dateRangeStart" type="date" class="date-input" placeholder="Start date" />
|
||||||
|
<span class="shrink-0 font-medium text-secondary">-</span>
|
||||||
|
<input v-model="dateRangeEnd" type="date" class="date-input" placeholder="End date" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="filter-group">
|
||||||
|
<label class="filter-label">{{ t('pages.gallery.pasteFormat') }}</label>
|
||||||
|
<select v-model="pasteStyle" class="custom-select" @change="handlePasteStyleChange">
|
||||||
|
<option
|
||||||
|
v-for="(value, key) in pasteStyleMap"
|
||||||
|
:key="key"
|
||||||
|
:value="value"
|
||||||
|
class="bg-bg-tertiary text-sm text-main"
|
||||||
|
>
|
||||||
|
{{ key }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="filter-group">
|
||||||
|
<label class="filter-label">{{ t('pages.gallery.urlType') }}</label>
|
||||||
|
<select v-model="useShortUrl" class="custom-select" @change="handleUseShortUrlChange">
|
||||||
|
<option
|
||||||
|
v-for="(value, key) in shortURLMap"
|
||||||
|
:key="key"
|
||||||
|
:value="value"
|
||||||
|
class="bg-bg-tertiary text-sm text-main"
|
||||||
|
>
|
||||||
|
{{ key }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="filter-group">
|
||||||
|
<label class="filter-label">{{ t('pages.gallery.sort') }}</label>
|
||||||
|
<div class="sort-dropdown relative">
|
||||||
|
<button class="sort-button" :class="{ active: sortDropdownOpen }" @click="toggleSortDropdown($event)">
|
||||||
|
<SortAscIcon :size="14" />
|
||||||
|
{{ t(`pages.gallery.sortBy.${currentSortField}`) }}
|
||||||
|
<ChevronDownIcon :size="14" />
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
v-show="sortDropdownOpen"
|
||||||
|
class="sort-options fixed z-10 mt-[2px] min-w-[150px] overflow-hidden rounded-md border border-border-secondary bg-bg-tertiary shadow-lg"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-for="key in ['name', 'ext', 'time', 'check']"
|
||||||
|
:key="key"
|
||||||
|
class="block min-h-[unset] w-full cursor-pointer border-none bg-bg-tertiary px-2 py-1 text-center text-sm leading-[1.4] text-main transition-all duration-fast ease-apple hover:bg-accent-hover"
|
||||||
|
@click="sortFile(key as any)"
|
||||||
|
>
|
||||||
|
{{ t(`pages.gallery.sortBy.${key}`) }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Second Row - Search and Actions -->
|
||||||
|
<div class="mb-1 flex w-full flex-wrap items-start gap-3">
|
||||||
|
<div class="relative flex min-w-[100px] flex-row items-center gap-2">
|
||||||
|
<SearchIcon :size="16" class="absolute left-3 z-1 text-secondary" />
|
||||||
|
<input
|
||||||
|
v-model="searchText"
|
||||||
|
type="text"
|
||||||
|
class="search-input"
|
||||||
|
:placeholder="$t('pages.gallery.searchFilename')"
|
||||||
|
/>
|
||||||
|
<button v-if="searchText" class="clear-button" @click="cleanSearch">
|
||||||
|
<XIcon :size="15" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="relative flex min-w-[100px] flex-row items-center gap-2">
|
||||||
|
<LinkIcon :size="16" class="absolute left-3 z-1 text-secondary" />
|
||||||
|
<input
|
||||||
|
v-model="searchTextURL"
|
||||||
|
type="text"
|
||||||
|
class="search-input"
|
||||||
|
:placeholder="t('pages.gallery.searchUrl')"
|
||||||
|
/>
|
||||||
|
<button v-if="searchTextURL" class="clear-button" @click="cleanSearchUrl">
|
||||||
|
<XIcon :size="14" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-1 flex-wrap gap-3">
|
||||||
|
<button class="action-btn copy-btn" :class="{ active: isMultiple(choosedList) }" @click="multiCopy">
|
||||||
|
<ClipboardIcon :size="16" />
|
||||||
|
<span class="mt-1"> {{ t('pages.gallery.copy') }}</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="action-btn edit-btn"
|
||||||
|
:class="{ active: filterList.length > 0 }"
|
||||||
|
@click="() => (isShowBatchRenameDialog = true)"
|
||||||
|
>
|
||||||
|
<EditIcon :size="16" />
|
||||||
|
<span class="mt-1"> {{ t('pages.gallery.edit') }}</span>
|
||||||
|
</button>
|
||||||
|
<button class="action-btn delete-btn" :class="{ active: isMultiple(choosedList) }" @click="multiRemove">
|
||||||
|
<TrashIcon :size="16" />
|
||||||
|
<span class="mt-1">
|
||||||
|
{{ `${t('pages.gallery.delete')}${selectedCount > 0 ? ` (${selectedCount})` : ''}` }}</span
|
||||||
|
>
|
||||||
|
</button>
|
||||||
|
<button class="action-btn select-btn" :class="{ active: filterList.length > 0 }" @click="toggleSelectAll">
|
||||||
|
<CheckSquareIcon :size="16" />
|
||||||
|
<span class="mt-1">{{ isAllSelected ? t('pages.gallery.cancel') : t('pages.gallery.selectAll') }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Gallery Grid -->
|
||||||
|
<div
|
||||||
|
class="no-scrollbar flex min-h-[500px] w-full flex-1 flex-col flex-wrap items-center justify-center gap-2 overflow-auto rounded-2xl border border-border-secondary p-1 shadow-md"
|
||||||
|
>
|
||||||
|
<div v-if="filterList.length === 0" class="flex flex-col items-center justify-center px-8 py-16 text-center">
|
||||||
|
<ImageIcon :size="64" class="mb-4 text-accent" />
|
||||||
|
<h3 class="mx-0 mt-0 mb-2 text-xl font-semibold text-main">{{ t('pages.gallery.noImagesFound') }}</h3>
|
||||||
|
<p class="m-0 text-secondary">{{ t('pages.gallery.tryAdjustingFilters') }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<VirtualScroller
|
||||||
|
v-else
|
||||||
|
:key="componentKey"
|
||||||
|
ref="virtualScrollerRef"
|
||||||
|
:view-mode="viewMode"
|
||||||
|
class="virtual-gallery-scroller min-h-0 w-full flex-1 p-1"
|
||||||
|
:items="filterList"
|
||||||
|
:item-height="300"
|
||||||
|
:grid-breakpoints="effectiveGridBreakpoints"
|
||||||
|
key-field="key"
|
||||||
|
>
|
||||||
|
<template #default="{ item, index }">
|
||||||
|
<div
|
||||||
|
class="group/image m-0 box-border flex h-[calc(100%-8px)] w-full cursor-pointer flex-col overflow-hidden rounded-lg border border-border-secondary transition-all duration-fast ease-apple hover:-translate-y-[2px] hover:border-border hover:shadow-md [.selected]:border-2 [.selected]:border-accent [.selected]:shadow-md"
|
||||||
|
:class="{ selected: choosedList[item.id || ''] }"
|
||||||
|
@click="handleChooseImage(!choosedList[item.id || ''], index)"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="relative flex aspect-auto min-h-0 flex-1 items-center justify-center overflow-hidden"
|
||||||
|
@click.stop="zoomImage(index)"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
:src="
|
||||||
|
imageErrorStates[item.key || '']
|
||||||
|
? './errorLoading.png'
|
||||||
|
: isAlwaysForceReload
|
||||||
|
? addCacheBustParam(item.src)
|
||||||
|
: item.src
|
||||||
|
"
|
||||||
|
class="h-full w-full object-contain transition-all duration-fast ease-apple"
|
||||||
|
:class="{ loading: !imageLoadStates[item.key || ''] }"
|
||||||
|
@load="onImageLoad(item.key || '')"
|
||||||
|
@error="onImageError(item.key || '')"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-if="!imageLoadStates[item.key || '']"
|
||||||
|
class="absolute inset-0 flex items-center justify-center bg-surface-elevated"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="h-[24px] w-[24px] animate-spin rounded-full border-2 border-t-2 border-border-secondary border-t-accent"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex min-h-[80px] shrink-0 flex-col justify-between p-3">
|
||||||
|
<div
|
||||||
|
class="mb-3 overflow-hidden text-sm font-medium text-ellipsis whitespace-nowrap text-main"
|
||||||
|
:title="(item.fileName || '').toString().length > 30 ? item.fileName || '' : ''"
|
||||||
|
>
|
||||||
|
{{ formatFileName(item.fileName || '') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<button :title="t('pages.gallery.copy')" class="icon-button copy-icon" @click.stop="copy(item)">
|
||||||
|
<ClipboardIcon :size="16" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
:title="t('pages.gallery.edit')"
|
||||||
|
class="icon-button edit-icon"
|
||||||
|
@click.stop="openDialog(item)"
|
||||||
|
>
|
||||||
|
<EditIcon :size="16" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
:title="t('pages.gallery.delete')"
|
||||||
|
class="icon-button delete-icon"
|
||||||
|
@click.stop="remove(item, index)"
|
||||||
|
>
|
||||||
|
<TrashIcon :size="16" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label class="relative flex cursor-pointer items-center" @click.stop>
|
||||||
|
<input
|
||||||
|
v-model="choosedList[item.id ? item.id : '']"
|
||||||
|
type="checkbox"
|
||||||
|
class="peer absolute h-0 w-0 cursor-pointer opacity-0"
|
||||||
|
@change="e => handleChooseImage((e.target as HTMLInputElement).checked, index)"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
class="relative inline-block h-[16px] w-[16px] rounded-sm border-2 border-border transition-all duration-fast ease-apple peer-checked:border-accent-hover peer-checked:bg-accent peer-checked:after:absolute peer-checked:after:top-[-2px] peer-checked:after:left-px peer-checked:after:text-[12px] peer-checked:after:font-bold peer-checked:after:text-white peer-checked:after:content-['✓']"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</VirtualScroller>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<!-- Custom Image Preview Modal -->
|
<!-- Custom Image Preview Modal -->
|
||||||
<transition name="modal">
|
<transition name="modal">
|
||||||
<div
|
<div
|
||||||
v-if="gallerySliderControl.visible"
|
v-if="gallerySliderControl.visible"
|
||||||
class="image-preview-modal"
|
class="image-preview-modal fixed inset-0 z-1000 flex items-center justify-center outline-none"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@click.stop
|
@click.stop
|
||||||
@wheel="handleImageWheel"
|
@wheel="handleImageWheel"
|
||||||
@keydown="handleKeydown"
|
@keydown="handleKeydown"
|
||||||
>
|
>
|
||||||
<div class="modal-backdrop" :class="advancedAnimation" />
|
<div class="absolute inset-0 bg-black/50" :class="{ 'advanced-animation': enableAdvancedAnimation }" />
|
||||||
<div class="modal-content">
|
<div class="relative max-h-[90vh] max-w-[90vw] overflow-hidden rounded-xl bg-surface shadow-lg">
|
||||||
<button class="modal-close" @click="handleClose">
|
<button
|
||||||
|
class="absolute top-4 right-4 z-10 flex h-6 w-6 cursor-pointer items-center justify-center rounded-full border border-white bg-danger/90 text-white hover:bg-danger hover:text-white"
|
||||||
|
@click="handleClose"
|
||||||
|
>
|
||||||
<XIcon :size="24" />
|
<XIcon :size="24" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Zoom controls -->
|
<!-- Zoom controls -->
|
||||||
<div class="zoom-controls">
|
<div class="absolute top-4 left-4 z-10 flex items-center gap-2 rounded-lg bg-black/70 p-2">
|
||||||
<button class="zoom-btn" :disabled="imagePreviewState.scale <= 0.1" @click="zoomOut">
|
<button class="zoom-btn" :disabled="imagePreviewState.scale <= 0.1" @click="zoomOut">
|
||||||
<span>-</span>
|
<span>-</span>
|
||||||
</button>
|
</button>
|
||||||
<span class="zoom-level">{{ Math.round(imagePreviewState.scale * 100) }}%</span>
|
<span class="min-w-[50px] text-center text-sm font-medium text-white"
|
||||||
|
>{{ Math.round(imagePreviewState.scale * 100) }}%</span
|
||||||
|
>
|
||||||
<button class="zoom-btn" :disabled="imagePreviewState.scale >= 5" @click="zoomIn">
|
<button class="zoom-btn" :disabled="imagePreviewState.scale >= 5" @click="zoomIn">
|
||||||
<span>+</span>
|
<span>+</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="zoom-btn reset-btn" @click="resetImageTransform">Reset</button>
|
<button class="zoom-btn reset-btn" @click="resetImageTransform">Reset</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="image-navigation">
|
<div class="relative flex items-center">
|
||||||
<button
|
<button
|
||||||
class="nav-button prev"
|
class="nav-button prev"
|
||||||
:disabled="gallerySliderControl.index === 0"
|
:disabled="gallerySliderControl.index === 0"
|
||||||
@@ -310,7 +371,7 @@
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="image-viewer"
|
class="relative flex h-[80vh] w-[90vw] items-center justify-center overflow-hidden bg-black select-none active:cursor-grab!"
|
||||||
@mousedown="handleImageMouseDown"
|
@mousedown="handleImageMouseDown"
|
||||||
@mousemove="handleImageMouseMove"
|
@mousemove="handleImageMouseMove"
|
||||||
@mouseup="handleImageMouseUp"
|
@mouseup="handleImageMouseUp"
|
||||||
@@ -323,7 +384,7 @@
|
|||||||
ref="previewImageRef"
|
ref="previewImageRef"
|
||||||
:src="currentPreviewImage?.src"
|
:src="currentPreviewImage?.src"
|
||||||
:alt="currentPreviewImage?.intro"
|
:alt="currentPreviewImage?.intro"
|
||||||
class="preview-image"
|
class="block h-auto max-h-none w-auto max-w-none origin-center object-contain"
|
||||||
:style="imageTransformStyle"
|
:style="imageTransformStyle"
|
||||||
@load="onPreviewImageLoad"
|
@load="onPreviewImageLoad"
|
||||||
@dragstart.prevent
|
@dragstart.prevent
|
||||||
@@ -340,10 +401,14 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="image-details">
|
<div class="flex items-center justify-between border border-border-secondary px-6 py-4">
|
||||||
<h3>{{ currentPreviewImage?.intro }}</h3>
|
<h3 class="m-0 mr-4 flex-1 overflow-hidden text-base font-semibold text-ellipsis text-main">
|
||||||
<div class="image-counter">{{ gallerySliderControl.index + 1 }} / {{ filterList.length }}</div>
|
{{ currentPreviewImage?.intro }}
|
||||||
<div class="image-help-text">
|
</h3>
|
||||||
|
<div class="mt-1 mr-4 text-sm font-semibold whitespace-nowrap text-main">
|
||||||
|
{{ gallerySliderControl.index + 1 }} / {{ filterList.length }}
|
||||||
|
</div>
|
||||||
|
<div class="mt-1 text-center text-xs font-medium text-main">
|
||||||
{{ t('pages.gallery.previewHelp') }}
|
{{ t('pages.gallery.previewHelp') }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -353,15 +418,20 @@
|
|||||||
|
|
||||||
<!-- Edit URL Modal -->
|
<!-- Edit URL Modal -->
|
||||||
<transition name="modal">
|
<transition name="modal">
|
||||||
<div v-if="dialogVisible" class="modal-overlay" :class="advancedAnimation" @click="dialogVisible = false">
|
<div
|
||||||
|
v-if="dialogVisible"
|
||||||
|
class="modal-overlay"
|
||||||
|
:class="{ 'advanced-animation': enableAdvancedAnimation }"
|
||||||
|
@click="dialogVisible = false"
|
||||||
|
>
|
||||||
<div class="modal-container" @click.stop>
|
<div class="modal-container" @click.stop>
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h3>{{ t('pages.gallery.changeImageUrl') }}</h3>
|
<h3 class="m-0 text-xl font-semibold text-main">{{ t('pages.gallery.changeImageUrl') }}</h3>
|
||||||
<button class="modal-close-btn" @click="dialogVisible = false">
|
<button class="modal-close-btn" @click="dialogVisible = false">
|
||||||
<XIcon :size="20" />
|
<XIcon :size="20" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="p-6">
|
||||||
<input v-model="imgInfo.imgUrl" type="text" class="form-input" placeholder="Enter new URL" />
|
<input v-model="imgInfo.imgUrl" type="text" class="form-input" placeholder="Enter new URL" />
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
@@ -381,18 +451,17 @@
|
|||||||
<div
|
<div
|
||||||
v-if="isShowBatchRenameDialog"
|
v-if="isShowBatchRenameDialog"
|
||||||
class="modal-overlay"
|
class="modal-overlay"
|
||||||
:class="advancedAnimation"
|
:class="{ 'advanced-animation': enableAdvancedAnimation }"
|
||||||
@click="isShowBatchRenameDialog = false"
|
|
||||||
>
|
>
|
||||||
<div class="modal-container large" @click.stop>
|
<div class="modal-container" @click.stop>
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h3>{{ t('pages.gallery.batchEditUrl') }}</h3>
|
<h3 class="m-0 text-xl font-semibold text-main">{{ t('pages.gallery.batchEditUrl') }}</h3>
|
||||||
<button class="modal-close-btn" @click="isShowBatchRenameDialog = false">
|
<button class="modal-close-btn" @click="isShowBatchRenameDialog = false">
|
||||||
<XIcon :size="20" />
|
<XIcon :size="20" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="p-6">
|
||||||
<div class="form-group">
|
<div class="mb-6 last:mb-0">
|
||||||
<label class="form-label">
|
<label class="form-label">
|
||||||
{{ t('pages.gallery.regexPattern', { matched: matchedCount || 0 }) }}
|
{{ t('pages.gallery.regexPattern', { matched: matchedCount || 0 }) }}
|
||||||
</label>
|
</label>
|
||||||
@@ -404,20 +473,34 @@
|
|||||||
@focus="showMatchedUrls = true"
|
@focus="showMatchedUrls = true"
|
||||||
@blur="showMatchedUrls = false"
|
@blur="showMatchedUrls = false"
|
||||||
/>
|
/>
|
||||||
<div v-if="showMatchedUrls && matchedUrls.length > 0" class="matched-urls-tooltip">
|
<div
|
||||||
<div class="tooltip-header">Matched URLs ({{ matchedUrls.length }}):</div>
|
v-if="showMatchedUrls && matchedUrls.length > 0"
|
||||||
<div class="tooltip-content">
|
class="absolute z-1000 mt-2 max-h-[300px] max-w-[650px] overflow-hidden rounded-md border border-border-secondary bg-bg-tertiary p-0 shadow-md"
|
||||||
<div v-for="(url, index) in matchedUrls" :key="index" class="url-item">
|
>
|
||||||
|
<div
|
||||||
|
class="border-b border-b-border-secondary bg-bg-secondary px-4 py-3 text-sm font-semibold text-main"
|
||||||
|
>
|
||||||
|
Matched URLs ({{ matchedUrls.length }}):
|
||||||
|
</div>
|
||||||
|
<div class="max-h-[240px] overflow-auto p-2">
|
||||||
|
<div
|
||||||
|
v-for="(url, index) in matchedUrls"
|
||||||
|
:key="index"
|
||||||
|
class="rounded-sm px-3 py-2 font-['SF_Mono',Monaco,'Cascadia_Code','Roboto_Mono',Consolas,'Courier_New',monospace] text-sm break-all text-secondary transition-all duration-fast ease-apple hover:bg-surface-elevated"
|
||||||
|
>
|
||||||
{{ url }}
|
{{ url }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="mb-6 last:mb-0">
|
||||||
<label class="form-label">
|
<label class="form-label">
|
||||||
{{ t('pages.gallery.replacedWith') }}
|
{{ t('pages.gallery.replacedWith') }}
|
||||||
<button class="info-button" @click="showFormatInfo = !showFormatInfo">
|
<button
|
||||||
|
class="flex h-[20px] w-[20px] cursor-pointer items-center justify-around rounded-full border-none bg-accent text-white transition-all duration-fast ease-apple hover:bg-accent-hover"
|
||||||
|
@click="showFormatInfo = !showFormatInfo"
|
||||||
|
>
|
||||||
<InfoIcon :size="16" />
|
<InfoIcon :size="16" />
|
||||||
</button>
|
</button>
|
||||||
</label>
|
</label>
|
||||||
@@ -425,10 +508,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Format Info Panel -->
|
<!-- Format Info Panel -->
|
||||||
<div v-if="showFormatInfo" class="form-group">
|
<div v-if="showFormatInfo" class="mb-6 last:mb-0">
|
||||||
<label>{{ t('pages.settings.upload.availablePlaceholders') }}</label>
|
<label>{{ t('pages.settings.upload.availablePlaceholders') }}</label>
|
||||||
<div class="placeholder-help">
|
<div
|
||||||
<div class="placeholder-category">
|
class="mt-3 max-h-[400px] overflow-y-auto rounded-lg border border-border bg-bg-tertiary p-0 shadow-sm"
|
||||||
|
>
|
||||||
|
<div class="border-b border-b-border last:border-b-0">
|
||||||
<div class="category-title">
|
<div class="category-title">
|
||||||
{{ t('pages.settings.upload.placeholder.categoryTime') }}
|
{{ t('pages.settings.upload.placeholder.categoryTime') }}
|
||||||
</div>
|
</div>
|
||||||
@@ -701,10 +786,6 @@ function copyPlaceholder(placeholder: string) {
|
|||||||
message.success(t('pages.settings.upload.copySuccess', { content: placeholder }))
|
message.success(t('pages.settings.upload.copySuccess', { content: placeholder }))
|
||||||
}
|
}
|
||||||
|
|
||||||
const advancedAnimation = computed(() => ({
|
|
||||||
advancedAnimation: enableAdvancedAnimation.value,
|
|
||||||
}))
|
|
||||||
|
|
||||||
const filterList = computed(() => {
|
const filterList = computed(() => {
|
||||||
return getGallery()
|
return getGallery()
|
||||||
})
|
})
|
||||||
@@ -774,7 +855,6 @@ function onPreviewImageLoad() {
|
|||||||
function togglePicBedDropdown(event?: Event) {
|
function togglePicBedDropdown(event?: Event) {
|
||||||
picBedDropdownOpen.value = !picBedDropdownOpen.value
|
picBedDropdownOpen.value = !picBedDropdownOpen.value
|
||||||
if (sortDropdownOpen.value) sortDropdownOpen.value = false
|
if (sortDropdownOpen.value) sortDropdownOpen.value = false
|
||||||
|
|
||||||
if (picBedDropdownOpen.value && event) {
|
if (picBedDropdownOpen.value && event) {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
const trigger = event.target as HTMLElement
|
const trigger = event.target as HTMLElement
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,20 +1,24 @@
|
|||||||
@import "tailwindcss" reference;
|
@import 'tailwindcss' reference;
|
||||||
@import "../../assets/css/theme.css" reference;
|
@import '../../assets/css/theme.css' reference;
|
||||||
@import "../../assets/css/utilities.css" reference;
|
@import '../../assets/css/utilities.css' reference;
|
||||||
|
|
||||||
.segmented-button {
|
.segmented-button {
|
||||||
@apply flex items-center border-none border-r-border-secondary border-r py-2.5 px-4 text-sm font-[inherit] font-medium text-secondary duration-fast ease-standard gap-2 cursor-pointer whitespace-nowrap last:border-r-0 hover:text-accent-hover hover:bg-bg-tertiary;
|
@apply flex cursor-pointer items-center gap-2 border-r border-none border-r-border-secondary px-4 py-2.5 font-[inherit] text-sm font-medium whitespace-nowrap text-secondary duration-fast ease-standard last:border-r-0 hover:bg-bg-tertiary hover:text-accent-hover;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Quick Actions Card */
|
/* Quick Actions Card */
|
||||||
.quick-action-button {
|
.quick-action-button {
|
||||||
@apply relative flex flex-1 items-center border border-border-secondary rounded-lg px-4 py-3.5 font-[inherit] text-left bg-bg-secondary duration-medium ease-standard gap-2 cursor-pointer hover:border-accent-hover hover:shadow-md hover:-translate-y-[2px] [.has-badge]:pr-12 max-xs:py-3 max-xs:px-3.5 focus-visible:focus-ring;
|
@apply relative flex flex-1 cursor-pointer items-center gap-2 rounded-lg border border-border-secondary bg-bg-secondary px-4 py-3.5 text-left font-[inherit] duration-medium ease-standard hover:-translate-y-[2px] hover:border-accent-hover hover:shadow-md focus-visible:focus-ring max-xs:px-3.5 max-xs:py-3 [.has-badge]:pr-12;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-tab {
|
.filter-tab {
|
||||||
@apply py-2 px-4 text-sm font-medium border border-border-secondary rounded-md bg-bg-secondary text-secondary cursor-pointer transition-all duration-fast ease-standard whitespace-nowrap shadow-sm hover:border-accent hover:text-main hover:-translate-y-px hover:shadow-md [.active]:bg-accent [.active]:border-transparent [.active]:text-white [.active]:shadow-lg;
|
@apply cursor-pointer rounded-md border border-border-secondary bg-bg-secondary px-4 py-2 text-sm font-medium whitespace-nowrap text-secondary shadow-sm transition-all duration-fast ease-standard;
|
||||||
|
@apply hover:-translate-y-px hover:border-accent hover:text-main hover:shadow-md;
|
||||||
|
@apply [.active]:border-transparent [.active]:bg-accent [.active]:text-white [.active]:shadow-lg;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-icon-btn {
|
.task-icon-btn {
|
||||||
@apply flex items-center justify-center w-[32px] h-[32px] p-0 border border-border rounded-md bg-surface-elevated text-secondary cursor-pointer transition-all duration-fast ease-standard hover:border-accent hover:bg-accent hover:text-white hover:-translate-y-px [.is-high]:bg-warning [.is-high]:border-warning [.is-high]:text-white [.danger:hover]:bg-danger [.danger:hover]:border-danger [.danger:hover]:text-white;
|
@apply flex h-[32px] w-[32px] cursor-pointer items-center justify-center rounded-md border border-border bg-surface-elevated p-0 text-secondary transition-all duration-fast ease-standard;
|
||||||
|
@apply hover:-translate-y-px hover:border-accent hover:bg-accent hover:text-white;
|
||||||
|
@apply [.danger:hover]:border-danger [.danger:hover]:bg-danger [.danger:hover]:text-white [.is-high]:border-warning [.is-high]:bg-warning [.is-high]:text-white;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user