17 Commits

Author SHA1 Message Date
673af0e39c v1.6.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-18 08:28:40 +00:00
dc8774718d fix(sio-combox): tweak dropdown shadow and bind close event on conversation selector 2025-12-18 08:28:40 +00:00
9a87888f5a v1.6.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-18 08:16:46 +00:00
d61c3b6643 feat(conversation-selector): add conversation status badges to conversation selector and include status in sample data 2025-12-18 08:16:46 +00:00
c8554418de update 2025-12-17 11:50:04 +00:00
c1a8a57729 v1.5.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 11:41:47 +00:00
053d0c8e8f 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 2025-12-17 11:41:47 +00:00
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
a066e0de73 v1.3.0
Some checks failed
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-08 23:03:02 +00:00
7731054f0e feat(components): Add reusable message input component, refactor element properties to use accessor, update styles and docs, bump dependencies 2025-12-08 23:03:02 +00:00
5f48ecf7af update 2025-07-14 18:26:14 +00:00
f9d281c496 update 2025-07-14 18:22:02 +00:00
22 changed files with 5352 additions and 5223 deletions

View File

@@ -1,5 +1,61 @@
# Changelog # Changelog
## 2025-12-18 - 1.6.1 - fix(sio-combox)
tweak dropdown shadow and bind close event on conversation selector
- Replaced unsafeCSS(shadows.xl) with explicit box-shadow values for the combox dropdown to adjust visual appearance
- Added @close listener on <sio-conversation-selector> to call this.close(), enabling the selector to close the combox when it emits a close event
- Affected file: ts_web/elements/sio-combox.ts
## 2025-12-18 - 1.6.0 - feat(conversation-selector)
add conversation status badges to conversation selector and include status in sample data
- Introduce TConversationStatus type and add optional status property to IConversation
- Render status badges in sio-conversation-selector with CSS classes and a getBadgeLabel helper
- Update sample conversations in sio-combox.ts to include statuses: 'new', 'needs-action', 'waiting', 'resolved'
## 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
- 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)
Add reusable message input component, refactor element properties to use accessor, update styles and docs, bump dependencies
- Add new <sio-message-input> component (auto-resizing textarea, file picker, send/files events) and integrate it into sio-conversation-view
- Refactor multiple element class fields to use the 'accessor' property pattern (sio-button, sio-combox, sio-conversation-view, sio-dropdown-menu, sio-fab, sio-icon, sio-image-lightbox, sio-pdf-viewer, sio-recorder, etc.)
- Significant visual and UX updates to sio-button (new secondary variant, sizing, spacing, icon sizing, focus/disabled behavior)
- Move inline conversation input logic into the new component and simplify message handling (dispatch send-message event with text and attachments)
- Improve PDF viewer: safer async pdf.js loader, resize observer for responsive rendering, better error fallback and lifecycle cleanup
- Enhance image lightbox (PDF handling via sio-pdf-viewer, zoom/drag controls, keyboard shortcuts, download/open actions)
- Add icon caching and make lucide upgrade (cache limit, PascalCase lookup) to reduce re-renders
- Polish tokens/styles: spacing, radii, shadows, transitions and small responsive/layout tweaks (e.g. main page padding)
- Update README to reflect the public package name, components, quick start and development instructions
- Bump runtime and dev dependencies (dees-element/domtools/wcctools, lucide, git.zone tools, push.rocks smartenv/types) and adjust tsconfig (target/module settings simplified)
## 2025-04-20 - 1.2.4 - fix(build) ## 2025-04-20 - 1.2.4 - fix(build)
Update build script and async function signature Update build script and async function signature

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.2.5", "version": "1.6.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",
@@ -15,26 +15,25 @@
"author": "Lossless GmbH", "author": "Lossless GmbH",
"license": "UNLICENSED", "license": "UNLICENSED",
"dependencies": { "dependencies": {
"@design.estate/dees-domtools": "^2.3.3", "@design.estate/dees-domtools": "^2.3.6",
"@design.estate/dees-element": "^2.1.2", "@design.estate/dees-element": "^2.1.3",
"@design.estate/dees-wcctools": "^1.1.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.525.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": "^2.6.4", "@git.zone/tsbuild": "^4.0.2",
"@git.zone/tsbundle": "^2.5.1", "@git.zone/tsbundle": "^2.6.3",
"@git.zone/tsrun": "^1.3.3", "@git.zone/tsrun": "^2.0.1",
"@git.zone/tstest": "^2.3.1", "@git.zone/tstest": "^3.1.3",
"@git.zone/tswatch": "^2.1.2", "@git.zone/tswatch": "^2.3.13",
"@push.rocks/projectinfo": "^5.0.2", "@push.rocks/projectinfo": "^5.0.2",
"@push.rocks/smartenv": "^5.0.12", "@push.rocks/smartenv": "^6.0.0",
"@push.rocks/tapbundle": "^6.0.3", "@types/node": "^25.0.3"
"@types/node": "^22.14.1"
}, },
"files": [ "files": [
"ts/**/*", "ts/**/*",

8690
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

220
readme.md
View File

@@ -1,34 +1,204 @@
# @social.io/private/catalog # @social.io/catalog
the element catalog for the lossless organization A modern, beautifully designed UI component library for building conversational interfaces and support chat experiences. Built with Lit Element and TypeScript.
## Availabililty and Links ## Issue Reporting and Security
- [npmjs.org (npm package)](https://www.npmjs.com/package/@social.io_private/catalog) For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
- [gitlab.com (source)](https://gitlab.com/social.io/private/catalog)
- [github.com (source mirror)](https://github.com/social.io/private/catalog)
- [docs (typedoc)](https://social.io/private.gitlab.io/catalog/)
## Status for master ## 🎯 Features
| Status Category | Status Badge | - **Complete Chat UI** - Ready-to-use conversation components with message threads, typing indicators, and attachments
| ----------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | - **Floating Action Button** - Eye-catching FAB with smooth animations for triggering the chat interface
| GitLab Pipelines | [![pipeline status](https://gitlab.com/social.io/private/catalog/badges/master/pipeline.svg)](https://lossless.cloud) | - **PDF Viewer** - Built-in PDF rendering with zoom, pagination, and download capabilities
| GitLab Pipline Test Coverage | [![coverage report](https://gitlab.com/social.io/private/catalog/badges/master/coverage.svg)](https://lossless.cloud) | - **Image Lightbox** - Full-featured lightbox with zoom, pan, and keyboard navigation
| npm | [![npm downloads per month](https://badgen.net/npm/dy/@social.io_private/catalog)](https://lossless.cloud) | - **Modern Design Tokens** - Consistent styling with customizable colors, spacing, typography, and shadows
| Snyk | [![Known Vulnerabilities](https://badgen.net/snyk/social.io/private/catalog)](https://lossless.cloud) | - **Dark Mode Ready** - Full light/dark theme support out of the box
| TypeScript Support | [![TypeScript](https://badgen.net/badge/TypeScript/>=%203.x/blue?icon=typescript)](https://lossless.cloud) | - **Accessibility** - Keyboard navigation and proper ARIA attributes
| node Support | [![node](https://img.shields.io/badge/node->=%2010.x.x-blue.svg)](https://nodejs.org/dist/latest-v10.x/docs/api/) | - **TypeScript First** - Full type definitions for all components
| Code Style | [![Code Style](https://badgen.net/badge/style/prettier/purple)](https://lossless.cloud) |
| PackagePhobia (total standalone install weight) | [![PackagePhobia](https://badgen.net/packagephobia/install/@social.io_private/catalog)](https://lossless.cloud) |
| PackagePhobia (package size on registry) | [![PackagePhobia](https://badgen.net/packagephobia/publish/@social.io_private/catalog)](https://lossless.cloud) |
| BundlePhobia (total size when bundled) | [![BundlePhobia](https://badgen.net/bundlephobia/minzip/@social.io_private/catalog)](https://lossless.cloud) |
## Usage ## 📦 Installation
For further information read the linked docs at the top of this readme. ```bash
npm install @social.io/catalog
# or
pnpm add @social.io/catalog
```
## Legal ## 🚀 Quick Start
> UNLICENSED licensed | **&copy;** [Task Venture Capital GmbH](https://task.vc) ```typescript
> | By using this npm module you agree to our [privacy policy](https://lossless.gmbH/privacy) import { SioFab, SioCombox } from '@social.io/catalog';
// Components auto-register as custom elements
// Just use them in your HTML:
```
```html
<!-- Floating Action Button that opens the chat -->
<sio-fab></sio-fab>
<!-- Or use the full chat box directly -->
<sio-combox></sio-combox>
```
## 🧩 Components
### Core Components
| Component | Description |
|-----------|-------------|
| `<sio-fab>` | Floating action button with animated chat icon |
| `<sio-combox>` | Complete chat interface with conversation list and message view |
| `<sio-button>` | Styled button with variants (primary, secondary, destructive, outline, ghost) |
| `<sio-icon>` | Lucide icon wrapper with size and color customization |
| `<sio-dropdown-menu>` | Animated dropdown menu with keyboard support |
### Conversation Components
| Component | Description |
|-----------|-------------|
| `<sio-conversation-selector>` | Searchable list of conversations with unread indicators |
| `<sio-conversation-view>` | Message thread with typing indicators and file attachments |
| `<sio-message-input>` | Auto-expanding textarea with file upload |
### Media Components
| Component | Description |
|-----------|-------------|
| `<sio-image-lightbox>` | Fullscreen image viewer with zoom and pan |
| `<sio-pdf-viewer>` | PDF renderer with page navigation and zoom controls |
### Utility Components
| Component | Description |
|-----------|-------------|
| `<sio-recorder>` | Session recording using rrweb |
## 💅 Styling & Theming
The library uses CSS custom properties for theming. The design system includes:
- **Colors** - Primary, secondary, accent, destructive, muted, and semantic colors
- **Typography** - System font stack with size and weight variants
- **Spacing** - Consistent spacing scale (0.5rem increments)
- **Radius** - Border radius tokens from sm to full
- **Shadows** - Elevation system from sm to 2xl
- **Transitions** - Smooth animation presets
### Dark Mode
Dark mode is automatically supported. The components use `bdTheme()` helper that switches between light and dark values:
```typescript
import { bdTheme } from '@social.io/catalog';
// Usage in styles
css`
background: ${bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 10%)')};
`
```
## 📖 Usage Examples
### Basic Chat FAB
```html
<sio-fab></sio-fab>
```
The FAB opens a complete chat interface when clicked. It includes:
- Keyboard shortcut (Ctrl+S) to toggle
- Smooth scale and pulse animations
- Gradient background with glow effects
### Custom Button Variants
```html
<sio-button type="primary">Submit</sio-button>
<sio-button type="destructive">Delete</sio-button>
<sio-button type="outline">Cancel</sio-button>
<sio-button type="ghost" size="sm">
<sio-icon icon="settings"></sio-icon>
</sio-button>
```
### Image Lightbox
```typescript
const lightbox = document.querySelector('sio-image-lightbox');
lightbox.open({
url: 'https://example.com/photo.jpg',
name: 'My Photo',
size: 1024000
});
```
### PDF Viewer
```html
<sio-pdf-viewer
url="https://example.com/document.pdf"
fileName="document.pdf"
></sio-pdf-viewer>
```
### Dropdown Menu
```html
<sio-dropdown-menu
.items=${[
{ id: 'edit', label: 'Edit', icon: 'pencil' },
{ id: 'delete', label: 'Delete', icon: 'trash', destructive: true }
]}
@item-selected=${(e) => console.log('Selected:', e.detail.item)}
>
<sio-button type="ghost">
<sio-icon icon="more-vertical"></sio-icon>
</sio-button>
</sio-dropdown-menu>
```
## 🔧 Development
```bash
# Install dependencies
pnpm install
# Start development server with hot reload
pnpm watch
# Run tests
pnpm test
# Build for production
pnpm build
```
## 📚 Dependencies
- **@design.estate/dees-element** - Lit Element base with utilities
- **@design.estate/dees-domtools** - DOM manipulation helpers
- **lucide** - Beautiful open-source icons
- **rrweb** - Session recording/replay (for recorder component)
## License and Legal Information
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./LICENSE) file.
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
### Trademarks
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
### Company Information
Task Venture Capital GmbH
Registered at District Court Bremen HRB 35230 HB, Germany
For any legal inquiries or further information, please contact us via email at hello@task.vc.
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.

View File

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

View File

@@ -4,35 +4,35 @@ 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
@@ -42,41 +42,41 @@ export const colors = {
}, },
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
@@ -92,14 +92,14 @@ export const colors = {
// 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
@@ -153,6 +153,12 @@ export const getColor = (colorName: keyof typeof colors, isDark: boolean = false
}; };
// CSS helper for theme-aware colors // CSS helper for theme-aware colors
export const bdTheme = (colorName: keyof typeof colors) => { export const bdTheme = (colorNameOrLight: keyof typeof colors | string, dark?: string) => {
if (dark) {
// Direct color values provided
return cssManager.bdTheme(colorNameOrLight as string, dark);
}
// Color name from palette
const colorName = colorNameOrLight as keyof typeof colors;
return cssManager.bdTheme(colors[colorName].light, colors[colorName].dark); return cssManager.bdTheme(colors[colorName].light, colors[colorName].dark);
}; };

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

@@ -6,6 +6,7 @@ export * from './sio-dropdown-menu.js';
// Conversation components // Conversation components
export * from './sio-conversation-selector.js'; export * from './sio-conversation-selector.js';
export * from './sio-conversation-view.js'; export * from './sio-conversation-view.js';
export * from './sio-message-input.js';
export * from './sio-combox.js'; export * from './sio-combox.js';
// Other components // Other components

View File

@@ -36,19 +36,19 @@ export class SioButton extends DeesElement {
`; `;
@property({ type: String }) @property({ type: String })
public text: string = ''; public accessor text: string = '';
@property({ type: String }) @property({ type: String })
public type: 'default' | 'primary' | 'destructive' | 'outline' | 'ghost' = 'default'; public accessor type: 'default' | 'primary' | 'secondary' | 'destructive' | 'outline' | 'ghost' = 'default';
@property({ type: String }) @property({ type: String })
public size: 'sm' | 'default' | 'lg' = 'default'; public accessor size: 'sm' | 'default' | 'lg' = 'default';
@property({ type: Boolean, reflect: true }) @property({ type: Boolean, reflect: true })
public disabled: boolean = false; public accessor disabled: boolean = false;
@property({ type: String }) @property({ type: String })
public status: 'normal' | 'pending' | 'success' | 'error' = 'normal'; public accessor status: 'normal' | 'pending' | 'success' | 'error' = 'normal';
public static styles = [ public static styles = [
cssManager.defaultStyles, cssManager.defaultStyles,
@@ -68,117 +68,123 @@ export class SioButton extends DeesElement {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
white-space: nowrap; white-space: nowrap;
border-radius: ${unsafeCSS(radius.md)}; border-radius: 6px;
font-weight: 500; font-weight: 500;
transition: ${unsafeCSS(transitions.all)}; font-size: 14px;
line-height: 1;
letter-spacing: -0.01em;
transition: all 120ms ease;
cursor: pointer; cursor: pointer;
user-select: none; user-select: none;
outline: none; outline: none;
border: 1px solid transparent; border: none;
gap: ${unsafeCSS(spacing["2"])}; gap: 6px;
font-size: 0.875rem;
line-height: 1.5;
} }
/* Size variants */ /* Size variants */
.button.size-sm { .button.size-sm {
height: 32px; height: 32px;
padding: 0 ${unsafeCSS(spacing["3"])}; padding: 0 12px;
font-size: 13px; font-size: 13px;
} }
.button.size-default { .button.size-default {
height: 36px; height: 36px;
padding: 0 ${unsafeCSS(spacing["4"])}; padding: 0 16px;
font-size: 14px; font-size: 14px;
} }
.button.size-lg { .button.size-lg {
height: 44px; height: 42px;
padding: 0 ${unsafeCSS(spacing["6"])}; padding: 0 24px;
font-size: 16px; font-size: 15px;
} }
/* Type variants */ /* Type variants */
.button.default { .button.default {
background: ${bdTheme('background')}; background: ${bdTheme('hsl(0 0% 95%)', 'hsl(0 0% 15%)')};
color: ${bdTheme('foreground')}; color: ${bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 95%)')};
border-color: ${bdTheme('border')};
box-shadow: ${unsafeCSS(shadows.sm)};
} }
.button.default:hover:not(.disabled) { .button.default:hover:not(.disabled) {
background: ${bdTheme('accent')}; background: ${bdTheme('hsl(0 0% 91%)', 'hsl(0 0% 20%)')};
border-color: ${bdTheme('accent')};
transform: translateY(-1px);
box-shadow: ${unsafeCSS(shadows.md)};
} }
.button.default:active:not(.disabled) { .button.default:active:not(.disabled) {
transform: translateY(0); background: ${bdTheme('hsl(0 0% 87%)', 'hsl(0 0% 18%)')};
box-shadow: ${unsafeCSS(shadows.sm)};
} }
.button.primary { .button.primary {
background: ${bdTheme('primary')}; background: ${bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 95%)')};
color: ${bdTheme('primaryForeground')}; color: ${bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 0%)')};
box-shadow: ${unsafeCSS(shadows.sm)};
} }
.button.primary:hover:not(.disabled) { .button.primary:hover:not(.disabled) {
opacity: 0.9; background: ${bdTheme('hsl(0 0% 25%)', 'hsl(0 0% 100%)')};
transform: translateY(-1px);
box-shadow: ${unsafeCSS(shadows.md)};
} }
.button.primary:active:not(.disabled) { .button.primary:active:not(.disabled) {
transform: translateY(0); background: ${bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 90%)')};
box-shadow: ${unsafeCSS(shadows.sm)};
} }
/* Secondary variant */
.button.secondary {
background: transparent;
color: ${bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 95%)')};
box-shadow: inset 0 0 0 1px ${bdTheme('hsl(0 0% 85%)', 'hsl(0 0% 25%)')};
}
.button.secondary:hover:not(.disabled) {
background: ${bdTheme('hsl(0 0% 96%)', 'hsl(0 0% 15%)')};
}
.button.secondary:active:not(.disabled) {
background: ${bdTheme('hsl(0 0% 92%)', 'hsl(0 0% 12%)')};
}
/* Destructive variant */
.button.destructive { .button.destructive {
background: ${bdTheme('destructive')}; background: ${bdTheme('hsl(0 100% 95%)', 'hsl(0 50% 20%)')};
color: ${bdTheme('destructiveForeground')}; color: ${bdTheme('hsl(0 100% 45%)', 'hsl(0 100% 75%)')};
} }
.button.destructive:hover:not(.disabled) { .button.destructive:hover:not(.disabled) {
opacity: 0.9; background: ${bdTheme('hsl(0 100% 45%)', 'hsl(0 100% 50%)')};
color: white;
} }
.button.destructive:active:not(.disabled) { .button.destructive:active:not(.disabled) {
transform: translateY(1px); background: ${bdTheme('hsl(0 100% 40%)', 'hsl(0 100% 45%)')};
color: white;
} }
.button.outline { .button.outline {
background: transparent; background: transparent;
color: ${bdTheme('foreground')}; color: ${bdTheme('hsl(0 0% 40%)', 'hsl(0 0% 70%)')};
border-color: ${bdTheme('border')}; box-shadow: inset 0 0 0 1.5px ${bdTheme('hsl(0 0% 90%)', 'hsl(0 0% 30%)')};
} }
.button.outline:hover:not(.disabled) { .button.outline:hover:not(.disabled) {
background: ${bdTheme('accent')}; color: ${bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 95%)')};
color: ${bdTheme('accentForeground')}; box-shadow: inset 0 0 0 1.5px ${bdTheme('hsl(0 0% 70%)', 'hsl(0 0% 50%)')};
} }
.button.outline:active:not(.disabled) { .button.outline:active:not(.disabled) {
transform: translateY(1px); background: ${bdTheme('hsl(0 0% 95%)', 'hsl(0 0% 15%)')};
} }
.button.ghost { .button.ghost {
background: transparent; background: transparent;
color: ${bdTheme('foreground')}; color: ${bdTheme('hsl(0 0% 40%)', 'hsl(0 0% 70%)')};
border-color: transparent;
} }
.button.ghost:hover:not(.disabled) { .button.ghost:hover:not(.disabled) {
background: ${bdTheme('accent')}; color: ${bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 95%)')};
color: ${bdTheme('accentForeground')}; background: ${bdTheme('hsl(0 0% 0% / 0.05)', 'hsl(0 0% 100% / 0.05)')};
border-color: transparent;
} }
.button.ghost:active:not(.disabled) { .button.ghost:active:not(.disabled) {
background: ${bdTheme('accent')}; background: ${bdTheme('hsl(0 0% 0% / 0.1)', 'hsl(0 0% 100% / 0.1)')};
opacity: 0.8;
} }
/* Status states */ /* Status states */
@@ -214,13 +220,31 @@ export class SioButton extends DeesElement {
.button.disabled { .button.disabled {
opacity: 0.5; opacity: 0.5;
cursor: not-allowed; cursor: not-allowed;
transform: none !important;
} }
/* Focus state */ /* Focus state */
.button:focus-visible { .button:focus-visible {
outline: 2px solid ${bdTheme('ring')}; outline: 2px solid ${bdTheme('hsl(0 0% 15% / 0.2)', 'hsl(0 0% 95% / 0.2)')};
outline-offset: 2px; outline-offset: 2px;
} }
/* Icon sizing within buttons */
.button sio-icon {
width: 16px;
height: 16px;
flex-shrink: 0;
}
.button.size-sm sio-icon {
width: 14px;
height: 14px;
}
.button.size-lg sio-icon {
width: 18px;
height: 18px;
}
`, `,
]; ];
@@ -260,6 +284,7 @@ export class SioButton extends DeesElement {
return; return;
} }
// Let the native click bubble normally // Let the native click bubble normally
// Don't dispatch a custom event to avoid double-triggering // Don't dispatch a custom event to avoid double-triggering
} }

View File

@@ -36,43 +36,76 @@ declare global {
export class SioCombox extends DeesElement { export class SioCombox extends DeesElement {
public static demo = () => html` <sio-combox></sio-combox> `; public static demo = () => html` <sio-combox></sio-combox> `;
// 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 }) @property({ type: Object })
public referenceObject: HTMLElement; public accessor referenceObject: HTMLElement;
@state() @state()
private selectedConversationId: string | null = null; private accessor selectedConversationId: string | null = null;
@state() @state()
private conversations: IConversation[] = [ private accessor isKeyboardVisible: boolean = false;
@state()
private accessor isOpen: boolean = false;
private keyboardBlurTimeout?: number;
@state()
private accessor conversations: IConversation[] = [
{ {
id: '1', id: '1',
title: 'Technical Support', title: 'Technical Support',
lastMessage: 'Thanks for your help with the login issue!', lastMessage: 'Thanks for your help with the login issue!',
time: '2 min ago', time: '2 min ago',
unread: true, unread: true,
status: 'new',
}, },
{ {
id: '2', id: '2',
title: 'Billing Question', title: 'Billing Question',
lastMessage: 'I need help understanding my invoice', lastMessage: 'I need help understanding my invoice',
time: '1 hour ago', time: '1 hour ago',
status: 'needs-action',
}, },
{ {
id: '3', id: '3',
title: 'Feature Request', title: 'Feature Request',
lastMessage: 'That would be great! Looking forward to it', lastMessage: 'That would be great! Looking forward to it',
time: 'Yesterday', time: 'Yesterday',
status: 'waiting',
}, },
{ {
id: '4', id: '4',
title: 'General Inquiry', title: 'General Inquiry',
lastMessage: 'Thank you for the information', lastMessage: 'Thank you for the information',
time: '2 days ago', time: '2 days ago',
status: 'resolved',
} }
]; ];
@state() @state()
private messages: { [conversationId: string]: IMessage[] } = { private accessor messages: { [conversationId: string]: IMessage[] } = {
'1': [ '1': [
{ id: '1', text: 'Hi, I\'m having trouble logging in', sender: 'user', time: '10:00 AM' }, { id: '1', text: 'Hi, I\'m having trouble logging in', sender: 'user', time: '10:00 AM' },
{ id: '2', text: 'I can help you with that. Can you tell me what error you\'re seeing?', sender: 'support', time: '10:02 AM' }, { id: '2', text: 'I can help you with that. Can you tell me what error you\'re seeing?', sender: 'support', time: '10:02 AM' },
@@ -127,36 +160,122 @@ 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');
}
}
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 = [ public static styles = [
cssManager.defaultStyles, cssManager.defaultStyles,
css` css`
:host { :host {
display: block; display: block;
position: fixed;
bottom: 100px;
right: 20px;
height: 600px; height: 600px;
width: 800px; width: 800px;
background: ${bdTheme('background')}; background: ${bdTheme('background')};
border-radius: ${unsafeCSS(radius['2xl'])}; border-radius: ${unsafeCSS(radius['2xl'])};
border: 1px solid ${bdTheme('border')}; border: 1px solid ${bdTheme('border')};
box-shadow: ${unsafeCSS(shadows.xl)}; box-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.35), 0 12px 24px -8px rgb(0 0 0 / 0.15);
overflow: hidden; overflow: hidden;
font-family: ${unsafeCSS(fontFamilies.sans)}; font-family: ${unsafeCSS(fontFamilies.sans)};
position: relative;
transform-origin: bottom right; 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) { :host(.open) {
animation: scaleIn 300ms cubic-bezier(0.34, 1.56, 0.64, 1); 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 { :host::before {
@@ -181,65 +300,84 @@ 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 {
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 {
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 {
@@ -263,6 +401,7 @@ export class SioCombox extends DeesElement {
.conversations=${this.conversations} .conversations=${this.conversations}
.selectedConversationId=${this.selectedConversationId} .selectedConversationId=${this.selectedConversationId}
@conversation-selected=${this.handleConversationSelected} @conversation-selected=${this.handleConversationSelected}
@close=${() => this.close()}
></sio-conversation-selector> ></sio-conversation-selector>
<sio-conversation-view <sio-conversation-view

View File

@@ -16,6 +16,8 @@ import { spacing, radius, shadows, transitions } from './00tokens.js';
import { fontFamilies, typography } from './00fonts.js'; import { fontFamilies, typography } from './00fonts.js';
// Types // Types
export type TConversationStatus = 'new' | 'waiting' | 'needs-action' | 'resolved';
export interface IConversation { export interface IConversation {
id: string; id: string;
title: string; title: string;
@@ -23,6 +25,7 @@ export interface IConversation {
time: string; time: string;
unread?: boolean; unread?: boolean;
avatar?: string; avatar?: string;
status?: TConversationStatus;
} }
declare global { declare global {
@@ -38,13 +41,13 @@ export class SioConversationSelector extends DeesElement {
`; `;
@property({ type: Array }) @property({ type: Array })
public conversations: IConversation[] = []; public accessor conversations: IConversation[] = [];
@property({ type: String }) @property({ type: String })
public selectedConversationId: string | null = null; public accessor selectedConversationId: string | null = null;
@state() @state()
private searchQuery: string = ''; private accessor searchQuery: string = '';
public static styles = [ public static styles = [
cssManager.defaultStyles, cssManager.defaultStyles,
@@ -64,11 +67,18 @@ export class SioConversationSelector extends DeesElement {
background: ${bdTheme('background')}; background: ${bdTheme('background')};
} }
.header-top {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: ${unsafeCSS(spacing["4"])};
}
.title { .title {
font-size: 1.25rem; font-size: 1.25rem;
line-height: 1.2; line-height: 1.2;
font-weight: 600; font-weight: 600;
margin: 0 0 ${unsafeCSS(spacing["4"])} 0; margin: 0;
color: ${bdTheme('foreground')}; color: ${bdTheme('foreground')};
letter-spacing: -0.025em; letter-spacing: -0.025em;
} }
@@ -225,6 +235,38 @@ export class SioConversationSelector extends DeesElement {
} }
} }
.badge {
display: inline-flex;
align-items: center;
padding: 2px ${unsafeCSS(spacing["2"])};
font-size: 0.625rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.025em;
border-radius: ${unsafeCSS(radius.full)};
white-space: nowrap;
}
.badge.new {
background: ${bdTheme('primary')}20;
color: ${bdTheme('primary')};
}
.badge.waiting {
background: ${bdTheme('muted')};
color: ${bdTheme('mutedForeground')};
}
.badge.needs-action {
background: hsl(38 92% 50% / 0.15);
color: hsl(38 92% 40%);
}
.badge.resolved {
background: ${bdTheme('success')}20;
color: ${bdTheme('success')};
}
.empty-state { .empty-state {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -259,7 +301,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 {
@@ -270,7 +322,25 @@ export class SioConversationSelector extends DeesElement {
return html` return html`
<div class="header"> <div class="header">
<h2 class="title">Messages</h2> <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>
<sio-button
type="primary"
size="sm"
@click=${() => this.startNewConversation()}
>
<sio-icon icon="plus" size="16"></sio-icon>
New
</sio-button>
</div>
<div class="search-box"> <div class="search-box">
<input <input
type="text" type="text"
@@ -278,6 +348,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>
@@ -294,6 +366,7 @@ export class SioConversationSelector extends DeesElement {
<span class="conversation-title"> <span class="conversation-title">
${conv.title} ${conv.title}
${conv.unread ? html`<span class="unread-dot"></span>` : ''} ${conv.unread ? html`<span class="unread-dot"></span>` : ''}
${conv.status ? html`<span class="badge ${conv.status}">${this.getBadgeLabel(conv.status)}</span>` : ''}
</span> </span>
<span class="conversation-time">${conv.time}</span> <span class="conversation-time">${conv.time}</span>
</div> </div>
@@ -321,4 +394,45 @@ export class SioConversationSelector extends DeesElement {
composed: true composed: true
})); }));
} }
private startNewConversation() {
// Dispatch event for parent components to handle new conversation creation
this.dispatchEvent(new CustomEvent('new-conversation', {
bubbles: 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
}));
}
private getBadgeLabel(status: TConversationStatus): string {
const labels: Record<TConversationStatus, string> = {
'new': 'New',
'waiting': 'Waiting',
'needs-action': 'Action',
'resolved': 'Resolved',
};
return labels[status];
}
} }

View File

@@ -15,9 +15,11 @@ import { colors, bdTheme } from './00colors.js';
import { spacing, radius, shadows, transitions } from './00tokens.js'; import { spacing, radius, shadows, transitions } from './00tokens.js';
import { fontFamilies, typography } from './00fonts.js'; import { fontFamilies, typography } from './00fonts.js';
import { SioDropdownMenu, type IDropdownMenuItem } from './sio-dropdown-menu.js'; import { SioDropdownMenu, type IDropdownMenuItem } from './sio-dropdown-menu.js';
import { SioMessageInput } from './sio-message-input.js';
// Make sure components are loaded // Make sure components are loaded
SioDropdownMenu; SioDropdownMenu;
SioMessageInput;
// Types // Types
export interface IAttachment { export interface IAttachment {
@@ -57,22 +59,19 @@ export class SioConversationView extends DeesElement {
`; `;
@property({ type: Object }) @property({ type: Object })
public conversation: IConversationData | null = null; public accessor conversation: IConversationData | null = null;
@state() @state()
private messageText: string = ''; private accessor isTyping: boolean = false;
@state() @state()
private isTyping: boolean = false; private accessor isDragging: boolean = false;
@state() @state()
private isDragging: boolean = false; private accessor uploadingFiles: Map<string, { file: File; progress: number }> = new Map();
@state() @state()
private uploadingFiles: Map<string, { file: File; progress: number }> = new Map(); private accessor pendingAttachments: IAttachment[] = [];
@state()
private pendingAttachments: IAttachment[] = [];
private dropdownMenuItems: IDropdownMenuItem[] = [ private dropdownMenuItems: IDropdownMenuItem[] = [
{ id: 'mute', label: 'Mute notifications', icon: 'bell-off' }, { id: 'mute', label: 'Mute notifications', icon: 'bell-off' },
@@ -238,45 +237,6 @@ export class SioConversationView extends DeesElement {
-webkit-backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
} }
.input-wrapper {
display: flex;
gap: ${unsafeCSS(spacing["2"])};
align-items: flex-end;
}
.message-input {
flex: 1;
min-height: 42px;
max-height: 120px;
padding: ${unsafeCSS(spacing["2.5"])} ${unsafeCSS(spacing[3])};
background: ${bdTheme('secondary')};
border: 1px solid ${bdTheme('border')};
border-radius: ${unsafeCSS(radius.xl)};
font-size: 0.9375rem;
color: ${bdTheme('foreground')};
outline: none;
resize: none;
font-family: ${unsafeCSS(fontFamilies.sans)};
line-height: 1.5;
transition: ${unsafeCSS(transitions.all)};
}
.message-input::placeholder {
color: ${bdTheme('mutedForeground')};
font-size: 0.875rem;
}
.message-input:focus {
border-color: ${bdTheme('ring')};
background: ${bdTheme('background')};
box-shadow: 0 0 0 3px ${bdTheme('ring')}15;
}
.input-actions {
display: flex;
gap: ${unsafeCSS(spacing["1"])};
}
.empty-state { .empty-state {
flex: 1; flex: 1;
display: flex; display: flex;
@@ -440,7 +400,8 @@ export class SioConversationView extends DeesElement {
.pending-attachments { .pending-attachments {
padding: ${unsafeCSS(spacing["2"])} ${unsafeCSS(spacing["3"])}; padding: ${unsafeCSS(spacing["2"])} ${unsafeCSS(spacing["3"])};
background: ${bdTheme('secondary')}; background: ${bdTheme('secondary')};
border-radius: ${unsafeCSS(radius.md)}; border: 1px solid ${bdTheme('border')};
border-radius: ${unsafeCSS(radius.lg)};
margin-bottom: ${unsafeCSS(spacing["2"])}; margin-bottom: ${unsafeCSS(spacing["2"])};
} }
@@ -480,10 +441,6 @@ export class SioConversationView extends DeesElement {
.remove-attachment:hover { .remove-attachment:hover {
color: ${bdTheme('destructive')}; color: ${bdTheme('destructive')};
} }
.file-input {
display: none;
}
`, `,
]; ];
@@ -606,41 +563,10 @@ export class SioConversationView extends DeesElement {
`)} `)}
</div> </div>
` : ''} ` : ''}
<div class="input-wrapper"> <sio-message-input
<textarea @send-message=${this.handleMessageSend}
class="message-input" @files-selected=${this.handleFilesSelected}
placeholder="Type a message..." ></sio-message-input>
.value=${this.messageText}
@input=${this.handleInput}
@keydown=${this.handleKeyDown}
rows="1"
></textarea>
<div class="input-actions">
<input
type="file"
class="file-input"
id="fileInput"
multiple
accept="image/*,.pdf,.doc,.docx,.txt"
@change=${this.handleFileSelect}
/>
<sio-button type="ghost" size="sm" @click=${(e: Event) => {
e.preventDefault();
e.stopPropagation();
this.openFileSelector();
}}>
<sio-icon icon="paperclip" size="16"></sio-icon>
</sio-button>
<sio-button
type="primary"
size="sm"
?disabled=${!this.messageText.trim() && this.pendingAttachments.length === 0}
@click=${this.sendMessage}
>
<sio-icon icon="send" size="16"></sio-icon>
</sio-button>
</div>
</div>
</div> </div>
`; `;
} }
@@ -652,28 +578,14 @@ export class SioConversationView extends DeesElement {
})); }));
} }
private handleInput(e: Event) { private handleMessageSend(event: CustomEvent) {
const textarea = e.target as HTMLTextAreaElement; const { text, attachments } = event.detail;
this.messageText = textarea.value;
// Auto-resize textarea if (!text.trim() && attachments.length === 0) return;
textarea.style.height = 'auto';
textarea.style.height = Math.min(textarea.scrollHeight, 120) + 'px';
}
private handleKeyDown(e: KeyboardEvent) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
this.sendMessage();
}
}
private sendMessage() {
if (!this.messageText.trim() && this.pendingAttachments.length === 0) return;
const message: IMessage = { const message: IMessage = {
id: Date.now().toString(), id: Date.now().toString(),
text: this.messageText.trim(), text: text.trim(),
sender: 'user', sender: 'user',
time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }), time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
status: 'sending', status: 'sending',
@@ -687,13 +599,8 @@ export class SioConversationView extends DeesElement {
composed: true composed: true
})); }));
// Clear input and attachments // Clear pending attachments
this.messageText = '';
this.pendingAttachments = []; this.pendingAttachments = [];
const textarea = this.shadowRoot?.querySelector('.message-input') as HTMLTextAreaElement;
if (textarea) {
textarea.style.height = 'auto';
}
// Simulate typing indicator (remove in production) // Simulate typing indicator (remove in production)
setTimeout(() => { setTimeout(() => {
@@ -704,6 +611,13 @@ export class SioConversationView extends DeesElement {
}, 1000); }, 1000);
} }
private handleFilesSelected(event: CustomEvent) {
const { files } = event.detail;
// Handle files if needed
// For now, we're handling attachments separately in the parent component
}
public updated() { public updated() {
// Scroll to bottom when new messages arrive // Scroll to bottom when new messages arrive
const container = this.shadowRoot?.querySelector('#messages'); const container = this.shadowRoot?.querySelector('#messages');

View File

@@ -49,13 +49,13 @@ export class SioDropdownMenu extends DeesElement {
`; `;
@property({ type: Array }) @property({ type: Array })
public items: IDropdownMenuItem[] = []; public accessor items: IDropdownMenuItem[] = [];
@property({ type: String }) @property({ type: String })
public align: 'left' | 'right' = 'right'; public accessor align: 'left' | 'right' = 'right';
@state() @state()
private isOpen: boolean = false; private accessor isOpen: boolean = false;
private documentClickHandler: (e: MouseEvent) => void; private documentClickHandler: (e: MouseEvent) => void;
private scrollHandler: () => void; private scrollHandler: () => void;
@@ -160,27 +160,6 @@ export class SioDropdownMenu extends DeesElement {
background: ${bdTheme('border')}; background: ${bdTheme('border')};
margin: ${unsafeCSS(spacing["1"])} 0; margin: ${unsafeCSS(spacing["1"])} 0;
} }
/* Mobile adjustments */
@media (max-width: 600px) {
.dropdown {
position: fixed;
top: auto !important;
left: ${unsafeCSS(spacing["4"])} !important;
right: ${unsafeCSS(spacing["4"])};
bottom: ${unsafeCSS(spacing["4"])};
width: auto;
transform-origin: bottom center;
}
.dropdown.open {
transform: translateY(0) scale(1);
}
.dropdown:not(.open) {
transform: translateY(10px) scale(0.95);
}
}
`, `,
]; ];

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;
@@ -29,13 +30,10 @@ declare global {
@customElement('sio-fab') @customElement('sio-fab')
export class SioFab extends DeesElement { export class SioFab extends DeesElement {
@property({ type: Boolean }) @property({ type: Boolean })
public showCombox = false; public accessor showCombox = false;
@state() @state()
private hasShownOnce = false; private accessor shouldPulse = false;
@state()
private shouldPulse = false;
public static demo = () => html` <sio-fab .showCombox=${true}></sio-fab> `; public static demo = () => html` <sio-fab .showCombox=${true}></sio-fab> `;
@@ -44,201 +42,186 @@ 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;
}
#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);
}
`,
// Mobile responsive styles - smaller FAB
cssManager.cssForPhablet(css`
:host {
--fab-size: 48px;
bottom: 16px;
right: 16px;
}
`),
];
public render(): TemplateResult { public render(): TemplateResult {
return html` return html`
${domtools.elementBasic.styles}
<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" <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' : ''}">
${this.showCombox || this.hasShownOnce ? html`
<sio-combox @close=${() => this.showCombox = false}></sio-combox>
` : ''}
</div>
`; `;
} }
@@ -246,12 +229,12 @@ export class SioFab extends DeesElement {
* toggles the combox * toggles the combox
*/ */
public async toggleCombox() { public async toggleCombox() {
console.log('toggle combox'); const combox = SioCombox.getInstance();
const wasOpen = this.showCombox; if (combox) {
this.showCombox = !this.showCombox; const wasOpen = combox.getIsOpen();
if (this.showCombox) { combox.toggle();
this.hasShownOnce = true; this.showCombox = combox.getIsOpen();
if (!wasOpen) { if (this.showCombox && !wasOpen) {
this.shouldPulse = true; this.shouldPulse = true;
} }
} }
@@ -261,6 +244,14 @@ export class SioFab extends DeesElement {
super.firstUpdated(args); super.firstUpdated(args);
const domtools = await this.domtoolsPromise; 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 // 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])
@@ -270,15 +261,14 @@ export class SioFab extends DeesElement {
} }
public async updated(changedProperties: Map<string | number | symbol, unknown>) { public async updated(changedProperties: Map<string | number | symbol, unknown>) {
await super.updated(changedProperties); super.updated(changedProperties);
// Set reference object when combox is rendered // Update host class based on combox state
if ((changedProperties.has('showCombox') || changedProperties.has('hasShownOnce')) && if (changedProperties.has('showCombox')) {
(this.showCombox || this.hasShownOnce)) { if (this.showCombox) {
const sioCombox: SioCombox = this.shadowRoot.querySelector('sio-combox'); this.classList.add('combox-open');
const mainBox: HTMLElement = this.shadowRoot.querySelector('#mainbox'); } else {
if (sioCombox && mainBox && !sioCombox.referenceObject) { this.classList.remove('combox-open');
sioCombox.referenceObject = mainBox;
} }
} }
} }

View File

@@ -29,16 +29,16 @@ export class SioIcon extends DeesElement {
`; `;
@property({ type: String }) @property({ type: String })
public icon: string; public accessor icon: string;
@property({ type: Number }) @property({ type: Number })
public size: number = 24; public accessor size: number = 24;
@property({ type: String }) @property({ type: String })
public color: string = 'currentColor'; public accessor color: string = 'currentColor';
@property({ type: Number }) @property({ type: Number })
public strokeWidth: number = 2; public accessor strokeWidth: number = 2;
// Cache for rendered icons // Cache for rendered icons
private static iconCache = new Map<string, string>(); private static iconCache = new Map<string, string>();

View File

@@ -46,10 +46,10 @@ export class SioImageLightbox extends DeesElement {
`; `;
@property({ type: Boolean }) @property({ type: Boolean })
public isOpen: boolean = false; public accessor isOpen: boolean = false;
@property({ type: Object }) @property({ type: Object })
public file: ILightboxFile | null = null; public accessor file: ILightboxFile | null = null;
// For backwards compatibility // For backwards compatibility
public get image(): ILightboxFile | null { public get image(): ILightboxFile | null {
@@ -60,16 +60,16 @@ export class SioImageLightbox extends DeesElement {
} }
@state() @state()
private fileLoaded: boolean = false; private accessor fileLoaded: boolean = false;
@state() @state()
private scale: number = 1; private accessor scale: number = 1;
@state() @state()
private translateX: number = 0; private accessor translateX: number = 0;
@state() @state()
private translateY: number = 0; private accessor translateY: number = 0;
private isDragging: boolean = false; private isDragging: boolean = false;
private startX: number = 0; private startX: number = 0;

View File

@@ -0,0 +1,297 @@
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';
}
}
}

View File

@@ -11,8 +11,8 @@ import {
} from '@design.estate/dees-element'; } from '@design.estate/dees-element';
// Import design tokens // Import design tokens
import { colors, bdTheme } from './00colors.js'; import { bdTheme } from './00colors.js';
import { spacing, radius, shadows, transitions } from './00tokens.js'; import { spacing, radius, shadows } from './00tokens.js';
import { fontFamilies } from './00fonts.js'; import { fontFamilies } from './00fonts.js';
declare global { declare global {
@@ -36,28 +36,28 @@ export class SioPdfViewer extends DeesElement {
`; `;
@property({ type: String }) @property({ type: String })
public url: string = ''; public accessor url: string = '';
@property({ type: String }) @property({ type: String })
public fileName: string = 'document.pdf'; public accessor fileName: string = 'document.pdf';
@state() @state()
private isLoading: boolean = true; private accessor isLoading: boolean = true;
@state() @state()
private hasError: boolean = false; private accessor hasError: boolean = false;
@state() @state()
private pdfDocument: any = null; private accessor pdfDocument: any = null;
@state() @state()
private currentPage: number = 1; private accessor currentPage: number = 1;
@state() @state()
private totalPages: number = 0; private accessor totalPages: number = 0;
@state() @state()
private scale: number = 1; private accessor scale: number = 1;
private static pdfJsLoaded: boolean = false; private static pdfJsLoaded: boolean = false;
private static pdfJsLoading: Promise<void> | null = null; private static pdfJsLoading: Promise<void> | null = null;
@@ -398,8 +398,8 @@ export class SioPdfViewer extends DeesElement {
} }
} }
protected async firstUpdated() { public async firstUpdated(_changedProperties: any) {
await super.firstUpdated(); super.firstUpdated(_changedProperties);
// Set up resize observer for responsive rendering after first render // Set up resize observer for responsive rendering after first render
const container = this.shadowRoot?.querySelector('.pdf-container'); const container = this.shadowRoot?.querySelector('.pdf-container');
@@ -414,7 +414,7 @@ export class SioPdfViewer extends DeesElement {
} }
public async updated(changedProperties: Map<string | number | symbol, unknown>) { public async updated(changedProperties: Map<string | number | symbol, unknown>) {
await super.updated(changedProperties); super.updated(changedProperties);
if (changedProperties.has('url') && this.url) { if (changedProperties.has('url') && this.url) {
await this.loadPdf(); await this.loadPdf();
} }

View File

@@ -47,7 +47,7 @@ export class SioRecorder extends DeesElement {
* Query for the div in our template that will be used for playback. * Query for the div in our template that will be used for playback.
*/ */
@query('#playback') @query('#playback')
private playbackDiv!: HTMLDivElement; private accessor playbackDiv!: HTMLDivElement;
static styles = css` static styles = css`
:host { :host {

View File

@@ -12,7 +12,7 @@ export const mainpage = () => html`
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 20px; padding: 0;
} }
.demo-section { .demo-section {
background: white; background: white;

View File

@@ -1,14 +1,10 @@
{ {
"compilerOptions": { "compilerOptions": {
"experimentalDecorators": true,
"useDefineForClassFields": false,
"target": "ES2022", "target": "ES2022",
"module": "NodeNext", "module": "NodeNext",
"moduleResolution": "NodeNext", "moduleResolution": "NodeNext",
"esModuleInterop": true, "esModuleInterop": true,
"verbatimModuleSyntax": true, "verbatimModuleSyntax": true,
"baseUrl": ".",
"paths": {}
}, },
"exclude": [ "exclude": [
"dist_*/**/*.d.ts" "dist_*/**/*.d.ts"