Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
353e3a9
feat(resizable): DLT-2097 add core resize engine (V1)
hynes-dialpad Mar 31, 2026
ea588c3
feat(resizable): DLT-2097 add keyboard accessibility and edit mode (V4)
hynes-dialpad Mar 31, 2026
514f924
feat(resizable): DLT-2097 add constraints, collapse, and programmatic…
hynes-dialpad Mar 31, 2026
29bd3e8
feat(resizable): DLT-2097 add peek overlay for collapsed panels (V5)
hynes-dialpad Mar 31, 2026
8fd70dc
feat(resizable): DLT-2097 add persistence and storage adapter interfa…
hynes-dialpad Mar 31, 2026
b2c6cf8
feat(resizable): DLT-2097 add offset positioning and useDOMCache shar…
hynes-dialpad Mar 31, 2026
356e6c1
docs(resizable): DLT-2097 add documentation, component registration, …
hynes-dialpad Mar 31, 2026
c92874b
refactor(resizable): DLT-2097 simplify after code review
hynes-dialpad Mar 31, 2026
378ad95
refactor(resizable): DLT-2097 consolidate clamping, map tokens, remov…
hynes-dialpad Mar 31, 2026
259276d
fix(resizable): DLT-2097 address code review findings
hynes-dialpad Mar 31, 2026
2af1324
fix(resizable): DLT-2097 register keyboard, peek, and offset stories
hynes-dialpad Mar 31, 2026
21f4819
test(resizable): DLT-2097 refactor keyboard and offset tests to use i…
hynes-dialpad Mar 31, 2026
fa2b031
fix(resizable): DLT-2097 add i18n message props for ARIA strings and …
hynes-dialpad Mar 31, 2026
22b40c7
fix(resizable): DLT-2097 replace createInstructions with aria-keyshor…
hynes-dialpad Mar 31, 2026
de47f22
fix(resizable): DLT-2097 rename component barrel to .js for TS build …
hynes-dialpad Mar 31, 2026
1c6e68e
fix(resizable): DLT-2097 exclude common/composables from typedoc
hynes-dialpad Mar 31, 2026
35a089d
fix(resizable): DLT-2097 add source code snippets to Default and Vert…
hynes-dialpad Mar 31, 2026
c2adf77
fix(resizable): DLT-2097 fix MDX sub-component tables and remove defa…
hynes-dialpad Mar 31, 2026
daffb99
fix(resizable): DLT-2097 add d-w100p to story panels and fix handle h…
hynes-dialpad Mar 31, 2026
133a785
fix(resizable): DLT-2097 remove dead active state CSS rule
hynes-dialpad Mar 31, 2026
0be19bc
refactor(resizable): DLT-2097 use CSS logical properties for direction
hynes-dialpad Mar 31, 2026
21647b6
fix(resizable): DLT-2097 add component-level handle color variable
hynes-dialpad Mar 31, 2026
8685b1e
fix(resizable): DLT-2097 use Top/Bottom labels for column direction s…
hynes-dialpad Mar 31, 2026
d03dc1d
fix(resizable): DLT-2097 partial reset should not clear all saved state
hynes-dialpad Mar 31, 2026
1456466
fix(resizable): DLT-2097 fix reset to use proportional redistribution
hynes-dialpad Mar 31, 2026
d1f724c
docs(resizable): DLT-2097 improve persistence docs and remove default…
hynes-dialpad Mar 31, 2026
da84d6b
refactor(resizable): DLT-2097 rename files to match Dialtone convention
hynes-dialpad Mar 31, 2026
bfb9409
docs(resizable): DLT-2097 add JSDoc descriptions to all props for vue…
hynes-dialpad Mar 31, 2026
03fa836
fix(resizable): DLT-2097 rename CSS classes from dt- to d- prefix
hynes-dialpad Mar 31, 2026
6481ee0
fix(resizable): DLT-2097 apply reviewer style suggestions
hynes-dialpad Mar 31, 2026
2f81287
refactor(resizable,css): DLT-2097 extract styles to dialtone-css
hynes-dialpad Mar 31, 2026
2d0009f
fix(resizable): DLT-2097 move close button into sidebar, show open in…
hynes-dialpad Mar 31, 2026
e0ad946
chore: DLT-2097 gitignore .claude/scheduled_tasks.lock
hynes-dialpad Mar 31, 2026
29b13cb
fix(resizable): DLT-2097 implement resetBehavior before/after modes
hynes-dialpad Apr 2, 2026
eb5e2ea
refactor(resizable): DLT-2097 replace edit mode with standard W3C sep…
hynes-dialpad Apr 2, 2026
22e9c96
test(resizable): DLT-2097 fix always-pass tests and add keyboard, dra…
hynes-dialpad Apr 2, 2026
d20c160
docs(resizable): DLT-2097 update keyboard docs to standard W3C separa…
hynes-dialpad Apr 2, 2026
cfd21ba
fix(resizable): DLT-2097 guard MutationObserver constructor for test …
hynes-dialpad Apr 2, 2026
b8863a8
refactor(resizable): DLT-2097 make computeLayout single source of truth
hynes-dialpad Apr 3, 2026
70c2573
fix(resizable): DLT-2097 beta status, DtStack examples, code-example-…
hynes-dialpad Apr 3, 2026
26fd183
fix(resizable): DLT-2097 fix double-click reset not clearing runtime …
hynes-dialpad Apr 3, 2026
0046205
fix(resizable): DLT-2097 fix persistence story storageKey override fr…
hynes-dialpad Apr 3, 2026
43f96ee
fix(resizable): DLT-2097 add hover gutter for collapsed panels with p…
hynes-dialpad Apr 3, 2026
7f1c565
fix(resizable): DLT-2097 fix peek overlay and collapsed panel gutter
hynes-dialpad Apr 3, 2026
bdd2aff
refactor(resizable): DLT-2097 remove peek overlay (Beacon-specific)
hynes-dialpad Apr 3, 2026
c949be0
refactor(resizable): DLT-2097 simplify runtime state, merge files, co…
hynes-dialpad Apr 3, 2026
8448e91
refactor(resizable): DLT-2097 O(1) panel lookup, computed ARIA, cleanup
hynes-dialpad Apr 3, 2026
0259128
refactor(resizable): DLT-2097 pointer events + DOM-order handle registry
hynes-dialpad Apr 3, 2026
8f36b13
fix(resizable): DLT-2097 fix lock/collapse panel behavior
hynes-dialpad Apr 3, 2026
6ce7994
refactor(resizable): DLT-2097 remove locked panel feature
hynes-dialpad Apr 3, 2026
a7bd179
refactor(resizable): DLT-2097 merge Programmatic story into Collapsible
hynes-dialpad Apr 3, 2026
d4f5fc5
refactor(resizable): DLT-2097 remove Custom Adapter story
hynes-dialpad Apr 3, 2026
b866fe4
refactor(resizable): DLT-2097 merge Custom Sizes into Constraints story
hynes-dialpad Apr 3, 2026
ffa258d
fix(resizable): DLT-2097 fix doc page template parsing error
hynes-dialpad Apr 3, 2026
9828923
docs(resizable): DLT-2097 replace programmatic control with handle of…
hynes-dialpad Apr 3, 2026
a791737
refactor(resizable): DLT-2097 move offset props from handle to parent
hynes-dialpad Apr 3, 2026
6b63907
refactor(resizable): DLT-2097 simplify after final review
hynes-dialpad Apr 3, 2026
0cd35e2
test(resizable): DLT-2097 fix always-passing drag test assertion
hynes-dialpad Apr 3, 2026
1bcf873
Updated pnpm-lock
hynes-dialpad Apr 3, 2026
36f26ea
Merge remote-tracking branch 'origin/staging' into feat/DLT-2097-resi…
hynes-dialpad Apr 3, 2026
0a8de10
Updated pnpm-lock
hynes-dialpad Apr 3, 2026
9e6f7fe
refactor(resizable): DLT-2097 address Brad's post-approval comments
hynes-dialpad Apr 6, 2026
8e36db4
chore: NO-JIRA update pnpm-lock
hynes-dialpad Apr 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions apps/dialtone-documentation/docs/_data/site-nav.json
Original file line number Diff line number Diff line change
Expand Up @@ -733,6 +733,10 @@
"text": "Radio Group",
"link": "/components/radio-group.html"
},
{
"text": "Resizable",
"link": "/components/resizable.html"
},
{
"text": "Rich Text Editor",
"link": "/components/rich-text-editor.html"
Expand Down
357 changes: 357 additions & 0 deletions apps/dialtone-documentation/docs/components/resizable.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,357 @@
---
title: Resizable
description: A resizable layout component that allows users to resize adjacent panels by dragging a handle between them.
status: ready
Comment thread
francisrupert marked this conversation as resolved.
Outdated
thumb: true
storybook: https://dialtone.dialpad.com/vue/?path=/story/components-resizable--default
---

## Usage

The resizable component system consists of three parts: `DtResizable` (the group container), `DtResizablePanel` (the resizable content areas), and `DtResizableHandle` (the draggable dividers between panels). Panels are sized using percentage tokens (e.g., `"25p"` for 25% of the container).

<dialtone-usage>
<template #do>

- Adjustable sidebar layouts (e.g., navigation + content).
- Split-view editors or file browsers.
- Any layout where the user should control how space is distributed.
</template>

<template #dont>

- Fixed layouts that should not be user-adjustable.
- Single-panel layouts with no adjacent content.
- Layouts with more than 4 panels (consider tabs or navigation instead).
</template>

</dialtone-usage>

### Basic Two-Panel Layout

```vue
Comment thread
francisrupert marked this conversation as resolved.
<dt-resizable direction="row">
<dt-resizable-panel id="sidebar" initial-size="25p">
Sidebar content
</dt-resizable-panel>
<dt-resizable-handle />
<dt-resizable-panel id="content">
Main content
</dt-resizable-panel>
</dt-resizable>
```

### Three-Panel Layout

```vue
<dt-resizable direction="row">
<dt-resizable-panel id="sidebar" initial-size="20p">
Sidebar
</dt-resizable-panel>
<dt-resizable-handle />
<dt-resizable-panel id="content">
Content
</dt-resizable-panel>
<dt-resizable-handle />
<dt-resizable-panel id="details" initial-size="25p">
Details
</dt-resizable-panel>
</dt-resizable>
```

### Vertical (Column) Direction

```vue
<dt-resizable direction="column">
<dt-resizable-panel id="top" initial-size="40p">
Top panel
</dt-resizable-panel>
<dt-resizable-handle />
<dt-resizable-panel id="bottom">
Bottom panel
</dt-resizable-panel>
</dt-resizable>
```

## Constraints

Panels support user drag constraints (`userMinSize`/`userMaxSize`) and system viewport constraints (`systemMinSize`/`systemMaxSize`). User constraints define hard limits for drag interactions. System constraints define the range the layout engine uses during viewport resizes.

```vue
<dt-resizable direction="row">
<dt-resizable-panel
id="sidebar"
initial-size="30p"
user-min-size="20p"
user-max-size="50p"
>
Sidebar (min 20%, max 50%)
</dt-resizable-panel>
<dt-resizable-handle />
<dt-resizable-panel id="content" user-min-size="30p">
Content (min 30%)
</dt-resizable-panel>
</dt-resizable>
```

### Constraint Hierarchy

| Constraint | Purpose | Applied during |
|---|---|---|
| `userMinSize` / `userMaxSize` | Hard floor/ceiling for user dragging | Drag interactions |
| `systemMinSize` / `systemMaxSize` | Scaling range for the layout engine | Viewport resizes |
| `collapseSize` | Container width threshold for auto-collapse | Container resize |

System constraints fall back to user constraints when not specified. `systemMinSize` must be >= `userMinSize`, and `systemMaxSize` must be <= `userMaxSize`.

## Collapsible Panels

Mark a panel as `collapsible` to allow it to collapse to zero width. Use the `collapsed` prop for initial state, or call `collapsePanel()` programmatically.

```vue
<dt-resizable ref="group" direction="row">
<dt-resizable-panel
id="sidebar"
initial-size="25p"
user-min-size="20p"
collapsible
:collapsed="isSidebarCollapsed"
>
Collapsible sidebar
</dt-resizable-panel>
<dt-resizable-handle />
<dt-resizable-panel id="content">
Content
</dt-resizable-panel>
</dt-resizable>
```

### Auto-Collapse Rules

Use the `collapseRules` prop on `DtResizable` to define which panels collapse first when space is constrained. Lower priority numbers collapse first.

```vue
<dt-resizable
direction="row"
:collapse-rules="[
{ panelId: 'details', priority: 1 },
{ panelId: 'sidebar', priority: 2 },
]"
>
...
</dt-resizable>
```

## Persistence

Panel sizes can be persisted across page loads. Use `storageKey` for localStorage, or provide a custom adapter via the `:storage` prop.

### localStorage (Built-in)

```vue
<dt-resizable direction="row" storage-key="my-layout">
...
</dt-resizable>
```

### Custom Storage Adapter

Implement the `ResizableStorageAdapter` interface to persist layouts with Pinia, Vuex, IndexedDB, or any other storage backend.

```js
import { localStorageAdapter } from '@dialpad/dialtone-vue';

// Built-in localStorage factory
const adapter = localStorageAdapter('my-key');

// Custom adapter
const piniaAdapter = {
save(data) { store.setLayout(data); },
load() { return store.layout; },
clear() { store.clearLayout(); },
};
```

```vue
<dt-resizable direction="row" :storage="piniaAdapter">
...
</dt-resizable>
```

When both `storageKey` and `:storage` are provided, the custom adapter takes precedence.

## Peek Overlay

Collapsed panels can show a temporary overlay on hover or button trigger. Enable with `peekEnabled` on `DtResizablePanel`.

```vue
<dt-resizable-panel
id="sidebar"
initial-size="25p"
collapsible
peek-enabled
peek-trigger="hover"
peek-width="25p"
:peek-grace-period="150"
>
<template #default="{ isCollapsed, isPeeking }">
Panel content
</template>
<template #peek-trigger="{ togglePeek, isPeeking }">
<button @click="togglePeek">Toggle peek</button>
</template>
</dt-resizable-panel>
```

## Keyboard Accessibility

The resize handle supports keyboard navigation. Press `Tab` to reach the handle, then use arrow keys to resize. The handle announces size changes to screen readers via `aria-valuenow`.

Double-click a handle to reset the adjacent panels to their initial sizes.

### Edit Mode

Handles are focusable when edit mode is active (`tabindex="0"`). In normal mode, handles have `tabindex="-1"` to keep the tab order clean. Edit mode is managed internally by the `DtResizable` component.

## Programmatic Control

Access methods via a template ref on the `DtResizable` component.

```vue
<template>
<dt-resizable ref="group" direction="row">
...
</dt-resizable>
</template>

<script setup>
import { ref } from 'vue';
const group = ref(null);

// Collapse a panel
group.value.collapsePanel('sidebar', true);

// Lock a panel at its current size
group.value.lockPanel('sidebar');

// Unlock a panel
group.value.unlockPanel('sidebar');

// Resize a panel to a specific pixel size
group.value.resizePanel('sidebar', 300);

// Reset all panels to initial sizes
group.value.resetPanels();
</script>
```

### Exposed Methods

| Method | Signature | Description |
|---|---|---|
| `resizePanel` | `(panelId: string, size: number) => void` | Resize a panel to a specific pixel size |
| `collapsePanel` | `(panelId: string, collapsed: boolean) => void` | Collapse or expand a panel |
| `lockPanel` | `(panelId: string) => void` | Lock a panel at its current size |
| `unlockPanel` | `(panelId: string) => void` | Unlock a previously locked panel |
| `resetPanels` | `(beforePanelId?, afterPanelId?, behavior?) => void` | Reset panels to initial sizes |

### Exposed Readonly State

| Property | Type | Description |
|---|---|---|
| `state` | `readonly object` | Current layout state including `panels`, `containerSize`, `isResizing` |
| `panelConfigs` | `ComputedRef<Array>` | Panel configurations from the `panels` prop |
| `allocationStrategy` | `ComputedRef<string>` | Current space allocation strategy |

## Props

### DtResizable

| Prop | Type | Default | Description |
|---|---|---|---|
| `direction` | `'row' \| 'column'` | `'row'` | Layout direction. `'row'` for horizontal, `'column'` for vertical. |
| `storageKey` | `string` | `null` | localStorage key for persisting panel sizes. |
| `storage` | `ResizableStorageAdapter` | `null` | Custom storage adapter. Overrides `storageKey` when both provided. |
| `panels` | `Array` | `[]` | Panel configurations array for programmatic initialization. |
| `spaceAllocationStrategy` | `'proportional' \| 'preserve-manual'` | `'proportional'` | Strategy for redistributing space when panels open/close. |
| `collapseRules` | `Array<CollapseRule>` | `[]` | Rules defining which panels collapse first when space is constrained. |

### DtResizablePanel

| Prop | Type | Default | Description |
|---|---|---|---|
| `id` | `string` | **required** | Unique panel identifier. |
| `initialSize` | `string` | `undefined` | Initial size as percentage token (e.g., `'25p'` for 25%). |
| `userMinSize` | `string` | `undefined` | Minimum size for user drag interactions. |
| `userMaxSize` | `string` | `undefined` | Maximum size for user drag interactions. |
| `systemMinSize` | `string` | `undefined` | Minimum size for system viewport scaling. |
| `systemMaxSize` | `string` | `undefined` | Maximum size for system viewport scaling. |
| `collapseSize` | `string` | `undefined` | Container width threshold for auto-collapse. |
| `resizable` | `boolean` | `true` | Whether this panel can be resized by dragging. |
| `collapsible` | `boolean` | `false` | Whether this panel can be collapsed. |
| `collapsed` | `boolean` | `false` | Initial collapsed state. |
| `peekEnabled` | `boolean` | `false` | Enable peek overlay when panel is collapsed. |
| `peekTrigger` | `'hover' \| 'button' \| 'both'` | `'hover'` | What triggers the peek overlay. |
| `peekWhenManual` | `boolean` | `false` | Allow peek even for manually collapsed panels. |
| `peekWidth` | `string` | `undefined` | Width of the peek overlay. Uses `initialSize` if not set. |
| `peekGracePeriod` | `number` | `150` | Grace period (ms) before hiding peek on mouse leave. |

### DtResizableHandle

| Prop | Type | Default | Description |
|---|---|---|---|
| `beforePanelId` | `string` | `null` | ID of the panel before this handle. Auto-detected if not set. |
| `afterPanelId` | `string` | `null` | ID of the panel after this handle. Auto-detected if not set. |
| `disabled` | `boolean` | `false` | Disable resize interaction for this handle. |
| `disableResetOnDoubleClick` | `boolean` | `false` | Disable the double-click reset behavior. |
| `resetBehavior` | `'both' \| 'before' \| 'after' \| 'all'` | `'both'` | Which panels to reset on double-click. |
| `offsetElement` | `string` | `undefined` | CSS selector for an element to offset the handle position. |
| `offsetAmount` | `number` | `0` | Pixel offset amount for the handle position. |
| `offsetDirection` | `'start' \| 'end' \| 'both'` | `'both'` | Direction of the offset. |

## Events

### DtResizable

| Event | Payload | Description |
|---|---|---|
| `panel-resize` | `(panelId: string, size: number)` | Emitted when a panel is resized. |
| `panel-collapse` | `(panelId: string, collapsed: boolean)` | Emitted when a panel is collapsed or expanded. |
| `resize-start` | `(handleId: string)` | Emitted when a resize drag begins. |
| `resize-end` | `(handleId: string)` | Emitted when a resize drag ends. |

## Slots

### DtResizable

| Slot | Scoped Props | Description |
|---|---|---|
| `default` | `{ panels, direction, isResizing, spaceAllocationStrategy, resizePanel, collapsePanel, startResize, stopResize }` | Container for panels and handles. |

### DtResizablePanel

| Slot | Scoped Props | Description |
|---|---|---|
| `default` | `{ panel, isCollapsed, isResizing, isPeeking }` | Panel content. |
| `peek-trigger` | `{ togglePeek, isPeeking }` | Custom trigger element for the peek overlay. |
| `peek-content` | `{ exitPeek }` | Custom content for the peek overlay. Falls back to default slot. |

## Accessibility

- The `DtResizableHandle` renders as a `role="separator"` with `aria-orientation` matching the layout direction.
- Handles expose `aria-valuenow`, `aria-valuemin`, and `aria-valuemax` indicating the size of the panel before the handle as a percentage.
- Arrow keys resize panels in increments. Larger increments are used with `Shift` held.
- `Enter` or `Space` on a handle toggles edit mode, which makes handles focusable via `Tab`.
- `Escape` exits edit mode.
- Screen reader announcements describe size changes during keyboard resize.
- Double-click on a handle resets adjacent panels to their initial sizes.

## Size Tokens

All size props accept percentage tokens with a `p` suffix. The value represents a percentage of the container size.

| Token | Meaning |
|---|---|
| `'25p'` | 25% of container |
| `'50p'` | 50% of container |
| `'100p'` | 100% of container |
3 changes: 3 additions & 0 deletions common/components_list.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ module.exports = [
'presence.vue',
'radio.vue',
'radio_group.vue',
'dt_resizable.vue',
'dt_resizable_panel.vue',
'dt_resizable_handle.vue',
'rich_text_editor.vue',
'root_layout.vue',
'scroller.vue',
Expand Down
2 changes: 2 additions & 0 deletions packages/dialtone-vue/common/composables/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { useDOMCache } from './useDOMCache';
export type { DOMCacheOptions, DOMCacheMetrics } from './useDOMCache';
Loading
Loading