npm package: @munsonlabs/normandy
Description: Mission-ready components for modern interfaces.
Latest version: 0.1.3

# @munsonlabs/normandy

> Modular, mission-ready UI components for modern web apps.
> _Deploy elite interfaces with precision and control._

---

## Overview

**Normandy** is a lightweight, flexible Vue 3 UI toolkit built for front-end developers who want **responsive**, **composable**, and **production-grade** components — without the bloat.

Inspired by its namesake, the Normandy delivers **stealthy integration**, **high performance**, and **modular design** for any mission-critical web app.

---

## Features

- Flexible and responsive modal components.
- Customisable context menus with nested options.
- Lightweight resizable panes for dynamic layouts.
- Powerful drag-and-drop functionality.
- Customisable notification system.

## Installation

Install the package via npm:

```bash
npm install @munsonlabs/normandy
```

Or via pnpm:

```bash
pnpm add @munsonlabs/normandy
```

## Documentation

The key information has been extracted and displayed below for your convenience. For detailed usage examples and API documentation, refer to the individual component README files:

### Web Component

Normandy components can also be used as web components in non-Vue applications. You can interact with them using window event listeners. Import the `./web/autoload.js` to automatically register web components or if you want more manual control `./web/index.js`

Refer to the individual component documentation for examples (these more often than not accept the same payload as the composables and are access by using the component name prefixed with `x-` e.g. `x-modal` or `x-context-menu`):

### Components
<!-- replace-start -->
<details name="documentation"><summary>Pane</summary>

## Overview

A flexible and lightweight resizable pane component for Vue 3 applications, supporting both horizontal and vertical resizing.

### Features

- Supports horizontal (`x`) and vertical (`y`) resizing.
- Customisable styles and behaviour.
- Lightweight and easy to integrate.
- State management for pane visibility and size.

### Basic Example

```vue
<script setup>
  import { Pane, PaneGroup } from '@munsonlabs/normandy'

  const panes = [
    { id: 'pane1', defaultSize: '50%' },
    { id: 'pane2', defaultSize: '50%' },
  ]
</script>

<template>
  <PaneGroup axis="x">
    <Pane v-for="pane in panes" :id="pane.id" :key="pane.id" :default-size="pane.defaultSize">
      <div>Content for {{ pane.id }}</div>
    </Pane>
  </PaneGroup>
</template>
```

### Advanced Example with State Management

```vue
<script setup>
  import { Pane, PaneGroup, useGlobalPane } from '@munsonlabs/normandy'

  const { toggle, reset } = useGlobalPane()

  function togglePanes() {
    toggle()
  }

  function resetPanes() {
    reset()
  }
</script>

<template>
  <div>
    <button @click="togglePanes">
      Toggle Panes
    </button>
    <button @click="resetPanes">
      Reset Panes
    </button>
    <PaneGroup axis="y">
      <Pane id="pane1" default-size="30%">
        <div>Pane 1 Content</div>
      </Pane>
      <Pane id="pane2" default-size="70%">
        <div>Pane 2 Content</div>
      </Pane>
    </PaneGroup>
  </div>
</template>
```

## Props

### PaneGroup

| Prop   | Type      | Default | Description                          |
|--------|-----------|---------|--------------------------------------|
| `axis` | `String`  | `'x'`   | Axis of resizing (`'x'` or `'y'`).   |
| `fill` | `Boolean` | `false` | Whether the group fills its parent.  |
| `centre` | `Boolean` | `false` | Whether to centre the panes.        |

### Pane

| Prop          | Type      | Default   | Description                                      |
|---------------|-----------|-----------|--------------------------------------------------|
| `id`          | `String`  | `null`    | Unique identifier for the pane.                 |
| `defaultSize` | `String`  | `'auto'`  | Default size of the pane (e.g., `'50%'`).       |
| `minSize`     | `String`  | `'0'`     | Minimum size of the pane.                       |
| `maxSize`     | `String`  | `'100%'`  | Maximum size of the pane.                       |
| `centre`      | `Boolean` | `false`   | Whether to centre the pane content.             |

## Customisation

You can customise the styles by overriding the CSS variables defined in the package. For example:

```css
:root {
    --rpane-background-colour: #f9f9f9;
    --rpane-handle-surface-1: #cccccc;
    --rpane-handle-width: 8px;
    --rpane-handle-border-radius: 4px;
}

@media (prefers-colour-scheme: dark) {
    :root {
        --rpane-background-colour: #1e1e1e;
        --rpane-handle-surface-1: #444444;
        --rpane-handle-width: 8px;
        --rpane-handle-border-radius: 4px;
    }
}
```

