feat(ui): fix thumbnail size slider direction and add step buttons
This commit is contained in:
@@ -1,19 +1,34 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Icon from './Icon.svelte';
|
import Icon from './Icon.svelte';
|
||||||
import { density } from '$lib/stores/density.svelte';
|
import IconButton from './IconButton.svelte';
|
||||||
|
import { density, DENSITY_MIN, DENSITY_MAX } from '$lib/stores/density.svelte';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Icon name="grid_view" class="text-text-subtle size-4" />
|
<Icon name="grid_view" class="text-text-subtle size-4" />
|
||||||
|
<IconButton
|
||||||
|
name="density_small"
|
||||||
|
label="Уменьшить миниатюры"
|
||||||
|
disabled={density.size <= DENSITY_MIN}
|
||||||
|
onclick={() => density.stepSize(-1)}
|
||||||
|
class="size-7"
|
||||||
|
/>
|
||||||
<input
|
<input
|
||||||
type="range"
|
type="range"
|
||||||
min="2"
|
min={DENSITY_MIN}
|
||||||
max="8"
|
max={DENSITY_MAX}
|
||||||
step="1"
|
step="1"
|
||||||
value={density.cols}
|
value={density.size}
|
||||||
oninput={(e) => density.set(Number(e.currentTarget.value))}
|
oninput={(e) => density.setSize(Number(e.currentTarget.value))}
|
||||||
class="accent-primary h-1 w-24 cursor-pointer"
|
class="accent-primary h-1 w-24 cursor-pointer"
|
||||||
aria-label="Размер миниатюр"
|
aria-label="Размер миниатюр"
|
||||||
title="Размер миниатюр"
|
title="Размер миниатюр"
|
||||||
/>
|
/>
|
||||||
|
<IconButton
|
||||||
|
name="density_large"
|
||||||
|
label="Увеличить миниатюры"
|
||||||
|
disabled={density.size >= DENSITY_MAX}
|
||||||
|
onclick={() => density.stepSize(1)}
|
||||||
|
class="size-7"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ export const icons = {
|
|||||||
label: { path: '<path fill="currentColor" d="M17.63 5.84C17.27 5.33 16.67 5 16 5H5a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h11c.67 0 1.27-.33 1.63-.84L22 12z"/>' },
|
label: { path: '<path fill="currentColor" d="M17.63 5.84C17.27 5.33 16.67 5 16 5H5a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h11c.67 0 1.27-.33 1.63-.84L22 12z"/>' },
|
||||||
menu: { path: '<path fill="currentColor" d="M3 18v-2h18v2zm0-5v-2h18v2zm0-5V6h18v2z"/>' },
|
menu: { path: '<path fill="currentColor" d="M3 18v-2h18v2zm0-5v-2h18v2zm0-5V6h18v2z"/>' },
|
||||||
grid_view: { path: '<path fill="currentColor" d="M3 3v8h8V3zm10 0v8h8V3zM3 13v8h8v-8zm10 0v8h8v-8z"/>' },
|
grid_view: { path: '<path fill="currentColor" d="M3 3v8h8V3zm10 0v8h8V3zM3 13v8h8v-8zm10 0v8h8v-8z"/>' },
|
||||||
|
density_small: { path: '<path fill="currentColor" d="M3 22v-2h18v2zm0-6v-2h18v2zm0-6V8h18v2zm0-6V2h18v2z"/>' },
|
||||||
|
density_large: { path: '<path fill="currentColor" d="M3 21v-2h18v2zM3 5V3h18v2z"/>' },
|
||||||
wallpaper: { path: '<path fill="currentColor" d="M4 4h7V2H4c-1.1 0-2 .9-2 2v7h2zm6 9l-4 5h12l-3-4l-2.03 2.71zm7-4.5c0-.83-.67-1.5-1.5-1.5S14 7.67 14 8.5s.67 1.5 1.5 1.5S17 9.33 17 8.5M20 2h-7v2h7v7h2V4c0-1.1-.9-2-2-2m0 18h-7v2h7c1.1 0 2-.9 2-2v-7h-2zM4 13H2v7c0 1.1.9 2 2 2h7v-2H4z"/>' },
|
wallpaper: { path: '<path fill="currentColor" d="M4 4h7V2H4c-1.1 0-2 .9-2 2v7h2zm6 9l-4 5h12l-3-4l-2.03 2.71zm7-4.5c0-.83-.67-1.5-1.5-1.5S14 7.67 14 8.5s.67 1.5 1.5 1.5S17 9.33 17 8.5M20 2h-7v2h7v7h2V4c0-1.1-.9-2-2-2m0 18h-7v2h7c1.1 0 2-.9 2-2v-7h-2zM4 13H2v7c0 1.1.9 2 2 2h7v-2H4z"/>' },
|
||||||
fullscreen: { path: '<path fill="currentColor" d="M7 14H5v5h5v-2H7zm-2-4h2V7h3V5H5zm12 7h-3v2h5v-5h-2zM14 5v2h3v3h2V5z"/>' },
|
fullscreen: { path: '<path fill="currentColor" d="M7 14H5v5h5v-2H7zm-2-4h2V7h3V5H5zm12 7h-3v2h5v-5h-2zM14 5v2h3v3h2V5z"/>' },
|
||||||
fullscreen_exit: { path: '<path fill="currentColor" d="M5 16h3v3h2v-5H5zm3-8H5v2h5V5H8zm6 11h2v-3h3v-2h-5zm2-11v3h-2v2h5V5z"/>' },
|
fullscreen_exit: { path: '<path fill="currentColor" d="M5 16h3v3h2v-5H5zm3-8H5v2h5V5H8zm6 11h2v-3h3v-2h-5zm2-11v3h-2v2h5V5z"/>' },
|
||||||
|
|||||||
@@ -5,6 +5,11 @@ const MAX = 8;
|
|||||||
class DensityStore {
|
class DensityStore {
|
||||||
cols = $state(5);
|
cols = $state(5);
|
||||||
|
|
||||||
|
/** Размер миниатюр: больше значение — крупнее плитки (меньше колонок). */
|
||||||
|
get size(): number {
|
||||||
|
return MIN + MAX - this.cols;
|
||||||
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
if (typeof window === 'undefined') return;
|
if (typeof window === 'undefined') return;
|
||||||
const v = parseInt(localStorage.getItem(KEY) ?? '', 10);
|
const v = parseInt(localStorage.getItem(KEY) ?? '', 10);
|
||||||
@@ -15,6 +20,17 @@ class DensityStore {
|
|||||||
this.cols = Math.min(MAX, Math.max(MIN, Math.round(value)));
|
this.cols = Math.min(MAX, Math.max(MIN, Math.round(value)));
|
||||||
if (typeof window !== 'undefined') localStorage.setItem(KEY, String(this.cols));
|
if (typeof window !== 'undefined') localStorage.setItem(KEY, String(this.cols));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setSize(value: number) {
|
||||||
|
const size = Math.min(MAX, Math.max(MIN, Math.round(value)));
|
||||||
|
this.set(MIN + MAX - size);
|
||||||
|
}
|
||||||
|
|
||||||
|
stepSize(delta: number) {
|
||||||
|
this.setSize(this.size + delta);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export { MIN as DENSITY_MIN, MAX as DENSITY_MAX };
|
||||||
|
|
||||||
export const density = new DensityStore();
|
export const density = new DensityStore();
|
||||||
|
|||||||
@@ -472,6 +472,7 @@
|
|||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<DensityControl />
|
<DensityControl />
|
||||||
{#if canUpload}
|
{#if canUpload}
|
||||||
|
<span class="bg-border h-5 w-px shrink-0" aria-hidden="true"></span>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="text-text-muted hover:bg-bg-muted hover:text-text inline-flex items-center justify-center rounded-md p-1.5"
|
class="text-text-muted hover:bg-bg-muted hover:text-text inline-flex items-center justify-center rounded-md p-1.5"
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
created: 2026-10-11
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# Proposal
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
Регулятор размера миниатюр работает «наоборот»: сдвиг вправо увеличивает число колонок и
|
||||||
|
уменьшает плитки, тогда как пользователи ожидают обратного — вправо крупнее. Это делает
|
||||||
|
управление размером контринтуитивным, а без кнопок шага нельзя подстроить сетку одним нажатием.
|
||||||
|
Кнопка загрузки к тому же визуально сливается с регулятором в тулбаре альбома.
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- Инвертировать отображение регулятора: **вправо — крупнее** (меньше колонок), **влево — мельче**
|
||||||
|
(больше колонок). Каноническое значение и формат хранения не меняются.
|
||||||
|
- Добавить по бокам регулятора две иконочные кнопки шага: «−» (мельче) и «+» (крупнее), в пределах
|
||||||
|
существующего диапазона 2..8.
|
||||||
|
- Добавить иконки `density_small` и `density_large` в реестр иконок.
|
||||||
|
- Отделить кнопку загрузки от регулятора вертикальным разделителем в тулбаре альбома.
|
||||||
|
- Оставить единый регулятор в обоих местах — обзор альбомов и сетка фото альбома — с общим
|
||||||
|
сохранённым значением.
|
||||||
|
- Не менять ключ `localStorage` и смысл `density.cols`, чтобы не сломать клавиатурную навигацию
|
||||||
|
(шаг строки = число колонок) и не мигрировать сохранённые значения.
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### New Capabilities
|
||||||
|
|
||||||
|
Нет.
|
||||||
|
|
||||||
|
### Modified Capabilities
|
||||||
|
|
||||||
|
- `ui-workspace`: требование «Плотность сетки» уточняется — направление регулятора (вправо
|
||||||
|
крупнее), кнопки шага и единый размер в обоих местах.
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- Frontend: `lib/components/DensityControl.svelte`, `lib/stores/density.svelte.ts`,
|
||||||
|
`lib/icons/registry.ts`, `routes/(app)/albums/[id]/+page.svelte`.
|
||||||
|
- Сохранённое значение `localStorage['grid-cols']` и семантика `density.cols` остаются прежними.
|
||||||
|
- Backend, API и зависимости не затрагиваются.
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# Spec Delta
|
||||||
|
|
||||||
|
## MODIFIED Requirements
|
||||||
|
|
||||||
|
### Requirement: Плотность сетки
|
||||||
|
|
||||||
|
Сетка SHALL давать пользователю управлять размером миниатюр: сдвиг регулятора вправо SHALL
|
||||||
|
увеличивать миниатюры, влево — уменьшать. Регулятор SHALL сопровождаться кнопками шага в
|
||||||
|
сторону уменьшения и увеличения. Выбранный размер SHALL сохраняться между сессиями и SHALL
|
||||||
|
быть общим для обзора альбомов и сетки фото альбома.
|
||||||
|
|
||||||
|
#### Scenario: Изменение размера
|
||||||
|
|
||||||
|
- **WHEN** пользователь двигает регулятор плотности
|
||||||
|
- **THEN** размер миниатюр меняется, значение сохраняется и восстанавливается при следующем открытии
|
||||||
|
|
||||||
|
#### Scenario: Направление регулятора
|
||||||
|
|
||||||
|
- **WHEN** пользователь сдвигает регулятор вправо
|
||||||
|
- **THEN** миниатюры становятся крупнее; при сдвиге влево — мельче
|
||||||
|
|
||||||
|
#### Scenario: Кнопки шага
|
||||||
|
|
||||||
|
- **WHEN** пользователь нажимает кнопку увеличения или уменьшения рядом с регулятором
|
||||||
|
- **THEN** размер миниатюр меняется на один шаг и сохраняется
|
||||||
|
|
||||||
|
#### Scenario: Единый размер в обоих местах
|
||||||
|
|
||||||
|
- **WHEN** пользователь меняет размер на обзоре альбомов или в сетке фото альбома
|
||||||
|
- **THEN** тот же размер применяется в обоих местах
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# Tasks
|
||||||
|
|
||||||
|
## 1. Иконки плотности
|
||||||
|
|
||||||
|
- [x] 1.1 Добавить в `frontend/src/lib/icons/registry.ts` иконки `density_small` и `density_large` (Material Symbols, filled) — проверка: `cd frontend && npm run check` проходит без ошибок, обе иконки отрисованы как вектор через `<Icon name>`.
|
||||||
|
|
||||||
|
## 2. Управление размером миниатюр
|
||||||
|
|
||||||
|
- [x] 2.1 В `frontend/src/lib/stores/density.svelte.ts` добавить отображение «размер» ↔ `cols` (вправо = крупнее), сохранив `cols` каноническим и ключ `localStorage['grid-cols']` неизменным — проверка: `cd frontend && npm run check`.
|
||||||
|
- [x] 2.2 Переписать `frontend/src/lib/components/DensityControl.svelte`: ползунок с направлением «вправо крупнее», кнопки «−»/«+» (`density_small`/`density_large`) по сторонам, доступные имена, диапазон 2..8 — проверка: в `npm run dev` значение меняется ползунком и кнопками и сохраняется.
|
||||||
|
- [x] 2.3 Подтвердить, что регулятор показан на обоих экранах и делит одно значение (`frontend/src/routes/(app)/+page.svelte` и `frontend/src/routes/(app)/albums/[id]/+page.svelte`) — проверка: смена размера на обзоре альбомов отражается в сетке альбома и переживает перезагрузку.
|
||||||
|
|
||||||
|
## 3. Разделитель в тулбаре альбома
|
||||||
|
|
||||||
|
- [x] 3.1 Добавить вертикальный разделитель между `DensityControl` и кнопкой загрузки в `frontend/src/routes/(app)/albums/[id]/+page.svelte` — проверка: в `npm run dev` черта видна, доступные имена обеих кнопок сохранены.
|
||||||
|
|
||||||
|
## 4. Интеграционная проверка
|
||||||
|
|
||||||
|
- [x] 4.1 `cd frontend && npm run check && npm run build` проходит; вручную: направление ползунка (вправо — крупнее), шаг кнопками, сохранение значения, единый размер в обзоре альбомов и в сетке фото альбома.
|
||||||
|
|
||||||
|
## Workflow follow-up
|
||||||
|
|
||||||
|
- Заархивировать change после выполнения требований ревью проекта (`/opsx-archive`).
|
||||||
|
- Проверить результат после архивации.
|
||||||
Reference in New Issue
Block a user