Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7ccff9ff31 | |||
| bdd3f55354 | |||
| d0227fd5a6 | |||
| e7877ffaaf | |||
| ce676d0df9 | |||
| b52126a5ba |
25
changelog.md
25
changelog.md
@@ -1,5 +1,30 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2025-12-22 - 1.3.0 - feat(dees-mobile-gallery)
|
||||||
|
add mobile gallery component with fullscreen viewer, swipe navigation, action buttons (download/share/delete), PDF support and demo
|
||||||
|
|
||||||
|
- Adds new <dees-mobile-gallery> web component (ts_web/elements/00group-ui/dees-mobile-gallery) with ~650 lines of implementation.
|
||||||
|
- Component features: fullscreen viewing, touch swipe and keyboard navigation, thumbnails, startIndex, minimal mode, and PDF placeholder support.
|
||||||
|
- Action buttons and events: download, share, delete (configurable); emits 'close', 'delete', 'download', and 'share' events.
|
||||||
|
- Provides a static DeesMobileGallery.show(items, config) factory method that returns a Promise with the user action result.
|
||||||
|
- Includes demo (dees-mobile-gallery.demo.ts) with usage examples and exports the component from the 00group-ui index.
|
||||||
|
- Bumps package version from 1.1.0 to 1.2.0 in package.json.
|
||||||
|
|
||||||
|
## 2025-12-22 - 1.1.0 - feat(ui)
|
||||||
|
add mobile context menu and iconbutton components with demos and exports
|
||||||
|
|
||||||
|
- Added dees-mobile-contextmenu component (items API, viewport-aware positioning, touch handling, open/close animation) and a demo
|
||||||
|
- Added dees-mobile-iconbutton component (sm/md/lg sizes, disabled state, accessibility attributes) and a demo
|
||||||
|
- Added barrel index files for both components
|
||||||
|
- Exported the new components from ts_web/elements/00group-ui/index.ts
|
||||||
|
|
||||||
|
## 2025-12-22 - 1.0.2 - fix(dees-mobile-header)
|
||||||
|
adjust mobile header action slot layout and add documentation/license files
|
||||||
|
|
||||||
|
- ts_web/elements/00group-ui/dees-mobile-header/dees-mobile-header.ts: change ::slotted([slot="actions"]) from fixed width/height to min-width/min-height, switch display from block to flex and add align-items:center to improve vertical alignment
|
||||||
|
- Add MIT license file at /license
|
||||||
|
- Add extensive readme.md (documentation, install instructions, component usage, development and legal info)
|
||||||
|
|
||||||
## 2025-12-22 - 1.0.1 - fix()
|
## 2025-12-22 - 1.0.1 - fix()
|
||||||
no changes detected; no release required
|
no changes detected; no release required
|
||||||
|
|
||||||
|
|||||||
21
license
Normal file
21
license
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2024 Task Venture Capital GmbH
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@design.estate/dees-catalog-mobile",
|
"name": "@design.estate/dees-catalog-mobile",
|
||||||
"version": "1.0.1",
|
"version": "1.3.0",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "A mobile-optimized component catalog for building cross-platform business applications with touch-first UI components.",
|
"description": "A mobile-optimized component catalog for building cross-platform business applications with touch-first UI components.",
|
||||||
"main": "dist_ts_web/index.js",
|
"main": "dist_ts_web/index.js",
|
||||||
|
|||||||
417
readme.md
Normal file
417
readme.md
Normal file
@@ -0,0 +1,417 @@
|
|||||||
|
# @design.estate/dees-catalog-mobile
|
||||||
|
|
||||||
|
A mobile-optimized web component catalog for building cross-platform business applications with touch-first UI components. Built on Lit and designed for PWAs, native app wrappers, and mobile web.
|
||||||
|
|
||||||
|
## Issue Reporting and Security
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @design.estate/dees-catalog-mobile
|
||||||
|
# or
|
||||||
|
pnpm add @design.estate/dees-catalog-mobile
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📱 Features
|
||||||
|
|
||||||
|
- **Touch-First Design** – 44px minimum touch targets, tap feedback, and gesture-friendly interactions
|
||||||
|
- **Dark/Light Theme Support** – Automatic theme adaptation using `cssManager.bdTheme()`
|
||||||
|
- **iOS Safe Area Support** – Proper handling of notches and home indicators
|
||||||
|
- **iOS Keyboard Detection** – Automatic layout adjustments when soft keyboard appears
|
||||||
|
- **Smooth Animations** – Native-feeling slide transitions for view navigation
|
||||||
|
- **TypeScript Native** – Full type definitions and excellent IDE support
|
||||||
|
- **Lucide Icons** – 1000+ icons with intelligent caching
|
||||||
|
|
||||||
|
## 🧩 Components
|
||||||
|
|
||||||
|
### UI Components
|
||||||
|
|
||||||
|
#### `<dees-mobile-button>`
|
||||||
|
|
||||||
|
Versatile button with multiple variants and sizes.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<dees-mobile-button variant="primary" size="md">Click Me</dees-mobile-button>
|
||||||
|
<dees-mobile-button variant="destructive" loading>Processing...</dees-mobile-button>
|
||||||
|
<dees-mobile-button variant="ghost" icon><dees-mobile-icon icon="menu"></dees-mobile-icon></dees-mobile-button>
|
||||||
|
```
|
||||||
|
|
||||||
|
| Property | Type | Default | Description |
|
||||||
|
|----------|------|---------|-------------|
|
||||||
|
| `variant` | `'default' \| 'primary' \| 'secondary' \| 'outline' \| 'ghost' \| 'destructive' \| 'link'` | `'default'` | Button style variant |
|
||||||
|
| `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Button size |
|
||||||
|
| `disabled` | `boolean` | `false` | Disable the button |
|
||||||
|
| `loading` | `boolean` | `false` | Show loading spinner |
|
||||||
|
| `icon` | `boolean` | `false` | Icon-only mode (square button) |
|
||||||
|
| `type` | `'button' \| 'submit' \| 'reset'` | `'button'` | HTML button type |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### `<dees-mobile-icon>`
|
||||||
|
|
||||||
|
Lucide icon integration with smart caching.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<dees-mobile-icon icon="home" size="24" color="#3b82f6"></dees-mobile-icon>
|
||||||
|
<dees-mobile-icon icon="settings" size="32"></dees-mobile-icon>
|
||||||
|
```
|
||||||
|
|
||||||
|
| Property | Type | Default | Description |
|
||||||
|
|----------|------|---------|-------------|
|
||||||
|
| `icon` | `string` | `''` | Lucide icon name (e.g., `'home'`, `'settings'`, `'user'`) |
|
||||||
|
| `size` | `string \| number` | `'24'` | Icon size in pixels |
|
||||||
|
| `color` | `string` | `'currentColor'` | Icon color |
|
||||||
|
| `strokeWidth` | `string \| number` | `'2'` | Stroke width |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### `<dees-mobile-header>`
|
||||||
|
|
||||||
|
App header with flexible slot-based layout.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<dees-mobile-header title="My App" subtitle="Dashboard">
|
||||||
|
<dees-mobile-button slot="left-action" icon variant="ghost">
|
||||||
|
<dees-mobile-icon icon="menu"></dees-mobile-icon>
|
||||||
|
</dees-mobile-button>
|
||||||
|
<dees-mobile-button slot="actions" icon variant="ghost">
|
||||||
|
<dees-mobile-icon icon="search"></dees-mobile-icon>
|
||||||
|
</dees-mobile-button>
|
||||||
|
</dees-mobile-header>
|
||||||
|
```
|
||||||
|
|
||||||
|
| Property | Type | Default | Description |
|
||||||
|
|----------|------|---------|-------------|
|
||||||
|
| `title` | `string` | `''` | Header title |
|
||||||
|
| `subtitle` | `string` | `''` | Optional subtitle |
|
||||||
|
|
||||||
|
**Slots:** `left-action`, `content`, `middle`, `actions`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### `<dees-mobile-modal>`
|
||||||
|
|
||||||
|
Centered modal dialog with backdrop.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<dees-mobile-modal .open=${this.showModal} title="Confirm Action" @close=${() => this.showModal = false}>
|
||||||
|
<p>Are you sure you want to proceed?</p>
|
||||||
|
<div slot="footer">
|
||||||
|
<dees-mobile-button variant="outline" @click=${() => this.showModal = false}>Cancel</dees-mobile-button>
|
||||||
|
<dees-mobile-button variant="primary" @click=${this.confirm}>Confirm</dees-mobile-button>
|
||||||
|
</div>
|
||||||
|
</dees-mobile-modal>
|
||||||
|
```
|
||||||
|
|
||||||
|
| Property | Type | Default | Description |
|
||||||
|
|----------|------|---------|-------------|
|
||||||
|
| `open` | `boolean` | `false` | Controls visibility |
|
||||||
|
| `title` | `string` | `''` | Modal title |
|
||||||
|
| `showCloseButton` | `boolean` | `true` | Show close button in header |
|
||||||
|
|
||||||
|
**Events:** `close`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### `<dees-mobile-actionsheet>`
|
||||||
|
|
||||||
|
iOS-style bottom action sheet.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<dees-mobile-actionsheet
|
||||||
|
title="Choose an action"
|
||||||
|
.options=${[
|
||||||
|
{ id: 'edit', icon: 'pencil', title: 'Edit', subtitle: 'Modify this item' },
|
||||||
|
{ id: 'share', icon: 'share', title: 'Share' },
|
||||||
|
{ id: 'delete', icon: 'trash-2', iconColor: '#dc2626', title: 'Delete' }
|
||||||
|
]}
|
||||||
|
@select=${(e) => console.log('Selected:', e.detail.id)}
|
||||||
|
@close=${() => this.hideActionSheet()}
|
||||||
|
></dees-mobile-actionsheet>
|
||||||
|
```
|
||||||
|
|
||||||
|
| Property | Type | Default | Description |
|
||||||
|
|----------|------|---------|-------------|
|
||||||
|
| `title` | `string` | `''` | Sheet title |
|
||||||
|
| `options` | `IActionSheetOption[]` | `[]` | Action options |
|
||||||
|
| `cancelText` | `string` | `'Cancel'` | Cancel button text |
|
||||||
|
|
||||||
|
**Events:** `select`, `close`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### `<dees-mobile-toast>`
|
||||||
|
|
||||||
|
Toast notification system.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<dees-mobile-toast
|
||||||
|
.open=${this.showToast}
|
||||||
|
message="Item saved successfully!"
|
||||||
|
type="success"
|
||||||
|
duration="3000"
|
||||||
|
></dees-mobile-toast>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Layout Components
|
||||||
|
|
||||||
|
#### `<dees-mobile-navigation>`
|
||||||
|
|
||||||
|
Bottom tab navigation bar with badge support.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<dees-mobile-navigation
|
||||||
|
.activeTab=${'home'}
|
||||||
|
.tabs=${[
|
||||||
|
{ id: 'home', icon: 'home', label: 'Home' },
|
||||||
|
{ id: 'search', icon: 'search', label: 'Search' },
|
||||||
|
{ id: 'notifications', icon: 'bell', label: 'Alerts', badge: 5 },
|
||||||
|
{ id: 'profile', icon: 'user', label: 'Profile' }
|
||||||
|
]}
|
||||||
|
@tab-change=${(e) => this.activeTab = e.detail.tab}
|
||||||
|
></dees-mobile-navigation>
|
||||||
|
```
|
||||||
|
|
||||||
|
| Property | Type | Default | Description |
|
||||||
|
|----------|------|---------|-------------|
|
||||||
|
| `activeTab` | `string` | `''` | Currently active tab ID |
|
||||||
|
| `tabs` | `INavigationTab[]` | `[]` | Array of tab definitions |
|
||||||
|
|
||||||
|
**Events:** `tab-change`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### `<dees-mobile-applayout>`
|
||||||
|
|
||||||
|
Full-screen app layout with iOS keyboard handling.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<dees-mobile-applayout>
|
||||||
|
<dees-mobile-header slot="header" title="My App"></dees-mobile-header>
|
||||||
|
|
||||||
|
<div slot="content">
|
||||||
|
<!-- Your app content here -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<dees-mobile-navigation slot="navigation" .tabs=${this.tabs}></dees-mobile-navigation>
|
||||||
|
</dees-mobile-applayout>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Slots:** `header`, `content`, `navigation`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### `<dees-mobile-viewstack>` + `<dees-mobile-view>`
|
||||||
|
|
||||||
|
Stack-based navigation with smooth slide transitions. Perfect for drill-down interfaces (lists → detail → sub-detail).
|
||||||
|
|
||||||
|
```html
|
||||||
|
<dees-mobile-viewstack initial-view="lists" @view-changed=${this.handleViewChange}>
|
||||||
|
<dees-mobile-view view-id="lists">
|
||||||
|
<my-lists-view @item-click=${(e) => this.viewstack.pushView('detail')}></my-lists-view>
|
||||||
|
</dees-mobile-view>
|
||||||
|
|
||||||
|
<dees-mobile-view view-id="detail">
|
||||||
|
<my-detail-view @back=${() => this.viewstack.popView()}></my-detail-view>
|
||||||
|
</dees-mobile-view>
|
||||||
|
|
||||||
|
<dees-mobile-view view-id="subdetail">
|
||||||
|
<my-subdetail-view></my-subdetail-view>
|
||||||
|
</dees-mobile-view>
|
||||||
|
</dees-mobile-viewstack>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Viewstack API:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const viewstack = document.querySelector('dees-mobile-viewstack');
|
||||||
|
|
||||||
|
// Navigate forward (slides in from right)
|
||||||
|
await viewstack.pushView('detail');
|
||||||
|
|
||||||
|
// Navigate back (slides out to right)
|
||||||
|
await viewstack.popView();
|
||||||
|
|
||||||
|
// Replace current view (no animation)
|
||||||
|
viewstack.replaceView('other');
|
||||||
|
|
||||||
|
// Go back to root view
|
||||||
|
await viewstack.goToRoot(true); // animated
|
||||||
|
viewstack.goToRoot(false); // instant
|
||||||
|
|
||||||
|
// Properties
|
||||||
|
viewstack.currentView; // Current view ID
|
||||||
|
viewstack.stackDepth; // Number of views in stack
|
||||||
|
viewstack.canGoBack; // true if stack depth > 1
|
||||||
|
```
|
||||||
|
|
||||||
|
**Events:** `view-changed`, `transition-start`, `transition-end`, `navigate-back`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Input Components
|
||||||
|
|
||||||
|
#### `<dees-mobile-input>`
|
||||||
|
|
||||||
|
Form input with label, validation, and iOS zoom prevention.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<dees-mobile-input
|
||||||
|
label="Email Address"
|
||||||
|
type="email"
|
||||||
|
placeholder="you@example.com"
|
||||||
|
required
|
||||||
|
autocomplete="email"
|
||||||
|
@input=${(e) => console.log(e.detail.value)}
|
||||||
|
></dees-mobile-input>
|
||||||
|
|
||||||
|
<dees-mobile-input
|
||||||
|
label="Password"
|
||||||
|
type="password"
|
||||||
|
error="Password must be at least 8 characters"
|
||||||
|
></dees-mobile-input>
|
||||||
|
```
|
||||||
|
|
||||||
|
| Property | Type | Default | Description |
|
||||||
|
|----------|------|---------|-------------|
|
||||||
|
| `type` | `'text' \| 'email' \| 'password' \| 'number' \| 'tel' \| 'url' \| 'search'` | `'text'` | Input type |
|
||||||
|
| `label` | `string` | `''` | Label text |
|
||||||
|
| `placeholder` | `string` | `''` | Placeholder text |
|
||||||
|
| `value` | `string` | `''` | Input value |
|
||||||
|
| `disabled` | `boolean` | `false` | Disable input |
|
||||||
|
| `required` | `boolean` | `false` | Mark as required |
|
||||||
|
| `error` | `string` | `''` | Error message |
|
||||||
|
| `autocomplete` | `string` | `''` | Autocomplete hint |
|
||||||
|
|
||||||
|
**Events:** `input`, `change`, `input-focus`, `input-blur`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎨 Theming
|
||||||
|
|
||||||
|
Components automatically adapt to dark/light themes. Theme switching is handled via CSS variables and `cssManager.bdTheme()`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// In your component styles
|
||||||
|
css`
|
||||||
|
.my-element {
|
||||||
|
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
|
||||||
|
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
|
||||||
|
}
|
||||||
|
`
|
||||||
|
```
|
||||||
|
|
||||||
|
### CSS Variables
|
||||||
|
|
||||||
|
Components use these CSS custom properties:
|
||||||
|
|
||||||
|
```css
|
||||||
|
:root {
|
||||||
|
--dees-primary: #3b82f6;
|
||||||
|
--dees-background: #ffffff;
|
||||||
|
--dees-foreground: #09090b;
|
||||||
|
--dees-muted-foreground: #71717a;
|
||||||
|
--dees-border: #e4e4e7;
|
||||||
|
--dees-ring: #3b82f6;
|
||||||
|
--dees-danger: #dc2626;
|
||||||
|
--dees-radius: 0.5rem;
|
||||||
|
--dees-radius-lg: 0.75rem;
|
||||||
|
--dees-transition-fast: 150ms;
|
||||||
|
--safe-area-inset-bottom: env(safe-area-inset-bottom, 0px);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 Full Example
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import {
|
||||||
|
DeesMobileButton,
|
||||||
|
DeesMobileHeader,
|
||||||
|
DeesMobileNavigation,
|
||||||
|
DeesMobileViewstack,
|
||||||
|
DeesMobileView,
|
||||||
|
DeesMobileInput,
|
||||||
|
} from '@design.estate/dees-catalog-mobile';
|
||||||
|
|
||||||
|
// All components auto-register. Just use them in your templates:
|
||||||
|
const template = html`
|
||||||
|
<dees-mobile-applayout>
|
||||||
|
<dees-mobile-header slot="header" title="My App"></dees-mobile-header>
|
||||||
|
|
||||||
|
<dees-mobile-viewstack slot="content" initial-view="home">
|
||||||
|
<dees-mobile-view view-id="home">
|
||||||
|
<div class="home-content">
|
||||||
|
<h1>Welcome!</h1>
|
||||||
|
<dees-mobile-button variant="primary" @click=${() => viewstack.pushView('settings')}>
|
||||||
|
Go to Settings
|
||||||
|
</dees-mobile-button>
|
||||||
|
</div>
|
||||||
|
</dees-mobile-view>
|
||||||
|
|
||||||
|
<dees-mobile-view view-id="settings">
|
||||||
|
<div class="settings-content">
|
||||||
|
<dees-mobile-input label="Username" placeholder="Enter username"></dees-mobile-input>
|
||||||
|
<dees-mobile-button @click=${() => viewstack.popView()}>Back</dees-mobile-button>
|
||||||
|
</div>
|
||||||
|
</dees-mobile-view>
|
||||||
|
</dees-mobile-viewstack>
|
||||||
|
|
||||||
|
<dees-mobile-navigation
|
||||||
|
slot="navigation"
|
||||||
|
.activeTab=${'home'}
|
||||||
|
.tabs=${[
|
||||||
|
{ id: 'home', icon: 'home', label: 'Home' },
|
||||||
|
{ id: 'settings', icon: 'settings', label: 'Settings' },
|
||||||
|
]}
|
||||||
|
></dees-mobile-navigation>
|
||||||
|
</dees-mobile-applayout>
|
||||||
|
`;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install dependencies
|
||||||
|
pnpm install
|
||||||
|
|
||||||
|
# Start development server with wcctools
|
||||||
|
pnpm run watch
|
||||||
|
|
||||||
|
# Build for production
|
||||||
|
pnpm run build
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
pnpm test
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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.
|
||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@design.estate/dees-catalog-mobile',
|
name: '@design.estate/dees-catalog-mobile',
|
||||||
version: '1.0.1',
|
version: '1.3.0',
|
||||||
description: 'A mobile-optimized component catalog for building cross-platform business applications with touch-first UI components.'
|
description: 'A mobile-optimized component catalog for building cross-platform business applications with touch-first UI components.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import { html, type TemplateResult } from '@design.estate/dees-element';
|
||||||
|
import type { IContextMenuItem } from './dees-mobile-contextmenu.js';
|
||||||
|
|
||||||
|
export const demoFunc = (): TemplateResult => {
|
||||||
|
const showContextMenu = (e: MouseEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const items: IContextMenuItem[] = [
|
||||||
|
{
|
||||||
|
label: 'Edit',
|
||||||
|
icon: 'pencil',
|
||||||
|
action: () => console.log('Edit clicked'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Duplicate',
|
||||||
|
icon: 'copy',
|
||||||
|
action: () => console.log('Duplicate clicked'),
|
||||||
|
},
|
||||||
|
{ divider: true },
|
||||||
|
{
|
||||||
|
label: 'Share',
|
||||||
|
icon: 'share',
|
||||||
|
action: () => console.log('Share clicked'),
|
||||||
|
},
|
||||||
|
{ divider: true },
|
||||||
|
{
|
||||||
|
label: 'Delete',
|
||||||
|
icon: 'trash-2',
|
||||||
|
danger: true,
|
||||||
|
action: () => console.log('Delete clicked'),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
import('./dees-mobile-contextmenu.js').then(({ DeesMobileContextmenu }) => {
|
||||||
|
DeesMobileContextmenu.createAndShow(items, e.clientX, e.clientY);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<style>
|
||||||
|
.demo-container {
|
||||||
|
padding: 2rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-area {
|
||||||
|
padding: 3rem;
|
||||||
|
border: 2px dashed #e4e4e7;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
text-align: center;
|
||||||
|
cursor: context-menu;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-area:hover {
|
||||||
|
border-color: #a1a1aa;
|
||||||
|
background: #f4f4f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-description {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #71717a;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="demo-container">
|
||||||
|
<div class="demo-description">
|
||||||
|
Right-click (or long-press on touch) to show context menu
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="demo-area"
|
||||||
|
@contextmenu=${showContextMenu}
|
||||||
|
>
|
||||||
|
Right-click here
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
};
|
||||||
@@ -0,0 +1,283 @@
|
|||||||
|
import {
|
||||||
|
DeesElement,
|
||||||
|
css,
|
||||||
|
cssManager,
|
||||||
|
customElement,
|
||||||
|
html,
|
||||||
|
property,
|
||||||
|
state,
|
||||||
|
type TemplateResult,
|
||||||
|
} from '@design.estate/dees-element';
|
||||||
|
|
||||||
|
import { mobileComponentStyles } from '../../00componentstyles.js';
|
||||||
|
import '../dees-mobile-icon/dees-mobile-icon.js';
|
||||||
|
import { demoFunc } from './dees-mobile-contextmenu.demo.js';
|
||||||
|
|
||||||
|
export interface IContextMenuItem {
|
||||||
|
label?: string;
|
||||||
|
icon?: string;
|
||||||
|
action?: () => void;
|
||||||
|
danger?: boolean;
|
||||||
|
divider?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'dees-mobile-contextmenu': DeesMobileContextmenu;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@customElement('dees-mobile-contextmenu')
|
||||||
|
export class DeesMobileContextmenu extends DeesElement {
|
||||||
|
public static demo = demoFunc;
|
||||||
|
|
||||||
|
@property({ type: Array })
|
||||||
|
accessor items: IContextMenuItem[] = [];
|
||||||
|
|
||||||
|
@property({ type: Number })
|
||||||
|
accessor x: number = 0;
|
||||||
|
|
||||||
|
@property({ type: Number })
|
||||||
|
accessor y: number = 0;
|
||||||
|
|
||||||
|
@property({ type: Boolean })
|
||||||
|
accessor isTouch: boolean = false;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
accessor isClosing: boolean = false;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
accessor transformOrigin: string = 'top left';
|
||||||
|
|
||||||
|
public static styles = [
|
||||||
|
cssManager.defaultStyles,
|
||||||
|
mobileComponentStyles,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
position: fixed;
|
||||||
|
z-index: var(--dees-z-contextmenu, 10000);
|
||||||
|
}
|
||||||
|
|
||||||
|
:host(.closing) .menu {
|
||||||
|
animation: scaleOut 100ms ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu {
|
||||||
|
background: ${cssManager.bdTheme('#ffffff', '#18181b')};
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1);
|
||||||
|
padding: 0.5rem 0;
|
||||||
|
min-width: 180px;
|
||||||
|
animation: scaleIn 100ms ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 100ms ease;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item:hover {
|
||||||
|
background: ${cssManager.bdTheme('#f4f4f5', '#27272a')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item.danger {
|
||||||
|
color: ${cssManager.bdTheme('#dc2626', '#ef4444')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
height: 1px;
|
||||||
|
background: ${cssManager.bdTheme('#e4e4e7', '#27272a')};
|
||||||
|
margin: 0.25rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes scaleIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.9);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes scaleOut {
|
||||||
|
from {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.9);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory method to create and show a context menu
|
||||||
|
*/
|
||||||
|
public static createAndShow(
|
||||||
|
items: IContextMenuItem[],
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
isTouch = false
|
||||||
|
): DeesMobileContextmenu {
|
||||||
|
// Remove any existing context menu
|
||||||
|
const existing = document.querySelector('dees-mobile-contextmenu');
|
||||||
|
if (existing) {
|
||||||
|
existing.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new menu
|
||||||
|
const menu = document.createElement('dees-mobile-contextmenu') as DeesMobileContextmenu;
|
||||||
|
menu.items = items;
|
||||||
|
menu.x = x;
|
||||||
|
menu.y = y;
|
||||||
|
menu.isTouch = isTouch;
|
||||||
|
|
||||||
|
// Add to document
|
||||||
|
document.body.appendChild(menu);
|
||||||
|
|
||||||
|
// Position after render to handle viewport bounds
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
menu.adjustPosition();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close on outside click
|
||||||
|
const handleClick = (e: MouseEvent) => {
|
||||||
|
if (!e.composedPath().includes(menu)) {
|
||||||
|
menu.close();
|
||||||
|
document.removeEventListener('click', handleClick, true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add listener on next tick to avoid immediate close
|
||||||
|
setTimeout(() => {
|
||||||
|
document.addEventListener('click', handleClick, true);
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
return menu;
|
||||||
|
}
|
||||||
|
|
||||||
|
private adjustPosition(): void {
|
||||||
|
const rect = this.getBoundingClientRect();
|
||||||
|
const menuWidth = rect.width;
|
||||||
|
const menuHeight = rect.height;
|
||||||
|
const padding = 10;
|
||||||
|
|
||||||
|
let adjustedX = this.x;
|
||||||
|
let adjustedY = this.y;
|
||||||
|
|
||||||
|
// Calculate available space in each direction
|
||||||
|
const spaceTop = this.y - padding;
|
||||||
|
const spaceBottom = window.innerHeight - this.y - padding;
|
||||||
|
const spaceLeft = this.x - padding;
|
||||||
|
const spaceRight = window.innerWidth - this.x - padding;
|
||||||
|
|
||||||
|
// For touch interactions, prefer opening upward if there's space
|
||||||
|
if (this.isTouch && spaceTop >= menuHeight) {
|
||||||
|
// Open upward from touch point
|
||||||
|
adjustedY = this.y - menuHeight;
|
||||||
|
this.transformOrigin = 'bottom left';
|
||||||
|
|
||||||
|
// Adjust X if needed
|
||||||
|
if (spaceRight < menuWidth && spaceLeft >= menuWidth) {
|
||||||
|
adjustedX = this.x - menuWidth;
|
||||||
|
this.transformOrigin = 'bottom right';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Default behavior (open downward/rightward)
|
||||||
|
// Flip horizontally if not enough space on right
|
||||||
|
if (spaceRight < menuWidth && spaceLeft >= menuWidth) {
|
||||||
|
adjustedX = this.x - menuWidth;
|
||||||
|
this.transformOrigin = this.transformOrigin.replace('left', 'right');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flip vertically if not enough space below
|
||||||
|
if (spaceBottom < menuHeight && spaceTop >= menuHeight) {
|
||||||
|
adjustedY = this.y - menuHeight;
|
||||||
|
this.transformOrigin = this.transformOrigin.replace('top', 'bottom');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Final boundary checks to keep menu fully visible
|
||||||
|
adjustedX = Math.max(padding, Math.min(adjustedX, window.innerWidth - menuWidth - padding));
|
||||||
|
adjustedY = Math.max(padding, Math.min(adjustedY, window.innerHeight - menuHeight - padding));
|
||||||
|
|
||||||
|
this.style.left = `${adjustedX}px`;
|
||||||
|
this.style.top = `${adjustedY}px`;
|
||||||
|
|
||||||
|
// Update the menu's transform origin
|
||||||
|
const menu = this.shadowRoot?.querySelector('.menu') as HTMLElement;
|
||||||
|
if (menu) {
|
||||||
|
menu.style.transformOrigin = this.transformOrigin;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public close(): void {
|
||||||
|
if (this.isClosing) return;
|
||||||
|
|
||||||
|
this.isClosing = true;
|
||||||
|
this.classList.add('closing');
|
||||||
|
|
||||||
|
// Wait for the next frame to ensure animation starts
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
// Listen for animation end
|
||||||
|
const menu = this.shadowRoot?.querySelector('.menu');
|
||||||
|
if (menu) {
|
||||||
|
menu.addEventListener(
|
||||||
|
'animationend',
|
||||||
|
() => {
|
||||||
|
this.remove();
|
||||||
|
},
|
||||||
|
{ once: true }
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Fallback if menu not found
|
||||||
|
setTimeout(() => this.remove(), 100);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleItemClick(item: IContextMenuItem): void {
|
||||||
|
if (!item.divider && item.action) {
|
||||||
|
item.action();
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public render(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<div class="menu">
|
||||||
|
${this.items.map((item) =>
|
||||||
|
item.divider
|
||||||
|
? html`<div class="divider"></div>`
|
||||||
|
: html`
|
||||||
|
<button
|
||||||
|
class="menu-item ${item.danger ? 'danger' : ''}"
|
||||||
|
@click=${() => this.handleItemClick(item)}
|
||||||
|
>
|
||||||
|
${item.icon
|
||||||
|
? html`<dees-mobile-icon icon="${item.icon}" size="16"></dees-mobile-icon>`
|
||||||
|
: ''}
|
||||||
|
${item.label || ''}
|
||||||
|
</button>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './dees-mobile-contextmenu.js';
|
||||||
@@ -0,0 +1,213 @@
|
|||||||
|
import { html } from '@design.estate/dees-element';
|
||||||
|
import { injectCssVariables } from '../../00variables.js';
|
||||||
|
import type { DeesMobileGallery } from './dees-mobile-gallery.js';
|
||||||
|
|
||||||
|
export const demoFunc = () => {
|
||||||
|
injectCssVariables();
|
||||||
|
|
||||||
|
// Sample images for demo
|
||||||
|
const sampleImages = [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
url: 'https://picsum.photos/800/1200?random=1',
|
||||||
|
thumbnailUrl: 'https://picsum.photos/100/100?random=1',
|
||||||
|
filename: 'receipt_001.jpg',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
url: 'https://picsum.photos/1200/800?random=2',
|
||||||
|
thumbnailUrl: 'https://picsum.photos/100/100?random=2',
|
||||||
|
filename: 'shopping_photo.jpg',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
url: 'https://picsum.photos/800/800?random=3',
|
||||||
|
thumbnailUrl: 'https://picsum.photos/100/100?random=3',
|
||||||
|
filename: 'receipt_002.jpg',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '4',
|
||||||
|
url: 'https://example.com/document.pdf',
|
||||||
|
filename: 'invoice.pdf',
|
||||||
|
mimeType: 'application/pdf',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<style>
|
||||||
|
.demo-section {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
.demo-section h3 {
|
||||||
|
margin: 0 0 1rem 0;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--dees-muted-foreground);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
.demo-note {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--dees-muted-foreground);
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
.demo-buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="demo-section">
|
||||||
|
<h3>Basic Gallery</h3>
|
||||||
|
<div class="demo-buttons">
|
||||||
|
<dees-mobile-button
|
||||||
|
@click=${() => {
|
||||||
|
const gallery = document.createElement('dees-mobile-gallery') as DeesMobileGallery;
|
||||||
|
gallery.items = sampleImages.slice(0, 3);
|
||||||
|
gallery.config = {
|
||||||
|
showFilename: true,
|
||||||
|
showActions: true,
|
||||||
|
allowDownload: true,
|
||||||
|
allowDelete: false,
|
||||||
|
};
|
||||||
|
gallery.addEventListener('close', () => gallery.remove());
|
||||||
|
gallery.addEventListener('download', (e: CustomEvent) => {
|
||||||
|
console.log('Download requested:', e.detail);
|
||||||
|
});
|
||||||
|
document.body.appendChild(gallery);
|
||||||
|
}}
|
||||||
|
>Open Gallery (3 images)</dees-mobile-button>
|
||||||
|
</div>
|
||||||
|
<p class="demo-note">Fullscreen gallery with swipe navigation on mobile, arrow keys on desktop.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="demo-section">
|
||||||
|
<h3>Single Image</h3>
|
||||||
|
<div class="demo-buttons">
|
||||||
|
<dees-mobile-button
|
||||||
|
variant="outline"
|
||||||
|
@click=${() => {
|
||||||
|
const gallery = document.createElement('dees-mobile-gallery') as DeesMobileGallery;
|
||||||
|
gallery.items = [sampleImages[0]];
|
||||||
|
gallery.config = {
|
||||||
|
showFilename: true,
|
||||||
|
allowDownload: true,
|
||||||
|
};
|
||||||
|
gallery.addEventListener('close', () => gallery.remove());
|
||||||
|
document.body.appendChild(gallery);
|
||||||
|
}}
|
||||||
|
>View Single Image</dees-mobile-button>
|
||||||
|
</div>
|
||||||
|
<p class="demo-note">Single image view without navigation controls.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="demo-section">
|
||||||
|
<h3>With Delete Action</h3>
|
||||||
|
<div class="demo-buttons">
|
||||||
|
<dees-mobile-button
|
||||||
|
variant="outline"
|
||||||
|
@click=${() => {
|
||||||
|
const gallery = document.createElement('dees-mobile-gallery') as DeesMobileGallery;
|
||||||
|
gallery.items = sampleImages.slice(0, 3);
|
||||||
|
gallery.config = {
|
||||||
|
showFilename: true,
|
||||||
|
allowDownload: true,
|
||||||
|
allowDelete: true,
|
||||||
|
};
|
||||||
|
gallery.addEventListener('close', () => gallery.remove());
|
||||||
|
gallery.addEventListener('delete', (e: CustomEvent) => {
|
||||||
|
console.log('Delete requested:', e.detail);
|
||||||
|
gallery.remove();
|
||||||
|
});
|
||||||
|
document.body.appendChild(gallery);
|
||||||
|
}}
|
||||||
|
>Gallery with Delete</dees-mobile-button>
|
||||||
|
</div>
|
||||||
|
<p class="demo-note">Shows delete action button in red.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="demo-section">
|
||||||
|
<h3>Start at Specific Index</h3>
|
||||||
|
<div class="demo-buttons">
|
||||||
|
<dees-mobile-button
|
||||||
|
variant="outline"
|
||||||
|
@click=${() => {
|
||||||
|
const gallery = document.createElement('dees-mobile-gallery') as DeesMobileGallery;
|
||||||
|
gallery.items = sampleImages.slice(0, 3);
|
||||||
|
gallery.config = {
|
||||||
|
startIndex: 2,
|
||||||
|
showFilename: true,
|
||||||
|
allowDownload: true,
|
||||||
|
};
|
||||||
|
gallery.addEventListener('close', () => gallery.remove());
|
||||||
|
document.body.appendChild(gallery);
|
||||||
|
}}
|
||||||
|
>Open at Image 3</dees-mobile-button>
|
||||||
|
</div>
|
||||||
|
<p class="demo-note">Opens gallery starting at the third image.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="demo-section">
|
||||||
|
<h3>With PDF</h3>
|
||||||
|
<div class="demo-buttons">
|
||||||
|
<dees-mobile-button
|
||||||
|
variant="outline"
|
||||||
|
@click=${() => {
|
||||||
|
const gallery = document.createElement('dees-mobile-gallery') as DeesMobileGallery;
|
||||||
|
gallery.items = sampleImages;
|
||||||
|
gallery.config = {
|
||||||
|
startIndex: 3,
|
||||||
|
showFilename: true,
|
||||||
|
allowDownload: true,
|
||||||
|
};
|
||||||
|
gallery.addEventListener('close', () => gallery.remove());
|
||||||
|
document.body.appendChild(gallery);
|
||||||
|
}}
|
||||||
|
>Gallery with PDF</dees-mobile-button>
|
||||||
|
</div>
|
||||||
|
<p class="demo-note">PDF files show a placeholder with filename.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="demo-section">
|
||||||
|
<h3>Static Factory Method</h3>
|
||||||
|
<div class="demo-buttons">
|
||||||
|
<dees-mobile-button
|
||||||
|
variant="outline"
|
||||||
|
@click=${async () => {
|
||||||
|
const { DeesMobileGallery } = await import('./dees-mobile-gallery.js');
|
||||||
|
const result = await DeesMobileGallery.show(
|
||||||
|
sampleImages.slice(0, 3),
|
||||||
|
{
|
||||||
|
showFilename: true,
|
||||||
|
allowDownload: true,
|
||||||
|
allowDelete: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
console.log('Gallery result:', result);
|
||||||
|
}}
|
||||||
|
>Use Static show()</dees-mobile-button>
|
||||||
|
</div>
|
||||||
|
<p class="demo-note">Uses the static factory method that returns a promise.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="demo-section">
|
||||||
|
<h3>Minimal Mode</h3>
|
||||||
|
<div class="demo-buttons">
|
||||||
|
<dees-mobile-button
|
||||||
|
variant="outline"
|
||||||
|
@click=${() => {
|
||||||
|
const gallery = document.createElement('dees-mobile-gallery') as DeesMobileGallery;
|
||||||
|
gallery.items = sampleImages.slice(0, 3);
|
||||||
|
gallery.config = {
|
||||||
|
showFilename: false,
|
||||||
|
showActions: false,
|
||||||
|
};
|
||||||
|
gallery.addEventListener('close', () => gallery.remove());
|
||||||
|
document.body.appendChild(gallery);
|
||||||
|
}}
|
||||||
|
>Minimal Gallery</dees-mobile-button>
|
||||||
|
</div>
|
||||||
|
<p class="demo-note">No filename or action buttons - just images.</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
};
|
||||||
@@ -0,0 +1,650 @@
|
|||||||
|
import {
|
||||||
|
DeesElement,
|
||||||
|
css,
|
||||||
|
cssManager,
|
||||||
|
customElement,
|
||||||
|
html,
|
||||||
|
property,
|
||||||
|
state,
|
||||||
|
type TemplateResult,
|
||||||
|
} from '@design.estate/dees-element';
|
||||||
|
|
||||||
|
import { mobileComponentStyles } from '../../00componentstyles.js';
|
||||||
|
import '../dees-mobile-icon/dees-mobile-icon.js';
|
||||||
|
import { demoFunc } from './dees-mobile-gallery.demo.js';
|
||||||
|
|
||||||
|
export interface IGalleryItem {
|
||||||
|
id: string;
|
||||||
|
url: string;
|
||||||
|
thumbnailUrl?: string;
|
||||||
|
filename?: string;
|
||||||
|
mimeType?: string;
|
||||||
|
metadata?: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IGalleryConfig {
|
||||||
|
showFilename?: boolean;
|
||||||
|
showActions?: boolean;
|
||||||
|
allowDelete?: boolean;
|
||||||
|
allowDownload?: boolean;
|
||||||
|
allowShare?: boolean;
|
||||||
|
startIndex?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'dees-mobile-gallery': DeesMobileGallery;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@customElement('dees-mobile-gallery')
|
||||||
|
export class DeesMobileGallery extends DeesElement {
|
||||||
|
public static demo = demoFunc;
|
||||||
|
|
||||||
|
@property({ type: Array })
|
||||||
|
accessor items: IGalleryItem[] = [];
|
||||||
|
|
||||||
|
@property({ type: Object })
|
||||||
|
accessor config: IGalleryConfig = {};
|
||||||
|
|
||||||
|
@state()
|
||||||
|
accessor currentIndex: number = 0;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
accessor isLoading: boolean = true;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
accessor showThumbnails: boolean = false;
|
||||||
|
|
||||||
|
// Touch/swipe state
|
||||||
|
private touchStartX: number = 0;
|
||||||
|
private touchStartY: number = 0;
|
||||||
|
private touchDeltaX: number = 0;
|
||||||
|
private isSwiping: boolean = false;
|
||||||
|
private swipeThreshold: number = 50;
|
||||||
|
|
||||||
|
public static styles = [
|
||||||
|
cssManager.defaultStyles,
|
||||||
|
mobileComponentStyles,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: var(--dees-z-modal, 500);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: rgba(0, 0, 0, 0);
|
||||||
|
animation: fadeInGallery 0.2s ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeInGallery {
|
||||||
|
to {
|
||||||
|
background: rgba(0, 0, 0, 0.95);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: var(--dees-space-md);
|
||||||
|
padding-top: calc(var(--dees-space-md) + env(safe-area-inset-top, 0px));
|
||||||
|
background: linear-gradient(to bottom, rgba(0, 0, 0, 0.5), transparent);
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--dees-space-sm);
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-button {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border: none;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: var(--dees-radius-full);
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: background var(--dees-transition-fast);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-button:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filename {
|
||||||
|
color: white;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.counter {
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
touch-action: pan-y pinch-zoom;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: transform 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-container.swiping {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-image {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-image.loaded {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
position: absolute;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border: 3px solid rgba(255, 255, 255, 0.2);
|
||||||
|
border-top-color: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-button {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border: none;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: var(--dees-radius-full);
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: background var(--dees-transition-fast), opacity var(--dees-transition-fast);
|
||||||
|
z-index: 5;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-button:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-button.prev {
|
||||||
|
left: var(--dees-space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-button.next {
|
||||||
|
right: var(--dees-space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-button:disabled {
|
||||||
|
opacity: 0.3;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide nav buttons on mobile - use swipe instead */
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.nav-button {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-footer {
|
||||||
|
padding: var(--dees-space-md);
|
||||||
|
padding-bottom: calc(var(--dees-space-md) + env(safe-area-inset-bottom, 0px));
|
||||||
|
background: linear-gradient(to top, rgba(0, 0, 0, 0.5), transparent);
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: var(--dees-space-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--dees-space-xs);
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: var(--dees-space-sm);
|
||||||
|
border-radius: var(--dees-radius-md);
|
||||||
|
transition: background var(--dees-transition-fast);
|
||||||
|
min-width: 64px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button .icon {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
border-radius: var(--dees-radius-full);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button .label {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button.danger .icon {
|
||||||
|
background: rgba(220, 38, 38, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button.danger {
|
||||||
|
color: #fca5a5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumbnails {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--dees-space-xs);
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: var(--dees-space-md);
|
||||||
|
overflow-x: auto;
|
||||||
|
padding: 0 var(--dees-space-md);
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumbnail {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: var(--dees-radius-sm);
|
||||||
|
object-fit: cover;
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0.5;
|
||||||
|
transition: opacity var(--dees-transition-fast), transform var(--dees-transition-fast);
|
||||||
|
flex-shrink: 0;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumbnail:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumbnail.active {
|
||||||
|
opacity: 1;
|
||||||
|
border-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-preview {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: var(--dees-space-md);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-icon {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: var(--dees-radius-lg);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-text {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
|
||||||
|
async connectedCallback() {
|
||||||
|
await super.connectedCallback();
|
||||||
|
this.currentIndex = this.config.startIndex ?? 0;
|
||||||
|
document.addEventListener('keydown', this.handleKeydown);
|
||||||
|
}
|
||||||
|
|
||||||
|
async disconnectedCallback() {
|
||||||
|
await super.disconnectedCallback();
|
||||||
|
document.removeEventListener('keydown', this.handleKeydown);
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleKeydown = (e: KeyboardEvent) => {
|
||||||
|
switch (e.key) {
|
||||||
|
case 'Escape':
|
||||||
|
this.handleClose();
|
||||||
|
break;
|
||||||
|
case 'ArrowLeft':
|
||||||
|
this.goToPrevious();
|
||||||
|
break;
|
||||||
|
case 'ArrowRight':
|
||||||
|
this.goToNext();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleClose() {
|
||||||
|
this.dispatchEvent(new CustomEvent('close', {
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleTouchStart = (e: TouchEvent) => {
|
||||||
|
this.touchStartX = e.touches[0].clientX;
|
||||||
|
this.touchStartY = e.touches[0].clientY;
|
||||||
|
this.touchDeltaX = 0;
|
||||||
|
this.isSwiping = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleTouchMove = (e: TouchEvent) => {
|
||||||
|
const deltaX = e.touches[0].clientX - this.touchStartX;
|
||||||
|
const deltaY = e.touches[0].clientY - this.touchStartY;
|
||||||
|
|
||||||
|
// Only swipe horizontally if horizontal movement is greater than vertical
|
||||||
|
if (Math.abs(deltaX) > Math.abs(deltaY) && Math.abs(deltaX) > 10) {
|
||||||
|
this.isSwiping = true;
|
||||||
|
this.touchDeltaX = deltaX;
|
||||||
|
this.requestUpdate();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleTouchEnd = () => {
|
||||||
|
if (this.isSwiping) {
|
||||||
|
if (this.touchDeltaX > this.swipeThreshold) {
|
||||||
|
this.goToPrevious();
|
||||||
|
} else if (this.touchDeltaX < -this.swipeThreshold) {
|
||||||
|
this.goToNext();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.isSwiping = false;
|
||||||
|
this.touchDeltaX = 0;
|
||||||
|
this.requestUpdate();
|
||||||
|
};
|
||||||
|
|
||||||
|
private goToPrevious() {
|
||||||
|
if (this.currentIndex > 0) {
|
||||||
|
this.currentIndex--;
|
||||||
|
this.isLoading = true;
|
||||||
|
this.dispatchChangeEvent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private goToNext() {
|
||||||
|
if (this.currentIndex < this.items.length - 1) {
|
||||||
|
this.currentIndex++;
|
||||||
|
this.isLoading = true;
|
||||||
|
this.dispatchChangeEvent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private goToIndex(index: number) {
|
||||||
|
if (index >= 0 && index < this.items.length && index !== this.currentIndex) {
|
||||||
|
this.currentIndex = index;
|
||||||
|
this.isLoading = true;
|
||||||
|
this.dispatchChangeEvent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private dispatchChangeEvent() {
|
||||||
|
this.dispatchEvent(new CustomEvent('change', {
|
||||||
|
detail: {
|
||||||
|
index: this.currentIndex,
|
||||||
|
item: this.items[this.currentIndex],
|
||||||
|
},
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleImageLoad() {
|
||||||
|
this.isLoading = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleDownload() {
|
||||||
|
const item = this.items[this.currentIndex];
|
||||||
|
this.dispatchEvent(new CustomEvent('download', {
|
||||||
|
detail: item,
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleDelete() {
|
||||||
|
const item = this.items[this.currentIndex];
|
||||||
|
this.dispatchEvent(new CustomEvent('delete', {
|
||||||
|
detail: item,
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleShare() {
|
||||||
|
const item = this.items[this.currentIndex];
|
||||||
|
this.dispatchEvent(new CustomEvent('share', {
|
||||||
|
detail: item,
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
private isPdf(item: IGalleryItem): boolean {
|
||||||
|
return item.mimeType?.toLowerCase() === 'application/pdf' ||
|
||||||
|
item.filename?.toLowerCase().endsWith('.pdf') ||
|
||||||
|
item.url.toLowerCase().endsWith('.pdf');
|
||||||
|
}
|
||||||
|
|
||||||
|
public render(): TemplateResult {
|
||||||
|
const currentItem = this.items[this.currentIndex];
|
||||||
|
const showFilename = this.config.showFilename ?? true;
|
||||||
|
const showActions = this.config.showActions ?? true;
|
||||||
|
const allowDelete = this.config.allowDelete ?? false;
|
||||||
|
const allowDownload = this.config.allowDownload ?? true;
|
||||||
|
const allowShare = this.config.allowShare ?? false;
|
||||||
|
const showThumbnails = this.items.length > 1;
|
||||||
|
|
||||||
|
const swipeTransform = this.isSwiping ? `translateX(${this.touchDeltaX}px)` : '';
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="gallery-header">
|
||||||
|
<div class="header-left">
|
||||||
|
<button class="close-button" @click=${this.handleClose}>
|
||||||
|
<dees-mobile-icon icon="x" size="24"></dees-mobile-icon>
|
||||||
|
</button>
|
||||||
|
${showFilename && currentItem?.filename ? html`
|
||||||
|
<span class="filename">${currentItem.filename}</span>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
${this.items.length > 1 ? html`
|
||||||
|
<span class="counter">${this.currentIndex + 1} / ${this.items.length}</span>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="gallery-content"
|
||||||
|
@touchstart=${this.handleTouchStart}
|
||||||
|
@touchmove=${this.handleTouchMove}
|
||||||
|
@touchend=${this.handleTouchEnd}
|
||||||
|
>
|
||||||
|
${this.isLoading ? html`<div class="loading-spinner"></div>` : ''}
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="image-container ${this.isSwiping ? 'swiping' : ''}"
|
||||||
|
style=${swipeTransform ? `transform: ${swipeTransform}` : ''}
|
||||||
|
>
|
||||||
|
${currentItem ? (
|
||||||
|
this.isPdf(currentItem) ? html`
|
||||||
|
<div class="pdf-preview">
|
||||||
|
<div class="pdf-icon">
|
||||||
|
<dees-mobile-icon icon="file-text" size="48" color="white"></dees-mobile-icon>
|
||||||
|
</div>
|
||||||
|
<span class="pdf-text">${currentItem.filename || 'PDF Document'}</span>
|
||||||
|
</div>
|
||||||
|
` : html`
|
||||||
|
<img
|
||||||
|
class="gallery-image ${!this.isLoading ? 'loaded' : ''}"
|
||||||
|
src=${currentItem.url}
|
||||||
|
alt=${currentItem.filename || ''}
|
||||||
|
@load=${this.handleImageLoad}
|
||||||
|
/>
|
||||||
|
`
|
||||||
|
) : ''}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${this.items.length > 1 ? html`
|
||||||
|
<button
|
||||||
|
class="nav-button prev"
|
||||||
|
@click=${this.goToPrevious}
|
||||||
|
?disabled=${this.currentIndex === 0}
|
||||||
|
>
|
||||||
|
<dees-mobile-icon icon="chevron-left" size="28"></dees-mobile-icon>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="nav-button next"
|
||||||
|
@click=${this.goToNext}
|
||||||
|
?disabled=${this.currentIndex === this.items.length - 1}
|
||||||
|
>
|
||||||
|
<dees-mobile-icon icon="chevron-right" size="28"></dees-mobile-icon>
|
||||||
|
</button>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="gallery-footer">
|
||||||
|
${showThumbnails ? html`
|
||||||
|
<div class="thumbnails">
|
||||||
|
${this.items.map((item, index) => html`
|
||||||
|
<img
|
||||||
|
class="thumbnail ${index === this.currentIndex ? 'active' : ''}"
|
||||||
|
src=${item.thumbnailUrl || item.url}
|
||||||
|
alt=""
|
||||||
|
@click=${() => this.goToIndex(index)}
|
||||||
|
/>
|
||||||
|
`)}
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
|
||||||
|
${showActions ? html`
|
||||||
|
<div class="actions">
|
||||||
|
${allowDownload ? html`
|
||||||
|
<button class="action-button" @click=${this.handleDownload}>
|
||||||
|
<div class="icon">
|
||||||
|
<dees-mobile-icon icon="download" size="22"></dees-mobile-icon>
|
||||||
|
</div>
|
||||||
|
<span class="label">Download</span>
|
||||||
|
</button>
|
||||||
|
` : ''}
|
||||||
|
|
||||||
|
${allowShare ? html`
|
||||||
|
<button class="action-button" @click=${this.handleShare}>
|
||||||
|
<div class="icon">
|
||||||
|
<dees-mobile-icon icon="share" size="22"></dees-mobile-icon>
|
||||||
|
</div>
|
||||||
|
<span class="label">Share</span>
|
||||||
|
</button>
|
||||||
|
` : ''}
|
||||||
|
|
||||||
|
${allowDelete ? html`
|
||||||
|
<button class="action-button danger" @click=${this.handleDelete}>
|
||||||
|
<div class="icon">
|
||||||
|
<dees-mobile-icon icon="trash-2" size="22"></dees-mobile-icon>
|
||||||
|
</div>
|
||||||
|
<span class="label">Delete</span>
|
||||||
|
</button>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Static factory method to show the gallery
|
||||||
|
*/
|
||||||
|
public static async show(
|
||||||
|
items: IGalleryItem[],
|
||||||
|
config: IGalleryConfig = {}
|
||||||
|
): Promise<{ action: 'close' | 'delete' | 'download' | 'share'; item?: IGalleryItem }> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const gallery = document.createElement('dees-mobile-gallery') as DeesMobileGallery;
|
||||||
|
gallery.items = items;
|
||||||
|
gallery.config = config;
|
||||||
|
|
||||||
|
const cleanup = () => {
|
||||||
|
gallery.remove();
|
||||||
|
};
|
||||||
|
|
||||||
|
gallery.addEventListener('close', () => {
|
||||||
|
cleanup();
|
||||||
|
resolve({ action: 'close' });
|
||||||
|
});
|
||||||
|
|
||||||
|
gallery.addEventListener('delete', (e: CustomEvent) => {
|
||||||
|
cleanup();
|
||||||
|
resolve({ action: 'delete', item: e.detail });
|
||||||
|
});
|
||||||
|
|
||||||
|
gallery.addEventListener('download', (e: CustomEvent) => {
|
||||||
|
// Don't close on download - user might want to download multiple
|
||||||
|
resolve({ action: 'download', item: e.detail });
|
||||||
|
});
|
||||||
|
|
||||||
|
gallery.addEventListener('share', (e: CustomEvent) => {
|
||||||
|
resolve({ action: 'share', item: e.detail });
|
||||||
|
});
|
||||||
|
|
||||||
|
document.body.appendChild(gallery);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
1
ts_web/elements/00group-ui/dees-mobile-gallery/index.ts
Normal file
1
ts_web/elements/00group-ui/dees-mobile-gallery/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './dees-mobile-gallery.js';
|
||||||
@@ -131,10 +131,11 @@ export class DeesMobileHeader extends DeesElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
::slotted([slot="actions"]) {
|
::slotted([slot="actions"]) {
|
||||||
width: 2.5rem;
|
min-width: 2.5rem;
|
||||||
height: 2.5rem;
|
min-height: 2.5rem;
|
||||||
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
|
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
|
||||||
display: block;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
::slotted([slot="actions"]:hover) {
|
::slotted([slot="actions"]:hover) {
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import { html, type TemplateResult } from '@design.estate/dees-element';
|
||||||
|
import './dees-mobile-iconbutton.js';
|
||||||
|
import '../dees-mobile-icon/dees-mobile-icon.js';
|
||||||
|
|
||||||
|
export const demoFunc = (): TemplateResult => {
|
||||||
|
return html`
|
||||||
|
<style>
|
||||||
|
.demo-container {
|
||||||
|
padding: 2rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-label {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
color: #71717a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="demo-container">
|
||||||
|
<div class="demo-section">
|
||||||
|
<div class="demo-label">Sizes</div>
|
||||||
|
<div class="demo-row">
|
||||||
|
<dees-mobile-iconbutton size="sm" label="Small">
|
||||||
|
<dees-mobile-icon icon="settings" size="16"></dees-mobile-icon>
|
||||||
|
</dees-mobile-iconbutton>
|
||||||
|
|
||||||
|
<dees-mobile-iconbutton size="md" label="Medium">
|
||||||
|
<dees-mobile-icon icon="settings" size="20"></dees-mobile-icon>
|
||||||
|
</dees-mobile-iconbutton>
|
||||||
|
|
||||||
|
<dees-mobile-iconbutton size="lg" label="Large">
|
||||||
|
<dees-mobile-icon icon="settings" size="24"></dees-mobile-icon>
|
||||||
|
</dees-mobile-iconbutton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="demo-section">
|
||||||
|
<div class="demo-label">Common Actions</div>
|
||||||
|
<div class="demo-row">
|
||||||
|
<dees-mobile-iconbutton label="Edit">
|
||||||
|
<dees-mobile-icon icon="pencil" size="20"></dees-mobile-icon>
|
||||||
|
</dees-mobile-iconbutton>
|
||||||
|
|
||||||
|
<dees-mobile-iconbutton label="Delete">
|
||||||
|
<dees-mobile-icon icon="trash-2" size="20"></dees-mobile-icon>
|
||||||
|
</dees-mobile-iconbutton>
|
||||||
|
|
||||||
|
<dees-mobile-iconbutton label="Share">
|
||||||
|
<dees-mobile-icon icon="share" size="20"></dees-mobile-icon>
|
||||||
|
</dees-mobile-iconbutton>
|
||||||
|
|
||||||
|
<dees-mobile-iconbutton label="More">
|
||||||
|
<dees-mobile-icon icon="more-vertical" size="20"></dees-mobile-icon>
|
||||||
|
</dees-mobile-iconbutton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="demo-section">
|
||||||
|
<div class="demo-label">States</div>
|
||||||
|
<div class="demo-row">
|
||||||
|
<dees-mobile-iconbutton label="Normal">
|
||||||
|
<dees-mobile-icon icon="check" size="20"></dees-mobile-icon>
|
||||||
|
</dees-mobile-iconbutton>
|
||||||
|
|
||||||
|
<dees-mobile-iconbutton label="Disabled" disabled>
|
||||||
|
<dees-mobile-icon icon="check" size="20"></dees-mobile-icon>
|
||||||
|
</dees-mobile-iconbutton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
};
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
import {
|
||||||
|
DeesElement,
|
||||||
|
css,
|
||||||
|
cssManager,
|
||||||
|
customElement,
|
||||||
|
html,
|
||||||
|
property,
|
||||||
|
type TemplateResult,
|
||||||
|
} from '@design.estate/dees-element';
|
||||||
|
|
||||||
|
import { mobileComponentStyles } from '../../00componentstyles.js';
|
||||||
|
import { demoFunc } from './dees-mobile-iconbutton.demo.js';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'dees-mobile-iconbutton': DeesMobileIconbutton;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@customElement('dees-mobile-iconbutton')
|
||||||
|
export class DeesMobileIconbutton extends DeesElement {
|
||||||
|
public static demo = demoFunc;
|
||||||
|
|
||||||
|
@property({ type: String })
|
||||||
|
accessor label: string = '';
|
||||||
|
|
||||||
|
@property({ type: Boolean })
|
||||||
|
accessor disabled: boolean = false;
|
||||||
|
|
||||||
|
@property({ type: String })
|
||||||
|
accessor size: 'sm' | 'md' | 'lg' = 'md';
|
||||||
|
|
||||||
|
public static styles = [
|
||||||
|
cssManager.defaultStyles,
|
||||||
|
mobileComponentStyles,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 150ms ease;
|
||||||
|
transform: scale(1);
|
||||||
|
position: relative;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover:not(:disabled) {
|
||||||
|
background-color: ${cssManager.bdTheme('#f4f4f5', '#27272a')};
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
button:active:not(:disabled) {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
button:focus-visible {
|
||||||
|
outline: 2px solid ${cssManager.bdTheme('#3b82f6', '#60a5fa')};
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sizes */
|
||||||
|
button.sm {
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.md {
|
||||||
|
width: 2.5rem;
|
||||||
|
height: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.lg {
|
||||||
|
width: 3rem;
|
||||||
|
height: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
::slotted(svg),
|
||||||
|
::slotted(div),
|
||||||
|
::slotted(dees-mobile-icon) {
|
||||||
|
width: 1.25rem;
|
||||||
|
height: 1.25rem;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.sm ::slotted(svg),
|
||||||
|
button.sm ::slotted(div),
|
||||||
|
button.sm ::slotted(dees-mobile-icon) {
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.lg ::slotted(svg),
|
||||||
|
button.lg ::slotted(div),
|
||||||
|
button.lg ::slotted(dees-mobile-icon) {
|
||||||
|
width: 1.5rem;
|
||||||
|
height: 1.5rem;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
|
||||||
|
public render(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<button
|
||||||
|
class=${this.size}
|
||||||
|
?disabled=${this.disabled}
|
||||||
|
aria-label=${this.label}
|
||||||
|
title=${this.label}
|
||||||
|
>
|
||||||
|
<slot></slot>
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './dees-mobile-iconbutton.js';
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
// Core UI Components
|
// Core UI Components
|
||||||
export * from './dees-mobile-button/index.js';
|
export * from './dees-mobile-button/index.js';
|
||||||
|
export * from './dees-mobile-contextmenu/index.js';
|
||||||
|
export * from './dees-mobile-gallery/index.js';
|
||||||
export * from './dees-mobile-icon/index.js';
|
export * from './dees-mobile-icon/index.js';
|
||||||
|
export * from './dees-mobile-iconbutton/index.js';
|
||||||
export * from './dees-mobile-header/index.js';
|
export * from './dees-mobile-header/index.js';
|
||||||
export * from './dees-mobile-modal/index.js';
|
export * from './dees-mobile-modal/index.js';
|
||||||
export * from './dees-mobile-actionsheet/index.js';
|
export * from './dees-mobile-actionsheet/index.js';
|
||||||
|
|||||||
Reference in New Issue
Block a user