### Available CSS Variables

| Variable                      | Description                                      |
|-------------------------------|--------------------------------------------------|
| `--rpane-background-colour`   | Background colour of the pane.                   |
| `--rpane-handle-surface-1`    | Colour of the handle surface.                    |
| `--rpane-handle-width`        | Width of the handle.                             |
| `--rpane-handle-border-radius`| Border radius of the handle.                     |
</details>
<details name="documentation"><summary><span style="display:inline-flex;align-items:center;gap:8px;">
  Modal <img src="https://img.shields.io/badge/Web%20Component-ready-green?style=flat" />
</span></summary>

## Overview

A flexible and responsive modal component for Vue 3 applications, supporting both desktop and mobile layouts.

### Features

- Automatically adapts to mobile and desktop layouts.
- Configurable snap thresholds for resizing.
- Supports custom content, events, and slots.
- Lightweight and easy to integrate.

### Basic Example

```vue
<script setup>
  import { Modal } from '@munsonlabs/normandy'
  import { ref } from 'vue'

  const isModalOpen = ref(false)

  function openModal() {
    isModalOpen.value = true
  }
</script>

<template>
  <div>
    <button @click="openModal">
      Open Modal
    </button>
    <Modal v-model="isModalOpen" title="Example Modal">
      <p>This is the content of the modal.</p>
    </Modal>
  </div>
</template>
```

### Advanced Example with `useModal`

```vue
<script setup>
  import { GlobalModal, useModal } from '@munsonlabs/normandy'

  const { open } = useModal()

  function openCustomModal() {
    open({
      component: CustomComponent,
      baseProps: { title: 'Custom Modal' },
      componentProps: { message: 'Hello from the modal!' },
    })
  }
</script>

<template>
  <div>
    <button @click="openCustomModal">
      Open Custom Modal
    </button>
    <GlobalModal />
  </div>
</template>
```

### Web Component Example
```html
	<x-modal>
		<h1>Modal Content</h1>
	</x-modal>
	<button id="show-button">Show Modal</button>
	<button id="hide-button">Hide Modal</button>
	<script type="module">
		import './web/autoload.js';

		const showButton = document.getElementById('show-button');
		const hideButton = document.getElementById('hide-button');

		showButton.addEventListener('click', () => {
			const event = new CustomEvent('modal:show');
			window.dispatchEvent(event);
		})

		hideButton.addEventListener('click', () => {
			const event = new CustomEvent('modal:hide');
			window.dispatchEvent(event);
		});
	</script>
```

## Props

| Prop            | Type       | Default   | Description                                      |
|-----------------|------------|-----------|--------------------------------------------------|
| `modelValue`    | `Boolean`  | `false`   | Controls the visibility of the modal.            |
| `title`         | `String`   | `null`    | Title of the modal.                              |
| `position`      | `String`   | `'y'`     | Position of the modal (`'x'` or `'y'`).          |
| `align`         | `String`   | `'end'`   | Alignment of the modal (`'start'` or `'end'`).   |
| `snapThreshold` | `Array`    | `[100]`   | Snap thresholds for resizing.                    |
| `closeThreshold`| `Number`   | `10`      | Threshold for closing the modal.                 |

## Slots

| Slot       | Description                                      |
|------------|--------------------------------------------------|
| `default`  | Content to display inside the modal.             |
| `dismiss`  | Custom dismiss button content.                   |

## Customisation

You can customise the styles by overriding the CSS variables defined in the package. For example:

```css
:root {
    --modal-surface-1: #ffffff;
    --modal-text-1: #333333;
    --modal-border-radius: 8px;
    --modal-shadow: 0px 4px 16px rgba(0, 0, 0, 0.1);
}

@media (prefers-colour-scheme: dark) {
    :root {
        --modal-surface-1: #1e1e1e;
        --modal-text-1: #f5f5f5;
        --modal-border-radius: 8px;
        --modal-shadow: 0px 4px 16px rgba(0, 0, 0, 0.5);
    }
}
```

### Available CSS Variables

