Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6e29f0b51a | |||
| 5e25f86a0b | |||
| 01dad7cc5e | |||
| a4a3c6dc50 | |||
| 193b1f5234 | |||
| 95e92a5533 | |||
| 1caeae9ec9 | |||
| c534d1d084 | |||
| 23592f3a15 | |||
| 66493f793f | |||
| efd142d63d | |||
| 9ab16c85ba | |||
| ba791ee18a | |||
| b49707a727 | |||
| 2ea29cffbb | |||
| 8e18898542 | |||
| 7e7608c63a | |||
| 4cf9f3cd77 | |||
| 1d799532e9 | |||
| 39e94a11f8 | |||
| 9f3cdde7eb |
26
changelog.md
26
changelog.md
@@ -1,5 +1,31 @@
|
||||
# Changelog
|
||||
|
||||
## 2025-04-20 - 1.2.4 - fix(build)
|
||||
Update build script and async function signature
|
||||
|
||||
- Added '--skiplibcheck' flag to the tsbuild command in package.json
|
||||
- Changed startRecording return type to Promise<void> in ts_web/elements/sio-recorder.ts for proper async handling
|
||||
|
||||
## 2025-04-20 - 1.2.3 - fix(core)
|
||||
Update dependency versions and adjust UI CSS for fab and combox elements
|
||||
|
||||
- Bumped @design.estate/dees-catalog, dees-domtools, dees-element, and @social.io/interfaces to newer versions in package.json
|
||||
- Updated devDependencies to latest compatible versions
|
||||
- Adjusted CSS positioning in sio-fab and sio-combox for improved layout
|
||||
|
||||
## 2025-01-26 - 1.2.2 - fix(sio-recorder)
|
||||
Fixed the recording loop and ensured it stops correctly
|
||||
|
||||
- Added an await for domtoolsPromise in startRecording method.
|
||||
- Introduced a while loop to manage the recording status effectively.
|
||||
- Added delay and stop function call within the loop to manage record sessions.
|
||||
|
||||
## 2025-01-25 - 1.2.1 - fix(sio-recorder)
|
||||
Enhance styling and positioning for rrweb player elements in SioRecorder component.
|
||||
|
||||
- Fixed positioning and added styling for the replayer mouse and iframe in the SioRecorder component.
|
||||
- Ensured compatibility with cross-origin iframes.
|
||||
|
||||
## 2025-01-25 - 1.2.0 - feat(elements)
|
||||
Added sio-recorder element for recording and replaying sessions
|
||||
|
||||
|
||||
56
demo.html
Normal file
56
demo.html
Normal 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>
|
||||
31
package.json
31
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@social.io/catalog",
|
||||
"version": "1.2.0",
|
||||
"version": "1.2.5",
|
||||
"private": false,
|
||||
"description": "catalog for social.io",
|
||||
"main": "dist_ts_web/index.js",
|
||||
@@ -8,33 +8,33 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "tstest test/",
|
||||
"build": "tsbuild tsfolders --allowimplicitany && tsbundle element --production",
|
||||
"build": "tsbuild tsfolders --allowimplicitany --skiplibcheck && tsbundle element --production",
|
||||
"watch": "tswatch element",
|
||||
"buildDocs": "tsdoc"
|
||||
},
|
||||
"author": "Lossless GmbH",
|
||||
"license": "UNLICENSED",
|
||||
"dependencies": {
|
||||
"@design.estate/dees-catalog": "^1.2.0",
|
||||
"@design.estate/dees-domtools": "^2.0.64",
|
||||
"@design.estate/dees-element": "^2.0.39",
|
||||
"@design.estate/dees-wcctools": "^1.0.90",
|
||||
"@design.estate/dees-domtools": "^2.3.3",
|
||||
"@design.estate/dees-element": "^2.1.2",
|
||||
"@design.estate/dees-wcctools": "^1.1.1",
|
||||
"@losslessone_private/loint-pubapi": "^1.0.14",
|
||||
"@social.io/interfaces": "^1.0.5",
|
||||
"@social.io/interfaces": "^1.2.1",
|
||||
"lucide": "^0.525.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.1.84",
|
||||
"@git.zone/tsbundle": "^2.0.15",
|
||||
"@git.zone/tsrun": "^1.2.49",
|
||||
"@git.zone/tstest": "^1.0.90",
|
||||
"@git.zone/tswatch": "^2.0.23",
|
||||
"@git.zone/tsbuild": "^2.6.4",
|
||||
"@git.zone/tsbundle": "^2.5.1",
|
||||
"@git.zone/tsrun": "^1.3.3",
|
||||
"@git.zone/tstest": "^2.3.1",
|
||||
"@git.zone/tswatch": "^2.1.2",
|
||||
"@push.rocks/projectinfo": "^5.0.2",
|
||||
"@push.rocks/smartenv": "^5.0.12",
|
||||
"@push.rocks/tapbundle": "^5.3.0",
|
||||
"@types/node": "^22.7.5"
|
||||
"@push.rocks/tapbundle": "^6.0.3",
|
||||
"@types/node": "^22.14.1"
|
||||
},
|
||||
"files": [
|
||||
"ts/**/*",
|
||||
@@ -58,5 +58,6 @@
|
||||
"bugs": {
|
||||
"url": "https://gitlab.com/social.io/private/catalog/issues"
|
||||
},
|
||||
"homepage": "https://gitlab.com/social.io/private/catalog#readme"
|
||||
"homepage": "https://gitlab.com/social.io/private/catalog#readme",
|
||||
"packageManager": "pnpm@10.7.0+sha512.6b865ad4b62a1d9842b61d674a393903b871d9244954f652b8842c2b553c72176b278f64c463e52d40fff8aba385c235c8c9ecf5cc7de4fd78b8bb6d49633ab6"
|
||||
}
|
||||
|
||||
7524
pnpm-lock.yaml
generated
7524
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -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.start();
|
||||
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();
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@social.io/catalog',
|
||||
version: '1.2.0',
|
||||
version: '1.2.4',
|
||||
description: 'catalog for social.io'
|
||||
}
|
||||
|
||||
158
ts_web/elements/00colors.ts
Normal file
158
ts_web/elements/00colors.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
import { cssManager } from '@design.estate/dees-element';
|
||||
|
||||
export const colors = {
|
||||
// Background colors - softer, more subtle
|
||||
background: {
|
||||
light: 'hsl(0 0% 100%)',
|
||||
dark: 'hsl(224 71.4% 4.1%)'
|
||||
},
|
||||
|
||||
// Foreground colors - less contrast for modern look
|
||||
foreground: {
|
||||
light: 'hsl(224 71.4% 4.1%)',
|
||||
dark: 'hsl(210 20% 98%)'
|
||||
},
|
||||
|
||||
// Card colors - subtle elevation
|
||||
card: {
|
||||
light: 'hsl(0 0% 100%)',
|
||||
dark: 'hsl(224 71.4% 4.1%)'
|
||||
},
|
||||
|
||||
cardForeground: {
|
||||
light: 'hsl(224 71.4% 4.1%)',
|
||||
dark: 'hsl(210 20% 98%)'
|
||||
},
|
||||
|
||||
// Popover colors
|
||||
popover: {
|
||||
light: 'hsl(0 0% 100%)',
|
||||
dark: 'hsl(222.2 84% 4.9%)'
|
||||
},
|
||||
|
||||
popoverForeground: {
|
||||
light: 'hsl(222.2 84% 4.9%)',
|
||||
dark: 'hsl(210 40% 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(210 20% 98%)',
|
||||
dark: 'hsl(224 71.4% 4.1%)'
|
||||
},
|
||||
|
||||
// Secondary colors - more subtle
|
||||
secondary: {
|
||||
light: 'hsl(220 14.3% 95.9%)',
|
||||
dark: 'hsl(215 27.9% 16.9%)'
|
||||
},
|
||||
|
||||
secondaryForeground: {
|
||||
light: 'hsl(220.9 39.3% 11%)',
|
||||
dark: 'hsl(210 20% 98%)'
|
||||
},
|
||||
|
||||
// Muted colors - softer grays
|
||||
muted: {
|
||||
light: 'hsl(220 14.3% 95.9%)',
|
||||
dark: 'hsl(215 27.9% 16.9%)'
|
||||
},
|
||||
|
||||
mutedForeground: {
|
||||
light: 'hsl(220 8.9% 46.1%)',
|
||||
dark: 'hsl(217.9 10.6% 64.9%)'
|
||||
},
|
||||
|
||||
// Accent colors - subtle hover states
|
||||
accent: {
|
||||
light: 'hsl(220 14.3% 95.9%)',
|
||||
dark: 'hsl(215 27.9% 16.9%)'
|
||||
},
|
||||
|
||||
accentForeground: {
|
||||
light: 'hsl(220.9 39.3% 11%)',
|
||||
dark: 'hsl(210 20% 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(220 13% 91%)',
|
||||
dark: 'hsl(215 27.9% 16.9%)'
|
||||
},
|
||||
|
||||
// Input color
|
||||
input: {
|
||||
light: 'hsl(214.3 31.8% 91.4%)',
|
||||
dark: 'hsl(217.2 32.6% 17.5%)'
|
||||
},
|
||||
|
||||
// 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 = (colorName: keyof typeof colors) => {
|
||||
return cssManager.bdTheme(colors[colorName].light, colors[colorName].dark);
|
||||
};
|
||||
145
ts_web/elements/00fonts.ts
Normal file
145
ts_web/elements/00fonts.ts
Normal 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
197
ts_web/elements/00tokens.ts
Normal 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(222.2 84% 4.9%)', 'hsl(212.7 26.8% 83.9%)')};
|
||||
}
|
||||
`;
|
||||
|
||||
// Disabled styles
|
||||
export const disabled = css`
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
`;
|
||||
@@ -1,5 +1,15 @@
|
||||
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-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';
|
||||
|
||||
266
ts_web/elements/sio-button.ts
Normal file
266
ts_web/elements/sio-button.ts
Normal file
@@ -0,0 +1,266 @@
|
||||
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 text: string = '';
|
||||
|
||||
@property({ type: String })
|
||||
public type: 'default' | 'primary' | 'destructive' | 'outline' | 'ghost' = 'default';
|
||||
|
||||
@property({ type: String })
|
||||
public size: 'sm' | 'default' | 'lg' = 'default';
|
||||
|
||||
@property({ type: Boolean, reflect: true })
|
||||
public disabled: boolean = false;
|
||||
|
||||
@property({ type: String })
|
||||
public 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: ${unsafeCSS(radius.md)};
|
||||
font-weight: 500;
|
||||
transition: ${unsafeCSS(transitions.all)};
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
outline: none;
|
||||
border: 1px solid transparent;
|
||||
gap: ${unsafeCSS(spacing["2"])};
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Size variants */
|
||||
.button.size-sm {
|
||||
height: 32px;
|
||||
padding: 0 ${unsafeCSS(spacing["3"])};
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.button.size-default {
|
||||
height: 36px;
|
||||
padding: 0 ${unsafeCSS(spacing["4"])};
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.button.size-lg {
|
||||
height: 44px;
|
||||
padding: 0 ${unsafeCSS(spacing["6"])};
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* Type variants */
|
||||
.button.default {
|
||||
background: ${bdTheme('background')};
|
||||
color: ${bdTheme('foreground')};
|
||||
border-color: ${bdTheme('border')};
|
||||
box-shadow: ${unsafeCSS(shadows.sm)};
|
||||
}
|
||||
|
||||
.button.default:hover:not(.disabled) {
|
||||
background: ${bdTheme('accent')};
|
||||
border-color: ${bdTheme('accent')};
|
||||
transform: translateY(-1px);
|
||||
box-shadow: ${unsafeCSS(shadows.md)};
|
||||
}
|
||||
|
||||
.button.default:active:not(.disabled) {
|
||||
transform: translateY(0);
|
||||
box-shadow: ${unsafeCSS(shadows.sm)};
|
||||
}
|
||||
|
||||
.button.primary {
|
||||
background: ${bdTheme('primary')};
|
||||
color: ${bdTheme('primaryForeground')};
|
||||
box-shadow: ${unsafeCSS(shadows.sm)};
|
||||
}
|
||||
|
||||
.button.primary:hover:not(.disabled) {
|
||||
opacity: 0.9;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: ${unsafeCSS(shadows.md)};
|
||||
}
|
||||
|
||||
.button.primary:active:not(.disabled) {
|
||||
transform: translateY(0);
|
||||
box-shadow: ${unsafeCSS(shadows.sm)};
|
||||
}
|
||||
|
||||
.button.destructive {
|
||||
background: ${bdTheme('destructive')};
|
||||
color: ${bdTheme('destructiveForeground')};
|
||||
}
|
||||
|
||||
.button.destructive:hover:not(.disabled) {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.button.destructive:active:not(.disabled) {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
.button.outline {
|
||||
background: transparent;
|
||||
color: ${bdTheme('foreground')};
|
||||
border-color: ${bdTheme('border')};
|
||||
}
|
||||
|
||||
.button.outline:hover:not(.disabled) {
|
||||
background: ${bdTheme('accent')};
|
||||
color: ${bdTheme('accentForeground')};
|
||||
}
|
||||
|
||||
.button.outline:active:not(.disabled) {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
.button.ghost {
|
||||
background: transparent;
|
||||
color: ${bdTheme('foreground')};
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.button.ghost:hover:not(.disabled) {
|
||||
background: ${bdTheme('accent')};
|
||||
color: ${bdTheme('accentForeground')};
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.button.ghost:active:not(.disabled) {
|
||||
background: ${bdTheme('accent')};
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
/* Focus state */
|
||||
.button:focus-visible {
|
||||
outline: 2px solid ${bdTheme('ring')};
|
||||
outline-offset: 2px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -4,11 +4,33 @@ import {
|
||||
html,
|
||||
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 {
|
||||
@@ -17,183 +39,314 @@ export class SioCombox extends DeesElement {
|
||||
@property({ type: Object })
|
||||
public 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 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 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 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 {
|
||||
display: block;
|
||||
height: 600px;
|
||||
width: 800px;
|
||||
background: ${bdTheme('background')};
|
||||
border-radius: ${unsafeCSS(radius['2xl'])};
|
||||
border: 1px solid ${bdTheme('border')};
|
||||
box-shadow: ${unsafeCSS(shadows.xl)};
|
||||
overflow: hidden;
|
||||
font-family: ${unsafeCSS(fontFamilies.sans)};
|
||||
position: relative;
|
||||
transform-origin: bottom right;
|
||||
}
|
||||
|
||||
:host(.animate-in) {
|
||||
animation: scaleIn 300ms cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
}
|
||||
|
||||
@keyframes scaleIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.9) translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
:host::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
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 {
|
||||
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;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
overscroll-behavior: contain;
|
||||
padding-bottom: 80px;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.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;
|
||||
.container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#greeting {
|
||||
padding-top: 50px;
|
||||
font-family: 'Dees Sans';
|
||||
margin: 0px;
|
||||
font-size: 30px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
#callToAction {
|
||||
font-family: 'Dees Sans';
|
||||
margin: 0px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.quicktabs {
|
||||
sio-conversation-selector {
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
bottom: 30px;
|
||||
display: grid;
|
||||
width: 100%;
|
||||
padding-bottom: 16px;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
background-image: linear-gradient(to bottom, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 1) 50%);
|
||||
padding-top: 24px;
|
||||
height: 100%;
|
||||
transition: left 300ms ease, opacity 200ms ease;
|
||||
}
|
||||
|
||||
.quicktabs .quicktab {
|
||||
text-align: center;
|
||||
sio-conversation-view {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
}
|
||||
.quicktabs .quicktab .quicktabicon {
|
||||
font-size: 20px;
|
||||
margin-bottom: 8px;
|
||||
height: 100%;
|
||||
transition: left 300ms ease, opacity 200ms ease;
|
||||
}
|
||||
|
||||
.quicktabs .quicktab .quicktabtext {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
/* 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>
|
||||
|
||||
|
||||
|
||||
.brandingbox {
|
||||
z-index: 101;
|
||||
text-align: center;
|
||||
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'};
|
||||
}
|
||||
</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>
|
||||
</div>
|
||||
<sio-subwidget-conversations></sio-subwidget-conversations>
|
||||
<sio-subwidget-onboardme></sio-subwidget-onboardme>
|
||||
</div>
|
||||
<sio-conversation-view
|
||||
.conversation=${conversationData}
|
||||
@back=${this.handleBack}
|
||||
@send-message=${this.handleSendMessage}
|
||||
@open-image=${this.handleOpenImage}
|
||||
@open-file=${this.handleOpenImage}
|
||||
></sio-conversation-view>
|
||||
</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(() => {
|
||||
setTimeout(() => {
|
||||
this.cssComputeInnerScroll();
|
||||
}, 200);
|
||||
});
|
||||
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(() => {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
324
ts_web/elements/sio-conversation-selector.ts
Normal file
324
ts_web/elements/sio-conversation-selector.ts
Normal file
@@ -0,0 +1,324 @@
|
||||
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 conversations: IConversation[] = [];
|
||||
|
||||
@property({ type: String })
|
||||
public selectedConversationId: string | null = null;
|
||||
|
||||
@state()
|
||||
private 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')};
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 1.25rem;
|
||||
line-height: 1.2;
|
||||
font-weight: 600;
|
||||
margin: 0 0 ${unsafeCSS(spacing["4"])} 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">
|
||||
<h2 class="title">Messages</h2>
|
||||
<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
|
||||
}));
|
||||
}
|
||||
}
|
||||
894
ts_web/elements/sio-conversation-view.ts
Normal file
894
ts_web/elements/sio-conversation-view.ts
Normal file
@@ -0,0 +1,894 @@
|
||||
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';
|
||||
|
||||
// Make sure components are loaded
|
||||
SioDropdownMenu;
|
||||
|
||||
// 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 conversation: IConversationData | null = null;
|
||||
|
||||
@state()
|
||||
private messageText: string = '';
|
||||
|
||||
@state()
|
||||
private isTyping: boolean = false;
|
||||
|
||||
@state()
|
||||
private isDragging: boolean = false;
|
||||
|
||||
@state()
|
||||
private uploadingFiles: Map<string, { file: File; progress: number }> = new Map();
|
||||
|
||||
@state()
|
||||
private 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);
|
||||
}
|
||||
|
||||
.input-wrapper {
|
||||
display: flex;
|
||||
gap: ${unsafeCSS(spacing["2"])};
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.message-input {
|
||||
flex: 1;
|
||||
min-height: 42px;
|
||||
max-height: 120px;
|
||||
padding: ${unsafeCSS(spacing["2.5"])} ${unsafeCSS(spacing[3])};
|
||||
background: ${bdTheme('secondary')};
|
||||
border: 1px solid ${bdTheme('border')};
|
||||
border-radius: ${unsafeCSS(radius.xl)};
|
||||
font-size: 0.9375rem;
|
||||
color: ${bdTheme('foreground')};
|
||||
outline: none;
|
||||
resize: none;
|
||||
font-family: ${unsafeCSS(fontFamilies.sans)};
|
||||
line-height: 1.5;
|
||||
transition: ${unsafeCSS(transitions.all)};
|
||||
}
|
||||
|
||||
.message-input::placeholder {
|
||||
color: ${bdTheme('mutedForeground')};
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.message-input:focus {
|
||||
border-color: ${bdTheme('ring')};
|
||||
background: ${bdTheme('background')};
|
||||
box-shadow: 0 0 0 3px ${bdTheme('ring')}15;
|
||||
}
|
||||
|
||||
.input-actions {
|
||||
display: flex;
|
||||
gap: ${unsafeCSS(spacing["1"])};
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
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-radius: ${unsafeCSS(radius.md)};
|
||||
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')};
|
||||
}
|
||||
|
||||
.file-input {
|
||||
display: none;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
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>
|
||||
` : ''}
|
||||
<div class="input-wrapper">
|
||||
<textarea
|
||||
class="message-input"
|
||||
placeholder="Type a message..."
|
||||
.value=${this.messageText}
|
||||
@input=${this.handleInput}
|
||||
@keydown=${this.handleKeyDown}
|
||||
rows="1"
|
||||
></textarea>
|
||||
<div class="input-actions">
|
||||
<input
|
||||
type="file"
|
||||
class="file-input"
|
||||
id="fileInput"
|
||||
multiple
|
||||
accept="image/*,.pdf,.doc,.docx,.txt"
|
||||
@change=${this.handleFileSelect}
|
||||
/>
|
||||
<sio-button type="ghost" size="sm" @click=${(e: Event) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.openFileSelector();
|
||||
}}>
|
||||
<sio-icon icon="paperclip" size="16"></sio-icon>
|
||||
</sio-button>
|
||||
<sio-button
|
||||
type="primary"
|
||||
size="sm"
|
||||
?disabled=${!this.messageText.trim() && this.pendingAttachments.length === 0}
|
||||
@click=${this.sendMessage}
|
||||
>
|
||||
<sio-icon icon="send" size="16"></sio-icon>
|
||||
</sio-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private handleBack() {
|
||||
this.dispatchEvent(new CustomEvent('back', {
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
}
|
||||
|
||||
private handleInput(e: Event) {
|
||||
const textarea = e.target as HTMLTextAreaElement;
|
||||
this.messageText = textarea.value;
|
||||
|
||||
// Auto-resize textarea
|
||||
textarea.style.height = 'auto';
|
||||
textarea.style.height = Math.min(textarea.scrollHeight, 120) + 'px';
|
||||
}
|
||||
|
||||
private handleKeyDown(e: KeyboardEvent) {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
this.sendMessage();
|
||||
}
|
||||
}
|
||||
|
||||
private sendMessage() {
|
||||
if (!this.messageText.trim() && this.pendingAttachments.length === 0) return;
|
||||
|
||||
const message: IMessage = {
|
||||
id: Date.now().toString(),
|
||||
text: this.messageText.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 input and attachments
|
||||
this.messageText = '';
|
||||
this.pendingAttachments = [];
|
||||
const textarea = this.shadowRoot?.querySelector('.message-input') as HTMLTextAreaElement;
|
||||
if (textarea) {
|
||||
textarea.style.height = 'auto';
|
||||
}
|
||||
|
||||
// Simulate typing indicator (remove in production)
|
||||
setTimeout(() => {
|
||||
this.isTyping = true;
|
||||
setTimeout(() => {
|
||||
this.isTyping = false;
|
||||
}, 2000);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
298
ts_web/elements/sio-dropdown-menu.ts
Normal file
298
ts_web/elements/sio-dropdown-menu.ts
Normal 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 items: IDropdownMenuItem[] = [];
|
||||
|
||||
@property({ type: String })
|
||||
public align: 'left' | 'right' = 'right';
|
||||
|
||||
@state()
|
||||
private 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();
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,15 @@ import {
|
||||
import * as domtools from '@design.estate/dees-domtools';
|
||||
|
||||
import { SioCombox } from './sio-combox.js';
|
||||
import { SioIcon } from './sio-icon.js';
|
||||
import { state } from '@design.estate/dees-element';
|
||||
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,9 +28,15 @@ declare global {
|
||||
|
||||
@customElement('sio-fab')
|
||||
export class SioFab extends DeesElement {
|
||||
@property()
|
||||
@property({ type: Boolean })
|
||||
public showCombox = false;
|
||||
|
||||
@state()
|
||||
private hasShownOnce = false;
|
||||
|
||||
@state()
|
||||
private shouldPulse = false;
|
||||
|
||||
public static demo = () => html` <sio-fab .showCombox=${true}></sio-fab> `;
|
||||
|
||||
constructor() {
|
||||
@@ -42,121 +56,188 @@ 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);
|
||||
}
|
||||
|
||||
#mainbox {
|
||||
transition: all 0.2s;
|
||||
transition: ${transitions.all};
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
right: 0px;
|
||||
height: 60px;
|
||||
width: 60px;
|
||||
box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.3);
|
||||
box-shadow: 0 4px 16px -2px rgba(0, 0, 0, 0.1), 0 2px 8px -2px rgba(0, 0, 0, 0.06);
|
||||
line-height: 60px;
|
||||
text-align: center;
|
||||
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: ${radius.full};
|
||||
user-select: none;
|
||||
border: none;
|
||||
animation: fabEntrance 300ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
#mainbox::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.2) 0%, rgba(255, 255, 255, 0) 50%);
|
||||
opacity: 0;
|
||||
transition: opacity 200ms ease;
|
||||
}
|
||||
|
||||
#mainbox::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
left: -4px;
|
||||
right: -4px;
|
||||
bottom: -4px;
|
||||
background: linear-gradient(135deg, var(--fab-gradient-start), var(--fab-gradient-end));
|
||||
border-radius: inherit;
|
||||
z-index: -1;
|
||||
opacity: 0;
|
||||
filter: blur(12px);
|
||||
transition: opacity 300ms ease;
|
||||
}
|
||||
|
||||
#mainbox:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#mainbox:hover::after {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
@keyframes fabEntrance {
|
||||
from {
|
||||
transform: scale(0.8);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
#mainbox:hover {
|
||||
transform: scale(1.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%);
|
||||
}
|
||||
|
||||
#mainbox:hover {
|
||||
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;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#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);
|
||||
}
|
||||
|
||||
#mainbox .icon.open {
|
||||
opacity: ${this.showCombox ? '0' : '1'};
|
||||
pointer-events: ${this.showCombox ? 'none' : 'all'};
|
||||
transform: ${this.showCombox ? 'rotate(45deg) scale(0.9)' : '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')};
|
||||
transform: ${this.showCombox ? 'rotate(0deg) scale(1)' : '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: 2px;
|
||||
#mainbox .icon sio-icon {
|
||||
color: white;
|
||||
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.1));
|
||||
}
|
||||
|
||||
#mainbox .icon.close dees-icon {
|
||||
#mainbox .icon.close sio-icon {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
#comboxContainer {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-size: 24px;
|
||||
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: calc(60px + ${spacing["4"]});
|
||||
right: 0;
|
||||
transition: ${transitions.all};
|
||||
will-change: transform;
|
||||
transform: translateY(20px);
|
||||
bottom: 80px;
|
||||
transform: translateY(${spacing["5"]});
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#comboxContainer.show {
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
#comboxContainer.show sio-combox {
|
||||
transform: translateY(0px);
|
||||
opacity: 1;
|
||||
pointer-events: all;
|
||||
}
|
||||
</style>
|
||||
<div id="mainbox" @click=${this.toggleCombox}>
|
||||
<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="28"></sio-icon>
|
||||
</div>
|
||||
<div class="icon close">
|
||||
<dees-icon iconFa="xmark"></dees-icon>
|
||||
<sio-icon icon="x" size="22"></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>
|
||||
`;
|
||||
}
|
||||
@@ -166,20 +247,39 @@ 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>) {
|
||||
await super.updated(changedProperties);
|
||||
|
||||
// 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
163
ts_web/elements/sio-icon.ts
Normal 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 icon: string;
|
||||
|
||||
@property({ type: Number })
|
||||
public size: number = 24;
|
||||
|
||||
@property({ type: String })
|
||||
public color: string = 'currentColor';
|
||||
|
||||
@property({ type: Number })
|
||||
public 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;
|
||||
}
|
||||
}
|
||||
520
ts_web/elements/sio-image-lightbox.ts
Normal file
520
ts_web/elements/sio-image-lightbox.ts
Normal 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 isOpen: boolean = false;
|
||||
|
||||
@property({ type: Object })
|
||||
public 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 fileLoaded: boolean = false;
|
||||
|
||||
@state()
|
||||
private scale: number = 1;
|
||||
|
||||
@state()
|
||||
private translateX: number = 0;
|
||||
|
||||
@state()
|
||||
private 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);
|
||||
}
|
||||
}
|
||||
593
ts_web/elements/sio-pdf-viewer.ts
Normal file
593
ts_web/elements/sio-pdf-viewer.ts
Normal 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 { colors, bdTheme } from './00colors.js';
|
||||
import { spacing, radius, shadows, transitions } 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 url: string = '';
|
||||
|
||||
@property({ type: String })
|
||||
public fileName: string = 'document.pdf';
|
||||
|
||||
@state()
|
||||
private isLoading: boolean = true;
|
||||
|
||||
@state()
|
||||
private hasError: boolean = false;
|
||||
|
||||
@state()
|
||||
private pdfDocument: any = null;
|
||||
|
||||
@state()
|
||||
private currentPage: number = 1;
|
||||
|
||||
@state()
|
||||
private totalPages: number = 0;
|
||||
|
||||
@state()
|
||||
private 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();
|
||||
}
|
||||
}
|
||||
|
||||
protected async firstUpdated() {
|
||||
await super.firstUpdated();
|
||||
|
||||
// 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>) {
|
||||
await 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ const rrwebPlayer: typeof rrwebPlayerMod.default = rrwebPlayerMod as any;
|
||||
* export interface IRecordingEvent extends eventWithTime {}
|
||||
*
|
||||
* Here, for brevity, we define an empty interface
|
||||
* and cast all events to any.
|
||||
* and cast all events to any.
|
||||
*/
|
||||
export interface IRecordingEvent {}
|
||||
|
||||
@@ -32,6 +32,11 @@ export class SioRecorder extends DeesElement {
|
||||
*/
|
||||
private events: IRecordingEvent[] = [];
|
||||
|
||||
/**
|
||||
* status
|
||||
*/
|
||||
public status: 'recording' | 'playing' | 'stopped' = 'stopped';
|
||||
|
||||
/**
|
||||
* A reference to rrweb's stop recording function.
|
||||
* We'll store it when we begin a record session so we can call it later.
|
||||
@@ -59,9 +64,7 @@ export class SioRecorder extends DeesElement {
|
||||
`;
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div id="playback"></div>
|
||||
`;
|
||||
return html` <div id="playback"></div> `;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -101,22 +104,29 @@ export class SioRecorder extends DeesElement {
|
||||
* Starts an rrweb recording session that tracks the entire DOM,
|
||||
* including canvases and cross-origin iframes (if permissible).
|
||||
*/
|
||||
private startRecording(): void {
|
||||
private async startRecording(): Promise<void> {
|
||||
await this.domtoolsPromise;
|
||||
this.status = 'recording';
|
||||
this.events = [];
|
||||
|
||||
// For capturing "everything," enable advanced flags:
|
||||
this.stopFn = rrweb.record({
|
||||
emit: (event: any) => {
|
||||
// If you have a stricter type:
|
||||
// this.events.push(event as IRecordingEvent);
|
||||
// else store as any:
|
||||
this.events.push(event);
|
||||
},
|
||||
// Some recommended settings to capture the "complete" page:
|
||||
recordCanvas: true, // record canvas elements
|
||||
recordCrossOriginIframes: true, // attempt capturing cross-origin iframes
|
||||
// checkoutEveryNms: 1000, // check every N milliseconds
|
||||
});
|
||||
while (this.status === 'recording') {
|
||||
this.stopFn = rrweb.record({
|
||||
emit: (event: any) => {
|
||||
// If you have a stricter type:
|
||||
// this.events.push(event as IRecordingEvent);
|
||||
// else store as any:
|
||||
this.events.push(event);
|
||||
},
|
||||
// Some recommended settings to capture the "complete" page:
|
||||
recordCanvas: true, // record canvas elements
|
||||
recordCrossOriginIframes: true, // attempt capturing cross-origin iframes
|
||||
|
||||
// checkoutEveryNms: 1000, // check every N milliseconds
|
||||
});
|
||||
await this.domtools.convenience.smartdelay.delayFor(1000);
|
||||
await this.stopFn();
|
||||
}
|
||||
|
||||
console.log('Recording has started...');
|
||||
}
|
||||
@@ -138,7 +148,7 @@ export class SioRecorder extends DeesElement {
|
||||
private async playRecording(): Promise<void> {
|
||||
await this.domtoolsPromise;
|
||||
if (!this.playbackDiv) return;
|
||||
const replayer =new rrwebPlayer({
|
||||
const replayer = new rrwebPlayer({
|
||||
target: this.playbackDiv, // customizable root element
|
||||
props: {
|
||||
events: this.events as any,
|
||||
@@ -146,7 +156,6 @@ export class SioRecorder extends DeesElement {
|
||||
showController: false,
|
||||
width: this.playbackDiv.offsetWidth,
|
||||
height: this.playbackDiv.offsetHeight,
|
||||
|
||||
},
|
||||
});
|
||||
this.domtools.convenience.smartdelay.delayFor(0).then(async () => {
|
||||
@@ -163,10 +172,11 @@ export class SioRecorder extends DeesElement {
|
||||
public async fixPosition() {
|
||||
await this.domtoolsPromise;
|
||||
await this.domtools.convenience.smartdelay.delayFor(0);
|
||||
const iframe = this.shadowRoot.querySelector('iframe');
|
||||
const playbackDiv = this.shadowRoot.querySelector('#playback') as HTMLElement;
|
||||
const replayerWrapper = this.shadowRoot.querySelector('.replayer-wrapper') as HTMLElement;
|
||||
const replayerMouse = this.shadowRoot.querySelector('.replayer-mouse') as HTMLElement;
|
||||
const replayerMouseTail = this.shadowRoot.querySelector('.replayer-mouse-tail') as HTMLElement;
|
||||
const iframe = this.shadowRoot.querySelector('iframe');
|
||||
replayerWrapper.style.position = 'absolute';
|
||||
replayerWrapper.style.top = '0px';
|
||||
replayerWrapper.style.left = '0px';
|
||||
@@ -176,6 +186,21 @@ export class SioRecorder extends DeesElement {
|
||||
iframe.style.position = 'absolute';
|
||||
iframe.style.top = '0px';
|
||||
iframe.style.left = '0px';
|
||||
iframe.style.border = 'none';
|
||||
|
||||
// set z-index
|
||||
replayerWrapper.style.zIndex = '1000';
|
||||
iframe.style.zIndex = '1000';
|
||||
replayerMouse.style.zIndex = '1002';
|
||||
replayerMouseTail.style.zIndex = '1001';
|
||||
|
||||
// lets show a mouse cursor
|
||||
replayerMouse.style.width = '10px';
|
||||
replayerMouse.style.height = '10px';
|
||||
replayerMouse.style.background = 'green';
|
||||
replayerMouse.style.transform = 'translate(-50%, -50%)';
|
||||
replayerMouse.style.borderRadius = '50%';
|
||||
replayerMouse.style.border = '1px solid white';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -185,4 +210,4 @@ export class SioRecorder extends DeesElement {
|
||||
this.stopRecording();
|
||||
this.playRecording();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -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: 20px;
|
||||
}
|
||||
.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>
|
||||
`;
|
||||
Reference in New Issue
Block a user