From c31c982af578b245ba9c56ee17d8d330e76856cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriele=20Vigan=C3=B2?= Date: Mon, 12 Oct 2026 18:32:25 +0200 Subject: [PATCH 1/3] fix(mobile): tighten toolbars, stacked cards and section navigation On phones a segmented filter shares its row with the filter button instead of wrapping alone, unset values drop out of stacked cards, copyable IDs keep their touch target without the extra line height, and the current section pill scrolls into view. Overview numbers share a baseline with their sections two per row, FAQ questions wrap, category pills run to the screen edges, project links truncate, and the user header stacks its meta without a dangling separator. Chip lists size their fit from the text the chip actually shows. --- docs/design.md | 20 +++++++++++-------- src/components/primitives/chip-overflow.tsx | 17 ++++++++++++---- src/components/primitives/copyable-text.tsx | 5 +++-- src/components/primitives/data-table.tsx | 5 +++-- .../primitives/group-label-badges.tsx | 3 ++- src/components/primitives/label-chip.tsx | 7 ++++++- src/components/primitives/unset.tsx | 2 +- src/components/shell/page-bar.tsx | 13 +++++++++--- src/components/shell/panel.tsx | 16 +++++++++++++-- src/features/faqs/faq-items.tsx | 5 +++-- src/features/faqs/faqs-page.tsx | 2 +- src/features/projects/project-card.tsx | 11 ++++++---- src/features/telegram/user-detail/profile.tsx | 7 +++++-- 13 files changed, 80 insertions(+), 33 deletions(-) diff --git a/docs/design.md b/docs/design.md index b82ddef3..722cd2d1 100644 --- a/docs/design.md +++ b/docs/design.md @@ -101,7 +101,8 @@ content in place. Between 760px and 1439px, the content follows the panel as it **Page header.** Scrolls with the page (the page, not the document, scrolls: the column below the top bar is the scroll container). `h1` 24px (22px on mobile) with the context line in 14px muted, actions on the right at 40px; below -760px the actions take the full width under the title. The toolbar row follows 20px below. `/` focuses the page +760px the actions take the full width under the title. The toolbar row follows 20px below. Below 760px the search takes its own row and a segmented filter fills the rest of the row it shares (scrolling +sideways when its options don't fit); the `/` hint is hidden. `/` focuses the page search from anywhere; `Esc` clears it. A service with a single section (WhatsApp) shows its icon in a 44px tile next to the `h1`. @@ -113,7 +114,7 @@ the `h1`. **Small screens** (below 760px). Rail and panel leave the layout. A 56px top bar holds the logo (Overview), the service title ("PoliNetwork Admin" on Overview), Search, the theme toggle and the avatar; the service's sections follow -as horizontally scrolling pills that scroll away with the page; a 64px bottom nav lists the five services (current one +as horizontally scrolling pills that scroll away with the page, scrolled so the current one is in view; a 64px bottom nav lists the five services (current one accent with a top bar, Reports with the open-reports dot). Section pills and bottom nav are the only navigation; there is no sheet. @@ -142,7 +143,9 @@ does not move when data arrives. **List.** A `DataTable` with search and filters in the toolbar. One row height (52px), no dense mode. Columns declare a priority; from 760px the lowest hide first when the table surface narrows, never the first or the actions column. Below 760px every row is a stacked card: the first column on its own line, the others inline as 12px meta -(`mobile: "full"` gives a column its own line, `"hidden"` drops it), the actions on their own line. Give columns +(`mobile: "full"` gives a column its own line, `"hidden"` drops it), the actions on their own line. An unset value +(`Unset`) is left out of the card rather than shown as a lone dash, and copyable values keep a 36px touch target +without making their line taller. Give columns whose content length varies a `width` (the table switches to fixed layout and the title column takes the rest), so nothing jumps across search, filters and paging. Pagination shows in the table footer once rows exceed the smallest page size (20; options 20/50/100). The page resets to page 1 itself when search, filters or sort change (`DataTable` @@ -174,8 +177,9 @@ further apart, without a red border. **Overview.** A 52px search that opens the command palette; a hero card with the Telegram users total, its growth over the selected range and a TanStack Charts line (3M per day, 1Y per week, All per month; one 2px accent line, a light grid, month labels, a dashed hover guide and a tooltip with the date, total and growth), and Telegram groups, Open -reports and Active grants as links on its right (a row under it on mobile); then "Areas": one card per service whose -whole surface leads where the rail icon does, with each section as a 44px button. +reports and Active grants as links on its right (a row under it on mobile, numbers on one baseline and without the "To +review" flag); then "Areas": one card per service whose whole surface leads where the rail icon does, with each section +as a 44px button (two per row on mobile). **States.** While a route loads, its `pendingComponent` renders `PageBar`, `PageContent` and the template's skeleton (`TableSkeleton`, `CardsSkeleton`, `RecordSkeleton`, `SettingsListSkeleton`, labelled "Loading {things}…"), so nothing @@ -413,12 +417,12 @@ Rules that the templates above don't imply. - **Projects**: one column at every width, because card order is the order on the website; the whole card drags to reorder (pointer after 5px, so its buttons and links still click; touch after a 250ms press; keyboard with Space/Enter on the focused card). A grip marks the card's left edge from 760px and its border takes the hover line. - A failed reorder reverts. Mobile project links wrap. Segments replace `Count`. + A failed reorder reverts. Project links truncate at every width (the hint shows the full URL). Segments replace `Count`. - **Associations**: 17px semibold titles, IT/EN tags in a left column beside the description, and a divided footer with the public-link count left and Manage links right; edit/delete stay in the header. Links are edited in their own dialog, not inline. -- **FAQs**: the categories are a list beside the questions (icon, Italian title, count; pills below 1100px) ending in a - dashed "Add category"; the selected category's header holds its edit and delete actions and the question search. +- **FAQs**: the categories are a list beside the questions (icon, Italian title, count; pills below 1100px, running to + the screen edges on phones) ending in a dashed "Add category"; on phones questions wrap up to three lines; the selected category's header holds its edit and delete actions and the question search. - **Freshman guide**: the latest edition is a card (version, "Latest", publication date, Download PDF) above the "All editions" table. Mobile editions remain compact single rows: version, date, PDF and delete. - **Categories**: each fixed root is a card that opens it, with its first five sub-categories as chips carrying their diff --git a/src/components/primitives/chip-overflow.tsx b/src/components/primitives/chip-overflow.tsx index b2084aa3..f4de4ee0 100644 --- a/src/components/primitives/chip-overflow.tsx +++ b/src/components/primitives/chip-overflow.tsx @@ -15,6 +15,8 @@ type ChipOverflowProps = Omit, "children"> & { renderItem: (item: T) => ReactNode /** The item's full name in the overflow popover and its accessible label. */ itemLabel: (item: T) => string + /** The text the chip shows, when shorter than `itemLabel`; sizes the fit. */ + chipText?: (item: T) => string } /** Estimated chip geometry (12px DM Sans): padding and dot, average glyph width, the chip's max width. */ @@ -46,7 +48,15 @@ function fittingCount(labels: string[], max: number, width: number) { * truncating a chip to a stub, a "+n" `CountBadge` lists the remaining items in a hoverable popover that also opens * by click or keyboard. */ -export function ChipOverflow({ items, max = 2, renderItem, itemLabel, className, ...props }: ChipOverflowProps) { +export function ChipOverflow({ + items, + max = 2, + renderItem, + itemLabel, + chipText = itemLabel, + className, + ...props +}: ChipOverflowProps) { const ref = useRef(null) const [width, setWidth] = useState(null) useLayoutEffect(() => { @@ -58,10 +68,9 @@ export function ChipOverflow({ items, max = 2, renderItem, itemLabel, classNa observer.observe(element) return () => observer.disconnect() }, []) - const labels = items.map(itemLabel) - const count = width === null ? max : Math.min(max, fittingCount(labels, max, width)) + const count = width === null ? max : Math.min(max, fittingCount(items.map(chipText), max, width)) const shown = items.slice(0, count) - const rest = labels.slice(count) + const rest = items.slice(count).map(itemLabel) return (
void copy()} className={cn( - "relative block max-w-full max-shell:min-h-9 max-shell:min-w-9 cursor-pointer truncate rounded-(--pn-r-1) text-left", + // The ::after pad gives touch a 36px target without making the line taller in stacked cards. + "relative block max-w-full cursor-pointer rounded-(--pn-r-1) text-left max-shell:after:absolute max-shell:after:-inset-y-2 max-shell:after:-inset-x-1 max-shell:after:min-w-9", className )} > - {children ?? value} + {children ?? value} ) } diff --git a/src/components/primitives/data-table.tsx b/src/components/primitives/data-table.tsx index 18307c97..0b1286a7 100644 --- a/src/components/primitives/data-table.tsx +++ b/src/components/primitives/data-table.tsx @@ -118,8 +118,9 @@ const mobileCellRole = { main: "max-shell:basis-full max-shell:whitespace-normal", inline: "max-shell:min-w-0 max-shell:flex-1 max-shell:text-left max-shell:text-[14px] max-shell:font-medium", end: "max-shell:ml-auto max-shell:shrink-0", - text: "max-shell:basis-full max-shell:min-w-0 max-shell:text-[14px] max-shell:whitespace-normal max-shell:text-(--pn-fg)", - meta: "max-shell:min-w-0 max-shell:text-left max-shell:text-xs max-shell:text-(--pn-fg-muted)", + text: "max-shell:basis-full max-shell:min-w-0 max-shell:text-[14px] max-shell:whitespace-normal max-shell:text-(--pn-fg) max-shell:[&:has(>[data-unset])]:hidden", + // An unset value (`Unset`, "—") is dropped from the card instead of showing a lone dash. + meta: "max-shell:min-w-0 max-shell:text-left max-shell:text-xs max-shell:text-(--pn-fg-muted) max-shell:[&:has(>[data-unset])]:hidden", full: "max-shell:basis-full max-shell:min-w-0", hidden: "max-shell:hidden", } diff --git a/src/components/primitives/group-label-badges.tsx b/src/components/primitives/group-label-badges.tsx index 01d9a7ad..0313d6c7 100644 --- a/src/components/primitives/group-label-badges.tsx +++ b/src/components/primitives/group-label-badges.tsx @@ -4,7 +4,7 @@ import { isCategoryLabel } from "@/features/group-labels/label-tree" import type { GroupLabel } from "@/features/group-labels/types" import { ChipOverflow } from "./chip-overflow" -import { LabelChip } from "./label-chip" +import { LabelChip, labelChipText } from "./label-chip" import { labelDisplayName } from "./label-name" import { Unset } from "./unset" @@ -34,6 +34,7 @@ export function GroupLabelBadges({ labels, max = 2, hrefForLabel, renderLink, cl items={ordered} max={max} itemLabel={(label) => labelDisplayName(label.label)} + chipText={(label) => labelChipText(label.label)} renderItem={(label) => ( ) const title = labelDisplayName(label.label) - const text = isCategoryLabel(label.label) ? formatLabelCompact(label.label) : title + const text = labelChipText(label.label) return ( ) } + +/** The text a `LabelChip` shows: categories as their last two segments, other labels by display name. */ +export function labelChipText(label: string) { + return isCategoryLabel(label) ? formatLabelCompact(label) : labelDisplayName(label) +} diff --git a/src/components/primitives/unset.tsx b/src/components/primitives/unset.tsx index f15689fe..8c099f63 100644 --- a/src/components/primitives/unset.tsx +++ b/src/components/primitives/unset.tsx @@ -3,7 +3,7 @@ import { cn } from "@/lib/utils" /** The em dash that stands in for every absent value. */ export function Unset({ className }: { className?: string }) { return ( - + — ) diff --git a/src/components/shell/page-bar.tsx b/src/components/shell/page-bar.tsx index ce7c2551..6ff50876 100644 --- a/src/components/shell/page-bar.tsx +++ b/src/components/shell/page-bar.tsx @@ -306,7 +306,7 @@ export function SearchField({ value, onChange, placeholder, label, inputRef, cla autoComplete="off" spellCheck={false} data-page-search="" - className="h-10 w-full rounded-[9px] border border-(--pn-line-strong) bg-(--pn-surface) pr-10 pl-10 text-[14px] text-(--pn-fg) transition-[border-color] duration-120 placeholder:text-(--pn-fg-subtle) hover:border-(--pn-hover-line) max-shell:text-base [&::-webkit-search-cancel-button]:appearance-none" + className="h-10 w-full rounded-[9px] border border-(--pn-line-strong) bg-(--pn-surface) pr-10 pl-10 text-[14px] text-(--pn-fg) transition-[border-color] duration-120 text-ellipsis placeholder:text-(--pn-fg-subtle) hover:border-(--pn-hover-line) max-shell:text-base [&::-webkit-search-cancel-button]:appearance-none" /> {value !== "" ? (