diff --git a/changelog.md b/changelog.md index 928601a..b6b064e 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,16 @@ # Changelog +## 2025-12-17 - 1.5.0 - feat(combox) +Introduce singleton SioCombox attached to document.body with open/close/toggle API and animated show/hide; integrate SioFab to use the singleton and update styles/positioning + +- Add SioCombox.createOnBody() and SioCombox.getInstance() singletons +- Add isOpen state, open(), close(), toggle(), getIsOpen() and emit opened/closed/close events +- Move combox out of the FAB shadow DOM — attach to body and position fixed bottom-right with z-index and enter/exit transitions +- Update mobile layout to full-screen sizing and adjust transform origin for phablet +- Update SioFab to create the singleton on firstUpdated(), listen for close events, and toggle the singleton instead of rendering it inside the FAB +- Remove previous in-FAB combox container markup/CSS and hasShownOnce logic +- Minor visual/UX improvements: scale/opacity transitions, pointer-events control, and positioning variables for consistent behavior + ## 2025-12-17 - 1.4.1 - fix(ui) handle on-screen keyboard visibility to adjust layout and prevent inputs from being obscured diff --git a/ts_web/00_commitinfo_data.ts b/ts_web/00_commitinfo_data.ts index b79eb97..3e9e5c1 100644 --- a/ts_web/00_commitinfo_data.ts +++ b/ts_web/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@social.io/catalog', - version: '1.4.1', + version: '1.5.0', description: 'catalog for social.io' } diff --git a/ts_web/elements/sio-combox.ts b/ts_web/elements/sio-combox.ts index 5e7ba58..d3fb126 100644 --- a/ts_web/elements/sio-combox.ts +++ b/ts_web/elements/sio-combox.ts @@ -36,6 +36,27 @@ declare global { export class SioCombox extends DeesElement { public static demo = () => html` `; + // Singleton instance + private static instance: SioCombox | null = null; + + /** + * Creates and appends a singleton combox to document.body + */ + public static createOnBody(): SioCombox { + if (!SioCombox.instance) { + SioCombox.instance = new SioCombox(); + document.body.appendChild(SioCombox.instance); + } + return SioCombox.instance; + } + + /** + * Gets the singleton instance if it exists + */ + public static getInstance(): SioCombox | null { + return SioCombox.instance; + } + @property({ type: Object }) public accessor referenceObject: HTMLElement; @@ -45,6 +66,9 @@ export class SioCombox extends DeesElement { @state() private accessor isKeyboardVisible: boolean = false; + @state() + private accessor isOpen: boolean = false; + private keyboardBlurTimeout?: number; @state() @@ -174,6 +198,48 @@ export class SioCombox extends DeesElement { this.removeAttribute('keyboard-visible'); } } + if (changedProperties.has('isOpen')) { + if (this.isOpen) { + this.classList.add('open'); + this.dispatchEvent(new CustomEvent('opened', { bubbles: true, composed: true })); + } else { + this.classList.remove('open'); + this.dispatchEvent(new CustomEvent('closed', { bubbles: true, composed: true })); + } + } + } + + /** + * Opens the combox + */ + public open() { + this.isOpen = true; + } + + /** + * Closes the combox + */ + public close() { + this.isOpen = false; + this.dispatchEvent(new CustomEvent('close', { bubbles: true, composed: true })); + } + + /** + * Toggles the combox open/closed state + */ + public toggle() { + if (this.isOpen) { + this.close(); + } else { + this.open(); + } + } + + /** + * Returns whether the combox is currently open + */ + public getIsOpen(): boolean { + return this.isOpen; } public static styles = [ @@ -181,6 +247,9 @@ export class SioCombox extends DeesElement { css` :host { display: block; + position: fixed; + bottom: 100px; + right: 20px; height: 600px; width: 800px; background: ${bdTheme('background')}; @@ -189,25 +258,22 @@ export class SioCombox extends DeesElement { box-shadow: ${unsafeCSS(shadows.xl)}; overflow: hidden; font-family: ${unsafeCSS(fontFamilies.sans)}; - position: relative; transform-origin: bottom right; + z-index: 10001; + + /* Hidden by default */ + opacity: 0; + pointer-events: none; + transform: scale(0.95) translateY(10px); + transition: opacity 200ms ease, transform 200ms ease; } - - :host(.animate-in) { - animation: scaleIn 300ms cubic-bezier(0.34, 1.56, 0.64, 1); + + :host(.open) { + opacity: 1; + pointer-events: all; + transform: scale(1) translateY(0); } - - @keyframes scaleIn { - from { - opacity: 0; - transform: scale(0.9) translateY(10px); - } - to { - opacity: 1; - transform: scale(1) translateY(0); - } - } - + :host::before { content: ''; position: absolute; @@ -243,9 +309,19 @@ export class SioCombox extends DeesElement { // Mobile responsive layout - full screen with sliding mechanics cssManager.cssForPhablet(css` :host { - width: 100%; - height: 100%; + top: 0; + left: 0; + right: 0; + bottom: 0; + width: 100vw; + height: 100vh; + height: 100dvh; border-radius: 0; + transform-origin: center center; + } + + :host(.open) { + transform: scale(1) translateY(0); } :host::before { diff --git a/ts_web/elements/sio-fab.ts b/ts_web/elements/sio-fab.ts index 3d98e69..049af79 100644 --- a/ts_web/elements/sio-fab.ts +++ b/ts_web/elements/sio-fab.ts @@ -32,9 +32,6 @@ export class SioFab extends DeesElement { @property({ type: Boolean }) public accessor showCombox = false; - @state() - private accessor hasShownOnce = false; - @state() private accessor shouldPulse = false; @@ -62,7 +59,6 @@ export class SioFab extends DeesElement { --fab-gradient-hover-end: #c026d3; --fab-shadow-color: rgba(139, 92, 246, 0.25); --fab-size: 60px; - --fab-combox-offset: calc(var(--fab-size) + ${unsafeCSS(spacing["4"])}); } #mainbox { @@ -201,63 +197,13 @@ export class SioFab extends DeesElement { #mainbox .icon.close sio-icon { transform: scale(1); } - - #comboxContainer { - position: absolute; - bottom: 0; - right: 0; - pointer-events: none; - } - - #comboxContainer sio-combox { - position: absolute; - bottom: var(--fab-combox-offset); - right: 0; - transition: ${unsafeCSS(transitions.all)}; - will-change: transform; - transform: translateY(${unsafeCSS(spacing["5"])}); - opacity: 0; - pointer-events: none; - } - - #comboxContainer.show { - pointer-events: all; - } - - #comboxContainer.show sio-combox { - transform: translateY(0px); - opacity: 1; - pointer-events: all; - } `, - // Mobile responsive styles - smaller FAB and full-screen combox + // Mobile responsive styles - smaller FAB cssManager.cssForPhablet(css` :host { --fab-size: 48px; bottom: 16px; right: 16px; - will-change: auto; - } - - #comboxContainer { - position: fixed; - top: 0; - left: 0; - bottom: auto; - right: auto; - width: 100vw; - height: 100vh; - height: 100dvh; - } - - #comboxContainer sio-combox { - bottom: 0; - right: 0; - transform: none; - } - - #comboxContainer.show sio-combox { - transform: none; } `), ]; @@ -276,11 +222,6 @@ export class SioFab extends DeesElement { -
- ${this.showCombox || this.hasShownOnce ? html` - this.showCombox = false}> - ` : ''} -
`; } @@ -288,12 +229,12 @@ export class SioFab extends DeesElement { * toggles the combox */ public async toggleCombox() { - console.log('toggle combox'); - const wasOpen = this.showCombox; - this.showCombox = !this.showCombox; - if (this.showCombox) { - this.hasShownOnce = true; - if (!wasOpen) { + const combox = SioCombox.getInstance(); + if (combox) { + const wasOpen = combox.getIsOpen(); + combox.toggle(); + this.showCombox = combox.getIsOpen(); + if (this.showCombox && !wasOpen) { this.shouldPulse = true; } } @@ -303,6 +244,14 @@ export class SioFab extends DeesElement { super.firstUpdated(args); const domtools = await this.domtoolsPromise; + // Create the singleton combox on body + const combox = SioCombox.createOnBody(); + + // Listen for close events + combox.addEventListener('close', () => { + this.showCombox = false; + }); + // Set up keyboard shortcut domtools.keyboard .on([domtools.keyboard.keyEnum.Ctrl, domtools.keyboard.keyEnum.S]) @@ -322,15 +271,5 @@ export class SioFab extends DeesElement { this.classList.remove('combox-open'); } } - - // Set reference object when combox is rendered - if ((changedProperties.has('showCombox') || changedProperties.has('hasShownOnce')) && - (this.showCombox || this.hasShownOnce)) { - const sioCombox: SioCombox = this.shadowRoot.querySelector('sio-combox'); - const mainBox: HTMLElement = this.shadowRoot.querySelector('#mainbox'); - if (sioCombox && mainBox && !sioCombox.referenceObject) { - sioCombox.referenceObject = mainBox; - } - } } }