@trimble-oss/moduswebcomponents
Version:
Modus Web Components is a modern, accessible UI library built with Stencil JS that provides reusable web components following Trimble's Modus design system. This updated version focuses on improved flexibility, enhanced theming options, comprehensive cust
701 lines (657 loc) • 25 kB
JavaScript
import { withActions } from "@storybook/addon-actions/decorator";
import { html } from "lit";
import { ifDefined } from "lit/directives/if-defined.js";
import { createShadowHostClass } from "../../providers/shadow-dom/shadow-host-helper";
const meta = {
title: 'Components/Utility Panel',
component: 'modus-wc-utility-panel',
args: {
expanded: false,
'push-content': true,
},
argTypes: {
'custom-class': {
control: { type: 'text' },
},
expanded: {
control: { type: 'boolean' },
},
'push-content': {
control: { type: 'boolean' },
},
},
decorators: [
withActions,
(story) => {
// Set up targetElement after story renders
requestAnimationFrame(() => {
const panels = document.querySelectorAll('modus-wc-utility-panel');
panels.forEach((panel) => {
const container = panel.closest('.demo-container');
if (container) {
const contentElement = container.querySelector('.main-content');
if (contentElement && panel) {
panel.targetElement = contentElement;
}
}
});
});
return story();
},
],
parameters: {
actions: {
handles: ['panelOpened', 'panelClosed'],
},
docs: {
description: {
component: "A utility panel component that slides in from the right side of the screen. It can either push content or display as an overlay.\n\nThe component supports `<slot>` called 'header' for panel title content, 'body' for main content area, and 'footer' for action buttons or additional controls.",
},
},
layout: 'fullscreen',
},
};
// Shared CSS styles as string for documentation
const utilityPanelStyles = `
.demo-container {
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
background: white;
}
modus-wc-navbar {
flex-shrink: 0;
}
.main-content-wrapper {
flex: 1;
overflow: hidden;
position: relative;
background: var(--modus-wc-color-base-page);
}
.main-content {
height: 100%;
padding: 20px;
background: var(--modus-wc-color-base-page);
overflow: auto;
}
.main-content p,
.modus-wc-utility-panel-body p {
margin-bottom: var(--modus-wc-spacing-2xl);
}
.modus-wc-utility-panel-header {
font-size: 18px;
font-weight: 600;
}
.modus-wc-utility-panel-body {
padding: 20px 0;
}
.modus-wc-utility-panel-footer {
display: flex;
gap: 10px;
justify-content: flex-end;
}
`;
const scriptBlock = `
<script>
requestAnimationFrame(() => {
// Select the specific container for this story
const container = document.querySelector('.demo-container');
if (container) {
// Find the specific panel and content inside this container
const panel = container.querySelector('modus-wc-utility-panel');
const contentElement = container.querySelector('.main-content');
// Link them together
if (panel && contentElement) {
panel.targetElement = contentElement;
}
}
});
const navbar = document.querySelector('modus-wc-navbar');
if (navbar) {
navbar.visibility = { user: false };
}
</script>`;
export default meta;
export const Default = {
parameters: {
docs: {
source: {
transform: () => `
<style>
${utilityPanelStyles}
</style>
<div class="demo-container">
<modus-wc-navbar id="navbar-default">
<div slot="end">
<modus-wc-tooltip content="Toggle Utility Panel" position="left">
<modus-wc-button
id="toggle-btn"
color="primary"
size="sm"
variant="outlined"
onclick="const panel = this.closest('.demo-container').querySelector('modus-wc-utility-panel'); panel.expanded = !panel.expanded"
>
<modus-wc-icon name="menu"></modus-wc-icon>
</modus-wc-button>
</modus-wc-tooltip>
</div>
</modus-wc-navbar>
<div class="main-content-wrapper">
<div id="main-content" class="main-content">
<h1>Main Content Area</h1>
<p>
This is the main content area below the navbar. When the utility
panel opens with pushContent=true, this content will be pushed to
the left.
</p>
<p>
This is an example of how the utility panel interacts with the main
content. When the panel opens with push content enabled, this area
will shift to the left to make room for the panel.
</p>
<p>
The content area maintains its full functionality while the panel is
open. Users can continue to interact with the main content while
accessing the utility panel features.
</p>
</div>
<modus-wc-utility-panel
expanded="false"
push-content="true"
>
<div slot="header" class="modus-wc-utility-panel-header">
Utility Panel Header
</div>
<div slot="body" class="modus-wc-utility-panel-body">
<p>This is the utility panel body content.</p>
<p>
You can add any content here including forms, lists, or other
components.
</p>
<modus-wc-text-input
label="Example Input"
placeholder="Enter text..."
>
</modus-wc-text-input>
</div>
<div slot="footer" class="modus-wc-utility-panel-footer">
<modus-wc-button color="tertiary" size="sm"
>Cancel</modus-wc-button
>
<modus-wc-button color="primary" size="sm">Save</modus-wc-button>
</div>
</modus-wc-utility-panel>
</div>
</div>
${scriptBlock}`,
},
},
},
render: (args) => html `
<style>
${utilityPanelStyles}
</style>
<div class="demo-container">
<modus-wc-navbar id="navbar-default" .visibility=${{ user: false }}>
<div slot="end">
<modus-wc-tooltip content="Toggle Utility Panel" position="left">
<modus-wc-button
color="primary"
size="sm"
variant="outlined"
onclick="const panel = this.closest('.demo-container').querySelector('modus-wc-utility-panel'); panel.expanded = !panel.expanded"
>
<modus-wc-icon name="menu"></modus-wc-icon>
</modus-wc-button>
</modus-wc-tooltip>
</div>
</modus-wc-navbar>
<div class="main-content-wrapper">
<div id="main-content" class="main-content">
<h1>Main Content Area</h1>
<p>
This is the main content area below the navbar. When the utility
panel opens with pushContent=true, this content will be pushed to
the left.
</p>
<p>
This is an example of how the utility panel interacts with the main
content. When the panel opens with push content enabled, this area
will shift to the left to make room for the panel.
</p>
<p>
The content area maintains its full functionality while the panel is
open. Users can continue to interact with the main content while
accessing the utility panel features.
</p>
</div>
<modus-wc-utility-panel
custom-class=${ifDefined(args['custom-class'])}
?expanded=${args.expanded}
?push-content=${args['push-content']}
>
<div slot="header" class="modus-wc-utility-panel-header">
Utility Panel Header
</div>
<div slot="body" class="modus-wc-utility-panel-body">
<p>This is the utility panel body content.</p>
<p>
You can add any content here including forms, lists, or other
components.
</p>
<modus-wc-text-input
label="Example Input"
placeholder="Enter text..."
>
</modus-wc-text-input>
</div>
<div slot="footer" class="modus-wc-utility-panel-footer">
<modus-wc-button color="tertiary" size="sm">Cancel</modus-wc-button>
<modus-wc-button color="primary" size="sm">Save</modus-wc-button>
</div>
</modus-wc-utility-panel>
</div>
</div>
`,
};
export const Expanded = {
args: {
expanded: true,
'push-content': true,
},
parameters: {
docs: {
source: {
transform: () => `
<style>
${utilityPanelStyles}
.modus-wc-utility-panel-body {
background: var(--modus-wc-color-base-100);
}
</style>
<div class="demo-container">
<modus-wc-navbar id="navbar-expanded">
<div slot="end">
<modus-wc-button
id="toggle-btn-expanded"
color="primary"
size="sm"
variant="outlined"
onclick="const panel = document.getElementById('panel-expanded'); panel.expanded = !panel.expanded"
>
<modus-wc-icon name="menu"></modus-wc-icon>
</modus-wc-button>
</div>
</modus-wc-navbar>
<div class="main-content-wrapper">
<div id="main-content-expanded" class="main-content">
<h1>Main Content Area (Expanded Story)</h1>
<p>
This story shows the panel already expanded. The content should be
pushed to the left.
</p>
<p>
The utility panel provides quick access to additional tools and
information. It can be used for settings, filters, or any
supplementary content that enhances the main application.
</p>
</div>
<modus-wc-utility-panel
id="panel-expanded"
expanded="true"
push-content="true"
>
<div slot="header" class="modus-wc-utility-panel-header">
Expanded Panel Header
</div>
<div slot="body" class="modus-wc-utility-panel-body">
<p>This panel starts in the expanded state.</p>
</div>
<div slot="footer" class="modus-wc-utility-panel-footer">
<modus-wc-button color="tertiary" size="sm"
>Cancel</modus-wc-button
>
<modus-wc-button color="primary" size="sm">Save</modus-wc-button>
</div>
</modus-wc-utility-panel>
</div>
</div>
${scriptBlock}`,
},
},
},
render: (args) =>
// prettier-ignore
html `
<style>
${utilityPanelStyles}
.modus-wc-utility-panel-body {
padding: 20px 0;
background: var(--modus-wc-color-base-100);
}
</style>
<div class="demo-container">
<modus-wc-navbar id="navbar-expanded" .visibility=${{ user: false }}>
<div slot="end">
<modus-wc-button
color="primary"
size="sm"
variant="outlined"
onclick="const panel = this.closest('.demo-container').querySelector('modus-wc-utility-panel'); panel.expanded = !panel.expanded"
>
<modus-wc-icon name="menu"></modus-wc-icon>
</modus-wc-button>
</div>
</modus-wc-navbar>
<div class="main-content-wrapper">
<div id="main-content-expanded" class="main-content">
<h1>Main Content Area (Expanded Story)</h1>
<p>
This story shows the panel already expanded. The content should be
pushed to the left.
</p>
<p>
The utility panel provides quick access to additional tools and
information. It can be used for settings, filters, or any
supplementary content that enhances the main application.
</p>
</div>
<modus-wc-utility-panel
id="panel-expanded"
custom-class=${ifDefined(args['custom-class'])}
?expanded=${args.expanded}
?push-content=${args['push-content']}
>
<div slot="header" class="modus-wc-utility-panel-header">
Expanded Panel Header
</div>
<div slot="body" class="modus-wc-utility-panel-body">
<p>This panel starts in the expanded state.</p>
</div>
<div slot="footer" class="modus-wc-utility-panel-footer">
<modus-wc-button color="tertiary" size="sm">Cancel</modus-wc-button>
<modus-wc-button color="primary" size="sm">Save</modus-wc-button>
</div>
</modus-wc-utility-panel>
</div>
</div>
`,
};
export const OverlayMode = {
args: {
expanded: true,
'push-content': false,
},
parameters: {
docs: {
source: {
transform: () => `
<style>
${utilityPanelStyles}
</style>
<div class="demo-container">
<modus-wc-navbar id="navbar-overlay">
<div slot="end">
<modus-wc-button
id="toggle-btn-overlay"
color="primary"
size="sm"
variant="outlined"
onclick="const panel = document.getElementById('panel-overlay'); panel.expanded = !panel.expanded"
>
<modus-wc-icon name="menu"></modus-wc-icon>
</modus-wc-button>
</div>
</modus-wc-navbar>
<div class="main-content-wrapper">
<div id="main-content-overlay" class="main-content">
<h1>Main Content Area (Overlay Mode)</h1>
<p>
In overlay mode, the panel appears over the content without pushing
it.
</p>
<p>
This example demonstrates the overlay mode where the panel appears
on top of the content without pushing it aside. This is useful when
you want to preserve the layout of the main content area.
</p>
</div>
<modus-wc-utility-panel
id="panel-overlay"
expanded="true"
push-content="false"
>
<div slot="header" class="modus-wc-utility-panel-header">
Overlay Panel Header
</div>
<div slot="body" class="modus-wc-utility-panel-body">
<p>This panel overlays the content without pushing it.</p>
</div>
<div slot="footer" class="modus-wc-utility-panel-footer">
<modus-wc-button color="tertiary" size="sm"
>Cancel</modus-wc-button
>
<modus-wc-button color="primary" size="sm">Save</modus-wc-button>
</div>
</modus-wc-utility-panel>
</div>
</div>
${scriptBlock}`,
},
},
},
render: (args) => html `
<style>
${utilityPanelStyles}
</style>
<div class="demo-container">
<modus-wc-navbar id="navbar-overlay" .visibility=${{ user: false }}>
<div slot="end">
<modus-wc-button
color="primary"
size="sm"
variant="outlined"
onclick="const panel = document.getElementById('panel-overlay'); panel.expanded = !panel.expanded"
>
<modus-wc-icon name="menu"></modus-wc-icon>
</modus-wc-button>
</div>
</modus-wc-navbar>
<div class="main-content-wrapper">
<div id="main-content-overlay" class="main-content">
<h1>Main Content Area (Overlay Mode)</h1>
<p>
In overlay mode, the panel appears over the content without pushing
it.
</p>
<p>
This example demonstrates the overlay mode where the panel appears
on top of the content without pushing it aside. This is useful when
you want to preserve the layout of the main content area.
</p>
</div>
<modus-wc-utility-panel
id="panel-overlay"
custom-class=${ifDefined(args['custom-class'])}
?expanded=${args.expanded}
?push-content=${args['push-content']}
>
<div slot="header" class="modus-wc-utility-panel-header">
Overlay Panel Header
</div>
<div slot="body" class="modus-wc-utility-panel-body">
<p>This panel overlays the content without pushing it.</p>
</div>
<div slot="footer" class="modus-wc-utility-panel-footer">
<modus-wc-button color="tertiary" size="sm">Cancel</modus-wc-button>
<modus-wc-button color="primary" size="sm">Save</modus-wc-button>
</div>
</modus-wc-utility-panel>
</div>
</div>
`,
};
export const WithoutHeaderFooter = {
args: {
expanded: false,
'push-content': true,
},
render: (args) => html `
<style>
.demo-container {
height: 100vh;
position: relative;
overflow: hidden;
background: var(--modus-wc-color-base-page);
}
.main-content {
height: 100%;
padding: 20px;
background: var(--modus-wc-color-base-page);
}
</style>
<div class="demo-container">
<div id="main-content-2" class="main-content">
<h1>Main Content Area</h1>
<modus-wc-button
onclick="const panel = this.closest('.demo-container').querySelector('modus-wc-utility-panel'); panel.expanded = !panel.expanded"
>
Toggle Panel
</modus-wc-button>
</div>
<modus-wc-utility-panel
id="panel-simple"
custom-class=${ifDefined(args['custom-class'])}
?expanded=${args.expanded}
?push-content=${args['push-content']}
>
<div slot="body">
<h3>Simple Body Content</h3>
<p>This panel only has body content without header or footer.</p>
</div>
</modus-wc-utility-panel>
</div>
`,
};
export const ShadowDomParent = {
render: (args) => {
if (!customElements.get('utility-panel-shadow-host')) {
const UtilityPanelShadowHost = createShadowHostClass({
componentTag: 'modus-wc-utility-panel',
propsMapper: (v, el) => {
el.expanded = Boolean(v.expanded);
el.pushContent = Boolean(v['push-content']);
// Build full layout on first render: add slot content to el, then
// move el into the demo layout inside the helper's wrapper.
if (!el.hasAttribute('data-layout-built')) {
el.setAttribute('data-layout-built', '');
const shadowRoot = el.getRootNode();
const wrapper = el.parentElement;
// Slot content
const header = document.createElement('div');
header.setAttribute('slot', 'header');
header.className = 'modus-wc-utility-panel-header';
header.textContent = 'Utility Panel Header';
const body = document.createElement('div');
body.setAttribute('slot', 'body');
body.className = 'modus-wc-utility-panel-body';
const bp1 = document.createElement('p');
bp1.textContent = 'This is the utility panel body content.';
const bp2 = document.createElement('p');
bp2.textContent =
'You can add any content here including forms, lists, or other components.';
const input = document.createElement('modus-wc-text-input');
input.setAttribute('label', 'Example Input');
input.setAttribute('placeholder', 'Enter text...');
body.appendChild(bp1);
body.appendChild(bp2);
body.appendChild(input);
const footer = document.createElement('div');
footer.setAttribute('slot', 'footer');
footer.className = 'modus-wc-utility-panel-footer';
const cancelBtn = document.createElement('modus-wc-button');
cancelBtn.setAttribute('color', 'tertiary');
cancelBtn.setAttribute('size', 'sm');
cancelBtn.textContent = 'Cancel';
const saveBtn = document.createElement('modus-wc-button');
saveBtn.setAttribute('color', 'primary');
saveBtn.setAttribute('size', 'sm');
saveBtn.textContent = 'Save';
footer.appendChild(cancelBtn);
footer.appendChild(saveBtn);
el.appendChild(header);
el.appendChild(body);
el.appendChild(footer);
// Navbar with toggle button
const navbar = document.createElement('modus-wc-navbar');
navbar.visibility = {
user: false,
};
navbar.userCard = {
name: '',
email: '',
};
const endSlot = document.createElement('div');
endSlot.setAttribute('slot', 'end');
const tooltip = document.createElement('modus-wc-tooltip');
tooltip.setAttribute('content', 'Toggle Utility Panel');
tooltip.setAttribute('position', 'left');
const toggleBtn = document.createElement('modus-wc-button');
toggleBtn.setAttribute('color', 'primary');
toggleBtn.setAttribute('size', 'sm');
toggleBtn.setAttribute('variant', 'outlined');
const menuIcon = document.createElement('modus-wc-icon');
menuIcon.setAttribute('name', 'menu');
toggleBtn.appendChild(menuIcon);
tooltip.appendChild(toggleBtn);
endSlot.appendChild(tooltip);
navbar.appendChild(endSlot);
toggleBtn.addEventListener('buttonClick', () => {
el.expanded = !el.expanded;
});
// Main content
const mainContent = document.createElement('div');
mainContent.className = 'main-content';
const h1 = document.createElement('h1');
h1.textContent = 'Main Content Area';
const p1 = document.createElement('p');
p1.textContent =
'This is the main content area below the navbar. When the utility panel opens with pushContent=true, this content will be pushed to the left.';
const p2 = document.createElement('p');
p2.textContent =
'This is an example of how the utility panel interacts with the main content. When the panel opens with push content enabled, this area will shift to the left to make room for the panel.';
const p3 = document.createElement('p');
p3.textContent =
'The content area maintains its full functionality while the panel is open. Users can continue to interact with the main content while accessing the utility panel features.';
mainContent.appendChild(h1);
mainContent.appendChild(p1);
mainContent.appendChild(p2);
mainContent.appendChild(p3);
// Move el into the content wrapper
const contentWrapper = document.createElement('div');
contentWrapper.className = 'main-content-wrapper';
contentWrapper.appendChild(mainContent);
contentWrapper.appendChild(el);
const container = document.createElement('div');
container.className = 'demo-container';
container.appendChild(navbar);
container.appendChild(contentWrapper);
wrapper.appendChild(container);
const styleEl = document.createElement('style');
styleEl.textContent = utilityPanelStyles;
shadowRoot.appendChild(styleEl);
requestAnimationFrame(() => {
el.targetElement =
mainContent;
});
}
},
});
customElements.define('utility-panel-shadow-host', UtilityPanelShadowHost);
}
return html `<utility-panel-shadow-host
.props=${Object.assign({}, args)}
></utility-panel-shadow-host>`;
},
};