| Variable                  | Description                                      |
|---------------------------|--------------------------------------------------|
| `--modal-surface-1`       | Background colour of the modal.                  |
| `--modal-surface-2`       | Background colour for modal header.              |
| `--modal-surface-3`       | Background colour for modal handle.              |
| `--modal-text-1`          | Primary text colour.                             |
| `--modal-text-2`          | Secondary text colour.                           |
| `--modal-shadow`          | Shadow for the modal.                            |
| `--modal-border`          | Border colour of the modal.                      |
| `--modal-min-width`       | Minimum width of the modal.                      |
| `--modal-max-width`       | Maximum width of the modal.                      |
| `--modal-border-radius`   | Border radius of the modal.                      |
| `--modal-padding`         | Padding inside the modal.                        |
| `--modal-header-padding`  | Padding for the modal header.                    |
| `--modal-button-padding`  | Padding for modal buttons.                       |
| `--modal-content-padding` | Padding for the modal content.                   |
| `--modal-font-family`     | Font family for the modal.                       |
| `--modal-font-size-1`     | Font size for modal text.                        |
| `--modal-font-size-2`     | Font size for modal titles.                      |
</details>
<details name="documentation"><summary><span style="display:inline-flex;align-items:center;gap:8px;">
  Context Menu <img src="https://img.shields.io/badge/Web%20Component-ready-green?style=flat" />
</span></summary>

## Overview

A customisable and lightweight context menu component for Vue 3 applications.

### Features

- Fully customisable styles and behaviour.
- Supports nested menus and dynamic options.
- Keyboard and mouse navigation.
- Easy integration with Vue 3.

### Basic Example

```vue
<script setup>
  import { ContextMenu, useContextMenu } from '@munsonlabs/normandy'

  const { show, hide } = useContextMenu()

  function showMenu(event) {
    show({
      event,
      attachToMouse: true,
      options: [
        { name: 'Option 1', callback: () => alert('Option 1 selected') },
        { name: 'Option 2', callback: () => alert('Option 2 selected') },
      ],
    })
  }

  function hideMenu() {
    hide() // Hides the context menu
  }
</script>

<template>
  <div>
    <button @contextmenu.prevent="showMenu($event)">
      Right Click Me
    </button>
    <button @click="hideMenu">
      Hide Menu
    </button>
    <ContextMenu />
  </div>
</template>
```

### Nested Menu Example

```vue
<script setup>
  import { ContextMenu, useContextMenu } from '@munsonlabs/normandy'

  const { show } = useContextMenu()

  function showMenu(event) {
    show({
      event,
      attachToMouse: true,
      options: [
        { name: 'Option 1', callback: () => alert('Option 1 selected') },
        {
          name: 'More Options',
          options: [
            { name: 'Sub Option 1', callback: () => alert('Sub Option 1 selected') },
            { name: 'Sub Option 2', callback: () => alert('Sub Option 2 selected') },
          ]
        },
        { name: 'Option 3', callback: () => alert('Option 3 selected') },
      ],
    })
  }
</script>

<template>
  <div>
    <button @contextmenu.prevent="showMenu($event)">
      Right Click Me
    </button>
    <ContextMenu />
  </div>
</template>
```

### Web Component Example
```html
  <x-context-menu></x-context-menu>
	<button id="show-button">Show Menu</button>
	<script type="module">
		import './web/autoload.js';

		const showButton = document.getElementById('show-button');

		showButton.addEventListener('click', (evt) => {
			const event = new CustomEvent('context-menu:show', {
        detail: {
          event: evt,
          options: [
            { name: 'Option 1', callback: () => console.log('Option 1 selected') },
            { name: 'Option 2', callback: () => console.log('Option 2 selected') },
          ],
          attachToMouse: true,
          callback: (success) => console.log('Menu closed', success),
        },
      })
			window.dispatchEvent(event);
		})
	</script>
```

### Props

| Prop           | Type       | Description                          |
|----------------|------------|--------------------------------------|
| `options`      | `Array`    | Array of menu options.               |
| `callback`     | `Function` | Callback function for menu actions.  |
| `attachToMouse`| `Boolean`  | If `true`, the menu will appear at the mouse pointer's position. |

### Events

| Event       | Description                          |
|-------------|--------------------------------------|
| `selected`  | Triggered when a menu item is selected. |

## Customisation

You can customise the styles by overriding the CSS variables defined in the package. For example:

```css
/* Light Mode */
:root {
    --context-menu-surface-1: #ffffff;
    --context-menu-surface-2: #f0f0f0;
    --context-menu-text-1: #000000;
    --context-menu-text-2: #555555;
    --context-menu-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1);
    --context-menu-border: #e0e0e0;
}

/* Dark Mode */
@media (prefers-color-scheme: dark) {
    :root {
        --context-menu-surface-1: #333333;
        --context-menu-surface-2: #444444;
        --context-menu-text-1: #ffffff;
        --context-menu-text-2: #bbbbbb;
        --context-menu-shadow: 0px 4px 6px rgba(0, 0, 0, 0.5);
        --context-menu-border: #555555;
    }
}
```

