297 lines
7.0 KiB
TypeScript
297 lines
7.0 KiB
TypeScript
import {
|
|
DeesElement,
|
|
property,
|
|
html,
|
|
customElement,
|
|
type TemplateResult,
|
|
cssManager,
|
|
css,
|
|
unsafeCSS,
|
|
state,
|
|
} from '@design.estate/dees-element';
|
|
|
|
// Import design tokens
|
|
import { colors, bdTheme } from './00colors.js';
|
|
import { spacing, radius, shadows, transitions } from './00tokens.js';
|
|
import { fontFamilies, typography } from './00fonts.js';
|
|
|
|
declare global {
|
|
interface HTMLElementTagNameMap {
|
|
'sio-message-input': SioMessageInput;
|
|
}
|
|
}
|
|
|
|
@customElement('sio-message-input')
|
|
export class SioMessageInput extends DeesElement {
|
|
public static demo = () => html`
|
|
<sio-message-input style="width: 600px;"></sio-message-input>
|
|
`;
|
|
|
|
@property({ type: String })
|
|
public accessor placeholder: string = 'Type a message...';
|
|
|
|
@property({ type: Boolean })
|
|
public accessor disabled: boolean = false;
|
|
|
|
@state()
|
|
private accessor messageText: string = '';
|
|
|
|
@state()
|
|
private accessor pendingAttachments: File[] = [];
|
|
|
|
public static styles = [
|
|
cssManager.defaultStyles,
|
|
css`
|
|
:host {
|
|
display: block;
|
|
font-family: ${unsafeCSS(fontFamilies.sans)};
|
|
}
|
|
|
|
.input-container {
|
|
display: flex;
|
|
align-items: flex-end;
|
|
background: ${bdTheme('secondary')};
|
|
border: 1px solid ${bdTheme('border')};
|
|
border-radius: 24px;
|
|
padding: 2px;
|
|
transition: all 200ms ease;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.input-container:focus-within {
|
|
border-color: ${bdTheme('ring')};
|
|
background: ${bdTheme('background')};
|
|
}
|
|
|
|
.message-input {
|
|
flex: 1;
|
|
min-height: 44px;
|
|
max-height: 120px;
|
|
padding: 12px 16px;
|
|
background: transparent;
|
|
border: none;
|
|
font-size: 15px;
|
|
color: ${bdTheme('foreground')};
|
|
outline: none;
|
|
resize: none;
|
|
font-family: ${unsafeCSS(fontFamilies.sans)};
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.message-input::placeholder {
|
|
color: ${bdTheme('mutedForeground')};
|
|
}
|
|
|
|
.action-buttons {
|
|
display: flex;
|
|
gap: 4px;
|
|
align-items: center;
|
|
padding-right: 2px;
|
|
}
|
|
|
|
.action-button {
|
|
width: 40px;
|
|
height: 40px;
|
|
border-radius: 50%;
|
|
border: none;
|
|
background: transparent;
|
|
color: ${bdTheme('mutedForeground')};
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
transition: all 120ms ease;
|
|
position: relative;
|
|
outline: none;
|
|
}
|
|
|
|
.action-button:hover {
|
|
color: ${bdTheme('foreground')};
|
|
background: ${bdTheme('hsl(0 0% 0% / 0.05)', 'hsl(0 0% 100% / 0.05)')};
|
|
}
|
|
|
|
.action-button:active {
|
|
transform: scale(0.95);
|
|
}
|
|
|
|
.action-button.attachment {
|
|
opacity: 0.7;
|
|
}
|
|
|
|
.action-button.attachment:hover {
|
|
opacity: 1;
|
|
}
|
|
|
|
.action-button.send {
|
|
background: ${bdTheme('primary')};
|
|
color: white;
|
|
margin-left: 2px;
|
|
}
|
|
|
|
.action-button.send:hover {
|
|
background: ${bdTheme('hsl(221 83% 49%)', 'hsl(217 91% 65%)')};
|
|
}
|
|
|
|
.action-button.send:disabled {
|
|
opacity: 0.3;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.action-button.send:disabled:hover {
|
|
background: ${bdTheme('primary')};
|
|
transform: none;
|
|
}
|
|
|
|
.file-input {
|
|
display: none;
|
|
}
|
|
|
|
/* Icon styles */
|
|
sio-icon {
|
|
width: 20px;
|
|
height: 20px;
|
|
}
|
|
|
|
.action-button.send sio-icon {
|
|
width: 18px;
|
|
height: 18px;
|
|
}
|
|
`,
|
|
];
|
|
|
|
public render(): TemplateResult {
|
|
return html`
|
|
<div class="input-container">
|
|
<textarea
|
|
class="message-input"
|
|
placeholder="${this.placeholder}"
|
|
.value=${this.messageText}
|
|
@input=${this.handleInput}
|
|
@keydown=${this.handleKeyDown}
|
|
@focus=${this.handleFocus}
|
|
@blur=${this.handleBlur}
|
|
?disabled=${this.disabled}
|
|
rows="1"
|
|
></textarea>
|
|
|
|
<div class="action-buttons">
|
|
<input
|
|
type="file"
|
|
class="file-input"
|
|
id="fileInput"
|
|
multiple
|
|
accept="image/*,.pdf,.doc,.docx,.txt"
|
|
@change=${this.handleFileSelect}
|
|
/>
|
|
<button
|
|
class="action-button attachment"
|
|
@click=${this.openFileSelector}
|
|
?disabled=${this.disabled}
|
|
>
|
|
<sio-icon icon="paperclip"></sio-icon>
|
|
</button>
|
|
<button
|
|
class="action-button send"
|
|
?disabled=${!this.messageText.trim() || this.disabled}
|
|
@click=${this.sendMessage}
|
|
>
|
|
<sio-icon icon="send"></sio-icon>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
private handleInput(event: Event) {
|
|
const textarea = event.target as HTMLTextAreaElement;
|
|
this.messageText = textarea.value;
|
|
|
|
// Auto-resize textarea
|
|
textarea.style.height = 'auto';
|
|
textarea.style.height = `${textarea.scrollHeight}px`;
|
|
}
|
|
|
|
private handleKeyDown(event: KeyboardEvent) {
|
|
if (event.key === 'Enter' && !event.shiftKey) {
|
|
event.preventDefault();
|
|
this.sendMessage();
|
|
}
|
|
}
|
|
|
|
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() {
|
|
if (!this.messageText.trim() && this.pendingAttachments.length === 0) {
|
|
return;
|
|
}
|
|
|
|
this.dispatchEvent(new CustomEvent('send-message', {
|
|
detail: {
|
|
text: this.messageText,
|
|
attachments: this.pendingAttachments
|
|
},
|
|
bubbles: true,
|
|
composed: true
|
|
}));
|
|
|
|
// Clear input
|
|
this.messageText = '';
|
|
this.pendingAttachments = [];
|
|
|
|
// Reset textarea height
|
|
const textarea = this.shadowRoot?.querySelector('.message-input') as HTMLTextAreaElement;
|
|
if (textarea) {
|
|
textarea.style.height = 'auto';
|
|
}
|
|
}
|
|
|
|
private openFileSelector() {
|
|
const fileInput = this.shadowRoot?.querySelector('#fileInput') as HTMLInputElement;
|
|
if (fileInput) {
|
|
fileInput.click();
|
|
}
|
|
}
|
|
|
|
private handleFileSelect(event: Event) {
|
|
const input = event.target as HTMLInputElement;
|
|
const files = Array.from(input.files || []);
|
|
|
|
this.pendingAttachments = [...this.pendingAttachments, ...files];
|
|
|
|
this.dispatchEvent(new CustomEvent('files-selected', {
|
|
detail: { files },
|
|
bubbles: true,
|
|
composed: true
|
|
}));
|
|
|
|
input.value = ''; // Clear input for re-selection
|
|
}
|
|
|
|
public focus() {
|
|
const textarea = this.shadowRoot?.querySelector('.message-input') as HTMLTextAreaElement;
|
|
textarea?.focus();
|
|
}
|
|
|
|
public clear() {
|
|
this.messageText = '';
|
|
this.pendingAttachments = [];
|
|
const textarea = this.shadowRoot?.querySelector('.message-input') as HTMLTextAreaElement;
|
|
if (textarea) {
|
|
textarea.style.height = 'auto';
|
|
}
|
|
}
|
|
} |