# @linzjs/windows

[![semantic-release: react](https://img.shields.io/badge/semantic--release-react-e10079?logo=semantic-release)](https://github.com/semantic-release/semantic-release)

**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/windows
```

or with Yarn

```
yarn add @linzjs/windows
```

## Debug log

To enable debug logs set:

```javascript
localStorage.setItem('@linzjs/windows.debugEnabled', 'true');
```

## Demo

```bash
npm run storybook
```

---

## Components

### Modal (LuiModalAsync)

Promise-based async modals using the HTML `<dialog>` element. Show a modal, await the result, and continue.

![Generic modal example](https://github.com/matsondawson/windows-doc-images/raw/HEAD/docimages/generic-modal.png)

**Setup** — wrap your app with the context provider once at the root:

```tsx
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`:

```tsx
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:

```tsx
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                                                                                                        |
| ---------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| ![Outline modal](https://github.com/matsondawson/windows-doc-images/raw/HEAD/docimages/prefab-modal-outline.png) | ![Info modal](https://github.com/matsondawson/windows-doc-images/raw/HEAD/docimages/prefab-modal-info.png) | ![Warning modal](https://github.com/matsondawson/windows-doc-images/raw/HEAD/docimages/prefab-modal-warning.png) | ![Error modal](https://github.com/matsondawson/windows-doc-images/raw/HEAD/docimages/prefab-modal-error.png) |

| Success                                                                                                          | Progress                                                                                                           | Blocked                                                                                                          | Custom buttons                                                                                                         |
| ---------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| ![Success modal](https://github.com/matsondawson/windows-doc-images/raw/HEAD/docimages/prefab-modal-success.png) | ![Progress modal](https://github.com/matsondawson/windows-doc-images/raw/HEAD/docimages/prefab-modal-progress.png) | ![Blocked modal](https://github.com/matsondawson/windows-doc-images/raw/HEAD/docimages/prefab-modal-blocked.png) | ![Custom buttons modal](https://github.com/matsondawson/windows-doc-images/raw/HEAD/docimages/prefab-modal-custom.png) |

**Setup** — same `LuiModalAsyncContextProvider` as above.

**Invoke** — use `useLuiModalPrefab`:

```tsx
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:

```tsx
import { LuiModalDontShowSessionRemove } from '@linzjs/windows';

LuiModalDontShowSessionRemove('userId_surveyId_modalId');
```

---

### Upload Modal

A prefab modal for file upload interactions.

![Upload modal](https://github.com/matsondawson/windows-doc-images/raw/HEAD/docimages/file-upload.png)

**Setup** — same `LuiModalAsyncContextProvider` as above.

**Invoke** — use `useLuiModalUpload`:

```tsx
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`.

![Panel](https://github.com/matsondawson/windows-doc-images/raw/HEAD/docimages/show-panel.png)

**Setup** — wrap your app with the context provider once at the root:

```tsx
import { PanelsContextProvider } from '@linzjs/windows';

export const App = () => (
  <PanelsContextProvider baseZIndex={500}>
    <div>...the rest of your app...</div>
  </PanelsContextProvider>
);
```

**Define a panel component:**

```tsx
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:

```tsx
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`:

```tsx
<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.

```tsx
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.

![Tabbed panel example screenshot](https://github.com/matsondawson/windows-doc-images/raw/HEAD/docimages/panel-tabbed.png)

```tsx
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.

```tsx
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.

![ribbon-slider.png](https://github.com/matsondawson/windows-doc-images/raw/HEAD/docimages/ribbon-slider.png)
![ribbon-panel.png](https://github.com/matsondawson/windows-doc-images/raw/HEAD/docimages/ribbon-panel.png)
![ribbon-menu.png](https://github.com/matsondawson/windows-doc-images/raw/HEAD/docimages/ribbon-menu.png)
**Setup** — requires both `LuiModalAsyncContextProvider` and `PanelsContextProvider`:

```tsx
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'`
