Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bbb6d09ecf | |||
| 2f54ee3f85 | |||
| a066e0de73 | |||
| 7731054f0e | |||
| 5f48ecf7af | |||
| f9d281c496 | |||
| 6e29f0b51a | |||
| 5e25f86a0b | |||
| 01dad7cc5e | |||
| a4a3c6dc50 | |||
| 193b1f5234 | |||
| 95e92a5533 | |||
| 1caeae9ec9 | |||
| c534d1d084 | |||
| 23592f3a15 | |||
| 66493f793f | |||
| efd142d63d | |||
| 9ab16c85ba | |||
| ba791ee18a | |||
| b49707a727 | |||
| 2ea29cffbb |
28
changelog.md
28
changelog.md
@@ -1,5 +1,33 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2025-12-17 - 1.4.0 - feat(elements)
|
||||||
|
update design tokens and sio-fab component; bump deps and update npmextra config
|
||||||
|
|
||||||
|
- Refactor color tokens to a neutral HSL palette (ts_web/elements/00colors.ts) and adjust focus ring token (ts_web/elements/00tokens.ts).
|
||||||
|
- Refactor sio-fab: move styles to static property, add responsive FAB sizing and getMobileIconSize(), bind icon sizes, manage host class ('combox-open'), and tidy lifecycle methods for better behavior and mobile support.
|
||||||
|
- Bump dependencies and devDependencies: @design.estate/dees-wcctools -> ^2.0.1, lucide -> ^0.561.0; @git.zone/tsbuild -> ^4.0.2, @git.zone/tsrun -> ^2.0.1, @git.zone/tswatch -> ^2.3.13, @types/node -> ^25.0.3, etc.
|
||||||
|
- Update npmextra.json: rename configuration keys (gitzone -> @git.zone/cli, npmci -> @ship.zone/szci) and add release.registries and accessLevel for publishing.
|
||||||
|
|
||||||
|
## 2025-12-08 - 1.3.0 - feat(components)
|
||||||
|
Add reusable message input component, refactor element properties to use accessor, update styles and docs, bump dependencies
|
||||||
|
|
||||||
|
- Add new <sio-message-input> component (auto-resizing textarea, file picker, send/files events) and integrate it into sio-conversation-view
|
||||||
|
- Refactor multiple element class fields to use the 'accessor' property pattern (sio-button, sio-combox, sio-conversation-view, sio-dropdown-menu, sio-fab, sio-icon, sio-image-lightbox, sio-pdf-viewer, sio-recorder, etc.)
|
||||||
|
- Significant visual and UX updates to sio-button (new secondary variant, sizing, spacing, icon sizing, focus/disabled behavior)
|
||||||
|
- Move inline conversation input logic into the new component and simplify message handling (dispatch send-message event with text and attachments)
|
||||||
|
- Improve PDF viewer: safer async pdf.js loader, resize observer for responsive rendering, better error fallback and lifecycle cleanup
|
||||||
|
- Enhance image lightbox (PDF handling via sio-pdf-viewer, zoom/drag controls, keyboard shortcuts, download/open actions)
|
||||||
|
- Add icon caching and make lucide upgrade (cache limit, PascalCase lookup) to reduce re-renders
|
||||||
|
- Polish tokens/styles: spacing, radii, shadows, transitions and small responsive/layout tweaks (e.g. main page padding)
|
||||||
|
- Update README to reflect the public package name, components, quick start and development instructions
|
||||||
|
- Bump runtime and dev dependencies (dees-element/domtools/wcctools, lucide, git.zone tools, push.rocks smartenv/types) and adjust tsconfig (target/module settings simplified)
|
||||||
|
|
||||||
|
## 2025-04-20 - 1.2.4 - fix(build)
|
||||||
|
Update build script and async function signature
|
||||||
|
|
||||||
|
- 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)
|
## 2025-04-20 - 1.2.3 - fix(core)
|
||||||
Update dependency versions and adjust UI CSS for fab and combox elements
|
Update dependency versions and adjust UI CSS for fab and combox elements
|
||||||
|
|
||||||
|
|||||||
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>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"gitzone": {
|
"@git.zone/cli": {
|
||||||
"projectType": "wcc",
|
"projectType": "wcc",
|
||||||
"module": {
|
"module": {
|
||||||
"githost": "gitlab.com",
|
"githost": "gitlab.com",
|
||||||
@@ -9,11 +9,16 @@
|
|||||||
"npmPackagename": "@social.io_private/catalog",
|
"npmPackagename": "@social.io_private/catalog",
|
||||||
"license": "UNLICENSED",
|
"license": "UNLICENSED",
|
||||||
"projectDomain": "social.io"
|
"projectDomain": "social.io"
|
||||||
|
},
|
||||||
|
"release": {
|
||||||
|
"registries": [
|
||||||
|
"https://verdaccio.lossless.digital"
|
||||||
|
],
|
||||||
|
"accessLevel": "public"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"npmci": {
|
"@ship.zone/szci": {
|
||||||
"npmRegistryUrl": "verdaccio.lossless.one",
|
"npmRegistryUrl": "verdaccio.lossless.one",
|
||||||
"npmGlobalTools": [],
|
"npmGlobalTools": []
|
||||||
"npmAccessLevel": "private"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
27
package.json
27
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@social.io/catalog",
|
"name": "@social.io/catalog",
|
||||||
"version": "1.2.3",
|
"version": "1.4.0",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "catalog for social.io",
|
"description": "catalog for social.io",
|
||||||
"main": "dist_ts_web/index.js",
|
"main": "dist_ts_web/index.js",
|
||||||
@@ -8,33 +8,32 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "tstest test/",
|
"test": "tstest test/",
|
||||||
"build": "tsbuild tsfolders --allowimplicitany && tsbundle element --production",
|
"build": "tsbuild tsfolders --allowimplicitany --skiplibcheck && tsbundle element --production",
|
||||||
"watch": "tswatch element",
|
"watch": "tswatch element",
|
||||||
"buildDocs": "tsdoc"
|
"buildDocs": "tsdoc"
|
||||||
},
|
},
|
||||||
"author": "Lossless GmbH",
|
"author": "Lossless GmbH",
|
||||||
"license": "UNLICENSED",
|
"license": "UNLICENSED",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@design.estate/dees-catalog": "^1.5.6",
|
"@design.estate/dees-domtools": "^2.3.6",
|
||||||
"@design.estate/dees-domtools": "^2.3.2",
|
"@design.estate/dees-element": "^2.1.3",
|
||||||
"@design.estate/dees-element": "^2.0.42",
|
"@design.estate/dees-wcctools": "^2.0.1",
|
||||||
"@design.estate/dees-wcctools": "^1.0.90",
|
|
||||||
"@losslessone_private/loint-pubapi": "^1.0.14",
|
"@losslessone_private/loint-pubapi": "^1.0.14",
|
||||||
"@social.io/interfaces": "^1.2.1",
|
"@social.io/interfaces": "^1.2.1",
|
||||||
|
"lucide": "^0.561.0",
|
||||||
"rrweb": "2.0.0-alpha.4",
|
"rrweb": "2.0.0-alpha.4",
|
||||||
"rrweb-player": "1.0.0-alpha.4",
|
"rrweb-player": "1.0.0-alpha.4",
|
||||||
"rrweb-snapshot": "2.0.0-alpha.4"
|
"rrweb-snapshot": "2.0.0-alpha.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@git.zone/tsbuild": "^2.3.2",
|
"@git.zone/tsbuild": "^4.0.2",
|
||||||
"@git.zone/tsbundle": "^2.2.5",
|
"@git.zone/tsbundle": "^2.6.3",
|
||||||
"@git.zone/tsrun": "^1.3.3",
|
"@git.zone/tsrun": "^2.0.1",
|
||||||
"@git.zone/tstest": "^1.0.96",
|
"@git.zone/tstest": "^3.1.3",
|
||||||
"@git.zone/tswatch": "^2.1.0",
|
"@git.zone/tswatch": "^2.3.13",
|
||||||
"@push.rocks/projectinfo": "^5.0.2",
|
"@push.rocks/projectinfo": "^5.0.2",
|
||||||
"@push.rocks/smartenv": "^5.0.12",
|
"@push.rocks/smartenv": "^6.0.0",
|
||||||
"@push.rocks/tapbundle": "^5.6.3",
|
"@types/node": "^25.0.3"
|
||||||
"@types/node": "^22.14.1"
|
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"ts/**/*",
|
"ts/**/*",
|
||||||
|
|||||||
9014
pnpm-lock.yaml
generated
9014
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
220
readme.md
220
readme.md
@@ -1,34 +1,204 @@
|
|||||||
# @social.io/private/catalog
|
# @social.io/catalog
|
||||||
|
|
||||||
the element catalog for the lossless organization
|
A modern, beautifully designed UI component library for building conversational interfaces and support chat experiences. Built with Lit Element and TypeScript.
|
||||||
|
|
||||||
## Availabililty and Links
|
## Issue Reporting and Security
|
||||||
|
|
||||||
- [npmjs.org (npm package)](https://www.npmjs.com/package/@social.io_private/catalog)
|
For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
|
||||||
- [gitlab.com (source)](https://gitlab.com/social.io/private/catalog)
|
|
||||||
- [github.com (source mirror)](https://github.com/social.io/private/catalog)
|
|
||||||
- [docs (typedoc)](https://social.io/private.gitlab.io/catalog/)
|
|
||||||
|
|
||||||
## Status for master
|
## 🎯 Features
|
||||||
|
|
||||||
| Status Category | Status Badge |
|
- **Complete Chat UI** - Ready-to-use conversation components with message threads, typing indicators, and attachments
|
||||||
| ----------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
|
- **Floating Action Button** - Eye-catching FAB with smooth animations for triggering the chat interface
|
||||||
| GitLab Pipelines | [](https://lossless.cloud) |
|
- **PDF Viewer** - Built-in PDF rendering with zoom, pagination, and download capabilities
|
||||||
| GitLab Pipline Test Coverage | [](https://lossless.cloud) |
|
- **Image Lightbox** - Full-featured lightbox with zoom, pan, and keyboard navigation
|
||||||
| npm | [](https://lossless.cloud) |
|
- **Modern Design Tokens** - Consistent styling with customizable colors, spacing, typography, and shadows
|
||||||
| Snyk | [](https://lossless.cloud) |
|
- **Dark Mode Ready** - Full light/dark theme support out of the box
|
||||||
| TypeScript Support | [](https://lossless.cloud) |
|
- **Accessibility** - Keyboard navigation and proper ARIA attributes
|
||||||
| node Support | [](https://nodejs.org/dist/latest-v10.x/docs/api/) |
|
- **TypeScript First** - Full type definitions for all components
|
||||||
| Code Style | [](https://lossless.cloud) |
|
|
||||||
| PackagePhobia (total standalone install weight) | [](https://lossless.cloud) |
|
|
||||||
| PackagePhobia (package size on registry) | [](https://lossless.cloud) |
|
|
||||||
| BundlePhobia (total size when bundled) | [](https://lossless.cloud) |
|
|
||||||
|
|
||||||
## Usage
|
## 📦 Installation
|
||||||
|
|
||||||
For further information read the linked docs at the top of this readme.
|
```bash
|
||||||
|
npm install @social.io/catalog
|
||||||
|
# or
|
||||||
|
pnpm add @social.io/catalog
|
||||||
|
```
|
||||||
|
|
||||||
## Legal
|
## 🚀 Quick Start
|
||||||
|
|
||||||
> UNLICENSED licensed | **©** [Task Venture Capital GmbH](https://task.vc)
|
```typescript
|
||||||
> | By using this npm module you agree to our [privacy policy](https://lossless.gmbH/privacy)
|
import { SioFab, SioCombox } from '@social.io/catalog';
|
||||||
|
|
||||||
|
// Components auto-register as custom elements
|
||||||
|
// Just use them in your HTML:
|
||||||
|
```
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- Floating Action Button that opens the chat -->
|
||||||
|
<sio-fab></sio-fab>
|
||||||
|
|
||||||
|
<!-- Or use the full chat box directly -->
|
||||||
|
<sio-combox></sio-combox>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🧩 Components
|
||||||
|
|
||||||
|
### Core Components
|
||||||
|
|
||||||
|
| Component | Description |
|
||||||
|
|-----------|-------------|
|
||||||
|
| `<sio-fab>` | Floating action button with animated chat icon |
|
||||||
|
| `<sio-combox>` | Complete chat interface with conversation list and message view |
|
||||||
|
| `<sio-button>` | Styled button with variants (primary, secondary, destructive, outline, ghost) |
|
||||||
|
| `<sio-icon>` | Lucide icon wrapper with size and color customization |
|
||||||
|
| `<sio-dropdown-menu>` | Animated dropdown menu with keyboard support |
|
||||||
|
|
||||||
|
### Conversation Components
|
||||||
|
|
||||||
|
| Component | Description |
|
||||||
|
|-----------|-------------|
|
||||||
|
| `<sio-conversation-selector>` | Searchable list of conversations with unread indicators |
|
||||||
|
| `<sio-conversation-view>` | Message thread with typing indicators and file attachments |
|
||||||
|
| `<sio-message-input>` | Auto-expanding textarea with file upload |
|
||||||
|
|
||||||
|
### Media Components
|
||||||
|
|
||||||
|
| Component | Description |
|
||||||
|
|-----------|-------------|
|
||||||
|
| `<sio-image-lightbox>` | Fullscreen image viewer with zoom and pan |
|
||||||
|
| `<sio-pdf-viewer>` | PDF renderer with page navigation and zoom controls |
|
||||||
|
|
||||||
|
### Utility Components
|
||||||
|
|
||||||
|
| Component | Description |
|
||||||
|
|-----------|-------------|
|
||||||
|
| `<sio-recorder>` | Session recording using rrweb |
|
||||||
|
|
||||||
|
## 💅 Styling & Theming
|
||||||
|
|
||||||
|
The library uses CSS custom properties for theming. The design system includes:
|
||||||
|
|
||||||
|
- **Colors** - Primary, secondary, accent, destructive, muted, and semantic colors
|
||||||
|
- **Typography** - System font stack with size and weight variants
|
||||||
|
- **Spacing** - Consistent spacing scale (0.5rem increments)
|
||||||
|
- **Radius** - Border radius tokens from sm to full
|
||||||
|
- **Shadows** - Elevation system from sm to 2xl
|
||||||
|
- **Transitions** - Smooth animation presets
|
||||||
|
|
||||||
|
### Dark Mode
|
||||||
|
|
||||||
|
Dark mode is automatically supported. The components use `bdTheme()` helper that switches between light and dark values:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { bdTheme } from '@social.io/catalog';
|
||||||
|
|
||||||
|
// Usage in styles
|
||||||
|
css`
|
||||||
|
background: ${bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 10%)')};
|
||||||
|
`
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📖 Usage Examples
|
||||||
|
|
||||||
|
### Basic Chat FAB
|
||||||
|
|
||||||
|
```html
|
||||||
|
<sio-fab></sio-fab>
|
||||||
|
```
|
||||||
|
|
||||||
|
The FAB opens a complete chat interface when clicked. It includes:
|
||||||
|
- Keyboard shortcut (Ctrl+S) to toggle
|
||||||
|
- Smooth scale and pulse animations
|
||||||
|
- Gradient background with glow effects
|
||||||
|
|
||||||
|
### Custom Button Variants
|
||||||
|
|
||||||
|
```html
|
||||||
|
<sio-button type="primary">Submit</sio-button>
|
||||||
|
<sio-button type="destructive">Delete</sio-button>
|
||||||
|
<sio-button type="outline">Cancel</sio-button>
|
||||||
|
<sio-button type="ghost" size="sm">
|
||||||
|
<sio-icon icon="settings"></sio-icon>
|
||||||
|
</sio-button>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Image Lightbox
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const lightbox = document.querySelector('sio-image-lightbox');
|
||||||
|
lightbox.open({
|
||||||
|
url: 'https://example.com/photo.jpg',
|
||||||
|
name: 'My Photo',
|
||||||
|
size: 1024000
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### PDF Viewer
|
||||||
|
|
||||||
|
```html
|
||||||
|
<sio-pdf-viewer
|
||||||
|
url="https://example.com/document.pdf"
|
||||||
|
fileName="document.pdf"
|
||||||
|
></sio-pdf-viewer>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dropdown Menu
|
||||||
|
|
||||||
|
```html
|
||||||
|
<sio-dropdown-menu
|
||||||
|
.items=${[
|
||||||
|
{ id: 'edit', label: 'Edit', icon: 'pencil' },
|
||||||
|
{ id: 'delete', label: 'Delete', icon: 'trash', destructive: true }
|
||||||
|
]}
|
||||||
|
@item-selected=${(e) => console.log('Selected:', e.detail.item)}
|
||||||
|
>
|
||||||
|
<sio-button type="ghost">
|
||||||
|
<sio-icon icon="more-vertical"></sio-icon>
|
||||||
|
</sio-button>
|
||||||
|
</sio-dropdown-menu>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install dependencies
|
||||||
|
pnpm install
|
||||||
|
|
||||||
|
# Start development server with hot reload
|
||||||
|
pnpm watch
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
pnpm test
|
||||||
|
|
||||||
|
# Build for production
|
||||||
|
pnpm build
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📚 Dependencies
|
||||||
|
|
||||||
|
- **@design.estate/dees-element** - Lit Element base with utilities
|
||||||
|
- **@design.estate/dees-domtools** - DOM manipulation helpers
|
||||||
|
- **lucide** - Beautiful open-source icons
|
||||||
|
- **rrweb** - Session recording/replay (for recorder component)
|
||||||
|
|
||||||
|
## License and Legal Information
|
||||||
|
|
||||||
|
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./LICENSE) file.
|
||||||
|
|
||||||
|
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
### Trademarks
|
||||||
|
|
||||||
|
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
|
||||||
|
|
||||||
|
Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
|
||||||
|
|
||||||
|
### Company Information
|
||||||
|
|
||||||
|
Task Venture Capital GmbH
|
||||||
|
Registered at District Court Bremen HRB 35230 HB, Germany
|
||||||
|
|
||||||
|
For any legal inquiries or further information, please contact us via email at hello@task.vc.
|
||||||
|
|
||||||
|
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
|
||||||
|
|||||||
@@ -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';
|
import * as socialioCatalog from '../ts_web/index.js';
|
||||||
|
|
||||||
tap.test('', async () => {
|
tap.test('render combox component', async () => {
|
||||||
const sioFab: socialioCatalog.SioFab = webhelpers.fixture(webhelpers.html`<sio-fab></sio-fab>`);
|
// Create and add combox
|
||||||
|
const combox = new socialioCatalog.SioCombox();
|
||||||
|
combox.style.position = 'relative';
|
||||||
|
combox.style.width = '800px';
|
||||||
|
combox.style.height = '600px';
|
||||||
|
document.body.appendChild(combox);
|
||||||
|
|
||||||
|
await combox.updateComplete;
|
||||||
|
|
||||||
|
expect(combox).toBeInstanceOf(socialioCatalog.SioCombox);
|
||||||
|
|
||||||
|
// Check that the component rendered its content
|
||||||
|
const container = combox.shadowRoot.querySelector('.container');
|
||||||
|
expect(container).toBeTruthy();
|
||||||
|
|
||||||
|
const conversationSelector = combox.shadowRoot.querySelector('sio-conversation-selector');
|
||||||
|
expect(conversationSelector).toBeTruthy();
|
||||||
|
|
||||||
|
const conversationView = combox.shadowRoot.querySelector('sio-conversation-view');
|
||||||
|
expect(conversationView).toBeTruthy();
|
||||||
|
|
||||||
|
console.log('Combox component rendered successfully with all main elements');
|
||||||
|
|
||||||
|
document.body.removeChild(combox);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('render fab component', async () => {
|
||||||
|
// Create and add fab
|
||||||
|
const fab = new socialioCatalog.SioFab();
|
||||||
|
document.body.appendChild(fab);
|
||||||
|
|
||||||
|
await fab.updateComplete;
|
||||||
|
expect(fab).toBeInstanceOf(socialioCatalog.SioFab);
|
||||||
|
|
||||||
|
// Check main elements
|
||||||
|
const mainbox = fab.shadowRoot.querySelector('#mainbox');
|
||||||
|
expect(mainbox).toBeTruthy();
|
||||||
|
|
||||||
|
console.log('FAB component rendered successfully');
|
||||||
|
|
||||||
|
document.body.removeChild(fab);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('render image lightbox component', async () => {
|
||||||
|
// Create and add lightbox
|
||||||
|
const lightbox = new socialioCatalog.SioImageLightbox();
|
||||||
|
document.body.appendChild(lightbox);
|
||||||
|
|
||||||
|
await lightbox.updateComplete;
|
||||||
|
expect(lightbox).toBeInstanceOf(socialioCatalog.SioImageLightbox);
|
||||||
|
|
||||||
|
// Check main elements
|
||||||
|
const overlay = lightbox.shadowRoot.querySelector('.overlay');
|
||||||
|
expect(overlay).toBeTruthy();
|
||||||
|
|
||||||
|
const container = lightbox.shadowRoot.querySelector('.container');
|
||||||
|
expect(container).toBeTruthy();
|
||||||
|
|
||||||
|
// Test opening with an image
|
||||||
|
await lightbox.open({
|
||||||
|
url: 'https://picsum.photos/800/600',
|
||||||
|
name: 'Test Image',
|
||||||
|
size: 123456
|
||||||
|
});
|
||||||
|
|
||||||
|
await lightbox.updateComplete;
|
||||||
|
expect(lightbox.isOpen).toEqual(true);
|
||||||
|
|
||||||
|
// Test opening with a PDF
|
||||||
|
await lightbox.open({
|
||||||
|
url: 'data:application/pdf;base64,JVBERi0xLjMKJeLjz9MKMSAwIG9iago8PAovVHlwZSAvQ2F0YWxvZwovT3V0bGluZXMgMiAwIFIKL1BhZ2VzIDMgMCBSCj4+CmVuZG9iagoyIDAgb2JqCjw8Ci9UeXBlIC9PdXRsaW5lcwovQ291bnQgMAo+PgplbmRvYmoKMyAwIG9iago8PAovVHlwZSAvUGFnZXMKL0NvdW50IDEKL0tpZHMgWzQgMCBSXQo+PgplbmRvYmoKNCAwIG9iago8PAovVHlwZSAvUGFnZQovUGFyZW50IDMgMCBSCi9NZWRpYUJveCBbMCAwIDYxMiA3OTJdCi9Db250ZW50cyA1IDAgUgovUmVzb3VyY2VzIDw8Ci9Gb250IDw8Ci9GMSA2IDAgUgo+Pgo+Pgo+PgplbmRvYmoKNSAwIG9iago8PAovTGVuZ3RoIDQ0Cj4+CnN0cmVhbQpCVApxCjcwIDUwIFRECi9GMSAxMiBUZgooSGVsbG8gV29ybGQpIFRqCkVUClEKZW5kc3RyZWFtCmVuZG9iago2IDAgb2JqCjw8Ci9UeXBlIC9Gb250Ci9TdWJ0eXBlIC9UeXBlMQovQmFzZUZvbnQgL1RpbWVzLVJvbWFuCj4+CmVuZG9iagp4cmVmCjAgNwowMDAwMDAwMDAwIDY1NTM1IGYgCjAwMDAwMDAwMDkgMDAwMDAgbiAKMDAwMDAwMDA3NCAwMDAwMCBuIAowMDAwMDAwMTIwIDAwMDAwIG4gCjAwMDAwMDAxNzkgMDAwMDAgbiAKMDAwMDAwMDM2NCAwMDAwMCBuIAowMDAwMDAwNDY2IDAwMDAwIG4gCnRyYWlsZXIKPDwKL1NpemUgNwovUm9vdCAxIDAgUgo+PgpzdGFydHhyZWYKNTY1CiUlRU9G',
|
||||||
|
name: 'test.pdf',
|
||||||
|
type: 'application/pdf',
|
||||||
|
size: 565
|
||||||
|
});
|
||||||
|
|
||||||
|
await lightbox.updateComplete;
|
||||||
|
|
||||||
|
// Check that PDF viewer is rendered
|
||||||
|
const pdfViewer = lightbox.shadowRoot.querySelector('sio-pdf-viewer');
|
||||||
|
expect(pdfViewer).toBeTruthy();
|
||||||
|
|
||||||
|
console.log('Image lightbox component rendered successfully with both image and PDF support');
|
||||||
|
|
||||||
|
document.body.removeChild(lightbox);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('render dropdown menu component', async () => {
|
||||||
|
// Create and add dropdown menu
|
||||||
|
const dropdown = new socialioCatalog.SioDropdownMenu();
|
||||||
|
dropdown.items = [
|
||||||
|
{ id: 'option1', label: 'Option 1', icon: 'settings' },
|
||||||
|
{ id: 'option2', label: 'Option 2', icon: 'user' },
|
||||||
|
{ id: 'divider', label: '', divider: true },
|
||||||
|
{ id: 'delete', label: 'Delete', icon: 'trash', destructive: true }
|
||||||
|
];
|
||||||
|
document.body.appendChild(dropdown);
|
||||||
|
|
||||||
|
await dropdown.updateComplete;
|
||||||
|
expect(dropdown).toBeInstanceOf(socialioCatalog.SioDropdownMenu);
|
||||||
|
|
||||||
|
// Check main elements
|
||||||
|
const trigger = dropdown.shadowRoot.querySelector('.trigger');
|
||||||
|
expect(trigger).toBeTruthy();
|
||||||
|
|
||||||
|
const dropdownElement = dropdown.shadowRoot.querySelector('.dropdown');
|
||||||
|
expect(dropdownElement).toBeTruthy();
|
||||||
|
|
||||||
|
// Check menu items
|
||||||
|
const menuItems = dropdown.shadowRoot.querySelectorAll('.menu-item');
|
||||||
|
expect(menuItems.length).toEqual(3); // 3 items (excluding divider)
|
||||||
|
|
||||||
|
console.log('Dropdown menu component rendered successfully');
|
||||||
|
|
||||||
|
document.body.removeChild(dropdown);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('render pdf viewer component', async () => {
|
||||||
|
// Create and add PDF viewer
|
||||||
|
const pdfViewer = new socialioCatalog.SioPdfViewer();
|
||||||
|
pdfViewer.style.width = '600px';
|
||||||
|
pdfViewer.style.height = '400px';
|
||||||
|
pdfViewer.url = 'data:application/pdf;base64,JVBERi0xLjMKJeLjz9MKMSAwIG9iago8PAovVHlwZSAvQ2F0YWxvZwovT3V0bGluZXMgMiAwIFIKL1BhZ2VzIDMgMCBSCj4+CmVuZG9iagoyIDAgb2JqCjw8Ci9UeXBlIC9PdXRsaW5lcwovQ291bnQgMAo+PgplbmRvYmoKMyAwIG9iago8PAovVHlwZSAvUGFnZXMKL0NvdW50IDEKL0tpZHMgWzQgMCBSXQo+PgplbmRvYmoKNCAwIG9iago8PAovVHlwZSAvUGFnZQovUGFyZW50IDMgMCBSCi9NZWRpYUJveCBbMCAwIDYxMiA3OTJdCi9Db250ZW50cyA1IDAgUgovUmVzb3VyY2VzIDw8Ci9Gb250IDw8Ci9GMSA2IDAgUgo+Pgo+Pgo+PgplbmRvYmoKNSAwIG9iago8PAovTGVuZ3RoIDQ0Cj4+CnN0cmVhbQpCVApxCjcwIDUwIFRECi9GMSAxMiBUZgooSGVsbG8gV29ybGQpIFRqCkVUClEKZW5kc3RyZWFtCmVuZG9iago2IDAgb2JqCjw8Ci9UeXBlIC9Gb250Ci9TdWJ0eXBlIC9UeXBlMQovQmFzZUZvbnQgL1RpbWVzLVJvbWFuCj4+CmVuZG9iagp4cmVmCjAgNwowMDAwMDAwMDAwIDY1NTM1IGYgCjAwMDAwMDAwMDkgMDAwMDAgbiAKMDAwMDAwMDA3NCAwMDAwMCBuIAowMDAwMDAwMTIwIDAwMDAwIG4gCjAwMDAwMDAxNzkgMDAwMDAgbiAKMDAwMDAwMDM2NCAwMDAwMCBuIAowMDAwMDAwNDY2IDAwMDAwIG4gCnRyYWlsZXIKPDwKL1NpemUgNwovUm9vdCAxIDAgUgo+PgpzdGFydHhyZWYKNTY1CiUlRU9G';
|
||||||
|
pdfViewer.fileName = 'test.pdf';
|
||||||
|
document.body.appendChild(pdfViewer);
|
||||||
|
|
||||||
|
await pdfViewer.updateComplete;
|
||||||
|
expect(pdfViewer).toBeInstanceOf(socialioCatalog.SioPdfViewer);
|
||||||
|
|
||||||
|
// Check main elements
|
||||||
|
const container = pdfViewer.shadowRoot.querySelector('.container');
|
||||||
|
expect(container).toBeTruthy();
|
||||||
|
|
||||||
|
// PDF viewer uses canvas after loading, not iframe
|
||||||
|
// Just verify the component rendered correctly
|
||||||
|
expect(pdfViewer.url).toEqual('data:application/pdf;base64,JVBERi0xLjMKJeLjz9MKMSAwIG9iago8PAovVHlwZSAvQ2F0YWxvZwovT3V0bGluZXMgMiAwIFIKL1BhZ2VzIDMgMCBSCj4+CmVuZG9iagoyIDAgb2JqCjw8Ci9UeXBlIC9PdXRsaW5lcwovQ291bnQgMAo+PgplbmRvYmoKMyAwIG9iago8PAovVHlwZSAvUGFnZXMKL0NvdW50IDEKL0tpZHMgWzQgMCBSXQo+PgplbmRvYmoKNCAwIG9iago8PAovVHlwZSAvUGFnZQovUGFyZW50IDMgMCBSCi9NZWRpYUJveCBbMCAwIDYxMiA3OTJdCi9Db250ZW50cyA1IDAgUgovUmVzb3VyY2VzIDw8Ci9Gb250IDw8Ci9GMSA2IDAgUgo+Pgo+Pgo+PgplbmRvYmoKNSAwIG9iago8PAovTGVuZ3RoIDQ0Cj4+CnN0cmVhbQpCVApxCjcwIDUwIFRECi9GMSAxMiBUZgooSGVsbG8gV29ybGQpIFRqCkVUClEKZW5kc3RyZWFtCmVuZG9iago2IDAgb2JqCjw8Ci9UeXBlIC9Gb250Ci9TdWJ0eXBlIC9UeXBlMQovQmFzZUZvbnQgL1RpbWVzLVJvbWFuCj4+CmVuZG9iagp4cmVmCjAgNwowMDAwMDAwMDAwIDY1NTM1IGYgCjAwMDAwMDAwMDkgMDAwMDAgbiAKMDAwMDAwMDA3NCAwMDAwMCBuIAowMDAwMDAwMTIwIDAwMDAwIG4gCjAwMDAwMDAxNzkgMDAwMDAgbiAKMDAwMDAwMDM2NCAwMDAwMCBuIAowMDAwMDAwNDY2IDAwMDAwIG4gCnRyYWlsZXIKPDwKL1NpemUgNwovUm9vdCAxIDAgUgo+PgpzdGFydHhyZWYKNTY1CiUlRU9G');
|
||||||
|
expect(pdfViewer.fileName).toEqual('test.pdf');
|
||||||
|
|
||||||
|
console.log('PDF viewer component rendered successfully');
|
||||||
|
|
||||||
|
document.body.removeChild(pdfViewer);
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.start();
|
tap.start();
|
||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@social.io/catalog',
|
name: '@social.io/catalog',
|
||||||
version: '1.2.3',
|
version: '1.4.0',
|
||||||
description: 'catalog for social.io'
|
description: 'catalog for social.io'
|
||||||
}
|
}
|
||||||
|
|||||||
164
ts_web/elements/00colors.ts
Normal file
164
ts_web/elements/00colors.ts
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
import { cssManager } from '@design.estate/dees-element';
|
||||||
|
|
||||||
|
export const colors = {
|
||||||
|
// Background colors - softer, more subtle
|
||||||
|
background: {
|
||||||
|
light: 'hsl(0 0% 100%)',
|
||||||
|
dark: 'hsl(0 0% 4%)'
|
||||||
|
},
|
||||||
|
|
||||||
|
// Foreground colors - less contrast for modern look
|
||||||
|
foreground: {
|
||||||
|
light: 'hsl(0 0% 4%)',
|
||||||
|
dark: 'hsl(0 0% 98%)'
|
||||||
|
},
|
||||||
|
|
||||||
|
// Card colors - subtle elevation
|
||||||
|
card: {
|
||||||
|
light: 'hsl(0 0% 100%)',
|
||||||
|
dark: 'hsl(0 0% 4%)'
|
||||||
|
},
|
||||||
|
|
||||||
|
cardForeground: {
|
||||||
|
light: 'hsl(0 0% 4%)',
|
||||||
|
dark: 'hsl(0 0% 98%)'
|
||||||
|
},
|
||||||
|
|
||||||
|
// Popover colors
|
||||||
|
popover: {
|
||||||
|
light: 'hsl(0 0% 100%)',
|
||||||
|
dark: 'hsl(0 0% 5%)'
|
||||||
|
},
|
||||||
|
|
||||||
|
popoverForeground: {
|
||||||
|
light: 'hsl(0 0% 5%)',
|
||||||
|
dark: 'hsl(0 0% 98%)'
|
||||||
|
},
|
||||||
|
|
||||||
|
// Primary colors - modern indigo/blue
|
||||||
|
primary: {
|
||||||
|
light: 'hsl(221.2 83.2% 53.3%)',
|
||||||
|
dark: 'hsl(217.2 91.2% 59.8%)'
|
||||||
|
},
|
||||||
|
|
||||||
|
primaryForeground: {
|
||||||
|
light: 'hsl(0 0% 98%)',
|
||||||
|
dark: 'hsl(0 0% 4%)'
|
||||||
|
},
|
||||||
|
|
||||||
|
// Secondary colors - more subtle
|
||||||
|
secondary: {
|
||||||
|
light: 'hsl(0 0% 96%)',
|
||||||
|
dark: 'hsl(0 0% 17%)'
|
||||||
|
},
|
||||||
|
|
||||||
|
secondaryForeground: {
|
||||||
|
light: 'hsl(0 0% 11%)',
|
||||||
|
dark: 'hsl(0 0% 98%)'
|
||||||
|
},
|
||||||
|
|
||||||
|
// Muted colors - softer grays
|
||||||
|
muted: {
|
||||||
|
light: 'hsl(0 0% 96%)',
|
||||||
|
dark: 'hsl(0 0% 17%)'
|
||||||
|
},
|
||||||
|
|
||||||
|
mutedForeground: {
|
||||||
|
light: 'hsl(0 0% 46%)',
|
||||||
|
dark: 'hsl(0 0% 65%)'
|
||||||
|
},
|
||||||
|
|
||||||
|
// Accent colors - subtle hover states
|
||||||
|
accent: {
|
||||||
|
light: 'hsl(0 0% 96%)',
|
||||||
|
dark: 'hsl(0 0% 17%)'
|
||||||
|
},
|
||||||
|
|
||||||
|
accentForeground: {
|
||||||
|
light: 'hsl(0 0% 11%)',
|
||||||
|
dark: 'hsl(0 0% 98%)'
|
||||||
|
},
|
||||||
|
|
||||||
|
// Destructive colors - softer red
|
||||||
|
destructive: {
|
||||||
|
light: 'hsl(0 72.2% 50.6%)',
|
||||||
|
dark: 'hsl(0 62.8% 30.6%)'
|
||||||
|
},
|
||||||
|
|
||||||
|
destructiveForeground: {
|
||||||
|
light: 'hsl(0 0% 98%)',
|
||||||
|
dark: 'hsl(0 0% 98%)'
|
||||||
|
},
|
||||||
|
|
||||||
|
// Border color - very subtle
|
||||||
|
border: {
|
||||||
|
light: 'hsl(0 0% 91%)',
|
||||||
|
dark: 'hsl(0 0% 17%)'
|
||||||
|
},
|
||||||
|
|
||||||
|
// Input color
|
||||||
|
input: {
|
||||||
|
light: 'hsl(0 0% 91%)',
|
||||||
|
dark: 'hsl(0 0% 18%)'
|
||||||
|
},
|
||||||
|
|
||||||
|
// Ring color - subtle focus indicator
|
||||||
|
ring: {
|
||||||
|
light: 'hsl(221.2 83.2% 53.3%)',
|
||||||
|
dark: 'hsl(217.2 91.2% 59.8%)'
|
||||||
|
},
|
||||||
|
|
||||||
|
// Success colors - modern green
|
||||||
|
success: {
|
||||||
|
light: 'hsl(142.1 70.6% 45.3%)',
|
||||||
|
dark: 'hsl(144.9 80.4% 10%)'
|
||||||
|
},
|
||||||
|
|
||||||
|
successForeground: {
|
||||||
|
light: 'hsl(0 0% 100%)',
|
||||||
|
dark: 'hsl(144.9 80.4% 80%)'
|
||||||
|
},
|
||||||
|
|
||||||
|
// Chart colors
|
||||||
|
chart1: {
|
||||||
|
light: 'hsl(12 76% 61%)',
|
||||||
|
dark: 'hsl(12 76% 61%)'
|
||||||
|
},
|
||||||
|
|
||||||
|
chart2: {
|
||||||
|
light: 'hsl(173 58% 39%)',
|
||||||
|
dark: 'hsl(173 58% 39%)'
|
||||||
|
},
|
||||||
|
|
||||||
|
chart3: {
|
||||||
|
light: 'hsl(197 37% 24%)',
|
||||||
|
dark: 'hsl(197 37% 24%)'
|
||||||
|
},
|
||||||
|
|
||||||
|
chart4: {
|
||||||
|
light: 'hsl(43 74% 66%)',
|
||||||
|
dark: 'hsl(43 74% 66%)'
|
||||||
|
},
|
||||||
|
|
||||||
|
chart5: {
|
||||||
|
light: 'hsl(27 87% 67%)',
|
||||||
|
dark: 'hsl(27 87% 67%)'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper function to get color based on theme
|
||||||
|
export const getColor = (colorName: keyof typeof colors, isDark: boolean = false) => {
|
||||||
|
const color = colors[colorName];
|
||||||
|
return isDark ? color.dark : color.light;
|
||||||
|
};
|
||||||
|
|
||||||
|
// CSS helper for theme-aware colors
|
||||||
|
export const bdTheme = (colorNameOrLight: keyof typeof colors | string, dark?: string) => {
|
||||||
|
if (dark) {
|
||||||
|
// Direct color values provided
|
||||||
|
return cssManager.bdTheme(colorNameOrLight as string, dark);
|
||||||
|
}
|
||||||
|
// Color name from palette
|
||||||
|
const colorName = colorNameOrLight as keyof typeof colors;
|
||||||
|
return cssManager.bdTheme(colors[colorName].light, colors[colorName].dark);
|
||||||
|
};
|
||||||
145
ts_web/elements/00fonts.ts
Normal file
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(0 0% 5%)', 'hsl(0 0% 84%)')};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Disabled styles
|
||||||
|
export const disabled = css`
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
pointer-events: none;
|
||||||
|
`;
|
||||||
@@ -1,5 +1,16 @@
|
|||||||
export * from './sio-fab.js';
|
// Core components
|
||||||
|
export * from './sio-icon.js';
|
||||||
|
export * from './sio-button.js';
|
||||||
|
export * from './sio-dropdown-menu.js';
|
||||||
|
|
||||||
|
// Conversation components
|
||||||
|
export * from './sio-conversation-selector.js';
|
||||||
|
export * from './sio-conversation-view.js';
|
||||||
|
export * from './sio-message-input.js';
|
||||||
export * from './sio-combox.js';
|
export * from './sio-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-recorder.js';
|
||||||
|
export * from './sio-image-lightbox.js';
|
||||||
|
export * from './sio-pdf-viewer.js';
|
||||||
|
|||||||
291
ts_web/elements/sio-button.ts
Normal file
291
ts_web/elements/sio-button.ts
Normal file
@@ -0,0 +1,291 @@
|
|||||||
|
import {
|
||||||
|
DeesElement,
|
||||||
|
html,
|
||||||
|
property,
|
||||||
|
customElement,
|
||||||
|
cssManager,
|
||||||
|
css,
|
||||||
|
unsafeCSS,
|
||||||
|
type TemplateResult,
|
||||||
|
} from '@design.estate/dees-element';
|
||||||
|
|
||||||
|
// Import design tokens
|
||||||
|
import { colors, bdTheme } from './00colors.js';
|
||||||
|
import { spacing, radius, shadows, transitions } from './00tokens.js';
|
||||||
|
import { fontFamilies, typography } from './00fonts.js';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'sio-button': SioButton;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@customElement('sio-button')
|
||||||
|
export class SioButton extends DeesElement {
|
||||||
|
public static demo = () => html`
|
||||||
|
<div style="display: flex; gap: 16px; flex-wrap: wrap; align-items: center;">
|
||||||
|
<sio-button>Default</sio-button>
|
||||||
|
<sio-button type="primary">Primary</sio-button>
|
||||||
|
<sio-button type="destructive">Delete</sio-button>
|
||||||
|
<sio-button type="outline">Outline</sio-button>
|
||||||
|
<sio-button type="ghost">Ghost</sio-button>
|
||||||
|
<sio-button size="sm">Small</sio-button>
|
||||||
|
<sio-button size="lg">Large</sio-button>
|
||||||
|
<sio-button disabled>Disabled</sio-button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
@property({ type: String })
|
||||||
|
public accessor text: string = '';
|
||||||
|
|
||||||
|
@property({ type: String })
|
||||||
|
public accessor type: 'default' | 'primary' | 'secondary' | 'destructive' | 'outline' | 'ghost' = 'default';
|
||||||
|
|
||||||
|
@property({ type: String })
|
||||||
|
public accessor size: 'sm' | 'default' | 'lg' = 'default';
|
||||||
|
|
||||||
|
@property({ type: Boolean, reflect: true })
|
||||||
|
public accessor disabled: boolean = false;
|
||||||
|
|
||||||
|
@property({ type: String })
|
||||||
|
public accessor status: 'normal' | 'pending' | 'success' | 'error' = 'normal';
|
||||||
|
|
||||||
|
public static styles = [
|
||||||
|
cssManager.defaultStyles,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
display: inline-block;
|
||||||
|
font-family: ${unsafeCSS(fontFamilies.sans)};
|
||||||
|
}
|
||||||
|
|
||||||
|
:host([disabled]) {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
transition: all 120ms ease;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
outline: none;
|
||||||
|
border: none;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Size variants */
|
||||||
|
.button.size-sm {
|
||||||
|
height: 32px;
|
||||||
|
padding: 0 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.size-default {
|
||||||
|
height: 36px;
|
||||||
|
padding: 0 16px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.size-lg {
|
||||||
|
height: 42px;
|
||||||
|
padding: 0 24px;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Type variants */
|
||||||
|
.button.default {
|
||||||
|
background: ${bdTheme('hsl(0 0% 95%)', 'hsl(0 0% 15%)')};
|
||||||
|
color: ${bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 95%)')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.default:hover:not(.disabled) {
|
||||||
|
background: ${bdTheme('hsl(0 0% 91%)', 'hsl(0 0% 20%)')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.default:active:not(.disabled) {
|
||||||
|
background: ${bdTheme('hsl(0 0% 87%)', 'hsl(0 0% 18%)')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.primary {
|
||||||
|
background: ${bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 95%)')};
|
||||||
|
color: ${bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 0%)')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.primary:hover:not(.disabled) {
|
||||||
|
background: ${bdTheme('hsl(0 0% 25%)', 'hsl(0 0% 100%)')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.primary:active:not(.disabled) {
|
||||||
|
background: ${bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 90%)')};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Secondary variant */
|
||||||
|
.button.secondary {
|
||||||
|
background: transparent;
|
||||||
|
color: ${bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 95%)')};
|
||||||
|
box-shadow: inset 0 0 0 1px ${bdTheme('hsl(0 0% 85%)', 'hsl(0 0% 25%)')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.secondary:hover:not(.disabled) {
|
||||||
|
background: ${bdTheme('hsl(0 0% 96%)', 'hsl(0 0% 15%)')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.secondary:active:not(.disabled) {
|
||||||
|
background: ${bdTheme('hsl(0 0% 92%)', 'hsl(0 0% 12%)')};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Destructive variant */
|
||||||
|
.button.destructive {
|
||||||
|
background: ${bdTheme('hsl(0 100% 95%)', 'hsl(0 50% 20%)')};
|
||||||
|
color: ${bdTheme('hsl(0 100% 45%)', 'hsl(0 100% 75%)')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.destructive:hover:not(.disabled) {
|
||||||
|
background: ${bdTheme('hsl(0 100% 45%)', 'hsl(0 100% 50%)')};
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.destructive:active:not(.disabled) {
|
||||||
|
background: ${bdTheme('hsl(0 100% 40%)', 'hsl(0 100% 45%)')};
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.outline {
|
||||||
|
background: transparent;
|
||||||
|
color: ${bdTheme('hsl(0 0% 40%)', 'hsl(0 0% 70%)')};
|
||||||
|
box-shadow: inset 0 0 0 1.5px ${bdTheme('hsl(0 0% 90%)', 'hsl(0 0% 30%)')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.outline:hover:not(.disabled) {
|
||||||
|
color: ${bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 95%)')};
|
||||||
|
box-shadow: inset 0 0 0 1.5px ${bdTheme('hsl(0 0% 70%)', 'hsl(0 0% 50%)')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.outline:active:not(.disabled) {
|
||||||
|
background: ${bdTheme('hsl(0 0% 95%)', 'hsl(0 0% 15%)')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.ghost {
|
||||||
|
background: transparent;
|
||||||
|
color: ${bdTheme('hsl(0 0% 40%)', 'hsl(0 0% 70%)')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.ghost:hover:not(.disabled) {
|
||||||
|
color: ${bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 95%)')};
|
||||||
|
background: ${bdTheme('hsl(0 0% 0% / 0.05)', 'hsl(0 0% 100% / 0.05)')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.ghost:active:not(.disabled) {
|
||||||
|
background: ${bdTheme('hsl(0 0% 0% / 0.1)', 'hsl(0 0% 100% / 0.1)')};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status states */
|
||||||
|
.button.pending {
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
position: absolute;
|
||||||
|
left: ${unsafeCSS(spacing["3"])};
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
from { transform: rotate(0deg); }
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.success {
|
||||||
|
background: ${bdTheme('success')};
|
||||||
|
color: ${bdTheme('successForeground')};
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.error {
|
||||||
|
background: ${bdTheme('destructive')};
|
||||||
|
color: ${bdTheme('destructiveForeground')};
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Disabled state */
|
||||||
|
.button.disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Focus state */
|
||||||
|
.button:focus-visible {
|
||||||
|
outline: 2px solid ${bdTheme('hsl(0 0% 15% / 0.2)', 'hsl(0 0% 95% / 0.2)')};
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Icon sizing within buttons */
|
||||||
|
.button sio-icon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.size-sm sio-icon {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.size-lg sio-icon {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
|
||||||
|
public render(): TemplateResult {
|
||||||
|
const buttonClasses = [
|
||||||
|
'button',
|
||||||
|
this.type === 'primary' ? 'primary' : this.type,
|
||||||
|
`size-${this.size}`,
|
||||||
|
this.disabled ? 'disabled' : '',
|
||||||
|
this.status,
|
||||||
|
].filter(Boolean).join(' ');
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<button
|
||||||
|
class="${buttonClasses}"
|
||||||
|
?disabled=${this.disabled}
|
||||||
|
@click=${this.handleClick}
|
||||||
|
>
|
||||||
|
${this.status === 'pending' ? html`
|
||||||
|
<sio-icon class="spinner" icon="loader" size="16"></sio-icon>
|
||||||
|
` : ''}
|
||||||
|
${this.status === 'success' ? html`
|
||||||
|
<sio-icon icon="check" size="16"></sio-icon>
|
||||||
|
` : ''}
|
||||||
|
${this.status === 'error' ? html`
|
||||||
|
<sio-icon icon="x" size="16"></sio-icon>
|
||||||
|
` : ''}
|
||||||
|
<slot>${this.text}</slot>
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleClick(event: MouseEvent) {
|
||||||
|
if (this.disabled || this.status !== 'normal') {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Let the native click bubble normally
|
||||||
|
// Don't dispatch a custom event to avoid double-triggering
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,196 +5,348 @@ import {
|
|||||||
customElement,
|
customElement,
|
||||||
type TemplateResult,
|
type TemplateResult,
|
||||||
cssManager,
|
cssManager,
|
||||||
|
css,
|
||||||
|
unsafeCSS,
|
||||||
|
state,
|
||||||
} from '@design.estate/dees-element';
|
} from '@design.estate/dees-element';
|
||||||
import * as domtools from '@design.estate/dees-domtools';
|
import * as domtools from '@design.estate/dees-domtools';
|
||||||
|
|
||||||
import * as deesCatalog from '@design.estate/dees-catalog';
|
// Import design tokens
|
||||||
deesCatalog;
|
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')
|
@customElement('sio-combox')
|
||||||
export class SioCombox extends DeesElement {
|
export class SioCombox extends DeesElement {
|
||||||
public static demo = () => html` <sio-combox></sio-combox> `;
|
public static demo = () => html` <sio-combox></sio-combox> `;
|
||||||
|
|
||||||
@property({ type: Object })
|
@property({ type: Object })
|
||||||
public referenceObject: HTMLElement;
|
public accessor referenceObject: HTMLElement;
|
||||||
|
|
||||||
/**
|
@state()
|
||||||
* computes the button offset
|
private accessor selectedConversationId: string | null = null;
|
||||||
*/
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
public cssComputeInnerScroll() {
|
@state()
|
||||||
console.log(
|
private accessor conversations: IConversation[] = [
|
||||||
`SioCombox clientHeight: ${this.shadowRoot.querySelector('.mainbox').clientHeight}`
|
{
|
||||||
);
|
id: '1',
|
||||||
console.log(
|
title: 'Technical Support',
|
||||||
`SioCombox content scrollheight is: ${
|
lastMessage: 'Thanks for your help with the login issue!',
|
||||||
this.shadowRoot.querySelector('.contentbox').clientHeight
|
time: '2 min ago',
|
||||||
}`
|
unread: true,
|
||||||
);
|
},
|
||||||
if (
|
{
|
||||||
this.shadowRoot.querySelector('.mainbox').clientHeight <
|
id: '2',
|
||||||
this.shadowRoot.querySelector('.contentbox').clientHeight
|
title: 'Billing Question',
|
||||||
) {
|
lastMessage: 'I need help understanding my invoice',
|
||||||
(this.shadowRoot.querySelector('.mainbox') as HTMLElement).style.overflowY = 'scroll';
|
time: '1 hour ago',
|
||||||
} else {
|
},
|
||||||
(this.shadowRoot.querySelector('.mainbox') as HTMLElement).style.overflowY = 'hidden';
|
{
|
||||||
|
id: '3',
|
||||||
|
title: 'Feature Request',
|
||||||
|
lastMessage: 'That would be great! Looking forward to it',
|
||||||
|
time: 'Yesterday',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '4',
|
||||||
|
title: 'General Inquiry',
|
||||||
|
lastMessage: 'Thank you for the information',
|
||||||
|
time: '2 days ago',
|
||||||
}
|
}
|
||||||
}
|
];
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private accessor messages: { [conversationId: string]: IMessage[] } = {
|
||||||
|
'1': [
|
||||||
|
{ id: '1', text: 'Hi, I\'m having trouble logging in', sender: 'user', time: '10:00 AM' },
|
||||||
|
{ id: '2', text: 'I can help you with that. Can you tell me what error you\'re seeing?', sender: 'support', time: '10:02 AM' },
|
||||||
|
{ id: '3', text: 'It says "Invalid credentials" but I\'m sure my password is correct', sender: 'user', time: '10:03 AM' },
|
||||||
|
{ id: '4', text: 'Let me check your account. Please try resetting your password using the forgot password link.', sender: 'support', time: '10:05 AM' },
|
||||||
|
{
|
||||||
|
id: '5',
|
||||||
|
text: 'Here\'s a screenshot of the error',
|
||||||
|
sender: 'user',
|
||||||
|
time: '10:08 AM',
|
||||||
|
attachments: [{
|
||||||
|
id: 'att1',
|
||||||
|
name: 'error-screenshot.png',
|
||||||
|
size: 245780,
|
||||||
|
type: 'image/png',
|
||||||
|
url: 'https://picsum.photos/400/300?random=1'
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{ id: '6', text: 'Thanks for your help with the login issue!', sender: 'user', time: '10:10 AM' },
|
||||||
|
{
|
||||||
|
id: '7',
|
||||||
|
text: 'Here is the documentation you requested',
|
||||||
|
sender: 'support',
|
||||||
|
time: '10:15 AM',
|
||||||
|
attachments: [{
|
||||||
|
id: 'att2',
|
||||||
|
name: 'user-guide.pdf',
|
||||||
|
size: 2457600,
|
||||||
|
type: 'application/pdf',
|
||||||
|
url: 'data:application/pdf;base64,JVBERi0xLjMKJeLjz9MKMSAwIG9iago8PAovVHlwZSAvQ2F0YWxvZwovT3V0bGluZXMgMiAwIFIKL1BhZ2VzIDMgMCBSCj4+CmVuZG9iagoyIDAgb2JqCjw8Ci9UeXBlIC9PdXRsaW5lcwovQ291bnQgMAo+PgplbmRvYmoKMyAwIG9iago8PAovVHlwZSAvUGFnZXMKL0NvdW50IDEKL0tpZHMgWzQgMCBSXQo+PgplbmRvYmoKNCAwIG9iago8PAovVHlwZSAvUGFnZQovUGFyZW50IDMgMCBSCi9NZWRpYUJveCBbMCAwIDYxMiA3OTJdCi9Db250ZW50cyA1IDAgUgovUmVzb3VyY2VzIDw8Ci9Gb250IDw8Ci9GMSA2IDAgUgo+Pgo+Pgo+PgplbmRvYmoKNSAwIG9iago8PAovTGVuZ3RoIDQ0Cj4+CnN0cmVhbQpCVApxCjcwIDUwIFRECi9GMSAxMiBUZgooSGVsbG8gV29ybGQpIFRqCkVUClEKZW5kc3RyZWFtCmVuZG9iago2IDAgb2JqCjw8Ci9UeXBlIC9Gb250Ci9TdWJ0eXBlIC9UeXBlMQovQmFzZUZvbnQgL1RpbWVzLVJvbWFuCj4+CmVuZG9iagp4cmVmCjAgNwowMDAwMDAwMDAwIDY1NTM1IGYgCjAwMDAwMDAwMDkgMDAwMDAgbiAKMDAwMDAwMDA3NCAwMDAwMCBuIAowMDAwMDAwMTIwIDAwMDAwIG4gCjAwMDAwMDAxNzkgMDAwMDAgbiAKMDAwMDAwMDM2NCAwMDAwMCBuIAowMDAwMDAwNDY2IDAwMDAwIG4gCnRyYWlsZXIKPDwKL1NpemUgNwovUm9vdCAxIDAgUgo+PgpzdGFydHhyZWYKNTY1CiUlRU9G'
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'2': [
|
||||||
|
{ id: '1', text: 'I need help understanding my invoice', sender: 'user', time: '9:00 AM' },
|
||||||
|
{ id: '2', text: 'I\'d be happy to help explain your invoice. Which part is unclear?', sender: 'support', time: '9:05 AM' },
|
||||||
|
],
|
||||||
|
'3': [
|
||||||
|
{ id: '1', text: 'I\'d love to see dark mode support in your app!', sender: 'user', time: 'Yesterday' },
|
||||||
|
{ id: '2', text: 'Thanks for the suggestion! We\'re actually working on dark mode and it should be available next month.', sender: 'support', time: 'Yesterday' },
|
||||||
|
{ id: '3', text: 'That would be great! Looking forward to it', sender: 'user', time: 'Yesterday' },
|
||||||
|
],
|
||||||
|
'4': [
|
||||||
|
{ id: '1', text: 'Can you tell me more about your enterprise plans?', sender: 'user', time: '2 days ago' },
|
||||||
|
{ id: '2', text: 'Of course! Our enterprise plans include advanced features like SSO, dedicated support, and custom integrations.', sender: 'support', time: '2 days ago' },
|
||||||
|
{ id: '3', text: 'Thank you for the information', sender: 'user', time: '2 days ago' },
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
domtools.DomTools.setupDomTools();
|
domtools.DomTools.setupDomTools();
|
||||||
}
|
}
|
||||||
|
|
||||||
public render(): TemplateResult {
|
public static styles = [
|
||||||
return html`
|
cssManager.defaultStyles,
|
||||||
${domtools.elementBasic.styles}
|
css`
|
||||||
<style>
|
: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 {
|
:host {
|
||||||
overflow: hidden;
|
width: 100%;
|
||||||
font-family: 'Dees Sans';
|
|
||||||
position: absolute;
|
|
||||||
display: block;
|
|
||||||
height: ${this.cssComputeHeight()}px;
|
|
||||||
width: 375px;
|
|
||||||
background: ${this.goBright ? '#eeeeee' : '#000000'};
|
|
||||||
border-radius: 16px;
|
|
||||||
border: 1px solid rgba(250, 250, 250, 0.2);
|
|
||||||
right: 0px;
|
|
||||||
z-index: 10000;
|
|
||||||
box-shadow: 0px 0px 5px ${this.goBright ? 'rgba(0, 0, 0, 0.3)' : 'rgba(0, 0, 0, 0.5)'};
|
|
||||||
color: ${this.goBright ? '#333' : '#ccc'};
|
|
||||||
cursor: default;
|
|
||||||
user-select: none;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mainbox {
|
|
||||||
position: absolute;
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
border-radius: 0;
|
||||||
overflow: hidden;
|
|
||||||
overscroll-behavior: contain;
|
|
||||||
padding-bottom: 80px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.toppanel {
|
.container {
|
||||||
height: 200px;
|
position: relative;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#greeting {
|
sio-conversation-selector {
|
||||||
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 {
|
|
||||||
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, ${cssManager.bdTheme('#eeeeeb00', 'rgba(0, 0, 0, 0)')} 0%, ${cssManager.bdTheme('#eeeeebff', 'rgba(0, 0, 0, 1)')} 50%);
|
|
||||||
padding-top: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quicktabs .quicktab {
|
|
||||||
text-align: center;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.quicktabs .quicktab .quicktabicon {
|
|
||||||
font-size: 20px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quicktabs .quicktab .quicktabtext {
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.brandingbox {
|
|
||||||
z-index: 101;
|
|
||||||
text-align: center;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
bottom: 0px;
|
height: 100%;
|
||||||
left: 0px;
|
transition: left 300ms ease, opacity 200ms ease;
|
||||||
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">
|
sio-conversation-view {
|
||||||
<div class="contentbox">
|
position: absolute;
|
||||||
<div class="toppanel">
|
width: 100%;
|
||||||
<div id="greeting">Hello :)</div>
|
height: 100%;
|
||||||
<div id="callToAction">Ask us anything or share your feedback!</div>
|
transition: left 300ms ease, opacity 200ms ease;
|
||||||
</div>
|
}
|
||||||
<sio-subwidget-conversations></sio-subwidget-conversations>
|
|
||||||
<sio-subwidget-onboardme></sio-subwidget-onboardme>
|
/* Mobile navigation states */
|
||||||
</div>
|
.container.show-list sio-conversation-selector {
|
||||||
|
left: 0;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container.show-list sio-conversation-view {
|
||||||
|
left: 100%;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container.show-conversation sio-conversation-selector {
|
||||||
|
left: -100%;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container.show-conversation sio-conversation-view {
|
||||||
|
left: 0;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 601px) {
|
||||||
|
sio-conversation-selector {
|
||||||
|
width: 320px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
sio-conversation-view {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
|
||||||
|
public render(): TemplateResult {
|
||||||
|
const selectedConversation = this.selectedConversationId
|
||||||
|
? this.conversations.find(c => c.id === this.selectedConversationId)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const conversationData: IConversationData | null = selectedConversation
|
||||||
|
? {
|
||||||
|
id: selectedConversation.id,
|
||||||
|
title: selectedConversation.title,
|
||||||
|
messages: this.messages[selectedConversation.id] || []
|
||||||
|
}
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const containerClass = this.selectedConversationId ? 'show-conversation' : 'show-list';
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="container ${containerClass}">
|
||||||
|
<sio-conversation-selector
|
||||||
|
.conversations=${this.conversations}
|
||||||
|
.selectedConversationId=${this.selectedConversationId}
|
||||||
|
@conversation-selected=${this.handleConversationSelected}
|
||||||
|
></sio-conversation-selector>
|
||||||
|
|
||||||
|
<sio-conversation-view
|
||||||
|
.conversation=${conversationData}
|
||||||
|
@back=${this.handleBack}
|
||||||
|
@send-message=${this.handleSendMessage}
|
||||||
|
@open-image=${this.handleOpenImage}
|
||||||
|
@open-file=${this.handleOpenImage}
|
||||||
|
></sio-conversation-view>
|
||||||
</div>
|
</div>
|
||||||
<div class="quicktabs">
|
|
||||||
<div class="quicktab">
|
<sio-image-lightbox></sio-image-lightbox>
|
||||||
<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>
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async updated() {
|
private handleConversationSelected(event: CustomEvent) {
|
||||||
this.cssComputeHeight();
|
const conversation = event.detail.conversation as IConversation;
|
||||||
window.requestAnimationFrame(() => {
|
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(() => {
|
setTimeout(() => {
|
||||||
this.cssComputeInnerScroll();
|
const responseMessage: IMessage = {
|
||||||
}, 200);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
349
ts_web/elements/sio-conversation-selector.ts
Normal file
349
ts_web/elements/sio-conversation-selector.ts
Normal file
@@ -0,0 +1,349 @@
|
|||||||
|
import {
|
||||||
|
DeesElement,
|
||||||
|
property,
|
||||||
|
html,
|
||||||
|
customElement,
|
||||||
|
type TemplateResult,
|
||||||
|
cssManager,
|
||||||
|
css,
|
||||||
|
unsafeCSS,
|
||||||
|
state,
|
||||||
|
} from '@design.estate/dees-element';
|
||||||
|
|
||||||
|
// Import design tokens
|
||||||
|
import { colors, bdTheme } from './00colors.js';
|
||||||
|
import { spacing, radius, shadows, transitions } from './00tokens.js';
|
||||||
|
import { fontFamilies, typography } from './00fonts.js';
|
||||||
|
|
||||||
|
// Types
|
||||||
|
export interface IConversation {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
lastMessage: string;
|
||||||
|
time: string;
|
||||||
|
unread?: boolean;
|
||||||
|
avatar?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'sio-conversation-selector': SioConversationSelector;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@customElement('sio-conversation-selector')
|
||||||
|
export class SioConversationSelector extends DeesElement {
|
||||||
|
public static demo = () => html`
|
||||||
|
<sio-conversation-selector style="width: 320px; height: 600px;"></sio-conversation-selector>
|
||||||
|
`;
|
||||||
|
|
||||||
|
@property({ type: Array })
|
||||||
|
public accessor conversations: IConversation[] = [];
|
||||||
|
|
||||||
|
@property({ type: String })
|
||||||
|
public accessor selectedConversationId: string | null = null;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private accessor searchQuery: string = '';
|
||||||
|
|
||||||
|
public static styles = [
|
||||||
|
cssManager.defaultStyles,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
background: ${bdTheme('card')};
|
||||||
|
border-right: 1px solid ${bdTheme('border')};
|
||||||
|
font-family: ${unsafeCSS(fontFamilies.sans)};
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
padding: ${unsafeCSS(spacing["5"])} ${unsafeCSS(spacing["4"])};
|
||||||
|
border-bottom: 1px solid ${bdTheme('border')};
|
||||||
|
background: ${bdTheme('background')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-top {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: ${unsafeCSS(spacing["4"])};
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
line-height: 1.2;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0;
|
||||||
|
color: ${bdTheme('foreground')};
|
||||||
|
letter-spacing: -0.025em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-box {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: ${unsafeCSS(spacing["2.5"])} ${unsafeCSS(spacing["10"])} ${unsafeCSS(spacing["2.5"])} ${unsafeCSS(spacing["3"])};
|
||||||
|
background: ${bdTheme('background')};
|
||||||
|
border: 1px solid ${bdTheme('border')};
|
||||||
|
border-radius: ${unsafeCSS(radius.lg)};
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: ${bdTheme('foreground')};
|
||||||
|
outline: none;
|
||||||
|
transition: ${unsafeCSS(transitions.all)};
|
||||||
|
font-family: ${unsafeCSS(fontFamilies.sans)};
|
||||||
|
box-shadow: ${unsafeCSS(shadows.sm)};
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input::placeholder {
|
||||||
|
color: ${bdTheme('mutedForeground')};
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input:focus {
|
||||||
|
border-color: ${bdTheme('ring')};
|
||||||
|
box-shadow: 0 0 0 3px ${bdTheme('ring')}20;
|
||||||
|
background: ${bdTheme('background')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-icon {
|
||||||
|
position: absolute;
|
||||||
|
right: ${unsafeCSS(spacing["3"])};
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
color: ${bdTheme('mutedForeground')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.conversation-list {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: ${unsafeCSS(spacing["2"])};
|
||||||
|
}
|
||||||
|
|
||||||
|
.conversation-item {
|
||||||
|
padding: ${unsafeCSS(spacing["3.5"])};
|
||||||
|
margin-bottom: ${unsafeCSS(spacing["1.5"])};
|
||||||
|
background: ${bdTheme('background')};
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: ${unsafeCSS(radius.lg)};
|
||||||
|
cursor: pointer;
|
||||||
|
transition: ${unsafeCSS(transitions.all)};
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conversation-item:hover {
|
||||||
|
background: ${bdTheme('accent')};
|
||||||
|
transform: translateX(2px);
|
||||||
|
box-shadow: ${unsafeCSS(shadows.sm)};
|
||||||
|
}
|
||||||
|
|
||||||
|
.conversation-item.selected {
|
||||||
|
background: ${bdTheme('accent')};
|
||||||
|
border-color: ${bdTheme('border')};
|
||||||
|
box-shadow: ${unsafeCSS(shadows.sm)};
|
||||||
|
}
|
||||||
|
|
||||||
|
.conversation-item.selected::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 3px;
|
||||||
|
height: 60%;
|
||||||
|
background: ${bdTheme('primary')};
|
||||||
|
border-radius: 0 3px 3px 0;
|
||||||
|
animation: slideIn 200ms ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideIn {
|
||||||
|
from {
|
||||||
|
width: 0;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
width: 3px;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.conversation-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: ${unsafeCSS(spacing["1"])};
|
||||||
|
}
|
||||||
|
|
||||||
|
.conversation-title {
|
||||||
|
font-weight: 500;
|
||||||
|
color: ${bdTheme('foreground')};
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: ${unsafeCSS(spacing["2"])};
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conversation-time {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: ${bdTheme('mutedForeground')};
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conversation-preview {
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: ${bdTheme('mutedForeground')};
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
margin-top: ${unsafeCSS(spacing["0.5"])};
|
||||||
|
}
|
||||||
|
|
||||||
|
.unread-dot {
|
||||||
|
display: inline-block;
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
background: ${bdTheme('primary')};
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: pulse 2s ease-in-out infinite;
|
||||||
|
box-shadow: 0 0 0 0 ${bdTheme('primary')};
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
box-shadow: 0 0 0 0 ${bdTheme('primary')}40;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.9;
|
||||||
|
transform: scale(1.05);
|
||||||
|
box-shadow: 0 0 0 4px ${bdTheme('primary')}00;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
box-shadow: 0 0 0 0 ${bdTheme('primary')}00;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
padding: ${unsafeCSS(spacing["4"])};
|
||||||
|
text-align: center;
|
||||||
|
color: ${bdTheme('mutedForeground')};
|
||||||
|
gap: ${unsafeCSS(spacing["3"])};
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-icon {
|
||||||
|
font-size: 48px;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollbar styling */
|
||||||
|
.conversation-list::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conversation-list::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conversation-list::-webkit-scrollbar-thumb {
|
||||||
|
background: ${bdTheme('border')};
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conversation-list::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: ${bdTheme('mutedForeground')};
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
|
||||||
|
public render(): TemplateResult {
|
||||||
|
const filteredConversations = this.conversations.filter(conv =>
|
||||||
|
conv.title.toLowerCase().includes(this.searchQuery.toLowerCase()) ||
|
||||||
|
conv.lastMessage.toLowerCase().includes(this.searchQuery.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="header">
|
||||||
|
<div class="header-top">
|
||||||
|
<h2 class="title">Messages</h2>
|
||||||
|
<sio-button
|
||||||
|
type="primary"
|
||||||
|
size="sm"
|
||||||
|
@click=${() => this.startNewConversation()}
|
||||||
|
>
|
||||||
|
<sio-icon icon="plus" size="16"></sio-icon>
|
||||||
|
New
|
||||||
|
</sio-button>
|
||||||
|
</div>
|
||||||
|
<div class="search-box">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="search-input"
|
||||||
|
placeholder="Search conversations..."
|
||||||
|
.value=${this.searchQuery}
|
||||||
|
@input=${(e: Event) => this.searchQuery = (e.target as HTMLInputElement).value}
|
||||||
|
/>
|
||||||
|
<sio-icon class="search-icon" icon="search" size="16"></sio-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${filteredConversations.length > 0 ? html`
|
||||||
|
<div class="conversation-list">
|
||||||
|
${filteredConversations.map(conv => html`
|
||||||
|
<div
|
||||||
|
class="conversation-item ${this.selectedConversationId === conv.id ? 'selected' : ''}"
|
||||||
|
@click=${() => this.selectConversation(conv)}
|
||||||
|
>
|
||||||
|
<div class="conversation-header">
|
||||||
|
<span class="conversation-title">
|
||||||
|
${conv.title}
|
||||||
|
${conv.unread ? html`<span class="unread-dot"></span>` : ''}
|
||||||
|
</span>
|
||||||
|
<span class="conversation-time">${conv.time}</span>
|
||||||
|
</div>
|
||||||
|
<div class="conversation-preview">${conv.lastMessage}</div>
|
||||||
|
</div>
|
||||||
|
`)}
|
||||||
|
</div>
|
||||||
|
` : html`
|
||||||
|
<div class="empty-state">
|
||||||
|
<sio-icon class="empty-icon" icon="message-square"></sio-icon>
|
||||||
|
<h3>${this.searchQuery ? 'No matching conversations' : 'No conversations yet'}</h3>
|
||||||
|
<p>${this.searchQuery ? 'Try a different search term' : 'Start a new conversation to get started'}</p>
|
||||||
|
</div>
|
||||||
|
`}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private selectConversation(conversation: IConversation) {
|
||||||
|
this.selectedConversationId = conversation.id;
|
||||||
|
|
||||||
|
// Dispatch event for parent components
|
||||||
|
this.dispatchEvent(new CustomEvent('conversation-selected', {
|
||||||
|
detail: { conversation },
|
||||||
|
bubbles: true,
|
||||||
|
composed: true
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
private startNewConversation() {
|
||||||
|
// Dispatch event for parent components to handle new conversation creation
|
||||||
|
this.dispatchEvent(new CustomEvent('new-conversation', {
|
||||||
|
bubbles: true,
|
||||||
|
composed: true
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
808
ts_web/elements/sio-conversation-view.ts
Normal file
808
ts_web/elements/sio-conversation-view.ts
Normal file
@@ -0,0 +1,808 @@
|
|||||||
|
import {
|
||||||
|
DeesElement,
|
||||||
|
property,
|
||||||
|
html,
|
||||||
|
customElement,
|
||||||
|
type TemplateResult,
|
||||||
|
cssManager,
|
||||||
|
css,
|
||||||
|
unsafeCSS,
|
||||||
|
state,
|
||||||
|
} from '@design.estate/dees-element';
|
||||||
|
|
||||||
|
// Import design tokens
|
||||||
|
import { colors, bdTheme } from './00colors.js';
|
||||||
|
import { spacing, radius, shadows, transitions } from './00tokens.js';
|
||||||
|
import { fontFamilies, typography } from './00fonts.js';
|
||||||
|
import { SioDropdownMenu, type IDropdownMenuItem } from './sio-dropdown-menu.js';
|
||||||
|
import { SioMessageInput } from './sio-message-input.js';
|
||||||
|
|
||||||
|
// Make sure components are loaded
|
||||||
|
SioDropdownMenu;
|
||||||
|
SioMessageInput;
|
||||||
|
|
||||||
|
// Types
|
||||||
|
export interface IAttachment {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
size: number;
|
||||||
|
type: string;
|
||||||
|
url: string;
|
||||||
|
thumbnailUrl?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IMessage {
|
||||||
|
id: string;
|
||||||
|
text: string;
|
||||||
|
sender: 'user' | 'support';
|
||||||
|
time: string;
|
||||||
|
status?: 'sending' | 'sent' | 'delivered' | 'read';
|
||||||
|
attachments?: IAttachment[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IConversationData {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
messages: IMessage[];
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'sio-conversation-view': SioConversationView;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@customElement('sio-conversation-view')
|
||||||
|
export class SioConversationView extends DeesElement {
|
||||||
|
public static demo = () => html`
|
||||||
|
<sio-conversation-view style="width: 600px; height: 600px;"></sio-conversation-view>
|
||||||
|
`;
|
||||||
|
|
||||||
|
@property({ type: Object })
|
||||||
|
public accessor conversation: IConversationData | null = null;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private accessor isTyping: boolean = false;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private accessor isDragging: boolean = false;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private accessor uploadingFiles: Map<string, { file: File; progress: number }> = new Map();
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private accessor pendingAttachments: IAttachment[] = [];
|
||||||
|
|
||||||
|
private dropdownMenuItems: IDropdownMenuItem[] = [
|
||||||
|
{ id: 'mute', label: 'Mute notifications', icon: 'bell-off' },
|
||||||
|
{ id: 'pin', label: 'Pin conversation', icon: 'pin' },
|
||||||
|
{ id: 'search', label: 'Search in chat', icon: 'search' },
|
||||||
|
{ id: 'divider1', label: '', divider: true },
|
||||||
|
{ id: 'export', label: 'Export chat', icon: 'download' },
|
||||||
|
{ id: 'archive', label: 'Archive conversation', icon: 'archive' },
|
||||||
|
{ id: 'divider2', label: '', divider: true },
|
||||||
|
{ id: 'clear', label: 'Clear history', icon: 'trash-2', destructive: true }
|
||||||
|
];
|
||||||
|
|
||||||
|
public static styles = [
|
||||||
|
cssManager.defaultStyles,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
background: ${bdTheme('background')};
|
||||||
|
font-family: ${unsafeCSS(fontFamilies.sans)};
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
padding: ${unsafeCSS(spacing["4"])};
|
||||||
|
border-bottom: 1px solid ${bdTheme('border')};
|
||||||
|
background: ${bdTheme('background')};
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: ${unsafeCSS(spacing["3"])};
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
-webkit-backdrop-filter: blur(10px);
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 10;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.back-button {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-title {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0;
|
||||||
|
color: ${bdTheme('foreground')};
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: ${unsafeCSS(spacing["2"])};
|
||||||
|
position: relative;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messages-container {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: ${unsafeCSS(spacing["4"])};
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: ${unsafeCSS(spacing["3"])};
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: ${unsafeCSS(spacing["3"])};
|
||||||
|
max-width: 70%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.user {
|
||||||
|
align-self: flex-end;
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-bubble {
|
||||||
|
padding: ${unsafeCSS(spacing["2.5"])} ${unsafeCSS(spacing["3.5"])};
|
||||||
|
border-radius: ${unsafeCSS(radius["2xl"])};
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
position: relative;
|
||||||
|
box-shadow: ${unsafeCSS(shadows.sm)};
|
||||||
|
max-width: 100%;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.support .message-bubble {
|
||||||
|
background: ${bdTheme('secondary')};
|
||||||
|
color: ${bdTheme('secondaryForeground')};
|
||||||
|
border-bottom-left-radius: ${unsafeCSS(spacing["1"])};
|
||||||
|
border: 1px solid ${bdTheme('border')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.user .message-bubble {
|
||||||
|
background: ${bdTheme('primary')};
|
||||||
|
color: ${bdTheme('primaryForeground')};
|
||||||
|
border-bottom-right-radius: ${unsafeCSS(spacing["1"])};
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-time {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: ${bdTheme('mutedForeground')};
|
||||||
|
margin-top: ${unsafeCSS(spacing["1"])};
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.user .message-time {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typing-indicator {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: ${unsafeCSS(spacing["1"])};
|
||||||
|
padding: ${unsafeCSS(spacing["2"])} ${unsafeCSS(spacing["3"])};
|
||||||
|
background: ${bdTheme('muted')};
|
||||||
|
border-radius: ${unsafeCSS(radius.lg)};
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typing-dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
background: ${bdTheme('mutedForeground')};
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: typing 1.4s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typing-dot:nth-child(2) {
|
||||||
|
animation-delay: 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typing-dot:nth-child(3) {
|
||||||
|
animation-delay: 0.4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes typing {
|
||||||
|
0%, 60%, 100% {
|
||||||
|
opacity: 0.3;
|
||||||
|
transform: scale(0.8);
|
||||||
|
}
|
||||||
|
30% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-container {
|
||||||
|
padding: ${unsafeCSS(spacing["4"])};
|
||||||
|
border-top: 1px solid ${bdTheme('border')};
|
||||||
|
background: ${bdTheme('background')};
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
-webkit-backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: ${unsafeCSS(spacing["4"])};
|
||||||
|
padding: ${unsafeCSS(spacing["8"])};
|
||||||
|
text-align: center;
|
||||||
|
color: ${bdTheme('mutedForeground')};
|
||||||
|
animation: fadeIn 500ms ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-icon {
|
||||||
|
font-size: 64px;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollbar styling */
|
||||||
|
.messages-container::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messages-container::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messages-container::-webkit-scrollbar-thumb {
|
||||||
|
background: ${bdTheme('border')};
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messages-container::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: ${bdTheme('mutedForeground')};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* File drop zone */
|
||||||
|
.messages-container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop-overlay {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: ${bdTheme('background')}95;
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
-webkit-backdrop-filter: blur(8px);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 100;
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 200ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop-overlay.active {
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop-zone {
|
||||||
|
padding: ${unsafeCSS(spacing["8"])};
|
||||||
|
border: 2px dashed ${bdTheme('border')};
|
||||||
|
border-radius: ${unsafeCSS(radius.xl)};
|
||||||
|
background: ${bdTheme('card')};
|
||||||
|
text-align: center;
|
||||||
|
transition: ${unsafeCSS(transitions.all)};
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop-overlay.active .drop-zone {
|
||||||
|
border-color: ${bdTheme('primary')};
|
||||||
|
background: ${bdTheme('accent')};
|
||||||
|
transform: scale(1.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop-icon {
|
||||||
|
font-size: 48px;
|
||||||
|
color: ${bdTheme('primary')};
|
||||||
|
margin-bottom: ${unsafeCSS(spacing["4"])};
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop-text {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: ${bdTheme('foreground')};
|
||||||
|
margin-bottom: ${unsafeCSS(spacing["2"])};
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop-hint {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: ${bdTheme('mutedForeground')};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* File attachments */
|
||||||
|
.message-attachments {
|
||||||
|
margin-top: ${unsafeCSS(spacing["2"])};
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: ${unsafeCSS(spacing["2"])};
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-image {
|
||||||
|
max-width: 200px;
|
||||||
|
max-height: 200px;
|
||||||
|
border-radius: ${unsafeCSS(radius.lg)};
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: ${unsafeCSS(transitions.all)};
|
||||||
|
box-shadow: ${unsafeCSS(shadows.sm)};
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-image:hover {
|
||||||
|
transform: scale(1.02);
|
||||||
|
box-shadow: ${unsafeCSS(shadows.md)};
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-image img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-file {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: ${unsafeCSS(spacing["2"])};
|
||||||
|
padding: ${unsafeCSS(spacing["2"])} ${unsafeCSS(spacing["3"])};
|
||||||
|
background: ${bdTheme('secondary')};
|
||||||
|
border: 1px solid ${bdTheme('border')};
|
||||||
|
border-radius: ${unsafeCSS(radius.md)};
|
||||||
|
font-size: 0.875rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: ${unsafeCSS(transitions.all)};
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-file:hover {
|
||||||
|
background: ${bdTheme('accent')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-name {
|
||||||
|
font-weight: 500;
|
||||||
|
color: ${bdTheme('foreground')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-size {
|
||||||
|
color: ${bdTheme('mutedForeground')};
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pending attachments */
|
||||||
|
.pending-attachments {
|
||||||
|
padding: ${unsafeCSS(spacing["2"])} ${unsafeCSS(spacing["3"])};
|
||||||
|
background: ${bdTheme('secondary')};
|
||||||
|
border: 1px solid ${bdTheme('border')};
|
||||||
|
border-radius: ${unsafeCSS(radius.lg)};
|
||||||
|
margin-bottom: ${unsafeCSS(spacing["2"])};
|
||||||
|
}
|
||||||
|
|
||||||
|
.pending-attachment {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: ${unsafeCSS(spacing["2"])};
|
||||||
|
padding: ${unsafeCSS(spacing["1"])} 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pending-attachment-info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pending-attachment-name {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: ${bdTheme('foreground')};
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pending-attachment-size {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: ${bdTheme('mutedForeground')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-attachment {
|
||||||
|
padding: ${unsafeCSS(spacing["1"])};
|
||||||
|
cursor: pointer;
|
||||||
|
color: ${bdTheme('mutedForeground')};
|
||||||
|
transition: ${unsafeCSS(transitions.all)};
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-attachment:hover {
|
||||||
|
color: ${bdTheme('destructive')};
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
|
||||||
|
public render(): TemplateResult {
|
||||||
|
if (!this.conversation) {
|
||||||
|
return html`
|
||||||
|
<div class="empty-state">
|
||||||
|
<sio-icon class="empty-icon" icon="message-square"></sio-icon>
|
||||||
|
<h3>Select a conversation</h3>
|
||||||
|
<p>Choose a conversation from the sidebar to start messaging</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="header">
|
||||||
|
<sio-button
|
||||||
|
class="back-button"
|
||||||
|
type="ghost"
|
||||||
|
size="sm"
|
||||||
|
@click=${this.handleBack}
|
||||||
|
>
|
||||||
|
<sio-icon icon="arrow-left" size="16"></sio-icon>
|
||||||
|
</sio-button>
|
||||||
|
<h3 class="header-title">${this.conversation.title}</h3>
|
||||||
|
<div class="header-actions">
|
||||||
|
<sio-button type="ghost" size="sm">
|
||||||
|
<sio-icon icon="phone" size="16"></sio-icon>
|
||||||
|
</sio-button>
|
||||||
|
<sio-dropdown-menu
|
||||||
|
.items=${this.dropdownMenuItems}
|
||||||
|
@item-selected=${this.handleDropdownAction}
|
||||||
|
>
|
||||||
|
<sio-button type="ghost" size="sm">
|
||||||
|
<sio-icon icon="more-vertical" size="16"></sio-icon>
|
||||||
|
</sio-button>
|
||||||
|
</sio-dropdown-menu>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="messages-container" id="messages"
|
||||||
|
@dragover=${this.handleDragOver}
|
||||||
|
@dragleave=${this.handleDragLeave}
|
||||||
|
@drop=${this.handleDrop}
|
||||||
|
@dragenter=${this.handleDragOver}
|
||||||
|
>
|
||||||
|
<div class="drop-overlay ${this.isDragging ? 'active' : ''}"
|
||||||
|
@drop=${this.handleDrop}
|
||||||
|
@dragover=${(e: DragEvent) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
<div class="drop-zone">
|
||||||
|
<sio-icon class="drop-icon" icon="upload-cloud"></sio-icon>
|
||||||
|
<div class="drop-text">Drop files here</div>
|
||||||
|
<div class="drop-hint">Images and documents up to 10MB</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
${this.conversation.messages.map((msg, index) => html`
|
||||||
|
<div class="message ${msg.sender}" style="animation-delay: ${index * 50}ms">
|
||||||
|
<div class="message-content">
|
||||||
|
<div class="message-bubble">
|
||||||
|
${msg.text}
|
||||||
|
</div>
|
||||||
|
${msg.attachments && msg.attachments.length > 0 ? html`
|
||||||
|
<div class="message-attachments">
|
||||||
|
${msg.attachments.map(attachment =>
|
||||||
|
this.isImage(attachment.type) ? html`
|
||||||
|
<div class="attachment-image" @click=${() => this.openImage(attachment)}>
|
||||||
|
<img src="${attachment.url}" alt="${attachment.name}" />
|
||||||
|
</div>
|
||||||
|
` : attachment.type?.includes('pdf') || attachment.name?.toLowerCase().endsWith('.pdf') ? html`
|
||||||
|
<div class="attachment-file" @click=${() => this.openImage(attachment)}>
|
||||||
|
<sio-icon icon="file-text" size="16"></sio-icon>
|
||||||
|
<div>
|
||||||
|
<div class="attachment-name">${attachment.name}</div>
|
||||||
|
<div class="attachment-size">${this.formatFileSize(attachment.size)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
` : html`
|
||||||
|
<div class="attachment-file" @click=${() => this.downloadFile(attachment)}>
|
||||||
|
<sio-icon icon="file" size="16"></sio-icon>
|
||||||
|
<div>
|
||||||
|
<div class="attachment-name">${attachment.name}</div>
|
||||||
|
<div class="attachment-size">${this.formatFileSize(attachment.size)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
<div class="message-time">${msg.time}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`)}
|
||||||
|
|
||||||
|
${this.isTyping ? html`
|
||||||
|
<div class="message support">
|
||||||
|
<div class="typing-indicator">
|
||||||
|
<div class="typing-dot"></div>
|
||||||
|
<div class="typing-dot"></div>
|
||||||
|
<div class="typing-dot"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-container">
|
||||||
|
${this.pendingAttachments.length > 0 ? html`
|
||||||
|
<div class="pending-attachments">
|
||||||
|
${this.pendingAttachments.map(attachment => html`
|
||||||
|
<div class="pending-attachment">
|
||||||
|
<sio-icon icon="${this.getFileIcon(attachment.type)}" size="16"></sio-icon>
|
||||||
|
<div class="pending-attachment-info">
|
||||||
|
<div class="pending-attachment-name">${attachment.name}</div>
|
||||||
|
<div class="pending-attachment-size">${this.formatFileSize(attachment.size)}</div>
|
||||||
|
</div>
|
||||||
|
<div class="remove-attachment" @click=${() => this.removeAttachment(attachment.id)}>
|
||||||
|
<sio-icon icon="x" size="16"></sio-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`)}
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
<sio-message-input
|
||||||
|
@send-message=${this.handleMessageSend}
|
||||||
|
@files-selected=${this.handleFilesSelected}
|
||||||
|
></sio-message-input>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleBack() {
|
||||||
|
this.dispatchEvent(new CustomEvent('back', {
|
||||||
|
bubbles: true,
|
||||||
|
composed: true
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleMessageSend(event: CustomEvent) {
|
||||||
|
const { text, attachments } = event.detail;
|
||||||
|
|
||||||
|
if (!text.trim() && attachments.length === 0) return;
|
||||||
|
|
||||||
|
const message: IMessage = {
|
||||||
|
id: Date.now().toString(),
|
||||||
|
text: text.trim(),
|
||||||
|
sender: 'user',
|
||||||
|
time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
|
||||||
|
status: 'sending',
|
||||||
|
attachments: [...this.pendingAttachments]
|
||||||
|
};
|
||||||
|
|
||||||
|
// Dispatch event for parent to handle
|
||||||
|
this.dispatchEvent(new CustomEvent('send-message', {
|
||||||
|
detail: { message },
|
||||||
|
bubbles: true,
|
||||||
|
composed: true
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Clear pending attachments
|
||||||
|
this.pendingAttachments = [];
|
||||||
|
|
||||||
|
// Simulate typing indicator (remove in production)
|
||||||
|
setTimeout(() => {
|
||||||
|
this.isTyping = true;
|
||||||
|
setTimeout(() => {
|
||||||
|
this.isTyping = false;
|
||||||
|
}, 2000);
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleFilesSelected(event: CustomEvent) {
|
||||||
|
const { files } = event.detail;
|
||||||
|
// Handle files if needed
|
||||||
|
// For now, we're handling attachments separately in the parent component
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public updated() {
|
||||||
|
// Scroll to bottom when new messages arrive
|
||||||
|
const container = this.shadowRoot?.querySelector('#messages');
|
||||||
|
if (container) {
|
||||||
|
container.scrollTop = container.scrollHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// File handling methods
|
||||||
|
private handleDragOver(e: DragEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
this.isDragging = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleDragLeave(e: DragEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
// Check if we're actually leaving the messages container
|
||||||
|
const relatedTarget = e.relatedTarget as Node;
|
||||||
|
const container = e.currentTarget as HTMLElement;
|
||||||
|
|
||||||
|
if (!container.contains(relatedTarget)) {
|
||||||
|
this.isDragging = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleDrop(e: DragEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
this.isDragging = false;
|
||||||
|
|
||||||
|
const files = Array.from(e.dataTransfer?.files || []);
|
||||||
|
if (files.length > 0) {
|
||||||
|
this.processFiles(files);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private openFileSelector() {
|
||||||
|
const fileInput = this.shadowRoot?.querySelector('#fileInput') as HTMLInputElement;
|
||||||
|
if (fileInput) {
|
||||||
|
fileInput.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleFileSelect(e: Event) {
|
||||||
|
const input = e.target as HTMLInputElement;
|
||||||
|
const files = Array.from(input.files || []);
|
||||||
|
this.processFiles(files);
|
||||||
|
input.value = ''; // Clear input for re-selection
|
||||||
|
}
|
||||||
|
|
||||||
|
private async processFiles(files: File[]) {
|
||||||
|
const maxSize = 10 * 1024 * 1024; // 10MB
|
||||||
|
const validFiles = files.filter(file => {
|
||||||
|
if (file.size > maxSize) {
|
||||||
|
console.warn(`File ${file.name} exceeds 10MB limit`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const file of validFiles) {
|
||||||
|
const id = `${Date.now()}-${Math.random()}`;
|
||||||
|
const url = await this.fileToDataUrl(file);
|
||||||
|
|
||||||
|
const attachment: IAttachment = {
|
||||||
|
id,
|
||||||
|
name: file.name,
|
||||||
|
size: file.size,
|
||||||
|
type: file.type,
|
||||||
|
url,
|
||||||
|
thumbnailUrl: this.isImage(file.type) ? url : undefined
|
||||||
|
};
|
||||||
|
|
||||||
|
this.pendingAttachments = [...this.pendingAttachments, attachment];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fileToDataUrl(file: File): Promise<string> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = () => resolve(reader.result as string);
|
||||||
|
reader.onerror = reject;
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private removeAttachment(id: string) {
|
||||||
|
this.pendingAttachments = this.pendingAttachments.filter(a => a.id !== id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private isImage(type: string): boolean {
|
||||||
|
return type.startsWith('image/');
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatFileSize(bytes: number): string {
|
||||||
|
if (bytes === 0) return '0 Bytes';
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
private getFileIcon(type: string): string {
|
||||||
|
if (this.isImage(type)) return 'image';
|
||||||
|
if (type.includes('pdf')) return 'file-text';
|
||||||
|
if (type.includes('doc')) return 'file-text';
|
||||||
|
if (type.includes('sheet') || type.includes('excel')) return 'table';
|
||||||
|
return 'file';
|
||||||
|
}
|
||||||
|
|
||||||
|
private openImage(attachment: IAttachment) {
|
||||||
|
// Check if it's actually a PDF
|
||||||
|
if (attachment.type?.includes('pdf') || attachment.name?.toLowerCase().endsWith('.pdf')) {
|
||||||
|
this.dispatchEvent(new CustomEvent('open-file', {
|
||||||
|
detail: { attachment },
|
||||||
|
bubbles: true,
|
||||||
|
composed: true
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
this.dispatchEvent(new CustomEvent('open-image', {
|
||||||
|
detail: { attachment },
|
||||||
|
bubbles: true,
|
||||||
|
composed: true
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private downloadFile(attachment: IAttachment) {
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = attachment.url;
|
||||||
|
a.download = attachment.name;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleDropdownAction(event: CustomEvent) {
|
||||||
|
const { item } = event.detail as { item: IDropdownMenuItem };
|
||||||
|
|
||||||
|
// Dispatch event for parent to handle these actions
|
||||||
|
this.dispatchEvent(new CustomEvent('conversation-action', {
|
||||||
|
detail: {
|
||||||
|
action: item.id,
|
||||||
|
conversationId: this.conversation?.id
|
||||||
|
},
|
||||||
|
bubbles: true,
|
||||||
|
composed: true
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Log action for demo purposes
|
||||||
|
console.log('Conversation action:', item.id, item.label);
|
||||||
|
|
||||||
|
// Handle some actions locally for demo
|
||||||
|
switch (item.id) {
|
||||||
|
case 'search':
|
||||||
|
// Could open a search overlay
|
||||||
|
console.log('Opening search...');
|
||||||
|
break;
|
||||||
|
case 'export':
|
||||||
|
// Export conversation as JSON/text
|
||||||
|
this.exportConversation();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private exportConversation() {
|
||||||
|
if (!this.conversation) return;
|
||||||
|
|
||||||
|
const exportData = {
|
||||||
|
conversation: this.conversation.title,
|
||||||
|
exportDate: new Date().toISOString(),
|
||||||
|
messages: this.conversation.messages
|
||||||
|
};
|
||||||
|
|
||||||
|
const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `${this.conversation.title.replace(/\s+/g, '-')}-${Date.now()}.json`;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
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 accessor items: IDropdownMenuItem[] = [];
|
||||||
|
|
||||||
|
@property({ type: String })
|
||||||
|
public accessor align: 'left' | 'right' = 'right';
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private accessor isOpen: boolean = false;
|
||||||
|
|
||||||
|
private documentClickHandler: (e: MouseEvent) => void;
|
||||||
|
private scrollHandler: () => void;
|
||||||
|
private resizeHandler: () => void;
|
||||||
|
|
||||||
|
public static styles = [
|
||||||
|
cssManager.defaultStyles,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
font-family: ${unsafeCSS(fontFamilies.sans)};
|
||||||
|
}
|
||||||
|
|
||||||
|
.trigger {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% + 10px);
|
||||||
|
right: 0;
|
||||||
|
min-width: 200px;
|
||||||
|
background: ${bdTheme('background')};
|
||||||
|
border: 1px solid ${bdTheme('border')};
|
||||||
|
border-radius: ${unsafeCSS(radius.lg)};
|
||||||
|
box-shadow: ${unsafeCSS(shadows.lg)};
|
||||||
|
overflow: hidden;
|
||||||
|
z-index: 100000;
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-10px) scale(0.95);
|
||||||
|
pointer-events: none;
|
||||||
|
transition: all 200ms cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||||
|
transform-origin: top right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown.align-left {
|
||||||
|
right: auto;
|
||||||
|
left: 0;
|
||||||
|
transform-origin: top left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown.open {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: ${unsafeCSS(spacing["3"])};
|
||||||
|
padding: ${unsafeCSS(spacing["2.5"])} ${unsafeCSS(spacing["3"])};
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: ${bdTheme('foreground')};
|
||||||
|
cursor: pointer;
|
||||||
|
transition: ${unsafeCSS(transitions.all)};
|
||||||
|
user-select: none;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item:hover:not(.disabled) {
|
||||||
|
background: ${bdTheme('accent')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item:active:not(.disabled) {
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item.disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item.destructive {
|
||||||
|
color: ${bdTheme('destructive')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item.destructive:hover:not(.disabled) {
|
||||||
|
background: ${bdTheme('destructive')}10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: ${bdTheme('mutedForeground')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item.destructive .menu-icon {
|
||||||
|
color: ${bdTheme('destructive')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-label {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
height: 1px;
|
||||||
|
background: ${bdTheme('border')};
|
||||||
|
margin: ${unsafeCSS(spacing["1"])} 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile adjustments */
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.dropdown {
|
||||||
|
position: fixed;
|
||||||
|
top: auto !important;
|
||||||
|
left: ${unsafeCSS(spacing["4"])} !important;
|
||||||
|
right: ${unsafeCSS(spacing["4"])};
|
||||||
|
bottom: ${unsafeCSS(spacing["4"])};
|
||||||
|
width: auto;
|
||||||
|
transform-origin: bottom center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown.open {
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown:not(.open) {
|
||||||
|
transform: translateY(10px) scale(0.95);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
|
||||||
|
public render(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<div class="trigger" @click=${this.toggleDropdown}>
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
<div class="dropdown ${this.isOpen ? 'open' : ''} align-${this.align}">
|
||||||
|
${this.items.map(item =>
|
||||||
|
item.divider ? html`
|
||||||
|
<div class="divider"></div>
|
||||||
|
` : html`
|
||||||
|
<button
|
||||||
|
class="menu-item ${item.destructive ? 'destructive' : ''} ${item.disabled ? 'disabled' : ''}"
|
||||||
|
@click=${() => this.handleItemClick(item)}
|
||||||
|
?disabled=${item.disabled}
|
||||||
|
>
|
||||||
|
${item.icon ? html`
|
||||||
|
<sio-icon class="menu-icon" icon="${item.icon}" size="16"></sio-icon>
|
||||||
|
` : ''}
|
||||||
|
<span class="menu-label">${item.label}</span>
|
||||||
|
</button>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private toggleDropdown = (e: Event) => {
|
||||||
|
console.log('[Dropdown] Toggle called, current state:', this.isOpen);
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
this.isOpen = !this.isOpen;
|
||||||
|
console.log('[Dropdown] New state:', this.isOpen);
|
||||||
|
|
||||||
|
if (this.isOpen) {
|
||||||
|
this.addDocumentListener();
|
||||||
|
} else {
|
||||||
|
this.removeDocumentListener();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateDropdownPosition() {
|
||||||
|
// For absolute positioning, we don't need to calculate position dynamically
|
||||||
|
// The CSS handles it with top: calc(100% + 10px) and right: 0
|
||||||
|
console.log('[Dropdown] Position is handled by CSS (absolute positioning)');
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleItemClick(item: IDropdownMenuItem) {
|
||||||
|
if (item.disabled || item.divider) return;
|
||||||
|
|
||||||
|
this.isOpen = false;
|
||||||
|
this.removeDocumentListener();
|
||||||
|
|
||||||
|
// Dispatch custom event with the selected item
|
||||||
|
this.dispatchEvent(new CustomEvent('item-selected', {
|
||||||
|
detail: { item },
|
||||||
|
bubbles: true,
|
||||||
|
composed: true
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
private addDocumentListener() {
|
||||||
|
// Close dropdown when clicking outside
|
||||||
|
this.documentClickHandler = (e: MouseEvent) => {
|
||||||
|
const path = e.composedPath();
|
||||||
|
if (!path.includes(this)) {
|
||||||
|
this.isOpen = false;
|
||||||
|
this.removeDocumentListener();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update position on scroll/resize
|
||||||
|
this.scrollHandler = () => this.updateDropdownPosition();
|
||||||
|
this.resizeHandler = () => {
|
||||||
|
this.updateDropdownPosition();
|
||||||
|
if (window.innerWidth <= 600) {
|
||||||
|
// Close on mobile resize to prevent positioning issues
|
||||||
|
this.isOpen = false;
|
||||||
|
this.removeDocumentListener();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Delay to avoid immediate closing
|
||||||
|
setTimeout(() => {
|
||||||
|
document.addEventListener('click', this.documentClickHandler);
|
||||||
|
window.addEventListener('scroll', this.scrollHandler, true);
|
||||||
|
window.addEventListener('resize', this.resizeHandler);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private removeDocumentListener() {
|
||||||
|
if (this.documentClickHandler) {
|
||||||
|
document.removeEventListener('click', this.documentClickHandler);
|
||||||
|
}
|
||||||
|
if (this.scrollHandler) {
|
||||||
|
window.removeEventListener('scroll', this.scrollHandler, true);
|
||||||
|
}
|
||||||
|
if (this.resizeHandler) {
|
||||||
|
window.removeEventListener('resize', this.resizeHandler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async disconnectedCallback() {
|
||||||
|
await super.disconnectedCallback();
|
||||||
|
this.removeDocumentListener();
|
||||||
|
}
|
||||||
|
|
||||||
|
public close() {
|
||||||
|
this.isOpen = false;
|
||||||
|
this.removeDocumentListener();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,11 +6,20 @@ import {
|
|||||||
type TemplateResult,
|
type TemplateResult,
|
||||||
cssManager,
|
cssManager,
|
||||||
css,
|
css,
|
||||||
|
unsafeCSS,
|
||||||
|
state,
|
||||||
} from '@design.estate/dees-element';
|
} from '@design.estate/dees-element';
|
||||||
import * as domtools from '@design.estate/dees-domtools';
|
import * as domtools from '@design.estate/dees-domtools';
|
||||||
|
|
||||||
import { SioCombox } from './sio-combox.js';
|
import { SioCombox } from './sio-combox.js';
|
||||||
|
import { SioIcon } from './sio-icon.js';
|
||||||
SioCombox;
|
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 {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
@@ -20,8 +29,14 @@ declare global {
|
|||||||
|
|
||||||
@customElement('sio-fab')
|
@customElement('sio-fab')
|
||||||
export class SioFab extends DeesElement {
|
export class SioFab extends DeesElement {
|
||||||
@property()
|
@property({ type: Boolean })
|
||||||
public showCombox = false;
|
public accessor showCombox = false;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private accessor hasShownOnce = false;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private accessor shouldPulse = false;
|
||||||
|
|
||||||
public static demo = () => html` <sio-fab .showCombox=${true}></sio-fab> `;
|
public static demo = () => html` <sio-fab .showCombox=${true}></sio-fab> `;
|
||||||
|
|
||||||
@@ -30,137 +45,220 @@ export class SioFab extends DeesElement {
|
|||||||
domtools.DomTools.setupDomTools();
|
domtools.DomTools.setupDomTools();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static styles = [
|
||||||
|
cssManager.defaultStyles,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
will-change: transform;
|
||||||
|
position: absolute;
|
||||||
|
display: block;
|
||||||
|
bottom: 20px;
|
||||||
|
right: 20px;
|
||||||
|
z-index: 10000;
|
||||||
|
color: #fff;
|
||||||
|
--fab-gradient-start: #6366f1;
|
||||||
|
--fab-gradient-mid: #8b5cf6;
|
||||||
|
--fab-gradient-end: #a855f7;
|
||||||
|
--fab-gradient-hover-end: #c026d3;
|
||||||
|
--fab-shadow-color: rgba(139, 92, 246, 0.25);
|
||||||
|
--fab-size: 60px;
|
||||||
|
--fab-combox-offset: calc(var(--fab-size) + ${unsafeCSS(spacing["4"])});
|
||||||
|
}
|
||||||
|
|
||||||
|
#mainbox {
|
||||||
|
transition: ${unsafeCSS(transitions.all)};
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0px;
|
||||||
|
right: 0px;
|
||||||
|
height: var(--fab-size);
|
||||||
|
width: var(--fab-size);
|
||||||
|
box-shadow: 0 4px 16px -2px rgba(0, 0, 0, 0.1), 0 2px 8px -2px rgba(0, 0, 0, 0.06);
|
||||||
|
line-height: var(--fab-size);
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
background: linear-gradient(135deg, var(--fab-gradient-start) 0%, var(--fab-gradient-mid) 50%, var(--fab-gradient-end) 100%);
|
||||||
|
color: white;
|
||||||
|
border-radius: ${unsafeCSS(radius.full)};
|
||||||
|
user-select: none;
|
||||||
|
border: none;
|
||||||
|
animation: fabEntrance 300ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
overflow: hidden;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
-webkit-backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
#mainbox::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: linear-gradient(135deg, rgba(255, 255, 255, 0.2) 0%, rgba(255, 255, 255, 0) 50%);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 200ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mainbox::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -4px;
|
||||||
|
left: -4px;
|
||||||
|
right: -4px;
|
||||||
|
bottom: -4px;
|
||||||
|
background: linear-gradient(135deg, var(--fab-gradient-start), var(--fab-gradient-end));
|
||||||
|
border-radius: inherit;
|
||||||
|
z-index: -1;
|
||||||
|
opacity: 0;
|
||||||
|
filter: blur(12px);
|
||||||
|
transition: opacity 300ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mainbox:hover::before {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mainbox:hover::after {
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fabEntrance {
|
||||||
|
from {
|
||||||
|
transform: scale(0.8);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#mainbox:hover {
|
||||||
|
transform: scale(1.02);
|
||||||
|
background: linear-gradient(135deg, var(--fab-gradient-start) 0%, var(--fab-gradient-mid) 50%, var(--fab-gradient-hover-end) 100%);
|
||||||
|
box-shadow: 0 8px 20px -4px var(--fab-shadow-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
#mainbox:active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
box-shadow: 0 4px 12px -2px var(--fab-shadow-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
#mainbox.pulse::after {
|
||||||
|
animation: fabPulse 0.6s ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fabPulse {
|
||||||
|
0% {
|
||||||
|
box-shadow: 0 0 0 0 rgba(139, 92, 246, 0.4);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
box-shadow: 0 0 0 12px rgba(139, 92, 246, 0);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#mainbox .icon {
|
||||||
|
position: absolute;
|
||||||
|
top: 0px;
|
||||||
|
left: 0px;
|
||||||
|
will-change: transform, opacity;
|
||||||
|
transition: all 200ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mainbox .icon.open {
|
||||||
|
opacity: 1;
|
||||||
|
transform: rotate(0deg) scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#mainbox .icon.close {
|
||||||
|
opacity: 0;
|
||||||
|
transform: rotate(-45deg) scale(0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* When combox is open */
|
||||||
|
:host(.combox-open) #mainbox .icon.open {
|
||||||
|
opacity: 0;
|
||||||
|
transform: rotate(45deg) scale(0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
:host(.combox-open) #mainbox .icon.close {
|
||||||
|
opacity: 1;
|
||||||
|
transform: rotate(0deg) scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#mainbox .icon sio-icon {
|
||||||
|
color: white;
|
||||||
|
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.1));
|
||||||
|
}
|
||||||
|
|
||||||
|
#mainbox .icon.close sio-icon {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#comboxContainer {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#comboxContainer sio-combox {
|
||||||
|
position: absolute;
|
||||||
|
bottom: var(--fab-combox-offset);
|
||||||
|
right: 0;
|
||||||
|
transition: ${unsafeCSS(transitions.all)};
|
||||||
|
will-change: transform;
|
||||||
|
transform: translateY(${unsafeCSS(spacing["5"])});
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#comboxContainer.show {
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
|
|
||||||
|
#comboxContainer.show sio-combox {
|
||||||
|
transform: translateY(0px);
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
// Mobile responsive styles - smaller FAB on phablet and phone
|
||||||
|
cssManager.cssForPhablet(css`
|
||||||
|
:host {
|
||||||
|
--fab-size: 48px;
|
||||||
|
--fab-combox-offset: calc(var(--fab-size) + ${unsafeCSS(spacing["3"])});
|
||||||
|
bottom: 16px;
|
||||||
|
right: 16px;
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
];
|
||||||
|
|
||||||
public render(): TemplateResult {
|
public render(): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
${domtools.elementBasic.styles}
|
<div id="mainbox"
|
||||||
<style>
|
class="${this.shouldPulse ? 'pulse' : ''}"
|
||||||
:host {
|
@click=${this.toggleCombox}
|
||||||
will-change: transform;
|
@animationend=${() => { this.shouldPulse = false; }}
|
||||||
position: absolute;
|
>
|
||||||
display: block;
|
|
||||||
bottom: 20px;
|
|
||||||
right: 20px;
|
|
||||||
z-index: 10000;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
#mainbox {
|
|
||||||
transition: all 0.2s;
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0px;
|
|
||||||
right: 0px;
|
|
||||||
height: 60px;
|
|
||||||
width: 60px;
|
|
||||||
box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.3);
|
|
||||||
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%;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#mainbox:hover {
|
|
||||||
transform: scale(1.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
#mainbox:active {
|
|
||||||
transform: scale(0.95);
|
|
||||||
}
|
|
||||||
|
|
||||||
#mainbox .icon {
|
|
||||||
position: absolute;
|
|
||||||
top: 0px;
|
|
||||||
left: 0px;
|
|
||||||
will-change: transform;
|
|
||||||
transform: ${this.showCombox ? 'rotate(0deg)' : 'rotate(-360deg)'};
|
|
||||||
transition: all 0.2s;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
object-fit: contain;
|
|
||||||
-webkit-user-drag: none;
|
|
||||||
-khtml-user-drag: none;
|
|
||||||
-moz-user-drag: none;
|
|
||||||
-o-user-drag: none;
|
|
||||||
user-drag: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#mainbox .icon img {
|
|
||||||
filter: grayscale(1) ${cssManager.bdTheme('invert(1)', '')};
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
top: 0px;
|
|
||||||
left: 0px;
|
|
||||||
will-change: transform;
|
|
||||||
transform: scale(0.2, 0.2) translateY(-5px);
|
|
||||||
}
|
|
||||||
#mainbox .icon.open:hover img {
|
|
||||||
filter: grayscale(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#mainbox .icon.open {
|
|
||||||
opacity: ${this.showCombox ? '0' : '1'};
|
|
||||||
pointer-events: ${this.showCombox ? 'none' : 'all'};
|
|
||||||
}
|
|
||||||
|
|
||||||
#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')};
|
|
||||||
}
|
|
||||||
|
|
||||||
#mainbox .icon.open dees-icon {
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
font-size: 32px;
|
|
||||||
color: ${cssManager.bdTheme('#777', '#999')};
|
|
||||||
top: 0px;
|
|
||||||
left: 0px;
|
|
||||||
transform: translateY(2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
#mainbox .icon.close dees-icon {
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
font-size: 24px;
|
|
||||||
top: 0px;
|
|
||||||
left: 0px;
|
|
||||||
color: ${cssManager.bdTheme('#666', '#CCC')};
|
|
||||||
}
|
|
||||||
|
|
||||||
#comboxContainer sio-combox {
|
|
||||||
transition: transform 0.2s, opacity 0.2s;
|
|
||||||
will-change: transform;
|
|
||||||
transform: translateY(20px);
|
|
||||||
bottom: 80px;
|
|
||||||
opacity: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#comboxContainer.show sio-combox {
|
|
||||||
transform: translateY(0px);
|
|
||||||
opacity: 1;
|
|
||||||
pointer-events: all;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<div id="mainbox" @click=${this.toggleCombox}>
|
|
||||||
<div class="icon open">
|
<div class="icon open">
|
||||||
<dees-icon iconFA="message"></dees-icon>
|
<sio-icon icon="message-square" size="24"></sio-icon>
|
||||||
<img src="https://assetbroker.lossless.one/brandfiles/00general/favicon_socialio.svg" />
|
|
||||||
</div>
|
</div>
|
||||||
<div class="icon close">
|
<div class="icon close">
|
||||||
<dees-icon iconFa="xmark"></dees-icon>
|
<sio-icon icon="x" size="20"></sio-icon>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="comboxContainer" class="${this.showCombox ? 'show' : null}">
|
<div id="comboxContainer" class="${this.showCombox ? 'show' : ''}">
|
||||||
<sio-combox></sio-combox>
|
${this.showCombox || this.hasShownOnce ? html`
|
||||||
|
<sio-combox @close=${() => this.showCombox = false}></sio-combox>
|
||||||
|
` : ''}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -170,20 +268,48 @@ export class SioFab extends DeesElement {
|
|||||||
*/
|
*/
|
||||||
public async toggleCombox() {
|
public async toggleCombox() {
|
||||||
console.log('toggle combox');
|
console.log('toggle combox');
|
||||||
|
const wasOpen = this.showCombox;
|
||||||
this.showCombox = !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);
|
super.firstUpdated(args);
|
||||||
const domtools = await this.domtoolsPromise;
|
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
|
domtools.keyboard
|
||||||
.on([domtools.keyboard.keyEnum.Ctrl, domtools.keyboard.keyEnum.S])
|
.on([domtools.keyboard.keyEnum.Ctrl, domtools.keyboard.keyEnum.S])
|
||||||
.subscribe((event) => {
|
.subscribe(() => {
|
||||||
this.showCombox = !this.showCombox;
|
this.toggleCombox();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async updated(changedProperties: Map<string | number | symbol, unknown>) {
|
||||||
|
super.updated(changedProperties);
|
||||||
|
|
||||||
|
// Update host class based on combox state
|
||||||
|
if (changedProperties.has('showCombox')) {
|
||||||
|
if (this.showCombox) {
|
||||||
|
this.classList.add('combox-open');
|
||||||
|
} else {
|
||||||
|
this.classList.remove('combox-open');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set reference object when combox is rendered
|
||||||
|
if ((changedProperties.has('showCombox') || changedProperties.has('hasShownOnce')) &&
|
||||||
|
(this.showCombox || this.hasShownOnce)) {
|
||||||
|
const sioCombox: SioCombox = this.shadowRoot.querySelector('sio-combox');
|
||||||
|
const mainBox: HTMLElement = this.shadowRoot.querySelector('#mainbox');
|
||||||
|
if (sioCombox && mainBox && !sioCombox.referenceObject) {
|
||||||
|
sioCombox.referenceObject = mainBox;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
163
ts_web/elements/sio-icon.ts
Normal file
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 accessor icon: string;
|
||||||
|
|
||||||
|
@property({ type: Number })
|
||||||
|
public accessor size: number = 24;
|
||||||
|
|
||||||
|
@property({ type: String })
|
||||||
|
public accessor color: string = 'currentColor';
|
||||||
|
|
||||||
|
@property({ type: Number })
|
||||||
|
public accessor strokeWidth: number = 2;
|
||||||
|
|
||||||
|
// Cache for rendered icons
|
||||||
|
private static iconCache = new Map<string, string>();
|
||||||
|
private static readonly MAX_CACHE_SIZE = 100;
|
||||||
|
|
||||||
|
// Track last rendered properties to avoid unnecessary updates
|
||||||
|
private lastIcon: string | null = null;
|
||||||
|
private lastSize: number | null = null;
|
||||||
|
private lastColor: string | null = null;
|
||||||
|
private lastStrokeWidth: number | null = null;
|
||||||
|
|
||||||
|
public static styles = [
|
||||||
|
cssManager.defaultStyles,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
line-height: 1;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
#iconContainer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#iconContainer svg {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
|
||||||
|
public render(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<div id="iconContainer" style="width: ${this.size}px; height: ${this.size}px;"></div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
public updated() {
|
||||||
|
// Check if we need to update
|
||||||
|
if (
|
||||||
|
this.lastIcon === this.icon &&
|
||||||
|
this.lastSize === this.size &&
|
||||||
|
this.lastColor === this.color &&
|
||||||
|
this.lastStrokeWidth === this.strokeWidth
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update tracking properties
|
||||||
|
this.lastIcon = this.icon;
|
||||||
|
this.lastSize = this.size;
|
||||||
|
this.lastColor = this.color;
|
||||||
|
this.lastStrokeWidth = this.strokeWidth;
|
||||||
|
|
||||||
|
const container = this.shadowRoot?.querySelector('#iconContainer') as HTMLElement;
|
||||||
|
if (!container || !this.icon) return;
|
||||||
|
|
||||||
|
// Clear container
|
||||||
|
container.innerHTML = '';
|
||||||
|
|
||||||
|
// Create cache key
|
||||||
|
const cacheKey = `${this.icon}:${this.size}:${this.color}:${this.strokeWidth}`;
|
||||||
|
|
||||||
|
// Check cache
|
||||||
|
if (SioIcon.iconCache.has(cacheKey)) {
|
||||||
|
container.innerHTML = SioIcon.iconCache.get(cacheKey)!;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Convert icon name to PascalCase (e.g., 'message-square' -> 'MessageSquare')
|
||||||
|
const pascalCaseName = this.icon
|
||||||
|
.split('-')
|
||||||
|
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
|
||||||
|
.join('');
|
||||||
|
|
||||||
|
const iconComponent = (lucideIcons as any)[pascalCaseName];
|
||||||
|
if (!iconComponent) {
|
||||||
|
console.warn(`Lucide icon '${pascalCaseName}' not found`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the icon element
|
||||||
|
const svgElement = createElement(iconComponent, {
|
||||||
|
size: this.size,
|
||||||
|
color: this.color,
|
||||||
|
strokeWidth: this.strokeWidth,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (svgElement) {
|
||||||
|
// Cache the result
|
||||||
|
const svgString = svgElement.outerHTML;
|
||||||
|
SioIcon.iconCache.set(cacheKey, svgString);
|
||||||
|
|
||||||
|
// Limit cache size
|
||||||
|
if (SioIcon.iconCache.size > SioIcon.MAX_CACHE_SIZE) {
|
||||||
|
const firstKey = SioIcon.iconCache.keys().next().value;
|
||||||
|
SioIcon.iconCache.delete(firstKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append to container
|
||||||
|
container.appendChild(svgElement);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error rendering icon ${this.icon}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async disconnectedCallback() {
|
||||||
|
await super.disconnectedCallback();
|
||||||
|
// Clear references
|
||||||
|
this.lastIcon = null;
|
||||||
|
this.lastSize = null;
|
||||||
|
this.lastColor = null;
|
||||||
|
this.lastStrokeWidth = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
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 accessor isOpen: boolean = false;
|
||||||
|
|
||||||
|
@property({ type: Object })
|
||||||
|
public accessor file: ILightboxFile | null = null;
|
||||||
|
|
||||||
|
// For backwards compatibility
|
||||||
|
public get image(): ILightboxFile | null {
|
||||||
|
return this.file;
|
||||||
|
}
|
||||||
|
public set image(value: ILightboxFile | null) {
|
||||||
|
this.file = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private accessor fileLoaded: boolean = false;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private accessor scale: number = 1;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private accessor translateX: number = 0;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private accessor translateY: number = 0;
|
||||||
|
|
||||||
|
private isDragging: boolean = false;
|
||||||
|
private startX: number = 0;
|
||||||
|
private startY: number = 0;
|
||||||
|
|
||||||
|
public static styles = [
|
||||||
|
cssManager.defaultStyles,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 10000;
|
||||||
|
pointer-events: none;
|
||||||
|
font-family: ${unsafeCSS(fontFamilies.sans)};
|
||||||
|
isolation: isolate;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0);
|
||||||
|
backdrop-filter: blur(0px);
|
||||||
|
-webkit-backdrop-filter: blur(0px);
|
||||||
|
transition: all 300ms ease;
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay.open {
|
||||||
|
background: rgba(0, 0, 0, 0.9);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
-webkit-backdrop-filter: blur(10px);
|
||||||
|
pointer-events: all;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: ${unsafeCSS(spacing["8"])};
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.9);
|
||||||
|
transition: all 300ms cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.container.open {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-wrapper {
|
||||||
|
position: relative;
|
||||||
|
max-width: 90vw;
|
||||||
|
max-height: 90vh;
|
||||||
|
cursor: grab;
|
||||||
|
user-select: none;
|
||||||
|
transition: transform 100ms ease-out;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-wrapper.dragging {
|
||||||
|
cursor: grabbing;
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-wrapper.pdf {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image {
|
||||||
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 90vh;
|
||||||
|
border-radius: ${unsafeCSS(radius.lg)};
|
||||||
|
box-shadow: ${unsafeCSS(shadows["2xl"])};
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 300ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image.loaded {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-viewer {
|
||||||
|
width: 90vw;
|
||||||
|
height: 90vh;
|
||||||
|
border-radius: ${unsafeCSS(radius.lg)};
|
||||||
|
box-shadow: ${unsafeCSS(shadows["2xl"])};
|
||||||
|
background: white;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 300ms ease;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-viewer.loaded {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: ${unsafeCSS(spacing["2"])};
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
position: absolute;
|
||||||
|
top: ${unsafeCSS(spacing["4"])};
|
||||||
|
right: ${unsafeCSS(spacing["4"])};
|
||||||
|
display: flex;
|
||||||
|
gap: ${unsafeCSS(spacing["2"])};
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 200ms ease;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container.open .controls {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-button {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: ${unsafeCSS(radius.full)};
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
-webkit-backdrop-filter: blur(10px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: ${unsafeCSS(transitions.all)};
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-button:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-button:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
position: absolute;
|
||||||
|
bottom: ${unsafeCSS(spacing["4"])};
|
||||||
|
left: ${unsafeCSS(spacing["4"])};
|
||||||
|
right: ${unsafeCSS(spacing["4"])};
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
-webkit-backdrop-filter: blur(10px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: ${unsafeCSS(radius.lg)};
|
||||||
|
padding: ${unsafeCSS(spacing["3"])} ${unsafeCSS(spacing["4"])};
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
transition: all 200ms ease;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container.open .info {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-name {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: ${unsafeCSS(spacing["3"])};
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-button {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
opacity: 0.8;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: ${unsafeCSS(spacing["1"])};
|
||||||
|
font-size: 0.875rem;
|
||||||
|
padding: ${unsafeCSS(spacing["1"])} ${unsafeCSS(spacing["2"])};
|
||||||
|
border-radius: ${unsafeCSS(radius.md)};
|
||||||
|
transition: ${unsafeCSS(transitions.all)};
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-button:hover {
|
||||||
|
opacity: 1;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.container {
|
||||||
|
padding: ${unsafeCSS(spacing["4"])};
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
top: ${unsafeCSS(spacing["2"])};
|
||||||
|
right: ${unsafeCSS(spacing["2"])};
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
bottom: ${unsafeCSS(spacing["2"])};
|
||||||
|
left: ${unsafeCSS(spacing["2"])};
|
||||||
|
right: ${unsafeCSS(spacing["2"])};
|
||||||
|
padding: ${unsafeCSS(spacing["2"])} ${unsafeCSS(spacing["3"])};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
|
||||||
|
private isPDF(): boolean {
|
||||||
|
return this.file?.type?.includes('pdf') ||
|
||||||
|
this.file?.name?.toLowerCase().endsWith('.pdf') || false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public render(): TemplateResult {
|
||||||
|
const contentStyle = this.scale !== 1 || this.translateX !== 0 || this.translateY !== 0
|
||||||
|
? `transform: scale(${this.scale}) translate(${this.translateX}px, ${this.translateY}px)`
|
||||||
|
: '';
|
||||||
|
|
||||||
|
const isPDF = this.isPDF();
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="overlay ${this.isOpen ? 'open' : ''}" @click=${(e: Event) => {
|
||||||
|
if (e.target === e.currentTarget) this.close(e);
|
||||||
|
}}></div>
|
||||||
|
<div class="container ${this.isOpen ? 'open' : ''}">
|
||||||
|
${this.file ? html`
|
||||||
|
<div class="controls">
|
||||||
|
${!isPDF ? html`
|
||||||
|
<div class="control-button" @click=${this.zoomIn}>
|
||||||
|
<sio-icon icon="zoom-in" size="18"></sio-icon>
|
||||||
|
</div>
|
||||||
|
<div class="control-button" @click=${this.zoomOut}>
|
||||||
|
<sio-icon icon="zoom-out" size="18"></sio-icon>
|
||||||
|
</div>
|
||||||
|
<div class="control-button" @click=${this.resetZoom}>
|
||||||
|
<sio-icon icon="maximize-2" size="18"></sio-icon>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
<div class="control-button" @click=${(e: Event) => this.close(e)}>
|
||||||
|
<sio-icon icon="x" size="18"></sio-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="content-wrapper ${this.isDragging ? 'dragging' : ''} ${isPDF ? 'pdf' : ''}"
|
||||||
|
style="${contentStyle}"
|
||||||
|
@mousedown=${!isPDF ? this.startDrag : undefined}
|
||||||
|
@mousemove=${!isPDF ? this.drag : undefined}
|
||||||
|
@mouseup=${!isPDF ? this.endDrag : undefined}
|
||||||
|
@mouseleave=${!isPDF ? this.endDrag : undefined}
|
||||||
|
@wheel=${this.handleWheel}
|
||||||
|
>
|
||||||
|
${!this.fileLoaded ? html`
|
||||||
|
<div class="loading">
|
||||||
|
<sio-icon class="spinner" icon="loader" size="24"></sio-icon>
|
||||||
|
<span>Loading...</span>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
${isPDF ? html`
|
||||||
|
<sio-pdf-viewer
|
||||||
|
class="pdf-viewer ${this.fileLoaded ? 'loaded' : ''}"
|
||||||
|
.url="${this.file.url}"
|
||||||
|
.fileName="${this.file.name}"
|
||||||
|
@load=${() => this.fileLoaded = true}
|
||||||
|
></sio-pdf-viewer>
|
||||||
|
` : html`
|
||||||
|
<img
|
||||||
|
class="image ${this.fileLoaded ? 'loaded' : ''}"
|
||||||
|
src="${this.file.url}"
|
||||||
|
alt="${this.file.name}"
|
||||||
|
@load=${() => this.fileLoaded = true}
|
||||||
|
@error=${() => this.fileLoaded = false}
|
||||||
|
@click=${(e: Event) => e.stopPropagation()}
|
||||||
|
/>
|
||||||
|
`}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info">
|
||||||
|
<div class="info-name">${this.file.name}</div>
|
||||||
|
<div class="info-actions">
|
||||||
|
<button class="info-button" @click=${this.download}>
|
||||||
|
<sio-icon icon="download" size="16"></sio-icon>
|
||||||
|
Download
|
||||||
|
</button>
|
||||||
|
<button class="info-button" @click=${this.openInNewTab}>
|
||||||
|
<sio-icon icon="external-link" size="16"></sio-icon>
|
||||||
|
Open
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async open(file: ILightboxFile | ILightboxImage) {
|
||||||
|
this.file = file;
|
||||||
|
this.fileLoaded = false;
|
||||||
|
this.resetZoom();
|
||||||
|
this.isOpen = true;
|
||||||
|
|
||||||
|
// For PDFs, we'll handle loading state differently since it's in a separate component
|
||||||
|
if (this.isPDF()) {
|
||||||
|
// PDFs are handled by sio-pdf-viewer which manages its own loading state
|
||||||
|
this.fileLoaded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add keyboard listener
|
||||||
|
document.addEventListener('keydown', this.handleKeyDown);
|
||||||
|
}
|
||||||
|
|
||||||
|
private close = (e?: Event) => {
|
||||||
|
if (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isOpen = false;
|
||||||
|
document.removeEventListener('keydown', this.handleKeyDown);
|
||||||
|
|
||||||
|
// Dispatch close event
|
||||||
|
this.dispatchEvent(new CustomEvent('lightbox-close', {
|
||||||
|
bubbles: true,
|
||||||
|
composed: true
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Clean up after animation
|
||||||
|
setTimeout(() => {
|
||||||
|
this.file = null;
|
||||||
|
this.fileLoaded = false;
|
||||||
|
this.resetZoom();
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
this.close();
|
||||||
|
} else if (e.key === '+' || e.key === '=') {
|
||||||
|
this.zoomIn();
|
||||||
|
} else if (e.key === '-') {
|
||||||
|
this.zoomOut();
|
||||||
|
} else if (e.key === '0') {
|
||||||
|
this.resetZoom();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private zoomIn() {
|
||||||
|
this.scale = Math.min(this.scale * 1.2, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
private zoomOut() {
|
||||||
|
this.scale = Math.max(this.scale / 1.2, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
private resetZoom() {
|
||||||
|
this.scale = 1;
|
||||||
|
this.translateX = 0;
|
||||||
|
this.translateY = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleWheel = (e: WheelEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (e.ctrlKey || e.metaKey) {
|
||||||
|
// Zoom with ctrl/cmd + scroll
|
||||||
|
if (e.deltaY < 0) {
|
||||||
|
this.zoomIn();
|
||||||
|
} else {
|
||||||
|
this.zoomOut();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private startDrag = (e: MouseEvent) => {
|
||||||
|
if (this.scale > 1) {
|
||||||
|
this.isDragging = true;
|
||||||
|
this.startX = e.clientX - this.translateX;
|
||||||
|
this.startY = e.clientY - this.translateY;
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private drag = (e: MouseEvent) => {
|
||||||
|
if (this.isDragging && this.scale > 1) {
|
||||||
|
this.translateX = e.clientX - this.startX;
|
||||||
|
this.translateY = e.clientY - this.startY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private endDrag = () => {
|
||||||
|
this.isDragging = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private download() {
|
||||||
|
if (!this.file) return;
|
||||||
|
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = this.file.url;
|
||||||
|
a.download = this.file.name;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
}
|
||||||
|
|
||||||
|
private openInNewTab() {
|
||||||
|
if (!this.file) return;
|
||||||
|
window.open(this.file.url, '_blank');
|
||||||
|
}
|
||||||
|
|
||||||
|
public async disconnectedCallback() {
|
||||||
|
await super.disconnectedCallback();
|
||||||
|
document.removeEventListener('keydown', this.handleKeyDown);
|
||||||
|
}
|
||||||
|
}
|
||||||
279
ts_web/elements/sio-message-input.ts
Normal file
279
ts_web/elements/sio-message-input.ts
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
import {
|
||||||
|
DeesElement,
|
||||||
|
property,
|
||||||
|
html,
|
||||||
|
customElement,
|
||||||
|
type TemplateResult,
|
||||||
|
cssManager,
|
||||||
|
css,
|
||||||
|
unsafeCSS,
|
||||||
|
state,
|
||||||
|
} from '@design.estate/dees-element';
|
||||||
|
|
||||||
|
// Import design tokens
|
||||||
|
import { colors, bdTheme } from './00colors.js';
|
||||||
|
import { spacing, radius, shadows, transitions } from './00tokens.js';
|
||||||
|
import { fontFamilies, typography } from './00fonts.js';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'sio-message-input': SioMessageInput;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@customElement('sio-message-input')
|
||||||
|
export class SioMessageInput extends DeesElement {
|
||||||
|
public static demo = () => html`
|
||||||
|
<sio-message-input style="width: 600px;"></sio-message-input>
|
||||||
|
`;
|
||||||
|
|
||||||
|
@property({ type: String })
|
||||||
|
public accessor placeholder: string = 'Type a message...';
|
||||||
|
|
||||||
|
@property({ type: Boolean })
|
||||||
|
public accessor disabled: boolean = false;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private accessor messageText: string = '';
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private accessor pendingAttachments: File[] = [];
|
||||||
|
|
||||||
|
public static styles = [
|
||||||
|
cssManager.defaultStyles,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
font-family: ${unsafeCSS(fontFamilies.sans)};
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
background: ${bdTheme('secondary')};
|
||||||
|
border: 1px solid ${bdTheme('border')};
|
||||||
|
border-radius: 24px;
|
||||||
|
padding: 2px;
|
||||||
|
transition: all 200ms ease;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-container:focus-within {
|
||||||
|
border-color: ${bdTheme('ring')};
|
||||||
|
background: ${bdTheme('background')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-input {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 44px;
|
||||||
|
max-height: 120px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
font-size: 15px;
|
||||||
|
color: ${bdTheme('foreground')};
|
||||||
|
outline: none;
|
||||||
|
resize: none;
|
||||||
|
font-family: ${unsafeCSS(fontFamilies.sans)};
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-input::placeholder {
|
||||||
|
color: ${bdTheme('mutedForeground')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
align-items: center;
|
||||||
|
padding-right: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: ${bdTheme('mutedForeground')};
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 120ms ease;
|
||||||
|
position: relative;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button:hover {
|
||||||
|
color: ${bdTheme('foreground')};
|
||||||
|
background: ${bdTheme('hsl(0 0% 0% / 0.05)', 'hsl(0 0% 100% / 0.05)')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button.attachment {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button.attachment:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button.send {
|
||||||
|
background: ${bdTheme('primary')};
|
||||||
|
color: white;
|
||||||
|
margin-left: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button.send:hover {
|
||||||
|
background: ${bdTheme('hsl(221 83% 49%)', 'hsl(217 91% 65%)')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button.send:disabled {
|
||||||
|
opacity: 0.3;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button.send:disabled:hover {
|
||||||
|
background: ${bdTheme('primary')};
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Icon styles */
|
||||||
|
sio-icon {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button.send sio-icon {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
|
||||||
|
public render(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<div class="input-container">
|
||||||
|
<textarea
|
||||||
|
class="message-input"
|
||||||
|
placeholder="${this.placeholder}"
|
||||||
|
.value=${this.messageText}
|
||||||
|
@input=${this.handleInput}
|
||||||
|
@keydown=${this.handleKeyDown}
|
||||||
|
?disabled=${this.disabled}
|
||||||
|
rows="1"
|
||||||
|
></textarea>
|
||||||
|
|
||||||
|
<div class="action-buttons">
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
class="file-input"
|
||||||
|
id="fileInput"
|
||||||
|
multiple
|
||||||
|
accept="image/*,.pdf,.doc,.docx,.txt"
|
||||||
|
@change=${this.handleFileSelect}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
class="action-button attachment"
|
||||||
|
@click=${this.openFileSelector}
|
||||||
|
?disabled=${this.disabled}
|
||||||
|
>
|
||||||
|
<sio-icon icon="paperclip"></sio-icon>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="action-button send"
|
||||||
|
?disabled=${!this.messageText.trim() || this.disabled}
|
||||||
|
@click=${this.sendMessage}
|
||||||
|
>
|
||||||
|
<sio-icon icon="send"></sio-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleInput(event: Event) {
|
||||||
|
const textarea = event.target as HTMLTextAreaElement;
|
||||||
|
this.messageText = textarea.value;
|
||||||
|
|
||||||
|
// Auto-resize textarea
|
||||||
|
textarea.style.height = 'auto';
|
||||||
|
textarea.style.height = `${textarea.scrollHeight}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleKeyDown(event: KeyboardEvent) {
|
||||||
|
if (event.key === 'Enter' && !event.shiftKey) {
|
||||||
|
event.preventDefault();
|
||||||
|
this.sendMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sendMessage() {
|
||||||
|
if (!this.messageText.trim() && this.pendingAttachments.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dispatchEvent(new CustomEvent('send-message', {
|
||||||
|
detail: {
|
||||||
|
text: this.messageText,
|
||||||
|
attachments: this.pendingAttachments
|
||||||
|
},
|
||||||
|
bubbles: true,
|
||||||
|
composed: true
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Clear input
|
||||||
|
this.messageText = '';
|
||||||
|
this.pendingAttachments = [];
|
||||||
|
|
||||||
|
// Reset textarea height
|
||||||
|
const textarea = this.shadowRoot?.querySelector('.message-input') as HTMLTextAreaElement;
|
||||||
|
if (textarea) {
|
||||||
|
textarea.style.height = 'auto';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private openFileSelector() {
|
||||||
|
const fileInput = this.shadowRoot?.querySelector('#fileInput') as HTMLInputElement;
|
||||||
|
if (fileInput) {
|
||||||
|
fileInput.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleFileSelect(event: Event) {
|
||||||
|
const input = event.target as HTMLInputElement;
|
||||||
|
const files = Array.from(input.files || []);
|
||||||
|
|
||||||
|
this.pendingAttachments = [...this.pendingAttachments, ...files];
|
||||||
|
|
||||||
|
this.dispatchEvent(new CustomEvent('files-selected', {
|
||||||
|
detail: { files },
|
||||||
|
bubbles: true,
|
||||||
|
composed: true
|
||||||
|
}));
|
||||||
|
|
||||||
|
input.value = ''; // Clear input for re-selection
|
||||||
|
}
|
||||||
|
|
||||||
|
public focus() {
|
||||||
|
const textarea = this.shadowRoot?.querySelector('.message-input') as HTMLTextAreaElement;
|
||||||
|
textarea?.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
public clear() {
|
||||||
|
this.messageText = '';
|
||||||
|
this.pendingAttachments = [];
|
||||||
|
const textarea = this.shadowRoot?.querySelector('.message-input') as HTMLTextAreaElement;
|
||||||
|
if (textarea) {
|
||||||
|
textarea.style.height = 'auto';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
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 { bdTheme } from './00colors.js';
|
||||||
|
import { spacing, radius, shadows } from './00tokens.js';
|
||||||
|
import { fontFamilies } from './00fonts.js';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'sio-pdf-viewer': SioPdfViewer;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Window {
|
||||||
|
pdfjsLib?: any;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@customElement('sio-pdf-viewer')
|
||||||
|
export class SioPdfViewer extends DeesElement {
|
||||||
|
public static demo = () => html`
|
||||||
|
<sio-pdf-viewer
|
||||||
|
.url=${'data:application/pdf;base64,JVBERi0xLjMKJeLjz9MKMSAwIG9iago8PAovVHlwZSAvQ2F0YWxvZwovT3V0bGluZXMgMiAwIFIKL1BhZ2VzIDMgMCBSCj4+CmVuZG9iagoyIDAgb2JqCjw8Ci9UeXBlIC9PdXRsaW5lcwovQ291bnQgMAo+PgplbmRvYmoKMyAwIG9iago8PAovVHlwZSAvUGFnZXMKL0NvdW50IDEKL0tpZHMgWzQgMCBSXQo+PgplbmRvYmoKNCAwIG9iago8PAovVHlwZSAvUGFnZQovUGFyZW50IDMgMCBSCi9NZWRpYUJveCBbMCAwIDYxMiA3OTJdCi9Db250ZW50cyA1IDAgUgovUmVzb3VyY2VzIDw8Ci9Gb250IDw8Ci9GMSA2IDAgUgo+Pgo+Pgo+PgplbmRvYmoKNSAwIG9iago8PAovTGVuZ3RoIDQ0Cj4+CnN0cmVhbQpCVApxCjcwIDUwIFRECi9GMSAxMiBUZgooSGVsbG8gV29ybGQpIFRqCkVUClEKZW5kc3RyZWFtCmVuZG9iago2IDAgb2JqCjw8Ci9UeXBlIC9Gb250Ci9TdWJ0eXBlIC9UeXBlMQovQmFzZUZvbnQgL1RpbWVzLVJvbWFuCj4+CmVuZG9iagp4cmVmCjAgNwowMDAwMDAwMDAwIDY1NTM1IGYgCjAwMDAwMDAwMDkgMDAwMDAgbiAKMDAwMDAwMDA3NCAwMDAwMCBuIAowMDAwMDAwMTIwIDAwMDAwIG4gCjAwMDAwMDAxNzkgMDAwMDAgbiAKMDAwMDAwMDM2NCAwMDAwMCBuIAowMDAwMDAwNDY2IDAwMDAwIG4gCnRyYWlsZXIKPDwKL1NpemUgNwovUm9vdCAxIDAgUgo+PgpzdGFydHhyZWYKNTY1CiUlRU9G'}
|
||||||
|
.fileName=${'demo.pdf'}
|
||||||
|
style="width: 600px; height: 800px;"
|
||||||
|
></sio-pdf-viewer>
|
||||||
|
`;
|
||||||
|
|
||||||
|
@property({ type: String })
|
||||||
|
public accessor url: string = '';
|
||||||
|
|
||||||
|
@property({ type: String })
|
||||||
|
public accessor fileName: string = 'document.pdf';
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private accessor isLoading: boolean = true;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private accessor hasError: boolean = false;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private accessor pdfDocument: any = null;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private accessor currentPage: number = 1;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private accessor totalPages: number = 0;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private accessor scale: number = 1;
|
||||||
|
|
||||||
|
private static pdfJsLoaded: boolean = false;
|
||||||
|
private static pdfJsLoading: Promise<void> | null = null;
|
||||||
|
private renderTask: any = null;
|
||||||
|
private resizeObserver: ResizeObserver | null = null;
|
||||||
|
|
||||||
|
public static styles = [
|
||||||
|
cssManager.defaultStyles,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
background: ${bdTheme('background')};
|
||||||
|
font-family: ${unsafeCSS(fontFamilies.sans)};
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
background: ${bdTheme('muted')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-canvas-wrapper {
|
||||||
|
position: relative;
|
||||||
|
margin: ${unsafeCSS(spacing["4"])} auto;
|
||||||
|
box-shadow: ${unsafeCSS(shadows.lg)};
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas {
|
||||||
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-controls {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 10;
|
||||||
|
width: 100%;
|
||||||
|
padding: ${unsafeCSS(spacing["3"])};
|
||||||
|
background: ${bdTheme('background')};
|
||||||
|
border-bottom: 1px solid ${bdTheme('border')};
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: ${unsafeCSS(spacing["3"])};
|
||||||
|
box-shadow: ${unsafeCSS(shadows.sm)};
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-controls-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: ${unsafeCSS(spacing["2"])};
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-info {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: ${bdTheme('mutedForeground')};
|
||||||
|
min-width: 100px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
text-align: center;
|
||||||
|
color: ${bdTheme('mutedForeground')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
margin-bottom: ${unsafeCSS(spacing["2"])};
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-container {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
text-align: center;
|
||||||
|
padding: ${unsafeCSS(spacing["6"])};
|
||||||
|
background: ${bdTheme('card')};
|
||||||
|
border: 1px solid ${bdTheme('border')};
|
||||||
|
border-radius: ${unsafeCSS(radius.lg)};
|
||||||
|
box-shadow: ${unsafeCSS(shadows.md)};
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-icon {
|
||||||
|
color: ${bdTheme('destructive')};
|
||||||
|
margin-bottom: ${unsafeCSS(spacing["3"])};
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-title {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: ${bdTheme('foreground')};
|
||||||
|
margin-bottom: ${unsafeCSS(spacing["2"])};
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
color: ${bdTheme('mutedForeground')};
|
||||||
|
margin-bottom: ${unsafeCSS(spacing["4"])};
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: ${unsafeCSS(spacing["2"])};
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fallback-viewer {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: ${bdTheme('background')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.fallback-header {
|
||||||
|
padding: ${unsafeCSS(spacing["4"])};
|
||||||
|
border-bottom: 1px solid ${bdTheme('border')};
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
background: ${bdTheme('card')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.fallback-title {
|
||||||
|
font-weight: 500;
|
||||||
|
color: ${bdTheme('foreground')};
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: ${unsafeCSS(spacing["2"])};
|
||||||
|
}
|
||||||
|
|
||||||
|
.fallback-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: ${unsafeCSS(spacing["8"])};
|
||||||
|
}
|
||||||
|
|
||||||
|
.fallback-message {
|
||||||
|
text-align: center;
|
||||||
|
color: ${bdTheme('mutedForeground')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.fallback-icon {
|
||||||
|
font-size: 48px;
|
||||||
|
margin-bottom: ${unsafeCSS(spacing["4"])};
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fallback-text {
|
||||||
|
margin-bottom: ${unsafeCSS(spacing["4"])};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollbar styling */
|
||||||
|
.pdf-container::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-container::-webkit-scrollbar-track {
|
||||||
|
background: ${bdTheme('muted')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-container::-webkit-scrollbar-thumb {
|
||||||
|
background: ${bdTheme('border')};
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-container::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: ${bdTheme('mutedForeground')};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.pdf-controls {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: ${unsafeCSS(spacing["2"])};
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-controls-group {
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
|
||||||
|
public render(): TemplateResult {
|
||||||
|
if (this.hasError) {
|
||||||
|
return this.renderError();
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="container">
|
||||||
|
${this.isLoading ? html`
|
||||||
|
<div class="loading">
|
||||||
|
<sio-icon class="spinner" icon="loader" size="24"></sio-icon>
|
||||||
|
<div>Loading PDF...</div>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
|
||||||
|
${this.pdfDocument ? html`
|
||||||
|
<div class="pdf-controls">
|
||||||
|
<div class="pdf-controls-group">
|
||||||
|
<sio-button
|
||||||
|
type="ghost"
|
||||||
|
size="sm"
|
||||||
|
@click=${this.previousPage}
|
||||||
|
?disabled=${this.currentPage <= 1}
|
||||||
|
>
|
||||||
|
<sio-icon icon="chevron-left" size="16"></sio-icon>
|
||||||
|
</sio-button>
|
||||||
|
<div class="page-info">
|
||||||
|
Page ${this.currentPage} of ${this.totalPages}
|
||||||
|
</div>
|
||||||
|
<sio-button
|
||||||
|
type="ghost"
|
||||||
|
size="sm"
|
||||||
|
@click=${this.nextPage}
|
||||||
|
?disabled=${this.currentPage >= this.totalPages}
|
||||||
|
>
|
||||||
|
<sio-icon icon="chevron-right" size="16"></sio-icon>
|
||||||
|
</sio-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pdf-controls-group">
|
||||||
|
<sio-button
|
||||||
|
type="ghost"
|
||||||
|
size="sm"
|
||||||
|
@click=${this.zoomOut}
|
||||||
|
>
|
||||||
|
<sio-icon icon="zoom-out" size="16"></sio-icon>
|
||||||
|
</sio-button>
|
||||||
|
<sio-button
|
||||||
|
type="ghost"
|
||||||
|
size="sm"
|
||||||
|
@click=${this.resetZoom}
|
||||||
|
>
|
||||||
|
<sio-icon icon="maximize-2" size="16"></sio-icon>
|
||||||
|
</sio-button>
|
||||||
|
<sio-button
|
||||||
|
type="ghost"
|
||||||
|
size="sm"
|
||||||
|
@click=${this.zoomIn}
|
||||||
|
>
|
||||||
|
<sio-icon icon="zoom-in" size="16"></sio-icon>
|
||||||
|
</sio-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pdf-controls-group">
|
||||||
|
<sio-button
|
||||||
|
type="ghost"
|
||||||
|
size="sm"
|
||||||
|
@click=${this.downloadPdf}
|
||||||
|
>
|
||||||
|
<sio-icon icon="download" size="16"></sio-icon>
|
||||||
|
</sio-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pdf-container">
|
||||||
|
<div class="pdf-canvas-wrapper">
|
||||||
|
<canvas></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderError(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<div class="error-container">
|
||||||
|
<sio-icon class="error-icon" icon="alert-circle" size="48"></sio-icon>
|
||||||
|
<div class="error-title">Unable to display PDF</div>
|
||||||
|
<div class="error-message">
|
||||||
|
The PDF viewer couldn't load this document. This might be due to browser restrictions or an invalid PDF file.
|
||||||
|
</div>
|
||||||
|
<div class="error-actions">
|
||||||
|
<sio-button
|
||||||
|
type="primary"
|
||||||
|
size="sm"
|
||||||
|
@click=${this.downloadPdf}
|
||||||
|
>
|
||||||
|
<sio-icon icon="download" size="16"></sio-icon>
|
||||||
|
Download PDF
|
||||||
|
</sio-button>
|
||||||
|
<sio-button
|
||||||
|
type="outline"
|
||||||
|
size="sm"
|
||||||
|
@click=${this.openInNewTab}
|
||||||
|
>
|
||||||
|
<sio-icon icon="external-link" size="16"></sio-icon>
|
||||||
|
Open in New Tab
|
||||||
|
</sio-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async connectedCallback() {
|
||||||
|
await super.connectedCallback();
|
||||||
|
|
||||||
|
if (this.url) {
|
||||||
|
await this.loadPdf();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async firstUpdated(_changedProperties: any) {
|
||||||
|
super.firstUpdated(_changedProperties);
|
||||||
|
|
||||||
|
// Set up resize observer for responsive rendering after first render
|
||||||
|
const container = this.shadowRoot?.querySelector('.pdf-container');
|
||||||
|
if (container) {
|
||||||
|
this.resizeObserver = new ResizeObserver(() => {
|
||||||
|
if (this.pdfDocument && !this.isLoading) {
|
||||||
|
this.renderPage();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.resizeObserver.observe(container);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async updated(changedProperties: Map<string | number | symbol, unknown>) {
|
||||||
|
super.updated(changedProperties);
|
||||||
|
if (changedProperties.has('url') && this.url) {
|
||||||
|
await this.loadPdf();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-render when scale changes
|
||||||
|
if (changedProperties.has('scale') && this.pdfDocument && !this.isLoading) {
|
||||||
|
await this.renderPage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async loadPdfJs(): Promise<void> {
|
||||||
|
if (SioPdfViewer.pdfJsLoaded) return;
|
||||||
|
|
||||||
|
if (SioPdfViewer.pdfJsLoading) {
|
||||||
|
return SioPdfViewer.pdfJsLoading;
|
||||||
|
}
|
||||||
|
|
||||||
|
SioPdfViewer.pdfJsLoading = new Promise(async (resolve, reject) => {
|
||||||
|
try {
|
||||||
|
// Load PDF.js from jsDelivr
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.src = 'https://cdn.jsdelivr.net/npm/pdfjs-dist@3.11.174/build/pdf.min.js';
|
||||||
|
|
||||||
|
script.onload = () => {
|
||||||
|
if (window.pdfjsLib) {
|
||||||
|
// Configure worker
|
||||||
|
window.pdfjsLib.GlobalWorkerOptions.workerSrc =
|
||||||
|
'https://cdn.jsdelivr.net/npm/pdfjs-dist@3.11.174/build/pdf.worker.min.js';
|
||||||
|
SioPdfViewer.pdfJsLoaded = true;
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
reject(new Error('PDF.js failed to load'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
script.onerror = () => reject(new Error('Failed to load PDF.js script'));
|
||||||
|
|
||||||
|
document.head.appendChild(script);
|
||||||
|
} catch (error) {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return SioPdfViewer.pdfJsLoading;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async loadPdf() {
|
||||||
|
this.isLoading = true;
|
||||||
|
this.hasError = false;
|
||||||
|
this.pdfDocument = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Load PDF.js if not already loaded
|
||||||
|
await SioPdfViewer.loadPdfJs();
|
||||||
|
|
||||||
|
// Load the PDF document
|
||||||
|
const loadingTask = window.pdfjsLib.getDocument({
|
||||||
|
url: this.url,
|
||||||
|
// Enable range requests for better performance
|
||||||
|
disableRange: false,
|
||||||
|
// Enable streaming for large PDFs
|
||||||
|
disableStream: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.pdfDocument = await loadingTask.promise;
|
||||||
|
this.totalPages = this.pdfDocument.numPages;
|
||||||
|
this.currentPage = 1;
|
||||||
|
this.isLoading = false;
|
||||||
|
|
||||||
|
// Render the first page
|
||||||
|
await this.renderPage();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load PDF:', error);
|
||||||
|
this.hasError = true;
|
||||||
|
this.isLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async renderPage() {
|
||||||
|
if (!this.pdfDocument) return;
|
||||||
|
|
||||||
|
// Cancel any ongoing render task
|
||||||
|
if (this.renderTask) {
|
||||||
|
this.renderTask.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const page = await this.pdfDocument.getPage(this.currentPage);
|
||||||
|
const canvas = this.shadowRoot?.querySelector('canvas') as HTMLCanvasElement;
|
||||||
|
|
||||||
|
if (!canvas) return;
|
||||||
|
|
||||||
|
const context = canvas.getContext('2d');
|
||||||
|
const viewport = page.getViewport({ scale: this.scale });
|
||||||
|
|
||||||
|
// Set canvas dimensions
|
||||||
|
canvas.height = viewport.height;
|
||||||
|
canvas.width = viewport.width;
|
||||||
|
|
||||||
|
// Render PDF page into canvas context
|
||||||
|
const renderContext = {
|
||||||
|
canvasContext: context,
|
||||||
|
viewport: viewport
|
||||||
|
};
|
||||||
|
|
||||||
|
this.renderTask = page.render(renderContext);
|
||||||
|
await this.renderTask.promise;
|
||||||
|
} catch (error) {
|
||||||
|
if (error.name !== 'RenderingCancelledException') {
|
||||||
|
console.error('Error rendering page:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async previousPage() {
|
||||||
|
if (this.currentPage > 1) {
|
||||||
|
this.currentPage--;
|
||||||
|
await this.renderPage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async nextPage() {
|
||||||
|
if (this.currentPage < this.totalPages) {
|
||||||
|
this.currentPage++;
|
||||||
|
await this.renderPage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async zoomIn() {
|
||||||
|
this.scale = Math.min(this.scale * 1.2, 3);
|
||||||
|
await this.renderPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async zoomOut() {
|
||||||
|
this.scale = Math.max(this.scale / 1.2, 0.5);
|
||||||
|
await this.renderPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async resetZoom() {
|
||||||
|
this.scale = 1;
|
||||||
|
await this.renderPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
private downloadPdf() {
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = this.url;
|
||||||
|
a.download = this.fileName;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
}
|
||||||
|
|
||||||
|
private openInNewTab() {
|
||||||
|
window.open(this.url, '_blank');
|
||||||
|
}
|
||||||
|
|
||||||
|
public async disconnectedCallback() {
|
||||||
|
await super.disconnectedCallback();
|
||||||
|
|
||||||
|
// Clean up resize observer
|
||||||
|
if (this.resizeObserver) {
|
||||||
|
this.resizeObserver.disconnect();
|
||||||
|
this.resizeObserver = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cancel any ongoing render task
|
||||||
|
if (this.renderTask) {
|
||||||
|
this.renderTask.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destroy PDF document to free memory
|
||||||
|
if (this.pdfDocument) {
|
||||||
|
this.pdfDocument.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -47,7 +47,7 @@ export class SioRecorder extends DeesElement {
|
|||||||
* Query for the div in our template that will be used for playback.
|
* Query for the div in our template that will be used for playback.
|
||||||
*/
|
*/
|
||||||
@query('#playback')
|
@query('#playback')
|
||||||
private playbackDiv!: HTMLDivElement;
|
private accessor playbackDiv!: HTMLDivElement;
|
||||||
|
|
||||||
static styles = css`
|
static styles = css`
|
||||||
:host {
|
:host {
|
||||||
@@ -104,7 +104,7 @@ export class SioRecorder extends DeesElement {
|
|||||||
* Starts an rrweb recording session that tracks the entire DOM,
|
* Starts an rrweb recording session that tracks the entire DOM,
|
||||||
* including canvases and cross-origin iframes (if permissible).
|
* including canvases and cross-origin iframes (if permissible).
|
||||||
*/
|
*/
|
||||||
private async startRecording(): void {
|
private async startRecording(): Promise<void> {
|
||||||
await this.domtoolsPromise;
|
await this.domtoolsPromise;
|
||||||
this.status = 'recording';
|
this.status = 'recording';
|
||||||
this.events = [];
|
this.events = [];
|
||||||
|
|||||||
@@ -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';
|
import { html } from '@design.estate/dees-element';
|
||||||
|
|
||||||
export const mainpage = () => html` <lele-statusbar></lele-statusbar> `;
|
export const mainpage = () => html`
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
.demo-container {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.demo-section {
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 40px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||||
|
max-width: 1200px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
.component-demo {
|
||||||
|
margin: 20px 0;
|
||||||
|
position: relative;
|
||||||
|
min-height: 700px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div class="demo-container">
|
||||||
|
<div class="demo-section">
|
||||||
|
<h2>Social.io Catalog Components</h2>
|
||||||
|
|
||||||
|
<div class="component-demo">
|
||||||
|
<h3>FAB with Combox Demo</h3>
|
||||||
|
<sio-fab .showCombox=${true}></sio-fab>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
@@ -1,14 +1,10 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"experimentalDecorators": true,
|
|
||||||
"useDefineForClassFields": false,
|
|
||||||
"target": "ES2022",
|
"target": "ES2022",
|
||||||
"module": "NodeNext",
|
"module": "NodeNext",
|
||||||
"moduleResolution": "NodeNext",
|
"moduleResolution": "NodeNext",
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"verbatimModuleSyntax": true,
|
"verbatimModuleSyntax": true,
|
||||||
"baseUrl": ".",
|
|
||||||
"paths": {}
|
|
||||||
},
|
},
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"dist_*/**/*.d.ts"
|
"dist_*/**/*.d.ts"
|
||||||
|
|||||||
Reference in New Issue
Block a user