### Available CSS Variables

| Variable                        | Description                                      |
|---------------------------------|--------------------------------------------------|
| `--context-menu-surface-1`      | Background colour of the menu surface.           |
| `--context-menu-surface-2`      | Background colour for hover states.              |
| `--context-menu-text-1`         | Primary text colour.                             |
| `--context-menu-text-2`         | Secondary text colour.                           |
| `--context-menu-shadow`         | Shadow for the menu.                             |
| `--context-menu-border`         | Border colour of the menu.                       |
| `--context-menu-min-width`      | Minimum width of the menu.                      |
| `--context-menu-border-radius`  | Border radius of the menu.                      |
| `--context-menu-padding`        | Padding inside the menu.                        |
| `--context-menu-font-family`    | Font family for the menu.                       |
| `--context-menu-font-size`      | Font size of the menu text.                     |
| `--context-menu-item-padding`   | Padding for menu items.                         |
| `--context-menu-item-gap`       | Gap between menu items.                         |
| `--context-menu-content-gap`    | Gap between content elements in a menu item.    |
| `--context-menu-glyph-padding`  | Padding for glyphs in menu items.               |
| `--context-menu-overlay-colour` | Background colour for the menu overlay.          |
| `--context-menu-transition-duration` | Duration of menu transition animations.         |
| `--context-menu-transition-timing-function` | Timing function for menu transitions.           |
| `--context-menu-z-index`        | Z-index of the menu.                            |
</details>
<details name="documentation"><summary><span style="display:inline-flex;align-items:center;gap:8px;">
  Notification <img src="https://img.shields.io/badge/Web%20Component-ready-green?style=flat" />
</span></summary>

## Overview

A lightweight and customisable notification system for Vue 3 applications.

### Features

- Supports multiple notification types (e.g., success, error, info).
- Auto-dismiss functionality with customisable durations.
- Pause, resume, and manual dismissal of notifications.
- Fully customisable styles.


### Basic Example

```vue
<script setup>
  import { Notification, useNotification } from '@munsonlabs/normandy'

  const { add } = useNotification()

  function showNotification() {
    add({ message: 'This is a notification!', type: 'success' })
  }
</script>

<template>
  <div>
    <button @click="showNotification">
      Show Notification
    </button>
    <Notification />
  </div>
</template>
```

### Advanced Example with Auto-Dismiss and Custom Options

```vue
<script setup>
  import { Notification, useNotification } from '@munsonlabs/normandy'

  const { add } = useNotification()

  function showCustomNotification() {
    add({
      message: 'This notification will auto-dismiss in 5 seconds!',
      type: 'info',
      autoDismiss: true,
      remainingTime: 5000,
    })
  }
</script>

<template>
  <div>
    <button @click="showCustomNotification">
      Show Custom Notification
    </button>
    <Notification />
  </div>
</template>
```

### Web Component Example
```html
	<x-notification></x-notification>
	<button id="show-button">Show Notification</button>
	<script type="module">
		import './web/autoload.js';

		const showButton = document.getElementById('show-button');

		showButton.addEventListener('click', () => {
			const event = new CustomEvent('notification:action', {
				detail: {
          action: 'add', // add, remove, clear, pause, resume
          payload:{
            id: 1,
            message: 'This is a notification message',
            type: 'success',
            remainingTime: 3000,
            autoDismiss: true,
            forceDismiss: undefined,
          }
        }
      })
			window.dispatchEvent(event);
		});
  </script>
```


## API

### `useNotification`

#### Methods

| Method       | Description                                      |
|--------------|--------------------------------------------------|
| `add`        | Adds a new notification.                        |
| `remove`     | Removes a notification by its ID.               |
| `pause`      | Pauses all active notification timeouts.         |
| `resume`     | Resumes all paused notification timeouts.        |

#### `add` Options

| Option         | Type              | Default     | Description                                      |
|----------------|-------------------|-------------|--------------------------------------------------|
| `id`           | `String | Number` | Auto-generated | Unique identifier for the notification.         |
| `message`      | `String`          | `''`        | The message to display in the notification.     |
| `type`         | `String`          | `'info'`    | Type of notification (`'success'`, `'error'`, `'info'`, etc.). |
| `autoDismiss`  | `Boolean`         | `true`      | Whether the notification should auto-dismiss.   |
| `remainingTime`| `Number`          | `5000`      | Time in milliseconds before auto-dismiss.       |
| `forceDismiss` | `String | Number` | `undefined` | ID of a notification to dismiss before adding this one. |

