19 Commits

Author SHA1 Message Date
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
6e29f0b51a 1.2.5
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-07-14 18:14:44 +00:00
5e25f86a0b update 2025-07-14 18:14:40 +00:00
01dad7cc5e update 2025-07-14 18:13:02 +00:00
a4a3c6dc50 update 2025-07-14 17:44:52 +00:00
193b1f5234 update 2025-07-14 17:26:57 +00:00
95e92a5533 update 2025-07-14 15:31:15 +00:00
1caeae9ec9 update 2025-07-14 15:30:16 +00:00
c534d1d084 update 2025-07-14 15:24:41 +00:00
23592f3a15 update 2025-07-14 15:21:37 +00:00
66493f793f update 2025-07-14 15:11:47 +00:00
efd142d63d update 2025-07-14 15:07:39 +00:00
9ab16c85ba update 2025-07-14 14:54:54 +00:00
ba791ee18a update 2025-07-14 10:13:02 +00:00
27 changed files with 9228 additions and 5218 deletions

View File

@@ -1,5 +1,27 @@
# Changelog
## 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)
Update build script and async function signature

56
demo.html Normal file
View File

@@ -0,0 +1,56 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Social.io Catalog Demo</title>
<style>
body {
margin: 0;
padding: 20px;
font-family: sans-serif;
background: #f5f5f5;
}
.demo-section {
margin: 20px 0;
padding: 20px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
h2 {
margin-top: 0;
}
.component-container {
position: relative;
height: 600px;
margin: 20px 0;
border: 1px solid #ddd;
border-radius: 4px;
overflow: hidden;
}
sio-combox {
position: relative !important;
width: 100% !important;
height: 100% !important;
right: auto !important;
}
</style>
<script type="module" src="./dist_bundle/bundle.js"></script>
</head>
<body>
<div class="demo-section">
<h2>Combox Component</h2>
<div class="component-container">
<sio-combox></sio-combox>
</div>
</div>
<div class="demo-section">
<h2>FAB with Combox</h2>
<div style="position: relative; height: 700px;">
<sio-fab showCombox></sio-fab>
</div>
</div>
</body>
</html>

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@social.io/catalog",
"version": "1.2.4",
"version": "1.4.0",
"private": false,
"description": "catalog for social.io",
"main": "dist_ts_web/index.js",
@@ -15,26 +15,25 @@
"author": "Lossless GmbH",
"license": "UNLICENSED",
"dependencies": {
"@design.estate/dees-catalog": "^1.5.6",
"@design.estate/dees-domtools": "^2.3.2",
"@design.estate/dees-element": "^2.0.42",
"@design.estate/dees-wcctools": "^1.0.90",
"@design.estate/dees-domtools": "^2.3.6",
"@design.estate/dees-element": "^2.1.3",
"@design.estate/dees-wcctools": "^2.0.1",
"@losslessone_private/loint-pubapi": "^1.0.14",
"@social.io/interfaces": "^1.2.1",
"lucide": "^0.561.0",
"rrweb": "2.0.0-alpha.4",
"rrweb-player": "1.0.0-alpha.4",
"rrweb-snapshot": "2.0.0-alpha.4"
},
"devDependencies": {
"@git.zone/tsbuild": "^2.3.2",
"@git.zone/tsbundle": "^2.2.5",
"@git.zone/tsrun": "^1.3.3",
"@git.zone/tstest": "^1.0.96",
"@git.zone/tswatch": "^2.1.0",
"@git.zone/tsbuild": "^4.0.2",
"@git.zone/tsbundle": "^2.6.3",
"@git.zone/tsrun": "^2.0.1",
"@git.zone/tstest": "^3.1.3",
"@git.zone/tswatch": "^2.3.13",
"@push.rocks/projectinfo": "^5.0.2",
"@push.rocks/smartenv": "^5.0.12",
"@push.rocks/tapbundle": "^5.6.3",
"@types/node": "^22.14.1"
"@push.rocks/smartenv": "^6.0.0",
"@types/node": "^25.0.3"
},
"files": [
"ts/**/*",

9014
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)
- [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/)
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.
## Status for master
## 🎯 Features
| Status Category | Status Badge |
| ----------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| GitLab Pipelines | [![pipeline status](https://gitlab.com/social.io/private/catalog/badges/master/pipeline.svg)](https://lossless.cloud) |
| GitLab Pipline Test Coverage | [![coverage report](https://gitlab.com/social.io/private/catalog/badges/master/coverage.svg)](https://lossless.cloud) |
| npm | [![npm downloads per month](https://badgen.net/npm/dy/@social.io_private/catalog)](https://lossless.cloud) |
| Snyk | [![Known Vulnerabilities](https://badgen.net/snyk/social.io/private/catalog)](https://lossless.cloud) |
| TypeScript Support | [![TypeScript](https://badgen.net/badge/TypeScript/>=%203.x/blue?icon=typescript)](https://lossless.cloud) |
| node Support | [![node](https://img.shields.io/badge/node->=%2010.x.x-blue.svg)](https://nodejs.org/dist/latest-v10.x/docs/api/) |
| 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) |
- **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
- **PDF Viewer** - Built-in PDF rendering with zoom, pagination, and download capabilities
- **Image Lightbox** - Full-featured lightbox with zoom, pan, and keyboard navigation
- **Modern Design Tokens** - Consistent styling with customizable colors, spacing, typography, and shadows
- **Dark Mode Ready** - Full light/dark theme support out of the box
- **Accessibility** - Keyboard navigation and proper ARIA attributes
- **TypeScript First** - Full type definitions for all components
## 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)
> | By using this npm module you agree to our [privacy policy](https://lossless.gmbH/privacy)
```typescript
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

@@ -1,9 +1,149 @@
import { expect, expectAsync, tap, webhelpers } from '@push.rocks/tapbundle';
import { expect, tap } from '@git.zone/tstest/tapbundle';
import * as socialioCatalog from '../ts_web/index.js';
tap.test('', async () => {
const sioFab: socialioCatalog.SioFab = webhelpers.fixture(webhelpers.html`<sio-fab></sio-fab>`);
tap.test('render combox component', async () => {
// Create and add combox
const combox = new socialioCatalog.SioCombox();
combox.style.position = 'relative';
combox.style.width = '800px';
combox.style.height = '600px';
document.body.appendChild(combox);
await combox.updateComplete;
expect(combox).toBeInstanceOf(socialioCatalog.SioCombox);
// Check that the component rendered its content
const container = combox.shadowRoot.querySelector('.container');
expect(container).toBeTruthy();
const conversationSelector = combox.shadowRoot.querySelector('sio-conversation-selector');
expect(conversationSelector).toBeTruthy();
const conversationView = combox.shadowRoot.querySelector('sio-conversation-view');
expect(conversationView).toBeTruthy();
console.log('Combox component rendered successfully with all main elements');
document.body.removeChild(combox);
});
tap.test('render fab component', async () => {
// Create and add fab
const fab = new socialioCatalog.SioFab();
document.body.appendChild(fab);
await fab.updateComplete;
expect(fab).toBeInstanceOf(socialioCatalog.SioFab);
// Check main elements
const mainbox = fab.shadowRoot.querySelector('#mainbox');
expect(mainbox).toBeTruthy();
console.log('FAB component rendered successfully');
document.body.removeChild(fab);
});
tap.test('render image lightbox component', async () => {
// Create and add lightbox
const lightbox = new socialioCatalog.SioImageLightbox();
document.body.appendChild(lightbox);
await lightbox.updateComplete;
expect(lightbox).toBeInstanceOf(socialioCatalog.SioImageLightbox);
// Check main elements
const overlay = lightbox.shadowRoot.querySelector('.overlay');
expect(overlay).toBeTruthy();
const container = lightbox.shadowRoot.querySelector('.container');
expect(container).toBeTruthy();
// Test opening with an image
await lightbox.open({
url: 'https://picsum.photos/800/600',
name: 'Test Image',
size: 123456
});
await lightbox.updateComplete;
expect(lightbox.isOpen).toEqual(true);
// Test opening with a PDF
await lightbox.open({
url: 'data:application/pdf;base64,JVBERi0xLjMKJeLjz9MKMSAwIG9iago8PAovVHlwZSAvQ2F0YWxvZwovT3V0bGluZXMgMiAwIFIKL1BhZ2VzIDMgMCBSCj4+CmVuZG9iagoyIDAgb2JqCjw8Ci9UeXBlIC9PdXRsaW5lcwovQ291bnQgMAo+PgplbmRvYmoKMyAwIG9iago8PAovVHlwZSAvUGFnZXMKL0NvdW50IDEKL0tpZHMgWzQgMCBSXQo+PgplbmRvYmoKNCAwIG9iago8PAovVHlwZSAvUGFnZQovUGFyZW50IDMgMCBSCi9NZWRpYUJveCBbMCAwIDYxMiA3OTJdCi9Db250ZW50cyA1IDAgUgovUmVzb3VyY2VzIDw8Ci9Gb250IDw8Ci9GMSA2IDAgUgo+Pgo+Pgo+PgplbmRvYmoKNSAwIG9iago8PAovTGVuZ3RoIDQ0Cj4+CnN0cmVhbQpCVApxCjcwIDUwIFRECi9GMSAxMiBUZgooSGVsbG8gV29ybGQpIFRqCkVUClEKZW5kc3RyZWFtCmVuZG9iago2IDAgb2JqCjw8Ci9UeXBlIC9Gb250Ci9TdWJ0eXBlIC9UeXBlMQovQmFzZUZvbnQgL1RpbWVzLVJvbWFuCj4+CmVuZG9iagp4cmVmCjAgNwowMDAwMDAwMDAwIDY1NTM1IGYgCjAwMDAwMDAwMDkgMDAwMDAgbiAKMDAwMDAwMDA3NCAwMDAwMCBuIAowMDAwMDAwMTIwIDAwMDAwIG4gCjAwMDAwMDAxNzkgMDAwMDAgbiAKMDAwMDAwMDM2NCAwMDAwMCBuIAowMDAwMDAwNDY2IDAwMDAwIG4gCnRyYWlsZXIKPDwKL1NpemUgNwovUm9vdCAxIDAgUgo+PgpzdGFydHhyZWYKNTY1CiUlRU9G',
name: 'test.pdf',
type: 'application/pdf',
size: 565
});
await lightbox.updateComplete;
// Check that PDF viewer is rendered
const pdfViewer = lightbox.shadowRoot.querySelector('sio-pdf-viewer');
expect(pdfViewer).toBeTruthy();
console.log('Image lightbox component rendered successfully with both image and PDF support');
document.body.removeChild(lightbox);
});
tap.test('render dropdown menu component', async () => {
// Create and add dropdown menu
const dropdown = new socialioCatalog.SioDropdownMenu();
dropdown.items = [
{ id: 'option1', label: 'Option 1', icon: 'settings' },
{ id: 'option2', label: 'Option 2', icon: 'user' },
{ id: 'divider', label: '', divider: true },
{ id: 'delete', label: 'Delete', icon: 'trash', destructive: true }
];
document.body.appendChild(dropdown);
await dropdown.updateComplete;
expect(dropdown).toBeInstanceOf(socialioCatalog.SioDropdownMenu);
// Check main elements
const trigger = dropdown.shadowRoot.querySelector('.trigger');
expect(trigger).toBeTruthy();
const dropdownElement = dropdown.shadowRoot.querySelector('.dropdown');
expect(dropdownElement).toBeTruthy();
// Check menu items
const menuItems = dropdown.shadowRoot.querySelectorAll('.menu-item');
expect(menuItems.length).toEqual(3); // 3 items (excluding divider)
console.log('Dropdown menu component rendered successfully');
document.body.removeChild(dropdown);
});
tap.test('render pdf viewer component', async () => {
// Create and add PDF viewer
const pdfViewer = new socialioCatalog.SioPdfViewer();
pdfViewer.style.width = '600px';
pdfViewer.style.height = '400px';
pdfViewer.url = 'data:application/pdf;base64,JVBERi0xLjMKJeLjz9MKMSAwIG9iago8PAovVHlwZSAvQ2F0YWxvZwovT3V0bGluZXMgMiAwIFIKL1BhZ2VzIDMgMCBSCj4+CmVuZG9iagoyIDAgb2JqCjw8Ci9UeXBlIC9PdXRsaW5lcwovQ291bnQgMAo+PgplbmRvYmoKMyAwIG9iago8PAovVHlwZSAvUGFnZXMKL0NvdW50IDEKL0tpZHMgWzQgMCBSXQo+PgplbmRvYmoKNCAwIG9iago8PAovVHlwZSAvUGFnZQovUGFyZW50IDMgMCBSCi9NZWRpYUJveCBbMCAwIDYxMiA3OTJdCi9Db250ZW50cyA1IDAgUgovUmVzb3VyY2VzIDw8Ci9Gb250IDw8Ci9GMSA2IDAgUgo+Pgo+Pgo+PgplbmRvYmoKNSAwIG9iago8PAovTGVuZ3RoIDQ0Cj4+CnN0cmVhbQpCVApxCjcwIDUwIFRECi9GMSAxMiBUZgooSGVsbG8gV29ybGQpIFRqCkVUClEKZW5kc3RyZWFtCmVuZG9iago2IDAgb2JqCjw8Ci9UeXBlIC9Gb250Ci9TdWJ0eXBlIC9UeXBlMQovQmFzZUZvbnQgL1RpbWVzLVJvbWFuCj4+CmVuZG9iagp4cmVmCjAgNwowMDAwMDAwMDAwIDY1NTM1IGYgCjAwMDAwMDAwMDkgMDAwMDAgbiAKMDAwMDAwMDA3NCAwMDAwMCBuIAowMDAwMDAwMTIwIDAwMDAwIG4gCjAwMDAwMDAxNzkgMDAwMDAgbiAKMDAwMDAwMDM2NCAwMDAwMCBuIAowMDAwMDAwNDY2IDAwMDAwIG4gCnRyYWlsZXIKPDwKL1NpemUgNwovUm9vdCAxIDAgUgo+PgpzdGFydHhyZWYKNTY1CiUlRU9G';
pdfViewer.fileName = 'test.pdf';
document.body.appendChild(pdfViewer);
await pdfViewer.updateComplete;
expect(pdfViewer).toBeInstanceOf(socialioCatalog.SioPdfViewer);
// Check main elements
const container = pdfViewer.shadowRoot.querySelector('.container');
expect(container).toBeTruthy();
// PDF viewer uses canvas after loading, not iframe
// Just verify the component rendered correctly
expect(pdfViewer.url).toEqual('data:application/pdf;base64,JVBERi0xLjMKJeLjz9MKMSAwIG9iago8PAovVHlwZSAvQ2F0YWxvZwovT3V0bGluZXMgMiAwIFIKL1BhZ2VzIDMgMCBSCj4+CmVuZG9iagoyIDAgb2JqCjw8Ci9UeXBlIC9PdXRsaW5lcwovQ291bnQgMAo+PgplbmRvYmoKMyAwIG9iago8PAovVHlwZSAvUGFnZXMKL0NvdW50IDEKL0tpZHMgWzQgMCBSXQo+PgplbmRvYmoKNCAwIG9iago8PAovVHlwZSAvUGFnZQovUGFyZW50IDMgMCBSCi9NZWRpYUJveCBbMCAwIDYxMiA3OTJdCi9Db250ZW50cyA1IDAgUgovUmVzb3VyY2VzIDw8Ci9Gb250IDw8Ci9GMSA2IDAgUgo+Pgo+Pgo+PgplbmRvYmoKNSAwIG9iago8PAovTGVuZ3RoIDQ0Cj4+CnN0cmVhbQpCVApxCjcwIDUwIFRECi9GMSAxMiBUZgooSGVsbG8gV29ybGQpIFRqCkVUClEKZW5kc3RyZWFtCmVuZG9iago2IDAgb2JqCjw8Ci9UeXBlIC9Gb250Ci9TdWJ0eXBlIC9UeXBlMQovQmFzZUZvbnQgL1RpbWVzLVJvbWFuCj4+CmVuZG9iagp4cmVmCjAgNwowMDAwMDAwMDAwIDY1NTM1IGYgCjAwMDAwMDAwMDkgMDAwMDAgbiAKMDAwMDAwMDA3NCAwMDAwMCBuIAowMDAwMDAwMTIwIDAwMDAwIG4gCjAwMDAwMDAxNzkgMDAwMDAgbiAKMDAwMDAwMDM2NCAwMDAwMCBuIAowMDAwMDAwNDY2IDAwMDAwIG4gCnRyYWlsZXIKPDwKL1NpemUgNwovUm9vdCAxIDAgUgo+PgpzdGFydHhyZWYKNTY1CiUlRU9G');
expect(pdfViewer.fileName).toEqual('test.pdf');
console.log('PDF viewer component rendered successfully');
document.body.removeChild(pdfViewer);
});
tap.start();

View File

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

164
ts_web/elements/00colors.ts Normal file
View File

@@ -0,0 +1,164 @@
import { cssManager } from '@design.estate/dees-element';
export const colors = {
// Background colors - softer, more subtle
background: {
light: 'hsl(0 0% 100%)',
dark: 'hsl(0 0% 4%)'
},
// Foreground colors - less contrast for modern look
foreground: {
light: 'hsl(0 0% 4%)',
dark: 'hsl(0 0% 98%)'
},
// Card colors - subtle elevation
card: {
light: 'hsl(0 0% 100%)',
dark: 'hsl(0 0% 4%)'
},
cardForeground: {
light: 'hsl(0 0% 4%)',
dark: 'hsl(0 0% 98%)'
},
// Popover colors
popover: {
light: 'hsl(0 0% 100%)',
dark: 'hsl(0 0% 5%)'
},
popoverForeground: {
light: 'hsl(0 0% 5%)',
dark: 'hsl(0 0% 98%)'
},
// Primary colors - modern indigo/blue
primary: {
light: 'hsl(221.2 83.2% 53.3%)',
dark: 'hsl(217.2 91.2% 59.8%)'
},
primaryForeground: {
light: 'hsl(0 0% 98%)',
dark: 'hsl(0 0% 4%)'
},
// Secondary colors - more subtle
secondary: {
light: 'hsl(0 0% 96%)',
dark: 'hsl(0 0% 17%)'
},
secondaryForeground: {
light: 'hsl(0 0% 11%)',
dark: 'hsl(0 0% 98%)'
},
// Muted colors - softer grays
muted: {
light: 'hsl(0 0% 96%)',
dark: 'hsl(0 0% 17%)'
},
mutedForeground: {
light: 'hsl(0 0% 46%)',
dark: 'hsl(0 0% 65%)'
},
// Accent colors - subtle hover states
accent: {
light: 'hsl(0 0% 96%)',
dark: 'hsl(0 0% 17%)'
},
accentForeground: {
light: 'hsl(0 0% 11%)',
dark: 'hsl(0 0% 98%)'
},
// Destructive colors - softer red
destructive: {
light: 'hsl(0 72.2% 50.6%)',
dark: 'hsl(0 62.8% 30.6%)'
},
destructiveForeground: {
light: 'hsl(0 0% 98%)',
dark: 'hsl(0 0% 98%)'
},
// Border color - very subtle
border: {
light: 'hsl(0 0% 91%)',
dark: 'hsl(0 0% 17%)'
},
// Input color
input: {
light: 'hsl(0 0% 91%)',
dark: 'hsl(0 0% 18%)'
},
// Ring color - subtle focus indicator
ring: {
light: 'hsl(221.2 83.2% 53.3%)',
dark: 'hsl(217.2 91.2% 59.8%)'
},
// Success colors - modern green
success: {
light: 'hsl(142.1 70.6% 45.3%)',
dark: 'hsl(144.9 80.4% 10%)'
},
successForeground: {
light: 'hsl(0 0% 100%)',
dark: 'hsl(144.9 80.4% 80%)'
},
// Chart colors
chart1: {
light: 'hsl(12 76% 61%)',
dark: 'hsl(12 76% 61%)'
},
chart2: {
light: 'hsl(173 58% 39%)',
dark: 'hsl(173 58% 39%)'
},
chart3: {
light: 'hsl(197 37% 24%)',
dark: 'hsl(197 37% 24%)'
},
chart4: {
light: 'hsl(43 74% 66%)',
dark: 'hsl(43 74% 66%)'
},
chart5: {
light: 'hsl(27 87% 67%)',
dark: 'hsl(27 87% 67%)'
}
};
// Helper function to get color based on theme
export const getColor = (colorName: keyof typeof colors, isDark: boolean = false) => {
const color = colors[colorName];
return isDark ? color.dark : color.light;
};
// CSS helper for theme-aware 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);
};

145
ts_web/elements/00fonts.ts Normal file
View File

@@ -0,0 +1,145 @@
import { css } from '@design.estate/dees-element';
// Font families matching shadcn design
export const fontFamilies = {
sans: `'Geist Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif`,
mono: `'Geist Mono', 'SF Mono', Monaco, 'Inconsolata', 'Fira Code', 'Fira Mono', 'Droid Sans Mono', 'Courier New', monospace`
};
// Font sizes following shadcn scale
export const fontSizes = {
xs: '0.75rem', // 12px
sm: '0.875rem', // 14px
base: '1rem', // 16px
lg: '1.125rem', // 18px
xl: '1.25rem', // 20px
'2xl': '1.5rem', // 24px
'3xl': '1.875rem', // 30px
'4xl': '2.25rem', // 36px
'5xl': '3rem', // 48px
'6xl': '3.75rem', // 60px
'7xl': '4.5rem', // 72px
'8xl': '6rem', // 96px
'9xl': '8rem' // 128px
};
// Line heights
export const lineHeights = {
none: '1',
tight: '1.25',
snug: '1.375',
normal: '1.5',
relaxed: '1.625',
loose: '2'
};
// Font weights
export const fontWeights = {
thin: '100',
extralight: '200',
light: '300',
normal: '400',
medium: '500',
semibold: '600',
bold: '700',
extrabold: '800',
black: '900'
};
// Letter spacing
export const letterSpacing = {
tighter: '-0.05em',
tight: '-0.025em',
normal: '0em',
wide: '0.025em',
wider: '0.05em',
widest: '0.1em'
};
// Typography presets matching shadcn components
export const typography = {
h1: css`
font-size: 2.25rem;
line-height: 1.25;
font-weight: 700;
letter-spacing: -0.025em;
`,
h2: css`
font-size: 1.875rem;
line-height: 1.25;
font-weight: 600;
letter-spacing: -0.025em;
`,
h3: css`
font-size: 1.5rem;
line-height: 1.375;
font-weight: 600;
`,
h4: css`
font-size: 1.25rem;
line-height: 1.375;
font-weight: 600;
`,
body: css`
font-size: 1rem;
line-height: 1.5;
font-weight: 400;
`,
bodyLarge: css`
font-size: 1.125rem;
line-height: 1.625;
font-weight: 400;
`,
bodySmall: css`
font-size: 0.875rem;
line-height: 1.5;
font-weight: 400;
`,
caption: css`
font-size: 0.75rem;
line-height: 1.5;
font-weight: 400;
`,
code: css`
font-family: 'Geist Mono', 'SF Mono', Monaco, 'Inconsolata', 'Fira Code', 'Fira Mono', 'Droid Sans Mono', 'Courier New', monospace;
font-size: 0.875rem;
line-height: 1.5;
font-weight: 400;
`,
button: css`
font-size: 0.875rem;
line-height: 1;
font-weight: 500;
letter-spacing: 0.025em;
`,
input: css`
font-size: 1rem;
line-height: 1.5;
font-weight: 400;
`
};
// Font loading CSS
export const fontLoadingStyles = css`
@font-face {
font-family: 'Geist Sans';
src: url('https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap');
font-display: swap;
}
@font-face {
font-family: 'Geist Mono';
src: url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@100..800&display=swap');
font-display: swap;
}
`;

197
ts_web/elements/00tokens.ts Normal file
View File

@@ -0,0 +1,197 @@
import { css, cssManager } from '@design.estate/dees-element';
// Spacing scale
export const spacing = {
0: '0px',
px: '1px',
0.5: '0.125rem', // 2px
1: '0.25rem', // 4px
1.5: '0.375rem', // 6px
2: '0.5rem', // 8px
2.5: '0.625rem', // 10px
3: '0.75rem', // 12px
3.5: '0.875rem', // 14px
4: '1rem', // 16px
5: '1.25rem', // 20px
6: '1.5rem', // 24px
7: '1.75rem', // 28px
8: '2rem', // 32px
9: '2.25rem', // 36px
10: '2.5rem', // 40px
11: '2.75rem', // 44px
12: '3rem', // 48px
14: '3.5rem', // 56px
16: '4rem', // 64px
20: '5rem', // 80px
24: '6rem', // 96px
28: '7rem', // 112px
32: '8rem', // 128px
36: '9rem', // 144px
40: '10rem', // 160px
44: '11rem', // 176px
48: '12rem', // 192px
52: '13rem', // 208px
56: '14rem', // 224px
60: '15rem', // 240px
64: '16rem', // 256px
72: '18rem', // 288px
80: '20rem', // 320px
96: '24rem' // 384px
};
// Border radius
export const radius = {
none: '0px',
sm: '0.125rem', // 2px
DEFAULT: '0.375rem', // 6px - shadcn default
md: '0.375rem', // 6px
lg: '0.5rem', // 8px
xl: '0.75rem', // 12px
'2xl': '1rem', // 16px
'3xl': '1.5rem', // 24px
full: '9999px'
};
// Box shadows - more subtle for modern look
export const shadows = {
none: 'none',
sm: '0 1px 2px 0 rgb(0 0 0 / 0.03)',
DEFAULT: '0 2px 8px -2px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.04)',
md: '0 4px 12px -4px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.04)',
lg: '0 8px 24px -4px rgb(0 0 0 / 0.1), 0 2px 8px -2px rgb(0 0 0 / 0.04)',
xl: '0 24px 48px -12px rgb(0 0 0 / 0.18)',
'2xl': '0 32px 64px -12px rgb(0 0 0 / 0.14)',
inner: 'inset 0 2px 4px 0 rgb(0 0 0 / 0.03)',
// Theme-aware shadows
card: cssManager.bdTheme(
'0 2px 8px -2px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.04)',
'0 2px 8px -2px rgb(0 0 0 / 0.3), 0 2px 4px -2px rgb(0 0 0 / 0.2)'
),
dropdown: cssManager.bdTheme(
'0 8px 24px -4px rgb(0 0 0 / 0.1), 0 2px 8px -2px rgb(0 0 0 / 0.04)',
'0 8px 24px -4px rgb(0 0 0 / 0.3), 0 2px 8px -2px rgb(0 0 0 / 0.2)'
)
};
// Transitions
export const transitions = {
all: 'all 150ms cubic-bezier(0.4, 0, 0.2, 1)',
colors: 'color, background-color, border-color, text-decoration-color, fill, stroke 150ms cubic-bezier(0.4, 0, 0.2, 1)',
opacity: 'opacity 150ms cubic-bezier(0.4, 0, 0.2, 1)',
shadow: 'box-shadow 150ms cubic-bezier(0.4, 0, 0.2, 1)',
transform: 'transform 150ms cubic-bezier(0.4, 0, 0.2, 1)',
// Durations
fast: '150ms',
normal: '200ms',
slow: '300ms',
// Timing functions
ease: 'cubic-bezier(0.4, 0, 0.2, 1)',
easeIn: 'cubic-bezier(0.4, 0, 1, 1)',
easeOut: 'cubic-bezier(0, 0, 0.2, 1)',
easeInOut: 'cubic-bezier(0.4, 0, 0.2, 1)'
};
// Z-index scale
export const zIndex = {
0: 0,
10: 10,
20: 20,
30: 30,
40: 40,
50: 50,
dropdown: 1000,
sticky: 1020,
fixed: 1030,
modalBackdrop: 1040,
modal: 1050,
popover: 1060,
tooltip: 1070
};
// Breakpoints
export const breakpoints = {
sm: '640px',
md: '768px',
lg: '1024px',
xl: '1280px',
'2xl': '1536px'
};
// Container sizes
export const containers = {
sm: '640px',
md: '768px',
lg: '1024px',
xl: '1280px',
'2xl': '1536px',
full: '100%'
};
// Common component sizes
export const sizes = {
// Button/Input heights
sm: '2rem', // 32px
DEFAULT: '2.5rem', // 40px
lg: '3rem', // 48px
// Icon sizes
icon: {
sm: '1rem', // 16px
DEFAULT: '1.25rem', // 20px
lg: '1.5rem' // 24px
}
};
// Animation keyframes
export const animations = {
fadeIn: css`
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
`,
slideIn: css`
@keyframes slideIn {
from { transform: translateY(-10px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
`,
slideUp: css`
@keyframes slideUp {
from { transform: translateY(10px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
`,
scaleIn: css`
@keyframes scaleIn {
from { transform: scale(0.95); opacity: 0; }
to { transform: scale(1); opacity: 1; }
}
`,
spin: css`
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
`
};
// Focus ring styles
export const focusRing = css`
outline: 2px solid transparent;
outline-offset: 2px;
&:focus-visible {
outline-color: ${cssManager.bdTheme('hsl(0 0% 5%)', 'hsl(0 0% 84%)')};
}
`;
// Disabled styles
export const disabled = css`
opacity: 0.5;
cursor: not-allowed;
pointer-events: none;
`;

View File

@@ -1,5 +1,16 @@
export * from './sio-fab.js';
// Core components
export * from './sio-icon.js';
export * from './sio-button.js';
export * from './sio-dropdown-menu.js';
// Conversation components
export * from './sio-conversation-selector.js';
export * from './sio-conversation-view.js';
export * from './sio-message-input.js';
export * from './sio-combox.js';
export * from './sio-subwidget-onboardme.js';
export * from './sio-subwidget-conversations.js';
// Other components
export * from './sio-fab.js';
export * from './sio-recorder.js';
export * from './sio-image-lightbox.js';
export * from './sio-pdf-viewer.js';

View File

@@ -0,0 +1,291 @@
import {
DeesElement,
html,
property,
customElement,
cssManager,
css,
unsafeCSS,
type TemplateResult,
} 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-button': SioButton;
}
}
@customElement('sio-button')
export class SioButton extends DeesElement {
public static demo = () => html`
<div style="display: flex; gap: 16px; flex-wrap: wrap; align-items: center;">
<sio-button>Default</sio-button>
<sio-button type="primary">Primary</sio-button>
<sio-button type="destructive">Delete</sio-button>
<sio-button type="outline">Outline</sio-button>
<sio-button type="ghost">Ghost</sio-button>
<sio-button size="sm">Small</sio-button>
<sio-button size="lg">Large</sio-button>
<sio-button disabled>Disabled</sio-button>
</div>
`;
@property({ type: String })
public accessor text: string = '';
@property({ type: String })
public accessor type: 'default' | 'primary' | 'secondary' | 'destructive' | 'outline' | 'ghost' = 'default';
@property({ type: String })
public accessor size: 'sm' | 'default' | 'lg' = 'default';
@property({ type: Boolean, reflect: true })
public accessor disabled: boolean = false;
@property({ type: String })
public accessor status: 'normal' | 'pending' | 'success' | 'error' = 'normal';
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: inline-block;
font-family: ${unsafeCSS(fontFamilies.sans)};
}
:host([disabled]) {
pointer-events: none;
}
.button {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
white-space: nowrap;
border-radius: 6px;
font-weight: 500;
font-size: 14px;
line-height: 1;
letter-spacing: -0.01em;
transition: all 120ms ease;
cursor: pointer;
user-select: none;
outline: none;
border: none;
gap: 6px;
}
/* Size variants */
.button.size-sm {
height: 32px;
padding: 0 12px;
font-size: 13px;
}
.button.size-default {
height: 36px;
padding: 0 16px;
font-size: 14px;
}
.button.size-lg {
height: 42px;
padding: 0 24px;
font-size: 15px;
}
/* Type variants */
.button.default {
background: ${bdTheme('hsl(0 0% 95%)', 'hsl(0 0% 15%)')};
color: ${bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 95%)')};
}
.button.default:hover:not(.disabled) {
background: ${bdTheme('hsl(0 0% 91%)', 'hsl(0 0% 20%)')};
}
.button.default:active:not(.disabled) {
background: ${bdTheme('hsl(0 0% 87%)', 'hsl(0 0% 18%)')};
}
.button.primary {
background: ${bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 95%)')};
color: ${bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 0%)')};
}
.button.primary:hover:not(.disabled) {
background: ${bdTheme('hsl(0 0% 25%)', 'hsl(0 0% 100%)')};
}
.button.primary:active:not(.disabled) {
background: ${bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 90%)')};
}
/* 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 {
background: ${bdTheme('hsl(0 100% 95%)', 'hsl(0 50% 20%)')};
color: ${bdTheme('hsl(0 100% 45%)', 'hsl(0 100% 75%)')};
}
.button.destructive:hover:not(.disabled) {
background: ${bdTheme('hsl(0 100% 45%)', 'hsl(0 100% 50%)')};
color: white;
}
.button.destructive:active:not(.disabled) {
background: ${bdTheme('hsl(0 100% 40%)', 'hsl(0 100% 45%)')};
color: white;
}
.button.outline {
background: transparent;
color: ${bdTheme('hsl(0 0% 40%)', 'hsl(0 0% 70%)')};
box-shadow: inset 0 0 0 1.5px ${bdTheme('hsl(0 0% 90%)', 'hsl(0 0% 30%)')};
}
.button.outline:hover:not(.disabled) {
color: ${bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 95%)')};
box-shadow: inset 0 0 0 1.5px ${bdTheme('hsl(0 0% 70%)', 'hsl(0 0% 50%)')};
}
.button.outline:active:not(.disabled) {
background: ${bdTheme('hsl(0 0% 95%)', 'hsl(0 0% 15%)')};
}
.button.ghost {
background: transparent;
color: ${bdTheme('hsl(0 0% 40%)', 'hsl(0 0% 70%)')};
}
.button.ghost:hover:not(.disabled) {
color: ${bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 95%)')};
background: ${bdTheme('hsl(0 0% 0% / 0.05)', 'hsl(0 0% 100% / 0.05)')};
}
.button.ghost:active:not(.disabled) {
background: ${bdTheme('hsl(0 0% 0% / 0.1)', 'hsl(0 0% 100% / 0.1)')};
}
/* Status states */
.button.pending {
pointer-events: none;
opacity: 0.7;
}
.spinner {
position: absolute;
left: ${unsafeCSS(spacing["3"])};
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.button.success {
background: ${bdTheme('success')};
color: ${bdTheme('successForeground')};
pointer-events: none;
}
.button.error {
background: ${bdTheme('destructive')};
color: ${bdTheme('destructiveForeground')};
pointer-events: none;
}
/* Disabled state */
.button.disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none !important;
}
/* Focus state */
.button:focus-visible {
outline: 2px solid ${bdTheme('hsl(0 0% 15% / 0.2)', 'hsl(0 0% 95% / 0.2)')};
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;
}
`,
];
public render(): TemplateResult {
const buttonClasses = [
'button',
this.type === 'primary' ? 'primary' : this.type,
`size-${this.size}`,
this.disabled ? 'disabled' : '',
this.status,
].filter(Boolean).join(' ');
return html`
<button
class="${buttonClasses}"
?disabled=${this.disabled}
@click=${this.handleClick}
>
${this.status === 'pending' ? html`
<sio-icon class="spinner" icon="loader" size="16"></sio-icon>
` : ''}
${this.status === 'success' ? html`
<sio-icon icon="check" size="16"></sio-icon>
` : ''}
${this.status === 'error' ? html`
<sio-icon icon="x" size="16"></sio-icon>
` : ''}
<slot>${this.text}</slot>
</button>
`;
}
private handleClick(event: MouseEvent) {
if (this.disabled || this.status !== 'normal') {
event.preventDefault();
event.stopPropagation();
return;
}
// Let the native click bubble normally
// Don't dispatch a custom event to avoid double-triggering
}
}

View File

@@ -5,196 +5,348 @@ import {
customElement,
type TemplateResult,
cssManager,
css,
unsafeCSS,
state,
} from '@design.estate/dees-element';
import * as domtools from '@design.estate/dees-domtools';
import * as deesCatalog from '@design.estate/dees-catalog';
deesCatalog;
// Import design tokens
import { colors, bdTheme } from './00colors.js';
import { spacing, radius, shadows, transitions } from './00tokens.js';
import { fontFamilies, typography } from './00fonts.js';
// Import components
import { SioConversationSelector, type IConversation } from './sio-conversation-selector.js';
import { SioConversationView, type IMessage, type IConversationData, type IAttachment } from './sio-conversation-view.js';
import { SioImageLightbox, type ILightboxImage } from './sio-image-lightbox.js';
// Make sure components are loaded
SioConversationSelector;
SioConversationView;
SioImageLightbox;
declare global {
interface HTMLElementTagNameMap {
'sio-combox': SioCombox;
}
}
@customElement('sio-combox')
export class SioCombox extends DeesElement {
public static demo = () => html` <sio-combox></sio-combox> `;
@property({ type: Object })
public referenceObject: HTMLElement;
public accessor referenceObject: HTMLElement;
/**
* computes the button offset
*/
public cssComputeHeight() {
let height = window.innerHeight < 760 ? window.innerHeight : 760;
if (!this.referenceObject) {
console.log('SioCombox: no reference object set');
}
if (this.referenceObject) {
console.log(`referenceObject height is ${this.referenceObject.clientHeight}`);
height = height - (this.referenceObject.clientHeight + 60);
}
return height;
}
@state()
private accessor selectedConversationId: string | null = null;
public cssComputeInnerScroll() {
console.log(
`SioCombox clientHeight: ${this.shadowRoot.querySelector('.mainbox').clientHeight}`
);
console.log(
`SioCombox content scrollheight is: ${
this.shadowRoot.querySelector('.contentbox').clientHeight
}`
);
if (
this.shadowRoot.querySelector('.mainbox').clientHeight <
this.shadowRoot.querySelector('.contentbox').clientHeight
) {
(this.shadowRoot.querySelector('.mainbox') as HTMLElement).style.overflowY = 'scroll';
} else {
(this.shadowRoot.querySelector('.mainbox') as HTMLElement).style.overflowY = 'hidden';
}
@state()
private accessor conversations: IConversation[] = [
{
id: '1',
title: 'Technical Support',
lastMessage: 'Thanks for your help with the login issue!',
time: '2 min ago',
unread: true,
},
{
id: '2',
title: 'Billing Question',
lastMessage: 'I need help understanding my invoice',
time: '1 hour ago',
},
{
id: '3',
title: 'Feature Request',
lastMessage: 'That would be great! Looking forward to it',
time: 'Yesterday',
},
{
id: '4',
title: 'General Inquiry',
lastMessage: 'Thank you for the information',
time: '2 days ago',
}
];
@state()
private accessor messages: { [conversationId: string]: IMessage[] } = {
'1': [
{ 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: '3', text: 'It says "Invalid credentials" but I\'m sure my password is correct', sender: 'user', time: '10:03 AM' },
{ id: '4', text: 'Let me check your account. Please try resetting your password using the forgot password link.', sender: 'support', time: '10:05 AM' },
{
id: '5',
text: 'Here\'s a screenshot of the error',
sender: 'user',
time: '10:08 AM',
attachments: [{
id: 'att1',
name: 'error-screenshot.png',
size: 245780,
type: 'image/png',
url: 'https://picsum.photos/400/300?random=1'
}]
},
{ id: '6', text: 'Thanks for your help with the login issue!', sender: 'user', time: '10:10 AM' },
{
id: '7',
text: 'Here is the documentation you requested',
sender: 'support',
time: '10:15 AM',
attachments: [{
id: 'att2',
name: 'user-guide.pdf',
size: 2457600,
type: 'application/pdf',
url: 'data:application/pdf;base64,JVBERi0xLjMKJeLjz9MKMSAwIG9iago8PAovVHlwZSAvQ2F0YWxvZwovT3V0bGluZXMgMiAwIFIKL1BhZ2VzIDMgMCBSCj4+CmVuZG9iagoyIDAgb2JqCjw8Ci9UeXBlIC9PdXRsaW5lcwovQ291bnQgMAo+PgplbmRvYmoKMyAwIG9iago8PAovVHlwZSAvUGFnZXMKL0NvdW50IDEKL0tpZHMgWzQgMCBSXQo+PgplbmRvYmoKNCAwIG9iago8PAovVHlwZSAvUGFnZQovUGFyZW50IDMgMCBSCi9NZWRpYUJveCBbMCAwIDYxMiA3OTJdCi9Db250ZW50cyA1IDAgUgovUmVzb3VyY2VzIDw8Ci9Gb250IDw8Ci9GMSA2IDAgUgo+Pgo+Pgo+PgplbmRvYmoKNSAwIG9iago8PAovTGVuZ3RoIDQ0Cj4+CnN0cmVhbQpCVApxCjcwIDUwIFRECi9GMSAxMiBUZgooSGVsbG8gV29ybGQpIFRqCkVUClEKZW5kc3RyZWFtCmVuZG9iago2IDAgb2JqCjw8Ci9UeXBlIC9Gb250Ci9TdWJ0eXBlIC9UeXBlMQovQmFzZUZvbnQgL1RpbWVzLVJvbWFuCj4+CmVuZG9iagp4cmVmCjAgNwowMDAwMDAwMDAwIDY1NTM1IGYgCjAwMDAwMDAwMDkgMDAwMDAgbiAKMDAwMDAwMDA3NCAwMDAwMCBuIAowMDAwMDAwMTIwIDAwMDAwIG4gCjAwMDAwMDAxNzkgMDAwMDAgbiAKMDAwMDAwMDM2NCAwMDAwMCBuIAowMDAwMDAwNDY2IDAwMDAwIG4gCnRyYWlsZXIKPDwKL1NpemUgNwovUm9vdCAxIDAgUgo+PgpzdGFydHhyZWYKNTY1CiUlRU9G'
}]
},
],
'2': [
{ id: '1', text: 'I need help understanding my invoice', sender: 'user', time: '9:00 AM' },
{ id: '2', text: 'I\'d be happy to help explain your invoice. Which part is unclear?', sender: 'support', time: '9:05 AM' },
],
'3': [
{ id: '1', text: 'I\'d love to see dark mode support in your app!', sender: 'user', time: 'Yesterday' },
{ id: '2', text: 'Thanks for the suggestion! We\'re actually working on dark mode and it should be available next month.', sender: 'support', time: 'Yesterday' },
{ id: '3', text: 'That would be great! Looking forward to it', sender: 'user', time: 'Yesterday' },
],
'4': [
{ id: '1', text: 'Can you tell me more about your enterprise plans?', sender: 'user', time: '2 days ago' },
{ id: '2', text: 'Of course! Our enterprise plans include advanced features like SSO, dedicated support, and custom integrations.', sender: 'support', time: '2 days ago' },
{ id: '3', text: 'Thank you for the information', sender: 'user', time: '2 days ago' },
]
};
constructor() {
super();
domtools.DomTools.setupDomTools();
}
public render(): TemplateResult {
return html`
${domtools.elementBasic.styles}
<style>
public static styles = [
cssManager.defaultStyles,
css`
:host {
overflow: hidden;
font-family: 'Dees Sans';
position: absolute;
display: block;
height: ${this.cssComputeHeight()}px;
width: 375px;
background: ${this.goBright ? '#eeeeee' : '#000000'};
border-radius: 16px;
border: 1px solid rgba(250, 250, 250, 0.2);
right: 0px;
z-index: 10000;
box-shadow: 0px 0px 5px ${this.goBright ? 'rgba(0, 0, 0, 0.3)' : 'rgba(0, 0, 0, 0.5)'};
color: ${this.goBright ? '#333' : '#ccc'};
cursor: default;
user-select: none;
text-align: left;
}
.mainbox {
position: absolute;
height: 100%;
width: 100%;
height: 600px;
width: 800px;
background: ${bdTheme('background')};
border-radius: ${unsafeCSS(radius['2xl'])};
border: 1px solid ${bdTheme('border')};
box-shadow: ${unsafeCSS(shadows.xl)};
overflow: hidden;
overscroll-behavior: contain;
padding-bottom: 80px;
font-family: ${unsafeCSS(fontFamilies.sans)};
position: relative;
transform-origin: bottom right;
}
.toppanel {
height: 200px;
box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.5);
padding: 20px;
--bg-color: ${this.goBright ? '#00000050' : '#ffffff30'};
--dot-color: #ffffff00;
--dot-size: 1px;
--dot-space: 6px;
background: linear-gradient(45deg, var(--bg-color) 1px, var(--dot-color) 1px) top left;
background-size: var(--dot-space) var(--dot-space);
margin-bottom: -50px;
:host(.animate-in) {
animation: scaleIn 300ms cubic-bezier(0.34, 1.56, 0.64, 1);
}
#greeting {
padding-top: 50px;
font-family: 'Dees Sans';
margin: 0px;
font-size: 30px;
font-weight: 400;
@keyframes scaleIn {
from {
opacity: 0;
transform: scale(0.9) translateY(10px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
#callToAction {
font-family: 'Dees Sans';
margin: 0px;
font-weight: 400;
}
.quicktabs {
:host::before {
content: '';
position: absolute;
z-index: 100;
bottom: 30px;
display: grid;
inset: 0;
border-radius: ${unsafeCSS(radius['2xl'])};
padding: 1px;
background: linear-gradient(145deg, ${bdTheme('border')}, transparent 50%);
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
-webkit-mask-composite: exclude;
mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
mask-composite: exclude;
opacity: 0.5;
pointer-events: none;
}
.container {
display: flex;
height: 100%;
overflow: visible;
border-radius: ${unsafeCSS(radius['2xl'])};
}
/* Responsive layout */
@media (max-width: 600px) {
:host {
width: 100%;
padding-bottom: 16px;
grid-template-columns: repeat(2, 1fr);
background-image: linear-gradient(to bottom, ${cssManager.bdTheme('#eeeeeb00', 'rgba(0, 0, 0, 0)')} 0%, ${cssManager.bdTheme('#eeeeebff', 'rgba(0, 0, 0, 1)')} 50%);
padding-top: 24px;
height: 100%;
border-radius: 0;
}
.quicktabs .quicktab {
text-align: center;
width: 100%;
}
.quicktabs .quicktab .quicktabicon {
font-size: 20px;
margin-bottom: 8px;
.container {
position: relative;
}
.quicktabs .quicktab .quicktabtext {
font-size: 12px;
font-weight: 600;
}
.brandingbox {
z-index: 101;
text-align: center;
sio-conversation-selector {
position: absolute;
width: 100%;
bottom: 0px;
left: 0px;
font-size: 12px;
padding: 8px;
border-top: 1px solid rgba(250, 250, 250, 0.1);
font-family: 'Dees Sans';
box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.2);
background: ${this.goBright ? '#EEE' : '#000'};
color: ${this.goBright ? '#333' : '#777'};
height: 100%;
transition: left 300ms ease, opacity 200ms ease;
}
</style>
<div class="mainbox">
<div class="contentbox">
<div class="toppanel">
<div id="greeting">Hello :)</div>
<div id="callToAction">Ask us anything or share your feedback!</div>
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-selector {
width: 320px;
flex-shrink: 0;
}
sio-conversation-view {
flex: 1;
}
}
`,
];
public render(): TemplateResult {
const selectedConversation = this.selectedConversationId
? this.conversations.find(c => c.id === this.selectedConversationId)
: null;
const conversationData: IConversationData | null = selectedConversation
? {
id: selectedConversation.id,
title: selectedConversation.title,
messages: this.messages[selectedConversation.id] || []
}
: null;
const containerClass = this.selectedConversationId ? 'show-conversation' : 'show-list';
return html`
<div class="container ${containerClass}">
<sio-conversation-selector
.conversations=${this.conversations}
.selectedConversationId=${this.selectedConversationId}
@conversation-selected=${this.handleConversationSelected}
></sio-conversation-selector>
<sio-conversation-view
.conversation=${conversationData}
@back=${this.handleBack}
@send-message=${this.handleSendMessage}
@open-image=${this.handleOpenImage}
@open-file=${this.handleOpenImage}
></sio-conversation-view>
</div>
<sio-subwidget-conversations></sio-subwidget-conversations>
<sio-subwidget-onboardme></sio-subwidget-onboardme>
</div>
</div>
<div class="quicktabs">
<div class="quicktab">
<div class="quicktabicon">
<dees-icon iconFA="message"></dees-icon>
</div>
<div class="quicktabtext">Conversations</div>
</div>
<div class="quicktab">
<div class="quicktabicon">
<dees-icon iconFA="mugHot"></dees-icon>
</div>
<div class="quicktabtext">Onboarding</div>
</div>
</div>
<div class="brandingbox">powered by social.io</div>
<sio-image-lightbox></sio-image-lightbox>
`;
}
async updated() {
this.cssComputeHeight();
window.requestAnimationFrame(() => {
private handleConversationSelected(event: CustomEvent) {
const conversation = event.detail.conversation as IConversation;
this.selectedConversationId = conversation.id;
// Mark conversation as read
const convIndex = this.conversations.findIndex(c => c.id === conversation.id);
if (convIndex !== -1) {
this.conversations[convIndex] = { ...this.conversations[convIndex], unread: false };
this.conversations = [...this.conversations];
}
}
private handleBack() {
// For mobile view, go back to conversation list
this.selectedConversationId = null;
}
private handleSendMessage(event: CustomEvent) {
const message = event.detail.message as IMessage;
const conversationId = this.selectedConversationId;
if (conversationId) {
// Add message to the conversation
if (!this.messages[conversationId]) {
this.messages[conversationId] = [];
}
this.messages[conversationId] = [...this.messages[conversationId], message];
this.messages = { ...this.messages };
// Update conversation's last message
const convIndex = this.conversations.findIndex(c => c.id === conversationId);
if (convIndex !== -1) {
this.conversations[convIndex] = {
...this.conversations[convIndex],
lastMessage: message.text,
time: 'Just now'
};
// Move conversation to top
const [conv] = this.conversations.splice(convIndex, 1);
this.conversations = [conv, ...this.conversations];
}
// Simulate a response after a delay (remove in production)
setTimeout(() => {
this.cssComputeInnerScroll();
}, 200);
});
const responseMessage: IMessage = {
id: Date.now().toString(),
text: 'Thanks for your message! We\'ll get back to you shortly.',
sender: 'support',
time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
};
this.messages[conversationId] = [...this.messages[conversationId], responseMessage];
this.messages = { ...this.messages };
}, 3000);
}
}
private handleOpenImage(event: CustomEvent) {
const attachment = event.detail.attachment as IAttachment;
const lightbox = this.shadowRoot?.querySelector('sio-image-lightbox') as SioImageLightbox;
if (lightbox && attachment) {
const lightboxFile: ILightboxImage = {
url: attachment.url,
name: attachment.name,
size: attachment.size,
type: attachment.type
};
lightbox.open(lightboxFile);
}
}
}

View File

@@ -0,0 +1,349 @@
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';
// Types
export interface IConversation {
id: string;
title: string;
lastMessage: string;
time: string;
unread?: boolean;
avatar?: string;
}
declare global {
interface HTMLElementTagNameMap {
'sio-conversation-selector': SioConversationSelector;
}
}
@customElement('sio-conversation-selector')
export class SioConversationSelector extends DeesElement {
public static demo = () => html`
<sio-conversation-selector style="width: 320px; height: 600px;"></sio-conversation-selector>
`;
@property({ type: Array })
public accessor conversations: IConversation[] = [];
@property({ type: String })
public accessor selectedConversationId: string | null = null;
@state()
private accessor searchQuery: string = '';
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: flex;
flex-direction: column;
height: 100%;
background: ${bdTheme('card')};
border-right: 1px solid ${bdTheme('border')};
font-family: ${unsafeCSS(fontFamilies.sans)};
}
.header {
padding: ${unsafeCSS(spacing["5"])} ${unsafeCSS(spacing["4"])};
border-bottom: 1px solid ${bdTheme('border')};
background: ${bdTheme('background')};
}
.header-top {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: ${unsafeCSS(spacing["4"])};
}
.title {
font-size: 1.25rem;
line-height: 1.2;
font-weight: 600;
margin: 0;
color: ${bdTheme('foreground')};
letter-spacing: -0.025em;
}
.search-box {
position: relative;
}
.search-input {
width: 100%;
padding: ${unsafeCSS(spacing["2.5"])} ${unsafeCSS(spacing["10"])} ${unsafeCSS(spacing["2.5"])} ${unsafeCSS(spacing["3"])};
background: ${bdTheme('background')};
border: 1px solid ${bdTheme('border')};
border-radius: ${unsafeCSS(radius.lg)};
font-size: 0.875rem;
color: ${bdTheme('foreground')};
outline: none;
transition: ${unsafeCSS(transitions.all)};
font-family: ${unsafeCSS(fontFamilies.sans)};
box-shadow: ${unsafeCSS(shadows.sm)};
}
.search-input::placeholder {
color: ${bdTheme('mutedForeground')};
font-weight: 400;
}
.search-input:focus {
border-color: ${bdTheme('ring')};
box-shadow: 0 0 0 3px ${bdTheme('ring')}20;
background: ${bdTheme('background')};
}
.search-icon {
position: absolute;
right: ${unsafeCSS(spacing["3"])};
top: 50%;
transform: translateY(-50%);
color: ${bdTheme('mutedForeground')};
}
.conversation-list {
flex: 1;
overflow-y: auto;
padding: ${unsafeCSS(spacing["2"])};
}
.conversation-item {
padding: ${unsafeCSS(spacing["3.5"])};
margin-bottom: ${unsafeCSS(spacing["1.5"])};
background: ${bdTheme('background')};
border: 1px solid transparent;
border-radius: ${unsafeCSS(radius.lg)};
cursor: pointer;
transition: ${unsafeCSS(transitions.all)};
position: relative;
}
.conversation-item:hover {
background: ${bdTheme('accent')};
transform: translateX(2px);
box-shadow: ${unsafeCSS(shadows.sm)};
}
.conversation-item.selected {
background: ${bdTheme('accent')};
border-color: ${bdTheme('border')};
box-shadow: ${unsafeCSS(shadows.sm)};
}
.conversation-item.selected::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 3px;
height: 60%;
background: ${bdTheme('primary')};
border-radius: 0 3px 3px 0;
animation: slideIn 200ms ease-out;
}
@keyframes slideIn {
from {
width: 0;
opacity: 0;
}
to {
width: 3px;
opacity: 1;
}
}
.conversation-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: ${unsafeCSS(spacing["1"])};
}
.conversation-title {
font-weight: 500;
color: ${bdTheme('foreground')};
display: flex;
align-items: center;
gap: ${unsafeCSS(spacing["2"])};
font-size: 0.9375rem;
letter-spacing: -0.01em;
}
.conversation-time {
font-size: 0.75rem;
line-height: 1.5;
color: ${bdTheme('mutedForeground')};
opacity: 0.8;
}
.conversation-preview {
font-size: 0.8125rem;
line-height: 1.5;
color: ${bdTheme('mutedForeground')};
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-top: ${unsafeCSS(spacing["0.5"])};
}
.unread-dot {
display: inline-block;
width: 6px;
height: 6px;
background: ${bdTheme('primary')};
border-radius: 50%;
animation: pulse 2s ease-in-out infinite;
box-shadow: 0 0 0 0 ${bdTheme('primary')};
}
@keyframes pulse {
0% {
opacity: 1;
transform: scale(1);
box-shadow: 0 0 0 0 ${bdTheme('primary')}40;
}
50% {
opacity: 0.9;
transform: scale(1.05);
box-shadow: 0 0 0 4px ${bdTheme('primary')}00;
}
100% {
opacity: 1;
transform: scale(1);
box-shadow: 0 0 0 0 ${bdTheme('primary')}00;
}
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
padding: ${unsafeCSS(spacing["4"])};
text-align: center;
color: ${bdTheme('mutedForeground')};
gap: ${unsafeCSS(spacing["3"])};
}
.empty-icon {
font-size: 48px;
opacity: 0.5;
}
/* Scrollbar styling */
.conversation-list::-webkit-scrollbar {
width: 6px;
}
.conversation-list::-webkit-scrollbar-track {
background: transparent;
}
.conversation-list::-webkit-scrollbar-thumb {
background: ${bdTheme('border')};
border-radius: 3px;
}
.conversation-list::-webkit-scrollbar-thumb:hover {
background: ${bdTheme('mutedForeground')};
}
`,
];
public render(): TemplateResult {
const filteredConversations = this.conversations.filter(conv =>
conv.title.toLowerCase().includes(this.searchQuery.toLowerCase()) ||
conv.lastMessage.toLowerCase().includes(this.searchQuery.toLowerCase())
);
return html`
<div class="header">
<div class="header-top">
<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">
<input
type="text"
class="search-input"
placeholder="Search conversations..."
.value=${this.searchQuery}
@input=${(e: Event) => this.searchQuery = (e.target as HTMLInputElement).value}
/>
<sio-icon class="search-icon" icon="search" size="16"></sio-icon>
</div>
</div>
${filteredConversations.length > 0 ? html`
<div class="conversation-list">
${filteredConversations.map(conv => html`
<div
class="conversation-item ${this.selectedConversationId === conv.id ? 'selected' : ''}"
@click=${() => this.selectConversation(conv)}
>
<div class="conversation-header">
<span class="conversation-title">
${conv.title}
${conv.unread ? html`<span class="unread-dot"></span>` : ''}
</span>
<span class="conversation-time">${conv.time}</span>
</div>
<div class="conversation-preview">${conv.lastMessage}</div>
</div>
`)}
</div>
` : html`
<div class="empty-state">
<sio-icon class="empty-icon" icon="message-square"></sio-icon>
<h3>${this.searchQuery ? 'No matching conversations' : 'No conversations yet'}</h3>
<p>${this.searchQuery ? 'Try a different search term' : 'Start a new conversation to get started'}</p>
</div>
`}
`;
}
private selectConversation(conversation: IConversation) {
this.selectedConversationId = conversation.id;
// Dispatch event for parent components
this.dispatchEvent(new CustomEvent('conversation-selected', {
detail: { conversation },
bubbles: 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
}));
}
}

View File

@@ -0,0 +1,808 @@
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';
import { SioDropdownMenu, type IDropdownMenuItem } from './sio-dropdown-menu.js';
import { SioMessageInput } from './sio-message-input.js';
// Make sure components are loaded
SioDropdownMenu;
SioMessageInput;
// Types
export interface IAttachment {
id: string;
name: string;
size: number;
type: string;
url: string;
thumbnailUrl?: string;
}
export interface IMessage {
id: string;
text: string;
sender: 'user' | 'support';
time: string;
status?: 'sending' | 'sent' | 'delivered' | 'read';
attachments?: IAttachment[];
}
export interface IConversationData {
id: string;
title: string;
messages: IMessage[];
}
declare global {
interface HTMLElementTagNameMap {
'sio-conversation-view': SioConversationView;
}
}
@customElement('sio-conversation-view')
export class SioConversationView extends DeesElement {
public static demo = () => html`
<sio-conversation-view style="width: 600px; height: 600px;"></sio-conversation-view>
`;
@property({ type: Object })
public accessor conversation: IConversationData | null = null;
@state()
private accessor isTyping: boolean = false;
@state()
private accessor isDragging: boolean = false;
@state()
private accessor uploadingFiles: Map<string, { file: File; progress: number }> = new Map();
@state()
private accessor pendingAttachments: IAttachment[] = [];
private dropdownMenuItems: IDropdownMenuItem[] = [
{ id: 'mute', label: 'Mute notifications', icon: 'bell-off' },
{ id: 'pin', label: 'Pin conversation', icon: 'pin' },
{ id: 'search', label: 'Search in chat', icon: 'search' },
{ id: 'divider1', label: '', divider: true },
{ id: 'export', label: 'Export chat', icon: 'download' },
{ id: 'archive', label: 'Archive conversation', icon: 'archive' },
{ id: 'divider2', label: '', divider: true },
{ id: 'clear', label: 'Clear history', icon: 'trash-2', destructive: true }
];
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: flex;
flex-direction: column;
height: 100%;
background: ${bdTheme('background')};
font-family: ${unsafeCSS(fontFamilies.sans)};
}
.header {
padding: ${unsafeCSS(spacing["4"])};
border-bottom: 1px solid ${bdTheme('border')};
background: ${bdTheme('background')};
display: flex;
align-items: center;
gap: ${unsafeCSS(spacing["3"])};
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
position: sticky;
top: 0;
z-index: 10;
overflow: visible;
}
.back-button {
display: none;
}
@media (max-width: 600px) {
.back-button {
display: block;
}
}
.header-title {
font-size: 1.125rem;
line-height: 1.5;
font-weight: 600;
margin: 0;
color: ${bdTheme('foreground')};
flex: 1;
}
.header-actions {
display: flex;
gap: ${unsafeCSS(spacing["2"])};
position: relative;
overflow: visible;
}
.messages-container {
flex: 1;
overflow-y: auto;
padding: ${unsafeCSS(spacing["4"])};
display: flex;
flex-direction: column;
gap: ${unsafeCSS(spacing["3"])};
}
.message {
display: flex;
align-items: flex-start;
gap: ${unsafeCSS(spacing["3"])};
max-width: 70%;
}
.message.user {
align-self: flex-end;
flex-direction: row-reverse;
}
.message-bubble {
padding: ${unsafeCSS(spacing["2.5"])} ${unsafeCSS(spacing["3.5"])};
border-radius: ${unsafeCSS(radius["2xl"])};
font-size: 0.9375rem;
line-height: 1.6;
position: relative;
box-shadow: ${unsafeCSS(shadows.sm)};
max-width: 100%;
word-wrap: break-word;
}
.message.support .message-bubble {
background: ${bdTheme('secondary')};
color: ${bdTheme('secondaryForeground')};
border-bottom-left-radius: ${unsafeCSS(spacing["1"])};
border: 1px solid ${bdTheme('border')};
}
.message.user .message-bubble {
background: ${bdTheme('primary')};
color: ${bdTheme('primaryForeground')};
border-bottom-right-radius: ${unsafeCSS(spacing["1"])};
}
.message-time {
font-size: 0.75rem;
line-height: 1.5;
color: ${bdTheme('mutedForeground')};
margin-top: ${unsafeCSS(spacing["1"])};
}
.message.user .message-time {
text-align: right;
}
.typing-indicator {
display: flex;
align-items: center;
gap: ${unsafeCSS(spacing["1"])};
padding: ${unsafeCSS(spacing["2"])} ${unsafeCSS(spacing["3"])};
background: ${bdTheme('muted')};
border-radius: ${unsafeCSS(radius.lg)};
width: fit-content;
}
.typing-dot {
width: 8px;
height: 8px;
background: ${bdTheme('mutedForeground')};
border-radius: 50%;
animation: typing 1.4s infinite;
}
.typing-dot:nth-child(2) {
animation-delay: 0.2s;
}
.typing-dot:nth-child(3) {
animation-delay: 0.4s;
}
@keyframes typing {
0%, 60%, 100% {
opacity: 0.3;
transform: scale(0.8);
}
30% {
opacity: 1;
transform: scale(1);
}
}
.input-container {
padding: ${unsafeCSS(spacing["4"])};
border-top: 1px solid ${bdTheme('border')};
background: ${bdTheme('background')};
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
.empty-state {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
gap: ${unsafeCSS(spacing["4"])};
padding: ${unsafeCSS(spacing["8"])};
text-align: center;
color: ${bdTheme('mutedForeground')};
animation: fadeIn 500ms ease-out;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.empty-icon {
font-size: 64px;
opacity: 0.5;
}
/* Scrollbar styling */
.messages-container::-webkit-scrollbar {
width: 6px;
}
.messages-container::-webkit-scrollbar-track {
background: transparent;
}
.messages-container::-webkit-scrollbar-thumb {
background: ${bdTheme('border')};
border-radius: 3px;
}
.messages-container::-webkit-scrollbar-thumb:hover {
background: ${bdTheme('mutedForeground')};
}
/* File drop zone */
.messages-container {
position: relative;
}
.drop-overlay {
position: absolute;
inset: 0;
background: ${bdTheme('background')}95;
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
pointer-events: none;
opacity: 0;
transition: opacity 200ms ease;
}
.drop-overlay.active {
opacity: 1;
pointer-events: all;
}
.drop-zone {
padding: ${unsafeCSS(spacing["8"])};
border: 2px dashed ${bdTheme('border')};
border-radius: ${unsafeCSS(radius.xl)};
background: ${bdTheme('card')};
text-align: center;
transition: ${unsafeCSS(transitions.all)};
}
.drop-overlay.active .drop-zone {
border-color: ${bdTheme('primary')};
background: ${bdTheme('accent')};
transform: scale(1.02);
}
.drop-icon {
font-size: 48px;
color: ${bdTheme('primary')};
margin-bottom: ${unsafeCSS(spacing["4"])};
}
.drop-text {
font-size: 1.125rem;
font-weight: 500;
color: ${bdTheme('foreground')};
margin-bottom: ${unsafeCSS(spacing["2"])};
}
.drop-hint {
font-size: 0.875rem;
color: ${bdTheme('mutedForeground')};
}
/* File attachments */
.message-attachments {
margin-top: ${unsafeCSS(spacing["2"])};
display: flex;
flex-wrap: wrap;
gap: ${unsafeCSS(spacing["2"])};
}
.attachment-image {
max-width: 200px;
max-height: 200px;
border-radius: ${unsafeCSS(radius.lg)};
overflow: hidden;
cursor: pointer;
transition: ${unsafeCSS(transitions.all)};
box-shadow: ${unsafeCSS(shadows.sm)};
}
.attachment-image:hover {
transform: scale(1.02);
box-shadow: ${unsafeCSS(shadows.md)};
}
.attachment-image img {
width: 100%;
height: 100%;
object-fit: cover;
}
.attachment-file {
display: flex;
align-items: center;
gap: ${unsafeCSS(spacing["2"])};
padding: ${unsafeCSS(spacing["2"])} ${unsafeCSS(spacing["3"])};
background: ${bdTheme('secondary')};
border: 1px solid ${bdTheme('border')};
border-radius: ${unsafeCSS(radius.md)};
font-size: 0.875rem;
cursor: pointer;
transition: ${unsafeCSS(transitions.all)};
}
.attachment-file:hover {
background: ${bdTheme('accent')};
}
.attachment-name {
font-weight: 500;
color: ${bdTheme('foreground')};
}
.attachment-size {
color: ${bdTheme('mutedForeground')};
font-size: 0.75rem;
}
/* Pending attachments */
.pending-attachments {
padding: ${unsafeCSS(spacing["2"])} ${unsafeCSS(spacing["3"])};
background: ${bdTheme('secondary')};
border: 1px solid ${bdTheme('border')};
border-radius: ${unsafeCSS(radius.lg)};
margin-bottom: ${unsafeCSS(spacing["2"])};
}
.pending-attachment {
display: flex;
align-items: center;
gap: ${unsafeCSS(spacing["2"])};
padding: ${unsafeCSS(spacing["1"])} 0;
}
.pending-attachment-info {
flex: 1;
min-width: 0;
}
.pending-attachment-name {
font-size: 0.875rem;
font-weight: 500;
color: ${bdTheme('foreground')};
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.pending-attachment-size {
font-size: 0.75rem;
color: ${bdTheme('mutedForeground')};
}
.remove-attachment {
padding: ${unsafeCSS(spacing["1"])};
cursor: pointer;
color: ${bdTheme('mutedForeground')};
transition: ${unsafeCSS(transitions.all)};
}
.remove-attachment:hover {
color: ${bdTheme('destructive')};
}
`,
];
public render(): TemplateResult {
if (!this.conversation) {
return html`
<div class="empty-state">
<sio-icon class="empty-icon" icon="message-square"></sio-icon>
<h3>Select a conversation</h3>
<p>Choose a conversation from the sidebar to start messaging</p>
</div>
`;
}
return html`
<div class="header">
<sio-button
class="back-button"
type="ghost"
size="sm"
@click=${this.handleBack}
>
<sio-icon icon="arrow-left" size="16"></sio-icon>
</sio-button>
<h3 class="header-title">${this.conversation.title}</h3>
<div class="header-actions">
<sio-button type="ghost" size="sm">
<sio-icon icon="phone" size="16"></sio-icon>
</sio-button>
<sio-dropdown-menu
.items=${this.dropdownMenuItems}
@item-selected=${this.handleDropdownAction}
>
<sio-button type="ghost" size="sm">
<sio-icon icon="more-vertical" size="16"></sio-icon>
</sio-button>
</sio-dropdown-menu>
</div>
</div>
<div class="messages-container" id="messages"
@dragover=${this.handleDragOver}
@dragleave=${this.handleDragLeave}
@drop=${this.handleDrop}
@dragenter=${this.handleDragOver}
>
<div class="drop-overlay ${this.isDragging ? 'active' : ''}"
@drop=${this.handleDrop}
@dragover=${(e: DragEvent) => e.preventDefault()}
>
<div class="drop-zone">
<sio-icon class="drop-icon" icon="upload-cloud"></sio-icon>
<div class="drop-text">Drop files here</div>
<div class="drop-hint">Images and documents up to 10MB</div>
</div>
</div>
${this.conversation.messages.map((msg, index) => html`
<div class="message ${msg.sender}" style="animation-delay: ${index * 50}ms">
<div class="message-content">
<div class="message-bubble">
${msg.text}
</div>
${msg.attachments && msg.attachments.length > 0 ? html`
<div class="message-attachments">
${msg.attachments.map(attachment =>
this.isImage(attachment.type) ? html`
<div class="attachment-image" @click=${() => this.openImage(attachment)}>
<img src="${attachment.url}" alt="${attachment.name}" />
</div>
` : attachment.type?.includes('pdf') || attachment.name?.toLowerCase().endsWith('.pdf') ? html`
<div class="attachment-file" @click=${() => this.openImage(attachment)}>
<sio-icon icon="file-text" size="16"></sio-icon>
<div>
<div class="attachment-name">${attachment.name}</div>
<div class="attachment-size">${this.formatFileSize(attachment.size)}</div>
</div>
</div>
` : html`
<div class="attachment-file" @click=${() => this.downloadFile(attachment)}>
<sio-icon icon="file" size="16"></sio-icon>
<div>
<div class="attachment-name">${attachment.name}</div>
<div class="attachment-size">${this.formatFileSize(attachment.size)}</div>
</div>
</div>
`
)}
</div>
` : ''}
<div class="message-time">${msg.time}</div>
</div>
</div>
`)}
${this.isTyping ? html`
<div class="message support">
<div class="typing-indicator">
<div class="typing-dot"></div>
<div class="typing-dot"></div>
<div class="typing-dot"></div>
</div>
</div>
` : ''}
</div>
<div class="input-container">
${this.pendingAttachments.length > 0 ? html`
<div class="pending-attachments">
${this.pendingAttachments.map(attachment => html`
<div class="pending-attachment">
<sio-icon icon="${this.getFileIcon(attachment.type)}" size="16"></sio-icon>
<div class="pending-attachment-info">
<div class="pending-attachment-name">${attachment.name}</div>
<div class="pending-attachment-size">${this.formatFileSize(attachment.size)}</div>
</div>
<div class="remove-attachment" @click=${() => this.removeAttachment(attachment.id)}>
<sio-icon icon="x" size="16"></sio-icon>
</div>
</div>
`)}
</div>
` : ''}
<sio-message-input
@send-message=${this.handleMessageSend}
@files-selected=${this.handleFilesSelected}
></sio-message-input>
</div>
`;
}
private handleBack() {
this.dispatchEvent(new CustomEvent('back', {
bubbles: true,
composed: true
}));
}
private handleMessageSend(event: CustomEvent) {
const { text, attachments } = event.detail;
if (!text.trim() && attachments.length === 0) return;
const message: IMessage = {
id: Date.now().toString(),
text: text.trim(),
sender: 'user',
time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
status: 'sending',
attachments: [...this.pendingAttachments]
};
// Dispatch event for parent to handle
this.dispatchEvent(new CustomEvent('send-message', {
detail: { message },
bubbles: true,
composed: true
}));
// Clear pending attachments
this.pendingAttachments = [];
// Simulate typing indicator (remove in production)
setTimeout(() => {
this.isTyping = true;
setTimeout(() => {
this.isTyping = false;
}, 2000);
}, 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() {
// Scroll to bottom when new messages arrive
const container = this.shadowRoot?.querySelector('#messages');
if (container) {
container.scrollTop = container.scrollHeight;
}
}
// File handling methods
private handleDragOver(e: DragEvent) {
e.preventDefault();
e.stopPropagation();
this.isDragging = true;
}
private handleDragLeave(e: DragEvent) {
e.preventDefault();
e.stopPropagation();
// Check if we're actually leaving the messages container
const relatedTarget = e.relatedTarget as Node;
const container = e.currentTarget as HTMLElement;
if (!container.contains(relatedTarget)) {
this.isDragging = false;
}
}
private handleDrop(e: DragEvent) {
e.preventDefault();
e.stopPropagation();
this.isDragging = false;
const files = Array.from(e.dataTransfer?.files || []);
if (files.length > 0) {
this.processFiles(files);
}
}
private openFileSelector() {
const fileInput = this.shadowRoot?.querySelector('#fileInput') as HTMLInputElement;
if (fileInput) {
fileInput.click();
}
}
private handleFileSelect(e: Event) {
const input = e.target as HTMLInputElement;
const files = Array.from(input.files || []);
this.processFiles(files);
input.value = ''; // Clear input for re-selection
}
private async processFiles(files: File[]) {
const maxSize = 10 * 1024 * 1024; // 10MB
const validFiles = files.filter(file => {
if (file.size > maxSize) {
console.warn(`File ${file.name} exceeds 10MB limit`);
return false;
}
return true;
});
for (const file of validFiles) {
const id = `${Date.now()}-${Math.random()}`;
const url = await this.fileToDataUrl(file);
const attachment: IAttachment = {
id,
name: file.name,
size: file.size,
type: file.type,
url,
thumbnailUrl: this.isImage(file.type) ? url : undefined
};
this.pendingAttachments = [...this.pendingAttachments, attachment];
}
}
private fileToDataUrl(file: File): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result as string);
reader.onerror = reject;
reader.readAsDataURL(file);
});
}
private removeAttachment(id: string) {
this.pendingAttachments = this.pendingAttachments.filter(a => a.id !== id);
}
private isImage(type: string): boolean {
return type.startsWith('image/');
}
private formatFileSize(bytes: number): string {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
private getFileIcon(type: string): string {
if (this.isImage(type)) return 'image';
if (type.includes('pdf')) return 'file-text';
if (type.includes('doc')) return 'file-text';
if (type.includes('sheet') || type.includes('excel')) return 'table';
return 'file';
}
private openImage(attachment: IAttachment) {
// Check if it's actually a PDF
if (attachment.type?.includes('pdf') || attachment.name?.toLowerCase().endsWith('.pdf')) {
this.dispatchEvent(new CustomEvent('open-file', {
detail: { attachment },
bubbles: true,
composed: true
}));
} else {
this.dispatchEvent(new CustomEvent('open-image', {
detail: { attachment },
bubbles: true,
composed: true
}));
}
}
private downloadFile(attachment: IAttachment) {
const a = document.createElement('a');
a.href = attachment.url;
a.download = attachment.name;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
private handleDropdownAction(event: CustomEvent) {
const { item } = event.detail as { item: IDropdownMenuItem };
// Dispatch event for parent to handle these actions
this.dispatchEvent(new CustomEvent('conversation-action', {
detail: {
action: item.id,
conversationId: this.conversation?.id
},
bubbles: true,
composed: true
}));
// Log action for demo purposes
console.log('Conversation action:', item.id, item.label);
// Handle some actions locally for demo
switch (item.id) {
case 'search':
// Could open a search overlay
console.log('Opening search...');
break;
case 'export':
// Export conversation as JSON/text
this.exportConversation();
break;
}
}
private exportConversation() {
if (!this.conversation) return;
const exportData = {
conversation: this.conversation.title,
exportDate: new Date().toISOString(),
messages: this.conversation.messages
};
const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${this.conversation.title.replace(/\s+/g, '-')}-${Date.now()}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
}

View File

@@ -0,0 +1,298 @@
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 } from './00fonts.js';
export interface IDropdownMenuItem {
id: string;
label: string;
icon?: string;
divider?: boolean;
destructive?: boolean;
disabled?: boolean;
}
declare global {
interface HTMLElementTagNameMap {
'sio-dropdown-menu': SioDropdownMenu;
}
}
@customElement('sio-dropdown-menu')
export class SioDropdownMenu extends DeesElement {
public static demo = () => html`
<div style="position: relative; height: 200px; display: flex; justify-content: center; padding-top: 50px;">
<sio-dropdown-menu .items=${[
{ id: 'mute', label: 'Mute notifications', icon: 'bell-off' },
{ id: 'pin', label: 'Pin conversation', icon: 'pin' },
{ id: 'divider1', label: '', divider: true },
{ id: 'export', label: 'Export chat', icon: 'download' },
{ id: 'clear', label: 'Clear history', icon: 'trash-2', destructive: true }
]}>
<sio-button type="ghost" size="sm">
<sio-icon icon="more-vertical" size="16"></sio-icon>
</sio-button>
</sio-dropdown-menu>
</div>
`;
@property({ type: Array })
public accessor items: IDropdownMenuItem[] = [];
@property({ type: String })
public accessor align: 'left' | 'right' = 'right';
@state()
private accessor isOpen: boolean = false;
private documentClickHandler: (e: MouseEvent) => void;
private scrollHandler: () => void;
private resizeHandler: () => void;
public static styles = [
cssManager.defaultStyles,
css`
:host {
position: relative;
display: inline-block;
font-family: ${unsafeCSS(fontFamilies.sans)};
}
.trigger {
cursor: pointer;
}
.dropdown {
position: absolute;
top: calc(100% + 10px);
right: 0;
min-width: 200px;
background: ${bdTheme('background')};
border: 1px solid ${bdTheme('border')};
border-radius: ${unsafeCSS(radius.lg)};
box-shadow: ${unsafeCSS(shadows.lg)};
overflow: hidden;
z-index: 100000;
opacity: 0;
transform: translateY(-10px) scale(0.95);
pointer-events: none;
transition: all 200ms cubic-bezier(0.34, 1.56, 0.64, 1);
transform-origin: top right;
}
.dropdown.align-left {
right: auto;
left: 0;
transform-origin: top left;
}
.dropdown.open {
opacity: 1;
transform: translateY(0) scale(1);
pointer-events: all;
}
.menu-item {
display: flex;
align-items: center;
gap: ${unsafeCSS(spacing["3"])};
padding: ${unsafeCSS(spacing["2.5"])} ${unsafeCSS(spacing["3"])};
font-size: 0.875rem;
line-height: 1.5;
color: ${bdTheme('foreground')};
cursor: pointer;
transition: ${unsafeCSS(transitions.all)};
user-select: none;
border: none;
background: none;
width: 100%;
text-align: left;
}
.menu-item:hover:not(.disabled) {
background: ${bdTheme('accent')};
}
.menu-item:active:not(.disabled) {
transform: scale(0.98);
}
.menu-item.disabled {
opacity: 0.5;
cursor: not-allowed;
}
.menu-item.destructive {
color: ${bdTheme('destructive')};
}
.menu-item.destructive:hover:not(.disabled) {
background: ${bdTheme('destructive')}10;
}
.menu-icon {
flex-shrink: 0;
color: ${bdTheme('mutedForeground')};
}
.menu-item.destructive .menu-icon {
color: ${bdTheme('destructive')};
}
.menu-label {
flex: 1;
}
.divider {
height: 1px;
background: ${bdTheme('border')};
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);
}
}
`,
];
public render(): TemplateResult {
return html`
<div class="trigger" @click=${this.toggleDropdown}>
<slot></slot>
</div>
<div class="dropdown ${this.isOpen ? 'open' : ''} align-${this.align}">
${this.items.map(item =>
item.divider ? html`
<div class="divider"></div>
` : html`
<button
class="menu-item ${item.destructive ? 'destructive' : ''} ${item.disabled ? 'disabled' : ''}"
@click=${() => this.handleItemClick(item)}
?disabled=${item.disabled}
>
${item.icon ? html`
<sio-icon class="menu-icon" icon="${item.icon}" size="16"></sio-icon>
` : ''}
<span class="menu-label">${item.label}</span>
</button>
`
)}
</div>
`;
}
private toggleDropdown = (e: Event) => {
console.log('[Dropdown] Toggle called, current state:', this.isOpen);
e.preventDefault();
e.stopPropagation();
this.isOpen = !this.isOpen;
console.log('[Dropdown] New state:', this.isOpen);
if (this.isOpen) {
this.addDocumentListener();
} else {
this.removeDocumentListener();
}
}
private updateDropdownPosition() {
// For absolute positioning, we don't need to calculate position dynamically
// The CSS handles it with top: calc(100% + 10px) and right: 0
console.log('[Dropdown] Position is handled by CSS (absolute positioning)');
}
private handleItemClick(item: IDropdownMenuItem) {
if (item.disabled || item.divider) return;
this.isOpen = false;
this.removeDocumentListener();
// Dispatch custom event with the selected item
this.dispatchEvent(new CustomEvent('item-selected', {
detail: { item },
bubbles: true,
composed: true
}));
}
private addDocumentListener() {
// Close dropdown when clicking outside
this.documentClickHandler = (e: MouseEvent) => {
const path = e.composedPath();
if (!path.includes(this)) {
this.isOpen = false;
this.removeDocumentListener();
}
};
// Update position on scroll/resize
this.scrollHandler = () => this.updateDropdownPosition();
this.resizeHandler = () => {
this.updateDropdownPosition();
if (window.innerWidth <= 600) {
// Close on mobile resize to prevent positioning issues
this.isOpen = false;
this.removeDocumentListener();
}
};
// Delay to avoid immediate closing
setTimeout(() => {
document.addEventListener('click', this.documentClickHandler);
window.addEventListener('scroll', this.scrollHandler, true);
window.addEventListener('resize', this.resizeHandler);
}, 0);
}
private removeDocumentListener() {
if (this.documentClickHandler) {
document.removeEventListener('click', this.documentClickHandler);
}
if (this.scrollHandler) {
window.removeEventListener('scroll', this.scrollHandler, true);
}
if (this.resizeHandler) {
window.removeEventListener('resize', this.resizeHandler);
}
}
public async disconnectedCallback() {
await super.disconnectedCallback();
this.removeDocumentListener();
}
public close() {
this.isOpen = false;
this.removeDocumentListener();
}
}

View File

@@ -6,11 +6,20 @@ import {
type TemplateResult,
cssManager,
css,
unsafeCSS,
state,
} from '@design.estate/dees-element';
import * as domtools from '@design.estate/dees-domtools';
import { SioCombox } from './sio-combox.js';
import { SioIcon } from './sio-icon.js';
SioCombox;
SioIcon;
// Import design tokens
import { colors, bdTheme } from './00colors.js';
import { spacing, radius, shadows, transitions, sizes } from './00tokens.js';
import { fontFamilies, typography } from './00fonts.js';
declare global {
interface HTMLElementTagNameMap {
@@ -20,8 +29,14 @@ declare global {
@customElement('sio-fab')
export class SioFab extends DeesElement {
@property()
public showCombox = false;
@property({ type: Boolean })
public accessor showCombox = false;
@state()
private accessor hasShownOnce = false;
@state()
private accessor shouldPulse = false;
public static demo = () => html` <sio-fab .showCombox=${true}></sio-fab> `;
@@ -30,10 +45,9 @@ export class SioFab extends DeesElement {
domtools.DomTools.setupDomTools();
}
public render(): TemplateResult {
return html`
${domtools.elementBasic.styles}
<style>
public static styles = [
cssManager.defaultStyles,
css`
:host {
will-change: transform;
position: absolute;
@@ -42,125 +56,209 @@ export class SioFab extends DeesElement {
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: all 0.2s;
transition: ${unsafeCSS(transitions.all)};
position: absolute;
bottom: 0px;
right: 0px;
height: 60px;
width: 60px;
box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.3);
line-height: 60px;
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;
color: #ccc;
cursor: pointer;
background: ${this.goBright
? 'linear-gradient(-45deg, #eeeeeb, #eeeeeb)'
: 'linear-gradient(-45deg, #222222, #333333)'};
border-radius: 50% 50% 50% 50%;
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.05);
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.95);
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;
transform: ${this.showCombox ? 'rotate(0deg)' : 'rotate(-360deg)'};
transition: all 0.2s;
will-change: transform, opacity;
transition: all 200ms cubic-bezier(0.4, 0, 0.2, 1);
height: 100%;
width: 100%;
object-fit: contain;
-webkit-user-drag: none;
-khtml-user-drag: none;
-moz-user-drag: none;
-o-user-drag: none;
user-drag: none;
}
#mainbox .icon img {
filter: grayscale(1) ${cssManager.bdTheme('invert(1)', '')};
position: absolute;
width: 100%;
top: 0px;
left: 0px;
will-change: transform;
transform: scale(0.2, 0.2) translateY(-5px);
}
#mainbox .icon.open:hover img {
filter: grayscale(0);
display: flex;
align-items: center;
justify-content: center;
}
#mainbox .icon.open {
opacity: ${this.showCombox ? '0' : '1'};
pointer-events: ${this.showCombox ? 'none' : 'all'};
opacity: 1;
transform: rotate(0deg) scale(1);
}
#mainbox .icon.close {
opacity: ${this.showCombox ? '1' : '0'};
pointer-events: ${this.showCombox ? 'all' : 'none'};
}
#mainbox .icon.close:hover dees-icon {
color: ${cssManager.bdTheme('#111', '#fff')};
opacity: 0;
transform: rotate(-45deg) scale(0.9);
}
#mainbox .icon.open dees-icon {
position: absolute;
width: 100%;
height: 100%;
font-size: 32px;
color: ${cssManager.bdTheme('#777', '#999')};
top: 0px;
left: 0px;
transform: translateY(2px);
/* When combox is open */
:host(.combox-open) #mainbox .icon.open {
opacity: 0;
transform: rotate(45deg) scale(0.9);
}
#mainbox .icon.close dees-icon {
: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;
width: 100%;
height: 100%;
font-size: 24px;
top: 0px;
left: 0px;
color: ${cssManager.bdTheme('#666', '#CCC')};
bottom: 0;
right: 0;
pointer-events: none;
}
#comboxContainer sio-combox {
transition: transform 0.2s, opacity 0.2s;
position: absolute;
bottom: var(--fab-combox-offset);
right: 0;
transition: ${unsafeCSS(transitions.all)};
will-change: transform;
transform: translateY(20px);
bottom: 80px;
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;
}
</style>
<div id="mainbox" @click=${this.toggleCombox}>
`,
// Mobile responsive styles - smaller FAB on phablet and phone
cssManager.cssForPhablet(css`
:host {
--fab-size: 48px;
--fab-combox-offset: calc(var(--fab-size) + ${unsafeCSS(spacing["3"])});
bottom: 16px;
right: 16px;
}
`),
];
public render(): TemplateResult {
return html`
<div id="mainbox"
class="${this.shouldPulse ? 'pulse' : ''}"
@click=${this.toggleCombox}
@animationend=${() => { this.shouldPulse = false; }}
>
<div class="icon open">
<dees-icon iconFA="message"></dees-icon>
<img src="https://assetbroker.lossless.one/brandfiles/00general/favicon_socialio.svg" />
<sio-icon icon="message-square" size="24"></sio-icon>
</div>
<div class="icon close">
<dees-icon iconFa="xmark"></dees-icon>
<sio-icon icon="x" size="20"></sio-icon>
</div>
</div>
<div id="comboxContainer" class="${this.showCombox ? 'show' : null}">
<sio-combox></sio-combox>
<div id="comboxContainer" class="${this.showCombox ? 'show' : ''}">
${this.showCombox || this.hasShownOnce ? html`
<sio-combox @close=${() => this.showCombox = false}></sio-combox>
` : ''}
</div>
`;
}
@@ -170,20 +268,48 @@ export class SioFab extends DeesElement {
*/
public async toggleCombox() {
console.log('toggle combox');
const wasOpen = this.showCombox;
this.showCombox = !this.showCombox;
if (this.showCombox) {
this.hasShownOnce = true;
if (!wasOpen) {
this.shouldPulse = true;
}
}
}
public async firstUpdated(args) {
public async firstUpdated(args: any) {
super.firstUpdated(args);
const domtools = await this.domtoolsPromise;
const sioCombox: SioCombox = this.shadowRoot.querySelector('sio-combox');
const mainBox: HTMLElement = this.shadowRoot.querySelector('#mainbox');
sioCombox.referenceObject = mainBox;
// Set up keyboard shortcut
domtools.keyboard
.on([domtools.keyboard.keyEnum.Ctrl, domtools.keyboard.keyEnum.S])
.subscribe((event) => {
this.showCombox = !this.showCombox;
.subscribe(() => {
this.toggleCombox();
});
}
public async updated(changedProperties: Map<string | number | symbol, unknown>) {
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
if ((changedProperties.has('showCombox') || changedProperties.has('hasShownOnce')) &&
(this.showCombox || this.hasShownOnce)) {
const sioCombox: SioCombox = this.shadowRoot.querySelector('sio-combox');
const mainBox: HTMLElement = this.shadowRoot.querySelector('#mainbox');
if (sioCombox && mainBox && !sioCombox.referenceObject) {
sioCombox.referenceObject = mainBox;
}
}
}
}

163
ts_web/elements/sio-icon.ts Normal file
View File

@@ -0,0 +1,163 @@
import {
DeesElement,
html,
property,
customElement,
cssManager,
css,
type TemplateResult,
} from '@design.estate/dees-element';
import * as lucideIcons from 'lucide';
import { createElement } from 'lucide';
declare global {
interface HTMLElementTagNameMap {
'sio-icon': SioIcon;
}
}
@customElement('sio-icon')
export class SioIcon extends DeesElement {
public static demo = () => html`
<div style="display: flex; gap: 16px; align-items: center;">
<sio-icon icon="search"></sio-icon>
<sio-icon icon="message-square" color="#3b82f6"></sio-icon>
<sio-icon icon="x" size="32"></sio-icon>
<sio-icon icon="send" strokeWidth="3"></sio-icon>
</div>
`;
@property({ type: String })
public accessor icon: string;
@property({ type: Number })
public accessor size: number = 24;
@property({ type: String })
public accessor color: string = 'currentColor';
@property({ type: Number })
public accessor strokeWidth: number = 2;
// Cache for rendered icons
private static iconCache = new Map<string, string>();
private static readonly MAX_CACHE_SIZE = 100;
// Track last rendered properties to avoid unnecessary updates
private lastIcon: string | null = null;
private lastSize: number | null = null;
private lastColor: string | null = null;
private lastStrokeWidth: number | null = null;
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: inline-flex;
align-items: center;
justify-content: center;
line-height: 1;
vertical-align: middle;
}
#iconContainer {
display: flex;
align-items: center;
justify-content: center;
}
#iconContainer svg {
display: block;
width: 100%;
height: 100%;
}
`,
];
public render(): TemplateResult {
return html`
<div id="iconContainer" style="width: ${this.size}px; height: ${this.size}px;"></div>
`;
}
public updated() {
// Check if we need to update
if (
this.lastIcon === this.icon &&
this.lastSize === this.size &&
this.lastColor === this.color &&
this.lastStrokeWidth === this.strokeWidth
) {
return;
}
// Update tracking properties
this.lastIcon = this.icon;
this.lastSize = this.size;
this.lastColor = this.color;
this.lastStrokeWidth = this.strokeWidth;
const container = this.shadowRoot?.querySelector('#iconContainer') as HTMLElement;
if (!container || !this.icon) return;
// Clear container
container.innerHTML = '';
// Create cache key
const cacheKey = `${this.icon}:${this.size}:${this.color}:${this.strokeWidth}`;
// Check cache
if (SioIcon.iconCache.has(cacheKey)) {
container.innerHTML = SioIcon.iconCache.get(cacheKey)!;
return;
}
try {
// Convert icon name to PascalCase (e.g., 'message-square' -> 'MessageSquare')
const pascalCaseName = this.icon
.split('-')
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
.join('');
const iconComponent = (lucideIcons as any)[pascalCaseName];
if (!iconComponent) {
console.warn(`Lucide icon '${pascalCaseName}' not found`);
return;
}
// Create the icon element
const svgElement = createElement(iconComponent, {
size: this.size,
color: this.color,
strokeWidth: this.strokeWidth,
});
if (svgElement) {
// Cache the result
const svgString = svgElement.outerHTML;
SioIcon.iconCache.set(cacheKey, svgString);
// Limit cache size
if (SioIcon.iconCache.size > SioIcon.MAX_CACHE_SIZE) {
const firstKey = SioIcon.iconCache.keys().next().value;
SioIcon.iconCache.delete(firstKey);
}
// Append to container
container.appendChild(svgElement);
}
} catch (error) {
console.error(`Error rendering icon ${this.icon}:`, error);
}
}
public async disconnectedCallback() {
await super.disconnectedCallback();
// Clear references
this.lastIcon = null;
this.lastSize = null;
this.lastColor = null;
this.lastStrokeWidth = null;
}
}

