diff --git a/frontend/src/lib/components/disk/TrashPanel.svelte b/frontend/src/lib/components/disk/TrashPanel.svelte index ea049de..935f2af 100644 --- a/frontend/src/lib/components/disk/TrashPanel.svelte +++ b/frontend/src/lib/components/disk/TrashPanel.svelte @@ -498,6 +498,16 @@ outline: 2px solid var(--color-primary); outline-offset: 1px; } + + @media (hover: hover) { + &:hover { + background-color: var(--color-chat-hover); + } + } + + @media (pointer: coarse) { + min-height: 2.75rem; + } } .list { diff --git a/frontend/src/lib/components/policy/PolicyCard.svelte b/frontend/src/lib/components/policy/PolicyCard.svelte index a2bb6f6..7619458 100644 --- a/frontend/src/lib/components/policy/PolicyCard.svelte +++ b/frontend/src/lib/components/policy/PolicyCard.svelte @@ -66,6 +66,7 @@ let confirming = $state(false); let moving = $state(false); let moved = $state(null); + let removing = $state(false); const isChat = $derived(scopeType === "chat"); const isFolder = $derived(scopeType === "folder"); @@ -143,6 +144,7 @@ try { await deletePolicy(rule.id); rule = null; + removing = false; onchange?.(); } catch { problem = "Не удалось удалить правило"; @@ -344,9 +346,39 @@ {/if} {#if rule && (isChat || isFolder)} - + {#if removing} +
+ Удалить правило? + + +
+ {:else} + + {/if} {/if} {/if} @@ -404,7 +436,7 @@ &.on { color: var(--color-text); - background-color: var(--color-chat-hover); + background-color: var(--color-background); } &:focus-visible { @@ -413,7 +445,7 @@ } @media (pointer: coarse) { - height: 2.5rem; + height: 2.75rem; } } @@ -517,7 +549,9 @@ gap: 0.5rem; align-items: center; - margin: 0.5rem 0.75rem 0.25rem; + flex-wrap: wrap; + + margin: 0.5rem 1rem 0.25rem; padding: 0.625rem 0.5rem 0.625rem 0.875rem; border-radius: 0.75rem; @@ -531,7 +565,7 @@ .files-text { display: flex; - flex: 1; + flex: 1 1 10rem; flex-direction: column; gap: 0.125rem; @@ -557,6 +591,21 @@ margin-top: 0.375rem; } + .removal { + display: flex; + gap: 0.25rem; + align-items: center; + + margin: 0.25rem 0.5rem 0; + padding-left: 0.5rem; + + font-size: 0.875rem; + + span { + flex: 1; + } + } + .remove { cursor: pointer; @@ -573,6 +622,10 @@ background: none; + @media (pointer: coarse) { + min-height: 2.75rem; + } + &:focus-visible { outline: 2px solid var(--color-error); outline-offset: -2px; diff --git a/frontend/src/lib/components/policy/PolicyEditor.svelte b/frontend/src/lib/components/policy/PolicyEditor.svelte index d8830ce..fcf17ce 100644 --- a/frontend/src/lib/components/policy/PolicyEditor.svelte +++ b/frontend/src/lib/components/policy/PolicyEditor.svelte @@ -5,12 +5,7 @@ getPolicyOverview, listFolders, } from "#lib/api/endpoints.js"; - import type { - CaptureToggles, - Folder, - PolicyRule, - PolicyScopeType, - } from "#lib/api/types.js"; + import type { Folder, PolicyRule, PolicyScopeType } from "#lib/api/types.js"; import PolicyCard from "#lib/components/policy/PolicyCard.svelte"; import Avatar from "#lib/components/ui/Avatar.svelte"; import Button from "#lib/components/ui/Button.svelte"; @@ -18,6 +13,7 @@ import Spinner from "#lib/components/ui/Spinner.svelte"; import { type Chip, + contentChips, DEFAULT_LABELS, diffChips, sameToggles, @@ -60,7 +56,6 @@ rules.find((rule) => rule.scope_type === scope) ).filter((rule): rule is PolicyRule => rule !== undefined) ); - const defaultBases = $derived(defaults); const folderRules = $derived( rules .filter((rule) => rule.scope_type === "folder") @@ -106,9 +101,31 @@ } function chips(rule: PolicyRule): Chip[] { - return diffChips(rule, rule.parent ? [rule.parent.toggles] : defaultBases); + return rule.parent + ? diffChips(rule, rule.parent.toggles) + : contentChips(rule); } + function reveal(node: HTMLElement) { + requestAnimationFrame(() => { + node.parentElement?.scrollIntoView({ block: "nearest" }); + }); + } + + function draftBefore(rule: PolicyRule): boolean { + if (draft?.scopeType !== "folder") { + return false; + } + const slot = folderOrder(draft.scopeId); + const next = folderRules.find((item) => folderOrder(item.scope_id) > slot); + return next?.id === rule.id; + } + + const draftLast = $derived( + draft?.scopeType === "folder" && + !folderRules.some((rule) => draftBefore(rule)) + ); + async function reload() { try { [rules, folders] = await Promise.all([ @@ -212,7 +229,7 @@ {:else} {#each chips(rule) as chip (chip.sign + chip.label)} - + {chip.sign === "plus" ? "+" : "−"} {chip.label} @@ -220,9 +237,7 @@ {/if} - {#if !extra} - - {/if} + {#if extra} -
+

Папки

- что меняют · побеждает первая подходящая + Что сохраняют. Из подходящих папок побеждает первая.
- {#if draft?.scopeType === "folder"} - {@render pending(draft)} - {/if} {#each folderRules as rule (rule.id)} + {#if draft && draftBefore(rule)} + {@render pending(draft)} + {/if} {@render row(rule, false)} {/each} + {#if draft && draftLast} + {@render pending(draft)} + {/if} {#if freeFolders.length > 0} + {:else} +

Ничего не найдено.

{/each}
{/if} @@ -418,13 +446,13 @@ .section-title { display: flex; - gap: 0.75rem; - align-items: baseline; - justify-content: space-between; + flex-direction: column; + gap: 0.125rem; padding: 0.875rem 1rem 0.375rem; font-size: 0.8125rem; + line-height: 1.35; color: var(--color-text-secondary); h3 { @@ -433,17 +461,14 @@ font-weight: var(--font-weight-medium); color: var(--color-primary); } - - span { - overflow: hidden; - text-align: end; - text-overflow: ellipsis; - white-space: nowrap; - } } .rule.open { - background-color: var(--color-chat-hover); + border-bottom: 1px solid var(--color-borders); + + .line { + background-color: var(--color-chat-hover); + } } .line { @@ -483,6 +508,12 @@ outline: 2px solid var(--color-primary); outline-offset: -2px; } + + @media (hover: hover) { + &:not(.static):hover { + background-color: var(--color-chat-hover); + } + } } .glyph { @@ -631,5 +662,16 @@ background-color: var(--color-chat-hover); } } + + @media (pointer: coarse) { + min-height: 2.75rem; + } + } + + .nothing { + margin: 0; + padding: 0.5rem 0.75rem; + font-size: 0.875rem; + color: var(--color-text-secondary); } diff --git a/frontend/src/lib/format/policy.ts b/frontend/src/lib/format/policy.ts index 868747b..2d3a2ca 100644 --- a/frontend/src/lib/format/policy.ts +++ b/frontend/src/lib/format/policy.ts @@ -45,6 +45,7 @@ const SHOWN_CHIPS = 3; const SHORT_LIST = 2; export interface Chip { + hint?: string; label: string; sign: "plus" | "minus"; } @@ -76,6 +77,19 @@ export function sameToggles(a: CaptureToggles, b: CaptureToggles): boolean { return TOGGLE_KEYS.every((key) => a[key] === b[key]); } +const EXCEPT_LABELS: Record = { + backfill: "бэкфилла", + media: "медиа", + messages: "сообщений", + presence: "присутствия", + profile_history: "истории профиля", + reactions: "реакций", + self_destruct_media: "исчезающих", + stories: "сторис", + stt: "расшифровки", + track_edits_deletes: "правок и удалений", +}; + export function summary(toggles: CaptureToggles): string { const on = TOGGLE_KEYS.filter((key) => toggles[key]); const off = TOGGLE_KEYS.filter((key) => !toggles[key]); @@ -89,8 +103,7 @@ export function summary(toggles: CaptureToggles): string { return "Только сообщения"; } if (off.length <= SHORT_LIST) { - const names = off.map((key) => TOGGLE_LABELS[key].toLowerCase()); - return `Всё, кроме: ${names.join(", ")}`; + return `Всё, кроме ${off.map((key) => EXCEPT_LABELS[key]).join(" и ")}`; } const names = on.slice(0, SHORT_LIST).map((key) => TOGGLE_LABELS[key]); const rest = on.length - names.length; @@ -107,42 +120,48 @@ export function parentLabel( return PARENT_LABELS[scopeType] ?? "по умолчанию"; } -function settings(count: number): string { - return `${count} ${plural(count, "настройка", "настройки", "настроек")}`; +function labelList(keys: ToggleKey[]): string { + return keys.map((key) => TOGGLE_LABELS[key]).join(", "); } -function collapse( - keys: ToggleKey[], - sign: Chip["sign"], - whole: string -): Chip[] { - if (keys.length === 0) { - return []; - } - if (keys.length > SHOWN_CHIPS) { - return [{ label: whole, sign }]; - } +function named(keys: ToggleKey[], sign: Chip["sign"]): Chip[] { return keys.map((key) => ({ label: TOGGLE_LABELS[key], sign })); } -export function diffChips( - rule: CaptureToggles, - bases: CaptureToggles[] -): Chip[] { - const plus = TOGGLE_KEYS.filter( - (key) => rule[key] && bases.some((base) => !base[key]) - ); - const minus = TOGGLE_KEYS.filter( - (key) => !rule[key] && bases.some((base) => base[key]) - ); - const allOn = TOGGLE_KEYS.every((key) => rule[key]); - const allOff = TOGGLE_KEYS.every((key) => !rule[key]); - return [ - ...collapse(plus, "plus", allOn ? "Всё включено" : settings(plus.length)), - ...collapse( - minus, - "minus", - allOff ? "Всё выключено" : settings(minus.length) - ), - ]; +function counted(keys: ToggleKey[], sign: Chip["sign"]): Chip[] { + if (keys.length === 0) { + return []; + } + if (keys.length <= SHOWN_CHIPS) { + return named(keys, sign); + } + const word = plural(keys.length, "настройка", "настройки", "настроек"); + return [{ hint: labelList(keys), label: `${keys.length} ${word}`, sign }]; +} + +export function contentChips(rule: CaptureToggles): Chip[] { + const on = TOGGLE_KEYS.filter((key) => rule[key]); + const off = TOGGLE_KEYS.filter((key) => !rule[key]); + if (off.length === 0) { + return [{ label: "Всё включено", sign: "plus" }]; + } + if (on.length === 0) { + return [{ label: "Всё выключено", sign: "minus" }]; + } + if (off.length <= SHOWN_CHIPS) { + return [{ label: "Всё", sign: "plus" }, ...named(off, "minus")]; + } + return [...counted(on, "plus"), ...counted(off, "minus")]; +} + +export function diffChips(rule: CaptureToggles, base: CaptureToggles): Chip[] { + const plus = TOGGLE_KEYS.filter((key) => rule[key] && !base[key]); + const minus = TOGGLE_KEYS.filter((key) => !rule[key] && base[key]); + if (plus.length > SHOWN_CHIPS && TOGGLE_KEYS.every((key) => rule[key])) { + return [{ label: "Всё включено", sign: "plus" }]; + } + if (minus.length > SHOWN_CHIPS && TOGGLE_KEYS.every((key) => !rule[key])) { + return [{ label: "Всё выключено", sign: "minus" }]; + } + return [...counted(plus, "plus"), ...counted(minus, "minus")]; }