diff --git a/changelog.md b/changelog.md
index 7795517..ce68ff7 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,5 +1,13 @@
# Changelog
+## 2025-12-22 - 1.1.0 - feat(ui)
+add mobile context menu and iconbutton components with demos and exports
+
+- Added dees-mobile-contextmenu component (items API, viewport-aware positioning, touch handling, open/close animation) and a demo
+- Added dees-mobile-iconbutton component (sm/md/lg sizes, disabled state, accessibility attributes) and a demo
+- Added barrel index files for both components
+- Exported the new components from ts_web/elements/00group-ui/index.ts
+
## 2025-12-22 - 1.0.2 - fix(dees-mobile-header)
adjust mobile header action slot layout and add documentation/license files
diff --git a/ts_web/00_commitinfo_data.ts b/ts_web/00_commitinfo_data.ts
index 570e68d..29e7ed6 100644
--- a/ts_web/00_commitinfo_data.ts
+++ b/ts_web/00_commitinfo_data.ts
@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@design.estate/dees-catalog-mobile',
- version: '1.0.2',
+ version: '1.1.0',
description: 'A mobile-optimized component catalog for building cross-platform business applications with touch-first UI components.'
}
diff --git a/ts_web/elements/00group-ui/dees-mobile-contextmenu/dees-mobile-contextmenu.demo.ts b/ts_web/elements/00group-ui/dees-mobile-contextmenu/dees-mobile-contextmenu.demo.ts
new file mode 100644
index 0000000..4f70ca9
--- /dev/null
+++ b/ts_web/elements/00group-ui/dees-mobile-contextmenu/dees-mobile-contextmenu.demo.ts
@@ -0,0 +1,82 @@
+import { html, type TemplateResult } from '@design.estate/dees-element';
+import type { IContextMenuItem } from './dees-mobile-contextmenu.js';
+
+export const demoFunc = (): TemplateResult => {
+ const showContextMenu = (e: MouseEvent) => {
+ e.preventDefault();
+
+ const items: IContextMenuItem[] = [
+ {
+ label: 'Edit',
+ icon: 'pencil',
+ action: () => console.log('Edit clicked'),
+ },
+ {
+ label: 'Duplicate',
+ icon: 'copy',
+ action: () => console.log('Duplicate clicked'),
+ },
+ { divider: true },
+ {
+ label: 'Share',
+ icon: 'share',
+ action: () => console.log('Share clicked'),
+ },
+ { divider: true },
+ {
+ label: 'Delete',
+ icon: 'trash-2',
+ danger: true,
+ action: () => console.log('Delete clicked'),
+ },
+ ];
+
+ import('./dees-mobile-contextmenu.js').then(({ DeesMobileContextmenu }) => {
+ DeesMobileContextmenu.createAndShow(items, e.clientX, e.clientY);
+ });
+ };
+
+ return html`
+
+
+
+
+ Right-click (or long-press on touch) to show context menu
+
+
+
+ Right-click here
+
+
+ `;
+};
diff --git a/ts_web/elements/00group-ui/dees-mobile-contextmenu/dees-mobile-contextmenu.ts b/ts_web/elements/00group-ui/dees-mobile-contextmenu/dees-mobile-contextmenu.ts
new file mode 100644
index 0000000..2af11fb
--- /dev/null
+++ b/ts_web/elements/00group-ui/dees-mobile-contextmenu/dees-mobile-contextmenu.ts
@@ -0,0 +1,283 @@
+import {
+ DeesElement,
+ css,
+ cssManager,
+ customElement,
+ html,
+ property,
+ state,
+ type TemplateResult,
+} from '@design.estate/dees-element';
+
+import { mobileComponentStyles } from '../../00componentstyles.js';
+import '../dees-mobile-icon/dees-mobile-icon.js';
+import { demoFunc } from './dees-mobile-contextmenu.demo.js';
+
+export interface IContextMenuItem {
+ label?: string;
+ icon?: string;
+ action?: () => void;
+ danger?: boolean;
+ divider?: boolean;
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'dees-mobile-contextmenu': DeesMobileContextmenu;
+ }
+}
+
+@customElement('dees-mobile-contextmenu')
+export class DeesMobileContextmenu extends DeesElement {
+ public static demo = demoFunc;
+
+ @property({ type: Array })
+ accessor items: IContextMenuItem[] = [];
+
+ @property({ type: Number })
+ accessor x: number = 0;
+
+ @property({ type: Number })
+ accessor y: number = 0;
+
+ @property({ type: Boolean })
+ accessor isTouch: boolean = false;
+
+ @state()
+ accessor isClosing: boolean = false;
+
+ @state()
+ accessor transformOrigin: string = 'top left';
+
+ public static styles = [
+ cssManager.defaultStyles,
+ mobileComponentStyles,
+ css`
+ :host {
+ position: fixed;
+ z-index: var(--dees-z-contextmenu, 10000);
+ }
+
+ :host(.closing) .menu {
+ animation: scaleOut 100ms ease-in;
+ }
+
+ .menu {
+ background: ${cssManager.bdTheme('#ffffff', '#18181b')};
+ border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
+ border-radius: 0.5rem;
+ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1);
+ padding: 0.5rem 0;
+ min-width: 180px;
+ animation: scaleIn 100ms ease-out;
+ }
+
+ .menu-item {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ padding: 0.5rem 1rem;
+ font-size: 0.875rem;
+ color: ${cssManager.bdTheme('#09090b', '#fafafa')};
+ cursor: pointer;
+ transition: all 100ms ease;
+ background: none;
+ border: none;
+ width: 100%;
+ text-align: left;
+ font-family: inherit;
+ }
+
+ .menu-item:hover {
+ background: ${cssManager.bdTheme('#f4f4f5', '#27272a')};
+ }
+
+ .menu-item.danger {
+ color: ${cssManager.bdTheme('#dc2626', '#ef4444')};
+ }
+
+ .divider {
+ height: 1px;
+ background: ${cssManager.bdTheme('#e4e4e7', '#27272a')};
+ margin: 0.25rem 0;
+ }
+
+ @keyframes scaleIn {
+ from {
+ opacity: 0;
+ transform: scale(0.9);
+ }
+ to {
+ opacity: 1;
+ transform: scale(1);
+ }
+ }
+
+ @keyframes scaleOut {
+ from {
+ opacity: 1;
+ transform: scale(1);
+ }
+ to {
+ opacity: 0;
+ transform: scale(0.9);
+ }
+ }
+ `,
+ ];
+
+ /**
+ * Factory method to create and show a context menu
+ */
+ public static createAndShow(
+ items: IContextMenuItem[],
+ x: number,
+ y: number,
+ isTouch = false
+ ): DeesMobileContextmenu {
+ // Remove any existing context menu
+ const existing = document.querySelector('dees-mobile-contextmenu');
+ if (existing) {
+ existing.remove();
+ }
+
+ // Create new menu
+ const menu = document.createElement('dees-mobile-contextmenu') as DeesMobileContextmenu;
+ menu.items = items;
+ menu.x = x;
+ menu.y = y;
+ menu.isTouch = isTouch;
+
+ // Add to document
+ document.body.appendChild(menu);
+
+ // Position after render to handle viewport bounds
+ requestAnimationFrame(() => {
+ menu.adjustPosition();
+ });
+
+ // Close on outside click
+ const handleClick = (e: MouseEvent) => {
+ if (!e.composedPath().includes(menu)) {
+ menu.close();
+ document.removeEventListener('click', handleClick, true);
+ }
+ };
+
+ // Add listener on next tick to avoid immediate close
+ setTimeout(() => {
+ document.addEventListener('click', handleClick, true);
+ }, 0);
+
+ return menu;
+ }
+
+ private adjustPosition(): void {
+ const rect = this.getBoundingClientRect();
+ const menuWidth = rect.width;
+ const menuHeight = rect.height;
+ const padding = 10;
+
+ let adjustedX = this.x;
+ let adjustedY = this.y;
+
+ // Calculate available space in each direction
+ const spaceTop = this.y - padding;
+ const spaceBottom = window.innerHeight - this.y - padding;
+ const spaceLeft = this.x - padding;
+ const spaceRight = window.innerWidth - this.x - padding;
+
+ // For touch interactions, prefer opening upward if there's space
+ if (this.isTouch && spaceTop >= menuHeight) {
+ // Open upward from touch point
+ adjustedY = this.y - menuHeight;
+ this.transformOrigin = 'bottom left';
+
+ // Adjust X if needed
+ if (spaceRight < menuWidth && spaceLeft >= menuWidth) {
+ adjustedX = this.x - menuWidth;
+ this.transformOrigin = 'bottom right';
+ }
+ } else {
+ // Default behavior (open downward/rightward)
+ // Flip horizontally if not enough space on right
+ if (spaceRight < menuWidth && spaceLeft >= menuWidth) {
+ adjustedX = this.x - menuWidth;
+ this.transformOrigin = this.transformOrigin.replace('left', 'right');
+ }
+
+ // Flip vertically if not enough space below
+ if (spaceBottom < menuHeight && spaceTop >= menuHeight) {
+ adjustedY = this.y - menuHeight;
+ this.transformOrigin = this.transformOrigin.replace('top', 'bottom');
+ }
+ }
+
+ // Final boundary checks to keep menu fully visible
+ adjustedX = Math.max(padding, Math.min(adjustedX, window.innerWidth - menuWidth - padding));
+ adjustedY = Math.max(padding, Math.min(adjustedY, window.innerHeight - menuHeight - padding));
+
+ this.style.left = `${adjustedX}px`;
+ this.style.top = `${adjustedY}px`;
+
+ // Update the menu's transform origin
+ const menu = this.shadowRoot?.querySelector('.menu') as HTMLElement;
+ if (menu) {
+ menu.style.transformOrigin = this.transformOrigin;
+ }
+ }
+
+ public close(): void {
+ if (this.isClosing) return;
+
+ this.isClosing = true;
+ this.classList.add('closing');
+
+ // Wait for the next frame to ensure animation starts
+ requestAnimationFrame(() => {
+ // Listen for animation end
+ const menu = this.shadowRoot?.querySelector('.menu');
+ if (menu) {
+ menu.addEventListener(
+ 'animationend',
+ () => {
+ this.remove();
+ },
+ { once: true }
+ );
+ } else {
+ // Fallback if menu not found
+ setTimeout(() => this.remove(), 100);
+ }
+ });
+ }
+
+ private handleItemClick(item: IContextMenuItem): void {
+ if (!item.divider && item.action) {
+ item.action();
+ this.close();
+ }
+ }
+
+ public render(): TemplateResult {
+ return html`
+
+ `;
+ }
+}
diff --git a/ts_web/elements/00group-ui/dees-mobile-contextmenu/index.ts b/ts_web/elements/00group-ui/dees-mobile-contextmenu/index.ts
new file mode 100644
index 0000000..6b6decf
--- /dev/null
+++ b/ts_web/elements/00group-ui/dees-mobile-contextmenu/index.ts
@@ -0,0 +1 @@
+export * from './dees-mobile-contextmenu.js';
diff --git a/ts_web/elements/00group-ui/dees-mobile-iconbutton/dees-mobile-iconbutton.demo.ts b/ts_web/elements/00group-ui/dees-mobile-iconbutton/dees-mobile-iconbutton.demo.ts
new file mode 100644
index 0000000..4225577
--- /dev/null
+++ b/ts_web/elements/00group-ui/dees-mobile-iconbutton/dees-mobile-iconbutton.demo.ts
@@ -0,0 +1,89 @@
+import { html, type TemplateResult } from '@design.estate/dees-element';
+import './dees-mobile-iconbutton.js';
+import '../dees-mobile-icon/dees-mobile-icon.js';
+
+export const demoFunc = (): TemplateResult => {
+ return html`
+
+
+
+
+
Sizes
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Common Actions
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
States
+
+
+
+
+
+
+
+
+
+
+
+ `;
+};
diff --git a/ts_web/elements/00group-ui/dees-mobile-iconbutton/dees-mobile-iconbutton.ts b/ts_web/elements/00group-ui/dees-mobile-iconbutton/dees-mobile-iconbutton.ts
new file mode 100644
index 0000000..611e5c9
--- /dev/null
+++ b/ts_web/elements/00group-ui/dees-mobile-iconbutton/dees-mobile-iconbutton.ts
@@ -0,0 +1,129 @@
+import {
+ DeesElement,
+ css,
+ cssManager,
+ customElement,
+ html,
+ property,
+ type TemplateResult,
+} from '@design.estate/dees-element';
+
+import { mobileComponentStyles } from '../../00componentstyles.js';
+import { demoFunc } from './dees-mobile-iconbutton.demo.js';
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'dees-mobile-iconbutton': DeesMobileIconbutton;
+ }
+}
+
+@customElement('dees-mobile-iconbutton')
+export class DeesMobileIconbutton extends DeesElement {
+ public static demo = demoFunc;
+
+ @property({ type: String })
+ accessor label: string = '';
+
+ @property({ type: Boolean })
+ accessor disabled: boolean = false;
+
+ @property({ type: String })
+ accessor size: 'sm' | 'md' | 'lg' = 'md';
+
+ public static styles = [
+ cssManager.defaultStyles,
+ mobileComponentStyles,
+ css`
+ :host {
+ display: inline-block;
+ }
+
+ button {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ background: transparent;
+ border: none;
+ border-radius: 0.375rem;
+ color: ${cssManager.bdTheme('#09090b', '#fafafa')};
+ cursor: pointer;
+ transition: all 150ms ease;
+ transform: scale(1);
+ position: relative;
+ -webkit-tap-highlight-color: transparent;
+ width: 100%;
+ height: 100%;
+ }
+
+ button:hover:not(:disabled) {
+ background-color: ${cssManager.bdTheme('#f4f4f5', '#27272a')};
+ transform: scale(1.1);
+ }
+
+ button:active:not(:disabled) {
+ transform: scale(0.95);
+ }
+
+ button:focus-visible {
+ outline: 2px solid ${cssManager.bdTheme('#3b82f6', '#60a5fa')};
+ outline-offset: 2px;
+ }
+
+ button:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+ }
+
+ /* Sizes */
+ button.sm {
+ width: 2rem;
+ height: 2rem;
+ }
+
+ button.md {
+ width: 2.5rem;
+ height: 2.5rem;
+ }
+
+ button.lg {
+ width: 3rem;
+ height: 3rem;
+ }
+
+ ::slotted(svg),
+ ::slotted(div),
+ ::slotted(dees-mobile-icon) {
+ width: 1.25rem;
+ height: 1.25rem;
+ pointer-events: none;
+ }
+
+ button.sm ::slotted(svg),
+ button.sm ::slotted(div),
+ button.sm ::slotted(dees-mobile-icon) {
+ width: 1rem;
+ height: 1rem;
+ }
+
+ button.lg ::slotted(svg),
+ button.lg ::slotted(div),
+ button.lg ::slotted(dees-mobile-icon) {
+ width: 1.5rem;
+ height: 1.5rem;
+ }
+ `,
+ ];
+
+ public render(): TemplateResult {
+ return html`
+
+ `;
+ }
+}
diff --git a/ts_web/elements/00group-ui/dees-mobile-iconbutton/index.ts b/ts_web/elements/00group-ui/dees-mobile-iconbutton/index.ts
new file mode 100644
index 0000000..ebee619
--- /dev/null
+++ b/ts_web/elements/00group-ui/dees-mobile-iconbutton/index.ts
@@ -0,0 +1 @@
+export * from './dees-mobile-iconbutton.js';
diff --git a/ts_web/elements/00group-ui/index.ts b/ts_web/elements/00group-ui/index.ts
index 046f290..0091544 100644
--- a/ts_web/elements/00group-ui/index.ts
+++ b/ts_web/elements/00group-ui/index.ts
@@ -1,6 +1,8 @@
// Core UI Components
export * from './dees-mobile-button/index.js';
+export * from './dees-mobile-contextmenu/index.js';
export * from './dees-mobile-icon/index.js';
+export * from './dees-mobile-iconbutton/index.js';
export * from './dees-mobile-header/index.js';
export * from './dees-mobile-modal/index.js';
export * from './dees-mobile-actionsheet/index.js';