diff --git a/docs/demos/Window.demo.contextMenu.tsx b/docs/demos/Window.demo.contextMenu.tsx new file mode 100644 index 0000000..66fcb95 --- /dev/null +++ b/docs/demos/Window.demo.contextMenu.tsx @@ -0,0 +1,160 @@ +import { Window } from '@gfazioli/mantine-window'; +import { Box, Button, Center, Menu, Text } from '@mantine/core'; +import { MantineDemo } from '@mantinex/demo'; +import { IconCopy, IconX } from '@tabler/icons-react'; +import { useState } from 'react'; + +const code = `import { useState } from 'react'; +import { Window } from '@gfazioli/mantine-window'; +import { Box, Button, Center, Menu, Text } from '@mantine/core'; +import { IconCopy, IconX } from '@tabler/icons-react'; + +type Pane = { id: number; x: number; y: number }; + +function Demo() { + const [panes, setPanes] = useState([{ id: 1, x: 40, y: 40 }]); + const [nextId, setNextId] = useState(2); + + const openPane = (x: number, y: number) => { + setPanes((current) => [...current, { id: nextId, x, y }]); + setNextId((id) => id + 1); + }; + + const closePane = (id: number) => { + setPanes((current) => current.filter((pane) => pane.id !== id)); + }; + + return ( + + {panes.length === 0 && ( +
+ +
+ )} + + {panes.map((pane) => ( + closePane(pane.id)} + > + + + + Right-click here + + The context menu actions really drive this window: duplicate it or close it. + + + + + + Window {pane.id} + } + onClick={() => openPane(pane.x + 48, pane.y + 48)} + > + Duplicate window + + + } + onClick={() => closePane(pane.id)} + > + Close window + + + + + ))} +
+ ); +} +`; + +type Pane = { id: number; x: number; y: number }; + +function Demo() { + const [panes, setPanes] = useState([{ id: 1, x: 40, y: 40 }]); + const [nextId, setNextId] = useState(2); + + const openPane = (x: number, y: number) => { + setPanes((current) => [...current, { id: nextId, x, y }]); + setNextId((id) => id + 1); + }; + + const closePane = (id: number) => { + setPanes((current) => current.filter((pane) => pane.id !== id)); + }; + + return ( + + {panes.length === 0 && ( +
+ +
+ )} + + {panes.map((pane) => ( + closePane(pane.id)} + > + + + + Right-click here + + The context menu actions really drive this window: duplicate it or close it. + + + + + + Window {pane.id} + } + onClick={() => openPane(pane.x + 48, pane.y + 48)} + > + Duplicate window + + + } + onClick={() => closePane(pane.id)} + > + Close window + + + + + ))} +
+ ); +} + +export const contextMenu: MantineDemo = { + type: 'code', + component: Demo, + code, + defaultExpanded: false, +}; diff --git a/docs/demos/Window.demo.menuBar.tsx b/docs/demos/Window.demo.menuBar.tsx new file mode 100644 index 0000000..b5e8f8a --- /dev/null +++ b/docs/demos/Window.demo.menuBar.tsx @@ -0,0 +1,310 @@ +import { Window } from '@gfazioli/mantine-window'; +import { Box, Button, Group, Menu, Paper, Stack, Text } from '@mantine/core'; +import { MantineDemo } from '@mantinex/demo'; +import { useState } from 'react'; + +const code = `import { useState } from 'react'; +import { Window } from '@gfazioli/mantine-window'; +import { Box, Button, Group, Menu, Paper, Stack, Text } from '@mantine/core'; + +const densities = { compact: 'xs', comfortable: 'sm', spacious: 'lg' } as const; + +function Demo() { + const [query, setQuery] = useState(''); + const [layers, setLayers] = useState(['Layer 1', 'Layer 2']); + const [nextLayer, setNextLayer] = useState(3); + const [lastAction, setLastAction] = useState(null); + const [showGrid, setShowGrid] = useState(true); + const [density, setDensity] = useState('comfortable'); + + // Every File command really operates on the canvas + const commands = [ + { + label: 'New layer', + action: () => { + setLayers((current) => [...current, \`Layer \${nextLayer}\`]); + setNextLayer((n) => n + 1); + }, + }, + { + label: 'Duplicate layer', + action: () => { + setLayers((current) => + current.length > 0 ? [...current, \`\${current[current.length - 1]} copy\`] : current + ); + }, + }, + { + label: 'Remove layer', + action: () => setLayers((current) => current.slice(0, -1)), + }, + { label: 'Export as PNG', action: () => {} }, + ]; + + const filtered = commands.filter((command) => + command.label.toLowerCase().includes(query.trim().toLowerCase()) + ); + + const runCommand = (command: (typeof commands)[number]) => { + command.action(); + setLastAction(command.label); + }; + + return ( + + + + {/* The menu bar */} + + + + + + + setQuery(event.currentTarget.value)} + placeholder="Search commands" + /> + {filtered.length > 0 ? ( + filtered.map((command) => ( + runCommand(command)}> + {command.label} + + )) + ) : ( + + Nothing found + + )} + + + + + + + + + Canvas + + Show grid + + + Density + setDensity(value as keyof typeof densities)} + > + Compact + Comfortable + Spacious + + + + + + {/* The canvas: layers, grid and density all react to the menus */} + + {layers.length > 0 ? ( + + {layers.map((layer, index) => ( + + {layer} + + ))} + + ) : ( + + No layers — use File → New layer + + )} + + + {/* Status bar: File menu commands land here */} + + {lastAction ? \`Last action: \${lastAction}\` : 'Run a command from the File menu'} + + + + + ); +} +`; + +const densities = { compact: 'xs', comfortable: 'sm', spacious: 'lg' } as const; + +function Demo() { + const [query, setQuery] = useState(''); + const [layers, setLayers] = useState(['Layer 1', 'Layer 2']); + const [nextLayer, setNextLayer] = useState(3); + const [lastAction, setLastAction] = useState(null); + const [showGrid, setShowGrid] = useState(true); + const [density, setDensity] = useState('comfortable'); + + const commands = [ + { + label: 'New layer', + action: () => { + setLayers((current) => [...current, `Layer ${nextLayer}`]); + setNextLayer((n) => n + 1); + }, + }, + { + label: 'Duplicate layer', + action: () => { + setLayers((current) => + current.length > 0 ? [...current, `${current[current.length - 1]} copy`] : current + ); + }, + }, + { + label: 'Remove layer', + action: () => setLayers((current) => current.slice(0, -1)), + }, + { label: 'Export as PNG', action: () => {} }, + ]; + + const filtered = commands.filter((command) => + command.label.toLowerCase().includes(query.trim().toLowerCase()) + ); + + const runCommand = (command: (typeof commands)[number]) => { + command.action(); + setLastAction(command.label); + }; + + return ( + + + + + + + + + + setQuery(event.currentTarget.value)} + placeholder="Search commands" + /> + {filtered.length > 0 ? ( + filtered.map((command) => ( + runCommand(command)}> + {command.label} + + )) + ) : ( + + Nothing found + + )} + + + + + + + + + Canvas + + Show grid + + + Density + setDensity(value as keyof typeof densities)} + > + Compact + Comfortable + Spacious + + + + + + + {layers.length > 0 ? ( + + {layers.map((layer, index) => ( + + {layer} + + ))} + + ) : ( + + No layers — use File → New layer + + )} + + + + {lastAction ? `Last action: ${lastAction}` : 'Run a command from the File menu'} + + + + + ); +} + +export const menuBar: MantineDemo = { + type: 'code', + component: Demo, + code, + defaultExpanded: false, +}; diff --git a/docs/demos/index.ts b/docs/demos/index.ts index 669bde7..e4f3123 100644 --- a/docs/demos/index.ts +++ b/docs/demos/index.ts @@ -1,6 +1,7 @@ export { callbacks } from './Window.demo.callbacks'; export { centered } from './Window.demo.centered'; export { configurator } from './Window.demo.configurator'; +export { contextMenu } from './Window.demo.contextMenu'; export { controlsPositionDemo } from './Window.demo.controlsPosition'; export { controlled } from './Window.demo.controlled'; export { controlledPosition } from './Window.demo.controlledPosition'; @@ -10,6 +11,7 @@ export { fullSizeHandles } from './Window.demo.fullSizeHandles'; export { group } from './Window.demo.group'; export { groupLayout } from './Window.demo.groupLayout'; export { layoutPresets } from './Window.demo.layoutPresets'; +export { menuBar } from './Window.demo.menuBar'; export { minMaxSize } from './Window.demo.minMaxSize'; export { mixedConstraints } from './Window.demo.mixedConstraints'; export { mixedDragBounds } from './Window.demo.mixedDragBounds'; diff --git a/docs/docs.mdx b/docs/docs.mdx index f5f8df9..d2d947f 100644 --- a/docs/docs.mdx +++ b/docs/docs.mdx @@ -166,6 +166,24 @@ You can render multiple windows in the same container. Each window maintains its +## Context Menu + +Mantine 9.3 introduces the Menu.ContextMenu component: attach desktop-style right-click +actions to any surface. Inside a Window it completes the desktop metaphor — and in this +demo the actions are real: duplicate the window or close it, with the windows rendered +dynamically from state. + + + +## Window Menu Bar + +The Menu.Search, Menu.CheckboxItem and Menu.RadioGroup components added in Mantine 9.3 +make a compact in-window menu bar straightforward. In this demo every menu entry drives +the workspace for real: the searchable File menu reports the executed command in the +status bar, while the View menu toggles the canvas grid and changes the layout density. + + + ## Window.Group Wrap multiple windows in a `Window.Group` to enable coordinated window management. The group provides: