From 41031aa7e676b587f8d526b9e591db3dbb74cdd5 Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Tue, 5 May 2026 23:37:15 -0400 Subject: [PATCH] Optimize icon picker for mobile with responsive sizing (#12275) * Increase emoji picker cell size on mobile Mobile uses a 40px button with a 32px emoji glyph (vs. 32px / 24px on desktop), so roughly 8 emojis fit across a typical phone screen for easier touch targeting. https://claude.ai/code/session_017Rrv75Rc6eZ7eb2iNpZxpu * tweaks --------- Co-authored-by: Claude --- .../IconPicker/components/GridTemplate.tsx | 29 +++++++++++++++---- .../IconPicker/components/IconButton.tsx | 7 ++++- app/components/IconPicker/index.tsx | 4 ++- .../Document/components/DocumentTitle.tsx | 2 +- 4 files changed, 33 insertions(+), 9 deletions(-) diff --git a/app/components/IconPicker/components/GridTemplate.tsx b/app/components/IconPicker/components/GridTemplate.tsx index fa7bf655cb..998ddc54e1 100644 --- a/app/components/IconPicker/components/GridTemplate.tsx +++ b/app/components/IconPicker/components/GridTemplate.tsx @@ -6,15 +6,21 @@ import { IconType } from "@shared/types"; import { IconLibrary } from "@shared/utils/IconLibrary"; import { Emoji } from "~/components/Emoji"; import Text from "~/components/Text"; +import useMobile from "~/hooks/useMobile"; import { TRANSLATED_CATEGORIES } from "../utils"; import Grid from "./Grid"; import { IconButton } from "./IconButton"; import { CustomEmoji } from "@shared/components/CustomEmoji"; /** - * icon/emoji size is 24px; and we add 4px padding on all sides, + * Desktop: 24px icon/emoji + 4px padding on all sides = 32px button. + * Mobile: 32px icon/emoji + 4px padding on all sides = 40px button, so + * roughly 8 emojis fit across a typical phone screen. */ -const BUTTON_SIZE = 32; +const BUTTON_SIZE_DESKTOP = 32; +const BUTTON_SIZE_MOBILE = 40; +const ICON_SIZE_DESKTOP = 24; +const ICON_SIZE_MOBILE = 32; type OutlineNode = { type: IconType.SVG; @@ -53,8 +59,11 @@ const GridTemplate = ( { width, height, data, empty, onIconSelect }: Props, ref: React.Ref ) => { + const isMobile = useMobile(); + const buttonSize = isMobile ? BUTTON_SIZE_MOBILE : BUTTON_SIZE_DESKTOP; + const iconSize = isMobile ? ICON_SIZE_MOBILE : ICON_SIZE_DESKTOP; // 24px padding for the Grid Container - const itemsPerRow = Math.floor((width - 24) / BUTTON_SIZE); + const itemsPerRow = Math.max(1, Math.floor((width - 24) / buttonSize)); const gridItems = compact( data.flatMap((node) => { @@ -84,7 +93,11 @@ const GridTemplate = ( onClick={() => onIconSelect({ id: item.name, value: item.name })} style={{ "--delay": `${item.delay}ms` } as React.CSSProperties} > - + {item.initial} @@ -96,7 +109,11 @@ const GridTemplate = ( key={item.id} onClick={() => onIconSelect({ id: item.id, value: item.value })} > - + {item.type === IconType.Custom ? ( ) : ( @@ -119,7 +136,7 @@ const GridTemplate = ( height={height} data={gridItems} columns={itemsPerRow} - itemWidth={BUTTON_SIZE} + itemWidth={buttonSize} /> ); }; diff --git a/app/components/IconPicker/components/IconButton.tsx b/app/components/IconPicker/components/IconButton.tsx index ebc7e6a891..0f662f8724 100644 --- a/app/components/IconPicker/components/IconButton.tsx +++ b/app/components/IconPicker/components/IconButton.tsx @@ -1,5 +1,5 @@ import styled from "styled-components"; -import { s, hover } from "@shared/styles"; +import { breakpoints, s, hover } from "@shared/styles"; import NudeButton from "~/components/NudeButton"; export const IconButton = styled(NudeButton)<{ delay?: number }>` @@ -10,4 +10,9 @@ export const IconButton = styled(NudeButton)<{ delay?: number }>` &: ${hover} { background: ${s("listItemHoverBackground")}; } + + @media (max-width: ${breakpoints.tablet - 1}px) { + width: 40px; + height: 40px; + } `; diff --git a/app/components/IconPicker/index.tsx b/app/components/IconPicker/index.tsx index 81a52fd34b..531187d786 100644 --- a/app/components/IconPicker/index.tsx +++ b/app/components/IconPicker/index.tsx @@ -79,7 +79,9 @@ const IconPicker = ({ const [activeTab, setActiveTab] = React.useState(defaultTab); - const popoverWidth = isMobile ? windowWidth : POPOVER_WIDTH; + // The Drawer's inner content has 6px padding on each side; subtract it + // so the panel doesn't overflow horizontally and itemsPerRow is correct. + const popoverWidth = isMobile ? windowWidth - 12 : POPOVER_WIDTH; const handleTabChange = React.useCallback((value: string) => { setActiveTab(value as TabName); diff --git a/app/scenes/Document/components/DocumentTitle.tsx b/app/scenes/Document/components/DocumentTitle.tsx index bc0bfba087..96b8047868 100644 --- a/app/scenes/Document/components/DocumentTitle.tsx +++ b/app/scenes/Document/components/DocumentTitle.tsx @@ -297,7 +297,7 @@ const StyledIconPicker = styled(IconPicker)` const Title = styled(ContentEditable)` position: relative; line-height: ${lineHeight}; - margin-top: ${(props: TitleProps) => (props.$containsIcon ? "10vh" : "3vh")}; + margin-top: 8vh; margin-bottom: 0.5em; font-size: ${fontSize}; font-weight: 600;