feat(frontend): capture policy shows what each rule changes, rule card opens from folder and chat menus and can trash downloaded files, trash is paged
This commit is contained in:
1 parent
f646f29ff9
commit
cb01d7dff8
16 files changed
+1536
-369
No files matched your search
@@ -1,241 +1,333 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import {
|
||||
createPolicy,
|
||||
deletePolicy,
|
||||
getPolicyOverview,
|
||||
listFolders,
|
||||
listPolicies,
|
||||
updatePolicy,
|
||||
} from "#lib/api/endpoints.js";
|
||||
import type {
|
||||
CaptureToggles,
|
||||
Folder,
|
||||
PolicyRecord,
|
||||
PolicyRule,
|
||||
PolicyScopeType,
|
||||
} from "#lib/api/types.js";
|
||||
import CaptureToggleList from "#lib/components/policy/CaptureToggleList.svelte";
|
||||
import PolicyCard from "#lib/components/policy/PolicyCard.svelte";
|
||||
import Avatar from "#lib/components/ui/Avatar.svelte";
|
||||
import Button from "#lib/components/ui/Button.svelte";
|
||||
import Icon from "#lib/components/ui/Icon.svelte";
|
||||
import Spinner from "#lib/components/ui/Spinner.svelte";
|
||||
import { accounts } from "#lib/stores/accounts.svelte.js";
|
||||
import {
|
||||
type Chip,
|
||||
DEFAULT_LABELS,
|
||||
diffChips,
|
||||
sameToggles,
|
||||
summary,
|
||||
} from "#lib/format/policy.js";
|
||||
import { createChatPicker } from "#lib/stores/chat-picker.svelte.js";
|
||||
import { chats } from "#lib/stores/chats.svelte.js";
|
||||
import { toasts } from "#lib/stores/toasts.svelte.js";
|
||||
|
||||
let policies = $state<PolicyRecord[]>([]);
|
||||
let folders = $state<Folder[]>([]);
|
||||
let loading = $state(true);
|
||||
let pickFolder = $state(false);
|
||||
let pickChat = $state(false);
|
||||
let chatFilter = $state("");
|
||||
interface Draft {
|
||||
scopeId: number;
|
||||
scopeType: "folder" | "chat";
|
||||
title: string;
|
||||
}
|
||||
|
||||
const DEFAULT_SCOPES: { label: string; scope: PolicyScopeType }[] = [
|
||||
{ label: "Личные чаты", scope: "default_dm" },
|
||||
{ label: "Группы", scope: "default_group" },
|
||||
{ label: "Каналы", scope: "default_channel" },
|
||||
const DEFAULT_ORDER: PolicyScopeType[] = [
|
||||
"default_dm",
|
||||
"default_group",
|
||||
"default_channel",
|
||||
];
|
||||
|
||||
const ALL_FALSE: CaptureToggles = {
|
||||
backfill: false,
|
||||
media: false,
|
||||
messages: false,
|
||||
presence: false,
|
||||
profile_history: false,
|
||||
reactions: false,
|
||||
self_destruct_media: false,
|
||||
stories: false,
|
||||
stt: false,
|
||||
track_edits_deletes: false,
|
||||
const DEFAULT_ICONS: Partial<Record<PolicyScopeType, string>> = {
|
||||
default_channel: "channel",
|
||||
default_dm: "user",
|
||||
default_group: "group",
|
||||
};
|
||||
|
||||
function pickToggles(t: CaptureToggles): CaptureToggles {
|
||||
return {
|
||||
backfill: t.backfill,
|
||||
media: t.media,
|
||||
messages: t.messages,
|
||||
presence: t.presence,
|
||||
profile_history: t.profile_history,
|
||||
reactions: t.reactions,
|
||||
self_destruct_media: t.self_destruct_media,
|
||||
stories: t.stories,
|
||||
stt: t.stt,
|
||||
track_edits_deletes: t.track_edits_deletes,
|
||||
};
|
||||
}
|
||||
let rules = $state<PolicyRule[]>([]);
|
||||
let folders = $state<Folder[]>([]);
|
||||
let loading = $state(true);
|
||||
let failed = $state(false);
|
||||
let problem = $state<string | null>(null);
|
||||
let expanded = $state<string | null>(null);
|
||||
let draft = $state<Draft | null>(null);
|
||||
let picking = $state<"folder" | "chat" | null>(null);
|
||||
let chatFilter = $state("");
|
||||
|
||||
const picker = createChatPicker();
|
||||
|
||||
const defaults = $derived(
|
||||
DEFAULT_SCOPES.map((d) => ({
|
||||
label: d.label,
|
||||
record: policies.find((p) => p.scope_type === d.scope),
|
||||
})).filter((d): d is { label: string; record: PolicyRecord } =>
|
||||
Boolean(d.record)
|
||||
)
|
||||
DEFAULT_ORDER.map((scope) =>
|
||||
rules.find((rule) => rule.scope_type === scope)
|
||||
).filter((rule): rule is PolicyRule => rule !== undefined)
|
||||
);
|
||||
const folderPolicies = $derived(
|
||||
policies.filter((p) => p.scope_type === "folder")
|
||||
const defaultBases = $derived<CaptureToggles[]>(defaults);
|
||||
const folderRules = $derived(
|
||||
rules
|
||||
.filter((rule) => rule.scope_type === "folder")
|
||||
.toSorted((a, b) => folderOrder(a.scope_id) - folderOrder(b.scope_id))
|
||||
);
|
||||
const chatPolicies = $derived(
|
||||
policies.filter((p) => p.scope_type === "chat")
|
||||
const chatRules = $derived(
|
||||
rules
|
||||
.filter((rule) => rule.scope_type === "chat")
|
||||
.toSorted((a, b) => title(a).localeCompare(title(b), "ru"))
|
||||
);
|
||||
const availableFolders = $derived(
|
||||
const freeFolders = $derived(
|
||||
folders.filter(
|
||||
(f) => !folderPolicies.some((p) => p.scope_id === f.folder_id)
|
||||
(folder) =>
|
||||
!folderRules.some((rule) => rule.scope_id === folder.folder_id)
|
||||
)
|
||||
);
|
||||
const picker = createChatPicker();
|
||||
const availableChats = $derived(
|
||||
const freeChats = $derived(
|
||||
picker.results.filter(
|
||||
(c) => !chatPolicies.some((p) => p.scope_id === c.chat_id)
|
||||
(chat) => !chatRules.some((rule) => rule.scope_id === chat.chat_id)
|
||||
)
|
||||
);
|
||||
|
||||
$effect(() => {
|
||||
picker.search(chatFilter);
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
chats.ensureMany(
|
||||
chatPolicies
|
||||
.map((policy) => policy.scope_id)
|
||||
.filter((id): id is number => id !== null)
|
||||
);
|
||||
});
|
||||
|
||||
function folderTitle(id: number | null): string {
|
||||
return folders.find((f) => f.folder_id === id)?.title ?? `Папка ${id}`;
|
||||
function folderOrder(id: number | null): number {
|
||||
return folders.find((folder) => folder.folder_id === id)?.order_index ?? 0;
|
||||
}
|
||||
|
||||
function chatTitle(id: number | null): string {
|
||||
return chats.byId(id ?? 0)?.title ?? `Чат ${id}`;
|
||||
function keyOf(scopeType: PolicyScopeType, scopeId: number | null): string {
|
||||
return `${scopeType}:${scopeId}`;
|
||||
}
|
||||
|
||||
function title(rule: PolicyRule): string {
|
||||
if (rule.scope_type === "chat") {
|
||||
return rule.title ?? `Чат ${rule.scope_id}`;
|
||||
}
|
||||
if (rule.scope_type === "folder") {
|
||||
return rule.title ?? `Папка ${rule.scope_id}`;
|
||||
}
|
||||
return DEFAULT_LABELS[rule.scope_type] ?? "";
|
||||
}
|
||||
|
||||
function redundant(rule: PolicyRule): boolean {
|
||||
return rule.parent !== null && sameToggles(rule, rule.parent.toggles);
|
||||
}
|
||||
|
||||
function chips(rule: PolicyRule): Chip[] {
|
||||
return diffChips(rule, rule.parent ? [rule.parent.toggles] : defaultBases);
|
||||
}
|
||||
|
||||
async function reload() {
|
||||
[policies, folders] = await Promise.all([listPolicies(), listFolders()]);
|
||||
}
|
||||
|
||||
async function toggle(
|
||||
record: PolicyRecord,
|
||||
key: keyof CaptureToggles,
|
||||
value: boolean
|
||||
) {
|
||||
const next = { ...pickToggles(record), [key]: value };
|
||||
policies = policies.map((p) =>
|
||||
p.id === record.id ? { ...p, [key]: value } : p
|
||||
);
|
||||
try {
|
||||
const saved = await updatePolicy(record.id, next);
|
||||
policies = policies.map((p) => (p.id === record.id ? saved : p));
|
||||
[rules, folders] = await Promise.all([
|
||||
getPolicyOverview(),
|
||||
listFolders(),
|
||||
]);
|
||||
failed = false;
|
||||
if (
|
||||
draft &&
|
||||
rules.some(
|
||||
(rule) =>
|
||||
rule.scope_type === draft?.scopeType &&
|
||||
rule.scope_id === draft?.scopeId
|
||||
)
|
||||
) {
|
||||
expanded = keyOf(draft.scopeType, draft.scopeId);
|
||||
draft = null;
|
||||
}
|
||||
} catch {
|
||||
toasts.error("Не удалось сохранить политику");
|
||||
await reload();
|
||||
}
|
||||
}
|
||||
|
||||
async function addOverride(scope: "folder" | "chat", scopeId: number) {
|
||||
try {
|
||||
const record = await createPolicy({
|
||||
...ALL_FALSE,
|
||||
scope_id: scopeId,
|
||||
scope_type: scope,
|
||||
});
|
||||
policies = [...policies, record];
|
||||
pickFolder = false;
|
||||
pickChat = false;
|
||||
chatFilter = "";
|
||||
} catch {
|
||||
toasts.error("Не удалось создать оверрайд");
|
||||
}
|
||||
}
|
||||
|
||||
async function removeOverride(record: PolicyRecord) {
|
||||
policies = policies.filter((p) => p.id !== record.id);
|
||||
try {
|
||||
await deletePolicy(record.id);
|
||||
} catch {
|
||||
toasts.error("Не удалось удалить оверрайд");
|
||||
await reload();
|
||||
}
|
||||
}
|
||||
|
||||
async function load(_account: number | null) {
|
||||
loading = true;
|
||||
chats.load();
|
||||
try {
|
||||
await reload();
|
||||
} catch {
|
||||
toasts.error("Не удалось загрузить политики");
|
||||
failed = true;
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
load(accounts.selectedId);
|
||||
function toggle(rule: PolicyRule) {
|
||||
const key = keyOf(rule.scope_type, rule.scope_id);
|
||||
expanded = expanded === key ? null : key;
|
||||
}
|
||||
|
||||
async function drop(rule: PolicyRule) {
|
||||
problem = null;
|
||||
try {
|
||||
await deletePolicy(rule.id);
|
||||
await reload();
|
||||
} catch {
|
||||
problem = "Не удалось убрать правило";
|
||||
}
|
||||
}
|
||||
|
||||
function start(scopeType: "folder" | "chat", scopeId: number, name: string) {
|
||||
draft = { scopeId, scopeType, title: name };
|
||||
picking = null;
|
||||
chatFilter = "";
|
||||
expanded = null;
|
||||
}
|
||||
|
||||
function togglePicker(kind: "folder" | "chat") {
|
||||
picking = picking === kind ? null : kind;
|
||||
if (picking === "chat") {
|
||||
picker.search(chatFilter);
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
chats.load();
|
||||
reload();
|
||||
});
|
||||
</script>
|
||||
|
||||
{#snippet card(
|
||||
record: PolicyRecord,
|
||||
title: string,
|
||||
onremove?: () => void
|
||||
{#snippet lead(
|
||||
rule: PolicyRule
|
||||
)}
|
||||
<div class="card">
|
||||
<div class="card-head">
|
||||
<span class="card-title">{title}</span>
|
||||
{#if record.account_id === null}
|
||||
<span class="shared">для всех аккаунтов</span>
|
||||
{/if}
|
||||
{#if onremove && record.account_id !== null}
|
||||
<button
|
||||
aria-label="Удалить оверрайд"
|
||||
class="remove"
|
||||
onclick={onremove}
|
||||
type="button"
|
||||
>
|
||||
<Icon name="close" size="1.125rem" />
|
||||
</button>
|
||||
{#if rule.scope_type === "chat" && rule.scope_id !== null}
|
||||
<Avatar
|
||||
avatar={{ id: rule.scope_id, kind: rule.scope_id > 0 ? "peer" : "chat" }}
|
||||
colorKey={rule.scope_id}
|
||||
hasAvatar={chats.byId(rule.scope_id)?.has_avatar ?? false}
|
||||
name={title(rule)}
|
||||
size={2.25}
|
||||
/>
|
||||
{:else}
|
||||
<span class="glyph">
|
||||
<Icon name={DEFAULT_ICONS[rule.scope_type] ?? "folder"} size="1.5rem" />
|
||||
</span>
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
{#snippet row(
|
||||
rule: PolicyRule,
|
||||
plain: boolean
|
||||
)}
|
||||
{@const key = keyOf(rule.scope_type, rule.scope_id)}
|
||||
{@const open = expanded === key}
|
||||
{@const extra = !plain && redundant(rule)}
|
||||
<div class="rule" class:open>
|
||||
<div class="line">
|
||||
<button
|
||||
aria-expanded={open}
|
||||
class="main"
|
||||
onclick={() => toggle(rule)}
|
||||
type="button"
|
||||
>
|
||||
{@render lead(rule)}
|
||||
<span class="text">
|
||||
<span class="name">{title(rule)}</span>
|
||||
{#if plain}
|
||||
<span class="sub">{summary(rule)}</span>
|
||||
{:else if extra}
|
||||
<span class="sub">Ничего не меняет</span>
|
||||
{:else}
|
||||
<span class="chips">
|
||||
{#each chips(rule) as chip (chip.sign + chip.label)}
|
||||
<span class="chip {chip.sign}">
|
||||
{chip.sign === "plus" ? "+" : "−"}
|
||||
{chip.label}
|
||||
</span>
|
||||
{/each}
|
||||
</span>
|
||||
{/if}
|
||||
</span>
|
||||
{#if !extra}
|
||||
<Icon name={open ? "up" : "down"} size="1.125rem" />
|
||||
{/if}
|
||||
</button>
|
||||
{#if extra}
|
||||
<Button onclick={() => drop(rule)} smaller variant="text">
|
||||
Убрать
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
<CaptureToggleList
|
||||
onchange={(key, value) => toggle(record, key, value)}
|
||||
toggles={record}
|
||||
/>
|
||||
{#if open}
|
||||
<div class="editor">
|
||||
<PolicyCard
|
||||
onchange={reload}
|
||||
parent={rule.parent}
|
||||
{rule}
|
||||
scopeId={rule.scope_id}
|
||||
scopeType={rule.scope_type}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
{#snippet pending(
|
||||
item: Draft
|
||||
)}
|
||||
<div class="rule open">
|
||||
<div class="line">
|
||||
<div class="main static">
|
||||
<span class="glyph">
|
||||
<Icon
|
||||
name={item.scopeType === "folder" ? "folder" : "user"}
|
||||
size="1.5rem"
|
||||
/>
|
||||
</span>
|
||||
<span class="text">
|
||||
<span class="name">{item.title}</span>
|
||||
<span class="sub">Новое правило</span>
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
onclick={() => {
|
||||
draft = null;
|
||||
}}
|
||||
smaller
|
||||
variant="text"
|
||||
>
|
||||
Отмена
|
||||
</Button>
|
||||
</div>
|
||||
<div class="editor">
|
||||
<PolicyCard
|
||||
onchange={reload}
|
||||
scopeId={item.scopeId}
|
||||
scopeType={item.scopeType}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
{#if loading}
|
||||
<div class="state"><Spinner /></div>
|
||||
{:else if failed}
|
||||
<div class="state">
|
||||
<p>Не удалось загрузить политику.</p>
|
||||
<Button onclick={reload} smaller variant="text">Повторить</Button>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="editor">
|
||||
<div class="policy">
|
||||
{#if problem}
|
||||
<p class="problem">{problem}</p>
|
||||
{/if}
|
||||
|
||||
<section>
|
||||
<div class="section-title">По умолчанию</div>
|
||||
{#each defaults as item (item.record.id)}
|
||||
{@render card(item.record, item.label)}
|
||||
<div class="section-title"><h3>По умолчанию</h3></div>
|
||||
{#each defaults as rule (rule.id)}
|
||||
{@render row(rule, true)}
|
||||
{/each}
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="section-title">Оверрайды папок</div>
|
||||
{#each folderPolicies as record (record.id)}
|
||||
{@render card(record, folderTitle(record.scope_id), () =>
|
||||
removeOverride(record)
|
||||
)}
|
||||
<div class="section-title">
|
||||
<h3>Папки</h3>
|
||||
<span>что меняют · побеждает первая подходящая</span>
|
||||
</div>
|
||||
{#if draft?.scopeType === "folder"}
|
||||
{@render pending(draft)}
|
||||
{/if}
|
||||
{#each folderRules as rule (rule.id)}
|
||||
{@render row(rule, false)}
|
||||
{/each}
|
||||
{#if availableFolders.length > 0}
|
||||
{#if freeFolders.length > 0}
|
||||
<button
|
||||
aria-expanded={picking === "folder"}
|
||||
class="add"
|
||||
onclick={() => {
|
||||
pickFolder = !pickFolder;
|
||||
}}
|
||||
onclick={() => togglePicker("folder")}
|
||||
type="button"
|
||||
>
|
||||
<Icon name="add" size="1.25rem" />
|
||||
<span>Добавить папку</span>
|
||||
<span>Правило для папки</span>
|
||||
</button>
|
||||
{#if pickFolder}
|
||||
{#if picking === "folder"}
|
||||
<div class="picker">
|
||||
{#each availableFolders as folder (folder.folder_id)}
|
||||
{#each freeFolders as folder (folder.folder_id)}
|
||||
<button
|
||||
class="pick-row"
|
||||
onclick={() => addOverride("folder", folder.folder_id)}
|
||||
class="pick"
|
||||
onclick={() => start("folder", folder.folder_id, folder.title)}
|
||||
type="button"
|
||||
>
|
||||
{folder.title}
|
||||
@@ -247,34 +339,44 @@
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="section-title">Оверрайды чатов</div>
|
||||
{#each chatPolicies as record (record.id)}
|
||||
{@render card(record, chatTitle(record.scope_id), () =>
|
||||
removeOverride(record)
|
||||
)}
|
||||
<div class="section-title">
|
||||
<h3>Чаты</h3>
|
||||
<span>что меняют · важнее папок</span>
|
||||
</div>
|
||||
{#if draft?.scopeType === "chat"}
|
||||
{@render pending(draft)}
|
||||
{/if}
|
||||
{#each chatRules as rule (rule.id)}
|
||||
{@render row(rule, false)}
|
||||
{/each}
|
||||
<button
|
||||
aria-expanded={picking === "chat"}
|
||||
class="add"
|
||||
onclick={() => {
|
||||
pickChat = !pickChat;
|
||||
}}
|
||||
onclick={() => togglePicker("chat")}
|
||||
type="button"
|
||||
>
|
||||
<Icon name="add" size="1.25rem" />
|
||||
<span>Добавить чат</span>
|
||||
<span>Правило для чата</span>
|
||||
</button>
|
||||
{#if pickChat}
|
||||
{#if picking === "chat"}
|
||||
<div class="picker">
|
||||
<input
|
||||
aria-label="Поиск чата"
|
||||
class="filter"
|
||||
oninput={() => picker.search(chatFilter)}
|
||||
placeholder="Поиск чата"
|
||||
type="text"
|
||||
bind:value={chatFilter}
|
||||
>
|
||||
{#each availableChats as chat (chat.chat_id)}
|
||||
{#each freeChats as chat (chat.chat_id)}
|
||||
<button
|
||||
class="pick-row"
|
||||
onclick={() => addOverride("chat", chat.chat_id)}
|
||||
class="pick"
|
||||
onclick={() =>
|
||||
start(
|
||||
"chat",
|
||||
chat.chat_id,
|
||||
chat.title ?? `Чат ${chat.chat_id}`
|
||||
)}
|
||||
type="button"
|
||||
>
|
||||
{chat.title ?? `Чат ${chat.chat_id}`}
|
||||
@@ -289,79 +391,190 @@
|
||||
<style lang="scss">
|
||||
.state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2rem;
|
||||
|
||||
padding: 2rem 1rem;
|
||||
|
||||
font-size: 0.9375rem;
|
||||
color: var(--color-text-secondary);
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.editor {
|
||||
.policy {
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
section {
|
||||
padding: 0.5rem 0;
|
||||
border-bottom: 1px solid var(--color-borders);
|
||||
.problem {
|
||||
margin: 0;
|
||||
padding: 0.75rem 1rem 0;
|
||||
font-size: 0.8125rem;
|
||||
color: var(--color-error);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
padding: 0.5rem 1rem;
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
|
||||
padding: 0.875rem 1rem 0.375rem;
|
||||
|
||||
font-size: 0.8125rem;
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: var(--color-primary);
|
||||
color: var(--color-text-secondary);
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: inherit;
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
span {
|
||||
overflow: hidden;
|
||||
text-align: end;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 0.25rem 0 0.5rem;
|
||||
.rule.open {
|
||||
background-color: var(--color-chat-hover);
|
||||
}
|
||||
|
||||
.card-head {
|
||||
.line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.25rem 1rem;
|
||||
padding-right: 0.5rem;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
flex: 1;
|
||||
font-size: 0.9375rem;
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
.shared {
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.remove {
|
||||
.main {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
padding: 0.25rem;
|
||||
border: 0;
|
||||
border-radius: 50%;
|
||||
color: var(--color-text-secondary);
|
||||
background-color: transparent;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-chat-hover);
|
||||
display: flex;
|
||||
flex: 1;
|
||||
gap: 0.75rem;
|
||||
align-items: center;
|
||||
|
||||
min-width: 0;
|
||||
min-height: 3.25rem;
|
||||
padding: 0.4375rem 0.5rem 0.4375rem 1rem;
|
||||
border: 0;
|
||||
|
||||
font: inherit;
|
||||
color: var(--color-text);
|
||||
text-align: start;
|
||||
|
||||
background: none;
|
||||
|
||||
:global(.icon) {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
&.static {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--color-primary);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
}
|
||||
|
||||
.glyph {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
width: 2.25rem;
|
||||
height: 2.25rem;
|
||||
}
|
||||
|
||||
.text {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 0.1875rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.name {
|
||||
overflow: hidden;
|
||||
font-size: 0.9375rem;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.sub {
|
||||
font-size: 0.8125rem;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
height: 1.375rem;
|
||||
padding: 0 0.5rem;
|
||||
border-radius: 0.6875rem;
|
||||
|
||||
font-size: 0.75rem;
|
||||
white-space: nowrap;
|
||||
|
||||
&.plus {
|
||||
color: var(--color-policy-plus);
|
||||
background-color: var(--color-policy-plus-fill);
|
||||
}
|
||||
|
||||
&.minus {
|
||||
color: var(--color-policy-minus);
|
||||
background-color: var(--color-policy-minus-fill);
|
||||
}
|
||||
}
|
||||
|
||||
.editor {
|
||||
padding-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.add {
|
||||
cursor: pointer;
|
||||
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
|
||||
width: 100%;
|
||||
padding: 0.625rem 1rem;
|
||||
height: 2.75rem;
|
||||
padding: 0 1rem 0 1.5rem;
|
||||
border: 0;
|
||||
|
||||
font: inherit;
|
||||
font-size: 0.9375rem;
|
||||
color: var(--color-primary);
|
||||
background-color: transparent;
|
||||
text-align: start;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-chat-hover);
|
||||
background: none;
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--color-primary);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
&:hover {
|
||||
background-color: var(--color-chat-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -375,10 +588,12 @@
|
||||
margin: 0.25rem 0.5rem 0.5rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 1px solid var(--color-borders);
|
||||
border-radius: var(--border-radius-default);
|
||||
border-radius: var(--border-radius-default-small);
|
||||
|
||||
font: inherit;
|
||||
font-size: 0.9375rem;
|
||||
color: var(--color-text);
|
||||
|
||||
background-color: var(--color-background);
|
||||
|
||||
&:focus {
|
||||
@@ -387,8 +602,9 @@
|
||||
}
|
||||
}
|
||||
|
||||
.pick-row {
|
||||
.pick {
|
||||
cursor: pointer;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
width: 100%;
|
||||
@@ -396,15 +612,24 @@
|
||||
border: 0;
|
||||
border-radius: 0.5rem;
|
||||
|
||||
font: inherit;
|
||||
font-size: 0.9375rem;
|
||||
color: var(--color-text);
|
||||
text-align: start;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: var(--color-text);
|
||||
background-color: transparent;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-chat-hover);
|
||||
background: none;
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--color-primary);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
&:hover {
|
||||
background-color: var(--color-chat-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user