View File

@@ -0,0 +1,520 @@
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 } from './00fonts.js';
// Import components
import { SioPdfViewer } from './sio-pdf-viewer.js';
SioPdfViewer;
export interface ILightboxFile {
url: string;
name: string;
size?: number;
type?: string;
}
// For backwards compatibility
export type ILightboxImage = ILightboxFile;
declare global {
interface HTMLElementTagNameMap {
'sio-image-lightbox': SioImageLightbox;
}
}
@customElement('sio-image-lightbox')
export class SioImageLightbox extends DeesElement {
public static demo = () => html`
<sio-image-lightbox .isOpen=${true} .file=${{
url: 'https://picsum.photos/800/600',
name: 'Demo Image',
type: 'image/jpeg'
}}></sio-image-lightbox>
`;
@property({ type: Boolean })
public accessor isOpen: boolean = false;
@property({ type: Object })
public accessor file: ILightboxFile | null = null;
// For backwards compatibility
public get image(): ILightboxFile | null {
return this.file;
}
public set image(value: ILightboxFile | null) {
this.file = value;
}
@state()
private accessor fileLoaded: boolean = false;
@state()
private accessor scale: number = 1;
@state()
private accessor translateX: number = 0;
@state()
private accessor translateY: number = 0;
private isDragging: boolean = false;
private startX: number = 0;
private startY: number = 0;
public static styles = [
cssManager.defaultStyles,
css`
:host {
position: fixed;
inset: 0;
z-index: 10000;
pointer-events: none;
font-family: ${unsafeCSS(fontFamilies.sans)};
isolation: isolate;
}
.overlay {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0);
backdrop-filter: blur(0px);
-webkit-backdrop-filter: blur(0px);
transition: all 300ms ease;
pointer-events: none;
opacity: 0;
}
.overlay.open {
background: rgba(0, 0, 0, 0.9);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
pointer-events: all;
opacity: 1;
}
.container {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
padding: ${unsafeCSS(spacing["8"])};
pointer-events: none;
opacity: 0;
transform: scale(0.9);
transition: all 300ms cubic-bezier(0.34, 1.56, 0.64, 1);
}
.container.open {
opacity: 1;
transform: scale(1);
pointer-events: all;
}
.content-wrapper {
position: relative;
max-width: 90vw;
max-height: 90vh;
cursor: grab;
user-select: none;
transition: transform 100ms ease-out;
z-index: 1;
}
.content-wrapper.dragging {
cursor: grabbing;
transition: none;
}
.content-wrapper.pdf {
cursor: default;
}
.image {
display: block;
max-width: 100%;
max-height: 90vh;
border-radius: ${unsafeCSS(radius.lg)};
box-shadow: ${unsafeCSS(shadows["2xl"])};
opacity: 0;
transition: opacity 300ms ease;
}
.image.loaded {
opacity: 1;
}
.pdf-viewer {
width: 90vw;
height: 90vh;
border-radius: ${unsafeCSS(radius.lg)};
box-shadow: ${unsafeCSS(shadows["2xl"])};
background: white;
opacity: 0;
transition: opacity 300ms ease;
border: none;
}
.pdf-viewer.loaded {
opacity: 1;
}
.loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: white;
display: flex;
align-items: center;
gap: ${unsafeCSS(spacing["2"])};
}
.spinner {
animation: spin 1s linear infinite;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.controls {
position: absolute;
top: ${unsafeCSS(spacing["4"])};
right: ${unsafeCSS(spacing["4"])};
display: flex;
gap: ${unsafeCSS(spacing["2"])};
opacity: 0;
transition: opacity 200ms ease;
z-index: 10;
}
.container.open .controls {
opacity: 1;
}
.control-button {
width: 40px;
height: 40px;
border-radius: ${unsafeCSS(radius.full)};
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
color: white;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: ${unsafeCSS(transitions.all)};
}
.control-button:hover {
background: rgba(0, 0, 0, 0.7);
transform: scale(1.1);
}
.control-button:active {
transform: scale(0.95);
}
.info {
position: absolute;
bottom: ${unsafeCSS(spacing["4"])};
left: ${unsafeCSS(spacing["4"])};
right: ${unsafeCSS(spacing["4"])};
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: ${unsafeCSS(radius.lg)};
padding: ${unsafeCSS(spacing["3"])} ${unsafeCSS(spacing["4"])};
color: white;
display: flex;
justify-content: space-between;
align-items: center;
opacity: 0;
transform: translateY(10px);
transition: all 200ms ease;
z-index: 10;
}
.container.open .info {
opacity: 1;
transform: translateY(0);
}
.info-name {
font-weight: 500;
font-size: 0.9375rem;
}
.info-actions {
display: flex;
gap: ${unsafeCSS(spacing["3"])};
}
.info-button {
background: none;
border: none;
color: white;
opacity: 0.8;
cursor: pointer;
display: flex;
align-items: center;
gap: ${unsafeCSS(spacing["1"])};
font-size: 0.875rem;
padding: ${unsafeCSS(spacing["1"])} ${unsafeCSS(spacing["2"])};
border-radius: ${unsafeCSS(radius.md)};
transition: ${unsafeCSS(transitions.all)};
}
.info-button:hover {
opacity: 1;
background: rgba(255, 255, 255, 0.1);
}
@media (max-width: 600px) {
.container {
padding: ${unsafeCSS(spacing["4"])};
}
.controls {
top: ${unsafeCSS(spacing["2"])};
right: ${unsafeCSS(spacing["2"])};
}
.info {
bottom: ${unsafeCSS(spacing["2"])};
left: ${unsafeCSS(spacing["2"])};
right: ${unsafeCSS(spacing["2"])};
padding: ${unsafeCSS(spacing["2"])} ${unsafeCSS(spacing["3"])};
}
}
`,
];
private isPDF(): boolean {
return this.file?.type?.includes('pdf') ||
this.file?.name?.toLowerCase().endsWith('.pdf') || false;
}
public render(): TemplateResult {
const contentStyle = this.scale !== 1 || this.translateX !== 0 || this.translateY !== 0
? `transform: scale(${this.scale}) translate(${this.translateX}px, ${this.translateY}px)`
: '';
const isPDF = this.isPDF();
return html`
<div class="overlay ${this.isOpen ? 'open' : ''}" @click=${(e: Event) => {
if (e.target === e.currentTarget) this.close(e);
}}></div>
<div class="container ${this.isOpen ? 'open' : ''}">
${this.file ? html`
<div class="controls">
${!isPDF ? html`
<div class="control-button" @click=${this.zoomIn}>
<sio-icon icon="zoom-in" size="18"></sio-icon>
</div>
<div class="control-button" @click=${this.zoomOut}>
<sio-icon icon="zoom-out" size="18"></sio-icon>
</div>
<div class="control-button" @click=${this.resetZoom}>
<sio-icon icon="maximize-2" size="18"></sio-icon>
</div>
` : ''}
<div class="control-button" @click=${(e: Event) => this.close(e)}>
<sio-icon icon="x" size="18"></sio-icon>
</div>
</div>
<div
class="content-wrapper ${this.isDragging ? 'dragging' : ''} ${isPDF ? 'pdf' : ''}"
style="${contentStyle}"
@mousedown=${!isPDF ? this.startDrag : undefined}
@mousemove=${!isPDF ? this.drag : undefined}
@mouseup=${!isPDF ? this.endDrag : undefined}
@mouseleave=${!isPDF ? this.endDrag : undefined}
@wheel=${this.handleWheel}
>
${!this.fileLoaded ? html`
<div class="loading">
<sio-icon class="spinner" icon="loader" size="24"></sio-icon>
<span>Loading...</span>
</div>
` : ''}
${isPDF ? html`
<sio-pdf-viewer
class="pdf-viewer ${this.fileLoaded ? 'loaded' : ''}"
.url="${this.file.url}"
.fileName="${this.file.name}"
@load=${() => this.fileLoaded = true}
></sio-pdf-viewer>
` : html`
<img
class="image ${this.fileLoaded ? 'loaded' : ''}"
src="${this.file.url}"
alt="${this.file.name}"
@load=${() => this.fileLoaded = true}
@error=${() => this.fileLoaded = false}
@click=${(e: Event) => e.stopPropagation()}
/>
`}
</div>
<div class="info">
<div class="info-name">${this.file.name}</div>
<div class="info-actions">
<button class="info-button" @click=${this.download}>
<sio-icon icon="download" size="16"></sio-icon>
Download
</button>
<button class="info-button" @click=${this.openInNewTab}>
<sio-icon icon="external-link" size="16"></sio-icon>
Open
</button>
</div>
</div>
` : ''}
</div>
`;
}
public async open(file: ILightboxFile | ILightboxImage) {
this.file = file;
this.fileLoaded = false;
this.resetZoom();
this.isOpen = true;
// For PDFs, we'll handle loading state differently since it's in a separate component
if (this.isPDF()) {
// PDFs are handled by sio-pdf-viewer which manages its own loading state
this.fileLoaded = true;
}
// Add keyboard listener
document.addEventListener('keydown', this.handleKeyDown);
}
private close = (e?: Event) => {
if (e) {
e.preventDefault();
e.stopPropagation();
}
this.isOpen = false;
document.removeEventListener('keydown', this.handleKeyDown);
// Dispatch close event
this.dispatchEvent(new CustomEvent('lightbox-close', {
bubbles: true,
composed: true
}));
// Clean up after animation
setTimeout(() => {
this.file = null;
this.fileLoaded = false;
this.resetZoom();
}, 300);
}
private handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
this.close();
} else if (e.key === '+' || e.key === '=') {
this.zoomIn();
} else if (e.key === '-') {
this.zoomOut();
} else if (e.key === '0') {
this.resetZoom();
}
}
private zoomIn() {
this.scale = Math.min(this.scale * 1.2, 3);
}
private zoomOut() {
this.scale = Math.max(this.scale / 1.2, 0.5);
}
private resetZoom() {
this.scale = 1;
this.translateX = 0;
this.translateY = 0;
}
private handleWheel = (e: WheelEvent) => {
e.preventDefault();
if (e.ctrlKey || e.metaKey) {
// Zoom with ctrl/cmd + scroll
if (e.deltaY < 0) {
this.zoomIn();
} else {
this.zoomOut();
}
}
}
private startDrag = (e: MouseEvent) => {
if (this.scale > 1) {
this.isDragging = true;
this.startX = e.clientX - this.translateX;
this.startY = e.clientY - this.translateY;
e.preventDefault();
}
}
private drag = (e: MouseEvent) => {
if (this.isDragging && this.scale > 1) {
this.translateX = e.clientX - this.startX;
this.translateY = e.clientY - this.startY;
}
}
private endDrag = () => {
this.isDragging = false;
}
private download() {
if (!this.file) return;
const a = document.createElement('a');
a.href = this.file.url;
a.download = this.file.name;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
private openInNewTab() {
if (!this.file) return;
window.open(this.file.url, '_blank');
}
public async disconnectedCallback() {
await super.disconnectedCallback();
document.removeEventListener('keydown', this.handleKeyDown);
}
}

