@ -1,6 +1,7 @@
import { Settings2Icon } from "lucide-react" ;
import { Columns2Icon, Columns3Icon , InfinityIcon , type LucideIcon , Rows3Icon , Settings2Icon } from "lucide-react" ;
import { Select , SelectContent , SelectItem , SelectTrigger , SelectValue } from "@/components/ui/select" ;
import { Switch } from "@/components/ui/switch" ;
import { Tooltip , TooltipContent , TooltipTrigger } from "@/components/ui/tooltip" ;
import { MAX_COLUMNS_VALUES , type MemoMaxColumns , useView } from "@/contexts/ViewContext" ;
import { cn } from "@/lib/utils" ;
import { useTranslate } from "@/utils/i18n" ;
@ -10,8 +11,15 @@ interface Props {
className? : string ;
}
// Derived from the context's canonical value list so the two can never drift; 0 renders as ∞.
const MAX_COLUMNS_OPTIONS = MAX_COLUMNS_VALUES . map ( ( value ) = > ( { value , label : value === 0 ? "∞" : String ( value ) } ) ) ;
// Keyed by the context's canonical value list, so adding a column option forces an icon and
// wording here at compile time. The i18n param is deliberately named `n`, not `count` —
// i18next would route `count` through plural-form lookup.
const LAYOUT_OPTIONS : Record < MemoMaxColumns , { icon : LucideIcon ; key : " layout - list " | " layout - columns " | " layout - auto " } > = {
1 : { icon : Rows3Icon , key : "layout-list" } ,
2 : { icon : Columns2Icon , key : "layout-columns" } ,
3 : { icon : Columns3Icon , key : "layout-columns" } ,
0 : { icon : InfinityIcon , key : "layout-auto" } ,
} ;
function MemoDisplaySettingMenu ( { className } : Props ) {
const t = useTranslate ( ) ;
@ -28,6 +36,9 @@ function MemoDisplaySettingMenu({ className }: Props) {
setMaxColumns ,
} = useView ( ) ;
const isApplying = orderByTimeAsc !== false || timeBasis !== "create_time" || compactMode || ! linkPreview || maxColumns !== 1 ;
// Multi-column grids always render compact tiles, so the toggle is shown as on and locked
// there; it only becomes a real choice at a single column.
const compactLocked = maxColumns !== 1 ;
return (
< Popover >
@ -37,20 +48,59 @@ function MemoDisplaySettingMenu({ className }: Props) {
< PopoverContent align = "end" alignOffset = { - 12 } sideOffset = { 14 } >
< div className = "flex flex-col gap-2 p-1" >
< div className = "w-full flex flex-row justify-between items-center" >
< span className = "text-sm shrink-0 mr-3 text-foreground" > { t ( "memo.columns" ) } < / span >
{ /* Radix only reports rendered item values, so no re-validation is needed here. */ }
< Select value = { String ( maxColumns ) } onValueChange = { ( value ) = > setMaxColumns ( Number ( value ) as MemoMaxColumns ) } >
< SelectTrigger size = "sm" className = "w-32" >
< SelectValue / >
< / SelectTrigger >
< SelectContent >
{ MAX_COLUMNS_OPTIONS . map ( ( option ) = > (
< SelectItem key = { option . value } value = { String ( option . value ) } >
{ option . label }
< / SelectItem >
) ) }
< / SelectContent >
< / Select >
< span className = "text-sm shrink-0 mr-3 text-foreground" > { t ( "memo.layout" ) } < / span >
{ / * A q u i e t m u t e d t r a c k ( 2 8 p x t a l l , b o r d e r l e s s ) ; o n l y t h e a c t i v e o p t i o n c a r r i e s t h e a c c e n t
fill . A radiogroup with roving tabindex , since the options are mutually exclusive . * / }
< div
role = "radiogroup"
aria - label = { t ( "memo.layout" ) }
className = "flex items-center gap-0.5 rounded-lg bg-muted/50 p-0.5"
onKeyDown = { ( event ) = > {
const delta =
event . key === "ArrowRight" || event . key === "ArrowDown"
? 1
: event . key === "ArrowLeft" || event . key === "ArrowUp"
? - 1
: 0 ;
if ( delta === 0 ) return ;
event . preventDefault ( ) ;
const index = MAX_COLUMNS_VALUES . indexOf ( maxColumns ) ;
const next = MAX_COLUMNS_VALUES [ ( index + delta + MAX_COLUMNS_VALUES . length ) % MAX_COLUMNS_VALUES . length ] ;
setMaxColumns ( next ) ;
event . currentTarget . querySelector < HTMLButtonElement > ( ` [data-value=" ${ next } "] ` ) ? . focus ( ) ;
} }
>
{ MAX_COLUMNS_VALUES . map ( ( value ) = > {
const { icon : Icon , key } = LAYOUT_OPTIONS [ value ] ;
const label = t ( ` memo. ${ key } ` , { n : value } ) ;
const active = maxColumns === value ;
return (
< Tooltip key = { value } >
< TooltipTrigger asChild >
< button
type = "button"
role = "radio"
aria - checked = { active }
aria - label = { label }
tabIndex = { active ? 0 : - 1 }
data - value = { value }
onClick = { ( ) = > setMaxColumns ( value ) }
className = { cn (
"grid h-6 w-7 place-items-center rounded-md transition-colors" ,
active ? "bg-accent text-accent-foreground" : "text-muted-foreground/70 hover:bg-accent/50 hover:text-foreground" ,
) }
>
< Icon className = "w-3.5 h-3.5" / >
< / button >
< / TooltipTrigger >
< TooltipContent >
< p className = "font-medium" > { label } < / p >
< p className = "text-primary-foreground/70" > { t ( ` memo. ${ key } -description ` , { n : value } ) } < / p >
< / TooltipContent >
< / Tooltip >
) ;
} ) }
< / div >
< / div >
< div className = "w-full flex flex-row justify-between items-center" >
< span className = "text-sm shrink-0 mr-3 text-foreground" > { t ( "memo.shown-time" ) } < / span >
@ -83,13 +133,12 @@ function MemoDisplaySettingMenu({ className }: Props) {
< / SelectContent >
< / Select >
< / div >
{ /* Multi-column grids always render compact tiles, so the toggle only applies to a single column. */ }
{ maxColumns === 1 && (
< div className = "w-full flex flex-row justify-between items-center" >
< span className = "text-sm shrink-0 mr-3 text-foreground" > { t ( "memo.compact-mode" ) } < / span >
< Switch checked = { compactMode } onCheckedChange = { setCompactMode } / >
< span className = { cn ( "text-sm shrink-0 mr-3" , compactLocked ? "text-muted-foreground" : "text-foreground" ) } >
{ t ( "memo.compact-mode" ) }
< / span >
< Switch checked = { compactLocked || compactMode } onCheckedChange = { setCompactMode } disabled = { compactLocked } / >
< / div >
) }
< div className = "w-full flex flex-row justify-between items-center" >
< span className = "text-sm shrink-0 mr-3 text-foreground" > { t ( "memo.link-preview" ) } < / span >
< Switch checked = { linkPreview } onCheckedChange = { setLinkPreview } / >