@linzjs/windows
v9.5.6
Published
[](https://github.com/semantic-release/semantic-release)
Readme
@linzjs/windows
Note: Make sure to install oxc plugin in Intellij/VSCode for development.
Reusable promise based windowing component for LINZ / Toitū te whenua.
React state based modals/windows are painful because they require:
- shared states for open/closed.
- callbacks/states for return values.
- inline modal/window includes, which prevent you from closing the invoking component before the modal/window has completed.
This module gives you promise based modals/windows which don't require all the state based boilerplate / inline-components.
So you can simply do this in your react-app:
const result = await showModal(TestModal, { props... })
<button onClick={() => openPanel({ componentFn: () => <Panel.../>, onClose=onClose)}>
Open panel
</button>Features
- Async HTML dialog based Modals.
- Draggable and resizeable, pop-in/out Panels/Windows.
Install
npm install @linzjs/windowsor with Yarn
yarn add @linzjs/windowsDebug log
To enable debug logs set:
localStorage.setItem("@linzjs/windows.debugEnabled", "true");Demo
npm run storybookComponents
Modal (LuiModalAsync)
Promise-based async modals using the HTML <dialog> element. Show a modal, await the result, and continue.

Setup — wrap your app with the context provider once at the root:
import { LuiModalAsyncContextProvider } from '@linzjs/windows';
export const App = () => (
<LuiModalAsyncContextProvider>
<div>...the rest of your app...</div>
</LuiModalAsyncContextProvider>
);Define a modal component — extend your props with LuiModalAsyncCallback<RESULT_TYPE> to declare the return type and receive resolve:
import React, { ReactElement } from 'react';
import {
LuiModalAsync,
LuiModalAsyncButtonContinue,
LuiModalAsyncButtonDismiss,
LuiModalAsyncButtonGroup,
LuiModalAsyncCallback,
LuiModalAsyncContent,
LuiModalAsyncHeader,
LuiModalAsyncMain,
} from '@linzjs/windows';
// The modal returns a number (or undefined if dismissed/escaped)
interface TestModalProps extends LuiModalAsyncCallback<number> {
text: string;
}
const TestModal = ({ text, resolve }: TestModalProps): ReactElement => {
const doSomething = () => {
resolve(10); // close modal and return 10
};
return (
<LuiModalAsync closeOnOverlayClick={false}>
<LuiModalAsyncMain>
<LuiModalAsyncHeader title={'Generic modal'} onHelpClick={() => alert('help link')} />
<LuiModalAsyncContent>{text}</LuiModalAsyncContent>
</LuiModalAsyncMain>
<LuiModalAsyncButtonGroup>
<LuiModalAsyncButtonDismiss autofocus={true}>Dismiss</LuiModalAsyncButtonDismiss>
<LuiModalAsyncButtonContinue level={'tertiary'} onClick={doSomething}>
Continue onClick
</LuiModalAsyncButtonContinue>
<LuiModalAsyncButtonContinue value={10}>Continue resolve value</LuiModalAsyncButtonContinue>
</LuiModalAsyncButtonGroup>
</LuiModalAsync>
);
};Invoke the modal — use useShowAsyncModal, await the result:
import { useShowAsyncModal } from '@linzjs/windows';
export const TestModalUsage = () => {
// modalOwnerRef is only required if you have popout windows
const { showModal, modalOwnerRef } = useShowAsyncModal();
const showModalHandler = async () => {
const result = await showModal(
TestModal,
{ text: "I'm generic modal content" },
{ showOnAllWindows: true },
);
if (!result) return alert('Modal closed');
alert(`Modal result is: ${String(result)}`);
};
// Add modalOwnerRef so that modals work in popout windows
return (
<div ref={modalOwnerRef}>
<button onClick={() => void showModalHandler()}>Click to show the modal</button>
</div>
);
};Prefab Modal
Pre-built modals for common use cases: outline, info, warning, error, success, progress, and blocked.
| Outline | Info | Warning | Error |
|---------|------|---------|-------|
|
|
|
|
|
| Success | Progress | Blocked | Custom buttons |
|---------|----------|---------|----------------|
|
|
|
|
|
Setup — same LuiModalAsyncContextProvider as above.
Invoke — use useLuiModalPrefab:
import { useLuiModalPrefab, LuiModalDontShowSessionRemove } from '@linzjs/windows';
export const PrefabModalUsage = () => {
const { modalOwnerRef, showPrefabModal } = useLuiModalPrefab();
return (
<div ref={modalOwnerRef} style={{ display: 'flex', gap: 10 }}>
{/* Info */}
<button
onClick={() =>
void showPrefabModal({
level: 'info',
title: 'You are a fantastic person',
children: 'Keep it up!',
})
}
>
Info
</button>
{/* Warning with custom buttons */}
<button
onClick={() =>
void showPrefabModal<'delete'>({
level: 'warning',
title: 'You are about to make changes',
children: 'Are you sure that you want to make these changes?',
helpLink: 'https://www.example.com/help',
buttons: [
{ title: 'Cancel', icon: 'ic_navigate_before', value: undefined },
{ title: 'Delete the world!', icon: 'ic_delete_solid', value: 'delete', level: 'error' },
],
}).then((result) => {
alert('Warning result: ' + result);
})
}
>
Warning
</button>
{/* Warning with "don't show again" */}
<button
onClick={() =>
void showPrefabModal({
level: 'warning',
title: 'Warning',
children: 'This message can be suppressed.',
dontShowAgainSessionKey: 'userId_surveyId_modalId',
buttons: [{ title: 'Dismiss', default: true }],
})
}
>
Warning + Don't show again
</button>
{/* Error */}
<button
onClick={() =>
void showPrefabModal({
level: 'error',
title: 'Something is not right',
children: 'Maybe stop doing that',
onHelpClick: () => {},
})
}
>
Error
</button>
{/* Success */}
<button
onClick={() =>
void showPrefabModal({
level: 'success',
title: 'You are a success',
children: 'Keep succeeding!',
onHelpClick: () => {},
})
}
>
Success
</button>
{/* Progress — polledCloseCondition closes the modal automatically */}
<button
onClick={() => {
let done = false;
setTimeout(() => (done = true), 5000);
void showPrefabModal({
level: 'progress',
title: 'Signing in progress',
children: 'This modal will close in 5 seconds unless cancelled.',
buttons: [{ title: 'Cancel', value: true }],
polledCloseCondition: () => done,
}).then((result) => {
alert(result === true ? 'Cancelled' : 'Timed-out');
});
}}
>
Progress
</button>
{/* Blocked */}
<button
onClick={() =>
void showPrefabModal({
level: 'blocked',
title: 'Plan Generation blocked',
children: "This CSD is being used by 'Joe Bloggs'. Please close Plan Generation.",
buttons: [{ title: 'Return to Survey Capture' }],
})
}
>
Blocked
</button>
</div>
);
};To clear a "don't show again" session key programmatically:
import { LuiModalDontShowSessionRemove } from '@linzjs/windows';
LuiModalDontShowSessionRemove('userId_surveyId_modalId');Upload Modal
A prefab modal for file upload interactions.

Setup — same LuiModalAsyncContextProvider as above.
Invoke — use useLuiModalUpload:
import { useLuiModalUpload } from '@linzjs/windows';
export const ModalUploadUsage = () => {
const { modalOwnerRef, showUploadModal } = useLuiModalUpload();
return (
<div ref={modalOwnerRef}>
<button
onClick={() =>
void showUploadModal({
title: 'Add georeferenced image',
content: 'Once your image has been uploaded, place markers to align the image to the spatial view.',
width: 480,
fileDescription: 'image',
fileFormatText: 'Format: jpg, jpeg, png',
acceptedExtensions: ['jpg', 'jpeg', 'png'],
customFileErrorMessage: 'Incorrect file format!',
}).then((file) => {
console.log(file);
})
}
>
Upload Image...
</button>
</div>
);
};Panel
Draggable, resizeable, pop-in/pop-out panel windows. Panels are opened imperatively via OpenPanelButton or openPanel.

Setup — wrap your app with the context provider once at the root:
import { PanelsContextProvider } from '@linzjs/windows';
export const App = () => (
<PanelsContextProvider baseZIndex={500}>
<div>...the rest of your app...</div>
</PanelsContextProvider>
);Define a panel component:
import { Panel, PanelContent, PanelHeader } from '@linzjs/windows';
export const ShowPanelComponent = ({ data }: { data: number }) => (
<Panel title={`Panel demo ${data}`} size={{ width: 640, height: 'auto' }}>
<PanelHeader
helpUrl={'#help'}
icon={'ic_add_adopt'}
onHelpClick={() => alert('Help!')}
/>
<PanelContent>
{/* panel body content */}
</PanelContent>
</Panel>
);
// Panel as modal (disables popout, uses fixed height)
export const ShowPanelModalComponent = ({ data }: { data: number }) => (
<Panel title={`Panel demo ${data}`} size={{ width: 640, height: 400 }} modal={true}>
<PanelHeader icon={'ic_add_adopt'} disablePopout={true} />
<PanelContent>
{/* panel body content */}
</PanelContent>
</Panel>
);Open panels — use OpenPanelButton or the openPanel function:
import { OpenPanelButton } from '@linzjs/windows';
export const TestShowPanel = () => (
<div style={{ display: 'flex', gap: 8 }}>
{/* uniqueId prevents duplicate panels */}
<OpenPanelButton
buttonText={'Panel 1'}
testId={'panel1'}
componentFn={() => <ShowPanelComponent data={1} />}
uniqueId="panel1"
/>
<OpenPanelButton
buttonText={'Panel 2'}
testId={'panel2'}
componentFn={() => <ShowPanelComponent data={2} />}
uniqueId="panel2"
/>
<OpenPanelButton
buttonText={'Panel as a Modal'}
componentFn={() => <ShowPanelModalComponent data={3} />}
/>
{/* Panel with dynamic bounds — recalculated on resize */}
<OpenPanelButton
buttonText={'Dynamic bounds panel'}
componentFn={() => (
<ShowPanelModalComponent
data={3}
resizeable={false}
dynamicBounds={() => ({
x: window.innerWidth / 3,
y: window.innerHeight / 4,
height: 'auto',
width: window.innerWidth * 0.4,
})}
/>
)}
/>
</div>
);Panel props:
| Prop | Type | Description |
|------|------|-------------|
| title | string | Panel title bar text |
| size | { width: number, height: number \| 'auto' } | Initial panel size |
| modal | boolean | Disable dragging and popout, center on screen |
| resizeable | boolean | Allow user resizing (default true) |
| maxHeight | number | Maximum height in px |
| maxWidth | number | Maximum width in px |
| minHeight | number | Minimum height in px |
| minWidth | number | Minimum width in px |
| dynamicBounds | () => { x, y, width, height } | Callback to recalculate position/size dynamically |
Saving panel state — pass panelStateOptions to PanelsContextProvider or OpenPanelButton:
<OpenPanelButton
buttonText={'Panel'}
componentFn={() => <ShowPanelComponent data={1} />}
panelStateOptions={{
saveStateIn: 'localStorage',
saveStateKey: 'userId',
}}
/>Panel with Docking
Panels can dock into a designated PanelDock area. Add dockTo to PanelHeader and place a PanelDock in your layout.
import { OpenPanelButton, Panel, PanelContent, PanelDock, PanelHeader } from '@linzjs/windows';
import { useState } from 'react';
// Panel that can dock to the left side
export const DockablePanel = ({ data }: { data: number }) => (
<Panel title={`Panel demo ${data}`} size={{ width: 640, height: 400 }}>
<PanelHeader dockTo={'leftSide'} />
<PanelContent>
{/* panel body content */}
</PanelContent>
</Panel>
);
export const TestShowPanel = () => {
const [visible, setVisible] = useState(true);
return (
<>
<div style={{ display: 'flex', gap: 8 }}>
<OpenPanelButton buttonText={'panel'} componentFn={() => <DockablePanel data={1} />} />
<button onClick={() => setVisible(!visible)}>Toggle dock visible</button>
</div>
{/* The panel docks into this area when the user clicks the dock button */}
{visible && <PanelDock id={'leftSide'}>The Panel will dock in here</PanelDock>}
</>
);
};Tabbed Panel
Panels support tabs via the @linzjs/lui LuiTabs components. Wrap the Panel in LuiTabs and use LuiTabsGroup/LuiTabsPanelSwitch in the header.

import { LuiTabs, LuiTabsGroup, LuiTabsPanel, LuiTabsPanelSwitch } from '@linzjs/lui';
import { OpenPanelButton, Panel, PanelContent, PanelHeader } from '@linzjs/windows';
export const TabbedPanelComponent = ({ data }: { data: number }) => (
<LuiTabs defaultPanel="africa">
<Panel title={`Panel demo ${data}`} size={{ width: 640, height: 400 }}>
<PanelHeader
icon={'ic_send'}
onHelpClick={() => alert('Help!!!')}
extraLeft={
<LuiTabsGroup ariaLabel="Animals">
<LuiTabsPanelSwitch targetPanel="africa">Africa</LuiTabsPanelSwitch>
<LuiTabsPanelSwitch targetPanel="asia">Asia</LuiTabsPanelSwitch>
</LuiTabsGroup>
}
/>
<PanelContent>
<LuiTabsPanel panel="africa">
<h2>African Countries</h2>
</LuiTabsPanel>
<LuiTabsPanel panel="asia">
<h2>Asian Countries</h2>
</LuiTabsPanel>
</PanelContent>
</Panel>
</LuiTabs>
);
export const TestShowTabbedPanel = () => (
<>
<OpenPanelButton buttonText={'TestPanel 1'} componentFn={() => <TabbedPanelComponent data={1} />} />
<OpenPanelButton buttonText={'TestPanel 2'} componentFn={() => <TabbedPanelComponent data={2} />} />
</>
);Panel with Global Modal
When a modal is shown from inside a panel that has been popped out into a separate window, use showOnAllWindows: true to ensure the modal appears in the correct window.
import { useLuiModalPrefab, OpenPanelButton, Panel, PanelContent, PanelHeader } from '@linzjs/windows';
// showOnAllWindows ensures the modal appears even in popped-out windows
const PanelContents = () => {
const { showPrefabModal } = useLuiModalPrefab();
return (
<div>
<button
onClick={() =>
void showPrefabModal({
showOnAllWindows: true,
level: 'info',
title: 'You are a fantastic person',
children: 'Keep it up!',
})
}
>
Show modal
</button>
</div>
);
};
export const TestShowPanelWithGlobalModal = () => (
<>
<OpenPanelButton buttonText={'TestPanel 1'} componentFn={() => (
<Panel title={'Panel demo 1'} size={{ width: 640, height: 400 }}>
<PanelHeader icon={'ic_send'} onHelpClick={() => alert('Help!!!')} />
<PanelContent><PanelContents /></PanelContent>
</Panel>
)} />
</>
);Ribbon
A toolbar ribbon with buttons, sliders, menus, and separators. Supports horizontal and vertical orientations.

Setup — requires both LuiModalAsyncContextProvider and PanelsContextProvider:
import {
RibbonButton,
RibbonButtonLink,
RibbonButtonOpenPanel,
RibbonButtonSlider,
RibbonContainer,
RibbonSeparator,
RibbonMenu,
RibbonMenuOption,
RibbonMenuSeparator,
Panel,
PanelContent,
PanelHeader,
} from '@linzjs/windows';
import { useState } from 'react';
export const TestRibbonPanel = () => {
const [selectedItem, setSelectedItem] = useState('ic_add_rectangle');
const [loading, setLoading] = useState(false);
const [processing, setProcessing] = useState(false);
return (
<>
<button onClick={() => setLoading(!loading)}>Toggle loading</button>
<button onClick={() => setProcessing(!processing)}>Toggle processing</button>
{/* Horizontal ribbon */}
<RibbonContainer style={{ position: 'absolute', left: 220, top: 120 }}>
{/* Button that opens a panel — disabled state */}
<RibbonButtonOpenPanel
disabled
title={'Marks'}
icon={'ic_marks'}
componentFn={() => <Panel title="Marks" size={{ width: 640, height: 400 }}><PanelHeader /><PanelContent /></Panel>}
loading={loading}
/>
<RibbonSeparator />
{/* Button that opens a panel — with processing indicator */}
<RibbonButtonOpenPanel
title={'Vectors'}
icon={'ic_line_irregular'}
componentFn={() => <Panel title="Vectors" size={{ width: 640, height: 400 }}><PanelHeader /><PanelContent /></Panel>}
loading={loading}
processing={processing}
processingMessage={'Validating...'}
/>
{/* Slider button — opens a sub-panel of options below */}
<RibbonButtonSlider title={selectedItem} icon={selectedItem} alignment={'down'} loading={loading}>
<RibbonContainer orientation={'vertical'}>
<RibbonButton
title={'Rectangle'}
icon={'ic_add_rectangle'}
selected={selectedItem === 'ic_add_rectangle'}
onClick={() => setSelectedItem('ic_add_rectangle')}
/>
<RibbonButton
title={'Other'}
icon={'ic_polygon_selection'}
selected={selectedItem === 'ic_polygon_selection'}
onClick={() => setSelectedItem('ic_polygon_selection')}
/>
</RibbonContainer>
</RibbonButtonSlider>
{/* Slider button — dropdown menu with autoClose */}
<RibbonButtonSlider title={'Other'} icon={'ic_more_vert'} alignment={'down'} autoClose={true} loading={loading}>
<RibbonMenu>
<RibbonMenuOption icon={'ic_define_nonprimary_diagram_circle'}>Circle</RibbonMenuOption>
<RibbonMenuOption icon={'ic_define_nonprimary_diagram_rectangle'} disabled={true}>Rectangle</RibbonMenuOption>
<RibbonMenuSeparator />
<RibbonMenuOption>Item 3</RibbonMenuOption>
<RibbonMenuSeparator />
<RibbonMenuOption icon={'ic_clear'}>Cancel</RibbonMenuOption>
</RibbonMenu>
</RibbonButtonSlider>
</RibbonContainer>
{/* Vertical ribbon */}
<RibbonContainer orientation={'vertical'} style={{ position: 'absolute', left: 220, top: 180 }}>
<RibbonButtonOpenPanel
title={'Vectors'}
icon={'ic_timeline'}
componentFn={() => <Panel title="Vectors" size={{ width: 640, height: 400 }}><PanelHeader /><PanelContent /></Panel>}
loading={loading}
/>
{/* Slider opens to the right */}
<RibbonButtonSlider title={selectedItem} icon={selectedItem} alignment={'right'} loading={loading}>
<RibbonContainer>
<RibbonButton
title={'Rectangle'}
icon={'ic_add_rectangle'}
selected={selectedItem === 'ic_add_rectangle'}
onClick={() => setSelectedItem('ic_add_rectangle')}
/>
</RibbonContainer>
</RibbonButtonSlider>
{/* External link button */}
<RibbonButtonLink href={'https://example.com/'} icon={'ic_link'} loading={loading} />
</RibbonContainer>
</>
);
};RibbonButtonSlider alignment options: 'down' | 'up' | 'left' | 'right' | 'left-up' | 'right-up' | 'right-center'
