feat(web): скрытие боковых панелей в альбоме и хоткеи размера миниатюр
В режиме просмотра альбома добавлена кнопка скрытия левой и правой боковых панелей (рядом с регулятором размера миниатюр) и хоткей Tab. В скрытом режиме панели не занимают место в раскладке, а появляются по наведению на левую или правую кромку окна и прячутся после ухода указателя. Клавиши + и - меняют размер миниатюр на шаг, как кнопки регулятора; хоткей работает на всех экранах приложения и не срабатывает при вводе текста, в открытых диалогах и меню. Спека ui-workspace дополнена требованиями о скрытии панелей и клавишах изменения размера миниатюр.
This commit is contained in:
@@ -7,6 +7,8 @@
|
|||||||
import Icon from './Icon.svelte';
|
import Icon from './Icon.svelte';
|
||||||
import ThemeSwitcher from './ThemeSwitcher.svelte';
|
import ThemeSwitcher from './ThemeSwitcher.svelte';
|
||||||
import { auth } from '$lib/stores/auth.svelte';
|
import { auth } from '$lib/stores/auth.svelte';
|
||||||
|
import { density } from '$lib/stores/density.svelte';
|
||||||
|
import { panels } from '$lib/stores/panels.svelte';
|
||||||
import type { Snippet } from 'svelte';
|
import type { Snippet } from 'svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -14,9 +16,11 @@
|
|||||||
rail?: Snippet;
|
rail?: Snippet;
|
||||||
panel?: Snippet;
|
panel?: Snippet;
|
||||||
panelVisible?: boolean;
|
panelVisible?: boolean;
|
||||||
|
/** Показывать переключатель скрытия боковых панелей (режим просмотра альбома). */
|
||||||
|
panelsToggle?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { children, rail, panel, panelVisible = true }: Props = $props();
|
let { children, rail, panel, panelVisible = true, panelsToggle = false }: Props = $props();
|
||||||
|
|
||||||
let railOpen = $state(false);
|
let railOpen = $state(false);
|
||||||
let panelOpen = $state(false);
|
let panelOpen = $state(false);
|
||||||
@@ -29,6 +33,11 @@
|
|||||||
|
|
||||||
let isFullscreen = $state(false);
|
let isFullscreen = $state(false);
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
// Режим скрытия действует только там, где он доступен (просмотр альбома).
|
||||||
|
if (!panelsToggle) panels.reset();
|
||||||
|
});
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
const r = parseInt(localStorage.getItem(RAIL.key) ?? '', 10);
|
const r = parseInt(localStorage.getItem(RAIL.key) ?? '', 10);
|
||||||
if (r >= RAIL.min && r <= RAIL.max) railWidth = r;
|
if (r >= RAIL.min && r <= RAIL.max) railWidth = r;
|
||||||
@@ -93,6 +102,75 @@
|
|||||||
const handle =
|
const handle =
|
||||||
'bg-transparent hover:bg-primary/40 focus-visible:bg-primary/40 hidden w-1 shrink-0 cursor-col-resize p-0 transition-colors outline-none';
|
'bg-transparent hover:bg-primary/40 focus-visible:bg-primary/40 hidden w-1 shrink-0 cursor-col-resize p-0 transition-colors outline-none';
|
||||||
|
|
||||||
|
const railClass = $derived(
|
||||||
|
`border-border bg-chrome absolute inset-y-0 left-0 z-40 w-60 overflow-y-auto border-r transition-transform motion-reduce:transition-none lg:w-[var(--rail-w)] ${
|
||||||
|
panels.hidden
|
||||||
|
? `${railOpen ? 'translate-x-0' : '-translate-x-full'} lg:z-30 ${panels.railPeek ? 'lg:translate-x-0' : 'lg:-translate-x-full'}`
|
||||||
|
: `lg:static lg:z-auto lg:translate-x-0 ${railOpen ? 'translate-x-0' : '-translate-x-full'}`
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
|
||||||
|
const panelClass = $derived(
|
||||||
|
`border-border bg-chrome absolute inset-y-0 right-0 z-40 w-72 overflow-y-auto border-l transition-transform motion-reduce:transition-none xl:w-[var(--panel-w)] ${
|
||||||
|
panels.hidden
|
||||||
|
? `${panelOpen ? 'translate-x-0' : 'translate-x-full'} xl:z-30 ${panels.panelPeek ? 'xl:translate-x-0' : 'xl:translate-x-full'}`
|
||||||
|
: `xl:static xl:z-auto xl:translate-x-0 ${panelOpen ? 'translate-x-0' : 'translate-x-full'}`
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
|
||||||
|
function isTyping(): boolean {
|
||||||
|
const el = document.activeElement as HTMLElement | null;
|
||||||
|
if (!el) return false;
|
||||||
|
const tag = el.tagName;
|
||||||
|
return tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || el.isContentEditable;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Как isTyping, но ползунок и служебные input'ы текстом не считаются. */
|
||||||
|
function isTextEntry(): boolean {
|
||||||
|
const el = document.activeElement as HTMLElement | null;
|
||||||
|
if (!el) return false;
|
||||||
|
if (el.isContentEditable) return true;
|
||||||
|
const tag = el.tagName;
|
||||||
|
if (tag === 'TEXTAREA' || tag === 'SELECT') return true;
|
||||||
|
if (tag === 'INPUT') {
|
||||||
|
return !['range', 'checkbox', 'radio', 'button', 'submit', 'reset'].includes(
|
||||||
|
(el as HTMLInputElement).type
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasOverlay(): boolean {
|
||||||
|
return !!document.querySelector('[role="dialog"][aria-modal="true"], [role="menu"]');
|
||||||
|
}
|
||||||
|
|
||||||
|
function onKeydown(ev: KeyboardEvent) {
|
||||||
|
if (ev.ctrlKey || ev.metaKey || ev.altKey || ev.repeat) return;
|
||||||
|
|
||||||
|
// `+`/`-` меняют размер миниатюр — как кнопки под регулятором.
|
||||||
|
if (ev.key === '+' || ev.key === '=' || ev.key === '-' || ev.key === '_') {
|
||||||
|
if (isTextEntry() || hasOverlay()) return;
|
||||||
|
ev.preventDefault();
|
||||||
|
density.stepSize(ev.key === '+' || ev.key === '=' ? 1 : -1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!panelsToggle || !panel || !panelVisible) return;
|
||||||
|
if (ev.key !== 'Tab' || ev.shiftKey) return;
|
||||||
|
if (isTyping() || hasOverlay()) return;
|
||||||
|
ev.preventDefault();
|
||||||
|
panels.toggle();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onAsideLeave(ev: MouseEvent, side: 'left' | 'right') {
|
||||||
|
if (!panels.hidden) return;
|
||||||
|
const el = ev.currentTarget as HTMLElement | null;
|
||||||
|
// Панель с фокусом не прячем — иначе клавиатурный пользователь теряет её.
|
||||||
|
if (el?.contains(document.activeElement)) return;
|
||||||
|
if (side === 'left') panels.railPeek = false;
|
||||||
|
else panels.panelPeek = false;
|
||||||
|
}
|
||||||
|
|
||||||
async function logout() {
|
async function logout() {
|
||||||
if (auth.refresh) {
|
if (auth.refresh) {
|
||||||
await authApi.logout(auth.refresh).catch(() => {});
|
await authApi.logout(auth.refresh).catch(() => {});
|
||||||
@@ -102,6 +180,8 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<svelte:window onkeydown={onKeydown} />
|
||||||
|
|
||||||
<div class="bg-canvas text-text flex h-screen flex-col overflow-hidden">
|
<div class="bg-canvas text-text flex h-screen flex-col overflow-hidden">
|
||||||
<header
|
<header
|
||||||
class="border-border bg-chrome flex shrink-0 items-center justify-between gap-2 border-b px-2 py-0.5 sm:px-3"
|
class="border-border bg-chrome flex shrink-0 items-center justify-between gap-2 border-b px-2 py-0.5 sm:px-3"
|
||||||
@@ -183,15 +263,16 @@
|
|||||||
></button>
|
></button>
|
||||||
{/if}
|
{/if}
|
||||||
<aside
|
<aside
|
||||||
class="border-border bg-chrome absolute inset-y-0 left-0 z-40 w-60 overflow-y-auto border-r transition-transform lg:static lg:z-auto lg:w-[var(--rail-w)] lg:translate-x-0 {railOpen
|
class={railClass}
|
||||||
? 'translate-x-0'
|
|
||||||
: '-translate-x-full'}"
|
|
||||||
style="--rail-w: {railWidth}px"
|
style="--rail-w: {railWidth}px"
|
||||||
aria-label="Альбомы"
|
aria-label="Альбомы"
|
||||||
|
inert={panels.hidden && !panels.railPeek && !railOpen}
|
||||||
|
onmouseleave={(e) => onAsideLeave(e, 'left')}
|
||||||
>
|
>
|
||||||
{#if rail}{@render rail()}{/if}
|
{#if rail}{@render rail()}{/if}
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
|
{#if !panels.hidden}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="{handle} lg:block"
|
class="{handle} lg:block"
|
||||||
@@ -200,6 +281,7 @@
|
|||||||
onpointerdown={(e) => startResize('left', e)}
|
onpointerdown={(e) => startResize('left', e)}
|
||||||
onkeydown={(e) => nudge('left', e)}
|
onkeydown={(e) => nudge('left', e)}
|
||||||
></button>
|
></button>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<main class="bg-canvas min-h-0 flex-1 overflow-y-auto">
|
<main class="bg-canvas min-h-0 flex-1 overflow-y-auto">
|
||||||
{@render children()}
|
{@render children()}
|
||||||
@@ -214,6 +296,7 @@
|
|||||||
onclick={() => (panelOpen = false)}
|
onclick={() => (panelOpen = false)}
|
||||||
></button>
|
></button>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if !panels.hidden}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="{handle} xl:block"
|
class="{handle} xl:block"
|
||||||
@@ -222,15 +305,35 @@
|
|||||||
onpointerdown={(e) => startResize('right', e)}
|
onpointerdown={(e) => startResize('right', e)}
|
||||||
onkeydown={(e) => nudge('right', e)}
|
onkeydown={(e) => nudge('right', e)}
|
||||||
></button>
|
></button>
|
||||||
|
{/if}
|
||||||
<aside
|
<aside
|
||||||
class="border-border bg-chrome absolute inset-y-0 right-0 z-40 w-72 overflow-y-auto border-l transition-transform xl:static xl:z-auto xl:w-[var(--panel-w)] xl:translate-x-0 {panelOpen
|
class={panelClass}
|
||||||
? 'translate-x-0'
|
|
||||||
: 'translate-x-full'}"
|
|
||||||
style="--panel-w: {panelWidth}px"
|
style="--panel-w: {panelWidth}px"
|
||||||
aria-label="Панель"
|
aria-label="Панель"
|
||||||
|
inert={panels.hidden && !panels.panelPeek && !panelOpen}
|
||||||
|
onmouseleave={(e) => onAsideLeave(e, 'right')}
|
||||||
>
|
>
|
||||||
{@render panel()}
|
{@render panel()}
|
||||||
</aside>
|
</aside>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if panels.hidden}
|
||||||
|
<div
|
||||||
|
class="group absolute inset-y-0 left-0 z-20 hidden w-1.5 lg:block"
|
||||||
|
aria-hidden="true"
|
||||||
|
onmouseenter={() => (panels.railPeek = true)}
|
||||||
|
>
|
||||||
|
<span class="block h-full w-full transition-colors group-hover:bg-primary/40"></span>
|
||||||
|
</div>
|
||||||
|
{#if panel && panelVisible}
|
||||||
|
<div
|
||||||
|
class="group absolute inset-y-0 right-0 z-20 hidden w-1.5 xl:block"
|
||||||
|
aria-hidden="true"
|
||||||
|
onmouseenter={() => (panels.panelPeek = true)}
|
||||||
|
>
|
||||||
|
<span class="block h-full w-full transition-colors group-hover:bg-primary/40"></span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<Icon name="grid_view" class="text-text-subtle size-4" />
|
<Icon name="grid_view" class="text-text-subtle size-4" />
|
||||||
<IconButton
|
<IconButton
|
||||||
name="remove"
|
name="remove"
|
||||||
label="Уменьшить миниатюры"
|
label="Уменьшить миниатюры (-)"
|
||||||
disabled={density.size <= DENSITY_MIN}
|
disabled={density.size <= DENSITY_MIN}
|
||||||
onclick={() => density.stepSize(-1)}
|
onclick={() => density.stepSize(-1)}
|
||||||
class="size-7"
|
class="size-7"
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
name="add"
|
name="add"
|
||||||
label="Увеличить миниатюры"
|
label="Увеличить миниатюры (+)"
|
||||||
disabled={density.size >= DENSITY_MAX}
|
disabled={density.size >= DENSITY_MAX}
|
||||||
onclick={() => density.stepSize(1)}
|
onclick={() => density.stepSize(1)}
|
||||||
class="size-7"
|
class="size-7"
|
||||||
|
|||||||
@@ -53,7 +53,9 @@ export const icons = {
|
|||||||
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"/>' },
|
||||||
contrast: { path: '<path fill="currentColor" fill-rule="evenodd" d="M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20zM12 4a8 8 0 1 0 0 16 8 8 0 0 0 0-16zM12 4a8 8 0 0 1 0 16z"/>' },
|
contrast: { path: '<path fill="currentColor" fill-rule="evenodd" d="M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20zM12 4a8 8 0 1 0 0 16 8 8 0 0 0 0-16zM12 4a8 8 0 0 1 0 16z"/>' },
|
||||||
check_all: { path: '<path fill="currentColor" d="M18 7l-1.41-1.41l-6.34 6.34l1.41 1.41zm4.24-1.41L11.66 16.17L7.48 12l-1.41 1.41L11.66 19l12-12zM.41 13.41L6 19l1.41-1.41L1.83 12z"/>' },
|
check_all: { path: '<path fill="currentColor" d="M18 7l-1.41-1.41l-6.34 6.34l1.41 1.41zm4.24-1.41L11.66 16.17L7.48 12l-1.41 1.41L11.66 19l12-12zM.41 13.41L6 19l1.41-1.41L1.83 12z"/>' },
|
||||||
uncheck_all: { path: '<path fill="currentColor" d="M1.79 12l5.58 5.59L5.96 19L.37 13.41zm.45-7.78L12.9 14.89l-1.28 1.28L7.44 12l-1.41 1.41L11.62 19l2.69-2.69l4.89 4.89l1.41-1.41L3.65 2.81zm14.9 9.27L23.62 7L22.2 5.59l-6.48 6.48zm-1.18-6.49l-1.41-1.41l-3.65 3.66l1.41 1.41z"/>' }
|
uncheck_all: { path: '<path fill="currentColor" d="M1.79 12l5.58 5.59L5.96 19L.37 13.41zm.45-7.78L12.9 14.89l-1.28 1.28L7.44 12l-1.41 1.41L11.62 19l2.69-2.69l4.89 4.89l1.41-1.41L3.65 2.81zm14.9 9.27L23.62 7L22.2 5.59l-6.48 6.48zm-1.18-6.49l-1.41-1.41l-3.65 3.66l1.41 1.41z"/>' },
|
||||||
|
left_panel_close: { path: '<path fill="currentColor" d="M16.5 16V8l-4 4zM5 21q-.825 0-1.412-.587T3 19V5q0-.825.588-1.412T5 3h14q.825 0 1.413.588T21 5v14q0 .825-.587 1.413T19 21zm5-2h9V5h-9z"/>' },
|
||||||
|
left_panel_open: { path: '<path fill="currentColor" d="M12.5 8v8l4-4zM5 21q-.825 0-1.412-.587T3 19V5q0-.825.588-1.412T5 3h14q.825 0 1.413.588T21 5v14q0 .825-.587 1.413T19 21zm5-2h9V5h-9z"/>' }
|
||||||
} satisfies Record<string, IconDef>;
|
} satisfies Record<string, IconDef>;
|
||||||
|
|
||||||
export type IconName = keyof typeof icons;
|
export type IconName = keyof typeof icons;
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
class PanelsStore {
|
||||||
|
/** Боковые панели убраны за край, содержимое занимает всю ширину. */
|
||||||
|
hidden = $state(false);
|
||||||
|
/** Левая панель показана у кромки в скрытом режиме. */
|
||||||
|
railPeek = $state(false);
|
||||||
|
/** Правая панель показана у кромки в скрытом режиме. */
|
||||||
|
panelPeek = $state(false);
|
||||||
|
|
||||||
|
toggle() {
|
||||||
|
this.hidden = !this.hidden;
|
||||||
|
this.railPeek = false;
|
||||||
|
this.panelPeek = false;
|
||||||
|
if (this.hidden && typeof document !== 'undefined') {
|
||||||
|
// Не оставляем фокус внутри панели, которая уезжает за край.
|
||||||
|
const el = document.activeElement as HTMLElement | null;
|
||||||
|
if (el?.closest('aside')) el.blur();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.hidden = false;
|
||||||
|
this.railPeek = false;
|
||||||
|
this.panelPeek = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const panels = new PanelsStore();
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
let { children } = $props();
|
let { children } = $props();
|
||||||
|
|
||||||
const showPanel = $derived($page.url.pathname !== '/');
|
const showPanel = $derived($page.url.pathname !== '/');
|
||||||
|
const albumView = $derived($page.url.pathname.startsWith('/albums/'));
|
||||||
|
|
||||||
// Personal stream: one subscription for the whole session, kept alive across
|
// Personal stream: one subscription for the whole session, kept alive across
|
||||||
// navigation inside the (app) group. The stream updates the album list
|
// navigation inside the (app) group. The stream updates the album list
|
||||||
@@ -23,7 +24,7 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<AppShell panelVisible={showPanel}>
|
<AppShell panelVisible={showPanel} panelsToggle={albumView}>
|
||||||
{#snippet rail()}
|
{#snippet rail()}
|
||||||
<AlbumRail />
|
<AlbumRail />
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
import { viewerReturn } from '$lib/stores/viewer-return';
|
import { viewerReturn } from '$lib/stores/viewer-return';
|
||||||
import { transfer } from '$lib/stores/transfer.svelte';
|
import { transfer } from '$lib/stores/transfer.svelte';
|
||||||
import { contextMenu, type ContextMenuItem } from '$lib/stores/context-menu.svelte';
|
import { contextMenu, type ContextMenuItem } from '$lib/stores/context-menu.svelte';
|
||||||
|
import { panels } from '$lib/stores/panels.svelte';
|
||||||
import { SSEClient, type SSEEvent } from '$lib/sse';
|
import { SSEClient, type SSEEvent } from '$lib/sse';
|
||||||
import type { Album, Photo } from '$lib/types';
|
import type { Album, Photo } from '$lib/types';
|
||||||
|
|
||||||
@@ -496,6 +497,18 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<DensityControl />
|
<DensityControl />
|
||||||
|
<span class="bg-border hidden h-5 w-px shrink-0 lg:block" aria-hidden="true"></span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="text-text-muted hover:bg-bg-muted hover:text-text hidden items-center justify-center rounded-md p-1.5 lg:inline-flex"
|
||||||
|
aria-label={panels.hidden ? 'Показать боковые панели (Tab)' : 'Скрыть боковые панели (Tab)'}
|
||||||
|
title={panels.hidden ? 'Показать боковые панели (Tab)' : 'Скрыть боковые панели (Tab)'}
|
||||||
|
aria-keyshortcuts="Tab"
|
||||||
|
aria-pressed={panels.hidden}
|
||||||
|
onclick={() => panels.toggle()}
|
||||||
|
>
|
||||||
|
<Icon name={panels.hidden ? 'left_panel_open' : 'left_panel_close'} />
|
||||||
|
</button>
|
||||||
{#if canUpload}
|
{#if canUpload}
|
||||||
<span class="bg-border h-5 w-px shrink-0" aria-hidden="true"></span>
|
<span class="bg-border h-5 w-px shrink-0" aria-hidden="true"></span>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -81,7 +81,9 @@ SHALL отображаться вне каркаса, но в общей тем
|
|||||||
Сетка SHALL давать пользователю управлять размером миниатюр: сдвиг регулятора вправо SHALL
|
Сетка SHALL давать пользователю управлять размером миниатюр: сдвиг регулятора вправо SHALL
|
||||||
увеличивать миниатюры, влево — уменьшать. Регулятор SHALL сопровождаться кнопками шага в
|
увеличивать миниатюры, влево — уменьшать. Регулятор SHALL сопровождаться кнопками шага в
|
||||||
сторону уменьшения и увеличения. Выбранный размер SHALL сохраняться между сессиями и SHALL
|
сторону уменьшения и увеличения. Выбранный размер SHALL сохраняться между сессиями и SHALL
|
||||||
быть общим для обзора альбомов и сетки фото альбома.
|
быть общим для обзора альбомов и сетки фото альбома. Клавиши `+` и `-` SHALL менять размер
|
||||||
|
миниатюр на шаг так же, как кнопки регулятора; при вводе текста и в открытых диалогах либо
|
||||||
|
меню эти клавиши SHALL сохранять обычное действие.
|
||||||
|
|
||||||
#### Scenario: Изменение размера
|
#### Scenario: Изменение размера
|
||||||
|
|
||||||
@@ -98,6 +100,16 @@ SHALL отображаться вне каркаса, но в общей тем
|
|||||||
- **WHEN** пользователь нажимает кнопку увеличения или уменьшения рядом с регулятором
|
- **WHEN** пользователь нажимает кнопку увеличения или уменьшения рядом с регулятором
|
||||||
- **THEN** размер миниатюр меняется на один шаг и сохраняется
|
- **THEN** размер миниатюр меняется на один шаг и сохраняется
|
||||||
|
|
||||||
|
#### Scenario: Клавиши размера
|
||||||
|
|
||||||
|
- **WHEN** пользователь нажимает `+` или `-`
|
||||||
|
- **THEN** размер миниатюр меняется на один шаг, как при нажатии кнопок регулятора
|
||||||
|
|
||||||
|
#### Scenario: Клавиши в поле ввода
|
||||||
|
|
||||||
|
- **WHEN** фокус находится в поле ввода или открыт диалог либо меню
|
||||||
|
- **THEN** `+` и `-` не меняют размер миниатюр
|
||||||
|
|
||||||
#### Scenario: Единый размер в обоих местах
|
#### Scenario: Единый размер в обоих местах
|
||||||
|
|
||||||
- **WHEN** пользователь меняет размер на обзоре альбомов или в сетке фото альбома
|
- **WHEN** пользователь меняет размер на обзоре альбомов или в сетке фото альбома
|
||||||
@@ -172,6 +184,34 @@ SHALL выдвигать правую панель поверх содержим
|
|||||||
- **WHEN** пользователь перезагружает приложение
|
- **WHEN** пользователь перезагружает приложение
|
||||||
- **THEN** восстановленные панели имеют сохранённую ширину
|
- **THEN** восстановленные панели имеют сохранённую ширину
|
||||||
|
|
||||||
|
### Requirement: Скрытие боковых панелей
|
||||||
|
В режиме просмотра альбома SHALL быть доступна кнопка скрытия левой и правой боковых панелей
|
||||||
|
в верхней панели сетки рядом с регулятором размера миниатюр и хоткей `Tab`. В скрытом режиме
|
||||||
|
панели SHALL не занимать место в раскладке, содержимое альбома SHALL занимать всю ширину,
|
||||||
|
а панели SHALL показываться при наведении указателя на левую или правую кромку окна и
|
||||||
|
скрываться после ухода указателя. Хоткей SHALL не срабатывать при вводе текста в поля и при
|
||||||
|
открытых модальных диалогах или меню.
|
||||||
|
|
||||||
|
#### Scenario: Переключение кнопкой
|
||||||
|
- **WHEN** пользователь нажимает кнопку скрытия боковых панелей в альбоме
|
||||||
|
- **THEN** обе панели уезжают за края, а сетка фото занимает всю ширину окна
|
||||||
|
|
||||||
|
#### Scenario: Переключение хоткеем
|
||||||
|
- **WHEN** пользователь нажимает `Tab` в режиме просмотра альбома
|
||||||
|
- **THEN** боковые панели скрываются или показываются
|
||||||
|
|
||||||
|
#### Scenario: Появление у кромки
|
||||||
|
- **WHEN** панели скрыты, и указатель подходит к левой или правой кромке окна
|
||||||
|
- **THEN** соответствующая панель выезжает поверх содержимого
|
||||||
|
|
||||||
|
#### Scenario: Скрытие после ухода указателя
|
||||||
|
- **WHEN** указатель покидает показанную у кромки панель
|
||||||
|
- **THEN** панель снова уезжает за край
|
||||||
|
|
||||||
|
#### Scenario: Поля ввода и диалоги
|
||||||
|
- **WHEN** фокус находится в поле ввода или открыт модальный диалог либо меню
|
||||||
|
- **THEN** `Tab` выполняет обычную навигацию и не переключает панели
|
||||||
|
|
||||||
### Requirement: Сворачиваемые разделы панели
|
### Requirement: Сворачиваемые разделы панели
|
||||||
Контекстная панель SHALL группировать содержимое в сворачиваемые разделы, а состояние
|
Контекстная панель SHALL группировать содержимое в сворачиваемые разделы, а состояние
|
||||||
сворачивания SHALL сохраняться; раздел «Информация» SHALL показывать метаданные фото,
|
сворачивания SHALL сохраняться; раздел «Информация» SHALL показывать метаданные фото,
|
||||||
|
|||||||
Reference in New Issue
Block a user