6 Commits

Author SHA1 Message Date
55e8e192c9 v1.4.1
Some checks failed
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-17 10:07:18 +00:00
286f6fd120 fix(ui): handle on-screen keyboard visibility to adjust layout and prevent inputs from being obscured 2025-12-17 10:07:18 +00:00
1401cd2c92 update 2025-12-17 09:27:53 +00:00
2323d1a01c update 2025-12-17 09:22:02 +00:00
bbb6d09ecf v1.4.0
Some checks failed
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-17 09:13:52 +00:00
2f54ee3f85 feat(elements): update design tokens and sio-fab component; bump deps and update npmextra config 2025-12-17 09:13:52 +00:00
11 changed files with 1312 additions and 382 deletions

View File

@@ -1,5 +1,22 @@
# Changelog # Changelog
## 2025-12-17 - 1.4.1 - fix(ui)
handle on-screen keyboard visibility to adjust layout and prevent inputs from being obscured
- Add keyboard visibility state (isKeyboardVisible) and keyboardBlurTimeout in sio-combox.ts
- Listen for custom 'input-focus' and 'input-blur' events and toggle keyboard-visible host attribute
- Dispatch 'input-focus'/'input-blur' from sio-conversation-selector and sio-message-input on focus/blur
- Add connected/disconnected lifecycle handlers and updated() hook to manage attribute and cleanup timeouts
- Apply :host([keyboard-visible]) CSS to set height to 100vh / 100dvh when keyboard is visible
## 2025-12-17 - 1.4.0 - feat(elements)
update design tokens and sio-fab component; bump deps and update npmextra config
- Refactor color tokens to a neutral HSL palette (ts_web/elements/00colors.ts) and adjust focus ring token (ts_web/elements/00tokens.ts).
- Refactor sio-fab: move styles to static property, add responsive FAB sizing and getMobileIconSize(), bind icon sizes, manage host class ('combox-open'), and tidy lifecycle methods for better behavior and mobile support.
- Bump dependencies and devDependencies: @design.estate/dees-wcctools -> ^2.0.1, lucide -> ^0.561.0; @git.zone/tsbuild -> ^4.0.2, @git.zone/tsrun -> ^2.0.1, @git.zone/tswatch -> ^2.3.13, @types/node -> ^25.0.3, etc.
- Update npmextra.json: rename configuration keys (gitzone -> @git.zone/cli, npmci -> @ship.zone/szci) and add release.registries and accessLevel for publishing.
## 2025-12-08 - 1.3.0 - feat(components) ## 2025-12-08 - 1.3.0 - feat(components)
Add reusable message input component, refactor element properties to use accessor, update styles and docs, bump dependencies Add reusable message input component, refactor element properties to use accessor, update styles and docs, bump dependencies

View File