View File

@@ -0,0 +1,279 @@
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}
?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 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

@@ -0,0 +1,593 @@
import {
DeesElement,
property,
html,
customElement,
type TemplateResult,
cssManager,
css,
unsafeCSS,
state,
} from '@design.estate/dees-element';
// Import design tokens
import { bdTheme } from './00colors.js';
import { spacing, radius, shadows } from './00tokens.js';
import { fontFamilies } from './00fonts.js';
declare global {
interface HTMLElementTagNameMap {
'sio-pdf-viewer': SioPdfViewer;
}
interface Window {
pdfjsLib?: any;
}
}
@customElement('sio-pdf-viewer')
export class SioPdfViewer extends DeesElement {
public static demo = () => html`
<sio-pdf-viewer
.url=${'data:application/pdf;base64,JVBERi0xLjMKJeLjz9MKMSAwIG9iago8PAovVHlwZSAvQ2F0YWxvZwovT3V0bGluZXMgMiAwIFIKL1BhZ2VzIDMgMCBSCj4+CmVuZG9iagoyIDAgb2JqCjw8Ci9UeXBlIC9PdXRsaW5lcwovQ291bnQgMAo+PgplbmRvYmoKMyAwIG9iago8PAovVHlwZSAvUGFnZXMKL0NvdW50IDEKL0tpZHMgWzQgMCBSXQo+PgplbmRvYmoKNCAwIG9iago8PAovVHlwZSAvUGFnZQovUGFyZW50IDMgMCBSCi9NZWRpYUJveCBbMCAwIDYxMiA3OTJdCi9Db250ZW50cyA1IDAgUgovUmVzb3VyY2VzIDw8Ci9Gb250IDw8Ci9GMSA2IDAgUgo+Pgo+Pgo+PgplbmRvYmoKNSAwIG9iago8PAovTGVuZ3RoIDQ0Cj4+CnN0cmVhbQpCVApxCjcwIDUwIFRECi9GMSAxMiBUZgooSGVsbG8gV29ybGQpIFRqCkVUClEKZW5kc3RyZWFtCmVuZG9iago2IDAgb2JqCjw8Ci9UeXBlIC9Gb250Ci9TdWJ0eXBlIC9UeXBlMQovQmFzZUZvbnQgL1RpbWVzLVJvbWFuCj4+CmVuZG9iagp4cmVmCjAgNwowMDAwMDAwMDAwIDY1NTM1IGYgCjAwMDAwMDAwMDkgMDAwMDAgbiAKMDAwMDAwMDA3NCAwMDAwMCBuIAowMDAwMDAwMTIwIDAwMDAwIG4gCjAwMDAwMDAxNzkgMDAwMDAgbiAKMDAwMDAwMDM2NCAwMDAwMCBuIAowMDAwMDAwNDY2IDAwMDAwIG4gCnRyYWlsZXIKPDwKL1NpemUgNwovUm9vdCAxIDAgUgo+PgpzdGFydHhyZWYKNTY1CiUlRU9G'}
.fileName=${'demo.pdf'}
style="width: 600px; height: 800px;"
></sio-pdf-viewer>
`;
@property({ type: String })
public accessor url: string = '';
@property({ type: String })
public accessor fileName: string = 'document.pdf';
@state()
private accessor isLoading: boolean = true;
@state()
private accessor hasError: boolean = false;
@state()
private accessor pdfDocument: any = null;
@state()
private accessor currentPage: number = 1;
@state()
private accessor totalPages: number = 0;
@state()
private accessor scale: number = 1;
private static pdfJsLoaded: boolean = false;
private static pdfJsLoading: Promise<void> | null = null;
private renderTask: any = null;
private resizeObserver: ResizeObserver | null = null;
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
width: 100%;
height: 100%;
position: relative;
background: ${bdTheme('background')};
font-family: ${unsafeCSS(fontFamilies.sans)};
}
.container {
width: 100%;
height: 100%;
position: relative;
display: flex;
flex-direction: column;
}
.pdf-container {
width: 100%;
height: 100%;
overflow: auto;
display: flex;
flex-direction: column;
align-items: center;
background: ${bdTheme('muted')};
}
.pdf-canvas-wrapper {
position: relative;
margin: ${unsafeCSS(spacing["4"])} auto;
box-shadow: ${unsafeCSS(shadows.lg)};
background: white;
}
canvas {
display: block;
max-width: 100%;
height: auto;
}
.pdf-controls {
position: sticky;
top: 0;
z-index: 10;
width: 100%;
padding: ${unsafeCSS(spacing["3"])};
background: ${bdTheme('background')};
border-bottom: 1px solid ${bdTheme('border')};
display: flex;
align-items: center;
justify-content: center;
gap: ${unsafeCSS(spacing["3"])};
box-shadow: ${unsafeCSS(shadows.sm)};
}
.pdf-controls-group {
display: flex;
align-items: center;
gap: ${unsafeCSS(spacing["2"])};
}
.page-info {
font-size: 0.875rem;
color: ${bdTheme('mutedForeground')};
min-width: 100px;
text-align: center;
}
.loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
color: ${bdTheme('mutedForeground')};
}
.spinner {
animation: spin 1s linear infinite;
margin-bottom: ${unsafeCSS(spacing["2"])};
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.error-container {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
padding: ${unsafeCSS(spacing["6"])};
background: ${bdTheme('card')};
border: 1px solid ${bdTheme('border')};
border-radius: ${unsafeCSS(radius.lg)};
box-shadow: ${unsafeCSS(shadows.md)};
max-width: 400px;
}
.error-icon {
color: ${bdTheme('destructive')};
margin-bottom: ${unsafeCSS(spacing["3"])};
}
.error-title {
font-size: 1.125rem;
font-weight: 600;
color: ${bdTheme('foreground')};
margin-bottom: ${unsafeCSS(spacing["2"])};
}
.error-message {
color: ${bdTheme('mutedForeground')};
margin-bottom: ${unsafeCSS(spacing["4"])};
font-size: 0.875rem;
}
.error-actions {
display: flex;
gap: ${unsafeCSS(spacing["2"])};
justify-content: center;
}
.fallback-viewer {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
background: ${bdTheme('background')};
}
.fallback-header {
padding: ${unsafeCSS(spacing["4"])};
border-bottom: 1px solid ${bdTheme('border')};
display: flex;
align-items: center;
justify-content: space-between;
background: ${bdTheme('card')};
}
.fallback-title {
font-weight: 500;
color: ${bdTheme('foreground')};
display: flex;
align-items: center;
gap: ${unsafeCSS(spacing["2"])};
}
.fallback-content {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: ${unsafeCSS(spacing["8"])};
}
.fallback-message {
text-align: center;
color: ${bdTheme('mutedForeground')};
}
.fallback-icon {
font-size: 48px;
margin-bottom: ${unsafeCSS(spacing["4"])};
opacity: 0.5;
}
.fallback-text {
margin-bottom: ${unsafeCSS(spacing["4"])};
}
/* Scrollbar styling */
.pdf-container::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.pdf-container::-webkit-scrollbar-track {
background: ${bdTheme('muted')};
}
.pdf-container::-webkit-scrollbar-thumb {
background: ${bdTheme('border')};
border-radius: 4px;
}
.pdf-container::-webkit-scrollbar-thumb:hover {
background: ${bdTheme('mutedForeground')};
}
/* Responsive */
@media (max-width: 600px) {
.pdf-controls {
flex-wrap: wrap;
gap: ${unsafeCSS(spacing["2"])};
}
.pdf-controls-group {
flex-wrap: nowrap;
}
}
`,
];
public render(): TemplateResult {
if (this.hasError) {
return this.renderError();
}
return html`
<div class="container">
${this.isLoading ? html`
<div class="loading">
<sio-icon class="spinner" icon="loader" size="24"></sio-icon>
<div>Loading PDF...</div>
</div>
` : ''}
${this.pdfDocument ? html`
<div class="pdf-controls">
<div class="pdf-controls-group">
<sio-button
type="ghost"
size="sm"
@click=${this.previousPage}
?disabled=${this.currentPage <= 1}
>
<sio-icon icon="chevron-left" size="16"></sio-icon>
</sio-button>
<div class="page-info">
Page ${this.currentPage} of ${this.totalPages}
</div>
<sio-button
type="ghost"
size="sm"
@click=${this.nextPage}
?disabled=${this.currentPage >= this.totalPages}
>
<sio-icon icon="chevron-right" size="16"></sio-icon>
</sio-button>
</div>
<div class="pdf-controls-group">
<sio-button
type="ghost"
size="sm"
@click=${this.zoomOut}
>
<sio-icon icon="zoom-out" size="16"></sio-icon>
</sio-button>
<sio-button
type="ghost"
size="sm"
@click=${this.resetZoom}
>
<sio-icon icon="maximize-2" size="16"></sio-icon>
</sio-button>
<sio-button
type="ghost"
size="sm"
@click=${this.zoomIn}
>
<sio-icon icon="zoom-in" size="16"></sio-icon>
</sio-button>
</div>
<div class="pdf-controls-group">
<sio-button
type="ghost"
size="sm"
@click=${this.downloadPdf}
>
<sio-icon icon="download" size="16"></sio-icon>
</sio-button>
</div>
</div>
<div class="pdf-container">
<div class="pdf-canvas-wrapper">
<canvas></canvas>
</div>
</div>
` : ''}
</div>
`;
}
private renderError(): TemplateResult {
return html`
<div class="error-container">
<sio-icon class="error-icon" icon="alert-circle" size="48"></sio-icon>
<div class="error-title">Unable to display PDF</div>
<div class="error-message">
The PDF viewer couldn't load this document. This might be due to browser restrictions or an invalid PDF file.
</div>
<div class="error-actions">
<sio-button
type="primary"
size="sm"
@click=${this.downloadPdf}
>
<sio-icon icon="download" size="16"></sio-icon>
Download PDF
</sio-button>
<sio-button
type="outline"
size="sm"
@click=${this.openInNewTab}
>
<sio-icon icon="external-link" size="16"></sio-icon>
Open in New Tab
</sio-button>
</div>
</div>
`;
}
public async connectedCallback() {
await super.connectedCallback();
if (this.url) {
await this.loadPdf();
}
}
public async firstUpdated(_changedProperties: any) {
super.firstUpdated(_changedProperties);
// Set up resize observer for responsive rendering after first render
const container = this.shadowRoot?.querySelector('.pdf-container');
if (container) {
this.resizeObserver = new ResizeObserver(() => {
if (this.pdfDocument && !this.isLoading) {
this.renderPage();
}
});
this.resizeObserver.observe(container);
}
}
public async updated(changedProperties: Map<string | number | symbol, unknown>) {
super.updated(changedProperties);
if (changedProperties.has('url') && this.url) {
await this.loadPdf();
}
// Re-render when scale changes
if (changedProperties.has('scale') && this.pdfDocument && !this.isLoading) {
await this.renderPage();
}
}
private static async loadPdfJs(): Promise<void> {
if (SioPdfViewer.pdfJsLoaded) return;
if (SioPdfViewer.pdfJsLoading) {
return SioPdfViewer.pdfJsLoading;
}
SioPdfViewer.pdfJsLoading = new Promise(async (resolve, reject) => {
try {
// Load PDF.js from jsDelivr
const script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/pdfjs-dist@3.11.174/build/pdf.min.js';
script.onload = () => {
if (window.pdfjsLib) {
// Configure worker
window.pdfjsLib.GlobalWorkerOptions.workerSrc =
'https://cdn.jsdelivr.net/npm/pdfjs-dist@3.11.174/build/pdf.worker.min.js';
SioPdfViewer.pdfJsLoaded = true;
resolve();
} else {
reject(new Error('PDF.js failed to load'));
}
};
script.onerror = () => reject(new Error('Failed to load PDF.js script'));
document.head.appendChild(script);
} catch (error) {
reject(error);
}
});
return SioPdfViewer.pdfJsLoading;
}
private async loadPdf() {
this.isLoading = true;
this.hasError = false;
this.pdfDocument = null;
try {
// Load PDF.js if not already loaded
await SioPdfViewer.loadPdfJs();
// Load the PDF document
const loadingTask = window.pdfjsLib.getDocument({
url: this.url,
// Enable range requests for better performance
disableRange: false,
// Enable streaming for large PDFs
disableStream: false,
});
this.pdfDocument = await loadingTask.promise;
this.totalPages = this.pdfDocument.numPages;
this.currentPage = 1;
this.isLoading = false;
// Render the first page
await this.renderPage();
} catch (error) {
console.error('Failed to load PDF:', error);
this.hasError = true;
this.isLoading = false;
}
}
private async renderPage() {
if (!this.pdfDocument) return;
// Cancel any ongoing render task
if (this.renderTask) {
this.renderTask.cancel();
}
try {
const page = await this.pdfDocument.getPage(this.currentPage);
const canvas = this.shadowRoot?.querySelector('canvas') as HTMLCanvasElement;
if (!canvas) return;
const context = canvas.getContext('2d');
const viewport = page.getViewport({ scale: this.scale });
// Set canvas dimensions
canvas.height = viewport.height;
canvas.width = viewport.width;
// Render PDF page into canvas context
const renderContext = {
canvasContext: context,
viewport: viewport
};
this.renderTask = page.render(renderContext);
await this.renderTask.promise;
} catch (error) {
if (error.name !== 'RenderingCancelledException') {
console.error('Error rendering page:', error);
}
}
}
private async previousPage() {
if (this.currentPage > 1) {
this.currentPage--;
await this.renderPage();
}
}
private async nextPage() {
if (this.currentPage < this.totalPages) {
this.currentPage++;
await this.renderPage();
}
}
private async zoomIn() {
this.scale = Math.min(this.scale * 1.2, 3);
await this.renderPage();
}
private async zoomOut() {
this.scale = Math.max(this.scale / 1.2, 0.5);
await this.renderPage();
}
private async resetZoom() {
this.scale = 1;
await this.renderPage();
}
private downloadPdf() {
const a = document.createElement('a');
a.href = this.url;
a.download = this.fileName;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
private openInNewTab() {
window.open(this.url, '_blank');
}
public async disconnectedCallback() {
await super.disconnectedCallback();
// Clean up resize observer
if (this.resizeObserver) {
this.resizeObserver.disconnect();
this.resizeObserver = null;
}
// Cancel any ongoing render task
if (this.renderTask) {
this.renderTask.cancel();
}
// Destroy PDF document to free memory
if (this.pdfDocument) {
this.pdfDocument.destroy();
}
}
}

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('#playback')
private playbackDiv!: HTMLDivElement;
private accessor playbackDiv!: HTMLDivElement;
static styles = css`
:host {

View File

@@ -1,156 +0,0 @@
import { DeesElement, property, html, customElement, type TemplateResult } from '@design.estate/dees-element';
import * as domtools from '@design.estate/dees-domtools';
import * as sioInterfaces from '@social.io/interfaces';
@customElement('sio-subwidget-conversations')
export class SioSubwidgetConversations extends DeesElement {
// STATIC
// INSTANCE
public conversations: sioInterfaces.ISioConversation[] = [
{
subject: 'Pricing page',
parties: [
{
id: '1',
description: 'Lossless Support',
name: 'Lossless Support',
},
{
id: '2',
description: 'you',
name: 'you',
},
],
conversationBlocks: [
{
partyId: '1',
text: 'Hello there :) How can we help you?',
},
{
partyId: '2',
text: 'Hi! Where is your pricing page?',
},
],
},{
subject: 'Pricing page',
parties: [
{
id: '1',
description: 'Lossless Support',
name: 'Lossless Support',
},
{
id: '2',
description: 'you',
name: 'you',
},
],
conversationBlocks: [
{
partyId: '1',
text: 'Hello there :) How can we help you?',
},
{
partyId: '2',
text: 'Hi! Where is your pricing page?',
},
],
},
];
public static demo = () => html`<sio-subwidget-conversations></sio-subwidget-conversations>`;
public render(): TemplateResult {
return html`
${domtools.elementBasic.styles}
<style>
:host {
color: ${this.goBright ? '#666' : '#ccc'};
font-family: 'Dees Sans';
}
.conversationbox {
padding: 20px;
transition: all 0.1s;
min-height: 200px;
margin: 20px;
background: ${this.goBright ? '#fff' : '#111111'};
border-radius: 16px;
border-top: 1px solid rgba(250, 250, 250, 0.1);
box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.3);
margin-bottom: 20px;
}
.conversationbox .text {
font-family: 'Dees Sans';
margin-bottom: 8px;
}
.conversation {
display: block;
transition: all 0.1s;
padding: 8px 0px 8px 0px;
border-bottom: 1px solid;
border-image: radial-gradient(rgba(136, 136, 136, 0.44), rgba(136, 136, 136, 0)) 1 / 1 / 0 stretch;
cursor: pointer;
}
.conversation:last-of-type {
border-bottom: none;
margin-bottom: 8px;
}
.conversation:hover {
cursor: default;
}
.conversation:hover .gridcontainer {
transform: translateX(2px)
}
.conversation .gridcontainer {
display: grid;
grid-template-columns: 50px auto;
transition: transform 0.2s;
}
.conversation .gridcontainer .profilePicture {
height: 40px;
width: 40px;
border-radius: 50px;
background: ${this.goBright ? '#EEE' : '#222'};
}
.conversation .gridcontainer .text .topLine {
font-family: 'Dees Sans';
padding-top: 3px;
font-size: 12px;
}
.gridcontainer .gridcontainer .text .bottomLine {
font-family: 'Dees Sans';
font-size: 14px;
}
</style>
<div class="conversationbox">
<div class="text">Your conversations:</div>
${this.conversations.map((conversationArg) => {
return html`
<div class="conversation">
<div class="gridcontainer">
<div class="profilePicture"></div>
<div class="text">
<div class="topLine">Today at 8:01</div>
<div class="bottomLine">${conversationArg.subject}</div>
</div>
</div>
</div>
`;
})}
<dees-button>View more</dees-button>
</div>
`;
}
}

View File

@@ -1,64 +0,0 @@
import { DeesElement, property, html, customElement, type TemplateResult } from '@design.estate/dees-element';
import * as domtools from '@design.estate/dees-domtools';
import * as deesCatalog from '@design.estate/dees-catalog';
deesCatalog;
@customElement('sio-subwidget-onboardme')
export class SioSubwidgetOnboardme extends DeesElement {
@property()
public showCombox = false;
public static demo = () => html`
<sio-subwidget-onboardme></sio-subwidget-onboardme>
`;
constructor() {
super();
domtools.DomTools.setupDomTools();
}
public render(): TemplateResult {
return html`
${domtools.elementBasic.styles}
<style>
:host {
display: block;
position: relative;
transition: all 0.1s;
min-height: 200px;
margin: 20px 20px 40px 20px;
background: ${this.goBright ? '#fafafa' : '#111111'};
border-radius: 16px;
border-top: 1px solid rgba(250,250,250,0.1);
box-shadow: 0px 0px 5px rgba(0,0,0,0.3);
padding: 24px 24px 32px 24px;
color: #CCC;
overflow: hidden;
}
:host(:hover) {
}
.brandingbox {
text-align: center;
position: absolute;
width: 100%;
bottom: 0px;
left: 0px;
font-size: 10px;
padding: 3px;
border-top: 1px solid rgba(250,250,250, 0.1);
font-family: 'Dees Code';
background: ${this.goBright ? '#eee' : '#111111'};
color: #666;
}
</style>
Or search through our documentation
<dees-input-text key="searchTerm" label="Search Term:"></dees-input-text>
<dees-button>Search</dees-button>
<div class="brandingbox">last updated: ${new Date().toISOString()}</div>
`;
}
}

View File

@@ -1,3 +1,45 @@
import { html } from '@design.estate/dees-element';
export const mainpage = () => html` <lele-statusbar></lele-statusbar> `;
export const mainpage = () => html`
<style>
body {
margin: 0;
padding: 0;
background: #f5f5f5;
}
.demo-container {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
}
.demo-section {
background: white;
border-radius: 8px;
padding: 40px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
max-width: 1200px;
width: 100%;
}
h2 {
margin-bottom: 20px;
font-family: sans-serif;
}
.component-demo {
margin: 20px 0;
position: relative;
min-height: 700px;
}
</style>
<div class="demo-container">
<div class="demo-section">
<h2>Social.io Catalog Components</h2>
<div class="component-demo">
<h3>FAB with Combox Demo</h3>
<sio-fab .showCombox=${true}></sio-fab>
</div>
</div>
</div>
`;

View File

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