feat(resizable): DLT-2097 add DtResizable panel layout component#1162
feat(resizable): DLT-2097 add DtResizable panel layout component#1162Joshua Hynes (hynes-dialpad) wants to merge 33 commits intostagingfrom
Conversation
Port resizable panel system from beacon-app as DtResizable. Three components (DtResizable, DtResizablePanel, DtResizableHandle), 10 composables with three-layer architecture (pure engine, state management, orchestration), drag interaction with shadow DOM for 60fps, and 63 passing tests across 4 Storybook stories. V1 of 7 vertical slices — core layout engine only.
… control (V2) - Re-add panels and spaceAllocationStrategy props to dt_resizable.vue - Wire spaceAllocationStrategy through scoped slot and defineExpose - Add 37 V2 tests covering constraint enforcement, auto-collapse rules, space allocation strategies, and programmatic control API surface - Add 3 Storybook stories: Constraints, Collapsible, Programmatic - Register V2 stories in dt_resizable.stories.js index
…ce (V3) - Add ResizableStorageAdapter interface and ResizableStoragePanelData type - Refactor useResizableStorage with localStorageAdapter(key) factory - Add :storage prop for custom adapter injection (Pinia, Vuex, IndexedDB) - Wire storageAdapter through useResizableGroup options - Custom adapter takes precedence over storageKey when both provided - 28 tests: save/load cycle, validation, corrupted data, adapter precedence - 2 stories: localStorage persistence demo, custom adapter example
…ed composable (V6)
…and package exports (V7) - Register dt_resizable, dt_resizable_panel, dt_resizable_handle in components_list.js - Export resizable components and common/composables from package index - Add Resizable entry to site-nav.json sidebar navigation - Create VuePress docs page with usage, constraints, collapsible, persistence, peek overlay, keyboard accessibility, programmatic control, and full API tables
- Extract DEFAULT_PANEL_SIZE and MIN_PANEL_SIZE_PX constants (was '50p' hardcoded in 9 locations, magic number 10 in 3) - Scope useDOMCache MutationObserver to container via observeRoot option (was observing document.body globally, invalidating on all DOM changes) - Add ref-counted cleanup for announcement DOM element (was never removed) - Remove void statements in keyboard resize callback (was suppressing unused-var warnings for no-op) - Remove dead useResizableGroupSetup export from barrel
…e dead code
- Add clampSize() and clampToTier() to constraintResolver.ts as the
single source of truth for constraint clamping. Replace 4 duplicate
implementations across useResizablePanelState, useResizableCalculations,
useResizablePanelControls, and computeLayout.
- Replace hardcoded SIZE_TOKENS map with runtime CSS custom property
resolution (reads --dt-size-{token} from getComputedStyle). Falls
back to static map in test/SSR environments. Percentages now parsed
dynamically from the 'p' suffix pattern.
- Remove dead useResizableGroupSetup (404 lines) — replaced by
useResizableGroup.ts in V1, never imported by any component.
useResizableCore.ts reduced from 623 to 195 lines.
- Fix handle class prop to accept String, Object, Array (was String-only) - Replace hardcoded 2px in focus-visible with --dt-size-200/300 tokens - Add comment explaining panelsChanged custom comparator rationale - Note storageKey/storage capture-at-mount limitation
Add missing story imports and exports for ResizableKeyboard, ResizablePeekHover, ResizablePeekButton, and ResizableOffset templates in dt_resizable.stories.js.
…t.each Replace repetitive it() blocks with parameterized it.each tables in KEYBOARD_INCREMENTS and offsetDirection test suites.
…announcements
- Add ResizableEditModeMessages interface with optional fields for all
edit mode announcements (editModeActivated, editModeDeactivated,
editModeNoHandles, panelsReset, allPanelsReset) with English defaults
- Add ResizableKeyboardMessages interface with resizeAnnouncement template
using {beforeId}, {afterId}, {beforePx}, {afterPx}, {action},
{incrementType} placeholders
- Add ariaLabel prop to DtResizableHandle for i18n override of the
default computed aria-label
- Add messages prop to DtResizable, wired through provide/inject to
both useResizableEditMode and useResizableKeyboard
- Add RESIZABLE_MESSAGES_KEY injection key for cross-component messaging
…tcuts and aria-description - Remove createInstructions() that injected a hidden DOM element with hardcoded English instructions linked via aria-describedby - Add aria-keyshortcuts attribute to handle: "Control+e" when inactive, full shortcut list when in edit mode - Add aria-description attribute with contextual hint: edit mode entry prompt when inactive, full controls summary when active - Add editModeDescription and editModeActiveDescription to ResizableEditModeMessages interface for i18n support - Update tests to verify new ARIA attributes instead of DOM element - Remove stale dt-resize-instructions cleanup from test afterEach hooks
Wiz Scan Summary
To detect these findings earlier in the dev lifecycle, try using Wiz Code VS Code Extension. |
…compat All other Dialtone component barrels use index.js (not index.ts). The TS compiler cannot resolve .vue module imports from .ts files without vue shim declarations. Renaming to .js matches the established convention and fixes the build.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 22b40c7af6
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "Codex (@codex) review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "Codex (@codex) address that feedback".
| if (globalEditMode.keydownListener === handleGlobalKeydown) { | ||
| document.removeEventListener('keydown', globalEditMode.keydownListener); | ||
| document.removeEventListener('click', handleDocumentClick, true); | ||
| globalEditMode.keydownListener = null; |
There was a problem hiding this comment.
Preserve edit-mode listeners while any resizable remains mounted
In useResizableEditMode, listener teardown is tied to function identity instead of active-instance count: if the instance that originally registered globalEditMode.keydownListener unmounts before later instances, lines 419–422 remove the global keydown/click listeners even though other DtResizable components are still mounted. In that multi-instance lifecycle (mount A, mount B, unmount A), Ctrl/Cmd+E and edit-mode navigation stop working for B until remount.
Useful? React with 👍 / 👎.
| `${beforePanelId.value}:${afterPanelId.value}`, | ||
| panels.value, | ||
| 0, | ||
| ); |
There was a problem hiding this comment.
Compute keyboard cursor from panel offset for non-first handles
Keyboard resize passes beforePanelLeft as 0 and computes newCursorPosition from only beforePanel.pixelSize, but processResizeMove expects a container-relative cursor plus the true left offset of the before panel. For any handle after the first panel in a 3+ panel layout, arrow-key resize will calculate sizes as if the pair started at x=0, causing incorrect widths/jumps and potential total-width overflow in the immediate DOM update.
Useful? React with 👍 / 👎.
| if (behavior === 'all') { | ||
| resetAllPanelPairs(); | ||
| } else { | ||
| resetSpecificPanelPair(beforePanelId, afterPanelId); | ||
| } |
There was a problem hiding this comment.
Honor resetBehavior values other than "all"
resetPanels collapses every non-all behavior (both, before, after) into the same resetSpecificPanelPair path, so DtResizableHandle’s documented resetBehavior="before"|"after" options are ignored. As implemented, those modes cannot produce different behavior from both, which breaks the advertised API contract for handle reset semantics.
Useful? React with 👍 / 👎.
The typedoc build-functions target generates "Date and Time" docs from common/*/index.js. The new common/composables/ directory (useDOMCache) is not part of that scope. Excluding it prevents potential resolution issues with .ts entry points in CI.
…ical stories The Default and Vertical stories lacked docs.source.code overrides, causing Storybook to auto-generate incorrect code showing the wrapper component tag (dt_resizable) instead of the actual template with DtResizablePanel and DtResizableHandle children.
…ult direction - Convert markdown tables to HTML tables for MDX rendering compatibility - Remove unnecessary direction="row" from code examples (it's the default)
|
Please add either the |
…over - Add d-w100p class to all story panel content divs so they fill width - Add styles for __indicator element (was unstyled in template) - Move hover/active background-color to __indicator with 150ms transition
Replace physical properties (top/left/right/bottom) with logical properties (inset-inline-start/end, inset-block, inline-size). Container sets writing-mode: vertical-lr for column direction, so all positioning rotates automatically. - Panel: inset-block: 0 + insetInlineStart/End (was top/bottom: 0 + left/right) - Handle: inset-block + inline-size (was separate row/column blocks) - Hit area: inset shorthand (was 4 physical properties) - Indicator: inset: 0 (was duplicate row/column blocks) - Panel content: writing-mode: horizontal-tb reset for normal text - Removed direction-switching JS in handle positioning
Define --dt-resizable-handle-color-surface on the handle root, defaulting to --dt-color-surface-info-strong. All hover, active, and focus-visible styles reference the component variable instead of the raw token — single place to update for theming.
resetAdjacentPanels was simulating a drag with beforePanelLeft=0, which produced wrong results for any handle that isn't the first. Now redistributes the combined space of both panels by their initial size ratio — correct for any position in the layout.
Rename dt_resizable*.vue/js/mdx → resizable*.vue/js/mdx to match
the Dialtone naming convention where filenames mirror the folder
name (e.g., badge/badge.vue, not badge/dt_badge.vue).
This enables vue-docgen-api to discover the component files via
the getValidFileList regex (^{folderName}\w*\.vue$), which powers
the auto-generated <component-vue-api> tables on the docs site.
Replace hand-written Props/Events/Slots HTML tables in the docs
page with <component-vue-api> tags for DtResizable,
DtResizablePanel, and DtResizableHandle.
Francis Rupert (francisrupert)
left a comment
There was a problem hiding this comment.
Just an initial quick response for style.
packages/dialtone-vue/components/resizable/dt_resizable_handle.vue
Outdated
Show resolved
Hide resolved
packages/dialtone-vue/components/resizable/dt_resizable_handle.vue
Outdated
Show resolved
Hide resolved
packages/dialtone-vue/components/resizable/dt_resizable_handle.vue
Outdated
Show resolved
Hide resolved
packages/dialtone-vue/components/resizable/dt_resizable_handle.vue
Outdated
Show resolved
Hide resolved
packages/dialtone-vue/components/resizable/dt_resizable_handle.vue
Outdated
Show resolved
Hide resolved
Dialtone CSS classes use d- prefix (d-modal, d-badge) while component names use Dt prefix (DtModal, DtBadge). Renamed CSS classes: d-resizable, d-resizable-panel, d-resizable-handle. Updated components_list.js to match renamed filenames.
- opacity: 0.3 → var(--dt-opacity-500) - transition: 150ms → var(--td150) - handle color: --dt-color-surface-info-strong → --dt-color-border-focus - focus-visible: simplified to background-color + box-shadow: none
Move all component styles from inline <style> blocks in Vue SFCs to packages/dialtone-css/lib/build/less/components/resizable.less. This matches the Dialtone convention where styles ship via the CSS package and are loaded by the documentation site. Includes peek overlay transition styles with --td150 token.
|
✔️ Deploy previews ready! |
Obligatory GIF (super important!)
🛠️ Type Of Change
📖 Jira Ticket
DLT-2097
Stories:
📖 Description
New
DtResizablecomponent system — a resizable panel layout ported from Beacon and genericized for Dialtone. Three components, 12 composables, 1 shared composable, 173 tests, 13 Storybook stories, and a full VuePress docs page.Components
Key Features
storageKeyfor localStorage,:storageprop for custom adapters (Pinia/Vuex/API)useDOMCacheshared composablemessagesprop with English defaultsArchitecture
computeLayout.ts) with zero Vue dependenciesuseResizableDrag.ts) — bypasses Vue reactivity for 60fps inline style writes--dt-size-*CSS custom properties at runtime (auto-syncs with token pipeline)📦 Cross-Package Impact
Note: Styles are inline in Vue SFCs (no dialtone-css changes) — this component requires JS for drag interaction; no CSS-only fallback exists.
📄 Documentation Artifacts
packages/dialtone-vue/components/resizable/vue-docgen-apiexpectsresizable.vuebut files usedt_resizable.vue— follow-up neededdocs/components/resizable.mdwith full API tablespackages/dialtone-vue/index.js💡 Context
This is an FY27 Q1 Key Result under the "Dialtone Next and Beacon Alignment" initiative (DLT-2979). The resizable panel system was originally built for Beacon's sidebar/main/peek layout. This PR ports it to Dialtone as a first-class component, genericized for any resizable panel layout use case.
The component was shaped, breadboarded, and sliced into 7 vertical increments before implementation. The shaping artifacts are in the session notes, not in this repo.
📝 Checklist
For all PRs:
For all Vue changes:
For all CSS changes:
If new component:
packages/dialtone-vue).packages/dialtone-csspackage. (N/A — styles inline in SFCs, component requires JS)apps/dialtone-documentation.common/components_list.js🔮 Next Steps
vue-docgen-apinaming convention (resizable.vuevsdt_resizable.vue) so component docs auto-populate--dt-layout-*tokens when they land on thenextbranch📷 Screenshots / GIFs
Deploy preview will show Storybook stories for all 13 variants.
🔗 Sources
beacon-app/src/components/ui/resizable/