feat: remove drag-and-drop functionality for TOTP and website rows; update styles and translations for improved user experience

This commit is contained in:
shuaiplus
2026-05-08 16:09:02 +08:00
parent 2e9bbe6801
commit 5809e3eebc
11 changed files with 80 additions and 402 deletions
+3 -77
View File
@@ -9,9 +9,6 @@
"version": "1.5.1",
"license": "LGPL-3.0",
"dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@noble/hashes": "^2.0.1",
"@tanstack/react-query": "^5.90.21",
"@zip.js/zip.js": "^2.8.22",
@@ -525,59 +522,6 @@
"node": ">=12"
}
},
"node_modules/@dnd-kit/accessibility": {
"version": "3.1.1",
"resolved": "https://registry.npmmirror.com/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz",
"integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==",
"license": "MIT",
"dependencies": {
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@dnd-kit/core": {
"version": "6.3.1",
"resolved": "https://registry.npmmirror.com/@dnd-kit/core/-/core-6.3.1.tgz",
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
"license": "MIT",
"dependencies": {
"@dnd-kit/accessibility": "^3.1.1",
"@dnd-kit/utilities": "^3.2.2",
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/@dnd-kit/sortable": {
"version": "10.0.0",
"resolved": "https://registry.npmmirror.com/@dnd-kit/sortable/-/sortable-10.0.0.tgz",
"integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==",
"license": "MIT",
"dependencies": {
"@dnd-kit/utilities": "^3.2.2",
"tslib": "^2.0.0"
},
"peerDependencies": {
"@dnd-kit/core": "^6.3.0",
"react": ">=16.8.0"
}
},
"node_modules/@dnd-kit/utilities": {
"version": "3.2.2",
"resolved": "https://registry.npmmirror.com/@dnd-kit/utilities/-/utilities-3.2.2.tgz",
"integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==",
"license": "MIT",
"dependencies": {
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@emnapi/runtime": {
"version": "1.8.1",
"resolved": "https://registry.npmmirror.com/@emnapi/runtime/-/runtime-1.8.1.tgz",
@@ -3518,19 +3462,6 @@
"node": ">=0.10.0"
}
},
"node_modules/react-dom": {
"version": "19.2.4",
"resolved": "https://registry.npmmirror.com/react-dom/-/react-dom-19.2.4.tgz",
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
"peerDependencies": {
"react": "^19.2.4"
}
},
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/read-cache/-/read-cache-1.0.0.tgz",
@@ -3688,13 +3619,6 @@
"queue-microtask": "^1.2.2"
}
},
"node_modules/scheduler": {
"version": "0.27.0",
"resolved": "https://registry.npmmirror.com/scheduler/-/scheduler-0.27.0.tgz",
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
"license": "MIT",
"peer": true
},
"node_modules/semver": {
"version": "7.7.4",
"resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz",
@@ -3944,7 +3868,9 @@
"version": "2.8.1",
"resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
"dev": true,
"license": "0BSD",
"optional": true
},
"node_modules/tsx": {
"version": "4.21.0",
-3
View File
@@ -56,9 +56,6 @@
"wrangler": "^4.71.0"
},
"dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@noble/hashes": "^2.0.1",
"@tanstack/react-query": "^5.90.21",
"@zip.js/zip.js": "^2.8.22",
+6 -122
View File
@@ -1,22 +1,5 @@
import type { JSX } from 'preact';
import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
import { Clipboard, Globe, GripVertical } from 'lucide-preact';
import {
closestCenter,
DndContext,
type DragEndEvent,
PointerSensor,
TouchSensor,
useSensor,
useSensors,
} from '@dnd-kit/core';
import {
arrayMove,
rectSortingStrategy,
SortableContext,
useSortable,
} from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { Clipboard, Globe } from 'lucide-preact';
import { copyTextToClipboard as copyTextWithFeedback } from '@/lib/clipboard';
import { calcTotpNow } from '@/lib/crypto';
import { t } from '@/lib/i18n';
@@ -34,7 +17,6 @@ interface TotpCodesPageProps {
const TOTP_PERIOD_SECONDS = 30;
const TOTP_RING_RADIUS = 14;
const TOTP_RING_CIRCUMFERENCE = 2 * Math.PI * TOTP_RING_RADIUS;
const TOTP_ORDER_STORAGE_KEY = 'nodewarden.totp-order';
const TOTP_REFRESH_BATCH_SIZE = 16;
function getTotpTimeState(): { windowId: number; remain: number } {
const epoch = Math.floor(Date.now() / 1000);
@@ -55,39 +37,18 @@ function TotpListIcon({ cipher }: { cipher: Cipher }) {
return <WebsiteIcon cipher={cipher} fallback={<Globe size={18} />} />;
}
interface SortableTotpRowProps {
interface TotpRowProps {
cipher: Cipher;
live: { code: string; remain: number } | null;
onCopy: (value: string) => void;
}
function SortableTotpRow(props: SortableTotpRowProps) {
const { attributes, listeners, setActivatorNodeRef, setNodeRef, transform, transition, isDragging } = useSortable({
id: props.cipher.id,
});
const dragButtonAttributes = attributes as JSX.HTMLAttributes<HTMLButtonElement>;
const style = {
transform: CSS.Transform.toString(transform),
transition,
};
function TotpRow(props: TotpRowProps) {
const name = props.cipher.decName || props.cipher.name || t('txt_no_name');
const username = props.cipher.login?.decUsername || '';
return (
<div ref={setNodeRef} style={style} className={`totp-code-row${isDragging ? ' is-dragging' : ''}`}>
<button
type="button"
ref={setActivatorNodeRef}
className="btn btn-secondary small totp-drag-btn"
title={t('txt_drag_to_reorder')}
aria-label={t('txt_drag_to_reorder')}
{...dragButtonAttributes}
{...listeners}
>
<GripVertical size={14} className="btn-icon" />
</button>
<div className="totp-code-row">
<div className="totp-code-info">
<div className="list-icon-wrap">
<TotpListIcon cipher={props.cipher} />
@@ -135,30 +96,7 @@ export default function TotpCodesPage(props: TotpCodesPageProps) {
const [totpCodes, setTotpCodes] = useState<Record<string, string | null>>({});
const [remainingSeconds, setRemainingSeconds] = useState(() => getTotpTimeState().remain);
const [columnCount, setColumnCount] = useState(1);
const [orderedIds, setOrderedIds] = useState<string[]>(() => {
if (typeof window === 'undefined') return [];
try {
const parsed = JSON.parse(String(window.localStorage.getItem(TOTP_ORDER_STORAGE_KEY) || '[]'));
return Array.isArray(parsed) ? parsed.map((id) => String(id || '').trim()).filter(Boolean) : [];
} catch {
return [];
}
});
const listRef = useRef<HTMLDivElement | null>(null);
const hasLoadedTotpItemsRef = useRef(false);
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: {
distance: 6,
},
}),
useSensor(TouchSensor, {
activationConstraint: {
delay: 120,
tolerance: 8,
},
}),
);
async function copyToClipboard(value: string): Promise<void> {
await copyTextWithFeedback(value, { successMessage: t('txt_code_copied') });
@@ -169,7 +107,7 @@ export default function TotpCodesPage(props: TotpCodesPageProps) {
[]
);
const baseTotpItems = useMemo(
const totpItems = useMemo(
() =>
props.ciphers
.filter((cipher) => isCipherVisibleInNormalVault(cipher) && !!cipher.login?.decTotp)
@@ -181,46 +119,6 @@ export default function TotpCodesPage(props: TotpCodesPageProps) {
[props.ciphers, nameCollator]
);
const totpItems = useMemo(() => {
if (!baseTotpItems.length) return [];
const orderMap = new Map(orderedIds.map((id, index) => [id, index]));
return [...baseTotpItems].sort((a, b) => {
const orderA = orderMap.get(a.id);
const orderB = orderMap.get(b.id);
if (orderA != null && orderB != null) return orderA - orderB;
if (orderA != null) return -1;
if (orderB != null) return 1;
const nameA = (a.decName || a.name || '').trim();
const nameB = (b.decName || b.name || '').trim();
return nameCollator.compare(nameA, nameB);
});
}, [baseTotpItems, orderedIds, nameCollator]);
const sortableTotpItems = useMemo(() => totpItems.map((cipher) => cipher.id), [totpItems]);
useEffect(() => {
if (!baseTotpItems.length) return;
hasLoadedTotpItemsRef.current = true;
const validIds = new Set(baseTotpItems.map((cipher) => cipher.id));
setOrderedIds((prev) => {
const filtered = prev.filter((id) => validIds.has(id));
const missing = baseTotpItems.map((cipher) => cipher.id).filter((id) => !filtered.includes(id));
const next = [...filtered, ...missing];
if (next.length === prev.length && next.every((id, index) => id === prev[index])) return prev;
return next;
});
}, [baseTotpItems]);
useEffect(() => {
if (typeof window === 'undefined') return;
if (!hasLoadedTotpItemsRef.current) return;
try {
window.localStorage.setItem(TOTP_ORDER_STORAGE_KEY, JSON.stringify(orderedIds));
} catch {
// ignore storage write failures
}
}, [orderedIds]);
useEffect(() => {
if (!totpItems.length) {
setTotpCodes({});
@@ -307,16 +205,6 @@ export default function TotpCodesPage(props: TotpCodesPageProps) {
return () => observer.disconnect();
}, []);
const handleDragEnd = (event: DragEndEvent) => {
const activeId = String(event.active.id);
const overId = event.over ? String(event.over.id) : null;
if (!overId || activeId === overId) return;
const fromIndex = orderedIds.indexOf(activeId);
const toIndex = orderedIds.indexOf(overId);
if (fromIndex === -1 || toIndex === -1 || fromIndex === toIndex) return;
setOrderedIds((prev) => arrayMove(prev, fromIndex, toIndex));
};
return (
<div className="totp-codes-page">
<div className="card">
@@ -330,18 +218,14 @@ export default function TotpCodesPage(props: TotpCodesPageProps) {
>
{!totpItems.length && props.loading && <LoadingState lines={6} />}
{!totpItems.length && !props.loading && <div className="empty">{t('txt_no_verification_codes')}</div>}
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
<SortableContext items={sortableTotpItems} strategy={rectSortingStrategy}>
{totpItems.map((cipher) => (
<SortableTotpRow
<TotpRow
key={cipher.id}
cipher={cipher}
live={totpCodes[cipher.id] ? { code: totpCodes[cipher.id] || '', remain: remainingSeconds } : null}
onCopy={(value) => void copyToClipboard(value)}
/>
))}
</SortableContext>
</DndContext>
</div>
</div>
</div>
+33 -100
View File
@@ -1,25 +1,8 @@
import type { JSX, RefObject } from 'preact';
import type { RefObject } from 'preact';
import { createPortal } from 'preact/compat';
import { CheckCheck, Download, GripVertical, Paperclip, Plus, QrCode, RefreshCw, Star, StarOff, Trash2, Upload, X } from 'lucide-preact';
import { ArrowDown, ArrowUp, CheckCheck, Download, Paperclip, Plus, QrCode, RefreshCw, Star, StarOff, Trash2, Upload, X } from 'lucide-preact';
import { useEffect, useRef, useState } from 'preact/hooks';
import { useDialogLifecycle } from '@/components/ConfirmDialog';
import {
closestCenter,
DndContext,
type DragEndEvent,
type DragStartEvent,
PointerSensor,
TouchSensor,
useSensor,
useSensors,
} from '@dnd-kit/core';
import {
SortableContext,
arrayMove,
useSortable,
verticalListSortingStrategy,
} from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import type { Cipher, Folder, VaultDraft, VaultDraftField } from '@/lib/types';
import { t } from '@/lib/i18n';
import {
@@ -67,46 +50,45 @@ interface VaultEditorProps {
onDeleteSelected: () => void;
}
interface SortableWebsiteRowProps {
id: string;
interface WebsiteRowProps {
uriEntry: VaultDraft['loginUris'][number];
index: number;
canRemove: boolean;
isDragging: boolean;
canMoveUp: boolean;
canMoveDown: boolean;
onUpdateUri: (index: number, value: string) => void;
onUpdateMatch: (index: number, value: number | null) => void;
onMove: (fromIndex: number, toIndex: number) => void;
onRemove: (index: number) => void;
}
function SortableWebsiteRow(props: SortableWebsiteRowProps) {
function WebsiteRow(props: WebsiteRowProps) {
const websiteMatchOptions = getWebsiteMatchOptions();
const { attributes, listeners, setActivatorNodeRef, setNodeRef, transform, transition, isDragging } = useSortable({
id: props.id,
});
const dragButtonAttributes = attributes as JSX.HTMLAttributes<HTMLButtonElement>;
const style = {
transform: CSS.Transform.toString(transform),
transition,
};
return (
<div
ref={setNodeRef}
style={style}
className={`website-row${isDragging || props.isDragging ? ' is-dragging' : ''}`}
>
<div className="website-row">
<div className="website-order-actions">
<button
type="button"
ref={setActivatorNodeRef}
className="btn btn-secondary small website-drag-btn"
title={t('txt_drag_to_reorder')}
aria-label={t('txt_drag_to_reorder')}
{...dragButtonAttributes}
{...listeners}
className="btn btn-secondary small website-order-btn"
title={t('txt_move_up')}
aria-label={t('txt_move_up')}
disabled={!props.canMoveUp}
onClick={() => props.onMove(props.index, props.index - 1)}
>
<GripVertical size={14} className="btn-icon" />
<ArrowUp size={14} className="btn-icon" />
</button>
<button
type="button"
className="btn btn-secondary small website-order-btn"
title={t('txt_move_down')}
aria-label={t('txt_move_down')}
disabled={!props.canMoveDown}
onClick={() => props.onMove(props.index, props.index + 1)}
>
<ArrowDown size={14} className="btn-icon" />
</button>
</div>
<input
className="input"
value={props.uriEntry.uri}
@@ -138,32 +120,14 @@ function SortableWebsiteRow(props: SortableWebsiteRowProps) {
export default function VaultEditor(props: VaultEditorProps) {
const createTypeOptions = getCreateTypeOptions();
const uriIdSeedRef = useRef(0);
const totpQrVideoRef = useRef<HTMLVideoElement | null>(null);
const totpQrFileRef = useRef<HTMLInputElement | null>(null);
const totpQrStreamRef = useRef<MediaStream | null>(null);
const totpQrFrameRef = useRef<number | null>(null);
const [uriItemIds, setUriItemIds] = useState<string[]>([]);
const [activeUriId, setActiveUriId] = useState<string | null>(null);
const [totpQrOpen, setTotpQrOpen] = useState(false);
const [totpQrStatus, setTotpQrStatus] = useState('');
const [totpQrBusy, setTotpQrBusy] = useState(false);
useDialogLifecycle(totpQrOpen, () => setTotpQrOpen(false));
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: {
distance: 6,
},
}),
useSensor(TouchSensor, {
activationConstraint: {
delay: 120,
tolerance: 8,
},
}),
);
const createUriId = () => `login-uri-${uriIdSeedRef.current++}`;
const stopTotpQrScanner = () => {
if (totpQrFrameRef.current != null) {
@@ -222,21 +186,6 @@ export default function VaultEditor(props: VaultEditorProps) {
}
};
useEffect(() => {
setUriItemIds((prev) => {
if (prev.length === props.draft.loginUris.length) return prev;
if (prev.length < props.draft.loginUris.length) {
return [...prev, ...Array.from({ length: props.draft.loginUris.length - prev.length }, () => createUriId())];
}
return prev.slice(0, props.draft.loginUris.length);
});
}, [props.draft.loginUris.length]);
useEffect(() => {
setUriItemIds(props.draft.loginUris.map(() => createUriId()));
setActiveUriId(null);
}, [props.draft.id, props.isCreating]);
useEffect(() => {
if (!totpQrOpen) {
stopTotpQrScanner();
@@ -324,28 +273,15 @@ export default function VaultEditor(props: VaultEditorProps) {
});
const addLoginUri = () => {
setUriItemIds((prev) => [...prev, createUriId()]);
props.onUpdateDraft({ loginUris: [...props.draft.loginUris, createEmptyLoginUri()] });
};
const removeLoginUri = (index: number) => {
setUriItemIds((prev) => prev.filter((_, itemIndex) => itemIndex !== index));
props.onUpdateDraft({ loginUris: props.draft.loginUris.filter((_, itemIndex) => itemIndex !== index) });
};
const handleWebsiteDragStart = (event: DragStartEvent) => {
setActiveUriId(String(event.active.id));
};
const handleWebsiteDragEnd = (event: DragEndEvent) => {
const activeId = String(event.active.id);
const overId = event.over ? String(event.over.id) : null;
setActiveUriId(null);
if (!overId || activeId === overId) return;
const fromIndex = uriItemIds.indexOf(activeId);
const toIndex = uriItemIds.indexOf(overId);
if (fromIndex === -1 || toIndex === -1 || fromIndex === toIndex) return;
setUriItemIds((prev) => arrayMove(prev, fromIndex, toIndex));
const moveLoginUri = (fromIndex: number, toIndex: number) => {
if (fromIndex < 0 || toIndex < 0 || fromIndex >= props.draft.loginUris.length || toIndex >= props.draft.loginUris.length || fromIndex === toIndex) return;
props.onReorderDraftLoginUri(fromIndex, toIndex);
};
@@ -435,23 +371,20 @@ export default function VaultEditor(props: VaultEditorProps) {
<Plus size={14} className="btn-icon" /> {t('txt_add_website')}
</button>
</div>
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragStart={handleWebsiteDragStart} onDragEnd={handleWebsiteDragEnd}>
<SortableContext items={uriItemIds} strategy={verticalListSortingStrategy}>
{props.draft.loginUris.map((uriEntry, index) => (
<SortableWebsiteRow
key={uriItemIds[index] ?? `uri-${index}`}
id={uriItemIds[index] ?? `uri-fallback-${index}`}
<WebsiteRow
key={`uri-${index}`}
uriEntry={uriEntry}
index={index}
canMoveUp={index > 0}
canMoveDown={index < props.draft.loginUris.length - 1}
canRemove={props.draft.loginUris.length > 1}
isDragging={activeUriId === uriItemIds[index]}
onUpdateUri={props.onUpdateDraftLoginUri}
onUpdateMatch={props.onUpdateDraftLoginUriMatch}
onMove={moveLoginUri}
onRemove={removeLoginUri}
/>
))}
</SortableContext>
</DndContext>
{props.draft.loginFido2Credentials.length > 0 && (
<>
<div className="section-head passkeys-section-head">
+2 -1
View File
@@ -550,8 +550,9 @@ const en: Record<string, string> = {
"txt_master_password_reprompt_2": "Master Password Reprompt",
"txt_max_access_count": "Max Access Count",
"txt_middle_name": "Middle Name",
"txt_drag_to_reorder": "Drag to reorder",
"txt_move": "Move",
"txt_move_up": "Move up",
"txt_move_down": "Move down",
"txt_move_selected_items": "Move Selected Items",
"txt_moved_selected_items": "Moved selected items",
"txt_name": "Name",
+2 -1
View File
@@ -550,8 +550,9 @@ const es: Record<string, string> = {
"txt_master_password_reprompt_2": "Solicitar contraseña maestra de nuevo",
"txt_max_access_count": "Número máximo de accesos",
"txt_middle_name": "Segundo nombre",
"txt_drag_to_reorder": "Arrastre para reordenar",
"txt_move": "Mover",
"txt_move_up": "Mover arriba",
"txt_move_down": "Mover abajo",
"txt_move_selected_items": "Mover elementos seleccionados",
"txt_moved_selected_items": "Elementos seleccionados movidos",
"txt_name": "Nombre",
+2 -1
View File
@@ -550,8 +550,9 @@ const ru: Record<string, string> = {
"txt_master_password_reprompt_2": "Повторный запрос мастер-пароля",
"txt_max_access_count": "Максимальное количество доступов",
"txt_middle_name": "Второе имя",
"txt_drag_to_reorder": "Перетащите, чтобы изменить порядок",
"txt_move": "Переместить",
"txt_move_up": "Переместить вверх",
"txt_move_down": "Переместить вниз",
"txt_move_selected_items": "Переместить выбранные элементы",
"txt_moved_selected_items": "Перемещены выбранные элементы",
"txt_name": "Имя",
+2 -1
View File
@@ -550,8 +550,9 @@ const zhCN: Record<string, string> = {
"txt_master_password_reprompt_2": "主密码二次确认",
"txt_max_access_count": "最大访问次数",
"txt_middle_name": "中间名",
"txt_drag_to_reorder": "拖动调整顺序",
"txt_move": "移动",
"txt_move_up": "上移",
"txt_move_down": "下移",
"txt_move_selected_items": "移动所选项目",
"txt_moved_selected_items": "已移动所选项目",
"txt_name": "名称",
+2 -1
View File
@@ -550,8 +550,9 @@ const zhTW: Record<string, string> = {
"txt_master_password_reprompt_2": "主密碼二次確認",
"txt_max_access_count": "最大訪問次數",
"txt_middle_name": "中間名",
"txt_drag_to_reorder": "拖動調整順序",
"txt_move": "移動",
"txt_move_up": "上移",
"txt_move_down": "下移",
"txt_move_selected_items": "移動所選項目",
"txt_moved_selected_items": "已移動所選項目",
"txt_name": "名稱",
+7 -32
View File
@@ -587,46 +587,21 @@
opacity var(--dur-fast) var(--ease-smooth);
}
.website-row.is-dragging {
@apply opacity-50;
border-color: rgba(37, 99, 235, 0.24);
background: color-mix(in srgb, var(--panel-soft) 92%, white 8%);
box-shadow: var(--shadow-sm);
.website-order-actions {
@apply grid gap-1;
}
.website-drag-btn {
@apply relative h-12 w-7 min-w-7 cursor-grab gap-0 overflow-visible rounded-[10px] p-0 opacity-[0.82];
.website-order-btn {
@apply h-[22px] w-7 min-w-7 gap-0 rounded-[8px] p-0;
color: var(--muted);
border-color: transparent;
background: transparent;
box-shadow: none;
touch-action: none;
-webkit-user-select: none;
user-select: none;
}
.website-drag-btn:hover {
.website-order-btn:hover:not(:disabled) {
color: var(--primary-strong);
border-color: transparent;
background: transparent;
box-shadow: none;
opacity: 1;
}
.website-drag-btn:active {
cursor: grabbing;
border-color: transparent;
background: transparent;
box-shadow: none;
}
.website-drag-btn::before {
content: '';
@apply absolute -inset-2 rounded-xl;
}
.website-drag-btn .btn-icon {
@apply opacity-90;
.website-order-btn:disabled {
@apply opacity-35;
}
.website-match-select {
+1 -43
View File
@@ -779,7 +779,7 @@
.totp-code-row {
@apply grid w-full min-w-0 max-w-none items-center gap-2.5 rounded-xl border p-3;
grid-template-columns: auto minmax(0, 1fr) auto;
grid-template-columns: minmax(0, 1fr) auto;
border-color: #e2e8f0;
background: #f8fafc;
transition:
@@ -790,52 +790,10 @@
opacity var(--dur-fast) var(--ease-smooth);
}
.totp-code-row.is-dragging {
@apply z-[2];
border-color: rgba(37, 99, 235, 0.3);
background: color-mix(in srgb, var(--panel) 88%, white 12%);
box-shadow: 0 18px 36px rgba(15, 23, 42, 0.14);
}
.totp-code-info {
@apply flex min-w-0 items-center gap-2.5;
}
.totp-drag-btn {
@apply relative h-[34px] w-6 min-w-6 cursor-grab self-center overflow-visible rounded-[10px] p-0 opacity-[0.82];
color: var(--muted);
touch-action: none;
-webkit-user-select: none;
user-select: none;
border-color: transparent;
background: transparent;
box-shadow: none;
}
.totp-drag-btn:hover {
color: var(--primary-strong);
border-color: transparent;
background: transparent;
box-shadow: none;
opacity: 1;
}
.totp-drag-btn:active {
cursor: grabbing;
border-color: transparent;
background: transparent;
box-shadow: none;
}
.totp-drag-btn::before {
content: '';
@apply absolute -inset-2.5 rounded-xl;
}
.totp-drag-btn .btn-icon {
@apply opacity-90;
}
.totp-code-main {
@apply flex min-w-0 shrink-0 items-center gap-1.5;
}