Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1d71f45309 | |||
| 47f2a64a61 | |||
| 3338261cfa | |||
| 888ec95185 | |||
| 673af0e39c | |||
| dc8774718d | |||
| 9a87888f5a | |||
| d61c3b6643 | |||
| c8554418de | |||
| c1a8a57729 | |||
| 053d0c8e8f |
@@ -17,6 +17,20 @@
|
|||||||
"accessLevel": "public"
|
"accessLevel": "public"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@git.zone/tsbundle": {
|
||||||
|
"bundles": [
|
||||||
|
{
|
||||||
|
"from": "./ts_web/index.ts",
|
||||||
|
"to": "./dist_bundle/bundle.js",
|
||||||
|
"outputMode": "bundle",
|
||||||
|
"bundler": "esbuild",
|
||||||
|
"production": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"@git.zone/tswatch": {
|
||||||
|
"preset": "element"
|
||||||
|
},
|
||||||
"@ship.zone/szci": {
|
"@ship.zone/szci": {
|
||||||
"npmRegistryUrl": "verdaccio.lossless.one",
|
"npmRegistryUrl": "verdaccio.lossless.one",
|
||||||
"npmGlobalTools": []
|
"npmGlobalTools": []
|
||||||
Vendored
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"json.schemas": [
|
"json.schemas": [
|
||||||
{
|
{
|
||||||
"fileMatch": ["/npmextra.json"],
|
"fileMatch": ["/.smartconfig.json"],
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -1,5 +1,47 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-04-02 - 1.7.1 - fix(build)
|
||||||
|
migrate smart config setup and align TypeScript compatibility updates
|
||||||
|
|
||||||
|
- replace npmextra.json with .smartconfig.json and simplify build/watch script usage
|
||||||
|
- update runtime and development dependencies to newer versions
|
||||||
|
- rename browser tests to the Chromium-specific convention expected by the test tooling
|
||||||
|
- add explicit property initializers and non-null assertions to satisfy stricter TypeScript checks
|
||||||
|
|
||||||
|
## 2026-01-02 - 1.7.0 - feat(recorder)
|
||||||
|
lazy-load rrweb and rrweb-player from CDN via SioServiceLibLoader and switch sio-recorder to use the loader
|
||||||
|
|
||||||
|
- Add SioServiceLibLoader service to lazy-load rrweb and rrweb-player (and inject rrweb-player CSS)
|
||||||
|
- Add versions file (CDN_VERSIONS and CDN_BASE) and export loader from ts_web/index.ts
|
||||||
|
- Update sio-recorder to use the loader for recording and playback instead of bundling rrweb/rrweb-player
|
||||||
|
- Remove rrweb, rrweb-player and rrweb-snapshot from dependencies and bump lucide and @design.estate/dees-wcctools versions
|
||||||
|
- Small package.json changes: remove --skiplibcheck from build script and bump @git.zone/tstest devDependency
|
||||||
|
|
||||||
|
## 2025-12-18 - 1.6.1 - fix(sio-combox)
|
||||||
|
tweak dropdown shadow and bind close event on conversation selector
|
||||||
|
|
||||||
|
- Replaced unsafeCSS(shadows.xl) with explicit box-shadow values for the combox dropdown to adjust visual appearance
|
||||||
|
- Added @close listener on <sio-conversation-selector> to call this.close(), enabling the selector to close the combox when it emits a close event
|
||||||
|
- Affected file: ts_web/elements/sio-combox.ts
|
||||||
|
|
||||||
|
## 2025-12-18 - 1.6.0 - feat(conversation-selector)
|
||||||
|
add conversation status badges to conversation selector and include status in sample data
|
||||||
|
|
||||||
|
- Introduce TConversationStatus type and add optional status property to IConversation
|
||||||
|
- Render status badges in sio-conversation-selector with CSS classes and a getBadgeLabel helper
|
||||||
|
- Update sample conversations in sio-combox.ts to include statuses: 'new', 'needs-action', 'waiting', 'resolved'
|
||||||
|
|
||||||
|
## 2025-12-17 - 1.5.0 - feat(combox)
|
||||||
|
Introduce singleton SioCombox attached to document.body with open/close/toggle API and animated show/hide; integrate SioFab to use the singleton and update styles/positioning
|
||||||
|
|
||||||
|
- Add SioCombox.createOnBody() and SioCombox.getInstance() singletons
|
||||||
|
- Add isOpen state, open(), close(), toggle(), getIsOpen() and emit opened/closed/close events
|
||||||
|
- Move combox out of the FAB shadow DOM — attach to body and position fixed bottom-right with z-index and enter/exit transitions
|
||||||
|
- Update mobile layout to full-screen sizing and adjust transform origin for phablet
|
||||||
|
- Update SioFab to create the singleton on firstUpdated(), listen for close events, and toggle the singleton instead of rendering it inside the FAB
|
||||||
|
- Remove previous in-FAB combox container markup/CSS and hasShownOnce logic
|
||||||
|
- Minor visual/UX improvements: scale/opacity transitions, pointer-events control, and positioning variables for consistent behavior
|
||||||
|
|
||||||
## 2025-12-17 - 1.4.1 - fix(ui)
|
## 2025-12-17 - 1.4.1 - fix(ui)
|
||||||
handle on-screen keyboard visibility to adjust layout and prevent inputs from being obscured
|
handle on-screen keyboard visibility to adjust layout and prevent inputs from being obscured
|
||||||
|
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
<!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,4 +1,4 @@
|
|||||||
Copyright (c) 2020 Lossless GmbH (hello@lossless.com)
|
Copyright (c) 2020 Task Venture Capital GmbH (hello@task.vc)
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
+14
-18
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@social.io/catalog",
|
"name": "@social.io/catalog",
|
||||||
"version": "1.4.1",
|
"version": "1.7.1",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "catalog for social.io",
|
"description": "catalog for social.io",
|
||||||
"main": "dist_ts_web/index.js",
|
"main": "dist_ts_web/index.js",
|
||||||
@@ -8,32 +8,28 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "tstest test/",
|
"test": "tstest test/",
|
||||||
"build": "tsbuild tsfolders --allowimplicitany --skiplibcheck && tsbundle element --production",
|
"build": "tsbuild tsfolders --allowimplicitany --skiplibcheck && tsbundle",
|
||||||
"watch": "tswatch element",
|
"watch": "tswatch",
|
||||||
"buildDocs": "tsdoc"
|
"buildDocs": "tsdoc"
|
||||||
},
|
},
|
||||||
"author": "Lossless GmbH",
|
"author": "Lossless GmbH",
|
||||||
"license": "UNLICENSED",
|
"license": "UNLICENSED",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@design.estate/dees-domtools": "^2.3.6",
|
"@design.estate/dees-domtools": "^2.5.1",
|
||||||
"@design.estate/dees-element": "^2.1.3",
|
"@design.estate/dees-element": "^2.2.3",
|
||||||
"@design.estate/dees-wcctools": "^2.0.1",
|
"@design.estate/dees-wcctools": "^3.8.0",
|
||||||
"@losslessone_private/loint-pubapi": "^1.0.14",
|
|
||||||
"@social.io/interfaces": "^1.2.1",
|
"@social.io/interfaces": "^1.2.1",
|
||||||
"lucide": "^0.561.0",
|
"lucide": "^1.6.0"
|
||||||
"rrweb": "2.0.0-alpha.4",
|
|
||||||
"rrweb-player": "1.0.0-alpha.4",
|
|
||||||
"rrweb-snapshot": "2.0.0-alpha.4"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@git.zone/tsbuild": "^4.0.2",
|
"@git.zone/tsbuild": "^4.4.0",
|
||||||
"@git.zone/tsbundle": "^2.6.3",
|
"@git.zone/tsbundle": "^2.10.0",
|
||||||
"@git.zone/tsrun": "^2.0.1",
|
"@git.zone/tsrun": "^2.0.2",
|
||||||
"@git.zone/tstest": "^3.1.3",
|
"@git.zone/tstest": "^3.6.0",
|
||||||
"@git.zone/tswatch": "^2.3.13",
|
"@git.zone/tswatch": "^3.3.2",
|
||||||
"@push.rocks/projectinfo": "^5.0.2",
|
"@push.rocks/projectinfo": "^5.0.2",
|
||||||
"@push.rocks/smartenv": "^6.0.0",
|
"@push.rocks/smartenv": "^6.0.0",
|
||||||
"@types/node": "^25.0.3"
|
"@types/node": "^25.5.0"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"ts/**/*",
|
"ts/**/*",
|
||||||
@@ -44,7 +40,7 @@
|
|||||||
"dist_ts_web/**/*",
|
"dist_ts_web/**/*",
|
||||||
"assets/**/*",
|
"assets/**/*",
|
||||||
"cli.js",
|
"cli.js",
|
||||||
"npmextra.json",
|
".smartconfig.json",
|
||||||
"readme.md"
|
"readme.md"
|
||||||
],
|
],
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
|
|||||||
Generated
+2710
-2745
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
# @social.io/catalog
|
# @social.io/catalog
|
||||||
|
|
||||||
A modern, beautifully designed UI component library for building conversational interfaces and support chat experiences. Built with Lit Element and TypeScript.
|
A modern, beautifully crafted UI component library for building conversational interfaces and support chat experiences. Built with [Lit Element](https://lit.dev/) and TypeScript, featuring a shadcn-inspired design system with full dark mode support. 🚀
|
||||||
|
|
||||||
## Issue Reporting and Security
|
## Issue Reporting and Security
|
||||||
|
|
||||||
@@ -8,20 +8,19 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
|
|||||||
|
|
||||||
## 🎯 Features
|
## 🎯 Features
|
||||||
|
|
||||||
- **Complete Chat UI** - Ready-to-use conversation components with message threads, typing indicators, and attachments
|
- **Complete Chat UI** — Ready-to-use conversation components with message threads, typing indicators, and file attachments
|
||||||
- **Floating Action Button** - Eye-catching FAB with smooth animations for triggering the chat interface
|
- **Floating Action Button** — Eye-catching FAB with smooth gradient animations and glow effects
|
||||||
- **PDF Viewer** - Built-in PDF rendering with zoom, pagination, and download capabilities
|
- **PDF Viewer** — Built-in PDF rendering with zoom, pagination, and download capabilities
|
||||||
- **Image Lightbox** - Full-featured lightbox with zoom, pan, and keyboard navigation
|
- **Image Lightbox** — Full-featured lightbox with zoom, pan, and keyboard navigation
|
||||||
- **Modern Design Tokens** - Consistent styling with customizable colors, spacing, typography, and shadows
|
- **Design Token System** — Comprehensive tokens for colors, spacing, typography, shadows, and animations
|
||||||
- **Dark Mode Ready** - Full light/dark theme support out of the box
|
- **Dark Mode Ready** — Full light/dark theme support out of the box via `bdTheme()` helper
|
||||||
- **Accessibility** - Keyboard navigation and proper ARIA attributes
|
- **Accessibility** — Keyboard navigation and proper ARIA attributes throughout
|
||||||
- **TypeScript First** - Full type definitions for all components
|
- **TypeScript First** — Full type definitions and IntelliSense for all components
|
||||||
|
- **Lazy-loaded Libraries** — rrweb for session recording is loaded on-demand from CDN via `SioServiceLibLoader`
|
||||||
|
|
||||||
## 📦 Installation
|
## 📦 Installation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install @social.io/catalog
|
|
||||||
# or
|
|
||||||
pnpm add @social.io/catalog
|
pnpm add @social.io/catalog
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -30,8 +29,7 @@ pnpm add @social.io/catalog
|
|||||||
```typescript
|
```typescript
|
||||||
import { SioFab, SioCombox } from '@social.io/catalog';
|
import { SioFab, SioCombox } from '@social.io/catalog';
|
||||||
|
|
||||||
// Components auto-register as custom elements
|
// Components auto-register as custom elements — just drop them into your HTML:
|
||||||
// Just use them in your HTML:
|
|
||||||
```
|
```
|
||||||
|
|
||||||
```html
|
```html
|
||||||
@@ -46,54 +44,74 @@ import { SioFab, SioCombox } from '@social.io/catalog';
|
|||||||
|
|
||||||
### Core Components
|
### Core Components
|
||||||
|
|
||||||
| Component | Description |
|
| Component | Tag | Description |
|
||||||
|-----------|-------------|
|
|-----------|-----|-------------|
|
||||||
| `<sio-fab>` | Floating action button with animated chat icon |
|
| `SioFab` | `<sio-fab>` | Floating action button with animated gradient, glow effects, and pulse animation |
|
||||||
| `<sio-combox>` | Complete chat interface with conversation list and message view |
|
| `SioCombox` | `<sio-combox>` | Complete chat interface combining conversation list, message view, and input |
|
||||||
| `<sio-button>` | Styled button with variants (primary, secondary, destructive, outline, ghost) |
|
| `SioButton` | `<sio-button>` | Styled button with variants: `primary`, `secondary`, `destructive`, `outline`, `ghost` |
|
||||||
| `<sio-icon>` | Lucide icon wrapper with size and color customization |
|
| `SioIcon` | `<sio-icon>` | [Lucide](https://lucide.dev/) icon wrapper with size, color, and strokeWidth customization |
|
||||||
| `<sio-dropdown-menu>` | Animated dropdown menu with keyboard support |
|
| `SioDropdownMenu` | `<sio-dropdown-menu>` | Animated dropdown menu with keyboard support and auto-positioning |
|
||||||
|
|
||||||
### Conversation Components
|
### Conversation Components
|
||||||
|
|
||||||
| Component | Description |
|
| Component | Tag | Description |
|
||||||
|-----------|-------------|
|
|-----------|-----|-------------|
|
||||||
| `<sio-conversation-selector>` | Searchable list of conversations with unread indicators |
|
| `SioConversationSelector` | `<sio-conversation-selector>` | Searchable list of conversations with unread indicators and status badges |
|
||||||
| `<sio-conversation-view>` | Message thread with typing indicators and file attachments |
|
| `SioConversationView` | `<sio-conversation-view>` | Message thread with typing indicators, timestamps, and file attachment support |
|
||||||
| `<sio-message-input>` | Auto-expanding textarea with file upload |
|
| `SioMessageInput` | `<sio-message-input>` | Auto-expanding textarea with file upload capabilities |
|
||||||
|
|
||||||
### Media Components
|
### Media Components
|
||||||
|
|
||||||
| Component | Description |
|
| Component | Tag | Description |
|
||||||
|-----------|-------------|
|
|-----------|-----|-------------|
|
||||||
| `<sio-image-lightbox>` | Fullscreen image viewer with zoom and pan |
|
| `SioImageLightbox` | `<sio-image-lightbox>` | Fullscreen image viewer with zoom, pan, and keyboard navigation; also handles PDF files |
|
||||||
| `<sio-pdf-viewer>` | PDF renderer with page navigation and zoom controls |
|
| `SioPdfViewer` | `<sio-pdf-viewer>` | PDF renderer with page navigation, zoom controls, and download button |
|
||||||
|
|
||||||
### Utility Components
|
### Utility Components
|
||||||
|
|
||||||
| Component | Description |
|
| Component | Tag | Description |
|
||||||
|-----------|-------------|
|
|-----------|-----|-------------|
|
||||||
| `<sio-recorder>` | Session recording using rrweb |
|
| `SioRecorder` | `<sio-recorder>` | Session recording and playback using [rrweb](https://www.rrweb.io/), lazy-loaded from CDN |
|
||||||
|
|
||||||
## 💅 Styling & Theming
|
### Services
|
||||||
|
|
||||||
The library uses CSS custom properties for theming. The design system includes:
|
| Export | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `SioServiceLibLoader` | CDN-based lazy loader for rrweb and rrweb-player libraries |
|
||||||
|
|
||||||
- **Colors** - Primary, secondary, accent, destructive, muted, and semantic colors
|
## 💅 Design System
|
||||||
- **Typography** - System font stack with size and weight variants
|
|
||||||
- **Spacing** - Consistent spacing scale (0.5rem increments)
|
The library ships a comprehensive, shadcn/ui-inspired design token system. All tokens are exported so you can use them in your own components.
|
||||||
- **Radius** - Border radius tokens from sm to full
|
|
||||||
- **Shadows** - Elevation system from sm to 2xl
|
### Colors
|
||||||
- **Transitions** - Smooth animation presets
|
|
||||||
|
```typescript
|
||||||
|
import { colors, bdTheme } from '@social.io/catalog/elements/00colors.js';
|
||||||
|
```
|
||||||
|
|
||||||
|
Includes `primary`, `secondary`, `accent`, `destructive`, `muted`, `success`, `background`, `foreground`, `card`, `popover`, `border`, `input`, `ring`, and 5 chart colors — each with light and dark variants.
|
||||||
|
|
||||||
|
### Spacing, Radius, Shadows & More
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { spacing, radius, shadows, transitions, sizes, zIndex, breakpoints } from '@social.io/catalog/elements/00tokens.js';
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Spacing** — Tailwind-compatible scale from `0` to `96` (0–384px)
|
||||||
|
- **Radius** — From `sm` (2px) through `full` (9999px)
|
||||||
|
- **Shadows** — Elevation system from `sm` to `2xl`, plus theme-aware `card` and `dropdown` shadows
|
||||||
|
- **Transitions** — Presets (`all`, `colors`, `opacity`, `shadow`, `transform`) with timing functions
|
||||||
|
- **Z-index** — Semantic layers: `dropdown`, `sticky`, `fixed`, `modal`, `popover`, `tooltip`
|
||||||
|
- **Breakpoints** — `sm` (640px) through `2xl` (1536px)
|
||||||
|
|
||||||
### Dark Mode
|
### Dark Mode
|
||||||
|
|
||||||
Dark mode is automatically supported. The components use `bdTheme()` helper that switches between light and dark values:
|
Dark mode is automatically supported. Use the `bdTheme()` helper for theme-aware values:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { bdTheme } from '@social.io/catalog';
|
import { bdTheme } from '@social.io/catalog/elements/00colors.js';
|
||||||
|
|
||||||
// Usage in styles
|
// Returns the correct value based on the current theme
|
||||||
css`
|
css`
|
||||||
background: ${bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 10%)')};
|
background: ${bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 10%)')};
|
||||||
`
|
`
|
||||||
@@ -101,40 +119,55 @@ css`
|
|||||||
|
|
||||||
## 📖 Usage Examples
|
## 📖 Usage Examples
|
||||||
|
|
||||||
### Basic Chat FAB
|
### 💬 Chat FAB
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<sio-fab></sio-fab>
|
<sio-fab></sio-fab>
|
||||||
```
|
```
|
||||||
|
|
||||||
The FAB opens a complete chat interface when clicked. It includes:
|
The FAB positions itself at the bottom-right corner and opens a complete chat interface when clicked. It features:
|
||||||
- Keyboard shortcut (Ctrl+S) to toggle
|
- Animated gradient background (indigo → violet → purple)
|
||||||
|
- Glow effect on hover
|
||||||
- Smooth scale and pulse animations
|
- Smooth scale and pulse animations
|
||||||
- Gradient background with glow effects
|
|
||||||
|
|
||||||
### Custom Button Variants
|
### 🔘 Buttons
|
||||||
|
|
||||||
```html
|
```html
|
||||||
|
<sio-button>Default</sio-button>
|
||||||
<sio-button type="primary">Submit</sio-button>
|
<sio-button type="primary">Submit</sio-button>
|
||||||
<sio-button type="destructive">Delete</sio-button>
|
<sio-button type="destructive">Delete</sio-button>
|
||||||
<sio-button type="outline">Cancel</sio-button>
|
<sio-button type="outline">Cancel</sio-button>
|
||||||
<sio-button type="ghost" size="sm">
|
<sio-button type="ghost" size="sm">
|
||||||
<sio-icon icon="settings"></sio-icon>
|
<sio-icon icon="settings"></sio-icon>
|
||||||
</sio-button>
|
</sio-button>
|
||||||
|
<sio-button size="lg">Large</sio-button>
|
||||||
|
<sio-button disabled>Disabled</sio-button>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Image Lightbox
|
Sizes: `sm`, `default`, `lg`. Status: `normal`, `pending`, `success`, `error`.
|
||||||
|
|
||||||
|
### 🖼️ Image Lightbox
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const lightbox = document.querySelector('sio-image-lightbox');
|
const lightbox = document.querySelector('sio-image-lightbox');
|
||||||
lightbox.open({
|
|
||||||
|
// Open with an image
|
||||||
|
await lightbox.open({
|
||||||
url: 'https://example.com/photo.jpg',
|
url: 'https://example.com/photo.jpg',
|
||||||
name: 'My Photo',
|
name: 'My Photo',
|
||||||
size: 1024000
|
size: 1024000
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Or open with a PDF
|
||||||
|
await lightbox.open({
|
||||||
|
url: 'https://example.com/document.pdf',
|
||||||
|
name: 'report.pdf',
|
||||||
|
type: 'application/pdf',
|
||||||
|
size: 565000
|
||||||
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
### PDF Viewer
|
### 📄 PDF Viewer
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<sio-pdf-viewer
|
<sio-pdf-viewer
|
||||||
@@ -143,12 +176,13 @@ lightbox.open({
|
|||||||
></sio-pdf-viewer>
|
></sio-pdf-viewer>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Dropdown Menu
|
### ☰ Dropdown Menu
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<sio-dropdown-menu
|
<sio-dropdown-menu
|
||||||
.items=${[
|
.items=${[
|
||||||
{ id: 'edit', label: 'Edit', icon: 'pencil' },
|
{ id: 'edit', label: 'Edit', icon: 'pencil' },
|
||||||
|
{ id: 'divider', label: '', divider: true },
|
||||||
{ id: 'delete', label: 'Delete', icon: 'trash', destructive: true }
|
{ id: 'delete', label: 'Delete', icon: 'trash', destructive: true }
|
||||||
]}
|
]}
|
||||||
@item-selected=${(e) => console.log('Selected:', e.detail.item)}
|
@item-selected=${(e) => console.log('Selected:', e.detail.item)}
|
||||||
@@ -159,6 +193,17 @@ lightbox.open({
|
|||||||
</sio-dropdown-menu>
|
</sio-dropdown-menu>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 🎨 Icons
|
||||||
|
|
||||||
|
Uses the full [Lucide](https://lucide.dev/) icon set. Pass kebab-case icon names:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<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>
|
||||||
|
```
|
||||||
|
|
||||||
## 🔧 Development
|
## 🔧 Development
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -168,23 +213,26 @@ pnpm install
|
|||||||
# Start development server with hot reload
|
# Start development server with hot reload
|
||||||
pnpm watch
|
pnpm watch
|
||||||
|
|
||||||
# Run tests
|
|
||||||
pnpm test
|
|
||||||
|
|
||||||
# Build for production
|
# Build for production
|
||||||
pnpm build
|
pnpm build
|
||||||
|
|
||||||
|
# Run tests (Chromium browser tests)
|
||||||
|
pnpm test
|
||||||
```
|
```
|
||||||
|
|
||||||
## 📚 Dependencies
|
## 📚 Dependencies
|
||||||
|
|
||||||
- **@design.estate/dees-element** - Lit Element base with utilities
|
| Package | Purpose |
|
||||||
- **@design.estate/dees-domtools** - DOM manipulation helpers
|
|---------|---------|
|
||||||
- **lucide** - Beautiful open-source icons
|
| [`@design.estate/dees-element`](https://www.npmjs.com/package/@design.estate/dees-element) | Lit Element base class with theming utilities |
|
||||||
- **rrweb** - Session recording/replay (for recorder component)
|
| [`@design.estate/dees-domtools`](https://www.npmjs.com/package/@design.estate/dees-domtools) | DOM manipulation and convenience helpers |
|
||||||
|
| [`@design.estate/dees-wcctools`](https://www.npmjs.com/package/@design.estate/dees-wcctools) | Web component catalog tools |
|
||||||
|
| [`lucide`](https://lucide.dev/) | Beautiful, consistent open-source icon set |
|
||||||
|
| [`rrweb`](https://www.rrweb.io/) | Session recording/replay (lazy-loaded from CDN) |
|
||||||
|
|
||||||
## License and Legal Information
|
## 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.
|
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.
|
**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.
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@social.io/catalog',
|
name: '@social.io/catalog',
|
||||||
version: '1.4.1',
|
version: '1.7.1',
|
||||||
description: 'catalog for social.io'
|
description: 'catalog for social.io'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,8 +36,29 @@ declare global {
|
|||||||
export class SioCombox extends DeesElement {
|
export class SioCombox extends DeesElement {
|
||||||
public static demo = () => html` <sio-combox></sio-combox> `;
|
public static demo = () => html` <sio-combox></sio-combox> `;
|
||||||
|
|
||||||
|
// Singleton instance
|
||||||
|
private static instance: SioCombox | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates and appends a singleton combox to document.body
|
||||||
|
*/
|
||||||
|
public static createOnBody(): SioCombox {
|
||||||
|
if (!SioCombox.instance) {
|
||||||
|
SioCombox.instance = new SioCombox();
|
||||||
|
document.body.appendChild(SioCombox.instance);
|
||||||
|
}
|
||||||
|
return SioCombox.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the singleton instance if it exists
|
||||||
|
*/
|
||||||
|
public static getInstance(): SioCombox | null {
|
||||||
|
return SioCombox.instance;
|
||||||
|
}
|
||||||
|
|
||||||
@property({ type: Object })
|
@property({ type: Object })
|
||||||
public accessor referenceObject: HTMLElement;
|
public accessor referenceObject: HTMLElement = null as any;
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
private accessor selectedConversationId: string | null = null;
|
private accessor selectedConversationId: string | null = null;
|
||||||
@@ -45,6 +66,9 @@ export class SioCombox extends DeesElement {
|
|||||||
@state()
|
@state()
|
||||||
private accessor isKeyboardVisible: boolean = false;
|
private accessor isKeyboardVisible: boolean = false;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private accessor isOpen: boolean = false;
|
||||||
|
|
||||||
private keyboardBlurTimeout?: number;
|
private keyboardBlurTimeout?: number;
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
@@ -55,24 +79,28 @@ export class SioCombox extends DeesElement {
|
|||||||
lastMessage: 'Thanks for your help with the login issue!',
|
lastMessage: 'Thanks for your help with the login issue!',
|
||||||
time: '2 min ago',
|
time: '2 min ago',
|
||||||
unread: true,
|
unread: true,
|
||||||
|
status: 'new',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '2',
|
id: '2',
|
||||||
title: 'Billing Question',
|
title: 'Billing Question',
|
||||||
lastMessage: 'I need help understanding my invoice',
|
lastMessage: 'I need help understanding my invoice',
|
||||||
time: '1 hour ago',
|
time: '1 hour ago',
|
||||||
|
status: 'needs-action',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '3',
|
id: '3',
|
||||||
title: 'Feature Request',
|
title: 'Feature Request',
|
||||||
lastMessage: 'That would be great! Looking forward to it',
|
lastMessage: 'That would be great! Looking forward to it',
|
||||||
time: 'Yesterday',
|
time: 'Yesterday',
|
||||||
|
status: 'waiting',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '4',
|
id: '4',
|
||||||
title: 'General Inquiry',
|
title: 'General Inquiry',
|
||||||
lastMessage: 'Thank you for the information',
|
lastMessage: 'Thank you for the information',
|
||||||
time: '2 days ago',
|
time: '2 days ago',
|
||||||
|
status: 'resolved',
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -174,6 +202,48 @@ export class SioCombox extends DeesElement {
|
|||||||
this.removeAttribute('keyboard-visible');
|
this.removeAttribute('keyboard-visible');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (changedProperties.has('isOpen')) {
|
||||||
|
if (this.isOpen) {
|
||||||
|
this.classList.add('open');
|
||||||
|
this.dispatchEvent(new CustomEvent('opened', { bubbles: true, composed: true }));
|
||||||
|
} else {
|
||||||
|
this.classList.remove('open');
|
||||||
|
this.dispatchEvent(new CustomEvent('closed', { bubbles: true, composed: true }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the combox
|
||||||
|
*/
|
||||||
|
public open() {
|
||||||
|
this.isOpen = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the combox
|
||||||
|
*/
|
||||||
|
public close() {
|
||||||
|
this.isOpen = false;
|
||||||
|
this.dispatchEvent(new CustomEvent('close', { bubbles: true, composed: true }));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles the combox open/closed state
|
||||||
|
*/
|
||||||
|
public toggle() {
|
||||||
|
if (this.isOpen) {
|
||||||
|
this.close();
|
||||||
|
} else {
|
||||||
|
this.open();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the combox is currently open
|
||||||
|
*/
|
||||||
|
public getIsOpen(): boolean {
|
||||||
|
return this.isOpen;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static styles = [
|
public static styles = [
|
||||||
@@ -181,31 +251,31 @@ export class SioCombox extends DeesElement {
|
|||||||
css`
|
css`
|
||||||
:host {
|
:host {
|
||||||
display: block;
|
display: block;
|
||||||
|
position: fixed;
|
||||||
|
bottom: 100px;
|
||||||
|
right: 20px;
|
||||||
height: 600px;
|
height: 600px;
|
||||||
width: 800px;
|
width: 800px;
|
||||||
background: ${bdTheme('background')};
|
background: ${bdTheme('background')};
|
||||||
border-radius: ${unsafeCSS(radius['2xl'])};
|
border-radius: ${unsafeCSS(radius['2xl'])};
|
||||||
border: 1px solid ${bdTheme('border')};
|
border: 1px solid ${bdTheme('border')};
|
||||||
box-shadow: ${unsafeCSS(shadows.xl)};
|
box-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.35), 0 12px 24px -8px rgb(0 0 0 / 0.15);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
font-family: ${unsafeCSS(fontFamilies.sans)};
|
font-family: ${unsafeCSS(fontFamilies.sans)};
|
||||||
position: relative;
|
|
||||||
transform-origin: bottom right;
|
transform-origin: bottom right;
|
||||||
|
z-index: 10001;
|
||||||
|
|
||||||
|
/* Hidden by default */
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transform: scale(0.95) translateY(10px);
|
||||||
|
transition: opacity 200ms ease, transform 200ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
:host(.animate-in) {
|
:host(.open) {
|
||||||
animation: scaleIn 300ms cubic-bezier(0.34, 1.56, 0.64, 1);
|
opacity: 1;
|
||||||
}
|
pointer-events: all;
|
||||||
|
transform: scale(1) translateY(0);
|
||||||
@keyframes scaleIn {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: scale(0.9) translateY(10px);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: scale(1) translateY(0);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:host::before {
|
:host::before {
|
||||||
@@ -243,9 +313,19 @@ export class SioCombox extends DeesElement {
|
|||||||
// Mobile responsive layout - full screen with sliding mechanics
|
// Mobile responsive layout - full screen with sliding mechanics
|
||||||
cssManager.cssForPhablet(css`
|
cssManager.cssForPhablet(css`
|
||||||
:host {
|
:host {
|
||||||
width: 100%;
|
top: 0;
|
||||||
height: 100%;
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
height: 100dvh;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
|
transform-origin: center center;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host(.open) {
|
||||||
|
transform: scale(1) translateY(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
:host::before {
|
:host::before {
|
||||||
@@ -321,6 +401,7 @@ export class SioCombox extends DeesElement {
|
|||||||
.conversations=${this.conversations}
|
.conversations=${this.conversations}
|
||||||
.selectedConversationId=${this.selectedConversationId}
|
.selectedConversationId=${this.selectedConversationId}
|
||||||
@conversation-selected=${this.handleConversationSelected}
|
@conversation-selected=${this.handleConversationSelected}
|
||||||
|
@close=${() => this.close()}
|
||||||
></sio-conversation-selector>
|
></sio-conversation-selector>
|
||||||
|
|
||||||
<sio-conversation-view
|
<sio-conversation-view
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ import { spacing, radius, shadows, transitions } from './00tokens.js';
|
|||||||
import { fontFamilies, typography } from './00fonts.js';
|
import { fontFamilies, typography } from './00fonts.js';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
|
export type TConversationStatus = 'new' | 'waiting' | 'needs-action' | 'resolved';
|
||||||
|
|
||||||
export interface IConversation {
|
export interface IConversation {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
@@ -23,6 +25,7 @@ export interface IConversation {
|
|||||||
time: string;
|
time: string;
|
||||||
unread?: boolean;
|
unread?: boolean;
|
||||||
avatar?: string;
|
avatar?: string;
|
||||||
|
status?: TConversationStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
@@ -232,6 +235,38 @@ export class SioConversationSelector extends DeesElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 2px ${unsafeCSS(spacing["2"])};
|
||||||
|
font-size: 0.625rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.025em;
|
||||||
|
border-radius: ${unsafeCSS(radius.full)};
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge.new {
|
||||||
|
background: ${bdTheme('primary')}20;
|
||||||
|
color: ${bdTheme('primary')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge.waiting {
|
||||||
|
background: ${bdTheme('muted')};
|
||||||
|
color: ${bdTheme('mutedForeground')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge.needs-action {
|
||||||
|
background: hsl(38 92% 50% / 0.15);
|
||||||
|
color: hsl(38 92% 40%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge.resolved {
|
||||||
|
background: ${bdTheme('success')}20;
|
||||||
|
color: ${bdTheme('success')};
|
||||||
|
}
|
||||||
|
|
||||||
.empty-state {
|
.empty-state {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -331,6 +366,7 @@ export class SioConversationSelector extends DeesElement {
|
|||||||
<span class="conversation-title">
|
<span class="conversation-title">
|
||||||
${conv.title}
|
${conv.title}
|
||||||
${conv.unread ? html`<span class="unread-dot"></span>` : ''}
|
${conv.unread ? html`<span class="unread-dot"></span>` : ''}
|
||||||
|
${conv.status ? html`<span class="badge ${conv.status}">${this.getBadgeLabel(conv.status)}</span>` : ''}
|
||||||
</span>
|
</span>
|
||||||
<span class="conversation-time">${conv.time}</span>
|
<span class="conversation-time">${conv.time}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -389,4 +425,14 @@ export class SioConversationSelector extends DeesElement {
|
|||||||
composed: true
|
composed: true
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getBadgeLabel(status: TConversationStatus): string {
|
||||||
|
const labels: Record<TConversationStatus, string> = {
|
||||||
|
'new': 'New',
|
||||||
|
'waiting': 'Waiting',
|
||||||
|
'needs-action': 'Action',
|
||||||
|
'resolved': 'Resolved',
|
||||||
|
};
|
||||||
|
return labels[status];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -57,9 +57,9 @@ export class SioDropdownMenu extends DeesElement {
|
|||||||
@state()
|
@state()
|
||||||
private accessor isOpen: boolean = false;
|
private accessor isOpen: boolean = false;
|
||||||
|
|
||||||
private documentClickHandler: (e: MouseEvent) => void;
|
private documentClickHandler!: (e: MouseEvent) => void;
|
||||||
private scrollHandler: () => void;
|
private scrollHandler!: () => void;
|
||||||
private resizeHandler: () => void;
|
private resizeHandler!: () => void;
|
||||||
|
|
||||||
public static styles = [
|
public static styles = [
|
||||||
cssManager.defaultStyles,
|
cssManager.defaultStyles,
|
||||||
@@ -160,27 +160,6 @@ export class SioDropdownMenu extends DeesElement {
|
|||||||
background: ${bdTheme('border')};
|
background: ${bdTheme('border')};
|
||||||
margin: ${unsafeCSS(spacing["1"])} 0;
|
margin: ${unsafeCSS(spacing["1"])} 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Mobile adjustments */
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
.dropdown {
|
|
||||||
position: fixed;
|
|
||||||
top: auto !important;
|
|
||||||
left: ${unsafeCSS(spacing["4"])} !important;
|
|
||||||
right: ${unsafeCSS(spacing["4"])};
|
|
||||||
bottom: ${unsafeCSS(spacing["4"])};
|
|
||||||
width: auto;
|
|
||||||
transform-origin: bottom center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown.open {
|
|
||||||
transform: translateY(0) scale(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown:not(.open) {
|
|
||||||
transform: translateY(10px) scale(0.95);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
+15
-76
@@ -32,9 +32,6 @@ export class SioFab extends DeesElement {
|
|||||||
@property({ type: Boolean })
|
@property({ type: Boolean })
|
||||||
public accessor showCombox = false;
|
public accessor showCombox = false;
|
||||||
|
|
||||||
@state()
|
|
||||||
private accessor hasShownOnce = false;
|
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
private accessor shouldPulse = false;
|
private accessor shouldPulse = false;
|
||||||
|
|
||||||
@@ -62,7 +59,6 @@ export class SioFab extends DeesElement {
|
|||||||
--fab-gradient-hover-end: #c026d3;
|
--fab-gradient-hover-end: #c026d3;
|
||||||
--fab-shadow-color: rgba(139, 92, 246, 0.25);
|
--fab-shadow-color: rgba(139, 92, 246, 0.25);
|
||||||
--fab-size: 60px;
|
--fab-size: 60px;
|
||||||
--fab-combox-offset: calc(var(--fab-size) + ${unsafeCSS(spacing["4"])});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#mainbox {
|
#mainbox {
|
||||||
@@ -201,63 +197,13 @@ export class SioFab extends DeesElement {
|
|||||||
#mainbox .icon.close sio-icon {
|
#mainbox .icon.close sio-icon {
|
||||||
transform: scale(1);
|
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 and full-screen combox
|
// Mobile responsive styles - smaller FAB
|
||||||
cssManager.cssForPhablet(css`
|
cssManager.cssForPhablet(css`
|
||||||
:host {
|
:host {
|
||||||
--fab-size: 48px;
|
--fab-size: 48px;
|
||||||
bottom: 16px;
|
bottom: 16px;
|
||||||
right: 16px;
|
right: 16px;
|
||||||
will-change: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
#comboxContainer {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
bottom: auto;
|
|
||||||
right: auto;
|
|
||||||
width: 100vw;
|
|
||||||
height: 100vh;
|
|
||||||
height: 100dvh;
|
|
||||||
}
|
|
||||||
|
|
||||||
#comboxContainer sio-combox {
|
|
||||||
bottom: 0;
|
|
||||||
right: 0;
|
|
||||||
transform: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#comboxContainer.show sio-combox {
|
|
||||||
transform: none;
|
|
||||||
}
|
}
|
||||||
`),
|
`),
|
||||||
];
|
];
|
||||||
@@ -276,11 +222,6 @@ export class SioFab extends DeesElement {
|
|||||||
<sio-icon icon="x" size="20"></sio-icon>
|
<sio-icon icon="x" size="20"></sio-icon>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="comboxContainer" class="${this.showCombox ? 'show' : ''}">
|
|
||||||
${this.showCombox || this.hasShownOnce ? html`
|
|
||||||
<sio-combox @close=${() => this.showCombox = false}></sio-combox>
|
|
||||||
` : ''}
|
|
||||||
</div>
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -288,12 +229,12 @@ export class SioFab extends DeesElement {
|
|||||||
* toggles the combox
|
* toggles the combox
|
||||||
*/
|
*/
|
||||||
public async toggleCombox() {
|
public async toggleCombox() {
|
||||||
console.log('toggle combox');
|
const combox = SioCombox.getInstance();
|
||||||
const wasOpen = this.showCombox;
|
if (combox) {
|
||||||
this.showCombox = !this.showCombox;
|
const wasOpen = combox.getIsOpen();
|
||||||
if (this.showCombox) {
|
combox.toggle();
|
||||||
this.hasShownOnce = true;
|
this.showCombox = combox.getIsOpen();
|
||||||
if (!wasOpen) {
|
if (this.showCombox && !wasOpen) {
|
||||||
this.shouldPulse = true;
|
this.shouldPulse = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -303,6 +244,14 @@ export class SioFab extends DeesElement {
|
|||||||
super.firstUpdated(args);
|
super.firstUpdated(args);
|
||||||
const domtools = await this.domtoolsPromise;
|
const domtools = await this.domtoolsPromise;
|
||||||
|
|
||||||
|
// Create the singleton combox on body
|
||||||
|
const combox = SioCombox.createOnBody();
|
||||||
|
|
||||||
|
// Listen for close events
|
||||||
|
combox.addEventListener('close', () => {
|
||||||
|
this.showCombox = false;
|
||||||
|
});
|
||||||
|
|
||||||
// Set up keyboard shortcut
|
// Set up keyboard shortcut
|
||||||
domtools.keyboard
|
domtools.keyboard
|
||||||
.on([domtools.keyboard.keyEnum.Ctrl, domtools.keyboard.keyEnum.S])
|
.on([domtools.keyboard.keyEnum.Ctrl, domtools.keyboard.keyEnum.S])
|
||||||
@@ -322,15 +271,5 @@ export class SioFab extends DeesElement {
|
|||||||
this.classList.remove('combox-open');
|
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export class SioIcon extends DeesElement {
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
@property({ type: String })
|
@property({ type: String })
|
||||||
public accessor icon: string;
|
public accessor icon: string = '';
|
||||||
|
|
||||||
@property({ type: Number })
|
@property({ type: Number })
|
||||||
public accessor size: number = 24;
|
public accessor size: number = 24;
|
||||||
@@ -140,7 +140,7 @@ export class SioIcon extends DeesElement {
|
|||||||
|
|
||||||
// Limit cache size
|
// Limit cache size
|
||||||
if (SioIcon.iconCache.size > SioIcon.MAX_CACHE_SIZE) {
|
if (SioIcon.iconCache.size > SioIcon.MAX_CACHE_SIZE) {
|
||||||
const firstKey = SioIcon.iconCache.keys().next().value;
|
const firstKey = SioIcon.iconCache.keys().next().value!;
|
||||||
SioIcon.iconCache.delete(firstKey);
|
SioIcon.iconCache.delete(firstKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -523,7 +523,7 @@ export class SioPdfViewer extends DeesElement {
|
|||||||
this.renderTask = page.render(renderContext);
|
this.renderTask = page.render(renderContext);
|
||||||
await this.renderTask.promise;
|
await this.renderTask.promise;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.name !== 'RenderingCancelledException') {
|
if ((error as any).name !== 'RenderingCancelledException') {
|
||||||
console.error('Error rendering page:', error);
|
console.error('Error rendering page:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,11 +6,7 @@ import {
|
|||||||
property,
|
property,
|
||||||
query,
|
query,
|
||||||
} from '@design.estate/dees-element';
|
} from '@design.estate/dees-element';
|
||||||
import * as rrwebMod from 'rrweb';
|
import { SioServiceLibLoader } from '../services/SioServiceLibLoader.js';
|
||||||
import rrwebPlayerMod from 'rrweb-player';
|
|
||||||
|
|
||||||
const rrweb: any = rrwebMod;
|
|
||||||
const rrwebPlayer: typeof rrwebPlayerMod.default = rrwebPlayerMod as any;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Use rrweb's eventWithTime if you like strict typing:
|
* Use rrweb's eventWithTime if you like strict typing:
|
||||||
@@ -106,6 +102,10 @@ export class SioRecorder extends DeesElement {
|
|||||||
*/
|
*/
|
||||||
private async startRecording(): Promise<void> {
|
private async startRecording(): Promise<void> {
|
||||||
await this.domtoolsPromise;
|
await this.domtoolsPromise;
|
||||||
|
|
||||||
|
// Load rrweb from CDN
|
||||||
|
const rrweb = await SioServiceLibLoader.getInstance().loadRrweb();
|
||||||
|
|
||||||
this.status = 'recording';
|
this.status = 'recording';
|
||||||
this.events = [];
|
this.events = [];
|
||||||
|
|
||||||
@@ -148,7 +148,12 @@ export class SioRecorder extends DeesElement {
|
|||||||
private async playRecording(): Promise<void> {
|
private async playRecording(): Promise<void> {
|
||||||
await this.domtoolsPromise;
|
await this.domtoolsPromise;
|
||||||
if (!this.playbackDiv) return;
|
if (!this.playbackDiv) return;
|
||||||
const replayer = new rrwebPlayer({
|
|
||||||
|
// Load rrweb-player from CDN
|
||||||
|
const rrwebPlayerBundle = await SioServiceLibLoader.getInstance().loadRrwebPlayer();
|
||||||
|
const RrwebPlayer = rrwebPlayerBundle.default;
|
||||||
|
|
||||||
|
const replayer = new RrwebPlayer({
|
||||||
target: this.playbackDiv, // customizable root element
|
target: this.playbackDiv, // customizable root element
|
||||||
props: {
|
props: {
|
||||||
events: this.events as any,
|
events: this.events as any,
|
||||||
@@ -172,11 +177,11 @@ export class SioRecorder extends DeesElement {
|
|||||||
public async fixPosition() {
|
public async fixPosition() {
|
||||||
await this.domtoolsPromise;
|
await this.domtoolsPromise;
|
||||||
await this.domtools.convenience.smartdelay.delayFor(0);
|
await this.domtools.convenience.smartdelay.delayFor(0);
|
||||||
const playbackDiv = this.shadowRoot.querySelector('#playback') as HTMLElement;
|
const playbackDiv = this.shadowRoot!.querySelector('#playback') as HTMLElement;
|
||||||
const replayerWrapper = this.shadowRoot.querySelector('.replayer-wrapper') as HTMLElement;
|
const replayerWrapper = this.shadowRoot!.querySelector('.replayer-wrapper') as HTMLElement;
|
||||||
const replayerMouse = this.shadowRoot.querySelector('.replayer-mouse') as HTMLElement;
|
const replayerMouse = this.shadowRoot!.querySelector('.replayer-mouse') as HTMLElement;
|
||||||
const replayerMouseTail = this.shadowRoot.querySelector('.replayer-mouse-tail') as HTMLElement;
|
const replayerMouseTail = this.shadowRoot!.querySelector('.replayer-mouse-tail') as HTMLElement;
|
||||||
const iframe = this.shadowRoot.querySelector('iframe');
|
const iframe = this.shadowRoot!.querySelector('iframe')!;
|
||||||
replayerWrapper.style.position = 'absolute';
|
replayerWrapper.style.position = 'absolute';
|
||||||
replayerWrapper.style.top = '0px';
|
replayerWrapper.style.top = '0px';
|
||||||
replayerWrapper.style.left = '0px';
|
replayerWrapper.style.left = '0px';
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
export * from './elements/index.js';
|
export * from './elements/index.js';
|
||||||
|
export * from './services/SioServiceLibLoader.js';
|
||||||
|
|||||||
@@ -0,0 +1,200 @@
|
|||||||
|
import { CDN_BASE, CDN_VERSIONS } from './versions.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* rrweb record options
|
||||||
|
*/
|
||||||
|
export interface IRrwebRecordOptions {
|
||||||
|
emit: (event: any) => void;
|
||||||
|
recordCanvas?: boolean;
|
||||||
|
recordCrossOriginIframes?: boolean;
|
||||||
|
checkoutEveryNms?: number;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* rrweb record function type
|
||||||
|
*/
|
||||||
|
export type TRrwebRecordFn = (options: IRrwebRecordOptions) => () => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bundle type for rrweb recording library
|
||||||
|
*/
|
||||||
|
export interface IRrwebBundle {
|
||||||
|
record: TRrwebRecordFn;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* rrweb-player constructor options
|
||||||
|
*/
|
||||||
|
export interface IRrwebPlayerOptions {
|
||||||
|
target: HTMLElement;
|
||||||
|
props: {
|
||||||
|
events: any[];
|
||||||
|
root?: HTMLElement;
|
||||||
|
showController?: boolean;
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* rrweb-player instance
|
||||||
|
*/
|
||||||
|
export interface IRrwebPlayerInstance {
|
||||||
|
play: () => Promise<void>;
|
||||||
|
pause: () => void;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* rrweb-player constructor type
|
||||||
|
*/
|
||||||
|
export type TRrwebPlayerConstructor = new (options: IRrwebPlayerOptions) => IRrwebPlayerInstance;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bundle type for rrweb-player
|
||||||
|
*/
|
||||||
|
export interface IRrwebPlayerBundle {
|
||||||
|
default: TRrwebPlayerConstructor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Singleton service for lazy-loading rrweb libraries from CDN.
|
||||||
|
*
|
||||||
|
* This reduces initial bundle size by loading libraries only when needed.
|
||||||
|
* Libraries are cached after first load to avoid duplicate fetches.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* const libLoader = SioServiceLibLoader.getInstance();
|
||||||
|
* const rrweb = await libLoader.loadRrweb();
|
||||||
|
* const stopFn = rrweb.record({ emit: (event) => { ... } });
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export class SioServiceLibLoader {
|
||||||
|
private static instance: SioServiceLibLoader;
|
||||||
|
|
||||||
|
// Cached library references
|
||||||
|
private rrwebLib: IRrwebBundle | null = null;
|
||||||
|
private rrwebPlayerLib: IRrwebPlayerBundle | null = null;
|
||||||
|
|
||||||
|
// Loading promises to prevent duplicate concurrent loads
|
||||||
|
private rrwebLoadingPromise: Promise<IRrwebBundle> | null = null;
|
||||||
|
private rrwebPlayerLoadingPromise: Promise<IRrwebPlayerBundle> | null = null;
|
||||||
|
|
||||||
|
private constructor() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the singleton instance of SioServiceLibLoader
|
||||||
|
*/
|
||||||
|
public static getInstance(): SioServiceLibLoader {
|
||||||
|
if (!SioServiceLibLoader.instance) {
|
||||||
|
SioServiceLibLoader.instance = new SioServiceLibLoader();
|
||||||
|
}
|
||||||
|
return SioServiceLibLoader.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load rrweb recording library from CDN
|
||||||
|
* @returns Promise resolving to rrweb module with record function
|
||||||
|
*/
|
||||||
|
public async loadRrweb(): Promise<IRrwebBundle> {
|
||||||
|
if (this.rrwebLib) {
|
||||||
|
return this.rrwebLib;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.rrwebLoadingPromise) {
|
||||||
|
return this.rrwebLoadingPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.rrwebLoadingPromise = (async () => {
|
||||||
|
const url = `${CDN_BASE}/rrweb@${CDN_VERSIONS.rrweb}/+esm`;
|
||||||
|
const module = await import(/* @vite-ignore */ url);
|
||||||
|
|
||||||
|
this.rrwebLib = {
|
||||||
|
record: module.record,
|
||||||
|
};
|
||||||
|
return this.rrwebLib;
|
||||||
|
})();
|
||||||
|
|
||||||
|
return this.rrwebLoadingPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load rrweb-player from CDN
|
||||||
|
* @returns Promise resolving to rrweb-player module
|
||||||
|
*/
|
||||||
|
public async loadRrwebPlayer(): Promise<IRrwebPlayerBundle> {
|
||||||
|
if (this.rrwebPlayerLib) {
|
||||||
|
return this.rrwebPlayerLib;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.rrwebPlayerLoadingPromise) {
|
||||||
|
return this.rrwebPlayerLoadingPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.rrwebPlayerLoadingPromise = (async () => {
|
||||||
|
const url = `${CDN_BASE}/rrweb-player@${CDN_VERSIONS.rrwebPlayer}/+esm`;
|
||||||
|
const module = await import(/* @vite-ignore */ url);
|
||||||
|
|
||||||
|
// Inject rrweb-player CSS styles
|
||||||
|
await this.injectRrwebPlayerStyles();
|
||||||
|
|
||||||
|
this.rrwebPlayerLib = {
|
||||||
|
default: module.default,
|
||||||
|
};
|
||||||
|
return this.rrwebPlayerLib;
|
||||||
|
})();
|
||||||
|
|
||||||
|
return this.rrwebPlayerLoadingPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inject rrweb-player CSS styles into the document head
|
||||||
|
*/
|
||||||
|
private async injectRrwebPlayerStyles(): Promise<void> {
|
||||||
|
const styleId = 'rrweb-player-cdn-styles';
|
||||||
|
if (document.getElementById(styleId)) {
|
||||||
|
return; // Already injected
|
||||||
|
}
|
||||||
|
|
||||||
|
const cssUrl = `${CDN_BASE}/rrweb-player@${CDN_VERSIONS.rrwebPlayer}/dist/style.css`;
|
||||||
|
try {
|
||||||
|
const response = await fetch(cssUrl);
|
||||||
|
const cssText = await response.text();
|
||||||
|
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.id = styleId;
|
||||||
|
style.textContent = cssText;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to load rrweb-player styles from CDN:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preload all rrweb libraries in parallel
|
||||||
|
* Useful for warming the cache before recording is needed
|
||||||
|
*/
|
||||||
|
public async preloadAll(): Promise<void> {
|
||||||
|
await Promise.all([
|
||||||
|
this.loadRrweb(),
|
||||||
|
this.loadRrwebPlayer(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a specific library is already loaded
|
||||||
|
*/
|
||||||
|
public isLoaded(library: 'rrweb' | 'rrwebPlayer'): boolean {
|
||||||
|
switch (library) {
|
||||||
|
case 'rrweb':
|
||||||
|
return this.rrwebLib !== null;
|
||||||
|
case 'rrwebPlayer':
|
||||||
|
return this.rrwebPlayerLib !== null;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* CDN versions for lazy-loaded libraries.
|
||||||
|
* Keep these in sync with package.json for type compatibility.
|
||||||
|
*/
|
||||||
|
export const CDN_VERSIONS = {
|
||||||
|
rrweb: '2.0.0-alpha.4',
|
||||||
|
rrwebPlayer: '1.0.0-alpha.4',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base CDN URL for jsdelivr ESM imports
|
||||||
|
*/
|
||||||
|
export const CDN_BASE = 'https://cdn.jsdelivr.net/npm';
|
||||||
Reference in New Issue
Block a user