@@ -1,5 +1,5 @@
{ {
"gitzone": { "@git.zone/cli": {
"projectType": "wcc", "projectType": "wcc",
"module": { "module": {
"githost": "gitlab.com", "githost": "gitlab.com",
@@ -9,11 +9,16 @@
"npmPackagename": "@social.io_private/catalog", "npmPackagename": "@social.io_private/catalog",
"license": "UNLICENSED", "license": "UNLICENSED",
"projectDomain": "social.io" "projectDomain": "social.io"
},
"release": {
"registries": [
"https://verdaccio.lossless.digital"
],
"accessLevel": "public"
} }
}, },
"npmci": { "@ship.zone/szci": {
"npmRegistryUrl": "verdaccio.lossless.one", "npmRegistryUrl": "verdaccio.lossless.one",
"npmGlobalTools": [], "npmGlobalTools": []
"npmAccessLevel": "private"
} }
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "@social.io/catalog", "name": "@social.io/catalog",
"version": "1.3.0", "version": "1.4.1",
"private": false, "private": false,
"description": "catalog for social.io", "description": "catalog for social.io",
"main": "dist_ts_web/index.js", "main": "dist_ts_web/index.js",
@@ -17,23 +17,23 @@
"dependencies": { "dependencies": {
"@design.estate/dees-domtools": "^2.3.6", "@design.estate/dees-domtools": "^2.3.6",
"@design.estate/dees-element": "^2.1.3", "@design.estate/dees-element": "^2.1.3",
"@design.estate/dees-wcctools": "^1.2.1", "@design.estate/dees-wcctools": "^2.0.1",
"@losslessone_private/loint-pubapi": "^1.0.14", "@losslessone_private/loint-pubapi": "^1.0.14",
"@social.io/interfaces": "^1.2.1", "@social.io/interfaces": "^1.2.1",
"lucide": "^0.556.0", "lucide": "^0.561.0",
"rrweb": "2.0.0-alpha.4", "rrweb": "2.0.0-alpha.4",
"rrweb-player": "1.0.0-alpha.4", "rrweb-player": "1.0.0-alpha.4",
"rrweb-snapshot": "2.0.0-alpha.4" "rrweb-snapshot": "2.0.0-alpha.4"
}, },
"devDependencies": { "devDependencies": {
"@git.zone/tsbuild": "^3.1.2", "@git.zone/tsbuild": "^4.0.2",
"@git.zone/tsbundle": "^2.6.3", "@git.zone/tsbundle": "^2.6.3",
"@git.zone/tsrun": "^2.0.0", "@git.zone/tsrun": "^2.0.1",
"@git.zone/tstest": "^3.1.3", "@git.zone/tstest": "^3.1.3",
"@git.zone/tswatch": "^2.3.5", "@git.zone/tswatch": "^2.3.13",
"@push.rocks/projectinfo": "^5.0.2", "@push.rocks/projectinfo": "^5.0.2",
"@push.rocks/smartenv": "^6.0.0", "@push.rocks/smartenv": "^6.0.0",
"@types/node": "^24.10.1" "@types/node": "^25.0.3"
}, },
"files": [ "files": [
"ts/**/*", "ts/**/*",

906
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@social.io/catalog', name: '@social.io/catalog',
version: '1.3.0', version: '1.4.1',
description: 'catalog for social.io' description: 'catalog for social.io'
} }

View File

@@ -4,102 +4,102 @@ export const colors = {
// Background colors - softer, more subtle // Background colors - softer, more subtle
background: { background: {
light: 'hsl(0 0% 100%)', light: 'hsl(0 0% 100%)',
dark: 'hsl(224 71.4% 4.1%)' dark: 'hsl(0 0% 4%)'
}, },
// Foreground colors - less contrast for modern look // Foreground colors - less contrast for modern look
foreground: { foreground: {
light: 'hsl(224 71.4% 4.1%)', light: 'hsl(0 0% 4%)',
dark: 'hsl(210 20% 98%)' dark: 'hsl(0 0% 98%)'
}, },
// Card colors - subtle elevation // Card colors - subtle elevation
card: { card: {
light: 'hsl(0 0% 100%)', light: 'hsl(0 0% 100%)',
dark: 'hsl(224 71.4% 4.1%)' dark: 'hsl(0 0% 4%)'
}, },
cardForeground: { cardForeground: {
light: 'hsl(224 71.4% 4.1%)', light: 'hsl(0 0% 4%)',
dark: 'hsl(210 20% 98%)' dark: 'hsl(0 0% 98%)'
}, },
// Popover colors // Popover colors
popover: { popover: {
light: 'hsl(0 0% 100%)', light: 'hsl(0 0% 100%)',
dark: 'hsl(222.2 84% 4.9%)' dark: 'hsl(0 0% 5%)'
}, },
popoverForeground: { popoverForeground: {
light: 'hsl(222.2 84% 4.9%)', light: 'hsl(0 0% 5%)',
dark: 'hsl(210 40% 98%)' dark: 'hsl(0 0% 98%)'
}, },
// Primary colors - modern indigo/blue // Primary colors - modern indigo/blue
primary: { primary: {
light: 'hsl(221.2 83.2% 53.3%)', light: 'hsl(221.2 83.2% 53.3%)',
dark: 'hsl(217.2 91.2% 59.8%)' dark: 'hsl(217.2 91.2% 59.8%)'
}, },
primaryForeground: { primaryForeground: {
light: 'hsl(210 20% 98%)', light: 'hsl(0 0% 98%)',
dark: 'hsl(224 71.4% 4.1%)' dark: 'hsl(0 0% 4%)'
}, },
// Secondary colors - more subtle // Secondary colors - more subtle
secondary: { secondary: {
light: 'hsl(220 14.3% 95.9%)', light: 'hsl(0 0% 96%)',
dark: 'hsl(215 27.9% 16.9%)' dark: 'hsl(0 0% 17%)'
}, },
secondaryForeground: { secondaryForeground: {
light: 'hsl(220.9 39.3% 11%)', light: 'hsl(0 0% 11%)',
dark: 'hsl(210 20% 98%)' dark: 'hsl(0 0% 98%)'
}, },
// Muted colors - softer grays // Muted colors - softer grays
muted: { muted: {
light: 'hsl(220 14.3% 95.9%)', light: 'hsl(0 0% 96%)',
dark: 'hsl(215 27.9% 16.9%)' dark: 'hsl(0 0% 17%)'
}, },
mutedForeground: { mutedForeground: {
light: 'hsl(220 8.9% 46.1%)', light: 'hsl(0 0% 46%)',
dark: 'hsl(217.9 10.6% 64.9%)' dark: 'hsl(0 0% 65%)'
}, },
// Accent colors - subtle hover states // Accent colors - subtle hover states
accent: { accent: {
light: 'hsl(220 14.3% 95.9%)', light: 'hsl(0 0% 96%)',
dark: 'hsl(215 27.9% 16.9%)' dark: 'hsl(0 0% 17%)'
}, },
accentForeground: { accentForeground: {
light: 'hsl(220.9 39.3% 11%)', light: 'hsl(0 0% 11%)',
dark: 'hsl(210 20% 98%)' dark: 'hsl(0 0% 98%)'
}, },
// Destructive colors - softer red // Destructive colors - softer red
destructive: { destructive: {
light: 'hsl(0 72.2% 50.6%)', light: 'hsl(0 72.2% 50.6%)',
dark: 'hsl(0 62.8% 30.6%)' dark: 'hsl(0 62.8% 30.6%)'
}, },
destructiveForeground: { destructiveForeground: {
light: 'hsl(0 0% 98%)', light: 'hsl(0 0% 98%)',
dark: 'hsl(0 0% 98%)' dark: 'hsl(0 0% 98%)'
}, },
// Border color - very subtle // Border color - very subtle
border: { border: {
light: 'hsl(220 13% 91%)', light: 'hsl(0 0% 91%)',
dark: 'hsl(215 27.9% 16.9%)' dark: 'hsl(0 0% 17%)'
}, },
// Input color // Input color
input: { input: {
light: 'hsl(214.3 31.8% 91.4%)', light: 'hsl(0 0% 91%)',
dark: 'hsl(217.2 32.6% 17.5%)' dark: 'hsl(0 0% 18%)'
}, },
// Ring color - subtle focus indicator // Ring color - subtle focus indicator

View File

@@ -185,7 +185,7 @@ export const focusRing = css`
outline: 2px solid transparent; outline: 2px solid transparent;
outline-offset: 2px; outline-offset: 2px;
&:focus-visible { &:focus-visible {
outline-color: ${cssManager.bdTheme('hsl(222.2 84% 4.9%)', 'hsl(212.7 26.8% 83.9%)')}; outline-color: ${cssManager.bdTheme('hsl(0 0% 5%)', 'hsl(0 0% 84%)')};
} }
`; `;

View File

@@ -42,6 +42,11 @@ export class SioCombox extends DeesElement {
@state() @state()
private accessor selectedConversationId: string | null = null; private accessor selectedConversationId: string | null = null;
@state()
private accessor isKeyboardVisible: boolean = false;
private keyboardBlurTimeout?: number;
@state() @state()
private accessor conversations: IConversation[] = [ private accessor conversations: IConversation[] = [
{ {
@@ -127,6 +132,50 @@ export class SioCombox extends DeesElement {
domtools.DomTools.setupDomTools(); domtools.DomTools.setupDomTools();
} }
async connectedCallback() {
await super.connectedCallback();
this.addEventListener('input-focus', this.handleInputFocus as EventListener);
this.addEventListener('input-blur', this.handleInputBlur as EventListener);
}
async disconnectedCallback() {
await super.disconnectedCallback();
this.removeEventListener('input-focus', this.handleInputFocus as EventListener);
this.removeEventListener('input-blur', this.handleInputBlur as EventListener);
if (this.keyboardBlurTimeout) {
clearTimeout(this.keyboardBlurTimeout);
}
}
private handleInputFocus = () => {
if (this.keyboardBlurTimeout) {
clearTimeout(this.keyboardBlurTimeout);
this.keyboardBlurTimeout = undefined;
}
this.isKeyboardVisible = true;
}
private handleInputBlur = () => {
if (this.keyboardBlurTimeout) {
clearTimeout(this.keyboardBlurTimeout);
}
this.keyboardBlurTimeout = window.setTimeout(() => {
this.isKeyboardVisible = false;
this.keyboardBlurTimeout = undefined;
}, 150);
}
updated(changedProperties: Map<string, any>) {
super.updated(changedProperties);
if (changedProperties.has('isKeyboardVisible')) {
if (this.isKeyboardVisible) {
this.setAttribute('keyboard-visible', '');
} else {
this.removeAttribute('keyboard-visible');
}
}
}
public static styles = [ public static styles = [
cssManager.defaultStyles, cssManager.defaultStyles,
css` css`
@@ -181,65 +230,74 @@ export class SioCombox extends DeesElement {
border-radius: ${unsafeCSS(radius['2xl'])}; border-radius: ${unsafeCSS(radius['2xl'])};
} }
/* Responsive layout */ /* Desktop layout (default) */
@media (max-width: 600px) { sio-conversation-selector {
:host { width: 320px;
width: 100%; flex-shrink: 0;
height: 100%;
border-radius: 0;
}
.container {
position: relative;
}
sio-conversation-selector {
position: absolute;
width: 100%;
height: 100%;
transition: left 300ms ease, opacity 200ms ease;
}
sio-conversation-view {
position: absolute;
width: 100%;
height: 100%;
transition: left 300ms ease, opacity 200ms ease;
}
/* Mobile navigation states */
.container.show-list sio-conversation-selector {
left: 0;
opacity: 1;
}
.container.show-list sio-conversation-view {
left: 100%;
opacity: 0;
}
.container.show-conversation sio-conversation-selector {
left: -100%;
opacity: 0;
}
.container.show-conversation sio-conversation-view {
left: 0;
opacity: 1;
}
} }
@media (min-width: 601px) { sio-conversation-view {
sio-conversation-selector { flex: 1;
width: 320px;
flex-shrink: 0;
}
sio-conversation-view {
flex: 1;
}
} }
`, `,
// Mobile responsive layout - full screen with sliding mechanics
cssManager.cssForPhablet(css`
:host {
width: 100%;
height: 100%;
border-radius: 0;
}
:host::before {
border-radius: 0;
}
.container {
position: relative;
overflow: hidden;
}
sio-conversation-selector {
position: absolute;
width: 100%;
height: 100%;
transition: left 300ms ease, opacity 200ms ease;
}
sio-conversation-view {
position: absolute;
width: 100%;
height: 100%;
transition: left 300ms ease, opacity 200ms ease;
}
/* Mobile navigation states */
.container.show-list sio-conversation-selector {
left: 0;
opacity: 1;
}
.container.show-list sio-conversation-view {
left: 100%;
opacity: 0;
}
.container.show-conversation sio-conversation-selector {
left: -100%;
opacity: 0;
}
.container.show-conversation sio-conversation-view {
left: 0;
opacity: 1;
}
/* Keyboard visible adjustments */
:host([keyboard-visible]) {
height: 100vh;
height: 100dvh;
}
`),
]; ];
public render(): TemplateResult { public render(): TemplateResult {

View File

@@ -266,7 +266,17 @@ export class SioConversationSelector extends DeesElement {
.conversation-list::-webkit-scrollbar-thumb:hover { .conversation-list::-webkit-scrollbar-thumb:hover {
background: ${bdTheme('mutedForeground')}; background: ${bdTheme('mutedForeground')};
} }
.close-button {
display: none;
}
`, `,
// Mobile: show close button
cssManager.cssForPhablet(css`
.close-button {
display: flex;
}
`),
]; ];
public render(): TemplateResult { public render(): TemplateResult {
@@ -278,9 +288,17 @@ export class SioConversationSelector extends DeesElement {
return html` return html`
<div class="header"> <div class="header">
<div class="header-top"> <div class="header-top">
<sio-button
class="close-button"
type="ghost"
size="sm"
@click=${() => this.handleClose()}
>
<sio-icon icon="x" size="20"></sio-icon>
</sio-button>
<h2 class="title">Messages</h2> <h2 class="title">Messages</h2>
<sio-button <sio-button
type="primary" type="primary"
size="sm" size="sm"
@click=${() => this.startNewConversation()} @click=${() => this.startNewConversation()}
> >
@@ -295,6 +313,8 @@ export class SioConversationSelector extends DeesElement {
placeholder="Search conversations..." placeholder="Search conversations..."
.value=${this.searchQuery} .value=${this.searchQuery}
@input=${(e: Event) => this.searchQuery = (e.target as HTMLInputElement).value} @input=${(e: Event) => this.searchQuery = (e.target as HTMLInputElement).value}
@focus=${this.handleInputFocus}
@blur=${this.handleInputBlur}
/> />
<sio-icon class="search-icon" icon="search" size="16"></sio-icon> <sio-icon class="search-icon" icon="search" size="16"></sio-icon>
</div> </div>
@@ -346,4 +366,27 @@ export class SioConversationSelector extends DeesElement {
composed: true composed: true
})); }));
} }
private handleClose() {
this.dispatchEvent(new CustomEvent('close', {
bubbles: true,
composed: true
}));
}
private handleInputFocus() {
setTimeout(() => {
this.dispatchEvent(new CustomEvent('input-focus', {
bubbles: true,
composed: true
}));
}, 50);
}
private handleInputBlur() {
this.dispatchEvent(new CustomEvent('input-blur', {
bubbles: true,
composed: true
}));
}
} }

View File

@@ -6,12 +6,13 @@ import {
type TemplateResult, type TemplateResult,
cssManager, cssManager,
css, css,
unsafeCSS,
state,
} from '@design.estate/dees-element'; } from '@design.estate/dees-element';
import * as domtools from '@design.estate/dees-domtools'; import * as domtools from '@design.estate/dees-domtools';
import { SioCombox } from './sio-combox.js'; import { SioCombox } from './sio-combox.js';
import { SioIcon } from './sio-icon.js'; import { SioIcon } from './sio-icon.js';
import { state } from '@design.estate/dees-element';
SioCombox; SioCombox;
SioIcon; SioIcon;
@@ -44,194 +45,235 @@ export class SioFab extends DeesElement {
domtools.DomTools.setupDomTools(); domtools.DomTools.setupDomTools();
} }
public static styles = [
cssManager.defaultStyles,
css`
:host {
will-change: transform;
position: absolute;
display: block;
bottom: 20px;
right: 20px;
z-index: 10000;
color: #fff;
--fab-gradient-start: #6366f1;
--fab-gradient-mid: #8b5cf6;
--fab-gradient-end: #a855f7;
--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 {
transition: ${unsafeCSS(transitions.all)};
position: absolute;
bottom: 0px;
right: 0px;
height: var(--fab-size);
width: var(--fab-size);
box-shadow: 0 4px 16px -2px rgba(0, 0, 0, 0.1), 0 2px 8px -2px rgba(0, 0, 0, 0.06);
line-height: var(--fab-size);
text-align: center;
cursor: pointer;
background: linear-gradient(135deg, var(--fab-gradient-start) 0%, var(--fab-gradient-mid) 50%, var(--fab-gradient-end) 100%);
color: white;
border-radius: ${unsafeCSS(radius.full)};
user-select: none;
border: none;
animation: fabEntrance 300ms cubic-bezier(0.4, 0, 0.2, 1);
overflow: hidden;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
#mainbox::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.2) 0%, rgba(255, 255, 255, 0) 50%);
opacity: 0;
transition: opacity 200ms ease;
}
#mainbox::after {
content: '';
position: absolute;
top: -4px;
left: -4px;
right: -4px;
bottom: -4px;
background: linear-gradient(135deg, var(--fab-gradient-start), var(--fab-gradient-end));
border-radius: inherit;
z-index: -1;
opacity: 0;
filter: blur(12px);
transition: opacity 300ms ease;
}
#mainbox:hover::before {
opacity: 1;
}
#mainbox:hover::after {
opacity: 0.3;
}
@keyframes fabEntrance {
from {
transform: scale(0.8);
opacity: 0;
}
to {
transform: scale(1);
opacity: 1;
}
}
#mainbox:hover {
transform: scale(1.02);
background: linear-gradient(135deg, var(--fab-gradient-start) 0%, var(--fab-gradient-mid) 50%, var(--fab-gradient-hover-end) 100%);
box-shadow: 0 8px 20px -4px var(--fab-shadow-color);
}
#mainbox:active {
transform: scale(0.98);
box-shadow: 0 4px 12px -2px var(--fab-shadow-color);
}
#mainbox.pulse::after {
animation: fabPulse 0.6s ease-out forwards;
}
@keyframes fabPulse {
0% {
box-shadow: 0 0 0 0 rgba(139, 92, 246, 0.4);
opacity: 1;
}
100% {
box-shadow: 0 0 0 12px rgba(139, 92, 246, 0);
opacity: 0;
}
}
#mainbox .icon {
position: absolute;
top: 0px;
left: 0px;
will-change: transform, opacity;
transition: all 200ms cubic-bezier(0.4, 0, 0.2, 1);
height: 100%;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
}
#mainbox .icon.open {
opacity: 1;
transform: rotate(0deg) scale(1);
}
#mainbox .icon.close {
opacity: 0;
transform: rotate(-45deg) scale(0.9);
}
/* When combox is open */
:host(.combox-open) #mainbox .icon.open {
opacity: 0;
transform: rotate(45deg) scale(0.9);
}
:host(.combox-open) #mainbox .icon.close {
opacity: 1;
transform: rotate(0deg) scale(1);
}
#mainbox .icon sio-icon {
color: white;
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.1));
}
#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
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;
}
`),
];
public render(): TemplateResult { public render(): TemplateResult {
return html` return html`
${domtools.elementBasic.styles} <div id="mainbox"
<style>
:host {
will-change: transform;
position: absolute;
display: block;
bottom: 20px;
right: 20px;
z-index: 10000;
color: #fff;
--fab-gradient-start: #6366f1;
--fab-gradient-mid: #8b5cf6;
--fab-gradient-end: #a855f7;
--fab-gradient-hover-end: #c026d3;
--fab-shadow-color: rgba(139, 92, 246, 0.25);
}
#mainbox {
transition: ${transitions.all};
position: absolute;
bottom: 0px;
right: 0px;
height: 60px;
width: 60px;
box-shadow: 0 4px 16px -2px rgba(0, 0, 0, 0.1), 0 2px 8px -2px rgba(0, 0, 0, 0.06);
line-height: 60px;
text-align: center;
cursor: pointer;
background: linear-gradient(135deg, var(--fab-gradient-start) 0%, var(--fab-gradient-mid) 50%, var(--fab-gradient-end) 100%);
color: white;
border-radius: ${radius.full};
user-select: none;
border: none;
animation: fabEntrance 300ms cubic-bezier(0.4, 0, 0.2, 1);
overflow: hidden;
position: relative;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
#mainbox::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.2) 0%, rgba(255, 255, 255, 0) 50%);
opacity: 0;
transition: opacity 200ms ease;
}
#mainbox::after {
content: '';
position: absolute;
top: -4px;
left: -4px;
right: -4px;
bottom: -4px;
background: linear-gradient(135deg, var(--fab-gradient-start), var(--fab-gradient-end));
border-radius: inherit;
z-index: -1;
opacity: 0;
filter: blur(12px);
transition: opacity 300ms ease;
}
#mainbox:hover::before {
opacity: 1;
}
#mainbox:hover::after {
opacity: 0.3;
}
@keyframes fabEntrance {
from {
transform: scale(0.8);
opacity: 0;
}
to {
transform: scale(1);
opacity: 1;
}
}
#mainbox:hover {
transform: scale(1.02);
background: linear-gradient(135deg, var(--fab-gradient-start) 0%, var(--fab-gradient-mid) 50%, var(--fab-gradient-hover-end) 100%);
}
#mainbox:hover {
box-shadow: 0 8px 20px -4px var(--fab-shadow-color);
}
#mainbox:active {
transform: scale(0.98);
box-shadow: 0 4px 12px -2px var(--fab-shadow-color);
}
#mainbox.pulse::after {
animation: fabPulse 0.6s ease-out forwards;
}
@keyframes fabPulse {
0% {
box-shadow: 0 0 0 0 rgba(139, 92, 246, 0.4);
opacity: 1;
}
100% {
box-shadow: 0 0 0 12px rgba(139, 92, 246, 0);
opacity: 0;
}
}
#mainbox .icon {
position: absolute;
top: 0px;
left: 0px;
will-change: transform, opacity;
transition: all 200ms cubic-bezier(0.4, 0, 0.2, 1);
height: 100%;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
}
#mainbox .icon.open {
opacity: ${this.showCombox ? '0' : '1'};
transform: ${this.showCombox ? 'rotate(45deg) scale(0.9)' : 'rotate(0deg) scale(1)'};
}
#mainbox .icon.close {
opacity: ${this.showCombox ? '1' : '0'};
transform: ${this.showCombox ? 'rotate(0deg) scale(1)' : 'rotate(-45deg) scale(0.9)'};
}
#mainbox .icon sio-icon {
color: white;
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.1));
}
#mainbox .icon.close sio-icon {
transform: scale(1);
}
#comboxContainer {
position: absolute;
bottom: 0;
right: 0;
pointer-events: none;
}
#comboxContainer sio-combox {
position: absolute;
bottom: calc(60px + ${spacing["4"]});
right: 0;
transition: ${transitions.all};
will-change: transform;
transform: translateY(${spacing["5"]});
opacity: 0;
pointer-events: none;
}
#comboxContainer.show {
pointer-events: all;
}
#comboxContainer.show sio-combox {
transform: translateY(0px);
opacity: 1;
pointer-events: all;
}
</style>
<div id="mainbox"
class="${this.shouldPulse ? 'pulse' : ''}" class="${this.shouldPulse ? 'pulse' : ''}"
@click=${this.toggleCombox} @click=${this.toggleCombox}
@animationend=${() => { this.shouldPulse = false; }} @animationend=${() => { this.shouldPulse = false; }}
> >
<div class="icon open"> <div class="icon open">
<sio-icon icon="message-square" size="28"></sio-icon> <sio-icon icon="message-square" size="24"></sio-icon>
</div> </div>
<div class="icon close"> <div class="icon close">
<sio-icon icon="x" size="22"></sio-icon> <sio-icon icon="x" size="20"></sio-icon>
</div> </div>
</div> </div>
<div id="comboxContainer" class="${this.showCombox ? 'show' : ''}"> <div id="comboxContainer" class="${this.showCombox ? 'show' : ''}">
@@ -260,7 +302,7 @@ export class SioFab extends DeesElement {
public async firstUpdated(args: any) { public async firstUpdated(args: any) {
super.firstUpdated(args); super.firstUpdated(args);
const domtools = await this.domtoolsPromise; const domtools = await this.domtoolsPromise;
// Set up keyboard shortcut // Set up keyboard shortcut
domtools.keyboard domtools.keyboard
.on([domtools.keyboard.keyEnum.Ctrl, domtools.keyboard.keyEnum.S]) .on([domtools.keyboard.keyEnum.Ctrl, domtools.keyboard.keyEnum.S])
@@ -268,12 +310,21 @@ export class SioFab extends DeesElement {
this.toggleCombox(); this.toggleCombox();
}); });
} }
public async updated(changedProperties: Map<string | number | symbol, unknown>) { public async updated(changedProperties: Map<string | number | symbol, unknown>) {
super.updated(changedProperties); super.updated(changedProperties);
// Update host class based on combox state
if (changedProperties.has('showCombox')) {
if (this.showCombox) {
this.classList.add('combox-open');
} else {
this.classList.remove('combox-open');
}
}
// Set reference object when combox is rendered // Set reference object when combox is rendered
if ((changedProperties.has('showCombox') || changedProperties.has('hasShownOnce')) && if ((changedProperties.has('showCombox') || changedProperties.has('hasShownOnce')) &&
(this.showCombox || this.hasShownOnce)) { (this.showCombox || this.hasShownOnce)) {
const sioCombox: SioCombox = this.shadowRoot.querySelector('sio-combox'); const sioCombox: SioCombox = this.shadowRoot.querySelector('sio-combox');
const mainBox: HTMLElement = this.shadowRoot.querySelector('#mainbox'); const mainBox: HTMLElement = this.shadowRoot.querySelector('#mainbox');

View File

@@ -168,6 +168,8 @@ export class SioMessageInput extends DeesElement {
.value=${this.messageText} .value=${this.messageText}
@input=${this.handleInput} @input=${this.handleInput}
@keydown=${this.handleKeyDown} @keydown=${this.handleKeyDown}
@focus=${this.handleFocus}
@blur=${this.handleBlur}
?disabled=${this.disabled} ?disabled=${this.disabled}
rows="1" rows="1"
></textarea> ></textarea>
@@ -216,6 +218,22 @@ export class SioMessageInput extends DeesElement {
} }
} }
private handleFocus() {
setTimeout(() => {
this.dispatchEvent(new CustomEvent('input-focus', {
bubbles: true,
composed: true
}));
}, 50);
}
private handleBlur() {
this.dispatchEvent(new CustomEvent('input-blur', {
bubbles: true,
composed: true
}));
}
private sendMessage() { private sendMessage() {
if (!this.messageText.trim() && this.pendingAttachments.length === 0) { if (!this.messageText.trim() && this.pendingAttachments.length === 0) {
return; return;