## Customisation

You can customise the styles by overriding the CSS variables defined in the package. For example:

```css
:root {
    --notification-surface-1: #ffffff;
    --notification-text-1: #333333;
    --notification-border-radius: 8px;
    --notification-shadow: 0px 4px 16px rgba(0, 0, 0, 0.1);
}

@media (prefers-colour-scheme: dark) {
    :root {
        --notification-surface-1: #1e1e1e;
        --notification-text-1: #f5f5f5;
        --notification-border-radius: 8px;
        --notification-shadow: 0px 4px 16px rgba(0, 0,
  }
}
```

### Available CSS Variables

| Variable                      | Description                                      |
|-------------------------------|--------------------------------------------------|
| `--notification-surface-1`    | Background color of the notification.            |
| `--notification-text-1`       | Primary text color.                              |
| `--notification-shadow`       | Shadow for the notification.                     |
| `--notification-border-radius`| Border radius of the notification.               |
| `--notification-padding`      | Padding inside the notification.                 |
| `--notification-font-family`  | Font family for the notification.                |
| `--notification-font-size`    | Font size of the notification text.              |
| `--notification-gap`          | Gap between notifications.                       |
</details>
<details name="documentation"><summary>Draggable</summary>

## Overview

A powerful and customisable drag-and-drop library for Vue 3 applications, supporting nested structures and advanced interactions.

### Features

- Drag-and-drop support for nested structures.
- Customisable placeholders and drag previews.
- Supports keyboard and mouse interactions.
- Lightweight and easy to integrate.

## Basic Example

```vue
<script setup>
  import { Draggable } from '@munsonlabs/normandy'

  const nestedItems = ref([
    {
      id: 1,
      name: 'Parent 1',
      children: [
        { id: 2, name: 'Child 1' },
        { id: 3, name: 'Child 2' },
      ],
    },
    {
      id: 4,
      name: 'Parent 2',
      children: [
        { id: 5, name: 'Child 3' },
      ],
    },
  ])
</script>

<template>
  <Draggable v-model="nestedItems">
    <template #default="{ item }">
      <div>{{ item.name }}</div>
    </template>
  </Draggable>
</template>
```

### Advanced Example using Renderer Component

```vue
<script setup>
  import { Draggable } from '@munsonlabs/normandy'

  const dropStore = ref({
    exampleId: {
      components: [
        { id: 1, name: 'Component 1' },
        { id: 2, name: 'Component 2' },
      ],
    },
  })
  const renderer = resolveComponent('DraggableContent')
</script>

<template>
  <Draggable
    v-if="dropStore.exampleId"
    id="1"
    v-model="dropStore.exampleId.components"
    :renderer="renderer"
  />
</template>
```

## Props

| Prop         | Type       | Default   | Description                                      |
|--------------|------------|-----------|--------------------------------------------------|
| `modelValue` | `Array`    | `[]`      | The data array to be rendered and manipulated.  |
| `renderer`   | `Component`| `null`    | Custom renderer component for items.            |
| `group`      | `String`   | `null`    | Group identifier for drag-and-drop operations.  |

## Events

| Event       | Description                                      |
|-------------|--------------------------------------------------|
| `update:modelValue` | Triggered when the data array is updated. |
| `drop`      | Triggered when an item is dropped.               |

## Customisation

You can customise the styles by overriding the CSS variables defined in the package. For example:

```css
:root {
    --placeholder-surface-1: rgba(106, 127, 233, 0.274);
    --placeholder-surface-2: rgb(73, 100, 241);
    --placeholder-border-radius: 0.3em;
}

@media (prefers-colour-scheme: dark) {
    :root {
        --placeholder-surface-1: rgba(106, 127, 233, 0.274);
        --placeholder-surface-2: rgb(73, 100, 241);
        --placeholder-border-radius: 0.3em;
    }
}
```

### Available CSS Variables

| Variable                      | Description                                      |
|-------------------------------|--------------------------------------------------|
| `--placeholder-surface-1`     | Background colour of the placeholder.            |
| `--placeholder-surface-2`     | Border colour of the placeholder.                |
| `--placeholder-border-radius` | Border radius of the placeholder.                |
</details>

<!-- replace-stop -->

## Development

To contribute to this package:

1. Clone the repository.
2. Navigate to the `packages/normandy` directory.
3. Install dependencies using `npm install`.
4. Run the development server with `npm run dev`.

## License

This project is licensed under the MIT License.