mirror of
https://github.com/priyanshujain/margin.git
synced 2026-10-02 19:17:03 +00:00
feat(nav): peel overlays with esc, close the book on a second esc
This commit is contained in:
1 parent
2e15dc2dc9
commit
fe39274ed7
12 files changed
+130
-59
No files matched your search
@@ -1,5 +1,6 @@
|
|||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { PAGE_TYPES } from "../model/book";
|
import { PAGE_TYPES } from "../model/book";
|
||||||
|
import { useEscapeLayer } from "../escape";
|
||||||
import { Icon } from "./Icon";
|
import { Icon } from "./Icon";
|
||||||
|
|
||||||
interface AddPageMenuProps {
|
interface AddPageMenuProps {
|
||||||
@@ -44,12 +45,13 @@ export function AddPageMenu({ onAdd, onAddPart }: AddPageMenuProps) {
|
|||||||
popRef.current?.querySelector<HTMLElement>(".add-page-item")?.focus();
|
popRef.current?.querySelector<HTMLElement>(".add-page-item")?.focus();
|
||||||
}, [open]);
|
}, [open]);
|
||||||
|
|
||||||
|
useEscapeLayer(open, () => {
|
||||||
|
setOpen(false);
|
||||||
|
btnRef.current?.focus();
|
||||||
|
});
|
||||||
|
|
||||||
const onMenuKeyDown = (e: React.KeyboardEvent) => {
|
const onMenuKeyDown = (e: React.KeyboardEvent) => {
|
||||||
if (e.key === "Escape") {
|
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
|
||||||
e.preventDefault();
|
|
||||||
setOpen(false);
|
|
||||||
btnRef.current?.focus();
|
|
||||||
} else if (e.key === "ArrowDown" || e.key === "ArrowUp") {
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const items = Array.from(popRef.current?.querySelectorAll<HTMLElement>(".add-page-item") ?? []);
|
const items = Array.from(popRef.current?.querySelectorAll<HTMLElement>(".add-page-item") ?? []);
|
||||||
const idx = items.indexOf(document.activeElement as HTMLElement);
|
const idx = items.indexOf(document.activeElement as HTMLElement);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useEffect, useState } from "react";
|
|||||||
import { Icon } from "./Icon";
|
import { Icon } from "./Icon";
|
||||||
import { ConfirmDialog } from "./ConfirmDialog";
|
import { ConfirmDialog } from "./ConfirmDialog";
|
||||||
import { useBackup } from "../store/useBackup";
|
import { useBackup } from "../store/useBackup";
|
||||||
|
import { useEscapeLayer } from "../escape";
|
||||||
import { formatBackupTime, gdriveListBackups } from "../backup";
|
import { formatBackupTime, gdriveListBackups } from "../backup";
|
||||||
|
|
||||||
export function BackupSettings() {
|
export function BackupSettings() {
|
||||||
@@ -38,6 +39,8 @@ export function BackupSettings() {
|
|||||||
};
|
};
|
||||||
}, [open, connected, lastBackup]);
|
}, [open, connected, lastBackup]);
|
||||||
|
|
||||||
|
useEscapeLayer(open, close);
|
||||||
|
|
||||||
if (!open) return null;
|
if (!open) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useEffect, useRef, type ReactNode } from "react";
|
import { useEffect, useRef, type ReactNode } from "react";
|
||||||
|
import { useEscapeLayer } from "../escape";
|
||||||
import { Icon } from "./Icon";
|
import { Icon } from "./Icon";
|
||||||
|
|
||||||
interface ConfirmDialogProps {
|
interface ConfirmDialogProps {
|
||||||
@@ -14,17 +15,10 @@ export function ConfirmDialog({ title, message, confirmLabel = "Delete", onConfi
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
confirmRef.current?.focus();
|
confirmRef.current?.focus();
|
||||||
const onKey = (e: KeyboardEvent) => {
|
|
||||||
if (e.key === "Escape") {
|
|
||||||
e.preventDefault();
|
|
||||||
onClose();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
window.addEventListener("keydown", onKey);
|
|
||||||
return () => window.removeEventListener("keydown", onKey);
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEscapeLayer(true, onClose);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="overlay" onClick={onClose}>
|
<div className="overlay" onClick={onClose}>
|
||||||
<div className="panel panel-confirm" onClick={(e) => e.stopPropagation()}>
|
<div className="panel panel-confirm" onClick={(e) => e.stopPropagation()}>
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { useWidth } from "../store/useWidth";
|
|||||||
import { WIDTH_OPTIONS } from "../width";
|
import { WIDTH_OPTIONS } from "../width";
|
||||||
import { bodyNumber, chapterKind, partNumber, partRoman } from "../model/book";
|
import { bodyNumber, chapterKind, partNumber, partRoman } from "../model/book";
|
||||||
import { saveBook } from "../library";
|
import { saveBook } from "../library";
|
||||||
|
import { useEscapeLayer } from "../escape";
|
||||||
import { isDesktop } from "../ipc";
|
import { isDesktop } from "../ipc";
|
||||||
import { useCompact } from "../useMedia";
|
import { useCompact } from "../useMedia";
|
||||||
import { issueSignature, rememberWord, runProof } from "../proofing";
|
import { issueSignature, rememberWord, runProof } from "../proofing";
|
||||||
@@ -59,6 +60,10 @@ export function EditorView() {
|
|||||||
const [findOpen, setFindOpen] = useState(false);
|
const [findOpen, setFindOpen] = useState(false);
|
||||||
const [findExpanded, setFindExpanded] = useState(false);
|
const [findExpanded, setFindExpanded] = useState(false);
|
||||||
const [proofPopover, setProofPopover] = useState<{ issue: ProofIssue; coords: ProofCoords } | null>(null);
|
const [proofPopover, setProofPopover] = useState<{ issue: ProofIssue; coords: ProofCoords } | null>(null);
|
||||||
|
const [escArmed, setEscArmed] = useState(false);
|
||||||
|
const escArmedRef = useRef(false);
|
||||||
|
escArmedRef.current = escArmed;
|
||||||
|
const escTimer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
||||||
|
|
||||||
const spelling = useProofing((s) => s.spelling);
|
const spelling = useProofing((s) => s.spelling);
|
||||||
const grammar = useProofing((s) => s.grammar);
|
const grammar = useProofing((s) => s.grammar);
|
||||||
@@ -84,6 +89,25 @@ export function EditorView() {
|
|||||||
setDock(false);
|
setDock(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEscapeLayer(true, () => {
|
||||||
|
clearTimeout(escTimer.current);
|
||||||
|
if (escArmedRef.current) {
|
||||||
|
setEscArmed(false);
|
||||||
|
useBook.getState().closeBook();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
(document.activeElement as HTMLElement | null)?.blur();
|
||||||
|
setEscArmed(true);
|
||||||
|
escTimer.current = setTimeout(() => setEscArmed(false), 1500);
|
||||||
|
});
|
||||||
|
useEscapeLayer(isCompact && (sidebarOpen || dock), closeDrawers);
|
||||||
|
useEscapeLayer(widthOpen, () => setWidthOpen(false));
|
||||||
|
useEscapeLayer(exportOpen, () => setExportOpen(false));
|
||||||
|
useEscapeLayer(moreOpen, () => setMoreOpen(false));
|
||||||
|
useEscapeLayer(!!proofPopover, () => setProofPopover(null));
|
||||||
|
|
||||||
|
useEffect(() => () => clearTimeout(escTimer.current), []);
|
||||||
|
|
||||||
const openFind = useCallback((expanded: boolean) => {
|
const openFind = useCallback((expanded: boolean) => {
|
||||||
setFindExpanded(expanded);
|
setFindExpanded(expanded);
|
||||||
setFindOpen(true);
|
setFindOpen(true);
|
||||||
@@ -173,19 +197,6 @@ export function EditorView() {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!widthOpen && !exportOpen && !moreOpen) return;
|
|
||||||
const onKey = (e: KeyboardEvent) => {
|
|
||||||
if (e.key === "Escape") {
|
|
||||||
setWidthOpen(false);
|
|
||||||
setExportOpen(false);
|
|
||||||
setMoreOpen(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
window.addEventListener("keydown", onKey);
|
|
||||||
return () => window.removeEventListener("keydown", onKey);
|
|
||||||
}, [widthOpen, exportOpen, moreOpen]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!notice) return;
|
if (!notice) return;
|
||||||
const timer = setTimeout(() => setNotice(null), 4000);
|
const timer = setTimeout(() => setNotice(null), 4000);
|
||||||
@@ -480,6 +491,12 @@ export function EditorView() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{escArmed && (
|
||||||
|
<div className="esc-hint">
|
||||||
|
Press <kbd>Esc</kbd> again for All books
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{settingsOpen && <Settings onClose={() => setSettingsOpen(false)} onSave={saveNow} />}
|
{settingsOpen && <Settings onClose={() => setSettingsOpen(false)} onSave={saveNow} />}
|
||||||
{exporting && (
|
{exporting && (
|
||||||
<div className="export-overlay">
|
<div className="export-overlay">
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { useExportPreview } from "../store/useExportPreview";
|
|||||||
import { bookToPdfInputs, unsupportedScripts } from "../export/typst";
|
import { bookToPdfInputs, unsupportedScripts } from "../export/typst";
|
||||||
import { compilePdf } from "../ipc";
|
import { compilePdf } from "../ipc";
|
||||||
import { saveBytes } from "../project";
|
import { saveBytes } from "../project";
|
||||||
|
import { useEscapeLayer } from "../escape";
|
||||||
import { Icon } from "./Icon";
|
import { Icon } from "./Icon";
|
||||||
|
|
||||||
pdfjs.GlobalWorkerOptions.workerSrc = workerUrl;
|
pdfjs.GlobalWorkerOptions.workerSrc = workerUrl;
|
||||||
@@ -81,13 +82,7 @@ function ExportPreviewModal() {
|
|||||||
};
|
};
|
||||||
}, [book]);
|
}, [book]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEscapeLayer(true, close);
|
||||||
const onKey = (e: KeyboardEvent) => {
|
|
||||||
if (e.key === "Escape") close();
|
|
||||||
};
|
|
||||||
window.addEventListener("keydown", onKey);
|
|
||||||
return () => window.removeEventListener("keydown", onKey);
|
|
||||||
}, [close]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const el = panelRef.current;
|
const el = panelRef.current;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { Editor as TiptapEditor } from "@tiptap/react";
|
|||||||
import { buildRegex, getSearchState } from "../editor/search";
|
import { buildRegex, getSearchState } from "../editor/search";
|
||||||
import { countMatches, replaceInContent } from "../search-book";
|
import { countMatches, replaceInContent } from "../search-book";
|
||||||
import { useBook } from "../store/useBook";
|
import { useBook } from "../store/useBook";
|
||||||
|
import { useEscapeLayer } from "../escape";
|
||||||
import { Icon } from "./Icon";
|
import { Icon } from "./Icon";
|
||||||
|
|
||||||
interface FindBarProps {
|
interface FindBarProps {
|
||||||
@@ -95,6 +96,8 @@ export function FindBar({ editor, open, initialExpanded, onClose }: FindBarProps
|
|||||||
};
|
};
|
||||||
}, [editor, pendingNav, activeChapterId]);
|
}, [editor, pendingNav, activeChapterId]);
|
||||||
|
|
||||||
|
useEscapeLayer(open, onClose);
|
||||||
|
|
||||||
if (!open || !editor) return null;
|
if (!open || !editor) return null;
|
||||||
|
|
||||||
const before = jsonCounts.slice(0, Math.max(0, activeIdx)).reduce((a, b) => a + b, 0);
|
const before = jsonCounts.slice(0, Math.max(0, activeIdx)).reduce((a, b) => a + b, 0);
|
||||||
@@ -163,9 +166,6 @@ export function FindBar({ editor, open, initialExpanded, onClose }: FindBarProps
|
|||||||
if (e.key === "Enter") {
|
if (e.key === "Enter") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.shiftKey ? prev() : next();
|
e.shiftKey ? prev() : next();
|
||||||
} else if (e.key === "Escape") {
|
|
||||||
e.preventDefault();
|
|
||||||
onClose();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -256,9 +256,6 @@ export function FindBar({ editor, open, initialExpanded, onClose }: FindBarProps
|
|||||||
if (e.key === "Enter") {
|
if (e.key === "Enter") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
replaceOne();
|
replaceOne();
|
||||||
} else if (e.key === "Escape") {
|
|
||||||
e.preventDefault();
|
|
||||||
onClose();
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
|
import { useEscapeLayer } from "../escape";
|
||||||
import { Icon } from "./Icon";
|
import { Icon } from "./Icon";
|
||||||
|
|
||||||
interface RowMenuProps {
|
interface RowMenuProps {
|
||||||
@@ -38,12 +39,13 @@ export function RowMenu({ label, onDuplicate, onDelete, onToggleTitle, titleHidd
|
|||||||
popRef.current?.querySelector<HTMLElement>(".row-menu-item")?.focus();
|
popRef.current?.querySelector<HTMLElement>(".row-menu-item")?.focus();
|
||||||
}, [open]);
|
}, [open]);
|
||||||
|
|
||||||
|
useEscapeLayer(open, () => {
|
||||||
|
setOpen(false);
|
||||||
|
btnRef.current?.focus();
|
||||||
|
});
|
||||||
|
|
||||||
const onMenuKeyDown = (e: React.KeyboardEvent) => {
|
const onMenuKeyDown = (e: React.KeyboardEvent) => {
|
||||||
if (e.key === "Escape") {
|
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
|
||||||
e.preventDefault();
|
|
||||||
setOpen(false);
|
|
||||||
btnRef.current?.focus();
|
|
||||||
} else if (e.key === "ArrowDown" || e.key === "ArrowUp") {
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const items = Array.from(popRef.current?.querySelectorAll<HTMLElement>(".row-menu-item") ?? []);
|
const items = Array.from(popRef.current?.querySelectorAll<HTMLElement>(".row-menu-item") ?? []);
|
||||||
const idx = items.indexOf(document.activeElement as HTMLElement);
|
const idx = items.indexOf(document.activeElement as HTMLElement);
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
fontStack,
|
fontStack,
|
||||||
pairingFor,
|
pairingFor,
|
||||||
} from "../model/fonts";
|
} from "../model/fonts";
|
||||||
|
import { useEscapeLayer } from "../escape";
|
||||||
import { Icon } from "./Icon";
|
import { Icon } from "./Icon";
|
||||||
|
|
||||||
const LANGUAGES = [
|
const LANGUAGES = [
|
||||||
@@ -94,16 +95,7 @@ export function Settings({ onClose, onSave }: { onClose: () => void; onSave: ()
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEscapeLayer(true, onClose);
|
||||||
const onKey = (e: KeyboardEvent) => {
|
|
||||||
if (e.key === "Escape") {
|
|
||||||
e.preventDefault();
|
|
||||||
onClose();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
window.addEventListener("keydown", onKey);
|
|
||||||
return () => window.removeEventListener("keydown", onKey);
|
|
||||||
}, [onClose]);
|
|
||||||
|
|
||||||
if (!book) return null;
|
if (!book) return null;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useUpdater } from "../store/useUpdater";
|
import { useUpdater } from "../store/useUpdater";
|
||||||
import { installUpdate, dismissUpdate } from "../updater";
|
import { installUpdate, dismissUpdate } from "../updater";
|
||||||
|
import { useEscapeLayer } from "../escape";
|
||||||
import { Icon } from "./Icon";
|
import { Icon } from "./Icon";
|
||||||
|
|
||||||
const TITLES: Record<string, string> = {
|
const TITLES: Record<string, string> = {
|
||||||
@@ -19,9 +20,11 @@ export function UpdateDialog() {
|
|||||||
const total = useUpdater((s) => s.total);
|
const total = useUpdater((s) => s.total);
|
||||||
const error = useUpdater((s) => s.error);
|
const error = useUpdater((s) => s.error);
|
||||||
|
|
||||||
|
const busy = phase === "downloading" || phase === "installing";
|
||||||
|
useEscapeLayer(phase !== "idle" && !busy, dismissUpdate);
|
||||||
|
|
||||||
if (phase === "idle") return null;
|
if (phase === "idle") return null;
|
||||||
|
|
||||||
const busy = phase === "downloading" || phase === "installing";
|
|
||||||
const pct = total > 0 ? Math.min(100, Math.round((downloaded / total) * 100)) : 0;
|
const pct = total > 0 ? Math.min(100, Math.round((downloaded / total) * 100)) : 0;
|
||||||
const close = busy ? undefined : dismissUpdate;
|
const close = busy ? undefined : dismissUpdate;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useEffect, useReducer, useRef, useState, type ReactNode } from "react";
|
import { useEffect, useReducer, useRef, useState, type ReactNode } from "react";
|
||||||
import type { Editor } from "@tiptap/react";
|
import type { Editor } from "@tiptap/react";
|
||||||
import { Icon } from "../components/Icon";
|
import { Icon } from "../components/Icon";
|
||||||
|
import { useEscapeLayer } from "../escape";
|
||||||
import type { Alignment } from "./align";
|
import type { Alignment } from "./align";
|
||||||
|
|
||||||
const ALIGN_ICONS: Record<Alignment, string> = {
|
const ALIGN_ICONS: Record<Alignment, string> = {
|
||||||
@@ -67,6 +68,12 @@ export function FloatingToolbar({ editor }: { editor: Editor | null }) {
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [editor, linkOpen]);
|
}, [editor, linkOpen]);
|
||||||
|
|
||||||
|
useEscapeLayer(linkOpen, () => {
|
||||||
|
setLinkOpen(false);
|
||||||
|
editor?.commands.focus();
|
||||||
|
});
|
||||||
|
useEscapeLayer(alignOpen, () => setAlignOpen(false));
|
||||||
|
|
||||||
if (!editor) return null;
|
if (!editor) return null;
|
||||||
|
|
||||||
const insertImage = async (file: File) => {
|
const insertImage = async (file: File) => {
|
||||||
@@ -158,9 +165,6 @@ export function FloatingToolbar({ editor }: { editor: Editor | null }) {
|
|||||||
if (e.key === "Enter") {
|
if (e.key === "Enter") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
applyLink();
|
applyLink();
|
||||||
} else if (e.key === "Escape") {
|
|
||||||
e.preventDefault();
|
|
||||||
setLinkOpen(false);
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
type Handler = () => void;
|
||||||
|
|
||||||
|
const layers: Handler[] = [];
|
||||||
|
let bound = false;
|
||||||
|
|
||||||
|
function onKeyDown(e: KeyboardEvent) {
|
||||||
|
if (e.key !== "Escape" || e.isComposing || e.defaultPrevented) return;
|
||||||
|
const top = layers[layers.length - 1];
|
||||||
|
if (!top) return;
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
top();
|
||||||
|
}
|
||||||
|
|
||||||
|
function pushLayer(handler: Handler): () => void {
|
||||||
|
if (!bound) {
|
||||||
|
window.addEventListener("keydown", onKeyDown, true);
|
||||||
|
bound = true;
|
||||||
|
}
|
||||||
|
layers.push(handler);
|
||||||
|
return () => {
|
||||||
|
const i = layers.lastIndexOf(handler);
|
||||||
|
if (i !== -1) layers.splice(i, 1);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useEscapeLayer(active: boolean, onEscape: () => void): void {
|
||||||
|
const latest = useRef(onEscape);
|
||||||
|
latest.current = onEscape;
|
||||||
|
useEffect(() => {
|
||||||
|
if (!active) return;
|
||||||
|
return pushLayer(() => latest.current());
|
||||||
|
}, [active]);
|
||||||
|
}
|
||||||
@@ -2096,6 +2096,32 @@ body.resizing .sidebar {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.esc-hint {
|
||||||
|
position: fixed;
|
||||||
|
top: 54px;
|
||||||
|
left: 50%;
|
||||||
|
z-index: 30;
|
||||||
|
padding: 7px 14px;
|
||||||
|
font-family: var(--font-ui);
|
||||||
|
font-size: var(--t-2);
|
||||||
|
color: var(--ink-soft);
|
||||||
|
background: var(--glass);
|
||||||
|
-webkit-backdrop-filter: blur(10px);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 999px;
|
||||||
|
box-shadow: var(--shadow-pop);
|
||||||
|
pointer-events: none;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
animation: find-drop 140ms var(--ease);
|
||||||
|
}
|
||||||
|
|
||||||
|
.esc-hint kbd {
|
||||||
|
font-family: var(--font-ui);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
.find-expand {
|
.find-expand {
|
||||||
flex: none;
|
flex: none;
|
||||||
width: 20px;
|
width: 20px;
|
||||||
|
|||||||
Reference in new issue
Block a user