upd
This commit is contained in:
@@ -1,12 +1,22 @@
|
||||
<template>
|
||||
<div class="layout">
|
||||
<header class="app-header">
|
||||
<div class="header-ambient" aria-hidden="true" />
|
||||
<div class="header-inner">
|
||||
<div class="header-brand">
|
||||
<span class="brand-mark" />
|
||||
<h1 class="brand-title">TMC <em>Items Manager</em></h1>
|
||||
<svg class="brand-icon" width="22" height="16" viewBox="0 0 22 16" fill="none" aria-hidden="true">
|
||||
<rect x="0.75" y="0.75" width="20.5" height="14.5" rx="2" stroke="currentColor" stroke-width="1.4" />
|
||||
<line x1="11" y1="0.75" x2="11" y2="15.25" stroke="currentColor" stroke-width="1.4" />
|
||||
</svg>
|
||||
<span class="brand-tag">TMC</span>
|
||||
<span class="brand-sep" aria-hidden="true" />
|
||||
<h1 class="brand-title">Items <em>Manager</em></h1>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<span class="header-meta-label">Item collection</span>
|
||||
<span class="header-vsep" aria-hidden="true" />
|
||||
<time class="header-date">{{ formattedDate }}</time>
|
||||
</div>
|
||||
<span class="header-label">Manage & organize</span>
|
||||
</div>
|
||||
</header>
|
||||
<main class="panels">
|
||||
@@ -35,7 +45,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, watch } from 'vue'
|
||||
import { computed, onMounted, watch } from 'vue'
|
||||
|
||||
const formattedDate = computed(() =>
|
||||
new Date().toLocaleDateString('en-US', { month: 'short', year: 'numeric' }),
|
||||
)
|
||||
|
||||
const {
|
||||
leftItems,
|
||||
@@ -50,7 +64,7 @@ const {
|
||||
fetchRight,
|
||||
selectItem,
|
||||
deselectItem,
|
||||
reorderSelected,
|
||||
reorderItem,
|
||||
addItem,
|
||||
} = useItems()
|
||||
|
||||
@@ -70,8 +84,8 @@ async function handleDeselect(id: number) {
|
||||
await deselectItem(id)
|
||||
}
|
||||
|
||||
async function handleReorder(ids: number[]) {
|
||||
await reorderSelected(ids)
|
||||
async function handleReorder(id: number, afterId: number | null): Promise<void> {
|
||||
await reorderItem(id, afterId)
|
||||
}
|
||||
|
||||
async function handleAdd(id: number) {
|
||||
@@ -89,39 +103,63 @@ async function handleAdd(id: number) {
|
||||
|
||||
.app-header {
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
background: var(--surface);
|
||||
border-bottom: 1px solid var(--border);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header-ambient {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(ellipse 55% 180% at 0% 50%, rgba(252, 246, 228, 0.26) 0%, transparent 100%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.header-inner {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 28px;
|
||||
height: 52px;
|
||||
height: 64px;
|
||||
}
|
||||
|
||||
.header-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
gap: 11px;
|
||||
}
|
||||
|
||||
.brand-mark {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background: var(--text-primary);
|
||||
border-radius: 50%;
|
||||
.brand-icon {
|
||||
color: var(--text-primary);
|
||||
flex-shrink: 0;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.brand-tag {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.64rem;
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.14em;
|
||||
color: var(--text-secondary);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.brand-sep {
|
||||
width: 1px;
|
||||
height: 15px;
|
||||
background: var(--border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.brand-title {
|
||||
margin: 0;
|
||||
font-family: var(--font-serif);
|
||||
font-size: 1.05rem;
|
||||
font-size: 1.08rem;
|
||||
font-weight: 400;
|
||||
color: var(--text-primary);
|
||||
letter-spacing: -0.02em;
|
||||
letter-spacing: -0.025em;
|
||||
line-height: 1;
|
||||
|
||||
em {
|
||||
@@ -130,14 +168,33 @@ async function handleAdd(id: number) {
|
||||
}
|
||||
}
|
||||
|
||||
.header-label {
|
||||
font-size: 0.72rem;
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.header-meta-label {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.header-vsep {
|
||||
width: 1px;
|
||||
height: 12px;
|
||||
background: var(--border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.header-date {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.68rem;
|
||||
color: var(--text-muted);
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.panels {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
<div class="panel">
|
||||
<div class="panel-header">
|
||||
<div class="panel-title-row">
|
||||
<h2 class="panel-title">All items</h2>
|
||||
<h2 class="panel-title">
|
||||
All items
|
||||
</h2>
|
||||
<button class="btn-primary" @click="showAddModal = true">
|
||||
<svg width="11" height="11" viewBox="0 0 11 11" fill="none" aria-hidden="true">
|
||||
<path d="M5.5 1V10M1 5.5H10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
|
||||
@@ -59,7 +61,9 @@
|
||||
<div v-if="showAddModal" class="modal-overlay" @click.self="closeModal">
|
||||
<div class="modal-card">
|
||||
<div class="modal-header">
|
||||
<p class="modal-title">Add new item</p>
|
||||
<p class="modal-title">
|
||||
Add new item
|
||||
</p>
|
||||
<button class="modal-close" aria-label="Close" @click="closeModal">
|
||||
<svg width="13" height="13" viewBox="0 0 13 13" fill="none" aria-hidden="true">
|
||||
<path d="M1.5 1.5L11.5 11.5M11.5 1.5L1.5 11.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
|
||||
@@ -74,8 +78,12 @@
|
||||
@keydown.enter="submitAdd"
|
||||
>
|
||||
<div class="modal-footer">
|
||||
<button class="btn-ghost" @click="closeModal">Cancel</button>
|
||||
<button class="btn-primary" @click="submitAdd">Add item</button>
|
||||
<button class="btn-ghost" @click="closeModal">
|
||||
Cancel
|
||||
</button>
|
||||
<button class="btn-primary" @click="submitAdd">
|
||||
Add item
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -161,6 +169,7 @@ function submitAdd() {
|
||||
|
||||
/* ── Header ── */
|
||||
.panel-header {
|
||||
height: 108px;
|
||||
padding: 18px 20px 16px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: var(--surface);
|
||||
@@ -209,7 +218,9 @@ function submitAdd() {
|
||||
font-size: 0.825rem;
|
||||
color: var(--text-primary);
|
||||
outline: none;
|
||||
transition: border-color 150ms ease, background 150ms ease;
|
||||
transition:
|
||||
border-color 150ms ease,
|
||||
background 150ms ease;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--text-muted);
|
||||
@@ -228,7 +239,7 @@ function submitAdd() {
|
||||
gap: 6px;
|
||||
padding: 6px 12px;
|
||||
background: var(--text-primary);
|
||||
color: #FFFFFF;
|
||||
color: #ffffff;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
font-family: var(--font-sans);
|
||||
@@ -237,7 +248,9 @@ function submitAdd() {
|
||||
cursor: pointer;
|
||||
letter-spacing: 0.01em;
|
||||
white-space: nowrap;
|
||||
transition: background 150ms ease, transform 100ms ease;
|
||||
transition:
|
||||
background 150ms ease,
|
||||
transform 100ms ease;
|
||||
|
||||
&:hover {
|
||||
background: #333333;
|
||||
@@ -257,7 +270,10 @@ function submitAdd() {
|
||||
font-size: 0.825rem;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
transition: color 150ms ease, border-color 150ms ease, background 150ms ease;
|
||||
transition:
|
||||
color 150ms ease,
|
||||
border-color 150ms ease,
|
||||
background 150ms ease;
|
||||
|
||||
&:hover {
|
||||
color: var(--text-primary);
|
||||
@@ -271,9 +287,16 @@ function submitAdd() {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
|
||||
&::-webkit-scrollbar { width: 3px; }
|
||||
&::-webkit-scrollbar-track { background: transparent; }
|
||||
&::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
|
||||
&::-webkit-scrollbar {
|
||||
width: 3px;
|
||||
}
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--border);
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.item-list {
|
||||
@@ -283,8 +306,14 @@ function submitAdd() {
|
||||
}
|
||||
|
||||
@keyframes item-in {
|
||||
from { opacity: 0; transform: translateY(6px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(6px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.item-row {
|
||||
@@ -300,7 +329,9 @@ function submitAdd() {
|
||||
&:hover {
|
||||
background: var(--surface-subtle);
|
||||
|
||||
.action-btn { opacity: 1; }
|
||||
.action-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -328,7 +359,10 @@ function submitAdd() {
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: color 120ms ease, background 120ms ease, opacity 120ms ease;
|
||||
transition:
|
||||
color 120ms ease,
|
||||
background 120ms ease,
|
||||
opacity 120ms ease;
|
||||
|
||||
&:hover {
|
||||
color: var(--text-primary);
|
||||
@@ -355,14 +389,24 @@ function submitAdd() {
|
||||
background: var(--text-muted);
|
||||
animation: dot-blink 1.2s ease-in-out infinite;
|
||||
|
||||
&:nth-child(2) { animation-delay: 0.2s; }
|
||||
&:nth-child(3) { animation-delay: 0.4s; }
|
||||
&:nth-child(2) {
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
&:nth-child(3) {
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes dot-blink {
|
||||
0%, 80%, 100% { opacity: 0.25; }
|
||||
40% { opacity: 1; }
|
||||
0%,
|
||||
80%,
|
||||
100% {
|
||||
opacity: 0.25;
|
||||
}
|
||||
40% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
@@ -429,7 +473,9 @@ function submitAdd() {
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
transition: color 120ms ease, background 120ms ease;
|
||||
transition:
|
||||
color 120ms ease,
|
||||
background 120ms ease;
|
||||
|
||||
&:hover {
|
||||
color: var(--text-primary);
|
||||
@@ -447,7 +493,9 @@ function submitAdd() {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-primary);
|
||||
outline: none;
|
||||
transition: border-color 150ms ease, background 150ms ease;
|
||||
transition:
|
||||
border-color 150ms ease,
|
||||
background 150ms ease;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--text-muted);
|
||||
@@ -477,7 +525,9 @@ function submitAdd() {
|
||||
transition: opacity 180ms ease;
|
||||
|
||||
.modal-card {
|
||||
transition: transform 220ms cubic-bezier(0.16, 1, 0.3, 1), opacity 180ms ease;
|
||||
transition:
|
||||
transform 220ms cubic-bezier(0.16, 1, 0.3, 1),
|
||||
opacity 180ms ease;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -485,7 +535,9 @@ function submitAdd() {
|
||||
transition: opacity 150ms ease;
|
||||
|
||||
.modal-card {
|
||||
transition: transform 150ms ease, opacity 150ms ease;
|
||||
transition:
|
||||
transform 150ms ease,
|
||||
opacity 150ms ease;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
<div class="panel">
|
||||
<div class="panel-header">
|
||||
<div class="panel-title-row">
|
||||
<h2 class="panel-title">Selected items</h2>
|
||||
<h2 class="panel-title">
|
||||
Selected items
|
||||
</h2>
|
||||
<span v-if="items.length" class="item-count">{{ items.length }}</span>
|
||||
</div>
|
||||
<div class="search-wrap">
|
||||
@@ -38,10 +40,10 @@
|
||||
>
|
||||
<span class="drag-handle" title="Drag to reorder" aria-hidden="true">
|
||||
<svg width="8" height="12" viewBox="0 0 8 12" fill="currentColor">
|
||||
<circle cx="2" cy="2" r="1.2" />
|
||||
<circle cx="6" cy="2" r="1.2" />
|
||||
<circle cx="2" cy="6" r="1.2" />
|
||||
<circle cx="6" cy="6" r="1.2" />
|
||||
<circle cx="2" cy="2" r="1.2" />
|
||||
<circle cx="6" cy="2" r="1.2" />
|
||||
<circle cx="2" cy="6" r="1.2" />
|
||||
<circle cx="6" cy="6" r="1.2" />
|
||||
<circle cx="2" cy="10" r="1.2" />
|
||||
<circle cx="6" cy="10" r="1.2" />
|
||||
</svg>
|
||||
@@ -85,7 +87,7 @@ const emit = defineEmits<{
|
||||
(e: 'update:search', val: string): void
|
||||
(e: 'loadMore'): void
|
||||
(e: 'deselect', id: number): void
|
||||
(e: 'reorder', ids: number[]): void
|
||||
(e: 'reorder', id: number, afterId: number | null): void
|
||||
}>()
|
||||
|
||||
const sentinel = ref<HTMLElement | null>(null)
|
||||
@@ -127,9 +129,10 @@ watch(() => props.items, (val) => {
|
||||
localItems.value = [...val]
|
||||
}, { deep: true })
|
||||
|
||||
function onDragEnd(): void {
|
||||
const ids = localItems.value.map(i => i.id!)
|
||||
emit('reorder', ids)
|
||||
function onDragEnd(event: { oldIndex: number, newIndex: number }): void {
|
||||
const moved = localItems.value[event.newIndex]
|
||||
const after = event.newIndex > 0 ? localItems.value[event.newIndex - 1] : null
|
||||
emit('reorder', moved.id!, after?.id ?? null)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -143,6 +146,7 @@ function onDragEnd(): void {
|
||||
|
||||
/* ── Header ── */
|
||||
.panel-header {
|
||||
height: 108px;
|
||||
padding: 18px 20px 16px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: var(--surface);
|
||||
@@ -206,7 +210,9 @@ function onDragEnd(): void {
|
||||
font-size: 0.825rem;
|
||||
color: var(--text-primary);
|
||||
outline: none;
|
||||
transition: border-color 150ms ease, background 150ms ease;
|
||||
transition:
|
||||
border-color 150ms ease,
|
||||
background 150ms ease;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--text-muted);
|
||||
@@ -223,9 +229,16 @@ function onDragEnd(): void {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
|
||||
&::-webkit-scrollbar { width: 3px; }
|
||||
&::-webkit-scrollbar-track { background: transparent; }
|
||||
&::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
|
||||
&::-webkit-scrollbar {
|
||||
width: 3px;
|
||||
}
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--border);
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.item-list {
|
||||
@@ -235,8 +248,14 @@ function onDragEnd(): void {
|
||||
}
|
||||
|
||||
@keyframes item-in {
|
||||
from { opacity: 0; transform: translateY(6px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(6px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.item-row {
|
||||
@@ -252,8 +271,12 @@ function onDragEnd(): void {
|
||||
&:hover {
|
||||
background: var(--surface-subtle);
|
||||
|
||||
.action-btn { opacity: 1; }
|
||||
.drag-handle { color: var(--text-secondary); }
|
||||
.action-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
.drag-handle {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -267,7 +290,9 @@ function onDragEnd(): void {
|
||||
transition: color 120ms ease;
|
||||
user-select: none;
|
||||
|
||||
&:active { cursor: grabbing; }
|
||||
&:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
}
|
||||
|
||||
.item-order {
|
||||
@@ -305,7 +330,10 @@ function onDragEnd(): void {
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
flex-shrink: 0;
|
||||
transition: color 120ms ease, background 120ms ease, opacity 120ms ease;
|
||||
transition:
|
||||
color 120ms ease,
|
||||
background 120ms ease,
|
||||
opacity 120ms ease;
|
||||
|
||||
&:hover {
|
||||
color: var(--text-primary);
|
||||
@@ -332,14 +360,24 @@ function onDragEnd(): void {
|
||||
background: var(--text-muted);
|
||||
animation: dot-blink 1.2s ease-in-out infinite;
|
||||
|
||||
&:nth-child(2) { animation-delay: 0.2s; }
|
||||
&:nth-child(3) { animation-delay: 0.4s; }
|
||||
&:nth-child(2) {
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
&:nth-child(3) {
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes dot-blink {
|
||||
0%, 80%, 100% { opacity: 0.25; }
|
||||
40% { opacity: 1; }
|
||||
0%,
|
||||
80%,
|
||||
100% {
|
||||
opacity: 0.25;
|
||||
}
|
||||
40% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
|
||||
@@ -75,8 +75,8 @@ export function useItems() {
|
||||
await Promise.all([fetchLeft(true), fetchRight(true)])
|
||||
}
|
||||
|
||||
async function reorderSelected(ids: number[]): Promise<void> {
|
||||
await client.api.itemsReorderUpdate({ ids })
|
||||
async function reorderItem(id: number, afterId: number | null): Promise<void> {
|
||||
await client.api.itemsReorderUpdate({ id, afterId })
|
||||
}
|
||||
|
||||
async function addItem(id: number): Promise<void> {
|
||||
@@ -97,7 +97,7 @@ export function useItems() {
|
||||
fetchRight,
|
||||
selectItem,
|
||||
deselectItem,
|
||||
reorderSelected,
|
||||
reorderItem,
|
||||
addItem,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -357,12 +357,13 @@ export class Api<
|
||||
*
|
||||
* @tags Items
|
||||
* @name ItemsReorderUpdate
|
||||
* @summary Queue a reorder of selected items
|
||||
* @summary Move a selected item after another item (works with filtered lists)
|
||||
* @request PUT:/api/items/reorder
|
||||
*/
|
||||
itemsReorderUpdate: (
|
||||
data: {
|
||||
ids: number[];
|
||||
id: number;
|
||||
afterId: number | null;
|
||||
},
|
||||
params: RequestParams = {},
|
||||
) =>
|
||||
|
||||
Reference in New Issue
Block a user