diff --git a/web/src/app/SelectionCheckbox.tsx b/web/src/app/SelectionCheckbox.tsx new file mode 100644 index 00000000..473e425b --- /dev/null +++ b/web/src/app/SelectionCheckbox.tsx @@ -0,0 +1,32 @@ +interface SelectionCheckboxProps { + selected: boolean; + className?: string; +} + +/** + * Shared circular multi-select indicator -- every list in this app that lets you pick multiple + * items (forward targets, message multi-select, create-group contact picker, favorites, blocklist) + * used to hand-copy this same circle-that-fills-with-a-checkmark markup into its own file, each + * with small, accidental class differences (a stray extra border in the selected state here, a + * slightly different gray there) -- fixing the same visual bug five separate times is exactly the + * failure mode a shared component exists to prevent. + * + * Draws the checkmark as a hand-drawn SVG path, not the Material Icons font glyph `Icon` uses + * everywhere else: the font's hinting isn't tuned for a ~13px glyph inside a 20px circle, so above + * 1x pixel density it renders visibly soft/asymmetric where a vector path stays crisp at any scale. + * A deliberate one-off exception to this app's usual `Icon`-for-everything convention, not a second + * icon system -- scoped to this one selection indicator. + */ +export function SelectionCheckbox({ selected, className = '' }: SelectionCheckboxProps) { + return ( + + {selected ? ( + + ) : null} + + ); +} diff --git a/web/src/features/contacts/BlocklistView.tsx b/web/src/features/contacts/BlocklistView.tsx index 552a4993..544b7658 100644 --- a/web/src/features/contacts/BlocklistView.tsx +++ b/web/src/features/contacts/BlocklistView.tsx @@ -5,6 +5,7 @@ import { Link } from 'react-router-dom'; import { Avatar } from '@/app/Avatar'; import { Icon } from '@/app/Icon'; +import { SelectionCheckbox } from '@/app/SelectionCheckbox'; import { Toast } from '@/app/Toast'; import { fetchAllPages } from '@/lib/api/client'; import { batchUnblockUsers, getBlocks, unblockUser } from '@/lib/api/friends'; @@ -114,15 +115,7 @@ export function BlocklistView() { onKeyDown={selectMode ? (e) => e.key === 'Enter' && toggleSelected(b.user.user_id) : undefined} className={`flex items-center gap-3 rounded-xl bg-white px-4 py-3 shadow-bubble ${selectMode ? 'cursor-pointer' : ''}`} > - {selectMode ? ( - - {selected.has(b.user.user_id) ? : null} - - ) : null} + {selectMode ? : null}
{body} - {selectMode ? ( - - {selected ? : null} - - ) : null} + {selectMode ? : null}
diff --git a/web/src/features/groups/CreateGroupModal.tsx b/web/src/features/groups/CreateGroupModal.tsx index 98f1a8b4..b6aacab3 100644 --- a/web/src/features/groups/CreateGroupModal.tsx +++ b/web/src/features/groups/CreateGroupModal.tsx @@ -3,6 +3,7 @@ import { useMemo, useState } from 'react'; import { useQuery } from '@tanstack/react-query'; import { Icon } from '@/app/Icon'; +import { SelectionCheckbox } from '@/app/SelectionCheckbox'; import { fetchAllPages } from '@/lib/api/client'; import { getFriends } from '@/lib/api/friends'; import { createGroup, getGroupGovernance, updateGroupGovernance } from '@/lib/api/groups'; @@ -133,13 +134,7 @@ export function CreateGroupModal({ onClose, onCreated }: CreateGroupModalProps) onClick={() => toggle(f.user_id, label)} className="flex w-full items-center gap-3 rounded-lg px-2 py-2 text-left hover:bg-surface-rail" > - - {checked ? : null} - + diff --git a/web/src/features/messages/ForwardPickerModal.tsx b/web/src/features/messages/ForwardPickerModal.tsx index 55a7d03e..6348435d 100644 --- a/web/src/features/messages/ForwardPickerModal.tsx +++ b/web/src/features/messages/ForwardPickerModal.tsx @@ -3,6 +3,7 @@ import { useMemo, useState } from 'react'; import type { Contact } from 'tinode-sdk'; import { Icon } from '@/app/Icon'; +import { SelectionCheckbox } from '@/app/SelectionCheckbox'; import { avatarBgClass, avatarLetter } from '@/lib/avatar'; import { useEscapeKey } from '@/lib/hooks'; import { useChatList, useTinode } from '@/lib/tinode/hooks'; @@ -14,19 +15,6 @@ interface ForwardPickerModalProps { onClose: () => void; } -/** A hand-drawn checkmark instead of the Material Icons font glyph this row used to use -- the font - * glyph's hinting isn't tuned for a 14px render inside a 20px circle, so at anything above 1x pixel - * density it comes out visibly soft/asymmetric where a vector path stays crisp at any scale. Not a - * systemic swap (`Icon` is still how the rest of the app draws icons) -- scoped to this one - * selection indicator, which is the one a user actually screenshotted and flagged as rough-looking. */ -function CheckMark({ className = '' }: { className?: string }) { - return ( - - ); -} - /** * README §3.6: multi-select targets + an optional comment, sent in one action. Used by the message * context menu's single-message "转发", the multi-select bulk bar's "转发", and favorites' bulk @@ -115,11 +103,7 @@ export function ForwardPickerModal({ onPick, onClose }: ForwardPickerModalProps) {avatarLetter(c.public?.fn)} {c.public?.fn || c.topic} - - {isSelected ? : null} - + ); }; diff --git a/web/src/features/messages/MessageBubble.tsx b/web/src/features/messages/MessageBubble.tsx index 1794e21b..c4a1f195 100644 --- a/web/src/features/messages/MessageBubble.tsx +++ b/web/src/features/messages/MessageBubble.tsx @@ -4,6 +4,7 @@ import { Link } from 'react-router-dom'; import { Avatar } from '@/app/Avatar'; import { Icon } from '@/app/Icon'; +import { SelectionCheckbox } from '@/app/SelectionCheckbox'; import { avatarBgClass, avatarLetter } from '@/lib/avatar'; import { callResultDisplay, contactCardContentOf, isRecalled, isServerDeleted, systemNoticeText, type ContactCardContent } from '@/lib/messages/dispatch'; import { renderDrafty } from '@/lib/messages/drafty'; @@ -119,14 +120,8 @@ export function MessageRow({ isRecalled(msg) ? (
) : ( - ) ) : null}