Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f5cc69ed53 | |||
| 57cbb739d2 | |||
| 90836f1c72 | |||
| a9c2d2230c | |||
| 3d266c89b2 | |||
| 56c087bc3a | |||
| 6d53259b75 | |||
| eefcd4f807 | |||
| ce9db08ca0 | |||
| 247a401982 | |||
| 68164f4d9e | |||
| 6c99b95ed7 |
+2
-1
@@ -1,4 +1,5 @@
|
||||
.nogit/
|
||||
.playwright-mcp/
|
||||
|
||||
# artifacts
|
||||
coverage/
|
||||
@@ -17,4 +18,4 @@ node_modules/
|
||||
dist/
|
||||
dist_*/
|
||||
|
||||
# custom
|
||||
# custom
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 47 KiB |
@@ -0,0 +1,82 @@
|
||||
{
|
||||
"@git.zone/cli": {
|
||||
"projectType": "wcc",
|
||||
"module": {
|
||||
"githost": "gitlab.com",
|
||||
"gitscope": "signature.digital",
|
||||
"gitrepo": "catalog",
|
||||
"description": "A comprehensive catalog of customizable web components designed for building and managing e-signature applications.",
|
||||
"npmPackagename": "@signature.digital/catalog",
|
||||
"license": "MIT",
|
||||
"projectDomain": "signature.digital",
|
||||
"keywords": [
|
||||
"e-signature",
|
||||
"web components",
|
||||
"digital signature",
|
||||
"signature capture",
|
||||
"ECMAScript Modules",
|
||||
"typescript",
|
||||
"component library",
|
||||
"contract management",
|
||||
"frontend development",
|
||||
"signature pad",
|
||||
"custom elements",
|
||||
"electronic signing",
|
||||
"npm package"
|
||||
]
|
||||
},
|
||||
"release": {
|
||||
"registries": [
|
||||
"https://verdaccio.lossless.digital",
|
||||
"https://registry.npmjs.org"
|
||||
],
|
||||
"accessLevel": "public"
|
||||
}
|
||||
},
|
||||
"@git.zone/tsbundle": {
|
||||
"bundles": [
|
||||
{
|
||||
"from": "./html/index.ts",
|
||||
"to": "./dist_bundle/bundle.js",
|
||||
"outputMode": "bundle",
|
||||
"bundler": "esbuild",
|
||||
"production": true,
|
||||
"includeFiles": [
|
||||
"./html/index.html"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"@git.zone/tswatch": {
|
||||
"server": {
|
||||
"enabled": true,
|
||||
"port": 3002,
|
||||
"serveDir": "./dist_watch/",
|
||||
"liveReload": true
|
||||
},
|
||||
"bundles": [
|
||||
{
|
||||
"name": "element-bundle",
|
||||
"from": "./html/index.ts",
|
||||
"to": "./dist_watch/bundle.js",
|
||||
"watchPatterns": [
|
||||
"./ts_web/**/*",
|
||||
"./html/index.ts"
|
||||
],
|
||||
"triggerReload": true,
|
||||
"bundler": "esbuild",
|
||||
"production": false
|
||||
},
|
||||
{
|
||||
"name": "html",
|
||||
"from": "./html/index.html",
|
||||
"to": "./dist_watch/index.html",
|
||||
"watchPatterns": [
|
||||
"./html/**/*"
|
||||
],
|
||||
"triggerReload": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"@ship.zone/szci": {}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
# Changelog
|
||||
|
||||
## 2026-05-02 - 1.3.0 - feat(workspace)
|
||||
introduce a responsive signature workspace demo and remove legacy contract editor components
|
||||
|
||||
- Adds a new `sdig-workspace` component with inbox, compose, sign, audit, developers, templates, team, and settings views.
|
||||
- Removes the previous contract editor module and related contract subcomponents from exports and source files.
|
||||
- Updates package exports, build configuration, and project metadata for the new workspace-focused catalog distribution.
|
||||
- Improves sign pad and sign box null-safety and groups demos under signature primitives and workspace categories.
|
||||
|
||||
## 2025-12-18 - 1.2.0 - feat(icons)
|
||||
migrate icon usage to the new dees-icon API and integrate collaboration sidebar into the editor
|
||||
|
||||
- Replaced deprecated .iconFA with .icon across multiple components
|
||||
- Updated lucide icon identifiers to PascalCase/camelCase to match new dees-icon format
|
||||
- Added sdig-collaboration-sidebar component and exported it from elements index
|
||||
- Integrated a toggleable editor sidebar (PanelRight) and wired comment/suggestion navigation & add-comment events in sdig-contracteditor
|
||||
- Added development hints (readme.hints.md) documenting dees-icon usage and icon name formats
|
||||
- Minor UI/styling tweak: .btn-ghost.active appearance
|
||||
|
||||
## 2025-12-18 - 1.1.0 - feat(catalog)
|
||||
add ContractEditor and many editor subcomponents; implement SignPad and SignBox; update README and bump dependencies
|
||||
|
||||
- New contract editor module with many subcomponents added (header, metadata, parties, content, terms, signatures, attachments, collaboration, audit)
|
||||
- Implemented signature UI components: sdig-signpad and sdig-signbox (canvas-based signature capture, undo/clear/export APIs)
|
||||
- Reorganized exports (ts_web/elements/index.ts) to expose new submodules and barrel files
|
||||
- Added editor types and smartstate store (ts_web/elements/sdig-contracteditor/types.ts and state.ts) for editor state management, undo/redo and events
|
||||
- Large README overhaul: improved usage examples, API docs, security/issue reporting section, theming and integration examples
|
||||
- Dependencies bumped in package.json (notable bumps: @design.estate/* packages, @git.zone tooling, signature_pad) and pnpm overrides added
|
||||
- tsconfig.json compiler options modified (removed experimentalDecorators and useDefineForClassFields) — may affect local build configurations
|
||||
|
||||
## 2024-12-19 - 1.0.59 - fix(dependencies)
|
||||
Update package dependencies and project metadata
|
||||
|
||||
- Updated package dependencies to the latest versions in package.json
|
||||
- Synchronized project description and keywords in npmextra.json with package.json
|
||||
|
||||
## 2023-11-28 - 1.0.55 to 1.0.58 - core updates
|
||||
Main changes include fixing and updating core functionalities.
|
||||
|
||||
- Fixed core issues and updated core functionalities in versions 1.0.55, 1.0.56, and 1.0.57.
|
||||
- Further updates and improvements were carried out in version 1.0.58.
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
// dees tools
|
||||
import * as deesWccTools from '@designestate/dees-wcctools';
|
||||
import * as deesDomTools from '@designestate/dees-domtools';
|
||||
import * as deesWccTools from '@design.estate/dees-wcctools';
|
||||
import * as deesDomTools from '@design.estate/dees-domtools';
|
||||
|
||||
// elements and pages
|
||||
import * as elements from '../ts_web/elements/index.js';
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
Copyright (c) 2014 Task Venture Capital GmbH (hello@task.vc)
|
||||
|
||||
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,18 +0,0 @@
|
||||
{
|
||||
"gitzone": {
|
||||
"projectType": "wcc",
|
||||
"module": {
|
||||
"githost": "gitlab.com",
|
||||
"gitscope": "signature.digital_private",
|
||||
"gitrepo": "catalog",
|
||||
"description": "a catalog containing components for e-signing",
|
||||
"npmPackagename": "@signature.digital_private/catalog",
|
||||
"license": "UNLICENSED",
|
||||
"projectDomain": "signature.digital"
|
||||
}
|
||||
},
|
||||
"npmci": {
|
||||
"npmGlobalTools": [],
|
||||
"npmAccessLevel": "private"
|
||||
}
|
||||
}
|
||||
+38
-21
@@ -1,32 +1,33 @@
|
||||
{
|
||||
"name": "@signature.digital_private/catalog",
|
||||
"version": "1.0.56",
|
||||
"name": "@signature.digital/catalog",
|
||||
"version": "1.3.0",
|
||||
"private": false,
|
||||
"description": "a catalog containing components for e-signing",
|
||||
"main": "dist_ts_web/index.js",
|
||||
"typings": "dist_ts_web/index.d.ts",
|
||||
"description": "A comprehensive catalog of customizable web components designed for building and managing e-signature applications.",
|
||||
"exports": {
|
||||
".": "./dist_ts_web/index.js"
|
||||
},
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "npm run build",
|
||||
"build": "tsbuild element && tsbundle element --production",
|
||||
"watch": "tswatch element"
|
||||
"test": "pnpm run build",
|
||||
"build": "tsbuild tsfolders --allowimplicitany && tsbundle",
|
||||
"watch": "tswatch"
|
||||
},
|
||||
"author": "Task Venture Capital GmbH",
|
||||
"license": "UNLICENSED",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@designestate/dees-domtools": "^1.0.41",
|
||||
"@designestate/dees-element": "^1.0.26",
|
||||
"@designestate/dees-wcctools": "^1.0.37",
|
||||
"@git.zone/tsrun": "^1.2.12",
|
||||
"@losslessone_private/loint-pubapi": "^1.0.9",
|
||||
"@pushrocks/smartexpress": "^3.0.76",
|
||||
"typescript": "^4.4.3"
|
||||
"@design.estate/dees-catalog": "3.81.0",
|
||||
"@design.estate/dees-domtools": "^2.5.6",
|
||||
"@design.estate/dees-element": "^2.2.4",
|
||||
"@design.estate/dees-wcctools": "^3.9.0",
|
||||
"@git.zone/tsrun": "^2.0.3",
|
||||
"signature_pad": "^5.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@git.zone/tsbuild": "^2.1.24",
|
||||
"@git.zone/tsbundle": "^1.0.72",
|
||||
"@git.zone/tswatch": "^1.0.50",
|
||||
"@pushrocks/projectinfo": "^4.0.5"
|
||||
"@git.zone/tsbuild": "^4.4.0",
|
||||
"@git.zone/tsbundle": "2.10.1",
|
||||
"@git.zone/tswatch": "^3.3.3",
|
||||
"@push.rocks/projectinfo": "^5.1.0",
|
||||
"@types/node": "^25.6.0"
|
||||
},
|
||||
"files": [
|
||||
"ts/**/*",
|
||||
@@ -37,10 +38,26 @@
|
||||
"dist_ts_web/**/*",
|
||||
"assets/**/*",
|
||||
"cli.js",
|
||||
"npmextra.json",
|
||||
".smartconfig.json",
|
||||
"license",
|
||||
"readme.md"
|
||||
],
|
||||
"browserslist": [
|
||||
"last 1 Chrome versions"
|
||||
],
|
||||
"keywords": [
|
||||
"e-signature",
|
||||
"web components",
|
||||
"digital signature",
|
||||
"signature capture",
|
||||
"ECMAScript Modules",
|
||||
"typescript",
|
||||
"component library",
|
||||
"contract management",
|
||||
"frontend development",
|
||||
"signature pad",
|
||||
"custom elements",
|
||||
"electronic signing",
|
||||
"npm package"
|
||||
]
|
||||
}
|
||||
|
||||
Generated
+6673
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,31 @@
|
||||
# Development Hints for @signature.digital/catalog
|
||||
|
||||
## dees-icon Usage
|
||||
|
||||
**Important**: Use the `.icon` property, NOT `.iconFA` (deprecated).
|
||||
|
||||
### Format
|
||||
```html
|
||||
<dees-icon .icon=${'prefix:IconName'}></dees-icon>
|
||||
```
|
||||
|
||||
### Lucide Icons (PascalCase)
|
||||
Lucide icons use **PascalCase** names:
|
||||
- `lucide:CheckCircle` ✓
|
||||
- `lucide:UserPlus` ✓
|
||||
- `lucide:PenTool` ✓
|
||||
- `lucide:Mail` ✓
|
||||
- `lucide:Users` ✓
|
||||
|
||||
**Wrong formats**:
|
||||
- `lucide:check-circle` ✗ (kebab-case doesn't work)
|
||||
- `lucide:userPlus` ✗ (camelCase doesn't work)
|
||||
|
||||
### FontAwesome Icons (camelCase)
|
||||
FontAwesome icons use **camelCase** names:
|
||||
- `fa:arrowRight`
|
||||
- `fa:magnifyingGlass`
|
||||
- `fa:penToSquare`
|
||||
|
||||
### Documentation
|
||||
See: https://code.foss.global/design.estate/dees-catalog/src/branch/main/readme.icons.md
|
||||
@@ -1,31 +1,172 @@
|
||||
# @signature.digital_private/catalog
|
||||
a catalog containing components for e-signing
|
||||
# @signature.digital/catalog
|
||||
|
||||
## Availabililty and Links
|
||||
* [npmjs.org (npm package)](https://www.npmjs.com/package/@signature.digital_private/catalog)
|
||||
* [gitlab.com (source)](https://gitlab.com/signature.digital_private/catalog)
|
||||
* [github.com (source mirror)](https://github.com/signature.digital_private/catalog)
|
||||
* [docs (typedoc)](https://signature.digital_private.gitlab.io/catalog/)
|
||||
Reusable web components for signature.digital product surfaces. The package ships signature capture primitives, a complete signature box, and a responsive workspace shell built with Lit and `@design.estate/dees-element`.
|
||||
|
||||
## Status for master
|
||||
Use it when you need browser-native custom elements for signing flows without pulling product-specific authentication, persistence, routing, or API behavior into the component layer.
|
||||
|
||||
Status Category | Status Badge
|
||||
-- | --
|
||||
GitLab Pipelines | [](https://lossless.cloud)
|
||||
GitLab Pipline Test Coverage | [](https://lossless.cloud)
|
||||
npm | [](https://lossless.cloud)
|
||||
Snyk | [](https://lossless.cloud)
|
||||
TypeScript Support | [](https://lossless.cloud)
|
||||
node Support | [](https://nodejs.org/dist/latest-v10.x/docs/api/)
|
||||
Code Style | [](https://lossless.cloud)
|
||||
PackagePhobia (total standalone install weight) | [](https://lossless.cloud)
|
||||
PackagePhobia (package size on registry) | [](https://lossless.cloud)
|
||||
BundlePhobia (total size when bundled) | [](https://lossless.cloud)
|
||||
## Issue Reporting and Security
|
||||
|
||||
## Usage
|
||||
Use TypeScript for best in class intellisense
|
||||
For further information read the linked docs at the top of this readme.
|
||||
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.
|
||||
|
||||
## Legal
|
||||
> UNLICENSED licensed | **©** [Task Venture Capital GmbH](https://task.vc)
|
||||
| By using this npm module you agree to our [privacy policy](https://lossless.gmbH/privacy)
|
||||
## Install
|
||||
|
||||
```shell
|
||||
pnpm add @signature.digital/catalog
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
Register all elements once in your browser bundle:
|
||||
|
||||
```typescript
|
||||
import '@signature.digital/catalog';
|
||||
```
|
||||
|
||||
Render the product workspace:
|
||||
|
||||
```html
|
||||
<sdig-workspace accent="#3b82f6" density="comfortable" theme="dark"></sdig-workspace>
|
||||
```
|
||||
|
||||
Render a standalone signature box:
|
||||
|
||||
```html
|
||||
<sdig-signbox></sdig-signbox>
|
||||
```
|
||||
|
||||
## Components
|
||||
|
||||
| Component | Tag | Purpose |
|
||||
|-----------|-----|---------|
|
||||
| `SdigWorkspace` | `<sdig-workspace>` | Product workspace shell with inbox, compose, signing, audit, developer, template, team, and settings views |
|
||||
| `SignBox` | `<sdig-signbox>` | Complete signature capture UI with clear, undo, and submit actions |
|
||||
| `SignPad` | `<sdig-signpad>` | Canvas-based signature capture primitive backed by `signature_pad` |
|
||||
|
||||
## Workspace Shell
|
||||
|
||||
`sdig-workspace` is the full product surface. It is responsive, supports dark/light themes, compact/comfortable density, an accent color, and a configurable initial view.
|
||||
|
||||
```html
|
||||
<sdig-workspace
|
||||
accent="#7c3aed"
|
||||
density="compact"
|
||||
theme="light"
|
||||
initialView="compose"
|
||||
></sdig-workspace>
|
||||
```
|
||||
|
||||
### Workspace Properties
|
||||
|
||||
| Property | Type | Default | Description |
|
||||
|----------|------|---------|-------------|
|
||||
| `accent` | `string` | `#3b82f6` | Accent color used for active navigation and highlights |
|
||||
| `density` | `'compact' \| 'comfortable'` | `comfortable` | Controls spacing density |
|
||||
| `theme` | `'dark' \| 'light'` | `dark` | Reflected to the host and used for design tokens |
|
||||
| `initialView` | `TWorkspaceView` | `inbox` | First view displayed after connection |
|
||||
|
||||
### Workspace Views
|
||||
|
||||
| View | Notes |
|
||||
|------|-------|
|
||||
| `inbox` | Demo document inbox with status, recipients, sender, page count, and deadlines |
|
||||
| `compose` | Document preparation surface with recipients and draggable-style field placements |
|
||||
| `sign` | Signing flow surface for reviewing and completing assigned fields |
|
||||
| `audit` | Audit trail surface for signature and document events |
|
||||
| `developers` | Developer-facing integration and API concept surface |
|
||||
| `templates` | Placeholder for reusable agreement templates |
|
||||
| `team` | Placeholder for workspace members and roles |
|
||||
| `settings` | Placeholder for workspace, billing, and security settings |
|
||||
|
||||
### Workspace Events
|
||||
|
||||
```typescript
|
||||
const workspace = document.querySelector('sdig-workspace');
|
||||
|
||||
workspace?.addEventListener('view-change', (event) => {
|
||||
const { view } = (event as CustomEvent<{ view: string }>).detail;
|
||||
console.log(view);
|
||||
});
|
||||
```
|
||||
|
||||
Child views can request navigation by dispatching `workspace-view-request` with `{ view }` in `detail`. The event bubbles and crosses the shadow boundary.
|
||||
|
||||
## Signature Box
|
||||
|
||||
Use `sdig-signbox` when you want the default capture UI with a heading, a signature pad, and built-in action controls:
|
||||
|
||||
```html
|
||||
<sdig-signbox></sdig-signbox>
|
||||
```
|
||||
|
||||
```typescript
|
||||
const signbox = document.querySelector('sdig-signbox');
|
||||
|
||||
signbox?.addEventListener('signature', (event) => {
|
||||
const { signature } = (event as CustomEvent<{ signature: unknown[] }>).detail;
|
||||
console.log(signature);
|
||||
});
|
||||
```
|
||||
|
||||
The `signature` event detail contains the `signature_pad` stroke data returned by `sdig-signpad.toData()`.
|
||||
|
||||
## Signature Pad
|
||||
|
||||
Use `sdig-signpad` directly when you need lower-level control over the canvas:
|
||||
|
||||
```html
|
||||
<sdig-signpad></sdig-signpad>
|
||||
```
|
||||
|
||||
```typescript
|
||||
const signpad = document.querySelector('sdig-signpad');
|
||||
|
||||
const strokes = await signpad?.toData();
|
||||
const svg = await signpad?.toSVG();
|
||||
|
||||
await signpad?.undo();
|
||||
await signpad?.clear();
|
||||
```
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `toData()` | Returns `signature_pad` stroke data |
|
||||
| `fromData(data)` | Restores `signature_pad` stroke data |
|
||||
| `toSVG()` | Returns an SVG representation without a background color |
|
||||
| `undo()` | Removes the latest stroke |
|
||||
| `clear()` | Clears the canvas |
|
||||
|
||||
## Demo Data Boundary
|
||||
|
||||
The workspace currently renders local demo UI data from `sdig-workspace.shared.ts`. Real accounts, server persistence, document import/export, notification delivery, authentication, and legal signing workflow state belong in `@signature.digital/app` or backend services, not in this component catalog.
|
||||
|
||||
## Development
|
||||
|
||||
```shell
|
||||
pnpm install
|
||||
pnpm run build
|
||||
pnpm test
|
||||
pnpm run watch
|
||||
```
|
||||
|
||||
The build compiles `ts_web/` into `dist_ts_web/` and bundles the component catalog demo from `html/index.ts` into `dist_bundle/`. Watch mode serves `dist_watch/` using the `.smartconfig.json` `@git.zone/tswatch` configuration.
|
||||
|
||||
## License and Legal Information
|
||||
|
||||
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [license](./license) file.
|
||||
|
||||
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
||||
|
||||
### Trademarks
|
||||
|
||||
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
|
||||
|
||||
Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
|
||||
|
||||
### Company Information
|
||||
|
||||
Task Venture Capital GmbH
|
||||
Registered at District Court Bremen HRB 35230 HB, Germany
|
||||
|
||||
For any legal inquiries or further information, please contact us via email at hello@task.vc.
|
||||
|
||||
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/**
|
||||
* autocreated commitinfo by @pushrocks/commitinfo
|
||||
* autocreated commitinfo by @push.rocks/commitinfo
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@signature.digital_private/catalog',
|
||||
version: '1.0.56',
|
||||
description: 'a catalog containing components for e-signing'
|
||||
name: '@signature.digital/catalog',
|
||||
version: '1.3.0',
|
||||
description: 'A comprehensive catalog of customizable web components designed for building and managing e-signature applications.'
|
||||
}
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
import { DeesElement, property, html, customElement, TemplateResult, css, cssManager } from '@designestate/dees-element';
|
||||
import * as domtools from '@designestate/dees-domtools';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'first-element': FirstElement;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('first-element')
|
||||
export class FirstElement extends DeesElement {
|
||||
public static demo = () => html`
|
||||
<first-element .aProp="${'test'}"></first-element>
|
||||
`;
|
||||
|
||||
@property({
|
||||
type: String
|
||||
})
|
||||
public aProp: string = 'loading...';
|
||||
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
domtools.DomTools.setupDomTools();
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
domtools.elementBasic.staticStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
background: blue;
|
||||
color: white;
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
`
|
||||
]
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<div class="mainbox">
|
||||
${this.aProp}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -1 +1,6 @@
|
||||
export * from './first-element.js';
|
||||
// Signature components
|
||||
export * from './sdig-signbox/index.js';
|
||||
export * from './sdig-signpad/index.js';
|
||||
|
||||
// Product workspace component
|
||||
export * from './sdig-workspace/index.js';
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from './sdig-signbox.js';
|
||||
@@ -0,0 +1,109 @@
|
||||
import { DeesElement, html, customElement, type TemplateResult, css, cssManager } from '@design.estate/dees-element';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'sdig-signbox': SignBox;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('sdig-signbox')
|
||||
export class SignBox extends DeesElement {
|
||||
public static demo = () => html`
|
||||
<sdig-signbox></sdig-signbox>
|
||||
`;
|
||||
public static demoGroups = ['Signature Digital Primitives'];
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
.mainbox {
|
||||
position: relative;
|
||||
background: ${cssManager.bdTheme('#eeeeeb', '#111111')};
|
||||
border-radius: 16px;
|
||||
max-width: 600px;
|
||||
margin: auto;
|
||||
overflow: hidden;
|
||||
color: ${cssManager.bdTheme('#111111', '#eeeeeb')};
|
||||
font-family: 'Roboto', sans-serif;
|
||||
box-shadow: ${cssManager.bdTheme('0px 0px 8px 0px #00000040', 'none')};
|
||||
}
|
||||
|
||||
.heading {
|
||||
padding: 4px;
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
margin-bottom: -20px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
sdig-signpad {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.actions {
|
||||
position: relative;
|
||||
padding: 0px 24px;
|
||||
font-size: 16px;
|
||||
background: ${cssManager.bdTheme('#eeeeeb', '#000000')};
|
||||
box-shadow: ${cssManager.bdTheme('0px 0px 8px 0px #00000040', 'none')};
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.button {
|
||||
cursor: pointer;
|
||||
margin: 0px 16px;
|
||||
padding: 16px 0px;
|
||||
color: ${cssManager.bdTheme('#666', '#999')};
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
color: ${cssManager.bdTheme('#111111', '#eeeeeb')};
|
||||
}
|
||||
`
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<div class="mainbox">
|
||||
<div class="heading">
|
||||
You may sign below:
|
||||
</div>
|
||||
<sdig-signpad></sdig-signpad>
|
||||
<div class="actions">
|
||||
<div class="button" @click=${async () => {
|
||||
await this.shadowRoot?.querySelector('sdig-signpad')?.clear();
|
||||
}}>
|
||||
Clear
|
||||
</div>
|
||||
<div class="button" @click=${async () => {
|
||||
await this.shadowRoot?.querySelector('sdig-signpad')?.undo();
|
||||
}}>
|
||||
Undo
|
||||
</div>
|
||||
<div class="button" @click=${async () => {
|
||||
const signaturePad = this.shadowRoot?.querySelector('sdig-signpad');
|
||||
if (!signaturePad) return;
|
||||
const signature = await signaturePad.toData();
|
||||
this.dispatchEvent(new CustomEvent('signature', {
|
||||
detail: {
|
||||
signature,
|
||||
}
|
||||
}));
|
||||
console.log(signature);
|
||||
}}>
|
||||
Submit Signature
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './sdig-signpad.js';
|
||||
@@ -0,0 +1,115 @@
|
||||
import { DeesElement, html, customElement, type TemplateResult, css, cssManager } from '@design.estate/dees-element';
|
||||
import * as plugins from '../../plugins.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'sdig-signpad': SignPad;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('sdig-signpad')
|
||||
export class SignPad extends DeesElement {
|
||||
public static demo = () => html`
|
||||
<sdig-signpad></sdig-signpad>
|
||||
`;
|
||||
public static demoGroups = ['Signature Digital Primitives'];
|
||||
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
color: white;
|
||||
position: relative;
|
||||
max-width: 600px;
|
||||
min-height: 280px;
|
||||
max-height: 400px;
|
||||
}
|
||||
|
||||
.mainbox {
|
||||
position: relative;
|
||||
width: 600px;
|
||||
height: 280px;
|
||||
}
|
||||
|
||||
.signline {
|
||||
position: absolute;
|
||||
bottom: 30%;
|
||||
width: 80%;
|
||||
left: 10%;
|
||||
border-top: 1px dashed ${cssManager.bdTheme('#00000040', '#ffffff20')};
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
canvas {
|
||||
filter: ${cssManager.bdTheme('invert(0)', 'invert(1)')};
|
||||
}
|
||||
`
|
||||
]
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<div class="mainbox">
|
||||
<div class="signline"></div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
public signaturePad?: typeof plugins.signaturePad.prototype;
|
||||
|
||||
public async firstUpdated() {
|
||||
await this.domtoolsPromise;
|
||||
const mainbox = this.shadowRoot?.querySelector('.mainbox');
|
||||
if (!mainbox) return;
|
||||
const canvas = document.createElement('canvas');
|
||||
mainbox.appendChild(canvas);
|
||||
await this.resizeCanvas();
|
||||
this.signaturePad = new plugins.signaturePad(canvas, {
|
||||
|
||||
});
|
||||
this.signaturePad.on();
|
||||
}
|
||||
|
||||
public async resizeCanvas() {
|
||||
const mainbox = this.shadowRoot?.querySelector('.mainbox');
|
||||
const canvas = this.shadowRoot?.querySelector('canvas');
|
||||
if (!mainbox || !canvas) return;
|
||||
const mainboxWidth = mainbox.clientWidth;
|
||||
const mainboxHeight = mainbox.clientHeight;
|
||||
canvas.width = mainboxWidth;
|
||||
canvas.height = mainboxHeight;
|
||||
if (this.signaturePad) {
|
||||
this.signaturePad.clear();
|
||||
}
|
||||
}
|
||||
|
||||
public async clear() {
|
||||
this.signaturePad?.clear();
|
||||
}
|
||||
|
||||
public async toData() {
|
||||
const returnData = this.signaturePad?.toData() || [];
|
||||
return returnData;
|
||||
}
|
||||
|
||||
public async fromData(dataArrayArg: any[]) {
|
||||
this.signaturePad?.fromData(dataArrayArg);
|
||||
}
|
||||
|
||||
public async toSVG() {
|
||||
return this.signaturePad?.toSVG({
|
||||
includeBackgroundColor: false,
|
||||
}) || '';
|
||||
}
|
||||
|
||||
public async undo() {
|
||||
const data = await this.toData();
|
||||
data.pop();
|
||||
await this.fromData(data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export * from './sdig-workspace.shared.js';
|
||||
export * from './sdig-workspace-inbox.js';
|
||||
export * from './sdig-workspace-compose.js';
|
||||
export * from './sdig-workspace-sign.js';
|
||||
export * from './sdig-workspace-audit.js';
|
||||
export * from './sdig-workspace-developers.js';
|
||||
export * from './sdig-workspace-placeholder.js';
|
||||
export * from './sdig-workspace.js';
|
||||
@@ -0,0 +1,37 @@
|
||||
import { DeesElement, html, customElement, type TemplateResult, css } from '@design.estate/dees-element';
|
||||
import { actionButton, demoRecipients, icon, pill, topBar, workspaceBaseStyles, workspaceDemoFrame } from './sdig-workspace.shared.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'sdig-workspace-audit': SdigWorkspaceAudit;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('sdig-workspace-audit')
|
||||
export class SdigWorkspaceAudit extends DeesElement {
|
||||
public static demo = () => workspaceDemoFrame(html`<sdig-workspace-audit></sdig-workspace-audit>`);
|
||||
public static demoGroups = ['Signature Digital Workspace'];
|
||||
|
||||
public static styles = [workspaceBaseStyles, css`
|
||||
.audit-grid { display: grid; grid-template-columns: minmax(0, 1fr) 360px; gap: 20px; }
|
||||
.event-row { display: grid; grid-template-columns: 24px 180px 1fr 200px; gap: 12px; padding: 14px 16px; border-bottom: 1px solid var(--border-subtle); align-items: center; }
|
||||
@media (max-width: 920px) { .audit-grid { grid-template-columns: 1fr; } .event-row { grid-template-columns: 24px 1fr; } .event-row .hide-mobile { display: none; } }
|
||||
`];
|
||||
|
||||
public render(): TemplateResult {
|
||||
const events = [
|
||||
['2026-05-02 14:32:18 UTC', 'Sarah Chen', 'Document signed', '81.221.4.18 · Brussels, BE', '0x4a7b…f29c', 'success'],
|
||||
['2026-05-02 14:31:54 UTC', 'Sarah Chen', 'Signature adopted (typed)', '81.221.4.18 · Brussels, BE', '0x4a7b…f29c', 'info'],
|
||||
['2026-05-02 14:28:02 UTC', 'Sarah Chen', 'Document opened', '81.221.4.18 · Brussels, BE', '', 'default'],
|
||||
['2026-05-02 11:02:11 UTC', 'Philipp K.', 'Document sent for signature', '92.42.114.7 · Berlin, DE', '0x1c8a…3b6f', 'info'],
|
||||
['2026-05-02 10:54:22 UTC', 'Philipp K.', 'Document created', '92.42.114.7 · Berlin, DE', '0x1c8a…3b6f', 'default'],
|
||||
];
|
||||
return html`
|
||||
${topBar({ breadcrumb: ['signature.digital', 'Inbox', 'doc_8mK3pL', 'Audit Trail'], title: 'Audit Trail', subtitle: pill('completed · cryptographically sealed', 'success', true), actions: html`${actionButton('Certificate (PDF)', 'outline', 'download')}${actionButton('Verify on chain', 'outline', 'hash')}` })}
|
||||
<div class="content-scroll audit-grid">
|
||||
<div class="card"><div style="height: 36px; padding: 0 16px; border-bottom: 1px solid var(--border-subtle); display: flex; align-items: center; justify-content: space-between;"><span style="font-size: 12px; font-weight: 600;">Event log</span><span class="mono" style="font-size: 10px; color: var(--text-muted);">${events.length} events · immutable</span></div>${events.map((event) => html`<div class="event-row"><div><span style="display: block; width: 8px; height: 8px; border-radius: 50%; background: ${event[5] === 'success' ? 'var(--success)' : event[5] === 'info' ? 'var(--accent)' : 'var(--text-dim)'};"></span></div><div class="mono hide-mobile" style="font-size: 11px; color: var(--text-muted);">${event[0]}</div><div><div style="font-size: 12px; font-weight: 500;">${event[2]}</div><div style="font-size: 10px; color: var(--text-muted); margin-top: 2px;">by ${event[1]} ${event[4] ? html`<span class="mono" style="color: var(--accent); margin-left: 8px;">${event[4]}</span>` : ''}</div></div><div class="mono hide-mobile" style="font-size: 10px; color: var(--text-muted); text-align: right;">${event[3]}</div></div>`)}</div>
|
||||
<div style="display: flex; flex-direction: column; gap: 16px;"><div class="card" style="padding: 16px;"><div class="label-upper">Document hash</div><div class="mono" style="font-size: 11px; color: var(--accent); word-break: break-all; line-height: 1.5; padding: 10px; background: var(--bg-el); border-radius: 4px; border: 1px solid var(--border-subtle);">0x4a7b8f29c91e3d2a5b6c8e0f1d3c5a7b9d2e4f6a8c1e3d5f7b9c1e3a5b7d9f0e</div></div><div class="card" style="padding: 16px;"><div class="label-upper">Signers</div>${demoRecipients.map((recipient) => html`<div class="recipient-line"><span class="avatar" style="background: ${recipient.color};">${recipient.name.split(' ').map((part) => part[0]).slice(0, 2).join('')}</span><div style="flex: 1;"><div style="font-size: 12px;">${recipient.name}</div><div class="mono" style="font-size: 10px; color: var(--text-muted);">${recipient.email}</div></div>${icon('check', 12)}</div>`)}</div><div class="card" style="padding: 16px; border-color: rgba(34,197,94,0.2);"><div style="display: inline-flex; align-items: center; gap: 6px; font-size: 11px; color: var(--success); margin-bottom: 6px;">${icon('shield', 13)} eIDAS Qualified · ESIGN Act compliant</div><div style="font-size: 11px; color: var(--text-muted); line-height: 1.55;">Open-source verifier available. Anyone can independently validate this signature against the public ledger.</div></div></div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,369 @@
|
||||
import { DeesElement, state, html, customElement, type TemplateResult, css } from '@design.estate/dees-element';
|
||||
import { actionButton, demoFields, demoRecipients, fakeDocument, icon, pill, topBar, workspaceBaseStyles, workspaceDemoFrame, type IFieldPlacement, type IRecipient } from './sdig-workspace.shared.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'sdig-workspace-compose': SdigWorkspaceCompose;
|
||||
}
|
||||
}
|
||||
|
||||
type TFieldDefinition = {
|
||||
type: IFieldPlacement['type'];
|
||||
icon: string;
|
||||
label: string;
|
||||
w: number;
|
||||
h: number;
|
||||
};
|
||||
|
||||
type TResizeHandle = 'n' | 'ne' | 'e' | 'se' | 's' | 'sw' | 'w' | 'nw';
|
||||
|
||||
type TFieldInteraction = {
|
||||
fieldId: string;
|
||||
mode: 'move' | 'resize';
|
||||
handle?: TResizeHandle;
|
||||
startClientX: number;
|
||||
startClientY: number;
|
||||
startField: IFieldPlacement;
|
||||
pageWidth: number;
|
||||
pageHeight: number;
|
||||
};
|
||||
|
||||
const fieldDefinitions: TFieldDefinition[] = [
|
||||
{ type: 'signature', icon: 'sign', label: 'Signature', w: 200, h: 50 },
|
||||
{ type: 'initials', icon: 'type', label: 'Initials', w: 120, h: 32 },
|
||||
{ type: 'date', icon: 'calendar', label: 'Date', w: 120, h: 32 },
|
||||
{ type: 'text', icon: 'type', label: 'Text field', w: 220, h: 32 },
|
||||
{ type: 'check', icon: 'check', label: 'Checkbox', w: 120, h: 32 },
|
||||
];
|
||||
|
||||
const resizeHandles: TResizeHandle[] = ['n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw'];
|
||||
|
||||
@customElement('sdig-workspace-compose')
|
||||
export class SdigWorkspaceCompose extends DeesElement {
|
||||
public static demo = () => workspaceDemoFrame(html`<sdig-workspace-compose></sdig-workspace-compose>`);
|
||||
public static demoGroups = ['Signature Digital Workspace'];
|
||||
|
||||
@state() private accessor step: number = 2;
|
||||
@state() private accessor activeRecipient: number = 0;
|
||||
@state() private accessor draggedRecipientId: number | null = null;
|
||||
@state() private accessor selectedFieldId: string | null = null;
|
||||
@state() private accessor recipients: IRecipient[] = [...demoRecipients];
|
||||
@state() private accessor fields: IFieldPlacement[] = [...demoFields];
|
||||
private draggedFieldDefinition: TFieldDefinition | null = null;
|
||||
private draggedFieldGrabOffset: { x: number; y: number } | null = null;
|
||||
private fieldInteraction: TFieldInteraction | null = null;
|
||||
|
||||
public static styles = [workspaceBaseStyles, css`
|
||||
.stepper { height: 44px; flex-shrink: 0; border-bottom: 1px solid var(--border-subtle); display: flex; align-items: center; padding: 0 24px; gap: 24px; overflow-x: auto; }
|
||||
.step { display: flex; align-items: center; gap: 8px; font-size: 12px; color: var(--text-muted); white-space: nowrap; background: transparent; }
|
||||
.step-number { width: 20px; height: 20px; border-radius: 50%; display: flex; align-items: center; justify-content: center; background: var(--bg-input); border: 1px solid var(--border); color: var(--text-muted); font-size: 10px; font-weight: 700; }
|
||||
.step.active { color: var(--text); font-weight: 500; }
|
||||
.step.active .step-number { background: var(--accent); border-color: var(--accent); color: white; }
|
||||
.step.done .step-number { background: var(--success); border-color: var(--success); color: white; }
|
||||
.compose-workspace { flex: 1; display: flex; overflow: hidden; }
|
||||
.palette { width: 260px; border-right: 1px solid var(--border-subtle); padding: 16px; background: var(--bg-el); overflow-y: auto; flex-shrink: 0; }
|
||||
.right-panel { width: 280px; border-left: 1px solid var(--border-subtle); padding: 16px; background: var(--bg-el); overflow-y: auto; flex-shrink: 0; }
|
||||
.field-tool { width: var(--tool-w); height: var(--tool-h); display: flex; align-items: center; gap: 8px; padding: 0 10px; background: color-mix(in srgb, var(--recipient-color) 8%, var(--bg-card)); border: 1.5px dashed var(--recipient-color); border-radius: 4px; font-size: 12px; color: var(--recipient-color); margin-bottom: 8px; cursor: grab; }
|
||||
.field-tool:active { cursor: grabbing; }
|
||||
.field-tool span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.swatch { width: 10px; height: 10px; border-radius: 2px; background: var(--recipient-color, var(--accent)); flex-shrink: 0; }
|
||||
.document-stage { flex: 1; overflow: auto; background: hsl(0 0% 8%); display: flex; flex-direction: column; align-items: center; padding: 32px; gap: 20px; }
|
||||
:host-context(sdig-workspace[theme='light']) .document-stage { background: hsl(0 0% 92%); }
|
||||
.recipient-line { cursor: grab; }
|
||||
.recipient-line.dragging { opacity: 0.45; border-color: var(--accent); }
|
||||
.page-drop-target { outline: 1px dashed transparent; outline-offset: 8px; }
|
||||
.page-drop-target.drag-over { outline-color: var(--accent); }
|
||||
.field-box { user-select: none; touch-action: none; }
|
||||
.field-box.selected { z-index: 5; cursor: move; }
|
||||
.field-content { width: 100%; height: 100%; display: flex; align-items: center; gap: 6px; pointer-events: none; overflow: hidden; }
|
||||
.field-content span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.resize-handle { position: absolute; z-index: 2; width: 9px; height: 9px; border-radius: 50%; background: var(--bg-card); border: 1.5px solid var(--field-color); box-shadow: 0 0 0 2px var(--bg-card); touch-action: none; }
|
||||
.resize-handle.n { top: -6px; left: 50%; transform: translateX(-50%); cursor: ns-resize; }
|
||||
.resize-handle.ne { top: -6px; right: -6px; cursor: nesw-resize; }
|
||||
.resize-handle.e { right: -6px; top: 50%; transform: translateY(-50%); cursor: ew-resize; }
|
||||
.resize-handle.se { right: -6px; bottom: -6px; cursor: nwse-resize; }
|
||||
.resize-handle.s { bottom: -6px; left: 50%; transform: translateX(-50%); cursor: ns-resize; }
|
||||
.resize-handle.sw { left: -6px; bottom: -6px; cursor: nesw-resize; }
|
||||
.resize-handle.w { left: -6px; top: 50%; transform: translateY(-50%); cursor: ew-resize; }
|
||||
.resize-handle.nw { left: -6px; top: -6px; cursor: nwse-resize; }
|
||||
.field-editor { margin-top: 16px; padding: 12px; }
|
||||
.field-editor-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
|
||||
.field-control { display: flex; flex-direction: column; gap: 4px; font-size: 10px; color: var(--text-muted); }
|
||||
.field-control.full { grid-column: 1 / -1; }
|
||||
.field-control input, .field-control select { width: 100%; height: 30px; padding: 0 8px; border: 1px solid var(--border); border-radius: 5px; background: var(--bg-input); color: var(--text); font-size: 12px; outline: none; }
|
||||
.field-control input:focus, .field-control select:focus { border-color: var(--accent); }
|
||||
@media (max-width: 920px) { .compose-workspace { flex-direction: column; overflow: auto; } .palette, .right-panel { width: 100%; border: 0; border-bottom: 1px solid var(--border-subtle); } .document-page { width: 560px; } }
|
||||
`];
|
||||
|
||||
public disconnectedCallback = async () => {
|
||||
this.stopFieldInteraction();
|
||||
await super.disconnectedCallback();
|
||||
};
|
||||
|
||||
private recipientColor(id: number): string {
|
||||
return this.recipients.find((recipient) => recipient.id === id)?.color || 'var(--accent)';
|
||||
}
|
||||
|
||||
private fieldIcon(type: IFieldPlacement['type']): string {
|
||||
if (type === 'signature') return 'sign';
|
||||
if (type === 'date') return 'calendar';
|
||||
if (type === 'check') return 'check';
|
||||
return 'type';
|
||||
}
|
||||
|
||||
private fieldDefinition(type: IFieldPlacement['type']): TFieldDefinition {
|
||||
return fieldDefinitions.find((definition) => definition.type === type) || fieldDefinitions[0];
|
||||
}
|
||||
|
||||
private clamp(value: number, min: number, max: number): number {
|
||||
return Math.max(min, Math.min(max, value));
|
||||
}
|
||||
|
||||
private updateField(fieldId: string, patch: Partial<IFieldPlacement>) {
|
||||
this.fields = this.fields.map((field) => field.id === fieldId ? { ...field, ...patch } : field);
|
||||
}
|
||||
|
||||
private updateSelectedField(patch: Partial<IFieldPlacement>) {
|
||||
if (!this.selectedFieldId) return;
|
||||
this.updateField(this.selectedFieldId, patch);
|
||||
}
|
||||
|
||||
private updateSelectedFieldNumber(property: 'x' | 'y' | 'w' | 'h', event: Event) {
|
||||
const value = Number((event.target as HTMLInputElement).value);
|
||||
if (!Number.isFinite(value)) return;
|
||||
const min = property === 'w' || property === 'h' ? 16 : 0;
|
||||
this.updateSelectedField({ [property]: Math.max(min, Math.round(value)) } as Partial<IFieldPlacement>);
|
||||
}
|
||||
|
||||
private resetSelectedFieldSize(field: IFieldPlacement) {
|
||||
const definition = this.fieldDefinition(field.type);
|
||||
this.updateSelectedField({ w: definition.w, h: definition.h });
|
||||
}
|
||||
|
||||
private removeSelectedField() {
|
||||
if (!this.selectedFieldId) return;
|
||||
this.fields = this.fields.filter((field) => field.id !== this.selectedFieldId);
|
||||
this.selectedFieldId = null;
|
||||
}
|
||||
|
||||
private handleDocumentClick = (event: MouseEvent) => {
|
||||
const target = event.target as HTMLElement | null;
|
||||
if (target?.closest('.field-box')) return;
|
||||
this.selectedFieldId = null;
|
||||
};
|
||||
|
||||
private startFieldInteraction(event: PointerEvent, field: IFieldPlacement, mode: TFieldInteraction['mode'], handle?: TResizeHandle) {
|
||||
if (event.button !== 0) return;
|
||||
const page = this.shadowRoot?.querySelector('.document-page') as HTMLElement | null;
|
||||
if (!page) return;
|
||||
const pageRect = page.getBoundingClientRect();
|
||||
this.selectedFieldId = field.id;
|
||||
this.fieldInteraction = {
|
||||
fieldId: field.id,
|
||||
mode,
|
||||
handle,
|
||||
startClientX: event.clientX,
|
||||
startClientY: event.clientY,
|
||||
startField: { ...field },
|
||||
pageWidth: pageRect.width,
|
||||
pageHeight: pageRect.height,
|
||||
};
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
window.addEventListener('pointermove', this.handleFieldPointerMove, { passive: false });
|
||||
window.addEventListener('pointerup', this.stopFieldInteraction);
|
||||
window.addEventListener('pointercancel', this.stopFieldInteraction);
|
||||
}
|
||||
|
||||
private startFieldMove(event: PointerEvent, field: IFieldPlacement) {
|
||||
this.startFieldInteraction(event, field, 'move');
|
||||
}
|
||||
|
||||
private startFieldResize(event: PointerEvent, field: IFieldPlacement, handle: TResizeHandle) {
|
||||
this.startFieldInteraction(event, field, 'resize', handle);
|
||||
}
|
||||
|
||||
private handleFieldPointerMove = (event: PointerEvent) => {
|
||||
if (!this.fieldInteraction) return;
|
||||
event.preventDefault();
|
||||
const interaction = this.fieldInteraction;
|
||||
const dx = event.clientX - interaction.startClientX;
|
||||
const dy = event.clientY - interaction.startClientY;
|
||||
const start = interaction.startField;
|
||||
|
||||
if (interaction.mode === 'move') {
|
||||
this.updateField(interaction.fieldId, {
|
||||
x: Math.round(this.clamp(start.x + dx, 0, interaction.pageWidth - start.w)),
|
||||
y: Math.round(this.clamp(start.y + dy, 0, interaction.pageHeight - start.h)),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const minWidth = 32;
|
||||
const minHeight = 24;
|
||||
let x = start.x;
|
||||
let y = start.y;
|
||||
let w = start.w;
|
||||
let h = start.h;
|
||||
const handle = interaction.handle || 'se';
|
||||
|
||||
if (handle.includes('e')) {
|
||||
w = this.clamp(start.w + dx, minWidth, interaction.pageWidth - start.x);
|
||||
}
|
||||
if (handle.includes('s')) {
|
||||
h = this.clamp(start.h + dy, minHeight, interaction.pageHeight - start.y);
|
||||
}
|
||||
if (handle.includes('w')) {
|
||||
x = this.clamp(start.x + dx, 0, start.x + start.w - minWidth);
|
||||
w = start.x + start.w - x;
|
||||
}
|
||||
if (handle.includes('n')) {
|
||||
y = this.clamp(start.y + dy, 0, start.y + start.h - minHeight);
|
||||
h = start.y + start.h - y;
|
||||
}
|
||||
|
||||
this.updateField(interaction.fieldId, {
|
||||
x: Math.round(x),
|
||||
y: Math.round(y),
|
||||
w: Math.round(w),
|
||||
h: Math.round(h),
|
||||
});
|
||||
};
|
||||
|
||||
private stopFieldInteraction = () => {
|
||||
this.fieldInteraction = null;
|
||||
window.removeEventListener('pointermove', this.handleFieldPointerMove);
|
||||
window.removeEventListener('pointerup', this.stopFieldInteraction);
|
||||
window.removeEventListener('pointercancel', this.stopFieldInteraction);
|
||||
};
|
||||
|
||||
private reorderRecipient(targetId: number) {
|
||||
if (this.draggedRecipientId === null || this.draggedRecipientId === targetId) return;
|
||||
const next = [...this.recipients];
|
||||
const fromIndex = next.findIndex((recipient) => recipient.id === this.draggedRecipientId);
|
||||
const toIndex = next.findIndex((recipient) => recipient.id === targetId);
|
||||
if (fromIndex === -1 || toIndex === -1) return;
|
||||
const [moved] = next.splice(fromIndex, 1);
|
||||
next.splice(toIndex, 0, moved);
|
||||
this.recipients = next.map((recipient, index) => ({ ...recipient, order: index + 1 }));
|
||||
this.draggedRecipientId = null;
|
||||
}
|
||||
|
||||
private addFieldFromDrop(event: DragEvent) {
|
||||
event.preventDefault();
|
||||
const page = event.currentTarget as HTMLElement;
|
||||
page.classList.remove('drag-over');
|
||||
const transferredType = event.dataTransfer?.getData('application/x-signature-field') as IFieldPlacement['type'];
|
||||
if (!this.draggedFieldDefinition && !transferredType) return;
|
||||
const definition = this.draggedFieldDefinition || this.fieldDefinition(transferredType);
|
||||
const transferredOffset = event.dataTransfer?.getData('application/x-signature-field-offset');
|
||||
const offset = this.draggedFieldGrabOffset || (transferredOffset ? JSON.parse(transferredOffset) as { x: number; y: number } : { x: definition.w / 2, y: definition.h / 2 });
|
||||
const rect = page.getBoundingClientRect();
|
||||
const x = Math.round(event.clientX - rect.left - offset.x);
|
||||
const y = Math.round(event.clientY - rect.top - offset.y);
|
||||
const nextField: IFieldPlacement = {
|
||||
id: `field_${Date.now()}`,
|
||||
type: definition.type,
|
||||
x: Math.max(0, Math.min(Math.max(0, rect.width - definition.w), x)),
|
||||
y: Math.max(0, Math.min(Math.max(0, rect.height - definition.h), y)),
|
||||
w: definition.w,
|
||||
h: definition.h,
|
||||
page: 1,
|
||||
recipient: this.activeRecipient,
|
||||
label: definition.label,
|
||||
};
|
||||
this.fields = [...this.fields, nextField];
|
||||
this.selectedFieldId = nextField.id;
|
||||
this.draggedFieldDefinition = null;
|
||||
this.draggedFieldGrabOffset = null;
|
||||
}
|
||||
|
||||
private startFieldToolDrag(event: DragEvent, fieldType: TFieldDefinition) {
|
||||
const toolRect = (event.currentTarget as HTMLElement).getBoundingClientRect();
|
||||
const offset = {
|
||||
x: Math.round(event.clientX - toolRect.left),
|
||||
y: Math.round(event.clientY - toolRect.top),
|
||||
};
|
||||
this.draggedFieldDefinition = fieldType;
|
||||
this.draggedFieldGrabOffset = offset;
|
||||
event.dataTransfer?.setData('application/x-signature-field', fieldType.type);
|
||||
event.dataTransfer?.setData('application/x-signature-field-offset', JSON.stringify(offset));
|
||||
if (event.dataTransfer) event.dataTransfer.effectAllowed = 'copy';
|
||||
}
|
||||
|
||||
private endFieldToolDrag() {
|
||||
this.draggedFieldDefinition = null;
|
||||
this.draggedFieldGrabOffset = null;
|
||||
}
|
||||
|
||||
private renderFieldEditor(field: IFieldPlacement): TemplateResult {
|
||||
return html`
|
||||
<div class="card field-editor">
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px;">
|
||||
<div style="font-size: 11px; font-weight: 600;">Field editor</div>
|
||||
${pill(this.fieldDefinition(field.type).label, 'info', true)}
|
||||
</div>
|
||||
<div class="field-editor-grid">
|
||||
<label class="field-control full">Label<input .value=${field.label} @input=${(event: Event) => this.updateSelectedField({ label: (event.target as HTMLInputElement).value })} /></label>
|
||||
<label class="field-control full">Assigned signer<select .value=${String(field.recipient)} @change=${(event: Event) => this.updateSelectedField({ recipient: Number((event.target as HTMLSelectElement).value) })}>${this.recipients.map((recipient) => html`<option value=${String(recipient.id)}>${recipient.order}. ${recipient.name}</option>`)}</select></label>
|
||||
<label class="field-control">X<input type="number" min="0" .value=${String(field.x)} @input=${(event: Event) => this.updateSelectedFieldNumber('x', event)} /></label>
|
||||
<label class="field-control">Y<input type="number" min="0" .value=${String(field.y)} @input=${(event: Event) => this.updateSelectedFieldNumber('y', event)} /></label>
|
||||
<label class="field-control">Width<input type="number" min="16" .value=${String(field.w)} @input=${(event: Event) => this.updateSelectedFieldNumber('w', event)} /></label>
|
||||
<label class="field-control">Height<input type="number" min="16" .value=${String(field.h)} @input=${(event: Event) => this.updateSelectedFieldNumber('h', event)} /></label>
|
||||
</div>
|
||||
<div style="display: flex; gap: 8px; margin-top: 12px;">
|
||||
<button class="btn outline small" style="flex: 1;" @click=${() => this.resetSelectedFieldSize(field)}>Reset size</button>
|
||||
<button class="btn ghost small" style="color: var(--error);" @click=${() => this.removeSelectedField()}>Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderResizeHandles(field: IFieldPlacement): TemplateResult {
|
||||
return html`${resizeHandles.map((handle) => html`<span class="resize-handle ${handle}" @pointerdown=${(event: PointerEvent) => this.startFieldResize(event, field, handle)}></span>`)}`;
|
||||
}
|
||||
|
||||
private renderStepper(): TemplateResult {
|
||||
const labels = ['Upload', 'Place fields', 'Recipients & routing', 'Review & send'];
|
||||
return html`
|
||||
<div class="stepper">
|
||||
${labels.map((label, index) => {
|
||||
const stepNumber = index + 1;
|
||||
return html`<button class="step ${stepNumber === this.step ? 'active' : stepNumber < this.step ? 'done' : ''}" @click=${() => this.step = stepNumber}><span class="step-number">${stepNumber < this.step ? '✓' : stepNumber}</span><span>${label}</span>${index < labels.length - 1 ? html`<span style="width: 24px; height: 1px; background: var(--border); margin-left: 8px;"></span>` : ''}</button>`;
|
||||
})}
|
||||
<div style="flex: 1;"></div><span class="mono" style="font-size: 11px; color: var(--text-muted);">doc_8mK3pL · 14 pages · 2.4 MB</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
public render(): TemplateResult {
|
||||
const selectedField = this.fields.find((field) => field.id === this.selectedFieldId);
|
||||
|
||||
return html`
|
||||
${topBar({ breadcrumb: ['signature.digital', 'Inbox', 'Compose'], title: 'Master Services Agreement', subtitle: pill('Draft · auto-saved'), actions: html`${actionButton('Save draft', 'ghost')}${actionButton('Preview', 'outline', 'eye')}${actionButton('Send for signature', 'primary', 'send')}` })}
|
||||
${this.renderStepper()}
|
||||
<div class="compose-workspace">
|
||||
<div class="palette">
|
||||
<div class="label-upper">Drag onto document</div>
|
||||
${fieldDefinitions.map((fieldType) => html`<div class="field-tool" style="--tool-w: ${fieldType.w}px; --tool-h: ${fieldType.h}px; --recipient-color: ${this.recipientColor(this.activeRecipient)};" draggable="true" @dragstart=${(event: DragEvent) => this.startFieldToolDrag(event, fieldType)} @dragend=${() => this.endFieldToolDrag()}>${icon(fieldType.icon, 14)}<span style="flex: 1;">${fieldType.label}</span></div>`)}
|
||||
<div style="height: 1px; background: var(--border-subtle); margin: 20px 0 16px;"></div>
|
||||
<div class="label-upper">Active for</div>
|
||||
${this.recipients.map((recipient) => html`<div class="recipient-line ${this.activeRecipient === recipient.id ? 'active' : ''}" @click=${() => this.activeRecipient = recipient.id}><span class="swatch" style="--recipient-color: ${recipient.color};"></span><span style="flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${recipient.name.split(' ')[0]}</span><span class="mono" style="font-size: 10px; color: var(--text-muted);">${this.fields.filter((field) => field.recipient === recipient.id).length}</span></div>`)}
|
||||
</div>
|
||||
<div class="document-stage">
|
||||
<div class="document-page page-drop-target" @click=${this.handleDocumentClick} @dragover=${(event: DragEvent) => { event.preventDefault(); if (event.dataTransfer) event.dataTransfer.dropEffect = 'copy'; (event.currentTarget as HTMLElement).classList.add('drag-over'); }} @dragleave=${(event: DragEvent) => (event.currentTarget as HTMLElement).classList.remove('drag-over')} @drop=${(event: DragEvent) => this.addFieldFromDrop(event)}>
|
||||
${fakeDocument()}
|
||||
${this.fields.map((field) => html`<div class="field-box ${this.selectedFieldId === field.id ? 'selected' : ''}" style="--x: ${field.x}px; --y: ${field.y}px; --w: ${field.w}px; --h: ${field.h}px; --field-color: ${this.recipientColor(field.recipient)};" @click=${() => this.selectedFieldId = field.id} @pointerdown=${(event: PointerEvent) => this.startFieldMove(event, field)}><div class="field-content">${icon(this.fieldIcon(field.type), 12)}<span>${field.label}</span></div>${this.selectedFieldId === field.id ? this.renderResizeHandles(field) : ''}</div>`)}
|
||||
<div class="mono" style="position: absolute; bottom: 12px; right: 16px; font-size: 9px; color: hsl(0 0% 60%);">Page 1 of 14</div>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: 10px; font-size: 11px; color: var(--text-muted);">${actionButton('Prev', 'outline')}${html`<span class="mono">1 / 14</span>`}${actionButton('Next', 'outline')}</div>
|
||||
</div>
|
||||
<div class="right-panel">
|
||||
<div class="label-upper">Signing order · drag to reorder</div>
|
||||
${this.recipients.map((recipient) => html`<div class="recipient-line ${this.draggedRecipientId === recipient.id ? 'dragging' : ''}" draggable="true" @dragstart=${() => this.draggedRecipientId = recipient.id} @dragover=${(event: DragEvent) => event.preventDefault()} @drop=${() => this.reorderRecipient(recipient.id)} @dragend=${() => this.draggedRecipientId = null}><span class="mono" style="width: 14px; font-size: 10px; color: var(--text-muted);">${recipient.order}</span><span class="avatar" style="background: ${recipient.color};">${recipient.name.split(' ').map((part) => part[0]).slice(0, 2).join('')}</span><div style="flex: 1; min-width: 0;"><div style="font-size: 12px; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${recipient.name}</div><div class="mono" style="font-size: 10px; color: var(--text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${recipient.email}</div></div>${icon('more', 12)}</div>`)}
|
||||
${selectedField ? this.renderFieldEditor(selectedField) : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { DeesElement, html, customElement, type TemplateResult, css } from '@design.estate/dees-element';
|
||||
import { actionButton, icon, pill, topBar, workspaceBaseStyles, workspaceDemoFrame } from './sdig-workspace.shared.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'sdig-workspace-developers': SdigWorkspaceDevelopers;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('sdig-workspace-developers')
|
||||
export class SdigWorkspaceDevelopers extends DeesElement {
|
||||
public static demo = () => workspaceDemoFrame(html`<sdig-workspace-developers></sdig-workspace-developers>`);
|
||||
public static demoGroups = ['Signature Digital Workspace'];
|
||||
|
||||
public static styles = [workspaceBaseStyles, css`
|
||||
.developer-grid { display: grid; grid-template-columns: minmax(0, 1fr) 320px; gap: 20px; }
|
||||
.stats-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; }
|
||||
.metric-card { padding: 14px; }
|
||||
.metric-value { font-family: 'Plus Jakarta Sans', Inter, sans-serif; font-size: 22px; font-weight: 600; letter-spacing: -0.02em; }
|
||||
pre.code { margin: 0; padding: 16px; background: var(--bg-el); border: 1px solid var(--border-subtle); border-radius: 6px; font-size: 12px; line-height: 1.7; color: var(--text-sec); overflow: auto; }
|
||||
@media (max-width: 920px) { .developer-grid, .stats-grid { grid-template-columns: 1fr; } }
|
||||
`];
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
${topBar({ breadcrumb: ['signature.digital', 'Developers'], title: 'Developers', subtitle: pill('API · v0.42', 'info', true), actions: html`${actionButton('View on GitHub', 'outline', 'github')}${actionButton('New API key', 'primary', 'plus')}` })}
|
||||
<div class="content-scroll developer-grid">
|
||||
<div style="display: flex; flex-direction: column; gap: 16px;">
|
||||
<div class="card" style="padding: 20px;"><div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px;"><span style="font-size: 14px; font-weight: 600;">Send a document in 8 lines</span><span class="pill">node</span></div><pre class="code mono">import { Signature } from '@signature.digital/sdk';
|
||||
|
||||
const sig = new Signature(process.env.SIGD_KEY);
|
||||
|
||||
await sig.documents.send({
|
||||
file: './msa.pdf',
|
||||
recipients: [{ name: 'Sarah Chen', email: 'sarah@acme.com' }],
|
||||
fields: 'auto',
|
||||
});</pre></div>
|
||||
<div class="stats-grid">${[['Requests this month', '14,892', '+8.2%'], ['P95 latency', '142ms', '-12ms'], ['Error rate', '0.04%', '✓']].map((metric) => html`<div class="card metric-card"><div class="metric-value">${metric[1]}</div><div style="font-size: 10px; color: var(--text-muted); margin-top: 2px;">${metric[0]}</div><div class="mono" style="font-size: 10px; color: var(--success); margin-top: 6px;">${metric[2]}</div></div>`)}</div>
|
||||
</div>
|
||||
<div style="display: flex; flex-direction: column; gap: 16px;">
|
||||
<div class="card" style="padding: 16px;"><div style="display: flex; align-items: center; gap: 8px; margin-bottom: 10px;">${icon('github', 14)}<span style="font-size: 12px; font-weight: 600;">signature-digital/core</span></div><div style="font-size: 11px; color: var(--text-muted); line-height: 1.5;">MIT-licensed. Self-host on your own infra, or use signature.digital cloud.</div></div>
|
||||
<div class="card" style="padding: 16px;"><div class="label-upper">Self-host status</div>${[['Docker image', 'ghcr.io/signature-digital'], ['Helm chart', 'v0.42.1'], ['Postgres ≥ 14', 'required'], ['S3-compatible', 'optional']].map((row) => html`<div style="display: flex; justify-content: space-between; font-size: 11px; margin-bottom: 6px;"><span style="color: var(--text-muted);">${row[0]}</span><span class="mono" style="color: var(--text-sec);">${row[1]}</span></div>`)}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import { DeesElement, property, state, html, customElement, type TemplateResult, css } from '@design.estate/dees-element';
|
||||
import { actionButton, demoDocuments, icon, pill, requestWorkspaceView, topBar, workspaceBaseStyles, type IDocumentRow, type TDensity } from './sdig-workspace.shared.js';
|
||||
import { workspaceDemoFrame } from './sdig-workspace.shared.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'sdig-workspace-inbox': SdigWorkspaceInbox;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('sdig-workspace-inbox')
|
||||
export class SdigWorkspaceInbox extends DeesElement {
|
||||
public static demo = () => workspaceDemoFrame(html`<sdig-workspace-inbox></sdig-workspace-inbox>`);
|
||||
public static demoGroups = ['Signature Digital Workspace'];
|
||||
|
||||
@property({ type: String }) public accessor density: TDensity = 'comfortable';
|
||||
@state() private accessor filter: string = 'all';
|
||||
@state() private accessor search: string = '';
|
||||
|
||||
public static styles = [workspaceBaseStyles, css`
|
||||
.filterbar { padding: 14px 24px; border-bottom: 1px solid var(--border-subtle); display: flex; align-items: center; gap: 8px; }
|
||||
.searchbox { display: flex; align-items: center; gap: 8px; padding: 0 10px; height: 32px; width: 280px; background: var(--bg-input); border: 1px solid var(--border); border-radius: 6px; }
|
||||
.searchbox input { flex: 1; min-width: 0; background: transparent; border: none; outline: none; color: var(--text); font-size: 12px; }
|
||||
.segmented { display: flex; gap: 2px; padding: 2px; background: var(--bg-el); border-radius: 6px; border: 1px solid var(--border-subtle); }
|
||||
.segmented button { padding: 4px 10px; font-size: 11px; font-weight: 500; border-radius: 4px; background: transparent; color: var(--text-muted); display: inline-flex; align-items: center; gap: 5px; }
|
||||
.segmented button.active { background: var(--bg-card); color: var(--text); box-shadow: inset 0 0 0 1px var(--border); }
|
||||
.doc-table { min-width: 880px; }
|
||||
.doc-head, .doc-row { display: grid; grid-template-columns: 32px minmax(220px,2.4fr) 150px 160px 90px 60px 32px; align-items: center; gap: 14px; padding: 0 16px; }
|
||||
.doc-head { height: 36px; border-bottom: 1px solid var(--border-subtle); color: var(--text-dim); font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; }
|
||||
.doc-row { height: 60px; border-bottom: 1px solid var(--border-subtle); cursor: pointer; transition: background 0.1s ease; }
|
||||
.doc-row.compact { height: 48px; }
|
||||
.doc-row:last-child { border-bottom: 0; }
|
||||
.doc-row:hover { background: var(--row-hover); }
|
||||
.doc-icon { width: 28px; height: 32px; border-radius: 4px; background: var(--bg-input); border: 1px solid var(--border); display: flex; align-items: center; justify-content: center; }
|
||||
.doc-title { font-size: 13px; color: var(--text); font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.doc-meta { margin-top: 2px; font-size: 11px; color: var(--text-muted); }
|
||||
.recipient-stack { display: flex; align-items: center; }
|
||||
.recipient-dot { width: 22px; height: 22px; border-radius: 50%; background: var(--bg-input); border: 1.5px solid var(--border); margin-left: -6px; font-size: 9px; font-weight: 600; color: var(--text-sec); display: flex; align-items: center; justify-content: center; }
|
||||
.recipient-dot:first-child { margin-left: 0; }
|
||||
.recipient-dot.signed { border-color: var(--success); color: var(--success); background: var(--bg-el); }
|
||||
.stats-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; margin-top: 24px; }
|
||||
.metric-card { padding: 16px; }
|
||||
.metric-value { font-family: 'Plus Jakarta Sans', Inter, sans-serif; font-size: 22px; font-weight: 600; letter-spacing: -0.02em; }
|
||||
@media (max-width: 920px) { .filterbar { padding: 12px 16px; display: block; } .searchbox { width: 100%; margin-bottom: 10px; } .stats-grid { grid-template-columns: 1fr; } }
|
||||
`];
|
||||
|
||||
private get filteredDocuments(): IDocumentRow[] {
|
||||
return demoDocuments
|
||||
.filter((doc) => this.filter === 'all' || doc.status === this.filter)
|
||||
.filter((doc) => !this.search || doc.title.toLowerCase().includes(this.search.toLowerCase()));
|
||||
}
|
||||
|
||||
private statusPill(status: IDocumentRow['status']): TemplateResult {
|
||||
const map = {
|
||||
awaiting: ['warning', 'awaiting signature'],
|
||||
signed: ['success', 'completed'],
|
||||
draft: ['default', 'draft'],
|
||||
declined: ['error', 'declined'],
|
||||
} as const;
|
||||
const [tone, label] = map[status];
|
||||
return pill(label, tone, true);
|
||||
}
|
||||
|
||||
private openDocument(doc: IDocumentRow) {
|
||||
requestWorkspaceView(this, doc.status === 'signed' ? 'audit' : 'sign');
|
||||
}
|
||||
|
||||
public render(): TemplateResult {
|
||||
const filters = [
|
||||
{ id: 'all', label: 'All', count: demoDocuments.length },
|
||||
{ id: 'awaiting', label: 'Awaiting', count: demoDocuments.filter((doc) => doc.status === 'awaiting').length },
|
||||
{ id: 'signed', label: 'Completed', count: demoDocuments.filter((doc) => doc.status === 'signed').length },
|
||||
{ id: 'draft', label: 'Drafts', count: demoDocuments.filter((doc) => doc.status === 'draft').length },
|
||||
{ id: 'declined', label: 'Declined', count: demoDocuments.filter((doc) => doc.status === 'declined').length },
|
||||
];
|
||||
|
||||
return html`
|
||||
${topBar({
|
||||
breadcrumb: ['signature.digital', 'Lossless GmbH', 'Inbox'],
|
||||
title: 'Inbox',
|
||||
subtitle: pill(`${demoDocuments.filter((doc) => doc.status === 'awaiting').length} need attention`, 'info'),
|
||||
actions: html`${actionButton('Import', 'outline', 'upload')}${actionButton('New document', 'primary', 'plus', () => requestWorkspaceView(this, 'compose'))}`,
|
||||
})}
|
||||
<div class="filterbar">
|
||||
<div class="searchbox">${icon('search', 13)}<input .value=${this.search} @input=${(event: Event) => this.search = (event.target as HTMLInputElement).value} placeholder="Search documents, recipients, IDs..." /><span class="mono" style="font-size: 10px; color: var(--text-dim); border: 1px solid var(--border); border-radius: 3px; padding: 1px 5px;">⌘K</span></div>
|
||||
<div style="flex: 1;"></div>
|
||||
<div class="segmented">${filters.map((filter) => html`<button class=${this.filter === filter.id ? 'active' : ''} @click=${() => this.filter = filter.id}>${filter.label}<span class="mono" style="color: var(--text-dim);">${filter.count}</span></button>`)}</div>
|
||||
</div>
|
||||
<div class="content-scroll">
|
||||
<div class="card" style="overflow-x: auto;">
|
||||
<div class="doc-table">
|
||||
<div class="doc-head"><span></span><span>Document</span><span>Status</span><span>Recipients</span><span>Deadline</span><span style="text-align: right;">Pages</span><span></span></div>
|
||||
${this.filteredDocuments.map((doc) => html`
|
||||
<div class="doc-row ${this.density === 'compact' ? 'compact' : ''}" @click=${() => this.openDocument(doc)}>
|
||||
<div class="doc-icon">${icon('file', 14)}</div>
|
||||
<div style="min-width: 0;"><div class="doc-title">${doc.title}</div><div class="doc-meta mono">${doc.id} · ${doc.sender} · ${doc.updated}</div></div>
|
||||
<div>${this.statusPill(doc.status)}</div>
|
||||
<div style="display: flex; align-items: center; gap: 8px;"><div class="recipient-stack">${doc.recipients.slice(0, 4).map((recipient) => html`<span class="recipient-dot ${recipient.signed ? 'signed' : ''}" title=${recipient.name}>${recipient.initials}</span>`)}</div><span style="font-size: 11px; color: var(--text-muted);">${doc.recipients.filter((recipient) => recipient.signed).length}/${doc.recipients.length}</span></div>
|
||||
<div class="mono" style="font-size: 11px; color: ${doc.deadline && doc.status === 'awaiting' ? 'var(--warning)' : 'var(--text-dim)'};">${doc.deadline ? html`${icon('clock', 11)} ${doc.deadline}` : '—'}</div>
|
||||
<div class="mono" style="font-size: 11px; color: var(--text-muted); text-align: right;">${doc.pages}</div>
|
||||
<div>${icon('more', 14)}</div>
|
||||
</div>
|
||||
`)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="stats-grid">
|
||||
${[
|
||||
{ label: 'Sent this month', value: '127', delta: '+24%', icon: 'send' },
|
||||
{ label: 'Avg time to sign', value: '4.2h', delta: '-18%', icon: 'clock' },
|
||||
{ label: 'Completion rate', value: '94.1%', delta: '+2.1%', icon: 'check' },
|
||||
{ label: 'API signatures', value: '2,481', delta: '+312', icon: 'code' },
|
||||
].map((metric) => html`<div class="card metric-card"><div style="display: flex; justify-content: space-between; margin-bottom: 12px;">${icon(metric.icon, 14)}<span class="mono" style="font-size: 10px; color: var(--success);">${metric.delta}</span></div><div class="metric-value">${metric.value}</div><div style="font-size: 11px; color: var(--text-muted); margin-top: 2px;">${metric.label}</div></div>`)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { DeesElement, property, html, customElement, type TemplateResult } from '@design.estate/dees-element';
|
||||
import { icon, pill, topBar, workspaceBaseStyles, workspaceDemoFrame } from './sdig-workspace.shared.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'sdig-workspace-placeholder': SdigWorkspacePlaceholder;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('sdig-workspace-placeholder')
|
||||
export class SdigWorkspacePlaceholder extends DeesElement {
|
||||
public static demo = () => workspaceDemoFrame(html`<sdig-workspace-placeholder label="Templates" subtitle="Reusable agreement templates"></sdig-workspace-placeholder>`);
|
||||
public static demoGroups = ['Signature Digital Workspace'];
|
||||
|
||||
@property({ type: String }) public accessor label: string = 'Section';
|
||||
@property({ type: String }) public accessor subtitle: string = 'Coming soon';
|
||||
public static styles = [workspaceBaseStyles];
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`${topBar({ breadcrumb: ['signature.digital', this.label], title: this.label, subtitle: pill('coming soon') })}<div class="content-scroll" style="display: flex; align-items: center; justify-content: center; flex-direction: column; color: var(--text-muted); gap: 8px;">${icon('folder', 32)}<div style="font-size: 13px; color: var(--text-sec);">${this.label}</div><div style="font-size: 11px;">${this.subtitle}</div></div>`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import { DeesElement, state, html, customElement, type TemplateResult, css } from '@design.estate/dees-element';
|
||||
import { actionButton, demoFields, fakeDocument, icon, pill, workspaceBaseStyles, workspaceDemoFrame, type IFieldPlacement } from './sdig-workspace.shared.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'sdig-workspace-sign': SdigWorkspaceSign;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('sdig-workspace-sign')
|
||||
export class SdigWorkspaceSign extends DeesElement {
|
||||
public static demo = () => workspaceDemoFrame(html`<sdig-workspace-sign></sdig-workspace-sign>`);
|
||||
public static demoGroups = ['Signature Digital Workspace'];
|
||||
|
||||
@state() private accessor activeFieldId: string = 'f1';
|
||||
@state() private accessor signedFieldIds: string[] = [];
|
||||
|
||||
public static styles = [workspaceBaseStyles, css`
|
||||
.recipient-header { height: 56px; flex-shrink: 0; padding: 0 24px; background: var(--bg-card); border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; }
|
||||
.logomark { width: 28px; height: 28px; border-radius: 6px; background: var(--bg-el); border: 1px solid var(--border-strong); display: inline-flex; align-items: center; justify-content: center; font-family: 'Plus Jakarta Sans', Inter, sans-serif; font-weight: 700; position: relative; }
|
||||
.logomark::after { content: ''; position: absolute; right: 5px; bottom: 5px; width: 4px; height: 4px; border-radius: 50%; background: var(--accent); }
|
||||
.sign-layout { flex: 1; display: flex; overflow: hidden; background: hsl(0 0% 96%); color: hsl(0 0% 10%); }
|
||||
:host-context(sdig-workspace[theme='dark']) .sign-layout { background: hsl(0 0% 6%); color: hsl(0 0% 95%); }
|
||||
.sign-body { flex: 1; overflow: auto; padding: 32px 32px 80px; display: flex; flex-direction: column; align-items: center; gap: 20px; }
|
||||
.sign-panel { width: 320px; border-left: 1px solid var(--border); background: var(--bg-card); padding: 20px; overflow: auto; flex-shrink: 0; }
|
||||
@media (max-width: 920px) { .recipient-header .actions { display: none; } .sign-layout { flex-direction: column; overflow: auto; } .sign-panel { width: 100%; border-left: 0; border-top: 1px solid var(--border); } .document-page { width: 560px; } }
|
||||
`];
|
||||
|
||||
private get signFields() {
|
||||
return demoFields.slice(0, 3);
|
||||
}
|
||||
|
||||
private fieldIcon(type: IFieldPlacement['type']): string {
|
||||
if (type === 'signature') return 'sign';
|
||||
if (type === 'date') return 'calendar';
|
||||
return 'type';
|
||||
}
|
||||
|
||||
private signField(fieldId: string) {
|
||||
if (!this.signedFieldIds.includes(fieldId)) {
|
||||
this.signedFieldIds = [...this.signedFieldIds, fieldId];
|
||||
}
|
||||
const next = this.signFields.find((field) => !this.signedFieldIds.includes(field.id) && field.id !== fieldId);
|
||||
if (next) this.activeFieldId = next.id;
|
||||
}
|
||||
|
||||
private renderSignedValue(field: IFieldPlacement): TemplateResult {
|
||||
if (field.type === 'signature') return html`<span style="font-family: 'Plus Jakarta Sans', cursive; font-size: 22px; font-weight: 600; font-style: italic; color: hsl(220 50% 30%);">Sarah Chen</span>`;
|
||||
if (field.type === 'date') return html`<span class="mono" style="font-size: 12px; color: hsl(0 0% 18%);">2026-05-02</span>`;
|
||||
return html`<span style="font-size: 12px; color: hsl(0 0% 18%);">Sarah Chen</span>`;
|
||||
}
|
||||
|
||||
public render(): TemplateResult {
|
||||
const completed = this.signedFieldIds.length;
|
||||
const progress = Math.round((completed / this.signFields.length) * 100);
|
||||
const activeField = this.signFields.find((field) => field.id === this.activeFieldId) || this.signFields[0];
|
||||
|
||||
return html`
|
||||
<div class="recipient-header">
|
||||
<div style="display: flex; align-items: center; gap: 12px;"><span class="logomark">s</span><div><div style="font-size: 12px; font-weight: 600;">Master Services Agreement</div><div class="mono" style="font-size: 10px; color: var(--text-muted);">From Lossless GmbH · doc_8mK3pL · 14 pages</div></div></div>
|
||||
<div class="actions"><span class="pill success">${icon('shield', 12)} Verified sender · DKIM ✓</span>${actionButton('Decline', 'outline')}${actionButton('PDF', 'outline', 'download')}</div>
|
||||
</div>
|
||||
<div class="progress-track"><div class="progress-fill" style="width: ${progress}%"></div></div>
|
||||
<div class="sign-layout">
|
||||
<div class="sign-body">
|
||||
<div class="document-page" style="width: 620px; min-height: 820px;">
|
||||
${fakeDocument()}
|
||||
${this.signFields.map((field) => {
|
||||
const filled = this.signedFieldIds.includes(field.id);
|
||||
const active = this.activeFieldId === field.id && !filled;
|
||||
return html`<div class="field-box ${active ? 'selected' : ''}" style="--x: ${field.x}px; --y: ${field.y}px; --w: ${field.w}px; --h: ${field.h}px; --field-color: ${filled ? 'transparent' : 'var(--accent)'}; color: ${filled ? 'hsl(220 50% 30%)' : 'var(--accent)'}; background: ${filled ? 'transparent' : 'color-mix(in srgb, var(--accent) 12%, transparent)'};" @click=${() => !filled ? this.signField(field.id) : undefined}>${filled ? this.renderSignedValue(field) : html`${icon(this.fieldIcon(field.type), 12)}<span>${active ? html`<span style="display: inline-block; width: 5px; height: 5px; border-radius: 50%; background: var(--accent); animation: pulse 1.4s infinite;"></span>` : ''}${field.label}</span>`}</div>`;
|
||||
})}
|
||||
<div class="mono" style="position: absolute; bottom: 14px; right: 18px; font-size: 9px; color: hsl(0 0% 60%);">Page 1 of 14</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sign-panel">
|
||||
<div style="display: flex; align-items: center; gap: 8px;"><span class="avatar" style="background: #60a5fa;">SC</span><div><div style="font-size: 13px; font-weight: 600;">Hi, Sarah</div><div class="mono" style="font-size: 10px; color: var(--text-muted);">sarah@acme.com</div></div></div>
|
||||
<div class="card" style="padding: 14px; margin-top: 16px;"><div class="label-upper">Your progress</div><div style="font-size: 24px; font-weight: 700;">${completed} <span style="color: var(--text-muted); font-weight: 400;">/ ${this.signFields.length}</span></div><div style="font-size: 11px; color: var(--text-muted); margin-top: 2px;">${this.signFields.length - completed === 0 ? 'All fields complete' : `${this.signFields.length - completed} fields remaining`}</div><div class="progress-track" style="margin-top: 12px;"><div class="progress-fill" style="width: ${progress}%"></div></div></div>
|
||||
<div style="margin-top: 20px;" class="label-upper">Step by step</div>
|
||||
${this.signFields.map((field, index) => {
|
||||
const filled = this.signedFieldIds.includes(field.id);
|
||||
const active = this.activeFieldId === field.id && !filled;
|
||||
return html`<div class="recipient-line ${active ? 'active' : ''}" @click=${() => !filled ? this.activeFieldId = field.id : undefined}><span class="avatar" style="width: 22px; height: 22px; background: ${filled ? 'var(--success)' : active ? 'var(--accent)' : 'var(--bg-input)'}; color: ${filled || active ? 'white' : 'var(--text-muted)'};">${filled ? '✓' : index + 1}</span><div style="flex: 1;"><div style="font-size: 12px; font-weight: 500; text-decoration: ${filled ? 'line-through' : 'none'};">${field.label}</div><div class="mono" style="font-size: 10px; color: var(--text-muted);">${field.type} · page ${field.page}</div></div>${active ? icon('chevronRight', 12) : ''}</div>`;
|
||||
})}
|
||||
<button class="btn primary" style="width: 100%; height: 44px; margin-top: 20px;" @click=${() => this.signField(activeField.id)}>${completed === this.signFields.length ? 'Finish & submit' : `Continue - ${activeField.label}`}</button>
|
||||
<div style="margin-top: 12px; padding: 10px; font-size: 10px; color: var(--text-muted); line-height: 1.5; text-align: center; border-radius: 6px; background: var(--bg-el);">By signing, you agree to the ESIGN Act & eIDAS terms.<br /><span class="mono">IP 81.221.4.18 · Brussels, BE</span></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,440 @@
|
||||
import { html, css, type TemplateResult } from '@design.estate/dees-element';
|
||||
import '@design.estate/dees-catalog/ts_web/elements/00group-utility/dees-icon/dees-icon.js';
|
||||
|
||||
export type TWorkspaceView =
|
||||
| 'inbox'
|
||||
| 'compose'
|
||||
| 'sign'
|
||||
| 'audit'
|
||||
| 'developers'
|
||||
| 'templates'
|
||||
| 'team'
|
||||
| 'settings';
|
||||
|
||||
export type TWorkspaceTheme = 'dark' | 'light';
|
||||
export type TDensity = 'compact' | 'comfortable';
|
||||
|
||||
export interface IDocumentRow {
|
||||
id: string;
|
||||
title: string;
|
||||
status: 'awaiting' | 'signed' | 'draft' | 'declined';
|
||||
recipients: Array<{ name: string; initials: string; signed: boolean }>;
|
||||
updated: string;
|
||||
sender: string;
|
||||
pages: number;
|
||||
deadline?: string;
|
||||
}
|
||||
|
||||
export interface IRecipient {
|
||||
id: number;
|
||||
name: string;
|
||||
email: string;
|
||||
color: string;
|
||||
order: number;
|
||||
}
|
||||
|
||||
export interface IFieldPlacement {
|
||||
id: string;
|
||||
type: 'signature' | 'date' | 'text' | 'initials' | 'check';
|
||||
x: number;
|
||||
y: number;
|
||||
w: number;
|
||||
h: number;
|
||||
page: number;
|
||||
recipient: number;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export const demoDocuments: IDocumentRow[] = [
|
||||
{ id: 'doc_8mK3pL', title: 'Master Services Agreement - Acme Corp', status: 'awaiting', recipients: [{ name: 'Sarah Chen', initials: 'SC', signed: true }, { name: 'David Park', initials: 'DP', signed: false }, { name: 'You', initials: 'PK', signed: true }], updated: '2 min ago', sender: 'You', pages: 14, deadline: 'May 5' },
|
||||
{ id: 'doc_2nQ7vR', title: 'NDA - Helio Robotics', status: 'signed', recipients: [{ name: 'Marcus Tan', initials: 'MT', signed: true }, { name: 'You', initials: 'PK', signed: true }], updated: '1h ago', sender: 'You', pages: 3 },
|
||||
{ id: 'doc_5tH1zM', title: 'Series B Term Sheet (Lead) v3', status: 'awaiting', recipients: [{ name: 'Anna Lindqvist', initials: 'AL', signed: false }, { name: 'Roy Banerjee', initials: 'RB', signed: true }, { name: 'You', initials: 'PK', signed: false }], updated: '3h ago', sender: 'Sequoia Counsel', pages: 22, deadline: 'May 3' },
|
||||
{ id: 'doc_9wB4cX', title: 'Employment Offer - Mira Abebe', status: 'declined', recipients: [{ name: 'Mira Abebe', initials: 'MA', signed: false }, { name: 'You', initials: 'PK', signed: true }], updated: 'yesterday', sender: 'You', pages: 6 },
|
||||
{ id: 'doc_1jF6kY', title: 'Lease - Berlin office Q3', status: 'draft', recipients: [{ name: 'You', initials: 'PK', signed: false }], updated: 'yesterday', sender: 'You', pages: 11 },
|
||||
{ id: 'doc_4dN8sP', title: 'API Reseller Agreement - Northwind', status: 'signed', recipients: [{ name: 'Lila Brooks', initials: 'LB', signed: true }, { name: 'You', initials: 'PK', signed: true }], updated: '2 days ago', sender: 'You', pages: 8 },
|
||||
];
|
||||
|
||||
export const demoRecipients: IRecipient[] = [
|
||||
{ id: 0, name: 'Sarah Chen', email: 'sarah@acme.com', color: '#60a5fa', order: 1 },
|
||||
{ id: 1, name: 'David Park', email: 'd.park@acme.com', color: '#fbbf24', order: 2 },
|
||||
{ id: 2, name: 'Philipp K.', email: 'philipp@lossless.com', color: '#3b82f6', order: 3 },
|
||||
];
|
||||
|
||||
export const demoFields: IFieldPlacement[] = [
|
||||
{ id: 'f1', type: 'signature', x: 60, y: 580, w: 200, h: 50, page: 1, recipient: 0, label: 'Signature' },
|
||||
{ id: 'f2', type: 'date', x: 320, y: 580, w: 120, h: 30, page: 1, recipient: 0, label: 'Date' },
|
||||
{ id: 'f3', type: 'text', x: 60, y: 460, w: 280, h: 30, page: 1, recipient: 1, label: 'Full legal name' },
|
||||
{ id: 'f4', type: 'signature', x: 60, y: 700, w: 200, h: 50, page: 1, recipient: 1, label: 'Counter-signature' },
|
||||
];
|
||||
|
||||
export const workspaceBaseStyles = css`
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
color: var(--text);
|
||||
background: var(--bg);
|
||||
font-family: Geist, Inter, Roboto, -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
font-feature-settings: 'cv11', 'tnum', 'cv05' 1;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
button, input, textarea { font: inherit; }
|
||||
button { border: 0; cursor: pointer; }
|
||||
dees-icon { flex-shrink: 0; }
|
||||
|
||||
.mono {
|
||||
font-family: 'Intel One Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
height: 56px;
|
||||
flex-shrink: 0;
|
||||
padding: 0 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
background: var(--bg);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.top-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.top-title > span:first-child {
|
||||
font-family: 'Plus Jakarta Sans', Inter, sans-serif;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.02em;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.btn {
|
||||
height: 34px;
|
||||
padding: 0 14px;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
letter-spacing: -0.01em;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
white-space: nowrap;
|
||||
transition: all 0.12s ease;
|
||||
}
|
||||
|
||||
.btn.small {
|
||||
height: 28px;
|
||||
padding: 0 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.btn.primary {
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
border: 1px solid var(--accent);
|
||||
}
|
||||
|
||||
.btn.outline {
|
||||
background: transparent;
|
||||
color: var(--text);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.btn.ghost {
|
||||
background: transparent;
|
||||
color: var(--text);
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.btn:hover { background-color: var(--hover); }
|
||||
|
||||
.pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
background: var(--bg-el);
|
||||
color: var(--text-sec);
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pill::before {
|
||||
content: '';
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
display: none;
|
||||
border-radius: 50%;
|
||||
background: currentColor;
|
||||
}
|
||||
|
||||
.pill.dot::before { display: block; }
|
||||
.pill.success { background: rgba(34,197,94,0.12); color: #4ade80; }
|
||||
.pill.warning { background: rgba(245,158,11,0.12); color: #fbbf24; }
|
||||
.pill.error { background: rgba(239,68,68,0.12); color: #f87171; }
|
||||
.pill.info { background: rgba(59,130,246,0.12); color: #60a5fa; }
|
||||
|
||||
.content-scroll {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.label-upper {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: var(--text-dim);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.document-page {
|
||||
position: relative;
|
||||
width: 600px;
|
||||
min-height: 800px;
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.35), 0 0 0 1px rgba(255,255,255,0.05);
|
||||
color: hsl(0 0% 20%);
|
||||
}
|
||||
|
||||
.fake-document {
|
||||
padding: 48px 56px;
|
||||
font-size: 11px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.fake-title {
|
||||
font-family: 'Plus Jakarta Sans', Inter, sans-serif;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 4px;
|
||||
color: hsl(0 0% 10%);
|
||||
}
|
||||
|
||||
.fake-line {
|
||||
height: 6px;
|
||||
background: hsl(0 0% 82%);
|
||||
margin-bottom: 7px;
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
.fake-line.heavy { background: hsl(0 0% 65%); }
|
||||
.fake-line.short { width: 70%; }
|
||||
|
||||
.field-box {
|
||||
position: absolute;
|
||||
left: var(--x);
|
||||
top: var(--y);
|
||||
width: var(--w);
|
||||
height: var(--h);
|
||||
background: color-mix(in srgb, var(--field-color) 13%, transparent);
|
||||
border: 1.5px dashed var(--field-color);
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 0 8px;
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
color: var(--field-color);
|
||||
}
|
||||
|
||||
.field-box.selected {
|
||||
border-style: solid;
|
||||
box-shadow: 0 0 0 4px color-mix(in srgb, var(--field-color) 18%, transparent);
|
||||
}
|
||||
|
||||
.recipient-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 10px;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--text-sec);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.recipient-line.active {
|
||||
background: var(--hover);
|
||||
border-color: var(--border-strong);
|
||||
}
|
||||
|
||||
.progress-track {
|
||||
height: 4px;
|
||||
background: var(--bg-el);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: var(--accent);
|
||||
transition: width 0.4s ease;
|
||||
}
|
||||
|
||||
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }
|
||||
|
||||
@media (max-width: 920px) {
|
||||
.topbar { padding: 0 16px; }
|
||||
.actions { display: none; }
|
||||
.content-scroll { padding: 16px; }
|
||||
}
|
||||
`;
|
||||
|
||||
export function icon(name: string, size = 14): TemplateResult {
|
||||
const iconMap: Record<string, string> = {
|
||||
inbox: 'lucide:Inbox', plus: 'lucide:Plus', folder: 'lucide:Folder', shield: 'lucide:Shield', code: 'lucide:Code2',
|
||||
user: 'lucide:User', settings: 'lucide:Settings', upload: 'lucide:Upload', file: 'lucide:FileText', sign: 'lucide:PenTool',
|
||||
clock: 'lucide:Clock', search: 'lucide:Search', more: 'lucide:MoreHorizontal', send: 'lucide:Send', check: 'lucide:Check',
|
||||
eye: 'lucide:Eye', calendar: 'lucide:Calendar', type: 'lucide:Type', download: 'lucide:Download', hash: 'lucide:Hash',
|
||||
github: 'lucide:GitBranch', git: 'lucide:GitBranch', server: 'lucide:Server', star: 'lucide:Star', sparkle: 'lucide:Sparkles',
|
||||
chevronRight: 'lucide:ChevronRight', chevronDown: 'lucide:ChevronDown', x: 'lucide:X', activity: 'lucide:Activity',
|
||||
};
|
||||
return html`<dees-icon .icon=${iconMap[name] || iconMap.file} style="font-size: ${size}px;"></dees-icon>`;
|
||||
}
|
||||
|
||||
export function pill(label: string, tone: 'default' | 'success' | 'warning' | 'error' | 'info' = 'default', dot = false): TemplateResult {
|
||||
return html`<span class="pill ${tone} ${dot ? 'dot' : ''}">${label}</span>`;
|
||||
}
|
||||
|
||||
export function actionButton(label: string, variant: 'primary' | 'outline' | 'ghost' = 'outline', iconName?: string, onClick?: () => void): TemplateResult {
|
||||
return html`<button class="btn ${variant}" @click=${onClick || (() => undefined)}>${iconName ? icon(iconName, 13) : ''}${label}</button>`;
|
||||
}
|
||||
|
||||
export function topBar(config: { breadcrumb: string[]; title: string; subtitle?: TemplateResult; actions?: TemplateResult }): TemplateResult {
|
||||
return html`
|
||||
<div class="topbar">
|
||||
<div style="min-width: 0; flex: 1;">
|
||||
<div class="breadcrumb">
|
||||
${config.breadcrumb.map((part, index) => html`${index > 0 ? icon('chevronRight', 10) : ''}<span>${part}</span>`)}
|
||||
</div>
|
||||
<div class="top-title"><span>${config.title}</span>${config.subtitle || ''}</div>
|
||||
</div>
|
||||
<div class="actions">${config.actions || ''}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
export function workspaceDemoFrame(content: TemplateResult, theme: TWorkspaceTheme = 'dark'): TemplateResult {
|
||||
const darkVars = `
|
||||
--accent: #3b82f6;
|
||||
--bg: hsl(0 0% 3.9%);
|
||||
--bg-el: hsl(0 0% 6%);
|
||||
--bg-card: hsl(0 0% 7%);
|
||||
--bg-input: hsl(0 0% 9%);
|
||||
--border: hsl(0 0% 14.9%);
|
||||
--border-subtle: hsl(0 0% 11%);
|
||||
--border-strong: hsl(0 0% 20%);
|
||||
--text: hsl(0 0% 98%);
|
||||
--text-sec: hsl(0 0% 63.9%);
|
||||
--text-muted: hsl(0 0% 48%);
|
||||
--text-dim: hsl(0 0% 32%);
|
||||
--hover: rgba(255,255,255,0.06);
|
||||
--hover-subtle: rgba(255,255,255,0.03);
|
||||
--row-hover: rgba(255,255,255,0.025);
|
||||
--success: #22c55e;
|
||||
--warning: #f59e0b;
|
||||
--error: #ef4444;
|
||||
`;
|
||||
const lightVars = `
|
||||
--accent: #3b82f6;
|
||||
--bg: hsl(0 0% 99%);
|
||||
--bg-el: hsl(0 0% 97%);
|
||||
--bg-card: hsl(0 0% 100%);
|
||||
--bg-input: hsl(0 0% 98%);
|
||||
--border: hsl(0 0% 90%);
|
||||
--border-subtle: hsl(0 0% 93%);
|
||||
--border-strong: hsl(0 0% 80%);
|
||||
--text: hsl(0 0% 9%);
|
||||
--text-sec: hsl(0 0% 32%);
|
||||
--text-muted: hsl(0 0% 45%);
|
||||
--text-dim: hsl(0 0% 62%);
|
||||
--hover: rgba(0,0,0,0.04);
|
||||
--hover-subtle: rgba(0,0,0,0.02);
|
||||
--row-hover: rgba(0,0,0,0.02);
|
||||
--success: #16a34a;
|
||||
--warning: #d97706;
|
||||
--error: #dc2626;
|
||||
`;
|
||||
|
||||
return html`<div style="${theme === 'dark' ? darkVars : lightVars} height: 720px; min-height: 720px; background: var(--bg); color: var(--text); overflow: hidden; font-family: Geist, Inter, Roboto, -apple-system, BlinkMacSystemFont, sans-serif;">${content}</div>`;
|
||||
}
|
||||
|
||||
export function fakeDocument(): TemplateResult {
|
||||
return html`
|
||||
<div class="fake-document">
|
||||
<div class="fake-title">Master Services Agreement</div>
|
||||
<div class="mono" style="font-size: 10px; color: hsl(0 0% 45%); margin-bottom: 24px;">Effective: May 2, 2026 · Acme Corp ↔ Lossless GmbH</div>
|
||||
${Array.from({ length: 18 }).map((_, index) => html`<div class="fake-line ${index % 5 === 0 ? 'heavy' : ''} ${index % 4 === 3 ? 'short' : ''}"></div>`)}
|
||||
<div style="height: 16px;"></div>
|
||||
${Array.from({ length: 8 }).map((_, index) => html`<div class="fake-line ${index % 3 === 2 ? 'short' : ''}"></div>`)}
|
||||
<div style="margin-top: 60px; font-size: 10px; font-weight: 600; color: hsl(0 0% 35%);">SIGNED ON BEHALF OF ACME CORP</div>
|
||||
<div style="margin-top: 70px; font-size: 10px; font-weight: 600; color: hsl(0 0% 35%);">SIGNED ON BEHALF OF LOSSLESS GMBH</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
export function requestWorkspaceView(element: HTMLElement, view: TWorkspaceView) {
|
||||
element.dispatchEvent(new CustomEvent('workspace-view-request', {
|
||||
detail: { view },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}));
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
import { DeesElement, property, state, html, customElement, type TemplateResult, css } from '@design.estate/dees-element';
|
||||
import { icon, type TDensity, type TWorkspaceTheme, type TWorkspaceView } from './sdig-workspace.shared.js';
|
||||
import './sdig-workspace-inbox.js';
|
||||
import './sdig-workspace-compose.js';
|
||||
import './sdig-workspace-sign.js';
|
||||
import './sdig-workspace-audit.js';
|
||||
import './sdig-workspace-developers.js';
|
||||
import './sdig-workspace-placeholder.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'sdig-workspace': SdigWorkspace;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('sdig-workspace')
|
||||
export class SdigWorkspace extends DeesElement {
|
||||
public static demo = () => html`<sdig-workspace></sdig-workspace>`;
|
||||
public static demoGroups = ['Signature Digital Workspace'];
|
||||
|
||||
@property({ type: String }) public accessor accent: string = '#3b82f6';
|
||||
@property({ type: String }) public accessor density: TDensity = 'comfortable';
|
||||
@property({ type: String, reflect: true }) public accessor theme: TWorkspaceTheme = 'dark';
|
||||
@property({ type: String }) public accessor initialView: TWorkspaceView = 'inbox';
|
||||
@state() private accessor view: TWorkspaceView = 'inbox';
|
||||
|
||||
public connectedCallback = async () => {
|
||||
await super.connectedCallback();
|
||||
this.view = this.initialView || 'inbox';
|
||||
this.addEventListener('workspace-view-request', this.handleViewRequest as EventListener);
|
||||
};
|
||||
|
||||
public disconnectedCallback = async () => {
|
||||
this.removeEventListener('workspace-view-request', this.handleViewRequest as EventListener);
|
||||
await super.disconnectedCallback();
|
||||
};
|
||||
|
||||
public static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 720px;
|
||||
--accent: #3b82f6;
|
||||
--bg: hsl(0 0% 3.9%);
|
||||
--bg-el: hsl(0 0% 6%);
|
||||
--bg-card: hsl(0 0% 7%);
|
||||
--bg-input: hsl(0 0% 9%);
|
||||
--border: hsl(0 0% 14.9%);
|
||||
--border-subtle: hsl(0 0% 11%);
|
||||
--border-strong: hsl(0 0% 20%);
|
||||
--text: hsl(0 0% 98%);
|
||||
--text-sec: hsl(0 0% 63.9%);
|
||||
--text-muted: hsl(0 0% 48%);
|
||||
--text-dim: hsl(0 0% 32%);
|
||||
--hover: rgba(255,255,255,0.06);
|
||||
--hover-subtle: rgba(255,255,255,0.03);
|
||||
--row-hover: rgba(255,255,255,0.025);
|
||||
--success: #22c55e;
|
||||
--warning: #f59e0b;
|
||||
--error: #ef4444;
|
||||
font-family: Geist, Inter, Roboto, -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
}
|
||||
|
||||
:host([theme='light']) {
|
||||
--bg: hsl(0 0% 99%);
|
||||
--bg-el: hsl(0 0% 97%);
|
||||
--bg-card: hsl(0 0% 100%);
|
||||
--bg-input: hsl(0 0% 98%);
|
||||
--border: hsl(0 0% 90%);
|
||||
--border-subtle: hsl(0 0% 93%);
|
||||
--border-strong: hsl(0 0% 80%);
|
||||
--text: hsl(0 0% 9%);
|
||||
--text-sec: hsl(0 0% 32%);
|
||||
--text-muted: hsl(0 0% 45%);
|
||||
--text-dim: hsl(0 0% 62%);
|
||||
--hover: rgba(0,0,0,0.04);
|
||||
--hover-subtle: rgba(0,0,0,0.02);
|
||||
--row-hover: rgba(0,0,0,0.02);
|
||||
--success: #16a34a;
|
||||
--warning: #d97706;
|
||||
--error: #dc2626;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
button { font: inherit; border: 0; cursor: pointer; }
|
||||
.workspace { display: flex; height: 100%; min-height: 720px; background: var(--bg); color: var(--text); overflow: hidden; }
|
||||
.sidebar { width: 220px; background: var(--bg); border-right: 1px solid var(--border-subtle); display: flex; flex-direction: column; flex-shrink: 0; height: 100%; }
|
||||
.brand { padding: 14px 16px 12px; display: flex; align-items: center; gap: 8px; }
|
||||
.logomark { width: 26px; height: 26px; border-radius: 6px; background: var(--bg-el); border: 1px solid var(--border-strong); display: inline-flex; align-items: center; justify-content: center; font-family: 'Plus Jakarta Sans', Inter, sans-serif; font-size: 13px; font-weight: 700; position: relative; }
|
||||
.logomark::after, .wordmark::after { content: ''; display: inline-block; border-radius: 50%; background: var(--accent); }
|
||||
.logomark::after { width: 4px; height: 4px; position: absolute; right: 5px; bottom: 5px; }
|
||||
.wordmark { font-size: 13px; font-weight: 500; letter-spacing: -0.02em; white-space: nowrap; }
|
||||
.wordmark .dot { color: var(--text-muted); }
|
||||
.wordmark::after { width: 4px; height: 4px; margin-left: 3px; transform: translateY(-1px); }
|
||||
.workspace-card { margin: 0 12px 8px; padding: 7px 10px; background: var(--bg-el); border: 1px solid var(--border-subtle); border-radius: 6px; display: flex; align-items: center; gap: 8px; }
|
||||
.workspace-badge { width: 18px; height: 18px; border-radius: 4px; background: linear-gradient(135deg, var(--accent), hsl(280 70% 60%)); color: white; font-size: 10px; font-weight: 700; display: flex; align-items: center; justify-content: center; }
|
||||
.workspace-name { font-size: 12px; font-weight: 500; line-height: 1.2; }
|
||||
.workspace-plan { font-size: 10px; color: var(--text-muted); }
|
||||
.nav-block { padding: 4px 0; }
|
||||
.nav-item { display: flex; align-items: center; gap: 10px; padding: 7px 10px; margin: 1px 8px; border-radius: 6px; color: var(--text-muted); background: transparent; transition: all 0.1s ease; font-size: 13px; position: relative; width: calc(100% - 16px); text-align: left; }
|
||||
.compact .nav-item { padding: 5px 10px; }
|
||||
.nav-item:hover { background: var(--hover-subtle); color: var(--text-sec); }
|
||||
.nav-item.active { background: var(--hover); color: var(--text); font-weight: 500; }
|
||||
.nav-item.active::before { content: ''; position: absolute; left: -8px; width: 2px; height: 14px; border-radius: 2px; background: var(--accent); }
|
||||
.nav-count { margin-left: auto; min-width: 18px; padding: 1px 6px; border-radius: 999px; background: var(--bg-el); color: var(--text-muted); font-size: 10px; text-align: center; }
|
||||
.github-card { margin: 8px 12px; padding: 10px; border: 1px solid var(--border-subtle); border-radius: 6px; background: var(--bg-el); }
|
||||
.sparkline { margin-top: 8px; display: flex; gap: 2px; align-items: flex-end; height: 18px; }
|
||||
.sparkline span { flex: 1; background: var(--border-strong); border-radius: 1px; }
|
||||
.sparkline span:nth-last-child(-n+4) { background: var(--accent); }
|
||||
.user-card { padding: 8px 12px; border-top: 1px solid var(--border-subtle); display: flex; align-items: center; gap: 10px; }
|
||||
.avatar { width: 26px; height: 26px; border-radius: 50%; background: var(--accent); color: white; font-size: 11px; font-weight: 700; display: flex; align-items: center; justify-content: center; }
|
||||
.main { flex: 1; min-width: 0; display: flex; flex-direction: column; overflow: hidden; }
|
||||
.view-host { flex: 1; min-height: 0; display: flex; flex-direction: column; }
|
||||
.statusbar { height: 24px; flex-shrink: 0; border-top: 1px solid var(--border-subtle); background: var(--bg); display: flex; align-items: center; padding: 0 16px; gap: 16px; font-size: 10px; color: var(--text-dim); font-family: 'Intel One Mono', ui-monospace, SFMono-Regular, Menlo, monospace; }
|
||||
@media (max-width: 920px) { .workspace { flex-direction: column; min-height: 100vh; } .sidebar { width: 100%; height: auto; border-right: 0; border-bottom: 1px solid var(--border-subtle); } .brand, .workspace-card, .github-card, .user-card { display: none; } .nav-block { display: flex; overflow-x: auto; padding: 8px; } .nav-item { width: auto; margin: 0 2px; } .statusbar { display: none; } }
|
||||
`;
|
||||
|
||||
private handleViewRequest = (event: CustomEvent<{ view: TWorkspaceView }>) => {
|
||||
this.setView(event.detail.view);
|
||||
};
|
||||
|
||||
private setView(viewArg: TWorkspaceView) {
|
||||
this.view = viewArg;
|
||||
this.dispatchEvent(new CustomEvent('view-change', { detail: { view: viewArg }, bubbles: true, composed: true }));
|
||||
}
|
||||
|
||||
private navButton(item: { id: TWorkspaceView; label: string; icon: string; count?: number }): TemplateResult {
|
||||
return html`<button class="nav-item ${this.view === item.id ? 'active' : ''}" @click=${() => this.setView(item.id)}>${icon(item.icon, 15)}<span>${item.label}</span>${item.count !== undefined ? html`<span class="nav-count">${item.count}</span>` : ''}</button>`;
|
||||
}
|
||||
|
||||
private renderSidebar(): TemplateResult {
|
||||
const navItems = [
|
||||
{ id: 'inbox', label: 'Inbox', icon: 'inbox', count: 4 },
|
||||
{ id: 'compose', label: 'Compose', icon: 'plus' },
|
||||
{ id: 'templates', label: 'Templates', icon: 'folder', count: 12 },
|
||||
{ id: 'audit', label: 'Audit Trail', icon: 'shield' },
|
||||
{ id: 'developers', label: 'Developers', icon: 'code' },
|
||||
] as Array<{ id: TWorkspaceView; label: string; icon: string; count?: number }>;
|
||||
const lowerItems = [
|
||||
{ id: 'team', label: 'Team', icon: 'user' },
|
||||
{ id: 'settings', label: 'Settings', icon: 'settings' },
|
||||
] as Array<{ id: TWorkspaceView; label: string; icon: string }>;
|
||||
|
||||
return html`
|
||||
<aside class="sidebar">
|
||||
<div class="brand"><span class="logomark">s</span><span class="wordmark">signature<span class="dot">.</span>digital</span></div>
|
||||
<div class="workspace-card"><span class="workspace-badge">L</span><div style="flex: 1; min-width: 0;"><div class="workspace-name">Lossless GmbH</div><div class="workspace-plan">Cloud · Pro</div></div>${icon('chevronDown', 12)}</div>
|
||||
<div class="nav-block">${navItems.map((item) => this.navButton(item))}</div>
|
||||
<div style="flex: 1;"></div>
|
||||
<div class="github-card"><div style="display: flex; align-items: center; gap: 6px; margin-bottom: 8px; font-size: 11px; color: var(--text-sec); font-family: 'Intel One Mono', ui-monospace;">${icon('github', 13)} signature-digital/core</div><div style="display: flex; gap: 12px; font-size: 11px; color: var(--text-muted);"><span>${icon('star', 11)} 8.2k</span><span>${icon('git', 11)} 248</span></div><div class="sparkline">${[3, 5, 2, 7, 4, 6, 8, 5, 9, 6, 4, 8, 7, 10].map((height) => html`<span style="height: ${height * 10}%"></span>`)}</div></div>
|
||||
<div class="nav-block" style="border-top: 1px solid var(--border-subtle); padding-top: 8px;">${lowerItems.map((item) => this.navButton(item))}</div>
|
||||
<div class="user-card"><span class="avatar">PK</span><div style="flex: 1; min-width: 0;"><div style="font-size: 12px; font-weight: 500;">Philipp K.</div><div style="font-family: 'Intel One Mono', ui-monospace; font-size: 10px; color: var(--text-muted); overflow: hidden; text-overflow: ellipsis;">philipp@lossless.com</div></div>${icon('more', 14)}</div>
|
||||
</aside>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderView(): TemplateResult {
|
||||
switch (this.view) {
|
||||
case 'inbox': return html`<sdig-workspace-inbox class="view-host" .density=${this.density}></sdig-workspace-inbox>`;
|
||||
case 'compose': return html`<sdig-workspace-compose class="view-host"></sdig-workspace-compose>`;
|
||||
case 'sign': return html`<sdig-workspace-sign class="view-host"></sdig-workspace-sign>`;
|
||||
case 'audit': return html`<sdig-workspace-audit class="view-host"></sdig-workspace-audit>`;
|
||||
case 'developers': return html`<sdig-workspace-developers class="view-host"></sdig-workspace-developers>`;
|
||||
case 'templates': return html`<sdig-workspace-placeholder class="view-host" label="Templates" subtitle="Reusable agreement templates"></sdig-workspace-placeholder>`;
|
||||
case 'team': return html`<sdig-workspace-placeholder class="view-host" label="Team" subtitle="Workspace members & roles"></sdig-workspace-placeholder>`;
|
||||
case 'settings': return html`<sdig-workspace-placeholder class="view-host" label="Settings" subtitle="Workspace, billing, security"></sdig-workspace-placeholder>`;
|
||||
default: return html`<sdig-workspace-inbox class="view-host" .density=${this.density}></sdig-workspace-inbox>`;
|
||||
}
|
||||
}
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`<div class="workspace ${this.density === 'compact' ? 'compact' : ''}" style="--accent: ${this.accent};" data-screen-label=${this.view}>${this.renderSidebar()}<main class="main">${this.renderView()}<div class="statusbar"><span style="display: inline-flex; align-items: center; gap: 5px;"><span style="width: 6px; height: 6px; border-radius: 50%; background: var(--success);"></span>api.signature.digital</span><span>eu-central-1</span><span>4 sigs queued</span><div style="flex: 1;"></div><span style="color: var(--accent);">Open Source · MIT</span><span>v0.42.1</span><span>${icon('git', 11)} main</span></div></main></div>`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// third party
|
||||
import signaturePadMod from 'signature_pad';
|
||||
type signaturePadType = (typeof import('signature_pad'))['default'];
|
||||
const signaturePad = signaturePadMod as any as signaturePadType;
|
||||
|
||||
export {
|
||||
signaturePad,
|
||||
}
|
||||
+3
-3
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"experimentalDecorators": true,
|
||||
"useDefineForClassFields": false,
|
||||
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"esModuleInterop": true,
|
||||
"verbatimModuleSyntax": true
|
||||
"verbatimModuleSyntax": true,
|
||||
"types": ["node"]
|
||||
},
|
||||
"exclude": [
|
||||
"dist_*/**/*.d.ts"
|
||||
|
||||
Reference in New Issue
Block a user