mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-04-09 15:07:10 +02:00
Improve mobile UX with larger touch targets (#223)
- Add useIsMobile hook for viewport detection - TabBar: larger height (48px), disable reorder on mobile, hide workspace numbers - Header buttons: larger touch targets for SpellbookDropdown, UserMenu, LayoutControls - Window toolbar: larger buttons (40px) on mobile - Mosaic dividers: wider (12px) on mobile for easier dragging - CommandLauncher: larger items, footer text, hide kbd hints on mobile - Editor suggestions: responsive widths, min-height 44px touch targets - EventFooter: larger kind/relay buttons on mobile - CodeCopyButton: larger padding and icon on mobile - MembersDropdown: larger trigger and list items on mobile All changes use mobile-first Tailwind (base styles for mobile, md: for desktop) to meet Apple HIG 44px minimum touch target recommendation. Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -20,13 +20,13 @@ export function CodeCopyButton({
|
||||
return (
|
||||
<button
|
||||
onClick={onCopy}
|
||||
className={`absolute top-2 right-2 p-2 bg-background/90 hover:bg-muted border border-border rounded transition-colors ${className}`.trim()}
|
||||
className={`absolute top-2 right-2 p-3 md:p-2 bg-background/90 hover:bg-muted border border-border rounded transition-colors ${className}`.trim()}
|
||||
aria-label={label}
|
||||
>
|
||||
{copied ? (
|
||||
<CopyCheck className="size-4 text-muted-foreground" />
|
||||
<CopyCheck className="size-5 md:size-4 text-muted-foreground" />
|
||||
) : (
|
||||
<Copy className="size-4 text-muted-foreground" />
|
||||
<Copy className="size-5 md:size-4 text-muted-foreground" />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
|
||||
@@ -241,7 +241,7 @@ export default function CommandLauncher({
|
||||
</div>
|
||||
)}
|
||||
{cmd.spellCommand && (
|
||||
<div className="text-[10px] opacity-50 font-mono truncate mt-0.5">
|
||||
<div className="text-xs md:text-[10px] opacity-50 font-mono truncate mt-0.5">
|
||||
{cmd.spellCommand}
|
||||
</div>
|
||||
)}
|
||||
@@ -254,7 +254,7 @@ export default function CommandLauncher({
|
||||
</Command.List>
|
||||
|
||||
<div className="command-footer">
|
||||
<div>
|
||||
<div className="hidden md:block">
|
||||
<kbd>↑↓</kbd> navigate
|
||||
<kbd>↵</kbd> execute
|
||||
<kbd>esc</kbd> close
|
||||
|
||||
@@ -41,15 +41,17 @@ export function EventFooter({ event }: EventFooterProps) {
|
||||
{/* Left: Kind Badge */}
|
||||
<button
|
||||
onClick={handleKindClick}
|
||||
className="group flex items-center gap-1.5 cursor-crosshair hover:text-foreground transition-colors"
|
||||
className="group flex items-center gap-2 md:gap-1.5 min-h-[44px] md:min-h-0 px-1 -mx-1 cursor-crosshair hover:text-foreground transition-colors"
|
||||
title={`View documentation for kind ${event.kind}`}
|
||||
>
|
||||
<KindBadge
|
||||
kind={event.kind}
|
||||
variant="compact"
|
||||
iconClassname="text-muted-foreground group-hover:text-foreground transition-colors size-3"
|
||||
iconClassname="text-muted-foreground group-hover:text-foreground transition-colors size-4 md:size-3"
|
||||
/>
|
||||
<span className="text-[10px] leading-[10px]">{kindName}</span>
|
||||
<span className="text-xs md:text-[10px] md:leading-[10px]">
|
||||
{kindName}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{/* Right: Relay Dropdown */}
|
||||
@@ -57,11 +59,11 @@ export function EventFooter({ event }: EventFooterProps) {
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
className="flex items-center gap-1 cursor-pointer hover:text-foreground transition-colors"
|
||||
className="flex items-center gap-2 md:gap-1 min-h-[44px] md:min-h-0 px-1 -mx-1 cursor-pointer hover:text-foreground transition-colors"
|
||||
title={`Seen on ${relays.length} relay${relays.length > 1 ? "s" : ""}`}
|
||||
>
|
||||
<Wifi className="size-3" />
|
||||
<span className="text-[10px] leading-[10px]">
|
||||
<Wifi className="size-4 md:size-3" />
|
||||
<span className="text-xs md:text-[10px] md:leading-[10px]">
|
||||
{relays.length}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
@@ -110,10 +110,10 @@ export function LayoutControls() {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6"
|
||||
className="h-10 w-10 md:h-6 md:w-6"
|
||||
aria-label="Layout settings"
|
||||
>
|
||||
<SlidersHorizontal className="h-3 w-3 text-muted-foreground" />
|
||||
<SlidersHorizontal className="h-5 w-5 md:h-3 md:w-3 text-muted-foreground" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-64">
|
||||
|
||||
@@ -285,7 +285,7 @@ export function SpellbookDropdown() {
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className={cn(
|
||||
"h-7 px-2 gap-1.5 text-muted-foreground hover:text-foreground",
|
||||
"h-10 md:h-7 px-2 gap-1.5 text-muted-foreground hover:text-foreground",
|
||||
activeSpellbook && "text-foreground font-medium",
|
||||
isTemporary && "ring-1 ring-amber-500/50",
|
||||
)}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { LayoutControls } from "./LayoutControls";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Reorder, useDragControls } from "framer-motion";
|
||||
import { Workspace } from "@/types/app";
|
||||
import { useIsMobile } from "@/hooks/useIsMobile";
|
||||
|
||||
interface TabItemProps {
|
||||
ws: Workspace;
|
||||
@@ -17,6 +18,7 @@ interface TabItemProps {
|
||||
saveLabel: () => void;
|
||||
setActiveWorkspace: (id: string) => void;
|
||||
startEditing: (id: string, label?: string) => void;
|
||||
isMobile: boolean;
|
||||
}
|
||||
|
||||
function TabItem({
|
||||
@@ -29,6 +31,7 @@ function TabItem({
|
||||
saveLabel,
|
||||
setActiveWorkspace,
|
||||
startEditing,
|
||||
isMobile,
|
||||
}: TabItemProps) {
|
||||
const dragControls = useDragControls();
|
||||
|
||||
@@ -38,7 +41,7 @@ function TabItem({
|
||||
value={ws}
|
||||
dragListener={false}
|
||||
dragControls={dragControls}
|
||||
whileDrag={{ scale: 1.05 }}
|
||||
whileDrag={isMobile ? undefined : { scale: 1.05 }}
|
||||
transition={{ type: "spring", stiffness: 500, damping: 30 }}
|
||||
className={cn(
|
||||
"flex items-center justify-center cursor-default outline-none",
|
||||
@@ -48,14 +51,14 @@ function TabItem({
|
||||
// Render input field when editing
|
||||
<div
|
||||
className={cn(
|
||||
"px-3 py-1 text-xs font-mono rounded flex items-center gap-2 flex-shrink-0",
|
||||
"px-3 py-2 md:py-1 text-sm md:text-xs font-mono rounded flex items-center gap-2 flex-shrink-0",
|
||||
isActive
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "bg-muted text-foreground",
|
||||
)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<span>{ws.number}</span>
|
||||
<span className="hidden md:inline">{ws.number}</span>
|
||||
<input
|
||||
type="text"
|
||||
value={editingLabel}
|
||||
@@ -73,28 +76,35 @@ function TabItem({
|
||||
// Render button when not editing
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center gap-0 px-1 py-0.5 text-xs font-mono rounded transition-colors whitespace-nowrap flex-shrink-0 group",
|
||||
"flex items-center gap-0 px-3 py-2 md:px-1 md:py-0.5 text-sm md:text-xs font-mono rounded transition-colors whitespace-nowrap flex-shrink-0 group",
|
||||
isActive
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "text-muted-foreground hover:text-foreground hover:bg-muted",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
onPointerDown={(e) => dragControls.start(e)}
|
||||
className="cursor-grab active:cursor-grabbing p-1 hover:bg-black/10 rounded flex items-center justify-center"
|
||||
>
|
||||
<GripVertical className="h-3 w-3 opacity-50 group-hover:opacity-100 transition-opacity" />
|
||||
</div>
|
||||
{/* Hide drag handle on mobile - reordering disabled */}
|
||||
{!isMobile && (
|
||||
<div
|
||||
onPointerDown={(e) => dragControls.start(e)}
|
||||
className="cursor-grab active:cursor-grabbing p-1 hover:bg-black/10 rounded flex items-center justify-center"
|
||||
>
|
||||
<GripVertical className="h-3 w-3 opacity-50 group-hover:opacity-100 transition-opacity" />
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
onClick={() => setActiveWorkspace(ws.id)}
|
||||
onDoubleClick={() => startEditing(ws.id, ws.label)}
|
||||
className="flex items-center gap-2 px-1 py-0.5 cursor-pointer"
|
||||
>
|
||||
<span>{ws.number}</span>
|
||||
{ws.label && ws.label.trim() && (
|
||||
{/* Hide workspace number on mobile - only useful for keyboard shortcuts */}
|
||||
<span className="hidden md:inline">{ws.number}</span>
|
||||
{ws.label && ws.label.trim() ? (
|
||||
<span style={{ width: `${ws.label.trim().length || 0}ch` }}>
|
||||
{ws.label.trim()}
|
||||
</span>
|
||||
) : (
|
||||
/* Show number as fallback on mobile when no label */
|
||||
<span className="md:hidden">{ws.number}</span>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
@@ -113,6 +123,7 @@ export function TabBar() {
|
||||
reorderWorkspaces,
|
||||
} = useGrimoire();
|
||||
const { workspaces, activeWorkspaceId } = state;
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
// State for inline label editing
|
||||
const [editingId, setEditingId] = useState<string | null>(null);
|
||||
@@ -195,12 +206,16 @@ export function TabBar() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="h-8 border-t border-border bg-background flex items-center px-2 gap-1 overflow-x-auto no-scrollbar">
|
||||
<div className="h-12 md:h-8 border-t border-border bg-background flex items-center px-2 gap-1 overflow-x-auto no-scrollbar">
|
||||
{/* Left side: Workspace tabs + new workspace button */}
|
||||
<Reorder.Group
|
||||
axis="x"
|
||||
values={sortedWorkspaces}
|
||||
onReorder={(newOrder) => reorderWorkspaces(newOrder.map((w) => w.id))}
|
||||
onReorder={
|
||||
isMobile
|
||||
? () => {} // No-op on mobile - reordering disabled
|
||||
: (newOrder) => reorderWorkspaces(newOrder.map((w) => w.id))
|
||||
}
|
||||
className="flex items-center gap-1 flex-nowrap list-none p-0 m-0"
|
||||
>
|
||||
{sortedWorkspaces.map((ws) => (
|
||||
@@ -215,6 +230,7 @@ export function TabBar() {
|
||||
saveLabel={saveLabel}
|
||||
setActiveWorkspace={setActiveWorkspace}
|
||||
startEditing={startEditing}
|
||||
isMobile={isMobile}
|
||||
/>
|
||||
))}
|
||||
</Reorder.Group>
|
||||
@@ -222,11 +238,11 @@ export function TabBar() {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 ml-1 flex-shrink-0"
|
||||
className="h-10 w-10 md:h-6 md:w-6 ml-1 flex-shrink-0"
|
||||
onClick={handleNewTab}
|
||||
aria-label="Create new workspace"
|
||||
>
|
||||
<Plus className="h-3 w-3" />
|
||||
<Plus className="h-5 w-5 md:h-3 md:w-3" />
|
||||
</Button>
|
||||
|
||||
{/* Spacer to push right side controls to the end */}
|
||||
|
||||
@@ -148,12 +148,12 @@ export function WindowToolbar({
|
||||
<Button
|
||||
variant="link"
|
||||
size="icon"
|
||||
className="text-muted-foreground"
|
||||
className="h-10 w-10 md:h-9 md:w-9 text-muted-foreground"
|
||||
onClick={handleEdit}
|
||||
title="Edit command"
|
||||
aria-label="Edit command"
|
||||
>
|
||||
<Pencil className="size-4" />
|
||||
<Pencil className="size-5 md:size-4" />
|
||||
</Button>
|
||||
|
||||
{/* Copy button for NIPs */}
|
||||
@@ -161,13 +161,17 @@ export function WindowToolbar({
|
||||
<Button
|
||||
variant="link"
|
||||
size="icon"
|
||||
className="text-muted-foreground"
|
||||
className="h-10 w-10 md:h-9 md:w-9 text-muted-foreground"
|
||||
onClick={handleCopyNip}
|
||||
title="Copy NIP markdown"
|
||||
aria-label="Copy NIP markdown"
|
||||
disabled={!nipContent}
|
||||
>
|
||||
{copied ? <CopyCheck /> : <Copy />}
|
||||
{copied ? (
|
||||
<CopyCheck className="size-5 md:size-4" />
|
||||
) : (
|
||||
<Copy className="size-5 md:size-4" />
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -177,11 +181,11 @@ export function WindowToolbar({
|
||||
<Button
|
||||
variant="link"
|
||||
size="icon"
|
||||
className="text-muted-foreground"
|
||||
className="h-10 w-10 md:h-9 md:w-9 text-muted-foreground"
|
||||
title="More actions"
|
||||
aria-label="More actions"
|
||||
>
|
||||
<MoreVertical className="size-4" />
|
||||
<MoreVertical className="size-5 md:size-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
@@ -248,12 +252,12 @@ export function WindowToolbar({
|
||||
<Button
|
||||
variant="link"
|
||||
size="icon"
|
||||
className="text-muted-foreground"
|
||||
className="h-10 w-10 md:h-9 md:w-9 text-muted-foreground"
|
||||
onClick={onClose}
|
||||
title="Close window"
|
||||
aria-label="Close window"
|
||||
>
|
||||
<X className="size-4" />
|
||||
<X className="size-5 md:size-4" />
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -21,9 +21,9 @@ export function MembersDropdown({ participants }: MembersDropdownProps) {
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button className="flex items-center gap-1 text-muted-foreground hover:text-foreground transition-colors">
|
||||
<button className="flex items-center gap-1 px-1 text-muted-foreground hover:text-foreground transition-colors">
|
||||
<Users2 className="size-3" />
|
||||
<span>{participants.length}</span>
|
||||
<span className="text-xs">{participants.length}</span>
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-64">
|
||||
|
||||
@@ -5,6 +5,14 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.grimoire-command-launcher {
|
||||
/* Use width with calc to stay centered (dialog uses translate-x-50%) */
|
||||
width: calc(100% - 32px);
|
||||
max-width: calc(100% - 32px);
|
||||
}
|
||||
}
|
||||
|
||||
.grimoire-command-content {
|
||||
width: 100%;
|
||||
}
|
||||
@@ -84,13 +92,19 @@
|
||||
}
|
||||
|
||||
.command-item {
|
||||
padding: 10px 12px;
|
||||
padding: 14px 16px;
|
||||
cursor: pointer;
|
||||
transition: all 0.1s ease;
|
||||
margin-bottom: 2px;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.command-item {
|
||||
padding: 10px 12px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Inverted selection - terminal style */
|
||||
.command-item[aria-selected="true"] {
|
||||
background: hsl(var(--foreground));
|
||||
@@ -163,25 +177,38 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 12px;
|
||||
font-size: 14px;
|
||||
color: hsl(var(--muted-foreground));
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.command-footer {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.command-footer-status {
|
||||
color: hsl(var(--accent));
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.command-footer kbd {
|
||||
padding: 2px 6px;
|
||||
padding: 4px 10px;
|
||||
background: hsl(var(--muted));
|
||||
border: 1px solid hsl(var(--border));
|
||||
font-size: 11px;
|
||||
font-size: 13px;
|
||||
font-family: monospace;
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.command-footer kbd {
|
||||
padding: 2px 6px;
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Scrollbar styling to match terminal aesthetic */
|
||||
.command-list::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
|
||||
@@ -103,9 +103,9 @@ export const EmojiSuggestionList = forwardRef<
|
||||
<div
|
||||
ref={listRef}
|
||||
role="listbox"
|
||||
className="max-h-[240px] w-[296px] overflow-y-auto border border-border/50 bg-popover p-2 text-popover-foreground shadow-md"
|
||||
className="max-h-[280px] w-full max-w-[296px] overflow-y-auto border border-border/50 bg-popover p-2 text-popover-foreground shadow-md"
|
||||
>
|
||||
<div className="grid grid-cols-8 gap-0.5">
|
||||
<div className="grid grid-cols-6 md:grid-cols-8 gap-1 md:gap-0.5">
|
||||
{items.map((item, index) => (
|
||||
<button
|
||||
key={`${item.shortcode}-${item.source}`}
|
||||
@@ -115,20 +115,22 @@ export const EmojiSuggestionList = forwardRef<
|
||||
onClick={() => command(item)}
|
||||
onMouseEnter={() => setSelectedIndex(index)}
|
||||
className={cn(
|
||||
"flex size-8 items-center justify-center rounded transition-colors",
|
||||
"flex size-10 md:size-8 items-center justify-center rounded transition-colors",
|
||||
index === selectedIndex ? "bg-muted" : "hover:bg-muted/60",
|
||||
)}
|
||||
title={`:${item.shortcode}:`}
|
||||
>
|
||||
{item.source === "unicode" ? (
|
||||
// Unicode emoji - render as text
|
||||
<span className="text-lg leading-none">{item.url}</span>
|
||||
<span className="text-xl md:text-lg leading-none">
|
||||
{item.url}
|
||||
</span>
|
||||
) : (
|
||||
// Custom emoji - render as image
|
||||
<img
|
||||
src={item.url}
|
||||
alt={`:${item.shortcode}:`}
|
||||
className="size-6 object-contain"
|
||||
className="size-7 md:size-6 object-contain"
|
||||
loading="lazy"
|
||||
onError={(e) => {
|
||||
// Replace with fallback on error
|
||||
|
||||
@@ -81,7 +81,7 @@ export const ProfileSuggestionList = forwardRef<
|
||||
<div
|
||||
ref={listRef}
|
||||
role="listbox"
|
||||
className="max-h-[300px] w-[320px] overflow-y-auto border border-border/50 bg-popover text-popover-foreground shadow-md"
|
||||
className="max-h-[300px] w-full max-w-[320px] overflow-y-auto border border-border/50 bg-popover text-popover-foreground shadow-md"
|
||||
>
|
||||
{items.map((item, index) => (
|
||||
<button
|
||||
@@ -90,7 +90,7 @@ export const ProfileSuggestionList = forwardRef<
|
||||
aria-selected={index === selectedIndex}
|
||||
onClick={() => command(item)}
|
||||
onMouseEnter={() => setSelectedIndex(index)}
|
||||
className={`flex w-full items-center gap-3 px-3 py-2 text-left transition-colors ${
|
||||
className={`flex w-full items-center gap-3 px-3 py-3 md:py-2 min-h-[44px] text-left transition-colors ${
|
||||
index === selectedIndex ? "bg-muted/60" : "hover:bg-muted/60"
|
||||
}`}
|
||||
>
|
||||
|
||||
@@ -81,7 +81,7 @@ export const SlashCommandSuggestionList = forwardRef<
|
||||
<div
|
||||
ref={listRef}
|
||||
role="listbox"
|
||||
className="max-h-[300px] w-[320px] overflow-y-auto border border-border/50 bg-popover text-popover-foreground shadow-md"
|
||||
className="max-h-[300px] w-full max-w-[320px] overflow-y-auto border border-border/50 bg-popover text-popover-foreground shadow-md"
|
||||
>
|
||||
{items.map((item, index) => (
|
||||
<button
|
||||
@@ -90,11 +90,11 @@ export const SlashCommandSuggestionList = forwardRef<
|
||||
aria-selected={index === selectedIndex}
|
||||
onClick={() => command(item)}
|
||||
onMouseEnter={() => setSelectedIndex(index)}
|
||||
className={`flex w-full items-center gap-3 px-3 py-2 text-left transition-colors ${
|
||||
className={`flex w-full items-center gap-3 px-3 py-3 md:py-2 min-h-[44px] text-left transition-colors ${
|
||||
index === selectedIndex ? "bg-muted/60" : "hover:bg-muted/60"
|
||||
}`}
|
||||
>
|
||||
<Terminal className="size-4 flex-shrink-0 text-popover-foreground/60" />
|
||||
<Terminal className="size-5 md:size-4 flex-shrink-0 text-popover-foreground/60" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate text-sm font-medium font-mono">
|
||||
/{item.name}
|
||||
|
||||
@@ -356,6 +356,7 @@ export default function UserMenu() {
|
||||
<Button
|
||||
size="sm"
|
||||
variant="link"
|
||||
className="h-10 w-10 md:h-8 md:w-auto p-2 md:p-1"
|
||||
aria-label={account ? "User menu" : "Log in"}
|
||||
>
|
||||
{account ? (
|
||||
|
||||
28
src/hooks/useIsMobile.ts
Normal file
28
src/hooks/useIsMobile.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
const MOBILE_BREAKPOINT = 768;
|
||||
|
||||
/**
|
||||
* Hook to detect if the viewport is mobile-sized.
|
||||
* Uses matchMedia for efficient updates on resize.
|
||||
*
|
||||
* @param breakpoint - Width threshold in pixels (default: 768)
|
||||
* @returns true if viewport width is below breakpoint
|
||||
*/
|
||||
export function useIsMobile(breakpoint = MOBILE_BREAKPOINT): boolean {
|
||||
const [isMobile, setIsMobile] = useState(() =>
|
||||
typeof window !== "undefined"
|
||||
? window.matchMedia(`(max-width: ${breakpoint - 1}px)`).matches
|
||||
: false,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const mql = window.matchMedia(`(max-width: ${breakpoint - 1}px)`);
|
||||
|
||||
const handler = (e: MediaQueryListEvent) => setIsMobile(e.matches);
|
||||
mql.addEventListener("change", handler);
|
||||
return () => mql.removeEventListener("change", handler);
|
||||
}, [breakpoint]);
|
||||
|
||||
return isMobile;
|
||||
}
|
||||
@@ -457,6 +457,19 @@ body.animating-layout
|
||||
margin: -2px 0;
|
||||
}
|
||||
|
||||
/* Mobile: Wider split dividers for touch dragging */
|
||||
@media (max-width: 767px) {
|
||||
.mosaic.mosaic-blueprint-theme.mosaic-blueprint-theme .mosaic-split.-row {
|
||||
width: 12px;
|
||||
margin: 0 -6px;
|
||||
}
|
||||
|
||||
.mosaic.mosaic-blueprint-theme.mosaic-blueprint-theme .mosaic-split.-column {
|
||||
height: 12px;
|
||||
margin: -6px 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Accessibility: Focus Indicators
|
||||
========================================================================== */
|
||||
|
||||
Reference in New Issue
Block a user